kubernetes-kubernetes-40e1192/000077500000000000000000000000001504711711200162745ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/.generated_files000066400000000000000000000013561504711711200214220ustar00rootroot00000000000000# Files that should be ignored by tools which do not want to consider generated # code. # # https://github.com/kubernetes/test-infra/blob/master/prow/plugins/size/size.go # # This file is a series of lines, each of the form: # # # Type can be: # path - an exact path to a single file # file-name - an exact leaf filename, regardless of path # path-prefix - a prefix match on the file path # file-prefix - a prefix match of the leaf filename (no path) # paths-from-repo - read a file from the repo and load file paths # file-prefix zz_generated. file-name types.generated.go file-name generated.pb.go file-name generated.proto file-name types_swagger_doc_generated.go path-prefix vendor/ path-prefix pkg/generated/ kubernetes-kubernetes-40e1192/.gitattributes000066400000000000000000000007761504711711200212010ustar00rootroot00000000000000# Always check-out / check-in files with LF line endings. * text=auto eol=lf hack/verify-flags/known-flags.txt merge=union test/test_owners.csv merge=union **/zz_generated.*.go linguist-generated=true **/types.generated.go linguist-generated=true **/generated.pb.go linguist-generated=true **/generated.proto **/types_swagger_doc_generated.go linguist-generated=true api/openapi-spec/*.json linguist-generated=true api/openapi-spec/**/*.json linguist-generated=true staging/**/go.sum linguist-generated=true kubernetes-kubernetes-40e1192/.github/000077500000000000000000000000001504711711200176345ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/.github/ISSUE_TEMPLATE/000077500000000000000000000000001504711711200220175ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/.github/ISSUE_TEMPLATE/bug-report.yaml000066400000000000000000000042441504711711200247750ustar00rootroot00000000000000name: Bug Report description: Report a bug encountered while operating Kubernetes labels: kind/bug body: - type: textarea id: problem attributes: label: What happened? description: | Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. If this matter is security related, please disclose it privately via https://kubernetes.io/security validations: required: true - type: textarea id: expected attributes: label: What did you expect to happen? validations: required: true - type: textarea id: repro attributes: label: How can we reproduce it (as minimally and precisely as possible)? validations: required: true - type: textarea id: additional attributes: label: Anything else we need to know? - type: textarea id: kubeVersion attributes: label: Kubernetes version value: |
```console $ kubectl version # paste output here ```
validations: required: true - type: textarea id: cloudProvider attributes: label: Cloud provider value: |
validations: required: true - type: textarea id: osVersion attributes: label: OS version value: |
```console # On Linux: $ cat /etc/os-release # paste output here $ uname -a # paste output here # On Windows: C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture # paste output here ```
- type: textarea id: installer attributes: label: Install tools value: |
- type: textarea id: runtime attributes: label: Container runtime (CRI) and version (if applicable) value: |
- type: textarea id: plugins attributes: label: Related plugins (CNI, CSI, ...) and versions (if applicable) value: |
kubernetes-kubernetes-40e1192/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002161504711711200240060ustar00rootroot00000000000000contact_links: - name: Support Request url: https://discuss.kubernetes.io about: Support request or question relating to Kubernetes kubernetes-kubernetes-40e1192/.github/ISSUE_TEMPLATE/enhancement.yaml000066400000000000000000000013561504711711200251750ustar00rootroot00000000000000name: Enhancement Tracking Issue description: Provide supporting details for a feature in development labels: kind/feature body: - type: textarea id: feature attributes: label: What would you like to be added? description: | Feature requests are unlikely to make progress as issues. Please consider engaging with SIGs on slack and mailing lists, instead. A proposal that works through the design along with the implications of the change can be opened as a KEP. See https://git.k8s.io/enhancements/keps#kubernetes-enhancement-proposals-keps validations: required: true - type: textarea id: rationale attributes: label: Why is this needed? validations: required: true kubernetes-kubernetes-40e1192/.github/ISSUE_TEMPLATE/failing-test.yaml000066400000000000000000000021501504711711200252670ustar00rootroot00000000000000name: Failing Test description: Report continuously failing tests or jobs in Kubernetes CI labels: kind/failing-test body: - type: textarea id: jobs attributes: label: Which jobs are failing? placeholder: | Please only use this template for submitting reports about continuously failing tests or jobs in Kubernetes CI. validations: required: true - type: textarea id: tests attributes: label: Which tests are failing? validations: required: true - type: textarea id: since attributes: label: Since when has it been failing? validations: required: true - type: input id: testgrid attributes: label: Testgrid link - type: textarea id: reason attributes: label: Reason for failure (if possible) - type: textarea id: additional attributes: label: Anything else we need to know? - type: textarea id: sigs attributes: label: Relevant SIG(s) description: You can identify the SIG from the "prowjob_config_url" on the testgrid dashboard for a test. value: /sig kubernetes-kubernetes-40e1192/.github/ISSUE_TEMPLATE/flaking-test.yaml000066400000000000000000000025611504711711200252770ustar00rootroot00000000000000name: Flaking Test description: Report flaky tests or jobs in Kubernetes CI labels: kind/flake body: - type: textarea id: jobs attributes: label: Which jobs are flaking? description: | Please only use this template for submitting reports about flaky tests or jobs (pass or fail with no underlying change in code) in Kubernetes CI. Links to go.k8s.io/triage and/or links to specific failures in spyglass are appreciated. Please see the deflaking doc (https://github.com/kubernetes/community/blob/master/contributors/devel/sig-testing/flaky-tests.md) for more guidance. validations: required: true - type: textarea id: tests attributes: label: Which tests are flaking? validations: required: true - type: textarea id: since attributes: label: Since when has it been flaking? validations: required: true - type: input id: testgrid attributes: label: Testgrid link - type: textarea id: reason attributes: label: Reason for failure (if possible) - type: textarea id: additional attributes: label: Anything else we need to know? - type: textarea id: sigs attributes: label: Relevant SIG(s) description: You can identify the SIG from the "prowjob_config_url" on the testgrid dashboard for a test. value: /sig kubernetes-kubernetes-40e1192/.github/OWNERS000066400000000000000000000007021504711711200205730ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners options: # make root approval non-recursive no_parent_owners: true reviewers: - alisondy - cblecker - guineveresaenger - mrbobbytables - nikhita - parispittman - palnabarun - kaslin - MadhavJivrajani - Priyankasaggu11929 approvers: - sig-contributor-experience-approvers - parispittman emeritus_approvers: - castrojo - Phillels labels: - sig/contributor-experience kubernetes-kubernetes-40e1192/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000055321504711711200234420ustar00rootroot00000000000000 #### What type of PR is this? #### What this PR does / why we need it: #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note ``` #### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.: ```docs ``` kubernetes-kubernetes-40e1192/.github/SECURITY.md000066400000000000000000000011501504711711200214220ustar00rootroot00000000000000# Security Policy ## Supported Versions Information about supported Kubernetes versions can be found on the [Kubernetes version and version skew support policy] page on the Kubernetes website. ## Reporting a Vulnerability Instructions for reporting a vulnerability can be found on the [Kubernetes Security and Disclosure Information] page. [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability kubernetes-kubernetes-40e1192/.gitignore000066400000000000000000000041071504711711200202660ustar00rootroot00000000000000# OSX leaves these everywhere on SMB shares ._* # OSX trash .DS_Store # Developers can store local stuff in dirs named __something __* # Eclipse files .classpath .project .settings/** # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA .idea/ *.iml # Vscode files .vscode # This is where the result of the go build goes /output*/ /_output*/ /_output # Emacs save files *~ \#*\# .\#* # Vim-related files [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist # cscope-related files cscope.* # Go test binaries *.test /hack/.test-cmd-auth # JUnit test output from ginkgo e2e tests /junit*.xml # Mercurial files **/.hg **/.hg* # Vagrant .vagrant network_closure.sh # Local cluster env variables /cluster/env.sh # Compiled binaries in third_party /third_party/pkg # Also ignore etcd installed by hack/install-etcd.sh /third_party/etcd* /default.etcd # Also ignore protoc installed by hack/install-protoc.sh /third_party/protoc* # User cluster configs .kubeconfig .tags* # Version file for dockerized build .dockerized-kube-version-defs # Web UI /www/master/node_modules/ /www/master/npm-debug.log /www/master/shared/config/development.json # Karma output /www/test_out # precommit temporary directories created by ./hack/verify-generated-docs.sh and ./hack/lib/util.sh /_tmp/ /doc_tmp/ # Test artifacts produced by Prow/kubetest2 jobs /_artifacts/ /_rundir/ # Go dependencies installed on Jenkins /_gopath/ # Config directories created by gcloud and gsutil on Jenkins /.config/gcloud*/ /.gsutil/ # CoreOS stuff /cluster/libvirt-coreos/coreos_*.img # Downloaded Kubernetes binary release /kubernetes/ # direnv .envrc files .envrc # Downloaded kubernetes binary release tar ball kubernetes.tar.gz # Phony test files used as part of coverage generation zz_generated_*_test.go # Just in time generated data in the source, should never be committed /test/e2e/generated/bindata.go # This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored !\.drone\.sec /bazel-* *.pyc # generated by verify-vendor.sh vendordiff.patch kubernetes-kubernetes-40e1192/.go-version000066400000000000000000000000071504711711200203620ustar00rootroot000000000000001.24.5 kubernetes-kubernetes-40e1192/CHANGELOG.md000077700000000000000000000000001504711711200226462CHANGELOG/README.mdustar00rootroot00000000000000kubernetes-kubernetes-40e1192/CHANGELOG/000077500000000000000000000000001504711711200175635ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/CHANGELOG/CHANGELOG-1.33.md000066400000000000000000010636631504711711200217750ustar00rootroot00000000000000 - [v1.33.3](#v1333) - [Downloads for v1.33.3](#downloads-for-v1333) - [Source Code](#source-code) - [Client Binaries](#client-binaries) - [Server Binaries](#server-binaries) - [Node Binaries](#node-binaries) - [Container Images](#container-images) - [Changelog since v1.33.2](#changelog-since-v1332) - [Changes by Kind](#changes-by-kind) - [Bug or Regression](#bug-or-regression) - [Other (Cleanup or Flake)](#other-cleanup-or-flake) - [Dependencies](#dependencies) - [Added](#added) - [Changed](#changed) - [Removed](#removed) - [v1.33.2](#v1332) - [Downloads for v1.33.2](#downloads-for-v1332) - [Source Code](#source-code-1) - [Client Binaries](#client-binaries-1) - [Server Binaries](#server-binaries-1) - [Node Binaries](#node-binaries-1) - [Container Images](#container-images-1) - [Changelog since v1.33.1](#changelog-since-v1331) - [Important Security Information](#important-security-information) - [CVE-2025-4563: Nodes can bypass dynamic resource allocation authorization checks](#cve-2025-4563-nodes-can-bypass-dynamic-resource-allocation-authorization-checks) - [Changes by Kind](#changes-by-kind-1) - [Feature](#feature) - [Bug or Regression](#bug-or-regression-1) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-1) - [Dependencies](#dependencies-1) - [Added](#added-1) - [Changed](#changed-1) - [Removed](#removed-1) - [v1.33.1](#v1331) - [Downloads for v1.33.1](#downloads-for-v1331) - [Source Code](#source-code-2) - [Client Binaries](#client-binaries-2) - [Server Binaries](#server-binaries-2) - [Node Binaries](#node-binaries-2) - [Container Images](#container-images-2) - [Changelog since v1.33.0](#changelog-since-v1330) - [Changes by Kind](#changes-by-kind-2) - [Bug or Regression](#bug-or-regression-2) - [Dependencies](#dependencies-2) - [Added](#added-2) - [Changed](#changed-2) - [Removed](#removed-2) - [v1.33.0](#v1330) - [Downloads for v1.33.0](#downloads-for-v1330) - [Source Code](#source-code-3) - [Client Binaries](#client-binaries-3) - [Server Binaries](#server-binaries-3) - [Node Binaries](#node-binaries-3) - [Container Images](#container-images-3) - [Changelog since v1.32.0](#changelog-since-v1320) - [Urgent Upgrade Notes](#urgent-upgrade-notes) - [(No, really, you MUST read this before you upgrade)](#no-really-you-must-read-this-before-you-upgrade) - [Changes by Kind](#changes-by-kind-3) - [Deprecation](#deprecation) - [API Change](#api-change) - [Feature](#feature-1) - [Documentation](#documentation) - [Bug or Regression](#bug-or-regression-3) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-2) - [Dependencies](#dependencies-3) - [Added](#added-3) - [Changed](#changed-3) - [Removed](#removed-3) - [v1.33.0-rc.1](#v1330-rc1) - [Downloads for v1.33.0-rc.1](#downloads-for-v1330-rc1) - [Source Code](#source-code-4) - [Client Binaries](#client-binaries-4) - [Server Binaries](#server-binaries-4) - [Node Binaries](#node-binaries-4) - [Container Images](#container-images-4) - [Changelog since v1.33.0-rc.0](#changelog-since-v1330-rc0) - [Changes by Kind](#changes-by-kind-4) - [Bug or Regression](#bug-or-regression-4) - [Dependencies](#dependencies-4) - [Added](#added-4) - [Changed](#changed-4) - [Removed](#removed-4) - [v1.33.0-rc.0](#v1330-rc0) - [Downloads for v1.33.0-rc.0](#downloads-for-v1330-rc0) - [Source Code](#source-code-5) - [Client Binaries](#client-binaries-5) - [Server Binaries](#server-binaries-5) - [Node Binaries](#node-binaries-5) - [Container Images](#container-images-5) - [Changelog since v1.33.0-beta.0](#changelog-since-v1330-beta0) - [Urgent Upgrade Notes](#urgent-upgrade-notes-1) - [(No, really, you MUST read this before you upgrade)](#no-really-you-must-read-this-before-you-upgrade-1) - [Changes by Kind](#changes-by-kind-5) - [Deprecation](#deprecation-1) - [API Change](#api-change-1) - [Feature](#feature-2) - [Bug or Regression](#bug-or-regression-5) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-3) - [Dependencies](#dependencies-5) - [Added](#added-5) - [Changed](#changed-5) - [Removed](#removed-5) - [v1.33.0-beta.0](#v1330-beta0) - [Downloads for v1.33.0-beta.0](#downloads-for-v1330-beta0) - [Source Code](#source-code-6) - [Client Binaries](#client-binaries-6) - [Server Binaries](#server-binaries-6) - [Node Binaries](#node-binaries-6) - [Container Images](#container-images-6) - [Changelog since v1.33.0-alpha.3](#changelog-since-v1330-alpha3) - [Changes by Kind](#changes-by-kind-6) - [API Change](#api-change-2) - [Feature](#feature-3) - [Bug or Regression](#bug-or-regression-6) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-4) - [Dependencies](#dependencies-6) - [Added](#added-6) - [Changed](#changed-6) - [Removed](#removed-6) - [v1.33.0-alpha.3](#v1330-alpha3) - [Downloads for v1.33.0-alpha.3](#downloads-for-v1330-alpha3) - [Source Code](#source-code-7) - [Client Binaries](#client-binaries-7) - [Server Binaries](#server-binaries-7) - [Node Binaries](#node-binaries-7) - [Container Images](#container-images-7) - [Changelog since v1.33.0-alpha.2](#changelog-since-v1330-alpha2) - [Urgent Upgrade Notes](#urgent-upgrade-notes-2) - [(No, really, you MUST read this before you upgrade)](#no-really-you-must-read-this-before-you-upgrade-2) - [Changes by Kind](#changes-by-kind-7) - [Deprecation](#deprecation-2) - [API Change](#api-change-3) - [Feature](#feature-4) - [Bug or Regression](#bug-or-regression-7) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-5) - [Dependencies](#dependencies-7) - [Added](#added-7) - [Changed](#changed-7) - [Removed](#removed-7) - [v1.33.0-alpha.2](#v1330-alpha2) - [Downloads for v1.33.0-alpha.2](#downloads-for-v1330-alpha2) - [Source Code](#source-code-8) - [Client Binaries](#client-binaries-8) - [Server Binaries](#server-binaries-8) - [Node Binaries](#node-binaries-8) - [Container Images](#container-images-8) - [Changelog since v1.33.0-alpha.1](#changelog-since-v1330-alpha1) - [Changes by Kind](#changes-by-kind-8) - [Deprecation](#deprecation-3) - [API Change](#api-change-4) - [Feature](#feature-5) - [Bug or Regression](#bug-or-regression-8) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-6) - [Dependencies](#dependencies-8) - [Added](#added-8) - [Changed](#changed-8) - [Removed](#removed-8) - [v1.33.0-alpha.1](#v1330-alpha1) - [Downloads for v1.33.0-alpha.1](#downloads-for-v1330-alpha1) - [Source Code](#source-code-9) - [Client Binaries](#client-binaries-9) - [Server Binaries](#server-binaries-9) - [Node Binaries](#node-binaries-9) - [Container Images](#container-images-9) - [Changelog since v1.32.0](#changelog-since-v1320-1) - [Urgent Upgrade Notes](#urgent-upgrade-notes-3) - [(No, really, you MUST read this before you upgrade)](#no-really-you-must-read-this-before-you-upgrade-3) - [Changes by Kind](#changes-by-kind-9) - [API Change](#api-change-5) - [Feature](#feature-6) - [Documentation](#documentation-1) - [Bug or Regression](#bug-or-regression-9) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-7) - [Dependencies](#dependencies-9) - [Added](#added-9) - [Changed](#changed-9) - [Removed](#removed-9) # v1.33.3 ## Downloads for v1.33.3 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes.tar.gz) | 363c52cddaec8b16d6fa00382446907db5d4df262c4ceda293bdcae3bc8033ebe662c4c32fa3f1f66e815b9a4c865ffe93f662f814c10b702359be692c00acfb [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-src.tar.gz) | d23bdc69123f4975a151224c450cbeadc97895f7645563daea67e01915549ea3fb5b31237598abed4fbe5add3c77ffd92e95cbe3f635cf2f4c0626a704f15fca ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-darwin-amd64.tar.gz) | 58fc38f9f7c8952d318ad79139310588e077d2efd5100b586079cbee1cf04211b91d035a897164283bfb792b497139b143dd8bea63b3b538eaa346fb9e9f0379 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-darwin-arm64.tar.gz) | 15adffb9517df740e806698db5c0e973b8a765ef1e999a94e7f60d3598b9fba3b1299b95b5cccb765d94688cd15e153c4a84f4c4f039c45504fd7d3f44e395a2 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-linux-386.tar.gz) | 7cc1891ac0b230ab90e78cb7bad48e0d0ae4cafc88c8563a82de0f79c6d8dbb429bc5f96a540c84bd7334d2d3978d3e81d80949499c8ea6a66fc166cf9b9196c [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-linux-amd64.tar.gz) | d4ef8efe17406ca3234c4628b0b4c14214f77b42056bd7db8298b0ace78305cf641e250572726996437c08bbb298aa7f942c6e748d4293478d11426a42666103 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-linux-arm.tar.gz) | 056378073fc2dd46533202c7d2d8dd3468f07a5853497d220d33827f37959934e10c7e10218e86df99c0b4136935fbab6167dd10586b0ec82caebf7806b99d53 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-linux-arm64.tar.gz) | e5cbf3394c0cab0d4443ed3731bb8010c5e7170bc41fc6bb269f00281643b441491fe4bb121058da8d52d7c87dc32b764e8b3670944b3cd8a1239e3b36430247 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-linux-ppc64le.tar.gz) | 8f5dca8a7390d63f5793067a3900256a2378534683957b9f3ef1e74338f23da4c0466703dd2fe7c6761ded9c5efbd36114a32d8ebacfab52a7a986f29be41f30 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-linux-s390x.tar.gz) | fbc8eaa3e8bd85beb0ca02167ff17ca87fba073e55a8cc55f5595339a7cc33f068af81e4525ba196dbce52d0874de8c5beecad988ea41d9fae69b8740136a26e [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-windows-386.tar.gz) | f3b4d95f0399521d93765b891e49f0c2b57b0d62f59254684cd0495679909306acb07eb630460369bd1335a5c97e786c40bfa3d318cceda04f36d0039ef368eb [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-windows-amd64.tar.gz) | d5953a6589159d69aed70f33d3f8c79d947f97659664ef254ae5a18dc2469899f1a0243d58b36324c246a76cc5ecdff93ddb81d864749185c2d8dd777040bad5 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-client-windows-arm64.tar.gz) | e126a72af5f56447236996060a29d9c47191b99b2891482d0f681e1a2640416a7f9151d658b579b7af15e0fb2167062d3a7e7062e8c9bca2342f020d1785813a ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-server-linux-amd64.tar.gz) | 2098b70d6e328e0c5777a20d95cb7c5f8f3cd9f26960165c0db3135e9ddfb5b22e3f5471a130692dc48185592f4684c9239ed8e505a51984e31604c9a2e9040e [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-server-linux-arm64.tar.gz) | a4b97b9141b49a5bcb2e271b85d03926503c4272689556814cb0714d114ef327c6b209c4b0f0b339475d1bdc9f3dfcaf865c8b4283abaeb0714d2d8602b57f63 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-server-linux-ppc64le.tar.gz) | ab326bb628ba477f18f9a33f5abdcd2f36486146f062b09f3f524f8162e6c3d2736699c463b14ef29cde4b9cae18117a6cbe962a63553b2938a240461605aaea [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-server-linux-s390x.tar.gz) | 8af631c137f65af10129765cdff2697c730ba4ab58b63aea96d73c69e5d4fa2c35ff23416dac24fcadd3f3b856d08cf8223c28b40f4e8a02bb3c698dece6501f ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-node-linux-amd64.tar.gz) | 90d5aa5c08d01febea7f2afe11fb7771568494e68c5cf7b2c1a245b9de24d7962e207efa218ecba45540a2f613b13cf561a8b5f5618f9422042f40a8d7e88988 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-node-linux-arm64.tar.gz) | a631b6236485979c98f1a99553e55e4f6a77bc6fcad444490095872a3516b761ad5097297dd730f1b8fb27bd613af4eea0d4fefc3379fa4724bf4915f8576ecb [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-node-linux-ppc64le.tar.gz) | 342873a2d9eea49bc4b1ca0eca03ba1d019d60a8068bc2f015f5e35f5438e970d8d0722f441778cecf0f72cb5b27082bd1b434fc0d532dc5eaf96533616a8822 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-node-linux-s390x.tar.gz) | b0fa7050445cd4d9ffbe8014f72b44984f47ccb1ba7b6fcb191a0d6a784e4c741d1a04584339e6f09d0aa9568120d22dc4cde95f81f79cb52b13105cf5a57a9c [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.3/kubernetes-node-windows-amd64.tar.gz) | 741b4e93de0053586220ac210856dff035c8bb64856f600006be73875a53846f55fb32d9262b3fc6aab7b81cca4b2cfe0d05716fbe9c89e8ab8a9ab4e56ae8e4 ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.2 ## Changes by Kind ### Bug or Regression - Fix a bug causing unexpected delay of creating pods for newly created jobs ([#132158](https://github.com/kubernetes/kubernetes/pull/132158), [@linxiulei](https://github.com/linxiulei)) [SIG Apps and Testing] - Fix regression introduced in 1.33 - where some Paginated LIST calls are falling back to etcd instead of serving from cache. ([#132337](https://github.com/kubernetes/kubernetes/pull/132337), [@hakuna-matatah](https://github.com/hakuna-matatah)) [SIG API Machinery] - Fix validation for Job with suspend=true, and completions=0 to set the Complete condition. ([#132728](https://github.com/kubernetes/kubernetes/pull/132728), [@mimowo](https://github.com/mimowo)) [SIG Apps and Testing] - Kubeadm: fixed issue where etcd member promotion fails with an error saying the member was already promoted ([#132280](https://github.com/kubernetes/kubernetes/pull/132280), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] ### Other (Cleanup or Flake) - Reduce logspam when calculating the container resources on linux ([#132272](https://github.com/kubernetes/kubernetes/pull/132272), [@Peac36](https://github.com/Peac36)) [SIG Node] ## Dependencies ### Added _Nothing has changed._ ### Changed _Nothing has changed._ ### Removed _Nothing has changed._ # v1.33.2 ## Downloads for v1.33.2 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes.tar.gz) | 6983c9b0c8005ab8b332eba337ed1ca8d14a1419d6cb26473ffdcf1a3ec564e107ff3baadc7306d01d1cd722470034de8ab936a1040e0d367efdaccbea911432 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-src.tar.gz) | ab55d41194cdcef73331add791ae438705436f1d280ba615293aa27727cf0cbf82c8d93b50e71ca2a2ab72d77a13232894a6e56a190c5ea7ffac3633606761a9 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-darwin-amd64.tar.gz) | 2ee37c2e6592a6f1c5da07c53098747985c644174a0dcba1aab55850382c19fb6ee96ac5f718d8b9a3df42a200d0ef6517deb3396f241a107805ef3e8c5a5729 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-darwin-arm64.tar.gz) | 7ef489ef82f1e6d3a4ca0424cf5a09b289a4d8778e52c567ee5dc80779c0d652015343f224f2556ff80b59d9745dd2ec8294955a33f1c6af2073256d8fc54b92 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-linux-386.tar.gz) | 0d1ee8cd9db1a131845bdaab59ff07fcc960468d4d231506ba500e7c361992dcec1530c0f6ba13742f6846052357dbff7b412ee7b95ef4e613afb6b311805f6b [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-linux-amd64.tar.gz) | 1d20d5f3705b2c585afc2814e7cc56f8cf0de223345f8dffb62c625697ae97698c5e9d62a13d9def2db4152c3d636e7eefba9cd6d750167c8bf5150c2034c272 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-linux-arm.tar.gz) | 41a3043805f20f98157464c3ddd0310336ca417a4775460344fe421dfdd04e3f69b7d99b2495fc1959e566230ae3280d998b5a689de473928d2f8895ea68e3bb [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-linux-arm64.tar.gz) | c82a54169ca775ac85aaa9ed17370eee2addb471442a85d52fa8cf4fbba59b31cef57d328e4cd56f5f6c1489c51203d658aa24ead855bd3518afae5ad993b823 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-linux-ppc64le.tar.gz) | 0e29bc915785911d6f23c1a6de3ec603db8edcb4504d5d87fca373943d6427fd47f1dfa874afded1157c870953a36caa4da24ca2008857cf664b417d66812f22 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-linux-s390x.tar.gz) | 00a38841c1a6419f63db255b76932db7cfd448177b8ae17f9147f4850e4030dce075eeebde5052ac818e5104f21c47b766af10043f0b739aa479509c19b5eb5d [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-windows-386.tar.gz) | 963980e4e11ee925a6c4d7b4c82e5e9bb357353be7aaa12368451f507074484a6085367f153c615d25905f3d0d3de67c2793a9e5ee7ed4e67779f646f7ab285c [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-windows-amd64.tar.gz) | e15af258c113f5e0b5d83812b53a4f62fa3550b0c0301a116d91a62fbec0448dc9ac9b825bce11dd5c2c649aa084ae1fc418381de1c51eeb06c38ab99096ec47 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-client-windows-arm64.tar.gz) | 25e3690418010cb8d5bb9882a60af91e39768650f80f9b2fca910e09917f6d8dec000c17c22011b501e6d72e4ecb4faaed1bc165cb7af4ba82361dce6e664e8c ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-server-linux-amd64.tar.gz) | 1831758107a36c6915d6b4257b44c63cd68e1788fdf412f40401015f483407de116d7cfd4d1e61b5e8ff959d2182a41d6f9b70e2248eb97cba718f3f8715eaa2 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-server-linux-arm64.tar.gz) | c355f704091efd969c0af60d87d4320b8f9ce6617dcb0429d7702ac85466a40c4ed71d1996c0e480e7bc562ecd49ec36213ee43fb0c98f6502eee1293b0ad01c [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-server-linux-ppc64le.tar.gz) | e1711fcdb303b1685712dd6e3a7cbf2ca209c2a49fa010e36fec1bde6b4df4675b873f843804602dab5705c7d0d7db61d98cc344c5aace009bd008b115d084cc [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-server-linux-s390x.tar.gz) | 570ec1707d9b08803ab9c307eef3c8a54cba6ffde032246ab3fe2186d6d9c199f353f65f1d798df522c40af53e195bff99ef64e56bfa2c9f3ee6b776ead3ce6f ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-node-linux-amd64.tar.gz) | ac478b9504b153cee9d5fea8595621d65380c1040013d2f55070c1fab5a06a035d1e8ca6c62da3f70d8e2a980d7d30765607fde57c6a27c3b42c2de1270cf18c [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-node-linux-arm64.tar.gz) | b7a0c5d2e51c81a879bc8785eabc10226d7c00e9cb337e572f41f00c8e5d122050401da6cc3a981db2eb8b5295d47fa69a4dc72de8ae4dad9964aa192f2f28ff [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-node-linux-ppc64le.tar.gz) | a64c192e0961089662351f1d74b9de66433064e86e1b986ef704c8e8ecfd9acc5dbe94cd906302666adc7a7463d1e04a36098f4f892ce2350dd66beb8c36d388 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-node-linux-s390x.tar.gz) | 4fcdde7c52f82c463effb13bce8b59014a585edff716203dfb33f25223710346501fc49acb245a90e2bc1e99642f23d951ac16aece1d4b167dd71e7c2c622c13 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.2/kubernetes-node-windows-amd64.tar.gz) | 89d12b1359b15f030afab110195d90227a38420a6ad93c84237317b958c4c13826e35fc9dba687e345c04f38f03e92045714acaee88f4f9c21f3a12a575de609 ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.1 ## Important Security Information This release contains changes that address the following vulnerabilities: ### CVE-2025-4563: Nodes can bypass dynamic resource allocation authorization checks A vulnerability exists in the NodeRestriction admission controller where nodes can bypass dynamic resource allocation authorization checks. When the DynamicResourceAllocation feature gate is enabled, the controller properly validates resource claim statuses during pod status updates but fails to perform equivalent validation during pod creation. This allows a compromised node to create mirror pods that access unauthorized dynamic resources, potentially leading to privilege escalation. **Affected Versions**: - kube-apiserver v1.32.0 - v1.32.5 - kube-apiserver v1.33.0 - v1.33.1 **Fixed Versions**: - kube-apiserver v1.32.6 - kube-apiserver v1.33.2 This vulnerability was reported by amitschendel. **CVSS Rating:** Low (2.7) [CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:L](https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:L) ## Changes by Kind ### Feature - Kubernetes is now built using Go 1.24.3 ([#131935](https://github.com/kubernetes/kubernetes/pull/131935), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Kubernetes is now built using Go 1.24.4 ([#132226](https://github.com/kubernetes/kubernetes/pull/132226), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] ### Bug or Regression - Do not expand volume on the node, if controller expansion is finished ([#131987](https://github.com/kubernetes/kubernetes/pull/131987), [@gnufied](https://github.com/gnufied)) [SIG Storage] - Do not log error event when waiting for expansion on the kubelet ([#132098](https://github.com/kubernetes/kubernetes/pull/132098), [@gnufied](https://github.com/gnufied)) [SIG Storage] - Fixes an issue where Windows kube-proxy's ModifyLoadBalancer API updates did not match HNS state in version 15.4. ModifyLoadBalancer policy is supported from Kubernetes 1.31+. ([#131649](https://github.com/kubernetes/kubernetes/pull/131649), [@princepereira](https://github.com/princepereira)) [SIG Windows] - Kubelet: close a loophole where static pods could reference arbitrary ResourceClaims. The pods created by the kubelet then don't run due to a sanity check, but such references shouldn't be allowed regardless. ([#131876](https://github.com/kubernetes/kubernetes/pull/131876), [@pohly](https://github.com/pohly)) [SIG Apps, Auth and Node] - The shorthand for --output flag in kubectl explain was accidentally deleted, but has been added back. ([#131993](https://github.com/kubernetes/kubernetes/pull/131993), [@superbrothers](https://github.com/superbrothers)) [SIG CLI] ### Other (Cleanup or Flake) - Improve error message when a pod with user namespaces is created and the runtime doesn't support user namespaces. ([#131781](https://github.com/kubernetes/kubernetes/pull/131781), [@rata](https://github.com/rata)) [SIG Node] ## Dependencies ### Added _Nothing has changed._ ### Changed - github.com/Microsoft/hnslib: [v0.0.8 → v0.1.1](https://github.com/Microsoft/hnslib/compare/v0.0.8...v0.1.1) ### Removed _Nothing has changed._ # v1.33.1 ## Downloads for v1.33.1 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes.tar.gz) | b9c8150e47fa9ce3a3882d8fa82b00d541ecf7a7a2c7a7c711283aa118eaffbb1b003edc23f6c76ec99fdc241d3692d74d051673eca8f7202891aa0b65b9cbd7 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-src.tar.gz) | 6aa0e6ef8b9e9b7d100b69306c14f854f2c990b65264ff75e0d1acec2a41883d02609c62e8d5d36e1978f23cbd41c59121a7cfdd775a1fe55939e7001704ffcb ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-darwin-amd64.tar.gz) | 61cab9aa44aac2216dc13d9e6599fd31f2cefbaacd61e6cd3d5256b40faec7d7277c9ce2ad20fd4369fad39dd17c7652ebac8af2ef2db679ae5e9287a450628a [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-darwin-arm64.tar.gz) | 214fc220f8be2d2717540dfe0a478923d7b46ce18392750d96b7b1d80f530a7496b06e0ad173e887467aa103760614dc7c8b9928c512b8645d351d47dd352ae4 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-linux-386.tar.gz) | 073711ad37292e638a7ab4a8312a77e0791a711935863d17acbcc55f37eba6acf6611fe22ee578b5c76420f086da0c183bfd3170e458d7c5f65fb24396957af4 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-linux-amd64.tar.gz) | e1681d7addac6d1a192d17d8989764fb8f1143b1bc568de491f757313f6836c8c3180c0ad0d101679f9ac6a27c447865977d691df0db642390923082b5b4024d [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-linux-arm.tar.gz) | 94c3b44d860710b20d3d33f2c593346a29bfbb7abb3eed3e1b0b7f7fcd9947d54460abe96cc5197fe8a3440d185821cd93a2836cdb5d8b221ad10b9c83c5ae43 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-linux-arm64.tar.gz) | 47692ea55565da56cddcb57820cd36d586d3a785dadc529d73ab659a904169aa15556d094ace5d2d40e47173c223af98203760abc53f361311bb1496734f6605 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-linux-ppc64le.tar.gz) | ff49e62c410eef5e25098a3d2eb9917e82f11829d618901a5180be4f47c70ca953a4f2304abfff3dbd2f7a7fbb6de8b18e0d755f3dc6eab4da812f8528c1b011 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-linux-s390x.tar.gz) | fc72881bba29394a350dd42fe6726a1a16cb4dd6eacad14e29ea3bc8565983043b1584f6ec972206a6178a38cc5a284bbadf87ca12f645ca00ade9fbba80503b [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-windows-386.tar.gz) | 871e6ea8f1ed45cc1cdd6cbcb6f1f2325658316e0fa16d814f29782b41f0f10ffacb4ad70f54ee08dbb079148db1007cf5f2e6c92814877f68d85e687664fed2 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-windows-amd64.tar.gz) | 75e1bc70c70cd04cc4bc42310eb9ac5aca5a3c021c5f374d1c7e452fde74bb2cf7ca4eefb6f29db6389e46b5c63e45a2fb24631bea4a549d25232d739ffa5e8b [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-client-windows-arm64.tar.gz) | 04f4f31d14bffbac211f2efc696bb06b114ebc42361f6faea24812eff7f643e70510a1fdf5c0cf7604c5cf941d7c058d8368b1d7d9ce89c2fccca694479a9d3a ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-server-linux-amd64.tar.gz) | 83f995e7378da98198bf0901f49ede13fa26a6109a7b10f47a62b645c11cb6937670ccccfe88260cac4d8f4b67c2d83c5f05926bf066abfaa7e8b84c799e3829 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-server-linux-arm64.tar.gz) | 4ebe5c40d9f67d3e85be5fd2228c0c99d182ab25ca808d4fe8a098936f5f67369d165ddfa0f7631e6a26f77c3b76651262fa790dc7bdef61fdc5f6bea09b502d [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-server-linux-ppc64le.tar.gz) | 0ad919ac90660621ce51588a40ecb5f35ec80b441a153b67541bb47a304e1256b8c9c00233852c00515d107ce7d35df4dcd2c942e03ea789f4a0d076685d741f [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-server-linux-s390x.tar.gz) | 7a8a117cab3ec7460f955fd593ad40b7d280e635cc76844db78253a3ac9046f169b70214a6791f0c132c275ce702dae809bdb9f991f9ef1f68ce26200e386d4f ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-node-linux-amd64.tar.gz) | 7ff156cf79389d256275c93ebe2278dd385bb63068b00f77224baa5bde2f96e4037e9ff9f5997ded87dfaf49dcb1061fd63119758ccef9c5e4bddec0c89090ef [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-node-linux-arm64.tar.gz) | c2f72075cd8185c767ccd7f7d00f7b1c34a4aca944e3efe9d6e2dd437b53844613d92be956652156858c08ec33052b0df75d40db3afc388aeaee8588472f3802 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-node-linux-ppc64le.tar.gz) | 09a3703da743e42531e7b74c558f52b7e1be741580de0d22e5f4621daef8a40f95e678d77cbe7accc1d9e9eb97be25c15ca823686c5840a69a16d0c1bb637b93 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-node-linux-s390x.tar.gz) | a6a0b87009b13c9432da58cac1604b608d84a380732f653746a9485490808e9f6a838576d6786e0900313077c44949cb7de3d16fd97ac165ac77eb707d55a5e3 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.1/kubernetes-node-windows-amd64.tar.gz) | d884eeae8670f075726452722017b0a1891951d2110177c5109dd9ca070af77772b67c074bf4dff1b6d3f154e46f2e96e78f915aa4b4add6418d1ace51c8d6f4 ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.0 ## Changes by Kind ### Bug or Regression - Check for newer resize fields when deciding recovery feature's status in kubelet ([#131437](https://github.com/kubernetes/kubernetes/pull/131437), [@gnufied](https://github.com/gnufied)) [SIG Storage] - Disable reading of disk geometry before calling expansion for ext and xfs filesystems ([#131636](https://github.com/kubernetes/kubernetes/pull/131636), [@gnufied](https://github.com/gnufied)) [SIG Storage] - Fixed a panic issue related to kubectl revision history kubernetes/kubectl#1724 ([#131496](https://github.com/kubernetes/kubernetes/pull/131496), [@tahacodes](https://github.com/tahacodes)) [SIG CLI] - Kube-scheduler: in Kubernetes 1.33, the number of devices that can be allocated per ResourceClaim was accidentally reduced to 16. Now the supported number of devices per ResourceClaim is 32 again. ([#131679](https://github.com/kubernetes/kubernetes/pull/131679), [@mortent](https://github.com/mortent)) [SIG Node] - Kubelet: fix a bug where the unexpected NodeResizeError condition was in PVC status when the csi driver does not support node volume expansion and the pvc has the ReadWriteMany access mode. ([#131523](https://github.com/kubernetes/kubernetes/pull/131523), [@carlory](https://github.com/carlory)) [SIG Storage] - Resolve a regression introduced in version 1.31 on Windows Proxy, where the creation of HNS endpoints fails if remote HNS endpoints with the same IP address have already been created. ([#131427](https://github.com/kubernetes/kubernetes/pull/131427), [@princepereira](https://github.com/princepereira)) [SIG Network and Windows] ## Dependencies ### Added _Nothing has changed._ ### Changed _Nothing has changed._ ### Removed _Nothing has changed._ # v1.33.0 [Documentation](https://docs.k8s.io) ## Downloads for v1.33.0 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes.tar.gz) | `d325cf208bec566b03ce9a3e56972f430243b46cad086ef9094d7e89e7ebab22e4e7869ad87c8bcb95370c4bcc6d43ca0fdff20c7f668c7db31122af6ef5fcb5` [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-src.tar.gz) | `0460b3327ef3ede807924e63da19ee78608c0ed1eebe80b9f4f201d26e1e1072d2902b4648db3d289069d0ad7707d4b37362eaf6a45e1f8c3687185ca8e83884` ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-darwin-amd64.tar.gz) | `a12e25581fd3716aa0db3ce5524ba7ae9a6e0606b92454c6c12c9b32b2900d17db2a85355c6f6d9bf6fa32ec1a1466df9501e5ab3510f5d8ae4193aafa0ba8f8` [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-darwin-arm64.tar.gz) | `7faacc4eda215101b8497c598e2e5ee8cd7889013b5888f17bc933f7785484e880a47c9e46504783cf503068f3462b21eecfa8a30a0f53c4a671633f528d0fa6` [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-linux-386.tar.gz) | `09e64479bfe760718685b0dddc060ee34e3efce029b1374254ffa09717148300692ee12e265fd1622746794d91aa7d407f258cab14905437c15e9876b47a24c5` [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-linux-amd64.tar.gz) | `23031beed988f77fa759d03c81f6e66ad39666e08ae56f1d8120c95b834dd06cb9d0d8aafc99152c8e4e880c000d613a0a560e985e81751cae91b445001096dd` [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-linux-arm.tar.gz) | `4ce625f861eab1f98c6fb39b93a1a9a50e669f31f65d713344aa36f8d00012cbb35a4d85ed9a15deffc42329e32d32b8b469f8f801e0232d9de50c768bbd058e` [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-linux-arm64.tar.gz) | `ba722521450771a326103bffc6095496620f67d2eceda233d006b02209277818a5a960903b0902ffaa055a6700b43505010066008e858a8197f8eeaf156fc814` [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-linux-ppc64le.tar.gz) | `26ebdc9f21ea90177c8503606373ca7cd62dc034c3c1886f8a9c4fe3822d70e53e51088cbddf09922fc81d4670af67e9c7d1cea920ed9d536f460cc8451c02f0` [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-linux-s390x.tar.gz) | `ba44c74096ec228362c37a47388e612736021c7d8a0c26b21af6c4970b2c2b4b6abd20561775a2425965ad158599fd7605da6a9ef1ec851fb5b53554be180977` [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-windows-386.tar.gz) | `74a065c301e18cf9a403e7f6976310d2d6cd99406194ad5f92bb270d2f2aadf8a8a3d0ac66a4528d4f43183ad43baf07dedbecca448293c3fa91f2c888af5118` [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-windows-amd64.tar.gz) | `89b3447b137780de65da653b6724ec7ccf9cdffe9e6b228d87f2b58060e51c15fb83f7b7ae6b70d3dbdbe7164d71f70650a81f37e47bad3c980a02092003aa32` [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-client-windows-arm64.tar.gz) | `b9cbfa357d48388aaff2565a85ad094e4b9642894b2fe2c565b9bb093ca007116b883463aa378ca8ac5993c1d5c4a581b9d8fe1ad4c4098fcf3c807c0bc67e32` ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-server-linux-amd64.tar.gz) | `487aea4b3e1066b4d7644b44195e8ca0d55bde4807d5c96d6fc020661b14cf356aebe1e3fd7c1f841ba1b5a0be9da097dfaf117f05b821f75dd0aa29cd99fb70` [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-server-linux-arm64.tar.gz) | `7ebebcb44435a18050beefbde7c6d2d36d86fee8908514b3f3e0925a93e0791193613c7b19f2a359b2330f0cb62ca39e1bfd9628ae6b9d713c5dcd21857ae845` [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-server-linux-ppc64le.tar.gz) | `07a93cac90368ed216caaf1ea3885051b2ec1843de90fea5464cc8f666aecc11519fad32a83b7989f8fd3d6fe3862060a23859398a3287c2f782c03dd134f4d8` [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-server-linux-s390x.tar.gz) | `ad3b3ad780f62944d0d6778461f0e8b81ae66391fa8eb666bac05cff95b22dd669ddd1917045240c54070313b1f6d81ed1868df084f6b4f46e8b1b49b5c0ae67` ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-node-linux-amd64.tar.gz) | `053b44d2fbf7e71d2bf4766448bfe755775bc33ab26f56e2b5a4c3d07981d75fc45d8c5f6ae6f4508fb5aff803000709c9ac8e9d7a5797d37b34be24c2a1975e` [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-node-linux-arm64.tar.gz) | `b367dabfd6697479c1e50f977898f479210588855202f0ea6e2f29ad435a9174e88c387e21e2495af8fa412faf5ac858706bbb88f20217d93b1e529fdc57c5d6` [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-node-linux-ppc64le.tar.gz) | `99a907d19183e9e50a6043acfc2fbf239a6ecf39707831fe563dda3cbadca3b9d11a6bbfcb9050f725713b7a9679421958a2e52ec549f823dd40fdaef34f6d02` [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-node-linux-s390x.tar.gz) | `52f802417f4ced7e82c3e24b54e9315ced590a8c9fdee63efb7820734fa6216551cf2683c907b3c211b5e19fe978f33ef1d6f85d58c10008930375fcb5f08231` [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0/kubernetes-node-windows-amd64.tar.gz) | `61ef82babea9d7f3f19dcc208dd692f65cdfc3cfd01d3e5c6c35897c6e2a1ae05952162f5e9dba08d87a49abdc27d102392619c5902238ef16fd44d44fbf5c9f` ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.32.0 ## Urgent Upgrade Notes ### (No, really, you MUST read this before you upgrade) - Added the ability to reduce both the initial delay and the maximum delay accrued between container restarts for a node for containers in `CrashLoopBackOff` across the cluster to the recommended values of `1s` initial delay and `60s` maximum delay. To set this for a node, turn on the feature gate `ReduceDefaultCrashLoopBackOffDecay`. If you are also using the feature gate `KubeletCrashLoopBackOffMax` with a configured per-node `CrashLoopBackOff.MaxContainerRestartPeriod`, the effective kubelet configuration will follow the conflict resolution policy described further in the documentation [here](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#reduced-container-restart-delay). ([#130711](https://github.com/kubernetes/kubernetes/pull/130711), [@lauralorenz](https://github.com/lauralorenz)) [SIG Node and Testing] - [Action Required] CSI drivers that call IsLikelyNotMountPoint should not assume false means that the path is a mount point. Each CSI driver needs to make sure correct usage of return value of IsLikelyNotMountPoint because if the file is an irregular file but not a mount point is acceptable ([#129370](https://github.com/kubernetes/kubernetes/pull/129370), [@andyzhangx](https://github.com/andyzhangx)) [SIG Storage and Windows] - Fixed the behavior of the `KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK` environment variable in the nftables proxier. The kernel version check is now skipped only when this variable is explicitly set to a non-empty value. To skip the check, set the `KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK` environment variable. ([#130401](https://github.com/kubernetes/kubernetes/pull/130401), [@ryota-sakamoto](https://github.com/ryota-sakamoto)) - Renamed `UpdatePodTolerations` action type to `UpdatePodToleration`. Action required for custom plugin developers to update their code to follow the rename. ([#129023](https://github.com/kubernetes/kubernetes/pull/129023), [@zhifei92](https://github.com/zhifei92)) [SIG Scheduling and Testing] ## Changes by Kind ### Deprecation - The EndpointSlice `hints` field has graduated to GA. The beta annotation `service.kubernetes.io/topology-mode` is now considered deprecated and will not graduate to GA. It remains operational for backward compatibility. Users are encouraged to use the `spec.trafficDistribution` field in the Service API for topology-aware routing configuration. ([#130742](https://github.com/kubernetes/kubernetes/pull/130742), [@gauravkghildiyal](https://github.com/gauravkghildiyal)) [SIG Network] - The `StorageCapacityScoring` feature gate was added to score nodes by available storage capacity. It's in alpha and disabled by default. The `VolumeCapacityPriority` alpha feature was replaced with this, and the default behavior was changed. The `VolumeCapacityPriority` preferred a node with the least allocatable, but the `StorageCapacityScoring` preferred a node with the maximum allocatable. See [KEP-4049](https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/4049-storage-capacity-scoring-of-nodes-for-dynamic-provisioning/README.md) for details. ([#128184](https://github.com/kubernetes/kubernetes/pull/128184), [@cupnes](https://github.com/cupnes)) [SIG Scheduling, Storage and Testing] - The `WatchFromStorageWithoutResourceVersion` feature was deprecated and can no longer be enabled. ([#129930](https://github.com/kubernetes/kubernetes/pull/129930), [@serathius](https://github.com/serathius)) - The pod `status.resize` field is now deprecated and will no longer be set. The status of a pod resize will be exposed under two new conditions: `PodResizeInProgress` and `PodResizePending` instead. ([#130733](https://github.com/kubernetes/kubernetes/pull/130733), [@natasha41575](https://github.com/natasha41575)) [SIG API Machinery, Apps, CLI, Node, Scheduling and Testing] - The v1 Endpoints API is now officially deprecated (though still fully supported). The API will not be removed, but all users should use the EndpointSlice API instead. ([#130098](https://github.com/kubernetes/kubernetes/pull/130098), [@danwinship](https://github.com/danwinship)) [SIG API Machinery and Network] ### API Change - A new alpha feature gate, `MutableCSINodeAllocatableCount`, has been introduced. When this feature gate is enabled, the `CSINode.Spec.Drivers[*].Allocatable.Count` field becomes mutable, and a new field, `NodeAllocatableUpdatePeriodSeconds`, is available in the `CSIDriver` object. This allows periodic updates to a node's reported allocatable volume capacity, preventing stateful pods from becoming stuck due to outdated information that kube-scheduler relies on. ([#130007](https://github.com/kubernetes/kubernetes/pull/130007), [@torredil](https://github.com/torredil)) [SIG Apps, Node, Scheduling and Storage] - Added feature gate `DRAPartitionableDevices`, when enabled, Dynamic Resource Allocation support partitionable devices allocation. ([#130764](https://github.com/kubernetes/kubernetes/pull/130764), [@cici37](https://github.com/cici37)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Cluster Lifecycle, Instrumentation, Network, Node, Scheduling, Storage and Testing] - Added DRA support for a "one-of" prioritized list of selection criteria to satisfy a device request in a resource claim. ([#128586](https://github.com/kubernetes/kubernetes/pull/128586), [@mortent](https://github.com/mortent)) [SIG API Machinery, Apps, Etcd, Node, Scheduling and Testing] - Added a `/flagz` endpoint for kubelet endpoint ([#128857](https://github.com/kubernetes/kubernetes/pull/128857), [@zhifei92](https://github.com/zhifei92)) [SIG Architecture, Instrumentation and Node] - Added a new `tolerance` field to HorizontalPodAutoscaler, overriding the cluster-wide default. Enabled via the HPAConfigurableTolerance alpha feature gate. ([#130797](https://github.com/kubernetes/kubernetes/pull/130797), [@jm-franc](https://github.com/jm-franc)) [SIG API Machinery, Apps, Autoscaling, Etcd, Node, Scheduling and Testing] - Added support for configuring custom stop signals with a new StopSignal container lifecycle ([#130556](https://github.com/kubernetes/kubernetes/pull/130556), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) [SIG API Machinery, Apps, Node and Testing] - Added support for in-place vertical scaling of Pods with sidecars (containers defined within `initContainers` where the `restartPolicy` is set to `Always`). ([#128367](https://github.com/kubernetes/kubernetes/pull/128367), [@vivzbansal](https://github.com/vivzbansal)) [SIG API Machinery, Apps, CLI, Node, Scheduling and Testing] - CPUManager Policy Options support is GA ([#130535](https://github.com/kubernetes/kubernetes/pull/130535), [@ffromani](https://github.com/ffromani)) [SIG API Machinery, Node and Testing] - Changed the Pod API to support `hugepage resources` at `spec` level for pod-level resources. ([#130577](https://github.com/kubernetes/kubernetes/pull/130577), [@KevinTMtz](https://github.com/KevinTMtz)) [SIG Apps, CLI, Node, Scheduling, Storage and Testing] - DRA API: The maximum number of pods that can use the same ResourceClaim is now 256 instead of 32. Downgrading a cluster where this relaxed limit is in use to Kubernetes 1.32.0 is not supported, as version 1.32.0 would refuse to update ResourceClaims with more than 32 entries in the `status.reservedFor` field. ([#129543](https://github.com/kubernetes/kubernetes/pull/129543), [@pohly](https://github.com/pohly)) [SIG API Machinery, Node and Testing] - DRA: CEL expressions using attribute strings exceeded the cost limit because their cost estimation was incomplete. ([#129661](https://github.com/kubernetes/kubernetes/pull/129661), [@pohly](https://github.com/pohly)) [SIG Node] - DRA: Device taints enable DRA drivers or admins to mark device as unusable, which prevents allocating them. Pods may also get evicted at runtime if a device becomes unusable, depending on the severity of the taint and whether the claim tolerates the taint. ([#130447](https://github.com/kubernetes/kubernetes/pull/130447), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Architecture, Auth, Etcd, Instrumentation, Node, Scheduling and Testing] - DRA: Starting Kubernetes 1.33, only users with access to an admin namespace with the `kubernetes.io/dra-admin-access` label are authorized to create ResourceClaim or ResourceClaimTemplate objects with the `adminAccess` field in this admin namespace if they want to and only they can reference these ResourceClaims or ResourceClaimTemplates in their pod or deployment specs. ([#130225](https://github.com/kubernetes/kubernetes/pull/130225), [@ritazh](https://github.com/ritazh)) [SIG API Machinery, Apps, Auth, Node and Testing] - DRA: when asking for "All" devices on a node, Kubernetes <= 1.32 proceeded to schedule pods onto nodes with no devices by not allocating any devices for those pods. Kubernetes 1.33 changes that to only picking nodes which have at least one device. Users who want the "proceed with scheduling also without devices" semantic can use the upcoming prioritized list feature with one sub-request for "all" devices and a second alternative with "count: 0". ([#129560](https://github.com/kubernetes/kubernetes/pull/129560), [@bart0sh](https://github.com/bart0sh)) [SIG API Machinery and Node] - Expanded the on-disk kubelet credential provider configuration to allow an optional `tokenAttribute` field to be configured. When it is set, the kubelet will provision a token with the given audience bound to the current pod and its service account. This KSA token along with required annotations on the KSA defined in configuration will be sent to the credential provider plugin via its standard input (along with the image information that is already sent today). The KSA annotations to be sent are configurable in the kubelet credential provider configuration. ([#128372](https://github.com/kubernetes/kubernetes/pull/128372), [@aramase](https://github.com/aramase)) [SIG API Machinery, Auth, Node and Testing] - Fixed the example validation rule in godoc: When configuring a JWT authenticator: If username.expression uses 'claims.email', then 'claims.email_verified' must be used in username.expression or extra[*].valueExpression or claimValidationRules[*].expression. An example claim validation rule expression that matches the validation automatically applied when username.claim is set to 'email' is 'claims.?email_verified.orValue(true) == true'. By explicitly comparing the value to true, we let type-checking see the result will be a boolean, and to make sure a non-boolean `email_verified` claim will be caught at runtime. ([#130875](https://github.com/kubernetes/kubernetes/pull/130875), [@aramase](https://github.com/aramase)) [SIG Auth and Release] - For the `InPlacePodVerticalScaling` feature, the API server will no longer set the resize status to `Proposed` upon receiving a resize request. ([#130574](https://github.com/kubernetes/kubernetes/pull/130574), [@natasha41575](https://github.com/natasha41575)) [SIG Apps, Node and Testing] - Graduate the `MatchLabelKeys` (MismatchLabelKeys) feature in PodAffinity (PodAntiAffinity) to GA ([#130463](https://github.com/kubernetes/kubernetes/pull/130463), [@sanposhiho](https://github.com/sanposhiho)) [SIG API Machinery, Apps, Node, Scheduling and Testing] - Graduated image volume sources to beta: - Allowed `subPath`/`subPathExpr` for image volumes - Added kubelet metrics `kubelet_image_volume_requested_total`, `kubelet_image_volume_mounted_succeed_total` and `kubelet_image_volume_mounted_errors_total` ([#130135](https://github.com/kubernetes/kubernetes/pull/130135), [@saschagrunert](https://github.com/saschagrunert)) [SIG API Machinery, Apps, Node and Testing] - Implemented a new status field, `.status.terminatingReplicas`, for Deployments and ReplicaSets to track terminating pods. The new field is present when the `DeploymentPodReplacementPolicy` feature gate is enabled. ([#128546](https://github.com/kubernetes/kubernetes/pull/128546), [@atiratree](https://github.com/atiratree)) [SIG API Machinery, Apps and Testing] - Implemented validation for `NodeSelectorRequirement` values in Kubernetes when creating pods. ([#128212](https://github.com/kubernetes/kubernetes/pull/128212), [@AxeZhan](https://github.com/AxeZhan)) [SIG Apps and Scheduling] - Improved how the API server responds to **list** requests where the response format negotiates to Protobuf. List responses in Protobuf are marshalled one element at the time, drastically reducing memory needed to serve large collections. Streaming list responses can be disabled via the `StreamingCollectionEncodingToProtobuf` feature gate. ([#129407](https://github.com/kubernetes/kubernetes/pull/129407), [@serathius](https://github.com/serathius)) [SIG API Machinery, Apps, Architecture, Auth, CLI, Cloud Provider, Network, Node, Release, Scheduling, Storage and Testing] - InPlacePodVerticalScaling: Memory limits cannot be decreased unless the memory resize restart policy is set to `RestartContainer`. Container resizePolicy is no longer mutable. ([#130183](https://github.com/kubernetes/kubernetes/pull/130183), [@tallclair](https://github.com/tallclair)) [SIG Apps and Node] - Introduced API type `coordination.k8s.io/v1beta1/LeaseCandidate` `CoordinatedLeaderElection` feature moves to Beta ([#130751](https://github.com/kubernetes/kubernetes/pull/130751), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery, Etcd and Testing] - Introduced API type `coordination.k8s.io/v1beta1/LeaseCandidate` ([#130291](https://github.com/kubernetes/kubernetes/pull/130291), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery, Etcd and Testing] - It introduces a new scope name `VolumeAttributesClass`. It matches all PVC objects that have the volume attributes class mentioned. If you want to limit the count of PVCs that have a specific volume attributes class. In that case, you can create a quota object with the scope name `VolumeAttributesClass` and a `matchExpressions` that match the volume attributes class. ([#124360](https://github.com/kubernetes/kubernetes/pull/124360), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Testing] - KEP-3857: Recursive Read-only (RRO) mounts: promote to GA ([#130116](https://github.com/kubernetes/kubernetes/pull/130116), [@AkihiroSuda](https://github.com/AkihiroSuda)) [SIG Apps, Node and Testing] - kubectl: Added alpha support for customizing kubectl behavior using preferences from a `kuberc` file, separate from `kubeconfig`. ([#125230](https://github.com/kubernetes/kubernetes/pull/125230), [@ardaguclu](https://github.com/ardaguclu)) [SIG API Machinery, CLI and Testing] - kubelet: added `KubeletConfiguration.subidsPerPod`. ([#130028](https://github.com/kubernetes/kubernetes/pull/130028), [@AkihiroSuda](https://github.com/AkihiroSuda)) [SIG API Machinery and Node] - Kubernetes components that accepted X.509 client certificate authentication now read the user UID from a certificate subject name RDN with object ID `1.3.6.1.4.1.57683.2`. An RDN with this object ID had to contain a string value and appear no more than once in the certificate subject. Reading the user UID from this RDN could be disabled by setting the beta feature gate `AllowParsingUserUIDFromCertAuth` to `false`(until the feature gate graduated to GA). ([#127897](https://github.com/kubernetes/kubernetes/pull/127897), [@modulitos](https://github.com/modulitos)) [SIG API Machinery, Auth and Testing] - `MergeDefaultEvictionSettings` indicates that defaults for the evictionHard, evictionSoft, evictionSoftGracePeriod, and evictionMinimumReclaim fields should be merged into values specified for those fields in this configuration. Signals specified in this configuration take precedence. Signals not specified in this configuration inherit their defaults. ([#127577](https://github.com/kubernetes/kubernetes/pull/127577), [@vaibhav2107](https://github.com/vaibhav2107)) [SIG API Machinery and Node] - New configuration is introduced to the kubelet that allows it to track container images and the list of authentication information that leads to their successful pulls. This data is persisted across reboots of the host and restarts of the kubelet. The kubelet ensures any image requiring credential verification is always pulled if authentication information from an image pull is not yet present, thus enforcing authentication / re-authentication. This means an image pull might be attempted even in cases where a pod requests the `IfNotPresent` image pull policy, and might lead to the pod not starting if its pull policy is `Never` and is unable to present authentication information that led to a previous successful pull of the image it is requesting. ([#128152](https://github.com/kubernetes/kubernetes/pull/128152), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Architecture, Auth, Node and Testing] - Promoted JobSuccessPolicy E2E to Conformance ([#130658](https://github.com/kubernetes/kubernetes/pull/130658), [@tenzen-y](https://github.com/tenzen-y)) [SIG API Machinery, Apps, Architecture and Testing] - Promoted `NodeInclusionPolicyInPodTopologySpread` to Stable in v1.33 ([#130920](https://github.com/kubernetes/kubernetes/pull/130920), [@kerthcet](https://github.com/kerthcet)) [SIG Apps, Node, Scheduling and Testing] - Promoted the `JobSuccessPolicy` to Stable. ([#130536](https://github.com/kubernetes/kubernetes/pull/130536), [@tenzen-y](https://github.com/tenzen-y)) [SIG API Machinery, Apps, Architecture and Testing] - Promoted the Job's `JobBackoffLimitPerIndex` feature-gate to stable. ([#130061](https://github.com/kubernetes/kubernetes/pull/130061), [@mimowo](https://github.com/mimowo)) [SIG API Machinery, Apps, Architecture and Testing] - Promoted the feature gate `AnyVolumeDataSource` to GA. ([#129770](https://github.com/kubernetes/kubernetes/pull/129770), [@sunnylovestiramisu](https://github.com/sunnylovestiramisu)) [SIG Apps, Storage and Testing] - Removed general available feature gate `CPUManager`. ([#129296](https://github.com/kubernetes/kubernetes/pull/129296), [@carlory](https://github.com/carlory)) [SIG API Machinery, Node and Testing] - Removed general available feature-gate `PDBUnhealthyPodEvictionPolicy`. ([#129500](https://github.com/kubernetes/kubernetes/pull/129500), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Auth] - Start reporting swap capacity as part of `node.status.nodeSystemInfo`. ([#129954](https://github.com/kubernetes/kubernetes/pull/129954), [@iholder101](https://github.com/iholder101)) [SIG API Machinery, Apps and Node] - Graduated the `MultiCIDRServiceAllocator` feature gate to stable, and the `DisableAllocatorDualWrite` feature gate to beta (disabled by default). **Action required** for Kubernetes cluster administrators and for distributions that manage the cluster Service CIDR. Kubernetes now allows users to define the cluster Service CIDR via an API object: ServiceCIDR. Distributions or administrators of Kubernetes may want to control that new Service CIDRs added to the cluster do not overlap with other networks on the cluster, that only belong to a specific range of IPs. Administrators may also prefer to retain the existing behavior of only having one ServiceCIDR per cluster. You can use `ValidatingAdmissionPolicy` to achieve this. ([#128971](https://github.com/kubernetes/kubernetes/pull/128971), [@aojea](https://github.com/aojea)) [SIG Apps, Architecture, Auth, CLI, Etcd, Network, Release and Testing] - The `ClusterTrustBundle` API is moving to `v1beta1`. In order for the `ClusterTrustBundleProjection` feature to work on the kubelet side, the `ClusterTrustBundle` API must be available at `v1beta1` version and the `ClusterTrustBundleProjection` feature gate must be enabled. If the API becomes later after kubelet started running, restart the kubelet to enable the feature. ([#128499](https://github.com/kubernetes/kubernetes/pull/128499), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Apps, Auth, Etcd, Node, Storage and Testing] - The Service trafficDistribution field, including the PreferClose option, has graduated to GA. Services that do not have the field configured will continue to operate with their existing behavior. Refer to the documentation https://kubernetes.io/docs/concepts/services-networking/service/#traffic-distribution for more details. ([#130673](https://github.com/kubernetes/kubernetes/pull/130673), [@gauravkghildiyal](https://github.com/gauravkghildiyal)) [SIG Apps, Network and Testing] - The feature gate `InPlacePodVerticalScalingAllocatedStatus` is deprecated and no longer used. The `AllocatedResources` field in `ContainerStatus` is now guarded by the `InPlacePodVerticalScaling` feature gate. ([#130880](https://github.com/kubernetes/kubernetes/pull/130880), [@tallclair](https://github.com/tallclair)) [SIG CLI, Node and Scheduling] - The kube-controller-manager will set the `observedGeneration` field on pod conditions when the `PodObservedGenerationTracking` feature gate is set. ([#130650](https://github.com/kubernetes/kubernetes/pull/130650), [@natasha41575](https://github.com/natasha41575)) [SIG API Machinery, Apps, Node, Scheduling, Storage, Testing and Windows] - The kube-scheduler will set the `observedGeneration` field on pod conditions when the `PodObservedGenerationTracking` feature gate is set. ([#130649](https://github.com/kubernetes/kubernetes/pull/130649), [@natasha41575](https://github.com/natasha41575)) [SIG Node, Scheduling and Testing] - The kubelet will set the `observedGeneration` field on pod conditions when the `PodObservedGenerationTracking` feature gate is set. ([#130573](https://github.com/kubernetes/kubernetes/pull/130573), [@natasha41575](https://github.com/natasha41575)) [SIG Apps, Node, Scheduling, Storage, Testing and Windows] - The minimum value validation of ReplicationController's `replicas` and `minReadySeconds` fields have been migrated to declarative validation. The requiredness of both fields is also declaratively validated. If the `DeclarativeValidation` feature gate is enabled, mismatches with existing validation are reported via metrics. If the `DeclarativeValidationTakeover` feature gate is enabled, declarative validation is the primary source of errors for migrated fields. ([#130725](https://github.com/kubernetes/kubernetes/pull/130725), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery, Apps, Architecture, CLI, Cluster Lifecycle, Instrumentation, Network, Node and Storage] - The `resource.k8s.io/v1beta1` API is deprecated and will be removed in 1.36. Use `v1beta2` instead. ([#129970](https://github.com/kubernetes/kubernetes/pull/129970), [@mortent](https://github.com/mortent)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] - Validation now requires new StatefulSets with a `.spec.serviceName` field value to pass DNS1123 validation. Previously created StatefulSets with an invalid `.spec.serviceName` field value could not create any pods, and should be deleted. - Published OpenAPI for the StatefulSet schema is corrected to indicate the `.spec.serviceName` is optional. ([#130233](https://github.com/kubernetes/kubernetes/pull/130233), [@soltysh](https://github.com/soltysh)) [SIG API Machinery, Apps and Testing] - When the `PreferSameTrafficDistribution` feature gate is enabled, a new `trafficDistribution` value `PreferSameNode` is available, which attempts to always route Service connections to an endpoint on the same node as the client. Additionally, `PreferSameZone` is introduced as an alias for `PreferClose`. ([#130844](https://github.com/kubernetes/kubernetes/pull/130844), [@danwinship](https://github.com/danwinship)) [SIG API Machinery, Apps, Network and Windows] - When the `PodObservedGenerationTracking` feature gate was set, the kubelet populated `status.observedGeneration` to reflect the latest `metadata.generation` it observed for the pod. ([#130352](https://github.com/kubernetes/kubernetes/pull/130352), [@natasha41575](https://github.com/natasha41575)) [SIG API Machinery, Apps, CLI, Node, Release, Scheduling, Storage, Testing and Windows] - When the `StrictIPCIDRValidation` feature gate is enabled, Kubernetes will be slightly stricter about what values will be accepted as IP addresses and network address ranges (“CIDR blocks”). In particular, octets within IPv4 addresses are not allowed to have any leading `0`s, and IPv4-mapped IPv6 values (e.g. `::ffff:192.168.0.1`) are forbidden. These sorts of values can potentially cause security problems when different components interpret the same string as referring to different IP addresses (as in CVE-2021-29923). This tightening applies only to fields in built-in API kinds, and not to custom resource kinds, values in Kubernetes configuration files, or command-line arguments. (When the feature gate is disabled, creating an object with such an invalid IP or CIDR value will result in a warning from the API server about the fact that it will be rejected in the future.) ([#122550](https://github.com/kubernetes/kubernetes/pull/122550), [#128786](https://github.com/kubernetes/kubernetes/pull/128786), [@danwinship](https://github.com/danwinship)) [SIG API Machinery, Apps, Network, Node, Scheduling and Testing] - `apidiscovery.k8s.io/v2beta1` API group is disabled by default ([#130347](https://github.com/kubernetes/kubernetes/pull/130347), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery and Testing] - `kubectl apply` now coerces `null` values for labels and annotations in manifests to empty string values, consistent with typed JSON metadata decoding, rather than dropping all labels and annotations ([#129257](https://github.com/kubernetes/kubernetes/pull/129257), [@liggitt](https://github.com/liggitt)) [SIG API Machinery] ### Feature - Added `ListFromCacheSnapshot` feature gate that allows apiserver to serve LISTs with exact RV and continuations from cache ([#130423](https://github.com/kubernetes/kubernetes/pull/130423), [@serathius](https://github.com/serathius)) [SIG API Machinery, Etcd and Testing] - Added Pressure Stall Information (PSI) metrics to node metrics. ([#130701](https://github.com/kubernetes/kubernetes/pull/130701), [@roycaihw](https://github.com/roycaihw)) [SIG Node and Testing] - Added Windows Server, Version 2025 for windows-servercore-cache test image ([#130935](https://github.com/kubernetes/kubernetes/pull/130935), [@aramase](https://github.com/aramase)) [SIG Testing and Windows] - Added metrics to expose the main known reasons for resource alignment errors ([#129950](https://github.com/kubernetes/kubernetes/pull/129950), [@ffromani](https://github.com/ffromani)) [SIG Node and Testing] - Added `SchedulerPopFromBackoffQ` feature gate that is in beta and enabled by default. Improved scheduling queue behavior by popping pods from the backoffQ when the activeQ is empty. This allows to process potentially schedulable pods ASAP, eliminating a penalty effect of the backoff queue. ([#130772](https://github.com/kubernetes/kubernetes/pull/130772), [@macsko](https://github.com/macsko)) [SIG Scheduling and Testing] - Added `apiserver.latency.k8s.io/authentication` annotation to the audit log to record the time spent authenticating slow requests. Also added `apiserver.latency.k8s.io/authorization` annotation to record the time spent authorizing slow requests. ([#130571](https://github.com/kubernetes/kubernetes/pull/130571), [@hakuna-matatah](https://github.com/hakuna-matatah)) - Added a `/flagz` endpoint for kube-proxy ([#128985](https://github.com/kubernetes/kubernetes/pull/128985), [@yongruilin](https://github.com/yongruilin)) [SIG Instrumentation and Network] - Added a `/status` endpoint for kube-proxy ([#128989](https://github.com/kubernetes/kubernetes/pull/128989), [@Henrywu573](https://github.com/Henrywu573)) [SIG Instrumentation and Network] - Added a `/statusz` HTTP endpoint to the kube-scheduler. ([#128818](https://github.com/kubernetes/kubernetes/pull/128818), [@yongruilin](https://github.com/yongruilin)) [SIG Architecture, Instrumentation, Scheduling and Testing] - Added a `/statusz` HTTP endpoint to the kubelet. ([#128811](https://github.com/kubernetes/kubernetes/pull/128811), [@zhifei92](https://github.com/zhifei92)) [SIG Architecture, Instrumentation and Node] - Added a `/statusz` endpoint for kube-controller-manager ([#128991](https://github.com/kubernetes/kubernetes/pull/128991), [@Henrywu573](https://github.com/Henrywu573)) [SIG API Machinery, Cloud Provider, Instrumentation and Testing] - Added a `/statusz` endpoint for kube-scheduler ([#128987](https://github.com/kubernetes/kubernetes/pull/128987), [@Henrywu573](https://github.com/Henrywu573)) [SIG Instrumentation, Scheduling and Testing] - Added a mechanism that calculates a digest of etcd and the watch cache every 5 minutes and exposes it as the `apiserver_storage_digest` metric. ([#130475](https://github.com/kubernetes/kubernetes/pull/130475), [@serathius](https://github.com/serathius)) [SIG API Machinery, Instrumentation and Testing] - Added a new CLI flag `--emulation-forward-compatible` Added a new CLI `--runtime-config-emulation-forward-compatible` ([#130354](https://github.com/kubernetes/kubernetes/pull/130354), [@siyuanfoundation](https://github.com/siyuanfoundation)) [SIG API Machinery, Etcd and Testing] - Added a new option `strict-cpu-reservation` for CPU Manager static policy. When this option is enabled, CPU cores in `reservedSystemCPUs` will be strictly used for system daemons and interrupt processing no longer available for any workload. ([#130290](https://github.com/kubernetes/kubernetes/pull/130290), [@psasnal](https://github.com/psasnal)) [SIG Node and Testing] - Added an alpha feature gate `OrderedNamespaceDeletion`. When enabled, the pods resources are deleted before all other resources during namespace deletion. ([#130035](https://github.com/kubernetes/kubernetes/pull/130035), [@cici37](https://github.com/cici37)) [SIG API Machinery, Apps and Testing] - Added e2e tests for volume group snapshots. ([#128972](https://github.com/kubernetes/kubernetes/pull/128972), [@manishym](https://github.com/manishym)) [SIG Cloud Provider, Storage and Testing] - Added unit test helpers to validate CEL and patterns in CustomResourceDefinitions. ([#129028](https://github.com/kubernetes/kubernetes/pull/129028), [@sttts](https://github.com/sttts)) - Added validation of `containerLogMaxFiles` within kubelet configuration files. ([#129072](https://github.com/kubernetes/kubernetes/pull/129072), [@kannon92](https://github.com/kannon92)) - Adding resource completion in kubectl debug command ([#130033](https://github.com/kubernetes/kubernetes/pull/130033), [@ardaguclu](https://github.com/ardaguclu)) [SIG CLI] - Adds a `/flagz` endpoint for kube-controller-manager endpoint ([#128824](https://github.com/kubernetes/kubernetes/pull/128824), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery and Instrumentation] - Allowed `ImageVolume` for Restricted PSA profiles. ([#130394](https://github.com/kubernetes/kubernetes/pull/130394), [@Barakmor1](https://github.com/Barakmor1)) - Allowed dynamic configuration of the service account name and audience that the kubelet could request a token for, as part of the node audience restriction feature. ([#130485](https://github.com/kubernetes/kubernetes/pull/130485), [@aramase](https://github.com/aramase)) [SIG Auth and Testing] - Automatically copy `topology.k8s.io/zone`, `topology.k8s.io/region` and `kubernetes.io/hostname` labels from Node objects to Pods when they are scheduled to a node (via the `pods/binding` endpoint) to allow applications that need to be explicitly aware of their assigned node topology to access this information via the downward API, rather than requiring permission to `get node` objects (exposing the entire API surface of the Node object to otherwise unprivileged workloads). ([#127092](https://github.com/kubernetes/kubernetes/pull/127092), [@munnerz](https://github.com/munnerz)) [SIG API Machinery, Node and Testing] - Bumped `ProcMountType` feature to on by default beta ([#130798](https://github.com/kubernetes/kubernetes/pull/130798), [@haircommander](https://github.com/haircommander)) [SIG Node] - Calculated pod resources are now cached when adding pods to NodeInfo in the scheduler framework, improving performance when processing unschedulable pods. ([#129635](https://github.com/kubernetes/kubernetes/pull/129635), [@macsko](https://github.com/macsko)) [SIG Scheduling] - `cel-go` has been bumped to `v0.23.2`. ([#129844](https://github.com/kubernetes/kubernetes/pull/129844), [@cici37](https://github.com/cici37)) [SIG API Machinery, Auth, Cloud Provider and Node] - Changed metadata management for Pods to populate `.metadata.generation` on writes. New pods will have a `metadata.generation` of 1; updates to mutable fields in the Pod `.spec` will result in `metadata.generation` being incremented by 1. ([#130181](https://github.com/kubernetes/kubernetes/pull/130181), [@natasha41575](https://github.com/natasha41575)) [SIG Apps, Node and Testing] - DRA: Starting Kubernetes 1.33, regular users with namespaced cluster `edit` role assigned have `read` permission to `resourceclaims`, `resourceclaims/status`,`resourceclaimtemplates`. And `write` permission for `resourceclaims`, `resourceclaimtemplates`. ([#130738](https://github.com/kubernetes/kubernetes/pull/130738), [@ritazh](https://github.com/ritazh)) [SIG Auth] - `DRAResourceClaimDeviceStatus` is now turned on by default allowing DRA-Drivers to report device status data for each allocated device. ([#130814](https://github.com/kubernetes/kubernetes/pull/130814), [@LionelJouin](https://github.com/LionelJouin)) [SIG Network and Node] - `DistributeCPUsAcrossNUMA` policy option is promoted to Beta. ([#130541](https://github.com/kubernetes/kubernetes/pull/130541), [@swatisehgal](https://github.com/swatisehgal)) [SIG Node] - Enabled the `OrderedNamespaceDeletion` feature gate by default. ([#130507](https://github.com/kubernetes/kubernetes/pull/130507), [@cici37](https://github.com/cici37)) [SIG API Machinery and Apps] - Enabled user namespaces support (feature gate `UserNamespacesSupport`) by default. ([#130138](https://github.com/kubernetes/kubernetes/pull/130138), [@rata](https://github.com/rata)) [SIG Node and Testing] - Endpoints resources created by the Endpoints controller now include a label indicating this. Users who manually create Endpoints can also add this label, but they should consider using `EndpointSlices` instead. ([#130564](https://github.com/kubernetes/kubernetes/pull/130564), [@danwinship](https://github.com/danwinship)) [SIG Apps and Network] - Errors returned by apiserver from uninitialized cache will include last error from etcd ([#130899](https://github.com/kubernetes/kubernetes/pull/130899), [@serathius](https://github.com/serathius)) [SIG API Machinery and Testing] - Errors that occur during pod resize actuation will now surface in the `PodResizeInProgress` condition. ([#130902](https://github.com/kubernetes/kubernetes/pull/130902), [@natasha41575](https://github.com/natasha41575)) - Extended the kube-apiserver loopback client certificate validity to 14 months to align with the updated Kubernetes support lifecycle. ([#130047](https://github.com/kubernetes/kubernetes/pull/130047), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG API Machinery and Auth] - Extended the schema of the kube-proxy `healthz` and `livez` HTTP endpoints to incorporate information about the corresponding IP family. ([#129271](https://github.com/kubernetes/kubernetes/pull/129271), [@aroradaman](https://github.com/aroradaman)) [SIG Network and Windows] - Fixed `SELinuxWarningController` defaults when running kube-controller-manager in a container. ([#130037](https://github.com/kubernetes/kubernetes/pull/130037), [@jsafrane](https://github.com/jsafrane)) [SIG Apps and Storage] - Fixed a bug to ensure container-level swap metrics are collected. ([#129486](https://github.com/kubernetes/kubernetes/pull/129486), [@iholder101](https://github.com/iholder101)) [SIG Node and Testing] - git-repo volume plugin has been disabled by default, with the option to turn it back ([#129923](https://github.com/kubernetes/kubernetes/pull/129923), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) - Graduated the `WinDSR` feature in the kube-proxy to beta. The `WinDSR` feature gate is now enabled by default. ([#130876](https://github.com/kubernetes/kubernetes/pull/130876), [@rzlink](https://github.com/rzlink)) [SIG Windows] - Graduated the asynchronous preemption feature in the scheduler to beta. Now the feature flag (SchedulerAsyncPreemption) is enabled by default. ([#130550](https://github.com/kubernetes/kubernetes/pull/130550), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Graduated `BtreeWatchCache` feature gate to GA. ([#129934](https://github.com/kubernetes/kubernetes/pull/129934), [@serathius](https://github.com/serathius)) - Graduated the `DisableNodeKubeProxyVersion` feature gate to enable by default, the kubelet no longer attempts to set the `.status.kubeProxyVersion` field for its associated Node. ([#129713](https://github.com/kubernetes/kubernetes/pull/129713), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Node] - Graduated the `KubeletFineGrainedAuthz` feature gate to beta; the gate is now enabled by default. ([#129656](https://github.com/kubernetes/kubernetes/pull/129656), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) [SIG Auth, CLI, Node, Storage and Testing] - If scheduling fails on PreBind or Bind, scheduler will retry the failed pod immediately after backoff time, regardless of the reason for failing. In this case EventsToRegister (QHints) will not be taken into consideration before retry. ([#130189](https://github.com/kubernetes/kubernetes/pull/130189), [@ania-borowiec](https://github.com/ania-borowiec)) [SIG Scheduling] - Implemented full support for contextual logging in `client-go/rest`. `BackoffManagerWithContext` was used instead of `BackoffManager` to ensure that the caller could interrupt the sleep. ([#127709](https://github.com/kubernetes/kubernetes/pull/127709), [@pohly](https://github.com/pohly)) [SIG API Machinery, Architecture, Auth, Cloud Provider, Instrumentation, Network and Node] - Improved how the API server responds to **list** requests where the response format negotiates to JSON. List responses in JSON are marshalled one element at a time, drastically reducing the memory needed to serve large collections. Streaming list responses can be disabled via the `StreamingJSONListEncoding` feature gate. ([#129334](https://github.com/kubernetes/kubernetes/pull/129334), [@serathius](https://github.com/serathius)) [SIG API Machinery, Architecture and Release] - Improved scheduling performance of pods with required topology spreading. ([#129119](https://github.com/kubernetes/kubernetes/pull/129119), [@macsko](https://github.com/macsko)) [SIG Scheduling] - Introduced the `LegacySidecarContainers` feature gate enabling the legacy code path that predates the `SidecarContainers` feature. This temporary feature gate is disabled by default, only available in v1.33, and will be removed in v1.34. ([#130058](https://github.com/kubernetes/kubernetes/pull/130058), [@gjkim42](https://github.com/gjkim42)) [SIG Node] - KEP-3619: fine-grained supplemental groups policy is graduated to Beta. Note that kubelet now rejects pods with `.spec.securityContext.supplementalGroupsPolicy: Strict` when scheduled to the node that does not support the feature (`.status.features.supplementalGroupsPolicy: false`). ([#130210](https://github.com/kubernetes/kubernetes/pull/130210), [@everpeace](https://github.com/everpeace)) [SIG Apps, Node and Testing] - kube-apiserver: Promoted the `ServiceAccountTokenNodeBinding` feature gate general availability. It is now locked to enabled. ([#129591](https://github.com/kubernetes/kubernetes/pull/129591), [@liggitt](https://github.com/liggitt)) [SIG Auth and Testing] - kube-apiserver: the `StorageObjectInUseProtection` admission plugin added the `kubernetes.io/vac-protection` finalizer to the given VolumeAttributesClass object when it is created if the feature-gate `VolumeAttributesClass` is turned on and `storage.k8s.io/v1beta1` is enabled. ([#130553](https://github.com/kubernetes/kubernetes/pull/130553), [@Phaow](https://github.com/Phaow)) [SIG Storage and Testing] - kubeadm: `kubeadm upgrade plan` now supports `--etcd-upgrade` flag to control whether the etcd upgrade plan should be displayed. Add an `EtcdUpgrade` field into `UpgradeConfiguration.Plan` for v1beta4. ([#130023](https://github.com/kubernetes/kubernetes/pull/130023), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - kubeadm: Added preflight check for `cp` on Linux nodes and `xcopy` on Windows nodes. These binaries are required for kubeadm to work properly. ([#130045](https://github.com/kubernetes/kubernetes/pull/130045), [@carlory](https://github.com/carlory)) - kubeadm: Improved `kubeadm init` and `kubeadm join` to provide consistent error messages when the kubelet failed or when failed to wait for control plane components. ([#130040](https://github.com/kubernetes/kubernetes/pull/130040), [@HirazawaUi](https://github.com/HirazawaUi)) - kubeadm: Promoted the feature gate `ControlPlaneKubeletLocalMode` to Beta. By default, kubeadm will use the local kube-apiserver endpoint for the kubelet when creating a cluster with `kubeadm init` or when joining control plane nodes with `kubeadm join`. Enabling the feature gate also affects the `kubeadm init phase kubeconfig kubelet` phase, where the flag `--control-plane-endpoint` no longer affects the generated kubeconfig `Server` field, but the flag `--apiserver-advertise-address` can now be used for the same purpose. ([#129956](https://github.com/kubernetes/kubernetes/pull/129956), [@chrischdi](https://github.com/chrischdi)) - kubeadm: graduated the WaitForAllControlPlaneComponents feature gate to Beta. When checking the health status of a control plane component, make sure that the address and port defined as arguments in the respective component's static Pod manifest are used. ([#129620](https://github.com/kubernetes/kubernetes/pull/129620), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: if the `NodeLocalCRISocket` feature gate is enabled, remove the `kubeadm.alpha.kubernetes.io/cri-socket` annotation from a given node on `kubeadm upgrade`. ([#129279](https://github.com/kubernetes/kubernetes/pull/129279), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle and Testing] - kubeadm: if the `NodeLocalCRISocket` feature gate is enabled, remove the flag `--container-runtime-endpoint` from the `/var/lib/kubelet/kubeadm-flags.env` file on `kubeadm upgrade`. ([#129278](https://github.com/kubernetes/kubernetes/pull/129278), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle] - kubeadm: removed preflight check for nsenter on Linux nodes kubeadm: added preflight check for `losetup` on Linux nodes. It's required by kubelet for keeping a block device opened. ([#129450](https://github.com/kubernetes/kubernetes/pull/129450), [@carlory](https://github.com/carlory)) [SIG Cluster Lifecycle] - kubeadm: removed the feature gate EtcdLearnerMode which graduated to GA in 1.32. ([#129589](https://github.com/kubernetes/kubernetes/pull/129589), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubelet + DRA: For DRA driver plugins (and only for those!), the kubelet now supports a rolling update with `maxSurge > 0` in the driver's DaemonSet. A DRA driver must support this, which can be done via the k8s.io/dynamic-resource-allocation/kubeletplugin helper package. ([#129832](https://github.com/kubernetes/kubernetes/pull/129832), [@pohly](https://github.com/pohly)) [SIG Node, Storage and Testing] - Kubernetes is now built with Go `1.24.2` ([#131369](https://github.com/kubernetes/kubernetes/pull/131369), [@ameukam](https://github.com/ameukam)) [SIG Release and Testing] - NodeRestriction admission now validates that the audience value, the kubelet requested a service account token for, is part of the pod spec volume. The kube-apiserver featuregate `ServiceAccountNodeAudienceRestriction` is enabled by default in 1.33. ([#130017](https://github.com/kubernetes/kubernetes/pull/130017), [@aramase](https://github.com/aramase)) - Pod resource checkpointing is now tracked by the `allocated_pods_state` and `actuated_pods_state` files, replacing the previously used `pod_status_manager_state`. ([#130599](https://github.com/kubernetes/kubernetes/pull/130599), [@tallclair](https://github.com/tallclair)) - `PodLifecycleSleepAction` is now turned on by default allowing users to create containers with sleep lifecycle action with a duration of zero seconds ([#130621](https://github.com/kubernetes/kubernetes/pull/130621), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) [SIG Node] - Promoted `RelaxedDNSSearchValidation` to beta, allowing for Pod search domains to be a single dot "." or contain an underscore "_". ([#130128](https://github.com/kubernetes/kubernetes/pull/130128), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps and Network] - Promoted in-place Pod vertical scaling to beta. The `InPlacePodVerticalScaling` feature gate is now enabled by default. ([#130905](https://github.com/kubernetes/kubernetes/pull/130905), [@tallclair](https://github.com/tallclair)) [SIG Node] - Promoted kubectl `--subresource` flag to stable. ([#130238](https://github.com/kubernetes/kubernetes/pull/130238), [@soltysh](https://github.com/soltysh)) - Promoted the `CRDValidationRatcheting` feature gate to GA in 1.33 ([#130013](https://github.com/kubernetes/kubernetes/pull/130013), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery] - Promoted the feature gate `CSIMigrationPortworx` to GA. If your applications are using Portworx volumes, please make sure that the corresponding Portworx CSI driver is installed on your cluster **before** upgrading to 1.31 or later because all operations for the in-tree `portworxVolume` type are redirected to the pxd.portworx.com CSI driver when the feature gate is enabled. ([#129297](https://github.com/kubernetes/kubernetes/pull/129297), [@gohilankit](https://github.com/gohilankit)) [SIG Storage] - Promoted the feature gate `HonorPVReclaimPolicy` to GA. ([#129583](https://github.com/kubernetes/kubernetes/pull/129583), [@carlory](https://github.com/carlory)) [SIG Apps, Storage and Testing] - Respect the incoming trace context for authenticated requests to the kube-apiserver for APIServer tracing. ([#127053](https://github.com/kubernetes/kubernetes/pull/127053), [@dashpole](https://github.com/dashpole)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Instrumentation, Network, Node and Testing] - SELinuxChangePolicy and SELinuxMount graduated to Beta. SELinuxMount stays off by default. ([#130544](https://github.com/kubernetes/kubernetes/pull/130544), [@jsafrane](https://github.com/jsafrane)) [SIG Auth, Node and Storage] - Scheduling Framework exposes NodeInfo to the ScorePlugin. ([#130537](https://github.com/kubernetes/kubernetes/pull/130537), [@saintube](https://github.com/saintube)) [SIG Scheduling, Storage and Testing] - The `RemoteRequestHeaderUID` feature moves to beta and is now enabled by default. This makes the kube-apiserver propagate UIDs in the `X-Remote-Uid` header in requests to the aggregated API servers. The header is not honored by default for incoming requests, but that can be enabled by setting the `--requestheader-uid-headers` flag explicitly. ([#130560](https://github.com/kubernetes/kubernetes/pull/130560), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Auth and Testing] - The `DeclarativeValidation` feature gate is enabled by default. When enabled, mismatches with existing hand written validation is reported via metrics. The `DeclarativeValidationTakeover` feature gate remains disabled by default. While disabled, validation errors produced by hand written validation are always return to the caller. To switch to declarative validation is primary source of errors for migrated fields, enable this feature gate. ([#130728](https://github.com/kubernetes/kubernetes/pull/130728), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery] - The `SidecarContainers` feature has graduated to GA. 'SidecarContainers' feature gate was locked to default value and will be removed in v1.36. If you were setting this feature gate explicitly, please remove it now. ([#129731](https://github.com/kubernetes/kubernetes/pull/129731), [@gjkim42](https://github.com/gjkim42)) [SIG Apps, Node, Scheduling and Testing] - The nftables mode of kube-proxy is now GA. (The iptables mode remains the default; you can select the nftables mode by passing `--proxy-mode nftables` or using a config file with `mode: nftables`. See the kube-proxy documentation for more details.) ([#129653](https://github.com/kubernetes/kubernetes/pull/129653), [@danwinship](https://github.com/danwinship)) [SIG Network] - Updated `/version` response to report binary version information separate from compatibility version ([#130019](https://github.com/kubernetes/kubernetes/pull/130019), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery, Architecture, Release and Testing] - Upgraded the `kubectl autoscale` subcommand to use `autoscaling/v2` rather than `autoscaling/v1` APIs. The command now attempts to use the `autoscaling/v2` API first. If the `autoscaling/v2` API is unavailable or an error occurs, it falls back to the `autoscaling/v1` API. ([#128950](https://github.com/kubernetes/kubernetes/pull/128950), [@googs1025](https://github.com/googs1025)) [SIG Autoscaling and CLI] - User namespaces support (feature gate UserNamespacesSupport) is now enabled by ([#130138](https://github.com/kubernetes/kubernetes/pull/130138), [@rata](https://github.com/rata)) [SIG Node and Testing] - Various controllers that write out IP address or CIDR values to API objects now ensure that they always write out the values in canonical form. ([#130101](https://github.com/kubernetes/kubernetes/pull/130101), [@danwinship](https://github.com/danwinship)) [SIG Apps, Network and Node] - `kubeproxy_conntrack_reconciler_deleted_entries_total` metric can be used to track cumulative sum of conntrack flows cleared by reconciler. ([#130204](https://github.com/kubernetes/kubernetes/pull/130204), [@aroradaman](https://github.com/aroradaman)) - `kubeproxy_conntrack_reconciler_sync_duration_seconds` metric can now be used to track conntrack reconciliation latency. ([#130200](https://github.com/kubernetes/kubernetes/pull/130200), [@aroradaman](https://github.com/aroradaman)) - The `StorageCapacityScoring` feature gate was added to score nodes by available storage capacity. It's in alpha and disabled by default. The `VolumeCapacityPriority` alpha feature was replaced with this, and the default behavior was changed. The `VolumeCapacityPriority` preferred a node with the least allocatable, but the `StorageCapacityScoring` preferred a node with the maximum allocatable. See [KEP-4049](https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/4049-storage-capacity-scoring-of-nodes-for-dynamic-provisioning/README.md) for details. ([#128184](https://github.com/kubernetes/kubernetes/pull/128184), [@cupnes](https://github.com/cupnes)) [SIG Scheduling, Storage and Testing] ### Documentation - Added an example of set-based requirements for the `-l` / `--selector` command line option to `kubectl`. ([#129106](https://github.com/kubernetes/kubernetes/pull/129106), [@rotsix](https://github.com/rotsix)) - kubeadm: improved the `kubeadm reset` message for manual cleanups and referenced https://k8s.io/docs/reference/setup-tools/kubeadm/kubeadm-reset/. ([#129644](https://github.com/kubernetes/kubernetes/pull/129644), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] ### Bug or Regression - --feature-gate=InOrderInformers (default on), causes informers to process watch streams in order as opposed to grouping updates for the same item close together. Binaries embedding client-go, but not wiring the featuregates can disable by setting the `KUBE_FEATURE_InOrderInformers=false`. ([#129568](https://github.com/kubernetes/kubernetes/pull/129568), [@deads2k](https://github.com/deads2k)) [SIG API Machinery] - Added a validation for the `revisionHistoryLimit` field in the `.spec` of a StatefulSet, to prevent it from being set to a negative value. ([#129017](https://github.com/kubernetes/kubernetes/pull/129017), [@ardaguclu](https://github.com/ardaguclu)) - Added progress tracking for volume permission and ownership changes. ([#130398](https://github.com/kubernetes/kubernetes/pull/130398), [@gnufied](https://github.com/gnufied)) [SIG Node and Storage] - Changed the signature of `PublishResources()` for ResourceSlices to accept a `resourceslice.DriverResources` parameter instead of a `Resources` parameter. ([#129142](https://github.com/kubernetes/kubernetes/pull/129142), [@googs1025](https://github.com/googs1025)) [SIG Node and Testing] - DRA: the explanation for why a pod which wasn't using ResourceClaims was unscheduleable included a useless "no new claims to deallocate" when it was unscheduleable for some other reasons. ([#129823](https://github.com/kubernetes/kubernetes/pull/129823), [@googs1025](https://github.com/googs1025)) [SIG Node and Scheduling] - Disabled InPlace Pod Resize for Swap enabled containers that does not have memory ResizePolicy as RestartContainer ([#130831](https://github.com/kubernetes/kubernetes/pull/130831), [@ajaysundark](https://github.com/ajaysundark)) [SIG Node and Testing] - Enabled ratcheting validation on `status` subresources for CustomResourceDefinitions. ([#129506](https://github.com/kubernetes/kubernetes/pull/129506), [@JoelSpeed](https://github.com/JoelSpeed)) - Fix: Adopted go1.23 behavior change in mount point parsing on Windows. ([#129368](https://github.com/kubernetes/kubernetes/pull/129368), [@andyzhangx](https://github.com/andyzhangx)) [SIG Storage and Windows] - Fixed CVE-2024-51744. ([#128621](https://github.com/kubernetes/kubernetes/pull/128621), [@kmala](https://github.com/kmala)) [SIG Auth, Cloud Provider and Node] - Fixed `kubectl wait --for=create` behavior with label selectors, to properly wait for resources with matching labels to appear. ([#128662](https://github.com/kubernetes/kubernetes/pull/128662), [@omerap12](https://github.com/omerap12)) [SIG CLI and Testing] - Fixed a bug in HorizontalPodAutoscaler. HPAs with `ContainerResource` metrics no longer return an error when container metrics are missing. Instead they use the same logic as `Resource` metrics to perform calculations. ([#127193](https://github.com/kubernetes/kubernetes/pull/127193), [@DP19](https://github.com/DP19)) [SIG Apps and Autoscaling] - Fixed a bug in the exclusive assignment availability check for the `InPlacePodVerticalScalingExclusiveCPUs` feature gate. ([#130559](https://github.com/kubernetes/kubernetes/pull/130559), [@esotsal](https://github.com/esotsal)) - Fixed a bug where adding an ephemeral container to a pod which references a new secret or config map doesn't give the pod access to that new secret or config map. (#114984, @cslink) ([#129670](https://github.com/kubernetes/kubernetes/pull/129670), [@cslink](https://github.com/cslink)) [SIG Auth] - Fixed a bug where kube-apiserver could emit a subsequent watch event even if the previous event failed to decrypt and was not emitted. ([#131020](https://github.com/kubernetes/kubernetes/pull/131020), [@wojtek-t](https://github.com/wojtek-t)) [SIG API Machinery and Etcd] - Fixed a bug where the kube-proxy `EndpointSliceCache` memory experienced a leak. ([#128929](https://github.com/kubernetes/kubernetes/pull/128929), [@orange30](https://github.com/orange30)) - Fixed a data race that could occur when a single Go type was serialized to CBOR concurrently for the first time within a program. ([#129170](https://github.com/kubernetes/kubernetes/pull/129170), [@benluddy](https://github.com/benluddy)) [SIG API Machinery] - Fixed a panic in kube-controller-manager handling StatefulSet objects when `revisionHistoryLimit` is negative. ([#129301](https://github.com/kubernetes/kubernetes/pull/129301), [@ardaguclu](https://github.com/ardaguclu)) - Fixed a regression in 1.32 that prevented pods with `postStart` hooks from starting. ([#129946](https://github.com/kubernetes/kubernetes/pull/129946), [@alex-petrov-vt](https://github.com/alex-petrov-vt)) - Fixed a regression in 1.32 where nodes could fail to report status and renew serving certificates after the kubelet restarted. ([#130348](https://github.com/kubernetes/kubernetes/pull/130348), [@aojea](https://github.com/aojea)) - Fixed a regression with the `ServiceAccountNodeAudienceRestriction` feature where `azureFile` volumes encountered 'failed to get service account token attributes' errors. ([#129993](https://github.com/kubernetes/kubernetes/pull/129993), [@aramase](https://github.com/aramase)) [SIG Auth and Testing] - Fixed a storage bug related to multipath. iSCSI and Fibre Channel devices attached to nodes via multipath now resolve correctly when partitioned. ([#128086](https://github.com/kubernetes/kubernetes/pull/128086), [@RomanBednar](https://github.com/RomanBednar)) - Fixed a test failure in `TestSetVolumeOwnershipOwner` for `fsGroup=3000` and symlink cases in `volume_linux_test.go`. The tests were failing due to invalid ownership verification and the issue has been resolved by adjusting file permission change handling, ensuring correct behavior when run as root. ([#130616](https://github.com/kubernetes/kubernetes/pull/130616), [@gnufied](https://github.com/gnufied)) - Fixed an issue in register-gen where imports for k8s.io/apimachinery/pkg/runtime and k8s.io/apimachinery/pkg/runtime/schema were missing. ([#129307](https://github.com/kubernetes/kubernetes/pull/129307), [@LionelJouin](https://github.com/LionelJouin)) [SIG API Machinery] - Fixed an issue in the CEL CIDR library where subnets contained within another CIDR were incorrectly rejected as not being contained. ([#130450](https://github.com/kubernetes/kubernetes/pull/130450), [@JoelSpeed](https://github.com/JoelSpeed)) - Fixed an issue where kubelet would unmount volumes of running pods upon restart if the referenced PVC was being deleted by the user. ([#130335](https://github.com/kubernetes/kubernetes/pull/130335), [@carlory](https://github.com/carlory)) [SIG Node, Storage and Testing] - Fixed an issue where pods did not correctly have a pending phase after the node reboot. ([#128516](https://github.com/kubernetes/kubernetes/pull/128516), [@gjkim42](https://github.com/gjkim42)) [SIG Node and Testing] - Fixed an issue with Kubernetes-style sidecar containers (in other words: init containers with an Always restart policy) and Services. Before the fix, named ports exposed by a sidecar could not be accessed using a Service. ([#128850](https://github.com/kubernetes/kubernetes/pull/128850), [@toVersus](https://github.com/toVersus)) [SIG Network and Testing] - Fixed compressed kubelet log file permissions to use uncompressed kubelet log file permissions. ([#129893](https://github.com/kubernetes/kubernetes/pull/129893), [@simonfogliato](https://github.com/simonfogliato)) [SIG Node] - Fixed in-tree to CSI migration for Portworx volumes, in clusters where Portworx security feature is enabled (it's a Portworx feature, not Kubernetes feature). It required secret data from the secret mentioned in-tree SC, to be passed in CSI requests which was not happening before this fix. ([#129630](https://github.com/kubernetes/kubernetes/pull/129630), [@gohilankit](https://github.com/gohilankit)) [SIG Storage] - Fixed a rare and sporadic network issues that occurred when the host was under heavy load. ([#130256](https://github.com/kubernetes/kubernetes/pull/130256), [@adrianmoisey](https://github.com/adrianmoisey)) - Fixed the bug where Events failed to be created when the referenced object name was not a valid Event name. Now, a UUID is used as the name instead of the referenced object name and the timestamp suffix. ([#129790](https://github.com/kubernetes/kubernetes/pull/129790), [@aojea](https://github.com/aojea)) - Fixed a 1.32 regression kube-proxy, when using a Service with External or LoadBalancer IPs on UDP services , was consuming a large amount of CPU because it was not filtering by the Service destination port and trying to delete all the UDP entries associated to the service. ([#130484](https://github.com/kubernetes/kubernetes/pull/130484), [@aojea](https://github.com/aojea)) [SIG Network] - Implemented logging and event recording for probe results with an `Unknown` status in the kubelet's prober module. This helped improve the diagnosis and monitoring of cases where container probes returned an `Unknown` result, enhancing the observability and reliability of health checks. ([#125901](https://github.com/kubernetes/kubernetes/pull/125901), [@jralmaraz](https://github.com/jralmaraz)) - Improved reboot event reporting. The kubelet will only emit one reboot Event when a server-level reboot is detected, even if the kubelet cannot write its status to the associated Node (which triggers a retry). ([#129151](https://github.com/kubernetes/kubernetes/pull/129151), [@rphillips](https://github.com/rphillips)) [SIG Node] - Includes WebSockets HTTPS proxy support ([#129872](https://github.com/kubernetes/kubernetes/pull/129872), [@seans3](https://github.com/seans3)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Instrumentation, Network and Node] - kube-apiserver: `--service-account-max-token-expiration` can now be used in combination with an external token signer `--service-account-signing-endpoint`, as long as the `--service-account-max-token-expiration` is not longer than the external token signer's max expiration. ([#129816](https://github.com/kubernetes/kubernetes/pull/129816), [@sambdavidson](https://github.com/sambdavidson)) [SIG API Machinery and Auth] - kube-apiserver: Fixed a bug where the `ResourceQuota` admission plugin did not respect any scope changes when a resource was updated, such as setting or unsetting the `terminationGracePeriodSeconds` field of an existing pod. ([#130060](https://github.com/kubernetes/kubernetes/pull/130060), [@carlory](https://github.com/carlory)) [SIG API Machinery, Scheduling and Testing] - kube-apiserver: shortening the grace period during a pod deletion no longer moves the `metadata.deletionTimestamp` into the past ([#122646](https://github.com/kubernetes/kubernetes/pull/122646), [@liggitt](https://github.com/liggitt)) [SIG API Machinery] - kube-proxy: Fixed a potential memory leak that could occur in clusters with a high volume of UDP workflows. ([#130032](https://github.com/kubernetes/kubernetes/pull/130032), [@aroradaman](https://github.com/aroradaman)) - kubeadm: Avoided loading the file passed to `--kubeconfig` during `kubeadm init` phases more than once. ([#129006](https://github.com/kubernetes/kubernetes/pull/129006), [@kokes](https://github.com/kokes)) - kubeadm: fixed a bug where an image is not pulled if there is an error with the sandbox image from CRI. ([#129594](https://github.com/kubernetes/kubernetes/pull/129594), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: fixed a bug where the `node.skipPhases` in UpgradeConfiguration is not respected by the `kubeadm upgrade node` subcommand. ([#129452](https://github.com/kubernetes/kubernetes/pull/129452), [@SataQiu](https://github.com/SataQiu)) - kubeadm: fixed panic when no UpgradeConfiguration was found in the config file. ([#130202](https://github.com/kubernetes/kubernetes/pull/130202), [@SataQiu](https://github.com/SataQiu)) - kubeadm: fixed the bug where the `v1beta4` `Timeouts.EtcdAPICall` field was not respected in etcd client operations, and the default timeout of 2 minutes was always used. ([#129859](https://github.com/kubernetes/kubernetes/pull/129859), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: if an addon is disabled in the ClusterConfiguration, skip it during upgrade. ([#129418](https://github.com/kubernetes/kubernetes/pull/129418), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: make sure that it is possible to health check the kube-apiserver when it has `--anonymous-auth=false` set and the `WaitForAllControlPlaneComponents` feature gate is enabled. ([#131036](https://github.com/kubernetes/kubernetes/pull/131036), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: run kernel version and OS version preflight checks for `kubeadm upgrade`. ([#129401](https://github.com/kubernetes/kubernetes/pull/129401), [@pacoxu](https://github.com/pacoxu)) - Provides an additional function argument to directly specify the version for the tools that the consumers wished to use. ([#129658](https://github.com/kubernetes/kubernetes/pull/129658), [@unmarshall](https://github.com/unmarshall)) - Removed a warning related to Linux user namespaces and kernel version. Previously, if the feature gate `UserNamespacesSupport` was enabled, the kubelet warned when detecting a Linux kernel version earlier than 6.3.0. While user namespace support generally requires kernel 6.3 or newer, it can also work on older kernels. ([#130243](https://github.com/kubernetes/kubernetes/pull/130243), [@rata](https://github.com/rata)) - Removed the limitation on exposing port 10250 externally using a Service. ([#129174](https://github.com/kubernetes/kubernetes/pull/129174), [@RyanAoh](https://github.com/RyanAoh)) [SIG Apps and Network] - Resolved a performance regression in default 1.31+ configurations, related to the `ConsistentListFromCache` feature, where rapid create/update API requests across different namespaces encounter increased latency. ([#130113](https://github.com/kubernetes/kubernetes/pull/130113), [@AwesomePatrol](https://github.com/AwesomePatrol)) - Revised scheduling behavior to correctly handle nominated node changes. Trigger rescheduling of pods if necessary when pods with nominated node names got deleted or nominated on a different node. ([#129058](https://github.com/kubernetes/kubernetes/pull/129058), [@dom4ha](https://github.com/dom4ha)) [SIG Scheduling, Storage and Testing] - The `/flagz` endpoint in kube-apiserver now correctly returns parsed flag values when the `ComponentFlagz` feature-gate is enabled. ([#130328](https://github.com/kubernetes/kubernetes/pull/130328), [@richabanker](https://github.com/richabanker)) [SIG API Machinery and Instrumentation] - The `BalancedAllocation` plugin now skips all best-effort (zero-requested) pods. ([#130260](https://github.com/kubernetes/kubernetes/pull/130260), [@Bowser1704](https://github.com/Bowser1704)) - The following roles have had `Watch` added to them (prefixed with `system:controller:`): - `cronjob-controller` - `endpoint-controller` - `endpointslice-controller` - `endpointslicemirroring-controller` - `horizontal-pod-autoscaler` - `node-controller` - `pod-garbage-collector` - `storage-version-migrator-controller` ([#130405](https://github.com/kubernetes/kubernetes/pull/130405), [@kariya-mitsuru](https://github.com/kariya-mitsuru)) [SIG Auth] - The response from kube-apiserver's `/flagz` endpoint would respond correctly with parsed flags value. ([#129996](https://github.com/kubernetes/kubernetes/pull/129996), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery, Architecture, Instrumentation and Testing] - When `cpu-manager-policy=static` is configured, containers meeting the qualifications for static cpu assignment (i.e. Containers with integer CPU `requests` in pods with `Guaranteed` QOS) will not have cfs quota enforced. Because this fix changes a long-established behavior, users observing a regressions can use the `DisableCPUQuotaWithExclusiveCPUs` feature gate (enabled by default) to restore the previous behavior. Please file an issue if you encounter problems and have to use the Feature Gate. ([#127525](https://github.com/kubernetes/kubernetes/pull/127525), [@scott-grimes](https://github.com/scott-grimes)) [SIG Node and Testing] - When using the Alpha `DRAResourceClaimDeviceStatus` feature, IP address values in the `NetworkDeviceData` are now validated more strictly. ([#129219](https://github.com/kubernetes/kubernetes/pull/129219), [@danwinship](https://github.com/danwinship)) [SIG Network] - YAML input that might previously have been misinterpreted as JSON is now correctly accepted. ([#130666](https://github.com/kubernetes/kubernetes/pull/130666), [@thockin](https://github.com/thockin)) - [kubectl] Improved the describe output for projected volume sources to clearly indicate whether Secret and ConfigMap entries are optional. ([#129457](https://github.com/kubernetes/kubernetes/pull/129457), [@gshaibi](https://github.com/gshaibi)) [SIG CLI] - kube-apiserver: Fixes an issue updating the default ServiceCIDR API object and creating dual-stack Service API objects when `--service-cluster-ip-range` flag passed to kube-apiserver is changed from single-stack to dual-stack. ([#131263](https://github.com/kubernetes/kubernetes/pull/131263), [@aojea](https://github.com/aojea)) [SIG API Machinery, Network and Testing] ### Other (Cleanup or Flake) - 1. kube-apiserver: removed the deprecated the `--cloud-provider` and `--cloud-config` CLI parameters. 2. removed generally available feature-gate `DisableCloudProviders` and `DisableKubeletCloudCredentialProviders` ([#130162](https://github.com/kubernetes/kubernetes/pull/130162), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider, Node and Testing] - Added metrics to capture CPU distribution across NUMA nodes ([#130491](https://github.com/kubernetes/kubernetes/pull/130491), [@swatisehgal](https://github.com/swatisehgal)) [SIG Node and Testing] - Add metrics to track allocation of Uncore (aka last-level aka L3) Cache blocks ([#130133](https://github.com/kubernetes/kubernetes/pull/130133), [@ffromani](https://github.com/ffromani)) [SIG Node and Testing] - Changed the dependency version for CoreDNS. Kubernetes tools now install CoreDNS `v1.12.0`. ([#128926](https://github.com/kubernetes/kubernetes/pull/128926), [@bzsuni](https://github.com/bzsuni)) [SIG Cloud Provider and Cluster Lifecycle] - Changed the error message displayed when a pod is trying to attach a volume that does not match the label/selector from "x node(s) had volume node affinity conflict" to "x node(s) didn't match PersistentVolume's node affinity". ([#129887](https://github.com/kubernetes/kubernetes/pull/129887), [@rhrmo](https://github.com/rhrmo)) [SIG Scheduling and Storage] - `client-gen` now sorts input group/versions to ensure stable output generation even with unsorted inputs ([#130626](https://github.com/kubernetes/kubernetes/pull/130626), [@BenTheElder](https://github.com/BenTheElder)) [SIG API Machinery] - e2e framework: `framework.WithFeatureGate` `[Alpha]`, `[Beta]` and `[Feature:OffByDefault]` tags are now set 1:1 with `Alpha`, `Beta`, `Feature:OffByDefault` Ginkgo labels, replacing`Feature:Alpha` and `Feature:Beta` labels. `BetaOffByDefault` is also added as a Ginkgo label only for off-by-default beta features ([#130908](https://github.com/kubernetes/kubernetes/pull/130908), [@BenTheElder](https://github.com/BenTheElder)) [SIG Testing] - E2e.test: [Feature:OffByDefault] was added to test names when specifying a feature gate that is not enabled by default. ([#130655](https://github.com/kubernetes/kubernetes/pull/130655), [@BenTheElder](https://github.com/BenTheElder)) [SIG Auth and Testing] - Extended the schema of kube-proxy's metrics / endpoints to incorporate information about the corresponding IP family. ([#129173](https://github.com/kubernetes/kubernetes/pull/129173), [@aroradaman](https://github.com/aroradaman)) [SIG Network and Windows] - Fixed a linting issue in `TestNodeDeletionReleaseCIDR`. ([#128856](https://github.com/kubernetes/kubernetes/pull/128856), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps and Network] - Flipped `StorageNamespaceIndex` feature gate to `false` and deprecated it. ([#129933](https://github.com/kubernetes/kubernetes/pull/129933), [@serathius](https://github.com/serathius)) - Implemented logging for failed transactions and the full table in `kube-proxy` with `nftables` when using log level 4 or higher. Logging is rate-limited to one entry every 24 hours to avoid performance issues. ([#128886](https://github.com/kubernetes/kubernetes/pull/128886), [@npinaeva](https://github.com/npinaeva)) - Implemented the `scheduler_cache_size` metric. Additionally, the `scheduler_scheduler_cache_size` metric is now deprecated in favor of `scheduler_cache_size`, and will be removed in v1.34. ([#128810](https://github.com/kubernetes/kubernetes/pull/128810), [@googs1025](https://github.com/googs1025)) - kube-apiserver: Inactive serving code is removed for `authentication.k8s.io/v1alpha1` APIs ([#129186](https://github.com/kubernetes/kubernetes/pull/129186), [@liggitt](https://github.com/liggitt)) [SIG Auth and Testing] - kubeadm: Use generic terminology in logs instead of direct mentions of YAML/JSON ([#130345](https://github.com/kubernetes/kubernetes/pull/130345), [@HirazawaUi](https://github.com/HirazawaUi)) - kubeadm: removed preflight check for `ip`, `iptables`, `ethtool` and `tc` on Linux nodes. kubelet and kube-proxy will continue to report `iptables` errors if its usage is required. The tools `ip`, `ethtool` and `tc` had legacy usage in the kubelet but are no longer required. ([#129131](https://github.com/kubernetes/kubernetes/pull/129131), [@pacoxu](https://github.com/pacoxu)) [SIG Cluster Lifecycle] - kubeadm: removed preflight check for `touch` on Linux nodes. ([#129317](https://github.com/kubernetes/kubernetes/pull/129317), [@carlory](https://github.com/carlory)) [SIG Cluster Lifecycle] - kubelet no longer logs multiple errors when running on a system with no iptables binaries installed. ([#129826](https://github.com/kubernetes/kubernetes/pull/129826), [@danwinship](https://github.com/danwinship)) [SIG Network and Node] - Reduced log verbosity for high-frequency, low-value log entries in Job, IPAM, and ReplicaSet controllers by adjusting them to V(2), V(4) and V(4) respectively. This change minimizes log noise while maintaining access to these logs when needed. ([#130591](https://github.com/kubernetes/kubernetes/pull/130591), [@fmuyassarov](https://github.com/fmuyassarov)) [SIG Apps and Network] - Removed alpha support for Windows HostNetwork containers. ([#130250](https://github.com/kubernetes/kubernetes/pull/130250), [@marosset](https://github.com/marosset)) [SIG Network, Node and Windows] - Removed general available feature gate `PersistentVolumeLastPhaseTransitionTime`. ([#129295](https://github.com/kubernetes/kubernetes/pull/129295), [@carlory](https://github.com/carlory)) [SIG Storage] - Removed general available feature-gate `AppArmor`. ([#129375](https://github.com/kubernetes/kubernetes/pull/129375), [@carlory](https://github.com/carlory)) [SIG Auth and Node] - Removed generally available feature gate `KubeProxyDrainingTerminatingNodes`. ([#129692](https://github.com/kubernetes/kubernetes/pull/129692), [@alexanderConstantinescu](https://github.com/alexanderConstantinescu)) [SIG Network] - Removed generally available feature-gate `AppArmorFields`. ([#129497](https://github.com/kubernetes/kubernetes/pull/129497), [@carlory](https://github.com/carlory)) [SIG Node] - Removed support for `v1alpha1` version of `ValidatingAdmissionPolicy` and `ValidatingAdmissionPolicyBinding` API kinds. ([#129207](https://github.com/kubernetes/kubernetes/pull/129207), [@Jefftree](https://github.com/Jefftree)) [SIG Etcd and Testing] - Removed the `JobPodFailurePolicy` feature gate, which graduated to GA in 1.31 and was unconditionally enabled. ([#129498](https://github.com/kubernetes/kubernetes/pull/129498), [@carlory](https://github.com/carlory)) - Removed the deprecated `pod_scheduling_duration_seconds` metric. Users need to migrate to `pod_scheduling_sli_duration_seconds`. ([#128906](https://github.com/kubernetes/kubernetes/pull/128906), [@sanposhiho](https://github.com/sanposhiho)) [SIG Instrumentation and Scheduling] - Renamed some metrics related to CoreDNS, see the [README](https://github.com/coredns/coredns/blob/v1.11.0/plugin/forward/README.md#metrics) for `v1.11.0` of CoreDNS. ([#129232](https://github.com/kubernetes/kubernetes/pull/129232), [@DamianSawicki](https://github.com/DamianSawicki)) - Show a warning message to inform users that the debug container's capabilities granted by debugging profile may not work as expected if a non-root user is specified in target Pod's `.Spec.SecurityContext.RunAsUser` field. ([#127696](https://github.com/kubernetes/kubernetes/pull/127696), [@mochizuki875](https://github.com/mochizuki875)) [SIG CLI and Testing] - The `SeparateCacheWatchRPC` feature gate is deprecated and disabled by default. ([#129929](https://github.com/kubernetes/kubernetes/pull/129929), [@serathius](https://github.com/serathius)) [SIG API Machinery] - Renamed coredns metrics, see https://github.com/coredns/coredns/blob/v1.11.0/plugin/forward/README.md#metrics. ([#129175](https://github.com/kubernetes/kubernetes/pull/129175), [@DamianSawicki](https://github.com/DamianSawicki)) [SIG Cloud Provider] - Updated CNI plugins to `v1.6.2`. ([#129776](https://github.com/kubernetes/kubernetes/pull/129776), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider, Node and Testing] - Updated cri-tools to `v1.32.0`. ([#129116](https://github.com/kubernetes/kubernetes/pull/129116), [@saschagrunert](https://github.com/saschagrunert)) - Updated the etcd client library to `v3.5.21` ([#131103](https://github.com/kubernetes/kubernetes/pull/131103), [@ahrtr](https://github.com/ahrtr)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Cluster Lifecycle, Etcd, Instrumentation, Network, Node and Storage] - kube-apiserver disables the beta WatchList feature by default in 1.33 in favor of the `StreamingCollectionEncodingToJSON` and `StreamingCollectionEncodingToProtobuf` features.kube-controller-manager no longer opts into enabling the WatchListClient feature in 1.33. ([#131359](https://github.com/kubernetes/kubernetes/pull/131359), [@deads2k](https://github.com/deads2k)) [SIG API Machinery] ## Dependencies ### Added - github.com/containerd/errdefs/pkg: [v0.3.0](https://github.com/containerd/errdefs/tree/pkg/v0.3.0) - github.com/klauspost/compress: [v1.18.0](https://github.com/klauspost/compress/tree/v1.18.0) - github.com/kylelemons/godebug: [v1.1.0](https://github.com/kylelemons/godebug/tree/v1.1.0) - github.com/opencontainers/cgroups: [v0.0.1](https://github.com/opencontainers/cgroups/tree/v0.0.1) - github.com/planetscale/vtprotobuf: [0393e58](https://github.com/planetscale/vtprotobuf/tree/0393e58) - github.com/russross/blackfriday: [v1.6.0](https://github.com/russross/blackfriday/tree/v1.6.0) - github.com/santhosh-tekuri/jsonschema/v5: [v5.3.1](https://github.com/santhosh-tekuri/jsonschema/tree/v5.3.1) - go.opentelemetry.io/auto/sdk: v1.1.0 - gopkg.in/go-jose/go-jose.v2: v2.6.3 - sigs.k8s.io/randfill: v1.0.0 ### Changed - cel.dev/expr: v0.18.0 → v0.19.1 - cloud.google.com/go/compute/metadata: v0.3.0 → v0.5.0 - cloud.google.com/go/compute: v1.25.1 → v1.23.3 - github.com/cilium/ebpf: [v0.16.0 → v0.17.3](https://github.com/cilium/ebpf/compare/v0.16.0...v0.17.3) - github.com/cncf/xds/go: [555b57e → b4127c9](https://github.com/cncf/xds/compare/555b57e...b4127c9) - github.com/containerd/containerd/api: [v1.7.19 → v1.8.0](https://github.com/containerd/containerd/compare/api/v1.7.19...api/v1.8.0) - github.com/containerd/errdefs: [v0.1.0 → v1.0.0](https://github.com/containerd/errdefs/compare/v0.1.0...v1.0.0) - github.com/containerd/ttrpc: [v1.2.5 → v1.2.6](https://github.com/containerd/ttrpc/compare/v1.2.5...v1.2.6) - github.com/containerd/typeurl/v2: [v2.2.0 → v2.2.2](https://github.com/containerd/typeurl/compare/v2.2.0...v2.2.2) - github.com/coredns/corefile-migration: [v1.0.24 → v1.0.25](https://github.com/coredns/corefile-migration/compare/v1.0.24...v1.0.25) - github.com/coreos/go-oidc: [v2.2.1+incompatible → v2.3.0+incompatible](https://github.com/coreos/go-oidc/compare/v2.2.1...v2.3.0) - github.com/cyphar/filepath-securejoin: [v0.3.4 → v0.4.1](https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...v0.4.1) - github.com/davecgh/go-spew: [d8f796a → v1.1.1](https://github.com/davecgh/go-spew/compare/d8f796a...v1.1.1) - github.com/envoyproxy/go-control-plane: [v0.12.0 → v0.13.0](https://github.com/envoyproxy/go-control-plane/compare/v0.12.0...v0.13.0) - github.com/envoyproxy/protoc-gen-validate: [v1.0.4 → v1.1.0](https://github.com/envoyproxy/protoc-gen-validate/compare/v1.0.4...v1.1.0) - github.com/go-logfmt/logfmt: [v0.5.1 → v0.4.0](https://github.com/go-logfmt/logfmt/compare/v0.5.1...v0.4.0) - github.com/golang-jwt/jwt/v4: [v4.5.0 → v4.5.2](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.2) - github.com/golang/glog: [v1.2.1 → v1.2.2](https://github.com/golang/glog/compare/v1.2.1...v1.2.2) - github.com/google/btree: [v1.0.1 → v1.1.3](https://github.com/google/btree/compare/v1.0.1...v1.1.3) - github.com/google/cadvisor: [v0.51.0 → v0.52.1](https://github.com/google/cadvisor/compare/v0.51.0...v0.52.1) - github.com/google/cel-go: [v0.22.0 → v0.23.2](https://github.com/google/cel-go/compare/v0.22.0...v0.23.2) - github.com/google/gnostic-models: [v0.6.8 → v0.6.9](https://github.com/google/gnostic-models/compare/v0.6.8...v0.6.9) - github.com/google/go-cmp: [v0.6.0 → v0.7.0](https://github.com/google/go-cmp/compare/v0.6.0...v0.7.0) - github.com/google/gofuzz: [v1.2.0 → v1.0.0](https://github.com/google/gofuzz/compare/v1.2.0...v1.0.0) - github.com/gorilla/websocket: [v1.5.0 → e064f32](https://github.com/gorilla/websocket/compare/v1.5.0...e064f32) - github.com/grpc-ecosystem/grpc-gateway/v2: [v2.20.0 → v2.24.0](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.20.0...v2.24.0) - github.com/matttproud/golang_protobuf_extensions: [v1.0.2 → v1.0.1](https://github.com/matttproud/golang_protobuf_extensions/compare/v1.0.2...v1.0.1) - github.com/opencontainers/image-spec: [v1.1.0 → v1.1.1](https://github.com/opencontainers/image-spec/compare/v1.1.0...v1.1.1) - github.com/opencontainers/runc: [v1.2.1 → v1.2.5](https://github.com/opencontainers/runc/compare/v1.2.1...v1.2.5) - github.com/pmezard/go-difflib: [5d4384e → v1.0.0](https://github.com/pmezard/go-difflib/compare/5d4384e...v1.0.0) - github.com/prometheus/client_golang: [v1.19.1 → v1.22.0](https://github.com/prometheus/client_golang/compare/v1.19.1...v1.22.0) - github.com/prometheus/common: [v0.55.0 → v0.62.0](https://github.com/prometheus/common/compare/v0.55.0...v0.62.0) - github.com/rogpeppe/go-internal: [v1.12.0 → v1.13.1](https://github.com/rogpeppe/go-internal/compare/v1.12.0...v1.13.1) - github.com/stretchr/testify: [v1.9.0 → v1.10.0](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0) - github.com/vishvananda/netlink: [b1ce50c → 62fb240](https://github.com/vishvananda/netlink/compare/b1ce50c...62fb240) - go.etcd.io/etcd/api/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/client/pkg/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/client/v2: v2.305.16 → v2.305.21 - go.etcd.io/etcd/client/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/pkg/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/raft/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/server/v3: v3.5.16 → v3.5.21 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc: v0.53.0 → v0.58.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp: v0.53.0 → v0.58.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc: v1.27.0 → v1.33.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel/metric: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel/sdk: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel/trace: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel: v1.28.0 → v1.33.0 - go.opentelemetry.io/proto/otlp: v1.3.1 → v1.4.0 - golang.org/x/crypto: v0.28.0 → v0.36.0 - golang.org/x/net: v0.30.0 → v0.38.0 - golang.org/x/oauth2: v0.23.0 → v0.27.0 - golang.org/x/sync: v0.8.0 → v0.12.0 - golang.org/x/sys: v0.26.0 → v0.31.0 - golang.org/x/term: v0.25.0 → v0.30.0 - golang.org/x/text: v0.19.0 → v0.23.0 - golang.org/x/time: v0.7.0 → v0.9.0 - google.golang.org/appengine: v1.6.7 → v1.4.0 - google.golang.org/genproto/googleapis/api: f6391c0 → e6fa225 - google.golang.org/genproto/googleapis/rpc: f6391c0 → e6fa225 - google.golang.org/grpc: v1.65.0 → v1.68.1 - google.golang.org/protobuf: v1.35.1 → v1.36.5 - k8s.io/gengo/v2: 2b36238 → 1244d31 - k8s.io/kube-openapi: 32ad38e → c8a335a - sigs.k8s.io/apiserver-network-proxy/konnectivity-client: v0.31.0 → v0.31.2 - sigs.k8s.io/kustomize/api: v0.18.0 → v0.19.0 - sigs.k8s.io/kustomize/cmd/config: v0.15.0 → v0.19.0 - sigs.k8s.io/kustomize/kustomize/v5: v5.5.0 → v5.6.0 - sigs.k8s.io/kustomize/kyaml: v0.18.1 → v0.19.0 - sigs.k8s.io/structured-merge-diff/v4: v4.4.2 → v4.6.0 ### Removed - github.com/asaskevich/govalidator: [f61b66f](https://github.com/asaskevich/govalidator/tree/f61b66f) - github.com/checkpoint-restore/go-criu/v6: [v6.3.0](https://github.com/checkpoint-restore/go-criu/tree/v6.3.0) - github.com/containerd/console: [v1.0.4](https://github.com/containerd/console/tree/v1.0.4) - github.com/go-kit/log: [v0.2.1](https://github.com/go-kit/log/tree/v0.2.1) - github.com/moby/sys/user: [v0.3.0](https://github.com/moby/sys/tree/user/v0.3.0) - github.com/seccomp/libseccomp-golang: [v0.10.0](https://github.com/seccomp/libseccomp-golang/tree/v0.10.0) - github.com/syndtr/gocapability: [42c35b4](https://github.com/syndtr/gocapability/tree/42c35b4) - github.com/urfave/cli: [v1.22.14](https://github.com/urfave/cli/tree/v1.22.14) - gopkg.in/square/go-jose.v2: v2.6.0 # v1.33.0-rc.1 ## Downloads for v1.33.0-rc.1 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes.tar.gz) | df48dbb829a60a7dc3943781d18a7958f2e2f23ba6ddcb0ca10a085034676c3da4b95cdd75a52618595080d11886bf6518c7e7659bf19a45447ed8666f7eeb79 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-src.tar.gz) | 134b43af462b83dd17b26f71a022e0722490ed47a9e26edf3de2703019d80dd3195c185a0bf4b90d300a6e1fb7dad9c052ad8571fda5bd67dc57162fa7f37046 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-darwin-amd64.tar.gz) | 4d4c60b7d4a78da1793959730416b683d6a5b0a788da4ea5b02e90ca66be36a5bb2bc547499d7effef7f1af5614f23b17feca97a3f807416c0d50121a21c1d5e [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-darwin-arm64.tar.gz) | 082f7cc482fd1f35a3eb7ee2d7b05fe71e69b7ac51263be70a5833b425532ddb8d50e4b988e54d381dde02d54cce4ef2993717ce2e599c8cb0283ef58cce7bdf [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-linux-386.tar.gz) | 07695d62f23d16d1ce338bbc58a08486a6a26dbcd2a46e785b7a3330d920054542ae67305f29deb0dbb800b49237989666384319b7a5caf00baaed08c5af2704 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-linux-amd64.tar.gz) | e59209b458f9da744da137d27b38d0c981f98fd92ef3e5128c45871694baa0fa2e299f6dc4d2330672917926a2173e5d53977d9a63a40010743cb25e3515752d [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-linux-arm.tar.gz) | 99f6ce6bdab4b7cd5a4bef9f2845f5b1f090a23a88ce5f88b4d30cdcf22a790f063fefa079ebb6c9d5c165cc0d126545618b8d725988d75bc02a25ce1a767e72 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-linux-arm64.tar.gz) | eda5296486c1f7122996de4805b5bc188253cc7d59b1074b62893d381bdfd5c03bc7452eb80dd7cb4b37ecc6fd9621c680182149fee7504d6a457e4a6e1f2820 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-linux-ppc64le.tar.gz) | 8cc853477a98e3e0a2bdf01eac06bd467a704435b3a5246ba868aeef3083e64f72a0b98c643ca2e8b64d7cf398cce0706c13208fa37155dadc601b3bca09d9fe [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-linux-s390x.tar.gz) | 73cddab2ef60969f9d517f3e0f0214a737dabdac39858e684edd9d6fccf0be4f5fe43e39983da34945214c536c9b0e338239619c4cf611092b28fe16fe4979ed [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-windows-386.tar.gz) | 494493821c56a59f5bada925751628b0adddd2259cb84e45835cadab0b08d551d8f0ff1cfe17dac15825218c6348f226bb56a117ad4c1484036bcc5df80f52bc [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-windows-amd64.tar.gz) | dc6df14a062787dc33279c4861e784d88abe09d6339308bbfdc99a4a4d344d015a6af2488ee5e1d8c1e10e6b65203c1eb7bddbe3a46051aae3fc6e33b84f02fc [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-client-windows-arm64.tar.gz) | 6693c2fe72d77e5d9671a56204ce0443a8e65993fd5aa12c51ef13eb8ca731eb8ee10d87193c1e5caf5de80c623464108fc027838913c958061752ec0c6b2a23 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-server-linux-amd64.tar.gz) | f313d42b518487b39346572ec408c256db89a3fcb8fc6786ae1d7e9492fb51caf13f15c8e776e7092fa03389474482162bb049cbcbcbd7dac0f0eba017b4296c [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-server-linux-arm64.tar.gz) | 0a6565dcdbb3c1b04aadc690782691338cb1e92154c7acc39f2c9e885f486e183029ed36288a4577649dbe342401f483c0b1332d1485e589ae253c0521ee221d [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-server-linux-ppc64le.tar.gz) | a4144dbe365d7bbcb284a87272aa8ced1ad2d0fecb4c9f140cf061d4bfebb161991c582d5a22584b032dc223dc5b6163fb2f11a1152024ca1c06f498c19c38da [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-server-linux-s390x.tar.gz) | 1956770fd0cfcecad3f21cdea43662850c3bc57f7a95c31898e6ff819c80685665aa2e552ddbc97868579a8d5852c6ef5b1775a26afdb3979dc531a72c68ad2e ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-node-linux-amd64.tar.gz) | ac66b4721650cf88875bf6756c1c56c1652513e789a15236e5188809305e1d5798b9767be69ee2d12a7983ec76c1f711994265e9c499b97e6387bc960e4616ac [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-node-linux-arm64.tar.gz) | 6283be94786119b084b57297edd97ea88aab8a265ae914f3f09e2342afc29662979e569584268873865bcb073799d81b6b6002ba409e071283cc375d7976e647 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-node-linux-ppc64le.tar.gz) | 9b43c2c8d69fa35793d95e35c9e3e8da519ba0a41d161e992c1ac0d58863ac3411c1f74fa2850ed5351b1c435332dac085e985e5189c828bc77960ee8e31e596 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-node-linux-s390x.tar.gz) | 26d030512f008613862ddac638d9ecdde619cd2db9cce1a3928d87cbf26039ebfb641116c679df1d89567e344f33def9f4bea226f4ebf7e2e90ecfd64f229547 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.1/kubernetes-node-windows-amd64.tar.gz) | 97bdd5a97a2fb6ac98d5e3fb410d4dcb9ce10927eaa0ec4a1dc8cc162a4699e536d4dbedaf251c152d8108be29fff4cdab2bff8cdef965f963f40771386cb2aa ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.0-rc.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.0-rc.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.0-rc.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.0-rc.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.0-rc.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.0-rc.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.0-rc.0 ## Changes by Kind ### Bug or Regression - Kube-apiserver: Fixes an issue updating the default ServiceCIDR API object and creating dual-stack Service API objects when `--service-cluster-ip-range` flag passed to kube-apiserver is changed from single-stack to dual-stack ([#131263](https://github.com/kubernetes/kubernetes/pull/131263), [@aojea](https://github.com/aojea)) [SIG API Machinery, Network and Testing] ## Dependencies ### Added _Nothing has changed._ ### Changed - github.com/prometheus/client_golang: [v1.22.0-rc.0 → v1.22.0](https://github.com/prometheus/client_golang/compare/v1.22.0-rc.0...v1.22.0) ### Removed _Nothing has changed._ # v1.33.0-rc.0 ## Downloads for v1.33.0-rc.0 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes.tar.gz) | d2b655a7e31a44ad13a2c55926cc5165c8a637f7d143600f3aa99abf5309930e3a5be5d3870d0445c3e80b601c4f749cca38b330a48024222317f8eabcffeaff [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-src.tar.gz) | e8e69a83dabab08df648ff6bf6e48dba64f5f0dda106507b7211ddeaef0170c2b72b4dcb71919b4dfa1dd76f7b9bdf58b896d294d125b43d5c0683f7c50fb1a4 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-darwin-amd64.tar.gz) | f2f5b712fac5936e3b44fb2e29b90207bc0e3556bdac169714c59435b0f4bc1eb78a62cc4f4171dc95b2cf8d66287a6159c642657af791a3e043c245aa58b09a [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-darwin-arm64.tar.gz) | 02d3984e873e4b5f8c323fc2292b19d1182db6c72bb0b717cff432e38a53c394c41fbfd96bad00f32c8c8b3e972879ea5f30ffef7f711a663f9d0667af21b980 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-linux-386.tar.gz) | e87fba03eb68636cb1bcbfea8965e552969408fbcc5b67d6ca10974d82c56dff697d24821ad53f2b838f562fb526a3b5f95efa3debc9cb3631483842541f5f72 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-linux-amd64.tar.gz) | 2fa497803a414b695c8370cb9d5e33db0f511bea0b1f39b1745f5950015f24ba0214a7734be208c7ad02f9f08e0c5fe8b7a9deba04dc5b12f814768cbc02e6a3 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-linux-arm.tar.gz) | 26f09c5e7fb6e6aea6a1086781f1e5cea3772f86be39e2d30bcc14c1e6f753366f6a93780fac6582b9616675f1f19a85916286c2f6ccc52d144fe1b1ba685fea [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-linux-arm64.tar.gz) | 464b83399ed94d8dd9bedc5fba0223008ee9f4678cd9ac1b71743d04910eab14f242ec58310574188502dab0a97822a3f3fe7ee40fa8bbd0b99c849e957f6bc5 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-linux-ppc64le.tar.gz) | 5928e6edd2dc1f98d17e850e5a0dcfa45f35ee2a4e86dfdc2359a1261ab5a636a065f84a81df591b326e18f652c56c68ebca8284ddaaf0763f808e8ba77e7163 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-linux-s390x.tar.gz) | 0c7dc49d2d6c3c0e776a008299154d27984f1956ee7f148037625a0afe3524cb72e433c8255b4b1c05488e474ee80bbefca20f9b15627ed5972a3c760a8d654b [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-windows-386.tar.gz) | 494d1d46b6d428b4e0490698d572b799b8b370709eaf4d8a4aab76447be76eac8e8c46f9b59eb31053fdfd5ba8a2284f1ccd3cb66d7ab0f8dd97d355ecbd7f06 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-windows-amd64.tar.gz) | 95ffde5b48fc91d72890abc478e36b1063ffc0b349edba586aab57abbda8f0d7bfe14d23d20096104c7f31629f616521a850361750c3180510eff0097eb22470 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-client-windows-arm64.tar.gz) | 0120450c0a9bac222303766abaa6a753199f33c8091f4404f6f43be68521773a82854c289fa59e284b203cdbdfa0290191421cf15f4075065568a00dacb0ab86 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-server-linux-amd64.tar.gz) | 6b86eed5db2fdce818aff8e86dc7487c02c7730889598457ec8b2f857dc311be7057eba0e2446f1d51c42ffc5a1b6db0d663fa8f610a5a84acad070dc0eb0d7b [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-server-linux-arm64.tar.gz) | 6a97e527af8d364fa544faee8bd693c8c4d1a610c84bcd4f409cef7885d56f49e510a097afee6befb3e8e368527c3d5a11fa45577b11b11ca880492eb11674f1 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-server-linux-ppc64le.tar.gz) | 85cfb1ab014f1e0e8ee3898825afdb3ec3ca153b8a01b4d9030d14fcb42ab75834a86dcede1bfd3c6f92bb1a95aebc4e13c250c2b4e36a13d2f8c627bbc2b28e [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-server-linux-s390x.tar.gz) | d76304fe4fd9b72e515efebe266b655338a7e8dda9ae53f3b425ad19db7c8b8af2d8004841c442619319f863f34a14e8a158a1c6d0197af5693a19362d95a712 ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-node-linux-amd64.tar.gz) | 75602088f4aa4ca9ab63cf56583cc4d5e8a6cc7c23f6e0f2267c9f340dedc29012b076526ac766ecbefd2bb68ae5ce11e4c31afbb22c78308507d58e40c3fd37 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-node-linux-arm64.tar.gz) | e431a3aa998dda22e91c1ae47f6b943eae6c1aaab9df65c54e4e0062f7d27b8caabe374685d54f736badbfdf80ee4eb1fcc33675bf5f2c83f3aa0ae621aed622 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-node-linux-ppc64le.tar.gz) | dc6a0ed9f08b89e8b837a7318a7887f39734e01ebbfc07fc684f0d097d6613f77b357e77fb92119a02f48568659a38221fee7ebbe6ebc832ad99858708bf2d69 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-node-linux-s390x.tar.gz) | 8c46e82057a7e63d6f1cd772b6796a6c331ee2d2bff08994af5b0d1d30e50f98c2c26e1c9350d44fb244829ad09c17e09fc4bd351fbfe70362b5eb8ef916c6d4 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-rc.0/kubernetes-node-windows-amd64.tar.gz) | 412e868d57e1dc2c595dd1b4a016805ceec8f9186aa6b6a52dbd121730179f18663325253dca344cb0ae012f1df2da7c8ca004bc28503b4918e4f52cf6d65daf ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.0-rc.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.0-rc.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.0-rc.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.0-rc.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.0-rc.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.0-rc.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.0-beta.0 ## Urgent Upgrade Notes ### (No, really, you MUST read this before you upgrade) - Added the ability to reduce both the initial delay and the maximum delay accrued between container restarts for a node for containers in `CrashLoopBackOff` across the cluster to the recommended values of `1s` initial delay and `60s` maximum delay. To set this for a node, turn on the feature gate `ReduceDefaultCrashLoopBackOffDecay`. If you are also using the feature gate `KubeletCrashLoopBackOffMax` with a configured per-node `CrashLoopBackOff.MaxContainerRestartPeriod`, the effective kubelet configuration will follow the conflict resolution policy described further in the documentation [here](TODO:link). ([#130711](https://github.com/kubernetes/kubernetes/pull/130711), [@lauralorenz](https://github.com/lauralorenz)) [SIG Node and Testing] ## Changes by Kind ### Deprecation - The EndpointSlice `hints` field has graduated to GA. The beta annotation `service.kubernetes.io/topology-mode` is now considered deprecated and will not graduate to GA. It remains operational for backward compatibility. Users are encouraged to use the `spec.trafficDistribution` field in the Service API for topology-aware routing configuration. ([#130742](https://github.com/kubernetes/kubernetes/pull/130742), [@gauravkghildiyal](https://github.com/gauravkghildiyal)) [SIG Network] - The `StorageCapacityScoring` feature gate was added to score nodes by available storage capacity. It's in alpha and disabled by default. The `VolumeCapacityPriority` alpha feature was replaced with this, and the default behavior was changed. The `VolumeCapacityPriority` preferred a node with the least allocatable, but the `StorageCapacityScoring` preferred a node with the maximum allocatable. See [KEP-4049](https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/4049-storage-capacity-scoring-of-nodes-for-dynamic-provisioning/README.md) for details. ([#128184](https://github.com/kubernetes/kubernetes/pull/128184), [@cupnes](https://github.com/cupnes)) [SIG Scheduling, Storage and Testing] - The pod `status.resize` field is now deprecated and will no longer be set. The status of a pod resize will be exposed under two new conditions: `PodResizeInProgress` and `PodResizePending` instead. ([#130733](https://github.com/kubernetes/kubernetes/pull/130733), [@natasha41575](https://github.com/natasha41575)) [SIG API Machinery, Apps, CLI, Node, Scheduling and Testing] ### API Change - A new alpha feature gate, `MutableCSINodeAllocatableCount`, has been introduced. When this feature gate is enabled, the `CSINode.Spec.Drivers[*].Allocatable.Count` field becomes mutable, and a new field, `NodeAllocatableUpdatePeriodSeconds`, is available in the `CSIDriver` object. This allows periodic updates to a node's reported allocatable volume capacity, preventing stateful pods from becoming stuck due to outdated information that kube-scheduler relies on. ([#130007](https://github.com/kubernetes/kubernetes/pull/130007), [@torredil](https://github.com/torredil)) [SIG Apps, Node, Scheduling and Storage] - Add feature gate `DRAPartitionableDevices`, when enabled, Dynamic Resource Allocation support partitionable devices allocation. ([#130764](https://github.com/kubernetes/kubernetes/pull/130764), [@cici37](https://github.com/cici37)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Cluster Lifecycle, Instrumentation, Network, Node, Scheduling, Storage and Testing] - Added a /flagz endpoint for kubelet endpoint ([#128857](https://github.com/kubernetes/kubernetes/pull/128857), [@zhifei92](https://github.com/zhifei92)) [SIG Architecture, Instrumentation and Node] - Added a new 'tolerance' field to HorizontalPodAutoscaler, overriding the cluster-wide default. Enabled via the HPAConfigurableTolerance alpha feature gate. ([#130797](https://github.com/kubernetes/kubernetes/pull/130797), [@jm-franc](https://github.com/jm-franc)) [SIG API Machinery, Apps, Autoscaling, Etcd, Node, Scheduling and Testing] - Added support for configuring custom stop signals with a new StopSignal container lifecycle ([#130556](https://github.com/kubernetes/kubernetes/pull/130556), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) [SIG API Machinery, Apps, Node and Testing] - CPUManager Policy Options support is GA ([#130535](https://github.com/kubernetes/kubernetes/pull/130535), [@ffromani](https://github.com/ffromani)) [SIG API Machinery, Node and Testing] - Changed the Pod API to support `hugepage resources` at `spec` level for pod-level resources. ([#130577](https://github.com/kubernetes/kubernetes/pull/130577), [@KevinTMtz](https://github.com/KevinTMtz)) [SIG Apps, CLI, Node, Scheduling, Storage and Testing] - DRA: Device taints enable DRA drivers or admins to mark device as unusable, which prevents allocating them. Pods may also get evicted at runtime if a device becomes unusable, depending on the severity of the taint and whether the claim tolerates the taint. ([#130447](https://github.com/kubernetes/kubernetes/pull/130447), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Architecture, Auth, Etcd, Instrumentation, Node, Scheduling and Testing] - DRA: Starting Kubernetes 1.33, only users with access to an admin namespace with the `kubernetes.io/dra-admin-access` label are authorized to create ResourceClaim or ResourceClaimTemplate objects with the `adminAccess` field in this admin namespace if they want to and only they can reference these ResourceClaims or ResourceClaimTemplates in their pod or deployment specs. ([#130225](https://github.com/kubernetes/kubernetes/pull/130225), [@ritazh](https://github.com/ritazh)) [SIG API Machinery, Apps, Auth, Node and Testing] - Expanded the on-disk kubelet credential provider configuration to allow an optional `tokenAttribute` field to be configured. When it is set, the Kubelet will provision a token with the given audience bound to the current pod and its service account. This KSA token along with required annotations on the KSA defined in configuration will be sent to the credential provider plugin via its standard input (along with the image information that is already sent today). The KSA annotations to be sent are configurable in the kubelet credential provider configuration. ([#128372](https://github.com/kubernetes/kubernetes/pull/128372), [@aramase](https://github.com/aramase)) [SIG API Machinery, Auth, Node and Testing] - Fixed the example validation rule in godoc: When configuring a JWT authenticator: If username.expression uses 'claims.email', then 'claims.email_verified' must be used in username.expression or extra[*].valueExpression or claimValidationRules[*].expression. An example claim validation rule expression that matches the validation automatically applied when username.claim is set to 'email' is 'claims.?email_verified.orValue(true) == true'. By explicitly comparing the value to true, we let type-checking see the result will be a boolean, and to make sure a non-boolean `email_verified` claim will be caught at runtime. ([#130875](https://github.com/kubernetes/kubernetes/pull/130875), [@aramase](https://github.com/aramase)) [SIG Auth and Release] - For the InPlacePodVerticalScaling feature, the API server will no longer set the resize status to `Proposed` upon receiving a resize request. ([#130574](https://github.com/kubernetes/kubernetes/pull/130574), [@natasha41575](https://github.com/natasha41575)) [SIG Apps, Node and Testing] - Graduate the MatchLabelKeys (MismatchLabelKeys) feature in PodAffinity (PodAntiAffinity) to GA ([#130463](https://github.com/kubernetes/kubernetes/pull/130463), [@sanposhiho](https://github.com/sanposhiho)) [SIG API Machinery, Apps, Node, Scheduling and Testing] - Graduated image volume sources to beta: - Allowed `subPath`/`subPathExpr` for image volumes - Added kubelet metrics `kubelet_image_volume_requested_total`, `kubelet_image_volume_mounted_succeed_total` and `kubelet_image_volume_mounted_errors_total` ([#130135](https://github.com/kubernetes/kubernetes/pull/130135), [@saschagrunert](https://github.com/saschagrunert)) [SIG API Machinery, Apps, Node and Testing] - Improved how the API server responds to **list** requests where the response format negotiates to Protobuf. List responses in Protobuf are marshalled one element at the time, drastically reducing memory needed to serve large collections. Streaming list responses can be disabled via the `StreamingCollectionEncodingToProtobuf` feature gate. ([#129407](https://github.com/kubernetes/kubernetes/pull/129407), [@serathius](https://github.com/serathius)) [SIG API Machinery, Apps, Architecture, Auth, CLI, Cloud Provider, Network, Node, Release, Scheduling, Storage and Testing] - Introduced API type coordination.k8s.io/v1beta1/LeaseCandidate CoordinatedLeaderElection feature is Beta ([#130751](https://github.com/kubernetes/kubernetes/pull/130751), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery, Etcd and Testing] - It introduces a new scope name `VolumeAttributesClass`. It matches all PVC objects that have the volume attributes class mentioned. If you want to limit the count of PVCs that have a specific volume attributes class. In that case, you can create a quota object with the scope name `VolumeAttributesClass` and a matchExpressions that match the volume attributes class. ([#124360](https://github.com/kubernetes/kubernetes/pull/124360), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Testing] - Kubelet: add KubeletConfiguration.subidsPerPod ([#130028](https://github.com/kubernetes/kubernetes/pull/130028), [@AkihiroSuda](https://github.com/AkihiroSuda)) [SIG API Machinery and Node] - New configuration is introduced to the kubelet that allows it to track container images and the list of authentication information that lead to their successful pulls . This data is persisted across reboots of the host and restarts of the kubelet. The kubelet ensures any image requiring credential verification is always pulled if authentication information from an image pull is not yet present, thus enforcing authentication / re-authentication. This means an image pull might be attempted even in cases where a pod requests the `IfNotPresent` image pull policy, and might lead to the pod not starting if its pull policy is `Never` and is unable to present authentication information that lead to a previous successful pull of the image it is requesting. ([#128152](https://github.com/kubernetes/kubernetes/pull/128152), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Architecture, Auth, Node and Testing] - Promote JobSuccessPolicy E2E to Conformance ([#130658](https://github.com/kubernetes/kubernetes/pull/130658), [@tenzen-y](https://github.com/tenzen-y)) [SIG API Machinery, Apps, Architecture and Testing] - Promote NodeInclusionPolicyInPodTopologySpread to Stable in v1.33 ([#130920](https://github.com/kubernetes/kubernetes/pull/130920), [@kerthcet](https://github.com/kerthcet)) [SIG Apps, Node, Scheduling and Testing] - Promote the JobSuccessPolicy to Stable. ([#130536](https://github.com/kubernetes/kubernetes/pull/130536), [@tenzen-y](https://github.com/tenzen-y)) [SIG API Machinery, Apps, Architecture and Testing] - Removed general available feature gate `CPUManager`. ([#129296](https://github.com/kubernetes/kubernetes/pull/129296), [@carlory](https://github.com/carlory)) [SIG API Machinery, Node and Testing] - Start reporting swap capacity as part of node.status.nodeSystemInfo. ([#129954](https://github.com/kubernetes/kubernetes/pull/129954), [@iholder101](https://github.com/iholder101)) [SIG API Machinery, Apps and Node] - The ClusterTrustBundle API is moving to v1beta1. In order for the ClusterTrustBundleProjection feature to work on the kubelet side, the ClusterTrustBundle API must be available at v1beta1 version and the ClusterTrustBundleProjection feature gate must be enabled. If the API becomes later after kubelet started running, restart the kubelet to enable the feature. ([#128499](https://github.com/kubernetes/kubernetes/pull/128499), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Apps, Auth, Etcd, Node, Storage and Testing] - The Service trafficDistribution field, including the PreferClose option, has graduated to GA. Services that do not have the field configured will continue to operate with their existing behavior. Refer to the documentation https://kubernetes.io/docs/concepts/services-networking/service/#traffic-distribution for more details. ([#130673](https://github.com/kubernetes/kubernetes/pull/130673), [@gauravkghildiyal](https://github.com/gauravkghildiyal)) [SIG Apps, Network and Testing] - The feature gate InPlacePodVerticalScalingAllocatedStatus is deprecated and no longer used. The AllocatedResources field in ContainerStatus is now guarded by the InPlacePodVerticalScaling feature gate. ([#130880](https://github.com/kubernetes/kubernetes/pull/130880), [@tallclair](https://github.com/tallclair)) [SIG CLI, Node and Scheduling] - The kube-controller-manager will set the `observedGeneration` field on pod conditions when the `PodObservedGenerationTracking` feature gate is set. ([#130650](https://github.com/kubernetes/kubernetes/pull/130650), [@natasha41575](https://github.com/natasha41575)) [SIG API Machinery, Apps, Node, Scheduling, Storage, Testing and Windows] - The kube-scheduler will set the `observedGeneration` field on pod conditions when the `PodObservedGenerationTracking` feature gate is set. ([#130649](https://github.com/kubernetes/kubernetes/pull/130649), [@natasha41575](https://github.com/natasha41575)) [SIG Node, Scheduling and Testing] - The kubelet will set the `observedGeneration` field on pod conditions when the `PodObservedGenerationTracking` feature gate is set. ([#130573](https://github.com/kubernetes/kubernetes/pull/130573), [@natasha41575](https://github.com/natasha41575)) [SIG Apps, Node, Scheduling, Storage, Testing and Windows] - The minimum value validation of ReplicationController's `replicas` and `minReadySeconds` fields have been migrated to declarative validation. The requiredness of both fields is also declaratively validated. If the `DeclarativeValidation` feature gate is enabled, mismatches with existing validation are reported via metrics. If the `DeclarativeValidationTakeover` feature gate is enabled, declarative validation is the primary source of errors for migrated fields. ([#130725](https://github.com/kubernetes/kubernetes/pull/130725), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery, Apps, Architecture, CLI, Cluster Lifecycle, Instrumentation, Network, Node and Storage] - The resource.k8s.io/v1beta1 API is deprecated and will be removed in 1.36. Use v1beta2 instead. ([#129970](https://github.com/kubernetes/kubernetes/pull/129970), [@mortent](https://github.com/mortent)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] - Validation now requires new StatefulSets with a `.spec.serviceName` field value to pass DNS1123 validation. Previously created StatefulSets with an invalid `.spec.serviceName` field value could not create any pods, and should be deleted. - Published OpenAPI for the StatefulSet schema is corrected to indicate the `.spec.serviceName` is optional. ([#130233](https://github.com/kubernetes/kubernetes/pull/130233), [@soltysh](https://github.com/soltysh)) [SIG API Machinery, Apps and Testing] - When the `ImprovedTrafficDistribution` feature gate is enabled, a new `trafficDistribution` value `PreferSameNode` is available, which attempts to always route Service connections to an endpoint on the same node as the client. Additionally, `PreferSameZone` is introduced as an alias for `PreferClose`. ([#130844](https://github.com/kubernetes/kubernetes/pull/130844), [@danwinship](https://github.com/danwinship)) [SIG API Machinery, Apps, Network and Windows] - When the `StrictIPCIDRValidation` feature gate is enabled, Kubernetes will be slightly stricter about what values will be accepted as IP addresses and network address ranges (“CIDR blocks”). In particular, octets within IPv4 addresses are not allowed to have any leading `0`s, and IPv4-mapped IPv6 values (e.g. `::ffff:192.168.0.1`) are forbidden. These sorts of values can potentially cause security problems when different components interpret the same string as referring to different IP addresses (as in CVE-2021-29923). This tightening applies only to fields in build-in API kinds, and not to custom resource kinds, values in Kubernetes configuration files, or command-line arguments. (When the feature gate is disabled, creating an object with such an invalid IP or CIDR value will result in a warning from the API server about the fact that it will be rejected in the future.) ([#122550](https://github.com/kubernetes/kubernetes/pull/122550), [@danwinship](https://github.com/danwinship)) [SIG API Machinery, Apps, Network, Node, Scheduling and Testing] - `apidiscovery.k8s.io/v2beta1` API group is disabled by default ([#130347](https://github.com/kubernetes/kubernetes/pull/130347), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery and Testing] ### Feature - Add ListFromCacheSnapshot feature gate that allows apiserver to serve LISTs with exact RV and continuations from cache ([#130423](https://github.com/kubernetes/kubernetes/pull/130423), [@serathius](https://github.com/serathius)) [SIG API Machinery, Etcd and Testing] - Add Pressure Stall Information (PSI) metrics to node metrics. ([#130701](https://github.com/kubernetes/kubernetes/pull/130701), [@roycaihw](https://github.com/roycaihw)) [SIG Node and Testing] - Add Windows Server, Version 2025 for windows-servercore-cache test image ([#130935](https://github.com/kubernetes/kubernetes/pull/130935), [@aramase](https://github.com/aramase)) [SIG Testing and Windows] - Add metrics to expose the main known reasons for resource alingment errors ([#129950](https://github.com/kubernetes/kubernetes/pull/129950), [@ffromani](https://github.com/ffromani)) [SIG Node and Testing] - Added SchedulerPopFromBackoffQ feature gate that is in beta and enabled by default. Improved scheduling queue behavior by popping pods from the backoffQ when the activeQ is empty. This allows to process potentially schedulable pods ASAP, eliminating a penalty effect of the backoff queue. ([#130772](https://github.com/kubernetes/kubernetes/pull/130772), [@macsko](https://github.com/macsko)) [SIG Scheduling and Testing] - Added a new cli flag "--emulation-forward-compatible" Added a new cli flag "--runtime-config-emulation-forward-compatible" ([#130354](https://github.com/kubernetes/kubernetes/pull/130354), [@siyuanfoundation](https://github.com/siyuanfoundation)) [SIG API Machinery, Etcd and Testing] - Added a new option `strict-cpu-reservation` for CPU Manager static policy. When this option is enabled, CPU cores in `reservedSystemCPUs` will be strictly used for system daemons and interrupt processing no longer available for any workload. ([#130290](https://github.com/kubernetes/kubernetes/pull/130290), [@psasnal](https://github.com/psasnal)) [SIG Node and Testing] - Adding resource completion in kubectl debug command ([#130033](https://github.com/kubernetes/kubernetes/pull/130033), [@ardaguclu](https://github.com/ardaguclu)) [SIG CLI] - Adds a /flagz endpoint for kube-controller-manager endpoint ([#128824](https://github.com/kubernetes/kubernetes/pull/128824), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery and Instrumentation] - Automatically copy `topology.k8s.io/zone`, `topology.k8s.io/region` and `kubernetes.io/hostname` labels from Node objects to Pods when they are scheduled to a node (via the `pods/binding` endpoint) to allow applications that need to be explicitly aware of their assigned node topology to access this information via the downward API, rather than requiring permission to `get node` objects (exposing the entire API surface of the Node object to otherwise unprivileged workloads). ([#127092](https://github.com/kubernetes/kubernetes/pull/127092), [@munnerz](https://github.com/munnerz)) [SIG API Machinery, Node and Testing] - Bump ProcMountType feature to on by default beta ([#130798](https://github.com/kubernetes/kubernetes/pull/130798), [@haircommander](https://github.com/haircommander)) [SIG Node] - DRA: Starting Kubernetes 1.33, regular users with namespaced cluster `edit` role assigned have `read` permission to `resourceclaims`, `resourceclaims/status`,`resourceclaimtemplates`. And `write` permission for `resourceclaims`, `resourceclaimtemplates`. ([#130738](https://github.com/kubernetes/kubernetes/pull/130738), [@ritazh](https://github.com/ritazh)) [SIG Auth] - DRAResourceClaimDeviceStatus is now turned on by default allowing DRA-Drivers to report device status data for each allocated device. ([#130814](https://github.com/kubernetes/kubernetes/pull/130814), [@LionelJouin](https://github.com/LionelJouin)) [SIG Network and Node] - Disabled git-repo volume plugin by default, with the option to turn it back on by setting feature-gate GitRepoVolumeDriver=true. ([#129923](https://github.com/kubernetes/kubernetes/pull/129923), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) [SIG Storage] - DistributeCPUsAcrossNUMA policy option is promoted to Beta. ([#130541](https://github.com/kubernetes/kubernetes/pull/130541), [@swatisehgal](https://github.com/swatisehgal)) [SIG Node] - Errors returned by apiserver from uninitialized cache will include last error from etcd ([#130899](https://github.com/kubernetes/kubernetes/pull/130899), [@serathius](https://github.com/serathius)) [SIG API Machinery and Testing] - Errors that occur during pod resize actuation will be surfaced in the `PodResizeInProgress` condition. ([#130902](https://github.com/kubernetes/kubernetes/pull/130902), [@natasha41575](https://github.com/natasha41575)) [SIG Node] - Graduate the `WinDSR` feature in the kube-proxy to beta. The `WinDSR` feature gate is now enabled by default. ([#130876](https://github.com/kubernetes/kubernetes/pull/130876), [@rzlink](https://github.com/rzlink)) [SIG Windows] - Graduate the asynchronous preemption feature in the scheduler to beta. Now the feature flag (SchedulerAsyncPreemption) is enabled by default. ([#130550](https://github.com/kubernetes/kubernetes/pull/130550), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Graduated the `DisableNodeKubeProxyVersion` feature gate to enable by default, the kubelet no longer attempts to set the `.status.kubeProxyVersion` field for its associated Node. ([#129713](https://github.com/kubernetes/kubernetes/pull/129713), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Node] - If scheduling fails on PreBind or Bind, scheduler will retry the failed pod immediately after backoff time, regardless of the reason for failing. In this case EventsToRegister (QHints) will not be taken into consideration before retry. ([#130189](https://github.com/kubernetes/kubernetes/pull/130189), [@ania-borowiec](https://github.com/ania-borowiec)) [SIG Scheduling] - KEP-3619: fined-grained supplemental groups policy is graduated to Beta. Note that kubelet now rejects pods with `.spec.securityContext.supplementalGroupsPolicy: Strict` when scheduled to the node that does not support the feature (`.status.features.supplementalGroupsPolicy: false`). ([#130210](https://github.com/kubernetes/kubernetes/pull/130210), [@everpeace](https://github.com/everpeace)) [SIG Apps, Node and Testing] - Kube-apiserver: the `StorageObjectInUseProtection` admission plugin added the `kubernetes.io/vac-protection` finalizer to the given VolumeAttributesClass object when it is created if the feature-gate `VolumeAttributesClass` is turned on and `storage.k8s.io/v1beta1` is enabled. ([#130553](https://github.com/kubernetes/kubernetes/pull/130553), [@Phaow](https://github.com/Phaow)) [SIG Storage and Testing] - Kubelet + DRA: For DRA driver plugins (and only for those!), the kubelet now supports a rolling update with `maxSurge > 0` in the driver's DaemonSet. A DRA driver must support this, which can be done via the k8s.io/dynamic-resource-allocation/kubeletplugin helper package. ([#129832](https://github.com/kubernetes/kubernetes/pull/129832), [@pohly](https://github.com/pohly)) [SIG Node, Storage and Testing] - PodLifecycleSleepAction is now turned on by default allowing users to create containers with sleep lifecycle action with a duration of zero seconds ([#130621](https://github.com/kubernetes/kubernetes/pull/130621), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) [SIG Node] - Promoted in-place Pod vertical scaling to beta. The `InPlacePodVerticalScaling` feature gate is now enabled by default. ([#130905](https://github.com/kubernetes/kubernetes/pull/130905), [@tallclair](https://github.com/tallclair)) [SIG Node] - Respect the incoming trace context for authenticated requests to the kube-apiserver for APIServer tracing. ([#127053](https://github.com/kubernetes/kubernetes/pull/127053), [@dashpole](https://github.com/dashpole)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Instrumentation, Network, Node and Testing] - SELinuxChangePolicy and SELinuxMount graduated to Beta. SELinuxMount stays off by default. ([#130544](https://github.com/kubernetes/kubernetes/pull/130544), [@jsafrane](https://github.com/jsafrane)) [SIG Auth, Node and Storage] - The RemoteRequestHeaderUID feature moves to beta and is now enabled by default. This makes the kube-apiserver propagate UIDs in the `X-Remote-Uid` header in requests to the aggregated API servers. The header is not honored by default for incoming requests, but that can be enabled by setting the `--requestheader-uid-headers` flag explicitly. ([#130560](https://github.com/kubernetes/kubernetes/pull/130560), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Auth and Testing] - The `DeclarativeValidation` feature gate is enabled by default. When enabled, mismatches with existing hand written validation is reported via metrics. The `DeclarativeValidationTakeover` feature gate remains disabled by default. While disabled, validation errors produced by hand written validation are always return to the caller. To switch to declarative validation is primary source of errors for migrated fields, enable this feature gate. ([#130728](https://github.com/kubernetes/kubernetes/pull/130728), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery] - Update /version response to report binary version information separate from compatibility version ([#130019](https://github.com/kubernetes/kubernetes/pull/130019), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery, Architecture, Release and Testing] - User namespaces support (feature gate UserNamespacesSupport) is enabled by default. If you want to use it, please check the documentation for the node requirements. ([#130138](https://github.com/kubernetes/kubernetes/pull/130138), [@rata](https://github.com/rata)) [SIG Node and Testing] ### Bug or Regression - Disable InPlace Pod Resize for Swap enabled containers that does not have memory ResizePolicy as RestartContainer ([#130831](https://github.com/kubernetes/kubernetes/pull/130831), [@ajaysundark](https://github.com/ajaysundark)) [SIG Node and Testing] - Fix a bug where kube-apiserver could emit an further watch even even if decryption failed for earlier event and it was not emitted. ([#131020](https://github.com/kubernetes/kubernetes/pull/131020), [@wojtek-t](https://github.com/wojtek-t)) [SIG API Machinery and Etcd] - Fixed an issue where pods did not correctly have a Pending phase after the node reboot. ([#128516](https://github.com/kubernetes/kubernetes/pull/128516), [@gjkim42](https://github.com/gjkim42)) [SIG Node and Testing] - Fixed compressed kubelet log file permissions to use uncompressed kubelet log file permissions. ([#129893](https://github.com/kubernetes/kubernetes/pull/129893), [@simonfogliato](https://github.com/simonfogliato)) [SIG Node] - Includes WebSockets HTTPS proxy support ([#129872](https://github.com/kubernetes/kubernetes/pull/129872), [@seans3](https://github.com/seans3)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Instrumentation, Network and Node] - Kubeadm: make sure that it is possible to health check the kube-apiserver when it has --anonymous-auth=false set and the WaitForAllControlPlaneComponents feature gate is enabled. ([#131036](https://github.com/kubernetes/kubernetes/pull/131036), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Revised scheduling behavior to correctly handle nominated node changes. Trigger rescheduling of pods if necessary when pods with nominated node names got deleted or nominated on a different node. ([#129058](https://github.com/kubernetes/kubernetes/pull/129058), [@dom4ha](https://github.com/dom4ha)) [SIG Scheduling, Storage and Testing] ### Other (Cleanup or Flake) - Add metrics to capture CPU distribution across NUMA nodes ([#130491](https://github.com/kubernetes/kubernetes/pull/130491), [@swatisehgal](https://github.com/swatisehgal)) [SIG Node and Testing] - Add metrics to track allocation of Uncore (aka last-level aka L3) Cache blocks ([#130133](https://github.com/kubernetes/kubernetes/pull/130133), [@ffromani](https://github.com/ffromani)) [SIG Node and Testing] - Client-gen now sorts input group/versions to ensure stable output generation even with unsorted inputs ([#130626](https://github.com/kubernetes/kubernetes/pull/130626), [@BenTheElder](https://github.com/BenTheElder)) [SIG API Machinery] - E2e framework: `framework.WithFeatureGate` `[Alpha]`, `[Beta]` and `[Feature:OffByDefault]` tags are now set 1:1 with `Alpha`, `Beta`, `Feature:OffByDefault` Ginkgo labels, replacing`Feature:Alpha` and `Feature:Beta` labels. `BetaOffByDefault` is also added as a Ginkgo label only for off-by-default beta features ([#130908](https://github.com/kubernetes/kubernetes/pull/130908), [@BenTheElder](https://github.com/BenTheElder)) [SIG Testing] - Reduced log verbosity for high-frequency, low-value log entries in Job, IPAM, and ReplicaSet controllers by adjusting them to V(2), V(4) and V(4) respectively. This change minimizes log noise while maintaining access to these logs when needed. ([#130591](https://github.com/kubernetes/kubernetes/pull/130591), [@fmuyassarov](https://github.com/fmuyassarov)) [SIG Apps and Network] - Removed alpha support for Windows HostNetwork containers. ([#130250](https://github.com/kubernetes/kubernetes/pull/130250), [@marosset](https://github.com/marosset)) [SIG Network, Node and Windows] - Removed general available feature gate `PersistentVolumeLastPhaseTransitionTime`. ([#129295](https://github.com/kubernetes/kubernetes/pull/129295), [@carlory](https://github.com/carlory)) [SIG Storage] - Show a warning message to inform users that the debug container's capabilities granted by debugging profile may not work as expected if a non-root user is specified in target Pod's `.Spec.SecurityContext.RunAsUser` field. ([#127696](https://github.com/kubernetes/kubernetes/pull/127696), [@mochizuki875](https://github.com/mochizuki875)) [SIG CLI and Testing] - Updates the etcd client library to v3.5.21 ([#131103](https://github.com/kubernetes/kubernetes/pull/131103), [@ahrtr](https://github.com/ahrtr)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Cluster Lifecycle, Etcd, Instrumentation, Network, Node and Storage] ## Dependencies ### Added _Nothing has changed._ ### Changed - github.com/golang-jwt/jwt/v4: [v4.5.1 → v4.5.2](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2) - github.com/gorilla/websocket: [v1.5.3 → e064f32](https://github.com/gorilla/websocket/compare/v1.5.3...e064f32) - go.etcd.io/etcd/api/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/client/pkg/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/client/v2: v2.305.16 → v2.305.21 - go.etcd.io/etcd/client/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/pkg/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/raft/v3: v3.5.16 → v3.5.21 - go.etcd.io/etcd/server/v3: v3.5.16 → v3.5.21 - golang.org/x/crypto: v0.35.0 → v0.36.0 - golang.org/x/net: v0.33.0 → v0.38.0 - golang.org/x/sync: v0.11.0 → v0.12.0 - golang.org/x/sys: v0.30.0 → v0.31.0 - golang.org/x/term: v0.29.0 → v0.30.0 - golang.org/x/text: v0.22.0 → v0.23.0 - k8s.io/kube-openapi: e5f78fe → c8a335a ### Removed _Nothing has changed._ # v1.33.0-beta.0 ## Downloads for v1.33.0-beta.0 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes.tar.gz) | 53a7e0e0ad351ca0cfb99ca3258835cd9356dd10df3dc9737dc3ef08510b8afc0eafcac503b6168c24c13bbd1a93f9a06508b5b5c5c5ec2f45e31f86012409e0 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-src.tar.gz) | 56d380d07e265c18f4b86e294b3944f330892588bd62301f8827ce726afd1e9d5e7335bc0c939c3a6297d2e4f5132c82d048858024718b10ff11c6e8d2c40cc9 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-darwin-amd64.tar.gz) | 3ae3b1bc58812ce8a2a1e3ca0014c15b00e3f4edcc72d7cbaa50cede697384d8765f5bb49aac0d5b786295528dc1b07f0135931cfda4f48e33022640fb3c6b7a [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-darwin-arm64.tar.gz) | 4cb64d8f647c454f1c2c640077724237dcf056b5c2c461e0c5022650667ceb34e013d0727d4ef6d129502c094776501dbab68e993e96a2c6697cde212b42723e [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-linux-386.tar.gz) | ff44a2e6622c25c89430006dba64d1e40b78dfe88445d7af35ec7e92979fe880c009130694a9163266fda771fe1da9e0ebcbe9735b3591b0344c2e67513d996a [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-linux-amd64.tar.gz) | 429c1b3838e0a7ce0c6f89b36589c8de64ca83ae1a197097feb1c19dbd9241f03648b590c8a57fa0c1ab1bcda769c46c2c562846bfe924317e86dba117f422b2 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-linux-arm.tar.gz) | aaa2d51b539d269e2b1ec89f5c6308afe23bd13f766fad6e949f424c5db2002f2400dedab8cea6922339c920414de66a16fbd5d752e518982ec501cb803c0339 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-linux-arm64.tar.gz) | 4591f8bdb027fe2eb52652335834777cb0ce509ff5643877724746210747ff3ca1d3b62b41d7c93e05dc9e30923e32e3cbe8fac856deccda2e958ed638b60e0f [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-linux-ppc64le.tar.gz) | 55c3d7b37929af918bb29f304bb94dd21e21cd50b920290b4309a11d1507c9f3ca7c0506e6e23f94b9503da593d727fb136bdcb12d2da8766b993655107cadeb [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-linux-s390x.tar.gz) | c7c1ba2071957e963d9d0824e061af0752489a9fcf9c2a601ce6d66dedbbc5f0f02c14c72cd16c48092024d71996a83bd59a2d01377fa537ff6093bef518e3fa [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-windows-386.tar.gz) | b2823da3f55a47940b3238e5b8d276e1ee6af6b10ffbd972ae1299da38f39e36b45772234f57d81c27759a4add33d486c29d8efc32deb779aba703fe3230a5cd [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-windows-amd64.tar.gz) | f5de40e2b5596f40cf59422ed22a448e1d397a11c73de4b1694b04c235bbb2538bd5bf705efb99dc5d7cf24da3d935f7530bbc8680180a9967de4bc341d745d3 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-client-windows-arm64.tar.gz) | 3c24e08b0634465bd7910389be63a09b9b750529c076e7759c9100c07dbeb9dd4d0caa0f871bd776261cec88059aecdf29e6dbbbabaf357b79cbcf620ee1b0d8 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-server-linux-amd64.tar.gz) | 8e2c99d48ecc0b806208a983837026943916580ccd2911362b4af5b3aa45e16703f18262fd64d81844854b06f7025c543a6964cc0b3455b5c300e099773c2847 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-server-linux-arm64.tar.gz) | 61688ad68057dd4c7f7f41206dfb558407a7317cdfdb33d305d81c08e93a0f5e11efbd68e10e12aeb7cc873550bbd822f270167ef2208f877bdb8db58f12f14b [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-server-linux-ppc64le.tar.gz) | 4349666887f862bd45bca0d0488128b33107e3f3ebc69cf9c67dbefbd3539431c4e3ff944b8e6948ad0896bbc9c7a99ac9242d478ee44d052a49d8519e9cc017 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-server-linux-s390x.tar.gz) | 351c6345cf88079c124d4f1a1401528d2c5ba1d8bc24ad16c49ef237890ff7090436224c631f5e4fe9a0a9a0a439e983f883854f24a1f96caeed5e9f12522e11 ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-node-linux-amd64.tar.gz) | e59bf9f26252f94bca19967b1816db3071f0936c1716c58381b4ec0601b16aa050b07404fe40accf17b978d0f08ceddf859e333ff0ba3982a9c161e5b165526a [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-node-linux-arm64.tar.gz) | a407c77a47a7fa38dd1cbf926e9b3b43a46e4dc46598b55cbc7dfa6073b7f6e429f2ba3c2e2d0c2cf8dcf51afb98c108d46986ed06c41a53973e8792d32c20a3 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-node-linux-ppc64le.tar.gz) | 33312107574b1a6403b63851c65b660f37a0086a7f143843d3877f8974081fb4076063b48bc2e0b0f6733690a2edf00c3f704fabc80903fd8f07690a9d86f52d [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-node-linux-s390x.tar.gz) | c253042a95cac403026ac69a304d0a41c36fa210d89c164f81b6388bd695720cf2b143b9543d79c965a5939a116aecffef2476b3f4888f6ab8da27bcd37529e3 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-beta.0/kubernetes-node-windows-amd64.tar.gz) | d6ef20e8f5fd6378065354c461221e879f16f90de58ea7c5662efe7981d10949031e986ff01dd719ad8d7e267491d8dba9fdfd2166265a87b863f9771241000f ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.0-beta.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.0-beta.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.0-beta.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.0-beta.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.0-beta.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.0-beta.0](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.0-alpha.3 ## Changes by Kind ### API Change - DRA support for a "one-of" prioritized list of selection criteria to satisfy a device request in a resource claim. ([#128586](https://github.com/kubernetes/kubernetes/pull/128586), [@mortent](https://github.com/mortent)) [SIG API Machinery, Apps, Etcd, Node, Scheduling and Testing] - For the InPlacePodVerticalScaling feature, the API server will no longer set the resize status to `Proposed` upon receiving a resize request. ([#130574](https://github.com/kubernetes/kubernetes/pull/130574), [@natasha41575](https://github.com/natasha41575)) [SIG Apps, Node and Testing] - The apiserver will now return warnings if you create objects with "invalid" IP or CIDR values (like "192.168.000.005", which should not have the extra zeros). Values with non-standard formats can introduce security problems, and will likely be forbidden in a future Kubernetes release. ([#128786](https://github.com/kubernetes/kubernetes/pull/128786), [@danwinship](https://github.com/danwinship)) [SIG Apps, Network and Node] - When the `PodObservedGenerationTracking` feature gate is set, the kubelet will populate `status.observedGeneration` to reflect the pod's latest `metadata.generation` that it has observed. ([#130352](https://github.com/kubernetes/kubernetes/pull/130352), [@natasha41575](https://github.com/natasha41575)) [SIG API Machinery, Apps, CLI, Node, Release, Scheduling, Storage, Testing and Windows] ### Feature - Add mechanism that every 5 minutes calculates a digest of etcd and watch cache and exposes it as `apiserver_storage_digest` metric ([#130475](https://github.com/kubernetes/kubernetes/pull/130475), [@serathius](https://github.com/serathius)) [SIG API Machinery, Instrumentation and Testing] - Adds apiserver.latency.k8s.io/authentication annotation to the audit log to record the time spent authenticating slow requests. - Adds apiserver.latency.k8s.io/authorization annotation to the audit log to record the time spent authorizing slow requests. ([#130571](https://github.com/kubernetes/kubernetes/pull/130571), [@hakuna-matatah](https://github.com/hakuna-matatah)) [SIG Auth] - Allow for dynamic configuration of service account name and audience kubelet can request a token for as part of the node audience restriction feature. ([#130485](https://github.com/kubernetes/kubernetes/pull/130485), [@aramase](https://github.com/aramase)) [SIG Auth and Testing] - Endpoints resources created by the Endpoints controller now have a label indicating this. (Users creating Endpoints by hand _can_ also add this label themselves, but they ought to switch to creating EndpointSlices rather than Endpoints anyway.) ([#130564](https://github.com/kubernetes/kubernetes/pull/130564), [@danwinship](https://github.com/danwinship)) [SIG Apps and Network] - Pod resource checkpointing is now tracked by the `allocated_pods_state` and `actuated_pods_state` files, and `pod_status_manager_state` is no longer used. ([#130599](https://github.com/kubernetes/kubernetes/pull/130599), [@tallclair](https://github.com/tallclair)) [SIG Node] - Scheduling Framework exposes NodeInfo to the ScorePlugin. ([#130537](https://github.com/kubernetes/kubernetes/pull/130537), [@saintube](https://github.com/saintube)) [SIG Scheduling, Storage and Testing] - Set feature gate `OrderedNamespaceDeletion` on by default. ([#130507](https://github.com/kubernetes/kubernetes/pull/130507), [@cici37](https://github.com/cici37)) [SIG API Machinery and Apps] ### Bug or Regression - Fix a bug on InPlacePodVerticalScalingExclusiveCPUs feature gate exclusive assignment availability check. ([#130559](https://github.com/kubernetes/kubernetes/pull/130559), [@esotsal](https://github.com/esotsal)) [SIG Node] - Fix kubelet restart unmounts volumes of running pods if the referenced PVC is being deleted by the user ([#130335](https://github.com/kubernetes/kubernetes/pull/130335), [@carlory](https://github.com/carlory)) [SIG Node, Storage and Testing] - Removed a warning around Linux user namespaces and kernel version. If the feature gate `UserNamespacesSupport` was enabled, the kubelet previously warned when detecting a Linux kernel version earlier than 6.3.0. User namespace support on Linux typically does still need kernel 6.3 or newer, but it can work in older kernels too. ([#130243](https://github.com/kubernetes/kubernetes/pull/130243), [@rata](https://github.com/rata)) [SIG Node] - The BalancedAllocation plugin will skip all best-effort (zero-requested) pod. ([#130260](https://github.com/kubernetes/kubernetes/pull/130260), [@Bowser1704](https://github.com/Bowser1704)) [SIG Scheduling] - YAML input which might previously have been confused for JSON is now accepted. ([#130666](https://github.com/kubernetes/kubernetes/pull/130666), [@thockin](https://github.com/thockin)) [SIG API Machinery] ### Other (Cleanup or Flake) - Changed the error message displayed when a pod is trying to attach a volume that does not match the label/selector from "x node(s) had volume node affinity conflict" to "x node(s) didn't match PersistentVolume's node affinity". ([#129887](https://github.com/kubernetes/kubernetes/pull/129887), [@rhrmo](https://github.com/rhrmo)) [SIG Scheduling and Storage] - Client-gen now sorts input group/versions to ensure stable output generation even with unsorted inputs ([#130626](https://github.com/kubernetes/kubernetes/pull/130626), [@BenTheElder](https://github.com/BenTheElder)) [SIG API Machinery] - E2e.test: [Feature:OffByDefault] is added to test names when specifying a featuregate which is not on by default ([#130655](https://github.com/kubernetes/kubernetes/pull/130655), [@BenTheElder](https://github.com/BenTheElder)) [SIG Auth and Testing] - Kubelet no longer logs multiple errors when running on a system with no iptables binaries installed. ([#129826](https://github.com/kubernetes/kubernetes/pull/129826), [@danwinship](https://github.com/danwinship)) [SIG Network and Node] ## Dependencies ### Added - github.com/containerd/errdefs/pkg: [v0.3.0](https://github.com/containerd/errdefs/tree/pkg/v0.3.0) - github.com/klauspost/compress: [v1.18.0](https://github.com/klauspost/compress/tree/v1.18.0) - github.com/kylelemons/godebug: [v1.1.0](https://github.com/kylelemons/godebug/tree/v1.1.0) - github.com/opencontainers/cgroups: [v0.0.1](https://github.com/opencontainers/cgroups/tree/v0.0.1) - github.com/russross/blackfriday: [v1.6.0](https://github.com/russross/blackfriday/tree/v1.6.0) - github.com/santhosh-tekuri/jsonschema/v5: [v5.3.1](https://github.com/santhosh-tekuri/jsonschema/tree/v5.3.1) - sigs.k8s.io/randfill: v1.0.0 ### Changed - cloud.google.com/go/compute: v1.25.1 → v1.23.3 - github.com/cilium/ebpf: [v0.16.0 → v0.17.3](https://github.com/cilium/ebpf/compare/v0.16.0...v0.17.3) - github.com/containerd/containerd/api: [v1.7.19 → v1.8.0](https://github.com/containerd/containerd/compare/api/v1.7.19...api/v1.8.0) - github.com/containerd/errdefs: [v0.1.0 → v1.0.0](https://github.com/containerd/errdefs/compare/v0.1.0...v1.0.0) - github.com/containerd/ttrpc: [v1.2.5 → v1.2.6](https://github.com/containerd/ttrpc/compare/v1.2.5...v1.2.6) - github.com/containerd/typeurl/v2: [v2.2.0 → v2.2.2](https://github.com/containerd/typeurl/compare/v2.2.0...v2.2.2) - github.com/cyphar/filepath-securejoin: [v0.3.5 → v0.4.1](https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.4.1) - github.com/go-logfmt/logfmt: [v0.5.1 → v0.4.0](https://github.com/go-logfmt/logfmt/compare/v0.5.1...v0.4.0) - github.com/google/cadvisor: [v0.51.0 → v0.52.1](https://github.com/google/cadvisor/compare/v0.51.0...v0.52.1) - github.com/google/go-cmp: [v0.6.0 → v0.7.0](https://github.com/google/go-cmp/compare/v0.6.0...v0.7.0) - github.com/google/gofuzz: [v1.2.0 → v1.0.0](https://github.com/google/gofuzz/compare/v1.2.0...v1.0.0) - github.com/matttproud/golang_protobuf_extensions: [v1.0.2 → v1.0.1](https://github.com/matttproud/golang_protobuf_extensions/compare/v1.0.2...v1.0.1) - github.com/opencontainers/image-spec: [v1.1.0 → v1.1.1](https://github.com/opencontainers/image-spec/compare/v1.1.0...v1.1.1) - github.com/opencontainers/runc: [v1.2.1 → v1.2.5](https://github.com/opencontainers/runc/compare/v1.2.1...v1.2.5) - github.com/prometheus/client_golang: [v1.19.1 → v1.22.0-rc.0](https://github.com/prometheus/client_golang/compare/v1.19.1...v1.22.0-rc.0) - github.com/prometheus/common: [v0.55.0 → v0.62.0](https://github.com/prometheus/common/compare/v0.55.0...v0.62.0) - golang.org/x/time: v0.7.0 → v0.9.0 - google.golang.org/appengine: v1.6.7 → v1.4.0 - google.golang.org/protobuf: v1.35.2 → v1.36.5 - k8s.io/kube-openapi: 2c72e55 → e5f78fe - sigs.k8s.io/structured-merge-diff/v4: v4.4.2 → v4.6.0 ### Removed - github.com/checkpoint-restore/go-criu/v6: [v6.3.0](https://github.com/checkpoint-restore/go-criu/tree/v6.3.0) - github.com/containerd/console: [v1.0.4](https://github.com/containerd/console/tree/v1.0.4) - github.com/go-kit/log: [v0.2.1](https://github.com/go-kit/log/tree/v0.2.1) - github.com/moby/sys/user: [v0.3.0](https://github.com/moby/sys/tree/user/v0.3.0) - github.com/seccomp/libseccomp-golang: [v0.10.0](https://github.com/seccomp/libseccomp-golang/tree/v0.10.0) - github.com/syndtr/gocapability: [42c35b4](https://github.com/syndtr/gocapability/tree/42c35b4) - github.com/urfave/cli: [v1.22.14](https://github.com/urfave/cli/tree/v1.22.14) # v1.33.0-alpha.3 ## Downloads for v1.33.0-alpha.3 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes.tar.gz) | 52751abcbaac8786aa52a8687c6c7d72c6aaa1a8e837ce873ecd66503a92a35c09fd01e85543240c5b51d0c9f97fd374a0dec64fea4acdda6e65b0b6bc183202 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-src.tar.gz) | cbd9967ec5bc31c509f8f9f09a6b40d977c30454554eb743e4c2da92382451fd1d8fae6f011738bccb11fc158fe8f31cc0ddf8d91be298ffa69da8a569c7ef3e ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-darwin-amd64.tar.gz) | 849b061df1d8cc4a727977329504476023bf4c4f4c4ea4b274e914874e3e960eb7b96f96d6377b582e0f17c44bb87aee72b494040dac0b3316f5761c0ad0f227 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-darwin-arm64.tar.gz) | 910c2f6df7bb8fb901db21f6ddd7e8ec3831cd9f195282139443535e8d590505a56bdf28a3414b3e8438b1ecf716b06b660713e6ed61a907bb381dee1b1391a7 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-linux-386.tar.gz) | 202d4af420be89c295facaf5604da05aed1716c8b8f4b81d9e408aaf56cb74593a652fa6b0d45c9c491e12a5c3fadd5eb1aa5988ec5b2d4975e2feb864df5327 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-linux-amd64.tar.gz) | d5bb5bea82ff07540188e0844454a40313752aae99c1dccba54673cb9155b22d8734b3500d83e93b5d59e44b173be666f40a5471927037fa90653b9f7e11f725 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-linux-arm.tar.gz) | 3cd086b710581dd40a0f6a3449b820a9a98a0721099d43844e2764a1d05ef4f62f3232bbbae7d65b63d9c0c994d8bdbba033c1042406beefea483c8358a9c29a [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-linux-arm64.tar.gz) | d3a65059addbf899bab1551b3eed78e2b3ef5222b01d041e6bad31454e8e7e05f7f3ae5650691b726bd2bbf8896fd9699f788aa939e1f27089d3fe4cfcccf8cc [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-linux-ppc64le.tar.gz) | 4fdbc47cbae8fe3f23cc0b42157542e98cadcf82056d9a36c239d6fd720afcbf307b3b01734893c62235ee39618a76a947ae821e12de87d4eb18d22b4bd93bfb [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-linux-s390x.tar.gz) | 5202a5b8afbb0685b370cc0d6866b7a8fe9ece8cc586052af538e438a38019b648c0c4f7b30834529c74f04f9ce740d057d7af77c144ba8b71755895dccd9866 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-windows-386.tar.gz) | 6841c68ae7281e7d0352a14123e9ffa06ea70b3d467184718f2894a2062c986b8d42f0d3446508ebe5c3128148592119664bccaad06f8f8dd04424185a7e8911 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-windows-amd64.tar.gz) | da49d82906d57efb03268a2df299eca13e73e33936bb0b15fe5ff6f93037e06818d7710200d5a69e911b361db3009094a05a022beee2fabe05cae744d13e62b3 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-client-windows-arm64.tar.gz) | 2e047a38d94083c2a89b848fa8b9877ee083ac973cd97fbaaa0a6cc05f46a9ca21b6b5769478843f3239c5d4f8ed343b77a30ab6c4d8f84e5f60569b754a93a9 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-server-linux-amd64.tar.gz) | 5c4849fb85141d8cc1e327a567c74650914cdee92d39e5a8513dacc0afe4424986e899eae6fbe6160eedf9bb5102921634330598a10ab41c20f370e07b5d8de0 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-server-linux-arm64.tar.gz) | a3db2fb73e65237181a92a908d713e033bd9c8be98ab538aea6f86945ad4b402c9b36b8496f69815667c50b3ab44b5c6a5f50a91d253a3b6e7964939cb59e8c1 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-server-linux-ppc64le.tar.gz) | f7593d0e205e4797b635cdff3b20e25b90981dd5403930fb6c4be3c99143bc37eda5f7c45bdf6088e4d61625f10b25fd3b5d0b4d1b2d460b47764b28645f395f [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-server-linux-s390x.tar.gz) | f0105558d1f31f710482e367d61a76e2c97207d36a74d16e3bf7b94c0482a867f551fcc505cd228768606a42a97e4620df2863fbeca0a737387a34734e7ae553 ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-node-linux-amd64.tar.gz) | fe4fdad8e3f0bb159fa8ad1d1fc2952d650d49e2e517b053636cc2969ba605f2c5258d468bfd2ac02f580d6d462f17aa98136a198c58dc56cb0fbd4ca53745ec [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-node-linux-arm64.tar.gz) | faaa6f5bcd238729b12557ac27f99741557921daa6bdbfca6c78f8f84390117cd6f378e41ed4e7afa57bba08b1f5e361a6984d299b8f4ed88c8c39890a0a03cd [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-node-linux-ppc64le.tar.gz) | 6a46ece235a5496c82ceaaf052bc07126ec6c3154cffa0a5c1ade77fd7edd445ed75692e1937a46384cad67800e9ea37b09ae8d342be0625155e4a5f0451f569 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-node-linux-s390x.tar.gz) | 06450e76910f8342cd2d48cdba5c7d5b1f2b5e4faa744d6b43184df2a127701bd626e90d15e7621bd1a0749ffa6581dae13eb651798f3023d0fae7f30907e9d8 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.3/kubernetes-node-windows-amd64.tar.gz) | 852b94df5a79fdf7d96e3fccfde67cb238bae90aa56b70271d545c158693c0437777caa62b2381e98520f510e9124ace9c9126cafd225e5cb5ca30c9099867ce ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.0-alpha.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.0-alpha.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.0-alpha.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.0-alpha.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.0-alpha.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.0-alpha.3](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.0-alpha.2 ## Urgent Upgrade Notes ### (No, really, you MUST read this before you upgrade) - The behavior of the KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK environment variable has been fixed in the nftables proxier. The kernel version check is only skipped when this variable is explicitly set to a non-empty value. If you need to skip the check, set the KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK environment variable. ([#130401](https://github.com/kubernetes/kubernetes/pull/130401), [@ryota-sakamoto](https://github.com/ryota-sakamoto)) [SIG Network] ## Changes by Kind ### Deprecation - The v1 Endpoints API is now officially deprecated (though still fully supported). The API will not be removed, but all users should use the EndpointSlice API instead. ([#130098](https://github.com/kubernetes/kubernetes/pull/130098), [@danwinship](https://github.com/danwinship)) [SIG API Machinery and Network] ### API Change - InPlacePodVerticalScaling: Memory limits cannot be decreased unless the memory resize restart policy is set to `RestartContainer`. Container resizePolicy is no longer mutable. ([#130183](https://github.com/kubernetes/kubernetes/pull/130183), [@tallclair](https://github.com/tallclair)) [SIG Apps and Node] - Introduced API type coordination.k8s.io/v1beta1/LeaseCandidate ([#130291](https://github.com/kubernetes/kubernetes/pull/130291), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery, Etcd and Testing] - KEP-3857: Recursive Read-only (RRO) mounts: promote to GA ([#130116](https://github.com/kubernetes/kubernetes/pull/130116), [@AkihiroSuda](https://github.com/AkihiroSuda)) [SIG Apps, Node and Testing] - MergeDefaultEvictionSettings indicates that defaults for the evictionHard, evictionSoft, evictionSoftGracePeriod, and evictionMinimumReclaim fields should be merged into values specified for those fields in this configuration. Signals specified in this configuration take precedence. Signals not specified in this configuration inherit their defaults. ([#127577](https://github.com/kubernetes/kubernetes/pull/127577), [@vaibhav2107](https://github.com/vaibhav2107)) [SIG API Machinery and Node] - Promote the Job's JobBackoffLimitPerIndex feature-gate to stable. ([#130061](https://github.com/kubernetes/kubernetes/pull/130061), [@mimowo](https://github.com/mimowo)) [SIG API Machinery, Apps, Architecture and Testing] - Promoted the feature gate `AnyVolumeDataSource` to GA. ([#129770](https://github.com/kubernetes/kubernetes/pull/129770), [@sunnylovestiramisu](https://github.com/sunnylovestiramisu)) [SIG Apps, Storage and Testing] ### Feature - Added a `/statusz` endpoint for kube-scheduler ([#128987](https://github.com/kubernetes/kubernetes/pull/128987), [@Henrywu573](https://github.com/Henrywu573)) [SIG Instrumentation, Scheduling and Testing] - Added a alpha feature gate `OrderedNamespaceDeletion`. When enabled, the pods resources are deleted before all other resources while namespace deletion to ensure workload security. ([#130035](https://github.com/kubernetes/kubernetes/pull/130035), [@cici37](https://github.com/cici37)) [SIG API Machinery, Apps and Testing] - Allow ImageVolume for Restricted PSA profiles ([#130394](https://github.com/kubernetes/kubernetes/pull/130394), [@Barakmor1](https://github.com/Barakmor1)) [SIG Auth] - Changed metadata management for Pods to populate `.metadata.generation` on writes. New pods will have a `metadata.generation` of 1; updates to mutable fields in the Pod `.spec` will result in `metadata.generation` being incremented by 1. ([#130181](https://github.com/kubernetes/kubernetes/pull/130181), [@natasha41575](https://github.com/natasha41575)) [SIG Apps, Node and Testing] - Extended the kube-apiserver loopback client certificate validity to 14 months to align with the updated Kubernetes support lifecycle. ([#130047](https://github.com/kubernetes/kubernetes/pull/130047), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG API Machinery and Auth] - Improved how the API server responds to **list** requests where the response format negotiates to JSON. List responses in JSON are marshalled one element at the time, drastically reducing memory needed to serve large collections. Streaming list responses can be disabled via the `StreamingJSONListEncoding` feature gate. ([#129334](https://github.com/kubernetes/kubernetes/pull/129334), [@serathius](https://github.com/serathius)) [SIG API Machinery, Architecture and Release] - Kubernetes is now built with go 1.24.0 ([#129688](https://github.com/kubernetes/kubernetes/pull/129688), [@cpanato](https://github.com/cpanato)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Cluster Lifecycle, Instrumentation, Network, Node, Release, Scheduling, Storage and Testing] - Promote RelaxedDNSSearchValidation to beta, allowing for Pod search domains to be a single dot "." or contain an underscore "_" ([#130128](https://github.com/kubernetes/kubernetes/pull/130128), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps and Network] - Promoted the `CRDValidationRatcheting` feature gate to GA in 1.33 ([#130013](https://github.com/kubernetes/kubernetes/pull/130013), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery] - Promoted the feature gate `HonorPVReclaimPolicy` to GA. ([#129583](https://github.com/kubernetes/kubernetes/pull/129583), [@carlory](https://github.com/carlory)) [SIG Apps, Storage and Testing] - Promotes kubectl --subresource flag to stable. ([#130238](https://github.com/kubernetes/kubernetes/pull/130238), [@soltysh](https://github.com/soltysh)) [SIG CLI] - Various controllers that write out IP address or CIDR values to API objects now ensure that they always write out the values in canonical form. ([#130101](https://github.com/kubernetes/kubernetes/pull/130101), [@danwinship](https://github.com/danwinship)) [SIG Apps, Network and Node] ### Bug or Regression - Add progress tracking to volumes permission and ownership change ([#130398](https://github.com/kubernetes/kubernetes/pull/130398), [@gnufied](https://github.com/gnufied)) [SIG Node and Storage] - Bugfix for Events that fail to be created when the referenced object name is not a valid Event name, using an UUID as name instead of the referenced object name and the timestamp suffix. ([#129790](https://github.com/kubernetes/kubernetes/pull/129790), [@aojea](https://github.com/aojea)) [SIG API Machinery] - CSI drivers that calls IsLikelyNotMountPoint should not assume false means that the path is a mount point. Each CSI driver needs to make sure correct usage of return value of IsLikelyNotMountPoint because if the file is an irregular file but not a mount point is acceptable ([#129370](https://github.com/kubernetes/kubernetes/pull/129370), [@andyzhangx](https://github.com/andyzhangx)) [SIG Storage and Windows] - Fix very rare and sporadic network issues when the host is under heavy load by adding retries for interrupted netlink calls ([#130256](https://github.com/kubernetes/kubernetes/pull/130256), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Network] - Fixed an issue in register-gen where imports for k8s.io/apimachinery/pkg/runtime and k8s.io/apimachinery/pkg/runtime/schema were missing. ([#129307](https://github.com/kubernetes/kubernetes/pull/129307), [@LionelJouin](https://github.com/LionelJouin)) [SIG API Machinery] - Fixes a 1.32 regression starting pods with postStart hooks specified ([#129946](https://github.com/kubernetes/kubernetes/pull/129946), [@alex-petrov-vt](https://github.com/alex-petrov-vt)) [SIG API Machinery] - Fixes a 1.32 regression where nodes may fail to report status and renew serving certificates after the kubelet restarts ([#130348](https://github.com/kubernetes/kubernetes/pull/130348), [@aojea](https://github.com/aojea)) [SIG Node] - Fixes an issue in the CEL CIDR library where subnets contained within another CIDR were incorrectly rejected as not contained ([#130450](https://github.com/kubernetes/kubernetes/pull/130450), [@JoelSpeed](https://github.com/JoelSpeed)) [SIG API Machinery] - Kube-apiserver: Fix a bug where the `ResourceQuota` admission plugin does not respect ANY scope change when a resource is being updated. i.e., to set/unset an existing pod's `terminationGracePeriodSeconds` field. ([#130060](https://github.com/kubernetes/kubernetes/pull/130060), [@carlory](https://github.com/carlory)) [SIG API Machinery, Scheduling and Testing] - Kube-apiserver: shortening the grace period during a pod deletion no longer moves the metadata.deletionTimestamp into the past ([#122646](https://github.com/kubernetes/kubernetes/pull/122646), [@liggitt](https://github.com/liggitt)) [SIG API Machinery] - Kube-proxy, when using a Service with External or LoadBalancer IPs on UDP services , was consuming a large amount of CPU because it was not filtering by the Service destination port and trying to delete all the UDP entries associated to the service. ([#130484](https://github.com/kubernetes/kubernetes/pull/130484), [@aojea](https://github.com/aojea)) [SIG Network] - Kubeadm: fix panic when no UpgradeConfiguration was found in the config file ([#130202](https://github.com/kubernetes/kubernetes/pull/130202), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - The following roles have had `Watch` added to them (prefixed with `system:controller:`): - `cronjob-controller` - `endpoint-controller` - `endpointslice-controller` - `endpointslicemirroring-controller` - `horizontal-pod-autoscaler` - `node-controller` - `pod-garbage-collector` - `storage-version-migrator-controller` ([#130405](https://github.com/kubernetes/kubernetes/pull/130405), [@kariya-mitsuru](https://github.com/kariya-mitsuru)) [SIG Auth] - The response from kube-apiserver /flagz endpoint would respond correctly with parsed flags value when the feature-gate ComponentFlagz is enabled ([#130328](https://github.com/kubernetes/kubernetes/pull/130328), [@richabanker](https://github.com/richabanker)) [SIG API Machinery and Instrumentation] - When using the Alpha DRAResourceClaimDeviceStatus feature, IP address values in the NetworkDeviceData are now validated more strictly. ([#129219](https://github.com/kubernetes/kubernetes/pull/129219), [@danwinship](https://github.com/danwinship)) [SIG Network] ### Other (Cleanup or Flake) - 1. kube-apiserver: removed the deprecated the `--cloud-provider` and `--cloud-config` CLI parameters. 2. removed generally available feature-gate `DisableCloudProviders` and `DisableKubeletCloudCredentialProviders` ([#130162](https://github.com/kubernetes/kubernetes/pull/130162), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider, Node and Testing] - Changed the error message displayed when a pod is trying to attach a volume that does not match the label/selector from "x node(s) had volume node affinity conflict" to "x node(s) didn't match PersistentVolume's node affinity". ([#129887](https://github.com/kubernetes/kubernetes/pull/129887), [@rhrmo](https://github.com/rhrmo)) [SIG Scheduling and Storage] - Kubeadm: Use generic terminology in logs instead of direct mentions of yaml/json. ([#130345](https://github.com/kubernetes/kubernetes/pull/130345), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle] - Remove the JobPodFailurePolicy feature gate that graduated to GA in 1.31 and was unconditionally enabled. ([#129498](https://github.com/kubernetes/kubernetes/pull/129498), [@carlory](https://github.com/carlory)) [SIG Apps] - Removed general available feature-gate `AppArmor`. ([#129375](https://github.com/kubernetes/kubernetes/pull/129375), [@carlory](https://github.com/carlory)) [SIG Auth and Node] - Removed generally available feature-gate `AppArmorFields`. ([#129497](https://github.com/kubernetes/kubernetes/pull/129497), [@carlory](https://github.com/carlory)) [SIG Node] ## Dependencies ### Added - github.com/planetscale/vtprotobuf: [0393e58](https://github.com/planetscale/vtprotobuf/tree/0393e58) - go.opentelemetry.io/auto/sdk: v1.1.0 ### Changed - cloud.google.com/go/compute/metadata: v0.3.0 → v0.5.0 - github.com/cncf/xds/go: [555b57e → b4127c9](https://github.com/cncf/xds/compare/555b57e...b4127c9) - github.com/envoyproxy/go-control-plane: [v0.12.0 → v0.13.0](https://github.com/envoyproxy/go-control-plane/compare/v0.12.0...v0.13.0) - github.com/envoyproxy/protoc-gen-validate: [v1.0.4 → v1.1.0](https://github.com/envoyproxy/protoc-gen-validate/compare/v1.0.4...v1.1.0) - github.com/golang/glog: [v1.2.1 → v1.2.2](https://github.com/golang/glog/compare/v1.2.1...v1.2.2) - github.com/gorilla/websocket: [v1.5.0 → v1.5.3](https://github.com/gorilla/websocket/compare/v1.5.0...v1.5.3) - github.com/grpc-ecosystem/grpc-gateway/v2: [v2.20.0 → v2.24.0](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.20.0...v2.24.0) - github.com/rogpeppe/go-internal: [v1.12.0 → v1.13.1](https://github.com/rogpeppe/go-internal/compare/v1.12.0...v1.13.1) - github.com/stretchr/testify: [v1.9.0 → v1.10.0](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0) - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc: v0.53.0 → v0.58.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp: v0.53.0 → v0.58.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc: v1.27.0 → v1.33.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel/metric: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel/sdk: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel/trace: v1.28.0 → v1.33.0 - go.opentelemetry.io/otel: v1.28.0 → v1.33.0 - go.opentelemetry.io/proto/otlp: v1.3.1 → v1.4.0 - golang.org/x/crypto: v0.31.0 → v0.35.0 - golang.org/x/oauth2: v0.23.0 → v0.27.0 - golang.org/x/sync: v0.10.0 → v0.11.0 - golang.org/x/sys: v0.28.0 → v0.30.0 - golang.org/x/term: v0.27.0 → v0.29.0 - golang.org/x/text: v0.21.0 → v0.22.0 - google.golang.org/genproto/googleapis/api: f6391c0 → e6fa225 - google.golang.org/genproto/googleapis/rpc: f6391c0 → e6fa225 - google.golang.org/grpc: v1.65.0 → v1.68.1 - google.golang.org/protobuf: v1.35.1 → v1.35.2 - k8s.io/gengo/v2: 2b36238 → 1244d31 - sigs.k8s.io/apiserver-network-proxy/konnectivity-client: v0.31.1 → v0.31.2 ### Removed _Nothing has changed._ # v1.33.0-alpha.2 ## Downloads for v1.33.0-alpha.2 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes.tar.gz) | ee13af765b25d466423e51cea5359effb1a095b9033032040bca8569a372656ab27ec38b8b9a4a85a7256f6390c33c0cb7d145ce876ccf282cdf5b3224560724 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-src.tar.gz) | bc32551357ae67573ac9ab4c650bcd547f46a29848e20fc3db286d0e45a22ed254ee2c8d6fe84c4288ebc3df6c3acb118435a532c9cf9f3f5e8d33f4512de806 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-darwin-amd64.tar.gz) | aab9eac3bc604831cfdc926f6d3f12afe6266a2c3808503141ad5780ffcd188f08db3fbad4fedc73da1c612d19bd2e55ba13031fef22ea4839cb294eb54b5767 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-darwin-arm64.tar.gz) | 373fa812af4ed11b9a3b278c44335fd3618c9fb77aa789311e07e37c4bad81e08b066528dd086356e0bb1e116fa807f0015bc71f225afd5bef4dbbe3079034e1 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-linux-386.tar.gz) | e9f8a8925b2b7d3cf89dbaad251f0224945be354ae62c7736b891c73e19334039e68ac7b2dda99f26df0d7028127ccb630de085d2ad45255e263cb03f1f1e552 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-linux-amd64.tar.gz) | 305ea43a314586911f32ae43b16f7a29274fe2a7d87b00b9fb57a4c5c885187a317272c731ddf9d41335905ff5f3640d7a4df7e68d070076e20ff1b2a32a78cd [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-linux-arm.tar.gz) | f012b9e7d46874748655782e125a1a9b7d22c9bee77226eea9c789bc67f5644a9c8380d5fa5d7cc161659011266b9be060dd663603d85b7256deaab4866697c2 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-linux-arm64.tar.gz) | 6952882b71ccc27412fce180844f2a5f9c147b5fb59c4b684d338b3cc767c6e0257f8edde1d1874acda0299ac7c22dba3788292dcbb083fdcc5e61387e8a16a8 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-linux-ppc64le.tar.gz) | d4138ece8741e29c4d4fce07cd9cda38f622b5133a8757334cf5992e3242791213391c2a7ae7db95fee1d70d31b17fda3215d591fb8c9788e0e7d606fcc3a87f [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-linux-s390x.tar.gz) | 511c4c53b20ecff1fc200e85a14211781e0d887a5536a3343a6a0c8ce05c175d073b810945fd1ddd2389318ea26e0ca412b7025ce9f168b76ad24a7ee85213a7 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-windows-386.tar.gz) | 68b781adad28a0ac8e19a624e6811f4e593ad4a1422294a40aa356f8ac05dfc5978f90b55a8716059b4a613caad8904961e9c7e74a4a803fed76c98739b126dd [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-windows-amd64.tar.gz) | 009f05ff583c6b43ffea01e9ff2f7e3cc13184646ce358338a2a1188f4750b02a9253a250c977576664d4d173ce8469a0d1be9a3968890a99969292ad1e001ec [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-client-windows-arm64.tar.gz) | 88dcf4ee3f86484d882632a10e63b7b6e64b844b17c3cc674a49e5ddab9cea091710e4503c46ee59d70fcf762dd1c4e954f5091154d23747a528ffa31d593273 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-server-linux-amd64.tar.gz) | 8023512c58f639b20bca94aa7bc3e908cd9fe2e213b655d1ad63da1507223651c6eb61ddf0d6670d664080e19e714640e3cf5aab4b9c6eb62fc0166cceabd3fd [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-server-linux-arm64.tar.gz) | 7bb2a4530294bafb8f43ddfcfeefdd3fc8629c8dbfd11c2e789a59a930fe624262698311ed149e2c98cdde9bbf321b8c77213b4f562a5120a35ae645d1abf1ce [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-server-linux-ppc64le.tar.gz) | 2f0071550e98d58b87dc56e5d27a1832827b256aa77ad4f68c3713ecd9e81fa66822d7604988c617c139d7e131e05664409f48f94f450cef467ab63727527e14 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-server-linux-s390x.tar.gz) | 620241063ca4f09b4c71a3659e301246e82d841921e7956759d4a3a74bae7dff1d0951f5aea6928039714569ffbb5040f1ca73633bd90123000f4e18e9f196df ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-node-linux-amd64.tar.gz) | d54a8d3406df58a6941837e988e32cdc93bd5025dca1910dbcc1c89d8fa29dc09375c24d7f109fcf4d72c977933c091c225241a0988893a642a35edac04ee38d [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-node-linux-arm64.tar.gz) | ddbf090dc9be5c30a968b655d2007485b8c94e5d95b7cd7e29bbb47ba562ae3ed5c15b965acd81acb715a8d706d967595601c5f0f8f5d6c0181626dcbe156c02 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-node-linux-ppc64le.tar.gz) | c1dd2e061b7b305d481791be17234a5ca02f9c0c302a6044ac2b87940b10c5fc9c2817e00f59adeaab8b564181f8ccda4640dcfde67784daea38361f6faa4b2a [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-node-linux-s390x.tar.gz) | 90974009d003cb911a54cad11bcca6805ceca64ed39120ce70029ece9c8e9a33d89803e92b5d251dce9f16267143914c1ed8542d9507cb3a020823a35b42cfdb [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.2/kubernetes-node-windows-amd64.tar.gz) | cc82205db3e6b6e1640ddbb4fbf8e1d81409c894c92aec1e2d5941c6a282414ada136d1f95403e25cb1f739095f838f6d40c97e65d2fa1dc2f3e6205bfb67249 ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.0-alpha.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.0-alpha.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.0-alpha.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.0-alpha.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.0-alpha.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.0-alpha.2](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.33.0-alpha.1 ## Changes by Kind ### Deprecation - The WatchFromStorageWithoutResourceVersion feature flag is deprecated and can no longer be enabled ([#129930](https://github.com/kubernetes/kubernetes/pull/129930), [@serathius](https://github.com/serathius)) [SIG API Machinery] ### API Change - Added support for in-place vertical scaling of Pods with sidecars (containers defined within `initContainers` where the `restartPolicy` is Always). ([#128367](https://github.com/kubernetes/kubernetes/pull/128367), [@vivzbansal](https://github.com/vivzbansal)) [SIG API Machinery, Apps, CLI, Node, Scheduling and Testing] - Kubectl: added alpha support for customizing kubectl behavior using preferences from a `kuberc` file (separate from kubeconfig). ([#125230](https://github.com/kubernetes/kubernetes/pull/125230), [@ardaguclu](https://github.com/ardaguclu)) [SIG API Machinery, CLI and Testing] ### Feature - Added a `/statusz` endpoint for kube-controller-manager ([#128991](https://github.com/kubernetes/kubernetes/pull/128991), [@Henrywu573](https://github.com/Henrywu573)) [SIG API Machinery, Cloud Provider, Instrumentation and Testing] - Fixed SELinuxWarningController defaults when running kube-controller-manager in a container. ([#130037](https://github.com/kubernetes/kubernetes/pull/130037), [@jsafrane](https://github.com/jsafrane)) [SIG Apps and Storage] - Graduate BtreeWatchCache feature gate to GA ([#129934](https://github.com/kubernetes/kubernetes/pull/129934), [@serathius](https://github.com/serathius)) [SIG API Machinery] - Introduced the `LegacySidecarContainers` feature gate enabling the legacy code path that predates the `SidecarContainers` feature. This temporary feature gate is disabled by default, only available in v1.33, and will be removed in v1.34. ([#130058](https://github.com/kubernetes/kubernetes/pull/130058), [@gjkim42](https://github.com/gjkim42)) [SIG Node] - Kubeadm: 'kubeadm upgrade plan' now supports '--etcd-upgrade' flag to control whether the etcd upgrade plan should be displayed. Add an `EtcdUpgrade` field into `UpgradeConfiguration.Plan` for v1beta4. ([#130023](https://github.com/kubernetes/kubernetes/pull/130023), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - Kubeadm: added preflight check for `cp` on Linux nodes and `xcopy` on Windows nodes. These binaries are required for kubeadm to work properly. ([#130045](https://github.com/kubernetes/kubernetes/pull/130045), [@carlory](https://github.com/carlory)) [SIG Cluster Lifecycle] - Kubeadm: improved `kubeadm init` and `kubeadm join` to provide consistent error messages when the kubelet failed or when failed to wait for control plane components. ([#130040](https://github.com/kubernetes/kubernetes/pull/130040), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle] - Kubeadm: promoted the feature gate `ControlPlaneKubeletLocalMode` to Beta. Kubeadm will per default use the local kube-apiserver endpoint for the kubelet when creating a cluster with "kubeadm init" or when joining control plane nodes with "kubeadm join". Enabling the feature gate also affects the `kubeadm init phase kubeconfig kubelet` phase, where the flag `--control-plane-endpoint` no longer affects the generated kubeconfig `Server` field, but the flag `--apiserver-advertise-address` can now be used for the same purpose. ([#129956](https://github.com/kubernetes/kubernetes/pull/129956), [@chrischdi](https://github.com/chrischdi)) [SIG Cluster Lifecycle] - Kubernetes is now built with go 1.23.5 ([#129962](https://github.com/kubernetes/kubernetes/pull/129962), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Kubernetes is now built with go 1.23.6 ([#130074](https://github.com/kubernetes/kubernetes/pull/130074), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - NodeRestriction admission now validates the audience value that kubelet is requesting a service account token for is part of the pod spec volume. The kube-apiserver featuregate `ServiceAccountNodeAudienceRestriction` is enabled by default in 1.33. ([#130017](https://github.com/kubernetes/kubernetes/pull/130017), [@aramase](https://github.com/aramase)) [SIG Auth] - The nftables mode of kube-proxy is now GA. (The iptables mode remains the default; you can select the nftables mode by passing `--proxy-mode nftables` or using a config file with `mode: nftables`. See the kube-proxy documentation for more details.) ([#129653](https://github.com/kubernetes/kubernetes/pull/129653), [@danwinship](https://github.com/danwinship)) [SIG Network] - `kubeproxy_conntrack_reconciler_deleted_entries_total` metric can be used to track cumulative sum of conntrack flows cleared by reconciler ([#130204](https://github.com/kubernetes/kubernetes/pull/130204), [@aroradaman](https://github.com/aroradaman)) [SIG Network] - `kubeproxy_conntrack_reconciler_sync_duration_seconds` metric can be used to track conntrack reconciliation latency ([#130200](https://github.com/kubernetes/kubernetes/pull/130200), [@aroradaman](https://github.com/aroradaman)) [SIG Network] ### Bug or Regression - Fix: adopt go1.23 behavior change in mount point parsing on Windows ([#129368](https://github.com/kubernetes/kubernetes/pull/129368), [@andyzhangx](https://github.com/andyzhangx)) [SIG Storage and Windows] - Fixes a regression with the ServiceAccountNodeAudienceRestriction feature where `azureFile` volumes encounter "failed to get service accoount token attributes" errors ([#129993](https://github.com/kubernetes/kubernetes/pull/129993), [@aramase](https://github.com/aramase)) [SIG Auth and Testing] - Kube-proxy: fixes a potential memory leak which can occur in clusters with high volume of UDP workflows ([#130032](https://github.com/kubernetes/kubernetes/pull/130032), [@aroradaman](https://github.com/aroradaman)) [SIG Network] - Resolves a performance regression in default 1.31+ configurations, related to the ConsistentListFromCache feature, where rapid create / update API requests across different namespaces encounter increased latency. ([#130113](https://github.com/kubernetes/kubernetes/pull/130113), [@AwesomePatrol](https://github.com/AwesomePatrol)) [SIG API Machinery] - The response from kube-apiserver /flagz endpoint would respond correctly with parsed flags value. ([#129996](https://github.com/kubernetes/kubernetes/pull/129996), [@yongruilin](https://github.com/yongruilin)) [SIG API Machinery, Architecture, Instrumentation and Testing] - When cpu-manager-policy=static is configured containers meeting the qualifications for static cpu assignment (i.e. Containers with integer CPU `requests` in pods with `Guaranteed` QOS) will not have cfs quota enforced. Because this fix changes a long-established behavior, users observing a regressions can use the DisableCPUQuotaWithExclusiveCPUs feature gate (default on) to restore the old behavior. Please file an issue if you encounter problems and have to use the Feature Gate. ([#127525](https://github.com/kubernetes/kubernetes/pull/127525), [@scott-grimes](https://github.com/scott-grimes)) [SIG Node and Testing] ### Other (Cleanup or Flake) - Flip StorageNamespaceIndex feature gate to false and deprecate it ([#129933](https://github.com/kubernetes/kubernetes/pull/129933), [@serathius](https://github.com/serathius)) [SIG Node] - The SeparateCacheWatchRPC feature gate is deprecated and disabled by default. ([#129929](https://github.com/kubernetes/kubernetes/pull/129929), [@serathius](https://github.com/serathius)) [SIG API Machinery] ## Dependencies ### Added _Nothing has changed._ ### Changed - github.com/vishvananda/netlink: [b1ce50c → 62fb240](https://github.com/vishvananda/netlink/compare/b1ce50c...62fb240) ### Removed _Nothing has changed._ # v1.33.0-alpha.1 ## Downloads for v1.33.0-alpha.1 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes.tar.gz) | 809c3565365eccf43761888113fe63c37a700edb6c662f4a29b93768d8d49d6c8ef052a6ffc41f61e9eecb22e006dc03c4399ad05886dc6a7635b2e573d0097d [kubernetes-src.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-src.tar.gz) | 204a8f6723e8c0b0350994174b43f3a9272dacbd4f2992919b8ec95748df6af53dea385210b89417f1eeaa733732fee6c80559f0779f02f7cb73ccde6384bc9b ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-darwin-amd64.tar.gz) | 7762f1e33b94102a7fb943dfda3067e69ac534aeca040e95462781bd5973ee2436fe60c4ca2eeaea79f210a07c91167629d620bafc5b108839c02a4865ee0b64 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-darwin-arm64.tar.gz) | ece5bda2f89981659957cc7bc40cd7db20283778c8f1755b9a21499057ec808708eeb7db3f195c0231ba43a0fd9165fb4bf6367183a486d82145414db2327790 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-linux-386.tar.gz) | 559689427abb113695ea3a1a1b3cbd388c0887dc8f775878337c1d413c1eb0fccfad161c9af23d7a40a0536b438bd800078fae182fcfde2905568ef4079b1062 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-linux-amd64.tar.gz) | ba65065523407b5596a9efc53f7dd2e5e37b39c3968bbdb13a50944a80635dfc5903395741b5cb0f5f24482384788271fa1354b56f7f6b0b2f7482237aea8cc8 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-linux-arm.tar.gz) | 585edd8319aec86378c16da7515f42fdcae5c618fba5dfba4af1455d5db8f5433fe16b95ff7193a2e648a847261ea51d3b412133459d33b48159ddf695a76f26 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-linux-arm64.tar.gz) | 5d228232661dd237df57181920ee73008e1b28eda0366a85d125f569b15a21ebae8f9e2536b244908f9f82184e097b4ac9722863eed352cd0c957b7444bcc5fa [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-linux-ppc64le.tar.gz) | 59e93927f46aff4f304ccad25a0d6220fa643c42c81b65015bd450d7615a809a8b4912efba0e66fe37f33def4b9fe77785ce43688582003c849377bde3277006 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-linux-s390x.tar.gz) | 7c3bd8c464b0a46a216deb1144e3b042cc218464de6e418345a644024de09a04ec78e13a7c5a3f17d90ad9fda254482dd17d05ae67cd267ee2e0504da8258cf2 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-windows-386.tar.gz) | 0ea8503268858c551f9b9e51eb360cc160c76cb19c72c434df79ed421766bcb9addd33e6092525ab8e3556f217ae55dfc13f4506afd27585b5031118a6005403 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-windows-amd64.tar.gz) | f811e3c8e5b4fa31f9ae3493d757b4511de6cf0fc37a161da3c25f1503cf11149af6b79b9abf11314abf2e4cf410f1e41b10414981c141f702bec297a2beeae7 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-client-windows-arm64.tar.gz) | a8dfbb963a5d719dc8890ef14340ce35880e006955a229ff9204bb35da2a29df41b6797dc02269f2cc8de361014f8dd6b2535a9414359b48d820ff2cf536c4e1 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-server-linux-amd64.tar.gz) | daf5f5f38ab4357a724d688bfc33f3344f340fc4896d6d0c3da777beb76abe133707bbb6bd47cb954cd46bd62d5f4a7311fcaa5cd99f3389472d846c15d2e604 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-server-linux-arm64.tar.gz) | 28d03d130e28eb7e812db35ca387eb515dfe8c21bbb2e7690285343d381ecd87828c0362ad19b3d13ec8d1d37763924cf9fdb1d814eb75d6e695322c27db06b4 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-server-linux-ppc64le.tar.gz) | b479688f8aaa93d48d5809d21f21837b67144a5c115370f5154b9a13005f47e579f9f54b8f6d371e97165bd4f1a3d8eda85d2a37c83ac1615ca4dad7155d9a6e [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-server-linux-s390x.tar.gz) | ed02308911595375b313b7df2fc6ad94b7dbcfc6f57fb0b9ced5512c4eca8f086852ea24bbfa7f3c146dc9cb98a1e5964dfc911dd46e41f815eeb884b82efdab ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-node-linux-amd64.tar.gz) | 846d0079fe2c53bdec279d6cc185f968cfed908762ce63c053830fdaeda78da4856f19253f98b908406694179da82dd2c387a4a08ad01d2522dc67832c7e2ac5 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-node-linux-arm64.tar.gz) | c6b35f71acf7e9009ba1c6d274f1d2655039a0de59c0dd3f544bf240a8e74c43fa7bf830377f7d87dc14ce271e2f312a85930804ddd236a6877d13410131028e [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-node-linux-ppc64le.tar.gz) | c67735374d4f9062c495040c1bb28fc7f15362908d116542e663c58c900fc5e7939468118603d2233c8a951175484d839039f9d2ee1e0473e227fa994a391480 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-node-linux-s390x.tar.gz) | 2161369d2590959d8d28f81fa1d642028c816a4ce761d7af3d3edae369cda2a58fe8fa466d16e071d34148331ae572512421296ec53a1f5a1312a00376d67a01 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.33.0-alpha.1/kubernetes-node-windows-amd64.tar.gz) | f8051a237f06566e6bfd51881e1ae50a359b76dd5c8865ba6f3bf936e8be327a9a71d22192e252d49a2fb243be601fd2ceb17ea989b21e57c35f833e7b977341 ### Container Images All container images are available as manifest lists and support the described architectures. It is also possible to pull a specific architecture directly by adding the "-$ARCH" suffix to the container image name. name | architectures ---- | ------------- [registry.k8s.io/conformance:v1.33.0-alpha.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/conformance-s390x) [registry.k8s.io/kube-apiserver:v1.33.0-alpha.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-apiserver-s390x) [registry.k8s.io/kube-controller-manager:v1.33.0-alpha.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-controller-manager-s390x) [registry.k8s.io/kube-proxy:v1.33.0-alpha.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-proxy-s390x) [registry.k8s.io/kube-scheduler:v1.33.0-alpha.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kube-scheduler-s390x) [registry.k8s.io/kubectl:v1.33.0-alpha.1](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl) | [amd64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-amd64), [arm64](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-arm64), [ppc64le](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-ppc64le), [s390x](https://console.cloud.google.com/artifacts/docker/k8s-artifacts-prod/southamerica-east1/images/kubectl-s390x) ## Changelog since v1.32.0 ## Urgent Upgrade Notes ### (No, really, you MUST read this before you upgrade) - Action required for custom plugin developers. The `UpdatePodTolerations` action type is renamed to `UpdatePodToleration`, you have to follow the renaming if you're using it. ([#129023](https://github.com/kubernetes/kubernetes/pull/129023), [@zhifei92](https://github.com/zhifei92)) [SIG Scheduling and Testing] ## Changes by Kind ### API Change - A new status field `.status.terminatingReplicas` is added to Deployments and ReplicaSets to allow tracking of terminating pods when the DeploymentReplicaSetTerminatingReplicas feature-gate is enabled. ([#128546](https://github.com/kubernetes/kubernetes/pull/128546), [@atiratree](https://github.com/atiratree)) [SIG API Machinery, Apps and Testing] - DRA API: the maximum number of pods which can use the same ResourceClaim is now 256 instead of 32. Beware that downgrading a cluster where this relaxed limit is in use to Kubernetes 1.32.0 is not supported because 1.32.0 would refuse to update ResourceClaims with more than 32 entries in the status.reservedFor field. ([#129543](https://github.com/kubernetes/kubernetes/pull/129543), [@pohly](https://github.com/pohly)) [SIG API Machinery, Node and Testing] - DRA: CEL expressions using attribute strings exceeded the cost limit because their cost estimation was incomplete. ([#129661](https://github.com/kubernetes/kubernetes/pull/129661), [@pohly](https://github.com/pohly)) [SIG Node] - DRA: when asking for "All" devices on a node, Kubernetes <= 1.32 proceeded to schedule pods onto nodes with no devices by not allocating any devices for those pods. Kubernetes 1.33 changes that to only picking nodes which have at least one device. Users who want the "proceed with scheduling also without devices" semantic can use the upcoming prioritized list feature with one sub-request for "all" devices and a second alternative with "count: 0". ([#129560](https://github.com/kubernetes/kubernetes/pull/129560), [@bart0sh](https://github.com/bart0sh)) [SIG API Machinery and Node] - Graduate MultiCIDRServiceAllocator to stable and DisableAllocatorDualWrite to beta (disabled by default). Action required for Kubernetes distributions that manage the cluster Service CIDR. This feature allows users to define the cluster Service CIDR via a new API object: ServiceCIDR. Distributions or administrators of Kubernetes may want to control that new Service CIDRs added to the cluster does not overlap with other networks on the cluster, that only belong to a specific range of IPs or just simple retain the existing behavior of only having one ServiceCIDR per cluster. An example of a Validation Admission Policy to achieve this is: --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicy metadata: name: "servicecidrs.default" spec: failurePolicy: Fail matchConstraints: resourceRules: - apiGroups: ["networking.k8s.io"] apiVersions: ["v1","v1beta1"] operations: ["CREATE", "UPDATE"] resources: ["servicecidrs"] matchConditions: - name: 'exclude-default-servicecidr' expression: "object.metadata.name != 'kubernetes'" variables: - name: allowed expression: "['10.96.0.0/16','2001:db8::/64']" validations: - expression: "object.spec.cidrs.all(i , variables.allowed.exists(j , cidr(j).containsCIDR(i)))" --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding metadata: name: "servicecidrs-binding" spec: policyName: "servicecidrs.default" validationActions: [Deny,Audit] --- ([#128971](https://github.com/kubernetes/kubernetes/pull/128971), [@aojea](https://github.com/aojea)) [SIG Apps, Architecture, Auth, CLI, Etcd, Network, Release and Testing] - Kubenetes starts validating NodeSelectorRequirement's values when creating pods. ([#128212](https://github.com/kubernetes/kubernetes/pull/128212), [@AxeZhan](https://github.com/AxeZhan)) [SIG Apps and Scheduling] - Kubernetes components that accept x509 client certificate authentication now read the user UID from a certificate subject name RDN with object id 1.3.6.1.4.1.57683.2. An RDN with this object id must contain a string value, and appear no more than once in the certificate subject. Reading the user UID from this RDN can be disabled by setting the beta feature gate `AllowParsingUserUIDFromCertAuth` to false (until the feature gate graduates to GA). ([#127897](https://github.com/kubernetes/kubernetes/pull/127897), [@modulitos](https://github.com/modulitos)) [SIG API Machinery, Auth and Testing] - Removed general available feature-gate `PDBUnhealthyPodEvictionPolicy`. ([#129500](https://github.com/kubernetes/kubernetes/pull/129500), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Auth] - `kubectl apply` now coerces `null` values for labels and annotations in manifests to empty string values, consistent with typed JSON metadata decoding, rather than dropping all labels and annotations ([#129257](https://github.com/kubernetes/kubernetes/pull/129257), [@liggitt](https://github.com/liggitt)) [SIG API Machinery] ### Feature - Add unit test helpers to validate CEL and patterns in CustomResourceDefinitions. ([#129028](https://github.com/kubernetes/kubernetes/pull/129028), [@sttts](https://github.com/sttts)) [SIG API Machinery] - Added a `/flagz` endpoint for kube-proxy ([#128985](https://github.com/kubernetes/kubernetes/pull/128985), [@yongruilin](https://github.com/yongruilin)) [SIG Instrumentation and Network] - Added a `/status` endpoint for kube-proxy ([#128989](https://github.com/kubernetes/kubernetes/pull/128989), [@Henrywu573](https://github.com/Henrywu573)) [SIG Instrumentation and Network] - Added e2e tests for volume group snapshots. ([#128972](https://github.com/kubernetes/kubernetes/pull/128972), [@manishym](https://github.com/manishym)) [SIG Cloud Provider, Storage and Testing] - Adds a /flagz endpoint for kube-scheduler endpoint ([#128818](https://github.com/kubernetes/kubernetes/pull/128818), [@yongruilin](https://github.com/yongruilin)) [SIG Architecture, Instrumentation, Scheduling and Testing] - Adds a /statusz endpoint for kubelet endpoint ([#128811](https://github.com/kubernetes/kubernetes/pull/128811), [@zhifei92](https://github.com/zhifei92)) [SIG Architecture, Instrumentation and Node] - Bugfix: Ensure container-level swap metrics are collected ([#129486](https://github.com/kubernetes/kubernetes/pull/129486), [@iholder101](https://github.com/iholder101)) [SIG Node and Testing] - Calculated pod resources are now cached when adding pods to NodeInfo in the scheduler framework, improving performance when processing unschedulable pods. ([#129635](https://github.com/kubernetes/kubernetes/pull/129635), [@macsko](https://github.com/macsko)) [SIG Scheduling] - Cel-go has been bumped to v0.23.2. ([#129844](https://github.com/kubernetes/kubernetes/pull/129844), [@cici37](https://github.com/cici37)) [SIG API Machinery, Auth, Cloud Provider and Node] - Client-go/rest: fully supports contextual logging. BackoffManagerWithContext should be used instead of BackoffManager to ensure that the caller can interrupt the sleep. ([#127709](https://github.com/kubernetes/kubernetes/pull/127709), [@pohly](https://github.com/pohly)) [SIG API Machinery, Architecture, Auth, Cloud Provider, Instrumentation, Network and Node] - Graduated the `KubeletFineGrainedAuthz` feature gate to beta; the gate is now enabled by default. ([#129656](https://github.com/kubernetes/kubernetes/pull/129656), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) [SIG Auth, CLI, Node, Storage and Testing] - Improved scheduling performance of pods with required topology spreading. ([#129119](https://github.com/kubernetes/kubernetes/pull/129119), [@macsko](https://github.com/macsko)) [SIG Scheduling] - Kube-apiserver: Promoted the `ServiceAccountTokenNodeBinding` feature gate general availability. It is now locked to enabled. ([#129591](https://github.com/kubernetes/kubernetes/pull/129591), [@liggitt](https://github.com/liggitt)) [SIG Auth and Testing] - Kube-proxy extends the schema of its healthz/ and livez/ endpoints to incorporate information about the corresponding IP family ([#129271](https://github.com/kubernetes/kubernetes/pull/129271), [@aroradaman](https://github.com/aroradaman)) [SIG Network and Windows] - Kubeadm: graduated the WaitForAllControlPlaneComponents feature gate to Beta. When checking the health status of a control plane component, make sure that the address and port defined as arguments in the respective component's static Pod manifest are used. ([#129620](https://github.com/kubernetes/kubernetes/pull/129620), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: if the `NodeLocalCRISocket` feature gate is enabled, remove the `kubeadm.alpha.kubernetes.io/cri-socket` annotation from a given node on `kubeadm upgrade`. ([#129279](https://github.com/kubernetes/kubernetes/pull/129279), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle and Testing] - Kubeadm: if the `NodeLocalCRISocket` feature gate is enabled, remove the flag `--container-runtime-endpoint` from the `/var/lib/kubelet/kubeadm-flags.env` file on `kubeadm upgrade`. ([#129278](https://github.com/kubernetes/kubernetes/pull/129278), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle] - Kubeadm: promoted the feature gate `ControlPlaneKubeletLocalMode` to Beta. Kubeadm will per default use the local kube-apiserver endpoint for the kubelet when creating a cluster with "kubeadm init" or when joining control plane nodes with "kubeadm join". Enabling the feature gate also affects the `kubeadm init phase kubeconfig kubelet` phase, where the flag `--control-plane-endpoint` no longer affects the generated kubeconfig `Server` field, but the flag `--apiserver-advertise-address` can now be used for the same purpose. ([#129956](https://github.com/kubernetes/kubernetes/pull/129956), [@chrischdi](https://github.com/chrischdi)) [SIG Cluster Lifecycle] - Kubeadm: removed preflight check for nsenter on Linux nodes kubeadm: added preflight check for `losetup` on Linux nodes. It's required by kubelet for keeping a block device opened. ([#129450](https://github.com/kubernetes/kubernetes/pull/129450), [@carlory](https://github.com/carlory)) [SIG Cluster Lifecycle] - Kubeadm: removed the feature gate EtcdLearnerMode which graduated to GA in 1.32. ([#129589](https://github.com/kubernetes/kubernetes/pull/129589), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubernetes is now built with go 1.23.4 ([#129422](https://github.com/kubernetes/kubernetes/pull/129422), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Kubernetes is now built with go 1.23.5 ([#129962](https://github.com/kubernetes/kubernetes/pull/129962), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Promoted the feature gate `CSIMigrationPortworx` to GA. If your applications are using Portworx volumes, please make sure that the corresponding Portworx CSI driver is installed on your cluster **before** upgrading to 1.31 or later because all operations for the in-tree `portworxVolume` type are redirected to the pxd.portworx.com CSI driver when the feature gate is enabled. ([#129297](https://github.com/kubernetes/kubernetes/pull/129297), [@gohilankit](https://github.com/gohilankit)) [SIG Storage] - The `SidecarContainers` feature has graduated to GA. 'SidecarContainers' feature gate was locked to default value and will be removed in v1.36. If you were setting this feature gate explicitly, please remove it now. ([#129731](https://github.com/kubernetes/kubernetes/pull/129731), [@gjkim42](https://github.com/gjkim42)) [SIG Apps, Node, Scheduling and Testing] - Upgrade autoscalingv1 to autoscalingv2 in kubectl autoscale cmd, The cmd will attempt to use the autoscaling/v2 API first. If the autoscaling/v2 API is not available or an error occurs, it will fall back to the autoscaling/v1 API. ([#128950](https://github.com/kubernetes/kubernetes/pull/128950), [@googs1025](https://github.com/googs1025)) [SIG Autoscaling and CLI] - Validate ContainerLogMaxFiles in kubelet config validation ([#129072](https://github.com/kubernetes/kubernetes/pull/129072), [@kannon92](https://github.com/kannon92)) [SIG Node] ### Documentation - Give example of set-based requirement for -l/--selector flag ([#129106](https://github.com/kubernetes/kubernetes/pull/129106), [@rotsix](https://github.com/rotsix)) [SIG CLI] - Kubeadm: improved the `kubeadm reset` message for manual cleanups and referenced https://k8s.io/docs/reference/setup-tools/kubeadm/kubeadm-reset/. ([#129644](https://github.com/kubernetes/kubernetes/pull/129644), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] ### Bug or Regression - --feature-gate=InOrderInformers (default on), causes informers to process watch streams in order as opposed to grouping updates for the same item close together. Binaries embedding client-go, but not wiring the featuregates can disable by setting the `KUBE_FEATURE_InOrderInformers=false`. ([#129568](https://github.com/kubernetes/kubernetes/pull/129568), [@deads2k](https://github.com/deads2k)) [SIG API Machinery] - Adding a validation for revisionHistoryLimit field in statefulset.spec to prevent it being set to negative value. ([#129017](https://github.com/kubernetes/kubernetes/pull/129017), [@ardaguclu](https://github.com/ardaguclu)) [SIG Apps] - DRA: the explanation for why a pod which wasn't using ResourceClaims was unscheduleable included a useless "no new claims to deallocate" when it was unscheduleable for some other reasons. ([#129823](https://github.com/kubernetes/kubernetes/pull/129823), [@googs1025](https://github.com/googs1025)) [SIG Node and Scheduling] - Enables ratcheting validation on status subresources for CustomResourceDefinitions ([#129506](https://github.com/kubernetes/kubernetes/pull/129506), [@JoelSpeed](https://github.com/JoelSpeed)) [SIG API Machinery] - Fix the issue where the named ports exposed by restartable init containers (a.k.a. sidecar containers) cannot be accessed using a Service. ([#128850](https://github.com/kubernetes/kubernetes/pull/128850), [@toVersus](https://github.com/toVersus)) [SIG Network and Testing] - Fixed `kubectl wait --for=create` behavior with label selectors, to properly wait for resources with matching labels to appear. ([#128662](https://github.com/kubernetes/kubernetes/pull/128662), [@omerap12](https://github.com/omerap12)) [SIG CLI and Testing] - Fixed a bug where adding an ephemeral container to a pod which references a new secret or config map doesn't give the pod access to that new secret or config map. (#114984, @cslink) ([#129670](https://github.com/kubernetes/kubernetes/pull/129670), [@cslink](https://github.com/cslink)) [SIG Auth] - Fixed a data race that could occur when a single Go type was serialized to CBOR concurrently for the first time within a program. ([#129170](https://github.com/kubernetes/kubernetes/pull/129170), [@benluddy](https://github.com/benluddy)) [SIG API Machinery] - Fixed a storage bug around multipath. iSCSI and Fibre Channel devices attached to nodes via multipath now resolve correctly if partitioned. ([#128086](https://github.com/kubernetes/kubernetes/pull/128086), [@RomanBednar](https://github.com/RomanBednar)) [SIG Storage] - Fixed in-tree to CSI migration for Portworx volumes, in clusters where Portworx security feature is enabled (it's a Portworx feature, not Kubernetes feature). It required secret data from the secret mentioned in-tree SC, to be passed in CSI requests which was not happening before this fix. ([#129630](https://github.com/kubernetes/kubernetes/pull/129630), [@gohilankit](https://github.com/gohilankit)) [SIG Storage] - Fixed: kube-proxy EndpointSliceCache memory is leaked ([#128929](https://github.com/kubernetes/kubernetes/pull/128929), [@orange30](https://github.com/orange30)) [SIG Network] - Fixes CVE-2024-51744 ([#128621](https://github.com/kubernetes/kubernetes/pull/128621), [@kmala](https://github.com/kmala)) [SIG Auth, Cloud Provider and Node] - Fixes a panic in kube-controller-manager handling StatefulSet objects when revisionHistoryLimit is negative ([#129301](https://github.com/kubernetes/kubernetes/pull/129301), [@ardaguclu](https://github.com/ardaguclu)) [SIG Apps] - HPA's with ContainerResource metrics will no longer error when container metrics are missing, instead they will use the same logic Resource metrics are using to make calculations ([#127193](https://github.com/kubernetes/kubernetes/pull/127193), [@DP19](https://github.com/DP19)) [SIG Apps and Autoscaling] - Implemented logging and event recording for probe results with an `Unknown` status in the kubelet's prober module. This helps in better diagnosing and monitoring cases where container probes return an `Unknown` result, improving the observability and reliability of health checks. ([#125901](https://github.com/kubernetes/kubernetes/pull/125901), [@jralmaraz](https://github.com/jralmaraz)) [SIG Node] - Improved reboot event reporting. The kubelet will only emit one reboot Event when a server-level reboot is detected, even if the kubelet cannot write its status to the associated Node (which triggers a retry). ([#129151](https://github.com/kubernetes/kubernetes/pull/129151), [@rphillips](https://github.com/rphillips)) [SIG Node] - Kube-apiserver: --service-account-max-token-expiration can now be used in combination with an external token signer --service-account-signing-endpoint, as long as the --service-account-max-token-expiration is not longer than the external token signer's max expiration. ([#129816](https://github.com/kubernetes/kubernetes/pull/129816), [@sambdavidson](https://github.com/sambdavidson)) [SIG API Machinery and Auth] - Kubeadm: avoid loading the file passed to `--kubeconfig` during `kubeadm init` phases more than once. ([#129006](https://github.com/kubernetes/kubernetes/pull/129006), [@kokes](https://github.com/kokes)) [SIG Cluster Lifecycle] - Kubeadm: fix a bug where the 'node.skipPhases' in UpgradeConfiguration is not respected by 'kubeadm upgrade node' command ([#129452](https://github.com/kubernetes/kubernetes/pull/129452), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - Kubeadm: fixed a bug where an image is not pulled if there is an error with the sandbox image from CRI. ([#129594](https://github.com/kubernetes/kubernetes/pull/129594), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: fixed the bug where the v1beta4 Timeouts.EtcdAPICall field was not respected in etcd client operations, and the default timeout of 2 minutes was always used. ([#129859](https://github.com/kubernetes/kubernetes/pull/129859), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: if an addon is disabled in the ClusterConfiguration, skip it during upgrade. ([#129418](https://github.com/kubernetes/kubernetes/pull/129418), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: run kernel version and OS version preflight checks on `kubeadm upgrade`. ([#129401](https://github.com/kubernetes/kubernetes/pull/129401), [@pacoxu](https://github.com/pacoxu)) [SIG Cluster Lifecycle] - Provides an additional function argument to directly specify the version for the tools that the consumers wishes to use ([#129658](https://github.com/kubernetes/kubernetes/pull/129658), [@unmarshall](https://github.com/unmarshall)) [SIG API Machinery] - Remove the limitation on exposing port 10250 externally in service. ([#129174](https://github.com/kubernetes/kubernetes/pull/129174), [@RyanAoh](https://github.com/RyanAoh)) [SIG Apps and Network] - This PR changes the signature of the `PublishResources` to accept a `resourceslice.DriverResources` parameter instead of a `Resources` parameter. ([#129142](https://github.com/kubernetes/kubernetes/pull/129142), [@googs1025](https://github.com/googs1025)) [SIG Node and Testing] - [kubectl] Improved the describe output for projected volume sources to clearly indicate whether Secret and ConfigMap entries are optional. ([#129457](https://github.com/kubernetes/kubernetes/pull/129457), [@gshaibi](https://github.com/gshaibi)) [SIG CLI] ### Other (Cleanup or Flake) - Implemented scheduler_cache_size metric. Also, scheduler_scheduler_cache_size metric is deprecated in favor of scheduler_cache_size, and will be removed at v1.34. ([#128810](https://github.com/kubernetes/kubernetes/pull/128810), [@googs1025](https://github.com/googs1025)) [SIG Scheduling] - Kube-apiserver: inactive serving code is removed for authentication.k8s.io/v1alpha1 APIs ([#129186](https://github.com/kubernetes/kubernetes/pull/129186), [@liggitt](https://github.com/liggitt)) [SIG Auth and Testing] - Kube-proxy extends the schema of its metrics/ endpoints to incorporate information about the corresponding IP family ([#129173](https://github.com/kubernetes/kubernetes/pull/129173), [@aroradaman](https://github.com/aroradaman)) [SIG Network and Windows] - Kube-proxy nftables logs the failed transactions and the full table when using log level 4 or higher. Logging is rate limited to one entry every 24 hours to avoid performance issues. ([#128886](https://github.com/kubernetes/kubernetes/pull/128886), [@npinaeva](https://github.com/npinaeva)) [SIG Network] - Kubeadm: removed preflight check for `ip`, `iptables`, `ethtool` and `tc` on Linux nodes. kubelet and kube-proxy will continue to report `iptables` errors if its usage is required. The tools `ip`, `ethtool` and `tc` had legacy usage in the kubelet but are no longer required. ([#129131](https://github.com/kubernetes/kubernetes/pull/129131), [@pacoxu](https://github.com/pacoxu)) [SIG Cluster Lifecycle] - Kubeadm: removed preflight check for `touch` on Linux nodes. ([#129317](https://github.com/kubernetes/kubernetes/pull/129317), [@carlory](https://github.com/carlory)) [SIG Cluster Lifecycle] - NOE ([#128856](https://github.com/kubernetes/kubernetes/pull/128856), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps and Network] - Removed generally available feature gate `KubeProxyDrainingTerminatingNodes`. ([#129692](https://github.com/kubernetes/kubernetes/pull/129692), [@alexanderConstantinescu](https://github.com/alexanderConstantinescu)) [SIG Network] - Removed support for v1alpha1 version of ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding API kinds. ([#129207](https://github.com/kubernetes/kubernetes/pull/129207), [@Jefftree](https://github.com/Jefftree)) [SIG Etcd and Testing] - The deprecated pod_scheduling_duration_seconds metric is removed. You can migrate to pod_scheduling_sli_duration_seconds. ([#128906](https://github.com/kubernetes/kubernetes/pull/128906), [@sanposhiho](https://github.com/sanposhiho)) [SIG Instrumentation and Scheduling] - This renames some coredns metrics, see https://github.com/coredns/coredns/blob/v1.11.0/plugin/forward/README.md#metrics. ([#129175](https://github.com/kubernetes/kubernetes/pull/129175), [@DamianSawicki](https://github.com/DamianSawicki)) [SIG Cloud Provider] - This renames some coredns metrics, see https://github.com/coredns/coredns/blob/v1.11.0/plugin/forward/README.md#metrics. ([#129232](https://github.com/kubernetes/kubernetes/pull/129232), [@DamianSawicki](https://github.com/DamianSawicki)) [SIG Cloud Provider] - Updated CNI plugins to v1.6.2. ([#129776](https://github.com/kubernetes/kubernetes/pull/129776), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider, Node and Testing] - Updated cri-tools to v1.32.0. ([#129116](https://github.com/kubernetes/kubernetes/pull/129116), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider] - Upgrade CoreDNS to v1.12.0 ([#128926](https://github.com/kubernetes/kubernetes/pull/128926), [@bzsuni](https://github.com/bzsuni)) [SIG Cloud Provider and Cluster Lifecycle] ## Dependencies ### Added - gopkg.in/go-jose/go-jose.v2: v2.6.3 ### Changed - cel.dev/expr: v0.18.0 → v0.19.1 - github.com/coredns/corefile-migration: [v1.0.24 → v1.0.25](https://github.com/coredns/corefile-migration/compare/v1.0.24...v1.0.25) - github.com/coreos/go-oidc: [v2.2.1+incompatible → v2.3.0+incompatible](https://github.com/coreos/go-oidc/compare/v2.2.1...v2.3.0) - github.com/cyphar/filepath-securejoin: [v0.3.4 → v0.3.5](https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...v0.3.5) - github.com/davecgh/go-spew: [d8f796a → v1.1.1](https://github.com/davecgh/go-spew/compare/d8f796a...v1.1.1) - github.com/golang-jwt/jwt/v4: [v4.5.0 → v4.5.1](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1) - github.com/google/btree: [v1.0.1 → v1.1.3](https://github.com/google/btree/compare/v1.0.1...v1.1.3) - github.com/google/cel-go: [v0.22.0 → v0.23.2](https://github.com/google/cel-go/compare/v0.22.0...v0.23.2) - github.com/google/gnostic-models: [v0.6.8 → v0.6.9](https://github.com/google/gnostic-models/compare/v0.6.8...v0.6.9) - github.com/pmezard/go-difflib: [5d4384e → v1.0.0](https://github.com/pmezard/go-difflib/compare/5d4384e...v1.0.0) - golang.org/x/crypto: v0.28.0 → v0.31.0 - golang.org/x/net: v0.30.0 → v0.33.0 - golang.org/x/sync: v0.8.0 → v0.10.0 - golang.org/x/sys: v0.26.0 → v0.28.0 - golang.org/x/term: v0.25.0 → v0.27.0 - golang.org/x/text: v0.19.0 → v0.21.0 - k8s.io/kube-openapi: 32ad38e → 2c72e55 - sigs.k8s.io/apiserver-network-proxy/konnectivity-client: v0.31.0 → v0.31.1 - sigs.k8s.io/kustomize/api: v0.18.0 → v0.19.0 - sigs.k8s.io/kustomize/cmd/config: v0.15.0 → v0.19.0 - sigs.k8s.io/kustomize/kustomize/v5: v5.5.0 → v5.6.0 - sigs.k8s.io/kustomize/kyaml: v0.18.1 → v0.19.0 ### Removed - github.com/asaskevich/govalidator: [f61b66f](https://github.com/asaskevich/govalidator/tree/f61b66f) - gopkg.in/square/go-jose.v2: v2.6.0kubernetes-kubernetes-40e1192/CHANGELOG/OWNERS000066400000000000000000000006411504711711200205240ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners options: # make root approval non-recursive no_parent_owners: true approvers: - release-engineering-approvers - release-managers - release-team-subproject-leads - satyampsoni # 1.32 Release Notes Lead reviewers: - release-managers - release-team-subproject-leads - satyampsoni # 1.32 Release Notes Lead labels: - sig/release - area/release-eng kubernetes-kubernetes-40e1192/CHANGELOG/README.md000066400000000000000000000025361504711711200210500ustar00rootroot00000000000000# CHANGELOGs - [CHANGELOG-1.33.md](./CHANGELOG-1.33.md) - [CHANGELOG-1.32.md](./CHANGELOG-1.32.md) - [CHANGELOG-1.31.md](./CHANGELOG-1.31.md) - [CHANGELOG-1.30.md](./CHANGELOG-1.30.md) - [CHANGELOG-1.29.md](./CHANGELOG-1.29.md) - [CHANGELOG-1.28.md](./CHANGELOG-1.28.md) - [CHANGELOG-1.27.md](./CHANGELOG-1.27.md) - [CHANGELOG-1.26.md](./CHANGELOG-1.26.md) - [CHANGELOG-1.25.md](./CHANGELOG-1.25.md) - [CHANGELOG-1.24.md](./CHANGELOG-1.24.md) - [CHANGELOG-1.23.md](./CHANGELOG-1.23.md) - [CHANGELOG-1.22.md](./CHANGELOG-1.22.md) - [CHANGELOG-1.21.md](./CHANGELOG-1.21.md) - [CHANGELOG-1.20.md](./CHANGELOG-1.20.md) - [CHANGELOG-1.19.md](./CHANGELOG-1.19.md) - [CHANGELOG-1.18.md](./CHANGELOG-1.18.md) - [CHANGELOG-1.17.md](./CHANGELOG-1.17.md) - [CHANGELOG-1.16.md](./CHANGELOG-1.16.md) - [CHANGELOG-1.15.md](./CHANGELOG-1.15.md) - [CHANGELOG-1.14.md](./CHANGELOG-1.14.md) - [CHANGELOG-1.13.md](./CHANGELOG-1.13.md) - [CHANGELOG-1.12.md](./CHANGELOG-1.12.md) - [CHANGELOG-1.11.md](./CHANGELOG-1.11.md) - [CHANGELOG-1.10.md](./CHANGELOG-1.10.md) - [CHANGELOG-1.9.md](./CHANGELOG-1.9.md) - [CHANGELOG-1.8.md](./CHANGELOG-1.8.md) - [CHANGELOG-1.7.md](./CHANGELOG-1.7.md) - [CHANGELOG-1.6.md](./CHANGELOG-1.6.md) - [CHANGELOG-1.5.md](./CHANGELOG-1.5.md) - [CHANGELOG-1.4.md](./CHANGELOG-1.4.md) - [CHANGELOG-1.3.md](./CHANGELOG-1.3.md) - [CHANGELOG-1.2.md](./CHANGELOG-1.2.md) kubernetes-kubernetes-40e1192/CONTRIBUTING.md000066400000000000000000000010151504711711200205220ustar00rootroot00000000000000# Contributing Welcome to Kubernetes! To learn more about contributing to the [Kubernetes code repo](README.md), check out the [Contributor's Guide](https://git.k8s.io/community/contributors/guide/). The [Kubernetes community repo](https://github.com/kubernetes/community) contains information about how to get started, how the community organizes, and more. ## Sign the CLA You must sign the [Contributor License Agreement](https://git.k8s.io/community/contributors/guide/README.md#sign-the-cla) in order to contribute. kubernetes-kubernetes-40e1192/LICENSE000066400000000000000000000261361504711711200173110ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. kubernetes-kubernetes-40e1192/LICENSES/000077500000000000000000000000001504711711200175015ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/LICENSES/LICENSE000066400000000000000000000265141504711711200205160ustar00rootroot00000000000000================================================================================ = Kubernetes licensed under: = Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. = LICENSE 3b83ef96387f14655fc854ddc3c6bd57 ================================================================================ kubernetes-kubernetes-40e1192/LICENSES/OWNERS000066400000000000000000000002271504711711200204420ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners options: # make root approval non-recursive no_parent_owners: true approvers: - dep-approvers kubernetes-kubernetes-40e1192/Makefile000077700000000000000000000000001504711711200234512build/root/Makefileustar00rootroot00000000000000kubernetes-kubernetes-40e1192/OWNERS000066400000000000000000000015161504711711200172370ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners filters: ".*": reviewers: - dep-reviewers - sig-architecture-approvers approvers: - dep-approvers # for go.mod/go.sum - sig-architecture-approvers # for OWNERS_ALIASES and other root files emeritus_approvers: - bgrant0607 - brendandburns - dchen1107 - jbeda - lavalamp - liggitt - smarterclayton - thockin - wojtek-t # go.{mod,sum} files relate to go dependencies, and should be reviewed by the # dep-approvers "go\\.(mod|sum)$": required_reviewers: - kubernetes/dep-approvers labels: - area/dependency # metrics.go files are sig-instrumentation related, and should be tagged # and reviewed by sig-instrumentation "metrics\\.go$": labels: - sig/instrumentation kubernetes-kubernetes-40e1192/OWNERS_ALIASES000066400000000000000000000273561504711711200204120ustar00rootroot00000000000000aliases: # Note: sig-architecture-approvers has approval on root files (including go.mod/go.sum) until https://github.com/kubernetes/test-infra/pull/21398 is resolved. # People with approve rights via this alias should defer dependency update PRs to dep-approvers. sig-architecture-approvers: - dims - derekwaynecarr - johnbelamaric # sig-auth subproject aliases sig-auth-audit-approvers: - sttts - tallclair sig-auth-audit-reviewers: - sttts - tallclair sig-auth-authenticators-approvers: - deads2k - liggitt - mikedanese - enj sig-auth-authenticators-reviewers: - deads2k - enj - liggitt - mikedanese - sttts - wojtek-t - aramase sig-auth-authorizers-approvers: - deads2k - liggitt - mikedanese sig-auth-authorizers-reviewers: - deads2k - dims - enj - liggitt - mikedanese - smarterclayton - sttts - thockin - wojtek-t # - emeritus # - ncdc sig-auth-certificates-approvers: - liggitt - mikedanese - smarterclayton sig-auth-certificates-reviewers: - deads2k - dims - enj - liggitt - mikedanese sig-auth-encryption-at-rest-approvers: - smarterclayton - enj sig-auth-encryption-at-rest-reviewers: - aramase - enj sig-auth-node-isolation-approvers: - deads2k - liggitt - mikedanese - tallclair sig-auth-node-isolation-reviewers: - deads2k - liggitt - mikedanese - tallclair sig-auth-policy-approvers: - deads2k - liggitt - tallclair sig-auth-policy-reviewers: - deads2k - liggitt - tallclair - krmayankk sig-auth-serviceaccounts-approvers: - deads2k - enj - liggitt - mikedanese sig-auth-serviceaccounts-reviewers: - deads2k - enj - liggitt - mikedanese # SIG Release release-engineering-approvers: - cpanato # SIG Technical Lead / RelEng subproject owner / Release Manager - jeremyrickard # SIG Chair / RelEng subproject owner / Release Manager - justaugustus # SIG Chair / RelEng subproject owner / Release Manager - puerco # SIG Technical Lead / RelEng subproject owner / Release Manager - saschagrunert # SIG Chair / RelEng subproject owner / Release Manager - Verolop # SIG Technical Lead / RelEng subproject owner / Release Manager - xmudrii # RelEng subproject lead / Release Manager release-managers: - cpanato - jeremyrickard - justaugustus - palnabarun - puerco - saschagrunert - Verolop - xmudrii release-team-subproject-leads: - gracenng # Release Team subproject lead - katcosgrove # Release Team subproject lead build-image-approvers: - BenTheElder - cblecker - cpanato # SIG Technical Lead / RelEng subproject owner / Release Manager - dims - jeremyrickard # SIG Chair / RelEng subproject owner / Release Manager - justaugustus # SIG Chair / RelEng subproject owner / Release Manager - palnabarun # Release Manager - puerco # SIG Technical Lead / RelEng subproject owner / Release Manager - saschagrunert # SIG Chair / RelEng subproject owner / Release Manager - Verolop # SIG Technical Lead / RelEng subproject owner / Release Manager - xmudrii # RelEng subproject lead / Release Manager build-image-reviewers: - BenTheElder - cblecker - cpanato # SIG Technical Lead / RelEng subproject owner / Release Manager - dims - jeremyrickard # SIG Chair / RelEng subproject owner / Release Manager #- justaugustus # SIG Chair / RelEng subproject owner / Release Manager - approvals only - palnabarun # Release Manager - puerco # SIG Technical Lead / RelEng subproject owner / Release Manager - saschagrunert # SIG Chair / RelEng subproject owner / Release Manager - Verolop # SIG Technical Lead / RelEng subproject owner / Release Manager - xmudrii # RelEng subproject lead / Release Manager sig-storage-approvers: - gnufied - jsafrane - msau42 - saad-ali - thockin - xing-yang # emeritus: # - rootfs sig-storage-reviewers: - carlory - gnufied - humblec - jsafrane - jingxu97 - mattcary - mauriciopoppe - msau42 - saikat-royc - xing-yang # emeritus: # - chrishenzie # - davidz627 # - Jiawei0227 # - rootfs # - verult sig-scheduling-maintainers: - alculquicondor - ahg-g - Huang-Wei - kerthcet - macsko - sanposhiho # emeritus: # - damemi # - bsalamat # - k82cn # - ravisantoshgudimetla # - wojtek-t sig-scheduling: - AxeZhan - damemi - denkensk - dom4ha - macsko - sanposhiho - kerthcet # emeritus: # - adtac # - liu-cong # - draveness # - hex108 # - resouer # - wgliang # - chendave sig-cli-maintainers: - ardaguclu - apelisse - brianpursley - eddiezane - seans3 - soltysh # emeritus: # - adohe # - brendandburns # - deads2k # - droot # - janetkuo # - mengqiy # - monopole # - smarterclayton # - KnVerey # - pwittrock sig-cli-reviewers: - ardaguclu - brianpursley - eddiezane - mpuckett159 - seans3 - soltysh sig-testing-reviewers: - bentheelder - cblecker - spiffxp - dims sig-node-approvers: - Random-Liu - dchen1107 - derekwaynecarr - yujuhong - sjenning - mrunalp - klueska - SergeyKanzhelev - tallclair # emeretus: # - dashpole # - vishh sig-node-cri-approvers: - msau42 - smarterclayton - thockin - saschagrunert - haircommander - mikebrow sig-node-reviewers: - Random-Liu - dchen1107 - derekwaynecarr - dims - endocrimes - feiskyer - mtaufen - sjenning - wzshiming - yujuhong - krmayankk - matthyx - odinuge - andrewsykim - mrunalp - SergeyKanzhelev - bobbypage - pacoxu - bart0sh - saschagrunert - haircommander - tzneal - rphillips - kannon92 - ffromani - tallclair sig-network-approvers: - andrewsykim - aojea - bowei - caseydavenport - danwinship - dcbw - freehan - khenidak - mrhohn - robscott - thockin sig-network-reviewers: - andrewsykim - aojea - aroradaman - bowei - caseydavenport - danwinship - dcbw - freehan - khenidak - mrhohn - robscott - thockin - tnqn sig-apps-approvers: - kow3ns - janetkuo - soltysh - smarterclayton - atiratree sig-apps-reviewers: - atiratree - janetkuo - kow3ns - krmayankk - mimowo - mortent - smarterclayton - soltysh # sig-apps-emeritus: # - tnozicka sig-autoscaling-maintainers: - gjtempleton - MaciekPytel sig-instrumentation-approvers: - logicalhan - dashpole - RainbowMango - serathius - dgrisonnet - rexagod - richabanker sig-instrumentation-reviewers: - dashpole - s-urbaniak - coffeepac - logicalhan - RainbowMango - serathius - dgrisonnet - pohly - mengjiao-liu - rexagod - richabanker # sig-instrumentation-emeritus # - brancz # - DirectXMan12 # - ehashman api-approvers: - deads2k - msau42 - smarterclayton - thockin - liggitt # subsets of api-approvers by sig area to help focus approval requests to those with domain knowledge sig-api-machinery-api-approvers: - deads2k - liggitt - smarterclayton sig-apps-api-approvers: - deads2k - liggitt - smarterclayton sig-auth-api-approvers: - deads2k - liggitt - smarterclayton sig-cli-api-approvers: - deads2k - liggitt - smarterclayton sig-cloud-provider-api-approvers: - liggitt - thockin sig-cluster-lifecycle-api-approvers: - deads2k - liggitt - smarterclayton sig-cluster-lifecycle-leads: - fabriziopandini - justinsb - neolit123 - vincepri sig-network-api-approvers: - smarterclayton - thockin sig-network-api-reviewers: - andrewsykim - caseydavenport - danwinship - thockin sig-scheduling-api-approvers: - msau42 - smarterclayton - thockin sig-security-approvers: - IanColdwater - tabbysable sig-security-reviewers: - IanColdwater - tabbysable sig-storage-api-approvers: - liggitt - msau42 - thockin sig-windows-api-approvers: - smarterclayton - thockin - liggitt api-reviewers: - andrewsykim - smarterclayton - thockin - liggitt - wojtek-t - deads2k - yujuhong - derekwaynecarr - caesarxuchao - mikedanese - sttts - dchen1107 - saad-ali - luxas - janetkuo - justinsb - pwittrock - tallclair - mwielgus - soltysh - jsafrane - dims - cici37 # - emeritus # - ncdc # api-reviewers targeted by sig area # see https://git.k8s.io/community/sig-architecture/api-review-process.md#training-reviews sig-api-machinery-api-reviewers: - caesarxuchao - deads2k - jpbetz - sttts - cici37 sig-apps-api-reviewers: - janetkuo - kow3ns - soltysh sig-auth-api-reviewers: - enj - mikedanese sig-cli-api-reviewers: - pwittrock - soltysh sig-cloud-provider-api-reviewers: - elmiko - cheftako - dims # sig-cluster-lifecycle-api-reviewers: # - # - sig-contributor-experience-approvers: - mrbobbytables - cblecker - nikhita - palnabarun - kaslin - MadhavJivrajani - Priyankasaggu11929 sig-docs-approvers: - jimangel - kbhawkey - onlydole - sftim sig-node-api-reviewers: - dchen1107 - derekwaynecarr - tallclair - yujuhong sig-scalability-approvers: - marseel - mborsz - wojtek-t # emeritus: # - mm4tt sig-scalability-reviewers: - marseel - mborsz - wojtek-t # emeritus: # - mm4tt sig-scheduling-api-reviewers: - alculquicondor sig-storage-api-reviewers: - deads2k - saad-ali - msau42 - jsafrane - xing-yang sig-windows-api-reviewers: - jayunit100 - jsturtevant - marosset # Note: dep-approvers has approval on root files (including OWNERS_ALIASES) until https://github.com/kubernetes/test-infra/pull/21398 is resolved. # People with approve rights via this alias should defer updates of root files other than go.mod/go.sum to dep-approvers. dep-approvers: - BenTheElder - cblecker - dims - thockin - sttts - soltysh - liggitt dep-reviewers: - logicalhan feature-approvers: - andrewsykim # Cloud Provider - ahg-g # Scheduling - ardaguclu # CLI - aojea # Network, Testing - danwinship # Network - dashpole # Instrumentation - dchen1107 # Node - deads2k # API Machinery - derekwaynecarr # Node - dgrisonnet # Instrumentation - dims # Architecture - eddiezane # CLI - enj # Auth - gjtempleton # Autoscaling - janetkuo # Apps - jayunit100 # Windows - jpbetz # API Machinery - jsafrane # Storage - jsturtevant # Windows - justinsb # Cluster Lifecycle - kow3ns # Apps - liggitt # Auth - logicalhan # Instrumentation - luxas # Cluster Lifecycle - maciekpytel # Autoscaling - marosset # Windows - mikezappa87 # Network - mrunalp # Node - msau42 # Storage - rexagod # Instrumentation - richabanker # Instrumentation - saad-ali # Storage - sergeykanzhelev # Node - shaneutt # Network - soltysh # Apps, CLI - sttts # API Machinery - tallclair # Auth - thockin # Network - xing-yang # Storage - wojtek-t # Scalability # conformance aliases https://git.k8s.io/enhancements/keps/sig-architecture/20190412-conformance-behaviors.md conformance-behavior-approvers: - smarterclayton - johnbelamaric - spiffxp - dims kubernetes-kubernetes-40e1192/README.md000066400000000000000000000104431504711711200175550ustar00rootroot00000000000000# Kubernetes (K8s) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/569/badge)](https://bestpractices.coreinfrastructure.org/projects/569) [![Go Report Card](https://goreportcard.com/badge/github.com/kubernetes/kubernetes)](https://goreportcard.com/report/github.com/kubernetes/kubernetes) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/kubernetes/kubernetes?sort=semver) ---- Kubernetes, also known as K8s, is an open source system for managing [containerized applications] across multiple hosts. It provides basic mechanisms for the deployment, maintenance, and scaling of applications. Kubernetes builds upon a decade and a half of experience at Google running production workloads at scale using a system called [Borg], combined with best-of-breed ideas and practices from the community. Kubernetes is hosted by the Cloud Native Computing Foundation ([CNCF]). If your company wants to help shape the evolution of technologies that are container-packaged, dynamically scheduled, and microservices-oriented, consider joining the CNCF. For details about who's involved and how Kubernetes plays a role, read the CNCF [announcement]. ---- ## To start using K8s See our documentation on [kubernetes.io]. Take a free course on [Scalable Microservices with Kubernetes]. To use Kubernetes code as a library in other applications, see the [list of published components](https://git.k8s.io/kubernetes/staging/README.md). Use of the `k8s.io/kubernetes` module or `k8s.io/kubernetes/...` packages as libraries is not supported. ## To start developing K8s The [community repository] hosts all information about building Kubernetes from source, how to contribute code and documentation, who to contact about what, etc. If you want to build Kubernetes right away there are two options: ##### You have a working [Go environment]. ``` git clone https://github.com/kubernetes/kubernetes cd kubernetes make ``` ##### You have a working [Docker environment]. ``` git clone https://github.com/kubernetes/kubernetes cd kubernetes make quick-release ``` For the full story, head over to the [developer's documentation]. ## Support If you need support, start with the [troubleshooting guide], and work your way through the process that we've outlined. That said, if you have questions, reach out to us [one way or another][communication]. [announcement]: https://cncf.io/news/announcement/2015/07/new-cloud-native-computing-foundation-drive-alignment-among-container [Borg]: https://research.google.com/pubs/pub43438.html?authuser=1 [CNCF]: https://www.cncf.io/about [communication]: https://git.k8s.io/community/communication [community repository]: https://git.k8s.io/community [containerized applications]: https://kubernetes.io/docs/concepts/overview/what-is-kubernetes/ [developer's documentation]: https://git.k8s.io/community/contributors/devel#readme [Docker environment]: https://docs.docker.com/engine [Go environment]: https://go.dev/doc/install [kubernetes.io]: https://kubernetes.io [Scalable Microservices with Kubernetes]: https://www.udacity.com/course/scalable-microservices-with-kubernetes--ud615 [troubleshooting guide]: https://kubernetes.io/docs/tasks/debug/ ## Community Meetings The [Calendar](https://www.kubernetes.dev/resources/calendar/) has the list of all the meetings in the Kubernetes community in a single location. ## Adopters The [User Case Studies](https://kubernetes.io/case-studies/) website has real-world use cases of organizations across industries that are deploying/migrating to Kubernetes. ## Governance Kubernetes project is governed by a framework of principles, values, policies and processes to help our community and constituents towards our shared goals. The [Kubernetes Community](https://github.com/kubernetes/community/blob/master/governance.md) is the launching point for learning about how we organize ourselves. The [Kubernetes Steering community repo](https://github.com/kubernetes/steering) is used by the Kubernetes Steering Committee, which oversees governance of the Kubernetes project. ## Roadmap The [Kubernetes Enhancements repo](https://github.com/kubernetes/enhancements) provides information about Kubernetes releases, as well as feature tracking and backlogs. kubernetes-kubernetes-40e1192/SECURITY_CONTACTS000066400000000000000000000012311504711711200207610ustar00rootroot00000000000000# Defined below are the security contacts for this repo. # # They are the contact point for the Security Response Committee (SRC) to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # To contact the SRC, please see https://github.com/kubernetes/committee-security-response#contacting-the-src # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ committee-security-response kubernetes-kubernetes-40e1192/SUPPORT.md000066400000000000000000000020651504711711200177750ustar00rootroot00000000000000## Support for deploying and using Kubernetes Welcome to Kubernetes! We use GitHub for tracking bugs and feature requests. This isn't the right place to get support for using Kubernetes, but the following resources are available below, thanks for understanding. ### Stack Overflow The Kubernetes Community is active on Stack Overflow, you can post your questions there: * [Kubernetes on Stack Overflow](https://stackoverflow.com/questions/tagged/kubernetes) * Here are some tips for [about how to ask good questions](https://stackoverflow.com/help/how-to-ask). * Don't forget to check to see [what's on topic](https://stackoverflow.com/help/on-topic). ### Documentation * [User Documentation](https://kubernetes.io/docs/) * [Troubleshooting Guide](https://kubernetes.io/docs/tasks/debug/) ### Real-time Chat * [Slack](https://kubernetes.slack.com) ([registration](https://slack.k8s.io)): The `#kubernetes-users` and `#kubernetes-novice` channels are usual places where people offer support. ### Forum * [Kubernetes Official Forum](https://discuss.kubernetes.io) kubernetes-kubernetes-40e1192/cmd/000077500000000000000000000000001504711711200170375ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/cmd/OWNERS000066400000000000000000000005551504711711200200040ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners options: # make root approval non-recursive no_parent_owners: true reviewers: - dchen1107 - dims - liggitt - mikedanese - smarterclayton - thockin - wojtek-t approvers: - dchen1107 - dims - liggitt - mikedanese - smarterclayton - thockin - wojtek-t emeritus_approvers: - lavalamp kubernetes-kubernetes-40e1192/cmd/kubectl/000077500000000000000000000000001504711711200204705ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/cmd/kubectl/OWNERS000066400000000000000000000002341504711711200214270ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-cli-maintainers reviewers: - sig-cli-reviewers labels: - area/kubectl - sig/cli kubernetes-kubernetes-40e1192/cmd/kubectl/kubectl.go000066400000000000000000000017031504711711200224510ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "k8s.io/component-base/cli" "k8s.io/kubectl/pkg/cmd" "k8s.io/kubectl/pkg/cmd/util" // Import to initialize client auth plugins. _ "k8s.io/client-go/plugin/pkg/client/auth" ) func main() { command := cmd.NewDefaultKubectlCommand() if err := cli.RunNoErrOutput(command); err != nil { // Pretty-print the error and exit with an error. util.CheckErr(err) } } kubernetes-kubernetes-40e1192/code-of-conduct.md000066400000000000000000000002241504711711200215650ustar00rootroot00000000000000# Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) kubernetes-kubernetes-40e1192/go.mod000066400000000000000000000271331504711711200174100ustar00rootroot00000000000000// This is a generated file. Do not edit directly. // Ensure you've carefully read // https://git.k8s.io/community/contributors/devel/sig-architecture/vendor.md // Run hack/pin-dependency.sh to change pinned dependency versions. // Run hack/update-vendor.sh to update go.mod files and the vendor directory. module k8s.io/kubernetes go 1.24.0 godebug default=go1.24 require ( bitbucket.org/bertimus9/systemstat v0.5.0 github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/hnslib v0.1.1 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/blang/semver/v4 v4.0.0 github.com/container-storage-interface/spec v1.9.0 github.com/coredns/corefile-migration v1.0.25 github.com/coreos/go-oidc v2.3.0+incompatible github.com/coreos/go-systemd/v22 v22.5.0 github.com/cpuguy83/go-md2man/v2 v2.0.4 github.com/cyphar/filepath-securejoin v0.4.1 github.com/distribution/reference v0.6.0 github.com/docker/go-units v0.5.0 github.com/emicklei/go-restful/v3 v3.11.0 github.com/fsnotify/fsnotify v1.7.0 github.com/go-logr/logr v1.4.2 github.com/go-openapi/jsonreference v0.20.2 github.com/godbus/dbus/v5 v5.1.0 github.com/gogo/protobuf v1.3.2 github.com/google/cadvisor v0.52.1 github.com/google/cel-go v0.23.2 github.com/google/gnostic-models v0.6.9 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 github.com/libopenstorage/openstorage v1.0.0 github.com/lithammer/dedent v1.1.0 github.com/moby/ipvs v1.1.0 github.com/moby/sys/userns v0.1.0 github.com/mrunalp/fileutils v0.5.1 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.35.1 github.com/opencontainers/cgroups v0.0.1 github.com/opencontainers/selinux v1.11.1 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.62.0 github.com/robfig/cron/v3 v3.0.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.10.0 github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa github.com/vishvananda/netns v0.0.4 go.etcd.io/etcd/api/v3 v3.5.21 go.etcd.io/etcd/client/pkg/v3 v3.5.21 go.etcd.io/etcd/client/v3 v3.5.21 go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 go.opentelemetry.io/otel v1.33.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 go.opentelemetry.io/otel/metric v1.33.0 go.opentelemetry.io/otel/sdk v1.33.0 go.opentelemetry.io/otel/trace v1.33.0 go.opentelemetry.io/proto/otlp v1.4.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.36.0 golang.org/x/net v0.38.0 golang.org/x/oauth2 v0.27.0 golang.org/x/sync v0.12.0 golang.org/x/sys v0.31.0 golang.org/x/term v0.30.0 golang.org/x/time v0.9.0 golang.org/x/tools v0.26.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 google.golang.org/grpc v1.68.1 google.golang.org/protobuf v1.36.5 gopkg.in/evanphx/json-patch.v4 v4.12.0 gopkg.in/go-jose/go-jose.v2 v2.6.3 k8s.io/api v0.0.0 k8s.io/apiextensions-apiserver v0.0.0 k8s.io/apimachinery v0.0.0 k8s.io/apiserver v0.0.0 k8s.io/cli-runtime v0.0.0 k8s.io/client-go v0.0.0 k8s.io/cloud-provider v0.0.0 k8s.io/cluster-bootstrap v0.0.0 k8s.io/code-generator v0.0.0 k8s.io/component-base v0.0.0 k8s.io/component-helpers v0.0.0 k8s.io/controller-manager v0.0.0 k8s.io/cri-api v0.0.0 k8s.io/cri-client v0.0.0 k8s.io/csi-translation-lib v0.0.0 k8s.io/dynamic-resource-allocation v0.0.0 k8s.io/endpointslice v0.0.0 k8s.io/externaljwt v0.0.0 k8s.io/klog/v2 v2.130.1 k8s.io/kms v0.0.0 k8s.io/kube-aggregator v0.0.0 k8s.io/kube-controller-manager v0.0.0 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff k8s.io/kube-proxy v0.0.0 k8s.io/kube-scheduler v0.0.0 k8s.io/kubectl v0.0.0 k8s.io/kubelet v0.0.0 k8s.io/metrics v0.0.0 k8s.io/mount-utils v0.0.0 k8s.io/pod-security-admission v0.0.0 k8s.io/sample-apiserver v0.0.0 k8s.io/system-validators v1.9.1 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/knftables v0.0.17 sigs.k8s.io/randfill v1.0.0 sigs.k8s.io/structured-merge-diff/v4 v4.6.0 sigs.k8s.io/yaml v1.4.0 ) require ( cel.dev/expr v0.19.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/containerd/containerd/api v1.8.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/ttrpc v1.2.6 // indirect github.com/containerd/typeurl/v2 v2.2.2 // indirect github.com/coredns/caddy v1.1.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/euank/go-kmsg-parser v2.0.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/camelcase v1.0.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/karrick/godirwalk v1.17.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.etcd.io/etcd/client/v2 v2.305.21 // indirect go.etcd.io/etcd/pkg/v3 v3.5.21 // indirect go.etcd.io/etcd/raft/v3 v3.5.21 // indirect go.etcd.io/etcd/server/v3 v3.5.21 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/kustomize/api v0.19.0 // indirect sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 // indirect sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect ) replace ( k8s.io/api => ./staging/src/k8s.io/api k8s.io/apiextensions-apiserver => ./staging/src/k8s.io/apiextensions-apiserver k8s.io/apimachinery => ./staging/src/k8s.io/apimachinery k8s.io/apiserver => ./staging/src/k8s.io/apiserver k8s.io/cli-runtime => ./staging/src/k8s.io/cli-runtime k8s.io/client-go => ./staging/src/k8s.io/client-go k8s.io/cloud-provider => ./staging/src/k8s.io/cloud-provider k8s.io/cluster-bootstrap => ./staging/src/k8s.io/cluster-bootstrap k8s.io/code-generator => ./staging/src/k8s.io/code-generator k8s.io/component-base => ./staging/src/k8s.io/component-base k8s.io/component-helpers => ./staging/src/k8s.io/component-helpers k8s.io/controller-manager => ./staging/src/k8s.io/controller-manager k8s.io/cri-api => ./staging/src/k8s.io/cri-api k8s.io/cri-client => ./staging/src/k8s.io/cri-client k8s.io/csi-translation-lib => ./staging/src/k8s.io/csi-translation-lib k8s.io/dynamic-resource-allocation => ./staging/src/k8s.io/dynamic-resource-allocation k8s.io/endpointslice => ./staging/src/k8s.io/endpointslice k8s.io/externaljwt => ./staging/src/k8s.io/externaljwt k8s.io/kms => ./staging/src/k8s.io/kms k8s.io/kube-aggregator => ./staging/src/k8s.io/kube-aggregator k8s.io/kube-controller-manager => ./staging/src/k8s.io/kube-controller-manager k8s.io/kube-proxy => ./staging/src/k8s.io/kube-proxy k8s.io/kube-scheduler => ./staging/src/k8s.io/kube-scheduler k8s.io/kubectl => ./staging/src/k8s.io/kubectl k8s.io/kubelet => ./staging/src/k8s.io/kubelet k8s.io/metrics => ./staging/src/k8s.io/metrics k8s.io/mount-utils => ./staging/src/k8s.io/mount-utils k8s.io/pod-security-admission => ./staging/src/k8s.io/pod-security-admission k8s.io/sample-apiserver => ./staging/src/k8s.io/sample-apiserver k8s.io/sample-cli-plugin => ./staging/src/k8s.io/sample-cli-plugin k8s.io/sample-controller => ./staging/src/k8s.io/sample-controller ) kubernetes-kubernetes-40e1192/go.sum000066400000000000000000001762731504711711200174470ustar00rootroot00000000000000bitbucket.org/bertimus9/systemstat v0.5.0 h1:n0aLnh2Jo4nBUBym9cE5PJDG8GT6g+4VuS2Ya2jYYpA= bitbucket.org/bertimus9/systemstat v0.5.0/go.mod h1:EkUWPp8lKFPMXP8vnbpT5JDI0W/sTiLZAvN8ONWErHY= cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= 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.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= cloud.google.com/go/accessapproval v1.7.4/go.mod h1:/aTEh45LzplQgFYdQdwPMR9YdX0UlhBmvB84uAmQKUc= cloud.google.com/go/accesscontextmanager v1.8.4/go.mod h1:ParU+WbMpD34s5JFEnGAnPBYAgUHozaTmDJU7aCU9+M= cloud.google.com/go/aiplatform v1.58.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= cloud.google.com/go/analytics v0.22.0/go.mod h1:eiROFQKosh4hMaNhF85Oc9WO97Cpa7RggD40e/RBy8w= cloud.google.com/go/apigateway v1.6.4/go.mod h1:0EpJlVGH5HwAN4VF4Iec8TAzGN1aQgbxAWGJsnPCGGY= cloud.google.com/go/apigeeconnect v1.6.4/go.mod h1:CapQCWZ8TCjnU0d7PobxhpOdVz/OVJ2Hr/Zcuu1xFx0= cloud.google.com/go/apigeeregistry v0.8.2/go.mod h1:h4v11TDGdeXJDJvImtgK2AFVvMIgGWjSb0HRnBSjcX8= cloud.google.com/go/appengine v1.8.4/go.mod h1:TZ24v+wXBujtkK77CXCpjZbnuTvsFNT41MUaZ28D6vg= cloud.google.com/go/area120 v0.8.4/go.mod h1:jfawXjxf29wyBXr48+W+GyX/f8fflxp642D/bb9v68M= cloud.google.com/go/artifactregistry v1.14.6/go.mod h1:np9LSFotNWHcjnOgh8UVK0RFPCTUGbO0ve3384xyHfE= cloud.google.com/go/asset v1.17.0/go.mod h1:yYLfUD4wL4X589A9tYrv4rFrba0QlDeag0CMcM5ggXU= cloud.google.com/go/assuredworkloads v1.11.4/go.mod h1:4pwwGNwy1RP0m+y12ef3Q/8PaiWrIDQ6nD2E8kvWI9U= cloud.google.com/go/automl v1.13.4/go.mod h1:ULqwX/OLZ4hBVfKQaMtxMSTlPx0GqGbWN8uA/1EqCP8= cloud.google.com/go/baremetalsolution v1.2.3/go.mod h1:/UAQ5xG3faDdy180rCUv47e0jvpp3BFxT+Cl0PFjw5g= cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU= cloud.google.com/go/beyondcorp v1.0.3/go.mod h1:HcBvnEd7eYr+HGDd5ZbuVmBYX019C6CEXBonXbCVwJo= cloud.google.com/go/bigquery v1.58.0/go.mod h1:0eh4mWNY0KrBTjUzLjoYImapGORq9gEPT7MWjCy9lik= cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk= cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU= cloud.google.com/go/certificatemanager v1.7.4/go.mod h1:FHAylPe/6IIKuaRmHbjbdLhGhVQ+CWHSD5Jq0k4+cCE= cloud.google.com/go/channel v1.17.4/go.mod h1:QcEBuZLGGrUMm7kNj9IbU1ZfmJq2apotsV83hbxX7eE= cloud.google.com/go/cloudbuild v1.15.0/go.mod h1:eIXYWmRt3UtggLnFGx4JvXcMj4kShhVzGndL1LwleEM= cloud.google.com/go/clouddms v1.7.3/go.mod h1:fkN2HQQNUYInAU3NQ3vRLkV2iWs8lIdmBKOx4nrL6Hc= cloud.google.com/go/cloudtasks v1.12.4/go.mod h1:BEPu0Gtt2dU6FxZHNqqNdGqIG86qyWKBPGnsb7udGY0= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= cloud.google.com/go/containeranalysis v0.11.3/go.mod h1:kMeST7yWFQMGjiG9K7Eov+fPNQcGhb8mXj/UcTiWw9U= cloud.google.com/go/datacatalog v1.19.2/go.mod h1:2YbODwmhpLM4lOFe3PuEhHK9EyTzQJ5AXgIy7EDKTEE= cloud.google.com/go/dataflow v0.9.4/go.mod h1:4G8vAkHYCSzU8b/kmsoR2lWyHJD85oMJPHMtan40K8w= cloud.google.com/go/dataform v0.9.1/go.mod h1:pWTg+zGQ7i16pyn0bS1ruqIE91SdL2FDMvEYu/8oQxs= cloud.google.com/go/datafusion v1.7.4/go.mod h1:BBs78WTOLYkT4GVZIXQCZT3GFpkpDN4aBY4NDX/jVlM= cloud.google.com/go/datalabeling v0.8.4/go.mod h1:Z1z3E6LHtffBGrNUkKwbwbDxTiXEApLzIgmymj8A3S8= cloud.google.com/go/dataplex v1.14.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c= cloud.google.com/go/dataproc/v2 v2.3.0/go.mod h1:G5R6GBc9r36SXv/RtZIVfB8SipI+xVn0bX5SxUzVYbY= cloud.google.com/go/dataqna v0.8.4/go.mod h1:mySRKjKg5Lz784P6sCov3p1QD+RZQONRMRjzGNcFd0c= cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= cloud.google.com/go/datastream v1.10.3/go.mod h1:YR0USzgjhqA/Id0Ycu1VvZe8hEWwrkjuXrGbzeDOSEA= cloud.google.com/go/deploy v1.17.0/go.mod h1:XBr42U5jIr64t92gcpOXxNrqL2PStQCXHuKK5GRUuYo= cloud.google.com/go/dialogflow v1.48.1/go.mod h1:C1sjs2/g9cEwjCltkKeYp3FFpz8BOzNondEaAlCpt+A= cloud.google.com/go/dlp v1.11.1/go.mod h1:/PA2EnioBeXTL/0hInwgj0rfsQb3lpE3R8XUJxqUNKI= cloud.google.com/go/documentai v1.23.7/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g= cloud.google.com/go/domains v0.9.4/go.mod h1:27jmJGShuXYdUNjyDG0SodTfT5RwLi7xmH334Gvi3fY= cloud.google.com/go/edgecontainer v1.1.4/go.mod h1:AvFdVuZuVGdgaE5YvlL1faAoa1ndRR/5XhXZvPBHbsE= cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= cloud.google.com/go/essentialcontacts v1.6.5/go.mod h1:jjYbPzw0x+yglXC890l6ECJWdYeZ5dlYACTFL0U/VuM= cloud.google.com/go/eventarc v1.13.3/go.mod h1:RWH10IAZIRcj1s/vClXkBgMHwh59ts7hSWcqD3kaclg= cloud.google.com/go/filestore v1.8.0/go.mod h1:S5JCxIbFjeBhWMTfIYH2Jx24J6BqjwpkkPl+nBA5DlI= cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= cloud.google.com/go/functions v1.15.4/go.mod h1:CAsTc3VlRMVvx+XqXxKqVevguqJpnVip4DdonFsX28I= cloud.google.com/go/gkebackup v1.3.4/go.mod h1:gLVlbM8h/nHIs09ns1qx3q3eaXcGSELgNu1DWXYz1HI= cloud.google.com/go/gkeconnect v0.8.4/go.mod h1:84hZz4UMlDCKl8ifVW8layK4WHlMAFeq8vbzjU0yJkw= cloud.google.com/go/gkehub v0.14.4/go.mod h1:Xispfu2MqnnFt8rV/2/3o73SK1snL8s9dYJ9G2oQMfc= cloud.google.com/go/gkemulticloud v1.1.0/go.mod h1:7NpJBN94U6DY1xHIbsDqB2+TFZUfjLUKLjUX8NGLor0= cloud.google.com/go/gsuiteaddons v1.6.4/go.mod h1:rxtstw7Fx22uLOXBpsvb9DUbC+fiXs7rF4U29KHM/pE= cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= cloud.google.com/go/iap v1.9.3/go.mod h1:DTdutSZBqkkOm2HEOTBzhZxh2mwwxshfD/h3yofAiCw= cloud.google.com/go/ids v1.4.4/go.mod h1:z+WUc2eEl6S/1aZWzwtVNWoSZslgzPxAboS0lZX0HjI= cloud.google.com/go/iot v1.7.4/go.mod h1:3TWqDVvsddYBG++nHSZmluoCAVGr1hAcabbWZNKEZLk= cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI= cloud.google.com/go/language v1.12.2/go.mod h1:9idWapzr/JKXBBQ4lWqVX/hcadxB194ry20m/bTrhWc= cloud.google.com/go/lifesciences v0.9.4/go.mod h1:bhm64duKhMi7s9jR9WYJYvjAFJwRqNj+Nia7hF0Z7JA= cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE= cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= cloud.google.com/go/managedidentities v1.6.4/go.mod h1:WgyaECfHmF00t/1Uk8Oun3CQ2PGUtjc3e9Alh79wyiM= cloud.google.com/go/maps v1.6.3/go.mod h1:VGAn809ADswi1ASofL5lveOHPnE6Rk/SFTTBx1yuOLw= cloud.google.com/go/mediatranslation v0.8.4/go.mod h1:9WstgtNVAdN53m6TQa5GjIjLqKQPXe74hwSCxUP6nj4= cloud.google.com/go/memcache v1.10.4/go.mod h1:v/d8PuC8d1gD6Yn5+I3INzLR01IDn0N4Ym56RgikSI0= cloud.google.com/go/metastore v1.13.3/go.mod h1:K+wdjXdtkdk7AQg4+sXS8bRrQa9gcOr+foOMF2tqINE= cloud.google.com/go/monitoring v1.17.0/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= cloud.google.com/go/networkconnectivity v1.14.3/go.mod h1:4aoeFdrJpYEXNvrnfyD5kIzs8YtHg945Og4koAjHQek= cloud.google.com/go/networkmanagement v1.9.3/go.mod h1:y7WMO1bRLaP5h3Obm4tey+NquUvB93Co1oh4wpL+XcU= cloud.google.com/go/networksecurity v0.9.4/go.mod h1:E9CeMZ2zDsNBkr8axKSYm8XyTqNhiCHf1JO/Vb8mD1w= cloud.google.com/go/notebooks v1.11.2/go.mod h1:z0tlHI/lREXC8BS2mIsUeR3agM1AkgLiS+Isov3SS70= cloud.google.com/go/optimization v1.6.2/go.mod h1:mWNZ7B9/EyMCcwNl1frUGEuY6CPijSkz88Fz2vwKPOY= cloud.google.com/go/orchestration v1.8.4/go.mod h1:d0lywZSVYtIoSZXb0iFjv9SaL13PGyVOKDxqGxEf/qI= cloud.google.com/go/orgpolicy v1.12.0/go.mod h1:0+aNV/nrfoTQ4Mytv+Aw+stBDBjNf4d8fYRA9herfJI= cloud.google.com/go/osconfig v1.12.4/go.mod h1:B1qEwJ/jzqSRslvdOCI8Kdnp0gSng0xW4LOnIebQomA= cloud.google.com/go/oslogin v1.13.0/go.mod h1:xPJqLwpTZ90LSE5IL1/svko+6c5avZLluiyylMb/sRA= cloud.google.com/go/phishingprotection v0.8.4/go.mod h1:6b3kNPAc2AQ6jZfFHioZKg9MQNybDg4ixFd4RPZZ2nE= cloud.google.com/go/policytroubleshooter v1.10.2/go.mod h1:m4uF3f6LseVEnMV6nknlN2vYGRb+75ylQwJdnOXfnv0= cloud.google.com/go/privatecatalog v0.9.4/go.mod h1:SOjm93f+5hp/U3PqMZAHTtBtluqLygrDrVO8X8tYtG0= cloud.google.com/go/pubsub v1.34.0/go.mod h1:alj4l4rBg+N3YTFDDC+/YyFTs6JAjam2QfYsddcAW4c= cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0= cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w= cloud.google.com/go/recommendationengine v0.8.4/go.mod h1:GEteCf1PATl5v5ZsQ60sTClUE0phbWmo3rQ1Js8louU= cloud.google.com/go/recommender v1.12.0/go.mod h1:+FJosKKJSId1MBFeJ/TTyoGQZiEelQQIZMKYYD8ruK4= cloud.google.com/go/redis v1.14.1/go.mod h1:MbmBxN8bEnQI4doZPC1BzADU4HGocHBk2de3SbgOkqs= cloud.google.com/go/resourcemanager v1.9.4/go.mod h1:N1dhP9RFvo3lUfwtfLWVxfUWq8+KUQ+XLlHLH3BoFJ0= cloud.google.com/go/resourcesettings v1.6.4/go.mod h1:pYTTkWdv2lmQcjsthbZLNBP4QW140cs7wqA3DuqErVI= cloud.google.com/go/retail v1.14.4/go.mod h1:l/N7cMtY78yRnJqp5JW8emy7MB1nz8E4t2yfOmklYfg= cloud.google.com/go/run v1.3.3/go.mod h1:WSM5pGyJ7cfYyYbONVQBN4buz42zFqwG67Q3ch07iK4= cloud.google.com/go/scheduler v1.10.5/go.mod h1:MTuXcrJC9tqOHhixdbHDFSIuh7xZF2IysiINDuiq6NI= cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w= cloud.google.com/go/security v1.15.4/go.mod h1:oN7C2uIZKhxCLiAAijKUCuHLZbIt/ghYEo8MqwD/Ty4= cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM= cloud.google.com/go/servicedirectory v1.11.3/go.mod h1:LV+cHkomRLr67YoQy3Xq2tUXBGOs5z5bPofdq7qtiAw= cloud.google.com/go/shell v1.7.4/go.mod h1:yLeXB8eKLxw0dpEmXQ/FjriYrBijNsONpwnWsdPqlKM= cloud.google.com/go/spanner v1.55.0/go.mod h1:HXEznMUVhC+PC+HDyo9YFG2Ajj5BQDkcbqB9Z2Ffxi0= cloud.google.com/go/speech v1.21.0/go.mod h1:wwolycgONvfz2EDU8rKuHRW3+wc9ILPsAWoikBEWavY= cloud.google.com/go/storagetransfer v1.10.3/go.mod h1:Up8LY2p6X68SZ+WToswpQbQHnJpOty/ACcMafuey8gc= cloud.google.com/go/talent v1.6.5/go.mod h1:Mf5cma696HmE+P2BWJ/ZwYqeJXEeU0UqjHFXVLadEDI= cloud.google.com/go/texttospeech v1.7.4/go.mod h1:vgv0002WvR4liGuSd5BJbWy4nDn5Ozco0uJymY5+U74= cloud.google.com/go/tpu v1.6.4/go.mod h1:NAm9q3Rq2wIlGnOhpYICNI7+bpBebMJbh0yyp3aNw1Y= cloud.google.com/go/trace v1.10.4/go.mod h1:Nso99EDIK8Mj5/zmB+iGr9dosS/bzWCJ8wGmE6TXNWY= cloud.google.com/go/translate v1.10.0/go.mod h1:Kbq9RggWsbqZ9W5YpM94Q1Xv4dshw/gr/SHfsl5yCZ0= cloud.google.com/go/video v1.20.3/go.mod h1:TnH/mNZKVHeNtpamsSPygSR0iHtvrR/cW1/GDjN5+GU= cloud.google.com/go/videointelligence v1.11.4/go.mod h1:kPBMAYsTPFiQxMLmmjpcZUMklJp3nC9+ipJJtprccD8= cloud.google.com/go/vision/v2 v2.7.5/go.mod h1:GcviprJLFfK9OLf0z8Gm6lQb6ZFUulvpZws+mm6yPLM= cloud.google.com/go/vmmigration v1.7.4/go.mod h1:yBXCmiLaB99hEl/G9ZooNx2GyzgsjKnw5fWcINRgD70= cloud.google.com/go/vmwareengine v1.0.3/go.mod h1:QSpdZ1stlbfKtyt6Iu19M6XRxjmXO+vb5a/R6Fvy2y4= cloud.google.com/go/vpcaccess v1.7.4/go.mod h1:lA0KTvhtEOb/VOdnH/gwPuOzGgM+CWsmGu6bb4IoMKk= cloud.google.com/go/webrisk v1.9.4/go.mod h1:w7m4Ib4C+OseSr2GL66m0zMBywdrVNTDKsdEsfMl7X0= cloud.google.com/go/websecurityscanner v1.6.4/go.mod h1:mUiyMQ+dGpPPRkHgknIZeCzSHJ45+fY4F52nZFDHm2o= cloud.google.com/go/workflows v1.12.3/go.mod h1:fmOUeeqEwPzIU81foMjTRQIdwQHADi/vEr1cx9R1m5g= 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/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab h1:UKkYhof1njT1/xq4SEg5z+VpTgjmNeHwPGRQl7takDI= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hnslib v0.1.1 h1:JsZy681SnvSOUAfCZVAxkX4LgQGp+CZZwPbLV0/pdF8= github.com/Microsoft/hnslib v0.1.1/go.mod h1:DRQR4IjLae6WHYVhW7uqe44hmFUiNhmaWA+jwMbz5tM= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 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/aws/aws-sdk-go-v2 v1.30.1/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/config v1.27.24/go.mod h1:aXzi6QJTuQRVVusAO8/NxpdTeTyr/wRcybdDtfUwJSs= github.com/aws/aws-sdk-go-v2/credentials v1.17.24/go.mod h1:Hld7tmnAkoBQdTMNYZGzztzKRdA4fCdn9L83LOoigac= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9/go.mod h1:WQr3MY7AxGNxaqAtsDWn+fBxmd4XvLkzeqQ8P1VM0/w= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13/go.mod h1:+rdA6ZLpaSeM7tSg/B0IEDinCIBJGmW8rKDFkYpP04g= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13/go.mod h1:i+kbfa76PQbWw/ULoWnp51EYVWH4ENln76fLQE3lXT8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15/go.mod h1:9xWJ3Q/S6Ojusz1UIkfycgD1mGirJfLLKqq3LPT7WN8= github.com/aws/aws-sdk-go-v2/service/sso v1.22.1/go.mod h1:/vWdhoIoYA5hYoPZ6fm7Sv4d8701PiG5VKe8/pPJL60= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2/go.mod h1:xyFHA4zGxgYkdD73VeezHt3vSKEG9EmFnGwoKlP00u4= github.com/aws/aws-sdk-go-v2/service/sts v1.30.1/go.mod h1:jiNR3JqT15Dm+QWq2SRgh0x0bCNSRP2L25+CqPNpJlQ= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 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.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.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/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/container-storage-interface/spec v1.9.0 h1:zKtX4STsq31Knz3gciCYCi1SXtO2HJDecIjDVboYavY= github.com/container-storage-interface/spec v1.9.0/go.mod h1:ZfDu+3ZRyeVqxZM0Ds19MVLkN2d1XJ5MAfi1L3VjlT0= github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 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/ttrpc v1.2.6 h1:zG+Kn5EZ6MUYCS1t2Hmt2J4tMVaLSFEJVOraDQwNPC4= github.com/containerd/ttrpc v1.2.6/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.2 h1:3jN/k2ysKuPCsln5Qv8bzR9cxal8XjkxPogJfSNO31k= github.com/containerd/typeurl/v2 v2.2.2/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= github.com/coredns/corefile-migration v1.0.25 h1:/XexFhM8FFlFLTS/zKNEWgIZ8Gl5GaWrHsMarGj/PRQ= github.com/coredns/corefile-migration v1.0.25/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0= github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/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= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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/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/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/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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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/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/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.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/euank/go-kmsg-parser v2.0.0+incompatible h1:cHD53+PLQuuQyLZeriD1V/esuG4MuU0Pjs5y6iknohY= github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= 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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 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-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 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/protobuf v1.2.0/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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cadvisor v0.52.1 h1:sC8SZ6jio9ds+P2dk51bgbeYeufxo55n0X3tmrpA9as= github.com/google/cadvisor v0.52.1/go.mod h1:OAhPcx1nOm5YwMh/JhpUOMKyv1YKLRtS9KgzWPndHmA= github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/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/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 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.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo= github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 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= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/libopenstorage/openstorage v1.0.0 h1:GLPam7/0mpdP8ZZtKjbfcXJBTIA/T1O6CBErVEFEyIM= github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= 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/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= 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/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/ipvs v1.1.0 h1:ONN4pGaZQgAx+1Scz5RvWV4Q7Gb+mvfRh3NsPS+1XQQ= github.com/moby/ipvs v1.1.0/go.mod h1:4VJMWuf098bsUMmZEiD4Tjk/O7mOn3l1PTD3s4OoYAs= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q= github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= 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-20190716064945-2f068394615f/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/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/cgroups v0.0.1 h1:MXjMkkFpKv6kpuirUa4USFBas573sSAY082B4CiHEVA= github.com/opencontainers/cgroups v0.0.1/go.mod h1:s8lktyhlGUqM7OSRL5P7eAW6Wb+kWPNvt4qvVfzA5vs= 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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v1.2.5/go.mod h1:dOQeFo29xZKBNeRBI0B19mJtfHv68YgCTh1X+YphA+4= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8= github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.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/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= 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/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 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/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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa h1:iAhToRwOrdk+pKzclvLM7nKZhsg8f7dVrgkFccDUbUw= github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA= go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8= go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= go.etcd.io/etcd/pkg/v3 v3.5.21 h1:jUItxeKyrDuVuWhdh0HtjUANwyuzcb7/FAeUfABmQsk= go.etcd.io/etcd/pkg/v3 v3.5.21/go.mod h1:wpZx8Egv1g4y+N7JAsqi2zoUiBIUWznLjqJbylDjWgU= go.etcd.io/etcd/raft/v3 v3.5.21 h1:dOmE0mT55dIUsX77TKBLq+RgyumsQuYeiRQnW/ylugk= go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs= go.etcd.io/etcd/server/v3 v3.5.21 h1:9w0/k12majtgarGmlMVuhwXRI2ob3/d1Ik3X5TKo0yU= go.etcd.io/etcd/server/v3 v3.5.21/go.mod h1:G1mOzdwuzKT1VRL7SqRchli/qcFrtLBTAQ4lV20sXXo= go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0 h1:Z6SbqeRZAl2OczfkFOqLx1BeYBDYehNjEnqluD7581Y= go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0/go.mod h1:XiglO+8SPMqM3Mqh5/rtxR1VHc63o8tb38QrU6tm4mU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo= go.opentelemetry.io/contrib/propagators/b3 v1.17.0/go.mod h1:IkfUfMpKWmynvvE0264trz0sf32NRTZL4nuAN9AbWRc= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 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.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-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-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog= k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/system-validators v1.9.1 h1:O8xrr08foamG+1uQjAdiTLt/fT+QQJ4QNREfCWvuOws= k8s.io/system-validators v1.9.1/go.mod h1:d4UVrxKu52s0BHU984Peb9VpIq4V9sd8xjTBV/waY/I= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/knftables v0.0.17 h1:wGchTyRF/iGTIjd+vRaR1m676HM7jB8soFtyr/148ic= sigs.k8s.io/knftables v0.0.17/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= sigs.k8s.io/kustomize/cmd/config v0.19.0/go.mod h1:29Vvdl26PidPLUDi7nfjYa/I0wHBkwCZp15Nlcc4y98= sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 h1:MWtRRDWCwQEeW2rnJTqJMuV6Agy56P53SkbVoJpN7wA= sigs.k8s.io/kustomize/kustomize/v5 v5.6.0/go.mod h1:XuuZiQF7WdcvZzEYyNww9A0p3LazCKeJmCjeycN8e1I= sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= kubernetes-kubernetes-40e1192/go.work000066400000000000000000000023141504711711200176050ustar00rootroot00000000000000// This is a generated file. Do not edit directly. go 1.24.0 godebug default=go1.24 use ( . ./staging/src/k8s.io/api ./staging/src/k8s.io/apiextensions-apiserver ./staging/src/k8s.io/apimachinery ./staging/src/k8s.io/apiserver ./staging/src/k8s.io/cli-runtime ./staging/src/k8s.io/client-go ./staging/src/k8s.io/cloud-provider ./staging/src/k8s.io/cluster-bootstrap ./staging/src/k8s.io/code-generator ./staging/src/k8s.io/component-base ./staging/src/k8s.io/component-helpers ./staging/src/k8s.io/controller-manager ./staging/src/k8s.io/cri-api ./staging/src/k8s.io/cri-client ./staging/src/k8s.io/csi-translation-lib ./staging/src/k8s.io/dynamic-resource-allocation ./staging/src/k8s.io/endpointslice ./staging/src/k8s.io/externaljwt ./staging/src/k8s.io/kms ./staging/src/k8s.io/kube-aggregator ./staging/src/k8s.io/kube-controller-manager ./staging/src/k8s.io/kube-proxy ./staging/src/k8s.io/kube-scheduler ./staging/src/k8s.io/kubectl ./staging/src/k8s.io/kubelet ./staging/src/k8s.io/metrics ./staging/src/k8s.io/mount-utils ./staging/src/k8s.io/pod-security-admission ./staging/src/k8s.io/sample-apiserver ./staging/src/k8s.io/sample-cli-plugin ./staging/src/k8s.io/sample-controller ) kubernetes-kubernetes-40e1192/go.work.sum000066400000000000000000000375451504711711200204260ustar00rootroot00000000000000cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= cloud.google.com/go/accessapproval v1.7.4 h1:ZvLvJ952zK8pFHINjpMBY5k7LTAp/6pBf50RDMRgBUI= cloud.google.com/go/accesscontextmanager v1.8.4 h1:Yo4g2XrBETBCqyWIibN3NHNPQKUfQqti0lI+70rubeE= cloud.google.com/go/aiplatform v1.58.0 h1:xyCAfpI4yUMOQ4VtHN/bdmxPQ8xoEkTwFM1nbVmuQhs= cloud.google.com/go/analytics v0.22.0 h1:w8KIgW8NRUHFVKjpkwCpLaHsr685tJ+ckPStOaSCZz0= cloud.google.com/go/apigateway v1.6.4 h1:VVIxCtVerchHienSlaGzV6XJGtEM9828Erzyr3miUGs= cloud.google.com/go/apigeeconnect v1.6.4 h1:jSoGITWKgAj/ssVogNE9SdsTqcXnryPzsulENSRlusI= cloud.google.com/go/apigeeregistry v0.8.2 h1:DSaD1iiqvELag+lV4VnnqUUFd8GXELu01tKVdWZrviE= cloud.google.com/go/appengine v1.8.4 h1:Qub3fqR7iA1daJWdzjp/Q0Jz0fUG0JbMc7Ui4E9IX/E= cloud.google.com/go/area120 v0.8.4 h1:YnSO8m02pOIo6AEOgiOoUDVbw4pf+bg2KLHi4rky320= cloud.google.com/go/artifactregistry v1.14.6 h1:/hQaadYytMdA5zBh+RciIrXZQBWK4vN7EUsrQHG+/t8= cloud.google.com/go/asset v1.17.0 h1:dLWfTnbwyrq/Kt8Tr2JiAbre1MEvS2Bl5cAMiYAy5Pg= cloud.google.com/go/assuredworkloads v1.11.4 h1:FsLSkmYYeNuzDm8L4YPfLWV+lQaUrJmH5OuD37t1k20= cloud.google.com/go/automl v1.13.4 h1:i9tOKXX+1gE7+rHpWKjiuPfGBVIYoWvLNIGpWgPtF58= cloud.google.com/go/baremetalsolution v1.2.3 h1:oQiFYYCe0vwp7J8ZmF6siVKEumWtiPFJMJcGuyDVRUk= cloud.google.com/go/batch v1.7.0 h1:AxuSPoL2fWn/rUyvWeNCNd0V2WCr+iHRCU9QO1PUmpY= cloud.google.com/go/beyondcorp v1.0.3 h1:VXf9SnrnSmj2BF2cHkoTHvOUp8gjsz1KJFOMW7czdsY= cloud.google.com/go/bigquery v1.58.0 h1:drSd9RcPVLJP2iFMimvOB9SCSIrcl+9HD4II03Oy7A0= cloud.google.com/go/billing v1.18.0 h1:GvKy4xLy1zF1XPbwP5NJb2HjRxhnhxjjXxvyZ1S/IAo= cloud.google.com/go/binaryauthorization v1.8.0 h1:PHS89lcFayWIEe0/s2jTBiEOtqghCxzc7y7bRNlifBs= cloud.google.com/go/certificatemanager v1.7.4 h1:5YMQ3Q+dqGpwUZ9X5sipsOQ1fLPsxod9HNq0+nrqc6I= cloud.google.com/go/channel v1.17.4 h1:yYHOORIM+wkBy3EdwArg/WL7Lg+SoGzlKH9o3Bw2/jE= cloud.google.com/go/cloudbuild v1.15.0 h1:9IHfEMWdCklJ1cwouoiQrnxmP0q3pH7JUt8Hqx4Qbck= cloud.google.com/go/clouddms v1.7.3 h1:xe/wJKz55VO1+L891a1EG9lVUgfHr9Ju/I3xh1nwF84= cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/contactcenterinsights v1.12.1 h1:EiGBeejtDDtr3JXt9W7xlhXyZ+REB5k2tBgVPVtmNb0= cloud.google.com/go/container v1.29.0 h1:jIltU529R2zBFvP8rhiG1mgeTcnT27KhU0H/1d6SQRg= cloud.google.com/go/containeranalysis v0.11.3 h1:5rhYLX+3a01drpREqBZVXR9YmWH45RnML++8NsCtuD8= cloud.google.com/go/datacatalog v1.19.2 h1:BV5sB7fPc8ccv/obwtHwQtCdLMAgI4KyaQWfkh8/mWg= cloud.google.com/go/dataflow v0.9.4 h1:7VmCNWcPJBS/srN2QnStTB6nu4Eb5TMcpkmtaPVhRt4= cloud.google.com/go/dataform v0.9.1 h1:jV+EsDamGX6cE127+QAcCR/lergVeeZdEQ6DdrxW3sQ= cloud.google.com/go/datafusion v1.7.4 h1:Q90alBEYlMi66zL5gMSGQHfbZLB55mOAg03DhwTTfsk= cloud.google.com/go/datalabeling v0.8.4 h1:zrq4uMmunf2KFDl/7dS6iCDBBAxBnKVDyw6+ajz3yu0= cloud.google.com/go/dataplex v1.14.0 h1:/WhVTR4v/L6ACKjlz/9CqkxkrVh2z7C44CLMUf0f60A= cloud.google.com/go/dataproc/v2 v2.3.0 h1:tTVP9tTxmc8fixxOd/8s6Q6Pz/+yzn7r7XdZHretQH0= cloud.google.com/go/dataqna v0.8.4 h1:NJnu1kAPamZDs/if3bJ3+Wb6tjADHKL83NUWsaIp2zg= cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= cloud.google.com/go/datastream v1.10.3 h1:Z2sKPIB7bT2kMW5Uhxy44ZgdJzxzE5uKjavoW+EuHEE= cloud.google.com/go/deploy v1.17.0 h1:P3SgJ+4rAktC2XqaI10G0ip/vzWluNBrC5VG0abMbLw= cloud.google.com/go/dialogflow v1.48.1 h1:1Uq2jDJzjJ3M4xYB608FCCFHfW3JmrTmHIxRSd7JGmY= cloud.google.com/go/dlp v1.11.1 h1:OFlXedmPP/5//X1hBEeq3D9kUVm9fb6ywYANlpv/EsQ= cloud.google.com/go/documentai v1.23.7 h1:hlYieOXUwiJ7HpBR/vEPfr8nfSxveLVzbqbUkSK0c/4= cloud.google.com/go/domains v0.9.4 h1:ua4GvsDztZ5F3xqjeLKVRDeOvJshf5QFgWGg1CKti3A= cloud.google.com/go/edgecontainer v1.1.4 h1:Szy3Q/N6bqgQGyxqjI+6xJZbmvPvnFHp3UZr95DKcQ0= cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0= cloud.google.com/go/essentialcontacts v1.6.5 h1:S2if6wkjR4JCEAfDtIiYtD+sTz/oXjh2NUG4cgT1y/Q= cloud.google.com/go/eventarc v1.13.3 h1:+pFmO4eu4dOVipSaFBLkmqrRYG94Xl/TQZFOeohkuqU= cloud.google.com/go/filestore v1.8.0 h1:/+wUEGwk3x3Kxomi2cP5dsR8+SIXxo7M0THDjreFSYo= cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= cloud.google.com/go/functions v1.15.4 h1:ZjdiV3MyumRM6++1Ixu6N0VV9LAGlCX4AhW6Yjr1t+U= cloud.google.com/go/gkebackup v1.3.4 h1:KhnOrr9A1tXYIYeXKqCKbCI8TL2ZNGiD3dm+d7BDUBg= cloud.google.com/go/gkeconnect v0.8.4 h1:1JLpZl31YhQDQeJ98tK6QiwTpgHFYRJwpntggpQQWis= cloud.google.com/go/gkehub v0.14.4 h1:J5tYUtb3r0cl2mM7+YHvV32eL+uZQ7lONyUZnPikCEo= cloud.google.com/go/gkemulticloud v1.1.0 h1:C2Suwn3uPz+Yy0bxVjTlsMrUCaDovkgvfdyIa+EnUOU= cloud.google.com/go/gsuiteaddons v1.6.4 h1:uuw2Xd37yHftViSI8J2hUcCS8S7SH3ZWH09sUDLW30Q= cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= cloud.google.com/go/iap v1.9.3 h1:M4vDbQ4TLXdaljXVZSwW7XtxpwXUUarY2lIs66m0aCM= cloud.google.com/go/ids v1.4.4 h1:VuFqv2ctf/A7AyKlNxVvlHTzjrEvumWaZflUzBPz/M4= cloud.google.com/go/iot v1.7.4 h1:m1WljtkZnvLTIRYW1YTOv5A6H1yKgLHR6nU7O8yf27w= cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM= cloud.google.com/go/language v1.12.2 h1:zg9uq2yS9PGIOdc0Kz/l+zMtOlxKWonZjjo5w5YPG2A= cloud.google.com/go/lifesciences v0.9.4 h1:rZEI/UxcxVKEzyoRS/kdJ1VoolNItRWjNN0Uk9tfexg= cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw= cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= cloud.google.com/go/managedidentities v1.6.4 h1:SF/u1IJduMqQQdJA4MDyivlIQ4SrV5qAawkr/ZEREkY= cloud.google.com/go/maps v1.6.3 h1:Qqs6Dza+PRp5CZO5AfgPnLwU1k3pp0IMFRDtLpT+aCA= cloud.google.com/go/mediatranslation v0.8.4 h1:VRCQfZB4s6jN0CSy7+cO3m4ewNwgVnaePanVCQh/9Z4= cloud.google.com/go/memcache v1.10.4 h1:cdex/ayDd294XBj2cGeMe6Y+H1JvhN8y78B9UW7pxuQ= cloud.google.com/go/metastore v1.13.3 h1:94l/Yxg9oBZjin2bzI79oK05feYefieDq0o5fjLSkC8= cloud.google.com/go/monitoring v1.17.0 h1:blrdvF0MkPPivSO041ihul7rFMhXdVp8Uq7F59DKXTU= cloud.google.com/go/networkconnectivity v1.14.3 h1:e9lUkCe2BexsqsUc2bjV8+gFBpQa54J+/F3qKVtW+wA= cloud.google.com/go/networkmanagement v1.9.3 h1:HsQk4FNKJUX04k3OI6gUsoveiHMGvDRqlaFM2xGyvqU= cloud.google.com/go/networksecurity v0.9.4 h1:947tNIPnj1bMGTIEBo3fc4QrrFKS5hh0bFVsHmFm4Vo= cloud.google.com/go/notebooks v1.11.2 h1:eTOTfNL1yM6L/PCtquJwjWg7ZZGR0URFaFgbs8kllbM= cloud.google.com/go/optimization v1.6.2 h1:iFsoexcp13cGT3k/Hv8PA5aK+FP7FnbhwDO9llnruas= cloud.google.com/go/orchestration v1.8.4 h1:kgwZ2f6qMMYIVBtUGGoU8yjYWwMTHDanLwM/CQCFaoQ= cloud.google.com/go/orgpolicy v1.12.0 h1:sab7cDiyfdthpAL0JkSpyw1C3mNqkXToVOhalm79PJQ= cloud.google.com/go/osconfig v1.12.4 h1:OrRCIYEAbrbXdhm13/JINn9pQchvTTIzgmOCA7uJw8I= cloud.google.com/go/oslogin v1.13.0 h1:gbA/G4p+youIR4O/Rk6DU181QlBlpwPS16kvJwqEz8o= cloud.google.com/go/phishingprotection v0.8.4 h1:sPLUQkHq6b4AL0czSJZ0jd6vL55GSTHz2B3Md+TCZI0= cloud.google.com/go/policytroubleshooter v1.10.2 h1:sq+ScLP83d7GJy9+wpwYJVnY+q6xNTXwOdRIuYjvHT4= cloud.google.com/go/privatecatalog v0.9.4 h1:Vo10IpWKbNvc/z/QZPVXgCiwfjpWoZ/wbgful4Uh/4E= cloud.google.com/go/pubsub v1.34.0 h1:ZtPbfwfi5rLaPeSvDC29fFoE20/tQvGrUS6kVJZJvkU= cloud.google.com/go/pubsublite v1.8.1 h1:pX+idpWMIH30/K7c0epN6V703xpIcMXWRjKJsz0tYGY= cloud.google.com/go/recaptchaenterprise/v2 v2.9.0 h1:Zrd4LvT9PaW91X/Z13H0i5RKEv9suCLuk8zp+bfOpN4= cloud.google.com/go/recommendationengine v0.8.4 h1:JRiwe4hvu3auuh2hujiTc2qNgPPfVp+Q8KOpsXlEzKQ= cloud.google.com/go/recommender v1.12.0 h1:tC+ljmCCbuZ/ybt43odTFlay91n/HLIhflvaOeb0Dh4= cloud.google.com/go/redis v1.14.1 h1:J9cEHxG9YLmA9o4jTSvWt/RuVEn6MTrPlYSCRHujxDQ= cloud.google.com/go/resourcemanager v1.9.4 h1:JwZ7Ggle54XQ/FVYSBrMLOQIKoIT/uer8mmNvNLK51k= cloud.google.com/go/resourcesettings v1.6.4 h1:yTIL2CsZswmMfFyx2Ic77oLVzfBFoWBYgpkgiSPnC4Y= cloud.google.com/go/retail v1.14.4 h1:geqdX1FNqqL2p0ADXjPpw8lq986iv5GrVcieTYafuJQ= cloud.google.com/go/run v1.3.3 h1:qdfZteAm+vgzN1iXzILo3nJFQbzziudkJrvd9wCf3FQ= cloud.google.com/go/scheduler v1.10.5 h1:eMEettHlFhG5pXsoHouIM5nRT+k+zU4+GUvRtnxhuVI= cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= cloud.google.com/go/security v1.15.4 h1:sdnh4Islb1ljaNhpIXlIPgb3eYj70QWgPVDKOUYvzJc= cloud.google.com/go/securitycenter v1.24.3 h1:crdn2Z2rFIy8WffmmhdlX3CwZJusqCiShtnrGFRwpeE= cloud.google.com/go/servicedirectory v1.11.3 h1:5niCMfkw+jifmFtbBrtRedbXkJm3fubSR/KHbxSJZVM= cloud.google.com/go/shell v1.7.4 h1:nurhlJcSVFZneoRZgkBEHumTYf/kFJptCK2eBUq/88M= cloud.google.com/go/spanner v1.55.0 h1:YF/A/k73EMYCjp8wcJTpkE+TcrWutHRlsCtlRSfWS64= cloud.google.com/go/speech v1.21.0 h1:qkxNao58oF8ghAHE1Eghen7XepawYEN5zuZXYWaUTA4= cloud.google.com/go/storagetransfer v1.10.3 h1:YM1dnj5gLjfL6aDldO2s4GeU8JoAvH1xyIwXre63KmI= cloud.google.com/go/talent v1.6.5 h1:LnRJhhYkODDBoTwf6BeYkiJHFw9k+1mAFNyArwZUZAs= cloud.google.com/go/texttospeech v1.7.4 h1:ahrzTgr7uAbvebuhkBAAVU6kRwVD0HWsmDsvMhtad5Q= cloud.google.com/go/tpu v1.6.4 h1:XIEH5c0WeYGaVy9H+UueiTaf3NI6XNdB4/v6TFQJxtE= cloud.google.com/go/trace v1.10.4 h1:2qOAuAzNezwW3QN+t41BtkDJOG42HywL73q8x/f6fnM= cloud.google.com/go/translate v1.10.0 h1:tncNaKmlZnayMMRX/mMM2d5AJftecznnxVBD4w070NI= cloud.google.com/go/video v1.20.3 h1:Xrpbm2S9UFQ1pZEeJt9Vqm5t2T/z9y/M3rNXhFoo8Is= cloud.google.com/go/videointelligence v1.11.4 h1:YS4j7lY0zxYyneTFXjBJUj2r4CFe/UoIi/PJG0Zt/Rg= cloud.google.com/go/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= cloud.google.com/go/vmmigration v1.7.4 h1:qPNdab4aGgtaRX+51jCOtJxlJp6P26qua4o1xxUDjpc= cloud.google.com/go/vmwareengine v1.0.3 h1:WY526PqM6QNmFHSqe2sRfK6gRpzWjmL98UFkql2+JDM= cloud.google.com/go/vpcaccess v1.7.4 h1:zbs3V+9ux45KYq8lxxn/wgXole6SlBHHKKyZhNJoS+8= cloud.google.com/go/webrisk v1.9.4 h1:iceR3k0BCRZgf2D/NiKviVMFfuNC9LmeNLtxUFRB/wI= cloud.google.com/go/websecurityscanner v1.6.4 h1:5Gp7h5j7jywxLUp6NTpjNPkgZb3ngl0tUSw6ICWvtJQ= cloud.google.com/go/workflows v1.12.3 h1:qocsqETmLAl34mSa01hKZjcqAvt699gaoFbooGGMvaM= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o= github.com/aws/aws-sdk-go-v2/config v1.27.24 h1:NM9XicZ5o1CBU/MZaHwFtimRpWx9ohAUAqkG6AqSqPo= github.com/aws/aws-sdk-go-v2/credentials v1.17.24 h1:YclAsrnb1/GTQNt2nzv+756Iw4mF8AOzcDfweWwwm/M= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 h1:Aznqksmd6Rfv2HQN9cpqIV/lQRMaIpJkLLaJ1ZI76no= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 h1:5SAoZ4jYpGH4721ZNoS1znQrhOfZinOhc4XuTXx/nVc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 h1:WIijqeaAO7TYFLbhsZmi2rgLEAtWOC1LhxCAVTJlSKw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 h1:I9zMeF107l0rJrpnHpjEiiTSCKYAIw8mALiXcPsGBiA= github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 h1:p1GahKIjyMDZtiKoIn0/jAj/TkMzfzndDv5+zi2Mhgc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 h1:ORnrOK0C4WmYV/uYt3koHEWBLYsRDwk2Np+eEoyV4Z0= github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 h1:+woJ607dllHJQtsnJLi52ycuqHMwlW+Wqm2Ppsfp4nQ= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/opencontainers/runc v1.2.5 h1:8KAkq3Wrem8bApgOHyhRI/8IeLXIfmZ6Qaw6DNSLnA4= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= sigs.k8s.io/kustomize/cmd/config v0.19.0 h1:D3uASwjHWHmNiEHu3pPJBJMBIsb+auFvHrHql3HAarU= kubernetes-kubernetes-40e1192/staging/000077500000000000000000000000001504711711200177305ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/OWNERS000066400000000000000000000006051504711711200206710ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners options: # make root approval non-recursive no_parent_owners: true approvers: - dchen1107 - dims - liggitt - smarterclayton - thockin - wojtek-t reviewers: - caesarxuchao - dchen1107 - deads2k - dims - liggitt - mikedanese - smarterclayton - sttts - thockin - wojtek-t emeritus_approvers: - lavalamp kubernetes-kubernetes-40e1192/staging/README.md000066400000000000000000000134131504711711200212110ustar00rootroot00000000000000# External Repository Staging Area This directory is the staging area for packages that have been split to their own repository. The content here will be periodically published to respective top-level k8s.io repositories. Repositories currently staged here: - [`k8s.io/api`](https://github.com/kubernetes/api) - [`k8s.io/apiextensions-apiserver`](https://github.com/kubernetes/apiextensions-apiserver) - [`k8s.io/apimachinery`](https://github.com/kubernetes/apimachinery) - [`k8s.io/apiserver`](https://github.com/kubernetes/apiserver) - [`k8s.io/cli-runtime`](https://github.com/kubernetes/cli-runtime) - [`k8s.io/client-go`](https://github.com/kubernetes/client-go) - [`k8s.io/cloud-provider`](https://github.com/kubernetes/cloud-provider) - [`k8s.io/cluster-bootstrap`](https://github.com/kubernetes/cluster-bootstrap) - [`k8s.io/code-generator`](https://github.com/kubernetes/code-generator) - [`k8s.io/component-base`](https://github.com/kubernetes/component-base) - [`k8s.io/component-helpers`](https://github.com/kubernetes/component-helpers) - [`k8s.io/controller-manager`](https://github.com/kubernetes/controller-manager) - [`k8s.io/cri-api`](https://github.com/kubernetes/cri-api) - [`k8s.io/cri-client`](https://github.com/kubernetes/cri-client) - [`k8s.io/csi-translation-lib`](https://github.com/kubernetes/csi-translation-lib) - [`k8s.io/dynamic-resource-allocation`](https://github.com/kubernetes/dynamic-resource-allocation) - [`k8s.io/endpointslice`](https://github.com/kubernetes/endpointslice) - [`k8s.io/externaljwt`](https://github.com/kubernetes/externaljwt) - [`k8s.io/kms`](https://github.com/kubernetes/kms) - [`k8s.io/kube-aggregator`](https://github.com/kubernetes/kube-aggregator) - [`k8s.io/kube-controller-manager`](https://github.com/kubernetes/kube-controller-manager) - [`k8s.io/kube-proxy`](https://github.com/kubernetes/kube-proxy) - [`k8s.io/kube-scheduler`](https://github.com/kubernetes/kube-scheduler) - [`k8s.io/kubectl`](https://github.com/kubernetes/kubectl) - [`k8s.io/kubelet`](https://github.com/kubernetes/kubelet) - [`k8s.io/metrics`](https://github.com/kubernetes/metrics) - [`k8s.io/mount-utils`](https://github.com/kubernetes/mount-utils) - [`k8s.io/pod-security-admission`](https://github.com/kubernetes/pod-security-admission) - [`k8s.io/sample-apiserver`](https://github.com/kubernetes/sample-apiserver) - [`k8s.io/sample-cli-plugin`](https://github.com/kubernetes/sample-cli-plugin) - [`k8s.io/sample-controller`](https://github.com/kubernetes/sample-controller) The code in the staging/ directory is authoritative, i.e. the only copy of the code. You can directly modify such code. ## Using staged repositories from Kubernetes code Kubernetes code uses the repositories in this directory via a Go workspace and module `replace` statements. For example, when Kubernetes code imports a package from the `k8s.io/client-go` repository, that import is resolved to `staging/src/k8s.io/client-go` relative to the project root: ```go // pkg/example/some_code.go package example import ( "k8s.io/client-go/dynamic" // resolves to staging/src/k8s.io/client-go/dynamic ) ``` ## Creating a new repository in staging ### Adding the staging repository in `kubernetes/kubernetes`: 1. Send an email to the SIG Architecture [mailing list](https://groups.google.com/forum/#!forum/kubernetes-sig-architecture) and the mailing list of the SIG which would own the repo requesting approval for creating the staging repository. 2. Once approval has been granted, create the new staging repository. 3. Update [`import-restrictions.yaml`](/staging/publishing/import-restrictions.yaml) to add the list of other staging repos that this new repo can import. 4. Add all mandatory template files to the staging repo as mentioned in https://github.com/kubernetes/kubernetes-template-project. 5. Make sure that the `.github/PULL_REQUEST_TEMPLATE.md` and `CONTRIBUTING.md` files mention that PRs are not directly accepted to the repo. 6. Ensure that `docs.go` file is added. Refer to [#kubernetes/kubernetes#91354](https://github.com/kubernetes/kubernetes/blob/release-1.24/staging/src/k8s.io/client-go/doc.go) for reference. 7. NOTE: Do not edit go.mod or go.sum in the new repo (staging/src/k8s.io//) manually. Run the following instead: ``` ./hack/update-vendor.sh ``` ### Creating the published repository 1. Create an [issue](https://github.com/kubernetes/org/issues/new?assignees=&labels=area%2Fgithub-repo&projects=&template=repo-create.yml&title=REQUEST%3A+%3CCreate+or+Migrate%3E+%3Cgithub+repo%3E) in the `kubernetes/org` repo to request creation of the respective published repository in the Kubernetes org. The published repository **must** have an initial empty commit. It also needs specific access rules and branch settings. See [#kubernetes/org#58](https://github.com/kubernetes/org/issues/58) for an example. 2. Setup branch protection and enable access to the `stage-bots` team by adding the repo in [`prow/config.yaml`](https://github.com/kubernetes/test-infra/blob/master/config/prow/config.yaml). See [#kubernetes/test-infra#9292](https://github.com/kubernetes/test-infra/pull/9292) for an example. 3. Once the repository has been created in the Kubernetes org, update the publishing-bot to publish the staging repository by updating: - [`rules.yaml`](/staging/publishing/rules.yaml): Make sure that the list of dependencies reflects the staging repos in the `Godeps.json` file. - [`repos.sh`](https://github.com/kubernetes/publishing-bot/blob/master/hack/repos.sh): Add the staging repo in the list of repos to be published. 4. Add the staging and published repositories as a subproject for the SIG that owns the repos in [`sigs.yaml`](https://github.com/kubernetes/community/blob/master/sigs.yaml). 5. Add the repo to the list of staging repos in this `README.md` file. kubernetes-kubernetes-40e1192/staging/src/000077500000000000000000000000001504711711200205175ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/000077500000000000000000000000001504711711200216325ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/000077500000000000000000000000001504711711200232635ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/.github/000077500000000000000000000000001504711711200246235ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/000077500000000000000000000000001504711711200270065ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000016371504711711200314250ustar00rootroot00000000000000--- name: Bug Report about: Report a bug encountered while using kubectl labels: kind/bug --- **What happened**: **What you expected to happen**: **How to reproduce it (as minimally and precisely as possible)**: **Anything else we need to know?**: **Environment**: - Kubernetes client and server versions (use `kubectl version`): - Cloud provider or hardware configuration: - OS (e.g: `cat /etc/os-release`): kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002161504711711200307750ustar00rootroot00000000000000contact_links: - name: Support Request url: https://discuss.kubernetes.io about: Support request or question relating to Kubernetes kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/enhancement.md000066400000000000000000000003531504711711200316160ustar00rootroot00000000000000--- name: Enhancement Request about: Suggest an enhancement to kubectl labels: kind/feature --- **What would you like to be added**: **Why is this needed**: kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002251504711711200304230ustar00rootroot00000000000000Sorry, we do not accept changes directly against this repository. Please see CONTRIBUTING.md for information on where and how to contribute instead. kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/CONTRIBUTING.md000066400000000000000000000011301504711711200255070ustar00rootroot00000000000000# Contributing guidelines ## How to become a contributor and submit your own code This repository does not directly accept contributions. Code in this repository is currently being copied from staging within the [Kubernetes repository](https://github.com/kubernetes/kubernetes/tree/master/staging). In order to contribute code to this repository, the coder must modify kubectl code in [staging](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/kubectl). See [this doc](https://github.com/kubernetes/community/blob/master/sig-cli/CONTRIBUTING.md) for how to contribute code. kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/LICENSE000066400000000000000000000261351504711711200242770ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/OWNERS000066400000000000000000000002341504711711200242220ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-cli-maintainers reviewers: - sig-cli-reviewers labels: - area/kubectl - sig/cli kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/README.md000066400000000000000000000031561504711711200245470ustar00rootroot00000000000000# Kubectl ![kubectl logo](./images/kubectl-logo-medium.png) [![Build Status](https://travis-ci.org/kubernetes/kubectl.svg?branch=master)](https://travis-ci.org/kubernetes/kubectl) [![GoDoc](https://godoc.org/k8s.io/kubectl?status.svg)](https://godoc.org/k8s.io/kubectl) The `k8s.io/kubectl` repo is used to track issues for the kubectl cli distributed with `k8s.io/kubernetes`. It also contains packages intended for use by client programs. E.g. these packages are vendored into `k8s.io/kubernetes` for use in the [kubectl](https://github.com/kubernetes/kubernetes/tree/master/cmd/kubectl) cli client. That client will eventually move here too. ## Contribution Requirements - Full unit-test coverage. - Go tools compliant (`go get`, `go test`, etc.). It needs to be vendorable somewhere else. - No dependence on `k8s.io/kubernetes`. Dependence on other repositories is fine. - Code must be usefully [commented](https://go.dev/doc/effective_go#commentary). Not only for developers on the project, but also for external users of these packages. - When reviewing PRs, you are encouraged to use Golang's [code review comments](https://github.com/golang/go/wiki/CodeReviewComments) page. - Packages in this repository should aspire to implement sensible, small interfaces and import a limited set of dependencies. ## Community, discussion, contribution, and support See [this document](https://github.com/kubernetes/community/tree/master/sig-cli) for how to reach the maintainers of this project. ### Code of conduct Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/SECURITY_CONTACTS000066400000000000000000000010551504711711200257540ustar00rootroot00000000000000# Defined below are the security contacts for this repo. # # They are the contact point for the Product Security Committee to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ eddiezane KnVerey natasha41575 soltysh kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/code-of-conduct.md000066400000000000000000000002241504711711200265540ustar00rootroot00000000000000# Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/doc.go000066400000000000000000000011131504711711200243530ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kubectl kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/docs/000077500000000000000000000000001504711711200242135ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/docs/maintainers/000077500000000000000000000000001504711711200265255ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/docs/maintainers/MAINTAINERS.md000066400000000000000000000054451504711711200306310ustar00rootroot00000000000000# SIG cli maintainers Guide ## Sustaining engineering tasks The following tasks need to be performed consistently as a part of maintaining the health of SIG cli. We will be developing an oncall rotation for working on these tasks, where the oncall is responsible to doing each task daily. ### Issue triage Routinely monitor the newly filed issues and triage them to make sure we identify regressions. [Kubectl repo](https://github.com/kubernetes/kubectl/issues) [Kubernetes repo](https://github.com/kubernetes/kubernetes/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Asig%2Fcli) Look for: - Requests for help - Don't spend a lot of time on these, but answer and close them if it is easy - Regressions and bugs - Find the root cause - Triage the severity - Issues only occurring in old versions but not in new versions are less severe - Simple issues for new contributors - Label these with "for-new-contributors" - Give them a priority - Make sure they are - Small - Well scoped - In areas of code with minimal technical debt - In areas of code with strong ownership already - Feature requests - Do one of - Close them with an explanation along the lines of "Don't have capacity right now, try reopening in 6 months" - Label them with a "priority" ### Test triage Monitor [test grid](https://testgrid.k8s.io/sig-cli-master) and make sure the tests are passing. If any tests are failing, debug them and send a fix. Ask for help if you get stuck. ### PR review Make sure PRs aren't getting stuck without attention. If reviewers routinely don't respond to PRs within a few days, we should take those reviewers out of the list. Look through the PR list with [SIG cli](https://github.com/kubernetes/kubernetes/pulls?utf8=%E2%9C%93&q=is%3Apr%20is%3Aopen%20label%3Asig%2Fcli) ## New contributor assistance - Look through issues labeled "for-new-contributors" that are assigned, and make sure they are active. If they haven't had activity in a couple days, ping the assignee and ask if help is needed. - Identify issues for new contributors to pick up - Figure out a progression for new contributors to become reviewers ## Per-release tasks ### At the start of the dev cycle - Write planned features for each release - Use the [template](../template.md) ### During code-freeze - Daily look at issues labeled with [sig/cli in the milestone](https://github.com/kubernetes/kubernetes/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Asig%2Fcli%20milestone%3Av1.9%20) and make sure they are owned and make progress - **Note:** You will need to update the milestone in the link to the current milestone ## Every 3-6 months tasks ### (3 months) Report about SIG cli at the community meeting TODO: fill this in ### (6 months) Setup a SIG cli face-to-face TODO: fill this in kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/docs/maintainers/issue_backlog.md000066400000000000000000000176551504711711200316770ustar00rootroot00000000000000# SIG CLI Issue Backlog Grooming work for new and existing contributors - Link: https://goo.gl/YEq33R - Author: @pwittrock - Last updated: 10/25/17 ## Background A goal of SIG CLI is to keep the SIG open to contributions from anyone willing to do the work to get involved. While many folks have shown interest in contributing, we have struggled to keep most folks engaged. Kubernetes recently conducted a survey of new contributors, and major themes were that: - Contributors don’t know where to start - There are too many details to learn before contributing - Communication is hard / everyone is too busy to help - Hard to get reviewers on PRs These challenges can be reduced by: - Providing a backlog for contributors to browse and pull work from. - Scoping work that can be done with minimal experience so folks can pick up and work on it. - Marking issues as good first time issues if they can be done by someone with no experience. - Reducing the need for constant communication by having the work be well defined and clearly scoped in the issues themselves. - Ensuring each issue has a stake holder that is committed to seeing that changes are reviewed. [New Contributors Project]: https://github.com/kubernetes/kubectl/projects/3 ## Contribution lifecycle 1. A [good issue](#what-makes-a-good-issue) is created with description and labels. 1. SIG agrees that work for issue will be accepted. 1. Issue moved to the _backlog_ column in the [New Contributors Project]. 1. Contributor assigns issue to self, or asks issue be assigned if they are not a Kubernetes org member. 1. Issue moved to the _assigned_ column in the [New Contributors Project]. 1. Contributor updates issue weekly with status updates, and pushes work to fork - Periodic feedback provided - Discussion between contributor and stakeholder occurs on issue 1. Contributor sends PR for review - Stakeholder ensures the appropriate reviewers exist - Discussion and updates occur on the PR 1. PR accepted and merged ## What makes a good issue? ### Stakeholder and Contributor A stakeholder typically files the issue, wants to see the work done, and will find reviewers for PRs that address the issue. The contributor is the issue assignee - they provide PRs for review to close the issue. The stakeholder may become the contributor. They must find a new stakeholder to review the work and help follow through on issue closure. ### Encapsulated Issues that require modifying large pieces of existing code are typically hard to accept without multiple reviewers, require a high degree of communication and require knowledge of the existing codebase. This makes them bad candidates for contributors looking to get started independently. Issues with good encapsulation have the following properties: - Minimal wiring or changes to existing code - Can disable / enable with a flag - Easy to review the contribution on its own without needing to examine other parts of the system - Low chance of needing to rebase or conflicting with changes made in parallel ### Consensus on work within the SIG Work described in issues in the backlog should be agreed upon by the SIG. PRs sent for review should have the code reviewed, not the _reason_ for doing the PR. SIG CLI needs to come up with low overhead a process for accepting proposed work. 1. Create an issue for the work 2. SIG agrees to accept work for the issue (as described) if it is completed 3. Add issue to the issue backlog ## Types of code contributions ### Code documentation Documenting code is an excellent starter task. It is easier to merge and get consensus on than writing tests or features. It also provides a structured approach to learning about the system and its components. For the packages that need it most, understanding the code base well enough to document it may be an involved task. - Adding doc.go files to packages that are missing them - Updating doc.go files that are empty placeholders - Adding examples of how to use types and functions to packages - Documenting functions with their purpose and usage ### Test coverage Improving test coverage and augmenting e2e tests with integration tests is also a good candidate for 1st and 2nd time contributors. Writing tests for libraries requires understanding how they behave and are meant to be used. Improving code coverage allows the project to move more quickly by reducing regressions issues that the SIG must field, and by providing a safety net for code reviewers ensuring changes don’t break existing functionality. [e2e tests]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-testing/e2e-tests.md - Write unit tests for functionality currently only covered by integration and [e2e tests]. > Integration tests may run processes, such as the apiserver, but do so locally. > E2e tests run a full Kubernetes cluster (remote). - Write integration tests for functionality currently only covered by [e2e tests]. - Improve coverage for edge cases and different inputs to functions. - Improve handling of invalid arguments. - Refactoring existing tests to pull out common code into reusable functions. > _This should be very well scoped as it impacts existing tests and reviewers need to make sure nothing breaks._ ### New libraries Encapsulated libraries (collections of functions devoted to one simple purpose - e.g. date/time utils) are great contributions for experienced contributors - either programming in Go, or with Kubernetes. Because the libraries are encapsulated, it is easier for reviewers to determine the correctness of their interactions with the existing system. If the functionality is new or can be disabled with a flag, the risk of accepting the change is reduced, improving the chance the change will be accepted. ### Modifying existing libraries Tasks to perform non-trivial changes to existing libraries should be reserved only for folks who have made multiple successful contributions of code - either tests or libraries. PRs to modify existing libraries typically have multiple reviewers, and can have subtle side effects that need to be carefully checked for. Improvements in documentation and testing (above) reduces the burden to modify existing code. ## Managing a backlog issue ### Setting Labels For contributors to pick up new tasks independently, the scope and complexity of the task must be well documented on the issue. We use labels to define the most important metadata about the issues in the backlog. #### Size - size/S > 4-10 hours - size/M > 10-20 hours - size/L > 20+ hours #### Type (docs / tests / feature) - type/code-cleanup > Usually some refactoring or small rewrites of code. - type/code-documentation > Write doc.go with package overview and examples or document types and functions. - type/code-feature > Usually a new go package / library for some functionality. Should be encapsulated. - type/code-test-coverage > Audit tests for a package. Run coverage tools and also manually look at what functions are missing unit or > integration tests. Write tests for these functions. ### Description - Clear description of what the outcome is - Pointers to examples if they exist - Clear stakeholder who will be responsive to the issue and is committed to getting the functionality added ## Assigning an issue once work has started 1. Contributor messages stakeholder on the issue, and maybe on slack 1. Stakeholder moves issue from backlog to assigned 1. Contributor updates issue weekly and publishes work in progress to a fork > The issue should be updated with a link to the fork. 1. Once work is ready for review, contributor files a PR and notifies the stakeholder ## What is expected as part of contributing a library? ### Documentation - doc.go - Comments on all functions and types ### Tests - Unit tests for functions and types - Integration tests with a local control plane (forthcoming) - Maybe e2e tests (only a couple) ### Ownership of addressing issues - Fix bugs discovered in the contribution after it has been accepted kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/docs/roadmap/000077500000000000000000000000001504711711200256365ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/docs/roadmap/template.md000066400000000000000000000004741504711711200300000ustar00rootroot00000000000000Use this template for writing roadmaps for releases # Release 1.X roadmap ## Planned Features ### Feature Y Short description Owners - Link to design proposal - Link to issue ### Feature Z Short description - Link to design proposal - Link to issue ## Planned Technical Debt Cleanup ## Planned Bug Fixes kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/go.mod000066400000000000000000000100551504711711200243720ustar00rootroot00000000000000// This is a generated file. Do not edit directly. module k8s.io/kubectl go 1.24.0 godebug default=go1.24 require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/chai2010/gettext-go v1.0.2 github.com/distribution/reference v0.6.0 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f github.com/fatih/camelcase v1.0.0 github.com/go-openapi/jsonreference v0.20.2 github.com/google/gnostic-models v0.6.9 github.com/google/go-cmp v0.7.0 github.com/jonboulle/clockwork v0.4.0 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de github.com/lithammer/dedent v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/moby/term v0.5.0 github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.35.1 github.com/pkg/errors v0.9.1 github.com/russross/blackfriday/v2 v2.1.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.10.0 golang.org/x/sys v0.31.0 gopkg.in/evanphx/json-patch.v4 v4.12.0 k8s.io/api v0.0.0 k8s.io/apimachinery v0.0.0 k8s.io/cli-runtime v0.0.0 k8s.io/client-go v0.0.0 k8s.io/component-base v0.0.0 k8s.io/component-helpers v0.0.0 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff k8s.io/metrics v0.0.0 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 sigs.k8s.io/kustomize/kyaml v0.19.0 sigs.k8s.io/structured-merge-diff/v4 v4.6.0 sigs.k8s.io/yaml v1.4.0 ) require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.26.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/kustomize/api v0.19.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect ) replace ( k8s.io/api => ../api k8s.io/apimachinery => ../apimachinery k8s.io/cli-runtime => ../cli-runtime k8s.io/client-go => ../client-go k8s.io/code-generator => ../code-generator k8s.io/component-base => ../component-base k8s.io/component-helpers => ../component-helpers k8s.io/metrics => ../metrics ) kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/go.sum000066400000000000000000000563701504711711200244310ustar00rootroot00000000000000cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 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/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.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/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/cpuguy83/go-md2man/v2 v2.0.4/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= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= 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/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.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= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/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/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 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/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= 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.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= sigs.k8s.io/kustomize/cmd/config v0.19.0/go.mod h1:29Vvdl26PidPLUDi7nfjYa/I0wHBkwCZp15Nlcc4y98= sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 h1:MWtRRDWCwQEeW2rnJTqJMuV6Agy56P53SkbVoJpN7wA= sigs.k8s.io/kustomize/kustomize/v5 v5.6.0/go.mod h1:XuuZiQF7WdcvZzEYyNww9A0p3LazCKeJmCjeycN8e1I= sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/images/000077500000000000000000000000001504711711200245305ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/images/kubectl-logo-full.png000066400000000000000000050572671504711711200306130ustar00rootroot00000000000000PNG  IHDR < mk pHYs.#.#x?v IDATx]l݇}ǶDڔIiCT#Ab#Ĵz(vXwpwݛ-L۫*:v`f)Kmt PSr mR)E—s?ףE!#q*/ɖz31r_ʲ<'V#MEQlI > oYx]51H+,F'xeREW~|֗dy'E,G@<(sVAON, &x.%2-_>W`y]M5X4rʲb2F@M++Io?[c~ׯo@[q)yNP x:p%Wʲ1 k($w{4p/>BDY`-p _ix:I&CO,TŁ, ]K^vXpb"K1PKK ]/}I6[#j`^Q]Y vgIx,'L,7Eq k }/[sfL<Zra!nx*@ 9Kq Oג I>)bKAÁԷ;Ae9au??wLEQtebÁכ-4㙿QZʄd1|I2cnO'xnSBذUn9/A GKo%پJtJ똸`Y,#eY7I]^p-` qAԗ&鯁^P\}<@#7ԤIg1_[5>9F\Q]Y ̿lp" e9aUTZ5/ E% "nh$W3?d1b+I~^{jXQKㆾ$Z`9%DYMlϓq\{ Ԉ(r'8ۯ@L|+ɗ1䏒htX#EQIɟ ɑa `pI?v'xPҸ@\ojHkǶut~us_{Ξ4oj=.9uߟsylfMLMV2=yPK@O2 D[ug ɿJ2hDxHEQlbp 7}3w>)<ؾk]޴5[{ Z.'D΍5[h1{m*ϽiPAW ?{#IT/=4ܧ(r{U`ZBִutE4oj5 +2:|gMҹ΀U  gmI7ȟR >CQ}=pah,/(,+>hTצsyML[Ԓ@rfhxb| IWÁ%/a鵅;Å.~hVܝą3nze W%\M5~I58^?HAM2Gh$ֽ(d1njҀ}gOlMlٓMmՏ/@XI,&1t.[|so jˉ,^8^gIyTC!x`ݹ#px"F $^cغ~p&+~zҹ[W#*I)Ro5?'pѾ4ojKִw$Yд5[.|q̩$LMVʙa#9"J=%`gp4,]d2jj쵩\:w6IrylfMW7HVQ` < h8B̰*]{$=ij$V/B\:7kӷE"bq|÷|w< G@)/!'Upe1ʚ6 ΜJ$?i#'[sI uh8jÁ$ISKk· ֝iT};]u%^D/;bdO_w$?45(8 U=uJwu5C,'$_M}^z$3~DF"x`E%q@#љ=i~5[wyS[:w12:$pTfߟsg3{m*Ͻi֓= _WR:th4DQ8qhW3?@L綤=.qO/Ј,(d1rx"ԣlI[Ƕ>ݙ;{љ=`]4~6sצs̩̾?V63mT,R$G<@<W6[zqk =ij W!&+2:J'/zs4=~ZϞ6 /WWy;I[Ggڻ{y+rVޥ񳙞ҹ4 |TeY^Ye`]<8,,BZ6lٓ֎l1 !uD,GW*~ aS~IQ @q !S0X=|rza(ɌX/,\q8KZkؖtޓ[֟!Det8SLO^4 hؖ$_'qͿ`(bK/8 @kǶl36T_utX6SL{1Μ񳙚7 Z&?VEWq@,jr%w^6 t`Y#lSŁ,F;,Jkji5@4~6ӓ\:wD̴aX 3,AsܝE%ոVa, dΞw0@Ød݋pT.ͥsg3=y0^8^9u(,^qx"֎m'͛Z ;צsyr:ɑTsNEїpIpD^~,9FPÊXp ZZ'v5ChtRŖ,I6[4nXiU4Lp4@C<(8sמ[-Sj1:Φrf(9PU'2P'/qH5~,96RE_$-Rw>tڛ= SJet8o3Hcd0+eY֖gEQlI2$Z5 stOVSQR kԷ֎mן/O=X!flh03rܛG)rz]Eѕjp8ԯϤ fkwAVd%'R R߮&9W` Eq8@C֨_{ySAjT>y,s3F_'R˲bX~`K^slҚ/_'rat8o ezA$^}1,SEQlI$YvlKdkwAܥL̩\>AT>q֙(R0Ng?]{E ljC?ԯ>7<K9Yԗ]{ҽ` 錟q |W740Wjн?͛Z @ Ù6J}qk5w9p_⇷O?WAPZZaꖫ P\s?"VCet8oChAu($/5 r`-]?kU&/V R䕲,GLz&x:R$&xTŁTCC֨ ;I/{7>Ł-iȊ|kӷ7yjljTÇASVp8$;Z; i4ܣ,Wklޝ?5Yɡ 7=Y|W)9XMEQt%9EY[M-y )_ܲqY湩ܲ}Kg36472=yѓ&9Շ spEQ9|k{{1`|9۟ܰ,_7˳+XOeP>y,s3Ӟw4) IDATXIX(TC笱w>%͛Z +`9.=uz޺2*wtOelh03Þw:)X ֽ($HrY;ҽ?}^H[GA`7JQŁ$/'yk5+fj fzATÇ#~ hxEQN5taѽ`zҽgl{;vk|W)9RQ[4"c[!_IV ikz4Olxo8Ss͏35ws7 k?9^sodm\b0a>REW"tXuM-y t>!{725w3fޝaP IrilƆ364i$yY'<K7w>_L~eiYra];s#o]7(P564C1ƉTÇ`)u(^tx5;B:: ,>/7 +}M]/ 25YuazAV$ey$TQS vXcuuړXvmMv{~wr6o{Rv'\}X S,`<P7ؒd BUڱVRښ;l|tuI~ }/#}Շw5+^}>Cj|f}?VZKl.Ó|pƆcG&9j0aC@*+k:նGx$yY>9KBoXc/nil2s??7>^|8n %xfEq KIYcu'6tx9sX9䆚zLo&/̸<0WT/>1@<f8EgZZ?C/ԄZ`9LMV264AWVT/>1@<Cܵ'0PS6Wj6s0Iɡ fcX瓼HYWP($aU4 f׿P~cikzff  ,#Gi5:4"+}3{tOV5k㣏7?Rӏ'gs,`Ō flh03Xy:$x`EVWCJ:w1Pz/xnz~1Vܥ9ګy1WV$I^.rM:֎mH/ԝOnȗۛk1GyГk^F^n'/d}'&x`YEѕ$:]{ o n}qKSecM?F*yՌq+OP<%7rZZL[GA'x7S fw373m%|1auvl˾}3Ӽ @xeC~5cCy\>1Vw))֖"tXӹ{1exM?FP*^Ǝ5:ŇXa5 }^H[GAGoxYOP&+իGi<+c[}ߟM֍[Ҳᑚ}|?< Dualh0#4>2w%tXyӹ{1u䆚}|_7)P_*^Ǝ5>:|a7i`]|&{~7=I@ݚT>nf 2+L@Jkؖ_HoWҼ ~{\?< ^粹#_ү5LLO^48kIr8IW$Gz}<N VV={t7]|qKSecM=xrz'w|+_?-T*yՌq+Zryk>ze雿3뇭`aeHlo+'?;,|W3TԿv>y,s3Yfw [d'xXg+5}^Ho@:: p6Wjrnz~1I%>+vX;/_k:2{m:#LO^42#|/zU`:֎mo{7ZGsnٰajf(?߿j~_|hujlh0#42豖|}b #xhpBӹkO!6>H5b݇32foCet8#7cHrYMpo JrzʳϿ=XF}$9{Ubݻ'd/^6JNo273mTÇ S|:@:z!i4 });6:7u=u_ÿ}_^kflh0ӓ A ؒj[cvlK/+ijUtˆ>Ք Ï֕Ň\4ɿ;c?{ɿGC6>,?'<ԹY;I/kd㣏mc6fLޝ^Ùk~taA ~۠Det8oS93l%|N VF=oscԈ tajf߬7>7>{~,\Âİ̥9ګ;v_>\1 ae<_f::PÞHښK[ɇazIK37/K/MMV2r̴A?$$yEg:R$/'aҚC/@4&O37睱-|?^kfwp:&xBڱ-{4oj54&s{\fT_\K:>ߕ7{w-oGWߴid\;?iiO3Ċ~_: :T8s}{֯?*3sL޿je?eI^.SE1zavl˾}33vfsϬcini;$;ykGZ􎝷>cGWot_Ùk>cd%>ܵ'tc h0\[?>U"5}jBUI>z%ȣ7?ƹ_d#ɟEq8IF@ (++IYrz. ω^N k<ރL[G1ݘ￟֧2PӮ^_{=ini1B xgյݘǷeik6^ G>|p%y(I^*r$@#<tWd&b~|$@D*AV8ӮtݭH{g)l6Kop7tynTQ@vwÏ6$-($7 a2$  !d~|'|d2O^_Ƙ,I֓8]n.X%VPtFڪ ds=<&+(oFr Ahbh):T~LB:*:\?9E\8o0~%-7ZuD %DɠCLKEQj@a<]jQzNQaY-BRi;oJtjklf|yo/$MYN*9 PWM^@ħ:Kjn# p'(:~pv(9zP|vI Qx3ƬPI7Me+רd rA'iF5UiQ6I礆\tJ7Sz q{hזM'~;oCQzk:{)>M ˲G@ܠbYDŤ1v~5t1HW-JQPԬ))a{}mG-ףxI' 8B!:z4)Щ&z9)Ƨ^j˲vafD1vE0Ȼ='U3yV:;6짽Yo'C5ӛ?\ɤ^R[(>'>& vEdP!Ql̝O0bQtHN'f,;\r+# T_:#FŇ!ܲ:`7`1Y~ePtBsOuȟxb<;M6$g8t{NjTuG'VY!:aAmD.DcgY-Rƌ.iktOunbs %9I%٩Q;Y)׫V_/#f(;ã0:\u)ϧ=26k%6TJ(<cV(Xt("Bl`'Cd;cޞgzOLBS}e1KNSw4r[(>M *,D$b1f IIcl(:;~pNƶy_׍1-M$b3Sã16ѩ>;Ia]bvSR(< jkO‹CX̓1fŇj"M:1Y1^$B96lbYxLdG.DES}AC8;"CX,'c̯%UXUG$ c`TwL5G[eFgMA|6Ni iٵW Ѿ7)EŇxT cLJ˲ڈ@$QxcVHTDWtZ)yRUqV1JId6QqtGܽƾ7PKQBa2SaY&")`1Tbl̝O?_@Oܭ;g $`>GG~D1@a܊$>0-ܲD (<HxƘ*ōQG׹ N u ޏcm1{$,  %|W&E }e c>B@|B}}0.(>bI1$UXF$ƋdYT"@d!$>)%9v(8~h?!ChoiVVjcLeY`<(<H(Ƙ% Pt"'B\=+ (}eQMuz e {0.~f)ڲD`,(<HƘ*%-'ѡD^ָ];; &oo zu1& O,!>!\*>RݢIFHGƘ=-˪&AfɒT.i=iE z.XVܮ}j>ƫ ]6B/|3!đI.4KaK1 ڈ@((<1IsMӢ֩e($qיgDGw@@51룎ŇqyT cLeY`$L8Ƙ%*cBΛkTlaɌ9&~JI1&xDEC.g,Sό1V[H  3Sã:HQal~նvԨTGZ}@\.>:ݤB M1{,> Qx1Y~eHh.=1Q2G LW@i4=Զ 0uuiB!t%2lTaYV$3c IՒ֋CH.|R);6gŁ(TIc~&o@:j*b #dv0 IDATzIem' qPx`[Ƙ Ntx4BSp|\ C~ښ-۬'+hVZz`\v%x7ARa fEa(܅K4w=a"T_xI u~.hnǯU#ieYuDL|ؒ1BRLYά[豧):qU혯#-=]ŷE7~&6@X4D.\i# G{ { A˥qOs\?C/!=UmۨOѡ{VReYmDL\؊1fMHcdi*[F%V0ACcrl#E3J樰dN' .\;j-=PxfR$A\ǥÝߺ_i3>~Ƈ^ReY(<cLIȜ.V=! `>;iԎG@4vr#A !E>C5֌m)G?Ԙ&pPxfꙧ|aD`MuǴ7~ KooV֗vrbfڈ(<cI$J.ǟ$kJS}^橄.Ef,uSx」][6%tv*: 6 5(Pt`7tQw˥ooۢxCxmjnK73'Ww~~]tmٛp.hnǯU Ie(< cI$icT|2p߯ :٧jkشtMͿQ3 ;ξ7оޘw-w.Eպ}^^)o 9-\"N?ԨCx􀪶mT'xጬ]i/D1fJII.=N%Vt*㗯K4I g;~pv%u{Tr(,P{L[[?;u'T Tr(=G7߹ 2ݗeouDTkmTgY^8#ۣ`(@@c*$K$9]n.X>t7w/,’۔W4Sܹ`B[^v'*_뒇]5iז_&֝r)h Knw.P~ /[a{'6uUYU[_ɋgd$UXFQxvƘ%6I*"+Y\ Hx-:~h&>\x;h~Sjn 鱫~ Kp"GHC1x8tvkUfnO~ _~ioi֯y*?GmmݞNUY gd N{N}Qx6Ƙ,I n{˟הj9CalIR^,ҕW4Si }S}^橐/$!J Ҭ㇂h.~ۂ=G」][6w_u47j֍p/푴ڲ:0ƔKI˙u= ' 4שYuj?nG--j?2P[b\e%̜ۡ\=ŠcjoiV{Kj? (ti2C𵒮Ytcdo#O.\cƣTm?9Y˲* STy1<˭ESɲfȿKu 㑐|^^Yİ̜\~4Wzz|z=& (<cL֒.J?Qj@w|^z57]3srZS}>kJ%eެ/doŇ6bQ3ƬPpC&i dr-XFS ,;m#ĜѽNT8{ns.bYGVS)e橰dnsmT֍:-l[+²Jbcf*XtXL+m>N_& 0Ɏ$͜&i3yu3P}V_/!FQNV}HwXiDD!1THZOsMӢ֩eF4ّN~:գ ]ڏt_6(8(ຌ1KPDCs*]V! 0$ݔYSR"'zTѣ> ]{; cx-N@tPx0$cLJI=VQj08ռ4I=7Я?6#t@Ⱥ=~{wlp};,>Y\ZC&i yM+ d*%9z>gY"R<ZZJMw$-RJrײEut{:Uf2a ^ReYۉ @2dI4Vp|-|lrL`LnI -ӾlnǯUvHZʹ`(< B&Iq-w4-zlZF`̲&.f59߭3]60nGhk?S!K,($cLIƵ.J?Qj@4ݐ6VkMۛUe1= N{# `(< S.BLuRqR-||Ɏ$3e˵&¦өu[cxZUA Px&$-&k̺U{ZslfMItt|7ǯOK*,k7QL`Ƙ IIZN[eT郏 ӕlϵy}&"vvUmۨ泄1 *,j# (<1fJIHZxXe(5M &.f5~tƫ7 nOެ^&KZʹ(<1&KRqkc[essS7gz|jYhnW'ch;,>0`IRi\rkTla#/U.xG'l jQgY¸VD\:TJz45UjR݄̤醴I^#@Tmۨ[vՒ'R_ceDcƘ Nu$+6_?)yܨ_}Q'>$%%w;~tC . Lu$i9i\ɝ7M[⻖  j9ݠ MZ?W Уt59#Cifd=5[Nz3l>KWYrM?% $2 @a|R>t7a)vj֔[կ6 8٧j= cz<#3E7*+ȝMWmwlI%%w+9E0Sx1fEŤq5%0@#/kS :[[u~5[a}ތ*,t!ZGs>~E0=aYV5a >؟1\Rp˭ʟW]}JI&Ekkt#+hޒr8kG Ϩ,a\Y˲*`cLu޼V٪5JMwlfMIښjfӹFXGoɝMWmwlIW:,i5((<6T6_ [/Ɏ$3e˵9߭3]6 (8yZFZunMl u47W_ɪ ZL{@B Stu*Y0@\='U3Z7ЯglQp/v,^l`B8]ڋl>KWb&< 0ahxXe(5M nLv$i.$gMLw :~v}ot! ө7k ZL{E: -g֭Z*;0@\9˩RlȦ'Wh4kw! QT~_ +1 Ƙp-˭UOGĽMgl۬;hFl60!u{:Uuv a\i0բp;oz~); b_UEh©DQz[g'֢7)g֭yd ;&<QT-XJ|Цg8TI'ztODў[i$7j{; L{@\c%LuVmkS5w+I=}ґݔ2veId9P#Z?TqRטDStGL,[A Lv$Tݐ6)" կ @]h߳ݺz755߰Ar|0 @cVKIA%KkO3$ݔˑ4s_:z(:Cv-<̚[yl t{:Uuv a\Y˲*Ƙ,:,' w4ݷ̝OLp(5I.uSO_:} }V_/!`S!/QpCQ(<afY`ف|Re+8HbC`ǮotʝQ^ۛUe.²J]Qx„*m-^S $='=Zӂl:/dՇ1S16EcITIV٪'U#ݻti۬H1\vik/,aK*,kQN(<00աBZ*.[{_Pj0@9][f=7ߤ_`ݞNUmݨÿB_ءഇ6Pxh`&IE!icT|2 ҿ?f= Ãrgg1\Gڏt_#]v@QxS!i=I{aZTIw3c<;M6Umۨ^&/ZR9KQ0Ɣ*8aiH9nբǞV0U혯^Gs>?9@A N{M @1~FA V>k`?斪x^)UYU[_IA,*'D`Ƙ NuXLRmuoW@:wv!Ǿ&lܨ_}Q'>$ N{& D :1%UJL,.V=A,J{y=:N5~ej{4w=āإ+ϴ/|ײJb@4Px`Rpr`X~ښHQhFlBlh?WsC5WX2[}J'(=GiuDH\Dv1AN[?⻖qb:u:"}cM*W lzeK%, N4=7<泄!K*,kQ R(<:THZKRqR[R݄>O۾qO{Ț*w o;Ppj_tݲ%)@tjFڡഇ6D-WL|IƘRI$K,yӴuLuVsZ <#I?_cHqʝM@h~n7ǯ*,Cp@a,=& @$E<ŇK>`g$yO.vu]{j6b,Kݓl9Rj >zeI~ĩmrb Wax K%^ >Qx@2TpD7M}Assb !~5yΟ aHPM$}G): 墤](>q1feDς T_Zu |j3 >jkt1?OZznL3 j_憺Q܅Kt=!.kTaYV%QFp'%M±>WZ H(Ƙ,IM,BuRC1?s:gdMUalmՑ=F5a$9ghtpc~_t˝ @p=VXF!|M~I%~ >Px@0ƔJ.(ѳ`Ņ&KT˜>-=]PS a"}TddMstGj?2)Js(01vI-NAbUv(< !c*$Orfݪ{׾/sR:~pN&,5˳{9bt0Ǡ8Ut™?~B&F}P}5~r0 *T* (<`B3tqg`*[\Wס߽;bpĊӥdeoҬ;Rtd%PfUm}Y~ogGqXi՜n_%MzvIzm&,c S@"Uvb;2kn&D-!__X2'.߿c:~pN̝ٗ=Z׼P+|ײJbKfkzEe<(<`1dIhgTӥ5n$5[JKOWZ[lX|tx.?s:ș>C,#lDŹ>UͿ1=\iq_C`EMN^︟dߟN IDATR+}3>$[T_cj9憺s2ae{$,dk9> L(ƘRI%%rLuk~"t}LrA7~&[fs`N4@b+}ߵL;t_9Ƙ%V[U͙2I8L2 cVHS=Vnx Q. zT ޹'>%xDL3cX.z7';eHuIҔ|2ѣ'i1f5gL87H,ea1RD7M}AssB~Nst]й>]yx~s'?URg  1_C _^S{G^/'"W([FegyCnzڲ6 nuVÄcL1Z ^v4Ձ tq? SgkkBJח$x { 'r̴咪1i&x) vBqe`nG&$w4=&-z|Rݜps] DB,/> 6tbLP{Lz)ڲ$*[FM̺5c('cLgĽh.^Dx`ɒT)DΡdr-|i$޾>?|ׯ9x elj@d%OJR] ?lMm:&j %cVXN<\l`/`kƘrI?K ˖~p/> b@==1fL3idCIɜAG~n0QH))!>e!m҈Wj{Si[7o$j >2l, 1Px-c$mǿ\/+!/Ƙ es6}Gk=vB`RpD g֭?gWDnB(w%)!B+=+[sTwONNEOOǝ450}Po͝<GJAh9+EI:rM'1 UbMW G߮S~=YuL_aYVg0qIGk`#s߀ܮ]X҂OlN7a{65(#tj-蘟Õ9U3Dв eʨ^ҳFw1PwGz'V$OJRҠɓ4xh$ Mħ&k2YÁޡALp(%Y:L!.}?uM]z{(4D`C48]n}{[U/uGol,0J)I*NzfMIQW^6gw0ӴOoRo2QcxTR1feY՜`%q! lW.+ r믿sِ̝%&(I ` `s8}}j='<3sU<H(o=GXs0(j:uRMإ&1T9⒴t59#CS;oM:yOynoWotiv#)GddMUa*s}c:)*{`yXK& ǴH&uτaIg˖k;r[g.~]0x:ԯknF{;75crٻ;Ϗ`0 (B0XK, -:k~];UaSU:5s;2÷-VEs(JAD!KIΏ$1@N>{?kQ$?М}kc(y*f=@0x0ƌQa Q\25Zsjw+rW>0Yg:m?,KB² AIOeMO=?SsE1fzLw/7wRJxp\L+ '>^ Ym" ֱ.tO%BRdߍ_kF@ќm`7C1_3(tݣ[7ct#өWQ=Byҫmx„ iG̨+/ N%̀A?SmP=<8v$-qlvFEaT.֗5vl|\דNy6ƓĄڿ{ys>P3v&Ϙ1CG/q Ga(8MKEP$7Iy҄􂄦{c9>v~\p/y/{ڸzvj %8^vLC2A3WCM8oKJɪE Oo#>#^rx s}s}3cؼaoXwKZ8(R6J +c̝VGqTZZW~p +y7uO:y ˶%YlK,Y1pP ۫C2*.|hf츁 !!oT@ST?}FYϲ$>`D>Ug:ʪ*5κFc//Zk?Pg8!Eࡸ12VcO # 1K$=ŵ7uji 仅 a0-u幮,;XCե⪩)9pcrWCy]е$P{{C5aH/=_Q[ogٖr9K\Nv̒eIXmk'v]~No Gig[4i6^v1+Yn8"iIj(i] *pN1c$=(iq?e.%S gp2'y}䛰|ƕ)HJN]*)0xA:v 1١3fѬӾ>,/?:{0aم UI6TqHx̖mɶN DÇ<dӁ)EpՋZ5i,WQ[ cnug;a%$8+Ƙ[%mRi޷}ٌ}es {L]5~Z9p:+V U"d9eqt{{iH|'˺f zs-0_xr ϟG+{\_vzx#L,-g΍$lP:Lxc(yQ^&PsiDn>'sO$e\$lKN֠Gc 0at\mz| UN=ʪ* <ߓI `8=9Tr]霧~<$S5o*O}Cʖ5d1YҝcG,'2ƌ1lRu%$|^~g<<(;2*%l`D~kLS(B䄓 ''29QDϙW+>1 Iw›G3l+oM^(.M &<NmTϜ֕JF(u$ߣ沒XS AuQ#v4z`FQICQ^֍Uرoik:oXgap {Gt^霧T"X͹:e0 ͪ}ur۫LW 5;^*]t%h%m6LGЎg~P!q' #w1wJZu'S՚4u-l#e0\Vm˲t4uVk*bl^*6K6 1,gYU?JU9}vmۢWߤ{#Q+S `©ŸG.p.^qt%7ծ~.plVG{GŮ!m{KjM1C\s]0},W6Hz#qd'|1fU)ji]چil#Zξޜzgh^Z_u1UKaDC XjjiX4]7" ĩM{9'ébTJ_]TOH⻽ʹs a)tѮyB;^-ja}osG;/j Q[z1K$c '"Q [gq'1^>ObBupmL_՘Zɲ9V>`Ʀ?節W:w9-#ʛoUuc 2,IRC4S2MJ aߧ%y\MR <x^UcjU3v|`/Oꢋ/QT1*?=uJTutb.S̛'Pҹ >땮>=qm{*^yJw>œ`6LkJ˂-FsƘ;/N NҼJ0Pb>a%T?z.kx2I9ڿkvOڤ_Z`B6y,ji6jy:p&T'tq1U}4y 2nR:7)= ձ$!I25Z-+efjc(WLzX81v!!Ƙ1*LuuWM8Ty|Bx(c\V3vj'Lj'LK/AwHRdBW\ۢyU7)[vߵCd=9pڿ{g`') aSܢmr/qgSh04Kګgvn7Lp@0?9Us< ј2qhn&k7[UkuE$G|-9fjT 4Ҳ jKBVvٳ|1f߸KW|@ \Be)Y9:WXCS0zի~|+RպꆛTYUES[[G(ۗ.\6c&5Ne3fꂋƖZڟ}Zmo?ˋ4+_cC#tީ۷f*Wb_<ȝ[8 vx9c:I tjF_|PQN҄щ@+]}s4CS0G/j.ԧoIdfL_Zm?vVVNj&MlMC:-GlfjƎU7D7Զfv!i8@HcHڤˆȨ9W-+Lf sa !Pz 5I}`7+#(դ^OvGiԱ8z805qjBMj( Cf7^m}U7ܤiT@d'ңͶ_Em* Ό@cU;Diݳq66y  S.P*a:3^@Ij߫PW+Us.o=ClվUww;X3]ҠQ߫N9jߪ<oW7^_ظRO0:^3fѤ/MDi*`|<!cY"(n-]چilx(!S ڻG;^U0I:D]:~N9L_Z7t &5P!xUz}o3jnBthWWw\j]}㗘n`G5usWԖq#"ƘuGjM35o*%S\3 0 U2^y_ik:|>{_VeU\ uŗN2}i:qik[tP([[;n_ȦOGЛm´c"Ƙ16Iu7/jՋZs"`<GMeL-UJ}ýyu^o'>M OO󪏄fyfkU@ST?})#_xI~U>f}ohe9| Jn42eӬB!2ghk5o*U ,xRru(&2~ۤsY'ޛN#yBtOI;Us>sӗiBHe'q29ۢnB < c\IOHʚ6]s]4@Y#,xBDBm2~W9YAֵwvo<'|ӗUYUEB k}Ú-[㬣 j<c̝VGeTf.Ms P<Kr,ˢ@x<ި1?"ڸz%&R"?rN@cIZ7uj|xvPry,(XuN?aPeUe'Զf:mҲqg aD 1cT8*ktw|< 1;}K;^R!Ov$8'?Smy(-]\q}&Jl IҌ(7oܥsn"b  T>ՑQ&ݣߗI*RURX)΋ӱUW/S6%t8> %diV!P7uj|f%^e8(>vʒ%-q €@cHzP ;Ϝ֕JF|r'Pl[DRmS Y7~%/uA:0Ƭ<*m^ԪxƔ:9(vKmye=QY#,xAƘ1*LuX&S՚tL,|^n.K!eۊ%|s@T[9F@9"0B$͈zk5o*U)r|ϥ˲'d'q29ۢvIKN@!01*j)- tw)M|_LscYeQ9{㙟Fe*LzD@9!PdƘ[%SDq6g{r A# ͪPd!_jc(ʒ8::"2)iu֚LUkU2Mh< g3XbAU=ڿ{ҧNPdRUc.T'//cl˲ S, $Lu_z9u]+uO{- ]Xdq eGЎg~>8y0Ƭ8 ktͷta庮ye[›ֶiIG[ǩHU_ʟyi>_b11[v,E2/R[{@tO[9F@x8Ƙ16I&S՚tL,%{<וy=wزdYv! KV07Pn؈=7~IUcj):ly|.;,Aӱq 1(+G(,]^:JY2\* E P[ߨyKWz4ԥW=YiSx@Y?e)6|rMS~[G;wE⥔lT<cL j¾sҺRhH_3(<Xv,x")1@i[\o*/$-q J4 1f"vh^ԪyKWv BJv$uf$. wRk$=>~9#0Ƙ;%=j] ]PHU8_$0<9LsJF1:i>Ia_guͿ{ι/7+y.W *'G\HeK=@YmWm}c3b pZ'iIZ7j=kL t<&XLv<.ێQ4Bjlq*Rn1xQ@l%++i MK>7~K81: 1vؤ,vg3g2C:g2e2"!E/ǩ~gx<<(KhT(,wƘf: ٮIPji]I@r9}@\2=:2/\>FBr (J {A `YzQcE~ 6zF'c6pr&jͿ_4u-4@ BgEx}ZDE% zՎ߿L眾"U^4)LRP@=OL -O(H$?ՖRt8:@dcHZ'iaz+4u-4^O*D(YE{N?ߧj.T"T՘ O$TUSʪ*&:Y۞Sۚʞc躌KK  $:j[JiMyLQ;H*SdBj.dr }@autڸzzR%-qg;]cL BvoTKJ6L |Nn.WC l!P,kqO>,'fzOw/o\*Lz )aMjBΦt4D'`1%**(2 tr| q+*d1`}9%pj@xm;Za/y `sAD1V&;:0eZZWp9s!n)Y9B %{ PG?6}quVp/ <(0,py+4u-4DΞg3:&;W<9$0ȶNNc,1Ke}d"Cz z+{"\8 v58@cVHZ5&S՚tL,"X'/? ,Kv,XwaIrsYyp?0h!.ۗ[[)!KwՓ#}Qq50 k$m2J~ZwPKJ 8) gsٿkǐovE~l_Bm=wj˯jWA̯-ߩ*ޫ1f  IҰC2UXsn9e{^+cGC[?P(=rԱ|ԉmQ(ݧHyգa=HÄ? PNF<%S՚Z|.[:sc{K۷}i<۟}" 瞍?YW+7N耑6&UycVm‹@`=P[ߨwUm4 Q6tKtLxp:6?߳CW:Cgz;~_ @jip.u{Y=?{{)F\E?<)e.&@8x%7p{a]ߔZ%Si6D+HOQ@[۷Rү6_Co ty11c8aJh 8Ҳ@-+i4Dyr=7P'K$hs/˾l_> .K!(#ݧ &]p]@UU9.~ȱi\Hl#z%ux%1pEuuq6 QrD% <87_Ɵ9EQL"3No翠k,첒Η^ҋ暯,4,$^{fy ` 6I5^ Ms \W\NcRm,䑻G}$GbTP&Q?4(+Tkgg7С!}ƌGB-zUDXH FTT-]%4f@< qZ 9+;3{w!3>]O {ym}5mv=v"t.=졇%l8%#% awUm4 Sꐕ"CX8'v۟}Z3gi?ڟ{VGik2.; {_{Mo^$KsBfM_ա=;ø6cz<x#,iBvoԼT=b QR.SYbqpn7>GejOtP\3ڻ#_z'?8oL:=2&5fj_C=PxW@a 'BQYdj4(}W6O$%ˢw# ~;_l|M\4" F\CC}ճ?_a\"PTa;Ϝ֕ -'db49Țwۿ=/gEp޺QHLvMQ?:IS5T"D5_Y1Q@KRC:P,(,PKJ :Աy8rXTJgRus( sesDBx8'[~Lw8WޣMty]nuo.uA+5+Uc.˯AgK/wV?H!"?G'%~qut#"a_M~yH;_l&VR+'uVdx_ݪwwCm;t3YE3,/U`'P6A!*ȧ5kŝwR wv=B$<swU]çh`FvEW.עh C}|(EՕZ$6&z;;(uo{Iϯ_C!*wX*bB?g /;8x,j>CaNCcD?k 4 9.Rh~TRrp?>WiL`QWxyFA/ՌkKUU]KQ`9z)D㿾M [Z( /ygmŅᛙi7;f֒[@CzgG}77)paH́Ubg OdY=N'3|;(xrN%N'`8r8r8M NWSWcR24<دWMjݦH!2.f~_~ŀe24̛ヌB z=!% Dҷ8uŪrjh`va_Oo =`!B%dDZvƧP[w/v4.1[tHN aP.ҩ2Sĩ.%(d|XL&i8r8r&PV"zG>W6 :[SkKkOPl{Bԧ5kŝwR3]]G a ,Ӓ[$cEuz}m^gɵ7 9NtVpN@h%2 M)N+ SN)Ia]ч,v3{$B٣g7h߫>DA,`gkW˯(~P4ad ! 5>,gk˦ q+Ha :=$Ig>H'3dR&T*C!+{$i4e8(ͱs7,\EW]Ä%Dȡ} eٓaBfLĕ͌sar.A Ļ;ܺ5qV#Oo`"tT+y- zJdbxt}@z;`v?QNUҥ( `v/Y_ H2 CNW 0dyz-owDd"**'ZVTJTLJh_FzOw6<د͏od?Y%IfЉ@Đt˗$0e_~.\~kQP/B<!r]ؐzvh~7 իvR6{i\׃|Pڷt"EAd" A$3Jʤ9G{d͚Wb1tT۳Զy"݇ /~nwv)r4箹E/7P[w?X߽Ss)cv@ǢF}~ `\ҩRɌ4] P1w}hk[m;%ޠ-Kl`6oP+g.49p6I ~=H62]xf]TUյaW;zL;H!@ wXK!wa=E==P$G9P]tjx_Nz[Pn@e3Y%i%it}TC^V#N 2YtD7GT &{tp~sK.)jx5[t[> hSqE˖Qe_Oaz<sءnl]jܾ&:ѮO=)!ŒNeJd(N%ts8iꕫ׾W7ȻoYk|S]63Ygb k6 `_zcB*Am}!o}bXPüy{(fJ$JJŹXYԺ:,-hY(aƵxe9fJ -o25Z'[dk4Kuɍw(0hn{I^T`e-mQ@$ǴuƋv" DE3FaaX3c8EG=q^Oq{}Zq׏T;)H1P,Efœwd/ ֝zwN()~xf65QTt0`Bu'8̈r]8E:pejo;TznPx ]b`'Xb'l&Ѹ೺[+)*J^ju9r(P.R'B"P`Py`-@?i ߄hx=04p:dmzeLx`ULVxZCIR Tj(ҧHϑ@p&M(`\:Wu>B kۼAmnPכ۬B-Zr!FnkBM{DazuQ_|C-[F!PpdF0R`PLS!!n!=.u8@ h8R<UpAuxG}?455e0j[6iStzXu<^ŀD jۼAC_!:9XSr(sKѻ>|@1cQxK;{SS<4ad 8+a]9z o<P;\r;eun0Mx~=.ţPn<('t}-H]o1 ?ǃ'NCqL>2 Ew[wj놇4K1FiAR- jݦ͏m:fJ,#P.:zu/V/^3,fP;{S=hhoQC If6O$zuhzjX"=|5ad?y{B>ҁݻu`nژ-o–6M7vƇzgC c%}טd@4tnfޭMM@J2%|M!vk翨@p Np8 =<^3oDž#dHN%E3Ff+WQHA>uIҭ˗ 8\L 421v="dCx > |.kݻsI:3HR߯Pc~ؽ[lQ [^ϧ<3=}g}p& PL!t$q;vA(CCzvnA6Ś2| e<&>R"sEu]ϭ_}#]\ t<^ŀuI;7\]on+튜(th ߶:|PJI^sm IDATKx~mչgOі}PPT Ts6KLL"?Lp,kO>g/{47Fx{n!G `װWE7t;Otnuk0M Qu҃RL x`WDZ87OP@ B@"mvGv Cӕ`e=-06B@f$(P@{Zvf]XSfD6ny}fلG+h?T"dcDwW+R!b~gωã[ _0rƣ;> /LS*[]Z6X; R}]v+ FO 8npr Na1ڲ|?j&zr(: @ u~yAǔesNi(Ϊu&m}! !~ MiM=W𑟉Ǣ=ةxlH;sS;5Kaym7 zs[V2iFA"ȃ`H!=O|6:.t Z^ie:8UWZgnC΁ ap2M牀C]y:;T"nWu]˯iu׹4պr;+nt{<~}mkEנ^ t|Vc~=:Ѯo7F-~ ^6H`n.Qur`PX=-R E[$0<^[ۿ^,t:T0]zg x=Pq|v(?' rv!Bx~Yy;) XcwIkt@X픯űS +P {SW߭D2P?} `+zy}jhV2iF<X%PWe˱ڲEjQVy'Lt;4%HeXRuGz;5S/nT@#`}=ƭں*M & #cq`n5Pm] nOhv{y ;NCuU}tXTOluE^w`];~g&Is>{a6tM.SCznYm70sa-S}N}S$՘-z4Wc|?їn_ ,A<P;X6޿"NÇ0 yݚ0+פ P!ⱨ_C VO<^^ ٟ5W@`ٿR"w8P>6~H߯9z|>3TӄҜj.I!]nKnl!p2NCAn6FN1]U<)0}xt?q"Kq{M.9Y\N* (WP*r5uPڹq^߸VҬDeˤ@#{9|B",l7soхoQUu-sQ.{,liQ [F4_* "Jv8)ދL&[_r$Ioo}NB{eƮaTKn%ay"r;dk*h8Y + &`Uә0 E+Rynvp{}Vv@يtS+Jt\ō9\eCjq-̦&AEzzFPB6W6iƟmYr7y  [ZFxh(E_i՘M9^I2E'"쐟 e_MpQ0lag(J54fX,*Sb QⱨuŎ^ em>E`r7x }BV`: ;-nטE <6۟X_(zPMvS1!CݎPq\n|. G(sSg͑B@Qn|.NQjR4(Sϭ_Xbǿ`iÃjvnyirAn,F HKr~s/R [s ]"zrYY.T$݋ =`8c9np2]7*vf 6<دk(]>ijsm->5yD*~k?|`rUadgzv90ʅn8}H( jNEԤMM#zOx~ؽ[<("֭a . CQ*KjŰʜ8 n S2Eܺ5]EW]ÆKtS+ts.XY|FlNs<>9PFu&Vb^\U@F =P<`Q4xM8T{Q`P1F`P [--c:jlTQ-[w뉟3Er;Usi8 &Z~=j}/t0kT“V"ebJ*Ys5m\6XɠCGyCC  C7U̦ݎLz.Kdl\rYy-I7N,Ùr;;VhaKf65q.D~ WR KK>xɜs ,L<΂ҩ2ik$nBͼvF!Qa [Z~<>|I-[VU~~mļ@CZ#q0itok9 iW㖧S9v):uC=~;*Zm]˗ѺEq(P2.*[>\nǹE=u8pƟYoA`]OĭyeB P(}v$!WSU^"NJbtf65) *C1e_E˖/2 =@{MEy:PjS/ki5s5i'avQ24M9na;ux_{E׀(|S6n`0}{={>OKMinf՘FA^jȠe2 nvX;rp{2hז-B|H)|t &F(HoB'tժJSgьv3)(Tuc2daO߿Z[ߨO5WW\ņxevnyNL <)p_ٟ_KnC)ۭ璿kuPcf65>ί3iTxS)(` P%_b]iu˦?Pl% :4\&]!&3 ]| Bt7u:wJwd^_3S*mz=w .<PBv0 C*>L0~lѯ7nNwv=xA =t@nktEl&c=1qZXyhŎƻIA6\I._$7EUQrhqcIk(K%3<4pAޅ;qHNCU> Y"Izh״ŲA47ke-j}a *X } u z b1 Cj~:vC$b)aâ+v@$ J -JВ*bmj(H <2%3Y,~X"bdYӬ/|MzP($B;% JzQR.c:S1U79sϧ9iNs47պ ۿH.(A}?|XOT4yꋊF:P9vчԺeSE~ ]~m~D;7A+r7>\t|)ӡ@B2ֹ5ʺc錶vz͐NXaɍ(w뉟oC3:޿_kV>o7O7}4Rl(@B:1w{5s :hnPI.ZǺފT?y5<دk(ނz;DLr1Te_G`R=WM!@=`xM&%3<4מ|ReO3kfSf65)hyyG5'^&L PO!H{uxw|;o͞KEpP%M9f?hBX<ڕTؿt㭚 lț殐ML3#P|5nyar[d&GJes^.mz,ȻP<P$v;>7dR9]['x4ju2s&LPc_psGgoݚąiH\XB0<4}I"=G4]8QNG&r@ѹk2,x />8(zwWܸ]\ *UE:fq~KX`N6 =D$]wA()ilve{U5wvؽ[{HO/\AM#ݹC-CqLi6X08imFk @B uvX`1;EYRx~ؽğߕq0I3K~sFU,^{I=q֟i7O7}1:U&(?c"$Iai QBx | V>N%B2UNAu%3`RmϦ_iǿ; xvwպ `^nO7}!٩e8ؙc:k"7ԟp4I!Jw+aWI."]*QakƂ? P_5@J 4(rtXvqOiHI ޕط'@av7<4Wݥ>Sfo6at*1 qwQ|@<`$N!JR0% :\,墻*"*Aag G<^BR&4{vo~X\xfCrX $ a8][#ߙLQ O"1R cj&zr;)>"HoBP%;`:hʚihb.>&Ρ~oءg =P(P(2%:Tl0A!BLӡ 9M Q{ƂC٪"xKviK^g$3Y>Tp:duhLt܃H#@v@E(Xc:0(Ϊ;*۔KA Z2|A#PvLS* knC ~Sטyd&:RJe9+KUܼ1X&|.k#V*%Ǵ鍶חnUs ƈ,0wE.CY44TS<<0a .k IDATᡴދ tpJ;g6 = 7nX$ }_2stӚ_3eRVR5)FCin٤>d>-_J\Ae4 {)BʩZC݆\NC5CR.ĐL~Ùb)?ѱD`!RɌ+;(A(Yҿeɧ;(;Nޑ\-93KW>` bCIE8f=gE74$BK v@9I(TMC!g cQJĢefWj<^(ˠX GLh}40N(0c#q q耞uuiX]@)x`;\$i S(*KZ7@^x>-b]]\IGDޮl&SOAN+VvQ`; CI].T<0 Phrava{f;P <^B o&JZ$d8>Uu#dCE %L wz[eH/efB rNixB8C XOhZs׶}f՗nUL:XIS(E)j0P"NBaa]8  ,_2(Ki?AW"!YvXUW3((r4F!` P`R&7ҩme;zL;Xׯ>C Z_ PnJ|5T`'lH hh T"M1̆OY@A <APx;Eq LpG[wzs.ZHGH"P|S]\~;>H˯%7aǃt*PRXb CdfG')Ph%0<XTOSeRMt0`Bm/iS90m\}rᅚ x}jݦߧ7`B:[O,]!&(,a7Džl&X4ᡤ_"o|DPsv`!"Ss_(cq%|,\M9J xضPV) GSJ%=pnDEI 0y_+?0x_o [0BI^.@QSQmޠ[#UHt^~~\-vh{#﷟DWu+>(-Rhź;4cb[40ض`YdFXJXJX^lz3|!xPh;<xU(.Ki0mޠߧH-t4Ar0uJ<,u>wH]ϗv\>e/7 "?La Ð?du'SJ%2JSt ,sݸF6pZ@}qJ tBgB0 M zyZ'Jb W" k߫t{a$]IKҬ2D!/|.]?$4|mhOe8yCPvg1Xܪy<k΍kƵ&s8 IsWHɼVr_~-Jpxr;ٞP=LVTCYa]8Y@E B7KW;%x!ykX|,pvA@IćS< X20KV-jv>ǥK3D;l|p{Lj\r'[*Q:Q"V&Q*(=p@!Js'Ls@p&tw@) DJR(czy}jhq|'/{\=?yC7"0.agplOt*hHJ&J'3t$in}ίs{v p8f0}nv$ ]*;Fhx"A/7d)K9w(Fi_6ҳ;|SE >̺x' aS=ϫ@UCۿn!y8~i_'@B %()Pckt_+<U\׺)J">8LW6qlq̿z}o*0vk5ٞ-*(ulH'3~;y.]sǽð#l"P(K6;Hҍk QI&1n]%1+`em7)} s}^8"`T7s9X-sd`lzhUq <, =(&;eWM_YΤ`T) SI^.@e3YTPն5Ҽe^>Ctqܪf eZ_$5\&]!&P8#0ض dL:t:t:L:̉@,QzOv =l ;s2`W{ޗ\ȅPJ! V"XHL!=f̩S_yhO<:Xvj*7 FZ5 A@@J Im?6PT.ZkGH~?dg)UwWě,%OҚ.ixY?HJvnXV7d㓇N&JCr#7L+stJ6ܰLajjM'T9WN.oYhB R*JrQ_m[R9 j]N9g3|Q >N @d(P~ \sӂ~s  kBa437 pㆪ`SG_=|wRB"/*`:(v@ǢbRx BK(B>~. Æ媠(Bv #n][Eۤ{>k9xyցd3aoy>C]jmKѰm-jܬ+A>s} B *p p6ixxa%mTv@* h$"+apH ;WN._װW,Pǟ)Amh :0aks!pLJJKs }dCIh\\UՋO_~OS'_K$R;K=~mbфNQ]=U.>=?dԐ EtڳVzk鴯9 ]O@Vvt.)I˱{P]JNpN#p=0Hu>RM2{]D"X{S9ɉW &V,@tԁd..kB^8*T&KH# }2|5srl7"grp =9+po}yZ:|M2<|X[ZI~mh5kfrڣ k~s\PآWҨvnXV7ds5M\&rprHӂr8j筘M@6$ߎd z|7MsW @"p10IApa.ßc =\'\ۦ;|ϯܼ<&=G<Biv?tfMr8׺_6S9N/H }6''>|(jf.]I3KwA wuzdy;W X Y/eu3iM2Ba**+)5B'# G]X4D̽E8! AGۻھvvo}JNsЁANq|X?N~ uuLN| 2|Rsn=6v$υincdI$}/&έ@%pڿ7LyƟ~HOd%\]@0GL 8[,=1  3tmxݗ$G+ɉ>TI@tK/zڒњt)gWAJn_Jf+1LR;Hm#}ܧy2c 8Goϧܼ|҉zGvkWț{W7AAY[֕'뭵Mn49t ''>d 0fUU/V~P %X^AB{uHefWd'8v&iRYP7|G4Ƞ-=srI*qbC 'W` p H@j_[s/:Ӓ.*X|ym]''>d*0jÄ\ir:A2 5NSj>kGb{Xai' xa%} dVO(P{Ǝ! pc݊\@@<.H< z{b uD;){ھv:޳WA,6CINΛS |# ]!`}̥UR6> #uei]o?PB w)9dW0 cZcUӴu4Ȑ} _y7D%tXk|>Rӕ:/oLaDK3{,tܖs*HL& >HRURS+~Q+2r~[/qi x8a$>j^zZbu D{<8c>8b/t2pԽ,Ez=ӏ!\#6*.a Rc+a#HobuR;7֎  ٹRŗ3t ur锎 Rb4a  ]/zfh*4賯^1T9F}[ոB7MsWdG3 cgݾeTraf=^Z?cF p? +w=n#Yx)P| AFu] S:LСVRZ|7,c-O > F~s:UU/.QCTuԝ|CR~45\Q<0ɒI*q> uy D{js ~qEew?8^oOL]'z]XWx=!8S0WɈ!b%,uuFf0mjڽoe*ԐCD=Tp+4 '/zڼb]q<\\%ec32=ATUd4Opewx8a%}4qn EZ<|X'Ҽ[=ZZR~R 9R4ldٙE8іhoyZwu=SlѫHw3”!|\C S^~Bⱄ¡=qd,`LK; U20Aq \n| .Mv3xze5oPAoTUbOAH]K39n۪.;/s=Gx8aIFCr-ͧ{@TTV|bRągO8ޮmi r=n8t=}Ţ%b q0pͥA/ ;]=qڃ 8dяLXn-Ŝ>oøR'phH:X|{ZU34sr* rPzz+ 1 X&WnGeBP̾So7 6+)-VSxochWV"e~ILw-d 3]Q"q @Ƃ&d|6R nkRnq9 B9`S[뤽 >$CS)|S̥UU>Y$?/*':\\Jisex8a$}j?bNAM /Q AZvXVҟv˳,%zv:Vkdž u{2rqRdpu^x=`9x„u>4΀eCHHiiJM t%C2>{ %ik[<20ɒI*q>J/RKVSwDaNkbpIh?#8VdD=F9x> %"Em)Q"$@e,[,U7VM9H҇:A:1u7Sx 䐩9P:Mv$U̖&,! ZozڼbUU/ԯ߬129~][PҫMl (0)v} -ڕ;`Nk#u0_ZD,G:q;kHd% WTqI"VR7Nw} ߰St.W}7~*;R^7_,/raR~LH }6''>5NeW \>}t|~KzBQ :IkRf܁ ^3=R/55s7v88YM?v[ɉS=|h}S_5CSjnͣ@tei*Jrl > ;ڣI\c$2MW@FRtFWm )>fF:yB(9AJ>Hݓ|>A~ xDQI^x,pL1]#ob7Tr8_9MwR&c fr`͘~f20n4j:߸O]:X/6HQ{+)Kz}HߧQ9*/ՈwagTvF9o[ku^?9 ܏ # ø]Oܾyy@Pˉvm|aC1\첯QM %8r_@Q܀JS Y9 Hiɠ%z-O%y!--o#i r |8&'>T9􉆒 5NSj_4^Y$ӈ4 T?D4Ї]QXoݯMw} {4e\En3 cgݾz- E454hc`x6>v/)-Ւ)(EPHOQ0PD#qt:{ھv:޳ă :8|ÅӤHgjNU4*tC[_8Pz_֮Kw K94jmGm=m^anf.]1b9~J4,?`ayO|\8׉c ikb<ledI$y uwDC/[{L.핫4lH 9_ᴒRݾj5pF s5Nfigz慭6Msw"a$5Ip>\5M\GCg=6>H1\g7R}9s(IR i0lLypzBQb SOIܰZ>풠S'9 |1ƅ}q P2em/p.ktkht^5O]~ U]E@ʽ|/!iiMt܇6a4I=^~X4} iwbK_K4k@ݡ iT1"JyAV7i7bVRn__ %pB*',L38GSzAVӿKCli(.]/hKFkJͪ^|Bﯓuoct܅aT=4v@vp$)Mq_ P1vG :]k9|DoK/>h|/Lӎfr,'M: K ɩѰ+,Ԛ:U]1E|_y[i( @qyyZ:| E=z<RYz{`iĉrǺ%(?ӧJ (Ü:܎<tؽ{O\4J&W9uw}Y/|ܡc|&^W),,Ƶ$&mQ >r4|U7jJM.%3Ep۷i(@Z@ҳn4f4>#bHmO dΎ^Ecl0(WE9ⱄ¡(?:ذZ^~_4eSz#/^IRWfHkl-Yf >O]~ MYW\7>:/i: @6aLMR1;h>{^1 0X-/<ӦkG!,SWG/|z9~ !hBa@]#obiɠ%z,u]LSKߓ \}M?tw/yY6IXi|b?Pfiz-Kz}0;_Q/driMt - dawPىv=z )Gx`l[~^Mv| dX4{7`/a"G1l*S `vo}ZAA&98b]9 ||$g~}v}`w49t '/~bMӔ: U}&BG<0m=TV/ D.C!<+UAn}'2.YJXn!yA*Bؠ;A`vo}Z׮PG{xdСҳoŞ]ܺvaRdmJccׇ$|}> E[^Ai20nSnZRVQ3.WIz^ZyTm44t X#{(J-YNyA~論f554,5d7+ab _¡"M?"q"Jy诞:pHӂp7_-uV?NY{-1S =|nѐAcrC/jf.]I3>y'#os66nsxa${Z6J+7v@=tK:ÓD{Na#GvojjhоW_QYeEd=58DQI)Dc c1ڹavlXP}'-*k廢F*$ά͒IM?O'|׍> FM&, {&hmH|Ih\\UՋ ;:?۷}4北L)cdIoyyZ:| EklS?zBx d>,aBji-)p̓KJKτ d\v٩ ϼDKsÇܬ w׼nbLgG"1 Baz{btg0mjOg.0q{b䐹ul\[:`9|{tC>&Y] }h+f|`zl?uIhUU/Ҕ:#3Ep۷M4Qp0 c&I%n4f4~[Z# q]O=L4);V1e9Bc'NΞD| bMv|.> v"XB޸zQ%< DGۻھvvo}ozmRɠS2Tnb3wڲ,goWS783d|T:\+.]/}(Eö:XW\7O3.WIzz44&: B0ha M$7cwĹ54 A!<*jjhоWp&?! B8WC s(E0/$ɵj]\edH7魙k-%!/m;bDer %'>= I0WSkT>iG^Q/d۷*ii'(8FRwL HSC6 Qx8ޮ55@!þr'T&C*.ɣ&SowL< ܑ]hdž:ؘ*_eTeUI_ 锵V:pY=.nLrpצe^D#*97Hֆԇ4u^Dqi{ݾ]iNb~=Zfb6>v@!<*-z^_k\lb%,uuF;~ S :c Ezt%0A8m_BG|S6iNuz8qNೲ4ڃ1Uq q R[3>![ҡz{VP@6'~꒲Ѫ^)5u/J/襕Kni. fIϺy_%+);^0kK{Pʆ'47k'TZM!N R>O%fվ 9{/o\Lqh uu4sr@|̽}4Ms #0&K&ĭ{+,-i Aüx0֖~A**+)QhB^( TAq=rRo֧} ug/HNt(4'0Y^#o9T`:;Ck)e_ys q >Hp aO? s5Nf KE|_y[4&: Eoa S20ɭ{+,ih(y1 ij]= v0wꈈd;|> s4 (=E#qEzƔ3 H]'u-r 嫬Praњ1tIἷ"&39CH~@H/w!ԇQO_~ U]EB]G?Ц(r6:$M6M@xa%}{4qn DJxo_WjinGVѣ4ut<&UwWBG>*oHukE )uR;7֎ ĹRe|K\2a@kc)p.r8Hw_ߥЇb4a 19KFkJͪ^"[k]p6vIe : A/a+7aߩk^K32iĉ˖c|iZBa%,<ѫX$N1 a⃕$IƂE5:X>|Nt|akj<˰=' H޸/m4H/~bUU/ԯ߬1x4et2 X&WnCeBLoBD{?CP!xGoOL Y*wHPygvX$X,As4h{W׮ۤ C:WOk-I6HѰQUՋU>iViJo4`?>1 cmJܺ˯TM4)Gx0֚Ro8L3]~VRWGD)'?܀9~/M(K 9ętag=A%њeMSҾ5j)G3||tR/j.RU"zxK+ݾ٦in`/2 c&InвQ]IyECi(Rnwu Qn\;I:)P0W0/7DT!=p`C{sjdii>C:8H_,5\r9?mt9Yڐ|;fc%e5fUU/V> wj}u:~m7oCd4[(؇ &Kn]^a?N#/@3yD^l-(؉U2.LF԰#?=Çe>i{R[M G\ElDS> >&8X,A :mi7 IDAT_BG|tpjO<'~rpL?r8dhf'9~G# R닶:XUՋ57l <&3EpK,4OQoy 34iSӦkĉrO.f_c56<|'kw1<%EQ Xa*kxa4=^ 4`{\rpl|>7='Em|^NA20n>KhWUߨ)5u*^ru6_i.`2 c_yN]Z:ޮoB PرT̑ F .JlBmRsn<U}w#8'0&K&ĭ{^# [ ;?%e5sr< ˽a6t6fn@zx|a$5IpJ/R+7Lz=(Ehu62"'oy15 #<SOwLHbx\ƂU.rS4= <#iZc~Z͗J.8{mZJd 5NUsl \쥕K~Cd4[&'Qܺh飛T0f6=N08];t/,܃ =I-z_b^,P_~oK$,c.jӓA|~ߠ0(9D{~o:Vkdž uwQ01dc)\u9YKQPʋI]:X/U7j;:?򶛷K,4OQH0 ^IuE=JC sh(lmz, :mwuϞ}[RGUia%,b E# E#ɐLrj){B{BAi2P1HҾ%e5srUU/8D9xa+oݯMw}۸4͇&$0ɒI*q&ܪkn=a#(\تYY]_YK8"p6a;yn<0<>'Q|>spC,P,Pԟ.mjڽ{OlLV@nͦFcyIi-?SlKBLqHѧ>JYۻ-̓Bi-liJMF.pǧ@0G`bU}WD7 <d0%ܺq_/[Aq`._miz=9N ф^С@vXСD~rp 9^9$ eK| G7]ϲ=!ko >Dާ#֛sJ~Gzk*ۂNvo}FU7z'͠/ȲvX4ZZ\c5vbkCQO>/Ѭ%Ky};B(0p:8uC֖uy}CZԐG8}kN 9xVfS/͕$b69|8Gj˶/lf.]_bшX>''̄:E]n7M\C7`<@2 c%G[Wa#(tdTSCRG:~y2.Lc'Vsc ƖS%}jz=/74g~H!Xo‹rprz#Kh%Cl"zYlW̖Hq J.'{ ŚZS)5u<,++x,sNm7oC,4& Ra4I|}csU\”8CSC56jk_`ðe6rNa#Gjx妞PHq{C(<1ckFLHфW7>WGMJ}`֧} ugIO*J{R!/&x(Grõ-C\r>Jɉ[ɉءTZ.zUIۗQU}f.]1c<Ӟ-O_*ii'& BaTOF7տWZg'ҾF47C Oڻ^zR{/t'͠'FE99yyI|[d: Ga$ʭ/q-(1n`>PH-g'y|NLa۶~ ~L;S -pzZC, iTUH(ӏY~S8t?򶛷}4肋00 1Y6I%n\nAnkW|v@ϔwƗ/Ѭ%K xKRx:[ p@!?gyV~KLq%@ᢧ} k7wvhs2)ۗQR6ZSjnVUb /.핕 rr)CDz(r6fn@x,a0IM*ܺXkf '}zB!jݳ̿jM7HJNh;p1vnXV7i߉_)_eCO#䐩9Pק8}kLqf^9X}HOIøRN!7ZW%1?(|Wڻ^zc9i2 5NSj>e)镕c`P\0[k]p:$M6Mn@x,aFI_s'ܪknqWNn SbфN=ROc?M(]ƂDKؖ :GB_!Llr8Lr F%ۑJ=ۗSU}f.]1M,KXTX7|  P':}սn.4'I øW=n]5tXO*- xR4w̔xwk{ (Ufߒ.z}C6pȒ<rpIAʖktzoݫЇtbT05)9᭧_}90WSkT>iq˲d%g=~|[x4etxa$/B7ܽZ);搢\( 5ģ %,KHܳ}gN?i2bt+&|{N:K <6mjo[7P~|KStzГm+RyY9c)YNI-MXB@z+RJ-/HUՋ f\Gvi: }C<0aZ$qE^ٔCs.%HXFg&7ģ eړ:wE"#F3+>wΐz{?{SEGۻھvvo}_Ptij$̮͑g5c$zo}OH% >|Wjth9irJFk4;%:[!iiMt.xaM&u߱BN˱AJF ",M(O(;5!P,0]N1\f|IMFC?]:c^Z{HJF׹>@!ksd 8^7'f.kJL͗J'r#@+!5Xe{!XSk4NECb[kuܺ]JNM0Qa<$n][uuͭi=ǐ\pĕ[-E#qKϬӮ-| 1"j߫/ 8x^9X}rpɃR 8rNupw~gGT&'>|N >Hҫ?:߳u9yŪ^_Y%ecB[d: F<0u˯xЖs  `qnֱZ;)D)947 JEH!X\*_em 䐩9Pק8}k9x}m!0 d^øҨH> >ڜ˶/F\BMo=piI8?1aLMR? pjr>ϧOR$M(O(&'ؐf>x?BA 6@W]#ob/:3 rfK5rpƗВ3|((MN|Í&s{/'>}%_5C3.WHpiMt΍xaÔ ;Lrs t]5⳶7d. X,h$pHD 6d;v? ϕ 6n窠ק8rF? 98r 0SHOo|J?NN~uIWPE^DA;%:[!ii'&|0Hucʯs $8X4x,xR4W<#)gO|lwߟG[e$P4J%U|A,=ڶ3$'|0!m5;߇e20n~2S +H#QR vHM7V|()K|x~mnLӜE'<GLүܺ5[3!E*(bB֋FJ-NMlEN"ᬭӂ nѠ}xB\~Znֲ# 1!_6LrH[H ys q >HRihGwꏥl]NIhUU/Ҕ}Y{n[%jcL L4q@1CdXwϩ:bUV#*[uGLcPhd9&& MwP~Ue[޻r5'`^WmCq$Dm۝Hj3p%ߖU9šBlK\o|Q?5{a :]ɚox`Pp8V8OUᣖy t76է>d[Zu1xohs?5!,pg G gDL]w:erpC-$I9t^xwÑ]P:kI+m׫T0!tN@0m۷IZcjv._>B0IJ=zgqIxwt}|~#]<]7~@(t?I~_o [ 8Oe<cC4_W=hN&\ eNbAi:.yVc\PT0GLgxo\^M--ջo6[qn0m۝ZkoU:riLp0Z}idzO4T0I:::鑛p٧c?_[FN :D}{Lq}9OBbq]?֓7za w>*&7ߖ$ۣ{Rq SR&L>Ggw\ |kV8N#m{=}[Θz3)[3"܀F)_R;٤C!z~iz$MBM:&]0M\{w_џq]:d E!!&9DLrM?mC=s 8g'|O|M R~Z_pU_K:3tX|ݯݻT^*].'>7繟}:g@mo3nZ2VNO*JprZ%zg~K5zwq>_4GwhA3;aYJG޾|{JƆ"-ŋe]tc=$ RkY$%0Cy,V< l}C;% IDATK/|[zy_Kjr-[IkoQޜS4}gdnЯM\ݦnNq:9ƶ$1EiiYji(۔ bܣ+jVk 8Aywwߣ{> O8`8IpdIfr|@~K=>Ϝwtx]l0C:eIaT*餶cArv}^\o*Cs!8oLq0Lq0D!,ݵ^|M}7zQWJ]ZR۔5gW5QS~=BC [aSN'kƟZ-@~m\T€-8q0m۝Zݭ?!ӜR5 Ǧ6Tʥ*~!?Ӽ RIKdBLB)l'\y0A A˺bA8gLq0Lq0Q9pEmQ9R~ >DROJ#o'>I*lK\\s,'qJzg<;n;Ks нmYV8N$8"m{EWߠo'2H$-Zj՚jUc8PA?׻w]<]Wb-J(I*N(J`J~( @ :rtߘ`~/`~? &93]/'^/ _]X)=}$95=_3b~OR8{9¶ X{{%uZ#חL6\k:?UU9Sy˥O(o|Q'Ixb)I*I*I(IF5WbUJ jx^#zty qm!Z&t 9krE"zr_Y' :՛IP'Di,M8uiR_e[Zu_|^T۔ F7~Fgir9]LmNq:9$1L.E+?yj{ԜϨ98:h86ztJG9~0#zuT碙VcLqCa\*tM8k`58u2~DZd)wmu~m_4>`D~ *v6ו_XuK~msgj[8mNqBBζNIM1{A<^T-KM-i!S9a CTaI| 'T&L6L&{SVj.TT0I 0f,u0a̵hPIg<&9rۥ 9_>d'H.8uMҎw$(ANw8抿Z&7|}q՗Lnύl bmOC4\rXrk^\MI%S Nbpk*f*@8R RdJyoJA&9P.~2!4dCn7[B!t+ԫK?փzyyo|@(:@kz'Ys3T&t&dR*bnUR;>Pɲ,jjN)I!k+p0BL3S8BOSk=|ä>ZvB=~|/xm̚O҅*2m_ߚXEWߠo&ٲL'J%d%,3D>Pvb\:t¯'JjjI+5sBYT U/3 :0a rP)9s!Qur0g͕TKe*3~Ie HMR"^쓺? [97U tSR͹RRL{$萟*]t A&9- ,CЋ3!ۆI!y" h-Hɿ׵^ͷ >^ ir9]Lm=E $l۾PIm՞uݪujv) HZjʥԜb!ܚJC6"(C 6$Cx[N}#~!?g^I#TBi ,L-':4ػAo|1&O|η zU* ڶ+5 $l!iݺM2I5R6 C*I!X%1!řmC=#~zT6Js9Rh &qyy/s h+~??`j6: J<@l^,'&q}k=M cYMIZJ:DMR*!p&9a$ ^?֨93v|ud}Ä)_'q9"kw̘{Ϭ]۶ں  ۶'J+ʹ3^40eYjjI9ؐskzx[}qЁ)!X%1!1Ł.M!7/J=t:']tCt ox>!{Qy^Sq\[RE^3vsK~msgb$wg'@xٶEgL}1{M @!=@ :hB`K"96BsȺ>TVp.%R_;v}C#AlRI/m }ئ2jj w.m\%S[SaN1xض}oXkoUA#B>:Px`KrKB`@?~| d}Ä)^T`Ć[<6yRL3knW-Fq6p̶퉒vHfZ\^VW~T uUsuPHHH$/4IK[su!ժ|/!-ڀr9}BamܐnIC$BgͰI#6WoA-/%??H|TJ%uwakfsyu̘)~"T;j{&OR8{9LE|fIL}1{M纪jrkݚ4ʯODBDRdL6|[FVb3!rMGz :r ,CЋ#@alfCIQ 8}h\n!=]:֛TJTF睯^ϻܻo6-8|N.SxٶI3ެn&1ժrkuP2R2b[r4ThFbtp#!jh~r^!cO8O8DgqՃMRyPAtė|0^Ue8d:ijN[R*15U8s7'<Ol۾PImq}k=MstCVUZQVBLZDG۲6U`:, ,CЋ3!ۆ)S'+ݏH $֩Repgܮm[L-8["xضC,ZzDpfj*X! T> "FT]su!ժ|_!A7?Khh$ B3!qT<Lae?M8e'H-H,@RЯw.L,NqI&!>m{XcN/$!}H(z@vXKUf#` :Wʺ$S\ʣX\jx Lr0$`~?`~?]qslC&/}߃5jr}@(ۥdjYLx̶6XkoUAZ"V UC_z@53lƬPQ^Ё)!X%1!1Ł.M!dt< 'ߖ|),zknҼe+ > ^}aGZ9}CmOWRiw\EZOqZUnVwfAlJgeKRS02 :r ,CЋ#09Dnϙ3|W" >I),3iC;Ϭ]۶XzNqE& m{E՝hz'Oĕ֎jrkUH*ሆDije%` e  :Px`KrKB`@?>5r 6'E7HSM<`X 魧W'FЯw.L,8to3<7l۾Mk_>u^@j"VUV &J+NhoB{ҙ$pk3Ĕ8H܈~(ޣ&'>L,o:,}F].94grMe?|׻o68t@xض)i6jqͺ;h"1RUT"rtIV"AaLsJmL+Ac1NvW :!fX8xhʉh5䬉C8nBY'9r0NY'3g% hҮo|-[Ysէot/pg ]fm4˴;.ѢoĄ뺪rk|H*0ZΚ,+ahAU wZ}~]!AB!X%rzx8҆3Q_~2~}!pPzDo$ OB6޹T3,Š4m%m6+S4sAb =LwʥbȠR-r"O9S9DQ9D"p9Swe|zn #s^Ԧ_3st@xm{VEWߠo&\mtIV"1riLȰ{ U^!3.F'7xl`I'!h39O&9H܆lv?"wsIP |@DmxK8^ l<@ضA"nDݵN\+M ŢZ5LL90IJ[GJ.eV nrlЊ!A%f "`HO>ŁpC4RߓCrn!|4Mfb[ǙO h۶"{՝uݪujv)M e>*IV"t۳)e#JcLЁIaX%1! 0!ȇ!=uC\6!GDHЯn^€8t@xqmBI;$V|.48p]!2M͒e|[V_Z ޠBhazCЋ[ /#B=e9Ө{4R >#EmZ5S˿qt@Xxqm{ϘVwZ~@LTeU+Lw8Q*U"d#zd%}E*lD$L՟kIL[jWnUƾ!Btp )-т , A_ au:xgqF=@_OK)k޲y4 ?W=8a <8ض}oVw~ҹZZi"1QZ%%S|[&RYY6)U4Wd#b J'LY[?T+B A˺l,Dal%rzqBA:<7BI5 'w&?*)%}|ȝGxRv68s`lڧ]JZJ~P2V2f#j֌[8O\SClDDY&&NИ%WRM|~ ,p?H>n?GalD4B2A )SOBgy@G >HҴ 5г_W-U0`b7:.۶'J"iiw.UW,&#rYJ0&3I6zd"ˠéTJʥ o*$0,r98!oB!IF$p*_~&'BqEA"c`ZI_7uݪu4)jlx ζ[wXR띥\RVkW+JxLt)7Tx`K}rAt0gB5qPaΒj5zQyohs?5,DFɶ6Vw&MkT&JPM8T&DTBmPLSBDuTʮJCUx&FC, ,iA.n53!7ĵ5.&9ϨOrp 9k+JFdy@'A_ٖV]d,YN#V*kK5p=_8Z (`l۞(o IDATi6j_>u^@VS8FB*U"AɦzV e Y%J[ur0^(nD?R-r98Spۄ5))ly@T  g >vi/XzNqEA `IL{Ƶ7[PVUXd#N!Ӝcjr4*TԒT"iZ 02s}a٢ ('S9\CΚ9!ٞr0g'9u3"dϳr>~{M,}8 xm{Vw{%uZi"1DɤR~r>-ךQs \H/Ӂ|}ԜOʲ]J%R8`du0!\wM(hр/{F~p0,p՞}Mt ? ?|/>yVm ir9]L,qFF %fRݙ\^׭ZiDbé%%S)66IL<)ͅ?pY+T":|5.ڠ!Eal%DBCa"~r0N>L> s>Hkn"*m׫T0+Am[$}ƴ?;toS}%P}@x@P<'I(KY{jP1կ~pECt 0rLrmC=#`~?rp 8g㭍u|~ ~-IR\VRf#,KldY@A"~Q ;S.T"e :r}9C A!S|}8nH)'SKBӐ)L@+ w.m\%K'qt<iض}oVkoUAqnrq8_ZJg AA):;FX 5K&A7Tx`KhD!ۆ)s9!gm~{M, 5p mwJnZhѷ@!aɍ ;P尃$44PU ~AB/):SVFI~F}è.G!d4xÈj;Z۳]n> ޒJG'rޢi4ʃGBomr-[I!6^.k8:Kl!iI5gry]jڧ]JT+U˥}ћH(ʲ,o*ۜb#;J[jj~VuUp'r)l`9\)amܐn SBO8O8NqPmu|?@#Az顓?̞IP zZR_vJJt8"xm{V|.4Vܦ<$Si%S) ՜(O]qE6"dIKdl^J5k402Ҍ[d]ǠCX8xhʉ؇}rB!I~r0( $\:4¾gmif5hk (0gW5QS~G(&m3kVXFqA^!'m{ͦ1{TUU)VBtZdhMZ&d0P@$ujdXaƲDjl`I 9pr 0KϨF}9Ø/A!TtiW:=|xJOz?7Eױ@!Ҫ+,ל% > ?W=8: (۶'J!iIugryݴIer4Q\VRc$I4\(!aYRnB*/mBCb :\ʣXN> 3S"OB r0輪^'0KI{ٟ%aYcϫáW}C| mZ}.t0]h(۶Jiu/\q:f/`ŢZ>yVmݷKW}ҷ:3h4 ɶ6VkoUA踮ʥ\?=h%CL%@A|0s$1!ltpC 6 8Huo !$`~?Fr8#$By:Af^s&qt@#x{mOC4nDݵN\+M纪TʪU*f}H*L(HJ!pD_-ls/5W lD4Jen}VF_RXo@83SFtir|t 9k\~5%qmG׾GCҞ'F-.?t {h5ng{CtmZ\N68^:Q<=۶/oM{}ڥ4KRVRQ?Eh%,Ku4M9ֶ,+8\d#FPSTc#fς *'S<h!90))))'I[3\?ay {>.ku5=qR qt@xkm/\rXr+ h!aɒ!7[IJ>%F7GJ.و3 esRv5 ,:i *wN7Ȟ >t\>W$65+M,}8k F l۞(i6>[׭ZG@ùZZ*֠|,`IDJs}TB*[3;֐CnC %@al6Q8rBO׀s6\CΚGpy/ > ~ytA=m7Ko;A!:Ysm1>I쥃Ƌزm{E&՜hz'O[[[^I$!ШSjm˲\RCClD@)Ky^?OgxJe{̠C?݂ ,C[ /1'S'S'SOBĥyLǤ?zojP膘+ 9{{xC۔5oJͼ*Υ8ioug>0^Ēmۋ%Ĵ}n]mMIMͧ5)R[SB~dZS<\RuU|@Y&7JXlM)"˃Ҟ' >`TiL,}8k vl۞(i6=_ WȵeMʥ_obU)TT(O-ls_R"x7#S|6G5$$їC! 9!ٞr0k9pYBsK|8#!u580z!2:xxyPzRףRǷ]#`~x^{a8^:`<۶7HZdR͙\^7}R\=7vs AӾޒUxH$-59F3JM >,)ך`/u 1Gש8Et 0r}2c+!BzaD6Q8r31_CuClc,_ >sE7|.z1-_jB!7|y5gr5'|`w.Uon8|`<۶Kiu/\q:f/z鄥Ss:5=)Wk:TE6b)Ra#˒I%vŒ!їgCn8xgLq0,Kr՞r0Nj>_/=$ybLiR7C |ȶ% >w.m\%K_8Z:`,< ۶'J+ͤg\{߮֔ԟo߼SP\ Ҙ/ZQСAzᤒ"r0$ rap|!PC4I~,F~H06@!t zk0slx8^:`< ۶7HZdRIjѷ+kzmMI]Ѣt2aTo՚TpH˵fܒf#Pšc74PU$AB/)g)c+#B]~2^6אBgn#^g 8ϰ&J/Ыc:~*u=Bir9]L+{8" l^,'}ݪeO˵L ;Cq`YnR2`30Lw&0>Z8rrq8qC%Lq}? 9OB4}7e KIKm35|/ >zk㪥*L+}8k l۞(i6\rXr/J',]=EmM){][*k|DV*T[{`;S _!S<[DC=,B!I~r0!gͣ% 9OS}J]G|8Sfb{Y8(u=*uoէotieItg/0RDm$-2K[})ͺ=~ W[|DZ54qc?;4tO.Z|F_NDC?4B!I~@.C!T{uCt 9kgB~RSaP :Dj/ >gܮm[L+{8" l^,'սh>R_5)KF/?6IL U/K$-5qf NЯn^€ipg-Q<۶'J!iIuw Kui[{PqS ,҄&B8J#{i%0֡g`<FbrOm˙˞1~2~@.C!T{F~r۲ibc-{GB/eM9_5[gN׋ڴk'qtiV?n۶JQ5OV M ^~j),L(ۜb3pJnՑaU*ý|R$a/k* Gt`C؊ ٖ5~>~r0gIIIߟ> > Z:b/Uj޲|kO?lZ[ǙOVĶ6Ts&ע땟|1 ;1K904oJMB6޹T3st)% *l۞(ii&սp}꘽-:53qh|f_Oajɶ$mnR&k(ĴvN:P9!Gt`C؊ ٖpe8Ozm,{I!>8Lu^Q97~UTS~ǜiL+O҅>5-QaZI_7p\ꎼ&DOrMO95*{S>v8\YOCnB!I~r0Qpa&h~hjc!ҪyV%˹=R,ajiotg1]o <۶KlR͙\^7}R\k ׏CA~MR*i.߽KJiLkRw\ &@SLR %S|0;n;xò|J DBMRϠ!I$`~?r%9jrLru? > A{4d/P)Vr7О=iT,eU5qCJL{7:n8k]fTcOam_9p/&OQnj >SL|@YC%6AIKɔTog"Cl!Qu2~>~F=0pm? 8O$HҐ cY>:.M}6oީm^?e`&=>I:s8/mnI0p0{j:ڲ>rMO9د^۩^St?WS w>D__Q 1ɔu +z{a뽹EםJetsuy{LRM)eSlF& IDATD5W%cp,p@UՊYeЁIa+2d[$_CAxkݐ^C(I~x%x/ 9Ϗz)!(s!{iG6} >d[Zk5_`^}u}vG]Ujio8[2ٶC,j^>u^x MjZ__ r0LqG,v?My>F7{F\66|-?%y2$ٳ_6NbylJ 4a/pg #ٶEgLy1{Aj֖џNE5=ȸyݷ뭁>31c7>HZjʥ&2rM}EU*5iBJGxaxЁIa*2[$_I5v9B'OB'k ϩsy-HxTz{]/M 7ԶmkzȴqN^;x#8mELc|-\qJ',]{Q}Cz8upTFs)S#S8D[jx0| ::aCC0=c$W 8RBO'I^ᨋ|7ՐK =W:gf[Z5oJ]dy9Ųyd<ܐrsjv3mqn^G"(mOWR)5gryݴIero&}rrSIZ{U5npB>6kλ< ,R9lsJ44RU#MuH$dC4_Q-DGA8gLq0'!/n?[n?oܹΙ-usTJOBߟn#^g 9OBpQ09?ϯsbCJ4q\-{4EOoQXnmAӶ qvzFm{E&ռp}꘽ EqC;tSݯ6yf~s< tIKM2٤)>jBYb%6x!eɲ8qQ\ GX?Et r0'!/ݭrz #}N;yu~ 0\Þ7,!r";﻽/qƯF7{թ8R>z<{n]󖭌}v'kmCmVqưm{&1{?u~=˧4G W[ Y孿P_ρEצh$7kECС@n!#T^۶xAeJ55w2idՔ #7O|'"1Rx B!I&M Nme"%:hNtn@PR8U':*9HX_uRrX$}(E,ttxbf}O]G%hϓYFXr8me"H̺:PtֺL-k+ކi׬|ى)20ãE6fbdHl^BH@˲zR]s{\g J4髍 B!41LW`tѼwuc/ݠ@N!)h?SNuA휝d8m(94l(977/sk'I\(9H3oYJIɁJgCQp.EDץjmGݔn> ̼vڲe/zj}(cb?նU!$>Px HeYVd*W#jUr0"FK[y@!BH4">$X ((Q,AqR  h蘡w ޴$ͯ(8'~WCMJ63Ю) iPpf(8ggV_K(>(^jΰ3 l1]Cf7͙ 'fFry[^̩szlg!!Dj,Z>j=kB5\{a鄴5:e.`BƠ@!BH8$FBC2i@ӣ}q((FB?!8hlAj='(9Yds2"g%V<ϨKS0q%^.Ǥ,v-JRYJ.Vܬ|q\|x(Xat_/+"",b }6/_GIV੧cǎCڋ ;QmmDH<@˲zlU殹}=߉LSۛ0 !+v-ca<9nid B!?tCC"a0?doJtJERnY(8rJ|*읳 ;94lA ֫ڶMD i,k-ԛLep[j$S5Lkod(Cn\e K;n`co_%EM 1B!2DBnРt]ޥ\rNE- ;8 Hh45/ArPgo5K`8,p_+";x=.꟟ϓYCî)>D3Z@n:?(":zۉp:^~tOhL=}t- G{(ڶz1IѶ^~F}(<BIJeY#\&eLthk2БNԼ"7z0X(T(2 #߬b/]a9B!R%8B7*]!0ބ(9.3ա@$A>с[Q`(xtrPЊupєNL1o'Z^k~I!:YR~E!bc ?x?7SOE8T3W=4[_gtPh6zjtÉNm^BH@˲UsqW"FXK?_Rz-3,Bs"C!B!xHn Ag7VvKt`': |vA! ,=kv W%g <-[⩧KAzNmBHd@˲VKz tNfxdphPkvN!B!&(8(p nC- ;,J%~5- vhkk;6l؀\.XZ߃.R<)9~ ;B]B8KWXGitĿɐd5:gHzOrMvi7å8nr?3Ix GGѶGЋ9NumY(<B²>kTyޝbƍbxetx/=D5`:R thk2R.sw\.\B![4=A7(8(p߫/:PpP6jPy~9o!_Ʋe˰du{UrJlܸџ23oyB6eD(HBkovqvZ?.^0F #ߐc~MҎgiL .7hh*c$sѺ',bJB" BTX@2Nqm݈}[72K/%wބIfFKy9p\^B!h3^p#ee$GgEyP;|/!By^r@Q< ca āgς b yvڵXb֭[Wu΁Azs얒"yRr൷rf)>Dpli$8deakt+nr?.}OPpcpРdnJ%ضB B4X}*|wDs2Ë<3=W.9ݑJ`zG3:҉]~G \B!$2:`$t(8)9|~~$ZACXJ0o~Px}dY\K,+Wbr~ ewȕg%,%kozW(>Dh?m*]G;nQ7MqaGq !Br c4sB0)w{с] )8FA<)8s 8C5bXv-|+kpp}}}ظqc;13o`f˓P}#QpeC?O .P9|y,CE%OcUxρm?nG? 4g&ԵzjҲvje/m{-!!D ,Z `*w]Q3b_5\z|dU_Sp麺: }ȍ !B(8%.02\[;guv^FPЊup t{V׼]w݅+W6e˖?q=6g%s얒"yRr൷rf=G**|ƽkmS3LvK̎EXtOnOû{}+Ki@>ϑ?A2]ڶBH@ ˲gj^0R*i3~u,d;'rnwe";AB!Ȉ>Nn04sB)(؊| 윝Pyc'd'Ut5yFc+=7HOjlB wMA<)9'%Gt8C@/+u{%Yk.*:xziLd/c`Sx!O8u(;^̡m *}m+@ !bYV=ܻnYz7Ë9n۷}҅r؊0w67|h =L!BBHh'I͗Q.=s,EK%r`(9'%@ry`՚xbZ*@,%~B5&͔Gp8+:fT:>">3>mB`10rB!@߽Hn ¯(9@Cr`چV<Ϩwq4?jtwBM{={6֮]@7niں<|ɒylT}}W;q?;V>/ADн] ;:wv87yg}͖-{Su&CT*ym} D а,jkU ;  ('6d;/@:ێt[{S;]٦s.~@B!(88WW؈⌿)P7j%Prx òyC!bڵ hك^r9UwysCYRrP?OJ^(CHCa⃈V0ׁܞچ?dG&{mC|;Iq@/pU+V۶W<!aYU |^GaWco!BH]hɂnPp ѧ8ZP!N}IA<)9./_{񾡙~3s9 jBuko[PnB}01Y5֘]|mSy T~&ͫ~ Á"e]OW_Wj=gNᣟ['YdoshS۶AQ P,kT7HZ!>2w6>nq2B! $ΔK|nCE>dYvN! %7 IA< 瞷={66lz}}}Xnϓy_s3D(@xݭYOА[03URDxYg=҃'ᡁyx c0S?F,]jݽFr3o 7@|08B|&c!B9񂃑Рo.ZB ! %7vrP?OvrP&O!v/ƪUI.'~'^n)9('%^{+wn] IDATS>%0M|4 =d?T钐Sv]}?㗟g(zSt; z֗i&σ1ӈ3Ϡy@)cm۽ ( BHX`*&S,G񑰻;.B!F7raPp d#P)uk>rCRDp(9$sFA<)9(yݻwGj?.#2ykɓy ט4SJAC:fTć(>z1Ͼ 3VT'ۀ B^|p? l_#҃VE۶A/T*۶AQ}!$(,j@*5ϻ^̸S &qTu&χKJgՑ|S_ /"$sѺ'*c !Aep2NGH4&RCB!QdLl(8rz.Bm-"!:A+Ȳsvq+C ]ϓ]sB}{~yk֬A__t{~%8nS.NNjEr1pćiH *]wW~v10i<ʫSofIc$:o\{w/7}|W@quOmW۶ @ ˲zVh#:MZ~B!wo08ꢖ.9("8P9~ CO,!O={66l eƒ8F}ZIA?_ 4$su>YCKzW:E c69_񱇡](2Rx BQMV,BjA@!bd _Nz)BşiV8j8=*)C9P?PpPO)g%/9āu5 djO *OUJ}T{p Λly*u-ӴSrPER.{P]S0Z@s&GMC=J?704C=`O`#h<8.J,k!J !8 aYV U#W1b |Vo< ؇T~0毀}no еPD*0Fy#09)n2k>ZT)9 B! ò^Ro2mL2s(8(r~;7)zUtf I`-o,:'+ osPvez{kc ϐ"'%1iz bΟ ó!l4gL|x0ӈXiW~"=Tz3Ƚ#E_ UPt8j3ܽkwslh:Y-Za B!w,jb M0;.C!R.P.98.2rH0MN!$:Д2 'u8!1^n AqԅSp @<86yF(8H7gfK,QzNb>Asy*9xlRvlh.c N|k*&tP$"?WQEnjp P&>S0'_ u EHC+T)yeY+mBPx 4ۻnd:'35B$7ZBYːvx *"J%nirA\LBu#!$6I=Kp'11 *$DrI0V-9I': uv.ae(yy=oݍeڍ]qnRrP?OJg'A pw= փ/V~ųt|W| 3 \ `ͽ~[/8Ar E%`Cq7?_AF3,BfߓE;/!DZ(<B|Ų^_RL$Yz7#$`̒@x;.;<b­q N#aHv}ZtMƄ3 \dbaT:7Pp DkC8)Vwm(.v 1y~SHVۡW=oP@6%O!qmDA,})ZGUr8 |V3`P0 MG7ڧJɋ-m{-!R'!~ӯR゚95%:v-0 ­ 5(;(;Eh]78Ҝ2Hjt.EBT(4FtCȜQrP?OvrP?ϨK5A*w`[aRrPd'󌃬KQ|<6R@V iN|^cՈP?] d0FKuOIB#,: hgAk81IAJ^ )@ ˲XJ]s`Myc&],2 R e5[*:H6[ !l);B"[('E?hK!by^p@Q< GA< 3S,O V܌zTbCpnq G6Al{Ȧ`5?нL]Dt~"AHuoIbU&\c5mn~Ƈ8&~N-Za BCBp!ucYV=*ԛLep[j$S 67FQAH0LLFBCKl!&@qԅSpPEcU;g2`A LLH]uBA TFt8ezw6C{ w@}Fx K'L7ːmWŖeٶi@*{͝21Bdq^>8]iF0O&v%N>Q!R׫(BHд@!_@qԅStDύ ̅yP;g2`A{5 qMf#}WmY ۶W"!5cYV~UtLœr7#DBƤk/L#e6Fz@w˜B¸QNH$t$LpJ%%ET*-S3u)9ő2ܳ4upS5My<$vqmhz)'8(ij;]=UgpUhsɓ'dKQPD4zJsB|8 bCG639ރUtU,*?вӀ) >kGw@}h<4݄DP2Z,,߶ABB!uݪ{gE2hu s'0myoFng{ڍߍqҀ4`&u;t8Yq((YUA@Q,PqO؋"̓ڹ{ $ }~1pV;<*<)X5%H"^u=.^: 3}[a3 f8mռeG_P|i]7B z.xuyEU͢à:!5aYVw\t][H%0wrnNΣE~xK!$4Ml d 4Zh$J P)T}.5N!D) G]83[tģCQrP?OJIA<-g]y E֚4%P捒)9}T%xCYΟ\v'pi:>(o*1Z(gD 's}ctcj4W,ZiZޣ.!үR~&FBΗbRDw[SLJh {s ḂI!ɒCSš%m%m;pFg`$(<B@)p ._с@ARǰϓC4zkjvIPjp~Cg)?뉈F/"e&BK; Į'pKI|2m0}@nϸ?Z9̴q@7akτNr;=.|4\P~_%$\(<Bx}C5-x쪈M]a<[m.hE{Rg[̶핼%$<(<BjAL$̸SLq\K +4]p@!$Rn #+>!1ђJP|8gBKQ%:Pr^8HX]ϓ]S(RzބCyRrPzjE/"aMxF5j}k,;0遢Cc*:(Ev m\{w0b&<@}n9O-Ze i  OX@*ο~$S B" L6!̥Rqѡ绯C'Jg;Pm.˃{En-?[ttrAW㳳 IDAT5!CReKztNfpB0( 7  XҒ6l204X@rqBH2ӼGWtP[ @5 NA QrP!q>L!:ޔ"rGUɁP)ΟC N|p'ogn:>mOE OvrP?Ovr7=o+@J8?)9D}lF/"%٧$4aߚе09zsEp޶Jz>ώݫ8y`SR>1; ԥ@fJS+|su}ܻX Ph`S~ >%B9'e-@z}?C#v3 X_iZEv4N,QMӐ@\fPHbwn?< #eӿm~q_@A<KA<)8D#Ovq5Ox^ş9O g.^{B!%ڏKЎ?@w4^|/?80@nj`L}XsmǤn}ʲCLth&hmjƋ'QcNAOYRneY+l^BH`iBY, 2]s`MBL6/=hd4]5־9m%SҩwBH!$ x\~?rLA (9x/]$\,u KA<)9'8D$jIA<)9x(}T%CDY0h=_^y=+"(:9~#:(AnPkjכx5PK=kat,rJ-1y5֫Rr˲m=*!@rg*Lep͝21B!dLz(9E׭iD ;;EH6%j5a$(x7uLhoP.  BSpӊV6qh` ]tJ.: }ϓC4MYvL41Xk >qIW.%զDX`᭕ ]} ;^k Oqw /Qt}!; "=~[J6 {5MG7A++> 2e !gIJT%O#9B,ljFTBTD⃦H$`$LN Q&5#5! 3ip2Jn!q8Ӌ܂}ط7@2"\V<Ϩ S0Ϩ 5AA]n.E/"עJӡT`þ_L0ӍQE*_W1g;!ۦA :<_N'L^zۨ4MЧ\.a4cx"d=JwYҶk&Px ؈ғ阄9KfbBbAE`H-!\Bz嗦H𒟨ihɘhIS?fC$`61wB2N`+~<S/%%aAHOvrP?OvrP?{7nAʒw)9~ Օ7-)8fJ<%$*>: V!?-5WÉ޿6#6A=[e"F;FzUJ^aY*۶Ai!òef+qm4ᢏ~H:ILjM \Gcs1˟E!AK&T]7udM"}łBH KtxgþMi|15@ARǰϓyRpP?O~]5Hڒg)8n`ZыthςqqOv`̀wtzhbGϻ Kt ZvZNWhjғ᷽@[tt `9&qPx eYmPh<\6ّ32uLlBW'jbfΣE8Z@!zБ6!a꜌ijIX,8VnY\0i!q.at]<x6Ϣ%ʈsQZ<.9xN<.9<%U|l6ߞ={˓'%ruҔhug8o ߠCǁO3=I/҃ʲCPt>tuoMěz#G#S!IUJ^nYJ۶(<1:{?W^9J|S&`\tQ'y;qyg/2 w6-p &BD44M2&'#D2I(cqEK]!g\l~q_GaXcIO6+EWbJK`' KòyYC4Fr3i^$<)$I)^D4J*Miև 8W}k=`Wpx o7 ]gS̆ ɓ'%rP)r /^EaƉ;W'@BvN`ۿ=Ou݅*:\ZbV;tuh Ux8vb(v'ӍBL4ݬJ+;!˲ܥJC]|ߟmٲ\{ae1M8<\fB!ԛMvuTDaā[? Q&$8 voo[ˑ{?mNWmЙmOPy~ϓyRrA0%MJjE/"עJ!u'fevn_5X<ʃ/ @qE9.kd{I|Z|a~3BSoB1%]0+UJ^`%  X 5.`_xa+n*S:R Lko ek/L;eB}ɮJa& $JJ!8!1\N% ?6@f2*}ɁCXJ0oϓC4zkp]SpZ-m<(82g|>XVH!Z&¸Cw?B1ǻ=|aRt>V`!pb;ј?xJ#@%r2z1Bݖem{  ˲ڠPw) 3kܹ3pcN:w4ã BH,i$ʰjd&$1xx$8/B"L(P,pCT ]&^82> v΢.9T5yF]r4%i~cppF)+(92o"p.Q^P\*M;99ς1{pzJ_LDۃޑݑG^\9UtBd:ZJs}50u &xDr[o #(<BVP'ӅBs׿ހ;XdcFG*tkSۓq?.BMeBG:ۄɮ*b$t$(J>NJD!PPto-}m\WdhM$;9ȶ$Pyc'd' zk еqFo_"ʼ v:~#"=جCCc/\}^wƈ;[[0iVYv; ާ8 -顖.NeH涫Pn@?eL@H9a*Xzؿ]ɐcD0 [M!XѠ9}HfYGS BT\(yz?@9`޷eɠ}X IA<)8D#OI87=oӣd-{ pnC%+]>/3vq44>  c8yw+$P_]E'Aph3{ OYwR?7؄Sh 'S}#^.1"ewwcj{!HC0 cu .%˗e(;ł _rOYڵ1NtEvHz8Θo{|/0yF]r pYXb?J!CၐbYV/7>ivyZR2B!9e"=!ɉZiCB,- 䏕Д205O@qEq!jǫ r_ī]UPCg˷=!bαyyRrP?O:k5TZ:I xwgMު8&qt^s*>۶29B!_BG;MzoD.3vy 5tCC&3ip2"Hɀi"054 h,;Nґ~?lf":L]|iL7Jad(ɜQrP?OJɟ83A9K!j𠙙h">[O|\!k}@x  8r-{nQtc\og@xmě0#IUJ^ R!ePpY5mvxӦMf#%׃ \!D攉TƄfőReB/ 8QW71rOYڵB=%`چV<ϨwqXVQu֔hC3c_@\np'o;!WKZUb3ު:v(> MG7ڧBݖe-m{%#v(<O׬H!.U=mMׄBiHghjcH6OxNb@ӹ +]8E?xn_`0|bӓNs %>}/y]ϓ]'%7e,ܦߥ,K!3)tt5#ԔPmdShj'$ͮ8~S_+ S"2BEiȷ&L䠂j^Os\^镡e\KzlR?ڡ?S^2p8=tt9]7ymt7>wQf<"!kvʃ[Qm^'fYnE{ GK6 xBD.]JESs+W.BssDsʷ`BNSsMR LSv(T&Mid< P-+U'5upl6X@ uq!z-דA~-"pH0tC]R`O\+tqH\CLI硚&} u0j( ; B *ȹT9Z mA|(t[åTՐ-Gohtt4L,3^VXMje nrO !5\IH؆In |ᡇ BCSk:Ma & 53ZRHg(| ӱTSVjUNN u_ 0h"^OEXOEAz*Y$p)Y׸;0 ~Q`Ȁ (=1)r0pNs 9/;䜛tuMpr'`;{ {pPp3V -FJ#Za v/HCNE3݂: vb1>S@H2r}<iF! w)?rʉ@!2;l?&NV(0QA9! =;@8VCqj9<7wa~{δX~ IWJiDp=Ia:  !{0̤(v +e,(ŕ23Q8V3ZRHg,&$PU")>^fp f;`=A$"HrFz*!s-S kwAVT~87Za (pwj\X.2p.JJ{TQ R~֭sP#k8x[CϓB:'q9p0c(tlLkeKF摲 P{tގjRdHwS.<00O넸8_1$':WCmG?:OzgAh)xk, !1X-LJ:]\lx 錅lS & H2pjZՁӣ3o1< 롯M~ 08Df]:r-]']ד.I2vrS < zeNtr0Cls λ طL"7 G5[l{<0](t &V\lOVt́FR6KZ[jKR).ނwVJO:!C!6¿ڱՎeq0?+qrąNBwJq6(5[fא2M)dRR 1]ue? T`@FCd]S}=)r_Ǫ'0|>]]]m=9Ԇ~X}]B͙˿{)5>k5z{GN>岕KHWeWL/%e񩝐/L!F/Fسq@~bAo>?VeA!m~@H9 T@*5霑mJOVJbտ ?w?]"I.րz`̛r` 6 C ' EА2״:`Ȁ (2 Rb^/+q)3e_?t{8߂oS[l{0I A{G\/::/ =h_>ENliN*%zF4B\..4pYd 1\.׏ILnߝ;ӟbgF+X{'F,!ij ۔f"Ȍ ?pp(~ qڵ%K@َIÊOG  8]"-n)pо8ȯ'IC4_yIеj)gZ)r0dK .碤th-rP :ƌxf^vXOLgwO{2: vJ+QWK_u^k)(4Ρ' ^mDǙj'&_jMBꞗ!Ƒ庤\Nsca?~Q9p&_FBm^I PE>9ű y"c:CVL&YqrɁ]M:BPi?1]zșj8*Vx=cImĝJ`=UaGSjHdcYw=ԏs-xrcUG +Tk!ij P VL]^~ QVL8(0>j4.K0@ o"5S .ȡ@z.rpU"5r A&s6G9>>5wji,r9 ze^"P 0I<wKpv3_ ZvI7úa~stJ83>Æ~X넬܉\C\K _BJւv9f'ĞK1# I) ,$!jRT:o5|={q2udqs{.݁B ZHNB6J[Hg&,odRP2gL!PÒͰ*ԁDN .WP7,H6JyiwB̳XTO%`جeB=y`FGGu0sM=vWsQR:,T 8Jmu)P ׿ ;SR9)vѕֲO98:xRG4گjFWmH Wٗ#3QǼp2mՎe(wAa nrO Sz kn@Gwu3AXI-dL!B 5D*3)Ҹ* f\5*J TIO!rPYlu-J\;TBZ]PU=k,⮟{n;e;Rq5>+=E"!E|/P; LӤ:C4@б⇓#tC5_ˡ.lFiHs"x&}nS1N"(VLG"zO'Os]#tܴV,Zy'Zۙ8BH458TuK-UD*M''@p=~ֺ/xڕ.YltKIsP<;ܼNn=)rp>*2 ҡA%3ـzeۑ?sD0m=)zL{㞶qx{?6g]+'V56:4Я m\}]4/7r>NS'Jhp7rty dfڅxRJGghh{CYdZ$.K2x{/q8m,k ޵I$DJs+I}jUIsӱusjL K@C_V|> ?2"EN "MyŁtBPz8$b.R{BI} S.oկ myBjJCJtt]GY+\˰2_ sZV@v!rfDD6i.={`ժ2D ?zwچΖ/wG8*~ӓ8zד..7LZz35N EE:9սOww4gJ?G;B:4tPm/-KaAydo }*_CT[q_$,=600$!@!/oν>dŪ;͸@6 NBfƔw`ە`h^Xi ZۙdBHMi&4 zS. !F SN'W%ϺI938Ĝ3S g~bA~=)r_98L-ү|:3m&1iNS B;G_}/}_#X@7.Yltk@=M9,"#c%LP&3"q);uԣ9 ze^ZCY)QL;!]%ܴΡ?pzhHpegW8pcH5 t2Վev,Evpr|'%) D6\1$ZXEL^w܋}c`!?Qc"<<mܴh :{zYBH8tx PP@aJ@09J[\^XA~ ˜F!rד".Az,BjߺxrxhX4!E ؀+C()T/Ù!kr?_Swu5Zts8;zía-~ȴR3w2nإyOcl! j6c4[J6|'dx!D>".jՎt.&CCسg! îTptg#;Lq!VbH EDBvC o!?ka;~fGMWdC)gue@=c ?N]HZ։-#SM!h֚#jGu=tep>}m Hɮ+Ci*EJV\ʹ()SD>3+/OZ"ջ8DU n/.pщ2@am2euUP'/S*o߽fi#X?4 #p,Բ]\u^H w[.3'3%CC)Q\~9%KzdI'!cW*8-?l[AL hiogQ!bY;F<B%9kC_{ocrq]̨|EoA,S kAG'82`JODE8:Aph(ێ/1jC{_["ꗁ_L-ݰn ҭ=OB*r[~W9CoQ4 %{曌sAa v1B@TIܽՎe"sP.W91sƾgw̑ñx,B t~ I }?_mX}֊Oνь/6,H|o&F)wBVQ .SPuqS%!&͋m`rJ wЦ.|M0WyN ,vRkXds{;tZYX|X0n<Wuevto u+u+.ODŽ=q[Ϡce`7uwEr:%yk.!B *>)>"6|;wl#@JG^݅wBp34BJp<&~ kf;̺LXSD"jRaJ$,AUSKCy eZ3::~L;Er *0Eǥy9]ωⴙ,P$ZK`Сw@ہ$tP>vYV{R[oit ty D<"(gDsbGA1 BP.PJ1y婯c 롯 sqEOlMkqo r9gյ>R9<0(ټ!5d^2h\QKeTMv_jp\9h㲺'z:"еjkr7Wj=*t Sq[Wk];%nAc97{ʤ [Qvt&z"'AH¡a\QmrS-F\bν(uBL!$QԹw/L!$po7v6m"rR)M1+rh0gN,E:9(9*Ԑc9h^OC%cG8<̘P#82BEcAKs.ωRفcPzr`:$ՁBYBqq[۩^mAc%l l'V aS. B ȣ_BNS/ $D8G$B!ӨE1g潁lR}hꀌEYlAw Aw.S/ZYV Da|8(Ѥ%{KeQi]ҼQnvt5?զ(ނҼQi]e X^jR)$ S@$;&v7Naɒ|C8! sgQ.kc{ 5A:C;޽@3BH(@)ߖ\.y```FW"[ܽ"ܹyFBrYc켹"pRBH} 9YZyXKd4^ c|mbn%_iڈ; U49T;IryΊB!Br|JB[ǎ{P.MHc{ZǗ4KX(BH(T+@nw!$`gS_@=~!Qx858(O_X— pTCQش 5ZЈA@=8@Өm^rA96J/K8EqQay@`Cɚ7!?n醵&}>LK`^܉nh^\4J蠼!Bk#b;}ٽ93/}DJrcI"<"~ AV;ڱbرAL 1.*9AwBH*z!~Ct(9")CqT䐴sqY#fͰV_nL1/B f> p:Q^#%~V$ wMb-6&.g#BP.k_&ӄE+d!QԘr 1;BeO̡ǰωu}+w>R\\;z8U@W晏\o˃NL/%~V$ IGY;bx8J̢Uq%L!$t%.p're&(|P.y۹XkY]B~9/] t^D[jc8~SA 7aש!]3q.s6.xصk}^P`̀!^ZC9||1.81Ko@]J67nFP>C )GB4Bg k4u&H.\\l%fX^& |?RцSSL!$0zJQ[c.$ؒ$r%E2loj-'/Y):Ӌ@ry()%5"n7V-5ytyO1QJ@hwe?ҤQ깏ٳx}L!ٳPz~˯o BJFv2)&'ľ˝(rkEKdtLP=)rAB=)rG[qX7rpaXP(rECY-ǰˑj67/Btt@gu˃˃3xty D3(x D_%Ymu)ǘB4d]}aց;܄5 {W2 IxDB奿}4k`KN2rI'4]P9'EEP@Ľ; [`A53e,zi8e@` y84սq;ݥaС5ay[ym;.ւxP@hw=`3ěof"ьv,Z:>:q'M-lT+5&"!K6J&(_zνZ,id_ͮ1Yj#ԘEds{!`bb*ԐiP`IQ䠠f 9]\zQznˣlJB$\Tj3֛(pHcOxSH?05,( d#qnwx@i4-T:oGcP@I ܹg3h;עsAL:` !ˍ! U2A x婯{9ہԃ_uuqqEAn?!D\OcA=\CׁX\hn!ȅcaLtq$)CqT䐴sq 9k =2?WC\)t9F]% f/W#%9#@B4CC{- V;`x8D&q߃dBiBBJ EF׸A }O?{}X}E@lR 9ȯ'Efԓ"!6?x oLC t>XqEU/!.Qi7uOȁq$IpHy߿p9Q \(t%T@{*;ּ+1rbPXFB4B4nX ` P.Whƚ Tprٴk6mЁ"xva" dJ젔b2!__R;& &+zi?t9QV 4;PL ;I⇫`Ϳ N?F(teAU0t% (g\.NJS.:TiMc)*Z R.Wc|2!:Ս[~=dpr{W[J)e(MbBHQ.y׺7pqB.1  s.JPOm\5*1fԐ.BIpou@ETrY9 h-ru$|[[A4($^1R}}V߾z7wyV7DkX7 T y#giZ;<45v?vk/k$ #B!pAP)sI>_wB4k6m{Ds[_e2MXzGq}$ŶUSI!$g8?zZ{gcD:dΒ$ru"X 9ԩ!EBIF!6?D\a,^9"i EoCBpFZE 歋q6{:"O2:q nՖs1>D1)n1 1ՎeF)z DSzVO<;܄EVӋEV7G',k2MML!*%qޣHkTۆR+'t!<г#yon8H08MR\<C0Ôӟބ,Bfܴx nZ $x鴅V>&_":!!rk8s5OZw&1}4+Ȝ)c+TBZ]0Bl2J<Ӫ9q#V:%ɱ OtP%3ِz~h~zpOlO@y`L9:xRG4گf*]5۪0њjg<K14\.?00pU#&AB@C{ NBѕ|DB9 c2y婯{۱Xk>ntqоN'kH!z*׻0kh#cA2e, y:vqupf21$r|ȴynBwtڏb X t߾]; NSEb4(x $f$;saB!xR(F/PH2!E7^ ~L "eAz֩!EBIz^չu fL\P2 11KtL8X:N<͂ ,Sc?m=x9:k)QEc+\vӞU0iu56] R}Pf nr}1 1NS' CCxL!B8VX (&CWXSc!a=<P1}kAi;5R4 ;[u]lZR0 ,Tt]Ьa= 9(ykd^WMyCrY) .D࠹A+|PBsIʠ!sXZP?e3x*(t̿xJK赟VOqx< H`~)b$(x $F@x{nA!l\@v J(W BH$rwXynnt%XO]}'쿺cImwBqv{W5vIA)ty $2L! Aoq CC79+~,BDrUTgľgw̑(/ؾװrB!l^(a,_]u +4\BiB!$bo .Rq wd3#SU!3GִTS?BO y5$]PvmÁxs6ky:,?f ↯Q?F{>C}7E^`b8{sf fLcmFϵ~\6A\&yk8BܽVʋr>VHBAJZ#I`W*ϸ:pX/}B*%%/P.P8.1]`ȁP5W`:H!|HK@[HgsB\Fjw/#qݺ:459)6J "ҡi =<myuJBm;ϗ12TD~dB R1/cd2*eI!h?wAii0X A -Ay= 5^%JGC9 M4KTaut݃.^#9s~sGI#p0-y(neиx7i\i gڐZIoWQW`. \41z=c CBB!FS?p^ #8뇾\fcptNB!D vX%\( ?2 UT+5T+D;AlҤUCɦ!D;>z!"[[d5^>DM=h.ڜ#tq G7sY*r7w9HJ"@<E _S!a'Ic >p[](t>~Sgnsq>z] ;|;=)vbpANB!D8vb .LF8PIGUǫ(OبVj8DT+5 U˸4\N9B}㭇) >hOܮ7f e>j၂!ѐa ;7E*]_ߌxxPS9r7-ZnNB!@SFКI) )B:;Og,Rտ[)+q(G8BVU85Ȅ=A\W@7%ȻyՀ8liꩄ̵L2״JT Ėpeupׯ9{H5eR[:tlHL4Y_p~ ;3Z!}栢) |ƭ¨r c#Q˃9XO1gEBcňD(x $";8MlC\ŷ">-%S'plky~ٴBI0soJ[H?02M mov8%`NM3 !&/ i@(r/XXOr(r_Oڋi _m9 T q)ɾ!xpN0biL5!Ä"D_ڇyr|:S>2.V \A5:\Sy}#A£ڱ Վty $k9S@H$݁sB=`YB!(pxxbx>yu& &_ʅ1YnnA]~i0Xa'r'ZR s4k]#js-cQH=sڟ;tI^:WgttT{M6wNr+P4(&鰠hr%~*i)(Mʠ!|uf28;Ьy}:r $<\5bD<>tw 144~w7󁶫؁B!bv3Gτ}8w3_C~=;.V=1N7%RdL9y=M9ԩ!EBIz]qy^ڕ^\4e,zi8e@` y89ڵ~p'k1Y,:8:R`iB??<< BBBB$ F D`W*yBB!Bds9r] I[W߽A 9"i 硬k*$M=$r;YEPycYƺ{ J蠂n:}|(s]* ]   ;Hٹs/^~:X\B!B/v\%c{_]u8&trg:93.ч?=<}aBř/|˜%&ݜF7/#Cov][Bx],NVN*r0o fR|-0Bug< [nՆ7qJOC>Z`=u :3]b@]}N=mb!H0qukG^2{ּ7w(.D<W'fhh֋8{vxƿOl8:(x B!Dw݃wb4?~֚d:2&o!XDZQ}k#oKwv-nh7); p[\[ ~pW6ѡ8**]]]رc:;;㝿C:9tN tri%q*N'Ԯ wrP"׽EA]4) m<ġf1۱s]<  OtwH6(ɓ…ֱ._w2MM,!B!D~xW󴟵WĥCdFܩ1t9DPO#c*ruAH=)r_&|׬ʟ_ m֭ؾ}{ι>ڵ ˖ d8(Ïǥy: 9&pH<-pPyH  nF.Cw|(_j0~wua:x:p%s ]݌CM{wϩ)41?]CEtB"mN8\,UC:5Z.֥wMhB!hʹo1 Bű=ϻuоglDFĝ!rhpC"PrU!Ҡ%J!ˊsAKC9#C$q׿T ߴi|Iׯǁn} QLc,q!'\ĸD _{sxI!|3|F4R˼9۪?_B`t$DT01%f:LK܇2r $:?gajN,Q@!B ÃV 'D~н;ՐCJy!rh͎tBH=Zɪ89^Y'I;7󾝝x';L'-zp{8(dx1.8:ApL. pNlx]L0WaLi r+\uGخpm.}=D ]$.]ljcbi;B!"Ki ~~__\w4_DCDvqмZAUAi65KBEry L ǝ~)TA:cUp*]AxȔ2 V̪8(>_e=\G'#]w2B\laώ˷UǬ}HQCa-?C@HAn ;rYK猍Mbzz6գه(v BJ֭#!RqTH0Br'p;v7ddg+qw~+$6YH܅"5ՔUk !q$k҅]񇻄Z~3ߵu^$AGGGY;::҂'O@ym̵,ánE (%9^MSk_=枾߷vҕ3@z woƸϽpf-'* rNCLVxWyLnj;fHqf;raffNåKIڤ؁B) ww -)vUa?ԅ<'G+od L^A-ml% ەoU S*hͲ"dk|L:h,hD"7k 3YiŷEᐶCK2JWrPyܕ8d%+9<kfU\`*ʢhkv,_!vhuWF{A"UYا]vY݁\3Er9v(v BϪ*?~~ Y_6pU؏q0`BsӶLUzd< a)r^P+MW/lo> wrf bM2'\[+UZd]Dj(E"|(B9\4(m!B{7p"Qx\ ;8HN4|^$Ljػws/ڴDVqp&vw/ܗtUk;ARj<>=7wF[֧鱥]XqbBnhuٳC[4 ya;,3T00EJ أ @Df/YWDoe6MgΜȘ(v BOC${U9^U@C=RD~یGv>D]%92 gNZ>O˖oʕC ՅBҊnqUX6I@IN lvDQ.~D"6y@j˂2JWrrơUr_J^_W{YaU IDATyCB׼Wu(\P;0= 1= ` H`j;EJ Xju6휩`5}I122Sav6;B!gmuQ<ߜ!k 2!euos1QW9u%[iÖ a)r`~iK=$[nmmm lD~~.h(u8:"o2CܗTPk~)?$8/7^3Zj[q" WTt(H9vp|y"cM!$]"Z~K= }A %hkkCGG#ۀ@?V6"jhk5(ɩn~M䚺He26,<}\E̯BwA\5K1 u bl3!f/'^Jb+JlRvSę1R*(x :_Y݁tY Xibxx3ظ>߇Ń(v x M ]~7u >q &LS\ seLBP Q0*;:2a!ifD099}Μ yy+mNu.(p*8O ϧ|IR9@*'=S D"rhL.=2+WC2k>DܡSCgn"όE$DQ&A7[6zڟs­(vk~f4jqqWBRCfM$)<b0drdBгSmʕ}w6cŊ*!J4>Oϧ~WD|~ԎF9\N 3Lld=HU@ǦH(}U2eԯAHm[w 0\ V?(S [)tȏ'g=Ǘ">X,d2с0UXmm@xVq(%̫8(QsPlC]a]e=떛y6TuبrB:vs=ퟂe7%Kn擉Db?E +<?q Lm؁|Hc/"Tp{&4d*#{h29^۰{6: _k MB`Ern͘HOg1R 9Ub| :W ]ہU;trQPuXŁDz+/Vq(%Cw0zU.uA­Xա@{U_4X<>nJL.T&~,m(x EB0Ͳۙ2>ddBN]! SB#GNo{dBchvUࣸ 003tb`UJBʝ5fgoB{ykC3:Jr (rP?9??)rukApb6K1}bU|XO[js%UOx6\]& P$8(˫QܤcBwɯTbNؐ< \g|ijjBCC<ׇU VnV6\Aԥ ECR]SqX< fV1c\{`y1n 7Z*T#ńB0X݁ˢI Sa Sa@33 |kz`YR9rCCgNXQŀ_G߽҇fffG/o*e^*nlh*>HD~zЉ=PP8^s\l:~`)v*FA)AHv*y 7A@[[|IO(..QF"!Ad(Q3 ǡ(\e0E/պۨ0vblbBȑDVE4OA$߷<<&Nf Yv duz;+L^Ńnýne1Ãǥ@=CM*7uC> TUC‡D,RBRA [wb4.+ TEj]܅pA,#/Y;)vDelU.(r&f4K"A|R`|gz .!.D"3ؿl:z6=7 9٩!p@*m(x EBnd3]"JNgk=fELeL<|zsАY"!\u*u>Dq|~5!dfLaXTB5۱&v; yq3SH&?wB"?db $G>~↖:ȡ '+9Oϧx|IN dG,c0,Fى$ @v ":Duğlf<0W!PZךDO Q2˟SVq(CwvZ*w4U*tp1~;!tXҰnh4|q)җ^ ?\ \Ugv xh6 %HtR (x ִn`z2 QV{r|"!QT噯!A5Ń>DWV"5L*ÀRTx4ŪԯA~ Q7j{ PFz&,(.?ezM4O!Ymh?PZJ\ m9D{d'_K9Sp2_u*2tg#aP=TtBa3~(K|&kIk{A_gbzna̴mzQM7UD"OP@M0 S2ۘބ+[`g*3Oo Mw00Dz~J?E|I.kbr13񏖬o罱 I"qՃSÁD".>Bn]tփaV{ҫ%ǏCrC_:V~7 ݧ!T@"!>u4/}E<2&"=GQJ8HkvQp.FsSz49zX9xH$kD 1aqE յP9dRMS!=r\5Uk)rP?9X0D4˞XŮ9RơCI[qA[RDNV*\?^_|}D/s܉b:v;~e"+:4dˡ򫀿R+\1KWLW]+D;w(x F  GnE<ƍ`^{{w< |nxd^焪Q#RHVXRPMטbtI> 6x>R@䞒Bd 92gvc%a%Gm+y M(rP?9O,_ɹ C$)O 6RXեn5Ѝsq=-="K$VqkVq8ah3qg| ;e:Rf`E[P谈 1=bm~7ޮΗGhdȆ!4/ L`E߷d{@UP@u mduRn H&3?;{ρQ&dgg1.Olv֑6n;Bڛ(t NACqxds5"/Na 9@B$_Gϴ[>O%ͳTqA|R~>Kp:WfrNp0D |%yDOOZ!~_£~)eA)r8ah Q)dOuhwYM"E\kk^ygmC3;Y+AYl25?f+ٯ"UH$:'n% 6 L1KH3xGP_)8!l}SesC8Ve="kiێe: UmC(zpYWSA&8w,o^$vqJRId ;W*PA\:l(n ԸZ%w 0vg; ȁU~,۽e0KR; Cb5˯S KUXX5Ur)y $IYT\\^5ș]*tp'N܂YaMޑz[ alۗ |L)[vކݻ1Ӝyýu[m_~T)t 0=Ajbmuo %!BrdA-,E"I9 <\xҲxKK ,>H-g^Z/k9x/L)q*v*2 e~Z:GZ }o?.=Svr|ZgǑ{OԕPϦPBD' }V<`BE3(v eϑ#'7={vm]xCH^)Zýav4?08iYY.܋ L!1͒yJRC~>)rP~Rl!;" ---hiiA4!ȼȧmkR%*?h+nh6\[B hu[*14}i!eB"yڸSзQb^jf}\Gf`,HDwiXၐl#GNĉ|Sƍ Q3=oLW1hz E:ĕQ٬`8xqC*cR@!13yŲTX9Q~>)rP~Rkm! r8"qDD"k MMM\8b 0>>Pr]80O\kv_?Ax/L)q(`~)=8pНIVN<8$)Ŝ v=p{ VZ8fY g/+u  ,7]s?ݽ8. Èαm_0a܄}l+vކPo'q=yd>Mp{S{/BJ6c ?qp Md%I8w=i}@xmd%)r!'EOKGw֯E̔!innFKK Z[[)p H/TxS(o@ZPq(C9pНwdyN Ek7.QE}cB*:\spij G;߬_Ub^&gdV͠7us"箙\n IDAT8 +<2Gu)v dz}}ì@cfj C'nA}kPr%C8uW1L3s41UUT-vL)v "d}AHERŌ"I1iB_"yb9CjDڐ466.ZZZ0$b1 ٹ }CQh6/ ,<ڵELCCxrjmK1 T`(-WՊ.7įr VNCLV^irbgȄ!_+|߅ďews\T ad1/"["$O'nd8 gJw Cе]4CX,0 Fg_⵳ &!Hӭw"=5aOBȯ9l 8HiSd'ɥ#WfrN0 Dhhh@KK˂!r1Htww/ zzzO[q\ VVqTAb gC7AL[jJ^ªbUvϯݜ'aaܞkAk@ohz*RJ^bVy;u]K%qcx HcF2ۘބ+[`H(=;e:HJ D];;?iD0/Oq4i5< Bd:o_tE]9(˫Q5NZሔhnn^ !߿P_>m7<x@*+9H`+9xhx׬q((ROWzTez*P==F$vcs! D9C!6H3<-[aϞ@dt#!\a0&9}aT`S$9Sb B#/Y?)@C<'pX6'EO>PA r#?*W 6> r\T $0-8eף X<+g Ř Q|ZBe\qn`M &I?}5䔾2Ȇ,2>!;b\gR6f+W#d)P(܆{`x;NMuaO> EXi>dUׅP_CZ,9 &g1X"1 ~A\>_@(ͭT}A-Sr%E"C[CAL[;Yfᑜ v%~B630 m}/g{*t0đ[/C@0bl ]"A.WYHX{.va HIt 'g B!d ' 'ERC9OԟҋavCTlȁRS0Ͼ}ښox s0~,UGR2L Awj,xgJBEUG[|od}UMHjWAR~>KpɯR#Rd޽ 0GQ.!ys/bi/#?An'@j_땺 ϜYAYɁPA]ڻX3eBŘv0/d;XC8%9.aͦsܠ}=9[]=,B%aDĥ^{B@\0^۰{6x@_uQ &iBYDrdΜ~{!B)p*n~>)pP?`%.@ 1!ȁ4ה%?L_-߂7B[qnu|ݳǡ QZO@LVPP{ űC on9 ; +9q ݜA5ϛ3>ٽaD?3M RHJ] f"pI qc=B)@ҏ,<B!8u%'mƒEA|R~>)rP?9X0~!<<]466"#S@qh4p?tttСC%|#>ۡmxGݱg$ i׬q(Рqah51~k(vmp{X _}VoR'P=}lCm)Wxΰ!E…q|ۇp` B5\2B"zlXS\6@6Luɘ[4-Q6bGq&$s$Yǚt5!ܴk~ L|9^fgz7=x//ؑ-<{ie7Jd73n?4"'HaF@70&[xbh"AG?胦i QPcB+<-Eb`*KeKܢY $ϧPA А0R8 ǘTSD~0HIЀ/xwDX4BHIbhkkCww7y|_FCCo=~bzDHyQ5 Q p8q<\9cvبpzVLBkC;N!x8'Be'%q^I!P@/)!%$bR|Z]/@o'r 0B#/:OPŪ.XA|d)98nҥܫO@ RD"۷/~455qaF&߿8x ۇH/NA|#ro'Ǽ/3k;ARj<>=7ՊLQcqh@#x8#B[v+:v`v5&+kbOhJ_r쟮ߩmܱB30 @6f7![7b˖u qPU+]kڵL*?'gB!eϩO-6/BbSEћռOXfhCеC+`/12ę!KfCss38Z[[F#(GKK ZZZ0>>NtttСCſ@mã7<Ix//q(d~)ݝT/ZM bߚgjoB hEWU;KVXoQpZ0#lZK}H9s3I(V3>WtvFeaD"*+*9љYP/;U٥"as7Vu(nyM<ƿ(ĿUv5055'0UN ƹC%v a Sd"!}}{ 3QVh{9VMm-jVd4C>BHYsKHOMX>O[T"|9u|KK)pTͧPA찕1C%9vssBw^ Ÿ"xX,v^޽{o9+;o)ͮ-CE^[,rRjܕ8\ޚ_vH68>kr)~׼š Kٳ{)hs [6,fE1'\T+y 1ag F0Cvw hΔ؁RNe70HM:a8kns-MkpLPOc !-G^~Rt+^[@dW*.hE{I34Xš$qs5\'X:}OٳDkk+\7s>xvb /q* t*Ev3S 7#n}]:Au2,tx-ɺ{r rUrښߩm܍;P@ /f0tfpA &CU֑\uw !a.zPY*hͪ]m91CET\$+ %:ݻD?X́Ru1="=U.KRŁ"k~ ԸLQ? xrbn4+1ȧEϊ:~EaãPݜ[oZ9glz*&4~d+W>0܉P@0"2H!jNgCr%ŀhV=`sK;x!R1Bʒ'N }r˟ ,<ڵ8c0}aco~Zjh £C^xtz|~Yy51bnj0i¥ ps%-e?o5nG~ s䟬oJ?g9]B8noBD"nXA!u hpPfu1,Bgzٳ+VT123SS~fg/4 J#vbg>ÄKc6e !h5=)pp% nGϷ0}9A@CL"w[JlE*dB]1*?=E_H$i<cF}28K0&jGq뇖E@v Rb2f ! ɑ!< \߄^wE;HPpB>\|HANY2j-Т[-Р$qc +W hkkC<G4"B\b||GGGڷ#Kx/8 c!.tNMӥ GE谧\,ӵ?GAe.TY>ݷլWbӣG+ͽ}Cn' -'s=D;Lx0bllLl&j.vDJ?4M+_ ]4jVd&/M3Bʆ7Kvk'J9,"eZ'`ɾ Z}u/AIDBfqq.!ttt*jz엠o<`JZh)޵"uaN0Dѭj~AG|.5!r=@[$"t.bW55;-AkA_23_ 0/'3&4̆c0}S==FɞDw$1)mT70YxV{ voTrl_G0+jOR@bt$e! 󻿂[:G}}9#XɡtY!3 A JJhWo?PU"þ}ֆ& BJMWW:::puC߼zlVr(/Vr20_8 )j~mT|`% +90m4mS }\pۺh1{T pq*'ڸ] WjgS/\܀\}g=Mr HĹ$y]{2aj`lۗ(B<=;dgg1Q wA|~v?l]0ȠH ;c !x+,C[ { I*R9A*_A Th[~eCZޱI9x:[yy32SE2  X%B~d8.>5*Zh)޵EL_׀lJJMCk\gXA!*h^@vk U[:,)fexJ٫M?{dNWE.-Z5g)S9Hs7IWS@aqlǑdRBٳ[c0%G0x(GZʡou[`G?ʚ(Ij2Y"yqSQd3dggQ# BJ//Z>OW? 98\{@>eZfrNwrCXA\Rl+W  Bh4 B!r3>>c>l?Zk;Q i!r1z .L {~j95)ra\;|Gjyž`@sr^ :M {rqU)} g_А\_ L`E߷dO$v"ɲsU (Re)3lY={v" 0ɋ#?s {[}y_S[k~XсNz:d !f0x?uw|52`| ^_q~勨bB5c ,yb0E{*Zç[(r('%_yϊ*thooG<ˆBd||hoo@q:\ }>ჯ£~)}?=BP-|6ylSP.DBN IDAT})c6VE H.vJ f.X|.Ⰴm^9,$E1{T-2'|T"OBpz;(q!5RF!x з hu=yo{*v]>U_ס[=РȡZCԳ0ޚkqpPp!:X:c!KoZvh"Ad nτ\h%2=/$ɭ(ă`6n DT{9`R(+0@a ^H1ȩ7ܲ͠聐3Oo{Ng7 Q@=NVFO3` I懕A(rp$r6o}LiK> }EyAd&`wWHq; !|iii)܏"޿QLp}ˑ]ZE> {1|27 FK58ߋ[~]-bZE:6>Y^X`:ίIVu1dzB@`uFuL_f+af+R+4T57nŝHcȄaJ왺*Dr+(x ^$.qMVa![$Aބb=fghIsC TM7hVEڕywT3 1\ri+t(Ef k{ЁBd`Ods?RdA$!4!|8r? 庿 1}[#/ P&}+:jY?s_x[X]RkdU^c%>67. T2?L!$ފ?|"!&O£Ѯ=N^v f~I,D"hjjBSSŸ󌏏BOOsgS0 _/&pc&x8 M 6'tp+eYMPay1~ZePKBhTgRP@F\fr<Bn͑#'14t{ĊU qiQ@!&S㣎Tw]pB*SP:rNiƶ} T !زeىP(`@ҏH ecdO:WB᫏| hVq&7 p32{mnnF<G<8ى%mU#j f{ퟂe Q{g2x(H0S C@aQ28bB-s?E|>.!B`*,j bύʶ"4-ɯپ|و8x ;@4Ekk+:::066g} Tyz{^ zWP|e&a3UCCC}YtwwS@!Q8]]7/ g/GJ耫]{|8u=s5וp]---Gccy8r݇]cyk;ﹳVzZTýt:]TS|atj 'wo|  B  ԴDފ*r0PHU ԧ6s}}^ttt/l9't_"~ ПgG*" z9 /~3r"uÒf:qV 9 d-ICrс}ͬ+ZBRpD38X0yF,(#ttEt`rդQBl沑jVw{f#Ѹ nV*"=ejWCs(TKAo`>\6IQT8h GA3yExh,ft3Dr06˳!9E~~(vB _f|ɽp-/kzJ7C~â:(2#"AQ,Dm\B35)cͦ^$p`W1 l,z 1Q|)D]z޽|?{~R4Qd8~ ٟ,z0mۆH$_w !|@WWq %}KȾ߀̸Y%uв 0 Sbo_/C)/P(H2$+mB+/#89`@=gj%Ayi4-M γ*)b6$x LtTýTQAçwo\SaQV0Q!Ad_-Rf^3,9ABZ7Gqȱ /e^WR66A 䇼n~^ӷA\.ZAގP(}q6: #{K`ΙtamRbdYd!d_m'\*naaxڇCh p=o9/B FIuyQk xDy;/:@_,$x0#&8t:TS4$x ̎D% eh(g ĉTA Q!@$'&ϞR}y6G@~5 a6AaEMh]:;EQHܹY C` o*=qQp.AA p]74 Gڙ4,׉&]F"Ry2 /opсp8 oHa~_KMja>9}/+(]^"9ϱh*K=+y;/LFGeM2{&#p/4"^)b<fG-հ ̶j $rjEh*UPE`3DU]T,lp^|>ЇO|>B֒Ѣ8$rL 6Ţ/! c:] XhDm6#ԃF 氒ȡ } rigE6$7hEQo>B!SF syE-1 29#H%3J{5#Ϻ&ިT3?b &9Mob(]NjH@*¨ ,#K,;Dztr*^rx{ʕ85 ,M!5 *'ԴTQE2Y" .3Gt( |>cF{{;0k3(J I䐗Q:;;My.AA̧===@GG1$=eho~80 ƘxYT'}ϡlWW"<k./E0*poIPaɼcODt^ur[4J萃m:TZvGۄ$<偘S#@] jA1y%xo}ć}63g.Qc(F "R!D'*Xౖ?{BKofP<$r(F}2L_xLu탢~"§MepCojMա }  nB(¾}=?)˿4>DmmYa9l7Q%0-rPs{s) ̈Pj$>) aRN @>̡~#!iT ^d%=FT*` z{XJf4*e%_avC;P mވ0G@~,~$nD;F`[7=0 7v`;/Q(ɰ1 ( v܉p8 M  ²LfoBDG<_S$SE0Ⱦz0v6AFr(L n$kgCu73>{Zy9YÅqs t%F 6!&8t:TS "j*I@aysrb :OT ːU50FȗZnGeu-7{\P A(hf)Iӈ(〕YLYDt#9X$Æ22Cgg'0|>0 (^)0CEE8X4cȞ:lΝ; -aUp[d$rn8 7PZ?)tltr77¥+$yW3VgVpԶBjMhG0-^K6O5DFLZag^tTX_p* @7J} fmۻix;4%$ǯaߓ H |}[0dDH(!*go `C 9h5C0 AQv\.A=x-EQϛɲl78r䈥EnS,޿9Xg-~r ^聍Et? rQ B| /ti<3 ̉ λv:NTyCt8N/Ed݁ #i8CQJewo^V@2'R!Dr;Pje &Apbl7կ*jKA{1+"6A&oհ CZ"T L\, /^C*01j{MApam{Ժ6wC;T~9;J8,rx<oneOb9X$7vkDuhmmEoo/eDNAz~ shB`a6F@B;-f imihdD-p`ȗ!TtŘ h  X,t`|F~F 4wqس-CJKTS St:W>E;!OoKǩnۇk&0LѨ$T( Ap-bMic_R}E8AGq^V=@^ZER-D$rHKf # ?m۶![JDAq\B\֑ C@4qSnj8[`,9إ^n6EAoo/~? t+ ;ď@C#K\d<׶df~àWko\fRbPe !tt͡ SJD<Zf*R$xX11 IDAT9.\Hf%(O)@Y yKF{hj]Mڶ؁ Y=`/$]Z0JV9b1ˍ/^P.Q&`9 d}s.=@Z߽֨DTgFyߋK/tGy`#gh!mRξM/TB48zB+[3C-ѭNBr"`>i* Br%$AܹA} ޸ 7nš`SLJ}wnyJ**,0 HM,\Fr GrZ-9δ=uhHKjjq=Eu  G @oo/Z[_1ԃ>k®SBk)E6U@亾 B$iKTp$~/+dE@'ÒB-:^&0 fmUb%It<f@9fD^%ml/)CNB?{5*7Q~*cTa v܎۰zcZ@"(ϜRb \EqQ*e}J5NX14D"҃Anz:AnD"tww7BڙhRkDKeq̨ȾhgrKEQ۷===e9B!NUeUCcGNJgHy#KdLo! /tYERyF#6)iNhEt7TS 3}geTKkJQ1|0d?OE0 !RH&T* ’ qs#"fI`o$r5o~3[[pIM-L,; wՁ  ȑ#hkkn[;,/C%ny36ρZ[[0<O|""Ju.<-rȾYK:,$flVXy3 y@?Z:Ҳ<<1>)p:D|TT S} ӿ+TnfA͐(FM $?8&r@9JHE0D'|^KQJo*SOCkڥAQd8Fwwwkʛ$cȾ -vDV>"u}+@ TrʺaY#î2"r@0&պ{Y"PT~~,avۭ偍EI4nEtz;aVeejWCZED: )]{yAM]8{ $E@MjT0دkUs֛ 7Sct2*ZE! <"<,*x0׏EJ(9L`,n`u Bhoo3-zp݈7H?"`a=fnDwv5#2A97ME0p.@>b[n-Ȏ@VAR EH r7f~@f]EQx@ADqq\B/HL{a}mRI`:'FOp󢭭 @b\ y'a$r38-  LqA^3T#p肇FPDu*I@SqT /}۴!74^_̳cҊO […+xPub JQC v Gr~6WZ3SYYdY0ƨMP8$r07MR0ם~D"\.ˏA7,zxsƉH`.vk`уMwvv"PT % ! vףB!=scx.EϛcȆWyCnG;Ҋe%㛹6M`nhG1QQ` ,9iH-$jR2 t\d7}!:G%;$-ǥ6PIBK8.lCb,qҡ2k?<E LGY<FPt LΕ$,@2lv٘4d UvTVɉ E~0 q`|(M@Sf8ԆWw0 E!3ME1)LݘE_vkՁ `}$A,C{{;\.WỲGOOOη+2n( ̂ςm{My=B NNrlΝe̕P(~I[-RRo0/!P:dmY~eBB~~avۭ=fj"࡬?BdNg;#"8 h* ɫ3ˉbԴ {!5m*;MM;H ( MՐd $$t B@VVۡ,!KXY=Wx5<95_TOfhIMBuL: ?/Cqsa ȡt hj"h|`gt Gzu@E-H"suc63`qс@ @`Z =t GK@,`곽$ LE8fWöːEq(/?@xOQB!n;-}RU%ڢQ,0hOk?I٠\HϏ2:̼9vHIx;)h4 RmI$!2N3KT2qm'"\l }ކ2YMJAQj ثhB51?F1]F)l@BXNg=aO֎y iN,ȕ6rz.R蝸Z v@^uoI6xq7CIXXMAX 1^I+geXTr7O+)Lf4*\r6.wfW'3HNf'R+~EJߪIPZc]2ط(::: nlzسgOA6mS]MD7;z3SjkkC0-lf9 JE$5k(3iAQa%^cu"r~l,mCwO@n~tPH$rB})}m;PGr$vS?5]6R&PM=qʩ5h4BU푊;hy"`CH<}6A^(Je!|VQ,C*vuIPb+dܻ AfRJ%)&)aj7$KYc c)ca uY"%m!D5٥"V턤Z__BP݌ W!n9/ehjww7~?-S1!Mp+n $z(Zl8ߏ+x<$#JN{{;B~?(lԾ?m 5c5 @կ${  oOdtiڢQ,0`܊1Y:ĦlI5FNUV5M/?Ֆ806QK8¤!(i3A.r21z2oXyݣ5GB;lPQA T1KQAu xJT،>pRoӍbŁe2HJVJ͋O/>M}mn'!y [,JkK >WuڊH$RcT7yn уǃ8. ה[[[ rBxBȡO8EA8vF}}=UQ1 s+{^'!o/@;mnhpn;* *t;[,І _7,CFA!~5I7 Y»GuX @v%IXް tA ~0xe}-H+7soN8g-3u {Ν|e7Fb1E=uKEJ;uAl۶ ~1[ #IGGBU 6x<x<:KlzgBjzM5b,$)^hr3ى@ @L4 Iv( r"{І_aMK:L"b`8nK> ثhr51R6o|RVK+?FT[ot`;11(a`]){\xP Ey L?ےR!KxJn'U8?A4Y@8l@jbL;rs@E[IPZc]iG vt( "HY&zgV vcΰ~hG$.sDQAnZ(%g! Jp+z{{O4D~xvo.4$"VF}h!SڅAڴrlk?Fb~PTݶ_ˢm!5m #B_?z !q:1†O[ʝTQD&]> vDf=Ey X^v$r07MȁCMF}e`y^v >f^ErvSpn%JM('=+]]]TN[[^/n7E zEe=⬥( d_ XK2 3pKuּ݁"9o S,#U¾ H`Hb^)X섾uYk Cɱ'.C9&F p:^EOs(UQVh_:6t,ZWW]]Ri_7CU m7G:M)7vJ HDci* a=f0!v5-K |A_HrǷQ[JkK&9C;Uo߾= Mo@Z&h.\CvW(Xl rettE [*Os?cF[?ۦ_KФXbZmmm$q߷y{k?yNJŎ@0}\I?7 [$t(w bǡ]zN8]ۃ_({bHdG)BB8Ng@%aUayEh0tLMp}JWV .鄇Kw"b4Ww~z԰ް4w*SA!P1OQAw7WaR)O* SBRU@u:jFõ$;QxogoM7~rM% JkK8DQEA$)C\rV@~7!)hL0Y؅A;a[[ \.-C)g|v KByR`E>x< zFؑ\W>0accȾ@4E˝Eau582cEsMG,wտ O$t3uac8;%tq٩9h4ꢚ.$x t '9,š h_vHiQCF;hiA+WܷB/loy@j0>XIV4VA%* BxHPZQ[^#oG/'pv!v J2揢$Nw˼_CjAoz$r(1.YD0B ^KȡdIi^vܡhF毂";,"vȿqɁYx CLIu??:{ fn`1}o$xHj[r'*G@j˚PB(Ng@y iN(2h_;6vQh?n5t4\PX5A$00d5 1ԩ]Oj-1EBe K*6HI~E@pΏpU7|rM75y˗WSg+a ]+aC7S8|)AEAuU5sfU aw~A'CqvN;'oӽe}Xv C܏vuuܘ8A?|2n|-P6rH Q F7>AE;kH~r̟G߂{ y IDATf[wC@Ɛj`!z,R2d8A_\a{ $66YF:C"?H !oHu-\VVVhl8@I ,QaBAbS!K:Td}}5@1h,x4U |1}kv*$r(1.a>ٟ0 rXzgϞlH:PQ[cIɌC;zYEQ,Q0X ``,"H HB[T@j{+v_'B۹sgF+2 ǣM<,896j(z(H$S؁ck -mmLv 6Asr$ܷli0Tʨ}8R 6LRn@m+ ObXU4 [ayBFTSփ08 NQK8¤!(: )3 9qr:hȉ7@Z&HXT-o );}&Ca2tydH i"#GQIjhB~Y<Bt:'9.QZ8$!xMĐ 'RSm+niA59 cP5!Qq-Œa!Hz&Q m;a:ڏ=N_AU \7$z(-vHx J!Y)|hx54"'_"~2R.nwf TVנ5ߎe57n+3}</ۍ}_;rg "x'ȁ lD%)7NYӎ}?~gg'`َgX nE]4i; w4aaT(6JϷ!SCCSbB(x<|ho'~|>_gOAn r,OK 5n틹=+H`ע}ȾW ۺ?(,c$tmG03_6vFgG &c eXw ers4Vk{$x Dtտr'o!5n6)CiqvfƑc`rVMD2j`A[T7A[ EI}ZZ%oCAp7?Z1R!tzC^uB%ǮB\޽lݺ:ۻֿ3PRO}`硾f?/$tO09 -o>ö34![r.F>)kABNg@-"Iخml0o С5k ϚK>jHUL3ON}aƿZ0J U;l|TKh{l~R&.HP:d۪ QTԌk#I] 6JS|Jo'򈛘\aį\6~M GYKy~ȡ8D"B~h/~ ;wW'zHz0U?j ===Ii9Hukk7,_iŦb9/zȑ#$J$]հ?;`#'FC:$WnrQC"r;^/ϯ7=]ֈ%8K( zzzHߏ;vn^ wBJ::9)@!۝jU,[tH쐳-BHI"{]GQ8i1H@g[%F6}*0 3 Aôȡ h_rgUMC2x2Df/2]0ϫ_-ߓeU68@<=衄 eeQ4&3H #뗣d_'T#pI fZ.aúvᄇ+c5!~5A3'ޫj'l A"xN;tA)H}P.]X,SπzM:xF!  r=Z 5l6,_aS ‰u MzҊa{_uaCF>Xg4|.:{>;%ds:l\"(P(Db|سgOհ{:׍~#Vʰɫqg#{SEÐ-A91~' h)]ض|Rz-r'Dvqk4 PMY<%tt_q &Z>DEp66țEsJ/tUCZOZ*r;\Ogn^eUѤ(HP:Uv)TdU PY* %]{3Y ?5@b_zC?o.CyzlzˮGLBҷ##t#5:ZgJ$p(1.F?6\.zpP ; )L܍qh`Wr5ֆ`0HsK===^3#jnn-PQ3:A =:o< `WE)V~x<D,I(7ثa糐 :k8ֆ@ @bD"x<|h];u.ᚹ)aO+Z% Jn#tiI"TEODvq4PMY<%t{jtt 4jbg`,X蠥ơ)Aq8Ykf-S5 ɴin/v^UP4…b-"xX(򂔯a 0첌JE;^=]nj +J#ILdπbڎZkMONőѨ];H*?+Gĵ4jxyog=n y'sxD%6%9hfٟ{ 3Q=p=T@~7=P$=6>m۶&ybAbzzzpyj!phi[md}lxVrCz~ICu!!?(K:bQ꾎|'!9׈"QPP9 zհϋ V"9X(&a7K}/99ABJ#l 5mgh>8Dñoh4ڲHHD)q:^E/S*=~Va!-Dz$u,Z,jƓ\O"Ֆ3X濟$ͳRş YFխ ]`d'Jި cAt,x8p~Ó*UbS,2*|]{;w_Y j|Ou#?!5?t%CqvD6g`ErA;?T`n&x&B#UM774lTմt`L./|gg' 5VN',&99ܘmV >)8nu;#IC ,zَA:tF!! ^{ E q#3ǡP.'<#r壉!+}Ob;`v rYd(фa){[` gqx•+q8L:b, &1|,%*2FM1>\4o%vcoeU |b@*L3P_Pf確^"ƫ )◕#V~~֭e}0PR hGvg%rB;b;w "|>\.n*Aj> vga!|phHlBXidڛTyoݽ'Ă|>۷5t v_#-Eb rj]% h+1}PETg=^9P?t֑FQrczvۭ]4q[Lm+4ܶ:N7Ք5_Rٹr'_FDˠb0@NvGR?RC'$ ]q8{1Ώ³fMɴȕk8(.&ڍ"b{ $7כ)@03,I2 b|au '1~Py C 组pL_D }6b$&tɈ'~*9T5]%KlFGqW*pP`(%Rc$r}3$k//+֭[vkooG0bK;5hGfα ?;\;-V9r41P(ǃ5k`׮]GtAUvߗaga{!:!-_c\7#-_p8Lc_Vlݺ.C7x Ǡ:k_v_#DP$v(>>?0BO,V8pΗPET7Z{湯 {;=J!tXTNFϔK, Y-3ԕ9S#I$$f֋Iq.zDRt:#ZE/~קV"ta}$v5ԩhwk]{/wm@}e%$I!f12)BDnY0+A{|[?ןp./k ]/@amiYc+V}O +Db2TRD ln < (EFI_;L,B*br< -K{ڎZkMTM_k8RKϓյXVSeDn5//hX݊x-?b+p]W1O2^XlN ֭[ؒBg@Ex}Cnx >uvv"PTbO_5AH 6,щ@3l8~Y P?}ڏA^"41r!C%V~x^j X ߍa3z"1^bn~jKzɤZӶCqfV]g1Via+P_ ~;efpv>ecEq%xFȮ3Y=N@f~sڪd4yu`r-Dfm:o|Gd\h4FenH@abzEOjBOSE$*ԉa3E܌6a[% &ӈ'΋BSa&c1$tXzۀ,ۖ@jX_eP9,FVLe4{5-JS7C8ŭh4@5enH@%-dTQDNDj|p;ѽe -7/D%qu|j1/h\bY9\[~s6TVT`!Cd+.]j޹-GT,:`tdc[F~hj?ARkj;`Sr&jH%HNdowga2sƏv e~U, [N`ƍq5:A(~0xe]qs<$~H`68vPQyArm1߼vH qK+1E,COOzzz9M*jHLNc CBPji]Qt"Tt"o.j?C[cizvvH4H^koMjvXDBsU= h.KP$t(-&v;l쌾51yqze+V6I_8BEv/i5inQtNg=>"^i8}Xbub4\w݅c$vH8wN_T쀥a?3TD:v!QWr>ɉg3eUxGw4aeS-lK}h({Jeh)j:$&3`Yn*FS 'Odx|Q2YTbO"]jok'-f-v]k##ɵPE+vP88\Tƫ ɚ啣ZHpv# 8ގP(EQ8,'C;]퐙*<_*vB("CrcǎU@o< }9:RQ?F B!jfgT Ks~p` iYc%رcz֡_zEQq׾$v(===hoo.vm| ,f=vUDO" IDAT/~܍L/kMUY r3uV3uwOj􌡵%g;m*ңpLIJeNI+w*EvthEin{oug)DQV˲)RRBub;ʭEV8mE{cQ}&7v 8VLEPJ%ǠeI%J\( lp `<I̜s޳93/ Vֶ@bLl_]m(|g!8AÇr z+xF s@$8@e!}QI67$ 0e0 sؤ;7_B{H`pg!o *jm=m衰$ 7"`#<)ωps$ ?K꼔goe4\<6|8.ߗ.cD{ dxW6c'[H*04(?A䐍Ir(X+dq2{@@vBWWWYBKBiE 8HnϿ4)j^&o.5@,!Hx&h$P+I.MO :`QMKgy= tے7kw^v> }}}hmmU&B> ~QaR2?9;W__{1ef:3t.8(/U}qPY̋*@:Ƚ$?1ZFˆ, hY%dY"V`uZ'-; v俵>_a,6>-'fT*QJp+ŇsgʫChbD5[ۗ-UU ԱӃc@ vH 3;@ͦd;/EF:S8qQ5=CHCaiͫh%\#C% &,nFx$""!7G܈  ~Az8"o^)02Jz=Mg> ޸AE)A%ATd a!rSK譍i.LC%${tttݘ80~ ?t0JՋü ާ } C :Tԃ`σxA٦p ^jyuj6@YPUH#CӃW]6 bY*h_?2 ,ؘW(L17=PWaJ4$#CZ7L:\x ZP)Iᓇnp''4qOS,'^Ґ߯Yؒݲ0x{35r^U˖į@%\qzp WBIa ?$0ü4'PػCaRKJgO !y99ے( QGoz_[ x9uSՋ2a?^$ !`ωPP=9G#cr(C] Շ iƞpf@Z0(3 h\V ^D8}]*bo-gPՊq8x޲.@YoؕM@< HP̴;w񠭭 D'UAgx7ms<ھTE}*ЅBy,(en/۞kLuyl𡻻2 /\Ov4=r/ ^־N 35 la603"00r8/? MZYրUdE}CW@K9 &8ۃt=y*WUM;Ogh0B+у¬faGH4^*@Qy&[ݳ "<2LajN`cI5=x}?9nQ<,8{quP*s'OD 3S1@kc,\4+Ao{Z1衧,l6x<E?s7vx/_= i\3T$ r#ڵ *q zġCKB| -_veymQē]!ftwwe>!{ %28$/y} 9ER©onf;J8n)={4$s4Ӊ~*`ZQH|J̹*';ѯ͒jsbmoi'F0:}ԧ@ǎ@W|&f]1XAf+<&+j@ykO=c~ Km B,.F86ʃxBh6P9DGT͛W-a3#\gC2 90B#f1vHc[>adDIJis1L-f"@pTb  t4mbK8p]8bmW``Ў{Mz=E軑=TWu2@iqGDŽO 3@Ⱥ"x^KbN$/‰2hhb!i]<61pt[mʐ$ (B4,MӠ ÀH[tVvʶ6YKPQ)B58AհmJdB}o7.l<2!-B1.]sVX WZy砫YH\:=Qŀ* Z3Wʈ*#ƙeH6* p휈Cu?l~uN\&nzZys8&X؁PIadg ԬCR| y!vg}9 GSA!sKXA`|! |25|EI `hz^\eh_ϵĔ;GqX6@gI)T[+`!|#Z-'_DQ(H9|L@,*8D&8pQlTqOq$q lD@4# yĢēQ s"mM8ϗ?>N`4[PY@2^FN9,a6$ <ǁ BrC1KQ p IhZg(& ~"+ UsS`Ruj P:utYpC7?/PN`x At駀ȁ27SIW}INV@vGtKF(e^Fum4ۍh46,3uttf7T`@ ~j&PƚXI7@NNqa\.:::TE8e~@Ɉen|dÞTjPkf!q pG:)VԂ8-jZ *QE[:: ֈE4w/W! &GrJ_ڎ.P- QlC4ʃrڄfNb#|mnΕo <oL &:ym}Pɼ6HQbDĒw!yå``"^ 0&Ko07fPYx߫Aߵ;X9|s[pmd*ob[ϋAܤW Q $QϋO"",kҶF87tcx?/]9^9k>k#رCU[#l3sR:Ow7i=qŷPr+zTKx\HV+\.Ngٍ^̦ZT0}L>in8\.OLgnP*s>7Y=z J=WdT0wJK:RŭIazޡU M-f(--o7 b((9鿮mղOl 8 r#OaO s`BBH:+.^CqlF#^촯ޣTȉWx+v~꺄r}.]0f_?j3UF$ $ 0s !0'E:=(AxADH zZ5eW kvpx:WKWx6uK.Y0"CnxFԖgAKFjɡEu wCωݻ^.Ͷiرc4)qHk jFz5ŭY6/ۢCkk+ɛHN3?tAwz_U7x.DJÜY̊GNB2j65s\p\*#9h·0SIPU+s!UlOּy1 l(@& M K21/tR2/Ŝ`%$@ڍe;@TT"C0XAÐl]=sSZϴNٔ c`щ1Emڸh^{h]x3Q D02&ٟe;HM!M$!!eIRziHnaJ1f@Sa;pU{t/>vc̼1f(,oEӟuwǗ@] ^D)5 "*qBUڱIHQ31DC!,k[-7fLZZ΢0C@H֞jW>E -b'CEYWq3w4"q`عs'\.Dmmmx{ߢzsvG4aDžA2āA8_w y4;L]9~شij]mTC TY}eln򕕗@,=Yu(7.YmJcY$ Ӣ>v1GM5H^$nԺN).A: xZwX Xyv#MwNf(TVMZr[쵨YdѤ#+݄C?Av>1';l;6wO$`x;F"""t}! pDre]yKK\N @<|F "'IE IDATC.(eY:ΠAA_h*Y#CZq/mOA&w޲Wo:`ƒշ&H__ ef,% Z*4\ tebҔ@G344 ^g&R_yam:0N19]xZK?nݎL&DD73`']Q=} KVkrX4~mX6X4!JzܱzFǀ (1xu(SZ4Tm1It./[-ji <>O'z 4pD caqП{P>Cr(HIGYBj񠭭,XǃN}r(qntvvAgm/g7!?y4!SߒѣG7DQk6I\ɀS m>+qAEYV\.8N!J\Sr}SUv0wh]vҵJx,Dx(JtRgW8mdW 4IMe9ߔݡvc ]S̎n_lY".ńܠ0;m;, ;Ǭx0'  $ :݃>1QyǗjwm غu )a$pЬS ĊֻP6h:[Qo@֠ "zb8Ƃ=z`Z--fh3aXAg0,x1`A;eÍ}l!G# hRɁPoAY )f38NU @wLD(p/P`oiL Ca&}敜d&xeGrH-)=}L|b L5$jE___R镻^Ȟ@ h҄}PҗxtFqw͉[ljֹ3_!ZL'Us@Z5//)¨Axo&`hj!UVPYeB}Cxl/<3VTUuφƣg&avC!BBL"8IcP%t׹y*Ԃ¼| v"DI_f-p,kޣDύQ8#еk@*j?~VLw"uW>Cxfp8E6'Uu+Vޅu#]D F8bYk [FW8j ԥO2ՁQM; ||*;v 6m¾}UsrP[[<OЃ4.SO@ ^.jaj^Zn^/:;;UR `6| ~6ZeU6 R ;m@ ])ZS"YY4rRXܤH<R$ ?zFAU5f! ~\֓X5gI&|dV"CtL9/W! B&QK-j;HfB-H%?0~%aoݱڼyf/p<-0_!Ɂ`Ԡ¬vDWހaspTras*vv hK_rd*3Ӏ3t ZfR)M{yv,j,:׋>R |]nEFTEpfslD%<O7/~OVj̓7_pȂc(RrAtI9jvun7no\-Z[[ N%(Ӊ#GHE=_xw,{_oA9eéwcmXպl" ƵAc`!n\^t:,550--&̗`ttW86pBC`4I'A}zYVojO[!"@ jD%bf۷. R F*rՅ@@/uAwzKfYY 9$)PPe<^+7tC_x,ZDWW0%(ݮ[jooGwwwYK٩*Jl Nh?()s˫!P:OGO@}Gșf@pAZ *˒qTL&>fE5a-rm#Ҫ}5]`,*(01apO`CTIߏEI¥ B1> ̐{H854sm 'JN㥡dBј$\d ;$ /!U AI1w?ǎts;5C @4 .*v6?Qy+#C8}ymJ*'GzX 5 ~=Ϡ!(O -D$4~I1ӈCww7Jm:NU%Nз b;RF)._L I d9HZgӕ#?>qr]6o/'y^8rƫ@:''P{%ho;>dJ/;ha)I+ vN2\ȕjb9}'%DQ{{Z6Ea׺|귕O]oh>]M#l;4Rp9F( hsaBס-L*;l\a<4. =$$ .*;Pvkk epԬ鸄`+ӛ8߹p-sJ)I4`aIv_p{ Ǯ_Ui܎>0> e3q79BQI6`<{@ ƚ^\8uB}mPj*@єip^xErԷ?(0AKjHɁU=7BW,J ]6^WPԛVP- hěyV)l=r xpxs2w(p GTVe{u:V݁%դ2TEJ=Exh)5A źH9h)R )"YKU/1/*}rܙgNIxjAT?D^h΅ TO^y?gX1uvv]jnSJiޅS*hO9W^zss3n7D%|ߴiSAmhmmnG[[v7I_p*ՁԚ `:AU~(fc4äo1FnI{mxt(\KxRXދ(@7>77뭈U,#%1.e|NRSaV :[5ˢe 687o0h۠#){W݂:O;L~)a)g;Œj>p@*e@oD羄-UX4c79 S= sDe O@8p37Š 0Y,Tmqjh(%?NZ`4I稓?.Ya@Mu #"@/\?!] )gkk+<Gnٽ`=!4R]`n}[RjrOpByF{bŔixsO5A + vw޼eZp8p8FN3yƒ:'y'PE__rP48weG3PkMot(\8weԷUE,ҳ#г`AjO? J`j^糓ZҮ]Opx'K/k>T?Bl58N(_%'9"#xLi|e:{@ߍ By0*P v]aίP vH!O$)$0+M^Y`0s1UZ;(Iܳ-|+Z"t />zqG:  &Uc1<`k81R v[Hj̓C -D$_$]Շ}ibQZVntttxԔ6EuzVRӿ.|4;`0TF #8{ڦW~To⹜P% |Z!2PICg Nzo8eUuMaN~V8ݙlz#ڪ#xLJfyLJf{qrnOЈeb]h(6<\I<8h@@ : FGn(P4 IUK41#\@n|\\U1JЛiq+R,C񠻻mmm ʯnt=,ءȥWsz1! <[29?!QdA$ ~յ&P4J:> R ydíh)'Z9 5RzQM^ . 2شiߏ.2Y@#uSRݫCE=@ն$-ěC~aJt]0"P5 fà6CcHm@v"J@6 شi$^/N'zzzT^f)+Ղ%\ϫU$mKa@`ƨ*#I.Yx4;Ti"c{f0%3! H"DL v#*|okadl#^\_B`u4Ae/^z` CW"Vm:85yph'_ڈRw WXa7YѱdglG ?|'rH"""Ҵx^D0*QuFlG4̑UP=e^ 8h)R )">w(j0 Yvx<nI rp:p\zGss3N')"^5^P|h{)b.oN@lmC{ÊZE=ڰ477r:WQ. ]]]0U fTmdO o}Oڪji1կڀ[.hZ\!eiaqZ'{Eu.e?@L',!]@-1{ }'.2"""Ҵ :ՠI,$QTvm0mbǢBCǸpMy/@y0\@TEJH.Wοj"o%"Q]]]j+v;A ^.9hu{{;< v풷X̪/ktT~6 ?8zJ"_N3g/A}~8N9rDufMj7  C< ;Lo]UH:(OV%; qPUdR²!ta`|~RS2$p; )zg) Zb; UR|ЍGX/Nԥ6?)'9FMaِ$;_8 n0[2l)+az:8;6].8Uj oÒշA +8zN :(+aJ:Y^ׄI-D@@/FZ3}?3ϱ 82|!ernO*DDDV1AMCo4&ӹMdoHy=GwnECP5sr@(RerqU}~F{ uDDtGufۊ rL[*`rCD!gٳnt\x}vykA*P+GA| {Nqo@N%p\ t:I*}GG*^-9A~(j P "4YP%I. $ ),@7;e?Ӳ{}>Ԕ#E찰{whz\u'"PXTWZ n0:ϵ8޻8vjƩKg'i<$ZiR @QF"Ao0$|FxLXR*(:1^3L&C#Hq/ gg^n(#G`Ɂ:""RիC`Z:}"CєuoE<@y7x3W ^y]Ѹvڅ}/B98R2Oәl-dOlP6EBU679:L@M?:L_r4v CI+f]u;IMiKxX@vLcW~J4>T*0:"^C+Ao{ !QhoDDӉcǎ)yE=}j[o|+*A*`fYb# &̚A/ EGM&版Cyݴ̪݀" {E>!CٴC  U  LHITNyo[6[=Ykx*}seIA*{D @ԃ0@Nz{[u=-;_Ϣ{zȸ}GY-hx.8`,=0ocP㊵oԘ]M$f"!^@@Da>׮ Zj\́46zp nw懂7"J~o>FȆXa!0=9st#WUę(d 0@`[%F^2>s`0G p #J9ڀq ;07>0Nh#M;k*n.`<&7]6PH)!!a) ?3ԱC18ыT | o$%)]p7Tnۅ1&&X[MW*/^ڇRȋÇ/W !uK?v[&y `tt###4RrWnXG LCNrf O055oǏ( R]kdd'O45Nxk= Ou3%B3MȬ A/P pM+|7j纺秩!tb>{$ 믿佚j{]TIu"AdjBSs#翏JN/0{#vp.Kl]v{{oo@"l%MX ǰ8K:XYvUn]pNt*a>}쬪5[ l#CQ`ݼ7a<7 N8@q7CO5vHt9dEv1m[=^=bߣKz{0B:qĦHP(!9sxu:~\j* cƣO,j 63;$=<(/o aj"A_T 5}vs*iѷ K7"qm5c"~z_K;>XX"I̾x1!$$vJ8  $rthTDT:%ef^5>Cwg>}켪5Z )BC MOOchhԹCIў@- tvk/_̿ |՘6 ָ&}@5i3ou9n԰E7 6bz즒(N{W!42UR+P1?Z$?7J{V1s=D7Irψ3CfD1y="٠~p:6X.{±AULӶi͕H XA` 7yJN1Q<Js,.+< 8>Sヘw~8mfٍlA$k*7Mm` ]`yY3L,T;> <$D%!JBY8qJ\W(zk#x'?46K -zWcBCd>Z@"bՑjVA867^`xPȡ|ɘ1\MNN7>ɷj#9ܟGU|s8NV0ࠩs5ᦣ jb NCݴC^K=hs?66~B(A^kW[;Ho ;{~~~jz4`Dqg ;k'ywwɲ}-X=?,vhv? +Z^Pv;nXnMBv>ᜰF0IspZ&! Q;E7yA쐷^7W>r}M8;qw"x:NkLL_"5a`G;8ڱ}-hO.pgo[v!x0b.٩W|͝;cH$JS9 [$1i(2"q֠4CMht@ɆzL\šzΪz^P[w7L5?رc(@rBqiz-} >y|c QFu 9@u:Xy8z]IoOi~FIMNNbhhv@B``RO ^ rf_y !!_;ӽ05Z0p^<^o\s81u: &hGvoB=[fn676:U|0:cluf/\:oރx1!p$1Rj]]FX[)A3LvVPP_"wg3gOCD"H~ IDATUԫg {v!$KQC7o?z(uv[tn)OP_6#SS8q&&&011AP*2vv~፾X98\ڛrVT8iGGf|@CxZ:Xc7Mj\v7???M5ex.*sc>wa$K[ӿ.mP#wŋ2z3VлA,nVN3&`\l!`Bq°CVl[L]>>Dhۜ*j7x$tӳv`.|!㶐aY~ۃ=eo\Ǚ6W5n h@K;G;R$r@yǪaՆl (Ќɏ=~e^m}W/ώoID"*g݁8T|"yUO)vVC8Ɖ'066z0 BӧM^{!sFX6a $rʄ#a ۣ2ap"zc5'#Jhv s*yٰ33tA?@_c6`+˘qq x&^_#Mюˍ;7{JDƓd-#DqaMQ0+ 4UU(A4HZ=="쐱{ցvlcY:c`qziɞ3?ChrD"Hjv5Ӣ>s90HvVVio)a||tdKA affqC#exu0ak9t:1*6w`˛gG7=̘!رc8y"9Jf '}< ̥})g cj,@;H ]Mn"(^K5HNbi߽ fp Ae`I$nܯ 8!G,=(Ξ3vH}1ۃǓ,a|PUb PW=Qtz4YQ2P<^Y WV0{ud6\n\n o\K )3 IDēpC h5@LU؀b -.iHlO;~E>ܧ]`K#\ٗ1lJ l1 8\ڗrVطXܬV(o;X78vn/DNk9 iv5o~~~j:T'Az鹱lg]TI$#'IÁu$-TACZ`=!c[!@5}r]+;V 젟"(`Kݐ l Ido[▔Y2/WF9` |%YP(a>}~ԸGcq k5pJS 5R! t]y ߅vM~8~8FGGl+Sa_7iOvKU:_4zUc\ݲeAgZoq[u9nոϾl,R/ @PΞws?U c|kZIa@˅+C%z2=Jl^ʁ `r棡"/ba69?S6s gb`m$I4- >h`.Qƙ݁mcl KnhHB.uBߋY3O. P jlSBpyu5͓CMKZ3fޝeE9=L,[/#LI=٩WiD"Hŷo A80HvVՓ[*aUhW ~|s8=PHUS0fffLW}U oͻͼ9Soo9T'&y!;f'N$&&&hNF&&&́$/O@ih?Ҿ;YAE hNG l!z挥x:ji}&KEV*//\-y*Jpwwu弇7n`Ams;ޭBpPLA3G1id!g},޴%t!!嵁Ѩ)CJk n3emL7`]@4A4Ա}C,]6%7CDWcݫ"[47s}uZDJPx e<98bu7 _X:OF__144DR5>>a"mx)C/ :p ͰM{>wZW=myBB9 ЯM̙34'#N`G$y!}8h?Bޔ;Y8Bˌ@ V;fm)x:k9 ɾ^zTS֨P3(^/=7vs﹋*_ /+&·W-(2CLs!|zat5rA;`^ اSD(Q$1AUTşg#ء< ywZڭgjm+ j97DF6HxW4$7&?F/_1%Qs/=DWp,`vU`YVn_& ѱ6xM7oM"HzR`o8*ZmQA+;s˗*b8#<Ǐctt,( addN25^qC#θdoR۠.P'9ذ+iЮ/3?2CjNSOal䒬  2/[t@̥)g bg^D.WZ,/=H?KqMDg_A^,S{xh&`Lsgֻk\ջZyK `O65A8nDbIV!ס\0a\C~`0 F)ʂCg,<ԉb- `ľGO@vݸ f/l^Iߵ6Bƭѽ`o+,5ԫ (| W7Ź}8pC7v!$n5;9o[C敘Ϋz^@mp0o'#_o/_2aLNN҃TBqi"~B?rnțv QsPov բZ=Ҽ24/z! U;vHoId";ccEVO~4{?sZʛI$JS|h<ǁiUUqgՙ!S-pߟCh0-gΜA0HI 188h*wC󫕁JCgcwY[q^6__0%Ois&aSD3%/۞q@1>>NݓTQƬL\7j ^CV}ٶHP_%W~oqS#K1s[naUj%2+".12R}^z@ 8@s;~ς3c)9jk9{w)|??NDy4>UIC dҨ[UJ $s@ɨ&&&088)dݿί51u Ƃ1&urС-*P(5Qg5@ǷZI~?.0d.a;v B]I+ ԩS?u3Ty?"y}\v{Kyt:X9~0 %mƓYB:́Z:;րΞwiG7D_ URw5tu/X%3QvHW*;\LB1!hBi9mz\c%쐫M_瀢tȻvABӠq(_j"1ⱬτbѳ/ːMm GĴ67Lik`WekHn cq@W_+4w`c~ ͝;pH$RMivU3 >,-$vTd*j Į#ޛ"DJkbbccc!Q8q5/yuu] =9X>r s \ٴ%~s3GRf^H'ԙش$N:`0qSHjrr5G^ߞͼFU5/[W| ͷ@KcEf<&m1̹w^UǮKn}Ma,zo>W]8`j(Bz_xpXkg4_LvX:P$heðCb`!Jg;lŞu-;SDȢy @YN( XN*|!g݁`Rnm۽7%E333iC{M add4p) aϨ:yV;A-*9W2_e͔䦦0881 SW&`0XR8TvgK{*^E_|{Ct,p6nbz;X:%c~t&Lu$]]]A꩞Kύgg\;S%չY^xp#]l"[Y 3v`v b!U׻o3VqUk\7p[̛BG>gP>5%p8cǎarrcccoIeMG}:T\pV'3%4Y\FՁ@s2v@Ãj4""XC M@v7q塪KȻ͝=g[Ҏn>zջVo~w|^εgܙΡB0q;̐`|e}ÊY8b Y֍7w>*;BQ) ±b U2|fB7UT-K8H5WP< p0aء)Q IDAT"8pь{IGp%ey;DIjJ+#7}& _DXQ㶡b͟ DrZtuWρ< 8rГ?< /  144DnD86-N߅Y !sk^ $ٴCVޅDСÿuGf^4ͬSN! bbb@URY6>Pwb!O 4( P@v!sǮ3c6 CB!z萲4>>cǎxWvW?Z|[8rPXm~ ȡX|ʹ1@Y3̩) `rr<ɐJ̌9Ӟ9hUjvH+ t]&ytЋyK.5 <5 LKiqhRܗ&xI^p&CoWWW4#t@ʫvJspœpv,u'UZ6=BXx;^=!-^&``(vНnor٣i@\QD]G TλCCŕUĶȎ bj]!2  BGMSՒ\\Wv ȰHl@C5mHSn7.  Yhk`We;0yc^؊eEdkNnb16ͯ#)dk `~}m7!x /83?~z/}i_|~"H$ivUݡ FCLC N*V@j|?7 C4w>GH$5;YF>{O=rj}z2>ub(a>}ڴ8 'ɛ5(䐞4 4ME}zs("m KBc3?6)QM/L=h?K{*^EveN{RPFsrd.$e Ul@\k{6'zaH$ŷ4@^=rjrX_(!! appSSSD(y!*7ɛC}ȡ=zkw t0,Z? xԜ;v @CCCUIEirrڤ *K*`G1_]vi@c+:l} n7x/vQU.gyo04W^ýiW^ ٳ@x]ɘmszfagRW<7l2mc ڸۻc@gE`س?rap"X E˙ QS%3Rt\T~Y6XKۼ>h&WAP,<$6WaYi} mXKwi,-ܣ[ q]@bn hx N KUr;ra5Mɕmk{0]~]rVT@ęLJ51e z]n'Id^p"5=ě!H" s kJs%!|p#P4 mθks F`!|a% b)v `H2 n,F4#ܶ*D%l'A/DB ,[ ,6r)Cڬz瀦)Jׇ`B4ĕ܇EApA${)C(4,zxp8`>əŵ%*&.]nmȹ*<(.7<)MhXZzp'q?ŵPYYm4G@Ϗ$M$z̬.cr K[ D8}0nt5, . uܹ d.?\ƚU0`y4˗L N*z1 v`-x%AD'ys r̳q1{'A/ CeG144`0@ @ݘTClͮ tѪ&ONk̷8vcfCCUv rt!kDݽ< ,YHzcvsU ilBg+xڌ+ynAau-;(ln9P("`:|[+Ea+;䊣öa"5@h!O`-A M~$AK "\@ bI!5XUA k PJY]y!~&6]6Ԃ|}_t<>E_pK ! ]O``jed{G om7Ƌp3@HέWV\k3W6?{gpS'bUk oo>q|w~w|l$TM]Pt7ˣ]lg*hpRq@B(SNƻ⡧*O t~qq >ʴq}KJ@FmքO5zvvkffØ.M%x- pѪ&=4MUWt. ""X^-(kK y}Ľ@]]]}4Ѯxׁ_{5U>TI$D.3PkkrXKd*) Tv@A C}qɒ`'"apyx@f9#G7v'$A5[ YJI0@$19P Bs8P@BՐP5D$ 2V pH%DOc)a-[Tx+Q0 b<`s_Md tmy׆>O}#,O?.:3chߨwHyڹ7a911I R'T(/^@^ho;dlfDuS:dHp |6}-TI$DtvAK;t^Z}]g:b=7dי,2A/tq}aRȠi96`uRl ; %0ǃ:V.ۯH"$@4mSfST.) 81RJynp(’5$aUIjr~>|4%s:@^W\o͊'ߚBK;4qS}jC. c`G;FnI:K/bj)4t}U`o|w?;FH$Rմpz5-=A`y4˗L N*HNR0,/ ί56 @s܆v֍APߒ1@Cu:[G~ ߖi #}DOf:s<9X`Ce<Q`:*3\)C*&VUcN:}>SG)z MۄBkbDb |.^MJѴMz k !T!ֶ]X %A a>F ?}MMLءi v`?eL^ӗ/K~?|]~2f"|N<5JTQNf< :$Oƌ1\jN*[Aq` Qf 0Adݞy wpysnv(oR ݟr$dř3guv`.8XȓCMmgͷ6E2X69!yu0f =@hXv.8KP#g"jQ6!*4 U}nWSU];S%]1PK+`5(;:oy$뙙eWa! YB73d4!=2VJ"-zR;f#a9/N` >\#V^R2-`8윪|4CCb'LJ""oaUu̯a5*gdHF CIhaj)ppb @*O|Eniŧ{zqWG;t J&AQ | þ[1V1L\S9s^_s+ZQ ~48T1]q[6yqV4˗L/TvB!^U貾&X9T#9T$,_ZuF9:T?jxeŚBO̖d8*AU0U$š!6 <]£CYPZ___khqDoHY*7o>`w̾rKQ#/ FCӿ gہiG7U +g 9 z8`Y>]A0 !؁(A3a3eLJ4_K%%˅^\nc1&YE.vh9X;^+ I\˂̄ҕP5\D놴Mj"5\FU-GiPGcV..b>hdcÅ"J*T1i#x:72]m67bn߄B"F`^}!ˍίbxﭘ^]ƩK0~f"+$GwG׏NTfMO~٩c'D2]xx` 9T+vҗN*C <đLW7 tvna:حY괡iPnAm ?WK&&&rșnK*jqeܔB^xN̵|Spc׍eqC D"|u9p[FUd*E'!M 6:U__x :u8 [=e^ W&JjX9T'u 9dI =aAH2i/Lsi/YkS BAokDw@.5t<\<{9;jE6>'wR=U&ןߟźj{]TI$@b-H?ɷp"WWܼtta<ϟ6:FD"7Lj8 k78:QyR[tRqԜg8H0PxGk {S@Yitr{3qW IDATUSNOyl 9TԼz&m\η B~b($G$SN{ͥg yqvXb`I@G) n^JBzY{y ࡂg!B՟+s6TA$@ڴ0{]"QEvП1q5 <ܐv3%{,OEL O;(Z Y=irV@>Jh@ ١ok~$AjC(Gpȕ *K ¸c_zDK]]hiG܄qL"rݎl)}f1 W;_~yoQ"H%kb om ":._3ڡCMOOC544Ǐĉ5ݕC8Xu,ʾ#3L#'o'zs֥m7I$[̥g yrvXb`?7fR2V2d6';7 ygͣ8s狀g6C F$@*QFM oc^srg$|1!snn]DN :זcڐokZN Fh[$ f03`l3S=>tUU]U]S~H$SNsPY^zCH?h}`G7!@A|Z\ 9U\]hh``###S; m.gʺXQ^$c`' 8K4 5~kHDwTob?.TV8&E fۿ1Qcs7MȆM c#|eh'Z<#c ߂B?Ў;ō^WU_=3U [; C+8c )v TՔ!3rt0;` 9zt`AI" NYX||+waLvH԰hCyZ CX06?h*MKC Ѥs,nM tox\9OΓVy=I̪5u.A@,y75ch܀&]ܮUcwhWKx|=xS859KضQU/ nY\~$'z H9kʛG9 K˅Fr *}l`ȋG  ;9`>C:!7g]NO}4E$Kd tW =(v(P5Hg^5~I@ҭ0Zh 7U4 Y/ ڋ WRw!C J@7y[l>[\M"Ha8"c筹ǣ= OizI a_J@;R v`/^S]K帯=HmIے 1)\ބowaI^ф T],ɶǣQGM\0 P K)\ 0pk CYL,DAVmu 𯺿7ypy̓`xh߼t>7Gax!Ub'IY5$5E ޚZ(3;P M&%bCD<"t߇F"s|?qj $)q[toMJGN LSuTC:p꾾> ba/LB>x 7"צC ?Pܬ+r0w}Ehll>OJ;3@CE< ZɡۡPbStXsv7/mУ/,3tكJP·@ >>>O}kU fjK_ƃƜ^8ExSgW_sĦFL;y  StS)v[ ;X8ӰCrX,X:s4f'9n4pUMNsL #KuwvEqECBr)I9הȰ8D%TZ,ANcyL/rwsXgpWEa7ɽ'7} Ӎ`ʛ1wdhs e,2+,\#f2B#kW!<πדܲ}'nޅ@¹t4B| OSjhUBI7.p?*=r'w axP~8 ˨4b摺ջa_f֪pGN 0qELvPlj9q7ƪ;,?$Pt-f ve7 `ƐR[eyk.Z;^'+%9BTS\s K 5n1E1DfnVq^\dئI7(l5 8 ޜ^vr ^BFo[|N=_=Xac랔#լ@xbX8E23Wf&?GvH>76ȞƱ{xg'1o=<}߂ z H9ɔC}` ^җv W7CzzzLTzC{ X@"צnγU͏vnnrs7ov|>tttPw'mߛT=9VY׋̗ۯl̷xRe.Ҽ\%73_2v`o C%'T0{rt0 n8uyE3~k* m\ξZX 7ɿ6jGrKڨRLOh:L2,z>c}1`MLmyJ!)X ;udl?P5T-euvXJ9GTPrdn6 鄠h:bVV9fq.r^yMb-`<Ͷ4ipPpHC IW%pӍjZVYq<U+$G ^N~¢1@gkz;hߔtHE GCO7ĕ[^!kHDʮ?4/ 7iM]hol dEMx7K+nC(o\aA$)~ܷQs!*Ӏ, LSu 9`3 |RǏGww7Z[[DZVoo/裏柙CH?Y\QTa/dD5iP=ڗvYʣ ~vi0żfƒ? 'o/;9T54r#o@Ku]r⿖?wXrb&\g.]g1P#82@sd q Lc3M Cc! 'pu[j}Lxy;OYr{xg'[W08;z/]|!%?NW0>U+ LSuP8I{o*?uzzzEवD0D{{sz??vh VfV*/D !JV%9p1&n˹Ŧ<2w^fO*`l.gʺ_v'`p?5MhhCءXܢkL>yМ~(Z -kHTRgeQC3DI `O~~=lCSP vl|;l 2$V9<R@2|+i*7'"tiaEWp2|`P `(u;"S^2V\MHlΖ}J8O)<^nރ_M_ww#z.'_;Gg;ƙo|&D"7iq3%ȡHv&zL,^S.O< &EZ`0~ttt`dd$ ?OnGC ZeedQQDȡs^@];cǎќAjRf2z`*ey1w3FDz@V\o,;í!pg.y^$\E@0=yܕV}P״Qs7M T@Ŧ1ܶgZ؁1 <հ搳LPMo<tP<:!6%ap ;`|1Nbi+XHzur7Zͅp LJ8TuBS{77bO Z_/5ey1̭%J)ɨIUeq ! Z=>t4KnFm AL˯K_Wvi~eoj!Ou}ڷ64:߆Gvs{0BDZ!|ŽPPd;U99Fh& ~@Gڠ`0cpp0 (?HҶS`iYQ :, R%@a܆R']%Y9r\H4;;kC6Xѳ{U 99u;ҿRu<$fEVlA ;X9C덟ffuxmUf*8c5cͽ疪)!쾩˃e"aE+ï _\M\^j/h-c;a%:ka;:A6!{6֢U3-\Gڃ9˹FI;9ӱc !ma^Q0ps?Qv.'vƏ]>bS#=aizPA[Lv` L3LTSy,,ն0; ; HӈYhpvHt*tDjŌV"DMHQcK֤/D1ps ?QuZ65 M8Iw{P޽=f0teXر~0;,בӍOy Na82ދ7jH̗ A LSu =mOL B'>G~K,|ۋVr)duc,1L]ҕA1y<}1|oтDPd<# r(ReChȁWHv 㩧Bkk+:;;EڠnGz6vӖC)% @yM0nkk+ 4g%E 4ixown8)T2NDW@ʳ\iC?s BCGGݼHI nzzowo@J+t|8Yrx,U"@nE)NdgӖȑ#nKJ^<3; 7݌ثJr(v+0V>?b,i:@,Va:sւ9jհ\nm=H R":@@=^M775ms=v anUzV6(,ExNB1^6ɫ^ |y` @ /KKSq:Fku]NS@7-O`hu$R(4q1we*}l ȡڡa'~j,<2?ߏ.?~ܚA wk9AiV*.@q󏁀2Cq+3aI']=vir'ͻ;`iPY\/z_r4V0+dy+v !vc9ݼLC*+rx@ >>>OW*?ZבuWk_756z3 ˦w4'~?sWǸ׾a8d(fkTUQpz ~mbO!7gԎR ,+voWZH@߰\< ԍNh\ܧK qM77,I_YMөqۍ1\ boV- njHސa0!PAS] M]PcD,'_;'_;ZȎ}l= =|ssxj@"_Cߠ&HCB/TBCBW!ʵ0's&D*~?z{{ OX< 9'`*qb!; 2yAr$С8)5 π.XvZχtvvR& ̭=݋ثZš!UXul Tn o+!G̻0 < $WԲ ^BD @?]DC [=eepzdQcsp5Y-cG?w6_ mLv`&[r}sڿσGH1@bc)@a`MX"7QI*7⪎Cz>Uu $8=fU*Au qUCXQTDԤC6JaK7pfr\q~W+ Sb; ~*,`@uy<VN 5sM;оi[ĬCeE%^ #W w߇!m]|]6=#a|cߢTbx@.ȢPd;U8q;LwOB;"t"嬮.&5R~Ҿ E]T t(Mqs89'8..Xv̫.? 󡿿*RZ ;|g- =tG_?_}h@"F շk v@J_&۩:r(v؏LE$@?:;;188hIpTg( pusHlw#^AӼwWn#(>oO@)z{{Օ 3~@7 U =(vHCiqP<:Vdz$F}ʞ9kjYnm=H S·oHZ@0])"!=yqT/ ^86m+EѠ$5W-<Uob$Y1}!rݸEru| \n2(ʺ` 90;`&+o<:zl̰u@ׅ2%v0rsNtHWKuDT ]C\Q5-czR\rt,o@Fqrl?>xÉz߸m-m]*q@BnHaHhEǓѱd vh~8ֺ;`˩OY\/rq0_.^!Kؐ3aXm+Mc?^x Gck].p, c̻<|ؙn^&|P[X5=tX&Pda/@ W.e~mj|-(uhH_%ApӬQUU,Iv%Htʶn31އ>Rx1C ] v`&`0-rAgR$vXd;,Xα=_:1c\"sDUu٭!*PME0a>ǚ7(~26~2~Cȱ{W4n۰6i|]=^4UWv@ʣ,#s[xq:ɀ7} }S?;XW xn=UwN77Bw}1<੩G"&x[m0G ^"^] |3󡻻n^$Cww7z)kzMx8 ={ Ҷ5f :C65b8M0^$k}id##1v0ǺUk w%kÊ~/:'{/~E@?#+K;~O)q iޏ_0uNi|wD*?M^yȷKypBJ_&۩: _*EUg΁bpN1%nv`FO%3vg K S·E P ҕ2'ra2WV#j|a\% ,3=N=82'<'$<ؤ7w4 ssʗeowl8ܞ7LJc=K?US 䒮2ahq`UdT].q8\"!α!38e pzN\̴Pc:_n @ sGv PUj8$}J'u@8ԗK6%V ;$@K[={ѳC(K_s/`o&ɾwcnᱳ'R8?7lOf6|{(v$Rib8-m}oFD8Bm*]KK ;,UGGىAKG".A.ոer,larX[˩x$Pqu'NC. ~)qIZ~iJCYCrq(qH3@݀7Ǧ< 563ܲ왳Q s7IZI6Bw %B9zx;%|{iHbBTUpp:;BĦ zǾƴwk(ͼS ̰DUu.<6vYA`49mf>u0o/C3W`fU1CLSU&i*bb:+\Crب&yEYvsX sϸ677mxrxCÒ<TUt;6 9,+*ോ/`QPkѳ0 ozϕ~ΞT^<xfU H>-a$:g*e D*#rxFJ_(۩:r(vXc?LZ@*2[P_?tݛ^LΑzNaSmT%ξRAq vg^E4w%߮ "T2NDW@*TUJ7СC줛 Cww7zʢv = iǯCqʋ W1f/ ssX4tڥg3ֆ^H)4:dMpwpT$җvʸ >@JKb@(dEoBG][76tCӼAu6}9HE7a_n9Na|f,QltN6?V3v9ߢIV* .@x?])"}!{+w+tVZrXA2)qU86(hɒuʙ-sJ"duu˒8FFw 3;wvH.s=vv5 m΀:ܰʵYEjz{{A/%/u҇O@k^6Dz&ejeR%f4 2 5-f+Ú:vւ9jհ\omJi+tI:U2.a'O p`|AH$a%92.yS Bi,3ÚM݁Z@* ,,?w!وu,YT4E IDATVhUQ& *׳m/N'ϡm$) 7HYƣe!*%m?-ēN5TUY.Lmc @ _KH|ʢCG^tEׇKx~rIYF_?s?&ҧh!B"\/Od9@D9"݁d]]]8~5Ʀ}A}Tلe'e q+q~iI ZI8 u0:^R#V>>?mÕ5VP陔 Ky&Ejn t(m+谦& OM-:H$kbȄÖ2h"T2NDW@*TD'0w|_M=z(m#D~hooǣ>jMj=8a[Tqp4Ǵ$k^/Qh }U=z($IG1 ' Yc5/r+qlc ;X\"Ҹwf,hV|?r[ h+eLڼ56TNKt `KŦ }߱LCCgRJ%Vg=tY+=|,nmM T|p#Qk%QXpO8 :Mn%C·20fiR2OTb.R\*7'ؘjlsx8Oڻ?Ƿ -ÓHg,'H$jvgV,Cڨ\T_#tzS;UW^kS9nV$aߏGZ6ߠ_01[cSAlM̥@W7”I0emN49wQbNs& Fo,|>@J*}/Mmh4b9> 0Q.[>(avjvx MTUu&ymvl^38Mk2M*TjM=LWȸ@: `6ե{ulDo/5RQ45R׵;$o9SC 9l2쀜`T0]7.ňtc&t,X̚B\1k.ѡ[O޾ 5PȩFkm-n6` D8@"C-o5*x X\Jy|LJFwp@"WW4~pQy1NA.e˥0i%K7*F0Dgg'B%y&w>8G/ 7q@́C3:t:fW"xdg3!@*ܨߴ vK7J1\"=W5bgG< E;:^s/\^%ttHy]ۀ5cYYJ6Bw %BB:J.ELJW# >lA]EхS/#j@2+i&QHl-8n{: krRͨ!dڻvX5(kI%Iv:9])JE}TT F  k ȡ!3){UAvixOYtGK^U{1uQWkk+ՅǏ[QH *H?-amP>{T[$UT@di͹K_>y"`'> S@ge93j <ۢc&cE;;0#qr{]H.@O;!ͻiVRjZD:n>FUP{0ܗ}Y$AI+s5Ya`(5-HEչY4H&$!=>ۢaq#ETQ6 U*0(0;ʅt (b|_OvhR/eW /xUd'7EE֜sz{0јY !'i] p<6 ?vx100@?n:vK^ hfCL[dƓ+s7~b-X ^g犹8/9AI^3.ibV-@R"t8GO ^x}N3|â[nQ pyU]`ʆMXfwL2v<`ƀغ T 3@ a`cY똎V$T@LKm4Wp҅pd9,pH;$X'qpJS" cppxMC>pE?@>(`p*nsm1]W%9:!un"׌M*X ^BD Y(a bA2@C.o3[?tAyeҁ9B rdY 5;,*b+H눪*c5_Rȗ<]t1Mp8T;_˛_O\B ? Fo>ڼwN3nY"ءd1,f]Ds9F!.V.)G ^ .Ee@ۘWLJz'Ρge F&Ҫ0N_o?K$&8#yHu^t"wmL]3 8 vX}1#֞Cta:&|sܥ= ~|q`@~]8էI:$ں9:|M yd Z% |͏U΋&ڮ~M|ñc,W m/u>ceHƲ.^āmM̥T ע5t|K p"y^!6e F>ǎC__$! qQ}Fck4Afs^F]H49XG,<Mrsy\#l[.㹼bz@r:M*T/fP"ࡌ\vC6E"%fCO.,7 EsvCoHcvH /*`,K!tz`3~x4W083U@IU2wEah.'^z_xXm[}ϾhsE>[W&aw6l9]TUގOSF:$kp[Ǚ-ƃH$ĶDFv,6Dqr αTeꊙǮ"T&9sqMyؚLe|IΈJj䪙I$X ˱HIJE!>ޏ?@R$7zobI$vwc PT*+~ԑB)2fJE\.-@]8١>]hOC( ߊ{g ފX H76Q.`~t)~h3ػw/;X,fz 8V5&:08Wh+߶ν+Wq}@G Mr47طAOYg?4'r0J\=h,dx}F{T >"K&_{֑bf+:\Ufk"k[C3"eJχ8OH@| EM)1S*<tWwJA~RC4$21ފW?~W믾pg_spFH]h2V>s.v_v+';bhh8E~I @56ԛVq`Ud%ŵjN(e4ˇ)% "hH}|nؿl`'ZAׅ~p3wV8okJ8w J;::_ND<G:ϣrMOpkkZEՙ옡><(kйu۾5}SwV !dX,cttyW!L^ѹUb3G=:78YvU7';X5{dEokc0~A;Wx: D"O ^`@ٙ5ZdUq-ف4^D&;i4NF sflk& j|:aT+ s;A!%80~y9QDD@PhWgy /xZDv(2.K E,ed](SA;曑Gn{?hbKbVt})ſDvK%8Tkw/1V9m`ݻ 9X,fݘz 09f>mgCm8;d2X `,1mQ& 67,˳t+aq{Eo 23pYlI|O_ Q~Ҕ3͂pI&y Yi@U^6q! ' bM!vCכH$x+5'hrRkl 5b!$oD[#JE!&UIŷ_܍nyorddpGk0y- 8@r @k4`AL}-Jdo/NAzP=ebppj|\Mla& IDAT!-pCkۇ!+$g!hiƲ<كt:9 ivs{JN#U?@lQxDCtX] 1p/\ ƶ`f$EQURXQ{R_(A3١:duDU}Du( Uʞ.FiDsM?)̗˦^;0ы+zűLtڸ b$(:UQJH7fFEh  .8*$vrivh^aBu(K}Eb& /G`%a5 h\@@p {oE?h(0?yܢDBTCۓ`^[$ &C.`` ޓC|m>d2<+y?;5E1zї!{[WX x, iG!u:LRɟ@~|sxؽ{دcjO%rZvh) Ij <@\-Nt./$lp/bg/x 5'P찬sъᰮAĺbxyjyz@ @+~Ϋd!o[q,ćbصq:œ*';h_u*Ćo"u +}?"y젂"#/'g2B] p #'ߺf-Nx_LY3ugze-Ss0v8@=H)ؓY"add8áC,Sz]kg Q[߱SNlTt`x]Ks㐎}pF8+NWTXc].}w_|rz}fɡ5Qy]b2/<ևVQ^ah"Wυ[[98H$}#QRd3o$aPаn57 5#k˪G\ iHv0FXJ$iJ p+hs d0()_PNvXy\QuŋVTwϨ%:@O8]6Ny T!B~e6| #$om7S#q~0ߧBvC,Bgsep>39eQRd|iTAs\!>dmt#Y>z'='>ppXKgǭ`wij.P͡ ݋>>r-do-NAzP=':Όfu=P6ZyF80hiƲ\166T*Ż6L&188h(|IMF *-ebhxګ`[>#*8eM{B3TH" @ |! ~qB"B1Nc䚟Pn Bi D?#Ð"/H ]I0tB>*AyҜ@U4Є`dEzDj(| |n@䫡hOجr#aIb>^|yȉ,{nc uU݄[":Íq [D:|>D][(u'6-[Vյ4(mLsB1!ٟVt L 8_DVk(AAp.Nz`P/vu|3.~aGo(fHZ&>Wx`a%Voa^ 9{6G9}а޽{KׇL&||?B~GƧsW+d8Us-([R҉Cx|Ad2q޽9< %R8JJU%jCü[׮p#:P y1a$@gO("EN!T8|~rBirc9N:bl;xM0^: V찊fk\ivH_4UhbӅ~DB34E5lDx0Lvhò⢖+p-ջg3Ӷ9ҁGc *-ZDG>y3za޴2}WE}j&C$V-&;7d5 , d%ŦP(u\!>D NCo-8F|889$&ϾuC8m]4ڜມ#I a(~cۇC!.P^#c6LrWnO~ |Aa:7S [wΟàX{,áCo>9<> [\x8&8'8xhq?4Ye+Ik5`vNt0=~џ왶^JeB(_V%84_. G(7Z/lO18' ` 0Lv'4 _lU(qVKtt` 0١Mަla~Z7ek@Ӗt=_cB~?F#;_$N,R/TH&bV IV 5".4UW5>2,!;T) +)APIbN>|7^O4NLJE}(٬a*nqj$ Pk Q9}PQ\݁CHnh sOY7O|Q&Wg#ZΟ92m(JyUeS@~ ?e#Nc``(޽{<-Syh``G=V/f D{1PPyiJ&-aS^a(Cdž.'/!xҤnCM^r ﻎ E6CןH$xP1$&P7unq]=فH[Hv*Vo]=BA>|Spug'A4%;Ճ%;H +nEf}kaEx$ئpזx?݅7TUV2@<&ډ]7")ՅDGᕟQ[g!١I`'P,Jpi1|b2d6,'H#"H.!PX{Su6egQQ _Rdw(]͉~Gnc[m'W<~'ԏ] F\:'KkL&0Kp06?ມ[ͪ;ppp#L"Ncpp8 #@ʙظe٩֡Nxh-~g(@2߁s488t:d2G[!&?Sy"ɡ'E%51 NuRy8]&&`EnPuOtXfA mErc>JɞPrOʃ3wubl;oA#I]3d$?@o{gV6wt bP4h Pqz*Xne0W.K+; Z 3Pdi|%;x-c~6|+1t ªCtXVu.MAuQG5NAPա j$ہ%C$C $YA,a&W@vYm#$͊>B%څD8??D$DCɱfrEd?VXR|@@=Tl+70ŏ9C&'`oIՁ7nv\ؔ`R\!)JCϩ%$I >NPQXQ@v΀`O[ζSaGo(fX<~wbK<Pě!6/%8]~~Ǽ6rnTaHRedt+7H9^ Z;;Nxh=Oݑ[j+O̟PfYZ 4$<; <PVT\p9!\u{isڸZ@"j}._;_76EjY7WU=uU>s (q9c"X5/H$xXqx )V .]ַq͢ ١q߷;iw@,ZKGFfD X{ȕh l@2ZIQp|f+u7&pSkbxSoft v7' p@lH 4?/ fsD d@׃%r(~SBdel uh";P|jMCQahG}q7'`c naIƿJkJ3'@g2[\98xؿ?b1b eUH/?'98]T]@z:.`:}UY2 {!KUbߏQC$/ð Ȃe|/}n`fcjot~.<8Dw (ԣߚ T*j 'Ķ BU;oaM>@١4z&+x%dEI5A[T2xcc`YT0xlg L|aG|=nܰ;#dBDr`6 Je>sE,d747M" i b^ID8NdXJ#p]Bd=c62a>b?a͗}oqy8 HZ7t.Qq``% 8PՋ~g^\݁uB:Fuߴ8GMb>ڢ u2wRwt{-RsXy27)(?gH}Fr:+v-"˂*䰺FThq9':b::yPÊ-1EdW_⑁:8[` .]f=_ZB'fFv :jEj!;OndR? H UM6dy^i[dw~oZk&$::>;SYgP~@*$!< 6(%~s(\VIBQ1/RĞw5%Ό-Bw(??.!PWE:-S';X1?YѨ+J|x lǯ91q&<7, 8ponWqhI^f_D L"Ncpp| ݥpC]5m8Z*MпDD,h,0hZ|tUℊT 79`n&98n՟K)":pEkj/uU=/.2Cv(sKX5ow"Z+O H$XMlFr#t=ݮQCt+FxRVz:Ҥ ZFv*d<ճOEx$̹ [cTqmkI-BaߣG$(C> %IJLIvd@#)"y0`D`ف0uJχ`[:aK:$Qt atw1١* 2:1azćRn88g (<3Zqݫ_$Ps4QuA~؏BqbŬ>ν ۭ,_8Hr+B㺊ܽ{7 r9Bj+(MCz{RKb:}q%s*\x8Ɂs8A/CrXSv9.a},NԾvV9^$lx^QDWJJSKS'!Ɂ?8X(}Go @6 %DNN ;,d[|2فbFeUćUaly|nݘd ;p]H6wx]Ɂ pQJ4'###%aNo!ASC:](CE+W/ᡯBγp0eOn]>tG/x;XjM?2 x_p ^]P8EM%r m/q?^4cGs}C6cwίM-)eFVUngͫwπYZ+<3<=;̒T[@jj~w)١'JmNSW mN|Oq>-!@|+7l@e%CҢ-' -eZQu%gUXQW+ (LpdC#j IDAT?PN./v==uTx[pS1yk[0_,3Kp06?IbohWw`xؿ?bucm/dJy!NvLwT*;>H&Xx\ayHy2{Rz{{q0 3*zMo%xMšEձQ>H`]-%5 !d 6L<(`U&>': 8>=m OSVHSu${zpMg'~@*D@nuzeBvP:,KKx@Nćꂾp oGnclrxG7?Ódo.8p%}Ǖ{K$g88܁!i[t컐O07٦Њ9ڲVO=zxqY?Xgd2NBfTO;NQ謗k\`{YFR4; [NВ]VE/2Ukq DWwXF@c.7 0+_"vnFH E7v륯a݈pބ@j LvQ{h!١-gYQمYQāS'=gMjQAxλ\C#:@ ςq!K$ E; _`= +@\!;( ) _M3m:?gN RHt/Eyb͗koq8<3<)<q40^5KPZ~1H&d2x-W9^y8֩9ؒ"웃90t8WM栏PEwRubؿ?>x<E0psK9zj{ ,Uko4gvSu`#&IN100L&~ VO)g Ɂns'u=dl!Jo3]7jPOA|Yn02 6´Wr0^/@߆ФQ#* ږ%>vk7{ڕ3)2TVA,bD"#UcHj'YCkvtֹ inڰA%i A=iDv MK7k%;$T3!';!3=^<{6&)BsL%u2>]z jDQʙJ Vn&uQ;jєIt2a6p ";مdEn/.xţGq|z1m;]We0eз 7n؀]E90K̉rAJeT_8tB#XAvВBQ$)0_,!'e"HQVP$Dsr̗J(- f <۷6VG !V/#7݋19ŧypq3}7U\x'UX9?Jrp}СCbMqHo|ʹwFH"Z8 <QɤsjDa(0c[=88L&0Okڔpa|KYМ'9ݢF):1QU`|a# .%<(ELd6m.Ǯgm \1>p ¬;]|Doqgk?f@2𤲃b`|q BvȊ"~zgm':wwooyJD [f9e !F˷%_ I e\MQ*πذ>B(ga!R²&IRJ" E,ʐ{ZG%I||9Q\EԒoyN݁X C^q\Lr3rYsdyLDe N|HŶ"}ӽϣ7e)/79Tq +pyYݳsmW ~6<ې_HSk&]B!ac R 5rJߡjJ .$ߠ }a"BI1/֒L!S\B(H5um ';drgrqTb 3bs/,@Ta5B 6ߌ/ax뭈٬x%<~'q?(f0?y^Z$qs06!S\e(޽{ˇa!`xx| -сr9;+X,^38tҴ5݉l VUvލ1 598Zq2kP}kBQʞC}&j[I~pZsMjmDh{y׍W>;{Y"Hh`pvd ^6,tͮHDI@5ą_CvP@찖5И접|=@١ܤ(E`T%ly%ycGG?<,s 9Q͎ RAZaUHtt ݃'`wde(]QnXMn!h:*h0ΐx$A] Pi#;d"拥Ƅ8 #Ke\.m-]cT96"\aBzhh2}(cdm$=m:G}{YQw 78p= Ɯ3zK2o Ծ{nR)pFFFpb֩4W{ֺC+_2|6+7&v#?euwb[氜ei[߷MC:"chhϡ4=P*^CzY;8}]; ̳S:ؔu.ИvTfDI#]Na,AxYzOqvrz:nzfyPګ?!7mI5ZU?F|9@4څ`F,SRQVjQJ1[,6%=8Јpf~xyeL󶍻 `pf T%EIĚ~EQV0](BR#pϝ ;4\V`\AQ֗Kon5!+ooGp|z1wזx⎻0t *':7nFߺum8;DvA9';hZA2Cp; F.$ C! !\,AdbPDa5t~NK+,լt+?*b[e<![+x_x(kWr0V/WInʅKSruw"#NcxxdmH~ k n1;Nx`FڥWsX[Rn[߇:XQu0B^UOz϶>XZ(ʕܶik\ɁjRx(M|>DMiȇ'*̂f5t/-A X5?HpApƒkH$a˻pTOg~hmivl5Bv}ߘШ%iCCi';XgU}JCV1XyPk"EA!ïΞ==gM:{zpλ[Kt':@@=p !(~P*%*u$@WBpX +D(7 MѾaO,WĚuY,QdZm4-_l؇j/ȒuvDe nޫ>]`pM[S?zq7' L}Kz  *)`@=T/p{mcnnt懃ȑ#օ|ǐO,T:mR}6W0fj\Aaa[Tq1`Fǧ20o _z˂*sK u1Nt!xk:/ߏx]({o!Nxp3u[qjO^0yl[׵&E}Q!$i:(OTR7x_ӒF+t!;مHڕyIX5BRh%!3={{?}rh8 }6$:: ;t vmXKh Hv PְX Y&;PT,WrP;6޿  e('/J(9, 0Ev 6H-5F&)chhA*,s1kbKʞI^$9m=ÏÔC)/]>.$rfOTxP&v88`Kl|*@>sɯΖ;tPjO]IamDkԤIXPTS75p>\\ +V.//d>oсGv~ }6${zrصqv\Dv CoyIDV1S, +P(5n(%5J;Nܲop&;̩! E,ZK%PJ:ف?[EzȇCVmdt37݋$ǓO>z;wD__FFF066a xc=f,t{PnÚGs2 #CNw PsHyHflicǎ &U./zOH &;0 E6q o¬BGZjnZ 7mPm١A>`P١e~! i%; \Gv頭`|qQ5AIuѬD5SJ^m**ܾOy>ukm@Ž6m ;|@0X!:mZ>zhHTf~.b83?Nu"e 6u(q'"ddJP.+d-b9"CM>+@PW녰=d;QB~Pi7clxRw}=pm;H))n1m!8\ze%bhht `nnCCCqSy.>chht;݋cǎK<3|ǀo>xS=RDa+aRy}ujːrE[jU88ܻ.1ek[c0qĖ^›e!hsآh[86yҏ(<,^k՘OxضfɣJ8h@v*ĉfiH/W *j#;hjIMeLu o'`C(KǗ8xgmKwmيw~;n@皾@?@|صqc1[w,u P-B$۰LtEc ډ |7~(fa7&4Bxe 6xFRqCC9?.=m122/| qyܹCCC0xF^|~we 4ae A"@Wޫ>M7a'g{qO܇8l?O|33'gB=Z/W~ؠn&\@N744x핵uUF8p>NP0w$pqU~:7~$ WC__ #_Pf^[㰣2flݻ1::f8|0|ɦ/@`300!NR}}}سgY3̼_ǜ/hLVx'Jnl-Md;>$Ut<AE\Њ 2~{\A xO1߶"'tMFlUibߛj>,";@١-i!;PJ+X(,5ddl HO/+&X'MU:'o2˶6wt{|}V;)ݝ]71j r3\(S,@сA_Zג,cPAvm _do#a~a j(!WB<ƑEo؏=p؍Kf#8xy\;eJC++ky\vk{j9}X,mF8ә[UfgZ![X =47066!_[ߎT*t:͝k1/]'ս\Łj._:.۫]W˜ƒ!q,zD[2$즶]!鯿l # 7mذR#;d!T*-1 `PPt֒\t@WK[%\Q+)J#'6jQwC!`S8߯SgPPĘ!: _={{/^q n߁',nЇ C<B_;/i ;@0gm6* PryEe}Qy,*$aX cu%p=Nv0Cd!;FH(>4e*_[m1C?݉/ a 9@v~yYenǷ>Qt DGv׷쮣3@0T!:n_"@U ;(74!EyHlIWUd Vd UwHt/vbEs+1~Dx㺄`% 8PՋzȔ6$8T]Z:=~~V#%BG ھӦdX,5yKvqفR+?+ ,Z>3\QR{8v==|a f"S,hWq0oD{ʙUw^3k8R?a ###8vz{X94tm!͹36ow  P.i[9rRC:sawl)G(8| LBps4 F%e E_udջ2 fqq/7d=钫=pYصޮԣ$ޖ)Vr`~A_Zu'bFi8py;qs" ),cw 8 )A~no2\0vg̭/PsX(0K/V0Ƹ&d2$I&eki `tZwu~׶'8A/Jr>$*ڌ"]Flڍwソk_*kF,N,)-%\J.$N(V%| +$-ZTH[DɅ$$v,9c˒,Y뾬l]fF#yf]~kk-kO<(֊Q׵* kS"@̢<8R xp먙j;N|| CA\{ݎg荣hNB `#~_^]U.ԉdJx2=ln|V@RT#SKu$3WdmM|9I$L>jj-5kP!DP >!4 t*ㄅ.-n2jK!4T, *Klz v>0ωrޡ Ih*$P$d#$g3|bZ_PX>5!ǮBK*n/bG{`bҴLС$o'l*2@UQB`S{9JgFw:j@Q1^t &3S^)Eh* YUY`^M:ߌ},܂3:3++!gIXQ6vС&~+;U #U_- W |Ӄ N݋X,Ơ&CjmmEOOXN@9ϐ{tEGprd,{RXtI'.nh@:0ÏG,)%`Ϟ=Acc#s2۷c||quBGGR#r}.]v m xBD|ǚ|T(_\ʙҒB*2F|*~V/Vs~_ox+MaώgpբZ;s)'ƃ]n/+Y؁! v[|ߨ"(H]jKZi;8EԣwYUzuGd*y t\]!Hn;N7Cm۶2,B%1 H6w]ȒCcc#z{{Yg2Vss3z{{q?/A9j,ɘzr;5N] qv񣖕m63蒩hwvt=w؁na6іn?龔APY&Mnj燁HOL 3_taJRESPWWXs~;y>IǙ3ԕK/4;LWm0;$'c0v@8VdZ]NdEŹx˦dpϟ~g!,h]wXn!-+Hʌy C9$g8K.T*$ifTl>xr.u!u@{ ;Ύc1 +*FS)/i/ A|0I_Ü+(xsW^~- fG}[F1V$SYI1;lN@6V]8;W*8rÅ4؁$)^4@rM-vGv v3o vUU`p:߾뼵/jjMQ7`''6|JwIR BฒUѠg.̍e`rH6ԡ7s%aP@RU;DU_|b/0ў{W@Q{h^7 }!ӪOnɾ:d*&=7z.Ujp]tf0[w.Ǜ>^ oB={Slھ}aۘ(сvQPpa(G,)@_ќEwpCiwPOؼyPǏ@9!o m6tttLۋX,f[t e3ԳCyW>uݻpa#~oyV-/qCīLLF IDATDxίF:*_hw<&h+%kA'&yZw58ήL"?[|GspYA* QLːD` <3QU5Fw&:6>3v"]fT霰tIʅ頻K,ء;Pv(y?tSQ؁„zSjR<;PMiK-~<}cQt89>7^?s|SdB 0xUMwqjwPMu/a\ͭ:͹D"ȣy41u`?Ar C}[!]PWWXns}<@uuu1Mv]^P=; 9Ҵ?6 ;y9*dΘedHّYJxC He ڗyw?RCVϜw>~b ׬>C݇F{x73yCgPHC`Fuf(ZW u㯼`=,SD}ܢMAL>7,@N@=,Wտ@SC旓c(VrW!oע4773GqРJҡW:mm6CLe6_7~xa3-Z]ȫ`Q0]*U?Gh098\0e}sYy!",8v ==/ACP~ EP)9ۇ| c6g;Xt8TCם~խ7vjٕI$w+mm0?]{U9kQI)_8*SrGr.b]t\9|7窣ƅ-6$E-&K!T Ma&+"0m4s_Rb~/Ek ΟǛspll #$TJ|۰CQmMl/bz jR)g~?rδE E/9-_P2D= :ez=? rg#98*P6\QKqh.i˖-hlld/ݻ"] R.L a쿙boWW166:ɰ-p<  _rco3PgYto(;|zPG̺)D9Ð?b9e3Jb`ˢ)H*dwe 8c-kua13C%$ ֶqߖ4՞"3" ꞧ_2Ti>='a =nj7-*@_2i? -yhPV{ԣ&g| Mi";>z{{i&]iWL"NC>nʁ즦&tvvLdulق R?5ߐ=q>A':z4-G"Et`2ee׽|㝖M߂zWW}!8^qol?kfL&^lv7?z^ /~?5hSDBGA9k9ha ٗS[cӧ;e"<8_ɥzg(͟#a>vO^;wd5tq Pr/b K ;oGX&eIC`,aa@_Dג Yv,#-+_O[°C/0`Cx2#AT-lKX!Nw[T~nH=z7mzt|O~cշvuk a"9xI:8PewUح&[f`ߖ{vl?wG4BbžT^ L>Wضm[ٶ{jll4_$BzH_ra o@yɽ?̋Bm^`m۶ v`2,3aaQ!,>bQo3jE1eT? t,Ooj5^y1<~-?cϡ&5x 5aK`kA̡}GJǀuAqk0>>X,nL)CSSSYپuVvGֆ[~ ? w'2oҽ??e񣀒-[Dcc#|&2 v}ܢ묟 !+J~Ǯ:򪇖e9 p+}HGކf<kTFNU6zЗڡ:GGۤd*#PrbPfS3rCZP>z$r15O:,5;0f[C ;~ ;.Rji`y2#R0oUӣPNJR55dDfnڌ/n\X](5*Tq96w&'qyH*41tX#T(0׵E}׭jI W^YSaTA `m6^D6*`fB ##\0q_(zV :ſuS3Eqcך6z~_kNԄ沺0J޼Kց[3qHOB8 =T3TMEٳg<4Ѐ~===ldMu\m|[ J߳V]]]30-Paɛ;ܹb%XP}+Q@Q(rAq<r0`EH幥V؁R K/Fm x0q kBϽ]{6`/糰@ D?"!$X"7c`S۟bS?^1ymF{H'Gw৏!5y-:=3tDxXAy:JQ/eGy&3/rBs6r) lPPBvpK.۷oGGGs&ڊ^ܹSAF7(huF9?D"صk:4uvvo7 v mAXsͳ#axIEqXL !VLZQ>veQ_dbT%c?jQI>+su0G3$!u_GP~7+Cy4.X;WdozU#2_+I/ʛ;;p'ӑ"s(vQ:.Zy"S`kQ0.U"k,_g29A^Xq)Esxm9}l!Eﭯ"`Ma-/<nkc]Āg+Ԋ9X.vjVǘI$p?ĉg_0OHpsV`R("NM ^Ák0;#AbVLt.uEm)PBZVUm:agND#}w͵9$ ~a5κR Hg2V1%̵j%GHAXdzp-A@QbBϏdv8YbAv6}d*|n4O>Nb~͟ 6|]}\2%_u];ˮ8hj۶mb>^ P ď `d>444efC۶mC{{;Y26]Np`e%{?w(IMߏ~i_קm}9,:& ^lfkDVpzؼg{̰A3c Jk QZ֮F֮֜biSoc#xgEbxIJ&iyŠDI;c.d9`-(`cCOdiACAܰ>oa>zd2L-h/<;򊊋\0wnΡDEwu=XBtZ` @r&ZT'exteu e ;@sM iLJ  jO'x!z@aV *;`y*x3@X`/7c,.cZ?Ȃ9qphGLn:>>?} l1Rӱެ\ǁ\]jUִꔡW~3/ʖEw(,%1"gׅ"sdՅX,14Lh |Ǟ={h?z{{P۶mÉ'a31x v!KnuYjmm>NO_~5ȴ X;^$rd6w<z}oWk;%F#K77}ߋ;1>]C>Ea孷ލ}awb7#TSmQ7tx.I΍T."<8Tuuu"N\:W!C aGnǘ >\߈?[ }F)Fo9 KC]@ 9Ʉ/`UHK[yizHɗ?&ղJNt"-ee=VY{Re"5EL AH.Et`*"ϡ.v:Ԥ .}n@,xr糀 &DXY%06/*;Qɽ{Want7|ʡ_O~9[PzTe 9P00A] rx:7440ࡀDwBc'ۼP,$~_u__,W4Ekk+o>lݺu:0΢0577C{ IDAT[lq2Ё*#a޽3s0U|Ps.6(=?/]zG rpIE| :jܱҘ#'zꑧ->TS݇diehN|O4ppeS'Ż+:I%oUތm|7HnZz>zJҹsC3GBrDw S;PJ!)Jζ(nK O:E60}0}%ċOP Y \%d*ն(9|4SabC̳B!M%1c4H:T#K1%,0kQ!Uk@Df[߁Cw8u~ ]t-0KvH7t88;VGGtEwf-Aش́,W,Cww7Ν;]v9544`׮]8EQ`ΝT>y7~QCQ:;;hWȫ%(;n;`Zt M\/́&Cp||ܐ>i3XǯϏ+]5]8TSȣ"!Ful>ҵMCe%*}>p"P `y$-lJ4T];8^~>QG̵c t ·(8Ia"LչSP9ndT[HK*6MZLI'Gw৏!5y-R]3oc?^5`QQAN,+][[{QGwByޕU l6zG,cmFhkkCoo/N8]v$CSS~ j6Ov9Ͽ&VTǫ{ԣ&SO/3@rHS&v Z[[ե9$~>;-oi/BãTgϞ=11?>b1477#CLىv Zm">1٦v,$'{FwC|x/:rԴ; v.E;I| b&% <`^w,_Q@8_BE@Kᜰ (IkNZK<HH2|z $ 6>{ vfE%T~^@Sv{E`\Nb[߁?l'غա:v/tUtsb+RfXxwGM' ;` _L.{3材܌F477e2MhmmEww7:;;uGOH---bhiiA4ed֦{~5gB-?>'^mtūuO!pw^ rpHUءN(Rt7r`󫳳SW:nI $PcQMCؼ߂:oߎ^ttSF@LLNT1tww===#D"QHZZZT!ϔ ;йs2\zMMYuuuIQG^F`,3<)&es'5 pw-}|WAKS=ze D* fW ;hj!%EP .wٵK:bTs`y!|7$z(^7)ДnZrʍsVakqL@K¡ hIVg|vQB&H%eGrguP] {K#[sヒO;7U Tpu IUq>y7r*ա\֞NJnbB00Ӈ,3R O菲JP"-7ݻ166&abbb*'EыQl䡩$.F(fDQDQ477_E, ̖caZx^׼FAhhh(etx`UL*vvKr5JJ/+Q2Ё V98T{(hr$\$72vVzwnn؉:oK=$HdJi~~:ӷ0,G6x?T_Ւ:Z0|0xyV߀OZ;gv ; * .ɅU89R( ŰL:D?`ntlIIT"VV=Q X: ف㲰)֟^@i6`jڋ'~> iE8q<<aZܫ*&EֱB1@& ,@YFҫԣ@ڜ?:@V]AuM{"! \]7ᵢ*evz.?t<9疖bɣn]%[@l5{_9 0WWW2111iPcc#UD[[i}VlwY뀁m7vtt0A:gݚҨ#ˉi A̤kgtƦˠh)˜S t({I` hJŸt :#䔜7L!Xz{6򼧼!TS-_z}?7 "p`r8R,=R ީ]?xmu$8^d-[wO!PvX\PiK<@DhbEF6 oh3`m-y ;\Rב#KaQ]s-*D.pXvj, **4.vcW˧2R7 vА2ho aAP:Du0ߏ(?#6BU (:v vs6ĉyM0 Dn0(PL>n/F僚=OOG>z&C 9rXM:¸]6eY2\B$a& ]W:nm6Zi)y߿XlLLLLLLLQGGon_΀(c4ׇ~PQG^-{d֏;ۄz)Ty)G;lz:=\잾-ߵNįBies\w<\ pg[o6%u9H]]]G<8Luuu1Mgdz}Psmu͂?6= 7-D (J |dFnE8R^OtcSJU\9(ki ?:_kQ ܵ2q'AHtJ/އjkbC.#!˘%SI(m @ErxBCXyߐ`Sa+N8's~Td=PgK b;igEw3_W^1ym~ӚO~cHMgjHGR /B#i_ zKXyKNЋӴ󨧧7U@4Z|h/-ْ{e}}}hnnFoo/s:&&&&&&;v0'3>aE+Kc1ntfNAhjj޽o?@Ahp(X;y z+g>H`1s4v;!fs^R]wl7ꑧ4s1/h,ƒ3Ni['}" %翮j!}\yaHJ/H|flH8"Dձ%bJU]3*v vV56~fҺx#z6܅piMrÞgtg2GglppࠪPeWypz ̱Ng5c򫣣CW:[of[/= 1聉#Css3ɐB\p:"#7-": - ]9]gg's.A9 `CMK9" =}\~l@LAaV#_ÍԁC:SS+&ޱP@YX[,wG|xĒ2>qJ|e5 n犄dEgv qj4{<ŜX)SIz]IPseUt | >|1! vș7*BFRUT$ȏ_ͮb.2|]@wWE{Hq.$apbkrXJ?:|-ARhaa%eyn+֌Vˢ;͌^SaJ }APT5C*_ g/G,CqU?אW঻7oY~?܂yA'^О0Pe،Erp (^`]t<9Kׁ{!nIL'!l)l߾\~7%?Vl; 9W.gZjmm՜F9 2CQnۣ`Jv?艰DS&ۥi?sIidPhNk/Ѳ;pu=rС/9K[@ ō.0 5.؁ ~qn C`3 )*2R0LU% 2ig2)𧊢kCopuM-u\v™fj/voT ;E!HPDu8'l1y Ͱd6 IDATRģ''1Jb<ƹt J& vPݓS㊆Dww6pXB\2 ,Ҋ&Rr #9 ;YNvSfÜrhJl{`CHFp ༚™LoGq*=8k 8ᐍ0Q!î7Vd:>=ſ:ss s98*e9斸,+~9 KﭹҘtBDk~_=tuuccc11b1tuu!ouڧCdcI4MMMd<BPr_VDjT0.с8Drt8"`VgX!smCA}P{[_w,Pj29ctޱU>z tnU "oćwk~ǐIK]b禫ନǁȜu1oA,@y9|;Y{3o&v )*$Yɛx8DӉx1EQz@un]VHُ]u~Pd#;X g,zlCډrL7u!n3;2PtT >Tf+*x7y9=Ꙕ3x;u8Evp-7Wc߆̛m=c ?aŋv%Q4,`M.q=!gM&Nɳ1!w^EW*N~_R{"1聉aE,Mɏ. n6%rABkNPDSGr9:֝أ`:v~ k?=5{|ny# ;CjJËbYb(g@E}QxE8 ~R~ |?ŕU - 7"9Lx``wB  > RA70̓|ߞ$%`1Ehx sH=DMơ_s R٢D M9EW ˒^g瓸FFF066FJVCU<Is !/۬3WQ@9 <|{s n^1slk2IUqį9";pw@/4}lە2j*\-0_KG>hkSauo&^|86d$ MFt+*"CL*u7(GSkQ/ ;Swn2ʫ_z4YFjv+rRߺĜ:!\@[U:g} ';0av2C훔7.فR2 Ӿj01_,k}db ˶W۵.kmAaP0*L=d*@jJyoxǕ*Ԅ`E16]#%Oo] ң&*\!JjZ]Ȝ155YyfK6j]Vsh!IC:UW1ydY<3<`988888|NR ,W!'=k[R+B&@Z!-ǰ-İ UnEL=<_mL`&P7f yk5 ) ׮oajy ?r (.nݻc2$* lSG}q{qpppppp#!ٳd(j0p*CzFZG ӶCwIuz,XsheP}To$b@H!( JK_<sPI()'<PzeWB ;Xv /ٳS;{퟼ $ |~j'I*9^ #,QH[jT=D,o"ΗXR"[>r" @ֶD2q9bkI1͛Bu;+X"H;2Z{z{3ٍ. 4x=eٲ7K%X'hY"@%iYCZeF`Q8NFrgaIhJ'VK֖y<1x$׵b@q&0;wr8U`e6 nS=O9 ';ԇ[r;@bm#9gHb~m@DBc dnEH)js3 > 6)١*dF7KE|pYI>2MsI"ک?~]\]^fN;;zܒT n\dһ=e٪C:Д$@F06ՔbY] Jl:a#8RwAJRwjU P*p֗jt)g~ k ?ӧ㪃K3/'рzw}vm#<\fbIy]l0;RRѯB|ȕR=䓘&LMM!bffƕH|ѯ$;$jV {oWyp'pz/]$8!pET'DEZz>8Qxets|2$1vK2N( 峑wx8j+<> Qg.%OK¿ۚ I$ wOv؞h_$"Zr #;≝tXP4 ޟRhu1;lLJ"l9gcͻ:qkO*U_%(E@}3i4*e׺B݁lDV#҆?8lu% c{zw1zPIJd`ٰQ\#j3}|O %{U<}8.oाHߑ/()9D?ͫ(` '}Éăٳfr<988888\$xGP+wC>U@rxXHYB>\8NȲttiRv 'I&*i]3'N.x?Jvbk>~F ܏{qC/Z[JCZD+::.@P̖(ldZ'0cvx:0Y/`*)|F8(h-GF~/w|!;18s}8>I+eIhcn[ #;"& 2Ovvskdu7!;l>l&$Nv،;Tw8kwǤaԿ)|c0UeuGf:\B X*Uu{N#;FNv`N9Hc ҅d4s|_1LꈶX7'ڦ-GI$lusZK&Gcz7&+(--%!<6$׊[jw}n8',2Nf|B#u–,mSFn,} ϻ 1==̓!r٬{dQ dH9[g Wyp'"]Sq +9z ԩg^15 /x!y4I!8Pyh{8##:Jk^|I*PK>ڱ!;$d}i^O A J#;h3ݯ!FX|dL3b[nߨP<{lXD)r^\{+pyi "tӬzGQѣ|Q-ˀrf5ؖ\_,X1jaR,*Xn|Rt&]{6"z`Ö+$u:?wEفT7M>m$b!qQFZiB6hֺ {͋>$vJ4K%=H1L}k?}<}S(.s̽ {Fv؏'?6+.X]I6uο ;' 8!<Hrv==OuM|(d>iˀԺ,,'=pppppp84,Ν;J~DMC>E} kAޓ٫/ld_Dbރ=w S38}i':q?xPҞ=}QNvzs6X3xڐG`*10/:{@}zys Ջ:4ou/ zDUDd͠ ۓ]8GJQC4C0%@65ݭ*HHdɱbRjÕe\>=z*7R u\qP"=tu!I aXbb EwI~/GL @P$ /$hۃ Ҳ$Ma-y;amLJDK\S_ z'H?ӧ㪃K3$=a Hp_^W40z$;)^d666>lB.ó>˜N߸>aBz8z(&''yspppppaffƕH|ѯ$\3DQ́'<)ܜY'9p:]н5"KQ/7D1Fck Ijx}=^y_,ԜØT=Xj%| +L-_ޅ}vܞZˆ A׈ldk R;1Lnx4&;$X[͛^P6*Je EF_ō '@Hc ݊ϓe߼:Ȏ荁7%T0e(/@AVue~2 %wmJvب yh _ Ta'dz;Ea}[-Ua٨z"JᰯTobˆ&H5K+ćLXbsoOǥ&\@=\+Jvw}n/ܷ\7^fND9x=&s$! .pxI9s9GLNNѣ' WwgFR4<#쭜ͺIܥ4>zAJDNLG/@D~4 {?PӺby*A,@Er>/PohcwA}#Q} hU!'ջgGDAT!&hف(en( 6;PJQ6e20H .ː .1#jV߮nO4EbLw V Vdv;B&8D)ry  !,\P,3'7LDhVs?Q ;تkA{n;.D+V(IvG(/4hf!K ^)2fyU\`#>Pgl蕚T2"&fM'0uJJK xoWQ\7^e]m 8dTr̭0p9' ف>pY p\>H Wwpl0oP؞H3{8ccՇ$Q@BDI`_6vT'p5m3BC#%uu5uvX1V M1iؖLb[2!MC6"Mldh3U7#;,V*+ w8T0Rd{Y:0a6~ŊJ:nKWBvyxA\Vv @+d NݾC')-BkY[śPM~4%nI# $ȵIZC3?m׵"ɌHlY +/@郃[{?_½D@D$ ꚴSC@ZᎁAH&!6فT%;&ACKv˕:1nU\k(obB2 W.W{}Qw˖ȍE:"eStqVI%:L uFv mCU~nS۾d7m\)-5{r( lS-ЈM5 kZ2DDڤ q!Dw~ml6If?-!j}=Nx`Ƙӭ<?N5nZ̭G'<sv ߀93">}'=p [=aMPe(↡١jb7PE"qynA7L&u$ فRi2^\ĕ%\Y^"rz1YrP[XTZt郙-Hr$JmC`0#mcle P~;ad!D&%5Q{ΜE FA2LJ]-㢌X7Ǻ5DdԅDv<?}<}xG.=88p{%q%K)9<ƒK;N$9 h ESCC{ <w%czz?Gbjj l333G۠ÏtΝ;ǃ8F#8t(/ZLn|- m݁mg]5+*5])1~ADztW@/qn/KxWb%sy/}?| ~jɉABP%D#Mu$HwgUQ^!* Iv 2X';4Hd,tto%0՗XuWeFA+2$Q&8J); w=v1B?sX ^K߆WO|/¼#So?o|bN+\)"b24wb翛jàY,ѐP|$;2ȦAv U^WtAϖU$hrhh"!%jT\l빙k웣ݩ2x+[g $TQhNTbйd lF 3mBNvprEǵBK ImàzZ|MS!n6"zT';c m dv@1{Ʒv>芉p?=V;2׊a$N 9D46̽Ȝ;X $AbWNPm$!y Zɓ'q88888"\.l6gϺA5}|}ì[og'<@/.\*SLf$ m >!dZJr7P˜ öxރ3w܇/xoI$HjѺ' l4!;D6ꥫEl,M6#;Dkd£PX1gҬ`` _PΗ(FUYDlbt̖H]C(bPӂIxeFڀ$9Xu J)Cƚ:čbl><$;X Y^OvD$$}X}'+;DpfٗWvhr;Ǒy7OG(<]Ja[p%]!uQ_]Ȝ luF?-0;~ĥ[N: !88888"idY;wΝ E ^4xY}ڛNK TO KQU AD[o?x?;^(GBAah3tP;HA/s|/Oxo?`Gke `T3 H) &HZm ';lVwhNv j,ʺ(i6ųн͟| H}(0<0CQȰ~|3ao/~/JTTa*):kQq{0IdM >0>|zl'"" Hi25ɂP{١ꃚ}}8fԦdfei> ;P+4W6U\P?3E@ NV.+(oyJ}`*A!6! 5S#|f6nre${2OF@PJvXAtӬ?{Id$[qQnفδ#dh %5e"")*M /d-Bkxb:}ś o!m= :L!.\U{kJs Nu yuM!حW4)is>,,r $xB!';qӮ8ǜ  A U\Rm"q';JvþC nU' Mý@)+ٻvWw\X]àaOw e(`cCT-dMߛBehQMa V}h!١d(\$;aRV5 ^]Ei<ȵ)'ؠ;xO>շ"4;]<SB/]Uh,nPg"scǎkǐAXmZ0IWJ533~P#Ԙɓ']OaH'BJvhZǃeb?1dd2"KQ}$NgppW_ kv$v1YCT{sC G8d2dy3p[<6r@,/ P[aזd % {Sc`;j:OvNgW"Ȋuu&;Ev dhx: AD@1JaVN3& 2B,hJn5sf#-dހduBFvh;yfTkɊ0蟴Iq_6B=օnv= 2Ǻ''A % H1L}NWL̽*Djm{?{W{uCD7"9DɯP0C]9s:P\.Y#:?ݭq8s|l)5N6ٳgP z ⶏxOw6ןLMMNM/hB"Dcф♨i@d;4L,:!6:K=ϐfsx6ӱGpz_g2Ы㑷=%iׯś{Uhm=fs;Q!r}lyv` ' -< q#Lk~yvIĶ0^C[!H`'lћo[~KVW8k0:EA/0gnܓLtGRihu@SDhJ߄ԡ2%;ԙ7Cj }vH +cQ-e( ԣ$ ͆*o7wP]g ;Tfb1e˕ J.Ȃ}~!W*hҳ(A$<(*b^+)4 xpsWd,* ;YY3C{LIm##Ov+%Tj*^Bo ʦi@& J3dP(C–BJuO a d_fZ_} }L(װsfNң>yT|uF|!UPx Wxۏ 1hpcOC2Wǖ-={ӘBOOHΞ=Z~mC`"t 5/;;z)-ؚvr"ʫsEwEK@L~`Li΁#8yF:j>דÐI~ {;qƒd2#X6t50#WOvج`Iv$Q\%; d:>le}J!+BMץ? =i١{!D6,{(|du`+NElӌnL Nvw(?so1) K  +!(BTW$kuzAUWq&nW `CXGuumo ;T땂-(6턇@A^iJESo`Q_:50_.0+E4Nځ@.pއ[[t/^!>}śZ"\ *GUǽ_mCJ#;@o5;xs,hb;`xsw+%E63<;/Ȉd递ڸ|ܠн9͹sxgq￟9pmM#WpXϼy<8>- ;Z6[,+i4zz9 g~|om0$#Yt2OjWQC;ɑCMNz(7`+.Mz#q;*@OžT ydhn`@=BvrNy,m; d u!;Tt9cXv:Q?Ar\P_.R J:J:\_*r>A FIcb=HHw 4$n("EAцO*';oaRڞq'JӾ* X2*-+:؁NM̗ (zw &j?A|wC{U<}8WquH PQѡ_!8Lr_^TFCH.@{NذPEaG!+%xG199;D6EޥCU$ s #Y͠}ɸʃ38Gkޖi=s~ 6u-_ ڄccth (Hl s,Wy#xƯk?c0EP UIdeS0^ MK۾]աZ5Q LЛT Hp쐐%l1NSH $$q@+dBCvpcqib3Bv eo|T_&dIYɨUu41\Bݨ0b_.͂;ƒ%IЗ!ʐ6)dN`@!34x^#;J?.<~sC|k?فi}ϘQLNwެ%:jA/9'=MX&#%-{U|s_ ||\y}߬ก;H!.X]T؋C{{8ӎ wz/-C)jm- Aw%'Obbbw,OOɓ!~ϗRtBJgpDxXnBE\ŁdzEWًӐ@;(: $9C '*tX!Ԅ)0-)|xhv'S(CۂhdL߭D"[QЧУ(57)[oM{7k!4LLs G' D- >^ڢC;P1 Ev`dk"Ζ*;QhP(8^Q _(/W3z=DBZ%6k:!;X|IȊHdԤ ѱ 2)uF;6 f|&; rF&yQ';pli)㨑|ߥD qYjmDvU[k̺֭";@RoW!)*ލQ";˒^Fi*cjI#j SOFzxIz+(--]6*8 5Nr"9DFa/3g5>>Z8!<< )?#@tAs BW-㘙q%?!: uO~ n>wHd?e_RkhΏs@ܒ^~\Ձ8 Ŝ,w<NPB>?p0t0{/Y `gb}g;WV"n0ЭB6e$;l$cK<^90#}itu[Qjm$;fMvq_n:X 3١AT a:ɵIgWl..d+i:ilkRKd5uÌB#2" a홤 fFd@xovBUM6|Fe (q>NvhɆNM,Iԛ۠ğ( T#0uulIkg_Rx! 9Iݦi_? ᶏb:#brrw8p<.6ȣ_vt1~ ނ}pK:U'9V\Ɂs‘GK;$:k$9M~8‰Tjn^Ntm0nr}P d0/]%;*Nl?9r[!HW,f9[CEl';]SUHBeF!;X%sVÚ!AU@Pߏ @4ddG݁!(*u۹GvMy᪃4%l** X(VlpC7LЍ %<<e݂nbFmQE";F݁9:v:찆(cXKAI4!H+Ǻ8١e r ۤdױ$=L}Us|<0˙K3?a }G6TfooDpk4B~yQi 4+木; cdd7a/sMm5CHϺR|>'O'\8uky wC zXTH@P6>N+ o;HzU9dE'jݭZvia6H ; l-mKCKxx@1BNvp<2P=x2brՁGG DQ@ ĤnhG 7 ;d4HW`А@ I$i:+RBKAv0LnGv0UDv&bbbTAb BWM7PAume09١%;EȢ:ʾ$!Y튓[kPc]PՃNv/5QF`ۤlP FۋL}!zG-<3/)^7#zxE+_^To6EC`)u4gWD'ŋqӘ̌kyJ0pO4؞/"m x"\Ӄ1h_~_%3zϡ-:Ǟ* w8H[| o¼Ǻ6XXۇ+4qJv eI;0ڿY.㭅 rg̐l"n'H^5vkh$Šm"`ReWi6Lno`) c╒`T VqDe>x/G/m7Ls({WLх7pZr9:_9lh:ݱKÐuW=33LOOa>(."megtnZI9̛yLڝq=ڢ7(QeP}!"ׁ].rǦ5YTL3 57 p] -I$ n2j}/$֣a([?,o쐔$$p4ݏ#}آŚGEvJHi3AA,UU"١E62*UtкdeRJo벺]\\&H2: ^lnlC6SWpei }븲+Kepm呺uAV9|}l9 RbcV@&R\….,vaqoyo.qҶw{9 f;$eRˉж ñ>Lp0ƫxq_Hf^`O-̅dEvd>•mF svC* pr-ieّl؎,eR_0t+ydYLNN\.qt-;|'j+됋/Nt'{@n`93zϡ-:caU[iZb]memqnDq +V_iAu8XJrG*Νw-͝G%V&'} !B!{ Dhq1ڛF&GLn=nCo";4:N(BPUYYg+!;dO/a%Hf0JŖ+0م\+.W(Kv{bSwPxBvRH{!;^J7Ke\/"]*|4ЪntdAB1GU1 iMUup=\:_2 t727] ֤k88Œu50ŷLk!Lj9{HwKo u I奩/3dk8Rݡnv5?o[YÏW>ɓ'O)`ffƽy++}ZsSI|'ka`dd5pi{Wq;/bvs8 |Txh ~(`2zNx_!Sw a^cg$`[nMY6DvHHvwuA$*gdbrnDDUWX{ +khAݡAl Ơ갖%%P )(\Z\nq: D^ZtLٓJC(ûKz!;oRbnKMR).wU;4';)TJŕ?Aum?\<\MtizBAv^ 6x[ϒq7}I㿁D뷈'KշNZ ݻV"=p%_^T'9T7S;HۯNe#(N#k[$ҵ|ILLL ѡ8s xy^ z #6`[.tcN PN H3zϡ-;u0UF.ءN Cha`M҃{>{,m  Z=LTW.TACO ֗Tp[IU"LvUUh_1x?١~fVd25u&Г|4M#@#WH s pn@}^ǭ:9ܹ1ٳfar8vN:^yȼ|\[8!ӥK> ?ii=ڢ$Nx!j슝씠HI5%F(q=Qa _HdakNxU)4 .8u5őn='u1 }݃CþȭrݲzهdZ[Բ}Z@&M Z,//ڵkI 'anSnޏ*o|"xgf IDATn^ybm;?i==D,1g=w%+X,Lu4CmϦѝJK"g5u%I!}E}v,"h\z%x#sx<*C ;uhx^C_>h tx x$,av\ۀe3݆NœCN6m[Ae8kY[C{RvΐNhY!0b Ea q#H 3ntB$ ܸ#aHU|hPWl/9=p)F^Idd4}MSg@ -͓.<<,'fv =ۭ!΁s6M-2_C=FE4%}E9t0etg5*Ag PgDaJ!=IKAC|f"K}"3F2 xw5,//I&-v ,//ڵkI*砿 &20e;>1mrH~@Z;#n*3lݕ;qgFVybT} ;:&ƒ{O| v,WO7t"Fm`SU ^(W*D<؁x ;xL)*C1MM)' v`7GZݷSpe# ۧ)ӹZ 6h^${o($GןdP[s]Ud y3 2thNP8v! un`jrG.6Z ѯc3p <[4=_1 ,U*/45 %CBS ;paq@t̼H؁9 ^WΏQ}(7siرZl\;V OZ RZg%@N Om2l4CE7Q p|(pRڏ< o =;@ͧr2 /^ [xnɶt:v4G"K;pIC{w{+(ceejUvfiҤI.]/)AK~[£ )gm, `nBOI!AN.{2snTueMPxKHn ߦSx^o?[W1ug_u:Xlgru{rHSR´ÝOH֘(ӯؖO:3B́Ɨzo@=nh_zFfiҤIԪ*VVVpʕTqlLTs\Dl1[MYpqrcsj]^%ECô$w}3M1@>N*u >i \KX1`a|%D;j(C v }; C&t)9Oa0uhK~.Ѿr6QLz6 Alcqc3୼-+a$ }8;$}?8H:.,ZNQScG&^Z`@$j8b1kQ U Q~򪆂6r(j:4EhE7Ka ox`Nw~ꂷZOA\ererHWHO; /Ö0== is> =@{ʜ<萄K Bŋxd&MOvNE?uG9{Ä<ۍ9 ba'UKaCYpaRA~K%av9wROh,ieAxVc[T {I=$ M` T?-ENQk N]$5??=Q|R[y0zQVu!_;(DAL4UE?, r8Pr n)n젫 Tet̛&c94hF4H8dmǦ@"# ;1ĔF#jl+suG< n T A%^qT4r 3i݅aZ=|ӟO}P>wFM4 Rf(_qZ |MpHYEf" x~ui5@X\ )տį7 N.M4i /Qٖ/W~P 7t c#>%) gSlsTǘd̲Ю-3y< 6!+8ǣP} <,N4,#5!YrGqo K3Q\wt2}2xʾ?:#>B݁ab(* (7|1RcRwacNpt 3 ѷۮf#G,Pah.4 jP@oq;C@l脧1 v> QQqH!IAԱwmJmrm ѢcZ ȼ4EM9hf<>kׇ~F! F%D/8CО:Mw w/l_ s`;+=F%{>ƚpь+sW9Kn]q|'K4(3R\7鶫WVWWeg&M,TAC[M(_LxѮR8Վ籱֜O@9YxA<.Ժ/)SpVM Ym*t$wɦ1rSSr@H<{پrEgvi z:-u"8":ə ׹5 xk9#9J'_…:}?5\QTB5B@T|v aw4dH!pt8ƫV1/B,Jua7ED+5hwl;@ݲQSrЕÍ%]r6#T?4U@):i C(鄑bȋ;D\iXn0mϵײp(#g[fdFˡ:ضeK!uz0|k~)?mQj.ݍN/ 8iU Cn.xےC<r7ؗAJK?z&M+„W{zDBp[(s`L*m?u1YN'ĘewbL6~!nwQs'%,}XRE}ZZ2J|gߎ:>v,]=AͿ[ iҤI„/@oj!<RFt; R>ծϾ6'P/eO%;GX24R9,s1%rv%0&dp)P5'tTT5๙g ;c IDATBhv~SlblEM{Dw>0!p SrKzSz (aBUFPm#W*Xj9A7rc#mTy_iV`$!đ4B+1?#:FߠCo'AXLi`o]dLhe1a<{hjl }n]ަb#jH݀{Oᗎ,$*|ps0$2ءm;n{3A PPPBg ;p/*whV de3dÁ<|*|V|/ 3mjJcxTE!}!|CiU㦓4d6 ev㧱cI7"]JaSWIM zsxR~>zio6#bQq7(]yC*ʦFB=d9vmeajA!ݦ{ ڛ)v_:VWW M4i!XZŅ B3@]M&#KC R qpNd$&]*#{X;u4_fJަ=-^gw$gGlyv%4N=:Ct&$`Ž쇟++83_ .s`ǥ{oP;AUA 7R[րQ$!C;OaXb_6`Yz!i2`(@?]촢Wxx-a=CKpC6`@$1U`Ay` ;2$FñRhPG5kmי:!cA'=X}}Q83 ,bdP29dţ o#tG@I Scz%yQ4iƴjelllv(_H{ uz;zE={}}jUvv̵֓xLIA:fL9E6'3zp<#{쀏w\qCt$S"uÿ3۹qЁ^%;{-8 zX~t q,Dž庁aZf[&juބ=o+;0P3\!&ASvP At4;򱥤a77L,fa_jP@d XB#| +ÈAyNA3Cʶv)1|,dec@w<\5N3@33C֚;0Y^^L[[[crT!dHtAok[cSհ,1J&Mvv}J:W!K`[#c$qTx9x9Kg3M֕Tu,~fq#6|j[;ݺ5kL ׇׅeQ}xp9bw*WƩ\B0eBS)OG;=ֲ8q(p!:@t3# R.`V6h FݶQwl(֡È(B@THvr]ˊe|y(V1Pv !$ !k.RT^Y6:b bhBkF3fj[bY%{ZÄU(}W/Nu&S[VQІ?BLݡe;#Tʾ,h\Zo?4: ;K)0?\'.Z މK؁R;է@+Q%j#; }\ËS܍'nt%xW ^j$3#"T^&khdq5:'ѧ_VOMWHvp`Jt / 3 vZJ@ w';7+$?ncfxla(W# dtԺBB\L pMljmy¡)ގegbIU!hE6wQ\ød C>{%aa3CXiD=f ǍϼǴƹu7Ad$1|*ϋS@{;:,r%,Tr6)9t4|5?c" x0J]J ~RCrO[9%(G4VL+++Vr`&M!VVq…"abb2f_3R8|vVeU 4}YשtV6e<{7F/g|݅fWf*eꝻ?UoϿ8T#9n;-z}H!x0ݸtUBKءo]*:8K:ϣ;LMMǎmyq}[x0U Oh>?0~!vIPw9)Nu:+$*`@,%<$Ѽ(DOd#/i,!联н{n?" 9d-oJ*s0f< s uF$go1Dl*\W/pP|>| 9O؁qlC~L][NWa^K)uc&އ! c#/aل`7w];wϣ:, {vOu`9> BC=$x*]Н;y$䐂${AΟ?iƥ ](A9 ~$|E2G!>هd<'.Ժ.o< mԯk?^Yge,"ZP w=6CP懁?vuZ Npia󻰰,r}hrNUJ+gͅYg6I P .U0xv Nm`,؁t' `vv펲b54-#xC^6UqCi 8ш犆~8c jq]biF ÈՄv<*3fJǥЉ? ;K ;خ:\'p S`jD})3سD=WS-/?8_<e〚&ha?f4_Kf / siH4[M{QQ:KnIa+wA_ x%yQ4iZbee?aCSK4{KT|"KiϮӶp9ux)˙Cn,OfJwJ8:B\]Mߞ]7CH4CÚ1 NKDvc ·er 1ҭ[a^OX.xM/йA"ըa`b=WJ }|x9 n4Pд `L v]U`jjfai ]MUXTW偱)jK9ŖZ ' ζؼ;Q؁DFa3;DX' b϶٪c5Qcf5٪ؙ Τ5,(;Feїa}ó瓯?O"ڷ *YSrHHŁf*ˎr30ː6hj}6,-!5:kAdR 핿9(a| I&{ﺲZ^[^A9=&??GxR '؍xpkFEr|MSѫ82RPyh=IwgrLrY2јeN!. 5͟ ;o=gNqhvy+r}H pG6FK^ba Q/xO^gD ;XͶǧ期cs~&b*:ZƇr:v[O ,F m^;q!OYH!C=O"!ivWthp)ŎBñ0Io7D݌0a0A_5#KY?nruM達MC-P"*R;%ь6<,--Aqbj) TsJ W&#{=HuiZbuukkk\ YXYYeOڵk e M-3?DA#- Aj6*/nCؚZe㍧x*g|زe_ \ɝ2ݹO7 {SԷqyq m6k4A߽#b~"M.%}Ø+aXJz!,e{@uh (:m`qi {B׳J'^xi( *Q((:8ћ!\K,6U!(vi/?d QOu,,VRfLMT-6ϲTjv- - v?0pxyU@]н{0ijB'%h5;_t6s[E'Xoj&ŘC,nфL0lL Ŀw˗{ɁBP{Ǿ>Mvu\r._, K.qUfހzrJCQ9'/S7nHæIwwF ωq5O[\ lI8sfmv-gF0fpW^:Los3t"N yXFRx.1¶EtJ u9 i ;ё v0!!58L#oh0u/LD?젫-`]U{W#ޅB?kG/)n;֧d<򺆂5j؁HL-/NMx>314ug UA9#ֶ4UL!Ezk>nǨ62 M]Jqwo{;Vvqk]62!0KA)$r^u݈vxv&FC?jRt}IyYRo[29/A7MRO>#cg_ =SYylìc"o6>ub1t!CDieD_\!fN}?:0/_mƳU|k_ءۮ^&s%μ)u| YH"񬟥qhkdl;/>שtV6e<{7F/gRO&j05p#,sT\'x>ipء07{aa^\ =NG>6 xN+<48+bSsu2,o&@)gt0 z$ A9g`*L1r΀#aR7#@(d{C }'K%-@D):r:^u;誂[Sbo-S߹{Y Va[~MMLaA,Th8 r{;htЃg6[:qihQ;H؁8Cñn 244͇ϫ޹9u9UCI3RP^i8^ ;DXۊճ_ zp?V8Cd߷Qhא=2rRPfKn!}cPio9Mv7Ǻ LQ XWgw;Py&VWWqʼn.k׮ut8J7ڙw5"@ CƣGOF7yF<{z9U!dj-wĮLY.pȣq,9up.eQw87;?lӍϭҒ {&Vx;߬,`:g)7C[&0GЁ*tU;B 93E( ̖( 7몊b4a_BpX‰R eÀBLU|>r9:q&v8J@9|6ءhj: ʏj5ܚ۪򺆩7F ;ESTI-fwm@zl53tLb˾ IDATP]?b;V;V+qǥ;ݪ)L% *Y3nTUJϸiAQ4z;ވ}mg ~_cf$Bv22Fƒݾ}=C!*n/l9/.B=WJi^&agvʂp[ .5'bM?pZ^,5/SgLq;CzsC:!=,C뉸>s -e~B< ;wcɭP~/YY=D6ı<b\bcˢ:AZ&Jva|܍ >@ρyY1!Эds,Q/v0 <y.I*Ld@:`.(4E2q|s (h !@ݶwTD+:K׃ASC`!JAWsz&Tz}y,VG?&j35svZ3v{PhSI7%O,Ștp@aH.vm A!˨xzN3;l[-4;(j:TaY}:T7Ww,֠SAE7a(XsRhs[ Q*;ē@bٯ>` 2aDݽRy1m0{cJWlYyOTx6BvTtTt;wG|VauuUb/$g/^%`AUw sP~I퇺յlo2fC g ;X,}r(Wl~J؁9t"? Á5l;Dn xA11jmf*ʺ9?-+Y2aù~O*=Dv͔v0 @ŁfTŁƛPblTwzMCDH56 P/p|0nܸ .Ȃk kkkk_k{_"HUM޸qCƥwGh ]*qVߍuO< t5e  (lڭvcο77a>[=;Y^|6PZvy+cxv ۽[wzс7v?ϗajJ򰃮Aء+Pd&vy<# ] >dfK98T{q9̞e{E?4rJ5EA%g`T@Ԡ)(&KyTrFl. ziT DxhR;`|u?\k[Ea|Ǩψ;7!a)EnSw, Oc^D4AAqc>W9+bJ!jGk\^>a/njho:?2!![Ř~aΗ// -:0lCҎJ"J= *W9K666ACX^^FVaZ .\餉i\ R8)?}JPxn< @̍ *TC~|7`1&;-?wq3[_fkZq}I!\{CDa_؂ws7þ"DPP8W\'_`Q;zO+0p#ؿ74`l?@xC :j3 uX=*!(f9Lu-qH#$1Q.~x3@ڒ(:f TDQa8qCfOMaЁf98Y.i,v~Q4zRl[~~}H!P3a vh@߱lg%Łv wزx].ڮ˔K)l7—"z' 0u'sD.p!: E =¿#@̢C r%;rX)rwz!-ƒTsw\ Tszr 6 ;uȂȀUUqOC]Ť6,uc"z^̭)<5'Hciٻ# nNEٔ7e:qC^ٜ<[ @EYKf'$w}_rn%|gQ9֌)a}[XX8-_xϋHA8sfpb& Ƃ(!@;Hh $2؁3aCك *GU a+OlanSQf8R4Q2ug M9}_E070[aCԡ*qoo-/*Wy AW̘C.3G :V0ck[PAIu:"(;ЁU!Lϳ0Zmn% q\X`UVƂ6u qЈ2-! AX҈HX/TC wO@N?z<&9!ƤhhF!"ӟ@ZxuXK972E)22իWV͟~cC^dҧL EtJ`-?o;=W"?!=t`Xi/"1Zޏ4hj=aH7_5OICidvn?Ra:}D n+q(,i \G#FS{Kzs)a;tk Pq4c|5 ;ǟќOݡ8;Rmr8mn.\\PCfTH#Ddƙעxx {5}_cfb-}{&!0DJ,EhnI5ħV./_sZxvE>%!iBane?hJ,'-f;X6w~aϖ PBV7cfNirv{@a;w@9bL o F;a&%WU֋rKp|)LVÍ7n(Vpذ)B;:/ARjpJKĸ 5r.{~Ky̋ӧOhɄKg3Ԕe9mjĜm$Bv2%߹w]Ϙ>A\7JfE|v)!SVv ?JW:N% @hءW@;6Ohv 2 ;64؁9O zv7OL3 ;ģT@pʡ3B0kK\ #apV vwp]ʝ8Vqlvhv(\Bu?1p]1C\dvz 6(/)^@_ngrn13]UQJŁ{9M4Ȼ[ 0E)(R\1omjX__}1kZ5ϝ:VVVdJ1QfggvںF"XxSɀ JʩPsm? ȱ6EeqKߏmsSK/+.s)a}[XX8-OxbFSxpo ᎎ$NeŨ`& vІ; ;Ám;>MFx^@ÏG: 7aߔ1 B"Ig!_x== ejOBQ#aF|[ޥ@ӵv8ێ8BK]'%8"~%#i O @=zyOiؒ9_V #y?ALQ&9r I)c#8f0=\v .],{/w0 O~?j8V xe Dߑ ^cU^buƚSi]tMɘf >Ħ@8iY3|W!x8؁Dw?jCBy#K b)t Z(8Lc؁"Cj$%ahӈz[-6)`5@3KUbO,H'ЁgAӐmbNR!I^9ha1ee0ׯ_H,T?F_+/Jh(FBj)ER>X>(]n9=H!Tαbn[mxf&x}8rf+gv~'}±,1rS@s,K}*8! 򰾹=|OM>ۍ6xuMAu&lwZ }E ,$^?{Wr ~yΩ+.UDHRv[g+ă" y9k[XXwkJ˻}0KmnR@q)v. HPynyN_DG?3S-X|VH*x\ F)oڡ 1^IA쐬jBzs_@AvЄe'.7߲;<$bpArI(Z,\!ECm栺 DHƳ&{|rFONz}}Vv rul=ZBia-lܽZSBDi=1$Su  (I6{N22^\%\Do$36m= h6`߆򑅏r}pOvsL:Ѝd`G]!->!\w܉דAvcurbZw} &;DpS5H6#lj1 I䐮K7to8_/;Dš|'A?<.CRsd.6! Zq[(XTs- /ЂХpd8Daz7=']͛7199IA 8 %&KxdVcsqg?)b+T! - f !."6?Deu vߣ˗ ۦBy3]PմRbDxaUm+<@g^×1pzHnK~$Upe` l"e'CuJ M5jсYݡ9%J{uY<] IDATN@ڮ uhݧDvGY%x˩|ֻxX.\|c>tĆR&D`X+W@c'|ܻkrSGB}dAdX밶g4R/5wIO:ts1 f>gvSnxp_7̽#A ^Y:ȔJIB$H_a9/\̅nX)1Ij&K@-199o|*KvHCK@zx7q F1:*JVZȍ ~t1@npRy؄HgxJ'!9 s`2*니MقK͔$RVV]}Oa@1"c2c⩼ ;$FC64]#HCJ; iҜ%yj}* KP_Y:sot`fm-iC,U*/W@P=6[TsHA:Cv9 "쩎^{  AFYȚOD˟.3==M YT$lDpHZ>nu m ,r2H";);Ig>y쓸z旼?Kyk-H${cHC荅Rqh\ҡ 8~(ܒ̅n,O!No<"9NS#9J.";Ͻ!ehT%]!(d]J~"7[3>,'Nx(JhМ#)aWDrHr<ז睴N .?Ҳ/߭oT'awPm&sU5%Qe8ޖ*~E*wU,ށ%0Ph}|ZFv ;/"; a l?H.Қ.~LXHitT T )݀iOrwiH:2VAc Yd:?7f^Bc}6P]qHrPUtH{W=a%E|B#9 Hr$9IPs 4~_Lпz3.1>>N( _JA|?^9PٸЀ.z%8ɡ+(9G0d_gA:Q-/]{';0O},ԒY]GBs9i*-3+fD"o젌NvWYq;xLx9@2]מ'5ܐMSH>D3d4Gҹ-5$Rq cm[Y!"\wiaQZ՚e ԫ^+gQC*C,(h׳XN&?#Kvh=h6^ Pw`IXy\g1ua'zz/,Un4QyP6h|CJ{: ١u/: =ϝ<%U;?r١fZWclrUssɜ >Od_םT&6ZHh&#";~'jh;q 00";}!aG/4iKe0ͣHyw@$ľP'@u!B!ԋjK.^t;j׉h;~GN8=V9c75m?ݡ\[E}wTwhNIhRAdD$";4 J4 8.i7n>Jz g:= ]CE ؉ZŁm_ʐ> Qv Ntmd4=FT<s504-~IAEŖF/}6W("94B*<~s̊"ɘ/TIj%d5F؋oJ@AECB|ͧ_˿)}}}RD066g+09Hz ?~|6}?Xi ak \Hϒ&a34> WTQxor2=iA ^0Pv\}/ۃLOM9#]oIvأm9tK*فsU%K`*S;0ׯMATb \:塬?8cV661%,nmb^7  ?yI6KC,,2!0 Y@@&B:#1z?+\f}aIG\3w\UΪbaDEm9ϼgJ ȷ#;"-]kU?d';4;$Zف)#tOℬ+S0rTU#;lRmT"+a1ӆT\Plņz?XLJ 6uT, zBp\-(1}j!N+qZQH,z4z42n<{!őL< r?|dqi ꇲcB~E#3(s9Us)RqN78ȑԈ;tk^ )ͧ d=7YPd!;M';@d@T&0hs j 6Q'<8;$ "cgI.+_NEFt^e|}ճ*wX#<Z|pӟ jv7/7^gL';X{-!Jvhr e9ف*<==T`TĢjRs,nmtouq҃c פ\CW%aÇ~^U[Yd al?";W?Z]NvP v+vɂ-`۪cݬbmUm6wl!z vέw17C$uPh*K!B܉JH|ThgϞ0Mw0;;FkC.i#z^,}(Nx.̟.{ٽyNKJ 4$.C*bkpl/q#0(7Or࡫m8M:DWɁYYM!I ɫDb ѼXFᡴ=ҪVANiE Cf]|%;@ᙁ@~eifkNX:]4S&jcwZ=";i8۫M<>wR9ʃ7i=1L }y{2(3ͦPHoY+;zuͶN\ܴw|h#.R8 NdKk;ӱߢmb^@$r ;YŶeb2nVQzLPY745bNg/?skMHdUC -~SxOrH?9dJl#(c<j\yXV?'ٳg166F_!Gvcpa"Xtuӛ#FFF *pn11gWMn?ꐊ!yM5Q <ԖCrCthOڗ <̲c; V FAJ.Mqӿ\?V*<ڷH\j&;BDv tdxxRSy於-eEzJ:2|:Cטz簐m;1";@ ]\#,yI;Od0ڑxTwU^q߈ ?ɹ?QϨVdzJشkX>>Ͼ-,5TWX\"9A۪y<ozs(!bPմ:`޼qS="; Ͻ%:9G_t7o$M0==nvqx ų&)<pH{Mx$H-C~ 󊍿jNj5P2o׼ϟ?O OCH@ y>|M\p&D(АX_g Rsh!噀5!Vw,JIs"9P@l^qjr~.[_=544D wO@>OIC_T믿)`E o6̟]{H*ٯ1 זh99\);-fFt؅}FLia16~=vLhdIٶyO +<`pp0ri"Wiz;eׁ-B( |+Է˱ׯ_GX찛%b>#z^,M0<<a7Jw>X>1!YF DxcܑDrxNΔ/j9IzCB, xǾeb`d_lĐʇEa"A*%JvӟyEY'ϗכTh~՞Dv@G?ɖf"%;0LmDvH<~l Oo>kn.$>Fڣ&`Cu'IcǑl)M9@:ȅLv̟ m0a(T;!uCtڵ+!9! 0W}8 WK6׶`6I1W<ş~O?447R5"9^[探ܟtcW^(gP*0>>ޝd]īЎ~Sccct8"*=;w.O滗a?k>):-}&x<. cDrxNҔ{ 'Ax@qZZp-᡺AD%(<`G!dz{8jWJX\4ߠ ; YK\oY.t"Ёd$"C7c@DGߋ[a^xwH6|/B'{0AH= ЖC/_"{m(fK,dUn~o,w$D55z |P[{UUw-ַqj :|VP4:* zFN =HDrh,#헒CXAP5I!D6-ú5o曢.Ο?I׊`||7oƍ(5k1`5c||R&K(~:.^^4m{0m?] ^>TZELV%# xS(3wBJ᡾8u#-PrС$HLz|)C'Xm7-`+{D|*QxHaXEL}<,WpwoQoC.hrבZ]d%A%i/DvP/)eemKR`!Ded: xy{ެ~N~ΞM !up_$;Z,V7fv~p 踻Y\eXT 52WűAuNa lQP_Lzov5Vz/E%8$ J iF"$&Rs830`[pHr oE0;;~ڸС/zG@ItM")EK.ƍ(}&/߅̟.?uXs?y'‘mMVpB|wN!3M9s`qE"GH!I/eP!7[BbW]q@h1sE.@9 'a8WvVO6VvqPsrA× SU&e̓b19JzS) +eO0*\,kB}\џJC]p저 1";6tqÏ(CvhMym- PmCiÿ,8jժ`|fu {~hDqT/v$ ?Ps:μ-4}32L<RrXxE)R@j7p2zkjbbSSS4׽U';t#8y'=曘I!0??sEc]XscGэ&1ʯTOq" .&s_$T*IZ-R݅0{OjpA*eY/dz8=+[%wd;^,Ov`2|i:&A~/5C0;e_~E!psyمC c8uH|9#fuM̎0~seiϺGvح;~{_U OvTfyW7Y+5ҽ5'դbp*AP-NH[P+.^Hd1;;+_Xeþ/IcG$/zp(~:nܸh|·a?۰7MJiX+A2o"sbNh I': GZ2 SZ9촲PC %v"~JK{A~_GUa`.;U쯨3ف";0(;!Q|QNhWc&وl$3!,^(RUYWyOlĜn`om?Ha>P?t]>nJIQvi#G]tdRxH1]U<= aU&Ucœ*+pZ5ߜTD.@}[҆.n0H!51x5g{0o^y?Y>`WuSPիWq% BBc9Rjm | G貱:U{x" ?B2P}XAU>t@)oG+=+:ÓߺvB*Ш/5/\&Yo M,TFHC٫`:v8o4&''i:+Q߉#;$LG^vS K">) {0o6o| r'?}I&wDrxNڔ #,s,sXܢ}*IznѮ!ID}Py~DzaʽSG0A,ý׶_!;8N$;gꪠ0tHd.N(aܹ#mrR&eg0p(Lݷ@|";G.D+d_v [_P0p=爼rWO+0r9:Rm+\'ZОOXCDvc7>fخ(Uj(kت82]/DvH e2로WDm 3 acM9$>EψڧlռML=zYFc y=# ,Қg}:Hi`LP7cd_ЯghF(vs6w4#=P/u5H<ညCҒuooՑKa `,}K0-v󘜜C\bwVxh65Ub !=555\|###kW?QuMU/ryTB(UUZC$p4ZtB @W% EW9u+]wI>0W\IaCIfL\Y G8b6``Y/I!#h}D%l<vOm{Tv>--,oUQ\`LAnají*J*%8K_Ns":$2 +޾{I\թZFaddD-A >bnnN}>o|w`; vgوSBFme&]c8B?;s21BF<maނ0_w; |7n$:뿄% vZP{Fe \hQf ~)6G6qCZAmÁA_4iTaf8n;جXٮ´m١FvȪ;k>N${U7   ?={VU_hzGwnDl$6 Gth#ȑ@)>qC >zpJolڂٺ$,p c;|.(kpTmAlFU-/2jåk%6%;໾Dv`n!;fӧjg;k ja4t0 V6cQreB냒RybSRqQcV ĆЏzXcB&;$s";K!Rl܃ጦ{KZӃnah'}}[='5Kzثb"8xKa ꗚ5(J9=<@j5o/ݯY>`W" `zzZN4d3&DS."<$iի@PPn6o|"̟'؋/Utg? HGDrxNԔwGdx.+z}?[ݮ2 KTxj5 vS(ٞ@a!%uB,rHͻ`>)Uͦ<c0p@!dUxqz5 .ÏDo*܎˩<|gwBoˬ:֛M"1TېXq+"(Q"y";2W{%-UۈG idT2LGG?c/{Ù^~@́]CT"o<@IŁfgxzN?.>|9t1|+0oG>RCPŋ177)$O(d.]*K:'CԺBX,brrSSS(JqΟ?/+߃hO߄5pj>1/8ˈ_ɳ t{@"Y/`*xŗ1pZ2 XU"tݮgU5-Dx@.~oy ; pn<;<";Aj\.y2hV&`Y9fF݁jh6ђFso۹|Ɩi6pd-T:x _4M])EvF`Od`W?ƒ]#헲C~%u\I5mCr( fgg199IS gV`'5ՖP^(AH199ׯsk׮R{^̷̟gGvgWk/8EBoRqL&RG_' Wݪ:-ն-jcm}=6H:Cd)j <Z{F%q|w:nc6ف/8h%ͺԴ2tY6@ C>Kɕƒ )M5$&;0w=_Ro/[bAv!.y'!6zrX2ƄIi#8{&*h`(Yt;U zF~@MuU~7H+E8ζ]HGE ˘FGGiu)Dž8rAwU[Be@h^\r󘙙˗qYgq}uG>nю޽8!js̐q|#Cs.2QӬOBAT! r" _IJ X]ߠf)rBX{~;,,CZ U?7Jns`/ǠmmlX!;fYpOv4@ץ=mm%CwtTd*-!X@ޛJ U DUdI A^wۏV0pqS+_}a7$CnCG^]}㷋Iréz^i`H1y-tiiўsm6̽O,# 9ADtY2!‚%ݺ۪۞1K[~*>tR\~tgٸs(X١CsJ`O m=d w(١iݽCb߽PMdc#G##=|)H7 ز ;A~7Bo >goPGIF _T,rE!6x4&mH!q)Mѫb )OH:C,z42FthcSz旼 ,( vu`W#rܯ+9$֎*I}4:h)YQ###z5L؋aƭ] A!DFaddQ(JfffpE%ԙR_4 QMXw_RLuByAh]dbu'۩<4ܕAwFm=ZJ/"<;m< ƒ**<@x?.kOهZzNܭfP[O6O!.Tв 1FRze0#1Bo*ϟ6MB$<L"AF`y#"|[zJvW+;0cc";ļ Rv tٸ*+a)Jeq,ݳC,rۇcg_T3 |s}{soA:!$C$IIgx3n ;X~U)CP177YLNNeBKȐ`لZd}ℇqiҥK^sΩ`Wކ5^#yC0!.)b|::A1 2*@ACN@_|iRy YH pi ~DGTvwUړВ_vw{hk]Cs'0fMkKz`-04_+";$Ça/`Wsڵk(Jr i:Br;x ؇㞎unLFၔ`~axx~:JnܸchhHr/W`NgN!ܜH>O4*"\& v>|XaO)JP /@x81 '> ]$Č < NZYuQC h . i$RœRmܸځ{gζq u.9A:Nv`6~O&\vdƀ\ʠ$XӨUy'*aY>F}iψ@߈Ϳ%Rv a(YL%ǪCk<9DXU=X~_W044˗/cnn$Ν;'\Y~%T<Qw8{,MB(ƅ pupq5LLLy0aswv<}->boɜ Us2;Y!"x?>z'Ee.Z#u#1|%qg"<ۨ*<U cyv` qCkͶ١ c! ١t/ipU6_rFF𤇃=z7][u3s gsPAӀqIJ珽2{/zHM> TyV2`. y]zd$\x$!<!s";摿KB7R wo ',ۋў:󪷡^_QCz4 TVM$'C҇ Ct ÁvI7/|`)-P(`bb7n<\Q81>>.1Vˋ u5(@ Ņ 0;;5\z( Jކ5M\Q|Cq-P\p"<V9Ba83~RCXƒS`ow6 }(H!D_K?XH/cVś%: ?<.t毃oΫLo}p Jm$&֦' s>~%< VXM@r^T*.vI׮]CT]@&a Q< /lKTCX$P*033ϫPW`]KHʝ-C"M C 8PG_QxS(uN[BP‡&@~EiAggǏ@W6̑g厫ڵH  nNِɋAƒ[nɑ0{XdWOГIHo}4Re0xlZ|} =0ןöi|XyZ2`?`ChXsTL >*cis{翍+XTUXI ] d%\W$cȧ Ho|<=Y ПMCט/v١tOhܩSR|ֻT;шslM,oWYfYOs=mXӲQ(X.R}3_ \,I!6},FһW@ kԙW1-7vov5E͏yߜ/?cՑ+kuS T2"$m`DDr !fgfrָә`B$_9]K)ք2UZ|}߹kޖReQẂ0ÍH4P9eU4C#-j@n{I?N\3>Q)V*wY;!Q['skNIh|I4'Xr!4+k i/m+Ad HLϝ'=<*1?lfx١nXޮ`V?HІV_*q';YAɵތQm闾LA> *_ 1UV;`ֶJ x[G"9cQXo%cl}o^B"9 l94 "<sr 177˗/+\뫰W~oa/MWcڷ7W(H >'C=N&RGFy?~qGZ!8_OS}RuP*'DtY<`ɴcî~} ܯoaî% ӵhdq羀./K¹# uq;n=l{ "8KÑVT0<<8GbQ*Ɂ@2Fo aJÁwOBr .`zzkkkv&&&P()%߹k 8+ڻ3-%B#Cs( ??(8dn؁*pݯ_##d?UUF/˨;| "155R\x###jg{UK0PXaR:{$eXv"9|NڒCN<g'~Vy/6'؇>T>+\"9.H8@ OAEU +r{O0zRܭm~{eڷaw$;Զ5|&% t2{HDv | R3g eCx[֥Q6Dw$ 9N+i'qVЗ׻*AŕW҆Rd|Nd{fG ";My^Xsl,6h#G?ͷ^~qjnoߜ& )9ijA."?zeEYuk`:shhϟ ̛ymZ ]mBZ^(cO tfggWܹ7|wPS+$JDz@7嗱ͼ&ٞԐ;BP5~Jci5ˡdJlnBc!@7{n{߷%gnrv%5u Q~ yohp~sA(#F{H q(j6VP߿){rs-˾ȸ,ƒMz"<21Π:}ar!?w?d.?ֆ[+w9 yOS9 ۢ0w0gNJQ~&gb9_=PUdR mf\ x8cy8v9ƌ>:NȇRJ.;*XR h6 8N}xJꝿNg r9k9m9-sP% 7ˎr$@?;I콙O8 :h (c[7Qrb8w!_ 4>|9(6=a uj9uǃbb&k,Qtzh2X,k,plkޙbJNN <;Xp1Wm!{Ͽ V*{QCJnࡌ;+ikJ^?кooIrC Ūߩ=Hmm=aUFJy6_Ʒn_B4qJS\4 ;l1؁3տ́+8cMx@UЃߞ9zP^0J,PVa>mvI2UD-G2yevsۣ9vгjkfOe/KI`T0tr!я#̢-ǠN~oh`oXfn\š) @j`Mqsi3d*gξ i weA+;V!1DQ @rT(`1`5Ht>r|0pEhW^yOnҭ0EHw lZq󚚾MNՔ9s<5yv٣,HFu#6Wm= г{7s}F sߨ mfq: <vsrpQF2 ڨ;;ae#M vιmq˚qַ";4I*ʪ^H#hehW 9X;.*E"AsQH7cױ5Yڽp7P|o%̤coI4=Es7s 500ab1\zgϞEOO+䠦A[A[uF0V pYU3:FJ6Fvant$n%H lv ۵"sxx>(/߹c JMP6 FU۩?@AVGwvEӠa`=CͰgmvpwtUȷ+`;$iW'4۱cz[RSj"l|l -?#C绦\PR.=ge& אGucԩS8<Dr 5BKb@k $(qy>}`'!/ O-5@Y77-Stp%ȁ%4NA>m\{-?x9;)c  ȧR:R6ky ր"M+ta[s<97<@JMCSe:Tϭؾ]x`첿/xU "\! Rs:Sv2C\8 vX^/ I4`R^*<ٵ D!ؿ0n^'&zk*nmntm.Ȳu6[ ; vP}ۄ*;Pt;hf[ >;pi78>1D3xޏ g {{ߠW">8.0$ڊvi.m15#9ӗVH@} WwN șI$._tmotU\ =e ]X&DMPDQ #`ddF.](9k|1meNA47Yqs'Lp :ҾNnoؘvOB x=Bq}ky3C9Y-|kRyxGQj=iaN㝻7ջ޷J>x Bh4ISqdYv\>~8^D}d~gra[rZMe3VP)CN6tA͝A"Ґχp{{g-ԟ:b*#Ig΄AYagJ!R CQ%C ?3ۏ`G=f,ìYC#uTGsG~}B*y7"9.Qh*9(kv%9I4\2 ~2 (߇4r;_2#9\x;H400>3du[>t`$D^;whccc8kKYI$k188/B4+xgqᆨV\26WPk(,_<=kЊ YMSC pNU4ig#rXߠZ9 t9rJμ n`r>Vր<8CJn1{ѱos:=Qu06u /{gevX(H2 >XrvXx+$Rv"o>֠@0N*KIS갪D"K:Cau\ zp d/{'|3$'.!яO#(#6:uvVh h[73555v9f6]2X ^c6 ParG۟|/ ]B9He$ʃh0K3C98V]Ŝ](&[ϝ;X,1={"P_[Q̿u6&AMFͪX,f:C:Avۛx!#Cʢr{`eGs>c,o.G<4E[}mGb }_ķQJV]+}! ak_6J@H/ Mڛ2ᎹQ Gx`QvP5 YYBZ0a>b! NV˶Ӂ18;ذwIdZbCdvȇmVVt7{hA0|隲TcvNdL6jơ%Gerkuh ,@;tUF.eQkL@UGC__A$ 2r^e` `Ž#e6]0I$y{ǡ!D",..W^ӧ Eة!n}/m{fܲh9s}8܌gg.pZM %Nl'1KZ=dfEWU7h(ZjSνt aWz#<ح3s'@']G o2?|m܊//e`їmЃM g KY;)evXAjm"ֆ9\4CFkLQSʝ u> pg$UWu rpSʮKZ- 9ٳC$!ȁD2AF"<@A`СH]ɜuUDrB0<ƶ2BO:%%gK[OPW`ۆao[r<9 vi3P?Ed&Nvv,ro5q>wטy}b9fZvFj`Hw9(g2(( |oY1N,G `G~N-T^svhN6Iűe7ǙCO" WLBXʺ\dOKwC:r++>>p L _U7LTYUVHckꗀpZcU>|7H( ###Lԅ]Z{%v;HW̙3H&x"".^T*H͵V}ȗm6#y@=6;8M8SMCa۶o/hȾ{ ]]i/-;6xof9p b:ѷ~A@MzpC0|cƈ=9zP#MU-/: W`j$PH Zzp꘎0 @{J@WhIYsiU/r E^xmF<}j[V2P@̤l 100AK"٠9m1 uZR:o-D" x`S$aO$p͸!ȁdFNOuNt #<̚|jI3l)w,ӊkW*U'Vx^ǥo\`J#瘀ㇿA]3\?|Gx:.ʋ"ݠT`,DO9(;69xS堉5!#Y,yGyhxD~>9L6h"]{LH<1Dj;l6[#iNPbm АPu_m{ξbi;8}l>{9Td:CbIon<4^7?x+!_})`N$kUm9Vr3*W WVٰ? k kY{ pOQln/ I/ہF瑳%aiU@9ήNZH$s /6d9 v]tGMZvMԸsbD"p)erјHۃ@b2#YzZj6>P5@Fi%S)AS}q.]3*Mk&^n/P#':чӨRj! צyu1uۗx|AUae?zC`ƭfaGKn?r<ى3qSjѣ|zvMs;f M+zTrmX\_qA^AИê<"P( ;X0.* ,Οxs(٫\~4({!*Bm~ۺ4~/s,{?[;@b5в"5UrZ'В?x6AM݀4( ȹH:i`8~i-g7H.P8!EtRCihh:Qd@\XmcJs-ۏCMi}ÿ9 Iff4H =?bD뾽v$I3B뚷8ǾDζ<8#XlX`if)4|]Gds19a_%͛æP,B{6p{;b5X0 0@f|\~s*fq#=S+j m=z V# ;ϕxEN:ڢO(epkl0{>-sm qX SBG#~`]!ٷٷ g5jfEpTW"Bpmu> .·LaDy ZL&y {1W{^́ hK7kÇ]ڣ7HW_eJ.ݴxhfM{D"O]ۻFD"QӧOd@h9 '32W=(Ɍdg3u/uxoFL̢qSʗ }"I<κ/ec7z#<*t<'=dcǐ7TׄÿG }UՋvP,^Cm vɫj[6:HHKG2gNx4+%yt֞ xhءvlQ/؁_>v:(J:Ԫ`=`p5 v8 vpx~T h=p<W{8^]e}ur3Ц^::Gw ܰCMBK SԅR$ɿb5DHU@Li(]@ҡSN1_2Pa@Z!Qސu᪣ 100 z$9`NAbk̾_DrBրX, wVdĞ;w:&qdFw@b2# 'zK+jU$=~,]gJag:dFˮNGKj=*/' mh:^PV'*tdYvd?3Jy(EɊEBSf"s_ٵ9 Rz0CT[;lpgsR#o-;ǟ ul5|!k0MNɓjΑh* 2 >?|i_6F2U/uJiײP2wߪmP@ x)Þw@QoL68ippr0P {Pjj»%С0x;>}z%<Le'ML6h)@ӯ$"i )Ab12B3us3,^0ZiIZXaH0^~\߳m$T4< sN?._]ʰZW8MK-yQ$\ ;lI?PҶESUQȊu^XvlՅ+sF.(@нoS# X.X{KZ7g~_;5\J<b6R8'9~ARDCzlQlARm5145sڱy,5RxҪ24(#{YZEv &󓁮8S[%[6 qʭ <'d\:.0  544s!JsPn4l{zz088F"@x囀j*@շ=-CI"Hz狍sF,C4E4E$A4eA bhh&(&FǓdg'3gx(jwu ?vYQfDx m۷RH,luphzfXO~_7ec|C[lwww<X]:ealX`i-j3 <!d'߅Z0O~=8KǤa zVmQ؁cs>h> 3URI~I~CWDKSb4Hj'b(џ,~*FZ)7y݁Vm l/"vZ<+E2vU}Z'aq"Pj}R-@˰jl/a )kKBu=?9RƜ\iVB! ^h6@"Wҩ#w/7 E!D"U8F8޴.$5"L ۋp8~!M\˭B5${uV\`k1$3.SD Ve2xɉmQ˶P@'9[ &7C})N@=~c>/"pGTx ]>];nK[*n+Y6F|Y$#]~]4 򲂠 %~iuH / c?O"ڄ 64yC@,jae < ;pךr,t| ݃iUBC vh궸^vEv$4NH4a\ 71%;7ǡ|M(5p=.]8Ye@b10 5C# yt)Li/ݿ>mO{<{߅̜D"P(->ŀ -A5mf$;7?s'1˵4P@7HV,IciiirWZ'oM?媻BoWt소 x.9Ir^bjX!7TΨdmcCf$ BnA^Vj7m#];o܎6`=Ay˱v5#hV\ ~έ%`O9iBzsp\[}8sID{?ş{ vGPYr |Mhɟv-&WW3Y ###4N 0<(4|]GjrߣN])o?]B !a vضR #>lb9/[4`1["=uՅ+sm{՟9zn2G՜ MDQJ`]q|("/ˆ6}>D;\ zt֨.TUǹI}Nh,Z!яapcMBCϋ8Ax(.ekiCyUF6YS*5~ \!G> .PsOf;ŵgJD$pi\pa ߏ  QǒHM:=sƗ_KZ`ͯ^KD5D"%#8A[KQn2%-Zxr3I6RgJE׉oPT,OO5%e-,,3udeq{0U6+lG!<,LCO34 Ow?|f5ӹMuQԷ!ZT5a 찶`i@*_Į棄E u¼<|!>Fd&ݵe|~MQKq:)}n et}*^>ZF>p#qn@j́-;Q#;O"@Er t@ [?y{<xD@EJ.`QΗ&loӺ"cXCҙΉ@ 䫿ox,5lxFtYQΝC4m===k7Dp8Çc||߅;{ 48_W}9s}7tX$.b1s`CEx ȁ63Ϝ wS:5vzԙ$e$Crb ֽ{mEa9|*0XS>pAd!eH[8evBubշ47=8.}=B&˔NJ<X 衠_x]> UP3ss`ija^/ M$+*2E ^Hb_#ގU؁xv190I[ <@q~ Mj>B㷑S$~# bߏ.jtP'(3Lt`dE-]y|m (&!/J6(E )*<p޶4ֽ-'5a'^P^;(p+Axxxnenzd.\bNq3׮\w~̴sfly]uдq[<;<|(@VUd $AWD)?6b/Z~{ vepd/Kp@>~!5OB˧0<gzii wŧ?i<ef!)}KfHd D"@R@N`H%+PW$#Ā NJɂ ?G2D(yu[ꫯ^~__ZH$# n6M]oA= .pB/q V '&H$i ,]v0*RCČd-ڏg!˥ P ޿j-TࡵP eʜu9$Ht2i^FD"K"< <@ qMZ幕Vd{@r8o<$'&G2?_'`j =FaP?x%8㖆iZ(k%{(ɭê}P9IS~534^p>Ǎd?fhT@*[mb`ǩX d4n%~gXtU[- 5$)* tA|&lE=mPC]#驃뜑$pzT;6ruPU@`/[WCgOϋ;8!V%xm;|NC 8Xn  k*:џWG%>ǖ]~=򕯬qȲl0pD"h7x}} +g~ -=VDfgn$x3d0i@XÅu7(enUՐ-%AKt^4d $Bvǩ.kf"M Ι,#;lTg{q[lVeHKӦDeبCv]~Yß\#x1~AGQUijYijY<~? ]?dP#yWEwB-yp*B,+ X+AYo[NyA_eWw'C27<$4SfYD c b%t3cUU7ê"v"pZ<"^Om8}ҌKz [a4(8);Ѧ?vŹmq޷v TWGIM`x.^Z.\__ _zꩵr p]/%Iw:=hCxsB9`h4THʵPoa ahhdhbUT/B! ̙38s .\`OJj2e?pگmPΜ9~rXDrFFFX<45衱=f$;7?T {r Nl'w 9G Zq)Yzv!H qC!GH$,ߋ#-O'r!o(|*mvڋ7E+ 46[}NW60oO4u|~Lb9]f!EG둟7)tZ;_Z8ͳ2 " 8zeɜ\ŢԤv٥ߢU.ꎗjv[U4#Tž#.?-I2!H߼Cލ?$`"z,ā62jQ9V(<- ؁sU b/ˢ/pOpd/k.`R˭~Unh¹{?5J IDAT9r/lT pI~M`gp'!4 l}?Crҿ`ϊH$iuܹsF*wo~O\0"Ё@DjEQ|e[3;N࿭g!䏡e2wo;AAr%Li?8/?_'y/׿k߼_f/8S˓0DXYOSyCuWeJyHKӦEx_[y |pM6UH5?MnBrx~4 RdgfN륊N5! x(+%{Ci^\j61.ΏYbW?mݍ2F;iIT ?<@X\5vЗ!&->,Uq ɥ ٢J,_2P{B*\l?watU^VU s5<U˩%]u䬝-ml vءдkgOt7NBj&5pk?B!я3@OKu:~7~Ep$I[ҨO@񗠌}ǀF$5E$9D"HhΜ9d2٩d0{%@~H$d >V jHvn*C5#!9NF E3PU wX! ࡼŽUՒr9ƞcUU)%h°=mk+*֝>@q24D>{37tn!pLvpFٲldE9\4S4z"Md䋆\7Dvn;p% &տvnqbtYb{9P됽HGY\.~?۷ȧiDQ\~|RS4zD 52䫿묦g1v5Fpi!H$D,gߊZ\<#wmt H$2@A]G~K :H$Nts@CNx{TAhir)dU,BAW~χ}{TJm{x(>'VJnz<\/VrIˢ<)|6OM%ܘ[0,@jzvWLؤCߦU6 Pׇp{>q萡*};`qޒjFqdү(V92ξi5\s8s.k}f(l`mV9;Թ gE08_H-u\>Eeոp~}[c$g!_Sӱ ȡlRH$`G$N>m_Jj2\S:H$29oKx{Hvn*vxw@r/&uD'v]w~C26@&pLSM 2ê̄73{d0`T ޽wӈg2>T;8/;X*^2<B)( j-}!Fh0aUy]CFĄͽ,?0_DZXh~jf <9< D"{7Nן2:~ 79pCIuZrHD"٢p8agE0l5q i,k&$DrZaҵ?:#W!.A \qsJ6FFr/cNqa->ql*!i[TՑv2X0ݻ˘f-0N#-Ie! QT=9%tXFGG穿6zz/@K75paܫ+znp8LK"H p$I?oN@+ ߻t H$e$ö7%ko)Ɍdgt.p?s25?MHrx/8CzvK3׮'x DL2V?4P٠A3+tc'ia U)iɞͅ t>;IdEJ| d.4iȪjkRtZ[r8t.i<;G<;y7v#3fS;Acߏ@`6n(ONX&=jPN:DasI`-j]w(Rz<ߺ4C^6z#&{HM CO9,)_g>ܾ}{g,#˸~:$ךq5qYGq(7<5k㉀D"Ab1?}}}c/8{P~$DrFFF9.xw.F0A$&3ʟ]Fjo'. [| zxPy l'ڳy-&p#H[cNpD$ϙn$!d5 }gyǻ:~j8mߦ f&B[Fu(W,?Zmq4Ξ q&*a?bu繶?x9Οavvg1<=3}>2b<8*n,!t8xҁi%C|.܊ I9a;V`l?R&Nۓ̂9Ȋ$CeeY:YlXTAxph﷥Od CgNx̆_\ʵ([ǟܣ-E z8,fVNfb_02<1\4a>xVl6$ [8~a&D{{p""sj9x)p;\7ߠcow;Qsc7Y;ɅcQvfn7 l>oY^}}2'懋(z)v8]rv!9O5 yn:Vea,@̷aLE4&A @%a68+= Esd|.lA_Frى {]Hd"C8F2񞚚C&lzStɸ:I@AXImm-ى;voӃ@ \ٶm9;9ǿe]߂ XFsIsܔ( G%jWJfd4H i7nd oƫ;ڃ7jS?Ձӿ~o# #A{*(H3ԚY9 fRoˏfΩl[ mv޿7@pkHHvY:7P;g}J v\'cik.>>xC ,ndٌꃔ|YN!v“B,.sBD8 ˴c~e,,y*]?u)1IDxd6cs,E^2,PGX(QI` +0x18苉b;XaCGfmJW$v h2QiLbHJceGᙫnGCtd=>bk8w܄dbBl!_bm# Pcg q%A}Ů]AQWz6ŭa$?S<pk,j<4a;N'*׮Eߣg՝?_ўCO Fxk}\XްYQ Wsh-rBm[u,0HyVl6&!ȅ(ZOv!-Ɖ?YAJprptw#<< YQ!10(w:\my =2pp, C!@ljEׄ(倃?uY<c*]Y^:HbԔvc#S:G5g<>x:9$ ke;30lPfN APۊ"f(̜‡ʵm ;.>߿w}7:::.1!~(W0a5~҂EA@ VDQAg}fc28=ox605' fRc nE#7ûUU[u/@I2$pT4_vÇpBT@' s9KA8Y!&05%y,IYF93yf=d1NCˉJ>0WuA as&1W(t3xql v6(("0 FgT1gzbxC,իWd|=OXxgDAaKގ^z)‡۷A鴴LonJ#w&zָ\RL"P`ˌ@J %S~`\r._ayc{p'S{.q=<8˃ys(眓>|/E axz<@ݼYJy8z~02/L{X>H2$YS@bPvL1 ܎ =uз^*O*mz<؊u[L{$ά&'EQџLsxFq.BWlYeL` Àf; vX̟Y[:&bE-|x&_̜‡֚-d{ؿ?nttddc$;C \0᧡F1I@AhooGgg'onqeC" $mmmĎ; `oу1 \R\m]dyPi*u!zhPvMJp/vclW `ypI0l4m *0ѵvl^Ss㦴/3۷*''by'|)5׃a9$b IDATuup iI{< H :Ty8s :Xu o3)Myȳ,ey8ׇPyebhēU ^pEEWl$9ֲ(Мu .Yɥۈ,ed瘾2&NbK1Ke@Ih . oѼqg_Ӄ|+ػwoeVZUqD^s>H-pDd/3Om6Ё 3@DCC!uJΣ[FsI3LGO(i~&`]K5}M.{ߵ]N"%SxgS{4Nٖ9mMZe\ǹۆKp ][JL"ߙϊۤ,ቮww815;ЌdRFpEDFGib =zH!6.OJA50:o=ׇ]gLBL1Kl*;١|Ebnk,=Ը 3ى sN G?l#9 S<<& , innF8c=$t  [d:Nj}2}f6jF)wPO5ya+G%:Lrӳ;~y MB HYuS)[Tؔe`8s&N[MdEB][+x%\+ 8:Ғ2Kpk`>c‡d:c} Z)=, YsMUU$D 4I$&I 'HIi pb7etӧM҉ADQʣ`i1ϱ(:ߓ)iX!T(C@f9/vVذ @I ev0x>3]r'>v'㹱LV͙a1BK6f{/;>(Ho_=:'UPADI܌vKضmی~?  p6mqtG7UtHP(QR͠f D'C|&ꖒ)~ yբ %CT~V`eWvo$C!6!>5nom,9>Sʗs!u5`:]'h [S$3qa,2 J!񢶼.e3DPYG(pUE\OK|$dI1#xp,<ea82%HJrFI祃>ruV'Oc_kZB}a,ʓmH[, '7ǚ4Xr;@\0B`؁~+Ysd|u;XU~AT$ ʐU >΁J a /QkD9nXO.%!< \n{V̳ɎV#nDۺOj-O>!)z!D"477x}˖-ؿ?-)#7ҝRJ^]E~?CDA,hllB!aRV- hooGSSۧ55G \5{\ QR͠DO(i~SXv-l] f#vGk/BT_Q;(}g )gȋ ӾaT ֮՟諹0TíOJ7'Ho킘об ޕ7f1#PR1꼹2 .7V5a8^@}e///ᓟ$ͯO }d'~ʙu}gDADAAQB477c9??^ C DI5:0ɇh[t# 4۠$/@>jO<ljoa۶m$v   (Aڰ}#/B -O/Qu6juP;z<4{ű G.{ߵN>)b׃k>_5nJ0^v-7ǝ߽7!sB\v Zbs㦶ѳ(`^˖`1idXpT4ӱ![W!-O0WL$pD&qqз?ʁxZH2 "0H7p2 I1)F?._ʒ00:xAW3zP&Izx_QNϼ2?,N+}B2T,Pf M3!<ssg.C?YmhÐLQ:b~1M@Re0&ruS;Qwp<|A+\~dK"$ $cH1(%ceN>#0kٟw\u;1_wvvFGG۲e ͳɟ; %V>~QAAA(FvCMĂn$?d J`t`` =}D)ţ ށ/hO9q8mU$$"ǡ*RGhiO RT̼`8n髿]_U}v\ `Xސw ~~$,.v,KpytD'GlIze"C{lަIQh %:, jʨ(ǎ}q<±DǒL*PXYVj.2+X+M`T6Df'ba*v0v;kV@ũEP19b5¶Y`CË.?VX,C,"/51,AT}ȈK"jnj BޅMņ;!zغu+^/ͷCqQ3&MjCvV     9]NP(QR͠ PP45 [aaՇ?dhvb>f|? NкWpG sGIŐn%g{<ꂥC6MeՕ]oUVAn y_5<+6u ?Μă!2ñ,\8D&oQ0 d/zAJ1OA1޽()IM"-+yiT\rUģ ^DL;LU N@= ap^gL{zgKHP86//=^5CAb Ȉ(Y&I"ua)HiaNfl.?BޅK6Dsr;iMC~2avOkMM ZZZ(X   ٳ6m]^,9R~8@T3' R\ưv]?=\d9=La*JJ0͎s,0kvcϾGIpLNCѰE N7׿$v 9!`xo88+WsJvnafY*v$E_lup ExQDJ*L vUF"dE)Xچ^"x&S%q#B}ՑEa;t:) ~Qlf&6*J<94k nF^%EQ2'w{6K-ZmE*"QM6 rin؁(u= 8^fN@ZO 44&M޷c=9yp7/) E䓏B=z   znӿD^4=g9'-_{鵲̿3'ӗ6ju0>T)U× 8*>|.{ߵ9AJp'0cXV4'y =8uOwB(_ Vp3]Ӕalv] 7ˁS)k͟\N AelT,"a}T e}rx{+!'iC|gxCèqQt)_b$% X 큡T5)C6!9F. xzp߂6;8?ɴOd92߇ >HAiE@:.wfW++JFrStj#:KW\a.`̫e3񨪗PvAM~(u ouAAA|ۗƈ@z`O95- w?e](}:9U~`8Pm.V= H#``K>H#Ӊx'xWnc{p?@b`8|_5nWp=>}KPĴq(89n<JYRc2/ZҶE_oUVIŐزN<+6UVCo.\@2 (XsdEERQ@eaH 'Ӗ'X-rS|ǪUTo-E>)}:&814h+ Ng ;O#:O_ I0t'j ƚ0A,d8Y V8g}/QK|&g@R||YV \}!&h.6=cg!纡j{Q@:wk.   b^Z[[Px!ȋtH'v"Ɨ~>vC~as0fng]]l,Λ ,2]p9َ]ǟ@rȘW?xnv~} ף4oc%$xDGp.,2ߕov*"&=,:}ٝ/>lh*`b"}ѴXXUd}uazO ᡷpI.OĜT -iiI젭}(6C!8;͆f;6lbz)*7V+Pc +]u/BˏrT 8)A`Y<@&Dy.#|003C4-˯3nw|2 w^xS@:mHGun hjj    dϞ=x0ngCǾB&wBnv!2 uжZlɿ`]Ku_~yNv/9C? )2ow>x?WNt7v"'},.n.8Eۅ6./}. Y£iWRt EqEHĥ։DL, k;jao?G[;U,߸|jr"jjRoF$xDm-V:2< Gz-iׂ,ko@݋Hzca2s x e oxHIPf2=dIZV~0* aiAaU7~QtĢz>4̞U`AK;T5H:7k<} C^ot&}x>ءm؁=Has,C!|(pY#Co17||i~]g C~oO5Lc466R AAAY؈;vlLFA:C_xNrs <@ sn0ng/Vأg_hH^}fۭ>w'W*kVJ+iOP[h fʗk.s.%ms.߾Uwt'1i *MH7Ӟ$U~t <w\pl֙\.%&v.PY\׿^A_QQo%3 `уb)A8)ۃlU|N.uF`W~0I` U2E;>_Pfm؁y7 <-].doo$vF,x'? g&5ԁ=h8D߁<䷿/B:tӏa:555݁   Ů]@ϧD v!}/!ҩGH߫,A[@?ekt;4Г!zVER2|3+kpw N feyP8WI; l^2EkY}Օ\6s"!9n jRq1|Cd{>@@o7´SPf&ompq yAǃuu㫯Cg0k=@JD)7;$v0O}1\L\4;_ة7g;Q}H@㈪A)~)~)"y\aax]ehz)GX@j`\=&.?c@6B ?z3P. /2^#HGPa.h   ]ٳ6m"G̵Ks_̈ ^eq/<(ၰ CR2Ï?Swŝޏ+j ߫n7\l:UJiB)?-eRQ0e* _E*6 kz2D9,j[~#r"`\fH9u }$u(s ALqurY`Ie G6| y.YroC$Z_"qH9^rV!ׇ}CCx譃k-u$9dR篘Ce9?s!5/ԗb;LL5=))[ֳLZ0`juܥ J`4^-`L<*iMb{[x${0cH[r3>y=wrR+(puBK) H2a, 鶮|yNj.$ L :z, zؾ};ZZZh  D(B4'a;v $A$b{ `p+**Аrڵ?N%rBZZZCtHi֤ =%ϊͶT'[6^`mOP_Ye3ADžDmBYEld_sf &zMuPRNqfllzz1W\Wd=0߆alkC;cbb#ƤT_d@4'6J8ev-t~)~1C|옐-^~B1".巋Qf=S϶1= _ nHe~`T 28 =Koߎ6h   5# ! ̙3 p:W_+4(AJ{{;ZZZ(ۃ G9PUY ;~m hL4ȳaַ0[R_ a9r !Me*׬Uw6kF^>ͯaE^ڟ-X ?\a VF"f$x`Gëhϡ3PN$ֆ={`hhbbr~V_wkA%eb_z&Ss=\n49>+[þz<8KIQ$TDR! ȁ:XLjz4sޓm%Y7Wу{R`>`]K) t4Ͼ?7=+kpfyxVl6d|ߎ."F/n;\=>e}>z,[bkO'u5z11ELbռPp9v\ ensA]bdsy`riP.etHFb'EiH2^LZ䦾1&Wp:>7r' cxlT^b 5Gd֝y;{lbIvr_4ȓ|;%zX2;Ѹ=*LFu$x`83ae #z&1;v@kk+9  `(vڅ]v <ϣ ֭Ç?a466b˖-4A؎P(h4:ߣnvq,SlZ0ek8+MD"`1+~>5=a<;DzA< 5ya5̵B) lx2Wфʵk+vXkѯ~<s\λ&.f=Rvt}KA7^$}v*RV]H%|. ?4ˉ(RpVMYX"sO%qUWcee-J$GRaѢ̭ bP(OoO'@:?.ĂW:%.rFcc3E=/aTuE|W=SREb s"e)CVX%v`L{v;H` :(nn?@0pi0Q0q'8d(`)PAY@p8@,h$z ۷oGkk+jkkAA&k.ab!ID"D"ǦMzj|#g>  J}}Ŀϗp\ 15E}A{-;ؔQ)mbnwόgf<(xPndt=&/@I^,먄Q=ۅʵk ;f|K%kݪK zXQinzM`0+ .>@9?e}""ߵ^x'][s=X60z (2pb‘趣kDZ`+֮lJPNT-çW a8 ox!ȉ2o@ʮh_!psЦ4yі/a-7_>Qs(a;f`W;/H?k|I5Zڟ;ZfOL$ܷvALQʕT'li ,q!zː9Yϭ_kkjA,6mڄo'er A3߾}sl&pK~gyAC\U~N2Cɴ01,o/fU E0ђ(^>%ẒpO/~vbkraw$i6B<՟_jKt)in>z䗺{_S.zOi*Oa&}հ6AjѺu@f1DDQQ= ē${~^y<qbhp:t/t&NJ@Nba̭ c\(rCW|)E_*탏w7cb&C=(*m)|3!QϾ&`<[!m188,7(#pnW!7J:#zHk*'Vqey(8֘#W^ _? [s}ζm,xD }N" fvi<ȯ~H?ª>zXjsO'ue<5׃a@"~-MeV~&wOݏ3ucUZ[{}XdFl?d@dvP$^YǢ,qx'250p:ݼ؂ճT3,U/tc?4ˬ?$v_`@8ɹf] e\݁/URbӭ%<&فb '(ҳy3 Z CH)FsO= 466~Av'" ppVJ1 tAC @ee%6n܈S(8& qVYi\$r qUY>?/< !z!LP [$v߼_zDs9ʛ {޶[H``@ڥIpqT5w~_iO,I<]~`6*uLN~K0zF.[}3x߃OҶ*{ը,xPy`}JZ媪"LC[gT`¹z, CL,zo\[׏,Xzsf'[0sPCr̜./A;d,L1Yɬ@7㗨 ;kA%Ռ0𮅳:}' JiJ2*$n.&ICCjkkQ__?1AؚqQ[C|wg۶mC[[Ad͞={pm#l󨪪ºuԄn 55t {@X(ت-~CU~VFNAz0ԀEإ,E>?m5zcFr5pT> J .ϼסm O~ F| j'O~{FȻ/jzY_ow g ir,z8`fNM%s(s %vӀ IDAT@ob,=\V av'VA&|ȻaFd0Y * :GkZJ-;L|3!e2g @[ϕ?! Ȉu\ҐL#`<xyފDaӦM7֢C B†qCƚ ߏ6455 1/p#*UǦMp7lB!>o& )m>>0]h.'œd)z31˃aY,8oOboN~PSgmTH$b<8H7WWJFn8u}&0l3v ELj*n8^g>![k–:=9neܾ$4%YQ1dK rN+qS9vp1WŸ_ PL, +{A<   ( ;]<IaآH_ vH@H`DE3%UUT9%;s)eފDp`"K޻U}4#idƶl|m  IiAn=HR޴Ey->y4MNGnh $6FGlm$u_cɺ{YуoY?5BBЄU(ѣGKݏ?8 ""Uyu: P|5(`us`-~qfUIc*4M8k_oܸw}7z!)DDDEbܰ {<@dNWw?KA֕& h \Dy05Q~(zD`ugѝxg~S4r}BY @B}_C]#yoە@'ZOgbCNa]^/|]xJ\_d,^׼۔:Oۈu>5yJ93 ZLīi$CVrWi`886Qr8LJ1Hc@##aD%)r/Y>.7i4VTgs䬌PA)YZa3*øN gΰX,^!e=@˯Hdҙ*4 MQbmbmA e)x6݁('R4*CTeh -*OsQ3'pq!CdEA^>csІQ[ĭ1w~૖dN$NBG_‚+o)3,>۸XPX<=h\U(Y,Mg,Kj,_ @r=vcm[`^?dugwҝɔ&t;LCyRv-9zH&Gut҂ b+rF]U)#',;L,$˼=Hd2."2JkQ*9dx'$/ŷ75AR4 TjcaXh._!K x hW893 } =՗es=S>cY7o˲mGb\Dw`owPj2hPG( nMMM[CCG-X]Yt/؋b~GZt/ |Q?(w+h 2ajpEȁWp{+ l-r <e.wy [^Fm;.kסY6P e7+҂ ?'F/#wRW;>pG>:q v "=ˏ皅b*CT_EDfn4|oJ,fJ_pYGY3~W% `++a `T<}S9j9DFp0^EжE!s3zEK!^'@Ί5zus6._+%x0S>/V/#,CSwtA7|3n j 4Jl#Ҭ1GҕFpxzS5p E=t*UUZ$nIBpp^M.? $z0Θrfxlz98AW=;Ln E,ӓGFur܆98lNTr; zm!Ș2+ 8hk3d-V$̣< N@[W4ش)?5XjDeN[y !)L(_‰WhQ=֕f+_2T>νKS~ۂxp ֌1@Q)聦Ɇ'sA+qF=:r}7yntWs |zN,EDåLN-1| ì-bXFk;jx{[vX*(3ݡƽ4:lAr*xxDb'ZGa1>'5'~4\B| I2U|w[ݯ@=4u5xp3::clsਿTOB;oCӅF;Y6WAQ$`b <@N×7܄-ŀMswV'E{20!3_TDG2J_\_ø$r2(СTƃߩ&KSq.yz f@T>YSyESXD0Dts= p0MŐrrr hv2Ă<Ō$1Fې~ӟ\.6mD:hB ===c?j@]`9c (- M6z8dJ=~ Q 3%/`okm#P>Hiq!vԄF}x衇MGDDTPatvv#(b7ߠK=@89X/ēKD$AT8R:3# Ά iaaO__nl>J xІtςS eGg)4uya_>o <<ƛxoJo[Ϻ<.` >#&=P> jMU$ų@:v /7.\v#v._ ,lJ9FQ;grJs1&tzl( %y-*DG!ֳ$CrJb]Q4lD@Ț YOG(̀1AS$Dzx;x@DbTJ@VՌVRYPr~ъA3vexY(9 vrWpl80qo` 8ywA@T( Mm_N`/C";XDvm979抢*iK, ;$UW(⪌,!( .^AY0Def!L*2EB678ʬ LŨ7u!vp6 >,^uK%mN^x466raӦM&GmB$jC'P 5+Wj@9hsm]wߡ\wCi  6l`a3qR]vh4qHƥ&erԄ[o{/lBn\""w6| z *S$+P.+Wڵw+8Da>5Q^W?\^'CȪheg,M;x#+?1~JO .8sbHCt/WR~t`o}KxC@ z`YJ?(ea9d{Ppr0eEv(AH  9H5-fP `#Xv${BN/2Ht2WGhy@TDe"AҔTTHM"K\RDÛD`X9/$F $SrTf!ĄTfDk.v~pxDсIپ};xY&Gk Qfc\\@;}V^<*W^rECi9@ @ƜՅm۶eo*o[4gyYRG}-9-~!x<ظq#nWsscB:z5@I3ߎGw5Yg ~7X M{Fm;Ng.gAszO k 4䴎sJ ۸:^{AP,g\@k&iJfh;Y0bf:w૿~W2I֕tF3h+؍ga;6 ÄPT\J*XJ͝_V g}y됔RnHtkT";䭿0-ݖ/0,'潮&'ot~7UB, ;aРAÈH{r޼Zei$i z#Pg3<Ȋ@Taq @O-+w-ߎz ׇ_|*hɘӉ_~yk6m"""(HԆ4Oh B |=n5[q@DTFjooǮ]·4ꛋֆݳFB=5e=v;-Zua֭hkk#78)v =`c1< 뼶,X6B^@iU(M79&3vIW6 ZTkLr"xx~g%<L!] g5aGdI5\IfI& W,;}^a$,FF-X@y4=s( CDESU$$2kU0gI8DP$ĕ}S4 ]-8R&Uc9$%wʇA䩦oѱ&8ED IDAT|\_G"կ~8|I–-[fraӦMHPh`ꩤ% QuK&mln8ģM)Bdvލ={d.IZ-0B MT7˲ǺuHDDDYps#|5%H1^d5@i%xZg>жdr*`}k>z'IN[ Ҙqon |]Z^kʻ W|cҬg+?X<׏Ѿ?@d`PzQ3(I0bGt0}{YceB[hUA+Dy CƎᲪ"84$߆4|1t-ACtYy}vJx`ɚ> 0c#dzl6IUSѶj`kd*I =e33Q 1EBTErA/JSԓO@#bsi^D<E3U0,QCF.iX;L;%S?,'?w}83sϡӼAmoߎ'xbkQWW""* N]]]/Yr_2a:s;ݹs!"*<! &Jj]@QXlկ~Rgg'Z[[MsQ|tjΞr15@Jua<7l$MyI`ҡGU]iʅV*L+k*Fiz(h]?uVª7^JH\|dgpmYse b,nS>zb: ;[zVLqA@rfij/\7$PgnO[_pyNdQI·ß9٭d lcxkFP7SRDPmGM0%?1)0a qlnE;tC;XMvTCF%#L EP/ PLյ" Ÿ`,F$M\I2̯h*T2/c( 56V8jQ>Y66> 7k`'xHdgH àp/ɦ ԧRuRDSLwP(D"An"" с6477(lذ;v={pС;`|wY UmpMO[E0U젥CjyXnt]]]f#""go\N{miӞݳ nCWW랆&8sGAn4(%3g>~?Nnb""҂P(SmC(t\=-D_k R^\w5q :QD`Sݺ5(R놀2JtP;zݡՙf8=7[ joi2p @̄&bIrSHS$!+)jhe;ӬCZGqgR} ntMۙϑۇ(#y<tvvb޽Y\h^ҩrTNH|#j D9SFUJ@Q<8TAL+KrQW!].ygpg,ϣ.}8{P>ɫWl-~p_f᫗{≛6!X<ɚCCT[ rMQR8.XSY9eSQ5fV< z "H9pSUn} u^8oj^:{^8֮= SEgwffla;hl5'xZOP{iOh0h6x~XvzNYY:˥A+2v *rM0*':S`qI"prHA_o UM7 vN^Ek5|/7o_|<{{{z\?xuuu!# Nw\`pwۍ@!=@i҂󨷹ѓw*Çe˖9^/DDs( N+gavԁjRXP5w/iPB|$"""e틭oY 2 L)UQA MT,G׿6rcͫ6diO#5OWiK\zRXhs1;3Kc 29L~E*@mC=0hn~s@~hZy?}>%;`0cүu vO|ˉƮ@Nx%B .8o6~c_u[ݹ7M b6;VW i](],xȊ2Z©iw?o?h4yx#hMZ~gsgs ka g=PjLs J^K1rL4 )JY<_v`LW1ŲN`"DQłi`;E+8-Sӱ)PLNx5jW9QNE-3cTHaxe UCl6 7+**H_%,ԩS@ 0CxFQtwwgP0{n477񠡡;v={v`*f`W5= n,tUS~`o5nnGUw QG#3´C{6[ڧ!4}\,((獩Әu Xz5~#ADDѳ ,~W.Gc3YZ6i$Bñ& CdP[Fw96L:ncJyQh(]#\z;=_+\4l.$",i5z48xQeb}6q;d{EQ`8/v#;}OYj$9|VVgh@RL` ;L,AGzPs,;Q ۰)|ΞΣu*|~9 Qy&+,%EF;1r̀SQR; **^ءPSiƤ$rֱ= cI]{EgyC`C`▬K V4J*2 5 <8h<+AabB lځmPΝ?/| S>^o#Qos'4Çψ^N|lhDo)۾u?TŊ Jiѭ:z MDDT6ά-WGll8 eMT3.Sࡇ" ќ GzPP̢+GviLW$hb} `!Rv(d*[O8S|澾.3"C)n6/Ro4QAlp/m)W2W18V2(=p}ضtCCT[ 0=̈E7e;Lis ;L֘MQSIE!R?gف8hr[Dŭ,#$9e2@TT[$3M.;SUz$]G]Ss|mms{饗e˖':u 7oUkk+ۇ9;K ^'2vZbJFPhlaܚ _r(ǒؐ YKt#1EDDTN@kWg *0w5zZ暦聈(ce =5@ إ08HeWv̓vxnV [Rc!; :}Ⱥ@ђϲ|>_H-Xx8;gfYW}v dQԿQe$Nh9cj9`ʼYC *8`*hHq0z Q RK&4Ο@Vމh/=czo\y M5^\tnxN*c @2RQlNX΄rr;L|Y >̩{%E1oe\F$6EY,R)>gBE+3!m!"2,!֬TM`(U ́}`|bèĈD\AS4J+~XU^nG`z p\V*Pףgֵ\:s "Ȕ6ͦax<bR`1zUаs ؋hPj 4,FW PA(c%%.Y/^-[ADD4#Oġڻ#>,⫡IVAfUtUj4X>P| %&M4v{v?V*q['~4r<x@=B[*6-/j~'d4 (v[u骭&yɑq Ch6q;ny3Yewpv`RmF2;~.I((Yb_H4]{aw ~WeyT |&u۷#'=stp__OLZ.ziE+xk2BQ$ؘh9f̱kCRLh.P鎤r ch1C14_9bv %#<0QN5*%Kva>>n OrӻItI܍ i(".QF Ǫ̰ɝM;fCm3<3>Å;xyCm _ '* atvv ͠( ۶mî]p(Tr0~` t kftË"h\f20N^~\n.|{,_ܶ-CaS\,((獠J@$hXeV*ovdjBzM9,< }ꉃY&.^TL&2k>z'e8Ӹt4ϖ;кuk?cHo#]`=yeպ]_~pVGNlqx;SM0Pto 7CW\r}@߼֎q!1~K۝tڄ"ATk8z6~6gE;MI9LTHEo-c&`alZ΁B%hܟ\`tKLCaPOi習ą(H<@=ћo3>.ذ#rw{UM! E)9d;Ke i[T)l{hg؅a!x:?""QGGnjcBKZ!@D2r(e]\UFep%눊Bfo|QӐ}060X[Pix( NS(0p,xW}l X'4UFDYSh㶬8C[N58QSC6Bo2a:0! #;d{,>jnɲb<D{ӯgO&~E'az9YBw$7'ݼ2cL^`\gClqX_Q0G[TMàǹDcC8…ItOr8EmT P@̓B\0)4ҠATiaIS1,1* f=PYx3NUKқ gO#< <#l|FH!$ʩ,+24c 4;́y]\FU5 14< hzWޅ 8:֯;D"z <̔C|>X6\qwV;s "\.WBĸM]]]A9\QʱԄ{ ,<2ݬ=QXKf[:`jS{=0_A;-q0}iعs'(SB){~+ֆvJ ڻd1]dpUhe~-HnbgCia C\ mGhcH IDAT(a̧%}˔;P{`וWSX@d6ɱ؇=zPN/GGYb.=x < [Ge#:`!= sfx(5goc852d<2D`p3_\ ۗ.ųGс`TSxz |0v /oOf^a ZK9Q =e|~r?e5 u^˗3YrYN>R Wd %0YrE%+"QjTJZvಆ(Ubt聥iTvCkKVfɚQyT$UZ&I4J`ɟS"|;>hO0"'ugoزeetwwcժUy▖8p'cX񨾗`pJ[Soo/ w :;;pXb(fPBNt ""*<nj9R,X2f_v ^_2Sgg"{֭['ߟutxb߾}r,@ `E0}1E]iTqmX49S`*+ kCAnecQóy p׭[xPrL徾bmw:h~+͙7)8`wWbhJa)tx_|WNQ3(n[,{. |<{}]ؼS&+?-8Se:e s\ =J@Q?M+~0vYڪ"EF3L2LMs8-8|2=4 |0]YBw$n ?f58Oրrfه<T%F^) )7Dv(n/5(gP(gݛZ;H,D UcR E6%Oâvjm#4R|SFS%P dfGktDUdSЃ*#Po >~i˿˔}}}8ZYh᷹uݺYۛ(P]]]i Ozf!uVKfkN$ȍ2M+ J@߰NaJ?Cm,YA`*]3X׃E!T)D /9~WמFVS};_]u}UC\P%4 &6}^p!Vd> bFy(aNw~W%<Ã<h->L+~…x?ΜNg'[| sX'df,88 /o愇 $ $^r:f IӦY͖D]`[WD1,%2JRUzf{ɊN@ɻ9*[2 v(Pl1  BS*8Bj@2:D;L ŀӃ MQ٭r95-׶t#:vаw,D?lٙuPFjfѓ('ymUNG X|UuktQarLML? ~W ^wgua gNçwWd岜Á%852vkĒY%))%eS\$EXB`$$ߖ(Jdtmv)~`B?xxjncSV3).U񣻷i/G% /ǧ_{Ϟ|}،EOe9/E0|o ]Fo|l*l񜡌oHJ8-27 sIs r']vx#/PRLW3-;&v *^EeѴ9y4 ;o4͠Fp ̞µ^Y|,*H`YDBkUMz#Z[dzF@ ܬ a }ǝ(ću&H!?GU߿ hmmE(""Q[[[tU]E `)\q~ )m)n!""2 &q0sdy;f f]r~pno0ZZZcǎ+FFFpٳ۶mEQhnnF[[:;;@ 6hL5/7Yx>XOh<ʵ_PH$9U;p쾳,ϣ" RIp:P@r4{LLH-ӝޜn+hlV7»l|W?)&/sw>:x јNB!ʪim2bSMXBp, QQmOtt=DOz0kej_t |HYp`w߁_U_[3ᓄ";။856,e>GHH(c6# mlQD1fQ⪦aXJR" Q tl=Q JDH_ToIU[9Iv *^QEe2h8nΆ6<'O8ގ6ܜ vvP4u#Ԭ.9OĒ֮ zp1aopSeV@G~pO="/ͻ!?Rwniz{{MmgWWZZZЀg2>޽09r*"vP+Rpσkzƽ:ᆬ7z  9:+"""bL.#b̺:9vf%qb0Jp?qKСCطov1@޽PEHHm<&cr96Rcc_+HIoe84yH~7kءr4`!#n߂V[C<˩Tu4W*x VU2s.nTK-~)/eBQEY¾@I')*±$"II_4q衘"+X ]bP\d5Vx#Ytr^/xl:8;p}SrDC0܏ny&( v~r;d^Ʈg,_ExR ED|dR1(q!9f0W@h"1H D LE #Rsnv38s'*#ɚfr;an `l<osZ΁E <󬆳/Z4FC% _Y"Y&Ԗ(pضm[NTك@ @NӴ,[7Qwe; {b n VJio#""<ոRE"JG-CC'?!FDގm۶UCaϞ=ذa<Z[[A@}𐱩'CvuPF9hwj><PGR#*=* F>5%7j&QLGxV8G }zC}(0p,X>=5 W5$%%'5 DC摢R؁*VaJ,;dO^M57TE{/]>yc#عrU^w^ǫgA)ܼp^SB㪥vR'"4caNG:γLR2q19vȚHi cria'_sLp"[$QqJa1nݩ29,|+(D-{dã^pÞfnΎ^R>i:.ܬ`(z{{H/2i N}}}5填 ®]ӓ;{趮L;O XBx@ 8LrPyH])L*zbN2BA*N} k6wT|DBסkihkU_~]h)ܲ,A-MƼL.zq4ԗ*mh߻t;Xg6}~t7FIviJ)m`D(kY.F qY)g*Y]a dK9֢gN $Y  ܅7ledFkxb$N|1.)]ć!%/:Rj[MZAvPsd3Mܪmq]ݤfߎyjP !IvirB[&*o_Gx)皊YOj;6TCU@ 0ZXW=:\ iAfw`Pu2v]L.gUŹsl5y8 00>>La``2<aA5/pmn/5Dj@):PPP&͹/y9^`xޙ3J:;;KXnرcطo z{{)Qt}0}~G8.6 <Vy jN6'Z \z1>pK? sćHCIL@M_؜p<Ƹda1ly)/ *<['IM5TFLKҎ"UuȆ"(;|'.\MpPϹrt,oOwV`}V*QT0z{{q!G+K Y< %yUAvˡTwĕt%UMZvvnT_PiSFgư,D7wv>pxQ4 OSy'6U?y"7h5[r#BTWgVC){3m<8Qc+l9n+Hхk Ŧ_+=g{[lɸ``>s"n}ƋAX03)4|6lQIIRr 6O:TB3äO-/Y@(5/E!US4 E2C,VyB󌢄d }:0ZX8 dЙk w 9\x[q4Ysp5fRyDww7SO9yC0D__azS9`6n۞Vo| |P~@I=1IEػw9\>;Us(e WG' `ppxjHxG)fyNmk>C)63%9h)+ <0ut2:ɪ4AޏcˁU/R;c&]-GE,cPCY^.z4CxiBg.AݨY^}BzL?ϝLf'h{M3 I";Iۥ`GǁgX4ck} CReH=KkIq--/_I0FR!ޟŎGA闏ɔ+7|,;ڰ0kbɗ1V[#c*aDd)gzDmyiK5֮mXpPH?'Z3gE6aIJ_VweWzp<!*EvЈ :4@d9H a).$䥛{8qM1HvvQv]H2|o~3ƟOK8rn]orr>WxθJJu|BӧjwFGG:;;ى`0T2@& m$ö-G˴w #/a&ctuuQBAQkcnj5f]OIZƒ>93r`000twwwm9NWѠ\Z|z fL Ӏ[ʮ~ OU]^z:dq}[՛ǚ м-PC* %/i@MJwNxòp'UBgm5;1o߇ݏ~gLm:۾N ǻ[xAy&(zw4'j#;̃cW:j0y-cQǣ걢d&>!>\K&k:ߴۃhc|ZgEaR l\!;,$Ʊz:ec2//@4Hj, .l*K>=>Av(,%k -KQF IDATI׊,8 @Q٘UNe{`=A!unXAgϋP$tBb9&y4 >J+/qZΤ_b>`ө4Ug}xFPU?;w, ݻϟ~8}4.\Psrbb9=Ǐ"CPCOOz{{D7m޻U ɡZ7zZ: CزeWvDuja%9PXAaxx$3bƽ` %8Lエ$Do\1}сnPCh pn\5,5=67^"3'E߀ "OoN:5Tz=wcUOv$UJ m].KʘFb&}1fǴ ၨ):.p4@@ <\8*OG:nL|Kx]"v>x|_-|)(ѫoٹÏ7qܛs;[}''Hra}vJd%ZSj%;*Bv0W(: s<PϠхs ByK8⊂s;6p{:Z-jL&c$r:kF>u@.yŒNf*}O)p@J9vVBBWU[#z(R*CZMxd A4ty@@$W-=?TH:30$0$Whc`Ȳ\iR^sj9 ^NY={'Ođ#G Bhoo/ioA"EL *JxN h Is=gH7όm _SvI=ԫ MQ_DAAя~_%0uR3NFFFߏALLLn=9ILZұ}:11{=%?>i7@!&u`Ep©P' z=xUG˅;1s9d4=?|M4ol/:o]p'>A$tvm˕Է/]?LHힶxſѭO~y_`8CxGYצ޾?o?Ysx/Xٱʫu)y蚆e$n΀Եd:?9w}JA{W_?p'<\^paWc=[Šj8\ CH*pKʶ0`xDp*1>d"f-eHM|X- xlVi$nuXFv03,v5EP6cM}ΰisEOZX^*CR1IzpCc*#3 ,'[hp6UqM1і}b94dc:׶d!Sry&ó[8%J .?m`u@elvZxՈ(á';I-ӯ%郧D2j8F_2C (ypq "pU6FNػw/կ.FFF6]wloD,{0>>@ @Ic&Lv>M7y]m*Ts"4Wk5[s7"V&GJsbhh:2!chhCCC%9̟o=0fk4H2M~gr< bӃnRCCC8t[p%9PH4bwSh7 -z,U|xqm!3#x|<4̂4*q&:уuM9 P5@-M0htH ;xy~ZscP҃Csωs.NC}= G((lYUvs֒ ʫ x*Ad9pUp-';:"bCW{q F t ds \Va9̖i;f>g8|FOO{phoo/Y`ll 9Oփ`ams s^ʧlDׇǏSV{64ώ$.n nM,nq~[K  p1G XFx$ړ5QwwwSGa2RFGG;KHILq5I /%?1{Rcttǎñc(!z{{\ක飊'9o[k H`ͳxE,!(;s8ܾmچ E-Ʋ9%'ͫi{Kf+39ĚnAۃ"#(p .&M>kcU`FjTm9vPu d]CJWU/Fn Ap.p9@7m^/>v|xlVxy">v8a OaH!}X>7ULd %e";,43`]<Xqtsܞ%; ljyxg[$ 'ёT5y%zEb4Y*ف`VV+Y}]JS]IMrIcD|vB $IxV<~Pqܹ͞|6Q^Mv Y)ֻR ԊDOOҩT 8*-vOٻDLtWWu$'|T:mV-L['+=ԭDKN~$ SOĉ  cxxdƻŽ^jqߟB= ƻQc:O~شi:;;׷RKĉ'Le>1ˤٚ]֝p3ŷڠ¯AE\>/8%;č ";˝5;s3tQÄ\9} pz0u_*{? 78"&s6H^y+ ܸn({.b".H*ym|kd'`YDn tL 79hgd.Nɬ#=ĎEC˃k< <]V_ ܜFβtL ( JU%Ue"pCg&hQT >g :! t$H)W0s"` ?!BвVgm }ڃ!F{m^6Eq5V~jޡJr56{INPPP6~apgbB/6C`a&}N$zzzp D"G֏[0[[o;up 2GnsT3FFFj¾GFFc:H6W,U~T1WP!O- -QT~.K)oO >S؞8xꝡfsЄ耣 Yk&o{ ^ɑ_< u@n7IХwjznZZGLq-&{$A17M) 9#8b8Jݏ $]J/čD6Œnٸ[֎RC|;v>vˮL``>/c?yrpYϔȞVILU0/uHiՇ3YH;"ˡ >^rX0X^^@+ T^;RO]0x*Kp;lQRRmEu9[;IݡDdHfPGtsa:LP^+U8a!vJv0 ev܎Oh{ϟG*UڇE[zмoQam\{y3d]-z 0`~ڈUrPV /z4@AAv2{.ZrRHT͡R^qpOIc``uc_1ɸH?K+pqo>AV-ahh]]]M& ]Tʢ`9uw=tJxy0ֿ硲T^I{ -| ޱty;Хc|fY^;<֝;Lơ DǍT=zLR &Km-Rwș:!R6WΊ24BV<cZyoqجyٰ/>x_~mnz^!b'#o{1Jtx$5_59rϟ?0pq<|FMn/Z=~zh/0|M-/8-.yyH~X22oJS+=PTN1+A!*uajElV֚ǀ"b@EAc}1BߎLeu̙U̙3PU{ uvpv}rQC%n#%HA=zļMB (6M8t oNE}Qs[Nt/T͡j3Ӭ]:8~*yPa~rT{9۷@===G8xáCR.=\qPŠV fw0[J r=1ai~{nl0/*fsZՆ5]L͏=%<8O=]8*Xulog;o7?w'{{dc]<ֿ#Gn= "a$r1U)"j@JI+T#`ΑˠA|d_7AJ'dQT 3Y2 rX1E*!:Z=`toTVgϞӧW?55U!! a||###Hڇ^;= noqYGr(lA?V}䇉 uHtvv.L*qAqsDC˞+` ~686C5ա <~뷨s0qԅm>aϗoH0io0::'N,?tww>A|;).9'4%8nƇ%bYSÐ/@ d$[ᅦLB$\9;]rnHԷ1Gݹ{ˀtP~a'u:noC z'K™2hpߚ{6#ksoNWht353{P"F) &=%;,:쐏` b<h2׼x,D̀mHE\׋;v[q:toŵ2/O^˓mڊM$_=e,){@2Žja1HlpjxyqסόBnS*D"|;Y 444 *Y9}}})'wd*Ğ2^ gn B *%Է8t3ivduC:٪)iU@= W~f(ȯ @}ۚ8M+O,DrX0khټSo_{l־' /񄩴֯;I׊SRue>1GTlAձ*V. E?q[2 IDAThqyp.z|냙H+>i"~A qdFLOaͳF0:=/n߅k/{) ;,u#]ׂ2h'%";,-AUD`;[El8 =Pehݘx#~^g[cd ֐NQ{e%a5u:a* 2s.Y.>G{lravq+cedRW@taZL&bGWz$枨_lnAw#H``,N"wdόo[px}mPc6#oBW`z 6RਲCt:wp80h<(Ҳ>@]p5zEAm>? ԰0w!Wֻ3\ɄaIjEd]Xj28ϲUEzdrCӀ?r'N\ᬞPU XlJiw+6.CMZ$BYCp<ՄC__?N"ۋzp:G< ɡ#eoNF(((2Z~'*R5[@1xLbddľ8^ yh^(khhXF~'!Ca`||###pix֧왒jh\Ǯz|0&:D}1(46v`ú]V Z2\RP[[qzE(&5}}b[uSo+8-ygкkG˙z<[m/Iyr"8Sӆ;[,qȰ,noiƑ3@ .,(w',3K +i ;N!;QbV-U9}SDLULq:!=aLQ | LeJn߈6mV5HLAd %XS7gOm9顦!fa@@P$4$Gˡ֨ O 3q|XEQb'5U>dhT$Ff^f*AprLuumbM)\fGD"Pr )D4 :!^NV yP6+I?HQ.|Jz(X q2``|W8Wwkهȯg÷dvttjC0Ąt_TTDvxïC=<9aǎ3~$TT(mnU&GyN:R`p%-mC-_ [@0\~H YճŽOJnW$[ }]X==6f{ ⩿C}k 8Hzյ4c掊j۷?L1z6E+>BNj̈́Sb4k7pԫe-o,x\pShټI^/vSiߌ1:=UJY#HIIXE`#+d:k0Zz7ǁ!!dT oZ~1dX 12[ĵ-1-;hɬp)(((2')5 tղ:d-q֥4Lw=n Mq0١}r֏Au'T *^_J F{&ݥvKXDŽ{8\8_%;Bo8|TmJxX4wJC7z ;8h0Al.4^1x$R%*p#HU#uu@Qd 9)_ c7_p@taҪa}Ԋ[\Z򀒽]ny7yaް>)^N#6V Hfm(.pN** )'CiG\l i2$bf|b9x~^1smؑ`6Jx 0YRF C)V&j˰ꊋ$zbজ@JS+⪌TjRJL(%53J2#a%<,C`YjTCPmmeU ގe8 u&a?ᶯ@ ~[~-mݹP)0ndvl:5Mr0b][ʡSg)N dP+Ruiv8qXk;Z+m%Obtt ť'CHd[2~j̪ >dP%f%Z-S[ᅦ,1uO۵`5^Iִ=PjX^in`rWewb!xE]"v>x5@[Ki\\D agI0#Y8FGbA&G?('I DQHfX,(#c a4= B[3(;4y˿Ao=45WuL)>ŀZ߀vގ:ݾk=w/O^\$>&Yڮ&=zk4a`_a,p Rۈ何;QM.m[/8k.4.x!7ZD/Uv(/_t6. KOF/x,/At+)AUAH-DaZIb";Ĭ"&J=Th9$^T˔Bd]%;CBWr*>)DGL*O6ndd';:$<iY:kR塿,?TSmqɁT\Jx(͹)% ",j5w@L0 ^xLbppnC}x\pW ~bo.S7W*nCң(=0KUx`C%C2"ԩaUGUCd1u~ݶڎ۶T[i[eͶ T_PYW+>eFl@$̪<&cq"QC:$d# V r`:,la#ٳXҟ S)riCǍfn8 'hYFv`ʦR-7cg}r) ,V~P3/8}^0>vmnihWQ/1Ex:#-W*TK;GZAzgv _ *۶r 3ax8>V!P6LA8!;iՅB1 scXx9 >^K% p){R?X]%:8]ΓԂ: qKwe ;TB7]m=!fTЖ7*7c'OıcǐL|&ȑ"'wfin݃!5G,0#`t'A9@  `޽>UsCo.f=ߪ=e[ ܆!y{N> 6p[UΈMOB)0b(!6sN7 &6=BSȓ/BS5:ԁ{?0Vc_B #z4m5©W! &<hauvŨ<ŷ`3TPQDH ]4{n+OVDbGg@ebc pSX.>/.7>QRȐ m/x+?W4!J~:a#7܇mY?]ŧѯnܜCHc25k,!IQYI^51g*`LlDZKҵ-;8g [G? ~'>0u[+K -/J4(Jk+2u`}URW^oAtّ\,0`F塖I,5u5F'pԫe3/ FL-?MJA7'|^WR9Up$=2LmtR))I6ϕ2 e# zȡE`L%KP;AXNfv}Qʄ}yTdHwieQd9L)Oy ]1eedVoos~/~/ryhbyV<󳾾>[}If߂>3ߜrYjSt!En8kP[G?z{{ՅN0 ى.(m΍%R8Եױk}PT-[t6#6q5aчݥn g}Za p., JMp jo5Q7Ic-BAPC߃pt]]>/>OS)ԯ5Nx Jf&C ;G@569~]*AIdeF֯E򵊷%%U9C钚ko+8ΦঔB۠\buT YW 4M. I~Ii*͌eY ޷PaD7K A_=B8&1Tt@V $M*V'nzqt.ݾ '/]Y\K&l)|z6]ncY~&6 mqs^`MplIm.[/ٲ#=H6K8&-H.-9ƥ-CuC@̰hze۰\ kҷ wZ8NQ-,W:Qp]ITvv J*kapp{m:*Oma*ILB I^.^l l`_ LdK ˘7ƕrC ]}zl B4\>/ڗLOATxPSj("uȕJso+w}(w`y/~\9f5lG +p{<IkpdqYgip@%u Bф73 Sd Q]}hsV$RqRq^y6 ۺyT@ّ̜ qdFLOa`,FlKݳmZ)Mx"FSg/aos+Mvʼn(P}lqdiH:Sa"_CX0*:#N/Հ;}kHe@J`8(Lω1OX~署!rqUIaAM߹vb9H`EyR9)MBem.3 S8vnp]_x|_\O} 쑘BHx"!0p|sypǏSƒE8~8:d.ܺT!L G805>̨;okp>qH0qSwh$@D"!Ғ(K" ɖADK2C0N2vJ&J& 4|rN`Kb:cɖ@HO$Q2m$Abzh ]vw=h[UoVgx¡Ck.ttt#СC˩s9(ɡ](Ǿg\]]Uw0\vˎe[D(pdIR:{ PcgaʄPmT@ʶ\L1vJrp$I3&rY(SPC@B j67A0 # *Y{QEV0MPtԷ_AnK52t.;vss6ɧBUyR8==;y9SL )o5 I \"Ym:H04 /Z;D. ދ ދ- &bKӷEvX$$xTdÛ|=K$px ~0:H S ͨSd?ePMK}bI=BIQa3䜾x~p҃ՆY/&Ir%庒_%Jv荎?vCB+y!5Xa!a!Lw ϓ!Z\{Ayc8HVY&M aQ0.%2Evز&gY|5qY\~eKDJ| @2U!8mq^ف,I ~e4D$!y& @* !rh_MZv1b1Bi9W_}Wdyb7k]i,Ȋ{4(̈́oVA]3<<^z#hmm50kbR*6Ca$PgꮶMMM4 $:d8NtuuQL !z:}>/8]/Y3ӆعg4#``*v P0(vfc {bmɋДwr2: 6_p~@C+3NqC$7e(R t|70H S(.Z"I 7eAC',XJUx|3ΜGlfiM㆒MmH˸>q<[V4E lU5dޟ!p)P0~فXUWk%dE) )X`9&RI&cdbjenCdY@4}h'9y_':Hv4 YBRfHd= <\ഽ4FP0&d˰>dci_WA cW0R52ň9B"cYĬ Ze,0`JJ@[0C'*>jW\fsRYiAY 3;%} О`x$U9$MYDxРaZJA^v]T .KHy/:wx"8rq|+_&aΛbՅ vc_]tشiJ? Lt(tSHfTTTdz{{naux?0ZZZM3%flذ.\пFO`o_hCthcuuu64!<#; mkD[B85/Uw ىLʋt4A9_9L'Ia`Pw%1 TerA0rm]$(V7@;2BFB 6zdxLvAc۩˲h,+GTeAo}`ABv\X jf^?~ɴꃬdpw(ؿm;ݴ.\I&l?Kxl(TYyM1Tv?ݿ#! ֢./፿rOmچ6ou lۋ Ae y[V *EHHi:W 0Q FH' S;0eMńh",PbU xVܫFLN h߶o܊@l2kBTvUBZΕݛ* s ճGU2G1G7AֽnGrZn y[4L sXG2-k5G 蟜3wpsxf4d$s*8/Blv $guQ4DD}dilוl1َPҾȚH̓!ڄ"PvJEF z#(ETp8|]`-ZÍhJ ([{`(,԰dYD!lξDzX.MNuI0OWQ \WRT\"{\Axs^0ZlpE*y[1(~f> -@EE:::ٳP'^St67ZUM6/52=ե?qv===jtvv]vСCD"ThkkCYYfggǬy0B -<=Aa&ohfSSw&>P;/)8dkUZFvXd!(3.;_9h[KiiCͦF:fS#?=rڊ)a?o*su; {yi/9ty=ز6c1 D7KwI!gy9,ٺ7O2De$YqY̮6䜸}Jv/Vr|~Ŏj쭬G/NسD8 j溚;_y>`q%ow~1_3Ǥӳ\pNWķuh@B%:=Bì$s,7.*) LIDԵ)1q!I*Ejj .@ɯaӛ/_ɳWq 7Q_}eLR80,Ʀ4iRDGbtA%G]p<%;>r֛+u   fdQ)J[+ïToE'dǃDŘ,]`=`葝tݶhl4&_\|Y7a5(2׀UKJhkk]M7MMM) "\GvÇֆH$B#*Μ. 6Yy_ d}pwS#Qj/uzǷKAl/!?8okMcF[4"?r@؊4< eA{Գ{t),+{.|J[r?7omηoC՛7WsȧC6 HaÍ'S1˅+ՃB_sY)A_;(a|,*M08`n@b(iރ#ć <*2z d%=)Br"u؏Eq94$=>&eIi)*Y<4hH2"R*irdo :Mo>[O}@1Dۙ#M~p`nU^?E"J젪@Jՙ)Ya·ZO_'ri%R ;,@'oY/ ?a &_Y/dMŴBJɋw[wjʮ,vRMdeXY8e'.t2-~PYpajDTOOW ;53)䠕RvgkMCtlW,I/B&ǜ*p-[`/"?TsJvF|KIL.q:S{2136  ^,ρO«h]&ֱ~sP(cF u;99QRi1u}nWQ5$j+M  ۑsG4 i@uć Ó}< &$FSsR`d1uiu8 l,} 2'90 &$&$fd!wU G|.b 1YDDJAR6 !QMC(.Fr8tSQzt)gJN` Qc ) ),*776bI9I9E -?d%uu 6x˱[>0H*2dU5\ֳD#Wx?^ ][Cl_`5e*DՆIT:PY+ЄI8{U fS58k.455g>[1{ΞOH4a:l0OQ8kgY*@Usp+G ݿa/4X݁Pఛ࠹i"P\bkM&1fqDoA2E*0&S=&-þ_4iƯӨLhfs5B󞚀" wcrJ^ QgDjfVw`Ms}^@YicnߣLRqbJ^+( d{=qAf9*fwV8௹`A mfL4Ɗ,'>gHS@\ć,/9qu.AT 0YqLOL i*.f0^#]8|5`,3!yat C;8B m=w ~fIqi⃐~g+O!K\qްwd[_P,tyʳM"GTMøNz( E”\+՛PylvM+/"|FU<@9C$zzz ;O h:.Ѐ Jr(@RLV$z͎e4!_egc*[y'< OnVVz'Lqѱkk(!V⨻Pp4@;23W1?h[@M.|aYjk:~.5۶^2zF|9$Y +r6@U7L<0 (_=8Bv@arjG1}v+ć:oC qc8 e'>߶#M ëxfYNN֞;\m܆g苺*I!XTd8FcrUMAOq\L z'Ì$,'YkaVQ{7Oto IDATS}Q!GPE;$<{u<@SU˞vׯCr X ˺M$n݄xk*>lD}xm6&H ɐ.ˀwv>,#O)"1㍔]EcFPXnˢ-dkj<إ"k(EqLd2`-*8kQy;4$8k7cFsHb(ŰPđ,Х8tZѱ6Um>s,E Z>xxE%)3*1# xBgC*֩BCLQnWr[@Zs&Ӱ6 tmy2*M8y65VSR vD_$Fh3Է.Gg[ tvvF2?>G$>iQhr+>4xw݉h]n}}}ZQD"hkk@<`eebLpÐA5M|ţ>zm|@@IycD= /Œsb?M`ʶ.:wRyl &;PcF̑_vDN;И@IZ1(txd.c'?q߇dt&&z %{1/$ptc^'q#|rtĩqܿL )G!!fc>U%ů%,P}YaG|\^ 1s$M"I6M|L|_'109a4ٕ蟜@xF%*=!N|U.OwYrV!=$)Mx(PeAUP$}XPX0rZod]OYo1Z&3yb7f<)7:,?~J}EG sh_{P0,| yO`agߚQ/D(0y&y{txlSn|G8 xTwuJ3z}eKY201nED/U8:俊e컯7|%d]ZƻDΧscX%; ·mQl>+;fyi<9r 9E*S$ *y?8zP AUh a~C%}lx¢6-ˤDLuɀ9GES3UBDLI^fqs'AS+(vCjꢋhjjBGGA3\X Y6AIB|C9k׮eCX—;bG_$:kwV4oB[DH$R10lZ5k|Y1E[[[яk۶m3pFO}GSq҅o.^WW{tuu"3Uwx Ӛ@G}.mCZfI.Pc.3䱗';07<2424qc %;P ;[& @_W.w˻85 1|F7Dz[Po>UcJ 5K!?䔊%;?I6 Ò9bKZX۬0+}>< -w{ډ{-1Ic? "ca_I%6_[Ϳ%)ckf_V*cFFNtxj+퓘 wd&8P,߳j*”U1bRW> OxmǓf4xoR⃳K0X"UywlHI7Ks^x(c1x‚# /"zPdP|dIS⓾:k=kɓ9qFeLvXR G|f3*y< kN_,"4x[(Çchhn,DggK)uP;ZDe~e@&]6?xO|eT_&5Mv L.6pOz0Z[[)H~z9->Z`1C4cքԈ]F OVg-!8pkAs?:?;%fkUЭPXDNB}Dir˅U$؊= uSf XBAQ  cS{2珟;NV$]n:ajK#Hɒ5y6)ea*g2$ﳬYAvN[9, px-e6؎>" r}{bzΠ6; Zlb!`+_v+%䈜5A9gI-,a:<72ցp;/8Nts^_{ ^Qh zn~ kw97 IEƄ)]ʷy#?\#>xWql^a0 BU@P0(㽨Qxiidm*~C9އJޏOkxœ/|{'^eXPnJM}T:HpA$]~MֿUC_d"`OJ=Ȣu3T֢pYw[p,lZ:i,Isi2}=F+C>{d W 0B_`*'+>m';d;l`>nt(F{2~ Z< u܏[[>&7|ܱII\ ᱘eYW+yzA52á@s܇VxJEWp;//:="w>mB[ն`BL`V0DBξ淿^'f rއ_e>ϰ8ϰp<žw+=Y4@ՀhYUȃd*uz5| /}Xўy&d=OfC]s8b85cnxֹҒ5U|{~fj2Tޔm1Z[[CٳP̹V}i aЬSPg~]t_7gm- FS. $t]]]3G?6S5>|MMMسgL0T͡d NCuuux-QR(LIԑm7 hc.d{j2!~4!6 O_-u(LQ$FO{x B]M*Jϔ@alWqyx <bSSg̀*8FDie#TW'uӝ"; ͬӒ ; ދWP{} LzcxwH 悸c$Nj e&= A/˙9fાY !y%upv>7|8|]Ǧծ`-5%IEFDN":dBCOi ~M9!+F Oa!,zzzS}C㮃?t%nDgS#ZnCDl+Jrx_[$ @fh\,MpU ! -u؊lV% 56%r3j65cQ(]Hi? 1q}`r6鋗]w~W-|‘?~ڿtLjfqHJN%Ix;#  o]bqen'Mk7ʩS_8Pmgr lp0yV<)e!6hZS3g'7e`xPp فX?ZqY5ڕ5?S< .O?/݇g/ot>| Z{ oksZ+i805{t/%xT4 H͑4~DeώGӉ)f(V;dΑH;Xd"""3,~AdPwx,˷œD4ܾyE$_8BECMT{L-9nPy'eceC tRIU<XtvXaEƒ>,c>!ٓ_-I젯fs3ZqA$L$D$dBRU1I”d2i! #%HH QVjʟBb@< ks=w/k0G~Jn 7cv/KBx >CV !(gX]#=8HvH۞yp5z'gm7;?;Qsp^&b^M]wa!{&ADNP %Ss+aF_ [ R4xP bxSl"Dge{ #*#nD?kX!*)WV2ƒ~6`UaH"qPE$bRt' :.IckCć!%s>t91}]vU,šC  -YcU/(򋧟~P9eV=[8Pp78P#Z>)% guNPhUkld]& ?C :k;& |j0]V'!l ؟5EDs&2;~y3Wira+LO4\XjFMC@: .w%r?`[|O:KxH l&a|L3y?A)~{FC:Il~ uDvH&RI%iMn?*x YQ!( ("%s7HvX8 2r5Wo6m4<;NHmɕ/ed3x/=>w&aXiLy%]Aɯ ''095FGƤԠh**_[ph-g,+ _Q;@`b>jАRd"9.xKa@^*V}Vq.9rz_g';ؘd  A SڊٳP_碊Psp%0B$7ak8 .[aP:8 MMMEG"~_a&o4`⢹G.:Va)N,Pq@с h??VX;XBpp0ɡ@9 UhuD0/REAH/B#hd.b7< On7Vx3\{l bahj1s5y3r9^J ڝ񄣤NԬSCzdOJ߹CF1\N 牄,APM4K?߯Ca'R8v=?dM$ {ՠ\uIUSPնyMW42-{f{7_qu 6MVr/Bx{|pP{Q `f"G!dyX9x|s8n= Įf0ş5ލ9C/ރOTo]|mg`(>T!3T Z\(^CL3%A &o=!4z-G]NK*Α]ץYg\KmE 趵[%EFsj_hOxZ-J6@>!U icdQ{e( G%`k sjIvyӯ% џc؇2 2gD) "EC:;; %Ƨo_ l4<~(z^Jr 4.҂}.u=KLIԑm7 aAκJh˳P"'!ylb3Tn$؊= M4?ܰƲ A\:3\ReQkB9s(SQ<% ,oK>Co0qLtps=|yR  st,VjsJ4hQd"#c򹇰`VhdD`?44쩭LBöU^@W?zV p^>W?J3wxt@oo/]S,FEE:;;VP.С /Ibl'>,~iv=!O7xU݁]oOdb!<СCs [g6L5&'M= H.<cŗќĂ8 Ttlק f\Jrl?[$`s@mmބ*V&;賳!Cj2>H߂9eZ@9jO_ж424q3>(n_Q ֬(kx9kkheTxۈc9Ƨ_8P~W/C6az- ENLqLRL0plv1)ut"+ Wce(@VU˟_y\^rγlH(BY\oaw"0nz;^ "rU1ʵEK\[dQU0=YK2OYS{\ Mڪ.ȋm Wos1iE&C[ 4!C 'Se,qKShQbq{9[f4S1IK\OVصkW$0;ί`ȃ=@BfR,%;r6t91C h80[Vnm,ypaJESSvЯUߚ5/(h( 7Ah1+صT&jNk4 dتKppPR?h֖ -yq;;vCA B{Pc_x0Q^[MEfߦ&0}(Y23W1360h}oΙ6ܮ_j(6 1a#E,{WTWJ$Ap)!UP\c n ; 6HTb Ncpv*F|!_K$U0B/ќ Y֟ܕZLfV{xp}#qCYؒGE3o>Š $Sମd =hęyc_*xq%m$>Ys{ԈB 8x5[̒PU(4S,T|pt,z unj}\H(R!tﱠX-s^}}mjxYsi`Ebʝ%<@Q?J3J룷ڄ.PDca b\RSCE{ElEEHDXby%FT1|S )bS Gg?YCK5P>iȗ.҂BQw`&)uxc_`c`"3TiH/S5B`+IwBa7>Ӥ߇:>S4kj Ӌ_zC׹co:F7z ѵ` ć?.'~啯sؕUJPLuQ 8H@3tXpV&.mh=&>C[RAb ӆB!Fu;0{Xgb$J3 }@v:ogB/#<m PF?/hIOA~5^M_&;4\Պri9('QQDSë<ư\LBxIl5vsиl[`[(H/A"a&N\)2';$UBp_YUD!_EAb؏+b]뻮3oB<j>iM{w!4\݊mjEJ?1fGi Ӝ{EnKch7C&n%QyJ\", 4ta`&<B  #t 8 pZ\fF}ۘ @@{QsE R2kYoraa_͡Țﳚ} s2ken?LGνjTJ?tE JL([єA8@Qm[‚-@A$!!۪C8h FE=r !qr,*;ށtګ05ѩ7Ôt)U$ 8s 0 "=376"<8ƌ#;y.YtֵPxs_H\pvڦ|-o\O#6T,PX^AC@{)$o-1 ѯ|*C1޷7m o YQ P~;F\q۝t>) 퀸~s S,*C|K_JgL Hw3à{ kڎ$Phcæg#C50R'D^oH{k^ R n}XMhm!xavGխݱ y>To/[[PpJc&`p S*8z+O=ToGmáw.6,SwCtMfg H_~͓ TF𐜜fBAz82 NJn)hłiug5α$[DW6Y yx`/S`%-!􍂝V~P#_݀EZ1ro>8pS2UD 7npcy|;強r.}j);yC"?^=~ [;;1pGq2FH+Mކ[? Rn{z=Y;Sݍx<2?qt%FnLP=C'`Ft'3{V<0kl]r!;PQ@rW3' IDAT 6*G^U~v>쟡V6L|CݲIUUٹMg'Fx`pnNx8=ƒH$pٸ)<,P1hT&06_t$UDF^0 U( F$ɱ2 ?k"Fv0Q,8d\_M%K2k{vtDkdvaaΩ#=TÚ ײzi<NGL@j| B*#@107f.+Bqp4jkY\!+YcFbnIC.JpbP7ub)O~^El][WIjA'eW>v=[MZC5U8_]e1諸6psnݎϿ].Rz0;|'{\, @@_fC:&8Zߨ߃]{+^jQWamZ FT 8BƁ7~LAʃF $&s)LdS+l ә$ҩHI.?XK9$zj3z&La%"d^53FJ@v`bc,=jr?^|4O_(٨t\9.LE4J_Hz{2Wsp_SB,=Fi*'9^ 8uK@VH$ҊB5s`\v@qu)ǎѣr Ā6fJ2WB!3;n*|2Wq;0;i?|!#2/Cy![:6X|C6}Go]lc6v7CiCP@ YN7qvI`599m9MmN!T*W.c*0<%=߈8ꚼgĆ/ z3W١ȫ6)\~i1E a_aAj+찀..5Q;g&cHn؋ 3x^ϫwVTv BW2 Hc09XՂo^w/]`EkS!*+=o@GW{x3\yA//Q2ΥG- )THʦ4]Б14{zimj,ZQdaA"U2JBvX΁m=#Ͽ]xo3yG@\;,+gI+hZ=sk[ƪ .^-G.ןB\FyZӹ4Ԃ]t@Qe{? V3ЉQLGv[}8nϯ4QP\TA!qP# ]CV׊wv*IZl;bPkkg3GU<5&ekZKfJcK,Py}$i7f!%8J;9KdΘ}sypĦ 5>R.l`w 2ąaV7lac(g&`pta < IU!~ir𐇖װw<eUY%nKRнD`*}5OC(HIZ,_eATE#T!igsP:!H* 3ټRm1/X X˔,8{j.-aE7"Ix!=9:_~AA>S-Rn'^+Z޶nC8V*A5K>W7T\w@rSu/SSs 忡Rwp[vY%;@ QUt},瀡!9rTw+&9lpUN~q=z-d&`pTgoxA(}xkCF!9fӰذ׺%f<㼫*6飙wF%E Eսd`+/,J^X-jqUvpiMCRS1.~="LF0f<ͶTo~|d ma|)ٺ~ Wr$pskU>fڽv5{ݯ>COzp[aT|3I(>C~<.d^@ ebkQ i9XqH8 yC@N&&LEƇYD5Bd /89T!;<% 5]glDG\"):-['G5cB8\uOp5PTC{{;>P2І,{9td-w55Ed.H.tA޷gIA-Y!;d<[MZSOOOB{|0C; 4ǖ8[|_g%@a=m/e&u 6Wra\&90[]d=?'F n-mmC}k ~瓓ep9 t߾81>(Ud?A7<̛9)Q ,(HB5 8;Ǜ3Ir:\fs9prbğu Dvؠ=Bow LXѵUi9/L!d!%nv7އ~Ͷs8蓕Vn =8@G\;Vw(FdYE7Uc֭ Yb{ӝ z ۩ҩƌW){Zʙ^П=z!3<11hmM\bT0Hiwepg986yƑ*U >7[SadEXEJʆ[?tFwb薏n Usqq*ĕZ!yUW9Mi &wnMf ohoف3i3 dDȺu*}gu+mu*1z/F"se<'9X[m<QG^Oq,>PU3Jd=}TIze޷m"K6RwɌG-שmmme9ioZMww=U;G sKxe]g3j%vڅ~.Vai3;8Op(icYwl]>:+%RFDM^ nt >< C]fd2ȩL?۶\Lz4^( ~yƟM/-yE({`b+:3khkm$:95HaP59g1GvX<ͯ9U\ ҭc@%6æ,eyE$ґW˜GBj_VqޢO` ?$#;)&.WlLAK/~ ONZWxmɜpaKҰwĀɟyk5sΨ<<4TBw2Hs-p{3}dH(%\4ds PaouP S7 qyćdlC%ڄ ;fMy'=+%ob{IzĂb^>$;ܻ|y{dGŊٝP'EQ¼?9vhu(ַEuN%f"p5}nLvMrը"B)1DD7eJoUcs~JҒv υ P>gk4*:y,UbsuFA[А HfT*m"`kZ?d+md'|,wX;CpxԱ,;`.K6ȓҞ㧜_|?-#&Y{)3,L1q~KL݁ޟEFazI`m#H 3ˈ0' 1@^ N6\\lK+nfzׇaJRwp1aSK,] %g?$sd7@s|p-P"#&A|)#;1FNfƫZ6LWUdm\/G)BA3N[pu鏮\-;Q%9(@(( iUŬFvhCU D;A#LsN<'=(J@q2E1S!D;pB``ei"Қ8j+"ڪz HM:Qf$Z sG7:z;lj,B?J)0+}}0C?#MUޫߍݡYMr䪊fB\oH6Q@@ st;5#+9Ga7}ɁC(/-h#~Uw0TJ<{7k_czrڞ tuu.Z@OwͩȟtΩ1V|q ,UEu H fZM70FS܎]GKKu Qjbkq!+e <0[mdi`dFm1sG#{MIN~[w_͆fJt{^A LBn63;X$<$LnI) j6K65۷aK^LLyYF|R)gU5ts-.8Ȁ6 --CdYnK !U䖓q*䃄j fljl%mD+a@@"qh{*nF,%Cy7  kPIV->#; ֢wߦ$\({U2I5Wceđ[1 IDATyNDx^UP#'V8'11=w-qu+K82Ɍ!AzG^Y-W]rB&=E)Q61Ov9~oi-[ Q DBۇ|8`hhCCCuNoF[[sݿK>#HX_'aL6>H4 sg@(bSoxZTw9Cv0r%~zrj|/;a]c95$oO?|r-3n}̠`EeRQ!V݁6l):YK\Uz6}Dπ!DN4Wpݯv!Zyu6+< P-Tgy2;: Qi] ]c_p754JKl,12Q%]b| [FҺeT!5Τ,‚8Gr#5MXPD!-թǧG\LQݡaLE d(^P^Lkk+޾j[hkkÇtRkT\ltS=q|mUڎ9TwCv4x OUzVʏlr?77[\eI,hmmŁ[̈epV2le};\B<Goo/:Y6IYkp>b&at(;Ar5aB!MYNG١0q~TԳXU1f4  瞵fdUtNHUYِFᡪa s>x5xċPR)KV|mA7hF du *1:D>Ess{t)ɡxFzy)-bQAPds\^-᝷ P炍IX0JIv`ٶbMRsIR5BR6N3Dv""sQ5TKqV;Z\'`ݍ7е@bpfFv0 BGQfpu(q%e#=%; q].D_KP%J)g޿" UXM2OpXn6T63188 ѱHXA__۩?+eLacWKg<_Jqc:o(:̓GSxRgr hܭ>s >"H\;~tvvEΧ8x 0::jE ?p;3(.PqXlQu G[;!6qe2>iYg D0[m{b|e=Zu!z6e Cϵ- /~v p E .yV(2̎U>ၷp:Ck-hv/&8m9*|> U} .ӃW9 8JR}p^5p[ivzS?ʂ; e |ʛ!0=՚O|q JcM?pUS!1l0{ g-8;mkKlaN0 2v]7޴ke*$o.6K*&@aY-: hmvspBG$Kb'm b`PcUDi/Fv} B WgT% |2nm8@_Kz">k~0ISe{ڌjnc|_&d@sv/V 6\o.|U[4b`FX":v5-rx)`ӧ a 0w`cj cjjMeV_  h=D3҃t8ZduV9ɲ(rJ8,R߂)>l^!"&py)X ^wV⛨]jI]dĆcNѣG '!SZ%==t3a5g2o* ֒h`Ύ܈F[[[T;lâۅv@r;ɟX}xxV`p===?ch5eH]_u3#>}G>VU.Y-\݁)99ٺ;σ#2;վ/o.sm?$+m[QUcFc0w`&`p A/KsɔOAuߛ.9a]ݡom*ë3x[%7ZVy0EHxIε";p|`(#7u#)zjc Ik$;Xj??m`/W9[+1idVXx9`CWⅵ7RAxW7lz93nNY#?o%<⏑_1؊آiSRߒPp W O?cP%HTFL Zn]bԈ_3kY'ޚ t ? E{VWmE\UiP.e8~@zܢhg3Zy՗ᏳmڶhA_Kd>W.@ #+ "d^@JS3%^lFW rLacGc4Hyi0 hBWWb0C{{; \w l/ #:ب>n 8qub3d@ 0t%& %u("9(3,I5c=fůB0iQf^,:::r](7ژ׿L>[[u0[my=&AԸ/7d>lhWνgZ""MkI8f[f}!넇/F]BC]r:!s'sePi򒫪 |kblY]xC`l *N\ ҺQ8H|609@Y@$_v, $$ET١pwY~A1 \ɥ0h.YdrH6T r ~#<0e++"H+pjaĤ bRur[duknpmepR KL]1{vq'ǠЇc١̐H$p1<EWW &hkku~0x>n gMOR;*e2oڶEUk2-@0:A2A٭QkMgvUld nƒOv䃼@A\vQ> 皚Ϻeappn:n='Wcq/[sOٙ0[oz.@ Eߒ: cEM)Cfɏ!^yev 3ٱ sO Y|aWfOyቯ?\j)fk:n_ aN+>b=D͎m>wrlG.c[TF%N{ v~.‡P :/< ;@9KmI^  m&vsr>H_ΟŅ"m1VtLf2?2 ;x U D1j yUY$qG(,Sv V]c>r6H?B& %)5)5\ uHz` 2m8p0 %A*lܾUhٺq[/j,Юᝍ۩| Lx ȪyBߪXO5|FOm!&E Yn AV/Sy s9١~:;/^?};<5Fgg qN(=$^ q-[Q裠%";p2Rw ١F ΗsfW7:u|(YmkJtsM˜~WP^ԓC;D^6mmm=hLIihCO>m35S?QL_ɑi2{>tb2Q=x2S?\h4˽T̩;@b[>Pʃ裏 x>XHՑd.78O>$Z[[Y7z ӭc^+Tr,v^ a/찾AHݾn3;0;gW; P<3[jvXNKyx7O}ee%˱-%H`YL9`zQOWO".5s\sЗ8h *ff>92LLj.[ʏ$@(N"$1L`DQ# WW6 .B'by[v"KvRJ4@0Ibj]aǡNuq>6Lybm@D {@F:҃/j+`kQv8Ƃ9PuW^m aLmHc#Ww_-͵,z/H6q.;H~9oxv';ge@kƑ.ڂK=>Etp[yr| /Bi00, Hȑ#ىߡ?ZO˜ӵ3w;VHQpi& ))aO7yŹYu[P;)sj<>BC;cȃe Or-J{qcpĮ](̀:/r? #>6\~a|Μ Ɂ9,y~:bd&mg#}tYo"uFv`0 +R0 P|e&`p  V=o9V(Y9TK2$>?W #"ȸ%ڄX[d(: >z\W{0kN?G)ߦ t<֢֒-ERQ#gooA #/}5Rc+x% C^ IDAT=72SP{pp<|9$YAJW0ײH R"e1{' &[ŕKu^5Ͷ9@x>9 X.Koki_? #ʓ L裏BB Gjj,zxFݡQx9̾?(jP\ Y%/_OOOٍ^$Ϩ|d=bût>|x->>'?IuMh-Fj[Ԧ/ `gZ,kCT~޴pҧ:?9%-{; ٠6#;0k?1rg`)<,!@f]fw=,h\8k^pd5Ͻ ec2}:^Ze 5E6@Rܠfg&-EA^RhK 򁲚R/v氵v5U ,I YU[*DaYB$ *-f/+mWWOd'L"q'~ e/"_fq1iwۯJ*`Sǜ{Ll]n#2x~uÐ$Jz𑽕a 5Mm;D՛N* Q(X-OMgʂ+ɑ*y5 _x$D<H" B7= 'gSBi^ - |,Y?{P o\"=oY^7v8Pğ+>@X C,#,O-6S CL ARI(!̋:2&[PDdu C}>&I폷͓?Ip$d{[wc`f?L';3J38Q!ydqT83F e/ݧ`^pC-%;yu7V` 9Z_D)P")|/7~kZx*WURW1`-}o]H"=0LvyYѩ Ʀ׵0< Ss(1¦#`$YNۋn݇mqMP(JlQV$E ŲEGvDGJBxIsI'f&Y 3~3fŲAGdZAJd-(j# z?4^q֯[u/@K8###N/~mn__N}lYPyl̞4m{EPnH뷬8wS}XZKw&EkJǍXhddD݁lp_)H^k]?_ <h/(%0x]A@R4W!\.9Qx.Kj-,RLOCS2AW?Ԓe 9r<,REU!AJ1 ((/HcQ)Į/=sjag$cBSU .=,?XE,($͞3rΫnͭ.ŧs$̼ : \}ݴBcXDVTۊGw m|\T9ĩNWqa]2=|{E\s٠j`n{Y3>6% ( ;,B4N hY30 H)+*PbfwhilْOu謫I[@OGѹ9>F8bjc a0r- DVLniف,k>iZHZ b1Օ0mW!C5 _u B7ᘽSOѣeY_F.^_A(ݗ_o wttt8@ϯS)h?bpTWK_e[ 4(?S1c25`ܛ*G[;W-78J^ƜX C@>6?6{"}UPlR"=F0t&!qnq<,p7荗 W.܆~aS969BD@ݡL`%<9~TyO^:~u2lr9mmmhooםnttH^Yno+zn>Wc⪌nwCnϹMC!%û+ϗnC\ X&{PD =R#7 T7Ty OXl"\h\>[|Ѽt39TnjBI~Wt .-$NlXp c% _ks6`Bch5;Ժ17]>z3O3\өGa6̭G^СCe5 L-e> J3ڋ`2GE__+={p ߅˼])M hn}> `` zQrp*^inq|]WMG 8;B%(K~kխ0sqDHӎ#R9 ĈCM^]>w!:7Emm)ŗjBGl!hI^C[ouMLjQ>A*/pc}'(G,SC5 0Yc*n#;nE5]0vc܆FOJnϷ&''19w8|Td/|01&ŠO?!$ /{Y`G4Tv9!<Վ+ \>_YR;Wu֨,+oN N`D \. ͧ3 ާ4 =p=;/Lfۤa,ow0[aVe~kVMN4/^Gmn*˘e a"Pa;Խ'gb*՝xdO ć ާd@&_CXJI;3 ۊ#߬R yY.ֲ .%:x^tww#.XE$GGG199ѕ-+` +F"SE6 )i_[r>V@X9eyxRyij= 78ᑪU; 1x<â曳,;h]J[;OV d %> 4jk Tci;)NaVfr|UfZ>5@K~6kJ#W0|*_+YƮ@ m?6Ew$CZYӏ:}$a)Kʚ1tSVs;f`xG 2b=|U8qe:::{"x~LjZ^M 23Sy㭷r:ylxxG5 v{=w eـlJ j|R9F\]5p>+;c}ƾ85^fqxpljvft -ݝ%]{nu ^tl2Ŷ2g(1[^9r To̩1Hc*ǃW_}>i%%lwm j:*΃5e'=-\<PMDuoTwZH_8c1#%qR,;=5{Zw|&:7YL-NN <~-gW`WCv^x0GLld"nji#tK  D0wcK\T{c>ء"͞Z,q̬?4rh<ۏ  R?QV6 jx8R聄8Pjv_׍g.;GV\omD!xhyؤBgrb:4Y|{Y]oߌ&eP D}C /R0TG@{E XQS(g[VH N<-\.P-xiGZ6n=ފ/JR=,w=?W/8 4@RsEkwg(Ȇ-J:%;JA"AFLE5Gk܏3<,a5Muߣ댫TZpVM>t. 9,hawcR!ݳiKZS}͑6$f&9 #X? /3sQCV ":FOftoBcl~ Cc%?ȗR՚195*Cߨ2zo*76%?vv~LJwl`CCCnp-J|TNӐ[wSNapppl޽2Cv |Gi7q҅5U$ǏGGxW(0|h BcywGuY6`%6*&@X:F~w#j!{سQT90x`ݵU+xp̱Ii].Q[ey|r>u#HH2$znQJJ$-^|GJS}CMkCbNV)\<IUK%Z ̉QH)Pǻt&I3Ht H)Ch?| |I+(Ѓ Y89sS1ϝ^!Fz櫒0J7,쿹p- lTD)r5V=<n_3Q8)̲$@%SңtXQXgu[x~p\%~# vx14 Y-[J7q<7^2ݩ3 t$,,T{)S[X g^A}SJOl N,4âYA\Q 2D4ab8}_zO^:Y6F!8p0N7`ӍeۯjX.ELS%M šLU\[E1Oe2CH湧Las.,rX:uq4r +E! 7lڂ-ېhe׫dUA&;?B?@ td;kѷ? ?a~z IDAT\w0tz X>GcW _~\gJs[7֫;QV-Su6O O}?K٥ G/럣 x0mĆ@qxӍc``}}}pO>$B2i4>ӡ } /`X^L #M;zJz:6B1ةܳ}O,a @THTIÀ7&AUlR&l ]^a`p cQKvd%!s 8%uh29J"z^@=[ 謭v $ؠP\RTac3$ݿ :5& >:=|T@jm)>6ڲ*([}>㰃Nc` x/] Bc #-`2*~}_׾5ԅ7 s_L~ٺÁC6.tkl|x]EA%@JؾDe 1YFBѦ!jl~ͪ;8C~VH!} *YAaEDLWA ˣ Bc>Vo>fl Hu>u /;/N + [lW!ļZođ.wlK]𕕿^z{{,5Z:099^x/l+3H8:}''pdv_Ç Dg~΍[m#P@g1뱊OB[5?D]l+x@ o,wk!B6 }]=ҥVTxBA]xti^lۖU-eh#{8)Abu"**M2_JAűX3TfyL$1?d8{F ^CoĹkN_>n˯qYr`nƮ;ȪGoᴽͰC0"#)ܰC˾"j ޣ`> %tPoL(1W뿆,Ta*[Biء N'.ANm}-˶8*WS)Uc!PaYa;k,nLF f!9×X&'.o9N@=x< .)P, ]/W<ullk) jR%$a**Mטr /k2$xUEˋ"b"d!+0Ig8OaƂCR}G7+v,3ypZ/c#aU[ q|ٷjkk#<8jfvw|S||3o8z)94ooq8|s"t W_G;NXT+Id`ZK6k7jԵDIHt(sPXajCf}mMS?2YtA ŇDEED6*h YABQdԹ,LݟʋK ?p>Үپ 7~csK8Y2"ԥQp[> v{c(H(מ_zwN ls0f2Gbw]6NB<09oDhۼqݹ Qy(A;t^w:y C5i_@#Lx0::t!e"`7}fkaxUǞ;sljn65U*-R`[y>oj+P9c3~/eK>or{Nh <t &_ #_>&GRmz 6daj\"&ʠb2H~P"1O) z< v &"K!r _Q{V'_ * +qYwi@X6ھO9P|gw =0_=*DNTxptK=ϛy8”F"S9ONcmmmŁũ 0::}{я~d[YGOc$2#[~킱fb>VV8/6߁8͟36\znD|E#!`Z?RtJ\nf!@ބx+e4nhFpiߢ}U@GξG EBRo5i* .^[}v `fT|hTa:j1Whd ou$PEoM B:mt0#pcd_3ޠ% |Q ѓٖիuMr/ٗ z Gկ,Ty&M TОt|Ic[|\[Onq|]WMG`RՁe'*{>7UQmYe3:ܚUɁw0*8McZ4dQxDZACuSxmd$*ޞ}O ͣŃeOBJ e͓Xmǡ\::E Yz9fEJ3c+@vXw Ck1oYl*-뺱p&ΣOF %YfHx O4P-ڴ?R/y \}mO;qXxw1Ocѷ#*r$( @"ŃRE 3"v d,xotx?˗?LY.q8 3Pv6bc#;>ã} ٿtZǭ)?OܷlׁSḄ$?K[Nxҟumݟ*e=`'ؘ*A{ ;cMi؁Ǐ +Nmu?uW士م˲OSa4v41*)jh쪱Čv`Ǭ4wSqSi8V YԏIymZm%b%qjSZ< 8O}=6b]Ao)猫28[`;,&,'}IUtǪ|` ;"PRQ=CzUd]8`ņ6Ҏ1PwK晷 ^r!ypVݢ{s.d=,_xzPDkooǑ#G8t6؊(t%_49(>sn 'B0pu fUǠeJ-QU0/0/Jq̋1̈QD4BօWB%SRO~7P<\ҳn3;OB:o=[\2g?  4GuKڡ4 :J^o6BRTGIq3 ,3L!dQXL{1_??O2,,PA:n=' 3mU_ڇԻ0)s}:t'JrppPHwfc ҴGf%5rkS.}<ػwa l8uʘ5S oC8 ?8.>4ZwHAKJC%(ƿapȪiYK`;1,Nཟ7ekNާ {3._sc. Ni nYDR ec9nHHtd+Q1ED zBq Y@륕q<,p7荗 G܆',kwvdc'q*lleyvC zNNE-CGW/mp @ ǃ8e=3F"Su.'T9mʍXۆo\?_4_znDA2T *HPBIRh=+&㆒0<،JMmE,/Q%sp#YBBUp _3uvԵoE__P]-cnEOZML3ZCF[,ĂM?YUPi lIL<龜q3oi|33*Dל~߼}مgqֱG_7MS6ߚ2ss 8nkG禴 BM.=Xg JQ݁V)>l _=zt6]%dطoN8;|v3޲,'>tؘqPLU,JyuU4t|&@HEVO ;7UHT3Ux_[㌟*7;?]OIDi;O杷$<ڵ[0,Ϯ~J)( !d"DX027z N~Udp5ϯ~U$A]VvϠ?4c&M{vwa!zБ݌B ΍!cV!,9[cĎJ@"ol 4 7 ~OMbWhs8C&Il;ĭϿa{K_#)'䄥r=G{{6`~?w8N3,ry8s Μ9˗/5住p9|xt +>aWMKQ޷ ͟ʼn%iCJǜlŠ99XCPHBR"ZԱ.{o,5 Zօ%EߜFJUavXEU%8؃ȁ?oPbl$!{hnhaS1%p_VU,˶2 F<d{𡳩k{‚Lӝ`,ͭNC 56/a~ARAЇld%V~uc-f];Lt࠳+1W{WF.^ kyk`Wt"Xoo!U"`[{>GStl@r:4@<j,1#k_-/A;8fQwȥ)`qJ 4{rbsUU ^Rƙpj󓿊_|i sAz}\\Zݴ Mc}I oCBla݈ " l^M5@:`hlTw(؁:W R0I}eDY8$bsJmo&Q8Z}ډѥ^12%$%~{OJq٬s>pmR΢ܳmmmxyV@nn[s>`CCC𰡠,H8|EvPTZ|!ŸfD[?4 4@3zΦZ ~v?ڃ8g:((kσmI6!%nsX&U l0 ;XWUw`p ;!Hc9*i{EWW6<8 PAڿO="=lΣʩx u6tBՊVu*-Apz;W[~ M>kX"t;owO]mtqzVZ}=QR)+2q}.^-DX{SЩ`eym>{<%} 1ɢUkk]Qz0 =\\ZğvaY"v7ؿyXO'އIe Y^E,!;ﴋU4CY $f,fԯa]>p, +vX:NXB=p/jaxj?8'&q bY^2MUJe,FoofEJ6۽{g52044cǎ`$2#[֕n4?낏 ~~΍>xP/:97ӱQ%ݰ"(@ea, H)2 - hH Ţ/S4 aq1e)ԶZ';f^A\\Z[U(6Ā"Cabv!k9W0 (,O`5M*C>kσ>ϖj5%%6(i@)qGk}K( +*fi_щE,1Cijy!TJ^V5?@CCCflppФCD8W5ՅaǟE`0~m<[_m9M)\u{غ]e5~ qvpr8Mѹgwֿn7zQx LYd}/vjvfT.iR,^7df/\,_\PEw21>TYVATU[k)pqi 뿿]2-B0o0DP)K[b}\,B4g<âS aVj2еU"J$aL5a (*dRJxNw=&UY׼gWdVq<,p7Tp} ~mwK(Pd;)u#[J^{S_Vmmm8rʆ3oؐomhhe9/ 8n]9mS{>t#j܎cx_u IDATtO$ygaZ$[Xl! k0,!4N܅>U$j`)-ŀ,lDzlt!^w%]iيNo]~@37mbhSsjldUEBQEADE]#X^,0C& 18υ|J kP^lt'- Bci._ B]:5t4vxsO@o݌Gw_^VuHvd0vpLԁl}yRF 51;a82*G'Z;:," J˶<4-֝hn?8}1 T̿*˃~]B=aI^]wގ=vpl^g}_οV-NXWٷ;NHY6APm" F-$496ybCT#yͷ@`W'cIdP(!=YN\t\,_=&c J.C˵Qա kna$B JPh<嬀i_ X&CrLׂP7IGlceAnu`oF+y:rUtڍ| `Sv/Ehy?v!7_>U+)}GO[V|5:,C ?~1>>^־{eq}~CX0P{ tms7wUǠ./-dBRUT@@\ǢVnr+UF' U~c[t`y9upQָP;L/|핳yO?REl~?z 6vF<ԸN{"t CsgqF8x@ڰ.îU9rݶÊK;X[— :jppSz;oJBjTp&Vz*젨4FAT\FW!IPqI!`MC' 0G*Y{F@={`W|Q Į@>`| 5[ DVtL*'ؕ={6D& ÜRU\{vU J]ݡhemF塯###Φ_2> E.TʃCGG d@a %˶3嘊SuPmeuPqB]SQEHA,_;psl8}oj˫஫u2=nP3\l6ύiγnb! t㮯B)<ՠ[CA GCT)M=KFfC6ak $UaU|l(tzCV|!!L1LvD% ;LL \ Ƣ$U;Mx'%/: sRlBdMT!no.wJ@eED5C AVԚ9%W>6PeTPCu8j8_[ ,wJWu,H:eM\>|jzݎGO+Vã:uz@j'oV y ǒP]v}pFAE` ghwbg & Al Bc^ђϞs_םԩS4uc[__|IGAk?i؁Ǐ;Cmhhǎ3L>¸s6t8FJNP^7Fo0ByI^E>g9(N?Jxcy8`wAs_~DX)b&e*0P|C}xI8TYír}m"~e|k;ݸw$bG}|<a찦RÁ2JAˀCqUMÆ(VU<<,OX'T5ܚyJQ}cD="Z/'q}, ЁQu/ȶm fStY ;<8p@n9+ ߏo~U =J1J1{Ttգ{q⋺ӎ'B:6ȷ'+b*(W;bX>0fɁ*d@pX!T @ >:j mEu_b'= M]}N V YLVUOT3Ch4IU!*L]I2 SA1b1#|<b0 bie" ؔ)͵=IsE4v%Y(*}@so}f cv`TJࠡtL~SF&k PНJm&~5Ӂx<ػwdccc!nSEs" eۀNY*AŅVu0.SY M> ֨j:cq-w܀2òD$=U36f~jEȻnk.A/8?Uq݄N. Nq C5J`p8dۅ=4mԯO?~? E /x? = , ڵ"5}* IިBgdP.B́rKe%gE4 Lvxf8cl=T/R_'r|[w`68yqcmǚ;q+>eK`׋+Kvc֭?{bǎhnn. axx@`e[F 6G~om5q8ls9';xu z59Gar-hat]u PRQSoڏCw~2 ;drP,K/@ 23~P-)51E6=%A#uBTJ1v&QBFW԰s~ vHw dx,,}F]Ҿ0GiC6-4fs^ر:q)ճ )F~'k`>f(8IĬ~PZGV>hf|`[{S5-. x&:PT9 'fv0AYc\sy>Y`?ȁh3_ЕFK_r;ÊM~_(RD*g}%r.Wuf4?-ڂ&+s}'Gq.Zn+o O}b8; =leeNb~ïp(@gR1;X4FmWA |H+|G=*1G-+6Hr &VE]  Tߗ8tS.cT_FM80m*l=dY{ݵ~Կ5S`6[w/DA^ .gӭ@xVLڂ_`j+$"TYyMUlW,wk\< a@L+|\z 2.AEpȩP#96"+:s Bc4^Vy59Cewжxת9WcrPISPcP(B"`<,'{PJ!) "`"X 4!K+A}òƎ1`YM3*Q EHXc%;{Y2)>bqо) Re5Cq?[Ua!KB16_VѣlUW!mXtP}}}Wb7 y*/Ŀ@t>_ءUffM+DoLu\L+CMF/`GPPj89qUI#iYFcKvcl0`%L`J4?_~%].b]VB^Mk )r4$$!HI_ز=e3Ҽ1=#h9s̙񙽿{xVRHC7\UC~QM &y׸|8Zh!h=C3xxiaBx f[CU6Y۴ _4dg"+oBB43ƛn{a˗G<}W&^?`pvj \@a9R .*?Q43aI!ܐӉ=(gI2uJ_|UM?!m^ZbdHflb58*@Z>}xﮆqWQ]~BÅ:GŪW]hM&d^ Lg1$W՜g/v"YIQrРƠ9lIDb*!-EUy z0BvL$4ek/R*9\YWpljJe}:GiR? ʼ߱D|~xhhm?oB?j&0BQ QݶF5kPjY_u::`0H^6'> COL!;rJ4XVPBG!Oz>,i\EFOOB d\*ס۳j@yO5~ȐL21N.IU09mx/~Q8=dX\sxĖߤ),QwI#LxS&,\a_S!Uq!U!1AdZ~+Y-Yq8*|0}.4]Ò=Y> e# Y[ӓF,IITe1I,e@V̄.tRCBXIl7.3 IDATk"]XUV^V)R(AXcfMQ&-J8+FW.q.R:̮_ܾZCʩ,!)G;ۆ&'<:U}ey#v&;, ߏ{b۶m|(mmk t1"%=܊VgD!\P-|Jk,Se 3|QIH?8^BWg#: Irӕ)}%ü h';Psʚ>dPEQW(F50p,j\lrUp,,WPḲZ;A`ʋ$aC㋊u^ oEcGx`TWA3}]з H9P)  k" z-3̺=._j+ 񀡨'K50O=\6>PBG!^׼=088p|v@9֕X5q sq*T]9ʹ51 %~P5~5!OjhW\D@@Lف 'F+ή?+A| O'Qw _X@Emmh~(jllW-)߆ -Ԗj84c24F,M}]62=S$(H~@ePtT5V|5HWB?Le$M Vӿ* ɒFxgq!HXy8@x{y!d{  p|Id&~. $V4ǨR*?~]ʳGJ ƖiaOfo܋t]UGctũ 4%dt?f%M8UpzV8TuHUwSt64Ue|y׭EnmjmиxcǶԹlAv0eQԃ *(c9+,!K uU/ p ,EY.RMCWoޚt@0Nz[;0¹H~7͟)A`yVFqCq?|#97`W_b |1$Hvp\hjj*ɹ)!;Khw`mv"ҴGHJt"аG z.-H*l\ƵEvT:A'xEƜȣsf'=LvRy!<в=[ojen AUUD_bAWBPqWjvlES8lX.с(T:98|2opq)Ic:8MMyVHC&C&2m tnEs+B1 afw46?:oʬސ LSI6U7뙮8'DsqmqF%x[v7`J@|`x1ܖ~ l mp\Wyln k1n?]cˡH1*+35P g:$M%Yl6҅ r}Y]qvK눓S0?_9AM`$c8-k Ka~ࣱ+O A2@XsǕ")R7 kqϠm1ÓNcioߊ [K, n3hiA5ЂFR͠龪`Ƣ(_P&3,.<BXPKKY8`,8Z`Pl١]4 CekϽ,oן73o#${G,&=VLqͥ"*Uٴqo=Wv#;:HC~ߡ`_ƬaNgrCQ׻|ķu#*K!;سkBż$%sNR,H2, ;]U轶-7e#* ** U[ygi.y!"  ESy8^Ƣ+}PQdqSzenc>lE[ ?9ob S[.=I_:K$ u_s5mqy;ԕ!BrjhOKz{{ G6O<[oUw /2<ۉ,ZZZQDgg'Y@'/^ EF 2 '-0؋P:ѽzleoUG$7Bvpvg'^:qIBT(j39}UZ9QSreN?hkki@6@0D 0oMJCNo%%[ykZ 1<ߠX/ o}J ojŝݟ%dM1WRje9qJAE5#Ul\v a̟ߐج)e$ EV !쐘 tyBzMe1"I+XJ.=D&&1o!Dq>1⎍qGFlQ #rUW )BvЙBv=eV j, +JI5 o[{o͏[֯;z{cך4j jQgj&>rHbz yoAd&53:mD5=7_ۂ]wW )"ieRP?Jkͱps,$EAB!J9G`h \g8xy#R * 'ˠeQrFv2Q(+PR24XpqtE˟TJٛJnߒ Y'0x~ɈDjoɋ-hW{?D(`b,8dzx\Nv(D^J L͍СCRÓO>;8Kp-TYsUFSS!;@!J@*T*PYb2e3~JL]'@;[@E4:Lx/>SA7& |[{u\~CL֔lR1Bv:MF~G{{S];lԽgKd"X_X&ɺK866`?.X. hp񾦦+ZekX0Mqyx{/AgA֓(-mL8D *&(\eL _Zp44|pv&Dd EBqCTw:]0u%F' 絭dχm۶*ߝy8~GZn*JamP!,=Tʍ`4MIO57E E2߷STqY O5?뚋{;ѽzmFdd4[Y5Ur^JXy4() 59j:bֶY*AR*  Ma(𒌅x2[@RT$D`\3ISiPE*8yzt\nA{ɫW󽿱 a5j_FnrBڿ:i#$+WSf(ouBtL㝐O~'A?Y_W$IW)M6z:; [Pv#Qkjt;,رE+dhN0TBvеACH+2h 8C 絭 ],,mۆ: qQ2/ΣӘώ Q`07y| ʑ!M3@iLTUS,j (2`a a;}qUM쪪GλQU̶mB$NvX:O3l.!&IPT"C QQdTqpth i槎PTYqQFB JN[Y6!H2<.S)ZPFM',hUH{ _ "X-̲Mvn[~d6@%Tw $Ӧ͎u`n rK= Bp G*P RtM_`p~] f--9iu˄;?F;fDff+rbuũu:vVjI[0W2pY=Oї;/@Yc$Ad;*GX [ɕVw$f_'qbdw1CeX%Bm9b6ʉ@Yl?C:(V ?-'37.d%3AEyle(6Om#Pc[s᷍&.L q[B6k3|La 0/p|ǃ'x^on)hii޽{ˊpa<3sZ\Og%<j]FL@I$  + ja!;%U(p2ߪ)#$ ."|#FQ <"> yLi`emARFy,d*o2&DH%\34pt ClPH( _ LHvdU/) FTrKrw4NPCq @`|k_3/-)}R ͛@Q~M e>Pi=*?\J%UW*Qذd2iJPl~L4=iz4S_!;k~x{ ΥAE\S+ 2dciwx*HuY%㷈S!~3>ߌODۍuU[:4=ҡŅVH18;:> MR]ȹOj0 k0bV +Iƚ(k#XXxm:c]IbG~k瓷vc;DdcɃC9NaqWKehjj*ra۶mMPXDeOg?$!)*xQy4$[RS\BvN<.QBRT%;Pꊵj{?n 80#Y|?Av PU9 j<Mv> F|^$ ,ЉPKB-I ÙeQa!;,os(`d&$Pp rQw j|`xp===EW$Hb߾}p݈B,hB |;طoi(6A @8 8 %;?l呪+-(yZW[Ѽ=T֭#΀S97}G7&ȹW zgAxظdA,HR9tyYn:g#LrD>d$"%QΉx_\+ZUWBRiP DӉ8Up& U9x%Y (gI2ҍSi @C,P&чsͳoGTd%=|;Kcygo G—h(!;h(8&5dlu I_G0h8_^a~,[~|$Ўs9MJJF竳:1kNX5VuBv0o""CTes}KXRIj2} ˕پy)-' *mҧ@r}!; nx>C箓B(Jv4b.Hz`h;7A-R0aҚ ;HCdK%3,muBt($=mPt}gAi|7M}B<*[݅SO' & /`8>1P dP(aPJ)0 U%A_V?`|7خLJ MSuopK tb荷p짿s9ݺ8=]jkF(i%-'4iBͥftIUb;uv"CA&]e;llܳr:?5UޑdDA| } }N30*!s2Fy>3BN  Q7쐖 H2lDAQQJ';hf,)(Pѕ}7熆C IDATwc>.x*TH*:DfUDy6< guhKLsUyo &izT?_U'˘eg_DX mkk>A^/vW. E“#]O3GvkUϜAhS> !;طk ټ2W@R8J]]AlŁ"Ja7,Mƙ"bYUDG[ OqQ$+_#jKP 8Gvȥ2)L1F5f:e8jp`d& S:6Di;5[V`nedϢ@8XZS˔a QtFfbWfۭ{2>#Q@ 2M7f|UvwRe<}<9]N\N̏O%?L6*5:,!"jb1Ac ټ設G><[^kX!MNonC|Dzj5xN|cS:7tۯT7&GdWQQͮ@Mz1.f؈}s{x^ttte q!]qp= ՛r=^>|PL&ht?;j6*z 7yJ@.ZtL#6U)Bv8x8$E`YVifAFq:1't;ޯ$#bfdHb0.!;Ti,/:P9K QN;P#%}@oo!5P`p)CqG?q2Fx0O >nN*AfAtuu72.?l呪+o(<$T'!Oȓ/& P`vUK <5t@?C렸"˘:?WۈOojo?e$@l. úN}9r}ID}pn'|'0YTνvDݖk3%HSCn:͞E~Y˂dTwȆ%Շo ߵȢW&>˸װ?x= 9 ""֚&TP/uEpRY vyG5o:ն X'tl%De}7DEH۱Uu}>#SZhjjZ3d @`=&rڵ = zzz2*Bu|}*0@O< M+(١'jL 2#O$;p4_M ;a#;d[f t*m:]r84 GӨuLd`;*qϢ`.ct!h s êـ쐗3Npr!83 ׷.΂MY0%4.nSg n[7fDe0@Q!;]]]xK`[>QCDF gRukc*Pi(KH{%|j|XdnQ/ϟ8ZvC0=B$g8WUeEos.'nAߡW?@_yC րꂢX7#!j|Uǹ['لY`o^wuͦ%9t Y9N0?=dj9,' i@;zG{(Hhjj¶m ! 9utt`pp=)?@WWW`AΜ=b5y9Sj;,ř@Y!M+[d# 4EY?h";'s lBӕ3V^׈ aҟ (Mx0#"ELE8ZL,W";@C؜v4|g(';$㡸ͧ)Ј~B`*rX(`c{;===D'>a(tfpQڛDߩV&(Lڽ=bK$A `F+ 1,Z!ϾnX"ɽ8G٬bnDaT g[ ^ef{mN;,Ct.ظ=Զa3<Hqwj#axkI# ȀxQ|쥟`ݡUDCVP"|<)1"^aJ:mIP,(>|?mbZcA-)K!eBBJ`_X|\" U*u@W mllD (H=`۶mXKy&Ʈ]LO Er~8sBfŜbFixH/|6n.51 %~PELEvH%<Px[ 5ZSǘǡCOXDb͆ ~)̙BvzlHO^@PRGn:EPbPyL> 5>j,2[maZv 7-m'ЪPY"ώbw1o[NC:=JeL@^(:u^^&+CkW')i̎"6B7 2v^~ IDATNg8n9b~r I(<:(U[f"HL(%!tN 'گnk>% ǁbH-Br^]! =:lk?{*UFG@GNW@,44C 3bq.m(b%݅^7܁{[Fa/dV{('et$Ul֠_2QÕ *WoF]o X6_D&z-5Kvk2=\ Ȯ F秫vY$B-#!;dܪ\&h: W6YlLe'NVr‘ PYY;XK-7}q~gpB/q)/Ezz4  N.谝㻔YJ ;Xx+Iwp ĉEc=f,4򡄏˄P9{ؖCT!9K5>5>hi8|jܠJ[W2;E T~t (7FQ$$"b#{"˘\G~#Cd~H3>x{hlB_0m3%~"!z71101б+/K19mMö۳$WB$o ݸ;I:l!`Umꓧ5c~eLYݏvB1y6q.#6& `cpiVV"xؤ9sG1UZOQUQty7T*dtm>AD%s~ai> 6)IE*;U~6xse3C$.;%;,G*̌~ZCå+,;| H;j8Fwww ?nhoDz[G~^@zŔ=ׇgy& rZ2F gRu)YJQ$!:Xn~ub+-#VB!LͿݟEI(2ڈJ/\^[AeLH?zNe8U 4(\%du#Ǹ~} i*9d8?f(-6ǧ05ߺ8$B3Xw.I!+,=gL;dA0vݏk?ԉcݤQ,ݛC|x`e+ Eޓ%"`uZ/3c@V.,jNy]0zrA io_8ӝfqe+uGyjjj¶m۰ߟy ]j÷VSy͋Y]-|n;(s?MʆP'1&[ VqΕJT~iÉ* ZdlrRZY!3J>/Fq&…yD"1H "0QQVp;T81tn&'8Fn|"-$ٰa TP۲*F*7ݴ/Gm`Xݡq6==m Y *s_+m?Fvؿ?!;xm (ǺweӓAh@\UOBMU!O P3+ZA{Z1hGC7;Bv s.iieS߼FF9xyo:Ҙ1u~y}Ge`*ֺÚZRDЋJLl\w|b]5|)5p}{+^O %>LGps ZØ<}tlGKǵ*#Ϻqq4pz*PiPu3{7jǏx SM瞆ɖlDqnٍփ3W>bʦ.c`2|;P3_$tXmwUU "Pd(;%P+^ iɅE_8֏Fb7ݜT{(:ɀӱT F' `j>eUHp, g;ڵ%^wM+;,! ===<5__U,h^ ; f[lÁ#;EHӪTsӡ?4EA8MQ"8Bln فWU1AB\@ED1(p ɪF F"QVyASXYF׉ !.JdEQ]q\)5[';ODg+Gdc3mLWcD)3s FWyV%.3.=V6&wm]|q K/jzgb"yzyWyFkM9&&mOC$LTX' rxpm]ֽoƑCʊ ]8ҳE'j׭o@eƭm[Jdkj\z儇W''{}>վ6," dy I*9b @ZÌG_m _nlIt@: ;:\A.$IBܴ ;#@Ҋum08=|g.D@Uۖ RK)I\!N5i4>S6ҶŲ 6es夅<-!J{#;0xF/_ⱌ`&N&:1g< uvϥf\}wd9Ixւkz/ ȑ# ak_qs]<=yZT_2S@fS qHɆo >B 'mMhu 4 ]&DU䦡^5o2!0(3gLEhq3xOϡ hS^cɍ(^SM";>/@%GJ^Kg.~Au^"&\Jxp!d\#9]+Y+PwdlH1x7~uk.Fx `. ;wMw~vM{ / ٭ ;"Xrvo'-FS `W oS qg2f?=j[p/ ;w#&IS+6+).}9нK_llnCG('ο)k[`MJ5)PQ\q69LԞ5: 0z} dzۜKxǎ;V=@͇r]^:D ҪfΣ{m~SڼazʑO^2ၒ]*jafԹ~~:d]iСCnuy,*';06e@l N@jHd$옔 Z0!% 9u~OjT AtȊfy aD#;\2 }y0*viq>%ɨx ?6C";0iUrtR\Pw %;{q™ ۮ!9}5tD6 ^.ň=zhkkzzzO`||Tx- d4AA|zvIJ|_3F5N: 5?ۡpGbb19Ws=wo^t'0XG`jx:L3SH$0H`"qO?qaK:]6 ?˃e5%Y@,-KG(8;Ї `iC$> x`\li dcPJހL6Vt`,=6j;::: OF*'}"y嵵p26XWrH2;qh6xd?e8 &R躹^ۮ}N5D܌#G#-KGWr؄NnUq⊭KХY\:*t咬|C9#d&AhK:ֵ`M㰮ukdY$'nuhZQ^[68ڈ {ĄGQ۬ ;IaVݡ^/1dѾoWU=R6ܺE ?YϚ58&g+fP)MऺCfuȸ# j}&l#$oY:ؾΛ T4̂IC2ppN۰ y,Ҋ>e'Ff'm%9,b tM!Jny3$*+yۜn;>G NoDxF:BVco39GT0y㌏K֢ÇwBxI/'܏P:4oãIN;3aBDHа1H` @pn|ije6VZe<.!=̧A7/kHYXe{Ykc&%>wrAgyOgG-Ewnr;U$YCK;]nHuf mPtx?}zwͯ4N ,a`œ TbJJ";UHӮ,&;MCZsYӐQyAA0W'NA0&j$;BϚ#;\h@ހaza,l|quu]G&"#* -Yr-/\!t#%537EHc?3}{;w"^y444H'N@"A~[{Z'Q>_˒@AǨG<weJvp)SQ܆`#mXH)36j qi|ua@+@| UԯΜw߉oVc XӰk]8W4UE|lX~P ١۰TPBI@k$R}8`.`5q۞ͯe|Cԛ4~m; g;/nƐ"91.ܰ-:֞Dbٍɩ7Vղ&&M;55:vRSQL\yٺHw_B28>>;9rT8yEFx53Аi k >7aRKFetwm. ~Col‰ 6j+г#P%BLڞo"X`0 `M`8_0+>xVC|s|j5!ք\EvP$SC()CV:pyoV}{TCÆ%#atͭfZ%<\d ̼ ߕ_3 ̸cصXcX ypFICe*܆یݎ{k6b%Ab;OfCzK7J#>29k\ablekg>u!*d8 .'B{zy@2F=x_ރo@ K/}M-*&/o &/ IDAT:ޏf!fO]/Jc\JCbc&õɏC6<}DZ3bca|й%~fy{tJ١@:`87gȾ}iV}7q` /UH<6.u6B!P,:Fk.;wg~XJ: a%g޵Hv@2QU ŕQHjM̐XdĦ4Z6!˕|F4#dy[adş<83PvHfd(NPQRTTΙHf8n*QR1<6Ov(]X3;wݻwoDQD__Ν;EQq866n꼂b5t7ѣGkla`޽<~ M糕_V(TI*,S`Ёy{/Vl2c/'(I˥foihEpN(}O݆321@{pCzf|N4pN#1J V+#; }kvCʃxאJ552:QT(v]\Aa <-gifD{Qq!ChscJrX5s:+ CXei JU5N`~6Vm5:Jthq3}QU ˑ`eWf. ohBח> (O,ǡe#}M J}b'GpR슩pFo@.m`ƒ7O~Ejz8QCb"QFpf:"`V/SY)m'a M.nlڊ-4dk:| v࠿Pt r g=:W{5-~B#|6 $!|i~ř$>$phw*Wr+2U$`zPj6lw/-?~@On rАu?C\G[nRB_+P'91s |uaGJ8·<B a`9"#*Qt"iӒΖ/q-7UNCNӧɻlx3-qӧOԩSUP%ذaǫN更2\nSb[gm,#YZm0[WsPeBW]!p;Ȃ׀"ԙ3PfJr{SiESXMU]L?Ayq~dX׺l_V IFlظȻ侠?́Tfιk q06j))띳/pw>6[Dƒ@Jf/^2vR>UȦfӳqG컅6W8HE(\sܩM7|EbfsMS2p_6Ov/3.E&s[6odwb)Li8rfXf&Ń.߰6-n6Ŗ_F̛gRNDMATL︔ƄD']kW*N} ud;X0a2dE,e١l0%ڃxh^I/F7f~jKudrIR^eʹCKϋ Œo|̝P_w<]a/~\cǎ㕍a6:i?YhgZm=;t 8]djWuvZ-Ṇ.N%6 Fvc6%;PY11pi !>6_}id_%Q%yĆG._5AxT݁U_R0.ZUwhM&9x5϶tb9MZ`1ErG +&>/ιn)&j[XǚT 2 ͫ}O~p?A"Ajf߇ Kg05~3#'yÌZ{7?Ƕ݄&ߌbWN!^7WCTL@Ԕa6xIJ,۰ 4779}1!cΥH+si(w@$r-"wiNvBZZZ&j.CCCÙd}Hc誻8}*j~b)B;I:I5$A iP ~"ppy@v d` d#beP2NDEDI-]^t.{(J ܹsѨ9S~un^ԃkgz~Z-UDkk+{=GI~>,yZ.Foo/nc;ѪFJ,LͭrP'_0?O[׀,4-9y]#D7ѧz:}S؂,&>*- M߃,Jvl\],g% 99\\3ԯD22Jxp)ĶQL;= 6hQopۭ[DA֙Qr[ ձTǩu9\=E5n+>fbVo-aI'-x/")?f;Bv> b O^Bd9c~j\g?iy5ÿ3Ax9 :-g&ZD /+7·nBϾoqd7@6FTܗ$NgG`Uw=B2ub>lyy8&ɢNrm(0!1 6.; r DsVUPJ NG`Wݻ쐖eȚVAug>U%;&BZ"; eh8xx<#;&;!hFȠxL<4BCJ1<.HK=7L޽{+c ݴC}tC'x9I;v٫fڿ?2 >OSûN?`C[v$:Qڞtzn Zv5\uU9? <Ϻqڃ;.|j S)/ѸV0T3CWrֵnojrb(̈ uVى}%:lr LP޶{'{w՞DFZYpsxn4XfR|0m"51l,Nnkntlh&~|w~0kΐtB!J85 dn^AYVg~nGO O|x [~.q3[־gf0.;%Wx1oz'%gwi؆D"P n&cT'c+]}$'f&jlR*iWzNvs 6?}-(H+mq0&s zxx`>~d2,-&QZyu`/k%j3dx~8QZ/QwdjܯT}0B)pAR<DZcLɛubYGŜ{"*.@kk+ư=HE"?35>:t-?|@={l"9PP8^ r6=4[Mo#E9]xRihK `} ~o~JZt,r [6^%3j 85AqH+H}~}.Z &S$EH oEr\MQ%>63hnA~˃]Uɯ)@ԑt2i p:D\AvX62`f=y'C@G]_tۉFGp~W9*; ;MB6Dvg< W|ނ8]$|[_x~'>x+, ݨqeߩ:Y SeԡG̏.~iG(hDI9Fn,7_RuƆI|!a*:$Eh*ҢD:XZB,HH ҢIՠIeO,Ppc0|Lv^Y1@t"TV6Fv0<,ɗF{{{{IEQ <8q/WgU:Ώll-?~:»l?e~|_D,5h4.SϓH Vcv$'禡eG)@A|T"w.U_A<seg|c73r"OU]T L F]8-(et젓vqvۤd29YFnكEd?32Ji;v+فju̦YA{x `I8xx2DpP5YIEJK01b*!"!a.#!-)T 9Yr ) s&"b-#TM7޾M;\?s AK?3,hXϳ 񴌪%X=k{La +Rv(֢taLV |5=@7<w7>pUzzzhO|Z`'D&3d ߬f%:rR.|zmӵ0.)ف,LRy4;>/oDyz >?7H!ɔ͍cZΚHNxⳟ`+y޽{ Y okkJ벷uyd(W[wERV(X;>(ʓE_TMGFR0;̜LNho೦#O)'a&T\D"%!S}ϰNP_Q(hϰaMH@C W %;ͩH$$L09%bjZD,!!+*H1p811n.Vw-P 缧NE7lݸ)E]\>Sa]yӃDQ_ߏH$R_{{;:4 RC"7(Ɂ9::ڞJ'K256I5-=->{͜EOC{ױ8DVZ@SU$'05*-Ǣo~bYLv|>Էl6_o㎠W|H%u>\gϢx""߱0Ź^Na /PL6HcZxdn8qa(˷!^cv=Mmt /> )Upn/'GїYrNjAIj^C3`rUwVxUӧOVZ SxPxUyiUBnZ--ݞvvcND IDAT"͋)W2S$jܲ`lN92$ /!`}B>D^4a5d: +Šd/ !ge h\C΃Hؓk#dH9p}#1=$k끐lL2LX90<617mQz.}AKn4?O>xFgEvn|[>m*\"PP8nS͒tnpl 3]Tlg]N@\dW5uN0[? -d xO6%;P MU(Z7CUR3ŽPw`X|r?VI僮,BN~/rcG+ꚍwnE+0%Ww^X>#+& L|p Uf̠9RЊl"Wx O~Q5gkYō@0M]w!PX+s#=8y.M|\׀'هڵM~{Hu?tx?"=Syb@ѵڪHq%IKM4 #Ámy^}A|en|. ;Ls;~gx-3lL]׌ # N%aCuQr1$ W䵻VX]1 y3:UwqbdJ";(MƅuVLA8 &T,* ƕU ڭ| g.Lmڮiعw/zDZ < a*3kaf.QVUie$3M[jdxXx9!glP Tc6NtY:d"+sC1c,ف73Ya.dTQTa%"P;VSa=h4JtwwoxV=3aKMa\J/u6aZ΢on}s+}ub9j(&Ń +:mqp;r׌*z! '^f/W,Cggehnnu P' (FpT!dYK+W!MҳXcQtnC=YHv=ZIvY\9Ś(#09E<1V[?cXMdXJ#;|Ē +?S!A$#;,m.%C4y/{d87jDe &;,ʞ4AvWwQ|v ΊAm;Dt#Gꧠǎ7Ah}Ԇqz;3XhgZm=W=OPWH:BM("ɘ+EUc8M_k١yĆGHY#Y4㛸}#VFF 09ftc嬈cn‰rgL "#]8vNLzi@_k}h[?}mkblR%D&p*v}s0-g-sLƥŰ8Ta’IM,9\xq ć 1 ䷘[;I.c\. %;@\Љ=y2mMdNBvHq!#yeju lU'23%❛S*&;fU,S7fwI9e06vXe}(Y3ئ|T8a$V(߰|K ((Aoo/>W 燰O* ;P%PSvK[+ih!J67]xߠD GKg0 c%}oWyЄ/YNvX۲V<4UElxtt,w/CN85*ꊧխ8:9 p,Υ0=Hi8!ƒY>փ8*IH k߷ PpE&=76y5yY3Q ೂ c.8K-R35ż>iݱCe "9|pl>uxTop;F@5!e fxt2.s]_x ^DZցf_z?qxk;7 e+"⊈K:y3,B;8>v5.]ϑ/|ng7TW⊉Њ!;85:%Mm833^@b,4y +Ra \pފ›Qxػweonn-*)"0DV<.5_OơC($قEwn"jqtMhUn$;5YN;an/[.|xAA(c$;hLu>/P4-!^q<|_7T"֞3M UuMheŏASڈ᱀\umr, zKQxnGJұ8c_Vlܾ0Usx?]F; =Ԛc}栯aí>}[p1E|jK;oT:j_ih{muЗ&>9{׶Xn1Ffk-LTL aQò|Ru]ӻq);v p=zGS8nS8Co󒷍S!to(1Quw ?g],Rw`*l. ;\QQXSD;젲׊ƉA$5coӆ%N8gX^#qq"o6*/1o6el=@q't+*'SͯSwXL6دPY36!l˸( QT }%zeB/zKLu7qY6N757> yBoo/i}}}ꪼ/~lV:֭9V5LUmc].'hsvk<~ `xQ({qEi77O9&JRgcxCbltzw܆] "6}݈FR*>iy 3DajF,c](١P?/,k]Pd]WdEN2 0Kcg q̿Fs[nF纆^kKq|:u^/B dz0SҴPUP&ETM1J 81N  6Q Dxdδ@ݡ6ƒYuqи{*P;VhAWU"5$yR[?nt[)s 6jۡ4q:9hsP^<]w߉ۏ=wptɉ)iCϏ>^} WpﳴPъo[ӇUa]c'Xl FKK]1]MޱvJTqp+H 7DÍ5=uO^5vd|QJXD2 ԓK!;VO$!Dm{w7>:0Z︭|}7@篝}cwf&p|C)_w5vvJ#[e!Mcd`_q7OS! e= m1Y;%?[/iA^vڎgmN%ɾ֭S`.8e;s׷q'EmRYq5[gʨ;$;\B<4KvXqDzxxߋ%0 mSw8y$i59W tC PҒӹ\>/=vzzz~q:jj`g-]I*TjzZוe;}SPTY$' E/}4d:]={QCz VOusiBxj1N0>7sT8SD ?۾w4*qΖfHnR2ߦq]nA$@BK&)₱. 'bRh^%#y UM4y3ēȮJ D<$LP<&/B:,*%C\@ 4vmh4ݷblܳ9{oھRFXүH]uf2S}N-B;pk-uoLO$N$W9(Q˵\.~e 4wpd׳wi$碆$urwvXs@:%|apDV|2nƢU{'{$Y{N@ . 8=TC|Vp A4|XN߉Dq}[_NT܇!^&"@t`iummm՝|In<~0IBb Ĥ.L'hS+7;LqNP,*[1K#4Upi$J1 Mkw|+:/3i\\Y>V/6^Fd҈dM+N'j].Nu7 see )Jc,v1HDRyN\nZvX`Q=?up nΜ9+S{<ȔTd% iWéSHԼ1v *yLM$(%;Y!t3 %3%996h˶qz=h0ϣt+:ó6ڎLL3 g-~'H`57:nhCn\zOǐxJJ IDAT̂4x%~ӴƞzRrN]Nl{]e}~rS]WbFt"xxVs/@\}yn$"#wyQk XL;P+EtmJQCC:w8}y-w)[#Eorqy`.9?y×{;}{ӷFgm}D'& o nlxTҿ{*`jѓ$=pkF- 3qYav}`Y;\Β .'[Zd(ypy։JcgftY a>%LCC|>)v\pb4x B-m\vo/nl//mt.rwM;mCbaY,"#IH>BePT:pdKik`Ԙ+]nƵHiむ ,.7NG Czl=Nq^0'O H>kN_uPYa.A ;@m`%g~>DJ` /%Р,$<`n&+>/P*φL`?uuu l0x`'Jssc/L+3vP>LsFQew:QFBBǍzUN'Wds\U/"Bڔ!xf:oOa();99y,TU1z?+n'1aB}L@VTau,*( aU,'!K;\'LWJaS b<G4%DDޏ;8v@jB$2WoӶO! 9y%l7%5iG߀8C!_PG~Y}_H`)1ĵ!̍kZk*<~hvh~W3vpWVaɒqSPF)} M&odkNǹ2;T6Z-ߨeFF InW".I>[J SSӵg0rM@Hk6u o.??@,hrZDq:bSӨnj|:0w4iq}N^!{np~.&pbW+iڍnꝨJءB{C1U/XMrH8Ŷ2fB1$ Ѐlݵh_Y2k45]V?}$ ܌o qe}`xx}}}m:ėV:m76ĴZe;XvX P`(* 6P&^;qə'Ds55AR ;xID()"LPr!p톋aTKe;(P <;hŁ`-eiT8HyIUǂaLrv#d}8.^}qTӢ$"2煒LQw椧OƩS2v`$v;tDRSY ?A@"CIAI CNrڶE끽hv5.ATRut$~ԵRu|%Z=vmQ@bdzyEm߁[˸0SpUu'vD%d6P;4'qnzw7x s^20Rg7̗]97?W_}BOO~9r'OĩSݍgnKw_t+@)AVW,agvuկ Pț,@xto9.MO:jqlk3m۾zz[鼰0#bԺ\YԖ_38YE ([sv/#_{hB:`OPYc2$CQ7Gr1`X@̍$9Dm8fRxúΝC"ّ;X-J1U{!M %?uٰhA2gEL@IOm%m79 95%5%q8o8ػJ'T?{?{P8~W[lXJ摊3nh)iڑ&]پeӷ@rU=2:]nVj-\][KcƮ\UTO ־ sck s5Ѓ߄oe$~ p\vI@M9AV/ql{m͜~z q_SWJ5DE`"Ӏ[,xrsvv\W}I8<466VO<.ױwׄOSNm\ka|%;7i;(T` w+`ǕlPt\*u8RͲae`.o2~{*鬫5ŋF gpqbW#IKt_PV3a;,=ue(sp~}U1JaDag CeQU@] .gUU;.quX7OEC_3>#l~؁:` Jtw3ghOĸT;ֽ"+L!ᨮtO0 т`yC1?K"LnM3Ί ACv(0oBwB Gvh}S#? vJt"[#5 vHEp[WvjJ`<% sw{iV $_Ƣe!rWWVSJÃ> OLg "ӳ'fWNyTC͵blM[X L~/NV[L^]̃5- YtgEL@N@ҰNQyW!AG)| Jj֑h v_H "[h)N똾mlAs[ew4àyn4͍Cv)Vn !?7,Bswh|rǮ\U#d`sqonQ_C4qx֮Wo)ߨwh;7qkRPۉCb6!7ZJfA;6>x$mhwε>U_M%n9fA1=Bq^sh[V, ~_~&aB@;8d(5iU?7g\Im/:;hP`T?ҔwCyC\6[ݶW07 ]i{zzRrqhN,wQ}>qo#Ƃ(FdaHp> :U@w0Ph9;~~sw"΋׵a" 1|05wCE^Yerl ;EQpb ]ܨB1QɀauJcIʄ4Q K{.)V;%}]Y444Xp9]At/ B\LHP \fgG!vY6Ӑ!/A{#i9cHxx??:ڟ< #*sL޸etx:CkqRu|(x:(]v5]YA=Yb TӇfeHiO<BtPdQs:#0ucX??ŧ4ڡ8P iBԛE*0?5fT6٤?ݡ17wSQ]z,J<D/ >7Ζ@~F?ɂ 4m~~x=eMN_ P!ˬ԰#g]*%]]IhvY~͹,Ѵ`mW/# -ȡVNmk)j[Mvd#ý|;P*ڱCJ:}J_Dzo𗾂,ss>o}|{<^7 /s3P%xv}cxv8umpa>7bk|FQ@-,!(#Ae\ջعshLU8ɵ{+y5tJV%촷<9hY9Ygr;: EkČRfqMR(⼭za|?}⩯>G "(1ĵ!̍YM?p.'ss.vQl4ԍo >2hκ6Ȓnm_)a-rm-n!]eȒE5: `]Nm/0e,+eDiN~6`øz {hOc@+K%ٌMMɞ_:0wT0޸>Z{ܳ{N};%'Kl2kafCRYH陝m\S3^ciUΏZ+xfglCxF[[F.qn|]@B4Pܖߕ*_kB4;]űdV.Fݣ^_T5LySww7/ɓ' ?~xBGAKK.aq͕_\FDw)OٙmBAuٛvݽ)Pp04صv(J^&oۄz S˚>9atĩO2$DVql۶>'~ %ʌK1T T C(\,Z nO998΁(#cvEO=",P AKL:FO ZKPs 񔀿Kǎl;s挾gC/i8 w4g}YD"gM%;\AvYdf7)`^e1ơ!lܰTN.#"sĦ2Ʈ-1wpSf|߶d*`5`v8dɼu?q#换P(gvx( uP֬jw"UG:e :t,#՜נ}pdXI\2P|?~ ۞nƬ}v3ŷ.۸'sM8X TY~('C,N4TlߍF`L;薏.KZ{Ijkk3%V===ƿl5IhnnFKK ˮ=teAo}ѡf#i}i 1e]e,iEyż,3G ʊf.쐖$  @E8FKӪ^ Jܧ- v@i-J> N}.L9kW?kW?[pa 5hEy3UU;/o?^0H`t\ۅ3 ?XF[e;>2cԸ9<^Y; CEGN ; $0<(]<Ņ%o@"ϡ^t;wD L>ٽ# {wJ .r▦th===hSe0OdA,SPhqmMNCN܆BvwnXh ڟ<]d$:C]TaKιi+U4Wxxy#=9͹,=WjcϤ$>w>i+wk2[[%ځzi8x@I^ ʆ:CpJǓ柘 9.FۉƼqwοEW:ƣrHL.3~?8 =4>(?6{@M9iZ~x=Y}ir?/x9Rbd\#>JuJ@@Hk/~oSamiudNֶ{WQY{ͦ% e놐OzJ\> I7Ebqwdo; SPؙRbb~TXoP!%fҫa Q.`y`]kOgNy^mo-/KSGco85-e]ZDžSx s㦸: 6. ˉ#%0hؽ*%fݸ~݁MӶd;MxȌ9dA&B̭;o˒t!C9®1C2ߚ6o}$IEc?~媆q(`zH;LUnE wxX9^+Y{Arնqcţ5f,9r0vuߑRH,TSن55OE9˄(.MNd ?5u訮E>8@@+ Jda 8.N5;x e)'ͨ>VFJd7ͪSJit~bxRtru1< %젘W3,j IDAT".;H)FQH& ZPyQ:]`ŗ>KH >(>M!u~Ώ݁p!ppKzIO|t+?% %.\"M,ڀ ^D*-BV4c2@Qf@QaJņ43^pulvv s^z%K4]C䁧/=qy "ʯ^|+_1Ѧ[gGLN{yS:nJ: _tr(79؋{ѴN/ٸÓ$!>3xxVzdCf|VJEMsh!AFM'@Md+Q4{# +2%E)]x9}-휓 :>q}k vg8CUtݼ,|YI$79n>d֒SCê/<{WWJ 6Y_75|ILpУ4ڮ3GAd$:9.e w+95J6~* 7(Дƺiy`A%X ^qxG6|k2fE(nGA[mVy"sB:%J"J``bCQN#!ZKTxS\vL܄|;R ^h eu 1 X2k2f7a+ETJgW8þ ""oA`&Ta+ڂ29f % Puid(% nUCa=P)jrҖ\>*>dۜyM[;Y8g eDžKe ;,buH Q )%9;N,ñ4X KAE +կ2(M,G)p,S; 1e ;:1&b3 vX*r1pcV3X  p%ho+(G5y,$"4|;hC6tCRS߸V(,Ȥ%97ȉ@B!u;!@T.M[.<5H~Kοu#hP<5eIvqxHEHNw;y /~9OӜ ӧyl<]r\A[Kj'-q\.yYuK ?>l+ + DY Ê^Pp2 8B<5@4z>Jb2\tXo,ʬP[E>n5;,D\XސOҌ?n߷X?*2q@BC9ivk+#=uuu%G?(FFFL'ftww/Lw꣢Ao.Dhk:;ntf2Ҕc a=mCzgd n)Ә.Cn=p'%V[-*H,T\7F5𢄐D(܃\φvVN?>g jo"؁DID 4jqX<%7tue׋.K/t^i} QiCadd_DDPOO^|Em ؁89x#&"B'H<(+sIƠdf. 9=h=8rCrtu̳cɦ4ày;nu3vϙPwa06%5Oz涮t4Tn*Cd,` -]sKghgN. vPXL1ސzpmꒊ,죫oVXtU)}Œ SFal:J<Y.ۥmy?~۲8;4x8UI AB^(PK"$YehR)'THY% ߏwp'LlKfv3w:4ءouQ~ )ޢ(b">hqYHw=40L ~-[ZkܭqwXyIB(D(}f98z\usp( 11˥cep\3y%3IeY߷v43teRWH[[[q1Kǥ/brR{@Vnӕ⪠O׷치E`"Nd!)io8+21;J8T.:84i'҉$bӪ6Kժ.@o̹wilmߚkuCPljoہwG$VdJVSF=NwACvϨ-?[ӳqx`8\mRSdmAayo![?b2RZ>FdLR\P)TY(]ʦo x䴭h~έhVҀĤq~cA>w .xݦLVd$ mdd ,ML88ZQTR.Y!>SϏ?n߇b[젹;|`9M[[ xԩS@h w5=~8"HYĺNZ<F˴P(}v0o2,Ӧ=n09/ʆyQ:r~ヒ=&pԞqj~7XgToܾ7ngw~b~Ԣj}Di`ģ`z2jȰh\ [)w0N Iiֺq?5 E~@ݬagBDM`&2Ő7o1%=U&eдM{ډQ+1Al*ybUsxmL1%a߉-qu\.TojIޛA#69mzzwWs^&<,X)J1[E 탨ۉ:(Wg'zNy.„*'C(Wkz uO[KGBRm{ǪqM:q:Q2.<֡H֗Pߺo|] M;هWFE!)2ҒI1%\Fff@AپױSPY;lrQEtw|$e@ ^<%G@-Fݍuoo/ۋ7|Ӳr~zwF; ;(T%pbr~*1d`8.2 P03/C}J6@ v=0z.Bʺ7=dZ4z$#Kct&8/) {PsoolJט}`q' ҥKѕrTo'I`B|[Ûoa6%*KE"tvvye ;̻7y,[W\vY8lFӞ]]$! tx~S\Nt|(Z$Tׂf9tٻkY4E `sѲ1!3wWWA;nt< E!3HE٥W6i`!aiӃU5v9SlTR5xhz&uCjRغg} lkHs.0*&R_f!:1\ު@z><ꦭwC"ݭY?yLnƢ ʧcwvΛjMYBZ,{ (ƻ>مa'g8} ̄7Z[2n(g^HmIvPuoB`(+ xW/GOO9q.\@ww7njGWWFFF,/{,{v(Jl6^xX'?7(_cg)a3q1LjヸFUS{~$aM87wZkku[naむxDpdnG2ӝ!6_dxczAqZuJе_ ^oQ.^+So<=$+}!?s@$"*' ;8v02bONY ,q(|Y5We}-/Na !YE<< yz j|zɈ9u;ė,Y8nJqM-YOjue,K'.n@v!25oQrMjJRysҦڭUњ"cA wޛ%uyHEPY_ 6RND$U4 :n̞5k@y?yNo|i3 A8)bUA,D_);, P2Z?}d $DA .NjDZ-G% X&j" UC Mv{ R@p>|`ǏG0D 0qY ]|{「_@%lءlx.T)fQa10v7˂DK{Pvb^FJ,ρ2\É:@Ǣ;2V5 DNb؛;<^eI+\Nz\+,(@%ϵD9Ө-Oo_O5im+^x(gΜ>%8@W>b@T86 i[@CSU4j|\yɁ I JfDN/@ÐSc)frz=hz,齨#}hCI"9 tHEa9q.'ڟf׶% 65mU[Y506xMsh='8'f .8U].x֔R228)iv)p"&dNx{geT/`[J;,/PY5EgBdogBͫ09fan2j-Gm|ivXH:¦..Yj.meԦ+ֹPY`hL# uhwzjS(-solJсcǎ9drrRs ;ŀ$*ĸTMɢ(z{{Eڛ,`@tڤ)ۻ: 9$nU,nW3DRbF@ljI7^)qw_K[CWK$̌ܵ$o`, `sQ݋3:Z3/Qow蟼RKekqȩ^rNmF%E=ħLɫ'ӳ2Sޱz!>=s2^?vW6c}u$sKnN.ۅTLq:v~zC$ ]8_ǏϿDl*l|_# m}R uh_!8ތcM8wFG0*rgvƱ-V  zp!zGS!Ae4 C0 Hd| J/ϴ2sQ>Lsa( ~E/J{[3 -,7yQ&]kvXy9 YׇNt>1HbvJqhA. @86zvXy7~1z~饗2n;wNW:y IuCx *+_9l ;ȁ I,܆(8%qr*LٴIf49mdvWH?[oCӦǹwiݛqu0Ws8 ,5c#kjk6䴠2%MA;܋Ag,P}/4.rm9^jz#KyL,DP/>:f)>= QHHEbpq6UM L #2rq@s@G͂jvybVXTRE IDATc8.̺3\p`BHtUmɱZ??Fͫ8{w4(/89@hDד{E}<3fŲX[n5İ/J q^D<% `ԎM=1HD]WBkkkv+01U{5@v1yOODTWWN>m5b؁@dHN:hv+GȡO-L:%ѦQb.TRZJyג'ǾO[vV6dщIK5!dҔ|*|$%%̛PMyv\WJ1)%젵vГ ,5Kr` |}A[Y减P(pW>"Fcq`i^j.\K7ѳݬc(ܰ9R=Zb;YYKEutdq +UA-%DՅ7|ӔJ;(Ύ I%5QbWLNCN܆mlFӞ6dIB*6_4ƥq{i?yoEUV͛]&l꼖+Hvm~2&@)Օ# + "%ׄCe:&NI<_W5t|wO kIq8p$bYcY|,9V Bڶ5?OF05u9Xl6;\@GKgfK<+Bu;ӷ%#5:0| A/^hy >(,m#sTHP\Һ 6dN'f,{\J%u;BVch.騪C[e>Lŷ}Gl؁BiyHZb3Nto݇`]E,ņvZ#CQbc9`Ww"'nZ`؁cLzHdIId{ZHA)qǓC_F3Y"-KPOeh ]x6,pEC?A9xxA]]]}KNut(P*AX1 m$AS|m ;퓥`r^f@ v`r F .tZ<~=b8>#pv.vfL0Le!gDU Fc'CZi\C>v.E@@ו /T`s!_zcJ'QIfH2P0-]1bPCk\^燆8qh3gt+wHUq(RJWj<En6LtARS'xE($.t4ݔtrJr(;E~lF끽h=uI#ڴ33HE:Lѽ}-q.':x-uXRg_m *kɦ)H",e:|=bAE "$Yip;pg!rtid8/vTFǴ *S^$R!o~|E{xpb+l:"$JN A߂CN ,pBQۋns`,@vڤI1%=U$W!qJ6(=!8k5JDDFr8'"nj͏=ظE`k%y-E;+xWI@,=%v};,O(VOB;X3;#`f^I mFO# S @'7]Yτ T A grڪ*Aσ*?ڪ+9AoZ3₀хWw.]8ka6-ǘ;7\DYQ7Q&][V{uw0vX8ps4I:x" t6qBAzk$'nBIg~2 @T2_4'3ro+L$0%=m %=EL]ɉPel(@DZSIϣUR+pԒkf4|HfFF-sx7.+-WNr+bJ1Rj)ښ66՟6-4M|zVsA>/i4M[Rxpp|dq̙S<%tg<~l۳6%bE`{Q Ĵ}0>xHo3li(MáFNxquq/}/1'~={MKa̺Zrxob3 Z9 XS`M-,?ob } c(!F:kVGmՕhd ;fű("4/">{ථ{\< %RL["ۊĴݶ20kLřnҳ[eFZkw]ug➪([r%SPݩݪ4U%w;M%Pn˲LQ x/yŹ[" {9{:nZPv "Ypn r(@M̋CJa1~ZECww7 }&Ϟ=$6N*4XV;"!sw sſ9cp8Aڮ N:EfjlɁvVtuU]d򜂔9\:l{qt}-QQ-("95|ƘMWpe\XۅWb8rշԸTdַ=mٴޠ-5$>avG*+qw".hQwh IG]]Ijuihe!\FZz# r$GY8D)N,p9`;6X0>xЃ^mr>n*2R4L<(o("LSe2MN^*gі] X mfsv"2u'IaIA9|v':<'jppp)allV}u/{1k; @:v@8-h?F1Nw Eㅢ##>tak'v /q&rEb @b'ܬn֎@ |E׆tNjHs^S Nj4sw\a1k(2o[+a Z Zw_zp\^r3v}T"=]bb ih@zzZӹ-=]{>{D^@!(dw,-ƯrggGM. vw{]l[.OG;`c/rvp.kd]A\^Rn ZI,_ƲjF\ gdxfcNz`wT?0yMbljoy=KC =ź+h*arĹ-|6 )v%a-0@!ҜAJب=kPŰz۰;`8vnddDLOA axwE009sgarY BQp8bΥ6_]P!@SMa|~7)1~rn-;;m{`EqTTō?Cd |_eWF#z#W^#4pwIk4 dS|Y$_.v̜?W8<2QAj.Ws/T埯ꀇdtD5;/ޗwy# o@M6svfX'*'rOۭʊ,T <NUT\R˙W?c{#k]q|H)QgZ_^6`6֋)^y?uK}S<*1}QN@e]?~n%mP> 6`+svϫvfv ZF`Wgaء6ak3> kk =C-X `b*ù;_ܝ^= ~ڛ 6!޴^:/" A::_GsB+ێf؁x vXm~4?TԶx ߿gŋFxn2W8H\0e`0Owggcȡ/B srlPN *P?jʟaim<El!@(ٯuCN5s/Rm{p?)>8W7NN߽p,ОY&n_A:Ao}UDQbexB%'NҸZe}ae/al]gsIa"!'lR؁M ;pZzBV͗U`؁ЎʥbABAd6|TlAז-ܵx>l$±Fg2abq.\z. 2ֺ@0*S1 rMM-Q`~Ze~9r'cj\ M̝չCx:6O`[fGptYH;ݜs [} ѣGsl][۹!ViEϜ@27 Y{1MJ݀rn3~9l8֑PCv>n8pbfź] {Ek㦰 `#Uq$H;ELsPAY>tL_U}hH*Bf/VsjjFyx!pl |@N5\yȭS9qpSWЙ3gp""t1߱]. 0=CndR (LҰvbF>He,3p0ub!H21:1 &kn6pcgq @"\ @sÀ6(#;lTIءұ0zzzL{ }. EPb~+~nh8ȹE27髾GmTkxxGE"   1bZiԭ1K;9)uԲ}@KϢ6**Z!?z|9|=x7*)%׋ R,I1;%$Q@~pu䇚wZgW M,mf0<:35倇1vt< g/lv;lv l= ?x D/>O"fw'_o=r:Z{P ;{ڃ T,1]V\Ϝm8t1W۷i [ V(!QԝtoAW vw/? )4ťqσPG "%1Fa[`P=S+@$soټiq Ta˧3H-o206n~Tq8 `Ik`C{zz_S gVNQR`JLjXX%,+fc``BT+chhhno|7:/i9SMe(EdRlCo.;SQժ$QD.9Ёu4t"VFTdZOS~CM26M;-Ug[v8J)?QVJV傼rxȧLV_ >Ql ,rxDs.Hdiw;pr! -=]k>9zȅo6 '1nR6so8 (Z@XP@*6-M^fT1NJ?.o~O=R!8C)l W`gjh/mxS8L,%\"ppa9 Fvpϝ9e֪֩*\]2ցUaN@ʍAS}䧪 ϵИYC[ \PK3B- B:"3rycL&{X,m˂î .vP2ۺް %|k_>F&'Tܦ^uK:roE޿e6wyld^rns KhPa=zDX߃yr^cN+bh Apu0I~YHrmO>WPQyvE9csDk :aW@yAP]; x ٬niR ENaX F QWpq@_,&Vj4%v\?!; Sέl{f'm<Q"eձ mf# aQONq|L7 G:N/ ;\ܐ֜N|Uc4AA.~%… U]N!o+EcMC\A^;1kV|- xaFdʰcbZZ`s8^q ;]4ƜViEj^pu}3tomF+ԷBEBBGz~OC+ tv{R\?!7ȵ{`Rhj~5;C2@`RhT:;}r3 3D\;%&w'`)w~{xu}/sqbYUN m ^2DNa+ T}N=f A LX?zį?'wv؝oD BzEa뫍bW$NΐW( ݠ_͘TF Dv0-fH? ͂0 ;i,>4@][U^ SO[f%t/K1t7۱ A]1`~6Rvب]V ((X9~.]#mEaQB(0Ν;GTTىP(8B!4 84O74:-7kJ,-%3AA߫⳴mPQPAvZvvÿMHom6 JE̎;8?|גgV=*EQR;$Ŭrx%B::ɸ;I(.%O]>Nօ[F;H=d.,@vNlFwAUvyϭpypoŇ\ eClin.oЮNCv>QBI,).oٟgO~J,;/'p m!ekv=<PtyP<#;3GB^sZV px">Le8pA099_5&''%!mCFo#>hB} AҘ LaqN/p ;>C @5; X~HZ MG X:6L!<5[!O"+}ބYzn6ء1 .'1rDڇa|0cǎȑ#x"0.^H]6E|h6`;S' 9z*+1 .:/@-MxḱJ,S3țv5t{Zvu57Ը+4?1 ?&5@y،7DQJȏLZM]H+ <-/h̃,UpݴA,[6o)[uIȄ H.^U=/v8|)]8 :˿;>'E{.f ԌjAiT Ljߩ\pCy N/ tz3aP8{lb귻';^w|ӯgIZ ;h;VV& fL( %`*s (Lt*ɛ4AQpl\/Arq]-1 BU]-ZJ @MukKwbIg-VP_MTV|?[8[C5+p!:tFqmܾ}ccctAUK P]]]CUD"$'ۃ~~IELѽ%ȅ9|9MB 9gFH}~}͂Jrұ9 :GSə PY7N)1vlb csƦ_x>#Yv#SࡴtZ-S,Bfqa^u~q B?PIK4IEMܫ3sW.*TT"~TD Dx\{KjjVY(QMycn6 |"~sT,9biV˪*v%RBѲwعBfc/cwn4zel}1Y.e`!c w/&:ҥK An ]-@ 8CJ+ 6B9H{H,/!mPJ>'- WlN ܜ>Vc >ww?R7AMa&j?f*\2_s 1st$._*R] '8Hۂuq[?$xj}k;_ ֗O3= 2AʂZwҿx"O\X ۶mt:xFmU';M-:q|/y?GEvOWgKP٤}{u eՀ"j|:K&_AC YKOn@F;FD1_dn]HSޜM;-`sTk38@{iy47av|J.JWjݛusF= Ց"'77,PSNK @Ma뵯 bͬGS;pAx\!!#CP FQٳ(᳘J$`̪߽JfLV#Z2]SA4vHf;0&LόjXX!e.&8^ DdI$- @ܙIgngvb |ǣj}5Z'F3(|nHXGW׋cǎ[Z]+]7 0ÈwjW+%@UѣGc6`;æ+5&!sӐ%sf24R îٰoB?m"TT$"ҳsHJ- :P)0;!׺wL%b.A5 Bƪ } <(V#rëiֶƚ:uvyOǔ4[O.R Bc.tzᵳȈ)|V|}=rW+ۭsW@JG/U4(bbՊFƹs,S. 81)_vXTN󨷻_$Կ4Zi T DaOYπ5e-S;gvbi[S E&|A]$ݳc1ġ'ao۲z3vPӦI]ұ+\r ~rz8qzzzԼ @Y(bX3`||V&fPfW<Լm*TT$x$g%SeO^.]aD2Ѿ޸U(cs^"1TK IDATHn%  \>sKDR!( ~=iqwoxsi]*P$|?j^U홭oGa$^]P'Ci?؊DtZ7-0BM,>9]K>cv+_֥+D^t{8C{NSD ;;H A D% E,1oEesa`[;Àa%8h<4݀n>uX[8T%@=wyEq ZCˏa7uv3JBX V#@2-Zi@k5)@wv_Qޕ.&ڛp"1z0lڤutvnGu9p8wz$(lJ4JK,%[x05#zﺻq |>PiF@jXvw>"EaѥaO**e9sCCC6vqo1Zabz2aU  7@pw7m.TT:d\?Cd-)@UrEXqu䇵33t prZ5$E)sC,P>J ضĔ^Cr'@i[PAC;lebS6,oJ23SÌ^umڱ="`Un_!53[yGye&G *BBHU_~ß{ǩ7p1o{-U,2Aaɺ\*<]!bёLM-@¶E֩E ;,Өxufbel:yN!o:]~ս" NA* dA>c{ :P U_W#?_V"zEi. <筘)ȁOcc@7|pց3_;W);G}UQ(Vr|m 1P"oaADT أxxՍg@N\2wYа* 5.]6x-X\DVz-;_/; dpܹ8;f/I2=A,$hr#Y*Bjn+vƶ ag,R/Fߡnڧn/gUK"r~\uD3e  D0 .O{<ꁇO33]cgBECm{U@СCD7.T*>G4E4ZXN122dLxJ!K5uvӅ,$YyuyיYCȧfnRKL:aR11-5֋ȕۗrȓ{i:0553 _d^<n}4ԛ*iWÖ:rS1wn# Zwֶ7â UpSR [ۑjR[ʃ\h-A_MGv,2dA%2rXvX~eX˿1L|p0HRSxLS6 ]"_QpzۆNyu ###Āw\RΞ='O:{[1ԺC{+`w-a8J⿒swY>Hx#mC^uz5Rm;n ;(Q&c,;T&͆@8-SaRZ'o#LCDa7v(ţDY ߚįs0[SN]So->\[ߚҬญхr[Fb;CŏZ Ft]n:,PQUtu#x{6iR3M ! Ӳ(n@HNnvB}]i1JI|9S@|\jСEƨTTf>ns~G+}5z#  \JE`P,HFg|x0 $6F yky48<[]Ȣ~mkY/6oms` M0sw-:M7L<ۍoBZh6[Z* tx ЅmR!~/vC=@ZDA<IqDe Ӣk]%l ̊VSciU|+~9ڔXNx=ԫΝ;}6zzz*_w(97n :a!ֵnd,C%ˠٴ&76 WL^|ކ)gk' :,u(T);[ӤNw |/x[>3k_,H8QXo$.MKT(m U|:7ͮᑅPykr8w9!N##X-NLL @Ȭ122LF}݅^rhE^ݢ/Ny Ai^el O]CBC~ۚ|6k='3wN$RP V-Z"e'-:젦$rp/Թ@\q,/OG qb^ =[>]>5~6s&ل+4_ߣs9+oȑ#Z+~JEEETpCCC# ߅MPpLe}s@%KUE)sI߸ΧaW}_8*͢E!;oNhq\?!f^eg'^9ft]g(d ~yEx_dffV[;3Cf%; u!/[$ lm[)A^0&Xdƥ]%QDAT{y]l:k/#;7_,w4|x:]׉(bn S\[/ vK27G?ҔZqwX*O`;3Tw5SKho3z._*,7}eW؍tn>.Fb=8ܲH.L+ {?vN$2]zee%r`lp=FL$%l$ȐV8[0K;-;+JhsonXy###""A(L{KӸ}6q2p8'OΝ;ю0ԺG"tՂ\l`8HsYƆ{T5vú+OQX 2j.G $2dZ|;3U:OaUÆ-V؁?F$,ˀlj{1B/5O⏿76C8Զ%g!+OF+eYL^8r䈥]V j3wK6_7;lg*S,A.AdQR7PtthcTTժ|&Ԍsf~0p c:O>lϋdΏb~qѲ[xq g㭾}x [QaS؁ule X,ck?^^Lpf v`lCZ+zqOU>22Ç F#D"dt8}.VMD^aH|V[غ1…}$ ;$A2`pWf ;;ĴTקɰvLyAA5LkRt4PvQ[~xC/."|n=4e&5pŝ8Ng^ȅ<0;o~`u****+]`o9XG!ZTgRU@0UHk:S8mkZxjc)csOp"OG:^4l렠C kv*U{_~syi}m `D[@bKnqѨK҆yeGY,YlK@ȢdB$#k'BOT6N^L?WN9w0I)w`l"0ʶǃ_~sd fcvqzs3 E3Y"ǭh uE0ٝdx9؎zў ^AӥSݰ(" `0`]U,z*-x+J#pcTdpE:tHY"p|87n pEbo wXYt>P*lePcsXN<隥2u!#%P0 vuvV[1져;*n ʸ`.+vaJ]@|(:?nJ$A$!%A`HЃEvpK .۲et6a&ׅYMӃ޷ceminxL8=PA9ꑩTC`t~0B铪s=?p IDAT|A?%`x*]^WOAW998e)ք%/Xg87 υ'p.>|fgRA^=|{g>O9,&>l蚪VD***GE]7dZTq6tu1rn |κ] 3-:BIұ9Ce]z=/? t)Idd h:Y^:wsW/ퟍkHMM/pX6?lE(3bL)uxطEDl6һ*M_,"lݥZD DҐzRC}Uֳmca{M8vd (BF a=nUN96$Nmx\%o }x7e]p ns]rYq6ۇl@ή1$A&NyI يj bW~1Pty8*8j 7n@(}/nܸX,VqZt'OĹstoY(BvL 2J١F~_d1hb=IpyLˉxI݋Ќcw٨6aN6i`sԔQ5E2,؁ׄk\+m7:W#IX,b_ST#UwaC.`0ԅPN"p⧿ؗξ7^ډ 6vޞL \s0Z/29`0*****JD044 .O&\g*3Y(:9hCkݻPB pɔ)Cf>?/tvv.ڝNԷ6QPYSf3wU?> oKsԳ;;D_^ɱk`܈PࡌhĊ/6X=*wx0}$J`6ϖZW"F"EP r{]l[u'k3YÀۍP^]IUʧasڞwW=]x'?gL{&Q V~m?[/%AE ~B> ޷|'~N{sk8|ۏH3Vgn6_"Yo'p̸wލAٳDDDDDDĉx'N-On ܦLj4) M\(9\D]Z{c]H$"ZGKC!r%%СS(֊vЁR;ҸkuUŸxUX1'2!CuzP @2htl\veZU9ExB޳m+sQ{Q{H#'94I"8\*;ƤCۖڶO\4s`ׁ}"H^Fi. 㖺dȢC$#Ǻ$MY ۶O\R~h~={mBO1(G$.:85h5C{>G/l+-9y$N:eK2~8uo %vXR/ (. 4N_~}UK+sjc4)`˂fi']a!sr dvo+Ϩ*knjbD :ʂZ98;p}EEA*=Eׇ/;0* ۱gۆeo'ӘN]vuuapp"b"""""˴~[s՗qr LTmIyhZ`܉mvu~=D捈o ݑh3Y\.<,)!Ҷ4ÐNdr v822ߴ }񱺬sL"zvῴSEQ߄vl%_BWő-3O<ח+MT\?IǎGBAsW ;߳Rw.yv1y~TA8^f!sfP\j橩iK"F:F:Cb?U^pKxvtp{ɭYU#C T+j*MCц0>wb(olrl="VZ@f]N:CY (\t ,[066yq1mxX`&&@4sjZ t:*}e 7V^4uǺcb}^֚ v} Fִtf{aazWrUpoMBx {/ĝx1[`{w2/?3D+o%|aG y"]z'Ձ4 M&lCkOHOMޟ qf{hʈutx8Pަ H(,XYxꩧկ~cF.ömomyǥKJ>=SoymĖxmG  >G )C!Oɫ0p2WCfu?'t-+6u;P: ء";P&KvX[S\PfLU> /οJT.%)@n歕Ҋ}ܻ؁p˗m>y}/_H`v 1t~Bln bϞ=fFE"R DDDMT*'NX]@ꙨmY5EWq D<]!|DD&UQPH_p t{}ϽRSzrtcvvqc5܄&/[S}17oۺ3DSǶ~[ҵ/$n2/@STGr7r)y$uH]O u]e[~sRx}Pts]h.&3\>y]H<.,$8(fY~sxHX<N8Z/BhۢO#mRf eh=+N oq*xq~g燦B4&F7̄.)GZ,KϬùspy˃qRu;HȲ LLL@.ѣ֯v+H݈UzaDe )uVpo?DZ6w?tݡru7h X_m逥Hh:fڅnk#/L#%:ҩ`hEz̢JDك'?y'N2Ny },򼌧tO.AQuCСCtpHfz"""Fs=t:mIZ/OxՁ@VQSI Ĵ+QBMfKڽv mވH=*HU[p<8{Ic_B!eX;>['Èv9D-;\x%^S|ﺫڤ8բu*#򤮺{PW9&ࡑhKw.LʜV>Et^/cs3}=B8l(]_~Bԧ3 ĵZMo`8~hȍn1XO3261y 1>P9=4`8;葢MvcO5[9Zcǎg|%12צ\.9K@0qbwG_F\4 P?LRt -h0@QVId;P:۱Qi 7(XV#:/raj$5;`<ka5i) 8 k3 ;{i ?YO~Nmx1 ϿTS؋;>:o9*Օ 649h`@4lx@,<ǁ(f3ku3OZ{,z Em` +#K% @j7Ha\f 1n_ؾ`Pf]bppО9 cxx+\.g 䰤ӧO㖖M޻9ա(B)᷹r}~ , <0 @RfN]jmvb 5(+r괼:ͭ Н `kǝZaMXN@ ^~[u a@Dab!;pwqxTM088{ZS,#@DDDdFLܖ?rAgn貫e@>jDxLDTAKC!^xݠc5.l 2з(.ƳBA&ڳ?)M>װ}#?矎 # aD>_啎cسpckn2r33{ǽ@*V^ ʆT(:DoB<ĬZ&oCo/q./>\kĤw 4RSӎ$!W:χf0>n8uSi'aX !jh.;/B컶8С?w~/gpkN.Z < v݁3oU:u {ŶmyMscǎ̙3`&LؒC7I%+%A;IE| TtEHJ<4K3>g/mu;Pv0iO7>t{,acx@`oȰAR!!$@ݏf(0 XG!(KU,gK Sx7CСCسg&"""";ѭY]@ꙨMoU0Mq!C̟}[{paӞ ]|YK4#q+P݃4 Ҿ7RW> EdgmpV1Erb.ap{; B Jsxp<#C%T=OTt> +;̯kXՂr O$(TjQ~!Uu.swѐCǶ_~Bqn`#=b @sfҹ> Q mgW!]zfA4YSh*8M3 rv0tzqbZF(;w}| |CafǂeP6U; gU:AUKyJ(e), GTc/(zp.ՅÇСCCbY9onܼQ0Pb9z&63QDM\&&)8ehi:GH%"Zb&܂s1 bupn[A!kQu"fF V-u@'SHNL:~쩩i OkJ }O%Z12"O8^+˲W>ɾŀBwN@4\v$~s椚Ãߥ*2L@RE<괻ˀ/sW 9`S~o^T4ަl/<4^;ש.@1յmwMFǀ.F,ܖ/+8ּV! #8;9 KkvdtvPIT֦MdpydY(RLu(bQ+P\>#G 6j8H=u-%([{ڍpM 'JDb̩A <4-Xg2g$ `Z[]?JTpƎGSka!ot'9 IDATUvr_H!iAȕ_8S!o$^wл=U0 곂f9vMAYL,m[E{ _7` |x}S? d }Mv=0 <%{a,'7}-lԩSػwo] ȑ#[b̗zƉ-uL蒕 u~ qv)"fWδyWN3MiqOcء`YUU ]~Oj9Nʺ1aa%@2aVZ*ai-Y|@ /<"~!"~_tvs=QTg]*:Y5 EQq4!, |J"P]=8k8rWyfCa޽ػw/ '""""bo~]w񺍥"CfH,Vc5) M\4sP/>>%-?dZ{{z!үqd窻vwm+C/k /2qlW;dfxX_cZXAZIY  V&!VqYADDYׇJzohA,.a24Xub:h .߱E?$G0Xz0{Q`B֯$OswvCb1|%K8%<;:C7]Zi)blt?c7f|:9b+0' [fGzqeS=zCe\v);( ܄,c1`eZ;PM.OFa&[ ;x |~(m_NEp P |0;T;u QTJ]?  O4ȲBQPhC,LYBأ޽'~ב.(܀k@}Q4Cfi7+dVe/Ay8oi=]3=w6},-dA BZ)ޤR,YhҌ:lƮ1`4 فDƥvc[&&QH?v`?(}Bu-ੈ}>df$c bj@K~V oWղY^rϕ2i,x?swT#* =q4Q%sc\uVG'/$(:A3 hq֫rTx-  šg/C]E1/K|ӛvKngv0wA`N}(>~k+N'G˯n"dMKY{^<xn] g ;==cǎ_'ӧqq{uS:X<4bAkpwXt5%x]kAuȀr]umUZaUiiАDlvOFVusʴ< ;웖rx]gwT5 A*_|)V=k>ݭp ]-k&aEQR%W2Y |  G#` y\<":>y6 ~pT@-%P[{Akk+A1YcMx5lfDnUWjsj-4>?hg{Ӻ:s$;E Y,,T0Bd<*=oh[v^O4\ߨ[\ÃFw,lZ[Hr"4U3^?\Y@[w]_u}D" EP˜4T )`kX؇bqߞşɟ`Ϟ=rsR DDDM={_4v..yN\4Cfi7;ЄhKJU9w,M6k5X("Y77;7@4B@KAKrt|scaC_o;xUzaNl/UɉI3jZ ;s\U)"w23SLK_vqa+ڟ,irr'Oĉ'MvDDDDvrh l,& 3:hrJ'yK~=E{Ʀ\i=;7<ٜ}L ,!_@fzE /<î;hW"mGvhgy`vt;~VMvE*j%hyAV0bPI?8SK`)ڶcbH8젷mR r-n Nv1[Ҳ3f)LG`af9(] vX[b+,.VT,1)0o^;TLӀ|^ܬ"݇рVv6[;>OcxxDNDDDDͨXgs vtm@^؁лsˢDPG$~=HGAeQ2@PHk(yvTDZ_;=wa[ n211wYs]4o+X`JG8;)0p"P ;uM` 9F?v. )jmCm4m[-JJmÒh=j{sdEȲv;@5z,_= n8rN>MnTHTQ& w"B)B-C ~VB> ֽزWp?j- fGq[:dX."*Laȁ_ڳ?D!<}7@aFC 9+0ޱM13</TP(9?""}ZZ]*A|xOf]ɷp u,!{xͮS~"jY!qQjcX+/o7,2 \wxJ!v Z(|[Npؖ:  _)~#ג@`bq#o5O^%ێ1!7ߺ _CCC8v+mo%{qr#:4 泀4zv8)O/31&PMX󶚹4N8=Z]cb VVNívr(`kU>kTL:;[+'oU YJ |--hdvXo>7`@ 7_CSY|),XQ(P߻ ݷV)xg w8<qqh ݜ.B']su*@> 5ɾru'S{k5)qx\ݺђ s(ܼ(I2 +^edAܨ>mo@o d#d Z~Ĥqg%!m=.E,$ӎxo)y ̜f/.mэ `\= ;y]nM Ö2IEȚ }?1pgʛsD;s mۆÇ;F\GHi2~8NmBA5( ihLsu v89FͰ>l1Ugee[iZӢ9Kgo?Kcc,I= e񁡡 X]z rHVaX@j^:GN˶eeHhj H >|c~gHyOcxxO<Q#""""Z?yDnnQ)BfJ0[.Ay8oYX+>ǃz4* ҉dg-Yxfvw u/r -`\Kb\E*iA:c҉[1ّקtC>Brbҕ{}=Ú 7P 񰻃"C=7B/U&C]**tVGZ(L: !Qyl D߽5_bbuJP&K܁oS>(ecږJTii(ء18j4qR9[6 'JdAQQd:T@NaWbawi  /c?U$$4@Yz0Oc^~cMPę3gO@:qw """"2ءN3#jv@5qt I?TVKe{/1ZO;>x+"md:,Ĵd `@m7qӭ#PDݑ5@Vvnh'7a4rsԽU /)`Ã" ^-NgHYx!p\SUPEAh0E7rx "]QB68PmRɹpqpv(#3Y"́ 9XAp$'qQ_έ+ƜպUhʬ7/B!d,;CUW3uآȞ2ijALJ.seLymL`{jz=~)o<$ÞpNl9?#i?~۶möml+c,v;7"젨%Ё~((xP_^@CtLp8XOtqa8ҝPڜcvЌ젷^Lm(a?[3;P_sVpv0ڏkl~=zٮ*"~(##:M-9>9|AtOx vX7+=;G (*]ŵnA׷ڒny99r'N ЃEڇ-ugH,"4a"[PYw>{?K EӰ0c[ Z ɔe'SO1y]vthW&/[1N27; U>4߅II+a_"y Rg0}f.^B[a&z!;O1EpCx[\a&qӞHDV@1q5XnB6EpQRō T(8{RLgo8Mh\RPfP1rؾw HE0!qdw>G) F[l1-u%--2S8 >d/$&Bq}"B,R ?h&])vot#0%c.8R (% Uھ;p6};sj;܉}1V݈C/hlչZrl4Q` ag)f8|_ͰĘfV_zGjO+]'YTyYt8ư^[)e;{v]=;8;T7gJ2fs a7+nԫÒdiEOi@VM׎|{7rqdd@uy6GDDDHRwr jvX PX^kO~C+["D[,_Ŝ rs[d  㯿ay^@68Zv_ؙEVeѻs($Ռ^}_-9W}2ef|/lW_-GWONgؼ{ahr@亩‚sH֫j+WRnzƒgڄsiU= )hfg} \!YY-tߺ0\@ذq4=5$V1=?bSS <,.ڃ |0\~Ok¯qU [c-eOn~'߰X'D>8ah|G:vΜ9Ss:Q ̤lI` #  H|UAT,HEL9L9̊zKX;,qKjGy`e a]@|y IDATkg P( a3&F Lv^j7 P|ϐxU˷) Ǣi$$PT!MXWrF>DKŹ"3 =w o;/X~Cd@DDDT_}@e?!m"* x}A OCm;毛v.|hDߵ=;w ~K@Bq̎;H< Ͽۖ\]O>`H{A⼭]Pa1=|UGa 찢 Ek_w+ϓ{Qxcq+\/WdTLW/fY,)[a!:b}72/@$gG+ xH2e kgYs6VuC3Bq2踳7ypq'!raAx=L%7oO#|; -&_vXCM gp>?mKu ๝ʚ_jJG TF $s|wSH?h*2Y1̃WeSD\2H+%^D$ESq09`}jD*܄Q`JG8;Th|YЫ> Dжv܆M}c堃ϻ^ׇo|  om;pt`|>t #E:N)S%+v D,O7$/<;K;8Bo~s,0LnѺJ$gi_9yw;wCd^,.zAST4AQ7h8E\b& _I#7PJ۳ ]Ę6XoƵńbssG8:UևF~]7"{i1l^ٺ~Hk{cսbY^|93$jvhϒ$5T\w@kw'VȢ_(HU>B!) io:xc F[%ɂ vDѲ11"h'SHNLR췿T8d~^u.;<,|QVJ:[j_jW;"kH%5R) ё:W'.)`iQ좫ϒ%S? vܺ ;#! |2w쐚Ƌ.~l9;?{w0mFrNTvna] k{U_(h901쐚 voڄ~vgp5D$t˓C+ d:T䛢We;h_fY2/8v#apHd-tu]NߕTI|,17w,M 201AxDSӐv/p"?rsG?p۶ n|2ti?~Cc;};(*0rJa"W/>0-jKQ>*~oM#z(>{2cƫ֊ʦ֓EV!--DϏ0!rh|h vjVZ 'ο`6eSC~sVsU h>X;|S| (Ww>ws%@DDDTk CwѲ24~f RBwu`>{k[)DZ:x!Vy6f-^+PZsۀhTBA0F`vtdʕK?X;<BY, _;ȔS/)&h5s2`C9[r `jxQ\tw;.vQ̭(f:LV@|̘px &.\T 쨳}SXO 9Q}Yp܏PF]-ow__К@3 i(?3l)ӄW9||4ty`X~O Mr<0DwQU*,@DESW$X+\ S5 ׄ,gD^ə7k>xUNk4XƩgK`;4,@O(aCص6YAFw'#м6Zx i@n:ÒZ=NN5ʣt@ UA|O=xGHTTdu%0'`k<849(/@MfIZ\_<>|iD ?Ʒ0ulٌ'@vukXG:Iʋ5 ]ua=zC1 ;"0ź?5h(*2291WXxdN3;iEX3܋>\{`}E@5qp1Uc>vߨ;8-C0f^,*3Pg!;A3D:%ׇB, 3Y} Òh'j@U? yۇ/KC"w'&<d xmppt/Єh|Pȓ?}ǒ>գCt"jh3Yρ-\x%KԱe3v1Џhg5 F|xw.{=ú`EeۼzsBW /h]k0YHJKːCAtw\\0]$Z$uavp6Zȕ+Gtѽs+PX0RDLb2S.`.8 .pn$\mUpe!YS1/" ((ݭVXƠYvtaʁ۩cG؁rv̜&6*бT}dp> [<-QdYWw1TJ4y@M5K򣴔]??/48z(N8A]R  @DDD 'PC}J#EtRJ0S{A&[܆^=񾛈. 2ӳIHMMc~+.Cfk0ڂx_o{A;w`~|d)7ms_q>Brbҵ_}_-s[N`%7;7V("foȔo 3]^:Ҿ')ޢb}+CiK3w취DȺh* ֜loxb&#ʸ;Ȃ]R@Z0;et Q4 ..UAL^3 ١+ 363zp[km3O G0ۂ.<z {}0>K˔_t<0wVroBO?4O,ȑ#э0@Q@`nDo CUaa|-"rqezðTkbB?]<tT###8r\s""" CGY'KD*98<*4iN9(%{/>^D +/__biX+v܇;w{XiAkO6mft`s$ ɔk/ϖŞ |`/cGr\vX;@~jʫҕI"<@vqUHQԬ__12)b!vr5d5(9+FUsm}ҹTpx0/1rU1{՝vyr[Jk7THpXirGމȶ}wl 5 ⳯_K\t8o| $CA牊g.R17Vshe5g{| _@.W7zꩧ022RS_ٸx;LbE`:]C^1'J0 o%MXʖ`亭J7").~ͺXF;َ^(mh(P2FL;-rVOkm,V @1|7#~,g`x@xo|@%y8{[`54'o#͌vH*@-^ O2Gv jXɢk0~oY ;p?v؇?L`"^E޻qw~{ $!%’X6EWrtc*f8J&kUtBݢ[wת\ٛD&ЯqBʢt# c> = |P N>}^}7qkCv:xi %Ci;w:Z6PF%vOěvvvzl,rK;eـrI/䍠Յn3Eྒྷ.nWlxدUXaxP< pcPm:>ÃWPp J1vPu Q˹p9hkY;Xt}PI47+ٻM7ւO4w@D<4dXAո<t#T`M_"vN7n㳟,FFF{qb6EƤ]$Ǵ(( a'dB[tt&SWfNjNxP7nw;lM7;4PMٌ~ۘSk C9*n+J~0,_ =4ωEx+Fc%@L' +?q7vz8y2G;ڊeY,yuJDDDdxCkv/GE\?<) ]xL?Z1P2~A2܈64UEv:ZzN'0Wm ^ЁGc"Gcv_ ysb"N*yso|e5~sXn:*Bb!jwc䩝9M3ۛ9A7#u󶡴sL@oˤt62>I|M%Shֵ)FtMO,ȭ"V4~%Se u;R1} :i'q{1q'`u ǗX!HuՖh'q_( )")d}w¤T*dN W;3Fu؁m`}[>@J`%/u2`_> g ˫P5~680h+i֏6?Ebh 8N0)#U!/h,p dU(emYFu`iߞ\OCzA43k3w m? +Ѫw̌_< JR]s@RcC1b xʖ7]ZlfZbbI ƃt9hVsxPe/B0~?mKy@>+ l gj_(ٍ`~M[ JnTY_c4Y&{ڊSd\NNr| /ކ6DZ*G^h}ɻXz&R]cKbX {-U$F"R˃E =T75;YN}^2PU*>8xOkQ|mbXpvyEĄCZ?`]l*WK0GُOAэԇ7J hjz|Y H ģ4F|5@.ypScK.ކ6^>E .YF4taVv뫩91 Gf/*/@=@@)&kʙxLO`d<ΗMb@}jW>\^΁4EWѣ< /X3]mC+u}Gm/Ҝ9¦X6s&Jp6oF!uaS돔żl-yv78 ;`aDAR}Ur_7 ;;Kah@(STc1Sj::[B急üpz8=N>bׯ(}DDDDHAp_ȡ>I.xvx=(5i,r3Z[ж{)?D1\ E>][u}8a^ S A:%)q,2S1}UȂhK~ȳ_"\2̭[x&9v[P' U׾T%EH` xQH$ q\W XWa5YiOdX.,44rs)\N޽imBoCcmR  j(`}85Te|w MŤX@OpkA\1xV} ?~z\!)@Tdž([Wl!@HE0)0) i*X @- 8quyp0Ҏwvv`C(l(esP&ۮ<=;X6'$<"?RzrU"u\-6, ZZ sUP0N# ԛ. t((HZ$E: >GcDd\E j-.zvC' `PB, 6 DGvjTk[mG{-vx:F{3_ŵ7޲-C_z3u|i#;0*ːx~tKDtxbdl|1._(狞̔}7ykR.9)Ew Uayp_\3="͵i"ǛEۖC .&&`\H֮mO C@$bA(Uӡ(*t TAuP4G:Y;PF(H9*SZh hcicX=O3z{{q1ǯ_y:uj% :u y智s| DDDD訅p&*V"ltqyvF HSU$oHex)S[{ʿsߵmx_0?z=B wo͐DƥH2&n,ɂ7^cMxS|?6v;V.>m,3栈C=hff&ɲiۛW}/5ig;Vo8Bpv9扠hkiAj2T ֺ;,C,kʭ"JRUICl˂Ѿs %D#Us yy[~PK(&]YX6lrwcm F BD)?(TQ1<}퇖}wo>ZrA| }M;Եؚb>#eK* ؕTEUQ7v5< 3?U&R4Z^nv;@[ d 7[V.eW\ L Y:.o-, Lj88N|{1oá5A u(*KsiF}>YQ!E&֞wP*z*eYy$I[(>]&hB8,$͗|ˋl6q==,1|t/T=|_G__Pׅ=OO?{ {"Qk||>bA٬~tԎsDOȡ[CJ`foCw 5D9)ϭeɹ<g:0woCtL"S* LNASUwx㥗g!Co}?zڏ!%,opff[&y]psABPD[y.lB(Y~ò5ަ0; >/ \§365Y\mwB^eѺ*V [Ja=KɆf[n,g]"?eA&T,_ϫ7D[d)PT]X61$#Fg0X:t"kjTRA7ZY T''ݟLEUƸCOC'!V"H;>n~àxz\_vwc"ѡ6+"RJ IC3 8<7q 7m^ vvX3_a`;ǯBqdNp%V`(޽fn밺gq+e2Ѿ@PݰEYFQZpå  ;A{5*o0v| >(r-cnk ;,[IE6`(o_~3E6Uk\ATO!8^ޘ cz/:?44d*^u;v ǎaLL,;rN>>RQ&mTЪDOȡP.K vhى_;gD¤7ė0/6 wo#傼p[K:ݳ ϶G A{XtN"S&O$ݻg c>p];=WW_ypΝC)f &j7v"̅l >ټCb2NycQTSʼn(i T5puy5[dQ@lq鬩-BSkV?nKEdfv]X]guG(\%fk삯|>_RXr>Fce}8Ԏ@hЖB{J8+Ư7s8ֺއblQj7GG?1nr>ML?E24xIY%;wK |=''ߴ4w?=4ظM;2.8`,oqkZɎfQ+%D#q$ǁ_w~8Ժ DUh:R% AߊCsqirX+`XF7O]<Xabhě[I݅,U#(CKt>lQWY ܟy6Udn2~Cͨ%us$Z}s7+@xG/@ ںFR+W7D@-!G0ɓDr*:8rC,ɞ784nBh"D! &7'9ZȂ  9DFsXSoZ~h]j܁N?X ,n;0q|t/,q/"Zb p"P(JPΤTA'or~Ҷ0><"CsTaevYe2@UٶUϰ6˙)\LuSpy1) %#0h  ݡa;O~̤.y~g+5@g8- batPKY볏ׂ1iksˋ0A-uzu;^@DTf8}4V9TSO=z0 +nZ-0DM٪R}CG}`Pɛ4ETϕnW|>sՅ47qu *%.r !<_lÎKyH(X"Б882~.(&|9E5?cYR Dt @ @=5J1 B)&kʦ;r] Me7C#Ag0(Go$0oTvPሁnaŀSe<7q /}AWq4ܿ{V?8=!E,AwZH_vwc)"Bѝ8밃k9$~Maq^r | >0=G}<w"M,FLSٗa [e7P3//+d PAS^?oCcTEZCAe:kac]ecJBm]F(Yk~o]@tleZB7Q,! yTXRP,4}C1b0arĢ~\/k8k/ׇޚ͏by8EDa-OF.gSO=q8qTV-{yPrشD,tU|Q~W¬N@dInCKɛx[/'zW1Uw?|1pC0G+c +y]_c* ~=tK@o(:}B,$e>ە^nj'{`iBCw1q_ )q1@S]&& *;%E-ROι:VaQS0#sH%@zRT% %U[bO0ӻ?(۰KeƉRij#v5?.Uq?¬=V~h. >1P43Kh [=vi7Lۦwna@5(I/x~d(466?G!f2%+@?A|9]aK_vq8y$^|Ś͉Ν`000N%<yS eaattCCCdܯ#+ KqPJr-,K(+4upHn]~ c?xՖ|3;. YUmSzL(2o^{qwLWLF9)hi )`FeJo1"E Y%`Y~GnYgL#S.u֞_6S>򲡞E,hh Lj%1Onc}n>+pBJׅ QqE:+Є o;eř!$]'j*{JsCF0#7 CNj?w?nxv`sMXu:;lZ؁r1,Lj,aC@G?^FN}8:1ރ]f/8nE0q8 QtC`T9 :Lliv0z  %׍0\X[,xI(ii,h0`ɣ86#,eyY:tV`W^cGFF022b``7eR""7f1<</PIcccѣGIѣG /N/MAW|c<;؀ n 6u5UMauU6EU^Uq?ua |v\|_`7WRK<޾xW޻%_T׳9>=ͥZ\ȳ<ߦw6mLҶ eAk;, !hB4CKSѰ8`ԩSxqvX۫mVG<SO=gvXP.O<ǏXjD ʝaRK_Cu]^.?¶B$4O ISU$oNx v2Y\KNahk :&%'r vv- [a{(<ncdn2}>t̜jaγ<33j`7YĚ^,-a{`EsO0J`lMQ49GY4^ϥUGbbpMnOgqc`CYA„p3BA`#iTYukD\yxKm4.d*dAư/X zQL:\*{^8s@J9S:t 8׍aU0-s8_qf~p#8iT (wڴWjBz<)R8j;EӠM4 "0]&1zvР-} `0븘KIϝ.p9=370уc;zˮarJR%AAg4pA o>5Hce޳ ;0;PU|gUU Pd /@+u e1muBbQ?0!ry8ƍ9mttgΜ7\[9qEׇC!Q6]tpx"&&&nvpE _n## P}Vv5w"Ҋ`t羚/SWiGd츯`t v,:z&4Fiء6t]׃Bj } Um a3x" ?_L^2O]k8Ԇ]عhPZ \ZAt7D[xND`ʸcj#ll[kcʣ+Mn07k Y꼽^ %yz[pOSYΞ=Cv8w._{1̙38t"Q4::\tɳC__pQpN8aˎ06P2k&85,tbs @D V_KBeapœÃЎEubqU{QE8u#2P #*%Rm nZAa?̋);Տ!Yi "hKvb/8 Px)[N\ŹIn*!8{ ޺:vh"&o gs茆 mn\l k݆Ӷ Lr;S&9Wv:`>iv談{1;wǎ[~W,bttxrXpy?}}} DDknX /a8q[mO8/ZvХ o·@Jr ZYa5UC`z"39割Ȃ+-ՋCmՁLU171 \v,Mx/}Tz' gc .iԋd, 'Rh޹&A m4[,Dwee E,lݴD# [i_ "cs|"@P2瞺 !o]GÆQea}vנ[U+| =l"yvPu IqA5)%kԷ)(6 7cڀÆrm [`6˂Q%U* jec@J`%/u2`WCÂX h[`?%P 5bp1PғN\D)75Hb0б =;F`03G,@g$HػCUui6_z6TvX,/=X*탦 դX7c談ghi a``ׯ__jL7?=>tEo"Dz Q=i|||px"rܦ;Ɖ ȳԻܺejOӎOXtgZn433dLvc'eMQPLk *L>i(T^l{i0x5l@ks9 `w/D}SC23ݸUaA/_˗#+Hʮ ~su* :r*AjyUאW*_Bqed3ryuk`u .{V{]zm5v';@(PqQ2q h9D}Oﹿ ?̸?p[㱎]ܽ>V#eTI@Jdt7F =W ;(k ;d:֛s&iZﳔMe8@"Xt?S,>cppG}dpp׾f-USt2@6LG7^uxccc /D__C mJ#84}La޵0!;7CY9.\ . GѣG7=8u ZvЊ7&_Ӷ @dQ]Ju} ?T ;x0<;O0pGʏ^:zrucuufϖ Tfb/ 39Z]v;wȳ_An~w69x誊tUyg A:oUKWW$ D M])0PLtMY+4P<,7?7pe~z8ZG|&9w eRd X3hcA4tF x0w͖!l᭮ UXb@$9XQ$jwct:Q R圸 5>l ,{ղj@>o4<]Z>;) ENql͠*'|$܊U5 bN^v(IM :nKyhPր!y;[r lp,)%|s=BEeW P3wF:Sy5jk-. E G'ta ^ƱHUCi2nĉ=z$No ط-;>*g/aO k4-ZuXn0ٟ0N9Dh+В3N+:u @bpppAxrRFGGq9cttr`zE@x247 4[q9h4=iKiȓvϿ(>rdG)Ѕyj拢Ń<9.)_@.B.8n Yq͛,MTmWe\~ydA)}!_0>{wfWOE$c&ϑ4˺uiZQ2ٚkEE&៿>E!-?DnmQ7d,*pe\L !˃kٖ6/MghAϖpB2 A]T$M ms..svѮ2ʠ8lJt$̱21jG@zm7̌ڛ,Ch2٪=DU\2:>f>Xӊ'O,$PkOMvv997~ӷ|1,RfGLchu`虬Mа{GL摷BԖkiz]^TZxs `aS݉͒4hcy+ .6};"LB >_)؁0VY 5 ;V( m:ޱ;`<ٹk8} L2׀8>&䪟c-BdN>7sC't q=]TBl`>S05Ok> U(4(9P OȏKPH{ 9AAhܘ\kby7w]A {Q@ ܰaJ242ra'`n#h@~iG6j"iapp###x-f~zХE@j,jgr7JÃ}!׻:@vO0sݲIQطj%.Y*I5=nWWSm8>WW\|;w~#csC޵1)7dS@m*x|lh/`>< 5EAn1PK vMJ L],2hѾb#Qh*/U--V3{ Y(DY)_|Y߇@SE;M@2ގU'UZEꗩZY\n=kKm:'O5 ]S -܀5 Ɋ@*c47>_ݬ7'UX(4-U0+琐^H ģ98+ZbTPn##m狘pwa*;,bX"o}M4mNWUtc:f`B*`l{a't}p ($ڴkpS-L^|>t,9>b־7 ;YPTLerq̊󃏡 `|.X|+j=3)$r^qY&Eyfa]aUѐM|eu *AϿ>2B.[vrkEȦ`Ft}\.&j9jbbxgرcC__=y2q6uQ6m92d䐳Xu{Xt:3g̙30j?'xw,&.}ȃ<9fZMhwJ^}KHjE(OKZs*dyH؀Bnr^-/q8wܻ۱(Re eѱoOENT.Bzv*׮q^}w~`m]UAvn<?JE򙚚 bsnȇO.MQyqWT)A5\+$oϛ\'tK ~tt^sy4cR'ƻ/>߲Cm M; ;CUaq@3P4$MkP4 Ao$8~ʟ0[*m wK_/ZZ"!@́[qK7Nv?N˘W}ObIlоbFu}d\[LZ2ˠ#?Д3yӮ Uv 2g%mMt%>%Ʉ_?@-rY4X.6 PYdf<8Ug'-!; :lM]~ R{N;SVXvHwΝ;T*֐JlX3ĝpq0e.%Vx022N>sҥKW&/A{Srpu-6ԅF-8p}r9Glp {]x d6$ 6F9;8BU=%\zᥲj>}?ggqaߘ>ؓ'+{dffmۘ[]5v^tIyr% Y-CvpMd:'z@ua~LA$Ci#n// ltgdQ/1 chA iP]UY_@Tj$վgBU\~\IZP9e [;[ ڃ-!5&ԛ5BT4yb*?g/L mMAA6:ZAGcsc&X!{ueaeQ-7*pq08HO&[rs>ӧkٳԇu) %лTӠЕ\IJx%gwMUM,qW"_rX%SHNT/ZQhmI7chEsowM':,LLU%>I&^tgh喚nB: >6QEq_|l|/a57u3e;3@jX (%lsNCMCSh7vq ztK0DDJrŮۃ{&"4BrcCLڢ%6$\ z"JUzP-rکrZy1[a) h:'O.& EWwE~* =,=$͎_ϓ153~fG6+a_ڡr1;BN\*]s@bx]DcvZ#[ @>Wn vn vUkձhLA)S\rvswx!3]v|.)2Ό_ 'T[`k i|4EKЇٌa oӟ fZ:v6;YeU[@udS"dIECSc*F^[:gh5C4#/M0@7`?.tβdjX]511-w?v2T]- T*ey[rp82.&莏n= %rUt:{ ~aK竉 +}ŕ-Q:˔2lYk2.gju&01(opnj mFp>CR{wԜaafY4vw"MT6T^.>NSב}\v7Uٹyi̼&.?#i؏ß{*̬2˻iFݒx(O|mRڦZ[&,khgz!C5J+-WvĮTRS庶zPeٹ94tO 4s50TI/1x  ič9+.(j!O՗R!8hH2hq'TdM?hkCU| yݓ'7W2BW嫒_1tsaqu*!Ht!Hf ,4vȫ2brq9h~9=1u>߲CmL ;;FPU64?g jhC:}̝?BV5c5\Ks>@8@"%؁ q'ϫ*HNg{ZnɈ8%d!/wMQ0~r܃Oz;W{(Uo41F@ ,&e $:'O.)B:}yY@eHyqQtATQA/\+5~h} @%)0PKy$<2kx:qo^H w݅5+n.l` aS`s0-BT?&ȃkTJ Yr}gЕ `x=y2&G~N7.ȍb_&Kt셛 MQ7u{ 6Fhw T+ .>f.kk6 ~ :ra"r4MCvn}i0?6HMMAwDC=(wtt2ob2@[ax8` x( 4tm~(}r &F\|I!d.PGW5dfg!2g4EصTEAdWncXց>s'Pe)b5<Ѫ pt$Ȃ`j7Bfkb7eҎd| =yr:EQ]&ڿ}!|?E~g[|rzՀMۅd}\Qڛ% 7'iJm\kR@LAu؎}^M\3A԰nۘ&H"$f `v0T@9\;vؼ I`0VVրM  6~~ zu'/W^0* DP>/%ob| |4C l` v( :CG!B4(q KH`hk2+`Fiq!_d䄢clJE`M` =g>uL̙{ԕ,4~PG""̡ S;=ҥKi50Z%*ՐjR5eTC C{`q)hHK:>u"k-ړO>>?~fG4ٳgq[:_M\X_$;+i:uuX ?QVc%ahޱĩ؞`cpf۝4U\*u& F_x YV:v}⡚HBcw ck/­"| a[@s<9<ߨ*cʻ&pOٻ&y8O"ڽfa]U^ïߠkxSSP 77ո[2#n˔ʸx0>1 T$ ٹ- EKnI,P|Ր! 6h<| ;W 4sU*W6[ϛsHMVͲUiCp(WqۧX[~m[,NxQUE#GWl&kdn y&"Hy""&T*s/:q.^E^Fbpy?NXஒö0l,G D}ڰlLN2b i;`k4e;xKLa/~8u /&ān aK!L饟] ,@4I@8>F:`Z&xqA5r A0]:oN?: B,ǜD,/ D KIh2k \eR2ZڹHU-]c;,W_ٿ_ʺFNq?UzCLxA5TV X)bQ̝󣘗^j;Ċy?T Ful(Vs`D]Z󇓰ȻռF  ,0[q8\so_5@#m[z늦#%HH k;h mS&㯜t;q(QLFPPM]Oׁ@5407@RsP =.@-p胟lY#iI6wy``CaS #"<ՀChnU 6>"yQ!Nchh5ՏqYK+IQj/{jsKSd]|t}ߞ=^B(d;W pXjLwp 8HM@cv]:<,)$f*~]Yw {{zpO#Vͣx],}?̮ɳ\M6뼛2kg]x_nu_eE )"h9߃k.:Ge',<| Pf{n]Ր/ /LR6`W 48 "|шRӱ1.wVD k»u|_h"@1^E8$MUQdla퇷=n{ɿz?_cY[ h;|ȩrjiY*BDfpMJ),n*9Α`c0Xus?l8pO_]02Gm;&- - c>tkvXaͻ&ECrAFc3$ʟ6fHcǗpyPUdF K[8!4Cl!Ѱ=O(òê&>{%P͛r펠+45ih5牢¿]j^kxJ-wZ-sv% cxxnvKs/j䶎R}SUeTfsr;?v| w||a[fYpA?|0`*;WvcHg;>574r85c@[vJ}`Dw$ fͯل{7qYJ~_q*,9@RcHDirgw`JYÊs!̖{FƠ{D R 24]말+0|)#QU7@-)z'by霹;-sT}쓓!1Q~C #n^ @S-wZn 6j= hqN"4 e_ԩS8~8k &N:e}?oIj{rUib˩< hց[zzG _Cx ~6G[k:C:6&(~@snYq~@R{w{vz*K">6Y*~K/fk4#'$[+*ҳˁ{zp'{!vv}7v]0xC5ʢǥ]5 Fli) RS14\`xHON~lv6`k (F&ZMV5``@) Btf@iA-$^fVrJ91X}JN͠w85Bf{+Vw?5'OZgf!_vhhhk>8.=?0SV~WLQWw}(%ukȫ2c\@B#&6D ȕy' N^D^sn]hޏ5`aU *um4>,H0$~$젛OXAU!i:tlp4>4ᗇNE-8;X8nw;"lJk7G0w2:iu,_Ud`~mCCN˿Ƌxa~c``u2B^Rȭ=& |A (r@ K4"A$A C?>% eI[a  !S۸<e4 q$ IB"qszX­8_0*sdvimm¿ Ժp=? ]Z χݠY,Z,vjݷ~Ӡ}>ZZlm6)csb^ XJ_ {ff֑W0\0e*pe/j%CK(\v'\pß,#=9UPt#6c/!1u^8Z7.rApmJֶ_f^rK0O; Ӊ< `!+bwkCm}5XBN-:&c 9zw2a}Uvu)\h,*e_,P-< IVɈ;,?*3Гf xH`=ھ 46wC'ONw`n6M ئzƵrMh]@w|?BKemv1<<\S}tdd~s"˞;ibqYҲoC˾c|.Ǝڱ(t؇ a4v{ChqjfD%>dAF D;k a4vwySY4s\|湲f ]G"5,#33 )`3.)?A ­M9x=L塥7.]1tU,?B |œCjjtMC.EolA(\YJqx 4`7oXWk]Ӷoe1n]AһRsIf\!:@P)H+>f y~ -IB rzyQҫ+!vrƾ_.vnNvBr8yx^ {O)"RJ>B7׀&]{4 :AU᧶~Wt #{ G$̭+x.yO'.;Zm󮻪W_kl;kwuIY@#Õ3.r*ouSyu9EBms-HzՆ`cZntcT_C0@hs[TEa6C 1 qg<)~{@X#Fw'hz(lr.]!H`r4! cm#lXvXYDmA04i:տV>NA#β$!Z?ȔR`h R+ R ,oP:9@@8Bo\^yg% ]Z]/`r99 P ᩼lXsMhB{ ?B]h)SNahh}}}5wo~N2*@zgw`?M.CWr;Rj_JW;#]&0%J^|ˏlvZkՒ2"y$IDvw;oAU_LOlY\|=Ki3?=E!v$7ĻӵrbP )N!axHMjJ0ݦ^;oa|$#dk,Y0-ͦϛ6Sk Prց^į Vr xIz %|'+M䡊Yh5dk,kXP(H B{3}Ⱥ &)E@L!!X؁OȫƿًgSΌswt 4n.yIt YEB#ZIIآZ>q3`XG`8/`p v|8޵ǻ`d2N^zi]:Ɠ `!07 CX\ RU> 4| F쐗9c!+oىE+]I]˲%C&EhhP=&e{Vhmc&^|֣h0\F޺(_1]FˢKS|VhK?V'k$N.9A!4t-["V`ivqd(~dž c],ѻn!;w|I`bNjAv@_T3Ӝ.W"Q{$GzSmtyUE|ljqGyYV?z\cwQ՘|d B&BͲhDB\.>6oy@|}"k(N#O@77/ ;7o)m&lmq NgEڵ \\McnT~~}eƱA^ǁf~<yQ$ |5f]dA};Ec7'd>_xuhy!K*_oW1.%wt}ޥk |/\/ڐhŅ@Z]/ȣl+;Bm{oG*.-a*{/ѶCj;6Qt SbSb-L-}ITh6~7Ui rCԲҥ,Im,^Q,=X]H_jz+$YSoܶ]/?]MELcߡKI?###FQ3ςnsruX+~(WP5aYp?ijIBsG5FcP"IB|lp4Ut&VP_¥^iV9k}zz@PTM)G.|;ϿEVg==8u ;vw[<ܘ)٥@!!16i{| !ZL<&lUH! ;Au]37߹;Fn1`S4%{/El;[ر~3A$aŚsM@P )(A߫A? : : ye=6َO02t؃kiH`Б ϝӧ1<<\Se>}4Ξ=Rչ@En[^TH./B;h_Y> lSMH01Y!>6Wv:wxfCk :=S& 6/"5;|1#9\|湲X6Sƾn:nhrs/&_\E"/ Uש{ŝa62hԭ/"I0|bB];څoShſtX!hnv@T%y75@=4 <1CcSq+b̵_>oI2%YMX:|J.C,@g!gfAt\+93 !f./ww3aEAw>-~v-Z'oPa׵kI9Ĥ4>.($`WQSQ$$<251SA$DMᴫCw`;|4na3(\nl:tddQa彃"DP/dIp&BU*0kK;ӯ#w?3:'f"+庿=NsBO^|.յytkcmKdDf!C!_!"1!a .&,i,KZ,~  @ptR$a$E5dF@<I7 * 9,0PF5oX\Ó7u4^/(MrSpG4)Dvۉz%ȡmEif_8G 0XX:P\2[9kirpTtǡeނ.%M{) fF1<<'||ϐP@wC!ࣶL/Is*װo_P >p,Bfte5)d *#@@ZvypOD)Aҵ9|RFrdpNc. Pt(?t0FFFj'O.]d\52wrT,c˯~ hcet:u$OX4iݺsxM>wx]W3hx5,&+n;j;^7ScO>$I4vSK!F.*o0vᕲ߇p~𱹪ι-O`Fs[4r_c/=cWxx?7uˁG"yyH<pITDw3G\~1b (hu}]kE@s[5j ]Sr]h'5wn-X(`t#V~?>On׶\ğ\D~ ItsataD:t$Bb󪌌*m 9,@%iWDzնA42>0$ ' V^_M,[:: i*-HTl1Au27;a;@}aJp8}3p9;l?7+1H]Do?[/||iz6|C8q-@H>;ggP^cz1_8M!(p,[g!S(U(f Pu.眀-"%җ>IP A9q7;#A38kUq{DܻC &%zz3TIԽcBֵWl{t7ŋ=s pTO>OTs/^48]vl ?.-X>G~K\ũ䓩_W\|9\~rv}!D;kC-5ߝ*E0f[[5\2WglM}+>NWFAB:Ͻ{uw~ζo<>p,}|J!51#$::Lݸ _[{y c7*]T nvMC}"ipvyi؁p9>PɷU2`5mŹb_ -,w~z mfdlqQ(a&H&F AU7 :5\k kIeRHBZ(۴(E[vrGa{);&.iw}GfKb_s%e @lB%+Q[fVfU_Ū̓|gɓ'缓}2͋ew;iTp*^' &M #IY{ธ] I' 8zCZK;Ӳ;Og~L v(Ɵ4ydz?:oX mHa੗7SaB./wYCL]z/Xņ0.ixK2_;ܼ^ ;fņF0zr eo󚡰C` i, ; Dz 9< r_;( v`.<1v9kL>{-M`iK>l[g]VIAKǭtZ]r vWulK#%PӶ,yD8xNSѝOeuOm-"tNx r}Ogg( >-+Tk)ig`mՃ= ɢ^|S\r]>"c#rqc!9!9q/kU7ec 1 yG/b5=߱aōn UdƼR֟ B|~ځ EMw@^3"RIŖD`_2%{{PM;J95aݶ$i ;EUa$qtK{QgC1mҪt՗Pc02-K:ֆZ*a85^8?2l̈^ [& -nmm㼀\p#8_<}Ǯhݼ|n7Y0z銮x@sM-b@3Q _} cW Mg|>T/k\ТX|i@eKF!>{->XI FD UxТU &oR.im'Sc1Z&U>=>2l<}2\m#ec!1[š e.nXwa#9.tސ^{JDz,bHZex_ >_5,ݙ0U6VD{y<?턗d$)x)v^BTd% -diG(bp@E`x<' Oॖ{C9Vv4 m pQcUMg`f;̑/K)viaڅ)`kX"K vPf~LE`lhA_޳ſ]ꆑkWgwߍ->ԩBE{'? 4&d($ 1I :`2,i:8AB(b7MU,^BN V%&*9w=8= ȉ\eO܂ҽQf}upH?Ŷ\e;!\47YQݳgۇ~emE _". Eپ 'NCuc;lGfY8c% O7u8_6u}(ZU]e7> !tx.P߶T%">*?zo~oægAU$!H DgoK~˖͸/uM+_j4\59A ˾䳳=cP( |V˛:߃v[_gJZ0twwXu5dg2HuFGU'步zb:Lqk-a o;6Z.Blp,'{55]_rޝM3N,l8zCۅ6seI*l WT_oY;P .5BIB6C6դzpI=Vlװv㦕Q8$FFLH[/XZPȒvē`*q%k[bIࡥV^0a("OK> )<^3Úέ H$MC#T!ȳ9xqf.ow XLM$+ZbE|oybt>ִq9hY6 _!>8021ćF,Sn9: [vH^h^r>mHz mO+тdI w@-m 䌃p1xnw zF[3!HM%s }dx< ^hw`:[뮝7AKH*Ip/ ;a´ 4 O;[5vi*/9E%#$0䰷O [mcm5@˳_h؁PQ焁a¿>LlhFbُӳ'[/oɵؽ/ϝǹWFMq3FjOQOUaY >'In:p sͰlgxD<2šQEʁjsnȃ Z`W# l)Cɇ<Пul6Z&Glp,>y zZVo>[Q ;TM4q nSM{EGfV$A@f,Y 5:KBڵ]4[kZ9(2cM0vq~EeYV͘ <kReUU\*Xzk;77">4hU4mc.E%Sv:,Y,U jh؅Ko,xնM%g4k[:!ecvۂѬ˱{AY|#/ˬCB|dٸvI5[ѲA@H@xj4!в koi2Į-O>|a/"|q}LqRrlEYtC{+pxK3KBș+ݲq=/-ñ_pxrǓx6] /ڍR3 I,˃x6`wF]`qVw2@bd԰4>6= gYvYd"YMcډaZl{`!IBbdTTS-ROpgՌeLMH= [Wt$sXsp]+OgOg@,U3ݮu(w iY,9$F 嬥)(7LԖ *[Cϕ5oR.)G>|>/yՇUW Kosdh46}|zM\:|]H*>Ǟp&O^jwauv z];kyXB-'Ie"CQ (8H f@pb!,|͵}iH-4(#NjȋҬ$M`RU=Za =g?ƿ]L:u"xOcѾ0 S8`" vIV H24C\ \S0aNlRR$%%% L*^|U,ꧪ;d{|!X,+3],4rm <#/@|Uu[š"lōj|LP*xqJUlghQ(|M㽜 9qFw*[ Znvm0ҿ8jQԛwWEۆ*ǸTZ5T@R|SUI.7 pyҬe "YA9aEȌE G8ޣ mOƧ2T SU`AjlXbAy]zw\=DG ehGѓ.\409X<8^UU !B֚Ų64 vs:}`]N;xjT8Am$YFbdrֵ-^dIE% rç! 8'[2HJͻvez W0H¢HbZИcfp1>2`XUvsF(._6UzfMʰy@Ѱ9&f; ~XW'F_w09}ND6/ (NDE o0su~fڃu`e5v}gR sQ ?Lnˀ*E=@,pg gYV ;u-aM>r9MR) 2;Kpg)Yk\;d)}ڌc4͟Q@b%:oo"BY٪6P~m`k {ϭT}_G"1(] zٓvñmL2,P^4ݩկZ_{vm0rTɯmpǮhݼ|nRc*/Vv8]=Gq?->w9o@Y`IFcٛoKz#r93:@jtݯP.?eՌ AKa[]׿Nh ޖiw7|MVVHo=|E] 3vC|` U Meo-O#68]pYU񫆂~5SK;$~V X/~PAO7`9Ͱ@E{:)3(ER#DPRfcr@}\G~.:yLnI0B)ҟ/A%\p \,2<..*0#!AhjЌ_Ꮓ 3>^sc2.;؁IH`z֚&aR !3ח Esz %u1ֺuO@( b fi@SMf˹!GW!>ŶRhᾤ9>VTy~?كϕ"@=P&,;Q(|l){`~3@>qcG mMɒ~\iGF᫿G6/q:텿|ngTyY [.i8's;9]>KۯlhTwjt;صkigvRR:Lԫ5DnH]gՌQ{Gt__o|ɲ !KdQBfKv2iz; <@.GUC(B d2%À ɒ]?AfD._6zwX \2 ŀ_S݆-}ΫFA9Ue1ɐsP|bh^L("56>t\O-5‘5@4Accȇ/BFˮ `[psOaӓOezT76,>¦>,]b9X @a'~<яpseɗO pLuho]c'?~|lt^+hXS/節[а 5/r Ň3x߆]uk Tl$8HƱ@i`?»qr3Z@ٰcUv A@6* 4@1ŴKv(_Zsf3-qBW0):' r8AZx<&gw#ƙd`Cc@mlΣo;1rWR Z<33B`Mӊ `o+k[[PXH(lȡbvm_6 9`߾}Uzߏ{[s'Uz3Pa68Z7ELA?ji;VonlYXo7 A7+,a Zd(\s:4A;%3xI;ۏ{t6k{~=N|MǺ,x"pIмK$DOƹ76$ H)gա <olZ(j=N`TVfOBTI5[50r,f \ ' uNlC,/OՎAPȧW @`J[=z{c Cʛ9~ߋ݆,CF29ҡI ><Q7-&EB`=Js^{y%hX^zi|Fq6v,'"!ICQ^n'=כVkЋA/C'T!Ŷlz\jDs޽{+w޽{߯\i=˟ԒF2>EHB$@޴~Jl.f)%S voز[oT'`'/iʒA3~=ңE9!Ie,@K$ JEdLN4D`mタukM)$HfK֎̂9}VlaT8{Syw񡐦@hմ ||(doYռS~:F.+XAe,;@ˁg+&ҬxBqJmjUVZ] 5!r>]169}.,`EDr:U}~+<\>=pdALAH@Χ-QoJ-/~̸\Y209lNlN/^cEHId\*? :'{[A$$Bmߥ3atg>6lFTx@R 9I2K+ $yC˃;x( ޡH)ڂpX41-]y+#*'@Vg]:뭌찃;EA<#uLa5O{l(=$2WNP6ՊgbcDυl^D*/TT9$Ֆ*ۏsBu p-zDρ֦*8Y e5)4@!74C23=,HSb:oo?]eEumC%Zm6j|lIyꩧߺc һnfK<|/B3ˉPa?)c IDAT6ge,#68\k}ڍO7]_H۰mZM8D!87iҫjlgw{\&P cxyPsටwbMSuȌEvsޣ S`kWQjw,XvYy\%T4MCo=;Ry kz<`.ȢXvl66:oCLLYm,d@;XxjD5'ఁU^(| 2پGF~o[(|R! onª[jUѪjoXӆ'~<qw,,p[ɔoW) AHvw+;V(2Zj%ǯfl6*dp |g m4V̸@$4 Q!+2DE Ayʌ׎ #K-ݩ6-pO) xt ;-K)fTS(Eb^ǐSfkUء֯Qi5Mis>. z# :|wNT,i L^gbR0 NfllF;82蹀P.:;FYC~Uu@p;i4(௫=4yChe|m c^'U9,Z<9K0ՄRak4i<vdC9fkj5ȏYrL:>ـCe?3Ŷ RyH$ؿ?v]qڹs'mۆ#Gh>W}&Tz㆚'gP߶ntY#HGrba[AwVm6V0I8իn沦s>zEo]UU]WSsAukq7)"9UҌĠCg ͲjH(<r7LO:p@qDr!Vr/]>,1H8.8 c\N9Z-پoizb-PPSK / !ec3۽S/oXv$.|a0wSvrst~' ' i5S6 @wL &g8SsB1ky#}o+iaCi2-kkFi4BkbA)'g@jbcU,Hrza;;b !A{ʥ}om6`ٶRUnJ`޽x衇4'g@N_U} bf̴_Y_Aˆugvge,:8l,^k ;ܱkZ7oHW7۰mQ|' K_ N+g̱n0 MWѓ|uZ:֡XwVcNh7rTyͦ@,r"W0.o < !ǁv!m|M@o Nl8.j>txA2N4l R.6lnkx+Z6Ǫ[tm3MfՇo ;VMQA:iSiHX mOk*<4?h͹E`6*dSȩT}(<ލ?7mڵOasAbuK[yP;CC3Id-/z 3ۘbµ0(_g:UrN` /2!Kest~Vf2ǣv\ja .@Qolh!!~u%;nGf*0ﱋvs0./B}Z=ֺ@Sd ^ϔ<ˉUʲvNsǚZ3!1a #A8 r/M!϶2ՐW4ݍÇi<6$' hj]8 ӷc/lF$")OɃoӺelc ;~T7ۍ6U& pݔsE<Θ~χM>Fu}MME]OdQpdb$ටwbϐ$ h h_h۩laͬ`~ <93o4<>BwY&u{ ֻ:,uKAa(mlu kWxdL >)h腜s<~$aq\*=gD^$T^usu>|Hb]OMnSt/AQPPg08!!KCEHp ~A:n=+jɖ/?ݲ1hR)[nJV}0=W 3fl6}P ;(ކy&q QV RcP"͗vڎ;t\PK9:?LS,/3\.a걟0 Lsyj#;V<%cDZO[o Aaθ% &7FK- S&CYAճpx Ly?##]|k- a8l5.g,PoTؿE`,=NHXRy㽜D[\!6OBRXkѕO$M0gu:}j)<5)dON.4Ks:lΎ׬3+mltB6tZ:n5$oZ`X̴:k[HIE.n7Cᡪn#L}|dTúe8w訮|Ru-hTPtXF,|F –߾o߈|g~>ݖo,Tz?7Z"?óՏOUDp_u p%7Vp1>21FrڝVUVЈbS'I/"AR,2"o\Uw#xR)uu'̓G+V0tM).^M]ڡR7-;@~*Pbao6/+EFβ[=8S1Pẗ|p/6VE{$`a,Q7`8&/Zv혲nvCpm8p8Owvvǁ4k<,n'Cɇ{6lٰ?,l882ĴY 5mEQNTS2xlءgrIBjlPx2UEӺ[ey1$G*f+Ef,>-=ut|de.76`[-SE (̇c'v[_;}'2᭫ڃֻ:,g,dwuTHaF2l3k.h̕7C:u5HG '_l4g+]9,M̵S"4C3$_fǙ;(2F ]1o[ v5|xWkay~Je vE)26/ XXV6s,u`/~rZV\,;-V8a SMd &18AkSL@ oXӆUw1.Qb555o}i}l|IoXN#>8H塏1PE`{MV] ^Rn4PDbh$$Ny(\?$b>`$[>~ ]so#v6͗`ʭ@~Zn]0>$Yg9OL KAE#̴&،Ï=BwWtD{|SQ L=AlOX8_YիO)6b,uiu~'[d)s~R%H4A9 @i d`$HjByi& JnYcA@0~[fm:#]7@}~>|Y3R2>RrR(>{ Von2Aԋ28beRHBeb>|6ΪgsuM?-ő_AAUc|렋賩0vC$,)çOLRD`.8nIfQd"BçOܛo3vj-gKغl\sW eמo8kBfEP b.h¸: zW}Q3$fc`mࡰi__rM$KY_=5}hٸbr؄{oBwo}:߾ry˧3Dcſ|emfaMR6ղ(> 2~bݿj.nM3CC1x_\"Kv xW,`<,n)BFH=nanr⌮s7{3V2-SUAad{a? c<uiRa%U$EU_dIB|d,hءimXE# bL+qv$8ilV !H 3ĆFLuu[1c. ,l%jɩ[]Zj̗Q!= 77PCEZ|(PH[G)/fAŀr2:>j/'\Z$W&!](1.'Z6-݇@˲%Tsūz+;+;ũ߿G?BrSl}:6|QKO$X._ξ_d>a;;Y_)}Y%?_jgmoՇ?z':42l6;L ~U/byw(%hOHASQ@*:``Z3: eT9'Ox;W:eD:󱖴 /&* jG i=Ձ&elpwy< $ogx !r׀8zPS1d E/t]X'@pc㪾jt4uV'j &LdsXŘ;+=vYA<@ns. "hNL6Ui A H["fCm:[ [ҳr抦d8};[aP1%70is~8c5J:,#@ʜU]_%E6`YRރUs$ bT?m0-oS|&cWmP׶t5iB&}]]|L ~Gԏ,e8a)N@͛nEvgz+:,=e _S#w^|AJ2rS IDAT[oGq5ô gp k &G _ S)W݂h9凮(~>r W8cAV;طWu A+0=XmyaJDCқ lG% Qv_~KUv_z }5̗2Zv lj4z\!&\DZf+ƕՌxQB$Ȋ$f]e,a[yd# 6[hw;x x xiFwSN )$'@(EO"P.P6@w=.|hTh JE;L=.ZW{)ivs Ld8("xli(HFTP G^ǚX# \̇V%A{L*/϶Uti޻q~x_DYe%NdYgD7Vv3IF;uvkaL|[uɽvKW3Ԣ2򐝺&>bBt86Uw""}:=qQ%*ehbw v5H̍Zcerg)%E7N^C`?ky()˥kǘ1j8<ߥ vX-f-=@II(i-Nbe7m|=05n.Nk)??K=OA{B>$^/p:nNAyap,(2}6vؕӝRqV)b`/Pe"hdC[po;‘@nXJ!"%p-P:L,F,8w ȋ"ŵ}W4d"r eòI@nU]:9?Rm@/&7|t!H](pk1sB M%@a v1ƱYiafrس-\ZEbhyU;ɮFhK$yx>9ra9PƼsymRΤX=LdN8a[ .]UhBR"F9sP lh?O8?8;<;t9CO?ǣ ڢR ;ӖLׇi83knf`P%RfRv*7\n ix%<a;2Be. pƿe1+Dž/DZ"P4NeRYNwx~L/KXTR&{A \YW[5_yrnSv9p/4wzqaz/@U7Ki@UT(`?7.ۚ*Ig!+jaQVVjsEl ;,T[ĩ G~Ϧ@<'1(LJIVc8s;zDUvXV&/ݏѠX2 z>E=pe&U&ʀa3f^,)kgi84xQN Ig{Ylr& ŎIdB 322K.axxؖ5L\ZPj<{uz^?IڤTfQ[/ M];D+F: %nʢɦI{E|,[/`rA$)|J#06ࡾjbX'"u{R&ʨC~n)z{n͑&2]Be*XmnvF:tr)RoG[ v@^|yqZz>LU^x:BC0t*:4 >￉r.a˖pX/Av(1 ͂\9ًވakD;xpC: -@4aF0 sxv>@u28}ZaءH*e(Z'A_hez1vA1S~th[Fth!+A y=+V/= ѡA>Cýj6쐞T\s=]kX*ߞ5/w^5+0+pnqy+޳|B, !hSPrn=S[;d2DUX@ႋ$pa; *`QfYUƴYQwn`9D:}L&?y{kuͻAzЪe>m1%_Qe_Ó_ 5i<>>)@")5:$ވ~AJm_Fk,t/T_PߕN%.Dѳa.O ݸO+>lՋ.T>ҕ`Yv(/0 ;ւ\B J  ;h㤪C` b!, /!:Ќ6v8R\XtIi;Wp3@2 y?ƅŪHYLc;Kޝp$a?dߪP= M@Uֆ"p݅gN>O&7QMl\\m~{x+ t^14xn+@bY5| .fKށ?Cj}Eڰ'ȁ3IId:<#]a,NrДKS?xٙYSнwoU]g!翄9:4+h:< Cu Z(0P <޻'Im8⻶-O xovl>Aںlymw{e)#e Z *lՃ!~g #ogx;Zq^/|\?)փEq j Ӻ* H5>SxawgyU](\ȢâRFNxi@IfyK ph#z^%\HeHZe>rta6~$Џn*S@wJG ɰv\w ڹð7 x !R*y"s9/o~.b;37 ;P4 gx%N6>{FB*^,Mi{<m;T}Vu|(C+ϓNg(ȥua;sI,&v~3a>TءbSo& ~@N.@58<D"dz,ղ@Y=vsRkP>̝D,){ı,K2İ`y+' +p0 " .QFU*a@eFVHI3p @T`MI3&`F]=1YEnA28iN^vq6VKwtK9@d֩| gca~g!HJ80v*Pu8 C;ٰMcxG.GUj[9{!چpP^?3nNj\sH]#l!9r&u t耡O<Ǐ۶ts&x_SPs ۳g="QC* 㐄.$ FS* ǧP.3a_,`'0z u={#;x`M^!sBŅ6Ydz#M_`e!H˸Lg,W:g"5:`*u/ b[Dz+2]r|/j|KTT!6y3コEa H H+ՕDjt ޯ`UЮ˗~!bVL++5^\ۆg!,檢i}p-jBԚE3(,=hJ4okѲ3r> q6TIhc;oCF;TyZ`7Ɲtkϋrls^-a`p v?ǑbkKـqY v X'~NJqjث H Wm% I }x=Pf;,;=N "4hUh(oQe ; o 8E#}ˇő|oMtnF;t}?冦_]g!du=W!C Nn}W.JrD5xPyhEzdI(Ъ33`nn{Pm:R;0N'\A-:he}1w\tr> );ia8މc2&_7, ;*'qϮ,Ec;Ru8LrbX{V ҩկ,Cj`p;XZ%1+BT5Ǹy M7聃7Mև?(5h v* (.Vs7뫾7eSSk[kΎ%pA{z+k7o2찬| H19W?&4XIZ.oSO~P>$)g"[[5-&_7T\N8a v/) "}swG#x_ HTwYv`79o(Ч Ш5pfon'@-Ȣɦv/`rr:ځOܫ]1 sQq%9$g1M̤ۧR["&Xudսέ毌;C?pg9+/,EA)\2dW`pھW>PgAFPma+<Ţd偢v71F4s˒\;~j*a͜ƨ\Y =!p*'x!Q젻 ;艫*Z%؇t碮R9|^SEt|݈{o4vϜA)PW [EEIX<.'ewP`TO-mvw.{(*w7[(P WV .CP]ljK-C+ФŕPŦ xO&ơC1 `ҵa͵6œ>'X)F=ϖ/Ik~84uÃojZAQQȁ3뭅wɓ˃ MLArSYjagmp84r ;3k ko ^l_4àlh< :1?>u_ģX>SMf}`~!n e"nwYImߴXzSvw?ޅLJׇ@6PCAj=ÉpOa<ӓ8=tUj?A- ގP˔XnqsCrtnչ ò`1;\ЁȲcYD9,䰬^1nE,l5w>pr^ ԻrVʹ_!Ըg#fa8%`yРd9a1NPNwz6pV;`r@{PKs;w32Fh@?(w e W5򢄼$a,2<7NYv4:|TQ[\<ǂ)}`Q S"%az3q\ x?ñ8@s-=?gGF?D坏qv,}xwE2tK IaxHVSۥea9r&u00ŏu;ydKd<4IuH[y9v?{ꦅiKŅ~f(l+%'U e]xgάvhrEp˸k0eA117uryw O ۲B䲈0 Ym۸J X(6ϑ?؁`^ؽbOO[MUp!Pt{s)&2Tj=4x?3@ňVE+y!jMɢRfltR gU5gG?pyۺМ2P%T?(<(ظ;4Y QUhd]kղCuo;4AO\ y)1ր:e'ء:57م;x. j]bAV0/`*_.=^/xGӔ t8iDYJ%R^9e(pNsrX}YOY' =3 oK󵖇Ow.xjaǟr/=;CZǍ *+bmLRZv )g"iV8xfxqq[睸<44)G:eevev@VE7F0ZֿhkU YX߅ڶ1Bv !i"'YݐiaZ~Q5 3E, txn.<}Wiqr> gx+K;)DQ;2v<ȨP׷\i3YCa0}o@d믢/BMOM?‚3!Xo'i<[ϱdȦ(pU³;a A$vwv SVxX-|p˒nc< o|Q^4.< ]!97kAawU80;P1ڃFVx}+7mSPoR{K %} Uhr.*2_.;[q!xX}!vwYնisր@ʙFVux iNɛ/n|xG<~A`!hlvƇo氃C`SFq jюOҏ0Va Й6ku :\FsHdģt#lFE-0U~6SoiXmyTZ姟0 $O<ܾO~?u*ūJwr\Ko\ӘnN.b6sbXV5H9ٺH2$ƿ]_ud2~G\)T!h*)KphO L(HC[$ ݻw @3 :A膱TO>;݀:Yf"ò i?܍QqU!FJ w,w,@6A`a hRϑ@ ]zpUɰ޶\ws+k~?oFoᵹO-{oL)AQp-X^FfyP=V{Ow5M!33J;@?_Vj~&8QBLNDgsvqwMimڕI9ٿHӪYLn^~e⋶-.Є}:&eSHQ ih´O1RDjéu t`2(lbm'%A%j}0Qs<&_3u>O o\_\@+]M$cvkxhX+K3r> MuqsMG>՛HNB`cE9]rfX^ܿ 2Xur  zo$Ex&m݋ݽH8=v oAZO5mR[^Î--jadbuKsrtWٵÐ$٢>^:5oL KJ%U]S?{Rì{aB;2F==KT:uL+:hVjm͆VSqH/Е__rmKI%3`eye _i|Qt%^QT_FҺI k a{Џ.Y ;c(aNXno' w=e-P\:N\(=nCAzǾ5H7yȒsYS\ ܦݘ34+_W7(go3Vjuy`{AƍR$#م&Hd;;0p߂[^]˚g P׸kѺO- 3\㖄\'~ B6kzYCC;P\Mq8q8Cs`UYVKJ4# q82wf@pKiHN`U@ˀ_ *DEi(8pȋe2X鄟ASzfEeuxa>{NWٺOSe3A;S;<bm.kX~ЖA1^ !ws圖n 0G(Tga>~\RS<-]̋뮻XfJ:-J_8I CŌ*D31}@UQVVqw'p%ۼz3 |3a ӭְؕ^ܲ/5YGyC~xN&deZ~G~~# wlr[xo|H1}7rcLf7: MP$!1_BF]E0ϵ&R-C+')eұU;5;@ TɢI^uk5!'lK#9Z]ז1=5rz@bff93aFOAzbwX'7[fͰ닁nj>(Ejc CdY"V(\x+C l޻e!qWzCprjDEY5TIojdQUJȕqeǃNגexoxfrΌߪ۹Rw{0౞j /7RW~sŌ1wCN pogXKZhYdTH%aŹA'=~VMġ?S_0n6*1=i(%j4SRT(Z%NR1 ;Cj凣;\wkwr¨*/&e&؁28~P߭hdn~pQKCU;22.&y0 զ`HF<,tr80l^׾rm W$y.y6-;hT7 r`渚PhWN@-zBv7Ҵ*&_7'N'l\>G \V4t&wGv$2M T~tᵟa˺9x`w(Xu͔B ޜ*3Z?;VBj%͂dA'~ 3oz^ P>Ssݱ\]75˛[Thc%S5/\9Ҵ.rY\qr:]P$Z5lU@Tj*B QD4BBEQDO hOp IDAT{+>,=$nP.wrQDZ[etXUs-.3ӆ>/z}DĘ A"uj"Z "dQ"mWs7Fko sg渶>8 Li 9n+T?G;ԖyS4%EU`)p4  ? (;48ʰ*PAW;t<8kǯ/LahZWH@ֱ8il^Hw79o|&^ZxG ;ܣ|"F= v }Pm7 ]*@I~G~Sd h0}FYXi.A(fFL^|ǟ< +*ւ )PsvcRΤ$(g(['Ol !uE}Rҙ]k5;1 ;%Vv, R Gϛ՛HN 5:iJb ŽpA~󅝫,UE\n9kM`Ip;-Nw|G| w~vNKFL?ǫNb`{7 zS`9Պ /@.+`CšROミ;ul} 7@Nm$4U۱CuS|CAQTdh,$נAPe 0: UYfiB-a<@np=/Xr{0|6CY/( >J- 汳#vuS"Z~˟ׁQ.^ڿF3(90O2rʔhoķw_WMuJX?(>jܦݸ˚4ҬHLӐ'+8N]JЁ74Zy\ 2'Hd;y' ;b-Q/o~.˘&}Czjڒ˺ϐձQz|PͰCz|kfL2`Ar7kvzvZ"?bu.D"1f6B3\x-ږ* PyΖ_bI`W m#y{P.tn0vSe1wEGXDDRŝAX,AEH%m8JosП b Ao@`6CFC07?:ACZ kj ERwp5֣ `=]m~h)±Ԗ ogSdW&z۲ u 8bs1+ 7s;o'ph0%ӾM[p_?𠑟v}(a( '͆U`^ܽG s se-P\g+]QJq}ߨ=,7%[EwqO}C,((XmLJӀgr3hQ4(g'(϶}1YN iZ&ƿ2~;\pqC.E5tJ UH6c!CM@ Y vb_ ?Zvݠֻ,[hKW˻;,錥90}zMqt}6^|0s捀,ƛ59sN7},QV-Kvm'xh|CS%R%PLw <y$D-%+nG8"QB8,;7,6 y.ʋePeI`6 Ky`(W0M<;m9Pif#)p֘Ċ"q46x(3 #n>0c<@<("2W b$@UyR0\J%\9|۳enn7;f4-k؉|dJ/ͭ "Gw+ğ> 6kWOg `qy( * 9XG@PЮ~B v7ZHӲ~;Ŀ+8qȑ#طoFFF] m3nhrޥXD;xD͓aCH=Rn^8t[ޜnB[Z E,LM[~Fp6~ˆ'?k\$XC5ADoA }-f{)",C+.VCW EF'z;+B "Q<^kǬjڊ{CYsre<\ڲA<T:,&g#4w|PJF߁1Мײ 瓺Dj5^Y؛IT?wǖnNs  =\OdKq6v`vXV^LwwLsSUa=eW8ݫa0DAJލw[v0;n?20ǭٲ;v֔y$??p,z萌it1 H;=%'P z5sAby-UՐX,e=psit$(o;"^񉱫x Li3eFy+_y''֫+UP\TiEt KD"NkCp?z@q1[=L7&+I#!͊CO<ǏDw;6Zy#J5L, k?3 ; Y\|u_aA|ƛH^aZA(\dUMPS]R4wcւEND"q*[s 3 1fC>+Hݚl1 11Y{DkX TPeNϣc_w~'*0~||~Qߚ*CΧt LXש`+}^l}qCa.Thè.C-iIY!XO i|pM7 O|tW&3e` qwIF`mBjS&MYO,ed9CAk*j( 8qn;la=is,莆w$Z> eC͆V&";Y{QfS$p/m}6vXjǍ|#aÐz5sA񫚆>1-akW} ~wXUF':K {8GeT 7@hQIQ-tcF՟193D8bz=KضE;$KԒMIJM Ap]ygI\YYUYBU|RӃb{9#TVW=5ߘJf~̮(_/ 0}kd,PDёQ?jGwzpuxJ-\7]|0"RUƅ@USq#v'`Kva*Ɉ\K)G{š;\ 8$$qpЉ}#Q Q!י+ ڂ5e=͛r~{邭qKХ.GʂG65TD_2t s12GԶeMªv>da?*CM5m-JCd,ˎ,gM\ĕ?Ý m{=p07D*3{8ReiSB E>_}W %Hǰ30~t})`g];2Nwyhc mn 5v#IJ'ΣW`Upaj@k긜ƳW~#-x]Ƚl3ICzÚ, UcQ*yQ8Eסh:\*΂IWewAyZ@Z4TޫI ?B/'M1-$EC"ڷۯD {8UyN0\Ŷd%:i(i.z7wM@?[eOԩS溃!4Wfb k w)݁(o9 vW߰;Ӄޣ+nvti}\78OioU_  EdA `C$  \Mg]+:b qxȣ/<|$1;ȳ!vdłMaS|\ '6g ;Ʀ-*vy!Fq=GgLn c` ۉ05W2ٶN2}^/LXdul( 0'Q?1`U95apJǬրtkSu@v1i{U}_ь4_QP xU&<\4 dI=oM =!F3Ҭ6GLpI8q"㦁Т|:!Ga3s8TVͯW $`yw/;\| j~C̺-mS7.rTxs+hAR'zA}G{Eh puǧg FηMv:6oFQ):e 8:`rn3dUJuNKehtt mm;&!! 4UBQ:6}a`,ƙ(-)dn {XӊCC֊CrFiC`vƕO[`>qz!KKl " }KdP"R)R(%ĵA\`?{;W<&sI[Zg8@aU9Zڙ7AST%[@_ijq:vȥXJ6IِlSY;=g~W4d].'g7XW e9;#y1uJ4HͅyxAHk N}37׼GZj_?XSksP>;µo+MW?f6CVu||) `]4\. .1sC:C.-,0{a%@۾8u@u룏r`<ׯ`U3Aϕ7QD;z,Y,xduC`66懟aN[߇/;j ĵA*REϻ;h`?á7?&:Rl.v6`=:vwKQ9i.A֍otsgb7LS$)z&kL\N`cTuR8`vwo؅_QkO>ęzޮW/>(F{ c(y ; c(R+c(jKĊCQ(Z1Y0 ~ZCnр^X~|w/pksFK|U=qhlAg-ntP8p\i܁W0L <ggQ@T1FֆSۿ2&ɓ뫈2ó>kQh}%蔺] XM)k+;r[y41DGƠB.gTE.z.u; vWLsySpQ74à ,ڐmfvvILL"55SV%'a vH+"9>wYc}MKٯ8yrxiqu̹qx 2H$Ў&@s-_d:a0 ( &IPh5 \P =AWyRAjjtW1;R:Cugj趥2^*_|dbqKa-Mnܥ ADT Yjn~%3?{]7rGvKR9ʊq@5v`)۽5`)ھX[ZU`@0'(n=[Ys`52<nCZSp.:ZP:Zg^d, !J;qiÜRx-{Ѹ5 >v:nn,+vb%awnrgM3J+Kw§AVrJa$nU,+? Px10=e-k | {M >95MJk*(h qv@T)FNnp<<䓦Т|;aHS#8&'L zgVijat4Où<ȺUȂ슓`fd*p"y4@}\}MwC7c7]|UuK,-^`wD6G(F@Y" 4AvdavC[CTUiʈJ"c"(gȺ=m8>n5ՒBV=7ULQ W3ˈca\{#k.ÇߣYN`gI4H<Ă)*&v>[3p;OvU:;[<bv] /9 hgDY۹L, v\&Éa2l0 IDATlӳgOdM73o"D>āT3S1|a`39vX .Q,.;-۲] vX9&I8?6`Z^s)W@K#4ɤSsn;,GJ) > 5W+}~Lpz /+/!n奲.C} l)ڸDZϤax鴤⇧PrIhYc.ȵ8*/2J1kr :#͊h-10{Eϛ ochhe_qQaH0Tf,&v~ 2h #::-!vY̻BKE_ P1SPWƑ6Ag:>ӱ(ʿ nw m_}uۺICty7/hݱH$2TQ?ޭBv/Ⱥd&̽(x/1HJ@?$`JP3(sj?8T4aNOĹنc;o/.L Su{`Wq,؁wa/b   kZ. *$YG0Zp{0 ;_co'::{kE~ ;~5=N'?|i_o;Wqͺ(h+:2 {l{ueJe(OK4?!F7ҴL7 <?'OVDvy0Q (:.A#_w<޷.$arpB25aм,T@EMlx8{=o(X JM 11YN6Z_Pm_}ix=}̪:-DUء DZ;; CM;vx L臭M \n7jZI#I b7+׌<6ey=9tOC%Ⱥ:z(BmIC-,,]4qGy<9x/Ipu6qCWD(PcvK<ˢ;sqPgw5#'& akX27낛-[ý͛Pw0q++bCVvyowPq%;9ةmxIGc;w?P^TWF~ԴmQ$bq44t:F ` &΅HDbsy).-#9^TDFF`e;QSttXMA/vo -jGZ%oFa`;PyI0:ԡf K&Dї %9@8WjavQJ| m i,Mqf&wȖVA^9s1+;qb똵" CQtsc?0 QUìj 0':i L[Xr kdFLٺu?2ʹOs\E9UlK!66fE(~W9O`=G/\W^;!w%!Hϻ5Dorlz)qgy\ӝ=&?[m%P$oll8.qR -K;۩:tԢ_ UϾHKDT5, ڂ5e =kfOFs)$e][+(bv|,%,<؜?|mu,pbԒ LvDt(!OTsёQL\DXRՁFkϮ޴'ֺVdtMޭ v0l?|tn/۞X❰І8c_&=mJ\fVˣE}{ q%s3wp T;ݔ$`JP{x؁*`ņ5|*ӀtvgZG>ݭJvOЇ899ZwȶV aşcRڏ*ɰJuAX~bimIڵ~^WC2pf]/ [C=q tuy&fs#G,ÍNy } 5祡i)jU (N,Y;PR4- kx"8tiAOp~b0tm|7*'w4GL:Ygp[+| i(6)55رqKM {p 0mk%N&$RS3yNǧL:> V갺N ذꣲ!3h_α2Q^Qu8955]sWnihF{r * (tfSfuq7KЃEnpMEC!𐚞}pX Ч,4_{Gv*pLaR!HD)h 02V}~ah[;r \=&Ґ,HIizh|2 @9e\e=P ݄CaX  ;(5~+yq:ov ؁P,zLp&6fjeTA#e؁2M&l?_O^ /4~2N|:c aşG)DRtX5wR0qLN'TIS mrzgW\9Y!%୅ =016.h@fjw,;py\~7nܰHo+{[πqQ2: =βY _ 79hJ'R<344yH9UJET 10{Eϛ 6<$~ӟrŅy0ҾTzr^4L!5Pf4&6*!fbCbHE2 ɡSnTQوIơri^j9|SloǨ.z(quR!9> |SR雠]nMpU7v9i]tj G"!Ct"3,C5n@5ϓVAT15^<C[^O!CW-\bbGȵ_׶m-~*W߰]6Z~PI (:2;ukمA O`1T #4:=s; cr%lcQ* Vo|'J@a0A>]p}u|O/^F- dh74ca3恇[q܃ߏ'N.ik;k8,*aOUfв~ Ecxx%-,t @ J7ҴJ :4'Nj C!N;_'PCwuNGML0qc׃v;(Eu\(dy㡧Pk3x42_qHtE+N۾ǨO~K$',yt~== &8RÆO)"雐o6 k12=TŇ4KfgKx IV+w@DPgY9@+(.C677/chhe_>(~ms>atn\N4 i0tҨXzp|ێ#[BBoQ7wPP4rkbcDc_Ny\m!9>O^%TIƪz|IId&:oܼ?&'LjaM3J+~@Er+uDAR\~=2[UU8[jZMmʟ<ݻwߟ~i:uRL?yҮ"VƲU]{}U,#8'EٚDRoi9PLae:?'O}:dx0R7'5t %Ir)k0%S-Vxs+q(]0s{9*];oFmS?*Դ6Z*5=Ԍ}tuPjX}d^A%HEƗ88Sf*"%7K>8 D( 566hsZ8C&-jl `kT<Qi*q/ma?Tƺ. M\q--M0O}7to624%7@(rFkBpWYu_nѷ!r |‹a[Hô CWL;R3Za@Uh h :$<Ⱥfmn2XH(2$}1fu ٬v)mvB,Ln3UiMi؅tƅ|sҿZu0j:W=ٝOy}}zOkCۺ.w{`şG)DR|hUtm巜9śTt&[yO:)x^/^_Rkxi&^2pbIh0*{ +|>tvv{\__100` g>ҕOJPQǦۣC_,ix_EaЅv3!QiZDS*2`r!ΩSp CTtۥ Х @WI#,{{xdy=haGrtXƀwmyvء씎ƐV ظ>:=hY|a}5Q"pC[sC6u]`YhiǖI$9[ uKҪXvnW6@!c#: U;kwy ܊[\IrU9ŶCs>v%(s {,h@mG-UaZ?21O@Iaب@"eذ9ӯݡVak`&/qtXOBuxXתI2R#C5S35R`cyyŤsNႨPA<̀04 v0epʤ\>;am ׅ p,oQJg =J!3'pM[@х]`JS0XH1%曚#Wr怑1=xi&5kQxgh&Mh;sΙ^|̺<ܻ h.o9!ԐI⠀ BX|mÁmC$DDs0{Eϛ +塽P;m(qK)QN,;lf߭f뉫C˅ǧg:].7Grz_. 6WT~*F7 dAk}h9|GVu^vhك]Ou|Fo-roGr|t&JWys!vz(Z rh R^-MhmK~_NC&nYFrb Ba)n* \'Y^v!Zލ v0̷1+bƿI.smùR_*H 4QPa\Noޱ:mޅ;2D cj}ntf\V;&cm@X[}MKI9Zu`~@s=]M`iiۘf(x} <^fan~W)T*z{&*I"CM@e<ݻ/ˁL=z:BAP,UVD&py?˛6o['Ǖ_r641 OK~k>c@d:a7ۋu";ByTʺ̎& ȏazХEx \۶ ,UL{)5=16UNk98/?t?mRIKҞ!HDEJ68аsWBip6;QhOK ,8:=!@[cUa@TKݰoH;[  EB嶧,r0v 0S®o>fx.-z9Ž`A疮R8/v`ZR 98|?5K`dnr@r~q_o3{o3^0wʈƟ.2r2 /ǮoU͕rH.f}6WP[Og\1 f/cUUǏlN&@7տ}eց&-?mڻ-eB4xhul\L]!)"7YmTaN&Zn'k ON[w8ؐcC Á@EkbP]Ʋw96"ҮZנ:Xί, ;P66"`G)Y vTK|q _ />foz2աU*؁2QfIAYsv ՀN+xnۛ^JP\Հ+C emsjxѷ{"]6Iff+@FV1!oE_ť+AFz8k}~|w?N^WAWohZjϘFM`}ξ>;w鴹FXWY KfahOKW37Ҵg1 <`%ux ㎨&LkݽumeǺ6HMfo(0h! JM@H$&E 6ߎኩ]QN:Ӌom9'u/I~i)N奔DUC#:<qDoڐbhhg*/qpiA|Wa~z W!2ͫM=0ux诤6JV^g88@Ӱ:f9b/GDdFVdf nOEWk7>8`AJ| s{kEvPeFPp ͮ=vw,>pqK1a+!9A' Ez&#m(XpoM<˫x7? S4kCF%pު5*@i ^F0W瘣 ?Y/˒F^랗9Tmrڊ8{ c|j׍A-F-1׎M_{13*F-8Gl!C Z>s3 "%( P|=@`ҚS_M<><v5 EStV^?>`eH "̲cEȌƀ^גTM_zn~)ZCWm(C+Nv S9ƾFpdEDdIھ)O%@a2u ;),`,s+ ½*p.y=r[Z,e©&g;p A@E;˙i<%\!X| e!ɂR0_<{,k3=h\ ;,,*>Dp謫w.3^ ("8\$m(߭Z.-A8g&jnڷ!]=hR4`,e%+/s#x& K ߝyq8Y^~"Q9i8 L+gGCInࡺ;rMuwL{wy<_ >aPJ`K598O/X ݡ*4Fv;a:Y8@ϵݏ~fm"C] >W P{;d}plW$oaoO5C{9:}v锔SϕK{vU hxsxlӀ s'>we24<^18nMa`O?4N:e*xB t`{i\huE "Jm K.?/!ZXQap޺0n]nC=; nXO\GA$&wW_{r _'Jjxp}VBj(O \`*GqirRx9z7WP'_TYpduCU[mBavs>[˅<>*]m4aα!:2؝ li/|5ϕt7?7?h 믅q'(<.%Ղ^ 6ʮ`?KP R|-|%wer:P|v0yN k$`(P.JGLpz2E߰r[ELI"jywee7@0 zmO_]:=![1ujm4k=Ad EiM6VA  AӴ{!>fm=T]w-ʿi *vgɫW7 owmhڷjC?g|azhXH$RQY AS!h*< i&DDVQ+4oa`FHkϮi1QD:p Rhk0xBcby7_T&m)8~ ޶-US;r.*CWO ݆j⁲}]|Kyhmj->(,CR*Uk<+=ȅ/pO^\x 2QB_VկpS+Ӊ.xEk=זW(|Y ;jŗq;X^C)5=كUӅgo͹+CNp0,A |0Y@P <~oEdznǼY*P6f 5o bPb.Ki ad,v}Ii<2Ft d!A'?n@yV:w弦Ӹ;= JLͲV- [֛>"n]^{ }}}8}4NYJ<`g2v]wSTtlL~zspbŴօ =.5όpx,_SX$nt[p vJ"TN̂#wl? &Onf boY.}AD&Gj=ڋ`!Oqgߤ$2Sm>jj{Z˸z+I$;ւ-/i!"Vry[,|":NE9-T* IDAT_}Vz9\Urਮd&,io6)Hb aͧ;_U}OaJVp;3-Or ,CJHI&a^Au6,Cn?б76zQcf16poA\X9>5{,R@d^ОW"`a, 2 ;lXqrvLmgE6,(X_VMet^w]ZfWjYm:y lym a^#Tcɹ<04Uy}Zuddhxx4EmY~I-~`hсs녧_{..tqpX pY=6GH:U^;Shyv@h7ҵJ..pz{{+rL: e'Fzjps݁h)`qfUeR2F?um.4 ܶՋ ZVNAư83Mר;$.vxxGv}%n@`_᭮޺.Aw1;8JjaDa=o_DnCNl#䍥`4SSSၨhn+TTH9,9&醁}|y_S_:R'(\&>U'GmOE9e.x}Ƈ ~o nzWʿXw^(4fh۴)U38sCrb(fhf? 3CuqX-@;6@V(8v;`z? p0"1$t  Ղ!Br}@Prދx'1f.8WV Qԛ+#U >TCG"X/m*oE`O[AIԗZ7FWe)d&84 q4 =(JÐb%|)ڃ35cvnmxx8dUGpgPj?ݰ| (ow@;iTp*]LI W,=<Ξ=ړ&)N:Q^ yVO>K*hSE&kw JZ\Ysw~^*Ǹw!W]Ӑ_[v{]:yzp?AbzR>9!аJrmhCcɅj?u? V/t\ tۖQ\<,H Mu^*xrf9 X!09%$ Uge \J < a ;ps^ .ۣXb}n/ p+J&=$IL`?>1ĭA듦";pJ:CEm ))BN|^ѱe!PI 4ykFr8,ls}ચHcHZz3\xЋ5o tÀ[QOg }e9Ț y}ww) AV d3< ?u$ʷz;6؁(*ú4j+|;ayEsW w,MM޾>\dSFȨ<vȩVA{S"ԥ30%xylkhOGaC! hbpr>)ƽ+xk_>~ ;"bá"AO)c߲uyؤL5{MfY j&}zh< Ѥ0P6BIJ֭ye 3ΟMt *] {k,*gP&R S QQP*)!{)A(sj~  ]!ZkU&*^\qu 8yoܽm9g٫GJ3 Ҝ#TYy2__7ulX=A M4sUW}㝅ȩ4tbJvo ֵ /aIzK׵I< U[ZrSh"ȺBp4Yנ~^vwhaO9ۮ `Dt+`AҴUn  dt ^Vտfq%0l -|rR57GֺB 1kK<?5};/6rmi[i'x7|:yGӘ&Cn(/ OfzPU56^UtWb%ollv[BL$XRՊ%* j]TBZDs),+\-0 xE-纏C̥OVtzl`H&a.sz<c3k'L^A\.Ny`y~Π ] VV-"tMr++!PZ`BcexCk vJ ӻȷC7 hciY cs\z!Ȯl@!,;G2J(^v_`iއiH'|itÁ~_Ol7/Dz㾽ʼnp=Xd^n{dV?{qF@ver/dWw Ca-J| $5o zބꂡH܁]z,b×=8T͟fj1XZƣ-5xwU%]sֳ, Iַ;߾/}nu?nɴ wE RU<|&|M,*@`J4o9`W_u$Ӗ4q+4ur݁@yAy[ ]YDDDJ 8{lc1eDžz~n08 %:RD<۟]8j_%'ܥ~CdL8‡*4 ֿI#N!̥ (.vq:0~+M ރ*`w6%ct(ܿNCI+rrG*Ԓ@ɕѲ](޷*D&Y_'X6sSM1S jɽ#6W,ߎoacY Nf7frZņ L%/N}/B['iţ4@pY/Wu $2 ||Q4g&/y*V9׆@\̵}lX\E e7&yux0r(x{7l+;cEڧQczvv(,-%`c4? $W#_3cy36s G觷-\&T_ۦȚ.g/: Ⱥ<! ߙ@ ֣4ƮI >C^1%aqPio$`8!q _{I:7+g-~;XZơ@k (!bV-PX8~L<}΂[hvg\@x(@`XFGGqE;w})@ҿl;5`jO9׉@DDDlѫ0dۋ .f:G``hdH"Oo+]sHm؄3rm-qVD{ 6 C:ShÚi>/x_ejuwh:( +g8/!C,F!Zސ*[6oүwy ZJmG 㾎7,x(1n4(,Y.?CV-k+K 9S|.CJQGKkF!m2$,fifZ;OU(R,mb:[e 8cCMKhs CNDtlJ:S`j-9-HcLZ#<|ah |Mu,I;n N:E؁H(ev.ɕkx,Јk)ąSYb$8 .0'/UjP2oN\;^TԱg9_~raA@^h^=|!P PsN]}ZhSr!y.ACp[M-a7PQCEFQO~wԀ t` ۠罇{rٳBގj-NN/EV(KXcI_@="J PF^^BOOOzW%;Yc9Mݾ6u@KZ-ix>&1,{)Gr;8+UV_@*iB  ϣZס[?xݲC뮪@cKpC6ՑN >F.o9zJ$z;.(~B.OoXcpetS!k*b4jg}UL-q<_~YAҺ;0|L2=KyF 6];owhfhc7134dY &K0CΥ!77TM\  loE?ߜ|߮"n"`rZIމg|nL>" )̩20ͯO8=q :hB0.=5 #=*$( j`d6QS Y7ߛ&VgT2Hмu9r IDATĪ93@Z?- D](6_ 8tiU.-8 c`nb!́"e97Y gϞӧmێCݙEtE|lƒ2.rVLY!sVr ""rd|6V/8}4\=5*%I#2?B RyDkϡSZO/#>i`ݾ6FxcbɹHH*aO-_dp?)nz2"IJ!'7Blԟ}},U0hr!Ƨ@T2CZSTxeܮeƇi|iUYk% ?W`6_apct)x ]+d1$/69m"FD4%1|Hq'b@.\):606pl\taIhA ZNs<`#mq9 ;m\;N1lrpVtW";:%,21 {un}wz`ȽLo?`l~LL" ])Ȱ6 hf=┻CCp3P%G4t5 :tL̇htCa(k[! ~)Ię7V[>"z$CMYڎ?E)kGI@ֱqȪX۟CԀW=?-=6ṲUeiϞ=[PcOOE)+^`J%rJֻۈ;Rr ""*AI5`>uq;w}}}F'uy{s >XN㨪'Nދ))(nJGS/NE&@tL1,AeR%1Sѣ`W=dqg1~O' d1轱eaow;3]_}?IԌZ&*x(Ѡ vZ2K9HIMcQ ű@7ţD4S$U}Cxm22LYpw/W'LwC}1Pc~ Be 4uoh]6h0'f_ \n}N" h`)e;P}%fvv}^5uEٯI k:x rӽ>oU][-oN]ӑ аYֶx'zxV߃SEÙ4`&4G@6P8BhU]u7w A<]|#5u{Hj0GW߸{:ӂƶ%3=5pFYNd$mm>0`+|Ύimf):Wb4a-EOty.@A_b9VxvJ% (پ:2oʂ;%yzt޳܁yNA}}}xmL~xPf%̍\yj=H}GO#ZĔkʢd2u/.8HߏVr*UVŐ[pSn(fZq=q2W% @VU_V C*,wÑc<WHـ\Nc;W&p 78ߺ 1w(_5n4zy0״47/- =6P\/ @`H -H9r[=[@*`I ޟEO[ @YP$倇|R Hևqmr<[>^ܡꕫ'2}Ur˄A ryA*N>m)9RU8= ʩda"Qe J=@OO^}ՒÏX ~@dac+'O#ZAɸW~Tzu.{B+v,BU;<- `8zp2Ox@*d$  6ruRt /W҉@#>1@3Lml~1@0)By;E: g0|(»K/C]@ЃA]m1*C΂, Z/@Ph޶>OOqy 70KVewGD "" \9(g;Qc!?DG8b9'CHeř9הgLǞTEG4ÐiBb4T4IIe-FY_b8m? &B y, CWUi8#?cxZT_%W]NÅCBU~aND'3"c:@?PNaNJ|ʹ<̥ yk8,)A&^ow^~e{HO3(>B,-ٚ((9M]Q%W~`) pVD[yk q["J}p-8w߿#4F~89HHN[N *OG15CT}Trc@Zuzr`h7*NN]Q&uv @Д䂴) CF2f,Fxg}Tާd`l_^3Wwg?EOWT߈.ɺ4mف22$p#࠻CN!/*?Ee#4t5 `̿0h17a" ii)w= ؄Vd C͡Xe Tـ1d-f6յ1 J=v2d].m??`>uK/ᥗ^±cpY}Mz70:|,˝"Fm=ba|_E__.^X/~ [?yTQQӐ[pMy?AXs~BM eǃ@t-d2H- paی\jU܌>fJ^n>CzY4?v4d$i,ZS9NX.$ƝskrD4$UkЍ5]748eN&{ѳm/CA*•w,L 3?i9DږiPK+qx(0%>Y3Cøw,8D&0Vν`}5"rU֋swp;ʷz;Lza:p}lvl^Q6ꐲZ.;DzuXMQݿns޳\U֑ 0[,Mqƴbc?~&gzW  s8QW%]LaIŽCsgM5bd J ,Т@O=4 U4 Ff0 [F.pg~PTy~h ~?uy6MDY[>;^(u:0dH"a Y[ Ha(o Q}``('|;\g}t,)k6t@DDe ŵ|`d& ҥKAoo/z{{ 9J+W/|0,IOX{G*(;i(a]8+-B͍Cn"1CrnJgvoݱ *مDo%/o^:>H c!ښ*>qKjjj1me[P$'MDR )jǕg@tFUwNkd+m֊| ,cz<`a=AƐ-t09w߆ݱrTVDYo4]`|5RUOᕏ@:w\% ZA ۮ|CP@gDp!"oVsF 0 U 5@΂+G|u8WQ>&Uu|_ޜCx o/ht4 zFb0T P6QSKGB1|ߤ8؄rTeh }g|>IE@>!Q,_`kjyʸҁk+0߹Cgom qPޖ{tyFz4ʙŢ hke,] qs!骆[!*^Mu4F3w#ÎU?Sn+TTHbajH4CumvmƬnh*Ti#+N0Ӗ  ~8gמ(n?;4jiBGrlq~ű!6>IEsB#!""KM˰:xZeo#a=O!ޮȰEU~[SG>'G98V;eVUx>F6!<̎O\qhy@c;e$BǓH(a;@ӛqu{0Q6Yg"~ EB1E<:0'z]~?= 0@vzC!À:?؝k z^32"9%3V vy2u8,,_c(}?B0 Uy4/hu5;t2m0% d[?ebP2Jgt/Joʂ= :@*حE$""#'֑y@|Wٳgqio&`nn/_}EvPfʼn̟ D{GIg_}凖W?gy*wUCXZT2u4h|8383ac~pjoztVl:3Gbޗ ^,jyQ %dTaݮw~f9 |bdɼیt~x2;Z[e־r rj,Bt#8ƹ: #q*A u{b ߼9mhRd!]2Bz`h۰ T|,J@(E2.~W?$C;.s%gM @-WBc+-Ͳl>m x` xПI{+< <Pa:A

#X{Ѩ n|ǎUL]HBb$4+P Vϕ%~ H۴ųrwq 3W f !r 2ɪ$aXc3z)i `BVSR%̋eh9Xm9ځrONhř1NS z0E&O5t܈Σ6^iæ%`+o읁r;kHHs e$:އ룿9A]m0`B_B,(`S3n<۴oCɭ_,؁p\|v>t8 gU`Lu7Pq[FxC`"ςkwiȁ JGEr(.]Ta>$ʜ^![N'I FcSkZ_ݟ @d#隆T41!*1ĭAW83h?u4JȘuw^l4[ 6?vv2 .T_cc+˶H FDDWB@4B;|߹~r24e{{'+<.R=Z]u>B)*$Q8wpݫ>RFGJ|3C~W IDATxC$P{·i+2x ;@vl;;p8? hGMVn:W)`q 0e8І&K|6]ƗC[_%343IE8j;l^}4Fv;|}n]pmi"RFU0+&p8DbRtypȷ*˘2<4X(EӶ<f$'-xCٿ,®?Q%hR^ ;8<`[]Ժz(u(0c}8 ;L8mMa a4jNC\Q|k}ˣH|s_~ 𩦎s*Bкd`M7--][~EBRK`PKi Ī y8=R|3ܳ iU}t\"] } i*z5_sAi]mڦ>N2Tau"PZ/-}~,G?/X-{\ z?w^nغ8 96ڙi]|Hy[ t{w#=ź7hA͞<|Yw > |`Iَ2/"ܡ:xL @'~0R-q~B"׸; E{um*bX!R2ܿڿgp`S]Eկ,%_$g,I؋?Mj +9o=hdCO @W(s骃=pUA.^/5uucNP,A# W m.HnϚ؟ɼh??/5zá2 i`yK焺 ԛЦt㴳|q\׀=YwUr """rEy! ;)7<l7t$UIw1í^Uk9TL򎔁8GvITԣeU [woY6ջv\0ryŽ(ø-Dw` rDD;$:\׀u p֚À*+FnR&o|ywPS@nz\.R_4 c a !)<b)ixq?9>22]i,}'7F~A^6G@q^ڬEPB(6!<~ zvRp v))JP]CLESPR` ~-hKE=qsRK6~LP_Jx)YX듸t}^^Ƞt?6ТW\_V;r@dz0V"׸;\}P24'_lU*[ã6kw >,| `e:ya `=;,1{J}]maM/Hbw g+Dw˵1*KhI T]i񻾍t <赁TzF"4~ ވ (MQ!.DY\ ژ7Kt{8=y5eUg3Cør(%@Qd# b=T!y6lT^r0rpICS@UU-b2pKS2@>aˇ;~mʱ.a'/ El^mw^,]͓eeXp]71ZqyE(;^#-<es3ޭR;AO_bWze miU]w:pts9v DDdPdiŭk : d: >^h뿲ё `"Hٞ78@@?.[n>-Z."<q t :x'SX>B*oǙ^s|˹D =G l/CW& ԿG#;D+8網mi "}nnօCQB;ASUH7V},J7=oOam #ڴ [O`_#ڲ?:tˆD~!510( ЬZsg`;6Q&hѹ:P-O̬C9p򣷂LB 5; R/z ҋӰW@F!m쁍aԿa @XR4]f*HscZ_4YA3 fm3?ES]Ujr9- pfb J U/w/?%|;]sjT\&9iay&(’cǥ^Bzzk= f@54j[ y>|N];VRAEҀ./B塈"(E<{ ^ &9ɋqk1q(%fK,[wN\C:tK;% \< rMѤɩs??;xhҿacm'VKZ4/CבO$+VP%5V?k1N<rX1C,_칛@_ Mf `xfvnLַ2V3(eڕ@QB^EUA^wWL<"hU5:;l+*)A,IY8ti7KW 07/[  }yyMQ P%Gjxy(xP>3ߵoF78=؋סzeR~Cǭ˖Y}iRiI{}Ur7E;zd7raۭEH'"G=M3Ör0^yZ_z:x.,2^0}tg&u0 V5IJnZ?ݶ3t?3"AY*Ԓ6+={ z򃶃qu Źl; i.Wfmc˰2{ +8`ELU'Kp+@EFVj5byVłP|2N<{*!@)焾 BWE JE|RDH<"#;C`0ȑ}wu؜_U>>0 !;x "Ҝ _~d]ycU<,^H4*ˈ5f 'SZzsMc8w‚rzjȭ'0֯Z^uxǏǏtԘN.`W{vDyiO'o DD!'ZxI$찟AotʷS@\;)rZخ@7 YLb<ȣJEQb^ 9N ` WPd\2ݟ J#qO|:ASönlnBR{YFT~cCCǛ;l9Llz#E S}"ǡX X1k*`9kpl*×`BC]P ]G>|Ao?lIP?8Exv{7~Kݞwm @DtՑ 0u`vLiwДP6`FYK_2a&Ǔ/.5 }\|~+QUMat0 sjzqF `P!kkmA`D3OT]ǍR;P=M#C +Q0 z(s*S'e j||z8"V?a<fy߅[^ZYi )_f'~̀rxpb:2z~ ٜǚxtg{@mmE% ÕY]`0 8)uЀV@epUؠ١Pҟ~K{!dqϱ@HC-5(1/x?bJ/ ;%O#{3V3j9UT {{<j@˕`wAR5(sMSςg ;4}[Pd~sсsy` +CQ|;Djʄퟀ.'`yb=֡~~m KVFt{}!Vi<]rI1[FuNF nC(nX 3˝%da@dhъICw XZ©/\}I4/"׋@4cOE]vV"TE'?)[X-L8˘z"i+zȠ:<XA-,;@QXֵaou]'ÿ#t.`S @DF @`ȜWtX|iV>Gn|#xJuOHkz-çN`y၎{7~K7(E`|a0X_'@:1KEٸS3p! >:5Z'tS[ɶCED,s6+7GI7+2 ;lÖCz{` @P< 10[c3[]yZ?eska8P k@O8,W^c_Kûegdehb2'`U=``#XCToY;\Nc g^LΨ7_9|ժ7 jUHuKѱ*[⇿?p ""Ai0T@WC)Kk|lTu TDu IDATC*m.ZN?Gw߽#K}N}uMC&&8A;q)WHO\jrX9W 8ԣ8ˈNMF: 94Tvgͱ $Nwwޕnk ]M3,ϡo0[L[])*ҫ1Jf߼܆sMbԉv{7~KW8:00@?:L;@3Mԁa$`>O3v H@s~tab9s籽 8ed04#E`!0+`/3xmoJ2&iK+x0Y3Z;4m8w})*ҹdWf@M`8|x=>WWn8`h3^Ё Q?#7C̓ ,".qyzvvO:U1so[>bzCg@ɢ>>9:^\J, ۲ҁԅ4R?[<Ёmtw Zqp+QC/Q E{@yGH=j0 9҄],7;,H>m3] yҁL9[p W?3I'ܲ9߃ Ѭ{8"Vq]"QX,044uݦLV "js v)wGq aHUџ:kx6$[J }1CNvBS 'F$H+{Zz#9=Au \Zɾ4' DDD] ;lC+)wVv @wREۚǁ#/8~di0\ޙ<%V$`݃IOJ/ ;%f5/{/.RYoagݝ:o;8Ǣ=PuU8>U0K nY?CބLO޿{GK J CbppR<}v#``lZ}cR&Oe(O_ҕvӊohBWHM.t@_&""A/%x̝Aݫ~@\we{̱3Hsv!La߶?#dx"Fckev ]Ez5r1cjSNMʩg S=7c{NbK.&Tp C Vyf3sILP 9Q Nwt\DJ{D>ܝ/khrw@3~P4m\Tĉ K})>Cue,`ivDDDgPn 1hVL-׆KYzJÀf M&MMD%sbyGǻXnpw[J pIUQj5bޢ Ǚ^ ͡K pAv0q⹋NdW֡fzxD/_r~ Pa.I G!\,>+"G 7;Ou"Cȵ4)KI 9,#w Ay𭧇0`1mY1}MjITY_1}lG|W&<&Cב\Z*:;ob]83u/ \]/k?FjymǤ9>D ".E0tAPWse+w= o:ǪuAp,vRၴub vEW.y75YÙsHTE P`Prby0 f1b&U^xA`TDh z #t#P_ad~z+y`2/W.=sW\&V\)nm&\5hitSG?Z^=XčsWm1ap=C`|2 uhOZ1e}.XP3'"xi}tzf;RryP2]|A7Pj0XTH" pn.qA0B!R krf &ˁSJ~g4X)PJoP XP4м[5n ݲCP^Z^ !ue_nx#^;ృr d(]2 mWojp0-0O򎐮BDt. C>v8.ij}шYwbMD D (B-$9[x`X,v9:KW|mR YUAH 9q#w/aK%3Cx{.Ң8Rq/0tmi1oXܦycphs74 ]ӺTIW1t=o:ò NndˏEsa~P4&""2&#N;Ur\v,,"n9=- sX6 g&ȵիf?Y70$Pd6;lkpL\ C/ ߭~h,yMbH/X!QR$q (~Xa-(ʐ6@yL&v(hLJ.ra0=eux$M3ꘈ @DDr_2\eo`VՖ#С3`"7t=}'ݡ -s /wFDt趋%8 ayx"SEqsrDW{˺6n$t.`M, ""jfB# [3<.NŏoY_e?6w%E9 \SU۫)nnu&SU>zm$7<:SO? 'wM#42d i@vEb :K5 M1Ue[dlO}UBPF>{:jQL,;l+I0ԵC XbĒѓnfVꇘ I/cX±PIAvwl2PD`L@nyiG~n4v] %Cc\*bηC6EkRd/S3\Q N6W-] Hm*N|no:զ%c ze[Pi;j>{=qx0v[cwG|DDDm` 70tYn5Pqu0CAAүa8g'ZΜ"ݢK3?}R gIhm nvq>tΓ#̡iP fJQX Vhrb hjR>Df[ʱȻҭ}%v IĘR4i)""q<>?h{XÐ4'xe!V{߾GB/*˦B2]{1CyaQM(2Lk^!Go]fö (w:P4 .4 .|4'I@{@[p1w. f=& tq\tov Dg8v`Qi0vżjn6HgkmQI#j쭸텷Ji˿#qc6W-8DUPTèhVu?^]|ܺ+ xq!`Pkn>4>d(XpЃzk#Eöt@jSA7_{:k7oYkKc27 (x[@ȉ ;[\Qm Uw0(@DDDM߽e {1v;.'.ȗA}ɥ{δH"sowo9Li}~wu&/4`KΖtMR,&+PeH-'PHG?q{[:HXlahhh<Ҫ|~3O`<Bpo j-h^W`h= ^Ҙ&x,?ۋw-57̅)JS;l4TY]ٷZ;8:lK ~tgčeS4 .r|PiuL"""&5c qvV`2$؁2юixq;Tۮj'n{ځ(Ќ {A<p*51`oqLf1;ݫŕ<=^_zem{3(`3Wz}aVe90=ōX^wZy|Fxaýqrr+C2f,@;e2J/;ؙk~ k"(4x}2 P,mh(۷ai5/AKNV8[7ZRpU9s*o}89uvJ.;0!4qwiE2(ĻN2ԕ ;B-AQ]80d@ؙHwB",s"<nxxho姷VincTVgp_7;> #ubw}ݞoIt ""2 6tDieR5y,c'kiƙs9&8 vJi63@:`NQE -SrHvVvWkxu&^>u3BB,5`m }YN 'ԀVw{8 㕇9mב{b"@v0,MI8Auܼƅ>gl~L+V 4(29؊-}rzPcCHct) A1hQ%(uc&]pFtne%3Fpi-,5r4dPk uG`7(Rt,G5͉ fplf36;;a5WC:Al"b6k )%WQ x%[^<C"lJodn"񁈨d[Oa.8ȠlVz qeDA}d}#UP>ؘCF&pS7j?Rq,~t>>ep@'SH\;r8XdT@ XrJDDieW8g`ч!tlnRx׊Dku\a S{rO@d#؁U:H w ^l 8q|~q1 <8 jx`=zPeZ7J-"2yvҙNC/_zBOaZ4Snн~Z3wG&i1>FLjAZxi RR[9V9j IDATS6\XK_륱ljUl}ba{p\cJKR);?KK/ Q =Uwz |YuҰa\caߐ|,rReyxNr/ͿI@X6o:QxWMǰ48?쳴G_H@8[{&?[61_g-yc"qy !th` ؠXPA{ #@qu?T/vyU6)(wO"ʠ dt@]Źy2([rJ~PW~vz rȼQf6Sx6՟i)} O\; 4-"Cj%B*qA 6H<4I&+mwjvfp h0 =ooOGUEr~ 5D/긶^2֍87J7Kab CCC&VA3y0.!V&| sxax%,8AXb*:ĚUYi˃R[B18r5&򅇇kl : ,L)hYa))vC{20S9 ?Nwr0aZay塙ϣѾ[*.FٌR`(AyP.+T zc@ 7Z\S}&IuaᣫWHQDjPHZ#"J{NcyL^>#)  A)P bm!R[RYk h:*X !Dtjl۟Vl~bWy3Cʒ1hq_ &"gxD>IT`X[ֹ<il% o@xcrҳh|W!XD&H.4;9Si)FPk}k͗Zhgd35L{]v&J@1y ~o<ԋngܝ̜^ 4(s ;P8예czqP}$zo$nڪb/ͽB岪0пN`$I @QwF(`YKci<]rqNC=>7)$>$QY0G WyËQ|-zk $8A?21RJKMg,i:n.qz2bz7QVѕ"qD20|. &A2rϟ0 _eVRvzSHDdp{ pAw7i?Qp ᏄIE%a5`öc䑇) n ԢUnϢt H;POb& l/CWͿY&z?u}X,(ʵK\1Lx ^֝.: s+4?ϑFK3؃9% mS^6L{1`1\lHAK76DO٪#,0Ynfxt%uxx@J #God}?j+g oı4w{ Qu˘/ܹ7H;;T͞BW5*_Xr ǃ΅28jӀ*յXӘdhӁA|e(iM_0>A"C*8網eU"ϋ^ "{ D>fhBOI/ p/Biڏ٪`c>:Z.)_"b60^e,tAAg9SfH5$U1[C/SO?i vh=>nZ;DIQ16VP~Kvjm`yXhT;L_}xywH?pnqx۱Q,ǘ?V98.SӰZ ;l'l&ggG8*aF b!ޏ__fOcü6)wMm@*@Q`C"-eJ 񸱒j b ,7<@r.?Lf /Ϳءbj:nOR`y&@,ֆ Yt{pvJ.4CBVg/CVxγ_tv`Qq/;Yf'0s+*x[=}$z4$ 9 =1򏠄0yR._oW~W35ۼn~vˤшȼZO!Ch?2;gis=MC"R+8qw|2B+z|.`y1Q!g ;C+sv ܰWS~#Tbѩ Ƈ[6/*|}a~γx+d& C' pc6ɕPTjE`"[( q]sY< Ż]\K7>q|%MQ\Z GWM+89ȅu&ȭ',[x7v`=<|оҵGLBDDD-Ehn>.>:"8p8钻WJ^S̄ ;T {VDUi`},5yL~a'{ gqGL̒,)'MGe@{:v0w|yQ0` Sj( 80 e vؙ.u Ұ ӣ2Y@5 2JP8._(pxaa\KzKwo?_aR6am}<`zn){TEG& W?rSUd -!gsxٚyJ/[ .t @ }.% &S9 CP[ΐP3P|jYL"QÉKZ\=gm-ވW GmW%FD 7ն p*4UM)>ru-Exha##!7oTऺPJ/6 d3~-L45/c>.@I Y\)YC`d" QB-I/`Ռ,%d{>ӌdqfM%fj&xߺ}cuo١o@ ,e0o̭S::@1glUy|Nö~}qf֭+obBj )zg'*uB`dq遐,} L ITXv6m&~LN{9P:Ua8\`|Âg= xqzwLWopӑ4͟yh"_VEH IVs ~)vP` d_Jln(P`X dbxO1;)*<#a{%Ҵv3/jRF HrCmwxdhu (w\XlԥQ$o8Q|=tӁKST|C.G6HCWמ6oK_Ne1w鳍)a"Nm9MENSbZ{`i Ei~QH"0=1 Qwp E! VyPe.*fpg'<_g?PWy/o5EEf9bk9-dj\lfo/\GAs"|s} 9 .1 d[-Ev(K㪄c!ZL`$3?|}7@UhcUu̦%%g%oxz߳LX#9w=Yn=';1pKb/پpfhG3 xx= 4KdAK 7LV)C`0*oɠ:b)A?_ =lE a&rj|Qc9 W`ӒDp]xen /߽V; Ӹ,; N}qЯokc5PPkXLHp5T ;îxZBv| ci|[цĖd;|>kmN Y/G0HNq1$YC:r U'i*ЖysNYsMeGWRT itui_F2ec@]i?2n djHUVśrnEpIp"y?Z E2d᳨@7zCM5 R< 9B,^%8+lHۻ:F`]Mh;OwaCU] g ZS~ZYa:;Ρ ϡݚO{sW~Ύ(:PXvl5JRi,M1>ԿQX- [.3m>_qoX&L_{hCjA@E/A@|4j5)*&Ҭz9a"H`TYU xf? Hl+wgKV'UEяT@26*e"_;fAQ^`T;Xp>W@ ]o}eN?8X =TKؖV7)==Nv0 ֗Y(mJm"\U5K[W>tĂCNޔƄ*B@Ndpscpp~ >hrRFS4{( pڬOa"25e͟>2?Q-)h#; Y֐HU&2rIP =ė-UdN͆Z驫LZ3r' (@tnOhlX IK4@@u*[qW_Fe p0~O߉R IDAT“ho=3:7[q`[%0q=\?a6+*1H@PTq h-ȩ,DArF4Ʀ;ҟ^~v}~P 2-It(|}-Lv*H9ӊ|Bd9]n sOA`YDu0hE$$i[`颥<IDrw:!R$C_ ێhVBz `OJ;P},TU/za<77l١>:i2nB+z j1L8p""נwʛH/~`)矹)Eb;׽Jn'&+ȧ) tM۹"7 w*  wGphFH\n-H-nQ mr]l_D"spk×U:;.qM\]w^V˷)<{! Z-so,:y!R7AiT&Ah㙣ّ)d@r˟gϷL- =N|$XE")!Gp)_ݰ޳Y}gqso?q=b=گHyK}s헋G6G|nv8zBhpzFz:SZJNB. DFǶB{Eܻ ߋ]%8p.t,1o#Ż3h5TCGJUc9Bo` {y8c>AnK% 7yA!qàR&c[(. B퍌M@ַlz3oo{嘥rc3W.Z˦d 95hEM>CTEp>ZihK\A5Rb:ʤM#E8NbWkH78@0+doqv iQC>Ұ K8ˆQ)se8Iv(sktZg~Y}Ä}p=VH Q w $;ȪIVWuF44 , c r4(rO**֩\T!=$ >bZ0@͍I%<١|) )nCNݏSxa#&gxN/}8k-Av?j4UO4( (6JPfVR*reT0_H<71Ժ셑@;xfyhw.pf!}~g ~2M'.{A{E=LdrUw{$SzcBx A[NIZQ݁`bwdbqL_y虯(/NN#1;i$Iܡ;DﱂrP֪5 򊊃HU!M`~wə5p쇧;@ σ9>7P ?0`[wCH$2EZmZ".8$<FZ`ۀh~AMQ!4CG2/!XȤ7^4u6Nr6[+N+z=uCPeCz1ZwƒarpW歽9t=ڶ'@@@,"racX#2Jv0.u ݯF='`Ejm'jL_oE"YF$K5Ώ1D_H_e4gӯF} RZItaYįsޯ7yK匦ͷp2mۯzڨPT]G*@Qt#MGTPjCd')AhS&xӁxSK)aR?}*pU7Âg? o~:~g7㙯 JS( KCScTɬ4t E>W4RTgr2uqePt}7\$"9۝/9Gx ym:j_mmЮɦAĈF\@ɄUO}wzSSD偌nEyrw0UVdН]Ӑ\Xj?~; Ϧ>i,NNaִ%e _O+ x(ӫ$zb;B{ػ vGp<A -f҉w>D:V[!^n}0SDs BNjt&K|6d]ۡ -u AJ0'!zɚkÍw.`T-x}֮͹:d4/N+UqUlᏂܞ/U]G8AvUdYKaOeR S$:D/T@8WR"TJ3hZC >_:ex<g&تjFSpf-:҃ HGA(_ l<$Y$ `Y ,Mx X u* i< H*AvX R3yC<$qD6 'w៎=q.jUn7gG t>w7Lr#g:`9 4MbɩW( Xk+ZSОhЭm}bTMG$C8r@u1::?O:N v+"}G0/<ڏ߾?m}%*d hn*87o)𰓐.CV7/DO?K;=}U"CCnذ x hJN빔fEtvD]݅CѾ7w5 MkH˭DmsD"p8rR 19R$u!.7xž\E^UI'4 P~3YՈFˍ\'WGl=   f [WvXҤRwXO~ZMeA+{3ðjM-Lvb̬|z5$T{/o9Lth=Ij;S֬A5Vi ?zmܔbP% דnXOr`U_Ar+.H*U:eb9Mi{$h ^g=j^F ,d s/UՀ " 'QTJ1CX@HeT}\UꚊqۏSK)Dvm4M6\:^ĭlύ;9w@M,3pK~5-f)p E^#]deq ќ/)GlߋKy ? k2OmMl{9zk `;V+,%H G3=Y2 GLa1^Uyr]p_ mAL'b˘x#t V:y]G+9y'3RiHDnvU[ J>[ で+Pws9? Bv hU0V w7mqe@v(m,:nU8nS8[fs^ofM-R|ĠeG ӈfi&m3&;T ';yX I2[{gjR gIxtknr puM7r2l^A(X#;TOKy 7 V1 pcj)xVֿ) [>5>w?> xa#۪%Q<.,}!WA^d2mȻhX.et,GCs"#XN#ʪUdcK'X$BzOUP.8"Y9>A@*[.CL]ŁGrRDzi$63þ^w +=| K,{}5,u ?qMeBv heF +N=\!d@v}uou=?5սd*.Ex@v k &تBT&af WƐCtN T3g)ϠD\p=a}0TZ sjٖ?O#e̳4(7u5>Okg3Y䒩_YJ+OC|nӿk7C w׻H6逯8GIp˘yo-qtG5A]#2uፄƫ7D kU9 Z>AD@tb6x4]b9_2"8Xtrw\GG[*2'Lm/z-39|FU)IxP2˯-:Bpazr CsEE V "`͇)*LW NwED*Z`%T/"B8Lv yEW_uzAJV рӤ6mgG@.Q 7z3~hM6t/{qk/N`4+jGS+jfC;MnC*f.)5έ q h(`Rn6. `sHC PB直ozⲵ{B#suU15Ɓ>3 (^$/l,tǫϽ| '[$D}>`}j-@urj{+ v tMl?uiIh7Bru Y3Y:7)OAJ4ԕ&0(naSz" %'!61UV0V~'B+WiM|H$2,i*\ j̓r=`i";N*].K . E|:g}#6\R-Ko{<A؍~; 16)0  qER>UGTԫd\׺Y |XIHg)7E{ i]!pBȊCnPD$EZQ0ڸdP@^n,١] r h /ف20Ys-zV( [>efCOg<eVվI;ﭨ=| pX1'j ;wYRM1\Ba%gs/ EuHeSfr*P&.fa=j Ulwau?Ҿ, c8{\g2\RVR2W8 *SPPTs!߷:j -8n j3U va[A%J FAZҹwkNv&ehܴ_{M5yChpf OwFǦ _nJyaqBp$DǦk:[oXC$9GZr#H ["hG3=Y2ԕ]-d6QPg MG7 .7Rƒ'6^aҨ#;9̏?1eZa)LA[LR9{9ogf{DD rTBv `\h8:_5aIvRn]M ۽tQyO!O١ԺIe&e-ɛ^0wt[&fY3 ҒLpy FmcS]8Ѿ /N)%/OS{2jC@StitW2^ IDAThx|lDvM#xMS[+7+p[}aPBVy_uC0,މ{ 8.G}}*ob##([+36щe#؎qV֟ON-ﮋ95eL]Ebn~"A),z:-MrrxCMw޻ɷ?Drf)ɇ0pe{HKXCbr & "qF&<Ô[F46܊E~|pwy~ӗ.7!E& 3wo>/Dǒ81ݟ{~_~dگށ-mǝ񓿱ā+&2!0 $ʢfo١+Hf//|j,zLm Kv Ѩfjk!;.5%$<6V#y@^kI0McޗrPEE][ 5=r>LR҃qvWpfΤd"Ҳ(tX*Av h`elQ"EQ[Ҹ] rQ偲a?nI0B$0n:֮8Ɲ,g|:1:zZU{;4S+[LK@hhM5Hx5FMh ]3Ydr )QGn[|/y'U;Sps&@}cĺ[(!;5/=Zp+"o HJh[<b;w2n4PxtM#ƈ6/_$\{Ah}n`(f툶YQZ(*.vi+$DwcG*׍W"${hٵ{6Uw@!`HAve9OZa " 4Eg^v !:4à\*7*ƻŃO?dLo)?![SȈՙ ryCKezIGlaXհXs_p2@lМ˒0첫u@}K3Avȭ<6gCaAP7Kvݨ^V/e73EkS¢"6(ZԢ`-K)G q]|0aItI@dmZ$=|s՚Trʒ,V12Paz^ df);l.GUu$3 |(z?-wTDgjm)[/dEЮ|$fj&ǀ G.@rcp['Wҝx5/O} ?>)9Cq(0x^#Py\up>Hx6*+ ^ Ć/as>7}cx9q'!Z}hHZ_F<T Nr5<$. =Nڕ5uuνwV$8t"|&Ӵ_d>֪;dbqܼ!.ZU_O'BW2NO!M@Jz^%'aq;pGp0N'O"RsKSwZ^iѭ ;H$plS r@$tPJnK@d94yh/H\‹uBth-Drvr.w-M/0uiy-bd؍y[SuKV7޾PRzD kW zv^ZB 4(4`y+Bvp\ݡdVp.q&n̷@4vTW^^3 ˶/Nʦaރn΃yڋ̑L>R6חe ;PyEG&@5|3db͊M"`ӇvyL- )Zu"s lzLvz8ݏ?hbyD:o{ 㙯kOͥ04ϸV! ڤPLE!+xnggvj-n}0b[q8Ɓ9keI*g~DFssz$ @ҭoS;Pȶʃ-C!^C"!: z|m(?2^@O-勌Y# i`@'й~cb^rA08Gc7YK,\Gɀ@@@@PM";8Dvsc%ݜW޵Fl<}vY,IҊ@quZͤx>q^uC7j _Aa}~"-#(^~/dVHʮFjpTuE; 5GCR5D3ijP5-p\6TN 7?謣}qi"( p鵍/&oQyh}hx8iCFR(0ze*ٜϭ1Mm`-n; ݆B/3<u͙[Ɓ>k(=$ q񳋵JR1Y;+W#UPYNx'.7>_~}WxM Exdv~;c_5]Ptt%ۧ!;Jn}ni ۔A[lCw޻#i7}q_ɇ0p8ix͜Lk!M`BKo~niB$ -R%ΝX@NQl*!^yyA4 w}!;l9^ՏY$2TY_?}ܽeܪC>^5zdss9ܰs8fi܃ߟwv`[u>/yLvPbmX/py&}Ã. { e *X^#<ة{lct<+ @/0챮 qCЏ%ML6>~сZY"E Dv(b( Wx='Id[`IQA3|4 #Qh#u[ʻ;+z}LG39^7WH+.! lIS^h׋YuJ"@]^,fd"5+Y:LlXrxp!:o`>ulQ|[pA`ZGVe-%P5,CjH0B4)A7SV4 @;#ϺM |XWf0zNrp}:%=KoG`]JH''hu,Ab/ON/WM0 [#r AzZ~rwUYn8bSm0Qw4"17RmR$8?Dy蚆22 M#EǦ1/Jt <RAEd $g"eї^1+&B״ߪ' 8'j<߇(DKAhpOp0ѝ填Q䢅y+:D RF_9R>ucqSlXcZ+K5t5rA@aO7.&g,U}$3dT]TCuU `ڮ;'3q+,فُʔ?aI A", gH bIŅH:Hg;DDvAӀ%ﱔHzXqpHEա4],K((^$; Za/>D9D3dmk^B?~Ն;wq^PMYJUfJ` ɅE3`YGdbqܼ!.Z:_0q?$`[ M :6 ]]ANpsCDrIJNk֥|o}O!61|3QwAZDݡHùV5lA~@;9EuL7'B d  kÓ0I*By:bEAz*'Zݸ{庥

# EP}0n E8ӺL& ,C}G\;xOjp<>7]GB$<2AXܵ16wT݁(<ȹM{'B%<['pjbrc\ֽ⼲C@Z{rG< 0:@}Z};H֕"Қb.mzV{]H3o%L%0}qT+d0 iں " y0^ŭ8/aq7k찺0t\Y\Fσcd.A&n؅-u&;'=$՝ 6D4$SF4hǮ2 ]k&;L(-<^zrYʬ) eQ g~:s7_~/(H46Xm{:5၌- zPgVšFKMLC|n?h*a=aq-cHW&(rSՁF_9/WSB`e$fM߱M4 ÀmMHi shA,HYmv(oh M \pc>]݅P@.E T[⻳Mq;9QcPL!v-M|>s'Oy@|񅱭ig_DOC;" T&^]y1d22~ ,Cȅ'<=4۲ҭ)y\=;%r.\7Iv9NGF*;,DY,E;l:uU2/-5PBD4kN1 dpP١x<.q`0!II缙JC>eoUt@03d A U = kd lYIYF^ӰA,Mc(btcYu&=y(N=]R٣d#Ҳ(='. A %Q/[vvڱNw:lvg6Vͤ*IlUlouJz3cw3N#eٲe[JDY^HZ .s ǽ$OJ pι{ι|{[< hPUnQAY;̟/XV.$1QDYE63.cF *4aiސ!1 ki8B䥓Ug^m?ؕ^X+ 1/xXA: Q8 x9p,qa(`9q ̺@yb>cv _p0܅_96К1qLfĒi{pv(vlA,:Ҳ |<Ø/k fÌ(B5m4# 47{~Lf%HڒXLj'۷| hICaɌ#ъ-ei<RbV1,KyK(f$eiEuMe(d) 79G;Xu~Boa}ÝpN )X߿vwOD%FEsp2Kfa T  m/ ;aH-#/c8w|mPw yc3BQRkv9 o#7vkLhN( x8lQV?݇Go[ 5]ȼAX۴o鮘F4PuiVuqA'>sׁ7tl bZĭyF+?79`k>}g,%+!14fkH'Á"BBUu={7Pϧ<]h4:DzAyy9: +"v0Ĥ]$إ)mV 00^(/ߺaL8nzM$L׋is+;cR [Wxya?{YGDW8;*{]Ww+iUIlͺ=,m~Uje_a9Y(|bsC|3 $X@za,*vs3!SB}ܺBxIj7O{ȿ6hU]SD,lRlFC:=$"C!j0''mH2@C|,xp/oÿ_6)jF67kLYCЈ.HON#OM};؋XJTdv`>>pbbbߕ '7Ͳسl:%+!~edo3#1|gi͏?"Pr*qϚv*a,[h8<'M׊Krt~NV. E}(2K@ޅ 4a"9H .;Ѱstlj+XŪkݡmKOUbP4 E{`L&nt9뫵D\ի 8;Ŝ|wXtjKZ_'Y6lhXzSN*H@m~i@5Yp0`zW3 ױ nEch8RP(۔! T>0;ATh{_ `^Km:BXil >i_?D^ bȘ3LILf)BΉ`rBVtkya +[L`ܬCSY{d}7a5p~q7\?\:' 0TUNe-KٌTB){5)9I5UAC)׃ `,g]zΊ(>BP-vX?!b& $"|,gEqnaod @0^2bd惎۷c+"vEppw7ּ x.B=+Kn,CaB :KVY쐙Jb~#b=G~D@&/^>ֹËGI45ueY,Nn5t_Oo}T FSI\OkCo?z}Ae7CwiS~?BX{'"۷6|?jtW]+ڸ;v^|E)qw MbT7)xpa@>~ %bE:~=uѲ+PZ|\ݲ#Q}r*m_mb󥟰 U̯/IliU "Y v/@VUݙpqq>],JNc D†*,W -xjUhqK  oCb{bM4Ʀ4E%Cy!8E;>h|& T[RPlᄏt; @` |,#{YϿ66H_]9#sH%紙MQ0)%a؎ 6ŮHCz.y-7%^EC Se9hry|>=U"Md Gjs Ʃ^EIBr,VWu1+"toۄWʋGpӗ{H*ek~W;B=]@ys;_x~Vx;}gDpS'!(v!1< ][=K%+3/ܳoT]V]xg?"s~9q~IeL]ƥWޱUr s )UAVSfHlFhF "jǀj@bd"##ˋp|x}o_ANU] A.Ew26Zݻ~ZbH<{ b8gmգ$B2ճߘF p8^<1[\UOO^@4fh mXءdV‰k !]&A 29q}m;Ջkqnކ霊3_+Sgby4ȢW^2@T$bUk@"c"pb'bN1)ÉiөttC0P+ տ(O/~DQ v[׮˃w-=5qc^ ~pFX@ ]ҖzB&N+lW^ZۮY:δF!*Tx5~ruo/;|7pX:+P(=4eQXpN^ṩ W/#`@Ybw17 b+D)1ϡEߏ[@XXCo(dzbvX/-Wc1]" xC!iUY& Vwb|úKϡx'~"v0aѹ:lGǞx"vXCǿa'.vjC]x? bBhKUPh IDATU1htsCS\&gXt{+0hu7b:Lao.ΜDL&Ųҍ';v4b9C[)K:N|m "a9_WiLN۲6EV'NAZõl$7@hJ;xo>z;`^`Á-!x7u f}U상 m/ vVJP.O&R ED%uX^t R9!C!b~Py@W5JbAe‚.F`|x=A_sK@[1gqh#)'.w%.]rW!}:$.L$wAK;/6Xa,,C),^?C$1_wXHSiMnMg1:AZR)RMyCU)^x4 8۱/Ԏ0 wt;I<۷0@DZ$NU-k5Zhl[\[ &.Ҩ@9hN]_Ňބb`!BV@X{qy8-(: }EFfBg<;7MQH# *v:d,.Ɵ#w"B^7BG [Nr38f/D<[_~;%T8>WKg.kdOwZiE#(@`Y u@[u  )TӓO!N!+?/8uo=ʝAA@?Tb9uo7nñ򮟽Szom[(pyHYIlj9s 3<&EMOXγ?xx=DC˂8h\NA5tN[8c9u*Ro+\b*J+q3WnFeRpx:1f˺ x~Jyp1lmPmF0GOϕH s|{x8N j 7 w h*1 ֠ ^7;~vY,ps͡k=;XWddyr듙ڈ_@[#Ĺ;ƽ6ȾlLWjh`ym2v.#jNFzҜAkͪ6s83@'(Ba*Դ}uoܬi!Np:CbYg]ޝfgVݱ)mTN_(^wte!L;^VԵk.r#t+1\A,@SCAp4Smf]!Je[n` lZꛝoXO8YscʆJ'Hd߫y,!8?n*9(3J0BWl1#vXTJ8VFPW2FN =3/[sˢ:N&rPKM'4"v\}!)f[W8.U[:ܦ@B~0;n*öV3'ڝ$H2PyƜ88(jH8T{6t8D0P ja%gCb, "Bvd8?bϼd;|=d T@^TOa0 SmFÃ{zk1ai^?ڽB]PO.'1¤(Z, '񳾏ųHJ:-5#ܹk#w>x= n]g9x鴩 A݁@ %9f+І-%XByFlR#Ёl1Y\-18F` ւ@A$)^VZU+~Z^0NA ˥Hf7ǚJ>K0\taI,P *2Ǖ-[8s'㱓o`k\/C%Ǘ@欬,-WF=V|D@ ԖTտ;K9qv~?<9pag^Bv:i+o;Hz[zkZ p]:G M\*QA7IHYiI 'ԭBo}BwuX E{(^yk 6W̽H|C-"8G&pД( Bu18ǂ k.[_Z T  ;tp^ -W~f(7 rXAY*/N*SrpX;@MQeӹ6 ,cخa׃6Py_\:]u-EB1E_so\}0vJ'N pa5'>665a~eZ9<.&Sq$[_ɜ wМh8t  ^4@^Q1bbmо\HOCU|79bgυnO(2Ry8yKy >\n)a}.l^ w@Ž\C91_Xw*|Xy$YWׁv}Q»| _gICc"&ůĔ f09$GG4ʝ-Zo,qa`Znɋ :]h~UB{7 vWȡ "u7:<Yw2Ԕ Y TY!]ی:;"ܿ ;On9 š_iݏ/;rP U`ꈣ0ui}v~FY Y%Mfxb,=Z^)AA@`?Fa #(2Rr }B9{C7;:c5g<u~z=hٱW|۶g_ſg._鏱S7UH3ၢku9sV>. @hFrS mX8>b]x;H)L*ZqELveKLV/=+U)NYʔVkcs<7uLno}I"ĻAE]/t@/bl !-.WygX|aԝXζMLNB1<yů:Gό౓q|?~\y +cĬ#00OcdFDOZҰ, vp) pdsdECNNӀ[`q3 V(TyҎ;g0 thE5@S`x@eu1mw4EA/wchkho3~+<}g kדf((r (뛑$$6ITY;|r,.^MዴdvaH "3\:T+v}b%TH!1<Ȼ|g,|m݀2Q7FH/<p>;^8ԽuQu nan<߇_wF߇Pgcn \6vl9_[3/B}mhr)J -MXwgp"w!.Ҋ~l5,o!CAZIW1M}x/áWhz(+G$Una!m|~~\\̅znJ"xs/O`m0k2\ ~ ;4.ֺ~M3 d4Ff$Fffj"vHHYYwp&66_qyCPچvLpsFbSv7c1:>ů0rnk&e``#l, Gx&4S~&iyÉqDg2aKP_b00RLi9d*4Ͱm")=G3[_]|-lm{Fn!9ي!:4p8B#~e+\li⪍&< M6( 5 %X=-M#:m852 y`]DE"N ]Zb13z?i<eahCϡD@(T09qv~k6К~C&QҫC/bJ]n/9B%P 9Uk nxb, B`ɴЈx˼XV4 wn;8(Tr/db>>3Qv,h}7uoqHALJx:fۑ"8+'NcVlD[ƺhZ/Ψr_mΒ`(;<Ɉɢy;Z7NDiF;Pwߩ y-qG7cc_ ,UAU=U9Eݨ @.W|ׁXV47^(^ %<Azr/ygF/{C M7wZN&:UÅXC4zZ}=P]6VZP#f\ܦ) 4I.އxZF:@7 p,GMQ+#v03gu"H./ԎiC0 :pZPŇA}"CώFaf4@̓b\Ly ]rb./ 6Kb,d} jй<"@Xy ?i7 `9AbxnUѾ8hBznңWكD-V0.e9@dU9MEVQT ˅˾EjPpϯOhLDE8RY4(0j`4 _p2^ttPEn"qo؋#̈5V._"ܳm[ ھ:; [" : IDAT9KUMwOs@z=HD"D"I3tL9ys|`zNS!kr>ҋ,<ÐLb]է/6cW5);؂Dw#}Qvw̻=:pۓ"g/{^\<|NO!xB5g~%{[q*qںF  CWpx3"|< +jT.GWkrm͈BB- :\ٓnnEStc1 \q2F^+2_x5vL?+ f3\=dk3Gžx -צ!tw(!E"dV*O&[؃}|wmǜBbo]{w·U7QHڬt=aإ&SBD 8,v]Gub"e뺁TRA6!`A:Z;B<^+ s|*ÕTO=V Y>7YEt}a\px@1k$I'Bّ& ЕXs1ݾX?`Ty[f!SPQj {'CQW1V73$a(Ic/'"ai+""TCcPҊl뽿Ԭ)u+0+v$x=7/<؄&M2ˮ@VUeKp}f R9ٲ<4EAO.,^rޛxٚ_s3l7܎.j񵽷o\U~ /p09| ,#۷BymՈEC ]3 t:>?6b\>مW N zG;L /~ANҐt8p{¾0hC@q6% CyGXG@ vwo|B(zI c] A^vq.Cx 8T<m-0&D$pqC/>FA$|o0_ v?n~a@9jy#-'CD a@ xBзfڦ^y #'KHã5}Ez*@ޝQ.I=7QCkh4D"}ceYU^㹟)\&Vp<o$,+wl܌:ÐƯ#<NURp+B[ .{]w׭#h`u~5'V]"q}qx M<5]띻ͻ;ynR-Js:/AXWyW"9 Kc Sd1m!}Y*V7 %7bkeI UIȨ v/p7ce/}jYa#)vO (- JS$yeX9dfs<z[';@\[p0Oo㊍y1Yķ.On7wo\ aih2/|y!AQjpY ~_jy Z:=̒=<}7 )gO7˝ GE`X  Jb(‹6(!.uBm!"`Ð,e?nXwmTy̠E_|m]C@$I7DP4cѺa_!mo tBMiHr4dDi<o픙Ja23Cz|zEyc-j]]; GӤه&OQ=Vl4+oƭ؁)n+L<=:"N/v}QTXlކ{zi=DPǴ{|?߇Wz:tW uvn7+0C$ƬǭpLnsmbĺ؁v@@ 4Z&9nOe8%DDDkDe@I^119 ;Lg;ρbꡖ<+e`@5Av6d Q֖ۮX1\v)f|Cus8t-u-E~(\/;E]v(DFUły +\ϵOvލCM(p^4uv<> ؎@?`l( e!DH罌PvhmPs<Bpsj#w>`ֿ㩟 #i&>f\?4ŁZqx ɌTQ` P|{p]w;< * S2Y2֠;U@n4l,0:3c'ѿ΃p6|Qi< yL감V(Yࡑ`ySZ;y,Z5}}9.?|IϫMxXJ$A 釖TH욳eA{`i@"K7IlbF"pwvW) &DE4*Ӯj@T} ]^PR(V!f]3m#* &ciP;X%À+a )7Bv{;>۹!_ɳQ|k^<~VvpHdD<`)BSQ,\ܧi“]@MXFY9 _0w{%M4q&&r+9;oPR/-Z,j4q2 sa%arhm\ VJ`0to7 7G& P{C5Y;_$Ѳ2Mn%QȮ>-̽_#kfD<1 ;ki<MsmjpvyڛH^=0>hnx'7m8@,0=m@dG!U@DDTybà0l; } ]\4BC﹫ u|}d΃!QN[ c .pC>|72jC y<,4mh_;䆒8xJK`"9YAf`dEܦjb%0w}`+/@z`"WoöfV)\e &1,MA`XH 0/qu rd!㣸<6l mY GH?^C p<>{?>{?N^븘Jz <ĦijmP=Rlv^'wPLvT> e{DA+'9>;Հ9_۷Iwmsvݴ-p ͘l*i # Pss8߆m블`OKKy[@St{%@7}'Y[,n3fΪ$Όs5Tna(p, ݩ '\ ġI\WҾwR+/nxh;>7,c $"YxFkY6Q,s'i?OliSsa>cycy" >io/Wf+I Y6&tr]N!I8*ٙOlޅ3)eoEC7N7=U/(ݴ74`Kj}sm "tONDDD%vwz&7Dp붻 \[l#0taPd͑Z,!]WN@W,_ߛ'I%D~j9a l=kqFtY82(2MdFd+r|`zϿղe)u`jX[UxE,0'-P`vbrX ED m؄}um"@ha03ׯ]™(:JwݝX]]󈈈V-OASz~>;E myPhBBc) b؁(taǭi?<1uw{&qtMjhA IDAT2 ҬG@[p4 b%9n3 ’Bj^5_NPbv5+ip D9%J% pPU@`;8&2y| ;@4hxliia6J! eȱZ4 d@i.D^js^ox|V`>SCXD<:“v1 f Ipff}9o$Ҧ}m Z"Qc^}A1wШ YOL _;C*D7ׇ`h+ -0ql"ȐYxPd4P5W(2i f&py|r/ q߆M艶,~ G!ֶz-{ >ob**2fĦ0m1Mabž(eӏ^uBq"DDDVZ9w@}23vMQrQ^~IM !=%v.mDA<(<jJS \v~u@ْ$0ݲZG[;̍ eIdeo  νO&5h_eD<RN"B0"gڌPsc ͉nN,NCQ^?/duoϝ;X齿nCU.oBY˸<âU6yp僭p~>㓠5lۆi7E[xͯ;̖jUȢ'< 8|AFq}OyMO]^ ? 4;\yrۥL7<iv~8 V;@Nf.(s%5lmyMa'%ySwwG}.We44 t #A(tp_ݱ l#n u w[@/Ͳ#!h|pğ5ps;63q!ZR GRLkv>{e0H|Hz?"[hP!&AW"#̦(½a:a݃v0D'{x \+S @TjsAIE7tNC .nE ՅhIK߂Qn=hv ;A~: z> [:sVP:6,DF3$/@Ֆ HLȰfեnZe EGEBHS vw( =.x#?/k|`3]8@EO M~/\-Ih g\Зgwն:L<Sc;]$Dk4)&jmi(2Mȹ YjjxkoCg^kQa ;re2f^$^svT F,{FW=:X\'ڤ LYxptQeUexXu9:g6=û7<@g!F!^mW8 /'hDZ:rSD@- MaP<d$q hT`s4U^pq@bQG ]K8 s[ŏ7𙺸=~/;v#~ HvL}ĵDג9EFDģ;9v`ݰl%4EE`)=i>s]}v}xk6~>zS}Z_a݁In_mvg(ފiĶ @@+.(+`l|HB΅hAǾ;PB j9߯4AX1Rb$ )}"?B~d!77u:>gd!ZG'Va;@jq;PVm&""g\Db w[.K%o(>4O/IdTњ胥{y~ɀ \u8PAԑ֕c_Sۤy 3bvyU݋X/,C;''7v%wf) 6Fs";$.zES7!t=.e2ĩIiں͋Sq v͘KnNP4E!&Ǚw^N *qcI6pOsoB 'ơn0+\Wy6Yv|!:DZF"-Cě%ģr,lJ&_ѽcU,DVaý_qwYx Xِ74 1k`z*|a)e'A2PG0<srd1ŨʦbH>^a)uBOPnQQBɌJԐց]Q5 fN U,`0FXXH*lRʼn4;؂Hh@PDDDGZwbq(]f1\<vԐt~dy<1LpMP3^Tnhj^KB,shnBN%şE6Xvz`Y@tQOءw ;>۾yr{ r,0A0\ىaT9-&`bTϥfLذr_fTP}tJBxq'ql&ϣh/pt?H㱁xw|h6v-``Ģ;Dk9^ǐe+50=Z,4ߒwl_uʛ}xOa* T JTN;\E06y< ʰLE˭ԍAyQЅp>#'q> !nM]QPLg!05mU[f$S/"U;6IaI*rR{65MdF"Ag_z t{~uViEۣ(gJ&H X9kgvLLx B"0ߞ5 tP<ìs9+ l bkǎvD=1p )((RDN!R"&!ֻ+zu:I#""ZUJQjf,(:.& MbvpĄ-]B+/?nW̼p|ΦCbY|.9to~td(> [ːpuH=4=>z7(u)9&sguZ)LPs#v SQ PIjw׫q>>8>Krpe>v{ |2QE-eaK!q} O}|u,nBi Қp#!")➮VSe/(Ddȫ*r$Ph5˶ <2齿n=PX;8qwgLaXPmb39X"`hO*86TtP 2LHBMn]R*Bڇҭ_@Fxb@t7n2Q5]jqr2̯@.[%ÿm3V` k+Tł Ѡ) 6ZvK]/wEsW1j83앫#`U5W0W24Z#[ܲ!f>m3.kvҧUE ($o~M_)4<Ͳu\!񡀣6#!Dz:7e>uqIf-Gfp6҈\I\_ԞQ pFn$ HH*єByCl]YE&%DʀC-.k Q54(P+*0V*0 M IDAT#"""j`)>+b[avSءWmwU;Rpv L]̹'P&'piU4/|8J{9۹!WMlRb+ %q~i<◃8/pq([Cع{ӃvPvxPp2_B1ĭlqAUŚ[r&_gCz P;Ix~en/ ARs(^E 8u"$Yַte!Hy-Dq׷ܜ HGA_ ȩ[IX[Ô ϙՏ4/ދ:>xG:}n@|bϞ9@ 6tZnM0<MNngNyw%+b}VH߳}k@h#sg5dxaIȎ$}{E|O |69 z_=97lƳH_t(躚-ӄ)4$K= T#4q2/JH$x@CZ)tyh5;@G(l e, ̋r "r8rǮߧJHMBt%"3v(`DDDҷ`<,Gb[9;̟(xg?-!˨mc;?F]Eܫ%Xi0- s`Xj ܍z]qhO ڮ(!Pя=`%uݺK7 A`0@́rK0Y,"3e)0=l"`PS75uh<]BѬFR+ Cۂ7݁~җCa7 Pty 8[$ě% i ]$&d鮟9+ ;Ԣ& xX~1\p|ΦߣųxGS\ee9|_8/v W\q0΁V."""2on\KXP7θDXoӁ~@+%tA"ߕIZc'25SwxWPd>"p2< ZQe8Y'C)R, P'"aS"38PJW~:ݔZsq]#n`SQ@i,p 61V+i{]:pf׿)gOt&w-hi"I`À[7 %U@DDDZni.in T_X \;\Ep6Uݶ`9 G.%#SQU ,>8ڲKWT/㷾5Qw~"|(Cld\AT*V A v-J0:PJԷ^!P;,Zl@#Cqt4l9KY| 7\|r#7D"!3"Ѵ4M@Қ öfiրIe'L\|TG0PP2!Ł Bӵ0@b*9"U,*khN. &sʶ ȉ>ؖ@ bK9Gkvibu8q^ q8558pqK_nRs }6&P2tfFe{m,ECd(Su@g1xhxO<ضC]u|88=mnv_Kab\<,=PM;j &\ȕ&eYZa6% p1dys';q4yoo`9vvoggHo/BL f:o(r r$ @4Ļ>xLB@d1aY9FHKqo>4;o/wVx%v7!ocYIhu`"}}jPek|SvbqJ]PP%(g6ޏ':[lqtYIk_㱋q}sa lO}wa4e!HyѲCH<59+ %]cYꈡ_rȦNoezI!+p^W}2v>{l" 3w}Dٺ <U^EtoB\== lߜb$Ou4 Ա"CR+qu3uw! gtl; 븦6CkXv~h9iB[TD,"@tzXG;Jul%a)}R͸teeW&N"3\5.K~|(DDYcPkzuއo~uݴcnh :S'C-V}ooWo|C;Q$hӶ@DQzqTY;.QQQBTmbHʫ͆ ) f^HS i@]\c,KA Va׮R'G.}aO8\{6΁r.K%tjj|hy#"%9px~-K(+4^m!S xX(;%@O^Ͽ>`#r`iep=](ږWm+s#VE 0o?Eض ݭ2r( =C U-$`a_I|5 ~4ˠcvnB͖gE~Ui(04A/MsM J_<} ޯ[Y9 T PG29*J\#oT]b$@DhKm4`6L*L4DZ|#04xGF TCxp C:LƧA1tti"" G0.׫+OO-"JК-\L%ЗJ eS-x?-6͠94/2 )ﶷOn<8p0R-m`HCu_V QGA9 2/{`_r^;0;Y0&gzݏ4o P4uWM8za]iICƵ۷1V(`Ϧ`?-+eءb{ *CM-^JŪϻ0a(x! AKôHHe<ֈ DY|↹O 4Owl7Cq^-Je艓o"y Py/`. un(9!HDDDF`kn @A/=8;[[LY-g{G`LEDӺ9liF0c4Uh) _g$N;ypoOH$"j@pCz'؁oc]?4 ٰ}g݃;}@x5AR|"Wdɖ,ZfTLqudfS8TJSR;M?fk*ON93lRcyl9LZYR!d@7u>Fono9s9oթu\mVav=5 @hb+WP* FID\ -+8W. #K[˨tV!k CB_$IBWš.+Tmph ư-3 zk&khY{ӧE% m(u/:>{ysmXڷ AHH!㊑83W9o6=vŠ`pw; A*NS9qϰ>.SK`mP/Q;@ ז_w+7C۽uDms:[BݵZBBBbhuI~+)zrעFcG׸geuw[qb޲PH]|-Sd[y8<{ݩ WI|381=@e3DϾt:<'/|Kr3 mG30 ^XV(d<bǬ|b_m gOM)),<-Mn.2/)>0l`\04SQmsyBk T068 Ueb`R޿?L2j| hqݽ?TV?MPdv}z|r"YaYmZ!,쌴(lÃԀ58z+VyI^ҰқO`MX0={-soLNmӲXA ;Te[Ѿ>dI1rOfх|‘ DyKsV޴[!Nްk3K++!MH_ʀm;tw찚 3:{b|mߣswV> C\w.q!&KZfKɵ@6Ή@!!!1nl4I~^򇩥g &}DvH9ŇC=kXlC{[һ@ޠ PpkuG:Ns IDAT_@^Z+1CRҮxq^+)8|`_NTwp?f,,]Q"ua',87Ϗ"sO8ďYk&t>јCly!($$$ƍ,);lmJJ33 pHߒNoKa E;!4w?`|p(p9g8azrكl4[w?W^kH^z aѺaXv8(m (@j~k!vx؆ZajGw]VfPחCNB߸gnJ=.3r,k!UVءr/ $  ~/ZqgKeGvۯ{1z\NMaXbaU何9^TE]+'B]_;6Ndl# ,^ 5}IUz(ެjۯ{aa'X ;H>ש:>\qf>lDžL\|vd o.]q*Lv.W$aI&orkd;b#Zs[ Q^C<||~&] T|M&?{QgBBBbج"rK.mZ;LJW^~kx.5 l=;:zoQWH{Y([6n6 f4L.%_գoc:W,ehCn݆u?yH.uU KVh jY9fMh0EC!mC;Cɓ0fgKC_tCa;#LLVU}xd;lk poS\8Z"P~Hm;Zv4lf[z,ZX%Tn2Nb RCHU CeQ,0T$je؍aECAu8?T0z34,a e' 9ՙShk)<{?x17\#usjpAZs\ 6#\LgD ( ˙nH}.w^=sV ._%VT ǮecWtN⍌* =D^:q.׶˨]W)ܳ,R<tb|%~rY@JC-C$IG5k.vg NF2nO<_{G˕ՠ /x/'㽤_;fi, ^*ɶ_x̄^j*qIMHMG4vNP vwÃï f`r &98V+7?k'm(O4e% ).Akm+ RN($$$ƍ"I zJ׍rG;<<3 (ٵp ZAQeBu t-'mb(*Le[?;=MWmGKwQz _.C\lH^iNEe \$/4ܵa-*\"B F[JIlnzaFCt}Biom3}o$}6v|dC`UeI")D?0' TuSDDT) Y 0G9HRŽya>u'ȏ5b  Զ~N6 qdq =;a|ۣ1Mי=&Db $1GO^~oi/߼ƋhJ ȡ$+ǦcpD7@M>WNu+<+- ?m%wx߾ +]kO.ۮRzǿU>;_o 1-LnCL窮Fhx 5BVodPJg12 ^ُ7 v +W]ϵmC9ousE!) cpChjIB+ǭZ0s˃ڝ0 ܠ+/", EB䚸8,TT8s0~8BB3ꕋ/v~;?޵s,.y@iO\f2b" TIq^qmb֠̃ 璷-\:aӄXyDd$xj' V/$ϴm 'эnSR89u2QJ)LJȲD;jo{+P'rVN;]eBڌSGD< ӭ)e\^2teIX"xbO7ƦIJ5g:uҜ9<$0ٶ3ͺ>Lü0 'z*=߆<5n;.ׇs)l r.MC#U?oN3tNtv305+|to^=SGެXJ33?7O<̝xs@RZ0JHHH[\?(odͳ׫cGDkTS{;Op'Pv˹;lx= @a>;:;(|a\CyFXQ1{2,wWRz4`M?@kOD;}m8pM^2ԉNЂ.U%" aY\t6mj (sE9{yc:bS0UC2EFQ$B|Adݞ-̵T{)cm7vOP(WC0xŹü>9콖nȍ3}fTO?vA  ;Hag_^ 0JΪa-py8?z0=z#"wo~K2چgf6g# ĸ!>ҏ[+ͥ+r&j|h3Or_gM/^\; Xq Hһtgj!wC!gC,gmwQ~_âAoR? Z]KA"Eh0]ϖ {?_8(r\LD0]6=Do,"ˢrjT:>ʫW:>ڴm bk/z.QBB[@A[o௼Pa jqw;خlA_Qqc yn&@wӬvW)@?iajyq,efj=Ύ];S'qV8ƫFl " x5=E=Ύ}x P^<˧$Hyˢl; VUaM]\We;BJWrw ʕׇyeÎDYJ2Gwt.rk|Y P+wrg 0S\xc]\fsU8=Nvpn{M`JO.o (ryZf|PuUPT MC-zoǁ;z;|eozu\:J i`>[[ CD qCi?܏ Mi><' x,˧}Ͱ_UoOlEw/w_E7DHu@z{bZTo;vvY4v:/Ҷ]8oiR+U;;ws{PC:m;zi2uYLgE^cM y{857UUZATJJRВ(t/,Ǚ?#* ) dI&ZS߽~%Rw4j8`[Xc 7Ml ̛e2FC]=z=}I/7|\M8Cȡ|!Mz^̠Am/ zބ́hPc^5xPXFa׭ -v+ PY^,̫Än出\n{`PؾOQ靟Oshu/hcBatle$3HfxXggGjn:Y nq]#kt18DhC.Ʋ4vpx0]"QTi,;Ql56=(:VjDzv> ;ViӃ~5ɫnͺ?ڻ#4ש1TieX{cyzavJ۶\9qgѸJ4 -dxu ޽DA)ciX$ qC(حzwc@ѱǒ`AC`/}7;p߁)@%~,%XeZPl& ad4:x|{‚]=t;U(,}9䅝~(8q~vƻy]|e~ $u<kx`sxx)zuf%WeoOϝ޿::|;h`1c?C>`%y],,GXuxX 5va>(`  hQLg(ئ%*Dh]tks1P k:ɪB#.o_ÿ!Q ZxvXc AL.1Q*̼m\JMɲ2P~m1YࠊY7~xZKxv~qpj*g..`DGۃfqw3i`CaEegl?SPSènhg.4=+U ş ùN^ؓ6ͱdW,0Q(2Q,H·$@V*a;2-,g8:,hS&U*ƜGKi[20Oz5%5]Z=\(WJ\ AJcLrc>9ubhG? W?ctdz$)ѓ>{2V٥SkJ>6k@7+} w9]\4Ʀ=Rv2 w?$!$kv뇺}Cڼc:hӐhe*%᳟9:{gX1.(*K/7 n[q"PPH%ms s#aC_4D229,F6'*Chƥ{6;u]v>,~"7WUUQZ{kxMyfaWe}L&K_7)ЭGHmL!o[d}Xg+ QNtؗՏA޶#U*HOMDHF;Խlf%2x-ik8;qF`cCRch2Ӄz=Cq724Z~|m?]f}eUY@VNl/yn$pH(5šo`޼e* 2sv\\J((0V(3 yj/w9F ؎Zb|T^g+:grkoe\͍q.;…؆p8?uS^̹=<ɓ:n 3q7'5Y3L+BH7J*kk{v+;]tVs"% Z1mrϵPQ!!!!1nl9^bع.gphB7z3u࡜^u)_SC]_?pdƒg(bՓ"w jmo 6v(}G?=~l$?PRLg8m(&Z]o F޾ȝ7RCUP7D4Pzƌ]w` ڌ*둎N=O y}UcObM5Kͫ~*EAN)C'{D m@Vvwh &V)c+g64xA8pm !*W,Priler/?ͮUk2`xZ=m@DXG0JofgyQ^I_ 4ۼ95Fvݼ]Z#um T~v}$yP$B25e=++EUgg#3iUY&k$":q]kQ\`躼t C1 I,YF#ʭi&F`YS?bn~(peed[/'z;&cBnQpRx5uWS79q3{8ɝA*uIEzavj 9PXFu3+qεo! ˵M9 F3ޚ8&֮*ͽv:Xw`gN@lOgxqw* #Idc󖭇C(ki7M4ݡo.~YX7\܃q эō>—'mMW74fD~t׶1 %lE5_?rg+6;w5de!!(0JEJ{"턗ri*`6U_W*M=thPU.T̥a’c3miC;ik&Xx~h.XkB ہ1zO4eJt"e4[ y~8ϝ : UutU]Q&S ":C5~u+%kLO<< =zy!uue$S2 a9KҺ ׍Ĝy) *e踢s*SrcZg`z?Oğu!^h$y^x`tt;MB {b_sD'>gv/~G٩oN cG¢…6xy+i ;p mM]J:x0|7|3I  D}_07eҾe`K wBȘeK }@awp-#uH{Dwҽ8LC,8楃$h8"sD\!fCaUyT:;˪am/@FtB(Dj|~3]%Xv,y@/#s؎tFK}܀jHc*XI=ƙ TsDGfj\^yp.$I8_"="S.9u3bVԃb v46oYG5$e%DW^(nnji8mZW|%Xm}J{izAs+:F<orܻr=CoO=߇_0HlolЪ,ئj-sku͒ab:K8F&O9WXttC4=2I]Ni2+*dFP7T*#KD ˴(0f̺c7|kN;I;ITEe zvzHT:pO~o]+=DsyCNFjq@*C$MENJA^cEOL|28<\gΘf>'/[^e}Yڵe)q!%B́} ۟wn}?fM?@kޘaFVzM̩ށ_:Ϗc{ ;xa͗-eh*2(BWrAKVD{D')\wudY:+OaήDw'|>azĂ3coQ[:c⹾C1*D8;r#W=<Q>h۪^|0Ju~ǁ+2쉷;P!WvS5 vq^/f=!!!!_І3-guEav`)L#]M.9J 35[HK~744 +@N.w(.jA vx+ZUV~ޭ+C_46qGfJ'T9:p/2=2**dt]Gi8SNPJR7VtU(X&eF(*h, SChEVsYHh%uӧ˼5t/=8;:g> A) ) UDЂ:s0_3޿߸>ϵ)ݿ޹PAQBB-,;vm˸;l4!XG\*vЂCw`Zg(oA;rj7S?T\Š>Q_ c玙6cOnڐCs D)s9a'IAwvhd0V B8D _ 0@~Tqr% ko]%AOUF2DTTGbr4<9 ' Ɔ+0RVFD&Zxc.~t[ >EUJC`]bm\I;8{Z}ynءzh6 W0#8,ځPΏ 9wwh "Q{^J/%'_7}Lt԰k޶H, d. ((r6,;x+i` yq-;LH97=mcw\R ]7zCkߎj[7,߁_uK=|}5vh8d s:~HDCU,*PD#Joa9 U` Yh==mf"g40Y^A@W]6+U\L]u|xMtOO]VlcI~zn6*]~pM)L(o{DghrMܩ%pwY=Ʃq[vHh!jhmo B}Ig5R@s1,U^xX>P7mՖWa%<אw2e *gi~MH]!`8]c3S8;vsGxa| bΦV['c`,$"iJ"A Kj@JTC[]"`+Idrw"}cu/S,s`?wto˔pg(Q"zOȗRԅd29nHU+AX ֓Nlk͓/E5>$O?_wanePPAZDv~wS;f:DApء`Gcۖn@E6vhriD:}yRxTVOAӵ`媩{ #`ګ yπfEwcpL$ԵU.DcN c9,/|Ϗ'?j-M;,zdJfV~W $!Q}vvVK"a:]d3Clo5ϪDU \ @iyҸqs:+KM[r~3ljљD!?|V.^6J,F6 Т.c%&"f*PupjBj%\pO~nkcZF)N =S c,  foa|Zq˥T{nSs1?H$c DX}ۦ=WZTe jV^O!Xht05&=ιnSDM,]NJ+|C~#s% IDATs? '8szςχDɄJ}WxxX[1}a1'kuthDUs 4R`be@4hw<_y}~q^|Sҽ!Wd0 )/73س=Ȭvpg;'6.Pmj;SuCZ5q[ 6殃kn{f׎ JLնJ;9zvc`Z7?zxV$SguOW~uqw]d]W|!`֓cZSl@P3HЗ>ͧ^mÙkHx3yl _Et"5T@)7?jVAlg_,,D/ӢT6qV,ɄUuP3%`o<la8VMy(3c1Ohkp'ɿnƧ\6zkuףZO&qwKCТetR~O-}w "aU2y"w|a/3\!`d"־B\Vf;V+Q,%[TB2Zҿ~ %4pJZ3 a˰m6kq@\bey{T$?#ug+]Wo Ӛ~SIv~wS;*_;('^qwheY+r̙DO1jjYO\>NƬ^ HHVq\緟3  >@Q$MI(GĒ%g7QnRQU+XٺIuFIU{++ެ]ߊHX$,S2A=  0x 0gO?`fӃo @OsNstsL pJB uOm^_,gXd[10AL嬕T;G, y#R^#BQu DC4e`0@~uo\>R_<',Ca(*tـS7AQ5km^lk`x7X㇮dvyh`& QDTMwu@Wg};h  |RNo^`J;أvwm=_NMfD޿w&C~LRIjݡR?ָ½ݶ򐜞!C JL"-WRPdX Wnݹw?n99o`y$R3ۈ\s4=J}B;OMƷ=TI+pCqLpcfj%СwM9{Kx "å.iEF^Ucwfif 4iD MQ 2ɪ Y[ʣTf[AQjk y*(Fzòs<_!HmE <~p 5rё4BwK0"Qe|ܙ.#x;&Ḱ 5CŴ1qրHx]rV`9͠燦U MC3iITO8R4 =iMSUAo 6V0P6 a^ 0B0Z_g"fzRǹ|ú•ldOlLOxr*C (2/~Sjhq`ۃʼnÃM:N#7F'Wc4iɭ!"]=0[o;a3 \`8(rTM}1i:De6UW*((( Ce= cu55W3Q'-g8 ϼM`R [;>Oaצs4Ѝ쬥do8*Fh}%eء3 @%Xذ}=`r;A0uYnCOA ڿeO]ذ o=[ip& "^,8J`(==|SĜ4Sq#ͦj=GoD]"nr6"Ǒ◮0x濭* MӇ1$[j(CO܍,6F<^0ҘIޙG6o|Bnxp@dKxR,JOrxfս˲JD.qzFwuݶqr}hUPdҘAǡC#B?}qxgjtM1z#DHmk;mҙntwpv0+[Cڴ|UuI oR* {,S% u*dd t}c2|nCIlyEAMS{xxr(R;P`(x>`&x5|oa@Q+֐:#QS\Nk$UnU?2?y}U]S9apRDDw笯xpss>OA`]:1c4@5rnh30/kZaIx7#/;VRղC~yEAߏt$#|s~b8Rb~+7/3w.Z!1~0)B?V77pd O}a V/,kqe0\"av([U%2\̢/G_k2PiB ".`r!([w(Xp`AҵAppEFI|f)! +{&/pt_;:5YN`bU*?i2j'?DD遤 \lVjP}a|>/q8R)hPcցq Z;*?fim;B.Gәa .1~@DTqƪG]M;8\AW\ECU@%6νXf_c~jEFp}| 5Խ^9xXt^塨Z4̸/L=UC2s- U(p:>O"Lt&7E|/qvSPw0d <ӣH||6pwv[q0ͻ( &J*N::T8F5d dI[cp׎TZC|1b:Gujk#D 37,=#, YrjEyTT @h(OV@^%@Wgj @Df+e*n+paN7z~b@fwWsEwuq}o yy woOݜt)u,t%]{bd% Ķt{Pz~'~l_Jeg6S/`xMП|m!;X;(>. Z4STayHMpVOJЛ% sgporK\fvuXA: 88<SYHc&yiܺpMٽ^/\"<V<b#1s|?%]y-"@. * uED2*A>R)-n'_ --֜<ˢU !!8..Xc] ;PN%i y#ZKj2"l{EkՁy`8<:@QaPn_%ME"G`R+ȐmL1\[1@$DxkFۀ:$ {38}ٷzhP2s(݀&u9OY!Vpv06(L|ffr}P<}揷y>"~(i 4MaG؏S"S"ɼ{aDz~T4mZVWqo!4(^hp ,Oxb@W:@tGS`8Wo "kx=ٹ *ҟJXJ wypN> H 5+)@7NCK[UߪCO0g/P@ꮖu5͍A~LhN)Srݡhݡ͂Çw/הwx- 2ZVc:B>`Z@B|Ed|@DT?r1CBd`3ruݡam `9h:ũB hNI1z)qyHUAֹ)lX(]; `) "`5ڙF8/K?ĨhJ@'!)qDγ|8P|Tf`'JءڶEE$&.0 IZam4 "5ta^'pݡ 3V#aP@' HێA<_@p΀4EG)pciuZ P>s^I7qWy9hE"DDG-|7tUG޵h%@Y?G O1"n?PWJqakX/aH (w 3hw MxY{jvw֝vsw{wFMx^ [@Fkߧ=tDy伈4<:$>GP/ŞieN&pƔ+a"D_sksi yETgҢZFE!@:~t df')OW.ל#%|`p; /;vXKJwZnQ7{Nw)3ȴsmD2PKσ$m)]DY4]i$Jdiz 0{׋h?} @V;O/!wj聢Yx: @8lVeBnUld\O/`g3f4y& XYҐ%pPC,C,⃢iOvkէEw}-#}-x A4K}' أ.Prh*A7Ә@mХ4q}(Q=#<ցW_ů=ybpB[Nws F^||G_ ruHM ~V ;勀MTtpس#Z a2T f~gIkv@TX,pč@u= ~ҘZ@,҅q{h#kN@DDK+X3$8\(`BSLZ`NUb4@5`Y_{T\DFR{]%),WU芊lA_I[8YEQ"]<;P0ޚt[WAdz>/ Fat~nv΁ %>\b>-C4MCuӰC߲!1W?r?$"hZ=! IDATha Es^Vjk+N =l =l_z5.DDM4n9, [:&W[ý+2q?ZsdhY{eys.h5gZo;Rj]IC~ZN(NAˍ54xzOEg+`r-N&(Ȣ}<_ !9>EO;XB8fFa8|T8Q3-:hI29Wtسcl^|ܘZqnLPN>Ͻ}QӖfgg/H<"'\f yty90F?<.R4mҪ+BTiRDD.RAQ0\@w G* E7^AN ~PX>&KУCf)VvL9y& <{V8.p,BTUxKkRiEs<,AT=amnu,fsvO,Ԗ~P[]tWwӷXSwoPӰNHې'z,*x0k6SGae,U}*f](@Qc:*f{h,X^q~a>%B-4E,k(Ya),Cᨆ|A>Ϛk0hj#R\6 ZsWk2"3ek^$J=]#0Vڿ&XeoVw+|oFĝ%;߹I q"(BLDuMM䄻Ĺ cx=G*?ɢ[5|@4 2 t Hpy'u ػ0{lV>( of搞_m++搉ϓN=dž`\٤t &{#E/]7 klZ EgGY,1rc #70:9[w۪.Yd)6i.k3 sp)CG`Vl*4]+@C\0 .\*M1N!'KP&@_yՅ0֔)d `+ p(kjzZv`;PJ˄KWJ t[\ĩ@ju y *Xt]G."m`~r& iyH?Twzc2<=JȀDK#G;;j(s/p?PɺoF xnƏ'ON0t;=|Fġ^μúߊRoC6M!U A0Ă:ڼ{1#QVWdVjut3*y$AUuixLCB*+ݮ?t< '8u9|с\<{;=7 AZhjTz`HіDDMdxAh1Krt U;f9_Oe{}wcz_EoOwF,CAP|Աk.]6FLR,-$u2t+"o9;m_z,Mj%8伈43k+"XqxZa:}=vMO9'm 0"w'= ORn.`APQ.kːu`Cw{G;w-* oC6/m:vP %<>ɍǓX7@0 rO`ȪM3*t2`!jiFTCDr-"DEA( aH4O|WbMi聈vi/?jޯ>Cf=[W'ҖcG]UK] C1DU$t;AKcI*`AW܏gM%k:ra)g HJ2Ov]dȭ Dv@B@s^Ǡ!\YьLf/ɱ8=!hS7^`Z;eoc(B_ov6w :RYقIQAS|AgLC̫PTvE3gDɌdZ# !RߏYC>CאEƆg .+/g'2) Xuhnhfjn "ry!te~(~hU<~T:<>]k8t% Vp`wk^;kw {UP^JZ򝆶u5;lwH):qwPxs`qrM=v`)["߽Ҕe|3Ґh r[ nuo1;Eس1SpyHY'ՋK*lG!}.`Ṿp> 5(o?@DE8K3KxG@DcڿtvXЗVh_r@'Pwݡ@a i:w[)x>X ŕEq-`i=uYG\;C`hZrםhqi~[8gs-? ^V":95 Z}gщ8ӱ GVPJyO!S1 ehӰ(e5]4c},+*Ȋ :>e `qAF$ʁs tGL 2yɬQR-ߏČZv(wUQ(h0\'Ļ59Kjss2" Zř U"JmgȁDzAA[+ٿxApctƌ9+ъl1 zbF zjpw ּz-;aص/6.]%ڿ:c|<2씻â-yncWjT6wNI7 / ѽ;Dt@KB ET"޽ BHo7\cÈ@=jR% ą˘n {˿;[<_#>`ܽps*Ox rJ5sǓ@J,N "7MiFw ODTtECUk2W*b Ot]Z[b_yaAESh:T]+B`( MR:xeqv(-h\4c< `TĽ݀GQi֖I[T5@V 4 4#Ha@T_XQ5d YeP?Ctz C~rM*ߛ҉SO+Gqp؞ 6LqhP|z!j6z&U@#@{qW*?d0Nu:y꺒ku9iTSlîѠy 3-p@~?[ckw@7\KK]7ݽ# ܩ}\mܲ!0f׃p[IoXS^}3o#(SsДh/n.E,NBntX |'`s*BaQMc,Wt`Fdr5m y5Q.?zFŊJσ{{Qs,qn6.4\@yp>ʦݜs@x23\0[#*1o># /1tBT(,;8M]. B6MS(( f#ۋ^,L ,J"O :$Mo>7(E PP8G3U/ *C[(>G&⠃[B:->: ;Pxuxn{Caoe֒T<5F]$< [WRv,|~]SLt[ HJ&ǙHwXq ?;-=P4 OtH? H[rQqL^_O_ēWLl,%o3Sg`Ϛ,O FWWկ$,?aqQF*+ARDJDoTX6ɫXPWY0 py@Jch<̀ JdF\PY}2*lc3Ҟ_@n]h6ܯk?7_zq8k3w\6{uQ2 Mr@Z-cW])@/m d@VWsL_Jo5f=;;f]ĚWŋ`:?ѼwO)nj}cɥ[U#p`#@tW9 "@Ch'GH\R0-Ȟ>ls,<]ҤR3pKאi= a]!q}K,!9^5~mWx8!Ǟ[ 5*/)<70޶m2tt ~ymg U[?rsF7#U@䠞K39%b  *$U̶e$p<oÿH'""rF T]CoTF޵@/漤RLJ'-s4ؙ&:;HnsUס*dMa v弫0}瞌*cNA0Gd+]~<Sz<,zc$>к3oO ?0>؍?؂uБ{AD#>_=HyB^x<߼0f9pFK/iѽ;_kK8Ĺ$q&p}8raj}ú_( _߰?EQ9$Hi@͡Vac>!4L֩H%e^EglAE*+#'((*Q qᝍxjh7o16AG /+jWP]t]́BD"9mAhg^[?Wݯ= $]teA 5#.[sO5.GsWpAWҶ\Ov D߼ڟ_??{A]hE>ٍA_:DF12?ȂUK:ُ8yTEOPX*u˨tUhnEӑʮx:cU߶}3<ܸSEpu,66;mI5EF@X3BDDԚcb>ֱoY ?<6ݷu-{ cݑXMV>w DL;u7 `scSmZv>֛@n &t]|Vn <`p^:w\Ԕ'Gd-PzzK0 {a{ @DNt8<ԋ=}QQW E,]n屖jwa ܾ5 GhH~nxk5k@0XD!O.7 }UˉA΋}Goڋotp3PBFn㕟^í;hk;"S`glK, ;sdrHdߏb\T3WȻxX/ES WXR(x]*[H IDATv]vo(X*uz Cs ANMGIUXw P nC]dkh[u0[^`y"*l@2r*ԇglͥ;8X,`"C5]9(LIyJuΧ[;vAZoT&>&\]X=-C?Ә{ʩÌWUTOC:^''`Ji5v('P۪|2 Ka9SsBc Z9_Ќq`@U_UthnNiYo꺻YiO޻Noӛ)t=`{k3wO5ؠyC+@hJAWEPDD.m ~CO_-Gv̽?=uٞ{eAO_?X^ f9s̃k4_AS4{N;@o:٫5ϾrIVGInM@S7y.֫o7!w'Oߋ腈h{j`5|PG>G.C8׵ctrFnL5=0ٍeԸ۳}2rW$^wݘ`GV_355Axf!S,KSxH -,a iR)DHy/-b<%1x6?u,1F߷CE{O`<mQ#DH*Z"l99;*Ua: %aWMi5]U!x]f6ZfJ6e.,ՠ*I+yioR>N$#58Q !~,W1󢈘Ogh2+L0BorECP@<=}Nd@8= zւrjUjT\3$8é1`D84PO_n8=PӋpkpd8 OC*h˸ugvX4р'*쯘Le ߋ1Bν(ݰu_߼S?<|RlXUl=I2#(ʷ!rm$U@DdjσjsE_a_4tq [z,곟|nͶ\e v5qU5;%Eԇ{w?Q97K\!.DDT!X/ q㣆p8ו *B/af!ݔmYZʺs<Oȵ"uDsp))T %lw7 4r ';&\3 }.bTvxu^L'B#ӏ?JMC~= %"Z';G]ǝ8;Te8(Ҽ*#JkVΨ8A1>lLWTDyJ=4X(j9U`]@N[FgDBvY._˚/0UӮ pIt/@{0I[| `b"FL9 71쌥jPvG;+NCjJ=xc`)' e)q!CVU|/q78~<ٻP$ Ehύ*?*Y9& p,f >/[s u`t`9^9i@m:9;@RVyB/7ߝ'3-tdY?Ǚ_>{r "k_#^'InM@+qp_|-C)0xrQާR3 59ؽ#G>'Bw{>ƹxpNipg p0WKxdsHQ,1caṾ'Wȴ,3 zc|o:7kq2LO[ź=4C9 ;Yn͗=-Ud <@C]<{<\_bZKDuS / "2LADDdA m[Q:d\ >tuӺ'Xt5جP߶<6)pw.PgZ>!Ɗe-u?󟭝Lƌ,%4>6l孫{+_]Vvzǥ_A.i_Gt+=mz琞6g弈 qշ t| \ "4ծE#󸾾3_?rS@a ܎na{K X,ݜE;4@TknlD 4/,h"BdJ/\{Ddxp`C*5BVU۶Ey+%^<]_N!<+95 'ZKC&;;(J *e6XCu(@ N!a؁w+ SV8ox4!=cӠBl jlj" XHm#U(܆s%_ ]4vFqi."[K=CYZLd7 ="4Y %3]SQq3fwTw/bVb[J^n^9ι9uIVbɎ,'H'2QeEA}zz} v`{z p0U]]]U]^@Lp >9bn'FH_\<(ԫ/r{6}{gz1ަc4Hrm~A9+]0~TJV¥x_~j khۿv9ا2qUquh LAK;9;` wOQ"QE400D:tcrYd5^ti /Ax@dKf՞ ;ql9+.I ́P>t,0S8ߋQC5uxxGmX 'hK{t hRVl D&"kSMQhRQY* ;p4 BsjQ xSp5i4~ ~pa* ?&efeRd g2K &/s\S͖M4:'УUar{@sEs(Pbi3 ~8;~h .f(Xw2(U$\v/܎(51KH@ lfI "rPN]8*>&ܾgس ;7DD_p.k˹enXiؐkeޣ,_$Jz ?pC,ZE2kKxx9MQ`I x]{Mj/9X\6֚e=d? ^:ƶAOllI;ڂwǏ#bh2}'"@ =a4Wg,tSOT C3^(*/g Y:(|reeKр\>8P/Z,DI4bș,q@10BVU T# }>\R=˟f8=(}as\ Kfh|m a(PS#ZyC:T 5`Cv6`vXh[;Wб~+e xXc3SfƤ.s4r9X7>)$qdE/ ;bPW/ijd^Gx(R1 (O 5 (IRDTp "r%}=y<Վ)S5 #4q&в0(nz.g{/.m۱mkWaR/NW0(==s q.#wm;46l;̯_6l:^,qW&+3ًU6~fߍHU@NU*VVǧnٱdR.^ueEvG[! M9tv 6a.auG9Ԟf vUqJ2tA"bWlacin60c˘(sn.Epn͐G/YvBclYo{$ƭCO'L& hsyId[r "r0g|_7ccG˗NHDnixCQ3 &Y;a s^WjbʷBo`m[-4smDв*+qKdעNН f{:;N- l8<Ľ~፭B堆?~zl 6}bV*(`%+ɟa\ETvc6k8v~v+˷+ڂ MmA'a2"##S vuo gniD@,o|YMERU`9rV0L,t}㰬/CkM9VLȅ=ROuv^b7ىVjhzbcBx*$`.(Y-Ȋ@DFB jz2ЍV_H3*_BAc~~ZИ.]+/O| |pȋm|L$oٍ[lC6xUT CM.3S]}u}ߖCu>7~fls@9$);#?|y^T:Q$w A8 :ܱgsp,908.љ.-ϐfH$Ҳ }|IEFO*G}l73D* K$-H—+0L _r*˒ԫ=[dۭt/to_ӱ"P&5ie=膁D >)3Ãx͂e1(9)Uq$DT햁xifZl\&R@gOI "[ Z6نF5!=sq(mw5` }tyIßArٮjjx9 Jk _f/pո)؂"R6d4;ە67_L DZʻ:,*'a>lF+0 %/kha칚?B:+"X{TV$k9P3!%/W  5h4LW-ˌ7 &豲I$sx6ZViS#V7!TmỤ̈̄oٱASu 04B((`m[]Ё|jv nji( Oera Z,P9 }jk9Fd{%)_F.f_B͖x} 4֯z5ގ-h_9jpr!""ѲĄÐG'9_8z_UuXV^ׂmVw'$8ُF?ֶU%ߌlDyGcV=a}@ 6v!ZJ#w{ӯvU0~PU?.;h}y]8 3E#W|n˰]g޸sR:_yۅmD-VtR,(䴽cuUE.+Ǻ)ӑÎQ|[T^G }#q%+4 9쎶,j썛ɤhp\)Evqm 7N qKIx KϦ ES4|<??7@>ÒaRT]Ce_`10/N98r,ڴmE?d zյST =qyEUs{8ߋ3U`KC޽wPZqzxrkA{ȪD+Gf}5Ia1L'H( /0GEٵgx82؂,eHc#/+@|Z9X2O cM<;LjD񊖠E4hnOB14 e5tasi ˪E"UaRmYƦ?㱵#ЃK!e=s. . ]z+k8 ~`5n -,3 *:>#p@M#0]i,I矹4vvoD|] 4,˩4xH1:mFy *SҀVq@>R+T p+ܨ@Jhh84>ދgDc_o:yh(ѱ 6#\:n$°4ֵYv(Gb8VV9Ώ9|kl` <SIA =]s/Fx=\.c`j,RbUmva}ɅȦFw Wu"oO!@*hEk!yejΙU*7>//Vݱֵ]8VN_]5SQq%s9 AIO;]ߌyAWl7 $޷&ܰg͂m֬؄@΁ Ο?CMBx+;/Н/;qwhn6`ۋV=# })ϛ~Al :̼r)s=<iKLC9NEpvnh{+_~>݅䢗gWZ `B?GFQ\|ܵ(,$Oc;@XB:a=<.6nݩ YD@-zGbV7NN_˞>Q(* Ņ.hKBD """4Z2<׉_9/ض5R2P 鴌m[#NiC6xس{𺩿AY~G'Oi0(?yԂrix-~LnG@O_6rЕxw^D7aM9`*ҁo? >@U'x 2; :l=/JVPtMP2GMVʊ#I=+fh0<7B`yI0|"tjBkQ\r;nvuK/DZ.c і,5`0U>r.>Y\{;҈Vmσs֯/+Q???.0D1̏G`NyY ^ژjŤiQ4 [-kmԁl Р/jcIyx9Pq1"(F;=2 ]`e'#b}čcE@DDD&bß|g'wgiN`˦Fx + U"!TzF'`%+ē?svX* 8 :uֱkڱ@Vl@_|ܱxT{]S'gDUEݑHN7/H ,.4)Tb(5uH9&&,C{P I٩:&~k#^m^=swo^qfzH%@00Nc,A(ㅢ zcN_TaB;~UV\s9Mx/뷾BNCl DlUt d篘 lVˀuءS\`j u'W \'SS )ؾ5hYY]műz{mk-k~Q,*cyA jzV @60qG ` KV nx>r1aB9R46j+ueb汰</ R4"m08;Bo߾g[]u.?> Z۱ tT}G"'wzX?c,z ?p=$YZ$ ,GȑyY55pGGhaǁZ|mmy+xP SI٫3IYĥl? mj bGdv6&0##?-EƠMehfvhiEQY4JMJa8a}bp{%л~-E3 [ 4)UD>+.s, EGꆁ!)~ri~Ӡj*Uz:Y~BasWHN!5invTI:ߑ[8;Lo$H~`~ /#61՝†er4cMꢖ]t S'豳փh_`/` PPS }yّ* ""'t==k+:?=.v8ukb;:]5ڎ- Mؔ艍A7{ffbk -`iDp10$e 9IMSnW\%"rSցB2ţ2MWk+GT]Cn\爡hD!F3i4 ^Co]C8rk vp<,;P#x˼PeAw߈yV`pHDdJ!;3 Gd範3k9 ;PU0y1އpxzc[<S@_&͡Z}2S(;AdEYgm@hp%եi3u(@.g ~gv1i8RiIЧ7}xr =LUKx-@2}PI|ÅӔʃ޾ m3_tK-)늶K(TPܱ@*?OszD@DT=ȁ91ὠ(_2Zt aH]w'俿D2H~ÆhY]zp^Vvᣖ=lgѶV xaHWaȣt(_Gh}#aD6p;XHt;x;sX SVA4 Ar@4-UV@34P|!N*yUkLsåA['ފ;<+_. Z۱;I')CWh+|%`iDDUMH$$]9 5tgӈz}dUz"ǃ^ƫ-𰳭Mn3Yä;WzpBג(p#670 \V-8~3^:o})] &A;DW*9+`247g;X)acP" |CaɈ ħ"5<ضY9핶4vG^n/uuګ@DT;8RDDD@y6KUW N8uz`[+Z|[GvD]\߲ lkq-4eW.{wtyT`Oi2IG]<7L9bsuҰU'*4$'38?Ʃ>h?q3>+@I!JXRGfBM8?{diX/0zFZRrYܵ{^8~>{d-c\;4bG*3N:t֝8ߋH"y`kXa`2P>Af@ Tn &VggҢJsyxb$tc$'!"~g9 #1zXYUmX h&>X/|q|c&M e6aIu&ȃYڸVy@m/O߃HY>!|:暲Ϊ@9vK{mizưlnOú:z tYo%a);mftãEZ,g!ut6h۷ 7|鳋 ̔sB?04p 8/ykU0 /*3 9x5!s vPe&ap,o?K<_N9km'aUvS.ww؟wwqDKLx D"=ZX\YM!]'A#'vmD+X75Un,q)TM~brT`+5 9l 7F:/0h,7J/ r- DDե ɢ52g`Pg зZ  K`XHbݩܺ)@b=Re 4! m@Y2y FAENӠOdYAɩEW+#4 D+Oϻ<+G%a~y}4Ώŭ:- $Ez(i!$B͌CϥA9?:2 y->0zpvXj[sfMPfnжϵ m`2㭁51U]! U͏u[33"`!C=@S۶FJn;4bꚪoh7܊o~ugth-W.s^W]#.0 $ʸܱwf9[uiwgH["RDr28_uΕ-OU2de lP2rDKMGjh 1p^:5~$ePnFeA!;O j6g;`$~cUqs3)T睩e]Cw2cn~vDC[ ןM"g 'Ezs1)r$jr9AڈzzF:F2Up1܈Z!B(슶~+ i8t""۲CJPt>|"Oe>lz@Ӑt8sVІM7ZB(@ ϑ)!_SAzp$f)7-5 .C:{w0iZءڮkB-͡’T#tEN!r_vkN."H@z n Mx<zVLF0N [CTX[""Z[~U[)H% r\!Mԡ5Te+M ZqubA4!|דJ+e>`4 X1PEaB9t>3_ VZ(kvp̍_Ԝ3} *aPIP|:Lݩ6jm,3ٖchxX`5c9N&Ns\K(EsMn;P!aR~CGS$6F:}z*=0b `3xy k[׶T ~f[&Vd3V>Iːd ϔn ;d3*i2 " _+֯?:f=P֐F_>dSX z#;"""%4&Q@*z&#/ \SPoh5[AS|;C6z8B2)rՁ~Al`E{p%3SiNNȌő הP%B/ .9;dFWmw p%|~tVnnmoFkMiUHr%]}<'HKX"Q500D:tc19:CNfx=xik=qqBv4EgU+Yݞh7w < aVoEkvp8P&rKA";Tqi\ J* 4$ZV26ʼnB ve@)2&ӠasYuQd^}?NCvūig\zi`3"IP:XȲXbH"!˳zYk|~d [oדykm7l1VQ\2*2fR=Z,]htk4"4 P}uȅ.ĥly~\j _сDC6ZNc"Z:T߈sCɻ@kM-Mr  NˣH!ъpLxiFt9qY( ;8 :[̐C.cup{3Q.1<#MQxEʄ -Gh1MQ$xvSv}zD?7 CmNe9Ma0a$zQG854جӆyb{ `ZrA! 'ZfJbJ⪣fۈ<1t|D;Ky2=m~|^vxX &{H!Z{)D,h]AaVD|U ԴΝ@ |#Q[PDm6hÿykJ\N˸=fj['^у~{0<{\ߌ. CcjwC3銹W{dP!"rBDDDDs[27ط>ij uY}z8^<2ֲlräm\s ֻ- s΁[H%T;G!T޳~mo3,VN}0+q`5g38Ǒ);//b}*;/] '3SyArV2cqdE;J^Fꈣy*Y <{vl§n]b vCJn+ M#`u ]2Cw*TabQOpim$ZL==$fȅnT(dM:oeLĤ,R!/@EP[qi0Ħp#e(eqGf\F q5T;1^!Iwo݁/#X{(o*^xkI%)o j'@eμf! {ZPuu{='ീW]8L@63x0;L~GBAZVMqUHE0 m~ȤuT:IѠ9$EPf,Cgh<0f3Y *;LwhΏ%ЗLY(;S@ЃH %!Q4 !|x-вLqpS=Ӌ3c$R9A_`|YULU*:2)56Nm7<=<W#]JZNg9]^&_3|E}R /niGcC/Mqk;iFO?ۉDz!{?! P݇AcD%n/?]c x?&wh$h)͋:5a?{^.;//▯>lbI@dI ӏD|u5`xT .BtΌqwlyy|,JT vm;w/{ 3cJuf3r<}/1'fu.nwywK^$Œh400|$%UIUA]J+2ʩ*|yx!qF52!Ts+yX:DI@}j}UȘ%U#xq%QVa gR oOX_T!QqQKv`|@є,j᤻4yM`HL&N,S/]qo-:6l y|\}93:%K`cl2l Ke r{IIr{@_$m IP|{'Ф7f%M%ؤX2$KdmeY?F5Ҝ333ˣ[>.Xf;F9X8QJ0W4EnfaU 0uU4 C#$iƑ#ĬIIQE4@BPs' b4C眕.-0HJ@)4@Ż{Y`h7$n>y8>'vHqL"bٜ2; KژM- H !b#􌗕< x"l-]Y݁ @\TUCb^Ռo|F\lƪr2APzcy-ms\pl:S컠K3Æ;~$LNuG_x BVu_;4"0`nYAaJK`*gp9˿彺>zwc~ZɅaY_ڸk+ӈ(9͟+(2"ACdLˏ~%@FZaC. =F\WlhK]%9;ֳ 6A}Ho=]gpz/ml#h^#`ָ"}/t8<` r+KˉAwZ AHC d(vH(2FDB ~GH CAnӱ3X{NXWiܜ9BbT_T_{<>!"tVƛ/KJ?/ౚQfffA̒ESp J&-w* IDAT;,rc&('$V[nqN"c%)|~Vgm1qi927EN$S˳wWd4o~:V'̏S [uW~"vk+DŽNH\L-cNʌ`~UbQYc`{!mu8rw&@"gϝН* PĨ3I28Ԏ3|io?8A_9_Atktma*##1HRzAtzkcu _+ݶ>b;ͬfww5g{~8T,App-l}Ѵ ":85"G b|]CfA:ߑSKs y럧y_6[Ŵbi=y)W#?+aM!|7ZA>)N.4[fIIr97[pW;]}5ܥE^]츢dur%ҥbAHg~@`6fA>k Da #"<@H ^&XtdҐ>u^M!k 1Xh_߀M)vV^iZ:B0i.UUB=ŏ;muUhD#R  Rf AV}:]DS8KsHB0DQ/^h*MiP)y\1ky>4I@~<<hA5[tϟ`06Y~cvV{bݟ@I/p5'ƟjČ)Y.u{c 6;Ϗ'uw"+ pMkhd0UԳLmkȤA("@ (PLmJf_Es,KA,ú٭&|큛G?}nNյjV8Ӣo M`4)44s9Js泥^hfݡݯC@:^o3F*芆fK! K3Xm) 1QS8dv"ʱjdkd$DB<'g㗎"֎Kp߶ȭ{܉ 5_ኑ VjώeTfGpI ca@04 b#A&RX΢w}1{ae5mC^i`$Y[Qܺh]fmm~Ш UwZTJX"CTp "#!&`i .vrEw(`z]"ar㜹:@f'K(w. qC_XPjF,sŶqY˃-`+1g8P?M :FR d-@~ڶ( V;vShq†xԻ6}#7O%*#',^3$ğ_hjFev|@aMw,k.bjOb014IUQAEEYX,m&tF?EL2`Lرx;9@sX|`htDT8n0怩n/iқz@S9+' ^~ cuIMWƋ>m =2'ŨR^0e7v!`b@6. y`g sKӿ|z·k8=P߮sƵ S!"l.K\-qY({V|עfa?+ܔ:,(qY4YG/Yjn'\~Aj6''\dAH `B!! "= R턭(c!YEw3rw\cGxfܵ*xNܰ ,QWakK$B)XPp(9FC<#Tv_:d@Aq{)ĐCO"XAIpλMVUy{ؿ偹OsƐV^ܜÉxXپ3)I_ζ ՀYN=3pJC>,!JJfmCkAR`13Wngg̅rg'LkQqu^f{Vz`n"թfa\w^sG{qWp(/cxw6xl3&ɕvkRэq{bhSܕ~56&|C  VŲ*߯9w_Is1õZygt3j.w (:\`4P$-|/CQMwx.XWs;.ֿ} 'D'_as*AĂ1#=p*pV > #?X΂l"Dvuwg`Ĩ=݁m}DVlk ᠡPAi2Nn'[<NX+DEtEm9^xZmWwS(KsD& $?]g^-;Q.%? W &Z1 c F\XQ.LPp’ʅB˃@yyI y(0YU!* L4^c~FT RvF0 #1  ts{pϚDP@p *N3Y3lk?9膹b@A E0q5EfTq)DG<,0!DdOCr`i:୷-Z5R'u\zΏw:YĊzpMӑ1 R`7qᅉmPdLT\fn]'N. 'o3ur08dX{L8U I&WZ$.jJXmcb`s˹ry5qpɠh?@UXf6+hTy^0j*'eCS95[Əv$}a_Eǔfa@ID4k61Cݎ)]X E0H+_+2 H !(i_WYSp7#m&<;<tMX'Lگ LEury9֯@˂e^wSB[ TjUX?=v H~X1X !(aYmwb wDNXUe\"mF8c^vX[^7j$PW{V*5X5n0 Jx^:7>e>lk .}]vDxEqD^^sYVRd:#߆ 3)xlMjgx#ii G IDAT3}9y ~3Ԧ9f(0ࢁ> `F2[Y"agyue94g*#!Y' ОF5dJ!d>e T_S?72(7 aY]0%۫R* k3R7Q+M[:]~%l^[ +7=øԫ>f3eMPY{U0]MәKh썋n~ eGz,} cLqyCgGVב};,,uZܼbf;zomxoǻ_P_΍9%E.@DU"QB͎Xteb<3NEȉ@!yy6EEK2;@0@"y_lQJf,GspFm -4=G;UAMfYCC+ˢZ0AB֏sjF=M0pT#A d%ף):v~EvN%6AVp j污x̂OyR)/ɀ}g>z>D|KuX‡S@T)0|a}TâD2A;@m}|9#,)zhݽA}œ`CUJJoA8(c;C##1$Rn/w7k.Rg}Xj4tkhۢOp y Oa]7V>]B3 %vC7彺sI+oVVB~OW|;ӔkZ c†!%0jXY6i2B"x 0a0G\`ecdۼxQ{(<:Vg'5\mRML IB@6V-Òh>{q Z@'dǡ2 !5wZWO H[R։Ҙ/LChfeWQ|aJ%mukg8%%]!;߹m& GP7dO&;]j]jG ,*ݐTP=30,ێ&sh1g11@K#dF 5Ѱ9XsJ+B#Q$Ĺ_p&Rx3؆.>D,¾.4K2K*O>5kȠ(K@ 5h[=H4g2.?T`݁b360Ia*'UIJ0MrO7ç;.r%{S4Ԕj֞|;T/qirɘvV-09mb1dאַn3˸z5W! [z*r',*:G|D2ޣ]x⢈H>4ae98f8Lf2 ieEFg8h>ڜCAF0>ݨuG(!;m&۸TCw0PLV/qkᰰ)r.\n<ˡsy A:àLn" mu D@ B[LEw/Ou"bYtHWŪQ#~M#g{eu׵('Ak-";SC0&̞wf;];`mYFǎ 8 qHɎ/WnOOPD<ǫ"dcJ;@mH/ QrKAoh %nՄXD$DeXLfі&Fv\&cxJ4hpѶ_ }PFO.&)|ɽuMLE_NۗCQttdb|"!!m4{@{Jwq7{cB턢(`;$u XN+}˂>~#{GqQ ݶ>;#yvw0Yָu_>ҕι]]HDfw3;l([]7oK* j[p2;x7F֍) V1Σ7kn5~ 4&<1ΣúF/z}umV ZG - ++ |InXQbmi2ƈ‰D^3 !(acbeIi"x 0C&'Ӿ* B x/ fxotN &uؾkGADVgo}K MQ(Pas(pl 5HN?B1K@^B`]QQp8 p=bl@RӉ,YCd❃DxijNۛRI4VT؜mìn29D;fvml@wpo0,1,Wgա89m؁ah,)=>^PD `Jn&? s ([=hr1k SV{WEڂ>=7H:!rO/o}M9 ,)LC܎݄^#Mp /j2_0Kvrw'oioyxRވz[~!#=WzRnq98Cwa&uW?Y2o}ݡsxC2LRqA.Z/I ;3:l۾t?qyB>&{~J:J,T9\`(tFX]KBP@Sxz ۋ?4;`x-v#4\RNP%O 9ۍF/MH-zHV^ * -,dU/鏹4K+v/VǁN3G(75!p^z5>;;S&-,ͦL/ # xu(GN;ΐ@Ѕn-ihr"~0$@ ,i(Au/r/GFb^*~/Oh&Bץ)x߇>'56uw0L(-Ԧ;K:2!FG8Ự揶j|f*v`fl}Q,`1b-佭oFG r8pލQL&3u}Ƽ;a]EU/+ 8IL(ˈ"BQA??7t_Td+;U V'L&M]˨cc]ڄ{#<uNls5s[W[`Jԕ5m5<2iB:8UqV)Bk+#&gVc?n2\R RqԩUFAO^f^Qcs!xPvoADs>&D^ ؤ6YEy&xIߦ'*g1(-3* <;rB* ,c/Ŵ3ER!נf%eV͛^L2Lf c(1@MW%gg<Β@ E.~V-e:I,D@ f R h_?e*R꿈3g|W?͟M2uyiK=c ~_Bp?jYznހʊVfX2%x} ~\Ż 1.[:/[઩BW%& &5o6h:v"&;K&ќ3*5FC-*#XƦoOz I@$*y9%bB֡) 6*l֬:~t0(69+2iJ PQQ=!h9d(hmmM)a#!bٜ 0)^w$ w.z+*2ɦ#D:{jDjcgJf퇔T@Km`pr^۱̿RQԴ3U$ar -ptt'(g[8|I *?$o͠Հ DGI >àpfI?PԼfG4&e)㿂$g㹨804ݽ]%<,q㫏߈^8޿z|V>l;PVj(CÍ!xֿs/?SC͍{?`hxV Y|n\bK?w8Inی_cNôycR_շoƾz]C[PzUNC;辆?~Mge98f8Lf-!v7C*:AlyVD@0:;¨GAjCw()MSרfC}hVVN)*8 0z3imE𠷯#'xsp1z?)X@?dYX,r`", 7G!*8$Ɖs[Qd\޸SCD s'@sp-2;T]"Ʉ ̟W6ytw?eRFyYFߏʊOߥ;6 ʝ4  _] !i@QN$vY3[-a`oD,cOɉ7jR)s$(IӌJX4 dI3Qyk.!( ( ځ׾`LD1󚏐,)t* "H:b.4Ԕcr4Ԕ/h{6x@.zr;wgi_o8mNsC-Sw4<>8֩g}r Vfb,~ìV3.I:E 6+qgz>B7Z3]߀G_+:_Gv@1?FC;+0ڬ4֖W?Bb,qp̰qLL0˪p:(j<:0t9]J'D@0:{=r/ENFJW>h *5<1c))2,lP=.3n^SֶZ}]fC nXZX7Qm/f_-D}Fs͆ʉM;)z 8U!(³iI >"A K*cvו?qX|9.]ʼ3s ,28WJor̟GwC1DNs1"0UR&DEQA,, ]>ಚന 2 IVJp, 3lbԕ)|Ca$og\NN>+yz-oEP.<ͮYki" ͶY94,ubwZUhs֓( L<8ڍϐ@0JjrʡΕ\ (M>scע))(JrXk a. '%j~cU B 9B1gG0s$wʅUnvEsޙ<O>z#Mp /j濆 b-Ɇò7޽/~hϿ3ڧ;/ݓɑc N \݀}X4v+WG]Ng,x?y^v㪩7%Yઙ\U[Κ0mٰ1`bXԹKj9#"9BR$3D x=3j{y"xЃ3igw_cfu 3o*=ݢǍ5߶jkCp{*H9?朡;:n3sppdb~cTٝpͤ317/[Ntx)<)>!'|mB6Ҝf:j/]8ٱ*Pw^OOŷJCX &E,?фhB b4s`ZØ3c$% 3faaeA\}re*v8p ē\oVo'''ko8Qdf尮0r9}xc2KA@(T1)۲) P J* -՚* 5N@ ѨcEnӊTWaiYZBe 9uIPdB BUCS`x>s IDAT`o}+{xBį;}dPⲠz+<.-1[>ÆO=2>҄B+Ռ_}ꡞ}UAsaYw{ef_>派Vq~B80֮ Q`Nl ki le%c"ޛwZ}pnh 9% aG8@B!cUXmܧ\< F’'ˑ==B1n6D}6.G9M/X=n}K =Pa:}+D¨r8yp[*mB&K|BDrg&;P%5g eRVf(P\!ƿIb䳝=CqhTqm휁9h(^?FC24*=N,[f,$%_AG5vyi,MP&!{S %XZ}oa5s<|7H<\5~xgtj\;:>pӬNo8o2uw`XVfξg~ׯC볞wbO=:m~#}4_y3y\݀pUfK wدkNkjm#څP@|dR1>cJ..PnBa-sVVB}4:םOn&mBPS9(b^ h-(2Z#P<  "x zl4jbawņQ^KCq_cFAetu >Y-j31TIk[E)bJ:-Ȫ+8(1eYx<pw٠@LhybAHg&ztkNg${\c9X4+-&nN!F8C^mΈ,fvb@WUe* '<,~2 Ͻm6&{w䒮=oEib< EN̜"&DJ:C~L iB_4>k9qIƥ`;l\\ԅutӍA&f*rJ8e {mq?Ɖ[^77Ԕa}\›2("4]ҵZ.@ B&Dhf=W< {Vx\6,(eCu+'<8Ԏ8! !QⲤG˂?+Cx0M1쐉9Swe5nM OtȞ09mYSZZ_{bַnllĺl\w6|a$b@s V k]9un7?WM8! qv{ۭ&D8<7u|bA@-m\`p#nH)HɒiK%;8FNcMVힴNܦ''m8&ݞqWcqkɗXrZR$H $PHB `$H$fpY 8{̞={q̴v \b[n-z{l6ȀB`* \|Q?r-[MׂSv1~26Ѓ'olXF9 =t+{cH0]fM"0igC q-;$&z9D~.Aa92;|ṗyOTn<?o$`V%2;ĢqT T3'* VHQ|I90Cp !~Bݶ_j#FҞJ%efC_'?I}JA)n 9Xxs05Tdi[?u7gя3Gn!JGn{VЂܿ 0̱A;4qtO@ifwebۮ+Wg.O>>L,"֞@T*z Ah-`0: UgǮs*7u#²IƲRρFy@[/+xvowKC:W)؆e5( MMMhhh(ܐN}}}`ݰТR ՁhIm%eBaWeo[ :4Ω̵:4H?ύgZ[[ ʕ+YY\%ʬZ=|dR;tnpN z8W/B، S)N@ ( vx<w vEĖZςe!QJXgtYprC=屏8S,V0gJvO߈?;,8ƔX seK"$e-5O}R:6|$qwK ,BZASR3>0MĠQ lF[WF781ADDDDDDDD^ixÂڦƪ1a:Ťkf^A ](hAϟمw_wOR)CdUkޙTJqQTU=v`!=ɻpϗ.޻3 ,q!]/>{\jOg3vln~n^xŠ{`NN.,O(Q|۠'}26 ~rkOx * 9Nl~BFK˹t@UObWjs6Up.ǭ*GQޢ,~?.^=PP6+x<,D[JJZ7<2_wH2p| uxc߽6m*z[;w0(@erPZJ^]TQ!87%/k+v9E$ltGlᔁ{s݅4]aVDl@yac% ;$ˤs D1RSW!Sa;}{sA_l <~<$lJ>}F|C\o9*Tr)D"(}x?D0"@H@I9nzQn|̯þGVxvB^S˪B=p_3Y:kuS%mfVkjV#_t-.:>{rryX ÇKAI48` L8͸ZPPk1/"Ϡc+ exjA{ܱ,H8^p(u) oA9@ε'<^ d$ &G*CJP KcطmFQA75xŋO$R :zݜʬ%:x;Aў?tbD`bC7T(в;/HU%UssQo xvP)e1`{(rrt :rE8-I߇F0OՀmB`7JuټC(u4QLJbng 'xU!QCr$ /LyQBǧ| Z%ݶ'sʵRBKtg:TE:5J^@Fcjw?{޾IhgVulXo5tvщWѳrI}ZV#ǓЉ}_y lhSl`7O Z_Ytf^.}㟂D$Zי+[S[NMrX"qDP7ccR U<<;翰@a!EX:l^V^լSZ?gΜQ%%t6`=~(W(!ZU%E!p{f@LHв5޺СIL:?"J7`|P(ۀ5;ZJaFkӞ %CsApϸwKSm=`}FV@Ǭ؎x]ch¬T.Z3k접0! V>w0PGSA!'?2hl Х=TE;qWGcbYϦr=bيLV.IxdPϹ)H Сڤt 9ÅےJE|?D%O\%hTbM< ^s:4&a4/࿵UJ2;a歹j#a¡eVvfnnsS s =k66<;zf6l<6xgڏ)1!T+c"V# ep#pʱϰ'7<؇]9 ҮT0k2U#fܯOi\&٨:$_}ƥĐHʼn:QvH\OL?GxU/Saq,А@DDDzZej BdtD/!lv'/#Y(o$Cπ\A^ej#@rj#o_Z}}`ZHqDZw_?POډz* 5պogj؈n %C r}1|9~ۛW>Oo\}Qye`N-!R%r#45c0C2(5Jl,VRm}kC;jyv]*8LD;MP?:Ÿ?^EfQaixad4PP24(W޸tN~*ؿi3LJ59!}1IH𲘴@/ 1oj/F݋M&67xeeFa6jC(sc/}2¤b]4nyte}>._/mf4@ HPtD@#l.t("vsϥ7qv@a=:ܩYp-8'ǵ$g[zt+p ŀ|:NVsbs'->bQ xђ9!"pk?샑7O=[173 5z$""Zے(XL %Y -F-4ytkU,BZB@dIwJC"htrz >ϾXm-5_޽xv6!f8c:}ͼ<۝]V V#V:X=[w2>+쐹tx"} XU(XvȥRQ["P&C)A"@y^Ίcv~\A9@z+<^ X8 dA^}Kҗ^ x,ho7Xv]G,T3UǏX,XDH\ Ejkk+~3 +WUUU$ IDATB +HEb51fy-eq&`\@_vܿ=E!E85eהe| }7T%4GWZ"AV(yBRZ  ֭C (b@0q64xsYTGP]UDXhΈ4%DU%:Tr k)/ͲkDrs{VN9dtQ, HDyf=^1 m-5>85`7Ǹ''@DDD '"j94*9j9$bqQ_H"%04/ CwwnE0 f02Yj(a1j~[5c~"x`hA166ȓC.`z'RVm*qtǎv\U?} jpLHx!J"P`Xj*>,t\ja?C}6nk~eaYxB6Z*EMCEɠV=;vGF$#T L׶@Trr:f(]B4 eA7܁tJ~7w5~-`aYا\/a"])r)O(ȹ՜yflΤ 1Z]_w1nc\BS5+MPW3+Bqi{HZ* ;$%PQc"D(riHiAC6x,S$otR9vikq텘)*\pRza, KQ-+C Xn6 7;B(fx x|6X1H%SGF8MB8|V_¨+ǖ-[@ (R)hT#Ml}x@Dd|$vc``WztyE/˺DZ;+BFC@cgǹ-[F{r{OZ r@[-|QIh%2JHĢaPo!sR%G,sD#ҟ[ńD9:,3}[py <؇]@g3JI\iFK~{K440DtB h ({fcD;%2HE"HEb%2Ru[sHƭ%b0%LR% 0Y, Zbx9.+)ף{|ޔ7.@Tҗ)f @DD$$Ib4 5JA_Km0xxr5 =:xKK?=a_@r0><7av]wb@5nogs΂w3Ѷ9u-W.h5ՐJ{:_1>xvrvX(J ֵ7xC9k zʮ5ء;{Jvre XLZXLZXgBHAS-=g.kL#30[ kO<'8cX,*xG*P} ハx?6hV@{% T %R*ΚUx儕Ɲ+01MO#BܖH$B!{ G""D%)yl6@nvm9XH&@/Wb*TͮaCIUqȟ 0ꛞu!lQ!S7U;op}ӫ6ih|_FiNyI$"1rr _$o(pL,Вf:Ff~2K)ʼnkJ,uH,[ˑyQK=ؿh3p rcF"3dH">\) O`퟾[?u7Tx~՗;]$XedT8bH鷓@tLz܁| (dcvqarUH* p燷l1DRbrJ r TZj+[L߫t~b].Л陈@Tzqc e1HEbr2P.C]#p")>3Y!e'6އ;ǟmN|8<'O f]Cq͝K!Np2K?= 4b|u rHOf\O<.vɉM}J[fn+~鮕b5q.+TqT[6K%:owTE8y&L0_v2lp˞Β@BcU9ڬh.M!-b.Q8\8= Qbڼނ?v(&~T|ȧ O߁güzHU,uH R$ȥRRj6h有1 f׳.{&/;N79Dx *ehhu2h1UNIu֜$%%CVnXz: BM7'F$(YRl,;"u֗/K)lasOp'876ˠ]f]jA?~dΠa|{ߘwn*OI0I744FTv*e%'s;~6)| %C)B- Aذ,D]xxu%znp:.oр/}gO܉!6>-ec4*Uݾ1>jV=vAS;=LҍU韯 BKIgr+ ehS0,", 3`|Hbx23aBaF~l"r-X FFY4N^UG[qwx>:fŨY6Hp5* B*$uwkKcF78{iHٶ./^|D᠇Mxtr?VzIJ[zmu]r` ~d 4r=YrNɣ//0ߥ32[O}>s]Jspv.G\ݿ07׹'~/k2T ڿS$J22<ߞvlnf^c!ep[U]η/$@dND,Z0ò`b,B(x Hln%CRhd2#nov ϸ]u`GM",[ pOj1Neݓ?2d0 \«NúuHVdR)hBp۶וћ9w| 7.κfσMJhrT})GD4Q,Ʉg6ak v @(^ϻ6&-cOVWwmc`<XLZ|Njoٵg1윬7C].R*LtA &Uj% JZWan%-\}|up]Yy pj.Ha;0Ze=a{wL.$@O*R ǽ~0v921YH) @DD IbhrhTrh=yq{]FCNy{ݗyeOg/~|MS^eDZ៿җ)V @p_0nˎ]^x ڿ3뺫f#-Fcjw{$7p8:{=9kv3q o==L\Mu%xr ;TmjPRID8NKld Ia؇'휁"F'?DJ!çwosKL0sW;p#눽N4jn+q 0Kg$n&H LVJNh Ec`cqfļ)PLm%Kt~@z*"D0daHe]u@Ӛ] /leOM`hm5R[ : VNo}*Uas΁i}"".j`८G¸֊2zݓ[Lfw22$bԩ 3b(_V2ur (M2h)9Iq zEy A0y T6o>r{o߸.9'(03Lw +h [p6ȹwz&'xC@J\*H{Ӵ-kvx=6Ta`^_ CUEɝ+c׉@HVг0Ď is`:fCdJCv6g:@,pgȕ _/ފ()<Nfyd|4F`ʪ/ <GO)eط?;=NkDX=\pr43iPo }(řA: ]* Ewۍ^ef3t:Ԉ8xl^T~q{׷vd\&9AR/!'+ h܏_q<)TLԕ!eZ |+fsOVݳz2+;E?t R98h21%Cex/Gz4Udͺ=艾1Bgɾ߹cx16C*5E.^!9ء?a\Fo+X`!t]QrJ0CR %XR Baj vc.8=E=wZjf5KDı/PЉWj@3b8BD0#9VȠ)445oKY'/ p.nGm5\@#8rolAaz~׭T,xރL p$ґS<@u #p|kc>Ti}`R1Zpưl47`5B*c /=vuk+qu0|BVu$a kוG%2SAHUz:9CG\Yv\r6eLeZxjNWVCAql}֍>x|t0NL_ &Fo^h4* ƍQ($r[qAJ&?m$ z7bDx{`L{S2IZNG(UsΝ7JoɥR<־Gqaԑk -5*Vszu_7O龜&k(}{:ݙ %]?JG"2e%&19$u|ۇqƦO1^;̹TAO vB`Cu K2za?Ʈ;@XنmUϘ>ds.c(%$_g(rSrر>oX'J+3aハ \qEu{ Li7M&Ghv\Ih <`e$y&9< 10&$\pwgaYFQT=F}8bAA)a乻5s?ZA{A0LFFF0>>Ϋluu5r9J.64+pϖkҸsM@!D,F:/)kuyN4xX`龓h=B4?|D f^{<.o ~0t MV0[_'ݐ0.SC. @<-rDpt(?ۥQ NM-0JQts_}$^Xh w)ԟqssR"A_N!gEsW3ˊDR-LʽTI!{:= xw?^)d kU4IJCqi Bj zr6cqD10" z;TCdb@(`d3Ӿ޼!4#`1iR׌Ř’V-F%ZAw.X^sV"7a[Q{g8|v꾁qj<6]tA` .sB[NodLyv|3KB7Y4-EKs<}eUD*e:d[>w?|8*hx1h~ӇUIAcF\dϟUV.w|9u{0k=[Lfw22)T0psDEWu]T*Oׯ_ 655XRDZ.5E܈.ufe"@II'ġc6t>[x|t|%eKvuFN Ӄ`m¿c)4ǹq.ke(QSY p!0>/*`6Z]kqv>gx>øpՁp᪃׳RQ6v{h.@,l<_$W@D $KPb o'FGN9RDsDD/@C4X(BE RhJLM<;kSvTh/eq9>gm1iGs <:p~(Iף`__~?6l6HbXD}q>E~*pwoٲvY@ၫF'1:9vBH\^49Qe=Xg/^UVR\Ű6PQeT^q N g"M=[`6Q[3<ޛWnފ;כCdqe0<DQ!YQ)_r? vHnRC6.o8M|ѡɫΛb}ZRb;7?9=鋲r?"((=s:7O4 Q.\!9u\Fք*nAL) @uV-GՌnÔ7[6͂NLMqǻ?؇]L1cܳU/T8b/S@Q Q.S u(CeyWAh\X,0>vpwNtZdI+24sjv7w_\.0 gr@)$rp@Jj:.ii¡ah @ol46Rд* Z|ͺ9{?ʒލ_|C9ٻ,FyJAC$)c!+ e?G _g]&Gp|6qg9+%׼\aL@r/L$Z]>f̶Yx؊KzA^Q'7Ҍp8+0q#fFP1hD B*p3NY9RD EU!m6{ u~/2<`0mާh$\vխV.z&pV\k& ɨo ?gYxޓ [X~\rWYN17l(ߵ$j1jb,b`}18֖trkw(cv^쭖kAIPGhc,2D`IAShm4g`2J<ܲeK'˺DTi,wfX צ=9?bŃAb<`}_ي䀒ͨ}vCpC0*0*eť; ; p['<-1fNW':̝xK˞\\(:/kj pիvЗ)y D@cZסmV3MTq|2<*b⮎F .ቂ^3a `vڠ\ArBI 9MZU zf1i@4Mı1_N}/z ֪r5Uswr};?$w6mS-.xlQѫQ|]ۋ}&}HF.]J*:?uy!.:qcE'~sjw߻8x_j؜_AB^wbeL4mϮdvIA*P=Çh9x81!<`ZWl^ըBSxaWr Ek'W{Tav4Wq/c6&fK5;à<@FD H"qA6HT^I53$yv$+a߶jUtq 2cXgFڜ~:V-/mjP7y6!*~V27Y+wTXFaEA# :&ʕtrjn^FM5++Ojxrێ5~L9b f MDSu]Cggg#d2*'*P\!!UE8%SKg~ZLZ<'l?/_~6≺Vŀ,t'Q%aT!$iʖ h\gW\Ui:q5J콽{aW- ZӿSl67@Wb~'YCޖwxw?+h\FZ]F3v7Ze"6DTP6BI:lv'. 2\a.; c%2 D43Yw[ZD,ŤaZ$U77aFW~(vPk{˵' pQE4-ЏkL)'W,$O\kp^<@K\֒n 14|ti^oO]J} 14)ϣg0q ޖvN _ PXa' _jn]"XkKwl3@ :ƫl.ݸKA "QBqa 䅁Y'l!WNBIQΊ7@.`"d2 M䠥k3tzf'<@:t"Ѫ<`6_6L!e4j4: yݜu<H~Ӝ˝w:+<CGqfFڴx/5 r #* mmmEߎ1 *k6V YDf %3PQ ւR)+-h b3aѡXlu8&o p=='_[}lOƷPA8$pGx8(<Gβ/9p̆9-F 6ԚZjUD'X F Fʄ# 7 fJ~(CQB3{oN/tPRXLڼCpP2,qYԶi>h<4䆶5w:}—WyˋClxUq|wyP[-n<5ȫ? ,f ֧Ũ cfKj) 585Cٿ 2dOޅtW9ιl=c# CiAE MMc }x}&:^oЭ3s:AwZjVgݣ۸sJJ(QiQ-Y7c׶Rٮ|ic7:+K{s9ic4vbؖ8XhɖHH $!?@ pH33{س{Vv͊Iatް^η>yܼF;f0 , ?&\!x`XUu0(<[JF%u3wZ8/M!<򰃆+A>7V/BgV ON&11vVFGw7c>wn@@1&2.5&"@b`UĒU֬xtTXksxasxgYΕڠix|m"8;!>R\.Yvg/ r9NfB"pA̭4a1 {I {I D A%UNF ;ܮ`,|ǀF\ _ӊ穌Zo07 cRV\o7W IO_`W`XaFkpWSTjWT3 JGb0Tضn_QCNx|ݦ;бʽC[EBbB,$`$Q*4=/QL&bܞ 죾fc޵?mk?頵0hhk4tݚh'̜ cÑшgz(d*duXy3~<rɷp; w4+}:xpX^(Xaz~l)A$A^Jm}cw/}ݖ>ǒ$Zʨ_ 1\vMh3Lc ,:6jõQ l2T`D`4~ ™eAxWfpڹ G/PV܎LZ-˗80::Jkߊ b"8*ɸ80@DI.B'Hg̑ȠH1uް`2Z%FF+>ݾ@J!]qB'p >>.ZЙ`hjjA,] ȱba}6Mj5.;DjRF!9>|x\9 6$c]~<=ԌT#'~ G G0<$UC%@& Pga$ vPZ!c{Zq7e|G ?P}1Lm,6װ 'qݽ0^<6{9ⵡv֢\Z< X<(Uqi) r<+//CfYQV%j o=DUzCA|-Wz%Zo=ew:{-5_N]=ABQƃ8Rw?e';vq K{{{UTpsOLﳎ#0BbP[7H ˇE4@>nm@w<.)>{4xpGNI{}@p.I&KH ~ŧU8C8 2qo/5H!_S<䬳;arr ᢬K$3fΘML봺Yd$ XL xxd$@8ٻV XV^?`S ZjAթ5>Fm1/ 7,5&0H+ }v<}7ٳN"};0>sc\{6׳+V5.% T6U%Z /eX,983'٧v 1ޞjix @زΈ6԰ +TRRSw !i eY;.B><LJhe3#xן/*5SI$aAnOΛ v{qDذ@x7߾h5šޱ^N[};FG^/ "}xE ?=8ϰ*nEY q8DajMu9=}adE,^|1En_B Oߣ$"3u?T6 dK@w XWu6~x @$ШaCZ. |H{> vsH+1bj=ɶl*B щ8F$$rP2/ߛrnfDܶRv IDAT }>f3`yqܔ 2r16wP?Noo/Hw66ܸ L}@l}VA#B%AS:C׍A1V20 ;,u- !! Dl򭭭~RMMM=b84ϙk `"jr3[EVHrѮ UA>26gKlbYYZDJJF]4pMoe+ղk!0nlj^Ѯ 86W5躙y'eZY֎izncwzxxK M,nCS{" ǃźaQNg.ڷeee~y6(.nrǡ_>Z~D'9Y/^Z^I#s\hg˧xօGn#k 3i<3jaeB|*|2> 1P ;d4~˨V̹z}xs5%P6&nd]l_@(2{} `\.lB.7+QHhިbs-\,cn>xZbεW èSs:EOՆ7[*d6>=B0 Os13B SK"鐄 P*ACi*jD"<)|w7lKZtaU?ebQC ͥTWRJxC#uydW-˞K+,XSplՏfJ)!SwΚ>iv* Kh$wa XF*@A%C&ǡ@8I">XQ,}I*jܢy im+ x6g< ""bEB,$ WPܛ'1݁$IWբ<-@!e[~&,i/#ʼnD2u"5LAjK&F@dWrw="8dǯnOWԑbJs{ߤ1c[A&~<| ZPA:m&]\?)c0Ts%;g=tv9 l DPŋvH ANnG8jrxt"NLs3͵5)9~c{K䤳܇v|$ !qW+VN+4R _Cjd5Еʡ+CE!2 EF ha_;JXxTWQJЙxEkGK!}uks)CPs=;()Ѳ~~CU.^}Պu[n{xp{ ۝{hg@i}"h߯O7g(Sa! :$uлpl9VMϻj,!] uM-8y =&Z49J F|zYzb[=`wz ޅ|>%hДJJ$FMpv\Hdh$RJIeX%S@)P>VV <*VF↱fPZ !Db2t~o] `Pd 3 ӹ CL\Nxg`#xpS9^Z;TƳ?^I_oEED"#/f*B!"|zQ`":;ЋC1ś~Ył:.BK導{BNȍH%Bw}0 Y~=R鲟Cww7"zL{{{sJw.vD'|I-)fqzxm *5QWQ3q#YXǜ86s)k@KE P\ovm(I>6|'B&O&&^ϙCz3Ҩ`@8dnj`|/AsZf::#~n0ٺJ)ڱB"h h H&!-ܷw[{VH(G,O]@"֗ERVե8ctCF# PZcn@OWt]llPΒ plx"RگJsuP4\NMMug{}p(nٲeYn$Ituuћ8aNzB!xB)e`yN\$^Yk$Q"C&uJ>9‘Q_=08ܾܾb!!DlL[4@CD"8N cppV6 vHlii/BƗEV)pT7  moF#ӌOq m Je- G-裬`N($At\^PA/q@wyzNbPw!x;?܎v ݃ߣ\Bgka2 B$DDs3cOhohK6BdW>Jcw?kuc*%!rRiq8ŧW-~ 5) F4jXAB̆E|f񱣪[ʫ>T0LJO@/' !:ȎP <*f@IQ¶jԔQŷ32uyxrpԛѤ5v [ԨH)eBxp/ֆ?ճ^%wej(+BIp: ԀϠID@`7 !$K> ˥\Kuփ\) 6\.8N$ E!) ĀI}vZCAt_긄1&h&u"5LAj;pGOw36e*|xMV=}LE?뱺B ;,Y3gm<d"">"7*YqQa"m8 *'^\+ <]_ Ӵ˧ V>^]IsVV¶fmFlX R d h,$9_QȦtļ"kH hṳ-ăw6yNU@@։݅|fj6Wc(u"/ixo ۸6G>J)qܼc`.o=Wzx8}qÖ!9Ս`zOVoLo.bM&km=ofIRH$A R)Tu;0hSADB M)\HѼ0BE;iZxwQS/~J;D;; h}%}ԒVB}Qb>BY(8#V+!R+ -OH ˇޥD%ǖzBVJeI ?!"QPѬXQL'U- uÂ]a){P AQtJpE|>Vj1v?g Wr|dr! Ícuw`\V*:OcHWf4GH,u:_2F C SWfhMf#(]%a|)A}=8 ~:Go=8v?q2۳߆ɀʤsID`x# P* u=aFXRI3dÒRDP, *jrS a zU n"_Jf$N K? rfDp&qfU"8~ރqVu`C4ذl,*"_|vw(daJC2^zxLۈ:G9.^B]mƂkhxG/\(8$]9qُ5ѮݺUUyl0!m N D2!X?}X+!#7pd+&lQ-?v@D88<~pxvoZ P8PxAjgt5 1GT Vsx) ˅HȟJ)~.E9R[kk +!S \w|pѺClmf; jK)}xG?؇]72^95!'Ϛ{3_tq im$]aCҭA*B*rmK.GS3F J4J@@VBM eOS:Ģq8]LBdz a65BJ~nv|'!)M ވJⰐ:wζ/B4{N>{tmi,kMҟ ウY24*7,@^sF.8htRQgxT%pCw0P}F&@"UJuSm^[slZ'U:bVŮ`̤u2eV煕\t+HLj2z5:pyoU`gcN],7,|@/UP3֭v(ziB&n\x|O]~zHτrP:lld8T*B%P~D1e BHDc+cN <͔?K@PWQ[EQw / [ccDo Lj#5/g| $JQ__ M؁jYaה2Lp lb2FO9 FA;rjo{nZtN>[Yz2,C3J*C&C|Ma\QYJ!B~i:q)g}rEb aWi%+ءS&p qm8<ޗSaP)Z pdpj$ YPZhNNBob!=|~V:S1}<.vꔁsy#5;id,L.Լu Yڸ@y,8od r eY W*DxJkC6Y[j|q7kso\0+L*nO0/&.sgS9K<,cX<Ԗ\Z@P&.G߻/؜=Wē_~1CJRWi _|25U 8O19O=E k'}Aj܊s$6 #BZnf&I8bPہp<*|X+6ZUQjfU x09pJp+g0a$!aTg:Rd5;.hRgt?҄{C ~smc*C.s]4RHJu,Qzj◎z j1Jޟtw  ioHnIv&l79 piP$t` uJtݴD)0T \\.ceE1Ds4##<^z9w&Oa!mٲey`a4?Q)TFW"XACUf09o-HA,LؠmojH:kF|^[z9PJ86W !fl &] $p(\olxtrq U~LWJRVE^u뤀8N~Horrna!φˤ}6tB'iknX ۊ08 D!Y ƬVf g>x<Sowo1'e($xhz|MlG*%A'osxa `N' ˈ-/ ^AW6N^+r{~~ Eq%Y XnZǏ~O} Gp':zhD[1'ix  e~Jٸv ;欕%JQ^^> *!%]!\ǞmoCU&<Ɣ} "w]; IDATB(,|HF4J@nN jS"P&Ai}]8(Wqg3w5DEA\>. 5Xu-Ui;|UhXޔ:EO?X92x\Hha]7," A`rrA+tp`+1J_yFu|2bFhA>ɥ|>RJ*)tnUbV+A/x aK_66ˬ/xb򸦜" JJaT QgA^_ue]h (%DLԾ]B(NoRryW?MPI/ƽ~/eP#VB. pf͚_tO( r(t`2tݰL\ ЃXH ؎cNK]UAf:5$!Q源hڜd\4`evY~5mh x܊cOzB@=hk4xGom];cס)Ia0Q|Qb۸8ǁJ*J"J"LLP;7N? vŨ#k.uxןCS[85G BE~RUI0]*_[y>\,DݛVؗ\.4*iA;W*^) κS+Čl/>Еȱ{lDv"D>զ'Slgl/lPԶYtH?ڽ `#V5J䔃pkxc8nøÏ5u܌{߿>zH ju]RZ Pq􏌡t;vDgmIA*`M`alaǎXesT: D2=g8aUz6Ce{ǮY/w0ш*Wi 3,׏} a~ Zc͌xlOkűb5W2 Jh\:oX\"[d0&.iU'Db[ukav9iNf[ Eb.J*Q"Y\,/xW׭VmqVX c|H_8)!_rheh ~4ZY`;ݼ쳟2–򪩉b F@Zk_0^|^NH$ &C "y& ZঊUnlrV˷Z] Bpx]wR1}U*t:DG&@Ҟd|"Hb?"RZT UhpBزf~'~_@Cr yB*LD,}r?7hxK։K.1$ 7^F#Do|hղ>GV)LN5)q9u?~ly{爛9UiOR,k&(K z&TlCu;Am/(3c|1g= DARHtt!cb6nSưZ4 WBJ ˨Ї,sTNCv\H5lU)8w܎1;nt~ȆZ@xxeXQUeZ]zu0(Jsm̆NeIH>ŷ3vyJԎ@5ع(j[ƻxr7fֱȆORÁh,qP P ňcGb i@4`+ !le0X:ù3\.PV(REEe ;,YR6ARVnLg~Z׌G.VZ>sA)Zd:ܭ@FY) .:@~N^Ns@ xPI(DPIt8_&[* P*T~` MVuXJ% *h}A#㨫(]6p 56۰qmdV9bEUw70/i$vF ɒ½p{xΜՍ`L޿:mIءU="8K.l[܏v4Q2hGIt_i5P( ck7G2m_ُv0~}}9Cy= B\" .r֔|O?.H?b:(ʲ}t`G'a((+FČeU4\YC<.R1^(w놅3HѨOBFa eh(M|)FzNLx;Zlk"x`R2 <tDp,7f-aH)!ҁ֢ `v;Ɉ[4T)!f'-GbX,H\BnͶT DVDh7:/Rڇ aQB8Pr %lGʃ.^׿r5jjjvD *3[e ;p2(_0^ox Gp7MR[Leˆ$c~sFiVѢ(R%pv_ ltvS4r*14` h2S2]MuzUB*ͫ hk4y)m fT5u χmƈ)zuSާшg'tISJ,V1s{xgTƦ/Ǧg9qG@Q+>OpikuV+Ǚ:҇0:0_ͿPr+WVbO*qVt1_)XաQrB(4%$ W0wp1I'C'cGU(n..' @Pв&K_+bV+BVuP E!-eq|d8\T .*׀ӝ|$˻n򳯭U)c$}zQ,N&_p$ ~Y1DB>">;,i%2h$R;zq0  %0 #*lrP$}Ψqu`tdq?N?\UNŞHuxlo3jac{Z"hB ?w2"0'8n:B.B(`/gɿt1 BDPC߯PL <meu4aʰBx7\.T2QQ=X%<ОXZ^Z+` ЕIof3`v҂nne>д^yXQD"3_ۆ;7wy|.*+TEu:h4̸O3xVX!|y'qk_O I*ƜOニvŝͨgKoER߳p\M :XݞB dv8V6äq 9 8Qu|ȳ,nFqmC^T`:Z]Q@B Y9f6f)p 3>jd[UiV^04)t8ߛĢД-i¡S3ׯ Gvkˮ xyЃջbkG.r`jD,YBb\uJ,dtq(ŵ;bx՝XHPz@Ċu "vH'?%ˢY>_/ߌn3Ȩzx?O=E VH؞VmokC8sŶc~s#j:VP6q̿[\a-*`(QJ:\6 o25!t(HƈI+r\ik4ޤ C)Lb8~0~;<Yw70~ڋBk!u7CjLnvtS:4logӲ1Q35 f\42MVF<#*DF~rAB2RCXh`p(Ź@zayayY HƊXՊj=zrY*f!l<KZeQ >v7-p$ho7]H}*HS0ϮpwVÁA\(d+@f <^Dz8L =؎f<\QJ &!, NN]^vu߄@rzxW簧 7`{K-z  <E!REY(& (c %!>|Ô1\X搛rk;i[m >+V'73.G7@&fn u$k͋n `긒;$a\7N`0c7D5ȪMV4<˘_BhN&ZlEχH&rZCRUTfI Jz)Ǒ$;VƊXJ ~4^OJc14,jȞþ l,z/l.gD=Iq}Hk_yNfrY]".*U D|v:Yj !#Vw`2:o; Jn}9><;vc1,nȁĆ*=Kw4':zcUv. 7 ~[wd,N> `R)B/bB &M$-ޟ҅8Y&G٩b\2<*<JtrKP,jňB@Y`s׼ڀo-w,VpkpIa~YxH!X'kë:G|7wi=ƍc(-L+Ki>sKO/*vHxэxYӁ@ U]xNZjZ3yiww7"wRKk?!GrR6G}))Ïh4kc\r6}e?J뙕w{{^t/ߑ@=8A{YZ lhfP`q=po/2Yj߇*<.@+;ޓq9?9؎z7kǥV.ȝ2GB(~u{bΐq(qjշX,F \B>F VFhg&jA|x[e"u* [BB?x:T0Yp`FHaGw7i㋏lnxb8vvB>JK$0R@(\|gw p`꿽͈/?y*[*L+Cids.&}c밽@$XnĕV 04J{2Ja*Z/ ̓~`XLƳS;ez 6|;| a>5lšu7Cju&H@nv裮}CFv-/c{ZyLyBcqiOqÅP#`rra)P"D#aь U!ݨl>/,ng՟T*-|X(YV^L=D`q<. y\+fǩyATUonwɎiǓma$|`;wFno7#QU A8Z؁F#4Z-*++Ѹv-4 4 <4e**U x6@Xf0Rl/LcY 8,QH@pxU\.R9,n3-l6 !cn7F \5hX;hvZ8|pariWaCWFUtְ.p{xZj{'nbB208G$`op,6TQ@8Àxb~&Y:s_L`Pw[_jE}B8=~Bdb޴[1hql/XׇÆիQJ>!b!jŊBW"g1֖Of]Xʂ~Y+y,2b+|}+G3K|WxkRfjP( m,=iM8xu|cY υѾJR`E89ُqzYё2V(gw{ '͸5­a%( /4J)JRݿxھqqo`-J(*tX螂on }%*9߳yAom&M\ҠQBH<#3'& ofŪ@(8GmE %F~rA+OX ! "( AHJ'K[!@+r`!c~CVu٬XD`"=y.Xne,5vouj"lԳ? 3-j8LA̅!JBIVԐfAĀpmO*JW "?G|4T ^,n v0! %0h1e |UK! ڐUCVVQtݴb![ H&O7~xdlWmx[n\0x c ~Hsukqk_v翼(LJM_܍wVA_@qv IDAT_===YXlz뷆]j3 0.wFC8jKQ2bB5T\5Q_JX$ p}9uk[6Fk~u G niDi[ߓvtFFM?TGs>~XV`>և͹|dSc 2c'* YF4<1\jШѡ`g7<rɩNlbhh8kѬ|< "RAh67~(8~ eD,jjC0(,2/Ѓ92yPW}_\Ntpjm!u]x^;},Fm2TRvy85j޹Gqw %RhQh+bRjYVm9#OGfn6M7ifȶv$rݸĎc[r*r$Yd %M$03pI)bp@9:@;w. 6xsՈ$ˁaSd@M2>(r22y Bbv$P1b2|j#]oMKspf6V.١Re*ܲayg֨k1S/Ȇ:])X][Vkl 1ci <[Z)qc8R)> Z_Wg/zcENP*٠]RJJ& ^Spwi3cd,zx{yQɹRbbk_<NQeAQx#qF#$pfcJ\&Xόsν?=ꏡ( A V\Y>{l4xFp C7`Cv*[ڕO Q/g~c~ v4xFiL!,Tr LL\-̘KLN_+[JG PI+s.490PJv0-.!T1 Q 20S]#?t9PZ Fc"# $b\y2<p0pn4hb;{ եP]481z ]W;qwkjݭXsEuXEqW֋>?0PI MF2y_´+ FƢ8ыJ Tig(8P,Ƒ.v >ǏRUa.d#vȾ] %ecs;=H)zHOܽnetz%ܲa>s] GC8>q\3$Km0=nY^.?<\bP|Ժm08$!0 %o٠٠aAոW2%ΝT*~*P@VARI ,'Z 5_ގoyNbBa9=z]UH]ߗq|Z?{93 äRd^C:܍pXyʕlooZ\#I|CZV<:;;A ׭[W0Fp,n4Mf%vuW&UܭsgP[#/{FYá^ [Gq#\-'nMQ(v6"Ywf6?ԇ: GYk|j~{&*Y$"C L%xEp@QBÉF eJ l<ΔcUĥm&S깟I>u1 G")GF$!W,|\.Wmrm8`e`osy F4*<_L,FJa.jFTXb7a^!WEᓂʔT8Q-Ew v#xa@T57w. >Gesx~SEJ o ZhHus,('fEYy2ljK\vaZ G;,zurA &Cv俾.Zņ/{^[E3XXㆼo36G%W3toOē9mP:-<)V. bYZUPZ`*q8:{|PI+HtVw%y#Z Z ፆ'̆q1tdzdDϷrP-62 $y\Q_Ɠb6=* Pۚ}xϽ%ySlk*džҜyfqbHy9@Hpp%Kon~ 46ZxM^VqZvK E\'J8aپ>/m@'XEE՗z-rcQ2p)(.dLbJ0_Z0k //Ze0s@lG#o&o?ªemQZ"/;!9)l`AvvWå7A7A6ƅhЕ<PgxJ''%M@'SB:q5F ^KAV)IGb.T M*@(Va2ܳ}t>ASkG@O bs֡)QJ P~^2 _~ e 'lMMMD‘$8&@`c6hQf1 N qȼp$ I.qƄ'J*Ew;xώ+ uwx~<}E&VVA{s!Arq 3ƣw( U"'ɅP2Č~O_V+0itШ?Rj7F8)3金ILN 9;xF 4.k[+mzIKa0Pa˯a5xnɅ;:O 5cNʞYt!5In6(oY|_񉼊Ί q, σXpsP)ЩՠGv _:7T:`tFblȸ^CDd$SO>I:a>B,u`@78+xwx]pO6ܲqբ1\ ,Ɉr)0laZJr4Tc}`ZtHd𩳸nF`2[:^R±B`a3(yFRUk)uq IJ*%ˑ{=pIBvnn@2ߔ\\>479cj|ֺMp1Zq}ph$ | ʮ[FQ'*GQTR4z2.%g|سq޿{/ED俾.XjGM|ź;8jpԮ,H;.f޺eC?5xrjoXIE I,Z rX.sP`ZPkܕ݉j+ BI#dbJT[Ja]9M%'%O_IN:NcƽԢ M>>"W A `/zy4֚PD[lm3ifȻjL |?|[zqOډ>9z6T;6:Xk,w=/;0Og>gE9sl߾KJ 3Ptj Z:5 +sgW>_VGB?v^rxxx.<ŧ)\ xaxK2;gvi薍uE])>P4h!BB)#z-^^KJ H:@X܀a2qe;L˝ ;^xG͕:ώWOaǶس1">h 1XJt(/7A2o(44ùexx~_T6.xSi$ E16}l.'o}SݶAabjB"2 P6j$. HM'ZUht8A1%v~vO1 -iCh qCx 4ˢ?<8N.v*%W A*HaYB.kd%HGQg&CTf8‡gр?}8΍\'lιv㥶^eA E{_H{D,հ8p _ˍ7HW@sot_ ;lxɍx/L Y4BFGfךFxy݈V)tZ /`*zP(<TU0 ]x!vYNs>W:*;=x<6ܶA2!p`8 B*%vX2" )`F4,KIS?iБٰ8EӢ[jE湙0\ Xr:0@S@VNClBK9pJ pP@ ?xl^~~uh> ͌5nUa7Lz-B=x[׎dyeOWʫر{v7$9I"Ib`0J+>?ӆaqI6ndq~rF`H"-6c4cqD'L56-J{h|Qp ߩn k_P}mT>Ow^Y႙MI dAzbQAQ* FFBA:IF8 &4:8ThY߄]K] PlBCGMvgj0"|pv1"')A1OP_~U˫n H0X.M-dbyC4!0]" Ftibr7&;?h(gEJE(~6sO j0WLz efL 7h[yo*Ƚ#t`8XLzuYw ZָO?tkyp̓&'vl[ݚ7I>p$CQejk ' (hc`&y[p .xke#aayև&5O뚄gv/Z{>2b>a W o% _{7Tέ*|ݡiǍK|}~0qI5Z+6 +4p\!"gTp0/!NDءCpFPNśܕhr8hwjaqPIy~Ccg\i@x\Vm,X#W AJ|}.%M՞Hc8A`^^G1޲k>P5#\V]^ raii5YxBk1PDLAl͕3]?P\B'7PA#'3 sf&fd:ա yӊPR(Y6B z"l?ع^\$)v1 ! fѦ~'~/pr{سlXuΆMfG e k^=[s'&2@Ԛ~QѣTo|F0S "xax\Bp(RVw%.4: =ELo@0U^T.sGe'nV[ m>bRBB}1:2IsҚG7>?wՠTۍ飛Β KA.E#Ъհ0k?9[lkg ۴8ч(6s^g+1NċoծRx%|&*Jkj wC֛Dk IDAT?U! i~|mɇƛ;鹄/Ld [660b "g3h\j\y\#` 1㽿')~c@lAc  :L-S3R ^I-EP*J (5N E,"6IAp^_݅q{Q?W^m+cǶ:ou/ñWn{Y?w ZN4fT4, d *5 יx$p:=>$KBd7pM0PD*W6'EDYZ zYJ)g8nb0a#W Ajr\mɵ >o[/]`IQ o nf]‡hrx BZ姏nFÈ麪-4c<,''V- TGڕYn]ecw4O(~}b}!>ӖR|b9><EJa$aZ]u.kG޾J,BRT*[ FEs5N R=F@1!\s_Rw귱f :{gщC1t!6@g8}ګ ˜r@˦S ӯIQE/]U:ⱆ{) ci@[Nb5NXLz0\J6tdA'X"!tƉ$J']0?*%F,&@1 ,pa^{qW(9l`[+"GXSg'n"gD(J6UV5omK85p$߅8/-G{0"stṇ7v)|X2nφR|]?q_~؞O׌SәjQ _(!~pT0f[-VK`cYEYKr kE v-kp h@T`t<(\IlK(>NhAieBz1bD r_E~#n/t7?8bvKvșB7?8#8[Wa`RM̿+ǟ@@ 6x=мh MU#K K@VިͅtztEȁdr}QbbC$"sBk1Pb'o@XJ4ac Ըm̦jkVLvO A479-y>4g}XUc˻Rю (NQ(ʫC8ĹQֹi<C7Qx;g&&vqC>~֯ 9NIAe\-q#{#24R0C<[Ӡ!I' a.;IIO#t)/uGxCg<6 NL3>>qFs)FgzEyUINpE =E"I,Q|@$>.@\ۘSc$TE6E zGi_v-03aXjƼ88EmJ B|RvnT׮]KnB?( XuzXtz16dJ[Io@c{FRJ " >,/5R)1/1Te blði%Ϥi4c .?y(N&|483b}x2!ɑD K1CVæ7A HME :.zq7Ͽ^K jFǢK~L)Z9i{VKNm ~ b$3]hٺnV:8h1^`O-vc ᣒh[4:qyj5RBVMmS3~2.O3KHt"$g1lVb ‡%eҽЎ9@|:?V:,[δ߲ƍ,!oЃ!0L kd,?>ᰨ7n9p3gΈ*/FKT ݧy`ܛDp@Zw`f|{Ƴz_8nȻo+/ʫá^jJ4" VH_. ˸/KN0GMWK=\-wK޶3QRᔼc4BNwi y`CN"I-ιR`1NE| W:?#wL`k*,3Tлg(`Q>'4:h;D8DZ 'mJ^/GF!_ga>/r9J*xp̨+-g\܃K%9|sOLn+#$FBɼ6TbCuKt4<OJ$O Wj(RNFJP''ӮV 402ů?w .ھk)%#Bd2Lb`PCB,R8#B8/,Z>귱 jt>:؍{~~&,ɨ iwObf-Fr-JV`}0T@hr^ޯJW=w{?#i;wݴd,&,&=JVL|Q^ IG:@ 8EH0*ʠRfܲƍ5nEO];׃m| {v7J^`Fe6Xs E)n4bdN'8 ^Qm}{`>9 %l4#`fVƼ/য?(y=Q>O{vQ-것K2s->a0qqn'_mBlKMP.Pc=()#MH|Z "`tppᄁ*D•ܱ1NVdT[f2^)GCGލ/|>2 W#{(@OAnuWa 'HWOC,z2‡@À jX]Lbɔ$+@!0"﵉9(z5=E]ށ3]:=lz(I < ܼuw;,ċN+|RM6b)1Qqb8#~_>J`fO~Zwx$xy(("g͕pi.n\L݄P* d'B:X$P, I'WZ zMNA@(NTJ%,&1q ùQԸK9mf|7WƁ]Mwue ZT*PQnx Q:I!aqY >28EшSe) gQHf-"疷Zpj9h`uDP`v N_$mߴnAD ~> FR(V)I,JK2pay|?Bta`8 *ss8GI`,GzbV>a<:u{!L ́s\o[ǰ,h cƓ'L <vp\Nǔ}!+Qb)̿.SF0ϗ+WN6 Dp[גk4TĹAٴGCgk3#p"e,G$Π"'\efحFQY k)T: \N3 ,^wݭ8p >BEo^οx/37䞑8IY\#?݆HTxo9p'ANl,FcHqMhZn߂_)_tݿޓp.v}O :VQV_=/y"C#(pT౎E(vVpY_vok{5ϾNXTw~fdsr O9lrWfvlC?9SLPL4~¨Ѡvϒ<(`,9R,Y2Zy|\.Wmrmcj"~::EzM-vj=#1|Gw7P(6òb'''7(릵0!TŤ@$bmG@ Vb`1 IDATKR/E8DӚs0 ع8p >x<~>-ܭu97ꏡָ|C!q{h8.pnrmdC4$iNѸ!,c8ädӯݸE  GjnM_2+3F8 YP- g:Dv#1^±Fk?.M^+f<Džhm)xP3:#UR49huW6pg%)njpDURԂVw%OL $cDu!Ʊr~ϷB! Qqµy2<@v).5`¯᝟Y++'z(H|Z `<@a7pM*eoJp.0rU`?anŀqht8kZ{Q\f!E R=h)5܎c`‘`2H) Ѣl-|_V)|kf~}37LwJ{pJ\vuZrZ ׭^/JWa CrgʥNT BNH-zaXRI63y>H!z`&vT9և*n:FYǧ~*\D QHv.x=BpY|9ևW^m~~5UKPsf9t~a9~tu Y0bǶՂ;1 INu-gyA:}IYnZ ºU.]U)zr@pBBg8#p#Zl 5-ph;Qm?I Gp;H%wp_,2`"4wσX9qkc;##P(@X~*z%r '}{oj%u{`A(΢f@HF |rL,6:Ozb,"z{Zp;(N]pxpwkڼDv 0= +DBSc \zjL%ЩT#,v&XH_~#`rWzc$lY_ ?Zc֬a{j@ ,V:,P.{a$N3|sr 5N+ttбs}#pᴙ%3)De*x@QdqUVNkc8=//~n>ÙeY^P 5Zq6YP-vcXhwnՉzNu_\}BAv &U>>˵2vyH)x4,8Mf\Հ=]} ~n_68}P'2X2jX]v pn ^ #E!@X>õ1Pyx05gVb@:=aRnA L|CQ>_"_"aZ|-R~0p`/bn0:q?I p;6`}j Z8lZn{ Z+vXHEå ]^Gt g.0m/ˬV]~Ò`IQB@J(þ`XnG Lz$uN :=_xIR  eϏAxp1i?  >F;`GP; vMpV+E}fPB Jw{3ͧ1~-6Vo+XN @QCpP,bn098au;AA&4qzb#&ws6<.#β7\ɥx A&Q M}\iBBv  97CG&xހp"h#$`74I@'Sh ia Je(8?L!Z\'_Su@ M hT*.-6'v^w/] g8o|y{m`8EE( fP&,;-|r|P;f ~t#+4pS$.^Xljsز\gn{ ?yQe h.'M 8R3rš]>RkiQnyx[odK#orqbB!KKB t3˴SڇμlM;l-o <Lt2ZL $iF !ĎGmٱhߏ#رutt$ߟ%Eֹ}^~2hlU28}Eb4Ʀݨ*kmo݇'xv P$GGw(zD 3VѠ۱=†(h#dr2<A]%T9){:P̭ljY e%Ko#/_5Se_eJN= Q} ̓L%|h1U+S+HE'2iC).sAv= T,Q_E$"uU "tb{}RNi<0(jTi iǖ&<ȫp8{2KY/v@ llqB0m]7xQct;:Օ5G wOGnjAÙ۳d,Y0 &}q+bq[M6Sy +^Yz1gs1:x4g;¹0{aTEaCEddEZtjV_}-C޺8BD%+LOyUl\Q453H 9;],ʝTr8P)nr <Kf2wy@{֢oʂ1rXEkv;۰]r29)?jTSBZ &}m71JC{fҋ?xpy)x BaIYC2-wlßWyeL8tҌi}jZntG!J.sðH$0TЈ%_b2E!Dذl6 F''e:V]To! ^c@΀\HJat炇B<L~;>|͍xdk RvoS` p& >!5Tl!|]x1:(`i|ٓD@ X!LbPIH˕m̫7bߍ 9r=EX"z <b1ǠРPN.8rϩ>8,O1b=nGpԟ{f2塈bY3HOEƅd&k[0[LqAQ^F]ռ9\~ A*J@(``$#fe}MFКFxa(dbTWI 83alzDD6Ov3>֨PWYcֆ%$ 5=6*bnsh  :@] Yy(R>C$Fcl R N͚{Ξv`OGf7ѹv遢@tٜD\4M^@4|>~93mTjNo(s1:# zE&PWI? >0={:r<+Q "t޿%vÇo+Lzp3+v4ƹçpD :/_LԹK\$Bȡz$M7eF,݊A[[T ǔSŀ%b&x9-:tUpdrTFuIr"bDp_ނ*gzm7{ \XB&ƔWm! aW"HP@;d/D8b"D\ʹR &2`RnplK;^,gl*0'z8ڳ^l\|e}UL}h֨ $Ys Y΃Yف΃V`tߚ"PfVv blڝ"=c{EmAB*-YPPUrU5 \~ xqGΎ^f47г_|f<˘oٱ_ ͠Wsm-q:a`^Wysee8JΙVאִ džJbC_]-h֔l[q|r,HjgMBGj`8E$H6+kDRE~q!-Tjˍz٥7dž0Vܾ\_,S::p)n\ @N \^ 2`2V\`xܖ DժdhmWcM6AiAش T$BW*_v|a:_m,H"P"1 ^blڍ֒2׋bQFZ_ê"JX>EO<˸ ~>u"r<փo~uƎ)3tG6-Onk_=3,涁 \ V$n Z~w^?:hqE"bl"r^^R:z( (  {>!p82l,y 2|w@Frcf%!4:jَ" H yբVB0w;F<[;.t6N@f &IL|8ٓiЩ$B8R(*IQ  GM:iش O~뾂dƦHR%Q(Z/B3ۤ-Έx\60zB-c[}=z+3>0;)$@PS+s'D1: gNuP?Tat"lN 'XuHKd Bdb!z5>}[vN*Ҏ iە*h`opbNq b*#7 !t%ش }81h{D4miDg @[%{l vexq3cO[f}[6S^v$FCyСEZ4kK'}ACBd^ˉ("r IDATzy v!7g(!w=Ť\TB!;`+Q+`xQv+eQHƮո{ ˮcU]i(w-kꏿԅڲoy!)>#>5 Çs~FoPL졫&WQY HPYrYcMw_^chL\0Z9GN?z<>@I/~Ki۷/)Rxwr=$s ia4TVo+[ʖ!M8y/\oYfb+p I~THc L+b&BlR!ΨRH+ ]n\0pъP$Vց|Z} [ȕS-818A ;]B&tcE͗RV  xvO?-9vjEo~='GAɹ&=ˤRyLcGJނp D;G6A-!E7_C܃xV%C&lhd90ꃢ'hRk88ԡX>t/˔+`FamM@8^ ZَfHyr6U If"E(&@`n|ryHֈ* gnT,`Q  DZXŪl]kY`E<>Lb=`E젔 &@ Iį;LB"d\HRpP!#aU!+r(KtZ$"516F$Vj p0\%JU A?ƂeS 7c{l+Mxc;T0!|T]9ܘ8y##~i @`:X=e%%ş! Qrw. ԕ״KdK`'C'8t L-[ 4o2NB+cC=6Ě!JJm,gR&?ޅ㫌!7\t<8yjBLMpOOEfwW+g0|xYwlm~wÒ^עP#̳'ȉ#Hne;ZW}C4^u:\}C-reٝ3o`⭃z 444{h4h4!|>_Frl\zH$ )%ؐ; v(x0FQ-.^HL{r`GOcClmaM@E83ŪZ4R޶F{e~tRYw]t&|9 GBF9+;Ja*C[mE+vhkc'.P%:HRB' gΌC70q ^c'@k hǶ!00-sj_:geTr 9!F'0a`p29@A%UFyFe E%5;Ӈw p^qvS_>T)f'[Zfĥ"L`Vm ;{ڱ$; Ё89?M/qZ:!Eky[rBS 7F?77- Z+PX?KD@ 7:PAkԃQ.M3C8wK;'0֊wb 0DqfҋG,ENSӣyr~ֵ;F 2$5ֽkx/gl@!c+0`sHÄ{,+|;b3U*쾵dY~¦cʼ\B8B8diK=|O7N Z ~{dN1ZLAQLM\'0'+$p G ́\(~4C"|;` -$hi֢Tεui]-c7#?p7ˇNC*Ꭾcʒ>Db4^>tsCZZtС]D1\" շ`qbkF4 . (!wHk l{vhq^ .xXp::80pUE(@gײNrW0VT23f ^wW@AEżW.9 \w֨LC&aⲧ,$"! X]~ r1 ˇcb<юǻä'SRbhc6u6( r+OXDp󤃸9p\iׇF7H `z0au ,߯9҉bh4=e/* (W|?[@@Turڹ#1SvJRVx78V ^d'78j`n&p褙TD43XY_=hm1./JQKv}漯w$x0蕬dHO$Rz}d"o$[Fz455riff4HJ!|΢PX?x Cl3er.p,T B^ɉa}>&< r"3I3/ Þ_}<-Ftѡ^\6@)9k333^¿,3\L*=GR8L?E ?|Xd2?.VUZ[/adӁxiă=PJT4q:żi_\ \x v?;w0DrBA'zMnhv$B!#>+HN@|Ztvkw?;K~/>DCMю_&I)PۣF6wM.zC8>sO9Gֶc0U^@B>+!1 Xd7n—/: HDs*F'pv2G$|c8(GlMV,ovr0O:Z3;Ӯ |0J0#cNG4M5]K&S)xaxaTWɋ.|{x~ Zsasɟƾ7'x fϸk}Ъdytw4U^Z)N7PIƦ]9pRxnݴ8Ƽ~E1bv [<9f$ElaCSS:*x8}4iy!A +ѹ*уB!F:@Z~'2o!g)1#P‡'ͬ:FE/>d9v Q__g4'2⇙ جD#TNl6U;!Ja 7#ZNY9K`1 ^=9Ŋ!uJ4TyS8:[3^llbh2ܼuʪ_- F$fA BUD w$ :D "Uٕ@X)(v˕O݋`N*dblY_ 2Ae%L+ auLJ=*++`ԫ ܳ{ϙ?>,-vwx00!5m9  XΓ`8)')r Z~IZatHg iARˍt,T74Mf^ӂD=0(jTi +tI HuB. >r!fYSD^>t?ϊ![' I2½8']r<;GF,x8.ťi+,K6𑡡 Ba%672YO<&PT+MlWؙ)iA}zok|[[~ƈrZM>NP(`2RI4+ ---Çbn{\p;0"~M"ѢTV68q"|S!L@ț˃5BUQ4Jm[v+$7l7vVUޘ-P45*^&geE Nj"x8rYkO|$o-;3"r ]FR/T}ƑX x P%B),:~g.>V+b&hDhD1*+! @(D G8&}ޫjC׊% Xs' /m_΋=33d*RzK!rz 8}B+B:[YGnF ߻|4+ϗ*w ~ٶ8{kR_/6u࡜fs:%;t2#|hU+@(/bt}%{ݥ5V By/Zr\qZ ~J~-471?%S)8"0!X4'‡'VI"#>#>dj :9;)PT ֏іjK{~QX&ͷvn?ɫjI˟o3z^8 ‡sX{>džX)s!p|r뫽}sP1;d j̓tحxމ kJ ^ABsJ65;&/hk}(شN?_9KN%pxsmovڅ|ǯM?}F$wUF|VfS*B^5}-3yg_MZpߝ7 ;62{>uw8Bhj?{t"nK 5B5W W\ryY݋x$ %tI$R!$!r1JB2xk#E_)`0,'.1}v8EQCXS.;;a>d*ﵶ}-ڽeY;:sÇ)N7L$sReDDpd0E;dyR5ܯ!WO'6WWW8F,`:&X?Ь7vaUAM܃F5+ Q;;LAN8)ùwNr",37T^ɜ/  !`/Tz΃î[ O@s˺].9"1\wZuF֯2{e|^R%{lݹJ Bj!|)Aف[?8|03[ѠÂb_l~U'Y 56usn?DT~W1:18jMu:]! wp ?yҁ-ZSR$pEX]T]+n |,2 PmUi7Zj8=gqyٟ`u-P(I׵N&|(Zx۶Mn>˔'f? B80-zN_iWxXY_I`egk*HN۝#VQ_I&^0Əycf%fN.WOx!ob v ĴTɼIȝ-xh!F'qfҋڢ64~;[Z0wCayE2bHfIш4W>DU rޏ-Kg?E[uXbh$O'_ؿCrہwv崝^7>q;ꉻ06B(/!p}l&Qbf\N ׾I:#;[G*w-ؽJV! f)OGg\LR56( r|}TQ2q 2˧3G T/.Xאַ^nZe))qӏ?;WkP%}fjj }}}leL0eJ>&}N-%\!IC\اV֬ǘׅS)I{YeY|8A uiJ#Y+A-"/U* w7aQ"%%bJ2JŸ 0fѲTVT@%Yy@\hX.,xI\߄o vP4B:Kv/j2JGQ';z Y(ڀ-Q!~9:=ގ .|湖Oy+#0 kz= x^.f滳at`-b$ |<Gy/xb^۳=hA֞o'ΕE[]+hu9vlʾŅ~6ϸ|_|nzrs)c }EfK|LEόԹ)Q|xps#ȭl @QDL +`PAXH2Z IDAT)x#xAee"1d"80hf@a % &}\p;HcdrަBXyӡH {~i8;zJ&+G3]9|[p^ҍǻoʺag<򴉹+nT J N<(9]___XbfIx} oPף>ӧ$\-~qI!ljD"]]][FfѥB271qw i .!ֈ]-0΢xSxzFb5P4`ll֢?-l`g<0a1b "ydHf SJddTTC"P-[xqm`?$5Q!qY$iH$SJtfB8: >b=ۡ~PPC e˒@x.`r6RctĠ`8&.{oUeHR.N Z ~Fle389N__= ru Z-LLp⇾ |^?;'I3.ZqmiiA}}=e -~quoDwGc^eua -12KP(k׮ 8z܄n4#Hn#}PE.@ۤ$ & ---DpiAEQX,pgs_izڱr",V7܏VtɉaMP__e`\.'dGTUpu<݁@"x ex.xJ!3kNav_x<5w'D*5uȶ5sfdm` , r ,Z x,/CTjHX6+F6R,^BQ}Fvy" b[Dr#Beq}Cڽ!-p(젏tօ 8"{َ<>2TYE+PK0;~^ur`B! `8h;81:\pTV=DVH,/ش ㅟ Dh4kEP mmm|0͘$8 gzog50P็H /-9J-4 rxLFiYj:Nfuյh?4WÚ5kEp LϩQL7?_>:~4 HFy}Ntvvb`xxgfM轛?@B#J{s^.KIxj|bu<o+ \*zlo̭+W@`6QWW#|>RtyF)`i Vة '{̂3^קּR[]}sYH %z`={$?j9J* b((R2]v"v lnht*rx޿8 F=GYAe%̩ zZ,tb,܀Ím/blMWF6.eW'ԭ&_*Pࣶ6b .p?2ӏ]DD^I4. H2q<ݞٵ'2P:h4dsEExgaw=$(hk?O}E$xG0 2LlkѬт㠓I$9ldɵ j-l^I%pf!W U7}vt]US{(S)xa10Ot3V)sڶ\fLzY1|Q Lzu-,!, PP \P>qwV]2ux:8(g &i- 'Yz -ho&mVhN XX!0 H˥A!c>^Aף.S>V͇N_p 2 lb};\֭#a볳휍N?+Zt9O Oq e`200 D$6l D9)k]vq6Ӱ:N(eb$"(d(y“쾌6V&Tf~^֭#ϣe**Ƞ@qX5aC`/tvqZ~Lds^$\OR^kaI#-8$c(0m٫ I/^chh.\`g{SC#_Yfn=d #So2BM*pNf0 e#ib͌F7sJq@7uVr`0pb̉Ʀ]x[G?; *|"t(8+CN?IZ_C9^d\ݡtI Ͷ$)P8(*D2?[(G2%P08. !!̽]cѯ 5 H.\!VόC*-ITQwXPI$P7\-G{zͤO̟LoDyaZjǏdDh"lQꝸ;AҪաJ#.;‡⑭ !z`pf,`sC> o$=Dh2h2@(C$A##Lbe_KD4^{(mP>* QT-'sG> [(þI#r͈"xH>]]]3DB[[NARȼe҃εu$! 1U+WsAQ iʏ>:|,UȚkxjvhhHePKe ;r8yA̺ 4E=׺JD 8tT<^4C6݁@`"x c7C PkDKg =%`EQa' DNAn@0 CKK9E𺙝9^۷M);ΎU7` qmAӧOsnZ#PY߿~Y fL&7dM<g-P*A*JdÍmً#_z4|;TP$ՔEwpСpbt HE5Ȕ,WX~xFj4ر8팴Zl٭}u֡y|AKK k#1h\Yx"ax"aHŒAPQQVm7൑ bSBYߔE0E#Bp-݁@`F pqyb[[ЪĴT;,Hq9m7 C)&0q͙I/֬`E9<0G9pw98 b vbazs}B&F*5D?698kM7붙)A=AƙM&S3s"H J!Hd y6"1W?0mcHvRR__jÍm?őEE;!?B]6$(?dg!d+H{lݹ8dvD@!V3j䬕% P[BueMp褙xhDcc#9Iާwڅ!V4+=ixPF6dɣ~uOOOFa4N}k';n 19 pzQ(Xݪ05gTq,< 8y$o-Kz932oahؖLzۡsW_t7z;kqӺy|cu^7\Qf VET}ة |4Rc"{({(DD<\ݞk{"v _Kp"P<`,:'T* 8R81*P QTskc}-{?'gl>n< LUn0>Ir#QLر6Le7H4I3Dͫ8;j^a:}]*6\Np$A}?wvluŦ" J:[ Z% * qwuu`޾};k=EE;ap2uúuk.3֭co{l1:A*e̓X]HvyPŪc 6.C´ڼӾѭJN’Yq;tҌg Z4s"v0Lصk:;;s;v:xE@a;' VTp{x=z<<9ygTp8XK?а Mz9$eƒs?_7G;܈SxԳ}}~s;v b[ڵ6KB yv뒾0r Q*=:K~tRy2u_ -!Hc`b"W T Cl6G~Hry,o5wVVN|}Pۙ6wϳ?Iĕ#;˯>qVDe`6AQmk.\.Vy@. \6CCCf)HPļ} ZmJE8uE_*j*+X4xUѐ c0|Q޻uw_8 ^@RiLZ&kL&.Ų6t6q}&LLwfg*ٝIIvԶ][rE%RLE 7 xy9<- 9|{yy|\R:_;0q1\F;CAYY$zFbc`m_ZZJȑ:MUa!J5(/)B]U)Zz4DyxZs*O? (\o_^FwݟTq(j&ikkME'ÃJ]Fw5~W;߻E[VOOq:BF vttP$k׮eUyBya~5Hk'nx/S>cƏ^ _@Qш~Zɿ$}HuXKqx©;7--/c!Ĥ{y,//+>a>>\@:=t|pyL wXAVOJ B~N8,2/nHeZ1,-H -CI&Ս+Qĩ$6@$1{J$+FQ;' \SYE/g,Ò+X=7*d*,(@C٠Go4ަZLk&%(٣:`DޮpttTra}vwTS>%,ԱX,Ye.3p`ttTWVW;rdftr@c8?{ ʮ,:h6{a! ZEZ> <yCKȆf%#giBMڵz(055%\y$qN!/~r$tttP[0z==Qf浌d82޿qÿU >4s?V]e1@$ʯ[LTZ\58E!gri-aҺ+D0ߺvv}p,uuuQ0uɄ?Ptߜ=|QRE9|aF=/?C!9cp|K  getwh2TBU~+aIM(Pt=n? loGGbIAjW";H8I7';=y6w:vױFb IDAT1w"-2/}sG %H B;|쭮| gqϷX^7P$x pB ,ݍi,3vTq=um7Dp cN~pCb,ew,"L!pq:!ؠ6X=wA+Ofڒ3:}JȴrDgA܎N-TEEp{TlZUr苵Q 1hHOPR Q& 9;{ٜN:H,Od$;Kh5jM*TWogA86>G4!8\ω9dkjP'YLڊbɂb0pQ477Kr7 >] |uCU?}sÌEwlGLн`D"\ތXl #&hDW?Ʒ_enG@ss5"x%'lknZY0E(jiyp3?(~6:||OOSP)y<Mrw Džgs9t(UXc05,=)8'"5*u\W :Eze7#Qĩdo?ak Ll$_B<7oHv>јYpj55R"T61iKHfAҩlȺJʤx0::[XXRxjy C#YD">m/*36rSSvE3! @@qxkCT==$$J]|1;Ny*T"`6/z=ɥ08===@8SWg2KR-E a|?eRO?b)HvS/C#"ԒLN-||::HEww7 y򄞞TTT NC|t7'lwN_ĎIR!!~R:NR*{A$Ɩ );:q{X N"2 3gc۫kHiC@2BH'BDE-kT*kmIߝ0^8 ;04(),8+.6UHZ3!+垱^;3oSDuHX\MNN2lnjE Db&O$ o8$u[n\ˏ<ڒku@2BHJqypG#XFP}zqeSUV4~I`bJU7cܞ\lKrM148߄32izqɋglw%;_[[[ey9\[nS,j(3@U}8`0>fs(}(/tqDjdA˩PcbKK7JE{];be]]]($[@mgq0bbP'r]_JC^RFG<>WDrfWHP..Kj;!;_TB?9D)]]]>OHvQ|+?uX>S<*v;w";t#&HKiWVV^*v__s7 lSc=xUsGb19|mU58)zxx|SPVCiVS3Ӿ!E*jn nu#ot 17H,׆>ƿ>">>QNb)* rB x&=؂" { éim4=!fTۉ[7$;q9ib0/|o>N{ Mbhtl"Rk5,5%h@ek:B`0a` qӃfQh>YKI3k\JC"PCKK$YW-V)5KQWU*4PmC_\6ӱz& Y1Ͳ߃4" @1A9*z=Dn;l?LbqQsN]E(qDstޅ^Kqc:ǍY'& gFflekWt:ES8=24Ff~rI aȹS)>Ao TH@ip\p1˙pygCaSa7 m5`'?,<Ӌ.g<;YA"9cFDE\,ڪjmCq0vȵb8j16ܹ?aD4X,yZX2*PSY LY#,,sA Ë> gRQ=1sDŠ$b(I))fm5˜P~,&H{p.")O?4eS8dN?2iY_Wj K;tN.YJbݱϑ؁6c3p+?߼}S](҈/bdOaØ `10Y',z0!G_^Fg?YzP hV29;D AJ(df]6lg<)}7]bRe?M5kTD2R&ldXyB& zoeނ<_$@|zsad։UA?߹YFyyT$ub wH$x 4C.ʡXGj,!xǗ1={BPx<x"!K b6A[,yzp)dIX)j͖s5~-(F&2q qC~6b>D0"BOO=$Q>uB_]84U:-Ԫ-+s|,qJVSH̩5Ccv\5TyI$ &J`~﫼,5'—÷֜]H3gĬiyð[L?}sPSO>҂/>|wĿzq?Թs=kg{gq`mD}ue1<Ry>1kkC5X lwkqrc :iu}ر߉y[LP8z(=ʸlfOcYn(rw 4A 'P|wy}k4 DbNO`TTٟBĸXA7x 2 3 'yaYV$yy8 ũmlwp"l4n?!Suz<LMMQÈ/)AEl׈p~gƌ/)Kb{g>cއ9,9E/_N[j W453sp0F !bȃƗN]͋9{z 7EĉĖa1)֣w] ;߻<ɠ|F̚vss轶iOJi֠T?l<مZNzc9mC8I4'Ď^56I@xug3{?\Yet8x =)V&9⊜(g-B 2<(5-(p&>{Lu|{d H1X=nIϙGGGH-}j{홶&''xcg2,:\:~:qdSXKJgs~7p{փ*|XVbA 7.e2ȻPˏGEQE>iXPYC|UP^R -c3Q!+}}}i=uU񙹼Swtt0\T0xǎ$cj꫉rR8=H^;߻E>+9tL3kR(;af__z5 6Z+%K-F{S s $j m9^"O؁ؖѢigqj(/)F ը7Lbm#Ӱ-2I !.y.}3󐀘HOɊ1Js;tɠ|f1ps{R˨`P|0مؿQD4'RE\dp_|*=?4 ˜ x L*)0y 3)@!Uq:&9C- WOR_ zE ExsM`EmN\v--T:4UNO{jKVH~oV &Imz-JY0:;;EeG8nܛCFIN )RE <Po,1;؁`vcjj(1<هZBCH‚7&b6b;FGGB`}FA 4 u/JTG9HG؁`y&8dʯp#+ӱ l_:G{S |9-B1L3?Ӕ0qY ςc= 0aYnXkj5-)E[SFyQ1 E)݁܅ӎpRIU8lmrb!ܺ5T72;uksAwttd}Ѽcա)cg6e<86=3[ 8|5b*,bfXˡXˡL2j*KQo,_iBkC? _Z|3`0DLag,vb]9>9=\~8J;w8MjB)chNzcg"& $DgU߷O"^]wġ5m1e8׫}֋{ LRB*$Z?SS]y%|0LBº6~ݔᡩ"cZ)E=Ao k <Ė` CcAfǾpXxVqcyr<~7)C;.}6! h'x([bjzxmZ xg00 9IAiqesdT%_Ƒ ^"Ro Dcxe ?/H_\ٖbdmF+Na5hX#dH*t(ӡT]Ak9l{;w7~i㗐1-pM"y^ZgLJeD#Ӱ-R?H8KKʤVJ)mwy[omD!---0Lie r:WNmmmi}4SQP/BeuUe7ְmrULR};_$P0^<?",xFtʛ%w"f՟&u,o~Ի?UBQ݁IH@ EA^[|fK*/߄c)w5)eFDZ%y68j55~?(prJc\ {MBl J@ h4*^BlO.68cޠ}׊wzXo[9Y+!(-F\ Ex\`<:#Vz=8$w2 xcTKE 1fϦ~{*0/))AyJА߄smFa*1mÞ={3E$4l|}݁w23NGO3wP0jU!,31z`0\Boe(!1!E)b[WRrٕRoB*$x ,pLx5[^x`1oSEN1V$;_f=< :ᓠ vyH| xe9w6eŤ><ӱݦ`g.Y12f1j˅YSz&1'c(-0զ`ϪC(ڤo:;;g#Zt$m!+7&|ٺKz9sɌp4\!7bGuyw8V"yABq8Z}}=()֦eNuӱmR`CySGbچ"E$!r׽h0@]l;y6/MPYx'scti(-N+[20f{1~n+b/D.a0(!q2s@ 9i*˺0R\ Ep8.B*$x  i21i[|lƸ{< EEc$U`ilRNuqM [7Iߞ577PXPP 5@A<h]-=iZų ũ[LSMkט/2*)0rf1 b{:::`:!+)ƽEk>.Y3eF殳^M0!9KOOsv.45XlWVVxEP[yG̈́a<;agv1kNSiy#Dr:55<]700eZEyWѥ8nĈeH@YR\d!ciC:=sN85?眸:=oD2 :*ސ$`aS%<$xn&~,{px%<lVX=nq~dhP]]Bþ7o2g=[Um" y)a!l6#`\$6xǎ1;+)0Utqe)q8 !1O[ I2nz[ZZ+vH@  p{2T( 7$V.K10;P?Hd8\R;l}֧eNŚvMsP+9_7bJ@dvI:KQSS[7fnXdLRFbAeAmZfҙX1bK{aN t,ϚJ}(/iٚ+G_u>cxҌ%!QfucOV1baA y(x wTB IWRuz$~[ HOA :W[ i$6?EGS >5h:',(ipXL33 A4TVp/n'np G\"z-MJee%n7(-((q(,,FAqq1T*$x09ɶd@{Ucf~?6tly(B&AiqxX.axhЯN]iS/x.Sm` >u\.kͦ~d! m^_r@ww7`dzQQ[,P Px"[2 4͆ֆjYǒ Ämj+ M=鐼P p>X!IVF0dvG݊[z:nUQSS&1BGk6?},yI,<(4뚶^pwg{fC i7&9c_$z<nIָ{w꓍FjtEEzZZZ0<<,_3lEj#(h;!X/_D8ӿOiO#V- ycz1N_Nژct}6NO7G wwؿQx?p`؎3\UCb8gC``@MM dU<?yAp7&XG ee?ol(cqu30gpgM$7{xw}Jz2CKK w8#P6B5}]P363Y tYOeJR2ciSk6[~ ~de:/E۴\b0su&h85iPGGlr$ټƕׯq% a"Xj8ڎ&Cd,RWϷ*hZ^k 'e"! "fvr= +)b0S1= @R0R#Od4y^!쫭ZǗ0<MĈ'zo™~aƝ~ܰhS,:.D~bwaiF&)+Ν;LǮXfa-R2m]~r!0L*)0.`ttXD9x)*h4%) ֠cSNXϱE[Hl^'D͢kUi<.peaw/q-X#W:AdP-fq킵.hx&Z})ap %EހHn,x` P"NEo?! Hg,fΟkVV <S-a0o N?ƣ?('\0j}p`*nSL}{|m)x`ff6-g7f[ͤa4АsbtQSSfA(iCKs4%uAՉ?3gST,VI8=%kw|6XP_Q3V6T::}a6ͳL5$\A<ϯg$18Y ;$Uc_,me[`rRoreL@$~fClw}ysvJdxGI m799<ߵ6kVHjie*,DFL;=:=|cX1 }ғx:l5. 8mmmB(ʋqCRWT b賱AI'!Rx<9 a+wDRdL===k8K7AN-3r/W7&<Dp8iܻ.^_ݠah-au>C̍Nܽ k-C̸yL#lUR͊%9xgM&L&ȱELc㫂B`99 O8=IW'dC[mVsN*HdIu8CmelǼ_d$mpHOK}BQ ޱc߃ҹ,-Rt1P* ly~'691/#;tN F&tGc_mj:L/ S?Z1f|ׄh_4^FQO.\qrnrvj"3=AqV 6fy*2][E[f8 r^M'ۭ,,$,|GGGq&l ]Dcpҁg6|7(0Ż2q Ւ\lhŌHsl6\v `hܶ}!̡z+6 3$\.Kߛ:ު^)`VT0>X^^|> ޱ\%T GDP2ED 9Cs %C tEhHs ```iOڀ|"}rAc4199Ihyx3MkǢ 媍"ja# * IDAT]fw`6/[?,reW%1$qLHgq4ʱvtMbN.9j$[9-bɶPK|c\J66YQgPS+ B&9~D>ArpAMQ1o|a>0 ƖX~h7JRpT\Vŷ|x{tAׅܶ/JA`,uB<ӧOKr936*2b`6`D',p8 m9W2t*qz^Q"QQ0b1={6yLMMajjj5@&,83+oH9srw9c:MPD$ז Mgg'Οj?2ֆjT@<IŬ$=Ϫs /f4;uL/z;nmE E:!"nbivIX@$S}xpl FE9XDpY1ML̳vw89|a *S Ip.IL"D"MD0^G[[ZZZ>#&t#i>/O=f9_.Stwxqz>ͤD;1ِupj1hooM`LNk9XNcU)6"<޹6OrNdx<p6e4^NC4g` *))AeFd:_V1Lo1of$4$$/aW(fd*c nI)X!FGG;Z,DlkkC{{;qmBKK s0fljŗϸ!Gqrc1V yH a2PSSɴb05x<̉=+^n;RTrw <Bp8' bK/Rq `Ư;aR"]ˈŗV[58߄35Qg(`fi輋*"Mhjjjhrn&phj۾)#%A*Ay+|0 L]7/Q T<:+r(C(£Mh4jL%YDIl uݍBPK˃:^ \w$6S\.תc 6s0AS())YY<Ζ-򥣒_or{713e]<̉<,/_/ŷwa6Yban_ ~nxxhhhq^h<:4 ^,JR%( !eZɷˊ]0RmsL()֢XR+k[Q>pG(#_ZBdfNNu-}a VH)xxfCҾ'޻3 [bDF:r͆ձf@`U$!Ezu2/Ǩ-)RԖ'sMz|,Nl/ݠM؛(C?: N]Y ANR.X+H4rEm}0ytUmmm؜cxA7s82 A{F"Aԕ}:*pp3xT" ㇤i'̂. f3DbSxttIL\E(Aާ)T)}s{8$~(f"TQTGW¹ Ctޅu^?n{仦61mh2@ԙ'ڵke:iL-y ?^F(`0H0;LNN2[z{4b-b-r-sf? Bx6qL&$3fOķ: :>99IC8=;M&L&h1TaaҪըӗb4WH͡T%=~p7 x,d>E@9~{/tO%DR8ճ?-x8p aY㘚 HB8hAiF_nSf'r؜lP(Nb9A@&c=7N8W\ 8,0gQ_NnOa0w'̥zho߾k >ziiGAVrM rTcvttll68*jghN|6=wE)K,38HH6llH@l uN24EXZZJPvjXAMwux`!U^:u8JrX"Hl;[ClN,{lq $KS1NDbwc2BPWR"!D2ͅYD!t>9DBP'{("=*5Z!7~:|>!ØÏ=u%s8*͢7~0DCjUD߆+b|Ѝ؄ye^vl65s#BW Q+sxu,MU8nI#AYhjjN<!3ܽ{w5ȧg$HD[[[^FQ3. U+ R UZ0;pvx<\.P %h4mn3?/$/; &!Η@dAlLHP6qy6OCuUFTWWz͸wbnnss\wo:,ْlm_d4%CNIf漦$vyH4L 3´MB $jgpH̀e%_"[–.e[C[[z㽴{i?{?w9p],Յ˗cѢE79'К$AE= mmm wJۋi??Tus\']d{`uq>v~za˗/8uӱR;:qtljj*?.koO/c׶YafWl6b||C*Kr4-(Z~廊#/V!w"!AȌi'z_;PeGu:(ȏm1%zHWk8; ذRDQgiT'nx;) DKc!NP tZSVV3碢g Q*qYO!ISbwVѮ&zqXVΪhg!>3|994 ^/^}f&˖`0`ٴAJf7#ZQիW_UY8as J5R1S bc?2qcP"kV|H:f4IMM…?ӺHVa}kg]Csex$uyu 7s^Qr'ՊDNL&&799_L @BءsXsh4b Ӊk1nGt<p\jzA-*e_ d8uw D'xݞ*<Aqw̳5*^܃Ec(U8:cz^2Y4 `6O\{֭(_u RІ+^@ ,Ze˖d2l F5&9xLDfb`` n*)JswO_?*<+T%0WĩfsK#w=+2[t8wsVyQtbll"U-l?_&ڬy4;]6e;a vI)Ӊژ 9HJLJBB09$(XkMt^20Xc2I.r1uf?O[cH'\:NX|%G{{;/B.--EAAzzz"׳|҄-BI(Wffh00EEO-ع""\#<ؐx5FX[[#5GYYYܸy8ڼc,~v!"ANB_{ݍ].6,|^J}Oɥ6bDC/!h}~/.SU*s,r~W1aUQ:l>|x +s963:.CW0 NOɩ^/@DEBYV2aq!Љ?1RNAs'b \7Hpl!jřMH-6v{v8`<||M~xRTJ<;y qKY{:35kdɍٛÛ`q1bZJx===qQuCW|H_0GA”[C" *>l a'`0@ȦJ|㌙pUhBqB(Ploă 7d }f&YY3s+,A$g̮oa٢2bU0St@ T#41?p3fFBB ,jYfF8,Ǥ100SIԬ౯daCǛWXPn6*+*`q>"*ӱ!RɹrGe]w7}?-j0N'` 1%h>@"t#{EuBV ?83b#Z^G<׹6WplZ#Z6,, !)$@,[U=O}Xp;`DMz9"߶Y"h8Cdff^188]łU?cQͱC?^0!v&\>V\<@ŸTF1L0q'r:lÉXw{5,L;%֖{Q<3"ZBZ}L%ϟUEGp ¾H=n^;4&Ͽۊr3CAI mm݆7&0^|%|dÿ*dTĉ,n.lV%l{i[rHLK7mDiga)kxxZ-JJ5551Qˀ=~ܣ~{F`k B G4b <''ὂ 2k͇Ox`m(j!ܾ14*{6ƞT199I9O>CCSURZ-%K()Fʊ lXDKo._^։Zr +V:Yl66 $֦p89vc13nJ:n7<AsƏܒ/_xs:A(B?Sr2Pyw(ge84 AƦҊԄ!˕SHR01Fj4-( f9UB),iADc) =`W?rwyxx金Nt܁p$bc̄yZ@ qYV8xPC,;/eggsv^:J_:`R{raLcޱ… Z'LrR,oKQCNL FC#܁Q %}}{j(S bPm2g4*2qþ+k5@q>./u /AȏߟTxÇ?WK3q "'C?_O!0b ›KrOgedD!u mBZƆ Pn6SR746b޽'h,*r<6!8})}f&rK"qhhDm]Dt:Ǜ[9j>||1Y,~ƭXY]]]z(//s8;;--- F v?ww CdXۍZ_>&Kggg $g8{Z&衫o > Gxqy gff3d"`4E=^/V=lC!c(U/8=vwgf2.MeɄ5&6UUrM2~uTss3sa"$E%",Y"Ґ Aq5$x psss_:4Xor_pm&*Jqz[p_ qCHQ gl,C6jI@25(7w^ػi7 $nQ/xij<;@CǛ1:PRV(ڡvދ:J qޯoxL`ID}}}qfv\cIG$g vIʙ'Y1QT@pÐgRLq5E"E15AvjdE-c|CLІKqWͿ'^cKصtB~ck1{tuuvnjXq睎 >i?9buӉw}q]Fcه^rAH o\%n~Luq$Do=.M.8.ai bi'Z}"OhVuSoWugW)÷~ 8\tx$7U{=*8KKc3cGNK PU<(XDR" HH`b S"j\1ueE=c8D(++ q hnnf:VhbfA[R_/Xkt-Bj]zv|]VV\ᡈoӢOB1aas{Wi1C%1cƦBəW B xR$Bcd_!BApr{J17$x p&77w;":FvI 7VTXl]چvb8F!(s}KN\+iywd z"wI:0~]ʊ'χ_~asZ. )*+*d®]q{sPb ZM6lI"!z=nق3صk ɖ"z`<?EL76 ALϊ0}}}LUDuw^1l6Ӽ"n;ص{7V+(zR1>Q$80[0%P$MÆ>3SK8Zصk`@<hnnfVf<\ç}bdQZ\,}#ҷVg?S ڔV;UX3֥ f-Du3 bt,/U~'r5J3 & brl_Uq&GLX,[18ks feը: <ʮ}yQPh=>q@굋vMMMLU8O~^mX؁ P(D@;wb`0z h},(Ea!X,x(1K¬1K/$#[Xve;&,yEƢVa6Q]]7韰ulXh d`m޸dΫ2"T2z==4NʒRMS$M R@rRS?ORN9j5IG5\.Nf8m QS]Mk]qݥSӡy?xO)r2d5N* SSz- 2^%͝N8ݲ+{piܐA.X'|䩵P+*զ6^m؁ DDdOvw[rVk%vX|^ENFuԆUܩΝ;)1KlX̶mٱ1=zT𲲲8A)Z,X ugx;f$v "KBIjj*;S|n1 ضm|Wwuu1'jD JW"@1 =&W d:6ժ{Us juiAj[l-[H Cf=ܝXA _kh4';w؁emNV0Ge:ׇ3m# t CAGɡ}~/R(ePaUQ:^~d(_xC+yQ~(F8k=$J5551xR4' ␉TW'xAc۶m$z%zQ+y9quf&vSx$l)ĜE* 4(oɀ^:yͦOmNIFMu5Te 6n .h@ѠZĬtvvTE~7RLCy-%ZA$DLM45%d;D tU~sX8Ad/5 ׈b;)!gCzz:c^np0'UUUӴE:ƕ%y=b Eb":- 73 D {8)u8A7<&vxoٞ^#uc -~l/`(j}KQײ]~~^{A{\b :Di"fݻyMeq tcrr{‰BUmooy$wx'!Ql6S$ڍr3e#fUHU[2bq.0Gõ5 u:T]8. ~1 }}}̟1PռijjmZ)?8"x, mF1u ;vπLBAKss3^=E:9"tckժ*iQ|&H?j5!G0D}}=sh NOsA;Ձ9oR9iX( rH@1:4@`MƜTwW~ 5Ja_l⼀ yԨ˘[s!9ւ cn޽{yM#+"%I)BU1B!z~D655a``@"x/衣%pFpMu5m $[+&YD>׋`\,.#'# SSHLK|?9IDMeR rkFO$f3ٶs8 Av YRN"Vزe 6UU;F1 x.})Ybj # 2 )]KYd y=xo)*ﹹV‹[b)v0mP.@BzYȑ#YD!(u OtRf(xcA/DBabNZVl{2.= r1 rH@vrhrl.(I|Caon8n#_e:o?gt\{\b+[jv8WMrR/Y.{AAIZ-_e<ˢ'm6!-UjP*cAd,\'nP)L}8{P$5X6Yj*ci FcccLg'xxvLFÄh4ضm · pojt "4662ڳ"ݍ܏JyNF6o\=?ބzŠf3, Bphhhy+J\1$$\%XЭJA.P^%o?]'4Lk_č`)BNFZܮKsUN%w'$x fU9GzmF*Zs{o.زq`y㢉#]W]v CE<6 NEi2&}>v[찹t-rSMGAPn6=KsI7 . ׮d8BdK`j3ȑ#9oWs9g6i^QnCwTv%zߜ.Ugχ=MJ?4 jy$&f)@FGGtJfD`my^M|ZQȹ4Goo/ds,\B={}l6FbJDl)R=%+H@A7AуFssϓi#Wb?_XEEnBq`OvKl)XbT6Quh"ʸΈ|NLbbXGRѠӌ C7db}4>9i۲8v~@~M6,}/`<4)J-#_]|.nA.f;bZ3 {-vFrdf}T(hK5>N@_ HPm|uu5*+*SzTb.K|Y<`"~ZC"}/[KM555AوAVSLM=TUUjtJ ":xlTժ|pZV;PZZJ7K;(-e_FGyObbDA@㩋p ;.1H@q bd N tL9&J}زq m]tPhRTW]o53*NX|^yڽڸ3߀91u}ȩ XdFRXBHf3ʖNTګ3ÃPRe|p>ޟagN2 t=3W,, 7 U%n3jUV#0|çNA'#T5vQYQ, 䃔:" TF N :땼J}C=.**SO>I7)1oZE.ơUurvA A  crXU?+\֛"33&**Ǿ}xM%+b&'%!' m uZ,5daYq stXE"nǦ*^J풭jK*YPX_zoj6 - 7)h4ҍIDHT{Ri9vXf8G"yo}pQvi>`0($K<<(]JIo=. ؁7B`ֹ֭z{{)sdq"[3efdnNvqA3 A#oʡ8:w[qOqQϙ>EDv;~sHNJ_X$e\۴d; P'+iQ|K`\,.Dnf2hV!;1׋6ɝK,> u m(1b0 nooGoo$Ϗš *jRM"hQSStl0CxISٺe /fI&Y iSyX\dߞzoT;u.VLfo$8cx-qVK.=p8L 6( r̿g2?: 0KuEs i8(3tL6Uu.5,Bo:h&;$x Urhߋ/^Ng<` F!Q˰Pþ O("YGrʣH|m.o-=J>Ψ`9ڽ7JH!1(IMÒ =nMτ.EM77A!)$nI1CwpǘT*cJ"` b<䬎^ DaQPyHA&hi&czZ֭[)&xa4Q#ٯAR8X+ 48s-Df0 x,DaZy~U|Z`fFg/1E]Cǫjh[7`ݲlc[z64*d]gt\eo?̙HIqZ-[d}D}}=6JshST"C-i (=%,M*4J tCDA FXŖ^9:8K72D1z555ǟ:uJc> a4G;^xspSʨ0::&BD}=<ȫ"]:/Y!s ;77,A.9iХD 5 MMMQJ|ylU˫oַH@ _U㚌'9 <1 0LǶG\+̊)xx;TWW؁%d ΦIp\'|{,TXm۶{)!8kL&^EykυNu l6l 4b;UZ,Iq`$xpiH@q19u|r6FJ2}S;g?9tlٸa_}I>$Es?x̎k/UbIceZ{n^mdk9( hJZ,aI~ƹ !B0$xAI^V˾p!8[tlssdbm5j.CZFA A 6Uv]ꏧ.?,Kp8݁ 2x@:ὂ<jEsG0ݝØܞ$\}x~L N=x`mjbǧ!*JeQ=>^fso&vZ3BEFc\oeEŌxtK=RN6GAl6q`===XhQ܍YG {f>>|Ӹ\.\.8}>M E4 9f̢{I~677c4AJ|ZŷFm~(\(J?u~{oiI\lAB&$)ziTx)38aw[iե0# 3:zEΈ}+|>6$'%asxb7Dlb%1Iu5|)IeCEpq3?yT3bSUUG>oƕ̙3Ĺww}Lӂ5&S\ZTW~CEE]R8sNUBRTVTp<SBx<&3q{\n%ѱˌƸMh4شiDgggwVVN'}IDټq'u.㫪y[VN.V9n~qq8֖BQ`Tgr;D4~xE?5\H80sl s8QtGAYAS.JCc J2}:\qT76ꑚ,(}ݺzqa_FV*%Nވ~f.B}׽g ^V#!,Xnn:.njq2mX=YZZZRDܚ-T</d;Rݎ^WtC,ugf8yOlb*"5z3TS\]^^\xF"~sƊޗ ¢:\ze`F9u BX( 8wnnC[ \ T VdwS;?gXԔ$ĩq70ӽxtX[H^q :?U6;:N>+"e4eH^FrbATATr,mAPuqLNN 8v=vyz Dו,gbAmm-ӆ*Zj4N~opϘgBm]N8!wGw7ۇ}lC5.,YRIp'ۄ"'# As;6ss x gbEaʕ3ajX]:;;z97n܈;Z8Q_/C;tXPS& fs\\MUU~8+wb ]^ӱLTWM<|Zƚ5kFhPYY}q>V*k\b8/*X|8Q_ZIٹbkE j*ԥh$r{O8n]{߮;'p8> AycQha,N]zt ^F{аkko@R!x_u?Ã/3o4?fØ " MSV.N#l/Y*?Ս:Kw{4#@ww7FGGEZ[vvҲ+s9T(JFBZrrLw M2 AqP3>NBXrTlmmHD`jϴf 渝6l`rykEˎuo?X,1ݍw$gf2擴jw?aT$E讛Vbq>#d;^Q*Ά]ԬV+>;ja! EE0 $Ժ 0o9rʕW1?_'OG8޻w/6l؀r9%  0dI?k笿&\9B3׳)7{>^|>?S g/Æ640)'~A 8)=L?>ǽ0h󡶮he4gzYz="M))#aaA 0_σ"m6v<"͍?hx`V#I x_X[u˲qu{ h'QB8FIRmȏֿ}>6b6@0D]dFGGϪB}6|fS;QKNJ.E4U2 ݬDMLn/dTADh؃ ˨C]] 3GFeEO༡zۋ=;;N1kpb QYQF{{;vލYu2vލĴA3 ]]PF>Bn*.ɓY\lc6j Nww_[ٹ0͜O0z ^s]LǪj|o&f7>|qAfTVTĬ𡲢mvc``ٲy~9u5Z-} ;xsX!r:YI IDAT%Y:VTTĴHÇq$˗o>8p6l%f555s> :ެG.r7jŮ]y~ +6 "EbB㸫9iq#FrswxpA orssN}uč 2֠4'jT$@2#SӽxX']Twn &iT8d9<c_2 \A| "RMKUOףWK3qɗWHH:nM кXVC%hdJAVa3 |1\M1Q6<+**b]ׯ{7E*¢pux\ĉ.%6o\+wcJjA#,DEee%6X,1wF#\ "l'S?biSшb!*p\6?[ KYI\?װO.ݵ94BH@ĵlpT{E*9r˘Z$' )I(~q8Kg^twjĺ۳a[3#:QxFa[0nƏZͅ xc&nAnildJ|M6 ,W*X]0  cGApb:!B*jeoc* 6g"&:JR^^AYQƓ&F i[OM͠1 8 VD*\h>NF!HOJ@nf  -.vx@C ptedde1z 1Q+1}5z2]3>* REkN/NSccLK y{{|cIZg%ת_܏KMn7,m0/^A|&''qu?MB׋G-v_+hCF訨w1+G#.#1+`F_r! < js) "Otx4֌~\M s^+&'2.{%<8guyp݊8M5{`zacddӳX$%%-(j%Tʂn ڔپm I س:immU50#?;v+F}8udRDPV'O*>B|>E`\:;;G/TC 5dWz(baJl==}_<@Ӆz^k;2LUu5">A6dXn2x|h~MKbrwxp|L= A,-NLuč•Irrk?7E?dLǯd'S?G#'UhX|jj8c(0$@U3]+WxNk-l".zzz09)Fg}ȜPЪTX>N}-6%*}4)1 A ))|F}̇F1{6 UUUtIl6{qY]:;;I@ܦ6l6\.CbhhhY+Hkb" V~Ez?:28qB߯.Kׅ(1 <O.0eh4 'GZIcuMNcXkj(Sl[ %vRe$O/ñcpl7>f)|{{;"pgWֈÑ7N].ܻB^̾Y79]9+`qM=Q \^^~@!C X8 <σq:$cŶ5Fb87z?@ޗ0!~${>=F1ShrcNT $aChv Ou4p$`G ?d"I*::&8%C2~wDFt R擒,+5^vlSD^kk+Kz:zjzѬg@OHgX`Ƌ/euydZ2>L\.XZ!q!Y$'#%ua@pJp.z%"~]lΜ,t:rEXVTT(5fGlNMc^kbr紴4\z5ӧQb2)N1<ԟo]Xz$'+B,zq1Z!}k=Nn}# Wz^UUU4x#7qǿG6cgH>ׁNF!)X ۋڋ<5M= A,ࡾm#Cx09fEwz}濜Ǚ}$q 40ov߼݂?^wajj&m?ѳ|Ø 7oQ|~T<޻ԃkb]&gsEAխYF"gbAZQ1IL0xˋZj "bYJ9}Lrg՞aY܃‡U$"&ɩ8}%Fk[?{D#6n\.I?#k.-Vg[݇T"8p3F111A 3NJp RR]] ^0e%`c{۷{+A!''FQT4!//~Ӄ9zEnE%&Nh4 I9x^8y2EJe#Ʌw9zzPU]C>9PRe"=RjNCjj*233&؅kn |~?N'N'v$J=dee@ n=wrYD]XŇJH!ARN⑖ƴu/b@H!X f`04^4y˵YT\S@M}+~x~\hZl߾]p/6 UUU!ɀI5Djɓ ' Ĉ) ud;(!RJCLL կJ: p vSt񃭧GË/6͂~qrm{.n%1Ac﹝hE.#>=bLH,C>v v]=.EZf'@; el cggs;uwtذaCH^qZC7vEߑ'?㢞XDG .$x b9<`Ur4ZG(\4;Fyn>~{*yR}-,IӡhkkUpQnV <(y\.IQeeE,<GRd1N'Ξ=_WW&\at:Թ&95R;;|_4 ^...FggNjFܮO8!~v<K}=ׂrw?85WHYG,@u/zTUWK"vX~=z=JKKo E"8qYY:ng<< xv83Ɂd%6_L@Fb DGGGyn||R$>zzpL%_ =JbRv5K!8y'r avd0`0¶)ŞEس6v#GSpL*%<l6\Y |oj+'t0͂f8~YNHL ,+u:]hk8w.4uO|ơgU=]_PeĴ4鎁;^gIT7-J]b+V`ƍw<>l[rWBGhXHHH@TT/kaEDAi^*:2GΉX@ ;_9DXe_ 6vz$ƫbs;ސ!8+gTVVrzqȑ3td-v&SqH}H8h2U;x|4@(OI BIˉ'*a|߀lͦE8>~聠p1Ơރb6=4z$0+ .#* عs(:p_F#xxn۳B|48,£ 6Ȣ[xcxp!9kYVLr%hw`Ŷtw8qI$a9K 9<ȃ`[lbx8><}ܰ0_%&"j&Iwx<gwzVznt!11 |Ƌ/D)X^_OPS L [ې=T50z9[,2 yu4^ɴFZK%Y6g^K.1_%&waٌb  iXDĹ|qY\>kZ3\˙Q8]\C ֭Fnf2g55%-EcC;qY %|8q$*JJGrβ>VTIˉb1N'~?xn EDEE!;;zv n'v.8MOJ@zR].t70k7aw #:ĵH A p8222^2`uI;7w,zxj>u❏u%M67?}xn[>.zqMU@fDJwFCquud#>?}ؔhfd@D )$x?6)0&t&z {'N.8z!JL&rv=,<x ˃NŋUZ[[qjeFp^<(%Y`^V?gNϙ8ӜZ(rswOyi>0jNVA,eO**UՒ$t:L}0'NW_ ks*)!G lv #9A]5HOJIfr<M$r>>.5r )LFzRr3SHCl6`rQT0qsłrLLP&6;^:O~']^Ihׅn,6Dss3E ?>^ezgܥw1DݩyKkqz:A<U,⡲m#Cx09EG@ ('*& )@Ǩoa$`rrA&z=ʇڻg{Vd5LN'ׂO<)Y`p`0 gΜa>!|pYdee!%9aP&`LU@7RP\\ K.|M='Bn}#@㵛 W1\ w;I`;##9䡾#~t{G;Eg^*C%z㿽u znQE$" 'u,w+cr+$6hhhu2KVi#9喥.4uBS|Pv| { u_^(! æ7EkgA\\J&a g.Xu:^'<5zA$ OC};Fo!-VMݏ=Z5N}g;L/|aopv}~ sX]j?ڌuw)!k׮I<%9l#]xÑ}CψˈAZ5 eV nxoO7@Dp\& xu][現d ^ ,Iݾ0YJz[laJ[[[rnJL&h4Nv_)`u b6y.Qb)׊k7\CrH?330-P{<--wE@.̤sBMbd %}"FVR[W'cZR|^^x\'f\;d?mx!ͮuJA@hy/e@u98^~nw;w:5wm?rhcm8{,;~Ƀ5˩r1ՈJRdffŋAmDݺ&pFAsx4Fؤ_x%71RE9Nj?u^/Eq2qؘvaiU|!xHILBfѣن xfVqhįU#&~W}p?!''P0gG0៤@(T.??_1m+0S_pBhZΪt:%b'K6YYY q`"C\h>n~}tXaŴ(FmB)RSS3~Ұa\zUpY]Xs(f1h!!' ***`X>&Durj|zAȃAp8>6/u08mݚzߗUttP=llNęʰJJo.p\ox ]\ b)P ߵks8z8rd[/r(įUC)+4GՇQnh@F>r"~Zlsrrʘ$q|Z䈉@I`xD[sd...I&PR\\ ^Ton:&%Ϳ;^#O48v|Ifr(zb[bJBE@VV9J@sjөgIE,NAٹs3=Gt=pyџ>Y' iL@YHۻu=44$y&<"B:***n}8Q+OMzpP"y@ 9üT{{F+kуx'uwpǸhTb5v.ys dX/((ꐮ ;5dL7xˋN'_ р@&S"3==M!Ak˭EFl-a*[[[uYpxg 7KNK?6Fq oZ,~$*2fcD23LM#^= .`zf c(ĀEPPPĊ^Wp*JHařx=I9uXl.Pv55L(/Rq(rݻer=k QH,T-9 !>(..:7xݳݺ&ޚiM!H@DP87ü75{3=4vE=38=Ǜۃ UpU2AѠn[U] >xuws 5d7v[?zܘ=5=[^@ƄDHf4x(- \b'SZW'++ )ax l0Xjw""@K"ku4w8LM3"^okslN\ar)S4t:Ol==\Z-SVi%}%v$ޭLsB\֬Ƶ~>RT*Ź;HKK``* q8oh䥾[-q:(߼(ZݟaęD=cn ̶|ԏH!&:L&#隚Xt._@K2 _ ] w7Vny!h1" 9| W1mLey^ .#up)\_Z%|g0\~~>s-9_rx/=dqkbH37K?4u .hܥ䀥^f_;*Y$-D95 bIr3S>e.'\ֻU2{,͇],GlO)r#9"mMhDYYYN]J A!/H@Xl֢4^ŇR(eiw\<<;;[6N.h}JhooGgg'E=q5;ΕaCA8m^;41=Q'>/BA 0H!wls HINƾ碮zѸ}vQ^?^Q1QoCL<;ۃN'6# èwADၐ3T7wx<paj6lP)kO٨4~YQ| ׭f j"ќF=BS'< L;HV+--U|f|>~b: EFD`qae~ ^{1U\Ҡ Űs=<Q`ٸ/]Yb]Tݺ&ޚ9-!Ob  8 gjo&:p8>BAx###iy;X ԱKa{^KeMFj`)rRu{͒Soԉx"Zv]PV~ W,QQ0σH;%p LMM6 ses ťZt2pG }#m4"%$,x2YRM&+FGAQcF#e'B@H@7fav};)T|H#xdff2%br3 $k\4 *ࢮG :m˖-KZ>K}wR:x˲;LMO10"oyHIĪ!DEyi̵W\f~_@:C IDAT1&KNDY;YR-E͚ЃyxVvsr-t nea}f㺿V6@ZZl$eto︟fypw|q DpxSA\d:{R~8҄&4_Vm:y05=-gipZ-ЈO>E4%,χV0k!I׊ah , ,x>6$' uK "tTHIpcT "rٸNNe ֯lc˖-RSS x LIIFo~IXғɡ\x]d;/=D\Xn#>b;h|/H)\hZTVVرc\/ |hhh@^^FcĹ(ۍv5 :]i .!oH@T<`U+w8^v;%&fssz*^ @/P\\!&gboo|{Wxk7!Ap8NL ,qNL~+jϞf#͙;֐=,F@@b"%9\XPWW5 JKKST(++CmmmP>>֠P / G^ ^"5P#ܰ(G˃YBIAA`D_EZ \.3nذ2^aPXa OYYYZ r(?I:$&z[AH:рJMHe(,y. P4]c;?5T8W%ͧ΂ L)܉! O8.=tB)4Er N̰CA kl6ncA]CuئR`6&p);SBO;bg45lPVG^t"2 X2hU`*)܇T*-,&N3ϵ\.i%eh%Pf PgQQԎlR5EF"-󸼼T*&w0,y1NmmmLi'^@WSݖ Cĺ,%55UpZp`ΞܻmYǒ[!.y/"֬W^ũSpEI+Ю9f3s7Ͼnj/u^@|@ +˧AL ܺƈ8A3aq^[USWoH !v}͆PVVt^uGC&:z0Ϫst[@}1 yzuz,z`;y<Lbz,v`Vqq1eDexF>OC^;Dx.[W8-MmwxO;R' Iq8K}'gq}rB%4MoqOZ5^V!?zO~1}pVE!$k޵Kut\#|Ùax>7߻ iM.t <9$5,g^b jb=PZ`0i4n<04̝f fj‘~?uJ#+g!igbwGF}a\S1D),,$!1D<Y#"LvmYG:bAX\i..X+pÆ IP/^\;@z--ZjPK.ԩShhhՑ 㿻S7W>p8j? Pf- xu,ahyW!~9:`):}3"9zFah48t萬zqѠeeeg6زeKPŶ;b?>7Zd!tQ]SEk#/˃?ܻu=LJb: P滻jӡ<t5vR$"X.,R԰$'SFEc04Xy LI|tyBi=_cݖRTϧĻ7Gb;dӉ~:doAӣ5T8p4MH>/gΜ!PT1+a/~{4u <!9[?Wo .f)l.OH\2_J,I*-waܑ#G` ŢFu>m,s䷱3%H`jzALs E v@ASr0 LJ% OtyZ]/>5A .[U0sK :z#+FQc!x;Cj2&hnfKFbEWVP$Ar9zqEHGZZ r+WsWGq <R_$:FRlL_xu,ga.Í/-DvrIrR)J((((@EyXU]a˖-0uF)--eZ@wϢ@KBj/)j &18<7air#\_r]a>{Nŋ> #??YVBTTO_0AI 5A!0⡲C [+L#:0.0#'=If^S;f Ovԉ__ŰW~YwO*Ďz14 |>,))XP]]8a2>cJlƙ3g7npxc  }#܉B [d$ u}Jj,G``<~:un !tT b>SI`z")תi|#v#ݢ0883gP'M Hx.͒% "j3 &^^iOp'ݖp<{:^NY$xKУegv:K_ntuu1A7[ F-j:hsCEy9-50z~tuu :Fz$aBR!//hj\%]<R;##iys-4:D b0Qw^zD~AR|=:qS'vyV6#$tXׯj,͆'Nz-#^lٳgt&^{,;'H@K3)PIr!739sVV๚N=H~GD\Șknnf%ܻ:!33KLO:=n!<ِ M] 4D F!Q 033<Pfgw\|;D  gd}F3t˻b}V z;F'EH䱵Ȉ_l\g˅pj(/GEy9XX,Ǖ,@\C~~>?GEEaʕ}7GY<rGMFFy;x4I$q-ܧNY );o׵߃~}CqE|ӰJ¦D;;[aTڭEFLQON+Vt Aaf2>kAhrʀx񜚐ZAzBǼd.DIb`a~&QEA$"%BwYn{*qjþ~[b2dBm]냊^@q/xDGGua7t@Dx@'7q>j]fET\МTrRu :+|p{'q[5A$ `6aXY얔;z^TUU-v(++Ehft=l\I+p0d+q2B  [^a=S*C 8kh4h ,:fɱQrw faZV '4^ZXeBENBLi/BviX{&SO[ > 9XD DbX_PT6=)w> J=@Q%[z޵ Z6"oJJ =8=8-X,>gn7@!&g8<yшc6 RD^ZvY'ɜisG{=3ΜrFceƉ9gGęf'f"l 9ѨD7мk5p@` [Usq]UO=]U2x^$><QBHd/..6oie{fXL ovqi 4ݒ4`wH&x< 7S[f8׋Ǐ+YK}}jV" gc'7msړ4N%٬([5,Me2\"QWwx)<{hn'R  E rU 4#Q4TB xHLDepv/}frK1f6Q'"*<r_އW"1dW(p 46qcb ˅g෿mª>w?TusYE"JZQnj]bc`SU--l=f:{hc_a1$M_fCciȄB !R 1)҇DK$vp8>釙wMZ| =P|B K߇'CFD!Nը!(,jݼ/Q#mo|8} ;}a>K8oN%2Wm]f& 0Ű*[<+I;Q`%ԍQoji{gZ }\hR2?~\a>ǹQ[[cΎ߰q1)1bK+^fp)(qFGGSnhy7,G(V*Z@etww -{\|OvQԥEa$aqDQ6)^FMeI$٥~pyhHAecGjRNGxAJ111.7cS6@>3kkkKqpUUUCx{SMՇo~ 6lf#IktWtӪ,'6Z[[#ZlF}}fl88ۗqi4xqBcbf X\B2 ? ˿MU,,,HD&ht" XlާNɐ^. _'y(&ҀxFD)",7Nk}!--@%ReiKX݁GݝHR&Ka;//_eԒ^-[D:|n]3(~m .AqO>\A/#ן-UVjm%ٳ8wSUffP{G(0fy IDATFcZA,BҰo^w9Ry3:mEKKKD0ػwWEEo:I,K9&{1.`E`i食@I_h?̉Y*c0Sc kf@3:=ɧ/M(߯荔LkI0Y4Ykh6G;gbbXҟg>mdwj}ɐxy UDϷDkkcSUU&mެms~3l6T1>>$Ix H$Iz;Z枙ɰ^?9LQVV^x\`@cc#9QxxDtZt<^555(--^ /;&I2:Q K-<4޺Qx._dp5 _’:Bee%ÓGE57OdBzzZZ^ !%5D*<$Kuot>I}hط/ۡ[h9A/.v$Y/!JI}DLxaMHzȲ,km1٧Y/צmKpPӧ1::έuՊ^x/}{hllTU%2͆gϢGn i*Ky䈒O&t[+;18?r^t()/47PBo:ߏowjz@z+ Ca޽Bcn7煖w_5 b3"n9( OY`EFz:s#"zToLqͶl_c`09enbbv]h'R ;\z: ˕ٔW"JJ+<ܞxF |-'O"(3[y"TUUBs"  WCgv!ǐ^X\%QP^Z~> n<+~25E#/A%P|8n>JKA5(&~D_|6.gVWɓ'U R}F<؈Qkkoۈ|DAoo/z{{Q^^ ayMn%Ir%<jHtmBo:-y0blVr3;ZĴlpMNF Y^'> \xQ8~9S )6vD*RBC}vE ٗۃݒ&(/ (nZf) A I¸kQ2~L['Ça``_bpp,#++ cł.^>^0.bkIA\?<Cg^[[N2475 BWWjv;v; QQQ=NPh5I)I<ˈHm^pfv苆<ngO ZGWWWD޽;"P[[n᠇\ެ6XdC̈<\s%`o9Ƌ1d#';~? %xJ$J<(挳W>5M_dW4@.JU? p|GEڛ* mW>+2I~n*IV{5Dee%;\x<✔ϐӏyml(++Cþ}0Q;Vl6[Att۷oO>' 29D@D"I Zf^,EO*eG ef4##>`Dwce62 l6+.?/EZh`aqy U~JڍhxB]L’eY8}s?Ƨݏf.Wne`aL S\~ ADJHKWa#\wMI%"@,+D KKz`>6,W`ihx"JHKcj֋y_>3'feרLbD'0q:̤ .0ЁtQ(*&&&4]sA e7V`P}| Y¼.|>^<~A15΢6v~_k,ǀ"R%iiz}ē}5SDaΤ zBFF233Q]]2_B5v6+^kU@43ᓚf$B=)[v͞~&٥)y$L8.A _|VUے`C쬫CsS~ ~p:8wۨE(B__6{Dc<RHjicҐxGY݁Hшo9dӍ,4999h]oƸsVC-ăADCމRɠǗdEY;lllD 9e"$~LTCtuxAAL׿}k1(UOMyx|>}yl@ r{C]]]\``?5lnkm<Idm-msVV~#[̸^zp"pדl%|gXP^^.. 7w3 /38Q(l|AG5YX\ļ/y_)W?S^\[u7\k\_gIIC&zB! ADD,bI,++&@pPc22$HD eTi~ Ȗkkc'Juu8|^Ԅ pe6 .\TniPDRG&Ncō6t35"j]}pM w^hz]QQy\.u]sw{TUVv]2=CE;iPaaaC%mu}j=kW0d鐑ct0d`hȸs9[Q}8q vzz:́0/" F<Ц[Ko͈,MFz:d""14 -Ҕ Ox|U߆ ~Cl"{Rşo_|f#ZOkk+FGGq`~^^kՊ=V+\.ε>/t:q9ԠR ק;Yr€"Ҋg\Ʀ!//oݬSf!T:::w^tڟk4xcQuu51??/\&}.BxUތt KKK<aZLĕj-1a!;KC (q(~,1dv5Y>GmVx""""DiVw8(--UHX $\sKz Rl{ݪm>=l6UdTb8?m6lNeݘ@}}v~҃$K(04A_pX+ۜ,N&X(cbttmtݫ^zS eyZ0.EL&uGREX}[&cfÐ1VTT8k5P?H0-EԜCdUI r%""} .\O2{>III t:dYYx!1{gH¿nN7|EKYYYL&?xǃ Y:|Z,իxq8p`5#JՇQkkf{9?jJ0kncccZ;Ht=(0F[yyyp:xOO*eQؿ^7ѕͷd% ?''ո|:_;qNLNaEEEœc<Idaq "-F Ҩd-""Mirs" H>6[CFF6^H%]KEE/w|7l۬9ш3ڌF3IZ,m*Wɨ=|> MM1k[SVV&<؈smmhooȲ/bQOP(*12 {/Qjbi$Ig6K( ⠇k-222}KЃbAyyPd⵳>ΖDf_|iiҒC!EV 8(|###1[G2-hk?pڐCFAS^v""{BviJ.]8u_d;ĐcIo*PF3W=j*eXp`s$𡷷n=joO$It6iɍToji]vᗗ]AP  l d3d660Qhw P* 0 `ԉbe&2KBIae4Wf3 }UjJ헆ַk[0؁( yG; 7љRٯ} o6; F<؈לo)Á .v't_n7ƴvޑ$eT DEie} : l0c(9ǏcC`ݎ|TVVjt:z=e߱c{=h~l-)vTYYn˽N><|/; xZY233cǎ _@fgKA%%%BI @iNl|"vqR+QX⪖؞DD U$>0EEEUJ?h}(1z/<+9ǗWsff/?_?chā[e\x1!c7v]89f}[KDDI$-m.T38z(Axzk+\.m_ш vvvbbbB4++ w>''UUUBxl'Od2P^^. HKz`* @f&xBD1F X/}0 -DDZ"*-c#PBDVad5_@ mx_x ?o~is>v16o*,|SNCΡR‘o|3AncccZ;Ht=8u M =B?G? wc1NAfd+_vUo]hiiaUUUᥗ^BcccsPnىo(B__Y>"L6i$Ic~mp ~;xI5V::χ'N|eeeٿ?/Yq4445x@Ͳ1??;Gz!c~~^hmZRRq ++川Ne8u_|vwii? ǎ%%Y4%$2Ed#imDD puuup8p~8~8t(+G(/ `5|_|y5K1Yl"q j(t⓵Tލ3OgFFeil'[Z@ ǃmnZs"IuD04NxD+| `JQsC" zpݫh4Y!''ո|к8u+^y'??_2N՜< q〇Y/ 1s5];qԡ۷oGooocֳyrD>R ^;ۉn~̍yV{VTUUHþ}Rhnv;PQQm BQDg%Irъt6i$I|[K<p=V̌f~=εy$ /7pFQ>߯f$Iow@DIAc>6©Nffx='OM#Ѵ z[=rrrV{/5S555ko};3ja!fY8@n/[% xHOOOlKN62 Ăb~ȈXd01= 7\49Y/<~H[8Q(~<ԤADJ#ݕPsHތx IDATkr8ҢI MMM0 3* n7ƴ֔3e"1Am~\Q\9}:j7ҧ#ͺaۣU^5;v@ff{q[dILee% #Z?[xǿWQDI%?K (^OT<1rMN`E񼽗qlBqA9ăOEyp=׸s.s?AkD,DlXR=' }drrSS1D?状IȽU/X#Q z^> i;$In&"(iHt +ZY϶^Ĵ6>lnjB#ڥ^7٨^5,wdK1tgz΋Sqoۣ!Dj'p{6\ٮ* Z38(VZ)h n}; [ pLj4kS̼ I.r1C"i4+< cl"R;y ;gms_:^9q6;s/|Ya b 'v;~%}S3ED>Z%ds 6_?t;fM'Oduш#GD|QT}t:u_cXPZZ*헆;t'\ 1L/ _n{lhY,GM)**RLPnHEјrWw +s#F7gw0:VJ ݻWwgDuu5 )N| B***UHL&fPUS;=y۷ޏcI]CϠ0&F@000 GJs.dN&596lfXκ:X,q`y^ଯOhG#I"Z(Y`V6_?t;Wf=Ҥ'O`4jDYYq un\p gɄYrL{{j ".@~ARPTJ 0,q f}r08JE-^Ldg,˸t߿L^96r:᚜djLc Pi&y%Ȝ7 'MV:Y<h 0NgNCQQl^[GGdr\hٴݛ$ x~M%%%HgիhmmEkk+`ZNc+j͆~&t\Z1 {  ()I...>࢖[?'Dχ--8|&g]"ۍ jy,..?{@HQL2`0!Z__Ʉ^%6D;$wH 7J }q | sl)TH&\pA#%pN+^ndtUUUIsFT(xrqɻr3~x|{[GQ'8Tmj65'?PRG=2+$VEE***váBl6w-&r"뭭hkkCps.-wΠҐj,@>ׇuuucC E 2:::`Z{(1I!Dt/ x %Iů8>z&uuufÞ`=0LQ x<{~qECpq^*|GG>Bؘgd p1$I.ji_KrxIs|>N7}hnjȈ &tvv[W Ҡêܐr헆}kf˒8ٌ̄k"J}l2 .Kxxۖ"fS\WɵQfDJOs<~2b&Y8ɒ 0dy:EEEkLLL((BVD#!,ǽ\CUUf™ӧyB挻 xŸ55'\ jx<pLwc%`0`ΝHhÇ7G %ae]]]՟~<;&I=ŀ"Jz$]\\Oaդ=}}}8} il>9r$zp7Vv O_X[2d2@W}>l70!r>v(**B__B֚I^fO "%D"r),{#:s挦3D uȲ ͖ @vvvدFfX ;yr"444`q @{l--}= &1Y" P lhTVV}QH\ .C:X(<6Q ,..az֋iSs v JiDZ1`"5x<Z)]R k 6ͨZ hhh@eeeAp |_ɖ&PUUvwwnk1 D(%HR;ĉ͇k%6B<5X,#=nz?_vv6߫zHqxꩧ{nA4jqi i,<~Ԭcw0!AzzzIJ J OFz:z@x-_`%36Q4ko tbqqBD)055QLp9GF$okӦMk'QlFmm->cݪ<؈*ea۵$IR x !IojmrWp{~؝՟Y\O-~AAvC^֭[\NtC=X,X}q̓K!IjPQQ{f$G%;g1Ŝ٨Ie ~oxIKKbG&),rDDǝ3l8(`J8%,k+ %cL1Hu&&&p8ݾ};L&SDc ZNɘQ4 lܸ999yM"0$X!,ue"==䜡Eȡ-..!(V=3û݃BR}!V*all C͛aݳ;r |>ߺ {H2?q( x Tu @#-Zܰa`čF>zkģf<%VnCCpL- 7 ,M:d dR EfV^O|x]h˗/ŋn&?~W -/2:::V'Ivv6籸~C=ۭ!:ߍO FBf} xaœfWUUUE2L 8 .8oXꐑ|Ȝ˸Q B u7' ƴE_ܖp޽[xx׬W!Q[lQ|mk9 [W`QמrB6֜gz/%τq׬eFGG&\f)Gxr?w %в%%%k7$8<8{rz~̈́0٘Q; k7މ Ɲ3_gT"TTeCWW;wbՊԫӰolkgee`0hqId'.ED$IzݹBQ]?~y?8aPǠ4Z^#bɠκ:\~]hbc ?nło9zp:X͞F>OQFnG}|7yrKV"YVK6oV4Tt2Q2<@yV6)**ZA,aU#9T4?^8,dYDe===B˚fTTT;Ɲ3=*¸s6."5C F6F5e .WJ]Seb%F$ x.!D<=d1D!ݞg|rӲ2<8D===[3˼dl&t\A>6 6 XUc@2h|4iqޑ$e~Q40&cFѱ] ~OvN?ygֲsD z={1准pIPVVDݎaUWZZGQQQL@oÓ+ ߨiJJJ"N O8Nn'8]\e`VN"-,e[k3??_zXh}K v ͙^)k=tfeSk굒LpVO 2rbb •o~^$w}6Yrrrq5&2r/w^~VYYQU?444AUę3g׿--j KAoiiiZەKD€"Ji$xqܸ?~0op~[xpopid:쀉x -, B6cǎƈ٩0+W18ƉcR#/%6DE2QNu#2ةL x~ERqr wXfw+e)Xƀf@41={$ɲA2Z##m IDAT)Y71!<>B $*ٟAggr& ۶mE $H2.109WĞwL&h_f⩧R]f~ǿݔ|h|晏F`"8&IDDQ€"Jy$ mw".htyCf<2goAhAvw(Kʫ?^헌~V5u\pAUec322ؾ}I&VtCi~Ĩ{'R?CIx#)ڡIqɲ ͦ"Tx;yb٩(-Bk@jm٢82ۑ]ee*Z&'g0 ^Ꙙ@,OXo@P$e DjZXԬ A63!D5fAPvM=91"ϭxMzkjj7J \eS!{2ş,OuZh4RPx1v%\FFƺVUyp#a9k5&5?>rDL+h'R3 //O> ?m(@D@$/zq pkMX+h100}ݗ2͛eߠl{c14֪/rĤfS^,..1{ Y;3Q QDD>d$FO|PxTUltx^o~ρ^Y.7n1%_@},fW4M}`1]~kݻw5",I_nv]hE+ڮlR%p8ň)zڜWC # ~HD-χVrDR=![G7MIӖbDD7$Yhmsssm`čn߅]kk :U3y.++KTa\s|G?O(f#%Mlg(yƝ3B_x0*b1Z~Fl*jD,p8I,G,(0``Ѡ ~y|A,..!(y9 W(A$0VR-b%P p°m CI8t® z ǢI]:zzz7mkE ]! -NuTVq~ѣG5}_,p'?8O["<Is_iiie_oӃB&kMzDžϞMYWƈaل6E[$Jho'Gd0i_( y?Ɣ>Bˆ;Q D&[ 4FGFO=R"O{©G8, "Y0/ #~i"Aı7',^W}}}د+SKEZ {T["[\_@ėpi* tޕx:W5/ZgLܞ9yCפֳ9'ug^3Mfgb;N5)CE lK$E~eKZ^iAѳs~t1sFKl ̚9{LePTTD?9r$,+zB'(Vy =A<AlHi@JJ *rqd7~w!4|8V%Y9[??(Ow'Dexs X ax̮)_e̴F&n9ܰ PVVtl wS2aj>ʼ$Δ (nXK@M41 sVU!:, RTI|>EW2t26 ۃ%Yd]m߾}ߗ:ֵl46Rć۶mc:+f9>gf\#* eee0 xQUUd,J=\.\vMwjH@9/RZ`~i&ijr2JXn|g=wBy hN>/,bpl]Dl b-)))8v오~{1ԩSCCCQF8,[*'h=;N' 9Dl(NX+.x0,x2UYip^ϴv療)Fp8LeYf,Y~anAD D#68s%sIkjV mnrCCC!FX,kZfqL53 O>yAn\sԬ㓰f8C8 0u塬,+֑ѢhA19X,z`cVAKRRSvP/sN3-//i~,hP^^zף\Xm#|>Ξ=1'o9E3󼛞l B  6PuV/ƷvDOMNFEn>^؏'gߵ%`~0 4$dhp!rrp<'LZnitވo'$$0HOOΝ;p}rY `X\Xl6f3 Ɇ\ ksshBNǸ, *Je^a^,] ^TNѠBt95w #)2* kjcA 8;1Z#jX! bawm*+(}(a29"뜮dSzr^Z-ʲf lBh7R٨ooȑ#ka nܸq%3'KKKsL=Tivb*[ZZ*1''GRݕlQS]lNaJJ vJJhD=AaXEn>~w!Z+ٍԴ_w.+?aqSg`noߓ;/| /t}Qk0<,oFݼy3!))Yɨd.eyXŝJ:!dSFncUUUEwccLwOl*ݱ)#V\)@XC4vtt@#H1 ̘2E,ƴAMȍ*‰@"@p`ۙ(mn.,SCs’AN!G$g*p8)]HIIe\97O"&MM@,s$xP4,Bb%1˃A'kGJ[=QVxq2.:X,P^^.Y 5Cf\ c3tdeI7a1ӛ Sv[]wJduC^^k9'&n4FwxR@mzJ '$x {C|O?<==mҋ_snaNXgA‰^y5ܻwq"I4044T*&TVV"1HspKGC]oBx溄jp"E .eф\Nq+\c)8Kxr5[oo/eUR8RKKK% Į't"I bs~FP0L"nEji@@ v 1-//gҰ`pIGx8bOl g*h$ ZK@&S|lz{{aH=WI9ʖ2h48~8$ Q&xh7'J%<*=A<AϋV鞉rm}Gjqp{a?ɿxq^\#S=<[!б逸~},qvKkFUʼnc5)> ~u*x<'|1ng>tݷo@Pb6[jZ᜘| N{v,YJw8YCs LR@BĶLtA,-ѻR)-&S|'vvvAȵf t YB4’qcpp-SSS-*=ЉfSۍ7oAfU2r w>mR(C)//ǙϽL&SԜ]ƍJ Si$"?AD=?9v;d> ϸT_W'IHaX$oߎ$Y111us׏lY*?KKKt#Qc 93D4AM)Ú91h`9X ,J%у2d^S9rD:tuu1KIIctl6ZgKnchhlyy9svհ:oF3.@ss{(c !;f<us{e2o>Ñ hTbOh' HBA_J]}hÄ':=ÿ~k6+ wpzA6цgДnGш=:u iii{{{#,GYYRSSʶuA XX6wfRcO?;Y,O 6;ȑ2$IKp\3475IZ ш>Т|dGrAKMR#<MZ H@$xP6, RTڕ衣lГ| 077'\RbBdMЈߏ7)ݞ CT*,fYs3Ѩh,th=Gt388ȼ^*.\gjj)x({N9b iduNLD[/*_|/G1h o~p َk}rsxܜՊ2\DZEV%#Iډ'$_7odv;%BK__Z>f[.n`bXoZ¼L$''Qc1MYa2=݊I!r5ѣG%}A!;8<sLm/o]~Kd(#=`3ۿ̉R D,zhh4Fx<fӳtV1y*5d~sYF[0X,JŔ$R٩xMƹz4$x `W|zO}.sajJ IDAT=qzaZcb#AKrUx<{jyFZ-ssHUKsTwՌYfs\ly6x68 rfBW uB햔R>---&;8NXS "VY}]p=A8DB{"LOLL"q xDJFq~6ez byNJtiΉ< ΜɱtAOO~dk1Fp>å?W7/:>#CF.UXv;5^S4 %A*1&8KfܼyJ!e@áY3?BR31ϔ/ace.ƿ~uB8o=Gcq 7AsrH{ PW_T.B)(aJinx<;P3iEѠ"eX`4%|UWW3RGvI=X N,bv7Z8-2e{CfFo6&''#np0MSStC ,X wfhDJFvz*=H!;ڮPhTU&2-F#fCoo/Z[[q%0]g~)3^?F:q'CSܹ7k}ɰo';K4j b5$gNX1thnnzf׮]n7oĭ[<466lVxjwjjhĥKK0mƛo*y/)#"Z ADxwVEܰ =<gOg,sR4qN:%)Nj0Xjfx{l/PzZP޽6^U(BRNF:qP__:;;%]#-- MM1qV+## ̙ Cf 3rµkG‡x`XсׯlXX` K`:>&$͍Cpl?5 4jBfgE>)>)݀0g27}бҚE6\vutttP`Yl͛7[oh4l6KWh4(//k;EBFo?]#m^X2៯u[0YyĄ_VàNzO߀! 癯wQub& czOdo d4~:, uZ[o֭[")Is+|f_=;!C7_ ټi6i> 1,:.lx"\.#_' h"T E';Cgiw,0zDqۗ.X3N'OJzMDY\3^&x(86pMEXX` L:+A9Fyyy(++7o2P曧NɖU(ҴI*ӑǥb cgkrA@?]F_2gq~::;;144$[!039Y8CJm8T^D 11{Fekҧ[ R 󲘲LB,I E×.]Z P|YT.Spg26?a#Eܹ7?08L6>-^`$_W̉$sb݌eZAVCtvv([%pXMZZm\gll\rR"~Юm|^\t FEۙ$%6z6  󼻠-սDaZ::du%oTcOA:=qjE#~8;Ӡ' .K 2UIԐQFucv]C Ljx׿5s|h4jU2Jwlи8.32 zk1ͽ[ff(++C~~> pAn_j!3AT67KK!Ax(jEci9ˏ-8%LªX80l6tttȒ@s(l6* 8B[[[$;wN(//GYY,XvC_XX(۵&''MA ms:*T͆N;wJ7b677k2X6/RxviitCL*>`0fkwdp M*,F!nII$] }sY@YRfEPLzM*qf<PDY g~C&?kȼt $+e[G8NhZ>'M,{ +928lEUU:L=V+t:]ZyZ-~t挬{ߏ~"peee!=]E<|,K%sX(***hB^/χ.^ܜDOA  U_jl\\^NAKq$٩k~$vf㕋ᔴǿ:$z~?.?#bffNǡ5XtttaـOIIe===y^tPC|U}`hdbFoo/FGGqѨT;).Tatt:%vQ#JDӡG***;X,v(+.Ƙ/]b.{z7 s1c mԘoFhhYӡg~cI7z{{_gp$KKK%;z<怌|edgH uRXZ)<>f"=]AD+$x /UZSRRΜjp=۳RaC;{ qa,$z?8IIv tn7PUUr';޽{t:诼wGt MMW^p8ڊrTTTD/kp# g`` S4IyyyDnGgguhii I6HqmLLN2HC n29x-ÒXXi/_sǡqoŃ_Dl$ \Sm_5`NS8xpm&Obp؎d 3O QY]z,--O~ )UeikMMtpBHFgg':;;e ^7 kj{ڬ'C)Y~pYܳ%q~8ЀnsۍVb}_&nG" 2 NӧO#d6]-~Ϗ)1vGc=HIIZp:Led\>1v!?? +C>Յn6mnzVh7 Bfx]PP-+ |z>_B|dj]])=ߵŞA .!8VCtboZ&el6…tAd&f۹s'f׏45f}o^|>xޘj8q@aٌ"TTTD7l-X;f/)+ɁNF#K Ev'X;z-/;vLJDN&L3^?&+UpzgϞ$< _fR.n7<ϊFU V(wpO!285MqS]`{g| 6+Ĕcgkci꺇/NO%H 'SR<>VYʜ8uuC.~=sZQɈx*r:Km_̙)$<{ u/& &<}]#p:UVE}}=\":@Ҩv =Oa!eXm&p`O!>":h|fq}%4Ч999@k&nذ$vbqm 6gǔb@>s"!1kɾ>!kJm3oazzZ땕y^xіj:XVfjVo6k4-nfba 4E_Il&x`q`O!yj,>kV, Bb̜xWQ..زfpj\^qԨ(]tX=j>΅ oxVyT*JsFF1oJ ڪ;;;%Ŕ0c*Zo9p6C;(I8r>{s0ku.rIbu6N44eZlC}$Snk޹~|' uXm&PQhS, }:l"냻2H=khL,^Wb1q0ա;Ͽ>@ѬHPR琚x&  "t4J'%%!+++;PW޺5DL/ LNuSӸ} v=&9Css3kLn Ǐi=j5eq PZZ,$0b*#T̆ټFuFj`ÁrAse`;jU5TԠM5 'FGGWCEsez, ij򲰰i/fxWSBm&e'_ K%9Pl! 0Pj3QQOgZ 9iLAJ>6'59RHO- h2KR[Ɖ(eLpe1zlq+_֌1bCsI2E]،=&Nj5sNC}]N'{zVl?b@ˆ ?z!&E 3WyM#AJA!+gRZ3220??BTز%)Hߨ~g/oTTA|ipكeHLLDヒ1993zzzsΐS8NVzP200s}8|EV*txKN^޽|,l|G͕R&Be2??r9G:ll{ŋl}8)QprR"l05{v.$‡ÕŸ.> 7iWMM5pʕ9&OYavGYqŁ,RHOr b>S$x0L0X'VĬ{D)@̳bU/y\^&1bplk4 Ǫvb'F046cbsXk0tF*^߯ŞWP@FEI>s1/@[^~F#\K"=-cqqccclkgM*˴JXz6[_kZ' IDATխڍFXCOT҉\r1E<# P Iz^EO+٘оvR7f$+20gwHNJt;JvlރkFwF :UpԄW^yy㤣ǏY{$''#%%E6#}BBRTI`ZC)t:҂3gt6#zw,,,`vv<&ӗv' Y.JqJKKCsssLgcq:`{XG@drJOԬ^?eyYԘEGڕTG/d2QFlvchh英s{N2щaU b"f@6! J4 b>D,ϞøGZ^z &1aٳ""2ON;HMQ_rrF0)elbX[,ǞhhTq\}}}Oo.c/ÊGkkkܙm>Yp864U#111l{]N kZhhLeC '6 uuu!B)/҈HH& -<ϻHLJJBVry2"bxؽ]Zfg`n沿\e{uV+ڍFLNNLrSƸnF댏3of`N1zPpiN)娿FARR޽T>Uk!1^CĚb1-vYHQ%7xJ r-I]5#pOz>}/RG8TWWmn.ӚZ .ĩ)*F r3e fxF//?[7zIL(.eۨ>YΉ ݚ˴0hllds dg,ϩ)*ǯ=lYim_ك_bdpjsԄ:IKKc`ۙǁ|h4OcexgϞ{~?DCC'{ըfgg5Ç*Şb^sʃ1Ԗbԓ'Nף ?'\$$$`nnY8tnlߖO11?hl??wnxzXVw&+Y&3>Tk`󨩮qqjQS]Ma^#++Q]]J~z څf1gީڅzoˀ}rnd`a؞C# 9Cn@PJa"ze_:,A,/_FwkjÀJTW8Nn[ "ijv_|erͯǫU8\YLqLLyBNgeHtfblCm-j 8N_w2RTIx`iPe+WlvW._L\zTW&I\F?66\dz50_:wJQ%Bf!˼:qtvapԩ/+= ' A 0 JCInj"-]CE=2ŘAJc?~ub9t+^\h%58h4+_qˏ0o}Xb; qhnj^c.p(7 ;pNá]] Y$z~sxt:cx L28g(S=/+Wp1.ǀ@Dk[cn~^G^i$J. EES8 @x8LRSTxh⾯kڋAt DnjURTfk,ԀM{ؐ}).x0=Uhv`Y]Y,}>Ξ;S/Pӡi%N!XSo%TgyA8M-nh[~湣ؽ3f `1X֘Yk0DÙ3gh y¸P]SZA y8jUpaQR5}ZJ5`2,T#' p|>#Y2SSlYj-{uWb=&K2iZա$-- ni rŋJmx&=!A(<A Po\JR" fSN?x&^>sUKĜ_|nǥ˗SmԄx8Abnn)s]A6jvXN|>tjta ԯ5P 1ذ@`^=b!E>JVqb(sb&XOOOyMp)R{*Ck<21k)":AZsUx>?.ʅ&Maⅼ,8K=a8l&b硞7 ClXgvSNf"ѦPl Y VÕ eո%T"gϢ?̗jyޕ`KZSo^jMͺ"^v""~9\Y} 50NvbdF%'%j,cLcjdc#jqʕ F~dL!FTRR"v;^d=X&LƁ=;p޸貁uuq?Ǭ"L&̦[%:$C]IIH/` {)cGW^yyÉ]v Jmo< AQPPp)սkh~@tSqw:_8%z0e48{zgjv6X<`hd*сzh4wT*jgnFFhssxdȅz?&m0p<u wy=N kԎWCֶ6fً!+= S>Q‡¼,؆I._4 <֎+%%%? ªe_X](AJ \$X=&}}}ާ=F֍ް N¼,fiPI&qpjD̨U8\[D 8Kz%85dt`M-ۭXv#;;{~*EDw v)JX٘=x`)gU2h/-/ ‡ %NNO2x<Lnc*W._,J(/^GsSgsqww\f~Chќ,۵u:kY?s +sQi|Ba-^PFs7MMZ0z*xs1•~،(BO AJAo|JxPԨOAvJVȄJz 1X=;Ơ CRttݻwN:mN>2eyhmk˗j475LN^eLhkk[q$u_X׀L@Ēw#X\(].צ.n[\̻b '}:҄K8j3Q NM!A _C HY2{cEm_+V.M1AHXSzM'=ϖPz(,*^ agL& q3vNmmhooWYڼ\KDe 2Mtqq`` p,gss+9,.^X5'͇7si܏3h4^5LZcȉ9w[/*HO AJAmYJ oKI)Hߨ*B|e*sU_3K'qN:Wf-)) avvsQwd9zr28fb\Văa׋.ӃW^yN yNOOOԈA}]#m }- i:yVDü+hAs*=quؽgϝc.$GH>dhR19}bpubt  bFPÁuǀïa~Zl$xJ۽Ձ-5E%j, C ŵO)|cuy䠤 T=XR]$ 'Kwl }R&%O5eNG"HŮi\.6]oCɸ}pNy0ʈdZP\ka[7ř!Ve*i9H;SbpʻwV@PWtݾl6S]!Y|p E;+Wp&'bĀjuxX1kǰ3>dz]Ex`c7owQll}t~G[㕴4TVVњ> >?y㍠BQHnA(<AD _Vw"{ҨӞt*W 3>rSr+V 'C ];$]| : ̮}}}(**FnA,,,޽{LP[{?SY,'6G^/^{5Y7bk--a}n4M@oƭ[;{Cf$&&0/ci=ǵ0GZJϾֶ6Qa#m3[xt kPXk`/qt8%:]Ą [Ų f`Gm.cgF G\נ"5E oޙ:vaq ""|~֭0R¼,TcpXÙ3ghM5Ȼy h2J~e-T3ՙ 0昂/G?3Ftww) ^wMKv};Nq܊!fbqݲ)$,;}`)ʕ+fЈtd4Lpkrl4nao`yςafT0fω9wy*sLQ#V 8պyqj =˗C)*2"ArR" ? ő:2.dc#=\&nrU"@5|<˵Y2r2Ҙ3A Qq ׌tjAYIk<\3 /0peqXKaȴ>{\%Gz}Pr@ ^bz7ۗ*"cISa9^ʲf 5>V5 = x^?V"_{uքh:mTߴ4Qf @TkbR sWW:4+ "|!O+ow; *DM~/)~9'K&7dI|Ç4 /> ^c*otN_ {lFyyJHH@NNΆ9今rפB݈ ;wN˗E _> zrKä1MSPCk3lE̙3/WL03?3^?ncمyY(ݱ CLokk{LhG;:`1p쀤@e288^1}4g} pRIhRV秇XC4>س#u]E2FzZSӚ:lm"6yuP$DġptbElvL =F. Ij&}Õddz(Bq}5X7WYY<_ig_l6ۼG "xkor XhC?AF!DRaUI^{=Lu83N`Yl^##"DoQ)N50ycb}+p[G A{wSfߏK. 0N'b||^*TnNm&ڍ%ʋsQZɻC"C>8'1y~ho^ atkYy9D`css8E|8w:v͉ D:֦&ڟ&f%+++q`ppqg^z Cok}"R%"< IDATx^t;>rɏR!u(_!t9]w%*YEYsKX=0 $AAD Ü5L/x[leMxFLSo@\pc y)CEY.u?@uu5x㍨2TY 20iЀ|+-..ppd*̯Jfڌ>R[]=TWW- Vך(&ؐ\p^ujy7WbP[[:=_p#AUANs=1GZ׋7w~㍘:l"CVv6'j  Zt9]4.Fݭ͐{bbׯ_g0?z z7|G1vV*Mя>BwwXaZE = ȀawJeov̼;2:aWP⁸;톭]d雎|? vNg_kjv>GEoo/ VvVR#l(2V36m^tGjk )J O;xG"ɠ3*ADc_}g+`NE3K`2h`NA!\-2's2zسga o4Һz*d:H$q0Dd1m&暐 wQEШJAH-87eiMMWCH518t qOp5wz``` =}P*d%*y҂LV^R!"r.-0`'TȐmCNw`wuZs üHHD4B_Agw}XjVC7I8._8Q\w/bP+XlJ)I .+&S”tگX6f?sPlƫY啠nt5*V \<sݻk׮EDl,H.xmR@"j3`[E1泻s[ =+ZCdp"pAr XdY kbk5 $n55p1t$|hlqW4ct]WWoOp~0) eu݊<起oZܣc2)6=_9y\ 6{cccp:d>H␪ODV!w9s/xtG.R oAD bR J~x/8bO^V'u ;7s8vO^w?|'ʇ+810gg/BnZ"5.q8SLe\5dbYլ7o"kQRY7fVi3Lr3޽< _$#Gh@2YMrZ46JRAID5\I|Ā.QlƤDlzہ{mgg'k)Crʕ)Hj3BSk3;roD%E(qx~h"zQ^ rٔhJ$StAy5E aбwN=PB( E&"V:~&,y'fR>gg0N5EրnBD|<]` G/A>&kQ'̺=6-"z8_̷aTAD+Oa0 sb,{\\'ܾqjuN p)nRdz@zkbyרvMGVQzQ2B$g\ىNA䕫m8p`=9/G9P)xc٥R)4MX}Չ||{r \fFؔpc{xf2>pV@R`9vUY`5 Nɫf餍(."qf&Nr >\"*f>A.*cU<AN/6/W));_''y ol*:Lp~b*:P8>{\thV*0>>A2inn%ގ[4*4*>?=> GT8]n\:͆ݻwc׮]&x%???3Qv}&T"tl :5xe<>˃1Gߠc ^ubA~~>5~wkg,ǁujy[.=ٌ1?D/!W {Q0 3AȵдuMd"hko?Aw0 $AEx Pn-FZ~F3e(n⇖!Hq ^*Ee(=-m^WF-{:g;}JUګWrO^Y*:L֮A"QnYnb6vBGss3fW-FIA 4h"9 8nQ*dH'bـdJP",\:&x'TNYB-"frL(3{| Q*'(Do~Q" Z$"V. aL&Vxt: R4ٟ*Z)_E9%qpcƍלCy8ZwUz`UIIJޖn7Z[[QX ҥKpݬVp8ug``8CjGD+KRtο'Jx"An 9RW iolpl;ޱ\aejCvs* y|&D={8FmqėkGq||. m }<.;T9su)IM~ը?;7nGW. t1n}k6jaHRèSÜcA ks9?L8ի3[4*4*|qFF1ψF!be8$ZSB AC%e0 7QiQȤH'"Y°{#ӹ :ff#v9 -zx7y٩r[l6|YjD/q(h >z_1͡?DwwXTA BTp T"]]]>fi,Jɠ Yˋsn`:B|Y(JVi/^Z*PP+W8zpa֭=r5q@ʘW;@\ז_֧q*V qS ćݻaw8X a[E`]NiA&Usݻ ʋs"b<jI͕+WX [_HsHKOLц|}jÅr2,rn|t~߄9480!lˋF69̅"ɷD2|y]!2nњ X*Ts0(s=>9J%Sd*^ ujjB_%'ax@;UYc޽Qw6Ia.AĥHp1kG-3 !jIcc#kta,A1lA[*_VC.SEvVҒ5xSK$‡iHֆf`ǣ A.Ş{.F#l:=c *Pͥ1ljy jlZ|@x7cb=4t xʺlc~XZ9ߣR!ZIl `+p;LCIPZɋr׋8x*m,q^l]$wbv]*٨DM̟^~$>-}NB<|>J;/F 9Ug>rjaQM@kj5uJ`yG,"A~g9嘰?o@eY+|خA}}8R[K4\yPȤ6y]m\s>Vu sE9y&m,;degSQp| 0YI b :" ÜX˟xH65~.#3- 7B 9fUw^\! + |(BydCڃڔҠ;G94[#NzJ'Q%!>ݬ"Lg[EqąT6+ͨb`[oќHF(Gܽ Ւqc#>>?=|c\n=VEX"C E-'jT StHK!ĨSc%&""2s=d3OIRA|snx^NdW VQ3lw ͋ 8I70KDh7qo.3!;pLLLJaLSIO"ZRvNqbZSaGRE]? O0SfL~^]]M)YYYyDTs:_@SDP^mלh愥uçy (.3A_1ɇ~Q͛"A %UЉnґJ`D85s~op ר{9ʨ5\fØVx)OEY._s9*}uu5v%aMBUUjYZjeUu+s׮>w``X 15VTocPװlXURBP !mui\'= Amz;jjn*8QSS9҂Ll]* <缼^/v*++9 =Cc? z7,"$mלa30xkUp76NPD\kCx}rN:LjwXL c$54*mO֪~)*cpx Rqe^[f_l,#YDLSk)(=A_pT 1Z9CHax> Y?;B_Ǐ?eo޼鋋0 $A  D0& b,D+MO<4G1cG ںOmN=PQ\ #\#p\φ6J OVퟲ bw8p6 TUξ7^E!5Ϝ9êliܲ710Lh*V_S˂;,--4iX_v]5! - qc||*<9Qn)z[K  o҂LS"W""S;0EZP*dP*d0px182J2 :t0Tw"/=tn\s^T`}`6THHL: c|$kUp?$L #q/6=t)_.\;p:*$b߃?–t6+R!tIL TAuM V+uQ>ĹB o$2qW\G OH~%Ew):p!+ IDAT\lgZWf]CFT1!a$T6FnT;(תCBt*dbpdt^SaHRf^D55E{ BbiD%t{>8nܸ[J$qˤPȤHPQ,kjjb41) 9!sS*d6309Jq@qDw֦&!"??w9yV ^}h͟^~}$A1^K B0 d`k{[$Udd'%ٹGk_:xhBg3t37!QGD #n0}üF{(kN֡0Qv;aki=A\t wt|hm)2d%FWKey֍nyoH]tSB > *]FRQe!dr.B&1Ia"={XE: gBSt8t oy֢;w$ bN< A!crz)4H`LRCVo5S 6 {10ڻ<>o9VʑLz}uN y=a&FVf&/{b lTd9h܎avww"/cP {\#Ry92)`>>n:bLHH{.y fN򠹝Avt p3ыž1::*߼yR\\$$ DM >vcΫ`KXfB+ƲoZ4_,ӟZ-Ғ5x를gLR㾬ލ,Fb޽]uwW^}ECfw[XY?wTȐaDfZ%/cnwGeA"<-}]3mAR 5YCAc ^yU;D8VQ9ׇo={tC&f%+O̞ mt sM "V9Fy607uH֒AS(KImC!'cw;Pw v'ϴC.u :5R("KC+^ywxyQ(^ywx)=ٌh]TȰ}^{7v;uXx20\DfZ/cU|^gX+1+< 5'{}~48SxspdsDD1я>BwwX?aAI @!j4LU>c=#cO'LM5ցnaH;w-)㾬\O5*ks Պ*l$oxma%vO< ;hݎjss3ʕ+eT߷5H>ȋʋsb-J2@\4j&fm#t-6 DDvv6oaCj@pQAۺy< :55B!*ֿ):ҁ\Z;0Bb7.uOyU+P+a/=ۄW-٨ջ4(G- SKXO`qmOYU$SA l6E9' i9bopnSc łZ^]c¶b;En=g_rNpdF~Ld z)TΣ9`M x"5L$ y082JQt]/1Id mm-]mhMMD$/l[3qcNf,fYOp&>>֡FTxC+^MVkLU+ bLRCjw?seyܤX,y{<ٳ'"Ξ=:[pR!CT]cYg&;}>n<38a~);~T⁍"xʨķYkͣ1wtP"fGǯa@^v*W`'0RZ Q} }}űcǰG/:1U (t4Hl$zQ{,"6!kZ4yHתKT&|0x+qe\55;WN[Q%9#T`{htF ϏGx%M]V`עeyuq6v/F1.]bV pJ1큍U!NE^:u λz`Se%a|DZIP`ih,xtm%G/F9{ީMz"Ї[̷aTAwB (a&%oݞ492Bz]]s1)xxRN\P*d/+p?(kN^˫!=۳w/FhkdoB>I<$ɵ >)fH`3v˵ZO-'= AkYQeC]}=׮DpsshZ\svj˙:(R$kUk@/):}lFcCkx^֢k׮Eڵ4558s✨CU CT}^.њ4^T}"*yQ"hr lL=bВԱI]}=:*Z)9N$3ꫨĖ*2"2<~߰J(E% MX y٩0?r RF :eM|ߑ2Rx A܋f40%kfLE~0u<} ^>[9jjjP`ڵ())I1؈&1aj& fAQ0L c}aHPbaN\ZxbjS6-Y7  .HF]CyB '+/x׋={`׮]!c`7&"M87V DC"x؉vM 9BO?8pYY .Ʊl8p v{0:M<9CQ IF+ňR!öb,YC`t/ص0c 9\p]{LR1Kt:c: ڠ7&ܣcp{["@*gUױap؋E>Eʹ\;;a`ȃfYܿtu:]CEǍ35uPW_`)Y RwL'Q)R!s[›?/֦&TmBFQđZ|"l(Q)wo|u[94>p˱"0E16 g>IZ7W+XČt#a&n`ÚE(Zl]gKVBڵt6atuuß\̷pa& .K "*b,'h0(!^R?iZcjC{1eI֪T>]QɊbX]p쳂ާ#G mٰӦ\K$S?:?/aēdZ_!D1bnscj˭z Xw'|˷!muiz:~,+:Y ? &b͆gΜA{{;p83٨^Bnf`0ׄ܌q9&|qA`ڵ+B6NcGGLӣ.3E$u]c] 1@D4C"= $/I4*ŭV7wH⡐I ;3Ty}6Ҝ05JlZ\&8ЊAI.W7GWPPĥ(-|qHOԹ+Z\SnfC}]GRR$և|$1Mܸ1u2č3&çy=8PSS˱cxꩧȑHq:h٦ !YBsmF}~tf;xp{$PJ 2М,bb ??jŏfC kS0$L0>J9-Š%fXb7⹷~.`00o"׋~c1߂ @$A!AD0̠dځIуNl1Ox4+rIKIXa!ÚXQeR*d0z0z\@P1^7*ڐZ)_/iIL|tUx֦b_f\ԙp0rhE''ʋa~)t2 \o^10͘f:23R|ihc4r˸NŃA.C9Ffu8Q*dhncp䅐Y+JZ B0ޚgbH.uRGm5J赪)tCH -dA!`)/ΉjQ ^d$+dR(d{( Nxǵ!&dOш]UJ œágJ;Kxi>>00EW ; r*W"QȤP$ISd"m!ӚZV,"ɽ{K$S6ic fW-;g+47nZδd -DeY^H%kίG"CޏQ1B0W& HAD) Ü5L/yf抦V5:v^y~Cy ): 8bbSofݔoP=4\$OA9醘ZWyrrM ^ NJN7 ڡX?,+zr3 2&* =Lζi+/iM-~kt@Z6,=eyXSO[ه}8u~ESB 6sG/<=ʫpG`my9׮qN0njj"N_NwZ5JݣRpܕ*e\O"fCGGGD@$7,;׊%f|[49M0}MKɅЇ[̷:0'& M͛7)AD1&igZB{x$J>3P̓)3?LܸAFnWwΠS0OewpnƵv*2dTeVb0g.pwnҒ5B9, AO~՟aC[s ۘSSxjv`.A$r ,rYNK鯗bM89j(??.[a TAA1d: `X˿ޔe)I$HJ'AT $xv^ׇ}>{PG er|SA1_n0}h؁3m!)d{z꾆ZZ-SN?|asW/ſ\z?4Yx7"/:wxrS/ '9OpΧ[NePi6ZV~gJ ćO#$FƔ)1E  ,1q?]sNFu#H`3}(/zkVk oH@`o>ʚg}FjD"xyQz|E IDATe6+m͡gX "UXS[Z*&5Z0F->f_aD1]1Zr3 XS+ׇ9q"+3S p:eXmkiȫlNF0a(GFqc}q\Vb`.h.M ::DKgQ1NёDTV?ʢs$tuu={| V2 3HI1?H@LЉ R)jHWkΣ̃}kc r./LJҠ]u_þ_ۿlisS ._E\\nv4/_>t gݎ{z x lCP@{KH&Aýx,9svsA)Zl_̺/Eǎa4b4N$bz9|x5܅9&78`vCnt:dNzX.Ca %j\]s_ /}sh"wF!vED4 3w~=|: ׹>^"R!sBȤ4_OzpDq,^wS{v1:-Yd͔h&gD1çCffM 䔀#fvG.ȍ\#^Z;z"➳23u|1f 8wDL"m-;\,Kb[oattT̷Q0YM C d2U8"J|A(RAHAN7DUw‘bxpw^>R>r [ç3XF%~_34>,׮ l8R[499K.Etx3'dHk\Ӓ5h\h.31bqbAcc7EE8|e):41ލ6c[k0bYGvԝig8F 13fm}(btv~db[E1="$a~(-`&h7*QZk{}~?Zs&eX_`Щx 3 rPXZo#X,|0S BVʑnĊ%ffPL!"_~|>sT`49F' WvQzzs2 pgԇa][}uֈ>,Doyx?Fwwo $<A&5?ksuzɛv(3~1]g7wB {| Xg?zp! V 5!Ve~wq,_oăAuM yn1|` [yz0~'h!+3*t㍬,o;rf(>9y_|L\\Ĵ(Q+s(>4~nsl)/y~ePW_zx q"6y t9ApcStI7(-t >n*lF-^ھ!eu~:"U 55P[l(4$'݀KXČ5Eh תE0hh4";++#v:v;$n ?&E noܸa(GF1>>q'|tDVVRƨ6 [her4^ HqyUy w9s8z6c3qO0ǃK.~DhY/G~9 ؆OzJ3_m/O;FF %f%`@1{oo2SUe\?0-bPWPDFN;9,CcOm-|rш1B'ǃzXCH;D&w6+DzDx)٨En1"*ҐKX*d >_*2K }aA^9xSb>mMR}H5 w{n2d5E hV,1#-YC$ x}෍5W޳ qoܟ0L r3w=> 5wCc=wcJt q$Gh J9^58H/mל 8}hϱ<Dt|6:\T*4Q0  !ADa2-{Bܟ>iAKuj _yR4>ȺEUF% 55E om GϏk}Gx`;ێ'_Ď'pu&fOgX7O 5l<[/ý" ?Ee@D'  lw]t9-s@Qru>J 2ɿAٶ0KX0ˁq0ê. 64Fb{|MHƳ@ѨxM A8F-.@aiVB00䁥-d̆҂LT%|!N %2K$|%kOY /wE͚ztñlD312"#'݀7aMB{#b# b|Ǟ>i@:ӱ1!0f)Ƭ #ro ?ðLJу1C@t-ř3P3f;s;}nSs:fY\rB&ˡ\.+JA;H@LNЉ[ php6y-"ciO-É فbAIߍt~n'p^ݽ#]s_*,UQGaRcaUgm#Ըx&g+<^N"SF)-F2U6Bȳ%]I10DJ L%qHҨKL!^VQ1+D0777ZqqTߑ B.0bYAQЁ_V,1ҧ> @ *zÁHz0~yXZ)#qT[+Q`^g'@'?֎^\܅a")_/{n愥ֳT*Err2DaZM  "F1L;DOJJ:(Rv4$ F)dIs=,`#《 ML$`F4&xK/'sziɩOmTӚ`~M/ m`{ Q hP䪎 wfanclzfZ{?捻CѾiAK0W~οYy== g+xx,[#؅PBh᭫ȍԱ1BA(AuoT!F{1HHLHEܹLO *! OiEv_MH0hS s2*U-8jE~"$$qqq y=KSkAxxM AD"xA qD1qo~}^Y'<̋}%wDdxC,]]-U?BmeNKKǮgW|Yo4F[K_M{/ç[>#ŋ^ $M!+ޮag^=)+Cx//ځE;XC֕S΅h±^ЖpՠX}1(:Dg PDHN@=Fՠ E~ЌKMȍcVgOyC:E݁~QL<"`}r0%!bAn~h_]ǟ~EG±x~2X4)3bޮ׀?C%rv+r2mCFd%Ha$ׇJU+ETœL/P&tKPh6<HݠpcVR,c#!bpuB%C5PS^kQHϱ>?Vag2f]"fM3vԸ]7z}h5qp:a`'tpf-Xd;L,@■o4U }يs52ӽ5oaHr |ՏN>4;ieԟ~k']PXs><%uaNֵX?\j[8\~mА3+yuϾss33*}e$~ 93D*e4L3Rga4(3NўCB5-GdNork?I=6qtcf'˿ű cc?ݖt" poGo䈠_cT[m:4쀾@<<s_HPVw2VdB'MGwnd0'3ӀNpiXՅ+U?LS5~m'M{/nv@u¿8ғH!t NU|.MZʀX,6ث `90ىk4;Ps9LAX2\o:=f"V댘#Z<7e;y پ&hlx*!:" 1/34&I] Fvw̟31?`;cpl?>y f988ZBɉi;ǚ`"j4Qa<^ q[4]дVupbiP O9gba֥ù5P8zrV{Z ;H@Arou%"eMUc!暝98^{̗{ =޿5݃9yFp@dhxaד,L1ag[9CpǸԤet֘'S.\eW?q)h#7zu~񩢭 [~Zl5.' cvzži5O\D F,:,)ťfn>&ʺ+`a$F_5kѤgꧬ۾HFzr|P # ݇B"`3܇g*l׶Q!,#F+lPsQw= ceR(? шI%"BCN-C}zѤD[n9mIq2_Ys=v T9(ф*8Ӓ㱾@œ̠sVI%uf2f'⾻Rsl%IrMu|>kAmƂ!*͜~I݁݇ʃBLEѦfxIWTHDB(fD#UTE$"עM4@dEN&m#ׇ)p\j4=.!!!HLLaVAI5A\~|IeQc%kPrY9ػ%D #Fe+C7kl"} Zuܫy(?t)^o-U?»G<.')N;7uM_Gx0~@jdʨ:6LLcqB̋L\C+-h98HCd}"l؅A,2.!4~68>lw6ۈ9vj.@Y (FݟEJţv5O^F |b -r+U-8jzJv=I[v:({DXh{7IA`EN&^T@ZZxB^7V]L-I=$x  z wIO7/ 2L̺/ `qBD ]O]϶pPz\Vz\NH)^o{^˙h8s7bKF@8@ k\; "{K'K{OçV۠A y:mh OY3imX3оoz%XqD0" o*|=[?ޏ|AzFAkM7J ń{(7x#=A#ۑ~ _dꑥܔc`Z[6LὔQCH NE>R&Pu>0_o4XEPqfaۆt P9 ĽbwD0Uo8M>qt82s1wHI2dH!)No_9+thwNKH_# *.*wɑ`֨cf Z4]htVGZouFJ\}-_a|ATTR)@VM=$x  F!.zmZexvj S0܇} ݗYr{䘋r6ter0*j4xRF۬;nD‘tgKXm<|8{\C IDAT9G7z??Ѕ:u]4y]\LE[Kv\!xK^Htո(cL[N¬#KHaD[ IqtGPERB`F$K+=}חd;+1# "'u)R1d0,0,0οBÙ!Dw㸨paŁ /H0݃QqI.L&BPf=31G Ah( #T SL@AJYu=;^>;%PIPMD{jy JUYualFʪQV]Dde()Սuo"UψmR9ʪȹowDc{)|R بxF9e@}}z# ftT^5쀶SxzV@ ; \._ ZeY166Z=p,]a\MMg.G@EMl{0ú]|;+}M٨kֿ9:V`4ǩ[._$˃//ځHhGxW#Xu~k&!H(Hфv9 S5KB|n F*Atbw)#kbbjbЌ.ydS0' [Pj{A<f{(ByNc8`l >:)3( R?pIZr<^TI=q:%W>84ϡMR1RgẶ~Ge1>T+ GBlϟ$̷ǵD6!ut\ݭt^͗WLDck /`h߸݃%! |{o7j8o|O@" &&V=H= ‹s $x  \!ˋceX0&z9ThYG2ՠ㏠~+k&u,L-{m.t!;y+oSg/$Pyq}]̢DrsBl$lS&'anOZ~mT+ M>T΋9h}۟{wx+^߯SV`OZzu_Kv=9t|ң0Lע;2pkF ۿY{\Dvq6;.rbAodmp7u׏~tï54)"7y_;2;@cbz0֙A0p=FM9Ft`BG/Dܷ}':06#W>R!دK:Mq$p$ЎH$@ Q,o.F9ptsG&M(W1.Kb,$g8xJ˧_@s Z?:mmUIAG ZCňJ~<y30bn<Ⱦ1_Qxi 9&\E\h uMVB= » \1~2IƜ6h⛥KqWTz'9 Z݃-if\VdM>oϽGG$n5]*-"8!pQ_EJO|'.1|V?m9(^8hOԧ^_.o53IS TUL0 4 &+(_BMh{#]4YQ12)DDdwdIsqX 'h`ɼ>0& l;I0o vтX$X$? ;#`җ(>pʫraf|dZ L˶|.~pSo8;A^M$"ɤc8ǑÝ[ l}ս-Ę=cx=Q&7YEg_?V.E}H@AL\.P 0;)?ݼ a"ji?W&p9R'`a> 9- C~}'1'"{su uZc;/e,aKX<021Ao4a۫Y/rP&c66億axWB>wWh=,\JA\?RLw?m` 'm*l:E e}u#؅޷Yq62ιg.س|qK]vuBx'˪/x~۟hB߾/-iOx48[pq]eisT'34s/o]5 А=۶7b=mf30>]!J?3YmE,BYI1A˜-6X&*JL1! [ƄEFoǧ+$ 4;h䧿)ťfAJIq2d07x:u%;3aoݥcn?NV`uhLsݱ8Zxn}l  $8()PJo9!gFː06 T @̜}er:meqi**U-N77hCQ۠AsJ S hR1<&`ڀO~{ܫj/(IH@AL\._ ar6]$h0s-چm=/7(Z m?,IE )'Ame֓o|@.,M4Hd2*U-(>p6#5a rK=0u3*~œTB;ЎWk؞\ Nۀ-s@~R.2 l?3zHl3UHClV{m{m10,_A#X+DaN3@E,T"ى H‚YHsr@%CCvgAhBً-:1#V{ΊleA969ͫiLm]:?r6H@b2CA\؊7:]?6DR^M~T=?`Qc}-X6hq;&R "bz2 ڂG3Z AA[b/%X5; 4̗03vMl{0Ffw0T܄{! |ՠ㏰'"*uv~wM_K KKr~pF>a¥}8r㱢x*ǿsugS6r{al3rPD[> ѾwШ۝F]+v%P ;z _~BT VÀ@!p8lw)`*xW! F&㉻P4;R?A19nQT3XGHضa) JU (3ع;0ʹSu(fµA$'Ƭ"vI_ìosȄfM'>rj褙ȡ0'!7VoK''ј ߥU|j-tA!UAZX./8_?}p,8F7zgX TZQjE񚠴NOI@RBDZq#8Xm]8ד)8Xٳf'J`lbۆP`rQz-:l.6?tsLr. ?R*s {hd>Xhi_39QtX:p|i#ljQ.C|Xovhm]:wx,DHSE$r JUk@Ɂs?߻E-   &lP US0')b4Yٗé/ܶ.JNԠ> s2Qr6k?NgMhJ_JwڔBl.~q6sEr.Jv)P)V꣞ػ/^/[݃g<*RgNWS0 uK8`=h)M맭K%}͞_Ҟkuow\m2fw "*☻8v!.th5ѪW*n۳T}Kj/;?;2瑰) r"vQRwѤQs"{\Ws\o4h6$v BCzfPru;;{u$9wW)Q$D K $|1]9a4:!Htbh^G\]I@Z|r/n.q=J,U+5(Gq.NWtu3SL?"F$̟=y"@ƚG5X5'h±Lx/P"OJ=1+M'U$?<@>4AZ  f@4_avR4~ylH 8u#Ww.6>ҷ]鐇%|܇}kT2}a΍^P;ZG+=*#Z$C͇==DHx{^ñO95 X3~QAo5Ylݵ6-#8 /Z,U|a }d(OOmO ?)7O6w}wZ^8Gǵrr\2vE&_é]J0Z슐QIQCC}nm<*H_l C!+4˕lf eAh(+A̞}vwCф& xH>oجohLJد݉(1 ܹvqhf ڻuLM@|u|{dENuL09,{]/sh4[s$~`? Ml{0')WfާP1Y_|;^H"'/n*>VZH%ںt~01nU#"L:o5>ݿxq27O;Q9O=ľج]0M{%j @A0B^[1\o/K[}8䣬F {XDI!O8kv@623&D )^8 s_G_^ IĶ.*GU%2(>pgEg;x"z0x^F&PQwG F L L&XjL9>TΩ[g=<ĈPK綣h$iPN|EAx6#w.\ZX $#2/޺blAcX-Iݧ3CCM2,QgWbϡryTm*w΋EE<1M4VwKqJT +W06wv n{4db$ذ@IӒU#dس} dGM~/aկ_/OW{͵ nϖ P8)vQ8<vCA].f'zENXuẅ́ a _c}m"F9vݣ$XR"cQQխ6;V~#7p9=~hGAۄ9|mÕ!H-.G?CCyy0 .~ CC Vj/HR Bc=A2=AQ۠Ac <يumRxX-~#bͻ3=L8F]~?CR1#_.\s?s=ٳNc^^u)~C7P}U>wm3VPœ̠z-rդ>+r2yue&YqI% Ϛ?gd3ͫ:Wܷy;DYm]t랈xde(@zJ¤s0Sf0#^NxE@HF84?sMtzJ+Z!073I$/|?]ji /$x  Xj1Q]ly4V}mϾSo4aׁS~;JU+z[WN <\$g4Xq0~0.mR 3뻨9>Qfd+6R >VamJOڲxn#h 6c]VK\X͞eH*d'j43%B*ƞ\KW~j-t7LH@Axd/˲f~ho6.,EU֮slVGn1O:z}h5z`Hd),}RSس}M0 0c) u #Cs1'-9[ƅ:@Aow$R06f.Dmw}Q<>=c)/WĈMYɸ?sLB)pAL•&hS;}lf/vȊ]8A*+r2@OOwXhl&bBџج:yS #/A ʪ"r#$x`y]%$x rx x&qt0ǞAyh)Q2| Ƚ#vp!T_W.t;u⇴xdNk+'Gb=Y;q ,}IϗaJz/ٳxa3ޖ+фGqz\y f"Oܰ@vݝ#r^o;![3,9YUTSΑG{>0'Y -WImLEanf~V~\OwA- Aj=r|- zT-Ɯ$Bm5o1ff)68'9(9^*x\^QԨ@l1(|۬_ըrW'܊6`۫7}P~tu="bNmSO7[ss XF.&f3t!GOo4M:7\iX_wlUOx'ú.6k l6 Y~¥a$f3 18hT64 92ޅڃ{FC=\][U vq^Rq6@=ƲzZh ںthQio FMAHBbk &g*CX3su䬔d+ x!F/ )8yU:A4G+Jܜ܁b5eC@sED/<`zN2՗Us"Q8gP@",Á=Fq!vOPhSA>z '1A{@;a1f'_ș-Iq2Nי;'^kZm(AD`@KAGhV\KCɂ_woJ=%kQ塶oYz& a[\l.]9QՌCR mCS̞-I-E7>D߾XE$pӓq9" >͗Wo[VUq3O1dؼ:" cFpcC2E )A5\XthZ?r v ޮ΍mAYu.2G+T~<?`Ubءj/PA$x  nDg)QmsKHa Ln;Oa8 VfɕVdbwIO.Ա.R8 cmJƂj˔zܾi}~~pdOE;ءQةs7"7^$مh hҡI9iLKGzJ*SY/ʦ$dד(Q}y Abb1a1OS 0k g%y8)Olm]:h;uP5hҡScь7NF_gx: B.bB!,A'Y EJ0hR>تB<vv3Wž]ՙپ0'#w?f@l$d3!O޺Y=z}2B_ wZGe鬊uR1^T0j<+C,/{B'v_kh]ȥӥh&獏pO$,H@ApV=84_z[/~S^f.eU{"B om.k?^ۏ6g{}PBJL}XjU-Wyu3OcmjP=cQka?19 {,0cVK`wyhջ_׋c\;`5VhwWʔ&cS0f.x wpl\mro`=r_N㲽hj԰mnى>_*QTJί33eF|fMFAw h% Lx604Hӣhi\0Q:)i^I-B Z,{I \Ghn݂jyӢ..?,ϫq*\h0q"U\DZr<(>x=+vGF@6sJ-޻<txV,ۓd Bg7ՁA۰#00]>T΋y[)Wu0>j;5 # 8Cn_wJkpZnvIa `zg׏s&~œL-Wl_[WOi]C8j\w}/:5X5kY*LKϊ?H`0Qf)#IS;3}.c%Z5pLo~}9TΙa$7;ܫu|heՐoAKg.` &8C6HH@L48#njcӴٳR77Z۷{`V F3NWckܫyHDR ܆)3g&Bi۟-@|l1uKT7L8v?/S8x#dpscQr.5lr:/EPA&4GApVkTq/&’̙b1 ;ns0U3lZqvwWp>PP`EN&N~께9Ǚ"l&~cGXܪ58Z|IjcYR3UEhى}x{HON`ʕ%"=r+QmS87Q,ޮ[j|ׄű ! #5MfckICu-FdCZrYںtxx kS F3xE1` 2b $TAUC:B9OgWu$q[{s^$-96QzqL5eV@-J V{A.opNS0'3>+g.2~$BY>[W߅zl_3m&mCU6 ;οyX⤟Uk#B$n{)ػe%'j4k.g%kPr@7ْ=>pe4CSfe8l1sĂDng8{5^mX5s9{K c>i&pU e[qj@dN/ځvXmnu֧[Om@jD[9z%x V| KG*YkDg+eRՂsVIiG_>Ӓ㑕@V) ;|2'=j04.%W  abX!-5Ba,{!^ݵ0%'*+r2>9T3ݽO=!T @L̠跩-pv RVYנKНOς6I'VdҜ7 |5_:h#xm"O9C{G +! ¤D2'LjE؁ o"gq/:v\uj? ?cdzl{0Jzo*U-GVp?f\3g&BD+#oǥbR)) {[GbL08zn+xn}Jv=⭫H`XGUAV=H-J Zv/wyf% "2O漆9Q݇ʧMSe Mk+S9[m;q-n K`k|ulBo5w}+g.5(g]T8`v2!%\-lk! ΅gۙD"1!F!h=X5 u}=EH&r11rw|FmUT*VfGѸhE}wH(?&ep(8<ȏ2'g6ܫaϡrZMs#K_/n*@aN&-7BO&APhGtdh.Z?6߹asz2'`*66ad )~xo㟟؀(Tf)zɣ PE\m} otHOI@ɮ'QG ]ygGH9~{%%P z~Zv &A?P P֋7>b|Ha.yczVAJѦJ+z0uq>:}(9Q8Vo4h_G֦ 60uv.ql&ٴ; XAۤ}J"DJg?5 !R8yl|(R#S[sG"Wz't7Sbqݮ˲yG\=?F4,W꥙ R" ̩Q8DaNfkr.󦾗lgB>1D$w)=A10L&bLwoR=ޕ)=nA/|VbqrS/$|sӟH*t볡g%]L}h - s2yu66Φ@wE+r2Qu%9JؼS\a\OIh *ZG.hr~g0jR3*%rÏWuͬsi6hФ@[nzPx-!zػ%g>ׄ܄(צD}f?r[W;Pf) ̉HƖ.y ;xMk7(z%d4`l9|c:_&O)rh:m]}])Ob~ ;/ˬ)>p{_Pz VTZ|n=\m`g WzpO,s_G'Kka8,A;%/9)/`gUhFѾRپxhsߤ.!RU8k5.t\;7#`m'o}ʪH/9U%)NrƉQ2eGUǗXpRa@;+Җ'W׻I'Mm/ e<-4A_8 1CzJ<"O@H{AYu=j=<2ֽJtӔ IDATUI p%( [QVl{Ef18[ѱ{g {{i{t'Eͥ)D@+TȥRUI+TZy@R}z/u}6"]&l~  >S/A^n>-aȕIʊ%]j>܃l͓HRͨ_K} R5®! Dp9h˰ʘ [v._d-ul;n_]箌DE% Q>?{[oHTxWY.GqіxP["4afQ  0LVxn{ڠRqsJ}=-zڱyw= p70w?msQ=s7jz n{yZCmd)>MkPt+~ųyw=v:}Ly6PwFy-\!4~GH O)/0 F "GAD`Nx%۱O1W1h6|ӎ a2?F\Zsn5>V!ݽcwke˨>m9 X;jkL4ԗ0lԯ/Zs(Wܖ͂0"4]kØp)RrH4yKy59 z鼙v㵾nFHM2 ڤTQx`˴1)^QJ6ڰiA}_(PeA2eIH@*ޠ?";$xW>W gNE,Ό)'@Z1.zp=b SГ 34QAb`xɆnLug,^b%%H\laNst1&6bİHx'O˿4᳆#8y*2}#x7եX@eT}#صO~?mj(3z Ȼm12@^4b u7_{4am4A =  " 0EOs;~Q'͙߅`aBJ{Gg=b3 E^lz@L |tl2L\W6U6@}#OhHfv'D`Y~'&Q8;H[y;pWkv76q{=^/{c2;-sYX}r)ȽWv5OyX?2h Y+v}j-i Y/9nMu|j=r6C{QzT&T/Cm a2"Kp'AH 5ȮiE̐Ezey(yQJJ^aT" .YAq30u!t |>Ҍ`ZA@VYZ!"-E QqO Hܘ[,V8]wV \Xzg%Y Ŋ`ߟG6h¦q]޲rplHK sqENeZ=7!8b +z ~HXIXmxoJ 054A =  A- q{Sgs`'}Fɬ7|EuK_<˻..P9l]s߿݉eٕ>{ >l<}rn]]>>+;])4puo'.i3՜#ݨHO>d7R*T9^/ۀWCyBM><njMS YnV"(HIR% "cqZ~카ZµspKڂXbܽyMcF5xͥ5sVm2T kt|_-+1ocؠA bO$b}\!„T I^YC8#xnW$>\[ {#P*I(Lz a뢀q9b`e2 nYNvrQX‚TlbTc6l6[ۡR%WW _u}#XGJZ0"l9[9. |"#lHQup D粉ú_:ak7@5(AD|C 0̐V 5^qouԯOfe"zF o2^5jU6/}/b b>fl$.TfE]8r~F&{GWb^ir +0;- &uԎM;b?],hTwk!HVrh1c]0HQReٕӺk箎Ұ &srY?ʌ2d)2".K`ue)!KK>QEO5ê% a6mhe%T„/bg^u-P$$]XHlc ؊_ĒY{B"]5fuS%o8D! f1ӉPp94K"]##D!GAEM|oܼ'WjGqu!Di>fD='YÑ>l]qSe^=J$Pb0zV62=ޕ~F+ÄfV\f X0(AD|CADT$zh@vjų?]:;݂́ԵAAyw=:lֻ$v?oo|~ڄALpwYTD *"T\.a_N A7N6s:mS9Y.e%J# |Vd1 7N8E{[y?\2rﻜ )y-\ óh{=ɎONhDhJ 04A& 0ZޛD|gGx$ceRr=¦j+ )-vlz o\$a#wF}cFwYzMʆT4@æx9oXF׺B>tjk\:L\Y oOM(y>?5oCv Je>擑YqJGO_ֺ*o>GW9S)\E=,g촂5 :sxoEz sw<l*;w};lj{]e1ۛmgR)7gsk|_5Lclhr3JAuu"Ht^q@pgЙÇ߼n%z䣬D*9vOrSmg 8y?+e8'}9< :ak_s.S/xk2%=R>?eyQo#|c >Nr.<[)-i "eR}:|B]geAgᅯ^ IVWE&D,e zFDP$\VJ$l1=% xb!v(;ADXi:/_>܃7~TH9<aD`rXLіxP["4ex<A P; "0 MVx*ޡ6r|| /]j4`Yi9>MxWv^ͻ@r#FP؃ÍPc$1-s^"!93Ya9A sƟGw0vt"4054A  aqCyNvao}Ϲ^8;S6/r9& MA:A M0׏b dlY (% MhnY3gvv8>uU[:|"\mdj.$bMٺ "ŀA m<V Ը:2D]wJ8P2OGNv~Ғddd 3#g~ L. ݗzp2& Lln\JNFibQZGaA~X.îM()?,I[Srw7k Dtt WMXC#Jx %4 Ez(ߙ=ɲaiAT%qi?[[7o{/|{L5*qSFknBm`㆗څϠCt[.xSe;xIk O} n^ص wCnqyu @ %T$V=Q76E I"UMd>"\e >m˯ Q.>C0n63sB".xxJk\HW/Xk-"HHv$R;APx< 7f@rS0W  u(PÅXzJgi B= -e-[c_NGdu;q2f'OS׳RDSD$>[}mɖ!70=[w@ODد+J\Kabs`'a9)jQ%Hf-{qC[Liů ۱dD_g _`߅j]Wv}ڄuTc%esn_ޯTG$b R.kE"!J"籉ú_:ak"4afU ĄkA10Zv )7mwl~*̓Ahυj+ h@2jǦwbƵ2L`6buS ̓0cF0F`vZ>5m0J֕sbeM`{݋?|~>𙐃c`]6llԯ/ZOk{/ַe/IрXshi4#֊D=|3+rMsw\Zt+ T:~X]UK҅$H$8}tT$b_IdNABb;L"r$nwxuS"8bGz:"; ͉ +~*yg  DE^ Ibpv %Xzg%JK(,y=˱Jo2 -{qWE_Y2u"sx$$. 'BS&ADbC6 aV4s;. 㗻?.yOX/1 M%MGռ2pl o}7>}ޝ]6/7g؏WwRTJki,<2g5\ϫM|r:L6S/9)N)9i,{[9L2 ڤ+¿˲+"M \ E0mo ip:Kb#wӢNޗiRfwܮK[' BX< a$ 㦾%xE`yj.y *W%';H$Rˏ G(-ѣDjZ&!Z`qz)-Y=5*U2~O/ޏuxeԎCx%7i}fΟ9Ue$gB@ =y4O0L(ADbC "&aNVx>qo;>9-\Nd`5طT@װyw=^zb%r}׸fژ#]h2NFt}n"g *KP[*tT^5e͜UHkhOy0=GGxePC$>7]>~! Y͛Gmصrl*Ҭn{C+GxmqZqQ#EΏ!B~5XID#gAQ\HC|C{'p؂wzX/ K OorQXPtfx&!.^ܷC;# IDAT]ݸj|jSqiۅ,J}rLCFi_XV^s3hm77>nDUN:-a ԄLA\}G[/&BSv2 F "!A0 Sj<$Dk34van ^Gҭs(WI3⣋Ƃ{8j#\m[~x{ ]x|~s^;/_úWCnzZi8!z0b%`mZ1D RejǝB2zZdz?_x|tS܃i2QP2p.##MA=zˇ|llѭ0:x鉕l-AAoCDA׈ŀ"9gHd3!<=܃sʉՁ/s0wT@ш F:y B᪱7|+;*JNSX_ ^VPTFDBAn[Ĺ pa\.b R JDr"#F bf@9 X@al ^rF orHo=m:njM֊A>)Bkg܃ł_,mhxnSxð~FpmV/ nT业7Q6*[mW(Ze6{og~3²;ߘ:P-Om=_[qv˨)\ӯr:9KPS(j w CYq6={R   /;@xصٻŬIDkHX4e̓D1`& J_W@nvH@g+ЅVY\]ƅn=P%'d1yJKO+\X,X7/YCqw{8IL ;xXn'29 D,r"vt"4@50C4A3侑 ejs[{MHJJhe>҅bu`o:[vg?Cٺ,21#۝ne`u]e0沅$y 3 8 M1#xPki JŅS3QMʞ4 {ᘱiBtCP`E4e"z4>,=wDBybtY5sV úՕawN"  fEH  x&mʢY|=_ ɛD*)}XXT/X,VtvuY1p6bhZqi<NvԽ; Jccca;sJT&93 & con68DDwp!Â.+ g\@Nhr`X{<'qB* 2j+ s6ѦwbƵQf97֏]lԯڸ~tSu~s0;IY2ld)2F0d75.Q?>13[Eь^d7[o\Fx;<5o_qT,AFWPǻ9g?f<` szYq1]f`zRҡv3] :ҺP   C⅘F$b]PAD|1JK$v@nv6^5bdv*%z1g h;skdOvDtNC( 2t3bHI'CQozh4g 04A3 2# 5Ж4H]d\ko|l> ]<^r6l3oߍ |tS}'̘c.[\Wf>R)RUT37t`vZ ̼i/w\e]wbuOqY\my#]7}?nW<[UwUrdaUi(ərST%)irΠ{Y-4p}AD!+~QXmDh 4ШAWE AA!.'06qnlcge'g່*E Rrw "wyyP*BwTIW}ӽ/AD o~x2Qo1 SGJ13ǛAD\0LV|3ڇ~zAǍMiCam.y-~XWyP5,?km R݇ӾG@< J"ǪKebUeA͗/EJcJ:ADv`9<Vr2d^;Bb@* >|! D%AG춴  ^gaA:vƀ3 Y+PDzJulq4Gh7wOB?g IB ! cn&qtW83 MpzLGLaH |bvZҞ8x*_S], Z[Y0 3fy7>չU1׎mtNKYq^ZjA8lLxm\.|Fw"p9YQ͸lQH$b3;92F&Nv*A!{Sԅ_W;]~7oJyp5yL*+Mob~i41gؽmeH@?>9(bajhD f6 WjT(/biiio#](偫j עٹ9ٙ(MReص? +G|TQ.@qJ>a;^90'qc X;ܟ!f5pzA%z#7.O`Ɛ݄,EFTJE>,:w)c۾8oCQJ~@w`]9t{J Zr>U[A@A?ЃBv@=~!]S(8u%qM/C@4qrEXF*6:zv#/{K= B::]5zՈ6٘냻&awj v|r_}{%F W'AD\0̐VЀ8=rh4L3_r *x-ڊ.sO˨~ v8R{[/HEVVbu>S PG>M(XLrĸ;˲@x촠}6|HUs0T(+ .̨0ǿC\T:,Ayde\UWۅ!)p*PSdJyc18]'"duNU\ ^" an{R&t ש A~F[{h".8)WGDrovD,[ Ǿl77't?Z,V\@ñjAܐN |wBka$f.wxwYAA:EDa.vX܃3fc_GfF8.1Є ٙ ͽ|s"k\dIJȒ4@} AOw0~QXmDhQ% |A "`fxnH$BFF\9"6buAUmEweԎ ^})>,[^Ymw2j1,JǶ_$[: 6J\忷nXƄKF"N CvS8 fG*\>)KYG`e=!QCߗ@i-2CvHՁ :`cve)p db~[ 16EDr?ԶI VU-'_j8e}ף<\1`dB\칌aӄF4!A jC<E.-UB6bSBXcD;Q% !AH ܈D-0cF\6A`wPch<˫eԎ o}-᮲yx{c6Gk{o̎}VO`k>mgXpӊv'^/sǖneه8m?0EA`E΂nAQչUaiS%0+P頔(xkt.Mzɗ! 5'_{ ^7:F\eԆ63yv\TFn)IraB;L=A]g-բ"8Eb7+| 6QNE! C>܃?|'~>x/b$(r  GeJ {+ۚGm8C𐔑9UQ*(K(\s"]fL{Bku! 1~+ AA "`fHV#ADsu#)\+~z iB'MA]VT@װiAly>?Pc? ؾbKp߬ب_﷏5sVag:9nlB!bY߃eS x$w!%Nx EEz @w!ݽ7kEPl"Ye\&D$`_!ѭ zH$͗/AAߵyԆׄIf"]Y(G3#4 P(u&N'rr_ @$f $R b1+*s|b SPu_8MAD㐙so#>p~xGx{?kl=cig"\<5'; (-#7'u |O}UGB^vp. ׷OwUND.E!J RV#a. 'BsԐ؁ xJ]@A$"㢇\JN^Á+ m"]}̻^l]X96w]epַ <ǃ@b 5Hp9CJw#]ؽV8=NHEѹCA} ~_C,R̓'%wu~IEֹ ^/:3Vӏ1qm70)`3'wh X>F$[mRL,n<K?F>?Hf2Cv`)WGw0ktP+S_%MejT,AE>$JCIeS ܥ9.KOT]G2d7䋂:x!Wb&bDkZu!^| p9XXdOG\]z]Rbhu;p``FRE/Cr(VE,ea#wOdTRtZusuHd15PW ̟1)+x8\,#~MkPt+~ųq߬xnA;@/9)V >ktj66D<Pch<+h@4:zq [/ĄXFز)IrU6oƎ; =`%1!xXgSN" FmE>v ap~1))/ZB‚ 1p6JW'wN+v$Lnqw{(KRB" b7[9m_VS/Mu V`bo[,a b8z1Q40u4ADרAD0LV@P(h`2pf2[mpz!Nj+=3OKnz`3w\C^t\smD 48HhUJI+rNW}0 &r{oZVrZi_тj2,x5Ux|bNZµA)dACvPz*ٹ &e.x]*hkN(؁\BַFE+Rlsb .V\n#ytxP]z%_D bEFDןГ `-9p -Hh*j(+cNs;ʞklֱ$t, BNnڔy69.3Sy+rD!D"repv vۘDʺu#1SH0N;A\!A1#=Tx>ے0L8{5kQ#tɳ=} 96dR(7fGi9ߋ~m"G{nK%J_P,F(3rI3 Llafd\XgfScSA $.T..x4`B$p 5Uz8:?¾0_Dzh٨HcYv%1RcQj 2,DnS>uw՜|ͫfǞÂGLnhU^~s\b tڈ͏*tX܃ꜥ &D/ RP^g^0Fʜ_V̊᮲yt1GC簷uƸ97B"$ XMIeHlAᛦ lC9(+aM.[Zczv5|}ޏ ]1Շ$|Neܶ /);zyqt*$ɺ1LX'06gq8 -"H4jU /tGADhP h4``ƌ 6PU[ !yֿ֭ĺ*C+ ƹ,< YH"Y eU@CmxVdpǷ\Q19: \TG? <6;-Qm/cS!]( ir E6J4EaY⽎s|TV~c^e>̓le5,T:?|."Wo^h^6丫l|RP bba[a"]&uAH9B=d2\l`A="$腈mD"@,&$AGJ΂ 7;DV Hn4ڤ찺hu~k^sLsx<[~3ط"B6Ry^ás[F8<.au)=P9 QƸءpv?|.Y8c'$|~91+&D EcІH:D)=aq8D89!]d2f " h\IB5ca;>UuqE HQU*Ք )fb]T%߉Fg _br\V܂s>%I7 l>3SyX"f,)(XBIs탓;A@ bF2.z:={{^;[4 W ?FgXk{XI  #ca06؉mq'n/!nvomvmB=4ِnO=iqcg֮!ql8 1`e#,HBwH#y= \f>1 ޮ}78]/Wccq޼j~Z;ʕ*_ZQQzc7 ח?yq'-=؜kmދ6Nmr i&вn2>9~uNNmag=av3QrH$Td,g#t̆LU:2Uw=4)>)uHE$ CI(;;c?YHR)kwfc12QT'ZcQ+ GN]@7$đ 1g#YyI&j%̦LL|!op ׇn|]#9d` d=!HKe̸Ke qw8w"sdT & ׃R`D "1  9sOH4JGB|H8wD*1&"$U8w{Ox#^/݆gdԷ_Ut=2QOu12:N4؄;u A4[z8Wɹ82`s#"OkBgαеJEHUܒl1lٰHk#ކ n5`ROvu^AuΏhE e=eÉK81w.(BVf{3)3REk'3 Ps2֋yzY)\_/o]R2^^`L3O^\5x)Ǝ$q C"B1Ql^.L/T.T>qPb &mSw #o/Ul3V XR!I|NMM>tu3l6CcE{$),/ȤR-NlCmj%+9o ;"s|{Յ{g&rfbr[$%+Q"+)Lv ۆAĂfBzNox=O/N҃F{PЂ`"JoGwzL![nSծ~aγ!=x>ɹVΥ;r.q)o<-zك=UlR1TT>w[#\~7/Cb#ۙsþ1>^3B8rJ;ზ᳜=\_0yjǙp[п5lߞsWжu3]HYp3GVdE66/yI`3|)UJ%7j&U݈YK]x,[`ƒ~ 1"d<,nXς'R!x'$ɍf| TȐ:N+NgjE,ߗ3  RS RuXcMOs6!DAj7wD+XONyd$C":6ac'Zy ]Wuu46ac9tyMvxjӞ%  bcZGIUЅ"+)sޢ{ڀU 3ͤ)u/[08`ÀӁ{WAafK<GV<%7>wb'{X 7uA.*v퇶'4}Θ%3A݅WY'c[z]2|^3bM©ݑ O:$&a&N%3ξ⛊ǓJ^/˰8/j_"ILq;N@`f˧Y!/g_1YeB 2iHz4FJRAkB, y祿o8J*ԟ bIVz +aŻEEL|@~^xZ- tjcFxwt~} ܿe sX5i> q9r/T@$Ö/}1c' k:-8rNvO؊ &ow_8]^7Jvxd oHx  Vh4x!? OH?6 JÞQn$Ʉ'OY͗mũ)7 Ë^NN٧R~_X;6:}^%jSCI`miJ7mQV;uV8[TΩ]P;2yv#].?YΊr<&f B 13 P488˧KSgϺ)ufߏ>fS=I*Βe1KLrPLH^1- $P.kwx%Cz H^O3ḏAEۉA086 ?Sv ^IF! +"?% ADR@xEtahlǵߟW\PTXx1HRb/_w/+qe9o9f#YD!uqIwЙ!ՔyT)ZĘ$νBG/6QJ378<kw&Kpo$Q6ij=KA A1j3qenriۜi4^?)9"{۱z-6Ir *}=SJ| {GoYi;kP{)ј iJ,ZX;n; w5[–p>][nǕI~f}|ss1}SE FD(U$+[qԅf Ԫ\_l-WA"wvfZx=*I|p-0Iݚr!NНD!`̱-'+MWA!N <"CIY)x;i`vq^N8CN9&t 1!_gj)j~S*y_/Qގu+.PnÊ ì{ܰG9/穢]0&ϵr gHLxòN5gKUpX^Ǚz&$MxJ qSňi Y7"B)S#(9bvS;Ҿgg2E֏Tpfg#xUR6Kf^0L tD-7uGFOu蚭Jvc 1 k|[|"KsL v;|;tD0A&/Л$G|olʀ, ƒר'p5Iv{Pxk[>M$p@ Y B(!AAĄP $޷%R"OkVDULBP/3ao١u*@*S弯Ӛ/Gt^ ,2bEćϮroV~#4HX3C~6>GLύJd6cs&[5͜v\*CLR(=TdBd\BbDF f&PbB"*dE6^4N^c܃02D@B @@*ef_({ 22Ex B08A s(T )<HH/cIəu\Bu);hfRu:͙?y ;|W,yI7O#&;TXaڳAP@A3S$=썚 IDATkVⷛ/Op)cwlN!/ {FQ?4exWdh󓼬FZT=rӘh1njnbÇӰl&[iM.a] ߎ@xpq%<\ο̩ΥB;Ly)RW2n =yZ'H\3ݔ$j̦j|d_ `+(P&ea>|+B K!>| s>  X/dݎ gf̗T R)#;@qoZ:~쭲Jftq9fmgf?y ?,ELğ>,CCRkHA: aD%XZ^:*5tE"'i G~ƱB^zjgBo_1>h( (@AÀj6H /eaU|ۛͿ~gZ V-c}YeT)yIu-=;1e=C\x~\ J Y ;Y9h JӊX''2Uqq8|΃p>B+rlɄ9W,'Kq+6c{8dIJ* 4uV陡MKVF ;(-}*ow4-}78w-s dU$;Aр  KUm햨 !d*Wr] ٢k핏Zh$SaMJދ8nYYլz -gkqe}:5D_3CXk^xƒDܜJiH{q.~g#~s v/s oTb%kZL$jډrT|MO 6~Sr#T_gٶ:Q%BTbQkw{qC. /O,n"$Dd +=e2 d"Q%H$<"0NAX7 U~}/2d:V,7#I%YlFv?y gۣ"8\TݻūpX~lYe9oUz: rX0Ji4=¤"He|/rN^KNL/$~؊/o]`SPvZ-te#  A1VeJCKM҃jT2UY|ge5|"X &>l_^YeLR͐K} 2ԎGܾvnm˟G6G4r 6#,O΋R}Ѭ3 Q;=CR\`;UUTE [YLܢ{Fq~?:Zȥ[V A {GcY YER4n5Kom/pN wOt;"v~6TT9MNqݰ8{aufG"^8<)9}cW`̈́1# A`BxHl ,RAAq}܍k׍g_UcbNuh;ێֳh;{mgqTTȐ wK[ ?/mggອ_/ #@.^ƺWQ*2)(`b[ܛp(8=8ڍ nLvd AAADC,RR[_D*οGt{"W+.9ɥ2ҙJ}=Co)a*kk5E$]ęvR-#Iamr*=5\9v=^^gYUN%b㶏8 bpl7y,+a4N è6L;QeD݅WQw˜4Ob˟G"uI QAT=!Xaw99yDlEBldeBG(!@$b"qBjJvCb`h4d5ۼkF^*;tZ9n,Yʔ H)V2߯D"^D%cp{b.ɦ,(HX7DdG/,8K4QHW3 "@Aa0!=< D.C,S]nO~z3Z2vCVR&JQ/]Y1"/{0!;!E© /*ȭ>EB0L}t;,38s>6QZOW2 "!i "'6[ cUJ<ʡ]P;J'5"Va3a(~RT?ȺMHx`݂oNtuäY D߉nA{*G&fߴ 6(9>Xm8%n xΟk'j:L"a^)fS*02@\He'L2J " ~?%JD4~mbTm Vmg12: 2GFcy.! \&4I޶z {@,Y\1А )ٙڜx %+I$Bxx|%/ݡҏy ]W_L@Ɉ4 EP7 (u܏iS@2wg\;yWOR 1}H"AD  X0!=x!iO0(ӱsю^/729&4Ɖ{' eʥ2dmK][ziMhq5^5?˺P9u1<'Ӕ: YK2mq.l1MoˆutWXH[`q˜dQm@\< 5jǛŌJӚPhJE*uA`r-sX_rt>kqJzfiJddE6̦ dlʤ"C 漷 PAqq8.@.Wו奟*1:?b,2dN kW|?f/^{ STIuHelF>HCRh@& FKLhy qpcR ͜9H3HeYX/^(!QI@aj=KA MFA,ZuF#= H^/74C4Yd W.!MCfR:2Uz%¿=~7>~tn) n~x*{jsf}^s|۪=C{Xi(/>>t {/5D0;Y0v~XVm]?N+f&$fq98lBRuQìWo'fB䦥s^9[-?99_?u=Ãpz 1Akǵ[/.@, a[\$bTƈQ|P3ASxN⿇_?@뙬ዛ7!{q]n7ۘIB'  Nas K LFGEBBČ@ 1^-W0r''@&J{]kMY(GjJvIX/߰5Tv5A4AZm4^+Kz[dґJ` VESRit{Foyss?SEQ.ρܑvᎧE1ۏ\Ah :.~^|hp$ >8-S׃uDץ$%02a$$ &A\/$X)z% AOxU "wwm~dt{~hkc96mX6}o,s. td!ؤ;L ,fie rdr@%v &9>soÉnA( aI ѐB$ `% (;4@AENAY҃Sf"YE\;!@ĞuEوP9`3}PUJ]xu_!a+v.!W==80"R,vӥ"*RX-CpGBO|iZf}<i Fxwzρ8n-CY(7m7IA }Xmla qPoTA7e9*? 11W(MbAt]ԹJ%+a6e@ǼEv] 8$:a*v|AC8{a 3chrm‘ TIb䈩!1氣?T0Q<fӍ( )>|92kB<ALRib+B$ u>BTdgwG~ڎ_7/+15E(^] iC9֮.cMxG?Lx$m[5f`P[kq7h>2z)\dN  )W '|ҏ'.^e+:@^uasZ2ߞD*V0] 1@AADj6xIy] qKkE~X0iG}{w:'ZQՀìS wr*Y pbw4W,IxwjXoT4qN+AԢjT)겡=(H&4\~}.~gۜkBa1z3$B۬nHS$Z;n.U+%(^j ˄$fB  PT^ 6C&ف'~aA拒^B <'A׽0m^#X46 Fm3-BqM%c8  !; z*ʐbbL A1p nhI=}#$&d Bd@APߍnyZsPՀG۞i?vXq>RJEi4Zڱr-lC8 yZ7mQV}j3U|+?sp:=<l[!+ v<ĉXR_h:ni KfBP;6po IQ('"fK=#px<9[߂;Ǹ'Zq8,ɍJ "RSt ݹ%D\>ARȯP0 è) ";  D)_xPЧ146jÚ?߉?{[϶침I.昰iz,51'Ͽͧ}m['xC*2s"ϜJc&Ƭ&{lqƑSҾyd*+ *Y8Yt'̤J#Hv  =6# "=tHMm Gz`K-L%;|h-<Dh}0eR} E"Pfrכ2t-C ""M޸V?Zx+wvܟ5!WA(wP{ԖHa DŽ5A'. $M{qC/dQGJ8 Pe$mBu }A(Ơ"{j2Ix1KxoF9;pᎈ5_ēP/DOl.g/C9tuAnf-'"#4e9.ntiB`Z ˙02+ X!1x+T}fr"||K$uj `}! AѪU ?AYa'Ah*_7gԗ;2:uCTD%xx:uAV"/;2 nt]`.sR^}!\?aՆoaqy1T>g,*QLHv  BAAjmI4aMKZ2 a{/[";|3x«q{ao>lYkw"> e Wm̰=vͺ}-CF>כg]iE϶ձj9q ;swv8E 휖 cODZbNΝ5#\Buv~R\_|;6]6Xm_kPo6P{Զki_/D+✭66}2PIk5v\2/@Ɋ0 IDAT D"PA?קR!fR@|;ڃd"AFO$HkceN }P1ۙY)Ntd B Dqxzwfҙi)>ow5੢]sc\o~ [0u=ja([`0Ò}"IXU9i!\f9%53vˤ<2m,N+݃$c𹌆qu9-XP[EA؜ .6$@Drc0kDp"ˁ9 2TKB@k!8ց'ٴΌ~*mw6q;|?9ضJV)]R!CNVZTQ^ƺMTW:NzX˺ z >edSmHVP$kb:&\K28HPQHg9A!fAAP D؞ $CTVHP;*i{P$ÌF'{E6ޮ}hjͿm{!m(Xb'{]Q3< yZӤ(x$ =iMx$i}~m݋ahR}R)>W?>ZVn߰g5?Q?(~r i .\~>~^_+sKPw32E0=W4ˆ}5 g[eQ(AiMKGYvδ/"QH^%;|Aj ?6#ݡ#կx.;I޶zFe&'!'+ (ٺg8+IxPR4X/jak7 "OaѺU0UEQ![q7Ǻ]I'<$POg9A!vHx  ZF^KOzw,xa(Uv1[_C3CPspN٠ օ㸊gl[,Z?)?Ă4@9c}ofB$%Ch.Y˾WLp~sώNG=Q %B<sz3 M Po6d\5 (* W1q>)B M+s B3{@h!R "1 W29Yi ~Gks-CG.9>Eٔ}oHAfZtg^nOƒTFA BÉKo@DCnd6G܏֘I;hʨ\ۅd -(  jF`HvV㑊?>j6̔`9xE|;/=cZ~di~ MOT?*E"%BMm."ݮ {w|_* i#È-֣"kLQ.{Rry|epf=f)׌a$ӛB! ro<9w8nNJNGBlDň'% 0uX J  A*7A&(&IHi08}wNIY9]#U^(>8ь>N6jL!I[֠0'*SJZ]op quMOB$:-ì8=xX~O H8s/L+6#veAyԝ5O(q<$v W"<>Ql\ aw] \4+m]^-c1t}-)(HSP/BU= !E!6 d4(4dM9[zF=HG~[[;8xZ<ȦYQ"E X(@#0}]r laوG?7fQJ,͹~(ζl;Ξ"IuT5E!5&ia/7t"zdVe;g mlŗ]"ҏ_N }2I+6%+8HPaj 'Hx  (0!=x&g6noxpf-C(aߕ.(οUMl3*1#ޱw8>nNyT|- ǹHrS;sr.*%mܶD_EPU4y&N1ڸnXl//֢CF+ 9 UaPhJh 1SpjܻRNK?黬XnFmc SE~__""9 1>!ބ,'  "JX D؞٤ f1daAFmy?|/^^c,T Zb8TM|϶q:nNyY y75nkT.$˵3U ϣT7= ÞѨ,3FKc*䦥 "e6 fE#A;qm'ZqZ JONAAP%~K{d59YiP*d' ;S~kږ+1Opm*@DmIJL*鶟xuƒDFqт0)t+Fݢ@ B~CnW4A!.Hx  (bZF#N~V|#͆ik]Zw muiQH| jFuθR)HdhjtWZMڵFv8wF%!c%:9׌͹fzDDx*A83(cw';5IP,`6eq x 2 Az=LUF]wU4]?MT L_u|«][4N38'Q˟45Pe(JJEbK@VR&vP1t٢cף.:h l50h 34 %ʲsPso zF&E#Cq-ԇ׷ҌxAA,|"L[ ޏY?0tU*!KARlʄ}܍q]so0e98} >.kӧa6ebub1q=u!1IuT*zAT ^4mSBݬ uE\EIq_~ɺ]k5i|2(@d;x$R)Y>&p@5AD㍃a6e`c2(7Nv$RsIx$^ˋFBVur "!  bĄP $fw"OkJ}WJ ,BZ\ E+`Psο2).K Q[FKc*%n̔qևApf+B4Dk7jfS&H >빵hV*ITM$6 q<3lK(T$!``F7*Ō\M?V6/EK'hV(N Ԝ,:✲o~S?)t?K`xtĉ:B2I+|n~ d}#th@A$ R )2 [j55co>7皑ffmqtd6}!Uºw ]L!g]]mðgum{srSd /ނ$jEV9j~a@ք}!>r ^ituOѤem_'BCl#^̺M $;AD| !  bZFcF%M!eAb\c!MOqjEoEF\& [ÞщǰrvGxI9q OqG8n;ýqxl[οU1Q<`~EDB;j) \dSLE4Xv:*˻=;;@a$Ð͹fi9[m}8g:" b.) 1I1ur `!n@$/a2 #Sd-6F~ %&}Ә_veEK&u!DZ%!E:.1bٖۑ6t !wx<ɟQ2.&Hr)7 fS(%Ae$;A%<AHn? b݅W8Sd؜gwr"Jʜ,nrM%TD ýMhwSE_[^߬dPćw=vXDu)uV7*\)mFx8=؊] aSu;PAÂA,.$dCpbrpNf&ADxų4=|<ݲ":7c7u^\NиlrNsjh-Ό7oS/+4?&ZDa+XNOvD*Vk ADAAAD:mt;,8`9ʩ\3كrґ}21aL.a_L4qDd;sg^.sݟ#1xU,;p&[X߉}!MOtA4-4p3\GOE]ij~{/5v![ea$N',@%e=;Be=u81"k)fq :L,'*DӘ~ÉG?!9^/#.ηU?'yd ضIJ>3S(+#~{z 1@!'+ +RtB *Eߏ|<}H8ۇdgd "=,}d/o?|8c(1 ADB#!  `ZFc5FM!i|t)uq\ȍ:䦥m )\AA lBZD =h9N&"M\q\_M8eg8eL̦LdRa^3fdR) :d}"a&̕KN0mkڎG;m ǭ+^!Z;EԇiK D*֘!ڱs nc6k{Hv   Vh4V < ޸܆n7z]R͹fsyjͅU9jV=]~{`9T.$˵3CM \vޮ}TEJ̎r<亰n}1&$iJR)59au`Lٻ{o'I2$NHL T xP!>jOQwNԶr=bsJTXikK+]-:z(i]h4@Bb.L΄f{fZ+KLf{̞1״A0>0_:7^aEPZ70lAقilk4ٕn|vz T.FUBXS$7vbBZ MF̃2@Aߨ$#ƌ Qq)@"x OzHx}Ixi KP^LJ>t6LJ^~F ל:\9 :R(fJ9HAQE$O [?|#._Nl~e?}kdc9vޯUU+"QOy#+<"cBHB%k˯Ȥ*΄r 20"Q%ߘf\EEI /[&k9N\?˩=W{,λzm5[m0#a IDAT1?`DŽ+>jsbعEl?k;|#ߣwz2ZnټJ9/# <ZPKl_?6Ұ u "l(]L΀ ePZF[N67`wrL.T4ΌBq ^0ayZ~ړQH*U3(e'iЪg1A$׋GTN$/ߢ=ٟ~VjBtbǣj#PRڔ*>ljv ;B! 0B!5rCjRp\x8?iBmwuǼ[cL5rGGm*|suhut`gzS+{ߎf|ܺ5xg^g"0Rgx/_7Ј-056]iYy摶lc)ejE\nya!|s/Ɠ+'m=lǁv4z@Oق'Wb}|hw8SUrT ̳BHR( >(GfG39!q f!QGhrY(f&]Z>QA#IStX[2DŎcׁԱӰ0FB5:$қ`zۛ~ wT1S?峽QKF 䝷~ܤQ9VkPw;~PKٟ|JlBmƔ Z%W.7z 8u ;BK!"qXpaoӑ&okm9/ {Z=)7wxUxz/\Xb*?Mh7>=Wlϗn{e<'=+^GF+*C>9ÑVufUغj,h?Ɩc_]MăuW5gPNopIقÙVl~e?Z:z㾟X ;B!s 4U'I\ŀ]^PA riIv Yf<< R|އ^IÒ>+^^fP轢 }8νu-q$;졳B\BB!$$cp%z1֠R#ߘzsRR\`&1wk9]786]P#[kƼ/cmAwbf)ع/[gEtbzhs<oU1O~ =2`;~^m)x:WG<3;žOc8E`Uc_',_oa<BI^4$1+72a "C`i u Z(I/J\\Z@#¡iٟ~cg/=vE?a*Q/&͈G!\ ! 10㞿>w mxIf 3Cfr 0w`AU?~lz+͂݋goǖg}JͰh2_oj#Vs^f !^Ums5C@r;8mGJ'=26؏?HEs"~Ys"uK4*x/zį0.KB 'bS(2 m" Ƹ2](r =D!t(f USt}q6zq;T3g;>U5КM{}B5>R؁BI>tBI -ɶ_O7$CgOׂ 3[r9/2N<Uxz/\kjXu=@@Cu9kΪ72s(3D+vɝ3SWPb?WeeBnWe>\ُgoòy N*="ﭼ rCzu!BIV ?ZSM&'W I\ __(dCOdV/ 5x-A`QS_(mer9t")nXSbv Bz$Bد'~I50LN/:FڲO1Hxu-v1`F~A!F]'C!?fNjQ*jWFݍ\̈́Nh=+%S( (04|BWEP0h0hU$D*:24Sto>,X:{ $i[G e"gbQQcJI% e rcv B!Dؙm`T *ް*Zū)qx/݌ y\^*XVE|-Xb*uz+u;;/FeFy~x\C;u|sh!@ ;B!ɍnB! ButIT*Y֞= J=ܨTxth>|S8ub.1?W ߺM۳sً1y+-mŎڨLm3gHb~Uy֤|t"O~u8N;{Jˠc&p91 ;@mfo&N;TCL!T SAEE0x ]3bo&|0 V;~r@ jz)(y#T &P'$߉}d,Ǖ>PwWP3(zp jqL կM'*%3fKaB!$mB!$QAo[21wk9S78)@a [ku9k^%`ū " >dkTzlȻOm{pJ|l*܈>!cGYbxPSO< )Zv[oޫmW0x[WW3LY!d[ #^u]#㢔&נ$)NFw !&EVP*CB͆OXxe\hlA䳄ǻZ#LЇHz}N5AfCClPnu#͠AAN:J ,@AIZ:z9/h5o'#dÃ> V/d=]l}$6 * vR Pp~Qd͈ɶ}Xw BHSRB!{Ovv66醗@Bm\oRK=4z; rSU3;aS [Z16z~J~UҰ m~>ٴ󾌍O)S)xqtml WP#Zg\bLa;A^9 㿺cˌ )"~ϗw2#mH&~t8Eky[w+hrXYAe\yaD`.N`#^QE):X^w;~P(?E!'W^MCb*C3-q*MrU7+n | *{d%>sTd@?nܹ M߇RqTfz Ur6j5 ψNaݿoAͯGͯߏpa U'M?m{(s-̺)"w2<;H{ebql}Xw osxS؁B !$@g"A44U3wFU*y ; ~@XH ٴx3;N콺E|39T Rځ z@cb1Xb*E"۞sOEٴ::r|*&3PUr\y5iūD 5B0v*ؚv|J 2x<z ?@o n-uD7wr'䊺X`I9 !9A2l aHDv m#BcǦ P(27l%cS|.S(f/!B c^?$GIt]T q»GQQx7/KXP|a.]# }GA}s'3J t۩H,߄yTY,}R ;B!s !$̡OGRw2SIij|>hv]o)_s wFU72%JQw`r`o6090U0f` Xb*5?Ҙ"EY(SkGȥ.nKC<8Po6ŁÂYb+g69Mo(Ȍ?F? ғSMg{LֳP bQA=S !BH+S-EJ-?BcmmK$>EiFXhLB:ir5HiװN"0ψӿISz<2Rr2|_.-^Ƒ9eq,˝wZ rѷS@mJM>smЊW .{)@!LٔBI~l]Kp4v Zn{ܽ `;9wcS^)S6c17֘gt8OH׉Y=zS:/cw.{zP}# 3A(jD۞ lfOxc =Io+vDT> քC~\n]߁- Ze^ [WWaȯg9`2S v!\[=JKoޞƮy*53(0C!8\8}^hïjp^VTBdzbױD7pY*A `>v!dP=?? ! 9. U $Ī\>{<\yfziuRh^BNFYp9i0{~N#wο]Do5zGStX[/EV8v}Vڕ 1$T!w?%=E!2w7BHЃp̄ߋaA#E}Fn^G9`)T:ecSFzX&&i ϜߎWvw؋C-f-ՇN፥/`MrZ>6|CE ;::r|JY|G_.1~2bVxC- J~U9WVѶO.S8e4=+Bim-f~KcRpp)ɺFm=K$Q|>ؽNd#Cj<͍8u[y(ò<^흮GZ,dl(ACUqp?rfud0iLfB&Dl @t^*#f;%$𺁠H7>pVyf:`p 0틕B{AFV ^C(^GlV-R'|#e-uM'KjOKW9ы_l<&۪2A{]}dxxs>)+P؁B!$F(3p:q6f|J ;x,8:c`Cy)_*s9/<n ^]@T/]AzܽC3AH ڰKJ|wE%t Rцݼ*ME@`?99;1`stDk>ޓ֋Y$>D'*8~\Kz 2jfGPQwρ;SrCd,ȏ/4 A/편bTtjBO.OT(jEƦR*ՐǖL(@!BHKg#Mxo.)SD]צ[39/w zSaY{֘'2kGsB=rxS ``hi?8hXNjˋv w XZ$ʠDV}# |ڕ1 !̦?FڹO4 3 !Fr !9 YC/sE=l*܈go絬P vodz'U,T?Nf^||cQ0֘aPpqetCp<Ϝߎ.6}y>9Q;Ѐ%R =5 DxƬSKL_x;B27Sq>? kpt08H k & w ;܆Zξ"e$Y!"o?_`+Y"=UT>C ˡUoԴj RJ,!vp*D"ep鏧"zlF < x=l};ߊTN_}Y_\cQ^LUjB=7K qP؁B!m\B!dɶ_ÐDuUxp/p2x|fh13/1"_ ޴A0Zh)'8܂^O J=ê˞l 7U?n^A?o^az0 ΪHj#8nfd('0[>t@W8, b!LغjְCG㏪U 20:Iջg}\: 5gv ;`P7 hBIKs(0#'%#Jƪ;u-:vREd-RVfp\(cqD3i6>!&s$m!ADX!&Wu\VlL~hc 7hհ 'ELV([ۊQdD54XSA8PI*ψSr4򊱟PTߧ9\^q]Gx/5`,ɶrLIq<;zb>ۇC ;B!d|B!dn&/~c=Q|#mH2(?[r^)' eçMcR%Rh5(TX|ւXQdʀVL7B1l18Ì۲ T#2zhfN<ׁ`}. |W>{h%,7O}S}qB8ÎKfv} 5l[i%ɛ,;\lMӒBH4m !%C4~ފoU.'o>j%?4y@EH묏 j.޿-Sq{D;-boÖsg =;? }dk̼I0&~`A~L\Y2p :/U8Gx:txTZ*"zzkp %f 3c? e•TcJt.m֍ ]o >83 z۝f“+lUeSS2ϗ\p*;]"RDȔAgxzz`HA$ Dr疌% ߗ$Ⱦ<誻kzEoEƙVw>r*Է^޺TCLU(6&qѤ.õd<)@!DB{:p^ÖO _KTmؽjj>uzu{=~T4&pxF@>2ʻ|$va\P;t4@T&k9e"  bol[py-dr`õ>ߏn6%R^Hq| ] *\0>M}64D d4~ :F5ct xZ:耙A, $DH(np9zj9?^ z @$À`@pG.<9A'+Wo~r-}Ms)߉5{g.^DFi!2Pgg$1dxK:)@!Y?{RB!sSvBFU*u;@'ZqG+RTzV:]S~}O>/`)k+u=Ls60|研TZVp^y~lڋgoG iLʌRW}#t!L%عEmzd[ uBs9. >U2 ?[ZA\7~t3TyP)x|ME0D}0WB<ܮihWb[qĝ=] !'vQJs.-]u>#]΢JwV ߺQ6[mxa׬8C 1(e0"s rn o6WNG[}ckH+QJưC TZ*'0 =@w/ *53P0} 3:=cewDL=tBڦI1sS?XA0Ć%APx R&`bLx(ʃLF1 ww_Hn._z?<#N:`fQȫ=#E;^f=X Z[!6 ;B!¡!2%{MA 7 }7^@1c3t{jF*1Kr@ J=6nx JѮ~h!yKPKj" :;?Gt_ؤ1)2A0 +a/3 ;dPf*\gsMWt":@V'ZmQ1/̈́oO{s3@0;\A &D*ĸI !!᳄y Rzm"$RJ&1&HJ-NpVORTkVď﮻1UyƘ/~N}|38vR̶^>b1fSؔ 9Lf{ʒᰃKS[C! BHR^xuBv6?T +M81ҵG:#;rzZ;%zy_ƯUu;`漌!ITyhwH+7>y;yP7B)3`O$;?}^J:IK_ ;Q{ŋd>D g5A!Д,AmkWfYnv0,_ɹTJ2-t$^ f%IB.W&d $DC/A(FFlMBܡP WA>@Z}NϜꣀׇy?k\p\m40Z `j;\^z%7HDfP6# 29 Ëću(@!!BP;!Clkw |<68wMx<=pVzdkW6'=^vxz"17ZNPC)^J ͦ>7-6Ѷ1IջgF;xVq9)χˇe-)A_75`*F]w}g*dfJrJ|wE%:z#CÔBTၐ9UI{KN0x\ iQBkJ# h[tr2(:_)`plGX !!8ǻ^83iV n{7wm+JTAjY%ԦԤ~3y5Fu[ŖEOL޲艱j hutvae I%1~]=с6k9.b1<ȩ EO"v x9ez&\}Cx.# ^;[GPG;sn}DZݾ3WquM͠`튘Rcс5#mGaB!$ﻨ !2 =Ғi =Vwn155Z?JKp { C@t)'p\y_5<ѮTq `[$?0^ Ji,6RybKcRP}ϻ3w~t8y<Ҋ-'q&k9*-+Z%srKLXoA0j؟qUb<7.brcxR(|#Yzlk塃n 9^BĢP} F-| <`j*PsO>2GJ=t8%Q؁B <B!dZ+T# C.YHS Wjuww)N Y9P+P;^0g['~im5vf g;J.DӶJԂa~Jm6iA0:]r“+d,1J*Pf*g<.jw`kHR&k9KL* $XWma( !ހ{}y):FLN4ԡ<7:F%vGVerL;|dR (EaHi~q&v)l?>!  +&_BKz zU ?_iU'Ҭh"|5]CvYSa hR߉OW{0hwP .~IpՂbڗJJOpGYiBu!Bf]s޲]T\./͏;rbA:] 7r<N B$3D]7Ѐoyv̓e2vf(NS(ٟ`{a#;(V˹Cd'HB! 6=v$?-%#^A&I|gf'"M|Cבɡ^TWnܳ)+ƕD\W]ӄl o܁ަ6t_ܖžC5ؼaUTTwW_yX͠`튘ޒNJo2e rtz ;B!D04ڊB!Jgˢ`[TM}CۻgCY*3,1"_ tUGx&0ޫ P;QՅ*j>5lrD<҆EEtQȨJ6U|mސ@5Y˱sًUsii\{aZmq2Sh/̉` ݪ f†2řA \GTjMP)s<7suHs#/z9/cQp ^7 Mw]Dryll8/O0F"9kOl4 B82B>\ <HMfuM~gޏ4ih[_|y/sy8~T-_"k&mu4es*ʠJ5ĴUv7ۇC\J]!΋B!Ig=_m)łZ{dj>iwLZe7q||Ǵ*vGB3(x8v(uul([QA)t(3``*w8ZC*٧patߥI}'ZՆHO XAtهqa紜J5F+}*CN^}u51*Ώ.x1L: c v/8Pk" ӆZ3_%2,>9RɆ&D @!(%g[9-ol_eѪ#|sytmB% Wf W(IXjů=~k IDAT6w8O?k}jp>^d-]>fс4La![(@!!d=txo.CKL0֟V `u<<ϫ8ub+  ;wo(4Hz8mᴮw/GZ1? _nIBB@DjAboR2~4P;Ѐ7N}߂s&D `*'nچ0\!#Kg@^_y-3cmS(xHy!v%q s.l[sZ'֕lq SC.BKJC=mte,=iB!7 J81A ";q%T-_iW~B͠`튘UwP(`49P]K4!B!$sao-`Twa 7Wy([^O9mse?Zml+ R2O?9 ðkg_::8oL%tXWӲv[]6l5IӋ@:r3o@vCuG _!LJuhmv>F kjT 0Z%8ȫ>//ᵼCi vp>ϧ1*JF}ç_|> <m+Q#¡ ~-Ra!BHYҹ"ph ` D]1Z |B9K#xUׄg)0L ^޾C5XmY:p{n9eP^4UwLT"==EaB!DBj|2㞿>w J{*^o+v$D_<޺W[G:OLCmLƌ23O3pvӅ*5.va'/a⍥/CP{al pa;P;ߴ[vDUT¶۷`S9v`2;QU/v&}A0x? ;ĉCcuNaAq;ǚ Jˢjc_] ~9|m2|fo dЗ˥?57xrD+f[]t -p~ 0JMaB!V52}M>G2#]vY䓓01 7V(I|ߊj&G"?_gL}+ui&d-]~Q؁B!1yA]@!>F+=,p@Y />Tz+֟|#YzZVE8;fިoA-kal9Ä#Lp߂URɶtmNlA~ oLvهphBTdhк%8ڂkCn|wE%粍:8W0z'[P0ee :HbHTA`Sdd=`+֗ᰝRIAB!Ys VrCd1Z /CGe`ϡoc C˫Xk]yuǻgxo{ڕq鳹Va ѤjBaB!*<B!Vqnx ;JKyeI;Ac6>ʹ߉#]7 ]azOj8-%s9u5p~s 25<#]؉/rLiL /܀W׊wcSFA N>QջLaϕ(9vfº̊JG,'Zj\gXm=8|㏪yU?ђ 9TdZ6dJ&6z|/ɶ9דM%NcAV@Z !.'7Fr[AetJ!fB!eq4|.TxP',ΏxIs Ё7 qj|<8\ވ<zM EКM1ﯹX?5B"55B <B!$*݃HÎƿ4l%RT.d-tt8ݘzwˢ'xF7 zzXEH&6rk}ᅝ1۷pa S:[k;ڰCۆWvE WS8N7}=|g%_yi&,#%uuv?=౲;PTycU8}Y/8&zg|[&Ӭ۾53zG^u=UzXkf鐚^Q؁B %u!B=] @YuoۨJE=bߓ`nkqMC8ͱQ.W;_6dk {nOFF;aGߋ`JKu#[D|m*ۊT̉@)_1 O3!ߘ΀eμi/kp~mh/81<>0C58e`!Ko5Z%4# *u=o`*4Lö8}SW,iq\PxK}XL4hl ,B!3LJ\.\吹Jem!BHLd/\<2Ґjphx_q:>-uYq=J ;TջۮpHZY:bu餦B&Ձ ; 3M!H } D!A${#>U u[}~^+`dȐk\/,1ry2>/!1iva'>,LE~} HB+%D ȕ If&s$?&\s5㑇29:.ᶸ_mD?}BQZĺ NA?`;P$g?qX=ZSXɪ;+f"4BQ\[3~|~w__x^_?`kOWk"|Zx~9̂8>F<^GDeE:5ẍxeӃ/ݍq6鱬Jx 9_s7Y݆~(o(w7y E%Bq)E%(T-}O7nbm/}MV-28aB%29w: "=/'M #!@26B H|dmXR: !B}5Ƞu z)1625Q%7Tb򘕷"dgm'L '=Uw@ @NN!4x B1 K}qKf4}wF|.c[a 8[aXSX͋b\zMTk*Xv'd X~m=ݙMt{hpoƄ+9 ^+Ѡ-s_ĵ2[TɰWoj8>z=C0܉F)G7zqqЃT}D ;k]A,;㏏OTmrm T!H{?"©R2y,~ !BI5j EtmRlUyZdFlU<a:砼FSpD۬UQef%/ @ @nn.!(@!`f 29QB+szs߱S8:OэRS0KczȡZ]= 2unWsGYVو-? 0_D'Wlǥ{Cc3q;ְ4ǰ̹u/"[`Cǒ\>yQyg=w Ra~nDB|{.ۭxyrP4"1 WD]$<_Ē4K"T@{Xal"4AC,ʃ@0\k A(Ȍk&!BHRy9n*@FHU*<7U2ҝ<{ϰt-]̩cg:.pCBI^A,#77}NaB!$MPB!qɡGȅ>چfGqBU~K?zs,l[ 9t]|e9\&s]p%WxԷa^χ+7عb{B`ڎXlwj\q4|&!jC|#&~DوЎR"mUnvGfYH"rHL(ߋ2{W$bq$!D~crE9,P!@*z r.?mBa/ =!BHRW ˝2c#Qf}\׏3m3w7T2kuj|36E[8Q~LZDždz%aVR؁BIbB!0[t:j ´ \3`Y~.mW  RA$Z۞ 0T0x #?R#[*KDT%f%R{B!d*g;jb2Ou;=iI Qȡ_ +~Z;QpuEp 2ۙ ,FŰt ߸;܂כsZ/mU4\C/7˯ sS/MܼB!iBC+_d=C.|q鿱-wkb˟\t`eN*Txr/6jMERku6 ixȖdůo)}o)ގr"46<Ԋ=`&~113sjhph݋m/++-8T- !ɐ/ArQ&g+%,+С\U*(b!Aoiƻ=qNxyJ%\?TR)t,8'B rq.k~)ÈgW  *5I*ks|KsgN nG:Bq B9ٞoBa*K(هsHEU7G̫C!N?3tCWu y})(DqʚL[^$Sar Ry+>̽vن7&ٚy,X[;qЩLD ;B!i>&BHB0 [6oG/K `CcBn>l!*scq<\/xM DEzl,;0{*07c[Pp+B!ujcctHBG孫zc!B*7L3l2k] !J 1$CGd&R؁BIS4B! 0LN24?y Z1wRx ==tb =ȗƽCw;9Qi:ڸBnNa .jmIvN=>ZxBnV=xMPBUeR ;$ѩЖ6[ Lk*xKPV=D`9>fF*KzDGVARc@Od|p6lyD) RBI&/KCg'ȳ5$ 9t 9U?ϴ<bOpG+l2*%K/\p1: [kW&oc 3BIOx BHB-'N~OBq6!Ch,̩k9u1%i-~Nl 8(0 l=Y_xC̕ be q gaUI9˰kW f(a >pݸN[ŪJp4 a,+(܆F(Uh>c0znCBFPܪe5هjMoTWj@w8U?A#x|aqU|c}RAy:sb u5 )6'.zBI BB!$ 06g9B.64[?K+svQsW?| uX=PK~ݷxj ?w;VW$ dp9';6C} [eعbx}n#Θmiv⢣fmr { -lO1i3bJB^QH$),nW HK000x @ R@ט'bIBLNaB!ql <+x`sbMg`HeB`Mmx,ZLϷsWcuu~0NA:g!2Rb?*4y7 +J?8ß<#mm)}=Ͽa;S؁Bx BHRM =gM~7f\ϖf%= z2b{V}+gE֞S?c<؂(L яc4̰+Fh\Ƹ-~mpuСeK0#)&X΃񙦂FTc[F{9-݊S=tYލqUڬ9v"5͎D(Vg<;7UkS CIHK0!") SD2?rPFBI$@!$Rь_>|^>R! :H ||( ]#IBr9~^ޛ)40eyKG}zg+8de7$m>۟Ž?qxtv kTЈ7w.5/yB7!BnÆ z_Kߓ{J*b == B!\oV+~VqZN>`rd81U{s_|aG}n#Z, zⶌC'9n욫̛ qރFZ4|vt;2p췢e;9-@$~L^h9SX_`P!`ya1PW0Sbu֌Ҭl`y QIR"Ńt"(@+ArƅBHro`9yO< >)ՠrJ>[T-r[\yYГ #MʡSJ ;B!ILC@!T0Mm@9lholxb=ia;G_54E{v_$>䮘֡zAu?z&C>N!pTY6tuVkU7nYNm9˦ΟD 2a13sjOC;3هng?\!78b%PK"֔VE'/Im J^epLÜدŹBRM;"gtBқnD!škdTa66jBX_6n9,KϞ:He^& 9U_A={ F7FSit1,]c^fA$Kh;$IZ2% "t !(@!2!JNLƣSm8x =V̩Ó+ws^~bc;p LM-t;qSjN3 Ku.k}ysjhph݋ 7~,rw?곗e}xԂV[;}p{g"y>5XS V|GkKfg:j݁H#].#}I Bi/wvׯ[OvS!͎~Q3`p "DL!BHG{v,>~mp-t jP(W, :x͙2!EvT޺:CQyNATt:Zs߸"4K/\mɤJ}h4~[@ q2!{BII lt@>quᶳi,c3P8.:zv+{_C eVκrmZm1-p _`}ࡱw?cmO%1_؏GXOȟ/uwmjm{shs^ qԂ_uE<7g PE'/治%peT' ڪY7@.Qw0 Mv'PDc@!~uhG12qyxԉa꒩*̓Z!J!EUi>ty&~-87 և߹=mu:=1쮔 <4J3?і!Htw<*|:?3*kǯ A/<7'C*ҽob +kMa_߄P؁B|x BHЃྏi\R|1% 篳^H 5cl 6Lr\ifuAf5iY oqߦxF>1 0944=áJ W>3 #S͋K͋ì` v~9Yꗰ<x;)5>\؍ó޷;j2k_b,j&j3B窒Zn{MFQ?/B}{|BؗaŗBg+!0PB!)m"ෙ} } _+''r'#2bCQހS#gY-O+͚+4YKtJV=aTCS+spSP`{6Di N<@+ѠgCh,ZA%VZۧ7L?T˱y]QT!ym'8@~/h a֮Q?>rT1 RB!dau!vOw]5.\NV4|r\rZ9LTn3rxtMx{$wksoM|ts[z^D[2&~Lm>wLRUnYmz(i7QxA"_~yۭ3{mARէcpyRiد9 LҔӵu[i$8 o b UyH6טHd !2Q'hñ)Q!x<^o:llk;_U.7 DR*8Y]#ڌBɒ{7 Zɩ]wv =˙3&0tB! !6&B Z׾zr^B eܘZ?av8lNZjmᦂ3~oJc6[7nO}n#)e+}G vh} wo%9U}x={_ð̺oFmvk$0tHѿ|1}lMJH*\>Ri_ +C'^ߌkd4B0"@t,ٰulI Q+! Y y}0?_ 5DZIZ2!QB!iaVNzxpOL^k߉=6}Ⱥ*u3q@|d5cygufY>4?UNX/C+ҍs* / 4ްό.Kll?<<.۬`borʐꖯ[{ȼϩ_κ_E'RHfaRs[_TwP% '-D8.C*+!MьQ'z0N ["N]^?zXznQEIEyr5(ӠjQJS糀 :̶^>'ڰmک .{4u c}X]|}J]PƱKp ^sl>a`w{Zd2zyR_7R!ݷasxݕ2Px[u!sW=jU-R⽺Buhy B^?eI 60 Jg%!p;BI;g6~mg/rM }'2jmglS./p~ݷxAIdžޥ?1x{$:"y>Ty8׃Mu nF.TRHE|*}ɓZx ;(%TvA <$xrJ He*;B!_Ӷu F=Ȫӻw}}u KKP$)]^?^?q>! RM x |u wK=3+|Tx9tҶ9G l\r6.Fgvg [?t{ں#fz>C8^5z3<^J];J㦂~Px죧X/ep !O0 OA~ET-\\яeN$xI4J?\ìv`ۦ54Db@<x|# B!ևK0t Nm!#H.aρO Mw?A(;Z{ػ?!~ٹw8= *TuzGZ燌o^Õ8WwHäKDνU^qSjTk*2ʜ:s``Y O0D/ä%k/X*09؇$:{ -5v(΅R*>;6QBXH#>*=+!2]ΣR\CsIԝ]^?h'vc3+iۘ#ϛwYlXsl>?gOe><(>6P}[XU52R<5U_D.7^$i2 +*-8ª{ )ICE? ovʡS~`3B!!&>ؠ<۹ىv/_NU[C U*b2~E=t߭y1`3 y &N۬XV;Q龈/6 <]Y7XQkI<᥯| tgG/PXúGs% <$PqoKRH 8bIBH|ÑcW_V(;ہ'{Œ:'LH P޲Cь/: %LUt,ؗu˕_9 wpp&_ݰ޵nX6n*@~22eg*F[EՏw #VdӁ@$eƺX_:4'eJׇ3o䷪HGi(= ;B!dk7B!dat: Cz !4blr Y1N =؃&& d a` vo@ͮ}~HR!aOa;æ:JuE}py4$D""@&B! = K%TՁBal,r] }]{,L\tMNEvp{x";҂MmxUwρI^iʱcϱq=>'msyGqTK!ȵMP0#GtN>C v|"<m}ضi JYw$uEg~G7~,]aSZP@, #OmiZxGUq #s <Α|ss.f47 ;B!do4B$ 4NTz'.v,/E!}fm \FC'yRzeb1j QڼBű-@?谌<ۇф>fCA-_?zmN!~>I0  [¤Bz ]x[}n4XqJ֣d*1c@ɒfh7rXWWJ n+(;Xsĉoe5bW7Mk颙Bb{(k2<4iQA~)C8L\ǢT~ 5He+VǦH&ccܮ}Cq}7i+?!B}FC@!L0LN zG6o}z`k 6ePx#65`eN*T3c 8x`|&)wVKYZ+zZ92|a?F0^S\|&8 _TbgB.HA: \b|qQ9QSR"zҵ>C >?,_{HA|16>I/`v+R) k>2ց z*!B s܊zM$B66oa8"#R [ь_>+۶g;0lqbwn5Xdasè˟"Mף._;+7Z87N ۸o}n *:)Jx,skFِѕ0t bף_~ ZM}oy]tPy<WypRP~~^ U &+9 .}(닼*xFPX/ډ+NeeaCt'B5_B!$S1 :zhl.}CIcЊsz2BM~`^?icY-Kǽeg 8L96 s2Z=k\Fr  UYB6r>lFVace-caz=|T$BUN>3DbIB*6<;ٹXu>`U}$/v_v<`ZqKǧfީrIr#yukQ_N=Ƨ%qcVm<)Hb-ʪ -SWPt"R"E6%k]*4]CDHÑɚ )@!\x$O 6;ilhϾ1crv<7b{Xө%*Zҭ{HxnlN|fny nn#`'djګB!uy+ :8JykΠAڪ~G7AL&0ݼϹ^'_Ut1MTyt]cN_S[Qg`x:z3geaVB!$1"!B2ڴC28ߓ VjMC\h%lۯz͐H_՚9 a0QO&RJ]3gjX/I-db1t* db tj re+$\,ƺ*+Berr<(%l9& /4LC++ 鮨ufŲ"z!`*5 g=n=WI>YI6J >AI QB"R D*3 4|qy?=Nvdz %9ϒe#K}U0 /aU {@^^ENVwW >*kPuyz(ucpþRB>m"~qlAŀvs+:F?ư;>UL <-XS.) *_js>FUj|3wu_UyHMHpU^hBKa(ș na,Pr)za_WbRD*;P؁B!Q!B20iՅ>\{zSawW{>EعⱩmƯ_ߟ7ż V*jj\3h;t;(4\EJ":E*Mʆ j QW;U %e]ԯFR=O`\R:..C~yPX^ S_3<ªM}IPWP4N'w]r.dB!q E㈄+T=F3~zXJ]v΀=b/~i ^o:K4ر+:̦*M"4&uk/~GO;zpj _Qkvky[Ȓe ma?N_yk:/`tსw|8ŷPVȰmZz}oXlPj!(Sfe0]4q^<؜+-pQA*N_Xqs}!Pe^]U ];9q1~8`!Bp4BY&>4W5x8ScpGlY>TnFx@Z`s]p&us :q^Q.:z2b|9jujMy]|&t;[Ժ v;6rmʳsJ lCefpF#n81[n&&..CWBbڌy<8;N'YW^}]?C !q UyUьǟ{ݿHh(Yh(YRM9JV mgOIރvV+v KK2YUu$(Vcڟ=+qlHA=|E/n9`tV=uۓߎ)Y j*綡ލW_F,&'S!6P'8zON_s$X9̐iR(ʼlx,3OZa,{ Mq>,q97Y긮H.qk2t ZydտR[2s? RفBaBYP٢<ͶS5;e_Ɔg nuQ=u>zes : v_Jz\A.*mSWȝeBEVk;Zv%C֩2\\l"es/Be摸 ԯ3 pybiIr|顿AWJ n[d@{ \Bouk+&BlU aSJ*!6 ]!YapKh:v],܋._Ϻ}, %P[%x)!U j?wDCzlЃR0:pћǵ;{qc/֕m;8ЩJyYX896 f0ZX[X.(u}^{Ǽ(:mZ'{Sۿ}VHr^%rcC:7JxȯYB8};[Ŷ2u*lLeࡵ3jf/== 虅vxa-t5'BWx BȂ0Ήo3y;wt;;\^WHk nʜ:h񓏟yZۯnE4+6:LR:yADZXReqҏ9ut;qSjN;9rW݁"gZq#C!tZ> 4͏ ԚXGX }Q E%B跏<~v7 /YMu(PMm? fOArBpp\p^y>ڛg>||t }'e|顿2$v $yH ^'ѷ&0B!$X^Sx4R-6.[Wqv{>89c?B(ws^=?(6w\p{zl.ung?B1D>i9y};*qtNE68goyIU.w0{|mOz~<;By.O;ka*Qx˚4;ۊ"9J'=' ?_=`=>XjN>aB!JO !2ztva _ú՘u[ 9j{m/<LY LUgظ VҌX{sw&ޫZr<3eyTm\*Jha 2K-½]x$&/U1kX4H1zeIY %+5Bdxa8 7Oև\_C=;t!'Cުޗt9X!BדЃT t "w!^A0[` q߁7jfo\1asC;`gxm`[n2xx n;B[aszNU*fUO+v.=ѷ*+QE7y56nlj mĩ2޶G3QR4co7yL xk }cj}ި =x.td]'^e}ePZ9sUE(,?1YF:R3~xԺOaGicp/0ӝB!|PB!d4c|#y? _mnK-IǮJ3&m@à+r^<7Qă_3 ^ 9iXIv&0NT[?dzukp/>mvƁ3ǟ1q'v SykB} ]}zX|/ 'p=V-N ^oxr4Ko89I]\f`$B!\|/w|GV4;pK@.N3Way?&*BBș"u1!'u!BF;ajt:|+=)(3BVz] WY̋iib 5=m1sfC-I횞j|UeE6! _;,GG`~ZSWpp\tiloj^kr %m-]>5*:7BcmZo&&p9w=qCeyt&<} Y=IM'۰ !Bϰky1: 䦳ylO" DȖ-! wl>ːm|޼[6k6}%XT4eEbV!RQx+;:e4ѷ}-[yxu⵭x;{P5 IDAT@O޳_iY$ZP0 A6cnB B9`8a"nߞOA^+*Z w\UEHmWO`L9*=#~BZ1 Bg?!BDB! XΆX:uWq~OnF'$3~~;6Tw0pCVo8Wӈu͛y 3q?m :Y6om6 p\C3T٠ނo;QcGmO=P`ڃPC6zuScZa3mh9=`5s2pe|PUV~ ;vm:]NX^'R_/*"X@(z\@J !ddWwXݘVtƒ, p-/x={ DȐe!C P6 B@w^UQ ^)4l@+f(qyTYn:] 庘˅y qsgxtsN,l4maNٴz5L@JLLQfeAw@YB3q,Nwp r;r)eUZ0u'Y-%g**P*oT]ߣcTEB!<!BYg?|W#<> D?.Tcu 2װ%g&~뛷$8kk`ܝp,i"E\c #M5JT!Lm3a&SRA.p^eB#fW7MVl;y,i*>Ba/~ y C_[1C!A5XCA)QaW" ?l: ԛku;/4YSaHUmb28DZZ<. C>? J=浠|ֳtEϓ e=>sEyš|e!>wn6WP:1K7a_] L'8f/yrqR27CEpdJ'A*Nx, wu+D=oY-c7!"፶D~Tk/z7B~ Q7{=0ݙB!BB! 0 sNདG'`]jLUN) )VeJլUa( ^K/wx 5=,p+8ԣ(L3uJ:ՁMGaqUq] ʩ_ؽf*Mx?BV cc@{cU~o׸os?O=zc&0\,<}Pi .!$^"x ޸kuNOv|΃Ț4IlhjAEwVyh f@ ]L'N!5SwwÂ^?^RI¶DM΀$=kjpcfB5B!Άj=}aaOxmxTV1mas>zN6OxC.9ןQgBHjGH H 2V H\ed^kt10ؚvS ]5Ag~ L>%d0BzTA,>wFFPMt{q8 cǣjPoNrqEW2`ZjҰ!뺽x * oEŸ[}kWozmӨ5֔}5^dj貥J7wpÚέrج޸kI,]'O} ao7C*Ej&7v yZtlNh8;X&BHQB!at-i 8xmo܁giqY:rYNmi15GE#X0ݸ1<7ګYLu*Y/5w]zUFrـJH&TRmxCCW@XB>l>*n8l@Mv(*1iJjՁM zqcfSCv^ءOӁܴtikќ?Ec4ԁD06mҟ?B'"1nBQxbn;0'kJr+1>F!};bm߁zsȩgC"IqۄPoŔ)'Xs2+oM{$",7~XsKF.[6`[,k:. =+o/{S{olً9%HK1kko!2Uq5 pwd;^?*cilzKKBcf-ńBI$ <B!p0:NW`Hm G'ѕo@%af,\ͮ25=AL5UD"7S1cmw&iA8q1g0A󵳇uWe+73GTh<@6B_9"g2XwCu=1t//勞sd^ٳy%<}L77!pdBzU*HˠWgbk^* v8B* oy;aKAsKX9+idg5} x)CeÚ8mը] m$1J'aMeظ~ڌ[WBM!q[LIVա`v pe-$2Rku$4>ngSVHIIJT:*&xautB!$hT !BG t(zqW ]*'a]fgX6D>\-} 9ٽlqc^9tWO b¹T(͘ڞc?KE"jP̓ƉLN}|3ⶍbUPec]5uS]y3'~xk;6+]!x8h= |TTV@&_q56܊׊|RJp.9M( UD[say9-W1:o Ó 4@ W "&!#X(|\ÕRRO}e߾ 7[`xiًbO *moۃתWA)QbܭW[y ؼܹZu(Xat=ո[ywxp#lߋ3WA[ҥjY.~y5gheQlٴ3mw $.Ŏ]J'A3q,G6flFd4(yIr ]'-9y<89D"T*Dc++ K!d@_B!D<Ԏ}bO;7z~X}ԫ20O_ťe5٬}͇΀ 8>~簻3q׏u˄҈yX|6=.I}K%exv^^y" ;pb=j՘9f, 49X\zU\+ednvӄc&N:o.{v8- sWR4e:s@-KW^8VEڀt4"5z sPE8 ~?q>/ =\_  x=CB4ؚB@*Dbvא %\%B t mW}p{0@6갃-x諻+EBv[6ɝD{Yxܰ6s;(%*vM#.^VY+dMɨ_&BR%P `:!L!BHX:rX/W1uQJ|_^\e:/mI<'ڍYz^& pF6La*ԒcvkބO_%uACDEe AJ 2S#f5rFPw'jӭر᯸n/b'=N^TO6Mzu&}sӄڄB4r VR*\Tq r6u0 C 'F0O$RI')~&!-? ދ'9PU5%xl%bZp(!M߁ϛ`[ujيZbqT ԛk qp}a%V}Kع _l Gk#N5f@t1%vf)mo_l"ΟF$HZs } UImLJ 8;/עkٷ4vІO+axvx.Hr:)`5D]V؍r9Jh9TvoB!AB!0 sNkHW\HłFܘ~V4k *݈[ p>N;^ QQ`xpew8X{]|l癤 %pXC7+on IDATQ&S.0Q.8@5 ~lUuU[?yp=VxNo|*x*/a6:YS (⍒ڃ+Qtbu0 >?mB=Է!!$DRk;+v0N#ZMQKl8tP_- 0Fǣe+QQx+ҥK.Soŝ[ J kq%]7~;^m{bYy0E3SJ,DR?`$\w`Ovx{Z`mʹp =u[: jJ&ae8XNm)|˥qud[^D{^ BjDfhc ì3B!Ɋ2"B0`* 9&7Ώ;N}nu;.EDiu͛ 'PK;3tȐy3ns?26ྪIvPR1sX,.)Ós?]>< ;7))SF^vnŁ3܂  É# ;3װC,K$BrB3X/}^g'ddRxl/4+=!$9ql#w6ö-xf2p.ϊ}0+v.ooѨ;JV~ɚ(_# 7};.EP8;w|X1+o^L`kC;Bs¡mbKÆWakl>ϫgO޳h8/b s(D׉ӼɆ27{dz&Wno9d*JH.?=1~YP_q5#T]:njTA^,8af/Knr4U:`3w0x:mi؀}0|}^YZu7'hoA o3j/I͎uz/͵:]&iu;/Ňw7⑲PJT`m^;ƞ ?fU7qlC[H%ی8i۔Ѓ\P2w\|vB!CE${{9絆_؍&rddd %%e4tB!$5 <B!9ݣa/ H)=|,;eL.۠㽝ը驏Y?BNALϸ2z& 9+1zhӨ:9sJ eh ;~4tupZ]}zg}xlZ5C0@{)# Zo?XLQoG6Sr+Qʼt c f عJZ:rد'BAoB"~"gnBێ3][T4u-h>@ۋ >jf/Al=!yfZ濍|t{0 h>C8dڏ&qDQcJ/ H>`k^9 ڄzs-!~(+ hJ-1J=omlMXqt8:L|q՝DJʨh:{80cO Ne7Y؁C?߯v:.Ҡ76-gs @:S لB!IB!10a֏H$>%l{*E}O1,) oԲTi(RLdGT@KpSWTqj>U[ U:[ ^pg:tE ;)aqI{\,Shpvo/}T6SYԡÈ?/߄D*%Hh2)8F '!$pr^g08[ =b4~!@A:]&4tE5k㨪0%}DaOvnl> u쿨j_/ŷw7aքvݏw܍c][?",gATm4|-ZxݮЃߪ2'Rk1Xԝġ ЃᄽL'^;ŸWlN{~Rt'WvЙ@!3"Bat75%%6 d㠓e'l[qUYZu|-_{흐!Uʇ^wgR3xo7B&HC?jfxaGnH!B!q0:Ngjozz:嚄oxvp56pOmƍyH'WSxԏk^ B#ʠH! #^䰣1d 9O3=AC U:{珘]ysTa3w^,É Nx*Jy =?d"*⦉cchk/SJ(0t҄/ xL?FCP h!=!ijK0q(B S@7@ DSUwpE /桗c\)阒U }C$}nR|ey7[`PնTgM)X>iFNnjn,UE e#zps>O]~'82V}弊P GQyyBR5V}bpmhxy0uQȭʐžW?uSX mWAC1paƒj4fd":yr) fو yEY֤:y2ol> C'еhcf !2R/B!đNcѰzB3OϘ$=m N4?:0XeK3w%On@ 5z8N49 tq#]*Ԝ\)rbD"%(HEbY hptCW6aޔ9 2KB8ȹ{x^TmNkJ̮w*;8-V e06~ ?3 &Cء\,>/2>HTߵ歚Åhpv0,(@! <WtJ ,~ $jO׷ZfRof~`mDLTj6bվ;yT-]W:xH]lӲUm' 3\s~CPc>h?l^ zmTUjNBa=Quo|0d&+^ ࢘l_wbr2,:~JDA 2{{J j4}՞lN6^SvC7`;Y-s}a%޾h5euC_СT*av/m՛ka=67[Ak}BBPU"^c6|P e`B)ҸTʨjۍX $af:ҥjLєhoAUtk7aצlw*L㺘l27؟ A#>6|ƺsC|-9\#ԞUdϝ]ܫKxWs5 K|7ݎB!!BL= ѲX 3OP/gφO&ʃEÆn3^]d(DGF H_A,u6Vֆ#3jC mrc!Iy 1u`džRE%mI8Xˁc!ָt~t?z'@b- .$"! F1d4cԞlCS2!0AIQWw olS7н6SYقi4/k5V_o`_+A;}7#5u;eX>g=%.ho xevNn'/G%}0F!B!IBM1h_0܍ҧ]9Y LUN><a k˜KPbfŚ?snhA,BrNH|^a8 `醗\O2yB!di Hd2/?AA^ }zL[ͧxvTt5,\4Zav9-T 2:(߃c#%O)fŚwb_]K[/> ^ i*]$6Vu~AgwE Po sL5 ny+WmnXիpn:Ʃ&9c3C|sX߾dBUtoxx<%>]Hf~'t֦"U'Y9%㨃# =_qK?t1h4m.Ozfsr+jBR$aFά ֏u!8ESEټܼJi+r/۪ïּ x^*]4XŦr!܆6S7YD8qX֨^ @~q Ic!ՑVy=;XC3qlr_CΌː?|47KC !2QB!$ tuMsRx`Z$s 5=GQehIB!  ! !@~6@.f^K'sV&l_kzS皤AeaWEΠk':]Z][kn'892YB_H[($4ۋ/nb= hPBzoW( ߻ [CozRBݲKv/ۚ?)+0Xbxe1ߏ\\[6_y%S7gc $+⥗)RKnb2liy7ae a4p IDAT7YW23\muP *ф fOÌ%?6WC[l{UVˤk1#O 9f,*.b`1 [pړ۫TbMnX\JH,x@sW" HdΖ"U5W(?:~ "P Q <ʛ+53&6ݿ~)aDVwO>8)}ɡw^ ബ\ojWaam)e_gM'PYmo4Eo>s Nȟ@ b\Eь/m[A-?6:(`:҈o~;n/>HC /l;zcMk F>v\.:# %29f,LW!@.Gx>``ƺjN)sg#C mNyE!KSc@qM D,bMΰ=G(pӨnkVN3M,{gã*} 3CfH dB[E(ڍVG OSASjl[ڧj/V.hUEAPH $5L&dnkC"m֚5yx0ﺾk֚}񁳄%UI(W߭ǫէm}+Ŝ)V:BEǗOCK8vAkhv6/O߉ݝ[_v:C_u{7𵫳O 8vsU$txouuU{C Ue۾ymGR˘&Zm+(Orhcӯ = l=B 5X}ޱ!Rc!BHcYv+j a5 d2n7( %tr,:= %j LE:0S1 4rRN}2mqx[Z9  2t$aG}w'ww̙JNXH@dv KUTl٘uymG| };ARh,czxTK'GZ2>Wrtyq{7H-?Mhz2|a/Z]_Rwe]kj WO=L6_>RgMiM|J9W䞄oZSBZt4)C? /vB\L81Ŋ֮}!8a;MЖRP#0T<2h9Hʃ܄i+p`?dc!I-t%0eكnE!BA#b!B˲Ϫ0Y.Cv#P'H@s$e`>kHD `C E"hr4r$T=%\xh>M,זœ+Sz T/b?kd*Et{Trμاi0龗oY|PLTP*JR``R(.kF;]pc"7a-IIE) =hMmJh!>:}uPH0;Q_ ҥـxERB)$+6B!pKf AU*^A Nprf E; ($^AF.=~ ˩6Q BUHhrP320 6C46ifd`(t6W/{ފ<7Nrɀ!IXj1m< apROD3B,Q,]6{n#^]v1; 0Θ嚊8wsb%Ne܆qƉ1rnJ5S8'7Tu>zv۶ /H,vw+<6 W0\e8.~s R1,5X>y0׀z,I-]0 ]=h;>g/nLԄ: dEj}_lInഞp qeslF juS؁B!~F!CX]n6x6W$ à:#p~JC*̒KCՓCGg5`T*H\vm(\o#h_DЫTɕI& (P$xww  JB F,浝 [J'<1 0%rhP IXt/eoTw8+RM-bhr6 ;p?oKqe~^a_W,,hRzhïD_S]^v6njs~Epw'}' KXGSJǾݜ5L(߃`_kmI2llYfg#v^6Ǵ+x=H)ǚeq/_<ځcmSLAgN 4#pM=b\C}]pxvLZ]YzH4.}Z9VhC,>CJB)T4rB!$ǰ,ʙN@J%B4ZSʥRJe2(h1iD9]y|Ռ *FL O0(زR)YZҢ9W Zaf0W:"pM;;p8WPjS#aXk9RuB rI|h4ֈ"QbX| UZ(Ly.]lGm`_ jƖ :Q<BlmӼѷfd)=#~6(T{VC%j(:;bƼ&*C 6;P!.ET k~ ΧUboq̄r#.5ཌܔmudBڑ%p4fҢam @?ǡ,\Eh(;&7 g{=bႪ܀x B!(*B!Ų D Ņ":~'ߢLAts9bh(H$`̸0E`_d`D_(R|Do\egT S,-]CYEs[YԪ %K`5\0H 7NAɎHt2D6l336`/)|VDb!BŨК%s׭K Z ~UӞJi݊bq~ 3X:?]qnS4 "JzvNqH,pYX2a/Bn(Ek'm+Zzx8LΔ2(Pt;]ڍm6]vuc *=d* f>%a!B!Br˲^.}VTd@Vm١TzzUl>0ŐKC瓪3hOLE,&/CP F"IF.ΙA*Dߗ:쓗bz@ŢHLAg9$_[u0VsN- ߆ϾW?tŇ6AeƂ.R@Go(,BRlI0^`iIR^gҜ+nq[qc؇{WwӞ4\eBb?<=;kۄydt*c8ݭgLos(X8ƣݘPY1"UGa`oc;}]px $dA39(Н"tZo}x#(.7 VLVxDPDQagY]N8B!_i !B˲v[h aR)˅P(uVN 4E rjFF`$u =t@>%! ^1ez:e!49^B a8jLeԵΕwRG!a` >X8bJ$g7#hj-KYebs[WR|V>WWu'2V,LLN.^ֿ[bԸ1CknI|aX5NN`ˆb. c+l}ȹ h+QIB.Ka~Nxs47 Vy`i]XlN8yU0rjuʷUЃD( k95U0V=l݈ЌHo9ST NI./7,˾B4B!_{>C@!Xi6'"z)}D6l|>^/Z:W1i1(% 2ʊu)aɠpCaR;͛H~:|v{ft*h\E4ARATӵfL%N551P cJQ3: k@$h(02 >dD+?|[(>Yr|V>[v[K)P?Ѓ44`}?Kq&wgCu.O;_M-~6z,KL-krޘ{7mbc餟lm}0ع]y9mB y0 pfu۽кsX|Lؽ^"Ϳ)X p{#I~Hb(RH$b( Jxb!)xZ|sճA7lGq rSʏP8kR HXB< U,51(0Rad@0(Dȿa0̀NNmKU]uB>ׂ2n.냉㢑 D=P*M6=v6RH>\e] ;v-$zXyXp [_+ #$r屝ŨgF:uڴp$g DN }~X2FZ2&4 =o2^}{j)ްⲝAΜS1+u0Nro6uJMێ1 Uh>eDR!DDi9?R:I=?˲rzj&B"B!IJ3gB[h aC(l7 &\m4S'(@D@#S@% @8@xFɤ%R(q1*; YtzXt{=菄 ?pkw+Y{Ujh :?BÎN?*T&x?/bkps/ gCb"Q|ഄ{aCi3K)mK>7 14۟3བྷ0HË܈}D5"ezQ#Z@k^ہyo3F}'#K0ft7Kh&{V`abRa5ktq~'?~7/):EpׅrMEᾠ Vmʌy ܆9_R~A'`wzλK$YvP8 QHXkyw8'zxv|sMUR~ c(G:`;LG p 1;/?m]P D}RMTB@TXnY} !B.B!yeٝfy"⡇B98aNYd45c^ 8Ԍ LF+# # #Jn^Aa8_b3R wFap N0+Tm%~/wwaά^Zs) %) lkvwwբH}7#$,0C…{(glįh2DcTbǔrp{zxOşݖ󕊄$"$v?WU}=̮0d|?ޕ 4Bwsuyvlj0g|B_{c}[oMj}rޘ!2T^Fp$A=+j0ׅ#_svZzI߉ˆ@aᕋ}M:lڎk"Etgi} M= hi/i"``j%J:zhnscRG8g@ֽQO\0X 걜Xea?w!a*H&Ja0 1 D ;B!pC B),˺c6xP;V|`F3u#JJ2g4ӡhpPpt(~1a8t 60@'\"< ]}w',;eʥRX %K`5u n춵nokڜ#Q1 ~wHja4B}(ݤL@,v׋H@*DR4 =ۊG?'ƚYGWcmm; +xm>גi,(XP,ӝ3c8C >iGLȾ"%rMaw¯׽ԯɫxXÚڗq{,xBnjYkNN\T'8vM3c\lr*O~ J8o;\ep̵:jF< Y3LulZ]-B%-\*\@4/~"Ђ*mz\>Dy7 #IXPr#u^ܼzƣ`\S *=yLLG8pI׍O1rph.[7]*Fگ:WRA) tϲ, !B B!b6g@[H=BU9e% P,YσS^BAE"( iU&H0BS RM#\zxJhd򼨸m;pvWi*TSQf~y`-ZP|yDJ@j!2*săED$$q9ڍGMě ޅWEώ: VӉ#D`@qki,g8DL-r N^>ٯ N m%Wx{6vXbӀ6^ͻcJp +3rNzτTH?wEFR\ 1lyosubSIKozJcJfG0Hm$?C@KZ#loq+۫Vʰ,7#$I ;HDR[j1?t]vGxො]?rNiڏ^aDYs.^7 FN~0AT_c 6>UP7?~|aoJcsîέ{bYcTp͈1r.,1*u?ӲM?@;rUDu]g6 Ft v`o6^ǵ?ZР~!,PFOCϑ}_o; : 4#P9E`r|OmB|\| =,˺@!BBB!g6rA e021 r3aB5\LǍ=/Tb`I$)(QA&s"WA0PdP32=SppCUX^T Y!l#DA_ݙӳD'T(_RW.l"葄仕֡X7l&On=^}ײeƈ%Fq^^h¯%*RȊЎ,Ix?Gmɽкe7NteG鉙B!Dx B!0-%1b$* 3HFX, 3w#Sh`/?a8!R %ЁOp4 O(8_(W+8ȥR 8Ջ;v}:WPjSLE>o6L:O@&??+ ez .O;<6޳Nmuטd}>yV2*y(%r 7t`}'n][c+k"b?׺)e!1capdvU051iL(xt5D`UW@̱v~' N7_?rLZ]St(./~&[$ODB{Tšm651PB`-^ʻj1g5\/2)(@}9x`=c!M7NGՃN4=BJ*ˌ,7fl)*ˍ0nv9 _\n{G} aT>mׂ5d4]UuH,|?9`L{+0X3ljNmM^W%m.ٯ ^9C*:^۶ :T5,^ Ȳ+c 8y0ŊE7N}fno?OĶJb ><{=  x[ "5Ug°>zTy`r{rERK$PqI?z[F8@!>.7 v8H!BEB!r eߥj 2_5 #ЀcsЩ×|E7 K$(ɡbtp?B(E8E8zO[($ $bUnHߋݝhrر3'PVCPu\ T{3'`yR1]$WFR( t}_b{9l]=/~s1 :zoc;vcoq/^i[z~zv6톽דQ+eiXg"oB |O`u+X+?Vh+aVgur{,ͩGw=+xPUӟx$[ӞҠbc:Գ{>8?QeXܖaKL˹jA2XA)( Js6hċZa(LδfK.HB^)=G:~?ZsMU*1ЌJ }mnB|k]`9˲.B!QB!l6BoT]q%1R' q|8 obQ(H`DD L%@0T!D8b$! EOaGE}wgNTqKrEZg':^ Sxڼ鏡xlREy> tdr2P$3K`obd78ۻn^a۬Vʰ,7 $ mzL)on?״v݁2F&r{= /z-f0$2ïb}bI*i#GBer{rEJ[3b81zۺng{}_[Le݅B!$>(B!r+Wk֬yU,}?ဃ4$I,A+W/DLR5GPBGj8>j{Ǿī #_ޝ,aRn0RJ1Uz_pNzY Xl>dL~D@D  CY+<02:ΤTb۾#/>n& Gm“Bzca8w4*uW2{=x&\sHs\>?Bhn46}DpL*A}s'DAH)Un@4HJ}Ah$/{* yEЖPd6n8ҁ TeB!zTB!\l^B*1wT%uh,gWr*:vwwad}:ȥRXzX%JȣE-Sk9:u(,2 bQ;潼ғ["]=N aUK'@4ƫHByWHt鬀ƖughŴ0ΘB 5i.O;np50oX4Ǘ|} ?9$-VU9Sr3ЙΦ-R`I^Rٿ~ U@? ]bURÁ~8'A秇zk0;Ga{=˲rB!!BHBfL[h m+ðIPJi}Bw6W/{DÞ3ۭU(a 9ݶV 3X%`ˡOpXBǙ%5Țopj>nR!vM{)F&ɫ2w]_ ~ݝ8-ٯs ^Bimxl Lcw 9]ۼ&2=^;{Y+/x} GeM7Nu@)g ?&^7} ֛--HP2bIbl퇏!_Qm81R굸DChݲP:òB!!BHfN5 w y:B `aG?Ιm75CS&oϑ? _dyvb$CTy1)\^||<)w'P9ۻn^ᤶ}ta^YYn8YYn"F7ī;HEXS6c"sVYygZ[0J<\+-Yaxsf""zv^8þ_!Q¤.UXr-T"Th+/Y949g^MAŨtO1<=]^8ۼ| <`gGG_j?2N.AeDBuw;՛=&}'Ѷ#B|k@/lCU2,/Q`Qvr/|,?'`OxJ<>iW_rflw{,ޝ녬eq/V!UTq͈oa&݃O~zvOƪXI#b2'>މh,zvoֻZ̙b͉c8p~^m=eƏI qx^kk_ \^mٍ>, =B!d!Bofy"⡇B[F[q}:!df6woj,PSQ넮cJ}s vh 20b ;r.o oLjf->ũՇǮ DV1[IjgvI#~ra568`wϹT3ٯTڶ[*byzNUXaMpr3 IDATynM4;9˥*mNz&cƨY'wunŮ+d 1犛1te`k~ o4ő_Vxp~N<{ssng[of͈Ќ|peT/nlEV?)_к#D/eғ-!BHQB!$l6<`q!v\5 J)"tChr؇84;9/UCUAPtvaܧ] Sxڂ^p8^bba_ D߼CaB.K1e =t6|5ۓǨzL)UWfl)Uϭ|@F˴hXI]y?^aD9VycU8[WypY<*ZS҄+`ȹY]igQw lo;kLcɫ0fugA{ ۤVnIݓL{ࡵ/n^̓Q1dʃX"AJ%Ao[WR<@;^0T ߇8{Xuѓ-!BHv!Bl]!⎱1PBBR}z(!t*âJ춵b Uyq9 D":>$.d'#3ԏ;y :?̩cek0sX)ö}-xaӞ^rM5Z:0Psс.49s{N/Z\cvp>޼y'}Fy5gɫ|*5p1(<Գ{ߤ*C f#f}Wbʌ/XU, ^Xk__7̀v[Vp{M{xUp6FϺ&#۬.C[ng(؆#Ihybm`g{q(PbYQTA!](@!Bc6'R}(5=B~/lyQaE; Vc %[l^U+eX`:LA&$ X<bgJ$xU :'^ہ$ >W?W?l0pkmuĜ kvtSJҙZ:c(;`Sz*Cv3p@i.ܭٮ8w[73a//o\L:(XU-^ëm6Vyز.%,.8cMN+XqOtyڱ꣟`G{۶I)Kmx~5yUR။2 ]y~3Om߷^u׮MdENVz8/«cFLn p+-m<.hA#bI2,`f2`ՏKBFzx͌d²Xa\P,5dƒxl vZͷʃ?îέ7zAƎ} ?t psp%+P%O-6}+Z_X^ޔb)Ǻm_ Uʠr#JԎm< !3Uy&QmajT#Z閎B~n,DB!G#!BHʰ,{l6O m[ݧXn1v<& XBHz =~/_T 0p:( VhjƖ'w]=h8ڍcCAl0VĿz蒯ڧ0x_yB`_ >ׂ͜o\pZzaEX5`m㳂42-ԮŜ+'GoO\ BQ1*MX:gI? vLʎ`UW߭C!=ۤJ۱ G0zVf&wy+/X"KL0T'ڞ#B }'Ѷ#^,Nv !Br!BHJ,pl^BH/75%eT Mp6i8|^}UTT{tS/ud]e"d,ۙ҄&sLr:irLϻ!;g)!$gM!oB{@:b qm _}-Y?7m/Zg_-mD te|잂HD)`*#W\*$@QhZO(7+^hzd U7|=[mb0S8xŠǷ ƨǏ>Io e-6c늗ܴ*|wc}{QNv<,M`"qݹSQΓ0Bpˌ+_%X9*H1?&N֌**4泧4[{#PИ+k{o?9nqOnhBSiÅ{ٲl+^5Ox9_^mHin{6kSxݘ 0w"<d0?ȜIk每=?* <$;U.h%p쉹>xVp)KuWStlRNB1,NXEyJޟŰps1~+gx QA[""" FêBq?9Վ,X;AD)1`="9O0KB 3\KECYG_(}Gl=p)__ywTg  0c  ryWO(^ӖAig)0M]CMhڼ8&yz~m8&`nȪ>sgT_'7@EYUjT/*{S^a`\P/ ŀ&]21lp`gDP$TNδr٘Wデ:ոô'jwމ[W|paoҖlݜC2Ӷ~&\?~f#ʃNY޻;ѰДU@VL+٦xj/3@> ťД NBn"śѳ1wo=05tx@^~S@DDD|""""J+A2^(>8pxxRTh9(˫-L9ǯjgs E`!9.j? XJ9? v>Q " 1h!J@"~;v}ОqL$mYRjWn0ۏQ[:a߾:]닶^tēVdS%ņz޽Vew+e#q0L2A8˜o@Vo8oiUUxD.#KҾyp`RU󷷤|r}**0ٯ8G5 {p_͆یi9&MX,Xi2s@`8`$>I\7DNB +Y?n B!t Sd&/6.dU"C@ IL W<} ۇ~R~61TWel+!:Oc8{xZkS<Ш99t :w d-PpDKim|_;ۺ殍aBAC!t8ggn>oZ'`֗(qC}8-өë|)ÁDDJfaT BvpU Ӧ8Ӳm]xF߭qG¥0(Gx8|^\c 68;&*Jha.-SrXðj?'+/NH% "E"@0 ȗKk{]`DMYD;qgqL?tCc.?ٓmSJ}Z,Ua47v3Z>+Mz!Ya,^JfY^bWE.7j6ļ>e")j4Q)UJ9EWy=kXnOJ1g~A7DDDD"""" 0(8B!LKY(9|^i3aV>/;%MXBC1I}/ڇase$0~>TA!")VcHPEEWN멼:*nuM7iC_)[{3`:`!J9aӵm}oIhnﲉnSWF<l=EBk\@F@4Sj.ňG+0s-;vڴfUZsW ?w/b}?t@_AAEF=e! 2% -*J(-Vc}qgt[js_ŗY툈(JzPsvߞW`ws~\~7m; ӕu}tŠQl۲2'qNxڽi;@ôVqDデ1w UWGBD7yuiMJH6<^I@4p_͆XJx9fޒ@VWc7._< τߑcX<7%*EA,?>^?CWUdd#0^Լ/wda#v*"""eA0Ox 'ڱ4k-` :=>1ܐ^lq M2=9xnǗW% Lߺs% 湘XUwqڼӆ*<o}{-a $uxVRU}PW\饘+$S =~v'`wd$p#U7 __k92-Oi&ײIYf: nP7+ )wɹ_91 :v:O1ì[3Θ_/&/p~m]ZsJA,Śv|vTta#لQn)Ĭ{yzָÄ +A~{V=ARB* 8Tpa.u"6C8D81 B`CC?9(g\h4P 6a[)x2]X&CťM(..cr04ʉ62 nDS}uV=(_e$P@۸n hYjnlxerИρ{-񘮰'F:;ٴQ/Y_^zu[t;VRkƀ&x$> DOBۙoaDEeKz7n/N u^5&px}7bMx ZY9[/:0?CRǞja_gL؄B; KUቈ1@DDDD9GFc+.Op< VDqჸOɿ˃ Ӧ 1 2d3ϋ b9u>6~>랈 pDOMLE$D@a^(s198&>?.an4_ K 8ݿ4_<m -k:ſ͊fgSdwʊa,+om''N[࡮1/Ə^S*c*׉n3z"j*s;ZN@Ͼ#^,[ND:t𮔈8Kr֥ҵwf/j?8DZXV{p`x'$WO(-V$a~ŪqZ,B(n6gNJ(XND0 3""a@QQ޷kyﵼE+;hr>3Fk!{ntCxsե:|wgY". ضe]fTi3w]*t菳ňG@pݣh"]߂dlNlo⾚ 7<^?/ m2㊼ ;$2XJksf{D= C *p=Ң"KxRMޖR&Y_Y0l(0n6g+cT`'A8{aZd*P]F"VI:sQE%*&M :{5C V.n:]coF_G7.yqg`0WߟmNo:?SO.yz<'Ũ7w.u |CLlk`4!=BalJ:ۓ<~ꍕh0s 1sۅI>o^g꫱a>*X$U!`j*`.)g`9~~7~mXC>K!gBx˖*W{QD~W/XJ Gq Gs\ S7 SZ_YgzUEXf\1֝8c{;J$k C<^WpgAn """_ (hl@CC>T{(-V3D̯W<,Ǭf +! ¤ 6.UJhX :hTJ43۞DRL5wbsX9nԮM7~'+l]i[W֌:Cܴ :EiBb_ p3g9\ˈG ck=?3΄Bjy 6ml1֬@*iu 81xD"8t\l0K.CN-ƒoe U:xvэ0@DDDDyI^F ^A%CPRy<< \~*H!j"™&UXcEa/yvrO1a(<]ša)gRo"/#$@D c&4@A7xzÅ;vߎoc/<uXnZ5ףJ[aZ)z/ :a>dB ckG;gKSBd`/Ȼ~)Js:Q < ż~'’5ŐJ$G"yt'VuNO_NDDDDtC <Q^R7 rڃe8+~JʠJQ*QT@8w=[*@PB_\}x=g:j{<4/@,na ?Uk M 1c<7L4\@bQƒxM;j kAs0aIn=|lsazڅ5kQQ_[N2+f<~SX:eik'vh^f?(⯕r*mFC9c~PV]w}*`UOr䵉\2 #}W0 QAqTQ""""w FDC rڃT"`*`PN7 G"pp8Qà*ɻ~ɰ%4T$ΡAvփ-.͋C!UTPJDDyNR0(? MŅ{Ӆm_&r.`8mgPO40 ǻaۖu\;dԚX9t G1s}ʁtҸg~3sd/J)G-eVCHddH|!ҷ|Q)m:<~r$yAG㥻O=7_w9sxڎhMyAO%~*x~ ^5c_|zC'qϬ`e_*qUG6u)_dН|\ J+d=Vu‹ 4( <QAM `B\^Ol7F_OyR`Z,AC!wX&}9gL:o)CJ,pl^DsP@8epb9 A6ɀ"[:IxMRlk{S7nr)p7.\5M7{1ۇEV-8cJx9wVb72>I8|E-/YSG<f|SMwul>y?@-e_ZJk vy >=ڑ}]aoe'VuB'U:DDDD/\*h4P}1]aɌ,Tc7vH%d[O0qLXxRؠ>/ sa؜cByymU4[l=܂#L庼RR N=QD"@ =P2Zw]))Pנ'7@S|z] jJߟy1ôZ^F#`k:O[EoϤ7] + swS&jz oɊ}3d1P"D{)0[]s ᵓ?‘;b!Օ7@p}Y /<++ģJ+:pG!_c"1]ahTJVyR#`:DRv""""1@DDDDKK`.~B8p'ZaRTh982*AU{'azDҶ suy7io )>sMtu4;6atuX!irC b5 9 ( 6ZdV$>触 ="NR|հЄ-ynv7붱A Ke9* ZTia2C8{vyxh{ _fut[i=Wף0'=2 =~+ϞNYsLWh+o@~:K .е{ot)|MRx0,xdf(H"xB?*!7e:5\L&?֓u@DDDDK_>j4!ZA_jgfH$PӇ#qZ7br2 #d] kh'.r8kut8iLQa演H*eᜟN*NED v6hGy_iQT(ߟ+^(xpu=Cc.!0d>g`aJ4*FC k5zGqa2N|9ȺCێv| b/>\ȡX!ipJT]JR bD_ `|m}z$0mj*GJJ\r@&㹕,UԺ0ف IDATN^!ܿܿ܆%Xr =O}GtCMz~;ܰ"V7'oYMb?vƠC"ly-Mw%uyOhJ:Ei'wvo 󊟟<-ߢ&&wng4ܾ*R5p}yu /ڇ) bh27.7K< RS$Q MH{2:Ч-үP()y4`_;RJe!%3ﵼ k1jj"zKυ?upo ۞9g&N@^^#V%*,U9ُ /޳ Gl=Cnl1:q>>0aقR> LlʀH%̞UH%tD7 &7p@PJD7VT1W0C?;īoy.W{۰k׭[@;Z lk?IVw5(HQ)NB5[AqKzþz"A """LaH$AjDC ?za< γpPV) =| +'8]p9o :8|^|<`x5)Va2RU5Ƥ1@$N$ ]G04KD7g*k?؈_ُ ,s=[>}Q}qD;a ԥo^yTyWw؞mVtc[ N&|j実\Ul .L)KyMJ7n`lu/7 >u9Yb |Q.WxZ7 OF+?֓'8ÈVueWQ&1@DDDDK0؁.}b} })]9)asuCl j6 MuXbKk0NeA0_`j*ZABo>RTd.2KRbۖuI =x.2N;^~! bmXeKuxh{3|9mCފQP_[xtǴ$F#XP| c΂j6u$2T$}ݣ|IFNM!n=vb 2-JTKUI Ш S.˟X}UE;ܸ&lO=êDDDD5"""" hlEêBI~r+Mvd졖+_5៏'L:LZQA! 'lڇ*ŴbK-FXʱR Z%w <$J*yٗQVШx֤-/׭Cۛaʋjڼ l&}Uy8i|O~~Kf?DBwb2续;qƞb +v_լ˞m}HgJiq\!XR $7_[gh̫>'Kh'Wy]sGmBs ں>EOCc7y`TSi;ja~\ <n]F[ObI^?׉hUve ڏ(AJe4 V{8e50 5i =d L;*͸ҌH\TMy/*өQQͫ_S8uEH`.)~RY&(|[a4mI]no}w?ԥ>* """"ʺqvQrh4@CǸߋ_R<0e1>M(զCs4q|pR'<,>cgsǘoynvGS,غb;rM_m Igũ܃Hr5Q~X+n Gjc~IUu赶5֕x+VSiR<7wZ՛}iE[W/[o,FD!FU[/֓Na Vu """,Q]h4dygJBY\Z~žN=ӕwW_aBC Ҳ^94ur R3 mB(2=Q %"rOVB^&Ty >< xD;xš ŀO[3}ƶy#Ɂ~ۺޡ%vIdD?e?Vm|J%UAǡcWo%T25 xHUlm*muVQAd]z5v}KwhﴞB[W/nmAU{WϽy?гwh """'g% ͗= V{/¾ V|<4-Ee†|\]XbJn~y CY 9ZXRs ,U@!T^S̓(I2>`!qEE =!ھ}I*ʴxڞxTҗUwb8{丨v<2[J9cq=A7lWOƽtJVW@(j=cQx0,d? "A|CMim?,3P<}baZw}Z;_{U 4= }(A u7 =U.p¾#  Ѫ """"v <ȥҿpI^:y +Mvd% OMa2_-jutg9J 9Ҍ MlA"c '.f>^rXj1f|{ -KKxr%Y$D+Kp}b$KJEE  (W CuU/<WwZOe$0OzG;EYSH=+B'|vGq/\UҰNQ*:eu_kkpKI>$gF<f7t߰~] yxѸTBp& *.Ą('I;[DiM[iCߤ7pЃ7hpDq4,wo= iVu:Qnݗ Rvd"NG@M-n5aPF`sT7(PaBC `>ϛU^_aBmԊ8:q֓ƈeeEaTrf.QJD2ީP$e'J"@(7P@*d2MrQBv!ud^ ڍcoF5hڼ f}[ah̅shXh{F*ۄ߁nC U_pe]rӪމ nθ'b%xbXJkEMJdVC( wRW]\PJɬ\AQWހG]F;QW_Ә14fdc>x׭Wc)~î! ן[<՟yz? 9Vu """Q^L(+VS(mvZC4P% M pa|`֗̈́c 'l7tXRcR-XRcJ)ϚQ)`Ϫm"GS̬7$ <$EQ GDѪ'!`>J濻m]oJIicA̕hM4=)>[ kB2p$デq|0]w Z3$øo#C \ra/HRR*muuR5򇄶#ˆW@*-q'~~.%a8wTWwUS3G;r}&v LWUK8xŠMgDLzhl۲{A8&W<UNJ@DDDD{o. """"JKZ`fs γ`]B `s!2uۅ6Xi U0gGl=h_uXV7KkSg8Pȥ0 "I"e4ǮM)0n{-/㽖QU_{:.w'x *$C8Bz{xq-̸"k)MJ\Kiu=q5X/cPJy=rnc V08}޴nt/CFS\m1, K-/k.0hТR' LW{<7moИ+ƛO<<׭AѠAT~a@[קhzk* 0`ωqSWޘu8Ex챾΂G~/)PghZI2g1*{:NBUjmz=y^%=W֝q_{tT}7ˌ^=և)٪y[cX<|&^|Ao兮u@DDDDC@DDDDTxS.rݯnEBl_I˷,xl$F$wuN 31o`Voj]aяޠxƹsu#֬k[%{.XMz4jqYr""""1:Ǐ xx_&;҅sG mhݶfX6=E{ٷd(EsWK3J~*vR݊Et_c)e5He/`cVm+5 .Pl(u{Ucs ؟Kn/t"cUƂQ3x̍L}VTUq´΋XՁ* g=؇?^{Bqd!0?!0+j@u B$m߇ށWVﴣm9:W7dЕ칠QQ_gGmV Z Tjb<9DDr :}c+~|T']`E nوmŽ[7c cb6LK -U:D&8q-I}I4U֬Q${ͣ۲ !RR Ac,9Zfks+T/DuRtwBt8з?mW>swGYUuݫ}CkApʐzv 7 :QaXav~vx_g-j&  a00XQd0J)/~v|:3`밶ō׭D}]Uɟu6hԜLTLj.~U|UkÞo(|x;c ׭AcGLvY*Azq\h޲O1.Z[ٜ# }1Y%Mv\egsU jEnKȐ*fEXݏxV~1=$2LsqmΎufWA;^XvGz1 FYtV4W_!`|{O?/-5/Q|ٚ~+hsrh<~$銹F,h``0@`U""""X <V{@`Zނ-M0i6f4HI& G#8B&Ჿ8ch6CVCӎ׷`JwIWsi誆/>JZT""RF=0N}>]GCܑ;~w7ݺmDww]t^U c!t_6F &ø&S}CJ F'X!~QUpiWTᨠjĄgc6tN;?b<z 1.Cʵ;vN w[0 uNc;Dpnwӎ=uQPO>[<6oŎ8;1;s;VT5g`pd[Vfv@DDDD3J=kxã+[q.H&Na GS`$wb ZQw_%0J!/RSUsv*p:>f O ?$"*M+}=E>ffUtG+%ʢf`݉oIBсNfUkRZ1 irمK" GI^XK-s_N̮W)3H̏sgӕ7?[g/~z_5j ~l*<vSRPfcV#zUKfAnǪDDDD$Q 8f۽w9"9~zZj{֢h⠔ !Mę`1p eM/fСf[й,9L*>-%:(\M΁ "*Z}CkIͫ__=]V}֪ O {O>%k?[[QEFEمᨸR+GH'T .b 8TB|*6}vĎmh߉={E F+ PTl0],êDDDDk?""""&ž:OA IDAT8"9q<]loC[8 2\d kA8hLuytX2W͡YVcQvଶ,q, 0]@8U*P% SA͝ODDTx>F8(c8_v^Q}oA3DᨀKс6|ը1UYB+Ga^JLcGzIWw'0T-% [xؽ釋:D듢`Ŝ{Uw0ͰZP Uu8Ρ """""""'9XV޺6>|5XUU$a8H$rφ#~<3pū2z!a1Qçqnܭ&=]54< KT6 S?<(h4SUx\}T^zt|#xĘ\}0ouᔤ~qY`k*11:Uk3<(4Yg兴DH-%tTJ-RKņ-ؽkf2Q9NWxlвtlXՁ$; """"*0x ?:߃VKWT$Nh#;WcH4 :LsYmhwգ/n5T&SC̈́׷su#L̪/\At:Gt:*ZdJIK7L?yBKnК9@U2'%zN 򍃾 c1=CT:K>\?RUg6균wRTr Xt֢>~K ?I? :wcGNBFaa28wbU""""Zu1Qa]ƆQo4c)e\Ś/@Kbgn :̴EtV.su-X.۱gUҗN8]vY I@o$J΍P9DD΀hc]8"DwI'й;vOoU#ٽ+&#~'O W4 < Q3i:2YN<EVW۲lrСP< 3nS﬌ֿ]h̆Uhc MU{xj[9"9lcxAA$  ʠբq6,kjF*J\݈ՍhipCyH&h2Y ˅82@UzP!`5"qO=:G8 <8Ny*ZLz<͇᪝Ce嘬6,toFsOj*Fq TRPuᔤnm|Hc^ÛuV4y!kH (!"2h#UgD|􎟛f^o\# [Z()Tib#. Ծm/j{-DbPXkt⛟Ohu'}Eis(༿&Ò*t޽Y~K^)*^QP*ʤOO Q6x`b8ҋ.8;[36oGZ9,7PCUkQg*^ue& -PŦNE ?\\[~669hst9FD[C/ "BE 8DDDDD3޷sߌjOV{bhD(B<-\\VSᄦZYG@AKL]q_*]6 $D0pՊ1\jLHs~PrU!4DDT&6X}+tas{>c}軲pfϬ΢+:A LjEmm|H~-XhZ2n0yyMhP]]D"`0tG-5MU50hu`ՆjiQu.56o=Y鮘paL$V_L&WQ+W*wLgL@PYՃh)snVr w*QŤG=h_U{pp,YT_V;:g*t) .zrO/Do&*WrH2x Ύp2xg\LD_>9HXRLP %n)&^4U=T"p }'Ew@$cg24;\UhvZ0Q(r8*p:D"D"f2gj&WM6`jĪ6`S`Cy4htUj2.+Z2RVs N\8vS |uoFC:4*i?+9`5ҐzY?>| [QlB|&KYnyJU;Pgv޾+5u]2I5&,(ʱ y ;L %{Y_-JW;qi/Gy^5:(RCL6~.2ԙ]3BCmuw^co`dF{K7[{02) Lѱ zVR׷yu ~x ͂Uį* blF(B,〔Y 4r_@v֮7xĠͨCe%U﫳YVy N*d@6zZX/x  9^hJpJ`$u [G4^pb}4_hstOXrkNhZ2o;=IY %'*GSႌUoƇ~>z=A0>Qp*+ _ ܖeECc]+_EC>H4>Sy$ 3Bq;Z "0CE!rQÚ}\]gF|:ב0 """"Z&""""`Srݯxp JшH$D"AQbPpYm0ju[3 1!}ԠFFUVXnCŒN3@DyLtXf*d.ZG$gI7z$-DءSi⢄MIb7T{LQۏ&â' #I)R1ٹ:h}}3!Z <4ƒ"2^4UB_]L>ҋǮt*X!}NB-Zuo?zHjF :b$GSBKCv"q*sv^AxCADDDD?~MBDDDDp@[zzb1B!dY< Z- /Zfqq+5VP2?1 n&fNzC.A)}[Z gTz/w'meI`lrtxWDoW# "+ӗJA}xT!xEC. &cJFnm?˕_(RAő^|%Өu+x0/]7Y8f[)*(O8/s`:"žjld2h4""W}m3C 7?c)a9ϨaVQ_`euUpV[xt}Ȥǂʅ$]2@|0z$Icq$ 'r-:-L ~I: c4Y0owqDBURkN sفK"e(9TBhs?F0\})cUkQk,?֦b\EHDŜ+P qw{O} Fdߦe-@LL W#>}޿~%jo708~\ ઀5+҃ ^hzOre9"W8H{x"9n.{TqT>R`X`4 :+Mf˫j`VSЩ5p[m Pe4h-"m)56U䊾&jV\%xHgx җH;ܼf$g衬 9|A8bHzzVFzJ,Oݦ!*QA_y'j%Z Κs)痒#Ȓ6l_l'w}:v,hTx,: uiLh5cCtN{;{G|u&{K6o_-Ǿ3Oڿm"#!jE>|~>*>?yט?^ OD"onahqx """"ZA7#[i4 H ўVf kuyF \L U"'U[rظqȔ@C"*L,J:g/s ux. v۵0t0 <%댴jG6>TMdM1ڮYgunVw/`?&GzVeLL,O_붇<`kf8/1w[J?ЫY懱s2O&\]"B';wbG_?jF{q[=Գ89"y_l];gxGι@$g/vӂ x8DDDDD2w-]S?vE.Q^^xh*{51ÍPʒFFUV#dU U&NJ%UI,(L c;BL,&=Z-:I*.Z-g&$})(dASsjN 汒xsUm+4.rx4ڇ Oʾ VY,vC5~8p?x2[h}JC0>!)<攧Yk}.b)\tifٮ11aáy?grgo8_= ߈FyY|7"+-!{Aǡ """"DDDDDA^s#rkxc>[ LN@PYhtpB-IL-6 B%}龏$k"ml.L7WPJ:[־jչ5Wzܜ;6|sƋ7vR_\QcH WPGJmΎg(uu}$> gnv zo^=,k% |Dnֵyϥ8)vngan^(DDDDD`n!9Ar6~tx}M;!> S+u48વ "%+VhYm9 Lec$ƂQ! € G !D,AtGtՎ+Jx~IxW2>fg|'|\zhǮϯRlKLJy߶Q%1Rjub'psDtR v} 1^laJ7:W7KgWFݎz>X=A8Ρ """"RDDDDD4+AXGcvqM;xKɿ?bءBLJXloՈ{j2tJL[ҙ #w±<7nVuF&"۬sp 9R`Zp8`2 AêDDDDDWDDDDD4'A< lȕeY]{NO!a,V8d벺*4] g/ %qU<DDTzI3%q_ñƂQ 008>|xmf rz/D1,X r3]d{';Ƽ'?X1ը NJ WƗ<8~EV;ڱ{xWJ?F' #H a5[J24cg.d d،SJ1ͨ?`c$~|4&2Lp8X,P\/|Km pC@DDDDD 5};9"wL~ܻj x'DiY(h2誾 Xl*5",lM6 d3@&dg&jG "*L:wYKz\"bAIIAW6*Gquht6o߽ܿo9WKʪʶ`" #1m;MWw3ypKչ Wjc\"zJVxh*\j`kƿ~_w>ݛ~Xhstz$%*Wx['[3vuO[t72̽N8?=¢ֵVѳz=, z={Ӭ@DDDDTx <Q^>v_"ʝ1|Zj2MTt6 /$qj油v3i@}Pk*sl3i d삷dnBQ!@:{Z,Vh8c c %8Nyp#^!2b9-U4Qlۥ*C6ffE1:&P8ݛ~(~<Ӫ1:eJG#niyZSVvā/`($78 sD*owԳVsu?0 4 , L& "s(3HA +LϾ}xT*v7B$i`Xt^2mC(* U- *>',c7܃ލw$y-z H2A&AxȕuYLRx?.fM:?`UqZ%}-hipd ERUVtjj⬌L$_q(SAt%iNfN,Uo8DR'8_rc$a¹lc\5!%zXKn_ RVVݰp5:[v73롫7$Jy-5%XXj4F)]iklm|H~(uLvMmpqo^}Ur{^3œxUۮnkp5;DSa>y) A&ADDDDTx """"E јx<]Ͼ+[~7 Yw@R+%y4j5E}]4j~@9jMq dS+|+ $Z?*PFC&$& ➨ =*`x"K?GSo`p__7{J횫Vmߒ&8ɶxf'8L yz5.o?<ELLX"V;E>*ᚢ:o&d~~ʧb̴*?>wjP3G5UZ-Ո!O,k """" DDDDD$++LsEB16C:X4ʁ0KPf1 :ܻUVHwY|.j2%LY$QbFչ?TXbQ?{BXc,(SWw?{2Ἐ&[h} 6}U޷׳}47R̥YBW{$'`ڼ>K`ȃ#}ͲŠ5*vnRk١%҃Q_7sv)U'NX/.A#""""*AZd%Gm#28^LS 2JHIm;ۖu4<4+*z(Ⱦ 3vtn)NTkV4v ,٬p$@1D2 |xmXtŇr Ͽrd:+_=']$ >+OEL,g[3z%8p1ۜ+R!Tx`Շ?SŻ4F <ӏӑ^\ -΢}XUtKƴG8%j {K1 p:l :, <-<0@DDDDD#6\hC" :~vnD2s>oh-Qk7u4<$N ɹrQp_Y ITlz5RErȰCq$%x'@ k÷T{;\-0U{4ؚ{eʿɮݢno?CϬBYJ^ R'n'xJ$ \)Z.TwNfEEJ[h/1?\yGMM s|Ku p`Ax@3 ~u}2ذCFEj4JcLʕF#\*79`79\rpFMF gՀVU5dҫ;Lrx:"Ok F6w.>7ؚy6b<[>.T$~ғ#wVIeHeDh{9܍/}/֧0M*<&ÊO%A䜷ij=9K97W_>aRQktM yDW:NfE*1 WKjk[ܸ00wpGsf3V+;\U Q"""""*/`+GenL>tamM&&c -^3E/vH$"V| c608W龏%dݶ,AWw?y|+>Pɑ;sa@99p1&iE&{ sv8*h/zns&#MFpiFs?no%'+ͱf5dG*3{nƎ<ً+AY0;TJ6ߙIQ 5OY TktB$+sW9}z<|iL&X,h4|E^9DDDDDdv? `&4؇#Å{j9( L5;}mKG\QvHK;8HzTfm(UW yTZdRJWw?O}BV>dd;x(~q좷ތgY/f[Q=)Є#1ɛ4]q B޼nJܱo+7Q3EoP<$èj|5R$OPٚ>(~5xf8Pt%pFҿԮY3JgPk7no8` \?*G} """"A^s]Nqa|wMRxoȋ!/⩔l}pC+ֶrvfd\%T3i 2\e٨BZ'(<+VB!%?>|P}#x~ð <TT%_To}O>UءcxwzS%?mu<{o=}'qo?vT_v+K  IleT&= OND M֊ T:1.HXtֲ/W~!o$wz555U0Q]_Uylh4FMM z=O^] ;- <QI#£,F4|3J&8 "LRroE'>:W7K/}t;lTPbL&JN񘴱J%$CBUP+\±8~|C+>]^9ƃCPk| Hr7EmJIPgv@D>c;0*񁇋o<=tF 'Gr%<p`ȃnI:ݛ{*\Gy4 v;N' VRA&9݈,p@`*nDKp as#cY+9ܮaAWc"R~x!dҋXBz#'.w <>4x\P^W\vcs{36"YIA q)H/]T{@'G0  M͹EgFP vY0!b+ȥ֌gom}'g/vuV_v}59\ 'wڦҡ`b5^Tmu89")+ ^nrf$_//?* Vf'8&vS}Ç1[UU}KVT XAB_oY[]_g/3ied}).>ps@2H`Y =Un$>>~xjEd80uTyPZ*5vӼk,x0@ddQWw?|#A v:Se^#RQTQx%`N\KkJ>exz3-qj+A !- I㷣~l~3" IW}Ñ ߾jl&|+[v>YѷP}(F Ph J +T Op-,ô~86y56/ifjfnvU  9hсg`}50,x'o^=,pԏnFEO1Ɉ=)jD_{1ŎBWKpqWF9bRQ>I9gUΉsˠ1*)GKjd 6,r=q6Ʉ:X,-Av """"i <QE|ۃ 8{~K}RϚn|?nC}]W%1-}(%ThL@&*#*U:kճ̏P!{!F͊H]x!+Ĕ,}Ԛ+-:FY;[AΠjfܻn uu408J{cYgn˲%yF4>PQyGDݛՎvQm}'q"d)UJ<e+iis+~Yc \nKvŪdhڂR\'sW&ڎdngAAǡ """"[Sr0`~>pT6|08O9\l70!70VPlzzmqX<@Z+sˑZ=DLfIDbuuW)tmúkд͍ A%mTW@t: 6 wUȐ2hKM_F}]U᪵fu8%zZLEo_p=Y=)=,MVgvȂDQZЧD9зO>%{_Vő^U-1sRR[?Xj HRi2kS 7LX,hؗKv pCADDDDDa* m:8* '8!\oF%|C"!Ax'ᏄJr;W7-ka2J_UVcICQ@l(*$7%kWGm/?`_ﲏy}k5j5V;p7ЃZ3ͺ ՖV]4hipb,o8tաQImpchs-Ko^}PesP IDAT  y op}kؽ釒&K dXTفsw)C¢Y!+\! t+X@[ yp%&ϓ U mh㝧r^ngAq(h> <QE8uniUBaf/q[́GE}]f&棪}(!?>w8yK}l߂۰~+jwqGN(&#{YEC HeWѾ];Yǖ]_5gYgA4u콁+T&,rMϏ %:%VC)8yq$[-yDZAԸK%IW&J[lpv3@X n1EkVT9nMm=twxDߞ}Z*o[cVnkG-jQÍJ2-T" J >~gqk'bDžY1]Qoa~G3, %EIJx cLZ3 ۦԪ.O}x1ϯչ9tKB]ت"""""{W<)0Uwj1Ҕ;RR1$~P]6(=jUlJ$$/+3BB9hV5̏ÊtfݻQ`?F=F=put@/HQG2F=g͗:p(kUJ?^\[~DpceEadb~p#XIYowOѼgqT?Iƺ(zaJ=${ ,d?>V.%y=ז/:P8Ζ~>#v52%J\¡W"jv[{5 |}]Sz;0X """"""""""2AnP!U7q|Bۅ`WC mN8;qSNv̚aYa6Fj Pnn""^)7!*O 6fN=t#;/-Wd;' gm(l.?]b{Z|'-jkӪaka}az,I x/e ;hTXz!bakݖ]DC{-^(4=gOawXwsAGU-D[$d=6|],6^Y[wg(vaY RjUFTT%aK65V`kV`khӊHm4iX:ͣ@P&\)`5lTY|s;rIi t`eruz| Z*P<:glfO0:+LZ+o,&0UX >LcGv cCoõ`&4W%/ܹE-B$>l"ﱰ}v;rm.m}vJPhJcd wfm":^_'T9*nw">Z._){mK?jUd(Q|n11 ]BA,)ow]ʛ23vVdU.ڃ=]zq?(?^ C1 :B~ @MH(V48PsM-8݈v4Ku%{EXvUD^{m}*OVλ'+Ya+mu3x/žgt(\Ƚ0g)rS&awJͣSojk8s΢xMClG)=My)y8R9+qkT c._E&S0”zjf>aiaKADDDDDJb |#^9VVe%p3ʆxD>QTB`WhoBֆj!':bCMC *O7t#*4FͼgZwDIhऄ`FEpͣ ?fdz!w\[>6?)^Eױ~A@]g12F9]6X1} 3pB_afL+.@Y@-_m vB6Ap30 돯>1[(HqpBOh4Ɖ͂V rx """"" A vjeEĉtądې=*i#C.p#96{$$5hu@0tRju|3/7;EӍ<݈s8{%ug'C =WӅ7.Q9XyN1U(;כf,M`Gw'xCKᠬ` K2"mpP@bWB•Ѓ۝.J uw P|g%-܍V'˱򗫰joߑ4vZ^88RyQҸŶpͣkI}e'cX~1sIEo;"Sé ŷӁ`wԪzw˦C\AmDw Qg> ߃a}Șv0E8^;NlNI#]e g)h/DDDDDD"]ysgnj87 >,f11QYKH v{PBA5$lvrCod >,˒;LBQσ(I~fZ"8/!6}r\[>َsο:_WwW\{]2B[{9ǬcaҚ͢Q!ؗoð(|q]:^Ty0Cʐj=n|J<*P , 6$#[|H6pǭv~H%L#Ͱ%Y8ք>A;p?$$>Tj@hpzt '5 U*d&#ms7qՀV`0+csZ-Dv· ɰõJؿ+JN}yIϙ:< x0ٵUWmSщKV=d^QZZ^ F ^ ^˃(^>wWgADp]}'Nb/WIE&C5 -&)-%{6I0g)2c%cj5bw>͘)# 0~rm7. e!Bl=]8;rS&\(5g⁇K-HOE4Ն!c5ȘxQϠl,cЁbDDDDDD L 2Yz[#gd:Nk0Pر(Z4cJf!L=%e~Gy9.i2e!\K*4ȶlHҸ);f[G<$k5c,l/@iA 0 /_ >U*_{:1Vh`(4cr=Rbh-p5y /W jկz o DD_tFR =,]kֽ+yNF$DqB16e;&i܂%a-,5i1V-ǑRYkA{9@Pvyt7l;gZ¤5|4D?.+%1Gҫ w :ʉPՂ DDDDDx """""+o.`!,@'k8Y9i4Cwi5W;,BD$"">lUט7!Yӱsww^Xrص;vR[yG_4E՛@@8Q/Q=&7e۱ sա ѿk  {)y(ݲzL)>P..wܜ7~MyWP|}Nx؅S_p?ܘ2k^viT EO=~s~; ;- ,c؁;< +o<- ,eU;Py*L>3m, H5V?^[fF:VF();҃WHZxDZ߾kp"=Óx1Gu9˚-v=fL1/,蓣~?QIߤ5cħ{Uo5@P&K.Ы 1>ZVvBʕmB 2cn-Ea7*G4J$Wͣ$uq׽o-ӏ.utRQa"""""(!B ܇TMMp< BDDWUn//xw*|uK/./W hW;'Pa<0wV_ߴ,̚Ã(Ui=FB<f=$Q[Sx֌V)vLX،}q3ٶ!^: ܬ]RZ9AE6EH*ަ}U4* FQ3 IDATEqOXXȰ qM(|f'Ztl`wX-鶐:< /}<[K|,|7i͘'Gw]twwpڰr{JHw4//-e&Bs;KsCC_])(v 7 ^+kMvJuwxӿŏ,Q!}h(`(J|cEpz_W (DDCȦ]x`emm[ N'iՒ/>/5cn6Goϛ#oBuzCQσ(4D"rL;MDB_Q#v {}o:$tV7 &E"^B't8'׾UBY%0}W[ kw%[-j,ṓ?.4P娀Gbu9;Yka}?yO"vLX،&Oޢ}IC+OO;.IMK9 !u}7qnLW4NC}IO>,qϾ|s>>ay8 KBh(c"""""%jݾsW>Jxs?6T+9fQbH4wu>.p:"8]eJÆ߬pСG}.% Ζ>8Q%K' u8oL4txdZʺ bT32f5_ma#Ӓ:Wq%{0giX P9=.EI)sxdq}0x]6v7nSe:9wrۧ~C|%_|~BD墵CfF:tvޜђgxV{UXxQXsY0kjn"()LZ KjCTfkuqs,P|Q-> t !a@aﰟi 1UB|wyHY%f̭QiŪH7/[XȰDDDDDDQ`A/>SpyX!e;@  }r{}(|G]b$W2j:wcO E͑h6կM%J-Y_tPANDU:#zLn$Y!vH˸͘9KuۥvuQOF]rm<|q1y$QgAmkhTژ=$\$ng 8X*;^6y6}%gg_>M[AXƠѷ1@DDDDDGn> ү`/| ">|п/~_( AU5:<0wj:gx˱fݻ?m#bQyKYx2#cTon;"##ew)!i۔ ;HOʼ1`q&m"g9M:~(^<ՆqGZ* v3,}|>4Һ4XtCQsm5kʣ`{FumС """""Ꝇ% """""?Wn_ RVE*@yܖiCc|E!3/  >VCv^_k qBNZU߇eӮrl]PUEH0n@Ӆ/B*Q{աGӅ<,]Hc"jˢWiDj̚ËEDF26ka[FlTnFaz8 vY;Șbϵ+l¬vsz\ ͞^"-Pqr1_ A mi= ;^kC9A3֌)2vw8ͥQ ]B"""""8vMev >:cج&<4}<͢Ł`%@uw ??뺠C*M:$ iq{}(\gy}z>Og~VjHAjء'/lدc=>&3#j8CI5O= ~wƿ h"EDs=&;S6zS-sAUz%^:<;7{zI erS>oEwEw=]5&L̈c1#Lǘ7,V=n}^Jpu?ȏA"""""" x """""|P?~+1{j69&!A.q.G'ïOw7N8ݝNl0 F}ЊӍ(|#1* En'NtxɄ ?|cyⱈGc%ʎ|'Dxxd^zP)IHB '!6c0؋otD֬"[Mbޞ_zXxRusmxH.xnY!\[K:|N`/ruHj5ƅs=%n@~ :"""""!x||X5>,CÖlbabDwPCX =A::Eun9:Vɉf1BR]}̦]ش\5^:coH ;% NY^Sm}JqK Iw!z}e"5 EF\p 86fT )IeCvz ƥAS|w) xcM9;Xtɢ #LvmW;L`,:RZ_9K׭<~}Oߥ(0^vShEÍ%.w$]Z}(vHs):+A(谟 """"""""""!e<q[ s;rF(DQeSf`TjutDgG,qى N hۋ3?גŋc'pQ5jI^6o,:`HWIy`l`o.4Թ>Ŧ]hjm4 -8{+k~.uxƥ!\e DG@| +˚5rT=NQe,.u &m"fdEϳ(.a@.͞#LvdZQ5?S4mCrRNTzn8g+ߐ4nFQ?B/}Z :Ɉ"""""!e}]uج&<4}< AEpY~@m.R K.y4:}]w]k%ݾo6ocO/5ל}f[}yseΟ7̝EeTtv=RG@݂iyYN Z{ܢ]op~clTdZʺ IJy-}vypZoZtWf&ז/:P娈rXtqqΈ^;E˴d#ז?~\a7.xx`*A""""""0@DDDDDD >(`X{GcΝcaK60DQeKv(K݉K8ݝQoOGejIŠDïe<ķH ;,aTK/}篠쐨N_:sL C4]y^.p;Í{es~7Z-ȟ8[_v^|?y)w*ر~3Xzde'W_=]ZQM-p{}8{E|Ó:< ?. )Iu kxaNtx Mpq| ? _0zU-6&ɾ&Z0Cծ sJ <cFƜEF<Ն>LX،pxD[RK (﫿\WѩV̹s,͢ H/:=$$D .{xk[.0nDw>.l.o}ѧc-l6'F̟fwcϝ='}GR558ڎӍiptPC:`6=ʆqiqiVh;VHF1U-$/BF._Tl׆ uȿ/]<`xacByVamz:hɵ|q K&>6wuw^m7\ bxZ]vDoaϛKg\x*A"""""b79_J̞y%X8v2p^~ @ HR*5k ݗ#\A<8ݝsى ڽ͙7!*Ԟo^\޻7H ;_`W,t>jIyxHZ篈v3'^*O7C׏3+אi@DcF/jpD!Ӓ-ias4w3ؠQiJ@RTK6`'<"4.j7SF#:6yJvyaΓ{ fPHҸcAϕn%h9x*jADDDDDDy||X5>,ƽ^$&Pi& uGAe@Kp5{ls>ǰ("jՒk_3 6h6Mc;̛5}EXUo5 `ށs{}8XYp6Ĵ,MCA--6v| ?YSsO$T9*x^]9iVd[ Xth -εc|J>NIX_`mh.菔?^ OCԼ%{Dxn؃$2V1fr#M-|(偦"::ԲDDDDDD q<TT+9w䌄ɠeabTwCĸ+!.@PA8 ڎV'Czl]VL ?qB_;1_5EѰ13 咺<f!TnDj=R!Ԩ[^l?. neSaЂ[b̨Kì9;6$uQ R)w uDR`aRAovvbvEA6@6컫d{._\,ÒO).ߐePPC5FzxP*A"""""(KAVVF^盜X/a*јsXؒM,L n/Շ$nׇvcxQlEz˛Eł<-{`,_:^.6pkkI?wvTrKvQV; F=/fQ~*>\GSk{\k;:L˻J!Kq /8{g/|> 6 \M$vhT|+A-(Dϵ32f< v^mԢwa{D&9( `AF iT~ěz\BBf3 j5:y@*A"""""(IvM!sW>|kխ#1gXBh3253$9ݝp"ag~VmN'D_XRwyvU?/]S*"a' LvWal'W_5Z̚ËCjjm}(>R {^1-/ 涃C>Ó7. ٣RnCvzJ:x j9a$2Oo] L'ЁD`&M}~}&@yK`qچbzI"PP|n.SSNPy :E!=NDDDDDDa @n_ _.⫿\j٘ AD7έK`9bc;[]4EݽVFK$_:ϛ'ВQW j?-v3\S`GC>fQyL,Dx.Mظ`e-V^l޴kAqH5iDÒbTr}0 7bDžacz,ѝ32~ͭ-qxȢѵbL|Jtx1UܱZNZ]`0`2"&3&""""""09/>K9#1{Xd إ\ءG@BHOxgyr_B 8q_okY-ItOc6oux|<2OEص,;*t,YHT-*w{}aǠCl0fT /אN65m:J7עGK^g_J=o jƓE|],EP&)8o3׉ vpäMZШ4)#xpxPCބ\|c Ku IDAT>Sy {zϏ? s"v҃ou%6lс z`ؾqv͒ -x}ش}F=Z%/7ix`5ݭKP8R_?w9˚S-Qz: meɎc`[Fչj sCkϯO59y"J5,"ܣQib> $Ƶz=L&t:41@DDDDDcx """"""\yp5v}B0΁+qoh̹s,l&&P! RR%$>.Gh .]v,X?!jJv,,A茈JD12ldfAԸx$G2DjjmǦؾCjJy2-;W{c׀Ǥ'eaaNp @ہÍ%h"/.~Jlc^#o'۷%m=߶ꍘb/`h6GAoivybL|Jm;A%2kuxK$&&`0@/QRDDDDDD l! #Xx|z^0ȢD@+T`$!P`w7pa~M&{aK/4Pq$eQs>>gDk<ÌX?w1+ky!^xc'^{oaiw9~3cMZ3%0چPb)َG[۰ g t֘I1XqJEoJۖJͣ.\'( ,Lyy& e]nXn,Q[Dr¿\KlfAyu!YBbo]ADDDDDD%v0UQWr6 Lv}PH@ˡ$F}p-vwe} ;dgeg[`{WDUXxMSq$NW\x^zh4vqxXEX]Vô[xAMʱiwylO$[uW=޴ñ-GʼniOq~-1}@(݁s;bC}Pӥi }2H~ 6:k2c6J sֹj sh4aӵ4iQiԓ0i:D-G_!7eҀ;Mjq\OȨPxoDDDDDDx """""A!G>3E9|X5>,ƽicad v-/JP:opn.x}ۦCê"lzsx㭖$+ڜ.<&_gtwwc-YV'ǒtdeG1?q23QW/nXF>N75 ʭ&}73MC$dNĤڮ :HF;ނc-Y#cn32 a&~ zykg[MЫ 0k{҉?ƿwœ+M]2ZV1^bQ*Alqqc/Γx( ܁vDQ`DDDDDD""""""T 0JaTǁYM=52a2p|q :ש7]8hjiGs[|/303O]k'7VK>ݱ'/b 6@hjFAuʼn4sĚ?w.y!SP:X-:̼' 3awSya=t=ߎ]GtrdZ1doMu[vgT֧Q\[) @ʱ謽2c g UK;^|~1s݀f,_׆;ǩʫk;z#UoijW+Ro70e=@ʅ2EE{R@ݡ=BO8A9%utR(*\y3rn/D s8=}q8>;8CTP(Kf @{%OP ,^t]W_ ?-x1aG^ʓ[7! ȟ fzD<9~p -p{}H4yAQMg/(\VV̼7 ]֠ެIXIXέ-Z|ZThi;`7GKOk!u3ahwsp4dŎ ~mޘ(9o߱M(:.:KAbDpF!YMEl{6\m,o6:ۮ=[w%;r]ڲSm v_j tNar':6tf`1bZ s]s#sݼk.KT*v(-ǜZ*&;WH*DEF5F\&:<ߪͼAWw/GG ;hSԟsU(ؚaoU9:PĺaZ.Vb*)Q~-32æ/qங`Z,Ѭwٻ6Īk%?ƻ=ZM˫RgbSVKo+EeM ^Ǯ=}7yVo0?)^5Ps):*%w h{imFzZ7$GK˗vX[z݊G7b}GK =P{8-rY]b*>,UA( EX,3,1l]z@4Kicn:V94h3nKCh(08 _ |=x2む Bԁ^UFToti;3^?a߭oEUm7746L Fi_+] <;/ <60=^z\jXĩ: xLJa:܆}w{$ʱWܹ #8͝| ?[f 6-|Y6QÆGdH<;bn;:97(C݂=Ʒ%[/+PpwDDDDDD4DDDDDD%Bs"ne_`ߊۇpeLcZx^P#(¿/L|p_o}pԥu"{o~N{NJFB7962䀼tyh=˧J݋=젊  C;w3[GjuWk ر=|8mVM_``j~R~I_ڄ{OwS׾=1XLJٽ1:љ`yJhcReocG66 L`?ںG19Ze+$';SFV-G\ ^q--5iȯcB!DDDDDDD4"""""" xBJ!*>YD 9_}Z\vm`\6}}S1Pg.aق}Sκf:6g M:.ίAeȁ90{ތ݋u[j[{*?^敎 Vֹs^ =qī5{ۻ; qfouUz2?Iz#7E]~#e<}\V{ρ.u)q#wRFa]ζgG tqa^~Z6b*[Lmv+*Db+eS#z-FtXZ4{ BJX""""""$NDDDDDDp>|*[EX"qq|!(מŰCwFO׸/pۊ{` @8{~טFfސojǝ| ϸ/ MG^υnRǏfk4&}^xl =jBu&ZrNVW&kcҤs#FApPS* +f'g%>4wcI٘to/4g&ˣ>)| ZTO6>t7n-ngw.""""""u&!D"]w5{G"7c[%vwjn=raK<v_/!}*'H:΁l1X]ݽ)d3=.ofA׎cc̖>^[z{1υ+ڻ[A]G{1Y+eoO~?VeS=a|xS- 2'C Lc)Cg %_'ү )BA@ 82ٰ0Sď?e-EQN6!A!P{lK[y?4Ó[:DDDDDD$DDDDDD4!*h4yV~P2|~Wcn,mbP;7U|Մ^:63KXÜ Ҁg_*pzܰENMWDc+GSs Zjٳ|͖MJkN7)ErAt7zVO~Lb\4 CDGb[ĐOݺOXzf@^S*]YӁ{x*D9J1mVVLƣL)9ZZJq}OY uUcԵW%p9ud}=>?Wrl-zͧUuvj>hUiX1~<tm}ژ4LW g{=vއ ]LIV{jڎ`k<yw$)kuU^GO=9]6`X=jй7)`4[Pֲoz'u@>L,CK@DDDDDDc$BUv `ߊۇp! #$m8}hl{s6 ?sݲǧx/|ebg;ܰ`lظfKݼ8w,UuW(çՅY/dCz+A9ieHKUڲYߟǏe?K+c؁܉""""""Bs"/wj}7U<3&+1ZgOT*Oy|nAo0:5~5ӱsem~a䱪hSC?RqB!VmnQ`9<(SĪқtΒ֭a/B7)у.!$KGb٭&zM_``߯CC^_͒e=#`w"\=ahcR?~MjY:,z.: N/|I`h1rANcx#wXl `B'(`9x """"""!Db`fU|kǁNjb3EYv&wPp aj;gSǫojw) xo?̩w#{?9A|VĪbbhetG7r{WwՏϒ.rܷ9P8M1cFGcqBȈ0 ߹$S=6?$+fkE]GǞ&1XK_3F)j'ehqNpm]?FehǬ Vw.XJZ>bƅ^6;eڕ6^?C%KǴq7bŇxK<1cA!K%, yDDDDDDDW 0!ȪNن}+^`xH8a |3;tG]Z}Dd݅O}isw6%٥Ci'(;xhnX0~֯W164/#[ІIޓ_[rg4QńeXuTGݤhT~lN,yCaՏqtJKop!`w,\-6ݾNoLwFTX 25YnubBDc4];QXcn6y83$!8(QN;;EzVTrգwǯC_j:cدGv[m<|nZ45y8ica}""""""!LB!Bo5{GM㩷H}_q@hwtg<@H[Dp0ϻY]xr| :Py:ea6K>q˳NUDc綗\lwmO:ȝxU ?7tx>_N:ٌWUëGɞbxWݤh)Z.Vb*& cF<&`/]5`2!\;ܹ8W7;"2BfQQ6=?)?7LY&k܎m._W6/uH7]3sijkד]de;xi S9bו(!k3zbsi:-yK>W;kwd=mU鯏8g2BBB.FϜ@G,yCawwC7&Ͽ~U.gVZ\yٯ%Eřx~A> 6C޵FoτyNB鵉rh>%#WӪ0->;$^_smoux A\geL~_Sen>vC•χ1@UB!1!wN}i7qjߟR6l<2h(;&PNLx>XMIz/z,d[*k:0];$E#E.sYq^kVl8*DmVԵWyRյWa~v̚?^RָbԵWɿvȉǮ1ǷwbKCMQ򰽾Щe+oH;cTF(pmÛ^ĝވ-K ;3LBb؁DDDDDDD.BB[*ne_`ߊ?ç.@Yl8.@R w<?Vg.D _ƍs;$$4s cÃ_LŮsv]{ ȹ=]p_-BTbeW/[)]ڞn_=!gB]C wS}͕IÝK1]ԓ/g[0FRGYewT]RށلGuyJD9/bƅ^6;e6{02p9M{meR[#&'u,p"V'0$DDDDDD/x """"""r!.!D x*XC`ߊۇp; CttwV!mxo'jS?\7q$M"r.Hd YL}S[;b5aT1(xG^Wуb-}.+W?Fjs"x#2kf[szIYc.SM(j{}!~= ;^/%˿'˃ѢwCxs2(]QXwY;-p^L(Tcef <\OI͹ _맟jX(-(`9DDDDDDDu)x4M,Uв2|~xpCs'0Z'%XÜK>ew3qiK?AC#.̈́@dž u dէ^dKʘ?!$K>vlCC'L^Tt*k:2~},04tbe}~WKub\46=p3=.ݮ]e0X(IcQQǺ"bHx+T@r: (bwU:r``AgE(m(,ށEHPJ;<$DhiYBJÊe֣ObЪ.tyS=I/wc~RńJvoۿ(Dv"\ODž{Fs9[s`E?c(0@DDDDDD!B@h4K>d2a(TbnMObgTc^OzT+|w`<{AE_/LbB%@x4|& s] 83C CTwdFO}Ϙ5Īۧ~juB'~eMMV-`HɽXwhUi1Xx4젍Ituf$ nN%݀O$x]Q{g-O_>$yn֣>kW{B0><mVNY)kŵRFAXx$ŲN :*ZNEp8z{ SuIg gw nyS_4u^6$$8d|l*bO)! [CDDDDDDB]vi4UTz8P݀ P*Bq}$\6.9vY~~DEF^z ;;XmJ2?nhl»G\n/. fhJsdhlq3ԉO7uH^_LEnR4tKo]qn50K8>7{f^CtL׫HĒed]q#L"9 u¶3,}& rP(v|bGrJ!|!D%KADDDDDD"""""""/Bh4yee׎zCRbnE(Bgx,yL6YS7g[:l{c}CU W(b!oqI׈GI{ґf7sFv"zȄ/wwuސ_q2tr'Ȫ|Jr'Xz̀Vyo>jlwQ#&,>uPeŘEh4b:ܨ~4΀:"҂&m6ҹ BRy!@]G w %"""""" XA, !LB!B8G~lCa="Z\ cbF5Y$YUC#a|ܲd[wF\fK.89i¨}I-=\X|Z 3,K_!{[ntYޯArtehVyHV,S`̼g=J\) ,JY=?Rp͂T*ZhS)[:!D>DDDDDD40@DDDDDDcB!*<Sȏ\~ËbSh7X1h|LFz쒖Oi*;85=޽ƦaT*/ATddc#.KI5d:;1^\L>=qg72" yZÅnڭma'er[kºBgRuD"~[lZ>uH=䳖r~LP\>d2#ehѣY׊7(beM6* <"{nȉףTxaNH0"~jdC*D[^#^Yeb%!l0Y#ŒhBy>S4Zq 1(8(G+qa{qBzı+~3M߽ohgͿCUWd~ N<؅ރ `nx#n]odD6=p3̚k7Yȸ/XVm 8QzV̸:~]+Ml [Ͻ?>y^3w .nYO& &{ѢBXL#v&kyJqF>7/5ّa\I\Dk*;Ez0݊2|۹1}ֈ7O;?GȐG:!!%!"""""ш """"""CBF R*Vƿ ^+DϘ̩IP*YQL sW7GSB%wyx^dͿ;ԘHln}|QZτQ~s q e]e^/mٞvkq/<3F{CJ.gmrqvlNzࡽF)~M24Jr-zl=q. {ɓe٭tx=: s? <&;Dz ˀ'1vX( !&ƚ(*F*khY4~xU>c2&Adq**)hl=M>v? r˺ɚw݅+ MFt,m4[PZނo1a=3g#6VL@J3eo'#f7m~.֬9&#ts_-ZPl$-M|4G._;!AAv"iY1aN: RA~ސ{ɷ :L>q{Op~ZL.<<)SRrQX,k|(B,O_9rV{b&Gkew36* +f܇m {M#Qp5Zg$ 䰽Eafk&rttj5y?4pC0(P 0DDDDDD41@DDDDDD?/@FɁ#_?LӪq}DbzĔ8}ó3.d]k ͩ:-4W%\8>>nT_>j(N|b\4n˙WUxXx좯iS@r bUaW C|܂͗*(ؚ3fǕ5. x‚Y:qWdlz y/kE?ThUiM0שe];LpK &Lڤɷ`G6Ic*D[MŽ +g܏*wƎmЩҐr0gI!AM]6OY*_7.G~Q"$(sdOZ$9 .1vrR}""""""Q@Q]GC fCRbnT\a04`Oti.'yA9 % """"""=&!DB`!BVm(:!>SWqNJ@"4%5YgO64W%8qb_He5j][Uslı3DEF>6v&dtxȐaEu5\Qymcۋ1^[+JaVZg~vX0KVl=QN#l#<ߕ )1i[&=Z.#uYP羇jUiX1>۱٭zt#_㚉d8C+'5n51^Rp| OUKaŇȯc!ppB'c؁x """"""%BUX S Wy/{G[XcHR{S@ּkqydUPg?y ]V%T0?1!iu> 5<$iyW j^5jhlBeMGP'UK;Qn#]WyZMތĸ;ӋSUDw/X7꯿XŲ8gwj9M~4 7 %7@ vkLܹ$Q=B^֣O.τ6@Rv,y?1X@UY['yLKGx{]X8缦r8ЌalپJKe u5`[QX,VyD8Epw\E~\nһBl:8)yZiz¿"8\(n6|6U6 * ).$G|Ť\Oݶsƣʪ :, N\* _9C݂(mޏ2 :X"""""""x """"""a6TN`D >?95?D\270S&nY uv~9xHHסFNxl;5YW^ _Kݤhۧʚ:/*k:`>`;d^1z(k,& tɤNwTK~&姴_Rbǂ!WbKIcڻ[Q!ʑr˾izAN4q??Qv?ThUi>gwa`;o  ArmLPjz#o(_\->|{p5uejPX,yBɁY]ֈ(m)O0pnDDDDDDDÿqBJ4M,52Xjk5xjs' -̯,6tؓgjqie˙3G^Od݅T4f^3%'iQ Ǩ:ٌWVTs@1cFӁʚ̽04t!VXU V$NO.+9 U~WȈ0ܖ3 w.կ+WC[ngZ`BdI_+^&/O_!98<+p(;#iLv"ͧNp!<%=.=UdZa#M& ,Trc;,p!lZއBayJY9o;.'Pּ-q\ XF8*QpDDDDDDDF < ] 5@6֣JE(O$LӪ e|l(kG sWb#ȝgOAFT5"y=V Uǐ1Go0k_qTdd_?Ɔ1:1->&}_jr%$dkN>OIEct6U۰<}%'e}h|xCRRr}Ci~| J[CNu2 $CX"""""""6!Db"]v*kǁs>zT5dcq|,BXL|RcP~)X{{Ϫ!I-sBvmeR.8vtx+Jetw21ޣqLx;`,_@'`7P Yd!2" w.v;d^_.D;N~϶`?2'-H-\86/Xch^buDF^\;(b1!r[ש rir~~c~} W{uWX6׋%+3nG݃/1D'XŰ KQGee'ZDՅD6ࠠ ]X{pbCw= |פNX>,nW_BBBP I %E㶅pYd3O6CU'a} cFrm:piBejB(<2l/ą˃jm>o;= o㤹ptBX"""""""bry4ML`jh5Ռ}JiZ5'anGB:62q~d0{ T r:O?S_?#È '3Nƌkzj(}/ߖĸLwt腾l'Z;:za=?nRejuɘ ;DFaX$qr-'KWX34w_}*y[՞{{ q=H<}l-Ep8BB?/{)fQM:}l"8\S=%F b=k44 Wb[sǞ4"bdORE/:JPj0->eL\;Ѓs=MW[#>oyAt;AB &x """""""@F|X @O-xU?LӪYP""A߆fjPqQ؝E`ѿ`s6Y}xH5h*&w$ P ZM~6\fό֠50+- ӑ;/@Fƍ=C㍖Ӓ3=޿:ܨޕ4,lO;zzq+C`smh YO2݈YawylOzzܶS᳖rY_ނg>W2><mV<.mVy?J[㤹LF3],aBa#F,# l f4QtX"Ӫ ekZl>߯y`ٰ^D*HCLG`aYxI۱11x}E, cvWᏏŪۧ^k5XOFu3ebuwۥZwV"ֻU-(6( r&&f$L2 I~$!d9d&y=46T}!avͽ:_3u8y~KzrQĉ:_^~sၧ7 \[,bؑtHMld\wu.cGpUPMZ)bG gaZ>5WB:j>LnX1!㟚=\(Ny|;^'ò?%M,к.)Y*O,5]']/{GщznGm:C) p'p=}\qSGU.8tmxJuJznk3<]yW(gO: uvyc1 ng^\^O[O- ViɔC>~}KuQ[ԞaujwޭlrJoocz[3ݽ/ oO7TԸX򐔘,q*MCBBRRRdXu/U,II뻺N8G /Iz0\un4 @>:xL<&Ir55"sgkjE(;>t/3E\MU?m;>q!āLr2CcS@>),a\NkZtے˜ı#3<]Ff&^6Ry+-A;h{Ik+cܠ3&^ LަjydzG?q1}4>.Sw+6ka^ұGú%SV_uv3xwL\vC-uWm /T'VdA.rQ]nh|jfj*9[|+o ]% 1kQ“4 c:7HrSkӇa$rw֩;I" '3]xTEw,M,wn.>3#κ;46S^P~ $mW㦏 Ň<#G;H*1LD@׆sۼRrfR*~H9;=_sWNf.nV%c;Ƙ)s99sR+0҂ZwdžCZseKJKɊ~j.^] =YLW,Y+뉰uØ5Z몠s\k`8i $%&kXps4zޭجdYSA z>I%=xvQba73p$VrvTa;KY.;E vUժ1O@kL^} uqm75 qu^y+*;V<@HOW?(4}9-aFIiW%Å̘L`d5B9<=2 -i6mP7|DDauEdA|eCfOeMXo[¾ra-~'L7|޼飘y[xg=PT#)&f}\#7][=/a:;9 2dz&)Wҷչ?ЪJ>ճ۩{&s+K1[)P7*K,[1zίeT,[ǎ7vصxHa1ג30x],ڈ$i{I|z̝ojZiby8^QZpկ_@[sT73uxDv?4=~=Y,f %MokUj8&;ewN/*Gz*/]=`㹁C @1 %鶮ITdܝqS `^EujQ9ViW3`rGl0vx0^/s,>T>vh5kU^hTV5_yFZcS@+^٢%e6E'(-v-]&;}i\:}vzQT^_L _g<yE˃OﯕDVl6= wFu6wXN &<,__{c[I}?;q_"/XK{<2Ew<OyLt{%S^ݿ  IDATWE[7Gq4:ǥTkT7*K~@?gF\1_CNfz܄zCGt÷6;HҒ3 ;Dx੍voi*xZMK4w+6kٶT\_! w8^P<=*,sV[RlI)Rg|; s2l͟ndUV[zI-_Ma()gr/, m@\x/x kO05vuT~3xm7IAKo8a?_[RLhMrx9_Rs̜W2)9#ځA*ˏjjlթ7oکQ1~6jbl?~sٯ1`Zwjk}y䓴^zdzr!=0nPgF*T[hUUOTWSDsU:bjߋy^x扸}xXtuyˢyCcS@1aUN'w>Nz]$V>PyFOsbzc.w?zXn~[;,{{u2M3f|9+l(׌55vAb-̿59dX]:k 5G?//֊/>P}NjCʏw+6=^e\Q9׆z}ݮjG:/m:C/Q _~MTch#ѣM˪f+mӑR<7?<6߸a^}1{uzM}+4x'ނ |-ʝjHan=TXw褥M-}zRRRmSך59YVKRXj&zZ|k¶5 na4cjYoJJLh=vy[Z>YeۍpI_^;c'FF=\=yx%X5~JB[o/SK/Ї PG9@<!0 z>~@R )ol7=Y3WϪ^wFͪooԤWy~}zMյ Zt/gnλME_7}9-PKkZOP[{Y]aBa$HѰ 0 C^=k<5aY[4tyWaIӄ=@[>~I>KzmboZwt$]/8/$$$*B <!2P{xLufO^P՗I}f\?mx=Σdkc7Jw'ϸv_n;fzNj@N4)z+RtB -:/%S>\aQlIef̌s]S'Z#^ϗBoy9] BjaUA_cVJn7;A^8KGGGikkMMMnnnCGGp K%S;JSz?/`ݼ̈-5wz~_MЋN?\ݶP~}VkFe|-O4Rˡ!K#giTz9KR'׷iw>ڪs$B0*ݭQw}N׬3-=We4dM1]I;Gu"^Ok÷o=؋ͥ1wkk?}NjMFjC_SWdMv>ٮW? pڟy^MuWGG@Xx88͢Y z*g]Lzx5kbVB?nM1quzM}+-8vV=?.7ol7q-Z {Iy 5 I~Z͘xi.V}IY7j/zA'LϷ'ܥ{= O6hUzlcuѼiZtݴS܁ =|¶ _ɢezhܨ.r\,slαzyg>bjlvy5塚>0=ήcg:Kr*xʺLHH,6rw֩.Yqy{J=jko賆[1u^~=s4ji~uz}Tw}|xaw#vu?{L86;^>ZZPf5[#Ii ^K>ѣm2PeW6ɢeZw尜wפFJO %c¶p͚<m(75_g<pƔ%DKut~OnO=.VKzmNuo8۫ }zKHHȕ͔  ~@]<-aOk9? ݪj|g~.3c@ϧR{Bf@W#K YtMfͼHnÌz_56TجƦ5Zڪnz}tB$IŇso\\#߮&fOݒodiY9I!5]-ug"`FߣpAw8>o~ n *""8 θ$MB%y<< I TgڧM?W}fݸSoxXվs!%)nj<2۩9CƷ6+WkÛ\ M-$~}ȹ#0qvJq>Ø[fG&mN{J=*<" I jim;K> zOҼ1 2Gқ wjAS]۠oye#5qȨ׼Dj.js{;XoSV~Ӱgވc%G$vChjEbbᓔԸ+&nqxRRRSSÇkĈr\r8p.ŒtxOA:::%~@0>+Tk6yOo{z_y'%&*L%%:;=_W1BޏA nG}uQ^W翠e\u(wףx^M}mHaIzp臢xcfͼHhע9䰃#ժo}#M޺*jOGu}pq=xMu@@[x֋f\Z1=>*>TuTV=_w8C._ ۾jGu|BVEm*l ^Uw@mAv8j\k/hCX%M]1|Vr4beee)##CJNNpwl!~5RNn T pI@?.qʝ8wƹ<Obo 붩_q>tfB[׆6iOPyEeDk7gM{Cnq 8{b\w}mm/) j̋Y۾ߵn[R=/^Bʊ76T,߉v<$9;h+[WVj$ᓔasuk҃[cz?ǫkEzgǁ_g<+&e_cTRbP~~SczVB~9.96bX٦ Uy];r.Nw7QiݹWp>߯rFPIZ/i [4I::: )1Q`Fw~@6.18w9NS,Q;~umj>;hտ57=J~&]ݵ4JkMzףZW^7V?ѣ":_n^m|ks11W~q^}੍*9\ԘOJAؓ+w)_}KseNجM-56 5>8+73o̍Z<\g G߯wM7~/giyaW+?A{q'鑙&fOݒl(SeCt[u%뉠zuY1}z 6P~5CHL ;::vQn.B3vyNEƧ붇==zf&B_)3DYE?PYEwUUw߀ƙGOwúUei]O_q|}i&{ 4ypIR៫m30]&56=F ?5s=ZO%?ZxhiWoxLJ;i8fpX&$$'y @8x@aLt:n*.wgi;K9Nea?F"zwѣCy0Ue?}Ba#ժwWިi|29̙oCcS@-my!\m8\.I˶ݧw?mjyӴiܮ)%ezcK=sJ3s^ ˾L-)%͓ET2UȡHuҺ_ZὛkTzCZwN:ۣ&K*TC%$$$:::x9@8x]BaYt;K^] );gj/(wu&mxttm 6 I,귊|fmjDS@M-j IwIYϽAǝϞX~>ovꀼgK+*>TңUZ5nq虯ҢI%hJ_?/g=zLy89ddsWFCթۏGl^y=:n8ǍR,%uvr(%$$Hvtt" p'P IU.8qRޯچ >4Z=*_1^7G5l>CA ;w: M֘ )ţr9z<øl{\GtO-SE˂ՏyT6h5ƆC&BSMcC5ژ-)E*)19.5~CA9W Jn@p9&KrIttt@3 cz?L"3C)snP5P;[ı#L$mq nCTW5鲑}R|J>1\|4]u[MVnTQ7jRm}>k҃[cꞴaGcS@1ս&\]L-)%R(Irgiy!"r;džO١'R˟}<ۂ۫ 7 rx霋CQPJחEmه=1_!^}T]M[r( '+) < Ba|*͢K ƹq)e;ǩ6yU[ߤg~mq@E+bTn,ϞpO+0qH=k{>.Oa驺]…SUr*xul9-:EKר!qAmO/~2aIӄa?*٩@[t>w%oSyIJJRrr,j]m->u rl'&$@8x Ø$*hܝ%wS3R5<#UvZZ~aa,tP}.7Ѝ'kM-'OM':n=Vzrtr3E6 7(KJL $s~%sY_丸m ~b2 WQnJk>Pon/15>5=*;:=J|gz*, \=] )@xb UO$3#U0\i6Pæ.zuDWE6馯L֌76-^RSı#d ʊk)Ъj MM _#U5R)gpiV&%_?#_Lӌg=)#+)19d2=U,q2[su.!Z>}b}m. ӌe!r[}}mFPT@šrC`3 ;p$7@ U#94e׈a8WhO1=jz]6:;,^Vnjp89RZ`fBGK[{u'T[7ݍƧǪև4/N90e OWԸկIDAToi[EA9bhtiҋr2ӵ:A^(yJ?~@,0=>3%K˼2omTIΐe:/WmFG:\bA =O%;q=AY4P0Fr:l="#Sё^VzC^.uFf;7*K ]u;"zD'ZZ9R`>{Nج:ew{ CբyNװ ;7(KJLT0\%%&=P|zcPcyZ^lE[n׺/=ke#;6pUCI7ꝲ--;g^a)S;c'{T6P 8xI!lUOa?8'? e8lrm="BVz8*ÓVs eؕjhdVFf;jș,#ja;-%:rxDEMӌqt:2ԝ~{ģEw f|ErAGjС[um-]m8gwl{\GyS7gzevXb&4" .:]&O ->S[R&fO {裭>:~ 6  <_x ą$%&*3#UY4Y-I2Ji['a ?dk\͝QYqUv6*ޯzCY-ZFյ AcC5s>?%glI)u^=nc7+'3=s1ahޘ!;<|PUoME$Bya>;^L.L͒.t4e׈a"BphX]δCcS@%TZy\Go`"'3]95\y4鲑a_Wr;ǚo&dZC:XG}_+ ^ x@P IUoFgg(a B s(+ %Ii6e8Rf⣭) C@*CDf]2Rj168l z6nI;WjXJޛ:]&O -9RZaN҅eG,tywݐ$wFٮc U+uȷG d63@CagFz?L"gvFH͚.{cRJYb(-&k:E :< n6˩C4Hյ ZtM/[|4/v^tqn=1s1 ul{\[P^t4؉ VqSAWOa=]Qatz7U`q;KvEnélCY.9NS,'FMV 1X-Ir:R4,îT[ߗw,]GjLVjyqαweC&dkc7}>f<'ٝ Sr˖2|֯{ߧ6(Q:\ 9Q@QeFN@8 #جc"Υ,JKXȡ7olzSҬמw+ORs㗵C]3uUB>~,ty})hw yT6PRjns`U]!dzr'Pa'0`(ܝ%IΒ="wKN\v3*T[x4Ui6e8Rd$ܪkh隠˃-)ESrGl?٠oZ8GUYilFv#B@Y_Ӿ ʍCE:;8x<`p aK=I Cw7w;!]h1;jȑj p{,q0*ݭQ5kt.rPH9p&@cYtɩ0DgglCY.iŻ@~Z78۩, 4~H*25vAb-̿dX]"kRDJIztȰ̥P|zc捹Qwj^:Qm  @e9B!)s\^Ǫy75:%gZ ] [tk]KP̓s㗵]ScVkFeEIIIJJJ$%''+11QD dBBC \N1Ν-I]AyVzuKJLTިJYb-ƺ3'dsdM1]Ia>ovjHxٚ;=jͭy>S.IX:D%''s'i8 <]@L*@l, ?zJM?Q[{{L!fa8SUr/%S>6)Y\mOEi'w5] 6|bO]uvQ  @q|!S?56Xm̜O͢QÕȋGJ+Z򓵦1!}yϼRR>@[;UG:+$$$ IIIIJJJ:n%@?IT. @Tv~#9d,pt"6TrGfvS+mo w|֯v=ɉɚ2bϻc WէCu{Of~ j:=PFImQ,*\]vXN_~fpB:;<ѳm$)3îѽQ 46dZU65.99Y%{˖6(?k۲81 .`N8Q 4@1Y=ɒT@$]:s\:ƅ3Wonvk|*j"ͧpC] x"0\u qISKL^^ CINPFI<QfFzTaSWA=( `uu(Pg.Fz<.J@tw,MU< =O!%`Fa~2V޽`(!)0&35ĭbnEI0xDzB @7Rgg# raN \=Bu( pa!@DnˆI T,L!z rϢ*`*VO]G@P +N* 7n!3 #W &KrS;::?ydY 8/bcFzAL4URz:7x:DHRAYT֮$N@0 f(x<(4B}uȕJ:C TL <L1 럹]_w+C]<OeCQ;F'$1YJ! ]?R&1W8BqgQ)`H)VWXA "@:# 1Y]$;HH$X;P%ލ@cFNCKnT 8Mz . ug%A~#,qf@B:D6bD y۔y<2J6Da}%ӻMt;׶4jZ[an}mx(% ^x`2 ى"-%[<O!W*GF߿IENDB`kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/images/kubectl-logo-medium.png000066400000000000000000003425371504711711200311210ustar00rootroot00000000000000PNG  IHDR,%^r߽gAMA a cHRMz&u0`:pQ<bKGD pHYs.#.#x?vtIME 4m7IDATxuǙU=<˼Z,!fN.vlwa.L tql̖I,]izfgwgId9f{#8A'~@HWJ@UާJ" D0,?"q4t]@;Ж Z[[_);A/(L4TӀF`0CS)B@ Bǂ`OL)* ĉx+OLy@sJ|`L(AsGӍsгe4>?'-m͑-.͑ V3&xtbU(Ā`p2 ɂ(B}0iH´V3 0!@#00 4.xظkq2it'Ip3i;d\9p"4&)46/ a;pZL?u|҉UySAsO's\9DPBHiaqeDJʉV)$RRAP`4`ØViZHDBHkjnL<yx8N'N%$H &`w;]$z:HvN<|0ZՄJJKKwlذ|4gH 8X'X %:e9hfZ5\4 0MpHI9ZkO4b5KF!i!yWV(O"gVL9|`si2} vLKk p Jy>B >Ԏx`'x4t^b㦢he+\2 RS>}uWPT*f#RRaYn @MD9xGz=7H\<>06R ;|Yf) li`Ϟ=)v=֓!LG)yV4 q :T3sEPUL[9g^BDJ*@I)4(_ ʑ!WLb][صv?=-{pm{$ۀw@y@U ftSqϸY_HYl@褖U7Q):S ᓐ2AHRll~`H]B{x؟GWʀ!/QdwO9hY%0:4$ $lų)mĆZ sQ׾;fǚ稚>Q/!$B ҉ZcۣwѴ1; h+aЯCɽہay)־wjs@x,-ZA0{,|):S)^?ˍdΦR룳y)# t|q a\HRNhkMڧ݂2|ԯut*v|7 = TJfv /|sb!x|G R6i ̜=fC@}"l  :eUm?N8vH0Mi+x~/!ػiO'܁cgt;;b}H/]h~uª82U8x@5̋Y|ѫiZ(s-73o+!l^8xG_G%UDEAAeRLհ)d [L'29f_rO~@gspŻ8f1݋hRJey&KWWWw )RR4P] Z<"e>b_,Bw|KKuv"@$aZc:ld\EgCbLwFF&h+d ?KAׁRJ UĴ{]=m>pI,VWWOt&I#oޅ@y.H+g镯vrxYNwz/"^ZNQV[OB"lX XxdB-03U,<,"EE<=l1uWJ)Jkj3T $Z\NfYR\YO7ަl/dCL4~*:q׫qB5&aM| AHЖ=ò_y# _v !EǬ! cBÐsX<۷"/3)0t݌VNRJk My{6cC8ƙTOô"NbӨ5ݛY߲{Hu#0m=bN#`Cy\|CùM\1c>˯ysϺP=NJ$K+-Й7st\ a!͹, $^f$<)G8:Yo~J}PuxCX{/hz L:s<8'@kNVAF4W5/{Py.J_|-K.i/>`FŒJiOM]vؼܒo&Hň%4$}ޢ_@}(ű+ ą&!~mٌmgpZeZ'!&3浕M)BbNe$;kmF)/P\vJ^cW Oa; Nm5ʙ)ב‘Wٙ4v*E($, t6ϡcr ƹ>@SwvuY D" B$W?A_G; 1cɲCӞZ dgS0^6it@0H8' cgܽke\0/2 goݾg~<6R^ZKXqh㔇N+ B~Xy\U }칼TνSewd*[!+5\l蜱cGD?ܳ/SVSW x`_?]I%i߿ r\z =0 a (}8NDZꤪq-;m[ql{h.KMEewOT b&htKXktբYKx{u%,U |I`suuK/FGQhwhUp:-D'+_&īXq;Xp՘vvGR}zeAe@)Pc9uણ9U(k$-"/%v7iF(IW;fI{L⽏/ \TČˉ! vt`֓I&y߲gz Ӄ] P(]8_C,@IPa< y@-&;ԧRC.S!dED\VL5WEOwVWW/!hu)4w4. ^|X  ')YEIOC`]R~Ivoxxw)%P`H ޔ֎(_0%a\v\zкg7vDAWA@KNVj:*CXjI4Ϗ+ZRJy4R5u\;;A܅Xz|Ç)%]M[ywfO$>< a={:/Fe~ٙQ>c1NZXeJelWȖsbrFUHpŠtkYsחT_7N:܍ R굎 S;O3|/ajsw}( \W4ML+0)]0?{XO:[aV0tL}2k7ۙ ^ZNe +'c,ZI]$@Z~)Xgr2<>+sAD|+H% lDa"-ô0 sNwΙcdɱm=$vl(k8N&)ظ3Vqi/B\HtN={0\x&Y)+/̿_% R\;3qs0_ru!%RH郗f'N& _(g^!KJ)\;㋏B U}R OItꗬd=F&: RC~^;z*~<->B圥YB lKČ$׶sKMJ)LOɊJ)u _T^IW"O aJ _ihұz4Htw~wϭ]{VSqX>Xs eRL?|z(>`(4.Ny(&Y);@Iu158H_E%F]+![V?ϯfuZ^|U- E,Ĵ$!5`@K{ eg qLvNA;t9 &[::R){+9=E)RWsUcӲCSD49**|Ydgǎ)jU X~[YrM:˂z[iZ-q3$3)-9(@2e`.ٷSu=\q<#OٷN5:qQ,ISVhOJߺ B๻orZc(+9'{h<۟uO3P(8I:yn*+Xx,8JjuIiXu8 vn%J ϥLuY|Y\6I`a+"q߷?CoKS3~:g;^nʎQJG9c%7[Y Sb]k__7 ރm9&ȤI*ϣ}{@)_LB%Rjն=۝(uv}֦ݸv!߯%z8k@)ރع^6?76Ŏ4@*\ J0zT OP,,ZJg$v޴d'P v1-?Q:m=W0O># =;kDsZG`=Щ?"V^_j0B!0d S-lן,۶ko;g\c^u<2ؙ#s]6y&_ոx93|܊aHB@DJ1ZCr Nx?{ɤS9a?lD@?']x9PQ IӶs#mA՜ ΄`={d4?hzǢW\Ò^CٴYr0 I$av<ȭBJng.IEU֤K _XEY0EŷhJ HՕU9MJIy}=3fAm~ecO)I:?q:Ќ$+$R\rH}I)MB!E,?UzbN)=^x+y= λX(mO?_?e4 ;v4 Y ².Ǘ@M⥐p)̹ 5l;ʢfS0;V}*D6B/’j~:v*5.X) lX-uY =Zp-w|Y:wo΂,m#/ee%JtkAU(V̙o0˯h7!"ZKQi`e7M:5HH]yGE-:t_6"r;>o; E.`s3_BS;utY2oB"qPd#)p;1Nִ i̚if*1HY{/锶5@"PAŸ\-(,qGݴ$E)cNO;̖f(Y 9vXwؿkNC/a㨝w~Zj+iE{ ^z#aPBfU5d<\Ѿ{Q($lqT /*Ѳ MAVs?Vga1kՅD˪uD=L/P5q]?xZD] :^\QR8S;P;,>*Gy_s WGHI:. Խ{Qv{'6YIgrG"᱔J"_i\=[xmlyfٯD̼T 䁈o5mECmGq\$sjaƅP%@$ a5TՔdWRRרseNʧIͦܚߤB uy/|Xy5w^,!A> | EcJ ⌔Rl{iVo6NBb,o$IS7gbT ?%@rFyjX*N:ؖu2";~~`۴.G;xM՞6^uTlz .b|(j_a=0|?CDIRҹP2o 5 ϼE aZm :k(rIݫͪꁯ[_joJg|\"T\ƅ/f8ko!hڰ֦W+89{ܽxY ViD)%۟ۿ-Z#]&NJ$H :AIU58ıC;QxڢG /? ;4@t>.*K )C&NE;|m<|oE|3*ŕULDlx o@)e?!kcDb A W&v<^O27?#K^ۼ|ZT3é6>0v瀪󨨟aZ wqn$RT|T4t<eAgk{6m$Sv2,QuUX$AjK@85 ˵ed6H(D}e!\/,8ktyt[7N R^@ŴF[<^rʫhvS*ϣn&䀹5/! 0F.=+6ySʗ>+j( Lrd~_I01QNuÔz. S"8ۼTXprQuLٶ)Jz7jWTnX 3/yG{ H'MS%E\;LZuMO<8UTKP?,;;ߧ@T>BoE "sMnɪѓI9|2YŶ[82VaFj5g͕wBf!D!J+r^@@HsJ~Lr]O??T}@ @(5bj$m~˲hsBLD&u't.rqQ?.?ѠyP&I'ާbc"3CpQIΏIL2P0j@m[K-gSv*}I9,gUoke]Ws]C՘UXK5&sq2JO?樜ISLj>l S*|>ysPsaPael9 MGԝ(]=4ձY~C3SW*iA;_Hw넂Eգ@)n~.>6=t/ `剂o> (Yo$I|)yjm)hTDg#A_;tl+th w3r=t6lާf&Fpq附l怕V9٫rzh8QK4Z~z$c! ò;0x%QRҌPg,M?jb}L~GMvd6[JWQ{&l1kdKh:5WJRN}ǎG@G3J%Le )Ł Oɾx5ǂ:Whs,-X0K4h(P4L:jd0tr]8":9.eLTB3lo `?۟s<{_4Pbk Vy#;U#6 mEnۈH DoվNVbbnwY-^Mg?l.ԭ"-b40}+"ZVURHIHO Lt: ZF:(>3qB%j„ %CP2J][lٯ#rSI.?T+>T1%}Q*վ&D̼ k* s\*Dih.aɼCO\P,fs۱AbI2tv#=ʹhҵG:f勂AՅś^Ò+ob`{KUt\ZvtJށ8H!lha0y$ L!7#L:8uu/A; P! RVPbZ3~2h[0ӧǟٻo+DsTI󠀙P 'b$gڹQyAp[G9\e>pkWֲZJܢpaY2jVdC..#/f6ٻ ψVN^s Z]/k^f(ϣd,x[AۣuC{a0+" GʚQ1<)ujcIi9,)plOb̀  @$JwyGYZj5P)j*սІoOo|[\q<ی=mL07&P ߢEŶAJmU4CzC kσ:4v{, @~ŧӺoz 'R#ޛ12=sXζrr(=&sWhQp&a8PQlb-B-.V!XR&LIVt :FmDʼIIJa ̀FT/q)B txٻnZs_ jFYK'Θ7x <οQʇ%M#+_TLgtPVg\2 ~>z/:szlt?J*4 S2]H78EYw '| yS˶/tA( 8!y8.nX#brǹiB7s!MJWV9ܱi^=۱ ,ĹA-@⌜ɗй_y.Ź .""$ҽ`ɤyI sXQ<տA;u2saYZoB60`5FN!UxLm1h~e b=_sCVm/;%a:V'oRHYKz3V8:|9YpbCk΄C8xܕ<?K@e@ʤtBݳq\! l{/ػ}sރ(;*R>jJm' vsv8aX8^QE g@QM>]GṒp>ž}p>9E(-~tLGy"P#R777c0C`5p̱:o*Ցz(P%rէ a0sZ^3_hb_S@TBT•K"b<ٵl&UgB<o'Fۿon=RQn 7a&S7gQSSJ0h#3.I# <7).Jc${ #B&4Ĕ @,'CyIy8li )GuCAg>H׀8o 52QGK]V5*ySX?0@s~^ߢh6XE-jeg@Z3'_1%TycRDVX $i@p+N!`v`De=l:J*߀2B$+WRy{8Թv"3 Τݭh޶v+Y%M SbS3yj͞2 ;.[4eQI t?q:5c,l c&6軎To>UjQg.t1i*8qQ?4&~6{: m-QD+t(fyq?Zߧ 8Ĉ鲥z6cdߦ~ -QodIzSWOIde&B3%GPԛCqjԟC([\~u_Gu=*-<*OU)=>5Ek-)EDdp<+NbdkY/AsYOINAG]&b*Vu$>"=39iȬ?iGA8VD(ff3pҦ@˱=RQŶGt.x6p[v䵆$Ęm GC&`P,QQy ;rFE15 \{Ĺ^– wqFXٹt0Cw}HOJ)m/.a ͡:{஖g'4S'Z83!={@KAzpdRC03Xz%= GV%)Pzڥi(N.`5wbDŲ<}*Ќq@P=bHl>Ǽf1wt(& .DHME,BN ( ]1އJ獏BzvS8vsHUu[N'NtEâaգPx8[AKK'w,--]H)`GC~;'CV0H$^5Jߓf/Cb Cr&1a7M_w4s>AG>?GQfRshDy%9QhB ɷH hM R}@ @qQ\,Ha5s'R pPS YA>>Hs`B"P}Iޓ͉7.V f-S'\1FgXf#۟}ՏұhNK҉6=t;^ΐ#\mm&u-& "47ѳں: x`x,ux? pF?%Qo.NA սr⟗T%AL QC[Jbw9j&ݯ]-˹ε<žQ"1mcb;0QS:.-7(\-aI!i۱;bpՖ\Ѣ1ҳ ~Ynn g/;x5l!j9\/yQ>&²ydAuo|^`."XH5Vy8y64u:uDy8);I8q|2*@K[䴋WRQ?Qk? :SHi:KY9.k?Q XRfzq kZGR)*ô(6飦[ZdɼVL <LDm߇:I@%ZQa`$ C`p O>}ފyφHħ# HVNgk%a mPU:23AQ ս*WQ4y@=wv# RT>DcG\IMv 3u$J7gL- $V)# ӯ/8| {дIJJ *OFA8ӑc~ h}N܂.0B ʗ> .W=9{yyqa 5fY g3Ӎ*Mە|/A;=!aVwu!~Jp͔*7žu[CSSh$Z!mx~5DXi)'\% $ZD-S4ă-} (o}(=[8؆Xy*d܎B_&h;79)ׄwh}jcJV^y v#K.atJKK 'WK/KuT*EGG۷oիyGyaib.C֞, fty,49#h>\!b3܇WoW5+^6}?;P.Vj˾ ב.[57e2OUWW{Su$=RVD@!]I+KB\cӦ&ZZ:Yr>O'BKOAz1 d_'TNٙTJA$jg{@SOA}t*KN>Iڨ@uҰQ:[V&s,ꍚqpxQMU\KYteee9p8LCC \p?AvC=;O6N9|diZ0]dwŧk+_Bunț2 =R̶\W8t7?xowrmy\|pA֖< R*|f g8VIn/x2oy pL9x-~TJYc[C[#PF%Mp3JHhi餬,Nq^RB0h6(=arnL)J)LˢaƸ1S~l yC1Eq7!PgXq*XKp%Ҵ):U{vgնo} |c̙:ZTRR翂W|nfۓN'3t)a>fn=o(w};Mמn>X 0(//eÎNd)rpCc^_ s- 3*whܪglV 2{!d&p;L:d" PxVQ桲b/3׻o#8mϠZ=w/Ebos7X]=S$vI : QUd&hq euӰCUrLKE, LS,D*m!b$Hg-͉y8fŢqƙ_JJJ8cl}ʓrw<%qVW»ś~}٣Gˮ]xg瞻r󺻻g>_o s=ؤBjkk+PÆOFD0/+g/ l }O~OGo~[ڗ!0oCyǥ܋߃,YHe6AW^B"Jg3>f#Ց}V' Xd%VsL)LPSD 7p훑][) 80<׾Ƿ5XC/&qXdƘ\R r:t.u<q-s,ΣB@W"3̓ } }|@t;&/)V90M& MOD텣c̰ZF j?)\z/TWWǷ]&}+>eY ջoO]~+\ BIE&aΝ<<[}- zB >wG?9n5½`38쳸+9Ӊ%淼+VO|*ܛU (@.M?a^irŕW({{{g?Æ=V1rP` G$"6`-B+Q}{n.^XȔ,$з<kՓ͕uhn B   P2$0km](s鬪!#jZX~ >!p(-۱G~%L!iJkJ"vI++ TǬW}iؠ,l肰5]!遂2x /lIEgg'nm< (YֈWaKpϦI$DUd`1}<۵g(??yk^IUUՄc[l~c0_;WsR8ʙ+WoΝ|mw>d(s^^"S'T"''tOTDշ2,B((!5crd[˺]d~BEdK D"}.\,%0]]2Pj,@:1oBR޸tU5-~ r[ 2,+-kgšx<'{y a@|pHǻ 0i{RԎ/XBU7׶ٹYzZu,ViMa y wl)$]mo'yJ)=7~BcBݲ@ʇIx(E.:ox׿gkx~ӟq;=ļ"+j#Emw|CNjǟ'~f>2ň7! qRXH $d2cօ3,,oO?-?|;&kYY=gӺ s7g ?ߟdɒa}$Iz|K_b}⹾iRMFp"XbT"6}@;T)T HD: 2\36ϊ)C8ٖ=8 d1+L4Vo +ϛˈLEy?ɶ]2id&,|9ezJ3=c6r C ?dE0V~׾ |_ҥKͭկ|?goP24[woaB]8<|_'ד)? 7cFB 'z{X{R@6i LDdlTbk~^xa^ Q x'ӧO=y;7 \N/ 4wX)xɒ30_aBh .zgN8Wp,Q3C#Œ A ΣE)\3>| B)Ks MN~ Z>a;%btt@uڿq㙞I!@b%XZ** M_40mdZ2?N9Ue5Z S LH!H{.C24R N2=gÙ;w5D+ R/`ΐN3FWnvom&LAvf6YWbsMdJ)(,PV;P4F~Ujs!v*IHQɤ1dl!qd^㽽 J{wGXnGrSCC_7yk_^>ruM?iii[?o2c,8fd2Tz$+pS!9bJsAeщp ֜Pg;yoM hllo!_~?v.}E̙;493X|H ?!?P1ݘ% WvMB5q![WDzz=0~4q*7\ߝ{vLF k#j:M=|o_o9L%#PoEcqMOwl|YYZc0Æa`-=XS lץ'*&h9M 0dGo/?۴K{(JvZsIj'݆uh YRZƊjS,$deOO?wͭUsO--gx2~XRs2E, qp2tSLvۏ@{șWdŋx {t>Oqy<ϣq9xԧ> Y#kN˕!@ $οpnN1M_x.5Y4k5<=椹|ӟ+K< 6PZZJCCC6۷ow5^UyG$@j͈{ճww^U1R@q ـ׿c(B(| Im.ox;㊅cͲ@gV jd2ً#̑FLK8L<4o=SKQY>XLLDJ礴>幔X&_Cz2ŜbpܼlkX=m !(Fhb1!BE&!$QKOO8lPV\)*yCB kڷroN'I`5[0R"eXZ6 )r 5Ɯ O'2i)tkc1'Pu V kByx뭬&j>1wHR ׃}vc1)oCDj;2Ę7~ơ:#DUOMd婈L^s7 pK)Yl٘cضmoy˛yrC/c"{jkq(nD TPj`%tPm`1jBtgƴKYb3cVfm $[qRCezR]ǪT+둾ϔӔ+mC4ham `{.Y'i8ӊfP O>+٠R14&H{6 ǥ3?%%xOly8nb<]&F$Fqm#NgS5k4GӦBSBбg+)7xAjsc>۞nV >7ݍ=:ihv2dƅL/P*ϒ: *ݷb}=}s75kW:.0E?xr}w>j=׌!Z^juFz,D HmȃB8Z۲,`xs5` `yDOB'6311/)zo̒2†`+)5H*z i 9RJљJ,JAPK2mX$`9\asI8=4]4$4ݩ4=4MұI..yU,rǞ-.Ms0+^L԰mv\~Yן༫Yu;(qX5RG p-ΑRjC,۶C+[Qa &㖑{KgUвIwoY:*(;EK>8@WW'i,ZI(Y5={x8N,QWW?Iǿt+ٴm6%a>h |`cp=5gK+AP j`؃P.M"txF|cxf4+ ba$cVE]:P:c9q0\U={_=J5%ۛh;# ;dQ+7aYY@%sz+T 8 qlOk`J'谙mٗփ<¶Ci ܢRREcTDL!1B7yhH;'R`KOz:сt9Sqve=Lj{3pŇNIG$z:nmA]4>DEmn?w7r`? _&f#@/~ /`e~H{@TIMگm{ @׵w\yٜzSZst`.;B;"{ˍ/tSc8P͇V@OhG' voP /PFgc%kE hA]uÀTvwA/ZCo:{aHLs(i ?M@[;|QBihӳ7 AA)b|)<ҮKg"MQ$r}%bZ/) s泲%GM3:/SZV60(/rW <zivx[ 5q{N#%lm[>˫?k8h )=4">-A.Sx{@n7bCillǘV ݇@e`"Ο-h7*>OXngHŒ3"8ctY#*@)*Պ`,g`q({v|V4hMkC؆- ^N: 'ٞF B8,)/ ղuKJoSkk] <sM"P :4}Mq=W)Z<Բ',/˖E4c-UB PUC`0"łVS[vsGur$س[0=܇!z}{ ׵Ŀ'0 fΜŇ?!}y7!˖% f^$j%RJgswFc!PpvX;pӛ4<]f?L2Nj-v G6H=֘9*CcdGPF(Dj}0k:ORI` 7>R&Ah?*0qŒ'?Xay>&e;"@Z"7:}g:M),3ryLhk^5 YzbBq&;9HP ]D"} F 7Sri(Duuu|b| _en̹7j'1a=Hdi4HcR 4$ ));B3~B=oO~SpÍ۸X<ƴi:W[2Z(aG{:T"P}kQDI|݇p~&RPF'ZKt|a:J F{ 'Rʾ> {. F檤ǒiHA];z70re07m?yLr*XHe28o%[fa3+XfAR"H1H'IJ' ˜Bj,fS3cSOOG? tr^w!!fؠOvePNt72V:c$B!w gͱq_^ڼ@sLB;Q>>ۉ̴o}7@{E8o ]X ɮ$1}t̠<]TŠ@X/? <gH!$Kaڐ^Q`GtMEiP8鄀w 4N`"!Z_$$ZTRD- CJYJf'>k?0֞\EA/vO\{oO\ɕZY oPGAJ;}>^is@FdlBRl9%8.=ɴJ`"J =ȸ.*åmūZ2Kn\7~r$e (:Rx]0[y[R,R<ܳ|_箿}s97\D`qvnQDڝ̛?~KJJB x%8{D`6x[uҜcWWw&J撋/ p<#עbuQkSGHje#,@X%л7>D.n&Bdw(<319#ϐ=1)9bF i8lћLӟL"8L$&ls=vt\H46e~I)a)E!˒#ԝI!j%a(AqKep|zl?8Gqu9,M$eF o|-7 ڇE_k-A8,E(or:׵?ů8+ }Yn׽w455M7wE뿫d0DsΛX,iVAFp|x{㷙X7/~9,Xy<[o _~9 wVmQq1ox1kauZA!a"" L'(a`E}QH\RBh]X q=*Kˀ֒ _P|E)H SCHGg`x;@+74RPjQCz 2#(+="`<֮4˗s~4>PE).F A!}%eN[ p\s]Ҏ'לT@ `!( $rXB 7gI E3YjؚyjUQZ7cRz¬7Qwv}dGnX!P?Q-Za؜<ø]}~VZEe @;r)s< (7h N(..?Aۉ/$-o[\vl;6ȧ؝@d%k q\˪jO 9 ˇ?I]`Wp8 $85, 2zLF#k,ӵ{#_iߪSgJ0N,{#9b=#Ex>mwCĜכTcwq\K +`0mz^TŒLA慎⋜US{-6JT=D(eYBA-1Wk0c6vF8 qEC3q'>&ᅬ/}{3߀NC+eٵ)V?/_δ =󩮮flEuQ]O{ԛF^F/QtZ$^|[h۽W)m,A;ى3sx$㟋%u<ϥ_]0kmWb͜"\YtM|.cy^ϵ^GWWQZZ:%()%ozӛ hii3fLXkk+[ ?`,e}}]cH_qߒlFg2i_^154HYqTnv@f շQ:wmDT %gi K:gxV 4C5lOVu \njADc^ӣWDI׾ɖI>.>4Ha5BBG*EtYt)ϴљJqyLsg%XB 7Q> @ 7$f|%)$ai⺊T)7GJ+gxx97/Yƙ55,()*&bX׽yJq\bE0m^c,(gz,@s,BCC2M_RR+s?Ϡ\VX<:m ZAaa 2+}Ǧ!F%Iw屻y[Xyn_cG Q( 8׹y3jXdiMЃr(r"`yy{>w$KO<0}^y@gx~Esظe짴s> z*RLT6è:0zݣ37Ko"#@@Gs{hyo~5w p)Zwn khSN[5rR& Ryz6toU,mz=s}y -*>/O}}}آ/ 9ïk7v_k[u l P2RK{w۶YgCkGeG%F՘zׁBJkEXH+JݩEGAI1.ecHd"Rg Pܭwks+p,,+2(`8&)00&0|Pb9Ӥ,'pҮK' Tl\QtːDuS0pyj*+B ;4 UaHIOPZeoH]ޱo"3H@A>=0&w{F%HkYdW: Rкl\ᘀp_@@aѽ Qݱ[% ө &fT41L#`t(88$2ϣ,EњH]iq\2,:$^9K,CЕLaJI!d>#jep{ 6P2 tDEDZ bf`Xٞ.j,Bzq5DE^jTBa>[$9My)Ha:.HF;ET1];wrccnP<*W%7I-<'>1nA1+Vm?ENi@ TqF?hz]]g6o^Yי*H}ADMEV*aUK0l/ )sYJRX(8Dϊb8|8&OF$\gpdvy_$"6 7ΙKcQaP %21A` V"f2#`D|/lFЀX<+,coS\TYūg&ԕˀH ,+1).±B4[&涊L)iM$Ұ+7)UB^іf @4a>9#Axl_Gr]23l 1=T5(^U9ϸ{nE݇JSGȓ9wƆiF;Pf)P?î;XoEU|Y~=S%H&$aL&3~Gv`{;26GF+iw}yCFk5? j> Aٽ/1Aet/jֽk7~GTz(! H{ :U&2t]B@zzMq9:x2 CDAL_Oe; ɎTbr*46N/ y`Pxaͺ--78?˸˸˹˹KyĮ]&-ݍ5:f*M[geֱ3fILfMRT  G~U`plIC ˎ6lunہ <NJcIDAT s4ҽvx oQes 敔4/ uTٌvgV A,dd^ AZnݾ~fin4`,fIi0T VPJWlH綔R )q#U ,a`Yۘ`5lߛs9y/-cԽB;:fx ~-کS@i LL?5Cq f<~ӟя'RxS8G~~(]5"M7l޴ik =ɏ-t1eļ&ckFtj@哐!qڨ\q &Lzy@%^WIuTx'd%˙}fYV# BB3h ԆyH".BJ L_b43~moMwCRg#;p 6Ak\z3V` +8+T6^{k-: :t9hi"!f?՛y^^0ҠBY90f<7ZCDU3hS>`UƜU!!¤p3,lK/x]WzAә2J+Xy V A4nz{|@ԯ8d)*~4K]2Z*RvٮP7>=2! fMO;sXORBu(W XRbyn VrkJu)'Fv >hZW幉@ҳPIPlۦX** įD {7 .j|/V+ܩ?FDV?~ m݆94ք¡q#5Ϩy%g(ш9\ꝳK)A]pAg}r $vײ0NNZUOfuSa)pv?JE5:ׯ˃0\.2X*Ҟ JSU Z`57jiB,lウ&$qVգ- 7Xwt">eU& oE5l3"]QХ2k?z#X=M4:%AG\"NL5G-1뾇(Qr&}3;< Z;7Q*\"c bZI9ta]gmU+СC|sͨU9.Z顦R.G."6m4z._'nr"QE-WfXF9M$dbya+fq6\"]x}7*. Bɤ)<U@*#@Jp]b/Z&ǫ?^%6?<% !({-ݔ8(˦H et!*wwkt#!(q]'O&erKNJXk( .a5XkmMC\סL0I͜VP(:V"p&>ܰ}\PR՛=w"yxqs[S%%%<3,ցoaEd'WwqusD0 gm+ "ʶwFp}ƿo^ic_:53e05jbe톪Oϙ<̚k/NJ^εt=a]L+ʮٿ)(4OےJp&7 $ϘFXp! +t|J77G chy(HfC&URs(q:c]}(>Ͳdy2LU>4U R^9XSZM\2tEEˇ*|/ S52h|RXY~r^ů#,PlLYTrn+lϧn\UiS)YۍZ:AGůȍ7~_shmm 8~h zZXdY-`,^ Ν<'€!<#ZF4b i‰fOvomق*_)Qr~ xOJ Geq/aU\ꔂCHU+ۑd i0 1\wAV:^XLSd.)) =Wp]<~7(8%Yz&LT^#iLXuƚx8GX׫R2Zȧ1h y V |vu1" ļ2.'g?aQ+ <3gbl7GQ݂[kSQ I<88?WE^H$|/(К.Fv!Nt5)f͚5e" Nsƍ\qY?s4Xqp 2]J }Ak$`{"F~h;ÈE̐_Z76k;X'N"T!PEjZ(+*Rf. ţ[' 72:w;s,%KhhHB(B 2(.xQ}l 4>Nr5 z,S t۫.]P9)seU-Ve:O:^\y.фBo([6xx_ٿ}.H9ygWP݊ʚ;k-pHti5;Ǎn.}*]^N{SHkُ]]BLqp b|iN<KFFQC ɢx55-MRR40!_q1F$ˣ2g/j8MPO8'YH Htcr~*h9KhS+  O%s4SrxU3[w#XAjAo:)pJu2蓤4^M]''TEUqm]/nyL8}-d(JiwqW9;i%K7qW; 4Yz5?FO͟+! H!9| O3ہ@dz$n0'̡88ȥo}=6V\~1|ТUGsAwh~,x3|Yrbrt?.L8@zlxSǎAփF0GFih ,ȔDtقihŲ_X 4 qcGYWH @C9ҫ ~WX|`QŘ.88c)b( Tk7k>W͔ $:iP ~7t7WFOFp; h?O֯lefes=Rk.{kv><eTU09\e˖ΰYUW"Aͽ={JN- : y)Ahx#BOf"T,<Ӝm"GL]媏Eף:F$LcO7Ƿo+ERN*L`r3Q4H'\& l#&y 'PM/mk;v+r}C,MQ!*&4Y."tI%M|l:s /eIJ]#qt@J+j)bczd /̶ZȓZ~a)##%=CQ/0LE`T !iln` w4PT>vAF]]d8I+ҥX{:6nD[[۹lRJ 9sGp~8ȱ' ,[_~lٲOoOBgλ94Â4{b VZͲep8G[q^{-W_}5.R1N<ž}{Nv~G3|drZӥ#^re-o^$Bx}(f+!z3m{:}Z,$qc鎙0ca!UvCV 8LυQup"Nvh%Xu#{=j(Jh&oC }Pr][pplHrXsU%5=K Ջz=|1ϻq!k9t$4HPku&oYDTM|m;XE_c)ts(.D(tJ(@XUW+c)IS_԰Iħ>B)%cccUBnCC#egݦs211AG߿#GryeWIшfσs뮗M|9;~g%z͓-3d3ߠx1]twu|rV^+Y|(ˣiI2dѢ^vRJ<Ǐ=^:M~C (7:KK(Jwt;*j*g-8CZ,|]t] ~iL09 `9ӑZ]>]*TO@\|Fc(.C!u=]]]QT¡RUEֆvA{8-K3B|v3U-d6Fפ]_S*JM6>-Ok mfs VȝʜFC_Ž?#9RJ"`YA,c͚5Y_s r3}l@9f)AǃN:WF ~@8#ѳ+ꐞ>48l'R*TkF,eP# R` C!hdP<ʲm`F1bk(_k|s~C#"8+0Co8^~+Sq!vG\ݲp{NGyx4WtV3FTI&MzjO@@ϕE&hA-3 ƓkVtT!)]LK޵({.Zxm 8g:gMEʴqygH+ÓxC)c(Yע^ cc/ow-$K={G2ŋyDggׂÇG?ʋ12Q"6Dt5JCY bx5@.r[;|wu[椩5Q"hu<"kg96_ꫯbժ[ccc|_0Xx17mb˖ Yt*/c8]8D 6XK>CPy!FPTZxRqgBYmE6ϧ>l\w^%K^9NFZ2ɞ!":-_2.2bQJ5Q(Zz-m>0::?ρ;xك8 hHWsײm,Ydx  T 9%} -Hk 5J?33 l່75LYF8LvxS0 4àSr``aWqdz9Gkf^D {a4Ak %?zߜ8,Ǒb-! hi)L38TEˡ츤$.35-xb hદE7S@:6C>r==ISjPjd+XR%;6 'V2֎gOYRJQ_TyZOJ?mY"UcmVwmE7m $;wc:ԥo5# vs>/`o~! ,)I+8L}*LKK+w W,ᑗ&W2 8=O3[OE|sAt bDtNi?4&7^Φ͛Xbh^ͷ]yhF/ I:J{/ CZ#x#wwnoH*Ivdr@$V?ZP+I V]#<G\0C!\+]%7PbYxH#H౟';i Gʔ\d**z'M-JCS8>W^UX 4PЅ`Q$IZ S2\ոC<0p<74ruCǛ>cMS ^%oiոs%Zw ;q-d2:FN#AE=*-c (jPU0f1vm)a6jin=÷8={QhXۀj3F mt{Ї>LOO ;N ְ\Aa9n^h/{$eQWW7/`ttz.R֬YsNX,ƥ^#!8P}/nwlzɼ,Ŋ%ȼ jCMDM@:9Frx`Ax.G(%ބzQM5{ A/Zir~'ޞYvJhH*Rs("&bg|~2&O0h$2P9u-$h&VRQgcpTҲ)+YBMTwIP\\?3 8HMB58XOSiG(fhaD1S}_|ɜ󿥥~x(jJ|>kjnw~k9|ԧ7 alNy?˷}O{dѢnV }!ΟX4GSPpwРXg^|EOB)yJy΃P p#jrjzDzjE%27sl(ѽ3(^8EQ܉rZA8vz6m7(6!\=je*b=γV$v;_@Ցv̋,l F$k32X"![ҳEQER9vZ<&"BL9U2FZ L\RJ SmHձ;7OA8/hJI?#Vu=BI`9NLs|3lj|<:q4oiZÇ.xw:|on\uK  |d?11Ȓp64 tEUh:Q͜n5yyRt2-;*,ﯪBʯu\h#pRkF/V"Z3z n9KB,w jb 6#厑vwXϏ5޼)I׿~Znxx}5melt~֯?EqJx{9ٟA/eAkj"˃xw}~ /Ka_YL>zbTV]9><;]t2x!KQf&PB .ykYyvHϣ/j>9ͺB` IƠ\M;拜M)ա/cb0~IhBQ(XwÌ%_ñRBUrMcCe]&=ێ У+sf,H w8叠D{КAY}ykb^3YJY?+~HfpRuwg>-l].c)"UX(>y"uiZonJyatiD\ϛ."Td Sh;P{[eJ3UjxAӌ!O5ueQ>wLAIϏUiLUV-*T: ɏwm&{΋6S(MՏϩB x@}-sNJ[(ǿ=UQm$g,q֜wٲ2ᔫE[ yLEP*ECVvF@J3(cǹw("xeWUsfqwȈ>]qp mox׽B3m*?B\"x-cE#6ܺ_>-/| De*ױ}_Ω 7HsJfNi 7 Z?؀ZSsRs-qv. CH;trUBZњEMGi?~5$]}r׬:jm$|=>O%([aX2 Sإ2VP<ǥkn7 D!ҋ{x|^h$ꌖJ4¨B-,O֔ux;e;q\M _h:~ƮbavIxqr Vw3_8,3|~e~vr|Ke?Vo {5寻.%$TʮsNC1F((K 0f&=BB5B, W6?$//Tv`)X~_W 4u(J\D A+bҩWi*d+_9Oմ`8E<{yGyO<grr۶ud2IGg'w7|hU ,0gvqyEc*V5N}S 8TUU)d&}E)^UEvHV#{[6Qu>sk^_VJ$N@ЄR]; t9u?(_k8EB"ZG\3ɦE1(^4N,:%w h7 tM.VYQ*@FP=Tge8Y,Wqۅ)Xwi. \L5 Xypi΋4qa  PpHB(QoW!PCZ]4fFach5:K*UOW,Nib~L a(( ;SL&hw@&,O ? SrChHF884#MQm?$5kSpԼc;LL}n keK:{tTҴ6#V%3vOx%[7oy+~ab1bH۷祗:<8y >n涆U4>غgs}|d;%_S)d.RTRZE1ep|Ӵee5Ǩ s*llg[rV'h G<2E)S"!U;(87uĹvQ7Olv% Q:^y?fdy޲w]D@_Xk8B"&:oqoZP}"HǞ$^i%TKH'ul\cus/]@Zc֪tn,Amx(poV^v-ٷCz{xxS#=]>BPu<],qDz"8dUgXqUk2PO]fsӐu*q~~?5M ʹ:I岽ePVIf?Ca q ?J>up޿>}tRLoV'NpQ>Ҷ#JB5ĉ5p8L4:B!Oӳ9-M3ϐBa%Qqہ_r^߸j)a"Rk˖.!MU^fu -;eEj'IoV` Pm,ץ^7_pES'sQ#9EU)^z%/k=Il1Q,`[c]Wwzs$f_zf^Ղݵ]dž_#/`U*g~n׾ΪU]8|K;Ob,~7~RvB3^n<ꬋ9U!1(S,=~D~_)x` 'vĉ %sxR;5a ׈ H.eIuT}F|*%65]_S.H430T{!~r"1.lje]Pf/خGu(ǡ8X(:-DEBA JTZ P1uQ=@9O/{ǟ{_^x9cn2_I`]R4>wQV_y xۈD"1SO=EZZZRVkq}ϲ6yf.pU uK|JŒCM4K Ejib [I&c`yP2P.9iD ͏Q+D.% Wx a=WusN&eWFT 6P) "(-4?CCC|ÍE59[ȷ }!Ʒ8xᅡt"8xVoN|oZJ;3(ͯ;#'(N`Uy%X,6fλUh>' `F[BQ I,ECs]FDhO>'62H_{.C"%?K.یR.Ҹ|Ɔyä/-o"V(O'N|0Q<4'^dzK4d" e T_FSF~2C~baPgp/ղ!̵ ,IZW]<Kka*ds=;m]eׅi5QլN2 <ذ !!;/0oy+HY]w_ٱcy{f , RunkXɻ˞[1L4Q V3[+ %;xUAfbh_H M ]D"#<Ɗ%R% nbdT,YJLʸ475_ΗNwkj?jiH/}׿1oSn|kMFK?-c~/|ھ};5Y|`'9>l_/{Y@k/wwb'M\н< cIO2?D> ACR&1DF D fk", XUqoa'G$3h\F)GQҝm&9rb&ukL߂T]C(*V]tmXKh<ׯebvu?мg'WOno#t ?_U烋ϧՈ1l9Z!wPvƥDU@Xѫ/M]|c':0r'O2\#=LW\7JkkljD"\s5W\+_JoΘz{56r~-\PϻI+yƲM5 ~uѵZZ*)ju޶,H)jVVsp)\  K _sǙ<}&ESh?1\溰Zছ~`իW_~ޮΫ_ĉ|PJZ/ɊkN~ʪs57"J|%gLm󮿐 7]?0_6P@3[Ndx{?ao@#}!% ;2^PYZCE}k>|a϶{ f-NURhE-?4@K@xe;hm]I5Sqm?Eko#LjitAL6ne'QC1OuofSJɸUT)S="kkᧃx47a0JAM$ .r!p9۶msQ,y<)GJ *@Z 6Ď|?RzөVlH%~ۊ[Rѯ+!T'jKMUߎt ~L07,e omrf! 3cx?~D nv1s$ >̇?aqvI,%NWGqDUTVgE&RQ  5QDU],fye{'(1C'jjD ]QR9umԔKdHj5=$V٥zBEc2Gr®cqy۰mge+f.xoqwp.cttOE~ ¬? @rLRPRN4;( ٔ{2_v1-˗OE:[#U!( =;FJN-sV'0J8P̄2Y̳xz+_DAnrVL4}U=Rv$ ~PބXf@M=ud —]QB?<&r$"lWҾfFɳV@vx#lOQ+nk%8p|ɏ3r$#~vUL?FEbayĴ)%vvxc59Kě%Oeh:d՘G^;U kDB\#')g!OcYH$tm PI(Nf֥$4 (OWoG#]<\(BD1%-6;CcǹkQ,,k } s[x)BlKl-ַrcQ.yG9ze s=N#NTձ--?PcIBQche2uSǸ:+)V5M%HFiHE "!dXGf.AGx=lfPbXEӜx'nEu| a _uC7˻s|{Bz vΊ5ܒ&CB^4;F8Pжr9 5uN@应 9ƁG"34̒7ӽ<͍D39ǶTc_:#,1,ѩc!Nn /8=@EEX6Ipg*sgKoBR&A)C53r$ui\ۡ0! AâN<f$+Vv7|gxKJu[$yG:^S>;˨]BAЬEВ6M#ݟJ7.jB!hE8B| _´Aw^uijRNcf<`誂&*i*^|=uky۪㱁>症m"JK,BO:β$Z6&hLGTO y9! 6@mxɧZ ϚDh%4J B:E<;W+C!ˣ=;?ͮ]?E~3Js:iq6ZS gpQQRM1ұU4-%؀#JFz/ lň\e3r$1VBK|BIN+Q5RW;$NI͍\w}?'Ynk^IE!$aZzD[OUehB!;t)=u ˲4qb޽S $" \kaSՑFQ-1[SJ Iz>NXu,ht*Ess3tttAKK tzA||Kod5=DhQ,k\|w/'|1܉]$o]B]g4Wñ,J,I&8K?D(cՕyj-M>ae9&pm?هzZPF7%GH\/F5U eWȠRʪJ˃"0Q<_)Xt4|h5#38BCO'Nb6wלGNP39k $U|׈1"! hIEijlKe]!Q)ETVt}tQB޵4I{P[nHw\5f-^f/YܱQV^{!k}um{ dRb&Ca|}|a7CKi_dK3Xta i6w=wP `,X#~c lCz7H{x_X"F)<ך,kce_G(H3=c'S%_Y( F8R䐞ım f\U>W;ٚ]]g fI)񤯃" (Ѣǘp,U|{h4Mcbb ~qīD,oDh-Bu}[/Uu.ItL1b/ȽH!V hM8pVf*A()*a辤!E"nbQwrO;?}MX,]wlٲd29T{ K9gR!97.CGP_0z߃0T'9^QteD:Y5no0՝l~Gh[wcY(@P089ة>?"#NlibWҲl ɖ&B<U.Q96hP?ӏcٜs}?eKBO?\Йvp"F(Ti˸,FJD·O(lwBs_4ȍ1~WqhY)"igN2z:첅cz$Q妈BWu c D &s>c'kDKCu#ȯ,<<)i3b .3qݱV3L\8M[$k[t-JfbnRbL\Pt吵H 8\𛝧?O8^z{{q2u&giJ&!`5Ú958Ƀ[ہ)u&eGe͍(nnS110ȱv2|Vֿ:7K+3Y?k9,&q,3/{粭,{ի8uH:Z*z!ķ%`V5@EUN[9$BqT=hnP(LŰ\/pɾIT`I?pepCtNSh&FOH.0"!4C'FƷda$cc<3;X9?H]FjNuAV啄UtY/˜SɈ=W~ʿo 2ܻ %VW5͗ɛ)&Tq4ο==`z6Ě*z23(⦎Z-ZNbJf*5NDQ )(xD**%YDᶫs:>K_2_VPewSrɩzj-';t|\PUHT/l]II9'76/Pp| ~i?ɖ&t-VHxcD"($XVUZ(edq,;^dCYsMxq;kop%_"_+_YPo9 (B(hAalf:YL3J0Ҥt] [&U!Y,[?CTuqB12ãX"t ш_T0Y\z=րk9@v?AfL$LҤGI&aECW|[zmbyei8M#kK}O'ˏ]xˢ рz/C\-vG< +-}g@lKudNMVeV=%WJ}KlnnbU}D*85`ih>xi 1HDEqW?W|3I*b׮]۷[V /,8rccclذaZӨef-,Kz.+>ab;eR.?g)Lp,( 0xxUY{4txDS#ZȜwU%7( }/a ׳W`D(r3)1,`aS_w6 " r17ĤES<}D[+qhg U(KYPU(Ut TµmJK~lKKAc1*zr~.̣f;m8euofL SxvR LGs"@LQ+:U'}VcI=Ws4IO(ENs+ m}V VozHʞKox4s.ٱMV< Dՠ{`gLUA- iԃ:s`sKtdR35VU-XM{^)hV%k.H-M!dx>~s֭;|~.l+7o &(Jo?gw'eFg8v{X-7Xd)mF1 mFNu,b?v={sN=XGmFM]_%۶]kn/EQhjjoy;hgйT R.JÏy鎶)sfU8'38MES)fLc'OGC&oTk30cQMA)erp8浴Y2CXilbG>D]Oo%J!74ĺފ cXn\Jc"_*gx]JJC^ ӤG0\_g9<=_칼i R$T׷;VMU`aJ)•8=mˑfKqA^]-6 ͠GEZ<"у|txSjڌ8YeK h, ڔ{LNҠV\Dž8VgsjNK洺Q|yH\拺hi_~b7_z'&yӅK4*ɻKR1~c Z"!3,@c:l.YLs%K#bũEB skY} U+%&z6{wo7'> vX( F^Y z, /魯fцusƬʖ{rcX"{xA6|]5( ɶ q5k3 8$psyb}C'}e*\fL}/&?K3O9ap~;)jk ?ދ3 ;L&_"G Ah^G~OqlH]vHat TƵmrr6绅-NC)_s]cH%ɽHϙ-Etg,ל#Ƕ  ~ꑣ'8}9z Bё8nZ%~ {qmట4Ua?w5\LRn>#Niڸkz1"H:KiRzpB+*bI7Ӭt=>dZS幒#47(K$6%^#t;B[7r |3T(W@2YB2aP6NOhopXHHcq[`!Zܷf)q.CA=.JtU_IHs^\Eiݱ,ݸ5W_0'ȏBU |mrp$VȮ{q7oĂhCj>Z+e탙f$B1arpXs;4,_+hgQ˶ҵy3u?y \n=(D@3jlU3äH4JhM..6F4k|yp'G6wÚF\U%\T=VL]إ"ry?'m ЧO*Yy"Y\8*\/D+i]TGFz$JN%z^HIeXG:xf,Jt[ -#2Y\Sq)˩0=Xh3bH0O_Ȗ\kP% k{{]RT%M&dDgSzJ'j"I= Z|znvͦMD}{?>J(,$PkXnxM#^Pul'L P氋%^A| nH:Y]_h*?5`ˌ'xZCndeqg8v/` ^CJ"lk|ưE3;_#!Ni`KUUqs.w!SȍL0~ɾA,2TjRJJ<zqLu7]CUP7O֘Ocs?:z/~dP iLSp&̮'MP h*gS̰EFnoh\Eav>+#uVuj+鳭'!iB<6rںb7†~8 K 4FDt4aEE7*`x\tq#ߎ\;avjcaF(ҥ #]pYP, aBiD4T5Ʌb6R6}~wogI5Ȁou.r^"~1s[YFNP+iZTi48%hmo˳$z FRI E!T'yPU@RɁnݡ'Sb=8s ߏӶnKz54P:GCc;A5PFv󕮵Iu&$4Se ǟyІ'TBkdMc u 4 MxRbEǦ`,mX'=b V44sA['5+ gOQ, 5м,"r?,p\-+жzTq]'B#-O EJ, #cdGP*?6Ω^bݿ ݛg)d{j#͚"oDV Zg(H=PCεPk BG]S9q1nXL-P'G{b)L%P!Q n*!-Ti Rd, ŀE1֦yxQ&d5֋PC5׈FP&ٱkvI&([-u,Ak 8͎a A . 7c]f GpnQ{*<<;H&bI ѕ@OJ\cp#g~Wp&3AkJrry(N:e3X"/0x(buբh="+׶R5J|hժ`=8-kVS/йWB3M<)(rɏRdpe&N.uPrvT _S+fQh S (3as9jkZM['9>|P}7|U$y~fz>vCGX~VxU0HT ).(eyR la|Wnd12mkH6S9TW,ܳK<ЄARvGRO4"B՜@ 8vY%+ K޵n9eGf3}\kWt,aZ"5t(ѐm7~oJӫJex;A~opy4Շ #JbP`\{Xĥ0/c]DQ|c͚8L~TOj0Yr*c٤Z+u#^{%[:_Z<)T ȍQr$ʕ{(tV>˳k0cQO ;0Dnx8NjDDSD"i&O0qb/j #݁▨_Mf߹Vκt#)eX)Pg გzUꦖ]bhw$RMgd|F.Oݖ EM {y; 5X>#^cY>.˽fF`١a95"W,%J`D|뮔1~?ӔKFzzi)`ijY@PوVGHBkQ.. 鼭{~w<0p-eK|F"0q#{N]yowmZE%c4օȔ,\$.*=;ᣯYo=QJ svK_&u{=_8 m4X9xZ :+O2 B{r˭)H$n9h? }n6^(=[on^can_)F~lb&P}1z6eiliƌEGvdh*2<P TE DS h!Ԯ=dFƈ2q4 Kp'(*mOdg t4l+g<'ULm9 *|p#=CRG1r j4Lj~&*X:o[Rk,oK[~\f u)\frpEUeʹ<#G#A)g ft_li5DRIMómL'9=1Ia|dr)(F =E>Pgd5g Ӣ]zWLG$ sah)Z +o۔=M59ui?H,q1u|.w}vFEbyi?]m80:INش7E RZ5x]| șpzoA!?Ϊ J`Meoݵs3snPbOoohjj梋.ĉ1=Qعއ'@ϦSăPTxC'(xi~vQfʹǷ 3~cu _OݒN" )Yu+(NR$/LY{5G'$5XھXjJtvh)gXIC*EStzԏ_Mţas>`U-!C *C'WUQ5v夤>~u8m+NGO=QU/^K#^֕'&9s|~,aPlG145( Q<9Be!DT CQ0|-{oU"AS(9.\֖6B( ټXY2CוdRI7m!k;q<#亯n}̝>,vvQ  @")J(Y,ɖ#N⼜$~8'dV*EPށw~ܙ.)[S>s,2| ]-p{$D7QN+zEM¬x'3订5iK29,.to8Պ߹LYJ X~RKv.D8t,D[ǶM'nȕMcA+;;EM}wo^ P(\Xb)܍7^Lӱ0 VP( x-KndSib1u[eDih DIʐ&t4z5l&15 p=r `e޻Z[ e y".wysZX !MC)Bk#,MаS 9xM+mnNvqz_?@Nw ,b%.*Jw]⾆YJELJ/`ږ\&.)7JYy$Q@elL[eY\2I:/͎]qJK y+ޞ+xlQ*j*X43(Zjkw.+vZe\X,rvƲL #Oeqt5tavn"PH-kZ̤[IltR|NӲw5k+x H#:G&*, x8Ae.+/r` n<@+E%|.>յT57bxlHU͈)xbVi4ˠ&_Fmx+Xc8̋XROfŽW78Y 帒[ağ^=_Qm[U)XE6ۖs*|:g'wMׇK]z9DPS,2ZeP\"=CTֻ~)99Jej K]H PQ0Yȴ\QzjBTF'_P1LIp+ H툾k:动뚂m.r+~W"F*[Mk7T"&̴0 d,r"wЇC!u!CB}#,#[.練űN 8n9qQ>RgE*>+ʲf[˽/Im-0M&P]U~fgsH?%z_?l!W9]5#گ_Z.7ufr6rf ({4NYѿXuC%J\J,`ۦ(e 59OlNs)|R^Z9HtrIDATlh$*%:, tWYu"t>`s-0Ys!r1XQ 磌}a嘿Cĩ+ n-&:Y ֨pݤ(T{}xUW9("ÃGQl`;yRK%1w)1|E=9 0z $Q~-GCRU^zZrG_ 2l򖱢U YSa{Gڋtµxfm6O3LR4i2`a "b2z>KNrYu h T]Xh/1 Bq)=TTXc~>Ώ-E1$G k^pDEq ճem `!(!J3OŞ]|v~$o*7ul[ߔz0;q長܋- Tmhch[Avؖ;C%g vDٔ&q|T1q =iw۾T渳%qÊ5%>&с14W+n_ϧb1viS',~+EEj}~|`Y]waW=mkˊ.& >NESHy~HMR~-:ԯ^&pvݯAYܼeJ d]Q̮PZc3N't"2 6 :Õ:i>bu.DFsm^z'77ฆ9"U)&i@PkHģ l/5ϟ̟|#n^|ΤރlIL.Wva.e- 9+sIߞ=H2h^\AzԶ1({CO^@cj7m23.bai@O4$M{$\=yl<ə^Dic *2 3E*d]z6G!%Mb 8ԩ+߳݅'u}[Af!~z}0*ђ'-<|Pt"ZVn$lEEˍ_u!-={MD>O2jlcLjla{}ӊ(<ٽ E92;v U ?8^_pqKQЙ8 A !'m"./'. "$_L?fSbE*,bf'W^SEG؋%cbR*>SZk<|55_b, uY$wpk AY%@`"yӤ`]<6wU/rʀnD6ari" MF2FU/ςmRk@qU1\{?YCc``LVgSmMn *Vdqz .iw#,diHj8%c37tS ?z?moC(ݞ]Uּ+VIDI2L$VAILqǿ78άʐY 5_'#X:S &O P<^0:9@*<^*4/"v{Y9eI ̤G[#˰nLS0J>۶|zMVp?}WǑ>|}lFOC,XK%~[!xn3IiqXi$dbunhțs 5~ m 9lՙq#P]W5E4duJFp-u5r2Փ|bkw% EDžWQ m͙dL@G+*X٢k?;9:l "QOU6x1CWtE7R`R`u+f_*aH&Tؼ?6RXZb%b ]Gl(,):b?0s̍᫫vT+nʰl4N>Fhj"e 'fFؖKGH.rD:<ͭiY][6lAH4n5 9WdnHN֪ʥ]skQ( 5>?/JeGP ϱI3qyaS㜞`)#Rɣ߶[~و&$A%aG}3 &1ӳX\2M>#6=6gR?A5M\wy"|fX/η( 4i~*]ZYY瓋<=3Z6*tk`H\>`4df:Hg0@Wq :)]'-%~kk&ÜY%KȪH(4tlFD~ #Mt,T׵25<Ռd7/-QIK^Ɗ7'b)I}]q;avE,ZCl4+ j/~|q?{>ocKG5np0JN=hpZpsv~{8zz0.yp!  Wӯ~ndvD:uV)9t3 (pfbJ,}Ɔ xrӫ3?%11` ]s4/ _]c/"8УbJdJa#H"F:KrvH '/1y{sTkf d\4์N!!H9)c75o._#qSy kY N:Xw{ @F/P(6N%mn`$JxK#\ ߴk(Qz*)1/BE벀I6p `. r\ !4?rI\U6ۋy4kh"DSE\JEsMʳ!裣$N7ԡ!lj 5H{ZUn䴈Kq{v г&J ^/J8o%7TPs CfMM!"AB aq%xm[x0q"pe#9ԯi2p$Jr?=|gdXS<ʆO+iAo6̂Sc6(1٥"r*J]J8e>~_;mQj#5'D%lFRdFrf#wfYyZ8/`qdqCh/׸ $?$$Kbq2biE(d.7ẹ [&K dWƸL⭑A)j>v6]]G - t[û 'QD*YdZNN`/SK*ڑwMIײ+P( ).j\pmH" 6uL,ҖfdYb nB4lhYl_ 1|gI[]6$Vd$Q`*φ-뙟]å(gy ?~{`Ha[gVǚ^L]:*કV>ob*^#ݓ4Xi;tLf Vne`pӴXer0kJ SskQ)~CZq4>}|=;w4._CGOw۾Hl$H&(NݶWicQ(Z/8vgp|DG'60Wk,L'.pry a-Pk(387sSXyeQi 4'\"QDEr8ԭ |м|bV$Aī˶:Vmkَa+J*Uz%K̯m-u<fPT%"`YB5QEA7(e\l M.fillf mAFdU2ۛooBkawlk!qeYp$4LEcJ7  _&0z|xw#i"ʓ1 ښ.uanKNX( Q |]~e(!E|JLHă x*k+8vy))İңl[KJ2Yc''U ' *5v~eB v,V9y#YވI+2g,Fdcdພ9k*~WRzÙEfaٓ4ݱŹ_A4 SK$˅Dl,Af1Nf!Ff~B )2؆ۥMfH,%s\_c8u-VKa-s !BF͇X UV伺V^z5, Ӷ0 ƫي5tS%@XUQ,L /*F?ď\ o[M/JKRhbGenJq .YE 0I( rTd>VϭbL3Wj]O69981} YGc nMg,iS_u:8l҅r~ɬd:A-ֵ ETJHdUNylDYW㴒D/bi,â UQTMPG0:$=(pR3,3qfO^BլE}UӲ:iƦ9Q !TKM!?p"ZOrZ~QU^e(A0V|xIBSܲ;,EAYXDAwaTT.XW{Xf}EPXQT0m,f)rnvv*dj tCrW/~Qj%I2pIōOUmeU:XŦpuBik9\F$kNt׍%ۈ2+i+ߙv=ۏ+܄!o\+fjtV 6cu>\nQD7@#R0b cV/XщlG ݀ 4_XT"dsƆAǚJA`J6t9|,u*Z[E:]7 2 uA>zz@0}[+g'x.*dc1of+ tO@r)i^=J Q}᧪ !pagA%*K),$ZoaTŒ^S?ûfwO>y85I̬ŚP%4m=\AaE2g L&U6PB.wc ,ϼ k[OV|>KwUJqY%4Y!˒u P.@ E-jtk#OnK_9Ε9X P(3$߿xd>i[]nzj\ӀGQW"XN/۸*nS'zĢeWgEf%ۂJ}F^κzn)S \,b[D46_ ARP5*Bl%^Dw/p=~5>, H& <;X<djqt VɦL~|pGQug~fd RX།i.8L!8|XK Ïl p?ݻE9~r|[],E]}hO|l _Y Q"Σz$Bf~!K3k> >˹o>Q߱Q(D'0 :a].]>A+/NY39Ff!N||K?||[Val6W=+4xB<Ը5Th^D68!Th69^;~\wNA4Tx;hn O/9~k삀/prRӳO~oUcdbNYY ˓OSQ&+yEdc Ə#s Shdr>g|{H%Z1knY,)RYe+\G5OuV~Qz> x f&ڥ݀Fdpi;ַPɛ]wO++}߶}5f()N?zGn:lQ^}II޵AoF|"[uD`g˴'R>ÇvtTeۼvlTMȭ&x"(RcZYC']c6BL'U7>a땓sS[[W *en1 C'gx%y JقuUV{JY_U]^fl~b3s'On$>K}O;͍!B<&uGNs楣LŒؖRQwЇ Hг[׎ Yŕ$zR_W(J\x[t,g0S[#RIЭM[ ˝нi u(ƩΞLj5]kٸa 0<GyE'*Ȋ@0AQg*ݱ87A`Zg Ȋ'>ƶ(6{d'!ۆl^g)az!lLAS (&BR&|`R&Pb%r-%]b1/QD1X=T,Ix ͥMdxȪ_hkS $/ I o կ1$r/χ܄WS%(D{CUAb)1}V.\o]KC]G%ҳ᫋?~y$ݟzwЏmY9Go}m|^t,E%"> KAI`3-,Iv>pXxfC>fq` fwS `Z6$o_8ɹ)VTJ:99y'nZNPl8(䏩p~azg@ 2Ի7H87772]ptlIT+=NJHZۦwadd>G5456"I󌍍 #Ir \T$|yh Ex/۶ 5jb,>E:WXu̕9/"I"5!"#ȲGSz2QA>oFD /H,0{d'!MU|N>;\F#G,et,F>op#2?v?lW{m=rMא\{zpkٱY6iVeDA`1fv1bQRD@R.e]Kz^@A7tjBȚNýtVTƚMEipraRun?!!&E@Y*u{VdtB$*5OۛZ´L$kT'V%66xa"i\!{Yv-eda6KYU"tA7M ب'aX(D{ݷ\,bu,9 eLى(d2,N,-jyщE7tBWM4uP_c#CM \)QdEeTkf ,BDdms"|km˱[]pǀׁr\4-bH@DFE)ǖ2:M,IdS$veMxkA|kTEr\.=Dgpp|>O:{-{] di m}554V8?kyyt av4UU:ʸ4 [DJ\ni\.,]LLySiĭ*,a&͆5Tg Dh5466i :ut^?KvƬ"˯ii#g.AvTY3olEDl ,`t6uw,^8ȃ[ۈ7Tb4"q--Jg~8|8S JU<~M0$ҿ8 FbK E9f""$4EFR]`ɏqe~.u yz d uo;a.Yg}|ww;[WfΏ0- kPjxe.LZ`0/d [S7{T*UvMOm Z2g&yn Dyn9QbӴ0-puj$k;:/3/Ieye߃!?~ǹ{G;y\.r g#6!J"5v͛tnG2Ksk;u~'8طiZx4ή:]׀$:$׍!겋]qmj>cwuGnU)`-c-.E"hQAX( ⩗OQM~;-˦wο|fƦ9sM ;64*:VD4tDSKLetF[8‡Z6PY`fSu@<%2N1N1,I3{NOt#Nn[LDT7e*"Zܳ/_ëgxKNjnHSϖ/X-?-!DQwس.x1DQDexWDLl۩-3dOUQ%y]y l_KuG0Y;5UY+O?G4$I"ʫ.46Yp (+bc/O_k.;2Mi4UrRYR7~5mtPr[I9kclZj|T}T}7tT'w׶5?;rFر⮽ubLyy$I;#h (3)F2L߅3z PW#g$m;6p;/=ub 4U*%(t"2"c3Qs}mU^ h6,at˄5/g833AS0 IDZ)N+[~t:]NwM,*\n"nxC(-%Gw5SpdvuVV%~tl*5L߅( 2{UVDaZ;YUl ;TUU)$SԯaٶM'gȒX^YM4I $t%u,AAwG>J^7%K"nn88B [rǞ=r:|P(cU{| Yp/(rW[=yfi bͬ*0Ka|.ƲivHe 2y29}}Ѵl; زm A&ɑ$929G ZV E(HUGȯ9x1Avb&[>2[kn ?^UGtнXMfpjCN7z*+Cr1dEҙ鿼Ⱥu]A4… 5TA&S ΓMپk4dYDUe}(8K8$Y"]$}$d3cNvt'(ѸKH@31ܠ#Ɔ98:̑񫜙qdJ1 U%pUyREd;#?uNwoP(yd2f?ĮL^E>r[#[Z4Wy0T\'d &قKq/;_=l{#/o&hU9J7,LL@׋&+4C<yh UAA ΁ nLmExt&ݨB!O'GΦ1lWUX Y ko{%G_~E6֮pQʉdnbi*F瘚SQqqgq< A,e>b1&Α5E#5*NV.j ͬmrs :Ld&O4a!f!"̒:uI x4VYZC[}n|tضi_STlEzGI$sMuijo]=CC}5m<C7\bj*΅K>_GfrJkk+ :g&xn09/RY%pkg3_~(}  OnI<z*˧1;Btr|&Kb>JCC)P`:7WI UptvP2Igg'I׋G UH"i3I^+TuMlmpjmv0sMp5'| ' Ų~bK\F͋_Qxpl`]MloHH[7/Β+3B^ôhsKN@l*d49&[~GU,3u޼:\*WUGi@ϧ6`<`nTE⍓d2Yi٤yj+4׆^-4 }QQ@UPn$I0 C-m= ;luCG.(]@,FgX}M$t&b)&q˰Ζj\|KeY6}cΑ(Oۅ(eKҲ,t]'ϓIg9Y. NNmES$'O⍫4L$b. "i\ } %+@k8BpthzPMUHcSc#Չu"N `LX?|ggFUTgffqilhA$ [Cؚp i27?O[ ~!>q=,`6ěG)l.Kti m5Fny˂ 099elزH$iNCm/? ;Al6U(`p|W&U뻩AӴ")=W:f||CLӆ߻~Ĵ!"W8qev65W([(111A8drh2מ;F4'ѶvnۅȲx]]HՉb x.qӱEq_; c]:wP`qq!1m-uMJx˰e6.\G>m$\.ǥKbiS77 A Hpt4mF;Q\2响͕Yy$MlX˵|WM<'J) 6 nDA`"dr+a[K)eV֏/6h^E$[P]K} An=-!,fl!MmM(TWWSQQ+WsǨٴPgcSo1-p]TUUUUjyD/5!2io_Ceee͢5a6iߏ륵U;mLNL`fr*ηw~pLJGe`ӳb^U\.Z[왳|'hb&KSUUL+VF޴7M n31BFIrMtX$˜ps_ñ~a%9SV18`Y&uu6- se2BJ/kj|^°,fNl-97CߛD>[W0Dεk0<]aOOG 0qn~up|TUERED ~9\ N(DQ&&&zvMZ #Ik֬)Mrfۖ-Uqf3bG%sMf97Egz4mŘܑHw$U8?IЧQ P[:'pedXdڵM]?,..Z"h.8?;|5r2|{E֡zCs$ȵs@2o>L={.[7;mHDgZvcxjiH<2mmeުS<<]]]BɔcjjVl[o'p.̂ESS ---e7}62bۺyc:ߝ:ʷϝc4d(Ұ5[~5A`bbX,-Nɛ lἺv[o E%\fe} -3,n/!|&ptZ*L4=-:-Um㫯]ȢH4]`gG-4߭Tֲ8:1x"FGG`d2$Ix<'&H..TI"o'af=ϊm۸\.6n#G!>lXCmEN  WF*"d0MY|< F OMd -0MQ֬q0 àP(:i$I((Ȳ,$IqkM:C7L?pڂi+Ω뺃ETUE4\. eW,ӱ gO2: p ,zǗصEannT*E>i,'+J{{;sHbEi/k;6( 1i@]_ Fz991N]86"G]H$dYlFQ~?@`]vٸqNôlTQ%A$Q,n&~?Xt:M6-[1xdE3:2nTVVf{poryG>}x/+q&˲ʿWUG.4MDQ6LE3~rٳd2J) 0---TWW߄!lQodjy&y%c8>q UfAxpb{{;W. iLAL$+"XuM$ϳk.Ya\+Wg8yeAtqi<׋(r9m:np+**ÌO-SS$<}3 t~B떪H @UU"Hd&Fb&C,8fDlt/m]@a]ge}"l.K۩{bww)2Y~ &mM<zV (). so x>|>IGH&}˃:Ibed4#SSSby!yzzzkؔ`br|>O0d|bX6oºimm@ ,A3griزe 5sI\ܽKa2ENN6SSSҹv-a^/"b9d,,,pud;o}壹`0HEEelBeKK:ϟֲiӦ( ٨3_nwfyar9fgg9w'Ndll͛7SYYY~ITVV`Z Wر};k֬)[.;kY=|uB7Y L$]rGR _c1ǎvCmM nQ) DQz{{9;:tuZV$QSSʰplcXȾ{YIuUU\Jliʕ+ SYUE6gss=12L膉^pjME_KƮ~e k x&KO'n 'w4Y$1,߹o /]L.e2%z3t$Qv5Av~;O}8qol#P>#/12DOOYގ,MTR>v#GxlڴWtl@k}w^> rBVnɚ$)}-[5KfZ'>ƞݻVL累u(F={89w[n-]n7d˲qWyx/PG?{)WiD"֭[Ǿ}xg9~8vUUUضa(===w}+vJj]Hps6:.Np8w}7?山)@ @KK w7^P(UV8b(s.^ɎWI{o=m۶?ea~vvۏr\[$od:?=ϡW/"Qz'D{gn $Ircb*ŵfHgT5E]g|>o]A^QtZZ&I;osl9Qz.E '<9d/zJ4ܶm?͛njX߿[.nNGVgcM;zZ9|ax|խx-*]}>LMME4,ID5dOs%7?9(K/Һb3*n>ǫ𻩉pi s0%zUZXp;3m(O>C} {Z[Ƞ*"s1WžSh6T v{xEoiiӧO344a׳m6 vǎӟ$!+ d\' J|Ri;6V_v`crrl.Wviʊ4M6lΩShhhp\RIB7zg974g?y6mXVަi+Wbx46nX^/'>2Fww7(ٹ9xiR[[K(AiF5y($ 4UϱNDz*'&p33Ű@OOky睌OLpY*++qJ@^hw.um8T wr Äa @ ,kiL"+LJZH,COQD ֲX+OpY-?ߙ˿Fw:?;# L'|q;z:7-oD",}"=v8?s<}k:QEIUIPҸXnE L/v(EV/(w8\ I]S;zԔ֭[y#;>gx?} ^w Ixٿ o>N<(ER?n,Y#,s9ΙL(!2N90!8u|`Z1Y\\o+Ɨ}>v﹃{e;vpQ,f1晷.k^6o\L&ó? (g<O}STVT`Y}]ɦMP{3Ζ0/  Y_?`?PoccMJ~DA(g}:ab), |bO 'N*A&&&( wڵ</_橧"5x].䈈ZBI*"@߼\@4-2y4oi9ŭjMg?K(7o|;{]/oU^-[Z1FO{m+ַb466N oxxg}HE۷m+۶m_VWdiUH(LMMo} Y4i6hP)ݤTj6s;!/$V{ٳT*WVݼK,,,%ݭt4VgO`l|p8\vgGFGI&ڵ%&sv橭S xy]J2gn);ycOY˗/˲ػwo:\Q[Kee%eQ[[˚ijjr֯ 0Ma2+s!I[Qi,CYFyd?+^Lόa"jM@{L~ kuIx]E'}}*Mş]ܵFuEУ02OI ?r(RLM堇uXnbMxp~A7U eY:s BH$ 8[kvÛ ]^:;yIRD" pێ+baǏc||VUU.b DW^!Nz<ဧL|=_X4Euvڅi[ܹu Oܳ=\xkv9ne7/#I>|$ ۺNw]QM0$\W![~cUy: tuuȸVUU*+p8LCC `Xmx5.EP(\;E$瑋T.Uaz!N,S,^,,lܰ,TWWˡdY.r' r9ICKXת9.S~VQi80ϲ1ì}]Ug{c?}6d&͕N-xFh?淾eY[.4^#us, " H]BQ˛ϴ7Me/HReS]Gc%^_UU# d2444PWWW^ຮ308(ՑNW-Ǚ.bJJ4M#JNl}@qeV.*)G b*: M.ʋS_z:v(k⡫dY)?Mc)-*&{̴>~$I* gggYZZO2\q]˅e98?:u ]יec{-ɻ%IZk=ksC3Pt06֬)ω $S)TVVL&oiՑ1rh__?[+ٿ}-G/M7ݻYΞ9[hjjs-~5H<9B a*_KX| H:@cKgSK'fgMƈ ~xtt mۦ-[ߜmH$SOFٵk.Z6lo]@ַ]b1-|@<.Kiiq!pgVeRH"SW`aaa"xhmiҡYMV#*Pp*|~A*^\EQUAX/ˬDUURGYaPYKNNM/G.3D-GDtbtfLPXd3\$\I b ԩSr2E$үr :;s g?˾}8~8/\P(D]]555et477377G4[|x ~M kkӥggJ%-¶srFR9[_"Acc\|7f--- ~>RU:0-M'xnz3F5Vڳnٝm;Ӊl9v󱰸H" QQQ( zIM x=k%Iv308HvDzltz$y%KKKkX mZQ% X3ݰp"O]CozIhjjZain7\+WREȯ1K)S$eTl޸ /|.ǭrdf"s7whmia޽$Ituvh4J?ϟg`p^Z[[ioo_QJ96˔UPV뵰`v@O]9m-c@Ŷm$)\C(*DQh4 .wigMI1J+#3ljUKn@*ghbJ8̲m&bU&a )YXU[~Lcc{{ضmr,65R <|J7,lڴ3gwGSeCް0LM]]eLdnnwif!~*FDL_~SNmhmmWWW~{劋i;+qc+;*Li} x\X?U>[>:+vA ǙW^OI]LR(evdjɅ$۶mGŋ?~J6lphZZZ@Z۶ill_5OTqc6w_KX_m@ OW1h:@gMuwe:::8tkְ}v," O}ܹsرL}R]]M4SGPdM~T4 jkmCBc,R :Z8]룦u=,.\8_K)Ͳ.&0.`",ƽBi4661?3F&WpJ~r1M$ټz4 Y$Vy5` %:[HRjL2 Of͚5ϳKtԥG(6g-\j9kkkW3=="6lʕ+-1EҖrhĶm.](x<^fDQnʱ^B*Z[PiKK x?΁e߾}ڵao,>L;Z 2+4Neӊ։ieV,az{{F/ ˲X,Fgggzxy.'He7#'qm儅(D""qG6k__Qɲc6),C!dQ:%볗?SP?QmNwN*[(»(.Ӳ=Jk)aUʠK.Q(cRSS]w˗r LfՍgddb?N 04yg'_rpQˉJ(|QYXX(J#Ln:(Lvy衇H&?~h4 Ȳ8/^vd2 Et._0-fggb7x PUUŶmۨKs1Ν?~OW %,I[ 01Pȱ{}{7u2[FH ӾimR4K3{-mϞ=s1@yB8f太+FR=$I{= sAjPD)/#KkrqqB.ͺn݌Ì*,--1>>Nmm-񱏕]R䉓'52y~ CqrYEuU[3<ɓ'q݄B2]r>/Dm&ֆ\fK{->M}dŇj*IJl M.22b@CUFhYwdX2C:^Ks(|fvvlr9RL|>O$vs$3N8`Dfȳ? Ks&2D:|5^ MOw7 i`?fff{:00oU.ۻc]?Z( ؀ǣw}=64>D"[`jJHR\έkx\\f!I|FFFtvFsܶ%'&HR</2::J*qםw|fGE,##g}k TvW~˲r|z1=J*6;n#tv5K?qwܱ"H/}!.]$"nFxhlhV| ʯgsl] 216C_}0g' yiq+Nf7=p%2 ( YAڵLLLo344L!d.&b[lܸ}lڴ2l1 ("~Z6tD"ImK638ǭy}|@UU`brX,VE~>Q~$ׁKQ-eJY`;or7L1rJ &3pŏ j"[%%3Η~`C} oqZZZ6q.2 NjTWWs]wO|BeYe7i Nen͍b6Td/YAe/]_bow D$qm]#K4O?4>-==NWkBQ6l Agp`z1Q:<̑GQCU؇3MV$oQ,N5?w l`b6̢Õ xƪ -lX/.sߎ54V +Ꮮ7>TCpwdeLF4/bS{-niۻVK@ z9JpG;zhZHq^dK{]mߝ+KG^+wf>yLʌv#XXOIǸD`$Ξv&9DaֹDԑ3{)WVX.L34#md<ey]K?')LkFͤvn7撕2#*|zn{j.ZQ%\{bb'6n$ҢY}>$ﱱnI^>',&'?'NOW'2fy4uaZTTD*G2668Ncfe{2m6\.n>]ܾcyagoLoXh.rA@D=>aQn!I&099hFuEzJv L 4v#OWJ  JLYgl(߀Ὤ\F:& NnLLN200H{{ǎHV9p{[4qi%U458d" DHgSO?ã?Fd*fa`4T(32 /lá#Ģ1E54{F"z2G1,"K1$cRuhattNcR/x9O봱e(`6InGEƙ- nqb9p,1<^=34< &XLPadQ`RVԕɦ;4̙59Mt /S,߃aeoSNrA)vtpاluX,Gر&r=Ƭ0c(- زc7FEQfwe;;;9H__/ )\mhm'H ,lZi455ƍ3(bAxoC]^/eBC,\_]Euvvb6)j$[.dYm!ajO8Cu.'XJ?#FWOh[.`" v7IGT[U!180$Ot2(D m}y. wz Kk K?}o2e">+ԖHW.!ln\GIǷ76L2 |XO̿p:§}a՘;1LK,baU5y g A  uIܸ"/Cޭ N1;]CIdnqU_⮶ت>WsXKSNG}_fG>ez|G.Xv_nm,.grkMu|7 h[FS,%οo4` Y?8GM4/[x݆,,en A9z*ut hFӍ=fTF&4 R_WLωHbZIxFx.+7M0^X1DCc0>'oh) Q_ܢTJs` 4r3Sگ2 ::xT*nA9#`8&bȳ¯S-3]c!6oexsv3ɿB:29}pK?hn0<@gjfy] 8}|佷~*.:>yحfJ=U[R΂|4r۵k+bRfy&b3y6e3VZ*.X^ͅ˫(u׀ȶps v?XH$ Z!p!+HEΪb5ܮg tr\Kύ1dD~e=gi8?[L9m?pI@fH$`$>8VDGo@*D/0;FǩsVO[J8dL Sj/޻7r{1G bmN.&Q!X]BW BTPUkM,Nh" tM@RÅt]'׭1s&Y$7(:l0uH%I/u#{ AȀK( 4 {frC6H05fdt?@_tݵ Jduƞ5'#`$nmffP:}18N04{.Hmmh `P^`PY$5.D*IDATKtb 1NS[! ܵ/ݟJ kJ9jgw!!{?qԐsV1YIѡfRat4DQ$ #tz[.'ZFp3*}k " \<ךּ!7jv"V$Oc(D㩌"լ!œ-A^<;8W*jX鯼R)2ᙎ% &~ige3<(~$jHج6iMu9s|8tm;wm\6dM(^ OY|w0=Mi$I' [DəbCs>Ŝ XL4rAz q^_|)|.WlFY(i^&|}&޾lGT'é~\f3cFiǭrS'H)̲D5l/:@B]y+jxC镤4tdF$j2 d*[q\lsAw^Bh,NW93{FCigUS<}NQU-3J[Ţ`:er3k$y)IY$_do^A" r=N\~Ӭ_jaXp:]NBSSLLN2:>n4̼%f|.BFm  5fLp2ԓLKH$SbBĹ Q~té_I`piJc6> H(oes`+SbKyoc*FTn)[ʯ#&yoD#SJ$qTzd5 98:r̒K]4LYWkY1! W{|n BA١mt:YH>yTX&-&(/sᰙ1bi7[8xVv v IF%ƃ4RS[PUh|CgTˆgAguj(+!zueio t_vS$P$N ˮmo׋ɤ 08U3I>k*Is}wI"C%#ŭ!B&3ո Il~vG:y%PSCfJ:ᕓi a!x@Dr#8d;d\G[K$o4LUEyk0&zKsI[2!&7(ip#j::7]d2!܊EZ>1vQ&ל{kʑ@3O lanInZT%4U]'i$4TdhC#!vLvY("x1鼚ƿ6Nݗ ުLӍX($=}\|\y% k~{XR* \6v4t%%ydsXX^:2UUM q꘿j!dTmg锉@P_>~L:|WUX3YcaA@ Fy1{&D=*7ߑ&܊lhq`(XY@, <Ce yDd ko+K.'eg> Uc +_m(++XQ|I]-A3 VYmR9/]#L%L7Ǐ[YEI{f>4kT_ (TdiF)s$VO }ϴ_C!8x#G廸Tqq0dz'ҊIlٟX5'Ss#/#wq DEF4ȱU9舢@p_>~`ՎV{ V*,ZGp@H[yS5I&)و&g;YkQ6Wq`(=u'}gBXz?w,x?ͩUo&%Pu(# );FDZK2&'B3~; ( b!+@ɱiT}}H96;~Eyup2A8j:{6krJFUNc/>5D^R =@@@8*>eLTd$UƧHU.^ B6+:#an'"zrz:5c߿ۮrںY 'Yj afdxI"D@k(/?Hl-+x^C/+ED&=ҺͅעH0IN"dot>KYCOa׳]5E:o(1$uoE%u܊jg9mS]HtGgsc,*aHI67R0f4b7Zs(':v (<B7|=Tz)Np&O"gސ֝$@yiV%']F1E9Hd6c*պsDcIaVU?˄?| Gru$ip@H_eByyutqZqt L'1093ރq= |W ll>lՂ Uc -;X^L$㡞$ 2Mҫ9je,1jnegt#{pR<& ֿ=O%WQj/,%P;FS,-7^@lu>ݝ #|}J~}=bkWu<:D}A1""nl]h*E("+$i:P7/sloL_0˴H?h2Ag-p `%!ý:>tmvrᯣBI,*݂iqXX,L7t0iP٤}t!bNTߐkNryyo26|WL&,.BHE-%;uŗ5 +фI2ㇹM\_rK= <4?7?e"QDY2{?Gk Yq[ͬ-5|f$ZGyXWhLAZF=8f._ dld >9F h.j$14ÃsDY6( /44E$IlfY.6&ShfZZE rn]9f%5b1 ?_a~|pmrpa~~?GT'i-MG $CD1i$$l:=~cVKw6=]K|Z&CXe+bm<-K`gT mGg8HɆJ̒UQ02cdB $qYe- ǣ$NG÷|2e뚑cz$gn{1/E %Rdpҙ>HY5&bhXX’"VԕⰙO&ϪԏDf!O"[J0;9J*SJTQzR6><Uj&yEA[;ڊ(ύiO"&5f+x Y= 䴞}2 Ory]?w %Wo&[HhIF㴇s[sY`rf.Nd%HfʁܷNc :LZOix4ɉv(yf?rWSKQ.*6LF$4gByn@e(^bFr7FritO鍜 R'Ǚls?ڲc0#SȒDa B,fbt2iaiMn653Qdar`&**[lU-S&y%yTNBTɳ( UK2v\/Eл(u^3?m^Va}mU]eGWud'1^v0([%3O> :g^,xOG~ߘ^]޵Sr7fDC '~GqRi/b:g% 6BZ3&<3zBr>O#t m'`F6|T[= LsOϽ{QQ+0vƆQ6>hp\ۿy/&Fu2cb&l HQj|~S4eY_Qmty.QvAQnp1[8x$ Y`%RZʭ ߊߚGk)5vL MW GX__ۮ\et]qA}%mxd5FRd$B+r=,S6,[QL25l2ύd=L?Vg fxsY$&6p 9߿7]=kޚV\ZspjG%-bǩ8E@3FrSx܊BkCm;c@F:#=ey\t<++ KY_*S呖&"U\͟\(WM&Z"tX?cDA`(2Eo(x4\P_5D9p[pJ3m9e{\!߿w3me޳ޥfusgI|_wp^%,/êX91XtYR8>g R,3I?w.7ֽr|4yuMsA(q@+(9եi,.5Q~&- GcD7r$bлi/>&$i?t5XYˀV؃& 0`^ -һ˯eU2+aꄓIVR49:½<=7\=?–<: MĥI<V:)Z&F9]=,XK{⶷EUUNM&Fhƺ \ N%1,2E/,feQ)] .WԼ^ֲrJ5-C^6l^4]7a/=\T*Dw-(P"۰z/8=u||՗xҏsc,[;8>#I 0TC_`T[=`$$zOXdARվ{EeTM2Ʊ >KtF!gf eIbAn>t $cd<+zsb~Ħ4lƃ1.ʡ>#aC,im1 @ ޽R)N0pI,tS/ $F۔$n g6> 02!>S'eJfs>Fd~z𛤴b:-<ݼe:+3joKrWu/n>ŎR>'~jȦfn Eޚ~I]<6>1Kx\TR{|RW-Bf #]v O*Xt=Yq a :!}-U^ӠcMeU45#2)qHvGZDM=ҁ8Cm\SRCÍYCQ,˘$ 93N$@ D4J_0@ZӰ:8<D0 ':<<#1I1%Yiqx1B!TsA}K*SNtK߾֪jp$d&^p#H&SzV/,sY.ә mlݏsSm)<<{!՞BZSq\ Hq]5ȢC/o>~voSku,/SB=?"x]zNj$e O簸FRӱ\`~|{זkBS5$ AHGb1=FXk4$i3Cs/ $G5yM89V;Jo K/,%,/mQf@c#l`v~C;^_fD%ɍk\V~-wSir#".gz9<4]"Vn]5ob]yK:`8$9l(! ح̸&\?=wMH&cQHAK|qݼ[H2m?=Mp6{ɱQ㝟iycXg~\NlQiɑܶ#: 1 2GU4]CD,EǾb ry}N䮆R]ȿ-|^KE*C~Ǟ]u{_IsvQc1ᰚ-s5QDAhx cPDCWI'8?? "Ƥ A i{*ȕe,NTx> RJ4BC,IfHT"ίa<ai~6YA‰VD͎,%X*x,a!Ƣ"j$wnm9R[b9,RYN"HFƈBDFi?O j1Y-R03_ t N0[Ow>i =K0 +NlEL4wpG>LBKGVjY}ɫRxw)v\&7k.dKSlU*mɇq(NT]EGeP(CdҚUTyho$ ɵ}L"\@2+dz@Ijt"٤'ӷB4bOb4y-+ؿ,`e-m0BFK0!C(򊸤ՆIȵ9N-':)x˧MF"az\\Q|>::0Gieea)v C$V'3]{AM7ˁ7iMt&m)7g}I"ɟgIiz1HDpy,xʊyj1~OKuMwtkO|U KY6UPD οpU_"ǒD|{;ey Ey='3!"p^( =ump(NLRdJAyaXs %Q!"N@Yq;'NY;1#'&SnepQSF#?>%ڿ<`x P& qUe@I.+d?$(Odd,vE l¾bQ^up`p*C1Q2qhU)s~~9> )9.e˛Nڟ;5gk'%fFav7/d 3-.dIu::z¢B~~qIqsxlk# + 0dAxAϑc% Ӄߢ#xEH :-@82&i@RQ&C5 H5X\Ul::ϡ=pWe* ! wtM0 KfxѤgd3^tU~| Cx_ʫi+c3B0S`u()ok(Wo(<ꠑ͘xcH x:.|St&휿y\663^;nQ*0x$|?q[g"@;m{fO6-m̮NRMI5Ht9]14t5l 6LI FB3#䉶?s|O8@ZH2}֐:^ZcT1GtHUΣЇE1<"<dg;_,ചye\z9|xG6m"RY^?í8EeWR[̣mkGeI>EB\&7BD<^KvXlЉ躎$H!{` NEz슓\[>с4qw$%Ad$:@·1š8|hgE#cb*B$tdIgfQ"kqs5BE> XEA L0N(XW]Q%0\j:J|:be.]WEeHȲHωi[&;&\5W\~4|v(((`6ɘM2Eepy r|4Tx"|X/::(m)wU6yuMGXE4cg3@5b$ɷ; rhxtEzL\[Q%"! F*`UJ '&Pc/R]|kIq..P2C.mV|!Rimf] 9:i%lFn(^qrIL&‚΄AǚL dB@eI4i3Swa5}IO 0)CXΜ˲j!+K+I3K IDRĒ$b1IiA67XJh*śW_,ʄSQ<tdJw lM?W܋cQ(-LZKPt@4] C\e*mCp E)wUSAu>Cbg99/e$:ġ=Y  < |VRS97aˋ1F3#0ji)W!l TdqzDvrͩ5fzKdgZV!;'ɟ)nZvd'E &Ua(D)F$q(KVg`t#%%q, gx^2{};fryTyjIqƢ,~©E8Io-:Mnj*}'7c>M4>o5 09A0h 2`LDA8Fg,#>y9+-&f c| Pوi'h NH$Ģ ],*((}b1t[ l(Q6}T9ߓ'89LK(StL0| k'>C[3R4v2ofQ4+2dJE4yrfp+OO>v?\&7jsҒ16\Fً Tj)CJtCxWtd1! ,UWQ5lî8fDҼmX$+ 86~0!#YD`I1~X^[` 6ۊcxZ[7a ٞ+2.,(ЋI1n IQd}@&U/Rr9zw0o .$A&oH0en,(fEm!>|3뮟e ƃYQUuDG<)BII\v }C,_,sA1) yx81% ~Rq"K<q5Nn'R  Msa9/DfģDSaT͘(J hQ4> %(Y2ya[,z]3 F#A\f7: %4eq ȴk:EqL.L@ɂX|Ǻ%.G>, 5LfzjC3G9 ~q/F5Ĵ(V5&h},c"jJrpZ(ue ٟ_ Ip5,E0n>!]v ݽTpG>bapx*4C2yu4u ~I%3:fEl⮇w3[ky㵕k`2?; ogw-4h'΁k`Tz.娺J4AG#  bL oF@@CcWVf~be$?9,]ױ<[z"`$fS4#A<f&Q!5u >MGDJK4##psjh3k"'r w6>`;(w|^!K)ub6Ȳ1VCD &#a&"@6LX"0۽x=nsyN~o`X1&^goXVS1~5=S021EusAǟAtr<7aAE/>I}6_Jw>Z `82@g~$Abi*~uLFɳ:kkn=pOo1 ֳ$w%DžWpOu>|\_JM2QJ05#]2G1tcIKb:PBJ^OG0*q p i:ǺF<@K8x ")X̢ӁPX"5@K(O QUQo'xXj!"ǎõW]1"[wŸ6}+YV!7_bÆN#Š̠lIC/#d8 yA:B'HhqDA| c0H}ٽ&`Z"Hptc=DcL1 8x#?1@kS$,b5)(ʋ[`xS&Y&JVL/@2iv>wc%GK/ՆG#fò* >XXLet<>~P~G> {Sǯmˏx~&Lf&u` xCtAtv.Oy`~~@ p1ppP (e`$A}X y;UEjKs(a5+ņa%H7ضg;(˖,e*!E4L$K$^رN~%NEFCk?VJ~cȇլP lj-S@Ёw? ʋz#9LQ&rm,ev2{ȷЉ{D2W.l#FIq\llŪHUNOR+lz(Ԓ}3B @i A ls?,#o` e4i3XFKlJQ\EPd"3ӻ:|  aS)kPQVʮ}|uLaJ՘dh~w3|XmfZM;jZ s EF#fibbb*J@} v^E6dXt}6KDP$ YfqMP :)̲IJyEYTF`*CbԖqw[- Mon`"`w,0~{t#c wf8ڏ"(MW3'>E. ) U 2JcbUl'XdS ->'G`HX2 Մi⶛88 62dB(d r KQ0HAش_;V7TH4#Ͼ w0^G6p$ 'F81y1f7i-EJKfC/L5d[0"b0$^^%v^C0pb`|Y9y7#Cʂ &",bEL"K躎,Ȓf$H$3Q5 MӉĒnǫP*GUcVRix2m4Hz&&ųy/aIy}=V/ j5l3BH+XUb4c/j]Y (fKg8W}z*Mt6[gڍR-11z8s/f3@̌2c$+22?w{2s 1)W aЋ1Ë-ʹv׼s2 a> Բy80<8+FRμ$f__jʼ`F(7!22!߹_?`nzX%tEXtdate:create2019-03-21T16:33:41-07:00+%tEXtdate:modify2019-03-21T16:21:05-07:00:IENDB`kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/images/kubectl-logo-small.png000066400000000000000000001731421504711711200307430ustar00rootroot00000000000000PNG  IHDRǟ^]gAMA a cHRMz&u0`:pQ<bKGD pHYs.#.#x?vtIME 4m7IDATxu$yU ó̻|CI'81Y`(91$2!;$l&Ie[@nj˼]=3w'0%gvfgWמqkii' @ Qjz lB H@P,ƀaZ___ֺ7!ğKh3BXGh-A?6=B ADa` ~sG* 7Kup$S_sa|I X mhZKf!i1hPXC X!ht`xh(-ɧH 1rCv3xh|Cpw{ T UE3\ƴCi%iYڎDj1BHi5S4!n>Gzdc۳][:S@2~ lZ5hBaj;ҵ,f@ӼeDjꐆVpVrP?җ0uɌ㑇`/Ymzʏ{1>'m 0/*ldw0|d/ Tv{`˧Zy$@c ZeZ9Z)‰:WgEϧuF+WtM\BcU[r fI)K^/'%ۤGٵl9DfjWr)Rc%a:<;c/`5mF55C)rE-~9OMs +/*I SyaCGb}ɦeaA\ a|!|!OBתkf 鏀UPu qlpC۞K֚(ҐD5Tdia*40 )  ? ,B<#U/ e =*HW׼X]V%&U) =5LA +8wv$BSל\c5ya{{XyE++Bclx;V(/#N;;x cK8 u/)kDLAf04;Ɠ?&;9L!8?3#shoWqXe8Z@լеl8gΚ\O| ?lWoH)7QVR Od3ǭ6Pd3=1pQgͦ%?`G"8<z3h(K*9IyIǓ9O~(`ƚX0~FM )oV~r ST7Zv\ǪH\ey2D+:M=ô͖5Q )ܛ!UxljR MC{J~SXKjf!-FGȦ,6 8i2y{|I][sVf) 7FB| f@3izA_Jug_# 幗 !ے?iiiy.Ugոgz?K Mfp/z+ 6\aNʬ=Ys N^}ؔkbJx;sgNAc#4v&Te#޷{wbZ6fZ. Q@zlc3͙WaKRp5t ڶ697`e)qivG*ק<{5_GFk%ZWk(|?:[ !$eM f~9a4N>8*Vvux"N3҅ 5< ]q=Ƕ8pʛғǟ!v=|/V(Lh<ɾY(Ӳ9DV>%H v3{D X Kq ejrI9w !iQ@;ASԌRѤ(Ɇ&LkRCVCc1ښFCH"&-$, ˦wV<*?O ?9@p_j7q>AXhOq ő2 %#&eOVyz\Y6yxZye%NTaҽo4v΢i}PIR=Gz#{psp 3= ~NAmhqMt,\݆0-:%Z@ώ8L"(O?V*Y>_!:V5Bs۽c X ۵;lq[ߎanI2x4\*3  Z gr<ҁ ,R-<_1p 7Hىq8I s+e/Wr`=c>Fj!e?>Cc2Gs?LՠU03 SbyӷrAyBkx PZK.>j}Ȍ25$N) M K&sq'f޶~w3y.u-m1Vw νyeݽ;2*Z A,bPTYh=X^KmۜRuC`9Fv|;Sꟺ^nʫ_h7ol)0پv2!MA' Uo'GeuJ LS"  EOh(|B6[RGs2:0e hFHAf;㻷c"R`GLQØTAz@>;=V#';_!=,׽ph^1fZC(,lÔ%[ЬPoΛ˒q _ȬK)t-^zrLSSW0LP%+mn;mKڄRN!4%NΛPC7BMrK^Eۂ W\4 q<{xѝ Oǝu9/3{t4.#ݻ8p&L$Y_RDk!0-_G /bIAg lH@%qHtQߵ=OF?MGUpU/mKV2s]w+ug& dΩp" D4`74ùuشRo{ok_U\/y#|.6#m7+f6-hsnz:pAC,?}-/! SgKhcl4&R!['>GHY״R-sVƟ X-vH? 5Te$jCyG๊t &a)#32Gd9ŭ_ ڞE}k!9۷;P!j,[_Gap{|Lj( 9M1dka)~$\;Zy|1{ݹ5O??|Y'!ZQz!0LfMDb'I/,K!$H tx-;Ѹ̷%'O{#S9P;%3ݏaiYqѵ,< ;FL|֯REƯm1z/|Ì#܌l?HY |_k6ZS,:q!pr9oL}h HCr|/KD)?[ i>VI.=D3uC>랔\G{߲`.j H+AS~0p{|R %7=OKmgN_x ac3zΔyu!7统8\!ĝ_,@p4Z_e#\/<;oGn5N>_^k ( :H4e#s7P[ٔC&U^2ϴ$^0*>0\ y盧Zy ˎoa г/Gr stVG\h[M@9~ W-F63#/gԟRD-?fOƦ\@|?T0#p4 xXyKO|6MOd_b$-=#!%Gv|CJȆT6 t%˓?c3SmsEj4G(l"(srWsػmw]0[ KއH#DJ .;!TܤYrYq}\{ߎ> `'N(ŠkúBnf9`$;RMC!5}OA `?[z>`՜+`E8Űlggssֵ1wjr,s<#(u|% G_w.ʓu%u)d=9BsY\9i64N?(w Qoϥw&WwpYB~k Y1OQ&VKDوp#= 7 0#ON5HmF<!5uDkҜ6.Ӓa4?c9,S\ |O+u:.x'm J](OuqnA9OWl6 mm_C9pKS̙jsѤ4pdd Ap$J\u*~Mn @`x'QhZmg`A?-3C$^/򋮡ei*9nȥ&\ |჌݇=[O>Rsh۹o>Ihi^|R:3+l\ Ecq4@4Q,]BiX!]) ;\{<=;`7 r;z|?4,|d -,\Km)@t/,p cGwq>Lnb! $Ϲ# ;Z_e"ʿeֺ JzT|ޥ8N+^ ,'?H,N8*;GirRS#!%BD7H)0-6p s%sm{Eg,~5iBZQvߥ*T'EqBP|7"!jA8|'d \v}OYlMޛdxC aU8F $VSGMS3uMAEsj=)wx/`.zV\5iytNq|R)~QH:!!|ϩ.E+$1 qBLSSsx\ůB4.uEyS0pb)20:DDR? T7ʩƅ픚8u 8xe5jjH66! نً;ȑB H<kh]yx1)+W ]~ E,Lz)L7ꅄPIL'_zSV*և_䁻;!ƲxD6KeUȺ刉n(|əo3 2~GEZ1,e!:лqcC!!Rx<>lA$IsZ~h)5S&d0&8r40 =;dR524>8Lkrg!㫏E`}K@]B Gi.=CY VSG}k;@M Q'_)N=OB1L/~{|T>er7DO|{hWj(Nc5O~-?.Z+VUuz`Eˌ!#N?vB5>?Ao~#zJGr$Ot+6ZWV+ggE5i88~(*Xtыh]Z^|њZb5 g\C_iTLmv46H8EХaPd9C<_H|ۥf=Š&KgY:orh+/zť4Hְ#J_fY3rf5LF㻇~>a_#IY< hx 0 7a#!g6VjQ]Jhh@7)k : Z77jb s˜fJ0syH G\)|d7ɗ"&vD.ٔSʷ,[Rzzvq8|,R=[戻PTWd?S}5^. 3_cphN 3w1ӵNЯ>)bU#Hɍ,}Fr6$ahBN6ℚQF|Jݘa|pGKKKӑ"FHMh3B #즵d2ZAF[z3x҅9ܕkI7>J%B9Yjg4CHJРF+|` h!~X˃iFUAFaw5T;WǙ#Nt3#rUh- 5KpbB3̃n֓(&^Oi82ZB01<=;p g:TsxSPrB6x+܂s}+y_XsR&g}~we F0qWdYx!#Tε*#TN]{km>9E'}k!oܠ_)@9"V ETY Zq)k]>wtcm;pH>b_:5Ɇfs!6\D׹ҐMp 0}ܵă~ž}&2d dtOΩWv/:DA$IQvT$g!G(ۃV|r._'{ @zף($FZOb\c۶CtwqYlxh_(1Y8IIG4:hlQr|j }9ٓRTH={/K}\ui\}Yj5 tOxx3G?%^FMM)_6! lذ/=Xn{59mߨ6#P>BG~k/H_odnfUd4oEL|miS/yuZ(32BnI3A@:xBrggqbT@eQ1Edd7ѡ0ё!d1CQZ[PS"vMD!SЙ AJx$QC}k*kԔ$5} *#L~{35v:Q%Nfu/Ϣ_Lx>NNQ]5 Q!r~@"Bٺe3wm֧b…XS$͛WJc$OTy\[WE]٫q|˯8o7 !w2 >k ֭[VT$*Ɉ#OFMe("6mj#tSX#hITA~'=aB\#hiiygSHC=5zj1+'f:y϶nr׳e`PJK;Ēݹ$:D:,l)(DJ6_QZ7M=ۨR]vpw~`EZ~.Yg箻⟾+&۬^XuW]UW]EggWeW o-S oi{!2^vIW\Y?u` tKO&"҆!"U=Y ݀?h]=MQPYDhInq Ajm)bf4YzMʫ7: ;PNX%؊8p+k2{ywg o7+ڻXQ@g\0_ʗ#_a<~Y6Ϫy'ۅϞ^/љCȦAʌ`ez0r!t*Bc zR+5Wa ^纸_<.Ipݏ}nhJyiG0JlMNyآzijkj]|;:\xx RJ|#=!e8Ç0~xP?x5Ӽr=sלK$Q7-YkPH<_zF[;g.f! TMi;T#~ |ȇ8m*淰hBuӏlckK_,/׿Ηw;iȖ!C5 r̫le)0uv!S=|_n|=ϿZl;TFyۍ#5f^{969sY(k|_]]i.+]T ˃hx9?J* |+!8yȍ E >nHB"=^kiiX͛UMj@g+5YX @k Cbp$YwN޽4ˉB˘YiBcIL[p55tH7<0%",do6?q<iFiMًXDCK~/UWM՝ =9p4y{~}|ӟ7}&}uZ:*tH&%O4|fv{x|[?K._(h:J)>ޏJP-%jHyNdMPcVNe aգǏ[Ve9}Ɨ"Ngc@KF`d1x& zJ7ovmIS\2pz,J¶},pC L))~7CvDuII԰@yd .Ziֱdx@.Tc3 d*! f|x:h ȸ;7qZ/UW@(dkSaZ" 5X0epM}0‰_?eްI:;;gt_!͘WS'^Q[[{eZPE6fe<>W_ \x}l[|r,f Z?B v!e+U#7S}&^+?nBI!1VjSլq` zv=C+OZmoz k`ldlBXmaHQzB&ָ<鼃e9.(x -43iѾ>~i +D>ՊX@Rd,w12uMrus(nwyogFB2r YDeNGW %V L,xrW^u5 QXYul FJTU#'C]ܨ+)*o, w?a>t#ZN"ڎOŠq^r[ ȡsXK@,K7ցgb" .nriqn`e>zMeB,FZM 4 lDgyWF ]fsÐCy`Y&VPX:8dL\y|Hӛyբ,HR 2ҡ(Wι2l>eI Gf^pW?rfFGڹe|´~Y|}nٍ a Ǚ$> Ta4o?N ݩLr~cy/9q |ͱ|cZ<L" 'P>mD7}47of\QMHi0~Xߴl0ZhLS$Gs 5R!% #(MH6Κϕs<4MHU׳p%XɗBj!D}+8uPo`.1x ^ȩf#é00U!ϙUgfWx* ]F'/,]vk2 7,&S@406t*cGWV՘ 7kYqhʊ!$Y6x2q8 4n?Hv/(iJ)@- =3vW= ܹڦ&qJikڣ1,)  Lx \/P9>Y [l l 1䌆V>l>旹3{`)9B&{Ok]SROO9)hёx^FyeppضF~sqNԡ_gdC,\0:ȚT D_Œ( x^ƛ^u-#T釓|'J2^V1@W*E!$2O!RV~ӝtF@k]nďB)B*3/`1B:|KHn.n8   >;0wrT Gyl<~/nFҶyʕ\I[,Z]=-0IqTށp< nZh9T!w;(j嫄ߟY|yх;ˑIBH O ge=&_riݛ7pK]l/u=v.rPNf޼U[d)Mq&"+'WH{ r݋cis/+T@Aٶ%BA;rՏS:֖/&nf7=vh#(^J|oUu/Jmdl\j7qwt-eSNb^m-PiH2'g\_%>feI1+Zÿ/8wtY:y3UA1x+eVJmƚ3 ^37-oy+v4˲x^ŗ\=q]KPSSS!ɇ>>.8B5k>r|K_c9˱cxV~Dp\}q7f[bJaXEX ՁQWzճ,{tq:4p5K a<3̏k5+fjGHovҨӴ\)࣋Vrzs v8T!x&aZhC N%u~UB fԇSHH=)tKT I$CUZ3,_RHSHb܀lb&r)xgushxໟ!M# ўärqv3n''q9*r5vɶCX) "2TOzOr{O)ٶWo'p[_k' AN 9c \]zII~biA{$E40g֙+Il8)/&Y'pT9?‰2hF&@X~ʵm|fO.mʯWp=)jUqÁ(Geh Lmm r >1P a(M棳7p={2>ٯ (Qb9aێ(u1踊[׿Gy9֚Ǽ-RrHedt-.EyyJJ[,Vg}$`ԒT'c8Ј /$Z \~3Ih( ^z9xe4ǢBys< !(8@c&Qdq Yw˪!=4hMp2ڈď[_l`_R!ZYSDZ&1ĐH Ex*n ¦-SJ:;DK4ʣ}\3gNn$¶OS%(w%@K< K%pԧ` S -Ⱥ}G$z;韓 vn !iޮQB(t\-{jU ctW`s,é_uT#d7{{Uow{lUf'}{G~q8_]ׅU&x!={Yx8[0µ򱪡>#$DZKrk Œ(Rx`C(8e-"#T I D?O . meH RnJ)B8D5 P aJe] I3D]L})V HEH,e N `dXtR45V/sLwN-aLhy.zCw#^~NKǏ~CTEZ[nT~HE{WֲcXMc]'`TH M> ɍH1Ӌ ji(d_w45sp,պnH(D|.P;d$@{>!dXƤZU*J.=D& (1W>"`!KT|R^ 7Ϙai]:@g'Ex9 %E\9ESR+T!3y\zey'۶X-U⭝ ėٷw}_W-ktop ʒO=i#NOo%%FJI"2%P*D=? VE?&!B,BF_ԫ j(o!aIAX(qiN!>6k}WC[0Iff`Ce Ú# TTRx#}t8ټoK/%ajRELb's1}lhh? =r[Fr,޲y&$P!0*#H!*Jh i6n=r+:g1+ "E5LqP9&zo:8t8jUGPM0,s"hDL#lEVDD,N֕d.G.3>(mJWzjjjY܉a Z[W< /sSkhK84эoAWGNz)%mwpdbb}{裏9v+/G)x d )WS} 5 Pf|JצbF"dGGcdzH$HkpHv\m̈́g'=V-$lA-8L4%T]E7,.n7yh$ Zw+PÔ%*DuI\ x81`ʷ#rJw)ņdQ7mrT`B?~MWW'֮aB!I]oy)_`O-*qMl55%/~FM$Y5kflG{@w)H/" (_rQ~DcGŠDɎRdڻ%$H7 %V2𼵸<~F !jxi 4hpu tP5pLdH,!KREi]\fiEV~mH!RIMBX\b<=?ea|7cnWo]UJf3 {.x~6nHjbYfUԭ"HU|a>_9z+7=f!,{Z!왚a]gsVwC0NaUfU*ϥ^ O<Hjű1n&o/||'C<~(lF8%B&&YaÆg -7q];16 (F#ց- -b@ǶpO ί8}w/ÄC!b8iH$3g._v)MI~-.O8\:AB&?ӰXAFm\Xpz<ǥKRd3l+i!, U+=j*H؞M`Z&ux/A.5mtZhG;~QkMö*gk)->0,L.mh=9~[{s[׵E::CIBBJ?ozr0 058dx<)N*J615=-kVRњ$5m-Brjk #5ek9y;K;i]GD&N6Kӂ ? қJ3-B"Mb@GUޞZ,@87tP瞁L4smΫQ\QFXs= Qi\libft<,i%9BRk7ØҤ_0mdH0_Ju\,̰]Hv>ϒIٳg/;Zbt)˞…\X:ogg/x|=і%)+TWta=E XS}]H${~͟i cO29M&߄\-[8ͮ~U{&f frٙcDWYlǟ:J>> ʞ u!V-:ӍkT:z95/`疘ۆm9n dp-LĤEgqfZ1vew/)ee [Ks&$֤2 rB1JkE_@V9G-9v/HmOB ,LAQd\Lm+( j)%:/Ljۆ4Te8]+_,g% $Gh |&Ԃ 4l 559N^޴E\S6FG'veIS Qſ0K l鳖 eϻ(ʡގmF!~y Vz&MM͸!>D| &!$zb&wl /\cB6#o\ȶ?0]zqUWo4?9Ys^z50j^T{/[jΚF3s"iv F.^0FQTj'h s+^i[8t;fI6k|l3X+d_$aGȨz H)/`ØU{|l6x#xh-*Q]p=rG9@|s)p=MBtqVEZx[Zq]Si O0C>|"e v._Xp/o>E&:#̹N>y8ǎée"@SH ^kA E49_e1 ]$ :s7Mwo/ѣ򗿜 VXc{t_dCbrO t2m,#Wя~m j:)m C|߬Zxva07B|}1a*뤑]-%Ma㖳h. l/*Ũ]Qw&0̂UqyՋ>0MBql۳{/iLMA(~nbq5ۑM ({$Z7>J0I9IL¯,j8D*I)'6EK,Ρ/i\wC*eL7(Cr&qΌ!`w[GM1ai%W,2X{)< r>u K#d1/U 4umDLU&F{9t7:?!=?9O`*(l"-2 mCե~3Oo-#d`o|x_ ֫=}\z%,[b a^/jBNba#M]]eg;}0_-3J? P7vr0..Eӽ^q_I$8aygQ^[E)e^Th1Bdwߣ]ӶH Ҳtў0L&LvPX (=[!LÞ(>D( Cd b`~q_:n m|@m ntWz:8ojY͆diW!^q+V,4W??t'_ʛZV]C}yGJ_J=laSnPX*liPcKCkaLkiXXٜdIM;mXR`H /\RFpX[<Ӎ􇆐x~}ի+M bqg rхarm"`"* %p-%CKmk-Dyl<4Um}t@8In"Ev,MݜY>W =?EkE$d#gẉ1 sj9hKayA[(AA)o^·QkyQbvJ/vobUj{a^Y8>rq~ӟ}C|"3ʸW6pcY\Z3&?G(hG)_:VYj CH Lz-8lMkh G 0l Ri|G{$!a-g?gP,k*pE ̋ipcH+/Nso /r 4~[y)+E544_b>5/͕\gCG+xk_;-(xms-_EY@g vd F0AGh>g^duqsGHp!H RW ;Ջ^ 4yOshHP?p2Ʈ;ujAQB3dG>pD\!l> >r#|'y:lwqe` $ ӮS"24yh+ŊQP_~09;ABZ|b| {k8̛7پ};]]]| p3 )1%mpYFE ?BKܗ_Țk O|yؑŽ;e%4ov Z)v G%d"J0  Çh^ل46ÇQȹ4̟'%9E|eaoJNnxB&Gn\K40--Ml<Wf_v_ xc*JvShZn$x[Z;{l,Ǯ0Xɉ^EmY f/9`Z->6{f6air(?FL BW| [Xr% Ԛ68§ _CLRBbL((kpCA*BВLTMGXy5Ul) |toqх>(~tvtrWOǑ#y~msRYHiYwA~VY~+.m%8v48Av ;% J;x_?G@qP64$32N~AX_Ey2݋4ʚkcf[HvZ榦i =ͼ oS͜y<!}\V7&+kmTjTJ/IBA\ZHS=400XT_CK}x[644?zoܵ#մyN潪mmWit~ eh\b0gJjRL7@y>eEQvy_jXDh:{?x8/y1zaAbB9ēɪe˖qeO}8;JIKqҚH+gR.IQjLG 5PB_`Ș(X_=B MW2601A[]dQzy"=2 ȸ.|8g"ryx!53phZWʙڞݻy~۷ZXMgWAQEFnUAz0uʹ/YEyԴbh4ZNJq]2#{jyhm ^x5sg%~9) ^i!%RHw`oWszk ,TI4*n0( %4/a3zRCPҎ2}m Q}Ug~x3"ʋj1/Ђ:&_8Q)1=P#mKTyRWzW4QOb}+jqV d;Y8J ťڷ5=e)d16“Cn ʣ.bY[-405dKF8R!,y.? nb _v96=AfbJFTQ9y` Ff6 wo}8Okj\dIғWqt~/}=BK:m:$HJU &-g>=P`uZhmMDkMjp}9e72/K!(ʧiv# I!s] ˚L0JACVdF޶'#6Ga;lj$32o-(Rxa\GV&u_s5ݏ&D&b+"lܹ!fϞeYiI~.ΆxW$@V)L?7Qe kza^W0duֶrVWzvx_h,]iQ.' ~-p+W+eCo#d~-b۶I4Hgg;ꢭHdC|k__%}z-VBJѧTKf/K '#jX,ٵ[t\ƜuI45 T0#=X(D?PDm'I8dGǰ#xR[F~"E(s<017@j`=89ǯTp7b t>ȜucQL&# N9.-D,K PjkfqZJjVEQSİΏ-0~\h-D&.{Ǐ.IrC T@h鿤Rj$ega h# 'xqnv޾0YPvιu}لlpױhѢ1[,f'%e2i[# N9N "xc[in`1̵X?B+U(·9l!wضhOѱb ׬$҄a(O<r 3ˮ 53z%лe+~;)H>ZZZzeXik4B:cE}긐7C{^0WzhP">60PhwZjЫ1`ױ`N;۱q^ H IΖ齝?3EXl5<޿ ߠ:VŹsgړYAv7;3*e%(ؕi2y%3?.U}D7Z Snxj!ֵٰ (66e3O( Y4/jyR; 0MI.Tܯby4MF&P65bsQ.' l?w}@u ۲)Ҙ7AvI9_ `a23qkZ$U¤\,y^ܪ-Sݥ h`m%PB%C[7#~_/H&465!uE׳++~ld(V @SX%4,3[y#"W2:W(es1y,\ 7ܮ%U "_euzC Bz9u l_M _ @ڶlw~5Qn@u,P݆'gm~q|^ܡlXDY+PH|UWE>,Ǎ(e'm˘*WsD)Z%` 2ABDJΤuG ކcjP\teP 2F*]B~UWCXvW6[ @pj$\6d;ͱӳ\VyTfC%cj#H:ƭ.7mf/oiܼooO,! PU(YZ\d/t^<ޝU{4 YVz Ae*2_xݷ<ܺ%+FV$l& a/SB׭Ǣb,"k|2 |~zT">2J[L)aW ҩNJm5DB"+cubB27f%$UqV"]=lDv(er֒ض7B`*zmY3<P"aAd-4DO۶HZkma%R:oj8l[pK $s`ZL)|9< {RE0ΈKuEIDATR-`0zdUҳ/O z7{:oGi׏"-NQJa5Jyy|2'g*SpF(RMRfQhf\dVmNa 6 #U6~m M%ǥJE紗ӹ/d鯉:@mL"mj|LJhbIb 2-x="[uc%ҙc'6\'I 0'>{}\oޥSN KO,ݿv]b+x^,ZXbmvJkz}_7*dMQC.@y50H/xyK^&݋*SOSLۺ[nvH{Gq`cKKKŪz$Krš1!?ɩyxn&,{54їpDW Ya1 'KkrO2)\FWĶ<3@-hy/ "(.#X jWE1tSX%1;R dEXSD6dWؒ/SE UQdAVoP\϶ǟz__/}^s9ݷ.avmmv:R=nGe,_ 1@fe/n{9E0EʅPe"Bv5Ar~K^!B!$@{P}^JB)Kϳ)sHtxG>@UHJb2OO_Fu\&ŋm,PgY^-Rp#Œn{n|*gz'9y8wF[Zyv1+FYV9E8 7mO, ߦ@/I-PHg=|Hc=u=DO,_^;R@^8r_]#B S/%J)˱<<hCDDX ,N  |e-6e}Z:BQ pl*""JP%F$8lz">b3pLLѲoZ:d J89}1s 2Y$Ey=,3}vW ]iӶm Oz:W`a]4$Jҕ zwS-bn/.ͳf[]hH6eLlDG wƦxv>Ʌ8%߫ iD.cac 6b7'.e)O[$$'hR_Sxe`O=?%P\EBྷYC0v)VFʎ tطy=WY˴H/Rfuz ͑IKH:Djj,Q_Hp$! ".Iů@h_ MJ6ks]mK&*S%sq< 1dHn[d 6/BNsnD@@/膅Z^#B]C`>?HqsGzXW6a 4;N$x z2Ьn~ߛEi%֒'Y綛XT-R.OfirP@zG!Qfтn6|V:r3JPA# Ttxu f0aRxMsCVȖKd:I mmgqrڵ,&E")(2GN26Em ,PȔ0 [P"ADWWۖOτS2~YJpD ڔVϳ-Lnnw.gG.=D<K(&l_Chro#i'UwSL9$ٍO-#]Ȓ$ | ϏU#W]?8Ǖm˙6Rt-ɿzb5"{0]Nkij88,#$lsi߷ 6-dUJ3od9(I9FYSf|.|N9G0J:%b0 6k#qvDnJ>U(Q75Vds+( x||HC=URK+4mBvu r2~pr@=w |F^ 4g AwqJ&9lJ9O@SѼPD al?P2||6yxb"Q6"= TWi|߽ܶ3gEڛ(nl EtW!*b`+e:6/Y`ҍbM`{ʮ(nZods;X6 D Uj;QWH>(DPCeE'Sdk2n83LٵdjcGJNi{*)rkW~Vlv[7[u~ s=T3X ʚ.P1уpظ%A\hӶP%y# ^ggH..9C`}B0~|7o4~1P YG~()$0e<DϧQ-ԫ!ǚ&m6d2QLP5:jӄ*u6p)prؾ#|üd,#WұPRa*26"C3%▭UL )NsGQYNMy?7gX br])$˵('dVVɬ62qVgW)N H""!i*ZGp 28 ΏRXK"*poUDTISY8Nffy=3:@$ eӀw-ۆZ_^q{afT{\nCg/bT""pr*):nُ'dFPNmn%"%pO8´͗/QmXग-Rla|OHY4yKo"R*[) Mu>m7gw.s׭u[3Mx}2弅aW{CVI snǷ4m:ܺp,P`l& pEoW+Y,nh6UR ЋGЭ2&[IH00ApyB7 4E"H>%C>nBq)(%ܪľ(UU -O_-[kk Q_!R)Iמ`ɬEr*!z,AKɏ6NJQl`kog&S 6X!k޿RГJ/R:G~5a.>qrlRVe"y[D!R[fo,wk\9ov5SR-^E4ze8W0{T~m7p-#w#nq+ rE ,S/bS9}Qdkιi>73ե4Z U\|\M~샸BW4Q*c Ji!"k*zD)Β]fly]j0:abUL,R`sCp)R78~miTUa1/(HE%HRE#ùQRLTfH[ki %pU1)AE \nzjxCk&Fynar Y "olUaSUh%)aUdrHzYs!P6Tq2YֻTW9~zA֝J $]"MOs/M1eX\zEuDB )\,8+٥b&Ӌ݂Q*]Z%=i8VQJ),Ʊ旨,[CJC); ֽgRC&Rz_Ei.Pɲ,Y+lxTh5mV3I#Vj&W&" q7q{ywZ|mx x A,CJȂl(lr8N"iFBx".+ ny7m햰VmzsoBZ?6~rPl^fh4tjBA7Ѩ][\t̝w:j>+{{qPwai-D".yዳvosZֱQ}VGQJ,[;&<0Ku:*d A-G2RBR>|>V̾zLfxrٹQCc_Ǒ< ([$ :H*&r3%^!nbzp.J$/~*vIN D/{ߵw>؋-p+O?7mAT\2hgf%v Q۶ܡQpݸ>FyefhطhC(狤f6Fg%sH‫R0K:yC~ng3;z]WŃ [q%`:``u òX͹GΞnV.en`2.gF66<2BhLT+H׏̤ltqs2edd-_W$D^kLS0Q$t(a ]LsW3e^Ns(5F@@3slꪩhxlT)[Ă.jBfX6 l(؊dTD(KsշY%lS̭Neqg"]2 C u\ ޔ_vK!X?me:1RIdٽ;Na~uSi#G:>O[Hw"KWg|,M@f>Or6O7QK,_85:Lb&Q(JFp^߰9Gj;w˅(3z-L'8iۄ4+o?tOdA,>Z4M_B/9CC8U41,wg8IX r1gWwo>FW MEcqfS)x}Dܞ3P!/ [&(nڇvF9 p,rUX2,Or\k'N31oktug)&.z#Zv0.3WhoPBEM647B"Y$u(C2urPMnO?=H~S >zO4Y(!*2ߋ A MձYi1L47״UULBE W)WB<@i~udmպ-_@* w- ԹHKԘXq(O'XȦxc];Zx=8kR6BU(>zӶsS576,IWo0+TJ XlqLnnz* GTֱ-Uz4r9D TI"rSS4.7$aR`SId~e'ˆmDU5lWiFiw ,rgK5i#:Z|nDK&&J+֍"x5괰mvQtTH},B<\p tHlt:M{{;kK˕ }Ӵ75Vs\O0m`? |hG)%pT㥷&T6O 9&;# ZM,!/QXMX'$ 5 OZaB>nLӢJ:aĂ^Ag/qNCeaWO#0zS*PDIt&Xj!G^/ל9U+Dog'wLύe٤ ޥ wx} òɚ 0 U;h.ZaWo#|D\W Rp&K$ 2ʺ3i*҆5(,M.[C7'?`%$[nR&|s J,Ȇfe}.5 9`VT~W@[[io=xܕ:=2BZ&&E*9t )uTU²l ٹe_>mT'.}7Kc29߃`䝀i$3W-H'LXqrk.<[Qlj`%7Ay5 b~lY<*^>?Zfm hrbldLd9^w @*ki޴mN^I 4gcdO}Ky-}0rEb!/$+S6wܚ M 87T>ǖ'(S zV׍lTHԉEFVHCU͚+KL-If d%tĥ*D}4Dz wss淹&( r"b;jo=O*S =]UR|<՜??2[031‘#Sj:+)ru~][/<$RDsDadYd!̚߿ [Ų972|dS|jP"5 9ƋyTܹ/~]?9ۭ!$KGgn˶d2\d9㶝=?drj}~|Y75n<Jˍi:SS\tbhua3!c=9RY%}Eˬ-U)y˩l.CGYNh6%"!^ȕ<31BuGhۆYd1I2\B4&>=^BV!V/pa*W%+++֛x6,-/DS̋>kb6WYAMMU–a&_nBL*\!kٱM+s+a077G&04ONѳǍ,I]_i_] ODzllkC2{g;08:l3c("bQ(5^X[[ɓNkkeۜ9}Dˣq^xi< ]۩ٶM>_`ll'u0>igH˴l'VQ/QDt*m薉O0-dRe xAin pK{_8Ih$B\\.c(EQdaaZMQKeܲb!t.|z-~< NYEˬ\6xd À6]eKcm-!~m|vɂ8@Cs3cccH:I֨9>7\!Hss3(/<LOOS.Y]]W[.00 E+[j\ZлfP^uQpŠ"XdY& yzm#称$x|֦nVQQ^(GO20LS .\HDOG(0+ XCWy8?@*yrdliq7?rTlNqy!CMEmȍ$Y}DO̝wEcCgϝZKo;m3˰s.X][cmmP(He K{vBUU(x bY6ရ6 /2::eY>gX[[# b"znmy[E9zd*|3uu{H&LMMݍjHMM a hNUEJw`{g=kk\a`qaYQ饹@ }o}+) iZ0IDcǎ1??KرcrO>I$vS(yWPH4B_o/R%s422Fϛ yL;CSDx;߅KXXXرc^vCgG{!Nsv؁f6B"9|033ȲL'DLGCGel.N$iDA(,l;-۾I4/?>7HŃ{LJa㘦ɇ>!XZZ_*~B! K祫Zm')eZNc,"Ǐ'=-A7r vӄaoI?9bLNMqydmƗeFfGmɧ… s9;oBt=T"Lö#s96ŒҼ mیQWWG:p.Ga۶|^:;9rF@o~~?3`6===ȲLuu5?qSSSC0$NKr(---LsnF00M'{4e dp7$B IE/,( 3;iokܹs q뭷RUUlۺE[k+h.u1?>LkhGGӔ`'kF3>JZlѣ,7xQ:;;4דJrTȲR!Q*aAAU obdeDQXȼ^dz L: _|no-s LWEg+THtݔe}QrgdtrSM iViXD òM9mpJ2M$2-nQg-D4l6$ItvtP*X][CE.^z|d2eYJ4erjjT RuUU$ Uq)*~ym _3cWq)6Ս4MEab>-imm UUen!a0 I(˸\.kj8T߾Z ҷuôRMtA 4wt: `qi EQM\YRi#iռ|0^-woGH$TUزe apqzzV3gNͮ" |WZ?1@*V9pd-3z;Ȓ [ takx=6ؽ{7]]]afggf֭ȲLgG'ΞW.q׾F<%A4yRѺrd) %kd %r2`S,PU* , MЇ>D!.D"bfj_~~dI3َ;%ݠX}VhtȵϗerK,jlD6{A:;;7>+oݨx=i Q3:Ǵ,2"^$Iz*Ӵl2"s+vVփA  =pT}wp(پ}; 2|2SSS 0 '֫? ?ds]1nBR뫟ccc9{ﻏ|=lٲMWOgNQ]ʦ (*'%u`-h҆XF]Ĵl.-͗Hd ׹psss0_hEXRtƵmDQ$L",38Z:p[fca|>"iׁK2s˩늅W>p륶Bm($S),uygʴ%QCgF< @h0$+&@ pCIj{{R)Lְme{+>-OUUtttpG6ӧ9477Jf2יֽvTR"6.TѦ-9DkBY&yon62 ضm~Ɩ6^87Nuķh Vn$3<4}>1Эޫ\.ya`u'`kkk,`BH?|_u*;m )Ʃ˯:JnY6i9 6 %ҹ"% )8`7R W,coJ p`;Z(ܡC;v uC$kkkȲB:WLi~ΤP]h7R=sso%Ib~%EGCi,YAN:#G^0~;~o}F71fA* I(XΔ M;b]d}GKs3Ǐ>###RS]*Ķ ù .M}>==^G /餳E\ԔS#LHXT*E*`gw=T8 >u n7a l&kDRL0 R(- pzd2X^^&HP.D"E }*q# wFR$ ,-.LRh۸t"/8ud4t`lEqs+HX,D+T(""333ܾ6/rlq)jkj6nS8x1cs>"UA yP楍5M;vJ. :R?4QDgS3 \EXzijjvSen7./+;~j"~W.ėy)2iiۅ(NѸP4żt6O9u#LOO6@E^y>?{&olHpj!^SNO7icK{-gYJj14xo Ess3􋿸g*g7Cfu޹?x{nz{]|'Bb<~or"\[o=2~I}C_OzOقISܛ/.o%eS465Lry6Bz|F⫫?: |lgvq u9țn\i[ltԖeVX\\cFՇt{ӴXHĢ1J2mr  dct681 ;>(2y4͵ x< mxnMtoYY[Kp1oI<4'+qi Ѡ mEg8{\&)0( GWanz$2Wq&f US1C*fdl${19x*GGC chbh,Ǫb̙3gz:1xIVǮ9T G 3r."+*v}>n7xGyx<(.Z5558 }uܽ/"* (b:4r׾.r2QFd=Yj~jM|ø,wrntOy;Él7)Ky.V f|l SKJωYښ IѰQ%i1MR AgV޴l16bpy} HdI˨MmćϭR6L5 y9qid%oӲu'd"aZ\[#T"}.LäPݬd :^2hyȒ*޶4998x!}BlUfM{\DnN" x8|~^TQ),. 52q#J9 +)T0h˅a 9RɄSo﹍['.Ms,9 "NQseuxQC5UUZ.@65 ^%;1ʖӅ$%y^H<Ł{L$t*ܾ}eFlZerطsGv݀,HUWS,s|_AEjc~gh:d '!/l۶qk nM#--_~Pd{Z0ڱkwo彍ك")\׈_3痹%p3o};frp2,sL-q4~=᭜N'ENΦP0t.M."xcwkr$*1 7(8$l6K{ntTGܴ=M+q)lX{ 82 0nXu7,p' l D ʹI5ھ 0HӬLUd9:@]wOQ4xIY\V fKCxW~Є.ws;o&TdYⅳ|󹋔&%Y..—ӹRK9#]-w{wA^MK)UPY%K@Pm͵oQh41-B Etfb$͎屹g9rC |~NfNR[o"T1-@央6$2yWR eܚBgS l¬Yx5Ɩ)8@![FK61+zK@QnP,(2NP鳌ONQsrpL,JnIhNwW](8Lؤ׉(9E %AKnq WG*?oEћsr3'{VsIS( %Gw2ƴMJ۪4{_3uo77㖪=t7HQs:"q( N.br~$Tng A@D}e$Qu240/r)nq(:Y邹jOkgmMGh5+z?a XUFgV8??'P(XK$8yGGi󞻶sin ͻډ\}μrƚ0zGTsnކͥY< ,Xտt675~ s0Cѝng(9SVKI>9Wt$>=4yyi8 t"eXX{swa KZGZNR2u.Ɋܷ ͨTJtTJA\&Qȳ2>c3nn➛{ WמwSnkCi4p~"Ӽ\lTE--xefhjT|=Rqd`;@{L{g/DPw+Ǟy]M4iѯ\^樺c?-툲L 8{ kYDAX~ +48_ USScpCq9l lgQyB@RqG$nڃg6 @J$HGv3hx#GVNwI475r0$QDEF"n}U |]NCUR _`Wqhn}6`zh`"=5|N刅i {+ "d%G*ږE9[@;1>gV]$ LmzDE*8?>~ $!}5J1ȵX8 %QΗ3Q Zy RRfK@'y:||&4{9EPafxo9>IMѷ0J0,Gf'zD=^l S*Qua:(7t 6y[g{wsPJfu'~l 7Qyocf66,}6Ǻr oYP.] W{즷.Mǂc9<-pL7kZ, Y7ICW7~ +Uxe$# m,y|YtK7؍OҳĴ01b_?EenYZ2Uq6mf-Jj}]έlc%gsVFXb&(ѠpPL)j.ridMadž o;/A6 W|Xw28Ȃ%1@J[kS 88ϩ(P/YnڇWvccst[$zϹv/5W&!O}_+z~;{;{i8$O yU!nYtT{$dKOz.މ7>7N)m7%ؼ3a(:kPd$XE0+U$4JDy7g2vܡK#`eh=VYS]|fuڷ-ölQ@$2 NQ(oRaC ׮JLd%Al _sc$uW7 LdgΌŗ`Utd2(o=Ȼ\~aQ 4j͸E"}jk\\^ o,Bw 5!"@eN\Q%b@%WL~y7ꯖ,G9ra+Ȍ^jqc'UEdwo==5WeC[C+CV6֠r$vwSw;En, ~Gw=0biI-aiʳx38K}-ԿԽR^ O4B1*w TD-ֱ)RCζ |} \v[F疡Wb$/'23PKmv_{'L̇Z>L-QG]s,/4¹!,%C{CGSg#و;^$5DMd[&5'ր͓deNo\ULlǓ_Sˣ jA)J9+\H QN2ooY(:;kYT!K'ȕq_8uCCtLb=DdQ'V($OsO㥷*F}X!hU[OOU5aEp+ a˅WUhX 03pW5*@r~iC45l:R6'{)2lO5zvK*d6UEΏT!ZF]cץ J)lsq|C'X6n=j/~#(^$Q>K]"i,M/~[6+w(V^yi*҈v6YbEjvb[e]4+ d`~6?A֦%0{TC.vgC !Յ@K$H@ڰ,V9Gyn6$ S`&(Ur2cU&L&HxUFHl2?^^WlrnګZ>*^٧u ]۴7F]\7$@cuwR<{|Ԛ@,^CMh$ l23xohN3S#1;O=ۺ>mw,&8|r˟sGq L"ss$Zk#xdVYeR΅; hbvc;L9N@ b hE4 냟]=?6bxCiph)Z4CԵMY)C2'ͭRE2{Ǻ;̿-]ph r).4:\6ÁFnm++Xn.(yGQyyzssD^,fHN:c_{W}.˶,,H _(8PhZ@B&6错S43KJ:Z 1m0|d˺YZV=ή0MI;/_gޞ\"Zn-{~r_%'hI }z`0|ӞdqV ëj]U˛pְ5^,_Ck*^|`"߭~eŭ>hjc`0^@` XL4BPFXL45͙cL%C |n\Lwͩ˘$t&ڻ ϐHa xt Ɂr$29C5L44I&ɐ5?1TCUCP_YHIߍm%i\s+^NJ5a3Y]c+ qM$(//-_T*©i v<C2ERTI8Ƨh*jG'qN )BIRH\,?WhR+0M*nZ*q |K$Ngh +kTy۬E+4$zzR ZN\JzΙXx2HhæB1He\pq6?tJw-mam՗t%Ql@ Oбv\t(rXNˊ[X[wZM`W_h!Di"$15G,V9 %Ahrk4amcɸpunMEI4"\(@iBR”HםKs )_ifY1/ 1v|d:?whFկO b *cdIX"M$;p}#h?]3p_u6f(k/faA#{_tN$3 H 6Kr3b.CL f#n|8VZO=eJA. 0L`U"1hMHe]Ib5;DE_r.(æ*NYW# HZ|US+m qz 91eyV.Ʀ)Y$T˨Lgx`'ZVUqw= GNOv#EFE(ө0z6MObYIEvA:£ɖ$$: &ƹo F/DIL6̃$wa.&XBuo !63=~S=E5k#if ;V'?Vu=G{8]B6*;qtDUm}[ƙ YSC/hjO 5ޅl=2dyW&Z0 |+NbzY;l~eG|tL&;M=o,tkgWg3_qM$9DxUXDy3꠹kh+L-* ɫ( $b)=@7u2S^V-Md2n[s+~-" 1D.G224Mʊ<d<r?(v3ܳkȒ峸w]‹~B >JYRۋXTHYZJn&MaHBI$2qJ]TDd=8DK*~z#D^e3Գ︦l? q( + ei5-()p!bVAO lW2ҳL'fIh?5ܷ%yo6sʛ]PǦb_/`Lt2TAf d'9ss\":Ϊ\ Q©h=›4ʆLXQ?%&IQ$01tiJ8tyt7CýF !$S9,Wg$HJ^첳J$Ň,/eEc 8l5ث̙KYF,O F&"?=7f"8ɷ7}Ú OSQ^ ˗u Odq4'Xr gBbϖ>tj?û~K0>N$5nX{j:?dNZJVr|m&4Yc蒐 C޻Fަ}8H5$cXxkSg}a^tPYJB (%0GF9;N CQRk2dYLFb9m[#g|'ìoE F?ɺ[V*H"09H|(.;-P/kVP\ݾ*Bv?oJO-5z©MŭڋuB{1ˊ[mݯp{=T7*B Ml5RLLB G&VCy{h3gçHdfB?pIy y! V!DH5 &IYd Πg 4EQ]r pzߏMӘ9I+Q4g:9rh/t8þF D [`AFomyQ9Ef:a&DzVvmᆢ唺*pnomfeY*ܚh?g'ydsyM /xܳlxHf=tR_XӶa96eb"Hs., !BO0hB 5ѮEd&Jω@ v?h׭q>†kdN )#A"ê>vq4^|bEj~uVkH(5LS W:mR) #KKC%ե>zzkuYz了7; *2ni׎ܵOx7YwWcgGFxpxl^^<;YS}tgzII 3n:b "J SVV&F Yr̪@+(7M`,,P]iSqh NݦӮRuXK_4Mc]DF' Ip:lg(ib5.%%F&hO` yv`6%/c q9Uc:` 4@)3i&O;= L @bv4M@ā0֚Vo =UuR|6UVɋE +Taѕ~;yRY"X~iQ +" ư:Xb2{Nz#%tEXtdate:create2019-03-21T16:24:16-07:00S%tEXtdate:modify2019-03-21T16:21:05-07:00:IENDB`kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/000077500000000000000000000000001504711711200240445ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/apps/000077500000000000000000000000001504711711200250075ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/apps/apps_suite_test.go000066400000000000000000000013621504711711200305530ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apps import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "testing" ) func TestApps(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Apps Suite") } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/apps/kind_visitor.go000066400000000000000000000046521504711711200300510ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apps import ( "fmt" "k8s.io/apimachinery/pkg/runtime/schema" ) // KindVisitor is used with GroupKindElement to call a particular function depending on the // Kind of a schema.GroupKind type KindVisitor interface { VisitDaemonSet(kind GroupKindElement) VisitDeployment(kind GroupKindElement) VisitJob(kind GroupKindElement) VisitPod(kind GroupKindElement) VisitReplicaSet(kind GroupKindElement) VisitReplicationController(kind GroupKindElement) VisitStatefulSet(kind GroupKindElement) VisitCronJob(kind GroupKindElement) } // GroupKindElement defines a Kubernetes API group elem type GroupKindElement schema.GroupKind // Accept calls the Visit method on visitor that corresponds to elem's Kind func (elem GroupKindElement) Accept(visitor KindVisitor) error { switch { case elem.GroupMatch("apps", "extensions") && elem.Kind == "DaemonSet": visitor.VisitDaemonSet(elem) case elem.GroupMatch("apps", "extensions") && elem.Kind == "Deployment": visitor.VisitDeployment(elem) case elem.GroupMatch("batch") && elem.Kind == "Job": visitor.VisitJob(elem) case elem.GroupMatch("", "core") && elem.Kind == "Pod": visitor.VisitPod(elem) case elem.GroupMatch("apps", "extensions") && elem.Kind == "ReplicaSet": visitor.VisitReplicaSet(elem) case elem.GroupMatch("", "core") && elem.Kind == "ReplicationController": visitor.VisitReplicationController(elem) case elem.GroupMatch("apps") && elem.Kind == "StatefulSet": visitor.VisitStatefulSet(elem) case elem.GroupMatch("batch") && elem.Kind == "CronJob": visitor.VisitCronJob(elem) default: return fmt.Errorf("no visitor method exists for %v", elem) } return nil } // GroupMatch returns true if and only if elem's group matches one // of the group arguments func (elem GroupKindElement) GroupMatch(groups ...string) bool { for _, g := range groups { if elem.Group == g { return true } } return false } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/apps/kind_visitor_test.go000066400000000000000000000120341504711711200311010ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apps import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("When KindVisitor accepts a GroupKind", func() { var visitor *TestKindVisitor BeforeEach(func() { visitor = &TestKindVisitor{map[string]int{}} }) It("should Visit DaemonSet iff the Kind is a DaemonSet", func() { kind := GroupKindElement{ Kind: "DaemonSet", Group: "apps", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "DaemonSet": 1, })) kind = GroupKindElement{ Kind: "DaemonSet", Group: "extensions", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "DaemonSet": 2, })) }) It("should Visit Deployment iff the Kind is a Deployment", func() { kind := GroupKindElement{ Kind: "Deployment", Group: "apps", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "Deployment": 1, })) kind = GroupKindElement{ Kind: "Deployment", Group: "extensions", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "Deployment": 2, })) }) It("should Visit Job iff the Kind is a Job", func() { kind := GroupKindElement{ Kind: "Job", Group: "batch", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "Job": 1, })) }) It("should Visit Pod iff the Kind is a Pod", func() { kind := GroupKindElement{ Kind: "Pod", Group: "", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "Pod": 1, })) kind = GroupKindElement{ Kind: "Pod", Group: "core", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "Pod": 2, })) }) It("should Visit ReplicationController iff the Kind is a ReplicationController", func() { kind := GroupKindElement{ Kind: "ReplicationController", Group: "", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "ReplicationController": 1, })) kind = GroupKindElement{ Kind: "ReplicationController", Group: "core", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "ReplicationController": 2, })) }) It("should Visit ReplicaSet iff the Kind is a ReplicaSet", func() { kind := GroupKindElement{ Kind: "ReplicaSet", Group: "extensions", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "ReplicaSet": 1, })) }) It("should Visit StatefulSet iff the Kind is a StatefulSet", func() { kind := GroupKindElement{ Kind: "StatefulSet", Group: "apps", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "StatefulSet": 1, })) }) It("should Visit CronJob iff the Kind is a CronJob", func() { kind := GroupKindElement{ Kind: "CronJob", Group: "batch", } Expect(kind.Accept(visitor)).ShouldNot(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{ "CronJob": 1, })) }) It("should give an error if the Kind is unknown", func() { kind := GroupKindElement{ Kind: "Unknown", Group: "apps", } Expect(kind.Accept(visitor)).Should(HaveOccurred()) Expect(visitor.visits).To(Equal(map[string]int{})) }) }) // TestKindVisitor increments a value each time a Visit method was called type TestKindVisitor struct { visits map[string]int } var _ KindVisitor = &TestKindVisitor{} func (t *TestKindVisitor) Visit(kind GroupKindElement) { t.visits[kind.Kind]++ } func (t *TestKindVisitor) VisitDaemonSet(kind GroupKindElement) { t.Visit(kind) } func (t *TestKindVisitor) VisitDeployment(kind GroupKindElement) { t.Visit(kind) } func (t *TestKindVisitor) VisitJob(kind GroupKindElement) { t.Visit(kind) } func (t *TestKindVisitor) VisitPod(kind GroupKindElement) { t.Visit(kind) } func (t *TestKindVisitor) VisitReplicaSet(kind GroupKindElement) { t.Visit(kind) } func (t *TestKindVisitor) VisitReplicationController(kind GroupKindElement) { t.Visit(kind) } func (t *TestKindVisitor) VisitStatefulSet(kind GroupKindElement) { t.Visit(kind) } func (t *TestKindVisitor) VisitCronJob(kind GroupKindElement) { t.Visit(kind) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/000077500000000000000000000000001504711711200246075ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go000066400000000000000000000030471504711711200262270ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // NewCmdAlpha creates a command that acts as an alternate root command for features in alpha func NewCmdAlpha(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "alpha", Short: i18n.T("Commands for features in alpha"), Long: templates.LongDesc(i18n.T("These commands correspond to alpha features that are not enabled in Kubernetes clusters by default.")), } // NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding // the help function ensures a reasonable message if someone types the hidden command anyway. if !cmd.HasAvailableSubCommands() { cmd.SetHelpFunc(func(*cobra.Command, []string) { cmd.Println(i18n.T("No alpha commands are available in this version of kubectl")) }) } return cmd } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/annotate/000077500000000000000000000000001504711711200264205ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go000066400000000000000000000404101504711711200305570ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package annotate import ( "bytes" "fmt" "io" "github.com/spf13/cobra" jsonpatch "gopkg.in/evanphx/json-patch.v4" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" "k8s.io/client-go/tools/clientcmd" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // AnnotateFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which // reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes // the logic itself easy to unit test type AnnotateFlags struct { // Common user flags All bool AllNamespaces bool DryRunStrategy cmdutil.DryRunStrategy FieldManager string FieldSelector string resource.FilenameOptions List bool Local bool OutputFormat string overwrite bool PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags resourceVersion string Selector string genericiooptions.IOStreams } // NewAnnotateFlags returns a default AnnotateFlags func NewAnnotateFlags(streams genericiooptions.IOStreams) *AnnotateFlags { return &AnnotateFlags{ PrintFlags: genericclioptions.NewPrintFlags("annotated").WithTypeSetter(scheme.Scheme), RecordFlags: genericclioptions.NewRecordFlags(), IOStreams: streams, } } // AnnotateOptions have the data required to perform the annotate operation type AnnotateOptions struct { all bool allNamespaces bool builder *resource.Builder dryRunStrategy cmdutil.DryRunStrategy enforceNamespace bool fieldSelector string fieldManager string resource.FilenameOptions genericiooptions.IOStreams list bool local bool namespace string newAnnotations map[string]string overwrite bool PrintObj printers.ResourcePrinterFunc Recorder genericclioptions.Recorder resources []string resourceVersion string removeAnnotations []string selector string unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) } var ( annotateLong = templates.LongDesc(i18n.T(` Update the annotations on one or more resources. All Kubernetes objects support the ability to store additional data with the object as annotations. Annotations are key/value pairs that can be larger than labels and include arbitrary string values such as structured JSON. Tools and system extensions may use annotations to store their own data. Attempting to set an annotation that already exists will fail unless --overwrite is set. If --resource-version is specified and does not match the current resource version on the server the command will fail.`)) annotateExample = templates.Examples(i18n.T(` # Update pod 'foo' with the annotation 'description' and the value 'my frontend' # If the same annotation is set multiple times, only the last value will be applied kubectl annotate pods foo description='my frontend' # Update a pod identified by type and name in "pod.json" kubectl annotate -f pod.json description='my frontend' # Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any existing value kubectl annotate --overwrite pods foo description='my frontend running nginx' # Update all pods in the namespace kubectl annotate pods --all description='my frontend running nginx' # Update pod 'foo' only if the resource is unchanged from version 1 kubectl annotate pods foo description='my frontend running nginx' --resource-version=1 # Update pod 'foo' by removing an annotation named 'description' if it exists # Does not require the --overwrite flag kubectl annotate pods foo description-`)) ) // NewCmdAnnotate creates the `annotate` command func NewCmdAnnotate(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { flags := NewAnnotateFlags(streams) cmd := &cobra.Command{ Use: "annotate [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]", DisableFlagsInUseLine: true, Short: i18n.T("Update the annotations on a resource"), Long: annotateLong + "\n\n" + cmdutil.SuggestAPIResources(parent), Example: annotateExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions(f, cmd, args) cmdutil.CheckErr(err) cmdutil.CheckErr(o.RunAnnotate()) }, } flags.AddFlags(cmd, streams) return cmd } // AddFlags registers flags for a cli. func (flags *AnnotateFlags) AddFlags(cmd *cobra.Command, ioStreams genericiooptions.IOStreams) { flags.PrintFlags.AddFlags(cmd) flags.RecordFlags.AddFlags(cmd) cmdutil.AddDryRunFlag(cmd) usage := "identifying the resource to update the annotation" cmdutil.AddFilenameOptionFlags(cmd, &flags.FilenameOptions, usage) cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, "kubectl-annotate") cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector) cmd.Flags().BoolVar(&flags.overwrite, "overwrite", flags.overwrite, "If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.") cmd.Flags().BoolVar(&flags.List, "list", flags.List, "If true, display the annotations for a given resource.") cmd.Flags().BoolVar(&flags.Local, "local", flags.Local, "If true, annotation will NOT contact api-server but run locally.") cmd.Flags().StringVar(&flags.FieldSelector, "field-selector", flags.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") cmd.Flags().BoolVar(&flags.All, "all", flags.All, "Select all resources, in the namespace of the specified resource types.") cmd.Flags().BoolVarP(&flags.AllNamespaces, "all-namespaces", "A", flags.AllNamespaces, "If true, check the specified action in all namespaces.") cmd.Flags().StringVar(&flags.resourceVersion, "resource-version", flags.resourceVersion, i18n.T("If non-empty, the annotation update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.")) } // ToOptions converts from CLI inputs to runtime inputs. func (flags *AnnotateFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, args []string) (*AnnotateOptions, error) { options := &AnnotateOptions{ all: flags.All, allNamespaces: flags.AllNamespaces, FilenameOptions: flags.FilenameOptions, fieldSelector: flags.FieldSelector, fieldManager: flags.FieldManager, IOStreams: flags.IOStreams, local: flags.Local, list: flags.List, overwrite: flags.overwrite, resourceVersion: flags.resourceVersion, Recorder: genericclioptions.NoopRecorder{}, selector: flags.Selector, } var err error flags.RecordFlags.Complete(cmd) options.Recorder, err = flags.RecordFlags.ToRecorder() if err != nil { return nil, err } options.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return nil, err } cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, options.dryRunStrategy) printer, err := flags.PrintFlags.ToPrinter() if err != nil { return nil, err } options.PrintObj = func(obj runtime.Object, out io.Writer) error { return printer.PrintObj(obj, out) } options.namespace, options.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil && !(options.local && clientcmd.IsEmptyConfig(err)) { return nil, err } options.builder = f.NewBuilder() options.unstructuredClientForMapping = f.UnstructuredClientForMapping // retrieves resource and annotation args from args // also checks args to verify that all resources are specified before annotations resources, annotationArgs, err := cmdutil.GetResourcesAndPairs(args, "annotation") if err != nil { return nil, err } options.resources = resources options.newAnnotations, options.removeAnnotations, err = parseAnnotations(annotationArgs) if err != nil { return nil, err } // Checks the options and flags to see if there is sufficient information run the command. if flags.List && len(flags.OutputFormat) > 0 { return nil, fmt.Errorf("--list and --output may not be specified together") } if flags.All && len(flags.Selector) > 0 { return nil, fmt.Errorf("cannot set --all and --selector at the same time") } if flags.All && len(flags.FieldSelector) > 0 { return nil, fmt.Errorf("cannot set --all and --field-selector at the same time") } if !flags.Local { if len(options.resources) < 1 && cmdutil.IsFilenameSliceEmpty(flags.Filenames, flags.Kustomize) { return nil, fmt.Errorf("one or more resources must be specified as or /") } } else { if options.dryRunStrategy == cmdutil.DryRunServer { return nil, fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?") } if len(options.resources) > 0 { return nil, fmt.Errorf("can only use local files by -f rsrc.yaml or --filename=rsrc.json when --local=true is set") } if cmdutil.IsFilenameSliceEmpty(flags.Filenames, flags.Kustomize) { return nil, fmt.Errorf("one or more files must be specified as -f rsrc.yaml or --filename=rsrc.json") } } if len(options.newAnnotations) < 1 && len(options.removeAnnotations) < 1 && !flags.List { return nil, fmt.Errorf("at least one annotation update is required") } err = validateAnnotations(options.removeAnnotations, options.newAnnotations) if err != nil { return nil, err } return options, nil } // RunAnnotate does the work func (o AnnotateOptions) RunAnnotate() error { b := o.builder. Unstructured(). LocalParam(o.local). ContinueOnError(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, &o.FilenameOptions). Flatten() if !o.local { b = b.LabelSelectorParam(o.selector). FieldSelectorParam(o.fieldSelector). AllNamespaces(o.allNamespaces). ResourceTypeOrNameArgs(o.all, o.resources...). Latest() } r := b.Do() if err := r.Err(); err != nil { return err } var singleItemImpliedResource bool r.IntoSingleItemImplied(&singleItemImpliedResource) // only apply resource version locking on a single resource. // we must perform this check after o.builder.Do() as // []o.resources can not accurately return the proper number // of resources when they are not passed in "resource/name" format. if !singleItemImpliedResource && len(o.resourceVersion) > 0 { return fmt.Errorf("--resource-version may only be used with a single resource") } return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } var outputObj runtime.Object obj := info.Object if o.dryRunStrategy == cmdutil.DryRunClient || o.local || o.list { if err := o.updateAnnotations(obj); err != nil { return err } outputObj = obj } else { mapping := info.ResourceMapping() name, namespace := info.Name, info.Namespace if len(o.resourceVersion) != 0 { // ensure resourceVersion is always sent in the patch by clearing it from the starting JSON accessor, err := meta.Accessor(obj) if err != nil { return err } accessor.SetResourceVersion("") } oldData, err := json.Marshal(obj) if err != nil { return err } if err := o.Recorder.Record(info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } if err := o.updateAnnotations(obj); err != nil { return err } newData, err := json.Marshal(obj) if err != nil { return err } patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) createdPatch := err == nil if err != nil { klog.V(2).Infof("couldn't compute patch: %v", err) } client, err := o.unstructuredClientForMapping(mapping) if err != nil { return err } helper := resource. NewHelper(client, mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager) if createdPatch { outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil) } else { outputObj, err = helper.Replace(namespace, name, false, obj) } if err != nil { return err } } if o.list { accessor, err := meta.Accessor(outputObj) if err != nil { return err } indent := "" if !singleItemImpliedResource { indent = " " gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(info.Object) if err != nil { return err } fmt.Fprintf(o.Out, "Listing annotations for %s.%s/%s:\n", gvks[0].Kind, gvks[0].Group, info.Name) } for k, v := range accessor.GetAnnotations() { fmt.Fprintf(o.Out, "%s%s=%s\n", indent, k, v) } return nil } return o.PrintObj(outputObj, o.Out) }) } // parseAnnotations retrieves new and remove annotations from annotation args func parseAnnotations(annotationArgs []string) (map[string]string, []string, error) { return cmdutil.ParsePairs(annotationArgs, "annotation", true) } // validateAnnotations checks the format of annotation args and checks removed annotations aren't in the new annotations map func validateAnnotations(removeAnnotations []string, newAnnotations map[string]string) error { var modifyRemoveBuf bytes.Buffer for _, removeAnnotation := range removeAnnotations { if _, found := newAnnotations[removeAnnotation]; found { if modifyRemoveBuf.Len() > 0 { modifyRemoveBuf.WriteString(", ") } modifyRemoveBuf.WriteString(fmt.Sprint(removeAnnotation)) } } if modifyRemoveBuf.Len() > 0 { return fmt.Errorf("can not both modify and remove the following annotation(s) in the same command: %s", modifyRemoveBuf.String()) } return nil } // validateNoAnnotationOverwrites validates that when overwrite is false, to-be-updated annotations don't exist in the object annotation map (yet) func validateNoAnnotationOverwrites(accessor metav1.Object, annotations map[string]string) error { var buf bytes.Buffer for key, value := range annotations { // change-cause annotation can always be overwritten if key == polymorphichelpers.ChangeCauseAnnotation { continue } if currValue, found := accessor.GetAnnotations()[key]; found && currValue != value { if buf.Len() > 0 { buf.WriteString("; ") } buf.WriteString(fmt.Sprintf("'%s' already has a value (%s)", key, currValue)) } } if buf.Len() > 0 { return fmt.Errorf("--overwrite is false but found the following declared annotation(s): %s", buf.String()) } return nil } // updateAnnotations updates annotations of obj func (o AnnotateOptions) updateAnnotations(obj runtime.Object) error { accessor, err := meta.Accessor(obj) if err != nil { return err } if !o.overwrite { if err := validateNoAnnotationOverwrites(accessor, o.newAnnotations); err != nil { return err } } annotations := accessor.GetAnnotations() if annotations == nil { annotations = make(map[string]string) } for key, value := range o.newAnnotations { annotations[key] = value } for _, annotation := range o.removeAnnotations { delete(annotations, annotation) } accessor.SetAnnotations(annotations) if len(o.resourceVersion) != 0 { accessor.SetResourceVersion(o.resourceVersion) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate_test.go000066400000000000000000000523231504711711200316240ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package annotate import ( "bytes" "io" "net/http" "reflect" "strings" "testing" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestValidateAnnotationOverwrites(t *testing.T) { tests := []struct { meta *metav1.ObjectMeta annotations map[string]string expectErr bool scenario string }{ { meta: &metav1.ObjectMeta{ Annotations: map[string]string{ "a": "A", "b": "B", }, }, annotations: map[string]string{ "a": "a", "c": "C", }, scenario: "share first annotation", expectErr: true, }, { meta: &metav1.ObjectMeta{ Annotations: map[string]string{ "a": "A", "c": "C", }, }, annotations: map[string]string{ "b": "B", "c": "c", }, scenario: "share second annotation", expectErr: true, }, { meta: &metav1.ObjectMeta{ Annotations: map[string]string{ "a": "A", "c": "C", }, }, annotations: map[string]string{ "b": "B", "d": "D", }, scenario: "no overlap", }, { meta: &metav1.ObjectMeta{}, annotations: map[string]string{ "a": "A", "b": "B", }, scenario: "no annotations", }, } for _, test := range tests { err := validateNoAnnotationOverwrites(test.meta, test.annotations) if test.expectErr && err == nil { t.Errorf("%s: unexpected non-error", test.scenario) } else if !test.expectErr && err != nil { t.Errorf("%s: unexpected error: %v", test.scenario, err) } } } func TestParseAnnotations(t *testing.T) { testURL := "https://test.com/index.htm?id=123#u=user-name" testJSON := `'{"kind":"SerializedReference","apiVersion":"v1","reference":{"kind":"ReplicationController","namespace":"default","name":"my-nginx","uid":"c544ee78-2665-11e5-8051-42010af0c213","apiVersion":"v1","resourceVersion":"61368"}}'` tests := []struct { annotations []string expected map[string]string expectedRemove []string scenario string expectedErr string expectErr bool }{ { annotations: []string{"a=b", "c=d"}, expected: map[string]string{"a": "b", "c": "d"}, expectedRemove: []string{}, scenario: "add two annotations", expectErr: false, }, { annotations: []string{"url=" + testURL, "fake.kubernetes.io/annotation=" + testJSON}, expected: map[string]string{"url": testURL, "fake.kubernetes.io/annotation": testJSON}, expectedRemove: []string{}, scenario: "add annotations with special characters", expectErr: false, }, { annotations: []string{}, expected: map[string]string{}, expectedRemove: []string{}, scenario: "add no annotations", expectErr: false, }, { annotations: []string{"a=b", "c=d", "e-"}, expected: map[string]string{"a": "b", "c": "d"}, expectedRemove: []string{"e"}, scenario: "add two annotations, remove one", expectErr: false, }, { annotations: []string{"ab", "c=d"}, expectedErr: "invalid annotation format: ab", scenario: "incorrect annotation input (missing =value)", expectErr: true, }, { annotations: []string{"a="}, expected: map[string]string{"a": ""}, expectedRemove: []string{}, scenario: "add valid annotation with empty value", expectErr: false, }, { annotations: []string{"ab", "a="}, expectedErr: "invalid annotation format: ab", scenario: "incorrect annotation input (missing =value)", expectErr: true, }, { annotations: []string{"-"}, expectedErr: "invalid annotation format: -", scenario: "incorrect annotation input (missing key)", expectErr: true, }, { annotations: []string{"=bar"}, expectedErr: "invalid annotation format: =bar", scenario: "incorrect annotation input (missing key)", expectErr: true, }, } for _, test := range tests { annotations, remove, err := parseAnnotations(test.annotations) switch { case test.expectErr && err == nil: t.Errorf("%s: unexpected non-error, should return %v", test.scenario, test.expectedErr) case test.expectErr && err.Error() != test.expectedErr: t.Errorf("%s: unexpected error %v, expected %v", test.scenario, err, test.expectedErr) case !test.expectErr && err != nil: t.Errorf("%s: unexpected error %v", test.scenario, err) case !test.expectErr && !reflect.DeepEqual(annotations, test.expected): t.Errorf("%s: expected %v, got %v", test.scenario, test.expected, annotations) case !test.expectErr && !reflect.DeepEqual(remove, test.expectedRemove): t.Errorf("%s: expected %v, got %v", test.scenario, test.expectedRemove, remove) } } } func TestValidateAnnotations(t *testing.T) { tests := []struct { removeAnnotations []string newAnnotations map[string]string expectedErr string scenario string }{ { expectedErr: "can not both modify and remove the following annotation(s) in the same command: a", removeAnnotations: []string{"a"}, newAnnotations: map[string]string{"a": "b", "c": "d"}, scenario: "remove an added annotation", }, { expectedErr: "can not both modify and remove the following annotation(s) in the same command: a, c", removeAnnotations: []string{"a", "c"}, newAnnotations: map[string]string{"a": "b", "c": "d"}, scenario: "remove added annotations", }, } for _, test := range tests { if err := validateAnnotations(test.removeAnnotations, test.newAnnotations); err == nil { t.Errorf("%s: unexpected non-error", test.scenario) } else if err.Error() != test.expectedErr { t.Errorf("%s: expected error %s, got %s", test.scenario, test.expectedErr, err.Error()) } } } func TestUpdateAnnotations(t *testing.T) { tests := []struct { obj runtime.Object overwrite bool version string annotations map[string]string remove []string expected runtime.Object expectedErr string }{ { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, annotations: map[string]string{"a": "b"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, annotations: map[string]string{"a": "c"}, expectedErr: "--overwrite is false but found the following declared annotation(s): 'a' already has a value (b)", }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, annotations: map[string]string{"a": "c"}, overwrite: true, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "c"}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, annotations: map[string]string{"c": "d"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b", "c": "d"}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, annotations: map[string]string{"c": "d"}, version: "2", expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b", "c": "d"}, ResourceVersion: "2", }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, annotations: map[string]string{}, remove: []string{"a"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b", "c": "d"}, }, }, annotations: map[string]string{"e": "f"}, remove: []string{"a"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ "c": "d", "e": "f", }, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b", "c": "d"}, }, }, annotations: map[string]string{"e": "f"}, remove: []string{"g"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ "a": "b", "c": "d", "e": "f", }, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b", "c": "d"}, }, }, remove: []string{"e"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ "a": "b", "c": "d", }, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{}, }, annotations: map[string]string{"a": "b"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{"a": "b"}, }, }, }, } for _, test := range tests { options := &AnnotateOptions{ overwrite: test.overwrite, newAnnotations: test.annotations, removeAnnotations: test.remove, resourceVersion: test.version, } err := options.updateAnnotations(test.obj) if test.expectedErr != "" { if err == nil { t.Errorf("unexpected non-error: %v", test) } if err.Error() != test.expectedErr { t.Errorf("error expected: %v, got: %v", test.expectedErr, err.Error()) } continue } if test.expectedErr == "" && err != nil { t.Errorf("unexpected error: %v %v", err, test) } if !reflect.DeepEqual(test.obj, test.expected) { t.Errorf("expected: %v, got %v", test.expected, test.obj) } } } func TestAnnotateErrors(t *testing.T) { testCases := map[string]struct { args []string errFn func(error) bool }{ "no args": { args: []string{}, errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") }, }, "not enough annotations": { args: []string{"pods"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "at least one annotation update is required") }, }, "wrong annotations": { args: []string{"pods", "-"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "at least one annotation update is required") }, }, "wrong annotations 2": { args: []string{"pods", "=bar"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "at least one annotation update is required") }, }, "no resources remove annotations": { args: []string{"pods-"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") }, }, "no resources add annotations": { args: []string{"pods=bar"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") }, }, } for k, testCase := range testCases { t.Run(k, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() iostreams, _, bufOut, bufErr := genericiooptions.NewTestIOStreams() cmd := NewCmdAnnotate("kubectl", tf, iostreams) cmd.SetOut(bufOut) cmd.SetErr(bufOut) flags := NewAnnotateFlags(iostreams) _, err := flags.ToOptions(tf, cmd, testCase.args) if !testCase.errFn(err) { t.Errorf("%s: unexpected error: %v", k, err) return } if bufOut.Len() > 0 { t.Errorf("buffer should be empty: %s", bufOut.String()) } if bufErr.Len() > 0 { t.Errorf("buffer should be empty: %s", bufErr.String()) } }) } } func TestAnnotateObject(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() iostreams, _, bufOut, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdAnnotate("kubectl", tf, iostreams) cmd.SetOut(bufOut) cmd.SetErr(bufOut) flags := NewAnnotateFlags(iostreams) args := []string{"pods/foo", "a=b", "c-"} options, err := flags.ToOptions(tf, cmd, args) if err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.RunAnnotate(); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestAnnotateResourceVersion(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewBufferString( `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"10"}}`, ))}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/pods/foo": body, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } if !bytes.Equal(body, []byte(`{"metadata":{"annotations":{"a":"b"},"resourceVersion":"10"}}`)) { t.Fatalf("expected patch with resourceVersion set, got %s", string(body)) } return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewBufferString( `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"11"}}`, ))}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() iostreams, _, bufOut, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdAnnotate("kubectl", tf, iostreams) cmd.SetOut(bufOut) cmd.SetErr(bufOut) //options := NewAnnotateOptions(iostreams) flags := NewAnnotateFlags(iostreams) flags.resourceVersion = "10" args := []string{"pods/foo", "a=b"} options, err := flags.ToOptions(tf, cmd, args) if err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.RunAnnotate(); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestAnnotateObjectFromFile(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/replicationcontrollers/cassandra": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/replicationcontrollers/cassandra": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() iostreams, _, bufOut, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdAnnotate("kubectl", tf, iostreams) cmd.SetOut(bufOut) cmd.SetErr(bufOut) flags := NewAnnotateFlags(iostreams) flags.Filenames = []string{"../../../testdata/controller.yaml"} args := []string{"a=b", "c-"} options, err := flags.ToOptions(tf, cmd, args) if err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.RunAnnotate(); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestAnnotateLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() iostreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdAnnotate("kubectl", tf, iostreams) flags := NewAnnotateFlags(iostreams) flags.Local = true flags.Filenames = []string{"../../../testdata/controller.yaml"} args := []string{"a=b"} options, err := flags.ToOptions(tf, cmd, args) if err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.RunAnnotate(); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestAnnotateMultipleObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil case "/namespaces/test/pods/bar": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[1])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() iostreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdAnnotate("kubectl", tf, iostreams) cmd.SetOut(iostreams.Out) cmd.SetErr(iostreams.Out) flags := NewAnnotateFlags(iostreams) flags.All = true args := []string{"pods", "a=b", "c-"} options, err := flags.ToOptions(tf, cmd, args) if err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.RunAnnotate(); err != nil { t.Fatalf("unexpected error: %v", err) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/000077500000000000000000000000001504711711200273135ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go000066400000000000000000000215041504711711200323500ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apiresources import ( "fmt" "io" "sort" "strings" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/discovery" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( apiresourcesExample = templates.Examples(` # Print the supported API resources kubectl api-resources # Print the supported API resources with more information kubectl api-resources -o wide # Print the supported API resources sorted by a column kubectl api-resources --sort-by=name # Print the supported namespaced resources kubectl api-resources --namespaced=true # Print the supported non-namespaced resources kubectl api-resources --namespaced=false # Print the supported API resources with a specific APIGroup kubectl api-resources --api-group=rbac.authorization.k8s.io`) ) // APIResourceOptions is the start of the data required to perform the operation. // As new fields are added, add them here instead of referencing the cmd.Flags() type APIResourceOptions struct { Output string SortBy string APIGroup string Namespaced bool Verbs []string NoHeaders bool Cached bool Categories []string groupChanged bool nsChanged bool discoveryClient discovery.CachedDiscoveryInterface genericiooptions.IOStreams } // groupResource contains the APIGroup and APIResource type groupResource struct { APIGroup string APIGroupVersion string APIResource metav1.APIResource } // NewAPIResourceOptions creates the options for APIResource func NewAPIResourceOptions(ioStreams genericiooptions.IOStreams) *APIResourceOptions { return &APIResourceOptions{ IOStreams: ioStreams, Namespaced: true, } } // NewCmdAPIResources creates the `api-resources` command func NewCmdAPIResources(restClientGetter genericclioptions.RESTClientGetter, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewAPIResourceOptions(ioStreams) cmd := &cobra.Command{ Use: "api-resources", Short: i18n.T("Print the supported API resources on the server"), Long: i18n.T("Print the supported API resources on the server."), Example: apiresourcesExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(restClientGetter, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunAPIResources()) }, } cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).") cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, `Output format. One of: (wide, name).`) cmd.Flags().StringVar(&o.APIGroup, "api-group", o.APIGroup, "Limit to resources in the specified API group.") cmd.Flags().BoolVar(&o.Namespaced, "namespaced", o.Namespaced, "If false, non-namespaced resources will be returned, otherwise returning namespaced resources by default.") cmd.Flags().StringSliceVar(&o.Verbs, "verbs", o.Verbs, "Limit to resources that support the specified verbs.") cmd.Flags().StringVar(&o.SortBy, "sort-by", o.SortBy, "If non-empty, sort list of resources using specified field. The field can be either 'name' or 'kind'.") cmd.Flags().BoolVar(&o.Cached, "cached", o.Cached, "Use the cached list of resources if available.") cmd.Flags().StringSliceVar(&o.Categories, "categories", o.Categories, "Limit to resources that belong to the specified categories.") return cmd } // Validate checks to the APIResourceOptions to see if there is sufficient information run the command func (o *APIResourceOptions) Validate() error { supportedOutputTypes := sets.New[string]("", "wide", "name") if !supportedOutputTypes.Has(o.Output) { return fmt.Errorf("--output %v is not available", o.Output) } supportedSortTypes := sets.New[string]("", "name", "kind") if len(o.SortBy) > 0 { if !supportedSortTypes.Has(o.SortBy) { return fmt.Errorf("--sort-by accepts only name or kind") } } return nil } // Complete adapts from the command line args and validates them func (o *APIResourceOptions) Complete(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args) } discoveryClient, err := restClientGetter.ToDiscoveryClient() if err != nil { return err } o.discoveryClient = discoveryClient o.groupChanged = cmd.Flags().Changed("api-group") o.nsChanged = cmd.Flags().Changed("namespaced") return nil } // RunAPIResources does the work func (o *APIResourceOptions) RunAPIResources() error { w := printers.GetNewTabWriter(o.Out) defer w.Flush() if !o.Cached { // Always request fresh data from the server o.discoveryClient.Invalidate() } errs := []error{} lists, err := o.discoveryClient.ServerPreferredResources() if err != nil { errs = append(errs, err) } resources := []groupResource{} for _, list := range lists { if len(list.APIResources) == 0 { continue } gv, err := schema.ParseGroupVersion(list.GroupVersion) if err != nil { continue } for _, resource := range list.APIResources { if len(resource.Verbs) == 0 { continue } // filter apiGroup if o.groupChanged && o.APIGroup != gv.Group { continue } // filter namespaced if o.nsChanged && o.Namespaced != resource.Namespaced { continue } // filter to resources that support the specified verbs if len(o.Verbs) > 0 && !sets.New[string](resource.Verbs...).HasAll(o.Verbs...) { continue } // filter to resources that belong to the specified categories if len(o.Categories) > 0 && !sets.New[string](resource.Categories...).HasAll(o.Categories...) { continue } resources = append(resources, groupResource{ APIGroup: gv.Group, APIGroupVersion: gv.String(), APIResource: resource, }) } } if o.NoHeaders == false && o.Output != "name" { if err = printContextHeaders(w, o.Output); err != nil { return err } } sort.Stable(sortableResource{resources, o.SortBy}) for _, r := range resources { switch o.Output { case "name": name := r.APIResource.Name if len(r.APIGroup) > 0 { name += "." + r.APIGroup } if _, err := fmt.Fprintf(w, "%s\n", name); err != nil { errs = append(errs, err) } case "wide": if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\t%v\n", r.APIResource.Name, strings.Join(r.APIResource.ShortNames, ","), r.APIGroupVersion, r.APIResource.Namespaced, r.APIResource.Kind, strings.Join(r.APIResource.Verbs, ","), strings.Join(r.APIResource.Categories, ",")); err != nil { errs = append(errs, err) } case "": if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n", r.APIResource.Name, strings.Join(r.APIResource.ShortNames, ","), r.APIGroupVersion, r.APIResource.Namespaced, r.APIResource.Kind); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.NewAggregate(errs) } return nil } func printContextHeaders(out io.Writer, output string) error { columnNames := []string{"NAME", "SHORTNAMES", "APIVERSION", "NAMESPACED", "KIND"} if output == "wide" { columnNames = append(columnNames, "VERBS", "CATEGORIES") } _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t")) return err } type sortableResource struct { resources []groupResource sortBy string } func (s sortableResource) Len() int { return len(s.resources) } func (s sortableResource) Swap(i, j int) { s.resources[i], s.resources[j] = s.resources[j], s.resources[i] } func (s sortableResource) Less(i, j int) bool { ret := strings.Compare(s.compareValues(i, j)) if ret > 0 { return false } else if ret == 0 { return strings.Compare(s.resources[i].APIResource.Name, s.resources[j].APIResource.Name) < 0 } return true } func (s sortableResource) compareValues(i, j int) (string, string) { switch s.sortBy { case "name": return s.resources[i].APIResource.Name, s.resources[j].APIResource.Name case "kind": return s.resources[i].APIResource.Kind, s.resources[j].APIResource.Kind } return s.resources[i].APIGroup, s.resources[j].APIGroup } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources_test.go000066400000000000000000000221731504711711200334120ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apiresources import ( "testing" "github.com/spf13/cobra" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestAPIResourcesComplete(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() cmd := NewCmdAPIResources(tf, genericiooptions.NewTestIOStreamsDiscard()) parentCmd := &cobra.Command{Use: "kubectl"} parentCmd.AddCommand(cmd) o := NewAPIResourceOptions(genericiooptions.NewTestIOStreamsDiscard()) err := o.Complete(tf, cmd, []string{}) if err != nil { t.Fatalf("Unexpected error: %v", err) } err = o.Complete(tf, cmd, []string{"foo"}) if err == nil { t.Fatalf("An error was expected but not returned") } expectedError := `unexpected arguments: [foo] See 'kubectl api-resources -h' for help and examples` if err.Error() != expectedError { t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError) } } func TestAPIResourcesValidate(t *testing.T) { testCases := []struct { name string optionSetupFn func(o *APIResourceOptions) expectedError string }{ { name: "no errors", optionSetupFn: func(o *APIResourceOptions) {}, expectedError: "", }, { name: "invalid output", optionSetupFn: func(o *APIResourceOptions) { o.Output = "foo" }, expectedError: "--output foo is not available", }, { name: "invalid sort by", optionSetupFn: func(o *APIResourceOptions) { o.SortBy = "foo" }, expectedError: "--sort-by accepts only name or kind", }, } for _, tc := range testCases { t.Run(tc.name, func(tt *testing.T) { o := NewAPIResourceOptions(genericiooptions.NewTestIOStreamsDiscard()) tc.optionSetupFn(o) err := o.Validate() if tc.expectedError == "" { if err != nil { tt.Fatalf("Unexpected error: %v", err) } } else { if err == nil { tt.Fatalf("An error was expected but not returned") } if err.Error() != tc.expectedError { tt.Fatalf("Unexpected error: %v, expected: %v", err, tc.expectedError) } } }) } } func TestAPIResourcesRun(t *testing.T) { dc := cmdtesting.NewFakeCachedDiscoveryClient() dc.PreferredResources = []*v1.APIResourceList{ { GroupVersion: "v1", APIResources: []v1.APIResource{ { Name: "foos", Namespaced: false, Kind: "Foo", Verbs: []string{"get", "list"}, ShortNames: []string{"f", "fo"}, Categories: []string{"some-category"}, }, { Name: "bars", Namespaced: true, Kind: "Bar", Verbs: []string{"get", "list", "create"}, ShortNames: []string{}, Categories: []string{}, }, }, }, { GroupVersion: "somegroup/v1", APIResources: []v1.APIResource{ { Name: "bazzes", Namespaced: true, Kind: "Baz", Verbs: []string{"get", "list", "create", "delete"}, ShortNames: []string{"b"}, Categories: []string{"some-category", "another-category"}, }, { Name: "NoVerbs", Namespaced: true, Kind: "NoVerbs", Verbs: []string{}, ShortNames: []string{"b"}, Categories: []string{}, }, }, }, { GroupVersion: "someothergroup/v1", APIResources: []v1.APIResource{}, }, } tf := cmdtesting.NewTestFactory().WithDiscoveryClient(dc) defer tf.Cleanup() testCases := []struct { name string commandSetupFn func(cmd *cobra.Command) expectedOutput string expectedInvalidations int }{ { name: "defaults", commandSetupFn: func(cmd *cobra.Command) {}, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bars v1 true Bar foos f,fo v1 false Foo bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "no headers", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("no-headers", "true") }, expectedOutput: `bars v1 true Bar foos f,fo v1 false Foo bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "specific api group", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("api-group", "somegroup") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "output wide", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("output", "wide") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND VERBS CATEGORIES bars v1 true Bar get,list,create foos f,fo v1 false Foo get,list some-category bazzes b somegroup/v1 true Baz get,list,create,delete some-category,another-category `, expectedInvalidations: 1, }, { name: "output name", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("output", "name") }, expectedOutput: `bars foos bazzes.somegroup `, expectedInvalidations: 1, }, { name: "namespaced", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("namespaced", "true") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bars v1 true Bar bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "single verb", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("verbs", "create") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bars v1 true Bar bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "multiple verbs", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("verbs", "create,delete") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "single category", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("categories", "some-category") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND foos f,fo v1 false Foo bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "multiple categories", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("categories", "some-category,another-category") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bazzes b somegroup/v1 true Baz `, expectedInvalidations: 1, }, { name: "sort by name", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("sort-by", "name") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bars v1 true Bar bazzes b somegroup/v1 true Baz foos f,fo v1 false Foo `, expectedInvalidations: 1, }, { name: "sort by kind", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("sort-by", "kind") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bars v1 true Bar bazzes b somegroup/v1 true Baz foos f,fo v1 false Foo `, expectedInvalidations: 1, }, { name: "cached", commandSetupFn: func(cmd *cobra.Command) { cmd.Flags().Set("cached", "true") }, expectedOutput: `NAME SHORTNAMES APIVERSION NAMESPACED KIND bars v1 true Bar foos f,fo v1 false Foo bazzes b somegroup/v1 true Baz `, expectedInvalidations: 0, }, } for _, tc := range testCases { t.Run(tc.name, func(tt *testing.T) { dc.Invalidations = 0 ioStreams, _, out, errOut := genericiooptions.NewTestIOStreams() cmd := NewCmdAPIResources(tf, ioStreams) tc.commandSetupFn(cmd) cmd.Run(cmd, []string{}) if errOut.Len() > 0 { t.Fatalf("unexpected error output: %s", errOut.String()) } if out.String() != tc.expectedOutput { tt.Fatalf("unexpected output: %s\nexpected: %s", out.String(), tc.expectedOutput) } if dc.Invalidations != tc.expectedInvalidations { tt.Fatalf("unexpected invalidations: %d, expected: %d", dc.Invalidations, tc.expectedInvalidations) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiversions.go000066400000000000000000000060031504711711200322030ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apiresources import ( "fmt" "sort" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/discovery" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( apiversionsExample = templates.Examples(i18n.T(` # Print the supported API versions kubectl api-versions`)) ) // APIVersionsOptions have the data required for API versions type APIVersionsOptions struct { discoveryClient discovery.CachedDiscoveryInterface genericiooptions.IOStreams } // NewAPIVersionsOptions creates the options for APIVersions func NewAPIVersionsOptions(ioStreams genericiooptions.IOStreams) *APIVersionsOptions { return &APIVersionsOptions{ IOStreams: ioStreams, } } // NewCmdAPIVersions creates the `api-versions` command func NewCmdAPIVersions(restClientGetter genericclioptions.RESTClientGetter, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewAPIVersionsOptions(ioStreams) cmd := &cobra.Command{ Use: "api-versions", Short: i18n.T("Print the supported API versions on the server, in the form of \"group/version\""), Long: i18n.T("Print the supported API versions on the server, in the form of \"group/version\"."), Example: apiversionsExample, DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(restClientGetter, cmd, args)) cmdutil.CheckErr(o.RunAPIVersions()) }, } return cmd } // Complete adapts from the command line args and factory to the data required func (o *APIVersionsOptions) Complete(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args) } var err error o.discoveryClient, err = restClientGetter.ToDiscoveryClient() return err } // RunAPIVersions does the work func (o *APIVersionsOptions) RunAPIVersions() error { // Always request fresh data from the server o.discoveryClient.Invalidate() groupList, err := o.discoveryClient.ServerGroups() if err != nil { return fmt.Errorf("couldn't get available api versions from server: %v", err) } apiVersions := metav1.ExtractGroupVersions(groupList) sort.Strings(apiVersions) for _, v := range apiVersions { fmt.Fprintln(o.Out, v) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiversions_test.go000066400000000000000000000051631504711711200332500ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apiresources import ( "testing" "github.com/spf13/cobra" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestAPIVersionsComplete(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() cmd := NewCmdAPIVersions(tf, genericiooptions.NewTestIOStreamsDiscard()) parentCmd := &cobra.Command{Use: "kubectl"} parentCmd.AddCommand(cmd) o := NewAPIVersionsOptions(genericiooptions.NewTestIOStreamsDiscard()) err := o.Complete(tf, cmd, []string{}) if err != nil { t.Fatalf("Unexpected error: %v", err) } err = o.Complete(tf, cmd, []string{"foo"}) if err == nil { t.Fatalf("An error was expected but not returned") } expectedError := `unexpected arguments: [foo] See 'kubectl api-versions -h' for help and examples` if err.Error() != expectedError { t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError) } } func TestAPIVersionsRun(t *testing.T) { dc := cmdtesting.NewFakeCachedDiscoveryClient() dc.Groups = []*v1.APIGroup{ { Name: "", Versions: []v1.GroupVersionForDiscovery{ {GroupVersion: "v1"}, }, }, { Name: "foo", Versions: []v1.GroupVersionForDiscovery{ {GroupVersion: "foo/v1beta1"}, {GroupVersion: "foo/v1"}, {GroupVersion: "foo/v2"}, }, }, { Name: "bar", Versions: []v1.GroupVersionForDiscovery{ {GroupVersion: "bar/v1"}, }, }, } tf := cmdtesting.NewTestFactory().WithDiscoveryClient(dc) defer tf.Cleanup() ioStreams, _, out, errOut := genericiooptions.NewTestIOStreams() cmd := NewCmdAPIVersions(tf, ioStreams) cmd.Run(cmd, []string{}) if errOut.Len() > 0 { t.Fatalf("unexpected error output: %s", errOut.String()) } expectedOutput := `bar/v1 foo/v1 foo/v1beta1 foo/v2 v1 ` if out.String() != expectedOutput { t.Fatalf("unexpected output: %s\nexpected: %s", out.String(), expectedOutput) } expectedInvalidations := 1 if dc.Invalidations != expectedInvalidations { t.Fatalf("unexpected invalidations: %d, expected: %d", dc.Invalidations, expectedInvalidations) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/000077500000000000000000000000001504711711200257345ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go000066400000000000000000001061441504711711200274160ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "context" "fmt" "io" "net/http" "github.com/spf13/cobra" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" 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/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/client-go/openapi3" "k8s.io/client-go/util/csaupgrade" "k8s.io/component-base/version" "k8s.io/klog/v2" cmddelete "k8s.io/kubectl/pkg/cmd/delete" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/util/prune" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/validation" ) // ApplyFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which // reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes // the logic itself easy to unit test type ApplyFlags struct { RecordFlags *genericclioptions.RecordFlags PrintFlags *genericclioptions.PrintFlags DeleteFlags *cmddelete.DeleteFlags FieldManager string Selector string Prune bool PruneResources []prune.Resource ApplySetRef string All bool Overwrite bool OpenAPIPatch bool Subresource string PruneAllowlist []string genericiooptions.IOStreams } // ApplyOptions defines flags and other configuration parameters for the `apply` command type ApplyOptions struct { Recorder genericclioptions.Recorder PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) DeleteOptions *cmddelete.DeleteOptions ServerSideApply bool ForceConflicts bool FieldManager string Selector string DryRunStrategy cmdutil.DryRunStrategy Prune bool PruneResources []prune.Resource cmdBaseName string All bool Overwrite bool OpenAPIPatch bool Subresource string ValidationDirective string Validator validation.Schema Builder *resource.Builder Mapper meta.RESTMapper DynamicClient dynamic.Interface OpenAPIGetter openapi.OpenAPIResourcesGetter OpenAPIV3Root openapi3.Root Namespace string EnforceNamespace bool genericiooptions.IOStreams // Objects (and some denormalized data) which are to be // applied. The standard way to fill in this structure // is by calling "GetObjects()", which will use the // resource builder if "objectsCached" is false. The other // way to set this field is to use "SetObjects()". // Subsequent calls to "GetObjects()" after setting would // not call the resource builder; only return the set objects. objects []*resource.Info objectsCached bool // Stores visited objects/namespaces for later use // calculating the set of objects to prune. VisitedUids sets.Set[types.UID] VisitedNamespaces sets.Set[string] // Function run after the objects are generated and // stored in the "objects" field, but before the // apply is run on these objects. PreProcessorFn func() error // Function run after all objects have been applied. // The standard PostProcessorFn is "PrintAndPrunePostProcessor()". PostProcessorFn func() error // ApplySet tracks the set of objects that have been applied, for the purposes of pruning. // See git.k8s.io/enhancements/keps/sig-cli/3659-kubectl-apply-prune ApplySet *ApplySet } var ( applyLong = templates.LongDesc(i18n.T(` Apply a configuration to a resource by file name or stdin. The resource name must be specified. This resource will be created if it doesn't exist yet. To use 'apply', always create the resource initially with either 'apply' or 'create --save-config'. JSON and YAML formats are accepted. Alpha Disclaimer: the --prune functionality is not yet complete. Do not use unless you are aware of what the current state is. See https://issues.k8s.io/34274.`)) applyExample = templates.Examples(i18n.T(` # Apply the configuration in pod.json to a pod kubectl apply -f ./pod.json # Apply resources from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml kubectl apply -k dir/ # Apply the JSON passed into stdin to a pod cat pod.json | kubectl apply -f - # Apply the configuration from all files that end with '.json' kubectl apply -f '*.json' # Note: --prune is still in Alpha # Apply the configuration in manifest.yaml that matches label app=nginx and delete all other resources that are not in the file and match label app=nginx kubectl apply --prune -f manifest.yaml -l app=nginx # Apply the configuration in manifest.yaml and delete all the other config maps that are not in the file kubectl apply --prune -f manifest.yaml --all --prune-allowlist=core/v1/ConfigMap`)) warningNoLastAppliedConfigAnnotation = "Warning: resource %[1]s is missing the %[2]s annotation which is required by %[3]s apply. %[3]s apply should only be used on resources created declaratively by either %[3]s create --save-config or %[3]s apply. The missing annotation will be patched automatically.\n" warningChangesOnDeletingResource = "Warning: Detected changes to resource %[1]s which is currently being deleted.\n" warningMigrationLastAppliedFailed = "Warning: failed to migrate kubectl.kubernetes.io/last-applied-configuration for Server-Side Apply. This is non-fatal and will be retried next time you apply. Error: %[1]s\n" warningMigrationPatchFailed = "Warning: server rejected managed fields migration to Server-Side Apply. This is non-fatal and will be retried next time you apply. Error: %[1]s\n" warningMigrationReapplyFailed = "Warning: failed to re-apply configuration after performing Server-Side Apply migration. This is non-fatal and will be retried next time you apply. Error: %[1]s\n" ) var ApplySetToolVersion = version.Get().GitVersion // NewApplyFlags returns a default ApplyFlags func NewApplyFlags(streams genericiooptions.IOStreams) *ApplyFlags { return &ApplyFlags{ RecordFlags: genericclioptions.NewRecordFlags(), DeleteFlags: cmddelete.NewDeleteFlags("The files that contain the configurations to apply."), PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), Overwrite: true, OpenAPIPatch: true, IOStreams: streams, } } // NewCmdApply creates the `apply` command func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { flags := NewApplyFlags(ioStreams) cmd := &cobra.Command{ Use: "apply (-f FILENAME | -k DIRECTORY)", DisableFlagsInUseLine: true, Short: i18n.T("Apply a configuration to a resource by file name or stdin"), Long: applyLong, Example: applyExample, Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions(f, cmd, baseName, args) cmdutil.CheckErr(err) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } flags.AddFlags(cmd) // apply subcommands cmd.AddCommand(NewCmdApplyViewLastApplied(f, flags.IOStreams)) cmd.AddCommand(NewCmdApplySetLastApplied(f, flags.IOStreams)) cmd.AddCommand(NewCmdApplyEditLastApplied(f, flags.IOStreams)) return cmd } // AddFlags registers flags for a cli func (flags *ApplyFlags) AddFlags(cmd *cobra.Command) { // bind flag structs flags.DeleteFlags.AddFlags(cmd) flags.RecordFlags.AddFlags(cmd) flags.PrintFlags.AddFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmdutil.AddServerSideApplyFlags(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, FieldManagerClientSideApply) cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector) cmdutil.AddPruningFlags(cmd, &flags.Prune, &flags.PruneAllowlist, &flags.All, &flags.ApplySetRef) cmd.Flags().BoolVar(&flags.Overwrite, "overwrite", flags.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration") cmd.Flags().BoolVar(&flags.OpenAPIPatch, "openapi-patch", flags.OpenAPIPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.") cmdutil.AddSubresourceFlags(cmd, &flags.Subresource, "If specified, apply will operate on the subresource of the requested object. Only allowed when using --server-side.") } // ToOptions converts from CLI inputs to runtime inputs func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseName string, args []string) (*ApplyOptions, error) { if len(args) != 0 { return nil, cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) } serverSideApply := cmdutil.GetServerSideApplyFlag(cmd) forceConflicts := cmdutil.GetForceConflictsFlag(cmd) dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd) if err != nil { return nil, err } dynamicClient, err := f.DynamicClient() if err != nil { return nil, err } fieldManager := GetApplyFieldManagerFlag(cmd, serverSideApply) // allow for a success message operation to be specified at print time toPrinter := func(operation string) (printers.ResourcePrinter, error) { flags.PrintFlags.NamePrintFlags.Operation = operation cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStrategy) return flags.PrintFlags.ToPrinter() } flags.RecordFlags.Complete(cmd) recorder, err := flags.RecordFlags.ToRecorder() if err != nil { return nil, err } deleteOptions, err := flags.DeleteFlags.ToOptions(dynamicClient, flags.IOStreams) if err != nil { return nil, err } err = deleteOptions.FilenameOptions.RequireFilenameOrKustomize() if err != nil { return nil, err } var openAPIV3Root openapi3.Root if !cmdutil.OpenAPIV3Patch.IsDisabled() { openAPIV3Client, err := f.OpenAPIV3Client() if err == nil { openAPIV3Root = openapi3.NewRoot(openAPIV3Client) } else { klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2") } } validationDirective, err := cmdutil.GetValidationDirective(cmd) if err != nil { return nil, err } validator, err := f.Validator(validationDirective) if err != nil { return nil, err } builder := f.NewBuilder() mapper, err := f.ToRESTMapper() if err != nil { return nil, err } namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil { return nil, err } var applySet *ApplySet if flags.ApplySetRef != "" { parent, err := ParseApplySetParentRef(flags.ApplySetRef, mapper) if err != nil { return nil, fmt.Errorf("invalid parent reference %q: %w", flags.ApplySetRef, err) } // ApplySet uses the namespace value from the flag, but not from the kubeconfig or defaults // This means the namespace flag is required when using a namespaced parent. if enforceNamespace && parent.IsNamespaced() { parent.Namespace = namespace } tooling := ApplySetTooling{Name: baseName, Version: ApplySetToolVersion} restClient, err := f.UnstructuredClientForMapping(parent.RESTMapping) if err != nil { return nil, fmt.Errorf("failed to initialize RESTClient for ApplySet: %w", err) } if restClient == nil { return nil, fmt.Errorf("could not build RESTClient for ApplySet") } applySet = NewApplySet(parent, tooling, mapper, restClient) } if flags.Prune { flags.PruneResources, err = prune.ParseResources(mapper, flags.PruneAllowlist) if err != nil { return nil, err } } o := &ApplyOptions{ // Store baseName for use in printing warnings / messages involving the base command name. // This is useful for downstream command that wrap this one. cmdBaseName: baseName, PrintFlags: flags.PrintFlags, DeleteOptions: deleteOptions, ToPrinter: toPrinter, ServerSideApply: serverSideApply, ForceConflicts: forceConflicts, FieldManager: fieldManager, Selector: flags.Selector, DryRunStrategy: dryRunStrategy, Prune: flags.Prune, PruneResources: flags.PruneResources, All: flags.All, Overwrite: flags.Overwrite, OpenAPIPatch: flags.OpenAPIPatch, Subresource: flags.Subresource, Recorder: recorder, Namespace: namespace, EnforceNamespace: enforceNamespace, Validator: validator, ValidationDirective: validationDirective, Builder: builder, Mapper: mapper, DynamicClient: dynamicClient, OpenAPIGetter: f, OpenAPIV3Root: openAPIV3Root, IOStreams: flags.IOStreams, objects: []*resource.Info{}, objectsCached: false, VisitedUids: sets.New[types.UID](), VisitedNamespaces: sets.New[string](), ApplySet: applySet, } o.PostProcessorFn = o.PrintAndPrunePostProcessor() return o, nil } // Validate verifies if ApplyOptions are valid and without conflicts. func (o *ApplyOptions) Validate() error { if o.ForceConflicts && !o.ServerSideApply { return fmt.Errorf("--force-conflicts only works with --server-side") } if o.DryRunStrategy == cmdutil.DryRunClient && o.ServerSideApply { return fmt.Errorf("--dry-run=client doesn't work with --server-side (did you mean --dry-run=server instead?)") } if o.ServerSideApply && o.DeleteOptions.ForceDeletion { return fmt.Errorf("--force cannot be used with --server-side") } if o.DryRunStrategy == cmdutil.DryRunServer && o.DeleteOptions.ForceDeletion { return fmt.Errorf("--dry-run=server cannot be used with --force") } if o.All && len(o.Selector) > 0 { return fmt.Errorf("cannot set --all and --selector at the same time") } if o.ApplySet != nil { if !o.Prune { return fmt.Errorf("--applyset requires --prune") } if err := o.ApplySet.Validate(context.TODO(), o.DynamicClient); err != nil { return err } } if o.Prune { // Do not force the recreation of an object(s) if we're pruning; this can cause // undefined behavior since object UID's change. if o.DeleteOptions.ForceDeletion { return fmt.Errorf("--force cannot be used with --prune") } if o.ApplySet != nil { if o.All { return fmt.Errorf("--all is incompatible with --applyset") } else if o.Selector != "" { return fmt.Errorf("--selector is incompatible with --applyset") } else if len(o.PruneResources) > 0 { return fmt.Errorf("--prune-allowlist is incompatible with --applyset") } } else { if !o.All && o.Selector == "" { return fmt.Errorf("all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector") } if o.ServerSideApply { return fmt.Errorf("--prune is in alpha and doesn't currently work on objects created by server-side apply") } } } if len(o.Subresource) > 0 && !o.ServerSideApply { return fmt.Errorf("--subresource can only be specified for --server-side") } return nil } func isIncompatibleServerError(err error) bool { // 415: Unsupported media type means we're talking to a server which doesn't // support server-side apply. if _, ok := err.(*errors.StatusError); !ok { // Non-StatusError means the error isn't because the server is incompatible. return false } return err.(*errors.StatusError).Status().Code == http.StatusUnsupportedMediaType } // GetObjects returns a (possibly cached) version of all the valid objects to apply // as a slice of pointer to resource.Info and an error if one or more occurred. // IMPORTANT: This function can return both valid objects AND an error, since // "ContinueOnError" is set on the builder. This function should not be called // until AFTER the "complete" and "validate" methods have been called to ensure that // the ApplyOptions is filled in and valid. func (o *ApplyOptions) GetObjects() ([]*resource.Info, error) { var err error = nil if !o.objectsCached { r := o.Builder. Unstructured(). Schema(o.Validator). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). LabelSelectorParam(o.Selector). Flatten(). Do() o.objects, err = r.Infos() if o.ApplySet != nil { if err := o.ApplySet.AddLabels(o.objects...); err != nil { return nil, err } } o.objectsCached = true } return o.objects, err } // SetObjects stores the set of objects (as resource.Info) to be // subsequently applied. func (o *ApplyOptions) SetObjects(infos []*resource.Info) { o.objects = infos o.objectsCached = true } // Run executes the `apply` command. func (o *ApplyOptions) Run() error { if o.PreProcessorFn != nil { klog.V(4).Infof("Running apply pre-processor function") if err := o.PreProcessorFn(); err != nil { return err } } // Enforce CLI specified namespace on server request. if o.EnforceNamespace { o.VisitedNamespaces.Insert(o.Namespace) } // Generates the objects using the resource builder if they have not // already been stored by calling "SetObjects()" in the pre-processor. errs := []error{} infos, err := o.GetObjects() if err != nil { errs = append(errs, err) } if len(infos) == 0 && len(errs) == 0 { return fmt.Errorf("no objects passed to apply") } if o.ApplySet != nil { if err := o.ApplySet.BeforeApply(infos, o.DryRunStrategy, o.ValidationDirective); err != nil { return err } } // Iterate through all objects, applying each one. for _, info := range infos { if err := o.applyOneObject(info); err != nil { errs = append(errs, err) } } // If any errors occurred during apply, then return error (or // aggregate of errors). if len(errs) == 1 { return errs[0] } if len(errs) > 1 { return utilerrors.NewAggregate(errs) } if o.PostProcessorFn != nil { klog.V(4).Infof("Running apply post-processor function") if err := o.PostProcessorFn(); err != nil { return err } } return nil } func (o *ApplyOptions) applyOneObject(info *resource.Info) error { o.MarkNamespaceVisited(info) if err := o.Recorder.Record(info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } if len(info.Name) == 0 { metadata, _ := meta.Accessor(info.Object) generatedName := metadata.GetGenerateName() if len(generatedName) > 0 { return fmt.Errorf("from %s: cannot use generate name with apply", generatedName) } } helper := resource.NewHelper(info.Client, info.Mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.FieldManager). WithFieldValidation(o.ValidationDirective) if o.ServerSideApply { // Send the full object to be applied on the server side. data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object) if err != nil { return cmdutil.AddSourceToErr("serverside-apply", info.Source, err) } options := metav1.PatchOptions{ Force: &o.ForceConflicts, } obj, err := helper. WithSubresource(o.Subresource). Patch( info.Namespace, info.Name, types.ApplyPatchType, data, &options, ) if err != nil { if isIncompatibleServerError(err) { err = fmt.Errorf("Server-side apply not available on the server: (%v)", err) } if errors.IsConflict(err) { err = fmt.Errorf(`%v Please review the fields above--they currently have other managers. Here are the ways you can resolve this warning: * If you intend to manage all of these fields, please re-run the apply command with the `+"`--force-conflicts`"+` flag. * If you do not intend to manage all of the fields, please edit your manifest to remove references to the fields that should keep their current managers. * You may co-own fields by updating your manifest to match the existing value; in this case, you'll become the manager if the other manager(s) stop managing the field (remove it from their configuration). See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`, err) } return err } info.Refresh(obj, true) // Migrate managed fields if necessary. // // By checking afterward instead of fetching the object beforehand and // unconditionally fetching we can make 3 network requests in the rare // case of migration and 1 request if migration is unnecessary. // // To check beforehand means 2 requests for most operations, and 3 // requests in worst case. if err = o.saveLastApplyAnnotationIfNecessary(helper, info); err != nil { fmt.Fprintf(o.ErrOut, warningMigrationLastAppliedFailed, err.Error()) } else if performedMigration, err := o.migrateToSSAIfNecessary(helper, info); err != nil { // Print-error as a warning. // This is a non-fatal error because object was successfully applied // above, but it might have issues since migration failed. // // This migration will be re-attempted if necessary upon next // apply. fmt.Fprintf(o.ErrOut, warningMigrationPatchFailed, err.Error()) } else if performedMigration { if obj, err = helper.Patch( info.Namespace, info.Name, types.ApplyPatchType, data, &options, ); err != nil { // Re-send original SSA patch (this will allow dropped fields to // finally be removed) fmt.Fprintf(o.ErrOut, warningMigrationReapplyFailed, err.Error()) } else { info.Refresh(obj, false) } } WarnIfDeleting(info.Object, o.ErrOut) if err := o.MarkObjectVisited(info); err != nil { return err } if o.shouldPrintObject() { return nil } printer, err := o.ToPrinter("serverside-applied") if err != nil { return err } if err = printer.PrintObj(info.Object, o.Out); err != nil { return err } return nil } // Get the modified configuration of the object. Embed the result // as an annotation in the modified configuration, so that it will appear // in the patch sent to the server. modified, err := util.GetModifiedConfiguration(info.Object, true, unstructured.UnstructuredJSONScheme) if err != nil { return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving modified configuration from:\n%s\nfor:", info.String()), info.Source, err) } if err := info.Get(); err != nil { if !errors.IsNotFound(err) { return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err) } // Create the resource if it doesn't exist // First, update the annotation used by kubectl apply if err := util.CreateApplyAnnotation(info.Object, unstructured.UnstructuredJSONScheme); err != nil { return cmdutil.AddSourceToErr("creating", info.Source, err) } // prune nulls when client-side apply does a create to match what will happen when client-side applying an update. // do this after CreateApplyAnnotation so the annotation matches what will be persisted on an update apply of the same manifest. if u, ok := info.Object.(runtime.Unstructured); ok { pruneNullsFromMap(u.UnstructuredContent()) } if o.DryRunStrategy != cmdutil.DryRunClient { // Then create the resource and skip the three-way merge obj, err := helper.Create(info.Namespace, true, info.Object) if err != nil { return cmdutil.AddSourceToErr("creating", info.Source, err) } info.Refresh(obj, true) } if err := o.MarkObjectVisited(info); err != nil { return err } if o.shouldPrintObject() { return nil } printer, err := o.ToPrinter("created") if err != nil { return err } if err = printer.PrintObj(info.Object, o.Out); err != nil { return err } return nil } if err := o.MarkObjectVisited(info); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { metadata, _ := meta.Accessor(info.Object) annotationMap := metadata.GetAnnotations() if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok { fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, info.ObjectName(), corev1.LastAppliedConfigAnnotation, o.cmdBaseName) } patcher, err := newPatcher(o, info, helper) if err != nil { return err } patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut) if err != nil { return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err) } info.Refresh(patchedObject, true) WarnIfDeleting(info.Object, o.ErrOut) if string(patchBytes) == "{}" && !o.shouldPrintObject() { printer, err := o.ToPrinter("unchanged") if err != nil { return err } if err = printer.PrintObj(info.Object, o.Out); err != nil { return err } return nil } } if o.shouldPrintObject() { return nil } printer, err := o.ToPrinter("configured") if err != nil { return err } if err = printer.PrintObj(info.Object, o.Out); err != nil { return err } return nil } func pruneNullsFromMap(data map[string]interface{}) { for k, v := range data { if v == nil { delete(data, k) } else { pruneNulls(v) } } } func pruneNullsFromSlice(data []interface{}) { for _, v := range data { pruneNulls(v) } } func pruneNulls(v interface{}) { switch v := v.(type) { case map[string]interface{}: pruneNullsFromMap(v) case []interface{}: pruneNullsFromSlice(v) } } // Saves the last-applied-configuration annotation in a separate SSA field manager // to prevent it from being dropped by users who have transitioned to SSA. // // If this operation is not performed, then the last-applied-configuration annotation // would be removed from the object upon the first SSA usage. We want to keep it // around for a few releases since it is required to downgrade to // SSA per [1] and [2]. This code should be removed once the annotation is // deprecated. // // - [1] https://kubernetes.io/docs/reference/using-api/server-side-apply/#downgrading-from-server-side-apply-to-client-side-apply // - [2] https://github.com/kubernetes/kubernetes/pull/90187 // // If the annotation is not already present, or if it is already managed by the // separate SSA fieldmanager, this is a no-op. func (o *ApplyOptions) saveLastApplyAnnotationIfNecessary( helper *resource.Helper, info *resource.Info, ) error { if o.FieldManager != fieldManagerServerSideApply { // There is no point in preserving the annotation if the field manager // will not remain default. This is because the server will not keep // the annotation up to date. return nil } // Send an apply patch with the last-applied-annotation // so that it is not orphaned by SSA in the following patch: accessor, err := meta.Accessor(info.Object) if err != nil { return err } // Get the current annotations from the object. annots := accessor.GetAnnotations() if annots == nil { annots = map[string]string{} } fieldManager := fieldManagerLastAppliedAnnotation originalAnnotation, hasAnnotation := annots[corev1.LastAppliedConfigAnnotation] // If the annotation does not already exist, we do not do anything if !hasAnnotation { return nil } // If there is already an SSA field manager which owns the field, then there // is nothing to do here. if owners := csaupgrade.FindFieldsOwners( accessor.GetManagedFields(), metav1.ManagedFieldsOperationApply, lastAppliedAnnotationFieldPath, ); len(owners) > 0 { return nil } justAnnotation := &unstructured.Unstructured{} justAnnotation.SetGroupVersionKind(info.Mapping.GroupVersionKind) justAnnotation.SetName(accessor.GetName()) justAnnotation.SetNamespace(accessor.GetNamespace()) justAnnotation.SetAnnotations(map[string]string{ corev1.LastAppliedConfigAnnotation: originalAnnotation, }) modified, err := runtime.Encode(unstructured.UnstructuredJSONScheme, justAnnotation) if err != nil { return nil } helperCopy := *helper newObj, err := helperCopy.WithFieldManager(fieldManager).Patch( info.Namespace, info.Name, types.ApplyPatchType, modified, nil, ) if err != nil { return err } return info.Refresh(newObj, false) } // Check if the returned object needs to have its kubectl-client-side-apply // managed fields migrated server-side-apply. // // field ownership metadata is stored in three places: // - server-side managed fields // - client-side managed fields // - and the last_applied_configuration annotation. // // The migration merges the client-side-managed fields into the // server-side-managed fields, leaving the last_applied_configuration // annotation in place. Server will keep the annotation up to date // after every server-side-apply where the following conditions are ment: // // 1. field manager is 'kubectl' // 2. annotation already exists func (o *ApplyOptions) migrateToSSAIfNecessary( helper *resource.Helper, info *resource.Info, ) (migrated bool, err error) { accessor, err := meta.Accessor(info.Object) if err != nil { return false, err } // To determine which field managers were used by kubectl for client-side-apply // we search for a manager used in `Update` operations which owns the // last-applied-annotation. // // This is the last client-side-apply manager which changed the field. // // There may be multiple owners if multiple managers wrote the same exact // configuration. In this case there are multiple owners, we want to migrate // them all. csaManagers := csaupgrade.FindFieldsOwners( accessor.GetManagedFields(), metav1.ManagedFieldsOperationUpdate, lastAppliedAnnotationFieldPath) managerNames := sets.New[string]() for _, entry := range csaManagers { managerNames.Insert(entry.Manager) } // Re-attempt patch as many times as it is conflicting due to ResourceVersion // test failing for i := 0; i < maxPatchRetry; i++ { var patchData []byte var obj runtime.Object patchData, err = csaupgrade.UpgradeManagedFieldsPatch( info.Object, managerNames, o.FieldManager) if err != nil { // If patch generation failed there was likely a bug. return false, err } else if patchData == nil { // nil patch data means nothing to do - object is already migrated return false, nil } // Send the patch to upgrade the managed fields if it is non-nil obj, err = helper.Patch( info.Namespace, info.Name, types.JSONPatchType, patchData, nil, ) if err == nil { // Stop retrying upon success. info.Refresh(obj, false) return true, nil } else if !errors.IsConflict(err) { // Only retry if there was a conflict return false, err } // Refresh the object for next iteration err = info.Get() if err != nil { // If there was an error fetching, return error return false, err } } // Reaching this point with non-nil error means there was a conflict and // max retries was hit // Return the last error witnessed (which will be a conflict) return false, err } func (o *ApplyOptions) shouldPrintObject() bool { // Print object only if output format other than "name" is specified shouldPrint := false output := *o.PrintFlags.OutputFormat shortOutput := output == "name" if len(output) > 0 && !shortOutput { shouldPrint = true } return shouldPrint } func (o *ApplyOptions) printObjects() error { if !o.shouldPrintObject() { return nil } infos, err := o.GetObjects() if err != nil { return err } if len(infos) > 0 { printer, err := o.ToPrinter("") if err != nil { return err } objToPrint := infos[0].Object if len(infos) > 1 { objs := []runtime.Object{} for _, info := range infos { objs = append(objs, info.Object) } list := &corev1.List{ TypeMeta: metav1.TypeMeta{ Kind: "List", APIVersion: "v1", }, ListMeta: metav1.ListMeta{}, } if err := meta.SetList(list, objs); err != nil { return err } objToPrint = list } if err := printer.PrintObj(objToPrint, o.Out); err != nil { return err } } return nil } // MarkNamespaceVisited keeps track of which namespaces the applied // objects belong to. Used for pruning. func (o *ApplyOptions) MarkNamespaceVisited(info *resource.Info) { if info.Namespaced() { o.VisitedNamespaces.Insert(info.Namespace) } } // MarkObjectVisited keeps track of UIDs of the applied // objects. Used for pruning. func (o *ApplyOptions) MarkObjectVisited(info *resource.Info) error { metadata, err := meta.Accessor(info.Object) if err != nil { return err } o.VisitedUids.Insert(metadata.GetUID()) return nil } // PrintAndPrunePostProcessor returns a function which meets the PostProcessorFn // function signature. This returned function prints all the // objects as a list (if configured for that), and prunes the // objects not applied. The returned function is the standard // apply post processor. func (o *ApplyOptions) PrintAndPrunePostProcessor() func() error { return func() error { ctx := context.TODO() if err := o.printObjects(); err != nil { return err } if o.Prune { if cmdutil.ApplySet.IsEnabled() && o.ApplySet != nil { if err := o.ApplySet.Prune(ctx, o); err != nil { // Do not update the ApplySet. If pruning failed, we want to keep the superset // of the previous and current resources in the ApplySet, so that the pruning // step of the next apply will be able to clean up the set correctly. return err } } else { p := newPruner(o) return p.pruneAll(o) } } return nil } } const ( // FieldManagerClientSideApply is the default client-side apply field manager. // // The default field manager is not `kubectl-apply` to distinguish from // server-side apply. FieldManagerClientSideApply = "kubectl-client-side-apply" // The default server-side apply field manager is `kubectl` // instead of a field manager like `kubectl-server-side-apply` // for backward compatibility to not conflict with old versions // of kubectl server-side apply where `kubectl` has already been the field manager. fieldManagerServerSideApply = "kubectl" fieldManagerLastAppliedAnnotation = "kubectl-last-applied" ) var ( lastAppliedAnnotationFieldPath = fieldpath.NewSet( fieldpath.MakePathOrDie( "metadata", "annotations", corev1.LastAppliedConfigAnnotation), ) ) // GetApplyFieldManagerFlag gets the field manager for kubectl apply // if it is not set. // // The default field manager is not `kubectl-apply` to distinguish between // client-side and server-side apply. func GetApplyFieldManagerFlag(cmd *cobra.Command, serverSide bool) string { // The field manager flag was set if cmd.Flag("field-manager").Changed { return cmdutil.GetFlagString(cmd, "field-manager") } if serverSide { return fieldManagerServerSideApply } return FieldManagerClientSideApply } // WarnIfDeleting prints a warning if a resource is being deleted func WarnIfDeleting(obj runtime.Object, stderr io.Writer) { metadata, _ := meta.Accessor(obj) if metadata != nil && metadata.GetDeletionTimestamp() != nil { // just warn the user about the conflict fmt.Fprintf(stderr, warningChangesOnDeletingResource, metadata.GetName()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_edit_last_applied.go000066400000000000000000000072021504711711200331370ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/editor" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( applyEditLastAppliedLong = templates.LongDesc(i18n.T(` Edit the latest last-applied-configuration annotations of resources from the default editor. The edit-last-applied command allows you to directly edit any API resource you can retrieve via the command-line tools. It will open the editor defined by your KUBE_EDITOR, or EDITOR environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows. You can edit multiple objects, although changes are applied one at a time. The command accepts file names as well as command-line arguments, although the files you point to must be previously saved versions of resources. The default format is YAML. To edit in JSON, specify "-o json". The flag --windows-line-endings can be used to force Windows line endings, otherwise the default for your operating system will be used. In the event an error occurs while updating, a temporary file will be created on disk that contains your unapplied changes. The most common error when updating a resource is another editor changing the resource on the server. When this occurs, you will have to apply your changes to the newer version of the resource, or update your temporary saved copy to include the latest resource version.`)) applyEditLastAppliedExample = templates.Examples(` # Edit the last-applied-configuration annotations by type/name in YAML kubectl apply edit-last-applied deployment/nginx # Edit the last-applied-configuration annotations by file in JSON kubectl apply edit-last-applied -f deploy.yaml -o json`) ) // NewCmdApplyEditLastApplied created the cobra CLI command for the `apply edit-last-applied` command. func NewCmdApplyEditLastApplied(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := editor.NewEditOptions(editor.ApplyEditMode, ioStreams) cmd := &cobra.Command{ Use: "edit-last-applied (RESOURCE/NAME | -f FILENAME)", DisableFlagsInUseLine: true, Short: i18n.T("Edit latest last-applied-configuration annotations of a resource/object"), Long: applyEditLastAppliedLong, Example: applyEditLastAppliedExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, args, cmd)) cmdutil.CheckErr(o.Run()) }, } // bind flag structs o.RecordFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) usage := "to use to edit the resource" cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmd.Flags().BoolVar(&o.WindowsLineEndings, "windows-line-endings", o.WindowsLineEndings, "Defaults to the line ending native to your platform.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, FieldManagerClientSideApply) cmdutil.AddValidateFlags(cmd) return cmd } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_set_last_applied.go000066400000000000000000000166121504711711200330120ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "bytes" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/editor" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // SetLastAppliedOptions defines options for the `apply set-last-applied` command.` type SetLastAppliedOptions struct { CreateAnnotation bool PrintFlags *genericclioptions.PrintFlags PrintObj printers.ResourcePrinterFunc FilenameOptions resource.FilenameOptions infoList []*resource.Info namespace string enforceNamespace bool dryRunStrategy cmdutil.DryRunStrategy shortOutput bool output string patchBufferList []PatchBuffer builder *resource.Builder unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) genericiooptions.IOStreams } // PatchBuffer caches changes that are to be applied. type PatchBuffer struct { Patch []byte PatchType types.PatchType } var ( applySetLastAppliedLong = templates.LongDesc(i18n.T(` Set the latest last-applied-configuration annotations by setting it to match the contents of a file. This results in the last-applied-configuration being updated as though 'kubectl apply -f ' was run, without updating any other parts of the object.`)) applySetLastAppliedExample = templates.Examples(i18n.T(` # Set the last-applied-configuration of a resource to match the contents of a file kubectl apply set-last-applied -f deploy.yaml # Execute set-last-applied against each configuration file in a directory kubectl apply set-last-applied -f path/ # Set the last-applied-configuration of a resource to match the contents of a file; will create the annotation if it does not already exist kubectl apply set-last-applied -f deploy.yaml --create-annotation=true `)) ) // NewSetLastAppliedOptions takes option arguments from a CLI stream and returns it at SetLastAppliedOptions type. func NewSetLastAppliedOptions(ioStreams genericiooptions.IOStreams) *SetLastAppliedOptions { return &SetLastAppliedOptions{ PrintFlags: genericclioptions.NewPrintFlags("configured").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdApplySetLastApplied creates the cobra CLI `apply` subcommand `set-last-applied`.` func NewCmdApplySetLastApplied(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewSetLastAppliedOptions(ioStreams) cmd := &cobra.Command{ Use: "set-last-applied -f FILENAME", DisableFlagsInUseLine: true, Short: i18n.T("Set the last-applied-configuration annotation on a live object to match the contents of a file"), Long: applySetLastAppliedLong, Example: applySetLastAppliedExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunSetLastApplied()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().BoolVar(&o.CreateAnnotation, "create-annotation", o.CreateAnnotation, "Will create 'last-applied-configuration' annotations if current objects doesn't have one") cmdutil.AddJsonFilenameFlag(cmd.Flags(), &o.FilenameOptions.Filenames, "Filename, directory, or URL to files that contains the last-applied-configuration annotations") return cmd } // Complete populates dry-run and output flag options. func (o *SetLastAppliedOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { var err error o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.output = cmdutil.GetFlagString(cmd, "output") o.shortOutput = o.output == "name" o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.builder = f.NewBuilder() o.unstructuredClientForMapping = f.UnstructuredClientForMapping cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj return nil } // Validate checks SetLastAppliedOptions for validity. func (o *SetLastAppliedOptions) Validate() error { r := o.builder. Unstructured(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, &o.FilenameOptions). Flatten(). Do() err := r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } patchBuf, diffBuf, patchType, err := editor.GetApplyPatch(info.Object.(runtime.Unstructured)) if err != nil { return err } // Verify the object exists in the cluster before trying to patch it. if err := info.Get(); err != nil { if errors.IsNotFound(err) { return err } return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err) } originalBuf, err := util.GetOriginalConfiguration(info.Object) if err != nil { return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err) } if originalBuf == nil && !o.CreateAnnotation { return fmt.Errorf("no last-applied-configuration annotation found on resource: %s, to create the annotation, run the command with --create-annotation", info.Name) } //only add to PatchBufferList when changed if !bytes.Equal(cmdutil.StripComments(originalBuf), cmdutil.StripComments(diffBuf)) { p := PatchBuffer{Patch: patchBuf, PatchType: patchType} o.patchBufferList = append(o.patchBufferList, p) o.infoList = append(o.infoList, info) } else { fmt.Fprintf(o.Out, "set-last-applied %s: no changes required.\n", info.Name) } return nil }) return err } // RunSetLastApplied executes the `set-last-applied` command according to SetLastAppliedOptions. func (o *SetLastAppliedOptions) RunSetLastApplied() error { for i, patch := range o.patchBufferList { info := o.infoList[i] finalObj := info.Object if o.dryRunStrategy != cmdutil.DryRunClient { mapping := info.ResourceMapping() client, err := o.unstructuredClientForMapping(mapping) if err != nil { return err } helper := resource. NewHelper(client, mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer) finalObj, err = helper.Patch(info.Namespace, info.Name, patch.PatchType, patch.Patch, nil) if err != nil { return err } } if err := o.PrintObj(finalObj, o.Out); err != nil { return err } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_test.go000066400000000000000000003757421504711711200304710ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "os" "path/filepath" "slices" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" dynamicfakeclient "k8s.io/client-go/dynamic/fake" openapiclient "k8s.io/client-go/openapi" "k8s.io/client-go/openapi/openapitest" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" testing2 "k8s.io/client-go/testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/util/csaupgrade" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/utils/ptr" "sigs.k8s.io/yaml" ) var ( fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "swagger.json")} fakeOpenAPIV3Legacy = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "api", "v1.json")} fakeOpenAPIV3AppsV1 = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "apis", "apps", "v1.json")} testingOpenAPISchemas = []testOpenAPISchema{AlwaysErrorsOpenAPISchema, FakeOpenAPISchema} AlwaysErrorsOpenAPISchema = testOpenAPISchema{ OpenAPISchemaFn: func() (openapi.Resources, error) { return nil, errors.New("cannot get openapi spec") }, OpenAPIV3ClientFunc: func() (openapiclient.Client, error) { return nil, errors.New("cannot get openapiv3 client") }, } FakeOpenAPISchema = testOpenAPISchema{ OpenAPISchemaFn: func() (openapi.Resources, error) { s, err := fakeSchema.OpenAPISchema() if err != nil { return nil, err } return openapi.NewOpenAPIData(s) }, OpenAPIV3ClientFunc: func() (openapiclient.Client, error) { c := openapitest.NewFakeClient() c.PathsMap["api/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3Legacy.SchemaBytesOrDie()} c.PathsMap["apis/apps/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3AppsV1.SchemaBytesOrDie()} return c, nil }, } AlwaysPanicSchema = testOpenAPISchema{ OpenAPISchemaFn: func() (openapi.Resources, error) { panic("error, openAPIV2 should not be called") }, OpenAPIV3ClientFunc: func() (openapiclient.Client, error) { return &OpenAPIV3ClientAlwaysPanic{}, nil }, } codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ) type OpenAPIV3ClientAlwaysPanic struct{} func (o *OpenAPIV3ClientAlwaysPanic) Paths() (map[string]openapiclient.GroupVersion, error) { panic("Cannot get paths") } func noopOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) { f(t) } func disableOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) { cmdtesting.WithAlphaEnvsDisabled([]cmdutil.FeatureGate{cmdutil.OpenAPIV3Patch}, t, f) } var applyFeatureToggles = []func(*testing.T, func(t *testing.T)){noopOpenAPIV3Patch, disableOpenAPIV3Patch} type testOpenAPISchema struct { OpenAPISchemaFn func() (openapi.Resources, error) OpenAPIV3ClientFunc func() (openapiclient.Client, error) } func TestApplyExtraArgsFail(t *testing.T) { f := cmdtesting.NewTestFactory() defer f.Cleanup() cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) _, err := flags.ToOptions(f, cmd, "kubectl", []string{"rc"}) require.EqualError(t, err, "Unexpected args: [rc]\nSee ' -h' for help and examples") } func TestAlphaEnablement(t *testing.T) { alphas := map[cmdutil.FeatureGate]string{ cmdutil.ApplySet: "applyset", } for feature, flag := range alphas { f := cmdtesting.NewTestFactory() defer f.Cleanup() cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) require.Nil(t, cmd.Flags().Lookup(flag), "flag %q should not be registered without the %q feature enabled", flag, feature) cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{feature}, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) require.NotNil(t, cmd.Flags().Lookup(flag), "flag %q should be registered with the %q feature enabled", flag, feature) }) } } func TestApplyFlagValidation(t *testing.T) { tests := []struct { args [][]string enableAlphas []cmdutil.FeatureGate expectedErr string }{ { args: [][]string{ {"force-conflicts", "true"}, }, expectedErr: "--force-conflicts only works with --server-side", }, { args: [][]string{ {"server-side", "true"}, {"dry-run", "client"}, }, expectedErr: "--dry-run=client doesn't work with --server-side (did you mean --dry-run=server instead?)", }, { args: [][]string{ {"force", "true"}, {"server-side", "true"}, }, expectedErr: "--force cannot be used with --server-side", }, { args: [][]string{ {"force", "true"}, {"dry-run", "server"}, }, expectedErr: "--dry-run=server cannot be used with --force", }, { args: [][]string{ {"all", "true"}, {"selector", "unused"}, }, expectedErr: "cannot set --all and --selector at the same time", }, { args: [][]string{ {"force", "true"}, {"prune", "true"}, {"all", "true"}, }, expectedErr: "--force cannot be used with --prune", }, { args: [][]string{ {"prune", "true"}, {"force", "true"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--force cannot be used with --prune", }, { args: [][]string{ {"server-side", "true"}, {"prune", "true"}, {"all", "true"}, }, expectedErr: "--prune is in alpha and doesn't currently work on objects created by server-side apply", }, { args: [][]string{ {"prune", "true"}, }, expectedErr: "all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector", }, { args: [][]string{ {"prune", "false"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--applyset requires --prune", }, { args: [][]string{ {"prune", "true"}, {"applyset", "mySecret"}, {"selector", "foo=bar"}, {"namespace", "myNs"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--selector is incompatible with --applyset", }, { args: [][]string{ {"prune", "true"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, {"all", "true"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--all is incompatible with --applyset", }, { args: [][]string{ {"prune", "true"}, {"applyset", "mySecret"}, {"namespace", "myNs"}, {"prune-allowlist", "core/v1/ConfigMap"}, }, enableAlphas: []cmdutil.FeatureGate{cmdutil.ApplySet}, expectedErr: "--prune-allowlist is incompatible with --applyset", }, } for i, test := range tests { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { f := cmdtesting.NewTestFactory() defer f.Cleanup() f.Client = &fake.RESTClient{} f.UnstructuredClient = f.Client cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", "unused") for _, arg := range test.args { if arg[0] == "namespace" { f.WithNamespace(arg[1]) } else { cmd.Flags().Set(arg[0], arg[1]) } } o, err := flags.ToOptions(f, cmd, "kubectl", []string{}) if err != nil { t.Fatalf("unexpected error creating apply options: %s", err) } err = o.Validate() if err == nil { t.Fatalf("missing expected error for case %d with args %+v", i, test.args) } if test.expectedErr != err.Error() { t.Errorf("expected error %s, got %s", test.expectedErr, err) } }) }) } } const ( filenameCM = "../../../testdata/apply/cm.yaml" filenameRC = "../../../testdata/apply/rc.yaml" filenameRCArgs = "../../../testdata/apply/rc-args.yaml" filenameRCLastAppliedArgs = "../../../testdata/apply/rc-lastapplied-args.yaml" filenameRCNoAnnotation = "../../../testdata/apply/rc-no-annotation.yaml" filenameRCLASTAPPLIED = "../../../testdata/apply/rc-lastapplied.yaml" filenameRCManagedFieldsLA = "../../../testdata/apply/rc-managedfields-lastapplied.yaml" filenameSVC = "../../../testdata/apply/service.yaml" filenameRCSVC = "../../../testdata/apply/rc-service.yaml" filenameNoExistRC = "../../../testdata/apply/rc-noexist.yaml" filenameRCPatchTest = "../../../testdata/apply/patch.json" dirName = "../../../testdata/apply/testdir" filenameRCJSON = "../../../testdata/apply/rc.json" filenamePodGeneratedName = "../../../testdata/apply/pod-generated-name.yaml" filenameWidgetClientside = "../../../testdata/apply/widget-clientside.yaml" filenameWidgetServerside = "../../../testdata/apply/widget-serverside.yaml" filenameDeployObjServerside = "../../../testdata/apply/deploy-serverside.yaml" filenameDeployObjClientside = "../../../testdata/apply/deploy-clientside.yaml" filenameApplySetCR = "../../../testdata/apply/applyset-cr.yaml" filenameApplySetCRD = "../../../testdata/apply/applysets-crd.yaml" ) func readConfigMapList(t *testing.T, filename string) [][]byte { data := readBytesFromFile(t, filename) cmList := corev1.ConfigMapList{} if err := runtime.DecodeInto(codec, data, &cmList); err != nil { t.Fatal(err) } var listCmBytes [][]byte for _, cm := range cmList.Items { cmBytes, err := runtime.Encode(codec, &cm) if err != nil { t.Fatal(err) } listCmBytes = append(listCmBytes, cmBytes) } return listCmBytes } func readBytesFromFile(t *testing.T, filename string) []byte { file, err := os.Open(filename) if err != nil { t.Fatal(err) } defer file.Close() data, err := io.ReadAll(file) if err != nil { t.Fatal(err) } return data } func readReplicationController(t *testing.T, filenameRC string) (string, []byte) { rcObj := readReplicationControllerFromFile(t, filenameRC) metaAccessor, err := meta.Accessor(rcObj) if err != nil { t.Fatal(err) } rcBytes, err := runtime.Encode(codec, rcObj) if err != nil { t.Fatal(err) } return metaAccessor.GetName(), rcBytes } func readReplicationControllerFromFile(t *testing.T, filename string) *corev1.ReplicationController { data := readBytesFromFile(t, filename) rc := corev1.ReplicationController{} if err := runtime.DecodeInto(codec, data, &rc); err != nil { t.Fatal(err) } return &rc } func readUnstructuredFromFile(t *testing.T, filename string) *unstructured.Unstructured { data := readBytesFromFile(t, filename) unst := unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, &unst); err != nil { t.Fatal(err) } return &unst } func readServiceFromFile(t *testing.T, filename string) *corev1.Service { data := readBytesFromFile(t, filename) svc := corev1.Service{} if err := runtime.DecodeInto(codec, data, &svc); err != nil { t.Fatal(err) } return &svc } func annotateRuntimeObject(t *testing.T, originalObj, currentObj runtime.Object, kind string) (string, []byte) { originalAccessor, err := meta.Accessor(originalObj) if err != nil { t.Fatal(err) } // The return value of this function is used in the body of the GET // request in the unit tests. Here we are adding a misc label to the object. // In tests, the validatePatchApplication() gets called in PATCH request // handler in fake round tripper. validatePatchApplication call // checks that this DELETE_ME label was deleted by the apply implementation in // kubectl. originalLabels := originalAccessor.GetLabels() originalLabels["DELETE_ME"] = "DELETE_ME" originalAccessor.SetLabels(originalLabels) original, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), originalObj) if err != nil { t.Fatal(err) } currentAccessor, err := meta.Accessor(currentObj) if err != nil { t.Fatal(err) } currentAnnotations := currentAccessor.GetAnnotations() if currentAnnotations == nil { currentAnnotations = make(map[string]string) } currentAnnotations[corev1.LastAppliedConfigAnnotation] = string(original) currentAccessor.SetAnnotations(currentAnnotations) current, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), currentObj) if err != nil { t.Fatal(err) } return currentAccessor.GetName(), current } func readAndAnnotateReplicationController(t *testing.T, filename string) (string, []byte) { rc1 := readReplicationControllerFromFile(t, filename) rc2 := readReplicationControllerFromFile(t, filename) return annotateRuntimeObject(t, rc1, rc2, "ReplicationController") } func readAndAnnotateService(t *testing.T, filename string) (string, []byte) { svc1 := readServiceFromFile(t, filename) svc2 := readServiceFromFile(t, filename) return annotateRuntimeObject(t, svc1, svc2, "Service") } func readAndAnnotateUnstructured(t *testing.T, filename string) (string, []byte) { obj1 := readUnstructuredFromFile(t, filename) obj2 := readUnstructuredFromFile(t, filename) return annotateRuntimeObject(t, obj1, obj2, "Widget") } func validatePatchApplication(t *testing.T, req *http.Request, patchType types.PatchType) { if got, wanted := req.Header.Get("Content-Type"), string(patchType); got != wanted { t.Fatalf("unexpected content-type expected: %s but actual %s\n", wanted, got) } patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } patchMap := map[string]interface{}{} if err := json.Unmarshal(patch, &patchMap); err != nil { t.Fatal(err) } annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok { t.Fatalf("patch does not contain annotation:\n%s\n", patch) } labelMap := walkMapPath(t, patchMap, []string{"metadata", "labels"}) if deleteMe, ok := labelMap["DELETE_ME"]; !ok || deleteMe != nil { t.Fatalf("patch does not remove deleted key: DELETE_ME:\n%s\n", patch) } } func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[string]interface{} { finish := start for i := 0; i < len(path); i++ { var ok bool finish, ok = finish[path[i]].(map[string]interface{}) if !ok { t.Fatalf("key:%s of path:%v not found in map:%v", path[i], path, start) } } return finish } func TestRunApplyPrintsValidObjectList(t *testing.T) { cmdtesting.InitTestErrorHandler(t) configMapList := readConfigMapList(t, filenameCM) pathCM := "/namespaces/test/configmaps" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case strings.HasPrefix(p, pathCM) && m == "GET": fallthrough case strings.HasPrefix(p, pathCM) && m == "PATCH": var body io.ReadCloser switch p { case pathCM + "/test0": body = io.NopCloser(bytes.NewReader(configMapList[0])) case pathCM + "/test1": body = io.NopCloser(bytes.NewReader(configMapList[1])) default: t.Errorf("unexpected request to %s", p) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameCM) cmd.Flags().Set("output", "json") cmd.Flags().Set("dry-run", "client") cmd.Run(cmd, []string{}) // ensure that returned list can be unmarshaled back into a configmap list cmList := corev1.List{} if err := runtime.DecodeInto(codec, buf.Bytes(), &cmList); err != nil { t.Fatal(err) } if len(cmList.Items) != 2 { t.Fatalf("Expected 2 items in the result; got %d", len(cmList.Items)) } if !strings.Contains(string(cmList.Items[0].Raw), "key1") { t.Fatalf("Did not get first ConfigMap at the first position") } if !strings.Contains(string(cmList.Items[1].Raw), "key2") { t.Fatalf("Did not get second ConfigMap at the second position") } } func TestRunApplyViewLastApplied(t *testing.T) { _, rcBytesWithConfig := readReplicationController(t, filenameRCLASTAPPLIED) _, rcBytesWithArgs := readReplicationController(t, filenameRCLastAppliedArgs) nameRC, rcBytes := readReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC tests := []struct { name, nameRC, pathRC, filePath, outputFormat, expectedErr, expectedOut, selector string args []string respBytes []byte }{ { name: "view with file", filePath: filenameRC, outputFormat: "", expectedErr: "", expectedOut: "test: 1234\n", selector: "", args: []string{}, respBytes: rcBytesWithConfig, }, { name: "test with file include `%s` in arguments", filePath: filenameRCArgs, outputFormat: "", expectedErr: "", expectedOut: "args: -random_flag=%s@domain.com\n", selector: "", args: []string{}, respBytes: rcBytesWithArgs, }, { name: "view with file json format", filePath: filenameRC, outputFormat: "json", expectedErr: "", expectedOut: "{\n \"test\": 1234\n}\n", selector: "", args: []string{}, respBytes: rcBytesWithConfig, }, { name: "view resource/name invalid format", filePath: "", outputFormat: "wide", expectedErr: "error: Unexpected -o output mode: wide, the flag 'output' must be one of yaml|json\nSee 'view-last-applied -h' for help and examples", expectedOut: "", selector: "", args: []string{"replicationcontroller", "test-rc"}, respBytes: rcBytesWithConfig, }, { name: "view resource with label", filePath: "", outputFormat: "", expectedErr: "", expectedOut: "test: 1234\n", selector: "name=test-rc", args: []string{"replicationcontroller"}, respBytes: rcBytesWithConfig, }, { name: "view resource without annotations", filePath: "", outputFormat: "", expectedErr: "error: no last-applied-configuration annotation found on resource: test-rc", expectedOut: "", selector: "", args: []string{"replicationcontroller", "test-rc"}, respBytes: rcBytes, }, { name: "view resource no match", filePath: "", outputFormat: "", expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-match)", expectedOut: "", selector: "", args: []string{"replicationcontroller", "no-match"}, respBytes: nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(test.respBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/namespaces/test/replicationcontrollers" && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(test.respBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/namespaces/test/replicationcontrollers/no-match" && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() cmdutil.BehaviorOnFatal(func(str string, code int) { if str != test.expectedErr { t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr) } }) ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApplyViewLastApplied(tf, ioStreams) if test.filePath != "" { cmd.Flags().Set("filename", test.filePath) } if test.outputFormat != "" { cmd.Flags().Set("output", test.outputFormat) } if test.selector != "" { cmd.Flags().Set("selector", test.selector) } cmd.Run(cmd, test.args) if buf.String() != test.expectedOut { t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut) } }) } } func TestApplyObjectWithoutAnnotation(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, rcBytes := readReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(rcBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": bodyRC := io.NopCloser(bytes.NewReader(rcBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" expectWarning := fmt.Sprintf(warningNoLastAppliedConfigAnnotation, "replicationcontrollers/test-rc", corev1.LastAppliedConfigAnnotation, "kubectl") if errBuf.String() != expectWarning { t.Fatalf("unexpected non-warning: %s\nexpected: %s", errBuf.String(), expectWarning) } if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } } func TestOpenAPIV3PatchFeatureFlag(t *testing.T) { // OpenAPIV3 smp apply is on by default. // Test that users can disable it to use OpenAPI V2 smp // An OpenAPI V3 root that always panics is used to ensure // the v3 code path is never exercised when the feature is disabled cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC t.Run("test apply when a local object is specified - openapi v2 smp", func(t *testing.T) { disableOpenAPIV3Patch(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = AlwaysPanicSchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } func TestOpenAPIV3DoesNotLoadV2(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = AlwaysPanicSchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) } func TestApplyObject(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyPruneObjects(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply returns correct output", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("prune", "true") cmd.Flags().Set("namespace", "test") cmd.Flags().Set("output", "yaml") cmd.Flags().Set("all", "true") cmd.Run(cmd, []string{}) if !strings.Contains(buf.String(), "test-rc") { t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "test-rc") } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyPruneObjectsWithAllowlist(t *testing.T) { cmdtesting.InitTestErrorHandler(t) // Read ReplicationController from the file we will use to apply. This one will not be pruned because it exists in the file. rc := readUnstructuredFromFile(t, filenameRC) err := setLastAppliedConfigAnnotation(rc) if err != nil { t.Fatal(err) } // Create another ReplicationController that can be pruned rc2 := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-rc2", "namespace": "test", "uid": "uid-rc2", }, }, } err = setLastAppliedConfigAnnotation(rc2) if err != nil { t.Fatal(err) } // Create a ConfigMap that can be pruned cm := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-cm", "namespace": "test", "uid": "uid-cm", }, }, } err = setLastAppliedConfigAnnotation(cm) if err != nil { t.Fatal(err) } // Create Namespace that can be pruned ns := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "Namespace", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-apply", "uid": "uid-ns", }, }, } err = setLastAppliedConfigAnnotation(ns) if err != nil { t.Fatal(err) } // Create a ConfigMap without a UID. Resources without a UID will not be pruned. cmNoUID := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-cm-nouid", "namespace": "test", }, }, } err = setLastAppliedConfigAnnotation(cmNoUID) if err != nil { t.Fatal(err) } // Create a ConfigMap without a last applied annotation. Resources without a last applied annotation will not be pruned. cmNoLastApplied := &unstructured.Unstructured{ Object: map[string]interface{}{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": map[string]interface{}{ "name": "test-cm-nolastapplied", "namespace": "test", "uid": "uid-cm-nolastapplied", }, }, } testCases := map[string]struct { currentResources []runtime.Object pruneAllowlist []string namespace string expectedPrunedResources []string expectedOutputs []string }{ "prune without namespace and allowlist should delete resources that are not in the specified file": { currentResources: []runtime.Object{rc, rc2, cm, ns}, expectedPrunedResources: []string{"test/test-cm", "test/test-rc2", "/test-apply"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "replicationcontroller/test-rc2 pruned", "namespace/test-apply pruned", }, }, // Deprecated: kubectl apply will no longer prune non-namespaced resources by default when used with the --namespace flag in a future release // namespace is a non-namespaced resource and will not be pruned in the future "prune with namespace and without allowlist should delete resources that are not in the specified file": { currentResources: []runtime.Object{rc, rc2, cm, ns}, namespace: "test", expectedPrunedResources: []string{"test/test-cm", "test/test-rc2", "/test-apply"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "replicationcontroller/test-rc2 pruned", "namespace/test-apply pruned", }, }, // Even namespace is a non-namespaced resource, it will be pruned if specified in pruneAllowList in the future "prune with namespace and allowlist should delete all matching resources": { currentResources: []runtime.Object{rc, cm, ns}, pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/Namespace"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm", "/test-apply"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "namespace/test-apply pruned", }, }, "prune with allowlist should delete only matching resources": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ConfigMap"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, "prune with allowlist specifying the same resource type multiple times should not fail": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/ConfigMap"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, "prune with allowlist should not delete resources that exist in the specified file": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ReplicationController"}, namespace: "test", expectedPrunedResources: []string{"test/test-rc2"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "replicationcontroller/test-rc2 pruned", }, }, "prune with allowlist specifying multiple resource types should delete matching resources": { currentResources: []runtime.Object{rc, rc2, cm}, pruneAllowlist: []string{"core/v1/ConfigMap", "core/v1/ReplicationController"}, namespace: "test", expectedPrunedResources: []string{"test/test-cm", "test/test-rc2"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", "replicationcontroller/test-rc2 pruned", }, }, "prune should not delete resources that are missing a UID": { currentResources: []runtime.Object{rc, cm, cmNoUID}, expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, "prune should not delete resources that are missing the last applied config annotation": { currentResources: []runtime.Object{rc, cm, cmNoLastApplied}, expectedPrunedResources: []string{"test/test-cm"}, expectedOutputs: []string{ "replicationcontroller/test-rc unchanged", "configmap/test-cm pruned", }, }, } for testCaseName, tc := range testCases { for _, testingOpenAPISchema := range testingOpenAPISchemas { t.Run(testCaseName, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/test-rc" && m == "GET": encoded := runtime.EncodeOrDie(unstructured.UnstructuredJSONScheme, rc) bodyRC := io.NopCloser(strings.NewReader(encoded)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/namespaces/test/replicationcontrollers/test-rc" && m == "PATCH": encoded := runtime.EncodeOrDie(unstructured.UnstructuredJSONScheme, rc) bodyRC := io.NopCloser(strings.NewReader(encoded)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() for _, resource := range tc.currentResources { if err := tf.FakeDynamicClient.Tracker().Add(resource); err != nil { t.Fatal(err) } } ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("prune", "true") cmd.Flags().Set("namespace", tc.namespace) cmd.Flags().Set("all", "true") for _, allow := range tc.pruneAllowlist { cmd.Flags().Set("prune-allowlist", allow) } cmd.Run(cmd, []string{}) if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } actualOutput := buf.String() for _, expectedOutput := range tc.expectedOutputs { if !strings.Contains(actualOutput, expectedOutput) { t.Fatalf("expected output to contain %q, but it did not. Actual Output:\n%s", expectedOutput, actualOutput) } } var prunedResources []string for _, action := range tf.FakeDynamicClient.Actions() { if action.GetVerb() == "delete" { deleteAction := action.(testing2.DeleteAction) prunedResources = append(prunedResources, deleteAction.GetNamespace()+"/"+deleteAction.GetName()) } } // Make sure nothing unexpected was pruned for _, resource := range prunedResources { if !slices.Contains(tc.expectedPrunedResources, resource) { t.Fatalf("expected %s not to be pruned, but it was", resource) } } // Make sure everything that was expected to be pruned was pruned for _, resource := range tc.expectedPrunedResources { if !slices.Contains(prunedResources, resource) { t.Fatalf("expected %s to be pruned, but it was not", resource) } } }) } } } func setLastAppliedConfigAnnotation(obj runtime.Object) error { accessor, err := meta.Accessor(obj) if err != nil { return err } annotations := accessor.GetAnnotations() if annotations == nil { annotations = make(map[string]string) accessor.SetAnnotations(annotations) } annotations[corev1.LastAppliedConfigAnnotation] = runtime.EncodeOrDie(unstructured.NewJSONFallbackEncoder(codec), obj) accessor.SetAnnotations(annotations) return nil } // Tests that apply of object in need of CSA migration results in a call // to patch it. func TestApplyCSAMigration(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, rcWithManagedFields := readAndAnnotateReplicationController(t, filenameRCManagedFieldsLA) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, openAPIFeatureToggle := range applyFeatureToggles { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() // The object after patch should be equivalent to the output of // csaupgrade.UpgradeManagedFields // // Parse object into unstructured, apply patch postPatchObj := &unstructured.Unstructured{} err := json.Unmarshal(rcWithManagedFields, &postPatchObj.Object) require.NoError(t, err) expectedPatch, err := csaupgrade.UpgradeManagedFieldsPatch(postPatchObj, sets.New(FieldManagerClientSideApply), "kubectl") require.NoError(t, err) err = csaupgrade.UpgradeManagedFields(postPatchObj, sets.New("kubectl-client-side-apply"), "kubectl") require.NoError(t, err) postPatchData, err := json.Marshal(postPatchObj) require.NoError(t, err) patches := 0 targetPatches := 2 applies := 0 tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": // During retry loop for patch fetch is performed. // keep returning the unchanged data if patches < targetPatches { bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil } t.Fatalf("should not do a fetch in serverside-apply") return nil, nil case p == pathRC && m == "PATCH": if got := req.Header.Get("Content-Type"); got == string(types.ApplyPatchType) { defer func() { applies += 1 }() switch applies { case 0: // initial apply. // Just return the same object but with managed fields bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case 1: // Second apply should include only last apply annotation unmodified // Return new object // NOTE: on a real server this would also modify the managed fields // just return the same object unmodified. It is not so important // for this test for the last-applied to appear in new field // manager response, only that the client asks the server to do it bodyRC := io.NopCloser(bytes.NewReader(rcWithManagedFields)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case 2, 3: // Before the last apply we have formed our JSONPAtch so it // should reply now with the upgraded object bodyRC := io.NopCloser(bytes.NewReader(postPatchData)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: require.Fail(t, "sent more apply requests than expected") return &http.Response{StatusCode: http.StatusBadRequest, Header: cmdtesting.DefaultHeader()}, nil } } else if got == string(types.JSONPatchType) { defer func() { patches += 1 }() // Require that the patch is equal to what is expected body, err := io.ReadAll(req.Body) require.NoError(t, err) require.Equal(t, expectedPatch, body) switch patches { case targetPatches - 1: bodyRC := io.NopCloser(bytes.NewReader(postPatchData)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: // Return conflict until the client has retried enough times return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil } } else { t.Fatalf("unexpected content-type: %s\n", got) return nil, nil } default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "yaml") cmd.Flags().Set("server-side", "true") cmd.Flags().Set("show-managed-fields", "true") cmd.Run(cmd, []string{}) // JSONPatch should have been attempted exactly the given number of times require.Equal(t, targetPatches, patches, "should retry as many times as a conflict was returned") require.Equal(t, 3, applies, "should perform specified # of apply calls upon migration") require.Empty(t, errBuf.String()) // ensure that in the future there will be no migrations necessary // (by showing migration is a no-op) rc := &corev1.ReplicationController{} if err := runtime.DecodeInto(codec, buf.Bytes(), rc); err != nil { t.Fatal(err) } upgradedRC := rc.DeepCopyObject() err = csaupgrade.UpgradeManagedFields(upgradedRC, sets.New("kubectl-client-side-apply"), "kubectl") require.NoError(t, err) require.NotEmpty(t, rc.ManagedFields) require.Equal(t, rc, upgradedRC, "upgrading should be no-op in future") // Apply the upgraded object. // Expect only a single PATCH call to apiserver ioStreams, _, _, errBuf = genericiooptions.NewTestIOStreams() cmd = NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "yaml") cmd.Flags().Set("server-side", "true") cmd.Flags().Set("show-managed-fields", "true") cmd.Run(cmd, []string{}) require.Empty(t, errBuf) require.Equal(t, 4, applies, "only a single call to server-side apply should have been performed") require.Equal(t, targetPatches, patches, "no more json patches should have been needed") }) } } func TestApplyObjectOutput(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC // Add some extra data to the post-patch object postPatchObj := &unstructured.Unstructured{} if err := json.Unmarshal(currentRC, &postPatchObj.Object); err != nil { t.Fatal(err) } postPatchLabels := postPatchObj.GetLabels() if postPatchLabels == nil { postPatchLabels = map[string]string{} } postPatchLabels["post-patch"] = "value" postPatchObj.SetLabels(postPatchLabels) postPatchData, err := json.Marshal(postPatchObj) if err != nil { t.Fatal(err) } for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply returns correct output", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(postPatchData)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "yaml") cmd.Run(cmd, []string{}) if !strings.Contains(buf.String(), "test-rc") { t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "test-rc") } if !strings.Contains(buf.String(), "post-patch: value") { t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "post-patch: value") } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyRetry(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply retries on conflict error", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { firstPatch := true retry := false getCount := 0 tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": getCount++ bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": if firstPatch { firstPatch = false statusErr := apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first")) bodyBytes, _ := json.Marshal(statusErr) bodyErr := io.NopCloser(bytes.NewReader(bodyBytes)) return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil } retry = true validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if !retry || getCount != 2 { t.Fatalf("apply didn't retry when get conflict error") } // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestApplyNonExistObject(t *testing.T) { nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers" pathNameRC := pathRC + "/" + nameRC tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil case p == pathNameRC && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil case p == pathRC && m == "POST": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Errorf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } } func TestApplyEmptyPatch(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, _ := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers" pathNameRC := pathRC + "/" + nameRC verifyPost := false var body []byte tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil case p == pathNameRC && m == "GET": if body == nil { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil } bodyRC := io.NopCloser(bytes.NewReader(body)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "POST": body, _ = io.ReadAll(req.Body) verifyPost = true bodyRC := io.NopCloser(bytes.NewReader(body)) return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() // 1. apply non exist object ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expectRC := "replicationcontroller/" + nameRC + "\n" if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } if !verifyPost { t.Fatal("No server-side post call detected") } // 2. test apply already exist object, will not send empty patch request ioStreams, _, buf, _ = genericiooptions.NewTestIOStreams() cmd = NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != expectRC { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) } } func TestApplyMultipleObjectsAsList(t *testing.T) { testApplyMultipleObjects(t, true) } func TestApplyMultipleObjectsAsFiles(t *testing.T) { testApplyMultipleObjects(t, false) } func testApplyMultipleObjects(t *testing.T, asList bool) { nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC nameSVC, currentSVC := readAndAnnotateService(t, filenameSVC) pathSVC := "/namespaces/test/services/" + nameSVC for _, testingOpenAPISchema := range testingOpenAPISchemas { t.Run("test apply on multiple objects", func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathRC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == pathSVC && m == "GET": bodySVC := io.NopCloser(bytes.NewReader(currentSVC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil case p == pathSVC && m == "PATCH": validatePatchApplication(t, req, types.StrategicMergePatchType) bodySVC := io.NopCloser(bytes.NewReader(currentSVC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodySVC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) if asList { cmd.Flags().Set("filename", filenameRCSVC) } else { cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("filename", filenameSVC) } cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // Names should come from the REST response, NOT the files expectRC := "replicationcontroller/" + nameRC + "\n" expectSVC := "service/" + nameSVC + "\n" // Test both possible orders since output is non-deterministic. expectOne := expectRC + expectSVC expectTwo := expectSVC + expectRC if buf.String() != expectOne && buf.String() != expectTwo { t.Fatalf("unexpected output: %s\nexpected: %s OR %s", buf.String(), expectOne, expectTwo) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) } } func readDeploymentFromFile(t *testing.T, file string) []byte { raw := readBytesFromFile(t, file) obj := &appsv1.Deployment{} if err := runtime.DecodeInto(codec, raw, obj); err != nil { t.Fatal(err) } objJSON, err := runtime.Encode(codec, obj) if err != nil { t.Fatal(err) } return objJSON } func TestApplyNULLPreservation(t *testing.T) { cmdtesting.InitTestErrorHandler(t) deploymentName := "nginx-deployment" deploymentPath := "/namespaces/test/deployments/" + deploymentName verifiedPatch := false deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside) for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply preserves NULL fields", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == deploymentPath && m == "GET": body := io.NopCloser(bytes.NewReader(deploymentBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == deploymentPath && m == "PATCH": patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } patchMap := map[string]interface{}{} if err := json.Unmarshal(patch, &patchMap); err != nil { t.Fatal(err) } annotationMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok { t.Fatalf("patch does not contain annotation:\n%s\n", patch) } strategy := walkMapPath(t, patchMap, []string{"spec", "strategy"}) if value, ok := strategy["rollingUpdate"]; !ok || value != nil { t.Fatalf("patch did not retain null value in key: rollingUpdate:\n%s\n", patch) } verifiedPatch = true // The real API server would had returned the patched object but Kubectl // is ignoring the actual return object. // TODO: Make this match actual server behavior by returning the patched object. body := io.NopCloser(bytes.NewReader(deploymentBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameDeployObjClientside) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expected := "deployment.apps/" + deploymentName + "\n" if buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } if !verifiedPatch { t.Fatal("No server-side patch call detected") } }) }) } } } // TestUnstructuredApply checks apply operations on an unstructured object func TestUnstructuredApply(t *testing.T) { cmdtesting.InitTestErrorHandler(t) name, curr := readAndAnnotateUnstructured(t, filenameWidgetClientside) path := "/namespaces/test/widgets/" + name verifiedPatch := false for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply works correctly with unstructured objects", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == path && m == "GET": body := io.NopCloser(bytes.NewReader(curr)) return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == path && m == "PATCH": validatePatchApplication(t, req, types.MergePatchType) verifiedPatch = true body := io.NopCloser(bytes.NewReader(curr)) return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameWidgetClientside) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expected := "widget.unit-test.test.com/" + name + "\n" if buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } if !verifiedPatch { t.Fatal("No server-side patch call detected") } }) }) } } } // TestUnstructuredIdempotentApply checks repeated apply operation on an unstructured object func TestUnstructuredIdempotentApply(t *testing.T) { cmdtesting.InitTestErrorHandler(t) serversideObject := readUnstructuredFromFile(t, filenameWidgetServerside) serversideData, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), serversideObject) if err != nil { t.Fatal(err) } path := "/namespaces/test/widgets/widget" for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == path && m == "GET": body := io.NopCloser(bytes.NewReader(serversideData)) return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == path && m == "PATCH": // In idempotent updates, kubectl will resolve to an empty patch and not send anything to the server // Thus, if we reach this branch, kubectl is unnecessarily sending a patch. patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } t.Fatalf("Unexpected Patch: %s", patch) return nil, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameWidgetClientside) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) expected := "widget.unit-test.test.com/widget\n" if buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestRunApplySetLastApplied(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC noExistRC, _ := readAndAnnotateReplicationController(t, filenameNoExistRC) noExistPath := "/namespaces/test/replicationcontrollers/" + noExistRC noAnnotationName, noAnnotationRC := readReplicationController(t, filenameRCNoAnnotation) noAnnotationPath := "/namespaces/test/replicationcontrollers/" + noAnnotationName tests := []struct { name, nameRC, pathRC, filePath, expectedErr, expectedOut, output string }{ { name: "set with exist object", filePath: filenameRC, expectedErr: "", expectedOut: "replicationcontroller/test-rc\n", output: "name", }, { name: "set with no-exist object", filePath: filenameNoExistRC, expectedErr: "Error from server (NotFound): the server could not find the requested resource (get replicationcontrollers no-exist)", expectedOut: "", output: "name", }, { name: "set for the annotation does not exist on the live object", filePath: filenameRCNoAnnotation, expectedErr: "error: no last-applied-configuration annotation found on resource: no-annotation, to create the annotation, run the command with --create-annotation", expectedOut: "", output: "name", }, { name: "set with exist object output json", filePath: filenameRCJSON, expectedErr: "", expectedOut: "replicationcontroller/test-rc\n", output: "name", }, { name: "set test for a directory of files", filePath: dirName, expectedErr: "", expectedOut: "replicationcontroller/test-rc\nreplicationcontroller/test-rc\n", output: "name", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == pathRC && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == noAnnotationPath && m == "GET": bodyRC := io.NopCloser(bytes.NewReader(noAnnotationRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == noExistPath && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil case p == pathRC && m == "PATCH": checkPatchString(t, req) bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() cmdutil.BehaviorOnFatal(func(str string, code int) { if str != test.expectedErr { t.Errorf("%s: unexpected error: %s\nexpected: %s", test.name, str, test.expectedErr) } }) ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApplySetLastApplied(tf, ioStreams) cmd.Flags().Set("filename", test.filePath) cmd.Flags().Set("output", test.output) cmd.Run(cmd, []string{}) if buf.String() != test.expectedOut { t.Fatalf("%s: unexpected output: %s\nexpected: %s", test.name, buf.String(), test.expectedOut) } }) } cmdutil.BehaviorOnFatal(func(str string, code int) {}) } func checkPatchString(t *testing.T, req *http.Request) { checkString := string(readBytesFromFile(t, filenameRCPatchTest)) patch, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } patchMap := map[string]interface{}{} if err := json.Unmarshal(patch, &patchMap); err != nil { t.Fatal(err) } annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) if _, ok := annotationsMap[corev1.LastAppliedConfigAnnotation]; !ok { t.Fatalf("patch does not contain annotation:\n%s\n", patch) } resultString := annotationsMap["kubectl.kubernetes.io/last-applied-configuration"] if resultString != checkString { t.Fatalf("patch annotation is not correct, expect:%s\n but got:%s\n", checkString, resultString) } } func TestForceApply(t *testing.T) { cmdtesting.InitTestErrorHandler(t) scheme := runtime.NewScheme() nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC pathRCList := "/namespaces/test/replicationcontrollers" expected := map[string]int{ "getOk": 6, "getNotFound": 1, "getList": 0, "patch": 6, "delete": 1, "post": 1, } // Set the patch retry back off period to something low, so the test can run more quickly patchRetryBackOffPeriod = 1 * time.Millisecond for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, openAPIFeatureToggle := range applyFeatureToggles { t.Run("test apply with --force", func(t *testing.T) { openAPIFeatureToggle(t, func(t *testing.T) { deleted := false isScaledDownToZero := false counts := map[string]int{} tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case strings.HasSuffix(p, pathRC) && m == "GET": if deleted { counts["getNotFound"]++ return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte{}))}, nil } counts["getOk"]++ var bodyRC io.ReadCloser if isScaledDownToZero { rcObj := readReplicationControllerFromFile(t, filenameRC) rcObj.Spec.Replicas = ptr.To[int32](0) rcBytes, err := runtime.Encode(codec, rcObj) if err != nil { t.Fatal(err) } bodyRC = io.NopCloser(bytes.NewReader(rcBytes)) } else { bodyRC = io.NopCloser(bytes.NewReader(currentRC)) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case strings.HasSuffix(p, pathRCList) && m == "GET": counts["getList"]++ rcObj := readUnstructuredFromFile(t, filenameRC) list := &unstructured.UnstructuredList{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "ReplicationControllerList", }, Items: []unstructured.Unstructured{*rcObj}, } listBytes, err := runtime.Encode(codec, list) if err != nil { t.Fatal(err) } bodyRCList := io.NopCloser(bytes.NewReader(listBytes)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRCList}, nil case strings.HasSuffix(p, pathRC) && m == "PATCH": counts["patch"]++ if counts["patch"] <= 6 { statusErr := apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first")) bodyBytes, _ := json.Marshal(statusErr) bodyErr := io.NopCloser(bytes.NewReader(bodyBytes)) return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: bodyErr}, nil } t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req) return nil, nil case strings.HasSuffix(p, pathRC) && m == "DELETE": counts["delete"]++ deleted = true bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case strings.HasSuffix(p, pathRC) && m == "PUT": counts["put"]++ bodyRC := io.NopCloser(bytes.NewReader(currentRC)) isScaledDownToZero = true return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil case strings.HasSuffix(p, pathRCList) && m == "POST": counts["post"]++ deleted = false isScaledDownToZero = false bodyRC := io.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) tf.FakeDynamicClient = fakeDynamicClient tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc tf.Client = tf.UnstructuredClient tf.ClientConfigVal = &restclient.Config{} ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("output", "name") cmd.Flags().Set("force", "true") cmd.Run(cmd, []string{}) for method, exp := range expected { if exp != counts[method] { t.Errorf("Unexpected amount of %q API calls, wanted %v got %v", method, exp, counts[method]) } } if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected { t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected) } if errBuf.String() != "" { t.Fatalf("unexpected error output: %s", errBuf.String()) } }) }) } } } func TestDontAllowForceApplyWithServerDryRun(t *testing.T) { expectedError := "error: --dry-run=server cannot be used with --force" cmdutil.BehaviorOnFatal(func(str string, code int) { panic(str) }) defer func() { actualError := recover() if expectedError != actualError { t.Fatalf(`expected error "%s", but got "%s"`, expectedError, actualError) } }() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("dry-run", "server") cmd.Flags().Set("force", "true") cmd.Run(cmd, []string{}) t.Fatalf(`expected error "%s"`, expectedError) } func TestDontAllowForceApplyWithServerSide(t *testing.T) { expectedError := "error: --force cannot be used with --server-side" cmdutil.BehaviorOnFatal(func(str string, code int) { panic(str) }) defer func() { actualError := recover() if expectedError != actualError { t.Fatalf(`expected error "%s", but got "%s"`, expectedError, actualError) } }() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("force", "true") cmd.Run(cmd, []string{}) t.Fatalf(`expected error "%s"`, expectedError) } func TestDontAllowApplyWithPodGeneratedName(t *testing.T) { expectedError := "error: from testing-: cannot use generate name with apply" cmdutil.BehaviorOnFatal(func(str string, code int) { if str != expectedError { t.Fatalf(`expected error "%s", but got "%s"`, expectedError, str) } }) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenamePodGeneratedName) cmd.Flags().Set("dry-run", "client") cmd.Run(cmd, []string{}) } func TestApplySetParentValidation(t *testing.T) { for name, test := range map[string]struct { applysetFlag string namespaceFlag string setup func(*testing.T, *cmdtesting.TestFactory) expectParentKind string expectBlankParentNs bool expectErr string }{ "parent type must be valid": { applysetFlag: "doesnotexist/thename", expectErr: "invalid parent reference \"doesnotexist/thename\": no matches for /, Resource=doesnotexist", }, "parent name must be present": { applysetFlag: "secret/", expectErr: "invalid parent reference \"secret/\": name cannot be blank", }, "configmap parents are valid": { applysetFlag: "configmap/thename", namespaceFlag: "mynamespace", expectParentKind: "ConfigMap", }, "secret parents are valid": { applysetFlag: "secret/thename", namespaceFlag: "mynamespace", expectParentKind: "Secret", }, "plural resource works": { applysetFlag: "secrets/thename", namespaceFlag: "mynamespace", expectParentKind: "Secret", }, "other namespaced builtin parents types are correctly parsed but invalid": { applysetFlag: "deployments.apps/thename", expectParentKind: "Deployment", expectErr: "[namespace is required to use namespace-scoped ApplySet, resource \"apps/v1, Resource=deployments\" is not permitted as an ApplySet parent]", }, "namespaced builtin parents with multi-segment groups are correctly parsed but invalid": { applysetFlag: "priorityclasses.scheduling.k8s.io/thename", expectParentKind: "PriorityClass", expectErr: "resource \"scheduling.k8s.io/v1alpha1, Resource=priorityclasses\" is not permitted as an ApplySet parent", }, "non-namespaced builtin types are correctly parsed but invalid": { applysetFlag: "namespaces/thename", expectParentKind: "Namespace", namespaceFlag: "somenamespace", expectBlankParentNs: true, expectErr: "resource \"/v1, Resource=namespaces\" is not permitted as an ApplySet parent", }, "parent namespace should use the value of the namespace flag": { applysetFlag: "mysecret", namespaceFlag: "mynamespace", expectParentKind: "Secret", }, "parent namespace should not use the default namespace from ClientConfig": { applysetFlag: "mysecret", setup: func(t *testing.T, f *cmdtesting.TestFactory) { // by default, the value "default" is used for the namespace // make sure this assumption still holds ns, overridden, err := f.ToRawKubeConfigLoader().Namespace() require.NoError(t, err) require.Falsef(t, overridden, "namespace unexpectedly overridden") require.Equal(t, "default", ns) }, expectBlankParentNs: true, expectParentKind: "Secret", expectErr: "namespace is required to use namespace-scoped ApplySet", }, "parent namespace should not use the default namespace from the user's kubeconfig": { applysetFlag: "mysecret", setup: func(t *testing.T, f *cmdtesting.TestFactory) { kubeConfig := clientcmdapi.NewConfig() kubeConfig.CurrentContext = "default" kubeConfig.Contexts["default"] = &clientcmdapi.Context{Namespace: "bar"} clientConfig := clientcmd.NewDefaultClientConfig(*kubeConfig, &clientcmd.ConfigOverrides{ ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"}}) f.WithClientConfig(clientConfig) }, expectBlankParentNs: true, expectParentKind: "Secret", expectErr: "namespace is required to use namespace-scoped ApplySet", }, } { t.Run(name, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("applyset", test.applysetFlag) cmd.Flags().Set("prune", "true") f := cmdtesting.NewTestFactory() defer f.Cleanup() setUpClientsForApplySetWithSSA(t, f) var expectedParentNs string if test.namespaceFlag != "" { f.WithNamespace(test.namespaceFlag) if !test.expectBlankParentNs { expectedParentNs = test.namespaceFlag } } if test.setup != nil { test.setup(t, f) } o, err := flags.ToOptions(f, cmd, "kubectl", []string{}) if test.expectErr == "" { require.NoError(t, err, "ToOptions error") } else if err != nil { require.EqualError(t, err, test.expectErr) return } assert.Equal(t, expectedParentNs, o.ApplySet.parentRef.Namespace) assert.Equal(t, test.expectParentKind, o.ApplySet.parentRef.GroupVersionKind.Kind) err = o.Validate() if test.expectErr != "" { require.EqualError(t, err, test.expectErr) } else { require.NoError(t, err, "Validate error") } }) }) } } func setUpClientsForApplySetWithSSA(t *testing.T, tf *cmdtesting.TestFactory, objects ...runtime.Object) { listMapping := map[schema.GroupVersionResource]string{ {Group: "", Version: "v1", Resource: "services"}: "ServiceList", {Group: "", Version: "v1", Resource: "replicationcontrollers"}: "ReplicationControllerList", {Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}: "CustomResourceDefinitionList", } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listMapping, objects...) tf.FakeDynamicClient = fakeDynamicClient tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/") var gvr schema.GroupVersionResource var name, namespace string if len(tokens) == 4 && tokens[0] == "namespaces" { // e.g. namespaces/my-ns/secrets/my-secret namespace = tokens[1] name = tokens[3] gvr = schema.GroupVersionResource{Version: "v1", Resource: tokens[2]} } else if len(tokens) == 2 && tokens[0] == "applysets" { gvr = schema.GroupVersionResource{Group: "company.com", Version: "v1", Resource: tokens[0]} name = tokens[1] } else { t.Fatalf("unexpected request: path segments %v: request: \n%#v", tokens, req) return nil, nil } switch req.Method { case "GET": obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err == nil { objJson, err := json.Marshal(obj) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.BytesBody(objJson)}, nil } else if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } else { t.Fatalf("error getting object: %v", err) } case "PATCH": require.Equal(t, string(types.ApplyPatchType), req.Header.Get("Content-Type"), "received patch request with unexpected patch type") var existing *unstructured.Unstructured existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if !apierrors.IsNotFound(err) { t.Fatalf("error getting object: %v", err) } } else { existing = existingObj.(*unstructured.Unstructured) } data, err := io.ReadAll(req.Body) require.NoError(t, err) patch := &unstructured.Unstructured{} err = runtime.DecodeInto(codec, data, patch) require.NoError(t, err) var returnData []byte if existing == nil { patch.SetUID("a-static-fake-uid") err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace) require.NoError(t, err, "error creating object") returnData, err = json.Marshal(patch) require.NoError(t, err, "error marshalling response: %v", err) } else { uid := existing.GetUID() patch.DeepCopyInto(existing) existing.SetUID(uid) err = fakeDynamicClient.Tracker().Update(gvr, existing, namespace) require.NoError(t, err, "error updating object") returnData, err = json.Marshal(existing) require.NoError(t, err, "error marshalling response") } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil default: t.Fatalf("unexpected request: %s\n%#v", req.URL.Path, req) return nil, nil } return nil, nil }), } tf.Client = tf.UnstructuredClient } func TestLoadObjects(t *testing.T) { f := cmdtesting.NewTestFactory().WithNamespace("test") defer f.Cleanup() f.Client = &fake.RESTClient{} f.UnstructuredClient = f.Client testFiles := []string{"testdata/prune/simple/manifest1", "testdata/prune/simple/manifest2"} for _, testFile := range testFiles { t.Run(testFile, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", testFile+".yaml") cmd.Flags().Set("applyset", filepath.Base(filepath.Dir(testFile))) cmd.Flags().Set("prune", "true") o, err := flags.ToOptions(f, cmd, "kubectl", []string{}) if err != nil { t.Fatalf("unexpected error creating apply options: %v", err) } err = o.Validate() if err != nil { t.Fatalf("unexpected error from validate: %v", err) } resources, err := o.GetObjects() if err != nil { t.Fatalf("GetObjects gave unexpected error %v", err) } var objectYAMLs []string for _, obj := range resources { y, err := yaml.Marshal(obj.Object) if err != nil { t.Fatalf("error marshaling object: %v", err) } objectYAMLs = append(objectYAMLs, string(y)) } got := strings.Join(objectYAMLs, "\n---\n\n") p := testFile + "-expected-getobjects.yaml" wantBytes, err := os.ReadFile(p) if err != nil { t.Fatalf("error reading file %q: %v", p, err) } want := string(wantBytes) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("GetObjects returned unexpected diff (-want +got):\n%s", diff) } }) }) } } func TestApplySetParentManagement(t *testing.T) { nameParentSecret := "my-set" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() replicationController := readUnstructuredFromFile(t, filenameRC) setUpClientsForApplySetWithSSA(t, tf, replicationController) failDeletes := false tf.FakeDynamicClient.PrependReactor("delete", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) { if failDeletes { return true, nil, fmt.Errorf("an error on the server (\"\") has prevented the request from succeeding") } return false, nil, nil }) cmdutil.BehaviorOnFatal(func(s string, i int) { if failDeletes && s == `error: pruning ReplicationController test/test-rc: an error on the server ("") has prevented the request from succeeding` { t.Logf("got expected error %q", s) } else { t.Fatalf("unexpected exit %d: %s", i, s) } }) defer cmdutil.DefaultBehaviorOnFatal() // Initially, the rc 'exists' server side but the svc and applyset secret do not // This should 'update' the rc and create the secret ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) createdSecret, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) createSecretYaml, err := yaml.Marshal(createdSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: ReplicationController applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(createSecretYaml)) // Next, do an apply that creates a second resource, the svc, and updates the applyset secret outbuff.Reset() errbuff.Reset() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\nservice/test-service serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedSecret, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) updatedSecretYaml, err := yaml.Marshal(updatedSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(updatedSecretYaml)) // Next, do an apply that attempts to remove the rc from the set, but pruning fails // Both types remain in the ApplySet failDeletes = true outbuff.Reset() errbuff.Reset() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "service/test-service serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedSecret, err = tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) updatedSecretYaml, err = yaml.Marshal(updatedSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(updatedSecretYaml)) // Finally, do an apply that successfully removes the rc and updates the set failDeletes = false outbuff.Reset() errbuff.Reset() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "service/test-service serverside-applied\nreplicationcontroller/test-rc pruned\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedSecret, err = tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "secrets", Version: "v1"}, "test", nameParentSecret) require.NoError(t, err) updatedSecretYaml, err = yaml.Marshal(updatedSecret) require.NoError(t, err) require.Equal(t, `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-kinds: Service applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test uid: a-static-fake-uid `, string(updatedSecretYaml)) } func TestApplySetInvalidLiveParent(t *testing.T) { nameParentSecret := "my-set" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() type testCase struct { gksAnnotation string toolingAnnotation string idLabel string expectErr string } validIDLabel := "applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1" validToolingAnnotation := "kubectl/v1.27.0" validGksAnnotation := "Deployment.apps,Namespace,Secret" for name, test := range map[string]testCase{ "group-resources annotation is required": { gksAnnotation: "", toolingAnnotation: validToolingAnnotation, idLabel: validIDLabel, expectErr: "error: parsing ApplySet annotation on \"secrets./my-set\": kubectl requires the \"applyset.kubernetes.io/contains-group-kinds\" annotation to be set on all ApplySet parent objects", }, "group-resources annotation should not contain invalid resources": { gksAnnotation: "does-not-exist", toolingAnnotation: validToolingAnnotation, idLabel: validIDLabel, expectErr: "error: parsing ApplySet annotation on \"secrets./my-set\": could not find mapping for kind in \"applyset.kubernetes.io/contains-group-kinds\" annotation: no matches for kind \"does-not-exist\" in group \"\"", }, "tooling annotation is required": { gksAnnotation: validGksAnnotation, toolingAnnotation: "", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is missing required annotation \"applyset.kubernetes.io/tooling\"", }, "tooling annotation must have kubectl prefix": { gksAnnotation: validGksAnnotation, toolingAnnotation: "helm/v3", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"helm\" instead of \"kubectl\"", }, "tooling annotation with invalid prefix with one segment can be parsed": { gksAnnotation: validGksAnnotation, toolingAnnotation: "helm", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"helm\" instead of \"kubectl\"", }, "tooling annotation with invalid prefix with many segments can be parsed": { gksAnnotation: validGksAnnotation, toolingAnnotation: "example.com/tool/why/v1", idLabel: validIDLabel, expectErr: "error: ApplySet parent object \"secrets./my-set\" already exists and is managed by tooling \"example.com/tool/why\" instead of \"kubectl\"", }, "ID label is required": { gksAnnotation: validGksAnnotation, toolingAnnotation: validToolingAnnotation, idLabel: "", expectErr: "error: ApplySet parent object \"secrets./my-set\" exists and does not have required label applyset.kubernetes.io/id", }, "ID label must match the ApplySet's real ID": { gksAnnotation: validGksAnnotation, toolingAnnotation: validToolingAnnotation, idLabel: "somethingelse", expectErr: fmt.Sprintf("error: ApplySet parent object \"secrets./my-set\" exists and has incorrect value for label \"applyset.kubernetes.io/id\" (got: somethingelse, want: %s)", validIDLabel), }, } { t.Run(name, func(t *testing.T) { require.NotEmpty(t, test.expectErr, "invalid test case") cmdutil.BehaviorOnFatal(func(s string, i int) { assert.Equal(t, test.expectErr, s) }) defer cmdutil.DefaultBehaviorOnFatal() secret := &unstructured.Unstructured{} secret.SetKind("Secret") secret.SetAPIVersion("v1") secret.SetName(nameParentSecret) secret.SetNamespace("test") annotations := make(map[string]string) labels := make(map[string]string) if test.gksAnnotation != "" { annotations[ApplySetGKsAnnotation] = test.gksAnnotation } if test.toolingAnnotation != "" { annotations[ApplySetToolingAnnotation] = test.toolingAnnotation } if test.idLabel != "" { labels[ApplySetParentIDLabel] = test.idLabel } secret.SetAnnotations(annotations) secret.SetLabels(labels) setUpClientsForApplySetWithSSA(t, tf, secret) cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameSVC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) }) } } func TestApplySet_ClusterScopedCustomResourceParent(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() replicationController := readUnstructuredFromFile(t, filenameRC) crd := readUnstructuredFromFile(t, filenameApplySetCRD) cr := readUnstructuredFromFile(t, filenameApplySetCR) setUpClientsForApplySetWithSSA(t, tf, replicationController, crd) ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams() cmdutil.BehaviorOnFatal(func(s string, i int) { require.Equal(t, "error: custom resource ApplySet parents cannot be created automatically", s) }) defer cmdutil.DefaultBehaviorOnFatal() // Initially, the rc 'exists' server side the parent CR does not. This should fail. cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", fmt.Sprintf("applysets.company.com/my-set")) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) cmdtesting.InitTestErrorHandler(t) // Simulate creating the CR parent out of band require.NoError(t, tf.FakeDynamicClient.Tracker().Add(cr)) cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", fmt.Sprintf("applysets.company.com/my-set")) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) updatedCR, err := tf.FakeDynamicClient.Tracker().Get(schema.GroupVersionResource{Resource: "applysets", Version: "v1", Group: "company.com"}, "", "my-set") require.NoError(t, err) updatedCRYaml, err := yaml.Marshal(updatedCR) require.NoError(t, err) require.Equal(t, `apiVersion: company.com/v1 kind: ApplySet metadata: annotations: applyset.kubernetes.io/additional-namespaces: test applyset.kubernetes.io/contains-group-kinds: ReplicationController applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-rhp1a-HVAVT_dFgyEygyA1BEB82HPp2o10UiFTpqtAs-v1 name: my-set `, string(updatedCRYaml)) } func TestApplyWithPruneV2(t *testing.T) { testdirs := []string{"testdata/prune/simple"} for _, testdir := range testdirs { t.Run(testdir, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "", Version: "v1", Resource: "namespaces"}: "NamespaceList", } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) tf.FakeDynamicClient = fakeDynamicClient tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { method := req.Method tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/") if len(tokens) == 2 && tokens[0] == "namespaces" && method == "GET" { name := tokens[1] gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} ns, err := fakeDynamicClient.Tracker().Get(gvr, "", name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, ns)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "GET" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "PATCH" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} var existing *unstructured.Unstructured existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if !apierrors.IsNotFound(err) { t.Fatalf("error getting object: %v", err) } } else { existing = existingObj.(*unstructured.Unstructured) } data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } patch := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, patch); err != nil { t.Fatalf("unexpected error: %v", err) } var returnData []byte if existing == nil { uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) patch.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace); err != nil { t.Fatalf("error creating object: %v", err) } b, err := json.Marshal(patch) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } else { patch.DeepCopyInto(existing) if err := fakeDynamicClient.Tracker().Update(gvr, existing, namespace); err != nil { t.Fatalf("error updating object: %v", err) } b, err := json.Marshal(existing) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil } if len(tokens) == 1 && tokens[0] == "namespaces" && method == "POST" { data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } u := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, u); err != nil { t.Fatalf("unexpected error: %v", err) } name := u.GetName() ns := u.GetNamespace() gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} existing, err := fakeDynamicClient.Tracker().Get(gvr, ns, name) if err != nil { if apierrors.IsNotFound(err) { existing = nil } else { t.Fatalf("error fetching object: %v", err) } } if existing != nil { return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil } uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) u.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, u, ns); err != nil { t.Fatalf("error creating object: %v", err) } body := cmdtesting.ObjBody(codec, u) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil } t.Fatalf("unexpected request: %v %v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.Client = tf.UnstructuredClient tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { manifests := []string{"manifest1", "manifest2"} for _, manifest := range manifests { t.Logf("applying manifest %v", manifest) cmd := &cobra.Command{} flags := NewApplyFlags(genericiooptions.NewTestIOStreamsDiscard()) flags.AddFlags(cmd) cmd.Flags().Set("filename", filepath.Join(testdir, manifest+".yaml")) cmd.Flags().Set("applyset", filepath.Base(testdir)) cmd.Flags().Set("prune", "true") cmd.Flags().Set("validate", "false") o, err := flags.ToOptions(tf, cmd, "kubectl", []string{}) if err != nil { t.Fatalf("unexpected error creating apply options: %v", err) } err = o.Validate() if err != nil { t.Fatalf("unexpected error from validate: %v", err) } var unifiedOutput bytes.Buffer o.Out = &unifiedOutput o.ErrOut = &unifiedOutput if err := o.Run(); err != nil { t.Errorf("error running apply: %v", err) } got := unifiedOutput.String() p := filepath.Join(testdir, manifest+"-expected-apply.txt") wantBytes, err := os.ReadFile(p) if err != nil { t.Fatalf("error reading file %q: %v", p, err) } want := string(wantBytes) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("apply output has unexpected diff (-want +got):\n%s", diff) } } }) }) } } func TestApplySetUpdateConflictsAreRetried(t *testing.T) { nameParentSecret := "my-set" pathSecret := "/namespaces/test/secrets/" + nameParentSecret secretYaml := `apiVersion: v1 kind: Secret metadata: annotations: applyset.kubernetes.io/additional-namespaces: "" applyset.kubernetes.io/contains-group-resources: replicationcontrollers applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$ creationTimestamp: null labels: applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1 name: my-set namespace: test ` tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() applyReturnedConflict := false appliedWithConflictsForced := false tf.Client = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.Method == "GET" && req.URL.Path == pathSecret { data, err := yaml.YAMLToJSON([]byte(secretYaml)) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } contentType := req.Header.Get("Content-Type") forceConflicts := req.URL.Query().Get("force") == "true" if req.Method == "PATCH" && contentType == string(types.ApplyPatchType) { // make the ApplySet secret SSA request fail unless conflicts are forced if req.URL.Path == pathSecret { if !forceConflicts { applyReturnedConflict = true return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(strings.NewReader("Apply failed with 1 conflict: conflict with \"other\": .metadata.annotations.applyset.kubernetes.io/contains-group-resources"))}, nil } appliedWithConflictsForced = true } data, err := io.ReadAll(req.Body) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } t.Fatalf("unexpected request to %s\n%#v", req.URL.Path, req) return nil, nil }), } tf.UnstructuredClient = tf.Client ioStreams, _, outbuff, errbuff := genericiooptions.NewTestIOStreams() cmdutil.BehaviorOnFatal(fatalNoExit(t, ioStreams)) defer cmdutil.DefaultBehaviorOnFatal() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied\n", outbuff.String()) assert.Equal(t, "", errbuff.String()) assert.Truef(t, applyReturnedConflict, "test did not simulate a conflict scenario") assert.Truef(t, appliedWithConflictsForced, "conflicts were never forced") } func TestApplyWithPruneV2Fail(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "", Version: "v1", Resource: "namespaces"}: "NamespaceList", } fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) tf.FakeDynamicClient = fakeDynamicClient failDelete := false fakeDynamicClient.PrependReactor("delete", "*", func(action testing2.Action) (handled bool, ret runtime.Object, err error) { if failDelete { return true, nil, fmt.Errorf("an error on the server (\"\") has prevented the request from succeeding") } return false, nil, nil }) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { method := req.Method tokens := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/") if len(tokens) == 2 && tokens[0] == "namespaces" && method == "GET" { name := tokens[1] gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} ns, err := fakeDynamicClient.Tracker().Get(gvr, "", name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, ns)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "GET" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} obj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if apierrors.IsNotFound(err) { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader()}, nil } t.Fatalf("error getting object: %v", err) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil } if len(tokens) == 4 && tokens[0] == "namespaces" && tokens[2] == "secrets" && method == "PATCH" { namespace := tokens[1] name := tokens[3] gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} var existing *unstructured.Unstructured existingObj, err := fakeDynamicClient.Tracker().Get(gvr, namespace, name) if err != nil { if !apierrors.IsNotFound(err) { t.Fatalf("error getting object: %v", err) } } else { existing = existingObj.(*unstructured.Unstructured) } data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } patch := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, patch); err != nil { t.Fatalf("unexpected error: %v", err) } var returnData []byte if existing == nil { uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) patch.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, patch, namespace); err != nil { t.Fatalf("error creating object: %v", err) } b, err := json.Marshal(patch) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } else { patch.DeepCopyInto(existing) if err := fakeDynamicClient.Tracker().Update(gvr, existing, namespace); err != nil { t.Fatalf("error updating object: %v", err) } b, err := json.Marshal(existing) if err != nil { t.Fatalf("error marshalling response: %v", err) } returnData = b } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(returnData))}, nil } if len(tokens) == 1 && tokens[0] == "namespaces" && method == "POST" { data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } u := &unstructured.Unstructured{} if err := runtime.DecodeInto(codec, data, u); err != nil { t.Fatalf("unexpected error: %v", err) } name := u.GetName() ns := u.GetNamespace() gvr := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} existing, err := fakeDynamicClient.Tracker().Get(gvr, ns, name) if err != nil { if apierrors.IsNotFound(err) { existing = nil } else { t.Fatalf("error fetching object: %v", err) } } if existing != nil { return &http.Response{StatusCode: http.StatusConflict, Header: cmdtesting.DefaultHeader()}, nil } uid := types.UID(fmt.Sprintf("%v", time.Now().UnixNano())) u.SetUID(uid) if err := fakeDynamicClient.Tracker().Create(gvr, u, ns); err != nil { t.Fatalf("error creating object: %v", err) } body := cmdtesting.ObjBody(codec, u) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil } t.Fatalf("unexpected request: %v %v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.Client = tf.UnstructuredClient tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc testdirs := []string{"testdata/prune/simple"} for _, testdir := range testdirs { t.Run(testdir, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { manifests := []string{"manifest1", "manifest2"} for i, manifest := range manifests { if i != 0 { t.Logf("will inject failures into future delete operations") failDelete = true } t.Logf("applying manifest %v", manifest) var unifiedOutput bytes.Buffer ioStreams := genericiooptions.IOStreams{ ErrOut: &unifiedOutput, Out: &unifiedOutput, In: bytes.NewBufferString(""), } cmdutil.BehaviorOnFatal(fatalNoExit(t, ioStreams)) defer cmdutil.DefaultBehaviorOnFatal() rootCmd := &cobra.Command{ Use: "kubectl", } kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0) kubeConfigFlags.AddFlags(rootCmd.PersistentFlags()) applyCmd := NewCmdApply("kubectl", tf, ioStreams) rootCmd.AddCommand(applyCmd) rootCmd.SetArgs([]string{ "apply", "--filename=" + filepath.Join(testdir, manifest+".yaml"), "--applyset=" + filepath.Base(testdir), "--namespace=default", "--prune=true", "--validate=false", }) if err := rootCmd.Execute(); err != nil { t.Errorf("error running apply command: %v", err) } got := unifiedOutput.String() p := filepath.Join(testdir, "scenarios", "error-on-apply", manifest+"-expected-apply.txt") wantBytes, err := os.ReadFile(p) if err != nil { t.Fatalf("error reading file %q: %v", p, err) } want := string(wantBytes) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("apply output has unexpected diff (-want +got):\n%s", diff) } } }) }) } } // fatalNoExit is a handler that replaces the default cmdutil.BehaviorOnFatal, // that still prints as expected, but does not call os.Exit (which terminates our tests) func fatalNoExit(t *testing.T, ioStreams genericiooptions.IOStreams) func(msg string, code int) { return func(msg string, code int) { if len(msg) > 0 { // add newline if needed if !strings.HasSuffix(msg, "\n") { msg += "\n" } fmt.Fprint(ioStreams.ErrOut, msg) } } } func TestApplySetDryRun(t *testing.T) { cmdtesting.InitTestErrorHandler(t) nameRC, rc := readReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC nameParentSecret := "my-set" pathSecret := "/namespaces/test/secrets/" + nameParentSecret tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() // Scenario: the rc 'exists' server side but the applyset secret does not // In dry run mode, non-dry run patch requests should not be made, and the secret should not be created serverSideData := map[string][]byte{ pathRC: rc, } fakeDryRunClient := func(t *testing.T, allowPatch bool) *fake.RESTClient { return &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.Method == "GET" { data, ok := serverSideData[req.URL.Path] if !ok { return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(nil))}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } if req.Method == "PATCH" && allowPatch && req.URL.Query().Get("dryRun") == "All" { data, err := io.ReadAll(req.Body) require.NoError(t, err) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(data))}, nil } t.Fatalf("unexpected request: to %s\n%#v", req.URL.Path, req) return nil, nil }), } } t.Run("server side dry run", func(t *testing.T) { ioStreams, _, outbuff, _ := genericiooptions.NewTestIOStreams() tf.Client = fakeDryRunClient(t, true) tf.UnstructuredClient = tf.Client cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("server-side", "true") cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Flags().Set("dry-run", "server") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc serverside-applied (server dry run)\n", outbuff.String()) assert.Len(t, serverSideData, 1, "unexpected creation") require.Nil(t, serverSideData[pathSecret], "secret was created") }) t.Run("client side dry run", func(t *testing.T) { ioStreams, _, outbuff, _ := genericiooptions.NewTestIOStreams() tf.Client = fakeDryRunClient(t, false) tf.UnstructuredClient = tf.Client cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmd := NewCmdApply("kubectl", tf, ioStreams) cmd.Flags().Set("filename", filenameRC) cmd.Flags().Set("applyset", nameParentSecret) cmd.Flags().Set("prune", "true") cmd.Flags().Set("dry-run", "client") cmd.Run(cmd, []string{}) }) assert.Equal(t, "replicationcontroller/test-rc configured (dry run)\n", outbuff.String()) assert.Len(t, serverSideData, 1, "unexpected creation") require.Nil(t, serverSideData[pathSecret], "secret was created") }) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_view_last_applied.go000066400000000000000000000130261504711711200331650ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "bytes" "encoding/json" "fmt" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "sigs.k8s.io/yaml" ) // ViewLastAppliedOptions defines options for the `apply view-last-applied` command.` type ViewLastAppliedOptions struct { FilenameOptions resource.FilenameOptions Selector string LastAppliedConfigurationList []string OutputFormat string All bool Factory cmdutil.Factory genericiooptions.IOStreams } var ( applyViewLastAppliedLong = templates.LongDesc(i18n.T(` View the latest last-applied-configuration annotations by type/name or file. The default output will be printed to stdout in YAML format. You can use the -o option to change the output format.`)) applyViewLastAppliedExample = templates.Examples(i18n.T(` # View the last-applied-configuration annotations by type/name in YAML kubectl apply view-last-applied deployment/nginx # View the last-applied-configuration annotations by file in JSON kubectl apply view-last-applied -f deploy.yaml -o json`)) ) // NewViewLastAppliedOptions takes option arguments from a CLI stream and returns it at ViewLastAppliedOptions type. func NewViewLastAppliedOptions(ioStreams genericiooptions.IOStreams) *ViewLastAppliedOptions { return &ViewLastAppliedOptions{ OutputFormat: "yaml", IOStreams: ioStreams, } } // NewCmdApplyViewLastApplied creates the cobra CLI `apply` subcommand `view-last-applied`.` func NewCmdApplyViewLastApplied(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { options := NewViewLastAppliedOptions(ioStreams) cmd := &cobra.Command{ Use: "view-last-applied (TYPE [NAME | -l label] | TYPE/NAME | -f FILENAME)", DisableFlagsInUseLine: true, Short: i18n.T("View the latest last-applied-configuration annotations of a resource/object"), Long: applyViewLastAppliedLong, Example: applyViewLastAppliedExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.Complete(cmd, f, args)) cmdutil.CheckErr(options.Validate()) cmdutil.CheckErr(options.RunApplyViewLastApplied(cmd)) }, } cmd.Flags().StringVarP(&options.OutputFormat, "output", "o", options.OutputFormat, `Output format. Must be one of (yaml, json)`) cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all resources in the namespace of the specified resource types") usage := "that contains the last-applied-configuration annotations" cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) cmdutil.AddLabelSelectorFlagVar(cmd, &options.Selector) return cmd } // Complete checks an object for last-applied-configuration annotations. func (o *ViewLastAppliedOptions) Complete(cmd *cobra.Command, f cmdutil.Factory, args []string) error { cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } r := f.NewBuilder(). Unstructured(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(enforceNamespace, args...). SelectAllParam(o.All). LabelSelectorParam(o.Selector). Latest(). Flatten(). Do() err = r.Err() if err != nil { return err } err = r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } configString, err := util.GetOriginalConfiguration(info.Object) if err != nil { return err } if configString == nil { return cmdutil.AddSourceToErr(fmt.Sprintf("no last-applied-configuration annotation found on resource: %s\n", info.Name), info.Source, err) } o.LastAppliedConfigurationList = append(o.LastAppliedConfigurationList, string(configString)) return nil }) if err != nil { return err } return nil } // Validate checks ViewLastAppliedOptions for validity. func (o *ViewLastAppliedOptions) Validate() error { return nil } // RunApplyViewLastApplied executes the `view-last-applied` command according to ViewLastAppliedOptions. func (o *ViewLastAppliedOptions) RunApplyViewLastApplied(cmd *cobra.Command) error { for _, str := range o.LastAppliedConfigurationList { switch o.OutputFormat { case "json": jsonBuffer := &bytes.Buffer{} err := json.Indent(jsonBuffer, []byte(str), "", " ") if err != nil { return err } fmt.Fprint(o.Out, string(jsonBuffer.Bytes())) case "yaml": yamlOutput, err := yaml.JSONToYAML([]byte(str)) if err != nil { return err } fmt.Fprint(o.Out, string(yamlOutput)) default: return cmdutil.UsageErrorf( cmd, "Unexpected -o output mode: %s, the flag 'output' must be one of yaml|json", o.OutputFormat) } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset.go000066400000000000000000000560631504711711200301360ustar00rootroot00000000000000/* Copyright 2023 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "context" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "sort" "strings" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) // Label and annotation keys from the ApplySet specification. // https://git.k8s.io/enhancements/keps/sig-cli/3659-kubectl-apply-prune#design-details-applyset-specification const ( // ApplySetToolingAnnotation is the key of the label that indicates which tool is used to manage this ApplySet. // Tooling should refuse to mutate ApplySets belonging to other tools. // The value must be in the format /. // Example value: "kubectl/v1.27" or "helm/v3" or "kpt/v1.0.0" ApplySetToolingAnnotation = "applyset.kubernetes.io/tooling" // ApplySetAdditionalNamespacesAnnotation annotation extends the scope of the ApplySet beyond the parent // object's own namespace (if any) to include the listed namespaces. The value is a comma-separated // list of the names of namespaces other than the parent's namespace in which objects are found // Example value: "kube-system,ns1,ns2". ApplySetAdditionalNamespacesAnnotation = "applyset.kubernetes.io/additional-namespaces" // Deprecated: ApplySetGRsAnnotation is a list of group-resources used to optimize listing of ApplySet member objects. // It is optional in the ApplySet specification, as tools can perform discovery or use a different optimization. // However, it is currently required in kubectl. // When present, the value of this annotation must be a comma separated list of the group-resources, // in the fully-qualified name format, i.e. .. // Example value: "certificates.cert-manager.io,configmaps,deployments.apps,secrets,services" // Deprecated and replaced by ApplySetGKsAnnotation, support for this can be removed in applyset beta or GA. DeprecatedApplySetGRsAnnotation = "applyset.kubernetes.io/contains-group-resources" // ApplySetGKsAnnotation is a list of group-kinds used to optimize listing of ApplySet member objects. // It is optional in the ApplySet specification, as tools can perform discovery or use a different optimization. // However, it is currently required in kubectl. // When present, the value of this annotation must be a comma separated list of the group-kinds, // in the fully-qualified name format, i.e. .. // Example value: "Certificate.cert-manager.io,ConfigMap,deployments.apps,Secret,Service" ApplySetGKsAnnotation = "applyset.kubernetes.io/contains-group-kinds" // ApplySetParentIDLabel is the key of the label that makes object an ApplySet parent object. // Its value MUST use the format specified in V1ApplySetIdFormat below ApplySetParentIDLabel = "applyset.kubernetes.io/id" // V1ApplySetIdFormat is the format required for the value of ApplySetParentIDLabel (and ApplysetPartOfLabel). // The %s segment is the unique ID of the object itself, which MUST be the base64 encoding // (using the URL safe encoding of RFC4648) of the hash of the GKNN of the object it is on, in the form: // base64(sha256(...)). V1ApplySetIdFormat = "applyset-%s-v1" // ApplysetPartOfLabel is the key of the label which indicates that the object is a member of an ApplySet. // The value of the label MUST match the value of ApplySetParentIDLabel on the parent object. ApplysetPartOfLabel = "applyset.kubernetes.io/part-of" // ApplysetParentCRDLabel is the key of the label that can be set on a CRD to identify // the custom resource type it defines (not the CRD itself) as an allowed parent for an ApplySet. ApplysetParentCRDLabel = "applyset.kubernetes.io/is-parent-type" ) var defaultApplySetParentGVR = schema.GroupVersionResource{Version: "v1", Resource: "secrets"} // ApplySet tracks the information about an applyset apply/prune type ApplySet struct { // parentRef is a reference to the parent object that is used to track the applyset. parentRef *ApplySetParentRef // toolingID is the value to be used and validated in the applyset.kubernetes.io/tooling annotation. toolingID ApplySetTooling // currentResources is the set of resources that are part of the sever-side set as of when the current operation started. currentResources map[schema.GroupKind]*kindInfo // currentNamespaces is the set of namespaces that contain objects in this applyset as of when the current operation started. currentNamespaces sets.Set[string] // updatedResources is the set of resources that will be part of the set as of when the current operation completes. updatedResources map[schema.GroupKind]*kindInfo // updatedNamespaces is the set of namespaces that will contain objects in this applyset as of when the current operation completes. updatedNamespaces sets.Set[string] restMapper meta.RESTMapper // client is a client specific to the ApplySet parent object's type client resource.RESTClient } var builtinApplySetParentGVRs = sets.New[schema.GroupVersionResource]( defaultApplySetParentGVR, schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, ) // ApplySetParentRef stores object and type meta for the parent object that is used to track the applyset. type ApplySetParentRef struct { Name string Namespace string *meta.RESTMapping } func (p ApplySetParentRef) IsNamespaced() bool { return p.Scope.Name() == meta.RESTScopeNameNamespace } // String returns the string representation of the parent object using the same format // that we expect to receive in the --applyset flag on the CLI. func (p ApplySetParentRef) String() string { return fmt.Sprintf("%s.%s/%s", p.Resource.Resource, p.Resource.Group, p.Name) } type ApplySetTooling struct { Name string Version string } func (t ApplySetTooling) String() string { return fmt.Sprintf("%s/%s", t.Name, t.Version) } // NewApplySet creates a new ApplySet object tracked by the given parent object. func NewApplySet(parent *ApplySetParentRef, tooling ApplySetTooling, mapper meta.RESTMapper, client resource.RESTClient) *ApplySet { return &ApplySet{ currentResources: make(map[schema.GroupKind]*kindInfo), currentNamespaces: make(sets.Set[string]), updatedResources: make(map[schema.GroupKind]*kindInfo), updatedNamespaces: make(sets.Set[string]), parentRef: parent, toolingID: tooling, restMapper: mapper, client: client, } } const applySetIDPartDelimiter = "." // ID is the label value that we are using to identify this applyset. // Format: base64(sha256(...)), using the URL safe encoding of RFC4648. func (a ApplySet) ID() string { unencoded := strings.Join([]string{a.parentRef.Name, a.parentRef.Namespace, a.parentRef.GroupVersionKind.Kind, a.parentRef.GroupVersionKind.Group}, applySetIDPartDelimiter) hashed := sha256.Sum256([]byte(unencoded)) b64 := base64.RawURLEncoding.EncodeToString(hashed[:]) // Label values must start and end with alphanumeric values, so add a known-safe prefix and suffix. return fmt.Sprintf(V1ApplySetIdFormat, b64) } // Validate imposes restrictions on the parent object that is used to track the applyset. func (a ApplySet) Validate(ctx context.Context, client dynamic.Interface) error { var errors []error if a.parentRef.IsNamespaced() && a.parentRef.Namespace == "" { errors = append(errors, fmt.Errorf("namespace is required to use namespace-scoped ApplySet")) } if !builtinApplySetParentGVRs.Has(a.parentRef.Resource) { // Determine which custom resource types are allowed as ApplySet parents. // Optimization: Since this makes requests, we only do this if they aren't using a default type. permittedCRParents, err := a.getAllowedCustomResourceParents(ctx, client) if err != nil { errors = append(errors, fmt.Errorf("identifying allowed custom resource parent types: %w", err)) } parentRefResourceIgnoreVersion := a.parentRef.Resource.GroupResource().WithVersion("") if !permittedCRParents.Has(parentRefResourceIgnoreVersion) { errors = append(errors, fmt.Errorf("resource %q is not permitted as an ApplySet parent", a.parentRef.Resource)) } } return utilerrors.NewAggregate(errors) } func (a *ApplySet) labelForCustomParentCRDs() *metav1.LabelSelector { return &metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{{ Key: ApplysetParentCRDLabel, Operator: metav1.LabelSelectorOpExists, }}, } } func (a *ApplySet) getAllowedCustomResourceParents(ctx context.Context, client dynamic.Interface) (sets.Set[schema.GroupVersionResource], error) { opts := metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(a.labelForCustomParentCRDs()), } list, err := client.Resource(schema.GroupVersionResource{ Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions", }).List(ctx, opts) if err != nil { return nil, err } set := sets.New[schema.GroupVersionResource]() for i := range list.Items { // Custom resources must be named `.` // and are served under `/apis///.../` gr := schema.ParseGroupResource(list.Items[i].GetName()) set.Insert(gr.WithVersion("")) } return set, nil } func (a *ApplySet) LabelsForMember() map[string]string { return map[string]string{ ApplysetPartOfLabel: a.ID(), } } // addLabels sets our tracking labels on each object; this should be called as part of loading the objects. func (a *ApplySet) AddLabels(objects ...*resource.Info) error { applysetLabels := a.LabelsForMember() for _, obj := range objects { accessor, err := meta.Accessor(obj.Object) if err != nil { return fmt.Errorf("getting accessor: %w", err) } labels := accessor.GetLabels() if labels == nil { labels = make(map[string]string) } for k, v := range applysetLabels { if _, found := labels[k]; found { return fmt.Errorf("ApplySet label %q already set in input data", k) } labels[k] = v } accessor.SetLabels(labels) } return nil } func (a *ApplySet) fetchParent() error { helper := resource.NewHelper(a.client, a.parentRef.RESTMapping) obj, err := helper.Get(a.parentRef.Namespace, a.parentRef.Name) if errors.IsNotFound(err) { if !builtinApplySetParentGVRs.Has(a.parentRef.Resource) { return fmt.Errorf("custom resource ApplySet parents cannot be created automatically") } return nil } else if err != nil { return fmt.Errorf("failed to fetch ApplySet parent object %q: %w", a.parentRef, err) } else if obj == nil { return fmt.Errorf("failed to fetch ApplySet parent object %q", a.parentRef) } labels, annotations, err := getLabelsAndAnnotations(obj) if err != nil { return fmt.Errorf("getting metadata from parent object %q: %w", a.parentRef, err) } toolAnnotation, hasToolAnno := annotations[ApplySetToolingAnnotation] if !hasToolAnno { return fmt.Errorf("ApplySet parent object %q already exists and is missing required annotation %q", a.parentRef, ApplySetToolingAnnotation) } if managedBy := toolingBaseName(toolAnnotation); managedBy != a.toolingID.Name { return fmt.Errorf("ApplySet parent object %q already exists and is managed by tooling %q instead of %q", a.parentRef, managedBy, a.toolingID.Name) } idLabel, hasIDLabel := labels[ApplySetParentIDLabel] if !hasIDLabel { return fmt.Errorf("ApplySet parent object %q exists and does not have required label %s", a.parentRef, ApplySetParentIDLabel) } if idLabel != a.ID() { return fmt.Errorf("ApplySet parent object %q exists and has incorrect value for label %q (got: %s, want: %s)", a.parentRef, ApplySetParentIDLabel, idLabel, a.ID()) } if a.currentResources, err = parseKindAnnotation(annotations, a.restMapper); err != nil { // TODO: handle GVRs for now-deleted CRDs return fmt.Errorf("parsing ApplySet annotation on %q: %w", a.parentRef, err) } a.currentNamespaces = parseNamespacesAnnotation(annotations) if a.parentRef.IsNamespaced() { a.currentNamespaces.Insert(a.parentRef.Namespace) } return nil } func (a *ApplySet) LabelSelectorForMembers() string { return metav1.FormatLabelSelector(&metav1.LabelSelector{ MatchLabels: a.LabelsForMember(), }) } // AllPrunableResources returns the list of all resources that should be considered for pruning. // This is potentially a superset of the resources types that actually contain resources. func (a *ApplySet) AllPrunableResources() []*kindInfo { var ret []*kindInfo for _, m := range a.currentResources { ret = append(ret, m) } return ret } // AllPrunableNamespaces returns the list of all namespaces that should be considered for pruning. // This is potentially a superset of the namespaces that actually contain resources. func (a *ApplySet) AllPrunableNamespaces() []string { var ret []string for ns := range a.currentNamespaces { ret = append(ret, ns) } return ret } func getLabelsAndAnnotations(obj runtime.Object) (map[string]string, map[string]string, error) { accessor, err := meta.Accessor(obj) if err != nil { return nil, nil, err } return accessor.GetLabels(), accessor.GetAnnotations(), nil } func toolingBaseName(toolAnnotation string) string { parts := strings.Split(toolAnnotation, "/") if len(parts) >= 2 { return strings.Join(parts[:len(parts)-1], "/") } return toolAnnotation } // kindInfo holds type information about a particular resource type. type kindInfo struct { restMapping *meta.RESTMapping } func parseKindAnnotation(annotations map[string]string, mapper meta.RESTMapper) (map[schema.GroupKind]*kindInfo, error) { annotation, ok := annotations[ApplySetGKsAnnotation] if !ok { if annotations[DeprecatedApplySetGRsAnnotation] != "" { return parseDeprecatedResourceAnnotation(annotations[DeprecatedApplySetGRsAnnotation], mapper) } // The spec does not require this annotation. However, 'missing' means 'perform discovery'. // We return an error because we do not currently support dynamic discovery in kubectl apply. return nil, fmt.Errorf("kubectl requires the %q annotation to be set on all ApplySet parent objects", ApplySetGKsAnnotation) } mappings := make(map[schema.GroupKind]*kindInfo) // Annotation present but empty means that this is currently an empty set. if annotation == "" { return mappings, nil } for _, gkString := range strings.Split(annotation, ",") { gk := schema.ParseGroupKind(gkString) restMapping, err := mapper.RESTMapping(gk) if err != nil { return nil, fmt.Errorf("could not find mapping for kind in %q annotation: %w", ApplySetGKsAnnotation, err) } mappings[gk] = &kindInfo{ restMapping: restMapping, } } return mappings, nil } func parseDeprecatedResourceAnnotation(annotation string, mapper meta.RESTMapper) (map[schema.GroupKind]*kindInfo, error) { mappings := make(map[schema.GroupKind]*kindInfo) // Annotation present but empty means that this is currently an empty set. if annotation == "" { return mappings, nil } for _, grString := range strings.Split(annotation, ",") { gr := schema.ParseGroupResource(grString) gvk, err := mapper.KindFor(gr.WithVersion("")) if err != nil { return nil, fmt.Errorf("invalid group resource in %q annotation: %w", DeprecatedApplySetGRsAnnotation, err) } restMapping, err := mapper.RESTMapping(gvk.GroupKind()) if err != nil { return nil, fmt.Errorf("could not find kind for resource in %q annotation: %w", DeprecatedApplySetGRsAnnotation, err) } mappings[gvk.GroupKind()] = &kindInfo{ restMapping: restMapping, } } return mappings, nil } func parseNamespacesAnnotation(annotations map[string]string) sets.Set[string] { annotation, ok := annotations[ApplySetAdditionalNamespacesAnnotation] if !ok { // this annotation is completely optional return sets.Set[string]{} } // Don't include an empty namespace if annotation == "" { return sets.Set[string]{} } return sets.New(strings.Split(annotation, ",")...) } // addResource registers the given resource and namespace as being part of the updated set of // resources being applied by the current operation. func (a *ApplySet) addResource(restMapping *meta.RESTMapping, namespace string) { gk := restMapping.GroupVersionKind.GroupKind() if _, found := a.updatedResources[gk]; !found { a.updatedResources[gk] = &kindInfo{ restMapping: restMapping, } } if restMapping.Scope == meta.RESTScopeNamespace && namespace != "" { a.updatedNamespaces.Insert(namespace) } } type ApplySetUpdateMode string var updateToLatestSet ApplySetUpdateMode = "latest" var updateToSuperset ApplySetUpdateMode = "superset" func (a *ApplySet) updateParent(mode ApplySetUpdateMode, dryRun cmdutil.DryRunStrategy, validation string) error { data, err := json.Marshal(a.buildParentPatch(mode)) if err != nil { return fmt.Errorf("failed to encode patch for ApplySet parent: %w", err) } // Note that because we are using SSA, we will remove any annotations we don't specify, // which is how we remove the deprecated contains-group-resources annotation. err = serverSideApplyRequest(a, data, dryRun, validation, false) if err != nil && errors.IsConflict(err) { // Try again with conflicts forced klog.Warningf("WARNING: failed to update ApplySet: %s\nApplySet field manager %s should own these fields. Retrying with conflicts forced.", err.Error(), a.FieldManager()) err = serverSideApplyRequest(a, data, dryRun, validation, true) } if err != nil { return fmt.Errorf("failed to update ApplySet: %w", err) } return nil } func serverSideApplyRequest(a *ApplySet, data []byte, dryRun cmdutil.DryRunStrategy, validation string, forceConficts bool) error { if dryRun == cmdutil.DryRunClient { return nil } helper := resource.NewHelper(a.client, a.parentRef.RESTMapping). DryRun(dryRun == cmdutil.DryRunServer). WithFieldManager(a.FieldManager()). WithFieldValidation(validation) options := metav1.PatchOptions{ Force: &forceConficts, } _, err := helper.Patch( a.parentRef.Namespace, a.parentRef.Name, types.ApplyPatchType, data, &options, ) return err } func (a *ApplySet) buildParentPatch(mode ApplySetUpdateMode) *metav1.PartialObjectMetadata { var newGKsAnnotation, newNsAnnotation string switch mode { case updateToSuperset: // If the apply succeeded but pruning failed, the set of group resources that // the ApplySet should track is the superset of the previous and current resources. // This ensures that the resources that failed to be pruned are not orphaned from the set. grSuperset := sets.KeySet(a.currentResources).Union(sets.KeySet(a.updatedResources)) newGKsAnnotation = generateKindsAnnotation(grSuperset) newNsAnnotation = generateNamespacesAnnotation(a.currentNamespaces.Union(a.updatedNamespaces), a.parentRef.Namespace) case updateToLatestSet: newGKsAnnotation = generateKindsAnnotation(sets.KeySet(a.updatedResources)) newNsAnnotation = generateNamespacesAnnotation(a.updatedNamespaces, a.parentRef.Namespace) } return &metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ Kind: a.parentRef.GroupVersionKind.Kind, APIVersion: a.parentRef.GroupVersionKind.GroupVersion().String(), }, ObjectMeta: metav1.ObjectMeta{ Name: a.parentRef.Name, Namespace: a.parentRef.Namespace, Annotations: map[string]string{ ApplySetToolingAnnotation: a.toolingID.String(), ApplySetGKsAnnotation: newGKsAnnotation, ApplySetAdditionalNamespacesAnnotation: newNsAnnotation, }, Labels: map[string]string{ ApplySetParentIDLabel: a.ID(), }, }, } } func generateNamespacesAnnotation(namespaces sets.Set[string], skip string) string { nsList := namespaces.Clone().Delete(skip).UnsortedList() sort.Strings(nsList) return strings.Join(nsList, ",") } func generateKindsAnnotation(resources sets.Set[schema.GroupKind]) string { var gks []string for gk := range resources { gks = append(gks, gk.String()) } sort.Strings(gks) return strings.Join(gks, ",") } func (a ApplySet) FieldManager() string { return fmt.Sprintf("%s-applyset", a.toolingID.Name) } // ParseApplySetParentRef creates a new ApplySetParentRef from a parent reference in the format [RESOURCE][.GROUP]/NAME func ParseApplySetParentRef(parentRefStr string, mapper meta.RESTMapper) (*ApplySetParentRef, error) { var gvr schema.GroupVersionResource var name string if groupRes, nameSuffix, hasTypeInfo := strings.Cut(parentRefStr, "/"); hasTypeInfo { name = nameSuffix gvr = schema.ParseGroupResource(groupRes).WithVersion("") } else { name = parentRefStr gvr = defaultApplySetParentGVR } if name == "" { return nil, fmt.Errorf("name cannot be blank") } gvk, err := mapper.KindFor(gvr) if err != nil { return nil, err } mapping, err := mapper.RESTMapping(gvk.GroupKind()) if err != nil { return nil, err } return &ApplySetParentRef{Name: name, RESTMapping: mapping}, nil } // Prune deletes any objects from the apiserver that are no longer in the applyset. func (a *ApplySet) Prune(ctx context.Context, o *ApplyOptions) error { printer, err := o.ToPrinter("pruned") if err != nil { return err } opt := &ApplySetDeleteOptions{ CascadingStrategy: o.DeleteOptions.CascadingStrategy, DryRunStrategy: o.DryRunStrategy, GracePeriod: o.DeleteOptions.GracePeriod, Printer: printer, IOStreams: o.IOStreams, } if err := a.pruneAll(ctx, o.DynamicClient, o.VisitedUids, opt); err != nil { return err } if err := a.updateParent(updateToLatestSet, o.DryRunStrategy, o.ValidationDirective); err != nil { return fmt.Errorf("apply and prune succeeded, but ApplySet update failed: %w", err) } return nil } // BeforeApply should be called before applying the objects. // It pre-updates the parent object so that it covers the resources that will be applied. // In this way, even if we are interrupted, we will not leak objects. func (a *ApplySet) BeforeApply(objects []*resource.Info, dryRunStrategy cmdutil.DryRunStrategy, validationDirective string) error { if err := a.fetchParent(); err != nil { return err } // Update the live parent object to the superset of the current and previous resources. // Doing this before the actual apply and prune operations improves behavior by ensuring // the live object contains the superset on failure. This may cause the next pruning // operation to make a larger number of GET requests than strictly necessary, but it prevents // object leakage from the set. The superset will automatically be reduced to the correct // set by the next successful operation. for _, info := range objects { a.addResource(info.ResourceMapping(), info.Namespace) } if err := a.updateParent(updateToSuperset, dryRunStrategy, validationDirective); err != nil { return err } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset_pruner.go000066400000000000000000000130371504711711200315230ustar00rootroot00000000000000/* Copyright 2023 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "context" "fmt" "sync" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/dynamic" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) type ApplySetDeleteOptions struct { CascadingStrategy metav1.DeletionPropagation DryRunStrategy cmdutil.DryRunStrategy GracePeriod int Printer printers.ResourcePrinter IOStreams genericiooptions.IOStreams } // PruneObject is an apiserver object that should be deleted as part of prune. type PruneObject struct { Name string Namespace string Mapping *meta.RESTMapping Object runtime.Object } // String returns a human-readable name of the object, for use in debug messages. func (p *PruneObject) String() string { s := p.Mapping.GroupVersionKind.GroupKind().String() if p.Namespace != "" { s += " " + p.Namespace + "/" + p.Name } else { s += " " + p.Name } return s } // FindAllObjectsToPrune returns the list of objects that will be pruned. // Calling this instead of Prune can be useful for dry-run / diff behaviour. func (a *ApplySet) FindAllObjectsToPrune(ctx context.Context, dynamicClient dynamic.Interface, visitedUids sets.Set[types.UID]) ([]PruneObject, error) { type task struct { namespace string restMapping *meta.RESTMapping err error results []PruneObject } var tasks []*task // We run discovery in parallel, in as many goroutines as priority and fairness will allow // (We don't expect many requests in real-world scenarios - maybe tens, unlikely to be hundreds) for gvk, resource := range a.AllPrunableResources() { scope := resource.restMapping.Scope switch scope.Name() { case meta.RESTScopeNameNamespace: for _, namespace := range a.AllPrunableNamespaces() { if namespace == "" { // Just double-check because otherwise we get cryptic error messages return nil, fmt.Errorf("unexpectedly encountered empty namespace during prune of namespace-scoped resource %v", gvk) } tasks = append(tasks, &task{ namespace: namespace, restMapping: resource.restMapping, }) } case meta.RESTScopeNameRoot: tasks = append(tasks, &task{ restMapping: resource.restMapping, }) default: return nil, fmt.Errorf("unhandled scope %q", scope.Name()) } } var wg sync.WaitGroup for i := range tasks { task := tasks[i] wg.Add(1) go func() { defer wg.Done() results, err := a.findObjectsToPrune(ctx, dynamicClient, visitedUids, task.namespace, task.restMapping) if err != nil { task.err = fmt.Errorf("listing %v objects for pruning: %w", task.restMapping.GroupVersionKind.String(), err) } else { task.results = results } }() } // Wait for all the goroutines to finish wg.Wait() var allObjects []PruneObject for _, task := range tasks { if task.err != nil { return nil, task.err } allObjects = append(allObjects, task.results...) } return allObjects, nil } func (a *ApplySet) pruneAll(ctx context.Context, dynamicClient dynamic.Interface, visitedUids sets.Set[types.UID], deleteOptions *ApplySetDeleteOptions) error { allObjects, err := a.FindAllObjectsToPrune(ctx, dynamicClient, visitedUids) if err != nil { return err } return a.deleteObjects(ctx, dynamicClient, allObjects, deleteOptions) } func (a *ApplySet) findObjectsToPrune(ctx context.Context, dynamicClient dynamic.Interface, visitedUids sets.Set[types.UID], namespace string, mapping *meta.RESTMapping) ([]PruneObject, error) { applysetLabelSelector := a.LabelSelectorForMembers() opt := metav1.ListOptions{ LabelSelector: applysetLabelSelector, } klog.V(2).Infof("listing objects for pruning; namespace=%q, resource=%v", namespace, mapping.Resource) objects, err := dynamicClient.Resource(mapping.Resource).Namespace(namespace).List(ctx, opt) if err != nil { return nil, err } var pruneObjects []PruneObject for i := range objects.Items { obj := &objects.Items[i] uid := obj.GetUID() if visitedUids.Has(uid) { continue } name := obj.GetName() pruneObjects = append(pruneObjects, PruneObject{ Name: name, Namespace: namespace, Mapping: mapping, Object: obj, }) } return pruneObjects, nil } func (a *ApplySet) deleteObjects(ctx context.Context, dynamicClient dynamic.Interface, pruneObjects []PruneObject, opt *ApplySetDeleteOptions) error { for i := range pruneObjects { pruneObject := &pruneObjects[i] name := pruneObject.Name namespace := pruneObject.Namespace mapping := pruneObject.Mapping if opt.DryRunStrategy != cmdutil.DryRunClient { if err := runDelete(ctx, namespace, name, mapping, dynamicClient, opt.CascadingStrategy, opt.GracePeriod, opt.DryRunStrategy == cmdutil.DryRunServer); err != nil { return fmt.Errorf("pruning %v: %w", pruneObject.String(), err) } } opt.Printer.PrintObj(pruneObject.Object, opt.IOStreams.Out) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/patcher.go000066400000000000000000000366231504711711200277230ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "context" "encoding/json" "fmt" "io" "time" "github.com/pkg/errors" "github.com/jonboulle/clockwork" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" 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" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/jsonmergepatch" "k8s.io/apimachinery/pkg/util/mergepatch" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/openapi3" "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/validation/spec" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/openapi" ) const ( // maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure maxPatchRetry = 5 // how many times we can retry before back off triesBeforeBackOff = 1 // groupVersionKindExtensionKey is the key used to lookup the // GroupVersionKind value for an object definition from the // definition's "extensions" map. groupVersionKindExtensionKey = "x-kubernetes-group-version-kind" ) // patchRetryBackOffPeriod is the period to back off when apply patch results in error. var patchRetryBackOffPeriod = 1 * time.Second var createPatchErrFormat = "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:" // Patcher defines options to patch OpenAPI objects. type Patcher struct { Mapping *meta.RESTMapping Helper *resource.Helper Overwrite bool BackOff clockwork.Clock Force bool CascadingStrategy metav1.DeletionPropagation Timeout time.Duration GracePeriod int // If set, forces the patch against a specific resourceVersion ResourceVersion *string // Number of retries to make if the patch fails with conflict Retries int OpenAPIGetter openapi.OpenAPIResourcesGetter OpenAPIV3Root openapi3.Root } func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) { var openAPIGetter openapi.OpenAPIResourcesGetter var openAPIV3Root openapi3.Root if o.OpenAPIPatch { openAPIGetter = o.OpenAPIGetter openAPIV3Root = o.OpenAPIV3Root } return &Patcher{ Mapping: info.Mapping, Helper: helper, Overwrite: o.Overwrite, BackOff: clockwork.NewRealClock(), Force: o.DeleteOptions.ForceDeletion, CascadingStrategy: o.DeleteOptions.CascadingStrategy, Timeout: o.DeleteOptions.Timeout, GracePeriod: o.DeleteOptions.GracePeriod, OpenAPIGetter: openAPIGetter, OpenAPIV3Root: openAPIV3Root, Retries: maxPatchRetry, }, nil } func (p *Patcher) delete(namespace, name string) error { options := asDeleteOptions(p.CascadingStrategy, p.GracePeriod) _, err := p.Helper.DeleteWithOptions(namespace, name, &options) return err } func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) { // Serialize the current configuration of the object from the server. current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) if err != nil { return nil, nil, errors.Wrapf(err, "serializing current configuration from:\n%v\nfor:", obj) } // Retrieve the original configuration of the object from the annotation. original, err := util.GetOriginalConfiguration(obj) if err != nil { return nil, nil, errors.Wrapf(err, "retrieving original configuration from:\n%v\nfor:", obj) } var patchType types.PatchType var patch []byte if p.OpenAPIV3Root != nil { gvkSupported, err := p.gvkSupportsPatchOpenAPIV3(p.Mapping.GroupVersionKind) if err != nil { // Realistically this error logging is not needed (not present in V2), // but would help us in debugging if users encounter a problem // with OpenAPI V3 not present in V2. klog.V(5).Infof("warning: OpenAPI V3 path does not exist - group: %s, version %s, kind %s\n", p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind) } else if gvkSupported { patch, err = p.buildStrategicMergePatchFromOpenAPIV3(original, modified, current) if err != nil { // Fall back to OpenAPI V2 if there is a problem // We should remove the fallback in the future, // but for the first release it might be beneficial // to fall back to OpenAPI V2 while logging the error // and seeing if we get any bug reports. fmt.Fprintf(errOut, "warning: error calculating patch from openapi v3 spec: %v\n", err) } else { patchType = types.StrategicMergePatchType } } else { klog.V(5).Infof("warning: OpenAPI V3 path does not support strategic merge patch - group: %s, version %s, kind %s\n", p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind) } } if patch == nil && p.OpenAPIGetter != nil { if openAPISchema, err := p.OpenAPIGetter.OpenAPISchema(); err == nil && openAPISchema != nil { // if openapischema is used, we'll try to get required patch type for this GVK from Open API. // if it fails or could not find any patch type, fall back to baked-in patch type determination. if patchType, err = p.getPatchTypeFromOpenAPI(openAPISchema, p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType { patch, err = p.buildStrategicMergeFromOpenAPI(openAPISchema, original, modified, current) if err != nil { // Warn user about problem and continue strategic merge patching using builtin types. fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err) } } } } if patch == nil { versionedObj, err := scheme.Scheme.New(p.Mapping.GroupVersionKind) if err == nil { patchType = types.StrategicMergePatchType patch, err = p.buildStrategicMergeFromBuiltins(versionedObj, original, modified, current) if err != nil { return nil, nil, errors.Wrapf(err, createPatchErrFormat, original, modified, current) } } else { if !runtime.IsNotRegisteredError(err) { return nil, nil, errors.Wrapf(err, "getting instance of versioned object for %v:", p.Mapping.GroupVersionKind) } patchType = types.MergePatchType patch, err = p.buildMergePatch(original, modified, current) if err != nil { return nil, nil, errors.Wrapf(err, createPatchErrFormat, original, modified, current) } } } if string(patch) == "{}" { return patch, obj, nil } if p.ResourceVersion != nil { patch, err = addResourceVersion(patch, *p.ResourceVersion) if err != nil { return nil, nil, errors.Wrap(err, "Failed to insert resourceVersion in patch") } } patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, nil) return patch, patchedObj, err } // buildMergePatch builds patch according to the JSONMergePatch which is used for // custom resource definitions. func (p *Patcher) buildMergePatch(original, modified, current []byte) ([]byte, error) { preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"), mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")} patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...) if err != nil { if mergepatch.IsPreconditionFailed(err) { return nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") } return nil, err } return patch, nil } // gvkSupportsPatchOpenAPIV3 checks if a particular GVK supports the patch operation. // It returns an error if the OpenAPI V3 could not be downloaded. func (p *Patcher) gvkSupportsPatchOpenAPIV3(gvk schema.GroupVersionKind) (bool, error) { gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{ Group: p.Mapping.GroupVersionKind.Group, Version: p.Mapping.GroupVersionKind.Version, }) if err != nil { return false, err } if gvSpec == nil || gvSpec.Paths == nil || gvSpec.Paths.Paths == nil { return false, fmt.Errorf("gvk group: %s, version: %s, kind: %s does not exist for OpenAPI V3", gvk.Group, gvk.Version, gvk.Kind) } for _, path := range gvSpec.Paths.Paths { if path.Patch != nil { if gvkMatchesSingle(p.Mapping.GroupVersionKind, path.Patch.Extensions) { if path.Patch.RequestBody == nil || path.Patch.RequestBody.Content == nil { // GVK exists but does not support requestBody. Indication of malformed OpenAPI. return false, nil } if _, ok := path.Patch.RequestBody.Content["application/strategic-merge-patch+json"]; ok { return true, nil } // GVK exists but strategic-merge-patch is not supported. Likely to be a CRD or aggregated resource. return false, nil } } } return false, nil } func gvkMatchesArray(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool { var gvkList []map[string]string err := ext.GetObject(groupVersionKindExtensionKey, &gvkList) if err != nil { return false } for _, gvkMap := range gvkList { if gvkMap["group"] == targetGVK.Group && gvkMap["version"] == targetGVK.Version && gvkMap["kind"] == targetGVK.Kind { return true } } return false } func gvkMatchesSingle(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool { var gvkMap map[string]string err := ext.GetObject(groupVersionKindExtensionKey, &gvkMap) if err != nil { return false } return gvkMap["group"] == targetGVK.Group && gvkMap["version"] == targetGVK.Version && gvkMap["kind"] == targetGVK.Kind } func (p *Patcher) buildStrategicMergePatchFromOpenAPIV3(original, modified, current []byte) ([]byte, error) { gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{ Group: p.Mapping.GroupVersionKind.Group, Version: p.Mapping.GroupVersionKind.Version, }) if err != nil { return nil, err } if gvSpec == nil || gvSpec.Components == nil { return nil, fmt.Errorf("OpenAPI V3 Components is nil") } for _, c := range gvSpec.Components.Schemas { if !gvkMatchesArray(p.Mapping.GroupVersionKind, c.Extensions) { continue } lookupPatchMeta := strategicpatch.PatchMetaFromOpenAPIV3{Schema: c, SchemaList: gvSpec.Components.Schemas} if openapiv3Patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil { return nil, err } else { return openapiv3Patch, nil } } return nil, nil } // buildStrategicMergeFromOpenAPI builds patch from OpenAPI if it is enabled. // This is used for core types which is published in openapi. func (p *Patcher) buildStrategicMergeFromOpenAPI(openAPISchema openapi.Resources, original, modified, current []byte) ([]byte, error) { schema := openAPISchema.LookupResource(p.Mapping.GroupVersionKind) if schema == nil { // Missing schema returns nil patch; also no error. return nil, nil } lookupPatchMeta := strategicpatch.PatchMetaFromOpenAPI{Schema: schema} if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil { return nil, err } else { return openapiPatch, nil } } // getPatchTypeFromOpenAPI looks up patch types supported by given GroupVersionKind in Open API. func (p *Patcher) getPatchTypeFromOpenAPI(openAPISchema openapi.Resources, gvk schema.GroupVersionKind) (types.PatchType, error) { if pc := openAPISchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil { for _, c := range pc { if c == string(types.StrategicMergePatchType) { return types.StrategicMergePatchType, nil } } return types.MergePatchType, nil } return types.MergePatchType, fmt.Errorf("unable to find any patch type for %s in Open API", gvk) } // buildStrategicMergeFromStruct builds patch from struct. This is used when // openapi endpoint is not working or user disables it by setting openapi-patch flag // to false. func (p *Patcher) buildStrategicMergeFromBuiltins(versionedObj runtime.Object, original, modified, current []byte) ([]byte, error) { lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj) if err != nil { return nil, err } patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite) if err != nil { return nil, err } return patch, nil } // Patch tries to patch an OpenAPI resource. On success, returns the merge patch as well // the final patched object. On failure, returns an error. func (p *Patcher) Patch(current runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) { var getErr error patchBytes, patchObject, err := p.patchSimple(current, modified, namespace, name, errOut) if p.Retries == 0 { p.Retries = maxPatchRetry } for i := 1; i <= p.Retries && apierrors.IsConflict(err); i++ { if i > triesBeforeBackOff { p.BackOff.Sleep(patchRetryBackOffPeriod) } current, getErr = p.Helper.Get(namespace, name) if getErr != nil { return nil, nil, getErr } patchBytes, patchObject, err = p.patchSimple(current, modified, namespace, name, errOut) } if err != nil { if (apierrors.IsConflict(err) || apierrors.IsInvalid(err)) && p.Force { patchBytes, patchObject, err = p.deleteAndCreate(current, modified, namespace, name) } else { err = cmdutil.AddSourceToErr("patching", source, err) } } return patchBytes, patchObject, err } func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, namespace, name string) ([]byte, runtime.Object, error) { if err := p.delete(namespace, name); err != nil { return modified, nil, err } // TODO: use wait if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, p.Timeout, true, func(ctx context.Context) (bool, error) { if _, err := p.Helper.Get(namespace, name); !apierrors.IsNotFound(err) { return false, err } return true, nil }); err != nil { return modified, nil, err } versionedObject, _, err := unstructured.UnstructuredJSONScheme.Decode(modified, nil, nil) if err != nil { return modified, nil, err } createdObject, err := p.Helper.Create(namespace, true, versionedObject) if err != nil { // restore the original object if we fail to create the new one // but still propagate and advertise error to user recreated, recreateErr := p.Helper.Create(namespace, true, original) if recreateErr != nil { err = fmt.Errorf("An error occurred force-replacing the existing object with the newly provided one:\n\n%v.\n\nAdditionally, an error occurred attempting to restore the original object:\n\n%v", err, recreateErr) } else { createdObject = recreated } } return modified, createdObject, err } func addResourceVersion(patch []byte, rv string) ([]byte, error) { var patchMap map[string]interface{} err := json.Unmarshal(patch, &patchMap) if err != nil { return nil, err } u := unstructured.Unstructured{Object: patchMap} a, err := meta.Accessor(&u) if err != nil { return nil, err } a.SetResourceVersion(rv) return json.Marshal(patchMap) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/prune.go000066400000000000000000000105571504711711200274240ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apply import ( "context" "fmt" "io" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/dynamic" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/prune" ) type pruner struct { mapper meta.RESTMapper dynamicClient dynamic.Interface visitedUids sets.Set[types.UID] visitedNamespaces sets.Set[string] labelSelector string fieldSelector string cascadingStrategy metav1.DeletionPropagation dryRunStrategy cmdutil.DryRunStrategy gracePeriod int toPrinter func(string) (printers.ResourcePrinter, error) out io.Writer } func newPruner(o *ApplyOptions) pruner { return pruner{ mapper: o.Mapper, dynamicClient: o.DynamicClient, labelSelector: o.Selector, visitedUids: o.VisitedUids, visitedNamespaces: o.VisitedNamespaces, cascadingStrategy: o.DeleteOptions.CascadingStrategy, dryRunStrategy: o.DryRunStrategy, gracePeriod: o.DeleteOptions.GracePeriod, toPrinter: o.ToPrinter, out: o.Out, } } func (p *pruner) pruneAll(o *ApplyOptions) error { namespacedRESTMappings, nonNamespacedRESTMappings, err := prune.GetRESTMappings(o.Mapper, o.PruneResources, o.Namespace != "") if err != nil { return fmt.Errorf("error retrieving RESTMappings to prune: %v", err) } for n := range p.visitedNamespaces { for _, m := range namespacedRESTMappings { if err := p.prune(n, m); err != nil { return fmt.Errorf("error pruning namespaced object %v: %v", m.GroupVersionKind, err) } } } for _, m := range nonNamespacedRESTMappings { if err := p.prune(metav1.NamespaceNone, m); err != nil { return fmt.Errorf("error pruning nonNamespaced object %v: %v", m.GroupVersionKind, err) } } return nil } func (p *pruner) prune(namespace string, mapping *meta.RESTMapping) error { objList, err := p.dynamicClient.Resource(mapping.Resource). Namespace(namespace). List(context.TODO(), metav1.ListOptions{ LabelSelector: p.labelSelector, FieldSelector: p.fieldSelector, }) if err != nil { return err } objs, err := meta.ExtractList(objList) if err != nil { return err } for _, obj := range objs { metadata, err := meta.Accessor(obj) if err != nil { return err } annots := metadata.GetAnnotations() if _, ok := annots[corev1.LastAppliedConfigAnnotation]; !ok { // don't prune resources not created with apply continue } uid := metadata.GetUID() if p.visitedUids.Has(uid) { continue } name := metadata.GetName() if p.dryRunStrategy != cmdutil.DryRunClient { if err := p.delete(namespace, name, mapping); err != nil { return err } } printer, err := p.toPrinter("pruned") if err != nil { return err } printer.PrintObj(obj, p.out) } return nil } func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping) error { ctx := context.TODO() return runDelete(ctx, namespace, name, mapping, p.dynamicClient, p.cascadingStrategy, p.gracePeriod, p.dryRunStrategy == cmdutil.DryRunServer) } func runDelete(ctx context.Context, namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascadingStrategy metav1.DeletionPropagation, gracePeriod int, serverDryRun bool) error { options := asDeleteOptions(cascadingStrategy, gracePeriod) if serverDryRun { options.DryRun = []string{metav1.DryRunAll} } return c.Resource(mapping.Resource).Namespace(namespace).Delete(ctx, name, options) } func asDeleteOptions(cascadingStrategy metav1.DeletionPropagation, gracePeriod int) metav1.DeleteOptions { options := metav1.DeleteOptions{} if gracePeriod >= 0 { options = *metav1.NewDeleteOptions(int64(gracePeriod)) } options.PropagationPolicy = &cascadingStrategy return options } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/000077500000000000000000000000001504711711200275455ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/000077500000000000000000000000001504711711200306765ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simple/000077500000000000000000000000001504711711200321675ustar00rootroot00000000000000manifest1-expected-apply.txt000066400000000000000000000000541504711711200374610ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simplenamespace/foo created namespace/bar created manifest1-expected-getobjects.yaml000066400000000000000000000004741504711711200406160ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simpleapiVersion: v1 kind: Namespace metadata: labels: applyset.kubernetes.io/part-of: applyset-bjd1LnyQq0mtUu-riZCqjDQOmh0iNb9O2RcuT12WR0k-v1 name: foo --- apiVersion: v1 kind: Namespace metadata: labels: applyset.kubernetes.io/part-of: applyset-bjd1LnyQq0mtUu-riZCqjDQOmh0iNb9O2RcuT12WR0k-v1 name: bar manifest1.yaml000066400000000000000000000001601504711711200346600ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simpleapiVersion: v1 kind: Namespace metadata: name: foo --- apiVersion: v1 kind: Namespace metadata: name: bar manifest2-expected-apply.txt000066400000000000000000000000551504711711200374630ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simplenamespace/foo unchanged namespace/bar pruned manifest2-expected-getobjects.yaml000066400000000000000000000002331504711711200406100ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simpleapiVersion: v1 kind: Namespace metadata: labels: applyset.kubernetes.io/part-of: applyset-bjd1LnyQq0mtUu-riZCqjDQOmh0iNb9O2RcuT12WR0k-v1 name: foo manifest2.yaml000066400000000000000000000000651504711711200346650ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simpleapiVersion: v1 kind: Namespace metadata: name: foo scenarios/000077500000000000000000000000001504711711200340765ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simpleerror-on-apply/000077500000000000000000000000001504711711200367645ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simple/scenariosmanifest1-expected-apply.txt000066400000000000000000000000541504711711200443350ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simple/scenarios/error-on-applynamespace/foo created namespace/bar created manifest2-expected-apply.txt000066400000000000000000000001741504711711200443410ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simple/scenarios/error-on-applynamespace/foo unchanged error: pruning Namespace bar: an error on the server ("") has prevented the request from succeeding kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/attach/000077500000000000000000000000001504711711200260535ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/attach/attach.go000066400000000000000000000271451504711711200276570ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package attach import ( "context" "fmt" "io" "net/url" "strings" "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/httpstream" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" "k8s.io/kubectl/pkg/cmd/exec" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/podcmd" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( attachExample = templates.Examples(i18n.T(` # Get output from running pod mypod; use the 'kubectl.kubernetes.io/default-container' annotation # for selecting the container to be attached or the first container in the pod will be chosen kubectl attach mypod # Get output from ruby-container from pod mypod kubectl attach mypod -c ruby-container # Switch to raw terminal mode; sends stdin to 'bash' in ruby-container from pod mypod # and sends stdout/stderr from 'bash' back to the client kubectl attach mypod -c ruby-container -i -t # Get output from the first pod of a replica set named nginx kubectl attach rs/nginx `)) ) const ( defaultPodAttachTimeout = 60 * time.Second defaultPodLogsTimeout = 20 * time.Second ) // AttachOptions declare the arguments accepted by the Attach command type AttachOptions struct { exec.StreamOptions // whether to disable use of standard error when streaming output from tty DisableStderr bool CommandName string Pod *corev1.Pod AttachFunc func(*AttachOptions, *corev1.Container, bool, remotecommand.TerminalSizeQueue) func() error Resources []string Builder func() *resource.Builder AttachablePodFn polymorphichelpers.AttachablePodForObjectFunc restClientGetter genericclioptions.RESTClientGetter Attach RemoteAttach GetPodTimeout time.Duration Config *restclient.Config } // NewAttachOptions creates the options for attach func NewAttachOptions(streams genericiooptions.IOStreams) *AttachOptions { return &AttachOptions{ StreamOptions: exec.StreamOptions{ IOStreams: streams, }, Attach: &DefaultRemoteAttach{}, AttachFunc: DefaultAttachFunc, } } // NewCmdAttach returns the attach Cobra command func NewCmdAttach(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewAttachOptions(streams) cmd := &cobra.Command{ Use: "attach (POD | TYPE/NAME) -c CONTAINER", DisableFlagsInUseLine: true, Short: i18n.T("Attach to a running container"), Long: i18n.T("Attach to a process that is already running inside an existing container."), Example: attachExample, ValidArgsFunction: completion.PodResourceNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout) cmdutil.AddContainerVarFlags(cmd, &o.ContainerName, o.ContainerName) cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", o.Stdin, "Pass stdin to the container") cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, "Stdin is a TTY") cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "Only print output from the remote session") return cmd } // RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing type RemoteAttach interface { Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error } // DefaultAttachFunc is the default AttachFunc used func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error { return func() error { restClient, err := restclient.RESTClientFor(o.Config) if err != nil { return err } req := restClient.Post(). Resource("pods"). Name(o.Pod.Name). Namespace(o.Pod.Namespace). SubResource("attach") req.VersionedParams(&corev1.PodAttachOptions{ Container: containerToAttach.Name, Stdin: o.Stdin, Stdout: o.Out != nil, Stderr: !o.DisableStderr, TTY: raw, }, scheme.ParameterCodec) return o.Attach.Attach(req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue) } } // DefaultRemoteAttach is the standard implementation of attaching type DefaultRemoteAttach struct{} // Attach executes attach to a running container func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { exec, err := createExecutor(url, config) if err != nil { return err } return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{ Stdin: stdin, Stdout: stdout, Stderr: stderr, Tty: tty, TerminalSizeQueue: terminalSizeQueue, }) } // createExecutor returns the Executor or an error if one occurred. func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) { exec, err := remotecommand.NewSPDYExecutor(config, "POST", url) if err != nil { return nil, err } // Fallback executor is default, unless feature flag is explicitly disabled. if !cmdutil.RemoteCommandWebsockets.IsDisabled() { // WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17). websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String()) if err != nil { return nil, err } exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, func(err error) bool { return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err) }) if err != nil { return nil, err } } return exec, nil } // Complete verifies command line arguments and loads data from the command environment func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.AttachablePodFn = polymorphichelpers.AttachablePodForObjectFn o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) if err != nil { return cmdutil.UsageErrorf(cmd, "%s", err.Error()) } o.Builder = f.NewBuilder o.Resources = args o.restClientGetter = f config, err := f.ToRESTConfig() if err != nil { return err } o.Config = config if o.CommandName == "" { o.CommandName = cmd.CommandPath() } return nil } // Validate checks that the provided attach options are specified. func (o *AttachOptions) Validate() error { if len(o.Resources) == 0 { return fmt.Errorf("at least 1 argument is required for attach") } if len(o.Resources) > 2 { return fmt.Errorf("expected POD, TYPE/NAME, or TYPE NAME, (at most 2 arguments) saw %d: %v", len(o.Resources), o.Resources) } if o.GetPodTimeout <= 0 { return fmt.Errorf("--pod-running-timeout must be higher than zero") } return nil } // Run executes a validated remote execution against a pod. func (o *AttachOptions) Run() error { if o.Pod == nil { b := o.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace() switch len(o.Resources) { case 1: b.ResourceNames("pods", o.Resources[0]) case 2: b.ResourceNames(o.Resources[0], o.Resources[1]) } obj, err := b.Do().Object() if err != nil { return err } o.Pod, err = o.findAttachablePod(obj) if err != nil { return err } if o.Pod.Status.Phase == corev1.PodSucceeded || o.Pod.Status.Phase == corev1.PodFailed { return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", o.Pod.Status.Phase) } // TODO: convert this to a clean "wait" behavior } // check for TTY containerToAttach, err := o.containerToAttachTo(o.Pod) if err != nil { return fmt.Errorf("cannot attach to the container: %v", err) } if o.TTY && !containerToAttach.TTY { o.TTY = false if !o.Quiet && o.ErrOut != nil { fmt.Fprintf(o.ErrOut, "error: Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name) } } else if !o.TTY && containerToAttach.TTY { // the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get // an error "Unrecognized input header" o.TTY = true } // ensure we can recover the terminal while attached t := o.SetupTTY() var sizeQueue remotecommand.TerminalSizeQueue if t.Raw { if size := t.GetSize(); size != nil { // fake resizing +1 and then back to normal so that attach-detach-reattach will result in the // screen being redrawn sizePlusOne := *size sizePlusOne.Width++ sizePlusOne.Height++ // this call spawns a goroutine to monitor/update the terminal size sizeQueue = t.MonitorSize(&sizePlusOne, size) } o.DisableStderr = true } if !o.Quiet { fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.") } if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil { return err } if msg := o.reattachMessage(containerToAttach.Name, t.Raw); msg != "" { fmt.Fprintln(o.Out, msg) } return nil } func (o *AttachOptions) findAttachablePod(obj runtime.Object) (*corev1.Pod, error) { attachablePod, err := o.AttachablePodFn(o.restClientGetter, obj, o.GetPodTimeout) if err != nil { return nil, err } o.StreamOptions.PodName = attachablePod.Name return attachablePod, nil } // containerToAttach returns a reference to the container to attach to, given by name. // use the kubectl.kubernetes.io/default-container annotation for selecting the container to be attached // or the first container in the pod will be chosen If name is empty. func (o *AttachOptions) containerToAttachTo(pod *corev1.Pod) (*corev1.Container, error) { return podcmd.FindOrDefaultContainerByName(pod, o.ContainerName, o.Quiet, o.ErrOut) } // GetContainerName returns the name of the container to attach to, with a fallback. func (o *AttachOptions) GetContainerName(pod *corev1.Pod) (string, error) { c, err := o.containerToAttachTo(pod) if err != nil { return "", err } return c.Name, nil } // reattachMessage returns a message to print after attach has completed, or // the empty string if no message should be printed. func (o *AttachOptions) reattachMessage(containerName string, rawTTY bool) string { if o.Quiet || !o.Stdin || !rawTTY || o.Pod.Spec.RestartPolicy != corev1.RestartPolicyAlways { return "" } if _, path := podcmd.FindContainerByName(o.Pod, containerName); strings.HasPrefix(path, "spec.ephemeralContainers") { return fmt.Sprintf("Session ended, the ephemeral container will not be restarted but may be reattached using '%s %s -c %s -i -t' if it is still running", o.CommandName, o.Pod.Name, containerName) } return fmt.Sprintf("Session ended, resume using '%s %s -c %s -i -t' command when the pod is running", o.CommandName, o.Pod.Name, containerName) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/attach/attach_test.go000066400000000000000000000462641504711711200307210ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package attach import ( "bytes" "fmt" "io" "net/http" "net/url" "strings" "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" "k8s.io/client-go/tools/remotecommand" "k8s.io/kubectl/pkg/cmd/exec" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/podcmd" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" ) type fakeRemoteAttach struct { url *url.URL err error } func (f *fakeRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { f.url = url return f.err } func fakeAttachablePodFn(pod *corev1.Pod) polymorphichelpers.AttachablePodForObjectFunc { return func(getter genericclioptions.RESTClientGetter, obj runtime.Object, timeout time.Duration) (*corev1.Pod, error) { return pod, nil } } func TestPodAndContainerAttach(t *testing.T) { tests := []struct { name string args []string options *AttachOptions expectError string expectedPodName string expectedContainerName string expectOut string obj *corev1.Pod }{ { name: "empty", options: &AttachOptions{GetPodTimeout: 1}, expectError: "at least 1 argument is required", }, { name: "too many args", options: &AttachOptions{GetPodTimeout: 2}, args: []string{"one", "two", "three"}, expectError: "at most 2 arguments", }, { name: "no container, no flags", options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout}, args: []string{"foo"}, expectedPodName: "foo", expectedContainerName: "bar", obj: attachPod(), expectOut: `Defaulted container "bar" out of: bar, debugger (ephem), initfoo (init)`, }, { name: "no container, no flags, sets default expected container as annotation", options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout}, args: []string{"foo"}, expectedPodName: "foo", expectedContainerName: "bar", obj: setDefaultContainer(attachPod(), "initfoo"), expectOut: ``, }, { name: "no container, no flags, sets default missing container as annotation", options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout}, args: []string{"foo"}, expectedPodName: "foo", expectedContainerName: "bar", obj: setDefaultContainer(attachPod(), "does-not-exist"), expectOut: `Defaulted container "bar" out of: bar, debugger (ephem), initfoo (init)`, }, { name: "container in flag", options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "bar"}, GetPodTimeout: 10000000}, args: []string{"foo"}, expectedPodName: "foo", expectedContainerName: "bar", obj: attachPod(), }, { name: "init container in flag", options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "initfoo"}, GetPodTimeout: 30}, args: []string{"foo"}, expectedPodName: "foo", expectedContainerName: "initfoo", obj: attachPod(), }, { name: "ephemeral container in flag", options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "debugger"}, GetPodTimeout: 30}, args: []string{"foo"}, expectedPodName: "foo", expectedContainerName: "debugger", obj: attachPod(), }, { name: "non-existing container", options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "wrong"}, GetPodTimeout: 10}, args: []string{"foo"}, expectedPodName: "foo", expectError: "container wrong not found in pod foo", obj: attachPod(), }, { name: "no container, no flags, pods and name", options: &AttachOptions{GetPodTimeout: 10000}, args: []string{"pods", "foo"}, expectedPodName: "foo", expectedContainerName: "bar", obj: attachPod(), }, { name: "invalid get pod timeout value", options: &AttachOptions{GetPodTimeout: 0}, args: []string{"pod/foo"}, expectedPodName: "foo", expectedContainerName: "bar", obj: attachPod(), expectError: "must be higher than zero", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // setup opts to fetch our test pod test.options.AttachablePodFn = fakeAttachablePodFn(test.obj) test.options.Resources = test.args if err := test.options.Validate(); err != nil { if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) { t.Errorf("unexpected error: expected %q, got %q", test.expectError, err) } return } pod, err := test.options.findAttachablePod(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "test"}, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "initfoo", }, }, Containers: []corev1.Container{ { Name: "foobar", }, }, EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemfoo", }, }, }, }, }) if err != nil { if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) { t.Errorf("unexpected error: expected %q, got %q", err, test.expectError) } return } if pod.Name != test.expectedPodName { t.Errorf("unexpected pod name: expected %q, got %q", test.expectedContainerName, pod.Name) } var buf bytes.Buffer test.options.ErrOut = &buf container, err := test.options.containerToAttachTo(attachPod()) if len(test.expectOut) > 0 && !strings.Contains(buf.String(), test.expectOut) { t.Errorf("unexpected output: output did not contain %q\n---\n%s", test.expectOut, buf.String()) } if err != nil { if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) { t.Errorf("unexpected error: expected %q, got %q", test.expectError, err) } return } if container.Name != test.expectedContainerName { t.Errorf("unexpected container name: expected %q, got %q", test.expectedContainerName, container.Name) } if test.options.PodName != test.expectedPodName { t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPodName, test.options.PodName) } if len(test.expectError) > 0 { t.Fatalf("expected error %q, but saw none", test.expectError) } }) } } func TestAttach(t *testing.T) { version := "v1" tests := []struct { name, version, podPath, fetchPodPath, attachPath, container string pod *corev1.Pod remoteAttachErr bool expectedErr string }{ { name: "pod attach", version: version, podPath: "/api/" + version + "/namespaces/test/pods/foo", fetchPodPath: "/namespaces/test/pods/foo", attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach", pod: attachPod(), container: "bar", }, { name: "pod attach error", version: version, podPath: "/api/" + version + "/namespaces/test/pods/foo", fetchPodPath: "/namespaces/test/pods/foo", attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach", pod: attachPod(), remoteAttachErr: true, container: "bar", expectedErr: "attach error", }, { name: "container not found error", version: version, podPath: "/api/" + version + "/namespaces/test/pods/foo", fetchPodPath: "/namespaces/test/pods/foo", attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach", pod: attachPod(), container: "foo", expectedErr: "cannot attach to the container: container foo not found in pod foo", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == test.podPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == test.fetchPodPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}} remoteAttach := &fakeRemoteAttach{} if test.remoteAttachErr { remoteAttach.err = fmt.Errorf("attach error") } options := &AttachOptions{ StreamOptions: exec.StreamOptions{ ContainerName: test.container, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), }, Attach: remoteAttach, GetPodTimeout: 1000, } options.restClientGetter = tf options.Namespace = "test" options.Resources = []string{"foo"} options.Builder = tf.NewBuilder options.AttachablePodFn = fakeAttachablePodFn(test.pod) options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error { return func() error { u, err := url.Parse(fmt.Sprintf("%s?container=%s", test.attachPath, containerToAttach.Name)) if err != nil { return err } return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue) } } err := options.Run() if test.expectedErr != "" && err.Error() != test.expectedErr { t.Errorf("%s: Unexpected exec error: %v", test.name, err) return } if test.expectedErr == "" && err != nil { t.Errorf("%s: Unexpected error: %v", test.name, err) return } if test.expectedErr != "" { return } if remoteAttach.url.Path != test.attachPath { t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path) return } if remoteAttach.url.Query().Get("container") != "bar" { t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query()) } }) } } func TestAttachWarnings(t *testing.T) { version := "v1" tests := []struct { name, container, version, podPath, fetchPodPath, expectedErr string pod *corev1.Pod stdin, tty bool }{ { name: "fallback tty if not supported", version: version, podPath: "/api/" + version + "/namespaces/test/pods/foo", fetchPodPath: "/namespaces/test/pods/foo", pod: attachPod(), stdin: true, tty: true, expectedErr: "Unable to use a TTY - container bar did not allocate one", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() streams, _, _, bufErr := genericiooptions.NewTestIOStreams() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == test.podPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == test.fetchPodPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}} options := &AttachOptions{ StreamOptions: exec.StreamOptions{ Stdin: test.stdin, TTY: test.tty, ContainerName: test.container, IOStreams: streams, }, Attach: &fakeRemoteAttach{}, GetPodTimeout: 1000, } options.restClientGetter = tf options.Namespace = "test" options.Resources = []string{"foo"} options.Builder = tf.NewBuilder options.AttachablePodFn = fakeAttachablePodFn(test.pod) options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error { return func() error { u, err := url.Parse("http://foo.bar") if err != nil { return err } return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue) } } if err := options.Run(); err != nil { t.Fatal(err) } if test.stdin && test.tty { if !test.pod.Spec.Containers[0].TTY { if !strings.Contains(bufErr.String(), test.expectedErr) { t.Errorf("%s: Expected TTY fallback warning for attach request: %s", test.name, bufErr.String()) return } } } }) } } func attachPod() *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, Containers: []corev1.Container{ { Name: "bar", }, }, InitContainers: []corev1.Container{ { Name: "initfoo", }, }, EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", }, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, } } func setDefaultContainer(pod *corev1.Pod, name string) *corev1.Pod { if pod.Annotations == nil { pod.Annotations = make(map[string]string) } pod.Annotations[podcmd.DefaultContainerAnnotationName] = name return pod } func TestReattachMessage(t *testing.T) { tests := []struct { name string pod *corev1.Pod rawTTY, stdin bool container string expected string }{ { name: "normal interactive session", pod: attachPod(), container: "bar", rawTTY: true, stdin: true, expected: "Session ended, resume using", }, { name: "no stdin", pod: attachPod(), container: "bar", rawTTY: true, stdin: false, expected: "", }, { name: "not connected to a real TTY", pod: attachPod(), container: "bar", rawTTY: false, stdin: true, expected: "", }, { name: "no restarts", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, Containers: []corev1.Container{{Name: "bar"}}, }, Status: corev1.PodStatus{Phase: corev1.PodRunning}, }, container: "bar", rawTTY: true, stdin: true, expected: "", }, { name: "ephemeral container", pod: attachPod(), container: "debugger", rawTTY: true, stdin: true, expected: "Session ended, the ephemeral container will not be restarted", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { options := &AttachOptions{ StreamOptions: exec.StreamOptions{ Stdin: test.stdin, }, Pod: test.pod, } if msg := options.reattachMessage(test.container, test.rawTTY); test.expected == "" && msg != "" { t.Errorf("reattachMessage(%v, %v) = %q, want empty string", test.container, test.rawTTY, msg) } else if !strings.Contains(msg, test.expected) { t.Errorf("reattachMessage(%v, %v) = %q, want string containing %q", test.container, test.rawTTY, msg, test.expected) } }) } } func TestCreateExecutor(t *testing.T) { url, err := url.Parse("http://localhost:8080/index.html") if err != nil { t.Fatalf("unable to parse test url: %v", err) } config := cmdtesting.DefaultClientConfig() // First, ensure that no environment variable creates the fallback executor. executor, err := createExecutor(url, config) if err != nil { t.Fatalf("unable to create executor: %v", err) } if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback { t.Errorf("expected fallback executor, got %#v", executor) } // Next, check turning on feature flag explicitly also creates fallback executor. t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true") executor, err = createExecutor(url, config) if err != nil { t.Fatalf("unable to create executor: %v", err) } if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback { t.Errorf("expected fallback executor, got %#v", executor) } // Finally, check explicit disabling does NOT create the fallback executor. t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false") executor, err = createExecutor(url, config) if err != nil { t.Fatalf("unable to create executor: %v", err) } if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback { t.Errorf("expected fallback executor, got %#v", executor) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/000077500000000000000000000000001504711711200255505ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/OWNERS000066400000000000000000000003601504711711200265070ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-auth-authenticators-approvers - sig-auth-authorizers-approvers reviewers: - sig-auth-authenticators-reviewers - sig-auth-authorizers-reviewers labels: - sig/auth kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/auth.go000066400000000000000000000023321504711711200270400ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) // NewCmdAuth returns an initialized Command instance for 'auth' sub command func NewCmdAuth(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { // Parent command to which all subcommands are added. cmds := &cobra.Command{ Use: "auth", Short: "Inspect authorization", Long: `Inspect authorization.`, Run: cmdutil.DefaultSubCommandRun(streams.ErrOut), } cmds.AddCommand(NewCmdCanI(f, streams)) cmds.AddCommand(NewCmdReconcile(f, streams)) cmds.AddCommand(NewCmdWhoAmI(f, streams)) return cmds } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani.go000066400000000000000000000331601504711711200270140ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "errors" "fmt" "io" "os" "sort" "strings" "github.com/spf13/cobra" authorizationv1 "k8s.io/api/authorization/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" discovery "k8s.io/client-go/discovery" authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/describe" rbacutil "k8s.io/kubectl/pkg/util/rbac" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" ) // CanIOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type CanIOptions struct { AllNamespaces bool Quiet bool NoHeaders bool Namespace string AuthClient authorizationv1client.AuthorizationV1Interface DiscoveryClient discovery.DiscoveryInterface Verb string Resource schema.GroupVersionResource NonResourceURL string Subresource string ResourceName string List bool genericiooptions.IOStreams WarningPrinter *printers.WarningPrinter } var ( canILong = templates.LongDesc(` Check whether an action is allowed. VERB is a logical Kubernetes API verb like 'get', 'list', 'watch', 'delete', etc. TYPE is a Kubernetes resource. Shortcuts and groups will be resolved. NONRESOURCEURL is a partial URL that starts with "/". NAME is the name of a particular Kubernetes resource. This command pairs nicely with impersonation. See --as global flag.`) canIExample = templates.Examples(` # Check to see if I can create pods in any namespace kubectl auth can-i create pods --all-namespaces # Check to see if I can list deployments in my current namespace kubectl auth can-i list deployments.apps # Check to see if service account "foo" of namespace "dev" can list pods in the namespace "prod" # You must be allowed to use impersonation for the global option "--as" kubectl auth can-i list pods --as=system:serviceaccount:dev:foo -n prod # Check to see if I can do everything in my current namespace ("*" means all) kubectl auth can-i '*' '*' # Check to see if I can get the job named "bar" in namespace "foo" kubectl auth can-i list jobs.batch/bar -n foo # Check to see if I can read pod logs kubectl auth can-i get pods --subresource=log # Check to see if I can access the URL /logs/ kubectl auth can-i get /logs/ # Check to see if I can approve certificates.k8s.io kubectl auth can-i approve certificates.k8s.io # List all allowed actions in namespace "foo" kubectl auth can-i --list --namespace=foo`) resourceVerbs = sets.New[string]("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "use", "bind", "impersonate", "*", "approve", "sign", "escalate", "attest") nonResourceURLVerbs = sets.New[string]("get", "put", "post", "head", "options", "delete", "patch", "*") // holds all the server-supported resources that cannot be discovered by clients. i.e. users and groups for the impersonate verb nonStandardResourceNames = sets.New[string]("users", "groups") ) // NewCmdCanI returns an initialized Command for 'auth can-i' sub command func NewCmdCanI(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := &CanIOptions{ IOStreams: streams, } cmd := &cobra.Command{ Use: "can-i VERB [TYPE | TYPE/NAME | NONRESOURCEURL]", DisableFlagsInUseLine: true, Short: "Check whether an action is allowed", Long: canILong, Example: canIExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, args)) cmdutil.CheckErr(o.Validate()) var err error if o.List { err = o.RunAccessList() } else { var allowed bool allowed, err = o.RunAccessCheck() if err == nil { if !allowed { os.Exit(1) } } } cmdutil.CheckErr(err) }, } cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If true, check the specified action in all namespaces.") cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "If true, suppress output and just return the exit code.") cmd.Flags().StringVar(&o.Subresource, "subresource", o.Subresource, "SubResource such as pod/log or deployment/scale") cmd.Flags().BoolVar(&o.List, "list", o.List, "If true, prints all allowed actions.") cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If true, prints allowed actions without headers") return cmd } // Complete completes all the required options func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error { // Set default WarningPrinter if not already set. if o.WarningPrinter == nil { o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) } if o.List { if len(args) != 0 { return errors.New("list option must be specified with no arguments") } } else { if o.Quiet { o.Out = io.Discard } switch len(args) { case 2: o.Verb = args[0] if strings.HasPrefix(args[1], "/") { o.NonResourceURL = args[1] break } resourceTokens := strings.SplitN(args[1], "/", 2) restMapper, err := f.ToRESTMapper() if err != nil { return err } o.Resource = o.resourceFor(restMapper, resourceTokens[0]) if len(resourceTokens) > 1 { o.ResourceName = resourceTokens[1] } default: errString := "you must specify two arguments: verb resource or verb resource/resourceName." usageString := "See 'kubectl auth can-i -h' for help and examples." return fmt.Errorf("%s\n%s", errString, usageString) } } var err error client, err := f.KubernetesClientSet() if err != nil { return err } o.AuthClient = client.AuthorizationV1() o.DiscoveryClient = client.Discovery() o.Namespace = "" if !o.AllNamespaces { o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } } return nil } // Validate makes sure provided values for CanIOptions are valid func (o *CanIOptions) Validate() error { if o.List { if o.Quiet || o.AllNamespaces || o.Subresource != "" { return errors.New("list option can't be specified with neither quiet, all-namespaces nor subresource options") } return nil } if o.WarningPrinter == nil { return fmt.Errorf("WarningPrinter can not be used without initialization") } if o.NonResourceURL != "" { if o.Subresource != "" { return fmt.Errorf("--subresource can not be used with NonResourceURL") } if o.Resource != (schema.GroupVersionResource{}) || o.ResourceName != "" { return fmt.Errorf("NonResourceURL and ResourceName can not specified together") } if !isKnownNonResourceVerb(o.Verb) { o.WarningPrinter.Print(fmt.Sprintf("verb '%s' is not a known verb\n", o.Verb)) } } else if !o.Resource.Empty() && !o.AllNamespaces && o.DiscoveryClient != nil { if namespaced, err := isNamespaced(o.Resource, o.DiscoveryClient); err == nil && !namespaced { if len(o.Resource.Group) == 0 { o.WarningPrinter.Print(fmt.Sprintf("resource '%s' is not namespace scoped\n", o.Resource.Resource)) } else { o.WarningPrinter.Print(fmt.Sprintf("resource '%s' is not namespace scoped in group '%s'\n", o.Resource.Resource, o.Resource.Group)) } } if !isKnownResourceVerb(o.Verb) { o.WarningPrinter.Print(fmt.Sprintf("verb '%s' is not a known verb\n", o.Verb)) } } if o.NoHeaders { return fmt.Errorf("--no-headers cannot be set without --list specified") } return nil } // RunAccessList lists all the access current user has func (o *CanIOptions) RunAccessList() error { sar := &authorizationv1.SelfSubjectRulesReview{ Spec: authorizationv1.SelfSubjectRulesReviewSpec{ Namespace: o.Namespace, }, } response, err := o.AuthClient.SelfSubjectRulesReviews().Create(context.TODO(), sar, metav1.CreateOptions{}) if err != nil { return err } return o.printStatus(response.Status) } // RunAccessCheck checks if user has access to a certain resource or non resource URL func (o *CanIOptions) RunAccessCheck() (bool, error) { var sar *authorizationv1.SelfSubjectAccessReview if o.NonResourceURL == "" { sar = &authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ ResourceAttributes: &authorizationv1.ResourceAttributes{ Namespace: o.Namespace, Verb: o.Verb, Group: o.Resource.Group, Resource: o.Resource.Resource, Subresource: o.Subresource, Name: o.ResourceName, }, }, } } else { sar = &authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ NonResourceAttributes: &authorizationv1.NonResourceAttributes{ Verb: o.Verb, Path: o.NonResourceURL, }, }, } } response, err := o.AuthClient.SelfSubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{}) if err != nil { return false, err } if response.Status.Allowed { fmt.Fprintln(o.Out, "yes") } else { fmt.Fprint(o.Out, "no") if len(response.Status.Reason) > 0 { fmt.Fprintf(o.Out, " - %v", response.Status.Reason) } if len(response.Status.EvaluationError) > 0 { fmt.Fprintf(o.Out, " - %v", response.Status.EvaluationError) } fmt.Fprintln(o.Out) } return response.Status.Allowed, nil } func (o *CanIOptions) resourceFor(mapper meta.RESTMapper, resourceArg string) schema.GroupVersionResource { if resourceArg == "*" { return schema.GroupVersionResource{Resource: resourceArg} } fullySpecifiedGVR, groupResource := schema.ParseResourceArg(strings.ToLower(resourceArg)) gvr := schema.GroupVersionResource{} if fullySpecifiedGVR != nil { gvr, _ = mapper.ResourceFor(*fullySpecifiedGVR) } if gvr.Empty() { var err error gvr, err = mapper.ResourceFor(groupResource.WithVersion("")) if err != nil { if !nonStandardResourceNames.Has(groupResource.String()) { if len(groupResource.Group) == 0 { o.WarningPrinter.Print(fmt.Sprintf("the server doesn't have a resource type '%s'\n", groupResource.Resource)) } else { o.WarningPrinter.Print(fmt.Sprintf("the server doesn't have a resource type '%s' in group '%s'\n", groupResource.Resource, groupResource.Group)) } } return schema.GroupVersionResource{Resource: resourceArg} } } return gvr } func (o *CanIOptions) printStatus(status authorizationv1.SubjectRulesReviewStatus) error { if status.Incomplete { o.WarningPrinter.Print(fmt.Sprintf("the list may be incomplete: %v", status.EvaluationError)) } breakdownRules := []rbacv1.PolicyRule{} for _, rule := range convertToPolicyRule(status) { breakdownRules = append(breakdownRules, rbacutil.BreakdownRule(rule)...) } compactRules, err := rbacutil.CompactRules(breakdownRules) if err != nil { return err } sort.Stable(rbacutil.SortableRuleSlice(compactRules)) w := printers.GetNewTabWriter(o.Out) defer w.Flush() allErrs := []error{} if !o.NoHeaders { if err := printAccessHeaders(w); err != nil { allErrs = append(allErrs, err) } } if err := printAccess(w, compactRules); err != nil { allErrs = append(allErrs, err) } return utilerrors.NewAggregate(allErrs) } func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule { ret := []rbacv1.PolicyRule{} for _, resource := range status.ResourceRules { ret = append(ret, rbacv1.PolicyRule{ Verbs: resource.Verbs, APIGroups: resource.APIGroups, Resources: resource.Resources, ResourceNames: resource.ResourceNames, }) } for _, nonResource := range status.NonResourceRules { ret = append(ret, rbacv1.PolicyRule{ Verbs: nonResource.Verbs, NonResourceURLs: nonResource.NonResourceURLs, }) } return ret } func printAccessHeaders(out io.Writer) error { columnNames := []string{"Resources", "Non-Resource URLs", "Resource Names", "Verbs"} _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t")) return err } func printAccess(out io.Writer, rules []rbacv1.PolicyRule) error { for _, r := range rules { if _, err := fmt.Fprintf(out, "%s\t%v\t%v\t%v\n", describe.CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs); err != nil { return err } } return nil } func isNamespaced(gvr schema.GroupVersionResource, discoveryClient discovery.DiscoveryInterface) (bool, error) { if gvr.Resource == "*" { return true, nil } apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(schema.GroupVersion{ Group: gvr.Group, Version: gvr.Version, }.String()) if err != nil { return true, err } for _, resource := range apiResourceList.APIResources { if resource.Name == gvr.Resource { return resource.Namespaced, nil } } return false, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gvr.Resource, gvr.Group) } func isKnownResourceVerb(s string) bool { return resourceVerbs.Has(s) } func isKnownNonResourceVerb(s string) bool { return nonResourceURLVerbs.Has(s) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani_test.go000066400000000000000000000233201504711711200300500ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "bytes" "fmt" "io" "net/http" "strings" "testing" authorizationv1 "k8s.io/api/authorization/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestRunAccessCheck(t *testing.T) { tests := []struct { name string o *CanIOptions args []string allowed bool serverErr error expectedBodyStrings []string }{ { name: "restmapping for args", o: &CanIOptions{}, args: []string{"get", "replicaset"}, allowed: true, expectedBodyStrings: []string{ `{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"replicasets"}}`, }, }, { name: "simple success", o: &CanIOptions{}, args: []string{"get", "deployments.extensions/foo"}, allowed: true, expectedBodyStrings: []string{ `{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, }, }, { name: "all namespaces", o: &CanIOptions{ AllNamespaces: true, }, args: []string{"get", "deployments.extensions/foo"}, allowed: true, expectedBodyStrings: []string{ `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, }, }, { name: "disallowed", o: &CanIOptions{ AllNamespaces: true, }, args: []string{"get", "deployments.extensions/foo"}, allowed: false, expectedBodyStrings: []string{ `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, }, }, { name: "forcedError", o: &CanIOptions{ AllNamespaces: true, }, args: []string{"get", "deployments.extensions/foo"}, allowed: false, serverErr: fmt.Errorf("forcedError"), expectedBodyStrings: []string{ `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, }, }, { name: "sub resource", o: &CanIOptions{ AllNamespaces: true, Subresource: "log", }, args: []string{"get", "pods"}, allowed: true, expectedBodyStrings: []string{ `{"resourceAttributes":{"verb":"get","resource":"pods","subresource":"log"}}`, }, }, { name: "nonResourceURL", o: &CanIOptions{}, args: []string{"get", "/logs"}, allowed: true, expectedBodyStrings: []string{ `{"nonResourceAttributes":{"path":"/logs","verb":"get"}}`, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { test.o.Out = io.Discard test.o.ErrOut = io.Discard tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { expectPath := "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews" if req.URL.Path != expectPath { t.Errorf("%s: expected %v, got %v", test.name, expectPath, req.URL.Path) return nil, nil } bodyBits, err := io.ReadAll(req.Body) if err != nil { t.Errorf("%s: %v", test.name, err) return nil, nil } body := string(bodyBits) for _, expectedBody := range test.expectedBodyStrings { if !strings.Contains(body, expectedBody) { t.Errorf("%s expecting %s in %s", test.name, expectedBody, body) } } return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString( fmt.Sprintf(`{"kind":"SelfSubjectAccessReview","apiVersion":"authorization.k8s.io/v1","status":{"allowed":%v}}`, test.allowed), )), }, test.serverErr }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}, ContentType: runtime.ContentTypeJSON}} if err := test.o.Complete(tf, test.args); err != nil { t.Errorf("%s: %v", test.name, err) return } actualAllowed, err := test.o.RunAccessCheck() switch { case test.serverErr == nil && err == nil: // pass case err != nil && test.serverErr != nil && strings.Contains(err.Error(), test.serverErr.Error()): // pass default: t.Errorf("%s: expected %v, got %v", test.name, test.serverErr, err) return } if actualAllowed != test.allowed { t.Errorf("%s: expected %v, got %v", test.name, test.allowed, actualAllowed) return } }) } } func TestRunAccessList(t *testing.T) { t.Run("test access list", func(t *testing.T) { options := &CanIOptions{List: true} expectedOutput := "Resources Non-Resource URLs Resource Names Verbs\n" + "job.* [] [test-resource] [get list]\n" + "pod.* [] [test-resource] [get list]\n" + " [/apis/*] [] [get]\n" + " [/version] [] [get]\n" tf := cmdtesting.NewTestFactory().WithNamespace("test") tf.ClientConfigVal.ContentType = runtime.ContentTypeJSON defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews": body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, getSelfSubjectRulesReview())))) return &http.Response{StatusCode: http.StatusOK, Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() options.IOStreams = ioStreams if err := options.Complete(tf, []string{}); err != nil { t.Errorf("got unexpected error when do Complete(): %v", err) return } err := options.RunAccessList() if err != nil { t.Errorf("got unexpected error when do RunAccessList(): %v", err) } else if buf.String() != expectedOutput { t.Errorf("expected %v\n but got %v\n", expectedOutput, buf.String()) } }) } func TestRunResourceFor(t *testing.T) { tests := []struct { name string o *CanIOptions resourceArg string expectGVR schema.GroupVersionResource expectedErrOut string }{ { name: "any resources", o: &CanIOptions{}, resourceArg: "*", expectGVR: schema.GroupVersionResource{ Resource: "*", }, }, { name: "server-supported standard resources without group", o: &CanIOptions{}, resourceArg: "pods", expectGVR: schema.GroupVersionResource{ Version: "v1", Resource: "pods", }, }, { name: "server-supported standard resources with group", o: &CanIOptions{}, resourceArg: "jobs", expectGVR: schema.GroupVersionResource{ Group: "batch", Version: "v1", Resource: "jobs", }, }, { name: "server-supported nonstandard resources", o: &CanIOptions{}, resourceArg: "users", expectGVR: schema.GroupVersionResource{ Resource: "users", }, }, { name: "invalid resources", o: &CanIOptions{}, resourceArg: "invalid", expectGVR: schema.GroupVersionResource{ Resource: "invalid", }, expectedErrOut: "Warning: the server doesn't have a resource type 'invalid'\n\n", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ioStreams, _, _, buf := genericiooptions.NewTestIOStreams() test.o.IOStreams = ioStreams test.o.WarningPrinter = printers.NewWarningPrinter(test.o.IOStreams.ErrOut, printers.WarningPrinterOptions{Color: false}) restMapper, err := tf.ToRESTMapper() if err != nil { t.Errorf("got unexpected error when do tf.ToRESTMapper(): %v", err) return } gvr := test.o.resourceFor(restMapper, test.resourceArg) if gvr != test.expectGVR { t.Errorf("expected %v\n but got %v\n", test.expectGVR, gvr) } if buf.String() != test.expectedErrOut { t.Errorf("expected %v\n but got %v\n", test.expectedErrOut, buf.String()) } }) } } func getSelfSubjectRulesReview() *authorizationv1.SelfSubjectRulesReview { return &authorizationv1.SelfSubjectRulesReview{ Status: authorizationv1.SubjectRulesReviewStatus{ ResourceRules: []authorizationv1.ResourceRule{ { Verbs: []string{"get", "list"}, APIGroups: []string{"*"}, Resources: []string{"pod", "job"}, ResourceNames: []string{"test-resource"}, }, }, NonResourceRules: []authorizationv1.NonResourceRule{ { Verbs: []string{"get"}, NonResourceURLs: []string{"/apis/*", "/version"}, }, }, }, } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/reconcile.go000066400000000000000000000255741504711711200300570ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "errors" "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" rbacv1 "k8s.io/api/rbac/v1" rbacv1alpha1 "k8s.io/api/rbac/v1alpha1" rbacv1beta1 "k8s.io/api/rbac/v1beta1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" rbacv1client "k8s.io/client-go/kubernetes/typed/rbac/v1" "k8s.io/component-helpers/auth/rbac/reconciliation" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/templates" ) // ReconcileOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type ReconcileOptions struct { PrintFlags *genericclioptions.PrintFlags FilenameOptions *resource.FilenameOptions DryRun bool RemoveExtraPermissions bool RemoveExtraSubjects bool Visitor resource.Visitor RBACClient rbacv1client.RbacV1Interface NamespaceClient corev1client.CoreV1Interface PrintObject printers.ResourcePrinterFunc genericiooptions.IOStreams } var ( reconcileLong = templates.LongDesc(` Reconciles rules for RBAC role, role binding, cluster role, and cluster role binding objects. Missing objects are created, and the containing namespace is created for namespaced objects, if required. Existing roles are updated to include the permissions in the input objects, and remove extra permissions if --remove-extra-permissions is specified. Existing bindings are updated to include the subjects in the input objects, and remove extra subjects if --remove-extra-subjects is specified. This is preferred to 'apply' for RBAC resources so that semantically-aware merging of rules and subjects is done.`) reconcileExample = templates.Examples(` # Reconcile RBAC resources from a file kubectl auth reconcile -f my-rbac-rules.yaml`) ) // NewReconcileOptions returns a new ReconcileOptions instance func NewReconcileOptions(ioStreams genericiooptions.IOStreams) *ReconcileOptions { return &ReconcileOptions{ FilenameOptions: &resource.FilenameOptions{}, PrintFlags: genericclioptions.NewPrintFlags("reconciled").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdReconcile holds the options for 'auth reconcile' sub command func NewCmdReconcile(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewReconcileOptions(streams) cmd := &cobra.Command{ Use: "reconcile -f FILENAME", DisableFlagsInUseLine: true, Short: "Reconciles rules for RBAC role, role binding, cluster role, and cluster role binding objects", Long: reconcileLong, Example: reconcileExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(cmd, f, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunReconcile()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to reconcile.") cmd.Flags().BoolVar(&o.RemoveExtraPermissions, "remove-extra-permissions", o.RemoveExtraPermissions, "If true, removes extra permissions added to roles") cmd.Flags().BoolVar(&o.RemoveExtraSubjects, "remove-extra-subjects", o.RemoveExtraSubjects, "If true, removes extra subjects added to rolebindings") cmdutil.AddDryRunFlag(cmd) return cmd } // Complete completes all the required options func (o *ReconcileOptions) Complete(cmd *cobra.Command, f cmdutil.Factory, args []string) error { if err := o.FilenameOptions.RequireFilenameOrKustomize(); err != nil { return err } if len(args) > 0 { return errors.New("no arguments are allowed") } dryRun, err := getClientSideDryRun(cmd) if err != nil { return err } o.DryRun = dryRun namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } r := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). ContinueOnError(). NamespaceParam(namespace).DefaultNamespace(). FilenameParam(enforceNamespace, o.FilenameOptions). Flatten(). Local(). Do() if err := r.Err(); err != nil { return err } o.Visitor = r clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.RBACClient, err = rbacv1client.NewForConfig(clientConfig) if err != nil { return err } o.NamespaceClient, err = corev1client.NewForConfig(clientConfig) if err != nil { return err } if o.DryRun { o.PrintFlags.Complete("%s (dry run)") } printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObject = printer.PrintObj return nil } // Validate makes sure provided values for ReconcileOptions are valid func (o *ReconcileOptions) Validate() error { if o.Visitor == nil { return errors.New("ReconcileOptions.Visitor must be set") } if o.RBACClient == nil { return errors.New("ReconcileOptions.RBACClient must be set") } if o.NamespaceClient == nil { return errors.New("ReconcileOptions.NamespaceClient must be set") } if o.PrintObject == nil { return errors.New("ReconcileOptions.Print must be set") } if o.Out == nil { return errors.New("ReconcileOptions.Out must be set") } if o.ErrOut == nil { return errors.New("ReconcileOptions.Err must be set") } return nil } // RunReconcile performs the execution func (o *ReconcileOptions) RunReconcile() error { return o.Visitor.Visit(func(info *resource.Info, err error) error { if err != nil { return err } switch t := info.Object.(type) { case *rbacv1.Role: reconcileOptions := reconciliation.ReconcileRoleOptions{ Confirm: !o.DryRun, RemoveExtraPermissions: o.RemoveExtraPermissions, Role: reconciliation.RoleRuleOwner{Role: t}, Client: reconciliation.RoleModifier{ NamespaceClient: o.NamespaceClient.Namespaces(), Client: o.RBACClient, }, } result, err := reconcileOptions.Run() if err != nil { return err } o.printResults(result.Role.GetObject(), nil, nil, result.MissingRules, result.ExtraRules, result.Operation, result.Protected) case *rbacv1.ClusterRole: reconcileOptions := reconciliation.ReconcileRoleOptions{ Confirm: !o.DryRun, RemoveExtraPermissions: o.RemoveExtraPermissions, Role: reconciliation.ClusterRoleRuleOwner{ClusterRole: t}, Client: reconciliation.ClusterRoleModifier{ Client: o.RBACClient.ClusterRoles(), }, } result, err := reconcileOptions.Run() if err != nil { return err } o.printResults(result.Role.GetObject(), nil, nil, result.MissingRules, result.ExtraRules, result.Operation, result.Protected) case *rbacv1.RoleBinding: reconcileOptions := reconciliation.ReconcileRoleBindingOptions{ Confirm: !o.DryRun, RemoveExtraSubjects: o.RemoveExtraSubjects, RoleBinding: reconciliation.RoleBindingAdapter{RoleBinding: t}, Client: reconciliation.RoleBindingClientAdapter{ Client: o.RBACClient, NamespaceClient: o.NamespaceClient.Namespaces(), }, } result, err := reconcileOptions.Run() if err != nil { return err } o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected) case *rbacv1.ClusterRoleBinding: reconcileOptions := reconciliation.ReconcileRoleBindingOptions{ Confirm: !o.DryRun, RemoveExtraSubjects: o.RemoveExtraSubjects, RoleBinding: reconciliation.ClusterRoleBindingAdapter{ClusterRoleBinding: t}, Client: reconciliation.ClusterRoleBindingClientAdapter{ Client: o.RBACClient.ClusterRoleBindings(), }, } result, err := reconcileOptions.Run() if err != nil { return err } o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected) case *rbacv1beta1.Role, *rbacv1beta1.RoleBinding, *rbacv1beta1.ClusterRole, *rbacv1beta1.ClusterRoleBinding, *rbacv1alpha1.Role, *rbacv1alpha1.RoleBinding, *rbacv1alpha1.ClusterRole, *rbacv1alpha1.ClusterRoleBinding: return fmt.Errorf("only rbac.authorization.k8s.io/v1 is supported: not %T", t) default: klog.V(1).Infof("skipping %#v", info.Object.GetObjectKind()) // skip ignored resources } return nil }) } func (o *ReconcileOptions) printResults(object runtime.Object, missingSubjects, extraSubjects []rbacv1.Subject, missingRules, extraRules []rbacv1.PolicyRule, operation reconciliation.ReconcileOperation, protected bool) { o.PrintObject(object, o.Out) caveat := "" if protected { caveat = ", but object opted out (rbac.authorization.kubernetes.io/autoupdate: false)" } switch operation { case reconciliation.ReconcileNone: return case reconciliation.ReconcileCreate: fmt.Fprintf(o.ErrOut, "\treconciliation required create%s\n", caveat) case reconciliation.ReconcileUpdate: fmt.Fprintf(o.ErrOut, "\treconciliation required update%s\n", caveat) case reconciliation.ReconcileRecreate: fmt.Fprintf(o.ErrOut, "\treconciliation required recreate%s\n", caveat) } if len(missingSubjects) > 0 { fmt.Fprintf(o.ErrOut, "\tmissing subjects added:\n") for _, s := range missingSubjects { fmt.Fprintf(o.ErrOut, "\t\t%+v\n", s) } } if o.RemoveExtraSubjects { if len(extraSubjects) > 0 { fmt.Fprintf(o.ErrOut, "\textra subjects removed:\n") for _, s := range extraSubjects { fmt.Fprintf(o.ErrOut, "\t\t%+v\n", s) } } } if len(missingRules) > 0 { fmt.Fprintf(o.ErrOut, "\tmissing rules added:\n") for _, r := range missingRules { fmt.Fprintf(o.ErrOut, "\t\t%+v\n", r) } } if o.RemoveExtraPermissions { if len(extraRules) > 0 { fmt.Fprintf(o.ErrOut, "\textra rules removed:\n") for _, r := range extraRules { fmt.Fprintf(o.ErrOut, "\t\t%+v\n", r) } } } } func getClientSideDryRun(cmd *cobra.Command) (bool, error) { dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd) if err != nil { return false, fmt.Errorf("error accessing --dry-run flag for command %s: %v", cmd.Name(), err) } if dryRunStrategy == cmdutil.DryRunServer { return false, fmt.Errorf("--dry-run=server for command %s is not supported yet", cmd.Name()) } return dryRunStrategy == cmdutil.DryRunClient, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/whoami.go000066400000000000000000000175631504711711200273770ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "fmt" "io" "github.com/spf13/cobra" authenticationv1 "k8s.io/api/authentication/v1" authenticationv1alpha1 "k8s.io/api/authentication/v1alpha1" authenticationv1beta1 "k8s.io/api/authentication/v1beta1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" authenticationv1client "k8s.io/client-go/kubernetes/typed/authentication/v1" authenticationv1alpha1client "k8s.io/client-go/kubernetes/typed/authentication/v1alpha1" authenticationv1beta1client "k8s.io/client-go/kubernetes/typed/authentication/v1beta1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/templates" ) // WhoAmIFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which // reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes // the logic itself easy to unit test. type WhoAmIFlags struct { RESTClientGetter genericclioptions.RESTClientGetter PrintFlags *genericclioptions.PrintFlags genericiooptions.IOStreams } // NewWhoAmIFlags returns a default WhoAmIFlags. func NewWhoAmIFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *WhoAmIFlags { return &WhoAmIFlags{ RESTClientGetter: restClientGetter, PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme), IOStreams: streams, } } // AddFlags registers flags for a cli. func (flags *WhoAmIFlags) AddFlags(cmd *cobra.Command) { flags.PrintFlags.AddFlags(cmd) } // ToOptions converts from CLI inputs to runtime inputs. func (flags *WhoAmIFlags) ToOptions(ctx context.Context, args []string) (*WhoAmIOptions, error) { w := &WhoAmIOptions{ ctx: ctx, IOStreams: flags.IOStreams, } clientConfig, err := flags.RESTClientGetter.ToRESTConfig() if err != nil { return nil, err } w.authV1alpha1Client, err = authenticationv1alpha1client.NewForConfig(clientConfig) if err != nil { return nil, err } w.authV1beta1Client, err = authenticationv1beta1client.NewForConfig(clientConfig) if err != nil { return nil, err } w.authV1Client, err = authenticationv1client.NewForConfig(clientConfig) if err != nil { return nil, err } if !flags.PrintFlags.OutputFlagSpecified() { w.resourcePrinterFunc = printTableSelfSubjectAccessReview } else { printer, err := flags.PrintFlags.ToPrinter() if err != nil { return nil, err } w.resourcePrinterFunc = printer.PrintObj } return w, nil } // WhoAmIOptions is the start of the data required to perform the operation. As new fields are added, // add them here instead of referencing the cmd.Flags() type WhoAmIOptions struct { authV1alpha1Client authenticationv1alpha1client.AuthenticationV1alpha1Interface authV1beta1Client authenticationv1beta1client.AuthenticationV1beta1Interface authV1Client authenticationv1client.AuthenticationV1Interface ctx context.Context resourcePrinterFunc printers.ResourcePrinterFunc genericiooptions.IOStreams } var ( whoAmILong = templates.LongDesc(` Experimental: Check who you are and your attributes (groups, extra). This command is helpful to get yourself aware of the current user attributes, especially when dynamic authentication, e.g., token webhook, auth proxy, or OIDC provider, is enabled in the Kubernetes cluster. `) whoAmIExample = templates.Examples(` # Get your subject attributes kubectl auth whoami # Get your subject attributes in JSON format kubectl auth whoami -o json `) ) // NewCmdWhoAmI returns an initialized Command for 'auth whoami' sub command. Experimental. func NewCmdWhoAmI(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { flags := NewWhoAmIFlags(restClientGetter, streams) cmd := &cobra.Command{ Use: "whoami", DisableFlagsInUseLine: true, Short: "Experimental: Check self subject attributes", Long: whoAmILong, Example: whoAmIExample, Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions(cmd.Context(), args) cmdutil.CheckErr(err) cmdutil.CheckErr(o.Run()) }, } flags.AddFlags(cmd) return cmd } var ( notEnabledErr = fmt.Errorf( "the selfsubjectreviews API is not enabled in the cluster\n" + "enable APISelfSubjectReview feature gate and authentication.k8s.io/v1alpha1 or authentication.k8s.io/v1beta1 API") forbiddenErr = fmt.Errorf( "the selfsubjectreviews API is not enabled in the cluster or you do not have permission to call it") ) // Run prints all user attributes. func (o WhoAmIOptions) Run() error { var ( res runtime.Object err error ) res, err = o.authV1Client. SelfSubjectReviews(). Create(context.TODO(), &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{}) if err != nil && errors.IsNotFound(err) { // Fallback to Beta API if Beta is not enabled res, err = o.authV1beta1Client. SelfSubjectReviews(). Create(context.TODO(), &authenticationv1beta1.SelfSubjectReview{}, metav1.CreateOptions{}) if err != nil && errors.IsNotFound(err) { // Fallback to Alpha API if Beta is not enabled res, err = o.authV1alpha1Client. SelfSubjectReviews(). Create(context.TODO(), &authenticationv1alpha1.SelfSubjectReview{}, metav1.CreateOptions{}) } } if err != nil { switch { case errors.IsForbidden(err): return forbiddenErr case errors.IsNotFound(err): return notEnabledErr default: return err } } return o.resourcePrinterFunc(res, o.Out) } func getUserInfo(obj runtime.Object) (authenticationv1.UserInfo, error) { switch obj.(type) { case *authenticationv1alpha1.SelfSubjectReview: return obj.(*authenticationv1alpha1.SelfSubjectReview).Status.UserInfo, nil case *authenticationv1beta1.SelfSubjectReview: return obj.(*authenticationv1beta1.SelfSubjectReview).Status.UserInfo, nil case *authenticationv1.SelfSubjectReview: return obj.(*authenticationv1.SelfSubjectReview).Status.UserInfo, nil default: return authenticationv1.UserInfo{}, fmt.Errorf("unexpected response type %T, expected SelfSubjectReview", obj) } } func printTableSelfSubjectAccessReview(obj runtime.Object, out io.Writer) error { ui, err := getUserInfo(obj) if err != nil { return err } w := printers.GetNewTabWriter(out) defer w.Flush() _, err = fmt.Fprintf(w, "ATTRIBUTE\tVALUE\n") if err != nil { return fmt.Errorf("cannot write a header: %w", err) } if ui.Username != "" { _, err := fmt.Fprintf(w, "Username\t%s\n", ui.Username) if err != nil { return fmt.Errorf("cannot write a username: %w", err) } } if ui.UID != "" { _, err := fmt.Fprintf(w, "UID\t%s\n", ui.UID) if err != nil { return fmt.Errorf("cannot write a uid: %w", err) } } if len(ui.Groups) > 0 { _, err := fmt.Fprintf(w, "Groups\t%v\n", ui.Groups) if err != nil { return fmt.Errorf("cannot write groups: %w", err) } } if len(ui.Extra) > 0 { for _, k := range sets.List(sets.KeySet(ui.Extra)) { v := ui.Extra[k] _, err := fmt.Fprintf(w, "Extra: %s\t%v\n", k, v) if err != nil { return fmt.Errorf("cannot write an extra: %w", err) } } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/auth/whoami_test.go000066400000000000000000000222021504711711200304200ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "bytes" "fmt" "io" "strings" "testing" authenticationv1 "k8s.io/api/authentication/v1" authenticationv1alpha1 "k8s.io/api/authentication/v1alpha1" authenticationv1beta1 "k8s.io/api/authentication/v1beta1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/printers" authfake "k8s.io/client-go/kubernetes/fake" core "k8s.io/client-go/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestWhoAmIRun(t *testing.T) { tests := []struct { name string o *WhoAmIOptions args []string serverErr error alphaDisabled bool betaDisabled bool stableDisabled bool expectedError error expectedBodyStrings []string }{ { name: "success test", o: &WhoAmIOptions{ resourcePrinterFunc: printTableSelfSubjectAccessReview, }, args: []string{}, expectedBodyStrings: []string{ `ATTRIBUTE VALUE`, `Username jane.doe`, `UID uniq-id`, `Groups [students teachers]`, `Extra: skills [reading learning]`, `Extra: subjects [math sports]`, ``, }, }, { name: "JSON test", o: &WhoAmIOptions{ resourcePrinterFunc: printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.JSONPrinter{}).PrintObj, }, args: []string{}, expectedBodyStrings: []string{ `{ "kind": "SelfSubjectReview", "apiVersion": "authentication.k8s.io/v1", "metadata": { "creationTimestamp": null }, "status": { "userInfo": { "username": "jane.doe", "uid": "uniq-id", "groups": [ "students", "teachers" ], "extra": { "skills": [ "reading", "learning" ], "subjects": [ "math", "sports" ] } } } } `, }, }, { name: "success test no alpha", o: &WhoAmIOptions{ resourcePrinterFunc: printTableSelfSubjectAccessReview, }, args: []string{}, alphaDisabled: true, expectedBodyStrings: []string{ `ATTRIBUTE VALUE`, `Username jane.doe`, `UID uniq-id`, `Groups [students teachers]`, `Extra: skills [reading learning]`, `Extra: subjects [math sports]`, ``, }, }, { name: "JSON test no alpha and stable", o: &WhoAmIOptions{ resourcePrinterFunc: printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.JSONPrinter{}).PrintObj, }, args: []string{}, alphaDisabled: true, stableDisabled: true, expectedBodyStrings: []string{ `{ "kind": "SelfSubjectReview", "apiVersion": "authentication.k8s.io/v1beta1", "metadata": { "creationTimestamp": null }, "status": { "userInfo": { "username": "jane.doe", "uid": "uniq-id", "groups": [ "students", "teachers" ], "extra": { "skills": [ "reading", "learning" ], "subjects": [ "math", "sports" ] } } } } `, }, }, { name: "success test no beta", o: &WhoAmIOptions{ resourcePrinterFunc: printTableSelfSubjectAccessReview, }, args: []string{}, betaDisabled: true, expectedBodyStrings: []string{ `ATTRIBUTE VALUE`, `Username jane.doe`, `UID uniq-id`, `Groups [students teachers]`, `Extra: skills [reading learning]`, `Extra: subjects [math sports]`, ``, }, }, { name: "JSON test no beta", o: &WhoAmIOptions{ resourcePrinterFunc: printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.JSONPrinter{}).PrintObj, }, args: []string{}, betaDisabled: true, expectedBodyStrings: []string{ `{ "kind": "SelfSubjectReview", "apiVersion": "authentication.k8s.io/v1", "metadata": { "creationTimestamp": null }, "status": { "userInfo": { "username": "jane.doe", "uid": "uniq-id", "groups": [ "students", "teachers" ], "extra": { "skills": [ "reading", "learning" ], "subjects": [ "math", "sports" ] } } } } `, }, }, { name: "all API disabled", o: &WhoAmIOptions{ resourcePrinterFunc: printTableSelfSubjectAccessReview, }, args: []string{}, betaDisabled: true, alphaDisabled: true, stableDisabled: true, expectedError: notEnabledErr, }, { name: "Forbidden error", o: &WhoAmIOptions{ resourcePrinterFunc: printTableSelfSubjectAccessReview, }, args: []string{}, serverErr: errors.NewForbidden( corev1.Resource("selfsubjectreviews"), "foo", fmt.Errorf("error"), ), expectedError: forbiddenErr, expectedBodyStrings: []string{}, }, { name: "NotFound error", o: &WhoAmIOptions{ resourcePrinterFunc: printTableSelfSubjectAccessReview, }, args: []string{}, serverErr: errors.NewNotFound(corev1.Resource("selfsubjectreviews"), "foo"), expectedError: notEnabledErr, expectedBodyStrings: []string{}, }, { name: "Server error", o: &WhoAmIOptions{ resourcePrinterFunc: printTableSelfSubjectAccessReview, }, args: []string{}, serverErr: fmt.Errorf("a random server-side error"), expectedError: fmt.Errorf("a random server-side error"), expectedBodyStrings: []string{}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var b bytes.Buffer test.o.Out = &b test.o.ErrOut = io.Discard tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() fakeAuthClientSet := &authfake.Clientset{} fakeAuthClientSet.AddReactor("create", "selfsubjectreviews", func(action core.Action) (handled bool, ret runtime.Object, err error) { if test.serverErr != nil { return true, nil, test.serverErr } ui := authenticationv1.UserInfo{ Username: "jane.doe", UID: "uniq-id", Groups: []string{"students", "teachers"}, Extra: map[string]authenticationv1.ExtraValue{ "subjects": {"math", "sports"}, "skills": {"reading", "learning"}, }, } switch action.GetResource().GroupVersion().String() { case "authentication.k8s.io/v1alpha1": if test.alphaDisabled { return true, nil, errors.NewNotFound(corev1.Resource("selfsubjectreviews"), "foo") } res := &authenticationv1alpha1.SelfSubjectReview{ Status: authenticationv1alpha1.SelfSubjectReviewStatus{ UserInfo: ui, }, } return true, res, nil case "authentication.k8s.io/v1beta1": if test.betaDisabled { return true, nil, errors.NewNotFound(corev1.Resource("selfsubjectreviews"), "foo") } res := &authenticationv1beta1.SelfSubjectReview{ Status: authenticationv1beta1.SelfSubjectReviewStatus{ UserInfo: ui, }, } return true, res, nil case "authentication.k8s.io/v1": if test.stableDisabled { return true, nil, errors.NewNotFound(corev1.Resource("selfsubjectreviews"), "foo") } res := &authenticationv1.SelfSubjectReview{ Status: authenticationv1.SelfSubjectReviewStatus{ UserInfo: ui, }, } return true, res, nil default: return false, nil, fmt.Errorf("unknown API") } }) test.o.authV1beta1Client = fakeAuthClientSet.AuthenticationV1beta1() test.o.authV1alpha1Client = fakeAuthClientSet.AuthenticationV1alpha1() test.o.authV1Client = fakeAuthClientSet.AuthenticationV1() err := test.o.Run() switch { case test.expectedError == nil && err == nil: // pass case err != nil && test.expectedError != nil && strings.Contains(err.Error(), test.expectedError.Error()): // pass default: t.Errorf("%s: expected %v, got %v", test.name, test.expectedError, err) return } res := b.String() expectedBody := strings.Join(test.expectedBodyStrings, "\n") if expectedBody != res { t.Errorf("%s: expected \n%q, got \n%q", test.name, expectedBody, res) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/autoscale/000077500000000000000000000000001504711711200265675ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/autoscale/autoscale.go000066400000000000000000000272571504711711200311130ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package autoscale import ( "context" "fmt" "github.com/spf13/cobra" "k8s.io/klog/v2" autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" autoscalingv1client "k8s.io/client-go/kubernetes/typed/autoscaling/v1" autoscalingv2client "k8s.io/client-go/kubernetes/typed/autoscaling/v2" "k8s.io/client-go/scale" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( autoscaleLong = templates.LongDesc(i18n.T(` Creates an autoscaler that automatically chooses and sets the number of pods that run in a Kubernetes cluster. The command will attempt to use the autoscaling/v2 API first, in case of an error, it will fall back to autoscaling/v1 API. Looks up a deployment, replica set, stateful set, or replication controller by name and creates an autoscaler that uses the given resource as a reference. An autoscaler can automatically increase or decrease number of pods deployed within the system as needed.`)) autoscaleExample = templates.Examples(i18n.T(` # Auto scale a deployment "foo", with the number of pods between 2 and 10, no target CPU utilization specified so a default autoscaling policy will be used kubectl autoscale deployment foo --min=2 --max=10 # Auto scale a replication controller "foo", with the number of pods between 1 and 5, target CPU utilization at 80% kubectl autoscale rc foo --max=5 --cpu-percent=80`)) ) // AutoscaleOptions declares the arguments accepted by the Autoscale command type AutoscaleOptions struct { FilenameOptions *resource.FilenameOptions RecordFlags *genericclioptions.RecordFlags Recorder genericclioptions.Recorder PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Name string Min int32 Max int32 CPUPercent int32 createAnnotation bool args []string enforceNamespace bool namespace string dryRunStrategy cmdutil.DryRunStrategy builder *resource.Builder fieldManager string HPAClientV1 autoscalingv1client.HorizontalPodAutoscalersGetter HPAClientV2 autoscalingv2client.HorizontalPodAutoscalersGetter scaleKindResolver scale.ScaleKindResolver genericiooptions.IOStreams } // NewAutoscaleOptions creates the options for autoscale func NewAutoscaleOptions(ioStreams genericiooptions.IOStreams) *AutoscaleOptions { return &AutoscaleOptions{ PrintFlags: genericclioptions.NewPrintFlags("autoscaled").WithTypeSetter(scheme.Scheme), FilenameOptions: &resource.FilenameOptions{}, RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, IOStreams: ioStreams, } } // NewCmdAutoscale returns the autoscale Cobra command func NewCmdAutoscale(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewAutoscaleOptions(ioStreams) validArgs := []string{"deployment", "replicaset", "replicationcontroller", "statefulset"} cmd := &cobra.Command{ Use: "autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS [--cpu-percent=CPU]", DisableFlagsInUseLine: true, Short: i18n.T("Auto-scale a deployment, replica set, stateful set, or replication controller"), Long: autoscaleLong, Example: autoscaleExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } // bind flag structs o.RecordFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) cmd.Flags().Int32Var(&o.Min, "min", -1, "The lower limit for the number of pods that can be set by the autoscaler. If it's not specified or negative, the server will apply a default value.") cmd.Flags().Int32Var(&o.Max, "max", -1, "The upper limit for the number of pods that can be set by the autoscaler. Required.") cmd.MarkFlagRequired("max") cmd.Flags().Int32Var(&o.CPUPercent, "cpu-percent", -1, "The target average CPU utilization (represented as a percent of requested CPU) over all the pods. If it's not specified or negative, a default autoscaling policy will be used.") cmd.Flags().StringVar(&o.Name, "name", "", i18n.T("The name for the newly created object. If not specified, the name of the input resource will be used.")) cmdutil.AddDryRunFlag(cmd) cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to autoscale.") cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-autoscale") return cmd } // Complete verifies command line arguments and loads data from the command environment func (o *AutoscaleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } discoveryClient, err := f.ToDiscoveryClient() if err != nil { return err } o.createAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.builder = f.NewBuilder() o.scaleKindResolver = scale.NewDiscoveryScaleKindResolver(discoveryClient) o.args = args o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } kubeClient, err := f.KubernetesClientSet() if err != nil { return err } o.HPAClientV2 = kubeClient.AutoscalingV2() o.HPAClientV1 = kubeClient.AutoscalingV1() o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) return o.PrintFlags.ToPrinter() } return nil } // Validate checks that the provided attach options are specified. func (o *AutoscaleOptions) Validate() error { if o.Max < 1 { return fmt.Errorf("--max=MAXPODS is required and must be at least 1, max: %d", o.Max) } if o.Max < o.Min { return fmt.Errorf("--max=MAXPODS must be larger or equal to --min=MINPODS, max: %d, min: %d", o.Max, o.Min) } return nil } func (o *AutoscaleOptions) Run() error { r := o.builder. Unstructured(). ContinueOnError(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, o.FilenameOptions). ResourceTypeOrNameArgs(false, o.args...). Flatten(). Do() if err := r.Err(); err != nil { return err } count := 0 err := r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } mapping := info.ResourceMapping() gvr := mapping.GroupVersionKind.GroupVersion().WithResource(mapping.Resource.Resource) if _, err := o.scaleKindResolver.ScaleForResource(gvr); err != nil { return fmt.Errorf("cannot autoscale a %v: %v", mapping.GroupVersionKind.Kind, err) } // handles the creation of HorizontalPodAutoscaler objects for both v2 and v1 APIs. // If v2 API fails, try to create and handle HorizontalPodAutoscaler using v1 API hpaV2 := o.createHorizontalPodAutoscalerV2(info.Name, mapping) if err := o.handleHPA(hpaV2); err != nil { klog.V(1).Infof("Encountered an error with the v2 HorizontalPodAutoscaler: %v. "+ "Falling back to try the v1 HorizontalPodAutoscaler", err) hpaV1 := o.createHorizontalPodAutoscalerV1(info.Name, mapping) if err := o.handleHPA(hpaV1); err != nil { return err } } count++ return nil }) if err != nil { return err } if count == 0 { return fmt.Errorf("no objects passed to autoscale") } return nil } // handleHPA handles the creation and management of a single HPA object. func (o *AutoscaleOptions) handleHPA(hpa runtime.Object) error { if err := o.Recorder.Record(hpa); err != nil { return fmt.Errorf("error recording current command: %w", err) } if o.dryRunStrategy == cmdutil.DryRunClient { printer, err := o.ToPrinter("created") if err != nil { return err } return printer.PrintObj(hpa, o.Out) } if err := util.CreateOrUpdateAnnotation(o.createAnnotation, hpa, scheme.DefaultJSONEncoder()); err != nil { return err } createOptions := metav1.CreateOptions{} if o.fieldManager != "" { createOptions.FieldManager = o.fieldManager } if o.dryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var actualHPA runtime.Object var err error switch typedHPA := hpa.(type) { case *autoscalingv2.HorizontalPodAutoscaler: actualHPA, err = o.HPAClientV2.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), typedHPA, createOptions) case *autoscalingv1.HorizontalPodAutoscaler: actualHPA, err = o.HPAClientV1.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), typedHPA, createOptions) default: return fmt.Errorf("unsupported HorizontalPodAutoscaler type %T", hpa) } if err != nil { return err } printer, err := o.ToPrinter("autoscaled") if err != nil { return err } return printer.PrintObj(actualHPA, o.Out) } func (o *AutoscaleOptions) createHorizontalPodAutoscalerV2(refName string, mapping *meta.RESTMapping) *autoscalingv2.HorizontalPodAutoscaler { name := o.Name if len(name) == 0 { name = refName } scaler := autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ APIVersion: mapping.GroupVersionKind.GroupVersion().String(), Kind: mapping.GroupVersionKind.Kind, Name: refName, }, MaxReplicas: o.Max, }, } if o.Min > 0 { scaler.Spec.MinReplicas = &o.Min } if o.CPUPercent >= 0 { scaler.Spec.Metrics = []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &o.CPUPercent, }, }, }, } } return &scaler } func (o *AutoscaleOptions) createHorizontalPodAutoscalerV1(refName string, mapping *meta.RESTMapping) *autoscalingv1.HorizontalPodAutoscaler { name := o.Name if len(name) == 0 { name = refName } scaler := autoscalingv1.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ APIVersion: mapping.GroupVersionKind.GroupVersion().String(), Kind: mapping.GroupVersionKind.Kind, Name: refName, }, MaxReplicas: o.Max, }, } if o.Min > 0 { v := int32(o.Min) scaler.Spec.MinReplicas = &v } if o.CPUPercent >= 0 { c := int32(o.CPUPercent) scaler.Spec.TargetCPUUtilizationPercentage = &c } return &scaler } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/autoscale/autoscale_test.go000066400000000000000000000372111504711711200321410ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package autoscale import ( "fmt" "testing" "github.com/stretchr/testify/assert" autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" ) type validateTestCase struct { name string options *AutoscaleOptions expectedError error } func TestAutoscaleValidate(t *testing.T) { tests := []validateTestCase{ { name: "valid options", options: &AutoscaleOptions{ Max: 10, Min: 1, }, expectedError: nil, }, { name: "max less than 1", options: &AutoscaleOptions{ Max: 0, Min: 1, }, expectedError: fmt.Errorf("--max=MAXPODS is required and must be at least 1, max: 0"), }, { name: "min greater than max", options: &AutoscaleOptions{ Max: 1, Min: 2, }, expectedError: fmt.Errorf("--max=MAXPODS must be larger or equal to --min=MINPODS, max: 1, min: 2"), }, { name: "zero min replicas", options: &AutoscaleOptions{ Max: 5, Min: 0, }, expectedError: nil, }, { name: "negative min replicas", options: &AutoscaleOptions{ Max: 5, Min: -2, }, expectedError: nil, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { errorGot := tc.options.Validate() assert.Equal(t, tc.expectedError, errorGot) }) } } type createHorizontalPodAutoscalerTestCase struct { name string options *AutoscaleOptions refName string mapping *meta.RESTMapping expectedHPAV2 *autoscalingv2.HorizontalPodAutoscaler expectedHPAV1 *autoscalingv1.HorizontalPodAutoscaler } func TestCreateHorizontalPodAutoscalerV2(t *testing.T) { tests := []createHorizontalPodAutoscalerTestCase{ { name: "create with all options", options: &AutoscaleOptions{ Name: "custom-name", Max: 10, Min: 2, CPUPercent: 80, }, refName: "deployment-1", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name", }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-1", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(10), Metrics: []autoscalingv2.MetricSpec{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget Type: autoscalingv2.UtilizationMetricType, AverageUtilization: ptr.To(int32(80)), }, }, }, }, }, }, }, { name: "create without min replicas", options: &AutoscaleOptions{ Name: "custom-name-2", Max: 10, Min: -1, CPUPercent: 80, }, refName: "deployment-2", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name-2", }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-2", }, MinReplicas: nil, MaxReplicas: int32(10), Metrics: []autoscalingv2.MetricSpec{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget Type: autoscalingv2.UtilizationMetricType, AverageUtilization: ptr.To(int32(80)), }, }, }, }, }, }, }, { name: "create without max replicas", options: &AutoscaleOptions{ Name: "custom-name-3", Max: -1, Min: 2, CPUPercent: 80, }, refName: "deployment-3", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name-3", }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-3", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(-1), Metrics: []autoscalingv2.MetricSpec{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget Type: autoscalingv2.UtilizationMetricType, AverageUtilization: ptr.To(int32(80)), }, }, }, }, }, }, }, { name: "create without cpu utilization", options: &AutoscaleOptions{ Name: "custom-name-4", Max: 10, Min: 2, CPUPercent: -1, }, refName: "deployment-4", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name-4", }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-4", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(10), }, }, }, { name: "create with replicaset reference", options: &AutoscaleOptions{ Name: "replicaset-hpa", Max: 5, Min: 1, CPUPercent: 70, }, refName: "frontend", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "ReplicaSet", }, }, expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "replicaset-hpa", }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "ReplicaSet", Name: "frontend", }, MinReplicas: ptr.To(int32(1)), MaxReplicas: int32(5), Metrics: []autoscalingv2.MetricSpec{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget Type: autoscalingv2.UtilizationMetricType, AverageUtilization: ptr.To(int32(70)), }, }, }, }, }, }, }, { name: "create with statefulset reference", options: &AutoscaleOptions{ Name: "statefulset-hpa", Max: 8, Min: 2, CPUPercent: 60, }, refName: "web", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "StatefulSet", }, }, expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "statefulset-hpa", }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "StatefulSet", Name: "web", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(8), Metrics: []autoscalingv2.MetricSpec{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ // Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget Type: autoscalingv2.UtilizationMetricType, AverageUtilization: ptr.To(int32(60)), }, }, }, }, }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { hpa := tc.options.createHorizontalPodAutoscalerV2(tc.refName, tc.mapping) assert.Equal(t, tc.expectedHPAV2, hpa) }) } } func TestCreateHorizontalPodAutoscalerV1(t *testing.T) { tests := []createHorizontalPodAutoscalerTestCase{ { name: "create with all options", options: &AutoscaleOptions{ Name: "custom-name", Max: 10, Min: 2, CPUPercent: 80, }, refName: "deployment-1", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV1: &autoscalingv1.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name", }, Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-1", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(10), TargetCPUUtilizationPercentage: ptr.To(int32(80)), }, }, }, { name: "create without min replicas", options: &AutoscaleOptions{ Name: "custom-name-2", Max: 10, Min: -1, CPUPercent: 80, }, refName: "deployment-2", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV1: &autoscalingv1.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name-2", }, Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-2", }, MinReplicas: nil, MaxReplicas: int32(10), TargetCPUUtilizationPercentage: ptr.To(int32(80)), }, }, }, { name: "create without max replicas", options: &AutoscaleOptions{ Name: "custom-name-3", Max: -1, Min: 2, CPUPercent: 80, }, refName: "deployment-3", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV1: &autoscalingv1.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name-3", }, Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-3", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(-1), TargetCPUUtilizationPercentage: ptr.To(int32(80)), }, }, }, { name: "create without cpu utilization", options: &AutoscaleOptions{ Name: "custom-name-4", Max: 10, Min: 2, CPUPercent: -1, }, refName: "deployment-4", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", }, }, expectedHPAV1: &autoscalingv1.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "custom-name-4", }, Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "deployment-4", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(10), }, }, }, { name: "create with replicaset reference", options: &AutoscaleOptions{ Name: "replicaset-hpa", Max: 5, Min: 1, CPUPercent: 70, }, refName: "frontend", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "ReplicaSet", }, }, expectedHPAV1: &autoscalingv1.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "replicaset-hpa", }, Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "ReplicaSet", Name: "frontend", }, MinReplicas: ptr.To(int32(1)), MaxReplicas: int32(5), TargetCPUUtilizationPercentage: ptr.To(int32(70)), }, }, }, { name: "create with statefulset reference", options: &AutoscaleOptions{ Name: "statefulset-hpa", Max: 8, Min: 2, CPUPercent: 60, }, refName: "web", mapping: &meta.RESTMapping{ GroupVersionKind: schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "StatefulSet", }, }, expectedHPAV1: &autoscalingv1.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "statefulset-hpa", }, Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "StatefulSet", Name: "web", }, MinReplicas: ptr.To(int32(2)), MaxReplicas: int32(8), TargetCPUUtilizationPercentage: ptr.To(int32(60)), }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { hpa := tc.options.createHorizontalPodAutoscalerV1(tc.refName, tc.mapping) assert.Equal(t, tc.expectedHPAV1, hpa) }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/certificates/000077500000000000000000000000001504711711200272545ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/certificates/certificates.go000066400000000000000000000242021504711711200322500ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package certificates import ( "context" "fmt" "io" "github.com/spf13/cobra" certificatesv1 "k8s.io/api/certificates/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" 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/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" v1 "k8s.io/client-go/kubernetes/typed/certificates/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // NewCmdCertificate returns `certificate` Cobra command func NewCmdCertificate(restClientGetter genericclioptions.RESTClientGetter, ioStreams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "certificate SUBCOMMAND", DisableFlagsInUseLine: true, Short: i18n.T("Modify certificate resources"), Long: i18n.T("Modify certificate resources."), Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, } cmd.AddCommand(NewCmdCertificateApprove(restClientGetter, ioStreams)) cmd.AddCommand(NewCmdCertificateDeny(restClientGetter, ioStreams)) return cmd } // CertificateOptions declares the arguments accepted by the certificate command type CertificateOptions struct { resource.FilenameOptions PrintFlags *genericclioptions.PrintFlags PrintObj printers.ResourcePrinterFunc csrNames []string outputStyle string certificatesV1Client v1.CertificatesV1Interface builder *resource.Builder genericiooptions.IOStreams } // NewCertificateOptions creates CertificateOptions struct for `certificate` command func NewCertificateOptions(ioStreams genericiooptions.IOStreams, operation string) *CertificateOptions { return &CertificateOptions{ PrintFlags: genericclioptions.NewPrintFlags(operation).WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // Complete loads data from the command environment func (o *CertificateOptions) Complete(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, args []string) error { o.csrNames = args o.outputStyle = cmdutil.GetFlagString(cmd, "output") printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object, out io.Writer) error { return printer.PrintObj(obj, out) } o.builder = resource.NewBuilder(restClientGetter) clientConfig, err := restClientGetter.ToRESTConfig() if err != nil { return err } o.certificatesV1Client, err = v1.NewForConfig(clientConfig) if err != nil { return err } return nil } // Validate checks if the provided `certificate` arguments are valid func (o *CertificateOptions) Validate() error { if len(o.csrNames) < 1 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { return fmt.Errorf("one or more CSRs must be specified as or -f ") } return nil } // NewCmdCertificateApprove returns the `certificate approve` Cobra command func NewCmdCertificateApprove(restClientGetter genericclioptions.RESTClientGetter, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCertificateOptions(ioStreams, "approved") cmd := &cobra.Command{ Use: "approve (-f FILENAME | NAME)", DisableFlagsInUseLine: true, Short: i18n.T("Approve a certificate signing request"), Long: templates.LongDesc(i18n.T(` Approve a certificate signing request. kubectl certificate approve allows a cluster admin to approve a certificate signing request (CSR). This action tells a certificate signing controller to issue a certificate to the requester with the attributes requested in the CSR. SECURITY NOTICE: Depending on the requested attributes, the issued certificate can potentially grant a requester access to cluster resources or to authenticate as a requested identity. Before approving a CSR, ensure you understand what the signed certificate can do. `)), Example: templates.Examples(i18n.T(` # Approve CSR 'csr-sqgzp' kubectl certificate approve csr-sqgzp `)), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(restClientGetter, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunCertificateApprove(cmdutil.GetFlagBool(cmd, "force"))) }, } o.PrintFlags.AddFlags(cmd) cmd.Flags().Bool("force", false, "Update the CSR even if it is already approved.") cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update") return cmd } // RunCertificateApprove approves a certificate signing request func (o *CertificateOptions) RunCertificateApprove(force bool) error { return o.modifyCertificateCondition( o.builder, force, addConditionIfNeeded(string(certificatesv1.CertificateDenied), string(certificatesv1.CertificateApproved), "KubectlApprove", "This CSR was approved by kubectl certificate approve."), ) } // NewCmdCertificateDeny returns the `certificate deny` Cobra command func NewCmdCertificateDeny(restClientGetter genericclioptions.RESTClientGetter, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCertificateOptions(ioStreams, "denied") cmd := &cobra.Command{ Use: "deny (-f FILENAME | NAME)", DisableFlagsInUseLine: true, Short: i18n.T("Deny a certificate signing request"), Long: templates.LongDesc(i18n.T(` Deny a certificate signing request. kubectl certificate deny allows a cluster admin to deny a certificate signing request (CSR). This action tells a certificate signing controller to not to issue a certificate to the requester. `)), Example: templates.Examples(i18n.T(` # Deny CSR 'csr-sqgzp' kubectl certificate deny csr-sqgzp `)), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(restClientGetter, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunCertificateDeny(cmdutil.GetFlagBool(cmd, "force"))) }, } o.PrintFlags.AddFlags(cmd) cmd.Flags().Bool("force", false, "Update the CSR even if it is already denied.") cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update") return cmd } // RunCertificateDeny denies a certificate signing request func (o *CertificateOptions) RunCertificateDeny(force bool) error { return o.modifyCertificateCondition( o.builder, force, addConditionIfNeeded(string(certificatesv1.CertificateApproved), string(certificatesv1.CertificateDenied), "KubectlDeny", "This CSR was denied by kubectl certificate deny."), ) } func (o *CertificateOptions) modifyCertificateCondition(builder *resource.Builder, force bool, modify func(csr runtime.Object) (runtime.Object, bool, error)) error { var found int r := builder. Unstructured(). ContinueOnError(). FilenameParam(false, &o.FilenameOptions). ResourceNames("certificatesigningrequests", o.csrNames...). RequireObject(true). Flatten(). Latest(). Do() err := r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } for i := 0; ; i++ { obj, ok := info.Object.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected *unstructured.Unstructured, got %T", obj) } if want, got := certificatesv1.Kind("CertificateSigningRequest"), obj.GetObjectKind().GroupVersionKind().GroupKind(); want != got { return fmt.Errorf("can only handle %s objects, got %s", want.String(), got.String()) } var csr runtime.Object // get a typed object csr, err = o.certificatesV1Client.CertificateSigningRequests().Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) if apierrors.IsNotFound(err) { return fmt.Errorf("could not find v1 version of %s: %v", obj.GetName(), err) } if err != nil { return err } modifiedCSR, hasCondition, err := modify(csr) if err != nil { return err } if !hasCondition || force { if mCSR, ok := modifiedCSR.(*certificatesv1.CertificateSigningRequest); ok { _, err = o.certificatesV1Client.CertificateSigningRequests().UpdateApproval(context.TODO(), mCSR.Name, mCSR, metav1.UpdateOptions{}) } else { return fmt.Errorf("can only handle certificates.k8s.io CertificateSigningRequest objects, got %T", mCSR) } if apierrors.IsConflict(err) && i < 10 { if err := info.Get(); err != nil { return err } continue } if err != nil { return err } } break } found++ return o.PrintObj(info.Object, o.Out) }) if found == 0 && err == nil { fmt.Fprintf(o.Out, "No resources found\n") } return err } func addConditionIfNeeded(mustNotHaveConditionType, conditionType, reason, message string) func(runtime.Object) (runtime.Object, bool, error) { return func(obj runtime.Object) (runtime.Object, bool, error) { if csr, ok := obj.(*certificatesv1.CertificateSigningRequest); ok { var alreadyHasCondition bool for _, c := range csr.Status.Conditions { if string(c.Type) == mustNotHaveConditionType { return nil, false, fmt.Errorf("certificate signing request %q is already %s", csr.Name, c.Type) } if string(c.Type) == conditionType { alreadyHasCondition = true } } if alreadyHasCondition { return csr, true, nil } csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{ Type: certificatesv1.RequestConditionType(conditionType), Status: corev1.ConditionTrue, Reason: reason, Message: message, LastUpdateTime: metav1.Now(), }) return csr, false, nil } else { return csr, false, nil } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/certificates/certificates_test.go000066400000000000000000000250131504711711200333100ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package certificates import ( "bytes" "io" "net/http" "reflect" "strings" "testing" "github.com/spf13/cobra" certificatesv1 "k8s.io/api/certificates/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" ) func TestCertificates(t *testing.T) { testcases := []struct { name string nov1 bool nov1beta1 bool command string force bool args []string expectFailure bool expectActions []string expectOutput string expectErrOutput string }{ { name: "approve existing", command: "approve", args: []string{"existing"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval`, }, expectOutput: `approved`, }, { name: "approve existing, no v1", nov1: true, nov1beta1: true, command: "approve", args: []string{"existing"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get, 404 }, expectFailure: true, expectErrOutput: `could not find the requested resource`, }, { name: "approve already approved", command: "approve", args: []string{"approved"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get }, expectOutput: `approved`, }, { name: "approve already approved, force", command: "approve", args: []string{"approved"}, force: true, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/approved/approval`, }, expectOutput: `approved`, }, { name: "approve already denied", command: "approve", args: []string{"denied"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get }, expectFailure: true, expectErrOutput: `is already Denied`, }, { name: "deny existing", command: "deny", args: []string{"existing"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval`, }, expectOutput: `denied`, }, { name: "deny existing, no v1", nov1: true, nov1beta1: true, command: "deny", args: []string{"existing"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get, 404 }, expectFailure: true, expectErrOutput: `could not find the requested resource`, }, { name: "deny already denied", command: "deny", args: []string{"denied"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get }, expectOutput: `denied`, }, { name: "deny already denied, force", command: "deny", args: []string{"denied"}, force: true, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/denied/approval`, }, expectOutput: `denied`, }, { name: "deny already approved", command: "deny", args: []string{"approved"}, expectActions: []string{ `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get }, expectFailure: true, expectErrOutput: `is already Approved`, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) existingV1 := &certificatesv1.CertificateSigningRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}, ObjectMeta: metav1.ObjectMeta{Name: "existing"}, } approvedV1 := &certificatesv1.CertificateSigningRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}, ObjectMeta: metav1.ObjectMeta{Name: "approved"}, Status: certificatesv1.CertificateSigningRequestStatus{Conditions: []certificatesv1.CertificateSigningRequestCondition{{Type: certificatesv1.CertificateApproved}}}, } deniedV1 := &certificatesv1.CertificateSigningRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}, ObjectMeta: metav1.ObjectMeta{Name: "denied"}, Status: certificatesv1.CertificateSigningRequestStatus{Conditions: []certificatesv1.CertificateSigningRequestCondition{{Type: certificatesv1.CertificateDenied}}}, } actions := []string{} fakeClient := fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { actions = append(actions, req.Method+" "+req.URL.Path) switch p, m := req.URL.Path, req.Method; { case tc.nov1 && strings.HasPrefix(p, "/apis/certificates.k8s.io/v1/"): return &http.Response{StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewBuffer([]byte{}))}, nil case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/missing" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusNotFound}, nil case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/existing" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1)}, nil case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval" && m == http.MethodPut: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1)}, nil case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/approved" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1)}, nil case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/approved/approval" && m == http.MethodPut: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1)}, nil case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/denied" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1)}, nil case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/denied/approval" && m == http.MethodPut: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }) tf.UnstructuredClientForMappingFunc = func(gv schema.GroupVersion) (resource.RESTClient, error) { versionedAPIPath := "" if gv.Group == "" { versionedAPIPath = "/api/" + gv.Version } else { versionedAPIPath = "/apis/" + gv.Group + "/" + gv.Version } return &fake.RESTClient{ VersionedAPIPath: versionedAPIPath, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fakeClient, }, nil } tf.Client = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fakeClient, } streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() tf.ClientConfigVal.Transport = fakeClient.Transport defer func() { // Restore cmdutil behavior. cmdutil.DefaultBehaviorOnFatal() }() // Check exit code. cmdutil.BehaviorOnFatal(func(e string, code int) { if !tc.expectFailure { t.Log(e) t.Errorf("unexpected failure exit code %d", code) } errbuf.Write([]byte(e)) }) var cmd *cobra.Command switch tc.command { case "approve": cmd = NewCmdCertificateApprove(tf, streams) case "deny": cmd = NewCmdCertificateDeny(tf, streams) default: t.Errorf("unknown command: %s", tc.command) } if tc.force { cmd.Flags().Set("force", "true") } cmd.Run(cmd, tc.args) if !strings.Contains(buf.String(), tc.expectOutput) { t.Errorf("expected output to contain %q:\n%s", tc.expectOutput, buf.String()) } if !strings.Contains(errbuf.String(), tc.expectErrOutput) { t.Errorf("expected error output to contain %q:\n%s", tc.expectErrOutput, errbuf.String()) } if !reflect.DeepEqual(tc.expectActions, actions) { t.Logf("stdout:\n%s", buf.String()) t.Logf("stderr:\n%s", errbuf.String()) t.Errorf("expected\n%s\ngot\n%s", strings.Join(tc.expectActions, "\n"), strings.Join(actions, "\n")) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo/000077500000000000000000000000001504711711200271445ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo/clusterinfo.go000066400000000000000000000114241504711711200320320ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package clusterinfo import ( "fmt" "io" "strconv" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" restclient "k8s.io/client-go/rest" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "github.com/spf13/cobra" ) var ( longDescr = templates.LongDesc(i18n.T(` Display addresses of the control plane and services with label kubernetes.io/cluster-service=true. To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.`)) clusterinfoExample = templates.Examples(i18n.T(` # Print the address of the control plane and cluster services kubectl cluster-info`)) ) type ClusterInfoOptions struct { genericiooptions.IOStreams Namespace string Builder *resource.Builder Client *restclient.Config } func NewCmdClusterInfo(restClientGetter genericclioptions.RESTClientGetter, ioStreams genericiooptions.IOStreams) *cobra.Command { o := &ClusterInfoOptions{ IOStreams: ioStreams, } cmd := &cobra.Command{ Use: "cluster-info", Short: i18n.T("Display cluster information"), Long: longDescr, Example: clusterinfoExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(restClientGetter, cmd)) cmdutil.CheckErr(o.Run()) }, } cmd.AddCommand(NewCmdClusterInfoDump(restClientGetter, ioStreams)) return cmd } func (o *ClusterInfoOptions) Complete(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command) error { var err error o.Client, err = restClientGetter.ToRESTConfig() if err != nil { return err } cmdNamespace := cmdutil.GetFlagString(cmd, "namespace") if cmdNamespace == "" { cmdNamespace = metav1.NamespaceSystem } o.Namespace = cmdNamespace o.Builder = resource.NewBuilder(restClientGetter) return nil } func (o *ClusterInfoOptions) Run() error { // TODO use generalized labels once they are implemented (#341) b := o.Builder. WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). LabelSelectorParam("kubernetes.io/cluster-service=true"). ResourceTypeOrNameArgs(false, []string{"services"}...). Latest() err := b.Do().Visit(func(r *resource.Info, err error) error { if err != nil { return err } printService(o.Out, "Kubernetes control plane", o.Client.Host) services := r.Object.(*corev1.ServiceList).Items for _, service := range services { var link string if len(service.Status.LoadBalancer.Ingress) > 0 { ingress := service.Status.LoadBalancer.Ingress[0] ip := ingress.IP if ip == "" { ip = ingress.Hostname } for _, port := range service.Spec.Ports { link += "http://" + ip + ":" + strconv.Itoa(int(port.Port)) + " " } } else { name := service.ObjectMeta.Name if len(service.Spec.Ports) > 0 { port := service.Spec.Ports[0] // guess if the scheme is https scheme := "" if port.Name == "https" || port.Port == 443 { scheme = "https" } // format is :: name = utilnet.JoinSchemeNamePort(scheme, service.ObjectMeta.Name, port.Name) } if len(o.Client.GroupVersion.Group) == 0 { link = o.Client.Host + "/api/" + o.Client.GroupVersion.Version + "/namespaces/" + service.ObjectMeta.Namespace + "/services/" + name + "/proxy" } else { link = o.Client.Host + "/api/" + o.Client.GroupVersion.Group + "/" + o.Client.GroupVersion.Version + "/namespaces/" + service.ObjectMeta.Namespace + "/services/" + name + "/proxy" } } name := service.ObjectMeta.Labels["kubernetes.io/name"] if len(name) == 0 { name = service.ObjectMeta.Name } printService(o.Out, name, link) } return nil }) o.Out.Write([]byte("\nTo further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.\n")) return err // TODO consider printing more information about cluster } func printService(out io.Writer, name, link string) { fmt.Fprint(out, name) fmt.Fprint(out, " is running at ") fmt.Fprint(out, link) fmt.Fprintln(out, "") } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo/clusterinfo_dump.go000066400000000000000000000231511504711711200330570ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package clusterinfo import ( "context" "fmt" "io" "os" "path" "path/filepath" "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" appsv1client "k8s.io/client-go/kubernetes/typed/apps/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) const ( defaultPodLogsTimeout = 20 * time.Second timeout = 5 * time.Minute ) type ClusterInfoDumpOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj printers.ResourcePrinterFunc OutputDir string AllNamespaces bool Namespaces []string Timeout time.Duration AppsClient appsv1client.AppsV1Interface CoreClient corev1client.CoreV1Interface Namespace string RESTClientGetter genericclioptions.RESTClientGetter LogsForObject polymorphichelpers.LogsForObjectFunc genericiooptions.IOStreams } func NewCmdClusterInfoDump(restClientGetter genericclioptions.RESTClientGetter, ioStreams genericiooptions.IOStreams) *cobra.Command { o := &ClusterInfoDumpOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme).WithDefaultOutput("json"), IOStreams: ioStreams, } cmd := &cobra.Command{ Use: "dump", Short: i18n.T("Dump relevant information for debugging and diagnosis"), Long: dumpLong, Example: dumpExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(restClientGetter, cmd)) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmd.Flags().StringVar(&o.OutputDir, "output-directory", o.OutputDir, i18n.T("Where to output the files. If empty or '-' uses stdout, otherwise creates a directory hierarchy in that directory")) cmd.Flags().StringSliceVar(&o.Namespaces, "namespaces", o.Namespaces, "A comma separated list of namespaces to dump.") cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If true, dump all namespaces. If true, --namespaces is ignored.") cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodLogsTimeout) return cmd } var ( dumpLong = templates.LongDesc(i18n.T(` Dump cluster information out suitable for debugging and diagnosing cluster problems. By default, dumps everything to stdout. You can optionally specify a directory with --output-directory. If you specify a directory, Kubernetes will build a set of files in that directory. By default, only dumps things in the current namespace and 'kube-system' namespace, but you can switch to a different namespace with the --namespaces flag, or specify --all-namespaces to dump all namespaces. The command also dumps the logs of all of the pods in the cluster; these logs are dumped into different directories based on namespace and pod name.`)) dumpExample = templates.Examples(i18n.T(` # Dump current cluster state to stdout kubectl cluster-info dump # Dump current cluster state to /path/to/cluster-state kubectl cluster-info dump --output-directory=/path/to/cluster-state # Dump all namespaces to stdout kubectl cluster-info dump --all-namespaces # Dump a set of namespaces to /path/to/cluster-state kubectl cluster-info dump --namespaces default,kube-system --output-directory=/path/to/cluster-state`)) ) func setupOutputWriter(dir string, defaultWriter io.Writer, filename string, fileExtension string) io.Writer { if len(dir) == 0 || dir == "-" { return defaultWriter } fullFile := filepath.Join(dir, filename) + fileExtension parent := filepath.Dir(fullFile) cmdutil.CheckErr(os.MkdirAll(parent, 0755)) file, err := os.Create(fullFile) cmdutil.CheckErr(err) return file } func (o *ClusterInfoDumpOptions) Complete(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command) error { printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj config, err := restClientGetter.ToRESTConfig() if err != nil { return err } o.CoreClient, err = corev1client.NewForConfig(config) if err != nil { return err } o.AppsClient, err = appsv1client.NewForConfig(config) if err != nil { return err } o.Timeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) if err != nil { return err } o.Namespace, _, err = restClientGetter.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.RESTClientGetter = restClientGetter o.LogsForObject = polymorphichelpers.LogsForObjectFn return nil } func (o *ClusterInfoDumpOptions) Run() error { nodes, err := o.CoreClient.Nodes().List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } fileExtension := ".txt" if o.PrintFlags.OutputFormat != nil { switch *o.PrintFlags.OutputFormat { case "json": fileExtension = ".json" case "yaml": fileExtension = ".yaml" } } if err := o.PrintObj(nodes, setupOutputWriter(o.OutputDir, o.Out, "nodes", fileExtension)); err != nil { return err } var namespaces []string if o.AllNamespaces { namespaceList, err := o.CoreClient.Namespaces().List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } for ix := range namespaceList.Items { namespaces = append(namespaces, namespaceList.Items[ix].Name) } } else { if len(o.Namespaces) == 0 { namespaces = []string{ metav1.NamespaceSystem, o.Namespace, } } else { namespaces = o.Namespaces } } for _, namespace := range namespaces { // TODO: this is repetitive in the extreme. Use reflection or // something to make this a for loop. events, err := o.CoreClient.Events(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } if err := o.PrintObj(events, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "events"), fileExtension)); err != nil { return err } rcs, err := o.CoreClient.ReplicationControllers(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } if err := o.PrintObj(rcs, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "replication-controllers"), fileExtension)); err != nil { return err } svcs, err := o.CoreClient.Services(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } if err := o.PrintObj(svcs, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "services"), fileExtension)); err != nil { return err } sets, err := o.AppsClient.DaemonSets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } if err := o.PrintObj(sets, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "daemonsets"), fileExtension)); err != nil { return err } deps, err := o.AppsClient.Deployments(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } if err := o.PrintObj(deps, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "deployments"), fileExtension)); err != nil { return err } rps, err := o.AppsClient.ReplicaSets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } if err := o.PrintObj(rps, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "replicasets"), fileExtension)); err != nil { return err } pods, err := o.CoreClient.Pods(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return err } if err := o.PrintObj(pods, setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, "pods"), fileExtension)); err != nil { return err } printContainer := func(writer io.Writer, container corev1.Container, pod *corev1.Pod) { writer.Write([]byte(fmt.Sprintf("==== START logs for container %s of pod %s/%s ====\n", container.Name, pod.Namespace, pod.Name))) defer writer.Write([]byte(fmt.Sprintf("==== END logs for container %s of pod %s/%s ====\n", container.Name, pod.Namespace, pod.Name))) requests, err := o.LogsForObject(o.RESTClientGetter, pod, &corev1.PodLogOptions{Container: container.Name}, timeout, false) if err != nil { // Print error and return. writer.Write([]byte(fmt.Sprintf("Create log request error: %s\n", err.Error()))) return } for _, request := range requests { data, err := request.DoRaw(context.TODO()) if err != nil { // Print error and return. writer.Write([]byte(fmt.Sprintf("Request log error: %s\n", err.Error()))) return } writer.Write(data) } } for ix := range pods.Items { pod := &pods.Items[ix] initcontainers := pod.Spec.InitContainers containers := pod.Spec.Containers writer := setupOutputWriter(o.OutputDir, o.Out, path.Join(namespace, pod.Name, "logs"), ".txt") for i := range initcontainers { printContainer(writer, initcontainers[i], pod) } for i := range containers { printContainer(writer, containers[i], pod) } } } dest := o.OutputDir if len(dest) > 0 && dest != "-" { fmt.Fprintf(o.Out, "Cluster info dumped to %s\n", dest) } return nil } clusterinfo_dump_test.go000066400000000000000000000040221504711711200340330ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package clusterinfo import ( "os" "path/filepath" "testing" "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestSetupOutputWriterNoOp(t *testing.T) { tests := []struct { name string outputWriter string }{ { name: "empty", outputWriter: "", }, { name: "stdout", outputWriter: "-", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, _, buf, _ := genericiooptions.NewTestIOStreams() f := cmdtesting.NewTestFactory() defer f.Cleanup() writer := setupOutputWriter(tt.outputWriter, buf, "/some/file/that/should/be/ignored", "") if writer != buf { t.Errorf("expected: %v, saw: %v", buf, writer) } }) } } func TestSetupOutputWriterFile(t *testing.T) { file := "output" extension := ".json" dir, err := os.MkdirTemp(os.TempDir(), "out") if err != nil { t.Errorf("unexpected error: %v", err) } fullPath := filepath.Join(dir, file) + extension defer os.RemoveAll(dir) _, _, buf, _ := genericiooptions.NewTestIOStreams() f := cmdtesting.NewTestFactory() defer f.Cleanup() writer := setupOutputWriter(dir, buf, file, extension) if writer == buf { t.Errorf("expected: %v, saw: %v", buf, writer) } output := "some data here" writer.Write([]byte(output)) data, err := os.ReadFile(fullPath) if err != nil { t.Errorf("unexpected error: %v", err) } if string(data) != output { t.Errorf("expected: %v, saw: %v", output, data) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go000066400000000000000000000462011504711711200257040ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "fmt" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "syscall" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" cliflag "k8s.io/component-base/cli/flag" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/cmd/annotate" "k8s.io/kubectl/pkg/cmd/apiresources" "k8s.io/kubectl/pkg/cmd/apply" "k8s.io/kubectl/pkg/cmd/attach" "k8s.io/kubectl/pkg/cmd/auth" "k8s.io/kubectl/pkg/cmd/autoscale" "k8s.io/kubectl/pkg/cmd/certificates" "k8s.io/kubectl/pkg/cmd/clusterinfo" "k8s.io/kubectl/pkg/cmd/completion" cmdconfig "k8s.io/kubectl/pkg/cmd/config" "k8s.io/kubectl/pkg/cmd/cp" "k8s.io/kubectl/pkg/cmd/create" "k8s.io/kubectl/pkg/cmd/debug" "k8s.io/kubectl/pkg/cmd/delete" "k8s.io/kubectl/pkg/cmd/describe" "k8s.io/kubectl/pkg/cmd/diff" "k8s.io/kubectl/pkg/cmd/drain" "k8s.io/kubectl/pkg/cmd/edit" "k8s.io/kubectl/pkg/cmd/events" cmdexec "k8s.io/kubectl/pkg/cmd/exec" "k8s.io/kubectl/pkg/cmd/explain" "k8s.io/kubectl/pkg/cmd/expose" "k8s.io/kubectl/pkg/cmd/get" "k8s.io/kubectl/pkg/cmd/label" "k8s.io/kubectl/pkg/cmd/logs" "k8s.io/kubectl/pkg/cmd/options" "k8s.io/kubectl/pkg/cmd/patch" "k8s.io/kubectl/pkg/cmd/plugin" "k8s.io/kubectl/pkg/cmd/portforward" "k8s.io/kubectl/pkg/cmd/proxy" "k8s.io/kubectl/pkg/cmd/replace" "k8s.io/kubectl/pkg/cmd/rollout" "k8s.io/kubectl/pkg/cmd/run" "k8s.io/kubectl/pkg/cmd/scale" "k8s.io/kubectl/pkg/cmd/set" "k8s.io/kubectl/pkg/cmd/taint" "k8s.io/kubectl/pkg/cmd/top" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/version" "k8s.io/kubectl/pkg/cmd/wait" "k8s.io/kubectl/pkg/kuberc" utilcomp "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/kubectl/pkg/cmd/kustomize" ) const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS" type KubectlOptions struct { PluginHandler PluginHandler Arguments []string ConfigFlags *genericclioptions.ConfigFlags genericiooptions.IOStreams } func defaultConfigFlags() *genericclioptions.ConfigFlags { return genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0) } // NewDefaultKubectlCommand creates the `kubectl` command with default arguments func NewDefaultKubectlCommand() *cobra.Command { ioStreams := genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} return NewDefaultKubectlCommandWithArgs(KubectlOptions{ PluginHandler: NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), Arguments: os.Args, ConfigFlags: defaultConfigFlags().WithWarningPrinter(ioStreams), IOStreams: ioStreams, }) } // NewDefaultKubectlCommandWithArgs creates the `kubectl` command with arguments func NewDefaultKubectlCommandWithArgs(o KubectlOptions) *cobra.Command { cmd := NewKubectlCommand(o) if o.PluginHandler == nil { return cmd } if len(o.Arguments) > 1 { cmdPathPieces := o.Arguments[1:] // only look for suitable extension executables if // the specified command does not already exist if foundCmd, foundArgs, err := cmd.Find(cmdPathPieces); err != nil { // Also check the commands that will be added by Cobra. // These commands are only added once rootCmd.Execute() is called, so we // need to check them explicitly here. var cmdName string // first "non-flag" arguments for _, arg := range cmdPathPieces { if !strings.HasPrefix(arg, "-") { cmdName = arg break } } switch cmdName { case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd: // Don't search for a plugin default: if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, 1); err != nil { fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err) os.Exit(1) } } } else if err == nil { if !cmdutil.CmdPluginAsSubcommand.IsDisabled() { // Command exists(e.g. kubectl create), but it is not certain that // subcommand also exists (e.g. kubectl create networkpolicy) // we also have to eliminate kubectl create -f if IsSubcommandPluginAllowed(foundCmd.Name()) && len(foundArgs) >= 1 && !strings.HasPrefix(foundArgs[0], "-") { subcommand := foundArgs[0] builtinSubcmdExist := false for _, subcmd := range foundCmd.Commands() { if subcmd.Name() == subcommand { builtinSubcmdExist = true break } } if !builtinSubcmdExist { if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, len(cmdPathPieces)-len(foundArgs)+1); err != nil { fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err) os.Exit(1) } } } } } } return cmd } // IsSubcommandPluginAllowed returns the given command is allowed // to use plugin as subcommand if the subcommand does not exist as builtin. func IsSubcommandPluginAllowed(foundCmd string) bool { allowedCmds := map[string]struct{}{"create": {}} _, ok := allowedCmds[foundCmd] return ok } // PluginHandler is capable of parsing command line arguments // and performing executable filename lookups to search // for valid plugin files, and execute found plugins. type PluginHandler interface { // exists at the given filename, or a boolean false. // Lookup will iterate over a list of given prefixes // in order to recognize valid plugin filenames. // The first filepath to match a prefix is returned. Lookup(filename string) (string, bool) // Execute receives an executable's filepath, a slice // of arguments, and a slice of environment variables // to relay to the executable. Execute(executablePath string, cmdArgs, environment []string) error } // DefaultPluginHandler implements PluginHandler type DefaultPluginHandler struct { ValidPrefixes []string } // NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of // given filename prefixes used to identify valid plugin filenames. func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler { return &DefaultPluginHandler{ ValidPrefixes: validPrefixes, } } // Lookup implements PluginHandler func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) { for _, prefix := range h.ValidPrefixes { path, err := exec.LookPath(fmt.Sprintf("%s-%s", prefix, filename)) if shouldSkipOnLookPathErr(err) || len(path) == 0 { continue } return path, true } return "", false } func Command(name string, arg ...string) *exec.Cmd { cmd := &exec.Cmd{ Path: name, Args: append([]string{name}, arg...), } if filepath.Base(name) == name { lp, err := exec.LookPath(name) if lp != "" && !shouldSkipOnLookPathErr(err) { // Update cmd.Path even if err is non-nil. // If err is ErrDot (especially on Windows), lp may include a resolved // extension (like .exe or .bat) that should be preserved. cmd.Path = lp } } return cmd } // Execute implements PluginHandler func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { // Windows does not support exec syscall. if runtime.GOOS == "windows" { cmd := Command(executablePath, cmdArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Env = environment err := cmd.Run() if err == nil { os.Exit(0) } return err } // invoke cmd binary relaying the environment and args given // append executablePath to cmdArgs, as execve will make first argument the "binary name". return syscall.Exec(executablePath, append([]string{executablePath}, cmdArgs...), environment) } // HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find // a plugin executable on the PATH that satisfies the given arguments. func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, minArgs int) error { var remainingArgs []string // all "non-flag" arguments for _, arg := range cmdArgs { if strings.HasPrefix(arg, "-") { break } remainingArgs = append(remainingArgs, strings.Replace(arg, "-", "_", -1)) } if len(remainingArgs) == 0 { // the length of cmdArgs is at least 1 return fmt.Errorf("flags cannot be placed before plugin name: %s", cmdArgs[0]) } foundBinaryPath := "" // attempt to find binary, starting at longest possible name with given cmdArgs for len(remainingArgs) > 0 { path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-")) if !found { remainingArgs = remainingArgs[:len(remainingArgs)-1] if len(remainingArgs) < minArgs { // we shouldn't continue searching with shorter names. // this is especially for not searching kubectl-create plugin // when kubectl-create-foo plugin is not found. break } continue } foundBinaryPath = path break } if len(foundBinaryPath) == 0 { return nil } // invoke cmd binary relaying the current environment and args given if err := pluginHandler.Execute(foundBinaryPath, cmdArgs[len(remainingArgs):], os.Environ()); err != nil { return err } return nil } // NewKubectlCommand creates the `kubectl` command and its nested children. func NewKubectlCommand(o KubectlOptions) *cobra.Command { warningHandler := rest.NewWarningWriter(o.IOStreams.ErrOut, rest.WarningWriterOptions{Deduplicate: true, Color: term.AllowsColorOutput(o.IOStreams.ErrOut)}) warningsAsErrors := false // Parent command to which all subcommands are added. cmds := &cobra.Command{ Use: "kubectl", Short: i18n.T("kubectl controls the Kubernetes cluster manager"), Long: templates.LongDesc(` kubectl controls the Kubernetes cluster manager. Find more information at: https://kubernetes.io/docs/reference/kubectl/`), Run: runHelp, // Hook before and after Run initialize and write profiles to disk, // respectively. PersistentPreRunE: func(cmd *cobra.Command, args []string) error { rest.SetDefaultWarningHandler(warningHandler) if cmd.Name() == cobra.ShellCompRequestCmd { // This is the __complete or __completeNoDesc command which // indicates shell completion has been requested. plugin.SetupPluginCompletion(cmd, args) } return initProfiling() }, PersistentPostRunE: func(*cobra.Command, []string) error { if err := flushProfiling(); err != nil { return err } if warningsAsErrors { count := warningHandler.WarningCount() switch count { case 0: // no warnings case 1: return fmt.Errorf("%d warning received", count) default: return fmt.Errorf("%d warnings received", count) } } return nil }, } // From this point and forward we get warnings on flags that contain "_" separators // when adding them with hyphen instead of the original name. cmds.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc) flags := cmds.PersistentFlags() addProfilingFlags(flags) flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code") pref := kuberc.NewPreferences() if cmdutil.KubeRC.IsEnabled() { pref.AddFlags(flags) } kubeConfigFlags := o.ConfigFlags if kubeConfigFlags == nil { kubeConfigFlags = defaultConfigFlags().WithWarningPrinter(o.IOStreams) } kubeConfigFlags.AddFlags(flags) matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) matchVersionKubeConfigFlags.AddFlags(flags) // Updates hooks to add kubectl command headers: SIG CLI KEP 859. addCmdHeaderHooks(cmds, kubeConfigFlags) f := cmdutil.NewFactory(matchVersionKubeConfigFlags) // Proxy command is incompatible with CommandHeaderRoundTripper, so // clear the WrapConfigFn before running proxy command. proxyCmd := proxy.NewCmdProxy(f, o.IOStreams) proxyCmd.PreRun = func(cmd *cobra.Command, args []string) { kubeConfigFlags.WrapConfigFn = nil } // Avoid import cycle by setting ValidArgsFunction here instead of in NewCmdGet() getCmd := get.NewCmdGet("kubectl", f, o.IOStreams) getCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f) debugCmd := debug.NewCmdDebug(f, o.IOStreams) debugCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f) groups := templates.CommandGroups{ { Message: "Basic Commands (Beginner):", Commands: []*cobra.Command{ create.NewCmdCreate(f, o.IOStreams), expose.NewCmdExposeService(f, o.IOStreams), run.NewCmdRun(f, o.IOStreams), set.NewCmdSet(f, o.IOStreams), }, }, { Message: "Basic Commands (Intermediate):", Commands: []*cobra.Command{ explain.NewCmdExplain("kubectl", f, o.IOStreams), getCmd, edit.NewCmdEdit(f, o.IOStreams), delete.NewCmdDelete(f, o.IOStreams), }, }, { Message: "Deploy Commands:", Commands: []*cobra.Command{ rollout.NewCmdRollout(f, o.IOStreams), scale.NewCmdScale(f, o.IOStreams), autoscale.NewCmdAutoscale(f, o.IOStreams), }, }, { Message: "Cluster Management Commands:", Commands: []*cobra.Command{ certificates.NewCmdCertificate(f, o.IOStreams), clusterinfo.NewCmdClusterInfo(f, o.IOStreams), top.NewCmdTop(f, o.IOStreams), drain.NewCmdCordon(f, o.IOStreams), drain.NewCmdUncordon(f, o.IOStreams), drain.NewCmdDrain(f, o.IOStreams), taint.NewCmdTaint(f, o.IOStreams), }, }, { Message: "Troubleshooting and Debugging Commands:", Commands: []*cobra.Command{ describe.NewCmdDescribe("kubectl", f, o.IOStreams), logs.NewCmdLogs(f, o.IOStreams), attach.NewCmdAttach(f, o.IOStreams), cmdexec.NewCmdExec(f, o.IOStreams), portforward.NewCmdPortForward(f, o.IOStreams), proxyCmd, cp.NewCmdCp(f, o.IOStreams), auth.NewCmdAuth(f, o.IOStreams), debugCmd, events.NewCmdEvents(f, o.IOStreams), }, }, { Message: "Advanced Commands:", Commands: []*cobra.Command{ diff.NewCmdDiff(f, o.IOStreams), apply.NewCmdApply("kubectl", f, o.IOStreams), patch.NewCmdPatch(f, o.IOStreams), replace.NewCmdReplace(f, o.IOStreams), wait.NewCmdWait(f, o.IOStreams), kustomize.NewCmdKustomize(o.IOStreams), }, }, { Message: "Settings Commands:", Commands: []*cobra.Command{ label.NewCmdLabel(f, o.IOStreams), annotate.NewCmdAnnotate("kubectl", f, o.IOStreams), completion.NewCmdCompletion(o.IOStreams.Out, ""), }, }, } groups.Add(cmds) filters := []string{"options"} // Hide the "alpha" subcommand if there are no alpha commands in this build. alpha := NewCmdAlpha(f, o.IOStreams) if !alpha.HasSubCommands() { filters = append(filters, alpha.Name()) } // Add plugin command group to the list of command groups. // The commands are only injected for the scope of showing help and completion, they are not // invoked directly. pluginCommandGroup := plugin.GetPluginCommandGroup(cmds) groups = append(groups, pluginCommandGroup) templates.ActsAsRootCommand(cmds, filters, groups...) utilcomp.SetFactoryForCompletion(f) registerCompletionFuncForGlobalFlags(cmds, f) cmds.AddCommand(alpha) cmds.AddCommand(cmdconfig.NewCmdConfig(f, clientcmd.NewDefaultPathOptions(), o.IOStreams)) cmds.AddCommand(plugin.NewCmdPlugin(o.IOStreams)) cmds.AddCommand(version.NewCmdVersion(f, o.IOStreams)) cmds.AddCommand(apiresources.NewCmdAPIVersions(f, o.IOStreams)) cmds.AddCommand(apiresources.NewCmdAPIResources(f, o.IOStreams)) cmds.AddCommand(options.NewCmdOptions(o.IOStreams.Out)) // Stop warning about normalization of flags. That makes it possible to // add the klog flags later. cmds.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc) if cmdutil.KubeRC.IsEnabled() { _, err := pref.Apply(cmds, o.Arguments, o.IOStreams.ErrOut) if err != nil { fmt.Fprintf(o.IOStreams.ErrOut, "error occurred while applying preferences %v\n", err) os.Exit(1) } } return cmds } // addCmdHeaderHooks performs updates on two hooks: // 1. Modifies the passed "cmds" persistent pre-run function to parse command headers. // These headers will be subsequently added as X-headers to every // REST call. // 2. Adds CommandHeaderRoundTripper as a wrapper around the standard // RoundTripper. CommandHeaderRoundTripper adds X-Headers then delegates // to standard RoundTripper. // // For beta, these hooks are updated unless the KUBECTL_COMMAND_HEADERS environment variable // is set, and the value of the env var is false (or zero). // See SIG CLI KEP 859 for more information: // // https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags) { // If the feature gate env var is set to "false", then do no add kubectl command headers. if value, exists := os.LookupEnv(kubectlCmdHeaders); exists { if value == "false" || value == "0" { klog.V(5).Infoln("kubectl command headers turned off") return } } klog.V(5).Infoln("kubectl command headers turned on") crt := &genericclioptions.CommandHeaderRoundTripper{} existingPreRunE := cmds.PersistentPreRunE // Add command parsing to the existing persistent pre-run function. cmds.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { crt.ParseCommandHeaders(cmd, args) return existingPreRunE(cmd, args) } wrapConfigFn := kubeConfigFlags.WrapConfigFn // Wraps CommandHeaderRoundTripper around standard RoundTripper. kubeConfigFlags.WrapConfigFn = func(c *rest.Config) *rest.Config { if wrapConfigFn != nil { c = wrapConfigFn(c) } c.Wrap(func(rt http.RoundTripper) http.RoundTripper { // Must be separate RoundTripper; not "crt" closure. // Fixes: https://github.com/kubernetes/kubectl/issues/1098 return &genericclioptions.CommandHeaderRoundTripper{ Delegate: rt, Headers: crt.Headers, } }) return c } } func runHelp(cmd *cobra.Command, args []string) { cmd.Help() } func registerCompletionFuncForGlobalFlags(cmd *cobra.Command, f cmdutil.Factory) { cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( "namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return utilcomp.CompGetResource(f, "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp })) cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( "context", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return utilcomp.ListContextsInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp })) cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( "cluster", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return utilcomp.ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp })) cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( "user", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return utilcomp.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp })) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/cmd_test.go000066400000000000000000000374501504711711200267510ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "fmt" "os" "reflect" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/kubectl/pkg/cmd/plugin" ) func TestNormalizationFuncGlobalExistence(t *testing.T) { // This test can be safely deleted when we will not support multiple flag formats root := NewKubectlCommand(KubectlOptions{IOStreams: genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}}) if root.Parent() != nil { t.Fatal("We expect the root command to be returned") } if root.GlobalNormalizationFunc() == nil { t.Fatal("We expect that root command has a global normalization function") } if reflect.ValueOf(root.GlobalNormalizationFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() { t.Fatal("root command seems to have a wrong normalization function") } sub := root for sub.HasSubCommands() { sub = sub.Commands()[0] } // In case of failure of this test check this PR: spf13/cobra#110 if reflect.ValueOf(sub.Flags().GetNormalizeFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() { t.Fatal("child and root commands should have the same normalization functions") } } func TestKubectlSubcommandShadowPlugin(t *testing.T) { tests := []struct { name string args []string expectPlugin string expectPluginArgs []string expectLookupError string }{ { name: "test that a plugin executable is found based on command args when builtin subcommand does not exist", args: []string{"kubectl", "create", "foo", "--bar", "--bar2", "--namespace", "test-namespace"}, expectPlugin: "plugin/testdata/kubectl-create-foo", expectPluginArgs: []string{"--bar", "--bar2", "--namespace", "test-namespace"}, }, { name: "test that a plugin executable is not found based on command args when also builtin subcommand does not exist", args: []string{"kubectl", "create", "foo2", "--bar", "--bar2", "--namespace", "test-namespace"}, expectLookupError: "unable to find a plugin executable \"kubectl-create-foo2\"", }, { name: "test that normal commands are able to be executed, when builtin subcommand exists", args: []string{"kubectl", "create", "job", "foo", "--image=busybox", "--dry-run=client", "--namespace", "test-namespace"}, expectPlugin: "", expectPluginArgs: []string{}, }, // rest of the tests are copied from TestKubectlCommandHandlesPlugins function, // just to retest them also when feature is enabled. { name: "test that normal commands are able to be executed, when no plugin overshadows them", args: []string{"kubectl", "config", "get-clusters"}, expectPlugin: "", expectPluginArgs: []string{}, }, { name: "test that a plugin executable is found based on command args", args: []string{"kubectl", "foo", "--bar"}, expectPlugin: "plugin/testdata/kubectl-foo", expectPluginArgs: []string{"--bar"}, }, { name: "test that a plugin does not execute over an existing command by the same name", args: []string{"kubectl", "version", "--client=true"}, }, { name: "test that a plugin does not execute over Cobra's help command", args: []string{"kubectl", "help"}, }, { name: "test that a plugin does not execute over Cobra's __complete command", args: []string{"kubectl", cobra.ShellCompRequestCmd, "de"}, }, { name: "test that a plugin does not execute over Cobra's __completeNoDesc command", args: []string{"kubectl", cobra.ShellCompNoDescRequestCmd, "de"}, }, { name: "test that a flag does not break Cobra's help command", args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", "help"}, }, { name: "test that a flag does not break Cobra's __complete command", args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompRequestCmd}, }, { name: "test that a flag does not break Cobra's __completeNoDesc command", args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { pluginsHandler := &testPluginHandler{ pluginsDirectory: "plugin/testdata", validPrefixes: plugin.ValidPluginFilenamePrefixes, } ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams}) // original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow if !pluginsHandler.lookedup && !pluginsHandler.executed { // args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed // to the command which might invoke only "kubectl" without any additional args and give false positives root.SetArgs(test.args[1:]) // Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test if err := root.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) } } if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) || (pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) { t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr) } if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 { // we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command) t.Fatalf("expected plugin execution, but did not occur") } if pluginsHandler.executedPlugin != test.expectPlugin { t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin) } if pluginsHandler.executed && len(test.expectPlugin) == 0 { t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin) } if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) { t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs) } }) } } func TestKubectlCommandHandlesPlugins(t *testing.T) { tests := []struct { name string args []string expectPlugin string expectPluginArgs []string expectLookupError string }{ { name: "test that normal commands are able to be executed, when no plugin overshadows them", args: []string{"kubectl", "config", "get-clusters"}, expectPlugin: "", expectPluginArgs: []string{}, }, { name: "test that normal commands are able to be executed, when no plugin overshadows them and shadowing feature is not enabled", args: []string{"kubectl", "create", "job", "foo", "--image=busybox", "--dry-run=client"}, expectPlugin: "", expectPluginArgs: []string{}, }, { name: "test that a plugin executable is found based on command args", args: []string{"kubectl", "foo", "--bar"}, expectPlugin: "plugin/testdata/kubectl-foo", expectPluginArgs: []string{"--bar"}, }, { name: "test that a plugin executable is found based on command args with positional argument", args: []string{"kubectl", "foo", "positional", "--bar"}, expectPlugin: "plugin/testdata/kubectl-foo", expectPluginArgs: []string{"positional", "--bar"}, }, { name: "test that an allowed subcommand plugin executable is found based on command args with positional argument", args: []string{"kubectl", "create", "foo", "positional", "--bar"}, expectPlugin: "plugin/testdata/kubectl-create-foo", expectPluginArgs: []string{"positional", "--bar"}, }, { name: "test that a plugin does not execute over an existing command by the same name", args: []string{"kubectl", "version", "--client=true"}, }, // The following tests make sure that commands added by Cobra cannot be shadowed by a plugin // See https://github.com/kubernetes/kubectl/issues/1116 { name: "test that a plugin does not execute over Cobra's help command", args: []string{"kubectl", "help"}, }, { name: "test that a plugin does not execute over Cobra's __complete command", args: []string{"kubectl", cobra.ShellCompRequestCmd, "de"}, }, { name: "test that a plugin does not execute over Cobra's __completeNoDesc command", args: []string{"kubectl", cobra.ShellCompNoDescRequestCmd, "de"}, }, // The following tests make sure that commands added by Cobra cannot be shadowed by a plugin // even when a flag is specified first. This can happen when using aliases. // See https://github.com/kubernetes/kubectl/issues/1119 { name: "test that a flag does not break Cobra's help command", args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", "help"}, }, { name: "test that a flag does not break Cobra's __complete command", args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompRequestCmd}, }, { name: "test that a flag does not break Cobra's __completeNoDesc command", args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd}, }, // As for the previous tests, an alias could add a flag without using the = form. // We don't support this case as parsing the flags becomes quite complicated (flags // that take a value, versus flags that don't) // { // name: "test that a flag with a space does not break Cobra's help command", // args: []string{"kubectl", "--kubeconfig", "/path/to/kubeconfig", "help"}, // }, // { // name: "test that a flag with a space does not break Cobra's __complete command", // args: []string{"kubectl", "--kubeconfig", "/path/to/kubeconfig", cobra.ShellCompRequestCmd}, // }, // { // name: "test that a flag with a space does not break Cobra's __completeNoDesc command", // args: []string{"kubectl", "--kubeconfig", "/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd}, // }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { pluginsHandler := &testPluginHandler{ pluginsDirectory: "plugin/testdata", validPrefixes: plugin.ValidPluginFilenamePrefixes, } ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams}) // original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow if !pluginsHandler.lookedup && !pluginsHandler.executed { // args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed // to the command which might invoke only "kubectl" without any additional args and give false positives root.SetArgs(test.args[1:]) // Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test if err := root.Execute(); err != nil { t.Fatalf("unexpected error: %v", err) } } if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) || (pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) { t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr) } if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 { // we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command) t.Fatalf("expected plugin execution, but did not occur") } if pluginsHandler.executedPlugin != test.expectPlugin { t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin) } if pluginsHandler.executed && len(test.expectPlugin) == 0 { t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin) } if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) { t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs) } }) } } type testPluginHandler struct { pluginsDirectory string validPrefixes []string // lookup results lookedup bool lookupErr error // execution results executed bool executedPlugin string withArgs []string withEnv []string } func (h *testPluginHandler) Lookup(filename string) (string, bool) { h.lookedup = true dir, err := os.Stat(h.pluginsDirectory) if err != nil { h.lookupErr = err return "", false } if !dir.IsDir() { h.lookupErr = fmt.Errorf("expected %q to be a directory", h.pluginsDirectory) return "", false } plugins, err := os.ReadDir(h.pluginsDirectory) if err != nil { h.lookupErr = err return "", false } filenameWithSuportedPrefix := "" for _, prefix := range h.validPrefixes { for _, p := range plugins { filenameWithSuportedPrefix = fmt.Sprintf("%s-%s", prefix, filename) if p.Name() == filenameWithSuportedPrefix { h.lookupErr = nil return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), true } } } h.lookupErr = fmt.Errorf("unable to find a plugin executable %q", filenameWithSuportedPrefix) return "", false } func (h *testPluginHandler) Execute(executablePath string, cmdArgs, env []string) error { h.executed = true h.executedPlugin = executablePath h.withArgs = cmdArgs h.withEnv = env return nil } func TestKubectlCommandHeadersHooks(t *testing.T) { tests := map[string]struct { envVar string addsHooks bool }{ "empty environment variable; hooks added": { envVar: "", addsHooks: true, }, "random env var value; hooks added": { envVar: "foo", addsHooks: true, }, "true env var value; hooks added": { envVar: "true", addsHooks: true, }, "false env var value; hooks NOT added": { envVar: "false", addsHooks: false, }, "zero env var value; hooks NOT added": { envVar: "0", addsHooks: false, }, } for name, testCase := range tests { t.Run(name, func(t *testing.T) { cmds := &cobra.Command{} kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() if kubeConfigFlags.WrapConfigFn != nil { t.Fatal("expected initial nil WrapConfigFn") } t.Setenv(kubectlCmdHeaders, testCase.envVar) addCmdHeaderHooks(cmds, kubeConfigFlags) // Valdidate whether the hooks were added. if testCase.addsHooks && kubeConfigFlags.WrapConfigFn == nil { t.Error("after adding kubectl command header, expecting non-nil WrapConfigFn") } if !testCase.addsHooks && kubeConfigFlags.WrapConfigFn != nil { t.Error("env var feature gate should have blocked setting WrapConfigFn") } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/completion/000077500000000000000000000000001504711711200267605ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go000066400000000000000000000157741504711711200314760ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import ( "fmt" "io" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) const defaultBoilerPlate = ` # Copyright 2016 The Kubernetes Authors. # # 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. ` var ( completionLong = templates.LongDesc(i18n.T(` Output shell completion code for the specified shell (bash, zsh, fish, or powershell). The shell code must be evaluated to provide interactive completion of kubectl commands. This can be done by sourcing it from the .bash_profile. Detailed instructions on how to do this are available here: for macOS: https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/#enable-shell-autocompletion for linux: https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#enable-shell-autocompletion for windows: https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/#enable-shell-autocompletion Note for zsh users: [1] zsh completions are only supported in versions of zsh >= 5.2.`)) completionExample = templates.Examples(i18n.T(` # Installing bash completion on macOS using homebrew ## If running Bash 3.2 included with macOS brew install bash-completion ## or, if running Bash 4.1+ brew install bash-completion@2 ## If kubectl is installed via homebrew, this should start working immediately ## If you've installed via other means, you may need add the completion to your completion directory kubectl completion bash > $(brew --prefix)/etc/bash_completion.d/kubectl # Installing bash completion on Linux ## If bash-completion is not installed on Linux, install the 'bash-completion' package ## via your distribution's package manager. ## Load the kubectl completion code for bash into the current shell source <(kubectl completion bash) ## Write bash completion code to a file and source it from .bash_profile kubectl completion bash > ~/.kube/completion.bash.inc printf " # kubectl shell completion source '$HOME/.kube/completion.bash.inc' " >> $HOME/.bash_profile source $HOME/.bash_profile # Load the kubectl completion code for zsh[1] into the current shell source <(kubectl completion zsh) # Set the kubectl completion code for zsh[1] to autoload on startup kubectl completion zsh > "${fpath[1]}/_kubectl" # Load the kubectl completion code for fish[2] into the current shell kubectl completion fish | source # To load completions for each session, execute once: kubectl completion fish > ~/.config/fish/completions/kubectl.fish # Load the kubectl completion code for powershell into the current shell kubectl completion powershell | Out-String | Invoke-Expression # Set kubectl completion code for powershell to run on startup ## Save completion code to a script and execute in the profile kubectl completion powershell > $HOME\.kube\completion.ps1 Add-Content $PROFILE "$HOME\.kube\completion.ps1" ## Execute completion code in the profile Add-Content $PROFILE "if (Get-Command kubectl -ErrorAction SilentlyContinue) { kubectl completion powershell | Out-String | Invoke-Expression }" ## Add completion code directly to the $PROFILE script kubectl completion powershell >> $PROFILE`)) ) var ( completionShells = map[string]func(out io.Writer, boilerPlate string, cmd *cobra.Command) error{ "bash": runCompletionBash, "zsh": runCompletionZsh, "fish": runCompletionFish, "powershell": runCompletionPwsh, } ) // NewCmdCompletion creates the `completion` command func NewCmdCompletion(out io.Writer, boilerPlate string) *cobra.Command { shells := []string{} for s := range completionShells { shells = append(shells, s) } cmd := &cobra.Command{ Use: "completion SHELL", DisableFlagsInUseLine: true, Short: i18n.T("Output shell completion code for the specified shell (bash, zsh, fish, or powershell)"), Long: completionLong, Example: completionExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(RunCompletion(out, boilerPlate, cmd, args)) }, ValidArgs: shells, } return cmd } // RunCompletion checks given arguments and executes command func RunCompletion(out io.Writer, boilerPlate string, cmd *cobra.Command, args []string) error { if len(args) == 0 { return cmdutil.UsageErrorf(cmd, "Shell not specified.") } if len(args) > 1 { return cmdutil.UsageErrorf(cmd, "Too many arguments. Expected only the shell type.") } run, found := completionShells[args[0]] if !found { return cmdutil.UsageErrorf(cmd, "Unsupported shell type %q.", args[0]) } return run(out, boilerPlate, cmd.Parent()) } func runCompletionBash(out io.Writer, boilerPlate string, kubectl *cobra.Command) error { if len(boilerPlate) == 0 { boilerPlate = defaultBoilerPlate } if _, err := out.Write([]byte(boilerPlate)); err != nil { return err } return kubectl.GenBashCompletionV2(out, true) } func runCompletionZsh(out io.Writer, boilerPlate string, kubectl *cobra.Command) error { zshHead := fmt.Sprintf("#compdef %[1]s\ncompdef _%[1]s %[1]s\n", kubectl.Name()) out.Write([]byte(zshHead)) if len(boilerPlate) == 0 { boilerPlate = defaultBoilerPlate } if _, err := out.Write([]byte(boilerPlate)); err != nil { return err } return kubectl.GenZshCompletion(out) } func runCompletionFish(out io.Writer, boilerPlate string, kubectl *cobra.Command) error { if len(boilerPlate) == 0 { boilerPlate = defaultBoilerPlate } if _, err := out.Write([]byte(boilerPlate)); err != nil { return err } return kubectl.GenFishCompletion(out, true) } func runCompletionPwsh(out io.Writer, boilerPlate string, kubectl *cobra.Command) error { if len(boilerPlate) == 0 { boilerPlate = defaultBoilerPlate } if _, err := out.Write([]byte(boilerPlate)); err != nil { return err } return kubectl.GenPowerShellCompletionWithDesc(out) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion_test.go000066400000000000000000000046141504711711200325240ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package completion import ( "strings" "testing" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" ) func TestBashCompletions(t *testing.T) { testCases := []struct { name string args []string expectedError string }{ { name: "bash", args: []string{"bash"}, }, { name: "zsh", args: []string{"zsh"}, }, { name: "fish", args: []string{"fish"}, }, { name: "powershell", args: []string{"powershell"}, }, { name: "no args", args: []string{}, expectedError: `Shell not specified. See 'kubectl completion -h' for help and examples`, }, { name: "too many args", args: []string{"bash", "zsh"}, expectedError: `Too many arguments. Expected only the shell type. See 'kubectl completion -h' for help and examples`, }, { name: "unsupported shell", args: []string{"foo"}, expectedError: `Unsupported shell type "foo". See 'kubectl completion -h' for help and examples`, }, } for _, tc := range testCases { t.Run(tc.name, func(tt *testing.T) { _, _, out, _ := genericiooptions.NewTestIOStreams() parentCmd := &cobra.Command{ Use: "kubectl", } cmd := NewCmdCompletion(out, defaultBoilerPlate) parentCmd.AddCommand(cmd) err := RunCompletion(out, defaultBoilerPlate, cmd, tc.args) if tc.expectedError == "" { if err != nil { tt.Fatalf("Unexpected error: %v", err) } if out.Len() == 0 { tt.Fatalf("Output was not written") } if !strings.Contains(out.String(), defaultBoilerPlate) { tt.Fatalf("Output does not contain boilerplate:\n%s", out.String()) } } else { if err == nil { tt.Fatalf("An error was expected but no error was returned") } if err.Error() != tc.expectedError { tt.Fatalf("Unexpected error: %v\nexpected: %v\n", err, tc.expectedError) } } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/000077500000000000000000000000001504711711200260545ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/config.go000066400000000000000000000077671504711711200276710ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "path" "strconv" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // NewCmdConfig creates a command object for the "config" action, and adds all child commands to it. func NewCmdConfig(restClientGetter genericclioptions.RESTClientGetter, pathOptions *clientcmd.PathOptions, streams genericiooptions.IOStreams) *cobra.Command { if len(pathOptions.ExplicitFileFlag) == 0 { pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag } cmd := &cobra.Command{ Use: "config SUBCOMMAND", DisableFlagsInUseLine: true, Short: i18n.T("Modify kubeconfig files"), Long: templates.LongDesc(i18n.T(` Modify kubeconfig files using subcommands like "kubectl config set current-context my-context". The loading order follows these rules: 1. If the --`) + pathOptions.ExplicitFileFlag + i18n.T(` flag is set, then only that file is loaded. The flag may only be set once and no merging takes place. 2. If $`) + pathOptions.EnvVar + i18n.T(` environment variable is set, then it is used as a list of paths (normal path delimiting rules for your system). These paths are merged. When a value is modified, it is modified in the file that defines the stanza. When a value is created, it is created in the first file that exists. If no files in the chain exist, then it creates the last file in the list. 3. Otherwise, `) + path.Join("${HOME}", pathOptions.GlobalFileSubpath) + i18n.T(` is used and no merging takes place.`)), Run: cmdutil.DefaultSubCommandRun(streams.ErrOut), } // file paths are common to all sub commands cmd.PersistentFlags().StringVar(&pathOptions.LoadingRules.ExplicitPath, pathOptions.ExplicitFileFlag, pathOptions.LoadingRules.ExplicitPath, "use a particular kubeconfig file") // TODO(juanvallejo): update all subcommands to work with genericiooptions.IOStreams cmd.AddCommand(NewCmdConfigView(streams, pathOptions)) cmd.AddCommand(NewCmdConfigSetCluster(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigSetCredentials(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigSetContext(restClientGetter, streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigSet(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigUnset(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigCurrentContext(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigUseContext(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigGetContexts(streams, pathOptions)) cmd.AddCommand(NewCmdConfigGetClusters(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigGetUsers(streams, pathOptions)) cmd.AddCommand(NewCmdConfigDeleteCluster(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigDeleteContext(streams.Out, streams.ErrOut, pathOptions)) cmd.AddCommand(NewCmdConfigDeleteUser(streams, pathOptions)) cmd.AddCommand(NewCmdConfigRenameContext(streams.Out, pathOptions)) return cmd } func toBool(propertyValue string) (bool, error) { boolValue := false if len(propertyValue) != 0 { var err error boolValue, err = strconv.ParseBool(propertyValue) if err != nil { return false, err } } return boolValue, nil } func helpErrorf(cmd *cobra.Command, format string, args ...interface{}) error { cmd.Help() msg := fmt.Sprintf(format, args...) return fmt.Errorf("%s", msg) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/config_test.go000066400000000000000000000761401504711711200307170ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "os" "path/filepath" "reflect" "strings" "testing" "github.com/google/go-cmp/cmp" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" utiltesting "k8s.io/client-go/util/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) func newRedFederalCowHammerConfig() clientcmdapi.Config { return clientcmdapi.Config{ AuthInfos: map[string]*clientcmdapi.AuthInfo{ "red-user": {Token: "red-token"}}, Clusters: map[string]*clientcmdapi.Cluster{ "cow-cluster": {Server: "http://cow.org:8080"}}, Contexts: map[string]*clientcmdapi.Context{ "federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster"}}, CurrentContext: "federal-context", } } func Example_view() { expectedConfig := newRedFederalCowHammerConfig() test := configCommandTest{ args: []string{"view"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } output := test.run(&testing.T{}) fmt.Printf("%v", output) // Output: // apiVersion: v1 // clusters: // - cluster: // server: http://cow.org:8080 // name: cow-cluster // contexts: // - context: // cluster: cow-cluster // user: red-user // name: federal-context // current-context: federal-context // kind: Config // preferences: {} // users: // - name: red-user // user: // token: REDACTED } func TestCurrentContext(t *testing.T) { startingConfig := newRedFederalCowHammerConfig() test := configCommandTest{ args: []string{"current-context"}, startingConfig: startingConfig, expectedConfig: startingConfig, expectedOutputs: []string{startingConfig.CurrentContext}, } test.run(t) } func TestSetCurrentContext(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() startingConfig := newRedFederalCowHammerConfig() newContextName := "the-new-context" startingConfig.Contexts[newContextName] = clientcmdapi.NewContext() expectedConfig.Contexts[newContextName] = clientcmdapi.NewContext() expectedConfig.CurrentContext = newContextName test := configCommandTest{ args: []string{"use-context", "the-new-context"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestSetNonExistentContext(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() test := configCommandTest{ args: []string{"use-context", "non-existent-config"}, startingConfig: expectedConfig, expectedConfig: expectedConfig, } func() { defer func() { // Restore cmdutil behavior. cmdutil.DefaultBehaviorOnFatal() }() // Check exit code. cmdutil.BehaviorOnFatal(func(e string, code int) { if code != 1 { t.Errorf("The exit code is %d, expected 1", code) } expectedOutputs := []string{`no context exists with the name: "non-existent-config"`} test.checkOutput(e, expectedOutputs, t) }) test.run(t) }() } func TestSetIntoExistingStruct(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() expectedConfig.AuthInfos["red-user"].Password = "new-path-value" // Fake value for testing. test := configCommandTest{ args: []string{"set", "users.red-user.password", "new-path-value"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestSetWithPathPrefixIntoExistingStruct(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["cow-cluster"].Server = "http://cow.org:8080/foo/baz" test := configCommandTest{ args: []string{"set", "clusters.cow-cluster.server", "http://cow.org:8080/foo/baz"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) dc := clientcmd.NewDefaultClientConfig(expectedConfig, &clientcmd.ConfigOverrides{}) dcc, err := dc.ClientConfig() if err != nil { t.Fatalf("unexpected error: %v", err) } expectedHost := "http://cow.org:8080/foo/baz" if expectedHost != dcc.Host { t.Fatalf("expected client.Config.Host = %q instead of %q", expectedHost, dcc.Host) } } func TestUnsetStruct(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() delete(expectedConfig.AuthInfos, "red-user") test := configCommandTest{ args: []string{"unset", "users.red-user"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestUnsetField(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() expectedConfig.AuthInfos["red-user"] = clientcmdapi.NewAuthInfo() test := configCommandTest{ args: []string{"unset", "users.red-user.token"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestSetIntoNewStruct(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() cluster := clientcmdapi.NewCluster() cluster.Server = "new-server-value" expectedConfig.Clusters["big-cluster"] = cluster test := configCommandTest{ args: []string{"set", "clusters.big-cluster.server", "new-server-value"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestSetBoolean(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() cluster := clientcmdapi.NewCluster() cluster.InsecureSkipTLSVerify = true expectedConfig.Clusters["big-cluster"] = cluster test := configCommandTest{ args: []string{"set", "clusters.big-cluster.insecure-skip-tls-verify", "true"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestSetIntoNewConfig(t *testing.T) { expectedConfig := *clientcmdapi.NewConfig() context := clientcmdapi.NewContext() context.AuthInfo = "fake-user" expectedConfig.Contexts["new-context"] = context test := configCommandTest{ args: []string{"set", "contexts.new-context.user", "fake-user"}, startingConfig: *clientcmdapi.NewConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestNewEmptyAuth(t *testing.T) { expectedConfig := *clientcmdapi.NewConfig() expectedConfig.AuthInfos["the-user-name"] = clientcmdapi.NewAuthInfo() test := configCommandTest{ args: []string{"set-credentials", "the-user-name"}, startingConfig: *clientcmdapi.NewConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestAdditionalAuth(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() authInfo.Token = "token" expectedConfig.AuthInfos["another-user"] = authInfo test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestEmbedClientCert(t *testing.T) { fakeCertFile, _ := os.CreateTemp(os.TempDir(), "") defer utiltesting.CloseAndRemove(t, fakeCertFile) fakeData := []byte("fake-data") os.WriteFile(fakeCertFile.Name(), fakeData, 0600) expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() authInfo.ClientCertificateData = fakeData expectedConfig.AuthInfos["another-user"] = authInfo test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=" + fakeCertFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestExecPlugin(t *testing.T) { fakeCertFile, _ := os.CreateTemp(os.TempDir(), "") defer utiltesting.CloseAndRemove(t, fakeCertFile) fakeData := []byte("fake-data") err := os.WriteFile(fakeCertFile.Name(), fakeData, 0600) if err != nil { t.Errorf("unexpected error %v", err) } expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() authInfo.Exec = &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", Args: []string{"arg1", "arg2"}, Env: []clientcmdapi.ExecEnvVar{ { Name: "FOO", Value: "bar", }, }, APIVersion: "client.authentication.k8s.io/v1", ProvideClusterInfo: false, InteractiveMode: "Never", } expectedConfig.AuthInfos["cred-exec-user"] = authInfo test := configCommandTest{ args: []string{ "set-credentials", "cred-exec-user", "--exec-api-version=client.authentication.k8s.io/v1", "--exec-command=example-client-go-exec-plugin", "--exec-arg=arg1,arg2", "--exec-env=FOO=bar", "--exec-interactive-mode=Never", }, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestExecPluginWithProveClusterInfo(t *testing.T) { fakeCertFile, _ := os.CreateTemp(os.TempDir(), "") defer utiltesting.CloseAndRemove(t, fakeCertFile) fakeData := []byte("fake-data") err := os.WriteFile(fakeCertFile.Name(), fakeData, 0600) if err != nil { t.Errorf("unexpected error %v", err) } expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() authInfo.Exec = &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", Args: []string{"arg1", "arg2"}, Env: []clientcmdapi.ExecEnvVar{ { Name: "FOO", Value: "bar", }, }, APIVersion: "client.authentication.k8s.io/v1", ProvideClusterInfo: true, InteractiveMode: "Always", } expectedConfig.AuthInfos["cred-exec-user"] = authInfo test := configCommandTest{ args: []string{ "set-credentials", "cred-exec-user", "--exec-api-version=client.authentication.k8s.io/v1", "--exec-command=example-client-go-exec-plugin", "--exec-arg=arg1,arg2", "--exec-env=FOO=bar", "--exec-interactive-mode=Always", "--exec-provide-cluster-info=true", }, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestEmbedClientKey(t *testing.T) { fakeKeyFile, _ := os.CreateTemp(os.TempDir(), "") defer utiltesting.CloseAndRemove(t, fakeKeyFile) fakeData := []byte("fake-data") os.WriteFile(fakeKeyFile.Name(), fakeData, 0600) expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() authInfo.ClientKeyData = fakeData expectedConfig.AuthInfos["another-user"] = authInfo test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagKeyFile + "=" + fakeKeyFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestEmbedNoKeyOrCertDisallowed(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagEmbedCerts + "=true"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } func() { defer func() { // Restore cmdutil behavior. cmdutil.DefaultBehaviorOnFatal() }() // Check exit code. cmdutil.BehaviorOnFatal(func(e string, code int) { if code != 1 { t.Errorf("The exit code is %d, expected 1", code) } expectedOutputs := []string{"--client-certificate", "--client-key", "embed"} test.checkOutput(e, expectedOutputs, t) }) test.run(t) }() } func TestEmptyTokenAndCertAllowed(t *testing.T) { fakeCertFile, _ := os.CreateTemp(os.TempDir(), "cert-file") defer utiltesting.CloseAndRemove(t, fakeCertFile) expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() authInfo.ClientCertificate = filepath.Base(fakeCertFile.Name()) expectedConfig.AuthInfos["another-user"] = authInfo test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=" + fakeCertFile.Name(), "--" + clientcmd.FlagBearerToken + "="}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestTokenAndCertAllowed(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() authInfo.Token = "token" authInfo.ClientCertificate = "/cert-file" expectedConfig.AuthInfos["another-user"] = authInfo test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=/cert-file", "--" + clientcmd.FlagBearerToken + "=token"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestTokenAndBasicDisallowed(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagUsername + "=myuser", "--" + clientcmd.FlagBearerToken + "=token"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } func() { defer func() { // Restore cmdutil behavior. cmdutil.DefaultBehaviorOnFatal() }() // Check exit code. cmdutil.BehaviorOnFatal(func(e string, code int) { if code != 1 { t.Errorf("The exit code is %d, expected 1", code) } expectedOutputs := []string{"--token", "--username"} test.checkOutput(e, expectedOutputs, t) }) test.run(t) }() } func TestBasicClearsToken(t *testing.T) { authInfoWithToken := clientcmdapi.NewAuthInfo() authInfoWithToken.Token = "token" authInfoWithBasic := clientcmdapi.NewAuthInfo() authInfoWithBasic.Username = "myuser" authInfoWithBasic.Password = "mypass" // Fake value for testing. startingConfig := newRedFederalCowHammerConfig() startingConfig.AuthInfos["another-user"] = authInfoWithToken expectedConfig := newRedFederalCowHammerConfig() expectedConfig.AuthInfos["another-user"] = authInfoWithBasic test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagUsername + "=myuser", "--" + clientcmd.FlagPassword + "=mypass"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestTokenClearsBasic(t *testing.T) { authInfoWithBasic := clientcmdapi.NewAuthInfo() authInfoWithBasic.Username = "myuser" authInfoWithBasic.Password = "mypass" // Fake value for testing. authInfoWithToken := clientcmdapi.NewAuthInfo() authInfoWithToken.Token = "token" startingConfig := newRedFederalCowHammerConfig() startingConfig.AuthInfos["another-user"] = authInfoWithBasic expectedConfig := newRedFederalCowHammerConfig() expectedConfig.AuthInfos["another-user"] = authInfoWithToken test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestTokenLeavesCert(t *testing.T) { authInfoWithCerts := clientcmdapi.NewAuthInfo() authInfoWithCerts.ClientCertificate = "cert" authInfoWithCerts.ClientCertificateData = []byte("certdata") authInfoWithCerts.ClientKey = "key" authInfoWithCerts.ClientKeyData = []byte("keydata") authInfoWithTokenAndCerts := clientcmdapi.NewAuthInfo() authInfoWithTokenAndCerts.Token = "token" authInfoWithTokenAndCerts.ClientCertificate = "cert" authInfoWithTokenAndCerts.ClientCertificateData = []byte("certdata") authInfoWithTokenAndCerts.ClientKey = "key" authInfoWithTokenAndCerts.ClientKeyData = []byte("keydata") startingConfig := newRedFederalCowHammerConfig() startingConfig.AuthInfos["another-user"] = authInfoWithCerts expectedConfig := newRedFederalCowHammerConfig() expectedConfig.AuthInfos["another-user"] = authInfoWithTokenAndCerts test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestCertLeavesToken(t *testing.T) { authInfoWithToken := clientcmdapi.NewAuthInfo() authInfoWithToken.Token = "token" authInfoWithTokenAndCerts := clientcmdapi.NewAuthInfo() authInfoWithTokenAndCerts.Token = "token" authInfoWithTokenAndCerts.ClientCertificate = "/cert" authInfoWithTokenAndCerts.ClientKey = "/key" startingConfig := newRedFederalCowHammerConfig() startingConfig.AuthInfos["another-user"] = authInfoWithToken expectedConfig := newRedFederalCowHammerConfig() expectedConfig.AuthInfos["another-user"] = authInfoWithTokenAndCerts test := configCommandTest{ args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=/cert", "--" + clientcmd.FlagKeyFile + "=/key"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestSetBytesBad(t *testing.T) { startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster() test := configCommandTest{ args: []string{"set", "clusters.another-cluster.certificate-authority-data", "cadata"}, startingConfig: startingConfig, expectedConfig: startingConfig, } func() { defer func() { // Restore cmdutil behavior. cmdutil.DefaultBehaviorOnFatal() }() // Check exit code. cmdutil.BehaviorOnFatal(func(e string, code int) { if code != 1 { t.Errorf("The exit code is %d, expected 1", code) } }) test.run(t) }() } func TestSetBytes(t *testing.T) { clusterInfoWithCAData := clientcmdapi.NewCluster() clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata") startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster() expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["another-cluster"] = clusterInfoWithCAData test := configCommandTest{ args: []string{"set", "clusters.another-cluster.certificate-authority-data", "cadata", "--set-raw-bytes"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestSetBase64Bytes(t *testing.T) { clusterInfoWithCAData := clientcmdapi.NewCluster() clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata") startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster() expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["another-cluster"] = clusterInfoWithCAData test := configCommandTest{ args: []string{"set", "clusters.another-cluster.certificate-authority-data", "Y2FkYXRh"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestUnsetBytes(t *testing.T) { clusterInfoWithCAData := clientcmdapi.NewCluster() clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata") startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clusterInfoWithCAData expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["another-cluster"] = clientcmdapi.NewCluster() test := configCommandTest{ args: []string{"unset", "clusters.another-cluster.certificate-authority-data"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestCAClearsInsecure(t *testing.T) { fakeCAFile, _ := os.CreateTemp(os.TempDir(), "ca-file") defer utiltesting.CloseAndRemove(t, fakeCAFile) clusterInfoWithInsecure := clientcmdapi.NewCluster() clusterInfoWithInsecure.InsecureSkipTLSVerify = true clusterInfoWithCA := clientcmdapi.NewCluster() clusterInfoWithCA.CertificateAuthority = filepath.Base(fakeCAFile.Name()) startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clusterInfoWithInsecure expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["another-cluster"] = clusterInfoWithCA test := configCommandTest{ args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=" + fakeCAFile.Name()}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestCAClearsCAData(t *testing.T) { clusterInfoWithCAData := clientcmdapi.NewCluster() clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata") clusterInfoWithCA := clientcmdapi.NewCluster() clusterInfoWithCA.CertificateAuthority = "/cafile" startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clusterInfoWithCAData expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["another-cluster"] = clusterInfoWithCA test := configCommandTest{ args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=/cafile", "--" + clientcmd.FlagInsecure + "=false"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestInsecureClearsCA(t *testing.T) { clusterInfoWithInsecure := clientcmdapi.NewCluster() clusterInfoWithInsecure.InsecureSkipTLSVerify = true clusterInfoWithCA := clientcmdapi.NewCluster() clusterInfoWithCA.CertificateAuthority = "cafile" clusterInfoWithCA.CertificateAuthorityData = []byte("cadata") startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clusterInfoWithCA expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["another-cluster"] = clusterInfoWithInsecure test := configCommandTest{ args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagInsecure + "=true"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestCADataClearsCA(t *testing.T) { fakeCAFile, _ := os.CreateTemp(os.TempDir(), "") defer utiltesting.CloseAndRemove(t, fakeCAFile) fakeData := []byte("cadata") os.WriteFile(fakeCAFile.Name(), fakeData, 0600) clusterInfoWithCAData := clientcmdapi.NewCluster() clusterInfoWithCAData.CertificateAuthorityData = fakeData clusterInfoWithCA := clientcmdapi.NewCluster() clusterInfoWithCA.CertificateAuthority = "cafile" startingConfig := newRedFederalCowHammerConfig() startingConfig.Clusters["another-cluster"] = clusterInfoWithCA expectedConfig := newRedFederalCowHammerConfig() expectedConfig.Clusters["another-cluster"] = clusterInfoWithCAData test := configCommandTest{ args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=" + fakeCAFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"}, startingConfig: startingConfig, expectedConfig: expectedConfig, } test.run(t) } func TestEmbedNoCADisallowed(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() test := configCommandTest{ args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagEmbedCerts + "=true"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } func() { defer func() { // Restore cmdutil behavior. cmdutil.DefaultBehaviorOnFatal() }() // Check exit code. cmdutil.BehaviorOnFatal(func(e string, code int) { if code != 1 { t.Errorf("The exit code is %d, expected 1", code) } expectedOutputs := []string{"--certificate-authority", "embed"} test.checkOutput(e, expectedOutputs, t) }) test.run(t) }() } func TestCAAndInsecureDisallowed(t *testing.T) { test := configCommandTest{ args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=cafile", "--" + clientcmd.FlagInsecure + "=true"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: newRedFederalCowHammerConfig(), } func() { defer func() { // Restore cmdutil behavior. cmdutil.DefaultBehaviorOnFatal() }() // Check exit code. cmdutil.BehaviorOnFatal(func(e string, code int) { if code != 1 { t.Errorf("The exit code is %d, expected 1", code) } expectedOutputs := []string{"certificate", "insecure"} test.checkOutput(e, expectedOutputs, t) }) test.run(t) }() } func TestMergeExistingAuth(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() authInfo := expectedConfig.AuthInfos["red-user"] authInfo.ClientKey = "/key" expectedConfig.AuthInfos["red-user"] = authInfo test := configCommandTest{ args: []string{"set-credentials", "red-user", "--" + clientcmd.FlagKeyFile + "=/key"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestNewEmptyCluster(t *testing.T) { expectedConfig := *clientcmdapi.NewConfig() expectedConfig.Clusters["new-cluster"] = clientcmdapi.NewCluster() test := configCommandTest{ args: []string{"set-cluster", "new-cluster"}, startingConfig: *clientcmdapi.NewConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestAdditionalCluster(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() cluster := clientcmdapi.NewCluster() cluster.CertificateAuthority = "/ca-location" cluster.InsecureSkipTLSVerify = false cluster.Server = "serverlocation" expectedConfig.Clusters["different-cluster"] = cluster test := configCommandTest{ args: []string{"set-cluster", "different-cluster", "--" + clientcmd.FlagAPIServer + "=serverlocation", "--" + clientcmd.FlagInsecure + "=false", "--" + clientcmd.FlagCAFile + "=/ca-location"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestOverwriteExistingCluster(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() cluster := clientcmdapi.NewCluster() cluster.Server = "serverlocation" expectedConfig.Clusters["cow-cluster"] = cluster test := configCommandTest{ args: []string{"set-cluster", "cow-cluster", "--" + clientcmd.FlagAPIServer + "=serverlocation"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestNewEmptyContext(t *testing.T) { expectedConfig := *clientcmdapi.NewConfig() expectedConfig.Contexts["new-context"] = clientcmdapi.NewContext() test := configCommandTest{ args: []string{"set-context", "new-context"}, startingConfig: *clientcmdapi.NewConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestAdditionalContext(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() context := clientcmdapi.NewContext() context.Cluster = "some-cluster" context.AuthInfo = "some-user" context.Namespace = "different-namespace" expectedConfig.Contexts["different-context"] = context test := configCommandTest{ args: []string{"set-context", "different-context", "--" + clientcmd.FlagClusterName + "=some-cluster", "--" + clientcmd.FlagAuthInfoName + "=some-user", "--" + clientcmd.FlagNamespace + "=different-namespace"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestMergeExistingContext(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() context := expectedConfig.Contexts["federal-context"] context.Namespace = "hammer" expectedConfig.Contexts["federal-context"] = context test := configCommandTest{ args: []string{"set-context", "federal-context", "--" + clientcmd.FlagNamespace + "=hammer"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } test.run(t) } func TestToBool(t *testing.T) { type test struct { in string out bool err string } tests := []test{ {"", false, ""}, {"true", true, ""}, {"on", false, `strconv.ParseBool: parsing "on": invalid syntax`}, } for _, curr := range tests { b, err := toBool(curr.in) if (len(curr.err) != 0) && err == nil { t.Errorf("Expected error: %v, but got nil", curr.err) } if (len(curr.err) == 0) && err != nil { t.Errorf("Unexpected error: %v", err) } if (err != nil) && (err.Error() != curr.err) { t.Errorf("Expected %v, got %v", curr.err, err) } if b != curr.out { t.Errorf("Expected %v, got %v", curr.out, b) } } } func testConfigCommand(args []string, startingConfig clientcmdapi.Config, t *testing.T) (string, clientcmdapi.Config) { fakeKubeFile, _ := os.CreateTemp(os.TempDir(), "") defer utiltesting.CloseAndRemove(t, fakeKubeFile) err := clientcmd.WriteToFile(startingConfig, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } argsToUse := make([]string, 0, 2+len(args)) argsToUse = append(argsToUse, "--kubeconfig="+fakeKubeFile.Name()) argsToUse = append(argsToUse, args...) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdConfig(tf, clientcmd.NewDefaultPathOptions(), streams) // "context" is a global flag, inherited from base kubectl command in the real world cmd.PersistentFlags().String("context", "", "The name of the kubeconfig context to use") cmd.SetArgs(argsToUse) cmd.Execute() config := clientcmd.GetConfigFromFileOrDie(fakeKubeFile.Name()) return buf.String(), *config } type configCommandTest struct { args []string startingConfig clientcmdapi.Config expectedConfig clientcmdapi.Config expectedOutputs []string } func (test configCommandTest) checkOutput(out string, expectedOutputs []string, t *testing.T) { for _, expectedOutput := range expectedOutputs { if !strings.Contains(out, expectedOutput) { t.Errorf("expected '%s' in output, got '%s'", expectedOutput, out) } } } func (test configCommandTest) run(t *testing.T) string { out, actualConfig := testConfigCommand(test.args, test.startingConfig, t) testSetNilMapsToEmpties(reflect.ValueOf(&test.expectedConfig)) testSetNilMapsToEmpties(reflect.ValueOf(&actualConfig)) testClearLocationOfOrigin(&actualConfig) if !apiequality.Semantic.DeepEqual(test.expectedConfig, actualConfig) { t.Errorf("diff: %v", cmp.Diff(test.expectedConfig, actualConfig)) t.Errorf("expected: %#v\n actual: %#v", test.expectedConfig, actualConfig) } test.checkOutput(out, test.expectedOutputs, t) return out } func testClearLocationOfOrigin(config *clientcmdapi.Config) { for key, obj := range config.AuthInfos { obj.LocationOfOrigin = "" config.AuthInfos[key] = obj } for key, obj := range config.Clusters { obj.LocationOfOrigin = "" config.Clusters[key] = obj } for key, obj := range config.Contexts { obj.LocationOfOrigin = "" config.Contexts[key] = obj } } func testSetNilMapsToEmpties(curr reflect.Value) { actualCurrValue := curr if curr.Kind() == reflect.Pointer { actualCurrValue = curr.Elem() } switch actualCurrValue.Kind() { case reflect.Map: for _, mapKey := range actualCurrValue.MapKeys() { currMapValue := actualCurrValue.MapIndex(mapKey) testSetNilMapsToEmpties(currMapValue) } case reflect.Struct: for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { currFieldValue := actualCurrValue.Field(fieldIndex) if currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil() { newValue := reflect.MakeMap(currFieldValue.Type()) currFieldValue.Set(newValue) } else { testSetNilMapsToEmpties(currFieldValue.Addr()) } } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/current_context.go000066400000000000000000000041321504711711200316310ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "io" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // CurrentContextOptions holds the command-line options for 'config current-context' sub command type CurrentContextOptions struct { ConfigAccess clientcmd.ConfigAccess } var ( currentContextLong = templates.LongDesc(i18n.T(` Display the current-context.`)) currentContextExample = templates.Examples(` # Display the current-context kubectl config current-context`) ) // NewCmdConfigCurrentContext returns a Command instance for 'config current-context' sub command func NewCmdConfigCurrentContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &CurrentContextOptions{ConfigAccess: configAccess} cmd := &cobra.Command{ Use: "current-context", Short: i18n.T("Display the current-context"), Long: currentContextLong, Example: currentContextExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(RunCurrentContext(out, options)) }, } return cmd } // RunCurrentContext performs the execution of 'config current-context' sub command func RunCurrentContext(out io.Writer, options *CurrentContextOptions) error { config, err := options.ConfigAccess.GetStartingConfig() if err != nil { return err } if config.CurrentContext == "" { err = fmt.Errorf("current-context is not set") return err } fmt.Fprintf(out, "%s\n", config.CurrentContext) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/current_context_test.go000066400000000000000000000043641504711711200326770ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" utiltesting "k8s.io/client-go/util/testing" "os" "strings" "testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type currentContextTest struct { startingConfig clientcmdapi.Config expectedError string } func newFederalContextConfig() clientcmdapi.Config { return clientcmdapi.Config{ CurrentContext: "federal-context", } } func TestCurrentContextWithSetContext(t *testing.T) { test := currentContextTest{ startingConfig: newFederalContextConfig(), expectedError: "", } test.run(t) } func TestCurrentContextWithUnsetContext(t *testing.T) { test := currentContextTest{ startingConfig: *clientcmdapi.NewConfig(), expectedError: "current-context is not set", } test.run(t) } func (test currentContextTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.startingConfig, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" options := CurrentContextOptions{ ConfigAccess: pathOptions, } buf := bytes.NewBuffer([]byte{}) err = RunCurrentContext(buf, &options) if len(test.expectedError) != 0 { if err == nil { t.Errorf("Did not get %v", test.expectedError) } else { if !strings.Contains(err.Error(), test.expectedError) { t.Errorf("Expected %v, but got %v", test.expectedError, err) } } return } if err != nil { t.Errorf("Unexpected error: %v", err) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go000066400000000000000000000045511504711711200314130ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "io" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( deleteClusterExample = templates.Examples(` # Delete the minikube cluster kubectl config delete-cluster minikube`) ) // NewCmdConfigDeleteCluster returns a Command instance for 'config delete-cluster' sub command func NewCmdConfigDeleteCluster(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { cmd := &cobra.Command{ Use: "delete-cluster NAME", DisableFlagsInUseLine: true, Short: i18n.T("Delete the specified cluster from the kubeconfig"), Long: i18n.T("Delete the specified cluster from the kubeconfig."), Example: deleteClusterExample, ValidArgsFunction: completion.ClusterCompletionFunc, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(runDeleteCluster(out, configAccess, cmd)) }, } return cmd } func runDeleteCluster(out io.Writer, configAccess clientcmd.ConfigAccess, cmd *cobra.Command) error { config, err := configAccess.GetStartingConfig() if err != nil { return err } args := cmd.Flags().Args() if len(args) != 1 { cmd.Help() return nil } configFile := configAccess.GetDefaultFilename() if configAccess.IsExplicitFile() { configFile = configAccess.GetExplicitFile() } name := args[0] _, ok := config.Clusters[name] if !ok { return fmt.Errorf("cannot delete cluster %s, not in %s", name, configFile) } delete(config.Clusters, name) if err := clientcmd.ModifyConfig(configAccess, *config, true); err != nil { return err } fmt.Fprintf(out, "deleted cluster %s from %s\n", name, configFile) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster_test.go000066400000000000000000000052661504711711200324560ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "fmt" utiltesting "k8s.io/client-go/util/testing" "os" "reflect" "testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type deleteClusterTest struct { config clientcmdapi.Config clusterToDelete string expectedClusters []string expectedOut string } func TestDeleteCluster(t *testing.T) { conf := clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.0.99"}, "otherkube": {Server: "https://192.168.0.100"}, }, } test := deleteClusterTest{ config: conf, clusterToDelete: "minikube", expectedClusters: []string{"otherkube"}, expectedOut: "deleted cluster minikube from %s\n", } test.run(t) } func (test deleteClusterTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigDeleteCluster(buf, pathOptions) cmd.SetArgs([]string{test.clusterToDelete}) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v", err) } expectedOutWithFile := fmt.Sprintf(test.expectedOut, fakeKubeFile.Name()) if expectedOutWithFile != buf.String() { t.Errorf("expected output %s, but got %s", expectedOutWithFile, buf.String()) return } // Verify cluster was removed from kubeconfig file config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } clusters := make([]string, 0, len(config.Clusters)) for k := range config.Clusters { clusters = append(clusters, k) } if !reflect.DeepEqual(test.expectedClusters, clusters) { t.Errorf("expected clusters %v, but found %v in kubeconfig", test.expectedClusters, clusters) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go000066400000000000000000000050731504711711200314160ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "io" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( deleteContextExample = templates.Examples(` # Delete the context for the minikube cluster kubectl config delete-context minikube`) ) // NewCmdConfigDeleteContext returns a Command instance for 'config delete-context' sub command func NewCmdConfigDeleteContext(out, errOut io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { cmd := &cobra.Command{ Use: "delete-context NAME", DisableFlagsInUseLine: true, Short: i18n.T("Delete the specified context from the kubeconfig"), Long: i18n.T("Delete the specified context from the kubeconfig."), Example: deleteContextExample, ValidArgsFunction: completion.ContextCompletionFunc, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(runDeleteContext(out, errOut, configAccess, cmd)) }, } return cmd } func runDeleteContext(out, errOut io.Writer, configAccess clientcmd.ConfigAccess, cmd *cobra.Command) error { config, err := configAccess.GetStartingConfig() if err != nil { return err } args := cmd.Flags().Args() if len(args) != 1 { cmd.Help() return nil } configFile := configAccess.GetDefaultFilename() if configAccess.IsExplicitFile() { configFile = configAccess.GetExplicitFile() } name := args[0] _, ok := config.Contexts[name] if !ok { return fmt.Errorf("cannot delete context %s, not in %s", name, configFile) } if config.CurrentContext == name { fmt.Fprint(errOut, "warning: this removed your active context, use \"kubectl config use-context\" to select a different one\n") } delete(config.Contexts, name) if err := clientcmd.ModifyConfig(configAccess, *config, true); err != nil { return err } fmt.Fprintf(out, "deleted context %s from %s\n", name, configFile) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context_test.go000066400000000000000000000053151504711711200324540ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "fmt" utiltesting "k8s.io/client-go/util/testing" "os" "reflect" "testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type deleteContextTest struct { config clientcmdapi.Config contextToDelete string expectedContexts []string expectedOut string } func TestDeleteContext(t *testing.T) { conf := clientcmdapi.Config{ Contexts: map[string]*clientcmdapi.Context{ "minikube": {Cluster: "minikube"}, "otherkube": {Cluster: "otherkube"}, }, } test := deleteContextTest{ config: conf, contextToDelete: "minikube", expectedContexts: []string{"otherkube"}, expectedOut: "deleted context minikube from %s\n", } test.run(t) } func (test deleteContextTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) errBuf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigDeleteContext(buf, errBuf, pathOptions) cmd.SetArgs([]string{test.contextToDelete}) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v", err) } expectedOutWithFile := fmt.Sprintf(test.expectedOut, fakeKubeFile.Name()) if expectedOutWithFile != buf.String() { t.Errorf("expected output %s, but got %s", expectedOutWithFile, buf.String()) return } // Verify context was removed from kubeconfig file config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } contexts := make([]string, 0, len(config.Contexts)) for k := range config.Contexts { contexts = append(contexts, k) } if !reflect.DeepEqual(test.expectedContexts, contexts) { t.Errorf("expected contexts %v, but found %v in kubeconfig", test.expectedContexts, contexts) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user.go000066400000000000000000000064771504711711200307210ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( deleteUserExample = templates.Examples(` # Delete the minikube user kubectl config delete-user minikube`) ) // DeleteUserOptions holds the data needed to run the command type DeleteUserOptions struct { user string configAccess clientcmd.ConfigAccess config *clientcmdapi.Config configFile string genericiooptions.IOStreams } // NewDeleteUserOptions creates the options for the command func NewDeleteUserOptions(ioStreams genericiooptions.IOStreams, configAccess clientcmd.ConfigAccess) *DeleteUserOptions { return &DeleteUserOptions{ configAccess: configAccess, IOStreams: ioStreams, } } // NewCmdConfigDeleteUser returns a Command instance for 'config delete-user' sub command func NewCmdConfigDeleteUser(streams genericiooptions.IOStreams, configAccess clientcmd.ConfigAccess) *cobra.Command { o := NewDeleteUserOptions(streams, configAccess) cmd := &cobra.Command{ Use: "delete-user NAME", DisableFlagsInUseLine: true, Short: i18n.T("Delete the specified user from the kubeconfig"), Long: i18n.T("Delete the specified user from the kubeconfig."), Example: deleteUserExample, ValidArgsFunction: completion.UserCompletionFunc, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } return cmd } // Complete sets up the command to run func (o *DeleteUserOptions) Complete(cmd *cobra.Command, args []string) error { config, err := o.configAccess.GetStartingConfig() if err != nil { return err } o.config = config if len(args) != 1 { return cmdutil.UsageErrorf(cmd, "user to delete is required") } o.user = args[0] configFile := o.configAccess.GetDefaultFilename() if o.configAccess.IsExplicitFile() { configFile = o.configAccess.GetExplicitFile() } o.configFile = configFile return nil } // Validate ensures the command has enough info to run func (o *DeleteUserOptions) Validate() error { _, ok := o.config.AuthInfos[o.user] if !ok { return fmt.Errorf("cannot delete user %s, not in %s", o.user, o.configFile) } return nil } // Run performs the command func (o *DeleteUserOptions) Run() error { delete(o.config.AuthInfos, o.user) if err := clientcmd.ModifyConfig(o.configAccess, *o.config, true); err != nil { return err } fmt.Fprintf(o.Out, "deleted user %s from %s\n", o.user, o.configFile) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user_test.go000066400000000000000000000116301504711711200317430ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "reflect" "strings" "testing" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestDeleteUserComplete(t *testing.T) { var tests = []struct { name string args []string err string }{ { name: "no args", args: []string{}, err: "user to delete is required", }, { name: "user provided", args: []string{"minikube"}, err: "", }, } for i := range tests { test := tests[i] t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() ioStreams, _, out, _ := genericiooptions.NewTestIOStreams() pathOptions, err := tf.PathOptionsWithConfig(clientcmdapi.Config{}) if err != nil { t.Fatalf("unexpected error executing command: %v", err) } cmd := NewCmdConfigDeleteUser(ioStreams, pathOptions) cmd.SetOut(out) options := NewDeleteUserOptions(ioStreams, pathOptions) if err := options.Complete(cmd, test.args); err != nil { if test.err == "" { t.Fatalf("unexpected error executing command: %v", err) } if !strings.Contains(err.Error(), test.err) { t.Fatalf("expected error to contain %v, got %v", test.err, err.Error()) } return } if options.configFile != pathOptions.GlobalFile { t.Fatalf("expected configFile to be %v, got %v", pathOptions.GlobalFile, options.configFile) } }) } } func TestDeleteUserValidate(t *testing.T) { var tests = []struct { name string user string config clientcmdapi.Config err string }{ { name: "user not in config", user: "kube", config: clientcmdapi.Config{ AuthInfos: map[string]*clientcmdapi.AuthInfo{ "minikube": {Username: "minikube"}, }, }, err: "cannot delete user kube", }, { name: "user in config", user: "kube", config: clientcmdapi.Config{ AuthInfos: map[string]*clientcmdapi.AuthInfo{ "minikube": {Username: "minikube"}, "kube": {Username: "kube"}, }, }, err: "", }, } for i := range tests { test := tests[i] t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() pathOptions, err := tf.PathOptionsWithConfig(test.config) if err != nil { t.Fatalf("unexpected error executing command: %v", err) } options := NewDeleteUserOptions(ioStreams, pathOptions) options.config = &test.config options.user = test.user if err := options.Validate(); err != nil { if !strings.Contains(err.Error(), test.err) { t.Fatalf("expected: %s but got %s", test.err, err.Error()) } return } }) } } func TestDeleteUserRun(t *testing.T) { var tests = []struct { name string user string config clientcmdapi.Config expectedUsers []string out string }{ { name: "delete user", user: "kube", config: clientcmdapi.Config{ AuthInfos: map[string]*clientcmdapi.AuthInfo{ "minikube": {Username: "minikube"}, "kube": {Username: "kube"}, }, }, expectedUsers: []string{"minikube"}, out: "deleted user kube from", }, } for i := range tests { test := tests[i] t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() ioStreams, _, out, _ := genericiooptions.NewTestIOStreams() pathOptions, err := tf.PathOptionsWithConfig(test.config) if err != nil { t.Fatalf("unexpected error executing command: %v", err) } options := NewDeleteUserOptions(ioStreams, pathOptions) options.config = &test.config options.configFile = pathOptions.GlobalFile options.user = test.user if err := options.Run(); err != nil { t.Fatalf("unexpected error executing command: %v", err) } if got := out.String(); !strings.Contains(got, test.out) { t.Fatalf("expected: %s but got %s", test.out, got) } config, err := clientcmd.LoadFromFile(options.configFile) if err != nil { t.Fatalf("unexpected error executing command: %v", err) } users := make([]string, 0, len(config.AuthInfos)) for user := range config.AuthInfos { users = append(users, user) } if !reflect.DeepEqual(test.expectedUsers, users) { t.Fatalf("expected %v, got %v", test.expectedUsers, users) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go000066400000000000000000000033521504711711200311110ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "io" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( getClustersExample = templates.Examples(` # List the clusters that kubectl knows about kubectl config get-clusters`) ) // NewCmdConfigGetClusters creates a command object for the "get-clusters" action, which // lists all clusters defined in the kubeconfig. func NewCmdConfigGetClusters(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { cmd := &cobra.Command{ Use: "get-clusters", Short: i18n.T("Display clusters defined in the kubeconfig"), Long: i18n.T("Display clusters defined in the kubeconfig."), Example: getClustersExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(runGetClusters(out, configAccess)) }, } return cmd } func runGetClusters(out io.Writer, configAccess clientcmd.ConfigAccess) error { config, err := configAccess.GetStartingConfig() if err != nil { return err } fmt.Fprintf(out, "NAME\n") for name := range config.Clusters { fmt.Fprintf(out, "%s\n", name) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters_test.go000066400000000000000000000037731504711711200321570ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "os" "testing" utiltesting "k8s.io/client-go/util/testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type getClustersTest struct { config clientcmdapi.Config expected string } func TestGetClusters(t *testing.T) { conf := clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.0.99"}, }, } test := getClustersTest{ config: conf, expected: `NAME minikube `, } test.run(t) } func TestGetClustersEmpty(t *testing.T) { test := getClustersTest{ config: clientcmdapi.Config{}, expected: "NAME\n", } test.run(t) } func (test getClustersTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigGetClusters(buf, pathOptions) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v", err) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("expected %v, but got %v", test.expected, buf.String()) } return } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go000066400000000000000000000124011504711711200311070ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "io" "sort" "strings" "github.com/liggitt/tabwriter" "github.com/spf13/cobra" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // GetContextsOptions contains the assignable options from the args. type GetContextsOptions struct { configAccess clientcmd.ConfigAccess nameOnly bool showHeaders bool contextNames []string outputFormat string noHeaders bool genericiooptions.IOStreams } var ( getContextsLong = templates.LongDesc(i18n.T(`Display one or many contexts from the kubeconfig file.`)) getContextsExample = templates.Examples(` # List all the contexts in your kubeconfig file kubectl config get-contexts # Describe one context in your kubeconfig file kubectl config get-contexts my-context`) ) // NewCmdConfigGetContexts creates a command object for the "get-contexts" action, which // retrieves one or more contexts from a kubeconfig. func NewCmdConfigGetContexts(streams genericiooptions.IOStreams, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &GetContextsOptions{ configAccess: configAccess, IOStreams: streams, } cmd := &cobra.Command{ Use: "get-contexts [(-o|--output=)name)]", DisableFlagsInUseLine: true, Short: i18n.T("Describe one or many contexts"), Long: getContextsLong, Example: getContextsExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.Complete(cmd, args)) cmdutil.CheckErr(options.Validate()) cmdutil.CheckErr(options.RunGetContexts()) }, } cmd.Flags().BoolVar(&options.noHeaders, "no-headers", options.noHeaders, "When using the default or custom-column output format, don't print headers (default print headers).") cmd.Flags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, `Output format. One of: (name).`) return cmd } // Complete assigns GetContextsOptions from the args. func (o *GetContextsOptions) Complete(cmd *cobra.Command, args []string) error { supportedOutputTypes := sets.New[string]("", "name") if !supportedOutputTypes.Has(o.outputFormat) { return fmt.Errorf("--output %v is not available in kubectl config get-contexts; resetting to default output format", o.outputFormat) } o.contextNames = args o.nameOnly = false if o.outputFormat == "name" { o.nameOnly = true } o.showHeaders = true if cmdutil.GetFlagBool(cmd, "no-headers") || o.nameOnly { o.showHeaders = false } return nil } // Validate ensures the of output format func (o *GetContextsOptions) Validate() error { return nil } // RunGetContexts implements all the necessary functionality for context retrieval. func (o GetContextsOptions) RunGetContexts() error { config, err := o.configAccess.GetStartingConfig() if err != nil { return err } out, found := o.Out.(*tabwriter.Writer) if !found { out = printers.GetNewTabWriter(o.Out) defer out.Flush() } // Build a list of context names to print, and warn if any requested contexts are not found. // Do this before printing the headers so it doesn't look ugly. allErrs := []error{} toPrint := []string{} if len(o.contextNames) == 0 { for name := range config.Contexts { toPrint = append(toPrint, name) } } else { for _, name := range o.contextNames { _, ok := config.Contexts[name] if ok { toPrint = append(toPrint, name) } else { allErrs = append(allErrs, fmt.Errorf("context %v not found", name)) } } } if o.showHeaders { err = printContextHeaders(out, o.nameOnly) if err != nil { allErrs = append(allErrs, err) } } sort.Strings(toPrint) for _, name := range toPrint { err = printContext(name, config.Contexts[name], out, o.nameOnly, config.CurrentContext == name) if err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } func printContextHeaders(out io.Writer, nameOnly bool) error { columnNames := []string{"CURRENT", "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE"} if nameOnly { columnNames = columnNames[:1] } _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t")) return err } func printContext(name string, context *clientcmdapi.Context, w io.Writer, nameOnly, current bool) error { if nameOnly { _, err := fmt.Fprintf(w, "%s\n", name) return err } prefix := " " if current { prefix = "*" } _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", prefix, name, context.Cluster, context.AuthInfo, context.Namespace) return err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts_test.go000066400000000000000000000124501504711711200321520ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "os" "testing" utiltesting "k8s.io/client-go/util/testing" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type getContextsTest struct { startingConfig clientcmdapi.Config names []string noHeader bool nameOnly bool expectedOut string } func TestGetContextsAll(t *testing.T) { tconf := clientcmdapi.Config{ CurrentContext: "shaker-context", Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}} test := getContextsTest{ startingConfig: tconf, names: []string{}, noHeader: false, nameOnly: false, expectedOut: `CURRENT NAME CLUSTER AUTHINFO NAMESPACE * shaker-context big-cluster blue-user saw-ns `, } test.run(t) } func TestGetContextsAllNoHeader(t *testing.T) { tconf := clientcmdapi.Config{ CurrentContext: "shaker-context", Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}} test := getContextsTest{ startingConfig: tconf, names: []string{}, noHeader: true, nameOnly: false, expectedOut: "* shaker-context big-cluster blue-user saw-ns\n", } test.run(t) } func TestGetContextsAllSorted(t *testing.T) { tconf := clientcmdapi.Config{ CurrentContext: "shaker-context", Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}, "abc": {AuthInfo: "blue-user", Cluster: "abc-cluster", Namespace: "kube-system"}, "xyz": {AuthInfo: "blue-user", Cluster: "xyz-cluster", Namespace: "default"}}} test := getContextsTest{ startingConfig: tconf, names: []string{}, noHeader: false, nameOnly: false, expectedOut: `CURRENT NAME CLUSTER AUTHINFO NAMESPACE abc abc-cluster blue-user kube-system * shaker-context big-cluster blue-user saw-ns xyz xyz-cluster blue-user default `, } test.run(t) } func TestGetContextsAllName(t *testing.T) { tconf := clientcmdapi.Config{ Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}} test := getContextsTest{ startingConfig: tconf, names: []string{}, noHeader: false, nameOnly: true, expectedOut: "shaker-context\n", } test.run(t) } func TestGetContextsAllNameNoHeader(t *testing.T) { tconf := clientcmdapi.Config{ CurrentContext: "shaker-context", Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}} test := getContextsTest{ startingConfig: tconf, names: []string{}, noHeader: true, nameOnly: true, expectedOut: "shaker-context\n", } test.run(t) } func TestGetContextsAllNone(t *testing.T) { test := getContextsTest{ startingConfig: *clientcmdapi.NewConfig(), names: []string{}, noHeader: true, nameOnly: false, expectedOut: "", } test.run(t) } func TestGetContextsSelectOneOfTwo(t *testing.T) { tconf := clientcmdapi.Config{ CurrentContext: "shaker-context", Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}, "not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}} test := getContextsTest{ startingConfig: tconf, names: []string{"shaker-context"}, noHeader: true, nameOnly: true, expectedOut: "shaker-context\n", } test.run(t) } func (test getContextsTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.startingConfig, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" streams, _, buf, _ := genericiooptions.NewTestIOStreams() options := GetContextsOptions{ configAccess: pathOptions, } cmd := NewCmdConfigGetContexts(streams, options.configAccess) if test.nameOnly { cmd.Flags().Set("output", "name") } if test.noHeader { cmd.Flags().Set("no-headers", "true") } cmd.Run(cmd, test.names) if len(test.expectedOut) != 0 { if buf.String() != test.expectedOut { t.Errorf("Expected\n%s\ngot\n%s", test.expectedOut, buf.String()) } return } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users.go000066400000000000000000000045161504711711200304110ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "sort" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( getUsersExample = templates.Examples(` # List the users that kubectl knows about kubectl config get-users`) ) // GetUsersOptions holds the data needed to run the command type GetUsersOptions struct { configAccess clientcmd.ConfigAccess genericiooptions.IOStreams } // NewGetUsersOptions creates the options for the command func NewGetUsersOptions(ioStreams genericiooptions.IOStreams, configAccess clientcmd.ConfigAccess) *GetUsersOptions { return &GetUsersOptions{ configAccess: configAccess, IOStreams: ioStreams, } } // NewCmdConfigGetUsers creates a command object for the "get-users" action, which // lists all users defined in the kubeconfig. func NewCmdConfigGetUsers(streams genericiooptions.IOStreams, configAccess clientcmd.ConfigAccess) *cobra.Command { o := NewGetUsersOptions(streams, configAccess) cmd := &cobra.Command{ Use: "get-users", Short: i18n.T("Display users defined in the kubeconfig"), Long: i18n.T("Display users defined in the kubeconfig."), Example: getUsersExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Run()) }, } return cmd } // Run performs the command func (o *GetUsersOptions) Run() error { config, err := o.configAccess.GetStartingConfig() if err != nil { return err } users := make([]string, 0, len(config.AuthInfos)) for user := range config.AuthInfos { users = append(users, user) } sort.Strings(users) fmt.Fprintf(o.Out, "NAME\n") for _, user := range users { fmt.Fprintf(o.Out, "%s\n", user) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users_test.go000066400000000000000000000035101504711711200314410ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "testing" "k8s.io/cli-runtime/pkg/genericiooptions" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestGetUsersRun(t *testing.T) { var tests = []struct { name string config clientcmdapi.Config expected string }{ { name: "no users", config: clientcmdapi.Config{}, expected: "NAME\n", }, { name: "some users", config: clientcmdapi.Config{ AuthInfos: map[string]*clientcmdapi.AuthInfo{ "minikube": {Username: "minikube"}, "admin": {Username: "admin"}, }, }, expected: `NAME admin minikube `, }, } for i := range tests { test := tests[i] t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() ioStreams, _, out, _ := genericiooptions.NewTestIOStreams() pathOptions, err := tf.PathOptionsWithConfig(test.config) if err != nil { t.Fatalf("unexpected error executing command: %v", err) } options := NewGetUsersOptions(ioStreams, pathOptions) if err = options.Run(); err != nil { t.Fatalf("unexpected error executing command: %v", err) } if got := out.String(); got != test.expected { t.Fatalf("expected: %s but got %s", test.expected, got) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/navigation_step_parser.go000066400000000000000000000113371504711711200331560ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "reflect" "strings" "k8s.io/apimachinery/pkg/util/sets" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type navigationSteps struct { steps []navigationStep currentStepIndex int } type navigationStep struct { stepValue string stepType reflect.Type } func newNavigationSteps(path string) (*navigationSteps, error) { steps := []navigationStep{} individualParts := strings.Split(path, ".") currType := reflect.TypeOf(clientcmdapi.Config{}) currPartIndex := 0 for currPartIndex < len(individualParts) { switch currType.Kind() { case reflect.Map: // if we're in a map, we need to locate a name. That name may contain dots, so we need to know what tokens are legal for the map's value type // for example, we could have a set request like: `set clusters.10.10.12.56.insecure-skip-tls-verify true`. We enter this case with // steps representing 10, 10, 12, 56, insecure-skip-tls-verify. The name is "10.10.12.56", so we want to collect all those parts together and // store them as a single step. In order to do that, we need to determine what set of tokens is a legal step AFTER the name of the map key // This set of reflective code pulls the type of the map values, uses that type to look up the set of legal tags. Those legal tags are used to // walk the list of remaining parts until we find a match to a legal tag or the end of the string. That name is used to burn all the used parts. mapValueType := currType.Elem().Elem() mapValueOptions, err := getPotentialTypeValues(mapValueType) if err != nil { return nil, err } nextPart := findNameStep(individualParts[currPartIndex:], sets.KeySet(mapValueOptions)) steps = append(steps, navigationStep{nextPart, mapValueType}) currPartIndex += len(strings.Split(nextPart, ".")) currType = mapValueType case reflect.Struct: nextPart := individualParts[currPartIndex] options, err := getPotentialTypeValues(currType) if err != nil { return nil, err } fieldType, exists := options[nextPart] if !exists { return nil, fmt.Errorf("unable to parse %v after %v at %v", path, steps, currType) } steps = append(steps, navigationStep{nextPart, fieldType}) currPartIndex += len(strings.Split(nextPart, ".")) currType = fieldType default: return nil, fmt.Errorf("unable to parse one or more field values of %v", path) } } return &navigationSteps{steps, 0}, nil } func (s *navigationSteps) pop() navigationStep { if s.moreStepsRemaining() { s.currentStepIndex++ return s.steps[s.currentStepIndex-1] } return navigationStep{} } func (s *navigationSteps) moreStepsRemaining() bool { return len(s.steps) > s.currentStepIndex } // findNameStep takes the list of parts and a set of valid tags that can be used after the name. It then walks the list of parts // until it find a valid "next" tag or until it reaches the end of the parts and then builds the name back up out of the individual parts func findNameStep(parts []string, typeOptions sets.Set[string]) string { if len(parts) == 0 { return "" } numberOfPartsInStep := findKnownValue(parts[1:], typeOptions) + 1 // if we didn't find a known value, then the entire thing must be a name if numberOfPartsInStep == 0 { numberOfPartsInStep = len(parts) } nextParts := parts[0:numberOfPartsInStep] return strings.Join(nextParts, ".") } // getPotentialTypeValues takes a type and looks up the tags used to represent its fields when serialized. func getPotentialTypeValues(typeValue reflect.Type) (map[string]reflect.Type, error) { if typeValue.Kind() == reflect.Pointer { typeValue = typeValue.Elem() } if typeValue.Kind() != reflect.Struct { return nil, fmt.Errorf("%v is not of type struct", typeValue) } ret := make(map[string]reflect.Type) for fieldIndex := 0; fieldIndex < typeValue.NumField(); fieldIndex++ { fieldType := typeValue.Field(fieldIndex) yamlTag := fieldType.Tag.Get("json") yamlTagName := strings.Split(yamlTag, ",")[0] ret[yamlTagName] = fieldType.Type } return ret, nil } func findKnownValue(parts []string, valueOptions sets.Set[string]) int { for i := range parts { if valueOptions.Has(parts[i]) { return i } } return -1 } navigation_step_parser_test.go000066400000000000000000000053661504711711200341430ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "reflect" "strings" "testing" "github.com/google/go-cmp/cmp" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type stepParserTest struct { path string expectedNavigationSteps navigationSteps expectedError string } func TestParseWithDots(t *testing.T) { test := stepParserTest{ path: "clusters.my.dot.delimited.name.server", expectedNavigationSteps: navigationSteps{ steps: []navigationStep{ {"clusters", reflect.TypeOf(make(map[string]*clientcmdapi.Cluster))}, {"my.dot.delimited.name", reflect.TypeOf(clientcmdapi.Cluster{})}, {"server", reflect.TypeOf("")}, }, }, } test.run(t) } func TestParseWithDotsEndingWithName(t *testing.T) { test := stepParserTest{ path: "contexts.10.12.12.12", expectedNavigationSteps: navigationSteps{ steps: []navigationStep{ {"contexts", reflect.TypeOf(make(map[string]*clientcmdapi.Context))}, {"10.12.12.12", reflect.TypeOf(clientcmdapi.Context{})}, }, }, } test.run(t) } func TestParseWithBadValue(t *testing.T) { test := stepParserTest{ path: "user.bad", expectedNavigationSteps: navigationSteps{ steps: []navigationStep{}, }, expectedError: "unable to parse user.bad after [] at api.Config", } test.run(t) } func TestParseWithNoMatchingValue(t *testing.T) { test := stepParserTest{ path: "users.jheiss.exec.command", expectedNavigationSteps: navigationSteps{ steps: []navigationStep{}, }, expectedError: "unable to parse one or more field values of users.jheiss.exec", } test.run(t) } func (test stepParserTest) run(t *testing.T) { actualSteps, err := newNavigationSteps(test.path) if len(test.expectedError) != 0 { if err == nil { t.Errorf("Did not get %v", test.expectedError) } else { if !strings.Contains(err.Error(), test.expectedError) { t.Errorf("Expected %v, but got %v", test.expectedError, err) } } return } if err != nil { t.Errorf("Unexpected error: %v", err) } if !reflect.DeepEqual(test.expectedNavigationSteps, *actualSteps) { t.Errorf("diff: %v", cmp.Diff(test.expectedNavigationSteps, *actualSteps)) t.Errorf("expected: %#v\n actual: %#v", test.expectedNavigationSteps, *actualSteps) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/rename_context.go000066400000000000000000000077101504711711200314230ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "fmt" "io" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // RenameContextOptions contains the options for running the rename-context cli command. type RenameContextOptions struct { configAccess clientcmd.ConfigAccess contextName string newName string } const ( renameContextUse = "rename-context CONTEXT_NAME NEW_NAME" ) var ( renameContextShort = i18n.T("Rename a context from the kubeconfig file") renameContextLong = templates.LongDesc(i18n.T(` Renames a context from the kubeconfig file. CONTEXT_NAME is the context name that you want to change. NEW_NAME is the new name you want to set. Note: If the context being renamed is the 'current-context', this field will also be updated.`)) renameContextExample = templates.Examples(` # Rename the context 'old-name' to 'new-name' in your kubeconfig file kubectl config rename-context old-name new-name`) ) // NewCmdConfigRenameContext creates a command object for the "rename-context" action func NewCmdConfigRenameContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &RenameContextOptions{configAccess: configAccess} cmd := &cobra.Command{ Use: renameContextUse, DisableFlagsInUseLine: true, Short: renameContextShort, Long: renameContextLong, Example: renameContextExample, ValidArgsFunction: completion.ContextCompletionFunc, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.Complete(cmd, args, out)) cmdutil.CheckErr(options.Validate()) cmdutil.CheckErr(options.RunRenameContext(out)) }, } return cmd } // Complete assigns RenameContextOptions from the args. func (o *RenameContextOptions) Complete(cmd *cobra.Command, args []string, out io.Writer) error { if len(args) != 2 { return helpErrorf(cmd, "Unexpected args: %v", args) } o.contextName = args[0] o.newName = args[1] return nil } // Validate makes sure that provided values for command-line options are valid func (o RenameContextOptions) Validate() error { if len(o.newName) == 0 { return errors.New("You must specify a new non-empty context name") } return nil } // RunRenameContext performs the execution for 'config rename-context' sub command func (o RenameContextOptions) RunRenameContext(out io.Writer) error { config, err := o.configAccess.GetStartingConfig() if err != nil { return err } configFile := o.configAccess.GetDefaultFilename() if o.configAccess.IsExplicitFile() { configFile = o.configAccess.GetExplicitFile() } context, exists := config.Contexts[o.contextName] if !exists { return fmt.Errorf("cannot rename the context %q, it's not in %s", o.contextName, configFile) } _, newExists := config.Contexts[o.newName] if newExists { return fmt.Errorf("cannot rename the context %q, the context %q already exists in %s", o.contextName, o.newName, configFile) } config.Contexts[o.newName] = context delete(config.Contexts, o.contextName) if config.CurrentContext == o.contextName { config.CurrentContext = o.newName } if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return err } fmt.Fprintf(out, "Context %q renamed to %q.\n", o.contextName, o.newName) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/rename_context_test.go000066400000000000000000000116501504711711200324600ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "fmt" "os" "strings" "testing" utiltesting "k8s.io/client-go/util/testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) const ( currentContext = "current-context" newContext = "new-context" nonexistentCurrentContext = "nonexistent-current-context" existentNewContext = "existent-new-context" ) var ( contextData = clientcmdapi.NewContext() ) type renameContextTest struct { description string initialConfig clientcmdapi.Config // initial config expectedConfig clientcmdapi.Config // expected config args []string // kubectl rename-context args expectedOut string // expected out message expectedErr string // expected error message } func TestRenameContext(t *testing.T) { initialConfig := clientcmdapi.Config{ CurrentContext: currentContext, Contexts: map[string]*clientcmdapi.Context{currentContext: contextData}} expectedConfig := clientcmdapi.Config{ CurrentContext: newContext, Contexts: map[string]*clientcmdapi.Context{newContext: contextData}} test := renameContextTest{ description: "Testing for kubectl config rename-context whose context to be renamed is the CurrentContext", initialConfig: initialConfig, expectedConfig: expectedConfig, args: []string{currentContext, newContext}, expectedOut: fmt.Sprintf("Context %q renamed to %q.\n", currentContext, newContext), expectedErr: "", } test.run(t) } func TestRenameNonexistentContext(t *testing.T) { initialConfig := clientcmdapi.Config{ CurrentContext: currentContext, Contexts: map[string]*clientcmdapi.Context{currentContext: contextData}} test := renameContextTest{ description: "Testing for kubectl config rename-context whose context to be renamed no exists", initialConfig: initialConfig, expectedConfig: initialConfig, args: []string{nonexistentCurrentContext, newContext}, expectedOut: "", expectedErr: fmt.Sprintf("cannot rename the context %q, it's not in", nonexistentCurrentContext), } test.run(t) } func TestRenameToAlreadyExistingContext(t *testing.T) { initialConfig := clientcmdapi.Config{ CurrentContext: currentContext, Contexts: map[string]*clientcmdapi.Context{ currentContext: contextData, existentNewContext: contextData}} test := renameContextTest{ description: "Testing for kubectl config rename-context whose the new name is already in another context.", initialConfig: initialConfig, expectedConfig: initialConfig, args: []string{currentContext, existentNewContext}, expectedOut: "", expectedErr: fmt.Sprintf("cannot rename the context %q, the context %q already exists", currentContext, existentNewContext), } test.run(t) } func (test renameContextTest) run(t *testing.T) { fakeKubeFile, _ := os.CreateTemp(os.TempDir(), "") defer utiltesting.CloseAndRemove(t, fakeKubeFile) err := clientcmd.WriteToFile(test.initialConfig, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" options := RenameContextOptions{ configAccess: pathOptions, contextName: test.args[0], newName: test.args[1], } buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigRenameContext(buf, options.configAccess) options.Complete(cmd, test.args, buf) options.Validate() err = options.RunRenameContext(buf) if len(test.expectedErr) != 0 { if err == nil { t.Errorf("Did not get %v", test.expectedErr) } else { if !strings.Contains(err.Error(), test.expectedErr) { t.Errorf("Expected error %v, but got %v", test.expectedErr, err) } } return } config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } _, oldExists := config.Contexts[currentContext] _, newExists := config.Contexts[newContext] if (!newExists) || (oldExists) || (config.CurrentContext != newContext) { t.Errorf("Failed in: %q\n expected %v\n but got %v", test.description, test.expectedConfig, *config) } if len(test.expectedOut) != 0 { if buf.String() != test.expectedOut { t.Errorf("Failed in:%q\n expected out %v\n but got %v", test.description, test.expectedOut, buf.String()) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set.go000066400000000000000000000166471504711711200272140ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "encoding/base64" "errors" "fmt" "io" "reflect" "strings" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" cliflag "k8s.io/component-base/cli/flag" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) type setOptions struct { configAccess clientcmd.ConfigAccess propertyName string propertyValue string setRawBytes cliflag.Tristate } var ( setLong = templates.LongDesc(i18n.T(` Set an individual value in a kubeconfig file. PROPERTY_NAME is a dot delimited name where each token represents either an attribute name or a map key. Map keys may not contain dots. PROPERTY_VALUE is the new value you want to set. Binary fields such as 'certificate-authority-data' expect a base64 encoded string unless the --set-raw-bytes flag is used. Specifying an attribute name that already exists will merge new fields on top of existing values.`)) setExample = templates.Examples(` # Set the server field on the my-cluster cluster to https://1.2.3.4 kubectl config set clusters.my-cluster.server https://1.2.3.4 # Set the certificate-authority-data field on the my-cluster cluster kubectl config set clusters.my-cluster.certificate-authority-data $(echo "cert_data_here" | base64 -i -) # Set the cluster field in the my-context context to my-cluster kubectl config set contexts.my-context.cluster my-cluster # Set the client-key-data field in the cluster-admin user using --set-raw-bytes option kubectl config set users.cluster-admin.client-key-data cert_data_here --set-raw-bytes=true`) ) // NewCmdConfigSet returns a Command instance for 'config set' sub command func NewCmdConfigSet(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &setOptions{configAccess: configAccess} cmd := &cobra.Command{ Use: "set PROPERTY_NAME PROPERTY_VALUE", DisableFlagsInUseLine: true, Short: i18n.T("Set an individual value in a kubeconfig file"), Long: setLong, Example: setExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.complete(cmd)) cmdutil.CheckErr(options.run()) fmt.Fprintf(out, "Property %q set.\n", options.propertyName) }, } f := cmd.Flags().VarPF(&options.setRawBytes, "set-raw-bytes", "", "When writing a []byte PROPERTY_VALUE, write the given string directly without base64 decoding.") f.NoOptDefVal = "true" return cmd } func (o setOptions) run() error { err := o.validate() if err != nil { return err } config, err := o.configAccess.GetStartingConfig() if err != nil { return err } steps, err := newNavigationSteps(o.propertyName) if err != nil { return err } setRawBytes := false if o.setRawBytes.Provided() { setRawBytes = o.setRawBytes.Value() } err = modifyConfig(reflect.ValueOf(config), steps, o.propertyValue, false, setRawBytes) if err != nil { return err } if err := clientcmd.ModifyConfig(o.configAccess, *config, false); err != nil { return err } return nil } func (o *setOptions) complete(cmd *cobra.Command) error { endingArgs := cmd.Flags().Args() if len(endingArgs) != 2 { return helpErrorf(cmd, "Unexpected args: %v", endingArgs) } o.propertyValue = endingArgs[1] o.propertyName = endingArgs[0] return nil } func (o setOptions) validate() error { if len(o.propertyValue) == 0 { return errors.New("you cannot use set to unset a property") } if len(o.propertyName) == 0 { return errors.New("you must specify a property") } return nil } func modifyConfig(curr reflect.Value, steps *navigationSteps, propertyValue string, unset bool, setRawBytes bool) error { currStep := steps.pop() actualCurrValue := curr if curr.Kind() == reflect.Pointer { actualCurrValue = curr.Elem() } switch actualCurrValue.Kind() { case reflect.Map: if !steps.moreStepsRemaining() && !unset { return fmt.Errorf("can't set a map to a value: %v", actualCurrValue) } mapKey := reflect.ValueOf(currStep.stepValue) mapValueType := curr.Type().Elem().Elem() if !steps.moreStepsRemaining() && unset { actualCurrValue.SetMapIndex(mapKey, reflect.Value{}) return nil } currMapValue := actualCurrValue.MapIndex(mapKey) needToSetNewMapValue := currMapValue.Kind() == reflect.Invalid if needToSetNewMapValue { if unset { return fmt.Errorf("current map key `%v` is invalid", mapKey.Interface()) } currMapValue = reflect.New(mapValueType.Elem()).Elem().Addr() actualCurrValue.SetMapIndex(mapKey, currMapValue) } err := modifyConfig(currMapValue, steps, propertyValue, unset, setRawBytes) if err != nil { return err } return nil case reflect.String: if steps.moreStepsRemaining() { return fmt.Errorf("can't have more steps after a string. %v", steps) } actualCurrValue.SetString(propertyValue) return nil case reflect.Slice: if steps.moreStepsRemaining() { return fmt.Errorf("can't have more steps after bytes. %v", steps) } innerKind := actualCurrValue.Type().Elem().Kind() if innerKind != reflect.Uint8 { return fmt.Errorf("unrecognized slice type. %v", innerKind) } if unset { actualCurrValue.Set(reflect.Zero(actualCurrValue.Type())) return nil } if setRawBytes { actualCurrValue.SetBytes([]byte(propertyValue)) } else { val, err := base64.StdEncoding.DecodeString(propertyValue) if err != nil { return fmt.Errorf("error decoding input value: %v", err) } actualCurrValue.SetBytes(val) } return nil case reflect.Bool: if steps.moreStepsRemaining() { return fmt.Errorf("can't have more steps after a bool. %v", steps) } boolValue, err := toBool(propertyValue) if err != nil { return err } actualCurrValue.SetBool(boolValue) return nil case reflect.Struct: for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { currFieldValue := actualCurrValue.Field(fieldIndex) currFieldType := actualCurrValue.Type().Field(fieldIndex) currYamlTag := currFieldType.Tag.Get("json") currFieldTypeYamlName := strings.Split(currYamlTag, ",")[0] if currFieldTypeYamlName == currStep.stepValue { thisMapHasNoValue := (currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil()) if thisMapHasNoValue { newValue := reflect.MakeMap(currFieldValue.Type()) currFieldValue.Set(newValue) if !steps.moreStepsRemaining() && unset { return nil } } if !steps.moreStepsRemaining() && unset { // if we're supposed to unset the value or if the value is a map that doesn't exist, create a new value and overwrite newValue := reflect.New(currFieldValue.Type()).Elem() currFieldValue.Set(newValue) return nil } return modifyConfig(currFieldValue.Addr(), steps, propertyValue, unset, setRawBytes) } } return fmt.Errorf("unable to locate path %#v under %v", currStep, actualCurrValue) } panic(fmt.Errorf("unrecognized type: %v", actualCurrValue)) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set_cluster.go000066400000000000000000000155161504711711200307470ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "fmt" "io" "os" "path/filepath" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cliflag "k8s.io/component-base/cli/flag" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) type setClusterOptions struct { configAccess clientcmd.ConfigAccess name string server cliflag.StringFlag tlsServerName cliflag.StringFlag insecureSkipTLSVerify cliflag.Tristate certificateAuthority cliflag.StringFlag embedCAData cliflag.Tristate proxyURL cliflag.StringFlag } var ( setClusterLong = templates.LongDesc(i18n.T(` Set a cluster entry in kubeconfig. Specifying a name that already exists will merge new fields on top of existing values for those fields.`)) setClusterExample = templates.Examples(` # Set only the server field on the e2e cluster entry without touching other values kubectl config set-cluster e2e --server=https://1.2.3.4 # Embed certificate authority data for the e2e cluster entry kubectl config set-cluster e2e --embed-certs --certificate-authority=~/.kube/e2e/kubernetes.ca.crt # Disable cert checking for the e2e cluster entry kubectl config set-cluster e2e --insecure-skip-tls-verify=true # Set the custom TLS server name to use for validation for the e2e cluster entry kubectl config set-cluster e2e --tls-server-name=my-cluster-name # Set the proxy URL for the e2e cluster entry kubectl config set-cluster e2e --proxy-url=https://1.2.3.4`) ) // NewCmdConfigSetCluster returns a Command instance for 'config set-cluster' sub command func NewCmdConfigSetCluster(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &setClusterOptions{configAccess: configAccess} cmd := &cobra.Command{ Use: fmt.Sprintf("set-cluster NAME [--%v=server] [--%v=path/to/certificate/authority] [--%v=true] [--%v=example.com]", clientcmd.FlagAPIServer, clientcmd.FlagCAFile, clientcmd.FlagInsecure, clientcmd.FlagTLSServerName), DisableFlagsInUseLine: true, Short: i18n.T("Set a cluster entry in kubeconfig"), Long: setClusterLong, Example: setClusterExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.complete(cmd)) cmdutil.CheckErr(options.run()) fmt.Fprintf(out, "Cluster %q set.\n", options.name) }, } options.insecureSkipTLSVerify.Default(false) cmd.Flags().Var(&options.server, clientcmd.FlagAPIServer, clientcmd.FlagAPIServer+" for the cluster entry in kubeconfig") cmd.Flags().Var(&options.tlsServerName, clientcmd.FlagTLSServerName, clientcmd.FlagTLSServerName+" for the cluster entry in kubeconfig") f := cmd.Flags().VarPF(&options.insecureSkipTLSVerify, clientcmd.FlagInsecure, "", clientcmd.FlagInsecure+" for the cluster entry in kubeconfig") f.NoOptDefVal = "true" cmd.Flags().Var(&options.certificateAuthority, clientcmd.FlagCAFile, "Path to "+clientcmd.FlagCAFile+" file for the cluster entry in kubeconfig") cmd.MarkFlagFilename(clientcmd.FlagCAFile) f = cmd.Flags().VarPF(&options.embedCAData, clientcmd.FlagEmbedCerts, "", clientcmd.FlagEmbedCerts+" for the cluster entry in kubeconfig") f.NoOptDefVal = "true" cmd.Flags().Var(&options.proxyURL, clientcmd.FlagProxyURL, clientcmd.FlagProxyURL+" for the cluster entry in kubeconfig") return cmd } func (o setClusterOptions) run() error { err := o.validate() if err != nil { return err } config, err := o.configAccess.GetStartingConfig() if err != nil { return err } startingStanza, exists := config.Clusters[o.name] if !exists { startingStanza = clientcmdapi.NewCluster() } cluster := o.modifyCluster(*startingStanza) config.Clusters[o.name] = &cluster if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return err } return nil } func (o *setClusterOptions) modifyCluster(existingCluster clientcmdapi.Cluster) clientcmdapi.Cluster { modifiedCluster := existingCluster if o.server.Provided() { modifiedCluster.Server = o.server.Value() // specifying a --server on the command line, overrides the TLSServerName that was specified in the kubeconfig file. // if both are specified, then the next if block will write the new TLSServerName. modifiedCluster.TLSServerName = "" } if o.tlsServerName.Provided() { modifiedCluster.TLSServerName = o.tlsServerName.Value() } if o.insecureSkipTLSVerify.Provided() { modifiedCluster.InsecureSkipTLSVerify = o.insecureSkipTLSVerify.Value() // Specifying insecure mode clears any certificate authority if modifiedCluster.InsecureSkipTLSVerify { modifiedCluster.CertificateAuthority = "" modifiedCluster.CertificateAuthorityData = nil } } if o.certificateAuthority.Provided() { caPath := o.certificateAuthority.Value() if o.embedCAData.Value() { modifiedCluster.CertificateAuthorityData, _ = os.ReadFile(caPath) modifiedCluster.InsecureSkipTLSVerify = false modifiedCluster.CertificateAuthority = "" } else { caPath, _ = filepath.Abs(caPath) modifiedCluster.CertificateAuthority = caPath // Specifying a certificate authority file clears certificate authority data and insecure mode if caPath != "" { modifiedCluster.InsecureSkipTLSVerify = false modifiedCluster.CertificateAuthorityData = nil } } } if o.proxyURL.Provided() { modifiedCluster.ProxyURL = o.proxyURL.Value() } return modifiedCluster } func (o *setClusterOptions) complete(cmd *cobra.Command) error { args := cmd.Flags().Args() if len(args) != 1 { return helpErrorf(cmd, "Unexpected args: %v", args) } o.name = args[0] return nil } func (o setClusterOptions) validate() error { if len(o.name) == 0 { return errors.New("you must specify a non-empty cluster name") } if o.insecureSkipTLSVerify.Value() && o.certificateAuthority.Value() != "" { return errors.New("you cannot specify a certificate authority and insecure mode at the same time") } if o.embedCAData.Value() { caPath := o.certificateAuthority.Value() if caPath == "" { return fmt.Errorf("you must specify a --%s to embed", clientcmd.FlagCAFile) } if _, err := os.Stat(caPath); err != nil { return fmt.Errorf("could not stat %s file %s: %v", clientcmd.FlagCAFile, caPath, err) } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set_cluster_test.go000066400000000000000000000155261504711711200320070ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "os" "testing" utiltesting "k8s.io/client-go/util/testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type setClusterTest struct { description string config clientcmdapi.Config args []string flags []string expected string expectedConfig clientcmdapi.Config } func TestCreateCluster(t *testing.T) { conf := clientcmdapi.Config{} test := setClusterTest{ description: "Testing 'kubectl config set-cluster' with a new cluster", config: conf, args: []string{"my-cluster"}, flags: []string{ "--server=http://192.168.0.1", "--tls-server-name=my-cluster-name", }, expected: `Cluster "my-cluster" set.` + "\n", expectedConfig: clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "http://192.168.0.1", TLSServerName: "my-cluster-name"}, }, }, } test.run(t) } func TestCreateClusterWithProxy(t *testing.T) { conf := clientcmdapi.Config{} test := setClusterTest{ description: "Testing 'kubectl config set-cluster' with a new cluster", config: conf, args: []string{"my-cluster"}, flags: []string{ "--server=http://192.168.0.1", "--tls-server-name=my-cluster-name", "--proxy-url=http://192.168.0.2", }, expected: `Cluster "my-cluster" set.` + "\n", expectedConfig: clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": { Server: "http://192.168.0.1", TLSServerName: "my-cluster-name", ProxyURL: "http://192.168.0.2", }, }, }, } test.run(t) } func TestModifyCluster(t *testing.T) { conf := clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "https://192.168.0.1", TLSServerName: "to-be-cleared"}, }, } test := setClusterTest{ description: "Testing 'kubectl config set-cluster' with an existing cluster", config: conf, args: []string{"my-cluster"}, flags: []string{ "--server=https://192.168.0.99", }, expected: `Cluster "my-cluster" set.` + "\n", expectedConfig: clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "https://192.168.0.99"}, }, }, } test.run(t) } // TestModifyClusterWithProxy tests setting proxy-url in kubeconfig func TestModifyClusterWithProxy(t *testing.T) { conf := clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "https://192.168.0.1", TLSServerName: "to-be-cleared"}, }, } test := setClusterTest{ description: "Testing 'kubectl config set-cluster' with an existing cluster", config: conf, args: []string{"my-cluster"}, flags: []string{ "--server=https://192.168.0.99", "--proxy-url=https://192.168.0.100", }, expected: `Cluster "my-cluster" set.` + "\n", expectedConfig: clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "https://192.168.0.99", ProxyURL: "https://192.168.0.100"}, }, }, } test.run(t) } // TestModifyClusterWithProxyOverride tests updating proxy-url // in kubeconfig which already exists func TestModifyClusterWithProxyOverride(t *testing.T) { conf := clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": { Server: "https://192.168.0.1", TLSServerName: "to-be-cleared", ProxyURL: "https://192.168.0.2", }, }, } test := setClusterTest{ description: "Testing 'kubectl config set-cluster' with an existing cluster", config: conf, args: []string{"my-cluster"}, flags: []string{ "--server=https://192.168.0.99", "--proxy-url=https://192.168.0.100", }, expected: `Cluster "my-cluster" set.` + "\n", expectedConfig: clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "https://192.168.0.99", ProxyURL: "https://192.168.0.100"}, }, }, } test.run(t) } func TestModifyClusterServerAndTLS(t *testing.T) { conf := clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "https://192.168.0.1"}, }, } test := setClusterTest{ description: "Testing 'kubectl config set-cluster' with an existing cluster", config: conf, args: []string{"my-cluster"}, flags: []string{ "--server=https://192.168.0.99", "--tls-server-name=my-cluster-name", }, expected: `Cluster "my-cluster" set.` + "\n", expectedConfig: clientcmdapi.Config{ Clusters: map[string]*clientcmdapi.Cluster{ "my-cluster": {Server: "https://192.168.0.99", TLSServerName: "my-cluster-name"}, }, }, } test.run(t) } func (test setClusterTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigSetCluster(buf, pathOptions) cmd.SetArgs(test.args) cmd.Flags().Parse(test.flags) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v, args: %v, flags: %v", err, test.args, test.flags) } config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("Failed in %q\n expected %v\n but got %v", test.description, test.expected, buf.String()) } } if len(test.args) > 0 { cluster, ok := config.Clusters[test.args[0]] if !ok { t.Errorf("expected cluster %v, but got nil", test.args[0]) return } if cluster.Server != test.expectedConfig.Clusters[test.args[0]].Server { t.Errorf("Fail in %q\n expected cluster server %v\n but got %v\n ", test.description, test.expectedConfig.Clusters[test.args[0]].Server, cluster.Server) } if cluster.TLSServerName != test.expectedConfig.Clusters[test.args[0]].TLSServerName { t.Errorf("Fail in %q\n expected cluster TLS server name %q\n but got %q\n ", test.description, test.expectedConfig.Clusters[test.args[0]].TLSServerName, cluster.TLSServerName) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set_context.go000066400000000000000000000117771504711711200307570ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "fmt" "io" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cliflag "k8s.io/component-base/cli/flag" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) type setContextOptions struct { configAccess clientcmd.ConfigAccess name string currContext bool cluster cliflag.StringFlag authInfo cliflag.StringFlag namespace cliflag.StringFlag } var ( setContextLong = templates.LongDesc(i18n.T(` Set a context entry in kubeconfig. Specifying a name that already exists will merge new fields on top of existing values for those fields.`)) setContextExample = templates.Examples(` # Set the user field on the gce context entry without touching other values kubectl config set-context gce --user=cluster-admin`) ) // NewCmdConfigSetContext returns a Command instance for 'config set-context' sub command func NewCmdConfigSetContext(restClientGetter genericclioptions.RESTClientGetter, out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &setContextOptions{configAccess: configAccess} cmd := &cobra.Command{ Use: fmt.Sprintf("set-context [NAME | --current] [--%v=cluster_nickname] [--%v=user_nickname] [--%v=namespace]", clientcmd.FlagClusterName, clientcmd.FlagAuthInfoName, clientcmd.FlagNamespace), DisableFlagsInUseLine: true, Short: i18n.T("Set a context entry in kubeconfig"), Long: setContextLong, Example: setContextExample, ValidArgsFunction: completion.ContextCompletionFunc, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.complete(cmd)) name, exists, err := options.run() cmdutil.CheckErr(err) if exists { fmt.Fprintf(out, "Context %q modified.\n", name) } else { fmt.Fprintf(out, "Context %q created.\n", name) } }, } cmd.Flags().BoolVar(&options.currContext, "current", options.currContext, "Modify the current context") cmd.Flags().Var(&options.cluster, clientcmd.FlagClusterName, clientcmd.FlagClusterName+" for the context entry in kubeconfig") cmd.Flags().Var(&options.authInfo, clientcmd.FlagAuthInfoName, clientcmd.FlagAuthInfoName+" for the context entry in kubeconfig") cmd.Flags().Var(&options.namespace, clientcmd.FlagNamespace, clientcmd.FlagNamespace+" for the context entry in kubeconfig") cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( "namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.CompGetResource(cmdutil.NewFactory(restClientGetter), "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp }, )) return cmd } func (o setContextOptions) run() (string, bool, error) { err := o.validate() if err != nil { return "", false, err } config, err := o.configAccess.GetStartingConfig() if err != nil { return "", false, err } name := o.name if o.currContext { if len(config.CurrentContext) == 0 { return "", false, errors.New("no current context is set") } name = config.CurrentContext } startingStanza, exists := config.Contexts[name] if !exists { startingStanza = clientcmdapi.NewContext() } context := o.modifyContext(*startingStanza) config.Contexts[name] = &context if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return name, exists, err } return name, exists, nil } func (o *setContextOptions) modifyContext(existingContext clientcmdapi.Context) clientcmdapi.Context { modifiedContext := existingContext if o.cluster.Provided() { modifiedContext.Cluster = o.cluster.Value() } if o.authInfo.Provided() { modifiedContext.AuthInfo = o.authInfo.Value() } if o.namespace.Provided() { modifiedContext.Namespace = o.namespace.Value() } return modifiedContext } func (o *setContextOptions) complete(cmd *cobra.Command) error { args := cmd.Flags().Args() if len(args) > 1 { return helpErrorf(cmd, "Unexpected args: %v", args) } if len(args) == 1 { o.name = args[0] } return nil } func (o setContextOptions) validate() error { if len(o.name) == 0 && !o.currContext { return errors.New("you must specify a non-empty context name or --current") } if len(o.name) > 0 && o.currContext { return errors.New("you cannot specify both a context name and --current") } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set_context_test.go000066400000000000000000000124571504711711200320120ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "os" "testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" utiltesting "k8s.io/client-go/util/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) type setContextTest struct { description string testContext string // name of the context being modified config clientcmdapi.Config //initiate kubectl config args []string //kubectl set-context args flags []string //kubectl set-context flags expected string //expectd out expectedConfig clientcmdapi.Config //expect kubectl config } func TestCreateContext(t *testing.T) { conf := clientcmdapi.Config{} test := setContextTest{ testContext: "shaker-context", description: "Testing for create a new context", config: conf, args: []string{"shaker-context"}, flags: []string{ "--cluster=cluster_nickname", "--user=user_nickname", "--namespace=namespace", }, expected: `Context "shaker-context" created.` + "\n", expectedConfig: clientcmdapi.Config{ Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "user_nickname", Cluster: "cluster_nickname", Namespace: "namespace"}}, }, } test.run(t) } func TestModifyContext(t *testing.T) { conf := clientcmdapi.Config{ Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}, "not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}} test := setContextTest{ testContext: "shaker-context", description: "Testing for modify a already exist context", config: conf, args: []string{"shaker-context"}, flags: []string{ "--cluster=cluster_nickname", "--user=user_nickname", "--namespace=namespace", }, expected: `Context "shaker-context" modified.` + "\n", expectedConfig: clientcmdapi.Config{ Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "user_nickname", Cluster: "cluster_nickname", Namespace: "namespace"}, "not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}, } test.run(t) } func TestModifyCurrentContext(t *testing.T) { conf := clientcmdapi.Config{ CurrentContext: "shaker-context", Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}, "not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}} test := setContextTest{ testContext: "shaker-context", description: "Testing for modify a current context", config: conf, args: []string{}, flags: []string{ "--current", "--cluster=cluster_nickname", "--user=user_nickname", "--namespace=namespace", }, expected: `Context "shaker-context" modified.` + "\n", expectedConfig: clientcmdapi.Config{ Contexts: map[string]*clientcmdapi.Context{ "shaker-context": {AuthInfo: "user_nickname", Cluster: "cluster_nickname", Namespace: "namespace"}, "not-this": {AuthInfo: "blue-user", Cluster: "big-cluster", Namespace: "saw-ns"}}}, } test.run(t) } func (test setContextTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() cmd := NewCmdConfigSetContext(tf, buf, pathOptions) cmd.SetArgs(test.args) cmd.Flags().Parse(test.flags) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v,kubectl set-context args: %v,flags: %v", err, test.args, test.flags) } config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("Fail in %q:\n expected %v\n but got %v\n", test.description, test.expected, buf.String()) } } if test.expectedConfig.Contexts != nil { expectContext := test.expectedConfig.Contexts[test.testContext] actualContext := config.Contexts[test.testContext] if expectContext.AuthInfo != actualContext.AuthInfo || expectContext.Cluster != actualContext.Cluster || expectContext.Namespace != actualContext.Namespace { t.Errorf("Fail in %q:\n expected Context %v\n but found %v in kubeconfig\n", test.description, expectContext, actualContext) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set_credentials.go000066400000000000000000000376361504711711200315720ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cliflag "k8s.io/component-base/cli/flag" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) type setCredentialsOptions struct { configAccess clientcmd.ConfigAccess name string clientCertificate cliflag.StringFlag clientKey cliflag.StringFlag token cliflag.StringFlag `datapolicy:"token"` username cliflag.StringFlag password cliflag.StringFlag `datapolicy:"password"` embedCertData cliflag.Tristate authProvider cliflag.StringFlag authProviderArgs map[string]string authProviderArgsToRemove []string execCommand cliflag.StringFlag execAPIVersion cliflag.StringFlag execInteractiveMode cliflag.StringFlag execProvideClusterInfo cliflag.Tristate execArgs []string execEnv map[string]string execEnvToRemove []string } const ( flagAuthProvider = "auth-provider" flagAuthProviderArg = "auth-provider-arg" flagExecCommand = "exec-command" flagExecAPIVersion = "exec-api-version" flagExecArg = "exec-arg" flagExecEnv = "exec-env" flagExecInteractiveMode = "exec-interactive-mode" flagExecProvideClusterInfo = "exec-provide-cluster-info" ) var ( setCredentialsLong = fmt.Sprintf(templates.LongDesc(i18n.T(` Set a user entry in kubeconfig. Specifying a name that already exists will merge new fields on top of existing values. Client-certificate flags: --%v=certfile --%v=keyfile Bearer token flags: --%v=bearer_token Basic auth flags: --%v=basic_user --%v=basic_password Bearer token and basic auth are mutually exclusive.`)), clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword) setCredentialsExample = templates.Examples(` # Set only the "client-key" field on the "cluster-admin" # entry, without touching other values kubectl config set-credentials cluster-admin --client-key=~/.kube/admin.key # Set basic auth for the "cluster-admin" entry kubectl config set-credentials cluster-admin --username=admin --password=uXFGweU9l35qcif # Embed client certificate data in the "cluster-admin" entry kubectl config set-credentials cluster-admin --client-certificate=~/.kube/admin.crt --embed-certs=true # Enable the Google Compute Platform auth provider for the "cluster-admin" entry kubectl config set-credentials cluster-admin --auth-provider=gcp # Enable the OpenID Connect auth provider for the "cluster-admin" entry with additional arguments kubectl config set-credentials cluster-admin --auth-provider=oidc --auth-provider-arg=client-id=foo --auth-provider-arg=client-secret=bar # Remove the "client-secret" config value for the OpenID Connect auth provider for the "cluster-admin" entry kubectl config set-credentials cluster-admin --auth-provider=oidc --auth-provider-arg=client-secret- # Enable new exec auth plugin for the "cluster-admin" entry kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta1 # Enable new exec auth plugin for the "cluster-admin" entry with interactive mode kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta1 --exec-interactive-mode=Never # Define new exec auth plugin arguments for the "cluster-admin" entry kubectl config set-credentials cluster-admin --exec-arg=arg1 --exec-arg=arg2 # Create or update exec auth plugin environment variables for the "cluster-admin" entry kubectl config set-credentials cluster-admin --exec-env=key1=val1 --exec-env=key2=val2 # Remove exec auth plugin environment variables for the "cluster-admin" entry kubectl config set-credentials cluster-admin --exec-env=var-to-remove-`) ) // NewCmdConfigSetCredentials returns a Command instance for 'config set-credentials' sub command func NewCmdConfigSetCredentials(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &setCredentialsOptions{configAccess: configAccess} return newCmdConfigSetCredentials(out, options) } // NewCmdConfigSetAuthInfo returns a Command instance for 'config set-credentials' sub command // DEPRECATED: Use NewCmdConfigSetCredentials instead func NewCmdConfigSetAuthInfo(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { return NewCmdConfigSetCredentials(out, configAccess) } func newCmdConfigSetCredentials(out io.Writer, options *setCredentialsOptions) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf( "set-credentials NAME [--%v=path/to/certfile] "+ "[--%v=path/to/keyfile] "+ "[--%v=bearer_token] "+ "[--%v=basic_user] "+ "[--%v=basic_password] "+ "[--%v=provider_name] "+ "[--%v=key=value] "+ "[--%v=exec_command] "+ "[--%v=exec_api_version] "+ "[--%v=arg] "+ "[--%v=key=value]", clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword, flagAuthProvider, flagAuthProviderArg, flagExecCommand, flagExecAPIVersion, flagExecArg, flagExecEnv, ), DisableFlagsInUseLine: true, Short: i18n.T("Set a user entry in kubeconfig"), Long: setCredentialsLong, Example: setCredentialsExample, Run: func(cmd *cobra.Command, args []string) { err := options.complete(cmd) if err != nil { cmd.Help() cmdutil.CheckErr(err) } cmdutil.CheckErr(options.run()) fmt.Fprintf(out, "User %q set.\n", options.name) }, } cmd.Flags().Var(&options.clientCertificate, clientcmd.FlagCertFile, "Path to "+clientcmd.FlagCertFile+" file for the user entry in kubeconfig") cmd.MarkFlagFilename(clientcmd.FlagCertFile) cmd.Flags().Var(&options.clientKey, clientcmd.FlagKeyFile, "Path to "+clientcmd.FlagKeyFile+" file for the user entry in kubeconfig") cmd.MarkFlagFilename(clientcmd.FlagKeyFile) cmd.Flags().Var(&options.token, clientcmd.FlagBearerToken, clientcmd.FlagBearerToken+" for the user entry in kubeconfig") cmd.Flags().Var(&options.username, clientcmd.FlagUsername, clientcmd.FlagUsername+" for the user entry in kubeconfig") cmd.Flags().Var(&options.password, clientcmd.FlagPassword, clientcmd.FlagPassword+" for the user entry in kubeconfig") cmd.Flags().Var(&options.authProvider, flagAuthProvider, "Auth provider for the user entry in kubeconfig") cmd.Flags().StringSlice(flagAuthProviderArg, nil, "'key=value' arguments for the auth provider") cmd.Flags().Var(&options.execCommand, flagExecCommand, "Command for the exec credential plugin for the user entry in kubeconfig") cmd.Flags().Var(&options.execAPIVersion, flagExecAPIVersion, "API version of the exec credential plugin for the user entry in kubeconfig") cmd.Flags().Var(&options.execInteractiveMode, flagExecInteractiveMode, "InteractiveMode of the exec credentials plugin for the user entry in kubeconfig") flagClusterInfo := cmd.Flags().VarPF(&options.execProvideClusterInfo, flagExecProvideClusterInfo, "", "ProvideClusterInfo of the exec credentials plugin for the user entry in kubeconfig") flagClusterInfo.NoOptDefVal = "true" cmd.Flags().StringSlice(flagExecArg, nil, "New arguments for the exec credential plugin command for the user entry in kubeconfig") cmd.Flags().StringArray(flagExecEnv, nil, "'key=value' environment values for the exec credential plugin") f := cmd.Flags().VarPF(&options.embedCertData, clientcmd.FlagEmbedCerts, "", "Embed client cert/key for the user entry in kubeconfig") f.NoOptDefVal = "true" return cmd } func (o setCredentialsOptions) run() error { err := o.validate() if err != nil { return err } config, err := o.configAccess.GetStartingConfig() if err != nil { return err } startingStanza, exists := config.AuthInfos[o.name] if !exists { startingStanza = clientcmdapi.NewAuthInfo() } authInfo := o.modifyAuthInfo(*startingStanza) config.AuthInfos[o.name] = &authInfo if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return err } return nil } func (o *setCredentialsOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.AuthInfo) clientcmdapi.AuthInfo { modifiedAuthInfo := existingAuthInfo var setToken, setBasic bool if o.clientCertificate.Provided() { certPath := o.clientCertificate.Value() if o.embedCertData.Value() { modifiedAuthInfo.ClientCertificateData, _ = os.ReadFile(certPath) modifiedAuthInfo.ClientCertificate = "" } else { certPath, _ = filepath.Abs(certPath) modifiedAuthInfo.ClientCertificate = certPath if len(modifiedAuthInfo.ClientCertificate) > 0 { modifiedAuthInfo.ClientCertificateData = nil } } } if o.clientKey.Provided() { keyPath := o.clientKey.Value() if o.embedCertData.Value() { modifiedAuthInfo.ClientKeyData, _ = os.ReadFile(keyPath) modifiedAuthInfo.ClientKey = "" } else { keyPath, _ = filepath.Abs(keyPath) modifiedAuthInfo.ClientKey = keyPath if len(modifiedAuthInfo.ClientKey) > 0 { modifiedAuthInfo.ClientKeyData = nil } } } if o.token.Provided() { modifiedAuthInfo.Token = o.token.Value() setToken = len(modifiedAuthInfo.Token) > 0 } if o.username.Provided() { modifiedAuthInfo.Username = o.username.Value() setBasic = setBasic || len(modifiedAuthInfo.Username) > 0 } if o.password.Provided() { modifiedAuthInfo.Password = o.password.Value() setBasic = setBasic || len(modifiedAuthInfo.Password) > 0 } if o.authProvider.Provided() { newName := o.authProvider.Value() // Only overwrite if the existing auth-provider is nil, or different than the newly specified one. if modifiedAuthInfo.AuthProvider == nil || modifiedAuthInfo.AuthProvider.Name != newName { modifiedAuthInfo.AuthProvider = &clientcmdapi.AuthProviderConfig{ Name: newName, } } } if modifiedAuthInfo.AuthProvider != nil { if modifiedAuthInfo.AuthProvider.Config == nil { modifiedAuthInfo.AuthProvider.Config = make(map[string]string) } for _, toRemove := range o.authProviderArgsToRemove { delete(modifiedAuthInfo.AuthProvider.Config, toRemove) } for key, value := range o.authProviderArgs { modifiedAuthInfo.AuthProvider.Config[key] = value } } if o.execCommand.Provided() { newExecCommand := o.execCommand.Value() // create new Exec if doesn't exist, otherwise just modify the command if modifiedAuthInfo.Exec == nil { modifiedAuthInfo.Exec = &clientcmdapi.ExecConfig{ Command: newExecCommand, } } else { modifiedAuthInfo.Exec.Command = newExecCommand // explicitly reset exec arguments modifiedAuthInfo.Exec.Args = nil } } // modify next values only if Exec exists, ignore these changes otherwise if modifiedAuthInfo.Exec != nil { if o.execAPIVersion.Provided() { modifiedAuthInfo.Exec.APIVersion = o.execAPIVersion.Value() } // rewrite exec arguments list with new values if o.execArgs != nil { modifiedAuthInfo.Exec.Args = o.execArgs } if o.execInteractiveMode.Provided() { modifiedAuthInfo.Exec.InteractiveMode = clientcmdapi.ExecInteractiveMode(o.execInteractiveMode.Value()) } if o.execProvideClusterInfo.Provided() { modifiedAuthInfo.Exec.ProvideClusterInfo = o.execProvideClusterInfo.Value() } // iterate over the existing exec env values and remove the specified if o.execEnvToRemove != nil { newExecEnv := []clientcmdapi.ExecEnvVar{} for _, value := range modifiedAuthInfo.Exec.Env { needToRemove := false for _, elemToRemove := range o.execEnvToRemove { if value.Name == elemToRemove { needToRemove = true break } } if !needToRemove { newExecEnv = append(newExecEnv, value) } } modifiedAuthInfo.Exec.Env = newExecEnv } // update or create specified environment variables for the exec plugin if o.execEnv != nil { newEnv := []clientcmdapi.ExecEnvVar{} for newEnvName, newEnvValue := range o.execEnv { needToCreate := true for i := 0; i < len(modifiedAuthInfo.Exec.Env); i++ { if modifiedAuthInfo.Exec.Env[i].Name == newEnvName { // update the existing value needToCreate = false modifiedAuthInfo.Exec.Env[i].Value = newEnvValue break } } if needToCreate { // create a new env value newEnv = append(newEnv, clientcmdapi.ExecEnvVar{Name: newEnvName, Value: newEnvValue}) } } modifiedAuthInfo.Exec.Env = append(modifiedAuthInfo.Exec.Env, newEnv...) } } // If any auth info was set, make sure any other existing auth types are cleared if setToken || setBasic { if !setToken { modifiedAuthInfo.Token = "" } if !setBasic { modifiedAuthInfo.Username = "" modifiedAuthInfo.Password = "" } } return modifiedAuthInfo } func (o *setCredentialsOptions) complete(cmd *cobra.Command) error { args := cmd.Flags().Args() if len(args) != 1 { return fmt.Errorf("unexpected args: %v", args) } authProviderArgs, err := cmd.Flags().GetStringSlice(flagAuthProviderArg) if err != nil { return err } if len(authProviderArgs) > 0 { newPairs, removePairs, err := cmdutil.ParsePairs(authProviderArgs, flagAuthProviderArg, true) if err != nil { return err } o.authProviderArgs = newPairs o.authProviderArgsToRemove = removePairs } execArgs, err := cmd.Flags().GetStringSlice(flagExecArg) if err != nil { return err } if len(execArgs) > 0 { o.execArgs = execArgs } execEnv, err := cmd.Flags().GetStringArray(flagExecEnv) if err != nil { return err } if len(execEnv) > 0 { newPairs, removePairs, err := cmdutil.ParsePairs(execEnv, flagExecEnv, true) if err != nil { return err } o.execEnv = newPairs o.execEnvToRemove = removePairs } o.name = args[0] return nil } func (o setCredentialsOptions) validate() error { if len(o.name) == 0 { return errors.New("you must specify a non-empty user name") } methods := []string{} if len(o.token.Value()) > 0 { methods = append(methods, fmt.Sprintf("--%v", clientcmd.FlagBearerToken)) } if len(o.username.Value()) > 0 || len(o.password.Value()) > 0 { methods = append(methods, fmt.Sprintf("--%v/--%v", clientcmd.FlagUsername, clientcmd.FlagPassword)) } if len(methods) > 1 { return fmt.Errorf("you cannot specify more than one authentication method at the same time: %v", strings.Join(methods, ", ")) } if o.embedCertData.Value() { certPath := o.clientCertificate.Value() keyPath := o.clientKey.Value() if certPath == "" && keyPath == "" { return fmt.Errorf("you must specify a --%s or --%s to embed", clientcmd.FlagCertFile, clientcmd.FlagKeyFile) } if certPath != "" { if _, err := os.Stat(certPath); err != nil { return fmt.Errorf("could not stat %s file %s: %v", clientcmd.FlagCertFile, certPath, err) } } if keyPath != "" { if _, err := os.Stat(keyPath); err != nil { return fmt.Errorf("could not stat %s file %s: %v", clientcmd.FlagKeyFile, keyPath, err) } } } if o.execInteractiveMode.Provided() { interactiveMode := o.execInteractiveMode.Value() if interactiveMode != string(clientcmdapi.IfAvailableExecInteractiveMode) && interactiveMode != string(clientcmdapi.AlwaysExecInteractiveMode) && interactiveMode != string(clientcmdapi.NeverExecInteractiveMode) { return fmt.Errorf("invalid interactive mode type, can be only IfAvailable, Never, Always") } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set_credentials_test.go000066400000000000000000000313771504711711200326250ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" utiltesting "k8s.io/client-go/util/testing" "os" "reflect" "testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cliflag "k8s.io/component-base/cli/flag" ) func stringFlagFor(s string) cliflag.StringFlag { var f cliflag.StringFlag f.Set(s) return f } func TestSetCredentialsOptions(t *testing.T) { tests := []struct { name string flags []string wantParseErr bool wantCompleteErr bool wantValidateErr bool wantOptions *setCredentialsOptions }{ { name: "test1", flags: []string{ "me", }, wantOptions: &setCredentialsOptions{ name: "me", }, }, { name: "test2", flags: []string{ "me", "--token=foo", }, wantOptions: &setCredentialsOptions{ name: "me", token: stringFlagFor("foo"), }, }, { name: "test3", flags: []string{ "me", "--username=jane", "--password=bar", }, wantOptions: &setCredentialsOptions{ name: "me", username: stringFlagFor("jane"), password: stringFlagFor("bar"), }, }, { name: "test4", // Cannot provide both token and basic auth. flags: []string{ "me", "--token=foo", "--username=jane", "--password=bar", }, wantValidateErr: true, }, { name: "test5", flags: []string{ "--auth-provider=oidc", "--auth-provider-arg=client-id=foo", "--auth-provider-arg=client-secret=bar", "me", }, wantOptions: &setCredentialsOptions{ name: "me", authProvider: stringFlagFor("oidc"), authProviderArgs: map[string]string{ "client-id": "foo", "client-secret": "bar", }, authProviderArgsToRemove: []string{}, }, }, { name: "test6", flags: []string{ "--auth-provider=oidc", "--auth-provider-arg=client-id-", "--auth-provider-arg=client-secret-", "me", }, wantOptions: &setCredentialsOptions{ name: "me", authProvider: stringFlagFor("oidc"), authProviderArgs: map[string]string{}, authProviderArgsToRemove: []string{ "client-id", "client-secret", }, }, }, { name: "test7", flags: []string{ "--auth-provider-arg=client-id-", // auth provider name not required "--auth-provider-arg=client-secret-", "me", }, wantOptions: &setCredentialsOptions{ name: "me", authProviderArgs: map[string]string{}, authProviderArgsToRemove: []string{ "client-id", "client-secret", }, }, }, { name: "test8", flags: []string{ "--auth-provider=oidc", "--auth-provider-arg=client-id", // values must be of form 'key=value' or 'key-' "me", }, wantCompleteErr: true, }, { name: "test9", flags: []string{ // No name for authinfo provided. }, wantCompleteErr: true, }, { name: "test10", flags: []string{ "--exec-command=example-client-go-exec-plugin", "me", }, wantOptions: &setCredentialsOptions{ name: "me", execCommand: stringFlagFor("example-client-go-exec-plugin"), }, }, { name: "test11", flags: []string{ "--exec-command=example-client-go-exec-plugin", "--exec-arg=arg1", "--exec-arg=arg2", "me", }, wantOptions: &setCredentialsOptions{ name: "me", execCommand: stringFlagFor("example-client-go-exec-plugin"), execArgs: []string{"arg1", "arg2"}, }, }, { name: "test12", flags: []string{ "--exec-command=example-client-go-exec-plugin", "--exec-env=key1=val1", "--exec-env=key2=val2", "--exec-env=env-remove1-", "--exec-env=env-remove2-", "me", }, wantOptions: &setCredentialsOptions{ name: "me", execCommand: stringFlagFor("example-client-go-exec-plugin"), execEnv: map[string]string{"key1": "val1", "key2": "val2"}, execEnvToRemove: []string{"env-remove1", "env-remove2"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buff := new(bytes.Buffer) opts := new(setCredentialsOptions) cmd := newCmdConfigSetCredentials(buff, opts) if err := cmd.ParseFlags(tt.flags); err != nil { if !tt.wantParseErr { t.Errorf("case %s: parsing error for flags %q: %v: %s", tt.name, tt.flags, err, buff) } return } if tt.wantParseErr { t.Errorf("case %s: expected parsing error for flags %q: %s", tt.name, tt.flags, buff) return } if err := opts.complete(cmd); err != nil { if !tt.wantCompleteErr { t.Errorf("case %s: complete() error for flags %q: %s", tt.name, tt.flags, buff) } return } if tt.wantCompleteErr { t.Errorf("case %s: complete() expected errors for flags %q: %s", tt.name, tt.flags, buff) return } if err := opts.validate(); err != nil { if !tt.wantValidateErr { t.Errorf("case %s: flags %q: validate failed: %v", tt.name, tt.flags, err) } return } if tt.wantValidateErr { t.Errorf("case %s: flags %q: expected validate to fail", tt.name, tt.flags) return } if !reflect.DeepEqual(opts, tt.wantOptions) { t.Errorf("case %s: flags %q: mis-matched options,\nwanted=%#v\ngot= %#v", tt.name, tt.flags, tt.wantOptions, opts) } }) } } func TestModifyExistingAuthInfo(t *testing.T) { tests := []struct { name string flags []string wantParseErr bool wantCompleteErr bool wantValidateErr bool existingAuthInfo clientcmdapi.AuthInfo wantAuthInfo clientcmdapi.AuthInfo }{ { name: "1. create new exec config", flags: []string{ "--exec-command=example-client-go-exec-plugin", "--exec-api-version=client.authentication.k8s.io/v1", "me", }, existingAuthInfo: clientcmdapi.AuthInfo{}, wantAuthInfo: clientcmdapi.AuthInfo{ Exec: &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", APIVersion: "client.authentication.k8s.io/v1", }, }, }, { name: "2. redefine exec args", flags: []string{ "--exec-arg=new-arg1", "--exec-arg=new-arg2", "me", }, existingAuthInfo: clientcmdapi.AuthInfo{ Exec: &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", APIVersion: "client.authentication.k8s.io/v1beta1", Args: []string{"existing-arg1", "existing-arg2"}, }, }, wantAuthInfo: clientcmdapi.AuthInfo{ Exec: &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", APIVersion: "client.authentication.k8s.io/v1beta1", Args: []string{"new-arg1", "new-arg2"}, }, }, }, { name: "3. reset exec args", flags: []string{ "--exec-command=example-client-go-exec-plugin", "me", }, existingAuthInfo: clientcmdapi.AuthInfo{ Exec: &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", APIVersion: "client.authentication.k8s.io/v1beta1", Args: []string{"existing-arg1", "existing-arg2"}, }, }, wantAuthInfo: clientcmdapi.AuthInfo{ Exec: &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", APIVersion: "client.authentication.k8s.io/v1beta1", }, }, }, { name: "4. modify exec env variables", flags: []string{ "--exec-command=example-client-go-exec-plugin", "--exec-env=name1=value1000", "--exec-env=name3=value3", "--exec-env=name2-", "--exec-env=non-existing-", "me", }, existingAuthInfo: clientcmdapi.AuthInfo{ Exec: &clientcmdapi.ExecConfig{ Command: "existing-command", APIVersion: "client.authentication.k8s.io/v1beta1", Env: []clientcmdapi.ExecEnvVar{ {Name: "name1", Value: "value1"}, {Name: "name2", Value: "value2"}, }, }, }, wantAuthInfo: clientcmdapi.AuthInfo{ Exec: &clientcmdapi.ExecConfig{ Command: "example-client-go-exec-plugin", APIVersion: "client.authentication.k8s.io/v1beta1", Env: []clientcmdapi.ExecEnvVar{ {Name: "name1", Value: "value1000"}, {Name: "name3", Value: "value3"}, }, }, }, }, { name: "5. modify auth provider arguments", flags: []string{ "--auth-provider=new-auth-provider", "--auth-provider-arg=key1=val1000", "--auth-provider-arg=key3=val3", "--auth-provider-arg=key2-", "--auth-provider-arg=non-existing-", "me", }, existingAuthInfo: clientcmdapi.AuthInfo{ AuthProvider: &clientcmdapi.AuthProviderConfig{ Name: "auth-provider", Config: map[string]string{ "key1": "val1", "key2": "val2", }, }, }, wantAuthInfo: clientcmdapi.AuthInfo{ AuthProvider: &clientcmdapi.AuthProviderConfig{ Name: "new-auth-provider", Config: map[string]string{ "key1": "val1000", "key3": "val3", }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buff := new(bytes.Buffer) opts := new(setCredentialsOptions) cmd := newCmdConfigSetCredentials(buff, opts) if err := cmd.ParseFlags(tt.flags); err != nil { if !tt.wantParseErr { t.Errorf("case %s: parsing error for flags %q: %v: %s", tt.name, tt.flags, err, buff) } return } if tt.wantParseErr { t.Errorf("case %s: expected parsing error for flags %q: %s", tt.name, tt.flags, buff) return } if err := opts.complete(cmd); err != nil { if !tt.wantCompleteErr { t.Errorf("case %s: complete() error for flags %q: %s", tt.name, tt.flags, buff) } return } if tt.wantCompleteErr { t.Errorf("case %s: complete() expected errors for flags %q: %s", tt.name, tt.flags, buff) return } if err := opts.validate(); err != nil { if !tt.wantValidateErr { t.Errorf("case %s: flags %q: validate failed: %v", tt.name, tt.flags, err) } return } if tt.wantValidateErr { t.Errorf("case %s: flags %q: expected validate to fail", tt.name, tt.flags) return } modifiedAuthInfo := opts.modifyAuthInfo(tt.existingAuthInfo) if !reflect.DeepEqual(modifiedAuthInfo, tt.wantAuthInfo) { t.Errorf("case %s: flags %q: mis-matched auth info,\nwanted=%#v\ngot= %#v", tt.name, tt.flags, tt.wantAuthInfo, modifiedAuthInfo) } }) } } type setCredentialsTest struct { description string config clientcmdapi.Config args []string flags []string expected string expectedConfig clientcmdapi.Config } func TestSetCredentials(t *testing.T) { conf := clientcmdapi.Config{} test := setCredentialsTest{ description: "Testing set credentials", config: conf, args: []string{"cluster-admin"}, flags: []string{ "--username=admin", "--password=uXFGweU9l35qcif", }, expected: `User "cluster-admin" set.` + "\n", expectedConfig: clientcmdapi.Config{ AuthInfos: map[string]*clientcmdapi.AuthInfo{ "cluster-admin": {Username: "admin", Password: "uXFGweU9l35qcif"}}, }, } test.run(t) } func (test setCredentialsTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigSetCredentials(buf, pathOptions) cmd.SetArgs(test.args) cmd.Flags().Parse(test.flags) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v,kubectl config set-credentials args: %v,flags: %v", err, test.args, test.flags) } config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("Fail in %q:\n expected %v\n but got %v\n", test.description, test.expected, buf.String()) } } if test.expectedConfig.AuthInfos != nil { expectAuthInfo := test.expectedConfig.AuthInfos[test.args[0]] actualAuthInfo := config.AuthInfos[test.args[0]] if expectAuthInfo.Username != actualAuthInfo.Username || expectAuthInfo.Password != actualAuthInfo.Password { t.Errorf("Fail in %q:\n expected AuthInfo%v\n but found %v in kubeconfig\n", test.description, expectAuthInfo, actualAuthInfo) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/set_test.go000066400000000000000000000051061504711711200302370ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "os" "testing" utiltesting "k8s.io/client-go/util/testing" "reflect" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type setConfigTest struct { description string config clientcmdapi.Config args []string expected string expectedConfig clientcmdapi.Config } func TestSetConfigCurrentContext(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", CurrentContext: "minikube", } expectedConfig := *clientcmdapi.NewConfig() expectedConfig.CurrentContext = "my-cluster" test := setConfigTest{ description: "Testing for kubectl config set current-context", config: conf, args: []string{"current-context", "my-cluster"}, expected: `Property "current-context" set.` + "\n", expectedConfig: expectedConfig, } test.run(t) } func (test setConfigTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigSet(buf, pathOptions) cmd.SetArgs(test.args) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v", err) } config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("Failed in:%q\n expected %v\n but got %v", test.description, test.expected, buf.String()) } } if !reflect.DeepEqual(*config, test.expectedConfig) { t.Errorf("Failed in: %q\n expected %v\n but got %v", test.description, *config, test.expectedConfig) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/unset.go000066400000000000000000000055171504711711200275510ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "fmt" "io" "reflect" "github.com/spf13/cobra" "k8s.io/kubectl/pkg/util/templates" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" ) type unsetOptions struct { configAccess clientcmd.ConfigAccess propertyName string } var ( unsetLong = templates.LongDesc(i18n.T(` Unset an individual value in a kubeconfig file. PROPERTY_NAME is a dot delimited name where each token represents either an attribute name or a map key. Map keys may not contain dots.`)) unsetExample = templates.Examples(` # Unset the current-context kubectl config unset current-context # Unset namespace in foo context kubectl config unset contexts.foo.namespace`) ) // NewCmdConfigUnset returns a Command instance for 'config unset' sub command func NewCmdConfigUnset(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &unsetOptions{configAccess: configAccess} cmd := &cobra.Command{ Use: "unset PROPERTY_NAME", DisableFlagsInUseLine: true, Short: i18n.T("Unset an individual value in a kubeconfig file"), Long: unsetLong, Example: unsetExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.complete(cmd, args)) cmdutil.CheckErr(options.run(out)) }, } return cmd } func (o unsetOptions) run(out io.Writer) error { err := o.validate() if err != nil { return err } config, err := o.configAccess.GetStartingConfig() if err != nil { return err } steps, err := newNavigationSteps(o.propertyName) if err != nil { return err } err = modifyConfig(reflect.ValueOf(config), steps, "", true, true) if err != nil { return err } if err := clientcmd.ModifyConfig(o.configAccess, *config, false); err != nil { return err } if _, err := fmt.Fprintf(out, "Property %q unset.\n", o.propertyName); err != nil { return err } return nil } func (o *unsetOptions) complete(cmd *cobra.Command, args []string) error { if len(args) != 1 { return helpErrorf(cmd, "Unexpected args: %v", args) } o.propertyName = args[0] return nil } func (o unsetOptions) validate() error { if len(o.propertyName) == 0 { return errors.New("you must specify a property") } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/unset_test.go000066400000000000000000000110471504711711200306030ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "os" "testing" utiltesting "k8s.io/client-go/util/testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type unsetConfigTest struct { description string config clientcmdapi.Config args []string expected string expectedErr string } func TestUnsetConfigString(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "minikube", } test := unsetConfigTest{ description: "Testing for kubectl config unset a value", config: conf, args: []string{"current-context"}, expected: `Property "current-context" unset.` + "\n", } test.run(t) } func TestUnsetConfigMap(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "minikube", } test := unsetConfigTest{ description: "Testing for kubectl config unset a map", config: conf, args: []string{"clusters"}, expected: `Property "clusters" unset.` + "\n", } test.run(t) } func TestUnsetUnexistConfig(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "minikube", } test := unsetConfigTest{ description: "Testing for kubectl config unset a unexist map key", config: conf, args: []string{"contexts.foo.namespace"}, expectedErr: "current map key `foo` is invalid", } test.run(t) } func (test unsetConfigTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigUnset(buf, pathOptions) opts := &unsetOptions{configAccess: pathOptions} err = opts.complete(cmd, test.args) if err == nil { err = opts.run(buf) } if test.expectedErr == "" && err != nil { t.Fatalf("unexpected error: %v", err) } config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } if err != nil && err.Error() != test.expectedErr { t.Fatalf("expected error:\n %v\nbut got error:\n%v", test.expectedErr, err) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("Failed in :%q\n expected %v\n but got %v", test.description, test.expected, buf.String()) } } if test.args[0] == "current-context" { if config.CurrentContext != "" { t.Errorf("Failed in :%q\n expected current-context nil,but got %v", test.description, config.CurrentContext) } } else if test.args[0] == "clusters" { if len(config.Clusters) != 0 { t.Errorf("Failed in :%q\n expected clusters nil map, but got %v", test.description, config.Clusters) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/use_context.go000066400000000000000000000054361504711711200307530ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "fmt" "io" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( useContextExample = templates.Examples(` # Use the context for the minikube cluster kubectl config use-context minikube`) ) type useContextOptions struct { configAccess clientcmd.ConfigAccess contextName string } // NewCmdConfigUseContext returns a Command instance for 'config use-context' sub command func NewCmdConfigUseContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &useContextOptions{configAccess: configAccess} cmd := &cobra.Command{ Use: "use-context CONTEXT_NAME", DisableFlagsInUseLine: true, Short: i18n.T("Set the current-context in a kubeconfig file"), Aliases: []string{"use"}, Long: `Set the current-context in a kubeconfig file.`, Example: useContextExample, ValidArgsFunction: completion.ContextCompletionFunc, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.complete(cmd)) cmdutil.CheckErr(options.run()) fmt.Fprintf(out, "Switched to context %q.\n", options.contextName) }, } return cmd } func (o useContextOptions) run() error { config, err := o.configAccess.GetStartingConfig() if err != nil { return err } err = o.validate(config) if err != nil { return err } config.CurrentContext = o.contextName return clientcmd.ModifyConfig(o.configAccess, *config, true) } func (o *useContextOptions) complete(cmd *cobra.Command) error { endingArgs := cmd.Flags().Args() if len(endingArgs) != 1 { return helpErrorf(cmd, "Unexpected args: %v", endingArgs) } o.contextName = endingArgs[0] return nil } func (o useContextOptions) validate(config *clientcmdapi.Config) error { if len(o.contextName) == 0 { return errors.New("empty context names are not allowed") } for name := range config.Contexts { if name == o.contextName { return nil } } return fmt.Errorf("no context exists with the name: %q", o.contextName) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/use_context_test.go000066400000000000000000000066161504711711200320130ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "bytes" "os" "testing" utiltesting "k8s.io/client-go/util/testing" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type useContextTest struct { description string config clientcmdapi.Config //initiate kubectl config args []string //kubectl config use-context args expected string //expect out expectedConfig clientcmdapi.Config //expect kubectl config } func TestUseContext(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "minikube", } test := useContextTest{ description: "Testing for kubectl config use-context", config: conf, args: []string{"my-cluster"}, expected: `Switched to context "my-cluster".` + "\n", expectedConfig: clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "my-cluster", }, } test.run(t) } func (test useContextTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" buf := bytes.NewBuffer([]byte{}) cmd := NewCmdConfigUseContext(buf, pathOptions) cmd.SetArgs(test.args) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v,kubectl config use-context args: %v", err, test.args) } config, err := clientcmd.LoadFromFile(fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error loading kubeconfig file: %v", err) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("Failed in :%q\n expected %v\n, but got %v\n", test.description, test.expected, buf.String()) } } if test.expectedConfig.CurrentContext != config.CurrentContext { t.Errorf("Failed in :%q\n expected config %v, but found %v\n in kubeconfig\n", test.description, test.expectedConfig, config) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/view.go000066400000000000000000000124451504711711200273630ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "errors" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/clientcmd/api/latest" cliflag "k8s.io/component-base/cli/flag" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // ViewOptions holds the command-line options for 'config view' sub command type ViewOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObject printers.ResourcePrinterFunc ConfigAccess clientcmd.ConfigAccess Merge cliflag.Tristate Flatten bool Minify bool RawByteData bool Context string OutputFormat string genericiooptions.IOStreams } var ( viewLong = templates.LongDesc(i18n.T(` Display merged kubeconfig settings or a specified kubeconfig file. You can use --output jsonpath={...} to extract specific values using a jsonpath expression.`)) viewExample = templates.Examples(` # Show merged kubeconfig settings kubectl config view # Show merged kubeconfig settings, raw certificate data, and exposed secrets kubectl config view --raw # Get the password for the e2e user kubectl config view -o jsonpath='{.users[?(@.name == "e2e")].user.password}'`) ) // NewCmdConfigView returns a Command instance for 'config view' sub command func NewCmdConfigView(streams genericiooptions.IOStreams, ConfigAccess clientcmd.ConfigAccess) *cobra.Command { o := &ViewOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme).WithDefaultOutput("yaml"), ConfigAccess: ConfigAccess, IOStreams: streams, } cmd := &cobra.Command{ Use: "view", Short: i18n.T("Display merged kubeconfig settings or a specified kubeconfig file"), Long: viewLong, Example: viewExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) o.Merge.Default(true) mergeFlag := cmd.Flags().VarPF(&o.Merge, "merge", "", "Merge the full hierarchy of kubeconfig files") mergeFlag.NoOptDefVal = "true" cmd.Flags().BoolVar(&o.RawByteData, "raw", o.RawByteData, "Display raw byte data and sensitive data") cmd.Flags().BoolVar(&o.Flatten, "flatten", o.Flatten, "Flatten the resulting kubeconfig file into self-contained output (useful for creating portable kubeconfig files)") cmd.Flags().BoolVar(&o.Minify, "minify", o.Minify, "Remove all information not used by current-context from the output") return cmd } // Complete completes the required command-line options func (o *ViewOptions) Complete(cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "unexpected arguments: %v", args) } if o.ConfigAccess.IsExplicitFile() { if !o.Merge.Provided() { o.Merge.Set("false") } } printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObject = printer.PrintObj o.Context = cmdutil.GetFlagString(cmd, "context") return nil } // Validate makes sure that provided values for command-line options are valid func (o ViewOptions) Validate() error { if !o.Merge.Value() && !o.ConfigAccess.IsExplicitFile() { return errors.New("if merge==false a precise file must be specified") } return nil } // Run performs the execution of 'config view' sub command func (o ViewOptions) Run() error { config, err := o.loadConfig() if err != nil { return err } if o.Minify { if len(o.Context) > 0 { config.CurrentContext = o.Context } if err := clientcmdapi.MinifyConfig(config); err != nil { return err } } if o.Flatten { if err := clientcmdapi.FlattenConfig(config); err != nil { return err } } else if !o.RawByteData { if err := clientcmdapi.RedactSecrets(config); err != nil { return err } clientcmdapi.ShortenConfig(config) } convertedObj, err := latest.Scheme.ConvertToVersion(config, latest.ExternalVersion) if err != nil { return err } return o.PrintObject(convertedObj, o.Out) } func (o ViewOptions) loadConfig() (*clientcmdapi.Config, error) { err := o.Validate() if err != nil { return nil, err } config, err := o.getStartingConfig() return config, err } // getStartingConfig returns the Config object built from the sources specified by the options, the filename read (only if it was a single file), and an error if something goes wrong func (o *ViewOptions) getStartingConfig() (*clientcmdapi.Config, error) { switch { case !o.Merge.Value(): return clientcmd.LoadFromFile(o.ConfigAccess.GetExplicitFile()) default: return o.ConfigAccess.GetStartingConfig() } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/config/view_test.go000066400000000000000000000201141504711711200304120ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "os" "testing" utiltesting "k8s.io/client-go/util/testing" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) type viewClusterTest struct { description string config clientcmdapi.Config //initiate kubectl config flags []string //kubectl config viw flags expected string //expect out } func TestViewCluster(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "minikube", AuthInfos: map[string]*clientcmdapi.AuthInfo{ "minikube": { ClientKeyData: []byte("notredacted"), Token: "notredacted", Username: "foo", Password: "notredacted", }, "mu-cluster": { ClientKeyData: []byte("notredacted"), Token: "notredacted", Username: "bar", Password: "notredacted", }, }, } test := viewClusterTest{ description: "Testing for kubectl config view", config: conf, expected: `apiVersion: v1 clusters: - cluster: server: https://192.168.99.100:8443 name: minikube - cluster: server: https://192.168.0.1:3434 name: my-cluster contexts: - context: cluster: minikube user: minikube name: minikube - context: cluster: my-cluster user: mu-cluster name: my-cluster current-context: minikube kind: Config preferences: {} users: - name: minikube user: client-key-data: DATA+OMITTED password: REDACTED token: REDACTED username: foo - name: mu-cluster user: client-key-data: DATA+OMITTED password: REDACTED token: REDACTED username: bar` + "\n", } test.run(t) } func TestViewClusterUnredacted(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "minikube", AuthInfos: map[string]*clientcmdapi.AuthInfo{ "minikube": { ClientKeyData: []byte("notredacted"), ClientCertificateData: []byte("plaintext"), Token: "notredacted", Username: "foo", Password: "notredacted", }, "mu-cluster": { ClientKeyData: []byte("notredacted"), ClientCertificateData: []byte("plaintext"), Token: "notredacted", Username: "bar", Password: "notredacted", }, }, } testCases := []struct { description string config clientcmdapi.Config flags []string expected string }{ { description: "Testing for kubectl config view --raw=true", config: conf, flags: []string{"--raw=true"}, expected: `apiVersion: v1 clusters: - cluster: server: https://192.168.99.100:8443 name: minikube - cluster: server: https://192.168.0.1:3434 name: my-cluster contexts: - context: cluster: minikube user: minikube name: minikube - context: cluster: my-cluster user: mu-cluster name: my-cluster current-context: minikube kind: Config preferences: {} users: - name: minikube user: client-certificate-data: cGxhaW50ZXh0 client-key-data: bm90cmVkYWN0ZWQ= password: notredacted token: notredacted username: foo - name: mu-cluster user: client-certificate-data: cGxhaW50ZXh0 client-key-data: bm90cmVkYWN0ZWQ= password: notredacted token: notredacted username: bar` + "\n", }, } for _, test := range testCases { cmdTest := viewClusterTest{ description: test.description, config: test.config, flags: test.flags, expected: test.expected, } cmdTest.run(t) } } func TestViewClusterMinify(t *testing.T) { conf := clientcmdapi.Config{ Kind: "Config", APIVersion: "v1", Clusters: map[string]*clientcmdapi.Cluster{ "minikube": {Server: "https://192.168.99.100:8443"}, "my-cluster": {Server: "https://192.168.0.1:3434"}, }, Contexts: map[string]*clientcmdapi.Context{ "minikube": {AuthInfo: "minikube", Cluster: "minikube"}, "my-cluster": {AuthInfo: "mu-cluster", Cluster: "my-cluster"}, }, CurrentContext: "minikube", AuthInfos: map[string]*clientcmdapi.AuthInfo{ "minikube": { ClientKeyData: []byte("notredacted"), Token: "notredacted", Username: "foo", Password: "notredacted", }, "mu-cluster": { ClientKeyData: []byte("notredacted"), Token: "notredacted", Username: "bar", Password: "notredacted", }, }, } testCases := []struct { description string config clientcmdapi.Config flags []string expected string }{ { description: "Testing for kubectl config view --minify=true", config: conf, flags: []string{"--minify=true"}, expected: `apiVersion: v1 clusters: - cluster: server: https://192.168.99.100:8443 name: minikube contexts: - context: cluster: minikube user: minikube name: minikube current-context: minikube kind: Config preferences: {} users: - name: minikube user: client-key-data: DATA+OMITTED password: REDACTED token: REDACTED username: foo` + "\n", }, { description: "Testing for kubectl config view --minify=true --context=my-cluster", config: conf, flags: []string{"--minify=true", "--context=my-cluster"}, expected: `apiVersion: v1 clusters: - cluster: server: https://192.168.0.1:3434 name: my-cluster contexts: - context: cluster: my-cluster user: mu-cluster name: my-cluster current-context: my-cluster kind: Config preferences: {} users: - name: mu-cluster user: client-key-data: DATA+OMITTED password: REDACTED token: REDACTED username: bar` + "\n", }, } for _, test := range testCases { cmdTest := viewClusterTest{ description: test.description, config: test.config, flags: test.flags, expected: test.expected, } cmdTest.run(t) } } func (test viewClusterTest) run(t *testing.T) { fakeKubeFile, err := os.CreateTemp(os.TempDir(), "") if err != nil { t.Fatalf("unexpected error: %v", err) } defer utiltesting.CloseAndRemove(t, fakeKubeFile) err = clientcmd.WriteToFile(test.config, fakeKubeFile.Name()) if err != nil { t.Fatalf("unexpected error: %v", err) } pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdConfigView(streams, pathOptions) // "context" is a global flag, inherited from base kubectl command in the real world cmd.Flags().String("context", "", "The name of the kubeconfig context to use") cmd.Flags().Parse(test.flags) if err := cmd.Execute(); err != nil { t.Fatalf("unexpected error executing command: %v,kubectl config view flags: %v", err, test.flags) } if len(test.expected) != 0 { if buf.String() != test.expected { t.Errorf("Failed in %q\n expected %v\n but got %v\n", test.description, test.expected, buf.String()) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/cp/000077500000000000000000000000001504711711200252115ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/cp/cp.go000066400000000000000000000410611504711711200261440ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cp import ( "archive/tar" "context" "errors" "fmt" "io" "os" "strings" "time" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/cmd/exec" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( cpExample = templates.Examples(i18n.T(` # !!!Important Note!!! # Requires that the 'tar' binary is present in your container # image. If 'tar' is not present, 'kubectl cp' will fail. # # For advanced use cases, such as symlinks, wildcard expansion or # file mode preservation, consider using 'kubectl exec'. # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace tar cf - /tmp/foo | kubectl exec -i -n -- tar xf - -C /tmp/bar # Copy /tmp/foo from a remote pod to /tmp/bar locally kubectl exec -n -- tar cf - /tmp/foo | tar xf - -C /tmp/bar # Copy /tmp/foo_dir local directory to /tmp/bar_dir in a remote pod in the default namespace kubectl cp /tmp/foo_dir :/tmp/bar_dir # Copy /tmp/foo local file to /tmp/bar in a remote pod in a specific container kubectl cp /tmp/foo :/tmp/bar -c # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace kubectl cp /tmp/foo /:/tmp/bar # Copy /tmp/foo from a remote pod to /tmp/bar locally kubectl cp /:/tmp/foo /tmp/bar`)) ) // CopyOptions have the data required to perform the copy operation type CopyOptions struct { Container string Namespace string NoPreserve bool MaxTries int ClientConfig *restclient.Config Clientset kubernetes.Interface ExecParentCmdName string args []string genericiooptions.IOStreams } // NewCopyOptions creates the options for copy func NewCopyOptions(ioStreams genericiooptions.IOStreams) *CopyOptions { return &CopyOptions{ IOStreams: ioStreams, } } // NewCmdCp creates a new Copy command. func NewCmdCp(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCopyOptions(ioStreams) cmd := &cobra.Command{ Use: "cp ", DisableFlagsInUseLine: true, Short: i18n.T("Copy files and directories to and from containers"), Long: i18n.T("Copy files and directories to and from containers."), Example: cpExample, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var comps []string if len(args) == 0 { if strings.IndexAny(toComplete, "/.~") == 0 { // Looks like a path, do nothing } else if strings.Contains(toComplete, ":") { // TODO: complete remote files in the pod } else if idx := strings.Index(toComplete, "/"); idx > 0 { // complete / namespace := toComplete[:idx] template := "{{ range .items }}{{ .metadata.namespace }}/{{ .metadata.name }}: {{ end }}" comps = completion.CompGetFromTemplate(&template, f, namespace, []string{"pod"}, toComplete) } else { // Complete namespaces followed by a / for _, ns := range completion.CompGetResource(f, "namespace", toComplete) { comps = append(comps, fmt.Sprintf("%s/", ns)) } // Complete pod names followed by a : for _, pod := range completion.CompGetResource(f, "pod", toComplete) { comps = append(comps, fmt.Sprintf("%s:", pod)) } // Finally, provide file completion if we need to. // We only do this if: // 1- There are other completions found (if there are no completions, // the shell will do file completion itself) // 2- If there is some input from the user (or else we will end up // listing the entire content of the current directory which could // be too many choices for the user) if len(comps) > 0 && len(toComplete) > 0 { if files, err := os.ReadDir("."); err == nil { for _, file := range files { filename := file.Name() if strings.HasPrefix(filename, toComplete) { if file.IsDir() { filename = fmt.Sprintf("%s/", filename) } // We are completing a file prefix comps = append(comps, filename) } } } } else if len(toComplete) == 0 { // If the user didn't provide any input to complete, // we provide a hint that a path can also be used comps = append(comps, "./", "/") } } } return comps, cobra.ShellCompDirectiveNoSpace }, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } cmdutil.AddContainerVarFlags(cmd, &o.Container, o.Container) cmd.Flags().BoolVarP(&o.NoPreserve, "no-preserve", "", false, "The copied file/directory's ownership and permissions will not be preserved in the container") cmd.Flags().IntVarP(&o.MaxTries, "retries", "", 0, "Set number of retries to complete a copy operation from a container. Specify 0 to disable or any negative value for infinite retrying. The default is 0 (no retry).") return cmd } var ( errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [[namespace/]pod:]file/path") ) func extractFileSpec(arg string) (fileSpec, error) { i := strings.Index(arg, ":") // filespec starting with a semicolon is invalid if i == 0 { return fileSpec{}, errFileSpecDoesntMatchFormat } if i == -1 { return fileSpec{ File: newLocalPath(arg), }, nil } pod, file := arg[:i], arg[i+1:] pieces := strings.Split(pod, "/") switch len(pieces) { case 1: return fileSpec{ PodName: pieces[0], File: newRemotePath(file), }, nil case 2: return fileSpec{ PodNamespace: pieces[0], PodName: pieces[1], File: newRemotePath(file), }, nil default: return fileSpec{}, errFileSpecDoesntMatchFormat } } // Complete completes all the required options func (o *CopyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { if cmd.Parent() != nil { o.ExecParentCmdName = cmd.Parent().CommandPath() } var err error o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.Clientset, err = f.KubernetesClientSet() if err != nil { return err } o.ClientConfig, err = f.ToRESTConfig() if err != nil { return err } o.args = args return nil } // Validate makes sure provided values for CopyOptions are valid func (o *CopyOptions) Validate() error { if len(o.args) != 2 { return fmt.Errorf("source and destination are required") } return nil } // Run performs the execution func (o *CopyOptions) Run() error { srcSpec, err := extractFileSpec(o.args[0]) if err != nil { return err } destSpec, err := extractFileSpec(o.args[1]) if err != nil { return err } if len(srcSpec.PodName) != 0 && len(destSpec.PodName) != 0 { return fmt.Errorf("one of src or dest must be a local file specification") } if len(srcSpec.File.String()) == 0 || len(destSpec.File.String()) == 0 { return errors.New("filepath can not be empty") } if len(srcSpec.PodName) != 0 { return o.copyFromPod(srcSpec, destSpec) } if len(destSpec.PodName) != 0 { return o.copyToPod(srcSpec, destSpec, &exec.ExecOptions{}) } return fmt.Errorf("one of src or dest must be a remote file specification") } // checkDestinationIsDir receives a destination fileSpec and // determines if the provided destination path exists on the // pod. If the destination path does not exist or is _not_ a // directory, an error is returned with the exit code received. func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error { options := &exec.ExecOptions{ StreamOptions: exec.StreamOptions{ IOStreams: genericiooptions.IOStreams{ Out: io.Discard, ErrOut: io.Discard, }, Namespace: dest.PodNamespace, PodName: dest.PodName, }, Command: []string{"test", "-d", dest.File.String()}, Executor: &exec.DefaultRemoteExecutor{}, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() done := make(chan error) go func() { done <- o.execute(options) }() select { case <-ctx.Done(): return ctx.Err() case err := <-done: return err } } func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error { if _, err := os.Stat(src.File.String()); err != nil { return fmt.Errorf("%s doesn't exist in local filesystem", src.File) } reader, writer := io.Pipe() srcFile := src.File.(localPath) destFile := dest.File.(remotePath) if err := o.checkDestinationIsDir(dest); err == nil { // If no error, dest.File was found to be a directory. // Copy specified src into it destFile = destFile.Join(srcFile.Base()) } else if errors.Is(err, context.DeadlineExceeded) { // we haven't decided destination is directory or not because context timeout is exceeded. // That's why, we should shortcut the process in here. return err } go func(src localPath, dest remotePath, writer io.WriteCloser) { defer writer.Close() cmdutil.CheckErr(makeTar(src, dest, writer)) }(srcFile, destFile, writer) var cmdArr []string if o.NoPreserve { cmdArr = []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-"} } else { cmdArr = []string{"tar", "-xmf", "-"} } destFileDir := destFile.Dir().String() if len(destFileDir) > 0 { cmdArr = append(cmdArr, "-C", destFileDir) } options.StreamOptions = exec.StreamOptions{ IOStreams: genericiooptions.IOStreams{ In: reader, Out: o.Out, ErrOut: o.ErrOut, }, Stdin: true, Namespace: dest.PodNamespace, PodName: dest.PodName, } options.Command = cmdArr options.Executor = &exec.DefaultRemoteExecutor{} return o.execute(options) } func (o *CopyOptions) copyFromPod(src, dest fileSpec) error { reader := newTarPipe(src, o) srcFile := src.File.(remotePath) destFile := dest.File.(localPath) // remove extraneous path shortcuts - these could occur if a path contained extra "../" // and attempted to navigate beyond "/" in a remote filesystem prefix := stripPathShortcuts(srcFile.StripSlashes().Clean().String()) return o.untarAll(src.PodNamespace, src.PodName, prefix, srcFile, destFile, reader) } type TarPipe struct { src fileSpec o *CopyOptions reader *io.PipeReader outStream *io.PipeWriter bytesRead uint64 retries int } func newTarPipe(src fileSpec, o *CopyOptions) *TarPipe { t := new(TarPipe) t.src = src t.o = o t.initReadFrom(0) return t } func (t *TarPipe) initReadFrom(n uint64) { t.reader, t.outStream = io.Pipe() options := &exec.ExecOptions{ StreamOptions: exec.StreamOptions{ IOStreams: genericiooptions.IOStreams{ In: nil, Out: t.outStream, ErrOut: t.o.Out, }, Namespace: t.src.PodNamespace, PodName: t.src.PodName, }, Command: []string{"tar", "cf", "-", t.src.File.String()}, Executor: &exec.DefaultRemoteExecutor{}, } if t.o.MaxTries != 0 { options.Command = []string{"sh", "-c", fmt.Sprintf("tar cf - %s | tail -c+%d", t.src.File, n)} } go func() { defer t.outStream.Close() cmdutil.CheckErr(t.o.execute(options)) }() } func (t *TarPipe) Read(p []byte) (n int, err error) { n, err = t.reader.Read(p) if err != nil { if t.o.MaxTries < 0 || t.retries < t.o.MaxTries { t.retries++ fmt.Printf("Resuming copy at %d bytes, retry %d/%d\n", t.bytesRead, t.retries, t.o.MaxTries) t.initReadFrom(t.bytesRead + 1) err = nil } else { fmt.Printf("Dropping out copy after %d retries\n", t.retries) } } else { t.bytesRead += uint64(n) } return } func makeTar(src localPath, dest remotePath, writer io.Writer) error { // TODO: use compression here? tarWriter := tar.NewWriter(writer) defer tarWriter.Close() srcPath := src.Clean() destPath := dest.Clean() return recursiveTar(srcPath.Dir(), srcPath.Base(), destPath.Dir(), destPath.Base(), tarWriter) } func recursiveTar(srcDir, srcFile localPath, destDir, destFile remotePath, tw *tar.Writer) error { matchedPaths, err := srcDir.Join(srcFile).Glob() if err != nil { return err } for _, fpath := range matchedPaths { stat, err := os.Lstat(fpath) if err != nil { return err } if stat.IsDir() { files, err := os.ReadDir(fpath) if err != nil { return err } if len(files) == 0 { //case empty directory hdr, _ := tar.FileInfoHeader(stat, fpath) hdr.Name = destFile.String() if err := tw.WriteHeader(hdr); err != nil { return err } } for _, f := range files { if err := recursiveTar(srcDir, srcFile.Join(newLocalPath(f.Name())), destDir, destFile.Join(newRemotePath(f.Name())), tw); err != nil { return err } } return nil } else if stat.Mode()&os.ModeSymlink != 0 { //case soft link hdr, _ := tar.FileInfoHeader(stat, fpath) target, err := os.Readlink(fpath) if err != nil { return err } hdr.Linkname = target hdr.Name = destFile.String() if err := tw.WriteHeader(hdr); err != nil { return err } } else { //case regular file or other file type like pipe hdr, err := tar.FileInfoHeader(stat, fpath) if err != nil { return err } hdr.Name = destFile.String() if err := tw.WriteHeader(hdr); err != nil { return err } f, err := os.Open(fpath) if err != nil { return err } defer f.Close() if _, err := io.Copy(tw, f); err != nil { return err } return f.Close() } } return nil } func (o *CopyOptions) untarAll(ns, pod string, prefix string, src remotePath, dest localPath, reader io.Reader) error { symlinkWarningPrinted := false // TODO: use compression here? tarReader := tar.NewReader(reader) for { header, err := tarReader.Next() if err != nil { if err != io.EOF { return err } break } // All the files will start with the prefix, which is the directory where // they were located on the pod, we need to strip down that prefix, but // if the prefix is missing it means the tar was tempered with. // For the case where prefix is empty we need to ensure that the path // is not absolute, which also indicates the tar file was tempered with. if !strings.HasPrefix(header.Name, prefix) { return fmt.Errorf("tar contents corrupted") } // basic file information mode := header.FileInfo().Mode() // header.Name is a name of the REMOTE file, so we need to create // a remotePath so that it goes through appropriate processing related // with cleaning remote paths destFileName := dest.Join(newRemotePath(header.Name[len(prefix):])) if !isRelative(dest, destFileName) { fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName) continue } if err := os.MkdirAll(destFileName.Dir().String(), 0755); err != nil { return err } if header.FileInfo().IsDir() { if err := os.MkdirAll(destFileName.String(), 0755); err != nil { return err } continue } if mode&os.ModeSymlink != 0 { if !symlinkWarningPrinted && len(o.ExecParentCmdName) > 0 { fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q (consider using \"%s exec -n %q %q -- tar cf - %q | tar xf -\")\n", destFileName, header.Linkname, o.ExecParentCmdName, ns, pod, src) symlinkWarningPrinted = true continue } fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q\n", destFileName, header.Linkname) continue } outFile, err := os.Create(destFileName.String()) if err != nil { return err } defer outFile.Close() if _, err := io.Copy(outFile, tarReader); err != nil { return err } if err := outFile.Close(); err != nil { return err } } return nil } func (o *CopyOptions) execute(options *exec.ExecOptions) error { if len(options.Namespace) == 0 { options.Namespace = o.Namespace } if len(o.Container) > 0 { options.ContainerName = o.Container } options.Config = o.ClientConfig options.PodClient = o.Clientset.CoreV1() if err := options.Validate(); err != nil { return err } return options.Run() } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/cp/cp_test.go000066400000000000000000000603021504711711200272020ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cp import ( "archive/tar" "bytes" "fmt" "io" "net/http" "os" "path/filepath" "reflect" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" kexec "k8s.io/kubectl/pkg/cmd/exec" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) type FileType int const ( RegularFile FileType = 0 SymLink FileType = 1 RegexFile FileType = 2 ) func TestExtractFileSpec(t *testing.T) { tests := []struct { spec string expectedPod string expectedNamespace string expectedFile string expectErr bool }{ { spec: "namespace/pod:/some/file", expectedPod: "pod", expectedNamespace: "namespace", expectedFile: "/some/file", }, { spec: "pod:/some/file", expectedPod: "pod", expectedFile: "/some/file", }, { spec: "/some/file", expectedFile: "/some/file", }, { spec: ":file:not:exist:in:local:filesystem", expectErr: true, }, { spec: "namespace/pod/invalid:/some/file", expectErr: true, }, { spec: "pod:/some/filenamewith:in", expectedPod: "pod", expectedFile: "/some/filenamewith:in", }, } for _, test := range tests { spec, err := extractFileSpec(test.spec) if test.expectErr && err == nil { t.Errorf("unexpected non-error") continue } if err != nil && !test.expectErr { t.Errorf("unexpected error: %v", err) continue } if spec.PodName != test.expectedPod { t.Errorf("expected: %s, saw: %s", test.expectedPod, spec.PodName) } if spec.PodNamespace != test.expectedNamespace { t.Errorf("expected: %s, saw: %s", test.expectedNamespace, spec.PodNamespace) } specFile := "" if spec.File != nil { specFile = spec.File.String() } if specFile != test.expectedFile { t.Errorf("expected: %s, saw: %s", test.expectedFile, specFile) } } } func TestGetPrefix(t *testing.T) { remoteSeparator := '/' osSeparator := os.PathSeparator tests := []struct { input string expected string }{ { input: "%[1]cfoo%[1]cbar", expected: "foo%[1]cbar", }, { input: "foo%[1]cbar", expected: "foo%[1]cbar", }, } for _, test := range tests { outRemote := newRemotePath(fmt.Sprintf(test.input, remoteSeparator)).StripSlashes() expectedRemote := fmt.Sprintf(test.expected, remoteSeparator) if outRemote.String() != expectedRemote { t.Errorf("remote expected: %s, saw: %s", expectedRemote, outRemote.String()) } outLocal := newLocalPath(fmt.Sprintf(test.input, osSeparator)).StripSlashes() expectedLocal := fmt.Sprintf(test.expected, osSeparator) if outLocal.String() != expectedLocal { t.Errorf("local expected: %s, saw: %s", expectedLocal, outLocal.String()) } } } func TestStripPathShortcuts(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "test single path shortcut prefix", input: "../foo/bar", expected: "foo/bar", }, { name: "test single path shortcut prefix", input: `..\foo\bar`, expected: "foo/bar", }, { name: "test multiple path shortcuts", input: "../../foo/bar", expected: "foo/bar", }, { name: "test multiple path shortcuts", input: `..\..\foo\bar`, expected: "foo/bar", }, { name: "test multiple path shortcuts with absolute path", input: "/tmp/one/two/../../foo/bar", expected: "tmp/foo/bar", }, { name: "test multiple path shortcuts with absolute path", input: `\tmp\one\two\..\..\foo\bar`, expected: "tmp/foo/bar", }, { name: "test multiple path shortcuts with no named directory", input: "../../", expected: "", }, { name: "test multiple path shortcuts with no named directory", input: `..\..\`, expected: "", }, { name: "test multiple path shortcuts with no named directory and no trailing slash", input: "../..", expected: "", }, { name: "test multiple path shortcuts with no named directory and no trailing slash", input: `..\..`, expected: "", }, { name: "test multiple path shortcuts with absolute path and filename containing leading dots", input: "/tmp/one/two/../../foo/..bar", expected: "tmp/foo/..bar", }, { name: "test multiple path shortcuts with absolute path and filename containing leading dots", input: `\tmp\one\two\..\..\foo\..bar`, expected: "tmp/foo/..bar", }, { name: "test multiple path shortcuts with no named directory and filename containing leading dots", input: "../...foo", expected: "...foo", }, { name: "test multiple path shortcuts with no named directory and filename containing leading dots", input: `..\...foo`, expected: "...foo", }, { name: "test filename containing leading dots", input: "...foo", expected: "...foo", }, { name: "test root directory", input: "/", expected: "", }, { name: "test root directory", input: `\`, expected: "", }, } for i, test := range tests { out := newRemotePath(test.input).StripShortcuts() if out.String() != test.expected { t.Errorf("expected[%d]: %s, saw: %s", i, test.expected, out) } } } func TestIsDestRelative(t *testing.T) { tests := []struct { base string dest string relative bool }{ { base: "/dir", dest: "/dir/../link", relative: false, }, { base: "/dir", dest: "/dir/../../link", relative: false, }, { base: "/dir", dest: "/link", relative: false, }, { base: "/dir", dest: "/dir/link", relative: true, }, { base: "/dir", dest: "/dir/int/../link", relative: true, }, { base: "dir", dest: "dir/link", relative: true, }, { base: "dir", dest: "dir/int/../link", relative: true, }, { base: "dir", dest: "dir/../../link", relative: false, }, } for i, test := range tests { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { if test.relative != isRelative(newLocalPath(test.base), newLocalPath(test.dest)) { t.Errorf("unexpected result for: base %q, dest %q", test.base, test.dest) } }) } } func checkErr(t *testing.T, err error) { if err != nil { t.Errorf("unexpected error: %v", err) t.FailNow() } } func TestTarUntar(t *testing.T) { dir, err := os.MkdirTemp("", "input") checkErr(t, err) dir = dir + "/" dir2, err := os.MkdirTemp("", "output") checkErr(t, err) dir2 = dir2 + "/" dir3, err := os.MkdirTemp("", "dir") checkErr(t, err) defer func() { os.RemoveAll(dir) os.RemoveAll(dir2) os.RemoveAll(dir3) }() files := []struct { name string nameList []string data string omitted bool fileType FileType }{ { name: "foo", data: "foobarbaz", fileType: RegularFile, }, { name: "dir/blah", data: "bazblahfoo", fileType: RegularFile, }, { name: "some/other/directory", data: "with more data here", fileType: RegularFile, }, { name: "blah", data: "same file name different data", fileType: RegularFile, }, { name: "gakki", data: "tmp/gakki", omitted: true, fileType: SymLink, }, { name: "relative_to_dest", data: dir2 + "/foo", omitted: true, fileType: SymLink, }, { name: "tricky_relative", data: dir3 + "/xyz", omitted: true, fileType: SymLink, }, { name: "absolute_path", data: "/tmp/gakki", omitted: true, fileType: SymLink, }, { name: "blah*", nameList: []string{"blah1", "blah2"}, data: "regexp file name", fileType: RegexFile, }, } for _, file := range files { completePath := dir + file.name if err := os.MkdirAll(filepath.Dir(completePath), 0755); err != nil { t.Fatalf("unexpected error: %v", err) } if file.fileType == RegularFile { createTmpFile(t, completePath, file.data) } else if file.fileType == SymLink { err := os.Symlink(file.data, completePath) if err != nil { t.Fatalf("unexpected error: %v", err) } } else if file.fileType == RegexFile { for _, fileName := range file.nameList { createTmpFile(t, dir+fileName, file.data) } } else { t.Fatalf("unexpected file type: %v", file) } } opts := NewCopyOptions(genericiooptions.NewTestIOStreamsDiscard()) writer := &bytes.Buffer{} if err := makeTar(newLocalPath(dir), newRemotePath(dir), writer); err != nil { t.Fatalf("unexpected error: %v", err) } reader := bytes.NewBuffer(writer.Bytes()) if err := opts.untarAll("", "", "", remotePath{}, newLocalPath(dir2), reader); err != nil { t.Fatalf("unexpected error: %v", err) } for _, file := range files { absPath := dir2 + strings.TrimPrefix(dir, os.TempDir()) filePath := absPath + file.name if file.fileType == RegularFile { cmpFileData(t, filePath, file.data) } else if file.fileType == SymLink { dest, err := os.Readlink(filePath) if file.omitted { if err != nil && strings.Contains(err.Error(), "no such file or directory") { continue } t.Fatalf("expected to omit symlink for %s", filePath) } if err != nil { t.Fatalf("unexpected error: %v", err) } if file.data != dest { t.Fatalf("expected: %s, saw: %s", file.data, dest) } } else if file.fileType == RegexFile { for _, fileName := range file.nameList { cmpFileData(t, dir+fileName, file.data) } } else { t.Fatalf("unexpected file type: %v", file) } } } func TestTarUntarWrongPrefix(t *testing.T) { dir, err := os.MkdirTemp("", "input") checkErr(t, err) dir = dir + "/" dir2, err := os.MkdirTemp("", "output") checkErr(t, err) defer func() { os.RemoveAll(dir) os.RemoveAll(dir2) }() completePath := dir + "foo" if err := os.MkdirAll(filepath.Dir(completePath), 0755); err != nil { t.Fatalf("unexpected error: %v", err) } createTmpFile(t, completePath, "sample data") opts := NewCopyOptions(genericiooptions.NewTestIOStreamsDiscard()) writer := &bytes.Buffer{} if err := makeTar(newLocalPath(dir), newRemotePath(dir), writer); err != nil { t.Fatalf("unexpected error: %v", err) } reader := bytes.NewBuffer(writer.Bytes()) err = opts.untarAll("", "", "verylongprefix-showing-the-tar-was-tempered-with", remotePath{}, newLocalPath(dir2), reader) if err == nil || !strings.Contains(err.Error(), "tar contents corrupted") { t.Fatalf("unexpected error: %v", err) } } func TestTarDestinationName(t *testing.T) { dir, err := os.MkdirTemp(os.TempDir(), "input") dir2, err2 := os.MkdirTemp(os.TempDir(), "output") if err != nil || err2 != nil { t.Errorf("unexpected error: %v | %v", err, err2) t.FailNow() } defer func() { if err := os.RemoveAll(dir); err != nil { t.Errorf("Unexpected error cleaning up: %v", err) } if err := os.RemoveAll(dir2); err != nil { t.Errorf("Unexpected error cleaning up: %v", err) } }() files := []struct { name string data string }{ { name: "foo", data: "foobarbaz", }, { name: "dir/blah", data: "bazblahfoo", }, { name: "some/other/directory", data: "with more data here", }, { name: "blah", data: "same file name different data", }, } // ensure files exist on disk for _, file := range files { completePath := dir + "/" + file.name if err := os.MkdirAll(filepath.Dir(completePath), 0755); err != nil { t.Errorf("unexpected error: %v", err) t.FailNow() } createTmpFile(t, completePath, file.data) } reader, writer := io.Pipe() go func() { if err := makeTar(newLocalPath(dir), newRemotePath(dir2), writer); err != nil { t.Errorf("unexpected error: %v", err) } }() tarReader := tar.NewReader(reader) for { hdr, err := tarReader.Next() if err == io.EOF { break } else if err != nil { t.Errorf("unexpected error: %v", err) t.FailNow() } if !strings.HasPrefix(hdr.Name, filepath.Base(dir2)) { t.Errorf("expected %q as destination filename prefix, saw: %q", filepath.Base(dir2), hdr.Name) } } } func TestBadTar(t *testing.T) { dir, err := os.MkdirTemp(os.TempDir(), "dest") if err != nil { t.Errorf("unexpected error: %v ", err) t.FailNow() } defer os.RemoveAll(dir) // More or less cribbed from https://golang.org/pkg/archive/tar/#example__minimal var buf bytes.Buffer tw := tar.NewWriter(&buf) var files = []struct { name string body string }{ {"/prefix/foo/bar/../../home/bburns/names.txt", "Down and back"}, } for _, file := range files { hdr := &tar.Header{ Name: file.name, Mode: 0600, Size: int64(len(file.body)), } if err := tw.WriteHeader(hdr); err != nil { t.Errorf("unexpected error: %v ", err) t.FailNow() } if _, err := tw.Write([]byte(file.body)); err != nil { t.Errorf("unexpected error: %v ", err) t.FailNow() } } if err := tw.Close(); err != nil { t.Errorf("unexpected error: %v ", err) t.FailNow() } opts := NewCopyOptions(genericiooptions.NewTestIOStreamsDiscard()) if err := opts.untarAll("", "", "/prefix", remotePath{}, newLocalPath(dir), &buf); err != nil { t.Errorf("unexpected error: %v ", err) t.FailNow() } for _, file := range files { _, err := os.Stat(dir + filepath.Clean(file.name[len("/prefix"):])) if err != nil { t.Errorf("Error finding file: %v", err) } } } func TestCopyToPod(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") ns := scheme.Codecs.WithoutConversion() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { responsePod := &v1.Pod{} return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCp(tf, ioStreams) srcFile, err := os.MkdirTemp("", "test") if err != nil { t.Errorf("unexpected error: %v", err) t.FailNow() } defer os.RemoveAll(srcFile) tests := map[string]struct { src string dest string expectedErr bool }{ "copy to directory": { src: srcFile, dest: "/tmp/", expectedErr: false, }, "copy to root": { src: srcFile, dest: "/", expectedErr: false, }, "copy to empty file name": { src: srcFile, dest: "", expectedErr: true, }, "copy unexisting file": { src: filepath.Join(srcFile, "nope"), dest: "/tmp", expectedErr: true, }, } for name, test := range tests { opts := NewCopyOptions(ioStreams) opts.Complete(tf, cmd, []string{test.src, fmt.Sprintf("pod-ns/pod-name:%s", test.dest)}) t.Run(name, func(t *testing.T) { err = opts.Run() //If error is NotFound error , it indicates that the //request has been sent correctly. //Treat this as no error. if test.expectedErr && errors.IsNotFound(err) { t.Errorf("expected error but didn't get one") } if !test.expectedErr && !errors.IsNotFound(err) { t.Errorf("unexpected error: %v", err) } }) } } func TestCopyToPodNoPreserve(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") ns := scheme.Codecs.WithoutConversion() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { responsePod := &v1.Pod{} return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, responsePod))))}, nil }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCp(tf, ioStreams) srcFile, err := os.MkdirTemp("", "test") if err != nil { t.Errorf("unexpected error: %v", err) t.FailNow() } defer os.RemoveAll(srcFile) tests := map[string]struct { expectedCmd []string nopreserve bool }{ "copy to pod no preserve user and permissions": { expectedCmd: []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-", "-C", "."}, nopreserve: true, }, "copy to pod preserve user and permissions": { expectedCmd: []string{"tar", "-xmf", "-", "-C", "."}, nopreserve: false, }, } opts := NewCopyOptions(ioStreams) src := fileSpec{ File: newLocalPath(srcFile), } dest := fileSpec{ PodNamespace: "pod-ns", PodName: "pod-name", File: newRemotePath("foo"), } opts.Complete(tf, cmd, nil) for name, test := range tests { t.Run(name, func(t *testing.T) { options := &kexec.ExecOptions{} opts.NoPreserve = test.nopreserve err = opts.copyToPod(src, dest, options) if !(reflect.DeepEqual(test.expectedCmd, options.Command)) { t.Errorf("expected cmd: %v, got: %v", test.expectedCmd, options.Command) } }) } } func TestValidate(t *testing.T) { tests := []struct { name string args []string expectedErr bool }{ { name: "Validate Succeed", args: []string{"1", "2"}, expectedErr: false, }, { name: "Validate Fail", args: []string{"1", "2", "3"}, expectedErr: true, }, } ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() opts := NewCopyOptions(ioStreams) for _, test := range tests { t.Run(test.name, func(t *testing.T) { opts.args = test.args err := opts.Validate() if (err != nil) != test.expectedErr { t.Errorf("expected error: %v, saw: %v, error: %v", test.expectedErr, err != nil, err) } }) } } func TestUntar(t *testing.T) { testdir, err := os.MkdirTemp("", "test-untar") require.NoError(t, err) defer os.RemoveAll(testdir) t.Logf("Test base: %s", testdir) basedir := testdir + "/" + "base" type file struct { path string linkTarget string // For link types expected string // Expect to find the file here (or not, if empty) } files := []file{{ // Absolute file within dest path: basedir + "/" + "abs", expected: basedir + basedir + "/" + "abs", }, { // Absolute file outside dest path: testdir + "/" + "abs-out", expected: basedir + testdir + "/" + "abs-out", }, { // Absolute nested file within dest path: basedir + "/" + "nested/nest-abs", expected: basedir + basedir + "/" + "nested/nest-abs", }, { // Absolute nested file outside dest path: basedir + "/" + "nested/../../nest-abs-out", expected: basedir + testdir + "/" + "nest-abs-out", }, { // Relative file inside dest path: "relative", expected: basedir + "/" + "relative", }, { // Relative file outside dest path: "../unrelative", expected: "", }, { // Relative file outside dest (windows) path: `..\unrelative-windows`, expected: "", }, { // Nested relative file inside dest path: "nested/nest-rel", expected: basedir + "/" + "nested/nest-rel", }, { // Nested relative file outside dest path: "nested/../../nest-unrelative", expected: "", }, { // Nested relative file outside dest (windows) path: `nested\..\..\nest-unrelative`, expected: "", }} links := []file{} for _, f := range files { links = append(links, file{ path: f.path + "-innerlink", linkTarget: "link-target", expected: "", }, file{ path: f.path + "-innerlink-abs", linkTarget: basedir + "/" + "link-target", expected: "", }, file{ path: f.path + "-backlink", linkTarget: ".." + "/" + "link-target", expected: "", }, file{ path: f.path + "-outerlink-abs", linkTarget: testdir + "/" + "link-target", expected: "", }) if f.expected != "" { // outerlink is the number of backticks to escape to testdir outerlink, _ := filepath.Rel(f.expected, testdir) links = append(links, file{ path: f.path + "outerlink", linkTarget: outerlink + "/" + "link-target", expected: "", }) } } files = append(files, links...) // Test back-tick escaping through a symlink. files = append(files, file{ path: "nested/again/back-link", linkTarget: "../../nested", expected: "", }, file{ path: "nested/again/back-link/../../../back-link-file", expected: basedir + "/" + "back-link-file", }) // Test chaining back-tick symlinks. files = append(files, file{ path: "nested/back-link-first", linkTarget: "../", expected: "", }, file{ path: "nested/back-link-first/back-link-second", linkTarget: "../", expected: "", }) files = append(files, file{ // Relative directory path with terminating / path: "direct/dir/", expected: "", }) buf := &bytes.Buffer{} tw := tar.NewWriter(buf) expectations := map[string]bool{} for _, f := range files { if f.expected != "" { expectations[f.expected] = false } if f.linkTarget == "" { hdr := &tar.Header{ Name: f.path, Mode: 0666, Size: int64(len(f.path)), } require.NoError(t, tw.WriteHeader(hdr), f.path) if !strings.HasSuffix(f.path, "/") { _, err := tw.Write([]byte(f.path)) require.NoError(t, err, f.path) } } else { hdr := &tar.Header{ Name: f.path, Mode: int64(0777 | os.ModeSymlink), Typeflag: tar.TypeSymlink, Linkname: f.linkTarget, } require.NoError(t, tw.WriteHeader(hdr), f.path) } } tw.Close() // Capture warnings to stderr for debugging. output := (*testWriter)(t) opts := NewCopyOptions(genericiooptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output}) require.NoError(t, opts.untarAll("", "", "", remotePath{}, newLocalPath(basedir), buf)) filepath.Walk(testdir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil // Ignore directories. } if _, ok := expectations[path]; !ok { t.Errorf("Unexpected file at %s", path) } else { expectations[path] = true } return nil }) for path, found := range expectations { if !found { t.Errorf("Missing expected file %s", path) } } } func TestUntar_SingleFile(t *testing.T) { testdir, err := os.MkdirTemp("", "test-untar") require.NoError(t, err) defer os.RemoveAll(testdir) dest := testdir + "/" + "target" buf := &bytes.Buffer{} tw := tar.NewWriter(buf) const ( srcName = "source" content = "file contents" ) hdr := &tar.Header{ Name: srcName, Mode: 0666, Size: int64(len(content)), } require.NoError(t, tw.WriteHeader(hdr)) _, err = tw.Write([]byte(content)) require.NoError(t, err) tw.Close() // Capture warnings to stderr for debugging. output := (*testWriter)(t) opts := NewCopyOptions(genericiooptions.IOStreams{In: &bytes.Buffer{}, Out: output, ErrOut: output}) require.NoError(t, opts.untarAll("", "", srcName, remotePath{}, newLocalPath(dest), buf)) cmpFileData(t, dest, content) } func createTmpFile(t *testing.T, filepath, data string) { f, err := os.Create(filepath) if err != nil { t.Fatalf("unexpected error: %v", err) } defer f.Close() if _, err := io.Copy(f, bytes.NewBuffer([]byte(data))); err != nil { t.Fatalf("unexpected error: %v", err) } if err := f.Close(); err != nil { t.Fatal(err) } } func cmpFileData(t *testing.T, filePath, data string) { actual, err := os.ReadFile(filePath) require.NoError(t, err) assert.EqualValues(t, data, actual) } type testWriter testing.T func (t *testWriter) Write(p []byte) (n int, err error) { t.Log(string(p)) return len(p), nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/cp/filespec.go000066400000000000000000000074341504711711200273420ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cp import ( "path" "path/filepath" "strings" ) type fileSpec struct { PodName string PodNamespace string File pathSpec } type pathSpec interface { String() string } // localPath represents a client-native path, which will differ based // on the client OS, its methods will use path/filepath package which // is OS dependant type localPath struct { file string } func newLocalPath(fileName string) localPath { file := stripTrailingSlash(fileName) return localPath{file: file} } func (p localPath) String() string { return p.file } func (p localPath) Dir() localPath { return newLocalPath(filepath.Dir(p.file)) } func (p localPath) Base() localPath { return newLocalPath(filepath.Base(p.file)) } func (p localPath) Clean() localPath { return newLocalPath(filepath.Clean(p.file)) } func (p localPath) Join(elem pathSpec) localPath { return newLocalPath(filepath.Join(p.file, elem.String())) } func (p localPath) Glob() (matches []string, err error) { return filepath.Glob(p.file) } func (p localPath) StripSlashes() localPath { return newLocalPath(stripLeadingSlash(p.file)) } func isRelative(base, target localPath) bool { relative, err := filepath.Rel(base.String(), target.String()) if err != nil { return false } return relative == "." || relative == stripPathShortcuts(relative) } // remotePath represents always UNIX path, its methods will use path // package which is always using `/` type remotePath struct { file string } func newRemotePath(fileName string) remotePath { // we assume remote file is a linux container but we need to convert // windows path separators to unix style for consistent processing file := strings.ReplaceAll(stripTrailingSlash(fileName), `\`, "/") return remotePath{file: file} } func (p remotePath) String() string { return p.file } func (p remotePath) Dir() remotePath { return newRemotePath(path.Dir(p.file)) } func (p remotePath) Base() remotePath { return newRemotePath(path.Base(p.file)) } func (p remotePath) Clean() remotePath { return newRemotePath(path.Clean(p.file)) } func (p remotePath) Join(elem pathSpec) remotePath { return newRemotePath(path.Join(p.file, elem.String())) } func (p remotePath) StripShortcuts() remotePath { p = p.Clean() return newRemotePath(stripPathShortcuts(p.file)) } func (p remotePath) StripSlashes() remotePath { return newRemotePath(stripLeadingSlash(p.file)) } // strips trailing slash (if any) both unix and windows style func stripTrailingSlash(file string) string { if len(file) == 0 { return file } if file != "/" && strings.HasSuffix(string(file[len(file)-1]), "/") { return file[:len(file)-1] } return file } func stripLeadingSlash(file string) string { // tar strips the leading '/' and '\' if it's there, so we will too return strings.TrimLeft(file, `/\`) } // stripPathShortcuts removes any leading or trailing "../" from a given path func stripPathShortcuts(p string) string { newPath := p trimmed := strings.TrimPrefix(newPath, "../") for trimmed != newPath { newPath = trimmed trimmed = strings.TrimPrefix(newPath, "../") } // trim leftover {".", ".."} if newPath == "." || newPath == ".." { newPath = "" } if len(newPath) > 0 && string(newPath[0]) == "/" { return newPath[1:] } return newPath } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/000077500000000000000000000000001504711711200260525ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create.go000066400000000000000000000337761504711711200276640ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "io" "net/url" "runtime" "strings" "github.com/spf13/cobra" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" kruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/editor" "k8s.io/kubectl/pkg/generate" "k8s.io/kubectl/pkg/rawhttp" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // CreateOptions is the commandline options for 'create' sub command type CreateOptions struct { PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string fieldManager string FilenameOptions resource.FilenameOptions Selector string EditBeforeCreate bool Raw string Recorder genericclioptions.Recorder PrintObj func(obj kruntime.Object) error genericiooptions.IOStreams } var ( createLong = templates.LongDesc(i18n.T(` Create a resource from a file or from stdin. JSON and YAML formats are accepted.`)) createExample = templates.Examples(i18n.T(` # Create a pod using the data in pod.json kubectl create -f ./pod.json # Create a pod based on the JSON passed into stdin cat pod.json | kubectl create -f - # Edit the data in registry.yaml in JSON then create the resource using the edited data kubectl create -f registry.yaml --edit -o json`)) ) // NewCreateOptions returns an initialized CreateOptions instance func NewCreateOptions(ioStreams genericiooptions.IOStreams) *CreateOptions { return &CreateOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, IOStreams: ioStreams, } } // NewCmdCreate returns new initialized instance of create sub command func NewCmdCreate(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCreateOptions(ioStreams) cmd := &cobra.Command{ Use: "create -f FILENAME", DisableFlagsInUseLine: true, Short: i18n.T("Create a resource from a file or from stdin"), Long: createLong, Example: createExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunCreate(f, cmd)) }, } // bind flag structs o.RecordFlags.AddFlags(cmd) usage := "to use to create the resource" cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddValidateFlags(cmd) cmd.Flags().BoolVar(&o.EditBeforeCreate, "edit", o.EditBeforeCreate, "Edit the API resource before creating") cmd.Flags().Bool("windows-line-endings", runtime.GOOS == "windows", "Only relevant if --edit=true. Defaults to the line ending native to your platform.") cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to POST to the server. Uses the transport specified by the kubeconfig file.") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-create") o.PrintFlags.AddFlags(cmd) // create subcommands cmd.AddCommand(NewCmdCreateNamespace(f, ioStreams)) cmd.AddCommand(NewCmdCreateQuota(f, ioStreams)) cmd.AddCommand(NewCmdCreateSecret(f, ioStreams)) cmd.AddCommand(NewCmdCreateConfigMap(f, ioStreams)) cmd.AddCommand(NewCmdCreateServiceAccount(f, ioStreams)) cmd.AddCommand(NewCmdCreateService(f, ioStreams)) cmd.AddCommand(NewCmdCreateDeployment(f, ioStreams)) cmd.AddCommand(NewCmdCreateClusterRole(f, ioStreams)) cmd.AddCommand(NewCmdCreateClusterRoleBinding(f, ioStreams)) cmd.AddCommand(NewCmdCreateRole(f, ioStreams)) cmd.AddCommand(NewCmdCreateRoleBinding(f, ioStreams)) cmd.AddCommand(NewCmdCreatePodDisruptionBudget(f, ioStreams)) cmd.AddCommand(NewCmdCreatePriorityClass(f, ioStreams)) cmd.AddCommand(NewCmdCreateJob(f, ioStreams)) cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams)) cmd.AddCommand(NewCmdCreateIngress(f, ioStreams)) cmd.AddCommand(NewCmdCreateToken(f, ioStreams)) return cmd } // Validate makes sure there is no discrepancy in command options func (o *CreateOptions) Validate() error { if err := o.FilenameOptions.RequireFilenameOrKustomize(); err != nil { return err } if len(o.Raw) > 0 { if o.EditBeforeCreate { return fmt.Errorf("--raw and --edit are mutually exclusive") } if len(o.FilenameOptions.Filenames) != 1 { return fmt.Errorf("--raw can only use a single local file or stdin") } if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 { return fmt.Errorf("--raw cannot read from a url") } if o.FilenameOptions.Recursive { return fmt.Errorf("--raw and --recursive are mutually exclusive") } if len(o.Selector) > 0 { return fmt.Errorf("--raw and --selector (-l) are mutually exclusive") } if o.PrintFlags.OutputFormat != nil && len(*o.PrintFlags.OutputFormat) > 0 { return fmt.Errorf("--raw and --output are mutually exclusive") } if _, err := url.ParseRequestURI(o.Raw); err != nil { return fmt.Errorf("--raw must be a valid URL path: %v", err) } } return nil } // Complete completes all the required options func (o *CreateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) } var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj kruntime.Object) error { return printer.PrintObj(obj, o.Out) } return nil } // RunCreate performs the creation func (o *CreateOptions) RunCreate(f cmdutil.Factory, cmd *cobra.Command) error { // raw only makes sense for a single file resource multiple objects aren't likely to do what you want. // the validator enforces this, so if len(o.Raw) > 0 { restClient, err := f.RESTClient() if err != nil { return err } return rawhttp.RawPost(restClient, o.IOStreams, o.Raw, o.FilenameOptions.Filenames[0]) } if o.EditBeforeCreate { return RunEditOnCreate(f, o.PrintFlags, o.RecordFlags, o.IOStreams, cmd, &o.FilenameOptions, o.fieldManager) } schema, err := f.Validator(o.ValidationDirective) if err != nil { return err } cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } r := f.NewBuilder(). Unstructured(). Schema(schema). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). LabelSelectorParam(o.Selector). Flatten(). Do() err = r.Err() if err != nil { return err } count := 0 err = r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info.Object, scheme.DefaultJSONEncoder()); err != nil { return cmdutil.AddSourceToErr("creating", info.Source, err) } if err := o.Recorder.Record(info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } if o.DryRunStrategy != cmdutil.DryRunClient { obj, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). WithFieldValidation(o.ValidationDirective). Create(info.Namespace, true, info.Object) if err != nil { return cmdutil.AddSourceToErr("creating", info.Source, err) } info.Refresh(obj, true) } count++ return o.PrintObj(info.Object) }) if err != nil { return err } if count == 0 { return fmt.Errorf("no objects passed to create") } return nil } // RunEditOnCreate performs edit on creation func RunEditOnCreate(f cmdutil.Factory, printFlags *genericclioptions.PrintFlags, recordFlags *genericclioptions.RecordFlags, ioStreams genericiooptions.IOStreams, cmd *cobra.Command, options *resource.FilenameOptions, fieldManager string) error { editOptions := editor.NewEditOptions(editor.EditBeforeCreateMode, ioStreams) editOptions.FilenameOptions = *options validationDirective, err := cmdutil.GetValidationDirective(cmd) if err != nil { return err } editOptions.ValidateOptions = cmdutil.ValidateOptions{ ValidationDirective: string(validationDirective), } editOptions.PrintFlags = printFlags editOptions.ApplyAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) editOptions.RecordFlags = recordFlags editOptions.FieldManager = "kubectl-create" err = editOptions.Complete(f, []string{}, cmd) if err != nil { return err } return editOptions.Run() } // NameFromCommandArgs is a utility function for commands that assume the first argument is a resource name func NameFromCommandArgs(cmd *cobra.Command, args []string) (string, error) { argsLen := cmd.ArgsLenAtDash() // ArgsLenAtDash returns -1 when -- was not specified if argsLen == -1 { argsLen = len(args) } if argsLen != 1 { return "", cmdutil.UsageErrorf(cmd, "exactly one NAME is required, got %d", argsLen) } return args[0], nil } // CreateSubcommandOptions is an options struct to support create subcommands type CreateSubcommandOptions struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags // Name of resource being created Name string // StructuredGenerator is the resource generator for the object being created StructuredGenerator generate.StructuredGenerator DryRunStrategy cmdutil.DryRunStrategy CreateAnnotation bool FieldManager string ValidationDirective string Namespace string EnforceNamespace bool Mapper meta.RESTMapper DynamicClient dynamic.Interface PrintObj printers.ResourcePrinterFunc genericiooptions.IOStreams } // NewCreateSubcommandOptions returns initialized CreateSubcommandOptions func NewCreateSubcommandOptions(ioStreams genericiooptions.IOStreams) *CreateSubcommandOptions { return &CreateSubcommandOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // Complete completes all the required options func (o *CreateSubcommandOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string, generator generate.StructuredGenerator) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name o.StructuredGenerator = generator o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } o.PrintObj = func(obj kruntime.Object, out io.Writer) error { return printer.PrintObj(obj, out) } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.DynamicClient, err = f.DynamicClient() if err != nil { return err } o.Mapper, err = f.ToRESTMapper() if err != nil { return err } return nil } // Run executes a create subcommand using the specified options func (o *CreateSubcommandOptions) Run() error { obj, err := o.StructuredGenerator.StructuredGenerate() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, obj, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { // create subcommands have compiled knowledge of things they create, so type them directly gvks, _, err := scheme.Scheme.ObjectKinds(obj) if err != nil { return err } gvk := gvks[0] mapping, err := o.Mapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version) if err != nil { return err } asUnstructured := &unstructured.Unstructured{} if err := scheme.Scheme.Convert(obj, asUnstructured, nil); err != nil { return err } if mapping.Scope.Name() == meta.RESTScopeNameRoot { o.Namespace = "" } createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } actualObject, err := o.DynamicClient.Resource(mapping.Resource).Namespace(o.Namespace).Create(context.TODO(), asUnstructured, createOptions) if err != nil { return err } // ensure we pass a versioned object to the printer obj = actualObject } else { if meta, err := meta.Accessor(obj); err == nil && o.EnforceNamespace { meta.SetNamespace(o.Namespace) } } return o.PrintObj(obj, o.Out) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_clusterrole.go000066400000000000000000000170031504711711200322700ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "strings" "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" cliflag "k8s.io/component-base/cli/flag" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( clusterRoleLong = templates.LongDesc(i18n.T(` Create a cluster role.`)) clusterRoleExample = templates.Examples(i18n.T(` # Create a cluster role named "pod-reader" that allows user to perform "get", "watch" and "list" on pods kubectl create clusterrole pod-reader --verb=get,list,watch --resource=pods # Create a cluster role named "pod-reader" with ResourceName specified kubectl create clusterrole pod-reader --verb=get --resource=pods --resource-name=readablepod --resource-name=anotherpod # Create a cluster role named "foo" with API Group specified kubectl create clusterrole foo --verb=get,list,watch --resource=rs.apps # Create a cluster role named "foo" with SubResource specified kubectl create clusterrole foo --verb=get,list,watch --resource=pods,pods/status # Create a cluster role name "foo" with NonResourceURL specified kubectl create clusterrole "foo" --verb=get --non-resource-url=/logs/* # Create a cluster role name "monitoring" with AggregationRule specified kubectl create clusterrole monitoring --aggregation-rule="rbac.example.com/aggregate-to-monitoring=true"`)) // Valid nonResource verb list for validation. validNonResourceVerbs = []string{"*", "get", "post", "put", "delete", "patch", "head", "options"} ) // CreateClusterRoleOptions is returned by NewCmdCreateClusterRole type CreateClusterRoleOptions struct { *CreateRoleOptions NonResourceURLs []string AggregationRule map[string]string FieldManager string } // NewCmdCreateClusterRole initializes and returns new ClusterRoles command func NewCmdCreateClusterRole(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { c := &CreateClusterRoleOptions{ CreateRoleOptions: NewCreateRoleOptions(ioStreams), AggregationRule: map[string]string{}, } cmd := &cobra.Command{ Use: "clusterrole NAME --verb=verb --resource=resource.group [--resource-name=resourcename] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a cluster role"), Long: clusterRoleLong, Example: clusterRoleExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(c.Complete(f, cmd, args)) cmdutil.CheckErr(c.Validate()) cmdutil.CheckErr(c.RunCreateRole()) }, } c.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringSliceVar(&c.Verbs, "verb", c.Verbs, "Verb that applies to the resources contained in the rule") cmd.Flags().StringSliceVar(&c.NonResourceURLs, "non-resource-url", c.NonResourceURLs, "A partial url that user should have access to.") cmd.Flags().StringSlice("resource", []string{}, "Resource that the rule applies to") cmd.Flags().StringArrayVar(&c.ResourceNames, "resource-name", c.ResourceNames, "Resource in the white list that the rule applies to, repeat this flag for multiple items") cmd.Flags().Var(cliflag.NewMapStringString(&c.AggregationRule), "aggregation-rule", "An aggregation label selector for combining ClusterRoles.") cmdutil.AddFieldManagerFlagVar(cmd, &c.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (c *CreateClusterRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { // Remove duplicate nonResourceURLs nonResourceURLs := []string{} for _, n := range c.NonResourceURLs { if !arrayContains(nonResourceURLs, n) { nonResourceURLs = append(nonResourceURLs, n) } } c.NonResourceURLs = nonResourceURLs return c.CreateRoleOptions.Complete(f, cmd, args) } // Validate makes sure there is no discrepency in CreateClusterRoleOptions func (c *CreateClusterRoleOptions) Validate() error { if c.Name == "" { return fmt.Errorf("name must be specified") } if len(c.AggregationRule) > 0 { if len(c.NonResourceURLs) > 0 || len(c.Verbs) > 0 || len(c.Resources) > 0 || len(c.ResourceNames) > 0 { return fmt.Errorf("aggregation rule must be specified without nonResourceURLs, verbs, resources or resourceNames") } return nil } // validate verbs. if len(c.Verbs) == 0 { return fmt.Errorf("at least one verb must be specified") } if len(c.Resources) == 0 && len(c.NonResourceURLs) == 0 { return fmt.Errorf("one of resource or nonResourceURL must be specified") } // validate resources if len(c.Resources) > 0 { for _, v := range c.Verbs { if !arrayContains(validResourceVerbs, v) { fmt.Fprintf(c.ErrOut, "Warning: '%s' is not a standard resource verb\n", v) } } if err := c.validateResource(); err != nil { return err } } //validate non-resource-url if len(c.NonResourceURLs) > 0 { for _, v := range c.Verbs { if !arrayContains(validNonResourceVerbs, v) { return fmt.Errorf("invalid verb: '%s' for nonResourceURL", v) } } for _, nonResourceURL := range c.NonResourceURLs { if nonResourceURL == "*" { continue } if nonResourceURL == "" || !strings.HasPrefix(nonResourceURL, "/") { return fmt.Errorf("nonResourceURL should start with /") } if strings.ContainsRune(nonResourceURL[:len(nonResourceURL)-1], '*') { return fmt.Errorf("nonResourceURL only supports wildcard matches when '*' is at the end") } } } return nil } // RunCreateRole creates a new clusterRole func (c *CreateClusterRoleOptions) RunCreateRole() error { clusterRole := &rbacv1.ClusterRole{ // this is ok because we know exactly how we want to be serialized TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "ClusterRole"}, } clusterRole.Name = c.Name var err error if len(c.AggregationRule) == 0 { rules, err := generateResourcePolicyRules(c.Mapper, c.Verbs, c.Resources, c.ResourceNames, c.NonResourceURLs) if err != nil { return err } clusterRole.Rules = rules } else { clusterRole.AggregationRule = &rbacv1.AggregationRule{ ClusterRoleSelectors: []metav1.LabelSelector{ { MatchLabels: c.AggregationRule, }, }, } } if err := util.CreateOrUpdateAnnotation(c.CreateAnnotation, clusterRole, scheme.DefaultJSONEncoder()); err != nil { return err } // Create ClusterRole. if c.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if c.FieldManager != "" { createOptions.FieldManager = c.FieldManager } createOptions.FieldValidation = c.ValidationDirective if c.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } clusterRole, err = c.Client.ClusterRoles().Create(context.TODO(), clusterRole, createOptions) if err != nil { return err } } return c.PrintObj(clusterRole) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_clusterrole_test.go000066400000000000000000000326701504711711200333360ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "testing" rbac "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestCreateClusterRole(t *testing.T) { clusterRoleName := "my-cluster-role" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{} tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tests := map[string]struct { verbs string resources string nonResourceURL string resourceNames string aggregationRule string expectedClusterRole *rbac.ClusterRole }{ "test-duplicate-resources": { verbs: "get,watch,list", resources: "pods,pods", expectedClusterRole: &rbac.ClusterRole{ TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}, ObjectMeta: metav1.ObjectMeta{ Name: clusterRoleName, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "watch", "list"}, Resources: []string{"pods"}, APIGroups: []string{""}, ResourceNames: []string{}, }, }, }, }, "test-valid-case-with-multiple-apigroups": { verbs: "get,watch,list", resources: "pods,deployments.extensions", expectedClusterRole: &rbac.ClusterRole{ TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}, ObjectMeta: metav1.ObjectMeta{ Name: clusterRoleName, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "watch", "list"}, Resources: []string{"pods"}, APIGroups: []string{""}, ResourceNames: []string{}, }, { Verbs: []string{"get", "watch", "list"}, Resources: []string{"deployments"}, APIGroups: []string{"extensions"}, ResourceNames: []string{}, }, }, }, }, "test-non-resource-url": { verbs: "get", nonResourceURL: "/logs/,/healthz", expectedClusterRole: &rbac.ClusterRole{ TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}, ObjectMeta: metav1.ObjectMeta{ Name: clusterRoleName, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get"}, NonResourceURLs: []string{"/logs/", "/healthz"}, }, }, }, }, "test-resource-and-non-resource-url": { verbs: "get", nonResourceURL: "/logs/,/healthz", resources: "pods", expectedClusterRole: &rbac.ClusterRole{ TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}, ObjectMeta: metav1.ObjectMeta{ Name: clusterRoleName, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get"}, Resources: []string{"pods"}, APIGroups: []string{""}, ResourceNames: []string{}, }, { Verbs: []string{"get"}, NonResourceURLs: []string{"/logs/", "/healthz"}, }, }, }, }, "test-aggregation-rules": { aggregationRule: "foo1=foo2,foo3=foo4", expectedClusterRole: &rbac.ClusterRole{ TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}, ObjectMeta: metav1.ObjectMeta{ Name: clusterRoleName, }, AggregationRule: &rbac.AggregationRule{ ClusterRoleSelectors: []metav1.LabelSelector{ { MatchLabels: map[string]string{ "foo1": "foo2", "foo3": "foo4", }, }, }, }, }, }, } for name, test := range tests { ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateClusterRole(tf, ioStreams) cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", "yaml") cmd.Flags().Set("verb", test.verbs) cmd.Flags().Set("resource", test.resources) cmd.Flags().Set("non-resource-url", test.nonResourceURL) cmd.Flags().Set("aggregation-rule", test.aggregationRule) if test.resourceNames != "" { cmd.Flags().Set("resource-name", test.resourceNames) } cmd.Run(cmd, []string{clusterRoleName}) actual := &rbac.ClusterRole{} if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), buf.Bytes(), actual); err != nil { t.Log(buf.String()) t.Fatal(err) } if !equality.Semantic.DeepEqual(test.expectedClusterRole, actual) { t.Errorf("%s:\nexpected:\n%#v\nsaw:\n%#v", name, test.expectedClusterRole, actual) } } } func TestClusterRoleValidate(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tests := map[string]struct { clusterRoleOptions *CreateClusterRoleOptions expectErr bool }{ "test-missing-name": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{}, }, expectErr: true, }, "test-missing-verb": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", }, }, expectErr: true, }, "test-missing-resource": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get"}, }, }, expectErr: true, }, "test-missing-resource-existing-apigroup": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get"}, Resources: []ResourceOptions{ { Group: "extensions", }, }, }, }, expectErr: true, }, "test-missing-resource-existing-subresource": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get"}, Resources: []ResourceOptions{ { SubResource: "scale", }, }, }, }, expectErr: true, }, "test-invalid-verb": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"invalid-verb"}, Resources: []ResourceOptions{ { Resource: "pods", }, }, }, }, expectErr: false, }, "test-nonresource-verb": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"post"}, Resources: []ResourceOptions{ { Resource: "pods", }, }, }, }, expectErr: false, }, "test-special-verb": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"use"}, Resources: []ResourceOptions{ { Resource: "pods", }, }, }, }, expectErr: true, }, "test-mix-verbs": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"impersonate", "use"}, Resources: []ResourceOptions{ { Resource: "userextras", SubResource: "scopes", }, }, }, }, expectErr: true, }, "test-special-verb-with-wrong-apigroup": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"impersonate"}, Resources: []ResourceOptions{ { Resource: "userextras", SubResource: "scopes", Group: "extensions", }, }, }, }, expectErr: true, }, "test-invalid-resource": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get"}, Resources: []ResourceOptions{ { Resource: "invalid-resource", }, }, }, }, expectErr: true, }, "test-resource-name-with-multiple-resources": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get"}, Resources: []ResourceOptions{ { Resource: "pods", }, { Resource: "deployments", Group: "extensions", }, }, }, }, expectErr: false, }, "test-valid-case": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "role-binder", Verbs: []string{"get", "list", "bind"}, Resources: []ResourceOptions{ { Resource: "roles", Group: "rbac.authorization.k8s.io", }, }, }, }, expectErr: false, }, "test-valid-case-with-subresource": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get", "list"}, Resources: []ResourceOptions{ { Resource: "replicasets", SubResource: "scale", }, }, }, }, expectErr: false, }, "test-valid-case-with-additional-resource": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"impersonate"}, Resources: []ResourceOptions{ { Resource: "userextras", SubResource: "scopes", Group: "authentication.k8s.io", }, }, }, }, expectErr: false, }, "test-invalid-empty-non-resource-url": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"create"}, }, NonResourceURLs: []string{""}, }, expectErr: true, }, "test-invalid-non-resource-url": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"create"}, }, NonResourceURLs: []string{"logs"}, }, expectErr: true, }, "test-invalid-non-resource-url-with-*": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"create"}, }, NonResourceURLs: []string{"/logs/*/"}, }, expectErr: true, }, "test-invalid-non-resource-url-with-multiple-*": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"create"}, }, NonResourceURLs: []string{"/logs*/*"}, }, expectErr: true, }, "test-invalid-verb-for-non-resource-url": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"create"}, }, NonResourceURLs: []string{"/logs/"}, }, expectErr: true, }, "test-resource-and-non-resource-url-specified-together": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get"}, Resources: []ResourceOptions{ { Resource: "replicasets", SubResource: "scale", }, }, }, NonResourceURLs: []string{"/logs/", "/logs/*"}, }, expectErr: false, }, "test-aggregation-rule-with-verb": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Verbs: []string{"get"}, }, AggregationRule: map[string]string{"foo-key": "foo-vlue"}, }, expectErr: true, }, "test-aggregation-rule-with-resource": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", Resources: []ResourceOptions{ { Resource: "replicasets", SubResource: "scale", }, }, }, AggregationRule: map[string]string{"foo-key": "foo-vlue"}, }, expectErr: true, }, "test-aggregation-rule-with-no-resource-url": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", }, NonResourceURLs: []string{"/logs/"}, AggregationRule: map[string]string{"foo-key": "foo-vlue"}, }, expectErr: true, }, "test-aggregation-rule": { clusterRoleOptions: &CreateClusterRoleOptions{ CreateRoleOptions: &CreateRoleOptions{ Name: "my-clusterrole", }, AggregationRule: map[string]string{"foo-key": "foo-vlue"}, }, expectErr: false, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { test.clusterRoleOptions.IOStreams = genericiooptions.NewTestIOStreamsDiscard() var err error test.clusterRoleOptions.Mapper, err = tf.ToRESTMapper() if err != nil { t.Fatal(err) } err = test.clusterRoleOptions.Validate() if test.expectErr && err == nil { t.Errorf("%s: expect error happens, but validate passes.", name) } if !test.expectErr && err != nil { t.Errorf("%s: unexpected error: %v", name, err) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_clusterrolebinding.go000066400000000000000000000167521504711711200336350ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "strings" "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" rbacclientv1 "k8s.io/client-go/kubernetes/typed/rbac/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( clusterRoleBindingLong = templates.LongDesc(i18n.T(` Create a cluster role binding for a particular cluster role.`)) clusterRoleBindingExample = templates.Examples(i18n.T(` # Create a cluster role binding for user1, user2, and group1 using the cluster-admin cluster role kubectl create clusterrolebinding cluster-admin --clusterrole=cluster-admin --user=user1 --user=user2 --group=group1`)) ) // ClusterRoleBindingOptions is returned by NewCmdCreateClusterRoleBinding type ClusterRoleBindingOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string ClusterRole string Users []string Groups []string ServiceAccounts []string FieldManager string CreateAnnotation bool Client rbacclientv1.RbacV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewClusterRoleBindingOptions creates a new *ClusterRoleBindingOptions with sane defaults func NewClusterRoleBindingOptions(ioStreams genericiooptions.IOStreams) *ClusterRoleBindingOptions { return &ClusterRoleBindingOptions{ Users: []string{}, Groups: []string{}, ServiceAccounts: []string{}, PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateClusterRoleBinding returns an initialized command instance of ClusterRoleBinding func NewCmdCreateClusterRoleBinding(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewClusterRoleBindingOptions(ioStreams) cmd := &cobra.Command{ Use: "clusterrolebinding NAME --clusterrole=NAME [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a cluster role binding for a particular cluster role"), Long: clusterRoleBindingLong, Example: clusterRoleBindingExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.ClusterRole, "clusterrole", "", i18n.T("ClusterRole this ClusterRoleBinding should reference")) cmd.MarkFlagRequired("clusterrole") cmd.Flags().StringArrayVar(&o.Users, "user", o.Users, "Usernames to bind to the clusterrole. The flag can be repeated to add multiple users.") cmd.Flags().StringArrayVar(&o.Groups, "group", o.Groups, "Groups to bind to the clusterrole. The flag can be repeated to add multiple groups.") cmd.Flags().StringArrayVar(&o.ServiceAccounts, "serviceaccount", o.ServiceAccounts, "Service accounts to bind to the clusterrole, in the format :. The flag can be repeated to add multiple service accounts.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") // Completion for relevant flags cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( "clusterrole", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.CompGetResource(f, "clusterrole", toComplete), cobra.ShellCompDirectiveNoFileComp })) return cmd } // Complete completes all the required options func (o *ClusterRoleBindingOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } cs, err := f.KubernetesClientSet() if err != nil { return err } o.Client = cs.RbacV1() o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Run calls the CreateSubcommandOptions.Run in ClusterRoleBindingOptions instance func (o *ClusterRoleBindingOptions) Run() error { clusterRoleBinding, err := o.createClusterRoleBinding() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, clusterRoleBinding, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error clusterRoleBinding, err = o.Client.ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding, createOptions) if err != nil { return fmt.Errorf("failed to create clusterrolebinding: %v", err) } } return o.PrintObj(clusterRoleBinding) } func (o *ClusterRoleBindingOptions) createClusterRoleBinding() (*rbacv1.ClusterRoleBinding, error) { clusterRoleBinding := &rbacv1.ClusterRoleBinding{ TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "ClusterRoleBinding"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: o.ClusterRole, }, } for _, user := range o.Users { clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: user, }) } for _, group := range o.Groups { clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: group, }) } for _, sa := range o.ServiceAccounts { tokens := strings.Split(sa, ":") if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { return nil, fmt.Errorf("serviceaccount must be :") } clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ Kind: rbacv1.ServiceAccountKind, APIGroup: "", Namespace: tokens[0], Name: tokens[1], }) } return clusterRoleBinding, nil } create_clusterrolebinding_test.go000066400000000000000000000044361504711711200346110ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "strconv" "testing" rbac "k8s.io/api/rbac/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateClusterRoleBinding(t *testing.T) { tests := []struct { options *ClusterRoleBindingOptions expected *rbac.ClusterRoleBinding }{ { options: &ClusterRoleBindingOptions{ ClusterRole: "fake-clusterrole", Users: []string{"fake-user"}, Groups: []string{"fake-group"}, ServiceAccounts: []string{"fake-namespace:fake-account"}, Name: "fake-binding", }, expected: &rbac.ClusterRoleBinding{ TypeMeta: v1.TypeMeta{ Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: v1.ObjectMeta{ Name: "fake-binding", }, RoleRef: rbac.RoleRef{ APIGroup: rbac.GroupName, Kind: "ClusterRole", Name: "fake-clusterrole", }, Subjects: []rbac.Subject{ { Kind: rbac.UserKind, APIGroup: "rbac.authorization.k8s.io", Name: "fake-user", }, { Kind: rbac.GroupKind, APIGroup: "rbac.authorization.k8s.io", Name: "fake-group", }, { Kind: rbac.ServiceAccountKind, Namespace: "fake-namespace", Name: "fake-account", }, }, }, }, } for i, tc := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { clusterRoleBinding, err := tc.options.createClusterRoleBinding() if err != nil { t.Errorf("unexpected error:\n%#v\n", err) return } if !apiequality.Semantic.DeepEqual(clusterRoleBinding, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, clusterRoleBinding) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap.go000066400000000000000000000325111504711711200316710ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "os" "path/filepath" "strings" "unicode/utf8" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/hash" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( configMapLong = templates.LongDesc(i18n.T(` Create a config map based on a file, directory, or specified literal value. A single config map may package one or more key/value pairs. When creating a config map based on a file, the key will default to the basename of the file, and the value will default to the file content. If the basename is an invalid key, you may specify an alternate key. When creating a config map based on a directory, each file whose basename is a valid key in the directory will be packaged into the config map. Any directory entries except regular files are ignored (e.g. subdirectories, symlinks, devices, pipes, etc).`)) configMapExample = templates.Examples(i18n.T(` # Create a new config map named my-config based on folder bar kubectl create configmap my-config --from-file=path/to/bar # Create a new config map named my-config with specified keys instead of file basenames on disk kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt # Create a new config map named my-config with key1=config1 and key2=config2 kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2 # Create a new config map named my-config from the key=value pairs in the file kubectl create configmap my-config --from-file=path/to/bar # Create a new config map named my-config from an env file kubectl create configmap my-config --from-env-file=path/to/foo.env --from-env-file=path/to/bar.env`)) ) // ConfigMapOptions holds properties for create configmap sub-command type ConfigMapOptions struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // Name of configMap (required) Name string // Type of configMap (optional) Type string // FileSources to derive the configMap from (optional) FileSources []string // LiteralSources to derive the configMap from (optional) LiteralSources []string // EnvFileSources to derive the configMap from (optional) EnvFileSources []string // AppendHash; if true, derive a hash from the ConfigMap and append it to the name AppendHash bool FieldManager string CreateAnnotation bool Namespace string EnforceNamespace bool Client corev1client.CoreV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewConfigMapOptions creates a new *ConfigMapOptions with default value func NewConfigMapOptions(ioStreams genericiooptions.IOStreams) *ConfigMapOptions { return &ConfigMapOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateConfigMap creates the `create configmap` Cobra command func NewCmdCreateConfigMap(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewConfigMapOptions(ioStreams) cmd := &cobra.Command{ Use: "configmap NAME [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Aliases: []string{"cm"}, Short: i18n.T("Create a config map from a local file, directory or literal value"), Long: configMapLong, Example: configMapExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key file can be specified using its file path, in which case file basename will be used as configmap key, or optionally with a key and file path, in which case the given key will be used. Specifying a directory will iterate each named file in the directory whose basename is a valid configmap key.") cmd.Flags().StringArrayVar(&o.LiteralSources, "from-literal", o.LiteralSources, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)") cmd.Flags().StringSliceVar(&o.EnvFileSources, "from-env-file", o.EnvFileSources, "Specify the path to a file to read lines of key=val pairs to create a configmap.") cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the configmap to its name.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete loads data from the command line environment func (o *ConfigMapOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = corev1client.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate checks if ConfigMapOptions has sufficient value to run func (o *ConfigMapOptions) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } if len(o.EnvFileSources) > 0 && (len(o.FileSources) > 0 || len(o.LiteralSources) > 0) { return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal") } return nil } // Run calls createConfigMap and filled in value for configMap object func (o *ConfigMapOptions) Run() error { configMap, err := o.createConfigMap() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, configMap, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } configMap, err = o.Client.ConfigMaps(o.Namespace).Create(context.TODO(), configMap, createOptions) if err != nil { return fmt.Errorf("failed to create configmap: %v", err) } } return o.PrintObj(configMap) } // createConfigMap fills in key value pair from the information given in // ConfigMapOptions into *corev1.ConfigMap func (o *ConfigMapOptions) createConfigMap() (*corev1.ConfigMap, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } configMap := &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Namespace: namespace, }, } configMap.Name = o.Name configMap.Data = map[string]string{} configMap.BinaryData = map[string][]byte{} if len(o.FileSources) > 0 { if err := handleConfigMapFromFileSources(configMap, o.FileSources); err != nil { return nil, err } } if len(o.LiteralSources) > 0 { if err := handleConfigMapFromLiteralSources(configMap, o.LiteralSources); err != nil { return nil, err } } if len(o.EnvFileSources) > 0 { if err := handleConfigMapFromEnvFileSources(configMap, o.EnvFileSources); err != nil { return nil, err } } if o.AppendHash { hash, err := hash.ConfigMapHash(configMap) if err != nil { return nil, err } configMap.Name = fmt.Sprintf("%s-%s", configMap.Name, hash) } return configMap, nil } // handleConfigMapFromLiteralSources adds the specified literal source // information into the provided configMap. func handleConfigMapFromLiteralSources(configMap *corev1.ConfigMap, literalSources []string) error { for _, literalSource := range literalSources { keyName, value, err := util.ParseLiteralSource(literalSource) if err != nil { return err } err = addKeyFromLiteralToConfigMap(configMap, keyName, value) if err != nil { return err } } return nil } // handleConfigMapFromFileSources adds the specified file source information // into the provided configMap func handleConfigMapFromFileSources(configMap *corev1.ConfigMap, fileSources []string) error { for _, fileSource := range fileSources { keyName, filePath, err := util.ParseFileSource(fileSource) if err != nil { return err } info, err := os.Stat(filePath) if err != nil { switch err := err.(type) { case *os.PathError: return fmt.Errorf("error reading %s: %v", filePath, err.Err) default: return fmt.Errorf("error reading %s: %v", filePath, err) } } if info.IsDir() { if strings.Contains(fileSource, "=") { return fmt.Errorf("cannot give a key name for a directory path") } fileList, err := os.ReadDir(filePath) if err != nil { return fmt.Errorf("error listing files in %s: %v", filePath, err) } for _, item := range fileList { itemPath := filepath.Join(filePath, item.Name()) if item.Type().IsRegular() { keyName = item.Name() err = addKeyFromFileToConfigMap(configMap, keyName, itemPath) if err != nil { return err } } } } else { if err := addKeyFromFileToConfigMap(configMap, keyName, filePath); err != nil { return err } } } return nil } // handleConfigMapFromEnvFileSources adds the specified env file source information // into the provided configMap func handleConfigMapFromEnvFileSources(configMap *corev1.ConfigMap, envFileSources []string) error { for _, envFileSource := range envFileSources { info, err := os.Stat(envFileSource) if err != nil { switch err := err.(type) { case *os.PathError: return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) default: return fmt.Errorf("error reading %s: %v", envFileSource, err) } } if info.IsDir() { return fmt.Errorf("env config file cannot be a directory") } err = cmdutil.AddFromEnvFile(envFileSource, func(key, value string) error { return addKeyFromLiteralToConfigMap(configMap, key, value) }) if err != nil { return err } } return nil } // addKeyFromFileToConfigMap adds a key with the given name to a ConfigMap, populating // the value with the content of the given file path, or returns an error. func addKeyFromFileToConfigMap(configMap *corev1.ConfigMap, keyName, filePath string) error { data, err := os.ReadFile(filePath) if err != nil { return err } if utf8.Valid(data) { return addKeyFromLiteralToConfigMap(configMap, keyName, string(data)) } err = validateNewConfigMap(configMap, keyName) if err != nil { return err } configMap.BinaryData[keyName] = data return nil } // addKeyFromLiteralToConfigMap adds the given key and data to the given config map, // returning an error if the key is not valid or if the key already exists. func addKeyFromLiteralToConfigMap(configMap *corev1.ConfigMap, keyName, data string) error { err := validateNewConfigMap(configMap, keyName) if err != nil { return err } configMap.Data[keyName] = data return nil } // validateNewConfigMap checks whether the keyname is valid // Note, the rules for ConfigMap keys are the exact same as the ones for SecretKeys. func validateNewConfigMap(configMap *corev1.ConfigMap, keyName string) error { if errs := validation.IsConfigMapKey(keyName); len(errs) > 0 { return fmt.Errorf("%q is not a valid key name for a ConfigMap: %s", keyName, strings.Join(errs, ",")) } if _, exists := configMap.Data[keyName]; exists { return fmt.Errorf("cannot add key %q, another key by that name already exists in Data for ConfigMap %q", keyName, configMap.Name) } if _, exists := configMap.BinaryData[keyName]; exists { return fmt.Errorf("cannot add key %q, another key by that name already exists in BinaryData for ConfigMap %q", keyName, configMap.Name) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap_test.go000066400000000000000000000343031504711711200327310ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "os" "testing" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateConfigMap(t *testing.T) { tests := map[string]struct { configMapName string configMapType string appendHash bool fromLiteral []string fromFile []string fromEnvFile []string setup func(t *testing.T, configMapOptions *ConfigMapOptions) func() expected *corev1.ConfigMap expectErr string }{ "create_foo_configmap": { configMapName: "foo", expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string]string{}, BinaryData: map[string][]byte{}, }, }, "create_foo_hash_configmap": { configMapName: "foo", appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-867km9574f", }, Data: map[string]string{}, BinaryData: map[string][]byte{}, }, }, "create_foo_type_configmap": { configMapName: "foo", configMapType: "my-type", expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string]string{}, BinaryData: map[string][]byte{}, }, }, "create_foo_type_hash_configmap": { configMapName: "foo", configMapType: "my-type", appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-867km9574f", }, Data: map[string]string{}, BinaryData: map[string][]byte{}, }, }, "create_foo_two_literal_configmap": { configMapName: "foo", fromLiteral: []string{"key1=value1", "key2=value2"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string]string{ "key1": "value1", "key2": "value2", }, BinaryData: map[string][]byte{}, }, }, "create_foo_two_literal_hash_configmap": { configMapName: "foo", fromLiteral: []string{"key1=value1", "key2=value2"}, appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-gcb75dd9gb", }, Data: map[string]string{ "key1": "value1", "key2": "value2", }, BinaryData: map[string][]byte{}, }, }, "create_foo_key1_=value1_configmap": { configMapName: "foo", fromLiteral: []string{"key1==value1"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string]string{ "key1": "=value1", }, BinaryData: map[string][]byte{}, }, }, "create_foo_key1_=value1_hash_configmap": { configMapName: "foo", fromLiteral: []string{"key1==value1"}, appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-bdgk9ttt7m", }, Data: map[string]string{ "key1": "=value1", }, BinaryData: map[string][]byte{}, }, }, "create_foo_from_file_foo1_foo2_configmap": { configMapName: "foo", setup: setupBinaryFile([]byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}), fromFile: []string{"foo1", "foo2"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string]string{ "foo1": "hello world", "foo2": "hello world", }, BinaryData: map[string][]byte{}, }, }, "create_foo_from_file_foo1_foo2_and_configmap": { configMapName: "foo", setup: setupBinaryFile([]byte{0xff, 0xfd}), fromFile: []string{"foo1", "foo2"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string]string{}, BinaryData: map[string][]byte{ "foo1": {0xff, 0xfd}, "foo2": {0xff, 0xfd}, }, }, }, "create_valid_env_from_env_file_configmap": { configMapName: "valid_env", setup: setupEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}}), fromEnvFile: []string{"file.env"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "valid_env", }, Data: map[string]string{ "key1": "value1", "key2": "value2", }, BinaryData: map[string][]byte{}, }, }, "create_two_valid_env_from_env_file_configmap": { configMapName: "two_valid_env", setup: setupEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}, {"key3=value3"}}), fromEnvFile: []string{"file1.env", "file2.env"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "two_valid_env", }, Data: map[string]string{ "key1": "value1", "key2": "value2", "key3": "value3", }, BinaryData: map[string][]byte{}, }, }, "create_valid_env_from_env_file_hash_configmap": { configMapName: "valid_env", setup: setupEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}}), fromEnvFile: []string{"file.env"}, appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "valid_env-2cgh8552ch", }, Data: map[string]string{ "key1": "value1", "key2": "value2", }, BinaryData: map[string][]byte{}, }, }, "create_two_valid_env_from_env_file_hash_configmap": { configMapName: "two_valid_env", setup: setupEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}, {"key3=value3"}}), fromEnvFile: []string{"file1.env", "file2.env"}, appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "two_valid_env-2m5tm82522", }, Data: map[string]string{ "key1": "value1", "key2": "value2", "key3": "value3", }, BinaryData: map[string][]byte{}, }, }, "create_get_env_from_env_file_configmap": { configMapName: "get_env", setup: func() func(t *testing.T, configMapOptions *ConfigMapOptions) func() { t.Setenv("g_key1", "1") t.Setenv("g_key2", "2") return setupEnvFile([][]string{{"g_key1", "g_key2="}}) }(), fromEnvFile: []string{"file.env"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "get_env", }, Data: map[string]string{ "g_key1": "1", "g_key2": "", }, BinaryData: map[string][]byte{}, }, }, "create_get_env_from_env_file_hash_configmap": { configMapName: "get_env", setup: func() func(t *testing.T, configMapOptions *ConfigMapOptions) func() { t.Setenv("g_key1", "1") t.Setenv("g_key2", "2") return setupEnvFile([][]string{{"g_key1", "g_key2="}}) }(), fromEnvFile: []string{"file.env"}, appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "get_env-54k882kkd2", }, Data: map[string]string{ "g_key1": "1", "g_key2": "", }, BinaryData: map[string][]byte{}, }, }, "create_value_with_space_from_env_file_configmap": { configMapName: "value_with_space", setup: setupEnvFile([][]string{{"key1= value1"}}), fromEnvFile: []string{"file.env"}, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "value_with_space", }, Data: map[string]string{ "key1": " value1", }, BinaryData: map[string][]byte{}, }, }, "create_value_with_space_from_env_file_hash_configmap": { configMapName: "valid_with_space", setup: setupEnvFile([][]string{{"key1= value1"}}), fromEnvFile: []string{"file.env"}, appendHash: true, expected: &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Name: "valid_with_space-b4448m7gdm", }, Data: map[string]string{ "key1": " value1", }, BinaryData: map[string][]byte{}, }, }, "create_invalid_configmap_filepath_contains_=": { configMapName: "foo", fromFile: []string{"key1=/file=2"}, expectErr: `key names or file paths cannot contain '='`, }, "create_invalid_configmap_filepath_key_contains_=": { configMapName: "foo", fromFile: []string{"=key=/file1"}, expectErr: `key names or file paths cannot contain '='`, }, "create_invalid_configmap_literal_key_contains_=": { configMapName: "foo", fromFile: []string{"=key=value1"}, expectErr: `key names or file paths cannot contain '='`, }, "create_invalid_configmap_duplicate_key1": { configMapName: "foo", fromLiteral: []string{"key1=value1", "key1=value2"}, expectErr: `cannot add key "key1", another key by that name already exists in Data for ConfigMap "foo"`, }, "create_invalid_configmap_no_file": { configMapName: "foo", fromFile: []string{"key1=/file1"}, expectErr: `error reading /file1: no such file or directory`, }, "create_invalid_configmap_invalid_literal": { configMapName: "foo", fromLiteral: []string{"key1value1"}, expectErr: `invalid literal source key1value1, expected key=value`, }, "create_invalid_configmap_invalid_filepath": { configMapName: "foo", fromFile: []string{"key1==file1"}, expectErr: `key names or file paths cannot contain '='`, }, "create_invalid_configmap_too_many_args": { configMapName: "too_many_args", fromFile: []string{"key1=/file1"}, fromEnvFile: []string{"file.env"}, expectErr: `from-env-file cannot be combined with from-file or from-literal`, }, "create_invalid_configmap_too_many_args_1": { configMapName: "too_many_args_1", fromLiteral: []string{"key1=value1"}, fromEnvFile: []string{"file.env"}, expectErr: `from-env-file cannot be combined with from-file or from-literal`, }, } // run all the tests for name, test := range tests { t.Run(name, func(t *testing.T) { var configMap *corev1.ConfigMap = nil configMapOptions := ConfigMapOptions{ Name: test.configMapName, Type: test.configMapType, AppendHash: test.appendHash, FileSources: test.fromFile, LiteralSources: test.fromLiteral, EnvFileSources: test.fromEnvFile, } if test.setup != nil { if teardown := test.setup(t, &configMapOptions); teardown != nil { defer teardown() } } err := configMapOptions.Validate() if err == nil { configMap, err = configMapOptions.createConfigMap() } if test.expectErr == "" { require.NoError(t, err) if !apiequality.Semantic.DeepEqual(configMap, test.expected) { t.Errorf("\nexpected:\n%#v\ngot:\n%#v", test.expected, configMap) } } else { require.Error(t, err) require.EqualError(t, err, test.expectErr) } }) } } func setupEnvFile(lines [][]string) func(*testing.T, *ConfigMapOptions) func() { return func(t *testing.T, configMapOptions *ConfigMapOptions) func() { files := []*os.File{} filenames := configMapOptions.EnvFileSources for _, filename := range filenames { file, err := os.CreateTemp("", filename) if err != nil { t.Errorf("unexpected error: %v", err) } files = append(files, file) } for i, f := range files { for _, l := range lines[i] { f.WriteString(l) f.WriteString("\r\n") } f.Close() configMapOptions.EnvFileSources[i] = f.Name() } return func() { for _, f := range files { os.Remove(f.Name()) } } } } func setupBinaryFile(data []byte) func(*testing.T, *ConfigMapOptions) func() { return func(t *testing.T, configMapOptions *ConfigMapOptions) func() { tmp, _ := os.MkdirTemp("", "") files := configMapOptions.FileSources for i, file := range files { f := tmp + "/" + file os.WriteFile(f, data, 0644) configMapOptions.FileSources[i] = f } return func() { for _, file := range files { f := tmp + "/" + file os.Remove(f) } } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_cronjob.go000066400000000000000000000145031504711711200313630ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "github.com/spf13/cobra" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" batchv1client "k8s.io/client-go/kubernetes/typed/batch/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( cronjobLong = templates.LongDesc(i18n.T(` Create a cron job with the specified name.`)) cronjobExample = templates.Examples(` # Create a cron job kubectl create cronjob my-job --image=busybox --schedule="*/1 * * * *" # Create a cron job with a command kubectl create cronjob my-job --image=busybox --schedule="*/1 * * * *" -- date`) ) // CreateCronJobOptions is returned by NewCreateCronJobOptions type CreateCronJobOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string Image string Schedule string Command []string Restart string Namespace string EnforceNamespace bool Client batchv1client.BatchV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string Builder *resource.Builder FieldManager string CreateAnnotation bool genericiooptions.IOStreams } // NewCreateCronJobOptions returns an initialized CreateCronJobOptions instance func NewCreateCronJobOptions(ioStreams genericiooptions.IOStreams) *CreateCronJobOptions { return &CreateCronJobOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateCronJob is a command to create CronJobs. func NewCmdCreateCronJob(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCreateCronJobOptions(ioStreams) cmd := &cobra.Command{ Use: "cronjob NAME --image=image --schedule='0/5 * * * ?' -- [COMMAND] [args...]", DisableFlagsInUseLine: false, Aliases: []string{"cj"}, Short: i18n.T("Create a cron job with the specified name"), Long: cronjobLong, Example: cronjobExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.Image, "image", o.Image, "Image name to run.") cmd.MarkFlagRequired("image") cmd.Flags().StringVar(&o.Schedule, "schedule", o.Schedule, "A schedule in the Cron format the job should be run with.") cmd.MarkFlagRequired("schedule") cmd.Flags().StringVar(&o.Restart, "restart", o.Restart, "job's restart policy. supported values: OnFailure, Never") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *CreateCronJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name if len(args) > 1 { o.Command = args[1:] } if len(o.Restart) == 0 { o.Restart = "OnFailure" } clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = batchv1client.NewForConfig(clientConfig) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.Builder = f.NewBuilder() o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Run performs the execution of 'create cronjob' sub command func (o *CreateCronJobOptions) Run() error { cronJob := o.createCronJob() if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, cronJob, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error cronJob, err = o.Client.CronJobs(o.Namespace).Create(context.TODO(), cronJob, createOptions) if err != nil { return fmt.Errorf("failed to create cronjob: %v", err) } } return o.PrintObj(cronJob) } func (o *CreateCronJobOptions) createCronJob() *batchv1.CronJob { cronjob := &batchv1.CronJob{ TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, }, Spec: batchv1.CronJobSpec{ Schedule: o.Schedule, JobTemplate: batchv1.JobTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: o.Name, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: o.Name, Image: o.Image, Command: o.Command, }, }, RestartPolicy: corev1.RestartPolicy(o.Restart), }, }, }, }, }, } if o.EnforceNamespace { cronjob.Namespace = o.Namespace } return cronjob } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_cronjob_test.go000066400000000000000000000061111504711711200324160ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "testing" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateCronJob(t *testing.T) { cronjobName := "test-job" tests := map[string]struct { image string command []string schedule string restart string expected *batchv1.CronJob }{ "just image and OnFailure restart policy": { image: "busybox", schedule: "0/5 * * * ?", restart: "OnFailure", expected: &batchv1.CronJob{ TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob"}, ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, Spec: batchv1.CronJobSpec{ Schedule: "0/5 * * * ?", JobTemplate: batchv1.JobTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: cronjobName, Image: "busybox", }, }, RestartPolicy: corev1.RestartPolicyOnFailure, }, }, }, }, }, }, }, "image, command , schedule and Never restart policy": { image: "busybox", command: []string{"date"}, schedule: "0/5 * * * ?", restart: "Never", expected: &batchv1.CronJob{ TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob"}, ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, Spec: batchv1.CronJobSpec{ Schedule: "0/5 * * * ?", JobTemplate: batchv1.JobTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: cronjobName, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: cronjobName, Image: "busybox", Command: []string{"date"}, }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, }, }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &CreateCronJobOptions{ Name: cronjobName, Image: tc.image, Command: tc.command, Schedule: tc.schedule, Restart: tc.restart, } cronjob := o.createCronJob() if !apiequality.Semantic.DeepEqual(cronjob, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, cronjob) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_deployment.go000066400000000000000000000206141504711711200321070ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "strings" "github.com/spf13/cobra" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" appsv1client "k8s.io/client-go/kubernetes/typed/apps/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( deploymentLong = templates.LongDesc(i18n.T(` Create a deployment with the specified name.`)) deploymentExample = templates.Examples(i18n.T(` # Create a deployment named my-dep that runs the busybox image kubectl create deployment my-dep --image=busybox # Create a deployment with a command kubectl create deployment my-dep --image=busybox -- date # Create a deployment named my-dep that runs the nginx image with 3 replicas kubectl create deployment my-dep --image=nginx --replicas=3 # Create a deployment named my-dep that runs the busybox image and expose port 5701 kubectl create deployment my-dep --image=busybox --port=5701 # Create a deployment named my-dep that runs multiple containers kubectl create deployment my-dep --image=busybox:latest --image=ubuntu:latest --image=nginx`)) ) // CreateDeploymentOptions is returned by NewCmdCreateDeployment type CreateDeploymentOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string Images []string Port int32 Replicas int32 Command []string Namespace string EnforceNamespace bool FieldManager string CreateAnnotation bool Client appsv1client.AppsV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewCreateDeploymentOptions returns an initialized CreateDeploymentOptions instance func NewCreateDeploymentOptions(ioStreams genericiooptions.IOStreams) *CreateDeploymentOptions { return &CreateDeploymentOptions{ Port: -1, Replicas: 1, PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateDeployment is a macro command to create a new deployment. // This command is better known to users as `kubectl create deployment`. func NewCmdCreateDeployment(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCreateDeploymentOptions(ioStreams) cmd := &cobra.Command{ Use: "deployment NAME --image=image -- [COMMAND] [args...]", DisableFlagsInUseLine: true, Aliases: []string{"deploy"}, Short: i18n.T("Create a deployment with the specified name"), Long: deploymentLong, Example: deploymentExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringSliceVar(&o.Images, "image", o.Images, "Image names to run. A deployment can have multiple images set for multi-container pod.") cmd.MarkFlagRequired("image") cmd.Flags().Int32Var(&o.Port, "port", o.Port, "The containerPort that this deployment exposes.") cmd.Flags().Int32VarP(&o.Replicas, "replicas", "r", o.Replicas, "Number of replicas to create. Default is 1.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the options func (o *CreateDeploymentOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name if len(args) > 1 { o.Command = args[1:] } clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = appsv1client.NewForConfig(clientConfig) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate makes sure there is no discrepency in provided option values func (o *CreateDeploymentOptions) Validate() error { if len(o.Images) > 1 && len(o.Command) > 0 { return fmt.Errorf("cannot specify multiple --image options and command") } return nil } // Run performs the execution of 'create deployment' sub command func (o *CreateDeploymentOptions) Run() error { deploy := o.createDeployment() if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, deploy, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error deploy, err = o.Client.Deployments(o.Namespace).Create(context.TODO(), deploy, createOptions) if err != nil { return fmt.Errorf("failed to create deployment: %v", err) } } return o.PrintObj(deploy) } func (o *CreateDeploymentOptions) createDeployment() *appsv1.Deployment { labels := map[string]string{"app": o.Name} selector := metav1.LabelSelector{MatchLabels: labels} namespace := "" if o.EnforceNamespace { namespace = o.Namespace } deploy := &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String(), Kind: "Deployment"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Labels: labels, Namespace: namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: &o.Replicas, Selector: &selector, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, }, Spec: o.buildPodSpec(), }, }, } if o.Port >= 0 && len(deploy.Spec.Template.Spec.Containers) > 0 { deploy.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{{ContainerPort: o.Port}} } return deploy } // buildPodSpec parses the image strings and assemble them into the Containers // of a PodSpec. This is all you need to create the PodSpec for a deployment. func (o *CreateDeploymentOptions) buildPodSpec() corev1.PodSpec { podSpec := corev1.PodSpec{Containers: []corev1.Container{}} for _, imageString := range o.Images { // Retain just the image name imageSplit := strings.Split(imageString, "/") name := imageSplit[len(imageSplit)-1] // Remove any tag or hash if strings.Contains(name, ":") { name = strings.Split(name, ":")[0] } if strings.Contains(name, "@") { name = strings.Split(name, "@")[0] } name = sanitizeAndUniquify(name) podSpec.Containers = append(podSpec.Containers, corev1.Container{ Name: name, Image: imageString, Command: o.Command, }) } return podSpec } // sanitizeAndUniquify replaces characters like "." or "_" into "-" to follow DNS1123 rules. // Then add random suffix to make it uniquified. func sanitizeAndUniquify(name string) string { if strings.ContainsAny(name, "_.") { name = strings.Replace(name, "_", "-", -1) name = strings.Replace(name, ".", "-", -1) name = fmt.Sprintf("%s-%s", name, utilrand.String(5)) } return name } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_deployment_test.go000066400000000000000000000143651504711711200331540ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "bytes" "io" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" ) func TestCreateDeployment(t *testing.T) { depName := "jonny-dep" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() fakeDiscovery := "{\"kind\":\"APIResourceList\",\"apiVersion\":\"v1\",\"groupVersion\":\"apps/v1\",\"resources\":[{\"name\":\"deployments\",\"singularName\":\"\",\"namespaced\":true,\"kind\":\"Deployment\",\"verbs\":[\"create\",\"delete\",\"deletecollection\",\"get\",\"list\",\"patch\",\"update\",\"watch\"],\"shortNames\":[\"deploy\"],\"categories\":[\"all\"]}]}" tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer([]byte(fakeDiscovery))), }, nil }), } tf.ClientConfigVal = &restclient.Config{} ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateDeployment(tf, ioStreams) cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", "name") cmd.Flags().Set("image", "hollywood/jonny.depp:v2") cmd.Run(cmd, []string{depName}) expectedOutput := "deployment.apps/" + depName + "\n" if buf.String() != expectedOutput { t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) } } func TestCreateDeploymentWithPort(t *testing.T) { depName := "jonny-dep" port := "5701" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() fakeDiscovery := "{\"kind\":\"APIResourceList\",\"apiVersion\":\"v1\",\"groupVersion\":\"apps/v1\",\"resources\":[{\"name\":\"deployments\",\"singularName\":\"\",\"namespaced\":true,\"kind\":\"Deployment\",\"verbs\":[\"create\",\"delete\",\"deletecollection\",\"get\",\"list\",\"patch\",\"update\",\"watch\"],\"shortNames\":[\"deploy\"],\"categories\":[\"all\"]}]}" tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer([]byte(fakeDiscovery))), }, nil }), } tf.ClientConfigVal = &restclient.Config{} ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateDeployment(tf, ioStreams) cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", "yaml") cmd.Flags().Set("port", port) cmd.Flags().Set("image", "hollywood/jonny.depp:v2") cmd.Run(cmd, []string{depName}) if !strings.Contains(buf.String(), port) { t.Errorf("unexpected output: %s\nexpected to contain: %s", buf.String(), port) } } func TestCreateDeploymentWithReplicas(t *testing.T) { depName := "jonny-dep" replicas := "3" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() fakeDiscovery := "{\"kind\":\"APIResourceList\",\"apiVersion\":\"v1\",\"groupVersion\":\"apps/v1\",\"resources\":[{\"name\":\"deployments\",\"singularName\":\"\",\"namespaced\":true,\"kind\":\"Deployment\",\"verbs\":[\"create\",\"delete\",\"deletecollection\",\"get\",\"list\",\"patch\",\"update\",\"watch\"],\"shortNames\":[\"deploy\"],\"categories\":[\"all\"]}]}" tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer([]byte(fakeDiscovery))), }, nil }), } tf.ClientConfigVal = &restclient.Config{} ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateDeployment(tf, ioStreams) cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", "jsonpath={.spec.replicas}") cmd.Flags().Set("replicas", replicas) cmd.Flags().Set("image", "hollywood/jonny.depp:v2") cmd.Run(cmd, []string{depName}) if buf.String() != replicas { t.Errorf("expected output: %s, but got: %s", replicas, buf.String()) } } func TestCreateDeploymentNoImage(t *testing.T) { depName := "jonny-dep" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() fakeDiscovery := "{\"kind\":\"APIResourceList\",\"apiVersion\":\"v1\",\"groupVersion\":\"apps/v1\",\"resources\":[{\"name\":\"deployments\",\"singularName\":\"\",\"namespaced\":true,\"kind\":\"Deployment\",\"verbs\":[\"create\",\"delete\",\"deletecollection\",\"get\",\"list\",\"patch\",\"update\",\"watch\"],\"shortNames\":[\"deploy\"],\"categories\":[\"all\"]}]}" tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer([]byte(fakeDiscovery))), }, nil }), } tf.ClientConfigVal = &restclient.Config{} ioStreams := genericiooptions.NewTestIOStreamsDiscard() cmd := NewCmdCreateDeployment(tf, ioStreams) cmd.Flags().Set("output", "name") options := &CreateDeploymentOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), DryRunStrategy: cmdutil.DryRunClient, IOStreams: ioStreams, } err := options.Complete(tf, cmd, []string{depName}) if err != nil { t.Fatalf("unexpected error: %v", err) } err = options.Run() assert.Error(t, err, "at least one image must be specified") } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress.go000066400000000000000000000341211504711711200313770ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "regexp" "strings" "github.com/spf13/cobra" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" networkingv1client "k8s.io/client-go/kubernetes/typed/networking/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( // Explaining the Regex below: // ^(?P[\w\*\-\.]*) -> Indicates the host - 0-N characters of letters, number, underscore, '-', '.' and '*' // (?P/.*) -> Indicates the path and MUST start with '/' - / + 0-N characters // Separator from host/path to svcname:svcport -> "=" // (?P[\w\-]+) -> Service Name (letters, numbers, '-') -> 1-N characters // Separator from svcname to svcport -> ":" // (?P[\w\-]+) -> Service Port (letters, numbers, '-') -> 1-N characters regexHostPathSvc = `^(?P[\w\*\-\.]*)(?P/.*)=(?P[\w\-]+):(?P[\w\-]+)` // This Regex is optional -> (....)? // (?Ptls) -> Verify if the argument after "," is 'tls' // Optional Separator from tls to the secret name -> "=?" // (?P[\w\-]+)? -> Optional secret name after the separator -> 1-N characters regexTLS = `(,(?Ptls)=?(?P[\w\-]+)?)?` // The validation Regex is the concatenation of hostPathSvc validation regex // and the TLS validation regex ruleRegex = regexHostPathSvc + regexTLS ingressLong = templates.LongDesc(i18n.T(` Create an ingress with the specified name.`)) ingressExample = templates.Examples(i18n.T(` # Create a single ingress called 'simple' that directs requests to foo.com/bar to svc # svc1:8080 with a TLS secret "my-cert" kubectl create ingress simple --rule="foo.com/bar=svc1:8080,tls=my-cert" # Create a catch all ingress of "/path" pointing to service svc:port and Ingress Class as "otheringress" kubectl create ingress catch-all --class=otheringress --rule="/path=svc:port" # Create an ingress with two annotations: ingress.annotation1 and ingress.annotations2 kubectl create ingress annotated --class=default --rule="foo.com/bar=svc:port" \ --annotation ingress.annotation1=foo \ --annotation ingress.annotation2=bla # Create an ingress with the same host and multiple paths kubectl create ingress multipath --class=default \ --rule="foo.com/=svc:port" \ --rule="foo.com/admin/=svcadmin:portadmin" # Create an ingress with multiple hosts and the pathType as Prefix kubectl create ingress ingress1 --class=default \ --rule="foo.com/path*=svc:8080" \ --rule="bar.com/admin*=svc2:http" # Create an ingress with TLS enabled using the default ingress certificate and different path types kubectl create ingress ingtls --class=default \ --rule="foo.com/=svc:https,tls" \ --rule="foo.com/path/subpath*=othersvc:8080" # Create an ingress with TLS enabled using a specific secret and pathType as Prefix kubectl create ingress ingsecret --class=default \ --rule="foo.com/*=svc:8080,tls=secret1" # Create an ingress with a default backend kubectl create ingress ingdefault --class=default \ --default-backend=defaultsvc:http \ --rule="foo.com/*=svc:8080,tls=secret1" `)) ) // CreateIngressOptions is returned by NewCmdCreateIngress type CreateIngressOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string IngressClass string Rules []string Annotations []string DefaultBackend string Namespace string EnforceNamespace bool CreateAnnotation bool Client networkingv1client.NetworkingV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string FieldManager string genericiooptions.IOStreams } // NewCreateIngressOptions creates the CreateIngressOptions to be used later func NewCreateIngressOptions(ioStreams genericiooptions.IOStreams) *CreateIngressOptions { return &CreateIngressOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateIngress is a macro command to create a new ingress. // This command is better known to users as `kubectl create ingress`. func NewCmdCreateIngress(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCreateIngressOptions(ioStreams) cmd := &cobra.Command{ Use: "ingress NAME --rule=host/path=service:port[,tls[=secret]] ", DisableFlagsInUseLine: true, Aliases: []string{"ing"}, Short: i18n.T("Create an ingress with the specified name"), Long: ingressLong, Example: ingressExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.IngressClass, "class", o.IngressClass, "Ingress Class to be used") cmd.Flags().StringArrayVar(&o.Rules, "rule", o.Rules, "Rule in format host/path=service:port[,tls=secretname]. Paths containing the leading character '*' are considered pathType=Prefix. tls argument is optional.") cmd.Flags().StringVar(&o.DefaultBackend, "default-backend", o.DefaultBackend, "Default service for backend, in format of svcname:port") cmd.Flags().StringArrayVar(&o.Annotations, "annotation", o.Annotations, "Annotation to insert in the ingress object, in the format annotation=value") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the options func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = networkingv1client.NewForConfig(clientConfig) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) return err } // Validate validates the Ingress object to be created func (o *CreateIngressOptions) Validate() error { if len(o.DefaultBackend) == 0 && len(o.Rules) == 0 { return fmt.Errorf("not enough information provided: every ingress has to either specify a default-backend (which catches all traffic) or a list of rules (which catch specific paths)") } rulevalidation, err := regexp.Compile(ruleRegex) if err != nil { return fmt.Errorf("failed to compile the regex") } for _, rule := range o.Rules { if match := rulevalidation.MatchString(rule); !match { return fmt.Errorf("rule %s is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", rule) } } for _, annotation := range o.Annotations { if an := strings.SplitN(annotation, "=", 2); len(an) != 2 { return fmt.Errorf("annotation %s is invalid and should be in format key=[value]", annotation) } } if len(o.DefaultBackend) > 0 && len(strings.Split(o.DefaultBackend, ":")) != 2 { return fmt.Errorf("default-backend should be in format servicename:serviceport") } return nil } // Run performs the execution of 'create ingress' sub command func (o *CreateIngressOptions) Run() error { ingress := o.createIngress() if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, ingress, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error ingress, err = o.Client.Ingresses(o.Namespace).Create(context.TODO(), ingress, createOptions) if err != nil { return fmt.Errorf("failed to create ingress: %v", err) } } return o.PrintObj(ingress) } func (o *CreateIngressOptions) createIngress() *networkingv1.Ingress { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } annotations := o.buildAnnotations() spec := o.buildIngressSpec() ingress := &networkingv1.Ingress{ TypeMeta: metav1.TypeMeta{APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Namespace: namespace, Annotations: annotations, }, Spec: spec, } return ingress } func (o *CreateIngressOptions) buildAnnotations() map[string]string { var annotations = make(map[string]string) for _, annotation := range o.Annotations { an := strings.SplitN(annotation, "=", 2) annotations[an[0]] = an[1] } return annotations } // buildIngressSpec builds the .spec from the diverse arguments passed to kubectl func (o *CreateIngressOptions) buildIngressSpec() networkingv1.IngressSpec { var ingressSpec networkingv1.IngressSpec if len(o.IngressClass) > 0 { ingressSpec.IngressClassName = &o.IngressClass } if len(o.DefaultBackend) > 0 { defaultbackend := buildIngressBackendSvc(o.DefaultBackend) ingressSpec.DefaultBackend = &defaultbackend } ingressSpec.TLS = o.buildTLSRules() ingressSpec.Rules = o.buildIngressRules() return ingressSpec } func (o *CreateIngressOptions) buildTLSRules() []networkingv1.IngressTLS { hostAlreadyPresent := make(map[string]struct{}) ingressTLSs := []networkingv1.IngressTLS{} var secret string for _, rule := range o.Rules { tls := strings.Split(rule, ",") if len(tls) == 2 { ingressTLS := networkingv1.IngressTLS{} host := strings.SplitN(rule, "/", 2)[0] secret = "" secretName := strings.Split(tls[1], "=") if len(secretName) > 1 { secret = secretName[1] } idxSecret := getIndexSecret(secret, ingressTLSs) // We accept the same host into TLS secrets only once if _, ok := hostAlreadyPresent[host]; !ok { if idxSecret > -1 { ingressTLSs[idxSecret].Hosts = append(ingressTLSs[idxSecret].Hosts, host) hostAlreadyPresent[host] = struct{}{} continue } if host != "" { ingressTLS.Hosts = append(ingressTLS.Hosts, host) } if secret != "" { ingressTLS.SecretName = secret } if len(ingressTLS.SecretName) > 0 || len(ingressTLS.Hosts) > 0 { ingressTLSs = append(ingressTLSs, ingressTLS) } hostAlreadyPresent[host] = struct{}{} } } } return ingressTLSs } // buildIngressRules builds the .spec.rules for an ingress object. func (o *CreateIngressOptions) buildIngressRules() []networkingv1.IngressRule { ingressRules := []networkingv1.IngressRule{} for _, rule := range o.Rules { removeTLS := strings.Split(rule, ",")[0] hostSplit := strings.SplitN(removeTLS, "/", 2) host := hostSplit[0] ingressPath := buildHTTPIngressPath(hostSplit[1]) ingressRule := networkingv1.IngressRule{} if host != "" { ingressRule.Host = host } idxHost := getIndexHost(ingressRule.Host, ingressRules) if idxHost > -1 { ingressRules[idxHost].IngressRuleValue.HTTP.Paths = append(ingressRules[idxHost].IngressRuleValue.HTTP.Paths, ingressPath) continue } ingressRule.IngressRuleValue = networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ ingressPath, }, }, } ingressRules = append(ingressRules, ingressRule) } return ingressRules } func buildHTTPIngressPath(pathsvc string) networkingv1.HTTPIngressPath { pathsvcsplit := strings.Split(pathsvc, "=") path := "/" + pathsvcsplit[0] service := pathsvcsplit[1] var pathType networkingv1.PathType pathType = "Exact" // If * in the End, turn pathType=Prefix but remove the * from the end if path[len(path)-1:] == "*" { pathType = "Prefix" path = path[0 : len(path)-1] } httpIngressPath := networkingv1.HTTPIngressPath{ Path: path, PathType: &pathType, Backend: buildIngressBackendSvc(service), } return httpIngressPath } func buildIngressBackendSvc(service string) networkingv1.IngressBackend { svcname := strings.Split(service, ":")[0] svcport := strings.Split(service, ":")[1] ingressBackend := networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: svcname, Port: parseServiceBackendPort(svcport), }, } return ingressBackend } func parseServiceBackendPort(port string) networkingv1.ServiceBackendPort { var backendPort networkingv1.ServiceBackendPort portIntOrStr := intstr.Parse(port) if portIntOrStr.Type == intstr.Int { backendPort.Number = portIntOrStr.IntVal } if portIntOrStr.Type == intstr.String { backendPort.Name = portIntOrStr.StrVal } return backendPort } func getIndexHost(host string, rules []networkingv1.IngressRule) int { for index, v := range rules { if v.Host == host { return index } } return -1 } func getIndexSecret(secretname string, tls []networkingv1.IngressTLS) int { for index, v := range tls { if v.SecretName == secretname { return index } } return -1 } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress_test.go000066400000000000000000000336741504711711200324520ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "testing" networkingv1 "k8s.io/api/networking/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateIngressValidation(t *testing.T) { tests := map[string]struct { defaultbackend string ingressclass string rules []string annotations []string expected string }{ "no default backend and rule": { defaultbackend: "", rules: []string{}, expected: "not enough information provided: every ingress has to either specify a default-backend (which catches all traffic) or a list of rules (which catch specific paths)", }, "invalid default backend separator": { defaultbackend: "xpto,4444", expected: "default-backend should be in format servicename:serviceport", }, "default backend without port": { defaultbackend: "xpto", expected: "default-backend should be in format servicename:serviceport", }, "default backend is ok": { defaultbackend: "xpto:4444", expected: "", }, "invalid annotation": { defaultbackend: "xpto:4444", annotations: []string{ "key1=value1", "key2", }, expected: "annotation key2 is invalid and should be in format key=[value]", }, "valid annotations": { defaultbackend: "xpto:4444", annotations: []string{ "key1=value1", "key2=", }, expected: "", }, "multiple conformant rules": { rules: []string{ "foo.com/path*=svc:8080", "bar.com/admin*=svc2:http", }, expected: "", }, "one invalid and two valid rules": { rules: []string{ "foo.com=svc:redis,tls", "foo.com/path/subpath*=othersvc:8080", "foo.com/*=svc:8080,tls=secret1", }, expected: "rule foo.com=svc:redis,tls is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", }, "service without port": { rules: []string{ "foo.com/=svc,tls", }, expected: "rule foo.com/=svc,tls is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", }, "valid tls rule without secret": { rules: []string{ "foo.com/=svc:http,tls=", }, expected: "", }, "valid tls rule with secret": { rules: []string{ "foo.com/=svc:http,tls=secret123", }, expected: "", }, "valid path with type prefix": { rules: []string{ "foo.com/admin*=svc:8080", }, expected: "", }, "wildcard host": { rules: []string{ "*.foo.com/admin*=svc:8080", }, expected: "", }, "invalid separation between ingress and service": { rules: []string{ "*.foo.com/path,svc:8080", }, expected: "rule *.foo.com/path,svc:8080 is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", }, "two invalid and one valid rule": { rules: []string{ "foo.com/path/subpath*=svc:redis,tls=blo", "foo.com=othersvc:8080", "foo.com/admin=svc,tls=secret1", }, expected: "rule foo.com=othersvc:8080 is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", }, "valid catch all rule": { rules: []string{ "/path/subpath*=svc:redis,tls=blo", }, expected: "", }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &CreateIngressOptions{ DefaultBackend: tc.defaultbackend, Rules: tc.rules, IngressClass: tc.ingressclass, Annotations: tc.annotations, } err := o.Validate() if err != nil && err.Error() != tc.expected { t.Errorf("unexpected error: %v", err) } if tc.expected != "" && err == nil { t.Errorf("expected error, got no error") } }) } } func TestCreateIngress(t *testing.T) { ingressName := "test-ingress" ingressClass := "nginx" pathTypeExact := networkingv1.PathTypeExact pathTypePrefix := networkingv1.PathTypePrefix tests := map[string]struct { defaultbackend string rules []string ingressclass string annotations []string expected *networkingv1.Ingress }{ "catch all host and default backend with default TLS returns empty TLS": { rules: []string{ "/=catchall:8080,tls=", }, ingressclass: ingressClass, defaultbackend: "service1:https", annotations: []string{}, expected: &networkingv1.Ingress{ TypeMeta: metav1.TypeMeta{ APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressName, Annotations: map[string]string{}, }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingressClass, DefaultBackend: &networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "service1", Port: networkingv1.ServiceBackendPort{ Name: "https", }, }, }, TLS: []networkingv1.IngressTLS{}, Rules: []networkingv1.IngressRule{ { Host: "", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", PathType: &pathTypeExact, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "catchall", Port: networkingv1.ServiceBackendPort{ Number: 8080, }, }, }, }, }, }, }, }, }, }, }, }, "catch all with path of type prefix and secret name": { rules: []string{ "/path*=catchall:8080,tls=secret1", }, ingressclass: ingressClass, defaultbackend: "service1:https", annotations: []string{}, expected: &networkingv1.Ingress{ TypeMeta: metav1.TypeMeta{ APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressName, Annotations: map[string]string{}, }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingressClass, DefaultBackend: &networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "service1", Port: networkingv1.ServiceBackendPort{ Name: "https", }, }, }, TLS: []networkingv1.IngressTLS{ { SecretName: "secret1", }, }, Rules: []networkingv1.IngressRule{ { Host: "", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/path", PathType: &pathTypePrefix, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "catchall", Port: networkingv1.ServiceBackendPort{ Number: 8080, }, }, }, }, }, }, }, }, }, }, }, }, "mixed hosts with mixed TLS configuration and a default backend": { rules: []string{ "foo.com/=foo-svc:8080,tls=", "foo.com/admin=foo-admin-svc:http,tls=", "bar.com/prefix*=bar-svc:8080,tls=bar-secret", "bar.com/noprefix=barnp-svc:8443,tls", "foobar.com/*=foobar-svc:https", "foobar1.com/*=foobar1-svc:https,tls=bar-secret", }, defaultbackend: "service2:8080", annotations: []string{}, expected: &networkingv1.Ingress{ TypeMeta: metav1.TypeMeta{ APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressName, Annotations: map[string]string{}, }, Spec: networkingv1.IngressSpec{ DefaultBackend: &networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "service2", Port: networkingv1.ServiceBackendPort{ Number: 8080, }, }, }, TLS: []networkingv1.IngressTLS{ { Hosts: []string{ "foo.com", }, }, { Hosts: []string{ "bar.com", "foobar1.com", }, SecretName: "bar-secret", }, }, Rules: []networkingv1.IngressRule{ { Host: "foo.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", PathType: &pathTypeExact, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "foo-svc", Port: networkingv1.ServiceBackendPort{ Number: 8080, }, }, }, }, { Path: "/admin", PathType: &pathTypeExact, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "foo-admin-svc", Port: networkingv1.ServiceBackendPort{ Name: "http", }, }, }, }, }, }, }, }, { Host: "bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/prefix", PathType: &pathTypePrefix, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "bar-svc", Port: networkingv1.ServiceBackendPort{ Number: 8080, }, }, }, }, { Path: "/noprefix", PathType: &pathTypeExact, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "barnp-svc", Port: networkingv1.ServiceBackendPort{ Number: 8443, }, }, }, }, }, }, }, }, { Host: "foobar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", PathType: &pathTypePrefix, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "foobar-svc", Port: networkingv1.ServiceBackendPort{ Name: "https", }, }, }, }, }, }, }, }, { Host: "foobar1.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", PathType: &pathTypePrefix, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "foobar1-svc", Port: networkingv1.ServiceBackendPort{ Name: "https", }, }, }, }, }, }, }, }, }, }, }, }, "simple ingress with annotation": { rules: []string{ "foo.com/=svc:8080,tls=secret1", "foo.com/subpath*=othersvc:8080,tls=secret1", }, annotations: []string{ "ingress.kubernetes.io/annotation1=bla", "ingress.kubernetes.io/annotation2=blo", "ingress.kubernetes.io/annotation3=ble", }, expected: &networkingv1.Ingress{ TypeMeta: metav1.TypeMeta{ APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressName, Annotations: map[string]string{ "ingress.kubernetes.io/annotation1": "bla", "ingress.kubernetes.io/annotation3": "ble", "ingress.kubernetes.io/annotation2": "blo", }, }, Spec: networkingv1.IngressSpec{ TLS: []networkingv1.IngressTLS{ { Hosts: []string{ "foo.com", }, SecretName: "secret1", }, }, Rules: []networkingv1.IngressRule{ { Host: "foo.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/", PathType: &pathTypeExact, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "svc", Port: networkingv1.ServiceBackendPort{ Number: 8080, }, }, }, }, { Path: "/subpath", PathType: &pathTypePrefix, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "othersvc", Port: networkingv1.ServiceBackendPort{ Number: 8080, }, }, }, }, }, }, }, }, }, }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &CreateIngressOptions{ Name: ingressName, IngressClass: tc.ingressclass, Annotations: tc.annotations, DefaultBackend: tc.defaultbackend, Rules: tc.rules, } ingress := o.createIngress() if !apiequality.Semantic.DeepEqual(ingress, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, ingress) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_job.go000066400000000000000000000177511504711711200305110ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "github.com/spf13/cobra" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" batchv1client "k8s.io/client-go/kubernetes/typed/batch/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "k8s.io/utils/ptr" ) var ( jobLong = templates.LongDesc(i18n.T(` Create a job with the specified name.`)) jobExample = templates.Examples(i18n.T(` # Create a job kubectl create job my-job --image=busybox # Create a job with a command kubectl create job my-job --image=busybox -- date # Create a job from a cron job named "a-cronjob" kubectl create job test-job --from=cronjob/a-cronjob`)) ) // CreateJobOptions is the command line options for 'create job' type CreateJobOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string Image string From string Command []string Namespace string EnforceNamespace bool Client batchv1client.BatchV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string Builder *resource.Builder FieldManager string CreateAnnotation bool genericiooptions.IOStreams } // NewCreateJobOptions initializes and returns new CreateJobOptions instance func NewCreateJobOptions(ioStreams genericiooptions.IOStreams) *CreateJobOptions { return &CreateJobOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateJob is a command to ease creating Jobs from CronJobs. func NewCmdCreateJob(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCreateJobOptions(ioStreams) cmd := &cobra.Command{ Use: "job NAME --image=image [--from=cronjob/name] -- [COMMAND] [args...]", DisableFlagsInUseLine: true, Short: i18n.T("Create a job with the specified name"), Long: jobLong, Example: jobExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.Image, "image", o.Image, "Image name to run.") cmd.Flags().StringVar(&o.From, "from", o.From, "The name of the resource to create a Job from (only cronjob is supported).") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *CreateJobOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name if len(args) > 1 { o.Command = args[1:] } clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = batchv1client.NewForConfig(clientConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.Builder = f.NewBuilder() o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate makes sure provided values and valid Job options func (o *CreateJobOptions) Validate() error { if (len(o.Image) == 0 && len(o.From) == 0) || (len(o.Image) != 0 && len(o.From) != 0) { return fmt.Errorf("either --image or --from must be specified") } if o.Command != nil && len(o.Command) != 0 && len(o.From) != 0 { return fmt.Errorf("cannot specify --from and command") } return nil } // Run performs the execution of 'create job' sub command func (o *CreateJobOptions) Run() error { var job *batchv1.Job if len(o.Image) > 0 { job = o.createJob() } else { infos, err := o.Builder. WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). ResourceTypeOrNameArgs(false, o.From). Flatten(). Latest(). Do(). Infos() if err != nil { return err } if len(infos) != 1 { return fmt.Errorf("from must be an existing cronjob") } switch obj := infos[0].Object.(type) { case *batchv1.CronJob: job = o.createJobFromCronJob(obj) default: return fmt.Errorf("unknown object type %T", obj) } } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, job, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error job, err = o.Client.Jobs(o.Namespace).Create(context.TODO(), job, createOptions) if err != nil { return fmt.Errorf("failed to create job: %v", err) } } return o.PrintObj(job) } func (o *CreateJobOptions) createJob() *batchv1.Job { job := &batchv1.Job{ // this is ok because we know exactly how we want to be serialized TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: o.Name, Image: o.Image, Command: o.Command, }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, } if o.EnforceNamespace { job.Namespace = o.Namespace } return job } func (o *CreateJobOptions) createJobFromCronJob(cronJob *batchv1.CronJob) *batchv1.Job { annotations := make(map[string]string) annotations["cronjob.kubernetes.io/instantiate"] = "manual" for k, v := range cronJob.Spec.JobTemplate.Annotations { annotations[k] = v } job := &batchv1.Job{ // this is ok because we know exactly how we want to be serialized TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Annotations: annotations, Labels: cronJob.Spec.JobTemplate.Labels, OwnerReferences: []metav1.OwnerReference{ { // we are not using metav1.NewControllerRef because it // sets BlockOwnerDeletion to true which additionally mandates // cronjobs/finalizer role and not backwards-compatible. APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob", Name: cronJob.GetName(), UID: cronJob.GetUID(), Controller: ptr.To(true), }, }, }, Spec: cronJob.Spec.JobTemplate.Spec, } if o.EnforceNamespace { job.Namespace = o.Namespace } return job } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_job_test.go000066400000000000000000000114551504711711200315430ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "strings" "testing" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) func TestCreateJobValidation(t *testing.T) { tests := map[string]struct { image string command []string from string expected string }{ "empty flags": { expected: "--image or --from must be specified", }, "both image and from specified": { image: "my-image", from: "cronjob/xyz", expected: "--image or --from must be specified", }, "from and command specified": { from: "cronjob/xyz", command: []string{"test", "command"}, expected: "cannot specify --from and command", }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &CreateJobOptions{ Image: tc.image, From: tc.from, Command: tc.command, } err := o.Validate() if err != nil && !strings.Contains(err.Error(), tc.expected) { t.Errorf("unexpected error: %v", err) } }) } } func TestCreateJob(t *testing.T) { jobName := "test-job" tests := map[string]struct { image string command []string expected *batchv1.Job }{ "just image": { image: "busybox", expected: &batchv1.Job{ TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"}, ObjectMeta: metav1.ObjectMeta{ Name: jobName, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: jobName, Image: "busybox", }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, }, }, "image and command": { image: "busybox", command: []string{"date"}, expected: &batchv1.Job{ TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"}, ObjectMeta: metav1.ObjectMeta{ Name: jobName, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: jobName, Image: "busybox", Command: []string{"date"}, }, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &CreateJobOptions{ Name: jobName, Image: tc.image, Command: tc.command, } job := o.createJob() if !apiequality.Semantic.DeepEqual(job, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, job) } }) } } func TestCreateJobFromCronJob(t *testing.T) { jobName := "test-job" cronJob := &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "test-image"}, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, }, }, } tests := map[string]struct { from *batchv1.CronJob expected *batchv1.Job }{ "from CronJob": { from: cronJob, expected: &batchv1.Job{ TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"}, ObjectMeta: metav1.ObjectMeta{ Name: jobName, Annotations: map[string]string{"cronjob.kubernetes.io/instantiate": "manual"}, OwnerReferences: []metav1.OwnerReference{ { APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "CronJob", Name: cronJob.GetName(), UID: cronJob.GetUID(), Controller: ptr.To(true), }, }, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "test-image"}, }, RestartPolicy: corev1.RestartPolicyNever, }, }, }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &CreateJobOptions{ Name: jobName, } job := o.createJobFromCronJob(tc.from) if !apiequality.Semantic.DeepEqual(job, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, job) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_namespace.go000066400000000000000000000121641504711711200316640ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" coreclient "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( namespaceLong = templates.LongDesc(i18n.T(` Create a namespace with the specified name.`)) namespaceExample = templates.Examples(i18n.T(` # Create a new namespace named my-namespace kubectl create namespace my-namespace`)) ) // NamespaceOptions is the options for 'create namespace' sub command type NamespaceOptions struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags // Name of resource being created Name string DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string CreateAnnotation bool FieldManager string Client *coreclient.CoreV1Client PrintObj func(obj runtime.Object) error genericiooptions.IOStreams } // NewNamespaceOptions creates a new *NamespaceOptions with sane defaults func NewNamespaceOptions(ioStreams genericiooptions.IOStreams) *NamespaceOptions { return &NamespaceOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateNamespace is a macro command to create a new namespace func NewCmdCreateNamespace(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewNamespaceOptions(ioStreams) cmd := &cobra.Command{ Use: "namespace NAME [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Aliases: []string{"ns"}, Short: i18n.T("Create a namespace with the specified name"), Long: namespaceLong, Example: namespaceExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *NamespaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = coreclient.NewForConfig(restConfig) if err != nil { return err } o.Name = name o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) return err } // Run calls the CreateSubcommandOptions.Run in NamespaceOpts instance func (o *NamespaceOptions) Run() error { namespace := o.createNamespace() if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, namespace, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error namespace, err = o.Client.Namespaces().Create(context.TODO(), namespace, createOptions) if err != nil { return err } } return o.PrintObj(namespace) } // createNamespace outputs a namespace object using the configured fields func (o *NamespaceOptions) createNamespace() *corev1.Namespace { namespace := &corev1.Namespace{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Namespace"}, ObjectMeta: metav1.ObjectMeta{Name: o.Name}, } return namespace } // Validate validates required fields are set to support structured generation func (o *NamespaceOptions) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_namespace_test.go000066400000000000000000000026131504711711200327210ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "testing" apiequality "k8s.io/apimachinery/pkg/api/equality" ) func TestCreateNamespace(t *testing.T) { tests := map[string]struct { options *NamespaceOptions expected *corev1.Namespace }{ "success_create": { options: &NamespaceOptions{ Name: "my-namespace", }, expected: &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-namespace", }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { namespace := tc.options.createNamespace() if !apiequality.Semantic.DeepEqual(namespace, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, namespace) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_pdb.go000066400000000000000000000202531504711711200304730ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "regexp" "github.com/spf13/cobra" policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" policyv1client "k8s.io/client-go/kubernetes/typed/policy/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( pdbLong = templates.LongDesc(i18n.T(` Create a pod disruption budget with the specified name, selector, and desired minimum available pods.`)) pdbExample = templates.Examples(i18n.T(` # Create a pod disruption budget named my-pdb that will select all pods with the app=rails label # and require at least one of them being available at any point in time kubectl create poddisruptionbudget my-pdb --selector=app=rails --min-available=1 # Create a pod disruption budget named my-pdb that will select all pods with the app=nginx label # and require at least half of the pods selected to be available at any point in time kubectl create pdb my-pdb --selector=app=nginx --min-available=50%`)) ) // PodDisruptionBudgetOpts holds the command-line options for poddisruptionbudget sub command type PodDisruptionBudgetOpts struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // Name of resource being created Name string MinAvailable string MaxUnavailable string // A label selector to use for this budget Selector string CreateAnnotation bool FieldManager string Namespace string EnforceNamespace bool Client *policyv1client.PolicyV1Client DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewPodDisruptionBudgetOpts creates a new *PodDisruptionBudgetOpts with sane defaults func NewPodDisruptionBudgetOpts(ioStreams genericiooptions.IOStreams) *PodDisruptionBudgetOpts { return &PodDisruptionBudgetOpts{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreatePodDisruptionBudget is a macro command to create a new pod disruption budget. func NewCmdCreatePodDisruptionBudget(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewPodDisruptionBudgetOpts(ioStreams) cmd := &cobra.Command{ Use: "poddisruptionbudget NAME --selector=SELECTOR --min-available=N [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Aliases: []string{"pdb"}, Short: i18n.T("Create a pod disruption budget with the specified name"), Long: pdbLong, Example: pdbExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.MinAvailable, "min-available", o.MinAvailable, i18n.T("The minimum number or percentage of available pods this budget requires.")) cmd.Flags().StringVar(&o.MaxUnavailable, "max-unavailable", o.MaxUnavailable, i18n.T("The maximum number or percentage of unavailable pods this budget requires.")) cmd.Flags().StringVar(&o.Selector, "selector", o.Selector, i18n.T("A label selector to use for this budget. Only equality-based selector requirements are supported.")) cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *PodDisruptionBudgetOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = policyv1client.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate checks to the PodDisruptionBudgetOpts to see if there is sufficient information run the command func (o *PodDisruptionBudgetOpts) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } if len(o.Selector) == 0 { return fmt.Errorf("a selector must be specified") } if len(o.MaxUnavailable) == 0 && len(o.MinAvailable) == 0 { return fmt.Errorf("one of min-available or max-unavailable must be specified") } if len(o.MaxUnavailable) > 0 && len(o.MinAvailable) > 0 { return fmt.Errorf("min-available and max-unavailable cannot be both specified") } // The following regex matches the following values: // 10, 20, 30%, 50% (number and percentage) // but not 10Gb, 20Mb re := regexp.MustCompile(`^[0-9]+%?$`) switch { case len(o.MinAvailable) > 0 && !re.MatchString(o.MinAvailable): return fmt.Errorf("invalid format specified for min-available") case len(o.MaxUnavailable) > 0 && !re.MatchString(o.MaxUnavailable): return fmt.Errorf("invalid format specified for max-unavailable") } return nil } // Run calls the CreateSubcommandOptions.Run in PodDisruptionBudgetOpts instance func (o *PodDisruptionBudgetOpts) Run() error { podDisruptionBudget, err := o.createPodDisruptionBudgets() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, podDisruptionBudget, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } podDisruptionBudget, err = o.Client.PodDisruptionBudgets(o.Namespace).Create(context.TODO(), podDisruptionBudget, createOptions) if err != nil { return fmt.Errorf("failed to create poddisruptionbudgets: %v", err) } } return o.PrintObj(podDisruptionBudget) } func (o *PodDisruptionBudgetOpts) createPodDisruptionBudgets() (*policyv1.PodDisruptionBudget, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } podDisruptionBudget := &policyv1.PodDisruptionBudget{ TypeMeta: metav1.TypeMeta{ APIVersion: policyv1.SchemeGroupVersion.String(), Kind: "PodDisruptionBudget", }, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Namespace: namespace, }, } selector, err := metav1.ParseToLabelSelector(o.Selector) if err != nil { return nil, err } podDisruptionBudget.Spec.Selector = selector switch { case len(o.MinAvailable) > 0: minAvailable := intstr.Parse(o.MinAvailable) podDisruptionBudget.Spec.MinAvailable = &minAvailable case len(o.MaxUnavailable) > 0: maxUnavailable := intstr.Parse(o.MaxUnavailable) podDisruptionBudget.Spec.MaxUnavailable = &maxUnavailable } return podDisruptionBudget, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_pdb_test.go000066400000000000000000000144411504711711200315340ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "testing" policyv1 "k8s.io/api/policy/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) func TestCreatePdbValidation(t *testing.T) { selectorOpts := "app=nginx" podAmountNumber := "3" podAmountPercent := "50%" tests := map[string]struct { options *PodDisruptionBudgetOpts expected string }{ "test-missing-name-param": { options: &PodDisruptionBudgetOpts{ Selector: selectorOpts, MinAvailable: podAmountNumber, }, expected: "name must be specified", }, "test-missing-selector-param": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", MinAvailable: podAmountNumber, }, expected: "a selector must be specified", }, "test-missing-max-unavailable-max-unavailable-param": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, }, expected: "one of min-available or max-unavailable must be specified", }, "test-both-min-available-max-unavailable-param": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MinAvailable: podAmountNumber, MaxUnavailable: podAmountPercent, }, expected: "min-available and max-unavailable cannot be both specified", }, "test-invalid-min-available-format": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MinAvailable: "10GB", }, expected: "invalid format specified for min-available", }, "test-invalid-max-unavailable-format": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MaxUnavailable: "10GB", }, expected: "invalid format specified for max-unavailable", }, "test-valid-min-available-format": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MaxUnavailable: podAmountNumber, }, expected: "", }, "test-valid-max-unavailable-format": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MaxUnavailable: podAmountPercent, }, expected: "", }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &PodDisruptionBudgetOpts{ Name: tc.options.Name, Selector: tc.options.Selector, MinAvailable: tc.options.MinAvailable, MaxUnavailable: tc.options.MaxUnavailable, } err := o.Validate() if err != nil && err.Error() != tc.expected { t.Errorf("unexpected error: %v", err) } if tc.expected != "" && err == nil { t.Errorf("expected error, got no error") } }) } } func TestCreatePdb(t *testing.T) { selectorOpts := "app=nginx" podAmountNumber := "3" podAmountPercent := "50%" selector, err := metav1.ParseToLabelSelector(selectorOpts) if err != nil { t.Errorf("unexpected error: %v", err) } minAvailableNumber := intstr.Parse(podAmountNumber) minAvailablePercent := intstr.Parse(podAmountPercent) maxUnavailableNumber := intstr.Parse(podAmountNumber) maxUnavailablePercent := intstr.Parse(podAmountPercent) tests := map[string]struct { options *PodDisruptionBudgetOpts expected *policyv1.PodDisruptionBudget }{ "test-valid-min-available-pods-number": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MinAvailable: podAmountNumber, }, expected: &policyv1.PodDisruptionBudget{ TypeMeta: metav1.TypeMeta{ Kind: "PodDisruptionBudget", APIVersion: "policy/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-pdb", }, Spec: policyv1.PodDisruptionBudgetSpec{ Selector: selector, MinAvailable: &minAvailableNumber, }, }, }, "test-valid-min-available-pods-percentage": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MinAvailable: podAmountPercent, }, expected: &policyv1.PodDisruptionBudget{ TypeMeta: metav1.TypeMeta{ Kind: "PodDisruptionBudget", APIVersion: "policy/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-pdb", }, Spec: policyv1.PodDisruptionBudgetSpec{ Selector: selector, MinAvailable: &minAvailablePercent, }, }, }, "test-valid-max-unavailable-pods-number": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MaxUnavailable: podAmountNumber, }, expected: &policyv1.PodDisruptionBudget{ TypeMeta: metav1.TypeMeta{ Kind: "PodDisruptionBudget", APIVersion: "policy/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-pdb", }, Spec: policyv1.PodDisruptionBudgetSpec{ Selector: selector, MaxUnavailable: &maxUnavailableNumber, }, }, }, "test-valid-max-unavailable-pods-percentage": { options: &PodDisruptionBudgetOpts{ Name: "my-pdb", Selector: selectorOpts, MaxUnavailable: podAmountPercent, }, expected: &policyv1.PodDisruptionBudget{ TypeMeta: metav1.TypeMeta{ Kind: "PodDisruptionBudget", APIVersion: "policy/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-pdb", }, Spec: policyv1.PodDisruptionBudgetSpec{ Selector: selector, MaxUnavailable: &maxUnavailablePercent, }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { podDisruptionBudget, err := tc.options.createPodDisruptionBudgets() if err != nil { t.Errorf("unexpected error:\n%#v\n", err) return } if !apiequality.Semantic.DeepEqual(podDisruptionBudget, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, podDisruptionBudget) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_priorityclass.go000066400000000000000000000152141504711711200326360ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" schedulingv1 "k8s.io/api/scheduling/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" schedulingv1client "k8s.io/client-go/kubernetes/typed/scheduling/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( pcLong = templates.LongDesc(i18n.T(` Create a priority class with the specified name, value, globalDefault and description.`)) pcExample = templates.Examples(i18n.T(` # Create a priority class named high-priority kubectl create priorityclass high-priority --value=1000 --description="high priority" # Create a priority class named default-priority that is considered as the global default priority kubectl create priorityclass default-priority --value=1000 --global-default=true --description="default priority" # Create a priority class named high-priority that cannot preempt pods with lower priority kubectl create priorityclass high-priority --value=1000 --description="high priority" --preemption-policy="Never"`)) ) // PriorityClassOptions holds the options for 'create priorityclass' sub command type PriorityClassOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string Value int32 GlobalDefault bool Description string PreemptionPolicy string FieldManager string CreateAnnotation bool Client *schedulingv1client.SchedulingV1Client DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewPriorityClassOptions returns an initialized PriorityClassOptions instance func NewPriorityClassOptions(ioStreams genericiooptions.IOStreams) *PriorityClassOptions { return &PriorityClassOptions{ Value: 0, PreemptionPolicy: "PreemptLowerPriority", PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreatePriorityClass is a macro command to create a new priorityClass. func NewCmdCreatePriorityClass(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewPriorityClassOptions(ioStreams) cmd := &cobra.Command{ Use: "priorityclass NAME --value=VALUE --global-default=BOOL [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Aliases: []string{"pc"}, Short: i18n.T("Create a priority class with the specified name"), Long: pcLong, Example: pcExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().Int32Var(&o.Value, "value", o.Value, i18n.T("the value of this priority class.")) cmd.Flags().BoolVar(&o.GlobalDefault, "global-default", o.GlobalDefault, i18n.T("global-default specifies whether this PriorityClass should be considered as the default priority.")) cmd.Flags().StringVar(&o.Description, "description", o.Description, i18n.T("description is an arbitrary string that usually provides guidelines on when this priority class should be used.")) cmd.Flags().StringVar(&o.PreemptionPolicy, "preemption-policy", o.PreemptionPolicy, i18n.T("preemption-policy is the policy for preempting pods with lower priority.")) cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *PriorityClassOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = schedulingv1client.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Run calls the CreateSubcommandOptions.Run in the PriorityClassOptions instance func (o *PriorityClassOptions) Run() error { priorityClass, err := o.createPriorityClass() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, priorityClass, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error priorityClass, err = o.Client.PriorityClasses().Create(context.TODO(), priorityClass, createOptions) if err != nil { return fmt.Errorf("failed to create priorityclass: %v", err) } } return o.PrintObj(priorityClass) } func (o *PriorityClassOptions) createPriorityClass() (*schedulingv1.PriorityClass, error) { preemptionPolicy := corev1.PreemptionPolicy(o.PreemptionPolicy) return &schedulingv1.PriorityClass{ // this is ok because we know exactly how we want to be serialized TypeMeta: metav1.TypeMeta{APIVersion: schedulingv1.SchemeGroupVersion.String(), Kind: "PriorityClass"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, }, Value: o.Value, GlobalDefault: o.GlobalDefault, Description: o.Description, PreemptionPolicy: &preemptionPolicy, }, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_priorityclass_test.go000066400000000000000000000046611504711711200337010ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "bytes" "io" "net/http" "testing" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestCreatePriorityClass(t *testing.T) { pcName := "my-pc" tf := cmdtesting.NewTestFactory() defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "scheduling.k8s.io", Version: "v1beta1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(&bytes.Buffer{}), }, nil }), } tf.ClientConfigVal = &restclient.Config{} outputFormat := "name" ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreatePriorityClass(tf, ioStreams) cmd.Flags().Set("value", "1000") cmd.Flags().Set("global-default", "true") cmd.Flags().Set("description", "my priority") cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", outputFormat) cmd.Flags().Set("preemption-policy", "Never") printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme) printFlags.OutputFormat = &outputFormat options := &PriorityClassOptions{ PrintFlags: printFlags, Name: pcName, IOStreams: ioStreams, } err := options.Complete(tf, cmd, []string{pcName}) if err != nil { t.Fatalf("unexpected error: %v", err) } err = options.Run() if err != nil { t.Fatalf("unexpected error: %v", err) } expectedOutput := "priorityclass.scheduling.k8s.io/" + pcName + "\n" if buf.String() != expectedOutput { t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota.go000066400000000000000000000174571504711711200310730ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" resourceapi "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" coreclient "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( quotaLong = templates.LongDesc(i18n.T(` Create a resource quota with the specified name, hard limits, and optional scopes.`)) quotaExample = templates.Examples(i18n.T(` # Create a new resource quota named my-quota kubectl create quota my-quota --hard=cpu=1,memory=1G,pods=2,services=3,replicationcontrollers=2,resourcequotas=1,secrets=5,persistentvolumeclaims=10 # Create a new resource quota named best-effort kubectl create quota best-effort --hard=pods=100 --scopes=BestEffort`)) ) // QuotaOpts holds the command-line options for 'create quota' sub command type QuotaOpts struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // The name of a quota object. Name string // The hard resource limit string before parsing. Hard string // The scopes of a quota object before parsing. Scopes string CreateAnnotation bool FieldManager string Namespace string EnforceNamespace bool Client *coreclient.CoreV1Client DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewQuotaOpts creates a new *QuotaOpts with sane defaults func NewQuotaOpts(ioStreams genericiooptions.IOStreams) *QuotaOpts { return &QuotaOpts{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateQuota is a macro command to create a new quota func NewCmdCreateQuota(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewQuotaOpts(ioStreams) cmd := &cobra.Command{ Use: "quota NAME [--hard=key1=value1,key2=value2] [--scopes=Scope1,Scope2] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Aliases: []string{"resourcequota"}, Short: i18n.T("Create a quota with the specified name"), Long: quotaLong, Example: quotaExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.Hard, "hard", o.Hard, i18n.T("A comma-delimited set of resource=quantity pairs that define a hard limit.")) cmd.Flags().StringVar(&o.Scopes, "scopes", o.Scopes, i18n.T("A comma-delimited set of quota scopes that must all match each object tracked by the quota.")) cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *QuotaOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = coreclient.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate checks to the QuotaOpts to see if there is sufficient information run the command. func (o *QuotaOpts) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } return nil } // Run does the work func (o *QuotaOpts) Run() error { resourceQuota, err := o.createQuota() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, resourceQuota, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } resourceQuota, err = o.Client.ResourceQuotas(o.Namespace).Create(context.TODO(), resourceQuota, createOptions) if err != nil { return fmt.Errorf("failed to create quota: %v", err) } } return o.PrintObj(resourceQuota) } func (o *QuotaOpts) createQuota() (*corev1.ResourceQuota, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } resourceQuota := &corev1.ResourceQuota{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ResourceQuota"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Namespace: namespace, }, } resourceList, err := populateResourceListV1(o.Hard) if err != nil { return nil, err } scopes, err := parseScopes(o.Scopes) if err != nil { return nil, err } resourceQuota.Spec.Hard = resourceList resourceQuota.Spec.Scopes = scopes return resourceQuota, nil } // populateResourceListV1 takes strings of form =,= // and returns ResourceList. func populateResourceListV1(spec string) (corev1.ResourceList, error) { // empty input gets a nil response to preserve generator test expected behaviors if spec == "" { return nil, nil } result := corev1.ResourceList{} resourceStatements := strings.Split(spec, ",") for _, resourceStatement := range resourceStatements { parts := strings.Split(resourceStatement, "=") if len(parts) != 2 { return nil, fmt.Errorf("Invalid argument syntax %v, expected =", resourceStatement) } resourceName := corev1.ResourceName(parts[0]) resourceQuantity, err := resourceapi.ParseQuantity(parts[1]) if err != nil { return nil, err } result[resourceName] = resourceQuantity } return result, nil } func parseScopes(spec string) ([]corev1.ResourceQuotaScope, error) { // empty input gets a nil response to preserve test expected behaviors if spec == "" { return nil, nil } scopes := strings.Split(spec, ",") result := make([]corev1.ResourceQuotaScope, 0, len(scopes)) for _, scope := range scopes { // intentionally do not verify the scope against the valid scope list. This is done by the apiserver anyway. if scope == "" { return nil, fmt.Errorf("invalid resource quota scope \"\"") } result = append(result, corev1.ResourceQuotaScope(scope)) } return result, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota_test.go000066400000000000000000000065571504711711200321310ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "testing" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateQuota(t *testing.T) { hards := []string{"cpu=1", "cpu=1,pods=42"} var resourceQuotaSpecLists []corev1.ResourceList for _, hard := range hards { resourceQuotaSpecList, err := populateResourceListV1(hard) if err != nil { t.Errorf("unexpected error: %v", err) } resourceQuotaSpecLists = append(resourceQuotaSpecLists, resourceQuotaSpecList) } tests := map[string]struct { options *QuotaOpts expected *corev1.ResourceQuota }{ "single resource": { options: &QuotaOpts{ Name: "my-quota", Hard: hards[0], Scopes: "", }, expected: &corev1.ResourceQuota{ TypeMeta: metav1.TypeMeta{ Kind: "ResourceQuota", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-quota", }, Spec: corev1.ResourceQuotaSpec{ Hard: resourceQuotaSpecLists[0], }, }, }, "single resource with a scope": { options: &QuotaOpts{ Name: "my-quota", Hard: hards[0], Scopes: "BestEffort", }, expected: &corev1.ResourceQuota{ TypeMeta: metav1.TypeMeta{ Kind: "ResourceQuota", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-quota", }, Spec: corev1.ResourceQuotaSpec{ Hard: resourceQuotaSpecLists[0], Scopes: []corev1.ResourceQuotaScope{"BestEffort"}, }, }, }, "multiple resources": { options: &QuotaOpts{ Name: "my-quota", Hard: hards[1], Scopes: "BestEffort", }, expected: &corev1.ResourceQuota{ TypeMeta: metav1.TypeMeta{ Kind: "ResourceQuota", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-quota", }, Spec: corev1.ResourceQuotaSpec{ Hard: resourceQuotaSpecLists[1], Scopes: []corev1.ResourceQuotaScope{"BestEffort"}, }, }, }, "single resource with multiple scopes": { options: &QuotaOpts{ Name: "my-quota", Hard: hards[0], Scopes: "BestEffort,NotTerminating", }, expected: &corev1.ResourceQuota{ TypeMeta: metav1.TypeMeta{ Kind: "ResourceQuota", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-quota", }, Spec: corev1.ResourceQuotaSpec{ Hard: resourceQuotaSpecLists[0], Scopes: []corev1.ResourceQuotaScope{"BestEffort", "NotTerminating"}, }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { resourceQuota, err := tc.options.createQuota() if err != nil { t.Errorf("unexpected error:\n%#v\n", err) return } if !apiequality.Semantic.DeepEqual(resourceQuota, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, resourceQuota) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_role.go000066400000000000000000000311041504711711200306640ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "strings" "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" clientgorbacv1 "k8s.io/client-go/kubernetes/typed/rbac/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( roleLong = templates.LongDesc(i18n.T(` Create a role with single rule.`)) roleExample = templates.Examples(i18n.T(` # Create a role named "pod-reader" that allows user to perform "get", "watch" and "list" on pods kubectl create role pod-reader --verb=get --verb=list --verb=watch --resource=pods # Create a role named "pod-reader" with ResourceName specified kubectl create role pod-reader --verb=get --resource=pods --resource-name=readablepod --resource-name=anotherpod # Create a role named "foo" with API Group specified kubectl create role foo --verb=get,list,watch --resource=rs.apps # Create a role named "foo" with SubResource specified kubectl create role foo --verb=get,list,watch --resource=pods,pods/status`)) // Valid resource verb list for validation. validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "deletecollection", "use", "bind", "escalate", "impersonate"} // Specialized verbs and GroupResources specialVerbs = map[string][]schema.GroupResource{ "use": { { Group: "policy", Resource: "podsecuritypolicies", }, { Group: "extensions", Resource: "podsecuritypolicies", }, }, "bind": { { Group: "rbac.authorization.k8s.io", Resource: "roles", }, { Group: "rbac.authorization.k8s.io", Resource: "clusterroles", }, }, "escalate": { { Group: "rbac.authorization.k8s.io", Resource: "roles", }, { Group: "rbac.authorization.k8s.io", Resource: "clusterroles", }, }, "impersonate": { { Group: "", Resource: "users", }, { Group: "", Resource: "serviceaccounts", }, { Group: "", Resource: "groups", }, { Group: "authentication.k8s.io", Resource: "userextras", }, }, } ) // AddSpecialVerb allows the addition of items to the `specialVerbs` map for non-k8s native resources. func AddSpecialVerb(verb string, gr schema.GroupResource) { resources, ok := specialVerbs[verb] if !ok { resources = make([]schema.GroupResource, 0, 1) } resources = append(resources, gr) specialVerbs[verb] = resources } // ResourceOptions holds the related options for '--resource' option type ResourceOptions struct { Group string Resource string SubResource string } // CreateRoleOptions holds the options for 'create role' sub command type CreateRoleOptions struct { PrintFlags *genericclioptions.PrintFlags Name string Verbs []string Resources []ResourceOptions ResourceNames []string DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string OutputFormat string Namespace string EnforceNamespace bool Client clientgorbacv1.RbacV1Interface Mapper meta.RESTMapper PrintObj func(obj runtime.Object) error FieldManager string CreateAnnotation bool genericiooptions.IOStreams } // NewCreateRoleOptions returns an initialized CreateRoleOptions instance func NewCreateRoleOptions(ioStreams genericiooptions.IOStreams) *CreateRoleOptions { return &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateRole returnns an initialized Command instance for 'create role' sub command func NewCmdCreateRole(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewCreateRoleOptions(ioStreams) cmd := &cobra.Command{ Use: "role NAME --verb=verb --resource=resource.group/subresource [--resource-name=resourcename] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a role with single rule"), Long: roleLong, Example: roleExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunCreateRole()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringSliceVar(&o.Verbs, "verb", o.Verbs, "Verb that applies to the resources contained in the rule") cmd.Flags().StringSlice("resource", []string{}, "Resource that the rule applies to") cmd.Flags().StringArrayVar(&o.ResourceNames, "resource-name", o.ResourceNames, "Resource in the white list that the rule applies to, repeat this flag for multiple items") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *CreateRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name // Remove duplicate verbs. verbs := []string{} for _, v := range o.Verbs { // VerbAll respresents all kinds of verbs. if v == "*" { verbs = []string{"*"} break } if !arrayContains(verbs, v) { verbs = append(verbs, v) } } o.Verbs = verbs // Support resource.group pattern. If no API Group specified, use "" as core API Group. // e.g. --resource=pods,deployments.extensions resources := cmdutil.GetFlagStringSlice(cmd, "resource") for _, r := range resources { sections := strings.SplitN(r, "/", 2) resource := &ResourceOptions{} if len(sections) == 2 { resource.SubResource = sections[1] } parts := strings.SplitN(sections[0], ".", 2) if len(parts) == 2 { resource.Group = parts[1] } resource.Resource = parts[0] if resource.Resource == "*" && len(parts) == 1 && len(sections) == 1 { o.Resources = []ResourceOptions{*resource} break } o.Resources = append(o.Resources, *resource) } // Remove duplicate resource names. resourceNames := []string{} for _, n := range o.ResourceNames { if !arrayContains(resourceNames, n) { resourceNames = append(resourceNames, n) } } o.ResourceNames = resourceNames // Complete other options for Run. o.Mapper, err = f.ToRESTMapper() if err != nil { return err } o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.OutputFormat = cmdutil.GetFlagString(cmd, "output") o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } clientset, err := f.KubernetesClientSet() if err != nil { return err } o.Client = clientset.RbacV1() return nil } // Validate makes sure there is no discrepency in provided option values func (o *CreateRoleOptions) Validate() error { if o.Name == "" { return fmt.Errorf("name must be specified") } // validate verbs. if len(o.Verbs) == 0 { return fmt.Errorf("at least one verb must be specified") } for _, v := range o.Verbs { if !arrayContains(validResourceVerbs, v) { fmt.Fprintf(o.ErrOut, "Warning: '%s' is not a standard resource verb\n", v) } } // validate resources. if len(o.Resources) == 0 { return fmt.Errorf("at least one resource must be specified") } return o.validateResource() } func (o *CreateRoleOptions) validateResource() error { for _, r := range o.Resources { if len(r.Resource) == 0 { return fmt.Errorf("resource must be specified if apiGroup/subresource specified") } if r.Resource == "*" { return nil } resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group} groupVersionResource, err := o.Mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}) if err == nil { resource = groupVersionResource } for _, v := range o.Verbs { if groupResources, ok := specialVerbs[v]; ok { match := false for _, extra := range groupResources { if resource.Resource == extra.Resource && resource.Group == extra.Group { match = true err = nil break } } if !match { return fmt.Errorf("can not perform '%s' on '%s' in group '%s'", v, resource.Resource, resource.Group) } } } if err != nil { return err } } return nil } // RunCreateRole performs the execution of 'create role' sub command func (o *CreateRoleOptions) RunCreateRole() error { role := &rbacv1.Role{ // this is ok because we know exactly how we want to be serialized TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "Role"}, } role.Name = o.Name rules, err := generateResourcePolicyRules(o.Mapper, o.Verbs, o.Resources, o.ResourceNames, []string{}) if err != nil { return err } role.Rules = rules if o.EnforceNamespace { role.Namespace = o.Namespace } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, role, scheme.DefaultJSONEncoder()); err != nil { return err } // Create role. if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } role, err = o.Client.Roles(o.Namespace).Create(context.TODO(), role, createOptions) if err != nil { return err } } return o.PrintObj(role) } func arrayContains(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false } func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resources []ResourceOptions, resourceNames []string, nonResourceURLs []string) ([]rbacv1.PolicyRule, error) { // groupResourceMapping is a apigroup-resource map. The key of this map is api group, while the value // is a string array of resources under this api group. // E.g. groupResourceMapping = {"extensions": ["replicasets", "deployments"], "batch":["jobs"]} groupResourceMapping := map[string][]string{} // This loop does the following work: // 1. Constructs groupResourceMapping based on input resources. // 2. Prevents pointing to non-existent resources. // 3. Transfers resource short name to long name. E.g. rs.extensions is transferred to replicasets.extensions for _, r := range resources { resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group} groupVersionResource, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}) if err == nil { resource = groupVersionResource } if len(r.SubResource) > 0 { resource.Resource = resource.Resource + "/" + r.SubResource } if !arrayContains(groupResourceMapping[resource.Group], resource.Resource) { groupResourceMapping[resource.Group] = append(groupResourceMapping[resource.Group], resource.Resource) } } // Create separate rule for each of the api group. rules := []rbacv1.PolicyRule{} for _, g := range sets.List(sets.KeySet(groupResourceMapping)) { rule := rbacv1.PolicyRule{} rule.Verbs = verbs rule.Resources = groupResourceMapping[g] rule.APIGroups = []string{g} rule.ResourceNames = resourceNames rules = append(rules, rule) } if len(nonResourceURLs) > 0 { rule := rbacv1.PolicyRule{} rule.Verbs = verbs rule.NonResourceURLs = nonResourceURLs rules = append(rules, rule) } return rules, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_role_test.go000066400000000000000000000430101504711711200317220ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "reflect" "testing" "github.com/google/go-cmp/cmp" rbac "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/equality" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestCreateRole(t *testing.T) { roleName := "my-role" testNameSpace := "test" tf := cmdtesting.NewTestFactory().WithNamespace(testNameSpace) defer tf.Cleanup() tf.Client = &fake.RESTClient{} tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tests := map[string]struct { verbs string resources string resourceNames string expectedRole *rbac.Role }{ "test-duplicate-resources": { verbs: "get,watch,list", resources: "pods,pods", expectedRole: &rbac.Role{ TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}, ObjectMeta: v1.ObjectMeta{ Name: roleName, Namespace: testNameSpace, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "watch", "list"}, Resources: []string{"pods"}, APIGroups: []string{""}, ResourceNames: []string{}, }, }, }, }, "test-subresources": { verbs: "get,watch,list", resources: "replicasets/scale", expectedRole: &rbac.Role{ TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}, ObjectMeta: v1.ObjectMeta{ Name: roleName, Namespace: testNameSpace, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "watch", "list"}, Resources: []string{"replicasets/scale"}, APIGroups: []string{"extensions"}, ResourceNames: []string{}, }, }, }, }, "test-subresources-with-apigroup": { verbs: "get,watch,list", resources: "replicasets.extensions/scale", expectedRole: &rbac.Role{ TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}, ObjectMeta: v1.ObjectMeta{ Name: roleName, Namespace: testNameSpace, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "watch", "list"}, Resources: []string{"replicasets/scale"}, APIGroups: []string{"extensions"}, ResourceNames: []string{}, }, }, }, }, "test-valid-case-with-multiple-apigroups": { verbs: "get,watch,list", resources: "pods,deployments.extensions", expectedRole: &rbac.Role{ TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}, ObjectMeta: v1.ObjectMeta{ Name: roleName, Namespace: testNameSpace, }, Rules: []rbac.PolicyRule{ { Verbs: []string{"get", "watch", "list"}, Resources: []string{"pods"}, APIGroups: []string{""}, ResourceNames: []string{}, }, { Verbs: []string{"get", "watch", "list"}, Resources: []string{"deployments"}, APIGroups: []string{"extensions"}, ResourceNames: []string{}, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateRole(tf, ioStreams) cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", "yaml") cmd.Flags().Set("verb", test.verbs) cmd.Flags().Set("resource", test.resources) if test.resourceNames != "" { cmd.Flags().Set("resource-name", test.resourceNames) } cmd.Run(cmd, []string{roleName}) actual := &rbac.Role{} if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), buf.Bytes(), actual); err != nil { t.Log(buf.String()) t.Fatal(err) } if !equality.Semantic.DeepEqual(test.expectedRole, actual) { t.Errorf("%s", cmp.Diff(test.expectedRole, actual)) } }) } } func TestValidate(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tests := map[string]struct { roleOptions *CreateRoleOptions expectErr bool }{ "test-missing-name": { roleOptions: &CreateRoleOptions{}, expectErr: true, }, "test-missing-verb": { roleOptions: &CreateRoleOptions{ Name: "my-role", }, expectErr: true, }, "test-missing-resource": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"get"}, }, expectErr: true, }, "test-missing-resource-existing-apigroup": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"get"}, Resources: []ResourceOptions{ { Group: "extensions", }, }, }, expectErr: true, }, "test-missing-resource-existing-subresource": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"get"}, Resources: []ResourceOptions{ { SubResource: "scale", }, }, }, expectErr: true, }, "test-invalid-verb": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"invalid-verb"}, Resources: []ResourceOptions{ { Resource: "pods", }, }, }, expectErr: false, }, "test-nonresource-verb": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"post"}, Resources: []ResourceOptions{ { Resource: "pods", }, }, }, expectErr: false, }, "test-special-verb": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"use"}, Resources: []ResourceOptions{ { Resource: "pods", }, }, }, expectErr: true, }, "test-mix-verbs": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"impersonate", "use"}, Resources: []ResourceOptions{ { Resource: "userextras", SubResource: "scopes", }, }, }, expectErr: true, }, "test-special-verb-with-wrong-apigroup": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"impersonate"}, Resources: []ResourceOptions{ { Resource: "userextras", SubResource: "scopes", Group: "extensions", }, }, }, expectErr: true, }, "test-invalid-resource": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"get"}, Resources: []ResourceOptions{ { Resource: "invalid-resource", }, }, }, expectErr: true, }, "test-resource-name-with-multiple-resources": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"get"}, Resources: []ResourceOptions{ { Resource: "pods", }, { Resource: "deployments", Group: "extensions", }, }, ResourceNames: []string{"foo"}, }, expectErr: false, }, "test-valid-case": { roleOptions: &CreateRoleOptions{ Name: "role-binder", Verbs: []string{"get", "list", "bind"}, Resources: []ResourceOptions{ { Resource: "roles", Group: "rbac.authorization.k8s.io", }, }, ResourceNames: []string{"foo"}, }, expectErr: false, }, "test-valid-case-with-subresource": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"get", "list"}, Resources: []ResourceOptions{ { Resource: "replicasets", SubResource: "scale", }, }, ResourceNames: []string{"bar"}, }, expectErr: false, }, "test-valid-case-with-additional-resource": { roleOptions: &CreateRoleOptions{ Name: "my-role", Verbs: []string{"impersonate"}, Resources: []ResourceOptions{ { Resource: "userextras", SubResource: "scopes", Group: "authentication.k8s.io", }, }, }, expectErr: false, }, } for name, test := range tests { test.roleOptions.IOStreams = genericiooptions.NewTestIOStreamsDiscard() var err error test.roleOptions.Mapper, err = tf.ToRESTMapper() if err != nil { t.Fatal(err) } err = test.roleOptions.Validate() if test.expectErr && err == nil { t.Errorf("%s: expect error happens but validate passes.", name) } if !test.expectErr && err != nil { t.Errorf("%s: unexpected error: %v", name, err) } } } func TestComplete(t *testing.T) { roleName := "my-role" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{} tf.ClientConfigVal = cmdtesting.DefaultClientConfig() defaultTestResources := "pods,deployments.extensions" tests := map[string]struct { params []string resources string roleOptions *CreateRoleOptions expected *CreateRoleOptions expectErr bool }{ "test-missing-name": { params: []string{}, resources: defaultTestResources, roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), }, expectErr: true, }, "test-duplicate-verbs": { params: []string{roleName}, resources: defaultTestResources, roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), Name: roleName, Verbs: []string{ "get", "watch", "list", "get", }, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{ "get", "watch", "list", }, Resources: []ResourceOptions{ { Resource: "pods", Group: "", }, { Resource: "deployments", Group: "extensions", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-verball": { params: []string{roleName}, resources: defaultTestResources, roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), Name: roleName, Verbs: []string{ "get", "watch", "list", "*", }, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "pods", Group: "", }, { Resource: "deployments", Group: "extensions", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-allresource": { params: []string{roleName}, resources: "*,pods", roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created"), Name: roleName, Verbs: []string{"*"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "*", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-allresource-subresource": { params: []string{roleName}, resources: "*/scale,pods", roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created"), Name: roleName, Verbs: []string{"*"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "*", SubResource: "scale", }, { Resource: "pods", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-allresrouce-allgroup": { params: []string{roleName}, resources: "*.*,pods", roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created"), Name: roleName, Verbs: []string{"*"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "*", Group: "*", }, { Resource: "pods", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-allresource-allgroup-subresource": { params: []string{roleName}, resources: "*.*/scale,pods", roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created"), Name: roleName, Verbs: []string{"*"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "*", Group: "*", SubResource: "scale", }, { Resource: "pods", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-allresource-specificgroup": { params: []string{roleName}, resources: "*.extensions,pods", roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created"), Name: roleName, Verbs: []string{"*"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "*", Group: "extensions", }, { Resource: "pods", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-allresource-specificgroup-subresource": { params: []string{roleName}, resources: "*.extensions/scale,pods", roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created"), Name: roleName, Verbs: []string{"*"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "*", Group: "extensions", SubResource: "scale", }, { Resource: "pods", }, }, ResourceNames: []string{}, }, expectErr: false, }, "test-duplicate-resourcenames": { params: []string{roleName}, resources: defaultTestResources, roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), Name: roleName, Verbs: []string{"*"}, ResourceNames: []string{"foo", "foo"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "pods", Group: "", }, { Resource: "deployments", Group: "extensions", }, }, ResourceNames: []string{"foo"}, }, expectErr: false, }, "test-valid-complete-case": { params: []string{roleName}, resources: defaultTestResources, roleOptions: &CreateRoleOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), Name: roleName, Verbs: []string{"*"}, ResourceNames: []string{"foo"}, }, expected: &CreateRoleOptions{ Name: roleName, Verbs: []string{"*"}, Resources: []ResourceOptions{ { Resource: "pods", Group: "", }, { Resource: "deployments", Group: "extensions", }, }, ResourceNames: []string{"foo"}, }, expectErr: false, }, } for name, test := range tests { cmd := NewCmdCreateRole(tf, genericiooptions.NewTestIOStreamsDiscard()) cmd.Flags().Set("resource", test.resources) err := test.roleOptions.Complete(tf, cmd, test.params) if !test.expectErr && err != nil { t.Errorf("%s: unexpected error: %v", name, err) } if test.expectErr { if err != nil { continue } else { t.Errorf("%s: expect error happens but test passes.", name) } } if test.roleOptions.Name != test.expected.Name { t.Errorf("%s:\nexpected name:\n%#v\nsaw name:\n%#v", name, test.expected.Name, test.roleOptions.Name) } if !reflect.DeepEqual(test.roleOptions.Verbs, test.expected.Verbs) { t.Errorf("%s:\nexpected verbs:\n%#v\nsaw verbs:\n%#v", name, test.expected.Verbs, test.roleOptions.Verbs) } if !reflect.DeepEqual(test.roleOptions.Resources, test.expected.Resources) { t.Errorf("%s:\nexpected resources:\n%#v\nsaw resources:\n%#v", name, test.expected.Resources, test.roleOptions.Resources) } if !reflect.DeepEqual(test.roleOptions.ResourceNames, test.expected.ResourceNames) { t.Errorf("%s:\nexpected resource names:\n%#v\nsaw resource names:\n%#v", name, test.expected.ResourceNames, test.roleOptions.ResourceNames) } } } func TestAddSpecialVerb(t *testing.T) { testCases := map[string]struct { verb string resource schema.GroupResource isNew bool }{ "existing verb": { verb: "use", resource: schema.GroupResource{Group: "my.custom.io", Resource: "one"}, isNew: false, }, "new verb": { verb: "new", resource: schema.GroupResource{Group: "my.custom.io", Resource: "two"}, isNew: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { AddSpecialVerb(tc.verb, tc.resource) resources, ok := specialVerbs[tc.verb] if !ok { t.Errorf("missing expected verb: %s", tc.verb) } if tc.isNew { if len(resources) != 1 { t.Errorf("new verb should only contain one resource resources:%#v", resources) } if !reflect.DeepEqual(tc.resource, resources[0]) { t.Errorf("miss expected resource:%#v", tc.resource) } return } for _, res := range resources { if reflect.DeepEqual(tc.resource, res) { return } } t.Errorf("missing expected resource:%#v", tc.resource) }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_rolebinding.go000066400000000000000000000176201504711711200322260ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "strings" "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" rbacclientv1 "k8s.io/client-go/kubernetes/typed/rbac/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( roleBindingLong = templates.LongDesc(i18n.T(` Create a role binding for a particular role or cluster role.`)) roleBindingExample = templates.Examples(i18n.T(` # Create a role binding for user1, user2, and group1 using the admin cluster role kubectl create rolebinding admin --clusterrole=admin --user=user1 --user=user2 --group=group1 # Create a role binding for service account monitoring:sa-dev using the admin role kubectl create rolebinding admin-binding --role=admin --serviceaccount=monitoring:sa-dev`)) ) // RoleBindingOptions holds the options for 'create rolebinding' sub command type RoleBindingOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string Namespace string EnforceNamespace bool ClusterRole string Role string Users []string Groups []string ServiceAccounts []string FieldManager string CreateAnnotation bool Client rbacclientv1.RbacV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewRoleBindingOptions creates a new *RoleBindingOptions with sane defaults func NewRoleBindingOptions(ioStreams genericiooptions.IOStreams) *RoleBindingOptions { return &RoleBindingOptions{ Users: []string{}, Groups: []string{}, ServiceAccounts: []string{}, PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateRoleBinding returns an initialized Command instance for 'create rolebinding' sub command func NewCmdCreateRoleBinding(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewRoleBindingOptions(ioStreams) cmd := &cobra.Command{ Use: "rolebinding NAME --clusterrole=NAME|--role=NAME [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a role binding for a particular role or cluster role"), Long: roleBindingLong, Example: roleBindingExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.ClusterRole, "clusterrole", "", i18n.T("ClusterRole this RoleBinding should reference")) cmd.Flags().StringVar(&o.Role, "role", "", i18n.T("Role this RoleBinding should reference")) cmd.Flags().StringArrayVar(&o.Users, "user", o.Users, "Usernames to bind to the role. The flag can be repeated to add multiple users.") cmd.Flags().StringArrayVar(&o.Groups, "group", o.Groups, "Groups to bind to the role. The flag can be repeated to add multiple groups.") cmd.Flags().StringArrayVar(&o.ServiceAccounts, "serviceaccount", o.ServiceAccounts, "Service accounts to bind to the role, in the format :. The flag can be repeated to add multiple service accounts.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *RoleBindingOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = rbacclientv1.NewForConfig(clientConfig) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) return err } // Validate validates required fields are set func (o *RoleBindingOptions) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } if (len(o.ClusterRole) == 0) == (len(o.Role) == 0) { return fmt.Errorf("exactly one of clusterrole or role must be specified") } return nil } // Run performs the execution of 'create rolebinding' sub command func (o *RoleBindingOptions) Run() error { roleBinding, err := o.createRoleBinding() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, roleBinding, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } roleBinding, err = o.Client.RoleBindings(o.Namespace).Create(context.TODO(), roleBinding, createOptions) if err != nil { return fmt.Errorf("failed to create rolebinding: %v", err) } } return o.PrintObj(roleBinding) } func (o *RoleBindingOptions) createRoleBinding() (*rbacv1.RoleBinding, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } roleBinding := &rbacv1.RoleBinding{ TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "RoleBinding"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Namespace: namespace, }, } switch { case len(o.Role) > 0: roleBinding.RoleRef = rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: "Role", Name: o.Role, } case len(o.ClusterRole) > 0: roleBinding.RoleRef = rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: o.ClusterRole, } } for _, user := range o.Users { roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: user, }) } for _, group := range o.Groups { roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: group, }) } for _, sa := range o.ServiceAccounts { tokens := strings.Split(sa, ":") if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { return nil, fmt.Errorf("serviceaccount must be :") } roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ Kind: rbacv1.ServiceAccountKind, APIGroup: "", Namespace: tokens[0], Name: tokens[1], }) } return roleBinding, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_rolebinding_test.go000066400000000000000000000043001504711711200332540ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "strconv" "testing" rbac "k8s.io/api/rbac/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateRoleBinding(t *testing.T) { tests := []struct { options *RoleBindingOptions expected *rbac.RoleBinding }{ { options: &RoleBindingOptions{ Role: "fake-role", Users: []string{"fake-user"}, Groups: []string{"fake-group"}, ServiceAccounts: []string{"fake-namespace:fake-account"}, Name: "fake-binding", }, expected: &rbac.RoleBinding{ TypeMeta: v1.TypeMeta{ Kind: "RoleBinding", APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: v1.ObjectMeta{ Name: "fake-binding", }, RoleRef: rbac.RoleRef{ APIGroup: rbac.GroupName, Kind: "Role", Name: "fake-role", }, Subjects: []rbac.Subject{ { Kind: rbac.UserKind, APIGroup: "rbac.authorization.k8s.io", Name: "fake-user", }, { Kind: rbac.GroupKind, APIGroup: "rbac.authorization.k8s.io", Name: "fake-group", }, { Kind: rbac.ServiceAccountKind, Namespace: "fake-namespace", Name: "fake-account", }, }, }, }, } for i, tc := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { roleBinding, err := tc.options.createRoleBinding() if err != nil { t.Errorf("unexpected error:\n%#v\n", err) return } if !apiequality.Semantic.DeepEqual(roleBinding, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, roleBinding) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret.go000066400000000000000000000337341504711711200312230ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/hash" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // NewCmdCreateSecret groups subcommands to create various types of secrets. // This is the entry point of create_secret.go which will be called by create.go func NewCmdCreateSecret(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "secret (docker-registry | generic | tls)", DisableFlagsInUseLine: true, Short: i18n.T("Create a secret using a specified subcommand"), Long: secretLong, Run: cmdutil.DefaultSubCommandRun(ioStreams.ErrOut), } cmd.AddCommand(NewCmdCreateSecretDockerRegistry(f, ioStreams)) cmd.AddCommand(NewCmdCreateSecretTLS(f, ioStreams)) cmd.AddCommand(NewCmdCreateSecretGeneric(f, ioStreams)) return cmd } var ( secretLong = templates.LongDesc(i18n.T(` Create a secret with specified type. A docker-registry type secret is for accessing a container registry. A generic type secret indicate an Opaque secret type. A tls type secret holds TLS certificate and its associated key.`)) secretForGenericLong = templates.LongDesc(i18n.T(` Create a secret based on a file, directory, or specified literal value. A single secret may package one or more key/value pairs. When creating a secret based on a file, the key will default to the basename of the file, and the value will default to the file content. If the basename is an invalid key or you wish to chose your own, you may specify an alternate key. When creating a secret based on a directory, each file whose basename is a valid key in the directory will be packaged into the secret. Any directory entries except regular files are ignored (e.g. subdirectories, symlinks, devices, pipes, etc).`)) secretForGenericExample = templates.Examples(i18n.T(` # Create a new secret named my-secret with keys for each file in folder bar kubectl create secret generic my-secret --from-file=path/to/bar # Create a new secret named my-secret with specified keys instead of names on disk kubectl create secret generic my-secret --from-file=ssh-privatekey=path/to/id_rsa --from-file=ssh-publickey=path/to/id_rsa.pub # Create a new secret named my-secret with key1=supersecret and key2=topsecret kubectl create secret generic my-secret --from-literal=key1=supersecret --from-literal=key2=topsecret # Create a new secret named my-secret using a combination of a file and a literal kubectl create secret generic my-secret --from-file=ssh-privatekey=path/to/id_rsa --from-literal=passphrase=topsecret # Create a new secret named my-secret from env files kubectl create secret generic my-secret --from-env-file=path/to/foo.env --from-env-file=path/to/bar.env`)) ) // CreateSecretOptions holds the options for 'create secret' sub command type CreateSecretOptions struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // Name of secret (required) Name string // Type of secret (optional) Type string // FileSources to derive the secret from (optional) FileSources []string // LiteralSources to derive the secret from (optional) LiteralSources []string // EnvFileSources to derive the secret from (optional) EnvFileSources []string // AppendHash; if true, derive a hash from the Secret data and type and append it to the name AppendHash bool FieldManager string CreateAnnotation bool Namespace string EnforceNamespace bool Client corev1client.CoreV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewSecretOptions creates a new *CreateSecretOptions with default value func NewSecretOptions(ioStreams genericiooptions.IOStreams) *CreateSecretOptions { return &CreateSecretOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateSecretGeneric is a command to create generic secrets from files, directories, or literal values func NewCmdCreateSecretGeneric(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewSecretOptions(ioStreams) cmd := &cobra.Command{ Use: "generic NAME [--type=string] [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a secret from a local file, directory, or literal value"), Long: secretForGenericLong, Example: secretForGenericExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.") cmd.Flags().StringArrayVar(&o.LiteralSources, "from-literal", o.LiteralSources, "Specify a key and literal value to insert in secret (i.e. mykey=somevalue)") cmd.Flags().StringSliceVar(&o.EnvFileSources, "from-env-file", o.EnvFileSources, "Specify the path to a file to read lines of key=val pairs to create a secret.") cmd.Flags().StringVar(&o.Type, "type", o.Type, i18n.T("The type of secret to create")) cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete loads data from the command line environment func (o *CreateSecretOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = corev1client.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate checks if CreateSecretOptions has sufficient value to run func (o *CreateSecretOptions) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } if len(o.EnvFileSources) > 0 && (len(o.FileSources) > 0 || len(o.LiteralSources) > 0) { return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal") } return nil } // Run calls createSecret which will create secret based on CreateSecretOptions // and makes an API call to the server func (o *CreateSecretOptions) Run() error { secret, err := o.createSecret() if err != nil { return err } err = util.CreateOrUpdateAnnotation(o.CreateAnnotation, secret, scheme.DefaultJSONEncoder()) if err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } secret, err = o.Client.Secrets(o.Namespace).Create(context.TODO(), secret, createOptions) if err != nil { return fmt.Errorf("failed to create secret %v", err) } } return o.PrintObj(secret) } // createSecret fills in key value pair from the information given in // CreateSecretOptions into *corev1.Secret func (o *CreateSecretOptions) createSecret() (*corev1.Secret, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } secret := newSecretObj(o.Name, namespace, corev1.SecretType(o.Type)) if len(o.LiteralSources) > 0 { if err := handleSecretFromLiteralSources(secret, o.LiteralSources); err != nil { return nil, err } } if len(o.FileSources) > 0 { if err := handleSecretFromFileSources(secret, o.FileSources); err != nil { return nil, err } } if len(o.EnvFileSources) > 0 { if err := handleSecretFromEnvFileSources(secret, o.EnvFileSources); err != nil { return nil, err } } if o.AppendHash { hash, err := hash.SecretHash(secret) if err != nil { return nil, err } secret.Name = fmt.Sprintf("%s-%s", secret.Name, hash) } return secret, nil } // newSecretObj will create a new Secret Object given name, namespace and secretType func newSecretObj(name, namespace string, secretType corev1.SecretType) *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Type: secretType, Data: map[string][]byte{}, } } // handleSecretFromLiteralSources adds the specified literal source // information into the provided secret func handleSecretFromLiteralSources(secret *corev1.Secret, literalSources []string) error { for _, literalSource := range literalSources { keyName, value, err := util.ParseLiteralSource(literalSource) if err != nil { return err } if err = addKeyFromLiteralToSecret(secret, keyName, []byte(value)); err != nil { return err } } return nil } // handleSecretFromFileSources adds the specified file source information into the provided secret func handleSecretFromFileSources(secret *corev1.Secret, fileSources []string) error { for _, fileSource := range fileSources { keyName, filePath, err := util.ParseFileSource(fileSource) if err != nil { return err } fileInfo, err := os.Stat(filePath) if err != nil { switch err := err.(type) { case *os.PathError: return fmt.Errorf("error reading %s: %v", filePath, err.Err) default: return fmt.Errorf("error reading %s: %v", filePath, err) } } // if the filePath is a directory if fileInfo.IsDir() { if strings.Contains(fileSource, "=") { return fmt.Errorf("cannot give a key name for a directory path") } fileList, err := os.ReadDir(filePath) if err != nil { return fmt.Errorf("error listing files in %s: %v", filePath, err) } for _, item := range fileList { itemPath := filepath.Join(filePath, item.Name()) if item.Type().IsRegular() { keyName = item.Name() if err := addKeyFromFileToSecret(secret, keyName, itemPath); err != nil { return err } } } // if the filepath is a file } else { if err := addKeyFromFileToSecret(secret, keyName, filePath); err != nil { return err } } } return nil } // handleSecretFromEnvFileSources adds the specified env files source information // into the provided secret func handleSecretFromEnvFileSources(secret *corev1.Secret, envFileSources []string) error { for _, envFileSource := range envFileSources { info, err := os.Stat(envFileSource) if err != nil { switch err := err.(type) { case *os.PathError: return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) default: return fmt.Errorf("error reading %s: %v", envFileSource, err) } } if info.IsDir() { return fmt.Errorf("env secret file cannot be a directory") } err = cmdutil.AddFromEnvFile(envFileSource, func(key, value string) error { return addKeyFromLiteralToSecret(secret, key, []byte(value)) }) if err != nil { return err } } return nil } // addKeyFromFileToSecret adds a key with the given name to a Secret, populating // the value with the content of the given file path, or returns an error. func addKeyFromFileToSecret(secret *corev1.Secret, keyName, filePath string) error { data, err := os.ReadFile(filePath) if err != nil { return err } return addKeyFromLiteralToSecret(secret, keyName, data) } // addKeyFromLiteralToSecret adds the given key and data to the given secret, // returning an error if the key is not valid or if the key already exists. func addKeyFromLiteralToSecret(secret *corev1.Secret, keyName string, data []byte) error { if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 { return fmt.Errorf("%q is not valid key name for a Secret %s", keyName, strings.Join(errs, ";")) } if _, entryExists := secret.Data[keyName]; entryExists { return fmt.Errorf("cannot add key %s, another key by that name already exists", keyName) } secret.Data[keyName] = data return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_docker.go000066400000000000000000000262361504711711200325510ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "encoding/base64" "encoding/json" "fmt" "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/hash" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( secretForDockerRegistryLong = templates.LongDesc(i18n.T(` Create a new secret for use with Docker registries. Dockercfg secrets are used to authenticate against Docker registries. When using the Docker command line to push images, you can authenticate to a given registry by running: '$ docker login DOCKER_REGISTRY_SERVER --username=DOCKER_USER --password=DOCKER_PASSWORD --email=DOCKER_EMAIL'. That produces a ~/.dockercfg file that is used by subsequent 'docker push' and 'docker pull' commands to authenticate to the registry. The email address is optional. When creating applications, you may have a Docker registry that requires authentication. In order for the nodes to pull images on your behalf, they must have the credentials. You can provide this information by creating a dockercfg secret and attaching it to your service account.`)) secretForDockerRegistryExample = templates.Examples(i18n.T(` # If you do not already have a .dockercfg file, create a dockercfg secret directly kubectl create secret docker-registry my-secret --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL # Create a new secret named my-secret from ~/.docker/config.json kubectl create secret docker-registry my-secret --from-file=path/to/.docker/config.json`)) ) // DockerConfigJSON represents a local docker auth config file // for pulling images. type DockerConfigJSON struct { Auths DockerConfig `json:"auths" datapolicy:"token"` // +optional HttpHeaders map[string]string `json:"HttpHeaders,omitempty" datapolicy:"token"` } // DockerConfig represents the config file used by the docker CLI. // This config that represents the credentials that should be used // when pulling images from specific image repositories. type DockerConfig map[string]DockerConfigEntry // DockerConfigEntry holds the user information that grant the access to docker registry type DockerConfigEntry struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty" datapolicy:"password"` Email string `json:"email,omitempty"` Auth string `json:"auth,omitempty" datapolicy:"token"` } // CreateSecretDockerRegistryOptions holds the options for 'create secret docker-registry' sub command type CreateSecretDockerRegistryOptions struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // Name of secret (required) Name string // FileSources to derive the secret from (optional) FileSources []string // Username for registry (required) Username string // Email for registry (optional) Email string // Password for registry (required) Password string `datapolicy:"password"` // Server for registry (required) Server string // AppendHash; if true, derive a hash from the Secret and append it to the name AppendHash bool FieldManager string CreateAnnotation bool Namespace string EnforceNamespace bool Client corev1client.CoreV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewSecretDockerRegistryOptions creates a new *CreateSecretDockerRegistryOptions with default value func NewSecretDockerRegistryOptions(ioStreams genericiooptions.IOStreams) *CreateSecretDockerRegistryOptions { return &CreateSecretDockerRegistryOptions{ Server: "https://index.docker.io/v1/", PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateSecretDockerRegistry is a macro command for creating secrets to work with Docker registries func NewCmdCreateSecretDockerRegistry(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewSecretDockerRegistryOptions(ioStreams) cmd := &cobra.Command{ Use: "docker-registry NAME --docker-username=user --docker-password=password --docker-email=email [--docker-server=string] [--from-file=[key=]source] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a secret for use with a Docker registry"), Long: secretForDockerRegistryLong, Example: secretForDockerRegistryExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.Username, "docker-username", o.Username, i18n.T("Username for Docker registry authentication")) cmd.Flags().StringVar(&o.Password, "docker-password", o.Password, i18n.T("Password for Docker registry authentication")) cmd.Flags().StringVar(&o.Email, "docker-email", o.Email, i18n.T("Email for Docker registry")) cmd.Flags().StringVar(&o.Server, "docker-server", o.Server, i18n.T("Server location for Docker registry")) cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.") cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, "+ "in which case a default name of "+corev1.DockerConfigJsonKey+" will be given to them, "+ "or optionally with a name and file path, in which case the given name will be used. "+ "Specifying a directory will iterate each named file in the directory that is a valid secret key. "+ "For this command, the key should always be "+corev1.DockerConfigJsonKey+".") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete loads data from the command line environment func (o *CreateSecretDockerRegistryOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = corev1client.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } for i := range o.FileSources { if !strings.Contains(o.FileSources[i], "=") { o.FileSources[i] = corev1.DockerConfigJsonKey + "=" + o.FileSources[i] } } return nil } // Validate checks if CreateSecretDockerRegistryOptions has sufficient value to run func (o *CreateSecretDockerRegistryOptions) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } if len(o.FileSources) == 0 && (len(o.Username) == 0 || len(o.Password) == 0 || len(o.Server) == 0) { return fmt.Errorf("either --from-file or the combination of --docker-username, --docker-password and --docker-server is required") } return nil } // Run calls createSecretDockerRegistry which will create secretDockerRegistry based on CreateSecretDockerRegistryOptions // and makes an API call to the server func (o *CreateSecretDockerRegistryOptions) Run() error { secretDockerRegistry, err := o.createSecretDockerRegistry() if err != nil { return err } err = util.CreateOrUpdateAnnotation(o.CreateAnnotation, secretDockerRegistry, scheme.DefaultJSONEncoder()) if err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } secretDockerRegistry, err = o.Client.Secrets(o.Namespace).Create(context.TODO(), secretDockerRegistry, createOptions) if err != nil { return fmt.Errorf("failed to create secret %v", err) } } return o.PrintObj(secretDockerRegistry) } // createSecretDockerRegistry fills in key value pair from the information given in // CreateSecretDockerRegistryOptions into *corev1.Secret func (o *CreateSecretDockerRegistryOptions) createSecretDockerRegistry() (*corev1.Secret, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } secretDockerRegistry := newSecretObj(o.Name, namespace, corev1.SecretTypeDockerConfigJson) if len(o.FileSources) > 0 { if err := handleSecretFromFileSources(secretDockerRegistry, o.FileSources); err != nil { return nil, err } } else { dockerConfigJSONContent, err := handleDockerCfgJSONContent(o.Username, o.Password, o.Email, o.Server) if err != nil { return nil, err } secretDockerRegistry.Data[corev1.DockerConfigJsonKey] = dockerConfigJSONContent } if o.AppendHash { hash, err := hash.SecretHash(secretDockerRegistry) if err != nil { return nil, err } secretDockerRegistry.Name = fmt.Sprintf("%s-%s", secretDockerRegistry.Name, hash) } return secretDockerRegistry, nil } // handleDockerCfgJSONContent serializes a ~/.docker/config.json file func handleDockerCfgJSONContent(username, password, email, server string) ([]byte, error) { dockerConfigAuth := DockerConfigEntry{ Username: username, Password: password, Email: email, Auth: encodeDockerConfigFieldAuth(username, password), } dockerConfigJSON := DockerConfigJSON{ Auths: map[string]DockerConfigEntry{server: dockerConfigAuth}, } return json.Marshal(dockerConfigJSON) } // encodeDockerConfigFieldAuth returns base64 encoding of the username and password string func encodeDockerConfigFieldAuth(username, password string) string { fieldValue := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(fieldValue)) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_docker_test.go000066400000000000000000000176401504711711200336070ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "encoding/json" "fmt" "os" "testing" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestCreateSecretDockerRegistry(t *testing.T) { username, password, email, server := "test-user", "test-password", "test-user@example.org", "https://index.docker.io/v1/" secretData, err := handleDockerCfgJSONContent(username, password, email, server) if err != nil { t.Errorf("unexpected error: %v", err) } secretDataNoEmail, err := handleDockerCfgJSONContent(username, password, "", server) if err != nil { t.Errorf("unexpected error: %v", err) } tests := map[string]struct { dockerRegistrySecretName string dockerUsername string dockerEmail string dockerPassword string dockerServer string appendHash bool expected *corev1.Secret expectErr bool }{ "create_secret_docker_registry_with_email": { dockerRegistrySecretName: "foo", dockerUsername: username, dockerPassword: password, dockerEmail: email, dockerServer: server, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: secretData, }, }, expectErr: false, }, "create_secret_docker_registry_with_email_hash": { dockerRegistrySecretName: "foo", dockerUsername: username, dockerPassword: password, dockerEmail: email, dockerServer: server, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-548cm7fgdh", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: secretData, }, }, expectErr: false, }, "create_secret_docker_registry_without_email": { dockerRegistrySecretName: "foo", dockerUsername: username, dockerPassword: password, dockerEmail: "", dockerServer: server, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: secretDataNoEmail, }, }, expectErr: false, }, "create_secret_docker_registry_without_email_hash": { dockerRegistrySecretName: "foo", dockerUsername: username, dockerPassword: password, dockerEmail: "", dockerServer: server, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-bff5bt4f92", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: secretDataNoEmail, }, }, expectErr: false, }, "create_invalid_secret_docker_registry_without_username": { dockerRegistrySecretName: "foo", dockerPassword: password, dockerEmail: "", dockerServer: server, expectErr: true, }, "create_invalid_secret_docker_registry_without_password": { dockerRegistrySecretName: "foo", dockerUsername: username, dockerEmail: "", dockerServer: server, expectErr: true, }, "create_invalid_secret_docker_registry_without_server": { dockerRegistrySecretName: "foo", dockerUsername: username, dockerPassword: password, dockerEmail: "", expectErr: true, }, } // Run all the tests for name, test := range tests { t.Run(name, func(t *testing.T) { var secretDockerRegistry *corev1.Secret = nil secretDockerRegistryOptions := CreateSecretDockerRegistryOptions{ Name: test.dockerRegistrySecretName, Username: test.dockerUsername, Email: test.dockerEmail, Password: test.dockerPassword, Server: test.dockerServer, AppendHash: test.appendHash, } err := secretDockerRegistryOptions.Validate() if err == nil { secretDockerRegistry, err = secretDockerRegistryOptions.createSecretDockerRegistry() } if !test.expectErr && err != nil { t.Errorf("test %s, unexpected error: %v", name, err) } if test.expectErr && err == nil { t.Errorf("test %s was expecting an error but no error occurred", name) } if !apiequality.Semantic.DeepEqual(secretDockerRegistry, test.expected) { t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, secretDockerRegistry) } }) } } func TestCreateSecretDockerRegistryFromFile(t *testing.T) { username, password, email, server := "test-user", "test-password", "test-user@example.org", "https://index.docker.io/v1/" secretData, err := handleDockerCfgJSONContent(username, password, email, server) if err != nil { t.Errorf("unexpected error: %v", err) } secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ corev1.DockerConfigJsonKey: secretData, }, } tests := map[string]struct { withKey bool expected *corev1.Secret }{ "create_secret_docker_registry_from_file_with_keyname": { withKey: true, expected: secret, }, "create_secret_docker_registry_from_file_without_keyname": { withKey: false, expected: secret, }, } // Run all the tests for name, test := range tests { t.Run(name, func(t *testing.T) { tmp, _ := os.MkdirTemp("", "input") defer func() { err := os.RemoveAll(tmp) if err != nil { t.Fatalf("Failed to teardown: %s", err) } }() dockerCfgFile := tmp + "/dockerconfig.json" err := os.WriteFile(dockerCfgFile, secretData, 0644) if err != nil { t.Errorf("unexpected error: %v", err) } tf := cmdtesting.NewTestFactory() defer tf.Cleanup() ioStreams, _, out, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateSecretDockerRegistry(tf, ioStreams) args := []string{"foo", "--dry-run=client", "-ojson"} if test.withKey { args = append(args, fmt.Sprintf("--from-file=%s=%s", corev1.DockerConfigJsonKey, dockerCfgFile)) } else { args = append(args, fmt.Sprintf("--from-file=%s", dockerCfgFile)) } cmd.SetArgs(args) err = cmd.Execute() if err != nil { t.Errorf("unexpected error: %v", err) } got := &corev1.Secret{} err = json.Unmarshal(out.Bytes(), got) if err != nil { t.Errorf("unexpected error: %v", err) } if !apiequality.Semantic.DeepEqual(got, test.expected) { t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, got) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_test.go000066400000000000000000000424611504711711200322570ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "os" "testing" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateSecretObject(t *testing.T) { secretObject := newSecretObj("foo", "foo-namespace", corev1.SecretTypeDockerConfigJson) expectedSecretObject := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "foo-namespace", }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{}, } t.Run("Creating a Secret Object", func(t *testing.T) { if !apiequality.Semantic.DeepEqual(secretObject, expectedSecretObject) { t.Errorf("expected:\n%#v\ngot:\n%#v", secretObject, expectedSecretObject) } }) } func TestCreateSecretGeneric(t *testing.T) { tests := map[string]struct { secretName string secretType string fromLiteral []string fromFile []string fromEnvFile []string appendHash bool setup func(t *testing.T, secretGenericOptions *CreateSecretOptions) func() expected *corev1.Secret expectErr string }{ "create_secret_foo": { secretName: "foo", expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string][]byte{}, }, }, "create_secret_foo_hash": { secretName: "foo", appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-949tdgdkgg", }, Data: map[string][]byte{}, }, }, "create_secret_foo_type": { secretName: "foo", secretType: "my-type", expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string][]byte{}, Type: "my-type", }, }, "create_secret_foo_type_hash": { secretName: "foo", secretType: "my-type", appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-dg474f9t76", }, Data: map[string][]byte{}, Type: "my-type", }, }, "create_secret_foo_two_literal": { secretName: "foo", fromLiteral: []string{"key1=value1", "key2=value2"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string][]byte{ "key1": []byte("value1"), "key2": []byte("value2"), }, }, }, "create_secret_foo_two_literal_hash": { secretName: "foo", fromLiteral: []string{"key1=value1", "key2=value2"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-tf72c228m4", }, Data: map[string][]byte{ "key1": []byte("value1"), "key2": []byte("value2"), }, }, }, "create_secret_foo_key1_=value1": { secretName: "foo", fromLiteral: []string{"key1==value1"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string][]byte{ "key1": []byte("=value1"), }, }, }, "create_secret_foo_key1_=value1_hash": { secretName: "foo", fromLiteral: []string{"key1==value1"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-fdcc8tkhh5", }, Data: map[string][]byte{ "key1": []byte("=value1"), }, }, }, "create_secret_foo_from_file_foo1_foo2_secret": { secretName: "foo", setup: setupSecretBinaryFile([]byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}), fromFile: []string{"foo1", "foo2"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string][]byte{ "foo1": []byte("hello world"), "foo2": []byte("hello world"), }, }, }, "create_secret_foo_from_file_foo1_foo2_hash": { secretName: "foo", setup: setupSecretBinaryFile([]byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}), fromFile: []string{"foo1", "foo2"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-hbkh2cdb57", }, Data: map[string][]byte{ "foo1": []byte("hello world"), "foo2": []byte("hello world"), }, }, }, "create_secret_foo_from_file_foo1_foo2_and": { secretName: "foo", setup: setupSecretBinaryFile([]byte{0xff, 0xfd}), fromFile: []string{"foo1", "foo2"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Data: map[string][]byte{ "foo1": {0xff, 0xfd}, "foo2": {0xff, 0xfd}, }, }, }, "create_secret_foo_from_file_foo1_foo2_and_hash": { secretName: "foo", setup: setupSecretBinaryFile([]byte{0xff, 0xfd}), fromFile: []string{"foo1", "foo2"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-mkhg4ktk4d", }, Data: map[string][]byte{ "foo1": {0xff, 0xfd}, "foo2": {0xff, 0xfd}, }, }, }, "create_secret_valid_env_from_env_file": { secretName: "valid_env", setup: setupSecretEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}}), fromEnvFile: []string{"file.env"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "valid_env", }, Data: map[string][]byte{ "key1": []byte("value1"), "key2": []byte("value2"), }, }, }, "create_secret_valid_env_from_env_file_hash": { secretName: "valid_env", setup: setupSecretEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}}), fromEnvFile: []string{"file.env"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "valid_env-bkb2m2965h", }, Data: map[string][]byte{ "key1": []byte("value1"), "key2": []byte("value2"), }, }, }, "create_two_secret_valid_env_from_env_file": { secretName: "two_valid_env", setup: setupSecretEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}, {"key3=value3"}}), fromEnvFile: []string{"file1.env", "file2.env"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "two_valid_env", }, Data: map[string][]byte{ "key1": []byte("value1"), "key2": []byte("value2"), "key3": []byte("value3"), }, }, }, "create_two_secret_valid_env_from_env_file_hash": { secretName: "two_valid_env", setup: setupSecretEnvFile([][]string{{"key1=value1", "#", "", "key2=value2"}, {"key3=value3"}}), fromEnvFile: []string{"file1.env", "file2.env"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "two_valid_env-gd56gct5cf", }, Data: map[string][]byte{ "key1": []byte("value1"), "key2": []byte("value2"), "key3": []byte("value3"), }, }, }, "create_secret_get_env_from_env_file": { secretName: "get_env", setup: func() func(t *testing.T, secretGenericOptions *CreateSecretOptions) func() { t.Setenv("g_key1", "1") t.Setenv("g_key2", "2") return setupSecretEnvFile([][]string{{"g_key1", "g_key2="}}) }(), fromEnvFile: []string{"file.env"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "get_env", }, Data: map[string][]byte{ "g_key1": []byte("1"), "g_key2": []byte(""), }, }, }, "create_secret_get_env_from_env_file_hash": { secretName: "get_env", setup: func() func(t *testing.T, secretGenericOptions *CreateSecretOptions) func() { t.Setenv("g_key1", "1") t.Setenv("g_key2", "2") return setupSecretEnvFile([][]string{{"g_key1", "g_key2="}}) }(), fromEnvFile: []string{"file.env"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "get_env-68mt8f2kkt", }, Data: map[string][]byte{ "g_key1": []byte("1"), "g_key2": []byte(""), }, }, }, "create_secret_value_with_space_from_env_file": { secretName: "value_with_space", setup: setupSecretEnvFile([][]string{{" key1= value1"}}), fromEnvFile: []string{"file.env"}, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "value_with_space", }, Data: map[string][]byte{ "key1": []byte(" value1"), }, }, }, "create_secret_value_with_space_from_env_file_hash": { secretName: "valid_with_space", setup: setupSecretEnvFile([][]string{{" key1= value1"}}), fromEnvFile: []string{"file.env"}, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "valid_with_space-bhkb4gfck6", }, Data: map[string][]byte{ "key1": []byte(" value1"), }, }, }, "create_invalid_secret_filepath_contains_=": { secretName: "foo", fromFile: []string{"key1=/file=2"}, expectErr: `key names or file paths cannot contain '='`, }, "create_invalid_secret_filepath_key_contains_=": { secretName: "foo", fromFile: []string{"=key=/file1"}, expectErr: `key names or file paths cannot contain '='`, }, "create_invalid_secret_literal_key_contains_=": { secretName: "foo", fromLiteral: []string{"=key=value1"}, expectErr: `invalid literal source =key=value1, expected key=value`, }, "create_invalid_secret_literal_key_with_invalid_character": { secretName: "foo", fromLiteral: []string{"key#1=value1"}, expectErr: `"key#1" is not valid key name for a Secret a valid config key must consist of alphanumeric characters, '-', '_' or '.' (e.g. 'key.name', or 'KEY_NAME', or 'key-name', regex used for validation is '[-._a-zA-Z0-9]+')`, }, "create_invalid_secret_env_key_contains_#": { secretName: "invalid_key", setup: setupSecretEnvFile([][]string{{"key#1=value1"}}), fromEnvFile: []string{"file.env"}, expectErr: `"key#1" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*')`, }, "create_invalid_secret_env_key_start_with_digit": { secretName: "invalid_key", setup: setupSecretEnvFile([][]string{{"1key=value1"}}), fromEnvFile: []string{"file.env"}, expectErr: `"1key" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*')`, }, "create_invalid_secret_env_key_with_invalid_character": { secretName: "invalid_key", setup: setupSecretEnvFile([][]string{{"key@=value1"}}), fromEnvFile: []string{"file.env"}, expectErr: `"key@" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*')`, }, "create_invalid_secret_duplicate_key1": { secretName: "foo", fromLiteral: []string{"key1=value1", "key1=value2"}, expectErr: `cannot add key key1, another key by that name already exists`, }, "create_invalid_secret_no_file": { secretName: "foo", fromFile: []string{"key1=/file1"}, expectErr: `error reading /file1: no such file or directory`, }, "create_invalid_secret_invalid_literal": { secretName: "foo", fromLiteral: []string{"key1value1"}, expectErr: `invalid literal source key1value1, expected key=value`, }, "create_invalid_secret_invalid_filepath": { secretName: "foo", fromFile: []string{"key1==file1"}, expectErr: `key names or file paths cannot contain '='`, }, "create_invalid_secret_no_name": { expectErr: `name must be specified`, }, "create_invalid_secret_too_many_args": { secretName: "too_many_args", fromFile: []string{"key1=/file1"}, fromEnvFile: []string{"foo"}, expectErr: `from-env-file cannot be combined with from-file or from-literal`, }, "create_invalid_secret_too_many_args_1": { secretName: "too_many_args_1", fromLiteral: []string{"key1=value1"}, fromEnvFile: []string{"foo"}, expectErr: `from-env-file cannot be combined with from-file or from-literal`, }, "create_invalid_secret_too_many_args_2": { secretName: "too_many_args_2", fromFile: []string{"key1=/file1"}, fromLiteral: []string{"key1=value1"}, fromEnvFile: []string{"foo"}, expectErr: `from-env-file cannot be combined with from-file or from-literal`, }, } // run all the tests for name, test := range tests { t.Run(name, func(t *testing.T) { var secret *corev1.Secret = nil secretOptions := CreateSecretOptions{ Name: test.secretName, Type: test.secretType, AppendHash: test.appendHash, FileSources: test.fromFile, LiteralSources: test.fromLiteral, EnvFileSources: test.fromEnvFile, } if test.setup != nil { if teardown := test.setup(t, &secretOptions); teardown != nil { defer teardown() } } err := secretOptions.Validate() if err == nil { secret, err = secretOptions.createSecret() } if test.expectErr == "" { require.NoError(t, err) if !apiequality.Semantic.DeepEqual(secret, test.expected) { t.Errorf("\nexpected:\n%#v\ngot:\n%#v", test.expected, secret) } } else { require.Error(t, err) require.EqualError(t, err, test.expectErr) } }) } } func setupSecretEnvFile(lines [][]string) func(*testing.T, *CreateSecretOptions) func() { return func(t *testing.T, secretOptions *CreateSecretOptions) func() { files := []*os.File{} filenames := secretOptions.EnvFileSources for _, filename := range filenames { file, err := os.CreateTemp("", filename) if err != nil { t.Errorf("unexpected error: %v", err) } files = append(files, file) } for i, f := range files { for _, l := range lines[i] { f.WriteString(l) f.WriteString("\r\n") } f.Close() secretOptions.EnvFileSources[i] = f.Name() } return func() { for _, f := range files { os.Remove(f.Name()) } } } } func setupSecretBinaryFile(data []byte) func(*testing.T, *CreateSecretOptions) func() { return func(t *testing.T, secretOptions *CreateSecretOptions) func() { tmp, _ := os.MkdirTemp("", "") files := secretOptions.FileSources for i, file := range files { f := tmp + "/" + file os.WriteFile(f, data, 0644) secretOptions.FileSources[i] = f } return func() { for _, file := range files { f := tmp + "/" + file os.RemoveAll(f) } } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_tls.go000066400000000000000000000166721504711711200321070ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "crypto/tls" "fmt" "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/hash" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( secretForTLSLong = templates.LongDesc(i18n.T(` Create a TLS secret from the given public/private key pair. The public/private key pair must exist beforehand. The public key certificate must be .PEM encoded and match the given private key.`)) secretForTLSExample = templates.Examples(i18n.T(` # Create a new TLS secret named tls-secret with the given key pair kubectl create secret tls tls-secret --cert=path/to/tls.crt --key=path/to/tls.key`)) ) // CreateSecretTLSOptions holds the options for 'create secret tls' sub command type CreateSecretTLSOptions struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // Name is the name of this TLS secret. Name string // Key is the path to the user's private key. Key string // Cert is the path to the user's public key certificate. Cert string // AppendHash; if true, derive a hash from the Secret and append it to the name AppendHash bool FieldManager string CreateAnnotation bool Namespace string EnforceNamespace bool Client corev1client.CoreV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewSecretTLSOptions creates a new *CreateSecretTLSOptions with default value func NewSecretTLSOptions(ioStrems genericiooptions.IOStreams) *CreateSecretTLSOptions { return &CreateSecretTLSOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStrems, } } // NewCmdCreateSecretTLS is a macro command for creating secrets to work with TLS client or server func NewCmdCreateSecretTLS(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewSecretTLSOptions(ioStreams) cmd := &cobra.Command{ Use: "tls NAME --cert=path/to/cert/file --key=path/to/key/file [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a TLS secret"), Long: secretForTLSLong, Example: secretForTLSExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.Cert, "cert", o.Cert, i18n.T("Path to PEM encoded public key certificate.")) cmd.Flags().StringVar(&o.Key, "key", o.Key, i18n.T("Path to private key associated with given certificate.")) cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete loads data from the command line environment func (o *CreateSecretTLSOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = corev1client.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate checks if CreateSecretTLSOptions hass sufficient value to run func (o *CreateSecretTLSOptions) Validate() error { // TODO: This is not strictly necessary. We can generate a self signed cert // if no key/cert is given. The only requirement is that we either get both // or none. See test/e2e/ingress_utils for self signed cert generation. if len(o.Key) == 0 || len(o.Cert) == 0 { return fmt.Errorf("key and cert must be specified") } return nil } // Run calls createSecretTLS which will create secretTLS based on CreateSecretTLSOptions // and makes an API call to the server func (o *CreateSecretTLSOptions) Run() error { secretTLS, err := o.createSecretTLS() if err != nil { return err } err = util.CreateOrUpdateAnnotation(o.CreateAnnotation, secretTLS, scheme.DefaultJSONEncoder()) if err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } secretTLS, err = o.Client.Secrets(o.Namespace).Create(context.TODO(), secretTLS, createOptions) if err != nil { return fmt.Errorf("failed to create secret %v", err) } } return o.PrintObj(secretTLS) } // createSecretTLS fills in key value pair from the information given in // CreateSecretTLSOptions into *corev1.Secret func (o *CreateSecretTLSOptions) createSecretTLS() (*corev1.Secret, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } tlsCert, err := readFile(o.Cert) if err != nil { return nil, err } tlsKey, err := readFile(o.Key) if err != nil { return nil, err } if _, err := tls.X509KeyPair(tlsCert, tlsKey); err != nil { return nil, err } // TODO: Add more validation. // 1. If the certificate contains intermediates, it is a valid chain. // 2. Format etc. secretTLS := newSecretObj(o.Name, namespace, corev1.SecretTypeTLS) secretTLS.Data[corev1.TLSCertKey] = []byte(tlsCert) secretTLS.Data[corev1.TLSPrivateKeyKey] = []byte(tlsKey) if o.AppendHash { hash, err := hash.SecretHash(secretTLS) if err != nil { return nil, err } secretTLS.Name = fmt.Sprintf("%s-%s", secretTLS.Name, hash) } return secretTLS, nil } // readFile just reads a file into a byte array. func readFile(file string) ([]byte, error) { b, err := os.ReadFile(file) if err != nil { return []byte{}, fmt.Errorf("Cannot read file %v, %v", file, err) } return b, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_tls_test.go000066400000000000000000000157151504711711200331430ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "os" "path/filepath" "testing" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var rsaCertPEM = `-----BEGIN CERTIFICATE----- MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V -----END CERTIFICATE----- ` var rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY----- MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G 6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g== -----END RSA PRIVATE KEY----- ` const mismatchRSAKeyPEM = `-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/665h55hWD4V2 kiQ+B/G9NNfBw69eBibEhI9vWkPUyn36GO2r3HPtRE63wBfFpV486ns9DoZnnAYE JaGjVNCCqS5tQyMBWp843o66KBrEgBpuddChigvyul33FhD1ImFnN+Vy0ajOJ+1/ Zai28zBXWbxCWEbqz7s8e2UsPlBd0Caj4gcd32yD2BwiHqzB8odToWRUT7l+pS8R qA1BruQvtjEIrcoWVlE170ZYe7+Apm96A+WvtVRkozPynxHF8SuEiw4hAh0lXR6b 4zZz4tZVV8ev2HpffveV/68GiCyeFDbglqd4sZ/Iga/rwu7bVY/BzFApHwu2hmmV XLnaa3uVAgMBAAECggEAG+kvnCdtPR7Wvw6z3J2VJ3oW4qQNzfPBEZVhssUC1mB4 f7W+Yt8VsOzdMdXq3yCUmvFS6OdC3rCPI21Bm5pLFKV8DgHUhm7idwfO4/3PHsKu lV/m7odAA5Xc8oEwCCZu2e8EHHWnQgwGex+SsMCfSCTRvyhNb/qz9TDQ3uVVFL9e 9a4OKqZl/GlRspJSuXhy+RSVulw9NjeX1VRjIbhqpdXAmQNXgShA+gZSQh8T/tgv XQYsMtg+FUDvcunJQf4OW5BY7IenYBV/GvsnJU8L7oD0wjNSAwe/iLKqV/NpYhre QR4DsGnmoRYlUlHdHFTTJpReDjWm+vH3T756yDdFAQKBgQD2/sP5dM/aEW7Z1TgS TG4ts1t8Rhe9escHxKZQR81dfOxBeCJMBDm6ySfR8rvyUM4VsogxBL/RhRQXsjJM 7wN08MhdiXG0J5yy/oNo8W6euD8m8Mk1UmqcZjSgV4vA7zQkvkr6DRJdybKsT9mE jouEwev8sceS6iBpPw/+Ws8z1QKBgQDG6uYHMfMcS844xKQQWhargdN2XBzeG6TV YXfNFstNpD84d9zIbpG/AKJF8fKrseUhXkJhkDjFGJTriD3QQsntOFaDOrHMnveV zGzvC4OTFUUFHe0SVJ0HuLf8YCHoZ+DXEeCKCN6zBXnUue+bt3NvLOf2yN5o9kYx SIa8O1vIwQKBgEdONXWG65qg/ceVbqKZvhUjen3eHmxtTZhIhVsX34nlzq73567a aXArMnvB/9Bs05IgAIFmRZpPOQW+RBdByVWxTabzTwgbh3mFUJqzWKQpvNGZIf1q 1axhNUA1BfulEwCojyyxKWQ6HoLwanOCU3T4JxDEokEfpku8EPn1bWwhAoGAAN8A eOGYHfSbB5ac3VF3rfKYmXkXy0U1uJV/r888vq9Mc5PazKnnS33WOBYyKNxTk4zV H5ZBGWPdKxbipmnUdox7nIGCS9IaZXaKt5VGUzuRnM8fvafPNDxz2dAV9e2Wh3qV kCUvzHrmqK7TxMvN3pvEvEju6GjDr+2QYXylD0ECgYAGK5r+y+EhtKkYFLeYReUt znvSsWq+JCQH/cmtZLaVOldCaMRL625hSl3XPPcMIHE14xi3d4njoXWzvzPcg8L6 vNXk3GiNldACS+vwk4CwEqe5YlZRm5doD07wIdsg2zRlnKsnXNM152OwgmcchDul rLTt0TTazzwBCgCD0Jkoqg== -----END PRIVATE KEY-----` func TestCreateSecretTLS(t *testing.T) { validCertTmpDir := t.TempDir() validKeyPath, validCertPath := writeKeyPair(validCertTmpDir, rsaKeyPEM, rsaCertPEM, t) invalidCertTmpDir := t.TempDir() invalidKeyPath, invalidCertPath := writeKeyPair(invalidCertTmpDir, "test", "test", t) mismatchCertTmpDir := t.TempDir() mismatchKeyPath, mismatchCertPath := writeKeyPair(mismatchCertTmpDir, rsaKeyPEM, mismatchRSAKeyPEM, t) tests := map[string]struct { tlsSecretName string tlsKey string tlsCert string appendHash bool expected *corev1.Secret expectErr bool }{ "create_secret_tls": { tlsSecretName: "foo", tlsKey: validKeyPath, tlsCert: validCertPath, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSPrivateKeyKey: []byte(rsaKeyPEM), corev1.TLSCertKey: []byte(rsaCertPEM), }, }, expectErr: false, }, "create_secret_tls_hash": { tlsSecretName: "foo", tlsKey: validKeyPath, tlsCert: validCertPath, appendHash: true, expected: &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-272h6tt825", }, Type: corev1.SecretTypeTLS, Data: map[string][]byte{ corev1.TLSPrivateKeyKey: []byte(rsaKeyPEM), corev1.TLSCertKey: []byte(rsaCertPEM), }, }, expectErr: false, }, "create_secret_invalid_tls": { tlsSecretName: "foo", tlsKey: invalidKeyPath, tlsCert: invalidCertPath, expectErr: true, }, "create_secret_mismatch_tls": { tlsSecretName: "foo", tlsKey: mismatchKeyPath, tlsCert: mismatchCertPath, expectErr: true, }, "create_invalid_filepath_and_certpath_secret_tls": { tlsSecretName: "foo", tlsKey: "testKeyPath", tlsCert: "testCertPath", expectErr: true, }, } // Run all the tests for name, test := range tests { t.Run(name, func(t *testing.T) { secretTLSOptions := CreateSecretTLSOptions{ Name: test.tlsSecretName, Key: test.tlsKey, Cert: test.tlsCert, AppendHash: test.appendHash, } secretTLS, err := secretTLSOptions.createSecretTLS() if !test.expectErr && err != nil { t.Errorf("test %s, unexpected error: %v", name, err) } if test.expectErr && err == nil { t.Errorf("test %s was expecting an error but no error occurred", name) } if !apiequality.Semantic.DeepEqual(secretTLS, test.expected) { t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, secretTLS) } }) } } func write(path, contents string, t *testing.T) { f, err := os.Create(path) if err != nil { t.Fatalf("Failed to create %v.", path) } defer f.Close() _, err = f.WriteString(contents) if err != nil { t.Fatalf("Failed to write to %v.", path) } } func writeKeyPair(tmpDirPath, key, cert string, t *testing.T) (keyPath, certPath string) { keyPath = filepath.Join(tmpDirPath, "tls.key") certPath = filepath.Join(tmpDirPath, "tls.cert") write(keyPath, key, t) write(certPath, cert, t) return } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service.go000066400000000000000000000321631504711711200313710ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "errors" "fmt" "strconv" "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" utilsnet "k8s.io/utils/net" ) // NewCmdCreateService is a macro command to create a new service func NewCmdCreateService(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "service", Aliases: []string{"svc"}, Short: i18n.T("Create a service using a specified subcommand"), Long: i18n.T("Create a service using a specified subcommand."), Run: cmdutil.DefaultSubCommandRun(ioStreams.ErrOut), } cmd.AddCommand(NewCmdCreateServiceClusterIP(f, ioStreams)) cmd.AddCommand(NewCmdCreateServiceNodePort(f, ioStreams)) cmd.AddCommand(NewCmdCreateServiceLoadBalancer(f, ioStreams)) cmd.AddCommand(NewCmdCreateServiceExternalName(f, ioStreams)) return cmd } // ServiceOptions holds the options for 'create service' sub command type ServiceOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string TCP []string Type corev1.ServiceType ClusterIP string NodePort int ExternalName string FieldManager string CreateAnnotation bool Namespace string EnforceNamespace bool Client corev1client.CoreV1Interface DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string genericiooptions.IOStreams } // NewServiceOptions creates a ServiceOptions struct func NewServiceOptions(ioStreams genericiooptions.IOStreams, serviceType corev1.ServiceType) *ServiceOptions { return &ServiceOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, Type: serviceType, } } // Complete completes all the required options func (o *ServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = corev1client.NewForConfig(clientConfig) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate if the options are valid func (o *ServiceOptions) Validate() error { if o.ClusterIP == corev1.ClusterIPNone && o.Type != corev1.ServiceTypeClusterIP { return fmt.Errorf("ClusterIP=None can only be used with ClusterIP service type") } if o.ClusterIP != corev1.ClusterIPNone && len(o.TCP) == 0 && o.Type != corev1.ServiceTypeExternalName { return fmt.Errorf("at least one tcp port specifier must be provided") } if o.Type == corev1.ServiceTypeExternalName { if errs := validation.IsDNS1123Subdomain(o.ExternalName); len(errs) != 0 { return fmt.Errorf("invalid service external name %s", o.ExternalName) } } return nil } func (o *ServiceOptions) createService() (*corev1.Service, error) { ports := []corev1.ServicePort{} for _, tcpString := range o.TCP { port, targetPort, err := parsePorts(tcpString) if err != nil { return nil, err } portName := strings.Replace(tcpString, ":", "-", -1) ports = append(ports, corev1.ServicePort{ Name: portName, Port: port, TargetPort: targetPort, Protocol: corev1.Protocol("TCP"), NodePort: int32(o.NodePort), }) } // setup default label and selector labels := map[string]string{} labels["app"] = o.Name selector := map[string]string{} selector["app"] = o.Name namespace := "" if o.EnforceNamespace { namespace = o.Namespace } service := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Labels: labels, Namespace: namespace, }, Spec: corev1.ServiceSpec{ Type: o.Type, Selector: selector, Ports: ports, ExternalName: o.ExternalName, }, } if len(o.ClusterIP) > 0 { service.Spec.ClusterIP = o.ClusterIP } return &service, nil } // Run the service command func (o *ServiceOptions) Run() error { service, err := o.createService() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, service, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } var err error service, err = o.Client.Services(o.Namespace).Create(context.TODO(), service, createOptions) if err != nil { return fmt.Errorf("failed to create %s service: %v", o.Type, err) } } return o.PrintObj(service) } var ( serviceClusterIPLong = templates.LongDesc(i18n.T(` Create a ClusterIP service with the specified name.`)) serviceClusterIPExample = templates.Examples(i18n.T(` # Create a new ClusterIP service named my-cs kubectl create service clusterip my-cs --tcp=5678:8080 # Create a new ClusterIP service named my-cs (in headless mode) kubectl create service clusterip my-cs --clusterip="None"`)) ) // NewCmdCreateServiceClusterIP is a command to create a ClusterIP service func NewCmdCreateServiceClusterIP(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewServiceOptions(ioStreams, corev1.ServiceTypeClusterIP) cmd := &cobra.Command{ Use: "clusterip NAME [--tcp=:] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a ClusterIP service"), Long: serviceClusterIPLong, Example: serviceClusterIPExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") cmd.Flags().StringVar(&o.ClusterIP, "clusterip", o.ClusterIP, i18n.T("Assign your own ClusterIP or set to 'None' for a 'headless' service (no loadbalancing).")) cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") cmdutil.AddDryRunFlag(cmd) return cmd } var ( serviceNodePortLong = templates.LongDesc(i18n.T(` Create a NodePort service with the specified name.`)) serviceNodePortExample = templates.Examples(i18n.T(` # Create a new NodePort service named my-ns kubectl create service nodeport my-ns --tcp=5678:8080`)) ) // NewCmdCreateServiceNodePort is a macro command for creating a NodePort service func NewCmdCreateServiceNodePort(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewServiceOptions(ioStreams, corev1.ServiceTypeNodePort) cmd := &cobra.Command{ Use: "nodeport NAME [--tcp=port:targetPort] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a NodePort service"), Long: serviceNodePortLong, Example: serviceNodePortExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmd.Flags().IntVar(&o.NodePort, "node-port", o.NodePort, "Port used to expose the service on each node in a cluster.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") cmdutil.AddDryRunFlag(cmd) return cmd } var ( serviceLoadBalancerLong = templates.LongDesc(i18n.T(` Create a LoadBalancer service with the specified name.`)) serviceLoadBalancerExample = templates.Examples(i18n.T(` # Create a new LoadBalancer service named my-lbs kubectl create service loadbalancer my-lbs --tcp=5678:8080`)) ) // NewCmdCreateServiceLoadBalancer is a macro command for creating a LoadBalancer service func NewCmdCreateServiceLoadBalancer(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewServiceOptions(ioStreams, corev1.ServiceTypeLoadBalancer) cmd := &cobra.Command{ Use: "loadbalancer NAME [--tcp=port:targetPort] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create a LoadBalancer service"), Long: serviceLoadBalancerLong, Example: serviceLoadBalancerExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") cmdutil.AddDryRunFlag(cmd) return cmd } var ( serviceExternalNameLong = templates.LongDesc(i18n.T(` Create an ExternalName service with the specified name. ExternalName service references to an external DNS address instead of only pods, which will allow application authors to reference services that exist off platform, on other clusters, or locally.`)) serviceExternalNameExample = templates.Examples(i18n.T(` # Create a new ExternalName service named my-ns kubectl create service externalname my-ns --external-name bar.com`)) ) // NewCmdCreateServiceExternalName is a macro command for creating an ExternalName service func NewCmdCreateServiceExternalName(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewServiceOptions(ioStreams, corev1.ServiceTypeExternalName) cmd := &cobra.Command{ Use: "externalname NAME --external-name external.name [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Create an ExternalName service"), Long: serviceExternalNameLong, Example: serviceExternalNameExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") cmd.Flags().StringVar(&o.ExternalName, "external-name", o.ExternalName, i18n.T("External name of service")) cmd.MarkFlagRequired("external-name") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") cmdutil.AddDryRunFlag(cmd) return cmd } func parsePorts(portString string) (int32, intstr.IntOrString, error) { portStringSlice := strings.Split(portString, ":") port, err := utilsnet.ParsePort(portStringSlice[0], true) if err != nil { return 0, intstr.FromInt32(0), err } if len(portStringSlice) == 1 { port32 := int32(port) return port32, intstr.FromInt32(port32), nil } var targetPort intstr.IntOrString if portNum, err := strconv.Atoi(portStringSlice[1]); err != nil { if errs := validation.IsValidPortName(portStringSlice[1]); len(errs) != 0 { return 0, intstr.FromInt32(0), errors.New(strings.Join(errs, ",")) } targetPort = intstr.FromString(portStringSlice[1]) } else { if errs := validation.IsValidPortNum(portNum); len(errs) != 0 { return 0, intstr.FromInt32(0), errors.New(strings.Join(errs, ",")) } targetPort = intstr.FromInt32(int32(portNum)) } return int32(port), targetPort, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service_test.go000066400000000000000000000227441504711711200324340ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "testing" restclient "k8s.io/client-go/rest" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" v1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericiooptions" ) func TestCreateServices(t *testing.T) { tests := []struct { name string serviceType v1.ServiceType tcp []string clusterip string externalName string nodeport int expected *v1.Service expectErr bool }{ { name: "clusterip-ok", tcp: []string{"456", "321:908"}, clusterip: "", serviceType: v1.ServiceTypeClusterIP, expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "clusterip-ok", Labels: map[string]string{"app": "clusterip-ok"}, }, Spec: v1.ServiceSpec{Type: "ClusterIP", Ports: []v1.ServicePort{{Name: "456", Protocol: "TCP", Port: 456, TargetPort: intstr.IntOrString{Type: 0, IntVal: 456, StrVal: ""}, NodePort: 0}, {Name: "321-908", Protocol: "TCP", Port: 321, TargetPort: intstr.IntOrString{Type: 0, IntVal: 908, StrVal: ""}, NodePort: 0}}, Selector: map[string]string{"app": "clusterip-ok"}, ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, }, expectErr: false, }, { name: "clusterip-missing", serviceType: v1.ServiceTypeClusterIP, expectErr: true, }, { name: "clusterip-none-wrong-type", tcp: []string{}, clusterip: "None", serviceType: v1.ServiceTypeNodePort, expectErr: true, }, { name: "clusterip-none-ok", tcp: []string{}, clusterip: "None", serviceType: v1.ServiceTypeClusterIP, expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "clusterip-none-ok", Labels: map[string]string{"app": "clusterip-none-ok"}, }, Spec: v1.ServiceSpec{Type: "ClusterIP", Ports: []v1.ServicePort{}, Selector: map[string]string{"app": "clusterip-none-ok"}, ClusterIP: "None", ExternalIPs: []string(nil), LoadBalancerIP: ""}, }, expectErr: false, }, { name: "clusterip-none-and-port-mapping", tcp: []string{"456:9898"}, clusterip: "None", serviceType: v1.ServiceTypeClusterIP, expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "clusterip-none-and-port-mapping", Labels: map[string]string{"app": "clusterip-none-and-port-mapping"}, }, Spec: v1.ServiceSpec{Type: "ClusterIP", Ports: []v1.ServicePort{{Name: "456-9898", Protocol: "TCP", Port: 456, TargetPort: intstr.IntOrString{Type: 0, IntVal: 9898, StrVal: ""}, NodePort: 0}}, Selector: map[string]string{"app": "clusterip-none-and-port-mapping"}, ClusterIP: "None", ExternalIPs: []string(nil), LoadBalancerIP: ""}, }, expectErr: false, }, { name: "loadbalancer-ok", tcp: []string{"456:9898"}, clusterip: "", serviceType: v1.ServiceTypeLoadBalancer, expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "loadbalancer-ok", Labels: map[string]string{"app": "loadbalancer-ok"}, }, Spec: v1.ServiceSpec{Type: "LoadBalancer", Ports: []v1.ServicePort{{Name: "456-9898", Protocol: "TCP", Port: 456, TargetPort: intstr.IntOrString{Type: 0, IntVal: 9898, StrVal: ""}, NodePort: 0}}, Selector: map[string]string{"app": "loadbalancer-ok"}, ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, }, expectErr: false, }, { name: "invalid-port", tcp: []string{"65536"}, clusterip: "None", serviceType: v1.ServiceTypeClusterIP, expectErr: true, }, { name: "invalid-port-mapping", tcp: []string{"8080:-abc"}, clusterip: "None", serviceType: v1.ServiceTypeClusterIP, expectErr: true, }, { expectErr: true, }, { name: "validate-ok", serviceType: v1.ServiceTypeClusterIP, tcp: []string{"123", "234:1234"}, clusterip: "", expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "validate-ok", Labels: map[string]string{"app": "validate-ok"}, }, Spec: v1.ServiceSpec{Type: "ClusterIP", Ports: []v1.ServicePort{ {Name: "123", Protocol: "TCP", Port: 123, TargetPort: intstr.IntOrString{Type: 0, IntVal: 123, StrVal: ""}, NodePort: 0}, {Name: "234-1234", Protocol: "TCP", Port: 234, TargetPort: intstr.IntOrString{Type: 0, IntVal: 1234, StrVal: ""}, NodePort: 0}, }, Selector: map[string]string{"app": "validate-ok"}, ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, }, expectErr: false, }, { name: "invalid-ClusterIPNone", serviceType: v1.ServiceTypeNodePort, tcp: []string{"123", "234:1234"}, clusterip: v1.ClusterIPNone, expectErr: true, }, { name: "TCP-none", serviceType: v1.ServiceTypeClusterIP, clusterip: "", expectErr: true, }, { name: "invalid-ExternalName", serviceType: v1.ServiceTypeExternalName, tcp: []string{"123", "234:1234"}, clusterip: "", externalName: "@oi:test", expectErr: true, }, { name: "externalName-ok", serviceType: v1.ServiceTypeExternalName, tcp: []string{"123", "234:1234"}, clusterip: "", externalName: "www.externalname.com", expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "externalName-ok", Labels: map[string]string{"app": "externalName-ok"}, }, Spec: v1.ServiceSpec{Type: "ExternalName", Ports: []v1.ServicePort{ {Name: "123", Protocol: "TCP", Port: 123, TargetPort: intstr.IntOrString{Type: 0, IntVal: 123, StrVal: ""}, NodePort: 0}, {Name: "234-1234", Protocol: "TCP", Port: 234, TargetPort: intstr.IntOrString{Type: 0, IntVal: 1234, StrVal: ""}, NodePort: 0}, }, Selector: map[string]string{"app": "externalName-ok"}, ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: "", ExternalName: "www.externalname.com"}, }, expectErr: false, }, { name: "my-node-port-service-ok", serviceType: v1.ServiceTypeNodePort, tcp: []string{"443:https", "30000:8000"}, clusterip: "", expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "my-node-port-service-ok", Labels: map[string]string{"app": "my-node-port-service-ok"}, }, Spec: v1.ServiceSpec{Type: "NodePort", Ports: []v1.ServicePort{ {Name: "443-https", Protocol: "TCP", Port: 443, TargetPort: intstr.IntOrString{Type: 1, IntVal: 0, StrVal: "https"}, NodePort: 0}, {Name: "30000-8000", Protocol: "TCP", Port: 30000, TargetPort: intstr.IntOrString{Type: 0, IntVal: 8000, StrVal: ""}, NodePort: 0}, }, Selector: map[string]string{"app": "my-node-port-service-ok"}, ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, }, expectErr: false, }, { name: "my-node-port-service-ok2", serviceType: v1.ServiceTypeNodePort, tcp: []string{"80:http"}, clusterip: "", nodeport: 4444, expected: &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "my-node-port-service-ok2", Labels: map[string]string{"app": "my-node-port-service-ok2"}, }, Spec: v1.ServiceSpec{Type: "NodePort", Ports: []v1.ServicePort{ {Name: "80-http", Protocol: "TCP", Port: 80, TargetPort: intstr.IntOrString{Type: 1, IntVal: 0, StrVal: "http"}, NodePort: 4444}, }, Selector: map[string]string{"app": "my-node-port-service-ok2"}, ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, }, expectErr: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { options := ServiceOptions{ Name: tc.name, Type: tc.serviceType, TCP: tc.tcp, ClusterIP: tc.clusterip, NodePort: tc.nodeport, ExternalName: tc.externalName, } var service *v1.Service err := options.Validate() if err == nil { service, err = options.createService() } if tc.expectErr && err == nil { t.Errorf("%s: expected an error, but createService passes.", tc.name) } if !tc.expectErr && err != nil { t.Errorf("%s: unexpected error: %v", tc.name, err) } if !apiequality.Semantic.DeepEqual(service, tc.expected) { t.Errorf("%s: expected:\n%#v\ngot:\n%#v", tc.name, tc.expected, service) } }) } } func TestCreateServiceWithNamespace(t *testing.T) { svcName := "test-service" ns := "test" tf := cmdtesting.NewTestFactory().WithNamespace(ns) defer tf.Cleanup() tf.ClientConfigVal = &restclient.Config{} ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateServiceClusterIP(tf, ioStreams) cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", "jsonpath={.metadata.namespace}") cmd.Flags().Set("clusterip", "None") cmd.Run(cmd, []string{svcName}) if buf.String() != ns { t.Errorf("expected output: %s, but got: %s", ns, buf.String()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_serviceaccount.go000066400000000000000000000133061504711711200327440ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" coreclient "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( serviceAccountLong = templates.LongDesc(i18n.T(` Create a service account with the specified name.`)) serviceAccountExample = templates.Examples(i18n.T(` # Create a new service account named my-service-account kubectl create serviceaccount my-service-account`)) ) // ServiceAccountOpts holds the options for 'create serviceaccount' sub command type ServiceAccountOpts struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // Name of resource being created Name string DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string CreateAnnotation bool FieldManager string Namespace string EnforceNamespace bool Mapper meta.RESTMapper Client *coreclient.CoreV1Client genericiooptions.IOStreams } // NewServiceAccountOpts creates a new *ServiceAccountOpts with sane defaults func NewServiceAccountOpts(ioStreams genericiooptions.IOStreams) *ServiceAccountOpts { return &ServiceAccountOpts{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateServiceAccount is a macro command to create a new service account func NewCmdCreateServiceAccount(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewServiceAccountOpts(ioStreams) cmd := &cobra.Command{ Use: "serviceaccount NAME [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Aliases: []string{"sa"}, Short: i18n.T("Create a service account with the specified name"), Long: serviceAccountLong, Example: serviceAccountExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the required options func (o *ServiceAccountOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } restConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = coreclient.NewForConfig(restConfig) if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } return nil } // Validate checks ServiceAccountOpts to see if there is sufficient information run the command. func (o *ServiceAccountOpts) Validate() error { if len(o.Name) == 0 { return fmt.Errorf("name must be specified") } return nil } // Run makes the api call to the server func (o *ServiceAccountOpts) Run() error { serviceAccount, err := o.createServiceAccount() if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, serviceAccount, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } createOptions.FieldValidation = o.ValidationDirective if o.DryRunStrategy == cmdutil.DryRunServer { createOptions.DryRun = []string{metav1.DryRunAll} } serviceAccount, err = o.Client.ServiceAccounts(o.Namespace).Create(context.TODO(), serviceAccount, createOptions) if err != nil { return fmt.Errorf("failed to create serviceaccount: %v", err) } } return o.PrintObj(serviceAccount) } func (o *ServiceAccountOpts) createServiceAccount() (*corev1.ServiceAccount, error) { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } serviceAccount := &corev1.ServiceAccount{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ServiceAccount"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Namespace: namespace, }, } serviceAccount.Name = o.Name return serviceAccount, nil } create_serviceaccount_test.go000066400000000000000000000030331504711711200337200ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "testing" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateServiceAccount(t *testing.T) { tests := map[string]struct { options *ServiceAccountOpts expected *corev1.ServiceAccount }{ "service account": { options: &ServiceAccountOpts{ Name: "my-service-account", }, expected: &corev1.ServiceAccount{ TypeMeta: metav1.TypeMeta{ Kind: "ServiceAccount", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-service-account", }, }, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { serviceAccount, err := tc.options.createServiceAccount() if err != nil { t.Errorf("unexpected error:\n%#v\n", err) return } if !apiequality.Semantic.DeepEqual(serviceAccount, tc.expected) { t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, serviceAccount) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_test.go000066400000000000000000000143541504711711200307120ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "net/http" "testing" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" ) func TestExtraArgsFail(t *testing.T) { cmdtesting.InitTestErrorHandler(t) f := cmdtesting.NewTestFactory() defer f.Cleanup() c := NewCmdCreate(f, genericiooptions.NewTestIOStreamsDiscard()) ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() options := NewCreateOptions(ioStreams) if options.Complete(f, c, []string{"rc"}) == nil { t.Errorf("unexpected non-error") } } func TestCreateObject(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() rc.Items[0].Name = "redis-master-controller" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreate(tf, ioStreams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response if buf.String() != "replicationcontroller/redis-master-controller\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestCreateMultipleObject(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, svc, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreate(tf, ioStreams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../testdata/frontend-service.yaml") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // Names should come from the REST response, NOT the files if buf.String() != "replicationcontroller/rc1\nservice/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestCreateDirectory(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() rc.Items[0].Name = "name" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreate(tf, ioStreams) cmd.Flags().Set("filename", "../../../testdata/replace/legacy") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/name\nreplicationcontroller/name\nreplicationcontroller/name\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestMissingFilenameError(t *testing.T) { var errStr string var exitCode int cmdutil.BehaviorOnFatal(func(str string, code int) { if errStr == "" { errStr = str exitCode = code } }) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreate(tf, ioStreams) cmd.Run(cmd, []string{}) if buf.Len() > 0 { t.Errorf("unexpected output: %s", buf.String()) } if len(errStr) == 0 { t.Errorf("unexpected non-error") } else if errStr != "error: must specify one of -f and -k" { t.Errorf("unexpected error: %s", errStr) } if exitCode != 1 { t.Errorf("unexpected exit code: %d", exitCode) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_token.go000066400000000000000000000214761504711711200310560ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/pflag" authenticationv1 "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" "k8s.io/utils/ptr" ) // TokenOptions is the data required to perform a token request operation. type TokenOptions struct { // PrintFlags holds options necessary for obtaining a printer PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error // Flags hold the parsed CLI flags. Flags *pflag.FlagSet // Name and namespace of service account to create a token for Name string Namespace string // BoundObjectKind is the kind of object to bind the token to. Optional. Can be Pod or Secret. BoundObjectKind string // BoundObjectName is the name of the object to bind the token to. Required if BoundObjectKind is set. BoundObjectName string // BoundObjectUID is the uid of the object to bind the token to. If unset, defaults to the current uid of the bound object. BoundObjectUID string // Audiences indicate the valid audiences for the requested token. If unset, defaults to the Kubernetes API server audiences. Audiences []string // Duration is the requested token lifetime. Optional. Duration time.Duration // CoreClient is the API client used to request the token. Required. CoreClient corev1client.CoreV1Interface // IOStreams are the output streams for the operation. Required. genericiooptions.IOStreams } var ( tokenLong = templates.LongDesc(`Request a service account token.`) tokenExample = templates.Examples(` # Request a token to authenticate to the kube-apiserver as the service account "myapp" in the current namespace kubectl create token myapp # Request a token for a service account in a custom namespace kubectl create token myapp --namespace myns # Request a token with a custom expiration kubectl create token myapp --duration 10m # Request a token with a custom audience kubectl create token myapp --audience https://example.com # Request a token bound to an instance of a Secret object kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret # Request a token bound to an instance of a Secret object with a specific UID kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc `) ) var boundObjectKinds = map[string]string{ "Pod": "v1", "Secret": "v1", "Node": "v1", } func NewTokenOpts(ioStreams genericiooptions.IOStreams) *TokenOptions { return &TokenOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateToken returns an initialized Command for 'create token' sub command func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewTokenOpts(ioStreams) cmd := &cobra.Command{ Use: "token SERVICE_ACCOUNT_NAME", DisableFlagsInUseLine: true, Short: "Request a service account token", Long: tokenLong, Example: tokenExample, ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "serviceaccount"), Run: func(cmd *cobra.Command, args []string) { if err := o.Complete(f, cmd, args); err != nil { cmdutil.CheckErr(err) return } if err := o.Validate(); err != nil { cmdutil.CheckErr(err) return } if err := o.Run(); err != nil { cmdutil.CheckErr(err) return } }, } o.PrintFlags.AddFlags(cmd) cmd.Flags().StringArrayVar(&o.Audiences, "audience", o.Audiences, "Audience of the requested token. If unset, defaults to requesting a token for use with the Kubernetes API server. May be repeated to request a token valid for multiple audiences.") cmd.Flags().DurationVar(&o.Duration, "duration", o.Duration, "Requested lifetime of the issued token. If not set or if set to 0, the lifetime will be determined by the server automatically. The server may return a token with a longer or shorter lifetime.") cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+ "Supported kinds are "+strings.Join(sets.List(sets.KeySet(boundObjectKinds)), ", ")+". "+ "If set, --bound-object-name must be provided.") cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+ "The token will expire when the object is deleted. "+ "Requires --bound-object-kind.") cmd.Flags().StringVar(&o.BoundObjectUID, "bound-object-uid", o.BoundObjectUID, "UID of an object to bind the token to. "+ "Requires --bound-object-kind and --bound-object-name. "+ "If unset, the UID of the existing object is used.") o.Flags = cmd.Flags() return cmd } // Complete completes all the required options func (o *TokenOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } client, err := f.KubernetesClientSet() if err != nil { return err } o.CoreClient = client.CoreV1() printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } return nil } // Validate makes sure provided values for TokenOptions are valid func (o *TokenOptions) Validate() error { if o.CoreClient == nil { return fmt.Errorf("no client provided") } if len(o.Name) == 0 { return fmt.Errorf("service account name is required") } if len(o.Namespace) == 0 { return fmt.Errorf("--namespace is required") } if o.Duration < 0 { return fmt.Errorf("--duration must be greater than or equal to 0") } if o.Duration%time.Second != 0 { return fmt.Errorf("--duration cannot be expressed in units less than seconds") } for _, aud := range o.Audiences { if len(aud) == 0 { return fmt.Errorf("--audience must not be an empty string") } } if len(o.BoundObjectKind) == 0 { if len(o.BoundObjectName) > 0 { return fmt.Errorf("--bound-object-name can only be set if --bound-object-kind is provided") } if len(o.BoundObjectUID) > 0 { return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided") } } else { if _, ok := boundObjectKinds[o.BoundObjectKind]; !ok { return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.List(sets.KeySet(boundObjectKinds)), ", ")) } if len(o.BoundObjectName) == 0 { return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided") } } return nil } // Run requests a token func (o *TokenOptions) Run() error { request := &authenticationv1.TokenRequest{ Spec: authenticationv1.TokenRequestSpec{ Audiences: o.Audiences, }, } if o.Duration > 0 { request.Spec.ExpirationSeconds = ptr.To(int64(o.Duration / time.Second)) } if len(o.BoundObjectKind) > 0 { request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{ Kind: o.BoundObjectKind, APIVersion: boundObjectKinds[o.BoundObjectKind], Name: o.BoundObjectName, UID: types.UID(o.BoundObjectUID), } } response, err := o.CoreClient.ServiceAccounts(o.Namespace).CreateToken(context.TODO(), o.Name, request, metav1.CreateOptions{}) if err != nil { return fmt.Errorf("failed to create token: %v", err) } if len(response.Status.Token) == 0 { return fmt.Errorf("failed to create token: no token in server response") } if o.PrintFlags.OutputFlagSpecified() { return o.PrintObj(response) } if term.IsTerminal(o.Out) { // include a newline when printing interactively fmt.Fprintf(o.Out, "%s\n", response.Status.Token) } else { // otherwise just print the token fmt.Fprintf(o.Out, "%s", response.Status.Token) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/create/create_token_test.go000066400000000000000000000256601504711711200321140ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "bytes" "encoding/json" "io" "net/http" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" kjson "sigs.k8s.io/json" authenticationv1 "k8s.io/api/authentication/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/utils/ptr" ) func TestCreateToken(t *testing.T) { tests := []struct { test string name string namespace string output string boundObjectKind string boundObjectName string boundObjectUID string audiences []string duration time.Duration serverResponseToken string serverResponseError string expectRequestPath string expectTokenRequest *authenticationv1.TokenRequest expectStdout string expectStderr string }{ { test: "simple", name: "mysa", expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, }, serverResponseToken: "abc", expectStdout: "abc", }, { test: "custom namespace", name: "custom-sa", namespace: "custom-ns", expectRequestPath: "/api/v1/namespaces/custom-ns/serviceaccounts/custom-sa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, }, serverResponseToken: "abc", expectStdout: "abc", }, { test: "yaml", name: "mysa", output: "yaml", expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, }, serverResponseToken: "abc", expectStdout: `apiVersion: authentication.k8s.io/v1 kind: TokenRequest metadata: creationTimestamp: null spec: audiences: null boundObjectRef: null expirationSeconds: null status: expirationTimestamp: null token: abc `, }, { test: "bad bound object kind", name: "mysa", boundObjectKind: "Foo", expectStderr: `error: supported --bound-object-kind values are Node, Pod, Secret`, }, { test: "bad bound object kind (node feature enabled)", name: "mysa", boundObjectKind: "Foo", expectStderr: `error: supported --bound-object-kind values are Node, Pod, Secret`, }, { test: "missing bound object name", name: "mysa", boundObjectKind: "Pod", expectStderr: `error: --bound-object-name is required if --bound-object-kind is provided`, }, { test: "invalid bound object name", name: "mysa", boundObjectName: "mypod", expectStderr: `error: --bound-object-name can only be set if --bound-object-kind is provided`, }, { test: "invalid bound object uid", name: "mysa", boundObjectUID: "myuid", expectStderr: `error: --bound-object-uid can only be set if --bound-object-kind is provided`, }, { test: "valid bound object", name: "mysa", boundObjectKind: "Pod", boundObjectName: "mypod", boundObjectUID: "myuid", expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, Spec: authenticationv1.TokenRequestSpec{ BoundObjectRef: &authenticationv1.BoundObjectReference{ Kind: "Pod", APIVersion: "v1", Name: "mypod", UID: "myuid", }, }, }, serverResponseToken: "abc", expectStdout: "abc", }, { test: "valid bound object (Node)", name: "mysa", boundObjectKind: "Node", boundObjectName: "mynode", boundObjectUID: "myuid", expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, Spec: authenticationv1.TokenRequestSpec{ BoundObjectRef: &authenticationv1.BoundObjectReference{ Kind: "Node", APIVersion: "v1", Name: "mynode", UID: "myuid", }, }, }, serverResponseToken: "abc", expectStdout: "abc", }, { test: "invalid audience", name: "mysa", audiences: []string{"test", "", "test2"}, expectStderr: `error: --audience must not be an empty string`, }, { test: "valid audiences", name: "mysa", audiences: []string{"test,value1", "test,value2"}, expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, Spec: authenticationv1.TokenRequestSpec{ Audiences: []string{"test,value1", "test,value2"}, }, }, serverResponseToken: "abc", expectStdout: "abc", }, { test: "invalid duration", name: "mysa", duration: -1, expectStderr: `error: --duration must be greater than or equal to 0`, }, { test: "invalid duration unit", name: "mysa", duration: time.Microsecond, expectStderr: `error: --duration cannot be expressed in units less than seconds`, }, { test: "valid duration", name: "mysa", duration: 1000 * time.Second, expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, Spec: authenticationv1.TokenRequestSpec{ ExpirationSeconds: ptr.To[int64](1000), }, }, serverResponseToken: "abc", expectStdout: "abc", }, { test: "zero duration act as default", name: "mysa", duration: 0 * time.Second, expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, Spec: authenticationv1.TokenRequestSpec{ ExpirationSeconds: nil, }, }, serverResponseToken: "abc", expectStdout: "abc", }, { test: "server error", name: "mysa", expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, }, serverResponseError: "bad bad request", expectStderr: `error: failed to create token: "bad bad request" is invalid`, }, { test: "server missing token", name: "mysa", expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token", expectTokenRequest: &authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, }, serverResponseToken: "", expectStderr: `error: failed to create token: no token in server response`, }, } for _, test := range tests { t.Run(test.test, func(t *testing.T) { defer cmdutil.DefaultBehaviorOnFatal() sawError := "" cmdutil.BehaviorOnFatal(func(str string, code int) { sawError = str }) namespace := "test" if test.namespace != "" { namespace = test.namespace } tf := cmdtesting.NewTestFactory().WithNamespace(namespace) defer tf.Cleanup() tf.Client = &fake.RESTClient{} var code int var body []byte if len(test.serverResponseError) > 0 { code = 422 response := apierrors.NewInvalid(schema.GroupKind{Group: "", Kind: ""}, test.serverResponseError, nil) response.ErrStatus.APIVersion = "v1" response.ErrStatus.Kind = "Status" body, _ = json.Marshal(response.ErrStatus) } else { code = 200 response := authenticationv1.TokenRequest{ TypeMeta: metav1.TypeMeta{ APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest", }, Status: authenticationv1.TokenRequestStatus{Token: test.serverResponseToken}, } body, _ = json.Marshal(response) } ns := scheme.Codecs.WithoutConversion() var tokenRequest *authenticationv1.TokenRequest tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Path != test.expectRequestPath { t.Fatalf("expected %q, got %q", test.expectRequestPath, req.URL.Path) } data, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } tokenRequest = &authenticationv1.TokenRequest{} if strictErrs, err := kjson.UnmarshalStrict(data, tokenRequest); err != nil { t.Fatal(err) } else if len(strictErrs) > 0 { t.Fatal(strictErrs) } return &http.Response{ StatusCode: code, Body: io.NopCloser(bytes.NewBuffer(body)), }, nil }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, stdout, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdCreateToken(tf, ioStreams) if test.output != "" { cmd.Flags().Set("output", test.output) } if test.boundObjectKind != "" { cmd.Flags().Set("bound-object-kind", test.boundObjectKind) } if test.boundObjectName != "" { cmd.Flags().Set("bound-object-name", test.boundObjectName) } if test.boundObjectUID != "" { cmd.Flags().Set("bound-object-uid", test.boundObjectUID) } for _, aud := range test.audiences { cmd.Flags().Set("audience", aud) } if test.duration != 0 { cmd.Flags().Set("duration", test.duration.String()) } cmd.Run(cmd, []string{test.name}) if !reflect.DeepEqual(tokenRequest, test.expectTokenRequest) { t.Fatalf("unexpected request:\n%s", cmp.Diff(test.expectTokenRequest, tokenRequest)) } if stdout.String() != test.expectStdout { t.Errorf("unexpected stdout:\n%s", cmp.Diff(test.expectStdout, stdout.String())) } if sawError != test.expectStderr { t.Errorf("unexpected stderr:\n%s", cmp.Diff(test.expectStderr, sawError)) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/debug/000077500000000000000000000000001504711711200256755ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go000066400000000000000000001061621504711711200273200ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package debug import ( "context" "encoding/json" "fmt" "os" "time" "github.com/distribution/reference" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/cmd/attach" "k8s.io/kubectl/pkg/cmd/exec" "k8s.io/kubectl/pkg/cmd/logs" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" "k8s.io/utils/ptr" "sigs.k8s.io/yaml" ) var ( debugLong = templates.LongDesc(i18n.T(` Debug cluster resources using interactive debugging containers. 'debug' provides automation for common debugging tasks for cluster objects identified by resource and name. Pods will be used by default if no resource is specified. The action taken by 'debug' varies depending on what resource is specified. Supported actions include: * Workload: Create a copy of an existing pod with certain attributes changed, for example changing the image tag to a new version. * Workload: Add an ephemeral container to an already running pod, for example to add debugging utilities without restarting the pod. * Node: Create a new pod that runs in the node's host namespaces and can access the node's filesystem. Note: When a non-root user is configured for the entire target Pod, some capabilities granted by debug profile may not work. `)) debugExample = templates.Examples(i18n.T(` # Create an interactive debugging session in pod mypod and immediately attach to it. kubectl debug mypod -it --image=busybox # Create an interactive debugging session for the pod in the file pod.yaml and immediately attach to it. # (requires the EphemeralContainers feature to be enabled in the cluster) kubectl debug -f pod.yaml -it --image=busybox # Create a debug container named debugger using a custom automated debugging image. kubectl debug --image=myproj/debug-tools -c debugger mypod # Create a copy of mypod adding a debug container and attach to it kubectl debug mypod -it --image=busybox --copy-to=my-debugger # Create a copy of mypod changing the command of mycontainer kubectl debug mypod -it --copy-to=my-debugger --container=mycontainer -- sh # Create a copy of mypod changing all container images to busybox kubectl debug mypod --copy-to=my-debugger --set-image=*=busybox # Create a copy of mypod adding a debug container and changing container images kubectl debug mypod -it --copy-to=my-debugger --image=debian --set-image=app=app:debug,sidecar=sidecar:debug # Create an interactive debugging session on a node and immediately attach to it. # The container will run in the host namespaces and the host's filesystem will be mounted at /host kubectl debug node/mynode -it --image=busybox `)) ) var nameSuffixFunc = utilrand.String type DebugAttachFunc func(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error // DebugOptions holds the options for an invocation of kubectl debug. type DebugOptions struct { Args []string ArgsOnly bool Attach bool AttachFunc DebugAttachFunc Container string CopyTo string Replace bool Env []corev1.EnvVar Image string Interactive bool KeepLabels bool KeepAnnotations bool KeepLiveness bool KeepReadiness bool KeepStartup bool KeepInitContainers bool Namespace string TargetNames []string PullPolicy corev1.PullPolicy Quiet bool SameNode bool SetImages map[string]string ShareProcesses bool TargetContainer string TTY bool Profile string CustomProfileFile string CustomProfile *corev1.Container Applier ProfileApplier explicitNamespace bool attachChanged bool shareProcessedChanged bool podClient corev1client.CoreV1Interface Builder *resource.Builder genericiooptions.IOStreams WarningPrinter *printers.WarningPrinter resource.FilenameOptions } // NewDebugOptions returns a DebugOptions initialized with default values. func NewDebugOptions(streams genericiooptions.IOStreams) *DebugOptions { return &DebugOptions{ Args: []string{}, IOStreams: streams, KeepInitContainers: true, TargetNames: []string{}, ShareProcesses: true, } } // NewCmdDebug returns a cobra command that runs kubectl debug. func NewCmdDebug(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { o := NewDebugOptions(streams) cmd := &cobra.Command{ Use: "debug (POD | TYPE[[.VERSION].GROUP]/NAME) [ -- COMMAND [args...] ]", DisableFlagsInUseLine: true, Short: i18n.T("Create debugging sessions for troubleshooting workloads and nodes"), Long: debugLong, Example: debugExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(restClientGetter, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run(restClientGetter, cmd)) }, } o.AddFlags(cmd) return cmd } func (o *DebugOptions) AddFlags(cmd *cobra.Command) { cmdutil.AddJsonFilenameFlag(cmd.Flags(), &o.FilenameOptions.Filenames, "identifying the resource to debug") cmd.Flags().BoolVar(&o.ArgsOnly, "arguments-only", o.ArgsOnly, i18n.T("If specified, everything after -- will be passed to the new container as Args instead of Command.")) cmd.Flags().BoolVar(&o.Attach, "attach", o.Attach, i18n.T("If true, wait for the container to start running, and then attach as if 'kubectl attach ...' were called. Default false, unless '-i/--stdin' is set, in which case the default is true.")) cmd.Flags().StringVarP(&o.Container, "container", "c", o.Container, i18n.T("Container name to use for debug container.")) cmd.Flags().StringVar(&o.CopyTo, "copy-to", o.CopyTo, i18n.T("Create a copy of the target Pod with this name.")) cmd.Flags().BoolVar(&o.Replace, "replace", o.Replace, i18n.T("When used with '--copy-to', delete the original Pod.")) cmd.Flags().StringToString("env", nil, i18n.T("Environment variables to set in the container.")) cmd.Flags().StringVar(&o.Image, "image", o.Image, i18n.T("Container image to use for debug container.")) cmd.Flags().BoolVar(&o.KeepLabels, "keep-labels", o.KeepLabels, i18n.T("If true, keep the original pod labels.(This flag only works when used with '--copy-to')")) cmd.Flags().BoolVar(&o.KeepAnnotations, "keep-annotations", o.KeepAnnotations, i18n.T("If true, keep the original pod annotations.(This flag only works when used with '--copy-to')")) cmd.Flags().BoolVar(&o.KeepLiveness, "keep-liveness", o.KeepLiveness, i18n.T("If true, keep the original pod liveness probes.(This flag only works when used with '--copy-to')")) cmd.Flags().BoolVar(&o.KeepReadiness, "keep-readiness", o.KeepReadiness, i18n.T("If true, keep the original pod readiness probes.(This flag only works when used with '--copy-to')")) cmd.Flags().BoolVar(&o.KeepStartup, "keep-startup", o.KeepStartup, i18n.T("If true, keep the original startup probes.(This flag only works when used with '--copy-to')")) cmd.Flags().BoolVar(&o.KeepInitContainers, "keep-init-containers", o.KeepInitContainers, i18n.T("Run the init containers for the pod. Defaults to true.(This flag only works when used with '--copy-to')")) cmd.Flags().StringToStringVar(&o.SetImages, "set-image", o.SetImages, i18n.T("When used with '--copy-to', a list of name=image pairs for changing container images, similar to how 'kubectl set image' works.")) cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server.")) cmd.Flags().BoolVarP(&o.Interactive, "stdin", "i", o.Interactive, i18n.T("Keep stdin open on the container(s) in the pod, even if nothing is attached.")) cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, i18n.T("If true, suppress informational messages.")) cmd.Flags().BoolVar(&o.SameNode, "same-node", o.SameNode, i18n.T("When used with '--copy-to', schedule the copy of target Pod on the same node.")) cmd.Flags().BoolVar(&o.ShareProcesses, "share-processes", o.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy.")) cmd.Flags().StringVar(&o.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name.")) cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, i18n.T("Allocate a TTY for the debugging container.")) cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Options are "legacy", "general", "baseline", "netadmin", "restricted" or "sysadmin".`)) cmd.Flags().StringVar(&o.CustomProfileFile, "custom", o.CustomProfileFile, i18n.T("Path to a JSON or YAML file containing a partial container spec to customize built-in debug profiles.")) } // Complete finishes run-time initialization of debug.DebugOptions. func (o *DebugOptions) Complete(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, args []string) error { var err error o.PullPolicy = corev1.PullPolicy(cmdutil.GetFlagString(cmd, "image-pull-policy")) // Arguments argsLen := cmd.ArgsLenAtDash() o.TargetNames = args // If there is a dash and there are args after the dash, extract the args. if argsLen >= 0 && len(args) > argsLen { o.TargetNames, o.Args = args[:argsLen], args[argsLen:] } // Attach attachFlag := cmd.Flags().Lookup("attach") if !attachFlag.Changed && o.Interactive { o.Attach = true } // Downstream tools may want to use their own customized // attach function to do extra work or use attach command // with different flags instead of the static one defined in // handleAttachPod. But if this function is not set explicitly, // we fall back to default. if o.AttachFunc == nil { o.AttachFunc = o.handleAttachPod } // Environment envStrings, err := cmd.Flags().GetStringToString("env") if err != nil { return fmt.Errorf("internal error getting env flag: %v", err) } for k, v := range envStrings { o.Env = append(o.Env, corev1.EnvVar{Name: k, Value: v}) } // Namespace o.Namespace, o.explicitNamespace, err = restClientGetter.ToRawKubeConfigLoader().Namespace() if err != nil { return err } // Record flags that the user explicitly changed from their defaults o.attachChanged = cmd.Flags().Changed("attach") o.shareProcessedChanged = cmd.Flags().Changed("share-processes") // Set default WarningPrinter if o.WarningPrinter == nil { o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) } if o.Applier == nil { kflags := KeepFlags{ Labels: o.KeepLabels, Annotations: o.KeepAnnotations, Liveness: o.KeepLiveness, Readiness: o.KeepReadiness, Startup: o.KeepStartup, InitContainers: o.KeepInitContainers, } applier, err := NewProfileApplier(o.Profile, kflags) if err != nil { return err } o.Applier = applier } if o.CustomProfileFile != "" { customProfileBytes, err := os.ReadFile(o.CustomProfileFile) if err != nil { return fmt.Errorf("must pass a container spec json file for custom profile: %w", err) } err = json.Unmarshal(customProfileBytes, &o.CustomProfile) if err != nil { err = yaml.Unmarshal(customProfileBytes, &o.CustomProfile) if err != nil { return fmt.Errorf("%s does not contain a valid container spec: %w", o.CustomProfileFile, err) } } } clientConfig, err := restClientGetter.ToRESTConfig() if err != nil { return err } client, err := kubernetes.NewForConfig(clientConfig) if err != nil { return err } o.podClient = client.CoreV1() o.Builder = resource.NewBuilder(restClientGetter) return nil } // Validate checks that the provided debug options are specified. func (o *DebugOptions) Validate() error { // Attach if o.Attach && o.attachChanged && len(o.Image) == 0 && len(o.Container) == 0 { return fmt.Errorf("you must specify --container or create a new container using --image in order to attach.") } // CopyTo if len(o.CopyTo) > 0 { if len(o.Image) == 0 && len(o.SetImages) == 0 && len(o.Args) == 0 { return fmt.Errorf("you must specify --image, --set-image or command arguments.") } if len(o.Args) > 0 && len(o.Container) == 0 && len(o.Image) == 0 { return fmt.Errorf("you must specify an existing container or a new image when specifying args.") } } else { // These flags are exclusive to --copy-to switch { case o.Replace: return fmt.Errorf("--replace may only be used with --copy-to.") case o.SameNode: return fmt.Errorf("--same-node may only be used with --copy-to.") case len(o.SetImages) > 0: return fmt.Errorf("--set-image may only be used with --copy-to.") case len(o.Image) == 0: return fmt.Errorf("you must specify --image when not using --copy-to.") } } // Image if len(o.Image) > 0 && !reference.ReferenceRegexp.MatchString(o.Image) { return fmt.Errorf("invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat) } // Name if len(o.TargetNames) == 0 && len(o.FilenameOptions.Filenames) == 0 { return fmt.Errorf("NAME or filename is required for debug") } // Pull Policy switch o.PullPolicy { case corev1.PullAlways, corev1.PullIfNotPresent, corev1.PullNever, "": // continue default: return fmt.Errorf("invalid image pull policy: %s", o.PullPolicy) } // SetImages for name, image := range o.SetImages { if !reference.ReferenceRegexp.MatchString(image) { return fmt.Errorf("invalid image name %q for container %q: %v", image, name, reference.ErrReferenceInvalidFormat) } } // TargetContainer if len(o.TargetContainer) > 0 { if len(o.CopyTo) > 0 { return fmt.Errorf("--target is incompatible with --copy-to. Use --share-processes instead.") } if !o.Quiet { fmt.Fprintf(o.Out, "Targeting container %q. If you don't see processes from this container it may be because the container runtime doesn't support this feature.\n", o.TargetContainer) // TODO(verb): Add a list of supported container runtimes to https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/ and then link here. } } // TTY if o.TTY && !o.Interactive { return fmt.Errorf("-i/--stdin is required for containers with -t/--tty=true") } // WarningPrinter if o.WarningPrinter == nil { return fmt.Errorf("WarningPrinter can not be used without initialization") } if o.CustomProfile != nil { if o.CustomProfile.Name != "" || len(o.CustomProfile.Command) > 0 || o.CustomProfile.Image != "" || o.CustomProfile.Lifecycle != nil || len(o.CustomProfile.VolumeDevices) > 0 { return fmt.Errorf("name, command, image, lifecycle and volume devices are not modifiable via custom profile") } } // Warning for legacy profile if o.Profile == ProfileLegacy { fmt.Fprintln(o.ErrOut, `--profile=legacy is deprecated and will be removed in the future. It is recommended to explicitly specify a profile, for example "--profile=general".`) } return nil } // Run executes a kubectl debug. func (o *DebugOptions) Run(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command) error { ctx := context.Background() r := o.Builder. WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). FilenameParam(o.explicitNamespace, &o.FilenameOptions). NamespaceParam(o.Namespace).DefaultNamespace().ResourceNames("pods", o.TargetNames...). Do() if err := r.Err(); err != nil { return err } err := r.Visit(func(info *resource.Info, err error) error { if err != nil { // TODO(verb): configurable early return return err } var ( debugPod *corev1.Pod containerName string visitErr error ) switch obj := info.Object.(type) { case *corev1.Node: debugPod, containerName, visitErr = o.visitNode(ctx, obj) case *corev1.Pod: debugPod, containerName, visitErr = o.visitPod(ctx, obj) default: visitErr = fmt.Errorf("%q not supported by debug", info.Mapping.GroupVersionKind) } if visitErr != nil { return visitErr } if o.Attach && len(containerName) > 0 && o.AttachFunc != nil { if err := o.AttachFunc(ctx, restClientGetter, cmd.Parent().CommandPath(), debugPod.Namespace, debugPod.Name, containerName); err != nil { return err } } return nil }) return err } // visitNode handles debugging for node targets by creating a privileged pod running in the host namespaces. // Returns an already created pod and container name for subsequent attach, if applicable. func (o *DebugOptions) visitNode(ctx context.Context, node *corev1.Node) (*corev1.Pod, string, error) { pods := o.podClient.Pods(o.Namespace) debugPod, err := o.generateNodeDebugPod(node) if err != nil { return nil, "", err } newPod, err := pods.Create(ctx, debugPod, metav1.CreateOptions{}) if err != nil { return nil, "", err } return newPod, newPod.Spec.Containers[0].Name, nil } // visitPod handles debugging for pod targets by (depending on options): // 1. Creating an ephemeral debug container in an existing pod, OR // 2. Making a copy of pod with certain attributes changed // // visitPod returns a pod and debug container name for subsequent attach, if applicable. func (o *DebugOptions) visitPod(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { if len(o.CopyTo) > 0 { return o.debugByCopy(ctx, pod) } return o.debugByEphemeralContainer(ctx, pod) } // debugByEphemeralContainer runs an EphemeralContainer in the target Pod for use as a debug container func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { klog.V(2).Infof("existing ephemeral containers: %v", pod.Spec.EphemeralContainers) podJS, err := json.Marshal(pod) if err != nil { return nil, "", fmt.Errorf("error creating JSON for pod: %v", err) } debugPod, debugContainer, err := o.generateDebugContainer(pod) if err != nil { return nil, "", err } klog.V(2).Infof("new ephemeral container: %#v", debugContainer) o.displayWarning((*corev1.Container)(&debugContainer.EphemeralContainerCommon), pod) debugJS, err := json.Marshal(debugPod) if err != nil { return nil, "", fmt.Errorf("error creating JSON for debug container: %v", err) } patch, err := strategicpatch.CreateTwoWayMergePatch(podJS, debugJS, pod) if err != nil { return nil, "", fmt.Errorf("error creating patch to add debug container: %v", err) } klog.V(2).Infof("generated strategic merge patch for debug container: %s", patch) pods := o.podClient.Pods(pod.Namespace) result, err := pods.Patch(ctx, pod.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "ephemeralcontainers") if err != nil { // The apiserver will return a 404 when the EphemeralContainers feature is disabled because the `/ephemeralcontainers` subresource // is missing. Unlike the 404 returned by a missing pod, the status details will be empty. if serr, ok := err.(*errors.StatusError); ok && serr.Status().Reason == metav1.StatusReasonNotFound && serr.ErrStatus.Details.Name == "" { return nil, "", fmt.Errorf("ephemeral containers are disabled for this cluster (error from server: %q)", err) } return nil, "", err } return result, debugContainer.Name, nil } // applyCustomProfile applies given partial container json file on to the profile // incorporated debug pod. func (o *DebugOptions) applyCustomProfile(debugPod *corev1.Pod, containerName string) error { o.CustomProfile.Name = containerName customJS, err := json.Marshal(o.CustomProfile) if err != nil { return fmt.Errorf("unable to marshall custom profile: %w", err) } var index int found := false for i, val := range debugPod.Spec.Containers { if val.Name == containerName { index = i found = true break } } if !found { return fmt.Errorf("unable to find the %s container in the pod %s", containerName, debugPod.Name) } var debugContainerJS []byte debugContainerJS, err = json.Marshal(debugPod.Spec.Containers[index]) if err != nil { return fmt.Errorf("unable to marshall container: %w", err) } patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{}) if err != nil { return fmt.Errorf("error creating three way patch to add debug container: %w", err) } err = json.Unmarshal(patchedContainer, &debugPod.Spec.Containers[index]) if err != nil { return fmt.Errorf("unable to unmarshall patched container to container: %w", err) } return nil } // applyCustomProfileEphemeral applies given partial container json file on to the profile // incorporated ephemeral container of the pod. func (o *DebugOptions) applyCustomProfileEphemeral(debugPod *corev1.Pod, containerName string) error { o.CustomProfile.Name = containerName customJS, err := json.Marshal(o.CustomProfile) if err != nil { return fmt.Errorf("unable to marshall custom profile: %w", err) } var index int found := false for i, val := range debugPod.Spec.EphemeralContainers { if val.Name == containerName { index = i found = true break } } if !found { return fmt.Errorf("unable to find the %s ephemeral container in the pod %s", containerName, debugPod.Name) } var debugContainerJS []byte debugContainerJS, err = json.Marshal(debugPod.Spec.EphemeralContainers[index]) if err != nil { return fmt.Errorf("unable to marshall ephemeral container:%w", err) } patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{}) if err != nil { return fmt.Errorf("error creating three way patch to add debug container: %w", err) } err = json.Unmarshal(patchedContainer, &debugPod.Spec.EphemeralContainers[index]) if err != nil { return fmt.Errorf("unable to unmarshall patched container to ephemeral container: %w", err) } return nil } // debugByCopy runs a copy of the target Pod with a debug container added or an original container modified func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) { copied, dc, err := o.generatePodCopyWithDebugContainer(pod) if err != nil { return nil, "", err } var debugContainer *corev1.Container for i := range copied.Spec.Containers { if copied.Spec.Containers[i].Name == dc { debugContainer = &copied.Spec.Containers[i] break } } o.displayWarning(debugContainer, copied) created, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{}) if err != nil { return nil, "", err } if o.Replace { err := o.podClient.Pods(pod.Namespace).Delete(ctx, pod.Name, *metav1.NewDeleteOptions(0)) if err != nil { return nil, "", err } } return created, dc, nil } // Display warning message if some capabilities are set by profile and non-root user is specified in .Spec.SecurityContext.RunAsUser.(#1650) func (o *DebugOptions) displayWarning(container *corev1.Container, pod *corev1.Pod) { if container == nil { return } if pod.Spec.SecurityContext.RunAsUser == nil || *pod.Spec.SecurityContext.RunAsUser == 0 { return } if container.SecurityContext == nil { return } if container.SecurityContext.RunAsUser != nil && *container.SecurityContext.RunAsUser == 0 { return } if (container.SecurityContext.Privileged == nil || !*container.SecurityContext.Privileged) && (container.SecurityContext.Capabilities == nil || len(container.SecurityContext.Capabilities.Add) == 0) { return } _, _ = fmt.Fprintln(o.ErrOut, `Warning: Non-root user is configured for the entire target Pod, and some capabilities granted by debug profile may not work. Please consider using "--custom" with a custom profile that specifies "securityContext.runAsUser: 0".`) } // generateDebugContainer returns a debugging pod and an EphemeralContainer suitable for use as a debug container // in the given pod. func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *corev1.EphemeralContainer, error) { name := o.computeDebugContainerName(pod) ec := &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: name, Env: o.Env, Image: o.Image, ImagePullPolicy: o.PullPolicy, Stdin: o.Interactive, TerminationMessagePolicy: corev1.TerminationMessageReadFile, TTY: o.TTY, }, TargetContainerName: o.TargetContainer, } if o.ArgsOnly { ec.Args = o.Args } else { ec.Command = o.Args } copied := pod.DeepCopy() copied.Spec.EphemeralContainers = append(copied.Spec.EphemeralContainers, *ec) if err := o.Applier.Apply(copied, name, copied); err != nil { return nil, nil, err } if o.CustomProfile != nil { err := o.applyCustomProfileEphemeral(copied, ec.Name) if err != nil { return nil, nil, err } } ec = &copied.Spec.EphemeralContainers[len(copied.Spec.EphemeralContainers)-1] return copied, ec, nil } // generateNodeDebugPod generates a debugging pod that schedules on the specified node. // The generated pod will run in the host PID, Network & IPC namespaces, and it will have the node's filesystem mounted at /host. func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, error) { cn := "debugger" // Setting a user-specified container name doesn't make much difference when there's only one container, // but the argument exists for pod debugging so it might be confusing if it didn't work here. if len(o.Container) > 0 { cn = o.Container } // The name of the debugging pod is based on the target node, and it's not configurable to // limit the number of command line flags. There may be a collision on the name, but this // should be rare enough that it's not worth the API round trip to check. pn := fmt.Sprintf("node-debugger-%s-%s", node.Name, nameSuffixFunc(5)) if !o.Quiet { fmt.Fprintf(o.Out, "Creating debugging pod %s with container %s on node %s.\n", pn, cn, node.Name) } p := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: pn, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: cn, Env: o.Env, Image: o.Image, ImagePullPolicy: o.PullPolicy, Stdin: o.Interactive, TerminationMessagePolicy: corev1.TerminationMessageReadFile, TTY: o.TTY, }, }, NodeName: node.Name, RestartPolicy: corev1.RestartPolicyNever, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, } if o.ArgsOnly { p.Spec.Containers[0].Args = o.Args } else { p.Spec.Containers[0].Command = o.Args } if err := o.Applier.Apply(p, cn, node); err != nil { return nil, err } if o.CustomProfile != nil { err := o.applyCustomProfile(p, cn) if err != nil { return nil, err } } return p, nil } // generatePodCopyWithDebugContainer takes a Pod and returns a copy and the debug container name of that copy func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*corev1.Pod, string, error) { copied := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: o.CopyTo, Namespace: pod.Namespace, Annotations: pod.Annotations, Labels: pod.Labels, }, Spec: *pod.Spec.DeepCopy(), } // set EphemeralContainers to nil so that the copy of pod can be created copied.Spec.EphemeralContainers = nil // change ShareProcessNamespace configuration only when commanded explicitly if o.shareProcessedChanged { copied.Spec.ShareProcessNamespace = ptr.To(o.ShareProcesses) } if !o.SameNode { copied.Spec.NodeName = "" } // Apply image mutations for i, c := range copied.Spec.Containers { override := o.SetImages["*"] if img, ok := o.SetImages[c.Name]; ok { override = img } if len(override) > 0 { copied.Spec.Containers[i].Image = override } } name, containerByName := o.Container, containerNameToRef(copied) c, ok := containerByName[name] if !ok { // Adding a new debug container if len(o.Image) == 0 { if len(o.SetImages) > 0 { // This was a --set-image only invocation return copied, "", nil } return nil, "", fmt.Errorf("you must specify image when creating new container") } if len(name) == 0 { name = o.computeDebugContainerName(copied) } copied.Spec.Containers = append(copied.Spec.Containers, corev1.Container{ Name: name, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }) c = &copied.Spec.Containers[len(copied.Spec.Containers)-1] } if len(o.Args) > 0 { if o.ArgsOnly { c.Args = o.Args } else { c.Command = o.Args c.Args = nil } } if len(o.Env) > 0 { c.Env = o.Env } if len(o.Image) > 0 { c.Image = o.Image } if len(o.PullPolicy) > 0 { c.ImagePullPolicy = o.PullPolicy } c.Stdin = o.Interactive c.TTY = o.TTY err := o.Applier.Apply(copied, c.Name, pod) if err != nil { return nil, "", err } if o.CustomProfile != nil { err = o.applyCustomProfile(copied, name) if err != nil { return nil, "", err } } return copied, name, nil } func (o *DebugOptions) computeDebugContainerName(pod *corev1.Pod) string { if len(o.Container) > 0 { return o.Container } cn, containerByName := "", containerNameToRef(pod) for len(cn) == 0 || (containerByName[cn] != nil) { cn = fmt.Sprintf("debugger-%s", nameSuffixFunc(5)) } if !o.Quiet { fmt.Fprintf(o.Out, "Defaulting debug container name to %s.\n", cn) } return cn } func containerNameToRef(pod *corev1.Pod) map[string]*corev1.Container { names := map[string]*corev1.Container{} for i := range pod.Spec.Containers { ref := &pod.Spec.Containers[i] names[ref.Name] = ref } for i := range pod.Spec.InitContainers { ref := &pod.Spec.InitContainers[i] names[ref.Name] = ref } for i := range pod.Spec.EphemeralContainers { ref := (*corev1.Container)(&pod.Spec.EphemeralContainers[i].EphemeralContainerCommon) names[ref.Name] = ref } return names } // waitForContainer watches the given pod until the container is running func (o *DebugOptions) waitForContainer(ctx context.Context, ns, podName, containerName string) (*corev1.Pod, error) { // TODO: expose the timeout ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, 0*time.Second) defer cancel() fieldSelector := fields.OneTermEqualSelector("metadata.name", podName).String() lw := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector return o.podClient.Pods(ns).List(ctx, options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { options.FieldSelector = fieldSelector return o.podClient.Pods(ns).Watch(ctx, options) }, } intr := interrupt.New(nil, cancel) var result *corev1.Pod err := intr.Run(func() error { ev, err := watchtools.UntilWithSync(ctx, lw, &corev1.Pod{}, nil, func(ev watch.Event) (bool, error) { klog.V(2).Infof("watch received event %q with object %T", ev.Type, ev.Object) switch ev.Type { case watch.Deleted: return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") } p, ok := ev.Object.(*corev1.Pod) if !ok { return false, fmt.Errorf("watch did not return a pod: %v", ev.Object) } s := getContainerStatusByName(p, containerName) if s == nil { return false, nil } klog.V(2).Infof("debug container status is %v", s) if s.State.Running != nil || s.State.Terminated != nil { return true, nil } if !o.Quiet && s.State.Waiting != nil && s.State.Waiting.Message != "" { o.WarningPrinter.Print(fmt.Sprintf("container %s: %s", containerName, s.State.Waiting.Message)) } return false, nil }) if ev != nil { result = ev.Object.(*corev1.Pod) } return err }) return result, err } func (o *DebugOptions) handleAttachPod(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error { opts := &attach.AttachOptions{ StreamOptions: exec.StreamOptions{ IOStreams: o.IOStreams, Stdin: o.Interactive, TTY: o.TTY, Quiet: o.Quiet, }, CommandName: cmdPath + " attach", Attach: &attach.DefaultRemoteAttach{}, } config, err := restClientGetter.ToRESTConfig() if err != nil { return err } opts.Config = config opts.AttachFunc = attach.DefaultAttachFunc pod, err := o.waitForContainer(ctx, ns, podName, containerName) if err != nil { return err } opts.Namespace = ns opts.Pod = pod opts.PodName = podName opts.ContainerName = containerName if opts.AttachFunc == nil { opts.AttachFunc = attach.DefaultAttachFunc } status := getContainerStatusByName(pod, containerName) if status == nil { // impossible path return fmt.Errorf("error getting container status of container name %q: %+v", containerName, err) } if status.State.Terminated != nil { klog.V(1).Info("Ephemeral container terminated, falling back to logs") return logOpts(ctx, restClientGetter, pod, opts) } if err := opts.Run(); err != nil { fmt.Fprintf(opts.ErrOut, "warning: couldn't attach to pod/%s, falling back to streaming logs: %v\n", podName, err) return logOpts(ctx, restClientGetter, pod, opts) } return nil } func getContainerStatusByName(pod *corev1.Pod, containerName string) *corev1.ContainerStatus { allContainerStatus := [][]corev1.ContainerStatus{pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses, pod.Status.EphemeralContainerStatuses} for _, statusSlice := range allContainerStatus { for i := range statusSlice { if statusSlice[i].Name == containerName { return &statusSlice[i] } } } return nil } // logOpts logs output from opts to the pods log. func logOpts(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Pod, opts *attach.AttachOptions) error { ctrName, err := opts.GetContainerName(pod) if err != nil { return err } requests, err := polymorphichelpers.LogsForObjectFn(restClientGetter, pod, &corev1.PodLogOptions{Container: ctrName}, opts.GetPodTimeout, false) if err != nil { return err } for _, request := range requests { if err := logs.DefaultConsumeRequest(ctx, request, opts.Out); err != nil { return err } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go000066400000000000000000002324661504711711200303660ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package debug import ( "fmt" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/utils/ptr" ) func TestGenerateDebugContainer(t *testing.T) { // Slightly less randomness for testing. defer func(old func(int) string) { nameSuffixFunc = old }(nameSuffixFunc) var suffixCounter int nameSuffixFunc = func(int) string { suffixCounter++ return fmt.Sprint(suffixCounter) } for _, tc := range []struct { name string opts *DebugOptions pod *corev1.Pod expected *corev1.EphemeralContainer }{ { name: "minimum fields", opts: &DebugOptions{ Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "namespace targeting", opts: &DebugOptions{ Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, TargetContainer: "myapp", Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, TargetContainerName: "myapp", }, }, { name: "debug args as container command", opts: &DebugOptions{ Args: []string{"/bin/echo", "one", "two", "three"}, Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Command: []string{"/bin/echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "debug args as container args", opts: &DebugOptions{ ArgsOnly: true, Container: "debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "random name generation", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "random name collision", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, }, }, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-2", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "pod with init containers", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init-container-1", }, { Name: "init-container-2", }, }, Containers: []corev1.Container{ { Name: "debugger", }, }, }, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "pod with ephemeral containers", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-1", }, }, { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-2", }, }, }, }, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", }, }, }, { name: "general profile", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileGeneral, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }, { name: "baseline profile", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileBaseline, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, { name: "restricted profile", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, }, { name: "netadmin profile", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileNetadmin, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, }, }, }, }, }, { name: "sysadmin profile", opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileSysadmin, }, expected: &corev1.EphemeralContainer{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, SecurityContext: &corev1.SecurityContext{ Privileged: ptr.To(true), }, }, }, }, } { t.Run(tc.name, func(t *testing.T) { var err error kflags := KeepFlags{ Labels: tc.opts.KeepLabels, Annotations: tc.opts.KeepAnnotations, Liveness: tc.opts.KeepLiveness, Readiness: tc.opts.KeepReadiness, Startup: tc.opts.KeepStartup, InitContainers: tc.opts.KeepInitContainers, } tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile, kflags) if err != nil { t.Fatalf("failed to create profile applier: %s: %v", tc.opts.Profile, err) } tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() suffixCounter = 0 if tc.pod == nil { tc.pod = &corev1.Pod{} } _, debugContainer, err := tc.opts.generateDebugContainer(tc.pod) if err != nil { t.Fatalf("fail to generate debug container: %v", err) } if diff := cmp.Diff(tc.expected, debugContainer); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestGeneratePodCopyWithDebugContainer(t *testing.T) { defer func(old func(int) string) { nameSuffixFunc = old }(nameSuffixFunc) var suffixCounter int nameSuffixFunc = func(int) string { suffixCounter++ return fmt.Sprint(suffixCounter) } for _, tc := range []struct { name string opts *DebugOptions havePod, wantPod *corev1.Pod }{ { name: "basic", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, }, }, }, }, }, { name: "same node", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, SameNode: true, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", Labels: map[string]string{ "app": "business", }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, }, }, NodeName: "node-1", }, }, }, { name: "metadata stripping", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", Labels: map[string]string{ "app": "business", }, Annotations: map[string]string{ "test": "test", }, ResourceVersion: "1", CreationTimestamp: metav1.Time{Time: time.Now()}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, }, }, }, }, }, { name: "add a debug container", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "customize envs", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Env: []corev1.EnvVar{{ Name: "TEST", Value: "test", }}, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, Env: []corev1.EnvVar{{ Name: "TEST", Value: "test", }}, }, }, }, }, }, { name: "debug args as container command", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Args: []string{"/bin/echo", "one", "two", "three"}, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, { Name: "debugger", Image: "busybox", Command: []string{"/bin/echo", "one", "two", "three"}, ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "debug args as container command", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Args: []string{"one", "two", "three"}, ArgsOnly: true, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, { Name: "debugger", Image: "busybox", Args: []string{"one", "two", "three"}, ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "modify existing command to debug args", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Args: []string{"sleep", "1d"}, PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Command: []string{"echo"}, Image: "app", Args: []string{"one", "two", "three"}, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "app", Command: []string{"sleep", "1d"}, ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "random name", opts: &DebugOptions{ CopyTo: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", }, { Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "random name collision", opts: &DebugOptions{ CopyTo: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, { Name: "debugger-2", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "pod with probes", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", KeepLiveness: true, KeepReadiness: true, KeepStartup: true, PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "business", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "pod with init containers", opts: &DebugOptions{ CopyTo: "debugger", Image: "busybox", KeepInitContainers: true, PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init-container-1", }, { Name: "init-container-2", }, }, Containers: []corev1.Container{ { Name: "debugger-1", }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init-container-1", }, { Name: "init-container-2", }, }, Containers: []corev1.Container{ { Name: "debugger-1", }, { Name: "debugger-2", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "pod with ephemeral containers", opts: &DebugOptions{ CopyTo: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, }, EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-1", }, }, { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "ephemeral-container-2", }, }, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger-1", }, { Name: "debugger-2", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, }, }, }, { name: "shared process namespace", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, ShareProcesses: true, shareProcessedChanged: true, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", ImagePullPolicy: corev1.PullAlways, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "Change image for a named container", opts: &DebugOptions{ Args: []string{}, CopyTo: "myapp-copy", Container: "app", Image: "busybox", TargetNames: []string{"myapp"}, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "myapp"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "myapp-copy"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "busybox"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, }, { name: "Change image for a named container with set-image", opts: &DebugOptions{ CopyTo: "myapp-copy", Container: "app", SetImages: map[string]string{"app": "busybox"}, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "myapp", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "myapp-copy", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "busybox"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, }, { name: "Change image for all containers with set-image", opts: &DebugOptions{ CopyTo: "myapp-copy", SetImages: map[string]string{"*": "busybox"}, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "myapp", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "myapp-copy", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "busybox"}, {Name: "sidecar", Image: "busybox"}, }, }, }, }, { name: "Change image for multiple containers with set-image", opts: &DebugOptions{ CopyTo: "myapp-copy", SetImages: map[string]string{"*": "busybox", "app": "app-debugger"}, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "myapp", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "myapp-copy", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "app-debugger"}, {Name: "sidecar", Image: "busybox"}, }, }, }, }, { name: "Add interactive debug container minimal args", opts: &DebugOptions{ Args: []string{}, Attach: true, CopyTo: "my-debugger", Image: "busybox", Interactive: true, TargetNames: []string{"mypod"}, TTY: true, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "mypod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "my-debugger"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, {Name: "sidecar", Image: "sidecarimage"}, { Name: "debugger-1", Image: "busybox", Stdin: true, TerminationMessagePolicy: corev1.TerminationMessageReadFile, TTY: true, }, }, }, }, }, { name: "Pod copy: add container and also mutate images", opts: &DebugOptions{ Args: []string{}, Attach: true, CopyTo: "my-debugger", Image: "debian", Interactive: true, Namespace: "default", SetImages: map[string]string{ "app": "app:debug", "sidecar": "sidecar:debug", }, ShareProcesses: true, TargetNames: []string{"mypod"}, TTY: true, Profile: ProfileLegacy, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "mypod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, {Name: "sidecar", Image: "sidecarimage"}, }, }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "my-debugger"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "app:debug"}, {Name: "sidecar", Image: "sidecar:debug"}, { Name: "debugger-1", Image: "debian", TerminationMessagePolicy: corev1.TerminationMessageReadFile, Stdin: true, TTY: true, }, }, }, }, }, { name: "general profile", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileGeneral, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "baseline profile", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileBaseline, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "baseline profile not share process when user explicitly disables it", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileBaseline, ShareProcesses: false, shareProcessedChanged: true, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, }, }, ShareProcessNamespace: ptr.To(false), }, }, }, { name: "restricted profile", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "netadmin profile", opts: &DebugOptions{ CopyTo: "debugger", Container: "debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileNetadmin, }, havePod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "target", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", }, }, NodeName: "node-1", }, }, wantPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "debugger", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, }, }, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, } { t.Run(tc.name, func(t *testing.T) { var err error kflags := KeepFlags{ Labels: tc.opts.KeepLabels, Annotations: tc.opts.KeepAnnotations, Liveness: tc.opts.KeepLiveness, Readiness: tc.opts.KeepReadiness, Startup: tc.opts.KeepStartup, InitContainers: tc.opts.KeepInitContainers, } tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile, kflags) if err != nil { t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) } tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() suffixCounter = 0 if tc.havePod == nil { tc.havePod = &corev1.Pod{} } gotPod, _, _ := tc.opts.generatePodCopyWithDebugContainer(tc.havePod) if diff := cmp.Diff(tc.wantPod, gotPod); diff != "" { t.Error("TestGeneratePodCopyWithDebugContainer: diff in generated object: (-want +got):\n", diff) } }) } } func TestGenerateNodeDebugPod(t *testing.T) { defer func(old func(int) string) { nameSuffixFunc = old }(nameSuffixFunc) var suffixCounter int nameSuffixFunc = func(int) string { suffixCounter++ return fmt.Sprint(suffixCounter) } for _, tc := range []struct { name string node *corev1.Node opts *DebugOptions expected *corev1.Pod }{ { name: "minimum options", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "debug args as container command", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Args: []string{"/bin/echo", "one", "two", "three"}, Container: "custom-debugger", Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "custom-debugger", Command: []string{"/bin/echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "debug args as container args", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ ArgsOnly: true, Container: "custom-debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileLegacy, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "custom-debugger", Args: []string{"echo", "one", "two", "three"}, Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "general profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileGeneral, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "baseline profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileBaseline, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, }, }, HostIPC: false, HostNetwork: false, HostPID: false, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: nil, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "restricted profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: nil, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "netadmin profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileNetadmin, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: nil, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, } { t.Run(tc.name, func(t *testing.T) { var err error kflags := KeepFlags{ Labels: tc.opts.KeepLabels, Annotations: tc.opts.KeepAnnotations, Liveness: tc.opts.KeepLiveness, Readiness: tc.opts.KeepReadiness, Startup: tc.opts.KeepStartup, InitContainers: tc.opts.KeepInitContainers, } tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile, kflags) if err != nil { t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) } tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() suffixCounter = 0 pod, err := tc.opts.generateNodeDebugPod(tc.node) if err != nil { t.Fatalf("Fail to generate node debug pod: %v", err) } if diff := cmp.Diff(tc.expected, pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestGenerateNodeDebugPodCustomProfile(t *testing.T) { for _, tc := range []struct { name string node *corev1.Node opts *DebugOptions expected *corev1.Pod }{ { name: "baseline profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileBaseline, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: ptr.To(false), }, }, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: nil, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "restricted profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, }, }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node-debugger-node-XXX-1", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: nil, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "netadmin profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileNetadmin, CustomProfile: &corev1.Container{ Env: []corev1.EnvVar{ { Name: "TEST_KEY", Value: "TEST_VALUE", }, }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, Env: []corev1.EnvVar{ { Name: "TEST_KEY", Value: "TEST_VALUE", }, }, VolumeMounts: nil, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, }, }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: nil, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, { name: "sysadmin profile", node: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, }, opts: &DebugOptions{ Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileSysadmin, CustomProfile: &corev1.Container{ Env: []corev1.EnvVar{ { Name: "TEST_KEY", Value: "TEST_VALUE", }, }, VolumeMounts: []corev1.VolumeMount{ { Name: "host-root", ReadOnly: true, MountPath: "/host", }, }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "debugger", Image: "busybox", ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageReadFile, Env: []corev1.EnvVar{ { Name: "TEST_KEY", Value: "TEST_VALUE", }, }, VolumeMounts: []corev1.VolumeMount{ { Name: "host-root", ReadOnly: true, MountPath: "/host", }, }, SecurityContext: &corev1.SecurityContext{ Privileged: ptr.To(true), }, }, }, HostIPC: true, HostNetwork: true, HostPID: true, NodeName: "node-XXX", RestartPolicy: corev1.RestartPolicyNever, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ Path: "/", }, }, }, }, Tolerations: []corev1.Toleration{ { Operator: corev1.TolerationOpExists, }, }, }, }, }, } { t.Run(tc.name, func(t *testing.T) { var err error kflags := KeepFlags{ Labels: tc.opts.KeepLabels, Annotations: tc.opts.KeepAnnotations, Liveness: tc.opts.KeepLiveness, Readiness: tc.opts.KeepReadiness, Startup: tc.opts.KeepStartup, InitContainers: tc.opts.KeepInitContainers, } tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile, kflags) if err != nil { t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) } tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() pod, err := tc.opts.generateNodeDebugPod(tc.node) if err != nil { t.Fatalf("Fail to generate node debug pod: %v", err) } tc.expected.Name = pod.Name if diff := cmp.Diff(tc.expected, pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestGenerateCopyDebugPodCustomProfile(t *testing.T) { for _, tc := range []struct { name string copyPod *corev1.Pod opts *DebugOptions expected *corev1.Pod }{ { name: "baseline profile", copyPod: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", }, }, opts: &DebugOptions{ SameNode: true, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileBaseline, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: ptr.To(false), }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", Containers: []corev1.Container{ { Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, Volumes: nil, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "restricted profile", copyPod: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", }, }, opts: &DebugOptions{ SameNode: true, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: ptr.To(false), }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", Containers: []corev1.Container{ { Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: ptr.To(false), RunAsNonRoot: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, LocalhostProfile: nil, }, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, Volumes: nil, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "sysadmin profile", copyPod: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", }, }, opts: &DebugOptions{ SameNode: true, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: ptr.To(false), }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", Containers: []corev1.Container{ { Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: ptr.To(false), RunAsNonRoot: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, LocalhostProfile: nil, }, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, Volumes: nil, ShareProcessNamespace: ptr.To(true), }, }, }, } { t.Run(tc.name, func(t *testing.T) { var err error kflags := KeepFlags{ Labels: tc.opts.KeepLabels, Annotations: tc.opts.KeepAnnotations, Liveness: tc.opts.KeepLiveness, Readiness: tc.opts.KeepReadiness, Startup: tc.opts.KeepStartup, InitContainers: tc.opts.KeepInitContainers, } tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile, kflags) if err != nil { t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) } tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() pod, dc, err := tc.opts.generatePodCopyWithDebugContainer(tc.copyPod) if err != nil { t.Fatalf("Fail to generate node debug pod: %v", err) } tc.expected.Spec.Containers[0].Name = dc if diff := cmp.Diff(tc.expected, pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestGenerateEphemeralDebugPodCustomProfile(t *testing.T) { for _, tc := range []struct { name string copyPod *corev1.Pod opts *DebugOptions expected *corev1.Pod }{ { name: "baseline profile", copyPod: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", }, }, opts: &DebugOptions{ SameNode: true, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileBaseline, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: ptr.To(false), }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, }, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, Volumes: nil, }, }, }, { name: "restricted profile", copyPod: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", }, }, opts: &DebugOptions{ SameNode: true, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: ptr.To(false), }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: ptr.To(false), RunAsNonRoot: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, LocalhostProfile: nil, }, }, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, Volumes: nil, }, }, }, { name: "sysadmin profile", copyPod: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", }, }, opts: &DebugOptions{ SameNode: true, Image: "busybox", PullPolicy: corev1.PullIfNotPresent, Profile: ProfileRestricted, CustomProfile: &corev1.Container{ ImagePullPolicy: corev1.PullNever, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: ptr.To(false), }, }, }, expected: &corev1.Pod{ Spec: corev1.PodSpec{ ServiceAccountName: "test", NodeName: "test-node", EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger-1", Image: "busybox", ImagePullPolicy: corev1.PullNever, TerminationMessagePolicy: corev1.TerminationMessageReadFile, VolumeMounts: nil, Stdin: true, TTY: false, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: ptr.To(false), RunAsNonRoot: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, LocalhostProfile: nil, }, }, }, }, }, HostIPC: false, HostNetwork: false, HostPID: false, Volumes: nil, }, }, }, } { t.Run(tc.name, func(t *testing.T) { var err error kflags := KeepFlags{ Labels: tc.opts.KeepLabels, Annotations: tc.opts.KeepAnnotations, Liveness: tc.opts.KeepLiveness, Readiness: tc.opts.KeepReadiness, Startup: tc.opts.KeepStartup, InitContainers: tc.opts.KeepInitContainers, } tc.opts.Applier, err = NewProfileApplier(tc.opts.Profile, kflags) if err != nil { t.Fatalf("Fail to create profile applier: %s: %v", tc.opts.Profile, err) } tc.opts.IOStreams = genericiooptions.NewTestIOStreamsDiscard() pod, ec, err := tc.opts.generateDebugContainer(tc.copyPod) if err != nil { t.Fatalf("Fail to generate node debug pod: %v", err) } tc.expected.Spec.EphemeralContainers[0].Name = ec.Name if diff := cmp.Diff(tc.expected, pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestCompleteAndValidate(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmpFilter := cmp.FilterPath(func(p cmp.Path) bool { switch p.String() { // IOStreams contains unexported fields case "IOStreams", "Applier": return true } return false }, cmp.Ignore()) tests := []struct { name, args string wantOpts *DebugOptions wantError bool }{ { name: "No targets", args: "--image=image", wantError: true, }, { name: "Invalid environment variables", args: "--image=busybox --env=FOO mypod", wantError: true, }, { name: "Invalid image name", args: "--image=image:label@deadbeef mypod", wantError: true, }, { name: "Invalid pull policy", args: "--image=image --image-pull-policy=whenever-you-feel-like-it", wantError: true, }, { name: "TTY without stdin", args: "--image=image --tty", wantError: true, }, { name: "Set image pull policy", args: "--image=busybox --image-pull-policy=Always mypod", wantOpts: &DebugOptions{ Args: []string{}, Image: "busybox", KeepInitContainers: true, Namespace: "test", PullPolicy: corev1.PullPolicy("Always"), ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, { name: "Multiple targets", args: "--image=busybox mypod1 mypod2", wantOpts: &DebugOptions{ Args: []string{}, Image: "busybox", KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod1", "mypod2"}, }, }, { name: "Arguments with dash", args: "--image=busybox mypod1 mypod2 -- echo 1 2", wantOpts: &DebugOptions{ Args: []string{"echo", "1", "2"}, Image: "busybox", KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod1", "mypod2"}, }, }, { name: "Interactive no attach", args: "-ti --image=busybox --attach=false mypod", wantOpts: &DebugOptions{ Args: []string{}, Attach: false, Image: "busybox", KeepInitContainers: true, Interactive: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, }, { name: "Set environment variables", args: "--image=busybox --env=FOO=BAR mypod", wantOpts: &DebugOptions{ Args: []string{}, Env: []corev1.EnvVar{{Name: "FOO", Value: "BAR"}}, Image: "busybox", KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, { name: "Ephemeral container: interactive session minimal args", args: "mypod -it --image=busybox", wantOpts: &DebugOptions{ Args: []string{}, Attach: true, Image: "busybox", Interactive: true, KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, }, { name: "Ephemeral container: non-interactive debugger with image and name", args: "--image=myproj/debug-tools --image-pull-policy=Always -c debugger mypod", wantOpts: &DebugOptions{ Args: []string{}, Container: "debugger", Image: "myproj/debug-tools", KeepInitContainers: true, Namespace: "test", PullPolicy: corev1.PullPolicy("Always"), Profile: ProfileLegacy, ShareProcesses: true, TargetNames: []string{"mypod"}, }, }, { name: "Ephemeral container: no image specified", args: "mypod", wantError: true, }, { name: "Ephemeral container: no image but args", args: "mypod -- echo 1 2", wantError: true, }, { name: "Ephemeral container: replace not allowed", args: "--replace --image=busybox mypod", wantError: true, }, { name: "Ephemeral container: same-node not allowed", args: "--same-node --image=busybox mypod", wantError: true, }, { name: "Ephemeral container: incompatible with --set-image", args: "--set-image=*=busybox mypod", wantError: true, }, { name: "Pod copy: interactive debug container minimal args", args: "mypod -it --image=busybox --copy-to=my-debugger", wantOpts: &DebugOptions{ Args: []string{}, Attach: true, CopyTo: "my-debugger", Image: "busybox", Interactive: true, KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, }, { name: "Pod copy: non-interactive with debug container, image name and command", args: "mypod --image=busybox --container=my-container --copy-to=my-debugger -- sleep 1d", wantOpts: &DebugOptions{ Args: []string{"sleep", "1d"}, Container: "my-container", CopyTo: "my-debugger", Image: "busybox", KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, { name: "Pod copy: explicit attach", args: "mypod --image=busybox --copy-to=my-debugger --attach -- sleep 1d", wantOpts: &DebugOptions{ Args: []string{"sleep", "1d"}, Attach: true, CopyTo: "my-debugger", Image: "busybox", KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, { name: "Pod copy: replace single image of existing container", args: "mypod --image=busybox --container=my-container --copy-to=my-debugger", wantOpts: &DebugOptions{ Args: []string{}, Container: "my-container", CopyTo: "my-debugger", Image: "busybox", KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, { name: "Pod copy: mutate existing container images", args: "mypod --set-image=*=busybox,app=app-debugger --copy-to=my-debugger", wantOpts: &DebugOptions{ Args: []string{}, CopyTo: "my-debugger", KeepInitContainers: true, Namespace: "test", SetImages: map[string]string{ "*": "busybox", "app": "app-debugger", }, ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, }, }, { name: "Pod copy: add container and also mutate images", args: "mypod -it --copy-to=my-debugger --image=debian --set-image=app=app:debug,sidecar=sidecar:debug", wantOpts: &DebugOptions{ Args: []string{}, Attach: true, CopyTo: "my-debugger", Image: "debian", Interactive: true, KeepInitContainers: true, Namespace: "test", SetImages: map[string]string{ "app": "app:debug", "sidecar": "sidecar:debug", }, ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, }, { name: "Pod copy: change command", args: "mypod -it --copy-to=my-debugger --container=mycontainer -- sh", wantOpts: &DebugOptions{ Attach: true, Args: []string{"sh"}, Container: "mycontainer", CopyTo: "my-debugger", Interactive: true, KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, }, { name: "Pod copy: change keep options from defaults", args: "mypod -it --image=busybox --copy-to=my-debugger --keep-labels=true --keep-annotations=true --keep-liveness=true --keep-readiness=true --keep-startup=true --keep-init-containers=false", wantOpts: &DebugOptions{ Args: []string{}, Attach: true, CopyTo: "my-debugger", Image: "busybox", Interactive: true, KeepLabels: true, KeepAnnotations: true, KeepLiveness: true, KeepReadiness: true, KeepStartup: true, KeepInitContainers: false, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"mypod"}, TTY: true, }, }, { name: "Pod copy: no image specified", args: "mypod -it --copy-to=my-debugger", wantError: true, }, { name: "Pod copy: args but no image specified", args: "mypod --copy-to=my-debugger -- echo milo", wantError: true, }, { name: "Pod copy: --target not allowed", args: "mypod --target --image=busybox --copy-to=my-debugger", wantError: true, }, { name: "Pod copy: invalid --set-image", args: "mypod --set-image=*=SUPERGOODIMAGE#1!!!! --copy-to=my-debugger", wantError: true, }, { name: "Pod copy: specifying attach without existing or newly created container", args: "mypod --set-image=*=busybox --copy-to=my-debugger --attach", wantError: true, }, { name: "Node: interactive session minimal args", args: "node/mynode -it --image=busybox", wantOpts: &DebugOptions{ Args: []string{}, Attach: true, Image: "busybox", Interactive: true, KeepInitContainers: true, Namespace: "test", ShareProcesses: true, Profile: ProfileLegacy, TargetNames: []string{"node/mynode"}, TTY: true, }, }, { name: "Node: no image specified", args: "node/mynode -it", wantError: true, }, { name: "Node: --replace not allowed", args: "--image=busybox --replace node/mynode", wantError: true, }, { name: "Node: --same-node not allowed", args: "--image=busybox --same-node node/mynode", wantError: true, }, { name: "Node: --set-image not allowed", args: "--image=busybox --set-image=*=busybox node/mynode", wantError: true, }, { name: "Node: --target not allowed", args: "node/mynode --target --image=busybox", wantError: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { opts := NewDebugOptions(ioStreams) var gotError error cmd := &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { gotError = opts.Complete(tf, cmd, args) if gotError != nil { return } gotError = opts.Validate() }, } cmd.SetArgs(strings.Split(tc.args, " ")) opts.AddFlags(cmd) cmdError := cmd.Execute() if tc.wantError { if cmdError != nil || gotError != nil { return } t.Fatalf("CompleteAndValidate got nil errors but wantError: %v", tc.wantError) } else if cmdError != nil { t.Fatalf("cmd.Execute got error '%v' executing test cobra.Command, wantError: %v", cmdError, tc.wantError) } else if gotError != nil { t.Fatalf("CompleteAndValidate got error: '%v', wantError: %v", gotError, tc.wantError) } if diff := cmp.Diff(tc.wantOpts, opts, cmpFilter, cmpopts.IgnoreFields(DebugOptions{}, "attachChanged", "shareProcessedChanged", "podClient", "WarningPrinter", "Applier", "explicitNamespace", "Builder", "AttachFunc")); diff != "" { t.Error("CompleteAndValidate unexpected diff in generated object: (-want +got):\n", diff) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go000066400000000000000000000311471504711711200300550ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package debug import ( "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubectl/pkg/util/podutils" "k8s.io/utils/ptr" ) type debugStyle int const ( // debug by ephemeral container ephemeral debugStyle = iota // debug by pod copy podCopy // debug node node // unsupported debug methodology unsupported ) const ( // NOTE: when you add a new profile string, remember to add it to the // --profile flag's help text // ProfileLegacy represents the legacy debugging profile which is backwards-compatible with 1.23 behavior. ProfileLegacy = "legacy" // ProfileGeneral contains a reasonable set of defaults tailored for each debugging journey. ProfileGeneral = "general" // ProfileBaseline is identical to "general" but eliminates privileges that are disallowed under // the baseline security profile, such as host namespaces, host volume, mounts and SYS_PTRACE. ProfileBaseline = "baseline" // ProfileRestricted is identical to "baseline" but adds configuration that's required // under the restricted security profile, such as requiring a non-root user and dropping all capabilities. ProfileRestricted = "restricted" // ProfileNetadmin offers elevated privileges for network debugging. ProfileNetadmin = "netadmin" // ProfileSysadmin offers elevated privileges for debugging. ProfileSysadmin = "sysadmin" ) type ProfileApplier interface { // Apply applies the profile to the given container in the pod. Apply(pod *corev1.Pod, containerName string, target runtime.Object) error } // NewProfileApplier returns a new Options for the given profile name. func NewProfileApplier(profile string, kflags KeepFlags) (ProfileApplier, error) { switch profile { case ProfileLegacy: return &legacyProfile{kflags}, nil case ProfileGeneral: return &generalProfile{kflags}, nil case ProfileBaseline: return &baselineProfile{kflags}, nil case ProfileRestricted: return &restrictedProfile{kflags}, nil case ProfileNetadmin: return &netadminProfile{kflags}, nil case ProfileSysadmin: return &sysadminProfile{kflags}, nil } return nil, fmt.Errorf("unknown profile: %s", profile) } type legacyProfile struct { KeepFlags } type generalProfile struct { KeepFlags } type baselineProfile struct { KeepFlags } type restrictedProfile struct { KeepFlags } type netadminProfile struct { KeepFlags } type sysadminProfile struct { KeepFlags } // KeepFlags holds the flag set that determine which fields to keep in the copy pod. type KeepFlags struct { Labels bool Annotations bool Liveness bool Readiness bool Startup bool InitContainers bool } // RemoveLabels removes labels from the pod. func (kflags KeepFlags) RemoveLabels(p *corev1.Pod) { if !kflags.Labels { p.Labels = nil } } // RemoveAnnotations remove annotations from the pod. func (kflags KeepFlags) RemoveAnnotations(p *corev1.Pod) { if !kflags.Annotations { p.Annotations = nil } } // RemoveProbes remove probes from all containers of the pod. func (kflags KeepFlags) RemoveProbes(p *corev1.Pod) { for i := range p.Spec.Containers { if !kflags.Liveness { p.Spec.Containers[i].LivenessProbe = nil } if !kflags.Readiness { p.Spec.Containers[i].ReadinessProbe = nil } if !kflags.Startup { p.Spec.Containers[i].StartupProbe = nil } } } // RemoveInitContainers remove initContainers from the pod. func (kflags KeepFlags) RemoveInitContainers(p *corev1.Pod) { if !kflags.InitContainers { p.Spec.InitContainers = nil } } func getDebugStyle(pod *corev1.Pod, target runtime.Object) (debugStyle, error) { switch target.(type) { case *corev1.Pod: if asserted, ok := target.(*corev1.Pod); ok { if pod != asserted { // comparing addresses return podCopy, nil } } return ephemeral, nil case *corev1.Node: return node, nil } return unsupported, fmt.Errorf("objects of type %T are not supported", target) } func (p *legacyProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { style, err := getDebugStyle(pod, target) if err != nil { return fmt.Errorf("legacy profile: %w", err) } switch style { case node: mountRootPartition(pod, containerName) useHostNamespaces(pod) case podCopy: p.Labels = false p.RemoveLabels(pod) case ephemeral: // no additional modifications needed } return nil } func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { style, err := getDebugStyle(pod, target) if err != nil { return fmt.Errorf("general profile: %w", err) } switch style { case node: mountRootPartition(pod, containerName) clearSecurityContext(pod, containerName) useHostNamespaces(pod) case podCopy: p.RemoveLabels(pod) p.RemoveAnnotations(pod) p.RemoveProbes(pod) p.RemoveInitContainers(pod) allowProcessTracing(pod, containerName) shareProcessNamespace(pod) case ephemeral: allowProcessTracing(pod, containerName) } return nil } func (p *baselineProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { style, err := getDebugStyle(pod, target) if err != nil { return fmt.Errorf("baseline profile: %w", err) } clearSecurityContext(pod, containerName) switch style { case podCopy: p.RemoveLabels(pod) p.RemoveAnnotations(pod) p.RemoveProbes(pod) p.RemoveInitContainers(pod) shareProcessNamespace(pod) case ephemeral, node: // no additional modifications needed } return nil } func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { style, err := getDebugStyle(pod, target) if err != nil { return fmt.Errorf("restricted profile: %w", err) } clearSecurityContext(pod, containerName) disallowRoot(pod, containerName) dropCapabilities(pod, containerName) disallowPrivilegeEscalation(pod, containerName) setSeccompProfile(pod, containerName) switch style { case podCopy: p.RemoveLabels(pod) p.RemoveAnnotations(pod) p.RemoveProbes(pod) p.RemoveInitContainers(pod) shareProcessNamespace(pod) case ephemeral, node: // no additional modifications needed } return nil } func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { style, err := getDebugStyle(pod, target) if err != nil { return fmt.Errorf("netadmin profile: %w", err) } allowNetadminCapability(pod, containerName) switch style { case node: useHostNamespaces(pod) case podCopy: p.RemoveLabels(pod) p.RemoveAnnotations(pod) p.RemoveProbes(pod) p.RemoveInitContainers(pod) shareProcessNamespace(pod) case ephemeral: // no additional modifications needed } return nil } func (p *sysadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { style, err := getDebugStyle(pod, target) if err != nil { return fmt.Errorf("sysadmin profile: %w", err) } setPrivileged(pod, containerName) switch style { case node: useHostNamespaces(pod) mountRootPartition(pod, containerName) case podCopy: // to mimic general, default and baseline p.RemoveLabels(pod) p.RemoveAnnotations(pod) p.RemoveProbes(pod) p.RemoveInitContainers(pod) shareProcessNamespace(pod) case ephemeral: // no additional modifications needed } return nil } // mountRootPartition mounts the host's root path at "/host" in the container. func mountRootPartition(p *corev1.Pod, containerName string) { const volumeName = "host-root" p.Spec.Volumes = append(p.Spec.Volumes, corev1.Volume{ Name: volumeName, VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }) podutils.VisitContainers(&p.Spec, podutils.Containers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{ MountPath: "/host", Name: volumeName, }) return false }) } // useHostNamespaces configures the pod to use the host's network, PID, and IPC // namespaces. func useHostNamespaces(p *corev1.Pod) { p.Spec.HostNetwork = true p.Spec.HostPID = true p.Spec.HostIPC = true } // shareProcessNamespace configures all containers in the pod to share the // process namespace. func shareProcessNamespace(p *corev1.Pod) { if p.Spec.ShareProcessNamespace == nil { p.Spec.ShareProcessNamespace = ptr.To(true) } } // clearSecurityContext clears the security context for the container. func clearSecurityContext(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } c.SecurityContext = nil return false }) } // setPrivileged configures the containers as privileged. func setPrivileged(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } if c.SecurityContext == nil { c.SecurityContext = &corev1.SecurityContext{} } c.SecurityContext.Privileged = ptr.To(true) return false }) } // disallowRoot configures the container to run as a non-root user. func disallowRoot(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } if c.SecurityContext == nil { c.SecurityContext = &corev1.SecurityContext{} } c.SecurityContext.RunAsNonRoot = ptr.To(true) return false }) } // dropCapabilities drops all Capabilities for the container func dropCapabilities(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } if c.SecurityContext == nil { c.SecurityContext = &corev1.SecurityContext{} } if c.SecurityContext.Capabilities == nil { c.SecurityContext.Capabilities = &corev1.Capabilities{} } c.SecurityContext.Capabilities.Drop = []corev1.Capability{"ALL"} c.SecurityContext.Capabilities.Add = nil return false }) } // allowProcessTracing grants the SYS_PTRACE capability to the container. func allowProcessTracing(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } addCapability(c, "SYS_PTRACE") return false }) } // allowNetadminCapability grants NET_ADMIN and NET_RAW capability to the container. func allowNetadminCapability(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } addCapability(c, "NET_ADMIN") addCapability(c, "NET_RAW") return false }) } func addCapability(c *corev1.Container, capability corev1.Capability) { if c.SecurityContext == nil { c.SecurityContext = &corev1.SecurityContext{} } if c.SecurityContext.Capabilities == nil { c.SecurityContext.Capabilities = &corev1.Capabilities{} } c.SecurityContext.Capabilities.Add = append(c.SecurityContext.Capabilities.Add, capability) } // disallowPrivilegeEscalation configures the containers not allowed PrivilegeEscalation func disallowPrivilegeEscalation(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } if c.SecurityContext == nil { c.SecurityContext = &corev1.SecurityContext{} } c.SecurityContext.AllowPrivilegeEscalation = ptr.To(false) return false }) } // setSeccompProfile apply SeccompProfile to the containers func setSeccompProfile(p *corev1.Pod, containerName string) { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { if c.Name != containerName { return true } if c.SecurityContext == nil { c.SecurityContext = &corev1.SecurityContext{} } c.SecurityContext.SeccompProfile = &corev1.SeccompProfile{Type: "RuntimeDefault"} return false }) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles_test.go000066400000000000000000001013061504711711200311070ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package debug import ( "fmt" "testing" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" ) var testNode = &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-XXX", }, } func TestLegacyProfile(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", }, }, }}, } tests := map[string]struct { pod *corev1.Pod containerName string target runtime.Object expectPod *corev1.Pod expectErr bool }{ "bad inputs results in error": { pod: nil, containerName: "dbg", target: runtime.Object(nil), expectErr: true, }, "debug by ephemeral container": { pod: pod, containerName: "dbg", target: pod, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{Name: "dbg", Image: "dbgimage"}, }, }}, }, }, "debug by pod copy": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN"}, }, }, }, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN"}, }, }, }, }, }, }, }, "debug by node": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN"}, }, }, }, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ HostNetwork: true, HostPID: true, HostIPC: true, Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN"}, }, }, VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { applier := &legacyProfile{KeepFlags{InitContainers: true}} err := applier.Apply(test.pod, test.containerName, test.target) if (err != nil) != test.expectErr { t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) } if err != nil { return } if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestGeneralProfile(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", }, }, }}, } tests := map[string]struct { pod *corev1.Pod containerName string target runtime.Object expectPod *corev1.Pod expectErr bool }{ "bad inputs results in error": { pod: nil, containerName: "dbg", target: runtime.Object(nil), expectErr: true, }, "debug by ephemeral container": { pod: pod, containerName: "dbg", target: pod, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }}, }, }, "debug by pod copy": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN"}, }, }, }, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "SYS_PTRACE"}, }, }, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, "debug by node": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ HostNetwork: true, HostPID: true, HostIPC: true, Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", VolumeMounts: []corev1.VolumeMount{ { MountPath: "/host", Name: "host-root", }, }, }, }, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{Path: "/"}, }, }, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { applier := &generalProfile{KeepFlags{InitContainers: true}} err := applier.Apply(test.pod, test.containerName, test.target) if (err != nil) != test.expectErr { t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) } if err != nil { return } if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestBaselineProfile(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }}, } tests := map[string]struct { pod *corev1.Pod containerName string target runtime.Object expectPod *corev1.Pod expectErr bool }{ "bad inputs results in error": { pod: nil, containerName: "dbg", target: runtime.Object(nil), expectErr: true, }, "debug by ephemeral container": { pod: pod, containerName: "dbg", target: pod, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", }, }, }}, }, }, "debug by pod copy": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ ShareProcessNamespace: ptr.To(true), InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, { Name: "dbg", Image: "dbgimage", }, }, }, }, }, "debug by node": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", }, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { applier := &baselineProfile{KeepFlags{InitContainers: true}} err := applier.Apply(test.pod, test.containerName, test.target) if (err != nil) != test.expectErr { t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) } if err != nil { return } if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestRestrictedProfile(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }}, } tests := map[string]struct { pod *corev1.Pod containerName string target runtime.Object expectPod *corev1.Pod expectErr bool }{ "bad inputs results in error": { pod: nil, containerName: "dbg", target: runtime.Object(nil), expectErr: true, }, "debug by ephemeral container": { pod: pod, containerName: "dbg", target: pod, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, }}, }, }, "debug by pod copy": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ ShareProcessNamespace: ptr.To(true), InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, }, }, }, "debug by node": { pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, AllowPrivilegeEscalation: ptr.To(false), SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, }, }, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { applier := &restrictedProfile{KeepFlags{InitContainers: true}} err := applier.Apply(test.pod, test.containerName, test.target) if (err != nil) != test.expectErr { t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) } if err != nil { return } if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestNetAdminProfile(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", }, }, }}, } tests := []struct { name string pod *corev1.Pod containerName string target runtime.Object expectPod *corev1.Pod expectErr error }{ { name: "nil target", pod: pod, containerName: "dbg", target: nil, expectErr: fmt.Errorf("netadmin profile: objects of type are not supported"), }, { name: "debug by ephemeral container", pod: pod, containerName: "dbg", target: pod, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, }, }, }, }, }}, }, }, { name: "debug by pod copy", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ ShareProcessNamespace: ptr.To(true), InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, }, }, }, }, }, }, }, { name: "debug by pod copy preserve existing capability", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ ShareProcessNamespace: ptr.To(true), Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN", "NET_RAW"}, }, }, }, }, }, }, }, { name: "debug by node", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ HostNetwork: true, HostPID: true, HostIPC: true, Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, }, }, }, }, }, }, }, { name: "debug by node preserve existing capability", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ HostNetwork: true, HostPID: true, HostIPC: true, Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN", "NET_RAW"}, }, }, }, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { applier := &netadminProfile{KeepFlags{InitContainers: true}} err := applier.Apply(test.pod, test.containerName, test.target) if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) { t.Fatalf("expect error: %v, got error: %v", test.expectErr, err) } if err != nil { return } if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } func TestSysAdminProfile(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", }, }, }}, } tests := []struct { name string pod *corev1.Pod containerName string target runtime.Object expectPod *corev1.Pod expectErr error }{ { name: "nil target", pod: pod, containerName: "dbg", target: nil, expectErr: fmt.Errorf("sysadmin profile: objects of type are not supported"), }, { name: "debug by ephemeral container", pod: pod, containerName: "dbg", target: pod, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Privileged: ptr.To(true), }, }, }, }}, }, }, { name: "debug by pod copy", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "podcopy", Labels: map[string]string{ "app": "podcopy", }, Annotations: map[string]string{ "test": "test", }, }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{{Name: "init-container"}}, Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Privileged: ptr.To(true), }, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "debug by pod copy preserve existing capability", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }, }, containerName: "dbg", target: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "app", Image: "appimage", LivenessProbe: &corev1.Probe{}, ReadinessProbe: &corev1.Probe{}, StartupProbe: &corev1.Probe{}, }, }, }, }, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "app", Image: "appimage"}, { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Privileged: ptr.To(true), Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, ShareProcessNamespace: ptr.To(true), }, }, }, { name: "debug by node", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "dbg", Image: "dbgimage"}, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ HostNetwork: true, HostPID: true, HostIPC: true, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}}, }, }, Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Privileged: ptr.To(true), }, VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}}, }, }, }, }, }, { name: "debug by node preserve existing capability", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, }, }, }, }, containerName: "dbg", target: testNode, expectPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "pod"}, Spec: corev1.PodSpec{ HostNetwork: true, HostPID: true, HostIPC: true, Volumes: []corev1.Volume{ { Name: "host-root", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}}, }, }, Containers: []corev1.Container{ { Name: "dbg", Image: "dbgimage", SecurityContext: &corev1.SecurityContext{ Privileged: ptr.To(true), Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"SYS_PTRACE"}, }, }, VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}}, }, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { applier := &sysadminProfile{KeepFlags{InitContainers: true}} err := applier.Apply(test.pod, test.containerName, test.target) if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) { t.Fatalf("expect error: %v, got error: %v", test.expectErr, err) } if err != nil { return } if diff := cmp.Diff(test.expectPod, test.pod); diff != "" { t.Error("unexpected diff in generated object: (-want +got):\n", diff) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/delete/000077500000000000000000000000001504711711200260515ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go000066400000000000000000000414771504711711200276570ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package delete import ( "fmt" "net/url" "strings" "time" "github.com/spf13/cobra" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdwait "k8s.io/kubectl/pkg/cmd/wait" "k8s.io/kubectl/pkg/rawhttp" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" ) var ( deleteLong = templates.LongDesc(i18n.T(` Delete resources by file names, stdin, resources and names, or by resources and label selector. JSON and YAML formats are accepted. Only one type of argument may be specified: file names, resources and names, or resources and label selector. Some resources, such as pods, support graceful deletion. These resources define a default period before they are forcibly terminated (the grace period) but you may override that value with the --grace-period flag, or pass --now to set a grace-period of 1. Because these resources often represent entities in the cluster, deletion may not be acknowledged immediately. If the node hosting a pod is down or cannot reach the API server, termination may take significantly longer than the grace period. To force delete a resource, you must specify the --force flag. Note: only a subset of resources support graceful deletion. In absence of the support, the --grace-period flag is ignored. IMPORTANT: Force deleting pods does not wait for confirmation that the pod's processes have been terminated, which can leave those processes running until the node detects the deletion and completes graceful deletion. If your processes use shared storage or talk to a remote API and depend on the name of the pod to identify themselves, force deleting those pods may result in multiple processes running on different machines using the same identification which may lead to data corruption or inconsistency. Only force delete pods when you are sure the pod is terminated, or if your application can tolerate multiple copies of the same pod running at once. Also, if you force delete pods, the scheduler may place new pods on those nodes before the node has released those resources and causing those pods to be evicted immediately. Note that the delete command does NOT do resource version checks, so if someone submits an update to a resource right when you submit a delete, their update will be lost along with the rest of the resource. After a CustomResourceDefinition is deleted, invalidation of discovery cache may take up to 6 hours. If you don't want to wait, you might want to run "kubectl api-resources" to refresh the discovery cache.`)) deleteExample = templates.Examples(i18n.T(` # Delete a pod using the type and name specified in pod.json kubectl delete -f ./pod.json # Delete resources from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml kubectl delete -k dir # Delete resources from all files that end with '.json' kubectl delete -f '*.json' # Delete a pod based on the type and name in the JSON passed into stdin cat pod.json | kubectl delete -f - # Delete pods and services with same names "baz" and "foo" kubectl delete pod,service baz foo # Delete pods and services with label name=myLabel kubectl delete pods,services -l name=myLabel # Delete a pod with minimal delay kubectl delete pod foo --now # Force delete a pod on a dead node kubectl delete pod foo --force # Delete all pods kubectl delete pods --all # Delete all pods only if the user confirms the deletion kubectl delete pods --all --interactive`)) ) type DeleteOptions struct { resource.FilenameOptions LabelSelector string FieldSelector string DeleteAll bool DeleteAllNamespaces bool CascadingStrategy metav1.DeletionPropagation IgnoreNotFound bool DeleteNow bool ForceDeletion bool WaitForDeletion bool Quiet bool WarnClusterScope bool Raw string Interactive bool GracePeriod int Timeout time.Duration DryRunStrategy cmdutil.DryRunStrategy Output string DynamicClient dynamic.Interface Mapper meta.RESTMapper Result *resource.Result PreviewResult *resource.Result previewResourceMap map[cmdwait.ResourceLocation]struct{} genericiooptions.IOStreams WarningPrinter *printers.WarningPrinter } func NewCmdDelete(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { deleteFlags := NewDeleteCommandFlags("containing the resource to delete.") cmd := &cobra.Command{ Use: "delete ([-f FILENAME] | [-k DIRECTORY] | TYPE [(NAME | -l label | --all)])", DisableFlagsInUseLine: true, Short: i18n.T("Delete resources by file names, stdin, resources and names, or by resources and label selector"), Long: deleteLong, Example: deleteExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { o, err := deleteFlags.ToOptions(nil, streams) cmdutil.CheckErr(err) cmdutil.CheckErr(o.Complete(f, args, cmd)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunDelete(f)) }, SuggestFor: []string{"rm"}, } deleteFlags.AddFlags(cmd) cmdutil.AddDryRunFlag(cmd) return cmd } func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) error { cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.WarnClusterScope = enforceNamespace && !o.DeleteAllNamespaces if o.DeleteAll || len(o.LabelSelector) > 0 || len(o.FieldSelector) > 0 { if f := cmd.Flags().Lookup("ignore-not-found"); f != nil && !f.Changed { // If the user didn't explicitly set the option, default to ignoring NotFound errors when used with --all, -l, or --field-selector o.IgnoreNotFound = true } } if o.DeleteNow { if o.GracePeriod != -1 { return fmt.Errorf("--now and --grace-period cannot be specified together") } o.GracePeriod = 1 } if o.GracePeriod == 0 && !o.ForceDeletion { // To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0 // into --grace-period=1. Users may provide --force to bypass this conversion. o.GracePeriod = 1 } if o.ForceDeletion && o.GracePeriod < 0 { o.GracePeriod = 0 } o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } // Set default WarningPrinter if not already set. if o.WarningPrinter == nil { o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) } if len(o.Raw) != 0 { return nil } r := f.NewBuilder(). Unstructured(). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). FieldSelectorParam(o.FieldSelector). SelectAllParam(o.DeleteAll). AllNamespaces(o.DeleteAllNamespaces). ResourceTypeOrNameArgs(false, args...).RequireObject(false). Flatten(). Do() err = r.Err() if err != nil { return err } o.Result = r if o.Interactive { // preview result will be used to list resources for confirmation prior to actual delete. // We can not use r as result object because it can only be used once. But we need to traverse // twice. Parameters in preview result must be equal to genuine result. previewr := f.NewBuilder(). Unstructured(). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). FieldSelectorParam(o.FieldSelector). SelectAllParam(o.DeleteAll). AllNamespaces(o.DeleteAllNamespaces). ResourceTypeOrNameArgs(false, args...).RequireObject(false). Flatten(). Do() err = previewr.Err() if err != nil { return err } o.PreviewResult = previewr o.previewResourceMap = make(map[cmdwait.ResourceLocation]struct{}) } o.Mapper, err = f.ToRESTMapper() if err != nil { return err } o.DynamicClient, err = f.DynamicClient() if err != nil { return err } return nil } func (o *DeleteOptions) Validate() error { if o.Output != "" && o.Output != "name" { return fmt.Errorf("unexpected -o output mode: %v. We only support '-o name'", o.Output) } if o.DeleteAll && len(o.LabelSelector) > 0 { return fmt.Errorf("cannot set --all and --selector at the same time") } if o.DeleteAll && len(o.FieldSelector) > 0 { return fmt.Errorf("cannot set --all and --field-selector at the same time") } if o.WarningPrinter == nil { return fmt.Errorf("WarningPrinter can not be used without initialization") } switch { case o.GracePeriod == 0 && o.ForceDeletion: o.WarningPrinter.Print("Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.") case o.GracePeriod > 0 && o.ForceDeletion: return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together") } if len(o.Raw) == 0 { return nil } if o.Interactive { return fmt.Errorf("--interactive can not be used with --raw") } if len(o.FilenameOptions.Filenames) > 1 { return fmt.Errorf("--raw can only use a single local file or stdin") } else if len(o.FilenameOptions.Filenames) == 1 { if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 { return fmt.Errorf("--raw cannot read from a url") } } if o.FilenameOptions.Recursive { return fmt.Errorf("--raw and --recursive are mutually exclusive") } if len(o.Output) > 0 { return fmt.Errorf("--raw and --output are mutually exclusive") } if _, err := url.ParseRequestURI(o.Raw); err != nil { return fmt.Errorf("--raw must be a valid URL path: %v", err) } return nil } func (o *DeleteOptions) RunDelete(f cmdutil.Factory) error { if len(o.Raw) > 0 { restClient, err := f.RESTClient() if err != nil { return err } if len(o.Filenames) == 0 { return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, "") } return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0]) } if o.Interactive { previewInfos := []*resource.Info{} if o.IgnoreNotFound { o.PreviewResult = o.PreviewResult.IgnoreErrors(errors.IsNotFound) } err := o.PreviewResult.Visit(func(info *resource.Info, err error) error { if err != nil { return err } previewInfos = append(previewInfos, info) o.previewResourceMap[cmdwait.ResourceLocation{ GroupResource: info.Mapping.Resource.GroupResource(), Namespace: info.Namespace, Name: info.Name, }] = struct{}{} return nil }) if err != nil { return err } if len(previewInfos) == 0 { fmt.Fprintf(o.Out, "No resources found\n") return nil } if !o.confirmation(previewInfos) { fmt.Fprintf(o.Out, "deletion is cancelled\n") return nil } } return o.DeleteResult(o.Result) } func (o *DeleteOptions) DeleteResult(r *resource.Result) error { found := 0 if o.IgnoreNotFound { r = r.IgnoreErrors(errors.IsNotFound) } warnClusterScope := o.WarnClusterScope deletedInfos := []*resource.Info{} uidMap := cmdwait.UIDMap{} err := r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } if o.Interactive { if _, ok := o.previewResourceMap[cmdwait.ResourceLocation{ GroupResource: info.Mapping.Resource.GroupResource(), Namespace: info.Namespace, Name: info.Name, }]; !ok { // resource not in the list of previewed resources based on resourceLocation return nil } } deletedInfos = append(deletedInfos, info) found++ options := &metav1.DeleteOptions{} if o.GracePeriod >= 0 { options = metav1.NewDeleteOptions(int64(o.GracePeriod)) } options.PropagationPolicy = &o.CascadingStrategy if warnClusterScope && info.Mapping.Scope.Name() == meta.RESTScopeNameRoot { o.WarningPrinter.Print("deleting cluster-scoped resources, not scoped to the provided namespace") warnClusterScope = false } if o.DryRunStrategy == cmdutil.DryRunClient { if !o.Quiet { o.PrintObj(info) } return nil } response, err := o.deleteResource(info, options) if err != nil { return err } resourceLocation := cmdwait.ResourceLocation{ GroupResource: info.Mapping.Resource.GroupResource(), Namespace: info.Namespace, Name: info.Name, } if status, ok := response.(*metav1.Status); ok && status.Details != nil { uidMap[resourceLocation] = status.Details.UID return nil } responseMetadata, err := meta.Accessor(response) if err != nil { // we don't have UID, but we didn't fail the delete, next best thing is just skipping the UID klog.V(1).Info(err) return nil } uidMap[resourceLocation] = responseMetadata.GetUID() return nil }) if err != nil { return err } if found == 0 { fmt.Fprintf(o.Out, "No resources found\n") return nil } if !o.WaitForDeletion { return nil } // if we don't have a dynamic client, we don't want to wait. Eventually when delete is cleaned up, this will likely // drop out. if o.DynamicClient == nil { return nil } // If we are dry-running, then we don't want to wait if o.DryRunStrategy != cmdutil.DryRunNone { return nil } effectiveTimeout := o.Timeout if effectiveTimeout == 0 { // if we requested to wait forever, set it to a week. effectiveTimeout = 168 * time.Hour } waitOptions := cmdwait.WaitOptions{ ResourceFinder: genericclioptions.ResourceFinderForResult(resource.InfoListVisitor(deletedInfos)), UIDMap: uidMap, DynamicClient: o.DynamicClient, Timeout: effectiveTimeout, Printer: printers.NewDiscardingPrinter(), ConditionFn: cmdwait.IsDeleted, IOStreams: o.IOStreams, } err = waitOptions.RunWait() if errors.IsForbidden(err) || errors.IsMethodNotSupported(err) { // if we're forbidden from waiting, we shouldn't fail. // if the resource doesn't support a verb we need, we shouldn't fail. klog.V(1).Info(err) return nil } return err } func (o *DeleteOptions) deleteResource(info *resource.Info, deleteOptions *metav1.DeleteOptions) (runtime.Object, error) { deleteResponse, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). DeleteWithOptions(info.Namespace, info.Name, deleteOptions) if err != nil { return nil, cmdutil.AddSourceToErr("deleting", info.Source, err) } if !o.Quiet { o.PrintObj(info) } return deleteResponse, nil } // PrintObj for deleted objects is special because we do not have an object to print. // This mirrors name printer behavior func (o *DeleteOptions) PrintObj(info *resource.Info) { operation := "deleted" groupKind := info.Mapping.GroupVersionKind kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group) if len(groupKind.Group) == 0 { kindString = strings.ToLower(groupKind.Kind) } if o.GracePeriod == 0 { operation = "force deleted" } switch o.DryRunStrategy { case cmdutil.DryRunClient: operation = fmt.Sprintf("%s (dry run)", operation) case cmdutil.DryRunServer: operation = fmt.Sprintf("%s (server dry run)", operation) } if o.Output == "name" { // -o name: prints resource/name fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name) return } // understandable output by default fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation) } func (o *DeleteOptions) confirmation(infos []*resource.Info) bool { fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos)) for _, info := range infos { groupKind := info.Mapping.GroupVersionKind kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group) if len(groupKind.Group) == 0 { kindString = strings.ToLower(groupKind.Kind) } fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name) } fmt.Fprint(o.Out, i18n.T("Do you want to continue?")+" (y/n): ") var input string _, err := fmt.Fscan(o.In, &input) if err != nil { return false } return strings.EqualFold(input, "y") } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_flags.go000066400000000000000000000207641504711711200310270ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package delete import ( "fmt" "strconv" "time" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/dynamic" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) // DeleteFlags composes common printer flag structs // used for commands requiring deletion logic. type DeleteFlags struct { FileNameFlags *genericclioptions.FileNameFlags LabelSelector *string FieldSelector *string All *bool AllNamespaces *bool CascadingStrategy *string Force *bool GracePeriod *int IgnoreNotFound *bool Now *bool Timeout *time.Duration Wait *bool Output *string Raw *string Interactive *bool } func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericiooptions.IOStreams) (*DeleteOptions, error) { options := &DeleteOptions{ DynamicClient: dynamicClient, IOStreams: streams, } // add filename options if f.FileNameFlags != nil { options.FilenameOptions = f.FileNameFlags.ToOptions() } if f.LabelSelector != nil { options.LabelSelector = *f.LabelSelector } if f.FieldSelector != nil { options.FieldSelector = *f.FieldSelector } // add output format if f.Output != nil { options.Output = *f.Output } if f.All != nil { options.DeleteAll = *f.All } if f.AllNamespaces != nil { options.DeleteAllNamespaces = *f.AllNamespaces } if f.CascadingStrategy != nil { var err error options.CascadingStrategy, err = parseCascadingFlag(streams, *f.CascadingStrategy) if err != nil { return nil, err } } if f.Force != nil { options.ForceDeletion = *f.Force } if f.GracePeriod != nil { options.GracePeriod = *f.GracePeriod } if f.IgnoreNotFound != nil { options.IgnoreNotFound = *f.IgnoreNotFound } if f.Now != nil { options.DeleteNow = *f.Now } if f.Timeout != nil { options.Timeout = *f.Timeout } if f.Wait != nil { options.WaitForDeletion = *f.Wait } if f.Raw != nil { options.Raw = *f.Raw } if f.Interactive != nil { options.Interactive = *f.Interactive } return options, nil } func (f *DeleteFlags) AddFlags(cmd *cobra.Command) { f.FileNameFlags.AddFlags(cmd.Flags()) if f.LabelSelector != nil { cmdutil.AddLabelSelectorFlagVar(cmd, f.LabelSelector) } if f.FieldSelector != nil { cmd.Flags().StringVarP(f.FieldSelector, "field-selector", "", *f.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") } if f.All != nil { cmd.Flags().BoolVar(f.All, "all", *f.All, "Delete all resources, in the namespace of the specified resource types.") } if f.AllNamespaces != nil { cmd.Flags().BoolVarP(f.AllNamespaces, "all-namespaces", "A", *f.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") } if f.Force != nil { cmd.Flags().BoolVar(f.Force, "force", *f.Force, "If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation.") } if f.CascadingStrategy != nil { cmd.Flags().StringVar( f.CascadingStrategy, "cascade", *f.CascadingStrategy, `Must be "background", "orphan", or "foreground". Selects the deletion cascading strategy for the dependents (e.g. Pods created by a ReplicationController). Defaults to background.`) cmd.Flags().Lookup("cascade").NoOptDefVal = "background" } if f.Now != nil { cmd.Flags().BoolVar(f.Now, "now", *f.Now, "If true, resources are signaled for immediate shutdown (same as --grace-period=1).") } if f.GracePeriod != nil { cmd.Flags().IntVar(f.GracePeriod, "grace-period", *f.GracePeriod, "Period of time in seconds given to the resource to terminate gracefully. Ignored if negative. Set to 1 for immediate shutdown. Can only be set to 0 when --force is true (force deletion).") } if f.Timeout != nil { cmd.Flags().DurationVar(f.Timeout, "timeout", *f.Timeout, "The length of time to wait before giving up on a delete, zero means determine a timeout from the size of the object") } if f.IgnoreNotFound != nil { cmd.Flags().BoolVar(f.IgnoreNotFound, "ignore-not-found", *f.IgnoreNotFound, "Treat \"resource not found\" as a successful delete. Defaults to \"true\" when --all is specified.") } if f.Wait != nil { cmd.Flags().BoolVar(f.Wait, "wait", *f.Wait, "If true, wait for resources to be gone before returning. This waits for finalizers.") } if f.Output != nil { cmd.Flags().StringVarP(f.Output, "output", "o", *f.Output, "Output mode. Use \"-o name\" for shorter output (resource/name).") } if f.Raw != nil { cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.") } if f.Interactive != nil { cmd.Flags().BoolVarP(f.Interactive, "interactive", "i", *f.Interactive, "If true, delete resource only when user confirms.") } } // NewDeleteCommandFlags provides default flags and values for use with the "delete" command func NewDeleteCommandFlags(usage string) *DeleteFlags { cascadingStrategy := "background" gracePeriod := -1 // setup command defaults all := false allNamespaces := false force := false ignoreNotFound := false now := false output := "" labelSelector := "" fieldSelector := "" timeout := time.Duration(0) wait := true raw := "" interactive := false filenames := []string{} recursive := false kustomize := "" return &DeleteFlags{ // Not using helpers.go since it provides function to add '-k' for FileNameOptions, but not FileNameFlags FileNameFlags: &genericclioptions.FileNameFlags{Usage: usage, Filenames: &filenames, Kustomize: &kustomize, Recursive: &recursive}, LabelSelector: &labelSelector, FieldSelector: &fieldSelector, CascadingStrategy: &cascadingStrategy, GracePeriod: &gracePeriod, All: &all, AllNamespaces: &allNamespaces, Force: &force, IgnoreNotFound: &ignoreNotFound, Now: &now, Timeout: &timeout, Wait: &wait, Output: &output, Raw: &raw, Interactive: &interactive, } } // NewDeleteFlags provides default flags and values for use in commands outside of "delete" func NewDeleteFlags(usage string) *DeleteFlags { cascadingStrategy := "background" gracePeriod := -1 force := false timeout := time.Duration(0) wait := false filenames := []string{} kustomize := "" recursive := false return &DeleteFlags{ FileNameFlags: &genericclioptions.FileNameFlags{Usage: usage, Filenames: &filenames, Kustomize: &kustomize, Recursive: &recursive}, CascadingStrategy: &cascadingStrategy, GracePeriod: &gracePeriod, // add non-defaults Force: &force, Timeout: &timeout, Wait: &wait, } } func parseCascadingFlag(streams genericiooptions.IOStreams, cascadingFlag string) (metav1.DeletionPropagation, error) { boolValue, err := strconv.ParseBool(cascadingFlag) // The flag is not a boolean if err != nil { switch cascadingFlag { case "orphan": return metav1.DeletePropagationOrphan, nil case "foreground": return metav1.DeletePropagationForeground, nil case "background": return metav1.DeletePropagationBackground, nil default: return metav1.DeletePropagationBackground, fmt.Errorf(`invalid cascade value (%v). Must be "background", "foreground", or "orphan"`, cascadingFlag) } } // The flag was a boolean if boolValue { fmt.Fprintf(streams.ErrOut, "warning: --cascade=%v is deprecated (boolean value) and can be replaced with --cascade=%s.\n", cascadingFlag, "background") return metav1.DeletePropagationBackground, nil } fmt.Fprintf(streams.ErrOut, "warning: --cascade=%v is deprecated (boolean value) and can be replaced with --cascade=%s.\n", cascadingFlag, "orphan") return metav1.DeletePropagationOrphan, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_test.go000066400000000000000000001105531504711711200307060ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package delete import ( "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "testing" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/utils/ptr" ) func fakecmd() *cobra.Command { cmd := &cobra.Command{ Use: "delete ([-f FILENAME] | TYPE [(NAME | -l label | --all)])", DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) {}, } cmdutil.AddDryRunFlag(cmd) return cmd } func TestDeleteFlagValidation(t *testing.T) { f := cmdtesting.NewTestFactory() defer f.Cleanup() tests := []struct { flags DeleteFlags args [][]string expectedErr string }{ { flags: DeleteFlags{ Raw: ptr.To("test"), Interactive: ptr.To(true), }, expectedErr: "--interactive can not be used with --raw", }, } for _, test := range tests { cmd := fakecmd() deleteOptions, err := test.flags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard()) if err != nil { t.Fatalf("unexpected error creating delete options: %s", err) } deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"} err = deleteOptions.Complete(f, nil, cmd) if err != nil { t.Fatalf("unexpected error creating delete options: %s", err) } err = deleteOptions.Validate() if err == nil { t.Fatalf("missing expected error") } if test.expectedErr != err.Error() { t.Errorf("expected error %s, got %s", test.expectedErr, err) } } } func TestDeleteObjectByTuple(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { // replication controller with cascade off case p == "/namespaces/test/replicationcontrollers/redis-master-controller" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil // secret with cascade on, but no client-side reaper case p == "/namespaces/test/secrets/mysecret" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: // Ensures no GET is performed when deleting by name t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"replicationcontrollers/redis-master-controller"}) if buf.String() != "replicationcontroller/redis-master-controller\n" { t.Errorf("unexpected output: %s", buf.String()) } // Test cascading delete of object without client-side reaper doesn't make GET requests streams, _, buf, _ = genericiooptions.NewTestIOStreams() cmd = NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"secrets/mysecret"}) if buf.String() != "secret/mysecret\n" { t.Errorf("unexpected output: %s", buf.String()) } } func hasExpectedPropagationPolicy(body io.ReadCloser, policy *metav1.DeletionPropagation) bool { if body == nil || policy == nil { return body == nil && policy == nil } var parsedBody metav1.DeleteOptions rawBody, _ := io.ReadAll(body) json.Unmarshal(rawBody, &parsedBody) if parsedBody.PropagationPolicy == nil { return false } return *policy == *parsedBody.PropagationPolicy } // TestCascadingStrategy tests that DeleteOptions.DeletionPropagation is appropriately set while deleting objects. func TestCascadingStrategy(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) var policy *metav1.DeletionPropagation tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m, b := req.URL.Path, req.Method, req.Body; { case p == "/namespaces/test/secrets/mysecret" && m == "DELETE" && hasExpectedPropagationPolicy(b, policy): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: return nil, nil } }), } // DeleteOptions.PropagationPolicy should be Background, when cascading strategy is empty (default). backgroundPolicy := metav1.DeletePropagationBackground policy = &backgroundPolicy streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"secrets/mysecret"}) if buf.String() != "secret/mysecret\n" { t.Errorf("unexpected output: %s", buf.String()) } // DeleteOptions.PropagationPolicy should be Foreground, when cascading strategy is foreground. foregroundPolicy := metav1.DeletePropagationForeground policy = &foregroundPolicy streams, _, buf, _ = genericiooptions.NewTestIOStreams() cmd = NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("cascade", "foreground") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"secrets/mysecret"}) if buf.String() != "secret/mysecret\n" { t.Errorf("unexpected output: %s", buf.String()) } // Test that delete options should be set to orphan when cascading strategy is orphan. orphanPolicy := metav1.DeletePropagationOrphan policy = &orphanPolicy streams, _, buf, _ = genericiooptions.NewTestIOStreams() cmd = NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("cascade", "orphan") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"secrets/mysecret"}) if buf.String() != "secret/mysecret\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteNamedObject(t *testing.T) { cmdtesting.InitTestErrorHandler(t) cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { // replication controller with cascade off case p == "/namespaces/test/replicationcontrollers/redis-master-controller" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil // secret with cascade on, but no client-side reaper case p == "/namespaces/test/secrets/mysecret" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: // Ensures no GET is performed when deleting by name t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"replicationcontrollers", "redis-master-controller"}) if buf.String() != "replicationcontroller/redis-master-controller\n" { t.Errorf("unexpected output: %s", buf.String()) } // Test cascading delete of object without client-side reaper doesn't make GET requests streams, _, buf, _ = genericiooptions.NewTestIOStreams() cmd = NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"secrets", "mysecret"}) if buf.String() != "secret/mysecret\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteObject(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response if buf.String() != "replicationcontroller/redis-master\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestPreviewResultEqualToResult(t *testing.T) { deleteFlags := NewDeleteCommandFlags("") deleteFlags.Interactive = ptr.To(true) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() streams, _, _, _ := genericiooptions.NewTestIOStreams() deleteOptions, err := deleteFlags.ToOptions(nil, streams) deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"} if err != nil { t.Errorf("unexpected error %v", err) } err = deleteOptions.Complete(tf, nil, fakecmd()) if err != nil { t.Errorf("unexpected error %v", err) } infos, err := deleteOptions.Result.Infos() if err != nil { t.Errorf("unexpected error %v", err) } previewInfos, err := deleteOptions.PreviewResult.Infos() if err != nil { t.Errorf("unexpected error %v", err) } if len(infos) != len(previewInfos) { t.Errorf("result and previewResult must match") } } func TestDeleteObjectWithInteractive(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, in, buf, _ := genericiooptions.NewTestIOStreams() fmt.Fprint(in, "y") cmd := NewCmdDelete(tf, streams) err := cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") if err != nil { t.Errorf("unexpected error %v", err) } err = cmd.Flags().Set("output", "name") if err != nil { t.Errorf("unexpected error %v", err) } err = cmd.Flags().Set("interactive", "true") if err != nil { t.Errorf("unexpected error %v", err) } cmd.Run(cmd, []string{}) if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): replicationcontroller/redis-master\n" { t.Errorf("unexpected output: %s", buf.String()) } streams, in, buf, _ = genericiooptions.NewTestIOStreams() fmt.Fprint(in, "n") cmd = NewCmdDelete(tf, streams) err = cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") if err != nil { t.Errorf("unexpected error %v", err) } err = cmd.Flags().Set("output", "name") if err != nil { t.Errorf("unexpected error %v", err) } err = cmd.Flags().Set("interactive", "true") if err != nil { t.Errorf("unexpected error %v", err) } cmd.Run(cmd, []string{}) if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): deletion is cancelled\n" { t.Errorf("unexpected output: %s", buf.String()) } if buf.String() == ": replicationcontroller/redis-master\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestGracePeriodScenarios(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tc := []struct { name string cmdArgs []string forceFlag bool nowFlag bool gracePeriodFlag string expectedGracePeriod string expectedOut string expectedErrOut string expectedDeleteRequestPath string expectedExitCode int }{ { name: "Deleting an object with --force should use grace period = 0", cmdArgs: []string{"pods/foo"}, forceFlag: true, expectedGracePeriod: "0", expectedOut: "pod/foo\n", expectedErrOut: "Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n", expectedDeleteRequestPath: "/namespaces/test/pods/foo", }, { name: "Deleting an object with --force and --grace-period 0 should use grade period = 0", cmdArgs: []string{"pods/foo"}, forceFlag: true, gracePeriodFlag: "0", expectedGracePeriod: "0", expectedOut: "pod/foo\n", expectedErrOut: "Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n", expectedDeleteRequestPath: "/namespaces/test/pods/foo", }, { name: "Deleting an object with --force and --grace-period > 0 should fail", cmdArgs: []string{"pods/foo"}, forceFlag: true, gracePeriodFlag: "10", expectedErrOut: "error: --force and --grace-period greater than 0 cannot be specified together", expectedExitCode: 1, }, { name: "Deleting an object with --grace-period 0 should use a grace period of 1", cmdArgs: []string{"pods/foo"}, gracePeriodFlag: "0", expectedGracePeriod: "1", expectedOut: "pod/foo\n", expectedDeleteRequestPath: "/namespaces/test/pods/foo", }, { name: "Deleting an object with --grace-period > 0 should use the specified grace period", cmdArgs: []string{"pods/foo"}, gracePeriodFlag: "10", expectedGracePeriod: "10", expectedOut: "pod/foo\n", expectedDeleteRequestPath: "/namespaces/test/pods/foo", }, { name: "Deleting an object with the --now flag should use grace period = 1", cmdArgs: []string{"pods/foo"}, nowFlag: true, expectedGracePeriod: "1", expectedOut: "pod/foo\n", expectedDeleteRequestPath: "/namespaces/test/pods/foo", }, { name: "Deleting an object with --now and --grace-period should fail", cmdArgs: []string{"pods/foo"}, nowFlag: true, gracePeriodFlag: "10", expectedErrOut: "error: --now and --grace-period cannot be specified together", expectedExitCode: 1, }, } for _, test := range tc { t.Run(test.name, func(t *testing.T) { // Use a custom fatal behavior with panic/recover so that we can test failure scenarios where // os.Exit() would normally be called cmdutil.BehaviorOnFatal(func(actualErrOut string, actualExitCode int) { if test.expectedExitCode != actualExitCode { t.Errorf("unexpected exit code:\n\tExpected: %d\n\tActual: %d\n", test.expectedExitCode, actualExitCode) } if test.expectedErrOut != actualErrOut { t.Errorf("unexpected error:\n\tExpected: %s\n\tActual: %s\n", test.expectedErrOut, actualErrOut) } panic(nil) }) defer func() { if test.expectedExitCode != 0 { recover() } }() // Setup a fake HTTP Client to capture whether a delete request was made or not and if so, // the actual grace period that was used. actualGracePeriod := "" deleteOccurred := false tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case m == "DELETE" && p == test.expectedDeleteRequestPath: data := make(map[string]interface{}) _ = json.NewDecoder(req.Body).Decode(&data) actualGracePeriod = strconv.FormatFloat(data["gracePeriodSeconds"].(float64), 'f', 0, 64) deleteOccurred = true return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } // Test the command using the flags specified in the test case streams, _, out, errOut := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("output", "name") if test.forceFlag { cmd.Flags().Set("force", "true") } if test.nowFlag { cmd.Flags().Set("now", "true") } if len(test.gracePeriodFlag) > 0 { cmd.Flags().Set("grace-period", test.gracePeriodFlag) } cmd.Run(cmd, test.cmdArgs) // Check actual vs expected conditions if len(test.expectedDeleteRequestPath) > 0 && !deleteOccurred { t.Errorf("expected http delete request to %s but it did not occur", test.expectedDeleteRequestPath) } if test.expectedGracePeriod != actualGracePeriod { t.Errorf("unexpected grace period:\n\tExpected: %s\n\tActual: %s\n", test.expectedGracePeriod, actualGracePeriod) } if out.String() != test.expectedOut { t.Errorf("unexpected output:\n\tExpected: %s\n\tActual: %s\n", test.expectedOut, out.String()) } if errOut.String() != test.expectedErrOut { t.Errorf("unexpected error output:\n\tExpected: %s\n\tActual: %s\n", test.expectedErrOut, errOut.String()) } }) } } func TestDeleteObjectNotFound(t *testing.T) { cmdtesting.InitTestErrorHandler(t) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } options := &DeleteOptions{ FilenameOptions: resource.FilenameOptions{ Filenames: []string{"../../../testdata/redis-master-controller.yaml"}, }, GracePeriod: -1, CascadingStrategy: metav1.DeletePropagationOrphan, Output: "name", IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := options.Complete(tf, []string{}, fakecmd()) if err != nil { t.Errorf("unexpected error: %v", err) } err = options.RunDelete(nil) if err == nil || !errors.IsNotFound(err) { t.Errorf("unexpected error: expected NotFound, got %v", err) } } func TestDeleteObjectIgnoreNotFound(t *testing.T) { cmdtesting.InitTestErrorHandler(t) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("ignore-not-found", "true") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteAllNotFound(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, svc, _ := cmdtesting.TestData() // Add an item to the list which will result in a 404 on delete svc.Items = append(svc.Items, corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) notFoundError := &errors.NewNotFound(corev1.Resource("services"), "foo").ErrStatus tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil case p == "/namespaces/test/services/foo" && m == "DELETE": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, notFoundError)}, nil case p == "/namespaces/test/services/baz" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } // Make sure we can explicitly choose to fail on NotFound errors, even with --all options := &DeleteOptions{ FilenameOptions: resource.FilenameOptions{}, GracePeriod: -1, CascadingStrategy: metav1.DeletePropagationOrphan, DeleteAll: true, IgnoreNotFound: false, Output: "name", IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := options.Complete(tf, []string{"services"}, fakecmd()) if err != nil { t.Errorf("unexpected error: %v", err) } err = options.RunDelete(nil) if err == nil || !errors.IsNotFound(err) { t.Errorf("unexpected error: expected NotFound, got %v", err) } } func TestDeleteAllIgnoreNotFound(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) // Add an item to the list which will result in a 404 on delete svc.Items = append(svc.Items, corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) notFoundError := &errors.NewNotFound(corev1.Resource("services"), "foo").ErrStatus tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil case p == "/namespaces/test/services/foo" && m == "DELETE": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, notFoundError)}, nil case p == "/namespaces/test/services/baz" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("all", "true") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"services"}) if buf.String() != "service/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteMultipleObject(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, svc, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/services/frontend" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../testdata/frontend-service.yaml") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/redis-master\nservice/frontend\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteMultipleObjectContinueOnMissing(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil case p == "/namespaces/test/services/frontend" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() options := &DeleteOptions{ FilenameOptions: resource.FilenameOptions{ Filenames: []string{"../../../testdata/redis-master-controller.yaml", "../../../testdata/frontend-service.yaml"}, }, GracePeriod: -1, CascadingStrategy: metav1.DeletePropagationOrphan, Output: "name", IOStreams: streams, } err := options.Complete(tf, []string{}, fakecmd()) if err != nil { t.Errorf("unexpected error: %v", err) } err = options.RunDelete(nil) if err == nil || !errors.IsNotFound(err) { t.Errorf("unexpected error: expected NotFound, got %v", err) } if buf.String() != "service/frontend\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteMultipleResourcesWithTheSameName(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, svc, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/baz" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/replicationcontrollers/foo" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/services/baz" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil case p == "/namespaces/test/services/foo" && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: // Ensures no GET is performed when deleting by name t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"replicationcontrollers,services", "baz", "foo"}) if buf.String() != "replicationcontroller/baz\nreplicationcontroller/foo\nservice/baz\nservice/foo\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteDirectory(t *testing.T) { cmdtesting.InitTestErrorHandler(t) _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("filename", "../../../testdata/replace/legacy") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/frontend\nreplicationcontroller/redis-master\nreplicationcontroller/redis-slave\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestDeleteMultipleSelector(t *testing.T) { cmdtesting.InitTestErrorHandler(t) pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/pods" && m == "GET": if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil case p == "/namespaces/test/services" && m == "GET": if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil case strings.HasPrefix(p, "/namespaces/test/pods/") && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil case strings.HasPrefix(p, "/namespaces/test/services/") && m == "DELETE": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDelete(tf, streams) cmd.Flags().Set("selector", "a=b") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"pods,services"}) if buf.String() != "pod/foo\npod/bar\nservice/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestResourceErrors(t *testing.T) { cmdtesting.InitTestErrorHandler(t) testCases := map[string]struct { args []string errFn func(error) bool }{ "no args": { args: []string{}, errFn: func(err error) bool { return strings.Contains(err.Error(), "You must provide one or more resources") }, }, "resources but no selectors": { args: []string{"pods"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "resource(s) were provided, but no name was specified") }, }, "multiple resources but no selectors": { args: []string{"pods,deployments"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "resource(s) were provided, but no name was specified") }, }, } for k, testCase := range testCases { t.Run(k, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() options := &DeleteOptions{ FilenameOptions: resource.FilenameOptions{}, GracePeriod: -1, CascadingStrategy: metav1.DeletePropagationOrphan, Output: "name", IOStreams: streams, } err := options.Complete(tf, testCase.args, fakecmd()) if !testCase.errFn(err) { t.Errorf("%s: unexpected error: %v", k, err) return } if buf.Len() > 0 { t.Errorf("buffer should be empty: %s", buf.String()) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/describe/000077500000000000000000000000001504711711200263675ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/describe/describe.go000066400000000000000000000210451504711711200305000ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package describe import ( "fmt" "strings" "github.com/spf13/cobra" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/describe" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( describeLong = templates.LongDesc(i18n.T(` Show details of a specific resource or group of resources. Print a detailed description of the selected resources, including related resources such as events or controllers. You may select a single object by name, all objects of that type, provide a name prefix, or label selector. For example: $ kubectl describe TYPE NAME_PREFIX will first check for an exact match on TYPE and NAME_PREFIX. If no such resource exists, it will output details for every resource that has a name prefixed with NAME_PREFIX.`)) describeExample = templates.Examples(i18n.T(` # Describe a node kubectl describe nodes kubernetes-node-emt8.c.myproject.internal # Describe a pod kubectl describe pods/nginx # Describe a pod identified by type and name in "pod.json" kubectl describe -f pod.json # Describe all pods kubectl describe pods # Describe pods by label name=myLabel kubectl describe pods -l name=myLabel # Describe all pods managed by the 'frontend' replication controller # (rc-created pods get the name of the rc as a prefix in the pod name) kubectl describe pods frontend`)) ) // DescribeFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, // which reflect the runtime requirements for the command. type DescribeFlags struct { Factory cmdutil.Factory Selector string AllNamespaces bool FilenameOptions *resource.FilenameOptions DescriberSettings *describe.DescriberSettings genericiooptions.IOStreams } // NewDescribeFlags returns a default DescribeFlags func NewDescribeFlags(f cmdutil.Factory, streams genericiooptions.IOStreams) *DescribeFlags { return &DescribeFlags{ Factory: f, FilenameOptions: &resource.FilenameOptions{}, DescriberSettings: &describe.DescriberSettings{ ShowEvents: true, ChunkSize: cmdutil.DefaultChunkSize, }, IOStreams: streams, } } // AddFlags registers flags for a cli func (flags *DescribeFlags) AddFlags(cmd *cobra.Command) { cmdutil.AddFilenameOptionFlags(cmd, flags.FilenameOptions, "containing the resource to describe") cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector) cmd.Flags().BoolVarP(&flags.AllNamespaces, "all-namespaces", "A", flags.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().BoolVar(&flags.DescriberSettings.ShowEvents, "show-events", flags.DescriberSettings.ShowEvents, "If true, display events related to the described object.") cmdutil.AddChunkSizeFlag(cmd, &flags.DescriberSettings.ChunkSize) } // ToOptions converts from CLI inputs to runtime input func (flags *DescribeFlags) ToOptions(parent string, args []string) (*DescribeOptions, error) { var err error namespace, enforceNamespace, err := flags.Factory.ToRawKubeConfigLoader().Namespace() if err != nil { return nil, err } if flags.AllNamespaces { enforceNamespace = false } if len(args) == 0 && cmdutil.IsFilenameSliceEmpty(flags.FilenameOptions.Filenames, flags.FilenameOptions.Kustomize) { return nil, fmt.Errorf("You must specify the type of resource to describe. %s\n", cmdutil.SuggestAPIResources(parent)) } builderArgs := args describer := func(mapping *meta.RESTMapping) (describe.ResourceDescriber, error) { return describe.DescriberFn(flags.Factory, mapping) } o := &DescribeOptions{ Selector: flags.Selector, Namespace: namespace, Describer: describer, NewBuilder: flags.Factory.NewBuilder, BuilderArgs: builderArgs, EnforceNamespace: enforceNamespace, AllNamespaces: flags.AllNamespaces, FilenameOptions: flags.FilenameOptions, DescriberSettings: flags.DescriberSettings, IOStreams: flags.IOStreams, } return o, nil } func NewCmdDescribe(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { flags := NewDescribeFlags(f, streams) cmd := &cobra.Command{ Use: "describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME)", DisableFlagsInUseLine: true, Short: i18n.T("Show details of a specific resource or group of resources"), Long: describeLong + "\n\n" + cmdutil.SuggestAPIResources(parent), Example: describeExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions(parent, args) cmdutil.CheckErr(err) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } flags.AddFlags(cmd) return cmd } func (o *DescribeOptions) Validate() error { return nil } func (o *DescribeOptions) Run() error { r := o.NewBuilder(). Unstructured(). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces). FilenameParam(o.EnforceNamespace, o.FilenameOptions). LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(true, o.BuilderArgs...). RequestChunksOf(o.DescriberSettings.ChunkSize). Flatten(). Do() err := r.Err() if err != nil { return err } allErrs := []error{} infos, err := r.Infos() if err != nil { if apierrors.IsNotFound(err) && len(o.BuilderArgs) == 2 { return o.DescribeMatchingResources(err, o.BuilderArgs[0], o.BuilderArgs[1]) } allErrs = append(allErrs, err) } errs := sets.New[string]() first := true for _, info := range infos { mapping := info.ResourceMapping() describer, err := o.Describer(mapping) if err != nil { if errs.Has(err.Error()) { continue } allErrs = append(allErrs, err) errs.Insert(err.Error()) continue } s, err := describer.Describe(info.Namespace, info.Name, *o.DescriberSettings) if err != nil { if errs.Has(err.Error()) { continue } allErrs = append(allErrs, err) errs.Insert(err.Error()) continue } if first { first = false fmt.Fprint(o.Out, s) } else { fmt.Fprintf(o.Out, "\n\n%s", s) } } if len(infos) == 0 && len(allErrs) == 0 { // if we wrote no output, and had no errors, be sure we output something. if o.AllNamespaces { fmt.Fprintln(o.ErrOut, "No resources found") } else { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) } } return utilerrors.NewAggregate(allErrs) } func (o *DescribeOptions) DescribeMatchingResources(originalError error, resource, prefix string) error { r := o.NewBuilder(). Unstructured(). NamespaceParam(o.Namespace).DefaultNamespace(). ResourceTypeOrNameArgs(true, resource). SingleResourceType(). RequestChunksOf(o.DescriberSettings.ChunkSize). Flatten(). Do() mapping, err := r.ResourceMapping() if err != nil { return err } describer, err := o.Describer(mapping) if err != nil { return err } infos, err := r.Infos() if err != nil { return err } isFound := false for ix := range infos { info := infos[ix] if strings.HasPrefix(info.Name, prefix) { isFound = true s, err := describer.Describe(info.Namespace, info.Name, *o.DescriberSettings) if err != nil { return err } fmt.Fprintf(o.Out, "%s\n", s) } } if !isFound { return originalError } return nil } type DescribeOptions struct { CmdParent string Selector string Namespace string Describer func(*meta.RESTMapping) (describe.ResourceDescriber, error) NewBuilder func() *resource.Builder BuilderArgs []string EnforceNamespace bool AllNamespaces bool DescriberSettings *describe.DescriberSettings FilenameOptions *resource.FilenameOptions genericiooptions.IOStreams } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/describe/describe_test.go000066400000000000000000000264161504711711200315460ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package describe import ( "fmt" "net/http" "strings" "testing" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/describe" "k8s.io/kubectl/pkg/scheme" ) // Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. func TestDescribeUnknownSchemaObject(t *testing.T) { d := &testDescriber{Output: "test output"} oldFn := describe.DescriberFn defer func() { describe.DescriberFn = oldFn }() describe.DescriberFn = d.describerFor tf := cmdtesting.NewTestFactory().WithNamespace("non-default") defer tf.Cleanup() _, _, codec := cmdtesting.NewExternalScheme() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, cmdtesting.NewInternalType("", "", "foo"))}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDescribe("kubectl", tf, streams) cmd.Run(cmd, []string{"type", "foo"}) if d.Name != "foo" || d.Namespace != "" { t.Errorf("unexpected describer: %#v", d) } if buf.String() != d.Output { t.Errorf("unexpected output: %s", buf.String()) } } // Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. func TestDescribeUnknownNamespacedSchemaObject(t *testing.T) { d := &testDescriber{Output: "test output"} oldFn := describe.DescriberFn defer func() { describe.DescriberFn = oldFn }() describe.DescriberFn = d.describerFor tf := cmdtesting.NewTestFactory() defer tf.Cleanup() _, _, codec := cmdtesting.NewExternalScheme() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, cmdtesting.NewInternalNamespacedType("", "", "foo", "non-default"))}, } tf.WithNamespace("non-default") streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDescribe("kubectl", tf, streams) cmd.Run(cmd, []string{"namespacedtype", "foo"}) if d.Name != "foo" || d.Namespace != "non-default" { t.Errorf("unexpected describer: %#v", d) } if buf.String() != d.Output { t.Errorf("unexpected output: %s", buf.String()) } } func TestDescribeObject(t *testing.T) { d := &testDescriber{Output: "test output"} oldFn := describe.DescriberFn defer func() { describe.DescriberFn = oldFn }() describe.DescriberFn = d.describerFor _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDescribe("kubectl", tf, streams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Run(cmd, []string{}) if d.Name != "redis-master" || d.Namespace != "test" { t.Errorf("unexpected describer: %#v", d) } if buf.String() != d.Output { t.Errorf("unexpected output: %s", buf.String()) } } func TestDescribeListObjects(t *testing.T) { d := &testDescriber{Output: "test output"} oldFn := describe.DescriberFn defer func() { describe.DescriberFn = oldFn }() describe.DescriberFn = d.describerFor pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDescribe("kubectl", tf, streams) cmd.Run(cmd, []string{"pods"}) if buf.String() != fmt.Sprintf("%s\n\n%s", d.Output, d.Output) { t.Errorf("unexpected output: %s", buf.String()) } } func TestDescribeObjectShowEvents(t *testing.T) { d := &testDescriber{Output: "test output"} oldFn := describe.DescriberFn defer func() { describe.DescriberFn = oldFn }() describe.DescriberFn = d.describerFor pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, } cmd := NewCmdDescribe("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard()) cmd.Flags().Set("show-events", "true") cmd.Run(cmd, []string{"pods"}) if d.Settings.ShowEvents != true { t.Errorf("ShowEvents = true expected, got ShowEvents = %v", d.Settings.ShowEvents) } } func TestDescribeObjectSkipEvents(t *testing.T) { d := &testDescriber{Output: "test output"} oldFn := describe.DescriberFn defer func() { describe.DescriberFn = oldFn }() describe.DescriberFn = d.describerFor pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, } cmd := NewCmdDescribe("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard()) cmd.Flags().Set("show-events", "false") cmd.Run(cmd, []string{"pods"}) if d.Settings.ShowEvents != false { t.Errorf("ShowEvents = false expected, got ShowEvents = %v", d.Settings.ShowEvents) } } func TestDescribeObjectChunkSize(t *testing.T) { d := &testDescriber{Output: "test output"} oldFn := describe.DescriberFn defer func() { describe.DescriberFn = oldFn }() describe.DescriberFn = d.describerFor pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, } cmd := NewCmdDescribe("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard()) cmd.Flags().Set("chunk-size", "100") cmd.Run(cmd, []string{"pods"}) if d.Settings.ChunkSize != 100 { t.Errorf("ChunkSize = 100 expected, got ChunkSize = %v", d.Settings.ChunkSize) } } func TestDescribeHelpMessage(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdDescribe("kubectl", tf, streams) cmd.SetArgs([]string{"-h"}) cmd.SetOut(buf) cmd.SetErr(buf) _, err := cmd.ExecuteC() if err != nil { t.Fatalf("Unexpected error: %v", err) } got := buf.String() expected := `describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME)` if !strings.Contains(got, expected) { t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got) } unexpected := `describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME) [flags]` if strings.Contains(got, unexpected) { t.Errorf("Expected not to contain: \n %v\nGot:\n %v\n", unexpected, got) } } func TestDescribeNoResourcesFound(t *testing.T) { testNS := "testns" testCases := []struct { name string flags map[string]string namespace string expectedOutput string expectedErr string }{ { name: "all namespaces", flags: map[string]string{"all-namespaces": "true"}, expectedOutput: "", expectedErr: "No resources found\n", }, { name: "all in namespace", namespace: testNS, expectedOutput: "", expectedErr: "No resources found in " + testNS + " namespace.\n", }, } cmdtesting.InitTestErrorHandler(t) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { pods, _, _ := cmdtesting.EmptyTestData() tf := cmdtesting.NewTestFactory().WithNamespace(testNS) defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, } streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdDescribe("kubectl", tf, streams) for name, value := range testCase.flags { _ = cmd.Flags().Set(name, value) } cmd.Run(cmd, []string{"pods"}) if e, a := testCase.expectedOutput, buf.String(); e != a { t.Errorf("Unexpected output:\nExpected:\n%v\nActual:\n%v", e, a) } if e, a := testCase.expectedErr, errbuf.String(); e != a { t.Errorf("Unexpected error:\nExpected:\n%v\nActual:\n%v", e, a) } }) } } type testDescriber struct { Name, Namespace string Settings describe.DescriberSettings Output string Err error } func (t *testDescriber) Describe(namespace, name string, describerSettings describe.DescriberSettings) (output string, err error) { t.Namespace, t.Name = namespace, name t.Settings = describerSettings return t.Output, t.Err } func (t *testDescriber) describerFor(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (describe.ResourceDescriber, error) { return t, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/diff/000077500000000000000000000000001504711711200255175ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff.go000066400000000000000000000520741504711711200267660ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package diff import ( "fmt" "io" "os" "path/filepath" "regexp" "strings" "github.com/jonboulle/clockwork" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" 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/types" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/client-go/openapi3" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/cmd/apply" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/util/prune" "k8s.io/kubectl/pkg/util/templates" "k8s.io/utils/exec" "sigs.k8s.io/yaml" ) var ( diffLong = templates.LongDesc(i18n.T(` Diff configurations specified by file name or stdin between the current online configuration, and the configuration as it would be if applied. The output is always YAML. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own diff command. Users can use external commands with params too, example: KUBECTL_EXTERNAL_DIFF="colordiff -N -u" By default, the "diff" command available in your path will be run with the "-u" (unified diff) and "-N" (treat absent files as empty) options. Exit status: 0 No differences were found. 1 Differences were found. >1 Kubectl or diff failed with an error. Note: KUBECTL_EXTERNAL_DIFF, if used, is expected to follow that convention.`)) diffExample = templates.Examples(i18n.T(` # Diff resources included in pod.json kubectl diff -f pod.json # Diff file read from stdin cat service.yaml | kubectl diff -f -`)) ) // Number of times we try to diff before giving-up const maxRetries = 4 // Constants for masking sensitive values const ( sensitiveMaskDefault = "***" sensitiveMaskBefore = "*** (before)" sensitiveMaskAfter = "*** (after)" ) // diffError returns the ExitError if the status code is less than 1, // nil otherwise. func diffError(err error) exec.ExitError { if err, ok := err.(exec.ExitError); ok && err.ExitStatus() <= 1 { return err } return nil } type DiffOptions struct { FilenameOptions resource.FilenameOptions ServerSideApply bool FieldManager string ForceConflicts bool ShowManagedFields bool Concurrency int Selector string OpenAPIGetter openapi.OpenAPIResourcesGetter OpenAPIV3Root openapi3.Root DynamicClient dynamic.Interface CmdNamespace string EnforceNamespace bool Builder *resource.Builder Diff *DiffProgram pruner *pruner tracker *tracker } func NewDiffOptions(ioStreams genericiooptions.IOStreams) *DiffOptions { return &DiffOptions{ Diff: &DiffProgram{ Exec: exec.New(), IOStreams: ioStreams, }, } } func NewCmdDiff(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { options := NewDiffOptions(streams) cmd := &cobra.Command{ Use: "diff -f FILENAME", DisableFlagsInUseLine: true, Short: i18n.T("Diff the live version against a would-be applied version"), Long: diffLong, Example: diffExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckDiffErr(options.Complete(f, cmd, args)) cmdutil.CheckDiffErr(options.Validate()) // `kubectl diff` propagates the error code from // diff or `KUBECTL_EXTERNAL_DIFF`. Also, we // don't want to print an error if diff returns // error code 1, which simply means that changes // were found. We also don't want kubectl to // return 1 if there was a problem. if err := options.Run(); err != nil { if exitErr := diffError(err); exitErr != nil { cmdutil.CheckErr(cmdutil.ErrExit) } cmdutil.CheckDiffErr(err) } }, } // Flag errors exit with code 1, however according to the diff // command it means changes were found. // Thus, it should return status code greater than 1. cmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { cmdutil.CheckDiffErr(cmdutil.UsageErrorf(cmd, "%s", err.Error())) return nil }) usage := "contains the configuration to diff" cmd.Flags().StringArray("prune-allowlist", []string{}, "Overwrite the default allowlist with for --prune") cmd.Flags().Bool("prune", false, "Include resources that would be deleted by pruning. Can be used with -l and default shows all resources would be pruned") cmd.Flags().BoolVar(&options.ShowManagedFields, "show-managed-fields", options.ShowManagedFields, "If true, include managed fields in the diff.") cmd.Flags().IntVar(&options.Concurrency, "concurrency", 1, "Number of objects to process in parallel when diffing against the live version. Larger number = faster, but more memory, I/O and CPU over that shorter period of time.") cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) cmdutil.AddServerSideApplyFlags(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &options.FieldManager, apply.FieldManagerClientSideApply) cmdutil.AddLabelSelectorFlagVar(cmd, &options.Selector) return cmd } // DiffProgram finds and run the diff program. The value of // KUBECTL_EXTERNAL_DIFF environment variable will be used a diff // program. By default, `diff(1)` will be used. type DiffProgram struct { Exec exec.Interface genericiooptions.IOStreams } func (d *DiffProgram) getCommand(args ...string) (string, exec.Cmd) { diff := "" if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" { diffCommand := strings.Split(envDiff, " ") diff = diffCommand[0] if len(diffCommand) > 1 { // Regex accepts: Alphanumeric (case-insensitive), dash and equal isValidChar := regexp.MustCompile(`^[a-zA-Z0-9-=]+$`).MatchString for i := 1; i < len(diffCommand); i++ { if isValidChar(diffCommand[i]) { args = append(args, diffCommand[i]) } } } } else { diff = "diff" args = append([]string{"-u", "-N"}, args...) } cmd := d.Exec.Command(diff, args...) cmd.SetStdout(d.Out) cmd.SetStderr(d.ErrOut) return diff, cmd } // Run runs the detected diff program. `from` and `to` are the directory to diff. func (d *DiffProgram) Run(from, to string) error { diff, cmd := d.getCommand(from, to) if err := cmd.Run(); err != nil { // Let's not wrap diff errors, or we won't be able to // differentiate them later. if diffErr := diffError(err); diffErr != nil { return diffErr } return fmt.Errorf("failed to run %q: %v", diff, err) } return nil } // Printer is used to print an object. type Printer struct{} // Print the object inside the writer w. func (p *Printer) Print(obj runtime.Object, w io.Writer) error { if obj == nil { return nil } data, err := yaml.Marshal(obj) if err != nil { return err } _, err = w.Write(data) return err } // DiffVersion gets the proper version of objects, and aggregate them into a directory. type DiffVersion struct { Dir *Directory Name string } // NewDiffVersion creates a new DiffVersion with the named version. func NewDiffVersion(name string) (*DiffVersion, error) { dir, err := CreateDirectory(name) if err != nil { return nil, err } return &DiffVersion{ Dir: dir, Name: name, }, nil } func (v *DiffVersion) getObject(obj Object) (runtime.Object, error) { switch v.Name { case "LIVE": return obj.Live(), nil case "MERGED": return obj.Merged() } return nil, fmt.Errorf("Unknown version: %v", v.Name) } // Print prints the object using the printer into a new file in the directory. func (v *DiffVersion) Print(name string, obj runtime.Object, printer Printer) error { f, err := v.Dir.NewFile(name) if err != nil { return err } defer f.Close() return printer.Print(obj, f) } // Directory creates a new temp directory, and allows to easily create new files. type Directory struct { Name string } // CreateDirectory does create the actual disk directory, and return a // new representation of it. func CreateDirectory(prefix string) (*Directory, error) { name, err := os.MkdirTemp("", prefix+"-") if err != nil { return nil, err } return &Directory{ Name: name, }, nil } // NewFile creates a new file in the directory. func (d *Directory) NewFile(name string) (*os.File, error) { return os.OpenFile(filepath.Join(d.Name, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) } // Delete removes the directory recursively. func (d *Directory) Delete() error { return os.RemoveAll(d.Name) } // Object is an interface that let's you retrieve multiple version of // it. type Object interface { Live() runtime.Object Merged() (runtime.Object, error) Name() string } // InfoObject is an implementation of the Object interface. It gets all // the information from the Info object. type InfoObject struct { LocalObj runtime.Object Info *resource.Info Encoder runtime.Encoder OpenAPIGetter openapi.OpenAPIResourcesGetter OpenAPIV3Root openapi3.Root Force bool ServerSideApply bool FieldManager string ForceConflicts bool genericiooptions.IOStreams } var _ Object = &InfoObject{} // Returns the live version of the object func (obj InfoObject) Live() runtime.Object { return obj.Info.Object } // Returns the "merged" object, as it would look like if applied or // created. func (obj InfoObject) Merged() (runtime.Object, error) { helper := resource.NewHelper(obj.Info.Client, obj.Info.Mapping). DryRun(true). WithFieldManager(obj.FieldManager) if obj.ServerSideApply { data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj.LocalObj) if err != nil { return nil, err } options := metav1.PatchOptions{ Force: &obj.ForceConflicts, FieldManager: obj.FieldManager, } return helper.Patch( obj.Info.Namespace, obj.Info.Name, types.ApplyPatchType, data, &options, ) } // Build the patcher, and then apply the patch with dry-run, unless the object doesn't exist, in which case we need to create it. if obj.Live() == nil { // Dry-run create if the object doesn't exist. return helper.CreateWithOptions( obj.Info.Namespace, true, obj.LocalObj, &metav1.CreateOptions{}, ) } var resourceVersion *string if !obj.Force { accessor, err := meta.Accessor(obj.Info.Object) if err != nil { return nil, err } str := accessor.GetResourceVersion() resourceVersion = &str } modified, err := util.GetModifiedConfiguration(obj.LocalObj, false, unstructured.UnstructuredJSONScheme) if err != nil { return nil, err } // This is using the patcher from apply, to keep the same behavior. // We plan on replacing this with server-side apply when it becomes available. patcher := &apply.Patcher{ Mapping: obj.Info.Mapping, Helper: helper, Overwrite: true, BackOff: clockwork.NewRealClock(), OpenAPIGetter: obj.OpenAPIGetter, OpenAPIV3Root: obj.OpenAPIV3Root, ResourceVersion: resourceVersion, } _, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, obj.ErrOut) return result, err } func (obj InfoObject) Name() string { group := "" if obj.Info.Mapping.GroupVersionKind.Group != "" { group = fmt.Sprintf("%v.", obj.Info.Mapping.GroupVersionKind.Group) } return group + fmt.Sprintf( "%v.%v.%v.%v", obj.Info.Mapping.GroupVersionKind.Version, obj.Info.Mapping.GroupVersionKind.Kind, obj.Info.Namespace, obj.Info.Name, ) } // toUnstructured converts a runtime.Object into an unstructured.Unstructured object. func toUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) { if obj == nil { return nil, nil } c, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj.DeepCopyObject()) if err != nil { return nil, fmt.Errorf("convert to unstructured: %w", err) } u := &unstructured.Unstructured{} u.SetUnstructuredContent(c) return u, nil } // Masker masks sensitive values in an object while preserving diff-able // changes. // // All sensitive values in the object will be masked with a fixed-length // asterisk mask. If two values are different, an additional suffix will // be added so they can be diff-ed. type Masker struct { from *unstructured.Unstructured to *unstructured.Unstructured } func NewMasker(from, to runtime.Object) (*Masker, error) { // Convert objects to unstructured f, err := toUnstructured(from) if err != nil { return nil, fmt.Errorf("convert to unstructured: %w", err) } t, err := toUnstructured(to) if err != nil { return nil, fmt.Errorf("convert to unstructured: %w", err) } // Run masker m := &Masker{ from: f, to: t, } if err := m.run(); err != nil { return nil, fmt.Errorf("run masker: %w", err) } return m, nil } // dataFromUnstructured returns the underlying nested map in the data key. func (m Masker) dataFromUnstructured(u *unstructured.Unstructured) (map[string]interface{}, error) { if u == nil { return nil, nil } data, found, err := unstructured.NestedMap(u.UnstructuredContent(), "data") if err != nil { return nil, fmt.Errorf("get nested map: %w", err) } if !found { return nil, nil } return data, nil } // run compares and patches sensitive values. func (m *Masker) run() error { // Extract nested map object from, err := m.dataFromUnstructured(m.from) if err != nil { return fmt.Errorf("extract 'data' field: %w", err) } to, err := m.dataFromUnstructured(m.to) if err != nil { return fmt.Errorf("extract 'data' field: %w", err) } for k := range from { // Add before/after suffix when key exists on both // objects and are not equal, so that it will be // visible in diffs. if _, ok := to[k]; ok { if from[k] != to[k] { from[k] = sensitiveMaskBefore to[k] = sensitiveMaskAfter continue } to[k] = sensitiveMaskDefault } from[k] = sensitiveMaskDefault } for k := range to { // Mask remaining keys that were not in 'from' if _, ok := from[k]; !ok { to[k] = sensitiveMaskDefault } } // Patch objects with masked data if m.from != nil && from != nil { if err := unstructured.SetNestedMap(m.from.UnstructuredContent(), from, "data"); err != nil { return fmt.Errorf("patch masked data: %w", err) } } if m.to != nil && to != nil { if err := unstructured.SetNestedMap(m.to.UnstructuredContent(), to, "data"); err != nil { return fmt.Errorf("patch masked data: %w", err) } } return nil } // From returns the masked version of the 'from' object. func (m *Masker) From() runtime.Object { return m.from } // To returns the masked version of the 'to' object. func (m *Masker) To() runtime.Object { return m.to } // Differ creates two DiffVersion and diffs them. type Differ struct { From *DiffVersion To *DiffVersion } func NewDiffer(from, to string) (*Differ, error) { differ := Differ{} var err error differ.From, err = NewDiffVersion(from) if err != nil { return nil, err } differ.To, err = NewDiffVersion(to) if err != nil { differ.From.Dir.Delete() return nil, err } return &differ, nil } // Diff diffs to versions of a specific object, and print both versions to directories. func (d *Differ) Diff(obj Object, printer Printer, showManagedFields bool) error { from, err := d.From.getObject(obj) if err != nil { return err } to, err := d.To.getObject(obj) if err != nil { return err } if !showManagedFields { from = omitManagedFields(from) to = omitManagedFields(to) } // Mask secret values if object is V1Secret if gvk := to.GetObjectKind().GroupVersionKind(); gvk.Version == "v1" && gvk.Kind == "Secret" { m, err := NewMasker(from, to) if err != nil { return err } from, to = m.From(), m.To() } if err := d.From.Print(obj.Name(), from, printer); err != nil { return err } if err := d.To.Print(obj.Name(), to, printer); err != nil { return err } return nil } func omitManagedFields(o runtime.Object) runtime.Object { a, err := meta.Accessor(o) if err != nil { // The object is not a `metav1.Object`, ignore it. return o } a.SetManagedFields(nil) return o } // Run runs the diff program against both directories. func (d *Differ) Run(diff *DiffProgram) error { return diff.Run(d.From.Dir.Name, d.To.Dir.Name) } // TearDown removes both temporary directories recursively. func (d *Differ) TearDown() { d.From.Dir.Delete() // Ignore error d.To.Dir.Delete() // Ignore error } func isConflict(err error) bool { return err != nil && errors.IsConflict(err) } func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) } var err error err = o.FilenameOptions.RequireFilenameOrKustomize() if err != nil { return err } o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd) o.FieldManager = apply.GetApplyFieldManagerFlag(cmd, o.ServerSideApply) o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd) if o.ForceConflicts && !o.ServerSideApply { return fmt.Errorf("--force-conflicts only works with --server-side") } if !o.ServerSideApply { o.OpenAPIGetter = f if !cmdutil.OpenAPIV3Patch.IsDisabled() { openAPIV3Client, err := f.OpenAPIV3Client() if err == nil { o.OpenAPIV3Root = openapi3.NewRoot(openAPIV3Client) } else { klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2") } } } o.DynamicClient, err = f.DynamicClient() if err != nil { return err } o.CmdNamespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } if cmdutil.GetFlagBool(cmd, "prune") { mapper, err := f.ToRESTMapper() if err != nil { return err } resources, err := prune.ParseResources(mapper, cmdutil.GetFlagStringArray(cmd, "prune-allowlist")) if err != nil { return err } o.tracker = newTracker() o.pruner = newPruner(o.DynamicClient, mapper, resources, o.Selector) } o.Builder = f.NewBuilder() return nil } // Run uses the factory to parse file arguments, find the version to // diff, and find each Info object for each files, and runs against the // differ. func (o *DiffOptions) Run() error { differ, err := NewDiffer("LIVE", "MERGED") if err != nil { return err } defer differ.TearDown() printer := Printer{} r := o.Builder. Unstructured(). VisitorConcurrency(o.Concurrency). NamespaceParam(o.CmdNamespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.FilenameOptions). LabelSelectorParam(o.Selector). Flatten(). Do() if err := r.Err(); err != nil { return err } err = r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } local := info.Object.DeepCopyObject() for i := 1; i <= maxRetries; i++ { if err = info.Get(); err != nil { if !errors.IsNotFound(err) { return err } info.Object = nil } force := i == maxRetries if force { klog.Warningf( "Object (%v: %v) keeps changing, diffing without lock", info.Object.GetObjectKind().GroupVersionKind(), info.Name, ) } obj := InfoObject{ LocalObj: local, Info: info, Encoder: scheme.DefaultJSONEncoder(), OpenAPIGetter: o.OpenAPIGetter, OpenAPIV3Root: o.OpenAPIV3Root, Force: force, ServerSideApply: o.ServerSideApply, FieldManager: o.FieldManager, ForceConflicts: o.ForceConflicts, IOStreams: o.Diff.IOStreams, } if o.tracker != nil { o.tracker.MarkVisited(info) } err = differ.Diff(obj, printer, o.ShowManagedFields) if !isConflict(err) { break } } apply.WarnIfDeleting(info.Object, o.Diff.ErrOut) return err }) if o.pruner != nil { prunedObjs, err := o.pruner.pruneAll(o.tracker, o.CmdNamespace != "") if err != nil { klog.Warningf("pruning failed and could not be evaluated err: %v", err) } // Print pruned objects into old file and thus, diff // command will show them as pruned. for _, p := range prunedObjs { name, err := getObjectName(p) if err != nil { klog.Warningf("pruning failed and object name could not be retrieved: %v", err) continue } if err := differ.From.Print(name, p, printer); err != nil { return err } } } if err != nil { return err } return differ.Run(o.Diff) } // Validate makes sure provided values for DiffOptions are valid func (o *DiffOptions) Validate() error { return nil } func getObjectName(obj runtime.Object) (string, error) { gvk := obj.GetObjectKind().GroupVersionKind() metadata, err := meta.Accessor(obj) if err != nil { return "", err } name := metadata.GetName() ns := metadata.GetNamespace() group := "" if gvk.Group != "" { group = fmt.Sprintf("%v.", gvk.Group) } return group + fmt.Sprintf( "%v.%v.%v.%v", gvk.Version, gvk.Kind, ns, name, ), nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff_test.go000066400000000000000000000340501504711711200300170ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package diff import ( "bytes" "fmt" "os" "path/filepath" "strings" "testing" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/utils/exec" ) type FakeObject struct { name string merged map[string]interface{} live map[string]interface{} } var _ Object = &FakeObject{} func (f *FakeObject) Name() string { return f.name } func (f *FakeObject) Merged() (runtime.Object, error) { // Return nil if merged object does not exist if f.merged == nil { return nil, nil } return &unstructured.Unstructured{Object: f.merged}, nil } func (f *FakeObject) Live() runtime.Object { // Return nil if live object does not exist if f.live == nil { return nil } return &unstructured.Unstructured{Object: f.live} } func TestDiffProgram(t *testing.T) { externalDiffCommands := [3]string{"diff", "diff -ruN", "diff --report-identical-files"} t.Setenv("LANG", "C") for i, c := range externalDiffCommands { t.Setenv("KUBECTL_EXTERNAL_DIFF", c) streams, _, stdout, _ := genericiooptions.NewTestIOStreams() diff := DiffProgram{ IOStreams: streams, Exec: exec.New(), } err := diff.Run("/dev/zero", "/dev/zero") if err != nil { t.Fatal(err) } // Testing diff --report-identical-files if i == 2 { output_msg := "Files /dev/zero and /dev/zero are identical\n" if output := stdout.String(); output != output_msg { t.Fatalf(`stdout = %q, expected = %s"`, output, output_msg) } } } } func TestPrinter(t *testing.T) { printer := Printer{} obj := &unstructured.Unstructured{Object: map[string]interface{}{ "string": "string", "list": []int{1, 2, 3}, "int": 12, }} buf := bytes.Buffer{} printer.Print(obj, &buf) want := `int: 12 list: - 1 - 2 - 3 string: string ` if buf.String() != want { t.Errorf("Print() = %q, want %q", buf.String(), want) } } func TestDiffVersion(t *testing.T) { diff, err := NewDiffVersion("MERGED") if err != nil { t.Fatal(err) } defer diff.Dir.Delete() obj := FakeObject{ name: "bla", live: map[string]interface{}{"live": true}, merged: map[string]interface{}{"merged": true}, } rObj, err := obj.Merged() if err != nil { t.Fatal(err) } err = diff.Print(obj.Name(), rObj, Printer{}) if err != nil { t.Fatal(err) } fcontent, err := os.ReadFile(filepath.Join(diff.Dir.Name, obj.Name())) if err != nil { t.Fatal(err) } econtent := "merged: true\n" if string(fcontent) != econtent { t.Fatalf("File has %q, expected %q", string(fcontent), econtent) } } func TestDirectory(t *testing.T) { dir, err := CreateDirectory("prefix") defer dir.Delete() if err != nil { t.Fatal(err) } _, err = os.Stat(dir.Name) if err != nil { t.Fatal(err) } if !strings.HasPrefix(filepath.Base(dir.Name), "prefix") { t.Fatalf(`Directory doesn't start with "prefix": %q`, dir.Name) } entries, err := os.ReadDir(dir.Name) if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("Directory should be empty, has %d elements", len(entries)) } _, err = dir.NewFile("ONE") if err != nil { t.Fatal(err) } _, err = dir.NewFile("TWO") if err != nil { t.Fatal(err) } entries, err = os.ReadDir(dir.Name) if err != nil { t.Fatal(err) } if len(entries) != 2 { t.Fatalf("ReadDir should have two elements, has %d elements", len(entries)) } err = dir.Delete() if err != nil { t.Fatal(err) } _, err = os.Stat(dir.Name) if err == nil { t.Fatal("Directory should be gone, still present.") } } func TestDiffer(t *testing.T) { diff, err := NewDiffer("LIVE", "MERGED") if err != nil { t.Fatal(err) } defer diff.TearDown() obj := FakeObject{ name: "bla", live: map[string]interface{}{"live": true}, merged: map[string]interface{}{"merged": true}, } err = diff.Diff(&obj, Printer{}, true) if err != nil { t.Fatal(err) } fcontent, err := os.ReadFile(filepath.Join(diff.From.Dir.Name, obj.Name())) if err != nil { t.Fatal(err) } econtent := "live: true\n" if string(fcontent) != econtent { t.Fatalf("File has %q, expected %q", string(fcontent), econtent) } fcontent, err = os.ReadFile(filepath.Join(diff.To.Dir.Name, obj.Name())) if err != nil { t.Fatal(err) } econtent = "merged: true\n" if string(fcontent) != econtent { t.Fatalf("File has %q, expected %q", string(fcontent), econtent) } } func TestShowManagedFields(t *testing.T) { diff, err := NewDiffer("LIVE", "MERGED") if err != nil { t.Fatal(err) } defer diff.TearDown() testCases := []struct { name string showManagedFields bool expectedFromContent string expectedToContent string }{ { name: "without managed fields", showManagedFields: false, expectedFromContent: `live: true metadata: name: foo `, expectedToContent: `merged: true metadata: name: foo `, }, { name: "with managed fields", showManagedFields: true, expectedFromContent: `live: true metadata: managedFields: mf-data name: foo `, expectedToContent: `merged: true metadata: managedFields: mf-data name: foo `, }, } for i, tc := range testCases { t.Run(tc.name, func(t *testing.T) { obj := FakeObject{ name: fmt.Sprintf("TestCase%d", i), live: map[string]interface{}{ "live": true, "metadata": map[string]interface{}{ "managedFields": "mf-data", "name": "foo", }, }, merged: map[string]interface{}{ "merged": true, "metadata": map[string]interface{}{ "managedFields": "mf-data", "name": "foo", }, }, } err = diff.Diff(&obj, Printer{}, tc.showManagedFields) if err != nil { t.Fatal(err) } actualFromContent, _ := os.ReadFile(filepath.Join(diff.From.Dir.Name, obj.Name())) if string(actualFromContent) != tc.expectedFromContent { t.Fatalf("File has %q, expected %q", string(actualFromContent), tc.expectedFromContent) } actualToContent, _ := os.ReadFile(filepath.Join(diff.To.Dir.Name, obj.Name())) if string(actualToContent) != tc.expectedToContent { t.Fatalf("File has %q, expected %q", string(actualToContent), tc.expectedToContent) } }) } } func TestMasker(t *testing.T) { type diff struct { from runtime.Object to runtime.Object } cases := []struct { name string input diff want diff }{ { name: "no_changes", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", // still masked "password": "***", // still masked }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", // still masked "password": "***", // still masked }, }, }, }, }, { name: "object_created", input: diff{ from: nil, // does not exist yet to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, }, want: diff{ from: nil, // does not exist yet to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", // no suffix needed "password": "***", // no suffix needed }, }, }, }, }, { name: "object_removed", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, to: nil, // removed }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", // no suffix needed "password": "***", // no suffix needed }, }, }, to: nil, // removed }, }, { name: "data_key_added", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", // added }, }, }, }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", "password": "***", // no suffix needed }, }, }, }, }, { name: "data_key_changed", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "456", // changed }, }, }, }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", "password": "*** (before)", // added suffix for diff }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", "password": "*** (after)", // added suffix for diff }, }, }, }, }, { name: "data_key_removed", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", // "password": "123", // removed }, }, }, }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", "password": "***", // no suffix needed }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", // "password": "***", }, }, }, }, }, { name: "empty_secret_from", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{}, // no data key }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{}, // no data key }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", "password": "***", }, }, }, }, }, { name: "empty_secret_to", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "abc", "password": "123", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{}, // no data key }, }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ "username": "***", "password": "***", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{}, // no data key }, }, }, { name: "invalid_data_key", input: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "some_other_key": map[string]interface{}{ // invalid key "username": "abc", "password": "123", }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "some_other_key": map[string]interface{}{ // invalid key "username": "abc", "password": "123", }, }, }, }, want: diff{ from: &unstructured.Unstructured{ Object: map[string]interface{}{ "some_other_key": map[string]interface{}{ "username": "abc", // skipped "password": "123", // skipped }, }, }, to: &unstructured.Unstructured{ Object: map[string]interface{}{ "some_other_key": map[string]interface{}{ "username": "abc", // skipped "password": "123", // skipped }, }, }, }, }, } for _, tc := range cases { tc := tc // capture range variable t.Run(tc.name, func(t *testing.T) { t.Parallel() m, err := NewMasker(tc.input.from, tc.input.to) if err != nil { t.Fatal(err) } from, to := m.From(), m.To() if from != nil && tc.want.from != nil { if diff := cmp.Diff(from, tc.want.from); diff != "" { t.Errorf("from: (-want +got):\n%s", diff) } } if to != nil && tc.want.to != nil { if diff := cmp.Diff(to, tc.want.to); diff != "" { t.Errorf("to: (-want +got):\n%s", diff) } } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/diff/prune.go000066400000000000000000000067771504711711200272200ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package diff import ( "context" "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/kubectl/pkg/util/prune" ) type tracker struct { visitedUids sets.Set[types.UID] visitedNamespaces sets.Set[string] } func newTracker() *tracker { return &tracker{ visitedUids: sets.New[types.UID](), visitedNamespaces: sets.New[string](), } } type pruner struct { mapper meta.RESTMapper dynamicClient dynamic.Interface labelSelector string resources []prune.Resource } func newPruner(dc dynamic.Interface, m meta.RESTMapper, r []prune.Resource, selector string) *pruner { return &pruner{ dynamicClient: dc, mapper: m, resources: r, labelSelector: selector, } } func (p *pruner) pruneAll(tracker *tracker, namespaceSpecified bool) ([]runtime.Object, error) { var allPruned []runtime.Object namespacedRESTMappings, nonNamespacedRESTMappings, err := prune.GetRESTMappings(p.mapper, p.resources, namespaceSpecified) if err != nil { return allPruned, fmt.Errorf("error retrieving RESTMappings to prune: %v", err) } for n := range tracker.visitedNamespaces { for _, m := range namespacedRESTMappings { if pobjs, err := p.prune(tracker, n, m); err != nil { return pobjs, fmt.Errorf("error pruning namespaced object %v: %v", m.GroupVersionKind, err) } else { allPruned = append(allPruned, pobjs...) } } } for _, m := range nonNamespacedRESTMappings { if pobjs, err := p.prune(tracker, metav1.NamespaceNone, m); err != nil { return allPruned, fmt.Errorf("error pruning nonNamespaced object %v: %v", m.GroupVersionKind, err) } else { allPruned = append(allPruned, pobjs...) } } return allPruned, nil } func (p *pruner) prune(tracker *tracker, namespace string, mapping *meta.RESTMapping) ([]runtime.Object, error) { objList, err := p.dynamicClient.Resource(mapping.Resource). Namespace(namespace). List(context.TODO(), metav1.ListOptions{ LabelSelector: p.labelSelector, }) if err != nil { return nil, err } objs, err := meta.ExtractList(objList) if err != nil { return nil, err } var pobjs []runtime.Object for _, obj := range objs { metadata, err := meta.Accessor(obj) if err != nil { return pobjs, err } annots := metadata.GetAnnotations() if _, ok := annots[corev1.LastAppliedConfigAnnotation]; !ok { continue } uid := metadata.GetUID() if tracker.visitedUids.Has(uid) { continue } pobjs = append(pobjs, obj) } return pobjs, nil } // MarkVisited marks visited namespaces and uids func (t *tracker) MarkVisited(info *resource.Info) { if info.Namespaced() { t.visitedNamespaces.Insert(info.Namespace) } metadata, err := meta.Accessor(info.Object) if err != nil { return } t.visitedUids.Insert(metadata.GetUID()) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/drain/000077500000000000000000000000001504711711200257045ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain.go000066400000000000000000000363331504711711200273400ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "errors" "fmt" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/drain" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" ) type DrainCmdOptions struct { PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinterFunc, error) Namespace string drainer *drain.Helper nodeInfos []*resource.Info genericiooptions.IOStreams WarningPrinter *printers.WarningPrinter } var ( cordonLong = templates.LongDesc(i18n.T(` Mark node as unschedulable.`)) cordonExample = templates.Examples(i18n.T(` # Mark node "foo" as unschedulable kubectl cordon foo`)) ) func NewCmdCordon(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewDrainCmdOptions(f, ioStreams) cmd := &cobra.Command{ Use: "cordon NODE", DisableFlagsInUseLine: true, Short: i18n.T("Mark node as unschedulable"), Long: cordonLong, Example: cordonExample, ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "node"), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.RunCordonOrUncordon(true)) }, } cmdutil.AddLabelSelectorFlagVar(cmd, &o.drainer.Selector) cmdutil.AddDryRunFlag(cmd) return cmd } var ( uncordonLong = templates.LongDesc(i18n.T(` Mark node as schedulable.`)) uncordonExample = templates.Examples(i18n.T(` # Mark node "foo" as schedulable kubectl uncordon foo`)) ) func NewCmdUncordon(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewDrainCmdOptions(f, ioStreams) cmd := &cobra.Command{ Use: "uncordon NODE", DisableFlagsInUseLine: true, Short: i18n.T("Mark node as schedulable"), Long: uncordonLong, Example: uncordonExample, ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "node"), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.RunCordonOrUncordon(false)) }, } cmdutil.AddLabelSelectorFlagVar(cmd, &o.drainer.Selector) cmdutil.AddDryRunFlag(cmd) return cmd } var ( drainLong = templates.LongDesc(i18n.T(` Drain node in preparation for maintenance. The given node will be marked unschedulable to prevent new pods from arriving. 'drain' evicts the pods if the API server supports [eviction](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/). Otherwise, it will use normal DELETE to delete the pods. The 'drain' evicts or deletes all pods except mirror pods (which cannot be deleted through the API server). If there are daemon set-managed pods, drain will not proceed without --ignore-daemonsets, and regardless it will not delete any daemon set-managed pods, because those pods would be immediately replaced by the daemon set controller, which ignores unschedulable markings. If there are any pods that are neither mirror pods nor managed by a replication controller, replica set, daemon set, stateful set, or job, then drain will not delete any pods unless you use --force. --force will also allow deletion to proceed if the managing resource of one or more pods is missing. 'drain' waits for graceful termination. You should not operate on the machine until the command completes. When you are ready to put the node back into service, use kubectl uncordon, which will make the node schedulable again. ![Workflow](https://kubernetes.io/images/docs/kubectl_drain.svg)`)) drainExample = templates.Examples(i18n.T(` # Drain node "foo", even if there are pods not managed by a replication controller, replica set, job, daemon set, or stateful set on it kubectl drain foo --force # As above, but abort if there are pods not managed by a replication controller, replica set, job, daemon set, or stateful set, and use a grace period of 15 minutes kubectl drain foo --grace-period=900`)) ) func NewDrainCmdOptions(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *DrainCmdOptions { o := &DrainCmdOptions{ PrintFlags: genericclioptions.NewPrintFlags("drained").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, drainer: &drain.Helper{ GracePeriodSeconds: -1, Out: ioStreams.Out, ErrOut: ioStreams.ErrOut, ChunkSize: cmdutil.DefaultChunkSize, }, } o.drainer.OnPodDeletionOrEvictionFinished = o.onPodDeletionOrEvictionFinished o.drainer.OnPodDeletionOrEvictionStarted = o.onPodDeletionOrEvictionStarted return o } // onPodDeletionOrEvictionFinished is called by drain.Helper, when eviction/deletetion of the pod is finished func (o *DrainCmdOptions) onPodDeletionOrEvictionFinished(pod *corev1.Pod, usingEviction bool, err error) { var verbStr string if usingEviction { if err != nil { verbStr = "eviction failed" } else { verbStr = "evicted" } } else { if err != nil { verbStr = "deletion failed" } else { verbStr = "deleted" } } printObj, err := o.ToPrinter(verbStr) if err != nil { fmt.Fprintf(o.ErrOut, "error building printer: %v\n", err) fmt.Fprintf(o.Out, "pod %s/%s %s\n", pod.Namespace, pod.Name, verbStr) } else { _ = printObj(pod, o.Out) } } // onPodDeletionOrEvictionStarted is called by drain.Helper, when eviction/deletion of the pod is started func (o *DrainCmdOptions) onPodDeletionOrEvictionStarted(pod *corev1.Pod, usingEviction bool) { if !klog.V(2).Enabled() { return } var verbStr string if usingEviction { verbStr = "eviction started" } else { verbStr = "deletion started" } printObj, err := o.ToPrinter(verbStr) if err != nil { fmt.Fprintf(o.ErrOut, "error building printer: %v\n", err) fmt.Fprintf(o.Out, "pod %s/%s %s\n", pod.Namespace, pod.Name, verbStr) } else { _ = printObj(pod, o.Out) } } func NewCmdDrain(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewDrainCmdOptions(f, ioStreams) cmd := &cobra.Command{ Use: "drain NODE", DisableFlagsInUseLine: true, Short: i18n.T("Drain node in preparation for maintenance"), Long: drainLong, Example: drainExample, ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "node"), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.RunDrain()) }, } cmd.Flags().BoolVar(&o.drainer.Force, "force", o.drainer.Force, "Continue even if there are pods that do not declare a controller.") cmd.Flags().BoolVar(&o.drainer.IgnoreAllDaemonSets, "ignore-daemonsets", o.drainer.IgnoreAllDaemonSets, "Ignore DaemonSet-managed pods.") cmd.Flags().BoolVar(&o.drainer.DeleteEmptyDirData, "delete-emptydir-data", o.drainer.DeleteEmptyDirData, "Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).") cmd.Flags().IntVar(&o.drainer.GracePeriodSeconds, "grace-period", o.drainer.GracePeriodSeconds, "Period of time in seconds given to each pod to terminate gracefully. If negative, the default value specified in the pod will be used.") cmd.Flags().DurationVar(&o.drainer.Timeout, "timeout", o.drainer.Timeout, "The length of time to wait before giving up, zero means infinite") cmd.Flags().StringVarP(&o.drainer.PodSelector, "pod-selector", "", o.drainer.PodSelector, "Label selector to filter pods on the node") cmd.Flags().BoolVar(&o.drainer.DisableEviction, "disable-eviction", o.drainer.DisableEviction, "Force drain to use delete, even if eviction is supported. This will bypass checking PodDisruptionBudgets, use with caution.") cmd.Flags().IntVar(&o.drainer.SkipWaitForDeleteTimeoutSeconds, "skip-wait-for-delete-timeout", o.drainer.SkipWaitForDeleteTimeoutSeconds, "If pod DeletionTimestamp older than N seconds, skip waiting for the pod. Seconds must be greater than 0 to skip.") cmdutil.AddChunkSizeFlag(cmd, &o.drainer.ChunkSize) cmdutil.AddDryRunFlag(cmd) cmdutil.AddLabelSelectorFlagVar(cmd, &o.drainer.Selector) return cmd } // Complete populates some fields from the factory, grabs command line // arguments and looks up the node using Builder func (o *DrainCmdOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error if len(args) == 0 && !cmd.Flags().Changed("selector") { return cmdutil.UsageErrorf(cmd, "USAGE: %s [flags]", cmd.Use) } if len(args) > 0 && len(o.drainer.Selector) > 0 { return cmdutil.UsageErrorf(cmd, "error: cannot specify both a node name and a --selector option") } o.drainer.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } if o.drainer.Client, err = f.KubernetesClientSet(); err != nil { return err } if len(o.drainer.PodSelector) > 0 { if _, err := labels.Parse(o.drainer.PodSelector); err != nil { return errors.New("--pod-selector= must be a valid label selector") } } o.nodeInfos = []*resource.Info{} o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.ToPrinter = func(operation string) (printers.ResourcePrinterFunc, error) { o.PrintFlags.NamePrintFlags.Operation = operation cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.drainer.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return nil, err } return printer.PrintObj, nil } // Set default WarningPrinter if not already set. if o.WarningPrinter == nil { o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) } builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). RequestChunksOf(o.drainer.ChunkSize). ResourceNames("nodes", args...). SingleResourceType(). Flatten() if len(o.drainer.Selector) > 0 { builder = builder.LabelSelectorParam(o.drainer.Selector). ResourceTypes("nodes") } r := builder.Do() if err = r.Err(); err != nil { return err } return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } if info.Mapping.Resource.GroupResource() != (schema.GroupResource{Group: "", Resource: "nodes"}) { return fmt.Errorf("error: expected resource of type node, got %q", info.Mapping.Resource) } o.nodeInfos = append(o.nodeInfos, info) return nil }) } // RunDrain runs the 'drain' command func (o *DrainCmdOptions) RunDrain() error { if err := o.RunCordonOrUncordon(true); err != nil { return err } drainedNodes := sets.New[string]() var fatal []error remainingNodes := []string{} for _, info := range o.nodeInfos { if err := o.deleteOrEvictPodsSimple(info); err == nil { drainedNodes.Insert(info.Name) printObj, err := o.ToPrinter("drained") if err != nil { return err } printObj(info.Object, o.Out) } else { fmt.Fprintf(o.ErrOut, "error: unable to drain node %q due to error: %s, continuing command...\n", info.Name, err) if !drainedNodes.Has(info.Name) { fatal = append(fatal, err) remainingNodes = append(remainingNodes, info.Name) } continue } } if len(remainingNodes) > 0 { fmt.Fprintf(o.ErrOut, "There are pending nodes to be drained:\n") for _, nodeName := range remainingNodes { fmt.Fprintf(o.ErrOut, " %s\n", nodeName) } } return utilerrors.NewAggregate(fatal) } func (o *DrainCmdOptions) deleteOrEvictPodsSimple(nodeInfo *resource.Info) error { list, errs := o.drainer.GetPodsForDeletion(nodeInfo.Name) if errs != nil { return utilerrors.NewAggregate(errs) } if warnings := list.Warnings(); warnings != "" { o.WarningPrinter.Print(warnings) } if o.drainer.DryRunStrategy == cmdutil.DryRunClient { for _, pod := range list.Pods() { fmt.Fprintf(o.Out, "evicting pod %s/%s (dry run)\n", pod.Namespace, pod.Name) } return nil } if err := o.drainer.DeleteOrEvictPods(list.Pods()); err != nil { pendingList, newErrs := o.drainer.GetPodsForDeletion(nodeInfo.Name) if pendingList != nil { pods := pendingList.Pods() if len(pods) != 0 { fmt.Fprintf(o.ErrOut, "There are pending pods in node %q when an error occurred: %v\n", nodeInfo.Name, err) for _, pendingPod := range pods { fmt.Fprintf(o.ErrOut, "%s/%s\n", "pod", pendingPod.Name) } } } if newErrs != nil { fmt.Fprintf(o.ErrOut, "Following errors occurred while getting the list of pods to delete:\n%s", utilerrors.NewAggregate(newErrs)) } return err } return nil } // RunCordonOrUncordon runs either Cordon or Uncordon. The desired value for // "Unschedulable" is passed as the first arg. func (o *DrainCmdOptions) RunCordonOrUncordon(desired bool) error { cordonOrUncordon := "cordon" if !desired { cordonOrUncordon = "un" + cordonOrUncordon } for _, nodeInfo := range o.nodeInfos { printError := func(err error) { fmt.Fprintf(o.ErrOut, "error: unable to %s node %q: %v\n", cordonOrUncordon, nodeInfo.Name, err) } gvk := nodeInfo.ResourceMapping().GroupVersionKind if gvk.Kind == "Node" { c, err := drain.NewCordonHelperFromRuntimeObject(nodeInfo.Object, scheme.Scheme, gvk) if err != nil { printError(err) continue } if updateRequired := c.UpdateIfRequired(desired); !updateRequired { printObj, err := o.ToPrinter(already(desired)) if err != nil { fmt.Fprintf(o.ErrOut, "error: %v\n", err) continue } printObj(nodeInfo.Object, o.Out) } else { if o.drainer.DryRunStrategy != cmdutil.DryRunClient { err, patchErr := c.PatchOrReplace(o.drainer.Client, o.drainer.DryRunStrategy == cmdutil.DryRunServer) if patchErr != nil { printError(patchErr) } if err != nil { printError(err) continue } } printObj, err := o.ToPrinter(changed(desired)) if err != nil { fmt.Fprintf(o.ErrOut, "%v\n", err) continue } printObj(nodeInfo.Object, o.Out) } } else { printObj, err := o.ToPrinter("skipped") if err != nil { fmt.Fprintf(o.ErrOut, "%v\n", err) continue } printObj(nodeInfo.Object, o.Out) } } return nil } // already() and changed() return suitable strings for {un,}cordoning func already(desired bool) string { if desired { return "already cordoned" } return "already uncordoned" } func changed(desired bool) string { if desired { return "cordoned" } return "uncordoned" } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain_test.go000066400000000000000000000733351504711711200304020ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "errors" "io" "net/http" "net/url" "os" "reflect" "strings" "sync/atomic" "testing" "time" "github.com/spf13/cobra" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/drain" "k8s.io/kubectl/pkg/scheme" "k8s.io/utils/ptr" ) const ( EvictionMethod = "Eviction" DeleteMethod = "Delete" ) var node *corev1.Node var cordonedNode *corev1.Node func TestMain(m *testing.M) { // Create a node. node = &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node", CreationTimestamp: metav1.Time{Time: time.Now()}, }, Status: corev1.NodeStatus{}, } // A copy of the same node, but cordoned. cordonedNode = node.DeepCopy() cordonedNode.Spec.Unschedulable = true os.Exit(m.Run()) } func TestCordon(t *testing.T) { tests := []struct { description string node *corev1.Node expected *corev1.Node cmd func(cmdutil.Factory, genericiooptions.IOStreams) *cobra.Command arg string expectFatal bool }{ { description: "node/node syntax", node: cordonedNode, expected: node, cmd: NewCmdUncordon, arg: "node/node", expectFatal: false, }, { description: "uncordon for real", node: cordonedNode, expected: node, cmd: NewCmdUncordon, arg: "node", expectFatal: false, }, { description: "uncordon does nothing", node: node, expected: node, cmd: NewCmdUncordon, arg: "node", expectFatal: false, }, { description: "cordon does nothing", node: cordonedNode, expected: cordonedNode, cmd: NewCmdCordon, arg: "node", expectFatal: false, }, { description: "cordon for real", node: node, expected: cordonedNode, cmd: NewCmdCordon, arg: "node", expectFatal: false, }, { description: "cordon missing node", node: node, expected: node, cmd: NewCmdCordon, arg: "bar", expectFatal: true, }, { description: "uncordon missing node", node: node, expected: node, cmd: NewCmdUncordon, arg: "bar", expectFatal: true, }, { description: "cordon for multiple nodes", node: node, expected: cordonedNode, cmd: NewCmdCordon, arg: "node node1 node2", expectFatal: false, }, { description: "uncordon for multiple nodes", node: cordonedNode, expected: node, cmd: NewCmdUncordon, arg: "node node1 node2", expectFatal: false, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() newNode := &corev1.Node{} updated := false tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { m := &MyReq{req} switch { case m.isFor("GET", "/nodes/node1"): fallthrough case m.isFor("GET", "/nodes/node2"): fallthrough case m.isFor("GET", "/nodes/node"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.node)}, nil case m.isFor("GET", "/nodes/bar"): return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("nope")}, nil case m.isFor("PATCH", "/nodes/node1"): fallthrough case m.isFor("PATCH", "/nodes/node2"): fallthrough case m.isFor("PATCH", "/nodes/node"): data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } defer req.Body.Close() oldJSON, err := runtime.Encode(codec, node) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{}) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } if !reflect.DeepEqual(test.expected.Spec, newNode.Spec) { t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec.Unschedulable, newNode.Spec.Unschedulable) } updated = true return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil default: t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := test.cmd(tf, ioStreams) var recovered interface{} sawFatal := false func() { defer func() { // Recover from the panic below. recovered = recover() // Restore cmdutil behavior cmdutil.DefaultBehaviorOnFatal() }() cmdutil.BehaviorOnFatal(func(e string, code int) { sawFatal = true panic(e) }) cmd.SetArgs(strings.Split(test.arg, " ")) cmd.Execute() }() switch { case recovered != nil && !sawFatal: t.Fatalf("got panic: %v", recovered) case test.expectFatal: if !sawFatal { t.Fatalf("%s: unexpected non-error", test.description) } if updated { t.Fatalf("%s: unexpected update", test.description) } case !test.expectFatal && sawFatal: t.Fatalf("%s: unexpected error", test.description) case !reflect.DeepEqual(test.expected.Spec, test.node.Spec) && !updated: t.Fatalf("%s: node never updated", test.description) } }) } } func TestDrain(t *testing.T) { labels := make(map[string]string) labels["my_key"] = "my_value" rc := corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{ Name: "rc", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, }, Spec: corev1.ReplicationControllerSpec{ Selector: labels, }, } rcPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "ReplicationController", Name: "rc", UID: "123", BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true), }, }, }, Spec: corev1.PodSpec{ NodeName: "node", }, } ds := appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "ds", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, }, Spec: appsv1.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: labels}, }, } dsPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "apps/v1", Kind: "DaemonSet", Name: "ds", BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true), }, }, }, Spec: corev1.PodSpec{ NodeName: "node", }, } dsTerminatedPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "apps/v1", Kind: "DaemonSet", Name: "ds", BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true), }, }, }, Spec: corev1.PodSpec{ NodeName: "node", }, Status: corev1.PodStatus{ Phase: corev1.PodSucceeded, }, } dsPodWithEmptyDir := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "apps/v1", Kind: "DaemonSet", Name: "ds", BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true), }, }, }, Spec: corev1.PodSpec{ NodeName: "node", Volumes: []corev1.Volume{ { Name: "scratch", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}}, }, }, }, } orphanedDsPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, }, Spec: corev1.PodSpec{ NodeName: "node", }, } job := batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "job", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, }, Spec: batchv1.JobSpec{ Selector: &metav1.LabelSelector{MatchLabels: labels}, }, } jobPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "Job", Name: "job", BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true), }, }, }, Spec: corev1.PodSpec{ NodeName: "node", Volumes: []corev1.Volume{ { Name: "scratch", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}}, }, }, }, } terminatedJobPodWithLocalStorage := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "Job", Name: "job", BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true), }, }, }, Spec: corev1.PodSpec{ NodeName: "node", Volumes: []corev1.Volume{ { Name: "scratch", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}}, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodSucceeded, }, } rs := appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "rs", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, }, Spec: appsv1.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: labels}, }, } rsPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "ReplicaSet", Name: "rs", BlockOwnerDeletion: ptr.To(true), Controller: ptr.To(true), }, }, }, Spec: corev1.PodSpec{ NodeName: "node", }, } nakedPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, }, Spec: corev1.PodSpec{ NodeName: "node", }, } emptydirPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, }, Spec: corev1.PodSpec{ NodeName: "node", Volumes: []corev1.Volume{ { Name: "scratch", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}}, }, }, }, } emptydirTerminatedPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "default", CreationTimestamp: metav1.Time{Time: time.Now()}, Labels: labels, }, Spec: corev1.PodSpec{ NodeName: "node", Volumes: []corev1.Volume{ { Name: "scratch", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}}, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodFailed, }, } tests := []struct { description string node *corev1.Node expected *corev1.Node pods []corev1.Pod rcs []corev1.ReplicationController replicaSets []appsv1.ReplicaSet args []string failUponEvictionOrDeletion bool expectWarning string expectFatal bool expectDelete bool expectOutputToContain string }{ { description: "RC-managed pod", node: node, expected: cordonedNode, pods: []corev1.Pod{rcPod}, rcs: []corev1.ReplicationController{rc}, args: []string{"node"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "DS-managed pod", node: node, expected: cordonedNode, pods: []corev1.Pod{dsPod}, rcs: []corev1.ReplicationController{rc}, args: []string{"node"}, expectFatal: true, expectDelete: false, }, { description: "DS-managed terminated pod", node: node, expected: cordonedNode, pods: []corev1.Pod{dsTerminatedPod}, rcs: []corev1.ReplicationController{rc}, args: []string{"node"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "orphaned DS-managed pod", node: node, expected: cordonedNode, pods: []corev1.Pod{orphanedDsPod}, rcs: []corev1.ReplicationController{}, args: []string{"node"}, expectFatal: true, expectDelete: false, }, { description: "orphaned DS-managed pod with --force", node: node, expected: cordonedNode, pods: []corev1.Pod{orphanedDsPod}, rcs: []corev1.ReplicationController{}, args: []string{"node", "--force"}, expectFatal: false, expectDelete: true, expectWarning: "Warning: deleting Pods that declare no controller: default/bar", expectOutputToContain: "node/node drained", }, { description: "DS-managed pod with --ignore-daemonsets", node: node, expected: cordonedNode, pods: []corev1.Pod{dsPod}, rcs: []corev1.ReplicationController{rc}, args: []string{"node", "--ignore-daemonsets"}, expectFatal: false, expectDelete: false, expectOutputToContain: "node/node drained", }, { description: "DS-managed pod with emptyDir with --ignore-daemonsets", node: node, expected: cordonedNode, pods: []corev1.Pod{dsPodWithEmptyDir}, rcs: []corev1.ReplicationController{rc}, args: []string{"node", "--ignore-daemonsets"}, expectWarning: "Warning: ignoring DaemonSet-managed Pods: default/bar", expectFatal: false, expectDelete: false, expectOutputToContain: "node/node drained", }, { description: "Job-managed pod with local storage", node: node, expected: cordonedNode, pods: []corev1.Pod{jobPod}, rcs: []corev1.ReplicationController{rc}, args: []string{"node", "--force", "--delete-emptydir-data=true"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "Job-managed terminated pod", node: node, expected: cordonedNode, pods: []corev1.Pod{terminatedJobPodWithLocalStorage}, rcs: []corev1.ReplicationController{rc}, args: []string{"node"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "RS-managed pod", node: node, expected: cordonedNode, pods: []corev1.Pod{rsPod}, replicaSets: []appsv1.ReplicaSet{rs}, args: []string{"node"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "naked pod", node: node, expected: cordonedNode, pods: []corev1.Pod{nakedPod}, rcs: []corev1.ReplicationController{}, args: []string{"node"}, expectFatal: true, expectDelete: false, }, { description: "naked pod with --force", node: node, expected: cordonedNode, pods: []corev1.Pod{nakedPod}, rcs: []corev1.ReplicationController{}, args: []string{"node", "--force"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "pod with EmptyDir", node: node, expected: cordonedNode, pods: []corev1.Pod{emptydirPod}, args: []string{"node", "--force"}, expectFatal: true, expectDelete: false, }, { description: "terminated pod with emptyDir", node: node, expected: cordonedNode, pods: []corev1.Pod{emptydirTerminatedPod}, rcs: []corev1.ReplicationController{rc}, args: []string{"node"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "pod with EmptyDir and --delete-emptydir-data", node: node, expected: cordonedNode, pods: []corev1.Pod{emptydirPod}, args: []string{"node", "--force", "--delete-emptydir-data=true"}, expectFatal: false, expectDelete: true, expectOutputToContain: "node/node drained", }, { description: "empty node", node: node, expected: cordonedNode, pods: []corev1.Pod{}, rcs: []corev1.ReplicationController{rc}, args: []string{"node"}, expectFatal: false, expectDelete: false, expectOutputToContain: "node/node drained", }, { description: "fail to list pods", node: node, expected: cordonedNode, pods: []corev1.Pod{rsPod}, replicaSets: []appsv1.ReplicaSet{rs}, args: []string{"node"}, expectFatal: true, expectDelete: true, failUponEvictionOrDeletion: true, }, } testEviction := false for i := 0; i < 2; i++ { testEviction = !testEviction var currMethod string if testEviction { currMethod = EvictionMethod } else { currMethod = DeleteMethod } for _, test := range tests { t.Run(test.description, func(t *testing.T) { newNode := &corev1.Node{} var deletions, evictions int32 tf := cmdtesting.NewTestFactory() defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { m := &MyReq{req} switch { case req.Method == "GET" && req.URL.Path == "/api": apiVersions := metav1.APIVersions{ Versions: []string{"v1"}, } return cmdtesting.GenResponseWithJsonEncodedBody(apiVersions) case req.Method == "GET" && req.URL.Path == "/apis": groupList := metav1.APIGroupList{ Groups: []metav1.APIGroup{ { Name: "policy", PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "policy/v1", }, }, }, } return cmdtesting.GenResponseWithJsonEncodedBody(groupList) case req.Method == "GET" && req.URL.Path == "/api/v1": resourceList := metav1.APIResourceList{ GroupVersion: "v1", } if testEviction { resourceList.APIResources = []metav1.APIResource{ { Name: drain.EvictionSubresource, Kind: drain.EvictionKind, Group: "policy", Version: "v1", }, } } return cmdtesting.GenResponseWithJsonEncodedBody(resourceList) case m.isFor("GET", "/nodes/node"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.node)}, nil case m.isFor("GET", "/namespaces/default/replicationcontrollers/rc"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.rcs[0])}, nil case m.isFor("GET", "/namespaces/default/daemonsets/ds"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &ds)}, nil case m.isFor("GET", "/namespaces/default/daemonsets/missing-ds"): return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.DaemonSet{})}, nil case m.isFor("GET", "/namespaces/default/jobs/job"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &job)}, nil case m.isFor("GET", "/namespaces/default/replicasets/rs"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.replicaSets[0])}, nil case m.isFor("GET", "/namespaces/default/pods/bar"): return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil case m.isFor("GET", "/pods"): if test.failUponEvictionOrDeletion && atomic.LoadInt32(&evictions) > 0 || atomic.LoadInt32(&deletions) > 0 { return nil, errors.New("request failed") } values, err := url.ParseQuery(req.URL.RawQuery) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } getParams := make(url.Values) getParams["fieldSelector"] = []string{"spec.nodeName=node"} getParams["limit"] = []string{"500"} if !reflect.DeepEqual(getParams, values) { t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, getParams, values) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.PodList{Items: test.pods})}, nil case m.isFor("GET", "/replicationcontrollers"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.ReplicationControllerList{Items: test.rcs})}, nil case m.isFor("PATCH", "/nodes/node"): data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } defer req.Body.Close() oldJSON, err := runtime.Encode(codec, node) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{}) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } if !reflect.DeepEqual(test.expected.Spec, newNode.Spec) { t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec, newNode.Spec) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil case m.isFor("DELETE", "/namespaces/default/pods/bar"): atomic.AddInt32(&deletions, 1) if test.failUponEvictionOrDeletion { return nil, errors.New("request failed") } return &http.Response{StatusCode: http.StatusNoContent, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.pods[0])}, nil case m.isFor("POST", "/namespaces/default/pods/bar/eviction"): atomic.AddInt32(&evictions, 1) if test.failUponEvictionOrDeletion { return nil, errors.New("request failed") } return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &metav1.Status{})}, nil default: t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, outBuf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdDrain(tf, ioStreams) var recovered interface{} sawFatal := false fatalMsg := "" func() { defer func() { // Recover from the panic below. recovered = recover() // Restore cmdutil behavior cmdutil.DefaultBehaviorOnFatal() }() cmdutil.BehaviorOnFatal(func(e string, code int) { sawFatal = true; fatalMsg = e; panic(e) }) cmd.SetArgs(test.args) cmd.Execute() }() switch { case recovered != nil && !sawFatal: t.Fatalf("got panic: %v", recovered) case test.expectFatal && !sawFatal: t.Fatalf("%s: unexpected non-error when using %s", test.description, currMethod) case !test.expectFatal && sawFatal: t.Fatalf("%s: unexpected error when using %s: %s", test.description, currMethod, fatalMsg) } deleted := deletions > 0 evicted := evictions > 0 if test.expectDelete { // Test Delete if !testEviction && !deleted { t.Fatalf("%s: pod never deleted", test.description) } // Test Eviction if testEviction { if !evicted { t.Fatalf("%s: pod never evicted", test.description) } if evictions > 1 { t.Fatalf("%s: asked to evict same pod %d too many times", test.description, evictions-1) } } } if !test.expectDelete { if deleted { t.Fatalf("%s: unexpected delete when using %s", test.description, currMethod) } if deletions > 1 { t.Fatalf("%s: asked to deleted same pod %d too many times", test.description, deletions-1) } } if deleted && evicted { t.Fatalf("%s: same pod deleted %d times and evicted %d times", test.description, deletions, evictions) } if len(test.expectWarning) > 0 { if len(errBuf.String()) == 0 { t.Fatalf("%s: expected warning, but found no stderr output", test.description) } // Mac and Bazel on Linux behave differently when returning newlines if a, e := errBuf.String(), test.expectWarning; !strings.Contains(a, e) { t.Fatalf("%s: actual warning message did not match expected warning message.\n Expecting:\n%v\n Got:\n%v", test.description, e, a) } } if len(test.expectOutputToContain) > 0 { out := outBuf.String() if !strings.Contains(out, test.expectOutputToContain) { t.Fatalf("%s: expected output to contain: %s\nGot:\n%s", test.description, test.expectOutputToContain, out) } } }) } } } type MyReq struct { Request *http.Request } func (m *MyReq) isFor(method string, path string) bool { req := m.Request return method == req.Method && (req.URL.Path == path || req.URL.Path == strings.Join([]string{"/api/v1", path}, "") || req.URL.Path == strings.Join([]string{"/apis/apps/v1", path}, "") || req.URL.Path == strings.Join([]string{"/apis/batch/v1", path}, "")) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/000077500000000000000000000000001504711711200255345ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/edit.go000066400000000000000000000105751504711711200270200ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package edit import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/editor" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( editLong = templates.LongDesc(i18n.T(` Edit a resource from the default editor. The edit command allows you to directly edit any API resource you can retrieve via the command-line tools. It will open the editor defined by your KUBE_EDITOR, or EDITOR environment variables, or fall back to 'vi' for Linux or 'notepad' for Windows. When attempting to open the editor, it will first attempt to use the shell that has been defined in the 'SHELL' environment variable. If this is not defined, the default shell will be used, which is '/bin/bash' for Linux or 'cmd' for Windows. You can edit multiple objects, although changes are applied one at a time. The command accepts file names as well as command-line arguments, although the files you point to must be previously saved versions of resources. Editing is done with the API version used to fetch the resource. To edit using a specific API version, fully-qualify the resource, version, and group. The default format is YAML. To edit in JSON, specify "-o json". The flag --windows-line-endings can be used to force Windows line endings, otherwise the default for your operating system will be used. In the event an error occurs while updating, a temporary file will be created on disk that contains your unapplied changes. The most common error when updating a resource is another editor changing the resource on the server. When this occurs, you will have to apply your changes to the newer version of the resource, or update your temporary saved copy to include the latest resource version.`)) editExample = templates.Examples(i18n.T(` # Edit the service named 'registry' kubectl edit svc/registry # Use an alternative editor KUBE_EDITOR="nano" kubectl edit svc/registry # Edit the job 'myjob' in JSON using the v1 API format kubectl edit job.v1.batch/myjob -o json # Edit the deployment 'mydeployment' in YAML and save the modified config in its annotation kubectl edit deployment/mydeployment -o yaml --save-config # Edit the 'status' subresource for the 'mydeployment' deployment kubectl edit deployment mydeployment --subresource='status'`)) ) // NewCmdEdit creates the `edit` command func NewCmdEdit(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := editor.NewEditOptions(editor.NormalEditMode, ioStreams) cmd := &cobra.Command{ Use: "edit (RESOURCE/NAME | -f FILENAME)", DisableFlagsInUseLine: true, Short: i18n.T("Edit a resource on the server"), Long: editLong, Example: editExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, args, cmd)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } // bind flag structs o.RecordFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) usage := "to use to edit the resource" cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddValidateFlags(cmd) cmd.Flags().BoolVarP(&o.OutputPatch, "output-patch", "", o.OutputPatch, "Output the patch if the resource is edited.") cmd.Flags().BoolVar(&o.WindowsLineEndings, "windows-line-endings", o.WindowsLineEndings, "Defaults to the line ending native to your platform.") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit") cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation) cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, edit will operate on the subresource of the requested object.") return cmd } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/edit_test.go000066400000000000000000000222221504711711200300470ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package edit import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" "k8s.io/kubectl/pkg/cmd/apply" "k8s.io/kubectl/pkg/cmd/create" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" yaml "sigs.k8s.io/yaml/goyaml.v2" ) type EditTestCase struct { Description string `yaml:"description"` // create or edit Mode string `yaml:"mode"` Args []string `yaml:"args"` Filename string `yaml:"filename"` Output string `yaml:"outputFormat"` OutputPatch string `yaml:"outputPatch"` SaveConfig string `yaml:"saveConfig"` Subresource string `yaml:"subresource"` Namespace string `yaml:"namespace"` ExpectedStdout []string `yaml:"expectedStdout"` ExpectedStderr []string `yaml:"expectedStderr"` ExpectedExitCode int `yaml:"expectedExitCode"` Steps []EditStep `yaml:"steps"` } type EditStep struct { // edit or request StepType string `yaml:"type"` // only applies to request RequestMethod string `yaml:"expectedMethod,omitempty"` RequestPath string `yaml:"expectedPath,omitempty"` RequestContentType string `yaml:"expectedContentType,omitempty"` Input string `yaml:"expectedInput"` // only applies to request ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"` Output string `yaml:"resultingOutput"` } func TestEdit(t *testing.T) { var ( name string testcase EditTestCase i int err error ) const updateEnvVar = "UPDATE_EDIT_FIXTURE_DATA" updateInputFixtures := os.Getenv(updateEnvVar) == "true" reqResp := func(req *http.Request) (*http.Response, error) { defer func() { i++ }() if i > len(testcase.Steps)-1 { t.Fatalf("%s, step %d: more requests than steps, got %s %s", name, i, req.Method, req.URL.Path) } step := testcase.Steps[i] body := []byte{} if req.Body != nil { body, err = io.ReadAll(req.Body) if err != nil { t.Fatalf("%s, step %d: %v", name, i, err) } } inputFile := filepath.Join("testdata", "testcase-"+name, step.Input) expectedInput, err := os.ReadFile(inputFile) if err != nil { t.Fatalf("%s, step %d: %v", name, i, err) } outputFile := filepath.Join("testdata", "testcase-"+name, step.Output) resultingOutput, err := os.ReadFile(outputFile) if err != nil { t.Fatalf("%s, step %d: %v", name, i, err) } if req.Method == "POST" && req.URL.Path == "/callback" { if step.StepType != "edit" { t.Fatalf("%s, step %d: expected edit step, got %s %s", name, i, req.Method, req.URL.Path) } if !bytes.Equal(body, expectedInput) { if updateInputFixtures { // Convenience to allow recapturing the input and persisting it here os.WriteFile(inputFile, body, os.FileMode(0644)) } else { t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, cmp.Diff(string(body), string(expectedInput))) t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar) } } return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(resultingOutput))}, nil } if step.StepType != "request" { t.Fatalf("%s, step %d: expected request step, got %s %s", name, i, req.Method, req.URL.Path) } body = tryIndent(body) expectedInput = tryIndent(expectedInput) if req.Method != step.RequestMethod || req.URL.Path != step.RequestPath || req.Header.Get("Content-Type") != step.RequestContentType { t.Fatalf( "%s, step %d: expected \n%s %s (content-type=%s)\ngot\n%s %s (content-type=%s)", name, i, step.RequestMethod, step.RequestPath, step.RequestContentType, req.Method, req.URL.Path, req.Header.Get("Content-Type"), ) } if !bytes.Equal(body, expectedInput) { if updateInputFixtures { // Convenience to allow recapturing the input and persisting it here os.WriteFile(inputFile, body, os.FileMode(0644)) } else { t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, cmp.Diff(string(body), string(expectedInput))) t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar) } } return &http.Response{StatusCode: step.ResponseStatusCode, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(resultingOutput))}, nil } handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { resp, _ := reqResp(req) for k, vs := range resp.Header { w.Header().Del(k) for _, v := range vs { w.Header().Add(k, v) } } w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) }) server := httptest.NewServer(handler) defer server.Close() t.Setenv("KUBE_EDITOR", "testdata/test_editor.sh") t.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback") testcases := sets.New[string]() filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { if err != nil { return err } if path == "testdata" { return nil } name := filepath.Base(path) if info.IsDir() { if strings.HasPrefix(name, "testcase-") { testcases.Insert(strings.TrimPrefix(name, "testcase-")) } return filepath.SkipDir } return nil }) // sanity check that we found the right folder if !testcases.Has("create-list") { t.Fatalf("Error locating edit testcases") } for _, testcaseName := range testcases.UnsortedList() { t.Run(testcaseName, func(t *testing.T) { i = 0 name = testcaseName testcase = EditTestCase{} testcaseDir := filepath.Join("testdata", "testcase-"+name) testcaseData, err := os.ReadFile(filepath.Join(testcaseDir, "test.yaml")) if err != nil { t.Fatalf("%s: %v", name, err) } if err := yaml.Unmarshal(testcaseData, &testcase); err != nil { t.Fatalf("%s: %v", name, err) } tf := cmdtesting.NewTestFactory() defer tf.Cleanup() tf.UnstructuredClientForMappingFunc = func(gv schema.GroupVersion) (resource.RESTClient, error) { versionedAPIPath := "" if gv.Group == "" { versionedAPIPath = "/api/" + gv.Version } else { versionedAPIPath = "/apis/" + gv.Group + "/" + gv.Version } return &fake.RESTClient{ VersionedAPIPath: versionedAPIPath, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(reqResp), }, nil } tf.WithNamespace(testcase.Namespace) tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() var cmd *cobra.Command switch testcase.Mode { case "edit": cmd = NewCmdEdit(tf, ioStreams) case "create": cmd = create.NewCmdCreate(tf, ioStreams) cmd.Flags().Set("edit", "true") case "edit-last-applied": cmd = apply.NewCmdApplyEditLastApplied(tf, ioStreams) default: t.Fatalf("%s: unexpected mode %s", name, testcase.Mode) } if len(testcase.Filename) > 0 { cmd.Flags().Set("filename", filepath.Join(testcaseDir, testcase.Filename)) } if len(testcase.Output) > 0 { cmd.Flags().Set("output", testcase.Output) } if len(testcase.OutputPatch) > 0 { cmd.Flags().Set("output-patch", testcase.OutputPatch) } if len(testcase.SaveConfig) > 0 { cmd.Flags().Set("save-config", testcase.SaveConfig) } if len(testcase.Subresource) > 0 { cmd.Flags().Set("subresource", testcase.Subresource) } cmdutil.BehaviorOnFatal(func(str string, code int) { errBuf.WriteString(str) if testcase.ExpectedExitCode != code { t.Errorf("%s: expected exit code %d, got %d: %s", name, testcase.ExpectedExitCode, code, str) } }) cmd.Run(cmd, testcase.Args) stdout := buf.String() stderr := errBuf.String() for _, s := range testcase.ExpectedStdout { if !strings.Contains(stdout, s) { t.Errorf("%s: expected to see '%s' in stdout\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr) } } for _, s := range testcase.ExpectedStderr { if !strings.Contains(stderr, s) { t.Errorf("%s: expected to see '%s' in stderr\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr) } } if i < len(testcase.Steps) { t.Errorf("%s: saw %d steps, testcase included %d additional steps that were not exercised", name, i, len(testcase.Steps)-i) } }) } } func tryIndent(data []byte) []byte { indented := &bytes.Buffer{} if err := json.Indent(indented, data, "", "\t"); err == nil { return indented.Bytes() } return data } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/000077500000000000000000000000001504711711200273455ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/README000066400000000000000000000026611504711711200302320ustar00rootroot00000000000000This folder contains test cases for interactive edit, and helpers for recording new test cases To record a new test: 1. Start a local cluster running unsecured on http://localhost:8080 (e.g. hack/local-up-cluster.sh) 2. Set up any pre-existing resources you want to be available on that server (namespaces, resources to edit, etc) 3. Run ./pkg/kubectl/cmd/testdata/edit/record_testcase.sh my-testcase 4. Run the desired `kubectl edit ...` command, and interact with the editor as desired until it completes. * You can do things that cause errors to appear in the editor (change immutable fields, fail validation, etc) * You can perform edit flows that invoke the editor multiple times * You can make out-of-band changes to the server resources that cause conflict errors to be returned * The API requests/responses and editor inputs/outputs are captured in your testcase folder 5. Type exit. 6. Inspect the captured requests/responses and inputs/outputs for sanity 7. Modify the generated test.yaml file: * Set a description of what the test is doing * Enter the args (if any) you invoked edit with * Enter the filename (if any) you invoked edit with * Enter the output format (if any) you invoked edit with * Optionally specify substrings to look for in the stdout or stderr of the edit command 8. Add your new testcase name to the list of testcases in edit_test.go 9. Run `go test ./pkg/kubectl/cmd -run TestEdit -v` to run edit tests kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/record.go000066400000000000000000000113141504711711200311520ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "strings" yaml "sigs.k8s.io/yaml/goyaml.v2" ) type EditTestCase struct { Description string `yaml:"description"` // create or edit Mode string `yaml:"mode"` Args []string `yaml:"args"` Filename string `yaml:"filename"` Output string `yaml:"outputFormat"` Namespace string `yaml:"namespace"` ExpectedStdout []string `yaml:"expectedStdout"` ExpectedStderr []string `yaml:"expectedStderr"` ExpectedExitCode int `yaml:"expectedExitCode"` Steps []EditStep `yaml:"steps"` } type EditStep struct { // edit or request StepType string `yaml:"type"` // only applies to request RequestMethod string `yaml:"expectedMethod,omitempty"` RequestPath string `yaml:"expectedPath,omitempty"` RequestContentType string `yaml:"expectedContentType,omitempty"` Input string `yaml:"expectedInput"` // only applies to request ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"` Output string `yaml:"resultingOutput"` } func main() { tc := &EditTestCase{ Description: "add a testcase description", Mode: "edit", Args: []string{"set", "args"}, ExpectedStdout: []string{"expected stdout substring"}, ExpectedStderr: []string{"expected stderr substring"}, } var currentStep *EditStep fmt.Println(http.ListenAndServe(":8081", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { // Record non-discovery things record := false switch segments := strings.Split(strings.Trim(req.URL.Path, "/"), "/"); segments[0] { case "api": // api, version record = len(segments) > 2 case "apis": // apis, group, version record = len(segments) > 3 case "callback": record = true } body, err := io.ReadAll(req.Body) checkErr(err) switch m, p := req.Method, req.URL.Path; { case m == "POST" && p == "/callback/in": if currentStep != nil { panic("cannot post input with step already in progress") } filename := fmt.Sprintf("%d.original", len(tc.Steps)) checkErr(os.WriteFile(filename, body, os.FileMode(0755))) currentStep = &EditStep{StepType: "edit", Input: filename} case m == "POST" && p == "/callback/out": if currentStep == nil || currentStep.StepType != "edit" { panic("cannot post output without posting input first") } filename := fmt.Sprintf("%d.edited", len(tc.Steps)) checkErr(os.WriteFile(filename, body, os.FileMode(0755))) currentStep.Output = filename tc.Steps = append(tc.Steps, *currentStep) currentStep = nil default: if currentStep != nil { panic("cannot make request with step already in progress") } urlCopy := *req.URL urlCopy.Host = "localhost:8080" urlCopy.Scheme = "http" proxiedReq, err := http.NewRequest(req.Method, urlCopy.String(), bytes.NewReader(body)) checkErr(err) proxiedReq.Header = req.Header resp, err := http.DefaultClient.Do(proxiedReq) checkErr(err) defer resp.Body.Close() bodyOut, err := io.ReadAll(resp.Body) checkErr(err) for k, vs := range resp.Header { for _, v := range vs { w.Header().Add(k, v) } } w.WriteHeader(resp.StatusCode) w.Write(bodyOut) if record { infile := fmt.Sprintf("%d.request", len(tc.Steps)) outfile := fmt.Sprintf("%d.response", len(tc.Steps)) checkErr(os.WriteFile(infile, tryIndent(body), os.FileMode(0755))) checkErr(os.WriteFile(outfile, tryIndent(bodyOut), os.FileMode(0755))) tc.Steps = append(tc.Steps, EditStep{ StepType: "request", Input: infile, Output: outfile, RequestContentType: req.Header.Get("Content-Type"), RequestMethod: req.Method, RequestPath: req.URL.Path, ResponseStatusCode: resp.StatusCode, }) } } tcData, err := yaml.Marshal(tc) checkErr(err) checkErr(os.WriteFile("test.yaml", tcData, os.FileMode(0755))) }))) } func checkErr(err error) { if err != nil { panic(err) } } func tryIndent(data []byte) []byte { indented := &bytes.Buffer{} if err := json.Indent(indented, data, "", "\t"); err == nil { return indented.Bytes() } return data } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/record_editor.sh000077500000000000000000000016521504711711200325340ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2017 The Kubernetes Authors. # # 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. set -o errexit set -o nounset set -o pipefail # send the original content to the server curl -s -k -XPOST "http://localhost:8081/callback/in" --data-binary "@${1}" # allow the user to edit the file vi "${1}" # send the resulting content to the server curl -s -k -XPOST "http://localhost:8081/callback/out" --data-binary "@${1}" kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/record_testcase.sh000077500000000000000000000034071504711711200330610ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2017 The Kubernetes Authors. # # 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. set -o errexit set -o nounset set -o pipefail if [[ -z "${1-}" ]]; then echo "Usage: record_testcase.sh testcase-name" exit 1 fi # Clean up the test server function cleanup { if [[ -n "${pid-}" ]]; then echo "Stopping recording server (${pid})" # kill the process `go run` launched pkill -P "${pid}" # kill the `go run` process itself kill -9 "${pid}" fi } testcase="${1}" test_root="$(dirname "${BASH_SOURCE[0]}")" testcase_dir="${test_root}/testcase-${testcase}" mkdir -p "${testcase_dir}" pushd "${testcase_dir}" export EDITOR="../record_editor.sh" go run "../record.go" & pid=$! trap cleanup EXIT echo "Started recording server (${pid})" # Make a kubeconfig that makes kubectl talk to our test server edit_kubeconfig="${TMP:-/tmp}/edit_test.kubeconfig" echo "apiVersion: v1 clusters: - cluster: server: http://localhost:8081 name: test contexts: - context: cluster: test user: test name: test current-context: test kind: Config users: [] " > "${edit_kubeconfig}" export KUBECONFIG="${edit_kubeconfig}" echo "Starting subshell. Type exit when finished." bash popd kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/test_editor.sh000077500000000000000000000020421504711711200322270ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2017 The Kubernetes Authors. # # 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. set -o errexit set -o nounset set -o pipefail # Send the file content to the server if command -v curl &>/dev/null; then curl -s -k -XPOST --data-binary "@${1}" -o "${1}.result" "${KUBE_EDITOR_CALLBACK}" elif command -v wget &>/dev/null; then wget --post-file="${1}" -O "${1}.result" "${KUBE_EDITOR_CALLBACK}" else echo "curl and wget are unavailable" >&2 exit 1 fi # Use the response as the edited version mv "${1}.result" "${1}" testcase-apply-edit-last-applied-list-fail/000077500000000000000000000000001504711711200374065ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200411450ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail0.response000066400000000000000000000014651504711711200413330ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/configmaps/cm1", "uid": "cc08a131-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3518", "creationTimestamp": "2017-05-20T15:20:03Z", "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" } }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value" } }1.request000066400000000000000000000000001504711711200411460ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail1.response000066400000000000000000000023051504711711200413260ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "d8b96f0b-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3525", "creationTimestamp": "2017-05-20T15:20:24Z", "labels": { "app": "svc1", "new-label": "foo" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "172.30.32.183", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }2.edited000066400000000000000000000013601504711711200407270ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value new-data3: newivalue kind: ConfigMap metadata: annotations: {} name: cm1 namespace: myproject - kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo new-label2: foo2 name: svc1 namespace: myproject spec: ports: - name: "80" port: 82 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 2.original000066400000000000000000000013001504711711200412670ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value kind: ConfigMap metadata: annotations: {} name: cm1 namespace: myproject - kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo name: svc1 namespace: myproject spec: ports: - name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 3.edited000066400000000000000000000015631504711711200407350ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # # The edited file had a syntax error: error converting YAML to JSON: yaml: line 12: could not find expected ':' # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value new-data3: newivalue kind: ConfigMap metadata: annotations: {} name: cm1 namespace: myproject - kind: Service apiVersion: v1 metadata: annotations: {} labels: app: svc1 new-label: foo new-label2: foo2 name: svc1 namespace: myproject spec: ports: - name: "80" port: 82 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 3.original000066400000000000000000000016411504711711200413000ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # # The edited file had a syntax error: unable to get type info from the object "*unstructured.Unstructured": Object 'apiVersion' is missing in 'object has no apiVersion field' # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value new-data3: newivalue kind: ConfigMap metadata: annotations: {} name: cm1 namespace: myproject - kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo new-label2: foo2 name: svc1 namespace: myproject spec: ports: - name: "80" port: 82 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 4.request000066400000000000000000000006101504711711200411600ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\",\"new-data3\":\"newivalue\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" } } }4.response000066400000000000000000000015211504711711200413300ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/configmaps/cm1", "uid": "cc08a131-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3554", "creationTimestamp": "2017-05-20T15:20:03Z", "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\",\"new-data3\":\"newivalue\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" } }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value" } }5.request000066400000000000000000000007741504711711200411740ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label2\":\"foo2\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":82,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } } }5.response000066400000000000000000000023631504711711200413360ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "d8b96f0b-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3555", "creationTimestamp": "2017-05-20T15:20:24Z", "labels": { "app": "svc1", "new-label": "foo" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label2\":\"foo2\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":82,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "172.30.32.183", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }test.yaml000066400000000000000000000022671504711711200412600ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-faildescription: if the user omits an API version, edit will fail mode: edit-last-applied args: - configmaps/cm1 - service/svc1 namespace: "myproject" expectedStdout: - configmap/cm1 edited - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/myproject/configmaps/cm1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedInput: 1.request resultingStatusCode: 200 resultingOutput: 1.response - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: edit expectedInput: 3.original resultingOutput: 3.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/myproject/configmaps/cm1 expectedContentType: application/merge-patch+json expectedInput: 4.request resultingStatusCode: 200 resultingOutput: 4.response - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedContentType: application/merge-patch+json expectedInput: 5.request resultingStatusCode: 200 resultingOutput: 5.response testcase-apply-edit-last-applied-list/000077500000000000000000000000001504711711200364755ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200402340ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list0.response000066400000000000000000000014651504711711200404220ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/configmaps/cm1", "uid": "cc08a131-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3518", "creationTimestamp": "2017-05-20T15:20:03Z", "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" } }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value" } }1.request000066400000000000000000000000001504711711200402350ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list1.response000066400000000000000000000023051504711711200404150ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "d8b96f0b-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3525", "creationTimestamp": "2017-05-20T15:20:24Z", "labels": { "app": "svc1", "new-label": "foo" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "172.30.32.183", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }2.edited000066400000000000000000000014011504711711200400120ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value new-data3: newivalue kind: ConfigMap metadata: annotations: {} name: cm1 namespace: myproject - kind: Service apiVersion: v1 metadata: annotations: {} labels: app: svc1 new-label: foo new-label2: foo2 name: svc1 namespace: myproject spec: ports: - name: "80" port: 82 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 2.original000066400000000000000000000013001504711711200403560ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value kind: ConfigMap metadata: annotations: {} name: cm1 namespace: myproject - kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo name: svc1 namespace: myproject spec: ports: - name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 3.request000066400000000000000000000006101504711711200402460ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\",\"new-data3\":\"newivalue\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" } } }3.response000066400000000000000000000015211504711711200404160ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/configmaps/cm1", "uid": "cc08a131-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3554", "creationTimestamp": "2017-05-20T15:20:03Z", "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"baz\":\"qux\",\"foo\":\"changed-value\",\"new-data\":\"new-value\",\"new-data2\":\"new-value\",\"new-data3\":\"newivalue\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{},\"name\":\"cm1\",\"namespace\":\"myproject\"}}\n" } }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value" } }4.request000066400000000000000000000007411504711711200402540ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label2\":\"foo2\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":82,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } } }4.response000066400000000000000000000023351504711711200404230ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "d8b96f0b-3d6f-11e7-8ef0-c85b76034b7b", "resourceVersion": "3555", "creationTimestamp": "2017-05-20T15:20:24Z", "labels": { "app": "svc1", "new-label": "foo" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label2\":\"foo2\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":82,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "172.30.32.183", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }test.yaml000066400000000000000000000021341504711711200403400ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-listdescription: add a testcase description mode: edit-last-applied args: - configmaps/cm1 - service/svc1 namespace: "myproject" expectedStdout: - configmap/cm1 edited - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/myproject/configmaps/cm1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedInput: 1.request resultingStatusCode: 200 resultingOutput: 1.response - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/myproject/configmaps/cm1 expectedContentType: application/merge-patch+json expectedInput: 3.request resultingStatusCode: 200 resultingOutput: 3.response - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedContentType: application/merge-patch+json expectedInput: 4.request resultingStatusCode: 200 resultingOutput: 4.response testcase-apply-edit-last-applied-syntax-error/000077500000000000000000000000001504711711200401775ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200417360ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error0.response000066400000000000000000000023061504711711200421170ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "1e16d988-3d72-11e7-8ef0-c85b76034b7b", "resourceVersion": "3731", "creationTimestamp": "2017-05-20T15:36:39Z", "labels": { "app": "svc1", "new-label": "foo" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "172.30.105.209", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }1.edited000066400000000000000000000006451504711711200415240ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo name: svc1 namespace: myproject spec ports: name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: { 1.original000066400000000000000000000006471504711711200420740ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo name: svc1 namespace: myproject spec: ports: - name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.edited000066400000000000000000000010561504711711200415220ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # # The edited file had a syntax error: error converting YAML to JSON: yaml: line 13: could not find expected ':' # kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo new-label1: foo1 name: svc1 namespace: myproject spec: ports: - name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.original000066400000000000000000000010621504711711200420650ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # # The edited file had a syntax error: error parsing edited-file: error converting YAML to JSON: yaml: line 13: could not find expected ':' # kind: Service metadata: annotations: {} labels: app: svc1 new-label: foo name: svc1 namespace: myproject spec ports: name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: { 3.request000066400000000000000000000007461504711711200417620ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label1\":\"foo1\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } } }3.response000066400000000000000000000023361504711711200421250ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "1e16d988-3d72-11e7-8ef0-c85b76034b7b", "resourceVersion": "3857", "creationTimestamp": "2017-05-20T15:36:39Z", "labels": { "app": "svc1", "new-label": "foo" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"svc1\",\"new-label\":\"foo\",\"new-label1\":\"foo1\"},\"name\":\"svc1\",\"namespace\":\"myproject\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":81}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "172.30.105.209", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }test.yaml000066400000000000000000000013541504711711200420450ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-errordescription: edit with a syntax error, then re-edit and save mode: edit-last-applied args: - service/svc1 namespace: myproject expectedStdout: - "service/svc1 edited" expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedContentType: application/merge-patch+json expectedInput: 3.request resultingStatusCode: 200 resultingOutput: 3.response testcase-apply-edit-last-applied/000077500000000000000000000000001504711711200355245ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200372630ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied0.response000066400000000000000000000026171504711711200374510ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "bc66b442-3d6a-11e7-8ef0-c85b76034b7b", "resourceVersion": "3036", "creationTimestamp": "2017-05-20T14:43:49Z", "labels": { "app": "svc1", "new-label": "new-value" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-01T21:14:09Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"myproject\",\"resourceVersion\":\"20820\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 80 } ], "selector": { "app": "svc1" }, "clusterIP": "172.30.136.24", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }1.edited000066400000000000000000000010351504711711200370430ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # apiVersion: v1 kind: Service metadata: annotations: {} creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 new-label: new-value name: svc1 namespace: myproject resourceVersion: "20820" spec: ports: - name: "80" port: 81 protocol: TCP targetPort: 92 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 1.original000066400000000000000000000010351504711711200374110ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # apiVersion: v1 kind: Service metadata: annotations: {} creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 new-label: new-value name: svc1 namespace: myproject resourceVersion: "20820" spec: ports: - name: "80" port: 81 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.request000066400000000000000000000011271504711711200373000ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-01T21:14:09Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"myproject\",\"resourceVersion\":\"20820\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":92}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } } }2.response000066400000000000000000000026171504711711200374530ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "myproject", "selfLink": "/api/v1/namespaces/myproject/services/svc1", "uid": "bc66b442-3d6a-11e7-8ef0-c85b76034b7b", "resourceVersion": "3093", "creationTimestamp": "2017-05-20T14:43:49Z", "labels": { "app": "svc1", "new-label": "new-value" }, "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-01T21:14:09Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"myproject\",\"resourceVersion\":\"20820\"},\"spec\":{\"ports\":[{\"name\":\"80\",\"port\":81,\"protocol\":\"TCP\",\"targetPort\":92}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 80 } ], "selector": { "app": "svc1" }, "clusterIP": "172.30.136.24", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } }test.yaml000066400000000000000000000012421504711711200373660ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applieddescription: add a testcase description mode: edit-last-applied args: - service - svc1 outputFormat: yaml namespace: myproject expectedStdout: - 'targetPort: 92' expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/myproject/services/svc1 expectedContentType: application/merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response testcase-create-list-error/000077500000000000000000000000001504711711200344425ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.edited000066400000000000000000000012531504711711200357620ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:44:47Z" labels: app: svc1modified name: svc1 namespace: edit-test resourceVersion: "2942" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 4149f70e-e9dc-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.118 ports: - name: "81" port: 82 protocol: TCP targetPort: 81 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 0.original000066400000000000000000000012431504711711200363270ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:44:47Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "2942" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 4149f70e-e9dc-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.118 ports: - name: "81" port: 81 protocol: TCP targetPort: 81 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 1.request000066400000000000000000000011111504711711200362060ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error{ "apiVersion": "v1", "kind": "Service", "metadata": { "creationTimestamp": "2017-02-03T06:44:47Z", "labels": { "app": "svc1modified" }, "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "4149f70e-e9dc-11e6-8c3b-acbc32c1ca87" }, "spec": { "clusterIP": "10.0.0.118", "ports": [ { "name": "81", "port": 82, "protocol": "TCP", "targetPort": 81 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } 1.response000066400000000000000000000011461504711711200363640ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "c07152b8-e9dc-11e6-8c3b-acbc32c1ca87", "resourceVersion": "3171", "creationTimestamp": "2017-02-03T06:48:21Z", "labels": { "app": "svc1modified" } }, "spec": { "ports": [ { "name": "81", "protocol": "TCP", "port": 82, "targetPort": 81 } ], "selector": { "app": "svc1" }, "clusterIP": "10.0.0.118", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } 2.edited000066400000000000000000000012551504711711200357660ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:44:43Z" labels: app: svc2modified name: svc2 namespace: edit-test resourceVersion: "2936" selfLink: /api/v1/namespaces/edit-test/services/svc2 uid: 3e9b10db-e9dc-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.182.1 ports: - name: "80" port: 80 protocol: VHF targetPort: 80 selector: app: svc2 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.original000066400000000000000000000012431504711711200363310ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:44:43Z" labels: app: svc2 name: svc2 namespace: edit-test resourceVersion: "2936" selfLink: /api/v1/namespaces/edit-test/services/svc2 uid: 3e9b10db-e9dc-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.182 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc2 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 3.request000066400000000000000000000011131504711711200362120ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error{ "apiVersion": "v1", "kind": "Service", "metadata": { "creationTimestamp": "2017-02-03T06:44:43Z", "labels": { "app": "svc2modified" }, "name": "svc2", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc2", "uid": "3e9b10db-e9dc-11e6-8c3b-acbc32c1ca87" }, "spec": { "clusterIP": "10.0.0.182.1", "ports": [ { "name": "80", "port": 80, "protocol": "VHF", "targetPort": 80 } ], "selector": { "app": "svc2" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } 3.response000066400000000000000000000013661504711711200363720ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-error{ "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "Service \"svc2\" is invalid: [spec.ports[0].protocol: Unsupported value: \"VHF\": supported values: TCP, UDP, SCTP, spec.clusterIP: Invalid value: \"10.0.0.182.1\": must be empty, 'None', or a valid IP address]", "reason": "Invalid", "details": { "name": "svc2", "kind": "Service", "causes": [ { "reason": "FieldValueNotSupported", "message": "Unsupported value: \"VHF\": supported values: TCP, UDP, SCTP", "field": "spec.ports[0].protocol" }, { "reason": "FieldValueInvalid", "message": "Invalid value: \"10.0.0.182.1\": must be empty, 'None', or a valid IP address", "field": "spec.clusterIP" } ] }, "code": 422 } svc.yaml000066400000000000000000000021251504711711200361210ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-errorapiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:44:47Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "2942" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 4149f70e-e9dc-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.118 ports: - name: "81" port: 81 protocol: TCP targetPort: 81 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:44:43Z" labels: app: svc2 name: svc2 namespace: edit-test resourceVersion: "2936" selfLink: /api/v1/namespaces/edit-test/services/svc2 uid: 3e9b10db-e9dc-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.182 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc2 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} resourceVersion: "" selfLink: "" test.yaml000066400000000000000000000014061504711711200363060ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list-errordescription: create list with errors mode: create filename: "svc.yaml" namespace: "edit-test" expectedStdout: - "service/svc1 created" expectedStderr: - "\"svc2\" is invalid" expectedExitCode: 1 steps: - type: edit expectedInput: 0.original resultingOutput: 0.edited - type: request expectedMethod: POST expectedPath: /api/v1/namespaces/edit-test/services expectedContentType: application/json expectedInput: 1.request resultingStatusCode: 201 resultingOutput: 1.response - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: POST expectedPath: /api/v1/namespaces/edit-test/services expectedContentType: application/json expectedInput: 3.request resultingStatusCode: 422 resultingOutput: 3.response kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list/000077500000000000000000000000001504711711200333725ustar00rootroot000000000000000.edited000066400000000000000000000007401504711711200346330ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test spec: ports: - name: "81" port: 82 protocol: TCP targetPort: 81 selector: app: svc1 sessionAffinity: None type: ClusterIP 0.original000066400000000000000000000007071504711711200352040ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: labels: app: svc1 name: svc1 namespace: edit-test spec: ports: - name: "81" port: 81 protocol: TCP targetPort: 81 selector: app: svc1 sessionAffinity: None type: ClusterIP 1.request000066400000000000000000000005771504711711200350760ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list{ "apiVersion": "v1", "kind": "Service", "metadata": { "labels": { "app": "svc1", "new-label": "new-value" }, "name": "svc1", "namespace": "edit-test" }, "spec": { "ports": [ { "name": "81", "port": 82, "protocol": "TCP", "targetPort": 81 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" } } 1.response000066400000000000000000000011721504711711200352340ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "208b27ed-ea5b-11e6-9b42-acbc32c1ca87", "resourceVersion": "1437", "creationTimestamp": "2017-02-03T21:52:59Z", "labels": { "app": "svc1", "new-label": "new-value" } }, "spec": { "ports": [ { "name": "81", "protocol": "TCP", "port": 82, "targetPort": 81 } ], "selector": { "app": "svc1" }, "clusterIP": "10.0.0.15", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } 2.edited000066400000000000000000000007401504711711200346350ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: labels: app: svc2 name: svc2 namespace: edit-test spec: ports: - name: "80" port: 80 protocol: TCP targetPort: 81 selector: app: svc2 new-label: new-value sessionAffinity: None type: ClusterIP 2.original000066400000000000000000000007071504711711200352060ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: labels: app: svc2 name: svc2 namespace: edit-test spec: ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc2 sessionAffinity: None type: ClusterIP 3.request000066400000000000000000000005771504711711200351000ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list{ "apiVersion": "v1", "kind": "Service", "metadata": { "labels": { "app": "svc2" }, "name": "svc2", "namespace": "edit-test" }, "spec": { "ports": [ { "name": "80", "port": 80, "protocol": "TCP", "targetPort": 81 } ], "selector": { "app": "svc2", "new-label": "new-value" }, "sessionAffinity": "None", "type": "ClusterIP" } } 3.response000066400000000000000000000011721504711711200352360ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc2", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc2", "uid": "31a1b8ae-ea5b-11e6-9b42-acbc32c1ca87", "resourceVersion": "1470", "creationTimestamp": "2017-02-03T21:53:27Z", "labels": { "app": "svc2" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 80, "targetPort": 81 } ], "selector": { "app": "svc2", "new-label": "new-value" }, "clusterIP": "10.0.0.55", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } svc.yaml000066400000000000000000000011501504711711200347670ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-listapiVersion: v1 items: - apiVersion: v1 kind: Service metadata: labels: app: svc1 name: svc1 spec: ports: - name: "81" port: 81 protocol: TCP targetPort: 81 selector: app: svc1 sessionAffinity: None type: ClusterIP - apiVersion: v1 kind: Service metadata: labels: app: svc2 name: svc2 namespace: edit-test spec: ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc2 sessionAffinity: None type: ClusterIP kind: List metadata: {} resourceVersion: "" selfLink: "" test.yaml000066400000000000000000000013731504711711200351620ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-listdescription: edit while creating from a list mode: create filename: "svc.yaml" namespace: "edit-test" expectedStdout: - service/svc1 created - service/svc2 created expectedExitCode: 0 steps: - type: edit expectedInput: 0.original resultingOutput: 0.edited - type: request expectedMethod: POST expectedPath: /api/v1/namespaces/edit-test/services expectedContentType: application/json expectedInput: 1.request resultingStatusCode: 201 resultingOutput: 1.response - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: POST expectedPath: /api/v1/namespaces/edit-test/services expectedContentType: application/json expectedInput: 3.request resultingStatusCode: 201 resultingOutput: 3.response testcase-edit-error-reedit/000077500000000000000000000000001504711711200344255ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200361640ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit0.response000066400000000000000000000011741504711711200363470ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87", "resourceVersion": "20820", "creationTimestamp": "2017-02-01T21:14:09Z", "labels": { "app": "svc1", "new-label": "new-value" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 80 } ], "selector": { "app": "svc1" }, "clusterIP": "10.0.0.146", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } 1.edited000066400000000000000000000012771504711711200357540ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "20820" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87 spec: clusterIP: 10.0.0.146.1 ports: - name: "80" port: 81 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 1.original000066400000000000000000000012751504711711200363200ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "20820" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87 spec: clusterIP: 10.0.0.146 ports: - name: "80" port: 81 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.request000066400000000000000000000000571504711711200362020ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit{ "spec": { "clusterIP": "10.0.0.146.1" } }2.response000066400000000000000000000013171504711711200363500ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit{ "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "Service \"svc1\" is invalid: [spec.clusterIP: Invalid value: \"10.0.0.146.1\": field is immutable, spec.clusterIP: Invalid value: \"10.0.0.146.1\": must be empty, 'None', or a valid IP address]", "reason": "Invalid", "details": { "name": "svc1", "kind": "Service", "causes": [ { "reason": "FieldValueInvalid", "message": "Invalid value: \"10.0.0.146.1\": field is immutable", "field": "spec.clusterIP" }, { "reason": "FieldValueInvalid", "message": "Invalid value: \"10.0.0.146.1\": must be empty, 'None', or a valid IP address", "field": "spec.clusterIP" } ] }, "code": 422 } 3.edited000066400000000000000000000016061504711711200357520ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "svc1" was not valid: # * spec.clusterIP: Invalid value: "10.0.0.146.1": field is immutable # * spec.clusterIP: Invalid value: "10.0.0.146.1": must be empty, 'None', or a valid IP address # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "20820" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87 spec: clusterIP: 10.0.0.146 ports: - name: "80" port: 82 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 3.original000066400000000000000000000016101504711711200363130ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "svc1" was not valid: # * spec.clusterIP: Invalid value: "10.0.0.146.1": field is immutable # * spec.clusterIP: Invalid value: "10.0.0.146.1": must be empty, 'None', or a valid IP address # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "20820" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87 spec: clusterIP: 10.0.0.146.1 ports: - name: "80" port: 81 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 4.request000066400000000000000000000003501504711711200362000ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit{ "spec": { "$setElementOrder/ports": [ { "port": 82 } ], "ports": [ { "name": "80", "port": 82, "protocol": "TCP", "targetPort": 80 }, { "$patch": "delete", "port": 81 } ] } }4.response000066400000000000000000000011741504711711200363530ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87", "resourceVersion": "21361", "creationTimestamp": "2017-02-01T21:14:09Z", "labels": { "app": "svc1", "new-label": "new-value" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 82, "targetPort": 80 } ], "selector": { "app": "svc1" }, "clusterIP": "10.0.0.146", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000020011504711711200362610ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reeditdescription: add a testcase description mode: edit args: - service - svc1 namespace: edit-test expectedStdout: - service/svc1 edited expectedStderr: - "error: services \"svc1\" is invalid" expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 422 resultingOutput: 2.response - type: edit expectedInput: 3.original resultingOutput: 3.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 4.request resultingStatusCode: 200 resultingOutput: 4.response testcase-edit-from-empty/000077500000000000000000000000001504711711200341215ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200356600ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-from-empty0.response000066400000000000000000000003021504711711200360330ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-from-empty{ "kind": "ConfigMapList", "apiVersion": "v1", "metadata": { "selfLink": "/api/v1/namespaces/edit-test/configmaps", "resourceVersion": "252" }, "items": [] } test.yaml000066400000000000000000000005341504711711200357660ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-from-emptydescription: add a testcase description mode: edit args: - configmap namespace: "edit-test" expectedStderr: - edit cancelled, no objects found expectedExitCode: 1 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/configmaps expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response testcase-edit-output-patch/000077500000000000000000000000001504711711200344575ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200362160ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch0.response000066400000000000000000000020661504711711200364020ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch{ "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "creationTimestamp": "2017-02-27T19:40:53Z", "labels": { "app": "svc1" }, "name": "svc1", "namespace": "edit-test", "resourceVersion": "670", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "a6c11186-fd24-11e6-b53c-480fcf4a5275" }, "spec": { "clusterIP": "10.0.0.204", "ports": [ { "name": "80", "port": 80, "protocol": "TCP", "targetPort": 80 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } 1.edited000066400000000000000000000020171504711711200357770ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} creationTimestamp: "2017-02-27T19:40:53Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "670" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: a6c11186-fd24-11e6-b53c-480fcf4a5275 spec: clusterIP: 10.0.0.204 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 1.original000066400000000000000000000017661504711711200363570ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} creationTimestamp: "2017-02-27T19:40:53Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "670" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: a6c11186-fd24-11e6-b53c-480fcf4a5275 spec: clusterIP: 10.0.0.204 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.request000066400000000000000000000014031504711711200362300ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-27T19:40:53Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"edit-test\",\"resourceVersion\":\"670\",\"selfLink\":\"/api/v1/namespaces/edit-test/services/svc1\",\"uid\":\"a6c11186-fd24-11e6-b53c-480fcf4a5275\"},\"spec\":{\"clusterIP\":\"10.0.0.204\",\"ports\":[{\"name\":\"80\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "labels": { "new-label": "new-value" } } }2.response000066400000000000000000000025341504711711200364040ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch{ "kind": "Service", "apiVersion": "v1", "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-27T19:40:53Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"edit-test\",\"resourceVersion\":\"670\",\"selfLink\":\"/api/v1/namespaces/edit-test/services/svc1\",\"uid\":\"a6c11186-fd24-11e6-b53c-480fcf4a5275\"},\"spec\":{\"clusterIP\":\"10.0.0.204\",\"ports\":[{\"name\":\"80\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "a6c11186-fd24-11e6-b53c-480fcf4a5275", "resourceVersion":"1045", "creationTimestamp":"2017-02-27T19:40:53Z", "labels": { "app": "svc1", "new-label": "new-value" } }, "spec": { "clusterIP": "10.0.0.204", "ports": [ { "name": "80", "port": 80, "protocol": "TCP", "targetPort": 80 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000032121504711711200363200ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch# kubectl create namespace edit-test # kubectl create service clusterip svc1 --tcp 80 --namespace=edit-test --save-config # kubectl edit service svc1 --namespace=edit-test --save-config=true --output-patch=true description: edit with flag --output-patch=true should output the patch mode: edit args: - service - svc1 saveConfig: "true" outputPatch: "true" namespace: edit-test expectedStdout: - 'Patch: {"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-27T19:40:53Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"edit-test\",\"resourceVersion\":\"670\",\"selfLink\":\"/api/v1/namespaces/edit-test/services/svc1\",\"uid\":\"a6c11186-fd24-11e6-b53c-480fcf4a5275\"},\"spec\":{\"clusterIP\":\"10.0.0.204\",\"ports\":[{\"name\":\"80\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"},"labels":{"new-label":"new-value"}}}' - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response testcase-edit-subresource-status/000077500000000000000000000000001504711711200357045ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200374430ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status0.response000066400000000000000000000051741504711711200376320ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { "deployment.kubernetes.io/revision": "1" }, "creationTimestamp": "2021-06-23T17:01:10Z", "generation": 5, "labels": { "app": "nginx" }, "name": "nginx", "namespace": "edit-test", "resourceVersion": "121107", "uid": "a598ee47-9635-482b-bacb-16c9e3ade05c" }, "spec": { "progressDeadlineSeconds": 600, "replicas": 3, "revisionHistoryLimit": 10, "selector": { "matchLabels": { "app": "nginx" } }, "strategy": { "rollingUpdate": { "maxSurge": "25%", "maxUnavailable": "25%" }, "type": "RollingUpdate" }, "template": { "metadata": { "creationTimestamp": null, "labels": { "app": "nginx" } }, "spec": { "containers": [ { "image": "gcr.io/kakaraparthy-devel/nginx:latest", "imagePullPolicy": "Always", "name": "nginx", "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File" } ], "dnsPolicy": "ClusterFirst", "restartPolicy": "Always", "schedulerName": "default-scheduler", "securityContext": {}, "terminationGracePeriodSeconds": 30 } } }, "status": { "availableReplicas": 3, "conditions": [ { "lastTransitionTime": "2021-06-23T17:01:10Z", "lastUpdateTime": "2021-06-23T17:01:18Z", "message": "ReplicaSet \"nginx-6f5fdbd667\" has successfully progressed.", "reason": "NewReplicaSetAvailable", "status": "True", "type": "Progressing" }, { "lastTransitionTime": "2021-06-23T17:59:01Z", "lastUpdateTime": "2021-06-23T17:59:01Z", "message": "Deployment has minimum availability.", "reason": "MinimumReplicasAvailable", "status": "True", "type": "Available" } ], "observedGeneration": 5, "readyReplicas": 3, "replicas": 3, "updatedReplicas": 3 } }1.edited000066400000000000000000000034211504711711200372240ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: apps/v1 kind: Deployment metadata: annotations: deployment.kubernetes.io/revision: "1" creationTimestamp: "2021-06-23T17:01:10Z" generation: 5 labels: app: nginx name: nginx namespace: edit-test resourceVersion: "121107" uid: a598ee47-9635-482b-bacb-16c9e3ade05c spec: progressDeadlineSeconds: 600 replicas: 3 revisionHistoryLimit: 10 selector: matchLabels: app: nginx strategy: rollingUpdate: maxSurge: 25% maxUnavailable: 25% type: RollingUpdate template: metadata: creationTimestamp: null labels: app: nginx spec: containers: - image: gcr.io/kakaraparthy-devel/nginx:latest imagePullPolicy: Always name: nginx resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 status: availableReplicas: 3 conditions: - lastTransitionTime: "2021-06-23T17:01:10Z" lastUpdateTime: "2021-06-23T17:01:18Z" message: ReplicaSet "nginx-6f5fdbd667" has successfully progressed. reason: NewReplicaSetAvailable status: "True" type: Progressing - lastTransitionTime: "2021-06-23T17:59:01Z" lastUpdateTime: "2021-06-23T17:59:01Z" message: Deployment has minimum availability. reason: MinimumReplicasAvailable status: "True" type: Available observedGeneration: 5 readyReplicas: 3 replicas: 4 updatedReplicas: 3 1.original000066400000000000000000000034211504711711200375720ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: apps/v1 kind: Deployment metadata: annotations: deployment.kubernetes.io/revision: "1" creationTimestamp: "2021-06-23T17:01:10Z" generation: 5 labels: app: nginx name: nginx namespace: edit-test resourceVersion: "121107" uid: a598ee47-9635-482b-bacb-16c9e3ade05c spec: progressDeadlineSeconds: 600 replicas: 3 revisionHistoryLimit: 10 selector: matchLabels: app: nginx strategy: rollingUpdate: maxSurge: 25% maxUnavailable: 25% type: RollingUpdate template: metadata: creationTimestamp: null labels: app: nginx spec: containers: - image: gcr.io/kakaraparthy-devel/nginx:latest imagePullPolicy: Always name: nginx resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 status: availableReplicas: 3 conditions: - lastTransitionTime: "2021-06-23T17:01:10Z" lastUpdateTime: "2021-06-23T17:01:18Z" message: ReplicaSet "nginx-6f5fdbd667" has successfully progressed. reason: NewReplicaSetAvailable status: "True" type: Progressing - lastTransitionTime: "2021-06-23T17:59:01Z" lastUpdateTime: "2021-06-23T17:59:01Z" message: Deployment has minimum availability. reason: MinimumReplicasAvailable status: "True" type: Available observedGeneration: 5 readyReplicas: 3 replicas: 3 updatedReplicas: 3 2.request000066400000000000000000000000471504711711200374600ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status{ "status": { "replicas": 4 } }2.response000066400000000000000000000051741504711711200376340ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { "deployment.kubernetes.io/revision": "1" }, "creationTimestamp": "2021-06-23T17:01:10Z", "generation": 5, "labels": { "app": "nginx" }, "name": "nginx", "namespace": "edit-test", "resourceVersion": "121107", "uid": "a598ee47-9635-482b-bacb-16c9e3ade05c" }, "spec": { "progressDeadlineSeconds": 600, "replicas": 3, "revisionHistoryLimit": 10, "selector": { "matchLabels": { "app": "nginx" } }, "strategy": { "rollingUpdate": { "maxSurge": "25%", "maxUnavailable": "25%" }, "type": "RollingUpdate" }, "template": { "metadata": { "creationTimestamp": null, "labels": { "app": "nginx" } }, "spec": { "containers": [ { "image": "gcr.io/kakaraparthy-devel/nginx:latest", "imagePullPolicy": "Always", "name": "nginx", "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File" } ], "dnsPolicy": "ClusterFirst", "restartPolicy": "Always", "schedulerName": "default-scheduler", "securityContext": {}, "terminationGracePeriodSeconds": 30 } } }, "status": { "availableReplicas": 3, "conditions": [ { "lastTransitionTime": "2021-06-23T17:01:10Z", "lastUpdateTime": "2021-06-23T17:01:18Z", "message": "ReplicaSet \"nginx-6f5fdbd667\" has successfully progressed.", "reason": "NewReplicaSetAvailable", "status": "True", "type": "Progressing" }, { "lastTransitionTime": "2021-06-23T17:59:01Z", "lastUpdateTime": "2021-06-23T17:59:01Z", "message": "Deployment has minimum availability.", "reason": "MinimumReplicasAvailable", "status": "True", "type": "Available" } ], "observedGeneration": 5, "readyReplicas": 3, "replicas": 4, "updatedReplicas": 3 } }test.yaml000066400000000000000000000014041504711711200375460ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-statusdescription: edit the status subresource mode: edit args: - deployment - nginx namespace: edit-test subresource: status expectedStdOut: - deployment.apps/nginx edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /apis/extensions/v1beta1/namespaces/edit-test/deployments/nginx/status expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /apis/apps/v1/namespaces/edit-test/deployments/nginx/status expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response testcase-immutable-name/000077500000000000000000000000001504711711200337745ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200355330ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-immutable-name0.response000066400000000000000000000007731504711711200357220ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-immutable-name{ "kind": "ConfigMapList", "apiVersion": "v1", "metadata": { "selfLink": "/api/v1/namespaces/edit-test/configmaps", "resourceVersion": "2308" }, "items": [ { "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "2071", "creationTimestamp": "2017-02-03T06:12:07Z" }, "data": { "baz": "qux", "foo": "changed-value2" } } ] } 1.edited000066400000000000000000000007661504711711200353250ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-immutable-name# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 data: baz: qux foo: changed-value2 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1-modified namespace: edit-test resourceVersion: "2071" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 1.original000066400000000000000000000007551504711711200356710ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-immutable-name# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 data: baz: qux foo: changed-value2 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "2071" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 test.yaml000066400000000000000000000006671504711711200356500ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-immutable-namedescription: try to mutate a fixed field mode: edit args: - configmap namespace: "edit-test" expectedStderr: - At least one of apiVersion, kind and name was changed expectedExitCode: 1 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/configmaps expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors/000077500000000000000000000000001504711711200334435ustar00rootroot000000000000000.request000066400000000000000000000000001504711711200351230ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors0.response000066400000000000000000000007721504711711200353110ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "ConfigMapList", "apiVersion": "v1", "metadata": { "selfLink": "/api/v1/namespaces/edit-test/configmaps", "resourceVersion": "1934" }, "items": [ { "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1903", "creationTimestamp": "2017-02-03T06:12:07Z" }, "data": { "baz": "qux", "foo": "changed-value" } } ] } 1.request000066400000000000000000000000001504711711200351240ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors1.response000066400000000000000000000013571504711711200353120ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "ServiceList", "apiVersion": "v1", "metadata": { "selfLink": "/api/v1/namespaces/edit-test/services", "resourceVersion": "1934" }, "items": [ { "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "9bec82be-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1904", "creationTimestamp": "2017-02-03T06:11:32Z", "labels": { "app": "svc1" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 82, "targetPort": 81 } ], "clusterIP": "10.0.0.248", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } ] } 10.request000066400000000000000000000000531504711711200352140ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "data": { "foo": "changed-value2" } }10.response000066400000000000000000000005401504711711200353630ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "2071", "creationTimestamp": "2017-02-03T06:12:07Z" }, "data": { "baz": "qux", "foo": "changed-value2" } } 2.edited000066400000000000000000000020271504711711200347060ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "1904" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.10 ports: - name: "80" port: 82 protocol: VHF targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} - apiVersion: v1 data: baz: qux foo: changed-value2 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1903" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 kind: List metadata: {} 2.original000066400000000000000000000020271504711711200352540ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1903" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "1904" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 82 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 3.request000066400000000000000000000002521504711711200351370ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "spec": { "$setElementOrder/ports": [ { "port": 82 } ], "clusterIP": "10.0.0.10", "ports": [ { "port": 82, "protocol": "VHF" } ] } }3.response000066400000000000000000000012741504711711200353120ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "Service \"svc1\" is invalid: [spec.clusterIP: Invalid value: \"10.0.0.10\": field is immutable, spec.ports[0].protocol: Unsupported value: \"VHF\": supported values: TCP, UDP, SCTP]", "reason": "Invalid", "details": { "name": "svc1", "kind": "Service", "causes": [ { "reason": "FieldValueInvalid", "message": "Invalid value: \"10.0.0.10\": field is immutable", "field": "spec.clusterIP" }, { "reason": "FieldValueNotSupported", "message": "Unsupported value: \"VHF\": supported values: TCP, UDP, SCTP", "field": "spec.ports[0].protocol" } ] }, "code": 422 } 4.request000066400000000000000000000000531504711711200351370ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "data": { "foo": "changed-value2" } }4.response000066400000000000000000000005401504711711200353060ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "2017", "creationTimestamp": "2017-02-03T06:12:07Z" }, "data": { "baz": "qux", "foo": "changed-value2" } } 5.edited000066400000000000000000000023561504711711200347160ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "svc1" was not valid: # * spec.clusterIP: Invalid value: "10.0.0.10": field is immutable # * spec.ports[0].protocol: Unsupported value: "VHF": supported values: TCP, UDP, SCTP # apiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 newvalue: modified name: svc1 namespace: edit-test resourceVersion: "1904" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 83 protocol: VHF targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} - apiVersion: v1 data: baz: qux foo: changed-value2 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1903" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 kind: List metadata: {} 5.original000066400000000000000000000023241504711711200352570ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "svc1" was not valid: # * spec.clusterIP: Invalid value: "10.0.0.10": field is immutable # * spec.ports[0].protocol: Unsupported value: "VHF": supported values: TCP, UDP, SCTP # apiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "1904" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.10 ports: - name: "80" port: 82 protocol: VHF targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} - apiVersion: v1 data: baz: qux foo: changed-value2 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1903" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 kind: List metadata: {} 6.request000066400000000000000000000004471504711711200351500ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "metadata": { "labels": { "newvalue": "modified" } }, "spec": { "$setElementOrder/ports": [ { "port": 83 } ], "ports": [ { "name": "80", "port": 83, "protocol": "VHF", "targetPort": 81 }, { "$patch": "delete", "port": 82 } ] } }6.response000066400000000000000000000007511504711711200353140ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "Service \"svc1\" is invalid: spec.ports[0].protocol: Unsupported value: \"VHF\": supported values: TCP, UDP, SCTP", "reason": "Invalid", "details": { "name": "svc1", "kind": "Service", "causes": [ { "reason": "FieldValueNotSupported", "message": "Unsupported value: \"VHF\": supported values: TCP, UDP, SCTP", "field": "spec.ports[0].protocol" } ] }, "code": 422 } 7.request000066400000000000000000000000531504711711200351420ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "data": { "foo": "changed-value2" } }7.response000066400000000000000000000005401504711711200353110ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "2017", "creationTimestamp": "2017-02-03T06:12:07Z" }, "data": { "baz": "qux", "foo": "changed-value2" } } 8.edited000066400000000000000000000022531504711711200347150ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "svc1" was not valid: # * spec.ports[0].protocol: Unsupported value: "VHF": supported values: TCP, UDP, SCTP # apiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 newvalue: modified name: svc1 namespace: edit-test resourceVersion: "1904" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 83 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} - apiVersion: v1 data: baz: qux foo: changed-value2 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1903" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 kind: List metadata: {} 8.original000066400000000000000000000022531504711711200352630ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "svc1" was not valid: # * spec.ports[0].protocol: Unsupported value: "VHF": supported values: TCP, UDP, SCTP # apiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 newvalue: modified name: svc1 namespace: edit-test resourceVersion: "1904" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 83 protocol: VHF targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} - apiVersion: v1 data: baz: qux foo: changed-value2 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1903" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 kind: List metadata: {} 9.request000066400000000000000000000004471504711711200351530ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "metadata": { "labels": { "newvalue": "modified" } }, "spec": { "$setElementOrder/ports": [ { "port": 83 } ], "ports": [ { "name": "80", "port": 83, "protocol": "TCP", "targetPort": 81 }, { "$patch": "delete", "port": 82 } ] } }9.response000066400000000000000000000011231504711711200353110ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "9bec82be-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "2070", "creationTimestamp": "2017-02-03T06:11:32Z", "labels": { "app": "svc1", "newvalue": "modified" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 83, "targetPort": 81 } ], "clusterIP": "10.0.0.248", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000042731504711711200352350ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errorsdescription: edit lists with errors and resubmit mode: edit args: - configmaps,services namespace: "edit-test" expectedStdout: - configmap/cm1 edited - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/configmaps expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services expectedInput: 1.request resultingStatusCode: 200 resultingOutput: 1.response - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 3.request resultingStatusCode: 422 resultingOutput: 3.response - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/configmaps/cm1 expectedContentType: application/strategic-merge-patch+json expectedInput: 4.request resultingStatusCode: 200 resultingOutput: 4.response - type: edit expectedInput: 5.original resultingOutput: 5.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 6.request resultingStatusCode: 422 resultingOutput: 6.response - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/configmaps/cm1 expectedContentType: application/strategic-merge-patch+json expectedInput: 7.request resultingStatusCode: 200 resultingOutput: 7.response - type: edit expectedInput: 8.original resultingOutput: 8.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 9.request resultingStatusCode: 200 resultingOutput: 9.response - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/configmaps/cm1 expectedContentType: application/strategic-merge-patch+json expectedInput: 10.request resultingStatusCode: 200 resultingOutput: 10.response kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record/000077500000000000000000000000001504711711200334055ustar00rootroot000000000000000.request000066400000000000000000000000001504711711200350650ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record0.response000066400000000000000000000007441504711711200352520ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1414", "creationTimestamp": "2017-02-03T06:12:07Z", "annotations":{"kubernetes.io/change-cause":"original creating command a"} }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value" } } 1.request000066400000000000000000000000001504711711200350660ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record1.response000066400000000000000000000012351504711711200352470ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "9bec82be-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1064", "creationTimestamp": "2017-02-03T06:11:32Z", "annotations":{"kubernetes.io/change-cause":"original creating command b"}, "labels": { "app": "svc1", "new-label": "foo" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "10.0.0.248", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } 2.edited000066400000000000000000000024531504711711200346530ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value new-data3: newivalue kind: ConfigMap metadata: annotations: kubernetes.io/change-cause: original creating command a creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1414" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 - apiVersion: v1 kind: Service metadata: annotations: kubernetes.io/change-cause: original creating command b creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 new-label: foo new-label2: foo2 name: svc1 namespace: edit-test resourceVersion: "1064" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 82 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 2.original000066400000000000000000000023731504711711200352220ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value kind: ConfigMap metadata: annotations: kubernetes.io/change-cause: original creating command a creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1414" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 - apiVersion: v1 kind: Service metadata: annotations: kubernetes.io/change-cause: original creating command b creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 new-label: foo name: svc1 namespace: edit-test resourceVersion: "1064" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 3.request000066400000000000000000000000541504711711200351010ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record{ "data": { "new-data3": "newivalue" } }3.response000066400000000000000000000007751504711711200352610ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1465", "creationTimestamp": "2017-02-03T06:12:07Z", "annotations":{"kubernetes.io/change-cause":"edit test cmd invocation"} }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value", "new-data3": "newivalue" } } 4.request000066400000000000000000000004451504711711200351060ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record{ "metadata": { "labels": { "new-label2": "foo2" } }, "spec": { "$setElementOrder/ports": [ { "port": 82 } ], "ports": [ { "name": "80", "port": 82, "protocol": "TCP", "targetPort": 81 }, { "$patch": "delete", "port": 81 } ] } }4.response000066400000000000000000000012631504711711200352530ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "9bec82be-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1466", "creationTimestamp": "2017-02-03T06:11:32Z", "annotations":{"kubernetes.io/change-cause":"edit test cmd invocation"}, "labels": { "app": "svc1", "new-label": "foo", "new-label2": "foo2" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 82, "targetPort": 81 } ], "clusterIP": "10.0.0.248", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000021431504711711200351710ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-recorddescription: add a testcase description mode: edit args: - configmaps/cm1 - service/svc1 namespace: "edit-test" expectedStdout: - configmap/cm1 edited - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/configmaps/cm1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedInput: 1.request resultingStatusCode: 200 resultingOutput: 1.response - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/configmaps/cm1 expectedContentType: application/strategic-merge-patch+json expectedInput: 3.request resultingStatusCode: 200 resultingOutput: 3.response - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 4.request resultingStatusCode: 200 resultingOutput: 4.response kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list/000077500000000000000000000000001504711711200321315ustar00rootroot000000000000000.request000066400000000000000000000000001504711711200336110ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list0.response000066400000000000000000000006261504711711200337750ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1414", "creationTimestamp": "2017-02-03T06:12:07Z" }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value" } } 1.request000066400000000000000000000000001504711711200336120ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list1.response000066400000000000000000000011171504711711200337720ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "9bec82be-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1064", "creationTimestamp": "2017-02-03T06:11:32Z", "labels": { "app": "svc1", "new-label": "foo" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 81 } ], "clusterIP": "10.0.0.248", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } 2.edited000066400000000000000000000022151504711711200333730ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value new-data3: newivalue kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1414" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 new-label: foo new-label2: foo2 name: svc1 namespace: edit-test resourceVersion: "1064" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 82 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 2.original000066400000000000000000000021351504711711200337420ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 data: baz: qux foo: changed-value new-data: new-value new-data2: new-value kind: ConfigMap metadata: creationTimestamp: "2017-02-03T06:12:07Z" name: cm1 namespace: edit-test resourceVersion: "1414" selfLink: /api/v1/namespaces/edit-test/configmaps/cm1 uid: b09bffab-e9d7-11e6-8c3b-acbc32c1ca87 - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-03T06:11:32Z" labels: app: svc1 new-label: foo name: svc1 namespace: edit-test resourceVersion: "1064" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 9bec82be-e9d7-11e6-8c3b-acbc32c1ca87 spec: clusterIP: 10.0.0.248 ports: - name: "80" port: 81 protocol: TCP targetPort: 81 sessionAffinity: None type: ClusterIP status: loadBalancer: {} kind: List metadata: {} 3.request000066400000000000000000000000541504711711200336250ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list{ "data": { "new-data3": "newivalue" } }3.response000066400000000000000000000006621504711711200340000ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "cm1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/configmaps/cm1", "uid": "b09bffab-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1465", "creationTimestamp": "2017-02-03T06:12:07Z" }, "data": { "baz": "qux", "foo": "changed-value", "new-data": "new-value", "new-data2": "new-value", "new-data3": "newivalue" } } 4.request000066400000000000000000000004451504711711200336320ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list{ "metadata": { "labels": { "new-label2": "foo2" } }, "spec": { "$setElementOrder/ports": [ { "port": 82 } ], "ports": [ { "name": "80", "port": 82, "protocol": "TCP", "targetPort": 81 }, { "$patch": "delete", "port": 81 } ] } }4.response000066400000000000000000000011501504711711200337720ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "9bec82be-e9d7-11e6-8c3b-acbc32c1ca87", "resourceVersion": "1466", "creationTimestamp": "2017-02-03T06:11:32Z", "labels": { "app": "svc1", "new-label": "foo", "new-label2": "foo2" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 82, "targetPort": 81 } ], "clusterIP": "10.0.0.248", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000021431504711711200337150ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-listdescription: add a testcase description mode: edit args: - configmaps/cm1 - service/svc1 namespace: "edit-test" expectedStdout: - configmap/cm1 edited - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/configmaps/cm1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedInput: 1.request resultingStatusCode: 200 resultingOutput: 1.response - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/configmaps/cm1 expectedContentType: application/strategic-merge-patch+json expectedInput: 3.request resultingStatusCode: 200 resultingOutput: 3.response - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 4.request resultingStatusCode: 200 resultingOutput: 4.response testcase-missing-service/000077500000000000000000000000001504711711200342065ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200357450ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-missing-service0.response000066400000000000000000000003411504711711200361230ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-missing-service{ "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "services \"missing\" not found", "reason": "NotFound", "details": { "name": "missing", "kind": "services" }, "code": 404 } test.yaml000066400000000000000000000005401504711711200360500ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-missing-servicedescription: add a testcase description mode: edit args: - service/missing namespace: "default" expectedStderr: - services "missing" not found expectedExitCode: 1 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/default/services/missing expectedInput: 0.request resultingStatusCode: 404 resultingOutput: 0.response kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-op/000077500000000000000000000000001504711711200322065ustar00rootroot000000000000000.request000066400000000000000000000000001504711711200336660ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-op0.response000066400000000000000000000004461504711711200340520ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-op{ "kind": "ConfigMap", "apiVersion": "v1", "metadata": { "name": "mymap", "namespace": "default", "selfLink": "/api/v1/namespaces/default/configmaps/mymap", "uid": "dbde42e9-e9d5-11e6-8c3b-acbc32c1ca87", "resourceVersion": "149", "creationTimestamp": "2017-02-03T05:59:00Z" } } 1.edited000066400000000000000000000007051504711711200334510ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-op# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T05:59:00Z" name: mymap namespace: default resourceVersion: "149" selfLink: /api/v1/namespaces/default/configmaps/mymap uid: dbde42e9-e9d5-11e6-8c3b-acbc32c1ca87 1.original000066400000000000000000000007051504711711200340170ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-op# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: ConfigMap metadata: creationTimestamp: "2017-02-03T05:59:00Z" name: mymap namespace: default resourceVersion: "149" selfLink: /api/v1/namespaces/default/configmaps/mymap uid: dbde42e9-e9d5-11e6-8c3b-acbc32c1ca87 test.yaml000066400000000000000000000006311504711711200337720ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-opdescription: no-op edit mode: edit args: - configmap/mymap namespace: "default" expectedStderr: - Edit cancelled, no changes made. expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/default/configmaps/mymap expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited testcase-not-update-annotation/000077500000000000000000000000001504711711200353275ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200370660ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation0.response000066400000000000000000000020661504711711200372520ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation{ "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "creationTimestamp": "2017-02-27T19:40:53Z", "labels": { "app": "svc1" }, "name": "svc1", "namespace": "edit-test", "resourceVersion": "670", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "a6c11186-fd24-11e6-b53c-480fcf4a5275" }, "spec": { "clusterIP": "10.0.0.204", "ports": [ { "name": "80", "port": 80, "protocol": "TCP", "targetPort": 80 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } 1.edited000066400000000000000000000020171504711711200366470ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} creationTimestamp: "2017-02-27T19:40:53Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "670" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: a6c11186-fd24-11e6-b53c-480fcf4a5275 spec: clusterIP: 10.0.0.204 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 1.original000066400000000000000000000017661504711711200372270ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} creationTimestamp: "2017-02-27T19:40:53Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "670" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: a6c11186-fd24-11e6-b53c-480fcf4a5275 spec: clusterIP: 10.0.0.204 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.request000066400000000000000000000001031504711711200370740ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation{ "metadata": { "labels": { "new-label": "new-value" } } }2.response000066400000000000000000000025001504711711200372450ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation{ "kind": "Service", "apiVersion": "v1", "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-27T19:40:53Z\",\"labels\":{\"app\":\"svc1\"},\"name\":\"svc1\",\"namespace\":\"edit-test\",\"resourceVersion\":\"670\",\"selfLink\":\"/api/v1/namespaces/edit-test/services/svc1\",\"uid\":\"a6c11186-fd24-11e6-b53c-480fcf4a5275\"},\"spec\":{\"clusterIP\":\"10.0.0.204\",\"ports\":[{\"name\":\"80\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "a6c11186-fd24-11e6-b53c-480fcf4a5275", "resourceVersion":"1045", "creationTimestamp":"2017-02-27T19:40:53Z", "labels": { "app": "svc1", "new-label": "new-value" } }, "spec": { "clusterIP": "10.0.0.204", "ports": [ { "name": "80", "port": 80, "protocol": "TCP", "targetPort": 80 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000016141504711711200371740ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation# kubectl create namespace edit-test # kubectl create service clusterip svc1 --tcp 80 --namespace=edit-test --save-config # kubectl edit service svc1 --namespace=edit-test --save-config=false description: edit with flag --save-config=false should not update the annotation mode: edit args: - service - svc1 saveConfig: "false" namespace: edit-test expectedStdout: - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response testcase-repeat-error/000077500000000000000000000000001504711711200335065ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200352450ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error0.response000066400000000000000000000011541504711711200354260ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "kubernetes", "namespace": "default", "selfLink": "/api/v1/namespaces/default/services/kubernetes", "uid": "6a8e8829-f15f-11e6-b041-acbc32c1ca87", "resourceVersion": "8", "creationTimestamp": "2017-02-12T20:11:19Z", "labels": { "component": "apiserver", "provider": "kubernetes" } }, "spec": { "ports": [ { "name": "https", "protocol": "TCP", "port": 443, "targetPort": 443 } ], "clusterIP": "10.0.0.1", "type": "ClusterIP", "sessionAffinity": "ClientIP" }, "status": { "loadBalancer": {} } } 1.edited000066400000000000000000000012711504711711200350270ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} 1.original000066400000000000000000000012671504711711200354020ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} 2.request000066400000000000000000000000551504711711200352610ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error{ "spec": { "clusterIP": "10.0.0.1.1" } }2.response000066400000000000000000000013231504711711200354260ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error{ "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "Service \"kubernetes\" is invalid: [spec.clusterIP: Invalid value: \"10.0.0.1.1\": field is immutable, spec.clusterIP: Invalid value: \"10.0.0.1.1\": must be empty, 'None', or a valid IP address]", "reason": "Invalid", "details": { "name": "kubernetes", "kind": "Service", "causes": [ { "reason": "FieldValueInvalid", "message": "Invalid value: \"10.0.0.1.1\": field is immutable", "field": "spec.clusterIP" }, { "reason": "FieldValueInvalid", "message": "Invalid value: \"10.0.0.1.1\": must be empty, 'None', or a valid IP address", "field": "spec.clusterIP" } ] }, "code": 422 } 3.edited000066400000000000000000000016041504711711200350310ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "kubernetes" was not valid: # * spec.clusterIP: Invalid value: "10.0.0.1.1": field is immutable # * spec.clusterIP: Invalid value: "10.0.0.1.1": must be empty, 'None', or a valid IP address # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} 3.original000066400000000000000000000016041504711711200353770ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # services "kubernetes" was not valid: # * spec.clusterIP: Invalid value: "10.0.0.1.1": field is immutable # * spec.clusterIP: Invalid value: "10.0.0.1.1": must be empty, 'None', or a valid IP address # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} test.yaml000066400000000000000000000015061504711711200353530ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-errordescription: add a testcase description mode: edit args: - service/kubernetes namespace: default expectedStderr: - "services \"kubernetes\" is invalid" - A copy of your changes has been stored - Edit cancelled, no valid changes were saved expectedExitCode: 1 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/default/services/kubernetes expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/default/services/kubernetes expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 422 resultingOutput: 2.response - type: edit expectedInput: 3.original resultingOutput: 3.edited testcase-schemaless-list/000077500000000000000000000000001504711711200341775ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200357360ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list0.response000066400000000000000000000011601504711711200361140ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "kubernetes", "namespace": "default", "selfLink": "/api/v1/namespaces/default/services/kubernetes", "uid": "6a8e8829-f15f-11e6-b041-acbc32c1ca87", "resourceVersion": "16953", "creationTimestamp": "2017-02-12T20:11:19Z", "labels": { "component": "apiserver", "provider": "kubernetes" } }, "spec": { "ports": [ { "name": "https", "protocol": "TCP", "port": 443, "targetPort": 443 } ], "clusterIP": "10.0.0.1", "type": "ClusterIP", "sessionAffinity": "ClientIP" }, "status": { "loadBalancer": {} } } 1.request000066400000000000000000000000001504711711200357370ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list1.response000066400000000000000000000005701504711711200361210ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "apiVersion": "company.com/v1", "kind": "Bar", "metadata": { "name": "test", "namespace": "default", "selfLink": "/apis/company.com/v1/namespaces/default/bars/test", "uid": "fd16c23d-f185-11e6-b041-acbc32c1ca87", "resourceVersion": "16954", "creationTimestamp": "2017-02-13T00:47:26Z" }, "some-field": "field1", "third-field": { "sub-field": "bar2" } } 2.request000066400000000000000000000000001504711711200357400ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list2.response000066400000000000000000000006331504711711200361220ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "apiVersion": "company.com/v1", "field1": "value1", "field2": true, "field3": [ 1 ], "field4": { "a": true, "b": false }, "kind": "Bar", "metadata": { "name": "test2", "namespace": "default", "selfLink": "/apis/company.com/v1/namespaces/default/bars/test2", "uid": "5ef5b446-f186-11e6-b041-acbc32c1ca87", "resourceVersion": "16955", "creationTimestamp": "2017-02-13T00:50:10Z" } } 3.edited000066400000000000000000000030101504711711200355130ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes new-label: new-value name: kubernetes namespace: default resourceVersion: "16953" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} - apiVersion: company.com/v1 kind: Bar metadata: creationTimestamp: "2017-02-13T00:47:26Z" name: test namespace: default resourceVersion: "16954" selfLink: /apis/company.com/v1/namespaces/default/bars/test uid: fd16c23d-f185-11e6-b041-acbc32c1ca87 some-field: field1 other-field: other-value third-field: sub-field: bar2 - apiVersion: company.com/v1 field1: value1 field2: true field3: - 1 - 2 field4: a: true b: false kind: Bar metadata: creationTimestamp: "2017-02-13T00:50:10Z" name: test2 namespace: default resourceVersion: "16955" selfLink: /apis/company.com/v1/namespaces/default/bars/test2 uid: 5ef5b446-f186-11e6-b041-acbc32c1ca87 kind: List metadata: {} 3.original000066400000000000000000000027141504711711200360730ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 items: - apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "16953" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} - apiVersion: company.com/v1 kind: Bar metadata: creationTimestamp: "2017-02-13T00:47:26Z" name: test namespace: default resourceVersion: "16954" selfLink: /apis/company.com/v1/namespaces/default/bars/test uid: fd16c23d-f185-11e6-b041-acbc32c1ca87 some-field: field1 third-field: sub-field: bar2 - apiVersion: company.com/v1 field1: value1 field2: true field3: - 1 field4: a: true b: false kind: Bar metadata: creationTimestamp: "2017-02-13T00:50:10Z" name: test2 namespace: default resourceVersion: "16955" selfLink: /apis/company.com/v1/namespaces/default/bars/test2 uid: 5ef5b446-f186-11e6-b041-acbc32c1ca87 kind: List metadata: {} 4.request000066400000000000000000000001031504711711200357460ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "metadata": { "labels": { "new-label": "new-value" } } }4.response000066400000000000000000000012151504711711200361210ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "kubernetes", "namespace": "default", "selfLink": "/api/v1/namespaces/default/services/kubernetes", "uid": "6a8e8829-f15f-11e6-b041-acbc32c1ca87", "resourceVersion": "17087", "creationTimestamp": "2017-02-12T20:11:19Z", "labels": { "component": "apiserver", "new-label": "new-value", "provider": "kubernetes" } }, "spec": { "ports": [ { "name": "https", "protocol": "TCP", "port": 443, "targetPort": 443 } ], "clusterIP": "10.0.0.1", "type": "ClusterIP", "sessionAffinity": "ClientIP" }, "status": { "loadBalancer": {} } } 5.request000066400000000000000000000000411504711711200357500ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "other-field": "other-value" }5.response000066400000000000000000000006271504711711200361300ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "apiVersion": "company.com/v1", "kind": "Bar", "metadata": { "name": "test", "namespace": "default", "selfLink": "/apis/company.com/v1/namespaces/default/bars/test", "uid": "fd16c23d-f185-11e6-b041-acbc32c1ca87", "resourceVersion": "17088", "creationTimestamp": "2017-02-13T00:47:26Z" }, "other-field": "other-value", "some-field": "field1", "third-field": { "sub-field": "bar2" } } 6.request000066400000000000000000000000341504711711200357530ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "field3": [ 1, 2 ] }6.response000066400000000000000000000006401504711711200361240ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "apiVersion": "company.com/v1", "field1": "value1", "field2": true, "field3": [ 1, 2 ], "field4": { "a": true, "b": false }, "kind": "Bar", "metadata": { "name": "test2", "namespace": "default", "selfLink": "/apis/company.com/v1/namespaces/default/bars/test2", "uid": "5ef5b446-f186-11e6-b041-acbc32c1ca87", "resourceVersion": "17089", "creationTimestamp": "2017-02-13T00:50:10Z" } } test.yaml000066400000000000000000000031471504711711200360470ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-listdescription: edit a mix of schema and schemaless data mode: edit args: - service/kubernetes - bars/test - bars/test2 namespace: default expectedStdout: - "service/kubernetes edited" - "bar.company.com/test edited" - "bar.company.com/test2 edited" expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/default/services/kubernetes expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: request expectedMethod: GET expectedPath: /apis/company.com/v1/namespaces/default/bars/test expectedInput: 1.request resultingStatusCode: 200 resultingOutput: 1.response - type: request expectedMethod: GET expectedPath: /apis/company.com/v1/namespaces/default/bars/test2 expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response - type: edit expectedInput: 3.original resultingOutput: 3.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/default/services/kubernetes expectedContentType: application/strategic-merge-patch+json expectedInput: 4.request resultingStatusCode: 200 resultingOutput: 4.response - type: request expectedMethod: PATCH expectedPath: /apis/company.com/v1/namespaces/default/bars/test expectedContentType: application/merge-patch+json expectedInput: 5.request resultingStatusCode: 200 resultingOutput: 5.response - type: request expectedMethod: PATCH expectedPath: /apis/company.com/v1/namespaces/default/bars/test2 expectedContentType: application/merge-patch+json expectedInput: 6.request resultingStatusCode: 200 resultingOutput: 6.response testcase-single-service/000077500000000000000000000000001504711711200340165ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200355550ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service0.response000066400000000000000000000011371504711711200357370ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87", "resourceVersion": "20715", "creationTimestamp": "2017-02-01T21:14:09Z", "labels": { "app": "svc1" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 80, "targetPort": 80 } ], "selector": { "app": "svc1" }, "clusterIP": "10.0.0.146", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } 1.edited000066400000000000000000000012751504711711200353430ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "20715" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87 spec: clusterIP: 10.0.0.146 ports: - name: "80" port: 81 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 1.original000066400000000000000000000012441504711711200357050ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-01T21:14:09Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "20715" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: 5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87 spec: clusterIP: 10.0.0.146 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.request000066400000000000000000000004511504711711200355710ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service{ "metadata": { "labels": { "new-label": "new-value" } }, "spec": { "$setElementOrder/ports": [ { "port": 81 } ], "ports": [ { "name": "80", "port": 81, "protocol": "TCP", "targetPort": 80 }, { "$patch": "delete", "port": 80 } ] } }2.response000066400000000000000000000011741504711711200357420ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "5f7da8db-e8c3-11e6-b7e2-acbc32c1ca87", "resourceVersion": "20820", "creationTimestamp": "2017-02-01T21:14:09Z", "labels": { "app": "svc1", "new-label": "new-value" } }, "spec": { "ports": [ { "name": "80", "protocol": "TCP", "port": 81, "targetPort": 80 } ], "selector": { "app": "svc1" }, "clusterIP": "10.0.0.146", "type": "ClusterIP", "sessionAffinity": "None" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000015071504711711200356640ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service# kubectl create namespace edit-test # kubectl create service clusterip svc1 --tcp 80 --namespace=edit-test # kubectl edit service svc1 --namespace=edit-test description: edit a single service, add a label and change a port mode: edit args: - service - svc1 namespace: edit-test expectedStdout: - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response testcase-syntax-error/000077500000000000000000000000001504711711200335545ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200353130ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error0.response000066400000000000000000000011541504711711200354740ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "kubernetes", "namespace": "default", "selfLink": "/api/v1/namespaces/default/services/kubernetes", "uid": "6a8e8829-f15f-11e6-b041-acbc32c1ca87", "resourceVersion": "8", "creationTimestamp": "2017-02-12T20:11:19Z", "labels": { "component": "apiserver", "provider": "kubernetes" } }, "spec": { "ports": [ { "name": "https", "protocol": "TCP", "port": 443, "targetPort": 443 } ], "clusterIP": "10.0.0.1", "type": "ClusterIP", "sessionAffinity": "ClientIP" }, "status": { "loadBalancer": {} } } 1.edited000066400000000000000000000012651504711711200351000ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec clusterIP: 10.0.0.1 ports: name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: { 1.original000066400000000000000000000012671504711711200354500ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} 2.edited000066400000000000000000000014741504711711200351030ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # The edited file had a syntax error: error converting YAML to JSON: yaml: line 17: could not find expected ':' # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes new-label: foo name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec: clusterIP: 10.0.0.1 ports: - name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: {} 2.original000066400000000000000000000015021504711711200354410ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # # The edited file had a syntax error: error parsing edited-file: error converting YAML to JSON: yaml: line 18: could not find expected ':' # apiVersion: v1 kind: Service metadata: creationTimestamp: "2017-02-12T20:11:19Z" labels: component: apiserver provider: kubernetes name: kubernetes namespace: default resourceVersion: "8" selfLink: /api/v1/namespaces/default/services/kubernetes uid: 6a8e8829-f15f-11e6-b041-acbc32c1ca87 spec clusterIP: 10.0.0.1 ports: name: https port: 443 protocol: TCP targetPort: 443 sessionAffinity: ClientIP type: ClusterIP status: loadBalancer: { 3.request000066400000000000000000000000751504711711200353320ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error{ "metadata": { "labels": { "new-label": "foo" } } }3.response000066400000000000000000000012061504711711200354750ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error{ "kind": "Service", "apiVersion": "v1", "metadata": { "name": "kubernetes", "namespace": "default", "selfLink": "/api/v1/namespaces/default/services/kubernetes", "uid": "6a8e8829-f15f-11e6-b041-acbc32c1ca87", "resourceVersion": "1174", "creationTimestamp": "2017-02-12T20:11:19Z", "labels": { "component": "apiserver", "new-label": "foo", "provider": "kubernetes" } }, "spec": { "ports": [ { "name": "https", "protocol": "TCP", "port": 443, "targetPort": 443 } ], "clusterIP": "10.0.0.1", "type": "ClusterIP", "sessionAffinity": "ClientIP" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000013731504711711200354230ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-errordescription: edit with a syntax error, then re-edit and save mode: edit args: - service/kubernetes namespace: default expectedStdout: - "service/kubernetes edited" expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/default/services/kubernetes expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: edit expectedInput: 2.original resultingOutput: 2.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/default/services/kubernetes expectedContentType: application/strategic-merge-patch+json expectedInput: 3.request resultingStatusCode: 200 resultingOutput: 3.response testcase-unknown-field-known-group-kind/000077500000000000000000000000001504711711200370665ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200406250ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kind0.response000066400000000000000000000007641504711711200410140ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kind{ "kind": "StorageClass", "apiVersion": "storage.k8s.io/v1beta1", "metadata": { "name": "foo", "selfLink": "/apis/storage.k8s.io/v1beta1/storageclassesfoo", "uid": "b2287558-f190-11e6-b041-acbc32c1ca87", "resourceVersion": "21388", "creationTimestamp": "2017-02-13T02:04:04Z", "labels": { "label1": "value1" } }, "provisioner": "foo", "parameters": { "baz": "qux", "foo": "bar" }, "unknownServerField1": { "data": true }, "unknownServerField2": { "data": true } } 1.edited000066400000000000000000000011661504711711200404120ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kind# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: storage.k8s.io/v1beta1 kind: StorageClass metadata: creationTimestamp: "2017-02-13T02:04:04Z" labels: label1: value1 label2: value2 name: foo resourceVersion: "21388" selfLink: /apis/storage.k8s.io/v1beta1/storageclassesfoo uid: b2287558-f190-11e6-b041-acbc32c1ca87 parameters: baz: qux foo: bar provisioner: foo unknownClientField: clientdata: true unknownServerField1: data: true 1.original000066400000000000000000000011361504711711200407550ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kind# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: storage.k8s.io/v1beta1 kind: StorageClass metadata: creationTimestamp: "2017-02-13T02:04:04Z" labels: label1: value1 name: foo resourceVersion: "21388" selfLink: /apis/storage.k8s.io/v1beta1/storageclassesfoo uid: b2287558-f190-11e6-b041-acbc32c1ca87 parameters: baz: qux foo: bar provisioner: foo unknownServerField1: data: true unknownServerField2: data: true 2.request000066400000000000000000000002151504711711200406370ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kind{ "metadata": { "labels": { "label2": "value2" } }, "unknownClientField": { "clientdata": true }, "unknownServerField2": null }2.response000066400000000000000000000010201504711711200410000ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kind{ "kind": "StorageClass", "apiVersion": "storage.k8s.io/v1beta1", "metadata": { "name": "foo", "selfLink": "/apis/storage.k8s.io/v1beta1/storageclassesfoo", "uid": "b2287558-f190-11e6-b041-acbc32c1ca87", "resourceVersion": "21431", "creationTimestamp": "2017-02-13T02:04:04Z", "labels": { "label1": "value1", "label2": "value2" } }, "provisioner": "foo", "parameters": { "baz": "qux", "foo": "bar" }, "unknownClientField": { "clientdata": true }, "unknownServerField1": { "data": true } } test.yaml000066400000000000000000000013321504711711200407300ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kinddescription: edit an unknown version of a known group/kind mode: edit args: - storageclasses.v1beta1.storage.k8s.io/foo namespace: default expectedStdout: - "storageclass.storage.k8s.io/foo edited" expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /apis/storage.k8s.io/v1beta1/storageclasses/foo expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /apis/storage.k8s.io/v1beta1/storageclasses/foo expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response testcase-unknown-version-known-group-kind/000077500000000000000000000000001504711711200374705ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200412270ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind0.response000066400000000000000000000006711504711711200414130ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind{ "kind": "StorageClass", "apiVersion": "storage.k8s.io/v0", "metadata": { "name": "foo", "selfLink": "/apis/storage.k8s.io/v0/storageclassesfoo", "uid": "b2287558-f190-11e6-b041-acbc32c1ca87", "resourceVersion": "21388", "creationTimestamp": "2017-02-13T02:04:04Z", "labels": { "label1": "value1" } }, "provisioner": "foo", "parameters": { "baz": "qux", "foo": "bar" }, "extraField": { "otherData": true } } 1.edited000066400000000000000000000011241504711711200410060ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: storage.k8s.io/v0 extraField: otherData: true addedData: "foo" kind: StorageClass metadata: creationTimestamp: "2017-02-13T02:04:04Z" labels: label1: value1 label2: value2 name: foo resourceVersion: "21388" selfLink: /apis/storage.k8s.io/v0/storageclassesfoo uid: b2287558-f190-11e6-b041-acbc32c1ca87 parameters: baz: qux foo: bar provisioner: foo 1.original000066400000000000000000000010561504711711200413600ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: storage.k8s.io/v0 extraField: otherData: true kind: StorageClass metadata: creationTimestamp: "2017-02-13T02:04:04Z" labels: label1: value1 name: foo resourceVersion: "21388" selfLink: /apis/storage.k8s.io/v0/storageclassesfoo uid: b2287558-f190-11e6-b041-acbc32c1ca87 parameters: baz: qux foo: bar provisioner: foo 2.request000066400000000000000000000001471504711711200412450ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind{ "extraField": { "addedData": "foo" }, "metadata": { "labels": { "label2": "value2" } } }2.response000066400000000000000000000007451504711711200414170ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind{ "kind": "StorageClass", "apiVersion": "storage.k8s.io/v0", "metadata": { "name": "foo", "selfLink": "/apis/storage.k8s.io/v0/storageclassesfoo", "uid": "b2287558-f190-11e6-b041-acbc32c1ca87", "resourceVersion": "21431", "creationTimestamp": "2017-02-13T02:04:04Z", "labels": { "label1": "value1", "label2": "value2" } }, "provisioner": "foo", "parameters": { "baz": "qux", "foo": "bar" }, "extraField": { "otherData": true, "addedData": true } } test.yaml000066400000000000000000000013011504711711200413260ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kinddescription: edit an unknown version of a known group/kind mode: edit args: - storageclasses.v0.storage.k8s.io/foo namespace: default expectedStdout: - "storageclass.storage.k8s.io/foo edited" expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /apis/storage.k8s.io/v0/storageclasses/foo expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /apis/storage.k8s.io/v0/storageclasses/foo expectedContentType: application/merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response testcase-update-annotation/000077500000000000000000000000001504711711200345315ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001504711711200362700ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation0.response000066400000000000000000000020661504711711200364540ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation{ "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "creationTimestamp": "2017-02-27T19:40:53Z", "labels": { "app": "svc1" }, "name": "svc1", "namespace": "edit-test", "resourceVersion": "670", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "a6c11186-fd24-11e6-b53c-480fcf4a5275" }, "spec": { "clusterIP": "10.0.0.204", "ports": [ { "name": "80", "port": 80, "protocol": "TCP", "targetPort": 80 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } 1.edited000066400000000000000000000020171504711711200360510ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} creationTimestamp: "2017-02-27T19:40:53Z" labels: app: svc1 new-label: new-value name: svc1 namespace: edit-test resourceVersion: "670" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: a6c11186-fd24-11e6-b53c-480fcf4a5275 spec: clusterIP: 10.0.0.204 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 1.original000066400000000000000000000017661504711711200364310ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: v1 kind: Service metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} creationTimestamp: "2017-02-27T19:40:53Z" labels: app: svc1 name: svc1 namespace: edit-test resourceVersion: "670" selfLink: /api/v1/namespaces/edit-test/services/svc1 uid: a6c11186-fd24-11e6-b53c-480fcf4a5275 spec: clusterIP: 10.0.0.204 ports: - name: "80" port: 80 protocol: TCP targetPort: 80 selector: app: svc1 sessionAffinity: None type: ClusterIP status: loadBalancer: {} 2.request000066400000000000000000000013631504711711200363070ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation{ "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-27T19:40:53Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"edit-test\",\"resourceVersion\":\"670\",\"selfLink\":\"/api/v1/namespaces/edit-test/services/svc1\",\"uid\":\"a6c11186-fd24-11e6-b53c-480fcf4a5275\"},\"spec\":{\"clusterIP\":\"10.0.0.204\",\"ports\":[{\"name\":\"80\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "labels": { "new-label": "new-value" } } }2.response000066400000000000000000000025341504711711200364560ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation{ "kind": "Service", "apiVersion": "v1", "metadata": { "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"creationTimestamp\":\"2017-02-27T19:40:53Z\",\"labels\":{\"app\":\"svc1\",\"new-label\":\"new-value\"},\"name\":\"svc1\",\"namespace\":\"edit-test\",\"resourceVersion\":\"670\",\"selfLink\":\"/api/v1/namespaces/edit-test/services/svc1\",\"uid\":\"a6c11186-fd24-11e6-b53c-480fcf4a5275\"},\"spec\":{\"clusterIP\":\"10.0.0.204\",\"ports\":[{\"name\":\"80\",\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" }, "name": "svc1", "namespace": "edit-test", "selfLink": "/api/v1/namespaces/edit-test/services/svc1", "uid": "a6c11186-fd24-11e6-b53c-480fcf4a5275", "resourceVersion":"1045", "creationTimestamp":"2017-02-27T19:40:53Z", "labels": { "app": "svc1", "new-label": "new-value" } }, "spec": { "clusterIP": "10.0.0.204", "ports": [ { "name": "80", "port": 80, "protocol": "TCP", "targetPort": 80 } ], "selector": { "app": "svc1" }, "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } test.yaml000066400000000000000000000016051504711711200363760ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation# kubectl create namespace edit-test # kubectl create service clusterip svc1 --tcp 80 --namespace=edit-test --save-config # kubectl edit service svc1 --namespace=edit-test --save-config=true description: edit with flag --save-config=true should update the annotation mode: edit args: - service - svc1 saveConfig: "true" namespace: edit-test expectedStdout: - service/svc1 edited expectedExitCode: 0 steps: - type: request expectedMethod: GET expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedInput: 0.request resultingStatusCode: 200 resultingOutput: 0.response - type: edit expectedInput: 1.original resultingOutput: 1.edited - type: request expectedMethod: PATCH expectedPath: /api/v1/namespaces/edit-test/services/svc1 expectedContentType: application/strategic-merge-patch+json expectedInput: 2.request resultingStatusCode: 200 resultingOutput: 2.response kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/events/000077500000000000000000000000001504711711200261135ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer.go000066400000000000000000000065261504711711200313370ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package events import ( "fmt" "io" "strings" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/cli-runtime/pkg/printers" ) // EventPrinter stores required fields to be used for // default printing for events command. type EventPrinter struct { NoHeaders bool AllNamespaces bool headersPrinted bool } // PrintObj prints different type of event objects. func (ep *EventPrinter) PrintObj(obj runtime.Object, out io.Writer) error { if !ep.NoHeaders && !ep.headersPrinted { ep.printHeadings(out) ep.headersPrinted = true } switch t := obj.(type) { case *corev1.EventList: for _, e := range t.Items { ep.printOneEvent(out, e) } case *corev1.Event: ep.printOneEvent(out, *t) default: return fmt.Errorf("unknown event type %t", t) } return nil } func (ep *EventPrinter) printHeadings(w io.Writer) { if ep.AllNamespaces { fmt.Fprintf(w, "NAMESPACE\t") } fmt.Fprintf(w, "LAST SEEN\tTYPE\tREASON\tOBJECT\tMESSAGE\n") } func (ep *EventPrinter) printOneEvent(w io.Writer, e corev1.Event) { interval := getInterval(e) if ep.AllNamespaces { fmt.Fprintf(w, "%v\t", e.Namespace) } fmt.Fprintf(w, "%s\t%s\t%s\t%s/%s\t%v\n", interval, printers.EscapeTerminal(e.Type), printers.EscapeTerminal(e.Reason), printers.EscapeTerminal(e.InvolvedObject.Kind), printers.EscapeTerminal(e.InvolvedObject.Name), printers.EscapeTerminal(strings.TrimSpace(e.Message)), ) } func getInterval(e corev1.Event) string { var interval string firstTimestampSince := translateMicroTimestampSince(e.EventTime) if e.EventTime.IsZero() { firstTimestampSince = translateTimestampSince(e.FirstTimestamp) } if e.Series != nil { interval = fmt.Sprintf("%s (x%d over %s)", translateMicroTimestampSince(e.Series.LastObservedTime), e.Series.Count, firstTimestampSince) } else if e.Count > 1 { interval = fmt.Sprintf("%s (x%d over %s)", translateTimestampSince(e.LastTimestamp), e.Count, firstTimestampSince) } else { interval = firstTimestampSince } return interval } // translateMicroTimestampSince returns the elapsed time since timestamp in // human-readable approximation. func translateMicroTimestampSince(timestamp metav1.MicroTime) string { if timestamp.IsZero() { return "" } return duration.HumanDuration(time.Since(timestamp.Time)) } // translateTimestampSince returns the elapsed time since timestamp in // human-readable approximation. func translateTimestampSince(timestamp metav1.Time) string { if timestamp.IsZero() { return "" } return duration.HumanDuration(time.Since(timestamp.Time)) } func NewEventPrinter(noHeader, allNamespaces bool) *EventPrinter { return &EventPrinter{ NoHeaders: noHeader, AllNamespaces: allNamespaces, } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer_test.go000066400000000000000000000200631504711711200323660ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package events import ( "bytes" "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) func TestPrintObj(t *testing.T) { tests := []struct { printer EventPrinter obj runtime.Object expected string }{ { printer: EventPrinter{ NoHeaders: false, AllNamespaces: false, }, obj: &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, expected: `LAST SEEN TYPE REASON OBJECT MESSAGE 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 `, }, { printer: EventPrinter{ NoHeaders: false, AllNamespaces: true, }, obj: &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "bar", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar2", Namespace: "foo2", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-11 * time.Minute)), }, }, }, }, expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica set bar-002 from 0 to 1 `, }, { printer: EventPrinter{ NoHeaders: true, AllNamespaces: false, }, obj: &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, expected: "12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1\n", }, { printer: EventPrinter{ NoHeaders: false, AllNamespaces: true, }, obj: &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 `, }, { printer: EventPrinter{ NoHeaders: true, AllNamespaces: true, }, obj: &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, expected: `foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 `, }, { printer: EventPrinter{ NoHeaders: false, AllNamespaces: false, }, obj: &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar\x1b", Namespace: "foo", }, Type: "test\x1b", Reason: "test\x1b", Message: "\x1b", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-1 * time.Minute)), }, }, }, }, expected: `LAST SEEN TYPE REASON OBJECT MESSAGE 60s (x3 over 20m) test^[ test^[ Deployment/bar^[ ^[ `, }, } for _, test := range tests { t.Run("", func(t *testing.T) { buffer := &bytes.Buffer{} if err := test.printer.PrintObj(test.obj, buffer); err != nil { t.Errorf("unexpected error: %v", err) } if buffer.String() != test.expected { t.Errorf("\nexpected:\n'%s'\nsaw\n'%s'\n", test.expected, buffer.String()) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/events/events.go000066400000000000000000000271611504711711200277550ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package events import ( "context" "fmt" "io" "sort" "strings" "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" runtimeresource "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" watchtools "k8s.io/client-go/tools/watch" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" ) var ( eventsLong = templates.LongDesc(i18n.T(` Display events. Prints a table of the most important information about events. You can request events for a namespace, for all namespace, or filtered to only those pertaining to a specified resource.`)) eventsExample = templates.Examples(i18n.T(` # List recent events in the default namespace kubectl events # List recent events in all namespaces kubectl events --all-namespaces # List recent events for the specified pod, then wait for more events and list them as they arrive kubectl events --for pod/web-pod-13je7 --watch # List recent events in YAML format kubectl events -oyaml # List recent only events of type 'Warning' or 'Normal' kubectl events --types=Warning,Normal`)) ) // EventsFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which // reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes // the logic itself easy to unit test. type EventsFlags struct { RESTClientGetter genericclioptions.RESTClientGetter PrintFlags *genericclioptions.PrintFlags AllNamespaces bool Watch bool NoHeaders bool ForObject string FilterTypes []string ChunkSize int64 genericiooptions.IOStreams } // NewEventsFlags returns a default EventsFlags func NewEventsFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *EventsFlags { return &EventsFlags{ RESTClientGetter: restClientGetter, PrintFlags: genericclioptions.NewPrintFlags("events").WithTypeSetter(scheme.Scheme), IOStreams: streams, ChunkSize: cmdutil.DefaultChunkSize, } } // EventsOptions is a set of options that allows you to list events. This is the object reflects the // runtime needs of an events command, making the logic itself easy to unit test. type EventsOptions struct { Namespace string AllNamespaces bool Watch bool FilterTypes []string forGVK schema.GroupVersionKind forName string client *kubernetes.Clientset PrintObj printers.ResourcePrinterFunc genericiooptions.IOStreams } // NewCmdEvents creates a new events command func NewCmdEvents(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { flags := NewEventsFlags(restClientGetter, streams) cmd := &cobra.Command{ Use: fmt.Sprintf("events [(-o|--output=)%s] [--for TYPE/NAME] [--watch] [--types=Normal,Warning]", strings.Join(flags.PrintFlags.AllowedFormats(), "|")), DisableFlagsInUseLine: true, Short: i18n.T("List events"), Long: eventsLong, Example: eventsExample, Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions() cmdutil.CheckErr(err) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } flags.AddFlags(cmd) flags.PrintFlags.AddFlags(cmd) return cmd } // AddFlags registers flags for a cli. func (flags *EventsFlags) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVarP(&flags.Watch, "watch", "w", flags.Watch, "After listing the requested events, watch for more events.") cmd.Flags().BoolVarP(&flags.AllNamespaces, "all-namespaces", "A", flags.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().StringVar(&flags.ForObject, "for", flags.ForObject, "Filter events to only those pertaining to the specified resource.") cmd.Flags().StringSliceVar(&flags.FilterTypes, "types", flags.FilterTypes, "Output only events of given types.") cmd.Flags().BoolVar(&flags.NoHeaders, "no-headers", flags.NoHeaders, "When using the default output format, don't print headers.") cmdutil.AddChunkSizeFlag(cmd, &flags.ChunkSize) } // ToOptions converts from CLI inputs to runtime inputs. func (flags *EventsFlags) ToOptions() (*EventsOptions, error) { o := &EventsOptions{ AllNamespaces: flags.AllNamespaces, Watch: flags.Watch, FilterTypes: flags.FilterTypes, IOStreams: flags.IOStreams, } var err error o.Namespace, _, err = flags.RESTClientGetter.ToRawKubeConfigLoader().Namespace() if err != nil { return nil, err } if flags.ForObject != "" { mapper, err := flags.RESTClientGetter.ToRESTMapper() if err != nil { return nil, err } var found bool o.forGVK, o.forName, found, err = decodeResourceTypeName(mapper, flags.ForObject) if err != nil { return nil, err } if !found { return nil, fmt.Errorf("--for must be in resource/name form") } } clientConfig, err := flags.RESTClientGetter.ToRESTConfig() if err != nil { return nil, err } o.client, err = kubernetes.NewForConfig(clientConfig) if err != nil { return nil, err } if len(o.FilterTypes) > 0 { o.FilterTypes = sets.List(sets.New[string](o.FilterTypes...)) } var printer printers.ResourcePrinter if flags.PrintFlags.OutputFormat != nil && len(*flags.PrintFlags.OutputFormat) > 0 { printer, err = flags.PrintFlags.ToPrinter() if err != nil { return nil, err } } else { printer = NewEventPrinter(flags.NoHeaders, flags.AllNamespaces) } o.PrintObj = func(object runtime.Object, writer io.Writer) error { return printer.PrintObj(object, writer) } return o, nil } func (o *EventsOptions) Validate() error { for _, val := range o.FilterTypes { if !strings.EqualFold(val, "Normal") && !strings.EqualFold(val, "Warning") { return fmt.Errorf("valid --types are Normal or Warning") } } return nil } // Run retrieves events func (o *EventsOptions) Run() error { ctx := context.TODO() namespace := o.Namespace if o.AllNamespaces { namespace = "" } listOptions := metav1.ListOptions{Limit: cmdutil.DefaultChunkSize} if o.forName != "" { listOptions.FieldSelector = fields.AndSelectors( fields.OneTermEqualSelector("involvedObject.kind", o.forGVK.Kind), fields.OneTermEqualSelector("involvedObject.apiVersion", o.forGVK.GroupVersion().String()), fields.OneTermEqualSelector("involvedObject.name", o.forName)).String() } if o.Watch { return o.runWatch(ctx, namespace, listOptions) } e := o.client.CoreV1().Events(namespace) el := &corev1.EventList{ TypeMeta: metav1.TypeMeta{ Kind: "EventList", APIVersion: "v1", }, } err := runtimeresource.FollowContinue(&listOptions, func(options metav1.ListOptions) (runtime.Object, error) { newEvents, err := e.List(ctx, options) if err != nil { return nil, runtimeresource.EnhanceListError(err, options, "events") } el.Items = append(el.Items, newEvents.Items...) return newEvents, nil }) if err != nil { return err } var filteredEvents []corev1.Event for _, e := range el.Items { if !o.filteredEventType(e.Type) { continue } if e.GetObjectKind().GroupVersionKind().Empty() { e.SetGroupVersionKind(schema.GroupVersionKind{ Version: "v1", Kind: "Event", }) } filteredEvents = append(filteredEvents, e) } el.Items = filteredEvents if len(el.Items) == 0 { if o.AllNamespaces { fmt.Fprintln(o.ErrOut, "No events found.") } else { fmt.Fprintf(o.ErrOut, "No events found in %s namespace.\n", o.Namespace) } return nil } w := printers.GetNewTabWriter(o.Out) sort.Sort(SortableEvents(el.Items)) o.PrintObj(el, w) w.Flush() return nil } func (o *EventsOptions) runWatch(ctx context.Context, namespace string, listOptions metav1.ListOptions) error { eventWatch, err := o.client.CoreV1().Events(namespace).Watch(ctx, listOptions) if err != nil { return err } w := printers.GetNewTabWriter(o.Out) cctx, cancel := context.WithCancel(ctx) defer cancel() intr := interrupt.New(nil, cancel) intr.Run(func() error { _, err := watchtools.UntilWithoutRetry(cctx, eventWatch, func(e watch.Event) (bool, error) { if e.Type == watch.Deleted { // events are deleted after 1 hour; don't print that return false, nil } if ev, ok := e.Object.(*corev1.Event); !ok || !o.filteredEventType(ev.Type) { return false, nil } if e.Object.GetObjectKind().GroupVersionKind().Empty() { e.Object.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{ Version: "v1", Kind: "Event", }) } o.PrintObj(e.Object, w) w.Flush() return false, nil }) return err }) return nil } // filteredEventType checks given event can be printed // by comparing it in filtered event flag. // If --event flag is not set by user, this function allows // all events to be printed. func (o *EventsOptions) filteredEventType(et string) bool { if len(o.FilterTypes) == 0 { return true } for _, t := range o.FilterTypes { if strings.EqualFold(t, et) { return true } } return false } // SortableEvents implements sort.Interface for []api.Event by time type SortableEvents []corev1.Event func (list SortableEvents) Len() int { return len(list) } func (list SortableEvents) Swap(i, j int) { list[i], list[j] = list[j], list[i] } func (list SortableEvents) Less(i, j int) bool { return eventTime(list[i]).Before(eventTime(list[j])) } // Return the time that should be used for sorting, which can come from // various places in corev1.Event. func eventTime(event corev1.Event) time.Time { if event.Series != nil { return event.Series.LastObservedTime.Time } if !event.LastTimestamp.Time.IsZero() { return event.LastTimestamp.Time } return event.EventTime.Time } // Inspired by k8s.io/cli-runtime/pkg/resource splitResourceTypeName() // decodeResourceTypeName handles type/name resource formats and returns a resource tuple // (empty or not), whether it successfully found one, and an error func decodeResourceTypeName(mapper meta.RESTMapper, s string) (gvk schema.GroupVersionKind, name string, found bool, err error) { if !strings.Contains(s, "/") { return } seg := strings.Split(s, "/") if len(seg) != 2 { err = fmt.Errorf("arguments in resource/name form may not have more than one slash") return } resource, name := seg[0], seg[1] fullySpecifiedGVR, groupResource := schema.ParseResourceArg(strings.ToLower(resource)) gvr := schema.GroupVersionResource{} if fullySpecifiedGVR != nil { gvr, _ = mapper.ResourceFor(*fullySpecifiedGVR) } if gvr.Empty() { gvr, err = mapper.ResourceFor(groupResource.WithVersion("")) if err != nil { return } } gvk, err = mapper.KindFor(gvr) if err != nil { return } found = true return } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/events/events_test.go000066400000000000000000000164251504711711200310150ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package events import ( "io" "net/http" "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func getFakeEvents() *corev1.EventList { return &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-30 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeWarning, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-28 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-18 * time.Minute)), }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "otherfoo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "otherfoo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-25 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Minute)), }, }, }, } } func TestEventIsSorted(t *testing.T) { codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) streams, _, buf, _ := genericiooptions.NewTestIOStreams() clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig()) if err != nil { t.Fatal(err) } clientset.CoreV1().RESTClient().(*restclient.RESTClient).Client = fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getFakeEvents())}, nil }) printer := NewEventPrinter(false, true) options := &EventsOptions{ AllNamespaces: true, client: clientset, PrintObj: func(object runtime.Object, writer io.Writer) error { return printer.PrintObj(object, writer) }, IOStreams: streams, } err = options.Run() if err != nil { t.Fatal(err) } expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestEventNoHeaders(t *testing.T) { codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) streams, _, buf, _ := genericiooptions.NewTestIOStreams() clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig()) if err != nil { t.Fatal(err) } clientset.CoreV1().RESTClient().(*restclient.RESTClient).Client = fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getFakeEvents())}, nil }) printer := NewEventPrinter(true, true) options := &EventsOptions{ AllNamespaces: true, client: clientset, PrintObj: func(object runtime.Object, writer io.Writer) error { return printer.PrintObj(object, writer) }, IOStreams: streams, } err = options.Run() if err != nil { t.Fatal(err) } expected := `foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestEventFiltered(t *testing.T) { codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) streams, _, buf, _ := genericiooptions.NewTestIOStreams() clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig()) if err != nil { t.Fatal(err) } clientset.CoreV1().RESTClient().(*restclient.RESTClient).Client = fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getFakeEvents())}, nil }) printer := NewEventPrinter(false, true) options := &EventsOptions{ AllNamespaces: true, client: clientset, FilterTypes: []string{"WARNING"}, PrintObj: func(object runtime.Object, writer io.Writer) error { return printer.PrintObj(object, writer) }, IOStreams: streams, } err = options.Run() if err != nil { t.Fatal(err) } expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/exec/000077500000000000000000000000001504711711200255335ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go000066400000000000000000000314241504711711200270120ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package exec import ( "context" "fmt" "io" "net/url" "time" dockerterm "github.com/moby/term" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/httpstream" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" coreclient "k8s.io/client-go/kubernetes/typed/core/v1" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" "k8s.io/apimachinery/pkg/api/meta" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/podcmd" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" ) var ( execExample = templates.Examples(i18n.T(` # Get output from running the 'date' command from pod mypod, using the first container by default kubectl exec mypod -- date # Get output from running the 'date' command in ruby-container from pod mypod kubectl exec mypod -c ruby-container -- date # Switch to raw terminal mode; sends stdin to 'bash' in ruby-container from pod mypod # and sends stdout/stderr from 'bash' back to the client kubectl exec mypod -c ruby-container -i -t -- bash -il # List contents of /usr from the first container of pod mypod and sort by modification time # If the command you want to execute in the pod has any flags in common (e.g. -i), # you must use two dashes (--) to separate your command's flags/arguments # Also note, do not surround your command and its flags/arguments with quotes # unless that is how you would execute it normally (i.e., do ls -t /usr, not "ls -t /usr") kubectl exec mypod -i -t -- ls -t /usr # Get output from running 'date' command from the first pod of the deployment mydeployment, using the first container by default kubectl exec deploy/mydeployment -- date # Get output from running 'date' command from the first pod of the service myservice, using the first container by default kubectl exec svc/myservice -- date `)) ) const ( defaultPodExecTimeout = 60 * time.Second ) func NewCmdExec(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { options := &ExecOptions{ StreamOptions: StreamOptions{ IOStreams: streams, }, Executor: &DefaultRemoteExecutor{}, } cmd := &cobra.Command{ Use: "exec (POD | TYPE/NAME) [-c CONTAINER] [flags] -- COMMAND [args...]", DisableFlagsInUseLine: true, Short: i18n.T("Execute a command in a container"), Long: i18n.T("Execute a command in a container."), Example: execExample, ValidArgsFunction: completion.PodResourceNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { argsLenAtDash := cmd.ArgsLenAtDash() cmdutil.CheckErr(options.Complete(f, cmd, args, argsLenAtDash)) cmdutil.CheckErr(options.Validate()) cmdutil.CheckErr(options.Run()) }, } cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodExecTimeout) cmdutil.AddJsonFilenameFlag(cmd.Flags(), &options.FilenameOptions.Filenames, "to use to exec into the resource") // TODO support UID cmdutil.AddContainerVarFlags(cmd, &options.ContainerName, options.ContainerName) cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc("container", completion.ContainerCompletionFunc(f))) cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", options.Stdin, "Pass stdin to the container") cmd.Flags().BoolVarP(&options.TTY, "tty", "t", options.TTY, "Stdin is a TTY") cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", options.Quiet, "Only print output from the remote session") return cmd } // RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing type RemoteExecutor interface { // Execute supports executing remote command in a pod. Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error // ExecuteWithContext, in contrast to Execute, supports stopping the remote command via context cancellation. ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error } // DefaultRemoteExecutor is the standard implementation of remote command execution type DefaultRemoteExecutor struct{} func (d *DefaultRemoteExecutor) Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { return d.ExecuteWithContext(context.Background(), url, config, stdin, stdout, stderr, tty, terminalSizeQueue) } func (*DefaultRemoteExecutor) ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { exec, err := createExecutor(url, config) if err != nil { return err } return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ Stdin: stdin, Stdout: stdout, Stderr: stderr, Tty: tty, TerminalSizeQueue: terminalSizeQueue, }) } // createExecutor returns the Executor or an error if one occurred. func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) { exec, err := remotecommand.NewSPDYExecutor(config, "POST", url) if err != nil { return nil, err } // Fallback executor is default, unless feature flag is explicitly disabled. if !cmdutil.RemoteCommandWebsockets.IsDisabled() { // WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17). websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String()) if err != nil { return nil, err } exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, func(err error) bool { return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err) }) if err != nil { return nil, err } } return exec, nil } type StreamOptions struct { Namespace string PodName string ContainerName string Stdin bool TTY bool // minimize unnecessary output Quiet bool // InterruptParent, if set, is used to handle interrupts while attached InterruptParent *interrupt.Handler genericiooptions.IOStreams // for testing overrideStreams func() (io.ReadCloser, io.Writer, io.Writer) isTerminalIn func(t term.TTY) bool } // ExecOptions declare the arguments accepted by the Exec command type ExecOptions struct { StreamOptions resource.FilenameOptions ResourceName string Command []string EnforceNamespace bool Builder func() *resource.Builder ExecutablePodFn polymorphichelpers.AttachablePodForObjectFunc restClientGetter genericclioptions.RESTClientGetter Pod *corev1.Pod Executor RemoteExecutor PodClient coreclient.PodsGetter GetPodTimeout time.Duration Config *restclient.Config } // Complete verifies command line arguments and loads data from the command environment func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []string, argsLenAtDash int) error { if len(argsIn) > 0 && argsLenAtDash != 0 { p.ResourceName = argsIn[0] } if argsLenAtDash > -1 { p.Command = argsIn[argsLenAtDash:] } else if len(argsIn) > 1 || (len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0) { return cmdutil.UsageErrorf(cmd, "exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead") } var err error p.Namespace, p.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } p.ExecutablePodFn = polymorphichelpers.AttachablePodForObjectFn p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) if err != nil { return cmdutil.UsageErrorf(cmd, "%s", err.Error()) } p.Builder = f.NewBuilder p.restClientGetter = f p.Config, err = f.ToRESTConfig() if err != nil { return err } clientset, err := f.KubernetesClientSet() if err != nil { return err } p.PodClient = clientset.CoreV1() return nil } // Validate checks that the provided exec options are specified. func (p *ExecOptions) Validate() error { if len(p.PodName) == 0 && len(p.ResourceName) == 0 && len(p.FilenameOptions.Filenames) == 0 { return fmt.Errorf("pod, type/name or --filename must be specified") } if len(p.Command) == 0 { return fmt.Errorf("you must specify at least one command for the container") } if p.Out == nil || p.ErrOut == nil { return fmt.Errorf("both output and error output must be provided") } return nil } func (o *StreamOptions) SetupTTY() term.TTY { t := term.TTY{ Parent: o.InterruptParent, Out: o.Out, } if !o.Stdin { // need to nil out o.In to make sure we don't create a stream for stdin o.In = nil o.TTY = false return t } t.In = o.In if !o.TTY { return t } if o.isTerminalIn == nil { o.isTerminalIn = func(tty term.TTY) bool { return tty.IsTerminalIn() } } if !o.isTerminalIn(t) { o.TTY = false if !o.Quiet && o.ErrOut != nil { fmt.Fprintln(o.ErrOut, "Unable to use a TTY - input is not a terminal or the right kind of file") } return t } // if we get to here, the user wants to attach stdin, wants a TTY, and o.In is a terminal, so we // can safely set t.Raw to true t.Raw = true if o.overrideStreams == nil { // use dockerterm.StdStreams() to get the right I/O handles on Windows o.overrideStreams = dockerterm.StdStreams } stdin, stdout, _ := o.overrideStreams() o.In = stdin t.In = stdin if o.Out != nil { o.Out = stdout t.Out = stdout } return t } // Run executes a validated remote execution against a pod. func (p *ExecOptions) Run() error { var err error // we still need legacy pod getter when PodName in ExecOptions struct is provided, // since there are any other command run this function by providing Podname with PodsGetter // and without resource builder, eg: `kubectl cp`. if len(p.PodName) != 0 { p.Pod, err = p.PodClient.Pods(p.Namespace).Get(context.TODO(), p.PodName, metav1.GetOptions{}) if err != nil { return err } } else { builder := p.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). FilenameParam(p.EnforceNamespace, &p.FilenameOptions). NamespaceParam(p.Namespace).DefaultNamespace() if len(p.ResourceName) > 0 { builder = builder.ResourceNames("pods", p.ResourceName) } obj, err := builder.Do().Object() if err != nil { return err } if meta.IsListType(obj) { return fmt.Errorf("cannot exec into multiple objects at a time") } p.Pod, err = p.ExecutablePodFn(p.restClientGetter, obj, p.GetPodTimeout) if err != nil { return err } } pod := p.Pod if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase) } containerName := p.ContainerName if len(containerName) == 0 { container, err := podcmd.FindOrDefaultContainerByName(pod, containerName, p.Quiet, p.ErrOut) if err != nil { return err } containerName = container.Name } // ensure we can recover the terminal while attached t := p.SetupTTY() var sizeQueue remotecommand.TerminalSizeQueue if t.Raw { // this call spawns a goroutine to monitor/update the terminal size sizeQueue = t.MonitorSize(t.GetSize()) // unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is // true p.ErrOut = nil } fn := func() error { restClient, err := restclient.RESTClientFor(p.Config) if err != nil { return err } // TODO: consider abstracting into a client invocation or client helper req := restClient.Post(). Resource("pods"). Name(pod.Name). Namespace(pod.Namespace). SubResource("exec") req.VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: p.Command, Stdin: p.Stdin, Stdout: p.Out != nil, Stderr: p.ErrOut != nil, TTY: t.Raw, }, scheme.ParameterCodec) return p.Executor.Execute(req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue) } if err := t.Safe(fn); err != nil { return err } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec_test.go000066400000000000000000000317711504711711200300560ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package exec import ( "bytes" "context" "fmt" "io" "net/http" "net/url" "reflect" "strings" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" "k8s.io/client-go/tools/remotecommand" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/term" ) type fakeRemoteExecutor struct { url *url.URL execErr error } func (f *fakeRemoteExecutor) Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { return f.ExecuteWithContext(context.Background(), url, config, stdin, stdout, stderr, tty, terminalSizeQueue) } func (f *fakeRemoteExecutor) ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { f.url = url return f.execErr } func TestPodAndContainer(t *testing.T) { tests := []struct { args []string argsLenAtDash int p *ExecOptions name string expectError bool expectedPod string expectedContainer string expectedArgs []string obj *corev1.Pod }{ { p: &ExecOptions{}, argsLenAtDash: -1, expectError: true, name: "empty", }, { p: &ExecOptions{}, argsLenAtDash: -1, expectError: true, name: "no cmd", obj: execPod(), }, { p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}}, argsLenAtDash: -1, expectError: true, name: "no cmd, w/ container", obj: execPod(), }, { p: &ExecOptions{}, args: []string{"foo", "cmd"}, argsLenAtDash: 0, expectError: true, name: "no pod, pod name is behind dash", obj: execPod(), }, { p: &ExecOptions{}, args: []string{"foo"}, argsLenAtDash: -1, expectError: true, name: "no cmd, w/o flags", obj: execPod(), }, { p: &ExecOptions{}, args: []string{"foo", "cmd"}, argsLenAtDash: 1, expectedPod: "foo", expectedArgs: []string{"cmd"}, name: "cmd, w/o flags", obj: execPod(), }, { p: &ExecOptions{}, args: []string{"foo", "cmd"}, argsLenAtDash: -1, expectError: true, name: "cmd, cmd is behind dash", obj: execPod(), }, { p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}}, args: []string{"foo", "cmd"}, argsLenAtDash: -1, expectError: true, name: "cmd, container in flag", obj: execPod(), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var err error tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return nil, nil }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() cmd := NewCmdExec(tf, genericiooptions.NewTestIOStreamsDiscard()) options := test.p options.ErrOut = bytes.NewBuffer([]byte{}) options.Out = bytes.NewBuffer([]byte{}) err = options.Complete(tf, cmd, test.args, test.argsLenAtDash) if !test.expectError && err != nil { t.Errorf("%s: unexpected error: %v", test.name, err) } err = options.Validate() if test.expectError && err == nil { t.Errorf("%s: unexpected non-error", test.name) } if !test.expectError && err != nil { t.Errorf("%s: unexpected error: %v", test.name, err) } if err != nil { return } pod, _ := options.ExecutablePodFn(tf, test.obj, defaultPodExecTimeout) if pod.Name != test.expectedPod { t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPod, options.PodName) } if options.ContainerName != test.expectedContainer { t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedContainer, options.ContainerName) } if !reflect.DeepEqual(test.expectedArgs, options.Command) { t.Errorf("%s: expected: %v, got %v", test.name, test.expectedArgs, options.Command) } }) } } func TestExec(t *testing.T) { version := "v1" tests := []struct { name, version, podPath, fetchPodPath, execPath string pod *corev1.Pod execErr bool }{ { name: "pod exec", version: version, podPath: "/api/" + version + "/namespaces/test/pods/foo", fetchPodPath: "/namespaces/test/pods/foo", execPath: "/api/" + version + "/namespaces/test/pods/foo/exec", pod: execPod(), }, { name: "pod exec error", version: version, podPath: "/api/" + version + "/namespaces/test/pods/foo", fetchPodPath: "/namespaces/test/pods/foo", execPath: "/api/" + version + "/namespaces/test/pods/foo/exec", pod: execPod(), execErr: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == test.podPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == test.fetchPodPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}} ex := &fakeRemoteExecutor{} if test.execErr { ex.execErr = fmt.Errorf("exec error") } params := &ExecOptions{ StreamOptions: StreamOptions{ PodName: "foo", ContainerName: "bar", IOStreams: genericiooptions.NewTestIOStreamsDiscard(), }, Executor: ex, } cmd := NewCmdExec(tf, genericiooptions.NewTestIOStreamsDiscard()) args := []string{"pod/foo", "--", "command"} if err := params.Complete(tf, cmd, args, 1); err != nil { t.Fatal(err) } err := params.Run() if test.execErr && err != ex.execErr { t.Errorf("%s: Unexpected exec error: %v", test.name, err) return } if !test.execErr && err != nil { t.Errorf("%s: Unexpected error: %v", test.name, err) return } if test.execErr { return } if ex.url.Path != test.execPath { t.Errorf("%s: Did not get expected path for exec request", test.name) return } if strings.Count(ex.url.RawQuery, "container=bar") != 1 { t.Errorf("%s: Did not get expected container query param for exec request", test.name) return } }) } } func execPod() *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, Containers: []corev1.Container{ { Name: "bar", }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, } } func TestSetupTTY(t *testing.T) { streams, _, _, stderr := genericiooptions.NewTestIOStreams() // test 1 - don't attach stdin o := &StreamOptions{ // InterruptParent: , Stdin: false, IOStreams: streams, TTY: true, } tty := o.SetupTTY() if o.In != nil { t.Errorf("don't attach stdin: o.In should be nil") } if tty.In != nil { t.Errorf("don't attach stdin: tty.In should be nil") } if o.TTY { t.Errorf("don't attach stdin: o.TTY should be false") } if tty.Raw { t.Errorf("don't attach stdin: tty.Raw should be false") } if len(stderr.String()) > 0 { t.Errorf("don't attach stdin: stderr wasn't empty: %s", stderr.String()) } // tests from here on attach stdin // test 2 - don't request a TTY o.Stdin = true o.In = &bytes.Buffer{} o.TTY = false tty = o.SetupTTY() if o.In == nil { t.Errorf("attach stdin, no TTY: o.In should not be nil") } if tty.In != o.In { t.Errorf("attach stdin, no TTY: tty.In should equal o.In") } if o.TTY { t.Errorf("attach stdin, no TTY: o.TTY should be false") } if tty.Raw { t.Errorf("attach stdin, no TTY: tty.Raw should be false") } if len(stderr.String()) > 0 { t.Errorf("attach stdin, no TTY: stderr wasn't empty: %s", stderr.String()) } // test 3 - request a TTY, but stdin is not a terminal o.Stdin = true o.In = &bytes.Buffer{} o.ErrOut = stderr o.TTY = true tty = o.SetupTTY() if o.In == nil { t.Errorf("attach stdin, TTY, not a terminal: o.In should not be nil") } if tty.In != o.In { t.Errorf("attach stdin, TTY, not a terminal: tty.In should equal o.In") } if o.TTY { t.Errorf("attach stdin, TTY, not a terminal: o.TTY should be false") } if tty.Raw { t.Errorf("attach stdin, TTY, not a terminal: tty.Raw should be false") } if !strings.Contains(stderr.String(), "input is not a terminal") { t.Errorf("attach stdin, TTY, not a terminal: expected 'input is not a terminal' to stderr") } // test 4 - request a TTY, stdin is a terminal o.Stdin = true o.In = &bytes.Buffer{} stderr.Reset() o.TTY = true overrideStdin := io.NopCloser(&bytes.Buffer{}) overrideStdout := &bytes.Buffer{} overrideStderr := &bytes.Buffer{} o.overrideStreams = func() (io.ReadCloser, io.Writer, io.Writer) { return overrideStdin, overrideStdout, overrideStderr } o.isTerminalIn = func(tty term.TTY) bool { return true } tty = o.SetupTTY() if o.In != overrideStdin { t.Errorf("attach stdin, TTY, is a terminal: o.In should equal overrideStdin") } if tty.In != o.In { t.Errorf("attach stdin, TTY, is a terminal: tty.In should equal o.In") } if !o.TTY { t.Errorf("attach stdin, TTY, is a terminal: o.TTY should be true") } if !tty.Raw { t.Errorf("attach stdin, TTY, is a terminal: tty.Raw should be true") } if len(stderr.String()) > 0 { t.Errorf("attach stdin, TTY, is a terminal: stderr wasn't empty: %s", stderr.String()) } if o.Out != overrideStdout { t.Errorf("attach stdin, TTY, is a terminal: o.Out should equal overrideStdout") } if tty.Out != o.Out { t.Errorf("attach stdin, TTY, is a terminal: tty.Out should equal o.Out") } } func TestCreateExecutor(t *testing.T) { url, err := url.Parse("http://localhost:8080/index.html") if err != nil { t.Fatalf("unable to parse test url: %v", err) } config := cmdtesting.DefaultClientConfig() // First, ensure that no environment variable creates the fallback executor. executor, err := createExecutor(url, config) if err != nil { t.Fatalf("unable to create executor: %v", err) } if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback { t.Errorf("expected fallback executor, got %#v", executor) } // Next, check turning on feature flag explicitly also creates fallback executor. t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true") executor, err = createExecutor(url, config) if err != nil { t.Fatalf("unable to create executor: %v", err) } if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback { t.Errorf("expected fallback executor, got %#v", executor) } // Finally, check explicit disabling does NOT create the fallback executor. t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false") executor, err = createExecutor(url, config) if err != nil { t.Fatalf("unable to create executor: %v", err) } if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback { t.Errorf("expected fallback executor, got %#v", executor) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/explain/000077500000000000000000000000001504711711200262475ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go000066400000000000000000000170661504711711200302500ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" openapiclient "k8s.io/client-go/openapi" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/explain" openapiv3explain "k8s.io/kubectl/pkg/explain/v2" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/util/templates" ) var ( explainLong = templates.LongDesc(i18n.T(` Describe fields and structure of various resources. This command describes the fields associated with each supported API resource. Fields are identified via a simple JSONPath identifier: .[.] Information about each field is retrieved from the server in OpenAPI format.`)) explainExamples = templates.Examples(i18n.T(` # Get the documentation of the resource and its fields kubectl explain pods # Get all the fields in the resource kubectl explain pods --recursive # Get the explanation for deployment in supported api versions kubectl explain deployments --api-version=apps/v1 # Get the documentation of a specific field of a resource kubectl explain pods.spec.containers # Get the documentation of resources in different format kubectl explain deployment --output=plaintext-openapiv2`)) ) const ( plaintextTemplateName = "plaintext" plaintextOpenAPIV2TemplateName = "plaintext-openapiv2" ) // ExplainFlags directly reflect the information that CLI is gathering via flags. // They will be converted to Options, which reflect the runtime requirements for // the command. type ExplainFlags struct { APIVersion string OutputFormat string Recursive bool genericiooptions.IOStreams } // NewExplainFlags returns a default ExplainFlags func NewExplainFlags(streams genericiooptions.IOStreams) *ExplainFlags { return &ExplainFlags{ OutputFormat: plaintextTemplateName, IOStreams: streams, } } // AddFlags registers flags for a cli func (flags *ExplainFlags) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&flags.Recursive, "recursive", flags.Recursive, "Print the fields of fields (Currently only 1 level deep)") cmd.Flags().StringVar(&flags.APIVersion, "api-version", flags.APIVersion, "Get different explanations for particular API version (API group/version)") cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", plaintextTemplateName, "Format in which to render the schema (plaintext, plaintext-openapiv2)") } // ToOptions converts from CLI inputs to runtime input func (flags *ExplainFlags) ToOptions(f cmdutil.Factory, parent string, args []string) (*ExplainOptions, error) { mapper, err := f.ToRESTMapper() if err != nil { return nil, err } // Only openapi v3 needs the discovery client. openAPIV3Client, err := f.OpenAPIV3Client() if err != nil { return nil, err } o := &ExplainOptions{ IOStreams: flags.IOStreams, Recursive: flags.Recursive, APIVersion: flags.APIVersion, OutputFormat: flags.OutputFormat, CmdParent: parent, args: args, Mapper: mapper, openAPIGetter: f, OpenAPIV3Client: openAPIV3Client, } return o, nil } // NewCmdExplain returns a cobra command for swagger docs func NewCmdExplain(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { flags := NewExplainFlags(streams) cmd := &cobra.Command{ Use: "explain TYPE [--recursive=FALSE|TRUE] [--api-version=api-version-group] [-o|--output=plaintext|plaintext-openapiv2]", DisableFlagsInUseLine: true, Short: i18n.T("Get documentation for a resource"), Long: explainLong + "\n\n" + cmdutil.SuggestAPIResources(parent), Example: explainExamples, Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions(f, parent, args) cmdutil.CheckErr(err) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } flags.AddFlags(cmd) return cmd } type ExplainOptions struct { genericiooptions.IOStreams Recursive bool APIVersion string // Name of the template to use with the openapiv3 template renderer. OutputFormat string CmdParent string args []string Mapper meta.RESTMapper openAPIGetter openapi.OpenAPIResourcesGetter // Client capable of fetching openapi documents from the user's cluster OpenAPIV3Client openapiclient.Client } func (o *ExplainOptions) Validate() error { if len(o.args) == 0 { return fmt.Errorf("You must specify the type of resource to explain. %s\n", cmdutil.SuggestAPIResources(o.CmdParent)) } if len(o.args) > 1 { return fmt.Errorf("We accept only this format: explain RESOURCE\n") } return nil } // Run executes the appropriate steps to print a model's documentation func (o *ExplainOptions) Run() error { var fullySpecifiedGVR schema.GroupVersionResource var fieldsPath []string var err error if len(o.APIVersion) == 0 { fullySpecifiedGVR, fieldsPath, err = explain.SplitAndParseResourceRequestWithMatchingPrefix(o.args[0], o.Mapper) if err != nil { return err } } else { // TODO: After we figured out the new syntax to separate group and resource, allow // the users to use it in explain (kubectl explain ). // Refer to issue #16039 for why we do this. Refer to PR #15808 that used "/" syntax. fullySpecifiedGVR, fieldsPath, err = explain.SplitAndParseResourceRequest(o.args[0], o.Mapper) if err != nil { return err } } // Fallback to openapiv2 implementation using special template name switch o.OutputFormat { case plaintextOpenAPIV2TemplateName: return o.renderOpenAPIV2(fullySpecifiedGVR, fieldsPath) case plaintextTemplateName: // Check whether the server reponds to OpenAPIV3. if _, err := o.OpenAPIV3Client.Paths(); err != nil { // Use v2 renderer if server does not support v3 return o.renderOpenAPIV2(fullySpecifiedGVR, fieldsPath) } fallthrough default: if len(o.APIVersion) > 0 { apiVersion, err := schema.ParseGroupVersion(o.APIVersion) if err != nil { return err } fullySpecifiedGVR.Group = apiVersion.Group fullySpecifiedGVR.Version = apiVersion.Version } return openapiv3explain.PrintModelDescription( fieldsPath, o.Out, o.OpenAPIV3Client, fullySpecifiedGVR, o.Recursive, o.OutputFormat, ) } } func (o *ExplainOptions) renderOpenAPIV2( fullySpecifiedGVR schema.GroupVersionResource, fieldsPath []string, ) error { var err error gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR) if gvk.Empty() { gvk, err = o.Mapper.KindFor(fullySpecifiedGVR.GroupResource().WithVersion("")) if err != nil { return err } } if len(o.APIVersion) != 0 { apiVersion, err := schema.ParseGroupVersion(o.APIVersion) if err != nil { return err } gvk = apiVersion.WithKind(gvk.Kind) } resources, err := o.openAPIGetter.OpenAPISchema() if err != nil { return err } schema := resources.LookupResource(gvk) if schema == nil { return fmt.Errorf("couldn't find resource for %q", gvk) } return explain.PrintModelDescription(fieldsPath, o.Out, schema, gvk, o.Recursive) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain_test.go000066400000000000000000000234621504711711200313040ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain_test import ( "errors" "path/filepath" "regexp" "testing" "k8s.io/apimachinery/pkg/api/meta" sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/discovery" openapiclient "k8s.io/client-go/openapi" "k8s.io/client-go/rest" clienttestutil "k8s.io/client-go/util/testing" "k8s.io/kubectl/pkg/cmd/explain" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/openapi" ) var ( testDataPath = filepath.Join("..", "..", "..", "testdata") fakeSchema = sptest.Fake{Path: filepath.Join(testDataPath, "openapi", "swagger.json")} FakeOpenAPISchema = testOpenAPISchema{ OpenAPISchemaFn: func() (openapi.Resources, error) { s, err := fakeSchema.OpenAPISchema() if err != nil { return nil, err } return openapi.NewOpenAPIData(s) }, } ) type testOpenAPISchema struct { OpenAPISchemaFn func() (openapi.Resources, error) } func TestExplainInvalidArgs(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() flags := explain.NewExplainFlags(genericiooptions.NewTestIOStreamsDiscard()) opts, err := flags.ToOptions(tf, "kubectl", []string{}) if err != nil { t.Fatalf("unexpected error %v", err) } err = opts.Validate() if err.Error() != "You must specify the type of resource to explain. Use \"kubectl api-resources\" for a complete list of supported resources.\n" { t.Error("unexpected non-error") } opts, err = flags.ToOptions(tf, "kubectl", []string{"resource1", "resource2"}) if err != nil { t.Fatalf("unexpected error %v", err) } err = opts.Validate() if err.Error() != "We accept only this format: explain RESOURCE\n" { t.Error("unexpected non-error") } } func TestExplainNotExistResource(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() flags := explain.NewExplainFlags(genericiooptions.NewTestIOStreamsDiscard()) opts, err := flags.ToOptions(tf, "kubectl", []string{"foo"}) if err != nil { t.Fatalf("unexpected error %v", err) } err = opts.Validate() if err != nil { t.Fatalf("unexpected error %v", err) } err = opts.Run() if _, ok := err.(*meta.NoResourceMatchError); !ok { t.Fatalf("unexpected error %v", err) } } type explainTestCase struct { Name string Args []string Flags map[string]string ExpectPattern []string ExpectErrorPattern string // Custom OpenAPI V3 client to use for the test. If nil, a default one will // be provided OpenAPIV3SchemaFn func() (openapiclient.Client, error) } var explainV2Cases = []explainTestCase{ { Name: "Basic", Args: []string{"pods"}, ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`}, }, { Name: "Recursive", Args: []string{"pods"}, Flags: map[string]string{"recursive": "true"}, ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`}, }, { Name: "DefaultAPIVersion", Args: []string{"horizontalpodautoscalers"}, Flags: map[string]string{"api-version": "autoscaling/v1"}, ExpectPattern: []string{`\s*VERSION:[\t ]*(v1|autoscaling/v1)\s*`}, }, { Name: "NonExistingAPIVersion", Args: []string{"pods"}, Flags: map[string]string{"api-version": "v99"}, ExpectErrorPattern: `couldn't find resource for \"/v99, (Kind=Pod|Resource=pods)\"`, }, { Name: "NonExistingResource", Args: []string{"foo"}, ExpectErrorPattern: `the server doesn't have a resource type "foo"`, }, } func TestExplainOpenAPIV2(t *testing.T) { runExplainTestCases(t, explainV2Cases) } func TestExplainOpenAPIV3(t *testing.T) { fallbackV3SchemaFn := func() (openapiclient.Client, error) { fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: "https://not.a.real.site:65543/"}) return fakeDiscoveryClient.OpenAPIV3(), nil } // Returns a client that causes fallback to v2 implementation cases := []explainTestCase{ { // No --output, but OpenAPIV3 enabled should fall back to v2 if // v2 is not available. Shows this by making openapiv3 client // point to a bad URL. So the fact the proper data renders is // indication v2 was used instead. Name: "Fallback", Args: []string{"pods"}, ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`}, OpenAPIV3SchemaFn: fallbackV3SchemaFn, }, { Name: "NonDefaultAPIVersion", Args: []string{"horizontalpodautoscalers"}, Flags: map[string]string{"api-version": "autoscaling/v2"}, ExpectPattern: []string{`\s*VERSION:[\t ]*(v2|autoscaling/v2)\s*`}, }, { // Show that explicitly specifying --output plaintext-openapiv2 causes // old implementation to be used even though OpenAPIV3 is enabled Name: "OutputPlaintextV2", Args: []string{"pods"}, Flags: map[string]string{"output": "plaintext-openapiv2"}, ExpectPattern: []string{`\s*KIND:[\t ]*Pod\s*`}, OpenAPIV3SchemaFn: fallbackV3SchemaFn, }, } cases = append(cases, explainV2Cases...) runExplainTestCases(t, cases) } func runExplainTestCases(t *testing.T, cases []explainTestCase) { fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3")) if err != nil { t.Fatalf("error starting fake openapi server: %v", err.Error()) } defer fakeServer.HttpServer.Close() openapiV3SchemaFn := func() (openapiclient.Client, error) { fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL}) return fakeDiscoveryClient.OpenAPIV3(), nil } tf := cmdtesting.NewTestFactory() defer tf.Cleanup() tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() type catchFatal error for _, tcase := range cases { t.Run(tcase.Name, func(t *testing.T) { // Catch os.Exit calls for tests which expect them // and replace them with panics that we catch in each test // to check if it is expected. cmdutil.BehaviorOnFatal(func(str string, code int) { panic(catchFatal(errors.New(str))) }) defer cmdutil.DefaultBehaviorOnFatal() var err error func() { defer func() { // Catch panic and check at end of test if it is // expected. if panicErr := recover(); panicErr != nil { if e := panicErr.(catchFatal); e != nil { err = e } else { panic(panicErr) } } }() if tcase.OpenAPIV3SchemaFn != nil { tf.OpenAPIV3ClientFunc = tcase.OpenAPIV3SchemaFn } else { tf.OpenAPIV3ClientFunc = openapiV3SchemaFn } cmd := explain.NewCmdExplain("kubectl", tf, ioStreams) for k, v := range tcase.Flags { if err := cmd.Flags().Set(k, v); err != nil { t.Fatal(err) } } cmd.Run(cmd, tcase.Args) }() for _, rexp := range tcase.ExpectPattern { if matched, err := regexp.MatchString(rexp, buf.String()); err != nil || !matched { if err != nil { t.Error(err) } else { t.Errorf("expected output to match regex:\n\t%s\ninstead got:\n\t%s", rexp, buf.String()) } } } if err != nil { if matched, regexErr := regexp.MatchString(tcase.ExpectErrorPattern, err.Error()); len(tcase.ExpectErrorPattern) == 0 || regexErr != nil || !matched { t.Fatalf("unexpected error: %s did not match regex %s (%v)", err.Error(), tcase.ExpectErrorPattern, regexErr) } } else if len(tcase.ExpectErrorPattern) > 0 { t.Fatalf("did not trigger expected error: %s in output:\n%s", tcase.ExpectErrorPattern, buf.String()) } }) buf.Reset() } } // OpenAPI V2 specifications retrieval -- should never be called. func panicOpenAPISchemaFn() (openapi.Resources, error) { panic("should never be called") } // OpenAPI V3 specifications retrieval does *not* retrieve V2 specifications. func TestExplainOpenAPIV3DoesNotLoadOpenAPIV2Specs(t *testing.T) { // Set up OpenAPI V3 specifications endpoint for explain. fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3")) if err != nil { t.Fatalf("error starting fake openapi server: %v", err.Error()) } defer fakeServer.HttpServer.Close() tf := cmdtesting.NewTestFactory() defer tf.Cleanup() tf.OpenAPIV3ClientFunc = func() (openapiclient.Client, error) { fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL}) return fakeDiscoveryClient.OpenAPIV3(), nil } // OpenAPI V2 specifications retrieval will panic if called. tf.OpenAPISchemaFunc = panicOpenAPISchemaFn // Explain the following resources, validating the command does not panic. cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard()) resources := []string{"pods", "services", "endpoints", "configmaps"} for _, resource := range resources { cmd.Run(cmd, []string{resource}) } // Verify retrieving OpenAPI V2 specifications will panic. defer func() { if panicErr := recover(); panicErr == nil { t.Fatal("expecting panic for openapi v2 retrieval") } }() // Set OpenAPI V2 output flag for explain. if err := cmd.Flags().Set("output", "plaintext-openapiv2"); err != nil { t.Fatal(err) } cmd.Run(cmd, []string{"pods"}) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/expose/000077500000000000000000000000001504711711200261125ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose.go000066400000000000000000000524421504711711200277530ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package expose import ( "fmt" "regexp" "strconv" "strings" "github.com/spf13/cobra" "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( exposeResources = i18n.T(`pod (po), service (svc), replicationcontroller (rc), deployment (deploy), replicaset (rs)`) exposeLong = templates.LongDesc(i18n.T(` Expose a resource as a new Kubernetes service. Looks up a deployment, service, replica set, replication controller or pod by name and uses the selector for that resource as the selector for a new service on the specified port. A deployment or replica set will be exposed as a service only if its selector is convertible to a selector that service supports, i.e. when the selector contains only the matchLabels component. Note that if no port is specified via --port and the exposed resource has multiple ports, all will be re-used by the new service. Also if no labels are specified, the new service will re-use the labels from the resource it exposes. Possible resources include (case insensitive): `) + exposeResources) exposeExample = templates.Examples(i18n.T(` # Create a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000 kubectl expose rc nginx --port=80 --target-port=8000 # Create a service for a replication controller identified by type and name specified in "nginx-controller.yaml", which serves on port 80 and connects to the containers on port 8000 kubectl expose -f nginx-controller.yaml --port=80 --target-port=8000 # Create a service for a pod valid-pod, which serves on port 444 with the name "frontend" kubectl expose pod valid-pod --port=444 --name=frontend # Create a second service based on the above service, exposing the container port 8443 as port 443 with the name "nginx-https" kubectl expose service nginx --port=443 --target-port=8443 --name=nginx-https # Create a service for a replicated streaming application on port 4100 balancing UDP traffic and named 'video-stream'. kubectl expose rc streamer --port=4100 --protocol=UDP --name=video-stream # Create a service for a replicated nginx using replica set, which serves on port 80 and connects to the containers on port 8000 kubectl expose rs nginx --port=80 --target-port=8000 # Create a service for an nginx deployment, which serves on port 80 and connects to the containers on port 8000 kubectl expose deployment nginx --port=80 --target-port=8000`)) ) // ExposeServiceOptions holds the options for kubectl expose command type ExposeServiceOptions struct { cmdutil.OverrideOptions FilenameOptions resource.FilenameOptions RecordFlags *genericclioptions.RecordFlags PrintFlags *genericclioptions.PrintFlags PrintObj printers.ResourcePrinterFunc Name string DefaultName string Selector string // Port will be used if a user specifies --port OR the exposed object as one port Port string // Ports will be used iff a user doesn't specify --port AND the exposed object has multiple ports Ports string Labels string ExternalIP string LoadBalancerIP string Type string Protocol string // Protocols will be used to keep port-protocol mapping derived from exposed object Protocols string TargetPort string PortName string SessionAffinity string ClusterIP string DryRunStrategy cmdutil.DryRunStrategy EnforceNamespace bool fieldManager string CanBeExposed polymorphichelpers.CanBeExposedFunc MapBasedSelectorForObject func(runtime.Object) (string, error) PortsForObject polymorphichelpers.PortsForObjectFunc ProtocolsForObject polymorphichelpers.MultiProtocolsWithForObjectFunc Namespace string Mapper meta.RESTMapper Builder *resource.Builder ClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) Recorder genericclioptions.Recorder genericiooptions.IOStreams } // exposeServiceFlags is a struct that contains the user input flags to the command. type ExposeServiceFlags struct { cmdutil.OverrideOptions PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags fieldManager string Protocol string // Port will be used if a user specifies --port OR the exposed object as one port Port string Type string LoadBalancerIP string Selector string Labels string TargetPort string ExternalIP string Name string SessionAffinity string ClusterIP string Recorder genericclioptions.Recorder FilenameOptions resource.FilenameOptions genericiooptions.IOStreams } func NewExposeFlags(ioStreams genericiooptions.IOStreams) *ExposeServiceFlags { return &ExposeServiceFlags{ RecordFlags: genericclioptions.NewRecordFlags(), PrintFlags: genericclioptions.NewPrintFlags("exposed").WithTypeSetter(scheme.Scheme), Recorder: genericclioptions.NoopRecorder{}, IOStreams: ioStreams, } } // NewCmdExposeService is a command to expose the service from user's input func NewCmdExposeService(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { flags := NewExposeFlags(streams) validArgs := []string{} resources := regexp.MustCompile(`\s*,`).Split(exposeResources, -1) for _, r := range resources { validArgs = append(validArgs, strings.Fields(r)[0]) } cmd := &cobra.Command{ Use: "expose (-f FILENAME | TYPE NAME) [--port=port] [--protocol=TCP|UDP|SCTP] [--target-port=number-or-name] [--name=name] [--external-ip=external-ip-of-service] [--type=type]", DisableFlagsInUseLine: true, Short: i18n.T("Take a replication controller, service, deployment or pod and expose it as a new Kubernetes service"), Long: exposeLong, Example: exposeExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions(cmd, args) cmdutil.CheckErr(err) cmdutil.CheckErr(o.Complete(f)) cmdutil.CheckErr(o.RunExpose(cmd, args)) }, } flags.AddFlags(cmd) return cmd } func (flags *ExposeServiceFlags) AddFlags(cmd *cobra.Command) { flags.PrintFlags.AddFlags(cmd) flags.RecordFlags.AddFlags(cmd) cmd.Flags().StringVar(&flags.Protocol, "protocol", flags.Protocol, i18n.T("The network protocol for the service to be created. Default is 'TCP'.")) cmd.Flags().StringVar(&flags.Port, "port", flags.Port, i18n.T("The port that the service should serve on. Copied from the resource being exposed, if unspecified")) cmd.Flags().StringVar(&flags.Type, "type", flags.Type, i18n.T("Type for this service: ClusterIP, NodePort, LoadBalancer, or ExternalName. Default is 'ClusterIP'.")) cmd.Flags().StringVar(&flags.LoadBalancerIP, "load-balancer-ip", flags.LoadBalancerIP, i18n.T("IP to assign to the LoadBalancer. If empty, an ephemeral IP will be created and used (cloud-provider specific).")) cmd.Flags().StringVar(&flags.Selector, "selector", flags.Selector, i18n.T("A label selector to use for this service. Only equality-based selector requirements are supported. If empty (the default) infer the selector from the replication controller or replica set.)")) cmd.Flags().StringVarP(&flags.Labels, "labels", "l", flags.Labels, "Labels to apply to the service created by this call.") cmd.Flags().StringVar(&flags.TargetPort, "target-port", flags.TargetPort, i18n.T("Name or number for the port on the container that the service should direct traffic to. Optional.")) cmd.Flags().StringVar(&flags.ExternalIP, "external-ip", flags.ExternalIP, i18n.T("Additional external IP address (not managed by Kubernetes) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP.")) cmd.Flags().StringVar(&flags.Name, "name", flags.Name, i18n.T("The name for the newly created object.")) cmd.Flags().StringVar(&flags.SessionAffinity, "session-affinity", flags.SessionAffinity, i18n.T("If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'")) cmd.Flags().StringVar(&flags.ClusterIP, "cluster-ip", flags.ClusterIP, i18n.T("ClusterIP to be assigned to the service. Leave empty to auto-allocate, or set to 'None' to create a headless service.")) cmdutil.AddFieldManagerFlagVar(cmd, &flags.fieldManager, "kubectl-expose") flags.AddOverrideFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmdutil.AddApplyAnnotationFlags(cmd) usage := "identifying the resource to expose a service" cmdutil.AddFilenameOptionFlags(cmd, &flags.FilenameOptions, usage) } func (flags *ExposeServiceFlags) ToOptions(cmd *cobra.Command, args []string) (*ExposeServiceOptions, error) { dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd) if err != nil { return nil, err } cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStrategy) printer, err := flags.PrintFlags.ToPrinter() if err != nil { return nil, err } flags.RecordFlags.Complete(cmd) recorder, err := flags.RecordFlags.ToRecorder() if err != nil { return nil, err } e := &ExposeServiceOptions{ DryRunStrategy: dryRunStrategy, PrintObj: printer.PrintObj, Recorder: recorder, IOStreams: flags.IOStreams, fieldManager: flags.fieldManager, PrintFlags: flags.PrintFlags, RecordFlags: flags.RecordFlags, FilenameOptions: flags.FilenameOptions, Protocol: flags.Protocol, Port: flags.Port, Type: flags.Type, LoadBalancerIP: flags.LoadBalancerIP, Selector: flags.Selector, Labels: flags.Labels, TargetPort: flags.TargetPort, ExternalIP: flags.ExternalIP, Name: flags.Name, SessionAffinity: flags.SessionAffinity, ClusterIP: flags.ClusterIP, OverrideOptions: flags.OverrideOptions, } return e, nil } // Complete loads data from the command line environment func (o *ExposeServiceOptions) Complete(f cmdutil.Factory) error { var err error o.Builder = f.NewBuilder() o.ClientForMapping = f.ClientForMapping o.CanBeExposed = polymorphichelpers.CanBeExposedFn o.MapBasedSelectorForObject = polymorphichelpers.MapBasedSelectorForObjectFn o.ProtocolsForObject = polymorphichelpers.MultiProtocolsForObjectFn o.PortsForObject = polymorphichelpers.PortsForObjectFn o.Mapper, err = f.ToRESTMapper() if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } return err } // RunExpose retrieves the Kubernetes Object from the API server and expose it to a // Kubernetes Service func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) error { r := o.Builder. WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(false, args...). Flatten(). Do() err := r.Err() if err != nil { return err } err = r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } mapping := info.ResourceMapping() if err := o.CanBeExposed(mapping.GroupVersionKind.GroupKind()); err != nil { return err } name := info.Name if len(name) > validation.DNS1035LabelMaxLength { name = name[:validation.DNS1035LabelMaxLength] } o.DefaultName = name // For objects that need a pod selector, derive it from the exposed object in case a user // didn't explicitly specify one via --selector if len(o.Selector) == 0 { s, err := o.MapBasedSelectorForObject(info.Object) if err != nil { return fmt.Errorf("couldn't retrieve selectors via --selector flag or introspection: %v", err) } o.Selector = s } isHeadlessService := o.ClusterIP == "None" // For objects that need a port, derive it from the exposed object in case a user // didn't explicitly specify one via --port if len(o.Port) == 0 { ports, err := o.PortsForObject(info.Object) if err != nil { return fmt.Errorf("couldn't find port via --port flag or introspection: %v", err) } switch len(ports) { case 0: if !isHeadlessService { return fmt.Errorf("couldn't find port via --port flag or introspection") } case 1: o.Port = ports[0] default: o.Ports = strings.Join(ports, ",") } } // Always try to derive protocols from the exposed object, may use // different protocols for different ports. protocolsMap, err := o.ProtocolsForObject(info.Object) if err != nil { return fmt.Errorf("couldn't find protocol via introspection: %v", err) } if protocols := makeProtocols(protocolsMap); len(protocols) > 0 { o.Protocols = protocols } if len(o.Labels) == 0 { labels, err := meta.NewAccessor().Labels(info.Object) if err != nil { return err } o.Labels = polymorphichelpers.MakeLabels(labels) } // Generate new object service, err := o.createService() if err != nil { return err } overrideService, err := o.NewOverrider(&corev1.Service{}).Apply(service) if err != nil { return err } if err := o.Recorder.Record(overrideService); err != nil { klog.V(4).Infof("error recording current command: %v", err) } if o.DryRunStrategy == cmdutil.DryRunClient { if meta, err := meta.Accessor(overrideService); err == nil && o.EnforceNamespace { meta.SetNamespace(o.Namespace) } return o.PrintObj(overrideService, o.Out) } if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), overrideService, scheme.DefaultJSONEncoder()); err != nil { return err } asUnstructured := &unstructured.Unstructured{} if err := scheme.Scheme.Convert(overrideService, asUnstructured, nil); err != nil { return err } gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(asUnstructured) if err != nil { return err } objMapping, err := o.Mapper.RESTMapping(gvks[0].GroupKind(), gvks[0].Version) if err != nil { return err } // Serialize the object with the annotation applied. client, err := o.ClientForMapping(objMapping) if err != nil { return err } actualObject, err := resource. NewHelper(client, objMapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Create(o.Namespace, false, asUnstructured) if err != nil { return err } return o.PrintObj(actualObject, o.Out) }) return err } func (o *ExposeServiceOptions) createService() (*corev1.Service, error) { if len(o.Selector) == 0 { return nil, fmt.Errorf("selector must be specified") } selector, err := parseLabels(o.Selector) if err != nil { return nil, err } var labels map[string]string if len(o.Labels) > 0 { labels, err = parseLabels(o.Labels) if err != nil { return nil, err } } name := o.Name if len(name) == 0 { name = o.DefaultName if len(name) == 0 { return nil, fmt.Errorf("name must be specified") } } var portProtocolMap map[string][]string if o.Protocols != "" { portProtocolMap, err = parseProtocols(o.Protocols) if err != nil { return nil, err } } // ports takes precedence over port since it will be // specified only when the user hasn't specified a port // via --port and the exposed object has multiple ports. var portString string portString = o.Ports if len(o.Ports) == 0 { portString = o.Port } ports := []corev1.ServicePort{} if len(portString) != 0 { portStringSlice := strings.Split(portString, ",") servicePortName := o.PortName for i, stillPortString := range portStringSlice { port, err := strconv.Atoi(stillPortString) if err != nil { return nil, err } name := servicePortName // If we are going to assign multiple ports to a service, we need to // generate a different name for each one. if len(portStringSlice) > 1 { name = fmt.Sprintf("port-%d", i+1) } protocol := o.Protocol switch { case len(protocol) == 0 && len(portProtocolMap) == 0: // Default to TCP, what the flag was doing previously. protocol = "TCP" case len(protocol) > 0 && len(portProtocolMap) > 0: // User has specified the --protocol while exposing a multiprotocol resource // We should stomp multiple protocols with the one specified ie. do nothing case len(protocol) == 0 && len(portProtocolMap) > 0: // no --protocol and we expose a multiprotocol resource protocol = "TCP" // have the default so we can stay sane if exposeProtocols, found := portProtocolMap[stillPortString]; found { if len(exposeProtocols) == 1 { protocol = exposeProtocols[0] break } for _, exposeProtocol := range exposeProtocols { name := fmt.Sprintf("port-%d-%s", i+1, strings.ToLower(exposeProtocol)) ports = append(ports, corev1.ServicePort{ Name: name, Port: int32(port), Protocol: corev1.Protocol(exposeProtocol), }) } continue } } ports = append(ports, corev1.ServicePort{ Name: name, Port: int32(port), Protocol: corev1.Protocol(protocol), }) } } service := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: labels, }, Spec: corev1.ServiceSpec{ Selector: selector, Ports: ports, }, } targetPortString := o.TargetPort if len(targetPortString) > 0 { targetPort := intstr.Parse(targetPortString) // Use the same target-port for every port for i := range service.Spec.Ports { service.Spec.Ports[i].TargetPort = targetPort } } else { // If --target-port or --container-port haven't been specified, this // should be the same as Port for i := range service.Spec.Ports { port := service.Spec.Ports[i].Port service.Spec.Ports[i].TargetPort = intstr.FromInt32(port) } } if len(o.ExternalIP) > 0 { service.Spec.ExternalIPs = []string{o.ExternalIP} } if len(o.Type) != 0 { service.Spec.Type = corev1.ServiceType(o.Type) } if service.Spec.Type == corev1.ServiceTypeLoadBalancer { service.Spec.LoadBalancerIP = o.LoadBalancerIP } if len(o.SessionAffinity) != 0 { switch corev1.ServiceAffinity(o.SessionAffinity) { case corev1.ServiceAffinityNone: service.Spec.SessionAffinity = corev1.ServiceAffinityNone case corev1.ServiceAffinityClientIP: service.Spec.SessionAffinity = corev1.ServiceAffinityClientIP default: return nil, fmt.Errorf("unknown session affinity: %s", o.SessionAffinity) } } if len(o.ClusterIP) != 0 { if o.ClusterIP == "None" { service.Spec.ClusterIP = corev1.ClusterIPNone } else { service.Spec.ClusterIP = o.ClusterIP } } return &service, nil } // parseLabels turns a string representation of a label set into a map[string]string func parseLabels(labelSpec string) (map[string]string, error) { if len(labelSpec) == 0 { return nil, fmt.Errorf("no label spec passed") } labels := map[string]string{} labelSpecs := strings.Split(labelSpec, ",") for ix := range labelSpecs { labelSpec := strings.Split(labelSpecs[ix], "=") if len(labelSpec) != 2 { return nil, fmt.Errorf("unexpected label spec: %s", labelSpecs[ix]) } if len(labelSpec[0]) == 0 { return nil, fmt.Errorf("unexpected empty label key") } labels[labelSpec[0]] = labelSpec[1] } return labels, nil } func makeProtocols(protocols map[string][]string) string { var out []string for key, value := range protocols { for _, s := range value { out = append(out, fmt.Sprintf("%s/%s", key, s)) } } return strings.Join(out, ",") } // parseProtocols turns a string representation of a protocols set into a map[string]string func parseProtocols(protocols string) (map[string][]string, error) { if len(protocols) == 0 { return nil, fmt.Errorf("no protocols passed") } portProtocolMap := map[string][]string{} protocolsSlice := strings.Split(protocols, ",") for ix := range protocolsSlice { portProtocol := strings.Split(protocolsSlice[ix], "/") if len(portProtocol) != 2 { return nil, fmt.Errorf("unexpected port protocol mapping: %s", protocolsSlice[ix]) } if len(portProtocol[0]) == 0 { return nil, fmt.Errorf("unexpected empty port") } if len(portProtocol[1]) == 0 { return nil, fmt.Errorf("unexpected empty protocol") } port := portProtocol[0] portProtocolMap[port] = append(portProtocolMap[port], portProtocol[1]) } return portProtocolMap, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose_test.go000066400000000000000000001344061504711711200310130ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package expose import ( "net/http" "strings" "testing" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestRunExposeService(t *testing.T) { tests := []struct { name string args []string ns string calls map[string]string input runtime.Object flags map[string]string output runtime.Object expected string status int }{ { name: "expose-service-from-service-no-selector-defined", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"app": "go"}, }, }, expected: "service/foo exposed", status: 200, }, { name: "expose-service-from-service", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, }, }, expected: "service/foo exposed", status: 200, }, { name: "no-name-passed-from-the-cli", args: []string{"service", "mayor"}, ns: "default", calls: map[string]string{ "GET": "/namespaces/default/services/mayor", "POST": "/namespaces/default/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "mayor", Namespace: "default", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"run": "this"}, }, }, // No --name flag specified below. Service will use the rc's name passed via the 'default-name' parameter flags: map[string]string{"selector": "run=this", "port": "80", "labels": "runas=amayor"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "mayor", Namespace: "", Labels: map[string]string{"runas": "amayor"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolTCP, Port: 80, TargetPort: intstr.FromInt32(80), }, }, Selector: map[string]string{"run": "this"}, }, }, expected: "service/mayor exposed", status: 200, }, { name: "expose-service", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "type": "LoadBalancer", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, Type: corev1.ServiceTypeLoadBalancer, }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "expose-affinity-service", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "type": "LoadBalancer", "session-affinity": "ClientIP", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, Type: corev1.ServiceTypeLoadBalancer, SessionAffinity: corev1.ServiceAffinityClientIP, }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "expose-service-cluster-ip", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "cluster-ip": "10.10.10.10", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, ClusterIP: "10.10.10.10", }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "expose-headless-service", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "cluster-ip": "None", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, ClusterIP: corev1.ClusterIPNone, }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "expose-headless-service-no-port", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "name": "foo", "labels": "svc=test", "cluster-ip": "None", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{}, Selector: map[string]string{"func": "stream"}, ClusterIP: corev1.ClusterIPNone, }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "expose-from-file", args: []string{}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/redis-master", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "redis-master", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"filename": "../../../testdata/redis-master-service.yaml", "selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "truncate-name", args: []string{"pod", "a-name-that-is-toooo-big-for-a-service-because-it-can-only-handle-63-characters"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/pods/a-name-that-is-toooo-big-for-a-service-because-it-can-only-handle-63-characters", "POST": "/namespaces/test/services", }, input: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, }, flags: map[string]string{"selector": "svc=frompod", "port": "90", "labels": "svc=frompod", "generator": "service/v2"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "a-name-that-is-toooo-big-for-a-service-because-it-can-only-handle-63-characters"[:63], Namespace: "", Labels: map[string]string{"svc": "frompod"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolTCP, Port: 90, TargetPort: intstr.FromInt32(90), }, }, Selector: map[string]string{"svc": "frompod"}, }, }, expected: "service/a-name-that-is-toooo-big-for-a-service-because-it-can-only-hand exposed", status: 200, }, { name: "expose-multiport-object", args: []string{"service", "foo"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/foo", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "multiport"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolTCP, Port: 80, TargetPort: intstr.FromInt32(80), }, { Protocol: corev1.ProtocolTCP, Port: 443, TargetPort: intstr.FromInt32(443), }, }, }, }, flags: map[string]string{"selector": "svc=fromfoo", "generator": "service/v2", "name": "fromfoo", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "fromfoo", Namespace: "", Labels: map[string]string{"svc": "multiport"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Name: "port-1", Protocol: corev1.ProtocolTCP, Port: 80, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Protocol: corev1.ProtocolTCP, Port: 443, TargetPort: intstr.FromInt32(443), }, }, Selector: map[string]string{"svc": "fromfoo"}, }, }, expected: "service/fromfoo exposed (dry run)", status: 200, }, { name: "expose-multiprotocol-object", args: []string{"service", "foo"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/foo", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "multiport"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolTCP, Port: 80, TargetPort: intstr.FromInt32(80), }, { Protocol: corev1.ProtocolUDP, Port: 8080, TargetPort: intstr.FromInt32(8080), }, { Protocol: corev1.ProtocolUDP, Port: 8081, TargetPort: intstr.FromInt32(8081), }, { Protocol: corev1.ProtocolSCTP, Port: 8082, TargetPort: intstr.FromInt32(8082), }, }, }, }, flags: map[string]string{"selector": "svc=fromfoo", "generator": "service/v2", "name": "fromfoo", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "fromfoo", Namespace: "", Labels: map[string]string{"svc": "multiport"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Name: "port-1", Protocol: corev1.ProtocolTCP, Port: 80, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Protocol: corev1.ProtocolUDP, Port: 8080, TargetPort: intstr.FromInt32(8080), }, { Name: "port-3", Protocol: corev1.ProtocolUDP, Port: 8081, TargetPort: intstr.FromInt32(8081), }, { Name: "port-4", Protocol: corev1.ProtocolSCTP, Port: 8082, TargetPort: intstr.FromInt32(8082), }, }, Selector: map[string]string{"svc": "fromfoo"}, }, }, expected: "service/fromfoo exposed (dry run)", status: 200, }, { name: "expose-service-from-service-no-selector-defined-sctp", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"protocol": "SCTP", "port": "14", "name": "foo", "labels": "svc=test"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolSCTP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"app": "go"}, }, }, expected: "service/foo exposed", status: 200, }, { name: "expose-service-from-service-sctp", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "SCTP", "port": "14", "name": "foo", "labels": "svc=test"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolSCTP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, }, }, expected: "service/foo exposed", status: 200, }, { name: "expose-service-cluster-ip-sctp", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "SCTP", "port": "14", "name": "foo", "labels": "svc=test", "cluster-ip": "10.10.10.10", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolSCTP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, ClusterIP: "10.10.10.10", }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "expose-headless-service-sctp", args: []string{"service", "baz"}, ns: "test", calls: map[string]string{ "GET": "/namespaces/test/services/baz", "POST": "/namespaces/test/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "SCTP", "port": "14", "name": "foo", "labels": "svc=test", "cluster-ip": "None", "dry-run": "client"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolSCTP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, ClusterIP: corev1.ClusterIPNone, }, }, expected: "service/foo exposed (dry run)", status: 200, }, { name: "namespace-yaml", args: []string{"service", "baz"}, ns: "testns", calls: map[string]string{ "GET": "/namespaces/testns/services/baz", "POST": "/namespaces/testns/services", }, input: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "testns", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, }, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "type": "LoadBalancer", "dry-run": "client", "output": "yaml"}, output: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"func": "stream"}, Type: corev1.ServiceTypeLoadBalancer, }, }, expected: "namespace: testns", status: 200, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace(test.ns) defer tf.Cleanup() codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == test.calls[m] && m == "GET": return &http.Response{StatusCode: test.status, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.input)}, nil case p == test.calls[m] && m == "POST": return &http.Response{StatusCode: test.status, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.output)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdExposeService(tf, ioStreams) cmd.SetOut(buf) cmd.SetErr(buf) for flag, value := range test.flags { cmd.Flags().Set(flag, value) } cmd.Run(cmd, test.args) out := buf.String() if test.expected == "" { t.Errorf("%s: Invalid test case. Specify expected result.\n", test.name) } if !strings.Contains(out, test.expected) { t.Errorf("%s: Unexpected output! Expected\n%s\ngot\n%s", test.name, test.expected, out) } }) } } func TestExposeOverride(t *testing.T) { tests := []struct { name string overrides string overrideType string expected string }{ { name: "expose with merge override type should replace the entire spec", overrides: `{"spec": {"ports": [{"protocol": "TCP", "port": 1111, "targetPort": 2222}]}, "selector": {"app": "go"}}`, overrideType: "merge", expected: `apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: svc: test name: foo namespace: test spec: ports: - port: 1111 protocol: TCP targetPort: 2222 selector: app: go status: loadBalancer: {} `, }, { name: "expose with strategic override type should add port before existing port", overrides: `{"spec": {"ports": [{"protocol": "TCP", "port": 1111, "targetPort": 2222}]}}`, overrideType: "strategic", expected: `apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: svc: test name: foo namespace: test spec: ports: - port: 1111 protocol: TCP targetPort: 2222 - port: 14 protocol: UDP targetPort: 14 selector: app: go status: loadBalancer: {} `, }, { name: "expose with json override type should add port before existing port", overrides: `[ {"op": "add", "path": "/spec/ports/0", "value": {"port": 1111, "protocol": "TCP", "targetPort": 2222}} ]`, overrideType: "json", expected: `apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: svc: test name: foo namespace: test spec: ports: - port: 1111 protocol: TCP targetPort: 2222 - port: 14 protocol: UDP targetPort: 14 selector: app: go status: loadBalancer: {} `, }, { name: "expose with json override type should add port after existing port", overrides: `[ {"op": "add", "path": "/spec/ports/1", "value": {"port": 1111, "protocol": "TCP", "targetPort": 2222}} ]`, overrideType: "json", expected: `apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: svc: test name: foo namespace: test spec: ports: - port: 14 protocol: UDP targetPort: 14 - port: 1111 protocol: TCP targetPort: 2222 selector: app: go status: loadBalancer: {} `, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services/baz" && m == "GET": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "go"}, }, })}, nil case p == "/namespaces/test/services" && m == "POST": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Protocol: corev1.ProtocolUDP, Port: 14, TargetPort: intstr.FromInt32(14), }, }, Selector: map[string]string{"app": "go"}, }, })}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdExposeService(tf, ioStreams) cmd.SetOut(buf) cmd.Flags().Set("protocol", "UDP") cmd.Flags().Set("port", "14") cmd.Flags().Set("name", "foo") cmd.Flags().Set("labels", "svc=test") cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("overrides", test.overrides) cmd.Flags().Set("override-type", test.overrideType) cmd.Flags().Set("output", "yaml") cmd.Run(cmd, []string{"service", "baz"}) out := buf.String() if test.expected == "" { t.Errorf("%s: Invalid test case. Specify expected result.\n", test.name) } if !strings.Contains(out, test.expected) { t.Errorf("%s: Unexpected output! Expected\n%s\ngot\n%s", test.name, test.expected, out) } }) } } func TestGenerateService(t *testing.T) { tests := map[string]struct { selector string name string port string protocol string protocols string targetPort string clusterIP string labels string externalIP string serviceType string sessionAffinity string setup func(t *testing.T, exposeServiceOptions *ExposeServiceOptions) func() expected *corev1.Service expectErr string }{ "test1": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "TCP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(1234), }, }, }, }, }, "test2": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "UDP", targetPort: "foobar", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "UDP", TargetPort: intstr.FromString("foobar"), }, }, }, }, }, "test3": { selector: "foo=bar,baz=blah", labels: "key1=value1,key2=value2", name: "test", port: "80", protocol: "TCP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Labels: map[string]string{ "key1": "value1", "key2": "value2", }, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(1234), }, }, }, }, }, "test4": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "UDP", externalIP: "1.2.3.4", targetPort: "foobar", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "UDP", TargetPort: intstr.FromString("foobar"), }, }, ExternalIPs: []string{"1.2.3.4"}, }, }, }, "test5": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "UDP", externalIP: "1.2.3.4", serviceType: "LoadBalancer", targetPort: "foobar", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "UDP", TargetPort: intstr.FromString("foobar"), }, }, Type: corev1.ServiceTypeLoadBalancer, ExternalIPs: []string{"1.2.3.4"}, }, }, }, "test6": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "UDP", targetPort: "foobar", serviceType: string(corev1.ServiceTypeNodePort), expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "UDP", TargetPort: intstr.FromString("foobar"), }, }, Type: corev1.ServiceTypeNodePort, }, }, }, "test7": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "UDP", targetPort: "foobar", serviceType: string(corev1.ServiceTypeNodePort), expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "UDP", TargetPort: intstr.FromString("foobar"), }, }, Type: corev1.ServiceTypeNodePort, }, }, }, "test8": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "TCP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(1234), }, }, }, }, }, "test9": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "TCP", sessionAffinity: "ClientIP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(1234), }, }, SessionAffinity: corev1.ServiceAffinityClientIP, }, }, }, "test10": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "TCP", clusterIP: "10.10.10.10", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(1234), }, }, ClusterIP: "10.10.10.10", }, }, }, "test11": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "TCP", clusterIP: "None", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(1234), }, }, ClusterIP: corev1.ClusterIPNone, }, }, }, "test12": { selector: "foo=bar", name: "test", port: "80,443", protocol: "TCP", targetPort: "foobar", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("foobar"), }, { Name: "port-2", Port: 443, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("foobar"), }, }, }, }, }, "test13": { selector: "foo=bar", name: "test", port: "80,443", protocol: "UDP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolUDP, TargetPort: intstr.FromInt32(1234), }, { Name: "port-2", Port: 443, Protocol: corev1.ProtocolUDP, TargetPort: intstr.FromInt32(1234), }, }, }, }, }, "test14": { selector: "foo=bar", name: "test", port: "80,443", protocol: "TCP", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Port: 443, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(443), }, }, }, }, }, "test15": { selector: "foo=bar", name: "test", port: "80,8080", protocols: "8080/UDP", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Port: 8080, Protocol: corev1.ProtocolUDP, TargetPort: intstr.FromInt32(8080), }, }, }, }, }, "test16": { selector: "foo=bar", name: "test", port: "80,8080,8081", protocols: "8080/UDP,8081/TCP", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Port: 8080, Protocol: corev1.ProtocolUDP, TargetPort: intstr.FromInt32(8080), }, { Name: "port-3", Port: 8081, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(8081), }, }, }, }, }, "test17": { selector: "foo=bar,baz=blah", name: "test", protocol: "TCP", clusterIP: "None", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{}, ClusterIP: corev1.ClusterIPNone, }, }, }, "test18": { selector: "foo=bar", name: "test", clusterIP: "None", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{}, ClusterIP: corev1.ClusterIPNone, }, }, }, "test19": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "SCTP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "SCTP", TargetPort: intstr.FromInt32(1234), }, }, }, }, }, "test20": { selector: "foo=bar,baz=blah", labels: "key1=value1,key2=value2", name: "test", port: "80", protocol: "SCTP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Labels: map[string]string{ "key1": "value1", "key2": "value2", }, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "SCTP", TargetPort: intstr.FromInt32(1234), }, }, }, }, }, "test21": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "SCTP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "SCTP", TargetPort: intstr.FromInt32(1234), }, }, }, }, }, "test22": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "SCTP", sessionAffinity: "ClientIP", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "SCTP", TargetPort: intstr.FromInt32(1234), }, }, SessionAffinity: corev1.ServiceAffinityClientIP, }, }, }, "test23": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "SCTP", clusterIP: "10.10.10.10", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "SCTP", TargetPort: intstr.FromInt32(1234), }, }, ClusterIP: "10.10.10.10", }, }, }, "test24": { selector: "foo=bar,baz=blah", name: "test", port: "80", protocol: "SCTP", clusterIP: "None", targetPort: "1234", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Port: 80, Protocol: "SCTP", TargetPort: intstr.FromInt32(1234), }, }, ClusterIP: corev1.ClusterIPNone, }, }, }, "test25": { selector: "foo=bar", name: "test", port: "80,443", protocol: "SCTP", targetPort: "foobar", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolSCTP, TargetPort: intstr.FromString("foobar"), }, { Name: "port-2", Port: 443, Protocol: corev1.ProtocolSCTP, TargetPort: intstr.FromString("foobar"), }, }, }, }, }, "test26": { selector: "foo=bar", name: "test", port: "80,443", protocol: "SCTP", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolSCTP, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Port: 443, Protocol: corev1.ProtocolSCTP, TargetPort: intstr.FromInt32(443), }, }, }, }, }, "test27": { selector: "foo=bar", name: "test", port: "80,8080", protocols: "8080/SCTP", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Port: 8080, Protocol: corev1.ProtocolSCTP, TargetPort: intstr.FromInt32(8080), }, }, }, }, }, "test28": { selector: "foo=bar", name: "test", port: "80,8080,8081,8082", protocols: "8080/UDP,8081/TCP,8082/SCTP", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", }, Ports: []corev1.ServicePort{ { Name: "port-1", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80), }, { Name: "port-2", Port: 8080, Protocol: corev1.ProtocolUDP, TargetPort: intstr.FromInt32(8080), }, { Name: "port-3", Port: 8081, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(8081), }, { Name: "port-4", Port: 8082, Protocol: corev1.ProtocolSCTP, TargetPort: intstr.FromInt32(8082), }, }, }, }, }, "test 29": { selector: "foo=bar,baz=blah", name: "test", protocol: "SCTP", targetPort: "1234", clusterIP: "None", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{}, ClusterIP: corev1.ClusterIPNone, }, }, }, // Fixed #114402 kubectl expose fails for apps with same-port, different-protocol "test #114402": { selector: "foo=bar,baz=blah", name: "test", clusterIP: "None", protocols: "53/TCP,53/UDP", port: "53", expected: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "test", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "foo": "bar", "baz": "blah", }, Ports: []corev1.ServicePort{ { Name: "port-1-tcp", Port: 53, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(53), }, { Name: "port-1-udp", Port: 53, Protocol: corev1.ProtocolUDP, TargetPort: intstr.FromInt32(53), }, }, ClusterIP: corev1.ClusterIPNone, }, }, }, "check selector": { name: "test", protocol: "SCTP", targetPort: "1234", clusterIP: "None", expectErr: `selector must be specified`, }, "check name": { selector: "foo=bar,baz=blah", protocol: "SCTP", targetPort: "1234", clusterIP: "None", expectErr: `name must be specified`, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { exposeServiceOptions := ExposeServiceOptions{ Selector: test.selector, Name: test.name, Protocol: test.protocol, Protocols: test.protocols, Port: test.port, ClusterIP: test.clusterIP, TargetPort: test.targetPort, Labels: test.labels, ExternalIP: test.externalIP, Type: test.serviceType, SessionAffinity: test.sessionAffinity, } service, err := exposeServiceOptions.createService() if test.expectErr == "" { require.NoError(t, err) if !apiequality.Semantic.DeepEqual(service, test.expected) { t.Errorf("\nexpected:\n%#v\ngot:\n%#v", test.expected, service) } } else { require.Error(t, err) require.EqualError(t, err, test.expectErr) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/000077500000000000000000000000001504711711200253665ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn.go000066400000000000000000000207151504711711200304520ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "bufio" "bytes" "errors" "fmt" "io" "reflect" "regexp" "strings" "github.com/liggitt/tabwriter" "k8s.io/apimachinery/pkg/api/meta" 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" "k8s.io/client-go/util/jsonpath" ) var jsonRegexp = regexp.MustCompile(`^\{\.?([^{}]+)\}$|^\.?([^{}]+)$`) // RelaxedJSONPathExpression attempts to be flexible with JSONPath expressions, it accepts: // - metadata.name (no leading '.' or curly braces '{...}' // - {metadata.name} (no leading '.') // - .metadata.name (no curly braces '{...}') // - {.metadata.name} (complete expression) // // And transforms them all into a valid jsonpath expression: // // {.metadata.name} func RelaxedJSONPathExpression(pathExpression string) (string, error) { if len(pathExpression) == 0 { return pathExpression, nil } submatches := jsonRegexp.FindStringSubmatch(pathExpression) if submatches == nil { return "", fmt.Errorf("unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'") } if len(submatches) != 3 { return "", fmt.Errorf("unexpected submatch list: %v", submatches) } var fieldSpec string if len(submatches[1]) != 0 { fieldSpec = submatches[1] } else { fieldSpec = submatches[2] } return fmt.Sprintf("{.%s}", fieldSpec), nil } // NewCustomColumnsPrinterFromSpec creates a custom columns printer from a comma separated list of

: pairs. // e.g. NAME:metadata.name,API_VERSION:apiVersion creates a printer that prints: // // NAME API_VERSION // foo bar func NewCustomColumnsPrinterFromSpec(spec string, decoder runtime.Decoder, noHeaders bool) (*CustomColumnsPrinter, error) { if len(spec) == 0 { return nil, fmt.Errorf("custom-columns format specified but no custom columns given") } parts := strings.Split(spec, ",") columns := make([]Column, len(parts)) for ix := range parts { colSpec := strings.SplitN(parts[ix], ":", 2) if len(colSpec) != 2 { return nil, fmt.Errorf("unexpected custom-columns spec: %s, expected
:", parts[ix]) } spec, err := RelaxedJSONPathExpression(colSpec[1]) if err != nil { return nil, err } columns[ix] = Column{Header: colSpec[0], FieldSpec: spec} } return &CustomColumnsPrinter{Columns: columns, Decoder: decoder, NoHeaders: noHeaders}, nil } func splitOnWhitespace(line string) []string { lineScanner := bufio.NewScanner(bytes.NewBufferString(line)) lineScanner.Split(bufio.ScanWords) result := []string{} for lineScanner.Scan() { result = append(result, lineScanner.Text()) } return result } // NewCustomColumnsPrinterFromTemplate creates a custom columns printer from a template stream. The template is expected // to consist of two lines, whitespace separated. The first line is the header line, the second line is the jsonpath field spec // For example, the template below: // NAME API_VERSION // {metadata.name} {apiVersion} func NewCustomColumnsPrinterFromTemplate(templateReader io.Reader, decoder runtime.Decoder) (*CustomColumnsPrinter, error) { scanner := bufio.NewScanner(templateReader) if !scanner.Scan() { return nil, fmt.Errorf("invalid template, missing header line. Expected format is one line of space separated headers, one line of space separated column specs.") } headers := splitOnWhitespace(scanner.Text()) if !scanner.Scan() { return nil, fmt.Errorf("invalid template, missing spec line. Expected format is one line of space separated headers, one line of space separated column specs.") } specs := splitOnWhitespace(scanner.Text()) if len(headers) != len(specs) { return nil, fmt.Errorf("number of headers (%d) and field specifications (%d) don't match", len(headers), len(specs)) } columns := make([]Column, len(headers)) for ix := range headers { spec, err := RelaxedJSONPathExpression(specs[ix]) if err != nil { return nil, err } columns[ix] = Column{ Header: headers[ix], FieldSpec: spec, } } return &CustomColumnsPrinter{Columns: columns, Decoder: decoder, NoHeaders: false}, nil } // Column represents a user specified column type Column struct { // The header to print above the column, general style is ALL_CAPS Header string // The pointer to the field in the object to print in JSONPath form // e.g. {.ObjectMeta.Name}, see pkg/util/jsonpath for more details. FieldSpec string } // CustomColumnPrinter is a printer that knows how to print arbitrary columns // of data from templates specified in the `Columns` array type CustomColumnsPrinter struct { Columns []Column Decoder runtime.Decoder NoHeaders bool // lastType records type of resource printed last so that we don't repeat // header while printing same type of resources. lastType reflect.Type } func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error { // we use reflect.Indirect here in order to obtain the actual value from a pointer. // we need an actual value in order to retrieve the package path for an object. // using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers. if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) { return errors.New(printers.InternalObjectPrinterErr) } if _, found := out.(*tabwriter.Writer); !found { w := printers.GetNewTabWriter(out) out = w defer w.Flush() } t := reflect.TypeOf(obj) if !s.NoHeaders && t != s.lastType { headers := make([]string, len(s.Columns)) for ix := range s.Columns { headers[ix] = s.Columns[ix].Header } fmt.Fprintln(out, strings.Join(headers, "\t")) s.lastType = t } parsers := make([]*jsonpath.JSONPath, len(s.Columns)) for ix := range s.Columns { parsers[ix] = jsonpath.New(fmt.Sprintf("column%d", ix)).AllowMissingKeys(true) if err := parsers[ix].Parse(s.Columns[ix].FieldSpec); err != nil { return err } } if meta.IsListType(obj) { objs, err := meta.ExtractList(obj) if err != nil { return err } for ix := range objs { if err := s.printOneObject(objs[ix], parsers, out); err != nil { return err } } } else { if err := s.printOneObject(obj, parsers, out); err != nil { return err } } return nil } func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jsonpath.JSONPath, out io.Writer) error { columns := make([]string, len(parsers)) switch u := obj.(type) { case *metav1.WatchEvent: if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(u.Object.Object)).Type().PkgPath()) { return errors.New(printers.InternalObjectPrinterErr) } unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(u.Object.Object) if err != nil { return err } obj = &unstructured.Unstructured{ Object: map[string]interface{}{ "type": u.Type, "object": unstructuredObject, }, } case *runtime.Unknown: if len(u.Raw) > 0 { var err error if obj, err = runtime.Decode(s.Decoder, u.Raw); err != nil { return fmt.Errorf("can't decode object for printing: %v (%s)", err, u.Raw) } } } for ix := range parsers { parser := parsers[ix] var values [][]reflect.Value var err error if unstructured, ok := obj.(runtime.Unstructured); ok { values, err = parser.FindResults(unstructured.UnstructuredContent()) } else { values, err = parser.FindResults(reflect.ValueOf(obj).Elem().Interface()) } if err != nil { return err } valueStrings := []string{} if len(values) == 0 || len(values[0]) == 0 { valueStrings = append(valueStrings, "") } for arrIx := range values { for valIx := range values[arrIx] { valueStrings = append(valueStrings, printers.EscapeTerminal(fmt.Sprint(values[arrIx][valIx].Interface()))) } } columns[ix] = strings.Join(valueStrings, ",") } fmt.Fprintln(out, strings.Join(columns, "\t")) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn_flags.go000066400000000000000000000066511504711711200316310ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "fmt" "os" "sort" "strings" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/kubectl/pkg/scheme" ) var columnsFormats = map[string]bool{ "custom-columns-file": true, "custom-columns": true, } // CustomColumnsPrintFlags provides default flags necessary for printing // custom resource columns from an inline-template or file. type CustomColumnsPrintFlags struct { NoHeaders bool TemplateArgument string } func (f *CustomColumnsPrintFlags) AllowedFormats() []string { formats := make([]string, 0, len(columnsFormats)) for format := range columnsFormats { formats = append(formats, format) } sort.Strings(formats) return formats } // ToPrinter receives an templateFormat and returns a printer capable of // handling custom-column printing. // Returns false if the specified templateFormat does not match a supported format. // Supported format types can be found in pkg/printers/printers.go func (f *CustomColumnsPrintFlags) ToPrinter(templateFormat string) (printers.ResourcePrinter, error) { if len(templateFormat) == 0 { return nil, genericclioptions.NoCompatiblePrinterError{} } templateValue := "" if len(f.TemplateArgument) == 0 { for format := range columnsFormats { format = format + "=" if strings.HasPrefix(templateFormat, format) { templateValue = templateFormat[len(format):] templateFormat = format[:len(format)-1] break } } } else { templateValue = f.TemplateArgument } if _, supportedFormat := columnsFormats[templateFormat]; !supportedFormat { return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &templateFormat, AllowedFormats: f.AllowedFormats()} } if len(templateValue) == 0 { return nil, fmt.Errorf("custom-columns format specified but no custom columns given") } // UniversalDecoder call must specify parameter versions; otherwise it will decode to internal versions. decoder := scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...) if templateFormat == "custom-columns-file" { file, err := os.Open(templateValue) if err != nil { return nil, fmt.Errorf("error reading template %s, %v\n", templateValue, err) } defer file.Close() p, err := NewCustomColumnsPrinterFromTemplate(file, decoder) return p, err } return NewCustomColumnsPrinterFromSpec(templateValue, decoder, f.NoHeaders) } // AddFlags receives a *cobra.Command reference and binds // flags related to custom-columns printing func (f *CustomColumnsPrintFlags) AddFlags(c *cobra.Command) {} // NewCustomColumnsPrintFlags returns flags associated with // custom-column printing, with default values set. // NoHeaders and TemplateArgument should be set by callers. func NewCustomColumnsPrintFlags() *CustomColumnsPrintFlags { return &CustomColumnsPrintFlags{ NoHeaders: false, TemplateArgument: "", } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn_flags_test.go000066400000000000000000000101021504711711200326520ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "bytes" "fmt" "os" "strings" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" ) func TestPrinterSupportsExpectedCustomColumnFormats(t *testing.T) { testObject := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} customColumnsFile, err := os.CreateTemp(os.TempDir(), "printers_jsonpath_flags") if err != nil { t.Fatalf("unexpected error: %v", err) } defer func(tempFile *os.File) { tempFile.Close() os.Remove(tempFile.Name()) }(customColumnsFile) fmt.Fprintf(customColumnsFile, "NAME\n.metadata.name") testCases := []struct { name string outputFormat string templateArg string expectedError string expectedParseError string expectedOutput string expectNoMatch bool }{ { name: "valid output format also containing the custom-columns argument succeeds", outputFormat: "custom-columns=NAME:.metadata.name", expectedOutput: "foo", }, { name: "valid output format and no --template argument results in an error", outputFormat: "custom-columns", expectedError: "custom-columns format specified but no custom columns given", }, { name: "valid output format and --template argument succeeds", outputFormat: "custom-columns", templateArg: "NAME:.metadata.name", expectedOutput: "foo", }, { name: "custom-columns template file should match, and successfully return correct value", outputFormat: "custom-columns-file", templateArg: customColumnsFile.Name(), expectedOutput: "foo", }, { name: "valid output format and invalid --template argument results in a parsing error from the printer", outputFormat: "custom-columns", templateArg: "invalid", expectedError: "unexpected custom-columns spec: invalid, expected
:", }, { name: "no printer is matched on an invalid outputFormat", outputFormat: "invalid", expectNoMatch: true, }, { name: "custom-columns printer should not match on any other format supported by another printer", outputFormat: "go-template", expectNoMatch: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { printFlags := CustomColumnsPrintFlags{ TemplateArgument: tc.templateArg, } p, err := printFlags.ToPrinter(tc.outputFormat) if tc.expectNoMatch { if !genericclioptions.IsNoCompatiblePrinterError(err) { t.Fatalf("expected no printer matches for output format %q", tc.outputFormat) } return } if genericclioptions.IsNoCompatiblePrinterError(err) { t.Fatalf("expected to match template printer for output format %q", tc.outputFormat) } if len(tc.expectedError) > 0 { if err == nil || !strings.Contains(err.Error(), tc.expectedError) { t.Errorf("expecting error %q, got %v", tc.expectedError, err) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } out := bytes.NewBuffer([]byte{}) err = p.PrintObj(testObject, out) if len(tc.expectedParseError) > 0 { if err == nil || !strings.Contains(err.Error(), tc.expectedParseError) { t.Errorf("expecting error %q, got %v", tc.expectedError, err) } return } if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out.String(), tc.expectedOutput) { t.Errorf("unexpected output: expecting %q, got %q", tc.expectedOutput, out.String()) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn_test.go000066400000000000000000000323551504711711200315140ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "bytes" "reflect" "strings" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/printers" "k8s.io/kubectl/pkg/scheme" ) // UniversalDecoder call must specify parameter versions; otherwise it will decode to internal versions. var decoder = scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...) func TestMassageJSONPath(t *testing.T) { tests := []struct { input string expectedOutput string expectErr bool }{ {input: "foo.bar", expectedOutput: "{.foo.bar}"}, {input: "{foo.bar}", expectedOutput: "{.foo.bar}"}, {input: ".foo.bar", expectedOutput: "{.foo.bar}"}, {input: "{.foo.bar}", expectedOutput: "{.foo.bar}"}, {input: "", expectedOutput: ""}, {input: "{foo.bar", expectErr: true}, {input: "foo.bar}", expectErr: true}, {input: "{foo.bar}}", expectErr: true}, {input: "{{foo.bar}", expectErr: true}, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { output, err := RelaxedJSONPathExpression(test.input) if err != nil && !test.expectErr { t.Errorf("unexpected error: %v", err) return } if test.expectErr { if err == nil { t.Error("unexpected non-error") } return } if output != test.expectedOutput { t.Errorf("input: %s, expected: %s, saw: %s", test.input, test.expectedOutput, output) } }) } } func TestNewColumnPrinterFromSpec(t *testing.T) { tests := []struct { spec string expectedColumns []Column expectErr bool name string noHeaders bool }{ { spec: "", expectErr: true, name: "empty", }, { spec: "invalid", expectErr: true, name: "invalid1", }, { spec: "invalid=foobar", expectErr: true, name: "invalid2", }, { spec: "invalid,foobar:blah", expectErr: true, name: "invalid3", }, { spec: "NAME:metadata.name,API_VERSION:apiVersion", name: "ok", expectedColumns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, { Header: "API_VERSION", FieldSpec: "{.apiVersion}", }, }, }, { spec: "API_VERSION:apiVersion", name: "no-headers", noHeaders: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { printer, err := NewCustomColumnsPrinterFromSpec(test.spec, decoder, test.noHeaders) if test.expectErr { if err == nil { t.Errorf("[%s] unexpected non-error", test.name) } return } if !test.expectErr && err != nil { t.Errorf("[%s] unexpected error: %v", test.name, err) return } if test.noHeaders { buffer := &bytes.Buffer{} printer.PrintObj(&corev1.Pod{}, buffer) if err != nil { t.Fatalf("An error occurred printing Pod: %#v", err) } if contains(strings.Fields(buffer.String()), "API_VERSION") { t.Errorf("unexpected header API_VERSION") } } else if !reflect.DeepEqual(test.expectedColumns, printer.Columns) { t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v\n", test.name, test.expectedColumns, printer.Columns) } }) } } func contains(arr []string, s string) bool { for i := range arr { if arr[i] == s { return true } } return false } const exampleTemplateOne = `NAME API_VERSION {metadata.name} {apiVersion}` const exampleTemplateTwo = `NAME API_VERSION {metadata.name} {apiVersion}` func TestNewColumnPrinterFromTemplate(t *testing.T) { tests := []struct { spec string expectedColumns []Column expectErr bool name string }{ { spec: "", expectErr: true, name: "empty", }, { spec: "invalid", expectErr: true, name: "invalid1", }, { spec: "invalid=foobar", expectErr: true, name: "invalid2", }, { spec: "invalid,foobar:blah", expectErr: true, name: "invalid3", }, { spec: exampleTemplateOne, name: "ok", expectedColumns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, { Header: "API_VERSION", FieldSpec: "{.apiVersion}", }, }, }, { spec: exampleTemplateTwo, name: "ok-2", expectedColumns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, { Header: "API_VERSION", FieldSpec: "{.apiVersion}", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { reader := bytes.NewBufferString(test.spec) printer, err := NewCustomColumnsPrinterFromTemplate(reader, decoder) if test.expectErr { if err == nil { t.Errorf("[%s] unexpected non-error", test.name) } return } if !test.expectErr && err != nil { t.Errorf("[%s] unexpected error: %v", test.name, err) return } if !reflect.DeepEqual(test.expectedColumns, printer.Columns) { t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v\n", test.name, test.expectedColumns, printer.Columns) } }) } } func TestColumnPrint(t *testing.T) { tests := []struct { columns []Column obj runtime.Object expectedOutput string }{ { columns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, }, obj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, expectedOutput: `NAME foo `, }, { columns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, }, obj: &corev1.PodList{ Items: []corev1.Pod{ {ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, {ObjectMeta: metav1.ObjectMeta{Name: "bar"}}, }, }, expectedOutput: `NAME foo bar `, }, { columns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, { Header: "API_VERSION", FieldSpec: "{.apiVersion}", }, }, obj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, TypeMeta: metav1.TypeMeta{APIVersion: "baz"}}, expectedOutput: `NAME API_VERSION foo baz `, }, { columns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, { Header: "API_VERSION", FieldSpec: "{.apiVersion}", }, { Header: "NOT_FOUND", FieldSpec: "{.notFound}", }, }, obj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, TypeMeta: metav1.TypeMeta{APIVersion: "baz"}}, expectedOutput: `NAME API_VERSION NOT_FOUND foo baz `, }, { columns: []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, }, obj: &corev1.PodList{ Items: []corev1.Pod{ {ObjectMeta: metav1.ObjectMeta{Name: "\x1b \r"}}, }, }, expectedOutput: `NAME ^[ \r `, }, } for _, test := range tests { t.Run(test.expectedOutput, func(t *testing.T) { printer := &CustomColumnsPrinter{ Columns: test.columns, Decoder: decoder, } buffer := &bytes.Buffer{} if err := printer.PrintObj(test.obj, buffer); err != nil { t.Errorf("unexpected error: %v", err) } if buffer.String() != test.expectedOutput { t.Errorf("\nexpected:\n'%s'\nsaw\n'%s'\n", test.expectedOutput, buffer.String()) } }) } } // this mimics how resource/get.go calls the customcolumn printer func TestIndividualPrintObjOnExistingTabWriter(t *testing.T) { columns := []Column{ { Header: "NAME", FieldSpec: "{.metadata.name}", }, { Header: "LONG COLUMN NAME", // name is longer than all values of label1 FieldSpec: "{.metadata.labels.label1}", }, { Header: "LABEL 2", FieldSpec: "{.metadata.labels.label2}", }, } objects := []*corev1.Pod{ {ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{"label1": "foo", "label2": "foo"}}}, {ObjectMeta: metav1.ObjectMeta{Name: "bar", Labels: map[string]string{"label1": "bar", "label2": "bar"}}}, } expectedOutput := `NAME LONG COLUMN NAME LABEL 2 foo foo foo bar bar bar ` buffer := &bytes.Buffer{} tabWriter := printers.GetNewTabWriter(buffer) printer := &CustomColumnsPrinter{ Columns: columns, Decoder: decoder, } for _, obj := range objects { if err := printer.PrintObj(obj, tabWriter); err != nil { t.Errorf("unexpected error: %v", err) } } tabWriter.Flush() if buffer.String() != expectedOutput { t.Errorf("\nexpected:\n'%s'\nsaw\n'%s'\n", expectedOutput, buffer.String()) } } func TestSliceColumnPrint(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "fake-name", Namespace: "fake-namespace", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "fake0", }, { Name: "fake1", }, { Name: "fake2", }, { Name: "fake3", }, }, }, } tests := []struct { name string spec string expectedOutput string expectErr bool }{ { name: "containers[0]", spec: "CONTAINER:.spec.containers[0].name", expectedOutput: `CONTAINER fake0 `, expectErr: false, }, { name: "containers[3]", spec: "CONTAINER:.spec.containers[3].name", expectedOutput: `CONTAINER fake3 `, expectErr: false, }, { name: "containers[5], illegal expression because it is out of bounds", spec: "CONTAINER:.spec.containers[5].name", expectedOutput: "", expectErr: true, }, { name: "containers[-1], it equals containers[3]", spec: "CONTAINER:.spec.containers[-1].name", expectedOutput: `CONTAINER fake3 `, expectErr: false, }, { name: "containers[-2], it equals containers[2]", spec: "CONTAINER:.spec.containers[-2].name", expectedOutput: `CONTAINER fake2 `, expectErr: false, }, { name: "containers[-4], it equals containers[0]", spec: "CONTAINER:.spec.containers[-4].name", expectedOutput: `CONTAINER fake0 `, expectErr: false, }, { name: "containers[-5], illegal expression because it is out of bounds", spec: "CONTAINER:.spec.containers[-5].name", expectedOutput: "", expectErr: true, }, { name: "containers[0:0], it equals empty set", spec: "CONTAINER:.spec.containers[0:0].name", expectedOutput: `CONTAINER `, expectErr: false, }, { name: "containers[0:3]", spec: "CONTAINER:.spec.containers[0:3].name", expectedOutput: `CONTAINER fake0,fake1,fake2 `, expectErr: false, }, { name: "containers[1:]", spec: "CONTAINER:.spec.containers[1:].name", expectedOutput: `CONTAINER fake1,fake2,fake3 `, expectErr: false, }, { name: "containers[3:1], illegal expression because start index is greater than end index", spec: "CONTAINER:.spec.containers[3:1].name", expectedOutput: "", expectErr: true, }, { name: "containers[0:-1], it equals containers[0:3]", spec: "CONTAINER:.spec.containers[0:-1].name", expectedOutput: `CONTAINER fake0,fake1,fake2 `, expectErr: false, }, { name: "containers[-1:], it equals containers[3:]", spec: "CONTAINER:.spec.containers[-1:].name", expectedOutput: `CONTAINER fake3 `, expectErr: false, }, { name: "containers[-4:], it equals containers[0:]", spec: "CONTAINER:.spec.containers[-4:].name", expectedOutput: `CONTAINER fake0,fake1,fake2,fake3 `, expectErr: false, }, { name: "containers[-3:-1], it equasl containers[1:3]", spec: "CONTAINER:.spec.containers[-3:-1].name", expectedOutput: `CONTAINER fake1,fake2 `, expectErr: false, }, { name: "containers[-2:-3], it equals containers[2:1], illegal expression because start index is greater than end index", spec: "CONTAINER:.spec.containers[-2:-3].name", expectedOutput: "", expectErr: true, }, { name: "containers[4:4], it equals empty set", spec: "CONTAINER:.spec.containers[4:4].name", expectedOutput: `CONTAINER `, expectErr: false, }, { name: "containers[-5:-5], it equals empty set", spec: "CONTAINER:.spec.containers[-5:-5].name", expectedOutput: `CONTAINER `, expectErr: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { printer, err := NewCustomColumnsPrinterFromSpec(test.spec, decoder, false) if err != nil { t.Errorf("test %s has unexpected error: %v", test.name, err) } buffer := &bytes.Buffer{} err = printer.PrintObj(pod, buffer) if test.expectErr { if err == nil { t.Errorf("test %s has unexpected error: %v", test.name, err) } } else { if err != nil { t.Errorf("test %s has unexpected error: %v", test.name, err) } else if buffer.String() != test.expectedOutput { t.Errorf("test %s has unexpected output:\nexpected: %s\nsaw: %s", test.name, test.expectedOutput, buffer.String()) } } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/get.go000066400000000000000000000630521504711711200265020ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "context" "encoding/json" "fmt" "io" "net/url" "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" 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" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" kubernetesscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" watchtools "k8s.io/client-go/tools/watch" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/rawhttp" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" "k8s.io/utils/ptr" ) // GetOptions contains the input to the get command. type GetOptions struct { PrintFlags *PrintFlags ToPrinter func(*meta.RESTMapping, *bool, bool, bool) (printers.ResourcePrinterFunc, error) IsHumanReadablePrinter bool CmdParent string resource.FilenameOptions Raw string Watch bool WatchOnly bool ChunkSize int64 OutputWatchEvents bool LabelSelector string FieldSelector string AllNamespaces bool Namespace string ExplicitNamespace bool Subresource string SortBy string ServerPrint bool NoHeaders bool IgnoreNotFound bool genericiooptions.IOStreams } var ( getLong = templates.LongDesc(i18n.T(` Display one or many resources. Prints a table of the most important information about the specified resources. You can filter the list using a label selector and the --selector flag. If the desired resource type is namespaced you will only see results in the current namespace if you don't specify any namespace. By specifying the output as 'template' and providing a Go template as the value of the --template flag, you can filter the attributes of the fetched resources.`)) getExample = templates.Examples(i18n.T(` # List all pods in ps output format kubectl get pods # List all pods in ps output format with more information (such as node name) kubectl get pods -o wide # List a single replication controller with specified NAME in ps output format kubectl get replicationcontroller web # List deployments in JSON output format, in the "v1" version of the "apps" API group kubectl get deployments.v1.apps -o json # List a single pod in JSON output format kubectl get -o json pod web-pod-13je7 # List a pod identified by type and name specified in "pod.yaml" in JSON output format kubectl get -f pod.yaml -o json # List resources from a directory with kustomization.yaml - e.g. dir/kustomization.yaml kubectl get -k dir/ # Return only the phase value of the specified pod kubectl get -o template pod/web-pod-13je7 --template={{.status.phase}} # List resource information in custom columns kubectl get pod test-pod -o custom-columns=CONTAINER:.spec.containers[0].name,IMAGE:.spec.containers[0].image # List all replication controllers and services together in ps output format kubectl get rc,services # List one or more resources by their type and names kubectl get rc/web service/frontend pods/web-pod-13je7 # List the 'status' subresource for a single pod kubectl get pod web-pod-13je7 --subresource status # List all deployments in namespace 'backend' kubectl get deployments.apps --namespace backend # List all pods existing in all namespaces kubectl get pods --all-namespaces`)) ) const ( useServerPrintColumns = "server-print" ) // NewGetOptions returns a GetOptions with default chunk size 500. func NewGetOptions(parent string, streams genericiooptions.IOStreams) *GetOptions { return &GetOptions{ PrintFlags: NewGetPrintFlags(), CmdParent: parent, IOStreams: streams, ChunkSize: cmdutil.DefaultChunkSize, ServerPrint: true, } } // NewCmdGet creates a command object for the generic "get" action, which // retrieves one or more resources from a server. func NewCmdGet(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewGetOptions(parent, streams) cmd := &cobra.Command{ Use: fmt.Sprintf("get [(-o|--output=)%s] (TYPE[.VERSION][.GROUP] [NAME | -l label] | TYPE[.VERSION][.GROUP]/NAME ...) [flags]", strings.Join(o.PrintFlags.AllowedFormats(), "|")), DisableFlagsInUseLine: true, Short: i18n.T("Display one or many resources"), Long: getLong + "\n\n" + cmdutil.SuggestAPIResources(parent), Example: getExample, // ValidArgsFunction is set when this function is called so that we have access to the util package Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run(f, args)) }, SuggestFor: []string{"list", "ps"}, } o.PrintFlags.AddFlags(cmd) cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to request from the server. Uses the transport specified by the kubeconfig file.") cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "After listing/getting the requested object, watch for changes.") cmd.Flags().BoolVar(&o.WatchOnly, "watch-only", o.WatchOnly, "Watch for changes to the requested object(s), without listing/getting first.") cmd.Flags().BoolVar(&o.OutputWatchEvents, "output-watch-events", o.OutputWatchEvents, "Output watch event objects when --watch or --watch-only is used. Existing objects are output as initial ADDED events.") cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.") cmd.Flags().StringVar(&o.FieldSelector, "field-selector", o.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") addServerPrintColumnFlags(cmd, o) cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to get from a server.") cmdutil.AddChunkSizeFlag(cmd, &o.ChunkSize) cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, gets the subresource of the requested object.") return cmd } // Complete takes the command arguments and factory and infers any remaining options. func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { if len(o.Raw) > 0 { if len(args) > 0 { return fmt.Errorf("arguments may not be passed when --raw is specified") } return nil } var err error o.Namespace, o.ExplicitNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } if o.AllNamespaces { o.ExplicitNamespace = false } if o.PrintFlags.HumanReadableFlags.SortBy != nil { o.SortBy = *o.PrintFlags.HumanReadableFlags.SortBy } o.NoHeaders = cmdutil.GetFlagBool(cmd, "no-headers") // TODO (soltysh): currently we don't support custom columns // with server side print. So in these cases force the old behavior. outputOption := cmd.Flags().Lookup("output").Value.String() if strings.Contains(outputOption, "custom-columns") || outputOption == "yaml" || strings.Contains(outputOption, "json") { o.ServerPrint = false } templateArg := "" if o.PrintFlags.TemplateFlags != nil && o.PrintFlags.TemplateFlags.TemplateArgument != nil { templateArg = *o.PrintFlags.TemplateFlags.TemplateArgument } // human readable printers have special conversion rules, so we determine if we're using one. if (len(*o.PrintFlags.OutputFormat) == 0 && len(templateArg) == 0) || *o.PrintFlags.OutputFormat == "wide" { o.IsHumanReadablePrinter = true } o.ToPrinter = func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { // make a new copy of current flags / opts before mutating printFlags := o.PrintFlags.Copy() if mapping != nil { printFlags.SetKind(mapping.GroupVersionKind.GroupKind()) } if withNamespace { printFlags.EnsureWithNamespace() } if withKind { printFlags.EnsureWithKind() } printer, err := printFlags.ToPrinter() if err != nil { return nil, err } printer, err = printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(printer, nil) if err != nil { return nil, err } if len(o.SortBy) > 0 { printer = &SortingPrinter{Delegate: printer, SortField: o.SortBy} } if outputObjects != nil { printer = &skipPrinter{delegate: printer, output: outputObjects} } if o.ServerPrint { printer = &TablePrinter{Delegate: printer} } return printer.PrintObj, nil } switch { case o.Watch: if len(o.SortBy) > 0 { fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch requested, --sort-by will be ignored for watch events received\n") } case o.WatchOnly: if len(o.SortBy) > 0 { fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch-only requested, --sort-by will be ignored\n") } default: if len(args) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { fmt.Fprintf(o.ErrOut, "You must specify the type of resource to get. %s\n\n", cmdutil.SuggestAPIResources(o.CmdParent)) fullCmdName := cmd.Parent().CommandPath() usageString := "Required resource not specified." if len(fullCmdName) > 0 && cmdutil.IsSiblingCommandExists(cmd, "explain") { usageString = fmt.Sprintf("%s\nUse \"%s explain \" for a detailed description of that resource (e.g. %[2]s explain pods).", usageString, fullCmdName) } return cmdutil.UsageErrorf(cmd, "%s", usageString) } } return nil } // Validate checks the set of flags provided by the user. func (o *GetOptions) Validate() error { if len(o.Raw) > 0 { if o.Watch || o.WatchOnly || len(o.LabelSelector) > 0 { return fmt.Errorf("--raw may not be specified with other flags that filter the server request or alter the output") } if o.PrintFlags.OutputFormat != nil && len(*o.PrintFlags.OutputFormat) > 0 { return fmt.Errorf("--raw and --output are mutually exclusive") } if _, err := url.ParseRequestURI(o.Raw); err != nil { return fmt.Errorf("--raw must be a valid URL path: %v", err) } } if o.PrintFlags.HumanReadableFlags.ShowLabels != nil && *o.PrintFlags.HumanReadableFlags.ShowLabels && o.PrintFlags.OutputFormat != nil { outputOption := *o.PrintFlags.OutputFormat if outputOption != "" && outputOption != "wide" { return fmt.Errorf("--show-labels option cannot be used with %s printer", outputOption) } } if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) { return fmt.Errorf("--output-watch-events option can only be used with --watch or --watch-only") } return nil } // OriginalPositioner and NopPositioner is required for swap/sort operations of data in table format type OriginalPositioner interface { OriginalPosition(int) int } // NopPositioner and OriginalPositioner is required for swap/sort operations of data in table format type NopPositioner struct{} // OriginalPosition returns the original position from NopPositioner object func (t *NopPositioner) OriginalPosition(ix int) int { return ix } // RuntimeSorter holds the required objects to perform sorting of runtime objects type RuntimeSorter struct { field string decoder runtime.Decoder objects []runtime.Object positioner OriginalPositioner } // Sort performs the sorting of runtime objects func (r *RuntimeSorter) Sort() error { // a list is only considered "sorted" if there are 0 or 1 items in it // AND (if 1 item) the item is not a Table object if len(r.objects) == 0 { return nil } if len(r.objects) == 1 { _, isTable := r.objects[0].(*metav1.Table) if !isTable { return nil } } includesTable := false includesRuntimeObjs := false for _, obj := range r.objects { switch t := obj.(type) { case *metav1.Table: includesTable = true if sorter, err := NewTableSorter(t, r.field); err != nil { return err } else if err := sorter.Sort(); err != nil { return err } default: includesRuntimeObjs = true } } // we use a NopPositioner when dealing with Table objects // because the objects themselves are not swapped, but rather // the rows in each object are swapped / sorted. r.positioner = &NopPositioner{} if includesRuntimeObjs && includesTable { return fmt.Errorf("sorting is not supported on mixed Table and non-Table object lists") } if includesTable { return nil } // if not dealing with a Table response from the server, assume // all objects are runtime.Object as usual, and sort using old method. var err error if r.positioner, err = SortObjects(r.decoder, r.objects, r.field); err != nil { return err } return nil } // OriginalPosition returns the original position of a runtime object func (r *RuntimeSorter) OriginalPosition(ix int) int { if r.positioner == nil { return 0 } return r.positioner.OriginalPosition(ix) } // WithDecoder allows custom decoder to be set for testing func (r *RuntimeSorter) WithDecoder(decoder runtime.Decoder) *RuntimeSorter { r.decoder = decoder return r } // NewRuntimeSorter returns a new instance of RuntimeSorter func NewRuntimeSorter(objects []runtime.Object, sortBy string) *RuntimeSorter { parsedField, err := RelaxedJSONPathExpression(sortBy) if err != nil { parsedField = sortBy } return &RuntimeSorter{ field: parsedField, decoder: kubernetesscheme.Codecs.UniversalDecoder(), objects: objects, } } func (o *GetOptions) transformRequests(req *rest.Request) { if !o.ServerPrint || !o.IsHumanReadablePrinter { return } req.SetHeader("Accept", strings.Join([]string{ fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), "application/json", }, ",")) // if sorting, ensure we receive the full object in order to introspect its fields via jsonpath if len(o.SortBy) > 0 { req.Param("includeObject", "Object") } } // Run performs the get operation. // TODO: remove the need to pass these arguments, like other commands. func (o *GetOptions) Run(f cmdutil.Factory, args []string) error { if len(o.Raw) > 0 { restClient, err := f.RESTClient() if err != nil { return err } return rawhttp.RawGet(restClient, o.IOStreams, o.Raw) } if o.Watch || o.WatchOnly { return o.watch(f, args) } chunkSize := o.ChunkSize if len(o.SortBy) > 0 { // TODO(juanvallejo): in the future, we could have the client use chunking // to gather all results, then sort them all at the end to reduce server load. chunkSize = 0 } r := f.NewBuilder(). Unstructured(). NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces). FilenameParam(o.ExplicitNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). FieldSelectorParam(o.FieldSelector). Subresource(o.Subresource). RequestChunksOf(chunkSize). ResourceTypeOrNameArgs(true, args...). ContinueOnError(). Latest(). Flatten(). TransformRequests(o.transformRequests). Do() if o.IgnoreNotFound { r.IgnoreErrors(apierrors.IsNotFound) } if err := r.Err(); err != nil { return err } if !o.IsHumanReadablePrinter { return o.printGeneric(r) } allErrs := []error{} errs := sets.New[string]() infos, err := r.Infos() if err != nil { allErrs = append(allErrs, err) } printWithKind := multipleGVKsRequested(infos) objs := make([]runtime.Object, len(infos)) for ix := range infos { objs[ix] = infos[ix].Object } var positioner OriginalPositioner if len(o.SortBy) > 0 { sorter := NewRuntimeSorter(objs, o.SortBy) if err := sorter.Sort(); err != nil { return err } positioner = sorter } var printer printers.ResourcePrinter var lastMapping *meta.RESTMapping // track if we write any output trackingWriter := &trackingWriterWrapper{Delegate: o.Out} // output an empty line separating output separatorWriter := &separatorWriterWrapper{Delegate: trackingWriter} w := printers.GetNewTabWriter(separatorWriter) allResourcesNamespaced := !o.AllNamespaces for ix := range objs { var mapping *meta.RESTMapping var info *resource.Info if positioner != nil { info = infos[positioner.OriginalPosition(ix)] mapping = info.Mapping } else { info = infos[ix] mapping = info.Mapping } allResourcesNamespaced = allResourcesNamespaced && info.Namespaced() printWithNamespace := o.AllNamespaces if mapping != nil && mapping.Scope.Name() == meta.RESTScopeNameRoot { printWithNamespace = false } if shouldGetNewPrinterForMapping(printer, lastMapping, mapping) { w.Flush() w.SetRememberedWidths(nil) // add linebreaks between resource groups (if there is more than one) // when it satisfies all following 3 conditions: // 1) it's not the first resource group // 2) it has row header // 3) we've written output since the last time we started a new set of headers if lastMapping != nil && !o.NoHeaders && trackingWriter.Written > 0 { separatorWriter.SetReady(true) } printer, err = o.ToPrinter(mapping, nil, printWithNamespace, printWithKind) if err != nil { if !errs.Has(err.Error()) { errs.Insert(err.Error()) allErrs = append(allErrs, err) } continue } lastMapping = mapping } printer.PrintObj(info.Object, w) } w.Flush() if trackingWriter.Written == 0 && !o.IgnoreNotFound && len(allErrs) == 0 { // if we wrote no output, and had no errors, and are not ignoring NotFound, be sure we output something if allResourcesNamespaced { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) } else { fmt.Fprintln(o.ErrOut, "No resources found") } } return utilerrors.NewAggregate(allErrs) } type trackingWriterWrapper struct { Delegate io.Writer Written int } func (t *trackingWriterWrapper) Write(p []byte) (n int, err error) { t.Written += len(p) return t.Delegate.Write(p) } type separatorWriterWrapper struct { Delegate io.Writer Ready bool } func (s *separatorWriterWrapper) Write(p []byte) (n int, err error) { // If we're about to write non-empty bytes and `s` is ready, // we prepend an empty line to `p` and reset `s.Read`. if len(p) != 0 && s.Ready { fmt.Fprintln(s.Delegate) s.Ready = false } return s.Delegate.Write(p) } func (s *separatorWriterWrapper) SetReady(state bool) { s.Ready = state } // watch starts a client-side watch of one or more resources. // TODO: remove the need for arguments here. func (o *GetOptions) watch(f cmdutil.Factory, args []string) error { r := f.NewBuilder(). Unstructured(). NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces). FilenameParam(o.ExplicitNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). FieldSelectorParam(o.FieldSelector). RequestChunksOf(o.ChunkSize). ResourceTypeOrNameArgs(true, args...). SingleResourceType(). Latest(). TransformRequests(o.transformRequests). Do() if err := r.Err(); err != nil { return err } infos, err := r.Infos() if err != nil { return err } if multipleGVKsRequested(infos) { return i18n.Errorf("watch is only supported on individual resources and resource collections - more than 1 resource was found") } info := infos[0] mapping := info.ResourceMapping() outputObjects := ptr.To(!o.WatchOnly) printer, err := o.ToPrinter(mapping, outputObjects, o.AllNamespaces, false) if err != nil { return err } obj, err := r.Object() if err != nil { return err } // watching from resourceVersion 0, starts the watch at ~now and // will return an initial watch event. Starting form ~now, rather // the rv of the object will insure that we start the watch from // inside the watch window, which the rv of the object might not be. rv := "0" isList := meta.IsListType(obj) if isList { // the resourceVersion of list objects is ~now but won't return // an initial watch event rv, err = meta.NewAccessor().ResourceVersion(obj) if err != nil { return err } } writer := printers.GetNewTabWriter(o.Out) // print the current object var objsToPrint []runtime.Object if isList { objsToPrint, _ = meta.ExtractList(obj) } else { objsToPrint = append(objsToPrint, obj) } for _, objToPrint := range objsToPrint { if o.OutputWatchEvents { objToPrint = &metav1.WatchEvent{Type: string(watch.Added), Object: runtime.RawExtension{Object: objToPrint}} } if err := printer.PrintObj(objToPrint, writer); err != nil { return fmt.Errorf("unable to output the provided object: %v", err) } } writer.Flush() if isList { // we can start outputting objects now, watches started from lists don't emit synthetic added events *outputObjects = true } else { // suppress output, since watches started for individual items emit a synthetic ADDED event first *outputObjects = false } // print watched changes w, err := r.Watch(rv) if err != nil { return err } ctx, cancel := context.WithCancel(context.Background()) defer cancel() intr := interrupt.New(nil, cancel) intr.Run(func() error { _, err := watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) { objToPrint := e.Object if o.OutputWatchEvents { objToPrint = &metav1.WatchEvent{Type: string(e.Type), Object: runtime.RawExtension{Object: objToPrint}} } if err := printer.PrintObj(objToPrint, writer); err != nil { return false, err } writer.Flush() // after processing at least one event, start outputting objects *outputObjects = true return false, nil }) return err }) return nil } func (o *GetOptions) printGeneric(r *resource.Result) error { // we flattened the data from the builder, so we have individual items, but now we'd like to either: // 1. if there is more than one item, combine them all into a single list // 2. if there is a single item and that item is a list, leave it as its specific list // 3. if there is a single item and it is not a list, leave it as a single item var errs []error singleItemImplied := false infos, err := r.IntoSingleItemImplied(&singleItemImplied).Infos() if err != nil { if singleItemImplied { return err } errs = append(errs, err) } if len(infos) == 0 && o.IgnoreNotFound { return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs))) } printer, err := o.ToPrinter(nil, nil, false, false) if err != nil { return err } var obj runtime.Object if !singleItemImplied || len(infos) != 1 { // we have zero or multple items, so coerce all items into a list. // we don't want an *unstructured.Unstructured list yet, as we // may be dealing with non-unstructured objects. Compose all items // into an corev1.List, and then decode using an unstructured scheme. list := corev1.List{ TypeMeta: metav1.TypeMeta{ Kind: "List", APIVersion: "v1", }, ListMeta: metav1.ListMeta{}, } for _, info := range infos { list.Items = append(list.Items, runtime.RawExtension{Object: info.Object}) } listData, err := json.Marshal(list) if err != nil { return err } converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, listData) if err != nil { return err } obj = converted } else { obj = infos[0].Object } isList := meta.IsListType(obj) if isList { items, err := meta.ExtractList(obj) if err != nil { return err } // take the items and create a new list for display list := &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{}, }, } if listMeta, err := meta.ListAccessor(obj); err == nil { list.Object["metadata"] = map[string]interface{}{ "resourceVersion": listMeta.GetResourceVersion(), } } for _, item := range items { list.Items = append(list.Items, *item.(*unstructured.Unstructured)) } if err := printer.PrintObj(list, o.Out); err != nil { errs = append(errs, err) } return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs))) } if printErr := printer.PrintObj(obj, o.Out); printErr != nil { errs = append(errs, printErr) } return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs))) } func addServerPrintColumnFlags(cmd *cobra.Command, opt *GetOptions) { cmd.Flags().BoolVar(&opt.ServerPrint, useServerPrintColumns, opt.ServerPrint, "If true, have the server return the appropriate table output. Supports extension APIs and CRDs.") } func shouldGetNewPrinterForMapping(printer printers.ResourcePrinter, lastMapping, mapping *meta.RESTMapping) bool { return printer == nil || lastMapping == nil || mapping == nil || mapping.Resource != lastMapping.Resource } func multipleGVKsRequested(infos []*resource.Info) bool { if len(infos) < 2 { return false } gvk := infos[0].Mapping.GroupVersionKind for _, info := range infos { if info.Mapping.GroupVersionKind != gvk { return true } } return false } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/get_flags.go000066400000000000000000000134241504711711200276540ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "fmt" "strings" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/kubectl/pkg/cmd/util" ) // PrintFlags composes common printer flag structs // used in the Get command. type PrintFlags struct { JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags NamePrintFlags *genericclioptions.NamePrintFlags CustomColumnsFlags *CustomColumnsPrintFlags HumanReadableFlags *HumanPrintFlags TemplateFlags *genericclioptions.KubeTemplatePrintFlags NoHeaders *bool OutputFormat *string } // SetKind sets the Kind option of humanreadable flags func (f *PrintFlags) SetKind(kind schema.GroupKind) { f.HumanReadableFlags.SetKind(kind) } // EnsureWithNamespace ensures that humanreadable flags return // a printer capable of printing with a "namespace" column. func (f *PrintFlags) EnsureWithNamespace() error { return f.HumanReadableFlags.EnsureWithNamespace() } // EnsureWithKind ensures that humanreadable flags return // a printer capable of including resource kinds. func (f *PrintFlags) EnsureWithKind() error { return f.HumanReadableFlags.EnsureWithKind() } // Copy returns a copy of PrintFlags for mutation func (f *PrintFlags) Copy() PrintFlags { printFlags := *f return printFlags } // AllowedFormats is the list of formats in which data can be displayed func (f *PrintFlags) AllowedFormats() []string { formats := f.JSONYamlPrintFlags.AllowedFormats() formats = append(formats, f.NamePrintFlags.AllowedFormats()...) formats = append(formats, f.TemplateFlags.AllowedFormats()...) formats = append(formats, f.CustomColumnsFlags.AllowedFormats()...) formats = append(formats, f.HumanReadableFlags.AllowedFormats()...) return formats } // ToPrinter attempts to find a composed set of PrintFlags suitable for // returning a printer based on current flag values. func (f *PrintFlags) ToPrinter() (printers.ResourcePrinter, error) { outputFormat := "" if f.OutputFormat != nil { outputFormat = *f.OutputFormat } noHeaders := false if f.NoHeaders != nil { noHeaders = *f.NoHeaders } f.HumanReadableFlags.NoHeaders = noHeaders f.CustomColumnsFlags.NoHeaders = noHeaders // for "get.go" we want to support a --template argument given, even when no --output format is provided if f.TemplateFlags.TemplateArgument != nil && len(*f.TemplateFlags.TemplateArgument) > 0 && len(outputFormat) == 0 { outputFormat = "go-template" } if p, err := f.TemplateFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) { return p, err } if f.TemplateFlags.TemplateArgument != nil { f.CustomColumnsFlags.TemplateArgument = *f.TemplateFlags.TemplateArgument } if p, err := f.JSONYamlPrintFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) { return p, err } if p, err := f.HumanReadableFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) { return p, err } if p, err := f.CustomColumnsFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) { return p, err } if p, err := f.NamePrintFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) { return p, err } return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()} } // AddFlags receives a *cobra.Command reference and binds // flags related to humanreadable and template printing. func (f *PrintFlags) AddFlags(cmd *cobra.Command) { f.JSONYamlPrintFlags.AddFlags(cmd) f.NamePrintFlags.AddFlags(cmd) f.TemplateFlags.AddFlags(cmd) f.HumanReadableFlags.AddFlags(cmd) f.CustomColumnsFlags.AddFlags(cmd) if f.OutputFormat != nil { cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: (%s). See custom columns [https://kubernetes.io/docs/reference/kubectl/#custom-columns], golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [https://kubernetes.io/docs/reference/kubectl/jsonpath/].`, strings.Join(f.AllowedFormats(), ", "))) util.CheckErr(cmd.RegisterFlagCompletionFunc( "output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var comps []string for _, format := range f.AllowedFormats() { if strings.HasPrefix(format, toComplete) { comps = append(comps, format) } } return comps, cobra.ShellCompDirectiveNoFileComp }, )) } if f.NoHeaders != nil { cmd.Flags().BoolVar(f.NoHeaders, "no-headers", *f.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).") } } // NewGetPrintFlags returns flags associated with humanreadable, // template, and "name" printing, with default values set. func NewGetPrintFlags() *PrintFlags { outputFormat := "" noHeaders := false return &PrintFlags{ OutputFormat: &outputFormat, NoHeaders: &noHeaders, JSONYamlPrintFlags: genericclioptions.NewJSONYamlPrintFlags(), NamePrintFlags: genericclioptions.NewNamePrintFlags(""), TemplateFlags: genericclioptions.NewKubeTemplatePrintFlags(), HumanReadableFlags: NewHumanPrintFlags(), CustomColumnsFlags: NewCustomColumnsPrintFlags(), } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/get_test.go000066400000000000000000003137341504711711200275460ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "bytes" "encoding/json" "fmt" "io" "net/http" "reflect" "strings" "testing" "github.com/google/go-cmp/cmp" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" batchv1 "k8s.io/api/batch/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 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/serializer/streaming" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" restclientwatch "k8s.io/client-go/rest/watch" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) var ( grace = int64(30) enableServiceLinks = corev1.DefaultEnableServiceLinks ) func testComponentStatusData() *corev1.ComponentStatusList { good := corev1.ComponentStatus{ Conditions: []corev1.ComponentCondition{ {Type: corev1.ComponentHealthy, Status: corev1.ConditionTrue, Message: "ok"}, }, ObjectMeta: metav1.ObjectMeta{Name: "servergood"}, } bad := corev1.ComponentStatus{ Conditions: []corev1.ComponentCondition{ {Type: corev1.ComponentHealthy, Status: corev1.ConditionFalse, Message: "", Error: "bad status: 500"}, }, ObjectMeta: metav1.ObjectMeta{Name: "serverbad"}, } unknown := corev1.ComponentStatus{ Conditions: []corev1.ComponentCondition{ {Type: corev1.ComponentHealthy, Status: corev1.ConditionUnknown, Message: "", Error: "fizzbuzz error"}, }, ObjectMeta: metav1.ObjectMeta{Name: "serverunknown"}, } return &corev1.ComponentStatusList{ Items: []corev1.ComponentStatus{good, bad, unknown}, } } // Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. func TestGetUnknownSchemaObject(t *testing.T) { t.Skip("This test is completely broken. The first thing it does is add the object to the scheme!") tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() _, _, codec := cmdtesting.NewExternalScheme() obj := &cmdtesting.ExternalType{ Kind: "Type", APIVersion: "apitest/unlikelyversion", Name: "foo", } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj), }, } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"type", "foo"}) expected := []runtime.Object{cmdtesting.NewInternalType("", "", "foo")} actual := []runtime.Object{} if len(actual) != len(expected) { t.Fatalf("expected: %#v, but actual: %#v", expected, actual) } t.Logf("actual: %#v", actual[0]) for i, obj := range actual { expectedJSON := runtime.EncodeOrDie(codec, expected[i]) expectedMap := map[string]interface{}{} if err := json.Unmarshal([]byte(expectedJSON), &expectedMap); err != nil { t.Fatal(err) } actualJSON := runtime.EncodeOrDie(codec, obj) actualMap := map[string]interface{}{} if err := json.Unmarshal([]byte(actualJSON), &actualMap); err != nil { t.Fatal(err) } if !reflect.DeepEqual(expectedMap, actualMap) { t.Errorf("expectedMap: %#v, but actualMap: %#v", expectedMap, actualMap) } } } // Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. func TestGetSchemaObject(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(corev1.SchemeGroupVersion) t.Logf("%v", string(runtime.EncodeOrDie(codec, &corev1.ReplicationController{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}))) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.ReplicationController{ObjectMeta: metav1.ObjectMeta{Name: "foo"}})}, } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.Run(cmd, []string{"replicationcontrollers", "foo"}) if !strings.Contains(buf.String(), "foo") { t.Errorf("unexpected output: %s", buf.String()) } } func TestGetObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME AGE foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetObjectSubresourceStatus(t *testing.T) { _, _, replicationcontrollers := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &replicationcontrollers.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("subresource", "status") cmd.Run(cmd, []string{"replicationcontrollers", "rc1"}) expected := `NAME AGE rc1 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetObjectSubresourceScale(t *testing.T) { _, _, replicationcontrollers := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: replicationControllersScaleSubresourceTableObjBody(codec, replicationcontrollers.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("subresource", "scale") cmd.Run(cmd, []string{"replicationcontrollers", "rc1"}) expected := `NAME DESIRED AVAILABLE rc1 1 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetTableObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME READY STATUS RESTARTS AGE foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetV1TableObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podV1TableObjBody(codec, pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME READY STATUS RESTARTS AGE foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetObjectsShowKind(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("show-kind", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME AGE pod/foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetTableObjectsShowKind(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("show-kind", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleResourceTypesShowKinds(t *testing.T) { pods, svcs, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/pods" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil case p == "/namespaces/test/replicationcontrollers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.ReplicationControllerList{})}, nil case p == "/namespaces/test/services" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svcs)}, nil case p == "/namespaces/test/statefulsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.StatefulSetList{})}, nil case p == "/namespaces/test/horizontalpodautoscalers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &autoscalingv1.HorizontalPodAutoscalerList{})}, nil case p == "/namespaces/test/jobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &batchv1.JobList{})}, nil case p == "/namespaces/test/cronjobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &batchv1beta1.CronJobList{})}, nil case p == "/namespaces/test/daemonsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.DaemonSetList{})}, nil case p == "/namespaces/test/deployments" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &extensionsv1beta1.DeploymentList{})}, nil case p == "/namespaces/test/replicasets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &extensionsv1beta1.ReplicaSetList{})}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, bufErr := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"all"}) expected := `NAME AGE pod/foo pod/bar NAME AGE service/baz ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } // The error out should be empty if e, a := "", bufErr.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTableResourceTypesShowKinds(t *testing.T) { pods, svcs, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/pods" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil case p == "/namespaces/test/replicationcontrollers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.ReplicationControllerList{})}, nil case p == "/namespaces/test/services" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svcs.Items...)}, nil case p == "/namespaces/test/statefulsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.StatefulSetList{})}, nil case p == "/namespaces/test/horizontalpodautoscalers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &autoscalingv1.HorizontalPodAutoscalerList{})}, nil case p == "/namespaces/test/jobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &batchv1.JobList{})}, nil case p == "/namespaces/test/cronjobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &batchv1beta1.CronJobList{})}, nil case p == "/namespaces/test/daemonsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.DaemonSetList{})}, nil case p == "/namespaces/test/deployments" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &extensionsv1beta1.DeploymentList{})}, nil case p == "/namespaces/test/replicasets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &extensionsv1beta1.ReplicaSetList{})}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, bufErr := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"all"}) expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } // The error out should be empty if e, a := "", bufErr.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestNoBlankLinesForGetMultipleTableResource(t *testing.T) { pods, svcs, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/pods" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil case p == "/namespaces/test/replicationcontrollers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/services" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svcs.Items...)}, nil case p == "/namespaces/test/statefulsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/horizontalpodautoscalers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/jobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/cronjobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/daemonsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/deployments" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/replicasets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, bufErr := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` for _, cmdArgs := range [][]string{ {"pods,services,jobs"}, {"deployments,pods,statefulsets,services,jobs"}, {"all"}, } { cmd.Run(cmd, cmdArgs) if e, a := expected, buf.String(); e != a { t.Errorf("[kubectl get %v] expected\n%v\ngot\n%v", cmdArgs, e, a) } // The error out should be empty if e, a := "", bufErr.String(); e != a { t.Errorf("[kubectl get %v] expected\n%v\ngot\n%v", cmdArgs, e, a) } buf.Reset() bufErr.Reset() } } func TestNoBlankLinesForGetAll(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/pods" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/replicationcontrollers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/services" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/statefulsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/horizontalpodautoscalers" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/jobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/cronjobs" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/daemonsets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/deployments" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil case p == "/namespaces/test/replicasets" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"all"}) expected := `` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } expectedErr := `No resources found in test namespace. ` if e, a := expectedErr, errbuf.String(); e != a { t.Errorf("expectedErr\n%v\ngot\n%v", e, a) } } func TestNotFoundMessageForGetNonNamespacedResources(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, } streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"persistentvolumes"}) expected := `` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } expectedErr := `No resources found ` if e, a := expectedErr, errbuf.String(); e != a { t.Errorf("expectedErr\n%v\ngot\n%v", e, a) } } func TestGetObjectsShowLabels(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("show-labels", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME AGE LABELS foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetTableObjectsShowLabels(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("show-labels", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME READY STATUS RESTARTS AGE LABELS foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetEmptyTable(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() emptyTable := io.NopCloser(bytes.NewBufferString(`{ "kind":"Table", "apiVersion":"meta.k8s.io/v1beta1", "metadata":{ "resourceVersion":"346" }, "columnDefinitions":[ {"name":"Name","type":"string","format":"name","description":"the name","priority":0} ], "rows":[] }`)) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: emptyTable}, } streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods"}) expected := `` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } expectedErr := `No resources found in test namespace. ` if e, a := expectedErr, errbuf.String(); e != a { t.Errorf("expectedErr\n%v\ngot\n%v", e, a) } } func TestGetObjectIgnoreNotFound(t *testing.T) { cmdtesting.InitTestErrorHandler(t) ns := &corev1.NamespaceList{ ListMeta: metav1.ListMeta{ ResourceVersion: "1", }, Items: []corev1.Namespace{ { ObjectMeta: metav1.ObjectMeta{Name: "testns", Namespace: "test", ResourceVersion: "11"}, Spec: corev1.NamespaceSpec{}, }, }, } tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/pods/nonexistentpod" && m == "GET": return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &ns.Items[0])}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("ignore-not-found", "true") cmd.Flags().Set("output", "yaml") cmd.Run(cmd, []string{"pods", "nonexistentpod"}) if buf.String() != "" { t.Errorf("unexpected output: %s", buf.String()) } } func TestEmptyResult(t *testing.T) { cmdtesting.InitTestErrorHandler(t) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.PodList{})}, nil }), } streams, _, _, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) // we're assuming that an empty file is being passed from stdin cmd.Flags().Set("filename", "-") cmd.Run(cmd, []string{}) if !strings.Contains(errbuf.String(), "No resources found") { t.Errorf("unexpected output: %q", errbuf.String()) } } func TestEmptyResultJSON(t *testing.T) { cmdtesting.InitTestErrorHandler(t) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.PodList{})}, nil }), } streams, _, outbuf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) // we're assuming that an empty file is being passed from stdin cmd.Flags().Set("filename", "-") cmd.Flags().Set("output", "json") cmd.Run(cmd, []string{}) if errbuf.Len() > 0 { t.Errorf("unexpected error: %q", errbuf.String()) } if !strings.Contains(outbuf.String(), `"items": []`) { t.Errorf("unexpected output: %q", outbuf.String()) } } func TestGetSortedObjects(t *testing.T) { pods := &corev1.PodList{ ListMeta: metav1.ListMeta{ ResourceVersion: "15", }, Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "11"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "9"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, } tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &corev1.SchemeGroupVersion}} streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) // sorting with metadata.name cmd.Flags().Set("sort-by", ".metadata.name") cmd.Run(cmd, []string{"pods"}) expected := `NAME AGE a b c ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetSortedObjectsUnstructuredTable(t *testing.T) { unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(sortTestTableData()[0]) if err != nil { t.Fatal(err) } unstructuredBytes, err := json.MarshalIndent(unstructuredMap, "", " ") if err != nil { t.Fatal(err) } // t.Log(string(unstructuredBytes)) body := io.NopCloser(bytes.NewReader(unstructuredBytes)) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &corev1.SchemeGroupVersion}} streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) // sorting with metadata.name cmd.Flags().Set("sort-by", ".metadata.name") cmd.Run(cmd, []string{"pods"}) expected := `NAME CUSTOM a custom-a b custom-b c custom-c ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func sortTestData() []runtime.Object { return []runtime.Object{ &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "11"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "9"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, } } func sortTestTableData() []runtime.Object { return []runtime.Object{ &metav1beta1.Table{ TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1beta1", Kind: "Table"}, ColumnDefinitions: []metav1beta1.TableColumnDefinition{ {Name: "NAME", Type: "string", Format: "name"}, {Name: "CUSTOM", Type: "string", Format: ""}, }, Rows: []metav1beta1.TableRow{ { Cells: []interface{}{"c", "custom-c"}, Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, }, { Cells: []interface{}{"b", "custom-b"}, Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "11"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, }, { Cells: []interface{}{"a", "custom-a"}, Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "9"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, }, }, }, } } func TestRuntimeSorter(t *testing.T) { tests := []struct { name string field string objs []runtime.Object op func(sorter *RuntimeSorter, objs []runtime.Object, out io.Writer) error expect string expectError string }{ { name: "ensure sorter works with an empty object list", field: "metadata.name", objs: []runtime.Object{}, op: func(sorter *RuntimeSorter, objs []runtime.Object, out io.Writer) error { return nil }, expect: "", }, { name: "ensure sorter returns original position", field: "metadata.name", objs: sortTestData(), op: func(sorter *RuntimeSorter, objs []runtime.Object, out io.Writer) error { for idx := range objs { p := sorter.OriginalPosition(idx) fmt.Fprintf(out, "%v,", p) } return nil }, expect: "2,1,0,", }, { name: "ensure sorter handles table object position", field: "metadata.name", objs: sortTestTableData(), op: func(sorter *RuntimeSorter, objs []runtime.Object, out io.Writer) error { for idx := range objs { p := sorter.OriginalPosition(idx) fmt.Fprintf(out, "%v,", p) } return nil }, expect: "0,", }, { name: "ensure sorter sorts table objects", field: "metadata.name", objs: sortTestData(), op: func(sorter *RuntimeSorter, objs []runtime.Object, out io.Writer) error { for _, o := range objs { fmt.Fprintf(out, "%s,", o.(*corev1.Pod).Name) } return nil }, expect: "a,b,c,", }, { name: "ensure sorter rejects mixed Table + non-Table object lists", field: "metadata.name", objs: append(sortTestData(), sortTestTableData()...), op: func(sorter *RuntimeSorter, objs []runtime.Object, out io.Writer) error { return nil }, expectError: "sorting is not supported on mixed Table", }, { name: "ensure sorter errors out on invalid jsonpath", field: "metadata.unknown", objs: sortTestData(), op: func(sorter *RuntimeSorter, objs []runtime.Object, out io.Writer) error { return nil }, expectError: "couldn't find any field with path", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { sorter := NewRuntimeSorter(tc.objs, tc.field) if err := sorter.Sort(); err != nil { if len(tc.expectError) > 0 && strings.Contains(err.Error(), tc.expectError) { return } if len(tc.expectError) > 0 { t.Fatalf("unexpected error: expecting %s, but got %s", tc.expectError, err) } t.Fatalf("unexpected error: %v", err) } out := bytes.NewBuffer([]byte{}) err := tc.op(sorter, tc.objs, out) if err != nil { t.Fatalf("unexpected error: %v", err) } if tc.expect != out.String() { t.Fatalf("unexpected output: expecting %s, but got %s", tc.expect, out.String()) } }) } } func TestGetObjectsIdentifiedByFile(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("filename", "../../../testdata/controller.yaml") cmd.Run(cmd, []string{}) expected := `NAME AGE foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetTableObjectsIdentifiedByFile(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("filename", "../../../testdata/controller.yaml") cmd.Run(cmd, []string{}) expected := `NAME READY STATUS RESTARTS AGE foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetListObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods"}) expected := `NAME AGE foo bar ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetListTableObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods"}) expected := `NAME READY STATUS RESTARTS AGE foo 0/0 0 bar 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetListComponentStatus(t *testing.T) { statuses := testComponentStatusData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: componentStatusTableObjBody(codec, (*statuses).Items...)}, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"componentstatuses"}) expected := `NAME STATUS MESSAGE ERROR servergood Healthy ok serverbad Unhealthy bad status: 500 serverunknown Unhealthy fizzbuzz error ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMixedGenericObjects(t *testing.T) { cmdtesting.InitTestErrorHandler(t) // ensure that a runtime.Object without // an ObjectMeta field is handled properly structuredObj := &metav1.Status{ TypeMeta: metav1.TypeMeta{ Kind: "Status", APIVersion: "v1", }, Status: "Success", Message: "", Reason: "", Code: 0, } tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, structuredObj)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("output", "json") cmd.Run(cmd, []string{"pods"}) expected := `{ "apiVersion": "v1", "items": [ { "apiVersion": "v1", "kind": "Status", "metadata": {}, "status": "Success" } ], "kind": "List", "metadata": { "resourceVersion": "" } } ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeObjects(t *testing.T) { pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil case "/namespaces/test/services": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods,services"}) expected := `NAME AGE pod/foo pod/bar NAME AGE service/baz ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeTableObjects(t *testing.T) { pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil case "/namespaces/test/services": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items...)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"pods,services"}) expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeObjectsAsList(t *testing.T) { pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil case "/namespaces/test/services": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("output", "json") cmd.Run(cmd, []string{"pods,services"}) expected := `{ "apiVersion": "v1", "items": [ { "apiVersion": "v1", "kind": "Pod", "metadata": { "creationTimestamp": null, "name": "foo", "namespace": "test", "resourceVersion": "10" }, "spec": { "containers": null, "dnsPolicy": "ClusterFirst", "enableServiceLinks": true, "restartPolicy": "Always", "securityContext": {}, "terminationGracePeriodSeconds": 30 }, "status": {} }, { "apiVersion": "v1", "kind": "Pod", "metadata": { "creationTimestamp": null, "name": "bar", "namespace": "test", "resourceVersion": "11" }, "spec": { "containers": null, "dnsPolicy": "ClusterFirst", "enableServiceLinks": true, "restartPolicy": "Always", "securityContext": {}, "terminationGracePeriodSeconds": 30 }, "status": {} }, { "apiVersion": "v1", "kind": "Service", "metadata": { "creationTimestamp": null, "name": "baz", "namespace": "test", "resourceVersion": "12" }, "spec": { "sessionAffinity": "None", "type": "ClusterIP" }, "status": { "loadBalancer": {} } } ], "kind": "List", "metadata": { "resourceVersion": "" } } ` if e, a := expected, buf.String(); e != a { t.Errorf("did not match:\n%v", cmp.Diff(e, a)) } } func TestGetMultipleTypeObjectsWithLabelSelector(t *testing.T) { pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { t.Fatalf("request url: %#v,and request: %#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil case "/namespaces/test/services": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("selector", "a=b") cmd.Run(cmd, []string{"pods,services"}) expected := `NAME AGE pod/foo pod/bar NAME AGE service/baz ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeTableObjectsWithLabelSelector(t *testing.T) { pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { t.Fatalf("request url: %#v,and request: %#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil case "/namespaces/test/services": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items...)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("selector", "a=b") cmd.Run(cmd, []string{"pods,services"}) expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeObjectsWithFieldSelector(t *testing.T) { pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil case "/namespaces/test/services": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svc)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("field-selector", "a=b") cmd.Run(cmd, []string{"pods,services"}) expected := `NAME AGE pod/foo pod/bar NAME AGE service/baz ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeTableObjectsWithFieldSelector(t *testing.T) { pods, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil case "/namespaces/test/services": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items...)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("field-selector", "a=b") cmd.Run(cmd, []string{"pods,services"}) expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeObjectsWithDirectReference(t *testing.T) { _, svc, _ := cmdtesting.TestData() node := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, } tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/nodes/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, node)}, nil case "/namespaces/test/services/bar": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"services/bar", "node/foo"}) expected := `NAME AGE service/baz NAME AGE node/foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestGetMultipleTypeTableObjectsWithDirectReference(t *testing.T) { _, svc, _ := cmdtesting.TestData() node := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, } tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/nodes/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: nodeTableObjBody(codec, *node)}, nil case "/namespaces/test/services/bar": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items[0])}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Run(cmd, []string{"services/bar", "node/foo"}) expected := `NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP NAME STATUS ROLES AGE VERSION node/foo Unknown ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func watchTestData() ([]corev1.Pod, []watch.Event) { pods := []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "test", ResourceVersion: "9", }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "test", ResourceVersion: "10", }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, } events := []watch.Event{ // current state events { Type: watch.Added, Object: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "test", ResourceVersion: "9", }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, { Type: watch.Added, Object: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "test", ResourceVersion: "10", }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, // resource events { Type: watch.Modified, Object: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "test", ResourceVersion: "11", }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, { Type: watch.Deleted, Object: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "test", ResourceVersion: "12", }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, } return pods, events } func TestWatchLabelSelector(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) podList := &corev1.PodList{ Items: pods, ListMeta: metav1.ListMeta{ ResourceVersion: "10", }, } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { t.Fatalf("request url: %#v,and request: %#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[2:])}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, podList)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Flags().Set("selector", "a=b") cmd.Run(cmd, []string{"pods"}) expected := `NAME AGE bar foo foo foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchTableLabelSelector(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) podList := &corev1.PodList{ Items: pods, ListMeta: metav1.ListMeta{ ResourceVersion: "10", }, } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { t.Fatalf("request url: %#v,and request: %#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Flags().Set("selector", "a=b") cmd.Run(cmd, []string{"pods"}) expected := `NAME READY STATUS RESTARTS AGE bar 0/0 0 foo 0/0 0 foo 0/0 0 foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchFieldSelector(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) podList := &corev1.PodList{ Items: pods, ListMeta: metav1.ListMeta{ ResourceVersion: "10", }, } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[2:])}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, podList)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Flags().Set("field-selector", "a=b") cmd.Run(cmd, []string{"pods"}) expected := `NAME AGE bar foo foo foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchTableFieldSelector(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) podList := &corev1.PodList{ Items: pods, ListMeta: metav1.ListMeta{ ResourceVersion: "10", }, } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) } switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Flags().Set("field-selector", "a=b") cmd.Run(cmd, []string{"pods"}) expected := `NAME READY STATUS RESTARTS AGE bar 0/0 0 foo 0/0 0 foo 0/0 0 foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchResource(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods[1])}, nil case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=foo" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[1:])}, nil } t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME AGE foo foo foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchStatus(t *testing.T) { pods, events := watchTestData() events = append(events, watch.Event{Type: "ERROR", Object: &metav1.Status{Status: "Failure", Reason: "InternalServerError", Message: "Something happened"}}) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods[1])}, nil case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=foo" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[1:])}, nil } t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME AGE foo foo foo STATUS REASON MESSAGE Failure InternalServerError Something happened ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchTableResource(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods[1])}, nil case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=foo" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[1:])}, nil } t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME READY STATUS RESTARTS AGE foo 0/0 0 foo 0/0 0 foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchResourceTable(t *testing.T) { columns := []metav1beta1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: "the name", Priority: 0}, {Name: "Active", Type: "boolean", Description: "active", Priority: 0}, } listTable := &metav1beta1.Table{ TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1beta1", Kind: "Table"}, ColumnDefinitions: columns, Rows: []metav1beta1.TableRow{ { Cells: []interface{}{"a", true}, Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "10"}, }, }, }, { Cells: []interface{}{"b", true}, Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "20"}, }, }, }, }, } events := []watch.Event{ { Type: watch.Added, Object: &metav1beta1.Table{ TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1beta1", Kind: "Table"}, ColumnDefinitions: columns, // first event includes the columns Rows: []metav1beta1.TableRow{{ Cells: []interface{}{"a", false}, Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "30"}, }, }, }}, }, }, { Type: watch.Deleted, Object: &metav1beta1.Table{ ColumnDefinitions: []metav1beta1.TableColumnDefinition{}, Rows: []metav1beta1.TableRow{{ Cells: []interface{}{"b", false}, Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "40"}, }, }, }}, }, }, } tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("watch") != "true" && req.URL.Query().Get("fieldSelector") == "" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, listTable)}, nil } if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events)}, nil } t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Run(cmd, []string{"pods"}) expected := `NAME ACTIVE a true b true a false b false ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchResourceWatchEvents(t *testing.T) { testcases := []struct { format string table bool expected string }{ { format: "", expected: `EVENT NAMESPACE NAME AGE ADDED test pod/bar ADDED test pod/foo MODIFIED test pod/foo DELETED test pod/foo `, }, { format: "", table: true, expected: `EVENT NAMESPACE NAME READY STATUS RESTARTS AGE ADDED test pod/bar 0/0 0 ADDED test pod/foo 0/0 0 MODIFIED test pod/foo 0/0 0 DELETED test pod/foo 0/0 0 `, }, { format: "wide", table: true, expected: `EVENT NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES ADDED test pod/bar 0/0 0 ADDED test pod/foo 0/0 0 MODIFIED test pod/foo 0/0 0 DELETED test pod/foo 0/0 0 `, }, { format: "json", expected: `{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"bar","namespace":"test","resourceVersion":"9"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} {"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"10"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} {"type":"MODIFIED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"11"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} {"type":"DELETED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"12"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} `, }, { format: "yaml", expected: `object: apiVersion: v1 kind: Pod metadata: creationTimestamp: null name: bar namespace: test resourceVersion: "9" spec: containers: null dnsPolicy: ClusterFirst enableServiceLinks: true restartPolicy: Always securityContext: {} terminationGracePeriodSeconds: 30 status: {} type: ADDED --- object: apiVersion: v1 kind: Pod metadata: creationTimestamp: null name: foo namespace: test resourceVersion: "10" spec: containers: null dnsPolicy: ClusterFirst enableServiceLinks: true restartPolicy: Always securityContext: {} terminationGracePeriodSeconds: 30 status: {} type: ADDED --- object: apiVersion: v1 kind: Pod metadata: creationTimestamp: null name: foo namespace: test resourceVersion: "11" spec: containers: null dnsPolicy: ClusterFirst enableServiceLinks: true restartPolicy: Always securityContext: {} terminationGracePeriodSeconds: 30 status: {} type: MODIFIED --- object: apiVersion: v1 kind: Pod metadata: creationTimestamp: null name: foo namespace: test resourceVersion: "12" spec: containers: null dnsPolicy: ClusterFirst enableServiceLinks: true restartPolicy: Always securityContext: {} terminationGracePeriodSeconds: 30 status: {} type: DELETED `, }, { format: `jsonpath={.type},{.object.metadata.name},{.object.metadata.resourceVersion}{"\n"}`, expected: `ADDED,bar,9 ADDED,foo,10 MODIFIED,foo,11 DELETED,foo,12 `, }, { format: `go-template={{.type}},{{.object.metadata.name}},{{.object.metadata.resourceVersion}}{{"\n"}}`, expected: `ADDED,bar,9 ADDED,foo,10 MODIFIED,foo,11 DELETED,foo,12 `, }, { format: `custom-columns=TYPE:.type,NAME:.object.metadata.name,RSRC:.object.metadata.resourceVersion`, expected: `TYPE NAME RSRC ADDED bar 9 ADDED foo 10 MODIFIED foo 11 DELETED foo 12 `, }, { format: `name`, expected: `pod/bar pod/foo pod/foo pod/foo `, }, } for _, tc := range testcases { t.Run(fmt.Sprintf("%s, table=%v", tc.format, tc.table), func(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) podList := &corev1.PodList{ Items: pods, ListMeta: metav1.ListMeta{ ResourceVersion: "10", }, } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/pods": if req.URL.Query().Get("watch") == "true" { if tc.table { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil } else { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[2:])}, nil } } if tc.table { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil } else { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, podList)}, nil } default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Flags().Set("all-namespaces", "true") cmd.Flags().Set("show-kind", "true") cmd.Flags().Set("output-watch-events", "true") if len(tc.format) > 0 { cmd.Flags().Set("output", tc.format) } cmd.Run(cmd, []string{"pods"}) if e, a := tc.expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } }) } } func TestWatchResourceIdentifiedByFile(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/replicationcontrollers/cassandra": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods[1])}, nil case "/namespaces/test/replicationcontrollers": if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=cassandra" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[1:])}, nil } t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch", "true") cmd.Flags().Set("filename", "../../../testdata/controller.yaml") cmd.Run(cmd, []string{}) expected := `NAME AGE foo foo foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchOnlyResource(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods[1])}, nil case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=foo" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[1:])}, nil } t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch-only", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME AGE foo foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchOnlyTableResource(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods[1])}, nil case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=foo" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[1:])}, nil } t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch-only", "true") cmd.Run(cmd, []string{"pods", "foo"}) expected := `NAME READY STATUS RESTARTS AGE foo 0/0 0 foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchOnlyList(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) podList := &corev1.PodList{ Items: pods, ListMeta: metav1.ListMeta{ ResourceVersion: "10", }, } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[2:])}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, podList)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch-only", "true") cmd.Run(cmd, []string{"pods"}) expected := `NAME AGE foo foo ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func TestWatchOnlyTableList(t *testing.T) { pods, events := watchTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) podList := &corev1.PodList{ Items: pods, ListMeta: metav1.ListMeta{ ResourceVersion: "10", }, } tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("watch") == "true" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil default: t.Fatalf("request url: %#v,and request: %#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("watch-only", "true") cmd.Run(cmd, []string{"pods"}) expected := `NAME READY STATUS RESTARTS AGE foo 0/0 0 foo 0/0 0 ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } } func watchBody(codec runtime.Codec, events []watch.Event) io.ReadCloser { buf := bytes.NewBuffer([]byte{}) enc := restclientwatch.NewEncoder(streaming.NewEncoder(buf, codec), codec) for i := range events { if err := enc.Encode(&events[i]); err != nil { panic(err) } } return io.NopCloser(buf) } var podColumns = []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, {Name: "Ready", Type: "string", Format: ""}, {Name: "Status", Type: "string", Format: ""}, {Name: "Restarts", Type: "integer", Format: ""}, {Name: "Age", Type: "string", Format: ""}, {Name: "IP", Type: "string", Format: "", Priority: 1}, {Name: "Node", Type: "string", Format: "", Priority: 1}, {Name: "Nominated Node", Type: "string", Format: "", Priority: 1}, {Name: "Readiness Gates", Type: "string", Format: "", Priority: 1}, } // build a meta table response from a pod list func podTableObjBody(codec runtime.Codec, pods ...corev1.Pod) io.ReadCloser { table := &metav1beta1.Table{ TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1beta1", Kind: "Table"}, ColumnDefinitions: podColumns, } for i := range pods { b := bytes.NewBuffer(nil) codec.Encode(&pods[i], b) table.Rows = append(table.Rows, metav1beta1.TableRow{ Object: runtime.RawExtension{Raw: b.Bytes()}, Cells: []interface{}{pods[i].Name, "0/0", "", int64(0), "", "", "", "", ""}, }) } data, err := json.Marshal(table) if err != nil { panic(err) } if !strings.Contains(string(data), `"meta.k8s.io/v1beta1"`) { panic("expected v1beta1, got " + string(data)) } return cmdtesting.BytesBody(data) } // build a meta table response from a pod list func podV1TableObjBody(codec runtime.Codec, pods ...corev1.Pod) io.ReadCloser { table := &metav1.Table{ TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1", Kind: "Table"}, ColumnDefinitions: podColumns, } for i := range pods { b := bytes.NewBuffer(nil) codec.Encode(&pods[i], b) table.Rows = append(table.Rows, metav1.TableRow{ Object: runtime.RawExtension{Raw: b.Bytes()}, Cells: []interface{}{pods[i].Name, "0/0", "", int64(0), "", "", "", "", ""}, }) } data, err := json.Marshal(table) if err != nil { panic(err) } if !strings.Contains(string(data), `"meta.k8s.io/v1"`) { panic("expected v1, got " + string(data)) } return cmdtesting.BytesBody(data) } // build meta table watch events from pod watch events func podTableWatchBody(codec runtime.Codec, events []watch.Event) io.ReadCloser { tableEvents := []watch.Event{} for i, e := range events { b := bytes.NewBuffer(nil) codec.Encode(e.Object, b) var columns []metav1.TableColumnDefinition if i == 0 { columns = podColumns } tableEvents = append(tableEvents, watch.Event{ Type: e.Type, Object: &metav1.Table{ ColumnDefinitions: columns, Rows: []metav1.TableRow{{ Object: runtime.RawExtension{Raw: b.Bytes()}, Cells: []interface{}{e.Object.(*corev1.Pod).Name, "0/0", "", int64(0), "", "", "", "", ""}, }}}, }) } return watchBody(codec, tableEvents) } // build a meta table response from a service list func serviceTableObjBody(codec runtime.Codec, services ...corev1.Service) io.ReadCloser { table := &metav1.Table{ ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, {Name: "Type", Type: "string", Format: ""}, {Name: "Cluster-IP", Type: "string", Format: ""}, {Name: "External-IP", Type: "string", Format: ""}, {Name: "Port(s)", Type: "string", Format: ""}, {Name: "Age", Type: "string", Format: ""}, }, } for i := range services { b := bytes.NewBuffer(nil) codec.Encode(&services[i], b) table.Rows = append(table.Rows, metav1.TableRow{ Object: runtime.RawExtension{Raw: b.Bytes()}, Cells: []interface{}{services[i].Name, "ClusterIP", "", "", "", ""}, }) } return cmdtesting.ObjBody(codec, table) } // build a meta table response from a node list func nodeTableObjBody(codec runtime.Codec, nodes ...corev1.Node) io.ReadCloser { table := &metav1.Table{ ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, {Name: "Status", Type: "string", Format: ""}, {Name: "Roles", Type: "string", Format: ""}, {Name: "Age", Type: "string", Format: ""}, {Name: "Version", Type: "string", Format: ""}, }, } for i := range nodes { b := bytes.NewBuffer(nil) codec.Encode(&nodes[i], b) table.Rows = append(table.Rows, metav1.TableRow{ Object: runtime.RawExtension{Raw: b.Bytes()}, Cells: []interface{}{nodes[i].Name, "Unknown", "", "", ""}, }) } return cmdtesting.ObjBody(codec, table) } // build a meta table response from a componentStatus list func componentStatusTableObjBody(codec runtime.Codec, componentStatuses ...corev1.ComponentStatus) io.ReadCloser { table := &metav1.Table{ ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, {Name: "Status", Type: "string", Format: ""}, {Name: "Message", Type: "string", Format: ""}, {Name: "Error", Type: "string", Format: ""}, }, } for _, v := range componentStatuses { b := bytes.NewBuffer(nil) codec.Encode(&v, b) var status string if v.Conditions[0].Status == corev1.ConditionTrue { status = "Healthy" } else { status = "Unhealthy" } table.Rows = append(table.Rows, metav1.TableRow{ Object: runtime.RawExtension{Raw: b.Bytes()}, Cells: []interface{}{v.Name, status, v.Conditions[0].Message, v.Conditions[0].Error}, }) } return cmdtesting.ObjBody(codec, table) } // build an empty table response func emptyTableObjBody(codec runtime.Codec) io.ReadCloser { table := &metav1.Table{ ColumnDefinitions: podColumns, } return cmdtesting.ObjBody(codec, table) } func replicationControllersScaleSubresourceTableObjBody(codec runtime.Codec, replicationControllers ...corev1.ReplicationController) io.ReadCloser { table := &metav1.Table{ ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "Desired", Type: "integer", Description: autoscalingv1.ScaleSpec{}.SwaggerDoc()["replicas"]}, {Name: "Available", Type: "integer", Description: autoscalingv1.ScaleStatus{}.SwaggerDoc()["replicas"]}, }, } for i := range replicationControllers { b := bytes.NewBuffer(nil) codec.Encode(&replicationControllers[i], b) table.Rows = append(table.Rows, metav1.TableRow{ Object: runtime.RawExtension{Raw: b.Bytes()}, Cells: []interface{}{replicationControllers[i].Name, replicationControllers[i].Spec.Replicas, replicationControllers[i].Status.Replicas}, }) } return cmdtesting.ObjBody(codec, table) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/humanreadable_flags.go000066400000000000000000000102551504711711200316640ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/printers" ) // HumanPrintFlags provides default flags necessary for printing. // Given the following flag values, a printer can be requested that knows // how to handle printing based on these values. type HumanPrintFlags struct { ShowKind *bool ShowLabels *bool SortBy *string ColumnLabels *[]string // get.go-specific values NoHeaders bool Kind schema.GroupKind WithNamespace bool } // SetKind sets the Kind option func (f *HumanPrintFlags) SetKind(kind schema.GroupKind) { f.Kind = kind } // EnsureWithKind sets the "Showkind" humanreadable option to true. func (f *HumanPrintFlags) EnsureWithKind() error { showKind := true f.ShowKind = &showKind return nil } // EnsureWithNamespace sets the "WithNamespace" humanreadable option to true. func (f *HumanPrintFlags) EnsureWithNamespace() error { f.WithNamespace = true return nil } // AllowedFormats returns more customized formating options func (f *HumanPrintFlags) AllowedFormats() []string { return []string{"wide"} } // ToPrinter receives an outputFormat and returns a printer capable of // handling human-readable output. func (f *HumanPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) { if len(outputFormat) > 0 && outputFormat != "wide" { return nil, genericclioptions.NoCompatiblePrinterError{Options: f, AllowedFormats: f.AllowedFormats()} } showKind := false if f.ShowKind != nil { showKind = *f.ShowKind } showLabels := false if f.ShowLabels != nil { showLabels = *f.ShowLabels } columnLabels := []string{} if f.ColumnLabels != nil { columnLabels = *f.ColumnLabels } p := printers.NewTablePrinter(printers.PrintOptions{ Kind: f.Kind, WithKind: showKind, NoHeaders: f.NoHeaders, Wide: outputFormat == "wide", WithNamespace: f.WithNamespace, ColumnLabels: columnLabels, ShowLabels: showLabels, }) // TODO(juanvallejo): handle sorting here return p, nil } // AddFlags receives a *cobra.Command reference and binds // flags related to human-readable printing to it func (f *HumanPrintFlags) AddFlags(c *cobra.Command) { if f.ShowLabels != nil { c.Flags().BoolVar(f.ShowLabels, "show-labels", *f.ShowLabels, "When printing, show all labels as the last column (default hide labels column)") } if f.SortBy != nil { c.Flags().StringVar(f.SortBy, "sort-by", *f.SortBy, "If non-empty, sort list types using this field specification. The field specification is expressed as a JSONPath expression (e.g. '{.metadata.name}'). The field in the API resource specified by this JSONPath expression must be an integer or a string.") } if f.ColumnLabels != nil { c.Flags().StringSliceVarP(f.ColumnLabels, "label-columns", "L", *f.ColumnLabels, "Accepts a comma separated list of labels that are going to be presented as columns. Names are case-sensitive. You can also use multiple flag options like -L label1 -L label2...") } if f.ShowKind != nil { c.Flags().BoolVar(f.ShowKind, "show-kind", *f.ShowKind, "If present, list the resource type for the requested object(s).") } } // NewHumanPrintFlags returns flags associated with // human-readable printing, with default values set. func NewHumanPrintFlags() *HumanPrintFlags { showLabels := false sortBy := "" showKind := false columnLabels := []string{} return &HumanPrintFlags{ NoHeaders: false, WithNamespace: false, ColumnLabels: &columnLabels, Kind: schema.GroupKind{}, ShowLabels: &showLabels, SortBy: &sortBy, ShowKind: &showKind, } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/humanreadable_flags_test.go000066400000000000000000000170451504711711200327270ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "bytes" "fmt" "regexp" "strings" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" ) func TestHumanReadablePrinterSupportsExpectedOptions(t *testing.T) { testTable := &metav1.Table{ ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, {Name: "Ready", Type: "string", Format: ""}, {Name: "Status", Type: "string", Format: ""}, {Name: "Restarts", Type: "integer", Format: ""}, {Name: "Age", Type: "string", Format: ""}, {Name: "IP", Type: "string", Format: "", Priority: 1}, {Name: "Node", Type: "string", Format: "", Priority: 1}, {Name: "Nominated Node", Type: "string", Format: "", Priority: 1}, {Name: "Readiness Gates", Type: "string", Format: "", Priority: 1}, }, Rows: []metav1.TableRow{{ Object: runtime.RawExtension{ Object: &corev1.Pod{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "foons", Labels: map[string]string{"l1": "value"}}, }, }, Cells: []interface{}{"foo", "0/0", "", int64(0), "", "", "", "", ""}, }}, } testPod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Pod", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "foons", Labels: map[string]string{ "l1": "value", }, }, } testCases := []struct { name string testObject runtime.Object showKind bool showLabels bool // TODO(juanvallejo): test sorting once it's moved to the HumanReadablePrinter sortBy string columnLabels []string noHeaders bool withNamespace bool outputFormat string expectedError string expectedOutput string expectNoMatch bool }{ { name: "empty output format matches a humanreadable printer", testObject: testPod.DeepCopy(), expectedOutput: "NAME\\ +AGE\nfoo\\ +\n", }, { name: "empty output format matches a humanreadable printer", testObject: testTable.DeepCopy(), expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\nfoo\\ +0/0\\ +0\\ +\n", }, { name: "\"wide\" output format prints", testObject: testPod.DeepCopy(), outputFormat: "wide", expectedOutput: "NAME\\ +AGE\nfoo\\ +\n", }, { name: "\"wide\" output format prints", testObject: testTable.DeepCopy(), outputFormat: "wide", expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\\ +IP\\ +NODE\\ +NOMINATED NODE\\ +READINESS GATES\nfoo\\ +0/0\\ +0\\ +\\ +\\ +\\ +\\ +\n", }, { name: "no-headers prints output with no headers", testObject: testPod.DeepCopy(), noHeaders: true, expectedOutput: "foo\\ +\n", }, { name: "no-headers prints output with no headers", testObject: testTable.DeepCopy(), noHeaders: true, expectedOutput: "foo\\ +0/0\\ +0\\ +\n", }, { name: "no-headers and a \"wide\" output format prints output with no headers and additional columns", testObject: testPod.DeepCopy(), outputFormat: "wide", noHeaders: true, expectedOutput: "foo\\ +\n", }, { name: "no-headers and a \"wide\" output format prints output with no headers and additional columns", testObject: testTable.DeepCopy(), outputFormat: "wide", noHeaders: true, expectedOutput: "foo\\ +0/0\\ +0\\ +\\ +\\ +\\ +\\ +\n", }, { name: "show-kind displays the resource's kind, even when printing a single type of resource", testObject: testPod.DeepCopy(), showKind: true, expectedOutput: "NAME\\ +AGE\npod/foo\\ +\n", }, { name: "show-kind displays the resource's kind, even when printing a single type of resource", testObject: testTable.DeepCopy(), showKind: true, expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\npod/foo\\ +0/0\\ +0\\ +\n", }, { name: "label-columns prints specified label values in new column", testObject: testPod.DeepCopy(), columnLabels: []string{"l1"}, expectedOutput: "NAME\\ +AGE\\ +L1\nfoo\\ +\\ +value\n", }, { name: "label-columns prints specified label values in new column", testObject: testTable.DeepCopy(), columnLabels: []string{"l1"}, expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\\ +L1\nfoo\\ +0/0\\ +0\\ +\\ +value\n", }, { name: "withNamespace displays an additional NAMESPACE column", testObject: testPod.DeepCopy(), withNamespace: true, expectedOutput: "NAMESPACE\\ +NAME\\ +AGE\nfoons\\ +foo\\ +\n", }, { name: "withNamespace displays an additional NAMESPACE column", testObject: testTable.DeepCopy(), withNamespace: true, expectedOutput: "NAMESPACE\\ +NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\nfoons\\ +foo\\ +0/0\\ +0\\ +\n", }, { name: "no printer is matched on an invalid outputFormat", testObject: testPod.DeepCopy(), outputFormat: "invalid", expectNoMatch: true, }, { name: "printer should not match on any other format supported by another printer", testObject: testPod.DeepCopy(), outputFormat: "go-template", expectNoMatch: true, }, } for _, tc := range testCases { t.Run(fmt.Sprintf("%s %T", tc.name, tc.testObject), func(t *testing.T) { printFlags := HumanPrintFlags{ ShowKind: &tc.showKind, ShowLabels: &tc.showLabels, SortBy: &tc.sortBy, ColumnLabels: &tc.columnLabels, NoHeaders: tc.noHeaders, WithNamespace: tc.withNamespace, } if tc.showKind { printFlags.Kind = schema.GroupKind{Kind: "pod"} } p, err := printFlags.ToPrinter(tc.outputFormat) if tc.expectNoMatch { if !genericclioptions.IsNoCompatiblePrinterError(err) { t.Fatalf("expected no printer matches for output format %q", tc.outputFormat) } return } if genericclioptions.IsNoCompatiblePrinterError(err) { t.Fatalf("expected to match template printer for output format %q", tc.outputFormat) } if len(tc.expectedError) > 0 { if err == nil || !strings.Contains(err.Error(), tc.expectedError) { t.Errorf("expecting error %q, got %v", tc.expectedError, err) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } out := bytes.NewBuffer([]byte{}) err = p.PrintObj(tc.testObject, out) if err != nil { t.Errorf("unexpected error: %v", err) } match, err := regexp.Match(tc.expectedOutput, out.Bytes()) if err != nil { t.Errorf("unexpected error: %v", err) } if !match { t.Errorf("unexpected output: expecting\n%s\ngot\n%s", tc.expectedOutput, out.String()) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/skip_printer.go000066400000000000000000000025401504711711200304270ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "io" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/printers" ) // skipPrinter allows conditionally suppressing object output via the output field. // table objects are suppressed by setting their Rows to nil (allowing column definitions to propagate to the delegate). // non-table objects are suppressed by not calling the delegate at all. type skipPrinter struct { delegate printers.ResourcePrinter output *bool } func (p *skipPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { if *p.output { return p.delegate.PrintObj(obj, writer) } table, isTable := obj.(*metav1.Table) if !isTable { return nil } table = table.DeepCopy() table.Rows = nil return p.delegate.PrintObj(table, writer) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/sorter.go000066400000000000000000000261211504711711200272350ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "fmt" "io" "reflect" "sort" "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" 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" "k8s.io/client-go/util/jsonpath" ) // SortingPrinter sorts list types before delegating to another printer. // Non-list types are simply passed through type SortingPrinter struct { SortField string Delegate printers.ResourcePrinter Decoder runtime.Decoder } func (s *SortingPrinter) PrintObj(obj runtime.Object, out io.Writer) error { if table, isTable := obj.(*metav1.Table); isTable && len(table.Rows) > 1 { parsedField, err := RelaxedJSONPathExpression(s.SortField) if err != nil { parsedField = s.SortField } if sorter, err := NewTableSorter(table, parsedField); err != nil { return err } else if err := sorter.Sort(); err != nil { return err } return s.Delegate.PrintObj(table, out) } if meta.IsListType(obj) { if err := s.sortObj(obj); err != nil { return err } return s.Delegate.PrintObj(obj, out) } return s.Delegate.PrintObj(obj, out) } func (s *SortingPrinter) sortObj(obj runtime.Object) error { objs, err := meta.ExtractList(obj) if err != nil { return err } if len(objs) == 0 { return nil } sorter, err := SortObjects(s.Decoder, objs, s.SortField) if err != nil { return err } switch list := obj.(type) { case *corev1.List: outputList := make([]runtime.RawExtension, len(objs)) for ix := range objs { outputList[ix] = list.Items[sorter.OriginalPosition(ix)] } list.Items = outputList return nil } return meta.SetList(obj, objs) } // SortObjects sorts the runtime.Object based on fieldInput and returns RuntimeSort that implements // the golang sort interface func SortObjects(decoder runtime.Decoder, objs []runtime.Object, fieldInput string) (*RuntimeSort, error) { for ix := range objs { item := objs[ix] switch u := item.(type) { case *runtime.Unknown: var err error // decode runtime.Unknown to runtime.Unstructured for sorting. // we don't actually want the internal versions of known types. if objs[ix], _, err = decoder.Decode(u.Raw, nil, &unstructured.Unstructured{}); err != nil { return nil, err } } } field, err := RelaxedJSONPathExpression(fieldInput) if err != nil { return nil, err } parser := jsonpath.New("sorting").AllowMissingKeys(true) if err := parser.Parse(field); err != nil { return nil, err } // We don't do any model validation here, so we traverse all objects to be sorted // and, if the field is valid to at least one of them, we consider it to be a // valid field; otherwise error out. // Note that this requires empty fields to be considered later, when sorting. var fieldFoundOnce bool for _, obj := range objs { values, err := findJSONPathResults(parser, obj) if err != nil { return nil, err } if len(values) > 0 && len(values[0]) > 0 { fieldFoundOnce = true break } } if !fieldFoundOnce { return nil, fmt.Errorf("couldn't find any field with path %q in the list of objects", field) } sorter := NewRuntimeSort(field, objs) sort.Sort(sorter) return sorter, nil } // RuntimeSort is an implementation of the golang sort interface that knows how to sort // lists of runtime.Object type RuntimeSort struct { field string objs []runtime.Object origPosition []int } // NewRuntimeSort creates a new RuntimeSort struct that implements golang sort interface func NewRuntimeSort(field string, objs []runtime.Object) *RuntimeSort { sorter := &RuntimeSort{field: field, objs: objs, origPosition: make([]int, len(objs))} for ix := range objs { sorter.origPosition[ix] = ix } return sorter } func (r *RuntimeSort) Len() int { return len(r.objs) } func (r *RuntimeSort) Swap(i, j int) { r.objs[i], r.objs[j] = r.objs[j], r.objs[i] r.origPosition[i], r.origPosition[j] = r.origPosition[j], r.origPosition[i] } func isLess(i, j reflect.Value) (bool, error) { switch i.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return i.Int() < j.Int(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return i.Uint() < j.Uint(), nil case reflect.Float32, reflect.Float64: return i.Float() < j.Float(), nil case reflect.String: return i.String() < j.String(), nil case reflect.Pointer: return isLess(i.Elem(), j.Elem()) case reflect.Struct: // sort metav1.Time in := i.Interface() if t, ok := in.(metav1.Time); ok { time := j.Interface().(metav1.Time) return t.Before(&time), nil } // sort resource.Quantity if iQuantity, ok := in.(resource.Quantity); ok { jQuantity := j.Interface().(resource.Quantity) return iQuantity.Cmp(jQuantity) < 0, nil } // fallback to the fields comparison for idx := 0; idx < i.NumField(); idx++ { less, err := isLess(i.Field(idx), j.Field(idx)) if err != nil || !less { return less, err } } return true, nil case reflect.Array, reflect.Slice: // note: the length of i and j may be different for idx := 0; idx < min(i.Len(), j.Len()); idx++ { less, err := isLess(i.Index(idx), j.Index(idx)) if err != nil || !less { return less, err } } return true, nil case reflect.Interface: if i.IsNil() && j.IsNil() { return false, nil } else if i.IsNil() { return true, nil } else if j.IsNil() { return false, nil } switch itype := i.Interface().(type) { case uint8: if jtype, ok := j.Interface().(uint8); ok { return itype < jtype, nil } case uint16: if jtype, ok := j.Interface().(uint16); ok { return itype < jtype, nil } case uint32: if jtype, ok := j.Interface().(uint32); ok { return itype < jtype, nil } case uint64: if jtype, ok := j.Interface().(uint64); ok { return itype < jtype, nil } case int8: if jtype, ok := j.Interface().(int8); ok { return itype < jtype, nil } case int16: if jtype, ok := j.Interface().(int16); ok { return itype < jtype, nil } case int32: if jtype, ok := j.Interface().(int32); ok { return itype < jtype, nil } case int64: if jtype, ok := j.Interface().(int64); ok { return itype < jtype, nil } case uint: if jtype, ok := j.Interface().(uint); ok { return itype < jtype, nil } case int: if jtype, ok := j.Interface().(int); ok { return itype < jtype, nil } case float32: if jtype, ok := j.Interface().(float32); ok { return itype < jtype, nil } case float64: if jtype, ok := j.Interface().(float64); ok { return itype < jtype, nil } case string: if jtype, ok := j.Interface().(string); ok { // check if it's a Quantity itypeQuantity, err := resource.ParseQuantity(itype) if err != nil { return itype < jtype, nil } jtypeQuantity, err := resource.ParseQuantity(jtype) if err != nil { return itype < jtype, nil } // Both strings are quantity return itypeQuantity.Cmp(jtypeQuantity) < 0, nil } default: return false, fmt.Errorf("unsortable type: %T", itype) } return false, fmt.Errorf("unsortable interface: %v", i.Kind()) default: return false, fmt.Errorf("unsortable type: %v", i.Kind()) } } func (r *RuntimeSort) Less(i, j int) bool { iObj := r.objs[i] jObj := r.objs[j] var iValues [][]reflect.Value var jValues [][]reflect.Value var err error parser := jsonpath.New("sorting").AllowMissingKeys(true) err = parser.Parse(r.field) if err != nil { panic(err) } iValues, err = findJSONPathResults(parser, iObj) if err != nil { klog.Fatalf("Failed to get i values for %#v using %s (%#v)", iObj, r.field, err) } jValues, err = findJSONPathResults(parser, jObj) if err != nil { klog.Fatalf("Failed to get j values for %#v using %s (%v)", jObj, r.field, err) } if len(iValues) == 0 || len(iValues[0]) == 0 { return true } if len(jValues) == 0 || len(jValues[0]) == 0 { return false } iField := iValues[0][0] jField := jValues[0][0] less, err := isLess(iField, jField) if err != nil { klog.Exitf("Field %s in %T is an unsortable type: %s, err: %v", r.field, iObj, iField.Kind().String(), err) } return less } // OriginalPosition returns the starting (original) position of a particular index. // e.g. If OriginalPosition(0) returns 5 than the // item currently at position 0 was at position 5 in the original unsorted array. func (r *RuntimeSort) OriginalPosition(ix int) int { if ix < 0 || ix > len(r.origPosition) { return -1 } return r.origPosition[ix] } type TableSorter struct { field string obj *metav1.Table parsedRows [][][]reflect.Value } func (t *TableSorter) Len() int { return len(t.obj.Rows) } func (t *TableSorter) Swap(i, j int) { t.obj.Rows[i], t.obj.Rows[j] = t.obj.Rows[j], t.obj.Rows[i] t.parsedRows[i], t.parsedRows[j] = t.parsedRows[j], t.parsedRows[i] } func (t *TableSorter) Less(i, j int) bool { iValues := t.parsedRows[i] jValues := t.parsedRows[j] if len(iValues) == 0 || len(iValues[0]) == 0 { return true } if len(jValues) == 0 || len(jValues[0]) == 0 { return false } iField := iValues[0][0] jField := jValues[0][0] less, err := isLess(iField, jField) if err != nil { klog.Exitf("Field %s in %T is an unsortable type: %s, err: %v", t.field, t.parsedRows, iField.Kind().String(), err) } return less } func (t *TableSorter) Sort() error { sort.Sort(t) return nil } func NewTableSorter(table *metav1.Table, field string) (*TableSorter, error) { var parsedRows [][][]reflect.Value parser := jsonpath.New("sorting").AllowMissingKeys(true) err := parser.Parse(field) if err != nil { return nil, fmt.Errorf("sorting error: %v", err) } fieldFoundOnce := false for i := range table.Rows { parsedRow, err := findJSONPathResults(parser, table.Rows[i].Object.Object) if err != nil { return nil, fmt.Errorf("Failed to get values for %#v using %s (%#v)", parsedRow, field, err) } parsedRows = append(parsedRows, parsedRow) if len(parsedRow) > 0 && len(parsedRow[0]) > 0 { fieldFoundOnce = true } } if len(table.Rows) > 0 && !fieldFoundOnce { return nil, fmt.Errorf("couldn't find any field with path %q in the list of objects", field) } return &TableSorter{ obj: table, field: field, parsedRows: parsedRows, }, nil } func findJSONPathResults(parser *jsonpath.JSONPath, from runtime.Object) ([][]reflect.Value, error) { if unstructuredObj, ok := from.(*unstructured.Unstructured); ok { return parser.FindResults(unstructuredObj.Object) } return parser.FindResults(reflect.ValueOf(from).Elem().Interface()) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/sorter_test.go000066400000000000000000000475601504711711200303060ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "encoding/json" "reflect" "strings" "testing" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" 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" "k8s.io/kubectl/pkg/scheme" "k8s.io/utils/ptr" ) func toUnstructuredOrDie(data []byte) *unstructured.Unstructured { unstrBody := map[string]interface{}{} err := json.Unmarshal(data, &unstrBody) if err != nil { panic(err) } return &unstructured.Unstructured{Object: unstrBody} } func encodeOrDie(obj runtime.Object) []byte { data, err := runtime.Encode(scheme.Codecs.LegacyCodec(corev1.SchemeGroupVersion), obj) if err != nil { panic(err.Error()) } return data } func createPodSpecResource(t *testing.T, memReq, memLimit, cpuReq, cpuLimit string) corev1.PodSpec { t.Helper() podSpec := corev1.PodSpec{ Containers: []corev1.Container{ { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{}, Limits: corev1.ResourceList{}, }, }, }, } req := podSpec.Containers[0].Resources.Requests if memReq != "" { memReq, err := resource.ParseQuantity(memReq) if err != nil { t.Errorf("memory request string is not a valid quantity") } req["memory"] = memReq } if cpuReq != "" { cpuReq, err := resource.ParseQuantity(cpuReq) if err != nil { t.Errorf("cpu request string is not a valid quantity") } req["cpu"] = cpuReq } limit := podSpec.Containers[0].Resources.Limits if memLimit != "" { memLimit, err := resource.ParseQuantity(memLimit) if err != nil { t.Errorf("memory limit string is not a valid quantity") } limit["memory"] = memLimit } if cpuLimit != "" { cpuLimit, err := resource.ParseQuantity(cpuLimit) if err != nil { t.Errorf("cpu limit string is not a valid quantity") } limit["cpu"] = cpuLimit } return podSpec } func createUnstructuredPodResource(t *testing.T, memReq, memLimit, cpuReq, cpuLimit string) unstructured.Unstructured { t.Helper() pod := &corev1.Pod{ Spec: createPodSpecResource(t, memReq, memLimit, cpuReq, cpuLimit), } return *toUnstructuredOrDie(encodeOrDie(pod)) } func TestSortingPrinter(t *testing.T) { a := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "a", }, } b := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "b", }, } c := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "c", }, } tests := []struct { obj runtime.Object sort runtime.Object field string name string expectedErr string }{ { name: "empty", obj: &corev1.PodList{ Items: []corev1.Pod{}, }, sort: &corev1.PodList{ Items: []corev1.Pod{}, }, field: "{.metadata.name}", }, { name: "in-order-already", obj: &corev1.PodList{ Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "a", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "b", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "c", }, }, }, }, sort: &corev1.PodList{ Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "a", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "b", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "c", }, }, }, }, field: "{.metadata.name}", }, { name: "reverse-order", obj: &corev1.PodList{ Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "b", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "c", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "a", }, }, }, }, sort: &corev1.PodList{ Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "a", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "b", }, }, { ObjectMeta: metav1.ObjectMeta{ Name: "c", }, }, }, }, field: "{.metadata.name}", }, { name: "random-order-timestamp", obj: &corev1.PodList{ Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Unix(300, 0), }, }, { ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Unix(100, 0), }, }, { ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Unix(200, 0), }, }, }, }, sort: &corev1.PodList{ Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Unix(100, 0), }, }, { ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Unix(200, 0), }, }, { ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Unix(300, 0), }, }, }, }, field: "{.metadata.creationTimestamp}", }, { name: "random-order-numbers", obj: &corev1.ReplicationControllerList{ Items: []corev1.ReplicationController{ { Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](5), }, }, { Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](1), }, }, { Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](9), }, }, }, }, sort: &corev1.ReplicationControllerList{ Items: []corev1.ReplicationController{ { Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](1), }, }, { Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](5), }, }, { Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](9), }, }, }, }, field: "{.spec.replicas}", }, { name: "v1.List in order", obj: &corev1.List{ Items: []runtime.RawExtension{ {Object: a, Raw: encodeOrDie(a)}, {Object: b, Raw: encodeOrDie(b)}, {Object: c, Raw: encodeOrDie(c)}, }, }, sort: &corev1.List{ Items: []runtime.RawExtension{ {Object: a, Raw: encodeOrDie(a)}, {Object: b, Raw: encodeOrDie(b)}, {Object: c, Raw: encodeOrDie(c)}, }, }, field: "{.metadata.name}", }, { name: "v1.List in reverse", obj: &corev1.List{ Items: []runtime.RawExtension{ {Object: c, Raw: encodeOrDie(c)}, {Object: b, Raw: encodeOrDie(b)}, {Object: a, Raw: encodeOrDie(a)}, }, }, sort: &corev1.List{ Items: []runtime.RawExtension{ {Object: a, Raw: encodeOrDie(a)}, {Object: b, Raw: encodeOrDie(b)}, {Object: c, Raw: encodeOrDie(c)}, }, }, field: "{.metadata.name}", }, { name: "some-missing-fields", obj: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", }, Items: []unstructured.Unstructured{ { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{ "availableReplicas": 2, }, }, }, { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{}, }, }, { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{ "availableReplicas": 1, }, }, }, }, }, sort: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", }, Items: []unstructured.Unstructured{ { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{}, }, }, { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{ "availableReplicas": 1, }, }, }, { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{ "availableReplicas": 2, }, }, }, }, }, field: "{.status.availableReplicas}", }, { name: "all-missing-fields", obj: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", }, Items: []unstructured.Unstructured{ { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{ "replicas": 0, }, }, }, { Object: map[string]interface{}{ "kind": "ReplicationController", "apiVersion": "v1", "status": map[string]interface{}{ "replicas": 0, }, }, }, }, }, field: "{.status.availableReplicas}", expectedErr: "couldn't find any field with path \"{.status.availableReplicas}\" in the list of objects", }, { name: "model-invalid-fields", obj: &corev1.ReplicationControllerList{ Items: []corev1.ReplicationController{ { Status: corev1.ReplicationControllerStatus{}, }, { Status: corev1.ReplicationControllerStatus{}, }, { Status: corev1.ReplicationControllerStatus{}, }, }, }, field: "{.invalid}", expectedErr: "couldn't find any field with path \"{.invalid}\" in the list of objects", }, { name: "empty fields", obj: &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Unix(300, 0)}, LastTimestamp: metav1.Unix(300, 0), }, { ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Unix(200, 0)}, }, }, }, sort: &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Unix(200, 0)}, }, { ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Unix(300, 0)}, LastTimestamp: metav1.Unix(300, 0), }, }, }, field: "{.lastTimestamp}", }, { name: "pod-resources-cpu-random-order-with-missing-fields", obj: &corev1.PodList{ Items: []corev1.Pod{ { Spec: createPodSpecResource(t, "", "", "0.5", ""), }, { Spec: createPodSpecResource(t, "", "", "10", ""), }, { Spec: createPodSpecResource(t, "", "", "100m", ""), }, { Spec: createPodSpecResource(t, "", "", "", ""), }, }, }, sort: &corev1.PodList{ Items: []corev1.Pod{ { Spec: createPodSpecResource(t, "", "", "", ""), }, { Spec: createPodSpecResource(t, "", "", "100m", ""), }, { Spec: createPodSpecResource(t, "", "", "0.5", ""), }, { Spec: createPodSpecResource(t, "", "", "10", ""), }, }, }, field: "{.spec.containers[].resources.requests.cpu}", }, { name: "pod-resources-memory-random-order-with-missing-fields", obj: &corev1.PodList{ Items: []corev1.Pod{ { Spec: createPodSpecResource(t, "128Mi", "", "", ""), }, { Spec: createPodSpecResource(t, "10Ei", "", "", ""), }, { Spec: createPodSpecResource(t, "8Ti", "", "", ""), }, { Spec: createPodSpecResource(t, "64Gi", "", "", ""), }, { Spec: createPodSpecResource(t, "55Pi", "", "", ""), }, { Spec: createPodSpecResource(t, "2Ki", "", "", ""), }, { Spec: createPodSpecResource(t, "", "", "", ""), }, }, }, sort: &corev1.PodList{ Items: []corev1.Pod{ { Spec: createPodSpecResource(t, "", "", "", ""), }, { Spec: createPodSpecResource(t, "2Ki", "", "", ""), }, { Spec: createPodSpecResource(t, "128Mi", "", "", ""), }, { Spec: createPodSpecResource(t, "64Gi", "", "", ""), }, { Spec: createPodSpecResource(t, "8Ti", "", "", ""), }, { Spec: createPodSpecResource(t, "55Pi", "", "", ""), }, { Spec: createPodSpecResource(t, "10Ei", "", "", ""), }, }, }, field: "{.spec.containers[].resources.requests.memory}", }, { name: "pod-unstructured-resources-cpu-random-order-with-missing-fields", obj: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", }, Items: []unstructured.Unstructured{ createUnstructuredPodResource(t, "", "", "0.5", ""), createUnstructuredPodResource(t, "", "", "10", ""), createUnstructuredPodResource(t, "", "", "100m", ""), createUnstructuredPodResource(t, "", "", "", ""), }, }, sort: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", }, Items: []unstructured.Unstructured{ createUnstructuredPodResource(t, "", "", "", ""), createUnstructuredPodResource(t, "", "", "100m", ""), createUnstructuredPodResource(t, "", "", "0.5", ""), createUnstructuredPodResource(t, "", "", "10", ""), }, }, field: "{.spec.containers[].resources.requests.cpu}", }, { name: "pod-unstructured-resources-memory-random-order-with-missing-fields", obj: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", }, Items: []unstructured.Unstructured{ createUnstructuredPodResource(t, "128Mi", "", "", ""), createUnstructuredPodResource(t, "10Ei", "", "", ""), createUnstructuredPodResource(t, "8Ti", "", "", ""), createUnstructuredPodResource(t, "64Gi", "", "", ""), createUnstructuredPodResource(t, "55Pi", "", "", ""), createUnstructuredPodResource(t, "2Ki", "", "", ""), createUnstructuredPodResource(t, "", "", "", ""), }, }, sort: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", }, Items: []unstructured.Unstructured{ createUnstructuredPodResource(t, "", "", "", ""), createUnstructuredPodResource(t, "2Ki", "", "", ""), createUnstructuredPodResource(t, "128Mi", "", "", ""), createUnstructuredPodResource(t, "64Gi", "", "", ""), createUnstructuredPodResource(t, "8Ti", "", "", ""), createUnstructuredPodResource(t, "55Pi", "", "", ""), createUnstructuredPodResource(t, "10Ei", "", "", ""), }, }, field: "{.spec.containers[].resources.requests.memory}", }, } for _, tt := range tests { t.Run(tt.name+" table", func(t *testing.T) { table := &metav1beta1.Table{} meta.EachListItem(tt.obj, func(item runtime.Object) error { table.Rows = append(table.Rows, metav1beta1.TableRow{ Object: runtime.RawExtension{Object: toUnstructuredOrDie(encodeOrDie(item))}, }) return nil }) expectedTable := &metav1beta1.Table{} meta.EachListItem(tt.sort, func(item runtime.Object) error { expectedTable.Rows = append(expectedTable.Rows, metav1beta1.TableRow{ Object: runtime.RawExtension{Object: toUnstructuredOrDie(encodeOrDie(item))}, }) return nil }) sorter, err := NewTableSorter(table, tt.field) if err == nil { err = sorter.Sort() } if err != nil { if len(tt.expectedErr) > 0 { if strings.Contains(err.Error(), tt.expectedErr) { return } t.Fatalf("%s: expected error containing: %q, got: \"%v\"", tt.name, tt.expectedErr, err) } t.Fatalf("%s: unexpected error: %v", tt.name, err) } if len(tt.expectedErr) > 0 { t.Fatalf("%s: expected error containing: %q, got none", tt.name, tt.expectedErr) } if !reflect.DeepEqual(table, expectedTable) { t.Errorf("[%s]\nexpected/saw:\n%s", tt.name, cmp.Diff(expectedTable, table)) } }) t.Run(tt.name, func(t *testing.T) { sort := &SortingPrinter{SortField: tt.field, Decoder: scheme.Codecs.UniversalDecoder()} err := sort.sortObj(tt.obj) if err != nil { if len(tt.expectedErr) > 0 { if strings.Contains(err.Error(), tt.expectedErr) { return } t.Fatalf("%s: expected error containing: %q, got: \"%v\"", tt.name, tt.expectedErr, err) } t.Fatalf("%s: unexpected error: %v", tt.name, err) } if len(tt.expectedErr) > 0 { t.Fatalf("%s: expected error containing: %q, got none", tt.name, tt.expectedErr) } if !reflect.DeepEqual(tt.obj, tt.sort) { t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v", tt.name, tt.sort, tt.obj) } }) } } func TestRuntimeSortLess(t *testing.T) { var testobj runtime.Object testobj = &corev1.PodList{ Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{ Name: "b", }, Spec: createPodSpecResource(t, "0.5", "", "1Gi", ""), }, { ObjectMeta: metav1.ObjectMeta{ Name: "c", }, Spec: createPodSpecResource(t, "2", "", "1Ti", ""), }, { ObjectMeta: metav1.ObjectMeta{ Name: "a", }, Spec: createPodSpecResource(t, "10m", "", "1Ki", ""), }, }, } testobjs, err := meta.ExtractList(testobj) if err != nil { t.Fatalf("ExtractList testobj got unexpected error: %v", err) } testfieldName := "{.metadata.name}" testruntimeSortName := NewRuntimeSort(testfieldName, testobjs) testfieldCPU := "{.spec.containers[].resources.requests.cpu}" testruntimeSortCPU := NewRuntimeSort(testfieldCPU, testobjs) testfieldMemory := "{.spec.containers[].resources.requests.memory}" testruntimeSortMemory := NewRuntimeSort(testfieldMemory, testobjs) tests := []struct { name string runtimeSort *RuntimeSort i int j int expectResult bool expectErr bool }{ { name: "test name b c less true", runtimeSort: testruntimeSortName, i: 0, j: 1, expectResult: true, }, { name: "test name c a less false", runtimeSort: testruntimeSortName, i: 1, j: 2, expectResult: false, }, { name: "test name b a less false", runtimeSort: testruntimeSortName, i: 0, j: 2, expectResult: false, }, { name: "test cpu 0.5 2 less true", runtimeSort: testruntimeSortCPU, i: 0, j: 1, expectResult: true, }, { name: "test cpu 2 10mi less false", runtimeSort: testruntimeSortCPU, i: 1, j: 2, expectResult: false, }, { name: "test cpu 0.5 10mi less false", runtimeSort: testruntimeSortCPU, i: 0, j: 2, expectResult: false, }, { name: "test memory 1Gi 1Ti less true", runtimeSort: testruntimeSortMemory, i: 0, j: 1, expectResult: true, }, { name: "test memory 1Ti 1Ki less false", runtimeSort: testruntimeSortMemory, i: 1, j: 2, expectResult: false, }, { name: "test memory 1Gi 1Ki less false", runtimeSort: testruntimeSortMemory, i: 0, j: 2, expectResult: false, }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { result := test.runtimeSort.Less(test.i, test.j) if result != test.expectResult { t.Errorf("case[%d]:%s Expected result: %v, Got result: %v", i, test.name, test.expectResult, result) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/get/table_printer.go000066400000000000000000000055011504711711200305500ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package get import ( "fmt" "io" 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" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/printers" "k8s.io/klog/v2" ) // TablePrinter decodes table objects into typed objects before delegating to another printer. // Non-table types are simply passed through type TablePrinter struct { Delegate printers.ResourcePrinter } func (t *TablePrinter) PrintObj(obj runtime.Object, writer io.Writer) error { table, err := decodeIntoTable(obj) if err == nil { return t.Delegate.PrintObj(table, writer) } // if we are unable to decode server response into a v1beta1.Table, // fallback to client-side printing with whatever info the server returned. klog.V(2).Infof("Unable to decode server response into a Table. Falling back to hardcoded types: %v", err) return t.Delegate.PrintObj(obj, writer) } var recognizedTableVersions = map[schema.GroupVersionKind]bool{ metav1beta1.SchemeGroupVersion.WithKind("Table"): true, metav1.SchemeGroupVersion.WithKind("Table"): true, } // assert the types are identical, since we're decoding both types into a metav1.Table var _ metav1.Table = metav1beta1.Table{} var _ metav1beta1.Table = metav1.Table{} func decodeIntoTable(obj runtime.Object) (runtime.Object, error) { event, isEvent := obj.(*metav1.WatchEvent) if isEvent { obj = event.Object.Object } if !recognizedTableVersions[obj.GetObjectKind().GroupVersionKind()] { return nil, fmt.Errorf("attempt to decode non-Table object") } unstr, ok := obj.(*unstructured.Unstructured) if !ok { return nil, fmt.Errorf("attempt to decode non-Unstructured object") } table := &metav1.Table{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, table); err != nil { return nil, err } for i := range table.Rows { row := &table.Rows[i] if row.Object.Raw == nil || row.Object.Object != nil { continue } converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, row.Object.Raw) if err != nil { return nil, err } row.Object.Object = converted } if isEvent { event.Object.Object = table return event, nil } return table, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/help/000077500000000000000000000000001504711711200255375ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/help/help.go000066400000000000000000000045661504711711200270310ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package help import ( "strings" "github.com/spf13/cobra" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var helpLong = templates.LongDesc(i18n.T(` Help provides help for any command in the application. Simply type kubectl help [path to command] for full details.`)) // NewCmdHelp returns the help Cobra command func NewCmdHelp() *cobra.Command { cmd := &cobra.Command{ Use: "help [command] | STRING_TO_SEARCH", DisableFlagsInUseLine: true, Short: i18n.T("Help about any command"), Long: helpLong, Run: RunHelp, } return cmd } // RunHelp checks given arguments and executes command func RunHelp(cmd *cobra.Command, args []string) { foundCmd, _, err := cmd.Root().Find(args) // NOTE(andreykurilin): actually, I did not find any cases when foundCmd can be nil, // but let's make this check since it is included in original code of initHelpCmd // from github.com/spf13/cobra if foundCmd == nil { cmd.Printf("Unknown help topic %#q.\n", args) cmd.Root().Usage() } else if err != nil { // print error message at first, since it can contain suggestions cmd.Println(err) argsString := strings.Join(args, " ") var matchedMsgIsPrinted = false for _, foundCmd := range foundCmd.Commands() { if strings.Contains(foundCmd.Short, argsString) { if !matchedMsgIsPrinted { cmd.Printf("Matchers of string '%s' in short descriptions of commands: \n", argsString) matchedMsgIsPrinted = true } cmd.Printf(" %-14s %s\n", foundCmd.Name(), foundCmd.Short) } } if !matchedMsgIsPrinted { // if nothing is found, just print usage cmd.Root().Usage() } } else { if len(args) == 0 { // help message for help command :) foundCmd = cmd } helpFunc := foundCmd.HelpFunc() helpFunc(foundCmd, args) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/kustomize/000077500000000000000000000000001504711711200266415ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/kustomize/kustomize.go000066400000000000000000000023441504711711200312250ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kustomize import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "sigs.k8s.io/kustomize/kustomize/v5/commands/build" "sigs.k8s.io/kustomize/kyaml/filesys" ) // NewCmdKustomize returns an adapted kustomize build command. func NewCmdKustomize(streams genericiooptions.IOStreams) *cobra.Command { h := build.MakeHelp("kubectl", "kustomize") return build.NewCmdBuild( filesys.MakeFsOnDisk(), &build.Help{ Use: h.Use, Short: i18n.T(h.Short), Long: templates.LongDesc(i18n.T(h.Long)), Example: templates.Examples(i18n.T(h.Example)), }, streams.Out) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/label/000077500000000000000000000000001504711711200256665ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/label/label.go000066400000000000000000000353471504711711200273100ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package label import ( "fmt" "reflect" "strings" "github.com/spf13/cobra" jsonpatch "gopkg.in/evanphx/json-patch.v4" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) const ( MsgNotLabeled = "not labeled" MsgLabeled = "labeled" MsgUnLabeled = "unlabeled" ) // LabelOptions have the data required to perform the label operation type LabelOptions struct { // Filename options resource.FilenameOptions RecordFlags *genericclioptions.RecordFlags PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) // Common user flags overwrite bool list bool local bool dryRunStrategy cmdutil.DryRunStrategy all bool allNamespaces bool resourceVersion string selector string fieldSelector string outputFormat string fieldManager string // results of arg parsing resources []string newLabels map[string]string removeLabels []string Recorder genericclioptions.Recorder namespace string enforceNamespace bool builder *resource.Builder unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) // Common shared fields genericiooptions.IOStreams } var ( labelLong = templates.LongDesc(i18n.T(` Update the labels on a resource. * A label key and value must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters each. * Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app. * If --overwrite is true, then existing labels can be overwritten, otherwise attempting to overwrite a label will result in an error. * If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used.`)) labelExample = templates.Examples(i18n.T(` # Update pod 'foo' with the label 'unhealthy' and the value 'true' kubectl label pods foo unhealthy=true # Update pod 'foo' with the label 'status' and the value 'unhealthy', overwriting any existing value kubectl label --overwrite pods foo status=unhealthy # Update all pods in the namespace kubectl label pods --all status=unhealthy # Update a pod identified by the type and name in "pod.json" kubectl label -f pod.json status=unhealthy # Update pod 'foo' only if the resource is unchanged from version 1 kubectl label pods foo status=unhealthy --resource-version=1 # Update pod 'foo' by removing a label named 'bar' if it exists # Does not require the --overwrite flag kubectl label pods foo bar-`)) ) func NewLabelOptions(ioStreams genericiooptions.IOStreams) *LabelOptions { return &LabelOptions{ RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, PrintFlags: genericclioptions.NewPrintFlags("labeled").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } func NewCmdLabel(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewLabelOptions(ioStreams) cmd := &cobra.Command{ Use: "label [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]", DisableFlagsInUseLine: true, Short: i18n.T("Update the labels on a resource"), Long: fmt.Sprintf(labelLong, validation.LabelValueMaxLength), Example: labelExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunLabel()) }, } o.RecordFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) cmd.Flags().BoolVar(&o.overwrite, "overwrite", o.overwrite, "If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels.") cmd.Flags().BoolVar(&o.list, "list", o.list, "If true, display the labels for a given resource.") cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, label will NOT contact api-server but run locally.") cmd.Flags().StringVar(&o.fieldSelector, "field-selector", o.fieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all resources, in the namespace of the specified resource types") cmd.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", o.allNamespaces, "If true, check the specified action in all namespaces.") cmd.Flags().StringVar(&o.resourceVersion, "resource-version", o.resourceVersion, i18n.T("If non-empty, the labels update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.")) usage := "identifying the resource to update the labels" cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddDryRunFlag(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-label") cmdutil.AddLabelSelectorFlagVar(cmd, &o.selector) return cmd } // Complete adapts from the command line args and factory to the data required. func (o *LabelOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.outputFormat = cmdutil.GetFlagString(cmd, "output") o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation // PrintFlagsWithDryRunStrategy must be done after NamePrintFlags.Operation is set cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) return o.PrintFlags.ToPrinter() } resources, labelArgs, err := cmdutil.GetResourcesAndPairs(args, "label") if err != nil { return err } o.resources = resources o.newLabels, o.removeLabels, err = parseLabels(labelArgs) if err != nil { return err } if o.list && len(o.outputFormat) > 0 { return fmt.Errorf("--list and --output may not be specified together") } o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil && !(o.local && clientcmd.IsEmptyConfig(err)) { return err } o.builder = f.NewBuilder() o.unstructuredClientForMapping = f.UnstructuredClientForMapping return nil } // Validate checks to the LabelOptions to see if there is sufficient information run the command. func (o *LabelOptions) Validate() error { if o.all && len(o.selector) > 0 { return fmt.Errorf("cannot set --all and --selector at the same time") } if o.all && len(o.fieldSelector) > 0 { return fmt.Errorf("cannot set --all and --field-selector at the same time") } if o.local { if o.dryRunStrategy == cmdutil.DryRunServer { return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?") } if len(o.resources) > 0 { return fmt.Errorf("can only use local files by -f pod.yaml or --filename=pod.json when --local=true is set") } if cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) { return fmt.Errorf("one or more files must be specified as -f pod.yaml or --filename=pod.json") } } else { if len(o.resources) < 1 && cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) { return fmt.Errorf("one or more resources must be specified as or /") } } if len(o.newLabels) < 1 && len(o.removeLabels) < 1 && !o.list { return fmt.Errorf("at least one label update is required") } return nil } // RunLabel does the work func (o *LabelOptions) RunLabel() error { b := o.builder. Unstructured(). LocalParam(o.local). ContinueOnError(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, &o.FilenameOptions). Flatten() if !o.local { b = b.LabelSelectorParam(o.selector). FieldSelectorParam(o.fieldSelector). AllNamespaces(o.allNamespaces). ResourceTypeOrNameArgs(o.all, o.resources...). Latest() } one := false r := b.Do().IntoSingleItemImplied(&one) if err := r.Err(); err != nil { return err } // only apply resource version locking on a single resource if !one && len(o.resourceVersion) > 0 { return fmt.Errorf("--resource-version may only be used with a single resource") } // TODO: support bulk generic output a la Get return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } var outputObj runtime.Object var dataChangeMsg string obj := info.Object if len(o.resourceVersion) != 0 { // ensure resourceVersion is always sent in the patch by clearing it from the starting JSON accessor, err := meta.Accessor(obj) if err != nil { return err } accessor.SetResourceVersion("") } oldData, err := json.Marshal(obj) if err != nil { return err } if o.dryRunStrategy == cmdutil.DryRunClient || o.local || o.list { err = labelFunc(obj, o.overwrite, o.resourceVersion, o.newLabels, o.removeLabels) if err != nil { return err } newObj, err := json.Marshal(obj) if err != nil { return err } dataChangeMsg = updateDataChangeMsg(oldData, newObj, o.overwrite) outputObj = info.Object } else { name, namespace := info.Name, info.Namespace if err != nil { return err } accessor, err := meta.Accessor(obj) if err != nil { return err } for _, label := range o.removeLabels { if _, ok := accessor.GetLabels()[label]; !ok { fmt.Fprintf(o.Out, "label %q not found.\n", label) } } if err := labelFunc(obj, o.overwrite, o.resourceVersion, o.newLabels, o.removeLabels); err != nil { return err } if err := o.Recorder.Record(obj); err != nil { klog.V(4).Infof("error recording current command: %v", err) } newObj, err := json.Marshal(obj) if err != nil { return err } dataChangeMsg = updateDataChangeMsg(oldData, newObj, o.overwrite) patchBytes, err := jsonpatch.CreateMergePatch(oldData, newObj) createdPatch := err == nil if err != nil { klog.V(2).Infof("couldn't compute patch: %v", err) } mapping := info.ResourceMapping() client, err := o.unstructuredClientForMapping(mapping) if err != nil { return err } helper := resource.NewHelper(client, mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager) if createdPatch { outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil) } else { outputObj, err = helper.Replace(namespace, name, false, obj) } if err != nil { return err } } if o.list { accessor, err := meta.Accessor(outputObj) if err != nil { return err } indent := "" if !one { indent = " " gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(info.Object) if err != nil { return err } fmt.Fprintf(o.Out, "Listing labels for %s.%s/%s:\n", gvks[0].Kind, gvks[0].Group, info.Name) } for k, v := range accessor.GetLabels() { fmt.Fprintf(o.Out, "%s%s=%s\n", indent, k, v) } return nil } printer, err := o.ToPrinter(dataChangeMsg) if err != nil { return err } return printer.PrintObj(info.Object, o.Out) }) } func updateDataChangeMsg(oldObj []byte, newObj []byte, overwrite bool) string { msg := MsgNotLabeled if !reflect.DeepEqual(oldObj, newObj) { msg = MsgLabeled if !overwrite && len(newObj) < len(oldObj) { msg = MsgUnLabeled } } return msg } func validateNoOverwrites(accessor metav1.Object, labels map[string]string) error { allErrs := []error{} for key, value := range labels { if currValue, found := accessor.GetLabels()[key]; found && currValue != value { allErrs = append(allErrs, fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", key, currValue)) } } return utilerrors.NewAggregate(allErrs) } func parseLabels(spec []string) (map[string]string, []string, error) { labels := map[string]string{} var remove []string for _, labelSpec := range spec { if strings.Contains(labelSpec, "=") { parts := strings.Split(labelSpec, "=") if len(parts) != 2 { return nil, nil, fmt.Errorf("invalid label spec: %v", labelSpec) } if errs := validation.IsValidLabelValue(parts[1]); len(errs) != 0 { return nil, nil, fmt.Errorf("invalid label value: %q: %s", labelSpec, strings.Join(errs, ";")) } labels[parts[0]] = parts[1] } else if strings.HasSuffix(labelSpec, "-") { remove = append(remove, labelSpec[:len(labelSpec)-1]) } else { return nil, nil, fmt.Errorf("unknown label spec: %v", labelSpec) } } for _, removeLabel := range remove { if _, found := labels[removeLabel]; found { return nil, nil, fmt.Errorf("can not both modify and remove a label in the same command") } } return labels, remove, nil } func labelFunc(obj runtime.Object, overwrite bool, resourceVersion string, labels map[string]string, remove []string) error { accessor, err := meta.Accessor(obj) if err != nil { return err } if !overwrite { if err := validateNoOverwrites(accessor, labels); err != nil { return err } } objLabels := accessor.GetLabels() if objLabels == nil { objLabels = make(map[string]string) } for key, value := range labels { objLabels[key] = value } for _, label := range remove { delete(objLabels, label) } accessor.SetLabels(objLabels) if len(resourceVersion) != 0 { accessor.SetResourceVersion(resourceVersion) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/label/label_test.go000066400000000000000000000575151504711711200303500ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package label import ( "bytes" "fmt" "io" "net/http" "reflect" "strings" "testing" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/json" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestValidateLabels(t *testing.T) { tests := []struct { meta *metav1.ObjectMeta labels map[string]string expectErr bool test string }{ { meta: &metav1.ObjectMeta{ Labels: map[string]string{ "a": "b", "c": "d", }, }, labels: map[string]string{ "a": "c", "d": "b", }, test: "one shared", expectErr: true, }, { meta: &metav1.ObjectMeta{ Labels: map[string]string{ "a": "b", "c": "d", }, }, labels: map[string]string{ "b": "d", "c": "a", }, test: "second shared", expectErr: true, }, { meta: &metav1.ObjectMeta{ Labels: map[string]string{ "a": "b", "c": "d", }, }, labels: map[string]string{ "b": "a", "d": "c", }, test: "no overlap", }, { meta: &metav1.ObjectMeta{}, labels: map[string]string{ "b": "a", "d": "c", }, test: "no labels", }, } for _, test := range tests { err := validateNoOverwrites(test.meta, test.labels) if test.expectErr && err == nil { t.Errorf("%s: unexpected non-error", test.test) } if !test.expectErr && err != nil { t.Errorf("%s: unexpected error: %v", test.test, err) } } } func TestParseLabels(t *testing.T) { tests := []struct { labels []string expected map[string]string expectedRemove []string expectErr bool }{ { labels: []string{"a=b", "c=d"}, expected: map[string]string{"a": "b", "c": "d"}, }, { labels: []string{}, expected: map[string]string{}, }, { labels: []string{"a=b", "c=d", "e-"}, expected: map[string]string{"a": "b", "c": "d"}, expectedRemove: []string{"e"}, }, { labels: []string{"ab", "c=d"}, expectErr: true, }, { labels: []string{"a=b", "c=d", "a-"}, expectErr: true, }, { labels: []string{"a="}, expected: map[string]string{"a": ""}, }, { labels: []string{"a=%^$"}, expectErr: true, }, } for _, test := range tests { labels, remove, err := parseLabels(test.labels) if test.expectErr && err == nil { t.Errorf("unexpected non-error: %v", test) } if !test.expectErr && err != nil { t.Errorf("unexpected error: %v %v", err, test) } if !reflect.DeepEqual(labels, test.expected) { t.Errorf("expected: %v, got %v", test.expected, labels) } if !reflect.DeepEqual(remove, test.expectedRemove) { t.Errorf("expected: %v, got %v", test.expectedRemove, remove) } } } func TestLabelFunc(t *testing.T) { tests := []struct { obj runtime.Object overwrite bool version string labels map[string]string remove []string expected runtime.Object expectErr string }{ { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"a": "b"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"a": "c"}, expectErr: "'a' already has a value (b), and --overwrite is false", }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"a": "c"}, overwrite: true, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "c"}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"c": "d"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "c": "d"}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"c": "d"}, version: "2", expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "c": "d"}, ResourceVersion: "2", }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{}, remove: []string{"a"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{}, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "c": "d"}, }, }, labels: map[string]string{"e": "f"}, remove: []string{"a"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "c": "d", "e": "f", }, }, }, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{}, }, labels: map[string]string{"a": "b"}, expected: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, }, } for _, test := range tests { err := labelFunc(test.obj, test.overwrite, test.version, test.labels, test.remove) if test.expectErr != "" { if err == nil { t.Errorf("unexpected non-error: %v", test) } if err.Error() != test.expectErr { t.Errorf("error expected: %v, got: %v", test.expectErr, err.Error()) } continue } if test.expectErr == "" && err != nil { t.Errorf("unexpected error: %v %v", err, test) } if !reflect.DeepEqual(test.obj, test.expected) { t.Errorf("expected: %v, got %v", test.expected, test.obj) } } } func TestLabelErrors(t *testing.T) { testCases := map[string]struct { args []string errFn func(error) bool }{ "no args": { args: []string{}, errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") }, }, "not enough labels": { args: []string{"pods"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "at least one label update is required") }, }, "wrong labels": { args: []string{"pods", "-"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "at least one label update is required") }, }, "wrong labels 2": { args: []string{"pods", "=bar"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "at least one label update is required") }, }, "no resources": { args: []string{"pods-"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") }, }, "no resources 2": { args: []string{"pods=bar"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "one or more resources must be specified") }, }, "resources but no selectors": { args: []string{"pods", "app=bar"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "resource(s) were provided, but no name was specified") }, }, "multiple resources but no selectors": { args: []string{"pods,deployments", "app=bar"}, errFn: func(err error) bool { return strings.Contains(err.Error(), "resource(s) were provided, but no name was specified") }, }, } for k, testCase := range testCases { t.Run(k, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdLabel(tf, ioStreams) cmd.SetOut(buf) cmd.SetErr(buf) opts := NewLabelOptions(ioStreams) err := opts.Complete(tf, cmd, testCase.args) if err == nil { err = opts.Validate() } if err == nil { err = opts.RunLabel() } if !testCase.errFn(err) { t.Errorf("%s: unexpected error: %v", k, err) return } if buf.Len() > 0 { t.Errorf("buffer should be empty: %s", buf.String()) } }) } } func TestLabelForResourceFromFile(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/replicationcontrollers/cassandra": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/replicationcontrollers/cassandra": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdLabel(tf, ioStreams) opts := NewLabelOptions(ioStreams) opts.Filenames = []string{"../../../testdata/controller.yaml"} err := opts.Complete(tf, cmd, []string{"a=b"}) if err == nil { err = opts.Validate() } if err == nil { err = opts.RunLabel() } if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(buf.String(), "labeled") { t.Errorf("did not set labels: %s", buf.String()) } } func TestLabelLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdLabel(tf, ioStreams) opts := NewLabelOptions(ioStreams) opts.Filenames = []string{"../../../testdata/controller.yaml"} opts.local = true err := opts.Complete(tf, cmd, []string{"a=b"}) if err == nil { err = opts.Validate() } if err == nil { err = opts.RunLabel() } if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(buf.String(), "labeled") { t.Errorf("did not set labels: %s", buf.String()) } } func TestLabelMultipleObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/pods": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil case "/namespaces/test/pods/bar": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[1])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams() opts := NewLabelOptions(ioStreams) opts.all = true cmd := NewCmdLabel(tf, ioStreams) err := opts.Complete(tf, cmd, []string{"pods", "a=b"}) if err == nil { err = opts.Validate() } if err == nil { err = opts.RunLabel() } if err != nil { t.Fatalf("unexpected error: %v", err) } if strings.Count(buf.String(), "labeled") != len(pods.Items) { t.Errorf("not all labels are set: %s", buf.String()) } } func TestLabelResourceVersion(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewBufferString( `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"10"}}`, ))}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/pods/foo": body, err := io.ReadAll(req.Body) if err != nil { t.Fatal(err) } if !bytes.Equal(body, []byte(`{"metadata":{"labels":{"a":"b"},"resourceVersion":"10"}}`)) { t.Fatalf("expected patch with resourceVersion set, got %s", string(body)) } return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewBufferString( `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","resourceVersion":"11"}}`, ))}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() iostreams, _, bufOut, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdLabel(tf, iostreams) cmd.SetOut(bufOut) cmd.SetErr(bufOut) options := NewLabelOptions(iostreams) options.resourceVersion = "10" args := []string{"pods/foo", "a=b"} if err := options.Complete(tf, cmd, args); err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.Validate(); err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.RunLabel(); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestRunLabelMsg(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.UnstructuredClient = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "testgroup", Version: "v1"}, NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.Method { case "GET": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewBufferString( `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","labels":{"existing":"abc"}}}`, ))}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } case "PATCH": switch req.URL.Path { case "/namespaces/test/pods/foo": return &http.Response{ StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewBufferString( `{"kind":"Pod","apiVersion":"v1","metadata":{"name":"foo","namespace":"test","labels":{"existing":"abc"}}}`, ))}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } default: t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() testCases := []struct { name string args []string overwrite bool dryRun string expectedOut string expectedError error }{ { name: "set new label", args: []string{"pods/foo", "foo=bar"}, expectedOut: "pod/foo labeled\n", }, { name: "attempt to set existing label without using overwrite flag", args: []string{"pods/foo", "existing=bar"}, expectedError: fmt.Errorf("'existing' already has a value (abc), and --overwrite is false"), }, { name: "set existing label", args: []string{"pods/foo", "existing=bar"}, overwrite: true, expectedOut: "pod/foo labeled\n", }, { name: "unset existing label", args: []string{"pods/foo", "existing-"}, expectedOut: "pod/foo unlabeled\n", }, { name: "unset nonexisting label", args: []string{"pods/foo", "foo-"}, expectedOut: `label "foo" not found. pod/foo not labeled `, }, { name: "set new label with server dry run", args: []string{"pods/foo", "foo=bar"}, dryRun: "server", expectedOut: "pod/foo labeled (server dry run)\n", }, { name: "set new label with client dry run", args: []string{"pods/foo", "foo=bar"}, dryRun: "client", expectedOut: "pod/foo labeled (dry run)\n", }, { name: "unset existing label with server dry run", args: []string{"pods/foo", "existing-"}, dryRun: "server", expectedOut: "pod/foo unlabeled (server dry run)\n", }, { name: "unset existing label with client dry run", args: []string{"pods/foo", "existing-"}, dryRun: "client", expectedOut: "pod/foo unlabeled (dry run)\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { iostreams, _, bufOut, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdLabel(tf, iostreams) cmd.SetOut(bufOut) cmd.SetErr(bufOut) if tc.dryRun != "" { cmd.Flags().Set("dry-run", tc.dryRun) } options := NewLabelOptions(iostreams) if tc.overwrite { options.overwrite = true } if err := options.Complete(tf, cmd, tc.args); err != nil { t.Fatalf("unexpected error: %v", err) } if err := options.Validate(); err != nil { t.Fatalf("unexpected error: %v", err) } err := options.RunLabel() if tc.expectedError == nil { if err != nil { t.Fatalf("unexpected error: %v", err) } } else { if err == nil { t.Fatalf("expected, but did not get, error: %s", tc.expectedError.Error()) } else if err.Error() != tc.expectedError.Error() { t.Fatalf("wrong error\ngot: %s\nexpected: %s\n", err.Error(), tc.expectedError.Error()) } } if bufOut.String() != tc.expectedOut { t.Fatalf("wrong output\ngot:\n%s\nexpected:\n%s\n", bufOut.String(), tc.expectedOut) } }) } } func TestLabelMsg(t *testing.T) { tests := []struct { obj runtime.Object overwrite bool resourceVersion string labels map[string]string remove []string expectObj runtime.Object expectMsg string expectErr bool }{ { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"a": "b"}, expectMsg: MsgNotLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{}, }, labels: map[string]string{"a": "b"}, expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, expectMsg: MsgLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"a": "c"}, overwrite: true, expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "c"}, }, }, expectMsg: MsgLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"c": "d"}, expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "c": "d"}, }, }, expectMsg: MsgLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{"c": "d"}, resourceVersion: "2", expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "c": "d"}, ResourceVersion: "2", }, }, expectMsg: MsgLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, labels: map[string]string{}, remove: []string{"a"}, expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{}, }, }, expectMsg: MsgUnLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b", "c": "d"}, }, }, labels: map[string]string{"e": "f"}, remove: []string{"a"}, expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "c": "d", "e": "f", }, }, }, expectMsg: MsgLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"status": "unhealthy"}, }, }, labels: map[string]string{"status": "healthy"}, overwrite: true, expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "status": "healthy", }, }, }, expectMsg: MsgLabeled, }, { obj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"status": "unhealthy"}, }, }, labels: map[string]string{"status": "healthy"}, overwrite: false, expectObj: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "status": "unhealthy", }, }, }, expectMsg: MsgNotLabeled, expectErr: true, }, } for _, test := range tests { oldData, err := json.Marshal(test.obj) if err != nil { t.Errorf("unexpected error: %v %v", err, test) } err = labelFunc(test.obj, test.overwrite, test.resourceVersion, test.labels, test.remove) if test.expectErr && err == nil { t.Errorf("unexpected non-error: %v", test) continue } if !test.expectErr && err != nil { t.Errorf("unexpected error: %v %v", err, test) } newObj, err := json.Marshal(test.obj) if err != nil { t.Errorf("unexpected error: %v %v", err, test) } dataChangeMsg := updateDataChangeMsg(oldData, newObj, test.overwrite) if dataChangeMsg != test.expectMsg { t.Errorf("unexpected dataChangeMsg: %v != %v, %v", dataChangeMsg, test.expectMsg, test) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/logs/000077500000000000000000000000001504711711200255535ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs.go000066400000000000000000000405421504711711200270530ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logs import ( "bufio" "context" "errors" "fmt" "io" "regexp" "sync" "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" ) const ( logsUsageStr = "logs [-f] [-p] (POD | TYPE/NAME) [-c CONTAINER]" ) var ( logsLong = templates.LongDesc(i18n.T(` Print the logs for a container in a pod or specified resource. If the pod has only one container, the container name is optional.`)) logsExample = templates.Examples(i18n.T(` # Return snapshot logs from pod nginx with only one container kubectl logs nginx # Return snapshot logs from pod nginx, prefixing each line with the source pod and container name kubectl logs nginx --prefix # Return snapshot logs from pod nginx, limiting output to 500 bytes kubectl logs nginx --limit-bytes=500 # Return snapshot logs from pod nginx, waiting up to 20 seconds for it to start running. kubectl logs nginx --pod-running-timeout=20s # Return snapshot logs from pod nginx with multi containers kubectl logs nginx --all-containers=true # Return snapshot logs from all pods in the deployment nginx kubectl logs deployment/nginx --all-pods=true # Return snapshot logs from all containers in pods defined by label app=nginx kubectl logs -l app=nginx --all-containers=true # Return snapshot logs from all pods defined by label app=nginx, limiting concurrent log requests to 10 pods kubectl logs -l app=nginx --max-log-requests=10 # Return snapshot of previous terminated ruby container logs from pod web-1 kubectl logs -p -c ruby web-1 # Begin streaming the logs from pod nginx, continuing even if errors occur kubectl logs nginx -f --ignore-errors=true # Begin streaming the logs of the ruby container in pod web-1 kubectl logs -f -c ruby web-1 # Begin streaming the logs from all containers in pods defined by label app=nginx kubectl logs -f -l app=nginx --all-containers=true # Display only the most recent 20 lines of output in pod nginx kubectl logs --tail=20 nginx # Show all logs from pod nginx written in the last hour kubectl logs --since=1h nginx # Show all logs with timestamps from pod nginx starting from August 30, 2024, at 06:00:00 UTC kubectl logs nginx --since-time=2024-08-30T06:00:00Z --timestamps=true # Show logs from a kubelet with an expired serving certificate kubectl logs --insecure-skip-tls-verify-backend nginx # Return snapshot logs from first container of a job named hello kubectl logs job/hello # Return snapshot logs from container nginx-1 of a deployment named nginx kubectl logs deployment/nginx -c nginx-1`)) selectorTail int64 = 10 logsUsageErrStr = fmt.Sprintf("expected '%s'.\nPOD or TYPE/NAME is a required argument for the logs command", logsUsageStr) ) const ( defaultPodLogsTimeout = 20 * time.Second ) type LogsOptions struct { Namespace string ResourceArg string AllContainers bool AllPods bool Options runtime.Object Resources []string ConsumeRequestFn func(context.Context, rest.ResponseWrapper, io.Writer) error // PodLogOptions SinceTime string SinceSeconds time.Duration Follow bool Previous bool Timestamps bool IgnoreLogErrors bool LimitBytes int64 Tail int64 Container string InsecureSkipTLSVerifyBackend bool // whether or not a container name was given via --container ContainerNameSpecified bool Selector string MaxFollowConcurrency int Prefix bool Object runtime.Object GetPodTimeout time.Duration RESTClientGetter genericclioptions.RESTClientGetter LogsForObject polymorphichelpers.LogsForObjectFunc AllPodLogsForObject polymorphichelpers.AllPodLogsForObjectFunc genericiooptions.IOStreams TailSpecified bool containerNameFromRefSpecRegexp *regexp.Regexp } func NewLogsOptions(streams genericiooptions.IOStreams) *LogsOptions { return &LogsOptions{ IOStreams: streams, Tail: -1, MaxFollowConcurrency: 5, containerNameFromRefSpecRegexp: regexp.MustCompile(`spec\.(?:initContainers|containers|ephemeralContainers){(.+)}`), } } // NewCmdLogs creates a new pod logs command func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewLogsOptions(streams) cmd := &cobra.Command{ Use: logsUsageStr, DisableFlagsInUseLine: true, Short: i18n.T("Print the logs for a container in a pod"), Long: logsLong, Example: logsExample, ValidArgsFunction: completion.PodResourceNameAndContainerCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunLogs()) }, } o.AddFlags(cmd) return cmd } func (o *LogsOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.AllPods, "all-pods", o.AllPods, "Get logs from all pod(s). Sets prefix to true.") cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).") cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.") cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output") cmd.Flags().Int64Var(&o.LimitBytes, "limit-bytes", o.LimitBytes, "Maximum bytes of logs to return. Defaults to no limit.") cmd.Flags().BoolVarP(&o.Previous, "previous", "p", o.Previous, "If true, print the logs for the previous instance of the container in a pod if it exists.") cmd.Flags().Int64Var(&o.Tail, "tail", o.Tail, "Lines of recent log file to display. Defaults to -1 with no selector, showing all log lines otherwise 10, if a selector is provided.") cmd.Flags().BoolVar(&o.IgnoreLogErrors, "ignore-errors", o.IgnoreLogErrors, "If watching / following pod logs, allow for any errors that occur to be non-fatal") cmd.Flags().StringVar(&o.SinceTime, "since-time", o.SinceTime, i18n.T("Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.")) cmd.Flags().DurationVar(&o.SinceSeconds, "since", o.SinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") cmd.Flags().StringVarP(&o.Container, "container", "c", o.Container, "Print the logs of this container") cmd.Flags().BoolVar(&o.InsecureSkipTLSVerifyBackend, "insecure-skip-tls-verify-backend", o.InsecureSkipTLSVerifyBackend, "Skip verifying the identity of the kubelet that logs are requested from. In theory, an attacker could provide invalid log content back. You might want to use this if your kubelet serving certificates have expired.") cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodLogsTimeout) cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) cmd.Flags().IntVar(&o.MaxFollowConcurrency, "max-log-requests", o.MaxFollowConcurrency, "Specify maximum number of concurrent logs to follow when using by a selector. Defaults to 5.") cmd.Flags().BoolVar(&o.Prefix, "prefix", o.Prefix, "Prefix each log line with the log source (pod name and container name)") } func (o *LogsOptions) ToLogOptions() (*corev1.PodLogOptions, error) { logOptions := &corev1.PodLogOptions{ Container: o.Container, Follow: o.Follow, Previous: o.Previous, Timestamps: o.Timestamps, InsecureSkipTLSVerifyBackend: o.InsecureSkipTLSVerifyBackend, } if len(o.SinceTime) > 0 { t, err := util.ParseRFC3339(o.SinceTime, metav1.Now) if err != nil { return nil, err } logOptions.SinceTime = &t } if o.LimitBytes != 0 { logOptions.LimitBytes = &o.LimitBytes } if o.SinceSeconds != 0 { // round up to the nearest second sec := int64(o.SinceSeconds.Round(time.Second).Seconds()) logOptions.SinceSeconds = &sec } if len(o.Selector) > 0 && o.Tail == -1 && !o.TailSpecified { logOptions.TailLines = &selectorTail } else if o.Tail != -1 { logOptions.TailLines = &o.Tail } return logOptions, nil } func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { o.ContainerNameSpecified = cmd.Flag("container").Changed o.TailSpecified = cmd.Flag("tail").Changed o.Resources = args switch len(args) { case 0: if len(o.Selector) == 0 { return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr) } case 1: o.ResourceArg = args[0] if len(o.Selector) != 0 { return cmdutil.UsageErrorf(cmd, "only a selector (-l) or a POD name is allowed") } case 2: o.ResourceArg = args[0] o.Container = args[1] default: return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr) } if o.AllPods { o.Prefix = true } var err error o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.ConsumeRequestFn = DefaultConsumeRequest o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) if err != nil { return err } o.Options, err = o.ToLogOptions() if err != nil { return err } o.RESTClientGetter = f o.LogsForObject = polymorphichelpers.LogsForObjectFn o.AllPodLogsForObject = polymorphichelpers.AllPodLogsForObjectFn if o.Object == nil { builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). SingleResourceType() if o.ResourceArg != "" { builder.ResourceNames("pods", o.ResourceArg) } if o.Selector != "" { builder.ResourceTypes("pods").LabelSelectorParam(o.Selector) } infos, err := builder.Do().Infos() if err != nil { if apierrors.IsNotFound(err) { err = fmt.Errorf("error from server (NotFound): %w in namespace %q", err, o.Namespace) } return err } if o.Selector == "" && len(infos) != 1 { return errors.New("expected a resource") } o.Object = infos[0].Object if o.Selector != "" && len(o.Object.(*corev1.PodList).Items) == 0 { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) } } return nil } func (o LogsOptions) Validate() error { if len(o.SinceTime) > 0 && o.SinceSeconds != 0 { return fmt.Errorf("at most one of `sinceTime` or `sinceSeconds` may be specified") } logsOptions, ok := o.Options.(*corev1.PodLogOptions) if !ok { return errors.New("unexpected logs options object") } if o.AllContainers && len(logsOptions.Container) > 0 { return fmt.Errorf("--all-containers=true should not be specified with container name %s", logsOptions.Container) } if o.ContainerNameSpecified && len(o.Resources) == 2 { return fmt.Errorf("only one of -c or an inline [CONTAINER] arg is allowed") } if o.LimitBytes < 0 { return fmt.Errorf("--limit-bytes must be greater than 0") } if logsOptions.SinceSeconds != nil && *logsOptions.SinceSeconds < int64(0) { return fmt.Errorf("--since must be greater than 0") } if logsOptions.TailLines != nil && *logsOptions.TailLines < -1 { return fmt.Errorf("--tail must be greater than or equal to -1") } return nil } // RunLogs retrieves a pod log func (o LogsOptions) RunLogs() error { var requests map[corev1.ObjectReference]rest.ResponseWrapper var err error if o.AllPods { requests, err = o.AllPodLogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers) } else { requests, err = o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers) } if err != nil { return err } if o.Follow && len(requests) > 1 { if len(requests) > o.MaxFollowConcurrency { return fmt.Errorf( "you are attempting to follow %d log streams, but maximum allowed concurrency is %d, use --max-log-requests to increase the limit", len(requests), o.MaxFollowConcurrency, ) } } ctx, cancel := context.WithCancel(context.Background()) defer cancel() intr := interrupt.New(nil, cancel) return intr.Run(func() error { if o.Follow && len(requests) > 1 { return o.parallelConsumeRequest(ctx, requests) } return o.sequentialConsumeRequest(ctx, requests) }) } func (o LogsOptions) parallelConsumeRequest(ctx context.Context, requests map[corev1.ObjectReference]rest.ResponseWrapper) error { reader, writer := io.Pipe() wg := &sync.WaitGroup{} wg.Add(len(requests)) for objRef, request := range requests { go func(objRef corev1.ObjectReference, request rest.ResponseWrapper) { defer wg.Done() out := o.addPrefixIfNeeded(objRef, writer) if err := o.ConsumeRequestFn(ctx, request, out); err != nil { if !o.IgnoreLogErrors { writer.CloseWithError(err) // It's important to return here to propagate the error via the pipe return } fmt.Fprintf(writer, "error: %v\n", err) } }(objRef, request) } go func() { wg.Wait() writer.Close() }() _, err := io.Copy(o.Out, reader) return err } func (o LogsOptions) sequentialConsumeRequest(ctx context.Context, requests map[corev1.ObjectReference]rest.ResponseWrapper) error { for objRef, request := range requests { out := o.addPrefixIfNeeded(objRef, o.Out) if err := o.ConsumeRequestFn(ctx, request, out); err != nil { if !o.IgnoreLogErrors { return err } fmt.Fprintf(o.Out, "error: %v\n", err) } } return nil } func (o LogsOptions) addPrefixIfNeeded(ref corev1.ObjectReference, writer io.Writer) io.Writer { if !o.Prefix || ref.FieldPath == "" || ref.Name == "" { return writer } // We rely on ref.FieldPath to contain a reference to a container // including a container name (not an index) so we can get a container name // without making an extra API request. var containerName string containerNameMatches := o.containerNameFromRefSpecRegexp.FindStringSubmatch(ref.FieldPath) if len(containerNameMatches) == 2 { containerName = containerNameMatches[1] } prefix := fmt.Sprintf("[pod/%s/%s] ", ref.Name, containerName) return &prefixingWriter{ prefix: []byte(prefix), writer: writer, } } // DefaultConsumeRequest reads the data from request and writes into // the out writer. It buffers data from requests until the newline or io.EOF // occurs in the data, so it doesn't interleave logs sub-line // when running concurrently. // // A successful read returns err == nil, not err == io.EOF. // Because the function is defined to read from request until io.EOF, it does // not treat an io.EOF as an error to be reported. func DefaultConsumeRequest(ctx context.Context, request rest.ResponseWrapper, out io.Writer) error { readCloser, err := request.Stream(ctx) if err != nil { return err } defer readCloser.Close() r := bufio.NewReader(readCloser) for { bytes, err := r.ReadBytes('\n') if _, err := out.Write(bytes); err != nil { return err } if err != nil { if err != io.EOF { return err } return nil } } } type prefixingWriter struct { prefix []byte writer io.Writer } func (pw *prefixingWriter) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } // Perform an "atomic" write of a prefix and p to make sure that it doesn't interleave // sub-line when used concurrently with io.PipeWrite. n, err := pw.writer.Write(append(pw.prefix, p...)) if n > len(p) { // To comply with the io.Writer interface requirements we must // return a number of bytes written from p (0 <= n <= len(p)), // so we are ignoring the length of the prefix here. return len(p), err } return n, err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs_test.go000066400000000000000000000722261504711711200301160ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logs import ( "bytes" "context" "errors" "fmt" "io" "net/http" "strings" "sync" "testing" "testing/iotest" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestLog(t *testing.T) { tests := []struct { name string opts func(genericiooptions.IOStreams) *LogsOptions expectedErr string expectedOutSubstrings []string }{ { name: "v1 - pod log", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "some-pod", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest return o }, expectedOutSubstrings: []string{"test log content\n"}, }, { name: "pod logs with prefix", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod", FieldPath: "spec.containers{test-container}", }: &responseWrapperMock{data: strings.NewReader("test log content\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Prefix = true return o }, expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"}, }, { name: "stateful set logs with all pods", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-sts-0", FieldPath: "spec.containers{test-container}", }: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-0\n")}, { Kind: "Pod", Name: "test-sts-1", FieldPath: "spec.containers{test-container}", }: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-1\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Prefix = true return o }, expectedOutSubstrings: []string{ "[pod/test-sts-0/test-container] test log content for pod test-sts-0\n", "[pod/test-sts-1/test-container] test log content for pod test-sts-1\n", }, }, { name: "pod logs with prefix: init container", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod", FieldPath: "spec.initContainers{test-container}", }: &responseWrapperMock{data: strings.NewReader("test log content\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Prefix = true return o }, expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"}, }, { name: "pod logs with prefix: ephemeral container", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod", FieldPath: "spec.ephemeralContainers{test-container}", }: &responseWrapperMock{data: strings.NewReader("test log content\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Prefix = true return o }, expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"}, }, { name: "get logs from multiple requests sequentially", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "some-pod-1", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")}, { Kind: "Pod", Name: "some-pod-2", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")}, { Kind: "Pod", Name: "some-pod-3", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest return o }, expectedOutSubstrings: []string{ "test log content from source 1\n", "test log content from source 2\n", "test log content from source 3\n", }, }, { name: "follow logs from multiple requests concurrently", opts: func(streams genericiooptions.IOStreams) *LogsOptions { wg := &sync.WaitGroup{} mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "some-pod-1", FieldPath: "spec.containers{some-container-1}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")}, { Kind: "Pod", Name: "some-pod-2", FieldPath: "spec.containers{some-container-2}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")}, { Kind: "Pod", Name: "some-pod-3", FieldPath: "spec.containers{some-container-3}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")}, }, wg: wg, } wg.Add(3) o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Follow = true return o }, expectedOutSubstrings: []string{ "test log content from source 1\n", "test log content from source 2\n", "test log content from source 3\n", }, }, { name: "fail to follow logs from multiple requests when there are more logs sources then MaxFollowConcurrency allows", opts: func(streams genericiooptions.IOStreams) *LogsOptions { wg := &sync.WaitGroup{} mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod-1", FieldPath: "spec.containers{test-container-1}", }: &responseWrapperMock{data: strings.NewReader("test log content\n")}, { Kind: "Pod", Name: "test-pod-2", FieldPath: "spec.containers{test-container-2}", }: &responseWrapperMock{data: strings.NewReader("test log content\n")}, { Kind: "Pod", Name: "test-pod-3", FieldPath: "spec.containers{test-container-3}", }: &responseWrapperMock{data: strings.NewReader("test log content\n")}, }, wg: wg, } wg.Add(3) o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.MaxFollowConcurrency = 2 o.Follow = true return o }, expectedErr: "you are attempting to follow 3 log streams, but maximum allowed concurrency is 2, use --max-log-requests to increase the limit", }, { name: "fail if LogsForObject fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) { return nil, errors.New("Error from the LogsForObject") } return o }, expectedErr: "Error from the LogsForObject", }, { name: "fail to get logs, if ConsumeRequestFn fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod-1", FieldPath: "spec.containers{test-container-1}", }: &responseWrapperMock{}, { Kind: "Pod", Name: "test-pod-2", FieldPath: "spec.containers{test-container-1}", }: &responseWrapperMock{}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error { return errors.New("Error from the ConsumeRequestFn") } return o }, expectedErr: "Error from the ConsumeRequestFn", }, { name: "follow logs from multiple requests concurrently with prefix", opts: func(streams genericiooptions.IOStreams) *LogsOptions { wg := &sync.WaitGroup{} mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod-1", FieldPath: "spec.containers{test-container-1}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")}, { Kind: "Pod", Name: "test-pod-2", FieldPath: "spec.containers{test-container-2}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")}, { Kind: "Pod", Name: "test-pod-3", FieldPath: "spec.containers{test-container-3}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")}, }, wg: wg, } wg.Add(3) o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Follow = true o.Prefix = true return o }, expectedOutSubstrings: []string{ "[pod/test-pod-1/test-container-1] test log content from source 1\n", "[pod/test-pod-2/test-container-2] test log content from source 2\n", "[pod/test-pod-3/test-container-3] test log content from source 3\n", }, }, { name: "fail to follow logs from multiple requests, if ConsumeRequestFn fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { wg := &sync.WaitGroup{} mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod-1", FieldPath: "spec.containers{test-container-1}", }: &responseWrapperMock{}, { Kind: "Pod", Name: "test-pod-2", FieldPath: "spec.containers{test-container-2}", }: &responseWrapperMock{}, { Kind: "Pod", Name: "test-pod-3", FieldPath: "spec.containers{test-container-3}", }: &responseWrapperMock{}, }, wg: wg, } wg.Add(3) o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error { return errors.New("Error from the ConsumeRequestFn") } o.Follow = true return o }, expectedErr: "Error from the ConsumeRequestFn", }, { name: "fail to follow logs, if ConsumeRequestFn fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "test-pod-1", FieldPath: "spec.containers{test-container-1}", }: &responseWrapperMock{}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error { return errors.New("Error from the ConsumeRequestFn") } o.Follow = true return o }, expectedErr: "Error from the ConsumeRequestFn", }, { name: "get logs from multiple requests and ignores the error if the container fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "some-pod-error-container", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{err: errors.New("error-container")}, { Kind: "Pod", Name: "some-pod-1", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")}, { Kind: "Pod", Name: "some-pod-2", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.IgnoreLogErrors = true return o }, expectedOutSubstrings: []string{ "error-container\n", "test log content from source 1\n", "test log content from source 2\n", }, }, { name: "get logs from multiple requests and an container fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "some-pod-error-container", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{err: errors.New("error-container")}, { Kind: "Pod", Name: "some-pod", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest return o }, expectedErr: "error-container", }, { name: "follow logs from multiple requests and ignores the error if the container fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "some-pod-error-container", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{err: errors.New("error-container")}, { Kind: "Pod", Name: "some-pod-1", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")}, { Kind: "Pod", Name: "some-pod-2", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.IgnoreLogErrors = true o.Follow = true return o }, expectedOutSubstrings: []string{ "error-container\n", "test log content from source 1\n", "test log content from source 2\n", }, }, { name: "follow logs from multiple requests and an container fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { mock := &logTestMock{ logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ { Kind: "Pod", Name: "some-pod-error-container", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{err: errors.New("error-container")}, { Kind: "Pod", Name: "some-pod", FieldPath: "spec.containers{some-container}", }: &responseWrapperMock{data: strings.NewReader("test log content from source\n")}, }, } o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Follow = true return o }, expectedErr: "error-container", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() streams, _, buf, _ := genericiooptions.NewTestIOStreams() opts := test.opts(streams) opts.Namespace = "test" opts.Object = testPod() opts.Options = &corev1.PodLogOptions{} err := opts.RunLogs() if err == nil && len(test.expectedErr) > 0 { t.Fatalf("expected error %q, got none", test.expectedErr) } if err != nil && !strings.Contains(err.Error(), test.expectedErr) { t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error()) } bufStr := buf.String() if test.expectedOutSubstrings != nil { for _, substr := range test.expectedOutSubstrings { if !strings.Contains(bufStr, substr) { t.Errorf("%s: expected to contain %#v. Output: %#v", test.name, substr, bufStr) } } } }) } } func testPod() *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, Containers: []corev1.Container{ { Name: "bar", }, }, }, } } func TestValidateLogOptions(t *testing.T) { f := cmdtesting.NewTestFactory() defer f.Cleanup() f.WithNamespace("") tests := []struct { name string args []string opts func(genericiooptions.IOStreams) *LogsOptions expected string }{ { name: "since & since-time", opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.SinceSeconds = time.Hour o.SinceTime = "2006-01-02T15:04:05Z" var err error o.Options, err = o.ToLogOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } return o }, args: []string{"foo"}, expected: "at most one of `sinceTime` or `sinceSeconds` may be specified", }, { name: "negative since-time", opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.SinceSeconds = -1 * time.Second var err error o.Options, err = o.ToLogOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } return o }, args: []string{"foo"}, expected: "must be greater than 0", }, { name: "negative limit-bytes", opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.LimitBytes = -100 var err error o.Options, err = o.ToLogOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } return o }, args: []string{"foo"}, expected: "must be greater than 0", }, { name: "negative tail", opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.Tail = -100 var err error o.Options, err = o.ToLogOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } return o }, args: []string{"foo"}, expected: "--tail must be greater than or equal to -1", }, { name: "container name combined with --all-containers", opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.AllContainers = true o.Container = "my-container" var err error o.Options, err = o.ToLogOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } return o }, args: []string{"my-pod", "my-container"}, expected: "--all-containers=true should not be specified with container", }, { name: "container name combined with second argument", opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.Container = "my-container" o.ContainerNameSpecified = true var err error o.Options, err = o.ToLogOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } return o }, args: []string{"my-pod", "my-container"}, expected: "only one of -c or an inline", }, } for _, test := range tests { streams := genericiooptions.NewTestIOStreamsDiscard() o := test.opts(streams) o.Resources = test.args err := o.Validate() if err == nil { t.Fatalf("expected error %q, got none", test.expected) } if !strings.Contains(err.Error(), test.expected) { t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expected, err.Error()) } } } func TestLogComplete(t *testing.T) { f := cmdtesting.NewTestFactory() defer f.Cleanup() tests := []struct { name string args []string opts func(genericiooptions.IOStreams) *LogsOptions expected string }{ { name: "One args case", args: []string{"foo"}, opts: func(streams genericiooptions.IOStreams) *LogsOptions { o := NewLogsOptions(streams) o.Selector = "foo" return o }, expected: "only a selector (-l) or a POD name is allowed", }, } for _, test := range tests { cmd := NewCmdLogs(f, genericiooptions.NewTestIOStreamsDiscard()) out := "" // checkErr breaks tests in case of errors, plus we just // need to check errors returned by the command validation o := test.opts(genericiooptions.NewTestIOStreamsDiscard()) err := o.Complete(f, cmd, test.args) if err == nil { t.Fatalf("expected error %q, got none", test.expected) } out = err.Error() if !strings.Contains(out, test.expected) { t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expected, out) } } } func TestDefaultConsumeRequest(t *testing.T) { tests := []struct { name string request restclient.ResponseWrapper expectedErr string expectedOut string }{ { name: "error from request stream", request: &responseWrapperMock{ err: errors.New("err from the stream"), }, expectedErr: "err from the stream", }, { name: "error while reading", request: &responseWrapperMock{ data: iotest.TimeoutReader(strings.NewReader("Some data")), }, expectedErr: iotest.ErrTimeout.Error(), expectedOut: "Some data", }, { name: "read with empty string", request: &responseWrapperMock{ data: strings.NewReader(""), }, expectedOut: "", }, { name: "read without new lines", request: &responseWrapperMock{ data: strings.NewReader("some string without a new line"), }, expectedOut: "some string without a new line", }, { name: "read with newlines in the middle", request: &responseWrapperMock{ data: strings.NewReader("foo\nbar"), }, expectedOut: "foo\nbar", }, { name: "read with newline at the end", request: &responseWrapperMock{ data: strings.NewReader("foo\n"), }, expectedOut: "foo\n", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { buf := &bytes.Buffer{} err := DefaultConsumeRequest(context.TODO(), test.request, buf) if err != nil && !strings.Contains(err.Error(), test.expectedErr) { t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error()) } if buf.String() != test.expectedOut { t.Errorf("%s: did not get expected log content. Got: %s", test.name, buf.String()) } }) } } func TestNoResourceFoundMessage(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) pods, _, _ := cmdtesting.EmptyTestData() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case "/namespaces/test/pods": if req.URL.Query().Get("labelSelector") == "foo" { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil } t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdLogs(tf, streams) o := NewLogsOptions(streams) o.Selector = "foo" err := o.Complete(tf, cmd, []string{}) if err != nil { t.Fatalf("Unexpected error, expected none, got %v", err) } expected := "" if e, a := expected, buf.String(); e != a { t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a) } expectedErr := "No resources found in test namespace.\n" if e, a := expectedErr, errbuf.String(); e != a { t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a) } } func TestNoPodInNamespaceFoundMessage(t *testing.T) { namespace, podName := "test", "bar" tf := cmdtesting.NewTestFactory().WithNamespace(namespace) defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) errStatus := apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, podName).Status() tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch req.URL.Path { case fmt.Sprintf("/namespaces/%s/pods/%s", namespace, podName): fallthrough case fmt.Sprintf("/namespaces/%s/pods", namespace): fallthrough case fmt.Sprintf("/api/v1/namespaces/%s", namespace): return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &errStatus)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdLogs(tf, streams) o := NewLogsOptions(streams) err := o.Complete(tf, cmd, []string{podName}) if err == nil { t.Fatal("Expected NotFound error, got nil") } expected := fmt.Sprintf("error from server (NotFound): pods %q not found in namespace %q", podName, namespace) if e, a := expected, err.Error(); e != a { t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a) } } type responseWrapperMock struct { data io.Reader err error } func (r *responseWrapperMock) DoRaw(context.Context) ([]byte, error) { data, _ := io.ReadAll(r.data) return data, r.err } func (r *responseWrapperMock) Stream(context.Context) (io.ReadCloser, error) { return io.NopCloser(r.data), r.err } type logTestMock struct { logsForObjectRequests map[corev1.ObjectReference]restclient.ResponseWrapper // We need a WaitGroup in some test cases to make sure that we fetch logs concurrently. // These test cases will finish successfully without the WaitGroup, but the WaitGroup // will help us to identify regression when someone accidentally changes // concurrent fetching to sequential wg *sync.WaitGroup } func (l *logTestMock) mockConsumeRequest(ctx context.Context, request restclient.ResponseWrapper, out io.Writer) error { readCloser, err := request.Stream(ctx) if err != nil { return err } defer readCloser.Close() // Just copy everything for a test sake _, err = io.Copy(out, readCloser) if l.wg != nil { l.wg.Done() l.wg.Wait() } return err } func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) { switch object.(type) { case *appsv1.Deployment: _, ok := options.(*corev1.PodLogOptions) if !ok { return nil, errors.New("provided options object is not a PodLogOptions") } return l.logsForObjectRequests, nil case *corev1.Pod: _, ok := options.(*corev1.PodLogOptions) if !ok { return nil, errors.New("provided options object is not a PodLogOptions") } return l.logsForObjectRequests, nil default: return nil, fmt.Errorf("cannot get the logs from %T", object) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/options/000077500000000000000000000000001504711711200263025ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/options/options.go000066400000000000000000000030211504711711200303200ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package options import ( "io" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "github.com/spf13/cobra" ) var ( optionsExample = templates.Examples(i18n.T(` # Print flags inherited by all commands kubectl options`)) ) // NewCmdOptions implements the options command func NewCmdOptions(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "options", Short: i18n.T("Print the list of flags inherited by all commands"), Long: i18n.T("Print the list of flags inherited by all commands"), Example: optionsExample, Run: func(cmd *cobra.Command, args []string) { cmd.Usage() }, } // The `options` command needs write its output to the `out` stream // (typically stdout). Without calling SetOutput here, the Usage() // function call will fall back to stderr. // // See https://github.com/kubernetes/kubernetes/pull/46394 for details. cmd.SetOut(out) cmd.SetErr(out) templates.UseOptionsTemplates(cmd) return cmd } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/patch/000077500000000000000000000000001504711711200257065ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/patch/patch.go000066400000000000000000000273661504711711200273520ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package patch import ( "fmt" "os" "reflect" "strings" "github.com/pkg/errors" "github.com/spf13/cobra" jsonpatch "gopkg.in/evanphx/json-patch.v4" "k8s.io/klog/v2" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var patchTypes = map[string]types.PatchType{"json": types.JSONPatchType, "merge": types.MergePatchType, "strategic": types.StrategicMergePatchType} // PatchOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type PatchOptions struct { resource.FilenameOptions RecordFlags *genericclioptions.RecordFlags PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Recorder genericclioptions.Recorder Local bool PatchType string Patch string PatchFile string Subresource string namespace string enforceNamespace bool dryRunStrategy cmdutil.DryRunStrategy outputFormat string args []string builder *resource.Builder unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) fieldManager string genericiooptions.IOStreams } var ( patchLong = templates.LongDesc(i18n.T(` Update fields of a resource using strategic merge patch, a JSON merge patch, or a JSON patch. JSON and YAML formats are accepted. Note: Strategic merge patch is not supported for custom resources.`)) patchExample = templates.Examples(i18n.T(` # Partially update a node using a strategic merge patch, specifying the patch as JSON kubectl patch node k8s-node-1 -p '{"spec":{"unschedulable":true}}' # Partially update a node using a strategic merge patch, specifying the patch as YAML kubectl patch node k8s-node-1 -p $'spec:\n unschedulable: true' # Partially update a node identified by the type and name specified in "node.json" using strategic merge patch kubectl patch -f node.json -p '{"spec":{"unschedulable":true}}' # Update a container's image; spec.containers[*].name is required because it's a merge key kubectl patch pod valid-pod -p '{"spec":{"containers":[{"name":"kubernetes-serve-hostname","image":"new image"}]}}' # Update a container's image using a JSON patch with positional arrays kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"new image"}]' # Update a deployment's replicas through the 'scale' subresource using a merge patch kubectl patch deployment nginx-deployment --subresource='scale' --type='merge' -p '{"spec":{"replicas":2}}'`)) ) func NewPatchOptions(ioStreams genericiooptions.IOStreams) *PatchOptions { return &PatchOptions{ RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, PrintFlags: genericclioptions.NewPrintFlags("patched").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } func NewCmdPatch(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewPatchOptions(ioStreams) cmd := &cobra.Command{ Use: "patch (-f FILENAME | TYPE NAME) [-p PATCH|--patch-file FILE]", DisableFlagsInUseLine: true, Short: i18n.T("Update fields of a resource"), Long: patchLong, Example: patchExample, ValidArgsFunction: completion.ResourceTypeAndNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunPatch()) }, } o.RecordFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) cmd.Flags().StringVarP(&o.Patch, "patch", "p", "", "The patch to be applied to the resource JSON file.") cmd.Flags().StringVar(&o.PatchFile, "patch-file", "", "A file containing a patch to be applied to the resource.") cmd.Flags().StringVar(&o.PatchType, "type", "strategic", fmt.Sprintf("The type of patch being provided; one of %v", sets.List(sets.KeySet(patchTypes)))) cmdutil.AddDryRunFlag(cmd) cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update") cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, patch will operate on the content of the file, not the server-side resource.") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-patch") cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, patch will operate on the subresource of the requested object.") return cmd } func (o *PatchOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.outputFormat = cmdutil.GetFlagString(cmd, "output") o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation return o.PrintFlags.ToPrinter() } o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil && !(o.Local && clientcmd.IsEmptyConfig(err)) { return err } o.args = args o.builder = f.NewBuilder() o.unstructuredClientForMapping = f.UnstructuredClientForMapping return nil } func (o *PatchOptions) Validate() error { if len(o.Patch) > 0 && len(o.PatchFile) > 0 { return fmt.Errorf("cannot specify --patch and --patch-file together") } if len(o.Patch) == 0 && len(o.PatchFile) == 0 { return fmt.Errorf("must specify --patch or --patch-file containing the contents of the patch") } if o.Local && len(o.args) != 0 { return fmt.Errorf("cannot specify --local and server resources") } if o.Local && o.dryRunStrategy == cmdutil.DryRunServer { return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?") } if len(o.PatchType) != 0 { if _, ok := patchTypes[strings.ToLower(o.PatchType)]; !ok { return fmt.Errorf("--type must be one of %v, not %q", sets.List(sets.KeySet(patchTypes)), o.PatchType) } } return nil } func (o *PatchOptions) RunPatch() error { patchType := types.StrategicMergePatchType if len(o.PatchType) != 0 { patchType = patchTypes[strings.ToLower(o.PatchType)] } var patchBytes []byte if len(o.PatchFile) > 0 { var err error patchBytes, err = os.ReadFile(o.PatchFile) if err != nil { return fmt.Errorf("unable to read patch file: %v", err) } } else { patchBytes = []byte(o.Patch) } patchBytes, err := yaml.ToJSON(patchBytes) if err != nil { return fmt.Errorf("unable to parse %q: %v", o.Patch, err) } r := o.builder. Unstructured(). ContinueOnError(). LocalParam(o.Local). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, &o.FilenameOptions). Subresource(o.Subresource). ResourceTypeOrNameArgs(false, o.args...). Flatten(). Do() err = r.Err() if err != nil { return err } count := 0 err = r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } count++ name, namespace := info.Name, info.Namespace if !o.Local && o.dryRunStrategy != cmdutil.DryRunClient { mapping := info.ResourceMapping() client, err := o.unstructuredClientForMapping(mapping) if err != nil { return err } helper := resource. NewHelper(client, mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). WithSubresource(o.Subresource) patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil) if err != nil { if apierrors.IsUnsupportedMediaType(err) { return errors.Wrap(err, fmt.Sprintf("%s is not supported by %s", patchType, mapping.GroupVersionKind)) } return err } didPatch := !reflect.DeepEqual(info.Object, patchedObj) // if the recorder makes a change, compute and create another patch if mergePatch, err := o.Recorder.MakeRecordMergePatch(patchedObj); err != nil { klog.V(4).Infof("error recording current command: %v", err) } else if len(mergePatch) > 0 { if recordedObj, err := helper.Patch(namespace, name, types.MergePatchType, mergePatch, nil); err != nil { klog.V(4).Infof("error recording reason: %v", err) } else { patchedObj = recordedObj } } printer, err := o.ToPrinter(patchOperation(didPatch)) if err != nil { return err } return printer.PrintObj(patchedObj, o.Out) } originalObjJS, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object) if err != nil { return err } originalPatchedObjJS, err := getPatchedJSON(patchType, originalObjJS, patchBytes, info.Object.GetObjectKind().GroupVersionKind(), scheme.Scheme) if err != nil { return err } targetObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, originalPatchedObjJS) if err != nil { return err } didPatch := !reflect.DeepEqual(info.Object, targetObj) printer, err := o.ToPrinter(patchOperation(didPatch)) if err != nil { return err } return printer.PrintObj(targetObj, o.Out) }) if err != nil { return err } if count == 0 { return fmt.Errorf("no objects passed to patch") } return nil } func getPatchedJSON(patchType types.PatchType, originalJS, patchJS []byte, gvk schema.GroupVersionKind, creater runtime.ObjectCreater) ([]byte, error) { switch patchType { case types.JSONPatchType: patchObj, err := jsonpatch.DecodePatch(patchJS) if err != nil { return nil, err } bytes, err := patchObj.Apply(originalJS) // TODO: This is pretty hacky, we need a better structured error from the json-patch if err != nil && strings.Contains(err.Error(), "doc is missing key") { msg := err.Error() ix := strings.Index(msg, "key:") key := msg[ix+5:] return bytes, fmt.Errorf("Object to be patched is missing field (%s)", key) } return bytes, err case types.MergePatchType: return jsonpatch.MergePatch(originalJS, patchJS) case types.StrategicMergePatchType: // get a typed object for this GVK if we need to apply a strategic merge patch obj, err := creater.New(gvk) if err != nil { return nil, fmt.Errorf("strategic merge patch is not supported for %s locally, try --type merge", gvk.String()) } return strategicpatch.StrategicMergePatch(originalJS, patchJS, obj) default: // only here as a safety net - go-restful filters content-type return nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType) } } func patchOperation(didPatch bool) string { if didPatch { return "patched" } return "patched (no change)" } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/patch/patch_test.go000066400000000000000000000205751504711711200304040ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package patch import ( "net/http" "strings" "testing" jsonpath "github.com/exponent-io/jsonpath" corev1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestPatchObject(t *testing.T) { _, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services/frontend" && (m == "PATCH" || m == "GET"): obj := svc.Items[0] // ensure patched object reflects successful // patch edits from the client if m == "PATCH" { obj.Spec.Type = "NodePort" } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &obj)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } stream, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdPatch(tf, stream) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("patch", `{"spec":{"type":"NodePort"}}`) cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{"services/frontend"}) // uses the name from the response if buf.String() != "service/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestPatchObjectFromFile(t *testing.T) { _, svc, _ := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services/frontend" && (m == "PATCH" || m == "GET"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } stream, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdPatch(tf, stream) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("patch", `{"spec":{"type":"NodePort"}}`) cmd.Flags().Set("output", "name") cmd.Flags().Set("filename", "../../../testdata/frontend-service.yaml") cmd.Run(cmd, []string{}) // uses the name from the response if buf.String() != "service/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestPatchNoop(t *testing.T) { _, svc, _ := cmdtesting.TestData() getObject := &svc.Items[0] patchObject := &svc.Items[0] tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services/frontend" && m == "PATCH": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, patchObject)}, nil case p == "/namespaces/test/services/frontend" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getObject)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } // Patched { patchObject = patchObject.DeepCopy() if patchObject.Annotations == nil { patchObject.Annotations = map[string]string{} } patchObject.Annotations["foo"] = "bar" stream, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdPatch(tf, stream) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("patch", `{"metadata":{"annotations":{"foo":"bar"}}}`) cmd.Run(cmd, []string{"services", "frontend"}) if buf.String() != "service/baz patched\n" { t.Errorf("unexpected output: %s", buf.String()) } } } func TestPatchObjectFromFileOutput(t *testing.T) { _, svc, _ := cmdtesting.TestData() svcCopy := svc.Items[0].DeepCopy() if svcCopy.Labels == nil { svcCopy.Labels = map[string]string{} } svcCopy.Labels["post-patch"] = "post-patch-value" tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/services/frontend" && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil case p == "/namespaces/test/services/frontend" && m == "PATCH": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, svcCopy)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } stream, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdPatch(tf, stream) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("patch", `{"spec":{"type":"NodePort"}}`) cmd.Flags().Set("output", "yaml") cmd.Flags().Set("filename", "../../../testdata/frontend-service.yaml") cmd.Run(cmd, []string{}) t.Log(buf.String()) // make sure the value returned by the server is used if !strings.Contains(buf.String(), "post-patch: post-patch-value") { t.Errorf("unexpected output: %s", buf.String()) } } func TestPatchSubresource(t *testing.T) { pod := cmdtesting.SubresourceTestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() expectedStatus := corev1.PodRunning codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/pods/foo/status" && (m == "PATCH" || m == "GET"): obj := pod // ensure patched object reflects successful // patch edits from the client if m == "PATCH" { obj.Status.Phase = expectedStatus } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } stream, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdPatch(tf, stream) cmd.Flags().Set("namespace", "test") cmd.Flags().Set("patch", `{"status":{"phase":"Running"}}`) cmd.Flags().Set("output", "json") cmd.Flags().Set("subresource", "status") cmd.Run(cmd, []string{"pod/foo"}) decoder := jsonpath.NewDecoder(buf) var actualStatus corev1.PodPhase decoder.SeekTo("status", "phase") decoder.Decode(&actualStatus) // check the status.phase value is updated in the response if actualStatus != expectedStatus { t.Errorf("unexpected pod status to be set to %s got: %s", expectedStatus, actualStatus) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/000077500000000000000000000000001504711711200261055ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin.go000066400000000000000000000174451504711711200277450ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "bytes" "fmt" "os" "path/filepath" "runtime" "strings" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( pluginLong = templates.LongDesc(i18n.T(` Provides utilities for interacting with plugins. Plugins provide extended functionality that is not part of the major command-line distribution. Please refer to the documentation and examples for more information about how write your own plugins. The easiest way to discover and install plugins is via the kubernetes sub-project krew: [krew.sigs.k8s.io]. To install krew, visit https://krew.sigs.k8s.io/docs/user-guide/setup/install`)) pluginExample = templates.Examples(i18n.T(` # List all available plugins kubectl plugin list # List only binary names of available plugins without paths kubectl plugin list --name-only`)) pluginListLong = templates.LongDesc(i18n.T(` List all available plugin files on a user's PATH. To see plugins binary names without the full path use --name-only flag. Available plugin files are those that are: - executable - anywhere on the user's PATH - begin with "kubectl-" `)) ValidPluginFilenamePrefixes = []string{"kubectl"} ) func NewCmdPlugin(streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "plugin [flags]", DisableFlagsInUseLine: true, Short: i18n.T("Provides utilities for interacting with plugins"), Long: pluginLong, Example: pluginExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args) }, } cmd.AddCommand(NewCmdPluginList(streams)) return cmd } type PluginListOptions struct { Verifier PathVerifier NameOnly bool PluginPaths []string genericiooptions.IOStreams } // NewCmdPluginList provides a way to list all plugin executables visible to kubectl func NewCmdPluginList(streams genericiooptions.IOStreams) *cobra.Command { o := &PluginListOptions{ IOStreams: streams, } cmd := &cobra.Command{ Use: "list", Short: i18n.T("List all visible plugin executables on a user's PATH"), Example: pluginExample, Long: pluginListLong, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(cmd)) cmdutil.CheckErr(o.Run()) }, } cmd.Flags().BoolVar(&o.NameOnly, "name-only", o.NameOnly, "If true, display only the binary name of each plugin, rather than its full path") return cmd } func (o *PluginListOptions) Complete(cmd *cobra.Command) error { o.Verifier = &CommandOverrideVerifier{ root: cmd.Root(), seenPlugins: make(map[string]string), } o.PluginPaths = filepath.SplitList(os.Getenv("PATH")) return nil } func (o *PluginListOptions) Run() error { plugins, pluginErrors := o.ListPlugins() if len(plugins) > 0 { fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n") } else { pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH")) } pluginWarnings := 0 for _, pluginPath := range plugins { if o.NameOnly { fmt.Fprintf(o.Out, "%s\n", filepath.Base(pluginPath)) } else { fmt.Fprintf(o.Out, "%s\n", pluginPath) } if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 { for _, err := range errs { fmt.Fprintf(o.ErrOut, " - %s\n", err) pluginWarnings++ } } } if pluginWarnings > 0 { if pluginWarnings == 1 { pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warning was found")) } else { pluginErrors = append(pluginErrors, fmt.Errorf("error: %v plugin warnings were found", pluginWarnings)) } } if len(pluginErrors) > 0 { errs := bytes.NewBuffer(nil) for _, e := range pluginErrors { fmt.Fprintln(errs, e) } return fmt.Errorf("%s", errs.String()) } return nil } // ListPlugins returns list of plugin paths. func (o *PluginListOptions) ListPlugins() ([]string, []error) { plugins := []string{} errors := []error{} for _, dir := range uniquePathsList(o.PluginPaths) { if len(strings.TrimSpace(dir)) == 0 { continue } files, err := os.ReadDir(dir) if err != nil { if _, ok := err.(*os.PathError); ok { fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err) continue } errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err)) continue } for _, f := range files { if f.IsDir() { continue } if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) { continue } plugins = append(plugins, filepath.Join(dir, f.Name())) } } return plugins, errors } // pathVerifier receives a path and determines if it is valid or not type PathVerifier interface { // Verify determines if a given path is valid Verify(path string) []error } type CommandOverrideVerifier struct { root *cobra.Command seenPlugins map[string]string } // Verify implements PathVerifier and determines if a given path // is valid depending on whether or not it overwrites an existing // kubectl command path, or a previously seen plugin. func (v *CommandOverrideVerifier) Verify(path string) []error { if v.root == nil { return []error{fmt.Errorf("unable to verify path with nil root")} } // extract the plugin binary name segs := strings.Split(path, "/") binName := segs[len(segs)-1] cmdPath := strings.Split(binName, "-") if len(cmdPath) > 1 { // the first argument is always "kubectl" for a plugin binary cmdPath = cmdPath[1:] } errors := []error{} if isExec, err := isExecutable(path); err == nil && !isExec { errors = append(errors, fmt.Errorf("warning: %s identified as a kubectl plugin, but it is not executable", path)) } else if err != nil { errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err)) } if existingPath, ok := v.seenPlugins[binName]; ok { errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath)) } else { v.seenPlugins[binName] = path } if cmd, _, err := v.root.Find(cmdPath); err == nil { errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath())) } return errors } func isExecutable(fullPath string) (bool, error) { info, err := os.Stat(fullPath) if err != nil { return false, err } if runtime.GOOS == "windows" { fileExt := strings.ToLower(filepath.Ext(fullPath)) switch fileExt { case ".bat", ".cmd", ".com", ".exe", ".ps1": return true, nil } return false, nil } if m := info.Mode(); !m.IsDir() && m&0111 != 0 { return true, nil } return false, nil } // uniquePathsList deduplicates a given slice of strings without // sorting or otherwise altering its order in any way. func uniquePathsList(paths []string) []string { seen := map[string]bool{} newPaths := []string{} for _, p := range paths { if seen[p] { continue } seen[p] = true newPaths = append(newPaths, p) } return newPaths } func hasValidPrefix(filepath string, validPrefixes []string) bool { for _, prefix := range validPrefixes { if !strings.HasPrefix(filepath, prefix+"-") { continue } return true } return false } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go000066400000000000000000000222331504711711200321650ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "bytes" "fmt" "io" "os" "os/exec" "path/filepath" "strconv" "strings" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) func GetPluginCommandGroup(kubectl *cobra.Command) templates.CommandGroup { // Find root level return templates.CommandGroup{ Message: i18n.T("Subcommands provided by plugins:"), Commands: registerPluginCommands(kubectl, false), } } // SetupPluginCompletion adds a Cobra command to the command tree for each // plugin. This is only done when performing shell completion that relate // to plugins. func SetupPluginCompletion(cmd *cobra.Command, args []string) { kubectl := cmd.Root() if len(args) > 0 { if strings.HasPrefix(args[0], "-") { // Plugins are not supported if the first argument is a flag, // so no need to add them in that case. return } if len(args) == 1 { // We are completing a subcommand at the first level so // we should include all plugins names. registerPluginCommands(kubectl, true) return } // We have more than one argument. // Check if we know the first level subcommand. // If we don't it could be a plugin and we'll need to add // the plugin commands for completion to work. found := false for _, subCmd := range kubectl.Commands() { if args[0] == subCmd.Name() { found = true break } } if !found { // We don't know the subcommand for which completion // is being called: it could be a plugin. // // When using a plugin, the kubectl global flags are not supported. // Therefore, when doing completion, we need to remove these flags // to avoid them being included in the completion choices. // This must be done *before* adding the plugin commands so that // when creating those plugin commands, the flags don't exist. kubectl.ResetFlags() cobra.CompDebugln("Cleared global flags for plugin completion", true) registerPluginCommands(kubectl, true) } } } // registerPluginCommand allows adding Cobra command to the command tree or extracting them for usage in // e.g. the help function or for registering the completion function func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Command) { userDefinedCommands := []*cobra.Command{} streams := genericclioptions.IOStreams{ In: &bytes.Buffer{}, Out: io.Discard, ErrOut: io.Discard, } o := &PluginListOptions{IOStreams: streams} o.Complete(kubectl) plugins, _ := o.ListPlugins() for _, plugin := range plugins { plugin = filepath.Base(plugin) args := []string{} // Plugins are named "kubectl-" or with more - such as // "kubectl--..." rawPluginArgs := strings.Split(plugin, "-")[1:] pluginArgs := rawPluginArgs[:1] if list { pluginArgs = rawPluginArgs } // Iterate through all segments, for kubectl-my_plugin-sub_cmd, we will end up with // two iterations: one for my_plugin and one for sub_cmd. for _, arg := range pluginArgs { // Underscores (_) in plugin's filename are replaced with dashes(-) // e.g. foo_bar -> foo-bar args = append(args, strings.ReplaceAll(arg, "_", "-")) } // In order to avoid that the same plugin command is added more than once, // find the lowest command given args from the root command parentCmd, remainingArgs, _ := kubectl.Find(args) if parentCmd == nil { parentCmd = kubectl } for _, remainingArg := range remainingArgs { cmd := &cobra.Command{ Use: remainingArg, // Add a description that will be shown with completion choices. // Make each one different by including the plugin name to avoid // all plugins being grouped in a single line during completion for zsh. Short: fmt.Sprintf(i18n.T("The command %s is a plugin installed by the user"), remainingArg), DisableFlagParsing: true, // Allow plugins to provide their own completion choices ValidArgsFunction: pluginCompletion, // A Run is required for it to be a valid command Run: func(cmd *cobra.Command, args []string) {}, } // Add the plugin command to the list of user defined commands userDefinedCommands = append(userDefinedCommands, cmd) if list { parentCmd.AddCommand(cmd) parentCmd = cmd } } } return userDefinedCommands } // pluginCompletion deals with shell completion beyond the plugin name, it allows to complete // plugin arguments and flags. // It will look on $PATH for a specific executable file that will provide completions // for the plugin in question. // // When called, this completion executable should print the completion choices to stdout. // The arguments passed to the executable file will be the arguments for the plugin currently // on the command-line. For example, if a user types: // // kubectl myplugin arg1 arg2 a // // the completion executable will be called with arguments: "arg1" "arg2" "a". // And if a user types: // // kubectl myplugin arg1 arg2 // // the completion executable will be called with arguments: "arg1" "arg2" "". Notice the empty // last argument which indicates that a new word should be completed but that the user has not // typed anything for it yet. // // Kubectl's plugin completion logic supports Cobra's ShellCompDirective system. This means a plugin // can optionally print : as its very last line to provide // directives to the shell on how to perform completion. If this directive is not present, the // cobra.ShellCompDirectiveDefault will be used. Please see Cobra's documentation for more details: // https://github.com/spf13/cobra/blob/master/shell_completions.md#dynamic-completion-of-nouns // // The completion executable should be named kubectl_complete-. For example, for a plugin // named kubectl-get_all, the completion file should be named kubectl_complete-get_all. The completion // executable must have executable permissions set on it and must be on $PATH. func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // Recreate the plugin name from the commandPath pluginName := strings.ReplaceAll(strings.ReplaceAll(cmd.CommandPath(), "-", "_"), " ", "-") path, found := lookupCompletionExec(pluginName) if !found { cobra.CompDebugln(fmt.Sprintf("Plugin %s does not provide a matching completion executable", pluginName), true) return nil, cobra.ShellCompDirectiveDefault } args = append(args, toComplete) cobra.CompDebugln(fmt.Sprintf("About to call: %s %s", path, strings.Join(args, " ")), true) return getPluginCompletions(path, args, os.Environ()) } // lookupCompletionExec will look for the existence of an executable // that can provide completion for the given plugin name. // The first filepath to match is returned, or a boolean false if // such an executable is not found. func lookupCompletionExec(pluginName string) (string, bool) { // Convert the plugin name into the plugin completion name by inserting "_complete" before the first -. // For example, convert kubectl-get_all to kubectl_complete-get_all pluginCompExec := strings.Replace(pluginName, "-", "_complete-", 1) cobra.CompDebugln(fmt.Sprintf("About to look for: %s", pluginCompExec), true) path, err := exec.LookPath(pluginCompExec) if err != nil || len(path) == 0 { return "", false } return path, true } // getPluginCompletions receives an executable's filepath, a slice // of arguments, and a slice of environment variables // to relay to the executable. // The executable is responsible for printing the completions of the // plugin for the current set of arguments. func getPluginCompletions(executablePath string, cmdArgs, environment []string) ([]string, cobra.ShellCompDirective) { buf := new(bytes.Buffer) prog := exec.Command(executablePath, cmdArgs...) prog.Stdin = os.Stdin prog.Stdout = buf prog.Stderr = os.Stderr prog.Env = environment var comps []string directive := cobra.ShellCompDirectiveDefault if err := prog.Run(); err == nil { for _, comp := range strings.Split(buf.String(), "\n") { // Remove any empty lines if len(comp) > 0 { comps = append(comps, comp) } } // Check if the last line of output is of the form :, which // indicates a Cobra ShellCompDirective. We do this for plugins // that use Cobra or the ones that wish to use this directive to // communicate a special behavior for the shell. if len(comps) > 0 { lastLine := comps[len(comps)-1] if len(lastLine) > 1 && lastLine[0] == ':' { if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { directive = cobra.ShellCompDirective(strInt) comps = comps[:len(comps)-1] } } } } return comps, directive } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go000066400000000000000000000156771504711711200310110ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "fmt" "os" "path/filepath" "reflect" "strings" "testing" "k8s.io/cli-runtime/pkg/genericiooptions" ) func TestPluginPathsAreUnaltered(t *testing.T) { tempDir, err := os.MkdirTemp(os.TempDir(), "test-cmd-plugins") if err != nil { t.Fatalf("unexpected error: %v", err) } tempDir2, err := os.MkdirTemp(os.TempDir(), "test-cmd-plugins2") if err != nil { t.Fatalf("unexpected error: %v", err) } // cleanup defer func() { if err := os.RemoveAll(tempDir); err != nil { panic(fmt.Errorf("unexpected cleanup error: %v", err)) } if err := os.RemoveAll(tempDir2); err != nil { panic(fmt.Errorf("unexpected cleanup error: %v", err)) } }() ioStreams, _, _, errOut := genericiooptions.NewTestIOStreams() verifier := newFakePluginPathVerifier() pluginPaths := []string{tempDir, tempDir2} o := &PluginListOptions{ Verifier: verifier, IOStreams: ioStreams, PluginPaths: pluginPaths, } // write at least one valid plugin file if _, err := os.CreateTemp(tempDir, "kubectl-"); err != nil { t.Fatalf("unexpected error %v", err) } if _, err := os.CreateTemp(tempDir2, "kubectl-"); err != nil { t.Fatalf("unexpected error %v", err) } if err := o.Run(); err != nil { t.Fatalf("unexpected error %v - %v", err, errOut.String()) } // ensure original paths remain unaltered if len(verifier.seenUnsorted) != len(pluginPaths) { t.Fatalf("saw unexpected plugin paths. Expecting %v, got %v", pluginPaths, verifier.seenUnsorted) } for actual := range verifier.seenUnsorted { if !strings.HasPrefix(verifier.seenUnsorted[actual], pluginPaths[actual]) { t.Fatalf("expected PATH slice to be unaltered. Expecting %v, but got %v", pluginPaths[actual], verifier.seenUnsorted[actual]) } } } func TestPluginPathsAreValid(t *testing.T) { tempDir, err := os.MkdirTemp(os.TempDir(), "test-cmd-plugins") if err != nil { t.Fatalf("unexpected error: %v", err) } // cleanup defer func() { if err := os.RemoveAll(tempDir); err != nil { panic(fmt.Errorf("unexpected cleanup error: %v", err)) } }() tc := []struct { name string pluginPaths []string pluginFile func() (*os.File, error) verifier *fakePluginPathVerifier expectVerifyErrors []error expectErr string expectErrOut string expectOut string }{ { name: "ensure no plugins found if no files begin with kubectl- prefix", pluginPaths: []string{tempDir}, verifier: newFakePluginPathVerifier(), pluginFile: func() (*os.File, error) { return os.CreateTemp(tempDir, "notkubectl-") }, expectErr: "error: unable to find any kubectl plugins in your PATH\n", }, { name: "ensure de-duplicated plugin-paths slice", pluginPaths: []string{tempDir, tempDir}, verifier: newFakePluginPathVerifier(), pluginFile: func() (*os.File, error) { return os.CreateTemp(tempDir, "kubectl-") }, expectOut: "The following compatible plugins are available:", }, { name: "ensure no errors when empty string or blank path are specified", pluginPaths: []string{tempDir, "", " "}, verifier: newFakePluginPathVerifier(), pluginFile: func() (*os.File, error) { return os.CreateTemp(tempDir, "kubectl-") }, expectOut: "The following compatible plugins are available:", }, } for _, test := range tc { t.Run(test.name, func(t *testing.T) { ioStreams, _, out, errOut := genericiooptions.NewTestIOStreams() o := &PluginListOptions{ Verifier: test.verifier, IOStreams: ioStreams, PluginPaths: test.pluginPaths, } // create files if test.pluginFile != nil { if _, err := test.pluginFile(); err != nil { t.Fatalf("unexpected error creating plugin file: %v", err) } } for _, expected := range test.expectVerifyErrors { for _, actual := range test.verifier.errors { if expected != actual { t.Fatalf("unexpected error: expected %v, but got %v", expected, actual) } } } err := o.Run() if err == nil && len(test.expectErr) > 0 { t.Fatalf("unexpected non-error: expected %v, but got nothing", test.expectErr) } else if err != nil && len(test.expectErr) == 0 { t.Fatalf("unexpected error: expected nothing, but got %v", err.Error()) } else if err != nil && err.Error() != test.expectErr { t.Fatalf("unexpected error: expected %v, but got %v", test.expectErr, err.Error()) } if len(test.expectErrOut) == 0 && errOut.Len() > 0 { t.Fatalf("unexpected error output: expected nothing, but got %v", errOut.String()) } else if len(test.expectErrOut) > 0 && !strings.Contains(errOut.String(), test.expectErrOut) { t.Fatalf("unexpected error output: expected to contain %v, but got %v", test.expectErrOut, errOut.String()) } if len(test.expectOut) == 0 && out.Len() > 0 { t.Fatalf("unexpected output: expected nothing, but got %v", out.String()) } else if len(test.expectOut) > 0 && !strings.Contains(out.String(), test.expectOut) { t.Fatalf("unexpected output: expected to contain %v, but got %v", test.expectOut, out.String()) } }) } } func TestListPlugins(t *testing.T) { pluginPath, _ := filepath.Abs("./testdata") expectPlugins := []string{ filepath.Join(pluginPath, "kubectl-create-foo"), filepath.Join(pluginPath, "kubectl-foo"), filepath.Join(pluginPath, "kubectl-version"), } verifier := newFakePluginPathVerifier() ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() pluginPaths := []string{pluginPath} o := &PluginListOptions{ Verifier: verifier, IOStreams: ioStreams, PluginPaths: pluginPaths, } plugins, errs := o.ListPlugins() if len(errs) > 0 { t.Fatalf("unexpected errors: %v", errs) } if !reflect.DeepEqual(expectPlugins, plugins) { t.Fatalf("saw unexpected plugins. Expecting %v, got %v", expectPlugins, plugins) } } type duplicatePathError struct { path string } func (d *duplicatePathError) Error() string { return fmt.Sprintf("path %q already visited", d.path) } type fakePluginPathVerifier struct { errors []error seen map[string]bool seenUnsorted []string } func (f *fakePluginPathVerifier) Verify(path string) []error { if f.seen[path] { err := &duplicatePathError{path} f.errors = append(f.errors, err) return []error{err} } f.seen[path] = true f.seenUnsorted = append(f.seenUnsorted, path) return nil } func newFakePluginPathVerifier() *fakePluginPathVerifier { return &fakePluginPathVerifier{seen: make(map[string]bool)} } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/000077500000000000000000000000001504711711200277165ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-create-foo000066400000000000000000000000521504711711200333110ustar00rootroot00000000000000#!/bin/bash echo "I am plugin create foo"kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-foo000077500000000000000000000000441504711711200320540ustar00rootroot00000000000000#!/bin/bash echo "I am plugin foo" kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-version000077500000000000000000000001561504711711200327620ustar00rootroot00000000000000#!/bin/bash # This plugin is a no-op and is used to test plugins # that overshadow existing kubectl commands kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/portforward/000077500000000000000000000000001504711711200271605ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/portforward/portforward.go000066400000000000000000000335611504711711200320700ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package portforward import ( "context" "fmt" "net/http" "net/url" "os" "os/signal" "strconv" "strings" "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/httpstream" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/kubernetes/scheme" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/portforward" "k8s.io/client-go/transport/spdy" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // PortForwardOptions contains all the options for running the port-forward cli command. type PortForwardOptions struct { Namespace string PodName string RESTClient restclient.Interface Config *restclient.Config PodClient corev1client.PodsGetter Address []string Ports []string PortForwarder portForwarder StopChannel chan struct{} ReadyChannel chan struct{} } var ( portforwardLong = templates.LongDesc(i18n.T(` Forward one or more local ports to a pod. Use resource type/name such as deployment/mydeployment to select a pod. Resource type defaults to 'pod' if omitted. If there are multiple pods matching the criteria, a pod will be selected automatically. The forwarding session ends when the selected pod terminates, and a rerun of the command is needed to resume forwarding.`)) portforwardExample = templates.Examples(i18n.T(` # Listen on ports 5000 and 6000 locally, forwarding data to/from ports 5000 and 6000 in the pod kubectl port-forward pod/mypod 5000 6000 # Listen on ports 5000 and 6000 locally, forwarding data to/from ports 5000 and 6000 in a pod selected by the deployment kubectl port-forward deployment/mydeployment 5000 6000 # Listen on port 8443 locally, forwarding to the targetPort of the service's port named "https" in a pod selected by the service kubectl port-forward service/myservice 8443:https # Listen on port 8888 locally, forwarding to 5000 in the pod kubectl port-forward pod/mypod 8888:5000 # Listen on port 8888 on all addresses, forwarding to 5000 in the pod kubectl port-forward --address 0.0.0.0 pod/mypod 8888:5000 # Listen on port 8888 on localhost and selected IP, forwarding to 5000 in the pod kubectl port-forward --address localhost,10.19.21.23 pod/mypod 8888:5000 # Listen on a random port locally, forwarding to 5000 in the pod kubectl port-forward pod/mypod :5000`)) ) const ( // Amount of time to wait until at least one pod is running defaultPodPortForwardWaitTimeout = 60 * time.Second ) func NewCmdPortForward(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { opts := NewDefaultPortForwardOptions(streams) cmd := &cobra.Command{ Use: "port-forward TYPE/NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]", DisableFlagsInUseLine: true, Short: i18n.T("Forward one or more local ports to a pod"), Long: portforwardLong, Example: portforwardExample, ValidArgsFunction: completion.ResourceAndPortCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(opts.Complete(f, cmd, args)) cmdutil.CheckErr(opts.Validate()) cmdutil.CheckErr(opts.RunPortForward()) }, } cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodPortForwardWaitTimeout) cmd.Flags().StringSliceVar(&opts.Address, "address", []string{"localhost"}, "Addresses to listen on (comma separated). Only accepts IP addresses or localhost as a value. When localhost is supplied, kubectl will try to bind on both 127.0.0.1 and ::1 and will fail if neither of these addresses are available to bind.") // TODO support UID return cmd } func NewDefaultPortForwardOptions(streams genericiooptions.IOStreams) *PortForwardOptions { return &PortForwardOptions{ PortForwarder: &defaultPortForwarder{ IOStreams: streams, }, } } type portForwarder interface { ForwardPorts(method string, url *url.URL, opts PortForwardOptions) error } type defaultPortForwarder struct { genericiooptions.IOStreams } func createDialer(method string, url *url.URL, opts PortForwardOptions) (httpstream.Dialer, error) { transport, upgrader, err := spdy.RoundTripperFor(opts.Config) if err != nil { return nil, err } dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) if !cmdutil.PortForwardWebsockets.IsDisabled() { tunnelingDialer, err := portforward.NewSPDYOverWebsocketDialer(url, opts.Config) if err != nil { return nil, err } // First attempt tunneling (websocket) dialer, then fallback to spdy dialer. dialer = portforward.NewFallbackDialer(tunnelingDialer, dialer, func(err error) bool { return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err) }) } return dialer, nil } func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts PortForwardOptions) error { dialer, err := createDialer(method, url, opts) if err != nil { return err } fw, err := portforward.NewOnAddresses(dialer, opts.Address, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut) if err != nil { return err } return fw.ForwardPorts() } // splitPort splits port string which is in form of [LOCAL PORT]:REMOTE PORT // and returns local and remote ports separately func splitPort(port string) (local, remote string) { parts := strings.Split(port, ":") if len(parts) == 2 { return parts[0], parts[1] } return parts[0], parts[0] } // Translates service port to target port // It rewrites ports as needed if the Service port declares targetPort. // It returns an error when a named targetPort can't find a match in the pod, or the Service did not declare // the port. func translateServicePortToTargetPort(ports []string, svc corev1.Service, pod corev1.Pod) ([]string, error) { var translated []string for _, port := range ports { localPort, remotePort := splitPort(port) portnum, err := strconv.Atoi(remotePort) if err != nil { svcPort, err := util.LookupServicePortNumberByName(svc, remotePort) if err != nil { return nil, err } portnum = int(svcPort) if localPort == remotePort { localPort = strconv.Itoa(portnum) } } containerPort, err := util.LookupContainerPortNumberByServicePort(svc, pod, int32(portnum)) if err != nil { // can't resolve a named port, or Service did not declare this port, return an error return nil, err } // convert the resolved target port back to a string remotePort = strconv.Itoa(int(containerPort)) if localPort != remotePort { translated = append(translated, fmt.Sprintf("%s:%s", localPort, remotePort)) } else { translated = append(translated, remotePort) } } return translated, nil } // convertPodNamedPortToNumber converts named ports into port numbers // It returns an error when a named port can't be found in the pod containers func convertPodNamedPortToNumber(ports []string, pod corev1.Pod) ([]string, error) { var converted []string for _, port := range ports { localPort, remotePort := splitPort(port) if remotePort == "" { return nil, fmt.Errorf("remote port cannot be empty") } containerPortStr := remotePort _, err := strconv.Atoi(remotePort) if err != nil { containerPort, err := util.LookupContainerPortNumberByName(pod, remotePort) if err != nil { return nil, err } containerPortStr = strconv.Itoa(int(containerPort)) } if localPort != remotePort { converted = append(converted, fmt.Sprintf("%s:%s", localPort, containerPortStr)) } else { converted = append(converted, containerPortStr) } } return converted, nil } func checkUDPPorts(udpOnlyPorts sets.Set[int], ports []string, obj metav1.Object) error { for _, port := range ports { _, remotePort := splitPort(port) portNum, err := strconv.Atoi(remotePort) if err != nil { switch v := obj.(type) { case *corev1.Service: svcPort, err := util.LookupServicePortNumberByName(*v, remotePort) if err != nil { return err } portNum = int(svcPort) case *corev1.Pod: ctPort, err := util.LookupContainerPortNumberByName(*v, remotePort) if err != nil { return err } portNum = int(ctPort) default: return fmt.Errorf("unknown object: %v", obj) } } if udpOnlyPorts.Has(portNum) { return fmt.Errorf("UDP protocol is not supported for %s", remotePort) } } return nil } // checkUDPPortInService returns an error if remote port in Service is a UDP port // TODO: remove this check after #47862 is solved func checkUDPPortInService(ports []string, svc *corev1.Service) error { udpPorts := sets.New[int]() tcpPorts := sets.New[int]() for _, port := range svc.Spec.Ports { portNum := int(port.Port) switch port.Protocol { case corev1.ProtocolUDP: udpPorts.Insert(portNum) case corev1.ProtocolTCP: tcpPorts.Insert(portNum) } } return checkUDPPorts(udpPorts.Difference(tcpPorts), ports, svc) } // checkUDPPortInPod returns an error if remote port in Pod is a UDP port // TODO: remove this check after #47862 is solved func checkUDPPortInPod(ports []string, pod *corev1.Pod) error { udpPorts := sets.New[int]() tcpPorts := sets.New[int]() for _, ct := range pod.Spec.Containers { for _, ctPort := range ct.Ports { portNum := int(ctPort.ContainerPort) switch ctPort.Protocol { case corev1.ProtocolUDP: udpPorts.Insert(portNum) case corev1.ProtocolTCP: tcpPorts.Insert(portNum) } } } return checkUDPPorts(udpPorts.Difference(tcpPorts), ports, pod) } // Complete completes all the required options for port-forward cmd. func (o *PortForwardOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error if len(args) < 2 { return cmdutil.UsageErrorf(cmd, "TYPE/NAME and list of ports are required for port-forward") } o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace() getPodTimeout, err := cmdutil.GetPodRunningTimeoutFlag(cmd) if err != nil { return cmdutil.UsageErrorf(cmd, "%s", err.Error()) } resourceName := args[0] builder.ResourceNames("pods", resourceName) obj, err := builder.Do().Object() if err != nil { return err } forwardablePod, err := polymorphichelpers.AttachablePodForObjectFn(f, obj, getPodTimeout) if err != nil { return err } o.PodName = forwardablePod.Name // handle service port mapping to target port if needed switch t := obj.(type) { case *corev1.Service: err = checkUDPPortInService(args[1:], t) if err != nil { return err } o.Ports, err = translateServicePortToTargetPort(args[1:], *t, *forwardablePod) if err != nil { return err } default: err = checkUDPPortInPod(args[1:], forwardablePod) if err != nil { return err } o.Ports, err = convertPodNamedPortToNumber(args[1:], *forwardablePod) if err != nil { return err } } clientset, err := f.KubernetesClientSet() if err != nil { return err } o.PodClient = clientset.CoreV1() o.Config, err = f.ToRESTConfig() if err != nil { return err } o.RESTClient, err = f.RESTClient() if err != nil { return err } o.StopChannel = make(chan struct{}, 1) o.ReadyChannel = make(chan struct{}) return nil } // Validate validates all the required options for port-forward cmd. func (o PortForwardOptions) Validate() error { if len(o.PodName) == 0 { return fmt.Errorf("pod name or resource type/name must be specified") } if len(o.Ports) < 1 { return fmt.Errorf("at least 1 PORT is required for port-forward") } if o.PortForwarder == nil || o.PodClient == nil || o.RESTClient == nil || o.Config == nil { return fmt.Errorf("client, client config, restClient, and portforwarder must be provided") } return nil } // Deprecated: Use RunPortForwardContext instead, which allows canceling. // RunPortForward implements all the necessary functionality for port-forward cmd. func (o PortForwardOptions) RunPortForward() error { return o.RunPortForwardContext(context.Background()) } // RunPortForwardContext implements all the necessary functionality for port-forward cmd. // It ends portforwarding when an error is received from the backend, or an os.Interrupt // signal is received, or the provided context is done. func (o PortForwardOptions) RunPortForwardContext(ctx context.Context) error { pod, err := o.PodClient.Pods(o.Namespace).Get(ctx, o.PodName, metav1.GetOptions{}) if err != nil { return err } if pod.Status.Phase != corev1.PodRunning { return fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) } signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt) defer signal.Stop(signals) returnCtx, returnCtxCancel := context.WithCancel(ctx) defer returnCtxCancel() go func() { select { case <-signals: case <-returnCtx.Done(): } if o.StopChannel != nil { close(o.StopChannel) } }() req := o.RESTClient.Post(). Resource("pods"). Namespace(o.Namespace). Name(pod.Name). SubResource("portforward") return o.PortForwarder.ForwardPorts("POST", req.URL(), o) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/portforward/portforward_test.go000066400000000000000000000557021504711711200331300ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package portforward import ( "context" "fmt" "net/http" "net/url" "reflect" "testing" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" "k8s.io/client-go/tools/portforward" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" ) type fakePortForwarder struct { method string url *url.URL pfErr error } func (f *fakePortForwarder) ForwardPorts(method string, url *url.URL, opts PortForwardOptions) error { f.method = method f.url = url return f.pfErr } func testPortForward(t *testing.T, flags map[string]string, args []string) { version := "v1" tests := []struct { name string podPath, pfPath string pod *corev1.Pod pfErr bool }{ { name: "pod portforward", podPath: "/api/" + version + "/namespaces/test/pods/foo", pfPath: "/api/" + version + "/namespaces/test/pods/foo/portforward", pod: execPod(), }, { name: "pod portforward error", podPath: "/api/" + version + "/namespaces/test/pods/foo", pfPath: "/api/" + version + "/namespaces/test/pods/foo/portforward", pod: execPod(), pfErr: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var err error tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ VersionedAPIPath: "/api/v1", GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == test.podPath && m == "GET": body := cmdtesting.ObjBody(codec, test.pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Errorf("%s: unexpected request: %#v\n%#v", test.name, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() ff := &fakePortForwarder{} if test.pfErr { ff.pfErr = fmt.Errorf("pf error") } opts := &PortForwardOptions{} ctx, cancel := context.WithCancel(context.Background()) defer cancel() cmd := NewCmdPortForward(tf, genericiooptions.NewTestIOStreamsDiscard()) cmd.Run = func(cmd *cobra.Command, args []string) { if err = opts.Complete(tf, cmd, args); err != nil { return } opts.PortForwarder = ff if err = opts.Validate(); err != nil { return } err = opts.RunPortForwardContext(ctx) } for name, value := range flags { cmd.Flags().Set(name, value) } cmd.Run(cmd, args) if test.pfErr && err != ff.pfErr { t.Errorf("%s: Unexpected port-forward error: %v", test.name, err) } if !test.pfErr && err != nil { t.Errorf("%s: Unexpected error: %v", test.name, err) } if test.pfErr { return } if ff.url == nil || ff.url.Path != test.pfPath { t.Errorf("%s: Did not get expected path for portforward request", test.name) } if ff.method != "POST" { t.Errorf("%s: Did not get method for attach request: %s", test.name, ff.method) } }) } } func TestPortForward(t *testing.T) { testPortForward(t, nil, []string{"foo", ":5000", ":1000"}) } func TestTranslateServicePortToTargetPort(t *testing.T) { cases := []struct { name string svc corev1.Service pod corev1.Pod ports []string translated []string err bool }{ { name: "test success 1 (int port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromInt32(8080), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, }, }, }, }, }, ports: []string{"80"}, translated: []string{"80:8080"}, err: false, }, { name: "test success 1 (int port with random local port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromInt32(8080), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, }, }, }, }, }, ports: []string{":80"}, translated: []string{":8080"}, err: false, }, { name: "test success 1 (int port with explicit local port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 8080, TargetPort: intstr.FromInt32(8080), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, }, }, }, }, }, ports: []string{"8000:8080"}, translated: []string{"8000:8080"}, err: false, }, { name: "test success 2 (clusterIP: None)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ ClusterIP: "None", Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromInt32(8080), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, }, }, }, }, }, ports: []string{"80"}, translated: []string{"80"}, err: false, }, { name: "test success 2 (clusterIP: None with random local port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ ClusterIP: "None", Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromInt32(8080), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, }, }, }, }, }, ports: []string{":80"}, translated: []string{":80"}, err: false, }, { name: "test success 3 (named target port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromString("http"), }, { Port: 443, TargetPort: intstr.FromString("https"), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, { Name: "https", ContainerPort: int32(8443)}, }, }, }, }, }, ports: []string{"80", "443"}, translated: []string{"80:8080", "443:8443"}, err: false, }, { name: "test success 3 (named target port with random local port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromString("http"), }, { Port: 443, TargetPort: intstr.FromString("https"), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, { Name: "https", ContainerPort: int32(8443)}, }, }, }, }, }, ports: []string{":80", ":443"}, translated: []string{":8080", ":8443"}, err: false, }, { name: "test success 4 (named service port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, Name: "http", TargetPort: intstr.FromInt32(8080), }, { Port: 443, Name: "https", TargetPort: intstr.FromInt32(8443), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { ContainerPort: int32(8080)}, { ContainerPort: int32(8443)}, }, }, }, }, }, ports: []string{"http", "https"}, translated: []string{"80:8080", "443:8443"}, err: false, }, { name: "test success 4 (named service port with random local port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, Name: "http", TargetPort: intstr.FromInt32(8080), }, { Port: 443, Name: "https", TargetPort: intstr.FromInt32(8443), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { ContainerPort: int32(8080)}, { ContainerPort: int32(8443)}, }, }, }, }, }, ports: []string{":http", ":https"}, translated: []string{":8080", ":8443"}, err: false, }, { name: "test success 4 (named service port and named pod container port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, Name: "http", TargetPort: intstr.FromString("http"), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{"http"}, translated: []string{"80"}, err: false, }, { name: "test success (targetPort omitted)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{"80"}, translated: []string{"80"}, err: false, }, { name: "test success (targetPort omitted with random local port)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{":80"}, translated: []string{":80"}, err: false, }, { name: "test failure 1 (named target port lookup failure)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromString("http"), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "https", ContainerPort: int32(443)}, }, }, }, }, }, ports: []string{"80"}, translated: []string{}, err: true, }, { name: "test failure 1 (named service port lookup failure)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromString("http"), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(8080)}, }, }, }, }, }, ports: []string{"https"}, translated: []string{}, err: true, }, { name: "test failure 2 (service port not declared)", svc: corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, TargetPort: intstr.FromString("http"), }, }, }, }, pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "https", ContainerPort: int32(443)}, }, }, }, }, }, ports: []string{"443"}, translated: []string{}, err: true, }, } for _, tc := range cases { translated, err := translateServicePortToTargetPort(tc.ports, tc.svc, tc.pod) if err != nil { if tc.err { continue } t.Errorf("%v: unexpected error: %v", tc.name, err) continue } if tc.err { t.Errorf("%v: unexpected success", tc.name) continue } if !reflect.DeepEqual(translated, tc.translated) { t.Errorf("%v: expected %v; got %v", tc.name, tc.translated, translated) } } } func execPod() *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, Containers: []corev1.Container{ { Name: "bar", }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, } } func TestConvertPodNamedPortToNumber(t *testing.T) { cases := []struct { name string pod corev1.Pod ports []string converted []string err bool }{ { name: "port number without local port", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{"80"}, converted: []string{"80"}, err: false, }, { name: "port number with local port", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{"8000:80"}, converted: []string{"8000:80"}, err: false, }, { name: "port number with random local port", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{":80"}, converted: []string{":80"}, err: false, }, { name: "named port without local port", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{"http"}, converted: []string{"80"}, err: false, }, { name: "named port with local port", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{"8000:http"}, converted: []string{"8000:80"}, err: false, }, { name: "named port with random local port", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "http", ContainerPort: int32(80)}, }, }, }, }, }, ports: []string{":http"}, converted: []string{":80"}, err: false, }, { name: "named port can not be found", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "https", ContainerPort: int32(443)}, }, }, }, }, }, ports: []string{"http"}, err: true, }, { name: "one of the requested named ports can not be found", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { Name: "https", ContainerPort: int32(443)}, }, }, }, }, }, ports: []string{"https", "http"}, err: true, }, { name: "empty port name", pod: corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ { ContainerPort: int32(27017)}, }, }, }, }, }, ports: []string{"28015:"}, converted: nil, err: true, }, } for _, tc := range cases { converted, err := convertPodNamedPortToNumber(tc.ports, tc.pod) if err != nil { if tc.err { continue } t.Errorf("%v: unexpected error: %v", tc.name, err) continue } if tc.err { t.Errorf("%v: unexpected success", tc.name) continue } if !reflect.DeepEqual(converted, tc.converted) { t.Errorf("%v: expected %v; got %v", tc.name, tc.converted, converted) } } } func TestCheckUDPPort(t *testing.T) { tests := []struct { name string pod *corev1.Pod service *corev1.Service ports []string expectError bool }{ { name: "forward to a UDP port in a Pod", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ {Protocol: corev1.ProtocolUDP, ContainerPort: 53}, }, }, }, }, }, ports: []string{"53"}, expectError: true, }, { name: "forward to a named UDP port in a Pod", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ {Protocol: corev1.ProtocolUDP, ContainerPort: 53, Name: "dns"}, }, }, }, }, }, ports: []string{"dns"}, expectError: true, }, { name: "Pod has ports with both TCP and UDP protocol (UDP first)", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ {Protocol: corev1.ProtocolUDP, ContainerPort: 53}, {Protocol: corev1.ProtocolTCP, ContainerPort: 53}, }, }, }, }, }, ports: []string{":53"}, }, { name: "Pod has ports with both TCP and UDP protocol (TCP first)", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Ports: []corev1.ContainerPort{ {Protocol: corev1.ProtocolTCP, ContainerPort: 53}, {Protocol: corev1.ProtocolUDP, ContainerPort: 53}, }, }, }, }, }, ports: []string{":53"}, }, { name: "forward to a UDP port in a Service", service: &corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ {Protocol: corev1.ProtocolUDP, Port: 53}, }, }, }, ports: []string{"53"}, expectError: true, }, { name: "forward to a named UDP port in a Service", service: &corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ {Protocol: corev1.ProtocolUDP, Port: 53, Name: "dns"}, }, }, }, ports: []string{"10053:dns"}, expectError: true, }, { name: "Service has ports with both TCP and UDP protocol (UDP first)", service: &corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ {Protocol: corev1.ProtocolUDP, Port: 53}, {Protocol: corev1.ProtocolTCP, Port: 53}, }, }, }, ports: []string{"53"}, }, { name: "Service has ports with both TCP and UDP protocol (TCP first)", service: &corev1.Service{ Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ {Protocol: corev1.ProtocolTCP, Port: 53}, {Protocol: corev1.ProtocolUDP, Port: 53}, }, }, }, ports: []string{"53"}, }, } for _, tc := range tests { var err error if tc.pod != nil { err = checkUDPPortInPod(tc.ports, tc.pod) } else if tc.service != nil { err = checkUDPPortInService(tc.ports, tc.service) } if err != nil { if tc.expectError { continue } t.Errorf("%v: unexpected error: %v", tc.name, err) continue } if tc.expectError { t.Errorf("%v: unexpected success", tc.name) } } } func TestCreateDialer(t *testing.T) { url, err := url.Parse("http://localhost:8080/index.html") if err != nil { t.Fatalf("unable to parse test url: %v", err) } config := cmdtesting.DefaultClientConfig() opts := PortForwardOptions{Config: config} // First, ensure that no environment variable creates the fallback dialer. dialer, err := createDialer("GET", url, opts) if err != nil { t.Fatalf("unable to create dialer: %v", err) } if _, isFallback := dialer.(*portforward.FallbackDialer); !isFallback { t.Errorf("expected fallback dialer, got %#v", dialer) } // Next, check turning on feature flag explicitly also creates fallback dialer. t.Setenv(string(cmdutil.PortForwardWebsockets), "true") dialer, err = createDialer("GET", url, opts) if err != nil { t.Fatalf("unable to create dialer: %v", err) } if _, isFallback := dialer.(*portforward.FallbackDialer); !isFallback { t.Errorf("expected fallback dialer, got %#v", dialer) } // Finally, check explicit disabling does NOT create the fallback dialer. t.Setenv(string(cmdutil.PortForwardWebsockets), "false") dialer, err = createDialer("GET", url, opts) if err != nil { t.Fatalf("unable to create dialer: %v", err) } if _, isFallback := dialer.(*portforward.FallbackDialer); isFallback { t.Errorf("expected fallback dialer, got %#v", dialer) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/profiling.go000066400000000000000000000044171504711711200271350ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "fmt" "os" "os/signal" "runtime" "runtime/pprof" "github.com/spf13/pflag" ) var ( profileName string profileOutput string ) func addProfilingFlags(flags *pflag.FlagSet) { flags.StringVar(&profileName, "profile", "none", "Name of profile to capture. One of (none|cpu|heap|goroutine|threadcreate|block|mutex)") flags.StringVar(&profileOutput, "profile-output", "profile.pprof", "Name of the file to write the profile to") } func initProfiling() error { var ( f *os.File err error ) switch profileName { case "none": return nil case "cpu": f, err = os.Create(profileOutput) if err != nil { return err } err = pprof.StartCPUProfile(f) if err != nil { return err } // Block and mutex profiles need a call to Set{Block,Mutex}ProfileRate to // output anything. We choose to sample all events. case "block": runtime.SetBlockProfileRate(1) case "mutex": runtime.SetMutexProfileFraction(1) default: // Check the profile name is valid. if profile := pprof.Lookup(profileName); profile == nil { return fmt.Errorf("unknown profile '%s'", profileName) } } // If the command is interrupted before the end (ctrl-c), flush the // profiling files c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { <-c f.Close() flushProfiling() os.Exit(0) }() return nil } func flushProfiling() error { switch profileName { case "none": return nil case "cpu": pprof.StopCPUProfile() case "heap": runtime.GC() fallthrough default: profile := pprof.Lookup(profileName) if profile == nil { return nil } f, err := os.Create(profileOutput) if err != nil { return err } defer f.Close() profile.WriteTo(f, 0) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/proxy/000077500000000000000000000000001504711711200257705ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/proxy/proxy.go000066400000000000000000000205051504711711200275020ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package proxy import ( "errors" "fmt" "net" "net/url" "os" "strings" "time" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/proxy" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // ProxyOptions have the data required to perform the proxy operation type ProxyOptions struct { // Common user flags staticDir string staticPrefix string apiPrefix string acceptPaths string rejectPaths string acceptHosts string rejectMethods string port int address string disableFilter bool unixSocket string keepalive time.Duration appendServerPath bool clientConfig *rest.Config filter *proxy.FilterServer genericiooptions.IOStreams } const ( defaultPort = 8001 defaultStaticPrefix = "/static/" defaultAPIPrefix = "/" defaultAddress = "127.0.0.1" ) var ( proxyLong = templates.LongDesc(i18n.T(` Creates a proxy server or application-level gateway between localhost and the Kubernetes API server. It also allows serving static content over specified HTTP path. All incoming data enters through one port and gets forwarded to the remote Kubernetes API server port, except for the path matching the static content path.`)) proxyExample = templates.Examples(i18n.T(` # To proxy all of the Kubernetes API and nothing else kubectl proxy --api-prefix=/ # To proxy only part of the Kubernetes API and also some static files # You can get pods info with 'curl localhost:8001/api/v1/pods' kubectl proxy --www=/my/files --www-prefix=/static/ --api-prefix=/api/ # To proxy the entire Kubernetes API at a different root # You can get pods info with 'curl localhost:8001/custom/api/v1/pods' kubectl proxy --api-prefix=/custom/ # Run a proxy to the Kubernetes API server on port 8011, serving static content from ./local/www/ kubectl proxy --port=8011 --www=./local/www/ # Run a proxy to the Kubernetes API server on an arbitrary local port # The chosen port for the server will be output to stdout kubectl proxy --port=0 # Run a proxy to the Kubernetes API server, changing the API prefix to k8s-api # This makes e.g. the pods API available at localhost:8001/k8s-api/v1/pods/ kubectl proxy --api-prefix=/k8s-api`)) ) // NewProxyOptions creates the options for proxy func NewProxyOptions(ioStreams genericiooptions.IOStreams) *ProxyOptions { return &ProxyOptions{ IOStreams: ioStreams, staticPrefix: defaultStaticPrefix, apiPrefix: defaultAPIPrefix, acceptPaths: proxy.DefaultPathAcceptRE, rejectPaths: proxy.DefaultPathRejectRE, acceptHosts: proxy.DefaultHostAcceptRE, rejectMethods: proxy.DefaultMethodRejectRE, port: defaultPort, address: defaultAddress, } } // NewCmdProxy returns the proxy Cobra command func NewCmdProxy(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewProxyOptions(ioStreams) cmd := &cobra.Command{ Use: "proxy [--port=PORT] [--www=static-dir] [--www-prefix=prefix] [--api-prefix=prefix]", DisableFlagsInUseLine: true, Short: i18n.T("Run a proxy to the Kubernetes API server"), Long: proxyLong, Example: proxyExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunProxy()) }, } cmd.Flags().StringVarP(&o.staticDir, "www", "w", o.staticDir, "Also serve static files from the given directory under the specified prefix.") cmd.Flags().StringVarP(&o.staticPrefix, "www-prefix", "P", o.staticPrefix, "Prefix to serve static files under, if static file directory is specified.") cmd.Flags().StringVar(&o.apiPrefix, "api-prefix", o.apiPrefix, "Prefix to serve the proxied API under.") cmd.Flags().StringVar(&o.acceptPaths, "accept-paths", o.acceptPaths, "Regular expression for paths that the proxy should accept.") cmd.Flags().StringVar(&o.rejectPaths, "reject-paths", o.rejectPaths, "Regular expression for paths that the proxy should reject. Paths specified here will be rejected even accepted by --accept-paths.") cmd.Flags().StringVar(&o.acceptHosts, "accept-hosts", o.acceptHosts, "Regular expression for hosts that the proxy should accept.") cmd.Flags().StringVar(&o.rejectMethods, "reject-methods", o.rejectMethods, "Regular expression for HTTP methods that the proxy should reject (example --reject-methods='POST,PUT,PATCH'). ") cmd.Flags().IntVarP(&o.port, "port", "p", o.port, "The port on which to run the proxy. Set to 0 to pick a random port.") cmd.Flags().StringVar(&o.address, "address", o.address, "The IP address on which to serve on.") cmd.Flags().BoolVar(&o.disableFilter, "disable-filter", o.disableFilter, "If true, disable request filtering in the proxy. This is dangerous, and can leave you vulnerable to XSRF attacks, when used with an accessible port.") cmd.Flags().StringVarP(&o.unixSocket, "unix-socket", "u", o.unixSocket, "Unix socket on which to run the proxy.") cmd.Flags().DurationVar(&o.keepalive, "keepalive", o.keepalive, "keepalive specifies the keep-alive period for an active network connection. Set to 0 to disable keepalive.") cmd.Flags().BoolVar(&o.appendServerPath, "append-server-path", o.appendServerPath, "If true, enables automatic path appending of the kube context server path to each request.") return cmd } // Complete adapts from the command line args and factory to the data required. func (o *ProxyOptions) Complete(f cmdutil.Factory) error { clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.clientConfig = clientConfig if !strings.HasSuffix(o.staticPrefix, "/") { o.staticPrefix += "/" } if !strings.HasSuffix(o.apiPrefix, "/") { o.apiPrefix += "/" } if o.appendServerPath == false { target, err := url.Parse(clientConfig.Host) if err != nil { return err } if target.Path != "" && target.Path != "/" { klog.Warning("Your kube context contains a server path " + target.Path + ", use --append-server-path to automatically append the path to each request") } } if o.disableFilter { if o.unixSocket == "" { klog.Warning("Request filter disabled, your proxy is vulnerable to XSRF attacks, please be cautious") } o.filter = nil } else { o.filter = &proxy.FilterServer{ AcceptPaths: proxy.MakeRegexpArrayOrDie(o.acceptPaths), RejectPaths: proxy.MakeRegexpArrayOrDie(o.rejectPaths), AcceptHosts: proxy.MakeRegexpArrayOrDie(o.acceptHosts), RejectMethods: proxy.MakeRegexpArrayOrDie(o.rejectMethods), } } return nil } // Validate checks to the ProxyOptions to see if there is sufficient information to run the command. func (o ProxyOptions) Validate() error { if o.port != defaultPort && o.unixSocket != "" { return errors.New("cannot set --unix-socket and --port at the same time") } if o.staticDir != "" { fileInfo, err := os.Stat(o.staticDir) if err != nil { klog.Warning("Failed to stat static file directory "+o.staticDir+": ", err) } else if !fileInfo.IsDir() { klog.Warning("Static file directory " + o.staticDir + " is not a directory") } } return nil } // RunProxy checks given arguments and executes command func (o ProxyOptions) RunProxy() error { server, err := proxy.NewServer(o.staticDir, o.apiPrefix, o.staticPrefix, o.filter, o.clientConfig, o.keepalive, o.appendServerPath) if err != nil { return err } // Separate listening from serving so we can report the bound port // when it is chosen by os (eg: port == 0) var l net.Listener if o.unixSocket == "" { l, err = server.Listen(o.address, o.port) } else { l, err = server.ListenUnix(o.unixSocket) } if err != nil { return err } fmt.Fprintf(o.IOStreams.Out, "Starting to serve on %s\n", l.Addr().String()) return server.ServeOnListener(l) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/replace/000077500000000000000000000000001504711711200262225ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace.go000066400000000000000000000264361504711711200301770ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replace import ( "context" "fmt" "net/url" "os" "path/filepath" "strings" "time" "github.com/spf13/cobra" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/kubectl/pkg/cmd/delete" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/rawhttp" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/validation" ) var ( replaceLong = templates.LongDesc(i18n.T(` Replace a resource by file name or stdin. JSON and YAML formats are accepted. If replacing an existing resource, the complete resource spec must be provided. This can be obtained by $ kubectl get TYPE NAME -o yaml`)) replaceExample = templates.Examples(i18n.T(` # Replace a pod using the data in pod.json kubectl replace -f ./pod.json # Replace a pod based on the JSON passed into stdin cat pod.json | kubectl replace -f - # Update a single-container pod's image version (tag) to v4 kubectl get pod mypod -o yaml | sed 's/\(image: myimage\):.*$/\1:v4/' | kubectl replace -f - # Force replace, delete and then re-create the resource kubectl replace --force -f ./pod.json`)) ) type ReplaceOptions struct { PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags DeleteFlags *delete.DeleteFlags DeleteOptions *delete.DeleteOptions DryRunStrategy cmdutil.DryRunStrategy validationDirective string PrintObj func(obj runtime.Object) error createAnnotation bool Schema validation.Schema Builder func() *resource.Builder BuilderArgs []string Namespace string EnforceNamespace bool Raw string Recorder genericclioptions.Recorder Subresource string genericiooptions.IOStreams fieldManager string } func NewReplaceOptions(streams genericiooptions.IOStreams) *ReplaceOptions { return &ReplaceOptions{ PrintFlags: genericclioptions.NewPrintFlags("replaced"), DeleteFlags: delete.NewDeleteFlags("The files that contain the configurations to replace."), IOStreams: streams, } } func NewCmdReplace(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewReplaceOptions(streams) cmd := &cobra.Command{ Use: "replace -f FILENAME", DisableFlagsInUseLine: true, Short: i18n.T("Replace a resource by file name or stdin"), Long: replaceLong, Example: replaceExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run(f)) }, } o.PrintFlags.AddFlags(cmd) o.DeleteFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to PUT to the server. Uses the transport specified by the kubeconfig file.") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-replace") cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, replace will operate on the subresource of the requested object.") return cmd } func (o *ReplaceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.validationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } o.createAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } dynamicClient, err := f.DynamicClient() if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } deleteOpts, err := o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams) if err != nil { return err } //Replace will create a resource if it doesn't exist already, so ignore not found error deleteOpts.IgnoreNotFound = true if o.PrintFlags.OutputFormat != nil { deleteOpts.Output = *o.PrintFlags.OutputFormat } if deleteOpts.GracePeriod == 0 { // To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0 // into --grace-period=1 and wait until the object is successfully deleted. deleteOpts.GracePeriod = 1 deleteOpts.WaitForDeletion = true } o.DeleteOptions = deleteOpts err = o.DeleteOptions.FilenameOptions.RequireFilenameOrKustomize() if err != nil { return err } schema, err := f.Validator(o.validationDirective) if err != nil { return err } o.Schema = schema o.Builder = f.NewBuilder o.BuilderArgs = args o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } return nil } func (o *ReplaceOptions) Validate() error { if o.DeleteOptions.GracePeriod >= 0 && !o.DeleteOptions.ForceDeletion { return fmt.Errorf("--grace-period must have --force specified") } if o.DeleteOptions.Timeout != 0 && !o.DeleteOptions.ForceDeletion { return fmt.Errorf("--timeout must have --force specified") } if o.DeleteOptions.ForceDeletion && o.DryRunStrategy != cmdutil.DryRunNone { return fmt.Errorf("--dry-run can not be used when --force is set") } if cmdutil.IsFilenameSliceEmpty(o.DeleteOptions.FilenameOptions.Filenames, o.DeleteOptions.FilenameOptions.Kustomize) { return fmt.Errorf("must specify --filename to replace") } if len(o.Raw) > 0 { if len(o.DeleteOptions.FilenameOptions.Filenames) != 1 { return fmt.Errorf("--raw can only use a single local file or stdin") } if strings.Index(o.DeleteOptions.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.DeleteOptions.FilenameOptions.Filenames[0], "https://") == 0 { return fmt.Errorf("--raw cannot read from a url") } if o.DeleteOptions.FilenameOptions.Recursive { return fmt.Errorf("--raw and --recursive are mutually exclusive") } if o.PrintFlags.OutputFormat != nil && len(*o.PrintFlags.OutputFormat) > 0 { return fmt.Errorf("--raw and --output are mutually exclusive") } if _, err := url.ParseRequestURI(o.Raw); err != nil { return fmt.Errorf("--raw must be a valid URL path: %v", err) } } return nil } func (o *ReplaceOptions) Run(f cmdutil.Factory) error { // raw only makes sense for a single file resource multiple objects aren't likely to do what you want. // the validator enforces this, so if len(o.Raw) > 0 { restClient, err := f.RESTClient() if err != nil { return err } return rawhttp.RawPut(restClient, o.IOStreams, o.Raw, o.DeleteOptions.Filenames[0]) } if o.DeleteOptions.ForceDeletion { return o.forceReplace() } r := o.Builder(). Unstructured(). Schema(o.Schema). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). Subresource(o.Subresource). Flatten(). Do() if err := r.Err(); err != nil { return err } return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.createAnnotation, info.Object, scheme.DefaultJSONEncoder()); err != nil { return cmdutil.AddSourceToErr("replacing", info.Source, err) } if err := o.Recorder.Record(info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } if o.DryRunStrategy == cmdutil.DryRunClient { return o.PrintObj(info.Object) } // Serialize the object with the annotation applied. obj, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). WithFieldValidation(o.validationDirective). WithSubresource(o.Subresource). Replace(info.Namespace, info.Name, true, info.Object) if err != nil { return cmdutil.AddSourceToErr("replacing", info.Source, err) } info.Refresh(obj, true) return o.PrintObj(info.Object) }) } func (o *ReplaceOptions) forceReplace() error { stdinInUse := false for i, filename := range o.DeleteOptions.FilenameOptions.Filenames { if filename == "-" { tempDir, err := os.MkdirTemp("", "kubectl_replace_") if err != nil { return err } defer os.RemoveAll(tempDir) tempFilename := filepath.Join(tempDir, "resource.stdin") err = cmdutil.DumpReaderToFile(os.Stdin, tempFilename) if err != nil { return err } o.DeleteOptions.FilenameOptions.Filenames[i] = tempFilename stdinInUse = true } } b := o.Builder(). Unstructured(). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace(). ResourceTypeOrNameArgs(false, o.BuilderArgs...).RequireObject(false). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). Subresource(o.Subresource). Flatten() if stdinInUse { b = b.StdinInUse() } r := b.Do() if err := r.Err(); err != nil { return err } if err := o.DeleteOptions.DeleteResult(r); err != nil { return err } timeout := o.DeleteOptions.Timeout if timeout == 0 { timeout = 5 * time.Minute } err := r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } return wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeout, true, func(ctx context.Context) (bool, error) { if err := info.Get(); !errors.IsNotFound(err) { return false, err } return true, nil }) }) if err != nil { return err } b = o.Builder(). Unstructured(). Schema(o.Schema). ContinueOnError(). NamespaceParam(o.Namespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions). Subresource(o.Subresource). Flatten() if stdinInUse { b = b.StdinInUse() } r = b.Do() err = r.Err() if err != nil { return err } count := 0 err = r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } if err := util.CreateOrUpdateAnnotation(o.createAnnotation, info.Object, scheme.DefaultJSONEncoder()); err != nil { return err } if err := o.Recorder.Record(info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } obj, err := resource.NewHelper(info.Client, info.Mapping). WithFieldManager(o.fieldManager). WithFieldValidation(o.validationDirective). Create(info.Namespace, true, info.Object) if err != nil { return err } count++ info.Refresh(obj, true) return o.PrintObj(info.Object) }) if err != nil { return err } if count == 0 { return fmt.Errorf("no objects passed to replace") } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace_test.go000066400000000000000000000256571504711711200312420ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replace import ( "net/http" "strings" "testing" corev1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestReplaceObject(t *testing.T) { _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) deleted := false tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil case p == "/namespaces/test/replicationcontrollers/redis-master" && m == http.MethodDelete: deleted = true fallthrough case p == "/namespaces/test/replicationcontrollers/redis-master" && m == http.MethodPut: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/replicationcontrollers/redis-master" && m == http.MethodGet: statusCode := http.StatusOK if deleted { statusCode = http.StatusNotFound } return &http.Response{StatusCode: statusCode, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdReplace(tf, streams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) // uses the name from the file, not the response if buf.String() != "replicationcontroller/rc1\n" { t.Errorf("unexpected output: %s", buf.String()) } buf.Reset() cmd.Flags().Set("force", "true") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/redis-master\nreplicationcontroller/rc1\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestReplaceMultipleObject(t *testing.T) { _, svc, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) redisMasterDeleted := false frontendDeleted := false tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil case p == "/namespaces/test/replicationcontrollers/redis-master" && m == http.MethodDelete: redisMasterDeleted = true fallthrough case p == "/namespaces/test/replicationcontrollers/redis-master" && m == http.MethodPut: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/replicationcontrollers/redis-master" && m == http.MethodGet: statusCode := http.StatusOK if redisMasterDeleted { statusCode = http.StatusNotFound } return &http.Response{StatusCode: statusCode, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case p == "/namespaces/test/services/frontend" && m == http.MethodDelete: frontendDeleted = true fallthrough case p == "/namespaces/test/services/frontend" && m == http.MethodPut: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil case p == "/namespaces/test/services/frontend" && m == http.MethodGet: statusCode := http.StatusOK if frontendDeleted { statusCode = http.StatusNotFound } return &http.Response{StatusCode: statusCode, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil case p == "/namespaces/test/services" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdReplace(tf, streams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../testdata/frontend-service.yaml") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/rc1\nservice/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } buf.Reset() cmd.Flags().Set("force", "true") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/redis-master\nservice/frontend\nreplicationcontroller/rc1\nservice/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestReplaceDirectory(t *testing.T) { _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) created := map[string]bool{} tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && m == http.MethodPut: created[p] = true return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && m == http.MethodGet: statusCode := http.StatusNotFound if created[p] { statusCode = http.StatusOK } return &http.Response{StatusCode: statusCode, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && m == http.MethodDelete: delete(created, p) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers") && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdReplace(tf, streams) cmd.Flags().Set("filename", "../../../testdata/replace/legacy") cmd.Flags().Set("namespace", "test") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/rc1\nreplicationcontroller/rc1\nreplicationcontroller/rc1\n" { t.Errorf("unexpected output: %s", buf.String()) } buf.Reset() cmd.Flags().Set("force", "true") cmd.Flags().Set("cascade", "false") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/frontend\nreplicationcontroller/redis-master\nreplicationcontroller/redis-slave\n"+ "replicationcontroller/rc1\nreplicationcontroller/rc1\nreplicationcontroller/rc1\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestForceReplaceObjectNotFound(t *testing.T) { _, _, rc := cmdtesting.TestData() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) tf.UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api/v1/namespaces/test" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Namespace{})}, nil case p == "/namespaces/test/replicationcontrollers/redis-master" && (m == http.MethodGet || m == http.MethodDelete): return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil case p == "/namespaces/test/replicationcontrollers" && m == http.MethodPost: return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdReplace(tf, streams) cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("force", "true") cmd.Flags().Set("cascade", "false") cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{}) if buf.String() != "replicationcontroller/rc1\n" { t.Errorf("unexpected output: %s", buf.String()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/000077500000000000000000000000001504711711200263075ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout.go000066400000000000000000000043241504711711200303410ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "github.com/lithammer/dedent" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( rolloutLong = templates.LongDesc(i18n.T(` Manage the rollout of one or many resources.`) + rolloutValidResources) rolloutExample = templates.Examples(` # Rollback to the previous deployment kubectl rollout undo deployment/abc # Check the rollout status of a daemonset kubectl rollout status daemonset/foo # Restart a deployment kubectl rollout restart deployment/abc # Restart deployments with the 'app=nginx' label kubectl rollout restart deployment --selector=app=nginx`) rolloutValidResources = dedent.Dedent(` Valid resource types include: * deployments * daemonsets * statefulsets `) ) // NewCmdRollout returns a Command instance for 'rollout' sub command func NewCmdRollout(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "rollout SUBCOMMAND", DisableFlagsInUseLine: true, Short: i18n.T("Manage the rollout of a resource"), Long: rolloutLong, Example: rolloutExample, Run: cmdutil.DefaultSubCommandRun(streams.Out), } // subcommands cmd.AddCommand(NewCmdRolloutHistory(f, streams)) cmd.AddCommand(NewCmdRolloutPause(f, streams)) cmd.AddCommand(NewCmdRolloutResume(f, streams)) cmd.AddCommand(NewCmdRolloutUndo(f, streams)) cmd.AddCommand(NewCmdRolloutStatus(f, streams)) cmd.AddCommand(NewCmdRolloutRestart(f, streams)) return cmd } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_history.go000066400000000000000000000147401504711711200321250ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "fmt" "sort" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( historyLong = templates.LongDesc(i18n.T(` View previous rollout revisions and configurations.`)) historyExample = templates.Examples(` # View the rollout history of a deployment kubectl rollout history deployment/abc # View the details of daemonset revision 3 kubectl rollout history daemonset/abc --revision=3`) ) // RolloutHistoryOptions holds the options for 'rollout history' sub command type RolloutHistoryOptions struct { PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Revision int64 Builder func() *resource.Builder Resources []string Namespace string EnforceNamespace bool LabelSelector string HistoryViewer polymorphichelpers.HistoryViewerFunc RESTClientGetter genericclioptions.RESTClientGetter resource.FilenameOptions genericiooptions.IOStreams } // NewRolloutHistoryOptions returns an initialized RolloutHistoryOptions instance func NewRolloutHistoryOptions(streams genericiooptions.IOStreams) *RolloutHistoryOptions { return &RolloutHistoryOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme), IOStreams: streams, } } // NewCmdRolloutHistory returns a Command instance for RolloutHistory sub command func NewCmdRolloutHistory(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewRolloutHistoryOptions(streams) validArgs := []string{"deployment", "daemonset", "statefulset"} cmd := &cobra.Command{ Use: "history (TYPE NAME | TYPE/NAME) [flags]", DisableFlagsInUseLine: true, Short: i18n.T("View rollout history"), Long: historyLong, Example: historyExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } cmd.Flags().Int64Var(&o.Revision, "revision", o.Revision, "See the details, including podTemplate of the revision specified") cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) o.PrintFlags.AddFlags(cmd) return cmd } // Complete completes all the required options func (o *RolloutHistoryOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { o.Resources = args var err error if o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace(); err != nil { return err } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation return o.PrintFlags.ToPrinter() } o.HistoryViewer = polymorphichelpers.HistoryViewerFn o.RESTClientGetter = f o.Builder = f.NewBuilder return nil } // Validate makes sure all the provided values for command-line options are valid func (o *RolloutHistoryOptions) Validate() error { if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { return fmt.Errorf("required resource not specified") } if o.Revision < 0 { return fmt.Errorf("revision must be a positive integer: %v", o.Revision) } return nil } // Run performs the execution of 'rollout history' sub command func (o *RolloutHistoryOptions) Run() error { r := o.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). ResourceTypeOrNameArgs(true, o.Resources...). ContinueOnError(). Latest(). Flatten(). Do() if err := r.Err(); err != nil { return err } if o.PrintFlags.OutputFlagSpecified() { printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } mapping := info.ResourceMapping() historyViewer, err := o.HistoryViewer(o.RESTClientGetter, mapping) if err != nil { return err } historyInfo, err := historyViewer.GetHistory(info.Namespace, info.Name) if err != nil { return err } if o.Revision > 0 { // Ensure the specified revision exists before printing revision, exists := historyInfo[o.Revision] if !exists { return fmt.Errorf("unable to find the specified revision") } if err := printer.PrintObj(revision, o.Out); err != nil { return err } } else { sortedKeys := make([]int64, 0, len(historyInfo)) for k := range historyInfo { sortedKeys = append(sortedKeys, k) } sort.Slice(sortedKeys, func(i, j int) bool { return sortedKeys[i] < sortedKeys[j] }) for _, k := range sortedKeys { printer.PrintObj(historyInfo[k], o.Out) } } return nil }) } return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } mapping := info.ResourceMapping() historyViewer, err := o.HistoryViewer(o.RESTClientGetter, mapping) if err != nil { return err } historyInfo, err := historyViewer.ViewHistory(info.Namespace, info.Name, o.Revision) if err != nil { return err } withRevision := "" if o.Revision > 0 { withRevision = fmt.Sprintf("with revision #%d", o.Revision) } printer, err := o.ToPrinter(fmt.Sprintf("%s\n%s", withRevision, historyInfo)) if err != nil { return err } return printer.PrintObj(info.Object, o.Out) }) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_history_test.go000066400000000000000000000366711504711711200331730ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "bytes" "io" "net/http" "testing" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" ) type fakeHistoryViewer struct { viewHistoryFn func(namespace, name string, revision int64) (string, error) getHistoryFn func(namespace, name string) (map[int64]runtime.Object, error) } func (h *fakeHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) { return h.viewHistoryFn(namespace, name, revision) } func (h *fakeHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) { return h.getHistoryFn(namespace, name) } func setupFakeHistoryViewer(t *testing.T) *fakeHistoryViewer { fhv := &fakeHistoryViewer{ viewHistoryFn: func(namespace, name string, revision int64) (string, error) { t.Fatalf("ViewHistory mock not implemented") return "", nil }, getHistoryFn: func(namespace, name string) (map[int64]runtime.Object, error) { t.Fatalf("GetHistory mock not implemented") return nil, nil }, } polymorphichelpers.HistoryViewerFn = func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (polymorphichelpers.HistoryViewer, error) { return fhv, nil } return fhv } func TestRolloutHistory(t *testing.T) { ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder) tf.Client = &RolloutPauseRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutPauseGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/foo" && m == "GET": responseDeployment := &appsv1.Deployment{} responseDeployment.Name = "foo" body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } testCases := map[string]struct { flags map[string]string expectedOutput string expectedRevision int64 }{ "should display ViewHistory output for all revisions": { expectedOutput: `deployment.apps/foo Fake ViewHistory Output `, expectedRevision: int64(0), }, "should display ViewHistory output for a single revision": { flags: map[string]string{"revision": "2"}, expectedOutput: `deployment.apps/foo with revision #2 Fake ViewHistory Output `, expectedRevision: int64(2), }, } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { fhv := setupFakeHistoryViewer(tt) var actualNamespace, actualName *string var actualRevision *int64 fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) { actualNamespace = &namespace actualName = &name actualRevision = &revision return "Fake ViewHistory Output\n", nil } streams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutHistory(tf, streams) for k, v := range tc.flags { cmd.Flags().Set(k, v) } cmd.Run(cmd, []string{"deployment/foo"}) expectedErrorOutput := "" if errBuf.String() != expectedErrorOutput { tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String()) } if buf.String() != tc.expectedOutput { tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String()) } expectedNamespace := "test" if actualNamespace == nil || *actualNamespace != expectedNamespace { tt.Fatalf("expected ViewHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace) } expectedName := "foo" if actualName == nil || *actualName != expectedName { tt.Fatalf("expected ViewHistory to have been called with name %s, but it was %v", expectedName, *actualName) } if actualRevision == nil { tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was ", tc.expectedRevision) } else if *actualRevision != tc.expectedRevision { tt.Fatalf("expected ViewHistory to have been called with revision %d, but it was %v", tc.expectedRevision, *actualRevision) } }) } } func TestMultipleResourceRolloutHistory(t *testing.T) { ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder) tf.Client = &RolloutPauseRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutPauseGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/foo" && m == "GET": responseDeployment := &appsv1.Deployment{} responseDeployment.Name = "foo" body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case p == "/namespaces/test/deployments/bar" && m == "GET": responseDeployment := &appsv1.Deployment{} responseDeployment.Name = "bar" body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } testCases := map[string]struct { flags map[string]string expectedOutput string }{ "should display ViewHistory output for all revisions": { expectedOutput: `deployment.apps/foo Fake ViewHistory Output deployment.apps/bar Fake ViewHistory Output `, }, "should display ViewHistory output for a single revision": { flags: map[string]string{"revision": "2"}, expectedOutput: `deployment.apps/foo with revision #2 Fake ViewHistory Output deployment.apps/bar with revision #2 Fake ViewHistory Output `, }, } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { fhv := setupFakeHistoryViewer(tt) fhv.viewHistoryFn = func(namespace, name string, revision int64) (string, error) { return "Fake ViewHistory Output\n", nil } streams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutHistory(tf, streams) for k, v := range tc.flags { cmd.Flags().Set(k, v) } cmd.Run(cmd, []string{"deployment/foo", "deployment/bar"}) expectedErrorOutput := "" if errBuf.String() != expectedErrorOutput { tt.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String()) } if buf.String() != tc.expectedOutput { tt.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String()) } }) } } func TestRolloutHistoryWithOutput(t *testing.T) { ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder) tf.Client = &RolloutPauseRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutPauseGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/foo" && m == "GET": responseDeployment := &appsv1.Deployment{} responseDeployment.Name = "foo" body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } testCases := map[string]struct { flags map[string]string expectedOutput string }{ "json": { flags: map[string]string{"revision": "2", "output": "json"}, expectedOutput: `{ "kind": "ReplicaSet", "apiVersion": "apps/v1", "metadata": { "name": "rev2", "creationTimestamp": null }, "spec": { "selector": null, "template": { "metadata": { "creationTimestamp": null }, "spec": { "containers": null } } }, "status": { "replicas": 0 } } `, }, "yaml": { flags: map[string]string{"revision": "2", "output": "yaml"}, expectedOutput: `apiVersion: apps/v1 kind: ReplicaSet metadata: creationTimestamp: null name: rev2 spec: selector: null template: metadata: creationTimestamp: null spec: containers: null status: replicas: 0 `, }, "yaml all revisions": { flags: map[string]string{"output": "yaml"}, expectedOutput: `apiVersion: apps/v1 kind: ReplicaSet metadata: creationTimestamp: null name: rev1 spec: selector: null template: metadata: creationTimestamp: null spec: containers: null status: replicas: 0 --- apiVersion: apps/v1 kind: ReplicaSet metadata: creationTimestamp: null name: rev2 spec: selector: null template: metadata: creationTimestamp: null spec: containers: null status: replicas: 0 `, }, "name": { flags: map[string]string{"output": "name"}, expectedOutput: `replicaset.apps/rev1 replicaset.apps/rev2 `, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { fhv := setupFakeHistoryViewer(t) var actualNamespace, actualName *string fhv.getHistoryFn = func(namespace, name string) (map[int64]runtime.Object, error) { actualNamespace = &namespace actualName = &name return map[int64]runtime.Object{ 1: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev1"}}, 2: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev2"}}, }, nil } streams, _, buf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutHistory(tf, streams) for k, v := range tc.flags { cmd.Flags().Set(k, v) } cmd.Run(cmd, []string{"deployment/foo"}) expectedErrorOutput := "" if errBuf.String() != expectedErrorOutput { t.Fatalf("expected error output: %s, but got %s", expectedErrorOutput, errBuf.String()) } if buf.String() != tc.expectedOutput { t.Fatalf("expected output: %s, but got: %s", tc.expectedOutput, buf.String()) } expectedNamespace := "test" if actualNamespace == nil || *actualNamespace != expectedNamespace { t.Fatalf("expected GetHistory to have been called with namespace %s, but it was %v", expectedNamespace, *actualNamespace) } expectedName := "foo" if actualName == nil || *actualName != expectedName { t.Fatalf("expected GetHistory to have been called with name %s, but it was %v", expectedName, *actualName) } }) } } func TestRolloutHistoryErrors(t *testing.T) { ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder) tf.Client = &RolloutPauseRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutPauseGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/foo" && m == "GET": responseDeployment := &appsv1.Deployment{} responseDeployment.Name = "foo" body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } testCases := map[string]struct { revision int64 outputFormat string expectedError string }{ "get non-existing revision as yaml": { revision: 999, outputFormat: "yaml", expectedError: "unable to find the specified revision", }, "get non-existing revision as json": { revision: 999, outputFormat: "json", expectedError: "unable to find the specified revision", }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { fhv := setupFakeHistoryViewer(t) fhv.getHistoryFn = func(namespace, name string) (map[int64]runtime.Object, error) { return map[int64]runtime.Object{ 1: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev1"}}, 2: &appsv1.ReplicaSet{ObjectMeta: v1.ObjectMeta{Name: "rev2"}}, }, nil } streams := genericiooptions.NewTestIOStreamsDiscard() o := NewRolloutHistoryOptions(streams) printFlags := &genericclioptions.PrintFlags{ JSONYamlPrintFlags: &genericclioptions.JSONYamlPrintFlags{ ShowManagedFields: true, }, OutputFormat: &tc.outputFormat, OutputFlagSpecified: func() bool { return true }, } o.PrintFlags = printFlags o.Revision = tc.revision if err := o.Complete(tf, nil, []string{"deployment/foo"}); err != nil { t.Fatalf("unexpected error: %v", err) } err := o.Run() if err != nil && err.Error() != tc.expectedError { t.Fatalf("expected '%s' error, but got: %v", tc.expectedError, err) } }) } } func TestValidate(t *testing.T) { opts := RolloutHistoryOptions{ Revision: 0, Resources: []string{"deployment/foo"}, } if err := opts.Validate(); err != nil { t.Fatalf("unexpected error: %s", err) } opts.Revision = -1 expectedError := "revision must be a positive integer: -1" if err := opts.Validate(); err == nil { t.Fatalf("unexpected non error") } else if err.Error() != expectedError { t.Fatalf("expected error %s, but got %s", expectedError, err.Error()) } opts.Revision = 0 opts.Resources = []string{} expectedError = "required resource not specified" if err := opts.Validate(); err == nil { t.Fatalf("unexpected non error") } else if err.Error() != expectedError { t.Fatalf("expected error %s, but got %s", expectedError, err.Error()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_pause.go000066400000000000000000000144351504711711200315420ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/kubectl/pkg/cmd/set" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // PauseOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type PauseOptions struct { PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Pauser polymorphichelpers.ObjectPauserFunc Builder func() *resource.Builder Namespace string EnforceNamespace bool Resources []string LabelSelector string resource.FilenameOptions genericiooptions.IOStreams fieldManager string } var ( pauseLong = templates.LongDesc(i18n.T(` Mark the provided resource as paused. Paused resources will not be reconciled by a controller. Use "kubectl rollout resume" to resume a paused resource. Currently only deployments support being paused.`)) pauseExample = templates.Examples(` # Mark the nginx deployment as paused # Any current state of the deployment will continue its function; new updates # to the deployment will not have an effect as long as the deployment is paused kubectl rollout pause deployment/nginx`) ) // NewCmdRolloutPause returns a Command instance for 'rollout pause' sub command func NewCmdRolloutPause(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := &PauseOptions{ PrintFlags: genericclioptions.NewPrintFlags("paused").WithTypeSetter(scheme.Scheme), IOStreams: streams, } validArgs := []string{"deployment"} cmd := &cobra.Command{ Use: "pause RESOURCE", DisableFlagsInUseLine: true, Short: i18n.T("Mark the provided resource as paused"), Long: pauseLong, Example: pauseExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunPause()) }, } o.PrintFlags.AddFlags(cmd) usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-rollout") cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) return cmd } // Complete completes all the required options func (o *PauseOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { o.Pauser = polymorphichelpers.ObjectPauserFn var err error o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.Resources = args o.Builder = f.NewBuilder o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation return o.PrintFlags.ToPrinter() } return nil } func (o *PauseOptions) Validate() error { if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { return fmt.Errorf("required resource not specified") } return nil } // RunPause performs the execution of 'rollout pause' sub command func (o *PauseOptions) RunPause() error { r := o.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). LabelSelectorParam(o.LabelSelector). FilenameParam(o.EnforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(true, o.Resources...). ContinueOnError(). Latest(). Flatten(). Do() if err := r.Err(); err != nil { return err } allErrs := []error{} infos, err := r.Infos() if err != nil { // restore previous command behavior where // an error caused by retrieving infos due to // at least a single broken object did not result // in an immediate return, but rather an overall // aggregation of errors. allErrs = append(allErrs, err) } patches := set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Pauser)) if len(patches) == 0 && len(allErrs) == 0 { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) return nil } for _, patch := range patches { info := patch.Info if patch.Err != nil { resourceString := info.Mapping.Resource.Resource if len(info.Mapping.Resource.Group) > 0 { resourceString = resourceString + "." + info.Mapping.Resource.Group } allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err)) continue } if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { printer, err := o.ToPrinter("already paused") if err != nil { allErrs = append(allErrs, err) continue } if err = printer.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } continue } obj, err := resource.NewHelper(info.Client, info.Mapping). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err)) continue } info.Refresh(obj, true) printer, err := o.ToPrinter("paused") if err != nil { allErrs = append(allErrs, err) continue } if err = printer.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_pause_test.go000066400000000000000000000054031504711711200325740ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "bytes" "io" "net/http" "testing" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericiooptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) var rolloutPauseGroupVersionEncoder = schema.GroupVersion{Group: "apps", Version: "v1"} func TestRolloutPause(t *testing.T) { deploymentName := "deployment/nginx-deployment" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutPauseGroupVersionEncoder) tf.Client = &RolloutPauseRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutPauseGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/nginx-deployment" && (m == "GET" || m == "PATCH"): responseDeployment := &appsv1.Deployment{} responseDeployment.Name = deploymentName body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutPause(tf, streams) cmd.Run(cmd, []string{deploymentName}) expectedOutput := "deployment.apps/" + deploymentName + " paused\n" if buf.String() != expectedOutput { t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) } } type RolloutPauseRESTClient struct { *fake.RESTClient } func (c *RolloutPauseRESTClient) Get() *restclient.Request { return c.RESTClient.Verb("GET") } func (c *RolloutPauseRESTClient) Patch(pt types.PatchType) *restclient.Request { return c.RESTClient.Verb("PATCH") } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_restart.go000066400000000000000000000147531504711711200321140ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/kubectl/pkg/cmd/set" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // RestartOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type RestartOptions struct { PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Resources []string Builder func() *resource.Builder Restarter polymorphichelpers.ObjectRestarterFunc Namespace string EnforceNamespace bool LabelSelector string resource.FilenameOptions genericiooptions.IOStreams fieldManager string } var ( restartLong = templates.LongDesc(i18n.T(` Restart a resource. Resource rollout will be restarted.`)) restartExample = templates.Examples(` # Restart all deployments in the test-namespace namespace kubectl rollout restart deployment -n test-namespace # Restart a deployment kubectl rollout restart deployment/nginx # Restart a daemon set kubectl rollout restart daemonset/abc # Restart deployments with the app=nginx label kubectl rollout restart deployment --selector=app=nginx`) ) // NewRolloutRestartOptions returns an initialized RestartOptions instance func NewRolloutRestartOptions(streams genericiooptions.IOStreams) *RestartOptions { return &RestartOptions{ PrintFlags: genericclioptions.NewPrintFlags("restarted").WithTypeSetter(scheme.Scheme), IOStreams: streams, } } // NewCmdRolloutRestart returns a Command instance for 'rollout restart' sub command func NewCmdRolloutRestart(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewRolloutRestartOptions(streams) validArgs := []string{"deployment", "daemonset", "statefulset"} cmd := &cobra.Command{ Use: "restart RESOURCE", DisableFlagsInUseLine: true, Short: i18n.T("Restart a resource"), Long: restartLong, Example: restartExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunRestart()) }, } usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-rollout") cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) o.PrintFlags.AddFlags(cmd) return cmd } // Complete completes all the required options func (o *RestartOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { o.Resources = args o.Restarter = polymorphichelpers.ObjectRestarterFn var err error o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation return o.PrintFlags.ToPrinter() } o.Builder = f.NewBuilder return nil } func (o *RestartOptions) Validate() error { if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { return fmt.Errorf("required resource not specified") } return nil } // RunRestart performs the execution of 'rollout restart' sub command func (o RestartOptions) RunRestart() error { r := o.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). FilenameParam(o.EnforceNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). ResourceTypeOrNameArgs(true, o.Resources...). ContinueOnError(). Latest(). Flatten(). Do() if err := r.Err(); err != nil { return err } allErrs := []error{} infos, err := r.Infos() if err != nil { // restore previous command behavior where // an error caused by retrieving infos due to // at least a single broken object did not result // in an immediate return, but rather an overall // aggregation of errors. allErrs = append(allErrs, err) } patches := set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Restarter)) if len(patches) == 0 && len(allErrs) == 0 { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) return nil } for _, patch := range patches { info := patch.Info if patch.Err != nil { resourceString := info.Mapping.Resource.Resource if len(info.Mapping.Resource.Group) > 0 { resourceString = resourceString + "." + info.Mapping.Resource.Group } allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err)) continue } if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { allErrs = append(allErrs, fmt.Errorf("failed to create patch for %v: if restart has already been triggered within the past second, please wait before attempting to trigger another", info.Name)) continue } obj, err := resource.NewHelper(info.Client, info.Mapping). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err)) continue } info.Refresh(obj, true) printer, err := o.ToPrinter("restarted") if err != nil { allErrs = append(allErrs, err) continue } if err = printer.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_restart_test.go000066400000000000000000000211431504711711200331420ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "bytes" "io" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) var rolloutRestartGroupVersionEncoder = schema.GroupVersion{Group: "apps", Version: "v1"} func TestRolloutRestartOne(t *testing.T) { deploymentName := "deployment/nginx-deployment" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutRestartGroupVersionEncoder) tf.Client = &RolloutRestartRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutRestartGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/nginx-deployment" && (m == "GET" || m == "PATCH"): responseDeployment := &appsv1.Deployment{} responseDeployment.Name = deploymentName body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutRestart(tf, streams) cmd.Run(cmd, []string{deploymentName}) expectedOutput := "deployment.apps/" + deploymentName + " restarted\n" if buf.String() != expectedOutput { t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) } } func TestRolloutRestartError(t *testing.T) { deploymentName := "deployment/nginx-deployment" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutRestartGroupVersionEncoder) tf.Client = &RolloutRestartRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutRestartGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/nginx-deployment" && (m == "GET" || m == "PATCH"): responseDeployment := &appsv1.Deployment{} responseDeployment.Name = deploymentName body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } streams, _, bufOut, _ := genericiooptions.NewTestIOStreams() opt := NewRolloutRestartOptions(streams) err := opt.Complete(tf, nil, []string{deploymentName}) assert.NoError(t, err) err = opt.Validate() assert.NoError(t, err) opt.Restarter = func(obj runtime.Object) ([]byte, error) { return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj) } expectedErr := "failed to create patch for nginx-deployment: if restart has already been triggered within the past second, please wait before attempting to trigger another" err = opt.RunRestart() if err == nil { t.Errorf("error expected but not fired") } if err.Error() != expectedErr { t.Errorf("unexpected error fired %v", err) } if bufOut.String() != "" { t.Errorf("unexpected message") } } // Tests that giving selectors with no matching objects shows an error func TestRolloutRestartSelectorNone(t *testing.T) { labelSelector := "app=test" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutRestartGroupVersionEncoder) tf.Client = &RolloutRestartRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutRestartGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m, q := req.URL.Path, req.Method, req.URL.Query(); { case p == "/namespaces/test/deployments" && m == "GET" && q.Get("labelSelector") == labelSelector: // Return an empty list responseDeployments := &appsv1.DeploymentList{} body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployments)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } streams, _, outBuf, errBuf := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutRestart(tf, streams) cmd.Flags().Set("selector", "app=test") cmd.Run(cmd, []string{"deployment"}) if len(outBuf.String()) != 0 { t.Errorf("expected empty output, but got: %s", outBuf.String()) } expectedError := "No resources found in test namespace.\n" if errBuf.String() != expectedError { t.Errorf("expected output: %s, but got: %s", expectedError, errBuf.String()) } } // Tests that giving selectors with no matching objects shows an error func TestRolloutRestartSelectorMany(t *testing.T) { firstDeployment := appsv1.Deployment{} firstDeployment.Name = "nginx-deployment-1" secondDeployment := appsv1.Deployment{} secondDeployment.Name = "nginx-deployment-2" labelSelector := "app=test" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutRestartGroupVersionEncoder) tf.Client = &RolloutRestartRESTClient{ RESTClient: &fake.RESTClient{ GroupVersion: rolloutRestartGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m, q := req.URL.Path, req.Method, req.URL.Query(); { case p == "/namespaces/test/deployments" && m == "GET" && q.Get("labelSelector") == labelSelector: // Return the list of 2 deployments responseDeployments := &appsv1.DeploymentList{} responseDeployments.Items = []appsv1.Deployment{firstDeployment, secondDeployment} body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, responseDeployments)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case (p == "/namespaces/test/deployments/nginx-deployment-1" || p == "/namespaces/test/deployments/nginx-deployment-2") && m == "PATCH": // Pick deployment based on path responseDeployment := firstDeployment if strings.HasSuffix(p, "nginx-deployment-2") { responseDeployment = secondDeployment } body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, &responseDeployment)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil } }), }, } streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutRestart(tf, streams) cmd.Flags().Set("selector", labelSelector) cmd.Run(cmd, []string{"deployment"}) expectedOutput := "deployment.apps/" + firstDeployment.Name + " restarted\ndeployment.apps/" + secondDeployment.Name + " restarted\n" if buf.String() != expectedOutput { t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) } } type RolloutRestartRESTClient struct { *fake.RESTClient } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_resume.go000066400000000000000000000145001504711711200317160ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/kubectl/pkg/cmd/set" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // ResumeOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type ResumeOptions struct { PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Resources []string Builder func() *resource.Builder Resumer polymorphichelpers.ObjectResumerFunc Namespace string EnforceNamespace bool LabelSelector string resource.FilenameOptions genericiooptions.IOStreams fieldManager string } var ( resumeLong = templates.LongDesc(i18n.T(` Resume a paused resource. Paused resources will not be reconciled by a controller. By resuming a resource, we allow it to be reconciled again. Currently only deployments support being resumed.`)) resumeExample = templates.Examples(` # Resume an already paused deployment kubectl rollout resume deployment/nginx`) ) // NewRolloutResumeOptions returns an initialized ResumeOptions instance func NewRolloutResumeOptions(streams genericiooptions.IOStreams) *ResumeOptions { return &ResumeOptions{ PrintFlags: genericclioptions.NewPrintFlags("resumed").WithTypeSetter(scheme.Scheme), IOStreams: streams, } } // NewCmdRolloutResume returns a Command instance for 'rollout resume' sub command func NewCmdRolloutResume(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewRolloutResumeOptions(streams) validArgs := []string{"deployment"} cmd := &cobra.Command{ Use: "resume RESOURCE", DisableFlagsInUseLine: true, Short: i18n.T("Resume a paused resource"), Long: resumeLong, Example: resumeExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunResume()) }, } usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-rollout") cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) o.PrintFlags.AddFlags(cmd) return cmd } // Complete completes all the required options func (o *ResumeOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { o.Resources = args o.Resumer = polymorphichelpers.ObjectResumerFn var err error o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation return o.PrintFlags.ToPrinter() } o.Builder = f.NewBuilder return nil } func (o *ResumeOptions) Validate() error { if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { return fmt.Errorf("required resource not specified") } return nil } // RunResume performs the execution of 'rollout resume' sub command func (o ResumeOptions) RunResume() error { r := o.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). LabelSelectorParam(o.LabelSelector). FilenameParam(o.EnforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(true, o.Resources...). ContinueOnError(). Latest(). Flatten(). Do() if err := r.Err(); err != nil { return err } allErrs := []error{} infos, err := r.Infos() if err != nil { // restore previous command behavior where // an error caused by retrieving infos due to // at least a single broken object did not result // in an immediate return, but rather an overall // aggregation of errors. allErrs = append(allErrs, err) } patches := set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Resumer)) if len(patches) == 0 && len(allErrs) == 0 { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) return nil } for _, patch := range patches { info := patch.Info if patch.Err != nil { resourceString := info.Mapping.Resource.Resource if len(info.Mapping.Resource.Group) > 0 { resourceString = resourceString + "." + info.Mapping.Resource.Group } allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err)) continue } if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { printer, err := o.ToPrinter("already resumed") if err != nil { allErrs = append(allErrs, err) continue } if err = printer.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } continue } obj, err := resource.NewHelper(info.Client, info.Mapping). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err)) continue } info.Refresh(obj, true) printer, err := o.ToPrinter("resumed") if err != nil { allErrs = append(allErrs, err) continue } if err = printer.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status.go000066400000000000000000000173561504711711200317550ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "context" "fmt" "time" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" ) var ( statusLong = templates.LongDesc(i18n.T(` Show the status of the rollout. By default 'rollout status' will watch the status of the latest rollout until it's done. If you don't want to wait for the rollout to finish then you can use --watch=false. Note that if a new rollout starts in-between, then 'rollout status' will continue watching the latest revision. If you want to pin to a specific revision and abort if it is rolled over by another revision, use --revision=N where N is the revision you need to watch for.`)) statusExample = templates.Examples(` # Watch the rollout status of a deployment kubectl rollout status deployment/nginx`) ) // RolloutStatusOptions holds the command-line options for 'rollout status' sub command type RolloutStatusOptions struct { PrintFlags *genericclioptions.PrintFlags Namespace string EnforceNamespace bool BuilderArgs []string LabelSelector string Watch bool Revision int64 Timeout time.Duration StatusViewerFn func(*meta.RESTMapping) (polymorphichelpers.StatusViewer, error) Builder func() *resource.Builder DynamicClient dynamic.Interface FilenameOptions *resource.FilenameOptions genericiooptions.IOStreams } // NewRolloutStatusOptions returns an initialized RolloutStatusOptions instance func NewRolloutStatusOptions(streams genericiooptions.IOStreams) *RolloutStatusOptions { return &RolloutStatusOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme), FilenameOptions: &resource.FilenameOptions{}, IOStreams: streams, Watch: true, Timeout: 0, } } // NewCmdRolloutStatus returns a Command instance for the 'rollout status' sub command func NewCmdRolloutStatus(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewRolloutStatusOptions(streams) validArgs := []string{"deployment", "daemonset", "statefulset"} cmd := &cobra.Command{ Use: "status (TYPE NAME | TYPE/NAME) [flags]", DisableFlagsInUseLine: true, Short: i18n.T("Show the status of the rollout"), Long: statusLong, Example: statusExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, usage) cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "Watch the status of the rollout until it's done.") cmd.Flags().Int64Var(&o.Revision, "revision", o.Revision, "Pin to a specific revision for showing its status. Defaults to 0 (last revision).") cmd.Flags().DurationVar(&o.Timeout, "timeout", o.Timeout, "The length of time to wait before ending watch, zero means never. Any other values should contain a corresponding time unit (e.g. 1s, 2m, 3h).") cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) return cmd } // Complete completes all the required options func (o *RolloutStatusOptions) Complete(f cmdutil.Factory, args []string) error { o.Builder = f.NewBuilder var err error o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.BuilderArgs = args o.StatusViewerFn = polymorphichelpers.StatusViewerFn o.DynamicClient, err = f.DynamicClient() if err != nil { return err } return nil } // Validate makes sure all the provided values for command-line options are valid func (o *RolloutStatusOptions) Validate() error { if len(o.BuilderArgs) == 0 && cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) { return fmt.Errorf("required resource not specified") } if o.Revision < 0 { return fmt.Errorf("revision must be a positive integer: %v", o.Revision) } return nil } // Run performs the execution of 'rollout status' sub command func (o *RolloutStatusOptions) Run() error { r := o.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). LabelSelectorParam(o.LabelSelector). FilenameParam(o.EnforceNamespace, o.FilenameOptions). ResourceTypeOrNameArgs(true, o.BuilderArgs...). ContinueOnError(). Latest(). Flatten(). Do() err := r.Err() if err != nil { return err } resourceFound := false err = r.Visit(func(info *resource.Info, _ error) error { resourceFound = true mapping := info.ResourceMapping() statusViewer, err := o.StatusViewerFn(mapping) if err != nil { return err } fieldSelector := fields.OneTermEqualSelector("metadata.name", info.Name).String() lw := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { options.FieldSelector = fieldSelector return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(context.TODO(), options) }, } // if the rollout isn't done yet, keep watching deployment status ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout) intr := interrupt.New(nil, cancel) return intr.Run(func() error { _, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, nil, func(e watch.Event) (bool, error) { switch t := e.Type; t { case watch.Added, watch.Modified: status, done, err := statusViewer.Status(e.Object.(runtime.Unstructured), o.Revision) if err != nil { return false, err } fmt.Fprintf(o.Out, "%s", status) // Quit waiting if the rollout is done if done { return true, nil } shouldWatch := o.Watch if !shouldWatch { return true, nil } return false, nil case watch.Deleted: // We need to abort to avoid cases of recreation and not to silently watch the wrong (new) object return true, fmt.Errorf("object has been deleted") default: return true, fmt.Errorf("internal error: unexpected event %#v", e) } }) return err }) }) if err != nil { return err } if !resourceFound { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status_test.go000066400000000000000000000244551504711711200330120ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "bytes" "io" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cgtesting "k8s.io/client-go/testing" "k8s.io/kubectl/pkg/scheme" "net/http" "testing" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) var rolloutStatusGroupVersionEncoder = schema.GroupVersion{Group: "apps", Version: "v1"} func TestRolloutStatus(t *testing.T) { deploymentName := "deployment/nginx-deployment" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") tf.ClientConfigVal = cmdtesting.DefaultClientConfig() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutStatusGroupVersionEncoder) tf.Client = &fake.RESTClient{ GroupVersion: rolloutStatusGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { dep := &appsv1.Deployment{} dep.Name = deploymentName body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, dep)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil }), } tf.FakeDynamicClient.WatchReactionChain = nil tf.FakeDynamicClient.AddWatchReactor("*", func(action cgtesting.Action) (handled bool, ret watch.Interface, err error) { fw := watch.NewFake() dep := &appsv1.Deployment{} dep.Name = deploymentName dep.Status = appsv1.DeploymentStatus{ Replicas: 1, UpdatedReplicas: 1, ReadyReplicas: 1, AvailableReplicas: 1, UnavailableReplicas: 0, Conditions: []appsv1.DeploymentCondition{{ Type: appsv1.DeploymentAvailable, }}, } c, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep.DeepCopyObject()) if err != nil { t.Errorf("unexpected err %s", err) } u := &unstructured.Unstructured{} u.SetUnstructuredContent(c) go fw.Add(u) return true, fw, nil }) streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutStatus(tf, streams) cmd.Run(cmd, []string{deploymentName}) expectedMsg := "deployment \"deployment/nginx-deployment\" successfully rolled out\n" if buf.String() != expectedMsg { t.Errorf("expected output: %s, but got: %s", expectedMsg, buf.String()) } } func TestRolloutStatusWithSelector(t *testing.T) { deploymentName := "deployment" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") tf.ClientConfigVal = cmdtesting.DefaultClientConfig() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutStatusGroupVersionEncoder) tf.Client = &fake.RESTClient{ GroupVersion: rolloutStatusGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { dep := &appsv1.Deployment{} dep.Name = deploymentName dep.Labels = make(map[string]string) dep.Labels["app"] = "api" body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, dep)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil }), } tf.FakeDynamicClient.WatchReactionChain = nil tf.FakeDynamicClient.AddWatchReactor("*", func(action cgtesting.Action) (handled bool, ret watch.Interface, err error) { fw := watch.NewFake() dep := &appsv1.Deployment{} dep.Name = deploymentName dep.Status = appsv1.DeploymentStatus{ Replicas: 1, UpdatedReplicas: 1, ReadyReplicas: 1, AvailableReplicas: 1, UnavailableReplicas: 0, Conditions: []appsv1.DeploymentCondition{{ Type: appsv1.DeploymentAvailable, }}, } dep.Labels = make(map[string]string) dep.Labels["app"] = "api" c, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep.DeepCopyObject()) if err != nil { t.Errorf("unexpected err %s", err) } u := &unstructured.Unstructured{} u.SetUnstructuredContent(c) go fw.Add(u) return true, fw, nil }) streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutStatus(tf, streams) cmd.Flags().Set("selector", "app=api") cmd.Run(cmd, []string{deploymentName}) expectedMsg := "deployment \"deployment\" successfully rolled out\n" if buf.String() != expectedMsg { t.Errorf("expected output: %s, but got: %s", expectedMsg, buf.String()) } } func TestRolloutStatusWatchDisabled(t *testing.T) { deploymentName := "deployment/nginx-deployment" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") tf.ClientConfigVal = cmdtesting.DefaultClientConfig() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutStatusGroupVersionEncoder) tf.Client = &fake.RESTClient{ GroupVersion: rolloutStatusGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { dep := &appsv1.Deployment{} dep.Name = deploymentName body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, dep)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil }), } tf.FakeDynamicClient.WatchReactionChain = nil tf.FakeDynamicClient.AddWatchReactor("*", func(action cgtesting.Action) (handled bool, ret watch.Interface, err error) { fw := watch.NewFake() dep := &appsv1.Deployment{} dep.Name = deploymentName dep.Status = appsv1.DeploymentStatus{ Replicas: 1, UpdatedReplicas: 1, ReadyReplicas: 1, AvailableReplicas: 1, UnavailableReplicas: 0, Conditions: []appsv1.DeploymentCondition{{ Type: appsv1.DeploymentAvailable, }}, } c, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep.DeepCopyObject()) if err != nil { t.Errorf("unexpected err %s", err) } u := &unstructured.Unstructured{} u.SetUnstructuredContent(c) go fw.Add(u) return true, fw, nil }) streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutStatus(tf, streams) cmd.Flags().Set("watch", "false") cmd.Run(cmd, []string{deploymentName}) expectedMsg := "deployment \"deployment/nginx-deployment\" successfully rolled out\n" if buf.String() != expectedMsg { t.Errorf("expected output: %s, but got: %s", expectedMsg, buf.String()) } } func TestRolloutStatusWatchDisabledUnavailable(t *testing.T) { deploymentName := "deployment/nginx-deployment" ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") tf.ClientConfigVal = cmdtesting.DefaultClientConfig() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutStatusGroupVersionEncoder) tf.Client = &fake.RESTClient{ GroupVersion: rolloutStatusGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { dep := &appsv1.Deployment{} dep.Name = deploymentName body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, dep)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil }), } tf.FakeDynamicClient.WatchReactionChain = nil tf.FakeDynamicClient.AddWatchReactor("*", func(action cgtesting.Action) (handled bool, ret watch.Interface, err error) { fw := watch.NewFake() dep := &appsv1.Deployment{} dep.Name = deploymentName dep.Status = appsv1.DeploymentStatus{ Replicas: 1, UpdatedReplicas: 1, ReadyReplicas: 1, AvailableReplicas: 0, UnavailableReplicas: 0, Conditions: []appsv1.DeploymentCondition{{ Type: appsv1.DeploymentAvailable, }}, } c, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep.DeepCopyObject()) if err != nil { t.Errorf("unexpected err %s", err) } u := &unstructured.Unstructured{} u.SetUnstructuredContent(c) go fw.Add(u) return true, fw, nil }) streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutStatus(tf, streams) cmd.Flags().Set("watch", "false") cmd.Run(cmd, []string{deploymentName}) expectedMsg := "Waiting for deployment \"deployment/nginx-deployment\" rollout to finish: 0 of 1 updated replicas are available...\n" if buf.String() != expectedMsg { t.Errorf("expected output: %s, but got: %s", expectedMsg, buf.String()) } } func TestRolloutStatusEmptyList(t *testing.T) { ns := scheme.Codecs.WithoutConversion() tf := cmdtesting.NewTestFactory().WithNamespace("test") tf.ClientConfigVal = cmdtesting.DefaultClientConfig() info, _ := runtime.SerializerInfoForMediaType(ns.SupportedMediaTypes(), runtime.ContentTypeJSON) encoder := ns.EncoderForVersion(info.Serializer, rolloutStatusGroupVersionEncoder) tf.Client = &fake.RESTClient{ GroupVersion: rolloutStatusGroupVersionEncoder, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { dep := &appsv1.DeploymentList{} body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, dep)))) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil }), } streams, _, _, err := genericiooptions.NewTestIOStreams() cmd := NewCmdRolloutStatus(tf, streams) cmd.Run(cmd, []string{"deployment"}) expectedMsg := "No resources found in test namespace.\n" if err.String() != expectedMsg { t.Errorf("expected output: %s, but got: %s", expectedMsg, err.String()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_undo.go000066400000000000000000000124061504711711200313660ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rollout import ( "fmt" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // UndoOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type UndoOptions struct { PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Builder func() *resource.Builder ToRevision int64 DryRunStrategy cmdutil.DryRunStrategy Resources []string Namespace string LabelSelector string EnforceNamespace bool RESTClientGetter genericclioptions.RESTClientGetter resource.FilenameOptions genericiooptions.IOStreams } var ( undoLong = templates.LongDesc(i18n.T(` Roll back to a previous rollout.`)) undoExample = templates.Examples(` # Roll back to the previous deployment kubectl rollout undo deployment/abc # Roll back to daemonset revision 3 kubectl rollout undo daemonset/abc --to-revision=3 # Roll back to the previous deployment with dry-run kubectl rollout undo --dry-run=server deployment/abc`) ) // NewRolloutUndoOptions returns an initialized UndoOptions instance func NewRolloutUndoOptions(streams genericiooptions.IOStreams) *UndoOptions { return &UndoOptions{ PrintFlags: genericclioptions.NewPrintFlags("rolled back").WithTypeSetter(scheme.Scheme), IOStreams: streams, ToRevision: int64(0), } } // NewCmdRolloutUndo returns a Command instance for the 'rollout undo' sub command func NewCmdRolloutUndo(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewRolloutUndoOptions(streams) validArgs := []string{"deployment", "daemonset", "statefulset"} cmd := &cobra.Command{ Use: "undo (TYPE NAME | TYPE/NAME) [flags]", DisableFlagsInUseLine: true, Short: i18n.T("Undo a previous rollout"), Long: undoLong, Example: undoExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunUndo()) }, } cmd.Flags().Int64Var(&o.ToRevision, "to-revision", o.ToRevision, "The revision to rollback to. Default to 0 (last revision).") usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmdutil.AddDryRunFlag(cmd) cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) o.PrintFlags.AddFlags(cmd) return cmd } // Complete completes all the required options func (o *UndoOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { o.Resources = args var err error o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } if o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace(); err != nil { return err } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) return o.PrintFlags.ToPrinter() } o.RESTClientGetter = f o.Builder = f.NewBuilder return err } func (o *UndoOptions) Validate() error { if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { return fmt.Errorf("required resource not specified") } return nil } // RunUndo performs the execution of 'rollout undo' sub command func (o *UndoOptions) RunUndo() error { r := o.Builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). NamespaceParam(o.Namespace).DefaultNamespace(). LabelSelectorParam(o.LabelSelector). FilenameParam(o.EnforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(true, o.Resources...). ContinueOnError(). Latest(). Flatten(). Do() if err := r.Err(); err != nil { return err } err := r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } rollbacker, err := polymorphichelpers.RollbackerFn(o.RESTClientGetter, info.ResourceMapping()) if err != nil { return err } result, err := rollbacker.Rollback(info.Object, nil, o.ToRevision, o.DryRunStrategy) if err != nil { return err } printer, err := o.ToPrinter(result) if err != nil { return err } return printer.PrintObj(info.Object, o.Out) }) return err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/run/000077500000000000000000000000001504711711200254135ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go000066400000000000000000000555541504711711200265640ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package run import ( "context" "fmt" "time" "github.com/distribution/reference" "github.com/spf13/cobra" "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" "k8s.io/kubectl/pkg/cmd/attach" cmddelete "k8s.io/kubectl/pkg/cmd/delete" "k8s.io/kubectl/pkg/cmd/exec" "k8s.io/kubectl/pkg/cmd/logs" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/generate" generateversioned "k8s.io/kubectl/pkg/generate/versioned" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/templates" uexec "k8s.io/utils/exec" ) var ( runLong = templates.LongDesc(i18n.T(`Create and run a particular image in a pod.`)) runExample = templates.Examples(i18n.T(` # Start a nginx pod kubectl run nginx --image=nginx # Start a hazelcast pod and let the container expose port 5701 kubectl run hazelcast --image=hazelcast/hazelcast --port=5701 # Start a hazelcast pod and set environment variables "DNS_DOMAIN=cluster" and "POD_NAMESPACE=default" in the container kubectl run hazelcast --image=hazelcast/hazelcast --env="DNS_DOMAIN=cluster" --env="POD_NAMESPACE=default" # Start a hazelcast pod and set labels "app=hazelcast" and "env=prod" in the container kubectl run hazelcast --image=hazelcast/hazelcast --labels="app=hazelcast,env=prod" # Dry run; print the corresponding API objects without creating them kubectl run nginx --image=nginx --dry-run=client # Start a nginx pod, but overload the spec with a partial set of values parsed from JSON kubectl run nginx --image=nginx --overrides='{ "apiVersion": "v1", "spec": { ... } }' # Start a busybox pod and keep it in the foreground, don't restart it if it exits kubectl run -i -t busybox --image=busybox --restart=Never # Start the nginx pod using the default command, but use custom arguments (arg1 .. argN) for that command kubectl run nginx --image=nginx -- ... # Start the nginx pod using a different command and custom arguments kubectl run nginx --image=nginx --command -- ... `)) ) const ( defaultPodAttachTimeout = 60 * time.Second ) var metadataAccessor = meta.NewAccessor() type RunObject struct { Object runtime.Object Mapping *meta.RESTMapping } type RunOptions struct { cmdutil.OverrideOptions PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags DeleteFlags *cmddelete.DeleteFlags DeleteOptions *cmddelete.DeleteOptions DryRunStrategy cmdutil.DryRunStrategy PrintObj func(runtime.Object) error Recorder genericclioptions.Recorder ArgsLenAtDash int Attach bool Expose bool Image string Interactive bool LeaveStdinOpen bool Port string Privileged bool Quiet bool TTY bool fieldManager string Namespace string EnforceNamespace bool genericiooptions.IOStreams } func NewRunOptions(streams genericiooptions.IOStreams) *RunOptions { return &RunOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), DeleteFlags: cmddelete.NewDeleteFlags("to use to replace the resource."), RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, IOStreams: streams, } } func NewCmdRun(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewRunOptions(streams) cmd := &cobra.Command{ Use: "run NAME --image=image [--env=\"key=value\"] [--port=port] [--dry-run=server|client] [--overrides=inline-json] [--command] -- [COMMAND] [args...]", DisableFlagsInUseLine: true, Short: i18n.T("Run a particular image on the cluster"), Long: runLong, Example: runExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd)) cmdutil.CheckErr(o.Run(f, cmd, args)) }, } o.DeleteFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd) addRunFlags(cmd, o) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout) return cmd } func addRunFlags(cmd *cobra.Command, opt *RunOptions) { cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringArray("annotations", []string{}, i18n.T("Annotations to apply to the pod.")) cmd.Flags().StringVar(&opt.Image, "image", opt.Image, i18n.T("The image for the container to run.")) cmd.MarkFlagRequired("image") cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server.")) cmd.Flags().Bool("rm", false, "If true, delete the pod after it exits. Only valid when attaching to the container, e.g. with '--attach' or with '-i/--stdin'.") cmd.Flags().StringArray("env", []string{}, "Environment variables to set in the container.") cmd.Flags().StringVar(&opt.Port, "port", opt.Port, i18n.T("The port that this container exposes.")) cmd.Flags().StringP("labels", "l", "", "Comma separated labels to apply to the pod. Will override previous values.") cmd.Flags().BoolVarP(&opt.Interactive, "stdin", "i", opt.Interactive, "Keep stdin open on the container in the pod, even if nothing is attached.") cmd.Flags().BoolVarP(&opt.TTY, "tty", "t", opt.TTY, "Allocate a TTY for the container in the pod.") cmd.Flags().BoolVar(&opt.Attach, "attach", opt.Attach, "If true, wait for the Pod to start running, and then attach to the Pod as if 'kubectl attach ...' were called. Default false, unless '-i/--stdin' is set, in which case the default is true. With '--restart=Never' the exit code of the container process is returned.") cmd.Flags().BoolVar(&opt.LeaveStdinOpen, "leave-stdin-open", opt.LeaveStdinOpen, "If the pod is started in interactive mode or with stdin, leave stdin open after the first attach completes. By default, stdin will be closed after the first attach completes.") cmd.Flags().String("restart", "Always", i18n.T("The restart policy for this Pod. Legal values [Always, OnFailure, Never].")) cmd.Flags().Bool("command", false, "If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default.") cmd.Flags().BoolVar(&opt.Expose, "expose", opt.Expose, "If true, create a ClusterIP service associated with the pod. Requires `--port`.") cmd.Flags().BoolVarP(&opt.Quiet, "quiet", "q", opt.Quiet, "If true, suppress prompt messages.") cmd.Flags().BoolVar(&opt.Privileged, "privileged", opt.Privileged, i18n.T("If true, run the container in privileged mode.")) cmdutil.AddFieldManagerFlagVar(cmd, &opt.fieldManager, "kubectl-run") opt.AddOverrideFlags(cmd) } func (o *RunOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.ArgsLenAtDash = cmd.ArgsLenAtDash() o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } dynamicClient, err := f.DynamicClient() if err != nil { return err } attachFlag := cmd.Flags().Lookup("attach") if !attachFlag.Changed && o.Interactive { o.Attach = true } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } deleteOpts, err := o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams) if err != nil { return err } deleteOpts.IgnoreNotFound = true deleteOpts.WaitForDeletion = false deleteOpts.GracePeriod = -1 deleteOpts.Quiet = o.Quiet o.DeleteOptions = deleteOpts return nil } func (o *RunOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) error { // Let kubectl run follow rules for `--`, see #13004 issue if len(args) == 0 || o.ArgsLenAtDash == 0 { return cmdutil.UsageErrorf(cmd, "NAME is required for run") } timeout, err := cmdutil.GetPodRunningTimeoutFlag(cmd) if err != nil { return cmdutil.UsageErrorf(cmd, "%v", err) } // validate image name if o.Image == "" { return fmt.Errorf("--image is required") } if !reference.ReferenceRegexp.MatchString(o.Image) { return fmt.Errorf("Invalid image name %q: %v", o.Image, reference.ErrReferenceInvalidFormat) } if o.TTY && !o.Interactive { return cmdutil.UsageErrorf(cmd, "-i/--stdin is required for containers with -t/--tty=true") } if o.Expose && len(o.Port) == 0 { return cmdutil.UsageErrorf(cmd, "--port must be set when exposing a service") } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } restartPolicy, err := getRestartPolicy(cmd, o.Interactive) if err != nil { return err } remove := cmdutil.GetFlagBool(cmd, "rm") if !o.Attach && remove { return cmdutil.UsageErrorf(cmd, "--rm should only be used for attached containers") } if o.Attach && o.DryRunStrategy != cmdutil.DryRunNone { return cmdutil.UsageErrorf(cmd, "--dry-run=[server|client] can't be used with attached containers options (--attach, --stdin, or --tty)") } if err := verifyImagePullPolicy(cmd); err != nil { return err } generators := generateversioned.GeneratorFn("run") generator, found := generators[generateversioned.RunPodV1GeneratorName] if !found { return cmdutil.UsageErrorf(cmd, "generator %q not found", generateversioned.RunPodV1GeneratorName) } names := generator.ParamNames() params := generate.MakeParams(cmd, names) params["name"] = args[0] if len(args) > 1 { params["args"] = args[1:] } params["annotations"] = cmdutil.GetFlagStringArray(cmd, "annotations") params["env"] = cmdutil.GetFlagStringArray(cmd, "env") var createdObjects = []*RunObject{} runObject, err := o.createGeneratedObject(f, cmd, generator, names, params, o.NewOverrider(&corev1.Pod{})) if err != nil { return err } createdObjects = append(createdObjects, runObject) allErrs := []error{} if o.Expose { serviceRunObject, err := o.generateService(f, cmd, params) if err != nil { allErrs = append(allErrs, err) } else { createdObjects = append(createdObjects, serviceRunObject) } } if o.Attach { if remove { defer o.removeCreatedObjects(f, createdObjects) } opts := &attach.AttachOptions{ StreamOptions: exec.StreamOptions{ IOStreams: o.IOStreams, Stdin: o.Interactive, TTY: o.TTY, Quiet: o.Quiet, }, GetPodTimeout: timeout, CommandName: cmd.Parent().CommandPath() + " attach", Attach: &attach.DefaultRemoteAttach{}, } config, err := f.ToRESTConfig() if err != nil { return err } opts.Config = config opts.AttachFunc = attach.DefaultAttachFunc clientset, err := kubernetes.NewForConfig(config) if err != nil { return err } attachablePod, err := polymorphichelpers.AttachablePodForObjectFn(f, runObject.Object, opts.GetPodTimeout) if err != nil { return err } err = handleAttachPod(f, clientset.CoreV1(), attachablePod.Namespace, attachablePod.Name, opts) if err != nil { return err } var pod *corev1.Pod waitForExitCode := !o.LeaveStdinOpen && (restartPolicy == corev1.RestartPolicyNever || restartPolicy == corev1.RestartPolicyOnFailure) if waitForExitCode { // we need different exit condition depending on restart policy // for Never it can either fail or succeed, for OnFailure only // success matters exitCondition := podCompleted if restartPolicy == corev1.RestartPolicyOnFailure { exitCondition = podSucceeded } pod, err = waitForPod(clientset.CoreV1(), attachablePod.Namespace, attachablePod.Name, opts.GetPodTimeout, exitCondition) if err != nil { return err } } else { // after removal is done, return successfully if we are not interested in the exit code return nil } switch pod.Status.Phase { case corev1.PodSucceeded: return nil case corev1.PodFailed: unknownRcErr := fmt.Errorf("pod %s/%s failed with unknown exit code", pod.Namespace, pod.Name) if len(pod.Status.ContainerStatuses) == 0 || pod.Status.ContainerStatuses[0].State.Terminated == nil { return unknownRcErr } // assume here that we have at most one status because kubectl-run only creates one container per pod rc := pod.Status.ContainerStatuses[0].State.Terminated.ExitCode if rc == 0 { return unknownRcErr } return uexec.CodeExitError{ Err: fmt.Errorf("pod %s/%s terminated (%s)\n%s", pod.Namespace, pod.Name, pod.Status.ContainerStatuses[0].State.Terminated.Reason, pod.Status.ContainerStatuses[0].State.Terminated.Message), Code: int(rc), } default: return fmt.Errorf("pod %s/%s left in phase %s", pod.Namespace, pod.Name, pod.Status.Phase) } } if runObject != nil { if err := o.PrintObj(runObject.Object); err != nil { return err } } return utilerrors.NewAggregate(allErrs) } func (o *RunOptions) removeCreatedObjects(f cmdutil.Factory, createdObjects []*RunObject) error { for _, obj := range createdObjects { namespace, err := metadataAccessor.Namespace(obj.Object) if err != nil { return err } var name string name, err = metadataAccessor.Name(obj.Object) if err != nil { return err } r := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). ContinueOnError(). NamespaceParam(namespace).DefaultNamespace(). ResourceNames(obj.Mapping.Resource.Resource+"."+obj.Mapping.Resource.Group, name). Flatten(). Do() if err := o.DeleteOptions.DeleteResult(r); err != nil { return err } } return nil } // waitForPod watches the given pod until the exitCondition is true func waitForPod(podClient corev1client.PodsGetter, ns, name string, timeout time.Duration, exitCondition watchtools.ConditionFunc) (*corev1.Pod, error) { ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout) defer cancel() fieldSelector := fields.OneTermEqualSelector("metadata.name", name).String() lw := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector return podClient.Pods(ns).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { options.FieldSelector = fieldSelector return podClient.Pods(ns).Watch(context.TODO(), options) }, } intr := interrupt.New(nil, cancel) var result *corev1.Pod err := intr.Run(func() error { ev, err := watchtools.UntilWithSync(ctx, lw, &corev1.Pod{}, nil, exitCondition) if ev != nil { result = ev.Object.(*corev1.Pod) } return err }) return result, err } func handleAttachPod(f cmdutil.Factory, podClient corev1client.PodsGetter, ns, name string, opts *attach.AttachOptions) error { pod, err := waitForPod(podClient, ns, name, opts.GetPodTimeout, podRunningAndReady) if err != nil && err != ErrPodCompleted { return err } if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { return logOpts(f, pod, opts) } opts.Pod = pod opts.PodName = name opts.Namespace = ns if opts.AttachFunc == nil { opts.AttachFunc = attach.DefaultAttachFunc } if err := opts.Run(); err != nil { fmt.Fprintf(opts.ErrOut, "warning: couldn't attach to pod/%s, falling back to streaming logs: %v\n", name, err) return logOpts(f, pod, opts) } return nil } // logOpts logs output from opts to the pods log. func logOpts(restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Pod, opts *attach.AttachOptions) error { ctrName, err := opts.GetContainerName(pod) if err != nil { return err } requests, err := polymorphichelpers.LogsForObjectFn(restClientGetter, pod, &corev1.PodLogOptions{Container: ctrName}, opts.GetPodTimeout, false) if err != nil { return err } for _, request := range requests { if err := logs.DefaultConsumeRequest(context.Background(), request, opts.Out); err != nil { return err } } return nil } func getRestartPolicy(cmd *cobra.Command, interactive bool) (corev1.RestartPolicy, error) { restart := cmdutil.GetFlagString(cmd, "restart") if len(restart) == 0 { if interactive { return corev1.RestartPolicyOnFailure, nil } return corev1.RestartPolicyAlways, nil } switch corev1.RestartPolicy(restart) { case corev1.RestartPolicyAlways: return corev1.RestartPolicyAlways, nil case corev1.RestartPolicyOnFailure: return corev1.RestartPolicyOnFailure, nil case corev1.RestartPolicyNever: return corev1.RestartPolicyNever, nil } return "", cmdutil.UsageErrorf(cmd, "invalid restart policy: %s", restart) } func verifyImagePullPolicy(cmd *cobra.Command) error { pullPolicy := cmdutil.GetFlagString(cmd, "image-pull-policy") switch corev1.PullPolicy(pullPolicy) { case corev1.PullAlways, corev1.PullIfNotPresent, corev1.PullNever: return nil case "": return nil } return cmdutil.UsageErrorf(cmd, "invalid image pull policy: %s", pullPolicy) } func (o *RunOptions) generateService(f cmdutil.Factory, cmd *cobra.Command, paramsIn map[string]interface{}) (*RunObject, error) { generators := generateversioned.GeneratorFn("expose") generator, found := generators[generateversioned.ServiceV2GeneratorName] if !found { return nil, fmt.Errorf("missing service generator: %s", generateversioned.ServiceV2GeneratorName) } names := generator.ParamNames() params := map[string]interface{}{} for key, value := range paramsIn { _, isString := value.(string) if isString { params[key] = value } } name, found := params["name"] if !found || len(name.(string)) == 0 { return nil, fmt.Errorf("name is a required parameter") } selector, found := params["labels"] if !found || len(selector.(string)) == 0 { selector = fmt.Sprintf("run=%s", name.(string)) } params["selector"] = selector if defaultName, found := params["default-name"]; !found || len(defaultName.(string)) == 0 { params["default-name"] = name } runObject, err := o.createGeneratedObject(f, cmd, generator, names, params, nil) if err != nil { return nil, err } if err := o.PrintObj(runObject.Object); err != nil { return nil, err } // separate yaml objects if o.PrintFlags.OutputFormat != nil && *o.PrintFlags.OutputFormat == "yaml" { fmt.Fprintln(o.Out, "---") } return runObject, nil } func (o *RunOptions) createGeneratedObject(f cmdutil.Factory, cmd *cobra.Command, generator generate.Generator, names []generate.GeneratorParam, params map[string]interface{}, overrider *cmdutil.Overrider) (*RunObject, error) { err := generate.ValidateParams(names, params) if err != nil { return nil, err } // TODO: Validate flag usage against selected generator. More tricky since --expose was added. obj, err := generator.Generate(params) if err != nil { return nil, err } mapper, err := f.ToRESTMapper() if err != nil { return nil, err } // run has compiled knowledge of the thing is creating gvks, _, err := scheme.Scheme.ObjectKinds(obj) if err != nil { return nil, err } mapping, err := mapper.RESTMapping(gvks[0].GroupKind(), gvks[0].Version) if err != nil { return nil, err } if overrider != nil { obj, err = overrider.Apply(obj) if err != nil { return nil, err } } if err := o.Recorder.Record(obj); err != nil { klog.V(4).Infof("error recording current command: %v", err) } actualObj := obj if o.DryRunStrategy != cmdutil.DryRunClient { if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), obj, scheme.DefaultJSONEncoder()); err != nil { return nil, err } client, err := f.ClientForMapping(mapping) if err != nil { return nil, err } actualObj, err = resource. NewHelper(client, mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Create(o.Namespace, false, obj) if err != nil { return nil, err } } else { if meta, err := meta.Accessor(actualObj); err == nil && o.EnforceNamespace { meta.SetNamespace(o.Namespace) } } return &RunObject{ Object: actualObj, Mapping: mapping, }, nil } // ErrPodCompleted is returned by PodRunning or PodContainerRunning to indicate that // the pod has already reached completed state. var ErrPodCompleted = fmt.Errorf("pod ran to completion") // podCompleted returns true if the pod has run to completion, false if the pod has not yet // reached running state, or an error in any other case. func podCompleted(event watch.Event) (bool, error) { switch event.Type { case watch.Deleted: return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") } switch t := event.Object.(type) { case *corev1.Pod: switch t.Status.Phase { case corev1.PodFailed, corev1.PodSucceeded: return true, nil } } return false, nil } // podSucceeded returns true if the pod has run to completion, false if the pod has not yet // reached running state, or an error in any other case. func podSucceeded(event watch.Event) (bool, error) { switch event.Type { case watch.Deleted: return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") } switch t := event.Object.(type) { case *corev1.Pod: return t.Status.Phase == corev1.PodSucceeded, nil } return false, nil } // podRunningAndReady returns true if the pod is running and ready, false if the pod has not // yet reached those states, returns ErrPodCompleted if the pod has run to completion, or // an error in any other case. func podRunningAndReady(event watch.Event) (bool, error) { switch event.Type { case watch.Deleted: return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") } switch t := event.Object.(type) { case *corev1.Pod: switch t.Status.Phase { case corev1.PodFailed, corev1.PodSucceeded: return false, ErrPodCompleted case corev1.PodRunning: conditions := t.Status.Conditions if conditions == nil { return false, nil } for i := range conditions { if conditions[i].Type == corev1.PodReady && conditions[i].Status == corev1.ConditionTrue { return true, nil } } } } return false, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/run/run_test.go000066400000000000000000000500471504711711200276130ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package run import ( "bytes" "fmt" "io" "net/http" "os" "reflect" "strconv" "strings" "testing" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" "k8s.io/kubectl/pkg/cmd/delete" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" ) func TestGetRestartPolicy(t *testing.T) { tests := []struct { input string interactive bool expected corev1.RestartPolicy expectErr bool }{ { input: "", expected: corev1.RestartPolicyAlways, }, { input: "", interactive: true, expected: corev1.RestartPolicyOnFailure, }, { input: string(corev1.RestartPolicyAlways), interactive: true, expected: corev1.RestartPolicyAlways, }, { input: string(corev1.RestartPolicyNever), interactive: true, expected: corev1.RestartPolicyNever, }, { input: string(corev1.RestartPolicyAlways), expected: corev1.RestartPolicyAlways, }, { input: string(corev1.RestartPolicyNever), expected: corev1.RestartPolicyNever, }, { input: "foo", expectErr: true, }, } for _, test := range tests { cmd := &cobra.Command{} cmd.Flags().String("restart", "", i18n.T("dummy restart flag)")) cmd.Flags().Lookup("restart").Value.Set(test.input) policy, err := getRestartPolicy(cmd, test.interactive) if test.expectErr && err == nil { t.Error("unexpected non-error") } if !test.expectErr && err != nil { t.Errorf("unexpected error: %v", err) } if !test.expectErr && policy != test.expected { t.Errorf("expected: %s, saw: %s (%s:%v)", test.expected, policy, test.input, test.interactive) } } } func TestGetEnv(t *testing.T) { test := struct { input []string expected []string }{ input: []string{"a=b", "c=d"}, expected: []string{"a=b", "c=d"}, } cmd := &cobra.Command{} cmd.Flags().StringSlice("env", test.input, "") envStrings := cmdutil.GetFlagStringSlice(cmd, "env") if len(envStrings) != 2 || !reflect.DeepEqual(envStrings, test.expected) { t.Errorf("expected: %s, saw: %s", test.expected, envStrings) } } func TestRunArgsFollowDashRules(t *testing.T) { one := int32(1) rc := &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{Name: "rc1", Namespace: "test", ResourceVersion: "18"}, Spec: corev1.ReplicationControllerSpec{ Replicas: &one, }, } tests := []struct { args []string argsLenAtDash int expectError bool name string }{ { args: []string{}, argsLenAtDash: -1, expectError: true, name: "empty", }, { args: []string{"foo"}, argsLenAtDash: -1, expectError: false, name: "no cmd", }, { args: []string{"foo", "sleep"}, argsLenAtDash: -1, expectError: false, name: "cmd no dash", }, { args: []string{"foo", "sleep"}, argsLenAtDash: 1, expectError: false, name: "cmd has dash", }, { args: []string{"foo", "sleep"}, argsLenAtDash: 0, expectError: true, name: "no name", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ GroupVersion: corev1.SchemeGroupVersion, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { if req.URL.Path == "/namespaces/test/pods" { return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, rc)}, nil } return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer([]byte("{}"))), }, nil }), } tf.ClientConfigVal = &restclient.Config{} cmd := NewCmdRun(tf, genericiooptions.NewTestIOStreamsDiscard()) cmd.Flags().Set("image", "nginx") printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme) printer, err := printFlags.ToPrinter() if err != nil { t.Errorf("unexpected error: %v", err) return } deleteFlags := delete.NewDeleteFlags("to use to replace the resource.") deleteOptions, err := deleteFlags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard()) if err != nil { t.Errorf("unexpected error: %v", err) return } opts := &RunOptions{ PrintFlags: printFlags, DeleteOptions: deleteOptions, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), Image: "nginx", PrintObj: func(obj runtime.Object) error { return printer.PrintObj(obj, os.Stdout) }, Recorder: genericclioptions.NoopRecorder{}, ArgsLenAtDash: test.argsLenAtDash, } err = opts.Run(tf, cmd, test.args) if test.expectError && err == nil { t.Errorf("unexpected non-error (%s)", test.name) } if !test.expectError && err != nil { t.Errorf("unexpected error: %v (%s)", err, test.name) } }) } } func TestGenerateService(t *testing.T) { tests := []struct { name string port string args []string params map[string]interface{} expectErr bool service corev1.Service expectPOST bool }{ { name: "basic", port: "80", args: []string{"foo"}, params: map[string]interface{}{ "name": "foo", }, expectErr: false, service: corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(80), }, }, Selector: map[string]string{ "run": "foo", }, }, }, expectPOST: true, }, { name: "custom labels", port: "80", args: []string{"foo"}, params: map[string]interface{}{ "name": "foo", "labels": "app=bar", }, expectErr: false, service: corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", Labels: map[string]string{"app": "bar"}, }, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 80, Protocol: "TCP", TargetPort: intstr.FromInt32(80), }, }, Selector: map[string]string{ "app": "bar", }, }, }, expectPOST: true, }, { expectErr: true, name: "missing port", expectPOST: false, }, { name: "dry-run", port: "80", args: []string{"foo"}, params: map[string]interface{}{ "name": "foo", }, expectErr: false, expectPOST: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { sawPOST := false tf := cmdtesting.NewTestFactory() defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.Client = &fake.RESTClient{ GroupVersion: corev1.SchemeGroupVersion, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case test.expectPOST && m == "POST" && p == "/namespaces/test/services": sawPOST = true body := cmdtesting.ObjBody(codec, &test.service) data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } defer req.Body.Close() svc := &corev1.Service{} if err := runtime.DecodeInto(codec, data, svc); err != nil { t.Fatalf("unexpected error: %v", err) } // Copy things that are defaulted by the system test.service.Annotations = svc.Annotations if !apiequality.Semantic.DeepEqual(&test.service, svc) { t.Errorf("expected:\n%v\nsaw:\n%v\n", &test.service, svc) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme) printer, err := printFlags.ToPrinter() if err != nil { t.Errorf("unexpected error: %v", err) return } ioStreams, _, buff, _ := genericiooptions.NewTestIOStreams() deleteFlags := delete.NewDeleteFlags("to use to replace the resource.") deleteOptions, err := deleteFlags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard()) if err != nil { t.Errorf("unexpected error: %v", err) return } opts := &RunOptions{ PrintFlags: printFlags, DeleteOptions: deleteOptions, IOStreams: ioStreams, Port: test.port, Recorder: genericclioptions.NoopRecorder{}, PrintObj: func(obj runtime.Object) error { return printer.PrintObj(obj, buff) }, Namespace: "test", } cmd := &cobra.Command{} cmd.Flags().Bool(cmdutil.ApplyAnnotationsFlag, false, "") cmd.Flags().Bool("record", false, "Record current kubectl command in the resource annotation. If set to false, do not record the command. If set to true, record the command. If not set, default to updating the existing annotation value only if one already exists.") addRunFlags(cmd, opts) if !test.expectPOST { opts.DryRunStrategy = cmdutil.DryRunClient } if len(test.port) > 0 { cmd.Flags().Set("port", test.port) test.params["port"] = test.port } _, err = opts.generateService(tf, cmd, test.params) if test.expectErr { if err == nil { t.Error("unexpected non-error") } return } if err != nil { t.Errorf("unexpected error: %v", err) } if test.expectPOST != sawPOST { t.Errorf("expectPost: %v, sawPost: %v", test.expectPOST, sawPOST) } }) } } func TestRunValidations(t *testing.T) { tests := []struct { name string args []string flags map[string]string expectedErr string }{ { name: "test missing name error", expectedErr: "NAME is required", }, { name: "test missing --image error", args: []string{"test"}, expectedErr: "--image is required", }, { name: "test invalid image name error", args: []string{"test"}, flags: map[string]string{ "image": "#", }, expectedErr: "Invalid image name", }, { name: "test rm errors when used on non-attached containers", args: []string{"test"}, flags: map[string]string{ "image": "busybox", "rm": "true", }, expectedErr: "rm should only be used for attached containers", }, { name: "test error on attached containers options", args: []string{"test"}, flags: map[string]string{ "image": "busybox", "attach": "true", "dry-run": "client", }, expectedErr: "can't be used with attached containers options", }, { name: "test error on attached containers options, with value from stdin", args: []string{"test"}, flags: map[string]string{ "image": "busybox", "stdin": "true", "dry-run": "client", }, expectedErr: "can't be used with attached containers options", }, { name: "test error on attached containers options, with value from stdin and tty", args: []string{"test"}, flags: map[string]string{ "image": "busybox", "tty": "true", "stdin": "true", "dry-run": "client", }, expectedErr: "can't be used with attached containers options", }, { name: "test error when tty=true and no stdin provided", args: []string{"test"}, flags: map[string]string{ "image": "busybox", "tty": "true", }, expectedErr: "stdin is required for containers with -t/--tty", }, { name: "test invalid override type error", args: []string{"test"}, flags: map[string]string{ "image": "busybox", "overrides": "{}", "override-type": "foo", }, expectedErr: "invalid override type: foo", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() _, _, codec := cmdtesting.NewExternalScheme() ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, cmdtesting.NewInternalType("", "", ""))}, } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, _, bufErr := genericiooptions.NewTestIOStreams() cmdutil.BehaviorOnFatal(func(str string, code int) { bufErr.Write([]byte(str)) }) cmd := NewCmdRun(tf, streams) for flagName, flagValue := range test.flags { cmd.Flags().Set(flagName, flagValue) } cmd.Run(cmd, test.args) var err error if bufErr.Len() > 0 { err = fmt.Errorf("%v", bufErr.String()) } if err != nil && len(test.expectedErr) > 0 { if !strings.Contains(err.Error(), test.expectedErr) { t.Errorf("unexpected error: %v", err) } } }) } } func TestExpose(t *testing.T) { tests := []struct { name string podName string imageName string podLabels map[string]string port int }{ { name: "test simple expose", podName: "test-pod", imageName: "test-image", podLabels: map[string]string{"color": "red", "shape": "square"}, port: 1234, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Logf("path: %v, method: %v", req.URL.Path, req.Method) switch p, m := req.URL.Path, req.Method; { case m == "POST" && p == "/namespaces/test/pods": pod := &corev1.Pod{} body := cmdtesting.ObjBody(codec, pod) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil case m == "POST" && p == "/namespaces/test/services": data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } service := &corev1.Service{} if err := runtime.DecodeInto(codec, data, service); err != nil { t.Fatalf("unexpected error: %v", err) } if service.ObjectMeta.Name != test.podName { t.Errorf("Invalid name on service. Expected:%v, Actual:%v", test.podName, service.ObjectMeta.Name) } if !reflect.DeepEqual(service.Spec.Selector, test.podLabels) { t.Errorf("Invalid selector on service. Expected:%v, Actual:%v", test.podLabels, service.Spec.Selector) } if len(service.Spec.Ports) != 1 && service.Spec.Ports[0].Port != int32(test.port) { t.Errorf("Invalid port on service: %v", service.Spec.Ports) } body := cmdtesting.ObjBody(codec, service) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Errorf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } streams, _, _, bufErr := genericiooptions.NewTestIOStreams() cmdutil.BehaviorOnFatal(func(str string, code int) { bufErr.Write([]byte(str)) }) cmd := NewCmdRun(tf, streams) cmd.Flags().Set("image", test.imageName) cmd.Flags().Set("expose", "true") cmd.Flags().Set("port", strconv.Itoa(test.port)) labels := []string{} for k, v := range test.podLabels { labels = append(labels, fmt.Sprintf("%s=%s", k, v)) } cmd.Flags().Set("labels", strings.Join(labels, ",")) cmd.Run(cmd, []string{test.podName}) if bufErr.Len() > 0 { err := fmt.Errorf("%v", bufErr.String()) if err != nil { t.Errorf("unexpected error: %v", err) } } }) } } func TestRunOverride(t *testing.T) { tests := []struct { name string overrides string overrideType string expectedOutput string }{ { name: "run with merge override type should replace spec", overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`, overrideType: "merge", expectedOutput: `apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: test name: test namespace: ns spec: containers: - name: test resources: limits: cpu: 200m dnsPolicy: ClusterFirst restartPolicy: Always status: {} `, }, { name: "run with no override type specified, should perform an RFC7396 JSON Merge Patch", overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`, overrideType: "", expectedOutput: `apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: test name: test namespace: ns spec: containers: - name: test resources: limits: cpu: 200m dnsPolicy: ClusterFirst restartPolicy: Always status: {} `, }, { name: "run with strategic override type should merge spec, preserving container image", overrides: `{"spec":{"containers":[{"name":"test","resources":{"limits":{"cpu":"200m"}}}]}}`, overrideType: "strategic", expectedOutput: `apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: test name: test namespace: ns spec: containers: - image: busybox name: test resources: limits: cpu: 200m dnsPolicy: ClusterFirst restartPolicy: Always status: {} `, }, { name: "run with json override type should perform add, replace, and remove operations", overrides: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"}, {"op": "replace", "path": "/spec/containers/0/resources", "value": {"limits": {"cpu": "200m"}}}, {"op": "remove", "path": "/spec/dnsPolicy"} ]`, overrideType: "json", expectedOutput: `apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: foo: bar run: test name: test namespace: ns spec: containers: - image: busybox name: test resources: limits: cpu: 200m restartPolicy: Always status: {} `, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("ns") defer tf.Cleanup() streams, _, bufOut, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdRun(tf, streams) cmd.Flags().Set("dry-run", "client") cmd.Flags().Set("output", "yaml") cmd.Flags().Set("image", "busybox") cmd.Flags().Set("overrides", test.overrides) cmd.Flags().Set("override-type", test.overrideType) cmd.Run(cmd, []string{"test"}) actualOutput := bufOut.String() if actualOutput != test.expectedOutput { t.Errorf("unexpected output.\n\nExpected:\n%v\nActual:\n%v", test.expectedOutput, actualOutput) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/scale/000077500000000000000000000000001504711711200256765ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/scale/scale.go000066400000000000000000000224641504711711200273240ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package scale import ( "fmt" "time" "github.com/spf13/cobra" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scale" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( scaleLong = templates.LongDesc(i18n.T(` Set a new size for a deployment, replica set, replication controller, or stateful set. Scale also allows users to specify one or more preconditions for the scale action. If --current-replicas or --resource-version is specified, it is validated before the scale is attempted, and it is guaranteed that the precondition holds true when the scale is sent to the server.`)) scaleExample = templates.Examples(i18n.T(` # Scale a replica set named 'foo' to 3 kubectl scale --replicas=3 rs/foo # Scale a resource identified by type and name specified in "foo.yaml" to 3 kubectl scale --replicas=3 -f foo.yaml # If the deployment named mysql's current size is 2, scale mysql to 3 kubectl scale --current-replicas=2 --replicas=3 deployment/mysql # Scale multiple replication controllers kubectl scale --replicas=5 rc/example1 rc/example2 rc/example3 # Scale stateful set named 'web' to 3 kubectl scale --replicas=3 statefulset/web`)) ) type ScaleOptions struct { FilenameOptions resource.FilenameOptions RecordFlags *genericclioptions.RecordFlags PrintFlags *genericclioptions.PrintFlags PrintObj printers.ResourcePrinterFunc Selector string All bool Replicas int ResourceVersion string CurrentReplicas int Timeout time.Duration Recorder genericclioptions.Recorder builder *resource.Builder namespace string enforceNamespace bool args []string shortOutput bool clientSet kubernetes.Interface scaler scale.Scaler unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) parent string dryRunStrategy cmdutil.DryRunStrategy genericiooptions.IOStreams } func NewScaleOptions(ioStreams genericiooptions.IOStreams) *ScaleOptions { return &ScaleOptions{ PrintFlags: genericclioptions.NewPrintFlags("scaled"), RecordFlags: genericclioptions.NewRecordFlags(), CurrentReplicas: -1, Recorder: genericclioptions.NoopRecorder{}, IOStreams: ioStreams, } } // NewCmdScale returns a cobra command with the appropriate configuration and flags to run scale func NewCmdScale(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewScaleOptions(ioStreams) validArgs := []string{"deployment", "replicaset", "replicationcontroller", "statefulset"} cmd := &cobra.Command{ Use: "scale [--resource-version=version] [--current-replicas=count] --replicas=COUNT (-f FILENAME | TYPE NAME)", DisableFlagsInUseLine: true, Short: i18n.T("Set a new size for a deployment, replica set, or replication controller"), Long: scaleLong, Example: scaleExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunScale()) }, } o.RecordFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources in the namespace of the specified resource types") cmd.Flags().StringVar(&o.ResourceVersion, "resource-version", o.ResourceVersion, i18n.T("Precondition for resource version. Requires that the current resource version match this value in order to scale.")) cmd.Flags().IntVar(&o.CurrentReplicas, "current-replicas", o.CurrentReplicas, "Precondition for current size. Requires that the current size of the resource match this value in order to scale. -1 (default) for no condition.") cmd.Flags().IntVar(&o.Replicas, "replicas", o.Replicas, "The new desired number of replicas. Required.") cmd.MarkFlagRequired("replicas") cmd.Flags().DurationVar(&o.Timeout, "timeout", 0, "The length of time to wait before giving up on a scale operation, zero means don't wait. Any other values should contain a corresponding time unit (e.g. 1s, 2m, 3h).") cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to set a new size") cmdutil.AddDryRunFlag(cmd) cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) return cmd } func (o *ScaleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.builder = f.NewBuilder() o.args = args o.shortOutput = cmdutil.GetFlagString(cmd, "output") == "name" o.clientSet, err = f.KubernetesClientSet() if err != nil { return err } o.scaler, err = scaler(f) if err != nil { return err } o.unstructuredClientForMapping = f.UnstructuredClientForMapping o.parent = cmd.Parent().Name() return nil } func (o *ScaleOptions) Validate() error { if o.Replicas < 0 { return fmt.Errorf("The --replicas=COUNT flag is required, and COUNT must be greater than or equal to 0") } if o.CurrentReplicas < -1 { return fmt.Errorf("The --current-replicas must specify an integer of -1 or greater") } return nil } // RunScale executes the scaling func (o *ScaleOptions) RunScale() error { r := o.builder. Unstructured(). ContinueOnError(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, &o.FilenameOptions). ResourceTypeOrNameArgs(o.All, o.args...). Flatten(). LabelSelectorParam(o.Selector). Do() err := r.Err() if err != nil { return err } // We don't immediately return infoErr if it is not nil. // Because we want to proceed for other valid resources and // at the end of the function, we'll return this // to show invalid resources to the user. infos, infoErr := r.Infos() if len(o.ResourceVersion) != 0 && len(infos) > 1 { return fmt.Errorf("cannot use --resource-version with multiple resources") } // only set a precondition if the user has requested one. A nil precondition means we can do a blind update, so // we avoid a Scale GET that may or may not succeed var precondition *scale.ScalePrecondition if o.CurrentReplicas != -1 || len(o.ResourceVersion) > 0 { precondition = &scale.ScalePrecondition{Size: o.CurrentReplicas, ResourceVersion: o.ResourceVersion} } retry := scale.NewRetryParams(1*time.Second, 5*time.Minute) var waitForReplicas *scale.RetryParams if o.Timeout != 0 && o.dryRunStrategy == cmdutil.DryRunNone { waitForReplicas = scale.NewRetryParams(1*time.Second, o.Timeout) } if len(infos) == 0 { if infoErr != nil { return fmt.Errorf("no objects passed to scale %w", infoErr) } return fmt.Errorf("no objects passed to scale") } for _, info := range infos { mapping := info.ResourceMapping() if o.dryRunStrategy == cmdutil.DryRunClient { if err := o.PrintObj(info.Object, o.Out); err != nil { return err } continue } if err := o.scaler.Scale(info.Namespace, info.Name, uint(o.Replicas), precondition, retry, waitForReplicas, mapping.Resource, o.dryRunStrategy == cmdutil.DryRunServer); err != nil { return err } // if the recorder makes a change, compute and create another patch if mergePatch, err := o.Recorder.MakeRecordMergePatch(info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } else if len(mergePatch) > 0 { client, err := o.unstructuredClientForMapping(mapping) if err != nil { return err } helper := resource.NewHelper(client, mapping) if _, err := helper.Patch(info.Namespace, info.Name, types.MergePatchType, mergePatch, nil); err != nil { klog.V(4).Infof("error recording reason: %v", err) } } err := o.PrintObj(info.Object, o.Out) if err != nil { return err } } return infoErr } func scaler(f cmdutil.Factory) (scale.Scaler, error) { scalesGetter, err := cmdutil.ScaleClientFn(f) if err != nil { return nil, err } return scale.NewScaler(scalesGetter), nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/000077500000000000000000000000001504711711200254025ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/env/000077500000000000000000000000001504711711200261725ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/env/doc.go000066400000000000000000000012341504711711200272660ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package env provides functions to incorporate environment variables into set env. package env kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse.go000066400000000000000000000105611504711711200305060ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package env import ( "bufio" "fmt" "io" "regexp" "strings" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" ) var argumentEnvironment = regexp.MustCompile("(?ms)^(.+)\\=(.*)$") // IsEnvironmentArgument checks whether a string is an environment argument, that is, whether it matches the "anycharacters=anycharacters" pattern. func IsEnvironmentArgument(s string) bool { return argumentEnvironment.MatchString(s) } // SplitEnvironmentFromResources separates resources from environment arguments. // Resources must come first. Arguments may have the "DASH-" syntax. func SplitEnvironmentFromResources(args []string) (resources, envArgs []string, ok bool) { first := true for _, s := range args { // this method also has to understand env removal syntax, i.e. KEY- isEnv := IsEnvironmentArgument(s) || strings.HasSuffix(s, "-") switch { case first && isEnv: first = false fallthrough case !first && isEnv: envArgs = append(envArgs, s) case first && !isEnv: resources = append(resources, s) case !first && !isEnv: return nil, nil, false } } return resources, envArgs, true } // parseIntoEnvVar parses the list of key-value pairs into kubernetes EnvVar. // envVarType is for making errors more specific to user intentions. func parseIntoEnvVar(spec []string, defaultReader io.Reader, envVarType string) ([]v1.EnvVar, []string, bool, error) { env := []v1.EnvVar{} exists := sets.New[string]() var remove []string usedStdin := false for _, envSpec := range spec { switch { case envSpec == "-": if defaultReader == nil { return nil, nil, usedStdin, fmt.Errorf("when '-' is used, STDIN must be open") } fileEnv, err := readEnv(defaultReader, envVarType) if err != nil { return nil, nil, usedStdin, err } env = append(env, fileEnv...) usedStdin = true case strings.Contains(envSpec, "="): parts := strings.SplitN(envSpec, "=", 2) if len(parts) != 2 { return nil, nil, usedStdin, fmt.Errorf("invalid %s: %v", envVarType, envSpec) } if errs := validation.IsEnvVarName(parts[0]); len(errs) != 0 { return nil, nil, usedStdin, fmt.Errorf("%q is not a valid key name: %s", parts[0], strings.Join(errs, ";")) } exists.Insert(parts[0]) env = append(env, v1.EnvVar{ Name: parts[0], Value: parts[1], }) case strings.HasSuffix(envSpec, "-"): remove = append(remove, envSpec[:len(envSpec)-1]) default: return nil, nil, usedStdin, fmt.Errorf("unknown %s: %v", envVarType, envSpec) } } for _, removeLabel := range remove { if _, found := exists[removeLabel]; found { return nil, nil, usedStdin, fmt.Errorf("can not both modify and remove the same %s in the same command", envVarType) } } return env, remove, usedStdin, nil } // ParseEnv parses the elements of the first argument looking for environment variables in key=value form and, if one of those values is "-", it also scans the reader and returns true for its third return value. // The same environment variable cannot be both modified and removed in the same command. func ParseEnv(spec []string, defaultReader io.Reader) ([]v1.EnvVar, []string, bool, error) { return parseIntoEnvVar(spec, defaultReader, "environment variable") } func readEnv(r io.Reader, envVarType string) ([]v1.EnvVar, error) { env := []v1.EnvVar{} scanner := bufio.NewScanner(r) for scanner.Scan() { envSpec := scanner.Text() if pos := strings.Index(envSpec, "#"); pos != -1 { envSpec = envSpec[:pos] } if strings.Contains(envSpec, "=") { parts := strings.SplitN(envSpec, "=", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid %s: %v", envVarType, envSpec) } env = append(env, v1.EnvVar{ Name: parts[0], Value: parts[1], }) } } if err := scanner.Err(); err != nil && err != io.EOF { return nil, err } return env, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse_test.go000066400000000000000000000062341504711711200315470ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package env import ( "fmt" "io" "strings" ) func ExampleIsEnvironmentArgument_true() { test := "returns=true" fmt.Println(IsEnvironmentArgument(test)) // Output: true } func ExampleIsEnvironmentArgument_false() { test := "returnsfalse" fmt.Println(IsEnvironmentArgument(test)) // Output: false } func ExampleSplitEnvironmentFromResources() { args := []string{`resource`, "ENV\\=ARG", `ONE\=MORE`, `DASH-`} fmt.Println(SplitEnvironmentFromResources(args)) // Output: [resource] [ENV\=ARG ONE\=MORE DASH-] true } func ExampleParseEnv_good_with_stdin() { r := strings.NewReader("FROM=READER") ss := []string{"ENV=VARIABLE", "ENV.TEST=VARIABLE", "AND=ANOTHER", "REMOVE-", "-"} fmt.Println(ParseEnv(ss, r)) // Output: // [{ENV VARIABLE nil} {ENV.TEST VARIABLE nil} {AND ANOTHER nil} {FROM READER nil}] [REMOVE] true } func ExampleParseEnv_good_with_stdin_and_error() { r := strings.NewReader("FROM=READER") ss := []string{"-", "This not in the key=value format."} fmt.Println(ParseEnv(ss, r)) // Output: // [] [] true "This not in the key" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*') } func ExampleParseEnv_good_without_stdin() { ss := []string{"ENV=VARIABLE", "ENV.TEST=VARIABLE", "AND=ANOTHER", "REMOVE-"} fmt.Println(ParseEnv(ss, nil)) // Output: // [{ENV VARIABLE nil} {ENV.TEST VARIABLE nil} {AND ANOTHER nil}] [REMOVE] false } func ExampleParseEnv_bad_first() { var r io.Reader bad := []string{"This not in the key=value format."} fmt.Println(ParseEnv(bad, r)) // Output: // [] [] false "This not in the key" is not a valid key name: a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1', regex used for validation is '[-._a-zA-Z][-._a-zA-Z0-9]*') } func ExampleParseEnv_bad_second() { var r io.Reader bad := []string{".=VARIABLE"} fmt.Println(ParseEnv(bad, r)) // Output: // [] [] false "." is not a valid key name: must not be '.' } func ExampleParseEnv_bad_third() { var r io.Reader bad := []string{"..=VARIABLE"} fmt.Println(ParseEnv(bad, r)) // Output: // [] [] false ".." is not a valid key name: must not be '..' } func ExampleParseEnv_bad_fourth() { var r io.Reader bad := []string{"..ENV=VARIABLE"} fmt.Println(ParseEnv(bad, r)) // Output: // [] [] false "..ENV" is not a valid key name: must not start with '..' } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_resolve.go000066400000000000000000000256721504711711200310640ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package env import ( "context" "fmt" "math" "strconv" "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/client-go/kubernetes" ) // ResourceStore defines a new resource store data structure. type ResourceStore struct { SecretStore map[string]*corev1.Secret ConfigMapStore map[string]*corev1.ConfigMap } // NewResourceStore returns a pointer to a new resource store data structure. func NewResourceStore() *ResourceStore { return &ResourceStore{ SecretStore: make(map[string]*corev1.Secret), ConfigMapStore: make(map[string]*corev1.ConfigMap), } } // getSecretRefValue returns the value of a secret in the supplied namespace func getSecretRefValue(client kubernetes.Interface, namespace string, store *ResourceStore, secretSelector *corev1.SecretKeySelector) (string, error) { secret, ok := store.SecretStore[secretSelector.Name] if !ok { var err error secret, err = client.CoreV1().Secrets(namespace).Get(context.TODO(), secretSelector.Name, metav1.GetOptions{}) if err != nil { return "", err } store.SecretStore[secretSelector.Name] = secret } if data, ok := secret.Data[secretSelector.Key]; ok { return string(data), nil } return "", fmt.Errorf("key %s not found in secret %s", secretSelector.Key, secretSelector.Name) } // getConfigMapRefValue returns the value of a configmap in the supplied namespace func getConfigMapRefValue(client kubernetes.Interface, namespace string, store *ResourceStore, configMapSelector *corev1.ConfigMapKeySelector) (string, error) { configMap, ok := store.ConfigMapStore[configMapSelector.Name] if !ok { var err error configMap, err = client.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapSelector.Name, metav1.GetOptions{}) if err != nil { return "", err } store.ConfigMapStore[configMapSelector.Name] = configMap } if data, ok := configMap.Data[configMapSelector.Key]; ok { return string(data), nil } return "", fmt.Errorf("key %s not found in config map %s", configMapSelector.Key, configMapSelector.Name) } // getFieldRef returns the value of the supplied path in the given object func getFieldRef(obj runtime.Object, from *corev1.EnvVarSource) (string, error) { return extractFieldPathAsString(obj, from.FieldRef.FieldPath) } // extractFieldPathAsString extracts the field from the given object // and returns it as a string. The object must be a pointer to an // API type. func extractFieldPathAsString(obj interface{}, fieldPath string) (string, error) { accessor, err := meta.Accessor(obj) if err != nil { return "", nil } if path, subscript, ok := splitMaybeSubscriptedPath(fieldPath); ok { switch path { case "metadata.annotations": if errs := validation.IsQualifiedName(strings.ToLower(subscript)); len(errs) != 0 { return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";")) } return accessor.GetAnnotations()[subscript], nil case "metadata.labels": if errs := validation.IsQualifiedName(subscript); len(errs) != 0 { return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";")) } return accessor.GetLabels()[subscript], nil default: return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath) } } switch fieldPath { case "metadata.annotations": return formatMap(accessor.GetAnnotations()), nil case "metadata.labels": return formatMap(accessor.GetLabels()), nil case "metadata.name": return accessor.GetName(), nil case "metadata.namespace": return accessor.GetNamespace(), nil case "metadata.uid": return string(accessor.GetUID()), nil } return "", fmt.Errorf("unsupported fieldPath: %v", fieldPath) } // splitMaybeSubscriptedPath checks whether the specified fieldPath is // subscripted, and // - if yes, this function splits the fieldPath into path and subscript, and // returns (path, subscript, true). // - if no, this function returns (fieldPath, "", false). // // Example inputs and outputs: // - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey", true) // - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c", true) // - "metadata.labels[”]" --> ("metadata.labels", "", true) // - "metadata.labels" --> ("metadata.labels", "", false) func splitMaybeSubscriptedPath(fieldPath string) (string, string, bool) { if !strings.HasSuffix(fieldPath, "']") { return fieldPath, "", false } s := strings.TrimSuffix(fieldPath, "']") parts := strings.SplitN(s, "['", 2) if len(parts) < 2 { return fieldPath, "", false } if len(parts[0]) == 0 { return fieldPath, "", false } return parts[0], parts[1], true } // formatMap formats map[string]string to a string. func formatMap(m map[string]string) (fmtStr string) { // output with keys in sorted order to provide stable output keys := sets.New[string]() for key := range m { keys.Insert(key) } for _, key := range sets.List(keys) { fmtStr += fmt.Sprintf("%v=%q\n", key, m[key]) } fmtStr = strings.TrimSuffix(fmtStr, "\n") return } // getResourceFieldRef returns the value of a resource in the given container func getResourceFieldRef(from *corev1.EnvVarSource, container *corev1.Container) (string, error) { return extractContainerResourceValue(from.ResourceFieldRef, container) } // ExtractContainerResourceValue extracts the value of a resource // in an already known container func extractContainerResourceValue(fs *corev1.ResourceFieldSelector, container *corev1.Container) (string, error) { divisor := resource.Quantity{} if divisor.Cmp(fs.Divisor) == 0 { divisor = resource.MustParse("1") } else { divisor = fs.Divisor } switch fs.Resource { case "limits.cpu": return convertResourceCPUToString(container.Resources.Limits.Cpu(), divisor) case "limits.memory": return convertResourceMemoryToString(container.Resources.Limits.Memory(), divisor) case "limits.ephemeral-storage": return convertResourceEphemeralStorageToString(container.Resources.Limits.StorageEphemeral(), divisor) case "requests.cpu": return convertResourceCPUToString(container.Resources.Requests.Cpu(), divisor) case "requests.memory": return convertResourceMemoryToString(container.Resources.Requests.Memory(), divisor) case "requests.ephemeral-storage": return convertResourceEphemeralStorageToString(container.Resources.Requests.StorageEphemeral(), divisor) } // handle extended standard resources with dynamic names // example: requests.hugepages- or limits.hugepages- if strings.HasPrefix(fs.Resource, "requests.") { resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "requests.")) if IsHugePageResourceName(resourceName) { return convertResourceHugePagesToString(container.Resources.Requests.Name(resourceName, resource.BinarySI), divisor) } } if strings.HasPrefix(fs.Resource, "limits.") { resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "limits.")) if IsHugePageResourceName(resourceName) { return convertResourceHugePagesToString(container.Resources.Limits.Name(resourceName, resource.BinarySI), divisor) } } return "", fmt.Errorf("Unsupported container resource : %v", fs.Resource) } // convertResourceCPUToString converts cpu value to the format of divisor and returns // ceiling of the value. func convertResourceCPUToString(cpu *resource.Quantity, divisor resource.Quantity) (string, error) { c := int64(math.Ceil(float64(cpu.MilliValue()) / float64(divisor.MilliValue()))) return strconv.FormatInt(c, 10), nil } // convertResourceMemoryToString converts memory value to the format of divisor and returns // ceiling of the value. func convertResourceMemoryToString(memory *resource.Quantity, divisor resource.Quantity) (string, error) { m := int64(math.Ceil(float64(memory.Value()) / float64(divisor.Value()))) return strconv.FormatInt(m, 10), nil } // convertResourceHugePagesToString converts hugepages value to the format of divisor and returns // ceiling of the value. func convertResourceHugePagesToString(hugePages *resource.Quantity, divisor resource.Quantity) (string, error) { m := int64(math.Ceil(float64(hugePages.Value()) / float64(divisor.Value()))) return strconv.FormatInt(m, 10), nil } // convertResourceEphemeralStorageToString converts ephemeral storage value to the format of divisor and returns // ceiling of the value. func convertResourceEphemeralStorageToString(ephemeralStorage *resource.Quantity, divisor resource.Quantity) (string, error) { m := int64(math.Ceil(float64(ephemeralStorage.Value()) / float64(divisor.Value()))) return strconv.FormatInt(m, 10), nil } // GetEnvVarRefValue returns the value referenced by the supplied EnvVarSource given the other supplied information. func GetEnvVarRefValue(kc kubernetes.Interface, ns string, store *ResourceStore, from *corev1.EnvVarSource, obj runtime.Object, c *corev1.Container) (string, error) { if from.SecretKeyRef != nil { return getSecretRefValue(kc, ns, store, from.SecretKeyRef) } if from.ConfigMapKeyRef != nil { return getConfigMapRefValue(kc, ns, store, from.ConfigMapKeyRef) } if from.FieldRef != nil { return getFieldRef(obj, from) } if from.ResourceFieldRef != nil { return getResourceFieldRef(from, c) } return "", fmt.Errorf("invalid valueFrom") } // GetEnvVarRefString returns a text description of whichever field is set within the supplied EnvVarSource argument. func GetEnvVarRefString(from *corev1.EnvVarSource) string { if from.ConfigMapKeyRef != nil { return fmt.Sprintf("configmap %s, key %s", from.ConfigMapKeyRef.Name, from.ConfigMapKeyRef.Key) } if from.SecretKeyRef != nil { return fmt.Sprintf("secret %s, key %s", from.SecretKeyRef.Name, from.SecretKeyRef.Key) } if from.FieldRef != nil { return fmt.Sprintf("field path %s", from.FieldRef.FieldPath) } if from.ResourceFieldRef != nil { containerPrefix := "" if from.ResourceFieldRef.ContainerName != "" { containerPrefix = fmt.Sprintf("%s/", from.ResourceFieldRef.ContainerName) } return fmt.Sprintf("resource field %s%s", containerPrefix, from.ResourceFieldRef.Resource) } return "invalid valueFrom" } // IsHugePageResourceName returns true if the resource name has the huge page // resource prefix. func IsHugePageResourceName(name corev1.ResourceName) bool { return strings.HasPrefix(string(name), corev1.ResourceHugePagesPrefix) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/helper.go000066400000000000000000000113311504711711200272070ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "strings" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/cli-runtime/pkg/resource" ) // selectContainers allows one or more containers to be matched against a string or wildcard func selectContainers(containers []v1.Container, spec string) ([]*v1.Container, []*v1.Container) { out := []*v1.Container{} skipped := []*v1.Container{} for i, c := range containers { if selectString(c.Name, spec) { out = append(out, &containers[i]) } else { skipped = append(skipped, &containers[i]) } } return out, skipped } // selectString returns true if the provided string matches spec, where spec is a string with // a non-greedy '*' wildcard operator. // TODO: turn into a regex and handle greedy matches and backtracking. func selectString(s, spec string) bool { if spec == "*" { return true } if !strings.Contains(spec, "*") { return s == spec } pos := 0 match := true parts := strings.Split(spec, "*") Loop: for i, part := range parts { if len(part) == 0 { continue } next := strings.Index(s[pos:], part) switch { // next part not in string case next < pos: fallthrough // first part does not match start of string case i == 0 && pos != 0: fallthrough // last part does not exactly match remaining part of string case i == (len(parts)-1) && len(s) != (len(part)+next): match = false break Loop default: pos = next } } return match } // Patch represents the result of a mutation to an object. type Patch struct { Info *resource.Info Err error Before []byte After []byte Patch []byte } // PatchFn is a function type that accepts an info object and returns a byte slice. // Implementations of PatchFn should update the object and return it encoded. type PatchFn func(runtime.Object) ([]byte, error) // CalculatePatch calls the mutation function on the provided info object, and generates a strategic merge patch for // the changes in the object. Encoder must be able to encode the info into the appropriate destination type. // This function returns whether the mutation function made any change in the original object. func CalculatePatch(patch *Patch, encoder runtime.Encoder, mutateFn PatchFn) bool { patch.Before, patch.Err = runtime.Encode(encoder, patch.Info.Object) patch.After, patch.Err = mutateFn(patch.Info.Object) if patch.Err != nil { return true } if patch.After == nil { return false } patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, patch.Info.Object) return true } // CalculatePatches calculates patches on each provided info object. If the provided mutateFn // makes no change in an object, the object is not included in the final list of patches. func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn PatchFn) []*Patch { var patches []*Patch for _, info := range infos { patch := &Patch{Info: info} if CalculatePatch(patch, encoder, mutateFn) { patches = append(patches, patch) } } return patches } func findEnv(env []v1.EnvVar, name string) (v1.EnvVar, bool) { for _, e := range env { if e.Name == name { return e, true } } return v1.EnvVar{}, false } // updateEnv adds and deletes specified environment variables from existing environment variables. // An added variable replaces all existing variables with the same name. // Removing a variable removes all existing variables with the same name. // If the existing list contains duplicates that are unrelated to the variables being added and removed, // those duplicates are left intact in the result. // If a variable is both added and removed, the removal takes precedence. func updateEnv(existing []v1.EnvVar, env []v1.EnvVar, remove []string) []v1.EnvVar { out := []v1.EnvVar{} covered := sets.New[string](remove...) for _, e := range existing { if covered.Has(e.Name) { continue } newer, ok := findEnv(env, e.Name) if ok { covered.Insert(e.Name) out = append(out, newer) continue } out = append(out, e) } for _, e := range env { if covered.Has(e.Name) { continue } covered.Insert(e.Name) out = append(out, e) } return out } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/helper_test.go000066400000000000000000000054741504711711200302610ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "reflect" "testing" v1 "k8s.io/api/core/v1" ) func Test_updateEnv(t *testing.T) { var ( env1 = v1.EnvVar{ Name: "env1", Value: "env1", } env2 = v1.EnvVar{ Name: "env2", Value: "env2", } env3 = v1.EnvVar{ Name: "env3", Value: "env3", } ) type args struct { existing []v1.EnvVar add []v1.EnvVar remove []string } tests := []struct { name string args args want []v1.EnvVar }{ { name: "case 1: add a new and remove another one", args: args{ existing: []v1.EnvVar{env1}, add: []v1.EnvVar{env2}, remove: []string{env1.Name}, }, want: []v1.EnvVar{env2}, }, { name: "case 2: in a collection of multiple env, add a new and remove another one", args: args{ existing: []v1.EnvVar{env1, env2}, add: []v1.EnvVar{env3}, remove: []string{env1.Name}, }, want: []v1.EnvVar{env2, env3}, }, { name: "case 3: items added are deduplicated", args: args{ existing: []v1.EnvVar{env1}, add: []v1.EnvVar{env2, env2}, remove: []string{env1.Name}, }, want: []v1.EnvVar{env2}, }, { name: "case 4: multi add and single remove", args: args{ existing: []v1.EnvVar{env1}, add: []v1.EnvVar{env2, env2, env2, env3}, remove: []string{env1.Name}, }, want: []v1.EnvVar{env2, env3}, }, { name: "case 5: add and remove the same env", args: args{ existing: []v1.EnvVar{env1}, add: []v1.EnvVar{env2, env1}, remove: []string{env1.Name, env1.Name}, }, want: []v1.EnvVar{env2}, }, { name: "case 6: existing duplicate unmodified by unrelated addition", args: args{ existing: []v1.EnvVar{env1, env1}, add: []v1.EnvVar{env2}, remove: nil, }, want: []v1.EnvVar{env1, env1, env2}, }, { name: "case 7: existing duplicate removed when added yet again", args: args{ existing: []v1.EnvVar{env1, env1, env2}, add: []v1.EnvVar{env1}, remove: nil, }, want: []v1.EnvVar{env1, env2}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := updateEnv(tt.args.existing, tt.args.add, tt.args.remove); !reflect.DeepEqual(got, tt.want) { t.Errorf("updateEnv() = %v, want %v", got, tt.want) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set.go000066400000000000000000000032071504711711200265260ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( setLong = templates.LongDesc(i18n.T(` Configure application resources. These commands help you make changes to existing application resources.`)) ) // NewCmdSet returns an initialized Command instance for 'set' sub command func NewCmdSet(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "set SUBCOMMAND", DisableFlagsInUseLine: true, Short: i18n.T("Set specific features on objects"), Long: setLong, Run: cmdutil.DefaultSubCommandRun(streams.ErrOut), } // add subcommands cmd.AddCommand(NewCmdImage(f, streams)) cmd.AddCommand(NewCmdResources(f, streams)) cmd.AddCommand(NewCmdSelector(f, streams)) cmd.AddCommand(NewCmdSubject(f, streams)) cmd.AddCommand(NewCmdServiceAccount(f, streams)) cmd.AddCommand(NewCmdEnv(f, streams)) return cmd } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go000066400000000000000000000415501504711711200274010ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "errors" "fmt" "regexp" "sort" "strings" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" envutil "k8s.io/kubectl/pkg/cmd/set/env" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/term" ) var ( validEnvNameRegexp = regexp.MustCompile("[^a-zA-Z0-9_]") envResources = ` pod (po), replicationcontroller (rc), deployment (deploy), daemonset (ds), statefulset (sts), cronjob (cj), replicaset (rs)` envLong = templates.LongDesc(i18n.T(` Update environment variables on a pod template. List environment variable definitions in one or more pods, pod templates. Add, update, or remove container environment variable definitions in one or more pod templates (within replication controllers or deployment configurations). View or modify the environment variable definitions on all containers in the specified pods or pod templates, or just those that match a wildcard. If "--env -" is passed, environment variables can be read from STDIN using the standard env syntax. Possible resources include (case insensitive): `) + envResources) envExample = templates.Examples(` # Update deployment 'registry' with a new environment variable kubectl set env deployment/registry STORAGE_DIR=/local # List the environment variables defined on a deployments 'sample-build' kubectl set env deployment/sample-build --list # List the environment variables defined on all pods kubectl set env pods --all --list # Output modified deployment in YAML, and does not alter the object on the server kubectl set env deployment/sample-build STORAGE_DIR=/data -o yaml # Update all containers in all replication controllers in the project to have ENV=prod kubectl set env rc --all ENV=prod # Import environment from a secret kubectl set env --from=secret/mysecret deployment/myapp # Import environment from a config map with a prefix kubectl set env --from=configmap/myconfigmap --prefix=MYSQL_ deployment/myapp # Import specific keys from a config map kubectl set env --keys=my-example-key --from=configmap/myconfigmap deployment/myapp # Remove the environment variable ENV from container 'c1' in all deployment configs kubectl set env deployments --all --containers="c1" ENV- # Remove the environment variable ENV from a deployment definition on disk and # update the deployment config on the server kubectl set env -f deploy.json ENV- # Set some of the local shell environment into a deployment config on the server env | grep RAILS_ | kubectl set env -e - deployment/registry`) ) // EnvOptions holds values for 'set env' command-lone options type EnvOptions struct { PrintFlags *genericclioptions.PrintFlags resource.FilenameOptions EnvParams []string All bool Resolve bool List bool Local bool Overwrite bool ContainerSelector string Selector string From string Prefix string Keys []string fieldManager string PrintObj printers.ResourcePrinterFunc envArgs []string resources []string output string dryRunStrategy cmdutil.DryRunStrategy builder func() *resource.Builder updatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc namespace string enforceNamespace bool clientset *kubernetes.Clientset genericiooptions.IOStreams WarningPrinter *printers.WarningPrinter } // NewEnvOptions returns an EnvOptions indicating all containers in the selected // pod templates are selected by default and allowing environment to be overwritten func NewEnvOptions(streams genericiooptions.IOStreams) *EnvOptions { return &EnvOptions{ PrintFlags: genericclioptions.NewPrintFlags("env updated").WithTypeSetter(scheme.Scheme), ContainerSelector: "*", Overwrite: true, IOStreams: streams, } } // NewCmdEnv implements the OpenShift cli env command func NewCmdEnv(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewEnvOptions(streams) cmd := &cobra.Command{ Use: "env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N", DisableFlagsInUseLine: true, Short: i18n.T("Update environment variables on a pod template"), Long: envLong, Example: envExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunEnv()) }, } usage := "the resource to update the env" cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmd.Flags().StringVarP(&o.ContainerSelector, "containers", "c", o.ContainerSelector, "The names of containers in the selected pod templates to change - may use wildcards") cmd.Flags().StringVarP(&o.From, "from", "", "", "The name of a resource from which to inject environment variables") cmd.Flags().StringVarP(&o.Prefix, "prefix", "", "", "Prefix to append to variable names") cmd.Flags().StringArrayVarP(&o.EnvParams, "env", "e", o.EnvParams, "Specify a key-value pair for an environment variable to set into each container.") cmd.Flags().StringSliceVarP(&o.Keys, "keys", "", o.Keys, "Comma-separated list of keys to import from specified resource") cmd.Flags().BoolVar(&o.List, "list", o.List, "If true, display the environment and any changes in the standard format. this flag will removed when we have kubectl view env.") cmd.Flags().BoolVar(&o.Resolve, "resolve", o.Resolve, "If true, show secret or configmap references when listing variables") cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set env will NOT contact api-server but run locally.") cmd.Flags().BoolVar(&o.All, "all", o.All, "If true, select all resources in the namespace of the specified resource types") cmd.Flags().BoolVar(&o.Overwrite, "overwrite", o.Overwrite, "If true, allow environment to be overwritten, otherwise reject updates that overwrite existing environment.") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-set") cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) o.PrintFlags.AddFlags(cmd) cmdutil.AddDryRunFlag(cmd) return cmd } func validateNoOverwrites(existing []v1.EnvVar, env []v1.EnvVar) error { for _, e := range env { if current, exists := findEnv(existing, e.Name); exists && current.Value != e.Value { return fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", current.Name, current.Value) } } return nil } func contains(key string, keyList []string) bool { if len(keyList) == 0 { return true } for _, k := range keyList { if k == key { return true } } return false } func (o *EnvOptions) keyToEnvName(key string) string { envName := strings.ToUpper(validEnvNameRegexp.ReplaceAllString(key, "_")) if envName != key { o.WarningPrinter.Print(fmt.Sprintf("key %s transferred to %s", key, envName)) } return envName } // Complete completes all required options func (o *EnvOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { if o.All && len(o.Selector) > 0 { return fmt.Errorf("cannot set --all and --selector at the same time") } ok := false o.resources, o.envArgs, ok = envutil.SplitEnvironmentFromResources(args) if !ok { return fmt.Errorf("all resources must be specified before environment changes: %s", strings.Join(args, " ")) } o.updatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn o.output = cmdutil.GetFlagString(cmd, "output") var err error o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj o.clientset, err = f.KubernetesClientSet() if err != nil { return err } o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.builder = f.NewBuilder // Set default WarningPrinter if not already set. if o.WarningPrinter == nil { o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)}) } return nil } // Validate makes sure provided values for EnvOptions are valid func (o *EnvOptions) Validate() error { if o.Local && o.dryRunStrategy == cmdutil.DryRunServer { return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?") } if len(o.Filenames) == 0 && len(o.resources) < 1 { return fmt.Errorf("one or more resources must be specified as or /") } if o.List && len(o.output) > 0 { return fmt.Errorf("--list and --output may not be specified together") } if len(o.Keys) > 0 && len(o.From) == 0 { return fmt.Errorf("when specifying --keys, a configmap or secret must be provided with --from") } if o.WarningPrinter == nil { return fmt.Errorf("WarningPrinter can not be used without initialization") } return nil } // RunEnv contains all the necessary functionality for the OpenShift cli env command func (o *EnvOptions) RunEnv() error { env, remove, envFromStdin, err := envutil.ParseEnv(append(o.EnvParams, o.envArgs...), o.In) if err != nil { return err } if len(o.From) != 0 { b := o.builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). LocalParam(o.Local). ContinueOnError(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, &o.FilenameOptions). Flatten() if !o.Local { b = b. LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, o.From). Latest() } if envFromStdin { b = b.StdinInUse() } infos, err := b.Do().Infos() if err != nil { return err } for _, info := range infos { switch from := info.Object.(type) { case *v1.Secret: for key := range from.Data { if contains(key, o.Keys) { envVar := v1.EnvVar{ Name: o.keyToEnvName(key), ValueFrom: &v1.EnvVarSource{ SecretKeyRef: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ Name: from.Name, }, Key: key, }, }, } env = append(env, envVar) } } case *v1.ConfigMap: for key := range from.Data { if contains(key, o.Keys) { envVar := v1.EnvVar{ Name: o.keyToEnvName(key), ValueFrom: &v1.EnvVarSource{ ConfigMapKeyRef: &v1.ConfigMapKeySelector{ LocalObjectReference: v1.LocalObjectReference{ Name: from.Name, }, Key: key, }, }, } env = append(env, envVar) } } default: return fmt.Errorf("unsupported resource specified in --from") } } } if len(o.Prefix) != 0 { for i := range env { env[i].Name = fmt.Sprintf("%s%s", o.Prefix, env[i].Name) } } b := o.builder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). LocalParam(o.Local). ContinueOnError(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(o.enforceNamespace, &o.FilenameOptions). Flatten() if !o.Local { b.LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, o.resources...). Latest() } if envFromStdin { b = b.StdinInUse() } infos, err := b.Do().Infos() if err != nil { return err } patches := CalculatePatches(infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) { _, err := o.updatePodSpecForObject(obj, func(spec *v1.PodSpec) error { resolutionErrorsEncountered := false initContainers, _ := selectContainers(spec.InitContainers, o.ContainerSelector) containers, _ := selectContainers(spec.Containers, o.ContainerSelector) containers = append(containers, initContainers...) objName, err := meta.NewAccessor().Name(obj) if err != nil { return err } gvks, _, err := scheme.Scheme.ObjectKinds(obj) if err != nil { return err } objKind := obj.GetObjectKind().GroupVersionKind().Kind if len(objKind) == 0 { for _, gvk := range gvks { if len(gvk.Kind) == 0 { continue } if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal { continue } objKind = gvk.Kind break } } if len(containers) == 0 { if gvks, _, err := scheme.Scheme.ObjectKinds(obj); err == nil { objKind := obj.GetObjectKind().GroupVersionKind().Kind if len(objKind) == 0 { for _, gvk := range gvks { if len(gvk.Kind) == 0 { continue } if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal { continue } objKind = gvk.Kind break } } o.WarningPrinter.Print(fmt.Sprintf("%s/%s does not have any containers matching %q", objKind, objName, o.ContainerSelector)) } return nil } for _, c := range containers { if !o.Overwrite { if err := validateNoOverwrites(c.Env, env); err != nil { return err } } c.Env = updateEnv(c.Env, env, remove) if o.List { resolveErrors := map[string][]string{} store := envutil.NewResourceStore() fmt.Fprintf(o.Out, "# %s %s, container %s\n", objKind, objName, c.Name) for _, env := range c.Env { // Print the simple value if env.ValueFrom == nil { fmt.Fprintf(o.Out, "%s=%s\n", env.Name, env.Value) continue } // Print the reference version if !o.Resolve { fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom)) continue } value, err := envutil.GetEnvVarRefValue(o.clientset, o.namespace, store, env.ValueFrom, obj, c) // Print the resolved value if err == nil { fmt.Fprintf(o.Out, "%s=%s\n", env.Name, value) continue } // Print the reference version and save the resolve error fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom)) errString := err.Error() resolveErrors[errString] = append(resolveErrors[errString], env.Name) resolutionErrorsEncountered = true } // Print any resolution errors errs := []string{} for err, vars := range resolveErrors { sort.Strings(vars) errs = append(errs, fmt.Sprintf("error retrieving reference for %s: %v", strings.Join(vars, ", "), err)) } sort.Strings(errs) for _, err := range errs { fmt.Fprintln(o.ErrOut, err) } } } if resolutionErrorsEncountered { return errors.New("failed to retrieve valueFrom references") } return nil }) if err == nil { return runtime.Encode(scheme.DefaultJSONEncoder(), obj) } return nil, err }) if o.List { return nil } allErrs := []error{} for _, patch := range patches { info := patch.Info if patch.Err != nil { name := info.ObjectName() allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err)) continue } // no changes if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { continue } if o.Local || o.dryRunStrategy == cmdutil.DryRunClient { if err := o.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } continue } actual, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { allErrs = append(allErrs, fmt.Errorf("failed to patch env update to pod template: %v", err)) continue } // make sure arguments to set or replace environment variables are set // before returning a successful message if len(env) == 0 && len(o.envArgs) == 0 { return fmt.Errorf("at least one environment variable must be provided") } if err := o.PrintObj(actual, o.Out); err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go000066400000000000000000000663031504711711200304430ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "io" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestSetEnvLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "name" streams, _, buf, bufErr := genericiooptions.NewTestIOStreams() opts := NewEnvOptions(streams) opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme) opts.FilenameOptions = resource.FilenameOptions{ Filenames: []string{"../../../testdata/controller.yaml"}, } opts.Local = true err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"env=prod"}) assert.NoError(t, err) err = opts.Validate() assert.NoError(t, err) err = opts.RunEnv() assert.NoError(t, err) if bufErr.Len() > 0 { t.Errorf("unexpected error: %s", bufErr.String()) } if !strings.Contains(buf.String(), "replicationcontroller/cassandra") { t.Errorf("did not set env: %s", buf.String()) } } func TestSetEnvLocalNamespace(t *testing.T) { tf := cmdtesting.NewTestFactory() defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "yaml" streams, _, buf, bufErr := genericiooptions.NewTestIOStreams() opts := NewEnvOptions(streams) opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme) opts.FilenameOptions = resource.FilenameOptions{ Filenames: []string{"../../../testdata/set/namespaced-resource.yaml"}, } opts.Local = true err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"env=prod"}) assert.NoError(t, err) err = opts.Validate() assert.NoError(t, err) err = opts.RunEnv() assert.NoError(t, err) if bufErr.Len() > 0 { t.Errorf("unexpected error: %s", bufErr.String()) } if !strings.Contains(buf.String(), "namespace: existing-ns") { t.Errorf("did not set env: %s", buf.String()) } } func TestSetMultiResourcesEnvLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "name" streams, _, buf, bufErr := genericiooptions.NewTestIOStreams() opts := NewEnvOptions(streams) opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme) opts.FilenameOptions = resource.FilenameOptions{ Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"}, } opts.Local = true err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"env=prod"}) assert.NoError(t, err) err = opts.Validate() assert.NoError(t, err) err = opts.RunEnv() assert.NoError(t, err) if bufErr.Len() > 0 { t.Errorf("unexpected error: %s", bufErr.String()) } expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n" if buf.String() != expectedOut { t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String()) } } func TestSetEnvRemote(t *testing.T) { inputs := []struct { name string object runtime.Object groupVersion schema.GroupVersion path string args []string }{ { name: "test extensions.v1beta1 replicaset", object: &extensionsv1beta1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "env=prod"}, }, { name: "test apps.v1beta2 replicaset", object: &appsv1beta2.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "env=prod"}, }, { name: "test appsv1 replicaset", object: &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "env=prod"}, }, { name: "test extensions.v1beta1 daemonset", object: &extensionsv1beta1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", "env=prod"}, }, { name: "test appsv1beta2 daemonset", object: &appsv1beta2.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", "env=prod"}, }, { name: "test appsv1 daemonset", object: &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", "env=prod"}, }, { name: "test extensions.v1beta1 deployment", object: &extensionsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "env=prod"}, }, { name: "test appsv1beta1 deployment", object: &appsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "env=prod"}, }, { name: "test appsv1beta2 deployment", object: &appsv1beta2.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "env=prod"}, }, { name: "test appsv1 deployment", object: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "env=prod"}, }, { name: "test appsv1beta1 statefulset", object: &appsv1beta1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", "env=prod"}, }, { name: "test appsv1beta2 statefulset", object: &appsv1beta2.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", "env=prod"}, }, { name: "test appsv1 statefulset", object: &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", "env=prod"}, }, { name: "set image batchv1 CronJob", object: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, }, }, groupVersion: batchv1.SchemeGroupVersion, path: "/namespaces/test/cronjobs/nginx", args: []string{"cronjob", "nginx", "env=prod"}, }, { name: "test corev1 replication controller", object: &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: corev1.ReplicationControllerSpec{ Template: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: corev1.SchemeGroupVersion, path: "/namespaces/test/replicationcontrollers/nginx", args: []string{"replicationcontroller", "nginx", "env=prod"}, }, } for _, input := range inputs { t.Run(input.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: input.groupVersion, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == input.path && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil case p == input.path && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } assert.Containsf(t, string(bytes), `"value":`+`"`+"prod"+`"`, "env not updated for %#v", input.object) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", "image", req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } outputFormat := "yaml" streams := genericiooptions.NewTestIOStreamsDiscard() opts := NewEnvOptions(streams) opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme) opts.Local = false opts.IOStreams = streams err := opts.Complete(tf, NewCmdEnv(tf, streams), input.args) assert.NoError(t, err) err = opts.RunEnv() assert.NoError(t, err) }) } } func TestSetEnvFromResource(t *testing.T) { mockConfigMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: "testconfigmap"}, Data: map[string]string{ "env": "prod", "test-key": "testValue", "test-key-two": "testValueTwo", }, } mockConfigMapUpperCaseKey := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: "testconfigmapuppercasekey"}, Data: map[string]string{ "ENV": "prod", "TEST_KEY": "testValue", "TEST_KEY_TWO": "testValueTwo", }, } mockSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: "testsecret"}, Data: map[string][]byte{ "env": []byte("prod"), "test-key": []byte("testValue"), "test-key-two": []byte("testValueTwo"), }, } mockSecretUpperCaseKey := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: "testsecretuppercasekey"}, Data: map[string][]byte{ "ENV": []byte("prod"), "TEST_KEY": []byte("testValue"), "TEST_KEY_TWO": []byte("testValueTwo"), }, } inputs := []struct { name string args []string from string keys []string assertIncludes []string assertExcludes []string warning bool }{ { name: "test from configmap", args: []string{"deployment", "nginx"}, from: "configmap/testconfigmap", keys: []string{}, assertIncludes: []string{ `{"name":"ENV","valueFrom":{"configMapKeyRef":{"key":"env","name":"testconfigmap"}}}`, `{"name":"TEST_KEY","valueFrom":{"configMapKeyRef":{"key":"test-key","name":"testconfigmap"}}}`, `{"name":"TEST_KEY_TWO","valueFrom":{"configMapKeyRef":{"key":"test-key-two","name":"testconfigmap"}}}`, }, assertExcludes: []string{}, warning: true, }, { name: "test from configmap with upper case key", args: []string{"deployment", "nginx"}, from: "configmap/testconfigmapuppercasekey", keys: []string{}, assertIncludes: []string{ `{"name":"ENV","valueFrom":{"configMapKeyRef":{"key":"ENV","name":"testconfigmapuppercasekey"}}}`, `{"name":"TEST_KEY","valueFrom":{"configMapKeyRef":{"key":"TEST_KEY","name":"testconfigmapuppercasekey"}}}`, `{"name":"TEST_KEY_TWO","valueFrom":{"configMapKeyRef":{"key":"TEST_KEY_TWO","name":"testconfigmapuppercasekey"}}}`, }, assertExcludes: []string{}, warning: false, }, { name: "test from secret", args: []string{"deployment", "nginx"}, from: "secret/testsecret", keys: []string{}, assertIncludes: []string{ `{"name":"ENV","valueFrom":{"secretKeyRef":{"key":"env","name":"testsecret"}}}`, `{"name":"TEST_KEY","valueFrom":{"secretKeyRef":{"key":"test-key","name":"testsecret"}}}`, `{"name":"TEST_KEY_TWO","valueFrom":{"secretKeyRef":{"key":"test-key-two","name":"testsecret"}}}`, }, assertExcludes: []string{}, warning: true, }, { name: "test from secret with upper case key", args: []string{"deployment", "nginx"}, from: "secret/testsecretuppercasekey", keys: []string{}, assertIncludes: []string{ `{"name":"ENV","valueFrom":{"secretKeyRef":{"key":"ENV","name":"testsecretuppercasekey"}}}`, `{"name":"TEST_KEY","valueFrom":{"secretKeyRef":{"key":"TEST_KEY","name":"testsecretuppercasekey"}}}`, `{"name":"TEST_KEY_TWO","valueFrom":{"secretKeyRef":{"key":"TEST_KEY_TWO","name":"testsecretuppercasekey"}}}`, }, assertExcludes: []string{}, warning: false, }, { name: "test from configmap with keys", args: []string{"deployment", "nginx"}, from: "configmap/testconfigmap", keys: []string{"env", "test-key-two"}, assertIncludes: []string{ `{"name":"ENV","valueFrom":{"configMapKeyRef":{"key":"env","name":"testconfigmap"}}}`, `{"name":"TEST_KEY_TWO","valueFrom":{"configMapKeyRef":{"key":"test-key-two","name":"testconfigmap"}}}`, }, assertExcludes: []string{`{"name":"TEST_KEY","valueFrom":{"configMapKeyRef":{"key":"test-key","name":"testconfigmap"}}}`}, warning: true, }, { name: "test from secret with keys", args: []string{"deployment", "nginx"}, from: "secret/testsecret", keys: []string{"env", "test-key-two"}, assertIncludes: []string{ `{"name":"ENV","valueFrom":{"secretKeyRef":{"key":"env","name":"testsecret"}}}`, `{"name":"TEST_KEY_TWO","valueFrom":{"secretKeyRef":{"key":"test-key-two","name":"testsecret"}}}`, }, assertExcludes: []string{`{"name":"TEST_KEY","valueFrom":{"secretKeyRef":{"key":"test-key","name":"testsecret"}}}`}, warning: true, }, } for _, input := range inputs { mockDeployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, } t.Run(input.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/configmaps/testconfigmap" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockConfigMap)}, nil case p == "/namespaces/test/configmaps/testconfigmapuppercasekey" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockConfigMapUpperCaseKey)}, nil case p == "/namespaces/test/secrets/testsecret" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockSecret)}, nil case p == "/namespaces/test/secrets/testsecretuppercasekey" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockSecretUpperCaseKey)}, nil case p == "/namespaces/test/deployments/nginx" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil case p == "/namespaces/test/deployments/nginx" && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } for _, include := range input.assertIncludes { assert.Contains(t, string(bytes), include) } for _, exclude := range input.assertExcludes { assert.NotContains(t, string(bytes), exclude) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil default: t.Errorf("%s: unexpected request: %#v\n%#v", input.name, req.URL, req) return nil, nil } }), } outputFormat := "yaml" streams, _, _, errOut := genericiooptions.NewTestIOStreams() opts := NewEnvOptions(streams) opts.From = input.from opts.Keys = input.keys opts.PrintFlags = genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme) opts.Local = false opts.IOStreams = streams err := opts.Complete(tf, NewCmdEnv(tf, streams), input.args) assert.NoError(t, err) err = opts.RunEnv() if input.warning { assert.Contains(t, errOut.String(), "Warning") } else { assert.NotContains(t, errOut.String(), "Warning") } assert.NoError(t, err) }) } } func TestSetEnvRemoteWithSpecificContainers(t *testing.T) { inputs := []struct { name string args []string selector string expectedContainers int }{ { name: "all containers", args: []string{"deployments", "redis", "env=prod"}, selector: "*", expectedContainers: 2, }, { name: "use wildcards to select some containers", args: []string{"deployments", "redis", "env=prod"}, selector: "red*", expectedContainers: 1, }, { name: "single container", args: []string{"deployments", "redis", "env=prod"}, selector: "redis", expectedContainers: 1, }, } for _, input := range inputs { mockDeployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "redis", Namespace: "test", }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init", Image: "redis", }, }, Containers: []corev1.Container{ { Name: "redis", Image: "redis", }, }, }, }, }, } t.Run(input.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/redis" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil case p == "/namespaces/test/deployments/redis" && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } updated := strings.Count(string(bytes), `"value":`+`"`+"prod"+`"`) if updated != input.expectedContainers { t.Errorf("expected %d containers to be selected but got %d \n", input.expectedContainers, updated) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil default: t.Errorf("%s: unexpected request: %#v\n%#v", input.name, req.URL, req) return nil, nil } }), } streams := genericiooptions.NewTestIOStreamsDiscard() opts := &EnvOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput("yaml").WithTypeSetter(scheme.Scheme), ContainerSelector: input.selector, Overwrite: true, IOStreams: streams, } err := opts.Complete(tf, NewCmdEnv(tf, streams), input.args) assert.NoError(t, err) err = opts.RunEnv() assert.NoError(t, err) }) } } func TestSetEnvDoubleStdinUsage(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} streams, bufIn, _, _ := genericiooptions.NewTestIOStreams() bufIn.WriteString("SOME_ENV_VAR_KEY=SOME_ENV_VAR_VAL") opts := NewEnvOptions(streams) opts.FilenameOptions = resource.FilenameOptions{ Filenames: []string{"-"}, } err := opts.Complete(tf, NewCmdEnv(tf, streams), []string{"-"}) assert.NoError(t, err) err = opts.Validate() assert.NoError(t, err) err = opts.RunEnv() assert.ErrorIs(t, err, resource.StdinMultiUseError) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_image.go000066400000000000000000000254551504711711200277010ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "github.com/spf13/cobra" "k8s.io/klog/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // ImageOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type SetImageOptions struct { resource.FilenameOptions PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags Infos []*resource.Info Selector string DryRunStrategy cmdutil.DryRunStrategy All bool Output string Local bool ResolveImage ImageResolverFunc fieldManager string PrintObj printers.ResourcePrinterFunc Recorder genericclioptions.Recorder UpdatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc Resources []string ContainerImages map[string]string genericiooptions.IOStreams } // ImageResolver is a func that receives an image name, and // resolves it to an appropriate / compatible image name. // Adds flexibility for future image resolving methods. type ImageResolverFunc func(in string) (string, error) // ImageResolver to use. var ImageResolver = resolveImageFunc var ( imageResources = i18n.T(` pod (po), replicationcontroller (rc), deployment (deploy), daemonset (ds), statefulset (sts), cronjob (cj), replicaset (rs)`) imageLong = templates.LongDesc(i18n.T(` Update existing container image(s) of resources. Possible resources include (case insensitive): `) + imageResources) imageExample = templates.Examples(` # Set a deployment's nginx container image to 'nginx:1.9.1', and its busybox container image to 'busybox' kubectl set image deployment/nginx busybox=busybox nginx=nginx:1.9.1 # Update all deployments' and rc's nginx container's image to 'nginx:1.9.1' kubectl set image deployments,rc nginx=nginx:1.9.1 --all # Update image of all containers of daemonset abc to 'nginx:1.9.1' kubectl set image daemonset abc *=nginx:1.9.1 # Print result (in yaml format) of updating nginx container image from local file, without hitting the server kubectl set image -f path/to/file.yaml nginx=nginx:1.9.1 --local -o yaml`) ) // NewImageOptions returns an initialized SetImageOptions instance func NewImageOptions(streams genericiooptions.IOStreams) *SetImageOptions { return &SetImageOptions{ PrintFlags: genericclioptions.NewPrintFlags("image updated").WithTypeSetter(scheme.Scheme), RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, IOStreams: streams, } } // NewCmdImage returns an initialized Command instance for the 'set image' sub command func NewCmdImage(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewImageOptions(streams) validArgs := []string{"daemonsets", "deployments", "pods", "cronjobs", "replicasets", "replicationcontrollers", "statefulsets"} cmd := &cobra.Command{ Use: "image (-f FILENAME | TYPE NAME) CONTAINER_NAME_1=CONTAINER_IMAGE_1 ... CONTAINER_NAME_N=CONTAINER_IMAGE_N", DisableFlagsInUseLine: true, Short: i18n.T("Update the image of a pod template"), Long: imageLong, Example: imageExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameNoRepeatCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd) usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, in the namespace of the specified resource types") cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set image will NOT contact api-server but run locally.") cmdutil.AddDryRunFlag(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-set") cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) return cmd } // Complete completes all required options func (o *SetImageOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.UpdatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.Output = cmdutil.GetFlagString(cmd, "output") o.ResolveImage = ImageResolver cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil && !(o.Local && clientcmd.IsEmptyConfig(err)) { return err } o.Resources, o.ContainerImages, err = getResourcesAndImages(args) if err != nil { return err } builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). LocalParam(o.Local). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). Flatten() if !o.Local { builder.LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, o.Resources...). Latest() } else { // if a --local flag was provided, and a resource was specified in the form // /, fail immediately as --local cannot query the api server // for the specified resource. if len(o.Resources) > 0 { return resource.LocalResourceError } } o.Infos, err = builder.Do().Infos() if err != nil { return err } return nil } // Validate makes sure provided values in SetImageOptions are valid func (o *SetImageOptions) Validate() error { errors := []error{} if o.All && len(o.Selector) > 0 { errors = append(errors, fmt.Errorf("cannot set --all and --selector at the same time")) } if len(o.Resources) < 1 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { errors = append(errors, fmt.Errorf("one or more resources must be specified as or /")) } if len(o.ContainerImages) < 1 { errors = append(errors, fmt.Errorf("at least one image update is required")) } else if len(o.ContainerImages) > 1 && hasWildcardKey(o.ContainerImages) { errors = append(errors, fmt.Errorf("all containers are already specified by *, but saw more than one container_name=container_image pairs")) } if o.Local && o.DryRunStrategy == cmdutil.DryRunServer { errors = append(errors, fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")) } return utilerrors.NewAggregate(errors) } // Run performs the execution of 'set image' sub command func (o *SetImageOptions) Run() error { allErrs := []error{} patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) { _, err := o.UpdatePodSpecForObject(obj, func(spec *v1.PodSpec) error { for name, image := range o.ContainerImages { resolvedImageName, err := o.ResolveImage(image) if err != nil { allErrs = append(allErrs, fmt.Errorf("error: unable to resolve image %q for container %q: %v", image, name, err)) if name == "*" { break } continue } initContainerFound := setImage(spec.InitContainers, name, resolvedImageName) containerFound := setImage(spec.Containers, name, resolvedImageName) if !containerFound && !initContainerFound { allErrs = append(allErrs, fmt.Errorf("error: unable to find container named %q", name)) } } return nil }) if err != nil { return nil, err } // record this change (for rollout history) if err := o.Recorder.Record(obj); err != nil { klog.V(4).Infof("error recording current command: %v", err) } return runtime.Encode(scheme.DefaultJSONEncoder(), obj) }) for _, patch := range patches { info := patch.Info if patch.Err != nil { name := info.ObjectName() allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err)) continue } // no changes if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { continue } if o.Local || o.DryRunStrategy == cmdutil.DryRunClient { if err := o.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } continue } // patch the change actual, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { allErrs = append(allErrs, fmt.Errorf("failed to patch image update to pod template: %v", err)) continue } if err := o.PrintObj(actual, o.Out); err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } func setImage(containers []v1.Container, containerName string, image string) bool { containerFound := false // Find the container to update, and update its image for i, c := range containers { if c.Name == containerName || containerName == "*" { containerFound = true containers[i].Image = image } } return containerFound } // getResourcesAndImages retrieves resources and container name:images pair from given args func getResourcesAndImages(args []string) (resources []string, containerImages map[string]string, err error) { pairType := "image" resources, imageArgs, err := cmdutil.GetResourcesAndPairs(args, pairType) if err != nil { return } containerImages, _, err = cmdutil.ParsePairs(imageArgs, pairType, false) return } func hasWildcardKey(containerImages map[string]string) bool { _, ok := containerImages["*"] return ok } // implements ImageResolver func resolveImageFunc(in string) (string, error) { return in, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_image_test.go000066400000000000000000000536721504711711200307420ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "io" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestImageLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "name" streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdImage(tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("output", outputFormat) cmd.Flags().Set("local", "true") opts := SetImageOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), FilenameOptions: resource.FilenameOptions{ Filenames: []string{"../../../testdata/controller.yaml"}}, Local: true, IOStreams: streams, } err := opts.Complete(tf, cmd, []string{"cassandra=thingy"}) if err == nil { err = opts.Validate() } if err == nil { err = opts.Run() } if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(buf.String(), "replicationcontroller/cassandra") { t.Errorf("did not set image: %s", buf.String()) } } func TestSetImageValidation(t *testing.T) { printFlags := genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme) testCases := []struct { name string imageOptions *SetImageOptions expectErr string }{ { name: "test resource < 1 and filenames empty", imageOptions: &SetImageOptions{PrintFlags: printFlags}, expectErr: "[one or more resources must be specified as or /, at least one image update is required]", }, { name: "test containerImages < 1", imageOptions: &SetImageOptions{ PrintFlags: printFlags, Resources: []string{"a", "b", "c"}, FilenameOptions: resource.FilenameOptions{ Filenames: []string{"testFile"}, }, }, expectErr: "at least one image update is required", }, { name: "test containerImages > 1 and all containers are already specified by *", imageOptions: &SetImageOptions{ PrintFlags: printFlags, Resources: []string{"a", "b", "c"}, FilenameOptions: resource.FilenameOptions{ Filenames: []string{"testFile"}, }, ContainerImages: map[string]string{ "test": "test", "*": "test", }, }, expectErr: "all containers are already specified by *, but saw more than one container_name=container_image pairs", }, { name: "success case", imageOptions: &SetImageOptions{ PrintFlags: printFlags, Resources: []string{"a", "b", "c"}, FilenameOptions: resource.FilenameOptions{ Filenames: []string{"testFile"}, }, ContainerImages: map[string]string{ "test": "test", }, }, expectErr: "", }, } for _, testCase := range testCases { err := testCase.imageOptions.Validate() if err != nil { if err.Error() != testCase.expectErr { t.Errorf("[%s]:expect err:%s got err:%s", testCase.name, testCase.expectErr, err.Error()) } } if err == nil && (testCase.expectErr != "") { t.Errorf("[%s]:expect err:%s got err:%v", testCase.name, testCase.expectErr, err) } } } func TestSetMultiResourcesImageLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "name" streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdImage(tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("output", outputFormat) cmd.Flags().Set("local", "true") opts := SetImageOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), FilenameOptions: resource.FilenameOptions{ Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"}}, Local: true, IOStreams: streams, } err := opts.Complete(tf, cmd, []string{"*=thingy"}) if err == nil { err = opts.Validate() } if err == nil { err = opts.Run() } if err != nil { t.Fatalf("unexpected error: %v", err) } expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n" if buf.String() != expectedOut { t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String()) } } func TestSetImageRemote(t *testing.T) { inputs := []struct { name string object runtime.Object groupVersion schema.GroupVersion path string args []string }{ { name: "set image extensionsv1beta1 ReplicaSet", object: &extensionsv1beta1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "*=thingy"}, }, { name: "set image appsv1beta2 ReplicaSet", object: &appsv1beta2.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "*=thingy"}, }, { name: "set image appsv1 ReplicaSet", object: &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "*=thingy"}, }, { name: "set image extensionsv1beta1 DaemonSet", object: &extensionsv1beta1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", "*=thingy"}, }, { name: "set image appsv1beta2 DaemonSet", object: &appsv1beta2.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", "*=thingy"}, }, { name: "set image appsv1 DaemonSet", object: &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", "*=thingy"}, }, { name: "set image extensionsv1beta1 Deployment", object: &extensionsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "*=thingy"}, }, { name: "set image appsv1beta1 Deployment", object: &appsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "*=thingy"}, }, { name: "set image appsv1beta2 Deployment", object: &appsv1beta2.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "*=thingy"}, }, { name: "set image appsv1 Deployment", object: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", "*=thingy"}, }, { name: "set image appsv1beta1 StatefulSet", object: &appsv1beta1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", "*=thingy"}, }, { name: "set image appsv1beta2 StatefulSet", object: &appsv1beta2.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", "*=thingy"}, }, { name: "set image appsv1 StatefulSet", object: &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", "*=thingy"}, }, { name: "set image batchv1 CronJob", object: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, }, }, groupVersion: batchv1.SchemeGroupVersion, path: "/namespaces/test/cronjobs/nginx", args: []string{"cronjob", "nginx", "*=thingy"}, }, { name: "set image corev1.ReplicationController", object: &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: corev1.ReplicationControllerSpec{ Template: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: corev1.SchemeGroupVersion, path: "/namespaces/test/replicationcontrollers/nginx", args: []string{"replicationcontroller", "nginx", "*=thingy"}, }, } for _, input := range inputs { t.Run(input.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: input.groupVersion, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == input.path && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil case p == input.path && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } assert.Containsf(t, string(bytes), `"image":`+`"`+"thingy"+`"`, "image not updated for %#v", input.object) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", "image", req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } outputFormat := "yaml" streams := genericiooptions.NewTestIOStreamsDiscard() cmd := NewCmdImage(tf, streams) cmd.Flags().Set("output", outputFormat) opts := SetImageOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), Local: false, IOStreams: streams, } err := opts.Complete(tf, cmd, input.args) assert.NoError(t, err) err = opts.Run() assert.NoError(t, err) }) } } func TestSetImageRemoteWithSpecificContainers(t *testing.T) { inputs := []struct { name string object runtime.Object groupVersion schema.GroupVersion path string args []string }{ { name: "set container image only", object: &extensionsv1beta1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, InitContainers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "nginx=thingy"}, }, { name: "set initContainer image only", object: &appsv1beta2.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "busybox", Image: "busybox", }, }, InitContainers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", "nginx=thingy"}, }, } for _, input := range inputs { t.Run(input.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: input.groupVersion, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == input.path && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil case p == input.path && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } assert.Containsf(t, string(bytes), `"image":"`+"thingy"+`","name":`+`"nginx"`, "image not updated for %#v", input.object) assert.NotContainsf(t, string(bytes), `"image":"`+"thingy"+`","name":`+`"busybox"`, "image updated for %#v", input.object) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", "image", req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } outputFormat := "yaml" streams := genericiooptions.NewTestIOStreamsDiscard() cmd := NewCmdImage(tf, streams) cmd.Flags().Set("output", outputFormat) opts := SetImageOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), Local: false, IOStreams: streams, } err := opts.Complete(tf, cmd, input.args) assert.NoError(t, err) err = opts.Run() assert.NoError(t, err) }) } } func TestSetImageResolver(t *testing.T) { f := func(in string) (string, error) { return "custom", nil } ImageResolver = f out, err := ImageResolver("my-image") if err != nil { t.Errorf("unexpected error from ImageResolver: %v", err) } else if out != "custom" { t.Errorf("expected: %s, found: %s", "custom", out) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_resources.go000066400000000000000000000254551504711711200306310ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "github.com/spf13/cobra" "k8s.io/klog/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" generateversioned "k8s.io/kubectl/pkg/generate/versioned" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( resourcesLong = templates.LongDesc(i18n.T(` Specify compute resource requirements (CPU, memory) for any resource that defines a pod template. If a pod is successfully scheduled, it is guaranteed the amount of resource requested, but may burst up to its specified limits. For each compute resource, if a limit is specified and a request is omitted, the request will default to the limit. Possible resources include (case insensitive): %s.`)) resourcesExample = templates.Examples(` # Set a deployments nginx container cpu limits to "200m" and memory to "512Mi" kubectl set resources deployment nginx -c=nginx --limits=cpu=200m,memory=512Mi # Set the resource request and limits for all containers in nginx kubectl set resources deployment nginx --limits=cpu=200m,memory=512Mi --requests=cpu=100m,memory=256Mi # Remove the resource requests for resources on containers in nginx kubectl set resources deployment nginx --limits=cpu=0,memory=0 --requests=cpu=0,memory=0 # Print the result (in yaml format) of updating nginx container limits from a local, without hitting the server kubectl set resources -f path/to/file.yaml --limits=cpu=200m,memory=512Mi --local -o yaml`) ) // SetResourcesOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags type SetResourcesOptions struct { resource.FilenameOptions PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags Infos []*resource.Info Selector string ContainerSelector string Output string All bool Local bool fieldManager string DryRunStrategy cmdutil.DryRunStrategy PrintObj printers.ResourcePrinterFunc Recorder genericclioptions.Recorder Limits string Requests string ResourceRequirements v1.ResourceRequirements UpdatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc Resources []string genericiooptions.IOStreams } // NewResourcesOptions returns a ResourcesOptions indicating all containers in the selected // pod templates are selected by default. func NewResourcesOptions(streams genericiooptions.IOStreams) *SetResourcesOptions { return &SetResourcesOptions{ PrintFlags: genericclioptions.NewPrintFlags("resource requirements updated").WithTypeSetter(scheme.Scheme), RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, ContainerSelector: "*", IOStreams: streams, } } // NewCmdResources returns initialized Command instance for the 'set resources' sub command func NewCmdResources(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewResourcesOptions(streams) cmd := &cobra.Command{ Use: "resources (-f FILENAME | TYPE NAME) ([--limits=LIMITS & --requests=REQUESTS]", DisableFlagsInUseLine: true, Short: i18n.T("Update resource requests/limits on objects with pod templates"), Long: fmt.Sprintf(resourcesLong, cmdutil.SuggestAPIResources("kubectl")), Example: resourcesExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd) //usage := "Filename, directory, or URL to a file identifying the resource to get from the server" //kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, in the namespace of the specified resource types") cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) cmd.Flags().StringVarP(&o.ContainerSelector, "containers", "c", o.ContainerSelector, "The names of containers in the selected pod templates to change, all containers are selected by default - may use wildcards") cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set resources will NOT contact api-server but run locally.") cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.Limits, "limits", o.Limits, "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'. Note that server side components may assign requests depending on the server configuration, such as limit ranges.") cmd.Flags().StringVar(&o.Requests, "requests", o.Requests, "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'. Note that server side components may assign requests depending on the server configuration, such as limit ranges.") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-set") return cmd } // Complete completes all required options func (o *SetResourcesOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.UpdatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn o.Output = cmdutil.GetFlagString(cmd, "output") o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil && !(o.Local && clientcmd.IsEmptyConfig(err)) { return err } builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). LocalParam(o.Local). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). Flatten() if !o.Local { builder.LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, args...). Latest() } else { // if a --local flag was provided, and a resource was specified in the form // /, fail immediately as --local cannot query the api server // for the specified resource. // TODO: this should be in the builder - if someone specifies tuples, fail when // local is true if len(args) > 0 { return resource.LocalResourceError } } o.Infos, err = builder.Do().Infos() return err } // Validate makes sure that provided values in ResourcesOptions are valid func (o *SetResourcesOptions) Validate() error { var err error if o.Local && o.DryRunStrategy == cmdutil.DryRunServer { return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?") } if o.All && len(o.Selector) > 0 { return fmt.Errorf("cannot set --all and --selector at the same time") } if len(o.Limits) == 0 && len(o.Requests) == 0 { return fmt.Errorf("you must specify an update to requests or limits (in the form of --requests/--limits)") } o.ResourceRequirements, err = generateversioned.HandleResourceRequirementsV1(map[string]string{"limits": o.Limits, "requests": o.Requests}) if err != nil { return err } return nil } // Run performs the execution of 'set resources' sub command func (o *SetResourcesOptions) Run() error { allErrs := []error{} patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) { transformed := false _, err := o.UpdatePodSpecForObject(obj, func(spec *v1.PodSpec) error { initContainers, _ := selectContainers(spec.InitContainers, o.ContainerSelector) containers, _ := selectContainers(spec.Containers, o.ContainerSelector) containers = append(containers, initContainers...) if len(containers) != 0 { for i := range containers { if len(o.Limits) != 0 && len(containers[i].Resources.Limits) == 0 { containers[i].Resources.Limits = make(v1.ResourceList) } for key, value := range o.ResourceRequirements.Limits { containers[i].Resources.Limits[key] = value } if len(o.Requests) != 0 && len(containers[i].Resources.Requests) == 0 { containers[i].Resources.Requests = make(v1.ResourceList) } for key, value := range o.ResourceRequirements.Requests { containers[i].Resources.Requests[key] = value } transformed = true } } else { allErrs = append(allErrs, fmt.Errorf("error: unable to find container named %s", o.ContainerSelector)) } return nil }) if err != nil { return nil, err } if !transformed { return nil, nil } // record this change (for rollout history) if err := o.Recorder.Record(obj); err != nil { klog.V(4).Infof("error recording current command: %v", err) } return runtime.Encode(scheme.DefaultJSONEncoder(), obj) }) for _, patch := range patches { info := patch.Info name := info.ObjectName() if patch.Err != nil { allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err)) continue } //no changes if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { continue } if o.Local || o.DryRunStrategy == cmdutil.DryRunClient { if err := o.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } continue } actual, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { allErrs = append(allErrs, fmt.Errorf("failed to patch resources update to pod template %v", err)) continue } if err := o.PrintObj(actual, o.Out); err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_resources_test.go000066400000000000000000000432341504711711200316630ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "io" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) func TestResourcesLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "name" streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdResources(tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("output", outputFormat) cmd.Flags().Set("local", "true") opts := SetResourcesOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), FilenameOptions: resource.FilenameOptions{ Filenames: []string{"../../../testdata/controller.yaml"}}, Local: true, Limits: "cpu=200m,memory=512Mi", Requests: "cpu=200m,memory=512Mi", ContainerSelector: "*", IOStreams: streams, } err := opts.Complete(tf, cmd, []string{}) if err == nil { err = opts.Validate() } if err == nil { err = opts.Run() } if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(buf.String(), "replicationcontroller/cassandra") { t.Errorf("did not set resources: %s", buf.String()) } } func TestSetMultiResourcesLimitsLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "name" streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdResources(tf, streams) cmd.SetOut(buf) cmd.SetErr(buf) cmd.Flags().Set("output", outputFormat) cmd.Flags().Set("local", "true") opts := SetResourcesOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), FilenameOptions: resource.FilenameOptions{ Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"}}, Local: true, Limits: "cpu=200m,memory=512Mi", Requests: "cpu=200m,memory=512Mi", ContainerSelector: "*", IOStreams: streams, } err := opts.Complete(tf, cmd, []string{}) if err == nil { err = opts.Validate() } if err == nil { err = opts.Run() } if err != nil { t.Fatalf("unexpected error: %v", err) } expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n" if buf.String() != expectedOut { t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String()) } } func TestSetResourcesRemote(t *testing.T) { inputs := []struct { name string object runtime.Object groupVersion schema.GroupVersion path string args []string }{ { name: "set image extensionsv1beta1 ReplicaSet", object: &extensionsv1beta1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx"}, }, { name: "set image appsv1beta2 ReplicaSet", object: &appsv1beta2.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx"}, }, { name: "set image appsv1 ReplicaSet", object: &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx"}, }, { name: "set image extensionsv1beta1 DaemonSet", object: &extensionsv1beta1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx"}, }, { name: "set image appsv1beta2 DaemonSet", object: &appsv1beta2.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx"}, }, { name: "set image appsv1 DaemonSet", object: &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DaemonSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx"}, }, { name: "set image extensionsv1beta1 Deployment", object: &extensionsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: extensionsv1beta1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx"}, }, { name: "set image appsv1beta1 Deployment", object: &appsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx"}, }, { name: "set image appsv1beta2 Deployment", object: &appsv1beta2.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx"}, }, { name: "set image appsv1 Deployment", object: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx"}, }, { name: "set image appsv1beta1 StatefulSet", object: &appsv1beta1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx"}, }, { name: "set image appsv1beta2 StatefulSet", object: &appsv1beta2.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx"}, }, { name: "set image appsv1 StatefulSet", object: &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx"}, }, { name: "set image batchv1 Job", object: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: batchv1.SchemeGroupVersion, path: "/namespaces/test/jobs/nginx", args: []string{"job", "nginx"}, }, { name: "set image corev1.ReplicationController", object: &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: corev1.ReplicationControllerSpec{ Template: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: corev1.SchemeGroupVersion, path: "/namespaces/test/replicationcontrollers/nginx", args: []string{"replicationcontroller", "nginx"}, }, } for i, input := range inputs { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: input.groupVersion, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == input.path && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil case p == input.path && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } assert.Containsf(t, string(bytes), "200m", "resources not updated for %#v", input.object) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", "resources", req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } outputFormat := "yaml" streams := genericiooptions.NewTestIOStreamsDiscard() cmd := NewCmdResources(tf, streams) cmd.Flags().Set("output", outputFormat) opts := SetResourcesOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), Limits: "cpu=200m,memory=512Mi", ContainerSelector: "*", IOStreams: streams, } err := opts.Complete(tf, cmd, input.args) if err == nil { err = opts.Validate() } if err == nil { err = opts.Run() } assert.NoError(t, err) }) } } func TestSetResourcesRemoteWithSpecificContainers(t *testing.T) { inputs := []struct { name string selector string args []string expectedContainers int }{ { name: "all containers", args: []string{"deployments", "redis"}, selector: "*", expectedContainers: 2, }, { name: "use wildcards to select some containers", args: []string{"deployments", "redis"}, selector: "red*", expectedContainers: 1, }, { name: "single container", args: []string{"deployments", "redis"}, selector: "redis", expectedContainers: 1, }, } for _, input := range inputs { mockDeployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "redis", Namespace: "test", }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { Name: "init", Image: "redis", }, }, Containers: []corev1.Container{ { Name: "redis", Image: "redis", }, }, }, }, }, } t.Run(input.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/namespaces/test/deployments/redis" && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil case p == "/namespaces/test/deployments/redis" && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } updated := strings.Count(string(bytes), "200m") if updated != input.expectedContainers { t.Errorf("expected %d containers to be selected but got %d \n", input.expectedContainers, updated) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(mockDeployment)}, nil default: t.Errorf("%s: unexpected request: %#v\n%#v", input.name, req.URL, req) return nil, nil } }), } outputFormat := "yaml" streams := genericiooptions.NewTestIOStreamsDiscard() cmd := NewCmdResources(tf, streams) cmd.Flags().Set("output", outputFormat) opts := SetResourcesOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), Limits: "cpu=200m,memory=512Mi", ContainerSelector: input.selector, IOStreams: streams, } err := opts.Complete(tf, cmd, input.args) if err == nil { err = opts.Validate() } if err == nil { err = opts.Run() } assert.NoError(t, err) }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_selector.go000066400000000000000000000201721504711711200304260ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "github.com/spf13/cobra" "k8s.io/klog/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // SetSelectorOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags() type SetSelectorOptions struct { // Bound ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags dryRunStrategy cmdutil.DryRunStrategy fieldManager string // set by args resources []string selector *metav1.LabelSelector resourceVersion string // computed WriteToServer bool PrintObj printers.ResourcePrinterFunc Recorder genericclioptions.Recorder ResourceFinder genericclioptions.ResourceFinder // set at initialization genericiooptions.IOStreams } var ( selectorLong = templates.LongDesc(i18n.T(` Set the selector on a resource. Note that the new selector will overwrite the old selector if the resource had one prior to the invocation of 'set selector'. A selector must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters. If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used. Note: currently selectors can only be set on Service objects.`)) selectorExample = templates.Examples(` # Set the labels and selector before creating a deployment/service pair kubectl create service clusterip my-svc --clusterip="None" -o yaml --dry-run=client | kubectl set selector --local -f - 'environment=qa' -o yaml | kubectl create -f - kubectl create deployment my-dep -o yaml --dry-run=client | kubectl label --local -f - environment=qa -o yaml | kubectl create -f -`) ) // NewSelectorOptions returns an initialized SelectorOptions instance func NewSelectorOptions(streams genericiooptions.IOStreams) *SetSelectorOptions { return &SetSelectorOptions{ ResourceBuilderFlags: genericclioptions.NewResourceBuilderFlags(). WithScheme(scheme.Scheme). WithAll(false). WithLocal(false). WithLatest(), PrintFlags: genericclioptions.NewPrintFlags("selector updated").WithTypeSetter(scheme.Scheme), RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, IOStreams: streams, } } // NewCmdSelector is the "set selector" command. func NewCmdSelector(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewSelectorOptions(streams) cmd := &cobra.Command{ Use: "selector (-f FILENAME | TYPE NAME) EXPRESSIONS [--resource-version=version]", DisableFlagsInUseLine: true, Short: i18n.T("Set the selector on a resource"), Long: fmt.Sprintf(selectorLong, validation.LabelValueMaxLength), Example: selectorExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunSelector()) }, } o.ResourceBuilderFlags.AddFlags(cmd.Flags()) o.PrintFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-set") cmd.Flags().StringVarP(&o.resourceVersion, "resource-version", "", o.resourceVersion, "If non-empty, the selectors update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource.") cmdutil.AddDryRunFlag(cmd) return cmd } // Complete assigns the SelectorOptions from args. func (o *SetSelectorOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } o.resources, o.selector, err = getResourcesAndSelector(args) if err != nil { return err } o.ResourceFinder = o.ResourceBuilderFlags.ToBuilder(f, o.resources) o.WriteToServer = !(*o.ResourceBuilderFlags.Local || o.dryRunStrategy == cmdutil.DryRunClient) cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj return err } // Validate basic inputs func (o *SetSelectorOptions) Validate() error { if o.selector == nil { return fmt.Errorf("one selector is required") } return nil } // RunSelector executes the command. func (o *SetSelectorOptions) RunSelector() error { r := o.ResourceFinder.Do() return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } patch := &Patch{Info: info} if len(o.resourceVersion) != 0 { // ensure resourceVersion is always sent in the patch by clearing it from the starting JSON accessor, err := meta.Accessor(info.Object) if err != nil { return err } accessor.SetResourceVersion("") } CalculatePatch(patch, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) { if len(o.resourceVersion) != 0 { accessor, err := meta.Accessor(info.Object) if err != nil { return nil, err } accessor.SetResourceVersion(o.resourceVersion) } selectErr := updateSelectorForObject(info.Object, *o.selector) if selectErr != nil { return nil, selectErr } // record this change (for rollout history) if err := o.Recorder.Record(patch.Info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } return runtime.Encode(scheme.DefaultJSONEncoder(), info.Object) }) if patch.Err != nil { return patch.Err } if !o.WriteToServer { return o.PrintObj(info.Object, o.Out) } actual, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { return err } return o.PrintObj(actual, o.Out) }) } func updateSelectorForObject(obj runtime.Object, selector metav1.LabelSelector) error { copyOldSelector := func() (map[string]string, error) { if len(selector.MatchExpressions) > 0 { return nil, fmt.Errorf("match expression %v not supported on this object", selector.MatchExpressions) } dst := make(map[string]string) for label, value := range selector.MatchLabels { dst[label] = value } return dst, nil } var err error switch t := obj.(type) { case *v1.Service: t.Spec.Selector, err = copyOldSelector() default: err = fmt.Errorf("setting a selector is only supported for Services") } return err } // getResourcesAndSelector retrieves resources and the selector expression from the given args (assuming selectors the last arg) func getResourcesAndSelector(args []string) (resources []string, selector *metav1.LabelSelector, err error) { if len(args) == 0 { return []string{}, nil, nil } resources = args[:len(args)-1] selector, err = metav1.ParseToLabelSelector(args[len(args)-1]) return resources, selector, err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_selector_test.go000066400000000000000000000214201504711711200314620ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "reflect" "strings" "testing" "github.com/stretchr/testify/assert" batchv1 "k8s.io/api/batch/v1" "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" ) func TestUpdateSelectorForObjectTypes(t *testing.T) { before := metav1.LabelSelector{MatchLabels: map[string]string{"fee": "true"}, MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "foo", Operator: metav1.LabelSelectorOpIn, Values: []string{"on", "yes"}, }, }} rc := v1.ReplicationController{} ser := v1.Service{} dep := extensionsv1beta1.Deployment{Spec: extensionsv1beta1.DeploymentSpec{Selector: &before}} ds := extensionsv1beta1.DaemonSet{Spec: extensionsv1beta1.DaemonSetSpec{Selector: &before}} rs := extensionsv1beta1.ReplicaSet{Spec: extensionsv1beta1.ReplicaSetSpec{Selector: &before}} job := batchv1.Job{Spec: batchv1.JobSpec{Selector: &before}} pvc := v1.PersistentVolumeClaim{Spec: v1.PersistentVolumeClaimSpec{Selector: &before}} sa := v1.ServiceAccount{} type args struct { obj runtime.Object selector metav1.LabelSelector } tests := []struct { name string args args wantErr bool }{ {name: "rc", args: args{ obj: &rc, selector: metav1.LabelSelector{}, }, wantErr: true, }, {name: "ser", args: args{ obj: &ser, selector: metav1.LabelSelector{}, }, wantErr: false, }, {name: "dep", args: args{ obj: &dep, selector: metav1.LabelSelector{}, }, wantErr: true, }, {name: "ds", args: args{ obj: &ds, selector: metav1.LabelSelector{}, }, wantErr: true, }, {name: "rs", args: args{ obj: &rs, selector: metav1.LabelSelector{}, }, wantErr: true, }, {name: "job", args: args{ obj: &job, selector: metav1.LabelSelector{}, }, wantErr: true, }, {name: "pvc - no updates", args: args{ obj: &pvc, selector: metav1.LabelSelector{}, }, wantErr: true, }, {name: "sa - no selector", args: args{ obj: &sa, selector: metav1.LabelSelector{}, }, wantErr: true, }, } for _, tt := range tests { if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr { t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr) } } } func TestUpdateNewSelectorValuesForObject(t *testing.T) { ser := v1.Service{} type args struct { obj runtime.Object selector metav1.LabelSelector } tests := []struct { name string args args wantErr bool }{ {name: "empty", args: args{ obj: &ser, selector: metav1.LabelSelector{ MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{}, }, }, wantErr: false, }, {name: "label-only", args: args{ obj: &ser, selector: metav1.LabelSelector{ MatchLabels: map[string]string{"b": "u"}, MatchExpressions: []metav1.LabelSelectorRequirement{}, }, }, wantErr: false, }, } for _, tt := range tests { if err := updateSelectorForObject(tt.args.obj, tt.args.selector); (err != nil) != tt.wantErr { t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr) } assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name) } } func TestUpdateOldSelectorValuesForObject(t *testing.T) { ser := v1.Service{Spec: v1.ServiceSpec{Selector: map[string]string{"fee": "true"}}} type args struct { obj runtime.Object selector metav1.LabelSelector } tests := []struct { name string args args wantErr bool }{ {name: "empty", args: args{ obj: &ser, selector: metav1.LabelSelector{ MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{}, }, }, wantErr: false, }, {name: "label-only", args: args{ obj: &ser, selector: metav1.LabelSelector{ MatchLabels: map[string]string{"fee": "false", "x": "y"}, MatchExpressions: []metav1.LabelSelectorRequirement{}, }, }, wantErr: false, }, {name: "expr-only - err", args: args{ obj: &ser, selector: metav1.LabelSelector{ MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "a", Operator: "In", Values: []string{"x", "y"}, }, }, }, }, wantErr: true, }, {name: "both - err", args: args{ obj: &ser, selector: metav1.LabelSelector{ MatchLabels: map[string]string{"b": "u"}, MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "a", Operator: "In", Values: []string{"x", "y"}, }, }, }, }, wantErr: true, }, } for _, tt := range tests { err := updateSelectorForObject(tt.args.obj, tt.args.selector) if (err != nil) != tt.wantErr { t.Errorf("%q. updateSelectorForObject() error = %v, wantErr %v", tt.name, err, tt.wantErr) } else if !tt.wantErr { assert.EqualValues(t, tt.args.selector.MatchLabels, ser.Spec.Selector, tt.name) } } } func TestGetResourcesAndSelector(t *testing.T) { type args struct { args []string } tests := []struct { name string args args wantResources []string wantSelector *metav1.LabelSelector wantErr bool }{ { name: "basic match", args: args{args: []string{"rc/foo", "healthy=true"}}, wantResources: []string{"rc/foo"}, wantErr: false, wantSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"healthy": "true"}, MatchExpressions: []metav1.LabelSelectorRequirement{}, }, }, { name: "basic expression", args: args{args: []string{"rc/foo", "buildType notin (debug, test)"}}, wantResources: []string{"rc/foo"}, wantErr: false, wantSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "buildType", Operator: "NotIn", Values: []string{"debug", "test"}, }, }, }, }, { name: "selector error", args: args{args: []string{"rc/foo", "buildType notthis (debug, test)"}}, wantResources: []string{"rc/foo"}, wantErr: true, wantSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{}, }, }, { name: "no resource and selector", args: args{args: []string{}}, wantResources: []string{}, wantErr: false, wantSelector: nil, }, } for _, tt := range tests { gotResources, gotSelector, err := getResourcesAndSelector(tt.args.args) if err != nil { if !tt.wantErr { t.Errorf("%q. getResourcesAndSelector() error = %v, wantErr %v", tt.name, err, tt.wantErr) } continue } if !reflect.DeepEqual(gotResources, tt.wantResources) { t.Errorf("%q. getResourcesAndSelector() gotResources = %v, want %v", tt.name, gotResources, tt.wantResources) } if !reflect.DeepEqual(gotSelector, tt.wantSelector) { t.Errorf("%q. getResourcesAndSelector() gotSelector = %v, want %v", tt.name, gotSelector, tt.wantSelector) } } } func TestSelectorTest(t *testing.T) { info := &resource.Info{ Object: &v1.Service{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"}, ObjectMeta: metav1.ObjectMeta{Namespace: "some-ns", Name: "cassandra"}, }, } labelToSet, err := metav1.ParseToLabelSelector("environment=qa") if err != nil { t.Fatal(err) } iostreams, _, buf, _ := genericiooptions.NewTestIOStreams() o := &SetSelectorOptions{ selector: labelToSet, ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(info), Recorder: genericclioptions.NoopRecorder{}, PrintObj: (&printers.NamePrinter{}).PrintObj, IOStreams: iostreams, } if err := o.RunSelector(); err != nil { t.Fatal(err) } if !strings.Contains(buf.String(), "service/cassandra") { t.Errorf("did not set selector: %s", buf.String()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_serviceaccount.go000066400000000000000000000165471504711711200316360ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "errors" "fmt" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( serviceaccountResources = i18n.T(`replicationcontroller (rc), deployment (deploy), daemonset (ds), job, replicaset (rs), statefulset`) serviceaccountLong = templates.LongDesc(i18n.T(` Update the service account of pod template resources. Possible resources (case insensitive) can be: `) + serviceaccountResources) serviceaccountExample = templates.Examples(i18n.T(` # Set deployment nginx-deployment's service account to serviceaccount1 kubectl set serviceaccount deployment nginx-deployment serviceaccount1 # Print the result (in YAML format) of updated nginx deployment with the service account from local file, without hitting the API server kubectl set sa -f nginx-deployment.yaml serviceaccount1 --local --dry-run=client -o yaml `)) ) // SetServiceAccountOptions encapsulates the data required to perform the operation. type SetServiceAccountOptions struct { PrintFlags *genericclioptions.PrintFlags RecordFlags *genericclioptions.RecordFlags fileNameOptions resource.FilenameOptions dryRunStrategy cmdutil.DryRunStrategy shortOutput bool all bool output string local bool updatePodSpecForObject polymorphichelpers.UpdatePodSpecForObjectFunc infos []*resource.Info serviceAccountName string fieldManager string PrintObj printers.ResourcePrinterFunc Recorder genericclioptions.Recorder genericiooptions.IOStreams } // NewSetServiceAccountOptions returns an initialized SetServiceAccountOptions instance func NewSetServiceAccountOptions(streams genericiooptions.IOStreams) *SetServiceAccountOptions { return &SetServiceAccountOptions{ PrintFlags: genericclioptions.NewPrintFlags("serviceaccount updated").WithTypeSetter(scheme.Scheme), RecordFlags: genericclioptions.NewRecordFlags(), Recorder: genericclioptions.NoopRecorder{}, IOStreams: streams, } } // NewCmdServiceAccount returns the "set serviceaccount" command. func NewCmdServiceAccount(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewSetServiceAccountOptions(streams) cmd := &cobra.Command{ Use: "serviceaccount (-f FILENAME | TYPE NAME) SERVICE_ACCOUNT", DisableFlagsInUseLine: true, Aliases: []string{"sa"}, Short: i18n.T("Update the service account of a resource"), Long: serviceaccountLong, Example: serviceaccountExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd) usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &o.fileNameOptions, usage) cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all resources, in the namespace of the specified resource types") cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, set serviceaccount will NOT contact api-server but run locally.") cmdutil.AddDryRunFlag(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-set") return cmd } // Complete configures serviceAccountConfig from command line args. func (o *SetServiceAccountOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } o.shortOutput = cmdutil.GetFlagString(cmd, "output") == "name" o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } if o.local && o.dryRunStrategy == cmdutil.DryRunServer { return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?") } o.output = cmdutil.GetFlagString(cmd, "output") o.updatePodSpecForObject = polymorphichelpers.UpdatePodSpecForObjectFn cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil && !(o.local && clientcmd.IsEmptyConfig(err)) { return err } if len(args) == 0 { return errors.New("serviceaccount is required") } o.serviceAccountName = args[len(args)-1] resources := args[:len(args)-1] builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). LocalParam(o.local). ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.fileNameOptions). Flatten() if !o.local { builder.ResourceTypeOrNameArgs(o.all, resources...). Latest() } o.infos, err = builder.Do().Infos() return err } // Run creates and applies the patch either locally or calling apiserver. func (o *SetServiceAccountOptions) Run() error { patchErrs := []error{} patchFn := func(obj runtime.Object) ([]byte, error) { _, err := o.updatePodSpecForObject(obj, func(podSpec *v1.PodSpec) error { podSpec.ServiceAccountName = o.serviceAccountName return nil }) if err != nil { return nil, err } // record this change (for rollout history) if err := o.Recorder.Record(obj); err != nil { klog.V(4).Infof("error recording current command: %v", err) } return runtime.Encode(scheme.DefaultJSONEncoder(), obj) } patches := CalculatePatches(o.infos, scheme.DefaultJSONEncoder(), patchFn) for _, patch := range patches { info := patch.Info name := info.ObjectName() if patch.Err != nil { patchErrs = append(patchErrs, fmt.Errorf("error: %s %v\n", name, patch.Err)) continue } if o.local || o.dryRunStrategy == cmdutil.DryRunClient { if err := o.PrintObj(info.Object, o.Out); err != nil { patchErrs = append(patchErrs, err) } continue } actual, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { patchErrs = append(patchErrs, fmt.Errorf("failed to patch ServiceAccountName %v", err)) continue } if err := o.PrintObj(actual, o.Out); err != nil { patchErrs = append(patchErrs, err) } } return utilerrors.NewAggregate(patchErrs) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_serviceaccount_test.go000066400000000000000000000315741504711711200326720ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "io" "net/http" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" ) const ( serviceAccount = "serviceaccount1" serviceAccountMissingErrString = "serviceaccount is required" resourceMissingErrString = `You must provide one or more resources by argument or filename. Example resource specifications include: '-f rsrc.yaml' '--filename=rsrc.json' ' ' ''` ) func TestSetServiceAccountLocal(t *testing.T) { inputs := []struct { yaml string apiGroup string }{ {yaml: "../../../testdata/set/replication.yaml", apiGroup: ""}, {yaml: "../../../testdata/set/daemon.yaml", apiGroup: "extensions"}, {yaml: "../../../testdata/set/redis-slave.yaml", apiGroup: "extensions"}, {yaml: "../../../testdata/set/job.yaml", apiGroup: "batch"}, {yaml: "../../../testdata/set/deployment.yaml", apiGroup: "extensions"}, } for i, input := range inputs { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } outputFormat := "yaml" streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdServiceAccount(tf, streams) cmd.Flags().Set("output", outputFormat) cmd.Flags().Set("local", "true") saConfig := SetServiceAccountOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), fileNameOptions: resource.FilenameOptions{ Filenames: []string{input.yaml}}, local: true, IOStreams: streams, } err := saConfig.Complete(tf, cmd, []string{serviceAccount}) assert.NoError(t, err) err = saConfig.Run() assert.NoError(t, err) assert.Containsf(t, buf.String(), "serviceAccountName: "+serviceAccount, "serviceaccount not updated for %s", input.yaml) }) } } func TestSetServiceAccountMultiLocal(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: ""}, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Version: ""}}} outputFormat := "name" streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdServiceAccount(tf, streams) cmd.Flags().Set("output", outputFormat) cmd.Flags().Set("local", "true") opts := SetServiceAccountOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), fileNameOptions: resource.FilenameOptions{ Filenames: []string{"../../../testdata/set/multi-resource-yaml.yaml"}}, local: true, IOStreams: streams, } err := opts.Complete(tf, cmd, []string{serviceAccount}) if err == nil { err = opts.Run() } if err != nil { t.Fatalf("unexpected error: %v", err) } expectedOut := "replicationcontroller/first-rc\nreplicationcontroller/second-rc\n" if buf.String() != expectedOut { t.Errorf("expected out:\n%s\nbut got:\n%s", expectedOut, buf.String()) } } func TestSetServiceAccountRemote(t *testing.T) { inputs := []struct { object runtime.Object groupVersion schema.GroupVersion path string args []string }{ { object: &extensionsv1beta1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", serviceAccount}, }, { object: &appsv1beta2.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1beta2.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", serviceAccount}, }, { object: &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.ReplicaSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/replicasets/nginx", args: []string{"replicaset", "nginx", serviceAccount}, }, { object: &extensionsv1beta1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", serviceAccount}, }, { object: &appsv1beta2.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", serviceAccount}, }, { object: &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/daemonsets/nginx", args: []string{"daemonset", "nginx", serviceAccount}, }, { object: &extensionsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: extensionsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", serviceAccount}, }, { object: &appsv1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", serviceAccount}, }, { object: &appsv1beta2.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", serviceAccount}, }, { object: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/deployments/nginx", args: []string{"deployment", "nginx", serviceAccount}, }, { object: &appsv1beta1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: appsv1beta1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", serviceAccount}, }, { object: &appsv1beta2.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: appsv1beta2.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", serviceAccount}, }, { object: &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, Spec: appsv1.StatefulSetSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx", }, }, }, }, }, }, groupVersion: appsv1.SchemeGroupVersion, path: "/namespaces/test/statefulsets/nginx", args: []string{"statefulset", "nginx", serviceAccount}, }, { object: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: batchv1.SchemeGroupVersion, path: "/namespaces/test/jobs/nginx", args: []string{"job", "nginx", serviceAccount}, }, { object: &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, groupVersion: corev1.SchemeGroupVersion, path: "/namespaces/test/replicationcontrollers/nginx", args: []string{"replicationcontroller", "nginx", serviceAccount}, }, } for i, input := range inputs { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: input.groupVersion, NegotiatedSerializer: scheme.Codecs.WithoutConversion(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == input.path && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil case p == input.path && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := io.ReadAll(stream) if err != nil { return nil, err } assert.Containsf(t, string(bytes), `"serviceAccountName":`+`"`+serviceAccount+`"`, "serviceaccount not updated for %#v", input.object) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: objBody(input.object)}, nil default: t.Errorf("%s: unexpected request: %s %#v\n%#v", "serviceaccount", req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), } outputFormat := "yaml" streams := genericiooptions.NewTestIOStreamsDiscard() cmd := NewCmdServiceAccount(tf, streams) cmd.Flags().Set("output", outputFormat) saConfig := SetServiceAccountOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), local: false, IOStreams: streams, } err := saConfig.Complete(tf, cmd, input.args) assert.NoError(t, err) err = saConfig.Run() assert.NoError(t, err) }) } } func TestServiceAccountValidation(t *testing.T) { inputs := []struct { name string args []string errorString string }{ {name: "test service account missing", args: []string{}, errorString: serviceAccountMissingErrString}, {name: "test service account resource missing", args: []string{serviceAccount}, errorString: resourceMissingErrString}, } for _, input := range inputs { t.Run(input.name, func(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tf.Client = &fake.RESTClient{ GroupVersion: schema.GroupVersion{Version: "v1"}, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) return nil, nil }), } outputFormat := "" streams := genericiooptions.NewTestIOStreamsDiscard() cmd := NewCmdServiceAccount(tf, streams) saConfig := &SetServiceAccountOptions{ PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput(outputFormat).WithTypeSetter(scheme.Scheme), IOStreams: streams, } err := saConfig.Complete(tf, cmd, input.args) assert.EqualError(t, err, input.errorString) }) } } func objBody(obj runtime.Object) io.ReadCloser { return cmdtesting.BytesBody([]byte(runtime.EncodeOrDie(scheme.DefaultJSONEncoder(), obj))) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_subject.go000066400000000000000000000240341504711711200302460ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "fmt" "strings" "github.com/spf13/cobra" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/clientcmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( subjectLong = templates.LongDesc(i18n.T(` Update the user, group, or service account in a role binding or cluster role binding.`)) subjectExample = templates.Examples(` # Update a cluster role binding for serviceaccount1 kubectl set subject clusterrolebinding admin --serviceaccount=namespace:serviceaccount1 # Update a role binding for user1, user2, and group1 kubectl set subject rolebinding admin --user=user1 --user=user2 --group=group1 # Print the result (in YAML format) of updating rolebinding subjects from a local, without hitting the server kubectl create rolebinding admin --role=admin --user=admin -o yaml --dry-run=client | kubectl set subject --local -f - --user=foo -o yaml`) ) type updateSubjects func(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject) // SubjectOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of // referencing the cmd.Flags type SubjectOptions struct { PrintFlags *genericclioptions.PrintFlags resource.FilenameOptions Infos []*resource.Info Selector string ContainerSelector string Output string All bool DryRunStrategy cmdutil.DryRunStrategy Local bool fieldManager string Users []string Groups []string ServiceAccounts []string namespace string PrintObj printers.ResourcePrinterFunc genericiooptions.IOStreams } // NewSubjectOptions returns an initialized SubjectOptions instance func NewSubjectOptions(streams genericiooptions.IOStreams) *SubjectOptions { return &SubjectOptions{ PrintFlags: genericclioptions.NewPrintFlags("subjects updated").WithTypeSetter(scheme.Scheme), IOStreams: streams, } } // NewCmdSubject returns the "new subject" sub command func NewCmdSubject(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewSubjectOptions(streams) cmd := &cobra.Command{ Use: "subject (-f FILENAME | TYPE NAME) [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run=server|client|none]", DisableFlagsInUseLine: true, Short: i18n.T("Update the user, group, or service account in a role binding or cluster role binding"), Long: subjectLong, Example: subjectExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run(addSubjects)) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "the resource to update the subjects") cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, in the namespace of the specified resource types") cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set subject will NOT contact api-server but run locally.") cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringArrayVar(&o.Users, "user", o.Users, "Usernames to bind to the role") cmd.Flags().StringArrayVar(&o.Groups, "group", o.Groups, "Groups to bind to the role") cmd.Flags().StringArrayVar(&o.ServiceAccounts, "serviceaccount", o.ServiceAccounts, "Service accounts to bind to the role") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-set") return cmd } // Complete completes all required options func (o *SubjectOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { o.Output = cmdutil.GetFlagString(cmd, "output") var err error o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = printer.PrintObj var enforceNamespace bool o.namespace, enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil && !(o.Local && clientcmd.IsEmptyConfig(err)) { return err } builder := f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). LocalParam(o.Local). ContinueOnError(). NamespaceParam(o.namespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). Flatten() if o.Local { // if a --local flag was provided, and a resource was specified in the form // /, fail immediately as --local cannot query the api server // for the specified resource. if len(args) > 0 { return resource.LocalResourceError } } else { builder = builder. LabelSelectorParam(o.Selector). ResourceTypeOrNameArgs(o.All, args...). Latest() } o.Infos, err = builder.Do().Infos() if err != nil { return err } return nil } // Validate makes sure provided values in SubjectOptions are valid func (o *SubjectOptions) Validate() error { if o.Local && o.DryRunStrategy == cmdutil.DryRunServer { return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?") } if o.All && len(o.Selector) > 0 { return fmt.Errorf("cannot set --all and --selector at the same time") } if len(o.Users) == 0 && len(o.Groups) == 0 && len(o.ServiceAccounts) == 0 { return fmt.Errorf("you must specify at least one value of user, group or serviceaccount") } for _, sa := range o.ServiceAccounts { tokens := strings.Split(sa, ":") if len(tokens) != 2 || tokens[1] == "" { return fmt.Errorf("serviceaccount must be :") } for _, info := range o.Infos { _, ok := info.Object.(*rbacv1.ClusterRoleBinding) if ok && tokens[0] == "" { return fmt.Errorf("serviceaccount must be :, namespace must be specified") } } } return nil } // Run performs the execution of "set subject" sub command func (o *SubjectOptions) Run(fn updateSubjects) error { patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) { subjects := []rbacv1.Subject{} for _, user := range sets.List(sets.New[string](o.Users...)) { subject := rbacv1.Subject{ Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: user, } subjects = append(subjects, subject) } for _, group := range sets.List(sets.New[string](o.Groups...)) { subject := rbacv1.Subject{ Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: group, } subjects = append(subjects, subject) } for _, sa := range sets.List(sets.New[string](o.ServiceAccounts...)) { tokens := strings.Split(sa, ":") namespace := tokens[0] name := tokens[1] if len(namespace) == 0 { namespace = o.namespace } subject := rbacv1.Subject{ Kind: rbacv1.ServiceAccountKind, Namespace: namespace, Name: name, } subjects = append(subjects, subject) } transformed, err := updateSubjectForObject(obj, subjects, fn) if transformed && err == nil { // TODO: switch UpdatePodSpecForObject to work on v1.PodSpec return runtime.Encode(scheme.DefaultJSONEncoder(), obj) } return nil, err }) allErrs := []error{} for _, patch := range patches { info := patch.Info name := info.ObjectName() if patch.Err != nil { allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err)) continue } //no changes if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { continue } if o.Local || o.DryRunStrategy == cmdutil.DryRunClient { if err := o.PrintObj(info.Object, o.Out); err != nil { allErrs = append(allErrs, err) } continue } actual, err := resource. NewHelper(info.Client, info.Mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) if err != nil { allErrs = append(allErrs, fmt.Errorf("failed to patch subjects to rolebinding: %v", err)) continue } if err := o.PrintObj(actual, o.Out); err != nil { allErrs = append(allErrs, err) } } return utilerrors.NewAggregate(allErrs) } // Note: the obj mutates in the function func updateSubjectForObject(obj runtime.Object, subjects []rbacv1.Subject, fn updateSubjects) (bool, error) { switch t := obj.(type) { case *rbacv1.RoleBinding: transformed, result := fn(t.Subjects, subjects) t.Subjects = result return transformed, nil case *rbacv1.ClusterRoleBinding: transformed, result := fn(t.Subjects, subjects) t.Subjects = result return transformed, nil default: return false, fmt.Errorf("setting subjects is only supported for RoleBinding/ClusterRoleBinding") } } func addSubjects(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject) { transformed := false updated := existings for _, item := range targets { if !contain(existings, item) { updated = append(updated, item) transformed = true } } return transformed, updated } func contain(slice []rbacv1.Subject, item rbacv1.Subject) bool { for _, v := range slice { if v == item { return true } } return false } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_subject_test.go000066400000000000000000000221041504711711200313010ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "reflect" "testing" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/resource" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestValidate(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() tests := map[string]struct { options *SubjectOptions expectErr bool }{ "test-missing-subjects": { options: &SubjectOptions{ Users: []string{}, Groups: []string{}, ServiceAccounts: []string{}, }, expectErr: true, }, "test-invalid-serviceaccounts": { options: &SubjectOptions{ Users: []string{}, Groups: []string{}, ServiceAccounts: []string{"foo"}, }, expectErr: true, }, "test-missing-serviceaccounts-name": { options: &SubjectOptions{ Users: []string{}, Groups: []string{}, ServiceAccounts: []string{"foo:"}, }, expectErr: true, }, "test-missing-serviceaccounts-namespace": { options: &SubjectOptions{ Infos: []*resource.Info{ { Object: &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "clusterrolebinding", }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "role", }, }, }, }, Users: []string{}, Groups: []string{}, ServiceAccounts: []string{":foo"}, }, expectErr: true, }, "test-valid-case": { options: &SubjectOptions{ Infos: []*resource.Info{ { Object: &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "rolebinding", Namespace: "one", }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "role", }, }, }, }, Users: []string{"foo"}, Groups: []string{"foo"}, ServiceAccounts: []string{"ns:foo"}, }, expectErr: false, }, } for name, test := range tests { err := test.options.Validate() if test.expectErr && err != nil { continue } if !test.expectErr && err != nil { t.Errorf("%s: unexpected error: %v", name, err) } } } func TestUpdateSubjectForObject(t *testing.T) { tests := []struct { Name string obj runtime.Object subjects []rbacv1.Subject expected []rbacv1.Subject wantErr bool }{ { Name: "invalid object type", obj: &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: "role", Namespace: "one", }, }, wantErr: true, }, { Name: "add resource with users in rolebinding", obj: &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "rolebinding", Namespace: "one", }, Subjects: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, }, }, subjects: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "b", }, }, expected: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "b", }, }, wantErr: false, }, { Name: "add resource with groups in rolebinding", obj: &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "rolebinding", Namespace: "one", }, Subjects: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "a", }, }, }, subjects: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "b", }, }, expected: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "b", }, }, wantErr: false, }, { Name: "add resource with serviceaccounts in rolebinding", obj: &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "rolebinding", Namespace: "one", }, Subjects: []rbacv1.Subject{ { Kind: "ServiceAccount", Namespace: "one", Name: "a", }, }, }, subjects: []rbacv1.Subject{ { Kind: "ServiceAccount", Namespace: "one", Name: "a", }, { Kind: "ServiceAccount", Namespace: "one", Name: "b", }, }, expected: []rbacv1.Subject{ { Kind: "ServiceAccount", Namespace: "one", Name: "a", }, { Kind: "ServiceAccount", Namespace: "one", Name: "b", }, }, wantErr: false, }, { Name: "add resource with serviceaccounts in clusterrolebinding", obj: &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "clusterrolebinding", }, Subjects: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "a", }, }, }, subjects: []rbacv1.Subject{ { Kind: "ServiceAccount", Namespace: "one", Name: "a", }, }, expected: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "Group", Name: "a", }, { Kind: "ServiceAccount", Namespace: "one", Name: "a", }, }, wantErr: false, }, } for _, tt := range tests { if _, err := updateSubjectForObject(tt.obj, tt.subjects, addSubjects); (err != nil) != tt.wantErr { t.Errorf("%q. updateSubjectForObject() error = %v, wantErr %v", tt.Name, err, tt.wantErr) } want := tt.expected var got []rbacv1.Subject switch t := tt.obj.(type) { case *rbacv1.RoleBinding: got = t.Subjects case *rbacv1.ClusterRoleBinding: got = t.Subjects } if !reflect.DeepEqual(got, want) { t.Errorf("%q. updateSubjectForObject() failed", tt.Name) t.Errorf("Got: %v", got) t.Errorf("Want: %v", want) } } } func TestAddSubject(t *testing.T) { tests := []struct { Name string existing []rbacv1.Subject subjects []rbacv1.Subject expected []rbacv1.Subject wantChange bool }{ { Name: "add resource with users", existing: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "b", }, }, subjects: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, }, expected: []rbacv1.Subject{ { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "a", }, { APIGroup: "rbac.authorization.k8s.io", Kind: "User", Name: "b", }, }, wantChange: false, }, { Name: "add resource with serviceaccounts", existing: []rbacv1.Subject{ { Kind: "ServiceAccount", Namespace: "one", Name: "a", }, { Kind: "ServiceAccount", Namespace: "one", Name: "b", }, }, subjects: []rbacv1.Subject{ { Kind: "ServiceAccount", Namespace: "two", Name: "a", }, }, expected: []rbacv1.Subject{ { Kind: "ServiceAccount", Namespace: "one", Name: "a", }, { Kind: "ServiceAccount", Namespace: "one", Name: "b", }, { Kind: "ServiceAccount", Namespace: "two", Name: "a", }, }, wantChange: true, }, } for _, tt := range tests { changed := false var got []rbacv1.Subject if changed, got = addSubjects(tt.existing, tt.subjects); (changed != false) != tt.wantChange { t.Errorf("%q. addSubjects() changed = %v, wantChange = %v", tt.Name, changed, tt.wantChange) } want := tt.expected if !reflect.DeepEqual(got, want) { t.Errorf("%q. addSubjects() failed", tt.Name) t.Errorf("Got: %v", got) t.Errorf("Want: %v", want) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/set/set_test.go000066400000000000000000000027161504711711200275710ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "testing" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" clientcmdutil "k8s.io/kubectl/pkg/cmd/util" ) func TestLocalAndDryRunFlags(t *testing.T) { f := clientcmdutil.NewFactory(genericclioptions.NewTestConfigFlags()) setCmd := NewCmdSet(f, genericiooptions.NewTestIOStreamsDiscard()) ensureLocalAndDryRunFlagsOnChildren(t, setCmd, "") } func ensureLocalAndDryRunFlagsOnChildren(t *testing.T, c *cobra.Command, prefix string) { for _, cmd := range c.Commands() { name := prefix + cmd.Name() if localFlag := cmd.Flag("local"); localFlag == nil { t.Errorf("Command %s does not implement the --local flag", name) } if dryRunFlag := cmd.Flag("dry-run"); dryRunFlag == nil { t.Errorf("Command %s does not implement the --dry-run flag", name) } ensureLocalAndDryRunFlagsOnChildren(t, cmd, name+".") } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/skiplookerr_go118.go000066400000000000000000000012621504711711200304220ustar00rootroot00000000000000//go:build !go1.19 // +build !go1.19 /* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd func shouldSkipOnLookPathErr(err error) bool { return err != nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/skiplookerr_go119.go000066400000000000000000000013611504711711200304230ustar00rootroot00000000000000//go:build go1.19 // +build go1.19 /* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmd import ( "errors" "os/exec" ) func shouldSkipOnLookPathErr(err error) bool { return err != nil && !errors.Is(err, exec.ErrDot) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/taint/000077500000000000000000000000001504711711200257265ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/taint/taint.go000066400000000000000000000275101504711711200274010ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package taint import ( "encoding/json" "fmt" "strings" "github.com/spf13/cobra" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/explain" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // TaintOptions have the data required to perform the taint operation type TaintOptions struct { PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) DryRunStrategy cmdutil.DryRunStrategy ValidationDirective string resources []string taintsToAdd []v1.Taint taintsToRemove []v1.Taint builder *resource.Builder selector string overwrite bool all bool fieldManager string ClientForMapping func(*meta.RESTMapping) (resource.RESTClient, error) genericiooptions.IOStreams Mapper meta.RESTMapper } var ( taintLong = templates.LongDesc(i18n.T(` Update the taints on one or more nodes. * A taint consists of a key, value, and effect. As an argument here, it is expressed as key=value:effect. * The key must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters. * Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app. * The value is optional. If given, it must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[2]d characters. * The effect must be NoSchedule, PreferNoSchedule or NoExecute. * Currently taint can only apply to node.`)) taintExample = templates.Examples(i18n.T(` # Update node 'foo' with a taint with key 'dedicated' and value 'special-user' and effect 'NoSchedule' # If a taint with that key and effect already exists, its value is replaced as specified kubectl taint nodes foo dedicated=special-user:NoSchedule # Remove from node 'foo' the taint with key 'dedicated' and effect 'NoSchedule' if one exists kubectl taint nodes foo dedicated:NoSchedule- # Remove from node 'foo' all the taints with key 'dedicated' kubectl taint nodes foo dedicated- # Add a taint with key 'dedicated' on nodes having label myLabel=X kubectl taint node -l myLabel=X dedicated=foo:PreferNoSchedule # Add to node 'foo' a taint with key 'bar' and no value kubectl taint nodes foo bar:NoSchedule`)) ) func NewCmdTaint(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { options := &TaintOptions{ PrintFlags: genericclioptions.NewPrintFlags("tainted").WithTypeSetter(scheme.Scheme), IOStreams: streams, } validArgs := []string{"node"} cmd := &cobra.Command{ Use: "taint NODE NAME KEY_1=VAL_1:TAINT_EFFECT_1 ... KEY_N=VAL_N:TAINT_EFFECT_N", DisableFlagsInUseLine: true, Short: i18n.T("Update the taints on one or more nodes"), Long: fmt.Sprintf(taintLong, validation.DNS1123SubdomainMaxLength, validation.LabelValueMaxLength), Example: taintExample, ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.Complete(f, cmd, args)) cmdutil.CheckErr(options.Validate()) cmdutil.CheckErr(options.RunTaint()) }, } options.PrintFlags.AddFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddLabelSelectorFlagVar(cmd, &options.selector) cmd.Flags().BoolVar(&options.overwrite, "overwrite", options.overwrite, "If true, allow taints to be overwritten, otherwise reject taint updates that overwrite existing taints.") cmd.Flags().BoolVar(&options.all, "all", options.all, "Select all nodes in the cluster") cmdutil.AddFieldManagerFlagVar(cmd, &options.fieldManager, "kubectl-taint") return cmd } // Complete adapts from the command line args and factory to the data required. func (o *TaintOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) (err error) { namespace, _, err := f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.Mapper, err = f.ToRESTMapper() if err != nil { return err } o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } // retrieves resource and taint args from args // also checks args to verify that all resources are specified before taints taintArgs := []string{} metTaintArg := false for _, s := range args { isTaint := strings.Contains(s, "=") || strings.Contains(s, ":") || strings.HasSuffix(s, "-") switch { case !metTaintArg && isTaint: metTaintArg = true fallthrough case metTaintArg && isTaint: taintArgs = append(taintArgs, s) case !metTaintArg && !isTaint: o.resources = append(o.resources, s) case metTaintArg && !isTaint: return fmt.Errorf("all resources must be specified before taint changes: %s", s) } } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation return o.PrintFlags.ToPrinter() } if len(o.resources) < 1 { return fmt.Errorf("one or more resources must be specified as ") } if len(taintArgs) < 1 { return fmt.Errorf("at least one taint update is required") } if o.taintsToAdd, o.taintsToRemove, err = parseTaints(taintArgs); err != nil { return cmdutil.UsageErrorf(cmd, "%s", err.Error()) } o.builder = f.NewBuilder(). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). ContinueOnError(). NamespaceParam(namespace).DefaultNamespace() if o.selector != "" { o.builder = o.builder.LabelSelectorParam(o.selector).ResourceTypes("node") } if o.all { o.builder = o.builder.SelectAllParam(o.all).ResourceTypes("node").Flatten().Latest() } if !o.all && o.selector == "" && len(o.resources) >= 2 { o.builder = o.builder.ResourceNames("node", o.resources[1:]...) } o.builder = o.builder.LabelSelectorParam(o.selector). Flatten(). Latest() o.ClientForMapping = f.ClientForMapping return nil } // validateFlags checks for the validation of flags for kubectl taints. func (o TaintOptions) validateFlags() error { // Cannot have a non-empty selector and all flag set. They are mutually exclusive. if o.all && o.selector != "" { return fmt.Errorf("setting 'all' parameter with a non empty selector is prohibited") } // If both selector and all are not set. if !o.all && o.selector == "" { if len(o.resources) < 2 { return fmt.Errorf("at least one resource name must be specified since 'all' parameter is not set") } else { return nil } } return nil } // Validate checks to the TaintOptions to see if there is sufficient information run the command. func (o TaintOptions) Validate() error { resourceType := strings.ToLower(o.resources[0]) fullySpecifiedGVR, _, err := explain.SplitAndParseResourceRequest(resourceType, o.Mapper) if err != nil { return err } gvk, err := o.Mapper.KindFor(fullySpecifiedGVR) if err != nil { return err } if gvk.Kind != "Node" { return fmt.Errorf("invalid resource type %s, only node types are supported", resourceType) } // check the format of taint args and checks removed taints aren't in the new taints list var conflictTaints []string for _, taintAdd := range o.taintsToAdd { for _, taintRemove := range o.taintsToRemove { if taintAdd.Key != taintRemove.Key { continue } if len(taintRemove.Effect) == 0 || taintAdd.Effect == taintRemove.Effect { conflictTaint := fmt.Sprintf("%s=%s", taintRemove.Key, taintRemove.Effect) conflictTaints = append(conflictTaints, conflictTaint) } } } if len(conflictTaints) > 0 { return fmt.Errorf("can not both modify and remove the following taint(s) in the same command: %s", strings.Join(conflictTaints, ", ")) } return o.validateFlags() } // RunTaint does the work func (o TaintOptions) RunTaint() error { r := o.builder.Do() if err := r.Err(); err != nil { return err } return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err } obj := info.Object name, namespace := info.Name, info.Namespace oldData, err := json.Marshal(obj) if err != nil { return err } operation, err := o.updateTaints(obj) if err != nil { return err } newData, err := json.Marshal(obj) if err != nil { return err } patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj) createdPatch := err == nil if err != nil { klog.V(2).Infof("couldn't compute patch: %v", err) } printer, err := o.ToPrinter(operation) if err != nil { return err } if o.DryRunStrategy == cmdutil.DryRunClient { if createdPatch { typedObj, err := scheme.Scheme.ConvertToVersion(info.Object, info.Mapping.GroupVersionKind.GroupVersion()) if err != nil { return err } nodeObj, ok := typedObj.(*v1.Node) if !ok { return fmt.Errorf("unexpected type %T", typedObj) } originalObjJS, err := json.Marshal(nodeObj) if err != nil { return err } originalPatchedObjJS, err := strategicpatch.StrategicMergePatch(originalObjJS, patchBytes, nodeObj) if err != nil { return err } targetObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, originalPatchedObjJS) if err != nil { return err } return printer.PrintObj(targetObj, o.Out) } return printer.PrintObj(obj, o.Out) } mapping := info.ResourceMapping() client, err := o.ClientForMapping(mapping) if err != nil { return err } helper := resource. NewHelper(client, mapping). DryRun(o.DryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). WithFieldValidation(o.ValidationDirective) var outputObj runtime.Object if createdPatch { outputObj, err = helper.Patch(namespace, name, types.StrategicMergePatchType, patchBytes, nil) } else { outputObj, err = helper.Replace(namespace, name, false, obj) } if err != nil { return err } return printer.PrintObj(outputObj, o.Out) }) } // updateTaints applies a taint option(o) to a node in cluster after computing the net effect of operation(i.e. does it result in an overwrite?), it reports back the end result in a way that user can easily interpret. func (o TaintOptions) updateTaints(obj runtime.Object) (string, error) { node, ok := obj.(*v1.Node) if !ok { return "", fmt.Errorf("unexpected type %T, expected Node", obj) } if !o.overwrite { if exists := checkIfTaintsAlreadyExists(node.Spec.Taints, o.taintsToAdd); len(exists) != 0 { return "", fmt.Errorf("node %s already has %v taint(s) with same effect(s) and --overwrite is false", node.Name, exists) } } operation, newTaints, err := reorganizeTaints(node, o.overwrite, o.taintsToAdd, o.taintsToRemove) if err != nil { return "", err } node.Spec.Taints = newTaints return operation, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/taint/taint_test.go000066400000000000000000000270251504711711200304410ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package taint import ( "io" "net/http" "reflect" "strings" "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" ) func generateNodeAndTaintedNode(oldTaints []corev1.Taint, newTaints []corev1.Taint) (*corev1.Node, *corev1.Node) { var taintedNode *corev1.Node // Create a node. node := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-name", CreationTimestamp: metav1.Time{Time: time.Now()}, }, Spec: corev1.NodeSpec{ Taints: oldTaints, }, Status: corev1.NodeStatus{}, } // A copy of the same node, but tainted. taintedNode = node.DeepCopy() taintedNode.Spec.Taints = newTaints return node, taintedNode } func equalTaints(taintsA, taintsB []corev1.Taint) bool { if len(taintsA) != len(taintsB) { return false } for _, taintA := range taintsA { found := false for _, taintB := range taintsB { if reflect.DeepEqual(taintA, taintB) { found = true break } } if !found { return false } } return true } func TestTaint(t *testing.T) { tests := []struct { description string oldTaints []corev1.Taint newTaints []corev1.Taint args []string expectFatal bool expectTaint bool }{ // success cases { description: "taints a node with effect NoSchedule", newTaints: []corev1.Taint{{ Key: "foo", Value: "bar", Effect: "NoSchedule", }}, args: []string{"node", "node-name", "foo=bar:NoSchedule"}, expectFatal: false, expectTaint: true, }, { description: "taints a node with effect PreferNoSchedule", newTaints: []corev1.Taint{{ Key: "foo", Value: "bar", Effect: "PreferNoSchedule", }}, args: []string{"node", "node-name", "foo=bar:PreferNoSchedule"}, expectFatal: false, expectTaint: true, }, { description: "update an existing taint on the node, change the value from bar to barz", oldTaints: []corev1.Taint{{ Key: "foo", Value: "bar", Effect: "NoSchedule", }}, newTaints: []corev1.Taint{{ Key: "foo", Value: "barz", Effect: "NoSchedule", }}, args: []string{"node", "node-name", "foo=barz:NoSchedule", "--overwrite"}, expectFatal: false, expectTaint: true, }, { description: "taints a node with two taints", newTaints: []corev1.Taint{{ Key: "dedicated", Value: "namespaceA", Effect: "NoSchedule", }, { Key: "foo", Value: "bar", Effect: "PreferNoSchedule", }}, args: []string{"node", "node-name", "dedicated=namespaceA:NoSchedule", "foo=bar:PreferNoSchedule"}, expectFatal: false, expectTaint: true, }, { description: "node has two taints with the same key but different effect, remove one of them by indicating exact key and effect", oldTaints: []corev1.Taint{{ Key: "dedicated", Value: "namespaceA", Effect: "NoSchedule", }, { Key: "dedicated", Value: "namespaceA", Effect: "PreferNoSchedule", }}, newTaints: []corev1.Taint{{ Key: "dedicated", Value: "namespaceA", Effect: "PreferNoSchedule", }}, args: []string{"node", "node-name", "dedicated:NoSchedule-"}, expectFatal: false, expectTaint: true, }, { description: "node has two taints with the same key but different effect, remove all of them with wildcard", oldTaints: []corev1.Taint{{ Key: "dedicated", Value: "namespaceA", Effect: "NoSchedule", }, { Key: "dedicated", Value: "namespaceA", Effect: "PreferNoSchedule", }}, newTaints: []corev1.Taint{}, args: []string{"node", "node-name", "dedicated-"}, expectFatal: false, expectTaint: true, }, { description: "node has two taints, update one of them and remove the other", oldTaints: []corev1.Taint{{ Key: "dedicated", Value: "namespaceA", Effect: "NoSchedule", }, { Key: "foo", Value: "bar", Effect: "PreferNoSchedule", }}, newTaints: []corev1.Taint{{ Key: "foo", Value: "barz", Effect: "PreferNoSchedule", }}, args: []string{"node", "node-name", "dedicated:NoSchedule-", "foo=barz:PreferNoSchedule", "--overwrite"}, expectFatal: false, expectTaint: true, }, // error cases { description: "invalid taint key", args: []string{"node", "node-name", "nospecialchars^@=banana:NoSchedule"}, expectFatal: true, expectTaint: false, }, { description: "invalid taint effect", args: []string{"node", "node-name", "foo=bar:NoExcute"}, expectFatal: true, expectTaint: false, }, { description: "duplicated taints with the same key and effect should be rejected", args: []string{"node", "node-name", "foo=bar:NoExcute", "foo=barz:NoExcute"}, expectFatal: true, expectTaint: false, }, { description: "add and remove taint with same key and effect should be rejected", args: []string{"node", "node-name", "foo=:NoExcute", "foo=:NoExcute-"}, expectFatal: true, expectTaint: false, }, { description: "can't update existing taint on the node, since 'overwrite' flag is not set", oldTaints: []corev1.Taint{{ Key: "foo", Value: "bar", Effect: "NoSchedule", }}, newTaints: []corev1.Taint{{ Key: "foo", Value: "bar", Effect: "NoSchedule", }}, args: []string{"node", "node-name", "foo=bar:NoSchedule"}, expectFatal: true, expectTaint: false, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { oldNode, expectNewNode := generateNodeAndTaintedNode(test.oldTaints, test.newTaints) newNode := &corev1.Node{} tainted := false tf := cmdtesting.NewTestFactory() defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, GroupVersion: corev1.SchemeGroupVersion, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { m := &MyReq{req} switch { case m.isFor("GET", "/nodes"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, oldNode)}, nil case m.isFor("GET", "/nodes/node-name"): return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, oldNode)}, nil case m.isFor("PATCH", "/nodes/node-name"): tainted = true data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } defer req.Body.Close() // apply the patch oldJSON, err := runtime.Encode(codec, oldNode) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{}) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } // decode the patch if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } if !equalTaints(expectNewNode.Spec.Taints, newNode.Spec.Taints) { t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, expectNewNode.Spec.Taints, newNode.Spec.Taints) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil case m.isFor("PUT", "/nodes/node-name"): tainted = true data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } defer req.Body.Close() if err := runtime.DecodeInto(codec, data, newNode); err != nil { t.Fatalf("%s: unexpected error: %v", test.description, err) } if !equalTaints(expectNewNode.Spec.Taints, newNode.Spec.Taints) { t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, expectNewNode.Spec.Taints, newNode.Spec.Taints) } return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil default: t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() cmd := NewCmdTaint(tf, genericiooptions.NewTestIOStreamsDiscard()) sawFatal := false func() { defer func() { // Recover from the panic below. if r := recover(); r != nil { t.Logf("Recovered: %v", r) } // Restore cmdutil behavior cmdutil.DefaultBehaviorOnFatal() }() cmdutil.BehaviorOnFatal(func(e string, code int) { sawFatal = true; panic(e) }) cmd.SetArgs(test.args) cmd.Execute() }() if test.expectFatal { if !sawFatal { t.Fatalf("%s: unexpected non-error", test.description) } } if test.expectTaint { if !tainted { t.Fatalf("%s: node not tainted", test.description) } } if !test.expectTaint { if tainted { t.Fatalf("%s: unexpected taint", test.description) } } }) } } func TestValidateFlags(t *testing.T) { tests := []struct { taintOpts TaintOptions description string expectFatal bool }{ { taintOpts: TaintOptions{selector: "myLabel=X", all: false}, description: "With Selector and without All flag", expectFatal: false, }, { taintOpts: TaintOptions{selector: "", all: true}, description: "Without selector and All flag", expectFatal: false, }, { taintOpts: TaintOptions{selector: "myLabel=X", all: true}, description: "With Selector and with All flag", expectFatal: true, }, { taintOpts: TaintOptions{selector: "", all: false, resources: []string{"node"}}, description: "Without Selector and All flags and if node name is not provided", expectFatal: true, }, { taintOpts: TaintOptions{selector: "", all: false, resources: []string{"node", "node-name"}}, description: "Without Selector and ALL flags and if node name is provided", expectFatal: false, }, } for _, test := range tests { sawFatal := false err := test.taintOpts.validateFlags() if err != nil { sawFatal = true } if test.expectFatal { if !sawFatal { t.Fatalf("%s expected not to fail", test.description) } } } } type MyReq struct { Request *http.Request } func (m *MyReq) isFor(method string, path string) bool { req := m.Request return method == req.Method && (req.URL.Path == path || req.URL.Path == strings.Join([]string{"/api/v1", path}, "") || req.URL.Path == strings.Join([]string{"/apis/extensions/v1beta1", path}, "") || req.URL.Path == strings.Join([]string{"/apis/batch/v1", path}, "")) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/taint/utils.go000066400000000000000000000161101504711711200274140ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package taints implements utilites for working with taints package taint import ( "fmt" "strings" corev1 "k8s.io/api/core/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" ) // Exported taint constant strings const ( MODIFIED = "modified" TAINTED = "tainted" UNTAINTED = "untainted" ) // parseTaints takes a spec which is an array and creates slices for new taints to be added, taints to be deleted. // It also validates the spec. For example, the form `` may be used to remove a taint, but not to add one. func parseTaints(spec []string) ([]corev1.Taint, []corev1.Taint, error) { var taints, taintsToRemove []corev1.Taint uniqueTaints := map[corev1.TaintEffect]sets.Set[string]{} for _, taintSpec := range spec { if strings.HasSuffix(taintSpec, "-") { taintToRemove, err := parseTaint(strings.TrimSuffix(taintSpec, "-")) if err != nil { return nil, nil, err } taintsToRemove = append(taintsToRemove, corev1.Taint{Key: taintToRemove.Key, Effect: taintToRemove.Effect}) } else { newTaint, err := parseTaint(taintSpec) if err != nil { return nil, nil, err } // validate that the taint has an effect, which is required to add the taint if len(newTaint.Effect) == 0 { return nil, nil, fmt.Errorf("invalid taint spec: %v", taintSpec) } // validate if taint is unique by if len(uniqueTaints[newTaint.Effect]) > 0 && uniqueTaints[newTaint.Effect].Has(newTaint.Key) { return nil, nil, fmt.Errorf("duplicated taints with the same key and effect: %v", newTaint) } // add taint to existingTaints for uniqueness check if len(uniqueTaints[newTaint.Effect]) == 0 { uniqueTaints[newTaint.Effect] = sets.Set[string]{} } uniqueTaints[newTaint.Effect].Insert(newTaint.Key) taints = append(taints, newTaint) } } return taints, taintsToRemove, nil } // parseTaint parses a taint from a string, whose form must be either // '=:', ':', or ''. func parseTaint(st string) (corev1.Taint, error) { var taint corev1.Taint var key string var value string var effect corev1.TaintEffect parts := strings.Split(st, ":") switch len(parts) { case 1: key = parts[0] case 2: effect = corev1.TaintEffect(parts[1]) if err := validateTaintEffect(effect); err != nil { return taint, err } partsKV := strings.Split(parts[0], "=") if len(partsKV) > 2 { return taint, fmt.Errorf("invalid taint spec: %v", st) } key = partsKV[0] if len(partsKV) == 2 { value = partsKV[1] if errs := validation.IsValidLabelValue(value); len(errs) > 0 { return taint, fmt.Errorf("invalid taint spec: %v, %s", st, strings.Join(errs, "; ")) } } default: return taint, fmt.Errorf("invalid taint spec: %v", st) } if errs := validation.IsQualifiedName(key); len(errs) > 0 { return taint, fmt.Errorf("invalid taint spec: %v, %s", st, strings.Join(errs, "; ")) } taint.Key = key taint.Value = value taint.Effect = effect return taint, nil } func validateTaintEffect(effect corev1.TaintEffect) error { if effect != corev1.TaintEffectNoSchedule && effect != corev1.TaintEffectPreferNoSchedule && effect != corev1.TaintEffectNoExecute { return fmt.Errorf("invalid taint effect: %v, unsupported taint effect", effect) } return nil } // reorganizeTaints returns the updated set of taints, taking into account old taints that were not updated, // old taints that were updated, old taints that were deleted, and new taints. func reorganizeTaints(node *corev1.Node, overwrite bool, taintsToAdd []corev1.Taint, taintsToRemove []corev1.Taint) (string, []corev1.Taint, error) { newTaints := append([]corev1.Taint{}, taintsToAdd...) oldTaints := node.Spec.Taints // add taints that already existing but not updated to newTaints added := addTaints(oldTaints, &newTaints) allErrs, deleted := deleteTaints(taintsToRemove, &newTaints) if (added && deleted) || overwrite { return MODIFIED, newTaints, utilerrors.NewAggregate(allErrs) } else if added { return TAINTED, newTaints, utilerrors.NewAggregate(allErrs) } return UNTAINTED, newTaints, utilerrors.NewAggregate(allErrs) } // deleteTaints deletes the given taints from the node's taintlist. func deleteTaints(taintsToRemove []corev1.Taint, newTaints *[]corev1.Taint) ([]error, bool) { allErrs := []error{} var removed bool for _, taintToRemove := range taintsToRemove { if len(taintToRemove.Effect) > 0 { *newTaints, removed = deleteTaint(*newTaints, &taintToRemove) } else { *newTaints, removed = deleteTaintsByKey(*newTaints, taintToRemove.Key) } if !removed { allErrs = append(allErrs, fmt.Errorf("taint %q not found", taintToRemove.ToString())) } } return allErrs, removed } // addTaints adds the newTaints list to existing ones and updates the newTaints List. // TODO: This needs a rewrite to take only the new values instead of appended newTaints list to be consistent. func addTaints(oldTaints []corev1.Taint, newTaints *[]corev1.Taint) bool { for _, oldTaint := range oldTaints { existsInNew := false for _, taint := range *newTaints { if taint.MatchTaint(&oldTaint) { existsInNew = true break } } if !existsInNew { *newTaints = append(*newTaints, oldTaint) } } return len(oldTaints) != len(*newTaints) } // checkIfTaintsAlreadyExists checks if the node already has taints that we want to add and returns a string with taint keys. func checkIfTaintsAlreadyExists(oldTaints []corev1.Taint, taints []corev1.Taint) string { var existingTaintList = make([]string, 0) for _, taint := range taints { for _, oldTaint := range oldTaints { if taint.Key == oldTaint.Key && taint.Effect == oldTaint.Effect { existingTaintList = append(existingTaintList, taint.Key) } } } return strings.Join(existingTaintList, ",") } // deleteTaintsByKey removes all the taints that have the same key to given taintKey func deleteTaintsByKey(taints []corev1.Taint, taintKey string) ([]corev1.Taint, bool) { newTaints := []corev1.Taint{} for i := range taints { if taintKey == taints[i].Key { continue } newTaints = append(newTaints, taints[i]) } return newTaints, len(taints) != len(newTaints) } // deleteTaint removes all the taints that have the same key and effect to given taintToDelete. func deleteTaint(taints []corev1.Taint, taintToDelete *corev1.Taint) ([]corev1.Taint, bool) { newTaints := []corev1.Taint{} for i := range taints { if taintToDelete.MatchTaint(&taints[i]) { continue } newTaints = append(newTaints, taints[i]) } return newTaints, len(taints) != len(newTaints) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/taint/utils_test.go000066400000000000000000000311001504711711200304470ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package taint import ( "reflect" "testing" corev1 "k8s.io/api/core/v1" ) func TestDeleteTaint(t *testing.T) { cases := []struct { name string taints []corev1.Taint taintToDelete *corev1.Taint expectedTaints []corev1.Taint expectedResult bool }{ { name: "delete taint with different name", taints: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, }, taintToDelete: &corev1.Taint{Key: "foo_1", Effect: corev1.TaintEffectNoSchedule}, expectedTaints: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, }, expectedResult: false, }, { name: "delete taint with different effect", taints: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, }, taintToDelete: &corev1.Taint{Key: "foo", Effect: corev1.TaintEffectNoExecute}, expectedTaints: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, }, expectedResult: false, }, { name: "delete taint successfully", taints: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, }, taintToDelete: &corev1.Taint{Key: "foo", Effect: corev1.TaintEffectNoSchedule}, expectedTaints: []corev1.Taint{}, expectedResult: true, }, { name: "delete taint from empty taint array", taints: []corev1.Taint{}, taintToDelete: &corev1.Taint{Key: "foo", Effect: corev1.TaintEffectNoSchedule}, expectedTaints: []corev1.Taint{}, expectedResult: false, }, } for _, c := range cases { taints, result := deleteTaint(c.taints, c.taintToDelete) if result != c.expectedResult { t.Errorf("[%s] should return %t, but got: %t", c.name, c.expectedResult, result) } if !reflect.DeepEqual(taints, c.expectedTaints) { t.Errorf("[%s] the result taints should be %v, but got: %v", c.name, c.expectedTaints, taints) } } } func TestDeleteTaintByKey(t *testing.T) { cases := []struct { name string taints []corev1.Taint taintKey string expectedTaints []corev1.Taint expectedResult bool }{ { name: "delete taint unsuccessfully", taints: []corev1.Taint{ { Key: "foo", Value: "bar", Effect: corev1.TaintEffectNoSchedule, }, }, taintKey: "foo_1", expectedTaints: []corev1.Taint{ { Key: "foo", Value: "bar", Effect: corev1.TaintEffectNoSchedule, }, }, expectedResult: false, }, { name: "delete taint successfully", taints: []corev1.Taint{ { Key: "foo", Value: "bar", Effect: corev1.TaintEffectNoSchedule, }, }, taintKey: "foo", expectedTaints: []corev1.Taint{}, expectedResult: true, }, { name: "delete taint from empty taint array", taints: []corev1.Taint{}, taintKey: "foo", expectedTaints: []corev1.Taint{}, expectedResult: false, }, } for _, c := range cases { taints, result := deleteTaintsByKey(c.taints, c.taintKey) if result != c.expectedResult { t.Errorf("[%s] should return %t, but got: %t", c.name, c.expectedResult, result) } if !reflect.DeepEqual(c.expectedTaints, taints) { t.Errorf("[%s] the result taints should be %v, but got: %v", c.name, c.expectedTaints, taints) } } } func TestCheckIfTaintsAlreadyExists(t *testing.T) { oldTaints := []corev1.Taint{ { Key: "foo_1", Value: "bar", Effect: corev1.TaintEffectNoSchedule, }, { Key: "foo_2", Value: "bar", Effect: corev1.TaintEffectNoSchedule, }, { Key: "foo_3", Value: "bar", Effect: corev1.TaintEffectNoSchedule, }, } cases := []struct { name string taintsToCheck []corev1.Taint expectedResult string }{ { name: "empty array", taintsToCheck: []corev1.Taint{}, expectedResult: "", }, { name: "no match", taintsToCheck: []corev1.Taint{ { Key: "foo_1", Effect: corev1.TaintEffectNoExecute, }, }, expectedResult: "", }, { name: "match one taint", taintsToCheck: []corev1.Taint{ { Key: "foo_2", Effect: corev1.TaintEffectNoSchedule, }, }, expectedResult: "foo_2", }, { name: "match two taints", taintsToCheck: []corev1.Taint{ { Key: "foo_2", Effect: corev1.TaintEffectNoSchedule, }, { Key: "foo_3", Effect: corev1.TaintEffectNoSchedule, }, }, expectedResult: "foo_2,foo_3", }, } for _, c := range cases { result := checkIfTaintsAlreadyExists(oldTaints, c.taintsToCheck) if result != c.expectedResult { t.Errorf("[%s] should return '%s', but got: '%s'", c.name, c.expectedResult, result) } } } func TestReorganizeTaints(t *testing.T) { node := &corev1.Node{ Spec: corev1.NodeSpec{ Taints: []corev1.Taint{ { Key: "foo", Value: "bar", Effect: corev1.TaintEffectNoSchedule, }, }, }, } cases := []struct { name string overwrite bool taintsToAdd []corev1.Taint taintsToDelete []corev1.Taint expectedTaints []corev1.Taint expectedOperation string expectedErr bool }{ { name: "no changes with overwrite is true", overwrite: true, taintsToAdd: []corev1.Taint{}, taintsToDelete: []corev1.Taint{}, expectedTaints: node.Spec.Taints, expectedOperation: MODIFIED, expectedErr: false, }, { name: "no changes with overwrite is false", overwrite: false, taintsToAdd: []corev1.Taint{}, taintsToDelete: []corev1.Taint{}, expectedTaints: node.Spec.Taints, expectedOperation: UNTAINTED, expectedErr: false, }, { name: "add new taint", overwrite: false, taintsToAdd: []corev1.Taint{ { Key: "foo_1", Effect: corev1.TaintEffectNoExecute, }, }, taintsToDelete: []corev1.Taint{}, expectedTaints: append([]corev1.Taint{{Key: "foo_1", Effect: corev1.TaintEffectNoExecute}}, node.Spec.Taints...), expectedOperation: TAINTED, expectedErr: false, }, { name: "delete taint with effect", overwrite: false, taintsToAdd: []corev1.Taint{}, taintsToDelete: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, }, expectedTaints: []corev1.Taint{}, expectedOperation: UNTAINTED, expectedErr: false, }, { name: "delete taint with no effect", overwrite: false, taintsToAdd: []corev1.Taint{}, taintsToDelete: []corev1.Taint{ { Key: "foo", }, }, expectedTaints: []corev1.Taint{}, expectedOperation: UNTAINTED, expectedErr: false, }, { name: "delete non-exist taint", overwrite: false, taintsToAdd: []corev1.Taint{}, taintsToDelete: []corev1.Taint{ { Key: "foo_1", Effect: corev1.TaintEffectNoSchedule, }, }, expectedTaints: node.Spec.Taints, expectedOperation: UNTAINTED, expectedErr: true, }, { name: "add new taint and delete old one", overwrite: false, taintsToAdd: []corev1.Taint{ { Key: "foo_1", Effect: corev1.TaintEffectNoSchedule, }, }, taintsToDelete: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, }, expectedTaints: []corev1.Taint{ { Key: "foo_1", Effect: corev1.TaintEffectNoSchedule, }, }, expectedOperation: MODIFIED, expectedErr: false, }, } for _, c := range cases { operation, taints, err := reorganizeTaints(node, c.overwrite, c.taintsToAdd, c.taintsToDelete) if c.expectedErr && err == nil { t.Errorf("[%s] expect to see an error, but did not get one", c.name) } else if !c.expectedErr && err != nil { t.Errorf("[%s] expect not to see an error, but got one: %v", c.name, err) } if !reflect.DeepEqual(c.expectedTaints, taints) { t.Errorf("[%s] expect to see taint list %#v, but got: %#v", c.name, c.expectedTaints, taints) } if c.expectedOperation != operation { t.Errorf("[%s] expect to see operation %s, but got: %s", c.name, c.expectedOperation, operation) } } } func TestParseTaints(t *testing.T) { cases := []struct { name string spec []string expectedTaints []corev1.Taint expectedTaintsToRemove []corev1.Taint expectedErr bool }{ { name: "invalid spec format", spec: []string{""}, expectedErr: true, }, { name: "invalid spec format", spec: []string{"foo=abc"}, expectedErr: true, }, { name: "invalid spec format", spec: []string{"foo=abc=xyz:NoSchedule"}, expectedErr: true, }, { name: "invalid spec format", spec: []string{"foo=abc:xyz:NoSchedule"}, expectedErr: true, }, { name: "invalid spec format for adding taint", spec: []string{"foo"}, expectedErr: true, }, { name: "invalid spec effect for adding taint", spec: []string{"foo=abc:invalid_effect"}, expectedErr: true, }, { name: "invalid spec effect for deleting taint", spec: []string{"foo:invalid_effect-"}, expectedErr: true, }, { name: "add new taints", spec: []string{"foo=abc:NoSchedule", "bar=abc:NoSchedule", "baz:NoSchedule", "qux:NoSchedule", "foobar=:NoSchedule"}, expectedTaints: []corev1.Taint{ { Key: "foo", Value: "abc", Effect: corev1.TaintEffectNoSchedule, }, { Key: "bar", Value: "abc", Effect: corev1.TaintEffectNoSchedule, }, { Key: "baz", Value: "", Effect: corev1.TaintEffectNoSchedule, }, { Key: "qux", Value: "", Effect: corev1.TaintEffectNoSchedule, }, { Key: "foobar", Value: "", Effect: corev1.TaintEffectNoSchedule, }, }, expectedErr: false, }, { name: "delete taints", spec: []string{"foo:NoSchedule-", "bar:NoSchedule-", "qux=:NoSchedule-", "dedicated-"}, expectedTaintsToRemove: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, { Key: "bar", Effect: corev1.TaintEffectNoSchedule, }, { Key: "qux", Effect: corev1.TaintEffectNoSchedule, }, { Key: "dedicated", }, }, expectedErr: false, }, { name: "add taints and delete taints", spec: []string{"foo=abc:NoSchedule", "bar=abc:NoSchedule", "baz:NoSchedule", "qux:NoSchedule", "foobar=:NoSchedule", "foo:NoSchedule-", "bar:NoSchedule-", "baz=:NoSchedule-"}, expectedTaints: []corev1.Taint{ { Key: "foo", Value: "abc", Effect: corev1.TaintEffectNoSchedule, }, { Key: "bar", Value: "abc", Effect: corev1.TaintEffectNoSchedule, }, { Key: "baz", Value: "", Effect: corev1.TaintEffectNoSchedule, }, { Key: "qux", Value: "", Effect: corev1.TaintEffectNoSchedule, }, { Key: "foobar", Value: "", Effect: corev1.TaintEffectNoSchedule, }, }, expectedTaintsToRemove: []corev1.Taint{ { Key: "foo", Effect: corev1.TaintEffectNoSchedule, }, { Key: "bar", Effect: corev1.TaintEffectNoSchedule, }, { Key: "baz", Value: "", Effect: corev1.TaintEffectNoSchedule, }, }, expectedErr: false, }, } for _, c := range cases { taints, taintsToRemove, err := parseTaints(c.spec) if c.expectedErr && err == nil { t.Errorf("[%s] expected error for spec %s, but got nothing", c.name, c.spec) } if !c.expectedErr && err != nil { t.Errorf("[%s] expected no error for spec %s, but got: %v", c.name, c.spec, err) } if !reflect.DeepEqual(c.expectedTaints, taints) { t.Errorf("[%s] expected returen taints as %v, but got: %v", c.name, c.expectedTaints, taints) } if !reflect.DeepEqual(c.expectedTaintsToRemove, taintsToRemove) { t.Errorf("[%s] expected return taints to be removed as %v, but got: %v", c.name, c.expectedTaintsToRemove, taintsToRemove) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/testing/000077500000000000000000000000001504711711200262645ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go000066400000000000000000000716571504711711200275410ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testing import ( "bytes" "fmt" "os" "path/filepath" "time" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta/testrestmapper" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/discovery" diskcached "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/dynamic" fakedynamic "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes" openapiclient "k8s.io/client-go/openapi" "k8s.io/client-go/openapi/openapitest" restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" "k8s.io/client-go/restmapper" scaleclient "k8s.io/client-go/scale" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/openapi" openapitesting "k8s.io/kubectl/pkg/util/openapi/testing" "k8s.io/kubectl/pkg/validation" ) // InternalType is the schema for internal type // +k8s:deepcopy-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type InternalType struct { Kind string APIVersion string Name string } // ExternalType is the schema for external type // +k8s:deepcopy-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ExternalType struct { Kind string `json:"kind"` APIVersion string `json:"apiVersion"` Name string `json:"name"` } // ExternalType2 is another schema for external type // +k8s:deepcopy-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ExternalType2 struct { Kind string `json:"kind"` APIVersion string `json:"apiVersion"` Name string `json:"name"` } // GetObjectKind returns the ObjectKind schema func (obj *InternalType) GetObjectKind() schema.ObjectKind { return obj } // SetGroupVersionKind sets the version and kind func (obj *InternalType) SetGroupVersionKind(gvk schema.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() } // GroupVersionKind returns GroupVersionKind schema func (obj *InternalType) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) } // GetObjectKind returns the ObjectKind schema func (obj *ExternalType) GetObjectKind() schema.ObjectKind { return obj } // SetGroupVersionKind returns the GroupVersionKind schema func (obj *ExternalType) SetGroupVersionKind(gvk schema.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() } // GroupVersionKind returns the GroupVersionKind schema func (obj *ExternalType) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) } // GetObjectKind returns the ObjectKind schema func (obj *ExternalType2) GetObjectKind() schema.ObjectKind { return obj } // SetGroupVersionKind sets the API version and obj kind from schema func (obj *ExternalType2) SetGroupVersionKind(gvk schema.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() } // GroupVersionKind returns the FromAPIVersionAndKind schema func (obj *ExternalType2) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) } // NewInternalType returns an initialized InternalType instance func NewInternalType(kind, apiversion, name string) *InternalType { item := InternalType{Kind: kind, APIVersion: apiversion, Name: name} return &item } func convertInternalTypeToExternalType(in *InternalType, out *ExternalType, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name return nil } func convertInternalTypeToExternalType2(in *InternalType, out *ExternalType2, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name return nil } func convertExternalTypeToInternalType(in *ExternalType, out *InternalType, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name return nil } func convertExternalType2ToInternalType(in *ExternalType2, out *InternalType, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name return nil } // InternalNamespacedType schema for internal namespaced types // +k8s:deepcopy-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type InternalNamespacedType struct { Kind string APIVersion string Name string Namespace string } // ExternalNamespacedType schema for external namespaced types // +k8s:deepcopy-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ExternalNamespacedType struct { Kind string `json:"kind"` APIVersion string `json:"apiVersion"` Name string `json:"name"` Namespace string `json:"namespace"` } // ExternalNamespacedType2 schema for external namespaced types // +k8s:deepcopy-gen=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ExternalNamespacedType2 struct { Kind string `json:"kind"` APIVersion string `json:"apiVersion"` Name string `json:"name"` Namespace string `json:"namespace"` } // GetObjectKind returns the ObjectKind schema func (obj *InternalNamespacedType) GetObjectKind() schema.ObjectKind { return obj } // SetGroupVersionKind sets the API group and kind from schema func (obj *InternalNamespacedType) SetGroupVersionKind(gvk schema.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() } // GroupVersionKind returns the GroupVersionKind schema func (obj *InternalNamespacedType) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) } // GetObjectKind returns the ObjectKind schema func (obj *ExternalNamespacedType) GetObjectKind() schema.ObjectKind { return obj } // SetGroupVersionKind sets the API version and kind from schema func (obj *ExternalNamespacedType) SetGroupVersionKind(gvk schema.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() } // GroupVersionKind returns the GroupVersionKind schema func (obj *ExternalNamespacedType) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) } // GetObjectKind returns the ObjectKind schema func (obj *ExternalNamespacedType2) GetObjectKind() schema.ObjectKind { return obj } // SetGroupVersionKind sets the API version and kind from schema func (obj *ExternalNamespacedType2) SetGroupVersionKind(gvk schema.GroupVersionKind) { obj.APIVersion, obj.Kind = gvk.ToAPIVersionAndKind() } // GroupVersionKind returns the GroupVersionKind schema func (obj *ExternalNamespacedType2) GroupVersionKind() schema.GroupVersionKind { return schema.FromAPIVersionAndKind(obj.APIVersion, obj.Kind) } // NewInternalNamespacedType returns an initialized instance of InternalNamespacedType func NewInternalNamespacedType(kind, apiversion, name, namespace string) *InternalNamespacedType { item := InternalNamespacedType{Kind: kind, APIVersion: apiversion, Name: name, Namespace: namespace} return &item } func convertInternalNamespacedTypeToExternalNamespacedType(in *InternalNamespacedType, out *ExternalNamespacedType, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name out.Namespace = in.Namespace return nil } func convertInternalNamespacedTypeToExternalNamespacedType2(in *InternalNamespacedType, out *ExternalNamespacedType2, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name out.Namespace = in.Namespace return nil } func convertExternalNamespacedTypeToInternalNamespacedType(in *ExternalNamespacedType, out *InternalNamespacedType, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name out.Namespace = in.Namespace return nil } func convertExternalNamespacedType2ToInternalNamespacedType(in *ExternalNamespacedType2, out *InternalNamespacedType, s conversion.Scope) error { out.Kind = in.Kind out.APIVersion = in.APIVersion out.Name = in.Name out.Namespace = in.Namespace return nil } // ValidVersion of API var ValidVersion = "v1" // InternalGV is the internal group version object var InternalGV = schema.GroupVersion{Group: "apitest", Version: runtime.APIVersionInternal} // UnlikelyGV is a group version object for unrecognised version var UnlikelyGV = schema.GroupVersion{Group: "apitest", Version: "unlikelyversion"} // ValidVersionGV is the valid group version object var ValidVersionGV = schema.GroupVersion{Group: "apitest", Version: ValidVersion} // NewExternalScheme returns required objects for ExternalScheme func NewExternalScheme() (*runtime.Scheme, meta.RESTMapper, runtime.Codec) { scheme := runtime.NewScheme() mapper, codec := AddToScheme(scheme) return scheme, mapper, codec } func registerConversions(s *runtime.Scheme) error { if err := s.AddConversionFunc((*InternalType)(nil), (*ExternalType)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertInternalTypeToExternalType(a.(*InternalType), b.(*ExternalType), scope) }); err != nil { return err } if err := s.AddConversionFunc((*InternalType)(nil), (*ExternalType2)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertInternalTypeToExternalType2(a.(*InternalType), b.(*ExternalType2), scope) }); err != nil { return err } if err := s.AddConversionFunc((*ExternalType)(nil), (*InternalType)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertExternalTypeToInternalType(a.(*ExternalType), b.(*InternalType), scope) }); err != nil { return err } if err := s.AddConversionFunc((*ExternalType2)(nil), (*InternalType)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertExternalType2ToInternalType(a.(*ExternalType2), b.(*InternalType), scope) }); err != nil { return err } if err := s.AddConversionFunc((*InternalNamespacedType)(nil), (*ExternalNamespacedType)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertInternalNamespacedTypeToExternalNamespacedType(a.(*InternalNamespacedType), b.(*ExternalNamespacedType), scope) }); err != nil { return err } if err := s.AddConversionFunc((*InternalNamespacedType)(nil), (*ExternalNamespacedType2)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertInternalNamespacedTypeToExternalNamespacedType2(a.(*InternalNamespacedType), b.(*ExternalNamespacedType2), scope) }); err != nil { return err } if err := s.AddConversionFunc((*ExternalNamespacedType)(nil), (*InternalNamespacedType)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertExternalNamespacedTypeToInternalNamespacedType(a.(*ExternalNamespacedType), b.(*InternalNamespacedType), scope) }); err != nil { return err } if err := s.AddConversionFunc((*ExternalNamespacedType2)(nil), (*InternalNamespacedType)(nil), func(a, b interface{}, scope conversion.Scope) error { return convertExternalNamespacedType2ToInternalNamespacedType(a.(*ExternalNamespacedType2), b.(*InternalNamespacedType), scope) }); err != nil { return err } return nil } // AddToScheme adds required objects into scheme func AddToScheme(scheme *runtime.Scheme) (meta.RESTMapper, runtime.Codec) { scheme.AddKnownTypeWithName(InternalGV.WithKind("Type"), &InternalType{}) scheme.AddKnownTypeWithName(UnlikelyGV.WithKind("Type"), &ExternalType{}) // This tests that kubectl will not confuse the external scheme with the internal scheme, even when they accidentally have versions of the same name. scheme.AddKnownTypeWithName(ValidVersionGV.WithKind("Type"), &ExternalType2{}) scheme.AddKnownTypeWithName(InternalGV.WithKind("NamespacedType"), &InternalNamespacedType{}) scheme.AddKnownTypeWithName(UnlikelyGV.WithKind("NamespacedType"), &ExternalNamespacedType{}) // This tests that kubectl will not confuse the external scheme with the internal scheme, even when they accidentally have versions of the same name. scheme.AddKnownTypeWithName(ValidVersionGV.WithKind("NamespacedType"), &ExternalNamespacedType2{}) utilruntime.Must(registerConversions(scheme)) codecs := serializer.NewCodecFactory(scheme) codec := codecs.LegacyCodec(UnlikelyGV) mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{UnlikelyGV, ValidVersionGV}) for _, gv := range []schema.GroupVersion{UnlikelyGV, ValidVersionGV} { for kind := range scheme.KnownTypes(gv) { gvk := gv.WithKind(kind) scope := meta.RESTScopeNamespace mapper.Add(gvk, scope) } } return mapper, codec } type FakeCachedDiscoveryClient struct { discovery.DiscoveryInterface Groups []*metav1.APIGroup Resources []*metav1.APIResourceList PreferredResources []*metav1.APIResourceList Invalidations int } func NewFakeCachedDiscoveryClient() *FakeCachedDiscoveryClient { return &FakeCachedDiscoveryClient{ Groups: []*metav1.APIGroup{}, Resources: []*metav1.APIResourceList{}, PreferredResources: []*metav1.APIResourceList{}, Invalidations: 0, } } func (d *FakeCachedDiscoveryClient) Fresh() bool { return true } func (d *FakeCachedDiscoveryClient) Invalidate() { d.Invalidations++ } func (d *FakeCachedDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { return d.Groups, d.Resources, nil } func (d *FakeCachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) { groupList := &metav1.APIGroupList{Groups: []metav1.APIGroup{}} for _, g := range d.Groups { groupList.Groups = append(groupList.Groups, *g) } return groupList, nil } func (d *FakeCachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) { return d.PreferredResources, nil } // TestFactory extends cmdutil.Factory type TestFactory struct { cmdutil.Factory kubeConfigFlags *genericclioptions.TestConfigFlags Client RESTClient ScaleGetter scaleclient.ScalesGetter UnstructuredClient RESTClient ClientConfigVal *restclient.Config FakeDynamicClient *fakedynamic.FakeDynamicClient tempConfigFile *os.File UnstructuredClientForMappingFunc resource.FakeClientFunc OpenAPISchemaFunc func() (openapi.Resources, error) OpenAPIV3ClientFunc func() (openapiclient.Client, error) } // NewTestFactory returns an initialized TestFactory instance func NewTestFactory() *TestFactory { // specify an optionalClientConfig to explicitly use in testing // to avoid polluting an existing user config. tmpFile, err := os.CreateTemp(os.TempDir(), "cmdtests_temp") if err != nil { panic(fmt.Sprintf("unable to create a fake client config: %v", err)) } loadingRules := &clientcmd.ClientConfigLoadingRules{ Precedence: []string{tmpFile.Name()}, MigrationRules: map[string]string{}, } overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmdapi.Cluster{Server: "http://localhost:8080"}} fallbackReader := bytes.NewBuffer([]byte{}) clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, fallbackReader) configFlags := genericclioptions.NewTestConfigFlags(). WithClientConfig(clientConfig). WithRESTMapper(testRESTMapper()) restConfig, err := clientConfig.ClientConfig() if err != nil { panic(fmt.Sprintf("unable to create a fake restclient config: %v", err)) } return &TestFactory{ Factory: cmdutil.NewFactory(configFlags), kubeConfigFlags: configFlags, FakeDynamicClient: fakedynamic.NewSimpleDynamicClient(scheme.Scheme), tempConfigFile: tmpFile, ClientConfigVal: restConfig, } } // WithNamespace is used to mention namespace reactively func (f *TestFactory) WithNamespace(ns string) *TestFactory { f.kubeConfigFlags.WithNamespace(ns) return f } // WithClientConfig sets the client config to use func (f *TestFactory) WithClientConfig(clientConfig clientcmd.ClientConfig) *TestFactory { f.kubeConfigFlags.WithClientConfig(clientConfig) return f } func (f *TestFactory) WithDiscoveryClient(discoveryClient discovery.CachedDiscoveryInterface) *TestFactory { f.kubeConfigFlags.WithDiscoveryClient(discoveryClient) return f } // Cleanup cleans up TestFactory temp config file func (f *TestFactory) Cleanup() { if f.tempConfigFile == nil { return } f.tempConfigFile.Close() os.Remove(f.tempConfigFile.Name()) } // ToRESTConfig is used to get ClientConfigVal from a TestFactory func (f *TestFactory) ToRESTConfig() (*restclient.Config, error) { return f.ClientConfigVal, nil } // ClientForMapping is used to Client from a TestFactory func (f *TestFactory) ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) { return f.Client, nil } // PathOptions returns a new PathOptions with a temp file func (f *TestFactory) PathOptions() *clientcmd.PathOptions { pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = f.tempConfigFile.Name() pathOptions.EnvVar = "" return pathOptions } // PathOptionsWithConfig writes a config to a temp file and returns PathOptions func (f *TestFactory) PathOptionsWithConfig(config clientcmdapi.Config) (*clientcmd.PathOptions, error) { pathOptions := f.PathOptions() err := clientcmd.WriteToFile(config, pathOptions.GlobalFile) if err != nil { return nil, err } return pathOptions, nil } // UnstructuredClientForMapping is used to get UnstructuredClient from a TestFactory func (f *TestFactory) UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) { if f.UnstructuredClientForMappingFunc != nil { return f.UnstructuredClientForMappingFunc(mapping.GroupVersionKind.GroupVersion()) } return f.UnstructuredClient, nil } // Validator returns a validation schema func (f *TestFactory) Validator(validateDirective string) (validation.Schema, error) { return validation.NullSchema{}, nil } // OpenAPISchema returns openapi resources func (f *TestFactory) OpenAPISchema() (openapi.Resources, error) { if f.OpenAPISchemaFunc != nil { return f.OpenAPISchemaFunc() } return openapitesting.EmptyResources{}, nil } func (f *TestFactory) OpenAPIV3Client() (openapiclient.Client, error) { if f.OpenAPIV3ClientFunc != nil { return f.OpenAPIV3ClientFunc() } return openapitest.NewFakeClient(), nil } // NewBuilder returns an initialized resource.Builder instance func (f *TestFactory) NewBuilder() *resource.Builder { return resource.NewFakeBuilder( func(version schema.GroupVersion) (resource.RESTClient, error) { if f.UnstructuredClientForMappingFunc != nil { return f.UnstructuredClientForMappingFunc(version) } if f.UnstructuredClient != nil { return f.UnstructuredClient, nil } return f.Client, nil }, f.ToRESTMapper, func() (restmapper.CategoryExpander, error) { return resource.FakeCategoryExpander, nil }, ) } // KubernetesClientSet initializes and returns the Clientset using TestFactory func (f *TestFactory) KubernetesClientSet() (*kubernetes.Clientset, error) { fakeClient := f.Client.(*fake.RESTClient) clientset := kubernetes.NewForConfigOrDie(f.ClientConfigVal) clientset.CoreV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AuthorizationV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AuthorizationV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AuthorizationV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AuthorizationV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AuthenticationV1alpha1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AutoscalingV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AutoscalingV2().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.BatchV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.CertificatesV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.CertificatesV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.ExtensionsV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.RbacV1alpha1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.RbacV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.StorageV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.StorageV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AppsV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AppsV1beta2().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.AppsV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.PolicyV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.PolicyV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.DiscoveryClient.RESTClient().(*restclient.RESTClient).Client = fakeClient.Client return clientset, nil } // DynamicClient returns a dynamic client from TestFactory func (f *TestFactory) DynamicClient() (dynamic.Interface, error) { if f.FakeDynamicClient != nil { return f.FakeDynamicClient, nil } return f.Factory.DynamicClient() } // RESTClient returns a REST client from TestFactory func (f *TestFactory) RESTClient() (*restclient.RESTClient, error) { // Swap out the HTTP client out of the client with the fake's version. fakeClient := f.Client.(*fake.RESTClient) restClient, err := restclient.RESTClientFor(f.ClientConfigVal) if err != nil { panic(err) } restClient.Client = fakeClient.Client return restClient, nil } // DiscoveryClient returns a discovery client from TestFactory func (f *TestFactory) DiscoveryClient() (discovery.CachedDiscoveryInterface, error) { fakeClient := f.Client.(*fake.RESTClient) cacheDir := filepath.Join("", ".kube", "cache", "discovery") cachedClient, err := diskcached.NewCachedDiscoveryClientForConfig(f.ClientConfigVal, cacheDir, "", time.Duration(10*time.Minute)) if err != nil { return nil, err } cachedClient.RESTClient().(*restclient.RESTClient).Client = fakeClient.Client return cachedClient, nil } func testRESTMapper() meta.RESTMapper { groupResources := testDynamicResources() mapper := restmapper.NewDiscoveryRESTMapper(groupResources) // for backwards compatibility with existing tests, allow rest mappings from the scheme to show up // TODO: make this opt-in? mapper = meta.FirstHitRESTMapper{ MultiRESTMapper: meta.MultiRESTMapper{ mapper, testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), }, } fakeDs := NewFakeCachedDiscoveryClient() expander := restmapper.NewShortcutExpander(mapper, fakeDs, nil) return expander } // ScaleClient returns the ScalesGetter from a TestFactory func (f *TestFactory) ScaleClient() (scaleclient.ScalesGetter, error) { return f.ScaleGetter, nil } func testDynamicResources() []*restmapper.APIGroupResources { return []*restmapper.APIGroupResources{ { Group: metav1.APIGroup{ Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1": { {Name: "pods", Namespaced: true, Kind: "Pod"}, {Name: "services", Namespaced: true, Kind: "Service"}, {Name: "replicationcontrollers", Namespaced: true, Kind: "ReplicationController"}, {Name: "componentstatuses", Namespaced: false, Kind: "ComponentStatus"}, {Name: "nodes", Namespaced: false, Kind: "Node"}, {Name: "secrets", Namespaced: true, Kind: "Secret"}, {Name: "configmaps", Namespaced: true, Kind: "ConfigMap"}, {Name: "namespacedtype", Namespaced: true, Kind: "NamespacedType"}, {Name: "namespaces", Namespaced: false, Kind: "Namespace"}, {Name: "resourcequotas", Namespaced: true, Kind: "ResourceQuota"}, }, }, }, { Group: metav1.APIGroup{ Name: "extensions", Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1beta1"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1beta1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1beta1": { {Name: "deployments", Namespaced: true, Kind: "Deployment"}, {Name: "replicasets", Namespaced: true, Kind: "ReplicaSet"}, }, }, }, { Group: metav1.APIGroup{ Name: "apps", Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1beta1"}, {Version: "v1beta2"}, {Version: "v1"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1beta1": { {Name: "deployments", Namespaced: true, Kind: "Deployment"}, {Name: "replicasets", Namespaced: true, Kind: "ReplicaSet"}, }, "v1beta2": { {Name: "deployments", Namespaced: true, Kind: "Deployment"}, }, "v1": { {Name: "deployments", Namespaced: true, Kind: "Deployment"}, {Name: "replicasets", Namespaced: true, Kind: "ReplicaSet"}, }, }, }, { Group: metav1.APIGroup{ Name: "batch", Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1beta1"}, {Version: "v1"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1beta1": { {Name: "cronjobs", Namespaced: true, Kind: "CronJob"}, }, "v1": { {Name: "jobs", Namespaced: true, Kind: "Job"}, }, }, }, { Group: metav1.APIGroup{ Name: "autoscaling", Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1"}, {Version: "v2"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v2"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1": { {Name: "horizontalpodautoscalers", Namespaced: true, Kind: "HorizontalPodAutoscaler"}, }, "v2": { {Name: "horizontalpodautoscalers", Namespaced: true, Kind: "HorizontalPodAutoscaler"}, }, }, }, { Group: metav1.APIGroup{ Name: "storage.k8s.io", Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1beta1"}, {Version: "v0"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1beta1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1beta1": { {Name: "storageclasses", Namespaced: false, Kind: "StorageClass"}, }, // bogus version of a known group/version/resource to make sure kubectl falls back to generic object mode "v0": { {Name: "storageclasses", Namespaced: false, Kind: "StorageClass"}, }, }, }, { Group: metav1.APIGroup{ Name: "rbac.authorization.k8s.io", Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1beta1"}, {Version: "v1"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1": { {Name: "clusterroles", Namespaced: false, Kind: "ClusterRole"}, }, "v1beta1": { {Name: "clusterrolebindings", Namespaced: false, Kind: "ClusterRoleBinding"}, }, }, }, { Group: metav1.APIGroup{ Name: "company.com", Versions: []metav1.GroupVersionForDiscovery{ {Version: "v1"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{Version: "v1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1": { {Name: "bars", Namespaced: true, Kind: "Bar"}, {Name: "applysets", Namespaced: false, Kind: "ApplySet"}, }, }, }, { Group: metav1.APIGroup{ Name: "unit-test.test.com", Versions: []metav1.GroupVersionForDiscovery{ {GroupVersion: "unit-test.test.com/v1", Version: "v1"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "unit-test.test.com/v1", Version: "v1"}, }, VersionedResources: map[string][]metav1.APIResource{ "v1": { {Name: "widgets", Namespaced: true, Kind: "Widget"}, }, }, }, { Group: metav1.APIGroup{ Name: "apitest", Versions: []metav1.GroupVersionForDiscovery{ {GroupVersion: "apitest/unlikelyversion", Version: "unlikelyversion"}, }, PreferredVersion: metav1.GroupVersionForDiscovery{ GroupVersion: "apitest/unlikelyversion", Version: "unlikelyversion"}, }, VersionedResources: map[string][]metav1.APIResource{ "unlikelyversion": { {Name: "types", SingularName: "type", Namespaced: false, Kind: "Type"}, }, }, }, } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/testing/interfaces.go000066400000000000000000000016301504711711200307360ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testing import ( "k8s.io/apimachinery/pkg/types" client "k8s.io/client-go/rest" ) // RESTClient is a client helper for dealing with RESTful resources // in a generic way. type RESTClient interface { Get() *client.Request Post() *client.Request Patch(types.PatchType) *client.Request Delete() *client.Request Put() *client.Request } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/testing/util.go000066400000000000000000000137201504711711200275730ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testing import ( "bytes" "encoding/json" "io" "net/http" "os" "testing" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" restclient "k8s.io/client-go/rest" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" ) var ( grace = int64(30) enableServiceLinks = corev1.DefaultEnableServiceLinks ) func DefaultHeader() http.Header { header := http.Header{} header.Set("Content-Type", runtime.ContentTypeJSON) return header } func DefaultClientConfig() *restclient.Config { return &restclient.Config{ APIPath: "/api", ContentConfig: restclient.ContentConfig{ NegotiatedSerializer: scheme.Codecs, ContentType: runtime.ContentTypeJSON, GroupVersion: &corev1.SchemeGroupVersion, }, } } func ObjBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) } func BytesBody(bodyBytes []byte) io.ReadCloser { return io.NopCloser(bytes.NewReader(bodyBytes)) } func StringBody(body string) io.ReadCloser { return io.NopCloser(bytes.NewReader([]byte(body))) } func TestData() (*corev1.PodList, *corev1.ServiceList, *corev1.ReplicationControllerList) { pods := &corev1.PodList{ ListMeta: metav1.ListMeta{ ResourceVersion: "15", }, Items: []corev1.Pod{ { ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "test", ResourceVersion: "11"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, }, }, } svc := &corev1.ServiceList{ ListMeta: metav1.ListMeta{ ResourceVersion: "16", }, Items: []corev1.Service{ { ObjectMeta: metav1.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: corev1.ServiceSpec{ SessionAffinity: "None", Type: corev1.ServiceTypeClusterIP, }, }, }, } one := int32(1) rc := &corev1.ReplicationControllerList{ ListMeta: metav1.ListMeta{ ResourceVersion: "17", }, Items: []corev1.ReplicationController{ { ObjectMeta: metav1.ObjectMeta{Name: "rc1", Namespace: "test", ResourceVersion: "18"}, Spec: corev1.ReplicationControllerSpec{ Replicas: &one, }, }, }, } return pods, svc, rc } // EmptyTestData returns no pod, service, or replication controller func EmptyTestData() (*corev1.PodList, *corev1.ServiceList, *corev1.ReplicationControllerList) { pods := &corev1.PodList{ ListMeta: metav1.ListMeta{ ResourceVersion: "15", }, Items: []corev1.Pod{}, } svc := &corev1.ServiceList{ ListMeta: metav1.ListMeta{ ResourceVersion: "16", }, Items: []corev1.Service{}, } rc := &corev1.ReplicationControllerList{ ListMeta: metav1.ListMeta{ ResourceVersion: "17", }, Items: []corev1.ReplicationController{}, } return pods, svc, rc } func SubresourceTestData() *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyAlways, DNSPolicy: corev1.DNSClusterFirst, TerminationGracePeriodSeconds: &grace, SecurityContext: &corev1.PodSecurityContext{}, EnableServiceLinks: &enableServiceLinks, }, Status: corev1.PodStatus{ Phase: corev1.PodPending, }, } } func GenResponseWithJsonEncodedBody(bodyStruct interface{}) (*http.Response, error) { jsonBytes, err := json.Marshal(bodyStruct) if err != nil { return nil, err } return &http.Response{StatusCode: http.StatusOK, Header: DefaultHeader(), Body: BytesBody(jsonBytes)}, nil } func InitTestErrorHandler(t *testing.T) { cmdutil.BehaviorOnFatal(func(str string, code int) { t.Errorf("Error running command (exit code %d): %s", code, str) }) } // WithAlphaEnvs calls func f with the given env-var-based feature gates enabled, // and then restores the original values of those variables. func WithAlphaEnvs(features []cmdutil.FeatureGate, t *testing.T, f func(*testing.T)) { for _, feature := range features { key := string(feature) if key != "" { oldValue := os.Getenv(key) err := os.Setenv(key, "true") require.NoError(t, err, "unexpected error setting alpha env") defer os.Setenv(key, oldValue) } } f(t) } // WithAlphaEnvs calls func f with the given env-var-based feature gates disabled, // and then restores the original values of those variables. func WithAlphaEnvsDisabled(features []cmdutil.FeatureGate, t *testing.T, f func(*testing.T)) { for _, feature := range features { key := string(feature) if key != "" { oldValue := os.Getenv(key) err := os.Setenv(key, "false") require.NoError(t, err, "unexpected error setting alpha env") defer os.Setenv(key, oldValue) } } f(t) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/testing/zz_generated.deepcopy.go000066400000000000000000000116501504711711200331060ustar00rootroot00000000000000//go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright The Kubernetes Authors. 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. */ // Code generated by deepcopy-gen. DO NOT EDIT. package testing import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalNamespacedType) DeepCopyInto(out *ExternalNamespacedType) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalNamespacedType. func (in *ExternalNamespacedType) DeepCopy() *ExternalNamespacedType { if in == nil { return nil } out := new(ExternalNamespacedType) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ExternalNamespacedType) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalNamespacedType2) DeepCopyInto(out *ExternalNamespacedType2) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalNamespacedType2. func (in *ExternalNamespacedType2) DeepCopy() *ExternalNamespacedType2 { if in == nil { return nil } out := new(ExternalNamespacedType2) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ExternalNamespacedType2) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalType) DeepCopyInto(out *ExternalType) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalType. func (in *ExternalType) DeepCopy() *ExternalType { if in == nil { return nil } out := new(ExternalType) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ExternalType) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalType2) DeepCopyInto(out *ExternalType2) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalType2. func (in *ExternalType2) DeepCopy() *ExternalType2 { if in == nil { return nil } out := new(ExternalType2) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ExternalType2) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InternalNamespacedType) DeepCopyInto(out *InternalNamespacedType) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalNamespacedType. func (in *InternalNamespacedType) DeepCopy() *InternalNamespacedType { if in == nil { return nil } out := new(InternalNamespacedType) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *InternalNamespacedType) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InternalType) DeepCopyInto(out *InternalType) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalType. func (in *InternalType) DeepCopy() *InternalType { if in == nil { return nil } out := new(InternalType) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *InternalType) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/top/000077500000000000000000000000001504711711200254115ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/top/top.go000066400000000000000000000040461504711711200265460ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package top import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" metricsapi "k8s.io/metrics/pkg/apis/metrics" ) const ( sortByCPU = "cpu" sortByMemory = "memory" ) var ( supportedMetricsAPIVersions = []string{ "v1beta1", } topLong = templates.LongDesc(i18n.T(` Display resource (CPU/memory) usage. The top command allows you to see the resource consumption for nodes or pods. This command requires Metrics Server to be correctly configured and working on the server. `)) ) func NewCmdTop(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "top", Short: i18n.T("Display resource (CPU/memory) usage"), Long: topLong, Run: cmdutil.DefaultSubCommandRun(streams.ErrOut), } // create subcommands cmd.AddCommand(NewCmdTopNode(f, nil, streams)) cmd.AddCommand(NewCmdTopPod(f, nil, streams)) return cmd } func SupportedMetricsAPIVersionAvailable(discoveredAPIGroups *metav1.APIGroupList) bool { for _, discoveredAPIGroup := range discoveredAPIGroups.Groups { if discoveredAPIGroup.Name != metricsapi.GroupName { continue } for _, version := range discoveredAPIGroup.Versions { for _, supportedVersion := range supportedMetricsAPIVersions { if version.Version == supportedVersion { return true } } } } return false } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node.go000066400000000000000000000151661504711711200275600ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package top import ( "context" "errors" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/discovery" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/metricsutil" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" metricsapi "k8s.io/metrics/pkg/apis/metrics" metricsV1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned" ) // TopNodeOptions contains all the options for running the top-node cli command. type TopNodeOptions struct { ResourceName string Selector string SortBy string NoHeaders bool UseProtocolBuffers bool ShowCapacity bool NodeClient corev1client.CoreV1Interface Printer *metricsutil.TopCmdPrinter DiscoveryClient discovery.DiscoveryInterface MetricsClient metricsclientset.Interface genericiooptions.IOStreams } var ( topNodeLong = templates.LongDesc(i18n.T(` Display resource (CPU/memory) usage of nodes. The top-node command allows you to see the resource consumption of nodes.`)) topNodeExample = templates.Examples(i18n.T(` # Show metrics for all nodes kubectl top node # Show metrics for a given node kubectl top node NODE_NAME`)) ) func NewCmdTopNode(f cmdutil.Factory, o *TopNodeOptions, streams genericiooptions.IOStreams) *cobra.Command { if o == nil { o = &TopNodeOptions{ IOStreams: streams, UseProtocolBuffers: true, } } cmd := &cobra.Command{ Use: "node [NAME | -l label]", DisableFlagsInUseLine: true, Short: i18n.T("Display resource (CPU/memory) usage of nodes"), Long: topNodeLong, Example: topNodeExample, ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "node"), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunTopNode()) }, Aliases: []string{"nodes", "no"}, } cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) cmd.Flags().StringVar(&o.SortBy, "sort-by", o.SortBy, "If non-empty, sort nodes list using specified field. The field can be either 'cpu' or 'memory'.") cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If present, print output without headers") cmd.Flags().BoolVar(&o.UseProtocolBuffers, "use-protocol-buffers", o.UseProtocolBuffers, "Enables using protocol-buffers to access Metrics API.") cmd.Flags().BoolVar(&o.ShowCapacity, "show-capacity", o.ShowCapacity, "Print node resources based on Capacity instead of Allocatable(default) of the nodes.") return cmd } func (o *TopNodeOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { if len(args) == 1 { o.ResourceName = args[0] } else if len(args) > 1 { return cmdutil.UsageErrorf(cmd, "%s", cmd.Use) } clientset, err := f.KubernetesClientSet() if err != nil { return err } o.DiscoveryClient = clientset.DiscoveryClient config, err := f.ToRESTConfig() if err != nil { return err } if o.UseProtocolBuffers { config.ContentType = "application/vnd.kubernetes.protobuf" } o.MetricsClient, err = metricsclientset.NewForConfig(config) if err != nil { return err } o.NodeClient = clientset.CoreV1() o.Printer = metricsutil.NewTopCmdPrinter(o.Out) return nil } func (o *TopNodeOptions) Validate() error { if len(o.SortBy) > 0 { if o.SortBy != sortByCPU && o.SortBy != sortByMemory { return errors.New("--sort-by accepts only cpu or memory") } } if len(o.ResourceName) > 0 && len(o.Selector) > 0 { return errors.New("only one of NAME or --selector can be provided") } return nil } func (o TopNodeOptions) RunTopNode() error { var err error selector := labels.Everything() if len(o.Selector) > 0 { selector, err = labels.Parse(o.Selector) if err != nil { return err } } apiGroups, err := o.DiscoveryClient.ServerGroups() if err != nil { return err } metricsAPIAvailable := SupportedMetricsAPIVersionAvailable(apiGroups) if !metricsAPIAvailable { return errors.New("Metrics API not available") } metrics, err := getNodeMetricsFromMetricsAPI(o.MetricsClient, o.ResourceName, selector) if err != nil { return err } if len(metrics.Items) == 0 { return errors.New("metrics not available yet") } var nodes []v1.Node if len(o.ResourceName) > 0 { node, err := o.NodeClient.Nodes().Get(context.TODO(), o.ResourceName, metav1.GetOptions{}) if err != nil { return err } nodes = append(nodes, *node) } else { nodeList, err := o.NodeClient.Nodes().List(context.TODO(), metav1.ListOptions{ LabelSelector: selector.String(), }) if err != nil { return err } nodes = append(nodes, nodeList.Items...) } availableResources := make(map[string]v1.ResourceList) for _, n := range nodes { if !o.ShowCapacity { availableResources[n.Name] = n.Status.Allocatable } else { availableResources[n.Name] = n.Status.Capacity } } return o.Printer.PrintNodeMetrics(metrics.Items, availableResources, o.NoHeaders, o.SortBy) } func getNodeMetricsFromMetricsAPI(metricsClient metricsclientset.Interface, resourceName string, selector labels.Selector) (*metricsapi.NodeMetricsList, error) { var err error versionedMetrics := &metricsV1beta1api.NodeMetricsList{} mc := metricsClient.MetricsV1beta1() nm := mc.NodeMetricses() if resourceName != "" { m, err := nm.Get(context.TODO(), resourceName, metav1.GetOptions{}) if err != nil { return nil, err } versionedMetrics.Items = []metricsV1beta1api.NodeMetrics{*m} } else { versionedMetrics, err = nm.List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()}) if err != nil { return nil, err } } metrics := &metricsapi.NodeMetricsList{} err = metricsV1beta1api.Convert_v1beta1_NodeMetricsList_To_metrics_NodeMetricsList(versionedMetrics, metrics, nil) if err != nil { return nil, err } return metrics, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node_test.go000066400000000000000000000363161504711711200306170ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package top import ( "bytes" "fmt" "io" "net/http" "reflect" "strings" "testing" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" core "k8s.io/client-go/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake" ) const ( apiPrefix = "api" apiVersion = "v1" ) func TestTopNodeAllMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) expectedMetrics, nodes := testNodeV1beta1MetricsData() expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil case p == "/apis": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil case p == expectedNodePath && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, nodes)}, nil default: t.Fatalf("unexpected request: %#v\nGot URL: %#v\n", req, req.URL) return nil, nil } }), } fakemetricsClientset := &metricsfake.Clientset{} fakemetricsClientset.AddReactor("list", "nodes", func(action core.Action) (handled bool, ret runtime.Object, err error) { return true, expectedMetrics, nil }) tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdTopNode(tf, nil, streams) // TODO in the long run, we want to test most of our commands like this. Wire the options struct with specific mocks // TODO then check the particular Run functionality and harvest results from fake clients cmdOptions := &TopNodeOptions{ IOStreams: streams, } if err := cmdOptions.Complete(tf, cmd, []string{}); err != nil { t.Fatal(err) } cmdOptions.MetricsClient = fakemetricsClientset if err := cmdOptions.Validate(); err != nil { t.Fatal(err) } if err := cmdOptions.RunTopNode(); err != nil { t.Fatal(err) } // Check the presence of node names in the output. result := buf.String() for _, m := range expectedMetrics.Items { if !strings.Contains(result, m.Name) { t.Errorf("missing metrics for %s: \n%s", m.Name, result) } } } func TestTopNodeWithNameMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := metrics.Items[0] expectedNode := nodes.Items[0] nonExpectedMetrics := metricsv1beta1api.NodeMetricsList{ ListMeta: metrics.ListMeta, Items: metrics.Items[1:], } expectedNodePath := fmt.Sprintf("/%s/%s/nodes/%s", apiPrefix, apiVersion, expectedMetrics.Name) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil case p == "/apis": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil case p == expectedNodePath && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &expectedNode)}, nil default: t.Fatalf("unexpected request: %#v\nGot URL: %#v\n", req, req.URL) return nil, nil } }), } fakemetricsClientset := &metricsfake.Clientset{} fakemetricsClientset.AddReactor("get", "nodes", func(action core.Action) (handled bool, ret runtime.Object, err error) { return true, &expectedMetrics, nil }) tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdTopNode(tf, nil, streams) // TODO in the long run, we want to test most of our commands like this. Wire the options struct with specific mocks // TODO then check the particular Run functionality and harvest results from fake clients cmdOptions := &TopNodeOptions{ IOStreams: streams, } if err := cmdOptions.Complete(tf, cmd, []string{expectedMetrics.Name}); err != nil { t.Fatal(err) } cmdOptions.MetricsClient = fakemetricsClientset if err := cmdOptions.Validate(); err != nil { t.Fatal(err) } if err := cmdOptions.RunTopNode(); err != nil { t.Fatal(err) } // Check the presence of node names in the output. result := buf.String() if !strings.Contains(result, expectedMetrics.Name) { t.Errorf("missing metrics for %s: \n%s", expectedMetrics.Name, result) } for _, m := range nonExpectedMetrics.Items { if strings.Contains(result, m.Name) { t.Errorf("unexpected metrics for %s: \n%s", m.Name, result) } } } func TestTopNodeWithLabelSelectorMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := &metricsv1beta1api.NodeMetricsList{ ListMeta: metrics.ListMeta, Items: metrics.Items[0:1], } expectedNodes := v1.NodeList{ ListMeta: nodes.ListMeta, Items: nodes.Items[0:1], } nonExpectedMetrics := &metricsv1beta1api.NodeMetricsList{ ListMeta: metrics.ListMeta, Items: metrics.Items[1:], } label := "key=value" expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m, _ := req.URL.Path, req.Method, req.URL.RawQuery; { case p == "/api": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil case p == "/apis": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil case p == expectedNodePath && m == "GET": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &expectedNodes)}, nil default: t.Fatalf("unexpected request: %#v\nGot URL: %#v\n", req, req.URL) return nil, nil } }), } fakemetricsClientset := &metricsfake.Clientset{} fakemetricsClientset.AddReactor("list", "nodes", func(action core.Action) (handled bool, ret runtime.Object, err error) { return true, expectedMetrics, nil }) tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdTopNode(tf, nil, streams) cmd.Flags().Set("selector", label) // TODO in the long run, we want to test most of our commands like this. Wire the options struct with specific mocks // TODO then check the particular Run functionality and harvest results from fake clients cmdOptions := &TopNodeOptions{ IOStreams: streams, } if err := cmdOptions.Complete(tf, cmd, []string{}); err != nil { t.Fatal(err) } cmdOptions.MetricsClient = fakemetricsClientset if err := cmdOptions.Validate(); err != nil { t.Fatal(err) } if err := cmdOptions.RunTopNode(); err != nil { t.Fatal(err) } // Check the presence of node names in the output. result := buf.String() for _, m := range expectedMetrics.Items { if !strings.Contains(result, m.Name) { t.Errorf("missing metrics for %s: \n%s", m.Name, result) } } for _, m := range nonExpectedMetrics.Items { if strings.Contains(result, m.Name) { t.Errorf("unexpected metrics for %s: \n%s", m.Name, result) } } } func TestTopNodeWithSortByCpuMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := &metricsv1beta1api.NodeMetricsList{ ListMeta: metrics.ListMeta, Items: metrics.Items[:], } expectedNodes := v1.NodeList{ ListMeta: nodes.ListMeta, Items: nodes.Items[:], } expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) expectedNodesNames := []string{"node2", "node3", "node1"} tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil case p == "/apis": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil case p == expectedNodePath && m == "GET": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &expectedNodes)}, nil default: t.Fatalf("unexpected request: %#v\nGot URL: %#v\n", req, req.URL) return nil, nil } }), } fakemetricsClientset := &metricsfake.Clientset{} fakemetricsClientset.AddReactor("list", "nodes", func(action core.Action) (handled bool, ret runtime.Object, err error) { return true, expectedMetrics, nil }) tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdTopNode(tf, nil, streams) cmd.Flags().Set("sort-by", "cpu") // TODO in the long run, we want to test most of our commands like this. Wire the options struct with specific mocks // TODO then check the particular Run functionality and harvest results from fake clients cmdOptions := &TopNodeOptions{ IOStreams: streams, SortBy: "cpu", } if err := cmdOptions.Complete(tf, cmd, []string{}); err != nil { t.Fatal(err) } cmdOptions.MetricsClient = fakemetricsClientset if err := cmdOptions.Validate(); err != nil { t.Fatal(err) } if err := cmdOptions.RunTopNode(); err != nil { t.Fatal(err) } // Check the presence of node names in the output. result := buf.String() for _, m := range expectedMetrics.Items { if !strings.Contains(result, m.Name) { t.Errorf("missing metrics for %s: \n%s", m.Name, result) } } resultLines := strings.Split(result, "\n") resultNodes := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line lineFirstColumn := strings.Split(line, " ")[0] resultNodes[i] = lineFirstColumn } if !reflect.DeepEqual(resultNodes, expectedNodesNames) { t.Errorf("kinds not matching:\n\texpectedKinds: %v\n\tgotKinds: %v\n", expectedNodesNames, resultNodes) } } func TestTopNodeWithSortByMemoryMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := &metricsv1beta1api.NodeMetricsList{ ListMeta: metrics.ListMeta, Items: metrics.Items[:], } expectedNodes := v1.NodeList{ ListMeta: nodes.ListMeta, Items: nodes.Items[:], } expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) expectedNodesNames := []string{"node2", "node3", "node1"} tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) ns := scheme.Codecs tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { case p == "/api": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil case p == "/apis": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil case p == expectedNodePath && m == "GET": return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &expectedNodes)}, nil default: t.Fatalf("unexpected request: %#v\nGot URL: %#v\n", req, req.URL) return nil, nil } }), } fakemetricsClientset := &metricsfake.Clientset{} fakemetricsClientset.AddReactor("list", "nodes", func(action core.Action) (handled bool, ret runtime.Object, err error) { return true, expectedMetrics, nil }) tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdTopNode(tf, nil, streams) cmd.Flags().Set("sort-by", "memory") // TODO in the long run, we want to test most of our commands like this. Wire the options struct with specific mocks // TODO then check the particular Run functionality and harvest results from fake clients cmdOptions := &TopNodeOptions{ IOStreams: streams, SortBy: "memory", } if err := cmdOptions.Complete(tf, cmd, []string{}); err != nil { t.Fatal(err) } cmdOptions.MetricsClient = fakemetricsClientset if err := cmdOptions.Validate(); err != nil { t.Fatal(err) } if err := cmdOptions.RunTopNode(); err != nil { t.Fatal(err) } // Check the presence of node names in the output. result := buf.String() for _, m := range expectedMetrics.Items { if !strings.Contains(result, m.Name) { t.Errorf("missing metrics for %s: \n%s", m.Name, result) } } resultLines := strings.Split(result, "\n") resultNodes := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line lineFirstColumn := strings.Split(line, " ")[0] resultNodes[i] = lineFirstColumn } if !reflect.DeepEqual(resultNodes, expectedNodesNames) { t.Errorf("kinds not matching:\n\texpectedKinds: %v\n\tgotKinds: %v\n", expectedNodesNames, resultNodes) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod.go000066400000000000000000000222241504711711200274060ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package top import ( "context" "errors" "fmt" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/discovery" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/metricsutil" "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" metricsapi "k8s.io/metrics/pkg/apis/metrics" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned" "github.com/spf13/cobra" "k8s.io/klog/v2" ) type TopPodOptions struct { ResourceName string Namespace string LabelSelector string FieldSelector string SortBy string AllNamespaces bool PrintContainers bool NoHeaders bool UseProtocolBuffers bool Sum bool PodClient corev1client.PodsGetter Printer *metricsutil.TopCmdPrinter DiscoveryClient discovery.DiscoveryInterface MetricsClient metricsclientset.Interface genericiooptions.IOStreams } const metricsCreationDelay = 2 * time.Minute var ( topPodLong = templates.LongDesc(i18n.T(` Display resource (CPU/memory) usage of pods. The 'top pod' command allows you to see the resource consumption of pods. Due to the metrics pipeline delay, they may be unavailable for a few minutes since pod creation.`)) topPodExample = templates.Examples(i18n.T(` # Show metrics for all pods in the default namespace kubectl top pod # Show metrics for all pods in the given namespace kubectl top pod --namespace=NAMESPACE # Show metrics for a given pod and its containers kubectl top pod POD_NAME --containers # Show metrics for the pods defined by label name=myLabel kubectl top pod -l name=myLabel`)) ) func NewCmdTopPod(f cmdutil.Factory, o *TopPodOptions, streams genericiooptions.IOStreams) *cobra.Command { if o == nil { o = &TopPodOptions{ IOStreams: streams, UseProtocolBuffers: true, } } cmd := &cobra.Command{ Use: "pod [NAME | -l label]", DisableFlagsInUseLine: true, Short: i18n.T("Display resource (CPU/memory) usage of pods"), Long: topPodLong, Example: topPodExample, ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "pod"), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunTopPod()) }, Aliases: []string{"pods", "po"}, } cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) cmd.Flags().StringVar(&o.FieldSelector, "field-selector", o.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") cmd.Flags().StringVar(&o.SortBy, "sort-by", o.SortBy, "If non-empty, sort pods list using specified field. The field can be either 'cpu' or 'memory'.") cmd.Flags().BoolVar(&o.PrintContainers, "containers", o.PrintContainers, "If present, print usage of containers within a pod.") cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If present, print output without headers.") cmd.Flags().BoolVar(&o.UseProtocolBuffers, "use-protocol-buffers", o.UseProtocolBuffers, "Enables using protocol-buffers to access Metrics API.") cmd.Flags().BoolVar(&o.Sum, "sum", o.Sum, "Print the sum of the resource usage") return cmd } func (o *TopPodOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error if len(args) == 1 { o.ResourceName = args[0] } else if len(args) > 1 { return cmdutil.UsageErrorf(cmd, "%s", cmd.Use) } o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } clientset, err := f.KubernetesClientSet() if err != nil { return err } o.DiscoveryClient = clientset.DiscoveryClient config, err := f.ToRESTConfig() if err != nil { return err } if o.UseProtocolBuffers { config.ContentType = "application/vnd.kubernetes.protobuf" } o.MetricsClient, err = metricsclientset.NewForConfig(config) if err != nil { return err } o.PodClient = clientset.CoreV1() o.Printer = metricsutil.NewTopCmdPrinter(o.Out) return nil } func (o *TopPodOptions) Validate() error { if len(o.SortBy) > 0 { if o.SortBy != sortByCPU && o.SortBy != sortByMemory { return errors.New("--sort-by accepts only cpu or memory") } } if len(o.ResourceName) > 0 && (len(o.LabelSelector) > 0 || len(o.FieldSelector) > 0) { return errors.New("only one of NAME or selector can be provided") } return nil } func (o TopPodOptions) RunTopPod() error { var err error labelSelector := labels.Everything() if len(o.LabelSelector) > 0 { labelSelector, err = labels.Parse(o.LabelSelector) if err != nil { return err } } fieldSelector := fields.Everything() if len(o.FieldSelector) > 0 { fieldSelector, err = fields.ParseSelector(o.FieldSelector) if err != nil { return err } } apiGroups, err := o.DiscoveryClient.ServerGroups() if err != nil { return err } metricsAPIAvailable := SupportedMetricsAPIVersionAvailable(apiGroups) if !metricsAPIAvailable { return errors.New("Metrics API not available") } metrics, err := getMetricsFromMetricsAPI(o.MetricsClient, o.Namespace, o.ResourceName, o.AllNamespaces, labelSelector, fieldSelector) if err != nil { return err } // First we check why no metrics have been received. if len(metrics.Items) == 0 { // If the API server query is successful but all the pods are newly created, // the metrics are probably not ready yet, so we return the error here in the first place. err := verifyEmptyMetrics(o, labelSelector, fieldSelector) if err != nil { return err } // if we had no errors, be sure we output something. if o.AllNamespaces { fmt.Fprintln(o.ErrOut, "No resources found") } else { fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace) } } return o.Printer.PrintPodMetrics(metrics.Items, o.PrintContainers, o.AllNamespaces, o.NoHeaders, o.SortBy, o.Sum) } func getMetricsFromMetricsAPI(metricsClient metricsclientset.Interface, namespace, resourceName string, allNamespaces bool, labelSelector labels.Selector, fieldSelector fields.Selector) (*metricsapi.PodMetricsList, error) { var err error ns := metav1.NamespaceAll if !allNamespaces { ns = namespace } versionedMetrics := &metricsv1beta1api.PodMetricsList{} if resourceName != "" { m, err := metricsClient.MetricsV1beta1().PodMetricses(ns).Get(context.TODO(), resourceName, metav1.GetOptions{}) if err != nil { return nil, err } versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m} } else { versionedMetrics, err = metricsClient.MetricsV1beta1().PodMetricses(ns).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()}) if err != nil { return nil, err } } metrics := &metricsapi.PodMetricsList{} err = metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, metrics, nil) if err != nil { return nil, err } return metrics, nil } func verifyEmptyMetrics(o TopPodOptions, labelSelector labels.Selector, fieldSelector fields.Selector) error { if len(o.ResourceName) > 0 { pod, err := o.PodClient.Pods(o.Namespace).Get(context.TODO(), o.ResourceName, metav1.GetOptions{}) if err != nil { return err } if err := checkPodAge(pod); err != nil { return err } } else { pods, err := o.PodClient.Pods(o.Namespace).List(context.TODO(), metav1.ListOptions{ LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String(), }) if err != nil { return err } if len(pods.Items) == 0 { return nil } for _, pod := range pods.Items { if err := checkPodAge(&pod); err != nil { return err } } } return errors.New("metrics not available yet") } func checkPodAge(pod *corev1.Pod) error { age := time.Since(pod.CreationTimestamp.Time) if age > metricsCreationDelay { message := fmt.Sprintf("Metrics not available for pod %s/%s, age: %s", pod.Namespace, pod.Name, age.String()) return errors.New(message) } else { klog.V(2).Infof("Metrics not yet available for pod %s/%s, age: %s", pod.Namespace, pod.Name, age.String()) return nil } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod_test.go000066400000000000000000000355201504711711200304500ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package top import ( "bytes" "io" "net/http" "net/url" "reflect" "strings" "testing" "time" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/rest/fake" core "k8s.io/client-go/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" metricsv1alpha1api "k8s.io/metrics/pkg/apis/metrics/v1alpha1" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake" ) const ( apibody = `{ "kind": "APIVersions", "versions": [ "v1" ], "serverAddressByClientCIDRs": [ { "clientCIDR": "0.0.0.0/0", "serverAddress": "10.0.2.15:8443" } ] }` apisbodyWithMetrics = `{ "kind": "APIGroupList", "apiVersion": "v1", "groups": [ { "name":"metrics.k8s.io", "versions":[ { "groupVersion":"metrics.k8s.io/v1beta1", "version":"v1beta1" } ], "preferredVersion":{ "groupVersion":"metrics.k8s.io/v1beta1", "version":"v1beta1" }, "serverAddressByClientCIDRs":null } ] }` ) func TestTopPod(t *testing.T) { testNS := "testns" testCases := []struct { name string namespace string options *TopPodOptions args []string expectedQuery string expectedPods []string expectedContainers []string namespaces []string containers bool listsNamespaces bool }{ { name: "all namespaces", options: &TopPodOptions{AllNamespaces: true}, namespaces: []string{testNS, "secondtestns", "thirdtestns"}, listsNamespaces: true, }, { name: "all in namespace", namespaces: []string{testNS, testNS}, }, { name: "pod with name", args: []string{"pod1"}, namespaces: []string{testNS}, }, { name: "pod with label selector", options: &TopPodOptions{LabelSelector: "key=value"}, expectedQuery: "labelSelector=" + url.QueryEscape("key=value"), namespaces: []string{testNS, testNS}, }, { name: "pod with field selector", options: &TopPodOptions{FieldSelector: "key=value"}, expectedQuery: "fieldSelector=" + url.QueryEscape("key=value"), namespaces: []string{testNS, testNS}, }, { name: "pod with container metrics", options: &TopPodOptions{PrintContainers: true}, args: []string{"pod1"}, expectedContainers: []string{ "container1-1", "container1-2", }, namespaces: []string{testNS}, containers: true, }, { name: "pod sort by cpu", options: &TopPodOptions{SortBy: "cpu"}, expectedPods: []string{"pod2", "pod3", "pod1"}, namespaces: []string{testNS, testNS, testNS}, }, { name: "pod sort by memory", options: &TopPodOptions{SortBy: "memory"}, expectedPods: []string{"pod2", "pod3", "pod1"}, namespaces: []string{testNS, testNS, testNS}, }, { name: "container sort by cpu", options: &TopPodOptions{PrintContainers: true, SortBy: "cpu"}, expectedContainers: []string{ "container2-3", "container2-2", "container2-1", "container3-1", "container1-2", "container1-1", }, namespaces: []string{testNS, testNS, testNS}, containers: true, }, { name: "container sort by memory", options: &TopPodOptions{PrintContainers: true, SortBy: "memory"}, expectedContainers: []string{ "container2-3", "container2-2", "container2-1", "container3-1", "container1-2", "container1-1", }, namespaces: []string{testNS, testNS, testNS}, containers: true, }, } cmdtesting.InitTestErrorHandler(t) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { metricsList := testV1beta1PodMetricsData() var expectedMetrics []metricsv1beta1api.PodMetrics var expectedContainerNames, nonExpectedMetricsNames []string for n, m := range metricsList { if n < len(testCase.namespaces) { m.Namespace = testCase.namespaces[n] expectedMetrics = append(expectedMetrics, m) for _, c := range m.Containers { expectedContainerNames = append(expectedContainerNames, c.Name) } } else { nonExpectedMetricsNames = append(nonExpectedMetricsNames, m.Name) } } fakemetricsClientset := &metricsfake.Clientset{} if len(expectedMetrics) == 1 { fakemetricsClientset.AddReactor("get", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { return true, &expectedMetrics[0], nil }) } else { fakemetricsClientset.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { res := &metricsv1beta1api.PodMetricsList{ ListMeta: metav1.ListMeta{ ResourceVersion: "2", }, Items: expectedMetrics, } return true, res, nil }) } tf := cmdtesting.NewTestFactory().WithNamespace(testNS) defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p := req.URL.Path; { case p == "/api": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil case p == "/apis": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil default: t.Fatalf("%s: unexpected request: %#v\nGot URL: %#v", testCase.name, req, req.URL) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdTopPod(tf, nil, streams) var cmdOptions *TopPodOptions if testCase.options != nil { cmdOptions = testCase.options } else { cmdOptions = &TopPodOptions{} } cmdOptions.IOStreams = streams // TODO in the long run, we want to test most of our commands like this. Wire the options struct with specific mocks // TODO then check the particular Run functionality and harvest results from fake clients. We probably end up skipping the factory altogether. if err := cmdOptions.Complete(tf, cmd, testCase.args); err != nil { t.Fatal(err) } cmdOptions.MetricsClient = fakemetricsClientset if err := cmdOptions.Validate(); err != nil { t.Fatal(err) } if err := cmdOptions.RunTopPod(); err != nil { t.Fatal(err) } // Check the presence of pod names&namespaces/container names in the output. result := buf.String() if testCase.containers { for _, containerName := range expectedContainerNames { if !strings.Contains(result, containerName) { t.Errorf("missing metrics for container %s: \n%s", containerName, result) } } } for _, m := range expectedMetrics { if !strings.Contains(result, m.Name) { t.Errorf("missing metrics for %s: \n%s", m.Name, result) } if testCase.listsNamespaces && !strings.Contains(result, m.Namespace) { t.Errorf("missing metrics for %s/%s: \n%s", m.Namespace, m.Name, result) } } for _, name := range nonExpectedMetricsNames { if strings.Contains(result, name) { t.Errorf("unexpected metrics for %s: \n%s", name, result) } } if testCase.expectedPods != nil { resultPods := getResultColumnValues(result, 0) if !reflect.DeepEqual(testCase.expectedPods, resultPods) { t.Errorf("pods not matching:\n\texpectedPods: %v\n\tresultPods: %v\n", testCase.expectedPods, resultPods) } } if testCase.expectedContainers != nil { resultContainers := getResultColumnValues(result, 1) if !reflect.DeepEqual(testCase.expectedContainers, resultContainers) { t.Errorf("containers not matching:\n\texpectedContainers: %v\n\tresultContainers: %v\n", testCase.expectedContainers, resultContainers) } } }) } } func getResultColumnValues(result string, columnIndex int) []string { resultLines := strings.Split(result, "\n") values := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line value := strings.Fields(line)[columnIndex] values[i] = value } return values } func TestTopPodNoResourcesFound(t *testing.T) { testNS := "testns" testCases := []struct { name string options *TopPodOptions namespace string expectedOutput string expectedErr string }{ { name: "all namespaces", options: &TopPodOptions{AllNamespaces: true}, expectedOutput: "", expectedErr: "No resources found\n", }, { name: "all in namespace", namespace: testNS, expectedOutput: "", expectedErr: "No resources found in " + testNS + " namespace.\n", }, } cmdtesting.InitTestErrorHandler(t) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fakemetricsClientset := &metricsfake.Clientset{} fakemetricsClientset.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { res := &metricsv1beta1api.PodMetricsList{ ListMeta: metav1.ListMeta{ ResourceVersion: "2", }, Items: nil, // No metrics found } return true, res, nil }) tf := cmdtesting.NewTestFactory().WithNamespace(testNS) defer tf.Cleanup() ns := scheme.Codecs.WithoutConversion() tf.Client = &fake.RESTClient{ NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p := req.URL.Path; { case p == "/api": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil case p == "/apis": return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil case p == "/api/v1/namespaces/"+testNS+"/pods": // Top Pod calls this endpoint to check if there are pods whenever it gets no metrics, // so we need to return no pods for this test scenario body, _ := marshallBody(metricsv1alpha1api.PodMetricsList{ ListMeta: metav1.ListMeta{ ResourceVersion: "2", }, Items: nil, // No pods found }) return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil default: t.Fatalf("%s: unexpected request: %#v\nGot URL: %#v", testCase.name, req, req.URL) return nil, nil } }), } tf.ClientConfigVal = cmdtesting.DefaultClientConfig() streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdTopPod(tf, nil, streams) var cmdOptions *TopPodOptions if testCase.options != nil { cmdOptions = testCase.options } else { cmdOptions = &TopPodOptions{} } cmdOptions.IOStreams = streams if err := cmdOptions.Complete(tf, cmd, nil); err != nil { t.Fatal(err) } cmdOptions.MetricsClient = fakemetricsClientset if err := cmdOptions.Validate(); err != nil { t.Fatal(err) } if err := cmdOptions.RunTopPod(); err != nil { t.Fatal(err) } if e, a := testCase.expectedOutput, buf.String(); e != a { t.Errorf("Unexpected output:\nExpected:\n%v\nActual:\n%v", e, a) } if e, a := testCase.expectedErr, errbuf.String(); e != a { t.Errorf("Unexpected error:\nExpected:\n%v\nActual:\n%v", e, a) } }) } } func testV1beta1PodMetricsData() []metricsv1beta1api.PodMetrics { return []metricsv1beta1api.PodMetrics{ { ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "test", ResourceVersion: "10", Labels: map[string]string{"key": "value"}}, Window: metav1.Duration{Duration: time.Minute}, Containers: []metricsv1beta1api.ContainerMetrics{ { Name: "container1-1", Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), }, }, { Name: "container1-2", Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(4, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(5*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI), }, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "pod2", Namespace: "test", ResourceVersion: "11", Labels: map[string]string{"key": "value"}}, Window: metav1.Duration{Duration: time.Minute}, Containers: []metricsv1beta1api.ContainerMetrics{ { Name: "container2-1", Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(7, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(8*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(9*(1024*1024), resource.DecimalSI), }, }, { Name: "container2-2", Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(10, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(11*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(12*(1024*1024), resource.DecimalSI), }, }, { Name: "container2-3", Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(13, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(14*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(15*(1024*1024), resource.DecimalSI), }, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "pod3", Namespace: "test", ResourceVersion: "12"}, Window: metav1.Duration{Duration: time.Minute}, Containers: []metricsv1beta1api.ContainerMetrics{ { Name: "container3-1", Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(7, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(8*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(9*(1024*1024), resource.DecimalSI), }, }, }, }, } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/top/top_test.go000066400000000000000000000103021504711711200275750ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package top import ( "bytes" "encoding/json" "io" "time" "testing" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) func TestTopSubcommandsExist(t *testing.T) { cmdtesting.InitTestErrorHandler(t) f := cmdtesting.NewTestFactory() defer f.Cleanup() cmd := NewCmdTop(f, genericiooptions.NewTestIOStreamsDiscard()) if !cmd.HasSubCommands() { t.Error("top command should have subcommands") } } func marshallBody(metrics interface{}) (io.ReadCloser, error) { result, err := json.Marshal(metrics) if err != nil { return nil, err } return io.NopCloser(bytes.NewReader(result)), nil } func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeList) { metrics := &metricsv1beta1api.NodeMetricsList{ ListMeta: metav1.ListMeta{ ResourceVersion: "1", }, Items: []metricsv1beta1api.NodeMetrics{ { ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}}, Window: metav1.Duration{Duration: time.Minute}, Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), }, }, { ObjectMeta: metav1.ObjectMeta{Name: "node2", ResourceVersion: "11"}, Window: metav1.Duration{Duration: time.Minute}, Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(5, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(7*(1024*1024), resource.DecimalSI), }, }, { ObjectMeta: metav1.ObjectMeta{Name: "node3", ResourceVersion: "11"}, Window: metav1.Duration{Duration: time.Minute}, Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(3, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(4*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(5*(1024*1024), resource.DecimalSI), }, }, }, } nodes := &v1.NodeList{ ListMeta: metav1.ListMeta{ ResourceVersion: "15", }, Items: []v1.Node{ { ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10"}, Status: v1.NodeStatus{ Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(10, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(20*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(30*(1024*1024), resource.DecimalSI), }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "node2", ResourceVersion: "11"}, Status: v1.NodeStatus{ Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(60*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(70*(1024*1024), resource.DecimalSI), }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "node3", ResourceVersion: "11"}, Status: v1.NodeStatus{ Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(30, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(40*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(50*(1024*1024), resource.DecimalSI), }, }, }, }, } return metrics, nodes } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/000077500000000000000000000000001504711711200255645ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/000077500000000000000000000000001504711711200270525ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/crlf/000077500000000000000000000000001504711711200300005ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/crlf/crlf.go000066400000000000000000000024401504711711200312550ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package crlf import ( "bytes" "io" ) type crlfWriter struct { io.Writer } // NewCRLFWriter implements a CR/LF line ending writer used for normalizing // text for Windows platforms. func NewCRLFWriter(w io.Writer) io.Writer { return crlfWriter{w} } func (w crlfWriter) Write(b []byte) (n int, err error) { for i, written := 0, 0; ; { next := bytes.Index(b[i:], []byte("\n")) if next == -1 { n, err := w.Writer.Write(b[i:]) return written + n, err } next = next + i n, err := w.Writer.Write(b[i:next]) if err != nil { return written + n, err } written += n n, err = w.Writer.Write([]byte("\r\n")) if err != nil { if n > 1 { n = 1 } return written + n, err } written++ i = next + 1 } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editoptions.go000066400000000000000000000636211504711711200317520ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package editor import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "reflect" goruntime "runtime" "strings" "github.com/spf13/cobra" jsonpatch "gopkg.in/evanphx/json-patch.v4" "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" 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/types" "k8s.io/apimachinery/pkg/util/mergepatch" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util/editor/crlf" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" ) // EditOptions contains all the options for running edit cli command. type EditOptions struct { resource.FilenameOptions RecordFlags *genericclioptions.RecordFlags PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) OutputPatch bool WindowsLineEndings bool cmdutil.ValidateOptions ValidationDirective string OriginalResult *resource.Result EditMode EditMode CmdNamespace string ApplyAnnotation bool ChangeCause string managedFields map[types.UID][]metav1.ManagedFieldsEntry genericiooptions.IOStreams Recorder genericclioptions.Recorder f cmdutil.Factory editPrinterOptions *editPrinterOptions updatedResultGetter func(data []byte) *resource.Result FieldManager string Subresource string } // NewEditOptions returns an initialized EditOptions instance func NewEditOptions(editMode EditMode, ioStreams genericiooptions.IOStreams) *EditOptions { return &EditOptions{ RecordFlags: genericclioptions.NewRecordFlags(), EditMode: editMode, PrintFlags: genericclioptions.NewPrintFlags("edited").WithTypeSetter(scheme.Scheme), editPrinterOptions: &editPrinterOptions{ // create new editor-specific PrintFlags, with all // output flags disabled, except json / yaml printFlags: (&genericclioptions.PrintFlags{ JSONYamlPrintFlags: genericclioptions.NewJSONYamlPrintFlags(), }).WithDefaultOutput("yaml"), ext: ".yaml", addHeader: true, }, WindowsLineEndings: goruntime.GOOS == "windows", Recorder: genericclioptions.NoopRecorder{}, IOStreams: ioStreams, } } type editPrinterOptions struct { printFlags *genericclioptions.PrintFlags ext string addHeader bool } func (e *editPrinterOptions) Complete(fromPrintFlags *genericclioptions.PrintFlags) error { if e.printFlags == nil { return fmt.Errorf("missing PrintFlags in editor printer options") } // bind output format from existing printflags if fromPrintFlags != nil && len(*fromPrintFlags.OutputFormat) > 0 { e.printFlags.OutputFormat = fromPrintFlags.OutputFormat } // prevent a commented header at the top of the user's // default editor if presenting contents as json. if *e.printFlags.OutputFormat == "json" { e.addHeader = false e.ext = ".json" return nil } // we default to yaml if check above is false, as only json or yaml are supported e.addHeader = true e.ext = ".yaml" return nil } func (e *editPrinterOptions) PrintObj(obj runtime.Object, out io.Writer) error { p, err := e.printFlags.ToPrinter() if err != nil { return err } return p.PrintObj(obj, out) } // Complete completes all the required options func (o *EditOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) error { var err error o.RecordFlags.Complete(cmd) o.Recorder, err = o.RecordFlags.ToRecorder() if err != nil { return err } if o.EditMode != NormalEditMode && o.EditMode != EditBeforeCreateMode && o.EditMode != ApplyEditMode { return fmt.Errorf("unsupported edit mode %q", o.EditMode) } o.editPrinterOptions.Complete(o.PrintFlags) if o.OutputPatch && o.EditMode != NormalEditMode { return fmt.Errorf("the edit mode doesn't support output the patch") } cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } b := f.NewBuilder(). Unstructured() if o.EditMode == NormalEditMode || o.EditMode == ApplyEditMode { // when do normal edit or apply edit we need to always retrieve the latest resource from server b = b.ResourceTypeOrNameArgs(true, args...).Latest() } r := b.NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &o.FilenameOptions). Subresource(o.Subresource). ContinueOnError(). Flatten(). Do() err = r.Err() if err != nil { return err } o.OriginalResult = r o.updatedResultGetter = func(data []byte) *resource.Result { // resource builder to read objects from edited data return f.NewBuilder(). Unstructured(). Stream(bytes.NewReader(data), "edited-file"). Subresource(o.Subresource). ContinueOnError(). Flatten(). Do() } o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { o.PrintFlags.NamePrintFlags.Operation = operation return o.PrintFlags.ToPrinter() } o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd) if err != nil { return err } o.CmdNamespace = cmdNamespace o.f = f return nil } // Validate checks the EditOptions to see if there is sufficient information to run the command. func (o *EditOptions) Validate() error { return nil } // Run performs the execution func (o *EditOptions) Run() error { edit := NewDefaultEditor(editorEnvs()) // editFn is invoked for each edit session (once with a list for normal edit, once for each individual resource in a edit-on-create invocation) editFn := func(infos []*resource.Info) error { var ( results = editResults{} original = []byte{} edited = []byte{} file string err error ) containsError := false // loop until we succeed or cancel editing for { // get the object we're going to serialize as input to the editor var originalObj runtime.Object switch len(infos) { case 1: originalObj = infos[0].Object default: l := &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{}, }, } for _, info := range infos { l.Items = append(l.Items, *info.Object.(*unstructured.Unstructured)) } originalObj = l } // generate the file to edit buf := &bytes.Buffer{} var w io.Writer = buf if o.WindowsLineEndings { w = crlf.NewCRLFWriter(w) } if o.editPrinterOptions.addHeader { results.header.writeTo(w, o.EditMode) } if !containsError { if err := o.extractManagedFields(originalObj); err != nil { return preservedFile(err, results.file, o.ErrOut) } if err := o.editPrinterOptions.PrintObj(originalObj, w); err != nil { return preservedFile(err, results.file, o.ErrOut) } original = buf.Bytes() } else { // In case of an error, preserve the edited file. // Remove the comments (header) from it since we already // have included the latest header in the buffer above. buf.Write(cmdutil.ManualStrip(edited)) } // launch the editor editedDiff := edited edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), o.editPrinterOptions.ext, buf) if err != nil { return preservedFile(err, results.file, o.ErrOut) } // If we're retrying the loop because of an error, and no change was made in the file, short-circuit if containsError && bytes.Equal(cmdutil.StripComments(editedDiff), cmdutil.StripComments(edited)) { return preservedFile(fmt.Errorf("%s", "Edit cancelled, no valid changes were saved."), file, o.ErrOut) } // cleanup any file from the previous pass if len(results.file) > 0 { os.Remove(results.file) } klog.V(4).Infof("User edited:\n%s", string(edited)) // Apply validation schema, err := o.f.Validator(o.ValidationDirective) if err != nil { return preservedFile(err, file, o.ErrOut) } err = schema.ValidateBytes(cmdutil.StripComments(edited)) if err != nil { results = editResults{ file: file, } containsError = true fmt.Fprintln(o.ErrOut, results.addError(apierrors.NewInvalid(corev1.SchemeGroupVersion.WithKind("").GroupKind(), "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), infos[0])) continue } // Compare content without comments if bytes.Equal(cmdutil.StripComments(original), cmdutil.StripComments(edited)) { os.Remove(file) fmt.Fprintln(o.ErrOut, "Edit cancelled, no changes made.") return nil } lines, err := hasLines(bytes.NewBuffer(edited)) if err != nil { return preservedFile(err, file, o.ErrOut) } if !lines { os.Remove(file) fmt.Fprintln(o.ErrOut, "Edit cancelled, saved file was empty.") return nil } results = editResults{ file: file, } // parse the edited file updatedInfos, err := o.updatedResultGetter(edited).Infos() if err != nil { // syntax error containsError = true results.header.reasons = append(results.header.reasons, editReason{head: fmt.Sprintf("The edited file had a syntax error: %v", err)}) continue } // not a syntax error as it turns out... containsError = false updatedVisitor := resource.InfoListVisitor(updatedInfos) // we need to add back managedFields to both updated and original object if err := o.restoreManagedFields(updatedInfos); err != nil { return preservedFile(err, file, o.ErrOut) } if err := o.restoreManagedFields(infos); err != nil { return preservedFile(err, file, o.ErrOut) } // need to make sure the original namespace wasn't changed while editing if err := updatedVisitor.Visit(resource.RequireNamespace(o.CmdNamespace)); err != nil { return preservedFile(err, file, o.ErrOut) } // iterate through all items to apply annotations if err := o.visitAnnotation(updatedVisitor); err != nil { return preservedFile(err, file, o.ErrOut) } switch o.EditMode { case NormalEditMode: err = o.visitToPatch(infos, updatedVisitor, &results) case ApplyEditMode: err = o.visitToApplyEditPatch(infos, updatedVisitor) case EditBeforeCreateMode: err = o.visitToCreate(updatedVisitor) default: err = fmt.Errorf("unsupported edit mode %q", o.EditMode) } if err != nil { return preservedFile(err, results.file, o.ErrOut) } // Handle all possible errors // // 1. retryable: propose kubectl replace -f // 2. notfound: indicate the location of the saved configuration of the deleted resource // 3. invalid: retry those on the spot by looping ie. reloading the editor if results.retryable > 0 { fmt.Fprintf(o.ErrOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file) return cmdutil.ErrExit } if results.notfound > 0 { fmt.Fprintf(o.ErrOut, "The edits you made on deleted resources have been saved to %q\n", file) return cmdutil.ErrExit } if len(results.edit) == 0 { if results.notfound == 0 { os.Remove(file) } else { fmt.Fprintf(o.Out, "The edits you made on deleted resources have been saved to %q\n", file) } return nil } if len(results.header.reasons) > 0 { containsError = true } } } switch o.EditMode { // If doing normal edit we cannot use Visit because we need to edit a list for convenience. Ref: #20519 case NormalEditMode: infos, err := o.OriginalResult.Infos() if err != nil { return err } if len(infos) == 0 { return errors.New("edit cancelled, no objects found") } return editFn(infos) case ApplyEditMode: infos, err := o.OriginalResult.Infos() if err != nil { return err } var annotationInfos []*resource.Info for i := range infos { data, err := util.GetOriginalConfiguration(infos[i].Object) if err != nil { return err } if data == nil { continue } tempInfos, err := o.updatedResultGetter(data).Infos() if err != nil { return err } annotationInfos = append(annotationInfos, tempInfos[0]) } if len(annotationInfos) == 0 { return errors.New("no last-applied-configuration annotation found on resources, to create the annotation, use command `kubectl apply set-last-applied --create-annotation`") } return editFn(annotationInfos) // If doing an edit before created, we don't want a list and instead want the normal behavior as kubectl create. case EditBeforeCreateMode: return o.OriginalResult.Visit(func(info *resource.Info, err error) error { return editFn([]*resource.Info{info}) }) default: return fmt.Errorf("unsupported edit mode %q", o.EditMode) } } func (o *EditOptions) extractManagedFields(obj runtime.Object) error { o.managedFields = make(map[types.UID][]metav1.ManagedFieldsEntry) if meta.IsListType(obj) { err := meta.EachListItem(obj, func(obj runtime.Object) error { uid, mf, err := clearManagedFields(obj) if err != nil { return err } o.managedFields[uid] = mf return nil }) return err } uid, mf, err := clearManagedFields(obj) if err != nil { return err } o.managedFields[uid] = mf return nil } func clearManagedFields(obj runtime.Object) (types.UID, []metav1.ManagedFieldsEntry, error) { metaObjs, err := meta.Accessor(obj) if err != nil { return "", nil, err } mf := metaObjs.GetManagedFields() metaObjs.SetManagedFields(nil) return metaObjs.GetUID(), mf, nil } func (o *EditOptions) restoreManagedFields(infos []*resource.Info) error { for _, info := range infos { metaObjs, err := meta.Accessor(info.Object) if err != nil { return err } mf := o.managedFields[metaObjs.GetUID()] metaObjs.SetManagedFields(mf) } return nil } func (o *EditOptions) visitToApplyEditPatch(originalInfos []*resource.Info, patchVisitor resource.Visitor) error { err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error { editObjUID, err := meta.NewAccessor().UID(info.Object) if err != nil { return err } var originalInfo *resource.Info for _, i := range originalInfos { originalObjUID, err := meta.NewAccessor().UID(i.Object) if err != nil { return err } if editObjUID == originalObjUID { originalInfo = i break } } if originalInfo == nil { return fmt.Errorf("no original object found for %#v", info.Object) } originalJS, err := encodeToJSON(originalInfo.Object.(runtime.Unstructured)) if err != nil { return err } editedJS, err := encodeToJSON(info.Object.(runtime.Unstructured)) if err != nil { return err } if reflect.DeepEqual(originalJS, editedJS) { printer, err := o.ToPrinter("skipped") if err != nil { return err } return printer.PrintObj(info.Object, o.Out) } err = o.annotationPatch(info) if err != nil { return err } printer, err := o.ToPrinter("edited") if err != nil { return err } return printer.PrintObj(info.Object, o.Out) }) return err } func (o *EditOptions) annotationPatch(update *resource.Info) error { patch, _, patchType, err := GetApplyPatch(update.Object.(runtime.Unstructured)) if err != nil { return err } mapping := update.ResourceMapping() client, err := o.f.UnstructuredClientForMapping(mapping) if err != nil { return err } helper := resource.NewHelper(client, mapping). WithFieldManager(o.FieldManager). WithFieldValidation(o.ValidationDirective). WithSubresource(o.Subresource) _, err = helper.Patch(o.CmdNamespace, update.Name, patchType, patch, nil) return err } // GetApplyPatch is used to get and apply patches func GetApplyPatch(obj runtime.Unstructured) ([]byte, []byte, types.PatchType, error) { beforeJSON, err := encodeToJSON(obj) if err != nil { return nil, []byte(""), types.MergePatchType, err } objCopy := obj.DeepCopyObject() accessor := meta.NewAccessor() annotations, err := accessor.Annotations(objCopy) if err != nil { return nil, beforeJSON, types.MergePatchType, err } if annotations == nil { annotations = map[string]string{} } annotations[corev1.LastAppliedConfigAnnotation] = string(beforeJSON) accessor.SetAnnotations(objCopy, annotations) afterJSON, err := encodeToJSON(objCopy.(runtime.Unstructured)) if err != nil { return nil, beforeJSON, types.MergePatchType, err } patch, err := jsonpatch.CreateMergePatch(beforeJSON, afterJSON) return patch, beforeJSON, types.MergePatchType, err } func encodeToJSON(obj runtime.Unstructured) ([]byte, error) { serialization, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) if err != nil { return nil, err } js, err := yaml.ToJSON(serialization) if err != nil { return nil, err } return js, nil } func (o *EditOptions) visitToPatch(originalInfos []*resource.Info, patchVisitor resource.Visitor, results *editResults) error { err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error { editObjUID, err := meta.NewAccessor().UID(info.Object) if err != nil { return err } var originalInfo *resource.Info for _, i := range originalInfos { originalObjUID, err := meta.NewAccessor().UID(i.Object) if err != nil { return err } if editObjUID == originalObjUID { originalInfo = i break } } if originalInfo == nil { return fmt.Errorf("no original object found for %#v", info.Object) } originalJS, err := encodeToJSON(originalInfo.Object.(runtime.Unstructured)) if err != nil { return err } editedJS, err := encodeToJSON(info.Object.(runtime.Unstructured)) if err != nil { return err } if reflect.DeepEqual(originalJS, editedJS) { // no edit, so just skip it. printer, err := o.ToPrinter("skipped") if err != nil { return err } return printer.PrintObj(info.Object, o.Out) } preconditions := []mergepatch.PreconditionFunc{ mergepatch.RequireKeyUnchanged("apiVersion"), mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name"), mergepatch.RequireKeyUnchanged("managedFields"), } // Create the versioned struct from the type defined in the mapping // (which is the API version we'll be submitting the patch to) versionedObject, err := scheme.Scheme.New(info.Mapping.GroupVersionKind) var patchType types.PatchType var patch []byte switch { case runtime.IsNotRegisteredError(err): // fall back to generic JSON merge patch patchType = types.MergePatchType patch, err = jsonpatch.CreateMergePatch(originalJS, editedJS) if err != nil { klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) return err } var patchMap map[string]interface{} err = json.Unmarshal(patch, &patchMap) if err != nil { klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) return err } for _, precondition := range preconditions { if !precondition(patchMap) { klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") } } case err != nil: return err default: patchType = types.StrategicMergePatchType patch, err = strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, versionedObject, preconditions...) if err != nil { klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) if mergepatch.IsPreconditionFailed(err) { return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") } return err } } if o.OutputPatch { fmt.Fprintf(o.Out, "Patch: %s\n", string(patch)) } patched, err := resource.NewHelper(info.Client, info.Mapping). WithFieldManager(o.FieldManager). WithFieldValidation(o.ValidationDirective). WithSubresource(o.Subresource). Patch(info.Namespace, info.Name, patchType, patch, nil) if err != nil { fmt.Fprintln(o.ErrOut, results.addError(err, info)) return nil } info.Refresh(patched, true) printer, err := o.ToPrinter("edited") if err != nil { return err } return printer.PrintObj(info.Object, o.Out) }) return err } func (o *EditOptions) visitToCreate(createVisitor resource.Visitor) error { err := createVisitor.Visit(func(info *resource.Info, incomingErr error) error { obj, err := resource.NewHelper(info.Client, info.Mapping). WithFieldManager(o.FieldManager). WithFieldValidation(o.ValidationDirective). Create(info.Namespace, true, info.Object) if err != nil { return err } info.Refresh(obj, true) printer, err := o.ToPrinter("created") if err != nil { return err } return printer.PrintObj(info.Object, o.Out) }) return err } func (o *EditOptions) visitAnnotation(annotationVisitor resource.Visitor) error { // iterate through all items to apply annotations err := annotationVisitor.Visit(func(info *resource.Info, incomingErr error) error { // put configuration annotation in "updates" if o.ApplyAnnotation { if err := util.CreateOrUpdateAnnotation(true, info.Object, scheme.DefaultJSONEncoder()); err != nil { return err } } if err := o.Recorder.Record(info.Object); err != nil { klog.V(4).Infof("error recording current command: %v", err) } return nil }) return err } // EditMode can be either NormalEditMode, EditBeforeCreateMode or ApplyEditMode type EditMode string const ( // NormalEditMode is an edit mode NormalEditMode EditMode = "normal_mode" // EditBeforeCreateMode is an edit mode EditBeforeCreateMode EditMode = "edit_before_create_mode" // ApplyEditMode is an edit mode ApplyEditMode EditMode = "edit_last_applied_mode" ) // editReason preserves a message about the reason this file must be edited again type editReason struct { head string other []string } // editHeader includes a list of reasons the edit must be retried type editHeader struct { reasons []editReason } // writeTo outputs the current header information into a stream func (h *editHeader) writeTo(w io.Writer, editMode EditMode) error { if editMode == ApplyEditMode { fmt.Fprint(w, `# Please edit the 'last-applied-configuration' annotations below. # Lines beginning with a '#' will be ignored, and an empty file will abort the edit. # `) } else { fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # `) } for _, r := range h.reasons { if len(r.other) > 0 { fmt.Fprintf(w, "# %s:\n", hashOnLineBreak(r.head)) } else { fmt.Fprintf(w, "# %s\n", hashOnLineBreak(r.head)) } for _, o := range r.other { fmt.Fprintf(w, "# * %s\n", hashOnLineBreak(o)) } fmt.Fprintln(w, "#") } return nil } // editResults capture the result of an update type editResults struct { header editHeader retryable int notfound int edit []*resource.Info file string } func (r *editResults) addError(err error, info *resource.Info) string { resourceString := info.Mapping.Resource.Resource if len(info.Mapping.Resource.Group) > 0 { resourceString = resourceString + "." + info.Mapping.Resource.Group } switch { case apierrors.IsInvalid(err): r.edit = append(r.edit, info) reason := editReason{ head: fmt.Sprintf("%s %q was not valid", resourceString, info.Name), } if err, ok := err.(apierrors.APIStatus); ok { if details := err.Status().Details; details != nil { for _, cause := range details.Causes { reason.other = append(reason.other, fmt.Sprintf("%s: %s", cause.Field, cause.Message)) } } } r.header.reasons = append(r.header.reasons, reason) return fmt.Sprintf("error: %s %q is invalid", resourceString, info.Name) case apierrors.IsNotFound(err): r.notfound++ return fmt.Sprintf("error: %s %q could not be found on the server", resourceString, info.Name) default: r.retryable++ return fmt.Sprintf("error: %s %q could not be patched: %v", resourceString, info.Name, err) } } // preservedFile writes out a message about the provided file if it exists to the // provided output stream when an error happens. Used to notify the user where // their updates were preserved. func preservedFile(err error, path string, out io.Writer) error { if len(path) > 0 { if _, err := os.Stat(path); !os.IsNotExist(err) { fmt.Fprintf(out, "A copy of your changes has been stored to %q\n", path) } } return err } // hasLines returns true if any line in the provided stream is non empty - has non-whitespace // characters, or the first non-whitespace character is a '#' indicating a comment. Returns // any errors encountered reading the stream. func hasLines(r io.Reader) (bool, error) { // TODO: if any files we read have > 64KB lines, we'll need to switch to bytes.ReadLine // TODO: probably going to be secrets s := bufio.NewScanner(r) for s.Scan() { if line := strings.TrimSpace(s.Text()); len(line) > 0 && line[0] != '#' { return true, nil } } if err := s.Err(); err != nil && err != io.EOF { return false, err } return false, nil } // hashOnLineBreak returns a string built from the provided string by inserting any necessary '#' // characters after '\n' characters, indicating a comment. func hashOnLineBreak(s string) string { r := "" for i, ch := range s { j := i + 1 if j < len(s) && ch == '\n' && s[j] != '#' { r += "\n# " } else { r += string(ch) } } return r } // editorEnvs returns an ordered list of env vars to check for editor preferences. func editorEnvs() []string { return []string{ "KUBE_EDITOR", "EDITOR", } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editoptions_test.go000066400000000000000000000274271504711711200330150ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package editor import ( "reflect" "strings" "testing" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" 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" "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/resource" ) func TestHashOnLineBreak(t *testing.T) { tests := []struct { original string expected string }{ { original: "", expected: "", }, { original: "\n", expected: "\n", }, { original: "a\na\na\n", expected: "a\n# a\n# a\n", }, { original: "a\n\n\na\n\n", expected: "a\n# \n# \n# a\n# \n", }, } for _, test := range tests { r := hashOnLineBreak(test.original) if r != test.expected { t.Errorf("expected: %s, saw: %s", test.expected, r) } } } func TestManagedFieldsExtractAndRestore(t *testing.T) { tests := map[string]struct { object runtime.Object managedFields map[types.UID][]metav1.ManagedFieldsEntry }{ "single object, empty managedFields": { object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("12345")}}, managedFields: map[types.UID][]metav1.ManagedFieldsEntry{ types.UID("12345"): nil, }, }, "multiple objects, empty managedFields": { object: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{}, }, Items: []unstructured.Unstructured{ { Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{ "uid": "12345", }, "spec": map[string]interface{}{}, "status": map[string]interface{}{}, }, }, { Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{ "uid": "98765", }, "spec": map[string]interface{}{}, "status": map[string]interface{}{}, }, }, }, }, managedFields: map[types.UID][]metav1.ManagedFieldsEntry{ types.UID("12345"): nil, types.UID("98765"): nil, }, }, "single object, all managedFields": { object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{ UID: types.UID("12345"), ManagedFields: []metav1.ManagedFieldsEntry{ { Manager: "test", Operation: metav1.ManagedFieldsOperationApply, }, }, }}, managedFields: map[types.UID][]metav1.ManagedFieldsEntry{ types.UID("12345"): { { Manager: "test", Operation: metav1.ManagedFieldsOperationApply, }, }, }, }, "multiple objects, all managedFields": { object: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{}, }, Items: []unstructured.Unstructured{ { Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{ "uid": "12345", "managedFields": []interface{}{ map[string]interface{}{ "manager": "test", "operation": "Apply", }, }, }, "spec": map[string]interface{}{}, "status": map[string]interface{}{}, }, }, { Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{ "uid": "98765", "managedFields": []interface{}{ map[string]interface{}{ "manager": "test", "operation": "Update", }, }, }, "spec": map[string]interface{}{}, "status": map[string]interface{}{}, }, }, }, }, managedFields: map[types.UID][]metav1.ManagedFieldsEntry{ types.UID("12345"): { { Manager: "test", Operation: metav1.ManagedFieldsOperationApply, }, }, types.UID("98765"): { { Manager: "test", Operation: metav1.ManagedFieldsOperationUpdate, }, }, }, }, "multiple objects, some managedFields": { object: &unstructured.UnstructuredList{ Object: map[string]interface{}{ "kind": "List", "apiVersion": "v1", "metadata": map[string]interface{}{}, }, Items: []unstructured.Unstructured{ { Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{ "uid": "12345", "managedFields": []interface{}{ map[string]interface{}{ "manager": "test", "operation": "Apply", }, }, }, "spec": map[string]interface{}{}, "status": map[string]interface{}{}, }, }, { Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{ "uid": "98765", }, "spec": map[string]interface{}{}, "status": map[string]interface{}{}, }, }, }, }, managedFields: map[types.UID][]metav1.ManagedFieldsEntry{ types.UID("12345"): { { Manager: "test", Operation: metav1.ManagedFieldsOperationApply, }, }, types.UID("98765"): nil, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { // operate on a copy, so we can compare the original and the modified object objCopy := test.object.DeepCopyObject() var infos []*resource.Info o := NewEditOptions(NormalEditMode, genericiooptions.NewTestIOStreamsDiscard()) err := o.extractManagedFields(objCopy) if err != nil { t.Errorf("unexpected extraction error %v", err) } if meta.IsListType(objCopy) { infos = []*resource.Info{} meta.EachListItem(objCopy, func(obj runtime.Object) error { metaObjs, _ := meta.Accessor(obj) if metaObjs.GetManagedFields() != nil { t.Errorf("unexpected managedFileds after extraction") } infos = append(infos, &resource.Info{Object: obj}) return nil }) } else { metaObjs, _ := meta.Accessor(objCopy) if metaObjs.GetManagedFields() != nil { t.Errorf("unexpected managedFileds after extraction") } infos = []*resource.Info{{Object: objCopy}} } err = o.restoreManagedFields(infos) if err != nil { t.Errorf("unexpected restore error %v", err) } if !reflect.DeepEqual(test.object, objCopy) { t.Errorf("mismatched object after extract and restore managedFields: %#v", objCopy) } if test.managedFields != nil && !reflect.DeepEqual(test.managedFields, o.managedFields) { t.Errorf("mismatched managedFields %#v vs %#v", test.managedFields, o.managedFields) } }) } } type testVisitor struct { updatedInfos []*resource.Info } func (tv *testVisitor) Visit(f resource.VisitorFunc) error { var err error for _, ui := range tv.updatedInfos { err = f(ui, err) } return err } var unregMapping = &meta.RESTMapping{ Resource: schema.GroupVersionResource{ Group: "a", Version: "b", Resource: "c", }, GroupVersionKind: schema.GroupVersionKind{ Group: "a", Version: "b", Kind: "d", }, } func TestEditOptions_visitToPatch(t *testing.T) { expectedErr := func(err error) bool { return err != nil && strings.Contains(err.Error(), "At least one of apiVersion, kind and name was changed") } type args struct { originalInfos []*resource.Info patchVisitor resource.Visitor results *editResults } tests := []struct { name string args args checkErr func(err error) bool }{ { name: "name-diff", args: args{ originalInfos: []*resource.Info{ { Namespace: "ns", Name: "before", Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Thingy", "metadata": map[string]interface{}{ "uid": "12345", "namespace": "ns", "name": "before", }, "spec": map[string]interface{}{}, }, }, Mapping: unregMapping, }, }, patchVisitor: &testVisitor{ updatedInfos: []*resource.Info{ { Namespace: "ns", Name: "after", Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Thingy", "metadata": map[string]interface{}{ "uid": "12345", "namespace": "ns", "name": "after", }, "spec": map[string]interface{}{}, }, }, Mapping: unregMapping, }, }, }, results: &editResults{}, }, checkErr: expectedErr, }, { name: "kind-diff", args: args{ originalInfos: []*resource.Info{ { Namespace: "ns", Name: "myname", Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Thingy", "metadata": map[string]interface{}{ "uid": "12345", "namespace": "ns", "name": "myname", }, "spec": map[string]interface{}{}, }, }, Mapping: unregMapping, }, }, patchVisitor: &testVisitor{ updatedInfos: []*resource.Info{ { Namespace: "ns", Name: "myname", Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "OtherThingy", "metadata": map[string]interface{}{ "uid": "12345", "namespace": "ns", "name": "myname", }, "spec": map[string]interface{}{}, }, }, Mapping: unregMapping, }, }, }, results: &editResults{}, }, checkErr: expectedErr, }, { name: "apiver-diff", args: args{ originalInfos: []*resource.Info{ { Namespace: "ns", Name: "myname", Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Thingy", "metadata": map[string]interface{}{ "uid": "12345", "namespace": "ns", "name": "myname", }, "spec": map[string]interface{}{}, }, }, Mapping: unregMapping, }, }, patchVisitor: &testVisitor{ updatedInfos: []*resource.Info{ { Namespace: "ns", Name: "myname", Object: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1alpha1", "kind": "Thingy", "metadata": map[string]interface{}{ "uid": "12345", "namespace": "ns", "name": "myname", }, "spec": map[string]interface{}{}, }, }, Mapping: unregMapping, }, }, }, results: &editResults{}, }, checkErr: expectedErr, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := &EditOptions{} if err := o.visitToPatch(tt.args.originalInfos, tt.args.patchVisitor, tt.args.results); !tt.checkErr(err) { t.Errorf("EditOptions.visitToPatch() %s error = %v", tt.name, err) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editor.go000066400000000000000000000064141504711711200306740ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package editor import ( "io" "os" "runtime" "strings" ) const ( // sorry, blame Git // TODO: on Windows rely on 'start' to launch the editor associated // with the given file type. If we can't because of the need of // blocking, use a script with 'ftype' and 'assoc' to detect it. defaultEditor = "vi" defaultShell = "/bin/bash" windowsEditor = "notepad" windowsShell = "cmd" ) // Editor holds the command-line args to fire up the editor type Editor struct { Args []string Shell bool } // NewDefaultEditor creates a struct Editor that uses the OS environment to // locate the editor program, looking at EDITOR environment variable to find // the proper command line. If the provided editor has no spaces, or no quotes, // it is treated as a bare command to be loaded. Otherwise, the string will // be passed to the user's shell for execution. func NewDefaultEditor(envs []string) Editor { args, shell := defaultEnvEditor(envs) return Editor{ Args: args, Shell: shell, } } func defaultEnvShell() []string { shell := os.Getenv("SHELL") if len(shell) == 0 { shell = platformize(defaultShell, windowsShell) } flag := "-c" if shell == windowsShell { flag = "/C" } return []string{shell, flag} } func defaultEnvEditor(envs []string) ([]string, bool) { var editor string for _, env := range envs { if len(env) > 0 { editor = os.Getenv(env) } if len(editor) > 0 { break } } if len(editor) == 0 { editor = platformize(defaultEditor, windowsEditor) } if !strings.Contains(editor, " ") { return []string{editor}, false } if !strings.ContainsAny(editor, "\"'\\") { return strings.Split(editor, " "), false } // rather than parse the shell arguments ourselves, punt to the shell shell := defaultEnvShell() return append(shell, editor), true } // LaunchTempFile reads the provided stream into a temporary file in the given directory // and file prefix, and then invokes Launch with the path of that file. It will return // the contents of the file after launch, any errors that occur, and the path of the // temporary file so the caller can clean it up as needed. func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) ([]byte, string, error) { f, err := os.CreateTemp("", prefix+"*"+suffix) if err != nil { return nil, "", err } defer f.Close() path := f.Name() if _, err := io.Copy(f, r); err != nil { os.Remove(path) return nil, path, err } // This file descriptor needs to close so the next process (Launch) can claim it. f.Close() if err := e.Launch(path); err != nil { return nil, path, err } bytes, err := os.ReadFile(path) return bytes, path, err } func platformize(linux, windows string) string { if runtime.GOOS == "windows" { return windows } return linux } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editor_test.go000066400000000000000000000040751504711711200317340ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package editor import ( "bytes" "os" "reflect" "strings" "testing" ) func TestArgs(t *testing.T) { if e, a := []string{"/bin/bash", "-c \"test\""}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } if e, a := []string{"/bin/bash", "-c", "test"}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: false}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } if e, a := []string{"/bin/bash", "-i -c \"test\""}, (Editor{Args: []string{"/bin/bash", "-i -c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } if e, a := []string{"/test", "test"}, (Editor{Args: []string{"/test"}}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } } func TestEditor(t *testing.T) { edit := Editor{Args: []string{"cat"}} testStr := "test something\n" contents, path, err := edit.LaunchTempFile("", "someprefix", bytes.NewBufferString(testStr)) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := os.Stat(path); err != nil { t.Fatalf("no temp file: %s", path) } defer os.Remove(path) if disk, err := os.ReadFile(path); err != nil || !bytes.Equal(contents, disk) { t.Errorf("unexpected file on disk: %v %s", err, string(disk)) } if !bytes.Equal(contents, []byte(testStr)) { t.Errorf("unexpected contents: %s", string(contents)) } if !strings.Contains(path, "someprefix") { t.Errorf("path not expected: %s", path) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/launcher_others.go000066400000000000000000000034571504711711200325770ustar00rootroot00000000000000//go:build !windows // +build !windows /* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package editor import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/util/term" ) func (e Editor) args(path string) []string { args := make([]string, len(e.Args)) copy(args, e.Args) if e.Shell { last := args[len(args)-1] args[len(args)-1] = fmt.Sprintf("%s %q", last, path) } else { args = append(args, path) } return args } // Launch opens the described or returns an error. The TTY will be protected, and // SIGQUIT, SIGTERM, and SIGINT will all be trapped. func (e Editor) Launch(path string) error { if len(e.Args) == 0 { return fmt.Errorf("no editor defined, can't open %s", path) } abs, err := filepath.Abs(path) if err != nil { return err } args := e.args(abs) cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin klog.V(5).Infof("Opening file with editor %v", args) if err := (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil { if errors.Is(err, exec.ErrNotFound) { return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " ")) } return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " ")) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/launcher_windows.go000066400000000000000000000050771504711711200327650ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package editor import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "syscall" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/util/term" ) // Enclose argument in double-quotes. Double each double-quote character as // an escape sequence. func cmdQuoteArg(arg string) string { var result strings.Builder result.WriteString(`"`) result.WriteString(strings.ReplaceAll(arg, `"`, `""`)) result.WriteString(`"`) return result.String() } func (e Editor) args(path string) []string { args := make([]string, len(e.Args)) copy(args, e.Args) if e.Shell { last := args[len(args)-1] if args[0] == windowsShell { // Use double-quotation around whole command line string // See https://stackoverflow.com/a/6378038 args[len(args)-1] = fmt.Sprintf(`"%s %s"`, last, cmdQuoteArg(path)) } else { args[len(args)-1] = fmt.Sprintf("%s %q", last, path) } } else { args = append(args, path) } return args } // Launch opens the described or returns an error. The TTY will be protected, and // SIGQUIT, SIGTERM, and SIGINT will all be trapped. func (e Editor) Launch(path string) error { if len(e.Args) == 0 { return fmt.Errorf("no editor defined, can't open %s", path) } abs, err := filepath.Abs(path) if err != nil { return err } args := e.args(abs) var cmd *exec.Cmd if e.Shell && args[0] == windowsShell { // Pass all arguments to cmd.exe as one string // See https://pkg.go.dev/os/exec#Command cmd = exec.Command(args[0]) cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.CmdLine = strings.Join(args, " ") } else { cmd = exec.Command(args[0], args[1:]...) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin klog.V(5).Infof("Opening file with editor %v", args) if err := (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil { if errors.Is(err, exec.ErrNotFound) { return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " ")) } return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " ")) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file.go000066400000000000000000000052011504711711200277000ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "bufio" "bytes" "fmt" "os" "strings" "unicode" "unicode/utf8" "k8s.io/apimachinery/pkg/util/validation" ) var utf8bom = []byte{0xEF, 0xBB, 0xBF} // processEnvFileLine returns a blank key if the line is empty or a comment. // The value will be retrieved from the environment if necessary. func processEnvFileLine(line []byte, filePath string, currentLine int) (key, value string, err error) { if !utf8.Valid(line) { return ``, ``, fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v", filePath, currentLine+1, line) } // We trim UTF8 BOM from the first line of the file but no others if currentLine == 0 { line = bytes.TrimPrefix(line, utf8bom) } // trim the line from all leading whitespace first line = bytes.TrimLeftFunc(line, unicode.IsSpace) // If the line is empty or a comment, we return a blank key/value pair. if len(line) == 0 || line[0] == '#' { return ``, ``, nil } data := strings.SplitN(string(line), "=", 2) key = data[0] if errs := validation.IsEnvVarName(key); len(errs) != 0 { return ``, ``, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";")) } if len(data) == 2 { value = data[1] } else { // No value (no `=` in the line) is a signal to obtain the value // from the environment. value = os.Getenv(key) } return } // AddFromEnvFile processes an env file allows a generic addTo to handle the // collection of key value pairs or returns an error. func AddFromEnvFile(filePath string, addTo func(key, value string) error) error { f, err := os.Open(filePath) if err != nil { return err } defer f.Close() scanner := bufio.NewScanner(f) currentLine := 0 for scanner.Scan() { // Process the current line, retrieving a key/value pair if // possible. scannedBytes := scanner.Bytes() key, value, err := processEnvFileLine(scannedBytes, filePath, currentLine) if err != nil { return err } currentLine++ if len(key) == 0 { // no key means line was empty or a comment continue } if err = addTo(key, value); err != nil { return err } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file_test.go000066400000000000000000000053261504711711200307470ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "strings" "testing" ) // Test the cases of proccessEnvFileLine that can be run without touching the // environment. func Test_processEnvFileLine(t *testing.T) { testCases := []struct { name string line []byte currentLine int expectedKey string expectedValue string expectedErr string }{ {"the utf8bom is trimmed on the first line", append(utf8bom, 'a', '=', 'c'), 0, "a", "c", ""}, {"the utf8bom is NOT trimmed on the second line", append(utf8bom, 'a', '=', 'c'), 1, "", "", "not a valid key name"}, {"no key is returned on a comment line", []byte{' ', '#', 'c'}, 0, "", "", ""}, {"no key is returned on a blank line", []byte{' ', ' ', '\t'}, 0, "", "", ""}, {"key is returned even with no value", []byte{' ', 'x', '='}, 0, "x", "", ""}, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { key, value, err := processEnvFileLine(tt.line, `filename`, tt.currentLine) t.Logf("Testing that %s.", tt.name) if tt.expectedKey != key { t.Errorf("\texpected key %q, received %q", tt.expectedKey, key) } if tt.expectedValue != value { t.Errorf("\texpected value %q, received %q", tt.expectedValue, value) } if len(tt.expectedErr) == 0 { if err != nil { t.Errorf("\tunexpected err %v", err) } } else { if !strings.Contains(err.Error(), tt.expectedErr) { t.Errorf("\terr %v doesn't match expected %q", err, tt.expectedErr) } } }) } } // proccessEnvFileLine needs to fetch the value from the environment if no // equals sign is provided. // For example: // // my_key1=alpha // my_key2=beta // my_key3 // // In this file, my_key3 must be fetched from the environment. // Test this capability. func Test_processEnvFileLine_readEnvironment(t *testing.T) { const realKey = "k8s_test_env_file_key" const realValue = `my_value` t.Setenv(realKey, `my_value`) key, value, err := processEnvFileLine([]byte(realKey), `filename`, 3) if err != nil { t.Fatal(err) } if key != realKey { t.Errorf(`expected key %q, received %q`, realKey, key) } if value != realValue { t.Errorf(`expected value %q, received %q`, realValue, value) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/factory.go000066400000000000000000000060761504711711200275730ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" openapiclient "k8s.io/client-go/openapi" restclient "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/validation" ) // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. // The rings are here for a reason. In order for composers to be able to provide alternative factory implementations // they need to provide low level pieces of *certain* functions so that when the factory calls back into itself // it uses the custom version of the function. Rather than try to enumerate everything that someone would want to override // we split the factory into rings, where each ring can depend on methods in an earlier ring, but cannot depend // upon peer methods in its own ring. // TODO: make the functions interfaces // TODO: pass the various interfaces on the factory directly into the command constructors (so the // commands are decoupled from the factory). type Factory interface { genericclioptions.RESTClientGetter // DynamicClient returns a dynamic client ready for use DynamicClient() (dynamic.Interface, error) // KubernetesClientSet gives you back an external clientset KubernetesClientSet() (*kubernetes.Clientset, error) // Returns a RESTClient for accessing Kubernetes resources or an error. RESTClient() (*restclient.RESTClient, error) // NewBuilder returns an object that assists in loading objects from both disk and the server // and which implements the common patterns for CLI interactions with generic resources. NewBuilder() *resource.Builder // Returns a RESTClient for working with the specified RESTMapping or an error. This is intended // for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer. ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) // Returns a RESTClient for working with Unstructured objects. UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) // Returns a schema that can validate objects stored on disk. Validator(validationDirective string) (validation.Schema, error) // Used for retrieving openapi v2 resources. openapi.OpenAPIResourcesGetter // OpenAPIV3Schema returns a client for fetching parsed schemas for // any group version OpenAPIV3Client() (openapiclient.Client, error) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go000066400000000000000000000144351504711711200324500ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. 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. */ // this file contains factories with no other dependencies package util import ( "errors" "sync" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" openapiclient "k8s.io/client-go/openapi" "k8s.io/client-go/openapi/cached" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/validation" ) type factoryImpl struct { clientGetter genericclioptions.RESTClientGetter // Caches OpenAPI document and parsed resources openAPIParser *openapi.CachedOpenAPIParser oapi *openapi.CachedOpenAPIGetter parser sync.Once getter sync.Once } func NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory { if clientGetter == nil { panic("attempt to instantiate client_access_factory with nil clientGetter") } f := &factoryImpl{ clientGetter: clientGetter, } return f } func (f *factoryImpl) ToRESTConfig() (*restclient.Config, error) { return f.clientGetter.ToRESTConfig() } func (f *factoryImpl) ToRESTMapper() (meta.RESTMapper, error) { return f.clientGetter.ToRESTMapper() } func (f *factoryImpl) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { return f.clientGetter.ToDiscoveryClient() } func (f *factoryImpl) ToRawKubeConfigLoader() clientcmd.ClientConfig { return f.clientGetter.ToRawKubeConfigLoader() } func (f *factoryImpl) KubernetesClientSet() (*kubernetes.Clientset, error) { clientConfig, err := f.ToRESTConfig() if err != nil { return nil, err } return kubernetes.NewForConfig(clientConfig) } func (f *factoryImpl) DynamicClient() (dynamic.Interface, error) { clientConfig, err := f.ToRESTConfig() if err != nil { return nil, err } return dynamic.NewForConfig(clientConfig) } // NewBuilder returns a new resource builder for structured api objects. func (f *factoryImpl) NewBuilder() *resource.Builder { return resource.NewBuilder(f.clientGetter) } func (f *factoryImpl) RESTClient() (*restclient.RESTClient, error) { clientConfig, err := f.ToRESTConfig() if err != nil { return nil, err } setKubernetesDefaults(clientConfig) return restclient.RESTClientFor(clientConfig) } func (f *factoryImpl) ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) { cfg, err := f.clientGetter.ToRESTConfig() if err != nil { return nil, err } if err := setKubernetesDefaults(cfg); err != nil { return nil, err } gvk := mapping.GroupVersionKind switch gvk.Group { case corev1.GroupName: cfg.APIPath = "/api" default: cfg.APIPath = "/apis" } gv := gvk.GroupVersion() cfg.GroupVersion = &gv return restclient.RESTClientFor(cfg) } func (f *factoryImpl) UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) { cfg, err := f.clientGetter.ToRESTConfig() if err != nil { return nil, err } if err := restclient.SetKubernetesDefaults(cfg); err != nil { return nil, err } cfg.APIPath = "/apis" if mapping.GroupVersionKind.Group == corev1.GroupName { cfg.APIPath = "/api" } gv := mapping.GroupVersionKind.GroupVersion() cfg.ContentConfig = resource.UnstructuredPlusDefaultContentConfig() cfg.GroupVersion = &gv return restclient.RESTClientFor(cfg) } func (f *factoryImpl) Validator(validationDirective string) (validation.Schema, error) { // client-side schema validation is only performed // when the validationDirective is strict. // If the directive is warn, we rely on the ParamVerifyingSchema // to ignore the client-side validation and provide a warning // to the user that attempting warn validation when SS validation // is unsupported is inert. if validationDirective == metav1.FieldValidationIgnore { return validation.NullSchema{}, nil } schema := validation.ConjunctiveSchema{ validation.NewSchemaValidation(f), validation.NoDoubleKeySchema{}, } dynamicClient, err := f.DynamicClient() if err != nil { return nil, err } // Create the FieldValidationVerifier for use in the ParamVerifyingSchema. discoveryClient, err := f.ToDiscoveryClient() if err != nil { return nil, err } // Memory-cache the OpenAPI V3 responses. The disk cache behavior is determined by // the discovery client. oapiV3Client := cached.NewClient(discoveryClient.OpenAPIV3()) queryParam := resource.QueryParamFieldValidation primary := resource.NewQueryParamVerifierV3(dynamicClient, oapiV3Client, queryParam) secondary := resource.NewQueryParamVerifier(dynamicClient, f.openAPIGetter(), queryParam) fallback := resource.NewFallbackQueryParamVerifier(primary, secondary) return validation.NewParamVerifyingSchema(schema, fallback, string(validationDirective)), nil } // OpenAPISchema returns metadata and structural information about // Kubernetes object definitions. func (f *factoryImpl) OpenAPISchema() (openapi.Resources, error) { openAPIGetter := f.openAPIGetter() if openAPIGetter == nil { return nil, errors.New("no openapi getter") } // Lazily initialize the OpenAPIParser once f.parser.Do(func() { // Create the caching OpenAPIParser f.openAPIParser = openapi.NewOpenAPIParser(f.openAPIGetter()) }) // Delegate to the OpenAPIPArser return f.openAPIParser.Parse() } func (f *factoryImpl) openAPIGetter() discovery.OpenAPISchemaInterface { discovery, err := f.clientGetter.ToDiscoveryClient() if err != nil { return nil } f.getter.Do(func() { f.oapi = openapi.NewOpenAPIGetter(discovery) }) return f.oapi } func (f *factoryImpl) OpenAPIV3Client() (openapiclient.Client, error) { discovery, err := f.clientGetter.ToDiscoveryClient() if err != nil { return nil, err } return cached.NewClient(discovery.OpenAPIV3()), nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go000066400000000000000000000777721504711711200276010ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "bytes" "errors" "fmt" "io" "net/url" "os" "strconv" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/pflag" jsonpatch "gopkg.in/evanphx/json-patch.v4" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" "k8s.io/client-go/scale" "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" utilexec "k8s.io/utils/exec" ) const ( ApplyAnnotationsFlag = "save-config" DefaultErrorExitCode = 1 DefaultChunkSize = 500 ) type debugError interface { DebugError() (msg string, args []interface{}) } // AddSourceToErr adds handleResourcePrefix and source string to error message. // verb is the string like "creating", "deleting" etc. // source is the filename or URL to the template file(*.json or *.yaml), or stdin to use to handle the resource. func AddSourceToErr(verb string, source string, err error) error { if source != "" { if statusError, ok := err.(apierrors.APIStatus); ok { status := statusError.Status() status.Message = fmt.Sprintf("error when %s %q: %v", verb, source, status.Message) return &apierrors.StatusError{ErrStatus: status} } return fmt.Errorf("error when %s %q: %v", verb, source, err) } return err } var fatalErrHandler = fatal // BehaviorOnFatal allows you to override the default behavior when a fatal // error occurs, which is to call os.Exit(code). You can pass 'panic' as a function // here if you prefer the panic() over os.Exit(1). func BehaviorOnFatal(f func(string, int)) { fatalErrHandler = f } // DefaultBehaviorOnFatal allows you to undo any previous override. Useful in // tests. func DefaultBehaviorOnFatal() { fatalErrHandler = fatal } // fatal prints the message (if provided) and then exits. If V(99) or greater, // klog.Fatal is invoked for extended information. This is intended for maintainer // debugging and out of a reasonable range for users. func fatal(msg string, code int) { // nolint:logcheck // Not using the result of klog.V(99) inside the if // branch is okay, we just use it to determine how to terminate. if klog.V(99).Enabled() { klog.FatalDepth(2, msg) } if len(msg) > 0 { // add newline if needed if !strings.HasSuffix(msg, "\n") { msg += "\n" } fmt.Fprint(os.Stderr, msg) } os.Exit(code) } // ErrExit may be passed to CheckError to instruct it to output nothing but exit with // status code 1. var ErrExit = fmt.Errorf("exit") // CheckErr prints a user friendly error to STDERR and exits with a non-zero // exit code. Unrecognized errors will be printed with an "error: " prefix. // // This method is generic to the command in use and may be used by non-Kubectl // commands. func CheckErr(err error) { checkErr(err, fatalErrHandler) } // CheckDiffErr prints a user friendly error to STDERR and exits with a // non-zero and non-one exit code. Unrecognized errors will be printed // with an "error: " prefix. // // This method is meant specifically for `kubectl diff` and may be used // by other commands. func CheckDiffErr(err error) { checkErr(err, func(msg string, code int) { fatalErrHandler(msg, code+1) }) } // isInvalidReasonStatusError returns true if this is an API Status error with reason=Invalid. // This is distinct from generic 422 errors we want to fall back to generic error handling. func isInvalidReasonStatusError(err error) bool { if !apierrors.IsInvalid(err) { return false } statusError, isStatusError := err.(*apierrors.StatusError) if !isStatusError { return false } status := statusError.Status() return status.Reason == metav1.StatusReasonInvalid } // checkErr formats a given error as a string and calls the passed handleErr // func with that string and an kubectl exit code. func checkErr(err error, handleErr func(string, int)) { // unwrap aggregates of 1 if agg, ok := err.(utilerrors.Aggregate); ok && len(agg.Errors()) == 1 { err = agg.Errors()[0] } if err == nil { return } switch { case err == ErrExit: handleErr("", DefaultErrorExitCode) case isInvalidReasonStatusError(err): status := err.(*apierrors.StatusError).Status() details := status.Details s := "The request is invalid" if details == nil { // if we have no other details, include the message from the server if present if len(status.Message) > 0 { s += ": " + status.Message } handleErr(s, DefaultErrorExitCode) return } if len(details.Kind) != 0 || len(details.Name) != 0 { s = fmt.Sprintf("The %s %q is invalid", details.Kind, details.Name) } else if len(status.Message) > 0 && len(details.Causes) == 0 { // only append the message if we have no kind/name details and no causes, // since default invalid error constructors duplicate that information in the message s += ": " + status.Message } if len(details.Causes) > 0 { errs := statusCausesToAggrError(details.Causes) handleErr(MultilineError(s+": ", errs), DefaultErrorExitCode) } else { handleErr(s, DefaultErrorExitCode) } case clientcmd.IsConfigurationInvalid(err): handleErr(MultilineError("Error in configuration: ", err), DefaultErrorExitCode) default: switch err := err.(type) { case *meta.NoResourceMatchError: switch { case len(err.PartialResource.Group) > 0 && len(err.PartialResource.Version) > 0: handleErr(fmt.Sprintf("the server doesn't have a resource type %q in group %q and version %q", err.PartialResource.Resource, err.PartialResource.Group, err.PartialResource.Version), DefaultErrorExitCode) case len(err.PartialResource.Group) > 0: handleErr(fmt.Sprintf("the server doesn't have a resource type %q in group %q", err.PartialResource.Resource, err.PartialResource.Group), DefaultErrorExitCode) case len(err.PartialResource.Version) > 0: handleErr(fmt.Sprintf("the server doesn't have a resource type %q in version %q", err.PartialResource.Resource, err.PartialResource.Version), DefaultErrorExitCode) default: handleErr(fmt.Sprintf("the server doesn't have a resource type %q", err.PartialResource.Resource), DefaultErrorExitCode) } case utilerrors.Aggregate: handleErr(MultipleErrors(``, err.Errors()), DefaultErrorExitCode) case utilexec.ExitError: handleErr(err.Error(), err.ExitStatus()) default: // for any other error type msg, ok := StandardErrorMessage(err) if !ok { msg = err.Error() if !strings.HasPrefix(msg, "error: ") { msg = fmt.Sprintf("error: %s", msg) } } handleErr(msg, DefaultErrorExitCode) } } } func statusCausesToAggrError(scs []metav1.StatusCause) utilerrors.Aggregate { errs := make([]error, 0, len(scs)) errorMsgs := sets.New[string]() for _, sc := range scs { // check for duplicate error messages and skip them msg := fmt.Sprintf("%s: %s", sc.Field, sc.Message) if errorMsgs.Has(msg) { continue } errorMsgs.Insert(msg) errs = append(errs, errors.New(msg)) } return utilerrors.NewAggregate(errs) } // StandardErrorMessage translates common errors into a human readable message, or returns // false if the error is not one of the recognized types. It may also log extended // information to klog. // // This method is generic to the command in use and may be used by non-Kubectl // commands. func StandardErrorMessage(err error) (string, bool) { if debugErr, ok := err.(debugError); ok { klog.V(4).Info(debugErr.DebugError()) } status, isStatus := err.(apierrors.APIStatus) switch { case isStatus: switch s := status.Status(); { case s.Reason == metav1.StatusReasonUnauthorized: return fmt.Sprintf("error: You must be logged in to the server (%s)", s.Message), true case len(s.Reason) > 0: return fmt.Sprintf("Error from server (%s): %s", s.Reason, err.Error()), true default: return fmt.Sprintf("Error from server: %s", err.Error()), true } case apierrors.IsUnexpectedObjectError(err): return fmt.Sprintf("Server returned an unexpected response: %s", err.Error()), true } switch t := err.(type) { case *url.Error: klog.V(4).Infof("Connection error: %s %s: %v", t.Op, t.URL, t.Err) switch { case strings.Contains(t.Err.Error(), "connection refused"): host := t.URL if server, err := url.Parse(t.URL); err == nil { host = server.Host } return fmt.Sprintf("The connection to the server %s was refused - did you specify the right host or port?", host), true } return fmt.Sprintf("Unable to connect to the server: %v", t.Err), true } return "", false } // MultilineError returns a string representing an error that splits sub errors into their own // lines. The returned string will end with a newline. func MultilineError(prefix string, err error) string { if agg, ok := err.(utilerrors.Aggregate); ok { errs := utilerrors.Flatten(agg).Errors() buf := &bytes.Buffer{} switch len(errs) { case 0: return fmt.Sprintf("%s%v\n", prefix, err) case 1: return fmt.Sprintf("%s%v\n", prefix, messageForError(errs[0])) default: fmt.Fprintln(buf, prefix) for _, err := range errs { fmt.Fprintf(buf, "* %v\n", messageForError(err)) } return buf.String() } } return fmt.Sprintf("%s%s\n", prefix, err) } // PrintErrorWithCauses prints an error's kind, name, and each of the error's causes in a new line. // The returned string will end with a newline. // Returns true if a case exists to handle the error type, or false otherwise. func PrintErrorWithCauses(err error, errOut io.Writer) bool { switch t := err.(type) { case *apierrors.StatusError: errorDetails := t.Status().Details if errorDetails != nil { fmt.Fprintf(errOut, "error: %s %q is invalid\n\n", errorDetails.Kind, errorDetails.Name) for _, cause := range errorDetails.Causes { fmt.Fprintf(errOut, "* %s: %s\n", cause.Field, cause.Message) } return true } } fmt.Fprintf(errOut, "error: %v\n", err) return false } // MultipleErrors returns a newline delimited string containing // the prefix and referenced errors in standard form. func MultipleErrors(prefix string, errs []error) string { buf := &bytes.Buffer{} for _, err := range errs { fmt.Fprintf(buf, "%s%v\n", prefix, messageForError(err)) } return buf.String() } // messageForError returns the string representing the error. func messageForError(err error) string { msg, ok := StandardErrorMessage(err) if !ok { msg = err.Error() } return msg } func UsageErrorf(cmd *cobra.Command, format string, args ...interface{}) error { msg := fmt.Sprintf(format, args...) return fmt.Errorf("%s\nSee '%s -h' for help and examples", msg, cmd.CommandPath()) } func IsFilenameSliceEmpty(filenames []string, directory string) bool { return len(filenames) == 0 && directory == "" } func GetFlagString(cmd *cobra.Command, flag string) string { s, err := cmd.Flags().GetString(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return s } // GetFlagStringSlice can be used to accept multiple argument with flag repetition (e.g. -f arg1,arg2 -f arg3 ...) func GetFlagStringSlice(cmd *cobra.Command, flag string) []string { s, err := cmd.Flags().GetStringSlice(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return s } // GetFlagStringArray can be used to accept multiple argument with flag repetition (e.g. -f arg1 -f arg2 ...) func GetFlagStringArray(cmd *cobra.Command, flag string) []string { s, err := cmd.Flags().GetStringArray(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return s } func GetFlagBool(cmd *cobra.Command, flag string) bool { b, err := cmd.Flags().GetBool(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return b } // Assumes the flag has a default value. func GetFlagInt(cmd *cobra.Command, flag string) int { i, err := cmd.Flags().GetInt(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return i } // Assumes the flag has a default value. func GetFlagInt32(cmd *cobra.Command, flag string) int32 { i, err := cmd.Flags().GetInt32(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return i } // Assumes the flag has a default value. func GetFlagInt64(cmd *cobra.Command, flag string) int64 { i, err := cmd.Flags().GetInt64(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return i } func GetFlagDuration(cmd *cobra.Command, flag string) time.Duration { d, err := cmd.Flags().GetDuration(flag) if err != nil { klog.Fatalf("error accessing flag %s for command %s: %v", flag, cmd.Name(), err) } return d } func GetPodRunningTimeoutFlag(cmd *cobra.Command) (time.Duration, error) { timeout := GetFlagDuration(cmd, "pod-running-timeout") if timeout <= 0 { return timeout, fmt.Errorf("--pod-running-timeout must be higher than zero") } return timeout, nil } type FeatureGate string const ( ApplySet FeatureGate = "KUBECTL_APPLYSET" CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW" OpenAPIV3Patch FeatureGate = "KUBECTL_OPENAPIV3_PATCH" RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS" PortForwardWebsockets FeatureGate = "KUBECTL_PORT_FORWARD_WEBSOCKETS" // DebugCustomProfile should be dropped in 1.34 DebugCustomProfile FeatureGate = "KUBECTL_DEBUG_CUSTOM_PROFILE" KubeRC FeatureGate = "KUBECTL_KUBERC" ) // IsEnabled returns true iff environment variable is set to true. // All other cases, it returns false. func (f FeatureGate) IsEnabled() bool { return strings.ToLower(os.Getenv(string(f))) == "true" } // IsDisabled returns true iff environment variable is set to false. // All other cases, it returns true. // This function is used for the cases where feature is enabled by default, // but it may be needed to provide a way to ability to disable this feature. func (f FeatureGate) IsDisabled() bool { return strings.ToLower(os.Getenv(string(f))) == "false" } func AddValidateFlags(cmd *cobra.Command) { cmd.Flags().String( "validate", "strict", `Must be one of: strict (or true), warn, ignore (or false). "true" or "strict" will use a schema to validate the input and fail the request if invalid. It will perform server side validation if ServerSideFieldValidation is enabled on the api-server, but will fall back to less reliable client-side validation if not. "warn" will warn about unknown or duplicate fields without blocking the request if server-side field validation is enabled on the API server, and behave as "ignore" otherwise. "false" or "ignore" will not perform any schema validation, silently dropping any unknown or duplicate fields.`, ) cmd.Flags().Lookup("validate").NoOptDefVal = "strict" } func AddFilenameOptionFlags(cmd *cobra.Command, options *resource.FilenameOptions, usage string) { AddJsonFilenameFlag(cmd.Flags(), &options.Filenames, "Filename, directory, or URL to files "+usage) AddKustomizeFlag(cmd.Flags(), &options.Kustomize) cmd.Flags().BoolVarP(&options.Recursive, "recursive", "R", options.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.") } func AddJsonFilenameFlag(flags *pflag.FlagSet, value *[]string, usage string) { flags.StringSliceVarP(value, "filename", "f", *value, usage) annotations := make([]string, 0, len(resource.FileExtensions)) for _, ext := range resource.FileExtensions { annotations = append(annotations, strings.TrimLeft(ext, ".")) } flags.SetAnnotation("filename", cobra.BashCompFilenameExt, annotations) } // AddKustomizeFlag adds kustomize flag to a command func AddKustomizeFlag(flags *pflag.FlagSet, value *string) { flags.StringVarP(value, "kustomize", "k", *value, "Process the kustomization directory. This flag can't be used together with -f or -R.") } // AddDryRunFlag adds dry-run flag to a command. Usually used by mutations. func AddDryRunFlag(cmd *cobra.Command) { cmd.Flags().String( "dry-run", "none", `Must be "none", "server", or "client". If client strategy, only print the object that would be sent, without sending it. If server strategy, submit server-side request without persisting the resource.`, ) cmd.Flags().Lookup("dry-run").NoOptDefVal = "unchanged" } func AddFieldManagerFlagVar(cmd *cobra.Command, p *string, defaultFieldManager string) { cmd.Flags().StringVar(p, "field-manager", defaultFieldManager, "Name of the manager used to track field ownership.") } func AddContainerVarFlags(cmd *cobra.Command, p *string, containerName string) { cmd.Flags().StringVarP(p, "container", "c", containerName, "Container name. If omitted, use the kubectl.kubernetes.io/default-container annotation for selecting the container to be attached or the first container in the pod will be chosen") } func AddServerSideApplyFlags(cmd *cobra.Command) { cmd.Flags().Bool("server-side", false, "If true, apply runs in the server instead of the client.") cmd.Flags().Bool("force-conflicts", false, "If true, server-side apply will force the changes against conflicts.") } func AddPodRunningTimeoutFlag(cmd *cobra.Command, defaultTimeout time.Duration) { cmd.Flags().Duration("pod-running-timeout", defaultTimeout, "The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running") } func AddApplyAnnotationFlags(cmd *cobra.Command) { cmd.Flags().Bool(ApplyAnnotationsFlag, false, "If true, the configuration of current object will be saved in its annotation. Otherwise, the annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.") } func AddApplyAnnotationVarFlags(cmd *cobra.Command, applyAnnotation *bool) { cmd.Flags().BoolVar(applyAnnotation, ApplyAnnotationsFlag, *applyAnnotation, "If true, the configuration of current object will be saved in its annotation. Otherwise, the annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.") } func AddChunkSizeFlag(cmd *cobra.Command, value *int64) { cmd.Flags().Int64Var(value, "chunk-size", *value, "Return large lists in chunks rather than all at once. Pass 0 to disable. This flag is beta and may change in the future.") } func AddLabelSelectorFlagVar(cmd *cobra.Command, p *string) { cmd.Flags().StringVarP(p, "selector", "l", *p, "Selector (label query) to filter on, supports '=', '==', '!=', 'in', 'notin'.(e.g. -l key1=value1,key2=value2,key3 in (value3)). Matching objects must satisfy all of the specified label constraints.") } func AddPruningFlags(cmd *cobra.Command, prune *bool, pruneAllowlist *[]string, all *bool, applySetRef *string) { // Flags associated with the original allowlist-based alpha cmd.Flags().StringArrayVar(pruneAllowlist, "prune-allowlist", *pruneAllowlist, "Overwrite the default allowlist with for --prune") cmd.Flags().BoolVar(all, "all", *all, "Select all resources in the namespace of the specified resource types.") // Flags associated with the new ApplySet-based alpha if ApplySet.IsEnabled() { cmd.Flags().StringVar(applySetRef, "applyset", *applySetRef, "[alpha] The name of the ApplySet that tracks which resources are being managed, for the purposes of determining what to prune. Live resources that are part of the ApplySet but have been removed from the provided configs will be deleted. Format: [RESOURCE][.GROUP]/NAME. A Secret will be used if no resource or group is specified.") cmd.Flags().BoolVar(prune, "prune", *prune, "Automatically delete previously applied resource objects that do not appear in the provided configs. For alpha1, use with either -l or --all. For alpha2, use with --applyset.") } else { // different docs for the shared --prune flag if only alpha1 is enabled cmd.Flags().BoolVar(prune, "prune", *prune, "Automatically delete resource objects, that do not appear in the configs and are created by either apply or create --save-config. Should be used with either -l or --all.") } } func AddSubresourceFlags(cmd *cobra.Command, subresource *string, usage string) { cmd.Flags().StringVar(subresource, "subresource", "", usage) CheckErr(cmd.RegisterFlagCompletionFunc("subresource", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { var commonSubresources = []string{"status", "scale", "resize"} return commonSubresources, cobra.ShellCompDirectiveNoFileComp })) } type ValidateOptions struct { ValidationDirective string } // Merge converts the passed in object to JSON, merges the fragment into it using an RFC7396 JSON Merge Patch, // and returns the resulting object // TODO: merge assumes JSON serialization, and does not properly abstract API retrieval func Merge(codec runtime.Codec, dst runtime.Object, fragment string) (runtime.Object, error) { // encode dst into versioned json and apply fragment directly too it target, err := runtime.Encode(codec, dst) if err != nil { return nil, err } patched, err := jsonpatch.MergePatch(target, []byte(fragment)) if err != nil { return nil, err } out, err := runtime.Decode(codec, patched) if err != nil { return nil, err } return out, nil } // StrategicMerge converts the passed in object to JSON, merges the fragment into it using a Strategic Merge Patch, // and returns the resulting object func StrategicMerge(codec runtime.Codec, dst runtime.Object, fragment string, dataStruct runtime.Object) (runtime.Object, error) { target, err := runtime.Encode(codec, dst) if err != nil { return nil, err } patched, err := strategicpatch.StrategicMergePatch(target, []byte(fragment), dataStruct) if err != nil { return nil, err } out, err := runtime.Decode(codec, patched) if err != nil { return nil, err } return out, nil } // JSONPatch converts the passed in object to JSON, performs an RFC6902 JSON Patch using operations specified in the // fragment, and returns the resulting object func JSONPatch(codec runtime.Codec, dst runtime.Object, fragment string) (runtime.Object, error) { target, err := runtime.Encode(codec, dst) if err != nil { return nil, err } patch, err := jsonpatch.DecodePatch([]byte(fragment)) if err != nil { return nil, err } patched, err := patch.Apply(target) if err != nil { return nil, err } out, err := runtime.Decode(codec, patched) if err != nil { return nil, err } return out, nil } // DumpReaderToFile writes all data from the given io.Reader to the specified file // (usually for temporary use). func DumpReaderToFile(reader io.Reader, filename string) error { f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } defer f.Close() buffer := make([]byte, 1024) for { count, err := reader.Read(buffer) if err == io.EOF { break } if err != nil { return err } _, err = f.Write(buffer[:count]) if err != nil { return err } } return nil } func GetServerSideApplyFlag(cmd *cobra.Command) bool { return GetFlagBool(cmd, "server-side") } func GetForceConflictsFlag(cmd *cobra.Command) bool { return GetFlagBool(cmd, "force-conflicts") } func GetFieldManagerFlag(cmd *cobra.Command) string { return GetFlagString(cmd, "field-manager") } func GetValidationDirective(cmd *cobra.Command) (string, error) { var validateFlag = GetFlagString(cmd, "validate") b, err := strconv.ParseBool(validateFlag) if err != nil { switch validateFlag { case "strict": return metav1.FieldValidationStrict, nil case "warn": return metav1.FieldValidationWarn, nil case "ignore": return metav1.FieldValidationIgnore, nil default: return metav1.FieldValidationStrict, fmt.Errorf(`invalid - validate option %q; must be one of: strict (or true), warn, ignore (or false)`, validateFlag) } } // The flag was a boolean if b { return metav1.FieldValidationStrict, nil } return metav1.FieldValidationIgnore, nil } type DryRunStrategy int const ( // DryRunNone indicates the client will make all mutating calls DryRunNone DryRunStrategy = iota // DryRunClient, or client-side dry-run, indicates the client will prevent // making mutating calls such as CREATE, PATCH, and DELETE DryRunClient // DryRunServer, or server-side dry-run, indicates the client will send // mutating calls to the APIServer with the dry-run parameter to prevent // persisting changes. // // Note that clients sending server-side dry-run calls should verify that // the APIServer and the resource supports server-side dry-run, and otherwise // clients should fail early. // // If a client sends a server-side dry-run call to an APIServer that doesn't // support server-side dry-run, then the APIServer will persist changes inadvertently. DryRunServer ) func GetDryRunStrategy(cmd *cobra.Command) (DryRunStrategy, error) { var dryRunFlag = GetFlagString(cmd, "dry-run") b, err := strconv.ParseBool(dryRunFlag) // The flag is not a boolean if err != nil { switch dryRunFlag { case cmd.Flag("dry-run").NoOptDefVal: klog.Warning(`--dry-run is deprecated and can be replaced with --dry-run=client.`) return DryRunClient, nil case "client": return DryRunClient, nil case "server": return DryRunServer, nil case "none": return DryRunNone, nil default: return DryRunNone, fmt.Errorf(`Invalid dry-run value (%v). Must be "none", "server", or "client".`, dryRunFlag) } } // The flag was a boolean if b { klog.Warningf(`--dry-run=%v is deprecated (boolean value) and can be replaced with --dry-run=%s.`, dryRunFlag, "client") return DryRunClient, nil } klog.Warningf(`--dry-run=%v is deprecated (boolean value) and can be replaced with --dry-run=%s.`, dryRunFlag, "none") return DryRunNone, nil } // PrintFlagsWithDryRunStrategy sets a success message at print time for the dry run strategy // // TODO(juanvallejo): This can be cleaned up even further by creating // a PrintFlags struct that binds the --dry-run flag, and whose // ToPrinter method returns a printer that understands how to print // this success message. func PrintFlagsWithDryRunStrategy(printFlags *genericclioptions.PrintFlags, dryRunStrategy DryRunStrategy) *genericclioptions.PrintFlags { switch dryRunStrategy { case DryRunClient: printFlags.Complete("%s (dry run)") case DryRunServer: printFlags.Complete("%s (server dry run)") } return printFlags } // GetResourcesAndPairs retrieves resources and "KEY=VALUE or KEY-" pair args from given args func GetResourcesAndPairs(args []string, pairType string) (resources []string, pairArgs []string, err error) { foundPair := false for _, s := range args { nonResource := (strings.Contains(s, "=") && s[0] != '=') || (strings.HasSuffix(s, "-") && s != "-") switch { case !foundPair && nonResource: foundPair = true fallthrough case foundPair && nonResource: pairArgs = append(pairArgs, s) case !foundPair && !nonResource: resources = append(resources, s) case foundPair && !nonResource: err = fmt.Errorf("all resources must be specified before %s changes: %s", pairType, s) return } } return } // ParsePairs retrieves new and remove pairs (if supportRemove is true) from "KEY=VALUE or KEY-" pair args func ParsePairs(pairArgs []string, pairType string, supportRemove bool) (newPairs map[string]string, removePairs []string, err error) { newPairs = map[string]string{} if supportRemove { removePairs = []string{} } var invalidBuf bytes.Buffer var invalidBufNonEmpty bool for _, pairArg := range pairArgs { if strings.Contains(pairArg, "=") && pairArg[0] != '=' { parts := strings.SplitN(pairArg, "=", 2) if len(parts) != 2 { if invalidBufNonEmpty { invalidBuf.WriteString(", ") } invalidBuf.WriteString(pairArg) invalidBufNonEmpty = true } else { newPairs[parts[0]] = parts[1] } } else if supportRemove && strings.HasSuffix(pairArg, "-") && pairArg != "-" { removePairs = append(removePairs, pairArg[:len(pairArg)-1]) } else { if invalidBufNonEmpty { invalidBuf.WriteString(", ") } invalidBuf.WriteString(pairArg) invalidBufNonEmpty = true } } if invalidBufNonEmpty { err = fmt.Errorf("invalid %s format: %s", pairType, invalidBuf.String()) return } return } // IsSiblingCommandExists receives a pointer to a cobra command and a target string. // Returns true if the target string is found in the list of sibling commands. func IsSiblingCommandExists(cmd *cobra.Command, targetCmdName string) bool { for _, c := range cmd.Parent().Commands() { if c.Name() == targetCmdName { return true } } return false } // DefaultSubCommandRun prints a command's help string to the specified output if no // arguments (sub-commands) are provided, or a usage error otherwise. func DefaultSubCommandRun(out io.Writer) func(c *cobra.Command, args []string) { return func(c *cobra.Command, args []string) { c.SetOut(out) c.SetErr(out) RequireNoArguments(c, args) c.Help() CheckErr(ErrExit) } } // RequireNoArguments exits with a usage error if extra arguments are provided. func RequireNoArguments(c *cobra.Command, args []string) { if len(args) > 0 { CheckErr(UsageErrorf(c, "unknown command %q", strings.Join(args, " "))) } } // StripComments will transform a YAML file into JSON, thus dropping any comments // in it. Note that if the given file has a syntax error, the transformation will // fail and we will manually drop all comments from the file. func StripComments(file []byte) []byte { stripped := file stripped, err := yaml.ToJSON(stripped) if err != nil { stripped = ManualStrip(file) } return stripped } // ManualStrip is used for dropping comments from a YAML file func ManualStrip(file []byte) []byte { stripped := []byte{} lines := bytes.Split(file, []byte("\n")) for i, line := range lines { trimline := bytes.TrimSpace(line) if bytes.HasPrefix(trimline, []byte("#")) && !bytes.HasPrefix(trimline, []byte("#!")) { continue } stripped = append(stripped, line...) if i < len(lines)-1 { stripped = append(stripped, '\n') } } return stripped } // ScaleClientFunc provides a ScalesGetter type ScaleClientFunc func(genericclioptions.RESTClientGetter) (scale.ScalesGetter, error) // ScaleClientFn gives a way to easily override the function for unit testing if needed. var ScaleClientFn ScaleClientFunc = scaleClient // scaleClient gives you back scale getter func scaleClient(restClientGetter genericclioptions.RESTClientGetter) (scale.ScalesGetter, error) { discoveryClient, err := restClientGetter.ToDiscoveryClient() if err != nil { return nil, err } clientConfig, err := restClientGetter.ToRESTConfig() if err != nil { return nil, err } setKubernetesDefaults(clientConfig) restClient, err := rest.RESTClientFor(clientConfig) if err != nil { return nil, err } resolver := scale.NewDiscoveryScaleKindResolver(discoveryClient) mapper, err := restClientGetter.ToRESTMapper() if err != nil { return nil, err } return scale.New(restClient, mapper, dynamic.LegacyAPIPathResolverFunc, resolver), nil } func Warning(cmdErr io.Writer, newGeneratorName, oldGeneratorName string) { fmt.Fprintf(cmdErr, "WARNING: New generator %q specified, "+ "but it isn't available. "+ "Falling back to %q.\n", newGeneratorName, oldGeneratorName, ) } // Difference removes any elements of subArray from fullArray and returns the result func Difference(fullArray []string, subArray []string) []string { exclude := make(map[string]bool, len(subArray)) for _, elem := range subArray { exclude[elem] = true } var result []string for _, elem := range fullArray { if _, found := exclude[elem]; !found { result = append(result, elem) } } return result } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go000066400000000000000000000404201504711711200306140ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( goerrors "errors" "fmt" "net/http" "os" "strings" "syscall" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubectl/pkg/scheme" "k8s.io/utils/exec" ) func TestMerge(t *testing.T) { tests := []struct { obj runtime.Object fragment string expected runtime.Object expectErr bool }{ { obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, }, fragment: fmt.Sprintf(`{ "apiVersion": "%s" }`, "v1"), expected: &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Spec: corev1.PodSpec{}, }, }, { obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, }, fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "volumes": [ {"name": "v1"}, {"name": "v2"} ] } }`, "v1"), expected: &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "v1", }, { Name: "v2", }, }, }, }, }, { obj: &corev1.Pod{}, fragment: "invalid json", expected: &corev1.Pod{}, expectErr: true, }, { obj: &corev1.Service{}, fragment: `{ "apiVersion": "badVersion" }`, expectErr: true, }, { obj: &corev1.Service{ Spec: corev1.ServiceSpec{}, }, fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "ports": [ { "port": 0 } ] } }`, "v1"), expected: &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, Spec: corev1.ServiceSpec{ Ports: []corev1.ServicePort{ { Port: 0, }, }, }, }, }, { obj: &corev1.Service{ Spec: corev1.ServiceSpec{ Selector: map[string]string{ "version": "v1", }, }, }, fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "selector": { "version": "v2" } } }`, "v1"), expected: &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "version": "v2", }, }, }, }, } codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) for i, test := range tests { out, err := Merge(codec, test.obj, test.fragment) if !test.expectErr { if err != nil { t.Errorf("testcase[%d], unexpected error: %v", i, err) } else if !apiequality.Semantic.DeepEqual(test.expected, out) { t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out)) } } if test.expectErr && err == nil { t.Errorf("testcase[%d], unexpected non-error", i) } } } func TestStrategicMerge(t *testing.T) { tests := []struct { obj runtime.Object dataStruct runtime.Object fragment string expected runtime.Object expectErr bool }{ { obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "c1", Image: "red-image", }, { Name: "c2", Image: "blue-image", }, }, }, }, dataStruct: &corev1.Pod{}, fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "containers": [ { "name": "c1", "image": "green-image" } ] } }`, schema.GroupVersion{Group: "", Version: "v1"}.String()), expected: &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "c1", Image: "green-image", }, { Name: "c2", Image: "blue-image", }, }, }, }, }, { obj: &corev1.Pod{}, dataStruct: &corev1.Pod{}, fragment: "invalid json", expected: &corev1.Pod{}, expectErr: true, }, { obj: &corev1.Service{}, dataStruct: &corev1.Pod{}, fragment: `{ "apiVersion": "badVersion" }`, expectErr: true, }, } codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) for i, test := range tests { out, err := StrategicMerge(codec, test.obj, test.fragment, test.dataStruct) if !test.expectErr { if err != nil { t.Errorf("testcase[%d], unexpected error: %v", i, err) } else if !apiequality.Semantic.DeepEqual(test.expected, out) { t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out)) } } if test.expectErr && err == nil { t.Errorf("testcase[%d], unexpected non-error", i) } } } func TestJSONPatch(t *testing.T) { tests := []struct { obj runtime.Object fragment string expected runtime.Object expectErr bool }{ { obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Labels: map[string]string{ "run": "test", }, }, }, fragment: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`, expected: &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", Labels: map[string]string{ "run": "test", "foo": "bar", }, }, Spec: corev1.PodSpec{}, }, }, { obj: &corev1.Pod{}, fragment: "invalid json", expected: &corev1.Pod{}, expectErr: true, }, { obj: &corev1.Pod{}, fragment: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`, expectErr: true, }, { obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Finalizers: []string{"foo", "bar", "test"}, }, }, fragment: `[ {"op": "replace", "path": "/metadata/finalizers/-1", "value": "baz"} ]`, expected: &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", Finalizers: []string{"foo", "bar", "baz"}, }, Spec: corev1.PodSpec{}, }, }, } codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) for i, test := range tests { out, err := JSONPatch(codec, test.obj, test.fragment) if !test.expectErr { if err != nil { t.Errorf("testcase[%d], unexpected error: %v", i, err) } else if !apiequality.Semantic.DeepEqual(test.expected, out) { t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out)) } } if test.expectErr && err == nil { t.Errorf("testcase[%d], unexpected non-error", i) } } } type checkErrTestCase struct { err error expectedErr string expectedCode int } func TestCheckInvalidErr(t *testing.T) { testCheckError(t, []checkErrTestCase{ { errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid1").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field"), "single", "details")}), "The Invalid1 \"invalidation\" is invalid: field: Invalid value: \"single\": details\n", DefaultErrorExitCode, }, { errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid2").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field1"), "multi1", "details"), field.Invalid(field.NewPath("field2"), "multi2", "details")}), "The Invalid2 \"invalidation\" is invalid: \n* field1: Invalid value: \"multi1\": details\n* field2: Invalid value: \"multi2\": details\n", DefaultErrorExitCode, }, { errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid3").GroupKind(), "invalidation", field.ErrorList{}), "The Invalid3 \"invalidation\" is invalid", DefaultErrorExitCode, }, { errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid4").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field4"), "multi4", "details"), field.Invalid(field.NewPath("field4"), "multi4", "details")}), "The Invalid4 \"invalidation\" is invalid: field4: Invalid value: \"multi4\": details\n", DefaultErrorExitCode, }, { &errors.StatusError{ErrStatus: metav1.Status{ Status: metav1.StatusFailure, Code: http.StatusUnprocessableEntity, Reason: metav1.StatusReasonInvalid, // Details is nil. }}, "The request is invalid", DefaultErrorExitCode, }, // invalid error that that includes a message but no details { &errors.StatusError{ErrStatus: metav1.Status{ Status: metav1.StatusFailure, Code: http.StatusUnprocessableEntity, Reason: metav1.StatusReasonInvalid, // Details is nil. Message: "Some message", }}, "The request is invalid: Some message", DefaultErrorExitCode, }, // webhook response that sets code=422 with no reason { &errors.StatusError{ErrStatus: metav1.Status{ Status: "Failure", Message: `admission webhook "my.webhook" denied the request without explanation`, Code: 422, }}, `Error from server: admission webhook "my.webhook" denied the request without explanation`, DefaultErrorExitCode, }, // webhook response that sets code=422 with no reason and non-nil details { &errors.StatusError{ErrStatus: metav1.Status{ Status: "Failure", Message: `admission webhook "my.webhook" denied the request without explanation`, Code: 422, Details: &metav1.StatusDetails{}, }}, `Error from server: admission webhook "my.webhook" denied the request without explanation`, DefaultErrorExitCode, }, // source-wrapped webhook response that sets code=422 with no reason { AddSourceToErr("creating", "configmap.yaml", &errors.StatusError{ErrStatus: metav1.Status{ Status: "Failure", Message: `admission webhook "my.webhook" denied the request without explanation`, Code: 422, }}), `Error from server: error when creating "configmap.yaml": admission webhook "my.webhook" denied the request without explanation`, DefaultErrorExitCode, }, // webhook response that sets reason=Invalid and code=422 and a message { &errors.StatusError{ErrStatus: metav1.Status{ Status: "Failure", Reason: "Invalid", Message: `admission webhook "my.webhook" denied the request without explanation`, Code: 422, }}, `The request is invalid: admission webhook "my.webhook" denied the request without explanation`, DefaultErrorExitCode, }, }) } func TestCheckNoResourceMatchError(t *testing.T) { testCheckError(t, []checkErrTestCase{ { &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Resource: "foo"}}, `the server doesn't have a resource type "foo"`, DefaultErrorExitCode, }, { &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Version: "theversion", Resource: "foo"}}, `the server doesn't have a resource type "foo" in version "theversion"`, DefaultErrorExitCode, }, { &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Version: "theversion", Resource: "foo"}}, `the server doesn't have a resource type "foo" in group "thegroup" and version "theversion"`, DefaultErrorExitCode, }, { &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Resource: "foo"}}, `the server doesn't have a resource type "foo" in group "thegroup"`, DefaultErrorExitCode, }, }) } func TestCheckExitError(t *testing.T) { testCheckError(t, []checkErrTestCase{ { exec.CodeExitError{Err: fmt.Errorf("pod foo/bar terminated"), Code: 42}, "pod foo/bar terminated", 42, }, }) } func testCheckError(t *testing.T, tests []checkErrTestCase) { var errReturned string var codeReturned int errHandle := func(err string, code int) { errReturned = err codeReturned = code } for _, test := range tests { checkErr(test.err, errHandle) if errReturned != test.expectedErr { t.Fatalf("Got: %s, expected: %s", errReturned, test.expectedErr) } if codeReturned != test.expectedCode { t.Fatalf("Got: %d, expected: %d", codeReturned, test.expectedCode) } } } func TestDumpReaderToFile(t *testing.T) { testString := "TEST STRING" tempFile, err := os.CreateTemp(os.TempDir(), "hlpers_test_dump_") if err != nil { t.Errorf("unexpected error setting up a temporary file %v", err) } defer syscall.Unlink(tempFile.Name()) defer tempFile.Close() defer func() { if !t.Failed() { os.Remove(tempFile.Name()) } }() err = DumpReaderToFile(strings.NewReader(testString), tempFile.Name()) if err != nil { t.Errorf("error in DumpReaderToFile: %v", err) } data, err := os.ReadFile(tempFile.Name()) if err != nil { t.Errorf("error when reading %s: %v", tempFile.Name(), err) } stringData := string(data) if stringData != testString { t.Fatalf("Wrong file content %s != %s", testString, stringData) } } func TestDifferenceFunc(t *testing.T) { tests := []struct { name string fullArray []string subArray []string expected []string }{ { name: "remove some", fullArray: []string{"a", "b", "c", "d"}, subArray: []string{"c", "b"}, expected: []string{"a", "d"}, }, { name: "remove all", fullArray: []string{"a", "b", "c", "d"}, subArray: []string{"b", "d", "a", "c"}, expected: nil, }, { name: "remove none", fullArray: []string{"a", "b", "c", "d"}, subArray: nil, expected: []string{"a", "b", "c", "d"}, }, } for _, tc := range tests { result := Difference(tc.fullArray, tc.subArray) if !cmp.Equal(tc.expected, result, cmpopts.SortSlices(func(x, y string) bool { return x < y })) { t.Errorf("%s -> Expected: %v, but got: %v", tc.name, tc.expected, result) } } } func TestGetValidationDirective(t *testing.T) { tests := []struct { validateFlag string expectedDirective string expectedErr error }{ { expectedDirective: metav1.FieldValidationStrict, }, { validateFlag: "true", expectedDirective: metav1.FieldValidationStrict, }, { validateFlag: "True", expectedDirective: metav1.FieldValidationStrict, }, { validateFlag: "strict", expectedDirective: metav1.FieldValidationStrict, }, { validateFlag: "warn", expectedDirective: metav1.FieldValidationWarn, }, { validateFlag: "ignore", expectedDirective: metav1.FieldValidationIgnore, }, { validateFlag: "false", expectedDirective: metav1.FieldValidationIgnore, }, { validateFlag: "False", expectedDirective: metav1.FieldValidationIgnore, }, { validateFlag: "foo", expectedDirective: metav1.FieldValidationStrict, expectedErr: goerrors.New(`invalid - validate option "foo"; must be one of: strict (or true), warn, ignore (or false)`), }, } for _, tc := range tests { cmd := &cobra.Command{} AddValidateFlags(cmd) if tc.validateFlag != "" { cmd.Flags().Set("validate", tc.validateFlag) } directive, err := GetValidationDirective(cmd) if directive != tc.expectedDirective { t.Errorf("validation directive, expected: %v, but got: %v", tc.expectedDirective, directive) } if tc.expectedErr != nil { if err.Error() != tc.expectedErr.Error() { t.Errorf("GetValidationDirective error, expected: %v, but got: %v", tc.expectedErr, err) } } else { if err != nil { t.Errorf("expecte no error, but got: %v", err) } } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/kubectl_match_version.go000066400000000000000000000100121504711711200324570ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "sync" "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/scheme" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/component-base/version" ) const ( flagMatchBinaryVersion = "match-server-version" ) // MatchVersionFlags is for setting the "match server version" function. type MatchVersionFlags struct { Delegate genericclioptions.RESTClientGetter RequireMatchedServerVersion bool checkServerVersion sync.Once matchesServerVersionErr error } var _ genericclioptions.RESTClientGetter = &MatchVersionFlags{} func (f *MatchVersionFlags) checkMatchingServerVersion() error { f.checkServerVersion.Do(func() { if !f.RequireMatchedServerVersion { return } discoveryClient, err := f.Delegate.ToDiscoveryClient() if err != nil { f.matchesServerVersionErr = err return } f.matchesServerVersionErr = discovery.MatchesServerVersion(version.Get(), discoveryClient) }) return f.matchesServerVersionErr } // ToRESTConfig implements RESTClientGetter. // Returns a REST client configuration based on a provided path // to a .kubeconfig file, loading rules, and config flag overrides. // Expects the AddFlags method to have been called. func (f *MatchVersionFlags) ToRESTConfig() (*rest.Config, error) { if err := f.checkMatchingServerVersion(); err != nil { return nil, err } clientConfig, err := f.Delegate.ToRESTConfig() if err != nil { return nil, err } // TODO we should not have to do this. It smacks of something going wrong. setKubernetesDefaults(clientConfig) return clientConfig, nil } func (f *MatchVersionFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig { return f.Delegate.ToRawKubeConfigLoader() } func (f *MatchVersionFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { if err := f.checkMatchingServerVersion(); err != nil { return nil, err } return f.Delegate.ToDiscoveryClient() } // ToRESTMapper returns a mapper. func (f *MatchVersionFlags) ToRESTMapper() (meta.RESTMapper, error) { if err := f.checkMatchingServerVersion(); err != nil { return nil, err } return f.Delegate.ToRESTMapper() } func (f *MatchVersionFlags) AddFlags(flags *pflag.FlagSet) { flags.BoolVar(&f.RequireMatchedServerVersion, flagMatchBinaryVersion, f.RequireMatchedServerVersion, "Require server version to match client version") } func NewMatchVersionFlags(delegate genericclioptions.RESTClientGetter) *MatchVersionFlags { return &MatchVersionFlags{ Delegate: delegate, } } // setKubernetesDefaults sets default values on the provided client config for accessing the // Kubernetes API or returns an error if any of the defaults are impossible or invalid. // TODO this isn't what we want. Each clientset should be setting defaults as it sees fit. func setKubernetesDefaults(config *rest.Config) error { // TODO remove this hack. This is allowing the GetOptions to be serialized. config.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} if config.APIPath == "" { config.APIPath = "/api" } if config.NegotiatedSerializer == nil { // This codec factory ensures the resources are not converted. Therefore, resources // will not be round-tripped through internal versions. Defaulting does not happen // on the client. config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() } return rest.SetKubernetesDefaults(config) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go000066400000000000000000000054401504711711200315100ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "fmt" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" ) type OverrideType string const ( // OverrideTypeJSON will use an RFC6902 JSON Patch to alter the generated output OverrideTypeJSON OverrideType = "json" // OverrideTypeMerge will use an RFC7396 JSON Merge Patch to alter the generated output OverrideTypeMerge OverrideType = "merge" // OverrideTypeStrategic will use a Strategic Merge Patch to alter the generated output OverrideTypeStrategic OverrideType = "strategic" ) const DefaultOverrideType = OverrideTypeMerge type OverrideOptions struct { Overrides string OverrideType OverrideType } func (o *OverrideOptions) AddOverrideFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.Overrides, "overrides", "", i18n.T("An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.")) cmd.Flags().StringVar((*string)(&o.OverrideType), "override-type", string(DefaultOverrideType), fmt.Sprintf("The method used to override the generated object: %s, %s, or %s.", OverrideTypeJSON, OverrideTypeMerge, OverrideTypeStrategic)) } func (o *OverrideOptions) NewOverrider(dataStruct runtime.Object) *Overrider { return &Overrider{ Options: o, DataStruct: dataStruct, } } type Overrider struct { Options *OverrideOptions DataStruct runtime.Object } func (o *Overrider) Apply(obj runtime.Object) (runtime.Object, error) { if len(o.Options.Overrides) == 0 { return obj, nil } codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) var overrideType OverrideType if len(o.Options.OverrideType) == 0 { overrideType = DefaultOverrideType } else { overrideType = o.Options.OverrideType } switch overrideType { case OverrideTypeJSON: return JSONPatch(codec, obj, o.Options.Overrides) case OverrideTypeMerge: return Merge(codec, obj, o.Options.Overrides) case OverrideTypeStrategic: return StrategicMerge(codec, obj, o.Options.Overrides, o.DataStruct) default: return nil, fmt.Errorf("invalid override type: %v", overrideType) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/podcmd/000077500000000000000000000000001504711711200270325ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/podcmd/podcmd.go000066400000000000000000000074651504711711200306430ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package podcmd import ( "fmt" "io" "strings" v1 "k8s.io/api/core/v1" "k8s.io/klog/v2" ) // DefaultContainerAnnotationName is an annotation name that can be used to preselect the interesting container // from a pod when running kubectl. const DefaultContainerAnnotationName = "kubectl.kubernetes.io/default-container" // FindContainerByName selects the named container from the spec of // the provided pod or return nil if no such container exists. func FindContainerByName(pod *v1.Pod, name string) (*v1.Container, string) { for i := range pod.Spec.Containers { if pod.Spec.Containers[i].Name == name { return &pod.Spec.Containers[i], fmt.Sprintf("spec.containers{%s}", name) } } for i := range pod.Spec.InitContainers { if pod.Spec.InitContainers[i].Name == name { return &pod.Spec.InitContainers[i], fmt.Sprintf("spec.initContainers{%s}", name) } } for i := range pod.Spec.EphemeralContainers { if pod.Spec.EphemeralContainers[i].Name == name { return (*v1.Container)(&pod.Spec.EphemeralContainers[i].EphemeralContainerCommon), fmt.Sprintf("spec.ephemeralContainers{%s}", name) } } return nil, "" } // FindOrDefaultContainerByName defaults a container for a pod to the first container if any // exists, or returns an error. It will print a message to the user indicating a default was // selected if there was more than one container. func FindOrDefaultContainerByName(pod *v1.Pod, name string, quiet bool, warn io.Writer) (*v1.Container, error) { var container *v1.Container if len(name) > 0 { container, _ = FindContainerByName(pod, name) if container == nil { return nil, fmt.Errorf("container %s not found in pod %s", name, pod.Name) } return container, nil } // this should never happen, but just in case if len(pod.Spec.Containers) == 0 { return nil, fmt.Errorf("pod %s/%s does not have any containers", pod.Namespace, pod.Name) } // read the default container the annotation as per // https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2227-kubectl-default-container if name := pod.Annotations[DefaultContainerAnnotationName]; len(name) > 0 { if container, _ = FindContainerByName(pod, name); container != nil { klog.V(4).Infof("Defaulting container name from annotation %s", container.Name) return container, nil } klog.V(4).Infof("Default container name from annotation %s was not found in the pod", name) } // pick the first container as per existing behavior container = &pod.Spec.Containers[0] if !quiet && (len(pod.Spec.Containers) > 1 || len(pod.Spec.InitContainers) > 0 || len(pod.Spec.EphemeralContainers) > 0) { fmt.Fprintf(warn, "Defaulted container %q out of: %s\n", container.Name, AllContainerNames(pod)) } klog.V(4).Infof("Defaulting container name to %s", container.Name) return &pod.Spec.Containers[0], nil } func AllContainerNames(pod *v1.Pod) string { var containers []string for _, container := range pod.Spec.Containers { containers = append(containers, container.Name) } for _, container := range pod.Spec.EphemeralContainers { containers = append(containers, fmt.Sprintf("%s (ephem)", container.Name)) } for _, container := range pod.Spec.InitContainers { containers = append(containers, fmt.Sprintf("%s (init)", container.Name)) } return strings.Join(containers, ", ") } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/printing.go000066400000000000000000000016511504711711200277500ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "fmt" "k8s.io/kubectl/pkg/util/templates" ) // SuggestAPIResources returns a suggestion to use the "api-resources" command // to retrieve a supported list of resources func SuggestAPIResources(parent string) string { return templates.LongDesc(fmt.Sprintf("Use \"%s api-resources\" for a complete list of supported resources.", parent)) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/sanity/000077500000000000000000000000001504711711200270735ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/util/sanity/cmd_sanity.go000066400000000000000000000115001504711711200315510ustar00rootroot00000000000000/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sanity import ( "fmt" "os" "regexp" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" "k8s.io/kubectl/pkg/util/templates" ) // CmdCheck is the commom type of functions to check cobra commands type CmdCheck func(cmd *cobra.Command) []error // GlobalCheck is the common type of functions to check global flags type GlobalCheck func() []error var ( // AllCmdChecks is the list of CmdCheck type functions AllCmdChecks = []CmdCheck{ CheckLongDesc, CheckExamples, CheckFlags, } // AllGlobalChecks is the list of GlobalCheck type functions AllGlobalChecks = []GlobalCheck{ CheckGlobalVarFlags, } ) // RunGlobalChecks runs all the GlobalCheck functions passed and checks for error func RunGlobalChecks(globalChecks []GlobalCheck) []error { fmt.Fprint(os.Stdout, "---+ RUNNING GLOBAL CHECKS\n") errors := []error{} for _, check := range globalChecks { errors = append(errors, check()...) } return errors } // RunCmdChecks runs all the CmdCheck functions passed, skipping skippable commands and looks for error func RunCmdChecks(cmd *cobra.Command, cmdChecks []CmdCheck, skipCmd []string) []error { cmdPath := cmd.CommandPath() for _, skipCmdPath := range skipCmd { if cmdPath == skipCmdPath { fmt.Fprintf(os.Stdout, "---+ skipping command %s\n", cmdPath) return []error{} } } errors := []error{} if cmd.HasSubCommands() { for _, subCmd := range cmd.Commands() { errors = append(errors, RunCmdChecks(subCmd, cmdChecks, skipCmd)...) } } fmt.Fprintf(os.Stdout, "---+ RUNNING COMMAND CHECKS on %q\n", cmdPath) for _, check := range cmdChecks { if err := check(cmd); len(err) > 0 { errors = append(errors, err...) } } return errors } // CheckLongDesc checks if the long description is valid func CheckLongDesc(cmd *cobra.Command) []error { fmt.Fprint(os.Stdout, " ↳ checking long description\n") cmdPath := cmd.CommandPath() long := cmd.Long if len(long) > 0 { if strings.Trim(long, " \t\n") != long { return []error{fmt.Errorf(`command %q: long description is not normalized, make sure you are calling templates.LongDesc (from pkg/cmd/templates) before assigning cmd.Long`, cmdPath)} } } return nil } // CheckExamples checks if the command examples are valid func CheckExamples(cmd *cobra.Command) []error { fmt.Fprint(os.Stdout, " ↳ checking examples\n") cmdPath := cmd.CommandPath() examples := cmd.Example errors := []error{} if len(examples) > 0 { for _, line := range strings.Split(examples, "\n") { if !strings.HasPrefix(line, templates.Indentation) { errors = append(errors, fmt.Errorf(`command %q: examples are not normalized, make sure you are calling templates.Examples (from pkg/cmd/templates) before assigning cmd.Example`, cmdPath)) } if trimmed := strings.TrimSpace(line); strings.HasPrefix(trimmed, "//") { errors = append(errors, fmt.Errorf(`command %q: we use # to start comments in examples instead of //`, cmdPath)) } } } return errors } // CheckFlags checks if the command-line flags are valid func CheckFlags(cmd *cobra.Command) []error { allFlagsSlice := []*pflag.Flag{} cmd.Flags().VisitAll(func(f *pflag.Flag) { allFlagsSlice = append(allFlagsSlice, f) }) cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { allFlagsSlice = append(allFlagsSlice, f) }) fmt.Fprintf(os.Stdout, " ↳ checking %d flags\n", len(allFlagsSlice)) errors := []error{} // check flags long names regex, err := regexp.Compile(`^[a-z]+[a-z\-]*$`) if err != nil { errors = append(errors, fmt.Errorf("command %q: unable to compile regex to check flags", cmd.CommandPath())) return errors } for _, flag := range allFlagsSlice { name := flag.Name if !regex.MatchString(name) { errors = append(errors, fmt.Errorf("command %q: flag name %q is invalid, long form of flag names can only contain lowercase characters or dash (must match %v)", cmd.CommandPath(), name, regex)) } } return errors } // CheckGlobalVarFlags checks if the global flags are valid func CheckGlobalVarFlags() []error { fmt.Fprint(os.Stdout, " ↳ checking flags from global vars\n") errors := []error{} pflag.CommandLine.VisitAll(func(f *pflag.Flag) { errors = append(errors, fmt.Errorf("flag %q is invalid, please don't register any flag under the global variable \"CommandLine\"", f.Name)) }) return errors } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/version/000077500000000000000000000000001504711711200262745ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/version/skew_warning.go000066400000000000000000000042201504711711200313170ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package version import ( "fmt" "io" "k8s.io/apimachinery/pkg/util/version" apimachineryversion "k8s.io/apimachinery/pkg/version" "math" ) // supportedMinorVersionSkew is the maximum supported difference between the client and server minor versions. // For example: client versions 1.18, 1.19, and 1.20 would be within the supported version skew for server version 1.19, // and server versions 1.18, 1.19, and 1.20 would be within the supported version skew for client version 1.19. const supportedMinorVersionSkew = 1 // printVersionSkewWarning prints a warning message if the difference between the client and version is greater than // the supported version skew. func printVersionSkewWarning(w io.Writer, clientVersion, serverVersion apimachineryversion.Info) error { parsedClientVersion, err := version.ParseSemantic(clientVersion.GitVersion) if err != nil { return err } parsedServerVersion, err := version.ParseSemantic(serverVersion.GitVersion) if err != nil { return err } majorVersionDifference := math.Abs(float64(parsedClientVersion.Major()) - float64(parsedServerVersion.Major())) minorVersionDifference := math.Abs(float64(parsedClientVersion.Minor()) - float64(parsedServerVersion.Minor())) if majorVersionDifference > 0 || minorVersionDifference > supportedMinorVersionSkew { fmt.Fprintf(w, "WARNING: version difference between client (%d.%d) and server (%d.%d) exceeds the supported minor version skew of +/-%d\n", parsedClientVersion.Major(), parsedClientVersion.Minor(), parsedServerVersion.Major(), parsedServerVersion.Minor(), supportedMinorVersionSkew) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/version/skew_warning_test.go000066400000000000000000000066531504711711200323720ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package version import ( "bytes" apimachineryversion "k8s.io/apimachinery/pkg/version" "testing" ) func TestPrintVersionSkewWarning(t *testing.T) { output := &bytes.Buffer{} testCases := []struct { name string clientVersion apimachineryversion.Info serverVersion apimachineryversion.Info isWarningExpected bool }{ { name: "Should not warn if server and client versions are same", clientVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, serverVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, isWarningExpected: false, }, { name: "Should not warn if server and client versions are same and server is alpha", clientVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, serverVersion: apimachineryversion.Info{GitVersion: "v1.19.7-alpha"}, isWarningExpected: false, }, { name: "Should not warn if server and client versions are same and server is beta", clientVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, serverVersion: apimachineryversion.Info{GitVersion: "v1.19.7-beta"}, isWarningExpected: false, }, { name: "Should not warn if server is 1 minor version ahead of client", clientVersion: apimachineryversion.Info{GitVersion: "v1.18.5"}, serverVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, isWarningExpected: false, }, { name: "Should not warn if server is 1 minor version behind client", clientVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, serverVersion: apimachineryversion.Info{GitVersion: "v1.18.5"}, isWarningExpected: false, }, { name: "Should warn if server is 2 minor versions ahead of client", clientVersion: apimachineryversion.Info{GitVersion: "v1.17.7"}, serverVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, isWarningExpected: true, }, { name: "Should warn if server is 2 minor versions behind client", clientVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, serverVersion: apimachineryversion.Info{GitVersion: "v1.17.7"}, isWarningExpected: true, }, { name: "Should warn if major versions are not equal", clientVersion: apimachineryversion.Info{GitVersion: "v1.19.1"}, serverVersion: apimachineryversion.Info{GitVersion: "v2.19.1"}, isWarningExpected: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { output.Reset() printVersionSkewWarning(output, tc.clientVersion, tc.serverVersion) if tc.isWarningExpected && output.Len() == 0 { t.Error("warning was expected, but not written to the output") } else if !tc.isWarningExpected && output.Len() > 0 { t.Errorf("warning was not expected, but was written to the output: %s", output.String()) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/version/version.go000066400000000000000000000130761504711711200303170ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package version import ( "encoding/json" "errors" "fmt" "runtime/debug" "github.com/spf13/cobra" "sigs.k8s.io/yaml" apimachineryversion "k8s.io/apimachinery/pkg/version" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/discovery" "k8s.io/client-go/tools/clientcmd" "k8s.io/component-base/version" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) // TODO(knverey): remove this hardcoding once kubectl being built with module support makes BuildInfo available. const kustomizeVersion = "v5.5.0" // Version is a struct for version information type Version struct { ClientVersion *apimachineryversion.Info `json:"clientVersion,omitempty" yaml:"clientVersion,omitempty"` KustomizeVersion string `json:"kustomizeVersion,omitempty" yaml:"kustomizeVersion,omitempty"` ServerVersion *apimachineryversion.Info `json:"serverVersion,omitempty" yaml:"serverVersion,omitempty"` } var ( versionExample = templates.Examples(i18n.T(` # Print the client and server versions for the current context kubectl version`)) ) // Options is a struct to support version command type Options struct { ClientOnly bool Output string args []string discoveryClient discovery.CachedDiscoveryInterface genericiooptions.IOStreams } // NewOptions returns initialized Options func NewOptions(ioStreams genericiooptions.IOStreams) *Options { return &Options{ IOStreams: ioStreams, } } // NewCmdVersion returns a cobra command for fetching versions func NewCmdVersion(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command { o := NewOptions(ioStreams) cmd := &cobra.Command{ Use: "version", Short: i18n.T("Print the client and server version information"), Long: i18n.T("Print the client and server version information for the current context."), Example: versionExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } cmd.Flags().BoolVar(&o.ClientOnly, "client", o.ClientOnly, "If true, shows client version only (no server required).") cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, "One of 'yaml' or 'json'.") return cmd } // Complete completes all the required options func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error if o.ClientOnly { return nil } o.discoveryClient, err = f.ToDiscoveryClient() // if we had an empty rest.Config, continue and just print out client information. // if we had an error other than being unable to build a rest.Config, fail. if err != nil && !clientcmd.IsEmptyConfig(err) { return err } o.args = args return nil } // Validate validates the provided options func (o *Options) Validate() error { if len(o.args) != 0 { return errors.New(fmt.Sprintf("extra arguments: %v", o.args)) } if o.Output != "" && o.Output != "yaml" && o.Output != "json" { return errors.New(`--output must be 'yaml' or 'json'`) } return nil } // Run executes version command func (o *Options) Run() error { var ( serverErr error versionInfo Version ) versionInfo.ClientVersion = func() *apimachineryversion.Info { v := version.Get(); return &v }() versionInfo.KustomizeVersion = getKustomizeVersion() if !o.ClientOnly && o.discoveryClient != nil { // Always request fresh data from the server o.discoveryClient.Invalidate() versionInfo.ServerVersion, serverErr = o.discoveryClient.ServerVersion() } switch o.Output { case "": fmt.Fprintf(o.Out, "Client Version: %s\n", versionInfo.ClientVersion.GitVersion) fmt.Fprintf(o.Out, "Kustomize Version: %s\n", versionInfo.KustomizeVersion) if versionInfo.ServerVersion != nil { fmt.Fprintf(o.Out, "Server Version: %s\n", versionInfo.ServerVersion.GitVersion) } case "yaml": marshalled, err := yaml.Marshal(&versionInfo) if err != nil { return err } fmt.Fprintln(o.Out, string(marshalled)) case "json": marshalled, err := json.MarshalIndent(&versionInfo, "", " ") if err != nil { return err } fmt.Fprintln(o.Out, string(marshalled)) default: // There is a bug in the program if we hit this case. // However, we follow a policy of never panicking. return fmt.Errorf("VersionOptions were not validated: --output=%q should have been rejected", o.Output) } if versionInfo.ServerVersion != nil { if err := printVersionSkewWarning(o.ErrOut, *versionInfo.ClientVersion, *versionInfo.ServerVersion); err != nil { return err } } return serverErr } func getKustomizeVersion() string { if modVersion, ok := GetKustomizeModVersion(); ok { return modVersion } return kustomizeVersion // other clients should provide their own fallback } func GetKustomizeModVersion() (string, bool) { info, ok := debug.ReadBuildInfo() if !ok { return "", false } for _, dep := range info.Deps { if dep.Path == "sigs.k8s.io/kustomize/kustomize/v4" || dep.Path == "sigs.k8s.io/kustomize/kustomize/v5" { return dep.Version, true } } return "", false } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/version/version_test.go000066400000000000000000000031461504711711200313530ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package version import ( "strings" "testing" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) func TestNewCmdVersionClientVersion(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() streams, _, buf, _ := genericiooptions.NewTestIOStreams() o := NewOptions(streams) if err := o.Complete(tf, &cobra.Command{}, nil); err != nil { t.Errorf("Unexpected error: %v", err) } if err := o.Validate(); err != nil { t.Errorf("Unexpected error: %v", err) } if err := o.Complete(tf, &cobra.Command{}, []string{"extraParameter0"}); err != nil { t.Errorf("Unexpected error: %v", err) } if err := o.Validate(); !strings.Contains(err.Error(), "extra arguments") { t.Errorf("Unexpected error: should fail to validate the args length greater than 0") } if err := o.Run(); err != nil { t.Errorf("Cannot execute version command: %v", err) } if !strings.Contains(buf.String(), "Client Version") { t.Errorf("unexpected output: %s", buf.String()) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/wait/000077500000000000000000000000001504711711200255535ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/wait/condition.go000066400000000000000000000156441504711711200301020ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wait import ( "context" "errors" "fmt" "io" "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" "k8s.io/kubectl/pkg/util/interrupt" ) // ConditionalWait hold information to check an API status condition type ConditionalWait struct { conditionName string conditionStatus string // errOut is written to if an error occurs errOut io.Writer } // IsConditionMet is a conditionfunc for waiting on an API condition to be met func (w ConditionalWait) IsConditionMet(ctx context.Context, info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) { return getObjAndCheckCondition(ctx, info, o, w.isConditionMet, w.checkCondition) } func (w ConditionalWait) checkCondition(obj *unstructured.Unstructured) (bool, error) { conditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions") if err != nil { return false, err } if !found { return false, nil } for _, conditionUncast := range conditions { condition := conditionUncast.(map[string]interface{}) name, found, err := unstructured.NestedString(condition, "type") if !found || err != nil || !strings.EqualFold(name, w.conditionName) { continue } status, found, err := unstructured.NestedString(condition, "status") if !found || err != nil { continue } generation, found, _ := unstructured.NestedInt64(obj.Object, "metadata", "generation") if found { observedGeneration, found := getObservedGeneration(obj, condition) if found && observedGeneration < generation { return false, nil } } return strings.EqualFold(status, w.conditionStatus), nil } return false, nil } func (w ConditionalWait) isConditionMet(event watch.Event) (bool, error) { if event.Type == watch.Error { // keep waiting in the event we see an error - we expect the watch to be closed by // the server err := apierrors.FromObject(event.Object) fmt.Fprintf(w.errOut, "error: An error occurred while waiting for the condition to be satisfied: %v", err) return false, nil } if event.Type == watch.Deleted { // this will chain back out, result in another get and an return false back up the chain return false, nil } obj := event.Object.(*unstructured.Unstructured) return w.checkCondition(obj) } type isCondMetFunc func(event watch.Event) (bool, error) type checkCondFunc func(obj *unstructured.Unstructured) (bool, error) // getObjAndCheckCondition will make a List query to the API server to get the object and check if the condition is met using check function. // If the condition is not met, it will make a Watch query to the server and pass in the condMet function func getObjAndCheckCondition(ctx context.Context, info *resource.Info, o *WaitOptions, condMet isCondMetFunc, check checkCondFunc) (runtime.Object, bool, error) { if len(info.Name) == 0 { return info.Object, false, fmt.Errorf("resource name must be provided") } endTime := time.Now().Add(o.Timeout) timeout := time.Until(endTime) errWaitTimeoutWithName := extendErrWaitTimeout(wait.ErrWaitTimeout, info) // nolint:staticcheck // SA1019 if o.Timeout == 0 { // If timeout is zero we will fetch the object(s) once only and check gottenObj, initObjGetErr := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(context.Background(), info.Name, metav1.GetOptions{}) if initObjGetErr != nil { return nil, false, initObjGetErr } if gottenObj == nil { return nil, false, fmt.Errorf("condition not met for %s", info.ObjectName()) } conditionCheck, err := check(gottenObj) if err != nil { return gottenObj, false, err } if !conditionCheck { return gottenObj, false, fmt.Errorf("condition not met for %s", info.ObjectName()) } return gottenObj, true, nil } if timeout < 0 { // we're out of time return info.Object, false, errWaitTimeoutWithName } mapping := info.ResourceMapping() // used to pass back meaningful errors if object disappears fieldSelector := fields.OneTermEqualSelector("metadata.name", info.Name).String() lw := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { options.FieldSelector = fieldSelector return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(context.TODO(), options) }, } // this function is used to refresh the cache to prevent timeout waits on resources that have disappeared preconditionFunc := func(store cache.Store) (bool, error) { _, exists, err := store.Get(&metav1.ObjectMeta{Namespace: info.Namespace, Name: info.Name}) if err != nil { return true, err } if !exists { return true, apierrors.NewNotFound(mapping.Resource.GroupResource(), info.Name) } return false, nil } intrCtx, cancel := context.WithCancel(ctx) defer cancel() var result runtime.Object intr := interrupt.New(nil, cancel) err := intr.Run(func() error { ev, err := watchtools.UntilWithSync(intrCtx, lw, &unstructured.Unstructured{}, preconditionFunc, watchtools.ConditionFunc(condMet)) if ev != nil { result = ev.Object } if errors.Is(err, context.DeadlineExceeded) { return errWaitTimeoutWithName } return err }) if err != nil { if errors.Is(err, wait.ErrWaitTimeout) { // nolint:staticcheck // SA1019 return result, false, errWaitTimeoutWithName } return result, false, err } return result, true, nil } func extendErrWaitTimeout(err error, info *resource.Info) error { return fmt.Errorf("%s on %s/%s", err.Error(), info.Mapping.Resource.Resource, info.Name) } func getObservedGeneration(obj *unstructured.Unstructured, condition map[string]interface{}) (int64, bool) { conditionObservedGeneration, found, _ := unstructured.NestedInt64(condition, "observedGeneration") if found { return conditionObservedGeneration, true } statusObservedGeneration, found, _ := unstructured.NestedInt64(obj.Object, "status", "observedGeneration") return statusObservedGeneration, found } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/wait/create.go000066400000000000000000000017661504711711200273570ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wait import ( "context" "fmt" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/resource" ) // IsCreated is a condition func for waiting for something to be created func IsCreated(ctx context.Context, info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) { if len(info.Name) == 0 || info.Object == nil { return nil, false, fmt.Errorf("resource name must be provided") } return info.Object, true, nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/wait/delete.go000066400000000000000000000112451504711711200273470ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wait import ( "context" "errors" "fmt" "io" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" "k8s.io/kubectl/pkg/util/interrupt" ) // IsDeleted is a condition func for waiting for something to be deleted func IsDeleted(ctx context.Context, info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) { if len(info.Name) == 0 { return info.Object, false, fmt.Errorf("resource name must be provided") } gottenObj, initObjGetErr := o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Get(ctx, info.Name, metav1.GetOptions{}) if apierrors.IsNotFound(initObjGetErr) { return info.Object, true, nil } if initObjGetErr != nil { // TODO this could do something slightly fancier if we wish return info.Object, false, initObjGetErr } resourceLocation := ResourceLocation{ GroupResource: info.Mapping.Resource.GroupResource(), Namespace: gottenObj.GetNamespace(), Name: gottenObj.GetName(), } if uid, ok := o.UIDMap[resourceLocation]; ok { if gottenObj.GetUID() != uid { return gottenObj, true, nil } } endTime := time.Now().Add(o.Timeout) timeout := time.Until(endTime) errWaitTimeoutWithName := extendErrWaitTimeout(wait.ErrWaitTimeout, info) // nolint:staticcheck // SA1019 if o.Timeout == 0 { // If timeout is zero check if the object exists once only if gottenObj == nil { return nil, true, nil } return gottenObj, false, fmt.Errorf("condition not met for %s", info.ObjectName()) } if timeout < 0 { // we're out of time return info.Object, false, errWaitTimeoutWithName } fieldSelector := fields.OneTermEqualSelector("metadata.name", info.Name).String() lw := &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).List(ctx, options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { options.FieldSelector = fieldSelector return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(ctx, options) }, } // this function is used to refresh the cache to prevent timeout waits on resources that have disappeared preconditionFunc := func(store cache.Store) (bool, error) { _, exists, err := store.Get(&metav1.ObjectMeta{Namespace: info.Namespace, Name: info.Name}) if err != nil { return true, err } if !exists { // since we're looking for it to disappear we just return here if it no longer exists return true, nil } return false, nil } intrCtx, cancel := context.WithCancel(ctx) defer cancel() intr := interrupt.New(nil, cancel) err := intr.Run(func() error { _, err := watchtools.UntilWithSync(intrCtx, lw, &unstructured.Unstructured{}, preconditionFunc, Wait{errOut: o.ErrOut}.IsDeleted) if errors.Is(err, context.DeadlineExceeded) { return errWaitTimeoutWithName } return err }) if err != nil { if errors.Is(err, wait.ErrWaitTimeout) { // nolint:staticcheck // SA1019 return gottenObj, false, errWaitTimeoutWithName } return gottenObj, false, err } return gottenObj, true, nil } // Wait has helper methods for handling watches, including error handling. type Wait struct { errOut io.Writer } // IsDeleted returns true if the object is deleted. It prints any errors it encounters. func (w Wait) IsDeleted(event watch.Event) (bool, error) { switch event.Type { case watch.Error: // keep waiting in the event we see an error - we expect the watch to be closed by // the server if the error is unrecoverable. err := apierrors.FromObject(event.Object) fmt.Fprintf(w.errOut, "error: An error occurred while waiting for the object to be deleted: %v", err) return false, nil case watch.Deleted: return true, nil default: return false, nil } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/wait/json.go000066400000000000000000000103171504711711200270550ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wait import ( "context" "errors" "fmt" "io" "reflect" "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/util/jsonpath" ) // JSONPathWait holds a JSONPath Parser which has the ability // to check for the JSONPath condition and compare with the API server provided JSON output. type JSONPathWait struct { matchAnyValue bool jsonPathValue string jsonPathParser *jsonpath.JSONPath // errOut is written to if an error occurs errOut io.Writer } // IsJSONPathConditionMet fulfills the requirements of the interface ConditionFunc which provides condition check func (j JSONPathWait) IsJSONPathConditionMet(ctx context.Context, info *resource.Info, o *WaitOptions) (runtime.Object, bool, error) { return getObjAndCheckCondition(ctx, info, o, j.isJSONPathConditionMet, j.checkCondition) } // isJSONPathConditionMet is a helper function of IsJSONPathConditionMet // which check the watch event and check if a JSONPathWait condition is met func (j JSONPathWait) isJSONPathConditionMet(event watch.Event) (bool, error) { if event.Type == watch.Error { // keep waiting in the event we see an error - we expect the watch to be closed by // the server err := apierrors.FromObject(event.Object) fmt.Fprintf(j.errOut, "error: An error occurred while waiting for the condition to be satisfied: %v", err) return false, nil } if event.Type == watch.Deleted { // this will chain back out, result in another get and an return false back up the chain return false, nil } // event runtime Object can be safely asserted to Unstructed // because we are working with dynamic client obj := event.Object.(*unstructured.Unstructured) return j.checkCondition(obj) } // checkCondition uses JSONPath parser to parse the JSON received from the API server // and check if it matches the desired condition func (j JSONPathWait) checkCondition(obj *unstructured.Unstructured) (bool, error) { queryObj := obj.UnstructuredContent() parseResults, err := j.jsonPathParser.FindResults(queryObj) if err != nil { return false, err } if len(parseResults) == 0 || len(parseResults[0]) == 0 { return false, nil } if err := verifyParsedJSONPath(parseResults); err != nil { return false, err } if j.matchAnyValue { return true, nil } isConditionMet, err := compareResults(parseResults[0][0], j.jsonPathValue) if err != nil { return false, err } return isConditionMet, nil } // verifyParsedJSONPath verifies the JSON received from the API server is valid. // It will only accept a single JSON func verifyParsedJSONPath(results [][]reflect.Value) error { if len(results) > 1 { return errors.New("given jsonpath expression matches more than one list") } if len(results[0]) > 1 { return errors.New("given jsonpath expression matches more than one value") } return nil } // compareResults will compare the reflect.Value from the result parsed by the // JSONPath parser with the expected value given by the value // // Since this is coming from an unstructured this can only ever be a primitive, // map[string]interface{}, or []interface{}. // We do not support the last two and rely on fmt to handle conversion to string // and compare the result with user input func compareResults(r reflect.Value, expectedVal string) (bool, error) { switch r.Interface().(type) { case map[string]interface{}, []interface{}: return false, errors.New("jsonpath leads to a nested object or list which is not supported") } s := fmt.Sprintf("%v", r.Interface()) return strings.TrimSpace(s) == strings.TrimSpace(expectedVal), nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go000066400000000000000000000316671504711711200270630ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wait import ( "context" "errors" "fmt" "io" "strings" "time" "github.com/spf13/cobra" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" watchtools "k8s.io/client-go/tools/watch" "k8s.io/client-go/util/jsonpath" cmdget "k8s.io/kubectl/pkg/cmd/get" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( waitLong = templates.LongDesc(i18n.T(` Experimental: Wait for a specific condition on one or many resources. The command takes multiple resources and waits until the specified condition is seen in the Status field of every given resource. Alternatively, the command can wait for the given set of resources to be created or deleted by providing the "create" or "delete" keyword as the value to the --for flag. A successful message will be printed to stdout indicating when the specified condition has been met. You can use -o option to change to output destination.`)) waitExample = templates.Examples(i18n.T(` # Wait for the pod "busybox1" to contain the status condition of type "Ready" kubectl wait --for=condition=Ready pod/busybox1 # The default value of status condition is true; you can wait for other targets after an equal delimiter (compared after Unicode simple case folding, which is a more general form of case-insensitivity) kubectl wait --for=condition=Ready=false pod/busybox1 # Wait for the pod "busybox1" to contain the status phase to be "Running" kubectl wait --for=jsonpath='{.status.phase}'=Running pod/busybox1 # Wait for pod "busybox1" to be Ready kubectl wait --for='jsonpath={.status.conditions[?(@.type=="Ready")].status}=True' pod/busybox1 # Wait for the service "loadbalancer" to have ingress kubectl wait --for=jsonpath='{.status.loadBalancer.ingress}' service/loadbalancer # Wait for the secret "busybox1" to be created, with a timeout of 30s kubectl create secret generic busybox1 kubectl wait --for=create secret/busybox1 --timeout=30s # Wait for the pod "busybox1" to be deleted, with a timeout of 60s, after having issued the "delete" command kubectl delete pod/busybox1 kubectl wait --for=delete pod/busybox1 --timeout=60s`)) ) // errNoMatchingResources is returned when there is no resources matching a query. var errNoMatchingResources = errors.New("no matching resources found") // WaitFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which // reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes // the logic itself easy to unit test type WaitFlags struct { RESTClientGetter genericclioptions.RESTClientGetter PrintFlags *genericclioptions.PrintFlags ResourceBuilderFlags *genericclioptions.ResourceBuilderFlags Timeout time.Duration ForCondition string genericiooptions.IOStreams } // NewWaitFlags returns a default WaitFlags func NewWaitFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *WaitFlags { return &WaitFlags{ RESTClientGetter: restClientGetter, PrintFlags: genericclioptions.NewPrintFlags("condition met"), ResourceBuilderFlags: genericclioptions.NewResourceBuilderFlags(). WithLabelSelector(""). WithFieldSelector(""). WithAll(false). WithAllNamespaces(false). WithLocal(false). WithLatest(), Timeout: 30 * time.Second, IOStreams: streams, } } // NewCmdWait returns a cobra command for waiting func NewCmdWait(restClientGetter genericclioptions.RESTClientGetter, streams genericiooptions.IOStreams) *cobra.Command { flags := NewWaitFlags(restClientGetter, streams) cmd := &cobra.Command{ Use: "wait ([-f FILENAME] | resource.group/resource.name | resource.group [(-l label | --all)]) [--for=create|--for=delete|--for condition=available|--for=jsonpath='{}'[=value]]", Short: i18n.T("Experimental: Wait for a specific condition on one or many resources"), Long: waitLong, Example: waitExample, DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { o, err := flags.ToOptions(args) cmdutil.CheckErr(err) cmdutil.CheckErr(o.RunWait()) }, SuggestFor: []string{"list", "ps"}, } flags.AddFlags(cmd) return cmd } // AddFlags registers flags for a cli func (flags *WaitFlags) AddFlags(cmd *cobra.Command) { flags.PrintFlags.AddFlags(cmd) flags.ResourceBuilderFlags.AddFlags(cmd.Flags()) cmd.Flags().DurationVar(&flags.Timeout, "timeout", flags.Timeout, "The length of time to wait before giving up. Zero means check once and don't wait, negative means wait for a week.") cmd.Flags().StringVar(&flags.ForCondition, "for", flags.ForCondition, "The condition to wait on: [create|delete|condition=condition-name[=condition-value]|jsonpath='{JSONPath expression}'=[JSONPath value]]. The default condition-value is true. Condition values are compared after Unicode simple case folding, which is a more general form of case-insensitivity.") } // ToOptions converts from CLI inputs to runtime inputs func (flags *WaitFlags) ToOptions(args []string) (*WaitOptions, error) { printer, err := flags.PrintFlags.ToPrinter() if err != nil { return nil, err } builder := flags.ResourceBuilderFlags.ToBuilder(flags.RESTClientGetter, args) clientConfig, err := flags.RESTClientGetter.ToRESTConfig() if err != nil { return nil, err } dynamicClient, err := dynamic.NewForConfig(clientConfig) if err != nil { return nil, err } conditionFn, err := conditionFuncFor(flags.ForCondition, flags.ErrOut) if err != nil { return nil, err } effectiveTimeout := flags.Timeout if effectiveTimeout < 0 { effectiveTimeout = 168 * time.Hour } o := &WaitOptions{ ResourceFinder: builder, DynamicClient: dynamicClient, Timeout: effectiveTimeout, ForCondition: flags.ForCondition, Printer: printer, ConditionFn: conditionFn, IOStreams: flags.IOStreams, } return o, nil } func conditionFuncFor(condition string, errOut io.Writer) (ConditionFunc, error) { lowercaseCond := strings.ToLower(condition) switch { case lowercaseCond == "delete": return IsDeleted, nil case lowercaseCond == "create": return IsCreated, nil case strings.HasPrefix(condition, "condition="): conditionName := strings.TrimPrefix(condition, "condition=") conditionValue := "true" if equalsIndex := strings.Index(conditionName, "="); equalsIndex != -1 { conditionValue = conditionName[equalsIndex+1:] conditionName = conditionName[0:equalsIndex] } return ConditionalWait{ conditionName: conditionName, conditionStatus: conditionValue, errOut: errOut, }.IsConditionMet, nil case strings.HasPrefix(condition, "jsonpath="): jsonPathInput := strings.TrimPrefix(condition, "jsonpath=") jsonPathExp, jsonPathValue, err := processJSONPathInput(jsonPathInput) if err != nil { return nil, err } j, err := newJSONPathParser(jsonPathExp) if err != nil { return nil, err } return JSONPathWait{ matchAnyValue: jsonPathValue == "", jsonPathValue: jsonPathValue, jsonPathParser: j, errOut: errOut, }.IsJSONPathConditionMet, nil } return nil, fmt.Errorf("unrecognized condition: %q", condition) } // newJSONPathParser will create a new JSONPath parser based on the jsonPathExpression func newJSONPathParser(jsonPathExpression string) (*jsonpath.JSONPath, error) { j := jsonpath.New("wait").AllowMissingKeys(true) if jsonPathExpression == "" { return nil, errors.New("jsonpath expression cannot be empty") } if err := j.Parse(jsonPathExpression); err != nil { return nil, err } return j, nil } // processJSONPathInput will parse and process the provided JSONPath input containing a JSON expression and optionally // a value for the matching condition. func processJSONPathInput(input string) (string, string, error) { jsonPathInput := splitJSONPathInput(input) if numOfArgs := len(jsonPathInput); numOfArgs < 1 || numOfArgs > 2 { return "", "", fmt.Errorf("jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'") } relaxedJSONPathExp, err := cmdget.RelaxedJSONPathExpression(jsonPathInput[0]) if err != nil { return "", "", err } if len(jsonPathInput) == 1 { return relaxedJSONPathExp, "", nil } jsonPathValue := strings.Trim(jsonPathInput[1], `'"`) if jsonPathValue == "" { return "", "", errors.New("jsonpath wait has to have a value after equal sign, like --for=jsonpath='{.status.readyReplicas}'=3") } return relaxedJSONPathExp, jsonPathValue, nil } // splitJSONPathInput splits the provided input string on single '='. Double '==' will not cause the string to be // split. E.g., "a.b.c====d.e.f===g.h.i===" will split to ["a.b.c====d.e.f==","g.h.i==",""]. func splitJSONPathInput(input string) []string { var output []string var element strings.Builder for i := 0; i < len(input); i++ { if input[i] == '=' { if i < len(input)-1 && input[i+1] == '=' { element.WriteString("==") i++ continue } output = append(output, element.String()) element.Reset() continue } element.WriteByte(input[i]) } return append(output, element.String()) } // ResourceLocation holds the location of a resource type ResourceLocation struct { GroupResource schema.GroupResource Namespace string Name string } // UIDMap maps ResourceLocation with UID type UIDMap map[ResourceLocation]types.UID // WaitOptions is a set of options that allows you to wait. This is the object reflects the runtime needs of a wait // command, making the logic itself easy to unit test with our existing mocks. type WaitOptions struct { ResourceFinder genericclioptions.ResourceFinder // UIDMap maps a resource location to a UID. It is optional, but ConditionFuncs may choose to use it to make the result // more reliable. For instance, delete can look for UID consistency during delegated calls. UIDMap UIDMap DynamicClient dynamic.Interface Timeout time.Duration ForCondition string Printer printers.ResourcePrinter ConditionFn ConditionFunc genericiooptions.IOStreams } // ConditionFunc is the interface for providing condition checks type ConditionFunc func(ctx context.Context, info *resource.Info, o *WaitOptions) (finalObject runtime.Object, done bool, err error) // RunWait runs the waiting logic func (o *WaitOptions) RunWait() error { ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout) defer cancel() if strings.ToLower(o.ForCondition) == "create" { // TODO(soltysh): this is not ideal solution, because we're polling every .5s, // and we have to use ResourceFinder, which contains the resource name. // In the long run, we should expose resource information from ResourceFinder, // or functions from ResourceBuilder for parsing those. Lastly, this poll // should be replaced with a ListWatch cache. if err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, o.Timeout, true, func(context.Context) (done bool, err error) { foundResource := false visitErr := o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error { foundResource = true return nil }) if apierrors.IsNotFound(visitErr) { return false, nil } if visitErr != nil { return false, visitErr } return foundResource, nil }); err != nil { if errors.Is(err, context.DeadlineExceeded) { return fmt.Errorf("%s", wait.ErrWaitTimeout.Error()) // nolint:staticcheck // SA1019 } return err } } visitCount := 0 visitFunc := func(info *resource.Info, err error) error { if err != nil { return err } visitCount++ finalObject, success, err := o.ConditionFn(ctx, info, o) if success { o.Printer.PrintObj(finalObject, o.Out) return nil } if err == nil { return fmt.Errorf("%v unsatisfied for unknown reason", finalObject) } return err } visitor := o.ResourceFinder.Do() isForDelete := strings.ToLower(o.ForCondition) == "delete" if visitor, ok := visitor.(*resource.Result); ok && isForDelete { visitor.IgnoreErrors(apierrors.IsNotFound) } err := visitor.Visit(visitFunc) if err != nil { return err } if visitCount == 0 && !isForDelete { return errNoMatchingResources } return err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait_test.go000066400000000000000000001676771504711711200301350ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package wait import ( "io" "strings" "testing" "time" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" 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" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" dynamicfakeclient "k8s.io/client-go/dynamic/fake" clienttesting "k8s.io/client-go/testing" ) const ( None string = "" podYAML string = ` apiVersion: v1 kind: Pod metadata: creationTimestamp: "1998-10-21T18:39:43Z" generateName: foo-b6699dcfb- labels: app: nginx pod-template-hash: b6699dcfb name: foo-b6699dcfb-rnv7t namespace: default ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: true controller: true kind: ReplicaSet name: foo-b6699dcfb uid: 8fc1088c-15d5-4a8c-8502-4dfcedef97b8 resourceVersion: "14203463" uid: e2cc99fa-5a28-44da-b880-4dded28882ef spec: containers: - image: nginx imagePullPolicy: IfNotPresent name: nginx ports: - containerPort: 80 protocol: TCP resources: limits: cpu: 500m memory: 128Mi requests: cpu: 250m memory: 64Mi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: kube-api-access-s64k4 readOnly: true dnsPolicy: ClusterFirst enableServiceLinks: true nodeName: knode0 preemptionPolicy: PreemptLowerPriority 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: kube-api-access-s64k4 projected: defaultMode: 420 sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt status: conditions: - lastProbeTime: null lastTransitionTime: "1998-10-21T18:39:37Z" status: "True" type: Initialized - lastProbeTime: null lastTransitionTime: "1998-10-21T18:39:42Z" status: "True" type: Ready - lastProbeTime: null lastTransitionTime: "1998-10-21T18:39:42Z" status: "True" type: ContainersReady - lastProbeTime: null lastTransitionTime: "1998-10-21T18:39:37Z" status: "True" type: PodScheduled containerStatuses: - containerID: containerd://e35792ba1d6e9a56629659b35dbdb93dacaa0a413962ee04775319f5438e493c image: docker.io/library/nginx:latest imageID: docker.io/library/nginx@sha256:644a70516a26004c97d0d85c7fe1d0c3a67ea8ab7ddf4aff193d9f301670cf36 lastState: {} name: nginx ready: true restartCount: 0 started: true state: running: startedAt: "1998-10-21T18:39:41Z" hostIP: 192.168.0.22 phase: Running podIP: 10.42.1.203 podIPs: - ip: 10.42.1.203 qosClass: Burstable startTime: "1998-10-21T18:39:37Z" ` ) func newUnstructuredList(items ...*unstructured.Unstructured) *unstructured.UnstructuredList { list := &unstructured.UnstructuredList{} for i := range items { list.Items = append(list.Items, *items[i]) } return list } func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": apiVersion, "kind": kind, "metadata": map[string]interface{}{ "namespace": namespace, "name": name, "uid": "some-UID-value", }, }, } } func newUnstructuredWithGeneration(apiVersion, kind, namespace, name string, generation int64) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": apiVersion, "kind": kind, "metadata": map[string]interface{}{ "namespace": namespace, "name": name, "uid": "some-UID-value", "generation": generation, }, }, } } func newUnstructuredStatus(status *metav1.Status) runtime.Unstructured { obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(status) if err != nil { panic(err) } return &unstructured.Unstructured{ Object: obj, } } func addCondition(in *unstructured.Unstructured, name, status string) *unstructured.Unstructured { conditions, _, _ := unstructured.NestedSlice(in.Object, "status", "conditions") conditions = append(conditions, map[string]interface{}{ "type": name, "status": status, }) unstructured.SetNestedSlice(in.Object, conditions, "status", "conditions") return in } func addConditionWithObservedGeneration(in *unstructured.Unstructured, name, status string, observedGeneration int64) *unstructured.Unstructured { conditions, _, _ := unstructured.NestedSlice(in.Object, "status", "conditions") conditions = append(conditions, map[string]interface{}{ "type": name, "status": status, "observedGeneration": observedGeneration, }) unstructured.SetNestedSlice(in.Object, conditions, "status", "conditions") return in } // createUnstructured parses the yaml string into a map[string]interface{}. Verifies that the string does not have // any tab characters. func createUnstructured(t *testing.T, config string) *unstructured.Unstructured { t.Helper() result := map[string]interface{}{} require.NotContains(t, config, "\t", "Yaml %s cannot contain tabs", config) require.NoError(t, yaml.Unmarshal([]byte(config), &result), "Could not parse config:\n\n%s\n", config) return &unstructured.Unstructured{ Object: result, } } func TestWaitForDeletion(t *testing.T) { scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList", {Group: "group", Version: "version", Resource: "theresource-1"}: "TheKindList", {Group: "group", Version: "version", Resource: "theresource-2"}: "TheKindList", } tests := []struct { name string infos []*resource.Info fakeClient func() *dynamicfakeclient.FakeDynamicClient timeout time.Duration uidMap UIDMap expectedErr string }{ { name: "missing on get", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) }, timeout: 10 * time.Second, }, { name: "handles no infos", infos: []*resource.Info{}, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClient(scheme) }, timeout: 10 * time.Second, expectedErr: errNoMatchingResources.Error(), }, { name: "uid conflict on get", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) count := 0 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { if count == 0 { count++ fakeWatch := watch.NewRaceFreeFake() go func() { time.Sleep(1 * time.Second) fakeWatch.Stop() }() return true, fakeWatch, nil } fakeWatch := watch.NewRaceFreeFake() return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, uidMap: UIDMap{ ResourceLocation{Namespace: "ns-foo", Name: "name-foo"}: types.UID("some-UID-value"), ResourceLocation{GroupResource: schema.GroupResource{Group: "group", Resource: "theresource"}, Namespace: "ns-foo", Name: "name-foo"}: types.UID("some-nonmatching-UID-value"), }, }, { name: "times out", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) return fakeClient }, timeout: 1 * time.Second, expectedErr: "timed out waiting for the condition on theresource/name-foo", }, { name: "delete for existing resource with no timeout", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) return fakeClient }, timeout: 0 * time.Second, expectedErr: "condition not met for theresource/name-foo", }, { name: "delete for nonexisting resource with no timeout", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thenonexistentresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) return fakeClient }, timeout: 0 * time.Second, expectedErr: "", }, { name: "handles watch close out", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") unstructuredObj.SetResourceVersion("123") unstructuredList := newUnstructuredList(unstructuredObj) unstructuredList.SetResourceVersion("234") return true, unstructuredList, nil }) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") unstructuredObj.SetResourceVersion("123") unstructuredList := newUnstructuredList(unstructuredObj) unstructuredList.SetResourceVersion("234") return true, unstructuredList, nil }) count := 0 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { if count == 0 { count++ fakeWatch := watch.NewRaceFreeFake() go func() { time.Sleep(1 * time.Second) fakeWatch.Stop() }() return true, fakeWatch, nil } fakeWatch := watch.NewRaceFreeFake() return true, fakeWatch, nil }) return fakeClient }, timeout: 3 * time.Second, expectedErr: "timed out waiting for the condition on theresource/name-foo", }, { name: "handles watch delete", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")) return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, }, { name: "handles watch delete multiple", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource-1"}, }, Name: "name-foo-1", Namespace: "ns-foo", }, { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource-2"}, }, Name: "name-foo-2", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("get", "theresource-1", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-1"), nil }) fakeClient.PrependReactor("get", "theresource-2", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-2"), nil }) fakeClient.PrependWatchReactor("theresource-1", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-1")) return true, fakeWatch, nil }) fakeClient.PrependWatchReactor("theresource-2", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo-2")) return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, }, { name: "ignores watch error", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) count := 0 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() if count == 0 { fakeWatch.Error(newUnstructuredStatus(&metav1.Status{ TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"}, Status: "Failure", Code: 500, Message: "Bad", })) fakeWatch.Stop() } else { fakeWatch.Action(watch.Deleted, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")) } count++ return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeClient := test.fakeClient() o := &WaitOptions{ ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...), UIDMap: test.uidMap, DynamicClient: fakeClient, Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), ConditionFn: IsDeleted, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWait() switch { case err == nil && len(test.expectedErr) == 0: case err != nil && len(test.expectedErr) == 0: t.Fatal(err) case err == nil && len(test.expectedErr) != 0: t.Fatalf("missing: %q", test.expectedErr) case err != nil && len(test.expectedErr) != 0: if !strings.Contains(err.Error(), test.expectedErr) { t.Fatalf("expected %q, got %q", test.expectedErr, err.Error()) } } }) } } func TestWaitForCondition(t *testing.T) { scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList", } tests := []struct { name string infos []*resource.Info fakeClient func() *dynamicfakeclient.FakeDynamicClient timeout time.Duration expectedErr string }{ { name: "present on get", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(addCondition( newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), "the-condition", "status-value", )), nil }) return fakeClient }, timeout: 10 * time.Second, }, { name: "handles no infos", infos: []*resource.Info{}, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClient(scheme) }, timeout: 10 * time.Second, expectedErr: errNoMatchingResources.Error(), }, { name: "handles empty object name", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) }, timeout: 10 * time.Second, expectedErr: "resource name must be provided", }, { name: "times out", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, addCondition( newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), "some-other-condition", "status-value", ), nil }) return fakeClient }, timeout: 1 * time.Second, expectedErr: `theresource.group "name-foo" not found`, }, { name: "for nonexisting resource with no timeout", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thenonexistingresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) return fakeClient }, timeout: 0 * time.Second, expectedErr: "thenonexistingresource.group \"name-foo\" not found", }, { name: "for existing resource with no timeout", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("get", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) return fakeClient }, timeout: 0 * time.Second, expectedErr: "condition not met for theresource/name-foo", }, { name: "handles watch close out", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { unstructuredObj := newUnstructured("group/version", "TheKind", "ns-foo", "name-foo") unstructuredObj.SetResourceVersion("123") unstructuredList := newUnstructuredList(unstructuredObj) unstructuredList.SetResourceVersion("234") return true, unstructuredList, nil }) count := 0 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { if count == 0 { count++ fakeWatch := watch.NewRaceFreeFake() go func() { time.Sleep(1 * time.Second) fakeWatch.Stop() }() return true, fakeWatch, nil } fakeWatch := watch.NewRaceFreeFake() return true, fakeWatch, nil }) return fakeClient }, timeout: 3 * time.Second, expectedErr: "timed out waiting for the condition on theresource/name-foo", }, { name: "handles watch condition change", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() fakeWatch.Action(watch.Modified, addCondition( newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), "the-condition", "status-value", )) return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, }, { name: "handles watch created", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(addCondition( newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), "the-condition", "status-value", )), nil }) return fakeClient }, timeout: 1 * time.Second, }, { name: "ignores watch error", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "ns-foo", "name-foo")), nil }) count := 0 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() if count == 0 { fakeWatch.Error(newUnstructuredStatus(&metav1.Status{ TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"}, Status: "Failure", Code: 500, Message: "Bad", })) fakeWatch.Stop() } else { fakeWatch.Action(watch.Modified, addCondition( newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), "the-condition", "status-value", )) } count++ return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, }, { name: "times out due to stale .status.conditions[0].observedGeneration", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, addConditionWithObservedGeneration( newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value", 1, ), nil }) return fakeClient }, timeout: 1 * time.Second, expectedErr: `theresource.group "name-foo" not found`, }, { name: "handles watch .status.conditions[0].observedGeneration change", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(addConditionWithObservedGeneration(newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value", 1)), nil }) fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() fakeWatch.Action(watch.Modified, addConditionWithObservedGeneration( newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value", 2, )) return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, }, { name: "times out due to stale .status.observedGeneration", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { instance := addCondition( newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value") unstructured.SetNestedField(instance.Object, int64(1), "status", "observedGeneration") return true, instance, nil }) return fakeClient }, timeout: 1 * time.Second, expectedErr: `theresource.group "name-foo" not found`, }, { name: "handles watch .status.observedGeneration change", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { instance := addCondition( newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value") unstructured.SetNestedField(instance.Object, int64(1), "status", "observedGeneration") return true, newUnstructuredList(instance), nil }) fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { instance := addCondition( newUnstructuredWithGeneration("group/version", "TheKind", "ns-foo", "name-foo", 2), "the-condition", "status-value") unstructured.SetNestedField(instance.Object, int64(2), "status", "observedGeneration") fakeWatch := watch.NewRaceFreeFake() fakeWatch.Action(watch.Modified, instance) return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeClient := test.fakeClient() o := &WaitOptions{ ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...), DynamicClient: fakeClient, Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), ConditionFn: ConditionalWait{conditionName: "the-condition", conditionStatus: "status-value", errOut: io.Discard}.IsConditionMet, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWait() switch { case err == nil && len(test.expectedErr) == 0: case err != nil && len(test.expectedErr) == 0: t.Fatal(err) case err == nil && len(test.expectedErr) != 0: t.Fatalf("missing: %q", test.expectedErr) case err != nil && len(test.expectedErr) != 0: if !strings.Contains(err.Error(), test.expectedErr) { t.Fatalf("expected %q, got %q", test.expectedErr, err.Error()) } } }) } } func TestWaitForCreate(t *testing.T) { scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList", } tests := []struct { name string infos []*resource.Info infosErr error fakeClient func() *dynamicfakeclient.FakeDynamicClient timeout time.Duration expectedErr string }{ { name: "missing resource, should hit timeout", infosErr: apierrors.NewNotFound(schema.GroupResource{Group: "group", Resource: "theresource"}, "name-foo"), fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) }, timeout: 1 * time.Second, expectedErr: "timed out waiting for the condition", }, { name: "wait should succeed", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Object: &corev1.Pod{}, // the resource type is irrelevant here Name: "name-foo", Namespace: "ns-foo", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) }, timeout: 1 * time.Second, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeClient := test.fakeClient() o := &WaitOptions{ ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...).WithError(test.infosErr), DynamicClient: fakeClient, Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), ConditionFn: IsCreated, ForCondition: "create", IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWait() switch { case err == nil && len(test.expectedErr) == 0: case err != nil && len(test.expectedErr) == 0: t.Fatal(err) case err == nil && len(test.expectedErr) != 0: t.Fatalf("missing: %q", test.expectedErr) case err != nil && len(test.expectedErr) != 0: if !strings.Contains(err.Error(), test.expectedErr) { t.Fatalf("expected %q, got %q", test.expectedErr, err.Error()) } } }) } } func TestWaitForDeletionIgnoreNotFound(t *testing.T) { scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList", } infos := []*resource.Info{} fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) o := &WaitOptions{ ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(infos...), DynamicClient: fakeClient, Printer: printers.NewDiscardingPrinter(), ConditionFn: IsDeleted, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), ForCondition: "delete", } err := o.RunWait() if err != nil { t.Fatalf("unexpected error: %v", err) } } // TestWaitForDifferentJSONPathCondition will run tests on different types of // JSONPath expression to check the JSONPath can be parsed correctly from a Pod Yaml // and check if the comparison returns as expected. func TestWaitForDifferentJSONPathExpression(t *testing.T) { scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList", } listReactionfunc := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(createUnstructured(t, podYAML)), nil } infos := []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "foo-b6699dcfb-rnv7t", Namespace: "default", }, } tests := []struct { name string fakeClient func() *dynamicfakeclient.FakeDynamicClient jsonPathExp string jsonPathValue string matchAnyValue bool expectedErr string }{ { name: "JSONPath entry not exist", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.foo.bar}", jsonPathValue: "baz", matchAnyValue: false, expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", }, { name: "compare boolean JSONPath entry", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.status.containerStatuses[0].ready}", jsonPathValue: "true", matchAnyValue: false, expectedErr: None, }, { name: "compare boolean JSONPath entry wrong value", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.status.containerStatuses[0].ready}", jsonPathValue: "false", matchAnyValue: false, expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", }, { name: "compare integer JSONPath entry", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.spec.containers[0].ports[0].containerPort}", jsonPathValue: "80", matchAnyValue: false, expectedErr: None, }, { name: "compare integer JSONPath entry wrong value", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.spec.containers[0].ports[0].containerPort}", jsonPathValue: "81", matchAnyValue: false, expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", }, { name: "compare string JSONPath entry", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.spec.nodeName}", jsonPathValue: "knode0", matchAnyValue: false, expectedErr: None, }, { name: "matches literal value of JSONPath entry without value condition", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.spec.nodeName}", jsonPathValue: "", matchAnyValue: true, expectedErr: None, }, { name: "matches complex types map[string]interface{} without value condition", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(createUnstructured(t, podYAML)), nil }) return fakeClient }, jsonPathExp: "{.spec}", jsonPathValue: "", matchAnyValue: true, expectedErr: None, }, { name: "compare string JSONPath entry wrong value", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.spec.nodeName}", jsonPathValue: "kmaster", matchAnyValue: false, expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", }, { name: "matches more than one value", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.status.conditions[*]}", jsonPathValue: "foo", matchAnyValue: false, expectedErr: "given jsonpath expression matches more than one value", }, { name: "matches more than one value without value condition", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.status.conditions[*]}", jsonPathValue: "", matchAnyValue: true, expectedErr: "given jsonpath expression matches more than one value", }, { name: "matches more than one list", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}", jsonPathValue: "foo", matchAnyValue: false, expectedErr: "given jsonpath expression matches more than one list", }, { name: "matches more than one list without value condition", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{range .status.conditions[*]}[{.status}] {end}", jsonPathValue: "", matchAnyValue: true, expectedErr: "given jsonpath expression matches more than one list", }, { name: "unsupported type []interface{}", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", listReactionfunc) return fakeClient }, jsonPathExp: "{.status.conditions}", jsonPathValue: "True", matchAnyValue: false, expectedErr: "jsonpath leads to a nested object or list which is not supported", }, { name: "unsupported type map[string]interface{}", fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(createUnstructured(t, podYAML)), nil }) return fakeClient }, jsonPathExp: "{.spec}", jsonPathValue: "foo", matchAnyValue: false, expectedErr: "jsonpath leads to a nested object or list which is not supported", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeClient := test.fakeClient() j, _ := newJSONPathParser(test.jsonPathExp) o := &WaitOptions{ ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(infos...), DynamicClient: fakeClient, Timeout: 1 * time.Second, Printer: printers.NewDiscardingPrinter(), ConditionFn: JSONPathWait{ matchAnyValue: test.matchAnyValue, jsonPathValue: test.jsonPathValue, jsonPathParser: j, errOut: io.Discard}.IsJSONPathConditionMet, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWait() switch { case err == nil && len(test.expectedErr) == 0: case err != nil && len(test.expectedErr) == 0: t.Fatal(err) case err == nil && len(test.expectedErr) != 0: t.Fatalf("missing: %q", test.expectedErr) case err != nil && len(test.expectedErr) != 0: if !strings.Contains(err.Error(), test.expectedErr) { t.Fatalf("expected %q, got %q", test.expectedErr, err.Error()) } } }) } } // TestWaitForJSONPathCondition will run tests to check whether // the List actions and Watch actions match what we expected func TestWaitForJSONPathCondition(t *testing.T) { scheme := runtime.NewScheme() listMapping := map[schema.GroupVersionResource]string{ {Group: "group", Version: "version", Resource: "theresource"}: "TheKindList", } tests := []struct { name string infos []*resource.Info fakeClient func() *dynamicfakeclient.FakeDynamicClient timeout time.Duration jsonPathExp string jsonPathValue string expectedErr string }{ { name: "present on get", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "foo-b6699dcfb-rnv7t", Namespace: "default", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList( createUnstructured(t, podYAML)), nil }) return fakeClient }, timeout: 3 * time.Second, jsonPathExp: "{.metadata.name}", jsonPathValue: "foo-b6699dcfb-rnv7t", expectedErr: None, }, { name: "handles no infos", infos: []*resource.Info{}, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClient(scheme) }, timeout: 10 * time.Second, expectedErr: errNoMatchingResources.Error(), }, { name: "handles empty object name", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Namespace: "default", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { return dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) }, timeout: 10 * time.Second, expectedErr: "resource name must be provided", }, { name: "times out", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "foo-b6699dcfb-rnv7t", Namespace: "default", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, createUnstructured(t, podYAML), nil }) return fakeClient }, timeout: 1 * time.Second, expectedErr: `theresource.group "foo-b6699dcfb-rnv7t" not found`, }, { name: "handles watch close out", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "foo-b6699dcfb-rnv7t", Namespace: "default", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { unstructuredObj := createUnstructured(t, podYAML) unstructuredObj.SetResourceVersion("123") unstructuredList := newUnstructuredList(unstructuredObj) unstructuredList.SetResourceVersion("234") return true, unstructuredList, nil }) count := 0 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { if count == 0 { count++ fakeWatch := watch.NewRaceFreeFake() go func() { time.Sleep(1 * time.Second) fakeWatch.Stop() }() return true, fakeWatch, nil } fakeWatch := watch.NewRaceFreeFake() return true, fakeWatch, nil }) return fakeClient }, timeout: 3 * time.Second, jsonPathExp: "{.metadata.name}", jsonPathValue: "foo", // use incorrect name so it'll keep waiting expectedErr: "timed out waiting for the condition on theresource/foo-b6699dcfb-rnv7t", }, { name: "handles watch condition change", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "foo-b6699dcfb-rnv7t", Namespace: "default", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { unstructuredObj := createUnstructured(t, podYAML) unstructuredObj.SetName("foo") return true, newUnstructuredList(unstructuredObj), nil }) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { unstructuredObj := createUnstructured(t, podYAML) return true, newUnstructuredList(unstructuredObj), nil }) return fakeClient }, timeout: 10 * time.Second, jsonPathExp: "{.metadata.name}", jsonPathValue: "foo-b6699dcfb-rnv7t", expectedErr: None, }, { name: "handles watch created", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "foo-b6699dcfb-rnv7t", Namespace: "default", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList( createUnstructured(t, podYAML)), nil }) return fakeClient }, timeout: 1 * time.Second, jsonPathExp: "{.spec.containers[0].image}", jsonPathValue: "nginx", expectedErr: None, }, { name: "ignores watch error", infos: []*resource.Info{ { Mapping: &meta.RESTMapping{ Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "theresource"}, }, Name: "foo-b6699dcfb-rnv7t", Namespace: "default", }, }, fakeClient: func() *dynamicfakeclient.FakeDynamicClient { fakeClient := dynamicfakeclient.NewSimpleDynamicClientWithCustomListKinds(scheme, listMapping) fakeClient.PrependReactor("list", "theresource", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { return true, newUnstructuredList(newUnstructured("group/version", "TheKind", "default", "foo-b6699dcfb-rnv7t")), nil }) count := 0 fakeClient.PrependWatchReactor("theresource", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) { fakeWatch := watch.NewRaceFreeFake() if count == 0 { fakeWatch.Error(newUnstructuredStatus(&metav1.Status{ TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"}, Status: "Failure", Code: 500, Message: "Bad", })) fakeWatch.Stop() } else { fakeWatch.Action(watch.Modified, createUnstructured(t, podYAML)) } count++ return true, fakeWatch, nil }) return fakeClient }, timeout: 10 * time.Second, jsonPathExp: "{.metadata.name}", jsonPathValue: "foo-b6699dcfb-rnv7t", expectedErr: None, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fakeClient := test.fakeClient() j, _ := newJSONPathParser(test.jsonPathExp) o := &WaitOptions{ ResourceFinder: genericclioptions.NewSimpleFakeResourceFinder(test.infos...), DynamicClient: fakeClient, Timeout: test.timeout, Printer: printers.NewDiscardingPrinter(), ConditionFn: JSONPathWait{ jsonPathValue: test.jsonPathValue, jsonPathParser: j, errOut: io.Discard}.IsJSONPathConditionMet, IOStreams: genericiooptions.NewTestIOStreamsDiscard(), } err := o.RunWait() switch { case err == nil && len(test.expectedErr) == 0: case err != nil && len(test.expectedErr) == 0: t.Fatal(err) case err == nil && len(test.expectedErr) != 0: t.Fatalf("missing: %q", test.expectedErr) case err != nil && len(test.expectedErr) != 0: if !strings.Contains(err.Error(), test.expectedErr) { t.Fatalf("expected %q, got %q", test.expectedErr, err.Error()) } } }) } } // TestConditionFuncFor tests that the condition string can be properly parsed into a ConditionFunc. func TestConditionFuncFor(t *testing.T) { tests := []struct { name string condition string expectedErr string }{ { name: "jsonpath missing JSONPath expression", condition: "jsonpath=", expectedErr: "jsonpath expression cannot be empty", }, { name: "jsonpath check for condition without value", condition: "jsonpath={.metadata.name}", expectedErr: None, }, { name: "jsonpath check for condition without value relaxed parsing", condition: "jsonpath=abc", expectedErr: None, }, { name: "jsonpath check for expression and value", condition: "jsonpath={.metadata.name}=foo-b6699dcfb-rnv7t", expectedErr: None, }, { name: "jsonpath check for expression and value relaxed parsing", condition: "jsonpath=.metadata.name=foo-b6699dcfb-rnv7t", expectedErr: None, }, { name: "jsonpath selecting based on condition", condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}=True`, expectedErr: None, }, { name: "jsonpath selecting based on condition relaxed parsing", condition: "jsonpath=status.conditions[?(@.type==\"Available\")].status=True", expectedErr: None, }, { name: "jsonpath selecting based on condition without value", condition: `jsonpath={.status.containerStatuses[?(@.name=="foo")].ready}`, expectedErr: None, }, { name: "jsonpath selecting based on condition without value relaxed parsing", condition: `jsonpath=.status.containerStatuses[?(@.name=="foo")].ready`, expectedErr: None, }, { name: "jsonpath invalid expression with repeated '='", condition: "jsonpath={.metadata.name}='test=wrong'", expectedErr: "jsonpath wait format must be --for=jsonpath='{.status.readyReplicas}'=3 or --for=jsonpath='{.status.readyReplicas}'", }, { name: "jsonpath undefined value after '='", condition: "jsonpath={.metadata.name}=", expectedErr: "jsonpath wait has to have a value after equal sign", }, { name: "jsonpath complex expressions not supported", condition: "jsonpath={.status.conditions[?(@.type==\"Failed\"||@.type==\"Complete\")].status}=True", expectedErr: "unrecognized character in action: U+007C '|'", }, { name: "jsonpath invalid expression", condition: "jsonpath={=True", expectedErr: "unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or " + "'{.name1.name2}'", }, { name: "condition delete", condition: "delete", expectedErr: None, }, { name: "condition true", condition: "condition=hello", expectedErr: None, }, { name: "condition with value", condition: "condition=hello=world", expectedErr: None, }, { name: "unrecognized condition", condition: "cond=invalid", expectedErr: "unrecognized condition: \"cond=invalid\"", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := conditionFuncFor(test.condition, io.Discard) switch { case err == nil && test.expectedErr != None: t.Fatalf("expected error %q, got nil", test.expectedErr) case err != nil && test.expectedErr == None: t.Fatalf("expected no error, got %q", err) case err != nil && test.expectedErr != None: if !strings.Contains(err.Error(), test.expectedErr) { t.Fatalf("expected error %q, got %q", test.expectedErr, err.Error()) } } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/000077500000000000000000000000001504711711200253115ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/OWNERS000066400000000000000000000003651504711711200262550ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # Disable inheritance as this is an api owners file options: no_parent_owners: true approvers: - api-approvers reviewers: - api-reviewers - sig-cli-reviewers labels: - kind/api-change kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/doc.go000066400000000000000000000013011504711711200264000ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. 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. */ // +k8s:deepcopy-gen=package // +groupName=kubectl.config.k8s.io package config // Package config import "k8s.io/kubectl/pkg/config" kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/install/000077500000000000000000000000001504711711200267575ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/install/install.go000066400000000000000000000021021504711711200307470ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package install installs the experimental API group, making it available as // an option to all of the API encoding/decoding machinery. package install import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/kubectl/pkg/config" "k8s.io/kubectl/pkg/config/v1alpha1" ) // Install registers the API group and adds types to a scheme func Install(scheme *runtime.Scheme) { utilruntime.Must(config.AddToScheme(scheme)) utilruntime.Must(v1alpha1.AddToScheme(scheme)) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/register.go000066400000000000000000000026141504711711200274670ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // GroupName is the group name used in this package const GroupName = "kubectl.config.k8s.io" // SchemeGroupVersion is group version used to register these objects var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} var ( // SchemeBuilder is the scheme builder with scheme init functions to run for this API package SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme is a global function that registers this API group & version to a scheme AddToScheme = SchemeBuilder.AddToScheme ) // addKnownTypes registers known types to the given scheme func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Preference{}, ) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/types.go000066400000000000000000000071671504711711200270170ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Preference stores elements of KubeRC configuration file type Preference struct { metav1.TypeMeta // overrides allows changing default flag values of commands. // This is especially useful, when user doesn't want to explicitly // set flags each time. // +optional Overrides []CommandOverride // aliases allows defining command aliases for existing kubectl commands, with optional default flag values. // If the alias name collides with a built-in command, built-in command always takes precedence. // Flag overrides defined in the overrides section do NOT apply to aliases for the same command. // kubectl [ALIAS NAME] [USER_FLAGS] [USER_EXPLICIT_ARGS] expands to // kubectl [COMMAND] # built-in command alias points to // [KUBERC_PREPEND_ARGS] // [USER_FLAGS] // [KUBERC_FLAGS] # rest of the flags that are not passed by user in [USER_FLAGS] // [USER_EXPLICIT_ARGS] // [KUBERC_APPEND_ARGS] // e.g. // - name: runx // command: run // flags: // - name: image // default: nginx // appendArgs: // - -- // - custom-arg1 // For example, if user invokes "kubectl runx test-pod" command, // this will be expanded to "kubectl run --image=nginx test-pod -- custom-arg1" // - name: getn // command: get // flags: // - name: output // default: wide // prependArgs: // - node // "kubectl getn control-plane-1" expands to "kubectl get node control-plane-1 --output=wide" // "kubectl getn control-plane-1 --output=json" expands to "kubectl get node --output=json control-plane-1" // +optional Aliases []AliasOverride } // AliasOverride stores the alias definitions. type AliasOverride struct { // Name is the name of alias that can only include alphabetical characters // If the alias name conflicts with the built-in command, // built-in command will be used. Name string // Command is the single or set of commands to execute, such as "set env" or "create" Command string // PrependArgs stores the arguments such as resource names, etc. // These arguments are inserted after the alias name. PrependArgs []string // AppendArgs stores the arguments such as resource names, etc. // These arguments are appended to the USER_ARGS. AppendArgs []string // Flag is allocated to store the flag definitions of alias Flags []CommandOverrideFlag } // CommandOverride stores the commands and their associated flag's // default values. type CommandOverride struct { // Command refers to a command whose flag's default value is changed. Command string // Flags is a list of flags storing different default values. Flags []CommandOverrideFlag } // CommandOverrideFlag stores the name and the specified default // value of the flag. type CommandOverrideFlag struct { // Flag name (long form, without dashes). Name string `json:"name"` // In a string format of a default value. It will be parsed // by kubectl to the compatible value of the flag. Default string `json:"default"` } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/000077500000000000000000000000001504711711200267265ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/doc.go000066400000000000000000000014671504711711200300320ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. 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. */ // +k8s:deepcopy-gen=package // +k8s:openapi-gen=true // +groupName=kubectl.config.k8s.io // +k8s:conversion-gen=k8s.io/kubectl/pkg/config // +k8s:defaulter-gen=TypeMeta package v1alpha1 // Package v1alpha1 import "k8s.io/kubectl/pkg/config/v1alpha1" kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/register.go000066400000000000000000000030041504711711200310760ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v1alpha1 import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // GroupName is the group name used in this package const GroupName = "kubectl.config.k8s.io" // SchemeGroupVersion is group version used to register these objects var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} var ( SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder AddToScheme = localSchemeBuilder.AddToScheme ) func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. localSchemeBuilder.Register(addKnownTypes) } // addKnownTypes registers known types to the given scheme func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Preference{}, ) return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/v1alpha1/types.go000066400000000000000000000100341504711711200304170ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v1alpha1 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Preference stores elements of KubeRC configuration file type Preference struct { metav1.TypeMeta `json:",inline"` // overrides allows changing default flag values of commands. // This is especially useful, when user doesn't want to explicitly // set flags each time. // +listType=atomic Overrides []CommandOverride `json:"overrides"` // aliases allows defining command aliases for existing kubectl commands, with optional default flag values. // If the alias name collides with a built-in command, built-in command always takes precedence. // Flag overrides defined in the overrides section do NOT apply to aliases for the same command. // kubectl [ALIAS NAME] [USER_FLAGS] [USER_EXPLICIT_ARGS] expands to // kubectl [COMMAND] # built-in command alias points to // [KUBERC_PREPEND_ARGS] // [USER_FLAGS] // [KUBERC_FLAGS] # rest of the flags that are not passed by user in [USER_FLAGS] // [USER_EXPLICIT_ARGS] // [KUBERC_APPEND_ARGS] // e.g. // - name: runx // command: run // flags: // - name: image // default: nginx // appendArgs: // - -- // - custom-arg1 // For example, if user invokes "kubectl runx test-pod" command, // this will be expanded to "kubectl run --image=nginx test-pod -- custom-arg1" // - name: getn // command: get // flags: // - name: output // default: wide // prependArgs: // - node // "kubectl getn control-plane-1" expands to "kubectl get node control-plane-1 --output=wide" // "kubectl getn control-plane-1 --output=json" expands to "kubectl get node --output=json control-plane-1" // +listType=atomic Aliases []AliasOverride `json:"aliases"` } // AliasOverride stores the alias definitions. type AliasOverride struct { // Name is the name of alias that can only include alphabetical characters // If the alias name conflicts with the built-in command, // built-in command will be used. Name string `json:"name"` // Command is the single or set of commands to execute, such as "set env" or "create" Command string `json:"command"` // PrependArgs stores the arguments such as resource names, etc. // These arguments are inserted after the alias name. // +listType=atomic PrependArgs []string `json:"prependArgs,omitempty"` // AppendArgs stores the arguments such as resource names, etc. // These arguments are appended to the USER_ARGS. // +listType=atomic AppendArgs []string `json:"appendArgs,omitempty"` // Flag is allocated to store the flag definitions of alias. // Flag only modifies the default value of the flag and if // user explicitly passes a value, explicit one is used. // +listType=atomic Flags []CommandOverrideFlag `json:"flags,omitempty"` } // CommandOverride stores the commands and their associated flag's // default values. type CommandOverride struct { // Command refers to a command whose flag's default value is changed. Command string `json:"command"` // Flags is a list of flags storing different default values. // +listType=atomic Flags []CommandOverrideFlag `json:"flags"` } // CommandOverrideFlag stores the name and the specified default // value of the flag. type CommandOverrideFlag struct { // Flag name (long form, without dashes). Name string `json:"name"` // In a string format of a default value. It will be parsed // by kubectl to the compatible value of the flag. Default string `json:"default"` } zz_generated.conversion.go000066400000000000000000000204451504711711200340500ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/v1alpha1//go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright The Kubernetes Authors. 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. */ // Code generated by conversion-gen. DO NOT EDIT. package v1alpha1 import ( unsafe "unsafe" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" config "k8s.io/kubectl/pkg/config" ) func init() { localSchemeBuilder.Register(RegisterConversions) } // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { if err := s.AddGeneratedConversionFunc((*AliasOverride)(nil), (*config.AliasOverride)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_AliasOverride_To_config_AliasOverride(a.(*AliasOverride), b.(*config.AliasOverride), scope) }); err != nil { return err } if err := s.AddGeneratedConversionFunc((*config.AliasOverride)(nil), (*AliasOverride)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_config_AliasOverride_To_v1alpha1_AliasOverride(a.(*config.AliasOverride), b.(*AliasOverride), scope) }); err != nil { return err } if err := s.AddGeneratedConversionFunc((*CommandOverride)(nil), (*config.CommandOverride)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_CommandOverride_To_config_CommandOverride(a.(*CommandOverride), b.(*config.CommandOverride), scope) }); err != nil { return err } if err := s.AddGeneratedConversionFunc((*config.CommandOverride)(nil), (*CommandOverride)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_config_CommandOverride_To_v1alpha1_CommandOverride(a.(*config.CommandOverride), b.(*CommandOverride), scope) }); err != nil { return err } if err := s.AddGeneratedConversionFunc((*CommandOverrideFlag)(nil), (*config.CommandOverrideFlag)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(a.(*CommandOverrideFlag), b.(*config.CommandOverrideFlag), scope) }); err != nil { return err } if err := s.AddGeneratedConversionFunc((*config.CommandOverrideFlag)(nil), (*CommandOverrideFlag)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(a.(*config.CommandOverrideFlag), b.(*CommandOverrideFlag), scope) }); err != nil { return err } if err := s.AddGeneratedConversionFunc((*Preference)(nil), (*config.Preference)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_Preference_To_config_Preference(a.(*Preference), b.(*config.Preference), scope) }); err != nil { return err } if err := s.AddGeneratedConversionFunc((*config.Preference)(nil), (*Preference)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_config_Preference_To_v1alpha1_Preference(a.(*config.Preference), b.(*Preference), scope) }); err != nil { return err } return nil } func autoConvert_v1alpha1_AliasOverride_To_config_AliasOverride(in *AliasOverride, out *config.AliasOverride, s conversion.Scope) error { out.Name = in.Name out.Command = in.Command out.PrependArgs = *(*[]string)(unsafe.Pointer(&in.PrependArgs)) out.AppendArgs = *(*[]string)(unsafe.Pointer(&in.AppendArgs)) out.Flags = *(*[]config.CommandOverrideFlag)(unsafe.Pointer(&in.Flags)) return nil } // Convert_v1alpha1_AliasOverride_To_config_AliasOverride is an autogenerated conversion function. func Convert_v1alpha1_AliasOverride_To_config_AliasOverride(in *AliasOverride, out *config.AliasOverride, s conversion.Scope) error { return autoConvert_v1alpha1_AliasOverride_To_config_AliasOverride(in, out, s) } func autoConvert_config_AliasOverride_To_v1alpha1_AliasOverride(in *config.AliasOverride, out *AliasOverride, s conversion.Scope) error { out.Name = in.Name out.Command = in.Command out.PrependArgs = *(*[]string)(unsafe.Pointer(&in.PrependArgs)) out.AppendArgs = *(*[]string)(unsafe.Pointer(&in.AppendArgs)) out.Flags = *(*[]CommandOverrideFlag)(unsafe.Pointer(&in.Flags)) return nil } // Convert_config_AliasOverride_To_v1alpha1_AliasOverride is an autogenerated conversion function. func Convert_config_AliasOverride_To_v1alpha1_AliasOverride(in *config.AliasOverride, out *AliasOverride, s conversion.Scope) error { return autoConvert_config_AliasOverride_To_v1alpha1_AliasOverride(in, out, s) } func autoConvert_v1alpha1_CommandOverride_To_config_CommandOverride(in *CommandOverride, out *config.CommandOverride, s conversion.Scope) error { out.Command = in.Command out.Flags = *(*[]config.CommandOverrideFlag)(unsafe.Pointer(&in.Flags)) return nil } // Convert_v1alpha1_CommandOverride_To_config_CommandOverride is an autogenerated conversion function. func Convert_v1alpha1_CommandOverride_To_config_CommandOverride(in *CommandOverride, out *config.CommandOverride, s conversion.Scope) error { return autoConvert_v1alpha1_CommandOverride_To_config_CommandOverride(in, out, s) } func autoConvert_config_CommandOverride_To_v1alpha1_CommandOverride(in *config.CommandOverride, out *CommandOverride, s conversion.Scope) error { out.Command = in.Command out.Flags = *(*[]CommandOverrideFlag)(unsafe.Pointer(&in.Flags)) return nil } // Convert_config_CommandOverride_To_v1alpha1_CommandOverride is an autogenerated conversion function. func Convert_config_CommandOverride_To_v1alpha1_CommandOverride(in *config.CommandOverride, out *CommandOverride, s conversion.Scope) error { return autoConvert_config_CommandOverride_To_v1alpha1_CommandOverride(in, out, s) } func autoConvert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(in *CommandOverrideFlag, out *config.CommandOverrideFlag, s conversion.Scope) error { out.Name = in.Name out.Default = in.Default return nil } // Convert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag is an autogenerated conversion function. func Convert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(in *CommandOverrideFlag, out *config.CommandOverrideFlag, s conversion.Scope) error { return autoConvert_v1alpha1_CommandOverrideFlag_To_config_CommandOverrideFlag(in, out, s) } func autoConvert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(in *config.CommandOverrideFlag, out *CommandOverrideFlag, s conversion.Scope) error { out.Name = in.Name out.Default = in.Default return nil } // Convert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag is an autogenerated conversion function. func Convert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(in *config.CommandOverrideFlag, out *CommandOverrideFlag, s conversion.Scope) error { return autoConvert_config_CommandOverrideFlag_To_v1alpha1_CommandOverrideFlag(in, out, s) } func autoConvert_v1alpha1_Preference_To_config_Preference(in *Preference, out *config.Preference, s conversion.Scope) error { out.Overrides = *(*[]config.CommandOverride)(unsafe.Pointer(&in.Overrides)) out.Aliases = *(*[]config.AliasOverride)(unsafe.Pointer(&in.Aliases)) return nil } // Convert_v1alpha1_Preference_To_config_Preference is an autogenerated conversion function. func Convert_v1alpha1_Preference_To_config_Preference(in *Preference, out *config.Preference, s conversion.Scope) error { return autoConvert_v1alpha1_Preference_To_config_Preference(in, out, s) } func autoConvert_config_Preference_To_v1alpha1_Preference(in *config.Preference, out *Preference, s conversion.Scope) error { out.Overrides = *(*[]CommandOverride)(unsafe.Pointer(&in.Overrides)) out.Aliases = *(*[]AliasOverride)(unsafe.Pointer(&in.Aliases)) return nil } // Convert_config_Preference_To_v1alpha1_Preference is an autogenerated conversion function. func Convert_config_Preference_To_v1alpha1_Preference(in *config.Preference, out *Preference, s conversion.Scope) error { return autoConvert_config_Preference_To_v1alpha1_Preference(in, out, s) } zz_generated.deepcopy.go000066400000000000000000000071621504711711200334740ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/v1alpha1//go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright The Kubernetes Authors. 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. */ // Code generated by deepcopy-gen. DO NOT EDIT. package v1alpha1 import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AliasOverride) DeepCopyInto(out *AliasOverride) { *out = *in if in.PrependArgs != nil { in, out := &in.PrependArgs, &out.PrependArgs *out = make([]string, len(*in)) copy(*out, *in) } if in.AppendArgs != nil { in, out := &in.AppendArgs, &out.AppendArgs *out = make([]string, len(*in)) copy(*out, *in) } if in.Flags != nil { in, out := &in.Flags, &out.Flags *out = make([]CommandOverrideFlag, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AliasOverride. func (in *AliasOverride) DeepCopy() *AliasOverride { if in == nil { return nil } out := new(AliasOverride) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommandOverride) DeepCopyInto(out *CommandOverride) { *out = *in if in.Flags != nil { in, out := &in.Flags, &out.Flags *out = make([]CommandOverrideFlag, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverride. func (in *CommandOverride) DeepCopy() *CommandOverride { if in == nil { return nil } out := new(CommandOverride) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommandOverrideFlag) DeepCopyInto(out *CommandOverrideFlag) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverrideFlag. func (in *CommandOverrideFlag) DeepCopy() *CommandOverrideFlag { if in == nil { return nil } out := new(CommandOverrideFlag) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Preference) DeepCopyInto(out *Preference) { *out = *in out.TypeMeta = in.TypeMeta if in.Overrides != nil { in, out := &in.Overrides, &out.Overrides *out = make([]CommandOverride, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.Aliases != nil { in, out := &in.Aliases, &out.Aliases *out = make([]AliasOverride, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Preference. func (in *Preference) DeepCopy() *Preference { if in == nil { return nil } out := new(Preference) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Preference) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } zz_generated.defaults.go000066400000000000000000000017651504711711200334760ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/v1alpha1//go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright The Kubernetes Authors. 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. */ // Code generated by defaulter-gen. DO NOT EDIT. package v1alpha1 import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // RegisterDefaults adds defaulters functions to the given scheme. // Public to allow building arbitrary schemes. // All generated defaulters are covering - they call all nested defaulters. func RegisterDefaults(scheme *runtime.Scheme) error { return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/config/zz_generated.deepcopy.go000066400000000000000000000071601504711711200321340ustar00rootroot00000000000000//go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright The Kubernetes Authors. 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. */ // Code generated by deepcopy-gen. DO NOT EDIT. package config import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AliasOverride) DeepCopyInto(out *AliasOverride) { *out = *in if in.PrependArgs != nil { in, out := &in.PrependArgs, &out.PrependArgs *out = make([]string, len(*in)) copy(*out, *in) } if in.AppendArgs != nil { in, out := &in.AppendArgs, &out.AppendArgs *out = make([]string, len(*in)) copy(*out, *in) } if in.Flags != nil { in, out := &in.Flags, &out.Flags *out = make([]CommandOverrideFlag, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AliasOverride. func (in *AliasOverride) DeepCopy() *AliasOverride { if in == nil { return nil } out := new(AliasOverride) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommandOverride) DeepCopyInto(out *CommandOverride) { *out = *in if in.Flags != nil { in, out := &in.Flags, &out.Flags *out = make([]CommandOverrideFlag, len(*in)) copy(*out, *in) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverride. func (in *CommandOverride) DeepCopy() *CommandOverride { if in == nil { return nil } out := new(CommandOverride) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommandOverrideFlag) DeepCopyInto(out *CommandOverrideFlag) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommandOverrideFlag. func (in *CommandOverrideFlag) DeepCopy() *CommandOverrideFlag { if in == nil { return nil } out := new(CommandOverrideFlag) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Preference) DeepCopyInto(out *Preference) { *out = *in out.TypeMeta = in.TypeMeta if in.Overrides != nil { in, out := &in.Overrides, &out.Overrides *out = make([]CommandOverride, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.Aliases != nil { in, out := &in.Aliases, &out.Aliases *out = make([]AliasOverride, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Preference. func (in *Preference) DeepCopy() *Preference { if in == nil { return nil } out := new(Preference) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Preference) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/describe/000077500000000000000000000000001504711711200256245ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/describe/describe.go000066400000000000000000006117521504711711200277470ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package describe import ( "bytes" "context" "crypto/x509" "fmt" "io" "net" "net/url" "reflect" "sort" "strconv" "strings" "text/tabwriter" "time" "unicode" "github.com/fatih/camelcase" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" certificatesv1beta1 "k8s.io/api/certificates/v1beta1" coordinationv1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" policyv1 "k8s.io/api/policy/v1" policyv1beta1 "k8s.io/api/policy/v1beta1" rbacv1 "k8s.io/api/rbac/v1" schedulingv1 "k8s.io/api/scheduling/v1" storagev1 "k8s.io/api/storage/v1" storagev1beta1 "k8s.io/api/storage/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" runtimeresource "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" clientset "k8s.io/client-go/kubernetes" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/reference" utilcsr "k8s.io/client-go/util/certificate/csr" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/certificate" deploymentutil "k8s.io/kubectl/pkg/util/deployment" "k8s.io/kubectl/pkg/util/event" "k8s.io/kubectl/pkg/util/fieldpath" "k8s.io/kubectl/pkg/util/qos" "k8s.io/kubectl/pkg/util/rbac" resourcehelper "k8s.io/kubectl/pkg/util/resource" "k8s.io/kubectl/pkg/util/slice" storageutil "k8s.io/kubectl/pkg/util/storage" ) // Each level has 2 spaces for PrefixWriter const ( LEVEL_0 = iota LEVEL_1 LEVEL_2 LEVEL_3 LEVEL_4 ) var ( // globally skipped annotations skipAnnotations = sets.New[string](corev1.LastAppliedConfigAnnotation) // DescriberFn gives a way to easily override the function for unit testing if needed DescriberFn DescriberFunc = Describer ) // Describer returns a Describer for displaying the specified RESTMapping type or an error. func Describer(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (ResourceDescriber, error) { clientConfig, err := restClientGetter.ToRESTConfig() if err != nil { return nil, err } // try to get a describer if describer, ok := DescriberFor(mapping.GroupVersionKind.GroupKind(), clientConfig); ok { return describer, nil } // if this is a kind we don't have a describer for yet, go generic if possible if genericDescriber, ok := GenericDescriberFor(mapping, clientConfig); ok { return genericDescriber, nil } // otherwise return an unregistered error return nil, fmt.Errorf("no description has been implemented for %s", mapping.GroupVersionKind.String()) } // PrefixWriter can write text at various indentation levels. type PrefixWriter interface { // Write writes text with the specified indentation level. Write(level int, format string, a ...interface{}) // WriteLine writes an entire line with no indentation level. WriteLine(a ...interface{}) // Flush forces indentation to be reset. Flush() } // prefixWriter implements PrefixWriter type prefixWriter struct { out io.Writer } var _ PrefixWriter = &prefixWriter{} // NewPrefixWriter creates a new PrefixWriter. func NewPrefixWriter(out io.Writer) PrefixWriter { return &prefixWriter{out: out} } func (pw *prefixWriter) Write(level int, format string, a ...interface{}) { levelSpace := " " prefix := "" for i := 0; i < level; i++ { prefix += levelSpace } output := fmt.Sprintf(prefix+format, a...) printers.WriteEscaped(pw.out, output) } func (pw *prefixWriter) WriteLine(a ...interface{}) { output := fmt.Sprintln(a...) printers.WriteEscaped(pw.out, output) } func (pw *prefixWriter) Flush() { if f, ok := pw.out.(flusher); ok { f.Flush() } } // nestedPrefixWriter implements PrefixWriter by increasing the level // before passing text on to some other writer. type nestedPrefixWriter struct { PrefixWriter indent int } var _ PrefixWriter = &prefixWriter{} // NewPrefixWriter creates a new PrefixWriter. func NewNestedPrefixWriter(out PrefixWriter, indent int) PrefixWriter { return &nestedPrefixWriter{PrefixWriter: out, indent: indent} } func (npw *nestedPrefixWriter) Write(level int, format string, a ...interface{}) { npw.PrefixWriter.Write(level+npw.indent, format, a...) } func (npw *nestedPrefixWriter) WriteLine(a ...interface{}) { npw.PrefixWriter.Write(npw.indent, "%s", fmt.Sprintln(a...)) } func describerMap(clientConfig *rest.Config) (map[schema.GroupKind]ResourceDescriber, error) { c, err := clientset.NewForConfig(clientConfig) if err != nil { return nil, err } m := map[schema.GroupKind]ResourceDescriber{ {Group: corev1.GroupName, Kind: "Pod"}: &PodDescriber{c}, {Group: corev1.GroupName, Kind: "ReplicationController"}: &ReplicationControllerDescriber{c}, {Group: corev1.GroupName, Kind: "Secret"}: &SecretDescriber{c}, {Group: corev1.GroupName, Kind: "Service"}: &ServiceDescriber{c}, {Group: corev1.GroupName, Kind: "ServiceAccount"}: &ServiceAccountDescriber{c}, {Group: corev1.GroupName, Kind: "Node"}: &NodeDescriber{c}, {Group: corev1.GroupName, Kind: "LimitRange"}: &LimitRangeDescriber{c}, {Group: corev1.GroupName, Kind: "ResourceQuota"}: &ResourceQuotaDescriber{c}, {Group: corev1.GroupName, Kind: "PersistentVolume"}: &PersistentVolumeDescriber{c}, {Group: corev1.GroupName, Kind: "PersistentVolumeClaim"}: &PersistentVolumeClaimDescriber{c}, {Group: corev1.GroupName, Kind: "Namespace"}: &NamespaceDescriber{c}, {Group: corev1.GroupName, Kind: "Endpoints"}: &EndpointsDescriber{c}, {Group: corev1.GroupName, Kind: "ConfigMap"}: &ConfigMapDescriber{c}, {Group: corev1.GroupName, Kind: "PriorityClass"}: &PriorityClassDescriber{c}, {Group: discoveryv1beta1.GroupName, Kind: "EndpointSlice"}: &EndpointSliceDescriber{c}, {Group: discoveryv1.GroupName, Kind: "EndpointSlice"}: &EndpointSliceDescriber{c}, {Group: autoscalingv2.GroupName, Kind: "HorizontalPodAutoscaler"}: &HorizontalPodAutoscalerDescriber{c}, {Group: extensionsv1beta1.GroupName, Kind: "Ingress"}: &IngressDescriber{c}, {Group: networkingv1beta1.GroupName, Kind: "Ingress"}: &IngressDescriber{c}, {Group: networkingv1beta1.GroupName, Kind: "IngressClass"}: &IngressClassDescriber{c}, {Group: networkingv1.GroupName, Kind: "Ingress"}: &IngressDescriber{c}, {Group: networkingv1.GroupName, Kind: "IngressClass"}: &IngressClassDescriber{c}, {Group: networkingv1beta1.GroupName, Kind: "ServiceCIDR"}: &ServiceCIDRDescriber{c}, {Group: networkingv1beta1.GroupName, Kind: "IPAddress"}: &IPAddressDescriber{c}, {Group: networkingv1.GroupName, Kind: "ServiceCIDR"}: &ServiceCIDRDescriber{c}, {Group: networkingv1.GroupName, Kind: "IPAddress"}: &IPAddressDescriber{c}, {Group: batchv1.GroupName, Kind: "Job"}: &JobDescriber{c}, {Group: batchv1.GroupName, Kind: "CronJob"}: &CronJobDescriber{c}, {Group: batchv1beta1.GroupName, Kind: "CronJob"}: &CronJobDescriber{c}, {Group: appsv1.GroupName, Kind: "StatefulSet"}: &StatefulSetDescriber{c}, {Group: appsv1.GroupName, Kind: "Deployment"}: &DeploymentDescriber{c}, {Group: appsv1.GroupName, Kind: "DaemonSet"}: &DaemonSetDescriber{c}, {Group: appsv1.GroupName, Kind: "ReplicaSet"}: &ReplicaSetDescriber{c}, {Group: certificatesv1beta1.GroupName, Kind: "CertificateSigningRequest"}: &CertificateSigningRequestDescriber{c}, {Group: storagev1.GroupName, Kind: "StorageClass"}: &StorageClassDescriber{c}, {Group: storagev1.GroupName, Kind: "CSINode"}: &CSINodeDescriber{c}, {Group: storagev1beta1.GroupName, Kind: "VolumeAttributesClass"}: &VolumeAttributesClassDescriber{c}, {Group: policyv1beta1.GroupName, Kind: "PodDisruptionBudget"}: &PodDisruptionBudgetDescriber{c}, {Group: policyv1.GroupName, Kind: "PodDisruptionBudget"}: &PodDisruptionBudgetDescriber{c}, {Group: rbacv1.GroupName, Kind: "Role"}: &RoleDescriber{c}, {Group: rbacv1.GroupName, Kind: "ClusterRole"}: &ClusterRoleDescriber{c}, {Group: rbacv1.GroupName, Kind: "RoleBinding"}: &RoleBindingDescriber{c}, {Group: rbacv1.GroupName, Kind: "ClusterRoleBinding"}: &ClusterRoleBindingDescriber{c}, {Group: networkingv1.GroupName, Kind: "NetworkPolicy"}: &NetworkPolicyDescriber{c}, {Group: schedulingv1.GroupName, Kind: "PriorityClass"}: &PriorityClassDescriber{c}, } return m, nil } // DescriberFor returns the default describe functions for each of the standard // Kubernetes types. func DescriberFor(kind schema.GroupKind, clientConfig *rest.Config) (ResourceDescriber, bool) { describers, err := describerMap(clientConfig) if err != nil { klog.V(1).Info(err) return nil, false } f, ok := describers[kind] return f, ok } // GenericDescriberFor returns a generic describer for the specified mapping // that uses only information available from runtime.Unstructured func GenericDescriberFor(mapping *meta.RESTMapping, clientConfig *rest.Config) (ResourceDescriber, bool) { // used to fetch the resource dynamicClient, err := dynamic.NewForConfig(clientConfig) if err != nil { return nil, false } // used to get events for the resource clientSet, err := clientset.NewForConfig(clientConfig) if err != nil { return nil, false } eventsClient := clientSet.CoreV1() return &genericDescriber{mapping, dynamicClient, eventsClient}, true } type genericDescriber struct { mapping *meta.RESTMapping dynamic dynamic.Interface events corev1client.EventsGetter } func (g *genericDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (output string, err error) { obj, err := g.dynamic.Resource(g.mapping.Resource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(g.events, obj, describerSettings.ChunkSize) } return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", obj.GetName()) w.Write(LEVEL_0, "Namespace:\t%s\n", obj.GetNamespace()) printLabelsMultiline(w, "Labels", obj.GetLabels()) printAnnotationsMultiline(w, "Annotations", obj.GetAnnotations()) printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".metadata.managedFields", ".metadata.name", ".metadata.namespace", ".metadata.labels", ".metadata.annotations") if events != nil { DescribeEvents(events, w) } return nil }) } func printUnstructuredContent(w PrefixWriter, level int, content map[string]interface{}, skipPrefix string, skip ...string) { fields := []string{} for field := range content { fields = append(fields, field) } sort.Strings(fields) for _, field := range fields { value := content[field] switch typedValue := value.(type) { case map[string]interface{}: skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field) if slice.Contains[string](skip, skipExpr, nil) { continue } w.Write(level, "%s:\n", smartLabelFor(field)) printUnstructuredContent(w, level+1, typedValue, skipExpr, skip...) case []interface{}: skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field) if slice.Contains[string](skip, skipExpr, nil) { continue } w.Write(level, "%s:\n", smartLabelFor(field)) for _, child := range typedValue { switch typedChild := child.(type) { case map[string]interface{}: printUnstructuredContent(w, level+1, typedChild, skipExpr, skip...) default: w.Write(level+1, "%v\n", typedChild) } } default: skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field) if slice.Contains[string](skip, skipExpr, nil) { continue } w.Write(level, "%s:\t%v\n", smartLabelFor(field), typedValue) } } } func smartLabelFor(field string) string { // skip creating smart label if field name contains // special characters other than '-' if strings.IndexFunc(field, func(r rune) bool { return !unicode.IsLetter(r) && r != '-' }) != -1 { return field } commonAcronyms := []string{"API", "URL", "UID", "OSB", "GUID"} parts := camelcase.Split(field) result := make([]string, 0, len(parts)) for _, part := range parts { if part == "_" { continue } if slice.Contains[string](commonAcronyms, strings.ToUpper(part), nil) { part = strings.ToUpper(part) } else { part = strings.Title(part) } result = append(result, part) } return strings.Join(result, " ") } // DefaultObjectDescriber can describe the default Kubernetes objects. var DefaultObjectDescriber ObjectDescriber func init() { d := &Describers{} err := d.Add( describeCertificateSigningRequest, describeCronJob, describeCSINode, describeDaemonSet, describeDeployment, describeEndpoints, describeEndpointSliceV1, describeEndpointSliceV1beta1, describeHorizontalPodAutoscalerV1, describeHorizontalPodAutoscalerV2, describeJob, describeLimitRange, describeNamespace, describeNetworkPolicy, describeNode, describePersistentVolume, describePersistentVolumeClaim, describePod, describePodDisruptionBudgetV1, describePodDisruptionBudgetV1beta1, describePriorityClass, describeQuota, describeReplicaSet, describeReplicationController, describeSecret, describeService, describeServiceAccount, describeStatefulSet, describeStorageClass, describeVolumeAttributesClass, ) if err != nil { klog.Fatalf("Cannot register describers: %v", err) } DefaultObjectDescriber = d } // NamespaceDescriber generates information about a namespace type NamespaceDescriber struct { clientset.Interface } func (d *NamespaceDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { ns, err := d.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } resourceQuotaList := &corev1.ResourceQuotaList{} err = runtimeresource.FollowContinue(&metav1.ListOptions{Limit: describerSettings.ChunkSize}, func(options metav1.ListOptions) (runtime.Object, error) { newList, err := d.CoreV1().ResourceQuotas(name).List(context.TODO(), options) if err != nil { return nil, runtimeresource.EnhanceListError(err, options, corev1.ResourceQuotas.String()) } resourceQuotaList.Items = append(resourceQuotaList.Items, newList.Items...) return newList, nil }) if err != nil { if apierrors.IsNotFound(err) { // Server does not support resource quotas. // Not an error, will not show resource quotas information. resourceQuotaList = nil } else { return "", err } } limitRangeList := &corev1.LimitRangeList{} err = runtimeresource.FollowContinue(&metav1.ListOptions{Limit: describerSettings.ChunkSize}, func(options metav1.ListOptions) (runtime.Object, error) { newList, err := d.CoreV1().LimitRanges(name).List(context.TODO(), options) if err != nil { return nil, runtimeresource.EnhanceListError(err, options, "limitranges") } limitRangeList.Items = append(limitRangeList.Items, newList.Items...) return newList, nil }) if err != nil { if apierrors.IsNotFound(err) { // Server does not support limit ranges. // Not an error, will not show limit ranges information. limitRangeList = nil } else { return "", err } } return describeNamespace(ns, resourceQuotaList, limitRangeList) } func describeNamespace(namespace *corev1.Namespace, resourceQuotaList *corev1.ResourceQuotaList, limitRangeList *corev1.LimitRangeList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", namespace.Name) printLabelsMultiline(w, "Labels", namespace.Labels) printAnnotationsMultiline(w, "Annotations", namespace.Annotations) w.Write(LEVEL_0, "Status:\t%s\n", string(namespace.Status.Phase)) if len(namespace.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n") w.Write(LEVEL_1, "Type\tStatus\tLastTransitionTime\tReason\tMessage\n") w.Write(LEVEL_1, "----\t------\t------------------\t------\t-------\n") for _, c := range namespace.Status.Conditions { w.Write(LEVEL_1, "%v\t%v\t%s\t%v\t%v\n", c.Type, c.Status, c.LastTransitionTime.Time.Format(time.RFC1123Z), c.Reason, c.Message) } } if resourceQuotaList != nil { w.Write(LEVEL_0, "\n") DescribeResourceQuotas(resourceQuotaList, w) } if limitRangeList != nil { w.Write(LEVEL_0, "\n") DescribeLimitRanges(limitRangeList, w) } return nil }) } func describeLimitRangeSpec(spec corev1.LimitRangeSpec, prefix string, w PrefixWriter) { for i := range spec.Limits { item := spec.Limits[i] maxResources := item.Max minResources := item.Min defaultLimitResources := item.Default defaultRequestResources := item.DefaultRequest ratio := item.MaxLimitRequestRatio set := map[corev1.ResourceName]bool{} for k := range maxResources { set[k] = true } for k := range minResources { set[k] = true } for k := range defaultLimitResources { set[k] = true } for k := range defaultRequestResources { set[k] = true } for k := range ratio { set[k] = true } for k := range set { // if no value is set, we output - maxValue := "-" minValue := "-" defaultLimitValue := "-" defaultRequestValue := "-" ratioValue := "-" maxQuantity, maxQuantityFound := maxResources[k] if maxQuantityFound { maxValue = maxQuantity.String() } minQuantity, minQuantityFound := minResources[k] if minQuantityFound { minValue = minQuantity.String() } defaultLimitQuantity, defaultLimitQuantityFound := defaultLimitResources[k] if defaultLimitQuantityFound { defaultLimitValue = defaultLimitQuantity.String() } defaultRequestQuantity, defaultRequestQuantityFound := defaultRequestResources[k] if defaultRequestQuantityFound { defaultRequestValue = defaultRequestQuantity.String() } ratioQuantity, ratioQuantityFound := ratio[k] if ratioQuantityFound { ratioValue = ratioQuantity.String() } msg := "%s%s\t%v\t%v\t%v\t%v\t%v\t%v\n" w.Write(LEVEL_0, msg, prefix, item.Type, k, minValue, maxValue, defaultRequestValue, defaultLimitValue, ratioValue) } } } // DescribeLimitRanges merges a set of limit range items into a single tabular description func DescribeLimitRanges(limitRanges *corev1.LimitRangeList, w PrefixWriter) { if len(limitRanges.Items) == 0 { w.Write(LEVEL_0, "No LimitRange resource.\n") return } w.Write(LEVEL_0, "Resource Limits\n Type\tResource\tMin\tMax\tDefault Request\tDefault Limit\tMax Limit/Request Ratio\n") w.Write(LEVEL_0, " ----\t--------\t---\t---\t---------------\t-------------\t-----------------------\n") for _, limitRange := range limitRanges.Items { describeLimitRangeSpec(limitRange.Spec, " ", w) } } // DescribeResourceQuotas merges a set of quota items into a single tabular description of all quotas func DescribeResourceQuotas(quotas *corev1.ResourceQuotaList, w PrefixWriter) { if len(quotas.Items) == 0 { w.Write(LEVEL_0, "No resource quota.\n") return } sort.Sort(SortableResourceQuotas(quotas.Items)) w.Write(LEVEL_0, "Resource Quotas\n") for _, q := range quotas.Items { w.Write(LEVEL_1, "Name:\t%s\n", q.Name) if len(q.Spec.Scopes) > 0 { scopes := make([]string, 0, len(q.Spec.Scopes)) for _, scope := range q.Spec.Scopes { scopes = append(scopes, string(scope)) } sort.Strings(scopes) w.Write(LEVEL_1, "Scopes:\t%s\n", strings.Join(scopes, ", ")) for _, scope := range scopes { helpText := helpTextForResourceQuotaScope(corev1.ResourceQuotaScope(scope)) if len(helpText) > 0 { w.Write(LEVEL_1, "* %s\n", helpText) } } } w.Write(LEVEL_1, "Resource\tUsed\tHard\n") w.Write(LEVEL_1, "--------\t---\t---\n") resources := make([]corev1.ResourceName, 0, len(q.Status.Hard)) for resource := range q.Status.Hard { resources = append(resources, resource) } sort.Sort(SortableResourceNames(resources)) for _, resource := range resources { hardQuantity := q.Status.Hard[resource] usedQuantity := q.Status.Used[resource] w.Write(LEVEL_1, "%s\t%s\t%s\n", string(resource), usedQuantity.String(), hardQuantity.String()) } } } // LimitRangeDescriber generates information about a limit range type LimitRangeDescriber struct { clientset.Interface } func (d *LimitRangeDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { lr := d.CoreV1().LimitRanges(namespace) limitRange, err := lr.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } return describeLimitRange(limitRange) } func describeLimitRange(limitRange *corev1.LimitRange) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", limitRange.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", limitRange.Namespace) w.Write(LEVEL_0, "Type\tResource\tMin\tMax\tDefault Request\tDefault Limit\tMax Limit/Request Ratio\n") w.Write(LEVEL_0, "----\t--------\t---\t---\t---------------\t-------------\t-----------------------\n") describeLimitRangeSpec(limitRange.Spec, "", w) return nil }) } // ResourceQuotaDescriber generates information about a resource quota type ResourceQuotaDescriber struct { clientset.Interface } func (d *ResourceQuotaDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { rq := d.CoreV1().ResourceQuotas(namespace) resourceQuota, err := rq.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } return describeQuota(resourceQuota) } func helpTextForResourceQuotaScope(scope corev1.ResourceQuotaScope) string { switch scope { case corev1.ResourceQuotaScopeTerminating: return "Matches all pods that have an active deadline. These pods have a limited lifespan on a node before being actively terminated by the system." case corev1.ResourceQuotaScopeNotTerminating: return "Matches all pods that do not have an active deadline. These pods usually include long running pods whose container command is not expected to terminate." case corev1.ResourceQuotaScopeBestEffort: return "Matches all pods that do not have resource requirements set. These pods have a best effort quality of service." case corev1.ResourceQuotaScopeNotBestEffort: return "Matches all pods that have at least one resource requirement set. These pods have a burstable or guaranteed quality of service." default: return "" } } func describeQuota(resourceQuota *corev1.ResourceQuota) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", resourceQuota.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", resourceQuota.Namespace) if len(resourceQuota.Spec.Scopes) > 0 { scopes := make([]string, 0, len(resourceQuota.Spec.Scopes)) for _, scope := range resourceQuota.Spec.Scopes { scopes = append(scopes, string(scope)) } sort.Strings(scopes) w.Write(LEVEL_0, "Scopes:\t%s\n", strings.Join(scopes, ", ")) for _, scope := range scopes { helpText := helpTextForResourceQuotaScope(corev1.ResourceQuotaScope(scope)) if len(helpText) > 0 { w.Write(LEVEL_0, " * %s\n", helpText) } } } w.Write(LEVEL_0, "Resource\tUsed\tHard\n") w.Write(LEVEL_0, "--------\t----\t----\n") resources := make([]corev1.ResourceName, 0, len(resourceQuota.Status.Hard)) for resource := range resourceQuota.Status.Hard { resources = append(resources, resource) } sort.Sort(SortableResourceNames(resources)) msg := "%v\t%v\t%v\n" for i := range resources { resourceName := resources[i] hardQuantity := resourceQuota.Status.Hard[resourceName] usedQuantity := resourceQuota.Status.Used[resourceName] if hardQuantity.Format != usedQuantity.Format { usedQuantity = *resource.NewQuantity(usedQuantity.Value(), hardQuantity.Format) } w.Write(LEVEL_0, msg, resourceName, usedQuantity.String(), hardQuantity.String()) } return nil }) } // PodDescriber generates information about a pod and the replication controllers that // create it. type PodDescriber struct { clientset.Interface } func (d *PodDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { pod, err := d.CoreV1().Pods(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { if describerSettings.ShowEvents { eventsInterface := d.CoreV1().Events(namespace) selector := eventsInterface.GetFieldSelector(&name, &namespace, nil, nil) initialOpts := metav1.ListOptions{ FieldSelector: selector.String(), Limit: describerSettings.ChunkSize, } events := &corev1.EventList{} err2 := runtimeresource.FollowContinue(&initialOpts, func(options metav1.ListOptions) (runtime.Object, error) { newList, err := eventsInterface.List(context.TODO(), options) if err != nil { return nil, runtimeresource.EnhanceListError(err, options, "events") } events.Items = append(events.Items, newList.Items...) return newList, nil }) if err2 == nil && len(events.Items) > 0 { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Pod '%v': error '%v', but found events.\n", name, err) DescribeEvents(events, w) return nil }) } } return "", err } var events *corev1.EventList if describerSettings.ShowEvents { if ref, err := reference.GetReference(scheme.Scheme, pod); err != nil { klog.Errorf("Unable to construct reference to '%#v': %v", pod, err) } else { ref.Kind = "" if _, isMirrorPod := pod.Annotations[corev1.MirrorPodAnnotationKey]; isMirrorPod { ref.UID = types.UID(pod.Annotations[corev1.MirrorPodAnnotationKey]) } events, _ = searchEvents(d.CoreV1(), ref, describerSettings.ChunkSize) } } return describePod(pod, events) } func describePod(pod *corev1.Pod, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", pod.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", pod.Namespace) if pod.Spec.Priority != nil { w.Write(LEVEL_0, "Priority:\t%d\n", *pod.Spec.Priority) } if len(pod.Spec.PriorityClassName) > 0 { w.Write(LEVEL_0, "Priority Class Name:\t%s\n", pod.Spec.PriorityClassName) } if pod.Spec.RuntimeClassName != nil && len(*pod.Spec.RuntimeClassName) > 0 { w.Write(LEVEL_0, "Runtime Class Name:\t%s\n", *pod.Spec.RuntimeClassName) } if len(pod.Spec.ServiceAccountName) > 0 { w.Write(LEVEL_0, "Service Account:\t%s\n", pod.Spec.ServiceAccountName) } if pod.Spec.NodeName == "" { w.Write(LEVEL_0, "Node:\t\n") } else { w.Write(LEVEL_0, "Node:\t%s\n", pod.Spec.NodeName+"/"+pod.Status.HostIP) } if pod.Status.StartTime != nil { w.Write(LEVEL_0, "Start Time:\t%s\n", pod.Status.StartTime.Time.Format(time.RFC1123Z)) } printLabelsMultiline(w, "Labels", pod.Labels) printAnnotationsMultiline(w, "Annotations", pod.Annotations) if pod.DeletionTimestamp != nil && pod.Status.Phase != corev1.PodFailed && pod.Status.Phase != corev1.PodSucceeded { w.Write(LEVEL_0, "Status:\tTerminating (lasts %s)\n", translateTimestampSince(*pod.DeletionTimestamp)) w.Write(LEVEL_0, "Termination Grace Period:\t%ds\n", *pod.DeletionGracePeriodSeconds) } else { w.Write(LEVEL_0, "Status:\t%s\n", string(pod.Status.Phase)) } if len(pod.Status.Reason) > 0 { w.Write(LEVEL_0, "Reason:\t%s\n", pod.Status.Reason) } if len(pod.Status.Message) > 0 { w.Write(LEVEL_0, "Message:\t%s\n", pod.Status.Message) } if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil { w.Write(LEVEL_0, "SeccompProfile:\t%s\n", pod.Spec.SecurityContext.SeccompProfile.Type) if pod.Spec.SecurityContext.SeccompProfile.Type == corev1.SeccompProfileTypeLocalhost { w.Write(LEVEL_0, "LocalhostProfile:\t%s\n", *pod.Spec.SecurityContext.SeccompProfile.LocalhostProfile) } } // remove when .IP field is deprecated w.Write(LEVEL_0, "IP:\t%s\n", pod.Status.PodIP) describePodIPs(pod, w, "") if controlledBy := printController(pod); len(controlledBy) > 0 { w.Write(LEVEL_0, "Controlled By:\t%s\n", controlledBy) } if len(pod.Status.NominatedNodeName) > 0 { w.Write(LEVEL_0, "NominatedNodeName:\t%s\n", pod.Status.NominatedNodeName) } if pod.Spec.Resources != nil { w.Write(LEVEL_0, "Resources:\n") describeResources(pod.Spec.Resources, w, LEVEL_1) } if len(pod.Spec.InitContainers) > 0 { describeContainers("Init Containers", pod.Spec.InitContainers, pod.Status.InitContainerStatuses, EnvValueRetriever(pod), w, "") } describeContainers("Containers", pod.Spec.Containers, pod.Status.ContainerStatuses, EnvValueRetriever(pod), w, "") if len(pod.Spec.EphemeralContainers) > 0 { var ec []corev1.Container for i := range pod.Spec.EphemeralContainers { ec = append(ec, corev1.Container(pod.Spec.EphemeralContainers[i].EphemeralContainerCommon)) } describeContainers("Ephemeral Containers", ec, pod.Status.EphemeralContainerStatuses, EnvValueRetriever(pod), w, "") } if len(pod.Spec.ReadinessGates) > 0 { w.Write(LEVEL_0, "Readiness Gates:\n Type\tStatus\n") for _, g := range pod.Spec.ReadinessGates { status := "" for _, c := range pod.Status.Conditions { if c.Type == g.ConditionType { status = fmt.Sprintf("%v", c.Status) break } } w.Write(LEVEL_1, "%v \t%v \n", g.ConditionType, status) } } if len(pod.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n Type\tStatus\n") for _, c := range pod.Status.Conditions { w.Write(LEVEL_1, "%v \t%v \n", c.Type, c.Status) } } describeVolumes(pod.Spec.Volumes, w, "") w.Write(LEVEL_0, "QoS Class:\t%s\n", qos.GetPodQOS(pod)) printLabelsMultiline(w, "Node-Selectors", pod.Spec.NodeSelector) printPodTolerationsMultiline(w, "Tolerations", pod.Spec.Tolerations) describeTopologySpreadConstraints(pod.Spec.TopologySpreadConstraints, w, "") if events != nil { DescribeEvents(events, w) } return nil }) } func printController(controllee metav1.Object) string { if controllerRef := metav1.GetControllerOf(controllee); controllerRef != nil { return fmt.Sprintf("%s/%s", controllerRef.Kind, controllerRef.Name) } return "" } func describePodIPs(pod *corev1.Pod, w PrefixWriter, space string) { if len(pod.Status.PodIPs) == 0 { w.Write(LEVEL_0, "%sIPs:\t\n", space) return } w.Write(LEVEL_0, "%sIPs:\n", space) for _, ipInfo := range pod.Status.PodIPs { w.Write(LEVEL_1, "IP:\t%s\n", ipInfo.IP) } } func describeTopologySpreadConstraints(tscs []corev1.TopologySpreadConstraint, w PrefixWriter, space string) { if len(tscs) == 0 { return } sort.Slice(tscs, func(i, j int) bool { return tscs[i].TopologyKey < tscs[j].TopologyKey }) w.Write(LEVEL_0, "%sTopology Spread Constraints:\t", space) for i, tsc := range tscs { if i != 0 { w.Write(LEVEL_0, "%s", space) w.Write(LEVEL_0, "%s", "\t") } w.Write(LEVEL_0, "%s:", tsc.TopologyKey) w.Write(LEVEL_0, "%v", tsc.WhenUnsatisfiable) w.Write(LEVEL_0, " when max skew %d is exceeded", tsc.MaxSkew) if tsc.LabelSelector != nil { w.Write(LEVEL_0, " for selector %s", metav1.FormatLabelSelector(tsc.LabelSelector)) } w.Write(LEVEL_0, "\n") } } func describeVolumes(volumes []corev1.Volume, w PrefixWriter, space string) { if len(volumes) == 0 { w.Write(LEVEL_0, "%sVolumes:\t\n", space) return } w.Write(LEVEL_0, "%sVolumes:\n", space) for _, volume := range volumes { nameIndent := "" if len(space) > 0 { nameIndent = " " } w.Write(LEVEL_1, "%s%v:\n", nameIndent, volume.Name) switch { case volume.VolumeSource.HostPath != nil: printHostPathVolumeSource(volume.VolumeSource.HostPath, w) case volume.VolumeSource.EmptyDir != nil: printEmptyDirVolumeSource(volume.VolumeSource.EmptyDir, w) case volume.VolumeSource.GCEPersistentDisk != nil: printGCEPersistentDiskVolumeSource(volume.VolumeSource.GCEPersistentDisk, w) case volume.VolumeSource.AWSElasticBlockStore != nil: printAWSElasticBlockStoreVolumeSource(volume.VolumeSource.AWSElasticBlockStore, w) case volume.VolumeSource.GitRepo != nil: printGitRepoVolumeSource(volume.VolumeSource.GitRepo, w) case volume.VolumeSource.Secret != nil: printSecretVolumeSource(volume.VolumeSource.Secret, w) case volume.VolumeSource.ConfigMap != nil: printConfigMapVolumeSource(volume.VolumeSource.ConfigMap, w) case volume.VolumeSource.NFS != nil: printNFSVolumeSource(volume.VolumeSource.NFS, w) case volume.VolumeSource.ISCSI != nil: printISCSIVolumeSource(volume.VolumeSource.ISCSI, w) case volume.VolumeSource.Glusterfs != nil: printGlusterfsVolumeSource(volume.VolumeSource.Glusterfs, w) case volume.VolumeSource.PersistentVolumeClaim != nil: printPersistentVolumeClaimVolumeSource(volume.VolumeSource.PersistentVolumeClaim, w) case volume.VolumeSource.Ephemeral != nil: printEphemeralVolumeSource(volume.VolumeSource.Ephemeral, w) case volume.VolumeSource.RBD != nil: printRBDVolumeSource(volume.VolumeSource.RBD, w) case volume.VolumeSource.Quobyte != nil: printQuobyteVolumeSource(volume.VolumeSource.Quobyte, w) case volume.VolumeSource.DownwardAPI != nil: printDownwardAPIVolumeSource(volume.VolumeSource.DownwardAPI, w) case volume.VolumeSource.AzureDisk != nil: printAzureDiskVolumeSource(volume.VolumeSource.AzureDisk, w) case volume.VolumeSource.VsphereVolume != nil: printVsphereVolumeSource(volume.VolumeSource.VsphereVolume, w) case volume.VolumeSource.Cinder != nil: printCinderVolumeSource(volume.VolumeSource.Cinder, w) case volume.VolumeSource.PhotonPersistentDisk != nil: printPhotonPersistentDiskVolumeSource(volume.VolumeSource.PhotonPersistentDisk, w) case volume.VolumeSource.PortworxVolume != nil: printPortworxVolumeSource(volume.VolumeSource.PortworxVolume, w) case volume.VolumeSource.ScaleIO != nil: printScaleIOVolumeSource(volume.VolumeSource.ScaleIO, w) case volume.VolumeSource.CephFS != nil: printCephFSVolumeSource(volume.VolumeSource.CephFS, w) case volume.VolumeSource.StorageOS != nil: printStorageOSVolumeSource(volume.VolumeSource.StorageOS, w) case volume.VolumeSource.FC != nil: printFCVolumeSource(volume.VolumeSource.FC, w) case volume.VolumeSource.AzureFile != nil: printAzureFileVolumeSource(volume.VolumeSource.AzureFile, w) case volume.VolumeSource.FlexVolume != nil: printFlexVolumeSource(volume.VolumeSource.FlexVolume, w) case volume.VolumeSource.Flocker != nil: printFlockerVolumeSource(volume.VolumeSource.Flocker, w) case volume.VolumeSource.Projected != nil: printProjectedVolumeSource(volume.VolumeSource.Projected, w) case volume.VolumeSource.CSI != nil: printCSIVolumeSource(volume.VolumeSource.CSI, w) case volume.VolumeSource.Image != nil: printImageVolumeSource(volume.VolumeSource.Image, w) default: w.Write(LEVEL_1, "\n") } } } func printHostPathVolumeSource(hostPath *corev1.HostPathVolumeSource, w PrefixWriter) { hostPathType := "" if hostPath.Type != nil { hostPathType = string(*hostPath.Type) } w.Write(LEVEL_2, "Type:\tHostPath (bare host directory volume)\n"+ " Path:\t%v\n"+ " HostPathType:\t%v\n", hostPath.Path, hostPathType) } func printEmptyDirVolumeSource(emptyDir *corev1.EmptyDirVolumeSource, w PrefixWriter) { var sizeLimit string if emptyDir.SizeLimit != nil && emptyDir.SizeLimit.Cmp(resource.Quantity{}) > 0 { sizeLimit = fmt.Sprintf("%v", emptyDir.SizeLimit) } else { sizeLimit = "" } w.Write(LEVEL_2, "Type:\tEmptyDir (a temporary directory that shares a pod's lifetime)\n"+ " Medium:\t%v\n"+ " SizeLimit:\t%v\n", emptyDir.Medium, sizeLimit) } func printGCEPersistentDiskVolumeSource(gce *corev1.GCEPersistentDiskVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tGCEPersistentDisk (a Persistent Disk resource in Google Compute Engine)\n"+ " PDName:\t%v\n"+ " FSType:\t%v\n"+ " Partition:\t%v\n"+ " ReadOnly:\t%v\n", gce.PDName, gce.FSType, gce.Partition, gce.ReadOnly) } func printAWSElasticBlockStoreVolumeSource(aws *corev1.AWSElasticBlockStoreVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tAWSElasticBlockStore (a Persistent Disk resource in AWS)\n"+ " VolumeID:\t%v\n"+ " FSType:\t%v\n"+ " Partition:\t%v\n"+ " ReadOnly:\t%v\n", aws.VolumeID, aws.FSType, aws.Partition, aws.ReadOnly) } func printGitRepoVolumeSource(git *corev1.GitRepoVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tGitRepo (a volume that is pulled from git when the pod is created)\n"+ " Repository:\t%v\n"+ " Revision:\t%v\n", git.Repository, git.Revision) } func printSecretVolumeSource(secret *corev1.SecretVolumeSource, w PrefixWriter) { optional := secret.Optional != nil && *secret.Optional w.Write(LEVEL_2, "Type:\tSecret (a volume populated by a Secret)\n"+ " SecretName:\t%v\n"+ " Optional:\t%v\n", secret.SecretName, optional) } func printConfigMapVolumeSource(configMap *corev1.ConfigMapVolumeSource, w PrefixWriter) { optional := configMap.Optional != nil && *configMap.Optional w.Write(LEVEL_2, "Type:\tConfigMap (a volume populated by a ConfigMap)\n"+ " Name:\t%v\n"+ " Optional:\t%v\n", configMap.Name, optional) } func printProjectedVolumeSource(projected *corev1.ProjectedVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tProjected (a volume that contains injected data from multiple sources)\n") for _, source := range projected.Sources { if source.Secret != nil { optional := source.Secret.Optional != nil && *source.Secret.Optional w.Write(LEVEL_2, "SecretName:\t%v\n"+ " Optional:\t%v\n", source.Secret.Name, optional) } else if source.DownwardAPI != nil { w.Write(LEVEL_2, "DownwardAPI:\ttrue\n") } else if source.ConfigMap != nil { optional := source.ConfigMap.Optional != nil && *source.ConfigMap.Optional w.Write(LEVEL_2, "ConfigMapName:\t%v\n"+ " Optional:\t%v\n", source.ConfigMap.Name, optional) } else if source.ServiceAccountToken != nil { w.Write(LEVEL_2, "TokenExpirationSeconds:\t%d\n", *source.ServiceAccountToken.ExpirationSeconds) } } } func printNFSVolumeSource(nfs *corev1.NFSVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tNFS (an NFS mount that lasts the lifetime of a pod)\n"+ " Server:\t%v\n"+ " Path:\t%v\n"+ " ReadOnly:\t%v\n", nfs.Server, nfs.Path, nfs.ReadOnly) } func printQuobyteVolumeSource(quobyte *corev1.QuobyteVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tQuobyte (a Quobyte mount on the host that shares a pod's lifetime)\n"+ " Registry:\t%v\n"+ " Volume:\t%v\n"+ " ReadOnly:\t%v\n", quobyte.Registry, quobyte.Volume, quobyte.ReadOnly) } func printPortworxVolumeSource(pwxVolume *corev1.PortworxVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tPortworxVolume (a Portworx Volume resource)\n"+ " VolumeID:\t%v\n", pwxVolume.VolumeID) } func printISCSIVolumeSource(iscsi *corev1.ISCSIVolumeSource, w PrefixWriter) { initiator := "" if iscsi.InitiatorName != nil { initiator = *iscsi.InitiatorName } w.Write(LEVEL_2, "Type:\tISCSI (an ISCSI Disk resource that is attached to a kubelet's host machine and then exposed to the pod)\n"+ " TargetPortal:\t%v\n"+ " IQN:\t%v\n"+ " Lun:\t%v\n"+ " ISCSIInterface\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n"+ " Portals:\t%v\n"+ " DiscoveryCHAPAuth:\t%v\n"+ " SessionCHAPAuth:\t%v\n"+ " SecretRef:\t%v\n"+ " InitiatorName:\t%v\n", iscsi.TargetPortal, iscsi.IQN, iscsi.Lun, iscsi.ISCSIInterface, iscsi.FSType, iscsi.ReadOnly, iscsi.Portals, iscsi.DiscoveryCHAPAuth, iscsi.SessionCHAPAuth, iscsi.SecretRef, initiator) } func printISCSIPersistentVolumeSource(iscsi *corev1.ISCSIPersistentVolumeSource, w PrefixWriter) { initiatorName := "" if iscsi.InitiatorName != nil { initiatorName = *iscsi.InitiatorName } w.Write(LEVEL_2, "Type:\tISCSI (an ISCSI Disk resource that is attached to a kubelet's host machine and then exposed to the pod)\n"+ " TargetPortal:\t%v\n"+ " IQN:\t%v\n"+ " Lun:\t%v\n"+ " ISCSIInterface\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n"+ " Portals:\t%v\n"+ " DiscoveryCHAPAuth:\t%v\n"+ " SessionCHAPAuth:\t%v\n"+ " SecretRef:\t%v\n"+ " InitiatorName:\t%v\n", iscsi.TargetPortal, iscsi.IQN, iscsi.Lun, iscsi.ISCSIInterface, iscsi.FSType, iscsi.ReadOnly, iscsi.Portals, iscsi.DiscoveryCHAPAuth, iscsi.SessionCHAPAuth, iscsi.SecretRef, initiatorName) } func printGlusterfsVolumeSource(glusterfs *corev1.GlusterfsVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tGlusterfs (a Glusterfs mount on the host that shares a pod's lifetime)\n"+ " EndpointsName:\t%v\n"+ " Path:\t%v\n"+ " ReadOnly:\t%v\n", glusterfs.EndpointsName, glusterfs.Path, glusterfs.ReadOnly) } func printGlusterfsPersistentVolumeSource(glusterfs *corev1.GlusterfsPersistentVolumeSource, w PrefixWriter) { endpointsNamespace := "" if glusterfs.EndpointsNamespace != nil { endpointsNamespace = *glusterfs.EndpointsNamespace } w.Write(LEVEL_2, "Type:\tGlusterfs (a Glusterfs mount on the host that shares a pod's lifetime)\n"+ " EndpointsName:\t%v\n"+ " EndpointsNamespace:\t%v\n"+ " Path:\t%v\n"+ " ReadOnly:\t%v\n", glusterfs.EndpointsName, endpointsNamespace, glusterfs.Path, glusterfs.ReadOnly) } func printPersistentVolumeClaimVolumeSource(claim *corev1.PersistentVolumeClaimVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tPersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)\n"+ " ClaimName:\t%v\n"+ " ReadOnly:\t%v\n", claim.ClaimName, claim.ReadOnly) } func printEphemeralVolumeSource(ephemeral *corev1.EphemeralVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tEphemeralVolume (an inline specification for a volume that gets created and deleted with the pod)\n") if ephemeral.VolumeClaimTemplate != nil { printPersistentVolumeClaim(NewNestedPrefixWriter(w, LEVEL_2), &corev1.PersistentVolumeClaim{ ObjectMeta: ephemeral.VolumeClaimTemplate.ObjectMeta, Spec: ephemeral.VolumeClaimTemplate.Spec, }, false /* not a full PVC */) } } func printRBDVolumeSource(rbd *corev1.RBDVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tRBD (a Rados Block Device mount on the host that shares a pod's lifetime)\n"+ " CephMonitors:\t%v\n"+ " RBDImage:\t%v\n"+ " FSType:\t%v\n"+ " RBDPool:\t%v\n"+ " RadosUser:\t%v\n"+ " Keyring:\t%v\n"+ " SecretRef:\t%v\n"+ " ReadOnly:\t%v\n", rbd.CephMonitors, rbd.RBDImage, rbd.FSType, rbd.RBDPool, rbd.RadosUser, rbd.Keyring, rbd.SecretRef, rbd.ReadOnly) } func printRBDPersistentVolumeSource(rbd *corev1.RBDPersistentVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tRBD (a Rados Block Device mount on the host that shares a pod's lifetime)\n"+ " CephMonitors:\t%v\n"+ " RBDImage:\t%v\n"+ " FSType:\t%v\n"+ " RBDPool:\t%v\n"+ " RadosUser:\t%v\n"+ " Keyring:\t%v\n"+ " SecretRef:\t%v\n"+ " ReadOnly:\t%v\n", rbd.CephMonitors, rbd.RBDImage, rbd.FSType, rbd.RBDPool, rbd.RadosUser, rbd.Keyring, rbd.SecretRef, rbd.ReadOnly) } func printDownwardAPIVolumeSource(d *corev1.DownwardAPIVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tDownwardAPI (a volume populated by information about the pod)\n Items:\n") for _, mapping := range d.Items { if mapping.FieldRef != nil { w.Write(LEVEL_3, "%v -> %v\n", mapping.FieldRef.FieldPath, mapping.Path) } if mapping.ResourceFieldRef != nil { w.Write(LEVEL_3, "%v -> %v\n", mapping.ResourceFieldRef.Resource, mapping.Path) } } } func printAzureDiskVolumeSource(d *corev1.AzureDiskVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tAzureDisk (an Azure Data Disk mount on the host and bind mount to the pod)\n"+ " DiskName:\t%v\n"+ " DiskURI:\t%v\n"+ " Kind: \t%v\n"+ " FSType:\t%v\n"+ " CachingMode:\t%v\n"+ " ReadOnly:\t%v\n", d.DiskName, d.DataDiskURI, *d.Kind, *d.FSType, *d.CachingMode, *d.ReadOnly) } func printVsphereVolumeSource(vsphere *corev1.VsphereVirtualDiskVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tvSphereVolume (a Persistent Disk resource in vSphere)\n"+ " VolumePath:\t%v\n"+ " FSType:\t%v\n"+ " StoragePolicyName:\t%v\n", vsphere.VolumePath, vsphere.FSType, vsphere.StoragePolicyName) } func printPhotonPersistentDiskVolumeSource(photon *corev1.PhotonPersistentDiskVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tPhotonPersistentDisk (a Persistent Disk resource in photon platform)\n"+ " PdID:\t%v\n"+ " FSType:\t%v\n", photon.PdID, photon.FSType) } func printCinderVolumeSource(cinder *corev1.CinderVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tCinder (a Persistent Disk resource in OpenStack)\n"+ " VolumeID:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n"+ " SecretRef:\t%v\n", cinder.VolumeID, cinder.FSType, cinder.ReadOnly, cinder.SecretRef) } func printCinderPersistentVolumeSource(cinder *corev1.CinderPersistentVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tCinder (a Persistent Disk resource in OpenStack)\n"+ " VolumeID:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n"+ " SecretRef:\t%v\n", cinder.VolumeID, cinder.FSType, cinder.ReadOnly, cinder.SecretRef) } func printScaleIOVolumeSource(sio *corev1.ScaleIOVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tScaleIO (a persistent volume backed by a block device in ScaleIO)\n"+ " Gateway:\t%v\n"+ " System:\t%v\n"+ " Protection Domain:\t%v\n"+ " Storage Pool:\t%v\n"+ " Storage Mode:\t%v\n"+ " VolumeName:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n", sio.Gateway, sio.System, sio.ProtectionDomain, sio.StoragePool, sio.StorageMode, sio.VolumeName, sio.FSType, sio.ReadOnly) } func printScaleIOPersistentVolumeSource(sio *corev1.ScaleIOPersistentVolumeSource, w PrefixWriter) { var secretNS, secretName string if sio.SecretRef != nil { secretName = sio.SecretRef.Name secretNS = sio.SecretRef.Namespace } w.Write(LEVEL_2, "Type:\tScaleIO (a persistent volume backed by a block device in ScaleIO)\n"+ " Gateway:\t%v\n"+ " System:\t%v\n"+ " Protection Domain:\t%v\n"+ " Storage Pool:\t%v\n"+ " Storage Mode:\t%v\n"+ " VolumeName:\t%v\n"+ " SecretName:\t%v\n"+ " SecretNamespace:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n", sio.Gateway, sio.System, sio.ProtectionDomain, sio.StoragePool, sio.StorageMode, sio.VolumeName, secretName, secretNS, sio.FSType, sio.ReadOnly) } func printLocalVolumeSource(ls *corev1.LocalVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tLocalVolume (a persistent volume backed by local storage on a node)\n"+ " Path:\t%v\n", ls.Path) } func printCephFSVolumeSource(cephfs *corev1.CephFSVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tCephFS (a CephFS mount on the host that shares a pod's lifetime)\n"+ " Monitors:\t%v\n"+ " Path:\t%v\n"+ " User:\t%v\n"+ " SecretFile:\t%v\n"+ " SecretRef:\t%v\n"+ " ReadOnly:\t%v\n", cephfs.Monitors, cephfs.Path, cephfs.User, cephfs.SecretFile, cephfs.SecretRef, cephfs.ReadOnly) } func printCephFSPersistentVolumeSource(cephfs *corev1.CephFSPersistentVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tCephFS (a CephFS mount on the host that shares a pod's lifetime)\n"+ " Monitors:\t%v\n"+ " Path:\t%v\n"+ " User:\t%v\n"+ " SecretFile:\t%v\n"+ " SecretRef:\t%v\n"+ " ReadOnly:\t%v\n", cephfs.Monitors, cephfs.Path, cephfs.User, cephfs.SecretFile, cephfs.SecretRef, cephfs.ReadOnly) } func printStorageOSVolumeSource(storageos *corev1.StorageOSVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tStorageOS (a StorageOS Persistent Disk resource)\n"+ " VolumeName:\t%v\n"+ " VolumeNamespace:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n", storageos.VolumeName, storageos.VolumeNamespace, storageos.FSType, storageos.ReadOnly) } func printStorageOSPersistentVolumeSource(storageos *corev1.StorageOSPersistentVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tStorageOS (a StorageOS Persistent Disk resource)\n"+ " VolumeName:\t%v\n"+ " VolumeNamespace:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n", storageos.VolumeName, storageos.VolumeNamespace, storageos.FSType, storageos.ReadOnly) } func printFCVolumeSource(fc *corev1.FCVolumeSource, w PrefixWriter) { lun := "" if fc.Lun != nil { lun = strconv.Itoa(int(*fc.Lun)) } w.Write(LEVEL_2, "Type:\tFC (a Fibre Channel disk)\n"+ " TargetWWNs:\t%v\n"+ " LUN:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n", strings.Join(fc.TargetWWNs, ", "), lun, fc.FSType, fc.ReadOnly) } func printAzureFileVolumeSource(azureFile *corev1.AzureFileVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tAzureFile (an Azure File Service mount on the host and bind mount to the pod)\n"+ " SecretName:\t%v\n"+ " ShareName:\t%v\n"+ " ReadOnly:\t%v\n", azureFile.SecretName, azureFile.ShareName, azureFile.ReadOnly) } func printAzureFilePersistentVolumeSource(azureFile *corev1.AzureFilePersistentVolumeSource, w PrefixWriter) { ns := "" if azureFile.SecretNamespace != nil { ns = *azureFile.SecretNamespace } w.Write(LEVEL_2, "Type:\tAzureFile (an Azure File Service mount on the host and bind mount to the pod)\n"+ " SecretName:\t%v\n"+ " SecretNamespace:\t%v\n"+ " ShareName:\t%v\n"+ " ReadOnly:\t%v\n", azureFile.SecretName, ns, azureFile.ShareName, azureFile.ReadOnly) } func printFlexPersistentVolumeSource(flex *corev1.FlexPersistentVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tFlexVolume (a generic volume resource that is provisioned/attached using an exec based plugin)\n"+ " Driver:\t%v\n"+ " FSType:\t%v\n"+ " SecretRef:\t%v\n"+ " ReadOnly:\t%v\n"+ " Options:\t%v\n", flex.Driver, flex.FSType, flex.SecretRef, flex.ReadOnly, flex.Options) } func printFlexVolumeSource(flex *corev1.FlexVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tFlexVolume (a generic volume resource that is provisioned/attached using an exec based plugin)\n"+ " Driver:\t%v\n"+ " FSType:\t%v\n"+ " SecretRef:\t%v\n"+ " ReadOnly:\t%v\n"+ " Options:\t%v\n", flex.Driver, flex.FSType, flex.SecretRef, flex.ReadOnly, flex.Options) } func printFlockerVolumeSource(flocker *corev1.FlockerVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tFlocker (a Flocker volume mounted by the Flocker agent)\n"+ " DatasetName:\t%v\n"+ " DatasetUUID:\t%v\n", flocker.DatasetName, flocker.DatasetUUID) } func printCSIVolumeSource(csi *corev1.CSIVolumeSource, w PrefixWriter) { var readOnly bool var fsType string if csi.ReadOnly != nil && *csi.ReadOnly { readOnly = true } if csi.FSType != nil { fsType = *csi.FSType } w.Write(LEVEL_2, "Type:\tCSI (a Container Storage Interface (CSI) volume source)\n"+ " Driver:\t%v\n"+ " FSType:\t%v\n"+ " ReadOnly:\t%v\n", csi.Driver, fsType, readOnly) printCSIPersistentVolumeAttributesMultiline(w, "VolumeAttributes", csi.VolumeAttributes) } func printCSIPersistentVolumeSource(csi *corev1.CSIPersistentVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tCSI (a Container Storage Interface (CSI) volume source)\n"+ " Driver:\t%v\n"+ " FSType:\t%v\n"+ " VolumeHandle:\t%v\n"+ " ReadOnly:\t%v\n", csi.Driver, csi.FSType, csi.VolumeHandle, csi.ReadOnly) printCSIPersistentVolumeAttributesMultiline(w, "VolumeAttributes", csi.VolumeAttributes) } func printCSIPersistentVolumeAttributesMultiline(w PrefixWriter, title string, annotations map[string]string) { printCSIPersistentVolumeAttributesMultilineIndent(w, "", title, "\t", annotations, sets.New[string]()) } func printCSIPersistentVolumeAttributesMultilineIndent(w PrefixWriter, initialIndent, title, innerIndent string, attributes map[string]string, skip sets.Set[string]) { w.Write(LEVEL_2, "%s%s:%s", initialIndent, title, innerIndent) if len(attributes) == 0 { w.WriteLine("") return } // to print labels in the sorted order keys := make([]string, 0, len(attributes)) for key := range attributes { if skip.Has(key) { continue } keys = append(keys, key) } if len(attributes) == 0 { w.WriteLine("") return } sort.Strings(keys) for i, key := range keys { if i != 0 { w.Write(LEVEL_2, initialIndent) w.Write(LEVEL_2, innerIndent) } line := fmt.Sprintf("%s=%s", key, attributes[key]) if len(line) > maxAnnotationLen { w.Write(LEVEL_2, "%s...\n", line[:maxAnnotationLen]) } else { w.Write(LEVEL_2, "%s\n", line) } } } func printImageVolumeSource(image *corev1.ImageVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tImage (a container image or OCI artifact)\n"+ " Reference:\t%v\n"+ " PullPolicy:\t%v\n", image.Reference, image.PullPolicy) } type PersistentVolumeDescriber struct { clientset.Interface } func (d *PersistentVolumeDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.CoreV1().PersistentVolumes() pv, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), pv, describerSettings.ChunkSize) } return describePersistentVolume(pv, events) } func printVolumeNodeAffinity(w PrefixWriter, affinity *corev1.VolumeNodeAffinity) { w.Write(LEVEL_0, "Node Affinity:\t") if affinity == nil || affinity.Required == nil { w.WriteLine("") return } w.WriteLine("") if affinity.Required != nil { w.Write(LEVEL_1, "Required Terms:\t") if len(affinity.Required.NodeSelectorTerms) == 0 { w.WriteLine("") } else { w.WriteLine("") for i, term := range affinity.Required.NodeSelectorTerms { printNodeSelectorTermsMultilineWithIndent(w, LEVEL_2, fmt.Sprintf("Term %v", i), "\t", term.MatchExpressions) } } } } // printLabelsMultiline prints multiple labels with a user-defined alignment. func printNodeSelectorTermsMultilineWithIndent(w PrefixWriter, indentLevel int, title, innerIndent string, reqs []corev1.NodeSelectorRequirement) { w.Write(indentLevel, "%s:%s", title, innerIndent) if len(reqs) == 0 { w.WriteLine("") return } for i, req := range reqs { if i != 0 { w.Write(indentLevel, "%s", innerIndent) } exprStr := fmt.Sprintf("%s %s", req.Key, strings.ToLower(string(req.Operator))) if len(req.Values) > 0 { exprStr = fmt.Sprintf("%s [%s]", exprStr, strings.Join(req.Values, ", ")) } w.Write(LEVEL_0, "%s\n", exprStr) } } func describePersistentVolume(pv *corev1.PersistentVolume, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", pv.Name) printLabelsMultiline(w, "Labels", pv.ObjectMeta.Labels) printAnnotationsMultiline(w, "Annotations", pv.ObjectMeta.Annotations) w.Write(LEVEL_0, "Finalizers:\t%v\n", pv.ObjectMeta.Finalizers) w.Write(LEVEL_0, "StorageClass:\t%s\n", storageutil.GetPersistentVolumeClass(pv)) if pv.ObjectMeta.DeletionTimestamp != nil { w.Write(LEVEL_0, "Status:\tTerminating (lasts %s)\n", translateTimestampSince(*pv.ObjectMeta.DeletionTimestamp)) } else { w.Write(LEVEL_0, "Status:\t%v\n", pv.Status.Phase) } if pv.Spec.ClaimRef != nil { w.Write(LEVEL_0, "Claim:\t%s\n", pv.Spec.ClaimRef.Namespace+"/"+pv.Spec.ClaimRef.Name) } else { w.Write(LEVEL_0, "Claim:\t%s\n", "") } w.Write(LEVEL_0, "Reclaim Policy:\t%v\n", pv.Spec.PersistentVolumeReclaimPolicy) w.Write(LEVEL_0, "Access Modes:\t%s\n", storageutil.GetAccessModesAsString(pv.Spec.AccessModes)) if pv.Spec.VolumeMode != nil { w.Write(LEVEL_0, "VolumeMode:\t%v\n", *pv.Spec.VolumeMode) } storage := pv.Spec.Capacity[corev1.ResourceStorage] w.Write(LEVEL_0, "Capacity:\t%s\n", storage.String()) printVolumeNodeAffinity(w, pv.Spec.NodeAffinity) w.Write(LEVEL_0, "Message:\t%s\n", pv.Status.Message) w.Write(LEVEL_0, "Source:\n") switch { case pv.Spec.HostPath != nil: printHostPathVolumeSource(pv.Spec.HostPath, w) case pv.Spec.GCEPersistentDisk != nil: printGCEPersistentDiskVolumeSource(pv.Spec.GCEPersistentDisk, w) case pv.Spec.AWSElasticBlockStore != nil: printAWSElasticBlockStoreVolumeSource(pv.Spec.AWSElasticBlockStore, w) case pv.Spec.NFS != nil: printNFSVolumeSource(pv.Spec.NFS, w) case pv.Spec.ISCSI != nil: printISCSIPersistentVolumeSource(pv.Spec.ISCSI, w) case pv.Spec.Glusterfs != nil: printGlusterfsPersistentVolumeSource(pv.Spec.Glusterfs, w) case pv.Spec.RBD != nil: printRBDPersistentVolumeSource(pv.Spec.RBD, w) case pv.Spec.Quobyte != nil: printQuobyteVolumeSource(pv.Spec.Quobyte, w) case pv.Spec.VsphereVolume != nil: printVsphereVolumeSource(pv.Spec.VsphereVolume, w) case pv.Spec.Cinder != nil: printCinderPersistentVolumeSource(pv.Spec.Cinder, w) case pv.Spec.AzureDisk != nil: printAzureDiskVolumeSource(pv.Spec.AzureDisk, w) case pv.Spec.PhotonPersistentDisk != nil: printPhotonPersistentDiskVolumeSource(pv.Spec.PhotonPersistentDisk, w) case pv.Spec.PortworxVolume != nil: printPortworxVolumeSource(pv.Spec.PortworxVolume, w) case pv.Spec.ScaleIO != nil: printScaleIOPersistentVolumeSource(pv.Spec.ScaleIO, w) case pv.Spec.Local != nil: printLocalVolumeSource(pv.Spec.Local, w) case pv.Spec.CephFS != nil: printCephFSPersistentVolumeSource(pv.Spec.CephFS, w) case pv.Spec.StorageOS != nil: printStorageOSPersistentVolumeSource(pv.Spec.StorageOS, w) case pv.Spec.FC != nil: printFCVolumeSource(pv.Spec.FC, w) case pv.Spec.AzureFile != nil: printAzureFilePersistentVolumeSource(pv.Spec.AzureFile, w) case pv.Spec.FlexVolume != nil: printFlexPersistentVolumeSource(pv.Spec.FlexVolume, w) case pv.Spec.Flocker != nil: printFlockerVolumeSource(pv.Spec.Flocker, w) case pv.Spec.CSI != nil: printCSIPersistentVolumeSource(pv.Spec.CSI, w) default: w.Write(LEVEL_1, "\n") } if events != nil { DescribeEvents(events, w) } return nil }) } type PersistentVolumeClaimDescriber struct { clientset.Interface } func (d *PersistentVolumeClaimDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.CoreV1().PersistentVolumeClaims(namespace) pvc, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } pc := d.CoreV1().Pods(namespace) pods, err := getPodsForPVC(pc, pvc, describerSettings) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), pvc, describerSettings.ChunkSize) } return describePersistentVolumeClaim(pvc, events, pods) } func getPodsForPVC(c corev1client.PodInterface, pvc *corev1.PersistentVolumeClaim, settings DescriberSettings) ([]corev1.Pod, error) { nsPods, err := getPodsInChunks(c, metav1.ListOptions{Limit: settings.ChunkSize}) if err != nil { return []corev1.Pod{}, err } var pods []corev1.Pod for _, pod := range nsPods.Items { for _, volume := range pod.Spec.Volumes { if volume.VolumeSource.PersistentVolumeClaim != nil && volume.VolumeSource.PersistentVolumeClaim.ClaimName == pvc.Name { pods = append(pods, pod) } } } ownersLoop: for _, ownerRef := range pvc.ObjectMeta.OwnerReferences { if ownerRef.Kind != "Pod" { continue } podIndex := -1 for i, pod := range nsPods.Items { if pod.UID == ownerRef.UID { podIndex = i break } } if podIndex == -1 { // Maybe the pod has been deleted continue } for _, pod := range pods { if pod.UID == nsPods.Items[podIndex].UID { // This owner pod is already recorded, look for pods between other owners continue ownersLoop } } pods = append(pods, nsPods.Items[podIndex]) } return pods, nil } func describePersistentVolumeClaim(pvc *corev1.PersistentVolumeClaim, events *corev1.EventList, pods []corev1.Pod) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) printPersistentVolumeClaim(w, pvc, true) printPodsMultiline(w, "Used By", pods) if len(pvc.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n") w.Write(LEVEL_1, "Type\tStatus\tLastProbeTime\tLastTransitionTime\tReason\tMessage\n") w.Write(LEVEL_1, "----\t------\t-----------------\t------------------\t------\t-------\n") for _, c := range pvc.Status.Conditions { w.Write(LEVEL_1, "%v \t%v \t%s \t%s \t%v \t%v\n", c.Type, c.Status, c.LastProbeTime.Time.Format(time.RFC1123Z), c.LastTransitionTime.Time.Format(time.RFC1123Z), c.Reason, c.Message) } } if events != nil { DescribeEvents(events, w) } return nil }) } // printPersistentVolumeClaim is used for both PVCs and PersistentVolumeClaimTemplate. For the latter, // we need to skip some fields which have no meaning. func printPersistentVolumeClaim(w PrefixWriter, pvc *corev1.PersistentVolumeClaim, isFullPVC bool) { if isFullPVC { w.Write(LEVEL_0, "Name:\t%s\n", pvc.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", pvc.Namespace) } w.Write(LEVEL_0, "StorageClass:\t%s\n", storageutil.GetPersistentVolumeClaimClass(pvc)) if isFullPVC { if pvc.ObjectMeta.DeletionTimestamp != nil { w.Write(LEVEL_0, "Status:\tTerminating (lasts %s)\n", translateTimestampSince(*pvc.ObjectMeta.DeletionTimestamp)) } else { w.Write(LEVEL_0, "Status:\t%v\n", pvc.Status.Phase) } } w.Write(LEVEL_0, "Volume:\t%s\n", pvc.Spec.VolumeName) printLabelsMultiline(w, "Labels", pvc.Labels) printAnnotationsMultiline(w, "Annotations", pvc.Annotations) if isFullPVC { w.Write(LEVEL_0, "Finalizers:\t%v\n", pvc.ObjectMeta.Finalizers) } storage := pvc.Spec.Resources.Requests[corev1.ResourceStorage] capacity := "" accessModes := "" if pvc.Spec.VolumeName != "" { accessModes = storageutil.GetAccessModesAsString(pvc.Status.AccessModes) storage = pvc.Status.Capacity[corev1.ResourceStorage] capacity = storage.String() } w.Write(LEVEL_0, "Capacity:\t%s\n", capacity) w.Write(LEVEL_0, "Access Modes:\t%s\n", accessModes) if pvc.Spec.VolumeMode != nil { w.Write(LEVEL_0, "VolumeMode:\t%v\n", *pvc.Spec.VolumeMode) } if pvc.Spec.DataSource != nil { w.Write(LEVEL_0, "DataSource:\n") if pvc.Spec.DataSource.APIGroup != nil { w.Write(LEVEL_1, "APIGroup:\t%v\n", *pvc.Spec.DataSource.APIGroup) } w.Write(LEVEL_1, "Kind:\t%v\n", pvc.Spec.DataSource.Kind) w.Write(LEVEL_1, "Name:\t%v\n", pvc.Spec.DataSource.Name) } } func describeContainers(label string, containers []corev1.Container, containerStatuses []corev1.ContainerStatus, resolverFn EnvVarResolverFunc, w PrefixWriter, space string) { statuses := map[string]corev1.ContainerStatus{} for _, status := range containerStatuses { statuses[status.Name] = status } describeContainersLabel(containers, label, space, w) for _, container := range containers { status, ok := statuses[container.Name] describeContainerBasicInfo(container, status, ok, space, w) describeContainerCommand(container, w) if ok { describeContainerState(status, w) } describeResources(&container.Resources, w, LEVEL_2) describeContainerProbe(container, w) if len(container.EnvFrom) > 0 { describeContainerEnvFrom(container, resolverFn, w) } describeContainerEnvVars(container, resolverFn, w) describeContainerVolumes(container, w) } } func describeContainersLabel(containers []corev1.Container, label, space string, w PrefixWriter) { none := "" if len(containers) == 0 { none = " " } w.Write(LEVEL_0, "%s%s:%s\n", space, label, none) } func describeContainerBasicInfo(container corev1.Container, status corev1.ContainerStatus, ok bool, space string, w PrefixWriter) { nameIndent := "" if len(space) > 0 { nameIndent = " " } w.Write(LEVEL_1, "%s%v:\n", nameIndent, container.Name) if ok { w.Write(LEVEL_2, "Container ID:\t%s\n", status.ContainerID) } w.Write(LEVEL_2, "Image:\t%s\n", container.Image) if ok { w.Write(LEVEL_2, "Image ID:\t%s\n", status.ImageID) } portString := describeContainerPorts(container.Ports) if strings.Contains(portString, ",") { w.Write(LEVEL_2, "Ports:\t%s\n", portString) } else { w.Write(LEVEL_2, "Port:\t%s\n", stringOrNone(portString)) } hostPortString := describeContainerHostPorts(container.Ports) if strings.Contains(hostPortString, ",") { w.Write(LEVEL_2, "Host Ports:\t%s\n", hostPortString) } else { w.Write(LEVEL_2, "Host Port:\t%s\n", stringOrNone(hostPortString)) } if container.SecurityContext != nil && container.SecurityContext.SeccompProfile != nil { w.Write(LEVEL_2, "SeccompProfile:\t%s\n", container.SecurityContext.SeccompProfile.Type) if container.SecurityContext.SeccompProfile.Type == corev1.SeccompProfileTypeLocalhost { w.Write(LEVEL_3, "LocalhostProfile:\t%s\n", *container.SecurityContext.SeccompProfile.LocalhostProfile) } } } func describeContainerPorts(cPorts []corev1.ContainerPort) string { ports := make([]string, 0, len(cPorts)) for _, cPort := range cPorts { ports = append(ports, fmt.Sprintf("%d/%s", cPort.ContainerPort, cPort.Protocol)) } return strings.Join(ports, ", ") } func describeContainerHostPorts(cPorts []corev1.ContainerPort) string { ports := make([]string, 0, len(cPorts)) for _, cPort := range cPorts { ports = append(ports, fmt.Sprintf("%d/%s", cPort.HostPort, cPort.Protocol)) } return strings.Join(ports, ", ") } func describeContainerCommand(container corev1.Container, w PrefixWriter) { if len(container.Command) > 0 { w.Write(LEVEL_2, "Command:\n") for _, c := range container.Command { for _, s := range strings.Split(c, "\n") { w.Write(LEVEL_3, "%s\n", s) } } } if len(container.Args) > 0 { w.Write(LEVEL_2, "Args:\n") for _, arg := range container.Args { for _, s := range strings.Split(arg, "\n") { w.Write(LEVEL_3, "%s\n", s) } } } } func describeResources(resources *corev1.ResourceRequirements, w PrefixWriter, level int) { if resources == nil { return } if len(resources.Limits) > 0 { w.Write(level, "Limits:\n") } for _, name := range SortedResourceNames(resources.Limits) { quantity := resources.Limits[name] w.Write(level+1, "%s:\t%s\n", name, quantity.String()) } if len(resources.Requests) > 0 { w.Write(level, "Requests:\n") } for _, name := range SortedResourceNames(resources.Requests) { quantity := resources.Requests[name] w.Write(level+1, "%s:\t%s\n", name, quantity.String()) } } func describeContainerState(status corev1.ContainerStatus, w PrefixWriter) { describeStatus("State", status.State, w) if status.LastTerminationState.Terminated != nil { describeStatus("Last State", status.LastTerminationState, w) } w.Write(LEVEL_2, "Ready:\t%v\n", printBool(status.Ready)) w.Write(LEVEL_2, "Restart Count:\t%d\n", status.RestartCount) } func describeContainerProbe(container corev1.Container, w PrefixWriter) { if container.LivenessProbe != nil { probe := DescribeProbe(container.LivenessProbe) w.Write(LEVEL_2, "Liveness:\t%s\n", probe) } if container.ReadinessProbe != nil { probe := DescribeProbe(container.ReadinessProbe) w.Write(LEVEL_2, "Readiness:\t%s\n", probe) } if container.StartupProbe != nil { probe := DescribeProbe(container.StartupProbe) w.Write(LEVEL_2, "Startup:\t%s\n", probe) } } func describeContainerVolumes(container corev1.Container, w PrefixWriter) { // Show volumeMounts none := "" if len(container.VolumeMounts) == 0 { none = "\t" } w.Write(LEVEL_2, "Mounts:%s\n", none) sort.Sort(SortableVolumeMounts(container.VolumeMounts)) for _, mount := range container.VolumeMounts { flags := []string{} if mount.ReadOnly { flags = append(flags, "ro") } else { flags = append(flags, "rw") } if len(mount.SubPath) > 0 { flags = append(flags, fmt.Sprintf("path=%q", mount.SubPath)) } w.Write(LEVEL_3, "%s from %s (%s)\n", mount.MountPath, mount.Name, strings.Join(flags, ",")) } // Show volumeDevices if exists if len(container.VolumeDevices) > 0 { w.Write(LEVEL_2, "Devices:%s\n", none) sort.Sort(SortableVolumeDevices(container.VolumeDevices)) for _, device := range container.VolumeDevices { w.Write(LEVEL_3, "%s from %s\n", device.DevicePath, device.Name) } } } func describeContainerEnvVars(container corev1.Container, resolverFn EnvVarResolverFunc, w PrefixWriter) { none := "" if len(container.Env) == 0 { none = "\t" } w.Write(LEVEL_2, "Environment:%s\n", none) for _, e := range container.Env { if e.ValueFrom == nil { for i, s := range strings.Split(e.Value, "\n") { if i == 0 { w.Write(LEVEL_3, "%s:\t%s\n", e.Name, s) } else { w.Write(LEVEL_3, "\t%s\n", s) } } continue } switch { case e.ValueFrom.FieldRef != nil: var valueFrom string if resolverFn != nil { valueFrom = resolverFn(e) } w.Write(LEVEL_3, "%s:\t%s (%s:%s)\n", e.Name, valueFrom, e.ValueFrom.FieldRef.APIVersion, e.ValueFrom.FieldRef.FieldPath) case e.ValueFrom.ResourceFieldRef != nil: valueFrom, err := resourcehelper.ExtractContainerResourceValue(e.ValueFrom.ResourceFieldRef, &container) if err != nil { valueFrom = "" } resource := e.ValueFrom.ResourceFieldRef.Resource if valueFrom == "0" && (resource == "limits.cpu" || resource == "limits.memory") { valueFrom = "node allocatable" } w.Write(LEVEL_3, "%s:\t%s (%s)\n", e.Name, valueFrom, resource) case e.ValueFrom.SecretKeyRef != nil: optional := e.ValueFrom.SecretKeyRef.Optional != nil && *e.ValueFrom.SecretKeyRef.Optional w.Write(LEVEL_3, "%s:\t\tOptional: %t\n", e.Name, e.ValueFrom.SecretKeyRef.Key, e.ValueFrom.SecretKeyRef.Name, optional) case e.ValueFrom.ConfigMapKeyRef != nil: optional := e.ValueFrom.ConfigMapKeyRef.Optional != nil && *e.ValueFrom.ConfigMapKeyRef.Optional w.Write(LEVEL_3, "%s:\t\tOptional: %t\n", e.Name, e.ValueFrom.ConfigMapKeyRef.Key, e.ValueFrom.ConfigMapKeyRef.Name, optional) } } } func describeContainerEnvFrom(container corev1.Container, resolverFn EnvVarResolverFunc, w PrefixWriter) { none := "" if len(container.EnvFrom) == 0 { none = "\t" } w.Write(LEVEL_2, "Environment Variables from:%s\n", none) for _, e := range container.EnvFrom { from := "" name := "" optional := false if e.ConfigMapRef != nil { from = "ConfigMap" name = e.ConfigMapRef.Name optional = e.ConfigMapRef.Optional != nil && *e.ConfigMapRef.Optional } else if e.SecretRef != nil { from = "Secret" name = e.SecretRef.Name optional = e.SecretRef.Optional != nil && *e.SecretRef.Optional } if len(e.Prefix) == 0 { w.Write(LEVEL_3, "%s\t%s\tOptional: %t\n", name, from, optional) } else { w.Write(LEVEL_3, "%s\t%s with prefix '%s'\tOptional: %t\n", name, from, e.Prefix, optional) } } } // DescribeProbe is exported for consumers in other API groups that have probes func DescribeProbe(probe *corev1.Probe) string { attrs := fmt.Sprintf("delay=%ds timeout=%ds period=%ds #success=%d #failure=%d", probe.InitialDelaySeconds, probe.TimeoutSeconds, probe.PeriodSeconds, probe.SuccessThreshold, probe.FailureThreshold) switch { case probe.Exec != nil: return fmt.Sprintf("exec %v %s", probe.Exec.Command, attrs) case probe.HTTPGet != nil: url := &url.URL{} url.Scheme = strings.ToLower(string(probe.HTTPGet.Scheme)) if len(probe.HTTPGet.Port.String()) > 0 { url.Host = net.JoinHostPort(probe.HTTPGet.Host, probe.HTTPGet.Port.String()) } else { url.Host = probe.HTTPGet.Host } url.Path = probe.HTTPGet.Path return fmt.Sprintf("http-get %s %s", url.String(), attrs) case probe.TCPSocket != nil: return fmt.Sprintf("tcp-socket %s:%s %s", probe.TCPSocket.Host, probe.TCPSocket.Port.String(), attrs) case probe.GRPC != nil: return fmt.Sprintf("grpc :%d %s %s", probe.GRPC.Port, *(probe.GRPC.Service), attrs) } return fmt.Sprintf("unknown %s", attrs) } type EnvVarResolverFunc func(e corev1.EnvVar) string // EnvValueFrom is exported for use by describers in other packages func EnvValueRetriever(pod *corev1.Pod) EnvVarResolverFunc { return func(e corev1.EnvVar) string { gv, err := schema.ParseGroupVersion(e.ValueFrom.FieldRef.APIVersion) if err != nil { return "" } gvk := gv.WithKind("Pod") internalFieldPath, _, err := scheme.Scheme.ConvertFieldLabel(gvk, e.ValueFrom.FieldRef.FieldPath, "") if err != nil { return "" // pod validation should catch this on create } valueFrom, err := fieldpath.ExtractFieldPathAsString(pod, internalFieldPath) if err != nil { return "" // pod validation should catch this on create } return valueFrom } } func describeStatus(stateName string, state corev1.ContainerState, w PrefixWriter) { switch { case state.Running != nil: w.Write(LEVEL_2, "%s:\tRunning\n", stateName) w.Write(LEVEL_3, "Started:\t%v\n", state.Running.StartedAt.Time.Format(time.RFC1123Z)) case state.Waiting != nil: w.Write(LEVEL_2, "%s:\tWaiting\n", stateName) if state.Waiting.Reason != "" { w.Write(LEVEL_3, "Reason:\t%s\n", state.Waiting.Reason) } case state.Terminated != nil: w.Write(LEVEL_2, "%s:\tTerminated\n", stateName) if state.Terminated.Reason != "" { w.Write(LEVEL_3, "Reason:\t%s\n", state.Terminated.Reason) } if state.Terminated.Message != "" { w.Write(LEVEL_3, "Message:\t%s\n", state.Terminated.Message) } w.Write(LEVEL_3, "Exit Code:\t%d\n", state.Terminated.ExitCode) if state.Terminated.Signal > 0 { w.Write(LEVEL_3, "Signal:\t%d\n", state.Terminated.Signal) } w.Write(LEVEL_3, "Started:\t%s\n", state.Terminated.StartedAt.Time.Format(time.RFC1123Z)) w.Write(LEVEL_3, "Finished:\t%s\n", state.Terminated.FinishedAt.Time.Format(time.RFC1123Z)) default: w.Write(LEVEL_2, "%s:\tWaiting\n", stateName) } } func describeVolumeClaimTemplates(templates []corev1.PersistentVolumeClaim, w PrefixWriter) { if len(templates) == 0 { w.Write(LEVEL_0, "Volume Claims:\t\n") return } w.Write(LEVEL_0, "Volume Claims:\n") for _, pvc := range templates { w.Write(LEVEL_1, "Name:\t%s\n", pvc.Name) w.Write(LEVEL_1, "StorageClass:\t%s\n", storageutil.GetPersistentVolumeClaimClass(&pvc)) printLabelsMultilineWithIndent(w, " ", "Labels", "\t", pvc.Labels, sets.New[string]()) printLabelsMultilineWithIndent(w, " ", "Annotations", "\t", pvc.Annotations, sets.New[string]()) if capacity, ok := pvc.Spec.Resources.Requests[corev1.ResourceStorage]; ok { w.Write(LEVEL_1, "Capacity:\t%s\n", capacity.String()) } else { w.Write(LEVEL_1, "Capacity:\t%s\n", "") } w.Write(LEVEL_1, "Access Modes:\t%s\n", pvc.Spec.AccessModes) } } func printBoolPtr(value *bool) string { if value != nil { return printBool(*value) } return "" } func printBool(value bool) string { if value { return "True" } return "False" } // ReplicationControllerDescriber generates information about a replication controller // and the pods it has created. type ReplicationControllerDescriber struct { clientset.Interface } func (d *ReplicationControllerDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { rc := d.CoreV1().ReplicationControllers(namespace) pc := d.CoreV1().Pods(namespace) controller, err := rc.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } selector := labels.SelectorFromSet(controller.Spec.Selector) running, waiting, succeeded, failed, err := getPodStatusForController(pc, selector, controller.UID, describerSettings) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), controller, describerSettings.ChunkSize) } return describeReplicationController(controller, events, running, waiting, succeeded, failed) } func describeReplicationController(controller *corev1.ReplicationController, events *corev1.EventList, running, waiting, succeeded, failed int) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", controller.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", controller.Namespace) w.Write(LEVEL_0, "Selector:\t%s\n", labels.FormatLabels(controller.Spec.Selector)) printLabelsMultiline(w, "Labels", controller.Labels) printAnnotationsMultiline(w, "Annotations", controller.Annotations) w.Write(LEVEL_0, "Replicas:\t%d current / %d desired\n", controller.Status.Replicas, *controller.Spec.Replicas) w.Write(LEVEL_0, "Pods Status:\t%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed) DescribePodTemplate(controller.Spec.Template, w) if len(controller.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n Type\tStatus\tReason\n") w.Write(LEVEL_1, "----\t------\t------\n") for _, c := range controller.Status.Conditions { w.Write(LEVEL_1, "%v \t%v\t%v\n", c.Type, c.Status, c.Reason) } } if events != nil { DescribeEvents(events, w) } return nil }) } func DescribePodTemplate(template *corev1.PodTemplateSpec, w PrefixWriter) { w.Write(LEVEL_0, "Pod Template:\n") if template == nil { w.Write(LEVEL_1, "") return } printLabelsMultiline(w, " Labels", template.Labels) if len(template.Annotations) > 0 { printAnnotationsMultiline(w, " Annotations", template.Annotations) } if len(template.Spec.ServiceAccountName) > 0 { w.Write(LEVEL_1, "Service Account:\t%s\n", template.Spec.ServiceAccountName) } if len(template.Spec.InitContainers) > 0 { describeContainers("Init Containers", template.Spec.InitContainers, nil, nil, w, " ") } describeContainers("Containers", template.Spec.Containers, nil, nil, w, " ") describeVolumes(template.Spec.Volumes, w, " ") describeTopologySpreadConstraints(template.Spec.TopologySpreadConstraints, w, " ") if len(template.Spec.PriorityClassName) > 0 { w.Write(LEVEL_1, "Priority Class Name:\t%s\n", template.Spec.PriorityClassName) } printLabelsMultiline(w, " Node-Selectors", template.Spec.NodeSelector) printPodTolerationsMultiline(w, " Tolerations", template.Spec.Tolerations) } // ReplicaSetDescriber generates information about a ReplicaSet and the pods it has created. type ReplicaSetDescriber struct { clientset.Interface } func (d *ReplicaSetDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { rsc := d.AppsV1().ReplicaSets(namespace) pc := d.CoreV1().Pods(namespace) rs, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) if err != nil { return "", err } running, waiting, succeeded, failed, getPodErr := getPodStatusForController(pc, selector, rs.UID, describerSettings) var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), rs, describerSettings.ChunkSize) } return describeReplicaSet(rs, events, running, waiting, succeeded, failed, getPodErr) } func describeReplicaSet(rs *appsv1.ReplicaSet, events *corev1.EventList, running, waiting, succeeded, failed int, getPodErr error) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", rs.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", rs.Namespace) w.Write(LEVEL_0, "Selector:\t%s\n", metav1.FormatLabelSelector(rs.Spec.Selector)) printLabelsMultiline(w, "Labels", rs.Labels) printAnnotationsMultiline(w, "Annotations", rs.Annotations) if controlledBy := printController(rs); len(controlledBy) > 0 { w.Write(LEVEL_0, "Controlled By:\t%s\n", controlledBy) } w.Write(LEVEL_0, "Replicas:\t%d current / %d desired\n", rs.Status.Replicas, *rs.Spec.Replicas) w.Write(LEVEL_0, "Pods Status:\t") if getPodErr != nil { w.Write(LEVEL_0, "error in fetching pods: %s\n", getPodErr) } else { w.Write(LEVEL_0, "%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed) } DescribePodTemplate(&rs.Spec.Template, w) if len(rs.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n Type\tStatus\tReason\n") w.Write(LEVEL_1, "----\t------\t------\n") for _, c := range rs.Status.Conditions { w.Write(LEVEL_1, "%v \t%v\t%v\n", c.Type, c.Status, c.Reason) } } if events != nil { DescribeEvents(events, w) } return nil }) } // JobDescriber generates information about a job and the pods it has created. type JobDescriber struct { clientset.Interface } func (d *JobDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { job, err := d.BatchV1().Jobs(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), job, describerSettings.ChunkSize) } return describeJob(job, events) } func describeJob(job *batchv1.Job, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", job.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", job.Namespace) if selector, err := metav1.LabelSelectorAsSelector(job.Spec.Selector); err == nil { w.Write(LEVEL_0, "Selector:\t%s\n", selector) } else { w.Write(LEVEL_0, "Selector:\tFailed to get selector: %s\n", err) } printLabelsMultiline(w, "Labels", job.Labels) printAnnotationsMultiline(w, "Annotations", job.Annotations) if controlledBy := printController(job); len(controlledBy) > 0 { w.Write(LEVEL_0, "Controlled By:\t%s\n", controlledBy) } if job.Spec.Parallelism != nil { w.Write(LEVEL_0, "Parallelism:\t%d\n", *job.Spec.Parallelism) } if job.Spec.Completions != nil { w.Write(LEVEL_0, "Completions:\t%d\n", *job.Spec.Completions) } else { w.Write(LEVEL_0, "Completions:\t\n") } if job.Spec.CompletionMode != nil { w.Write(LEVEL_0, "Completion Mode:\t%s\n", *job.Spec.CompletionMode) } if job.Spec.Suspend != nil { w.Write(LEVEL_0, "Suspend:\t%v\n", *job.Spec.Suspend) } if job.Spec.BackoffLimit != nil { w.Write(LEVEL_0, "Backoff Limit:\t%v\n", *job.Spec.BackoffLimit) } if job.Spec.TTLSecondsAfterFinished != nil { w.Write(LEVEL_0, "TTL Seconds After Finished:\t%v\n", *job.Spec.TTLSecondsAfterFinished) } if job.Status.StartTime != nil { w.Write(LEVEL_0, "Start Time:\t%s\n", job.Status.StartTime.Time.Format(time.RFC1123Z)) } if job.Status.CompletionTime != nil { w.Write(LEVEL_0, "Completed At:\t%s\n", job.Status.CompletionTime.Time.Format(time.RFC1123Z)) } if job.Status.StartTime != nil && job.Status.CompletionTime != nil { w.Write(LEVEL_0, "Duration:\t%s\n", duration.HumanDuration(job.Status.CompletionTime.Sub(job.Status.StartTime.Time))) } if job.Spec.ActiveDeadlineSeconds != nil { w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *job.Spec.ActiveDeadlineSeconds) } if job.Status.Ready == nil { w.Write(LEVEL_0, "Pods Statuses:\t%d Active / %d Succeeded / %d Failed\n", job.Status.Active, job.Status.Succeeded, job.Status.Failed) } else { w.Write(LEVEL_0, "Pods Statuses:\t%d Active (%d Ready) / %d Succeeded / %d Failed\n", job.Status.Active, *job.Status.Ready, job.Status.Succeeded, job.Status.Failed) } if job.Spec.CompletionMode != nil && *job.Spec.CompletionMode == batchv1.IndexedCompletion { w.Write(LEVEL_0, "Completed Indexes:\t%s\n", capIndexesListOrNone(job.Status.CompletedIndexes, 50)) } DescribePodTemplate(&job.Spec.Template, w) if events != nil { DescribeEvents(events, w) } return nil }) } func capIndexesListOrNone(indexes string, softLimit int) string { if len(indexes) == 0 { return "" } ix := softLimit for ; ix < len(indexes); ix++ { if indexes[ix] == ',' { break } } if ix >= len(indexes) { return indexes } return indexes[:ix+1] + "..." } // CronJobDescriber generates information about a cron job and the jobs it has created. type CronJobDescriber struct { client clientset.Interface } func (d *CronJobDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var events *corev1.EventList cronJob, err := d.client.BatchV1().CronJobs(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } if describerSettings.ShowEvents { events, _ = searchEvents(d.client.CoreV1(), cronJob, describerSettings.ChunkSize) } return describeCronJob(cronJob, events) } func describeCronJob(cronJob *batchv1.CronJob, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", cronJob.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", cronJob.Namespace) printLabelsMultiline(w, "Labels", cronJob.Labels) printAnnotationsMultiline(w, "Annotations", cronJob.Annotations) w.Write(LEVEL_0, "Schedule:\t%s\n", cronJob.Spec.Schedule) w.Write(LEVEL_0, "Concurrency Policy:\t%s\n", cronJob.Spec.ConcurrencyPolicy) w.Write(LEVEL_0, "Suspend:\t%s\n", printBoolPtr(cronJob.Spec.Suspend)) if cronJob.Spec.SuccessfulJobsHistoryLimit != nil { w.Write(LEVEL_0, "Successful Job History Limit:\t%d\n", *cronJob.Spec.SuccessfulJobsHistoryLimit) } else { w.Write(LEVEL_0, "Successful Job History Limit:\t\n") } if cronJob.Spec.FailedJobsHistoryLimit != nil { w.Write(LEVEL_0, "Failed Job History Limit:\t%d\n", *cronJob.Spec.FailedJobsHistoryLimit) } else { w.Write(LEVEL_0, "Failed Job History Limit:\t\n") } if cronJob.Spec.StartingDeadlineSeconds != nil { w.Write(LEVEL_0, "Starting Deadline Seconds:\t%ds\n", *cronJob.Spec.StartingDeadlineSeconds) } else { w.Write(LEVEL_0, "Starting Deadline Seconds:\t\n") } describeJobTemplate(cronJob.Spec.JobTemplate, w) if cronJob.Status.LastScheduleTime != nil { w.Write(LEVEL_0, "Last Schedule Time:\t%s\n", cronJob.Status.LastScheduleTime.Time.Format(time.RFC1123Z)) } else { w.Write(LEVEL_0, "Last Schedule Time:\t\n") } printActiveJobs(w, "Active Jobs", cronJob.Status.Active) if events != nil { DescribeEvents(events, w) } return nil }) } func describeJobTemplate(jobTemplate batchv1.JobTemplateSpec, w PrefixWriter) { if jobTemplate.Spec.Selector != nil { if selector, err := metav1.LabelSelectorAsSelector(jobTemplate.Spec.Selector); err == nil { w.Write(LEVEL_0, "Selector:\t%s\n", selector) } else { w.Write(LEVEL_0, "Selector:\tFailed to get selector: %s\n", err) } } else { w.Write(LEVEL_0, "Selector:\t\n") } if jobTemplate.Spec.Parallelism != nil { w.Write(LEVEL_0, "Parallelism:\t%d\n", *jobTemplate.Spec.Parallelism) } else { w.Write(LEVEL_0, "Parallelism:\t\n") } if jobTemplate.Spec.Completions != nil { w.Write(LEVEL_0, "Completions:\t%d\n", *jobTemplate.Spec.Completions) } else { w.Write(LEVEL_0, "Completions:\t\n") } if jobTemplate.Spec.ActiveDeadlineSeconds != nil { w.Write(LEVEL_0, "Active Deadline Seconds:\t%ds\n", *jobTemplate.Spec.ActiveDeadlineSeconds) } DescribePodTemplate(&jobTemplate.Spec.Template, w) } func printActiveJobs(w PrefixWriter, title string, jobs []corev1.ObjectReference) { w.Write(LEVEL_0, "%s:\t", title) if len(jobs) == 0 { w.WriteLine("") return } for i, job := range jobs { if i != 0 { w.Write(LEVEL_0, ", ") } w.Write(LEVEL_0, "%s", job.Name) } w.WriteLine("") } // DaemonSetDescriber generates information about a daemon set and the pods it has created. type DaemonSetDescriber struct { clientset.Interface } func (d *DaemonSetDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { dc := d.AppsV1().DaemonSets(namespace) pc := d.CoreV1().Pods(namespace) daemon, err := dc.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } selector, err := metav1.LabelSelectorAsSelector(daemon.Spec.Selector) if err != nil { return "", err } running, waiting, succeeded, failed, err := getPodStatusForController(pc, selector, daemon.UID, describerSettings) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), daemon, describerSettings.ChunkSize) } return describeDaemonSet(daemon, selector, events, running, waiting, succeeded, failed) } func describeDaemonSet(daemon *appsv1.DaemonSet, selector labels.Selector, events *corev1.EventList, running, waiting, succeeded, failed int) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", daemon.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", daemon.Namespace) w.Write(LEVEL_0, "Selector:\t%s\n", selector) w.Write(LEVEL_0, "Node-Selector:\t%s\n", labels.FormatLabels(daemon.Spec.Template.Spec.NodeSelector)) printLabelsMultiline(w, "Labels", daemon.Labels) printAnnotationsMultiline(w, "Annotations", daemon.Annotations) w.Write(LEVEL_0, "Desired Number of Nodes Scheduled: %d\n", daemon.Status.DesiredNumberScheduled) w.Write(LEVEL_0, "Current Number of Nodes Scheduled: %d\n", daemon.Status.CurrentNumberScheduled) w.Write(LEVEL_0, "Number of Nodes Scheduled with Up-to-date Pods: %d\n", daemon.Status.UpdatedNumberScheduled) w.Write(LEVEL_0, "Number of Nodes Scheduled with Available Pods: %d\n", daemon.Status.NumberAvailable) w.Write(LEVEL_0, "Number of Nodes Misscheduled: %d\n", daemon.Status.NumberMisscheduled) w.Write(LEVEL_0, "Pods Status:\t%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed) DescribePodTemplate(&daemon.Spec.Template, w) if events != nil { DescribeEvents(events, w) } return nil }) } // SecretDescriber generates information about a secret type SecretDescriber struct { clientset.Interface } func (d *SecretDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.CoreV1().Secrets(namespace) secret, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } return describeSecret(secret) } func describeSecret(secret *corev1.Secret) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", secret.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", secret.Namespace) printLabelsMultiline(w, "Labels", secret.Labels) printAnnotationsMultiline(w, "Annotations", secret.Annotations) w.Write(LEVEL_0, "\nType:\t%s\n", secret.Type) w.Write(LEVEL_0, "\nData\n====\n") for k, v := range secret.Data { switch { case k == corev1.ServiceAccountTokenKey && secret.Type == corev1.SecretTypeServiceAccountToken: w.Write(LEVEL_0, "%s:\t%s\n", k, string(v)) default: w.Write(LEVEL_0, "%s:\t%d bytes\n", k, len(v)) } } return nil }) } type IngressDescriber struct { client clientset.Interface } func (i *IngressDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var events *corev1.EventList // try ingress/v1 first (v1.19) and fallback to ingress/v1beta if an err occurs netV1, err := i.client.NetworkingV1().Ingresses(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(i.client.CoreV1(), netV1, describerSettings.ChunkSize) } return i.describeIngressV1(netV1, events) } netV1beta1, err := i.client.NetworkingV1beta1().Ingresses(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(i.client.CoreV1(), netV1beta1, describerSettings.ChunkSize) } return i.describeIngressV1beta1(netV1beta1, events) } return "", err } func (i *IngressDescriber) describeBackendV1beta1(ns string, backend *networkingv1beta1.IngressBackend) string { endpointSliceList, err := i.client.DiscoveryV1().EndpointSlices(ns).List(context.TODO(), metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", discoveryv1.LabelServiceName, backend.ServiceName), }) if err != nil { return fmt.Sprintf("", err) } service, err := i.client.CoreV1().Services(ns).Get(context.TODO(), backend.ServiceName, metav1.GetOptions{}) if err != nil { return fmt.Sprintf("", err) } spName := "" for i := range service.Spec.Ports { sp := &service.Spec.Ports[i] switch backend.ServicePort.Type { case intstr.String: if backend.ServicePort.StrVal == sp.Name { spName = sp.Name } case intstr.Int: if int32(backend.ServicePort.IntVal) == sp.Port { spName = sp.Name } } } return formatEndpointSlices(endpointSliceList.Items, sets.New(spName)) } func (i *IngressDescriber) describeBackendV1(ns string, backend *networkingv1.IngressBackend) string { if backend.Service != nil { sb := serviceBackendStringer(backend.Service) endpointSliceList, err := i.client.DiscoveryV1().EndpointSlices(ns).List(context.TODO(), metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", discoveryv1.LabelServiceName, backend.Service.Name), }) if err != nil { return fmt.Sprintf("%v ()", sb, err) } service, err := i.client.CoreV1().Services(ns).Get(context.TODO(), backend.Service.Name, metav1.GetOptions{}) if err != nil { return fmt.Sprintf("%v ()", sb, err) } spName := "" for i := range service.Spec.Ports { sp := &service.Spec.Ports[i] if backend.Service.Port.Number != 0 && backend.Service.Port.Number == sp.Port { spName = sp.Name } else if len(backend.Service.Port.Name) > 0 && backend.Service.Port.Name == sp.Name { spName = sp.Name } } ep := formatEndpointSlices(endpointSliceList.Items, sets.New(spName)) return fmt.Sprintf("%s (%s)", sb, ep) } if backend.Resource != nil { ic := backend.Resource apiGroup := "" if ic.APIGroup != nil { apiGroup = fmt.Sprintf("%v", *ic.APIGroup) } return fmt.Sprintf("APIGroup: %v, Kind: %v, Name: %v", apiGroup, ic.Kind, ic.Name) } return "" } func (i *IngressDescriber) describeIngressV1(ing *networkingv1.Ingress, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%v\n", ing.Name) printLabelsMultiline(w, "Labels", ing.Labels) w.Write(LEVEL_0, "Namespace:\t%v\n", ing.Namespace) w.Write(LEVEL_0, "Address:\t%v\n", ingressLoadBalancerStatusStringerV1(ing.Status.LoadBalancer, true)) ingressClassName := "" if ing.Spec.IngressClassName != nil { ingressClassName = *ing.Spec.IngressClassName } w.Write(LEVEL_0, "Ingress Class:\t%v\n", ingressClassName) def := ing.Spec.DefaultBackend ns := ing.Namespace defaultBackendDescribe := "" if def != nil { defaultBackendDescribe = i.describeBackendV1(ns, def) } w.Write(LEVEL_0, "Default backend:\t%s\n", defaultBackendDescribe) if len(ing.Spec.TLS) != 0 { describeIngressTLSV1(w, ing.Spec.TLS) } w.Write(LEVEL_0, "Rules:\n Host\tPath\tBackends\n") w.Write(LEVEL_1, "----\t----\t--------\n") count := 0 for _, rules := range ing.Spec.Rules { if rules.HTTP == nil { continue } count++ host := rules.Host if len(host) == 0 { host = "*" } w.Write(LEVEL_1, "%s\t\n", host) for _, path := range rules.HTTP.Paths { w.Write(LEVEL_2, "\t%s \t%s\n", path.Path, i.describeBackendV1(ing.Namespace, &path.Backend)) } } if count == 0 { w.Write(LEVEL_1, "%s\t%s\t%s\n", "*", "*", defaultBackendDescribe) } printAnnotationsMultiline(w, "Annotations", ing.Annotations) if events != nil { DescribeEvents(events, w) } return nil }) } func (i *IngressDescriber) describeIngressV1beta1(ing *networkingv1beta1.Ingress, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%v\n", ing.Name) printLabelsMultiline(w, "Labels", ing.Labels) w.Write(LEVEL_0, "Namespace:\t%v\n", ing.Namespace) w.Write(LEVEL_0, "Address:\t%v\n", ingressLoadBalancerStatusStringerV1beta1(ing.Status.LoadBalancer, true)) ingressClassName := "" if ing.Spec.IngressClassName != nil { ingressClassName = *ing.Spec.IngressClassName } w.Write(LEVEL_0, "Ingress Class:\t%v\n", ingressClassName) def := ing.Spec.Backend ns := ing.Namespace if def == nil { w.Write(LEVEL_0, "Default backend:\t\n") } else { w.Write(LEVEL_0, "Default backend:\t%s\n", i.describeBackendV1beta1(ns, def)) } if len(ing.Spec.TLS) != 0 { describeIngressTLSV1beta1(w, ing.Spec.TLS) } w.Write(LEVEL_0, "Rules:\n Host\tPath\tBackends\n") w.Write(LEVEL_1, "----\t----\t--------\n") count := 0 for _, rules := range ing.Spec.Rules { if rules.HTTP == nil { continue } count++ host := rules.Host if len(host) == 0 { host = "*" } w.Write(LEVEL_1, "%s\t\n", host) for _, path := range rules.HTTP.Paths { w.Write(LEVEL_2, "\t%s \t%s (%s)\n", path.Path, backendStringer(&path.Backend), i.describeBackendV1beta1(ing.Namespace, &path.Backend)) } } if count == 0 { w.Write(LEVEL_1, "%s\t%s \t\n", "*", "*") } printAnnotationsMultiline(w, "Annotations", ing.Annotations) if events != nil { DescribeEvents(events, w) } return nil }) } func describeIngressTLSV1beta1(w PrefixWriter, ingTLS []networkingv1beta1.IngressTLS) { w.Write(LEVEL_0, "TLS:\n") for _, t := range ingTLS { if t.SecretName == "" { w.Write(LEVEL_1, "SNI routes %v\n", strings.Join(t.Hosts, ",")) } else { w.Write(LEVEL_1, "%v terminates %v\n", t.SecretName, strings.Join(t.Hosts, ",")) } } } func describeIngressTLSV1(w PrefixWriter, ingTLS []networkingv1.IngressTLS) { w.Write(LEVEL_0, "TLS:\n") for _, t := range ingTLS { if t.SecretName == "" { w.Write(LEVEL_1, "SNI routes %v\n", strings.Join(t.Hosts, ",")) } else { w.Write(LEVEL_1, "%v terminates %v\n", t.SecretName, strings.Join(t.Hosts, ",")) } } } type IngressClassDescriber struct { client clientset.Interface } func (i *IngressClassDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var events *corev1.EventList // try IngressClass/v1 first (v1.19) and fallback to IngressClass/v1beta if an err occurs netV1, err := i.client.NetworkingV1().IngressClasses().Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(i.client.CoreV1(), netV1, describerSettings.ChunkSize) } return i.describeIngressClassV1(netV1, events) } netV1beta1, err := i.client.NetworkingV1beta1().IngressClasses().Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(i.client.CoreV1(), netV1beta1, describerSettings.ChunkSize) } return i.describeIngressClassV1beta1(netV1beta1, events) } return "", err } func (i *IngressClassDescriber) describeIngressClassV1beta1(ic *networkingv1beta1.IngressClass, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", ic.Name) printLabelsMultiline(w, "Labels", ic.Labels) printAnnotationsMultiline(w, "Annotations", ic.Annotations) w.Write(LEVEL_0, "Controller:\t%v\n", ic.Spec.Controller) if ic.Spec.Parameters != nil { w.Write(LEVEL_0, "Parameters:\n") if ic.Spec.Parameters.APIGroup != nil { w.Write(LEVEL_1, "APIGroup:\t%v\n", *ic.Spec.Parameters.APIGroup) } w.Write(LEVEL_1, "Kind:\t%v\n", ic.Spec.Parameters.Kind) w.Write(LEVEL_1, "Name:\t%v\n", ic.Spec.Parameters.Name) } if events != nil { DescribeEvents(events, w) } return nil }) } func (i *IngressClassDescriber) describeIngressClassV1(ic *networkingv1.IngressClass, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", ic.Name) printLabelsMultiline(w, "Labels", ic.Labels) printAnnotationsMultiline(w, "Annotations", ic.Annotations) w.Write(LEVEL_0, "Controller:\t%v\n", ic.Spec.Controller) if ic.Spec.Parameters != nil { w.Write(LEVEL_0, "Parameters:\n") if ic.Spec.Parameters.APIGroup != nil { w.Write(LEVEL_1, "APIGroup:\t%v\n", *ic.Spec.Parameters.APIGroup) } w.Write(LEVEL_1, "Kind:\t%v\n", ic.Spec.Parameters.Kind) w.Write(LEVEL_1, "Name:\t%v\n", ic.Spec.Parameters.Name) } if events != nil { DescribeEvents(events, w) } return nil }) } // ServiceCIDRDescriber generates information about a ServiceCIDR. type ServiceCIDRDescriber struct { client clientset.Interface } func (c *ServiceCIDRDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var events *corev1.EventList svcV1, err := c.client.NetworkingV1().ServiceCIDRs().Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(c.client.CoreV1(), svcV1, describerSettings.ChunkSize) } return c.describeServiceCIDRV1(svcV1, events) } svcV1beta1, err := c.client.NetworkingV1beta1().ServiceCIDRs().Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(c.client.CoreV1(), svcV1beta1, describerSettings.ChunkSize) } return c.describeServiceCIDRV1beta1(svcV1beta1, events) } return "", err } func (c *ServiceCIDRDescriber) describeServiceCIDRV1(svc *networkingv1.ServiceCIDR, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%v\n", svc.Name) printLabelsMultiline(w, "Labels", svc.Labels) printAnnotationsMultiline(w, "Annotations", svc.Annotations) w.Write(LEVEL_0, "CIDRs:\t%v\n", strings.Join(svc.Spec.CIDRs, ", ")) if len(svc.Status.Conditions) > 0 { w.Write(LEVEL_0, "Status:\n") w.Write(LEVEL_0, "Conditions:\n") w.Write(LEVEL_1, "Type\tStatus\tLastTransitionTime\tReason\tMessage\n") w.Write(LEVEL_1, "----\t------\t------------------\t------\t-------\n") for _, c := range svc.Status.Conditions { w.Write(LEVEL_1, "%v\t%v\t%s\t%v\t%v\n", c.Type, c.Status, c.LastTransitionTime.Time.Format(time.RFC1123Z), c.Reason, c.Message) } } if events != nil { DescribeEvents(events, w) } return nil }) } func (c *ServiceCIDRDescriber) describeServiceCIDRV1beta1(svc *networkingv1beta1.ServiceCIDR, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%v\n", svc.Name) printLabelsMultiline(w, "Labels", svc.Labels) printAnnotationsMultiline(w, "Annotations", svc.Annotations) w.Write(LEVEL_0, "CIDRs:\t%v\n", strings.Join(svc.Spec.CIDRs, ", ")) if len(svc.Status.Conditions) > 0 { w.Write(LEVEL_0, "Status:\n") w.Write(LEVEL_0, "Conditions:\n") w.Write(LEVEL_1, "Type\tStatus\tLastTransitionTime\tReason\tMessage\n") w.Write(LEVEL_1, "----\t------\t------------------\t------\t-------\n") for _, c := range svc.Status.Conditions { w.Write(LEVEL_1, "%v\t%v\t%s\t%v\t%v\n", c.Type, c.Status, c.LastTransitionTime.Time.Format(time.RFC1123Z), c.Reason, c.Message) } } if events != nil { DescribeEvents(events, w) } return nil }) } // IPAddressDescriber generates information about an IPAddress. type IPAddressDescriber struct { client clientset.Interface } func (c *IPAddressDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var events *corev1.EventList ipV1, err := c.client.NetworkingV1().IPAddresses().Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(c.client.CoreV1(), ipV1, describerSettings.ChunkSize) } return c.describeIPAddressV1(ipV1, events) } ipV1beta1, err := c.client.NetworkingV1beta1().IPAddresses().Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(c.client.CoreV1(), ipV1beta1, describerSettings.ChunkSize) } return c.describeIPAddressV1beta1(ipV1beta1, events) } return "", err } func (c *IPAddressDescriber) describeIPAddressV1(ip *networkingv1.IPAddress, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%v\n", ip.Name) printLabelsMultiline(w, "Labels", ip.Labels) printAnnotationsMultiline(w, "Annotations", ip.Annotations) if ip.Spec.ParentRef != nil { w.Write(LEVEL_0, "Parent Reference:\n") w.Write(LEVEL_1, "Group:\t%v\n", ip.Spec.ParentRef.Group) w.Write(LEVEL_1, "Resource:\t%v\n", ip.Spec.ParentRef.Resource) w.Write(LEVEL_1, "Namespace:\t%v\n", ip.Spec.ParentRef.Namespace) w.Write(LEVEL_1, "Name:\t%v\n", ip.Spec.ParentRef.Name) } if events != nil { DescribeEvents(events, w) } return nil }) } func (c *IPAddressDescriber) describeIPAddressV1beta1(ip *networkingv1beta1.IPAddress, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%v\n", ip.Name) printLabelsMultiline(w, "Labels", ip.Labels) printAnnotationsMultiline(w, "Annotations", ip.Annotations) if ip.Spec.ParentRef != nil { w.Write(LEVEL_0, "Parent Reference:\n") w.Write(LEVEL_1, "Group:\t%v\n", ip.Spec.ParentRef.Group) w.Write(LEVEL_1, "Resource:\t%v\n", ip.Spec.ParentRef.Resource) w.Write(LEVEL_1, "Namespace:\t%v\n", ip.Spec.ParentRef.Namespace) w.Write(LEVEL_1, "Name:\t%v\n", ip.Spec.ParentRef.Name) } if events != nil { DescribeEvents(events, w) } return nil }) } // ServiceDescriber generates information about a service. type ServiceDescriber struct { clientset.Interface } func (d *ServiceDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.CoreV1().Services(namespace) service, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } endpointSliceList, _ := d.DiscoveryV1().EndpointSlices(namespace).List(context.TODO(), metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", discoveryv1.LabelServiceName, name), }) var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), service, describerSettings.ChunkSize) } return describeService(service, endpointSliceList.Items, events) } func buildIngressString(ingress []corev1.LoadBalancerIngress) string { var buffer bytes.Buffer for i := range ingress { if i != 0 { buffer.WriteString(", ") } if ingress[i].IP != "" { buffer.WriteString(ingress[i].IP) if ingress[i].IPMode != nil { buffer.WriteString(fmt.Sprintf(" (%s)", *ingress[i].IPMode)) } } else { buffer.WriteString(ingress[i].Hostname) } } return buffer.String() } func describeService(service *corev1.Service, endpointSlices []discoveryv1.EndpointSlice, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", service.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", service.Namespace) printLabelsMultiline(w, "Labels", service.Labels) printAnnotationsMultiline(w, "Annotations", service.Annotations) w.Write(LEVEL_0, "Selector:\t%s\n", labels.FormatLabels(service.Spec.Selector)) w.Write(LEVEL_0, "Type:\t%s\n", service.Spec.Type) if service.Spec.IPFamilyPolicy != nil { w.Write(LEVEL_0, "IP Family Policy:\t%s\n", *(service.Spec.IPFamilyPolicy)) } if len(service.Spec.IPFamilies) > 0 { ipfamiliesStrings := make([]string, 0, len(service.Spec.IPFamilies)) for _, family := range service.Spec.IPFamilies { ipfamiliesStrings = append(ipfamiliesStrings, string(family)) } w.Write(LEVEL_0, "IP Families:\t%s\n", strings.Join(ipfamiliesStrings, ",")) } else { w.Write(LEVEL_0, "IP Families:\t%s\n", "") } w.Write(LEVEL_0, "IP:\t%s\n", service.Spec.ClusterIP) if len(service.Spec.ClusterIPs) > 0 { w.Write(LEVEL_0, "IPs:\t%s\n", strings.Join(service.Spec.ClusterIPs, ",")) } else { w.Write(LEVEL_0, "IPs:\t%s\n", "") } if len(service.Spec.ExternalIPs) > 0 { w.Write(LEVEL_0, "External IPs:\t%v\n", strings.Join(service.Spec.ExternalIPs, ",")) } if service.Spec.LoadBalancerIP != "" { w.Write(LEVEL_0, "Desired LoadBalancer IP:\t%s\n", service.Spec.LoadBalancerIP) } if service.Spec.ExternalName != "" { w.Write(LEVEL_0, "External Name:\t%s\n", service.Spec.ExternalName) } if len(service.Status.LoadBalancer.Ingress) > 0 { list := buildIngressString(service.Status.LoadBalancer.Ingress) w.Write(LEVEL_0, "LoadBalancer Ingress:\t%s\n", list) } for i := range service.Spec.Ports { sp := &service.Spec.Ports[i] name := sp.Name if name == "" { name = "" } w.Write(LEVEL_0, "Port:\t%s\t%d/%s\n", name, sp.Port, sp.Protocol) if sp.TargetPort.Type == intstr.Type(intstr.Int) { w.Write(LEVEL_0, "TargetPort:\t%d/%s\n", sp.TargetPort.IntVal, sp.Protocol) } else { w.Write(LEVEL_0, "TargetPort:\t%s/%s\n", sp.TargetPort.StrVal, sp.Protocol) } if sp.NodePort != 0 { w.Write(LEVEL_0, "NodePort:\t%s\t%d/%s\n", name, sp.NodePort, sp.Protocol) } w.Write(LEVEL_0, "Endpoints:\t%s\n", formatEndpointSlices(endpointSlices, sets.New(sp.Name))) } w.Write(LEVEL_0, "Session Affinity:\t%s\n", service.Spec.SessionAffinity) if service.Spec.ExternalTrafficPolicy != "" { w.Write(LEVEL_0, "External Traffic Policy:\t%s\n", service.Spec.ExternalTrafficPolicy) } if service.Spec.InternalTrafficPolicy != nil { w.Write(LEVEL_0, "Internal Traffic Policy:\t%s\n", *service.Spec.InternalTrafficPolicy) } if service.Spec.HealthCheckNodePort != 0 { w.Write(LEVEL_0, "HealthCheck NodePort:\t%d\n", service.Spec.HealthCheckNodePort) } if len(service.Spec.LoadBalancerSourceRanges) > 0 { w.Write(LEVEL_0, "LoadBalancer Source Ranges:\t%v\n", strings.Join(service.Spec.LoadBalancerSourceRanges, ",")) } if events != nil { DescribeEvents(events, w) } return nil }) } // EndpointsDescriber generates information about an Endpoint. type EndpointsDescriber struct { clientset.Interface } func (d *EndpointsDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.CoreV1().Endpoints(namespace) ep, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), ep, describerSettings.ChunkSize) } return describeEndpoints(ep, events) } func describeEndpoints(ep *corev1.Endpoints, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", ep.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", ep.Namespace) printLabelsMultiline(w, "Labels", ep.Labels) printAnnotationsMultiline(w, "Annotations", ep.Annotations) w.Write(LEVEL_0, "Subsets:\n") for i := range ep.Subsets { subset := &ep.Subsets[i] addresses := make([]string, 0, len(subset.Addresses)) for _, addr := range subset.Addresses { addresses = append(addresses, addr.IP) } addressesString := strings.Join(addresses, ",") if len(addressesString) == 0 { addressesString = "" } w.Write(LEVEL_1, "Addresses:\t%s\n", addressesString) notReadyAddresses := make([]string, 0, len(subset.NotReadyAddresses)) for _, addr := range subset.NotReadyAddresses { notReadyAddresses = append(notReadyAddresses, addr.IP) } notReadyAddressesString := strings.Join(notReadyAddresses, ",") if len(notReadyAddressesString) == 0 { notReadyAddressesString = "" } w.Write(LEVEL_1, "NotReadyAddresses:\t%s\n", notReadyAddressesString) if len(subset.Ports) > 0 { w.Write(LEVEL_1, "Ports:\n") w.Write(LEVEL_2, "Name\tPort\tProtocol\n") w.Write(LEVEL_2, "----\t----\t--------\n") for _, port := range subset.Ports { name := port.Name if len(name) == 0 { name = "" } w.Write(LEVEL_2, "%s\t%d\t%s\n", name, port.Port, port.Protocol) } } w.Write(LEVEL_0, "\n") } if events != nil { DescribeEvents(events, w) } return nil }) } // EndpointSliceDescriber generates information about an EndpointSlice. type EndpointSliceDescriber struct { clientset.Interface } func (d *EndpointSliceDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var events *corev1.EventList // try endpointslice/v1 first (v1.21) and fallback to v1beta1 if error occurs epsV1, err := d.DiscoveryV1().EndpointSlices(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), epsV1, describerSettings.ChunkSize) } return describeEndpointSliceV1(epsV1, events) } epsV1beta1, err := d.DiscoveryV1beta1().EndpointSlices(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), epsV1beta1, describerSettings.ChunkSize) } return describeEndpointSliceV1beta1(epsV1beta1, events) } func describeEndpointSliceV1(eps *discoveryv1.EndpointSlice, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", eps.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", eps.Namespace) printLabelsMultiline(w, "Labels", eps.Labels) printAnnotationsMultiline(w, "Annotations", eps.Annotations) w.Write(LEVEL_0, "AddressType:\t%s\n", string(eps.AddressType)) if len(eps.Ports) == 0 { w.Write(LEVEL_0, "Ports: \n") } else { w.Write(LEVEL_0, "Ports:\n") w.Write(LEVEL_1, "Name\tPort\tProtocol\n") w.Write(LEVEL_1, "----\t----\t--------\n") for _, port := range eps.Ports { portName := "" if port.Name != nil && len(*port.Name) > 0 { portName = *port.Name } portNum := "" if port.Port != nil { portNum = strconv.Itoa(int(*port.Port)) } w.Write(LEVEL_1, "%s\t%s\t%s\n", portName, portNum, *port.Protocol) } } if len(eps.Endpoints) == 0 { w.Write(LEVEL_0, "Endpoints: \n") } else { w.Write(LEVEL_0, "Endpoints:\n") for i := range eps.Endpoints { endpoint := &eps.Endpoints[i] addressesString := strings.Join(endpoint.Addresses, ", ") if len(addressesString) == 0 { addressesString = "" } w.Write(LEVEL_1, "- Addresses:\t%s\n", addressesString) w.Write(LEVEL_2, "Conditions:\n") readyText := "" if endpoint.Conditions.Ready != nil { readyText = strconv.FormatBool(*endpoint.Conditions.Ready) } w.Write(LEVEL_3, "Ready:\t%s\n", readyText) hostnameText := "" if endpoint.Hostname != nil { hostnameText = *endpoint.Hostname } w.Write(LEVEL_2, "Hostname:\t%s\n", hostnameText) if endpoint.TargetRef != nil { w.Write(LEVEL_2, "TargetRef:\t%s/%s\n", endpoint.TargetRef.Kind, endpoint.TargetRef.Name) } nodeNameText := "" if endpoint.NodeName != nil { nodeNameText = *endpoint.NodeName } w.Write(LEVEL_2, "NodeName:\t%s\n", nodeNameText) zoneText := "" if endpoint.Zone != nil { zoneText = *endpoint.Zone } w.Write(LEVEL_2, "Zone:\t%s\n", zoneText) } } if events != nil { DescribeEvents(events, w) } return nil }) } func describeEndpointSliceV1beta1(eps *discoveryv1beta1.EndpointSlice, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", eps.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", eps.Namespace) printLabelsMultiline(w, "Labels", eps.Labels) printAnnotationsMultiline(w, "Annotations", eps.Annotations) w.Write(LEVEL_0, "AddressType:\t%s\n", string(eps.AddressType)) if len(eps.Ports) == 0 { w.Write(LEVEL_0, "Ports: \n") } else { w.Write(LEVEL_0, "Ports:\n") w.Write(LEVEL_1, "Name\tPort\tProtocol\n") w.Write(LEVEL_1, "----\t----\t--------\n") for _, port := range eps.Ports { portName := "" if port.Name != nil && len(*port.Name) > 0 { portName = *port.Name } portNum := "" if port.Port != nil { portNum = strconv.Itoa(int(*port.Port)) } w.Write(LEVEL_1, "%s\t%s\t%s\n", portName, portNum, *port.Protocol) } } if len(eps.Endpoints) == 0 { w.Write(LEVEL_0, "Endpoints: \n") } else { w.Write(LEVEL_0, "Endpoints:\n") for i := range eps.Endpoints { endpoint := &eps.Endpoints[i] addressesString := strings.Join(endpoint.Addresses, ",") if len(addressesString) == 0 { addressesString = "" } w.Write(LEVEL_1, "- Addresses:\t%s\n", addressesString) w.Write(LEVEL_2, "Conditions:\n") readyText := "" if endpoint.Conditions.Ready != nil { readyText = strconv.FormatBool(*endpoint.Conditions.Ready) } w.Write(LEVEL_3, "Ready:\t%s\n", readyText) hostnameText := "" if endpoint.Hostname != nil { hostnameText = *endpoint.Hostname } w.Write(LEVEL_2, "Hostname:\t%s\n", hostnameText) if endpoint.TargetRef != nil { w.Write(LEVEL_2, "TargetRef:\t%s/%s\n", endpoint.TargetRef.Kind, endpoint.TargetRef.Name) } printLabelsMultilineWithIndent(w, " ", "Topology", "\t", endpoint.Topology, sets.New[string]()) } } if events != nil { DescribeEvents(events, w) } return nil }) } // ServiceAccountDescriber generates information about a service. type ServiceAccountDescriber struct { clientset.Interface } func (d *ServiceAccountDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.CoreV1().ServiceAccounts(namespace) serviceAccount, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } tokens := []corev1.Secret{} // missingSecrets is the set of all secrets present in the // serviceAccount but not present in the set of existing secrets. missingSecrets := sets.New[string]() secrets := corev1.SecretList{} err = runtimeresource.FollowContinue(&metav1.ListOptions{Limit: describerSettings.ChunkSize}, func(options metav1.ListOptions) (runtime.Object, error) { newList, err := d.CoreV1().Secrets(namespace).List(context.TODO(), options) if err != nil { return nil, runtimeresource.EnhanceListError(err, options, corev1.ResourceSecrets.String()) } secrets.Items = append(secrets.Items, newList.Items...) return newList, nil }) // errors are tolerated here in order to describe the serviceAccount with all // of the secrets that it references, even if those secrets cannot be fetched. if err == nil { // existingSecrets is the set of all secrets remaining on a // service account that are not present in the "tokens" slice. existingSecrets := sets.New[string]() for _, s := range secrets.Items { if s.Type == corev1.SecretTypeServiceAccountToken { name := s.Annotations[corev1.ServiceAccountNameKey] uid := s.Annotations[corev1.ServiceAccountUIDKey] if name == serviceAccount.Name && uid == string(serviceAccount.UID) { tokens = append(tokens, s) } } existingSecrets.Insert(s.Name) } for _, s := range serviceAccount.Secrets { if !existingSecrets.Has(s.Name) { missingSecrets.Insert(s.Name) } } for _, s := range serviceAccount.ImagePullSecrets { if !existingSecrets.Has(s.Name) { missingSecrets.Insert(s.Name) } } } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), serviceAccount, describerSettings.ChunkSize) } return describeServiceAccount(serviceAccount, tokens, missingSecrets, events) } func describeServiceAccount(serviceAccount *corev1.ServiceAccount, tokens []corev1.Secret, missingSecrets sets.Set[string], events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", serviceAccount.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", serviceAccount.Namespace) printLabelsMultiline(w, "Labels", serviceAccount.Labels) printAnnotationsMultiline(w, "Annotations", serviceAccount.Annotations) var ( emptyHeader = " " pullHeader = "Image pull secrets:" mountHeader = "Mountable secrets: " tokenHeader = "Tokens: " pullSecretNames = []string{} mountSecretNames = []string{} tokenSecretNames = []string{} ) for _, s := range serviceAccount.ImagePullSecrets { pullSecretNames = append(pullSecretNames, s.Name) } for _, s := range serviceAccount.Secrets { mountSecretNames = append(mountSecretNames, s.Name) } for _, s := range tokens { tokenSecretNames = append(tokenSecretNames, s.Name) } types := map[string][]string{ pullHeader: pullSecretNames, mountHeader: mountSecretNames, tokenHeader: tokenSecretNames, } for _, header := range sets.List(sets.KeySet(types)) { names := types[header] if len(names) == 0 { w.Write(LEVEL_0, "%s\t\n", header) } else { prefix := header for _, name := range names { if missingSecrets.Has(name) { w.Write(LEVEL_0, "%s\t%s (not found)\n", prefix, name) } else { w.Write(LEVEL_0, "%s\t%s\n", prefix, name) } prefix = emptyHeader } } } if events != nil { DescribeEvents(events, w) } return nil }) } // RoleDescriber generates information about a node. type RoleDescriber struct { clientset.Interface } func (d *RoleDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { role, err := d.RbacV1().Roles(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } breakdownRules := []rbacv1.PolicyRule{} for _, rule := range role.Rules { breakdownRules = append(breakdownRules, rbac.BreakdownRule(rule)...) } compactRules, err := rbac.CompactRules(breakdownRules) if err != nil { return "", err } sort.Stable(rbac.SortableRuleSlice(compactRules)) return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", role.Name) printLabelsMultiline(w, "Labels", role.Labels) printAnnotationsMultiline(w, "Annotations", role.Annotations) w.Write(LEVEL_0, "PolicyRule:\n") w.Write(LEVEL_1, "Resources\tNon-Resource URLs\tResource Names\tVerbs\n") w.Write(LEVEL_1, "---------\t-----------------\t--------------\t-----\n") for _, r := range compactRules { w.Write(LEVEL_1, "%s\t%v\t%v\t%v\n", CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs) } return nil }) } // ClusterRoleDescriber generates information about a node. type ClusterRoleDescriber struct { clientset.Interface } func (d *ClusterRoleDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { role, err := d.RbacV1().ClusterRoles().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } breakdownRules := []rbacv1.PolicyRule{} for _, rule := range role.Rules { breakdownRules = append(breakdownRules, rbac.BreakdownRule(rule)...) } compactRules, err := rbac.CompactRules(breakdownRules) if err != nil { return "", err } sort.Stable(rbac.SortableRuleSlice(compactRules)) return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", role.Name) printLabelsMultiline(w, "Labels", role.Labels) printAnnotationsMultiline(w, "Annotations", role.Annotations) w.Write(LEVEL_0, "PolicyRule:\n") w.Write(LEVEL_1, "Resources\tNon-Resource URLs\tResource Names\tVerbs\n") w.Write(LEVEL_1, "---------\t-----------------\t--------------\t-----\n") for _, r := range compactRules { w.Write(LEVEL_1, "%s\t%v\t%v\t%v\n", CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs) } return nil }) } func CombineResourceGroup(resource, group []string) string { if len(resource) == 0 { return "" } parts := strings.SplitN(resource[0], "/", 2) combine := parts[0] if len(group) > 0 && group[0] != "" { combine = combine + "." + group[0] } if len(parts) == 2 { combine = combine + "/" + parts[1] } return combine } // RoleBindingDescriber generates information about a node. type RoleBindingDescriber struct { clientset.Interface } func (d *RoleBindingDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { binding, err := d.RbacV1().RoleBindings(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", binding.Name) printLabelsMultiline(w, "Labels", binding.Labels) printAnnotationsMultiline(w, "Annotations", binding.Annotations) w.Write(LEVEL_0, "Role:\n") w.Write(LEVEL_1, "Kind:\t%s\n", binding.RoleRef.Kind) w.Write(LEVEL_1, "Name:\t%s\n", binding.RoleRef.Name) w.Write(LEVEL_0, "Subjects:\n") w.Write(LEVEL_1, "Kind\tName\tNamespace\n") w.Write(LEVEL_1, "----\t----\t---------\n") for _, s := range binding.Subjects { w.Write(LEVEL_1, "%s\t%s\t%s\n", s.Kind, s.Name, s.Namespace) } return nil }) } // ClusterRoleBindingDescriber generates information about a node. type ClusterRoleBindingDescriber struct { clientset.Interface } func (d *ClusterRoleBindingDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { binding, err := d.RbacV1().ClusterRoleBindings().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", binding.Name) printLabelsMultiline(w, "Labels", binding.Labels) printAnnotationsMultiline(w, "Annotations", binding.Annotations) w.Write(LEVEL_0, "Role:\n") w.Write(LEVEL_1, "Kind:\t%s\n", binding.RoleRef.Kind) w.Write(LEVEL_1, "Name:\t%s\n", binding.RoleRef.Name) w.Write(LEVEL_0, "Subjects:\n") w.Write(LEVEL_1, "Kind\tName\tNamespace\n") w.Write(LEVEL_1, "----\t----\t---------\n") for _, s := range binding.Subjects { w.Write(LEVEL_1, "%s\t%s\t%s\n", s.Kind, s.Name, s.Namespace) } return nil }) } // NodeDescriber generates information about a node. type NodeDescriber struct { clientset.Interface } func (d *NodeDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { mc := d.CoreV1().Nodes() node, err := mc.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } fieldSelector := fields.AndSelectors( fields.OneTermEqualSelector("spec.nodeName", name), fields.OneTermNotEqualSelector("status.phase", string(corev1.PodSucceeded)), fields.OneTermNotEqualSelector("status.phase", string(corev1.PodFailed)), ) // in a policy aware setting, users may have access to a node, but not all pods // in that case, we note that the user does not have access to the pods canViewPods := true initialOpts := metav1.ListOptions{ FieldSelector: fieldSelector.String(), Limit: describerSettings.ChunkSize, } nodeNonTerminatedPodsList, err := getPodsInChunks(d.CoreV1().Pods(namespace), initialOpts) if err != nil { if !apierrors.IsForbidden(err) { return "", err } canViewPods = false } var events *corev1.EventList if describerSettings.ShowEvents { if ref, err := reference.GetReference(scheme.Scheme, node); err != nil { klog.Errorf("Unable to construct reference to '%#v': %v", node, err) } else { // TODO: We haven't decided the namespace for Node object yet. // there are two UIDs for host events: // controller use node.uid // kubelet use node.name // TODO: Uniform use of UID events, _ = searchEvents(d.CoreV1(), ref, describerSettings.ChunkSize) ref.UID = types.UID(ref.Name) eventsInvName, _ := searchEvents(d.CoreV1(), ref, describerSettings.ChunkSize) // Merge the results of two queries events.Items = append(events.Items, eventsInvName.Items...) } } return describeNode(node, nodeNonTerminatedPodsList, events, canViewPods, &LeaseDescriber{d}) } type LeaseDescriber struct { client clientset.Interface } func describeNode(node *corev1.Node, nodeNonTerminatedPodsList *corev1.PodList, events *corev1.EventList, canViewPods bool, ld *LeaseDescriber) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", node.Name) if roles := findNodeRoles(node); len(roles) > 0 { w.Write(LEVEL_0, "Roles:\t%s\n", strings.Join(roles, ",")) } else { w.Write(LEVEL_0, "Roles:\t%s\n", "") } printLabelsMultiline(w, "Labels", node.Labels) printAnnotationsMultiline(w, "Annotations", node.Annotations) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", node.CreationTimestamp.Time.Format(time.RFC1123Z)) printNodeTaintsMultiline(w, "Taints", node.Spec.Taints) w.Write(LEVEL_0, "Unschedulable:\t%v\n", node.Spec.Unschedulable) if ld != nil { if lease, err := ld.client.CoordinationV1().Leases(corev1.NamespaceNodeLease).Get(context.TODO(), node.Name, metav1.GetOptions{}); err == nil { describeNodeLease(lease, w) } else { w.Write(LEVEL_0, "Lease:\tFailed to get lease: %s\n", err) } } if len(node.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n Type\tStatus\tLastHeartbeatTime\tLastTransitionTime\tReason\tMessage\n") w.Write(LEVEL_1, "----\t------\t-----------------\t------------------\t------\t-------\n") for _, c := range node.Status.Conditions { w.Write(LEVEL_1, "%v \t%v \t%s \t%s \t%v \t%v\n", c.Type, c.Status, c.LastHeartbeatTime.Time.Format(time.RFC1123Z), c.LastTransitionTime.Time.Format(time.RFC1123Z), c.Reason, c.Message) } } w.Write(LEVEL_0, "Addresses:\n") for _, address := range node.Status.Addresses { w.Write(LEVEL_1, "%s:\t%s\n", address.Type, address.Address) } printResourceList := func(resourceList corev1.ResourceList) { resources := make([]corev1.ResourceName, 0, len(resourceList)) for resource := range resourceList { resources = append(resources, resource) } sort.Sort(SortableResourceNames(resources)) for _, resource := range resources { value := resourceList[resource] w.Write(LEVEL_0, " %s:\t%s\n", resource, value.String()) } } if len(node.Status.Capacity) > 0 { w.Write(LEVEL_0, "Capacity:\n") printResourceList(node.Status.Capacity) } if len(node.Status.Allocatable) > 0 { w.Write(LEVEL_0, "Allocatable:\n") printResourceList(node.Status.Allocatable) } w.Write(LEVEL_0, "System Info:\n") w.Write(LEVEL_0, " Machine ID:\t%s\n", node.Status.NodeInfo.MachineID) w.Write(LEVEL_0, " System UUID:\t%s\n", node.Status.NodeInfo.SystemUUID) w.Write(LEVEL_0, " Boot ID:\t%s\n", node.Status.NodeInfo.BootID) w.Write(LEVEL_0, " Kernel Version:\t%s\n", node.Status.NodeInfo.KernelVersion) w.Write(LEVEL_0, " OS Image:\t%s\n", node.Status.NodeInfo.OSImage) w.Write(LEVEL_0, " Operating System:\t%s\n", node.Status.NodeInfo.OperatingSystem) w.Write(LEVEL_0, " Architecture:\t%s\n", node.Status.NodeInfo.Architecture) w.Write(LEVEL_0, " Container Runtime Version:\t%s\n", node.Status.NodeInfo.ContainerRuntimeVersion) w.Write(LEVEL_0, " Kubelet Version:\t%s\n", node.Status.NodeInfo.KubeletVersion) w.Write(LEVEL_0, " Kube-Proxy Version:\t%s\n", node.Status.NodeInfo.KubeProxyVersion) // remove when .PodCIDR is deprecated if len(node.Spec.PodCIDR) > 0 { w.Write(LEVEL_0, "PodCIDR:\t%s\n", node.Spec.PodCIDR) } if len(node.Spec.PodCIDRs) > 0 { w.Write(LEVEL_0, "PodCIDRs:\t%s\n", strings.Join(node.Spec.PodCIDRs, ",")) } if len(node.Spec.ProviderID) > 0 { w.Write(LEVEL_0, "ProviderID:\t%s\n", node.Spec.ProviderID) } if canViewPods && nodeNonTerminatedPodsList != nil { describeNodeResource(nodeNonTerminatedPodsList, node, w) } else { w.Write(LEVEL_0, "Pods:\tnot authorized\n") } if events != nil { DescribeEvents(events, w) } return nil }) } func describeNodeLease(lease *coordinationv1.Lease, w PrefixWriter) { w.Write(LEVEL_0, "Lease:\n") holderIdentity := "" if lease != nil && lease.Spec.HolderIdentity != nil { holderIdentity = *lease.Spec.HolderIdentity } w.Write(LEVEL_1, "HolderIdentity:\t%s\n", holderIdentity) acquireTime := "" if lease != nil && lease.Spec.AcquireTime != nil { acquireTime = lease.Spec.AcquireTime.Time.Format(time.RFC1123Z) } w.Write(LEVEL_1, "AcquireTime:\t%s\n", acquireTime) renewTime := "" if lease != nil && lease.Spec.RenewTime != nil { renewTime = lease.Spec.RenewTime.Time.Format(time.RFC1123Z) } w.Write(LEVEL_1, "RenewTime:\t%s\n", renewTime) } type StatefulSetDescriber struct { client clientset.Interface } func (p *StatefulSetDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { ps, err := p.client.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } pc := p.client.CoreV1().Pods(namespace) selector, err := metav1.LabelSelectorAsSelector(ps.Spec.Selector) if err != nil { return "", err } running, waiting, succeeded, failed, err := getPodStatusForController(pc, selector, ps.UID, describerSettings) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(p.client.CoreV1(), ps, describerSettings.ChunkSize) } return describeStatefulSet(ps, selector, events, running, waiting, succeeded, failed) } func describeStatefulSet(ps *appsv1.StatefulSet, selector labels.Selector, events *corev1.EventList, running, waiting, succeeded, failed int) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", ps.ObjectMeta.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", ps.ObjectMeta.Namespace) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", ps.CreationTimestamp.Time.Format(time.RFC1123Z)) w.Write(LEVEL_0, "Selector:\t%s\n", selector) printLabelsMultiline(w, "Labels", ps.Labels) printAnnotationsMultiline(w, "Annotations", ps.Annotations) w.Write(LEVEL_0, "Replicas:\t%d desired | %d total\n", *ps.Spec.Replicas, ps.Status.Replicas) w.Write(LEVEL_0, "Update Strategy:\t%s\n", ps.Spec.UpdateStrategy.Type) if ps.Spec.UpdateStrategy.RollingUpdate != nil { ru := ps.Spec.UpdateStrategy.RollingUpdate if ru.Partition != nil { w.Write(LEVEL_1, "Partition:\t%d\n", *ru.Partition) if ru.MaxUnavailable != nil { w.Write(LEVEL_1, "MaxUnavailable:\t%s\n", ru.MaxUnavailable.String()) } } } w.Write(LEVEL_0, "Pods Status:\t%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed) DescribePodTemplate(&ps.Spec.Template, w) describeVolumeClaimTemplates(ps.Spec.VolumeClaimTemplates, w) if events != nil { DescribeEvents(events, w) } return nil }) } type CertificateSigningRequestDescriber struct { client clientset.Interface } func (p *CertificateSigningRequestDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var ( crBytes []byte metadata metav1.ObjectMeta status string signerName string expirationSeconds *int32 username string events *corev1.EventList ) if csr, err := p.client.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{}); err == nil { crBytes = csr.Spec.Request metadata = csr.ObjectMeta conditionTypes := []string{} for _, c := range csr.Status.Conditions { conditionTypes = append(conditionTypes, string(c.Type)) } status = extractCSRStatus(conditionTypes, csr.Status.Certificate) signerName = csr.Spec.SignerName expirationSeconds = csr.Spec.ExpirationSeconds username = csr.Spec.Username if describerSettings.ShowEvents { events, _ = searchEvents(p.client.CoreV1(), csr, describerSettings.ChunkSize) } } else if csr, err := p.client.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{}); err == nil { crBytes = csr.Spec.Request metadata = csr.ObjectMeta conditionTypes := []string{} for _, c := range csr.Status.Conditions { conditionTypes = append(conditionTypes, string(c.Type)) } status = extractCSRStatus(conditionTypes, csr.Status.Certificate) if csr.Spec.SignerName != nil { signerName = *csr.Spec.SignerName } expirationSeconds = csr.Spec.ExpirationSeconds username = csr.Spec.Username if describerSettings.ShowEvents { events, _ = searchEvents(p.client.CoreV1(), csr, describerSettings.ChunkSize) } } else { return "", err } cr, err := certificate.ParseCSR(crBytes) if err != nil { return "", fmt.Errorf("Error parsing CSR: %v", err) } return describeCertificateSigningRequest(metadata, signerName, expirationSeconds, username, cr, status, events) } func describeCertificateSigningRequest(csr metav1.ObjectMeta, signerName string, expirationSeconds *int32, username string, cr *x509.CertificateRequest, status string, events *corev1.EventList) (string, error) { printListHelper := func(w PrefixWriter, prefix, name string, values []string) { if len(values) == 0 { return } w.Write(LEVEL_0, prefix+name+":\t") w.Write(LEVEL_0, strings.Join(values, "\n"+prefix+"\t")) w.Write(LEVEL_0, "\n") } return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", csr.Name) w.Write(LEVEL_0, "Labels:\t%s\n", labels.FormatLabels(csr.Labels)) w.Write(LEVEL_0, "Annotations:\t%s\n", labels.FormatLabels(csr.Annotations)) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", csr.CreationTimestamp.Time.Format(time.RFC1123Z)) w.Write(LEVEL_0, "Requesting User:\t%s\n", username) if len(signerName) > 0 { w.Write(LEVEL_0, "Signer:\t%s\n", signerName) } if expirationSeconds != nil { w.Write(LEVEL_0, "Requested Duration:\t%s\n", duration.HumanDuration(utilcsr.ExpirationSecondsToDuration(*expirationSeconds))) } w.Write(LEVEL_0, "Status:\t%s\n", status) w.Write(LEVEL_0, "Subject:\n") w.Write(LEVEL_0, "\tCommon Name:\t%s\n", cr.Subject.CommonName) w.Write(LEVEL_0, "\tSerial Number:\t%s\n", cr.Subject.SerialNumber) printListHelper(w, "\t", "Organization", cr.Subject.Organization) printListHelper(w, "\t", "Organizational Unit", cr.Subject.OrganizationalUnit) printListHelper(w, "\t", "Country", cr.Subject.Country) printListHelper(w, "\t", "Locality", cr.Subject.Locality) printListHelper(w, "\t", "Province", cr.Subject.Province) printListHelper(w, "\t", "StreetAddress", cr.Subject.StreetAddress) printListHelper(w, "\t", "PostalCode", cr.Subject.PostalCode) if len(cr.DNSNames)+len(cr.EmailAddresses)+len(cr.IPAddresses)+len(cr.URIs) > 0 { w.Write(LEVEL_0, "Subject Alternative Names:\n") printListHelper(w, "\t", "DNS Names", cr.DNSNames) printListHelper(w, "\t", "Email Addresses", cr.EmailAddresses) var uris []string for _, uri := range cr.URIs { uris = append(uris, uri.String()) } printListHelper(w, "\t", "URIs", uris) var ipaddrs []string for _, ipaddr := range cr.IPAddresses { ipaddrs = append(ipaddrs, ipaddr.String()) } printListHelper(w, "\t", "IP Addresses", ipaddrs) } if events != nil { DescribeEvents(events, w) } return nil }) } // HorizontalPodAutoscalerDescriber generates information about a horizontal pod autoscaler. type HorizontalPodAutoscalerDescriber struct { client clientset.Interface } func (d *HorizontalPodAutoscalerDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var events *corev1.EventList // autoscaling/v2 is introduced since v1.23 and autoscaling/v1 does not have full backward compatibility // with autoscaling/v2, so describer will try to get and describe hpa v2 object firstly, if it fails, // describer will fall back to do with hpa v1 object hpaV2, err := d.client.AutoscalingV2().HorizontalPodAutoscalers(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(d.client.CoreV1(), hpaV2, describerSettings.ChunkSize) } return describeHorizontalPodAutoscalerV2(hpaV2, events, d) } hpaV1, err := d.client.AutoscalingV1().HorizontalPodAutoscalers(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { if describerSettings.ShowEvents { events, _ = searchEvents(d.client.CoreV1(), hpaV1, describerSettings.ChunkSize) } return describeHorizontalPodAutoscalerV1(hpaV1, events, d) } return "", err } func describeHorizontalPodAutoscalerV2(hpa *autoscalingv2.HorizontalPodAutoscaler, events *corev1.EventList, d *HorizontalPodAutoscalerDescriber) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", hpa.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", hpa.Namespace) printLabelsMultiline(w, "Labels", hpa.Labels) printAnnotationsMultiline(w, "Annotations", hpa.Annotations) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", hpa.CreationTimestamp.Time.Format(time.RFC1123Z)) w.Write(LEVEL_0, "Reference:\t%s/%s\n", hpa.Spec.ScaleTargetRef.Kind, hpa.Spec.ScaleTargetRef.Name) w.Write(LEVEL_0, "Metrics:\t( current / target )\n") for i, metric := range hpa.Spec.Metrics { switch metric.Type { case autoscalingv2.ExternalMetricSourceType: if metric.External.Target.AverageValue != nil { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].External != nil && hpa.Status.CurrentMetrics[i].External.Current.AverageValue != nil { current = hpa.Status.CurrentMetrics[i].External.Current.AverageValue.String() } w.Write(LEVEL_1, "%q (target average value):\t%s / %s\n", metric.External.Metric.Name, current, metric.External.Target.AverageValue.String()) } else { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].External != nil { current = hpa.Status.CurrentMetrics[i].External.Current.Value.String() } w.Write(LEVEL_1, "%q (target value):\t%s / %s\n", metric.External.Metric.Name, current, metric.External.Target.Value.String()) } case autoscalingv2.PodsMetricSourceType: current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].Pods != nil { current = hpa.Status.CurrentMetrics[i].Pods.Current.AverageValue.String() } w.Write(LEVEL_1, "%q on pods:\t%s / %s\n", metric.Pods.Metric.Name, current, metric.Pods.Target.AverageValue.String()) case autoscalingv2.ObjectMetricSourceType: w.Write(LEVEL_1, "\"%s\" on %s/%s ", metric.Object.Metric.Name, metric.Object.DescribedObject.Kind, metric.Object.DescribedObject.Name) if metric.Object.Target.Type == autoscalingv2.AverageValueMetricType { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].Object != nil { current = hpa.Status.CurrentMetrics[i].Object.Current.AverageValue.String() } w.Write(LEVEL_0, "(target average value):\t%s / %s\n", current, metric.Object.Target.AverageValue.String()) } else { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].Object != nil { current = hpa.Status.CurrentMetrics[i].Object.Current.Value.String() } w.Write(LEVEL_0, "(target value):\t%s / %s\n", current, metric.Object.Target.Value.String()) } case autoscalingv2.ResourceMetricSourceType: w.Write(LEVEL_1, "resource %s on pods", string(metric.Resource.Name)) if metric.Resource.Target.AverageValue != nil { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].Resource != nil { current = hpa.Status.CurrentMetrics[i].Resource.Current.AverageValue.String() } w.Write(LEVEL_0, ":\t%s / %s\n", current, metric.Resource.Target.AverageValue.String()) } else { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].Resource != nil && hpa.Status.CurrentMetrics[i].Resource.Current.AverageUtilization != nil { current = fmt.Sprintf("%d%% (%s)", *hpa.Status.CurrentMetrics[i].Resource.Current.AverageUtilization, hpa.Status.CurrentMetrics[i].Resource.Current.AverageValue.String()) } target := "" if metric.Resource.Target.AverageUtilization != nil { target = fmt.Sprintf("%d%%", *metric.Resource.Target.AverageUtilization) } w.Write(LEVEL_1, "(as a percentage of request):\t%s / %s\n", current, target) } case autoscalingv2.ContainerResourceMetricSourceType: w.Write(LEVEL_1, "resource %s of container \"%s\" on pods", string(metric.ContainerResource.Name), metric.ContainerResource.Container) if metric.ContainerResource.Target.AverageValue != nil { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].ContainerResource != nil { current = hpa.Status.CurrentMetrics[i].ContainerResource.Current.AverageValue.String() } w.Write(LEVEL_0, ":\t%s / %s\n", current, metric.ContainerResource.Target.AverageValue.String()) } else { current := "" if len(hpa.Status.CurrentMetrics) > i && hpa.Status.CurrentMetrics[i].ContainerResource != nil && hpa.Status.CurrentMetrics[i].ContainerResource.Current.AverageUtilization != nil { current = fmt.Sprintf("%d%% (%s)", *hpa.Status.CurrentMetrics[i].ContainerResource.Current.AverageUtilization, hpa.Status.CurrentMetrics[i].ContainerResource.Current.AverageValue.String()) } target := "" if metric.ContainerResource.Target.AverageUtilization != nil { target = fmt.Sprintf("%d%%", *metric.ContainerResource.Target.AverageUtilization) } w.Write(LEVEL_1, "(as a percentage of request):\t%s / %s\n", current, target) } default: w.Write(LEVEL_1, "\n", string(metric.Type)) } } minReplicas := "" if hpa.Spec.MinReplicas != nil { minReplicas = fmt.Sprintf("%d", *hpa.Spec.MinReplicas) } w.Write(LEVEL_0, "Min replicas:\t%s\n", minReplicas) w.Write(LEVEL_0, "Max replicas:\t%d\n", hpa.Spec.MaxReplicas) // only print the hpa behavior if present if hpa.Spec.Behavior != nil { w.Write(LEVEL_0, "Behavior:\n") printDirectionBehavior(w, "Scale Up", hpa.Spec.Behavior.ScaleUp) printDirectionBehavior(w, "Scale Down", hpa.Spec.Behavior.ScaleDown) } w.Write(LEVEL_0, "%s pods:\t", hpa.Spec.ScaleTargetRef.Kind) w.Write(LEVEL_0, "%d current / %d desired\n", hpa.Status.CurrentReplicas, hpa.Status.DesiredReplicas) if len(hpa.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n") w.Write(LEVEL_1, "Type\tStatus\tReason\tMessage\n") w.Write(LEVEL_1, "----\t------\t------\t-------\n") for _, c := range hpa.Status.Conditions { w.Write(LEVEL_1, "%v\t%v\t%v\t%v\n", c.Type, c.Status, c.Reason, c.Message) } } if events != nil { DescribeEvents(events, w) } return nil }) } func printDirectionBehavior(w PrefixWriter, direction string, rules *autoscalingv2.HPAScalingRules) { if rules != nil { w.Write(LEVEL_1, "%s:\n", direction) if rules.StabilizationWindowSeconds != nil { w.Write(LEVEL_2, "Stabilization Window: %d seconds\n", *rules.StabilizationWindowSeconds) } if len(rules.Policies) > 0 { if rules.SelectPolicy != nil { w.Write(LEVEL_2, "Select Policy: %s\n", *rules.SelectPolicy) } else { w.Write(LEVEL_2, "Select Policy: %s\n", autoscalingv2.MaxChangePolicySelect) } w.Write(LEVEL_2, "Policies:\n") for _, p := range rules.Policies { w.Write(LEVEL_3, "- Type: %s\tValue: %d\tPeriod: %d seconds\n", p.Type, p.Value, p.PeriodSeconds) } } } } func describeHorizontalPodAutoscalerV1(hpa *autoscalingv1.HorizontalPodAutoscaler, events *corev1.EventList, d *HorizontalPodAutoscalerDescriber) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", hpa.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", hpa.Namespace) printLabelsMultiline(w, "Labels", hpa.Labels) printAnnotationsMultiline(w, "Annotations", hpa.Annotations) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", hpa.CreationTimestamp.Time.Format(time.RFC1123Z)) w.Write(LEVEL_0, "Reference:\t%s/%s\n", hpa.Spec.ScaleTargetRef.Kind, hpa.Spec.ScaleTargetRef.Name) if hpa.Spec.TargetCPUUtilizationPercentage != nil { w.Write(LEVEL_0, "Target CPU utilization:\t%d%%\n", *hpa.Spec.TargetCPUUtilizationPercentage) current := "" if hpa.Status.CurrentCPUUtilizationPercentage != nil { current = fmt.Sprintf("%d", *hpa.Status.CurrentCPUUtilizationPercentage) } w.Write(LEVEL_0, "Current CPU utilization:\t%s%%\n", current) } minReplicas := "" if hpa.Spec.MinReplicas != nil { minReplicas = fmt.Sprintf("%d", *hpa.Spec.MinReplicas) } w.Write(LEVEL_0, "Min replicas:\t%s\n", minReplicas) w.Write(LEVEL_0, "Max replicas:\t%d\n", hpa.Spec.MaxReplicas) w.Write(LEVEL_0, "%s pods:\t", hpa.Spec.ScaleTargetRef.Kind) w.Write(LEVEL_0, "%d current / %d desired\n", hpa.Status.CurrentReplicas, hpa.Status.DesiredReplicas) if events != nil { DescribeEvents(events, w) } return nil }) } func describeNodeResource(nodeNonTerminatedPodsList *corev1.PodList, node *corev1.Node, w PrefixWriter) { w.Write(LEVEL_0, "Non-terminated Pods:\t(%d in total)\n", len(nodeNonTerminatedPodsList.Items)) w.Write(LEVEL_1, "Namespace\tName\t\tCPU Requests\tCPU Limits\tMemory Requests\tMemory Limits\tAge\n") w.Write(LEVEL_1, "---------\t----\t\t------------\t----------\t---------------\t-------------\t---\n") allocatable := node.Status.Capacity if len(node.Status.Allocatable) > 0 { allocatable = node.Status.Allocatable } for _, pod := range nodeNonTerminatedPodsList.Items { req, limit := resourcehelper.PodRequestsAndLimits(&pod) cpuReq, cpuLimit, memoryReq, memoryLimit := req[corev1.ResourceCPU], limit[corev1.ResourceCPU], req[corev1.ResourceMemory], limit[corev1.ResourceMemory] fractionCpuReq := float64(cpuReq.MilliValue()) / float64(allocatable.Cpu().MilliValue()) * 100 fractionCpuLimit := float64(cpuLimit.MilliValue()) / float64(allocatable.Cpu().MilliValue()) * 100 fractionMemoryReq := float64(memoryReq.Value()) / float64(allocatable.Memory().Value()) * 100 fractionMemoryLimit := float64(memoryLimit.Value()) / float64(allocatable.Memory().Value()) * 100 w.Write(LEVEL_1, "%s\t%s\t\t%s (%d%%)\t%s (%d%%)\t%s (%d%%)\t%s (%d%%)\t%s\n", pod.Namespace, pod.Name, cpuReq.String(), int64(fractionCpuReq), cpuLimit.String(), int64(fractionCpuLimit), memoryReq.String(), int64(fractionMemoryReq), memoryLimit.String(), int64(fractionMemoryLimit), translateTimestampSince(pod.CreationTimestamp)) } w.Write(LEVEL_0, "Allocated resources:\n (Total limits may be over 100 percent, i.e., overcommitted.)\n") w.Write(LEVEL_1, "Resource\tRequests\tLimits\n") w.Write(LEVEL_1, "--------\t--------\t------\n") reqs, limits := getPodsTotalRequestsAndLimits(nodeNonTerminatedPodsList) cpuReqs, cpuLimits, memoryReqs, memoryLimits, ephemeralstorageReqs, ephemeralstorageLimits := reqs[corev1.ResourceCPU], limits[corev1.ResourceCPU], reqs[corev1.ResourceMemory], limits[corev1.ResourceMemory], reqs[corev1.ResourceEphemeralStorage], limits[corev1.ResourceEphemeralStorage] fractionCpuReqs := float64(0) fractionCpuLimits := float64(0) if allocatable.Cpu().MilliValue() != 0 { fractionCpuReqs = float64(cpuReqs.MilliValue()) / float64(allocatable.Cpu().MilliValue()) * 100 fractionCpuLimits = float64(cpuLimits.MilliValue()) / float64(allocatable.Cpu().MilliValue()) * 100 } fractionMemoryReqs := float64(0) fractionMemoryLimits := float64(0) if allocatable.Memory().Value() != 0 { fractionMemoryReqs = float64(memoryReqs.Value()) / float64(allocatable.Memory().Value()) * 100 fractionMemoryLimits = float64(memoryLimits.Value()) / float64(allocatable.Memory().Value()) * 100 } fractionEphemeralStorageReqs := float64(0) fractionEphemeralStorageLimits := float64(0) if allocatable.StorageEphemeral().Value() != 0 { fractionEphemeralStorageReqs = float64(ephemeralstorageReqs.Value()) / float64(allocatable.StorageEphemeral().Value()) * 100 fractionEphemeralStorageLimits = float64(ephemeralstorageLimits.Value()) / float64(allocatable.StorageEphemeral().Value()) * 100 } w.Write(LEVEL_1, "%s\t%s (%d%%)\t%s (%d%%)\n", corev1.ResourceCPU, cpuReqs.String(), int64(fractionCpuReqs), cpuLimits.String(), int64(fractionCpuLimits)) w.Write(LEVEL_1, "%s\t%s (%d%%)\t%s (%d%%)\n", corev1.ResourceMemory, memoryReqs.String(), int64(fractionMemoryReqs), memoryLimits.String(), int64(fractionMemoryLimits)) w.Write(LEVEL_1, "%s\t%s (%d%%)\t%s (%d%%)\n", corev1.ResourceEphemeralStorage, ephemeralstorageReqs.String(), int64(fractionEphemeralStorageReqs), ephemeralstorageLimits.String(), int64(fractionEphemeralStorageLimits)) extResources := make([]string, 0, len(allocatable)) hugePageResources := make([]string, 0, len(allocatable)) for resource := range allocatable { if resourcehelper.IsHugePageResourceName(resource) { hugePageResources = append(hugePageResources, string(resource)) } else if !resourcehelper.IsStandardContainerResourceName(string(resource)) && resource != corev1.ResourcePods { extResources = append(extResources, string(resource)) } } sort.Strings(extResources) sort.Strings(hugePageResources) for _, resource := range hugePageResources { hugePageSizeRequests, hugePageSizeLimits, hugePageSizeAllocable := reqs[corev1.ResourceName(resource)], limits[corev1.ResourceName(resource)], allocatable[corev1.ResourceName(resource)] fractionHugePageSizeRequests := float64(0) fractionHugePageSizeLimits := float64(0) if hugePageSizeAllocable.Value() != 0 { fractionHugePageSizeRequests = float64(hugePageSizeRequests.Value()) / float64(hugePageSizeAllocable.Value()) * 100 fractionHugePageSizeLimits = float64(hugePageSizeLimits.Value()) / float64(hugePageSizeAllocable.Value()) * 100 } w.Write(LEVEL_1, "%s\t%s (%d%%)\t%s (%d%%)\n", resource, hugePageSizeRequests.String(), int64(fractionHugePageSizeRequests), hugePageSizeLimits.String(), int64(fractionHugePageSizeLimits)) } for _, ext := range extResources { extRequests, extLimits := reqs[corev1.ResourceName(ext)], limits[corev1.ResourceName(ext)] w.Write(LEVEL_1, "%s\t%s\t%s\n", ext, extRequests.String(), extLimits.String()) } } func getPodsTotalRequestsAndLimits(podList *corev1.PodList) (reqs map[corev1.ResourceName]resource.Quantity, limits map[corev1.ResourceName]resource.Quantity) { reqs, limits = map[corev1.ResourceName]resource.Quantity{}, map[corev1.ResourceName]resource.Quantity{} for _, pod := range podList.Items { podReqs, podLimits := resourcehelper.PodRequestsAndLimits(&pod) for podReqName, podReqValue := range podReqs { if value, ok := reqs[podReqName]; !ok { reqs[podReqName] = podReqValue.DeepCopy() } else { value.Add(podReqValue) reqs[podReqName] = value } } for podLimitName, podLimitValue := range podLimits { if value, ok := limits[podLimitName]; !ok { limits[podLimitName] = podLimitValue.DeepCopy() } else { value.Add(podLimitValue) limits[podLimitName] = value } } } return } func DescribeEvents(el *corev1.EventList, w PrefixWriter) { if len(el.Items) == 0 { w.Write(LEVEL_0, "Events:\t\n") return } w.Flush() sort.Sort(event.SortableEvents(el.Items)) w.Write(LEVEL_0, "Events:\n Type\tReason\tAge\tFrom\tMessage\n") w.Write(LEVEL_1, "----\t------\t----\t----\t-------\n") for _, e := range el.Items { var interval string firstTimestampSince := translateMicroTimestampSince(e.EventTime) if e.EventTime.IsZero() { firstTimestampSince = translateTimestampSince(e.FirstTimestamp) } if e.Series != nil { interval = fmt.Sprintf("%s (x%d over %s)", translateMicroTimestampSince(e.Series.LastObservedTime), e.Series.Count, firstTimestampSince) } else if e.Count > 1 { interval = fmt.Sprintf("%s (x%d over %s)", translateTimestampSince(e.LastTimestamp), e.Count, firstTimestampSince) } else { interval = firstTimestampSince } source := e.Source.Component if source == "" { source = e.ReportingController } w.Write(LEVEL_1, "%v\t%v\t%s\t%v\t%v\n", e.Type, e.Reason, interval, source, strings.TrimSpace(e.Message), ) } } // DeploymentDescriber generates information about a deployment. type DeploymentDescriber struct { client clientset.Interface } func (dd *DeploymentDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { d, err := dd.client.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(dd.client.CoreV1(), d, describerSettings.ChunkSize) } var oldRSs, newRSs []*appsv1.ReplicaSet if _, oldResult, newResult, err := deploymentutil.GetAllReplicaSetsInChunks(d, dd.client.AppsV1(), describerSettings.ChunkSize); err == nil { oldRSs = oldResult if newResult != nil { newRSs = append(newRSs, newResult) } } return describeDeployment(d, oldRSs, newRSs, events) } func describeDeployment(d *appsv1.Deployment, oldRSs []*appsv1.ReplicaSet, newRSs []*appsv1.ReplicaSet, events *corev1.EventList) (string, error) { selector, err := metav1.LabelSelectorAsSelector(d.Spec.Selector) if err != nil { return "", err } return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", d.ObjectMeta.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", d.ObjectMeta.Namespace) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", d.CreationTimestamp.Time.Format(time.RFC1123Z)) printLabelsMultiline(w, "Labels", d.Labels) printAnnotationsMultiline(w, "Annotations", d.Annotations) w.Write(LEVEL_0, "Selector:\t%s\n", selector) w.Write(LEVEL_0, "Replicas:\t%d desired | %d updated | %d total | %d available | %d unavailable\n", *(d.Spec.Replicas), d.Status.UpdatedReplicas, d.Status.Replicas, d.Status.AvailableReplicas, d.Status.UnavailableReplicas) w.Write(LEVEL_0, "StrategyType:\t%s\n", d.Spec.Strategy.Type) w.Write(LEVEL_0, "MinReadySeconds:\t%d\n", d.Spec.MinReadySeconds) if d.Spec.Strategy.RollingUpdate != nil { ru := d.Spec.Strategy.RollingUpdate w.Write(LEVEL_0, "RollingUpdateStrategy:\t%s max unavailable, %s max surge\n", ru.MaxUnavailable.String(), ru.MaxSurge.String()) } DescribePodTemplate(&d.Spec.Template, w) if len(d.Status.Conditions) > 0 { w.Write(LEVEL_0, "Conditions:\n Type\tStatus\tReason\n") w.Write(LEVEL_1, "----\t------\t------\n") for _, c := range d.Status.Conditions { w.Write(LEVEL_1, "%v \t%v\t%v\n", c.Type, c.Status, c.Reason) } } if len(oldRSs) > 0 || len(newRSs) > 0 { w.Write(LEVEL_0, "OldReplicaSets:\t%s\n", printReplicaSetsByLabels(oldRSs)) w.Write(LEVEL_0, "NewReplicaSet:\t%s\n", printReplicaSetsByLabels(newRSs)) } if events != nil { DescribeEvents(events, w) } return nil }) } func printReplicaSetsByLabels(matchingRSs []*appsv1.ReplicaSet) string { // Format the matching ReplicaSets into strings. rsStrings := make([]string, 0, len(matchingRSs)) for _, rs := range matchingRSs { rsStrings = append(rsStrings, fmt.Sprintf("%s (%d/%d replicas created)", rs.Name, rs.Status.Replicas, *rs.Spec.Replicas)) } list := strings.Join(rsStrings, ", ") if list == "" { return "" } return list } func getPodStatusForController(c corev1client.PodInterface, selector labels.Selector, uid types.UID, settings DescriberSettings) ( running, waiting, succeeded, failed int, err error) { initialOpts := metav1.ListOptions{LabelSelector: selector.String(), Limit: settings.ChunkSize} rcPods, err := getPodsInChunks(c, initialOpts) if err != nil { return } for _, pod := range rcPods.Items { controllerRef := metav1.GetControllerOf(&pod) // Skip pods that are orphans or owned by other controllers. if controllerRef == nil || controllerRef.UID != uid { continue } switch pod.Status.Phase { case corev1.PodRunning: running++ case corev1.PodPending: waiting++ case corev1.PodSucceeded: succeeded++ case corev1.PodFailed: failed++ } } return } func getPodsInChunks(c corev1client.PodInterface, initialOpts metav1.ListOptions) (*corev1.PodList, error) { podList := &corev1.PodList{} err := runtimeresource.FollowContinue(&initialOpts, func(options metav1.ListOptions) (runtime.Object, error) { newList, err := c.List(context.TODO(), options) if err != nil { return nil, runtimeresource.EnhanceListError(err, options, corev1.ResourcePods.String()) } podList.Items = append(podList.Items, newList.Items...) return newList, nil }) return podList, err } // ConfigMapDescriber generates information about a ConfigMap type ConfigMapDescriber struct { clientset.Interface } func (d *ConfigMapDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.CoreV1().ConfigMaps(namespace) configMap, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", configMap.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", configMap.Namespace) printLabelsMultiline(w, "Labels", configMap.Labels) printAnnotationsMultiline(w, "Annotations", configMap.Annotations) w.Write(LEVEL_0, "\nData\n====\n") for k, v := range configMap.Data { w.Write(LEVEL_0, "%s:\n----\n", k) w.Write(LEVEL_0, "%s\n", string(v)) w.Write(LEVEL_0, "\n") } w.Write(LEVEL_0, "\nBinaryData\n====\n") for k, v := range configMap.BinaryData { w.Write(LEVEL_0, "%s: %s bytes\n", k, strconv.Itoa(len(v))) } w.Write(LEVEL_0, "\n") if describerSettings.ShowEvents { events, err := searchEvents(d.CoreV1(), configMap, describerSettings.ChunkSize) if err != nil { return err } if events != nil { DescribeEvents(events, w) } } return nil }) } // NetworkPolicyDescriber generates information about a networkingv1.NetworkPolicy type NetworkPolicyDescriber struct { clientset.Interface } func (d *NetworkPolicyDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { c := d.NetworkingV1().NetworkPolicies(namespace) networkPolicy, err := c.Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } return describeNetworkPolicy(networkPolicy) } func describeNetworkPolicy(networkPolicy *networkingv1.NetworkPolicy) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", networkPolicy.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", networkPolicy.Namespace) w.Write(LEVEL_0, "Created on:\t%s\n", networkPolicy.CreationTimestamp) printLabelsMultiline(w, "Labels", networkPolicy.Labels) printAnnotationsMultiline(w, "Annotations", networkPolicy.Annotations) describeNetworkPolicySpec(networkPolicy.Spec, w) return nil }) } func describeNetworkPolicySpec(nps networkingv1.NetworkPolicySpec, w PrefixWriter) { w.Write(LEVEL_0, "Spec:\n") w.Write(LEVEL_1, "PodSelector: ") if len(nps.PodSelector.MatchLabels) == 0 && len(nps.PodSelector.MatchExpressions) == 0 { w.Write(LEVEL_2, " (Allowing the specific traffic to all pods in this namespace)\n") } else { w.Write(LEVEL_2, "%s\n", metav1.FormatLabelSelector(&nps.PodSelector)) } ingressEnabled, egressEnabled := getPolicyType(nps) if ingressEnabled { w.Write(LEVEL_1, "Allowing ingress traffic:\n") printNetworkPolicySpecIngressFrom(nps.Ingress, " ", w) } else { w.Write(LEVEL_1, "Not affecting ingress traffic\n") } if egressEnabled { w.Write(LEVEL_1, "Allowing egress traffic:\n") printNetworkPolicySpecEgressTo(nps.Egress, " ", w) } else { w.Write(LEVEL_1, "Not affecting egress traffic\n") } w.Write(LEVEL_1, "Policy Types: %v\n", policyTypesToString(nps.PolicyTypes)) } func getPolicyType(nps networkingv1.NetworkPolicySpec) (bool, bool) { var ingress, egress bool for _, pt := range nps.PolicyTypes { switch pt { case networkingv1.PolicyTypeIngress: ingress = true case networkingv1.PolicyTypeEgress: egress = true } } return ingress, egress } func printNetworkPolicySpecIngressFrom(npirs []networkingv1.NetworkPolicyIngressRule, initialIndent string, w PrefixWriter) { if len(npirs) == 0 { w.Write(LEVEL_0, "%s%s\n", initialIndent, " (Selected pods are isolated for ingress connectivity)") return } for i, npir := range npirs { if len(npir.Ports) == 0 { w.Write(LEVEL_0, "%s%s\n", initialIndent, "To Port: (traffic allowed to all ports)") } else { for _, port := range npir.Ports { var proto corev1.Protocol if port.Protocol != nil { proto = *port.Protocol } else { proto = corev1.ProtocolTCP } if port.EndPort == nil { w.Write(LEVEL_0, "%s%s: %s/%s\n", initialIndent, "To Port", port.Port, proto) } else { w.Write(LEVEL_0, "%s%s: %s-%d/%s\n", initialIndent, "To Port Range", port.Port, *port.EndPort, proto) } } } if len(npir.From) == 0 { w.Write(LEVEL_0, "%s%s\n", initialIndent, "From: (traffic not restricted by source)") } else { for _, from := range npir.From { w.Write(LEVEL_0, "%s%s\n", initialIndent, "From:") if from.PodSelector != nil && from.NamespaceSelector != nil { w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "NamespaceSelector", metav1.FormatLabelSelector(from.NamespaceSelector)) w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "PodSelector", metav1.FormatLabelSelector(from.PodSelector)) } else if from.PodSelector != nil { w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "PodSelector", metav1.FormatLabelSelector(from.PodSelector)) } else if from.NamespaceSelector != nil { w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "NamespaceSelector", metav1.FormatLabelSelector(from.NamespaceSelector)) } else if from.IPBlock != nil { w.Write(LEVEL_1, "%sIPBlock:\n", initialIndent) w.Write(LEVEL_2, "%sCIDR: %s\n", initialIndent, from.IPBlock.CIDR) w.Write(LEVEL_2, "%sExcept: %v\n", initialIndent, strings.Join(from.IPBlock.Except, ", ")) } } } if i != len(npirs)-1 { w.Write(LEVEL_0, "%s%s\n", initialIndent, "----------") } } } func printNetworkPolicySpecEgressTo(npers []networkingv1.NetworkPolicyEgressRule, initialIndent string, w PrefixWriter) { if len(npers) == 0 { w.Write(LEVEL_0, "%s%s\n", initialIndent, " (Selected pods are isolated for egress connectivity)") return } for i, nper := range npers { if len(nper.Ports) == 0 { w.Write(LEVEL_0, "%s%s\n", initialIndent, "To Port: (traffic allowed to all ports)") } else { for _, port := range nper.Ports { var proto corev1.Protocol if port.Protocol != nil { proto = *port.Protocol } else { proto = corev1.ProtocolTCP } if port.EndPort == nil { w.Write(LEVEL_0, "%s%s: %s/%s\n", initialIndent, "To Port", port.Port, proto) } else { w.Write(LEVEL_0, "%s%s: %s-%d/%s\n", initialIndent, "To Port Range", port.Port, *port.EndPort, proto) } } } if len(nper.To) == 0 { w.Write(LEVEL_0, "%s%s\n", initialIndent, "To: (traffic not restricted by destination)") } else { for _, to := range nper.To { w.Write(LEVEL_0, "%s%s\n", initialIndent, "To:") if to.PodSelector != nil && to.NamespaceSelector != nil { w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "NamespaceSelector", metav1.FormatLabelSelector(to.NamespaceSelector)) w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "PodSelector", metav1.FormatLabelSelector(to.PodSelector)) } else if to.PodSelector != nil { w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "PodSelector", metav1.FormatLabelSelector(to.PodSelector)) } else if to.NamespaceSelector != nil { w.Write(LEVEL_1, "%s%s: %s\n", initialIndent, "NamespaceSelector", metav1.FormatLabelSelector(to.NamespaceSelector)) } else if to.IPBlock != nil { w.Write(LEVEL_1, "%sIPBlock:\n", initialIndent) w.Write(LEVEL_2, "%sCIDR: %s\n", initialIndent, to.IPBlock.CIDR) w.Write(LEVEL_2, "%sExcept: %v\n", initialIndent, strings.Join(to.IPBlock.Except, ", ")) } } } if i != len(npers)-1 { w.Write(LEVEL_0, "%s%s\n", initialIndent, "----------") } } } type StorageClassDescriber struct { clientset.Interface } func (s *StorageClassDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { sc, err := s.StorageV1().StorageClasses().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(s.CoreV1(), sc, describerSettings.ChunkSize) } return describeStorageClass(sc, events) } func describeStorageClass(sc *storagev1.StorageClass, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", sc.Name) w.Write(LEVEL_0, "IsDefaultClass:\t%s\n", storageutil.IsDefaultAnnotationText(sc.ObjectMeta)) w.Write(LEVEL_0, "Annotations:\t%s\n", labels.FormatLabels(sc.Annotations)) w.Write(LEVEL_0, "Provisioner:\t%s\n", sc.Provisioner) w.Write(LEVEL_0, "Parameters:\t%s\n", labels.FormatLabels(sc.Parameters)) w.Write(LEVEL_0, "AllowVolumeExpansion:\t%s\n", printBoolPtr(sc.AllowVolumeExpansion)) if len(sc.MountOptions) == 0 { w.Write(LEVEL_0, "MountOptions:\t\n") } else { w.Write(LEVEL_0, "MountOptions:\n") for _, option := range sc.MountOptions { w.Write(LEVEL_1, "%s\n", option) } } if sc.ReclaimPolicy != nil { w.Write(LEVEL_0, "ReclaimPolicy:\t%s\n", *sc.ReclaimPolicy) } if sc.VolumeBindingMode != nil { w.Write(LEVEL_0, "VolumeBindingMode:\t%s\n", *sc.VolumeBindingMode) } if sc.AllowedTopologies != nil { printAllowedTopologies(w, sc.AllowedTopologies) } if events != nil { DescribeEvents(events, w) } return nil }) } type VolumeAttributesClassDescriber struct { clientset.Interface } func (d *VolumeAttributesClassDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { vac, err := d.StorageV1beta1().VolumeAttributesClasses().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(d.CoreV1(), vac, describerSettings.ChunkSize) } return describeVolumeAttributesClass(vac, events) } func describeVolumeAttributesClass(vac *storagev1beta1.VolumeAttributesClass, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", vac.Name) w.Write(LEVEL_0, "Annotations:\t%s\n", labels.FormatLabels(vac.Annotations)) w.Write(LEVEL_0, "DriverName:\t%s\n", vac.DriverName) w.Write(LEVEL_0, "Parameters:\t%s\n", labels.FormatLabels(vac.Parameters)) if events != nil { DescribeEvents(events, w) } return nil }) } type CSINodeDescriber struct { clientset.Interface } func (c *CSINodeDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { csi, err := c.StorageV1().CSINodes().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(c.CoreV1(), csi, describerSettings.ChunkSize) } return describeCSINode(csi, events) } func describeCSINode(csi *storagev1.CSINode, events *corev1.EventList) (output string, err error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", csi.GetName()) printLabelsMultiline(w, "Labels", csi.GetLabels()) printAnnotationsMultiline(w, "Annotations", csi.GetAnnotations()) w.Write(LEVEL_0, "CreationTimestamp:\t%s\n", csi.CreationTimestamp.Time.Format(time.RFC1123Z)) w.Write(LEVEL_0, "Spec:\n") if csi.Spec.Drivers != nil { w.Write(LEVEL_1, "Drivers:\n") for _, driver := range csi.Spec.Drivers { w.Write(LEVEL_2, "%s:\n", driver.Name) w.Write(LEVEL_3, "Node ID:\t%s\n", driver.NodeID) if driver.Allocatable != nil && driver.Allocatable.Count != nil { w.Write(LEVEL_3, "Allocatables:\n") w.Write(LEVEL_4, "Count:\t%d\n", *driver.Allocatable.Count) } if driver.TopologyKeys != nil { w.Write(LEVEL_3, "Topology Keys:\t%s\n", driver.TopologyKeys) } } } if events != nil { DescribeEvents(events, w) } return nil }) } func printAllowedTopologies(w PrefixWriter, topologies []corev1.TopologySelectorTerm) { w.Write(LEVEL_0, "AllowedTopologies:\t") if len(topologies) == 0 { w.WriteLine("") return } w.WriteLine("") for i, term := range topologies { printTopologySelectorTermsMultilineWithIndent(w, LEVEL_1, fmt.Sprintf("Term %d", i), "\t", term.MatchLabelExpressions) } } func printTopologySelectorTermsMultilineWithIndent(w PrefixWriter, indentLevel int, title, innerIndent string, reqs []corev1.TopologySelectorLabelRequirement) { w.Write(indentLevel, "%s:%s", title, innerIndent) if len(reqs) == 0 { w.WriteLine("") return } for i, req := range reqs { if i != 0 { w.Write(indentLevel, "%s", innerIndent) } exprStr := fmt.Sprintf("%s %s", req.Key, "in") if len(req.Values) > 0 { exprStr = fmt.Sprintf("%s [%s]", exprStr, strings.Join(req.Values, ", ")) } w.Write(LEVEL_0, "%s\n", exprStr) } } type PodDisruptionBudgetDescriber struct { clientset.Interface } func (p *PodDisruptionBudgetDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { var ( pdbv1 *policyv1.PodDisruptionBudget pdbv1beta1 *policyv1beta1.PodDisruptionBudget err error ) pdbv1, err = p.PolicyV1().PodDisruptionBudgets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(p.CoreV1(), pdbv1, describerSettings.ChunkSize) } return describePodDisruptionBudgetV1(pdbv1, events) } // try falling back to v1beta1 in NotFound error cases if apierrors.IsNotFound(err) { pdbv1beta1, err = p.PolicyV1beta1().PodDisruptionBudgets(namespace).Get(context.TODO(), name, metav1.GetOptions{}) } if err == nil { var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(p.CoreV1(), pdbv1beta1, describerSettings.ChunkSize) } return describePodDisruptionBudgetV1beta1(pdbv1beta1, events) } return "", err } func describePodDisruptionBudgetV1(pdb *policyv1.PodDisruptionBudget, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", pdb.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", pdb.Namespace) if pdb.Spec.MinAvailable != nil { w.Write(LEVEL_0, "Min available:\t%s\n", pdb.Spec.MinAvailable.String()) } else if pdb.Spec.MaxUnavailable != nil { w.Write(LEVEL_0, "Max unavailable:\t%s\n", pdb.Spec.MaxUnavailable.String()) } if pdb.Spec.Selector != nil { w.Write(LEVEL_0, "Selector:\t%s\n", metav1.FormatLabelSelector(pdb.Spec.Selector)) } else { w.Write(LEVEL_0, "Selector:\t\n") } w.Write(LEVEL_0, "Status:\n") w.Write(LEVEL_2, "Allowed disruptions:\t%d\n", pdb.Status.DisruptionsAllowed) w.Write(LEVEL_2, "Current:\t%d\n", pdb.Status.CurrentHealthy) w.Write(LEVEL_2, "Desired:\t%d\n", pdb.Status.DesiredHealthy) w.Write(LEVEL_2, "Total:\t%d\n", pdb.Status.ExpectedPods) if events != nil { DescribeEvents(events, w) } return nil }) } func describePodDisruptionBudgetV1beta1(pdb *policyv1beta1.PodDisruptionBudget, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", pdb.Name) w.Write(LEVEL_0, "Namespace:\t%s\n", pdb.Namespace) if pdb.Spec.MinAvailable != nil { w.Write(LEVEL_0, "Min available:\t%s\n", pdb.Spec.MinAvailable.String()) } else if pdb.Spec.MaxUnavailable != nil { w.Write(LEVEL_0, "Max unavailable:\t%s\n", pdb.Spec.MaxUnavailable.String()) } if pdb.Spec.Selector != nil { w.Write(LEVEL_0, "Selector:\t%s\n", metav1.FormatLabelSelector(pdb.Spec.Selector)) } else { w.Write(LEVEL_0, "Selector:\t\n") } w.Write(LEVEL_0, "Status:\n") w.Write(LEVEL_2, "Allowed disruptions:\t%d\n", pdb.Status.DisruptionsAllowed) w.Write(LEVEL_2, "Current:\t%d\n", pdb.Status.CurrentHealthy) w.Write(LEVEL_2, "Desired:\t%d\n", pdb.Status.DesiredHealthy) w.Write(LEVEL_2, "Total:\t%d\n", pdb.Status.ExpectedPods) if events != nil { DescribeEvents(events, w) } return nil }) } // PriorityClassDescriber generates information about a PriorityClass. type PriorityClassDescriber struct { clientset.Interface } func (s *PriorityClassDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) { pc, err := s.SchedulingV1().PriorityClasses().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return "", err } var events *corev1.EventList if describerSettings.ShowEvents { events, _ = searchEvents(s.CoreV1(), pc, describerSettings.ChunkSize) } return describePriorityClass(pc, events) } func describePriorityClass(pc *schedulingv1.PriorityClass, events *corev1.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", pc.Name) w.Write(LEVEL_0, "Value:\t%v\n", pc.Value) w.Write(LEVEL_0, "GlobalDefault:\t%v\n", pc.GlobalDefault) w.Write(LEVEL_0, "PreemptionPolicy:\t%s\n", *pc.PreemptionPolicy) w.Write(LEVEL_0, "Description:\t%s\n", pc.Description) w.Write(LEVEL_0, "Annotations:\t%s\n", labels.FormatLabels(pc.Annotations)) if events != nil { DescribeEvents(events, w) } return nil }) } func stringOrNone(s string) string { return stringOrDefaultValue(s, "") } func stringOrDefaultValue(s, defaultValue string) string { if len(s) > 0 { return s } return defaultValue } func policyTypesToString(pts []networkingv1.PolicyType) string { formattedString := "" if pts != nil { strPts := []string{} for _, p := range pts { strPts = append(strPts, string(p)) } formattedString = strings.Join(strPts, ", ") } return stringOrNone(formattedString) } // newErrNoDescriber creates a new ErrNoDescriber with the names of the provided types. func newErrNoDescriber(types ...reflect.Type) error { names := make([]string, 0, len(types)) for _, t := range types { names = append(names, t.String()) } return ErrNoDescriber{Types: names} } // Describers implements ObjectDescriber against functions registered via Add. Those functions can // be strongly typed. Types are exactly matched (no conversion or assignable checks). type Describers struct { searchFns map[reflect.Type][]typeFunc } // DescribeObject implements ObjectDescriber and will attempt to print the provided object to a string, // if at least one describer function has been registered with the exact types passed, or if any // describer can print the exact object in its first argument (the remainder will be provided empty // values). If no function registered with Add can satisfy the passed objects, an ErrNoDescriber will // be returned // TODO: reorder and partial match extra. func (d *Describers) DescribeObject(exact interface{}, extra ...interface{}) (string, error) { exactType := reflect.TypeOf(exact) fns, ok := d.searchFns[exactType] if !ok { return "", newErrNoDescriber(exactType) } if len(extra) == 0 { for _, typeFn := range fns { if len(typeFn.Extra) == 0 { return typeFn.Describe(exact, extra...) } } typeFn := fns[0] for _, t := range typeFn.Extra { v := reflect.New(t).Elem() extra = append(extra, v.Interface()) } return fns[0].Describe(exact, extra...) } types := make([]reflect.Type, 0, len(extra)) for _, obj := range extra { types = append(types, reflect.TypeOf(obj)) } for _, typeFn := range fns { if typeFn.Matches(types) { return typeFn.Describe(exact, extra...) } } return "", newErrNoDescriber(append([]reflect.Type{exactType}, types...)...) } // Add adds one or more describer functions to the Describer. The passed function must // match the signature: // // func(...) (string, error) // // Any number of arguments may be provided. func (d *Describers) Add(fns ...interface{}) error { for _, fn := range fns { fv := reflect.ValueOf(fn) ft := fv.Type() if ft.Kind() != reflect.Func { return fmt.Errorf("expected func, got: %v", ft) } numIn := ft.NumIn() if numIn == 0 { return fmt.Errorf("expected at least one 'in' params, got: %v", ft) } if ft.NumOut() != 2 { return fmt.Errorf("expected two 'out' params - (string, error), got: %v", ft) } types := make([]reflect.Type, 0, numIn) for i := 0; i < numIn; i++ { types = append(types, ft.In(i)) } if ft.Out(0) != reflect.TypeOf(string("")) { return fmt.Errorf("expected string return, got: %v", ft) } var forErrorType error // This convolution is necessary, otherwise TypeOf picks up on the fact // that forErrorType is nil. errorType := reflect.TypeOf(&forErrorType).Elem() if ft.Out(1) != errorType { return fmt.Errorf("expected error return, got: %v", ft) } exact := types[0] extra := types[1:] if d.searchFns == nil { d.searchFns = make(map[reflect.Type][]typeFunc) } fns := d.searchFns[exact] fn := typeFunc{Extra: extra, Fn: fv} fns = append(fns, fn) d.searchFns[exact] = fns } return nil } // typeFunc holds information about a describer function and the types it accepts type typeFunc struct { Extra []reflect.Type Fn reflect.Value } // Matches returns true when the passed types exactly match the Extra list. func (fn typeFunc) Matches(types []reflect.Type) bool { if len(fn.Extra) != len(types) { return false } // reorder the items in array types and fn.Extra // convert the type into string and sort them, check if they are matched varMap := make(map[reflect.Type]bool) for i := range fn.Extra { varMap[fn.Extra[i]] = true } for i := range types { if _, found := varMap[types[i]]; !found { return false } } return true } // Describe invokes the nested function with the exact number of arguments. func (fn typeFunc) Describe(exact interface{}, extra ...interface{}) (string, error) { values := []reflect.Value{reflect.ValueOf(exact)} for i, obj := range extra { if obj != nil { values = append(values, reflect.ValueOf(obj)) } else { values = append(values, reflect.New(fn.Extra[i]).Elem()) } } out := fn.Fn.Call(values) s := out[0].Interface().(string) var err error if !out[1].IsNil() { err = out[1].Interface().(error) } return s, err } // printLabelsMultiline prints multiple labels with a proper alignment. func printLabelsMultiline(w PrefixWriter, title string, labels map[string]string) { printLabelsMultilineWithIndent(w, "", title, "\t", labels, sets.New[string]()) } // printLabelsMultiline prints multiple labels with a user-defined alignment. func printLabelsMultilineWithIndent(w PrefixWriter, initialIndent, title, innerIndent string, labels map[string]string, skip sets.Set[string]) { w.Write(LEVEL_0, "%s%s:%s", initialIndent, title, innerIndent) if len(labels) == 0 { w.WriteLine("") return } // to print labels in the sorted order keys := make([]string, 0, len(labels)) for key := range labels { if skip.Has(key) { continue } keys = append(keys, key) } if len(keys) == 0 { w.WriteLine("") return } sort.Strings(keys) for i, key := range keys { if i != 0 { w.Write(LEVEL_0, "%s", initialIndent) w.Write(LEVEL_0, "%s", innerIndent) } w.Write(LEVEL_0, "%s=%s\n", key, labels[key]) } } // printTaintsMultiline prints multiple taints with a proper alignment. func printNodeTaintsMultiline(w PrefixWriter, title string, taints []corev1.Taint) { printTaintsMultilineWithIndent(w, "", title, "\t", taints) } // printTaintsMultilineWithIndent prints multiple taints with a user-defined alignment. func printTaintsMultilineWithIndent(w PrefixWriter, initialIndent, title, innerIndent string, taints []corev1.Taint) { w.Write(LEVEL_0, "%s%s:%s", initialIndent, title, innerIndent) if len(taints) == 0 { w.WriteLine("") return } // to print taints in the sorted order sort.Slice(taints, func(i, j int) bool { cmpKey := func(taint corev1.Taint) string { return string(taint.Effect) + "," + taint.Key } return cmpKey(taints[i]) < cmpKey(taints[j]) }) for i, taint := range taints { if i != 0 { w.Write(LEVEL_0, "%s", initialIndent) w.Write(LEVEL_0, "%s", innerIndent) } w.Write(LEVEL_0, "%s\n", taint.ToString()) } } // printPodsMultiline prints multiple pods with a proper alignment. func printPodsMultiline(w PrefixWriter, title string, pods []corev1.Pod) { printPodsMultilineWithIndent(w, "", title, "\t", pods) } // printPodsMultilineWithIndent prints multiple pods with a user-defined alignment. func printPodsMultilineWithIndent(w PrefixWriter, initialIndent, title, innerIndent string, pods []corev1.Pod) { w.Write(LEVEL_0, "%s%s:%s", initialIndent, title, innerIndent) if len(pods) == 0 { w.WriteLine("") return } // to print pods in the sorted order sort.Slice(pods, func(i, j int) bool { cmpKey := func(pod corev1.Pod) string { return pod.Name } return cmpKey(pods[i]) < cmpKey(pods[j]) }) for i, pod := range pods { if i != 0 { w.Write(LEVEL_0, "%s", initialIndent) w.Write(LEVEL_0, "%s", innerIndent) } w.Write(LEVEL_0, "%s\n", pod.Name) } } // printPodTolerationsMultiline prints multiple tolerations with a proper alignment. func printPodTolerationsMultiline(w PrefixWriter, title string, tolerations []corev1.Toleration) { printTolerationsMultilineWithIndent(w, "", title, "\t", tolerations) } // printTolerationsMultilineWithIndent prints multiple tolerations with a user-defined alignment. func printTolerationsMultilineWithIndent(w PrefixWriter, initialIndent, title, innerIndent string, tolerations []corev1.Toleration) { w.Write(LEVEL_0, "%s%s:%s", initialIndent, title, innerIndent) if len(tolerations) == 0 { w.WriteLine("") return } // to print tolerations in the sorted order sort.Slice(tolerations, func(i, j int) bool { return tolerations[i].Key < tolerations[j].Key }) for i, toleration := range tolerations { if i != 0 { w.Write(LEVEL_0, "%s", initialIndent) w.Write(LEVEL_0, "%s", innerIndent) } w.Write(LEVEL_0, "%s", toleration.Key) if len(toleration.Value) != 0 { w.Write(LEVEL_0, "=%s", toleration.Value) } if len(toleration.Effect) != 0 { w.Write(LEVEL_0, ":%s", toleration.Effect) } // tolerations: // - operator: "Exists" // is a special case which tolerates everything if toleration.Operator == corev1.TolerationOpExists && len(toleration.Value) == 0 { if len(toleration.Key) != 0 || len(toleration.Effect) != 0 { w.Write(LEVEL_0, " op=Exists") } else { w.Write(LEVEL_0, "op=Exists") } } if toleration.TolerationSeconds != nil { w.Write(LEVEL_0, " for %ds", *toleration.TolerationSeconds) } w.Write(LEVEL_0, "\n") } } type flusher interface { Flush() } func tabbedString(f func(io.Writer) error) (string, error) { out := new(tabwriter.Writer) buf := &bytes.Buffer{} out.Init(buf, 0, 8, 2, ' ', 0) err := f(out) if err != nil { return "", err } out.Flush() return buf.String(), nil } type SortableResourceNames []corev1.ResourceName func (list SortableResourceNames) Len() int { return len(list) } func (list SortableResourceNames) Swap(i, j int) { list[i], list[j] = list[j], list[i] } func (list SortableResourceNames) Less(i, j int) bool { return list[i] < list[j] } // SortedResourceNames returns the sorted resource names of a resource list. func SortedResourceNames(list corev1.ResourceList) []corev1.ResourceName { resources := make([]corev1.ResourceName, 0, len(list)) for res := range list { resources = append(resources, res) } sort.Sort(SortableResourceNames(resources)) return resources } type SortableResourceQuotas []corev1.ResourceQuota func (list SortableResourceQuotas) Len() int { return len(list) } func (list SortableResourceQuotas) Swap(i, j int) { list[i], list[j] = list[j], list[i] } func (list SortableResourceQuotas) Less(i, j int) bool { return list[i].Name < list[j].Name } type SortableVolumeMounts []corev1.VolumeMount func (list SortableVolumeMounts) Len() int { return len(list) } func (list SortableVolumeMounts) Swap(i, j int) { list[i], list[j] = list[j], list[i] } func (list SortableVolumeMounts) Less(i, j int) bool { return list[i].MountPath < list[j].MountPath } type SortableVolumeDevices []corev1.VolumeDevice func (list SortableVolumeDevices) Len() int { return len(list) } func (list SortableVolumeDevices) Swap(i, j int) { list[i], list[j] = list[j], list[i] } func (list SortableVolumeDevices) Less(i, j int) bool { return list[i].DevicePath < list[j].DevicePath } var maxAnnotationLen = 140 // printAnnotationsMultiline prints multiple annotations with a proper alignment. // If annotation string is too long, we omit chars more than 200 length. func printAnnotationsMultiline(w PrefixWriter, title string, annotations map[string]string) { w.Write(LEVEL_0, "%s:\t", title) // to print labels in the sorted order keys := make([]string, 0, len(annotations)) for key := range annotations { if skipAnnotations.Has(key) { continue } keys = append(keys, key) } if len(keys) == 0 { w.WriteLine("") return } sort.Strings(keys) indent := "\t" for i, key := range keys { if i != 0 { w.Write(LEVEL_0, indent) } value := strings.TrimSuffix(annotations[key], "\n") if (len(value)+len(key)+2) > maxAnnotationLen || strings.Contains(value, "\n") { w.Write(LEVEL_0, "%s:\n", key) for _, s := range strings.Split(value, "\n") { w.Write(LEVEL_0, "%s %s\n", indent, shorten(s, maxAnnotationLen-2)) } } else { w.Write(LEVEL_0, "%s: %s\n", key, value) } } } func shorten(s string, maxLength int) string { if len(s) > maxLength { return s[:maxLength] + "..." } return s } // translateMicroTimestampSince returns the elapsed time since timestamp in // human-readable approximation. func translateMicroTimestampSince(timestamp metav1.MicroTime) string { if timestamp.IsZero() { return "" } return duration.HumanDuration(time.Since(timestamp.Time)) } // translateTimestampSince returns the elapsed time since timestamp in // human-readable approximation. func translateTimestampSince(timestamp metav1.Time) string { if timestamp.IsZero() { return "" } return duration.HumanDuration(time.Since(timestamp.Time)) } // Pass ports=nil for all ports. func formatEndpointSlices(endpointSlices []discoveryv1.EndpointSlice, ports sets.Set[string]) string { if len(endpointSlices) == 0 { return "" } var list []string max := 3 more := false count := 0 for i := range endpointSlices { if len(endpointSlices[i].Ports) == 0 { // It's possible to have headless services with no ports. for j := range endpointSlices[i].Endpoints { if len(list) == max { more = true } isReady := endpointSlices[i].Endpoints[j].Conditions.Ready == nil || *endpointSlices[i].Endpoints[j].Conditions.Ready if !isReady { // ready indicates that this endpoint is prepared to receive traffic, // according to whatever system is managing the endpoint. A nil value // indicates an unknown state. In most cases consumers should interpret this // unknown state as ready. // More info: vendor/k8s.io/api/discovery/v1/types.go continue } if !more { list = append(list, endpointSlices[i].Endpoints[j].Addresses[0]) } count++ } } else { // "Normal" services with ports defined. for j := range endpointSlices[i].Ports { port := endpointSlices[i].Ports[j] if ports == nil || ports.Has(*port.Name) { for k := range endpointSlices[i].Endpoints { if len(list) == max { more = true } addr := endpointSlices[i].Endpoints[k].Addresses[0] isReady := endpointSlices[i].Endpoints[k].Conditions.Ready == nil || *endpointSlices[i].Endpoints[k].Conditions.Ready if !isReady { // ready indicates that this endpoint is prepared to receive traffic, // according to whatever system is managing the endpoint. A nil value // indicates an unknown state. In most cases consumers should interpret this // unknown state as ready. // More info: vendor/k8s.io/api/discovery/v1/types.go continue } if !more { hostPort := net.JoinHostPort(addr, strconv.Itoa(int(*port.Port))) list = append(list, hostPort) } count++ } } } } } ret := strings.Join(list, ",") if more { return fmt.Sprintf("%s + %d more...", ret, count-max) } return ret } func extractCSRStatus(conditions []string, certificateBytes []byte) string { var approved, denied, failed bool for _, c := range conditions { switch c { case string(certificatesv1beta1.CertificateApproved): approved = true case string(certificatesv1beta1.CertificateDenied): denied = true case string(certificatesv1beta1.CertificateFailed): failed = true } } var status string // must be in order of precedence if denied { status += "Denied" } else if approved { status += "Approved" } else { status += "Pending" } if failed { status += ",Failed" } if len(certificateBytes) > 0 { status += ",Issued" } return status } // backendStringer behaves just like a string interface and converts the given backend to a string. func serviceBackendStringer(backend *networkingv1.IngressServiceBackend) string { if backend == nil { return "" } var bPort string if backend.Port.Number != 0 { sNum := int64(backend.Port.Number) bPort = strconv.FormatInt(sNum, 10) } else { bPort = backend.Port.Name } return fmt.Sprintf("%v:%v", backend.Name, bPort) } // backendStringer behaves just like a string interface and converts the given backend to a string. func backendStringer(backend *networkingv1beta1.IngressBackend) string { if backend == nil { return "" } return fmt.Sprintf("%v:%v", backend.ServiceName, backend.ServicePort.String()) } // findNodeRoles returns the roles of a given node. // The roles are determined by looking for: // * a node-role.kubernetes.io/="" label // * a kubernetes.io/role="" label func findNodeRoles(node *corev1.Node) []string { roles := sets.New[string]() for k, v := range node.Labels { switch { case strings.HasPrefix(k, LabelNodeRolePrefix): if role := strings.TrimPrefix(k, LabelNodeRolePrefix); len(role) > 0 { roles.Insert(role) } case k == NodeLabelRole && v != "": roles.Insert(v) } } return sets.List(roles) } // ingressLoadBalancerStatusStringerV1 behaves mostly like a string interface and converts the given status to a string. // `wide` indicates whether the returned value is meant for --o=wide output. If not, it's clipped to 16 bytes. func ingressLoadBalancerStatusStringerV1(s networkingv1.IngressLoadBalancerStatus, wide bool) string { ingress := s.Ingress result := sets.New[string]() for i := range ingress { if ingress[i].IP != "" { result.Insert(ingress[i].IP) } else if ingress[i].Hostname != "" { result.Insert(ingress[i].Hostname) } } r := strings.Join(sets.List(result), ",") if !wide && len(r) > LoadBalancerWidth { r = r[0:(LoadBalancerWidth-3)] + "..." } return r } // ingressLoadBalancerStatusStringerV1beta1 behaves mostly like a string interface and converts the given status to a string. // `wide` indicates whether the returned value is meant for --o=wide output. If not, it's clipped to 16 bytes. func ingressLoadBalancerStatusStringerV1beta1(s networkingv1beta1.IngressLoadBalancerStatus, wide bool) string { ingress := s.Ingress result := sets.New[string]() for i := range ingress { if ingress[i].IP != "" { result.Insert(ingress[i].IP) } else if ingress[i].Hostname != "" { result.Insert(ingress[i].Hostname) } } r := strings.Join(sets.List(result), ",") if !wide && len(r) > LoadBalancerWidth { r = r[0:(LoadBalancerWidth-3)] + "..." } return r } // searchEvents finds events about the specified object. // It is very similar to CoreV1.Events.Search, but supports the Limit parameter. func searchEvents(client corev1client.EventsGetter, objOrRef runtime.Object, limit int64) (*corev1.EventList, error) { ref, err := reference.GetReference(scheme.Scheme, objOrRef) if err != nil { return nil, err } stringRefKind := string(ref.Kind) var refKind *string if len(stringRefKind) > 0 { refKind = &stringRefKind } stringRefUID := string(ref.UID) var refUID *string if len(stringRefUID) > 0 { refUID = &stringRefUID } e := client.Events(ref.Namespace) fieldSelector := e.GetFieldSelector(&ref.Name, &ref.Namespace, refKind, refUID) initialOpts := metav1.ListOptions{FieldSelector: fieldSelector.String(), Limit: limit} eventList := &corev1.EventList{} err = runtimeresource.FollowContinue(&initialOpts, func(options metav1.ListOptions) (runtime.Object, error) { newEvents, err := e.List(context.TODO(), options) if err != nil { return nil, runtimeresource.EnhanceListError(err, options, "events") } eventList.Items = append(eventList.Items, newEvents.Items...) return newEvents, nil }) return eventList, err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/describe/describe_test.go000066400000000000000000006271161504711711200310070ustar00rootroot00000000000000/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package describe import ( "bytes" "fmt" "reflect" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/lithammer/dedent" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" coordinationv1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" policyv1 "k8s.io/api/policy/v1" policyv1beta1 "k8s.io/api/policy/v1beta1" schedulingv1 "k8s.io/api/scheduling/v1" storagev1 "k8s.io/api/storage/v1" storagev1beta1 "k8s.io/api/storage/v1beta1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/resource" 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/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/utils/ptr" ) type describeClient struct { T *testing.T Namespace string Err error kubernetes.Interface } func TestDescribePod(t *testing.T) { deletionTimestamp := metav1.Time{Time: time.Now().UTC().AddDate(-10, 0, 0)} gracePeriod := int64(1234) condition1 := corev1.PodConditionType("condition1") condition2 := corev1.PodConditionType("condition2") runningPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", DeletionTimestamp: &deletionTimestamp, DeletionGracePeriodSeconds: &gracePeriod, }, Spec: corev1.PodSpec{ ReadinessGates: []corev1.PodReadinessGate{ { ConditionType: condition1, }, { ConditionType: condition2, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{ { Type: condition1, Status: corev1.ConditionTrue, }, }, }, } tests := []struct { name string namespace string phase corev1.PodPhase wantOutput []string }{ { name: "foo", namespace: "bar", phase: "Running", wantOutput: []string{"bar", "Status:", "Terminating (lasts 10y)", "Termination Grace Period", "1234s"}, }, { name: "pod1", namespace: "ns1", phase: "Pending", wantOutput: []string{"pod1", "ns1", "Terminating (lasts 10y)", "Termination Grace Period", "1234s"}, }, { name: "pod2", namespace: "ns2", phase: "Succeeded", wantOutput: []string{"pod2", "ns2", "Succeeded"}, }, { name: "pod3", namespace: "ns3", phase: "Failed", wantOutput: []string{"pod3", "ns3", "Failed"}, }, } for i, test := range tests { pod := runningPod.DeepCopy() pod.Name, pod.Namespace, pod.Status.Phase = test.name, test.namespace, test.phase fake := fake.NewSimpleClientset(pod) c := &describeClient{T: t, Namespace: pod.Namespace, Interface: fake} d := PodDescriber{c} out, err := d.Describe(pod.Namespace, pod.Name, DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("case %d: unexpected error: %v", i, err) } for _, wantStr := range test.wantOutput { if !strings.Contains(out, wantStr) { t.Errorf("case %d didn't contain want(%s): unexpected out:\n%s", i, wantStr, out) } } } } func TestDescribePodServiceAccount(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ ServiceAccountName: "fooaccount", }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "Service Account:") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "fooaccount") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodEphemeralContainers(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Image: "busybox", }, }, }, }, Status: corev1.PodStatus{ EphemeralContainerStatuses: []corev1.ContainerStatus{ { Name: "debugger", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.NewTime(time.Now()), }, }, Ready: false, RestartCount: 0, }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "debugger:") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "busybox") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodNode(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ NodeName: "all-in-one", }, Status: corev1.PodStatus{ HostIP: "127.0.0.1", NominatedNodeName: "nodeA", }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "all-in-one/127.0.0.1") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "nodeA") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodTolerations(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ Tolerations: []corev1.Toleration{ {Operator: corev1.TolerationOpExists}, {Effect: corev1.TaintEffectNoSchedule, Operator: corev1.TolerationOpExists}, {Key: "key0", Operator: corev1.TolerationOpExists}, {Key: "key1", Value: "value1"}, {Key: "key2", Operator: corev1.TolerationOpEqual, Value: "value2", Effect: corev1.TaintEffectNoSchedule}, {Key: "key3", Value: "value3", Effect: corev1.TaintEffectNoExecute, TolerationSeconds: &[]int64{300}[0]}, {Key: "key4", Effect: corev1.TaintEffectNoExecute, TolerationSeconds: &[]int64{60}[0]}, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, " op=Exists\n") || !strings.Contains(out, ":NoSchedule op=Exists\n") || !strings.Contains(out, "key0 op=Exists\n") || !strings.Contains(out, "key1=value1\n") || !strings.Contains(out, "key2=value2:NoSchedule\n") || !strings.Contains(out, "key3=value3:NoExecute for 300s\n") || !strings.Contains(out, "key4:NoExecute for 60s\n") || !strings.Contains(out, "Tolerations:") { t.Errorf("unexpected out:\n%s", out) } } func TestDescribePodVolumes(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "image", VolumeSource: corev1.VolumeSource{Image: &corev1.ImageVolumeSource{Reference: "image", PullPolicy: corev1.PullIfNotPresent}}, }, }, }, } expected := dedent.Dedent(` Name: bar Namespace: foo Node: Labels: Annotations: Status: IP: IPs: Containers: Volumes: image: Type: Image (a container image or OCI artifact) Reference: image PullPolicy: IfNotPresent QoS Class: BestEffort Node-Selectors: Tolerations: Events: `)[1:] fakeClient := fake.NewSimpleClientset(pod) c := &describeClient{T: t, Namespace: "foo", Interface: fakeClient} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } assert.Equal(t, expected, out) } func TestDescribeTopologySpreadConstraints(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ { MaxSkew: 3, TopologyKey: "topology.kubernetes.io/test1", WhenUnsatisfiable: "DoNotSchedule", LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"key1": "val1", "key2": "val2"}}, }, { MaxSkew: 1, TopologyKey: "topology.kubernetes.io/test2", WhenUnsatisfiable: "ScheduleAnyway", }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "topology.kubernetes.io/test1:DoNotSchedule when max skew 3 is exceeded for selector key1=val1,key2=val2\n") || !strings.Contains(out, "topology.kubernetes.io/test2:ScheduleAnyway when max skew 1 is exceeded\n") { t.Errorf("unexpected out:\n%s", out) } } func TestDescribeSecret(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Data: map[string][]byte{ "username": []byte("YWRtaW4="), "password": []byte("MWYyZDFlMmU2N2Rm"), }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := SecretDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "bar") || !strings.Contains(out, "foo") || !strings.Contains(out, "username") || !strings.Contains(out, "8 bytes") || !strings.Contains(out, "password") || !strings.Contains(out, "16 bytes") { t.Errorf("unexpected out: %s", out) } if strings.Contains(out, "YWRtaW4=") || strings.Contains(out, "MWYyZDFlMmU2N2Rm") { t.Errorf("sensitive data should not be shown, unexpected out: %s", out) } } func TestDescribeNamespace(t *testing.T) { exampleNamespaceName := "example" testCases := []struct { name string namespace *corev1.Namespace expect []string }{ { name: "no quotas or limit ranges", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: exampleNamespaceName, }, Status: corev1.NamespaceStatus{ Phase: corev1.NamespaceActive, }, }, expect: []string{ "Name", exampleNamespaceName, "Status", string(corev1.NamespaceActive), "No resource quota", "No LimitRange resource.", }, }, { name: "has conditions", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: exampleNamespaceName, }, Status: corev1.NamespaceStatus{ Phase: corev1.NamespaceTerminating, Conditions: []corev1.NamespaceCondition{ { LastTransitionTime: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Message: "example message", Reason: "example reason", Status: corev1.ConditionTrue, Type: corev1.NamespaceDeletionContentFailure, }, }, }, }, expect: []string{ "Name", exampleNamespaceName, "Status", string(corev1.NamespaceTerminating), "Conditions", "Type", string(corev1.NamespaceDeletionContentFailure), "Status", string(corev1.ConditionTrue), "Reason", "example reason", "Message", "example message", "No resource quota", "No LimitRange resource.", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.namespace) c := &describeClient{T: t, Namespace: "", Interface: fake} d := NamespaceDescriber{c} out, err := d.Describe("", testCase.namespace.Name, DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } }) } } func TestDescribePodPriority(t *testing.T) { priority := int32(1000) fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{ PriorityClassName: "high-priority", Priority: &priority, }, }) c := &describeClient{T: t, Namespace: "", Interface: fake} d := PodDescriber{c} out, err := d.Describe("", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "high-priority") || !strings.Contains(out, "1000") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodRuntimeClass(t *testing.T) { runtimeClassNames := []string{"test1", ""} testCases := []struct { name string pod *corev1.Pod expect []string unexpect []string }{ { name: "test1", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{ RuntimeClassName: &runtimeClassNames[0], }, }, expect: []string{ "Name", "bar", "Runtime Class Name", "test1", }, unexpect: []string{}, }, { name: "test2", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{ RuntimeClassName: &runtimeClassNames[1], }, }, expect: []string{ "Name", "bar", }, unexpect: []string{ "Runtime Class Name", }, }, { name: "test3", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{}, }, expect: []string{ "Name", "bar", }, unexpect: []string{ "Runtime Class Name", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.pod) c := &describeClient{T: t, Interface: fake} d := PodDescriber{c} out, err := d.Describe("", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } for _, unexpected := range testCase.unexpect { if strings.Contains(out, unexpected) { t.Errorf("unexpected to find %q in output: %q", unexpected, out) } } }) } } func TestDescribePriorityClass(t *testing.T) { preemptLowerPriority := corev1.PreemptLowerPriority preemptNever := corev1.PreemptNever testCases := []struct { name string priorityClass *schedulingv1.PriorityClass expect []string }{ { name: "test1", priorityClass: &schedulingv1.PriorityClass{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Value: 10, GlobalDefault: false, PreemptionPolicy: &preemptLowerPriority, Description: "test1", }, expect: []string{ "Name", "bar", "Value", "10", "GlobalDefault", "false", "PreemptionPolicy", "PreemptLowerPriority", "Description", "test1", "Annotations", "", }, }, { name: "test2", priorityClass: &schedulingv1.PriorityClass{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Value: 100, GlobalDefault: true, PreemptionPolicy: &preemptNever, Description: "test2", }, expect: []string{ "Name", "bar", "Value", "100", "GlobalDefault", "true", "PreemptionPolicy", "Never", "Description", "test2", "Annotations", "", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.priorityClass) c := &describeClient{T: t, Interface: fake} d := PriorityClassDescriber{c} out, err := d.Describe("", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } }) } } func TestDescribeConfigMap(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "mycm", Namespace: "foo", }, Data: map[string]string{ "key1": "value1", "key2": "value2", }, BinaryData: map[string][]byte{ "binarykey1": {0xFF, 0xFE, 0xFD, 0xFC, 0xFB}, "binarykey2": {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA}, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ConfigMapDescriber{c} out, err := d.Describe("foo", "mycm", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "foo") || !strings.Contains(out, "mycm") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "key1") || !strings.Contains(out, "value1") || !strings.Contains(out, "key2") || !strings.Contains(out, "value2") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "binarykey1") || !strings.Contains(out, "5 bytes") || !strings.Contains(out, "binarykey2") || !strings.Contains(out, "6 bytes") { t.Errorf("unexpected out: %s", out) } } func TestDescribeLimitRange(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.LimitRange{ ObjectMeta: metav1.ObjectMeta{ Name: "mylr", Namespace: "foo", }, Spec: corev1.LimitRangeSpec{ Limits: []corev1.LimitRangeItem{ { Type: corev1.LimitTypePod, Max: getResourceList("100m", "10000Mi"), Min: getResourceList("5m", "100Mi"), MaxLimitRequestRatio: getResourceList("10", ""), }, { Type: corev1.LimitTypeContainer, Max: getResourceList("100m", "10000Mi"), Min: getResourceList("5m", "100Mi"), Default: getResourceList("50m", "500Mi"), DefaultRequest: getResourceList("10m", "200Mi"), MaxLimitRequestRatio: getResourceList("10", ""), }, { Type: corev1.LimitTypePersistentVolumeClaim, Max: getStorageResourceList("10Gi"), Min: getStorageResourceList("5Gi"), }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := LimitRangeDescriber{c} out, err := d.Describe("foo", "mylr", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } checks := []string{"foo", "mylr", "Pod", "cpu", "5m", "100m", "memory", "100Mi", "10000Mi", "10", "Container", "cpu", "10m", "50m", "200Mi", "500Mi", "PersistentVolumeClaim", "storage", "5Gi", "10Gi"} for _, check := range checks { if !strings.Contains(out, check) { t.Errorf("unexpected out: %s", out) } } } func getStorageResourceList(storage string) corev1.ResourceList { res := corev1.ResourceList{} if storage != "" { res[corev1.ResourceStorage] = resource.MustParse(storage) } return res } func getResourceList(cpu, memory string) corev1.ResourceList { res := corev1.ResourceList{} if cpu != "" { res[corev1.ResourceCPU] = resource.MustParse(cpu) } if memory != "" { res[corev1.ResourceMemory] = resource.MustParse(memory) } return res } func TestDescribeService(t *testing.T) { singleStack := corev1.IPFamilyPolicySingleStack testCases := []struct { name string service *corev1.Service endpointSlices []*discoveryv1.EndpointSlice expected string }{ { name: "test1", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(9527), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, InternalTrafficPolicy: ptr.To(corev1.ServiceInternalTrafficPolicyCluster), HealthCheckNodePort: 32222, }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { IP: "5.6.7.8", IPMode: ptr.To(corev1.LoadBalancerIPModeVIP), }, }, }, }, }, endpointSlices: []*discoveryv1.EndpointSlice{{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-abcde", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.2"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.3"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolTCP), }}, }}, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 LoadBalancer Ingress: 5.6.7.8 (VIP) Port: port-tcp 8080/TCP TargetPort: 9527/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527,10.244.0.2:9527,10.244.0.3:9527 Session Affinity: None External Traffic Policy: Local Internal Traffic Policy: Cluster HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test2", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("targetPort"), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, InternalTrafficPolicy: ptr.To(corev1.ServiceInternalTrafficPolicyLocal), HealthCheckNodePort: 32222, }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { IP: "5.6.7.8", }, }, }, }, }, endpointSlices: []*discoveryv1.EndpointSlice{ { ObjectMeta: metav1.ObjectMeta{ Name: "bar-12345", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.2"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolUDP), }}, }, { ObjectMeta: metav1.ObjectMeta{ Name: "bar-54321", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.3"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(false)}}, {Addresses: []string{"10.244.0.4"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.5"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolUDP), }}, }, }, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 LoadBalancer Ingress: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: targetPort/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527,10.244.0.2:9527,10.244.0.4:9527 + 1 more... Session Affinity: None External Traffic Policy: Local Internal Traffic Policy: Local HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test-ready-field-empty", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(9527), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, InternalTrafficPolicy: ptr.To(corev1.ServiceInternalTrafficPolicyCluster), HealthCheckNodePort: 32222, }, }, endpointSlices: []*discoveryv1.EndpointSlice{{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-abcdef", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolTCP), }}, }}, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: 9527/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527 Session Affinity: None External Traffic Policy: Local Internal Traffic Policy: Cluster HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test-ServiceIPFamily", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("targetPort"), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, HealthCheckNodePort: 32222, }, }, endpointSlices: []*discoveryv1.EndpointSlice{{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-123ab", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolTCP), }}, }}, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: targetPort/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527 Session Affinity: None External Traffic Policy: Local HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test-ServiceIPFamilyPolicy+ClusterIPs", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("targetPort"), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, IPFamilyPolicy: &singleStack, ClusterIPs: []string{"1.2.3.4"}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, HealthCheckNodePort: 32222, }, }, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Family Policy: SingleStack IP Families: IPv4 IP: 1.2.3.4 IPs: 1.2.3.4 Desired LoadBalancer IP: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: targetPort/TCP NodePort: port-tcp 31111/TCP Endpoints: Session Affinity: None External Traffic Policy: Local HealthCheck NodePort: 32222 Events: `)[1:], }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { objects := []runtime.Object{tc.service} for i := range tc.endpointSlices { objects = append(objects, tc.endpointSlices[i]) } fakeClient := fake.NewSimpleClientset(objects...) c := &describeClient{T: t, Namespace: "foo", Interface: fakeClient} d := ServiceDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } assert.Equal(t, tc.expected, out) }) } } func TestPodDescribeResultsSorted(t *testing.T) { // Arrange fake := fake.NewSimpleClientset( &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{Name: "one"}, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 1", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{Name: "two"}, Source: corev1.EventSource{Component: "scheduler"}, Message: "Item 2", FirstTimestamp: metav1.NewTime(time.Date(1987, time.June, 17, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(1987, time.June, 17, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{Name: "three"}, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 3", FirstTimestamp: metav1.NewTime(time.Date(2002, time.December, 25, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2002, time.December, 25, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, }, }, &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}, ) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} // Act out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) // Assert if err != nil { t.Errorf("unexpected error: %v", err) } VerifyDatesInOrder(out, "\n" /* rowDelimiter */, "\t" /* columnDelimiter */, t) } // VerifyDatesInOrder checks the start of each line for a RFC1123Z date // and posts error if all subsequent dates are not equal or increasing func VerifyDatesInOrder( resultToTest, rowDelimiter, columnDelimiter string, t *testing.T) { lines := strings.Split(resultToTest, rowDelimiter) var previousTime time.Time for _, str := range lines { columns := strings.Split(str, columnDelimiter) if len(columns) > 0 { currentTime, err := time.Parse(time.RFC1123Z, columns[0]) if err == nil { if previousTime.After(currentTime) { t.Errorf( "Output is not sorted by time. %s should be listed after %s. Complete output: %s", previousTime.Format(time.RFC1123Z), currentTime.Format(time.RFC1123Z), resultToTest) } previousTime = currentTime } } } } func TestDescribeResources(t *testing.T) { testCases := []struct { resources *corev1.ResourceRequirements expectedElements map[string]int }{ { resources: &corev1.ResourceRequirements{}, expectedElements: map[string]int{}, }, { resources: &corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, }, expectedElements: map[string]int{"cpu": 2, "memory": 2, "Requests": 1, "Limits": 1, "1k": 2, "100Mi": 2}, }, { resources: &corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, }, expectedElements: map[string]int{"cpu": 1, "memory": 1, "Limits": 1, "1k": 1, "100Mi": 1}, }, { resources: &corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, }, expectedElements: map[string]int{"cpu": 1, "memory": 1, "Requests": 1, "1k": 1, "100Mi": 1}, }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { out := new(bytes.Buffer) writer := NewPrefixWriter(out) describeResources(testCase.resources, writer, LEVEL_1) output := out.String() gotElements := make(map[string]int) for key, val := range testCase.expectedElements { count := strings.Count(output, key) if count == 0 { t.Errorf("expected to find %q in output: %q", val, output) continue } gotElements[key] = count } if !reflect.DeepEqual(gotElements, testCase.expectedElements) { t.Errorf("Expected %v, got %v in output string: %q", testCase.expectedElements, gotElements, output) } }) } } func TestDescribeContainers(t *testing.T) { trueVal := true testCases := []struct { container corev1.Container status corev1.ContainerStatus expectedElements []string }{ // Running state. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.NewTime(time.Now()), }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Running", "Ready", "True", "Restart Count", "7", "Image", "image", "Started"}, }, // Waiting state. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Waiting: &corev1.ContainerStateWaiting{ Reason: "potato", }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "Reason", "potato"}, }, // Terminated state. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ StartedAt: metav1.NewTime(time.Now()), FinishedAt: metav1.NewTime(time.Now()), Reason: "potato", ExitCode: 2, }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Terminated", "Ready", "True", "Restart Count", "7", "Image", "image", "Reason", "potato", "Started", "Finished", "Exit Code", "2"}, }, // Last Terminated { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.NewTime(time.Now()), }, }, LastTerminationState: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ StartedAt: metav1.NewTime(time.Now().Add(time.Second * 3)), FinishedAt: metav1.NewTime(time.Now()), Reason: "crashing", ExitCode: 3, }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Terminated", "Ready", "True", "Restart Count", "7", "Image", "image", "Started", "Finished", "Exit Code", "2", "crashing", "3"}, }, // No state defaults to waiting. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image"}, }, // Env { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap\tOptional: false"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{Prefix: "p_", ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap with prefix 'p_'\tOptional: false"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: &corev1.ConfigMapEnvSource{Optional: &trueVal, LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap\tOptional: true"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}, Optional: &trueVal}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret\tOptional: true"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{Prefix: "p_", SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret with prefix 'p_'\tOptional: false"}, }, // Command { container: corev1.Container{Name: "test", Image: "image", Command: []string{"sleep", "1000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "sleep", "1000"}, }, // Command with newline { container: corev1.Container{Name: "test", Image: "image", Command: []string{"sleep", "1000\n2000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"1000\n 2000"}, }, // Args { container: corev1.Container{Name: "test", Image: "image", Args: []string{"time", "1000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "time", "1000"}, }, // Args with newline { container: corev1.Container{Name: "test", Image: "image", Args: []string{"time", "1000\n2000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"1000\n 2000"}, }, // Using limits. { container: corev1.Container{ Name: "test", Image: "image", Resources: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1000"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("4G"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("20G"), }, }, }, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"cpu", "1k", "memory", "4G", "storage", "20G"}, }, // Using requests. { container: corev1.Container{ Name: "test", Image: "image", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1000"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("4G"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("20G"), }, }, }, expectedElements: []string{"cpu", "1k", "memory", "4G", "storage", "20G"}, }, // volumeMounts read/write { container: corev1.Container{ Name: "test", Image: "image", VolumeMounts: []corev1.VolumeMount{ { Name: "mounted-volume", MountPath: "/opt/", }, }, }, expectedElements: []string{"mounted-volume", "/opt/", "(rw)"}, }, // volumeMounts readonly { container: corev1.Container{ Name: "test", Image: "image", VolumeMounts: []corev1.VolumeMount{ { Name: "mounted-volume", MountPath: "/opt/", ReadOnly: true, }, }, }, expectedElements: []string{"Mounts", "mounted-volume", "/opt/", "(ro)"}, }, // volumeMounts subPath { container: corev1.Container{ Name: "test", Image: "image", VolumeMounts: []corev1.VolumeMount{ { Name: "mounted-volume", MountPath: "/opt/", SubPath: "foo", }, }, }, expectedElements: []string{"Mounts", "mounted-volume", "/opt/", "(rw,path=\"foo\")"}, }, // volumeDevices { container: corev1.Container{ Name: "test", Image: "image", VolumeDevices: []corev1.VolumeDevice{ { Name: "volume-device", DevicePath: "/dev/xvda", }, }, }, expectedElements: []string{"Devices", "volume-device", "/dev/xvda"}, }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { out := new(bytes.Buffer) pod := corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{testCase.container}, }, Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{testCase.status}, }, } writer := NewPrefixWriter(out) describeContainers("Containers", pod.Spec.Containers, pod.Status.ContainerStatuses, EnvValueRetriever(&pod), writer, "") output := out.String() for _, expected := range testCase.expectedElements { if !strings.Contains(output, expected) { t.Errorf("Test case %d: expected to find %q in output: %q", i, expected, output) } } }) } } func TestDescribers(t *testing.T) { first := &corev1.Event{} second := &corev1.Pod{} var third *corev1.Pod testErr := fmt.Errorf("test") d := Describers{} d.Add( func(e *corev1.Event, p *corev1.Pod) (string, error) { if e != first { t.Errorf("first argument not equal: %#v", e) } if p != second { t.Errorf("second argument not equal: %#v", p) } return "test", testErr }, ) if out, err := d.DescribeObject(first, second); out != "test" || err != testErr { t.Errorf("unexpected result: %s %v", out, err) } if out, err := d.DescribeObject(first, second, third); out != "" || err == nil { t.Errorf("unexpected result: %s %v", out, err) } else { if noDescriber, ok := err.(ErrNoDescriber); ok { if !reflect.DeepEqual(noDescriber.Types, []string{"*v1.Event", "*v1.Pod", "*v1.Pod"}) { t.Errorf("unexpected describer: %v", err) } } else { t.Errorf("unexpected error type: %v", err) } } d.Add( func(e *corev1.Event) (string, error) { if e != first { t.Errorf("first argument not equal: %#v", e) } return "simpler", testErr }, ) if out, err := d.DescribeObject(first); out != "simpler" || err != testErr { t.Errorf("unexpected result: %s %v", out, err) } } func TestDefaultDescribers(t *testing.T) { out, err := DefaultObjectDescriber.DescribeObject(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Pod `foo` in output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Service `foo` in output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: corev1.ReplicationControllerSpec{Replicas: ptr.To[int32](1)}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Replication Controller `foo` in output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Node `foo` output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: appsv1.StatefulSetSpec{Replicas: ptr.To[int32](1)}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing StatefulSet `foo` in output: %s", out) } } func TestGetPodsTotalRequests(t *testing.T) { testCases := []struct { name string pods *corev1.PodList expectedReqs map[corev1.ResourceName]resource.Quantity }{ { name: "test1", pods: &corev1.PodList{ Items: []corev1.Pod{ { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("300Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("1G"), }, }, }, { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("90m"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("120Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("200M"), }, }, }, }, }, }, { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("60m"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("43Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("500M"), }, }, }, { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("34m"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("83Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("700M"), }, }, }, }, }, }, }, }, expectedReqs: map[corev1.ResourceName]resource.Quantity{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1.184"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("546Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("2.4G"), }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { reqs, _ := getPodsTotalRequestsAndLimits(testCase.pods) if !apiequality.Semantic.DeepEqual(reqs, testCase.expectedReqs) { t.Errorf("Expected %v, got %v", testCase.expectedReqs, reqs) } }) } } func TestPersistentVolumeDescriber(t *testing.T) { block := corev1.PersistentVolumeBlock file := corev1.PersistentVolumeFilesystem foo := "glusterfsendpointname" deletionTimestamp := metav1.Time{Time: time.Now().UTC().AddDate(-10, 0, 0)} testCases := []struct { name string plugin string pv *corev1.PersistentVolume expectedElements []string unexpectedElements []string }{ { name: "test0", plugin: "hostpath", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ HostPath: &corev1.HostPathVolumeSource{Type: new(corev1.HostPathType)}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test1", plugin: "gce", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ GCEPersistentDisk: &corev1.GCEPersistentDiskVolumeSource{}, }, VolumeMode: &file, }, }, expectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test2", plugin: "ebs", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ AWSElasticBlockStore: &corev1.AWSElasticBlockStoreVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test3", plugin: "nfs", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ NFS: &corev1.NFSVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test4", plugin: "iscsi", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ ISCSI: &corev1.ISCSIPersistentVolumeSource{}, }, VolumeMode: &block, }, }, expectedElements: []string{"VolumeMode", "Block"}, }, { name: "test5", plugin: "gluster", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Glusterfs: &corev1.GlusterfsPersistentVolumeSource{}, }, }, }, expectedElements: []string{"EndpointsNamespace", ""}, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test6", plugin: "rbd", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ RBD: &corev1.RBDPersistentVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test7", plugin: "quobyte", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Quobyte: &corev1.QuobyteVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test8", plugin: "cinder", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Cinder: &corev1.CinderPersistentVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test9", plugin: "fc", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ FC: &corev1.FCVolumeSource{}, }, VolumeMode: &block, }, }, expectedElements: []string{"VolumeMode", "Block"}, }, { name: "test10", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, }, }, expectedElements: []string{"Node Affinity: "}, unexpectedElements: []string{"Required Terms", "Term "}, }, { name: "test11", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{}, }, }, expectedElements: []string{"Node Affinity: "}, unexpectedElements: []string{"Required Terms", "Term "}, }, { name: "test12", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{}, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms: "}, unexpectedElements: []string{"Term "}, }, { name: "test13", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: []corev1.NodeSelectorRequirement{}, }, { MatchExpressions: []corev1.NodeSelectorRequirement{}, }, }, }, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms", "Term 0", "Term 1"}, }, { name: "test14", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: []corev1.NodeSelectorRequirement{ { Key: "foo", Operator: "In", Values: []string{"val1", "val2"}, }, { Key: "foo", Operator: "Exists", }, }, }, }, }, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms", "Term 0", "foo in [val1, val2]", "foo exists"}, }, { name: "test15", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", DeletionTimestamp: &deletionTimestamp, }, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, }, }, expectedElements: []string{"Terminating (lasts 10y)"}, }, { name: "test16", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", GenerateName: "test-GenerateName", UID: "test-UID", CreationTimestamp: metav1.Time{Time: time.Now()}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, DeletionGracePeriodSeconds: new(int64), Labels: map[string]string{"label1": "label1", "label2": "label2", "label3": "label3"}, Annotations: map[string]string{"annotation1": "annotation1", "annotation2": "annotation2", "annotation3": "annotation3"}, }, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: []corev1.NodeSelectorRequirement{ { Key: "foo", Operator: "In", Values: []string{"val1", "val2"}, }, { Key: "foo", Operator: "Exists", }, }, }, }, }, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms", "Term 0", "foo in [val1, val2]", "foo exists"}, }, { name: "test17", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", GenerateName: "test-GenerateName", UID: "test-UID", CreationTimestamp: metav1.Time{Time: time.Now()}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, DeletionGracePeriodSeconds: new(int64), Labels: map[string]string{"label1": "label1", "label2": "label2", "label3": "label3"}, Annotations: map[string]string{"annotation1": "annotation1", "annotation2": "annotation2", "annotation3": "annotation3"}, }, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ CSI: &corev1.CSIPersistentVolumeSource{ Driver: "drive", VolumeHandle: "handler", ReadOnly: true, VolumeAttributes: map[string]string{ "Attribute1": "Value1", "Attribute2": "Value2", "Attribute3": "Value3", }, }, }, }, }, expectedElements: []string{"Driver", "VolumeHandle", "ReadOnly", "VolumeAttributes"}, }, { name: "test19", plugin: "gluster", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Glusterfs: &corev1.GlusterfsPersistentVolumeSource{ EndpointsNamespace: &foo, }, }, }, }, expectedElements: []string{"EndpointsNamespace", "glusterfsendpointname"}, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { fake := fake.NewSimpleClientset(test.pv) c := PersistentVolumeDescriber{fake} str, err := c.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.plugin, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected PV Describer output", test.plugin) } for _, expected := range test.expectedElements { if !strings.Contains(str, expected) { t.Errorf("expected to find %q in output: %q", expected, str) } } for _, unexpected := range test.unexpectedElements { if strings.Contains(str, unexpected) { t.Errorf("unexpected to find %q in output: %q", unexpected, str) } } }) } } func TestPersistentVolumeClaimDescriber(t *testing.T) { block := corev1.PersistentVolumeBlock file := corev1.PersistentVolumeFilesystem goldClassName := "gold" now := time.Now() deletionTimestamp := metav1.Time{Time: time.Now().UTC().AddDate(-10, 0, 0)} snapshotAPIGroup := "snapshot.storage.k8s.io" defaultDescriberSettings := &DescriberSettings{ShowEvents: true} testCases := []struct { name string pvc *corev1.PersistentVolumeClaim describerSettings *DescriberSettings expectedElements []string unexpectedElements []string }{ { name: "default", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedElements: []string{"Events"}, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "filesystem", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume2", StorageClassName: &goldClassName, VolumeMode: &file, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "block", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume3", StorageClassName: &goldClassName, VolumeMode: &block, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedElements: []string{"VolumeMode", "Block"}, }, // Tests for Status.Condition. { name: "condition-type", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume4", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Type: corev1.PersistentVolumeClaimResizing}, }, }, }, expectedElements: []string{"Conditions", "Type", "Resizing"}, }, { name: "condition-status", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume5", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Status: corev1.ConditionTrue}, }, }, }, expectedElements: []string{"Conditions", "Status", "True"}, }, { name: "condition-last-probe-time", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume6", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {LastProbeTime: metav1.Time{Time: now}}, }, }, }, expectedElements: []string{"Conditions", "LastProbeTime", now.Format(time.RFC1123Z)}, }, { name: "condition-last-transition-time", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume7", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {LastTransitionTime: metav1.Time{Time: now}}, }, }, }, expectedElements: []string{"Conditions", "LastTransitionTime", now.Format(time.RFC1123Z)}, }, { name: "condition-reason", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume8", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Reason: "OfflineResize"}, }, }, }, expectedElements: []string{"Conditions", "Reason", "OfflineResize"}, }, { name: "condition-message", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume9", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Message: "User request resize"}, }, }, }, expectedElements: []string{"Conditions", "Message", "User request resize"}, }, { name: "deletion-timestamp", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "bar", DeletionTimestamp: &deletionTimestamp, }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume10", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{}, }, expectedElements: []string{"Terminating (lasts 10y)"}, }, { name: "pvc-datasource", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "bar", }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume10", StorageClassName: &goldClassName, DataSource: &corev1.TypedLocalObjectReference{ Name: "srcpvc", Kind: "PersistentVolumeClaim", }, }, Status: corev1.PersistentVolumeClaimStatus{}, }, expectedElements: []string{"\nDataSource:\n Kind: PersistentVolumeClaim\n Name: srcpvc"}, }, { name: "snapshot-datasource", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "bar", }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume10", StorageClassName: &goldClassName, DataSource: &corev1.TypedLocalObjectReference{ Name: "src-snapshot", Kind: "VolumeSnapshot", APIGroup: &snapshotAPIGroup, }, }, Status: corev1.PersistentVolumeClaimStatus{}, }, expectedElements: []string{"DataSource:\n APIGroup: snapshot.storage.k8s.io\n Kind: VolumeSnapshot\n Name: src-snapshot\n"}, }, { name: "no-show-events", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, unexpectedElements: []string{"Events"}, describerSettings: &DescriberSettings{ShowEvents: false}, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { fake := fake.NewSimpleClientset(test.pvc) c := PersistentVolumeClaimDescriber{fake} var describerSettings DescriberSettings if test.describerSettings != nil { describerSettings = *test.describerSettings } else { describerSettings = *defaultDescriberSettings } str, err := c.Describe("foo", "bar", describerSettings) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected PVC Describer output", test.name) } for _, expected := range test.expectedElements { if !strings.Contains(str, expected) { t.Errorf("expected to find %q in output: %q", expected, str) } } for _, unexpected := range test.unexpectedElements { if strings.Contains(str, unexpected) { t.Errorf("unexpected to find %q in output: %q", unexpected, str) } } }) } } func TestGetPodsForPVC(t *testing.T) { goldClassName := "gold" testCases := []struct { name string pvc *corev1.PersistentVolumeClaim requiredObjects []runtime.Object expectedPods []string }{ { name: "pvc-unused", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pvc-name"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedPods: []string{}, }, { name: "pvc-in-pods-volumes-list", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pvc-name"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, requiredObjects: []runtime.Object{ &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod-name"}, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "volume", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "pvc-name", }, }, }, }, }, }, }, expectedPods: []string{"pod-name"}, }, { name: "pvc-owned-by-pod", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns", Name: "pvc-name", OwnerReferences: []metav1.OwnerReference{ { Kind: "Pod", Name: "pod-name", UID: "pod-uid", }, }, }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, requiredObjects: []runtime.Object{ &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod-name", UID: "pod-uid"}, }, }, expectedPods: []string{"pod-name"}, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { var objects []runtime.Object objects = append(objects, test.requiredObjects...) objects = append(objects, test.pvc) fake := fake.NewSimpleClientset(objects...) pods, err := getPodsForPVC(fake.CoreV1().Pods(test.pvc.ObjectMeta.Namespace), test.pvc, DescriberSettings{}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } for _, expectedPod := range test.expectedPods { foundPod := false for _, pod := range pods { if pod.Name == expectedPod { foundPod = true break } } if !foundPod { t.Errorf("Expected pod %s, but it was not returned: %v", expectedPod, pods) } } if len(test.expectedPods) != len(pods) { t.Errorf("Expected %d pods, but got %d pods", len(test.expectedPods), len(pods)) } }) } } func TestDescribeDeployment(t *testing.T) { labels := map[string]string{"k8s-app": "bar"} testCases := []struct { name string objects []runtime.Object expects []string }{ { name: "deployment with two mounted volumes", objects: []runtime.Object{ &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", CreationTimestamp: metav1.NewTime(time.Date(2021, time.Month(1), 1, 0, 0, 0, 0, time.UTC)), }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:latest", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", Labels: labels, OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:latest", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 1, ReadyReplicas: 1, AvailableReplicas: 1, }, }, }, expects: []string{ "Name: bar\nNamespace: foo", "CreationTimestamp: Fri, 01 Jan 2021 00:00:00 +0000", "Labels: k8s-app=bar", "Selector: k8s-app=bar", "Replicas: 1 desired | 0 updated | 0 total | 0 available | 0 unavailable", "Image: mytest-image:latest", "Mounts:\n /tmp/vol-bar from vol-bar (rw)\n /tmp/vol-foo from vol-foo (rw)", "OldReplicaSets: ", "NewReplicaSet: bar-001 (1/1 replicas created)", "Events: ", "Node-Selectors: ", "Tolerations: ", }, }, { name: "deployment during the process of rolling out", objects: []runtime.Object{ &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", CreationTimestamp: metav1.NewTime(time.Date(2021, time.Month(1), 1, 0, 0, 0, 0, time.UTC)), }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.DeploymentStatus{ Replicas: 3, UpdatedReplicas: 1, AvailableReplicas: 2, UnavailableReplicas: 1, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v1.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 2, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000002", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 1, ReadyReplicas: 0, AvailableReplicas: 1, }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-001 from 0 to 2", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-10 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-2 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-003", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled down replica set bar-002 from 2 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-1 * time.Minute)), }, }, expects: []string{ "Replicas: 2 desired | 1 updated | 3 total | 2 available | 1 unavailable", "Image: mytest-image:v2.0", "OldReplicaSets: bar-001 (2/2 replicas created)", "NewReplicaSet: bar-002 (1/1 replicas created)", "Events:\n", "Normal ScalingReplicaSet 12m (x3 over 20m) deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 10m deployment-controller Scaled up replica set bar-001 from 0 to 2", "Normal ScalingReplicaSet 2m deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 60s deployment-controller Scaled down replica set bar-002 from 2 to 1", }, }, { name: "deployment after successful rollout", objects: []runtime.Object{ &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", CreationTimestamp: metav1.NewTime(time.Date(2021, time.Month(1), 1, 0, 0, 0, 0, time.UTC)), }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.DeploymentStatus{ Replicas: 2, UpdatedReplicas: 2, AvailableReplicas: 2, UnavailableReplicas: 0, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](0), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v1.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 0, ReadyReplicas: 0, AvailableReplicas: 0, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000002", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 2, }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-001 from 0 to 2", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-10 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-2 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-003", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled down replica set bar-002 from 2 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-1 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-004", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 2", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Second)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-005", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled down replica set bar-001 from 2 to 0", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-3 * time.Second)), }, }, expects: []string{ "Replicas: 2 desired | 2 updated | 2 total | 2 available | 0 unavailable", "Image: mytest-image:v2.0", "OldReplicaSets: bar-001 (0/0 replicas created)", "NewReplicaSet: bar-002 (2/2 replicas created)", "Events:\n", "Normal ScalingReplicaSet 12m (x3 over 20m) deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 10m deployment-controller Scaled up replica set bar-001 from 0 to 2", "Normal ScalingReplicaSet 2m deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 60s deployment-controller Scaled down replica set bar-002 from 2 to 1", "Normal ScalingReplicaSet 15s deployment-controller Scaled up replica set bar-002 from 0 to 2", "Normal ScalingReplicaSet 3s deployment-controller Scaled down replica set bar-001 from 2 to 0", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fakeClient := fake.NewSimpleClientset(testCase.objects...) d := DeploymentDescriber{fakeClient} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expect := range testCase.expects { if !strings.Contains(out, expect) { t.Errorf("expected to find \"%s\" in:\n %s", expect, out) } } }) } } func TestDescribeJob(t *testing.T) { indexedCompletion := batchv1.IndexedCompletion cases := map[string]struct { job *batchv1.Job wantElements []string dontWantElements []string }{ "empty job": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{}, }, dontWantElements: []string{"Completed Indexes:", "Suspend:", "Backoff Limit:", "TTL Seconds After Finished:"}, }, "no completed indexes": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ CompletionMode: &indexedCompletion, }, }, wantElements: []string{"Completed Indexes: "}, }, "few completed indexes": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ CompletionMode: &indexedCompletion, }, Status: batchv1.JobStatus{ CompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32", }, }, wantElements: []string{"Completed Indexes: 0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32"}, }, "too many completed indexes": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ CompletionMode: &indexedCompletion, }, Status: batchv1.JobStatus{ CompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32-34,36,37", }, }, wantElements: []string{"Completed Indexes: 0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32-34,..."}, }, "suspend set to true": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ Suspend: ptr.To(true), TTLSecondsAfterFinished: ptr.To[int32](123), BackoffLimit: ptr.To[int32](1), }, }, wantElements: []string{ "Suspend: true", "TTL Seconds After Finished: 123", "Backoff Limit: 1", }, }, "suspend set to false": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ Suspend: ptr.To(false), }, }, wantElements: []string{"Suspend: false"}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { client := &describeClient{ T: t, Namespace: tc.job.Namespace, Interface: fake.NewSimpleClientset(tc.job), } describer := JobDescriber{Interface: client} out, err := describer.Describe(tc.job.Namespace, tc.job.Name, DescriberSettings{ShowEvents: true}) if err != nil { t.Fatalf("unexpected error describing object: %v", err) } for _, expected := range tc.wantElements { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output:\n %s", expected, out) } } for _, unexpected := range tc.dontWantElements { if strings.Contains(out, unexpected) { t.Errorf("unexpected to find %q in output:\n %s", unexpected, out) } } }) } } func TestDescribeIngress(t *testing.T) { ingresClassName := "test" backendV1beta1 := networkingv1beta1.IngressBackend{ ServiceName: "default-backend", ServicePort: intstr.FromInt32(80), } v1beta1 := fake.NewSimpleClientset(&networkingv1beta1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Labels: map[string]string{ "id1": "app1", "id2": "app2", }, Namespace: "foo", }, Spec: networkingv1beta1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1beta1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1beta1.IngressRuleValue{ HTTP: &networkingv1beta1.HTTPIngressRuleValue{ Paths: []networkingv1beta1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1beta1, }, }, }, }, }, }, }, }) backendV1 := networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "default-backend", Port: networkingv1.ServiceBackendPort{ Number: 80, }, }, } netv1 := fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1, }, }, }, }, }, }, }, }) backendResource := networkingv1.IngressBackend{ Resource: &corev1.TypedLocalObjectReference{ APIGroup: ptr.To("example.com"), Kind: "foo", Name: "bar", }, } backendResourceNoAPIGroup := networkingv1.IngressBackend{ Resource: &corev1.TypedLocalObjectReference{ Kind: "foo", Name: "bar", }, } tests := map[string]struct { input *fake.Clientset output string }{ "IngressRule.HTTP.Paths.Backend.Service v1beta1": { input: v1beta1, output: `Name: bar Labels: id1=app1 id2=app2 Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "IngressRule.HTTP.Paths.Backend.Service v1": { input: netv1, output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "IngressRule.HTTP.Paths.Backend.Resource v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendResource, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo APIGroup: example.com, Kind: foo, Name: bar Annotations: Events: ` + "\n", }, "IngressRule.HTTP.Paths.Backend.Resource v1 Without APIGroup": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendResourceNoAPIGroup, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo APIGroup: , Kind: foo, Name: bar Annotations: Events: ` + "\n", }, "Spec.DefaultBackend.Service & IngressRule.HTTP.Paths.Backend.Service v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendV1, IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: default-backend:80 () Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "Spec.DefaultBackend.Resource & IngressRule.HTTP.Paths.Backend.Resource v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendResource, IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendResource, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: APIGroup: example.com, Kind: foo, Name: bar Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo APIGroup: example.com, Kind: foo, Name: bar Annotations: Events: ` + "\n", }, "Spec.DefaultBackend.Resource & IngressRule.HTTP.Paths.Backend.Service v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendResource, IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: APIGroup: example.com, Kind: foo, Name: bar Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "DefaultBackend": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendV1, IngressClassName: &ingresClassName, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: default-backend:80 () Rules: Host Path Backends ---- ---- -------- * * default-backend:80 () Annotations: Events: `, }, "EmptyBackend": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- * * Annotations: Events: `, }, "EmptyIngressClassName": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendV1, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: Default backend: default-backend:80 () Rules: Host Path Backends ---- ---- -------- * * default-backend:80 () Annotations: Events: `, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: test.input} i := IngressDescriber{c} out, err := i.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != test.output { t.Log(out) t.Log(test.output) t.Errorf("expected: \n%q\n but got output: \n%q\n", test.output, out) } }) } } func TestDescribeIngressV1(t *testing.T) { ingresClassName := "test" defaultBackend := networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "default-backend", Port: networkingv1.ServiceBackendPort{ Number: 80, }, }, } fakeClient := fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Labels: map[string]string{ "id1": "app1", "id2": "app2", }, Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: defaultBackend, }, }, }, }, }, }, }, }) i := IngressDescriber{fakeClient} out, err := i.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "bar") || !strings.Contains(out, "foo") || !strings.Contains(out, "foo.bar.com") || !strings.Contains(out, "/foo") || !strings.Contains(out, "app1") || !strings.Contains(out, "app2") { t.Errorf("unexpected out: %s", out) } } func TestDescribeStorageClass(t *testing.T) { reclaimPolicy := corev1.PersistentVolumeReclaimRetain bindingMode := storagev1.VolumeBindingMode("bindingmode") f := fake.NewSimpleClientset(&storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", ResourceVersion: "4", Annotations: map[string]string{ "name": "foo", }, }, Provisioner: "my-provisioner", Parameters: map[string]string{ "param1": "value1", "param2": "value2", }, ReclaimPolicy: &reclaimPolicy, VolumeBindingMode: &bindingMode, AllowedTopologies: []corev1.TopologySelectorTerm{ { MatchLabelExpressions: []corev1.TopologySelectorLabelRequirement{ { Key: "failure-domain.beta.kubernetes.io/zone", Values: []string{"zone1"}, }, { Key: "kubernetes.io/hostname", Values: []string{"node1"}, }, }, }, { MatchLabelExpressions: []corev1.TopologySelectorLabelRequirement{ { Key: "failure-domain.beta.kubernetes.io/zone", Values: []string{"zone2"}, }, { Key: "kubernetes.io/hostname", Values: []string{"node2"}, }, }, }, }, }) s := StorageClassDescriber{f} out, err := s.Describe("", "foo", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "foo") || !strings.Contains(out, "my-provisioner") || !strings.Contains(out, "param1") || !strings.Contains(out, "param2") || !strings.Contains(out, "value1") || !strings.Contains(out, "value2") || !strings.Contains(out, "Retain") || !strings.Contains(out, "bindingmode") || !strings.Contains(out, "failure-domain.beta.kubernetes.io/zone") || !strings.Contains(out, "zone1") || !strings.Contains(out, "kubernetes.io/hostname") || !strings.Contains(out, "node1") || !strings.Contains(out, "zone2") || !strings.Contains(out, "node2") { t.Errorf("unexpected out: %s", out) } } func TestDescribeVolumeAttributesClass(t *testing.T) { expectedOut := `Name: foo Annotations: name=bar DriverName: my-driver Parameters: param1=value1,param2=value2 Events: ` f := fake.NewSimpleClientset(&storagev1beta1.VolumeAttributesClass{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", ResourceVersion: "4", Annotations: map[string]string{ "name": "bar", }, }, DriverName: "my-driver", Parameters: map[string]string{ "param1": "value1", "param2": "value2", }, }) s := VolumeAttributesClassDescriber{f} out, err := s.Describe("", "foo", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != expectedOut { t.Errorf("expected:\n %s\n but got output:\n %s diff:\n%s", expectedOut, out, cmp.Diff(out, expectedOut)) } } func TestDescribeCSINode(t *testing.T) { limit := ptr.To[int32](2) f := fake.NewSimpleClientset(&storagev1.CSINode{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: storagev1.CSINodeSpec{ Drivers: []storagev1.CSINodeDriver{ { Name: "driver1", NodeID: "node1", }, { Name: "driver2", NodeID: "node2", Allocatable: &storagev1.VolumeNodeResources{Count: limit}, }, }, }, }) s := CSINodeDescriber{f} out, err := s.Describe("", "foo", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "foo") || !strings.Contains(out, "driver1") || !strings.Contains(out, "node1") || !strings.Contains(out, "driver2") || !strings.Contains(out, "node2") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodDisruptionBudgetV1beta1(t *testing.T) { minAvailable := intstr.FromInt32(22) f := fake.NewSimpleClientset(&policyv1beta1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns1", Name: "pdb1", CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, }, Spec: policyv1beta1.PodDisruptionBudgetSpec{ MinAvailable: &minAvailable, }, Status: policyv1beta1.PodDisruptionBudgetStatus{ DisruptionsAllowed: 5, }, }) s := PodDisruptionBudgetDescriber{f} out, err := s.Describe("ns1", "pdb1", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "pdb1") || !strings.Contains(out, "ns1") || !strings.Contains(out, "22") || !strings.Contains(out, "5") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodDisruptionBudgetV1(t *testing.T) { minAvailable := intstr.FromInt32(22) f := fake.NewSimpleClientset(&policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns1", Name: "pdb1", CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, }, Spec: policyv1.PodDisruptionBudgetSpec{ MinAvailable: &minAvailable, }, Status: policyv1.PodDisruptionBudgetStatus{ DisruptionsAllowed: 5, }, }) s := PodDisruptionBudgetDescriber{f} out, err := s.Describe("ns1", "pdb1", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "pdb1") || !strings.Contains(out, "ns1") || !strings.Contains(out, "22") || !strings.Contains(out, "5") { t.Errorf("unexpected out: %s", out) } } func TestDescribeHorizontalPodAutoscaler(t *testing.T) { minReplicasVal := int32(2) targetUtilizationVal := int32(80) currentUtilizationVal := int32(50) maxSelectPolicy := autoscalingv2.MaxChangePolicySelect metricLabelSelector, err := metav1.ParseToLabelSelector("label=value") if err != nil { t.Errorf("unable to parse label selector: %v", err) } testsv2 := []struct { name string hpa autoscalingv2.HorizontalPodAutoscaler }{ { "minReplicas unset", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MaxReplicas: 10, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "external source type, target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "external source type, target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "external source type, target value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "external source type, target value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Current: autoscalingv2.MetricValueStatus{ Value: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "pods source type (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "pods source type (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "object source type target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "object source type target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricStatus{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "object source type target value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "object source type target value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricStatus{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Current: autoscalingv2.MetricValueStatus{ Value: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "resource source type, target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "resource source type, target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: corev1.ResourceCPU, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "resource source type, target utilization (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "resource source type, target utilization (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: corev1.ResourceCPU, Current: autoscalingv2.MetricValueStatus{ AverageUtilization: ¤tUtilizationVal, AverageValue: resource.NewMilliQuantity(40, resource.DecimalSI), }, }, }, }, }, }, }, { "container resource source type, target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "container resource source type, target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricStatus{ Name: corev1.ResourceCPU, Container: "application", Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "container resource source type, target utilization (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "container resource source type, target utilization (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricStatus{ Name: corev1.ResourceCPU, Container: "application", Current: autoscalingv2.MetricValueStatus{ AverageUtilization: ¤tUtilizationVal, AverageValue: resource.NewMilliQuantity(40, resource.DecimalSI), }, }, }, }, }, }, }, { "multiple metrics", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "other-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(400, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: corev1.ResourceCPU, Current: autoscalingv2.MetricValueStatus{ AverageUtilization: ¤tUtilizationVal, AverageValue: resource.NewMilliQuantity(40, resource.DecimalSI), }, }, }, }, }, }, }, { "scale up behavior specified", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "behavior-target", Kind: "Deployment", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ ScaleUp: &autoscalingv2.HPAScalingRules{ StabilizationWindowSeconds: ptr.To[int32](30), SelectPolicy: &maxSelectPolicy, Policies: []autoscalingv2.HPAScalingPolicy{ {Type: autoscalingv2.PodsScalingPolicy, Value: 10, PeriodSeconds: 10}, {Type: autoscalingv2.PercentScalingPolicy, Value: 10, PeriodSeconds: 10}, }, }, }, }, }, }, { "scale down behavior specified", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "behavior-target", Kind: "Deployment", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ ScaleDown: &autoscalingv2.HPAScalingRules{ StabilizationWindowSeconds: ptr.To[int32](30), Policies: []autoscalingv2.HPAScalingPolicy{ {Type: autoscalingv2.PodsScalingPolicy, Value: 10, PeriodSeconds: 10}, {Type: autoscalingv2.PercentScalingPolicy, Value: 10, PeriodSeconds: 10}, }, }, }, }, }, }, } for _, test := range testsv2 { t.Run(test.name, func(t *testing.T) { test.hpa.ObjectMeta = metav1.ObjectMeta{ Name: "bar", Namespace: "foo", } fake := fake.NewSimpleClientset(&test.hpa) desc := HorizontalPodAutoscalerDescriber{fake} str, err := desc.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected HPA Describer output", test.name) } t.Logf("Description for %q:\n%s", test.name, str) }) } testsV1 := []struct { name string hpa autoscalingv1.HorizontalPodAutoscaler }{ { "minReplicas unset", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MaxReplicas: 10, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "minReplicas set", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "with target no current", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, TargetCPUUtilizationPercentage: &targetUtilizationVal, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "with target and current", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, TargetCPUUtilizationPercentage: &targetUtilizationVal, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentCPUUtilizationPercentage: ¤tUtilizationVal, }, }, }, } for _, test := range testsV1 { t.Run(test.name, func(t *testing.T) { test.hpa.ObjectMeta = metav1.ObjectMeta{ Name: "bar", Namespace: "foo", } fake := fake.NewSimpleClientset(&test.hpa) desc := HorizontalPodAutoscalerDescriber{fake} str, err := desc.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected HPA Describer output", test.name) } t.Logf("Description for %q:\n%s", test.name, str) }) } } func TestDescribeEvents(t *testing.T) { events := &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "event-1", Namespace: "foo", }, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 1", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{ Name: "event-2", Namespace: "foo", }, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 1", EventTime: metav1.NewMicroTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Series: &corev1.EventSeries{ Count: 1, LastObservedTime: metav1.NewMicroTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), }, Type: corev1.EventTypeNormal, }, }, } m := map[string]ResourceDescriber{ "DaemonSetDescriber": &DaemonSetDescriber{ fake.NewSimpleClientset(&appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "DeploymentDescriber": &DeploymentDescriber{ fake.NewSimpleClientset(&appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{}, }, }, events), }, "EndpointsDescriber": &EndpointsDescriber{ fake.NewSimpleClientset(&corev1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "EndpointSliceDescriber": &EndpointSliceDescriber{ fake.NewSimpleClientset(&discoveryv1beta1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "JobDescriber": &JobDescriber{ fake.NewSimpleClientset(&batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "IngressDescriber": &IngressDescriber{ fake.NewSimpleClientset(&networkingv1beta1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "NodeDescriber": &NodeDescriber{ fake.NewSimpleClientset(&corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, }, events), }, "PersistentVolumeDescriber": &PersistentVolumeDescriber{ fake.NewSimpleClientset(&corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, }, events), }, "PodDescriber": &PodDescriber{ fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "ReplicaSetDescriber": &ReplicaSetDescriber{ fake.NewSimpleClientset(&appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](1), }, }, events), }, "ReplicationControllerDescriber": &ReplicationControllerDescriber{ fake.NewSimpleClientset(&corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](1), }, }, events), }, "Service": &ServiceDescriber{ fake.NewSimpleClientset(&corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "StorageClass": &StorageClassDescriber{ fake.NewSimpleClientset(&storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, }, events), }, "HorizontalPodAutoscaler": &HorizontalPodAutoscalerDescriber{ fake.NewSimpleClientset(&autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "ConfigMap": &ConfigMapDescriber{ fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, } for name, d := range m { t.Run(name, func(t *testing.T) { out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error for %q: %v", name, err) } if !strings.Contains(out, "bar") { t.Errorf("unexpected out for %q: %s", name, out) } if !strings.Contains(out, "Events:") { t.Errorf("events not found for %q when ShowEvents=true: %s", name, out) } out, err = d.Describe("foo", "bar", DescriberSettings{ShowEvents: false}) if err != nil { t.Errorf("unexpected error for %q: %s", name, err) } if !strings.Contains(out, "bar") { t.Errorf("unexpected out for %q: %s", name, out) } if strings.Contains(out, "Events:") { t.Errorf("events found for %q when ShowEvents=false: %s", name, out) } }) } } func TestPrintLabelsMultiline(t *testing.T) { key := "MaxLenAnnotation" value := strings.Repeat("a", maxAnnotationLen-len(key)-2) testCases := []struct { annotations map[string]string expectPrint string }{ { annotations: map[string]string{"col1": "asd", "COL2": "zxc"}, expectPrint: "Annotations:\tCOL2: zxc\n\tcol1: asd\n", }, { annotations: map[string]string{"MaxLenAnnotation": value}, expectPrint: fmt.Sprintf("Annotations:\t%s: %s\n", key, value), }, { annotations: map[string]string{"MaxLenAnnotation": value + "1"}, expectPrint: fmt.Sprintf("Annotations:\t%s:\n\t %s\n", key, value+"1"), }, { annotations: map[string]string{"MaxLenAnnotation": value + value}, expectPrint: fmt.Sprintf("Annotations:\t%s:\n\t %s\n", key, strings.Repeat("a", maxAnnotationLen-2)+"..."), }, { annotations: map[string]string{"key": "value\nwith\nnewlines\n"}, expectPrint: "Annotations:\tkey:\n\t value\n\t with\n\t newlines\n", }, { annotations: map[string]string{}, expectPrint: "Annotations:\t\n", }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { out := new(bytes.Buffer) writer := NewPrefixWriter(out) printAnnotationsMultiline(writer, "Annotations", testCase.annotations) output := out.String() if output != testCase.expectPrint { t.Errorf("Test case %d: expected to match:\n%q\nin output:\n%q", i, testCase.expectPrint, output) } }) } } func TestDescribeUnstructuredContent(t *testing.T) { testCases := []struct { expected string unexpected string }{ { expected: `API Version: v1 Dummy - Dummy: present dummy-dummy@dummy: present dummy/dummy: present dummy2: present Dummy Dummy: present Items: Item Bool: true Item Int: 42 Kind: Test Metadata: Creation Timestamp: 2017-04-01T00:00:00Z Name: MyName Namespace: MyNamespace Resource Version: 123 UID: 00000000-0000-0000-0000-000000000001 Status: ok URL: http://localhost `, }, { unexpected: "\nDummy 1:\tpresent\n", }, { unexpected: "Dummy 1", }, { unexpected: "Dummy 3", }, { unexpected: "Dummy3", }, { unexpected: "dummy3", }, { unexpected: "dummy 3", }, } out := new(bytes.Buffer) w := NewPrefixWriter(out) obj := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Test", "dummyDummy": "present", "dummy/dummy": "present", "dummy-dummy@dummy": "present", "dummy-dummy": "present", "dummy1": "present", "dummy2": "present", "metadata": map[string]interface{}{ "name": "MyName", "namespace": "MyNamespace", "creationTimestamp": "2017-04-01T00:00:00Z", "resourceVersion": 123, "uid": "00000000-0000-0000-0000-000000000001", "dummy3": "present", }, "items": []interface{}{ map[string]interface{}{ "itemBool": true, "itemInt": 42, }, }, "url": "http://localhost", "status": "ok", }, } printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".dummy1", ".metadata.dummy3") output := out.String() for _, test := range testCases { if len(test.expected) > 0 { if !strings.Contains(output, test.expected) { t.Errorf("Expected to find %q in: %q", test.expected, output) } } if len(test.unexpected) > 0 { if strings.Contains(output, test.unexpected) { t.Errorf("Didn't expect to find %q in: %q", test.unexpected, output) } } } } func TestDescribeResourceQuota(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Status: corev1.ResourceQuotaStatus{ Hard: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1"), corev1.ResourceName(corev1.ResourceLimitsCPU): resource.MustParse("2"), corev1.ResourceName(corev1.ResourceLimitsMemory): resource.MustParse("2G"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1G"), corev1.ResourceName(corev1.ResourceRequestsCPU): resource.MustParse("1"), corev1.ResourceName(corev1.ResourceRequestsMemory): resource.MustParse("1G"), }, Used: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("0"), corev1.ResourceName(corev1.ResourceLimitsCPU): resource.MustParse("0"), corev1.ResourceName(corev1.ResourceLimitsMemory): resource.MustParse("0G"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("0G"), corev1.ResourceName(corev1.ResourceRequestsCPU): resource.MustParse("0"), corev1.ResourceName(corev1.ResourceRequestsMemory): resource.MustParse("1000Ki"), }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ResourceQuotaDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := []string{"bar", "foo", "limits.cpu", "2", "limits.memory", "2G", "requests.cpu", "1", "requests.memory", "1024k", "1G"} for _, expected := range expectedOut { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } } func TestDescribeIngressClass(t *testing.T) { expectedOut := `Name: example-class Labels: Annotations: Controller: example.com/controller Parameters: APIGroup: v1 Kind: ConfigMap Name: example-parameters` + "\n" tests := map[string]struct { input *fake.Clientset output string }{ "basic IngressClass (v1beta1)": { input: fake.NewSimpleClientset(&networkingv1beta1.IngressClass{ ObjectMeta: metav1.ObjectMeta{ Name: "example-class", }, Spec: networkingv1beta1.IngressClassSpec{ Controller: "example.com/controller", Parameters: &networkingv1beta1.IngressClassParametersReference{ APIGroup: ptr.To("v1"), Kind: "ConfigMap", Name: "example-parameters", }, }, }), output: expectedOut, }, "basic IngressClass (v1)": { input: fake.NewSimpleClientset(&networkingv1.IngressClass{ ObjectMeta: metav1.ObjectMeta{ Name: "example-class", }, Spec: networkingv1.IngressClassSpec{ Controller: "example.com/controller", Parameters: &networkingv1.IngressClassParametersReference{ APIGroup: ptr.To("v1"), Kind: "ConfigMap", Name: "example-parameters", }, }, }), output: expectedOut, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: test.input} d := IngressClassDescriber{c} out, err := d.Describe("", "example-class", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != expectedOut { t.Log(out) t.Errorf("expected : %q\n but got output:\n %q", test.output, out) } }) } } func TestDescribeNetworkPolicies(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port: 80/TCP To Port: 82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Allowing egress traffic: To Port: 80/TCP To Port: 82/TCP To: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 To: PodSelector: id=app2,id2=app3 To: NamespaceSelector: id=app2,id2=app3 To: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 To: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) To: (traffic not restricted by destination) Policy Types: Ingress, Egress ` port80 := intstr.FromInt32(80) port82 := intstr.FromInt32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, Egress: []networkingv1.NetworkPolicyEgressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, To: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeIngressNetworkPolicies(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port: 80/TCP To Port: 82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Not affecting egress traffic Policy Types: Ingress ` port80 := intstr.FromInt32(80) port82 := intstr.FromInt32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeIsolatedEgressNetworkPolicies(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port: 80/TCP To Port: 82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Allowing egress traffic: (Selected pods are isolated for egress connectivity) Policy Types: Ingress, Egress ` port80 := intstr.FromInt32(80) port82 := intstr.FromInt32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeNetworkPoliciesWithPortRange(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port Range: 80-82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Allowing egress traffic: To Port Range: 80-82/TCP To: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 To: PodSelector: id=app2,id2=app3 To: NamespaceSelector: id=app2,id2=app3 To: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 To: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) To: (traffic not restricted by destination) Policy Types: Ingress, Egress ` port80 := intstr.FromInt(80) port82 := int32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80, EndPort: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, Egress: []networkingv1.NetworkPolicyEgressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80, EndPort: &port82, Protocol: &protoTCP}, }, To: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeServiceAccount(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Secrets: []corev1.ObjectReference{ { Name: "test-objectref", }, }, ImagePullSecrets: []corev1.LocalObjectReference{ { Name: "test-local-ref", }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ServiceAccountDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := `Name: bar Namespace: foo Labels: Annotations: Image pull secrets: test-local-ref (not found) Mountable secrets: test-objectref (not found) Tokens: Events: ` + "\n" if out != expectedOut { t.Errorf("expected : %q\n but got output:\n %q", expectedOut, out) } } func getHugePageResourceList(pageSize, value string) corev1.ResourceList { res := corev1.ResourceList{} if pageSize != "" && value != "" { res[corev1.ResourceName(corev1.ResourceHugePagesPrefix+pageSize)] = resource.MustParse(value) } return res } // mergeResourceLists will merge resoure lists. When two lists have the same resourece, the value from // the last list will be present in the result func mergeResourceLists(resourceLists ...corev1.ResourceList) corev1.ResourceList { result := corev1.ResourceList{} for _, rl := range resourceLists { for resource, quantity := range rl { result[resource] = quantity } } return result } func TestDescribeNode(t *testing.T) { holderIdentity := "holder" nodeCapacity := mergeResourceLists( getHugePageResourceList("2Mi", "4Gi"), getResourceList("8", "24Gi"), getHugePageResourceList("1Gi", "0"), ) nodeAllocatable := mergeResourceLists( getHugePageResourceList("2Mi", "2Gi"), getResourceList("4", "12Gi"), getHugePageResourceList("1Gi", "0"), ) fake := fake.NewSimpleClientset( &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", UID: "uid", }, Spec: corev1.NodeSpec{ Unschedulable: true, }, Status: corev1.NodeStatus{ Capacity: nodeCapacity, Allocatable: nodeAllocatable, }, }, &coordinationv1.Lease{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: corev1.NamespaceNodeLease, }, Spec: coordinationv1.LeaseSpec{ HolderIdentity: &holderIdentity, AcquireTime: &metav1.MicroTime{Time: time.Now().Add(-time.Hour)}, RenewTime: &metav1.MicroTime{Time: time.Now()}, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-with-resources", Namespace: "foo", }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "cpu-mem", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), Limits: getResourceList("2", "2Gi"), }, }, { Name: "hugepages", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getHugePageResourceList("2Mi", "512Mi"), Limits: getHugePageResourceList("2Mi", "512Mi"), }, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "event-1", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "bar", }, Message: "Node bar status is now: NodeHasNoDiskPressure", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{ Name: "event-2", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "0ceac5fb-a393-49d7-b04f-9ea5f18de5e9", }, Message: "Node bar status is now: NodeReady", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 2, Type: corev1.EventTypeNormal, }, }, }, ) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := NodeDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := []string{"Unschedulable", "true", "holder", `Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1 (25%) 2 (50%) memory 1Gi (8%) 2Gi (16%) ephemeral-storage 0 (0%) 0 (0%) hugepages-1Gi 0 (0%) 0 (0%) hugepages-2Mi 512Mi (25%) 512Mi (25%)`, `Node bar status is now: NodeHasNoDiskPressure`, `Node bar status is now: NodeReady`} for _, expected := range expectedOut { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } } func TestDescribeNodeWithSidecar(t *testing.T) { holderIdentity := "holder" nodeCapacity := mergeResourceLists( getHugePageResourceList("2Mi", "4Gi"), getResourceList("8", "24Gi"), getHugePageResourceList("1Gi", "0"), ) nodeAllocatable := mergeResourceLists( getHugePageResourceList("2Mi", "2Gi"), getResourceList("4", "12Gi"), getHugePageResourceList("1Gi", "0"), ) restartPolicy := corev1.ContainerRestartPolicyAlways fake := fake.NewSimpleClientset( &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", UID: "uid", }, Spec: corev1.NodeSpec{ Unschedulable: true, }, Status: corev1.NodeStatus{ Capacity: nodeCapacity, Allocatable: nodeAllocatable, }, }, &coordinationv1.Lease{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: corev1.NamespaceNodeLease, }, Spec: coordinationv1.LeaseSpec{ HolderIdentity: &holderIdentity, AcquireTime: &metav1.MicroTime{Time: time.Now().Add(-time.Hour)}, RenewTime: &metav1.MicroTime{Time: time.Now()}, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-with-resources", Namespace: "foo", }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ // sidecar, should sum into the total resources { Name: "init-container-1", RestartPolicy: &restartPolicy, Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), }, }, // non-sidecar { Name: "init-container-2", Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), }, }, }, Containers: []corev1.Container{ { Name: "cpu-mem", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), Limits: getResourceList("2", "2Gi"), }, }, { Name: "hugepages", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getHugePageResourceList("2Mi", "512Mi"), Limits: getHugePageResourceList("2Mi", "512Mi"), }, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "event-1", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "bar", }, Message: "Node bar status is now: NodeHasNoDiskPressure", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{ Name: "event-2", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "0ceac5fb-a393-49d7-b04f-9ea5f18de5e9", }, Message: "Node bar status is now: NodeReady", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 2, Type: corev1.EventTypeNormal, }, }, }, ) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := NodeDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := []string{"Unschedulable", "true", "holder", `Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 2 (50%) 2 (50%) memory 2Gi (16%) 2Gi (16%) ephemeral-storage 0 (0%) 0 (0%) hugepages-1Gi 0 (0%) 0 (0%) hugepages-2Mi 512Mi (25%) 512Mi (25%)`, `Node bar status is now: NodeHasNoDiskPressure`, `Node bar status is now: NodeReady`} for _, expected := range expectedOut { if !strings.Contains(out, expected) { t.Errorf("expected to find %s in output: %s", expected, out) } } } func TestDescribeStatefulSet(t *testing.T) { var partition int32 = 2672 var replicas int32 = 1 fake := fake.NewSimpleClientset(&appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.StatefulSetSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{}, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, }, UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ Type: appsv1.RollingUpdateStatefulSetStrategyType, RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ Partition: &partition, }, }, }, }) d := StatefulSetDescriber{fake} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOutputs := []string{ "bar", "foo", "Containers:", "mytest-image:latest", "Update Strategy", "RollingUpdate", "Partition", "2672", } for _, o := range expectedOutputs { if !strings.Contains(out, o) { t.Errorf("unexpected out: %s", out) break } } } func TestDescribeDaemonSet(t *testing.T) { fake := fake.NewSimpleClientset(&appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.DaemonSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"node-role.kubernetes.io/control-plane": "true"}, }, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, }, }, }) d := DaemonSetDescriber{fake} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOutputs := []string{ "bar", "foo", "Containers:", "mytest-image:latest", "Selector", "node-role.kubernetes.io/control-plane=true", } for _, o := range expectedOutputs { if !strings.Contains(out, o) { t.Errorf("unexpected out: %s", out) break } } } func TestDescribeEndpointSlice(t *testing.T) { protocolTCP := corev1.ProtocolTCP port80 := int32(80) testcases := map[string]struct { input *fake.Clientset output string }{ "EndpointSlices v1beta1": { input: fake.NewSimpleClientset(&discoveryv1beta1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", Namespace: "bar", }, AddressType: discoveryv1beta1.AddressTypeIPv4, Endpoints: []discoveryv1beta1.Endpoint{ { Addresses: []string{"1.2.3.4", "1.2.3.5"}, Conditions: discoveryv1beta1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-123"}, Topology: map[string]string{ "topology.kubernetes.io/zone": "us-central1-a", "topology.kubernetes.io/region": "us-central1", }, }, { Addresses: []string{"1.2.3.6", "1.2.3.7"}, Conditions: discoveryv1beta1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-124"}, Topology: map[string]string{ "topology.kubernetes.io/zone": "us-central1-b", "topology.kubernetes.io/region": "us-central1", }, }, }, Ports: []discoveryv1beta1.EndpointPort{ { Protocol: &protocolTCP, Port: &port80, }, }, }), output: `Name: foo.123 Namespace: bar Labels: Annotations: AddressType: IPv4 Ports: Name Port Protocol ---- ---- -------- 80 TCP Endpoints: - Addresses: 1.2.3.4,1.2.3.5 Conditions: Ready: true Hostname: TargetRef: Pod/test-123 Topology: topology.kubernetes.io/region=us-central1 topology.kubernetes.io/zone=us-central1-a - Addresses: 1.2.3.6,1.2.3.7 Conditions: Ready: true Hostname: TargetRef: Pod/test-124 Topology: topology.kubernetes.io/region=us-central1 topology.kubernetes.io/zone=us-central1-b Events: ` + "\n", }, "EndpointSlices v1": { input: fake.NewSimpleClientset(&discoveryv1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", Namespace: "bar", }, AddressType: discoveryv1.AddressTypeIPv4, Endpoints: []discoveryv1.Endpoint{ { Addresses: []string{"1.2.3.4", "1.2.3.5"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-123"}, Zone: ptr.To("us-central1-a"), NodeName: ptr.To("node-1"), }, { Addresses: []string{"1.2.3.6", "1.2.3.7"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-124"}, NodeName: ptr.To("node-2"), }, }, Ports: []discoveryv1.EndpointPort{ { Protocol: &protocolTCP, Port: &port80, }, }, }), output: `Name: foo.123 Namespace: bar Labels: Annotations: AddressType: IPv4 Ports: Name Port Protocol ---- ---- -------- 80 TCP Endpoints: - Addresses: 1.2.3.4, 1.2.3.5 Conditions: Ready: true Hostname: TargetRef: Pod/test-123 NodeName: node-1 Zone: us-central1-a - Addresses: 1.2.3.6, 1.2.3.7 Conditions: Ready: true Hostname: TargetRef: Pod/test-124 NodeName: node-2 Zone: Events: ` + "\n", }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: tc.input} d := EndpointSliceDescriber{c} out, err := d.Describe("bar", "foo.123", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != tc.output { t.Log(out) t.Errorf("expected :\n%s\nbut got output:\n%s", tc.output, out) } }) } } func TestDescribeServiceCIDR(t *testing.T) { testcases := map[string]struct { input *fake.Clientset output string }{ "ServiceCIDR v1beta1": { input: fake.NewSimpleClientset(&networkingv1beta1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16", "fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16, fd00:1:1::/64 Events: ` + "\n", }, "ServiceCIDR v1beta1 IPv4": { input: fake.NewSimpleClientset(&networkingv1beta1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16 Events: ` + "\n", }, "ServiceCIDR v1beta1 IPv6": { input: fake.NewSimpleClientset(&networkingv1beta1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.ServiceCIDRSpec{ CIDRs: []string{"fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: fd00:1:1::/64 Events: ` + "\n", }, "ServiceCIDR v1": { input: fake.NewSimpleClientset(&networkingv1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16", "fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16, fd00:1:1::/64 Events: ` + "\n", }, "ServiceCIDR v1 IPv4": { input: fake.NewSimpleClientset(&networkingv1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16 Events: ` + "\n", }, "ServiceCIDR v1 IPv6": { input: fake.NewSimpleClientset(&networkingv1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.ServiceCIDRSpec{ CIDRs: []string{"fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: fd00:1:1::/64 Events: ` + "\n", }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: tc.input} d := ServiceCIDRDescriber{c} out, err := d.Describe("bar", "foo.123", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != tc.output { t.Errorf("expected :\n%s\nbut got output:\n%s diff:\n%s", tc.output, out, cmp.Diff(tc.output, out)) } }) } } func TestDescribeIPAddress(t *testing.T) { testcases := map[string]struct { input *fake.Clientset output string }{ "IPAddress v1beta1": { input: fake.NewSimpleClientset(&networkingv1beta1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.IPAddressSpec{ ParentRef: &networkingv1beta1.ParentReference{ Group: "mygroup", Resource: "myresource", Namespace: "mynamespace", Name: "myname", }, }, }), output: `Name: foo.123 Labels: Annotations: Parent Reference: Group: mygroup Resource: myresource Namespace: mynamespace Name: myname Events: ` + "\n", }, "IPAddress v1": { input: fake.NewSimpleClientset(&networkingv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.IPAddressSpec{ ParentRef: &networkingv1.ParentReference{ Group: "mygroup", Resource: "myresource", Namespace: "mynamespace", Name: "myname", }, }, }), output: `Name: foo.123 Labels: Annotations: Parent Reference: Group: mygroup Resource: myresource Namespace: mynamespace Name: myname Events: ` + "\n", }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: tc.input} d := IPAddressDescriber{c} out, err := d.Describe("bar", "foo.123", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != tc.output { t.Errorf("expected :\n%s\nbut got output:\n%s diff:\n%s", tc.output, out, cmp.Diff(tc.output, out)) } }) } } func TestControllerRef(t *testing.T) { var replicas int32 = 1 f := fake.NewSimpleClientset( &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", UID: "123456", }, TypeMeta: metav1.TypeMeta{ Kind: "ReplicationController", }, Spec: corev1.ReplicationControllerSpec{ Replicas: &replicas, Selector: map[string]string{"abc": "xyz"}, Template: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, }, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "barpod", Namespace: "foo", Labels: map[string]string{"abc": "xyz"}, OwnerReferences: []metav1.OwnerReference{{Name: "bar", UID: "123456", Controller: ptr.To(true)}}, }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "orphan", Namespace: "foo", Labels: map[string]string{"abc": "xyz"}, }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "buzpod", Namespace: "foo", Labels: map[string]string{"abc": "xyz"}, OwnerReferences: []metav1.OwnerReference{{Name: "buz", UID: "654321", Controller: ptr.To(true)}}, }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }) d := ReplicationControllerDescriber{f} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: false}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "1 Running") { t.Errorf("unexpected out: %s", out) } } func TestDescribeTerminalEscape(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "mycm", Namespace: "foo", Annotations: map[string]string{"annotation1": "terminal escape: \x1b"}, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ConfigMapDescriber{c} out, err := d.Describe("foo", "mycm", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if strings.Contains(out, "\x1b") || !strings.Contains(out, "^[") { t.Errorf("unexpected out: %s", out) } } func TestDescribeSeccompProfile(t *testing.T) { testLocalhostProfiles := []string{"lauseafoodpod", "tikkamasalaconatiner", "dropshotephemeral"} testCases := []struct { name string pod *corev1.Pod expect []string }{ { name: "podLocalhostSeccomp", pod: &corev1.Pod{ Spec: corev1.PodSpec{ SecurityContext: &corev1.PodSecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &testLocalhostProfiles[0], }, }, }, }, expect: []string{ "SeccompProfile", "Localhost", "LocalhostProfile", testLocalhostProfiles[0], }, }, { name: "podOther", pod: &corev1.Pod{ Spec: corev1.PodSpec{ SecurityContext: &corev1.PodSecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, }, }, expect: []string{ "SeccompProfile", "RuntimeDefault", }, }, { name: "containerLocalhostSeccomp", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &testLocalhostProfiles[1], }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Localhost", "LocalhostProfile", testLocalhostProfiles[1], }, }, { name: "containerOther", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeUnconfined, }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Unconfined", }, }, { name: "ephemeralLocalhostSeccomp", pod: &corev1.Pod{ Spec: corev1.PodSpec{ EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &testLocalhostProfiles[2], }, }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Localhost", "LocalhostProfile", testLocalhostProfiles[2], }, }, { name: "ephemeralOther", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeUnconfined, }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Unconfined", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.pod) c := &describeClient{T: t, Interface: fake} d := PodDescriber{c} out, err := d.Describe("", "", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } }) } } func TestDescribeProjectedVolumesOptionalSecret(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "optional-secret", VolumeSource: corev1.VolumeSource{ Projected: &corev1.ProjectedVolumeSource{ Sources: []corev1.VolumeProjection{ { Secret: &corev1.SecretProjection{ LocalObjectReference: corev1.LocalObjectReference{ Name: "optional-secret", }, Optional: ptr.To(true), }, }, }, }, }, }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := "SecretName: optional-secret\n Optional: true" if !strings.Contains(out, expectedOut) { t.Errorf("expected to find %q in output: %q", expectedOut, out) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/describe/interface.go000066400000000000000000000050151504711711200301140ustar00rootroot00000000000000/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package describe import ( "fmt" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" ) const ( // LoadBalancerWidth is the width how we describe load balancer LoadBalancerWidth = 16 // LabelNodeRolePrefix is a label prefix for node roles // It's copied over to here until it's merged in core: https://github.com/kubernetes/kubernetes/pull/39112 LabelNodeRolePrefix = "node-role.kubernetes.io/" // NodeLabelRole specifies the role of a node NodeLabelRole = "kubernetes.io/role" ) // DescriberFunc gives a way to display the specified RESTMapping type type DescriberFunc func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (ResourceDescriber, error) // ResourceDescriber generates output for the named resource or an error // if the output could not be generated. Implementers typically // abstract the retrieval of the named object from a remote server. type ResourceDescriber interface { Describe(namespace, name string, describerSettings DescriberSettings) (output string, err error) } // DescriberSettings holds display configuration for each object // describer to control what is printed. type DescriberSettings struct { ShowEvents bool ChunkSize int64 } // ObjectDescriber is an interface for displaying arbitrary objects with extra // information. Use when an object is in hand (on disk, or already retrieved). // Implementers may ignore the additional information passed on extra, or use it // by default. ObjectDescribers may return ErrNoDescriber if no suitable describer // is found. type ObjectDescriber interface { DescribeObject(object interface{}, extra ...interface{}) (output string, err error) } // ErrNoDescriber is a structured error indicating the provided object or objects // cannot be described. type ErrNoDescriber struct { Types []string } // Error implements the error interface. func (e ErrNoDescriber) Error() string { return fmt.Sprintf("no describer has been defined for %v", e.Types) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/000077500000000000000000000000001504711711200251415ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/cordon.go000066400000000000000000000066651504711711200267710ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "context" "fmt" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes" ) // CordonHelper wraps functionality to cordon/uncordon nodes type CordonHelper struct { node *corev1.Node desired bool } // NewCordonHelper returns a new CordonHelper func NewCordonHelper(node *corev1.Node) *CordonHelper { return &CordonHelper{ node: node, } } // NewCordonHelperFromRuntimeObject returns a new CordonHelper, or an error if given object is not a // node or cannot be encoded as JSON func NewCordonHelperFromRuntimeObject(nodeObject runtime.Object, scheme *runtime.Scheme, gvk schema.GroupVersionKind) (*CordonHelper, error) { nodeObject, err := scheme.ConvertToVersion(nodeObject, gvk.GroupVersion()) if err != nil { return nil, err } node, ok := nodeObject.(*corev1.Node) if !ok { return nil, fmt.Errorf("unexpected type %T", nodeObject) } return NewCordonHelper(node), nil } // UpdateIfRequired returns true if c.node.Spec.Unschedulable isn't already set, // or false when no change is needed func (c *CordonHelper) UpdateIfRequired(desired bool) bool { c.desired = desired return c.node.Spec.Unschedulable != c.desired } // PatchOrReplace uses given clientset to update the node status, either by patching or // updating the given node object; it may return error if the object cannot be encoded as // JSON, or if either patch or update calls fail; it will also return a second error // whenever creating a patch has failed func (c *CordonHelper) PatchOrReplace(clientset kubernetes.Interface, serverDryRun bool) (error, error) { return c.PatchOrReplaceWithContext(context.TODO(), clientset, serverDryRun) } // PatchOrReplaceWithContext provides the option to pass a custom context while updating // the node status func (c *CordonHelper) PatchOrReplaceWithContext(clientCtx context.Context, clientset kubernetes.Interface, serverDryRun bool) (error, error) { client := clientset.CoreV1().Nodes() oldData, err := json.Marshal(c.node) if err != nil { return err, nil } c.node.Spec.Unschedulable = c.desired newData, err := json.Marshal(c.node) if err != nil { return err, nil } patchBytes, patchErr := strategicpatch.CreateTwoWayMergePatch(oldData, newData, c.node) if patchErr == nil { patchOptions := metav1.PatchOptions{} if serverDryRun { patchOptions.DryRun = []string{metav1.DryRunAll} } _, err = client.Patch(clientCtx, c.node.Name, types.StrategicMergePatchType, patchBytes, patchOptions) } else { updateOptions := metav1.UpdateOptions{} if serverDryRun { updateOptions.DryRun = []string{metav1.DryRunAll} } _, err = client.Update(clientCtx, c.node, updateOptions) } return err, patchErr } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/cordon_test.go000066400000000000000000000067501504711711200300230ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "testing" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) type newCordonHelperFromRuntimeObjectTestCase struct { name string nodeObject runtime.Object expectError bool expected *CordonHelper } func TestNewCordonHelperFromRuntimeObject(t *testing.T) { tests := []newCordonHelperFromRuntimeObjectTestCase{ { name: "valid node object", nodeObject: &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "test-node", }, }, expectError: false, expected: &CordonHelper{ node: &corev1.Node{ TypeMeta: metav1.TypeMeta{ Kind: "Node", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test-node", }, }, }, }, { name: "invalid object type", nodeObject: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", }, }, expectError: true, expected: nil, }, } scheme := runtime.NewScheme() _ = corev1.AddToScheme(scheme) gvk := schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "Node", } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { helper, err := NewCordonHelperFromRuntimeObject(tt.nodeObject, scheme, gvk) if tt.expectError && err == nil { t.Error("Expected error but got none") } if !tt.expectError && err != nil { t.Errorf("Unexpected error: %v", err) } if !tt.expectError && helper == nil { t.Error("Expected non-nil helper") } if tt.expected != nil && helper != nil { if diff := cmp.Diff(tt.expected.node, helper.node); diff != "" { t.Errorf("Node mismatch (-want +got):\n%s", diff) } } }) } } type updateIfRequiredTestCase struct { name string currentState bool desiredState bool expectUpdated bool } func TestUpdateIfRequired(t *testing.T) { tests := []updateIfRequiredTestCase{ { name: "no change required", currentState: true, desiredState: true, expectUpdated: false, }, { name: "update required - cordon", currentState: false, desiredState: true, expectUpdated: true, }, { name: "update required - uncordon", currentState: true, desiredState: false, expectUpdated: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { node := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "test-node", }, Spec: corev1.NodeSpec{ Unschedulable: tt.currentState, }, } helper := NewCordonHelper(node) updated := helper.UpdateIfRequired(tt.desiredState) if updated != tt.expectUpdated { t.Errorf("Expected UpdateIfRequired to return %v, got %v", tt.expectUpdated, updated) } if helper.desired != tt.desiredState { t.Errorf("Expected desired state to be %v, got %v", tt.desiredState, helper.desired) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/default.go000066400000000000000000000046171504711711200271240ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "fmt" corev1 "k8s.io/api/core/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" ) // This file contains default implementations of how to // drain/cordon/uncordon nodes. These functions may be called // directly, or their functionality copied into your own code, for // example if you want different output behaviour. // RunNodeDrain shows the canonical way to drain a node. // You should first cordon the node, e.g. using RunCordonOrUncordon func RunNodeDrain(drainer *Helper, nodeName string) error { // TODO(justinsb): Ensure we have adequate e2e coverage of this function in library consumers list, errs := drainer.GetPodsForDeletion(nodeName) if errs != nil { return utilerrors.NewAggregate(errs) } if warnings := list.Warnings(); warnings != "" { fmt.Fprintf(drainer.ErrOut, "WARNING: %s\n", warnings) } if err := drainer.DeleteOrEvictPods(list.Pods()); err != nil { // Maybe warn about non-deleted pods here return err } return nil } // RunCordonOrUncordon demonstrates the canonical way to cordon or uncordon a Node func RunCordonOrUncordon(drainer *Helper, node *corev1.Node, desired bool) error { if drainer.Ctx == nil { return fmt.Errorf("RunCordonOrUncordon error: drainer.Ctx can't be nil") } if drainer.Client == nil { return fmt.Errorf("RunCordonOrUncordon error: drainer.Client can't be nil") } // TODO(justinsb): Ensure we have adequate e2e coverage of this function in library consumers c := NewCordonHelper(node) if updateRequired := c.UpdateIfRequired(desired); !updateRequired { // Already done return nil } err, patchErr := c.PatchOrReplaceWithContext(drainer.Ctx, drainer.Client, false) if err != nil { if patchErr != nil { return fmt.Errorf("cordon error: %s; merge patch error: %w", err.Error(), patchErr) } return fmt.Errorf("cordon error: %w", err) } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/default_test.go000066400000000000000000000035611504711711200301600ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "context" "fmt" "testing" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/fake" ) func TestRunCordonOrUncordon(t *testing.T) { nilContextError := fmt.Errorf("RunCordonOrUncordon error: drainer.Ctx can't be nil") nilClientError := fmt.Errorf("RunCordonOrUncordon error: drainer.Client can't be nil") tests := []struct { description string drainer *Helper node *corev1.Node desired bool expectedError *error }{ { description: "nil context object", drainer: &Helper{ Client: fake.NewSimpleClientset(), }, desired: true, expectedError: &nilContextError, }, { description: "nil client object", drainer: &Helper{ Ctx: context.TODO(), }, desired: true, expectedError: &nilClientError, }, } for _, test := range tests { test := test t.Run(test.description, func(t *testing.T) { err := RunCordonOrUncordon(test.drainer, test.node, test.desired) if test.expectedError == nil { if err != nil { t.Fatalf("%s: did not expect error, got err=%s", test.description, err.Error()) } } else if err.Error() != (*test.expectedError).Error() { t.Fatalf("%s: the error does not match expected error, got err=%s, expected err=%s", test.description, err, *test.expectedError) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/drain.go000066400000000000000000000375011504711711200265730ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "context" "fmt" "io" "math" "time" corev1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" policyv1beta1 "k8s.io/api/policy/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) const ( // EvictionKind represents the kind of evictions object EvictionKind = "Eviction" // EvictionSubresource represents the kind of evictions object as pod's subresource EvictionSubresource = "pods/eviction" podSkipMsgTemplate = "pod %q has DeletionTimestamp older than %v seconds, skipping\n" ) // Helper contains the parameters to control the behaviour of drainer type Helper struct { Ctx context.Context Client kubernetes.Interface Force bool // GracePeriodSeconds is how long to wait for a pod to terminate. // IMPORTANT: 0 means "delete immediately"; set to a negative value // to use the pod's terminationGracePeriodSeconds. GracePeriodSeconds int IgnoreAllDaemonSets bool Timeout time.Duration DeleteEmptyDirData bool Selector string PodSelector string ChunkSize int64 // DisableEviction forces drain to use delete rather than evict DisableEviction bool // SkipWaitForDeleteTimeoutSeconds ignores pods that have a // DeletionTimeStamp > N seconds. It's up to the user to decide when this // option is appropriate; examples include the Node is unready and the pods // won't drain otherwise SkipWaitForDeleteTimeoutSeconds int // AdditionalFilters are applied sequentially after base drain filters to // exclude pods using custom logic. Any filter that returns PodDeleteStatus // with Delete == false will immediately stop execution of further filters. AdditionalFilters []PodFilter Out io.Writer ErrOut io.Writer DryRunStrategy cmdutil.DryRunStrategy // OnPodDeletedOrEvicted is called when a pod is evicted/deleted; for printing progress output // Deprecated: use OnPodDeletionOrEvictionFinished instead OnPodDeletedOrEvicted func(pod *corev1.Pod, usingEviction bool) // OnPodDeletionOrEvictionFinished is called when a pod is eviction/deletetion is failed; for printing progress output OnPodDeletionOrEvictionFinished func(pod *corev1.Pod, usingEviction bool, err error) // OnPodDeletionOrEvictionStarted is called when a pod eviction/deletion is started; for printing progress output OnPodDeletionOrEvictionStarted func(pod *corev1.Pod, usingEviction bool) } type waitForDeleteParams struct { ctx context.Context pods []corev1.Pod interval time.Duration timeout time.Duration usingEviction bool getPodFn func(string, string) (*corev1.Pod, error) onDoneFn func(pod *corev1.Pod, usingEviction bool) onFinishFn func(pod *corev1.Pod, usingEviction bool, err error) globalTimeout time.Duration skipWaitForDeleteTimeoutSeconds int out io.Writer } // CheckEvictionSupport uses Discovery API to find out if the server support // eviction subresource If support, it will return its groupVersion; Otherwise, // it will return an empty GroupVersion func CheckEvictionSupport(clientset kubernetes.Interface) (schema.GroupVersion, error) { discoveryClient := clientset.Discovery() // version info available in subresources since v1.8.0 in https://github.com/kubernetes/kubernetes/pull/49971 resourceList, err := discoveryClient.ServerResourcesForGroupVersion("v1") if err != nil { return schema.GroupVersion{}, err } for _, resource := range resourceList.APIResources { if resource.Name == EvictionSubresource && resource.Kind == EvictionKind && len(resource.Group) > 0 && len(resource.Version) > 0 { return schema.GroupVersion{Group: resource.Group, Version: resource.Version}, nil } } return schema.GroupVersion{}, nil } func (d *Helper) makeDeleteOptions() metav1.DeleteOptions { deleteOptions := metav1.DeleteOptions{} if d.GracePeriodSeconds >= 0 { gracePeriodSeconds := int64(d.GracePeriodSeconds) deleteOptions.GracePeriodSeconds = &gracePeriodSeconds } if d.DryRunStrategy == cmdutil.DryRunServer { deleteOptions.DryRun = []string{metav1.DryRunAll} } return deleteOptions } // DeletePod will delete the given pod, or return an error if it couldn't func (d *Helper) DeletePod(pod corev1.Pod) error { return d.Client.CoreV1().Pods(pod.Namespace).Delete(d.getContext(), pod.Name, d.makeDeleteOptions()) } // EvictPod will evict the given pod, or return an error if it couldn't func (d *Helper) EvictPod(pod corev1.Pod, evictionGroupVersion schema.GroupVersion) error { delOpts := d.makeDeleteOptions() switch evictionGroupVersion { case policyv1.SchemeGroupVersion: // send policy/v1 if the server supports it eviction := &policyv1.Eviction{ ObjectMeta: metav1.ObjectMeta{ Name: pod.Name, Namespace: pod.Namespace, }, DeleteOptions: &delOpts, } return d.Client.PolicyV1().Evictions(eviction.Namespace).Evict(d.getContext(), eviction) default: // otherwise, fall back to policy/v1beta1, supported by all servers that support the eviction subresource eviction := &policyv1beta1.Eviction{ ObjectMeta: metav1.ObjectMeta{ Name: pod.Name, Namespace: pod.Namespace, }, DeleteOptions: &delOpts, } return d.Client.PolicyV1beta1().Evictions(eviction.Namespace).Evict(d.getContext(), eviction) } } // GetPodsForDeletion receives resource info for a node, and returns those pods as PodDeleteList, // or error if it cannot list pods. All pods that are ready to be deleted can be obtained with .Pods(), // and string with all warning can be obtained with .Warnings(), and .Errors() for all errors that // occurred during deletion. func (d *Helper) GetPodsForDeletion(nodeName string) (*PodDeleteList, []error) { labelSelector, err := labels.Parse(d.PodSelector) if err != nil { return nil, []error{err} } podList := &corev1.PodList{} initialOpts := &metav1.ListOptions{ LabelSelector: labelSelector.String(), FieldSelector: fields.SelectorFromSet(fields.Set{"spec.nodeName": nodeName}).String(), Limit: d.ChunkSize, } err = resource.FollowContinue(initialOpts, func(options metav1.ListOptions) (runtime.Object, error) { newPods, err := d.Client.CoreV1().Pods(metav1.NamespaceAll).List(d.getContext(), options) if err != nil { podR := corev1.SchemeGroupVersion.WithResource(corev1.ResourcePods.String()) return nil, resource.EnhanceListError(err, options, podR.String()) } podList.Items = append(podList.Items, newPods.Items...) return newPods, nil }) if err != nil { return nil, []error{err} } list := filterPods(podList, d.makeFilters()) if errs := list.errors(); len(errs) > 0 { return list, errs } return list, nil } func filterPods(podList *corev1.PodList, filters []PodFilter) *PodDeleteList { pods := []PodDelete{} for _, pod := range podList.Items { var status PodDeleteStatus for _, filter := range filters { status = filter(pod) if !status.Delete { // short-circuit as soon as pod is filtered out // at that point, there is no reason to run pod // through any additional filters break } } // Add the pod to PodDeleteList no matter what PodDeleteStatus is, // those pods whose PodDeleteStatus is false like DaemonSet will // be catched by list.errors() pod.Kind = "Pod" pod.APIVersion = "v1" pods = append(pods, PodDelete{ Pod: pod, Status: status, }) } list := &PodDeleteList{items: pods} return list } // DeleteOrEvictPods deletes or evicts the pods on the api server func (d *Helper) DeleteOrEvictPods(pods []corev1.Pod) error { if len(pods) == 0 { return nil } // TODO(justinsb): unnecessary? getPodFn := func(namespace, name string) (*corev1.Pod, error) { return d.Client.CoreV1().Pods(namespace).Get(d.getContext(), name, metav1.GetOptions{}) } if !d.DisableEviction { evictionGroupVersion, err := CheckEvictionSupport(d.Client) if err != nil { return err } if !evictionGroupVersion.Empty() { return d.evictPods(pods, evictionGroupVersion, getPodFn) } } return d.deletePods(pods, getPodFn) } func (d *Helper) evictPods(pods []corev1.Pod, evictionGroupVersion schema.GroupVersion, getPodFn func(namespace, name string) (*corev1.Pod, error)) error { returnCh := make(chan error, 1) // 0 timeout means infinite, we use MaxInt64 to represent it. var globalTimeout time.Duration if d.Timeout == 0 { globalTimeout = time.Duration(math.MaxInt64) } else { globalTimeout = d.Timeout } ctx, cancel := context.WithTimeout(d.getContext(), globalTimeout) defer cancel() for _, pod := range pods { go func(pod corev1.Pod, returnCh chan error) { refreshPod := false for { switch d.DryRunStrategy { case cmdutil.DryRunServer: fmt.Fprintf(d.Out, "evicting pod %s/%s (server dry run)\n", pod.Namespace, pod.Name) default: if d.OnPodDeletionOrEvictionStarted != nil { d.OnPodDeletionOrEvictionStarted(&pod, true) } fmt.Fprintf(d.Out, "evicting pod %s/%s\n", pod.Namespace, pod.Name) } select { case <-ctx.Done(): // return here or we'll leak a goroutine. returnCh <- fmt.Errorf("error when evicting pods/%q -n %q: global timeout reached: %v", pod.Name, pod.Namespace, globalTimeout) return default: } // Create a temporary pod so we don't mutate the pod in the loop. activePod := pod if refreshPod { freshPod, err := getPodFn(pod.Namespace, pod.Name) // We ignore errors and let eviction sort it out with // the original pod. if err == nil { activePod = *freshPod } refreshPod = false } err := d.EvictPod(activePod, evictionGroupVersion) if err == nil { break } else if apierrors.IsNotFound(err) { returnCh <- nil return } else if apierrors.IsTooManyRequests(err) { fmt.Fprintf(d.ErrOut, "error when evicting pods/%q -n %q (will retry after 5s): %v\n", activePod.Name, activePod.Namespace, err) time.Sleep(5 * time.Second) } else if !activePod.ObjectMeta.DeletionTimestamp.IsZero() && apierrors.IsForbidden(err) && apierrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) { // an eviction request in a deleting namespace will throw a forbidden error, // if the pod is already marked deleted, we can ignore this error, an eviction // request will never succeed, but we will waitForDelete for this pod. break } else if apierrors.IsForbidden(err) && apierrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) { // an eviction request in a deleting namespace will throw a forbidden error, // if the pod is not marked deleted, we retry until it is. fmt.Fprintf(d.ErrOut, "error when evicting pod %q from terminating namespace %q (will retry after 5s): %v\n", activePod.Name, activePod.Namespace, err) time.Sleep(5 * time.Second) } else { returnCh <- fmt.Errorf("error when evicting pods/%q -n %q: %v", activePod.Name, activePod.Namespace, err) return } } if d.DryRunStrategy == cmdutil.DryRunServer { returnCh <- nil return } params := waitForDeleteParams{ ctx: ctx, pods: []corev1.Pod{pod}, interval: 1 * time.Second, timeout: time.Duration(math.MaxInt64), usingEviction: true, getPodFn: getPodFn, onDoneFn: d.OnPodDeletedOrEvicted, onFinishFn: d.OnPodDeletionOrEvictionFinished, globalTimeout: globalTimeout, skipWaitForDeleteTimeoutSeconds: d.SkipWaitForDeleteTimeoutSeconds, out: d.Out, } _, err := waitForDelete(params) if err == nil { returnCh <- nil } else { returnCh <- fmt.Errorf("error when waiting for pod %q in namespace %q to terminate: %v", pod.Name, pod.Namespace, err) } }(pod, returnCh) } doneCount := 0 var errors []error numPods := len(pods) for doneCount < numPods { select { case err := <-returnCh: doneCount++ if err != nil { errors = append(errors, err) } } } return utilerrors.NewAggregate(errors) } func (d *Helper) deletePods(pods []corev1.Pod, getPodFn func(namespace, name string) (*corev1.Pod, error)) error { // 0 timeout means infinite, we use MaxInt64 to represent it. var globalTimeout time.Duration if d.Timeout == 0 { globalTimeout = time.Duration(math.MaxInt64) } else { globalTimeout = d.Timeout } for _, pod := range pods { err := d.DeletePod(pod) if err != nil && !apierrors.IsNotFound(err) { return err } if d.OnPodDeletionOrEvictionStarted != nil { d.OnPodDeletionOrEvictionStarted(&pod, false) } } ctx := d.getContext() params := waitForDeleteParams{ ctx: ctx, pods: pods, interval: 1 * time.Second, timeout: globalTimeout, usingEviction: false, getPodFn: getPodFn, onDoneFn: d.OnPodDeletedOrEvicted, onFinishFn: d.OnPodDeletionOrEvictionFinished, globalTimeout: globalTimeout, skipWaitForDeleteTimeoutSeconds: d.SkipWaitForDeleteTimeoutSeconds, out: d.Out, } _, err := waitForDelete(params) return err } func waitForDelete(params waitForDeleteParams) ([]corev1.Pod, error) { pods := params.pods if params.ctx == nil { params.ctx = context.Background() } err := wait.PollUntilContextTimeout(params.ctx, params.interval, params.timeout, true, func(ctx context.Context) (done bool, err error) { pendingPods := []corev1.Pod{} for i, pod := range pods { p, err := params.getPodFn(pod.Namespace, pod.Name) // The implementation of getPodFn that uses client-go returns an empty Pod struct when there is an error, // so we need to check that err == nil and p != nil to know that a pod was found successfully. if apierrors.IsNotFound(err) || (err == nil && p != nil && p.ObjectMeta.UID != pod.ObjectMeta.UID) { if params.onFinishFn != nil { params.onFinishFn(&pod, params.usingEviction, nil) } else if params.onDoneFn != nil { params.onDoneFn(&pod, params.usingEviction) } continue } else if err != nil { if params.onFinishFn != nil { params.onFinishFn(&pod, params.usingEviction, err) } return false, err } else { if shouldSkipPod(*p, params.skipWaitForDeleteTimeoutSeconds) { fmt.Fprintf(params.out, podSkipMsgTemplate, pod.Name, params.skipWaitForDeleteTimeoutSeconds) continue } pendingPods = append(pendingPods, pods[i]) } } pods = pendingPods return len(pods) == 0, nil }) return pods, err } // Since Helper does not have a constructor, we can't enforce Helper.Ctx != nil // Multiple public methods prevent us from initializing the context in a single // place as well. func (d *Helper) getContext() context.Context { if d.Ctx != nil { return d.Ctx } return context.Background() } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/drain_test.go000066400000000000000000000376561504711711200276450ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "context" "errors" "fmt" "math" "os" "reflect" "sort" "strconv" "testing" "time" corev1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" policyv1beta1 "k8s.io/api/policy/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/fake" ktest "k8s.io/client-go/testing" ) func TestDeletePods(t *testing.T) { ifHasBeenCalled := map[string]bool{} tests := []struct { description string interval time.Duration timeout time.Duration ctxTimeoutEarly bool expectPendingPods bool expectError bool expectedError *error getPodFn func(namespace, name string) (*corev1.Pod, error) }{ { description: "Wait for deleting to complete", interval: 100 * time.Millisecond, timeout: 10 * time.Second, expectPendingPods: false, expectError: false, expectedError: nil, getPodFn: func(namespace, name string) (*corev1.Pod, error) { oldPodMap, _ := createPods(false) newPodMap, _ := createPods(true) if oldPod, found := oldPodMap[name]; found { if _, ok := ifHasBeenCalled[name]; !ok { ifHasBeenCalled[name] = true return &oldPod, nil } if oldPod.ObjectMeta.Generation < 4 { newPod := newPodMap[name] return &newPod, nil } return &corev1.Pod{}, apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, name) } return &corev1.Pod{}, apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, name) }, }, { description: "Pod found with same name but different UID", interval: 100 * time.Millisecond, timeout: 10 * time.Second, expectPendingPods: false, expectError: false, expectedError: nil, getPodFn: func(namespace, name string) (*corev1.Pod, error) { // Return a pod with the same name, but different UID return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, UID: "SOME_OTHER_UID", }, }, nil }, }, { description: "Deleting could timeout", interval: 200 * time.Millisecond, timeout: 3 * time.Second, expectPendingPods: true, expectError: true, expectedError: &context.DeadlineExceeded, getPodFn: func(namespace, name string) (*corev1.Pod, error) { oldPodMap, _ := createPods(false) if oldPod, found := oldPodMap[name]; found { return &oldPod, nil } return &corev1.Pod{}, fmt.Errorf("%q: not found", name) }, }, { description: "Context Canceled", interval: 1000 * time.Millisecond, timeout: 5 * time.Second, ctxTimeoutEarly: true, expectPendingPods: true, expectError: true, expectedError: &context.Canceled, getPodFn: func(namespace, name string) (*corev1.Pod, error) { oldPodMap, _ := createPods(false) if oldPod, found := oldPodMap[name]; found { return &oldPod, nil } return &corev1.Pod{}, fmt.Errorf("%q: not found", name) }, }, { description: "Skip Deleted Pod", interval: 200 * time.Millisecond, timeout: 3 * time.Second, expectPendingPods: false, expectError: false, expectedError: nil, getPodFn: func(namespace, name string) (*corev1.Pod, error) { oldPodMap, _ := createPods(false) if oldPod, found := oldPodMap[name]; found { dTime := &metav1.Time{Time: time.Now().Add(time.Duration(100) * time.Second * -1)} oldPod.ObjectMeta.SetDeletionTimestamp(dTime) return &oldPod, nil } return &corev1.Pod{}, fmt.Errorf("%q: not found", name) }, }, { description: "Client error could be passed out", interval: 200 * time.Millisecond, timeout: 5 * time.Second, expectPendingPods: true, expectError: true, expectedError: nil, getPodFn: func(namespace, name string) (*corev1.Pod, error) { return &corev1.Pod{}, errors.New("This is a random error for testing") }, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { _, pods := createPods(false) var ctx context.Context var cancel context.CancelFunc ctx = context.Background() if test.ctxTimeoutEarly { ctx, cancel = context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() } params := waitForDeleteParams{ ctx: ctx, pods: pods, interval: test.interval, timeout: test.timeout, usingEviction: false, getPodFn: test.getPodFn, onDoneFn: nil, globalTimeout: time.Duration(math.MaxInt64), out: os.Stdout, skipWaitForDeleteTimeoutSeconds: 10, } start := time.Now() pendingPods, err := waitForDelete(params) elapsed := time.Since(start) if test.expectError { if err == nil { t.Fatalf("%s: unexpected non-error", test.description) } else if test.expectedError != nil { if test.ctxTimeoutEarly { if elapsed >= test.timeout { t.Fatalf("%s: the supplied context did not effectively cancel the waitForDelete", test.description) } } else if *test.expectedError != err { t.Fatalf("%s: the error does not match expected error", test.description) } } } if !test.expectError && err != nil { t.Fatalf("%s: unexpected error", test.description) } if test.expectPendingPods && len(pendingPods) == 0 { t.Fatalf("%s: unexpected empty pods", test.description) } if !test.expectPendingPods && len(pendingPods) > 0 { t.Fatalf("%s: unexpected pending pods", test.description) } }) } } func createPods(ifCreateNewPods bool) (map[string]corev1.Pod, []corev1.Pod) { podMap := make(map[string]corev1.Pod) podSlice := []corev1.Pod{} for i := 0; i < 8; i++ { var uid types.UID if ifCreateNewPods { uid = types.UID(strconv.Itoa(i)) } else { uid = types.UID(strconv.Itoa(i) + strconv.Itoa(i)) } pod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod" + strconv.Itoa(i), Namespace: "default", UID: uid, Generation: int64(i), }, } podMap[pod.Name] = pod podSlice = append(podSlice, pod) } return podMap, podSlice } func addCoreNonEvictionSupport(t *testing.T, k *fake.Clientset) { coreResources := &metav1.APIResourceList{ GroupVersion: "v1", } k.Resources = append(k.Resources, coreResources) } // addEvictionSupport implements simple fake eviction support on the fake.Clientset func addEvictionSupport(t *testing.T, k *fake.Clientset, version string) { podsEviction := metav1.APIResource{ Name: "pods/eviction", Kind: "Eviction", Group: "policy", Version: version, } coreResources := &metav1.APIResourceList{ GroupVersion: "v1", APIResources: []metav1.APIResource{podsEviction}, } policyResources := &metav1.APIResourceList{ GroupVersion: "policy/v1", } k.Resources = append(k.Resources, coreResources, policyResources) // Delete pods when evict is called k.PrependReactor("create", "pods", func(action ktest.Action) (bool, runtime.Object, error) { if action.GetSubresource() != "eviction" { return false, nil, nil } namespace := "" name := "" switch version { case "v1": eviction := *action.(ktest.CreateAction).GetObject().(*policyv1.Eviction) namespace = eviction.Namespace name = eviction.Name case "v1beta1": eviction := *action.(ktest.CreateAction).GetObject().(*policyv1beta1.Eviction) namespace = eviction.Namespace name = eviction.Name default: t.Errorf("unknown version %s", version) } // Avoid the lock go func() { err := k.CoreV1().Pods(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) if err != nil { // Errorf because we can't call Fatalf from another goroutine t.Errorf("failed to delete pod: %s/%s", namespace, name) } }() return true, nil, nil }) } func TestCheckEvictionSupport(t *testing.T) { for _, evictionVersion := range []string{"", "v1", "v1beta1"} { t.Run(fmt.Sprintf("evictionVersion=%v", evictionVersion), func(t *testing.T) { k := fake.NewSimpleClientset() if len(evictionVersion) > 0 { addEvictionSupport(t, k, evictionVersion) } else { addCoreNonEvictionSupport(t, k) } apiGroup, err := CheckEvictionSupport(k) if err != nil { t.Fatalf("unexpected error: %v", err) } expectedAPIGroup := schema.GroupVersion{} if len(evictionVersion) > 0 { expectedAPIGroup = schema.GroupVersion{Group: "policy", Version: evictionVersion} } if apiGroup != expectedAPIGroup { t.Fatalf("expected apigroup %q, actual=%q", expectedAPIGroup, apiGroup) } }) } } func TestDeleteOrEvict(t *testing.T) { tests := []struct { description string evictionSupported bool disableEviction bool }{ { description: "eviction supported/enabled", evictionSupported: true, disableEviction: false, }, { description: "eviction unsupported/disabled", evictionSupported: false, disableEviction: false, }, { description: "eviction supported/disabled", evictionSupported: true, disableEviction: true, }, { description: "eviction unsupported/disabled", evictionSupported: false, disableEviction: false, }, } for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { h := &Helper{ Out: os.Stdout, GracePeriodSeconds: 10, OnPodDeletionOrEvictionStarted: func(pod *corev1.Pod, usingEviction bool) { if tc.evictionSupported && !tc.disableEviction { if !usingEviction { t.Errorf("%s: OnPodDeletionOrEvictionStarted callback failed while evicting; actual\n\t%v\nexpected\n\t%v", tc.description, usingEviction, !usingEviction) } } else if tc.evictionSupported && tc.disableEviction { if usingEviction { t.Errorf("%s: OnPodDeletionOrEvictionStarted callback failed while deleting; actual\n\t%v\nexpected\n\t%v", tc.description, !usingEviction, usingEviction) } } }, OnPodDeletedOrEvicted: func(pod *corev1.Pod, usingEviction bool) { if tc.evictionSupported && !tc.disableEviction { if !usingEviction { t.Errorf("%s: OnPodDeletedOrEvicted callback failed while evicting; actual\n\t%v\nexpected\n\t%v", tc.description, usingEviction, !usingEviction) } } else if tc.evictionSupported && tc.disableEviction { if usingEviction { t.Errorf("%s: OnPodDeletedOrEvicted callback failed while deleting; actual\n\t%v\nexpected\n\t%v", tc.description, !usingEviction, usingEviction) } } }, OnPodDeletionOrEvictionFinished: func(pod *corev1.Pod, usingEviction bool, err error) { if tc.evictionSupported && !tc.disableEviction { if !usingEviction { t.Errorf("%s: OnPodDeletionOrEvictionFinished callback failed while evicting; actual\n\t%v\nexpected\n\t%v", tc.description, usingEviction, !usingEviction) } } else if tc.evictionSupported && tc.disableEviction { if usingEviction { t.Errorf("%s: OnPodDeletionOrEvictionFinished callback failed while deleting; actual\n\t%v\nexpected\n\t%v", tc.description, !usingEviction, usingEviction) } } }, } // Create 4 pods, and try to remove the first 2 var expectedEvictions []policyv1.Eviction var create []runtime.Object deletePods := []corev1.Pod{} for i := 1; i <= 4; i++ { pod := &corev1.Pod{} pod.Name = fmt.Sprintf("mypod-%d", i) pod.Namespace = "default" create = append(create, pod) if i <= 2 { deletePods = append(deletePods, *pod) if tc.evictionSupported && !tc.disableEviction { eviction := policyv1.Eviction{} eviction.Namespace = pod.Namespace eviction.Name = pod.Name gracePeriodSeconds := int64(h.GracePeriodSeconds) eviction.DeleteOptions = &metav1.DeleteOptions{ GracePeriodSeconds: &gracePeriodSeconds, } expectedEvictions = append(expectedEvictions, eviction) } } } // Build the fake client k := fake.NewSimpleClientset(create...) if tc.evictionSupported { addEvictionSupport(t, k, "v1") } else { addCoreNonEvictionSupport(t, k) } h.Client = k h.DisableEviction = tc.disableEviction // Do the eviction if err := h.DeleteOrEvictPods(deletePods); err != nil { t.Fatalf("error from DeleteOrEvictPods: %v", err) } // Test that other pods are still there var remainingPods []string { podList, err := k.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{}) if err != nil { t.Fatalf("error listing pods: %v", err) } for _, pod := range podList.Items { remainingPods = append(remainingPods, pod.Namespace+"/"+pod.Name) } sort.Strings(remainingPods) } expected := []string{"default/mypod-3", "default/mypod-4"} if !reflect.DeepEqual(remainingPods, expected) { t.Errorf("%s: unexpected remaining pods after DeleteOrEvictPods; actual %v; expected %v", tc.description, remainingPods, expected) } // Test that pods were evicted as expected var actualEvictions []policyv1.Eviction for _, action := range k.Actions() { if action.GetVerb() != "create" || action.GetResource().Resource != "pods" || action.GetSubresource() != "eviction" { continue } eviction := *action.(ktest.CreateAction).GetObject().(*policyv1.Eviction) actualEvictions = append(actualEvictions, eviction) } sort.Slice(actualEvictions, func(i, j int) bool { return actualEvictions[i].Name < actualEvictions[j].Name }) if !reflect.DeepEqual(actualEvictions, expectedEvictions) { t.Errorf("%s: unexpected evictions; actual\n\t%v\nexpected\n\t%v", tc.description, actualEvictions, expectedEvictions) } }) } } func mockFilterSkip(_ corev1.Pod) PodDeleteStatus { return MakePodDeleteStatusSkip() } func mockFilterOkay(_ corev1.Pod) PodDeleteStatus { return MakePodDeleteStatusOkay() } func TestFilterPods(t *testing.T) { tCases := []struct { description string expectedPodListLen int additionalFilters []PodFilter }{ { description: "AdditionalFilter skip all", expectedPodListLen: 0, additionalFilters: []PodFilter{ mockFilterSkip, mockFilterOkay, }, }, { description: "AdditionalFilter okay all", expectedPodListLen: 1, additionalFilters: []PodFilter{ mockFilterOkay, }, }, { description: "AdditionalFilter Skip after Okay all skip", expectedPodListLen: 0, additionalFilters: []PodFilter{ mockFilterOkay, mockFilterSkip, }, }, { description: "No additionalFilters okay all", expectedPodListLen: 1, }, } for _, tc := range tCases { t.Run(tc.description, func(t *testing.T) { h := &Helper{ Force: true, AdditionalFilters: tc.additionalFilters, } pod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod", Namespace: "default", }, } podList := corev1.PodList{ Items: []corev1.Pod{ pod, }, } list := filterPods(&podList, h.makeFilters()) podsLen := len(list.Pods()) if podsLen != tc.expectedPodListLen { t.Errorf("%s: unexpected evictions; actual %v; expected %v", tc.description, podsLen, tc.expectedPodListLen) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/filter_test.go000066400000000000000000000036151504711711200300210ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestSkipDeletedFilter(t *testing.T) { tCases := []struct { timeStampAgeSeconds int skipWaitForDeleteTimeoutSeconds int expectedDelete bool }{ { timeStampAgeSeconds: 0, skipWaitForDeleteTimeoutSeconds: 20, expectedDelete: true, }, { timeStampAgeSeconds: 1, skipWaitForDeleteTimeoutSeconds: 20, expectedDelete: true, }, { timeStampAgeSeconds: 100, skipWaitForDeleteTimeoutSeconds: 20, expectedDelete: false, }, } for i, tc := range tCases { h := &Helper{ SkipWaitForDeleteTimeoutSeconds: tc.skipWaitForDeleteTimeoutSeconds, } pod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod", Namespace: "default", }, } if tc.timeStampAgeSeconds > 0 { dTime := &metav1.Time{Time: time.Now().Add(time.Duration(tc.timeStampAgeSeconds) * time.Second * -1)} pod.ObjectMeta.SetDeletionTimestamp(dTime) } podDeleteStatus := h.skipDeletedFilter(pod) if podDeleteStatus.Delete != tc.expectedDelete { t.Errorf("test %v: unexpected podDeleteStatus.delete; actual %v; expected %v", i, podDeleteStatus.Delete, tc.expectedDelete) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/drain/filters.go000066400000000000000000000173151504711711200271470ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package drain import ( "context" "fmt" "strings" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( daemonSetFatal = "DaemonSet-managed Pods (use --ignore-daemonsets to ignore)" daemonSetWarning = "ignoring DaemonSet-managed Pods" localStorageFatal = "Pods with local storage (use --delete-emptydir-data to override)" localStorageWarning = "deleting Pods with local storage" unmanagedFatal = "cannot delete Pods that declare no controller (use --force to override)" unmanagedWarning = "deleting Pods that declare no controller" ) // PodDelete informs filtering logic whether a pod should be deleted or not type PodDelete struct { Pod corev1.Pod Status PodDeleteStatus } // PodDeleteList is a wrapper around []PodDelete type PodDeleteList struct { items []PodDelete } // Pods returns a list of all pods marked for deletion after filtering. func (l *PodDeleteList) Pods() []corev1.Pod { pods := []corev1.Pod{} for _, i := range l.items { if i.Status.Delete { pods = append(pods, i.Pod) } } return pods } // Warnings returns all warning messages concatenated into a string. func (l *PodDeleteList) Warnings() string { ps := make(map[string][]string) for _, i := range l.items { if i.Status.Reason == PodDeleteStatusTypeWarning { ps[i.Status.Message] = append(ps[i.Status.Message], fmt.Sprintf("%s/%s", i.Pod.Namespace, i.Pod.Name)) } } msgs := []string{} for key, pods := range ps { msgs = append(msgs, fmt.Sprintf("%s: %s", key, strings.Join(pods, ", "))) } return strings.Join(msgs, "; ") } func (l *PodDeleteList) errors() []error { failedPods := make(map[string][]string) for _, i := range l.items { if i.Status.Reason == PodDeleteStatusTypeError { msg := i.Status.Message if msg == "" { msg = "unexpected error" } failedPods[msg] = append(failedPods[msg], fmt.Sprintf("%s/%s", i.Pod.Namespace, i.Pod.Name)) } } errs := make([]error, 0, len(failedPods)) for msg, pods := range failedPods { errs = append(errs, fmt.Errorf("cannot delete %s: %s", msg, strings.Join(pods, ", "))) } return errs } // PodDeleteStatus informs filters if a pod should be deleted type PodDeleteStatus struct { Delete bool Reason string Message string } // PodFilter takes a pod and returns a PodDeleteStatus type PodFilter func(corev1.Pod) PodDeleteStatus const ( // PodDeleteStatusTypeOkay is "Okay" PodDeleteStatusTypeOkay = "Okay" // PodDeleteStatusTypeSkip is "Skip" PodDeleteStatusTypeSkip = "Skip" // PodDeleteStatusTypeWarning is "Warning" PodDeleteStatusTypeWarning = "Warning" // PodDeleteStatusTypeError is "Error" PodDeleteStatusTypeError = "Error" ) // MakePodDeleteStatusOkay is a helper method to return the corresponding PodDeleteStatus func MakePodDeleteStatusOkay() PodDeleteStatus { return PodDeleteStatus{ Delete: true, Reason: PodDeleteStatusTypeOkay, } } // MakePodDeleteStatusSkip is a helper method to return the corresponding PodDeleteStatus func MakePodDeleteStatusSkip() PodDeleteStatus { return PodDeleteStatus{ Delete: false, Reason: PodDeleteStatusTypeSkip, } } // MakePodDeleteStatusWithWarning is a helper method to return the corresponding PodDeleteStatus func MakePodDeleteStatusWithWarning(delete bool, message string) PodDeleteStatus { return PodDeleteStatus{ Delete: delete, Reason: PodDeleteStatusTypeWarning, Message: message, } } // MakePodDeleteStatusWithError is a helper method to return the corresponding PodDeleteStatus func MakePodDeleteStatusWithError(message string) PodDeleteStatus { return PodDeleteStatus{ Delete: false, Reason: PodDeleteStatusTypeError, Message: message, } } // The filters are applied in a specific order, only the last filter's // message will be retained if there are any warnings. func (d *Helper) makeFilters() []PodFilter { baseFilters := []PodFilter{ d.skipDeletedFilter, d.daemonSetFilter, d.mirrorPodFilter, d.localStorageFilter, d.unreplicatedFilter, } return append(baseFilters, d.AdditionalFilters...) } func hasLocalStorage(pod corev1.Pod) bool { for _, volume := range pod.Spec.Volumes { if volume.EmptyDir != nil { return true } } return false } func (d *Helper) daemonSetFilter(pod corev1.Pod) PodDeleteStatus { // Note that we return false in cases where the pod is DaemonSet managed, // regardless of flags. // // The exception is for pods that are orphaned (the referencing // management resource - including DaemonSet - is not found). // Such pods will be deleted if --force is used. controllerRef := metav1.GetControllerOf(&pod) if controllerRef == nil || controllerRef.Kind != appsv1.SchemeGroupVersion.WithKind("DaemonSet").Kind { return MakePodDeleteStatusOkay() } // Any finished pod can be removed. if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { return MakePodDeleteStatusOkay() } if _, err := d.Client.AppsV1().DaemonSets(pod.Namespace).Get(context.TODO(), controllerRef.Name, metav1.GetOptions{}); err != nil { // remove orphaned pods with a warning if --force is used if apierrors.IsNotFound(err) && d.Force { return MakePodDeleteStatusWithWarning(true, err.Error()) } return MakePodDeleteStatusWithError(err.Error()) } if !d.IgnoreAllDaemonSets { return MakePodDeleteStatusWithError(daemonSetFatal) } return MakePodDeleteStatusWithWarning(false, daemonSetWarning) } func (d *Helper) mirrorPodFilter(pod corev1.Pod) PodDeleteStatus { if _, found := pod.ObjectMeta.Annotations[corev1.MirrorPodAnnotationKey]; found { return MakePodDeleteStatusSkip() } return MakePodDeleteStatusOkay() } func (d *Helper) localStorageFilter(pod corev1.Pod) PodDeleteStatus { if !hasLocalStorage(pod) { return MakePodDeleteStatusOkay() } // Any finished pod can be removed. if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { return MakePodDeleteStatusOkay() } if !d.DeleteEmptyDirData { return MakePodDeleteStatusWithError(localStorageFatal) } // TODO: this warning gets dropped by subsequent filters; // consider accounting for multiple warning conditions or at least // preserving the last warning message. return MakePodDeleteStatusWithWarning(true, localStorageWarning) } func (d *Helper) unreplicatedFilter(pod corev1.Pod) PodDeleteStatus { // any finished pod can be removed if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { return MakePodDeleteStatusOkay() } controllerRef := metav1.GetControllerOf(&pod) if controllerRef != nil { return MakePodDeleteStatusOkay() } if d.Force { return MakePodDeleteStatusWithWarning(true, unmanagedWarning) } return MakePodDeleteStatusWithError(unmanagedFatal) } func shouldSkipPod(pod corev1.Pod, skipDeletedTimeoutSeconds int) bool { return skipDeletedTimeoutSeconds > 0 && !pod.ObjectMeta.DeletionTimestamp.IsZero() && int(time.Now().Sub(pod.ObjectMeta.GetDeletionTimestamp().Time).Seconds()) > skipDeletedTimeoutSeconds } func (d *Helper) skipDeletedFilter(pod corev1.Pod) PodDeleteStatus { if shouldSkipPod(pod, d.SkipWaitForDeleteTimeoutSeconds) { return MakePodDeleteStatusSkip() } return MakePodDeleteStatusOkay() } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/000077500000000000000000000000001504711711200255045ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/OWNERS000066400000000000000000000001431504711711200264420ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - apelisse reviewers: - apelisse kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/explain.go000066400000000000000000000133241504711711200274760ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "fmt" "io" "strings" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/util/jsonpath" "k8s.io/kube-openapi/pkg/util/proto" ) type fieldsPrinter interface { PrintFields(proto.Schema) error } // jsonPathParse gets back the inner list of nodes we want to work with func jsonPathParse(in string) ([]jsonpath.Node, error) { // Remove trailing period just in case in = strings.TrimSuffix(in, ".") // Define initial jsonpath Parser jpp, err := jsonpath.Parse("user", "{."+in+"}") if err != nil { return nil, err } // Because of the way the jsonpath library works, the schema of the parser is [][]NodeList // meaning we need to get the outer node list, make sure it's only length 1, then get the inner node // list, and only then can we look at the individual nodes themselves. outerNodeList := jpp.Root.Nodes if len(outerNodeList) != 1 { return nil, fmt.Errorf("must pass in 1 jsonpath string") } // The root node is always a list node so this type assertion is safe return outerNodeList[0].(*jsonpath.ListNode).Nodes, nil } // SplitAndParseResourceRequest separates the users input into a model and fields func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (schema.GroupVersionResource, []string, error) { inResourceNodeList, err := jsonPathParse(inResource) if err != nil { return schema.GroupVersionResource{}, nil, err } if inResourceNodeList[0].Type() != jsonpath.NodeField { return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node") } resource := inResourceNodeList[0].(*jsonpath.FieldNode).Value gvr, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: resource}) if err != nil { return schema.GroupVersionResource{}, nil, err } var fieldsPath []string for _, node := range inResourceNodeList[1:] { if node.Type() != jsonpath.NodeField { return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, all nodes must be field nodes") } fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value) } return gvr, fieldsPath, nil } // SplitAndParseResourceRequestWithMatchingPrefix separates the users input into a model and fields // while selecting gvr whose (resource, group) prefix matches the resource func SplitAndParseResourceRequestWithMatchingPrefix(inResource string, mapper meta.RESTMapper) (gvr schema.GroupVersionResource, fieldsPath []string, err error) { inResourceNodeList, err := jsonPathParse(inResource) if err != nil { return schema.GroupVersionResource{}, nil, err } // Get resource from first node of jsonpath if inResourceNodeList[0].Type() != jsonpath.NodeField { return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node") } resource := inResourceNodeList[0].(*jsonpath.FieldNode).Value gvrs, err := mapper.ResourcesFor(schema.GroupVersionResource{Resource: resource}) if err != nil { return schema.GroupVersionResource{}, nil, err } for _, gvrItem := range gvrs { // Find first gvr whose gr prefixes requested resource groupResource := gvrItem.GroupResource().String() if strings.HasPrefix(inResource, groupResource) { resourceSuffix := inResource[len(groupResource):] var fieldsPath []string if len(resourceSuffix) > 0 { // Define another jsonpath Parser for the resource suffix resourceSuffixNodeList, err := jsonPathParse(resourceSuffix) if err != nil { return schema.GroupVersionResource{}, nil, err } if len(resourceSuffixNodeList) > 0 { nodeList := resourceSuffixNodeList[1:] for _, node := range nodeList { if node.Type() != jsonpath.NodeField { return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node") } fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value) } } } return gvrItem, fieldsPath, nil } } // If no match, take the first (the highest priority) gvr fieldsPath = []string{} if len(gvrs) > 0 { gvr = gvrs[0] fieldsPathNodeList, err := jsonPathParse(inResource) if err != nil { return schema.GroupVersionResource{}, nil, err } for _, node := range fieldsPathNodeList[1:] { if node.Type() != jsonpath.NodeField { return schema.GroupVersionResource{}, nil, fmt.Errorf("invalid jsonpath syntax, first node must be field node") } fieldsPath = append(fieldsPath, node.(*jsonpath.FieldNode).Value) } } return gvr, fieldsPath, nil } // PrintModelDescription prints the description of a specific model or dot path. // If recursive, all components nested within the fields of the schema will be // printed. func PrintModelDescription(fieldsPath []string, w io.Writer, schema proto.Schema, gvk schema.GroupVersionKind, recursive bool) error { fieldName := "" if len(fieldsPath) != 0 { fieldName = fieldsPath[len(fieldsPath)-1] } // Go down the fieldsPath to find what we're trying to explain schema, err := LookupSchemaForField(schema, fieldsPath) if err != nil { return err } b := fieldsPrinterBuilder{Recursive: recursive} f := &Formatter{Writer: w, Wrap: 80} return PrintModel(fieldName, f, b, schema, gvk) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/explain_test.go000066400000000000000000000120141504711711200305300ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "reflect" "testing" "k8s.io/apimachinery/pkg/api/meta/testrestmapper" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/kubectl/pkg/scheme" ) func TestSplitAndParseResourceRequest(t *testing.T) { tests := []struct { name string inResource string expectedGVR schema.GroupVersionResource expectedFieldsPath []string expectedErr bool }{ { name: "no trailing period", inResource: "pods.field2.field3", expectedGVR: schema.GroupVersionResource{Resource: "pods", Version: "v1"}, expectedFieldsPath: []string{"field2", "field3"}, }, { name: "trailing period with correct fieldsPath", inResource: "service.field2.field3.", expectedGVR: schema.GroupVersionResource{Resource: "services", Version: "v1"}, expectedFieldsPath: []string{"field2", "field3"}, }, { name: "field with dots 1", inResource: `service.field2['field\.with\.dots']`, expectedGVR: schema.GroupVersionResource{Resource: "services", Version: "v1"}, expectedFieldsPath: []string{"field2", "field.with.dots"}, }, { name: "field with dots 2", inResource: `service.field2.field\.with\.dots`, expectedGVR: schema.GroupVersionResource{Resource: "services", Version: "v1"}, expectedFieldsPath: []string{"field2", "field.with.dots"}, }, { name: "trailing period with incorrect fieldsPath", inResource: "node.field2.field3.", expectedGVR: schema.GroupVersionResource{Resource: "nodes", Version: "v1"}, expectedFieldsPath: []string{"field2", "field3", ""}, expectedErr: true, }, } mapper := testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotGVR, gotFieldsPath, err := SplitAndParseResourceRequest(tt.inResource, mapper) if err != nil { t.Errorf("unexpected error: %v", err) } if !reflect.DeepEqual(tt.expectedGVR, gotGVR) && !tt.expectedErr { t.Errorf("%s: expected inResource: %s, got: %s", tt.name, tt.expectedGVR, gotGVR) } if !reflect.DeepEqual(tt.expectedFieldsPath, gotFieldsPath) && !tt.expectedErr { t.Errorf("%s: expected fieldsPath: %s, got: %s", tt.name, tt.expectedFieldsPath, gotFieldsPath) } }) } } func TestSplitAndParseResourceRequestWithMatchingPrefix(t *testing.T) { tests := []struct { name string inResource string expectedGVR schema.GroupVersionResource expectedFieldsPath []string expectedErr bool }{ { name: "no trailing period", inResource: "pods.field2.field3", expectedGVR: schema.GroupVersionResource{Resource: "pods", Version: "v1"}, expectedFieldsPath: []string{"field2", "field3"}, }, { name: "trailing period with correct fieldsPath", inResource: "service.field2.field3.", expectedGVR: schema.GroupVersionResource{Resource: "services", Version: "v1"}, expectedFieldsPath: []string{"field2", "field3"}, }, { name: "field with dots 1", inResource: `service.field2['field\.with\.dots']`, expectedGVR: schema.GroupVersionResource{Resource: "services", Version: "v1"}, expectedFieldsPath: []string{"field2", "field.with.dots"}, }, { name: "field with dots 2", inResource: `service.field2.field\.with\.dots`, expectedGVR: schema.GroupVersionResource{Resource: "services", Version: "v1"}, expectedFieldsPath: []string{"field2", "field.with.dots"}, }, { name: "trailing period with incorrect fieldsPath", inResource: "node.field2.field3.", expectedGVR: schema.GroupVersionResource{Resource: "nodes", Version: "v1"}, expectedFieldsPath: []string{"field2", "field3", ""}, expectedErr: true, }, } mapper := testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotGVR, gotFieldsPath, err := SplitAndParseResourceRequestWithMatchingPrefix(tt.inResource, mapper) if err != nil { t.Errorf("unexpected error: %v", err) } if !reflect.DeepEqual(tt.expectedGVR, gotGVR) && !tt.expectedErr { t.Errorf("%s: expected inResource: %s, got: %s", tt.name, tt.expectedGVR, gotGVR) } if !reflect.DeepEqual(tt.expectedFieldsPath, gotFieldsPath) && !tt.expectedErr { t.Errorf("%s: expected fieldsPath: %s, got: %s", tt.name, tt.expectedFieldsPath, gotFieldsPath) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/field_lookup.go000066400000000000000000000050131504711711200305060ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "fmt" "k8s.io/kube-openapi/pkg/util/proto" ) // fieldLookup walks through a schema by following a path, and returns // the final schema. type fieldLookup struct { // Path to walk Path []string // Return information: Schema found, or error. Schema proto.Schema Error error } // SaveLeafSchema is used to detect if we are done walking the path, and // saves the schema as a match. func (f *fieldLookup) SaveLeafSchema(schema proto.Schema) bool { if len(f.Path) != 0 { return false } f.Schema = schema return true } // VisitArray is mostly a passthrough. func (f *fieldLookup) VisitArray(a *proto.Array) { if f.SaveLeafSchema(a) { return } // Passthrough arrays. a.SubType.Accept(f) } // VisitMap is mostly a passthrough. func (f *fieldLookup) VisitMap(m *proto.Map) { if f.SaveLeafSchema(m) { return } // Passthrough maps. m.SubType.Accept(f) } // VisitPrimitive stops the operation and returns itself as the found // schema, even if it had more path to walk. func (f *fieldLookup) VisitPrimitive(p *proto.Primitive) { // Even if Path is not empty (we're not expecting a leaf), // return that primitive. f.Schema = p } // VisitKind unstacks fields as it finds them. func (f *fieldLookup) VisitKind(k *proto.Kind) { if f.SaveLeafSchema(k) { return } subSchema, ok := k.Fields[f.Path[0]] if !ok { f.Error = fmt.Errorf("field %q does not exist", f.Path[0]) return } f.Path = f.Path[1:] subSchema.Accept(f) } func (f *fieldLookup) VisitArbitrary(a *proto.Arbitrary) { f.Schema = a } // VisitReference is mostly a passthrough. func (f *fieldLookup) VisitReference(r proto.Reference) { if f.SaveLeafSchema(r) { return } // Passthrough references. r.SubSchema().Accept(f) } // LookupSchemaForField looks for the schema of a given path in a base schema. func LookupSchemaForField(schema proto.Schema, path []string) (proto.Schema, error) { f := &fieldLookup{Path: path} schema.Accept(f) return f.Schema, f.Error } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/field_lookup_test.go000066400000000000000000000055411504711711200315530ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "testing" "k8s.io/apimachinery/pkg/runtime/schema" ) func TestFindField(t *testing.T) { schema := resources.LookupResource(schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "OneKind", }) if schema == nil { t.Fatal("Counldn't find schema v1.OneKind") } tests := []struct { name string path []string err string expectedPath string }{ { name: "test1", path: []string{}, expectedPath: "OneKind", }, { name: "test2", path: []string{"field1"}, expectedPath: "OneKind.field1", }, { name: "test3", path: []string{"field1", "array"}, expectedPath: "OtherKind.array", }, { name: "test4", path: []string{"field1", "what?"}, err: `field "what?" does not exist`, }, { name: "test5", path: []string{"field1", ""}, err: `field "" does not exist`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { path, err := LookupSchemaForField(schema, tt.path) gotErr := "" if err != nil { gotErr = err.Error() } gotPath := "" if path != nil { gotPath = path.GetPath().String() } if gotErr != tt.err || gotPath != tt.expectedPath { t.Errorf("LookupSchemaForField(schema, %v) = (path: %q, err: %q), expected (path: %q, err: %q)", tt.path, gotPath, gotErr, tt.expectedPath, tt.err) } }) } } func TestCrdFindField(t *testing.T) { schema := resources.LookupResource(schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "CrdKind", }) if schema == nil { t.Fatal("Counldn't find schema v1.CrdKind") } tests := []struct { name string path []string err string expectedPath string }{ { name: "test1", path: []string{}, expectedPath: "CrdKind", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { path, err := LookupSchemaForField(schema, tt.path) gotErr := "" if err != nil { gotErr = err.Error() } gotPath := "" if path != nil { gotPath = path.GetPath().String() } if gotErr != tt.err || gotPath != tt.expectedPath { t.Errorf("LookupSchemaForField(schema, %v) = (path: %q, err: %q), expected (path: %q, err: %q)", tt.path, gotPath, gotErr, tt.expectedPath, tt.err) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/fields_printer.go000066400000000000000000000045051504711711200310500ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import "k8s.io/kube-openapi/pkg/util/proto" // indentDesc is the level of indentation for descriptions. const indentDesc = 2 // regularFieldsPrinter prints fields with their type and description. type regularFieldsPrinter struct { Writer *Formatter Error error } var _ proto.SchemaVisitor = ®ularFieldsPrinter{} var _ fieldsPrinter = ®ularFieldsPrinter{} // VisitArray prints a Array type. It is just a passthrough. func (f *regularFieldsPrinter) VisitArray(a *proto.Array) { a.SubType.Accept(f) } // VisitKind prints a Kind type. It prints each key in the kind, with // the type, the required flag, and the description. func (f *regularFieldsPrinter) VisitKind(k *proto.Kind) { for _, key := range k.Keys() { v := k.Fields[key] required := "" if k.IsRequired(key) { required = " -required-" } if err := f.Writer.Write("%s\t<%s>%s", key, GetTypeName(v), required); err != nil { f.Error = err return } if err := f.Writer.Indent(indentDesc).WriteWrapped("%s", v.GetDescription()); err != nil { f.Error = err return } if err := f.Writer.Write(""); err != nil { f.Error = err return } } } // VisitMap prints a Map type. It is just a passthrough. func (f *regularFieldsPrinter) VisitMap(m *proto.Map) { m.SubType.Accept(f) } // VisitPrimitive prints a Primitive type. It stops the recursion. func (f *regularFieldsPrinter) VisitPrimitive(p *proto.Primitive) { // Nothing to do. Shouldn't really happen. } // VisitReference prints a Reference type. It is just a passthrough. func (f *regularFieldsPrinter) VisitReference(r proto.Reference) { r.SubSchema().Accept(f) } // PrintFields will write the types from schema. func (f *regularFieldsPrinter) PrintFields(schema proto.Schema) error { schema.Accept(f) return f.Error } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/fields_printer_builder.go000066400000000000000000000020121504711711200325450ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain // fieldsPrinterBuilder builds either a regularFieldsPrinter or a // recursiveFieldsPrinter based on the argument. type fieldsPrinterBuilder struct { Recursive bool } // BuildFieldsPrinter builds the appropriate fieldsPrinter. func (f fieldsPrinterBuilder) BuildFieldsPrinter(writer *Formatter) fieldsPrinter { if f.Recursive { return &recursiveFieldsPrinter{ Writer: writer, } } return ®ularFieldsPrinter{ Writer: writer, } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/fields_printer_test.go000066400000000000000000000036371504711711200321140ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "bytes" "testing" "k8s.io/apimachinery/pkg/runtime/schema" ) func TestFields(t *testing.T) { schema := resources.LookupResource(schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "OneKind", }) if schema == nil { t.Fatal("Couldn't find schema v1.OneKind") } want := `field1 -required- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus. Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum dictum, vel ullamcorper lorem egestas. field2 <[]map[string]string> This is an array of object of PrimitiveDef ` buf := bytes.Buffer{} f := Formatter{ Writer: &buf, Wrap: 80, } s, err := LookupSchemaForField(schema, []string{}) if err != nil { t.Fatalf("Invalid path %v: %v", []string{}, err) } if err := (fieldsPrinterBuilder{Recursive: false}).BuildFieldsPrinter(&f).PrintFields(s); err != nil { t.Fatalf("Failed to print fields: %v", err) } got := buf.String() if got != want { t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/formatter.go000066400000000000000000000077041504711711200300460ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "fmt" "io" "regexp" "strings" ) // Formatter helps you write with indentation, and can wrap text as needed. type Formatter struct { IndentLevel int Wrap int Writer io.Writer } // Indent creates a new Formatter that will indent the code by that much more. func (f Formatter) Indent(indent int) *Formatter { f.IndentLevel = f.IndentLevel + indent return &f } // Write writes a string with the indentation set for the // Formatter. This is not wrapping text. func (f *Formatter) Write(str string, a ...interface{}) error { // Don't indent empty lines if str == "" { _, err := io.WriteString(f.Writer, "\n") return err } indent := "" for i := 0; i < f.IndentLevel; i++ { indent = indent + " " } if len(a) > 0 { str = fmt.Sprintf(str, a...) } _, err := io.WriteString(f.Writer, indent+str+"\n") return err } // WriteWrapped writes a string with the indentation set for the // Formatter, and wraps as needed. func (f *Formatter) WriteWrapped(str string, a ...interface{}) error { if f.Wrap == 0 { return f.Write(str, a...) } text := fmt.Sprintf(str, a...) strs := wrapString(text, f.Wrap-f.IndentLevel) for _, substr := range strs { if err := f.Write(substr); err != nil { return err } } return nil } type line struct { wrap int words []string } func (l *line) String() string { return strings.Join(l.words, " ") } func (l *line) Empty() bool { return len(l.words) == 0 } func (l *line) Len() int { return len(l.String()) } // Add adds the word to the line, returns true if we could, false if we // didn't have enough room. It's always possible to add to an empty line. func (l *line) Add(word string) bool { newLine := line{ wrap: l.wrap, words: append(l.words, word), } if newLine.Len() <= l.wrap || len(l.words) == 0 { l.words = newLine.words return true } return false } var bullet = regexp.MustCompile(`^(\d+\.?|-|\*)\s`) func shouldStartNewLine(lastWord, str string) bool { // preserve line breaks ending in : if strings.HasSuffix(lastWord, ":") { return true } // preserve code blocks if strings.HasPrefix(str, " ") { return true } str = strings.TrimSpace(str) // preserve empty lines if len(str) == 0 { return true } // preserve lines that look like they're starting lists if bullet.MatchString(str) { return true } // otherwise combine return false } func wrapString(str string, wrap int) []string { wrapped := []string{} l := line{wrap: wrap} // track the last word added to the current line lastWord := "" flush := func() { if !l.Empty() { lastWord = "" wrapped = append(wrapped, l.String()) l = line{wrap: wrap} } } // iterate over the lines in the original description for _, str := range strings.Split(str, "\n") { // preserve code blocks and blockquotes as-is if strings.HasPrefix(str, " ") { flush() wrapped = append(wrapped, str) continue } // preserve empty lines after the first line, since they can separate logical sections if len(wrapped) > 0 && len(strings.TrimSpace(str)) == 0 { flush() wrapped = append(wrapped, "") continue } // flush if we should start a new line if shouldStartNewLine(lastWord, str) { flush() } words := strings.Fields(str) for _, word := range words { lastWord = word if !l.Add(word) { flush() if !l.Add(word) { panic("Couldn't add to empty line.") } } } } flush() return wrapped } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/formatter_test.go000066400000000000000000000076021504711711200311020ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "bytes" "testing" "github.com/google/go-cmp/cmp" ) func TestFormatterWrite(t *testing.T) { buf := bytes.Buffer{} f := Formatter{ Writer: &buf, } f.Write("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") // Indent creates a new Formatter f.Indent(5).Write("Morbi at turpis faucibus, gravida dolor ut, fringilla velit.") // So Indent(2) doesn't indent to 7 here. f.Indent(2).Write("Etiam maximus urna at tellus faucibus mattis.") want := `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. Etiam maximus urna at tellus faucibus mattis. ` if buf.String() != want { t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want) } } func TestFormatterWrappedWrite(t *testing.T) { buf := bytes.Buffer{} f := Formatter{ Writer: &buf, Wrap: 50, } f.WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit.") f.Indent(10).WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit.") // Test long words (especially urls) on their own line. f.Indent(20).WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. ThisIsAVeryLongWordThatDoesn'tFitOnALineOnItsOwn. Morbi at turpis faucibus, gravida dolor ut, fringilla velit.") // Test content that includes newlines, bullet points, and blockquotes/code blocks f.Indent(4).WriteWrapped(` This is an introductory paragraph that should end up on a continuous line. Example: Example text on its own line List: 1. Item with wrapping text 11. Another item with wrapping text * Bullet item with wrapping text - Dash item with wrapping text base64( code goes here and here )`) want := `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. ThisIsAVeryLongWordThatDoesn'tFitOnALineOnItsOwn. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. This is an introductory paragraph that should end up on a continuous line. Example: Example text on its own line List: 1. Item with wrapping text 11. Another item with wrapping text * Bullet item with wrapping text - Dash item with wrapping text base64( code goes here and here ) ` if buf.String() != want { t.Errorf("Diff:\n%s", cmp.Diff(buf.String(), want)) } } func TestDefaultWrap(t *testing.T) { buf := bytes.Buffer{} f := Formatter{ Writer: &buf, // Wrap is not set } f.WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. Etiam maximus urna at tellus faucibus mattis.") want := `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. Etiam maximus urna at tellus faucibus mattis. ` if buf.String() != want { t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/model_printer.go000066400000000000000000000105361504711711200307030ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/kube-openapi/pkg/util/proto" ) const ( // fieldIndentLevel is the level of indentation for fields. fieldIndentLevel = 3 // descriptionIndentLevel is the level of indentation for the // description. descriptionIndentLevel = 5 ) // modelPrinter prints a schema in Writer. Its "Builder" will decide if // it's recursive or not. type modelPrinter struct { Name string Type string Descriptions []string Writer *Formatter Builder fieldsPrinterBuilder GVK schema.GroupVersionKind Error error } var _ proto.SchemaVisitor = &modelPrinter{} func (m *modelPrinter) PrintKindAndVersion() error { if err := m.Writer.Write("KIND: %s", m.GVK.Kind); err != nil { return err } return m.Writer.Write("VERSION: %s\n", m.GVK.GroupVersion()) } // PrintDescription prints the description for a given schema. There // might be multiple description, since we collect descriptions when we // go through references, arrays and maps. func (m *modelPrinter) PrintDescription(schema proto.Schema) error { if err := m.Writer.Write("DESCRIPTION:"); err != nil { return err } empty := true for i, desc := range append(m.Descriptions, schema.GetDescription()) { if desc == "" { continue } empty = false if i != 0 { if err := m.Writer.Write(""); err != nil { return err } } if err := m.Writer.Indent(descriptionIndentLevel).WriteWrapped("%s", desc); err != nil { return err } } if empty { return m.Writer.Indent(descriptionIndentLevel).WriteWrapped("") } return nil } // VisitArray recurses inside the subtype, while collecting the type if // not done yet, and the description. func (m *modelPrinter) VisitArray(a *proto.Array) { m.Descriptions = append(m.Descriptions, a.GetDescription()) if m.Type == "" { m.Type = GetTypeName(a) } a.SubType.Accept(m) } // VisitKind prints a full resource with its fields. func (m *modelPrinter) VisitKind(k *proto.Kind) { if err := m.PrintKindAndVersion(); err != nil { m.Error = err return } if m.Type == "" { m.Type = GetTypeName(k) } if m.Name != "" { m.Writer.Write("RESOURCE: %s <%s>\n", m.Name, m.Type) } if err := m.PrintDescription(k); err != nil { m.Error = err return } if err := m.Writer.Write("\nFIELDS:"); err != nil { m.Error = err return } m.Error = m.Builder.BuildFieldsPrinter(m.Writer.Indent(fieldIndentLevel)).PrintFields(k) } // VisitMap recurses inside the subtype, while collecting the type if // not done yet, and the description. func (m *modelPrinter) VisitMap(om *proto.Map) { m.Descriptions = append(m.Descriptions, om.GetDescription()) if m.Type == "" { m.Type = GetTypeName(om) } om.SubType.Accept(m) } // VisitPrimitive prints a field type and its description. func (m *modelPrinter) VisitPrimitive(p *proto.Primitive) { if err := m.PrintKindAndVersion(); err != nil { m.Error = err return } if m.Type == "" { m.Type = GetTypeName(p) } if err := m.Writer.Write("FIELD: %s <%s>\n", m.Name, m.Type); err != nil { m.Error = err return } m.Error = m.PrintDescription(p) } func (m *modelPrinter) VisitArbitrary(a *proto.Arbitrary) { if err := m.PrintKindAndVersion(); err != nil { m.Error = err return } m.Error = m.PrintDescription(a) } // VisitReference recurses inside the subtype, while collecting the description. func (m *modelPrinter) VisitReference(r proto.Reference) { m.Descriptions = append(m.Descriptions, r.GetDescription()) r.SubSchema().Accept(m) } // PrintModel prints the description of a schema in writer. func PrintModel(name string, writer *Formatter, builder fieldsPrinterBuilder, schema proto.Schema, gvk schema.GroupVersionKind) error { m := &modelPrinter{Name: name, Writer: writer, Builder: builder, GVK: gvk} schema.Accept(m) return m.Error } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/model_printer_test.go000066400000000000000000000107331504711711200317410ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "bytes" "testing" "k8s.io/apimachinery/pkg/runtime/schema" ) func TestModel(t *testing.T) { oneKind := schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "OneKind", } controlCharacterKind := schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "ControlCharacterKind", } tests := []struct { path []string want string gvk schema.GroupVersionKind }{ { want: `KIND: OneKind VERSION: v1 DESCRIPTION: OneKind has a short description FIELDS: field1 -required- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus. Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum dictum, vel ullamcorper lorem egestas. field2 <[]map[string]string> This is an array of object of PrimitiveDef `, path: []string{}, gvk: oneKind, }, { want: `KIND: OneKind VERSION: v1 RESOURCE: field1 DESCRIPTION: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus. Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum dictum, vel ullamcorper lorem egestas. This is another kind of Kind FIELDS: array <[]integer> This array must be an array of int int This int must be an int object This is an object of string primitive string -required- This string must be a string `, path: []string{"field1"}, gvk: oneKind, }, { want: `KIND: OneKind VERSION: v1 FIELD: string DESCRIPTION: This string must be a string `, path: []string{"field1", "string"}, gvk: oneKind, }, { want: `KIND: OneKind VERSION: v1 FIELD: array <[]integer> DESCRIPTION: This array must be an array of int This is an int in an array `, path: []string{"field1", "array"}, gvk: oneKind, }, { want: `KIND: ControlCharacterKind VERSION: v1 DESCRIPTION: Control character % FIELDS: field1 <> Control character % `, path: []string{}, gvk: controlCharacterKind, }, } for _, test := range tests { schema := resources.LookupResource(test.gvk) if schema == nil { t.Fatalf("Couldn't find schema %v", test.gvk) } buf := bytes.Buffer{} if err := PrintModelDescription(test.path, &buf, schema, test.gvk, false); err != nil { t.Fatalf("Failed to PrintModelDescription for path %v: %v", test.path, err) } got := buf.String() if got != test.want { t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), test.want) } } } func TestCRDModel(t *testing.T) { gvk := schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "CrdKind", } schema := resources.LookupResource(gvk) if schema == nil { t.Fatal("Couldn't find schema v1.CrdKind") } tests := []struct { path []string want string }{ { path: []string{}, want: `KIND: CrdKind VERSION: v1 DESCRIPTION: `, }, } for _, test := range tests { buf := bytes.Buffer{} if err := PrintModelDescription(test.path, &buf, schema, gvk, false); err != nil { t.Fatalf("Failed to PrintModelDescription for path %v: %v", test.path, err) } got := buf.String() if got != test.want { t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), test.want) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/recursive_fields_printer.go000066400000000000000000000044671504711711200331460ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import "k8s.io/kube-openapi/pkg/util/proto" // indentPerLevel is the level of indentation for each field recursion. const indentPerLevel = 3 // recursiveFieldsPrinter recursively prints all the fields for a given // schema. type recursiveFieldsPrinter struct { Writer *Formatter Error error } var _ proto.SchemaVisitor = &recursiveFieldsPrinter{} var _ fieldsPrinter = &recursiveFieldsPrinter{} var visitedReferences = map[string]struct{}{} // VisitArray is just a passthrough. func (f *recursiveFieldsPrinter) VisitArray(a *proto.Array) { a.SubType.Accept(f) } // VisitKind prints all its fields with their type, and then recurses // inside each of these (pre-order). func (f *recursiveFieldsPrinter) VisitKind(k *proto.Kind) { for _, key := range k.Keys() { v := k.Fields[key] f.Writer.Write("%s\t<%s>", key, GetTypeName(v)) subFields := &recursiveFieldsPrinter{ Writer: f.Writer.Indent(indentPerLevel), } if err := subFields.PrintFields(v); err != nil { f.Error = err return } } } // VisitMap is just a passthrough. func (f *recursiveFieldsPrinter) VisitMap(m *proto.Map) { m.SubType.Accept(f) } // VisitPrimitive does nothing, since it doesn't have sub-fields. func (f *recursiveFieldsPrinter) VisitPrimitive(p *proto.Primitive) { // Nothing to do. } // VisitReference is just a passthrough. func (f *recursiveFieldsPrinter) VisitReference(r proto.Reference) { if _, ok := visitedReferences[r.Reference()]; ok { return } visitedReferences[r.Reference()] = struct{}{} r.SubSchema().Accept(f) delete(visitedReferences, r.Reference()) } // PrintFields will recursively print all the fields for the given // schema. func (f *recursiveFieldsPrinter) PrintFields(schema proto.Schema) error { schema.Accept(f) return f.Error } recursive_fields_printer_test.go000066400000000000000000000047431504711711200341230ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "bytes" "testing" "k8s.io/apimachinery/pkg/runtime/schema" tst "k8s.io/kubectl/pkg/util/openapi/testing" ) func TestRecursiveFields(t *testing.T) { schema := resources.LookupResource(schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "OneKind", }) if schema == nil { t.Fatal("Couldn't find schema v1.OneKind") } want := `field1 array <[]integer> int object primitive string field2 <[]map[string]string> ` buf := bytes.Buffer{} f := Formatter{ Writer: &buf, Wrap: 80, } s, err := LookupSchemaForField(schema, []string{}) if err != nil { t.Fatalf("Invalid path %v: %v", []string{}, err) } if err := (fieldsPrinterBuilder{Recursive: true}).BuildFieldsPrinter(&f).PrintFields(s); err != nil { t.Fatalf("Failed to print fields: %v", err) } got := buf.String() if got != want { t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want) } } func TestRecursiveFieldsWithSelfReferenceObjects(t *testing.T) { var resources = tst.NewFakeResources("test-recursive-swagger.json") schema := resources.LookupResource(schema.GroupVersionKind{ Group: "", Version: "v2", Kind: "OneKind", }) if schema == nil { t.Fatal("Couldn't find schema v2.OneKind") } want := `field1 referencefield referencesarray <[]Object> field2 reference referencefield referencesarray <[]Object> string ` buf := bytes.Buffer{} f := Formatter{ Writer: &buf, Wrap: 80, } s, err := LookupSchemaForField(schema, []string{}) if err != nil { t.Fatalf("Invalid path %v: %v", []string{}, err) } if err := (fieldsPrinterBuilder{Recursive: true}).BuildFieldsPrinter(&f).PrintFields(s); err != nil { t.Fatalf("Failed to print fields: %v", err) } got := buf.String() if got != want { t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/test-recursive-swagger.json000066400000000000000000000031241504711711200330200ustar00rootroot00000000000000{ "swagger": "2.0", "info": { "title": "Kubernetes", "version": "v1.9.0" }, "paths": {}, "definitions": { "OneKind": { "description": "OneKind has a short description", "required": [ "field1" ], "properties": { "field1": { "description": "This is first reference field", "$ref": "#/definitions/ReferenceKind" }, "field2": { "description": "This is other kind field with string and reference", "$ref": "#/definitions/OtherKind" } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "OneKind", "version": "v2" } ] }, "ReferenceKind": { "description": "This is reference Kind", "properties": { "referencefield": { "description": "This is reference to itself.", "$ref": "#/definitions/ReferenceKind" }, "referencesarray": { "description": "This is an array of references", "type": "array", "items": { "description": "This is reference object", "$ref": "#/definitions/ReferenceKind" } } } }, "OtherKind": { "description": "This is other kind with string and reference fields", "properties": { "string": { "description": "This string must be a string", "type": "string" }, "reference": { "description": "This is reference field.", "$ref": "#/definitions/ReferenceKind" } } } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/test-swagger.json000066400000000000000000000055341504711711200310220ustar00rootroot00000000000000{ "swagger": "2.0", "info": { "title": "Kubernetes", "version": "v1.9.0" }, "paths": {}, "definitions": { "PrimitiveDef": { "type": "string" }, "OneKind": { "description": "OneKind has a short description", "required": [ "field1" ], "properties": { "field1": { "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus. Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum dictum, vel ullamcorper lorem egestas.", "$ref": "#/definitions/OtherKind" }, "field2": { "description": "This is an array of object of PrimitiveDef", "type": "array", "items": { "description": "This is an object of PrimitiveDef", "type": "object", "additionalProperties": { "$ref": "#/definitions/PrimitiveDef" } } } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "OneKind", "version": "v1" } ] }, "ControlCharacterKind": { "description": "Control character %", "properties": { "field1": { "description": "Control character %", } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "ControlCharacterKind", "version": "v1" } ] }, "OtherKind": { "description": "This is another kind of Kind", "required": [ "string" ], "properties": { "string": { "description": "This string must be a string", "type": "string" }, "int": { "description": "This int must be an int", "type": "integer" }, "array": { "description": "This array must be an array of int", "type": "array", "items": { "description": "This is an int in an array", "type": "integer" } }, "object": { "description": "This is an object of string", "type": "object", "additionalProperties": { "description": "this is a string in an object", "type": "string" } }, "primitive": { "$ref": "#/definitions/PrimitiveDef" } } }, "CrdKind": { "x-kubernetes-group-version-kind": [ { "group": "", "kind": "CrdKind", "version": "v1" } ] } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/typename.go000066400000000000000000000031351504711711200276570ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "fmt" "k8s.io/kube-openapi/pkg/util/proto" ) // typeName finds the name of a schema type typeName struct { Name string } var _ proto.SchemaVisitor = &typeName{} // VisitArray adds the [] prefix and recurses. func (t *typeName) VisitArray(a *proto.Array) { s := &typeName{} a.SubType.Accept(s) t.Name = fmt.Sprintf("[]%s", s.Name) } // VisitKind just returns "Object". func (t *typeName) VisitKind(k *proto.Kind) { t.Name = "Object" } // VisitMap adds the map[string] prefix and recurses. func (t *typeName) VisitMap(m *proto.Map) { s := &typeName{} m.SubType.Accept(s) t.Name = fmt.Sprintf("map[string]%s", s.Name) } // VisitPrimitive returns the name of the primitive. func (t *typeName) VisitPrimitive(p *proto.Primitive) { t.Name = p.Type } // VisitReference is just a passthrough. func (t *typeName) VisitReference(r proto.Reference) { r.SubSchema().Accept(t) } // GetTypeName returns the type of a schema. func GetTypeName(schema proto.Schema) string { t := &typeName{} schema.Accept(t) return t.Name } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/typename_test.go000066400000000000000000000040001504711711200307060ustar00rootroot00000000000000/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package explain import ( "testing" "k8s.io/apimachinery/pkg/runtime/schema" tst "k8s.io/kubectl/pkg/util/openapi/testing" ) var resources = tst.NewFakeResources("test-swagger.json") func TestReferenceTypename(t *testing.T) { schema := resources.LookupResource(schema.GroupVersionKind{ Group: "", Version: "v1", Kind: "OneKind", }) if schema == nil { t.Fatal("Couldn't find schema v1.OneKind") } tests := []struct { name string path []string expected string }{ { // Kind is "Object" name: "test1", path: []string{}, expected: "Object", }, { // Reference is equal to pointed type "Object" name: "test2", path: []string{"field1"}, expected: "Object", }, { // Reference is equal to pointed type "string" name: "test3", path: []string{"field1", "primitive"}, expected: "string", }, { // Array of object of reference to string name: "test4", path: []string{"field2"}, expected: "[]map[string]string", }, { // Array of integer name: "test5", path: []string{"field1", "array"}, expected: "[]integer", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := LookupSchemaForField(schema, tt.path) if err != nil { t.Fatalf("Invalid tt.path %v: %v", tt.path, err) } got := GetTypeName(s) if got != tt.expected { t.Errorf("Got %q, expected %q", got, tt.expected) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/000077500000000000000000000000001504711711200260335ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/explain.go000066400000000000000000000050621504711711200300250ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2 import ( "encoding/json" "errors" "fmt" "io" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/openapi" ) // PrintModelDescription prints the description of a specific model or dot path. // If recursive, all components nested within the fields of the schema will be // printed. func PrintModelDescription( fieldsPath []string, w io.Writer, client openapi.Client, gvr schema.GroupVersionResource, recursive bool, outputFormat string, ) error { generator := NewGenerator() if err := registerBuiltinTemplates(generator); err != nil { return fmt.Errorf("error parsing builtin templates. Please file a bug on GitHub: %w", err) } return printModelDescriptionWithGenerator( generator, fieldsPath, w, client, gvr, recursive, outputFormat) } // Factored out for testability func printModelDescriptionWithGenerator( generator Generator, fieldsPath []string, w io.Writer, client openapi.Client, gvr schema.GroupVersionResource, recursive bool, outputFormat string, ) error { paths, err := client.Paths() if err != nil { return fmt.Errorf("failed to fetch list of groupVersions: %w", err) } var resourcePath string if len(gvr.Group) == 0 { resourcePath = fmt.Sprintf("api/%s", gvr.Version) } else { resourcePath = fmt.Sprintf("apis/%s/%s", gvr.Group, gvr.Version) } gv, exists := paths[resourcePath] if !exists { return fmt.Errorf("couldn't find resource for \"%v\"", gvr) } openAPISchemaBytes, err := gv.Schema(runtime.ContentTypeJSON) if err != nil { return fmt.Errorf("failed to fetch openapi schema for %s: %w", resourcePath, err) } var parsedV3Schema map[string]interface{} if err := json.Unmarshal(openAPISchemaBytes, &parsedV3Schema); err != nil { return fmt.Errorf("failed to parse openapi schema for %s: %w", resourcePath, err) } err = generator.Render(outputFormat, parsedV3Schema, gvr, fieldsPath, recursive, w) explainErr := explainError("") if errors.As(err, &explainErr) { return explainErr } return err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/explain_test.go000066400000000000000000000075761504711711200311000ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2 import ( "bytes" "encoding/json" "fmt" "testing" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/openapi/openapitest" ) var apiGroupsPath = "apis/discovery.k8s.io/v1" var apiGroupsGVR = schema.GroupVersionResource{ Group: "discovery.k8s.io", Version: "v1", Resource: "apigroups", } func TestExplainErrors(t *testing.T) { var buf bytes.Buffer // Validate error when GVR is not found in returned paths map. fakeClient := openapitest.NewFakeClient() err := PrintModelDescription(nil, &buf, fakeClient, schema.GroupVersionResource{ Group: "test0.example.com", Version: "v1", Resource: "doesntmatter", }, false, "unknown-format") require.ErrorContains(t, err, "couldn't find resource for \"test0.example.com/v1, Resource=doesntmatter\"") // Validate error when openapi client returns error. fakeClient.ForcedErr = fmt.Errorf("Always fails") err = PrintModelDescription(nil, &buf, fakeClient, apiGroupsGVR, false, "unknown-format") require.ErrorContains(t, err, "failed to fetch list of groupVersions") // Validate error when GroupVersion "Schema()" call returns error. fakeClient = openapitest.NewFakeClient() forceErrorGV := openapitest.FakeGroupVersion{ForcedErr: fmt.Errorf("Always fails")} fakeClient.PathsMap["apis/test1.example.com/v1"] = &forceErrorGV err = PrintModelDescription(nil, &buf, fakeClient, schema.GroupVersionResource{ Group: "test1.example.com", Version: "v1", Resource: "doesntmatter", }, false, "unknown-format") require.ErrorContains(t, err, "failed to fetch openapi schema ") // Validate error when returned bytes from GroupVersion "Schema" are invalid. parseErrorGV := openapitest.FakeGroupVersion{GVSpec: []byte(``)} fakeClient.PathsMap["apis/test2.example.com/v1"] = &parseErrorGV err = PrintModelDescription(nil, &buf, fakeClient, schema.GroupVersionResource{ Group: "test2.example.com", Version: "v1", Resource: "doesntmatter", }, false, "unknown-format") require.ErrorContains(t, err, "failed to parse openapi schema") // Validate error when render template is not recognized. client := openapitest.NewEmbeddedFileClient() err = PrintModelDescription(nil, &buf, client, apiGroupsGVR, false, "unknown-format") require.ErrorContains(t, err, "unrecognized format: unknown-format") } // Shows that the correct GVR is fetched from the open api client when // given to explain func TestExplainOpenAPIClient(t *testing.T) { var buf bytes.Buffer fileClient := openapitest.NewEmbeddedFileClient() paths, err := fileClient.Paths() require.NoError(t, err) gv, found := paths[apiGroupsPath] require.True(t, found) discoveryBytes, err := gv.Schema("application/json") require.NoError(t, err) var doc map[string]interface{} err = json.Unmarshal(discoveryBytes, &doc) require.NoError(t, err) gen := NewGenerator() err = gen.AddTemplate("Context", "{{ toJson . }}") require.NoError(t, err) expectedContext := TemplateContext{ Document: doc, GVR: apiGroupsGVR, Recursive: false, FieldPath: nil, } err = printModelDescriptionWithGenerator(gen, nil, &buf, fileClient, apiGroupsGVR, false, "Context") require.NoError(t, err) var actualContext TemplateContext err = json.Unmarshal(buf.Bytes(), &actualContext) require.NoError(t, err) require.Equal(t, expectedContext, actualContext) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/funcs.go000066400000000000000000000133221504711711200275010ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2 import ( "bytes" "encoding/json" "errors" "fmt" "reflect" "strings" "text/template" "github.com/go-openapi/jsonreference" "k8s.io/kubectl/pkg/util/term" ) type explainError string func (e explainError) Error() string { return string(e) } func WithBuiltinTemplateFuncs(tmpl *template.Template) *template.Template { return tmpl.Funcs(map[string]interface{}{ "throw": func(e string, args ...any) (string, error) { errString := fmt.Sprintf(e, args...) return "", explainError(errString) }, "toJson": func(obj any) (string, error) { res, err := json.Marshal(obj) return string(res), err }, "toPrettyJson": func(obj any) (string, error) { res, err := json.MarshalIndent(obj, "", " ") if err != nil { return "", err } return string(res), err }, "fail": func(message string) (string, error) { return "", errors.New(message) }, "wrap": func(l int, s string) (string, error) { buf := bytes.NewBuffer(nil) writer := term.NewWordWrapWriter(buf, uint(l)) _, err := writer.Write([]byte(s)) if err != nil { return "", err } return buf.String(), nil }, "split": func(s string, sep string) []string { return strings.Split(s, sep) }, "join": func(sep string, strs ...string) string { return strings.Join(strs, sep) }, "include": func(name string, data interface{}) (string, error) { buf := bytes.NewBuffer(nil) if err := tmpl.ExecuteTemplate(buf, name, data); err != nil { return "", err } return buf.String(), nil }, "ternary": func(a, b any, condition bool) any { if condition { return a } return b }, "first": func(list any) (any, error) { if list == nil { return nil, errors.New("list is empty") } tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) l := l2.Len() if l == 0 { return nil, errors.New("list is empty") } return l2.Index(0).Interface(), nil default: return nil, fmt.Errorf("first cannot be used on type: %T", list) } }, "last": func(list any) (any, error) { if list == nil { return nil, errors.New("list is empty") } tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) l := l2.Len() if l == 0 { return nil, errors.New("list is empty") } return l2.Index(l - 1).Interface(), nil default: return nil, fmt.Errorf("last cannot be used on type: %T", list) } }, "indent": func(amount int, str string) string { pad := strings.Repeat(" ", amount) return pad + strings.Replace(str, "\n", "\n"+pad, -1) }, "dict": func(keysAndValues ...any) (map[string]any, error) { if len(keysAndValues)%2 != 0 { return nil, errors.New("expected even # of arguments") } res := map[string]any{} for i := 0; i+1 < len(keysAndValues); i = i + 2 { if key, ok := keysAndValues[i].(string); ok { res[key] = keysAndValues[i+1] } else { return nil, fmt.Errorf("key of type %T is not a string as expected", key) } } return res, nil }, "contains": func(list any, value any) bool { if list == nil { return false } val := reflect.ValueOf(list) switch val.Kind() { case reflect.Array: case reflect.Slice: for i := 0; i < val.Len(); i++ { cur := val.Index(i) if cur.CanInterface() && reflect.DeepEqual(cur.Interface(), value) { return true } } return false default: return false } return false }, "set": func(dict map[string]any, keysAndValues ...any) (any, error) { if len(keysAndValues)%2 != 0 { return nil, errors.New("expected even number of arguments") } copyDict := make(map[string]any, len(dict)) for k, v := range dict { copyDict[k] = v } for i := 0; i < len(keysAndValues); i += 2 { key, ok := keysAndValues[i].(string) if !ok { return nil, errors.New("keys must be strings") } copyDict[key] = keysAndValues[i+1] } return copyDict, nil }, "list": func(values ...any) ([]any, error) { return values, nil }, "add": func(value, operand int) int { return value + operand }, "sub": func(value, operand int) int { return value - operand }, "mul": func(value, operand int) int { return value * operand }, "resolveRef": func(refAny any, document map[string]any) map[string]any { refString, ok := refAny.(string) if !ok { // if passed nil, or wrong type just treat the same // way as unresolved reference (makes for easier templates) return nil } // Resolve field path encoded by the ref ref, err := jsonreference.New(refString) if err != nil { // Unrecognized ref format. return nil } if !ref.HasFragmentOnly { // Downloading is not supported. Treat as not found return nil } fragment := ref.GetURL().Fragment components := strings.Split(fragment, "/") cur := document for _, k := range components { if len(k) == 0 { // first component is usually empty (#/components/) , etc continue } next, ok := cur[k].(map[string]any) if !ok { return nil } cur = next } return cur }, }) } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/funcs_test.go000066400000000000000000000247331504711711200305500ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2_test import ( "bytes" "testing" "text/template" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" v2 "k8s.io/kubectl/pkg/explain/v2" ) func TestFuncs(t *testing.T) { testcases := []struct { Name string FuncName string Source string Context any Expect string Error string }{ { Name: "err", FuncName: "fail", Source: `{{fail .}}`, Context: "this is a test", Error: "this is a test", }, { Name: "basic", FuncName: "wrap", Source: `{{wrap 3 .}}`, Context: "this is a really good test", Expect: "this\nis\na\nreally\ngood\ntest", }, { Name: "basic", FuncName: "split", Source: `{{split . "/"}}`, Context: "this/is/a/slash/separated/thing", Expect: "[this is a slash separated thing]", }, { // TODO: we should find a way to both realign line breaks and not break yaml texts in descriptions // example from https://github.com/kubernetes-sigs/cluster-api/blob/f495466327aa340ad8c36182eb4e2797e7e35bef/config/crd/bases/cluster.x-k8s.io_machinedrainrules.yaml#L89 Name: "make sure explain doesnt break current descriptions", FuncName: "wrap", Source: `{{wrap 76 .}}`, Context: "machines defines to which Machines this MachineDrainRule should be applied.\nIf machines is not set, the MachineDrainRule applies to all Machines in the Namespace.\nIf machines contains multiple selectors, the results are ORed.\nWithin a single Machine selector the results of selector and clusterSelector are ANDed.\nMachines will be selected from all Clusters in the Namespace unless otherwise restricted with the clusterSelector.\nExample: Selects control plane Machines in all Clusters or Machines with label \"os\" == \"linux\" in Clusters with label \"stage\" == \"production\".\n - selector:\n matchExpressions:\n - key: cluster.x-k8s.io/control-plane\n operator: Exists\n - selector:\n matchLabels:\n os: linux\n clusterSelector:\n matchExpressions:\n - key: stage\n operator: In\n values:\n - production", Expect: "machines defines to which Machines this MachineDrainRule should be applied.\nIf machines is not set, the MachineDrainRule applies to all Machines in the\nNamespace.\nIf machines contains multiple selectors, the results are ORed.\nWithin a single Machine selector the results of selector and clusterSelector\nare ANDed.\nMachines will be selected from all Clusters in the Namespace unless\notherwise restricted with the clusterSelector.\nExample: Selects control plane Machines in all Clusters or Machines with\nlabel \"os\" == \"linux\" in Clusters with label \"stage\" == \"production\".\n - selector:\n matchExpressions:\n - key: cluster.x-k8s.io/control-plane\n operator: Exists\n - selector:\n matchLabels:\n os: linux\n clusterSelector:\n matchExpressions:\n - key: stage\n operator: In\n values:\n - production", }, { Name: "basic", FuncName: "join", Source: `{{join "/" "this" "is" "a" "slash" "separated" "thing"}}`, Expect: "this/is/a/slash/separated/thing", }, { Name: "basic", FuncName: "include", Source: `{{define "myTemplate"}}{{.}}{{end}}{{$var := include "myTemplate" .}}{{$var}}`, Context: "hello, world!", Expect: "hello, world!", }, { Name: "nil", FuncName: "first", Source: `{{first .}}`, Context: nil, Error: "list is empty", }, { Name: "empty", FuncName: "first", Source: `{{first .}}`, Context: []string{}, Error: "list is empty", }, { Name: "basic", FuncName: "first", Source: `{{first .}}`, Context: []string{"first", "second", "third"}, Expect: "first", }, { Name: "wrongtype", FuncName: "first", Source: `{{first .}}`, Context: "test", Error: "first cannot be used on type: string", }, { Name: "nil", FuncName: "last", Source: `{{last .}}`, Context: nil, Error: "list is empty", }, { Name: "empty", FuncName: "last", Source: `{{last .}}`, Context: []string{}, Error: "list is empty", }, { Name: "basic", FuncName: "last", Source: `{{last .}}`, Context: []string{"first", "second", "third"}, Expect: "third", }, { Name: "wrongtype", FuncName: "last", Source: `{{last .}}`, Context: "test", Error: "last cannot be used on type: string", }, { Name: "none", FuncName: "indent", Source: `{{indent 0 .}}`, Context: "this is a string", Expect: "this is a string", }, { Name: "some", FuncName: "indent", Source: `{{indent 2 .}}`, Context: "this is a string", Expect: " this is a string", }, { Name: "empty", FuncName: "dict", Source: `{{dict | toJson}}`, Expect: "{}", }, { Name: "single value", FuncName: "dict", Source: `{{dict "key" "value" | toJson}}`, Expect: `{"key":"value"}`, }, { Name: "twoValues", FuncName: "dict", Source: `{{dict "key1" "val1" "key2" "val2" | toJson}}`, Expect: `{"key1":"val1","key2":"val2"}`, }, { Name: "oddNumberArgs", FuncName: "dict", Source: `{{dict "key1" 1 "key2" | toJson}}`, Error: "error calling dict: expected even # of arguments", }, { Name: "IntegerValue", FuncName: "dict", Source: `{{dict "key1" 1 | toJson}}`, Expect: `{"key1":1}`, }, { Name: "MixedValues", FuncName: "dict", Source: `{{dict "key1" 1 "key2" "val2" "key3" (dict "key1" "val1") | toJson}}`, Expect: `{"key1":1,"key2":"val2","key3":{"key1":"val1"}}`, }, { Name: "nil", FuncName: "contains", Source: `{{contains . "value"}}`, Context: nil, Expect: `false`, }, { Name: "empty", FuncName: "contains", Source: `{{contains . "value"}}`, Context: []string{}, Expect: `false`, }, { Name: "basic", FuncName: "contains", Source: `{{contains . "value"}}`, Context: []string{"value"}, Expect: `true`, }, { Name: "struct", FuncName: "contains", Source: `{{contains $.haystack $.needle}}`, Context: map[string]any{ "needle": schema.GroupVersionKind{Group: "testgroup.k8s.io", Version: "v1", Kind: "Kind"}, "haystack": []schema.GroupVersionKind{ {Group: "randomgroup.k8s.io", Version: "v1", Kind: "OtherKind"}, {Group: "testgroup.k8s.io", Version: "v1", Kind: "OtherKind"}, {Group: "testgroup.k8s.io", Version: "v1", Kind: "Kind"}, }, }, Expect: `true`, }, { Name: "nil", FuncName: "set", Source: `{{set nil "key" "value" | toJson}}`, Expect: `{"key":"value"}`, }, { Name: "empty", FuncName: "set", Source: `{{set (dict) "key" "value" | toJson}}`, Expect: `{"key":"value"}`, }, { Name: "OddArgs", FuncName: "set", Source: `{{set (dict) "key" "value" "key2" | toJson}}`, Error: `expected even number of arguments`, }, { Name: "NonStringKey", FuncName: "set", Source: `{{set (dict) 1 "value" | toJson}}`, Error: `keys must be strings`, }, { Name: "NilKey", FuncName: "set", Source: `{{set (dict) nil "value" | toJson}}`, Error: `keys must be strings`, }, { Name: "NilValue", FuncName: "set", Source: `{{set (dict) "key" nil | toJson}}`, Expect: `{"key":null}`, }, { Name: "OverwriteKey", FuncName: "set", Source: `{{set (dict "key1" "val1" "key2" "val2") "key1" nil | toJson}}`, Expect: `{"key1":null,"key2":"val2"}`, }, { Name: "OverwriteKeyWithLefover", FuncName: "set", Source: `{{set (dict "key1" "val1" "key2" "val2" "key3" "val3") "key1" nil | toJson}}`, Expect: `{"key1":null,"key2":"val2","key3":"val3"}`, }, { Name: "basic", FuncName: "add", Source: `{{add 1 2}}`, Expect: `3`, }, { Name: "basic", FuncName: "sub", Source: `{{sub 1 2}}`, Expect: `-1`, }, { Name: "basic", FuncName: "mul", Source: `{{mul 2 3}}`, Expect: `6`, }, { Name: "basic", FuncName: "resolveRef", Source: `{{resolveRef "#/components/schemas/myTypeName" . | toJson}}`, Context: map[string]any{ "components": map[string]any{ "schemas": map[string]any{ "myTypeName": map[string]any{ "key": "val", }, }, }, }, Expect: `{"key":"val"}`, }, { Name: "basicNameWithDots", FuncName: "resolveRef", Source: `{{resolveRef "#/components/schemas/myTypeName.with.dots" . | toJson}}`, Context: map[string]any{ "components": map[string]any{ "schemas": map[string]any{ "myTypeName.with.dots": map[string]any{ "key": "val", }, }, }, }, Expect: `{"key":"val"}`, }, { Name: "notFound", FuncName: "resolveRef", Source: `{{resolveRef "#/components/schemas/otherTypeName" . | toJson}}`, Context: map[string]any{ "components": map[string]any{ "schemas": map[string]any{ "myTypeName": map[string]any{ "key": "val", }, }, }, }, Expect: `null`, }, { Name: "url", FuncName: "resolveRef", Source: `{{resolveRef "http://swagger.com/swagger.json#/components/schemas/myTypeName" . | toJson}}`, Context: map[string]any{ "components": map[string]any{ "schemas": map[string]any{ "myTypeName": map[string]any{ "key": "val", }, }, }, }, Expect: `null`, }, } for _, tcase := range testcases { t.Run(tcase.FuncName+"/"+tcase.Name, func(t *testing.T) { tmpl, err := v2.WithBuiltinTemplateFuncs(template.New("me")).Parse(tcase.Source) require.NoError(t, err) buf := bytes.NewBuffer(nil) err = tmpl.Execute(buf, tcase.Context) if len(tcase.Error) > 0 { require.ErrorContains(t, err, tcase.Error) } else if output := buf.String(); len(tcase.Expect) > 0 { require.NoError(t, err) require.Contains(t, output, tcase.Expect) } }) } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/generator.go000066400000000000000000000052331504711711200303530ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2 import ( "fmt" "io" "text/template" "k8s.io/apimachinery/pkg/runtime/schema" ) type Generator interface { AddTemplate(name string, contents string) error Render( // Template to use for rendering templateName string, // Self-Contained OpenAPI Document Containing all schemas used by $ref // Only OpenAPI V3 documents are supported document map[string]interface{}, // Resource within OpenAPI document for which to render explain schema gvr schema.GroupVersionResource, // Field path of child of resource to focus output onto fieldSelector []string, // Boolean indicating whether the fields should be rendered recursively/deeply recursive bool, // Output writer writer io.Writer, ) error } type TemplateContext struct { GVR schema.GroupVersionResource Document map[string]interface{} Recursive bool FieldPath []string } type generator struct { templates map[string]*template.Template } func NewGenerator() Generator { return &generator{ templates: make(map[string]*template.Template), } } func (g *generator) AddTemplate(name string, contents string) error { compiled, err := WithBuiltinTemplateFuncs(template.New(name)).Parse(contents) if err != nil { return err } g.templates[name] = compiled return nil } func (g *generator) Render( // Template to use for rendering templateName string, // Self-Contained OpenAPI Document Containing all schemas used by $ref // Only OpenAPI V3 documents are supported document map[string]interface{}, // Resource within OpenAPI document for which to render explain schema gvr schema.GroupVersionResource, // Field path of child of resource to focus output onto fieldSelector []string, // Boolean indicating whether the fields should be rendered recursively/deeply recursive bool, // Output writer writer io.Writer, ) error { compiledTemplate, ok := g.templates[templateName] if !ok { return fmt.Errorf("unrecognized format: %s", templateName) } err := compiledTemplate.Execute(writer, TemplateContext{ Document: document, Recursive: recursive, FieldPath: fieldSelector, GVR: gvr, }) return err } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/generator_test.go000066400000000000000000000057741504711711200314240ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2 import ( "bytes" "encoding/json" "testing" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/openapi/openapitest" ) var appsv1Path = "apis/apps/v1" var appsDeploymentGVR = schema.GroupVersionResource{ Group: "apps", Version: "v1", Resource: "deployments", } // Shows generic throws error when attempting to `Render“ an invalid output name // And if it is then added as a template, no error is thrown upon `Render` func TestGeneratorMissingOutput(t *testing.T) { var buf bytes.Buffer var doc map[string]interface{} appsv1Bytes := bytesForGV(t, appsv1Path) err := json.Unmarshal(appsv1Bytes, &doc) require.NoError(t, err) gen := NewGenerator() badTemplateName := "bad-template" err = gen.Render(badTemplateName, doc, appsDeploymentGVR, nil, false, &buf) require.ErrorContains(t, err, "unrecognized format: "+badTemplateName) require.Zero(t, buf.Len()) err = gen.AddTemplate(badTemplateName, "ok") require.NoError(t, err) err = gen.Render(badTemplateName, doc, appsDeploymentGVR, nil, false, &buf) require.NoError(t, err) require.Equal(t, "ok", buf.String()) } // Shows that correct context with the passed object is passed to the template func TestGeneratorContext(t *testing.T) { var buf bytes.Buffer var doc map[string]interface{} appsv1Bytes := bytesForGV(t, appsv1Path) err := json.Unmarshal(appsv1Bytes, &doc) require.NoError(t, err) gen := NewGenerator() err = gen.AddTemplate("Context", "{{ toJson . }}") require.NoError(t, err) expectedContext := TemplateContext{ Document: doc, GVR: appsDeploymentGVR, Recursive: false, FieldPath: nil, } err = gen.Render("Context", expectedContext.Document, expectedContext.GVR, expectedContext.FieldPath, expectedContext.Recursive, &buf) require.NoError(t, err) var actualContext TemplateContext err = json.Unmarshal(buf.Bytes(), &actualContext) require.NoError(t, err) require.Equal(t, expectedContext, actualContext) } // bytesForGV returns the OpenAPI V3 spec for the passed // group/version as a byte slice. Assumes bytes are in json // format. The passed path string looks like: // // apis/apps/v1 func bytesForGV(t *testing.T, gvPath string) []byte { fakeClient := openapitest.NewEmbeddedFileClient() paths, err := fakeClient.Paths() require.NoError(t, err) gv, found := paths[gvPath] require.True(t, found) gvBytes, err := gv.Schema("application/json") require.NoError(t, err) return gvBytes } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/template.go000066400000000000000000000021741504711711200302010ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2 import ( "embed" "path/filepath" "strings" ) //go:embed templates/*.tmpl var rawBuiltinTemplates embed.FS func registerBuiltinTemplates(gen Generator) error { files, err := rawBuiltinTemplates.ReadDir("templates") if err != nil { return err } for _, entry := range files { contents, err := rawBuiltinTemplates.ReadFile("templates/" + entry.Name()) if err != nil { return err } err = gen.AddTemplate( strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())), string(contents)) if err != nil { return err } } return nil } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/template_test.go000066400000000000000000000024221504711711200312340ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package v2 import ( "os" "path" "strings" "testing" ) // Ensure that the templates are embededd correctly. func TestRegisterBuitinTemplates(t *testing.T) { myGenerator := NewGenerator().(*generator) err := registerBuiltinTemplates(myGenerator) if err != nil { t.Fatal(err) } // Show that generator now as a named template for each file in the `templates` // directory. files, err := os.ReadDir("templates") if err != nil { t.Fatal(err) } for _, templateFile := range files { name := templateFile.Name() ext := path.Ext(name) if ext != "tmpl" { continue } name = strings.TrimSuffix(name, ext) if _, ok := myGenerator.templates[name]; !ok { t.Fatalf("missing template: %v", name) } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/templates/000077500000000000000000000000001504711711200300315ustar00rootroot00000000000000apiextensions.k8s.io_v1.json000066400000000000000000004770221504711711200352720ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/templates{ "openapi": "3.0.0", "info": { "title": "Kubernetes", "version": "v1.26.0" }, "paths": { "/apis/apiextensions.k8s.io/v1/": { "get": { "tags": ["apiextensions_v1"], "description": "get available resources", "operationId": "getApiextensionsV1APIResources", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" } } } }, "401": { "description": "Unauthorized" } } } }, "/apis/apiextensions.k8s.io/v1/customresourcedefinitions": { "get": { "tags": ["apiextensions_v1"], "description": "list or watch objects of kind CustomResourceDefinition", "operationId": "listApiextensionsV1CustomResourceDefinition", "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionList" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionList" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionList" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionList" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "list", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "post": { "tags": ["apiextensions_v1"], "description": "create a CustomResourceDefinition", "operationId": "createApiextensionsV1CustomResourceDefinition", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "202": { "description": "Accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "post", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "delete": { "tags": ["apiextensions_v1"], "description": "delete collection of CustomResourceDefinition", "operationId": "deleteApiextensionsV1CollectionCustomResourceDefinition", "parameters": [ { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "gracePeriodSeconds", "in": "query", "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "orphanDependents", "in": "query", "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "propagationPolicy", "in": "query", "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "deletecollection", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "parameters": [ { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/{name}": { "get": { "tags": ["apiextensions_v1"], "description": "read the specified CustomResourceDefinition", "operationId": "readApiextensionsV1CustomResourceDefinition", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "put": { "tags": ["apiextensions_v1"], "description": "replace the specified CustomResourceDefinition", "operationId": "replaceApiextensionsV1CustomResourceDefinition", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "delete": { "tags": ["apiextensions_v1"], "description": "delete a CustomResourceDefinition", "operationId": "deleteApiextensionsV1CustomResourceDefinition", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "gracePeriodSeconds", "in": "query", "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "orphanDependents", "in": "query", "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "propagationPolicy", "in": "query", "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "202": { "description": "Accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "delete", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "patch": { "tags": ["apiextensions_v1"], "description": "partially update the specified CustomResourceDefinition", "operationId": "patchApiextensionsV1CustomResourceDefinition", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "force", "in": "query", "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", "schema": { "type": "boolean", "uniqueItems": true } } ], "requestBody": { "content": { "application/apply-patch+yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/json-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/strategic-merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "parameters": [ { "name": "name", "in": "path", "description": "name of the CustomResourceDefinition", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/{name}/status": { "get": { "tags": ["apiextensions_v1"], "description": "read status of the specified CustomResourceDefinition", "operationId": "readApiextensionsV1CustomResourceDefinitionStatus", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "put": { "tags": ["apiextensions_v1"], "description": "replace status of the specified CustomResourceDefinition", "operationId": "replaceApiextensionsV1CustomResourceDefinitionStatus", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "patch": { "tags": ["apiextensions_v1"], "description": "partially update status of the specified CustomResourceDefinition", "operationId": "patchApiextensionsV1CustomResourceDefinitionStatus", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "force", "in": "query", "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", "schema": { "type": "boolean", "uniqueItems": true } } ], "requestBody": { "content": { "application/apply-patch+yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/json-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/strategic-merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "parameters": [ { "name": "name", "in": "path", "description": "name of the CustomResourceDefinition", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/apiextensions.k8s.io/v1/watch/customresourcedefinitions": { "get": { "tags": ["apiextensions_v1"], "description": "watch individual changes to a list of CustomResourceDefinition. deprecated: use the 'watch' parameter with a list operation instead.", "operationId": "watchApiextensionsV1CustomResourceDefinitionList", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watchlist", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/apiextensions.k8s.io/v1/watch/customresourcedefinitions/{name}": { "get": { "tags": ["apiextensions_v1"], "description": "watch changes to an object of kind CustomResourceDefinition. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", "operationId": "watchApiextensionsV1CustomResourceDefinition", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watch", "x-kubernetes-group-version-kind": { "group": "apiextensions.k8s.io", "version": "v1", "kind": "CustomResourceDefinition" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "name", "in": "path", "description": "name of the CustomResourceDefinition", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] } }, "components": { "schemas": { "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceColumnDefinition": { "description": "CustomResourceColumnDefinition specifies a column for server side printing.", "type": "object", "required": ["name", "type", "jsonPath"], "properties": { "description": { "description": "description is a human readable description of this column.", "type": "string" }, "format": { "description": "format is an optional OpenAPI type definition for this column. The 'name' format is applied to the primary identifier column to assist in clients identifying column is the resource name. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details.", "type": "string" }, "jsonPath": { "description": "jsonPath is a simple JSON path (i.e. with array notation) which is evaluated against each custom resource to produce the value for this column.", "type": "string", "default": "" }, "name": { "description": "name is a human readable name for the column.", "type": "string", "default": "" }, "priority": { "description": "priority is an integer defining the relative importance of this column compared to others. Lower numbers are considered higher priority. Columns that may be omitted in limited space scenarios should be given a priority greater than 0.", "type": "integer", "format": "int32" }, "type": { "description": "type is an OpenAPI type definition for this column. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for details.", "type": "string", "default": "" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceConversion": { "description": "CustomResourceConversion describes how to convert different versions of a CR.", "type": "object", "required": ["strategy"], "properties": { "strategy": { "description": "strategy specifies how custom resources are converted between versions. Allowed values are: - `None`: The converter only change the apiVersion and would not touch any other field in the custom resource. - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information\n is needed for this option. This requires spec.preserveUnknownFields to be false, and spec.conversion.webhook to be set.", "type": "string", "default": "" }, "webhook": { "description": "webhook describes how to call the conversion webhook. Required when `strategy` is set to `Webhook`.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.WebhookConversion" } ] } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition": { "description": "CustomResourceDefinition represents a resource that should be exposed on the API server. Its name MUST be in the format \u003c.spec.name\u003e.\u003c.spec.group\u003e.", "type": "object", "required": ["spec"], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "metadata": { "description": "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" } ] }, "spec": { "description": "spec describes how the user wants the resources to appear", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionSpec" } ] }, "status": { "description": "status indicates the actual state of the CustomResourceDefinition", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionStatus" } ] } }, "x-kubernetes-group-version-kind": [ { "group": "apiextensions.k8s.io", "kind": "CustomResourceDefinition", "version": "v1" } ] }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionCondition": { "description": "CustomResourceDefinitionCondition contains details for the current condition of this pod.", "type": "object", "required": ["type", "status"], "properties": { "lastTransitionTime": { "description": "lastTransitionTime last time the condition transitioned from one status to another.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "message": { "description": "message is a human-readable message indicating details about last transition.", "type": "string" }, "reason": { "description": "reason is a unique, one-word, CamelCase reason for the condition's last transition.", "type": "string" }, "status": { "description": "status is the status of the condition. Can be True, False, Unknown.", "type": "string", "default": "" }, "type": { "description": "type is the type of the condition. Types include Established, NamesAccepted and Terminating.", "type": "string", "default": "" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionList": { "description": "CustomResourceDefinitionList is a list of CustomResourceDefinition objects.", "type": "object", "required": ["items"], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "items": { "description": "items list individual CustomResourceDefinition objects", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition" } ] } }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "metadata": { "description": "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" } ] } }, "x-kubernetes-group-version-kind": [ { "group": "apiextensions.k8s.io", "kind": "CustomResourceDefinitionList", "version": "v1" } ] }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionNames": { "description": "CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition", "type": "object", "required": ["plural", "kind"], "properties": { "categories": { "description": "categories is a list of grouped resources this custom resource belongs to (e.g. 'all'). This is published in API discovery documents, and used by clients to support invocations like `kubectl get all`.", "type": "array", "items": { "type": "string", "default": "" } }, "kind": { "description": "kind is the serialized kind of the resource. It is normally CamelCase and singular. Custom resource instances will use this value as the `kind` attribute in API calls.", "type": "string", "default": "" }, "listKind": { "description": "listKind is the serialized kind of the list for this resource. Defaults to \"`kind`List\".", "type": "string" }, "plural": { "description": "plural is the plural name of the resource to serve. The custom resources are served under `/apis/\u003cgroup\u003e/\u003cversion\u003e/.../\u003cplural\u003e`. Must match the name of the CustomResourceDefinition (in the form `\u003cnames.plural\u003e.\u003cgroup\u003e`). Must be all lowercase.", "type": "string", "default": "" }, "shortNames": { "description": "shortNames are short names for the resource, exposed in API discovery documents, and used by clients to support invocations like `kubectl get \u003cshortname\u003e`. It must be all lowercase.", "type": "array", "items": { "type": "string", "default": "" } }, "singular": { "description": "singular is the singular name of the resource. It must be all lowercase. Defaults to lowercased `kind`.", "type": "string" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionSpec": { "description": "CustomResourceDefinitionSpec describes how a user wants their resource to appear", "type": "object", "required": ["group", "names", "scope", "versions"], "properties": { "conversion": { "description": "conversion defines conversion settings for the CRD.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceConversion" } ] }, "group": { "description": "group is the API group of the defined custom resource. The custom resources are served under `/apis/\u003cgroup\u003e/...`. Must match the name of the CustomResourceDefinition (in the form `\u003cnames.plural\u003e.\u003cgroup\u003e`).", "type": "string", "default": "" }, "names": { "description": "names specify the resource and kind names for the custom resource.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionNames" } ] }, "preserveUnknownFields": { "description": "preserveUnknownFields indicates that object fields which are not specified in the OpenAPI schema should be preserved when persisting to storage. apiVersion, kind, metadata and known fields inside metadata are always preserved. This field is deprecated in favor of setting `x-preserve-unknown-fields` to true in `spec.versions[*].schema.openAPIV3Schema`. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-pruning for details.", "type": "boolean" }, "scope": { "description": "scope indicates whether the defined custom resource is cluster- or namespace-scoped. Allowed values are `Cluster` and `Namespaced`.", "type": "string", "default": "" }, "versions": { "description": "versions is the list of all API versions of the defined custom resource. Version names are used to compute the order in which served versions are listed in API discovery. If the version string is \"kube-like\", it will sort above non \"kube-like\" version strings, which are ordered lexicographically. \"Kube-like\" versions start with a \"v\", then are followed by a number (the major version), then optionally the string \"alpha\" or \"beta\" and another number (the minor version). These are sorted first by GA \u003e beta \u003e alpha (where GA is a version with no suffix such as beta or alpha), and then by comparing major version, then minor version. An example sorted list of versions: v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionVersion" } ] } } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionStatus": { "description": "CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition", "type": "object", "properties": { "acceptedNames": { "description": "acceptedNames are the names that are actually being used to serve discovery. They may be different than the names in spec.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionNames" } ] }, "conditions": { "description": "conditions indicate state for particular aspects of a CustomResourceDefinition", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionCondition" } ] }, "x-kubernetes-list-map-keys": ["type"], "x-kubernetes-list-type": "map" }, "storedVersions": { "description": "storedVersions lists all versions of CustomResources that were ever persisted. Tracking these versions allows a migration path for stored versions in etcd. The field is mutable so a migration controller can finish a migration to another version (ensuring no old objects are left in storage), and then remove the rest of the versions from this list. Versions may not be removed from `spec.versions` while they exist in this list.", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionVersion": { "description": "CustomResourceDefinitionVersion describes a version for CRD.", "type": "object", "required": ["name", "served", "storage"], "properties": { "additionalPrinterColumns": { "description": "additionalPrinterColumns specifies additional columns returned in Table output. See https://kubernetes.io/docs/reference/using-api/api-concepts/#receiving-resources-as-tables for details. If no columns are specified, a single column displaying the age of the custom resource is used.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceColumnDefinition" } ] } }, "deprecated": { "description": "deprecated indicates this version of the custom resource API is deprecated. When set to true, API requests to this version receive a warning header in the server response. Defaults to false.", "type": "boolean" }, "deprecationWarning": { "description": "deprecationWarning overrides the default warning returned to API clients. May only be set when `deprecated` is true. The default warning indicates this version is deprecated and recommends use of the newest served version of equal or greater stability, if one exists.", "type": "string" }, "name": { "description": "name is the version name, e.g. “v1”, “v2beta1”, etc. The custom resources are served under this version at `/apis/\u003cgroup\u003e/\u003cversion\u003e/...` if `served` is true.", "type": "string", "default": "" }, "schema": { "description": "schema describes the schema used for validation, pruning, and defaulting of this version of the custom resource.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation" } ] }, "served": { "description": "served is a flag enabling/disabling this version from being served via REST APIs", "type": "boolean", "default": false }, "storage": { "description": "storage indicates this version should be used when persisting custom resources to storage. There must be exactly one version with storage=true.", "type": "boolean", "default": false }, "subresources": { "description": "subresources specify what subresources this version of the defined custom resource have.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresources" } ] } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresourceScale": { "description": "CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.", "type": "object", "required": ["specReplicasPath", "statusReplicasPath"], "properties": { "labelSelectorPath": { "description": "labelSelectorPath defines the JSON path inside of a custom resource that corresponds to Scale `status.selector`. Only JSON paths without the array notation are allowed. Must be a JSON Path under `.status` or `.spec`. Must be set to work with HorizontalPodAutoscaler. The field pointed by this JSON path must be a string field (not a complex selector struct) which contains a serialized label selector in string form. More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource If there is no value under the given path in the custom resource, the `status.selector` value in the `/scale` subresource will default to the empty string.", "type": "string" }, "specReplicasPath": { "description": "specReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `spec.replicas`. Only JSON paths without the array notation are allowed. Must be a JSON Path under `.spec`. If there is no value under the given path in the custom resource, the `/scale` subresource will return an error on GET.", "type": "string", "default": "" }, "statusReplicasPath": { "description": "statusReplicasPath defines the JSON path inside of a custom resource that corresponds to Scale `status.replicas`. Only JSON paths without the array notation are allowed. Must be a JSON Path under `.status`. If there is no value under the given path in the custom resource, the `status.replicas` value in the `/scale` subresource will default to 0.", "type": "string", "default": "" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresourceStatus": { "description": "CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources. Status is represented by the `.status` JSON path inside of a CustomResource. When set, * exposes a /status subresource for the custom resource * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza", "type": "object" }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresources": { "description": "CustomResourceSubresources defines the status and scale subresources for CustomResources.", "type": "object", "properties": { "scale": { "description": "scale indicates the custom resource should serve a `/scale` subresource that returns an `autoscaling/v1` Scale object.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresourceScale" } ] }, "status": { "description": "status indicates the custom resource should serve a `/status` subresource. When enabled: 1. requests to the custom resource primary endpoint ignore changes to the `status` stanza of the object. 2. requests to the custom resource `/status` subresource ignore changes to anything other than the `status` stanza of the object.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceSubresourceStatus" } ] } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceValidation": { "description": "CustomResourceValidation is a list of validation methods for CustomResources.", "type": "object", "properties": { "openAPIV3Schema": { "description": "openAPIV3Schema is the OpenAPI v3 schema to use for validation and pruning.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" } ] } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ExternalDocumentation": { "description": "ExternalDocumentation allows referencing an external resource for extended documentation.", "type": "object", "properties": { "description": { "type": "string" }, "url": { "type": "string" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSON": { "description": "JSON represents any valid JSON value. These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil." }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps": { "description": "JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).", "type": "object", "properties": { "$ref": { "type": "string" }, "$schema": { "type": "string" }, "additionalItems": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrBool" }, "additionalProperties": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrBool" }, "allOf": { "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" } ] } }, "anyOf": { "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" } ] } }, "default": { "description": "default is a default value for undefined object fields. Defaulting is a beta feature under the CustomResourceDefaulting feature gate. Defaulting requires spec.preserveUnknownFields to be false.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSON" } ] }, "definitions": { "type": "object", "additionalProperties": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" } ] } }, "dependencies": { "type": "object", "additionalProperties": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrStringArray" } ] } }, "description": { "type": "string" }, "enum": { "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSON" } ] } }, "example": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSON" }, "exclusiveMaximum": { "type": "boolean" }, "exclusiveMinimum": { "type": "boolean" }, "externalDocs": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ExternalDocumentation" }, "format": { "description": "format is an OpenAPI v3 format string. Unknown formats are ignored. The following formats are validated:\n\n- bsonobjectid: a bson object ID, i.e. a 24 characters hex string - uri: an URI as parsed by Golang net/url.ParseRequestURI - email: an email address as parsed by Golang net/mail.ParseAddress - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. - ipv4: an IPv4 IP as parsed by Golang net.ParseIP - ipv6: an IPv6 IP as parsed by Golang net.ParseIP - cidr: a CIDR as parsed by Golang net.ParseCIDR - mac: a MAC address as parsed by Golang net.ParseMAC - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ - isbn: an ISBN10 or ISBN13 number string like \"0321751043\" or \"978-0321751041\" - isbn10: an ISBN10 number string like \"0321751043\" - isbn13: an ISBN13 number string like \"978-0321751041\" - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ - hexcolor: an hexadecimal color code like \"#FFFFFF: following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ - rgbcolor: an RGB color code like rgb like \"rgb(255,255,2559\" - byte: base64 encoded binary data - password: any kind of string - date: a date string like \"2006-01-02\" as defined by full-date in RFC3339 - duration: a duration string like \"22 ns\" as parsed by Golang time.ParseDuration or compatible with Scala duration format - datetime: a date time string like \"2014-12-15T19:30:20.000Z\" as defined by date-time in RFC3339.", "type": "string" }, "id": { "type": "string" }, "items": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrArray" }, "maxItems": { "type": "integer", "format": "int64" }, "maxLength": { "type": "integer", "format": "int64" }, "maxProperties": { "type": "integer", "format": "int64" }, "maximum": { "type": "number", "format": "double" }, "minItems": { "type": "integer", "format": "int64" }, "minLength": { "type": "integer", "format": "int64" }, "minProperties": { "type": "integer", "format": "int64" }, "minimum": { "type": "number", "format": "double" }, "multipleOf": { "type": "number", "format": "double" }, "not": { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" }, "nullable": { "type": "boolean" }, "oneOf": { "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" } ] } }, "pattern": { "type": "string" }, "patternProperties": { "type": "object", "additionalProperties": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" } ] } }, "properties": { "type": "object", "additionalProperties": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" } ] } }, "required": { "type": "array", "items": { "type": "string", "default": "" } }, "title": { "type": "string" }, "type": { "type": "string" }, "uniqueItems": { "type": "boolean" }, "x-kubernetes-embedded-resource": { "description": "x-kubernetes-embedded-resource defines that the value is an embedded Kubernetes runtime.Object, with TypeMeta and ObjectMeta. The type must be object. It is allowed to further restrict the embedded object. kind, apiVersion and metadata are validated automatically. x-kubernetes-preserve-unknown-fields is allowed to be true, but does not have to be if the object is fully specified (up to kind, apiVersion, metadata).", "type": "boolean" }, "x-kubernetes-int-or-string": { "description": "x-kubernetes-int-or-string specifies that this value is either an integer or a string. If this is true, an empty type is allowed and type as child of anyOf is permitted if following one of the following patterns:\n\n1) anyOf:\n - type: integer\n - type: string\n2) allOf:\n - anyOf:\n - type: integer\n - type: string\n - ... zero or more", "type": "boolean" }, "x-kubernetes-list-map-keys": { "description": "x-kubernetes-list-map-keys annotates an array with the x-kubernetes-list-type `map` by specifying the keys used as the index of the map.\n\nThis tag MUST only be used on lists that have the \"x-kubernetes-list-type\" extension set to \"map\". Also, the values specified for this attribute must be a scalar typed field of the child structure (no nesting is supported).\n\nThe properties specified must either be required or have a default value, to ensure those properties are present for all list items.", "type": "array", "items": { "type": "string", "default": "" } }, "x-kubernetes-list-type": { "description": "x-kubernetes-list-type annotates an array to further describe its topology. This extension must only be used on lists and may have 3 possible values:\n\n1) `atomic`: the list is treated as a single entity, like a scalar.\n Atomic lists will be entirely replaced when updated. This extension\n may be used on any type of list (struct, scalar, ...).\n2) `set`:\n Sets are lists that must not have multiple items with the same value. Each\n value must be a scalar, an object with x-kubernetes-map-type `atomic` or an\n array with x-kubernetes-list-type `atomic`.\n3) `map`:\n These lists are like maps in that their elements have a non-index key\n used to identify them. Order is preserved upon merge. The map tag\n must only be used on a list with elements of type object.\nDefaults to atomic for arrays.", "type": "string" }, "x-kubernetes-map-type": { "description": "x-kubernetes-map-type annotates an object to further describe its topology. This extension must only be used when type is object and may have 2 possible values:\n\n1) `granular`:\n These maps are actual maps (key-value pairs) and each fields are independent\n from each other (they can each be manipulated by separate actors). This is\n the default behaviour for all maps.\n2) `atomic`: the list is treated as a single entity, like a scalar.\n Atomic maps will be entirely replaced when updated.", "type": "string" }, "x-kubernetes-preserve-unknown-fields": { "description": "x-kubernetes-preserve-unknown-fields stops the API server decoding step from pruning fields which are not specified in the validation schema. This affects fields recursively, but switches back to normal pruning behaviour if nested properties or additionalProperties are specified in the schema. This can either be true or undefined. False is forbidden.", "type": "boolean" }, "x-kubernetes-validations": { "description": "x-kubernetes-validations describes a list of validation rules written in the CEL expression language. This field is an alpha-level. Using this field requires the feature gate `CustomResourceValidationExpressions` to be enabled.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ValidationRule" } ] }, "x-kubernetes-list-map-keys": ["rule"], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "rule", "x-kubernetes-patch-strategy": "merge" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrArray": { "description": "JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps or an array of JSONSchemaProps. Mainly here for serialization purposes." }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrBool": { "description": "JSONSchemaPropsOrBool represents JSONSchemaProps or a boolean value. Defaults to true for the boolean property." }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrStringArray": { "description": "JSONSchemaPropsOrStringArray represents a JSONSchemaProps or a string array." }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ServiceReference": { "description": "ServiceReference holds a reference to Service.legacy.k8s.io", "type": "object", "required": ["namespace", "name"], "properties": { "name": { "description": "name is the name of the service. Required", "type": "string", "default": "" }, "namespace": { "description": "namespace is the namespace of the service. Required", "type": "string", "default": "" }, "path": { "description": "path is an optional URL path at which the webhook will be contacted.", "type": "string" }, "port": { "description": "port is an optional service port at which the webhook will be contacted. `port` should be a valid port number (1-65535, inclusive). Defaults to 443 for backward compatibility.", "type": "integer", "format": "int32" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ValidationRule": { "description": "ValidationRule describes a validation rule written in the CEL expression language.", "type": "object", "required": ["rule"], "properties": { "message": { "description": "Message represents the message displayed when validation fails. The message is required if the Rule contains line breaks. The message must not contain line breaks. If unset, the message is \"failed rule: {Rule}\". e.g. \"must be a URL with the host matching spec.host\"", "type": "string" }, "rule": { "description": "Rule represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec The Rule is scoped to the location of the x-kubernetes-validations extension in the schema. The `self` variable in the CEL expression is bound to the scoped value. Example: - Rule scoped to the root of a resource with a status subresource: {\"rule\": \"self.status.actual \u003c= self.spec.maxDesired\"}\n\nIf the Rule is scoped to an object with properties, the accessible properties of the object are field selectable via `self.field` and field presence can be checked via `has(self.field)`. Null valued fields are treated as absent fields in CEL expressions. If the Rule is scoped to an object with additionalProperties (i.e. a map) the value of the map are accessible via `self[mapKey]`, map containment can be checked via `mapKey in self` and all entries of the map are accessible via CEL macros and functions such as `self.all(...)`. If the Rule is scoped to an array, the elements of the array are accessible via `self[i]` and also by macros and functions. If the Rule is scoped to a scalar, `self` is bound to the scalar value. Examples: - Rule scoped to a map of objects: {\"rule\": \"self.components['Widget'].priority \u003c 10\"} - Rule scoped to a list of integers: {\"rule\": \"self.values.all(value, value \u003e= 0 \u0026\u0026 value \u003c 100)\"} - Rule scoped to a string value: {\"rule\": \"self.startsWith('kube')\"}\n\nThe `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the object and from any x-kubernetes-embedded-resource annotated objects. No other metadata properties are accessible.\n\nUnknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields is not accessible in CEL expressions. This includes: - Unknown field values that are preserved by object schemas with x-kubernetes-preserve-unknown-fields. - Object properties where the property schema is of an \"unknown type\". An \"unknown type\" is recursively defined as:\n - A schema with no type and x-kubernetes-preserve-unknown-fields set to true\n - An array where the items schema is of an \"unknown type\"\n - An object where the additionalProperties schema is of an \"unknown type\"\n\nOnly property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible. Accessible property names are escaped according to the following rules when accessed in the expression: - '__' escapes to '__underscores__' - '.' escapes to '__dot__' - '-' escapes to '__dash__' - '/' escapes to '__slash__' - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:\n\t \"true\", \"false\", \"null\", \"in\", \"as\", \"break\", \"const\", \"continue\", \"else\", \"for\", \"function\", \"if\",\n\t \"import\", \"let\", \"loop\", \"package\", \"namespace\", \"return\".\nExamples:\n - Rule accessing a property named \"namespace\": {\"rule\": \"self.__namespace__ \u003e 0\"}\n - Rule accessing a property named \"x-prop\": {\"rule\": \"self.x__dash__prop \u003e 0\"}\n - Rule accessing a property named \"redact__d\": {\"rule\": \"self.redact__underscores__d \u003e 0\"}\n\nEquality on arrays with x-kubernetes-list-type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:\n - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and\n non-intersecting elements in `Y` are appended, retaining their partial order.\n - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values\n are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with\n non-intersecting keys are appended, retaining their partial order.", "type": "string", "default": "" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.WebhookClientConfig": { "description": "WebhookClientConfig contains the information to make a TLS connection with the webhook.", "type": "object", "properties": { "caBundle": { "description": "caBundle is a PEM encoded CA bundle which will be used to validate the webhook's server certificate. If unspecified, system trust roots on the apiserver are used.", "type": "string", "format": "byte" }, "service": { "description": "service is a reference to the service for this webhook. Either service or url must be specified.\n\nIf the webhook is running within the cluster, then you should use `service`.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.ServiceReference" } ] }, "url": { "description": "url gives the location of the webhook, in standard URL form (`scheme://host:port/path`). Exactly one of `url` or `service` must be specified.\n\nThe `host` should not refer to a service running in the cluster; use the `service` field instead. The host might be resolved via external DNS in some apiservers (e.g., `kube-apiserver` cannot resolve in-cluster DNS as that would be a layering violation). `host` may also be an IP address.\n\nPlease note that using `localhost` or `127.0.0.1` as a `host` is risky unless you take great care to run this webhook on all hosts which run an apiserver which might need to make calls to this webhook. Such installs are likely to be non-portable, i.e., not easy to turn up in a new cluster.\n\nThe scheme must be \"https\"; the URL must begin with \"https://\".\n\nA path is optional, and if present may be any string permissible in a URL. You may use the path to pass an arbitrary string to the webhook, for example, a cluster identifier.\n\nAttempting to use a user or basic auth e.g. \"user:password@\" is not allowed. Fragments (\"#...\") and query parameters (\"?...\") are not allowed, either.", "type": "string" } } }, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.WebhookConversion": { "description": "WebhookConversion describes how to call a conversion webhook", "type": "object", "required": ["conversionReviewVersions"], "properties": { "clientConfig": { "description": "clientConfig is the instructions for how to call the webhook if strategy is `Webhook`.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.WebhookClientConfig" } ] }, "conversionReviewVersions": { "description": "conversionReviewVersions is an ordered list of preferred `ConversionReview` versions the Webhook expects. The API server will use the first version in the list which it supports. If none of the versions specified in this list are supported by API server, conversion will fail for the custom resource. If a persisted Webhook configuration specifies allowed versions and does not include any versions known to the API Server, calls to the webhook will fail.", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", "required": ["name", "singularName", "namespaced", "kind", "verbs"], "properties": { "categories": { "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", "type": "array", "items": { "type": "string", "default": "" } }, "group": { "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", "type": "string" }, "kind": { "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", "type": "string", "default": "" }, "name": { "description": "name is the plural name of the resource.", "type": "string", "default": "" }, "namespaced": { "description": "namespaced indicates if a resource is namespaced or not.", "type": "boolean", "default": false }, "shortNames": { "description": "shortNames is a list of suggested short names of the resource.", "type": "array", "items": { "type": "string", "default": "" } }, "singularName": { "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", "type": "string", "default": "" }, "storageVersionHash": { "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", "type": "string" }, "verbs": { "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", "type": "array", "items": { "type": "string", "default": "" } }, "version": { "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", "type": "object", "required": ["groupVersion", "resources"], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "groupVersion": { "description": "groupVersion is the group and version this APIResourceList is for.", "type": "string", "default": "" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "resources": { "description": "resources contains the name of the resources and if they are namespaced.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" } ] } } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "APIResourceList", "version": "v1" } ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { "description": "DeleteOptions may be provided when deleting an API object.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "dryRun": { "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "type": "array", "items": { "type": "string", "default": "" } }, "gracePeriodSeconds": { "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "type": "integer", "format": "int64" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "orphanDependents": { "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "type": "boolean" }, "preconditions": { "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" } ] }, "propagationPolicy": { "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "type": "string" } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "DeleteOptions", "version": "v1" }, { "group": "admission.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "admission.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "admissionregistration.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "admissionregistration.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apiextensions.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "apiextensions.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apiregistration.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "apiregistration.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apps", "kind": "DeleteOptions", "version": "v1" }, { "group": "apps", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apps", "kind": "DeleteOptions", "version": "v1beta2" }, { "group": "authentication.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "authentication.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "authentication.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "authorization.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "authorization.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v1" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v2" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v2beta1" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v2beta2" }, { "group": "batch", "kind": "DeleteOptions", "version": "v1" }, { "group": "batch", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "certificates.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "certificates.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "coordination.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "coordination.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "discovery.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "discovery.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "events.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "events.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "extensions", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1beta2" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1beta3" }, { "group": "imagepolicy.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "internal.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "networking.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "node.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "node.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "node.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "policy", "kind": "DeleteOptions", "version": "v1" }, { "group": "policy", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "rbac.authorization.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "rbac.authorization.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "rbac.authorization.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "scheduling.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "scheduling.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "scheduling.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "storage.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "storage.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "storage.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" } ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", "type": "object", "properties": { "continue": { "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", "type": "string" }, "remainingItemCount": { "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", "type": "integer", "format": "int64" }, "resourceVersion": { "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", "type": "string" }, "selfLink": { "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", "type": "string" }, "fieldsType": { "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", "type": "string" }, "fieldsV1": { "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" } ] }, "manager": { "description": "Manager is an identifier of the workflow managing these fields.", "type": "string" }, "operation": { "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", "type": "string" }, "subresource": { "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", "type": "string" }, "time": { "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", "type": "object", "properties": { "annotations": { "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", "type": "object", "additionalProperties": { "type": "string", "default": "" } }, "creationTimestamp": { "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "deletionGracePeriodSeconds": { "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", "type": "integer", "format": "int64" }, "deletionTimestamp": { "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "finalizers": { "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", "type": "array", "items": { "type": "string", "default": "" }, "x-kubernetes-patch-strategy": "merge" }, "generateName": { "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", "type": "string" }, "generation": { "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", "type": "integer", "format": "int64" }, "labels": { "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", "type": "object", "additionalProperties": { "type": "string", "default": "" } }, "managedFields": { "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" } ] } }, "name": { "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", "type": "string" }, "namespace": { "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", "type": "string" }, "ownerReferences": { "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" } ] }, "x-kubernetes-patch-merge-key": "uid", "x-kubernetes-patch-strategy": "merge" }, "resourceVersion": { "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", "type": "string" }, "selfLink": { "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", "type": "string" }, "uid": { "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", "type": "object", "required": ["apiVersion", "kind", "name", "uid"], "properties": { "apiVersion": { "description": "API version of the referent.", "type": "string", "default": "" }, "blockOwnerDeletion": { "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", "type": "boolean" }, "controller": { "description": "If true, this reference points to the managing controller.", "type": "boolean" }, "kind": { "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string", "default": "" }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", "type": "string", "default": "" }, "uid": { "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", "type": "string", "default": "" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", "type": "object", "properties": { "resourceVersion": { "description": "Specifies the target ResourceVersion", "type": "string" }, "uid": { "description": "Specifies the target UID.", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { "description": "Status is a return value for calls that don't return other objects.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "code": { "description": "Suggested HTTP return code for this status, 0 if not set.", "type": "integer", "format": "int32" }, "details": { "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" } ] }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "message": { "description": "A human-readable description of the status of this operation.", "type": "string" }, "metadata": { "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" } ] }, "reason": { "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", "type": "string" }, "status": { "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "type": "string" } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "Status", "version": "v1" } ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", "type": "object", "properties": { "field": { "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", "type": "string" }, "message": { "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", "type": "string" }, "reason": { "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", "type": "object", "properties": { "causes": { "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" } ] } }, "group": { "description": "The group attribute of the resource associated with the status StatusReason.", "type": "string" }, "kind": { "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "name": { "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", "type": "string" }, "retryAfterSeconds": { "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", "type": "integer", "format": "int32" }, "uid": { "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", "type": "string", "format": "date-time" }, "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { "description": "Event represents a single event to a watched resource.", "type": "object", "required": ["type", "object"], "properties": { "object": { "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" } ] }, "type": { "type": "string", "default": "" } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "WatchEvent", "version": "v1" }, { "group": "admission.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "admission.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "admissionregistration.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "admissionregistration.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apiextensions.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "apiextensions.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apiregistration.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "apiregistration.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apps", "kind": "WatchEvent", "version": "v1" }, { "group": "apps", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apps", "kind": "WatchEvent", "version": "v1beta2" }, { "group": "authentication.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "authentication.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "authentication.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "authorization.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "authorization.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v1" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v2" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v2beta1" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v2beta2" }, { "group": "batch", "kind": "WatchEvent", "version": "v1" }, { "group": "batch", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "certificates.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "certificates.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "coordination.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "coordination.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "discovery.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "discovery.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "events.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "events.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "extensions", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1beta2" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1beta3" }, { "group": "imagepolicy.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "internal.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "networking.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "node.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "node.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "node.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "policy", "kind": "WatchEvent", "version": "v1" }, { "group": "policy", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "rbac.authorization.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "rbac.authorization.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "rbac.authorization.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "scheduling.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "scheduling.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "scheduling.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "storage.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "storage.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "storage.k8s.io", "kind": "WatchEvent", "version": "v1beta1" } ] }, "io.k8s.apimachinery.pkg.runtime.RawExtension": { "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", "type": "object" } }, "securitySchemes": { "BearerToken": { "type": "apiKey", "description": "Bearer Token authentication", "name": "authorization", "in": "header" } } } } batch.k8s.io_v1.json000066400000000000000000016537011504711711200334630ustar00rootroot00000000000000kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/templates{ "openapi": "3.0.0", "info": { "title": "Kubernetes", "version": "v1.27.1" }, "paths": { "/apis/batch/v1/": { "get": { "tags": [ "batch_v1" ], "description": "get available resources", "operationId": "getBatchV1APIResources", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" } } } }, "401": { "description": "Unauthorized" } } } }, "/apis/batch/v1/cronjobs": { "get": { "tags": [ "batch_v1" ], "description": "list or watch objects of kind CronJob", "operationId": "listBatchV1CronJobForAllNamespaces", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "list", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/batch/v1/jobs": { "get": { "tags": [ "batch_v1" ], "description": "list or watch objects of kind Job", "operationId": "listBatchV1JobForAllNamespaces", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "list", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/batch/v1/namespaces/{namespace}/cronjobs": { "get": { "tags": [ "batch_v1" ], "description": "list or watch objects of kind CronJob", "operationId": "listBatchV1NamespacedCronJob", "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobList" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "list", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "post": { "tags": [ "batch_v1" ], "description": "create a CronJob", "operationId": "createBatchV1NamespacedCronJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "202": { "description": "Accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "post", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "delete": { "tags": [ "batch_v1" ], "description": "delete collection of CronJob", "operationId": "deleteBatchV1CollectionNamespacedCronJob", "parameters": [ { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "gracePeriodSeconds", "in": "query", "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "orphanDependents", "in": "query", "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "propagationPolicy", "in": "query", "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "deletecollection", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "parameters": [ { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/batch/v1/namespaces/{namespace}/cronjobs/{name}": { "get": { "tags": [ "batch_v1" ], "description": "read the specified CronJob", "operationId": "readBatchV1NamespacedCronJob", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "put": { "tags": [ "batch_v1" ], "description": "replace the specified CronJob", "operationId": "replaceBatchV1NamespacedCronJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "delete": { "tags": [ "batch_v1" ], "description": "delete a CronJob", "operationId": "deleteBatchV1NamespacedCronJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "gracePeriodSeconds", "in": "query", "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "orphanDependents", "in": "query", "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "propagationPolicy", "in": "query", "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "202": { "description": "Accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "delete", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "patch": { "tags": [ "batch_v1" ], "description": "partially update the specified CronJob", "operationId": "patchBatchV1NamespacedCronJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "force", "in": "query", "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", "schema": { "type": "boolean", "uniqueItems": true } } ], "requestBody": { "content": { "application/apply-patch+yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/json-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/strategic-merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "parameters": [ { "name": "name", "in": "path", "description": "name of the CronJob", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/batch/v1/namespaces/{namespace}/cronjobs/{name}/status": { "get": { "tags": [ "batch_v1" ], "description": "read status of the specified CronJob", "operationId": "readBatchV1NamespacedCronJobStatus", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "put": { "tags": [ "batch_v1" ], "description": "replace status of the specified CronJob", "operationId": "replaceBatchV1NamespacedCronJobStatus", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "patch": { "tags": [ "batch_v1" ], "description": "partially update status of the specified CronJob", "operationId": "patchBatchV1NamespacedCronJobStatus", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "force", "in": "query", "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", "schema": { "type": "boolean", "uniqueItems": true } } ], "requestBody": { "content": { "application/apply-patch+yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/json-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/strategic-merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "parameters": [ { "name": "name", "in": "path", "description": "name of the CronJob", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/batch/v1/namespaces/{namespace}/jobs": { "get": { "tags": [ "batch_v1" ], "description": "list or watch objects of kind Job", "operationId": "listBatchV1NamespacedJob", "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobList" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "list", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "post": { "tags": [ "batch_v1" ], "description": "create a Job", "operationId": "createBatchV1NamespacedJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "202": { "description": "Accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "post", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "delete": { "tags": [ "batch_v1" ], "description": "delete collection of Job", "operationId": "deleteBatchV1CollectionNamespacedJob", "parameters": [ { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "gracePeriodSeconds", "in": "query", "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "orphanDependents", "in": "query", "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "propagationPolicy", "in": "query", "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "deletecollection", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "parameters": [ { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/batch/v1/namespaces/{namespace}/jobs/{name}": { "get": { "tags": [ "batch_v1" ], "description": "read the specified Job", "operationId": "readBatchV1NamespacedJob", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "put": { "tags": [ "batch_v1" ], "description": "replace the specified Job", "operationId": "replaceBatchV1NamespacedJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "delete": { "tags": [ "batch_v1" ], "description": "delete a Job", "operationId": "deleteBatchV1NamespacedJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "gracePeriodSeconds", "in": "query", "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "orphanDependents", "in": "query", "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "propagationPolicy", "in": "query", "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "202": { "description": "Accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "delete", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "patch": { "tags": [ "batch_v1" ], "description": "partially update the specified Job", "operationId": "patchBatchV1NamespacedJob", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "force", "in": "query", "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", "schema": { "type": "boolean", "uniqueItems": true } } ], "requestBody": { "content": { "application/apply-patch+yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/json-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/strategic-merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "parameters": [ { "name": "name", "in": "path", "description": "name of the Job", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/batch/v1/namespaces/{namespace}/jobs/{name}/status": { "get": { "tags": [ "batch_v1" ], "description": "read status of the specified Job", "operationId": "readBatchV1NamespacedJobStatus", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "get", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "put": { "tags": [ "batch_v1" ], "description": "replace status of the specified Job", "operationId": "replaceBatchV1NamespacedJobStatus", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } } ], "requestBody": { "content": { "*/*": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "put", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "patch": { "tags": [ "batch_v1" ], "description": "partially update status of the specified Job", "operationId": "patchBatchV1NamespacedJobStatus", "parameters": [ { "name": "dryRun", "in": "query", "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldManager", "in": "query", "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldValidation", "in": "query", "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "force", "in": "query", "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", "schema": { "type": "boolean", "uniqueItems": true } } ], "requestBody": { "content": { "application/apply-patch+yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/json-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } }, "application/strategic-merge-patch+json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "patch", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "parameters": [ { "name": "name", "in": "path", "description": "name of the Job", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } } ] }, "/apis/batch/v1/watch/cronjobs": { "get": { "tags": [ "batch_v1" ], "description": "watch individual changes to a list of CronJob. deprecated: use the 'watch' parameter with a list operation instead.", "operationId": "watchBatchV1CronJobListForAllNamespaces", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watchlist", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/batch/v1/watch/jobs": { "get": { "tags": [ "batch_v1" ], "description": "watch individual changes to a list of Job. deprecated: use the 'watch' parameter with a list operation instead.", "operationId": "watchBatchV1JobListForAllNamespaces", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watchlist", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/batch/v1/watch/namespaces/{namespace}/cronjobs": { "get": { "tags": [ "batch_v1" ], "description": "watch individual changes to a list of CronJob. deprecated: use the 'watch' parameter with a list operation instead.", "operationId": "watchBatchV1NamespacedCronJobList", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watchlist", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/batch/v1/watch/namespaces/{namespace}/cronjobs/{name}": { "get": { "tags": [ "batch_v1" ], "description": "watch changes to an object of kind CronJob. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", "operationId": "watchBatchV1NamespacedCronJob", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watch", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "CronJob" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "name", "in": "path", "description": "name of the CronJob", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/batch/v1/watch/namespaces/{namespace}/jobs": { "get": { "tags": [ "batch_v1" ], "description": "watch individual changes to a list of Job. deprecated: use the 'watch' parameter with a list operation instead.", "operationId": "watchBatchV1NamespacedJobList", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watchlist", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] }, "/apis/batch/v1/watch/namespaces/{namespace}/jobs/{name}": { "get": { "tags": [ "batch_v1" ], "description": "watch changes to an object of kind Job. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", "operationId": "watchBatchV1NamespacedJob", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/json;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } }, "application/yaml": { "schema": { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" } } } }, "401": { "description": "Unauthorized" } }, "x-kubernetes-action": "watch", "x-kubernetes-group-version-kind": { "group": "batch", "version": "v1", "kind": "Job" } }, "parameters": [ { "name": "allowWatchBookmarks", "in": "query", "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "continue", "in": "query", "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "fieldSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "labelSelector", "in": "query", "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "limit", "in": "query", "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "name", "in": "path", "description": "name of the Job", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "namespace", "in": "path", "description": "object name and auth scope, such as for teams and projects", "required": true, "schema": { "type": "string", "uniqueItems": true } }, { "name": "pretty", "in": "query", "description": "If 'true', then the output is pretty printed.", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersion", "in": "query", "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "resourceVersionMatch", "in": "query", "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", "schema": { "type": "string", "uniqueItems": true } }, { "name": "sendInitialEvents", "in": "query", "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", "schema": { "type": "boolean", "uniqueItems": true } }, { "name": "timeoutSeconds", "in": "query", "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", "schema": { "type": "integer", "uniqueItems": true } }, { "name": "watch", "in": "query", "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", "schema": { "type": "boolean", "uniqueItems": true } } ] } }, "components": { "schemas": { "io.k8s.api.batch.v1.CronJob": { "description": "CronJob represents the configuration of a single cron job.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "metadata": { "description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" } ] }, "spec": { "description": "Specification of the desired behavior of a cron job, including the schedule. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobSpec" } ] }, "status": { "description": "Current status of a cron job. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJobStatus" } ] } }, "x-kubernetes-group-version-kind": [ { "group": "batch", "kind": "CronJob", "version": "v1" } ] }, "io.k8s.api.batch.v1.CronJobList": { "description": "CronJobList is a collection of cron jobs.", "type": "object", "required": [ "items" ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "items": { "description": "items is the list of CronJobs.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.CronJob" } ] } }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "metadata": { "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" } ] } }, "x-kubernetes-group-version-kind": [ { "group": "batch", "kind": "CronJobList", "version": "v1" } ] }, "io.k8s.api.batch.v1.CronJobSpec": { "description": "CronJobSpec describes how the job execution will look like and when it will actually run.", "type": "object", "required": [ "schedule", "jobTemplate" ], "properties": { "concurrencyPolicy": { "description": "Specifies how to treat concurrent executions of a Job. Valid values are:\n\n- \"Allow\" (default): allows CronJobs to run concurrently; - \"Forbid\": forbids concurrent runs, skipping next run if previous run hasn't finished yet; - \"Replace\": cancels currently running job and replaces it with a new one\n\nPossible enum values:\n - `\"Allow\"` allows CronJobs to run concurrently.\n - `\"Forbid\"` forbids concurrent runs, skipping next run if previous hasn't finished yet.\n - `\"Replace\"` cancels currently running job and replaces it with a new one.", "type": "string", "enum": [ "Allow", "Forbid", "Replace" ] }, "failedJobsHistoryLimit": { "description": "The number of failed finished jobs to retain. Value must be non-negative integer. Defaults to 1.", "type": "integer", "format": "int32" }, "jobTemplate": { "description": "Specifies the job that will be created when executing a CronJob.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobTemplateSpec" } ] }, "schedule": { "description": "The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.", "type": "string", "default": "" }, "startingDeadlineSeconds": { "description": "Optional deadline in seconds for starting the job if it misses scheduled time for any reason. Missed jobs executions will be counted as failed ones.", "type": "integer", "format": "int64" }, "successfulJobsHistoryLimit": { "description": "The number of successful finished jobs to retain. Value must be non-negative integer. Defaults to 3.", "type": "integer", "format": "int32" }, "suspend": { "description": "This flag tells the controller to suspend subsequent executions, it does not apply to already started executions. Defaults to false.", "type": "boolean" }, "timeZone": { "description": "The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. If not specified, this will default to the time zone of the kube-controller-manager process. The set of valid time zone names and the time zone offset is loaded from the system-wide time zone database by the API server during CronJob validation and the controller manager during execution. If no system-wide time zone database can be found a bundled version of the database is used instead. If the time zone name becomes invalid during the lifetime of a CronJob or due to a change in host configuration, the controller will stop creating new new Jobs and will create a system event with the reason UnknownTimeZone. More information can be found in https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#time-zones", "type": "string" } } }, "io.k8s.api.batch.v1.CronJobStatus": { "description": "CronJobStatus represents the current state of a cron job.", "type": "object", "properties": { "active": { "description": "A list of pointers to currently running jobs.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ObjectReference" } ] }, "x-kubernetes-list-type": "atomic" }, "lastScheduleTime": { "description": "Information when was the last time the job was successfully scheduled.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "lastSuccessfulTime": { "description": "Information when was the last time the job successfully completed.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] } } }, "io.k8s.api.batch.v1.Job": { "description": "Job represents the configuration of a single job.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "metadata": { "description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" } ] }, "spec": { "description": "Specification of the desired behavior of a job. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobSpec" } ] }, "status": { "description": "Current status of a job. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobStatus" } ] } }, "x-kubernetes-group-version-kind": [ { "group": "batch", "kind": "Job", "version": "v1" } ] }, "io.k8s.api.batch.v1.JobCondition": { "description": "JobCondition describes current state of a job.", "type": "object", "required": [ "type", "status" ], "properties": { "lastProbeTime": { "description": "Last time the condition was checked.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "lastTransitionTime": { "description": "Last time the condition transit from one status to another.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "message": { "description": "Human readable message indicating details about last transition.", "type": "string" }, "reason": { "description": "(brief) reason for the condition's last transition.", "type": "string" }, "status": { "description": "Status of the condition, one of True, False, Unknown.", "type": "string", "default": "" }, "type": { "description": "Type of job condition, Complete or Failed.", "type": "string", "default": "" } } }, "io.k8s.api.batch.v1.JobList": { "description": "JobList is a collection of jobs.", "type": "object", "required": [ "items" ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "items": { "description": "items is the list of Jobs.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.Job" } ] } }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "metadata": { "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" } ] } }, "x-kubernetes-group-version-kind": [ { "group": "batch", "kind": "JobList", "version": "v1" } ] }, "io.k8s.api.batch.v1.JobSpec": { "description": "JobSpec describes how the job execution will look like.", "type": "object", "required": [ "template" ], "properties": { "activeDeadlineSeconds": { "description": "Specifies the duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it; value must be positive integer. If a Job is suspended (at creation or through an update), this timer will effectively be stopped and reset when the Job is resumed again.", "type": "integer", "format": "int64" }, "backoffLimit": { "description": "Specifies the number of retries before marking this job failed. Defaults to 6", "type": "integer", "format": "int32" }, "completionMode": { "description": "completionMode specifies how Pod completions are tracked. It can be `NonIndexed` (default) or `Indexed`.\n\n`NonIndexed` means that the Job is considered complete when there have been .spec.completions successfully completed Pods. Each Pod completion is homologous to each other.\n\n`Indexed` means that the Pods of a Job get an associated completion index from 0 to (.spec.completions - 1), available in the annotation batch.kubernetes.io/job-completion-index. The Job is considered complete when there is one successfully completed Pod for each index. When value is `Indexed`, .spec.completions must be specified and `.spec.parallelism` must be less than or equal to 10^5. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller skips updates for the Job.\n\nPossible enum values:\n - `\"Indexed\"` is a Job completion mode. In this mode, the Pods of a Job get an associated completion index from 0 to (.spec.completions - 1). The Job is considered complete when a Pod completes for each completion index.\n - `\"NonIndexed\"` is a Job completion mode. In this mode, the Job is considered complete when there have been .spec.completions successfully completed Pods. Pod completions are homologous to each other.", "type": "string", "enum": [ "Indexed", "NonIndexed" ] }, "completions": { "description": "Specifies the desired number of successfully finished pods the job should be run with. Setting to null means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "type": "integer", "format": "int32" }, "manualSelector": { "description": "manualSelector controls generation of pod labels and pod selectors. Leave `manualSelector` unset unless you are certain what you are doing. When false or unset, the system pick labels unique to this job and appends those labels to the pod template. When true, the user is responsible for picking unique labels and specifying the selector. Failure to pick a unique label may cause this and other jobs to not function correctly. However, You may see `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` API. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector", "type": "boolean" }, "parallelism": { "description": "Specifies the maximum desired number of pods the job should run at any given time. The actual number of pods running in steady state will be less than this number when ((.spec.completions - .status.successful) \u003c .spec.parallelism), i.e. when the work left to do is less than max parallelism. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "type": "integer", "format": "int32" }, "podFailurePolicy": { "description": "Specifies the policy of handling failed pods. In particular, it allows to specify the set of actions and conditions which need to be satisfied to take the associated action. If empty, the default behaviour applies - the counter of failed pods, represented by the jobs's .status.failed field, is incremented and it is checked against the backoffLimit. This field cannot be used in combination with restartPolicy=OnFailure.\n\nThis field is beta-level. It can be used when the `JobPodFailurePolicy` feature gate is enabled (enabled by default).", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.PodFailurePolicy" } ] }, "selector": { "description": "A label query over pods that should match the pod count. Normally, the system sets this field for you. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" } ] }, "suspend": { "description": "suspend specifies whether the Job controller should create Pods or not. If a Job is created with suspend set to true, no Pods are created by the Job controller. If a Job is suspended after creation (i.e. the flag goes from false to true), the Job controller will delete all active Pods associated with this Job. Users must design their workload to gracefully handle this. Suspending a Job will reset the StartTime field of the Job, effectively resetting the ActiveDeadlineSeconds timer too. Defaults to false.", "type": "boolean" }, "template": { "description": "Describes the pod that will be created when executing a job. The only allowed template.spec.restartPolicy values are \"Never\" or \"OnFailure\". More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodTemplateSpec" } ] }, "ttlSecondsAfterFinished": { "description": "ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes.", "type": "integer", "format": "int32" } } }, "io.k8s.api.batch.v1.JobStatus": { "description": "JobStatus represents the current state of a Job.", "type": "object", "properties": { "active": { "description": "The number of pending and running pods.", "type": "integer", "format": "int32" }, "completedIndexes": { "description": "completedIndexes holds the completed indexes when .spec.completionMode = \"Indexed\" in a text format. The indexes are represented as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the completed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\".", "type": "string" }, "completionTime": { "description": "Represents time when the job was completed. It is not guaranteed to be set in happens-before order across separate operations. It is represented in RFC3339 form and is in UTC. The completion time is only set when the job finishes successfully.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "conditions": { "description": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobCondition" } ] }, "x-kubernetes-list-type": "atomic", "x-kubernetes-patch-merge-key": "type", "x-kubernetes-patch-strategy": "merge" }, "failed": { "description": "The number of pods which reached phase Failed.", "type": "integer", "format": "int32" }, "ready": { "description": "The number of pods which have a Ready condition.\n\nThis field is beta-level. The job controller populates the field when the feature gate JobReadyPods is enabled (enabled by default).", "type": "integer", "format": "int32" }, "startTime": { "description": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "succeeded": { "description": "The number of pods which reached phase Succeeded.", "type": "integer", "format": "int32" }, "uncountedTerminatedPods": { "description": "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.UncountedTerminatedPods" } ] } } }, "io.k8s.api.batch.v1.JobTemplateSpec": { "description": "JobTemplateSpec describes the data a Job should have when created from a template", "type": "object", "properties": { "metadata": { "description": "Standard object's metadata of the jobs created from this template. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" } ] }, "spec": { "description": "Specification of the desired behavior of the job. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.JobSpec" } ] } } }, "io.k8s.api.batch.v1.PodFailurePolicy": { "description": "PodFailurePolicy describes how failed pods influence the backoffLimit.", "type": "object", "required": [ "rules" ], "properties": { "rules": { "description": "A list of pod failure policy rules. The rules are evaluated in order. Once a rule matches a Pod failure, the remaining of the rules are ignored. When no rule matches the Pod failure, the default handling applies - the counter of pod failures is incremented and it is checked against the backoffLimit. At most 20 elements are allowed.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.PodFailurePolicyRule" } ] }, "x-kubernetes-list-type": "atomic" } } }, "io.k8s.api.batch.v1.PodFailurePolicyOnExitCodesRequirement": { "description": "PodFailurePolicyOnExitCodesRequirement describes the requirement for handling a failed pod based on its container exit codes. In particular, it lookups the .state.terminated.exitCode for each app container and init container status, represented by the .status.containerStatuses and .status.initContainerStatuses fields in the Pod status, respectively. Containers completed with success (exit code 0) are excluded from the requirement check.", "type": "object", "required": [ "operator", "values" ], "properties": { "containerName": { "description": "Restricts the check for exit codes to the container with the specified name. When null, the rule applies to all containers. When specified, it should match one the container or initContainer names in the pod template.", "type": "string" }, "operator": { "description": "Represents the relationship between the container exit code(s) and the specified values. Containers completed with success (exit code 0) are excluded from the requirement check. Possible values are:\n\n- In: the requirement is satisfied if at least one container exit code\n (might be multiple if there are multiple containers not restricted\n by the 'containerName' field) is in the set of specified values.\n- NotIn: the requirement is satisfied if at least one container exit code\n (might be multiple if there are multiple containers not restricted\n by the 'containerName' field) is not in the set of specified values.\nAdditional values are considered to be added in the future. Clients should react to an unknown operator by assuming the requirement is not satisfied.\n\nPossible enum values:\n - `\"In\"`\n - `\"NotIn\"`", "type": "string", "default": "", "enum": [ "In", "NotIn" ] }, "values": { "description": "Specifies the set of values. Each returned container exit code (might be multiple in case of multiple containers) is checked against this set of values with respect to the operator. The list of values must be ordered and must not contain duplicates. Value '0' cannot be used for the In operator. At least one element is required. At most 255 elements are allowed.", "type": "array", "items": { "type": "integer", "format": "int32", "default": 0 }, "x-kubernetes-list-type": "set" } } }, "io.k8s.api.batch.v1.PodFailurePolicyOnPodConditionsPattern": { "description": "PodFailurePolicyOnPodConditionsPattern describes a pattern for matching an actual pod condition type.", "type": "object", "required": [ "type", "status" ], "properties": { "status": { "description": "Specifies the required Pod condition status. To match a pod condition it is required that the specified status equals the pod condition status. Defaults to True.", "type": "string", "default": "" }, "type": { "description": "Specifies the required Pod condition type. To match a pod condition it is required that specified type equals the pod condition type.", "type": "string", "default": "" } } }, "io.k8s.api.batch.v1.PodFailurePolicyRule": { "description": "PodFailurePolicyRule describes how a pod failure is handled when the requirements are met. One of onExitCodes and onPodConditions, but not both, can be used in each rule.", "type": "object", "required": [ "action", "onPodConditions" ], "properties": { "action": { "description": "Specifies the action taken on a pod failure when the requirements are satisfied. Possible values are:\n\n- FailJob: indicates that the pod's job is marked as Failed and all\n running pods are terminated.\n- Ignore: indicates that the counter towards the .backoffLimit is not\n incremented and a replacement pod is created.\n- Count: indicates that the pod is handled in the default way - the\n counter towards the .backoffLimit is incremented.\nAdditional values are considered to be added in the future. Clients should react to an unknown action by skipping the rule.\n\nPossible enum values:\n - `\"Count\"` This is an action which might be taken on a pod failure - the pod failure is handled in the default way - the counter towards .backoffLimit, represented by the job's .status.failed field, is incremented.\n - `\"FailJob\"` This is an action which might be taken on a pod failure - mark the pod's job as Failed and terminate all running pods.\n - `\"Ignore\"` This is an action which might be taken on a pod failure - the counter towards .backoffLimit, represented by the job's .status.failed field, is not incremented and a replacement pod is created.", "type": "string", "default": "", "enum": [ "Count", "FailJob", "Ignore" ] }, "onExitCodes": { "description": "Represents the requirement on the container exit codes.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.PodFailurePolicyOnExitCodesRequirement" } ] }, "onPodConditions": { "description": "Represents the requirement on the pod conditions. The requirement is represented as a list of pod condition patterns. The requirement is satisfied if at least one pattern matches an actual pod condition. At most 20 elements are allowed.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.batch.v1.PodFailurePolicyOnPodConditionsPattern" } ] }, "x-kubernetes-list-type": "atomic" } } }, "io.k8s.api.batch.v1.UncountedTerminatedPods": { "description": "UncountedTerminatedPods holds UIDs of Pods that have terminated but haven't been accounted in Job status counters.", "type": "object", "properties": { "failed": { "description": "failed holds UIDs of failed Pods.", "type": "array", "items": { "type": "string", "default": "" }, "x-kubernetes-list-type": "set" }, "succeeded": { "description": "succeeded holds UIDs of succeeded Pods.", "type": "array", "items": { "type": "string", "default": "" }, "x-kubernetes-list-type": "set" } } }, "io.k8s.api.core.v1.AWSElasticBlockStoreVolumeSource": { "description": "Represents a Persistent Disk resource in AWS.\n\nAn AWS EBS disk must exist before mounting to a container. The disk must also be in the same AWS zone as the kubelet. An AWS EBS disk can only be mounted as read/write once. AWS EBS volumes support ownership management and SELinux relabeling.", "type": "object", "required": [ "volumeID" ], "properties": { "fsType": { "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", "type": "string" }, "partition": { "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty).", "type": "integer", "format": "int32" }, "readOnly": { "description": "readOnly value true will force the readOnly setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", "type": "boolean" }, "volumeID": { "description": "volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.Affinity": { "description": "Affinity is a group of affinity scheduling rules.", "type": "object", "properties": { "nodeAffinity": { "description": "Describes node affinity scheduling rules for the pod.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeAffinity" } ] }, "podAffinity": { "description": "Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)).", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinity" } ] }, "podAntiAffinity": { "description": "Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)).", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAntiAffinity" } ] } } }, "io.k8s.api.core.v1.AzureDiskVolumeSource": { "description": "AzureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.", "type": "object", "required": [ "diskName", "diskURI" ], "properties": { "cachingMode": { "description": "cachingMode is the Host Caching mode: None, Read Only, Read Write.\n\nPossible enum values:\n - `\"None\"`\n - `\"ReadOnly\"`\n - `\"ReadWrite\"`", "type": "string", "enum": [ "None", "ReadOnly", "ReadWrite" ] }, "diskName": { "description": "diskName is the Name of the data disk in the blob storage", "type": "string", "default": "" }, "diskURI": { "description": "diskURI is the URI of data disk in the blob storage", "type": "string", "default": "" }, "fsType": { "description": "fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", "type": "string" }, "kind": { "description": "kind expected values are Shared: multiple blob disks per storage account Dedicated: single blob disk per storage account Managed: azure managed data disk (only in managed availability set). defaults to shared\n\nPossible enum values:\n - `\"Dedicated\"`\n - `\"Managed\"`\n - `\"Shared\"`", "type": "string", "enum": [ "Dedicated", "Managed", "Shared" ] }, "readOnly": { "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", "type": "boolean" } } }, "io.k8s.api.core.v1.AzureFileVolumeSource": { "description": "AzureFile represents an Azure File Service mount on the host and bind mount to the pod.", "type": "object", "required": [ "secretName", "shareName" ], "properties": { "readOnly": { "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", "type": "boolean" }, "secretName": { "description": "secretName is the name of secret that contains Azure Storage Account Name and Key", "type": "string", "default": "" }, "shareName": { "description": "shareName is the azure share Name", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.CSIVolumeSource": { "description": "Represents a source location of a volume to mount, managed by an external CSI driver", "type": "object", "required": [ "driver" ], "properties": { "driver": { "description": "driver is the name of the CSI driver that handles this volume. Consult with your admin for the correct name as registered in the cluster.", "type": "string", "default": "" }, "fsType": { "description": "fsType to mount. Ex. \"ext4\", \"xfs\", \"ntfs\". If not provided, the empty value is passed to the associated CSI driver which will determine the default filesystem to apply.", "type": "string" }, "nodePublishSecretRef": { "description": "nodePublishSecretRef is a reference to the secret object containing sensitive information to pass to the CSI driver to complete the CSI NodePublishVolume and NodeUnpublishVolume calls. This field is optional, and may be empty if no secret is required. If the secret object contains more than one secret, all secret references are passed.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "readOnly": { "description": "readOnly specifies a read-only configuration for the volume. Defaults to false (read/write).", "type": "boolean" }, "volumeAttributes": { "description": "volumeAttributes stores driver-specific properties that are passed to the CSI driver. Consult your driver's documentation for supported values.", "type": "object", "additionalProperties": { "type": "string", "default": "" } } } }, "io.k8s.api.core.v1.Capabilities": { "description": "Adds and removes POSIX capabilities from running containers.", "type": "object", "properties": { "add": { "description": "Added capabilities", "type": "array", "items": { "type": "string", "default": "" } }, "drop": { "description": "Removed capabilities", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.api.core.v1.CephFSVolumeSource": { "description": "Represents a Ceph Filesystem mount that lasts the lifetime of a pod Cephfs volumes do not support ownership management or SELinux relabeling.", "type": "object", "required": [ "monitors" ], "properties": { "monitors": { "description": "monitors is Required: Monitors is a collection of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", "type": "array", "items": { "type": "string", "default": "" } }, "path": { "description": "path is Optional: Used as the mounted root, rather than the full Ceph tree, default is /", "type": "string" }, "readOnly": { "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", "type": "boolean" }, "secretFile": { "description": "secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", "type": "string" }, "secretRef": { "description": "secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "user": { "description": "user is optional: User is the rados user name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", "type": "string" } } }, "io.k8s.api.core.v1.CinderVolumeSource": { "description": "Represents a cinder volume resource in Openstack. A Cinder volume must exist before mounting to a container. The volume must also be in the same region as the kubelet. Cinder volumes support ownership management and SELinux relabeling.", "type": "object", "required": [ "volumeID" ], "properties": { "fsType": { "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", "type": "string" }, "readOnly": { "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", "type": "boolean" }, "secretRef": { "description": "secretRef is optional: points to a secret object containing parameters used to connect to OpenStack.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "volumeID": { "description": "volumeID used to identify the volume in cinder. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.ClaimSource": { "description": "ClaimSource describes a reference to a ResourceClaim.\n\nExactly one of these fields should be set. Consumers of this type must treat an empty object as if it has an unknown value.", "type": "object", "properties": { "resourceClaimName": { "description": "ResourceClaimName is the name of a ResourceClaim object in the same namespace as this pod.", "type": "string" }, "resourceClaimTemplateName": { "description": "ResourceClaimTemplateName is the name of a ResourceClaimTemplate object in the same namespace as this pod.\n\nThe template will be used to create a new ResourceClaim, which will be bound to this pod. When this pod is deleted, the ResourceClaim will also be deleted. The name of the ResourceClaim will be \u003cpod name\u003e-\u003cresource name\u003e, where \u003cresource name\u003e is the PodResourceClaim.Name. Pod validation will reject the pod if the concatenated name is not valid for a ResourceClaim (e.g. too long).\n\nAn existing ResourceClaim with that name that is not owned by the pod will not be used for the pod to avoid using an unrelated resource by mistake. Scheduling and pod startup are then blocked until the unrelated ResourceClaim is removed.\n\nThis field is immutable and no changes will be made to the corresponding ResourceClaim by the control plane after creating the ResourceClaim.", "type": "string" } } }, "io.k8s.api.core.v1.ConfigMapEnvSource": { "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", "type": "object", "properties": { "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "optional": { "description": "Specify whether the ConfigMap must be defined", "type": "boolean" } } }, "io.k8s.api.core.v1.ConfigMapKeySelector": { "description": "Selects a key from a ConfigMap.", "type": "object", "required": [ "key" ], "properties": { "key": { "description": "The key to select.", "type": "string", "default": "" }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "optional": { "description": "Specify whether the ConfigMap or its key must be defined", "type": "boolean" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.ConfigMapProjection": { "description": "Adapts a ConfigMap into a projected volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a projected volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. Note that this is identical to a configmap volume source without the default mode.", "type": "object", "properties": { "items": { "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" } ] } }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "optional": { "description": "optional specify whether the ConfigMap or its keys must be defined", "type": "boolean" } } }, "io.k8s.api.core.v1.ConfigMapVolumeSource": { "description": "Adapts a ConfigMap into a volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. ConfigMap volumes support ownership management and SELinux relabeling.", "type": "object", "properties": { "defaultMode": { "description": "defaultMode is optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", "type": "integer", "format": "int32" }, "items": { "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" } ] } }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "optional": { "description": "optional specify whether the ConfigMap or its keys must be defined", "type": "boolean" } } }, "io.k8s.api.core.v1.Container": { "description": "A single application container that you want to run within a pod.", "type": "object", "required": [ "name" ], "properties": { "args": { "description": "Arguments to the entrypoint. The container image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", "type": "array", "items": { "type": "string", "default": "" } }, "command": { "description": "Entrypoint array. Not executed within a shell. The container image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", "type": "array", "items": { "type": "string", "default": "" } }, "env": { "description": "List of environment variables to set in the container. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvVar" } ] }, "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge" }, "envFrom": { "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvFromSource" } ] } }, "image": { "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images This field is optional to allow higher level config management to default or override container images in workload controllers like Deployments and StatefulSets.", "type": "string" }, "imagePullPolicy": { "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images\n\nPossible enum values:\n - `\"Always\"` means that kubelet always attempts to pull the latest image. Container will fail If the pull fails.\n - `\"IfNotPresent\"` means that kubelet pulls if the image isn't present on disk. Container will fail if the image isn't present and the pull fails.\n - `\"Never\"` means that kubelet never pulls an image, but only uses a local image. Container will fail if the image isn't present", "type": "string", "enum": [ "Always", "IfNotPresent", "Never" ] }, "lifecycle": { "description": "Actions that the management system should take in response to container lifecycle events. Cannot be updated.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Lifecycle" } ] }, "livenessProbe": { "description": "Periodic probe of container liveness. Container will be restarted if the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Probe" } ] }, "name": { "description": "Name of the container specified as a DNS_LABEL. Each container in a pod must have a unique name (DNS_LABEL). Cannot be updated.", "type": "string", "default": "" }, "ports": { "description": "List of ports to expose from the container. Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is listening on the default \"0.0.0.0\" address inside a container will be accessible from the network. Modifying this array with strategic merge patch may corrupt the data. For more information See https://github.com/kubernetes/kubernetes/issues/108255. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ContainerPort" } ] }, "x-kubernetes-list-map-keys": [ "containerPort", "protocol" ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "containerPort", "x-kubernetes-patch-strategy": "merge" }, "readinessProbe": { "description": "Periodic probe of container service readiness. Container will be removed from service endpoints if the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Probe" } ] }, "resizePolicy": { "description": "Resources resize policy for the container.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ContainerResizePolicy" } ] }, "x-kubernetes-list-type": "atomic" }, "resources": { "description": "Compute Resources required by this container. Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceRequirements" } ] }, "securityContext": { "description": "SecurityContext defines the security options the container should be run with. If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SecurityContext" } ] }, "startupProbe": { "description": "StartupProbe indicates that the Pod has successfully initialized. If specified, no other probes are executed until this completes successfully. If this probe fails, the Pod will be restarted, just as if the livenessProbe failed. This can be used to provide different probe parameters at the beginning of a Pod's lifecycle, when it might take a long time to load data or warm a cache, than during steady-state operation. This cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Probe" } ] }, "stdin": { "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", "type": "boolean" }, "stdinOnce": { "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", "type": "boolean" }, "terminationMessagePath": { "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", "type": "string" }, "terminationMessagePolicy": { "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.\n\nPossible enum values:\n - `\"FallbackToLogsOnError\"` will read the most recent contents of the container logs for the container status message when the container exits with an error and the terminationMessagePath has no contents.\n - `\"File\"` is the default behavior and will set the container status message to the contents of the container's terminationMessagePath when the container exits.", "type": "string", "enum": [ "FallbackToLogsOnError", "File" ] }, "tty": { "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", "type": "boolean" }, "volumeDevices": { "description": "volumeDevices is the list of block devices to be used by the container.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeDevice" } ] }, "x-kubernetes-patch-merge-key": "devicePath", "x-kubernetes-patch-strategy": "merge" }, "volumeMounts": { "description": "Pod volumes to mount into the container's filesystem. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeMount" } ] }, "x-kubernetes-patch-merge-key": "mountPath", "x-kubernetes-patch-strategy": "merge" }, "workingDir": { "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", "type": "string" } } }, "io.k8s.api.core.v1.ContainerPort": { "description": "ContainerPort represents a network port in a single container.", "type": "object", "required": [ "containerPort" ], "properties": { "containerPort": { "description": "Number of port to expose on the pod's IP address. This must be a valid port number, 0 \u003c x \u003c 65536.", "type": "integer", "format": "int32", "default": 0 }, "hostIP": { "description": "What host IP to bind the external port to.", "type": "string" }, "hostPort": { "description": "Number of port to expose on the host. If specified, this must be a valid port number, 0 \u003c x \u003c 65536. If HostNetwork is specified, this must match ContainerPort. Most containers do not need this.", "type": "integer", "format": "int32" }, "name": { "description": "If specified, this must be an IANA_SVC_NAME and unique within the pod. Each named port in a pod must have a unique name. Name for the port that can be referred to by services.", "type": "string" }, "protocol": { "description": "Protocol for port. Must be UDP, TCP, or SCTP. Defaults to \"TCP\".\n\nPossible enum values:\n - `\"SCTP\"` is the SCTP protocol.\n - `\"TCP\"` is the TCP protocol.\n - `\"UDP\"` is the UDP protocol.", "type": "string", "default": "TCP", "enum": [ "SCTP", "TCP", "UDP" ] } } }, "io.k8s.api.core.v1.ContainerResizePolicy": { "description": "ContainerResizePolicy represents resource resize policy for the container.", "type": "object", "required": [ "resourceName", "restartPolicy" ], "properties": { "resourceName": { "description": "Name of the resource to which this resource resize policy applies. Supported values: cpu, memory.", "type": "string", "default": "" }, "restartPolicy": { "description": "Restart policy to apply when specified resource is resized. If not specified, it defaults to NotRequired.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.DownwardAPIProjection": { "description": "Represents downward API info for projecting into a projected volume. Note that this is identical to a downwardAPI volume source without the default mode.", "type": "object", "properties": { "items": { "description": "Items is a list of DownwardAPIVolume file", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIVolumeFile" } ] } } } }, "io.k8s.api.core.v1.DownwardAPIVolumeFile": { "description": "DownwardAPIVolumeFile represents information to create the file containing the pod field", "type": "object", "required": [ "path" ], "properties": { "fieldRef": { "description": "Required: Selects a field of the pod: only annotations, labels, name and namespace are supported.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ObjectFieldSelector" } ] }, "mode": { "description": "Optional: mode bits used to set permissions on this file, must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", "type": "integer", "format": "int32" }, "path": { "description": "Required: Path is the relative path name of the file to be created. Must not be absolute or contain the '..' path. Must be utf-8 encoded. The first item of the relative path must not start with '..'", "type": "string", "default": "" }, "resourceFieldRef": { "description": "Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceFieldSelector" } ] } } }, "io.k8s.api.core.v1.DownwardAPIVolumeSource": { "description": "DownwardAPIVolumeSource represents a volume containing downward API info. Downward API volumes support ownership management and SELinux relabeling.", "type": "object", "properties": { "defaultMode": { "description": "Optional: mode bits to use on created files by default. Must be a Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", "type": "integer", "format": "int32" }, "items": { "description": "Items is a list of downward API volume file", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIVolumeFile" } ] } } } }, "io.k8s.api.core.v1.EmptyDirVolumeSource": { "description": "Represents an empty directory for a pod. Empty directory volumes support ownership management and SELinux relabeling.", "type": "object", "properties": { "medium": { "description": "medium represents what type of storage medium should back this directory. The default is \"\" which means to use the node's default medium. Must be an empty string (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", "type": "string" }, "sizeLimit": { "description": "sizeLimit is the total amount of local storage required for this EmptyDir volume. The size limit is also applicable for memory medium. The maximum usage on memory medium EmptyDir would be the minimum value between the SizeLimit specified here and the sum of memory limits of all containers in a pod. The default is nil which means that the limit is undefined. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" } ] } } }, "io.k8s.api.core.v1.EnvFromSource": { "description": "EnvFromSource represents the source of a set of ConfigMaps", "type": "object", "properties": { "configMapRef": { "description": "The ConfigMap to select from", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapEnvSource" } ] }, "prefix": { "description": "An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.", "type": "string" }, "secretRef": { "description": "The Secret to select from", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretEnvSource" } ] } } }, "io.k8s.api.core.v1.EnvVar": { "description": "EnvVar represents an environment variable present in a Container.", "type": "object", "required": [ "name" ], "properties": { "name": { "description": "Name of the environment variable. Must be a C_IDENTIFIER.", "type": "string", "default": "" }, "value": { "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", "type": "string" }, "valueFrom": { "description": "Source for the environment variable's value. Cannot be used if value is not empty.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvVarSource" } ] } } }, "io.k8s.api.core.v1.EnvVarSource": { "description": "EnvVarSource represents a source for the value of an EnvVar.", "type": "object", "properties": { "configMapKeyRef": { "description": "Selects a key of a ConfigMap.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapKeySelector" } ] }, "fieldRef": { "description": "Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['\u003cKEY\u003e']`, `metadata.annotations['\u003cKEY\u003e']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ObjectFieldSelector" } ] }, "resourceFieldRef": { "description": "Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceFieldSelector" } ] }, "secretKeyRef": { "description": "Selects a key of a secret in the pod's namespace", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretKeySelector" } ] } } }, "io.k8s.api.core.v1.EphemeralContainer": { "description": "An EphemeralContainer is a temporary container that you may add to an existing Pod for user-initiated activities such as debugging. Ephemeral containers have no resource or scheduling guarantees, and they will not be restarted when they exit or when a Pod is removed or restarted. The kubelet may evict a Pod if an ephemeral container causes the Pod to exceed its resource allocation.\n\nTo add an ephemeral container, use the ephemeralcontainers subresource of an existing Pod. Ephemeral containers may not be removed or restarted.", "type": "object", "required": [ "name" ], "properties": { "args": { "description": "Arguments to the entrypoint. The image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", "type": "array", "items": { "type": "string", "default": "" } }, "command": { "description": "Entrypoint array. Not executed within a shell. The image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", "type": "array", "items": { "type": "string", "default": "" } }, "env": { "description": "List of environment variables to set in the container. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvVar" } ] }, "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge" }, "envFrom": { "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvFromSource" } ] } }, "image": { "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images", "type": "string" }, "imagePullPolicy": { "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images\n\nPossible enum values:\n - `\"Always\"` means that kubelet always attempts to pull the latest image. Container will fail If the pull fails.\n - `\"IfNotPresent\"` means that kubelet pulls if the image isn't present on disk. Container will fail if the image isn't present and the pull fails.\n - `\"Never\"` means that kubelet never pulls an image, but only uses a local image. Container will fail if the image isn't present", "type": "string", "enum": [ "Always", "IfNotPresent", "Never" ] }, "lifecycle": { "description": "Lifecycle is not allowed for ephemeral containers.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Lifecycle" } ] }, "livenessProbe": { "description": "Probes are not allowed for ephemeral containers.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Probe" } ] }, "name": { "description": "Name of the ephemeral container specified as a DNS_LABEL. This name must be unique among all containers, init containers and ephemeral containers.", "type": "string", "default": "" }, "ports": { "description": "Ports are not allowed for ephemeral containers.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ContainerPort" } ] }, "x-kubernetes-list-map-keys": [ "containerPort", "protocol" ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "containerPort", "x-kubernetes-patch-strategy": "merge" }, "readinessProbe": { "description": "Probes are not allowed for ephemeral containers.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Probe" } ] }, "resizePolicy": { "description": "Resources resize policy for the container.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ContainerResizePolicy" } ] }, "x-kubernetes-list-type": "atomic" }, "resources": { "description": "Resources are not allowed for ephemeral containers. Ephemeral containers use spare resources already allocated to the pod.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceRequirements" } ] }, "securityContext": { "description": "Optional: SecurityContext defines the security options the ephemeral container should be run with. If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SecurityContext" } ] }, "startupProbe": { "description": "Probes are not allowed for ephemeral containers.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Probe" } ] }, "stdin": { "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", "type": "boolean" }, "stdinOnce": { "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", "type": "boolean" }, "targetContainerName": { "description": "If set, the name of the container from PodSpec that this ephemeral container targets. The ephemeral container will be run in the namespaces (IPC, PID, etc) of this container. If not set then the ephemeral container uses the namespaces configured in the Pod spec.\n\nThe container runtime must implement support for this feature. If the runtime does not support namespace targeting then the result of setting this field is undefined.", "type": "string" }, "terminationMessagePath": { "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", "type": "string" }, "terminationMessagePolicy": { "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.\n\nPossible enum values:\n - `\"FallbackToLogsOnError\"` will read the most recent contents of the container logs for the container status message when the container exits with an error and the terminationMessagePath has no contents.\n - `\"File\"` is the default behavior and will set the container status message to the contents of the container's terminationMessagePath when the container exits.", "type": "string", "enum": [ "FallbackToLogsOnError", "File" ] }, "tty": { "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", "type": "boolean" }, "volumeDevices": { "description": "volumeDevices is the list of block devices to be used by the container.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeDevice" } ] }, "x-kubernetes-patch-merge-key": "devicePath", "x-kubernetes-patch-strategy": "merge" }, "volumeMounts": { "description": "Pod volumes to mount into the container's filesystem. Subpath mounts are not allowed for ephemeral containers. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeMount" } ] }, "x-kubernetes-patch-merge-key": "mountPath", "x-kubernetes-patch-strategy": "merge" }, "workingDir": { "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", "type": "string" } } }, "io.k8s.api.core.v1.EphemeralVolumeSource": { "description": "Represents an ephemeral volume that is handled by a normal storage driver.", "type": "object", "properties": { "volumeClaimTemplate": { "description": "Will be used to create a stand-alone PVC to provision the volume. The pod in which this EphemeralVolumeSource is embedded will be the owner of the PVC, i.e. the PVC will be deleted together with the pod. The name of the PVC will be `\u003cpod name\u003e-\u003cvolume name\u003e` where `\u003cvolume name\u003e` is the name from the `PodSpec.Volumes` array entry. Pod validation will reject the pod if the concatenated name is not valid for a PVC (for example, too long).\n\nAn existing PVC with that name that is not owned by the pod will *not* be used for the pod to avoid using an unrelated volume by mistake. Starting the pod is then blocked until the unrelated PVC is removed. If such a pre-created PVC is meant to be used by the pod, the PVC has to updated with an owner reference to the pod once the pod exists. Normally this should not be necessary, but it may be useful when manually reconstructing a broken cluster.\n\nThis field is read-only and no changes will be made by Kubernetes to the PVC after it has been created.\n\nRequired, must not be nil.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PersistentVolumeClaimTemplate" } ] } } }, "io.k8s.api.core.v1.ExecAction": { "description": "ExecAction describes a \"run in container\" action.", "type": "object", "properties": { "command": { "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.api.core.v1.FCVolumeSource": { "description": "Represents a Fibre Channel volume. Fibre Channel volumes can only be mounted as read/write once. Fibre Channel volumes support ownership management and SELinux relabeling.", "type": "object", "properties": { "fsType": { "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", "type": "string" }, "lun": { "description": "lun is Optional: FC target lun number", "type": "integer", "format": "int32" }, "readOnly": { "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", "type": "boolean" }, "targetWWNs": { "description": "targetWWNs is Optional: FC target worldwide names (WWNs)", "type": "array", "items": { "type": "string", "default": "" } }, "wwids": { "description": "wwids Optional: FC volume world wide identifiers (wwids) Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously.", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.api.core.v1.FlexVolumeSource": { "description": "FlexVolume represents a generic volume resource that is provisioned/attached using an exec based plugin.", "type": "object", "required": [ "driver" ], "properties": { "driver": { "description": "driver is the name of the driver to use for this volume.", "type": "string", "default": "" }, "fsType": { "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". The default filesystem depends on FlexVolume script.", "type": "string" }, "options": { "description": "options is Optional: this field holds extra command options if any.", "type": "object", "additionalProperties": { "type": "string", "default": "" } }, "readOnly": { "description": "readOnly is Optional: defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", "type": "boolean" }, "secretRef": { "description": "secretRef is Optional: secretRef is reference to the secret object containing sensitive information to pass to the plugin scripts. This may be empty if no secret object is specified. If the secret object contains more than one secret, all secrets are passed to the plugin scripts.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] } } }, "io.k8s.api.core.v1.FlockerVolumeSource": { "description": "Represents a Flocker volume mounted by the Flocker agent. One and only one of datasetName and datasetUUID should be set. Flocker volumes do not support ownership management or SELinux relabeling.", "type": "object", "properties": { "datasetName": { "description": "datasetName is Name of the dataset stored as metadata -\u003e name on the dataset for Flocker should be considered as deprecated", "type": "string" }, "datasetUUID": { "description": "datasetUUID is the UUID of the dataset. This is unique identifier of a Flocker dataset", "type": "string" } } }, "io.k8s.api.core.v1.GCEPersistentDiskVolumeSource": { "description": "Represents a Persistent Disk resource in Google Compute Engine.\n\nA GCE PD must exist before mounting to a container. The disk must also be in the same GCE project and zone as the kubelet. A GCE PD can only be mounted as read/write once or read-only many times. GCE PDs support ownership management and SELinux relabeling.", "type": "object", "required": [ "pdName" ], "properties": { "fsType": { "description": "fsType is filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", "type": "string" }, "partition": { "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", "type": "integer", "format": "int32" }, "pdName": { "description": "pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", "type": "string", "default": "" }, "readOnly": { "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", "type": "boolean" } } }, "io.k8s.api.core.v1.GRPCAction": { "type": "object", "required": [ "port" ], "properties": { "port": { "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", "type": "integer", "format": "int32", "default": 0 }, "service": { "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.GitRepoVolumeSource": { "description": "Represents a volume that is populated with the contents of a git repository. Git repo volumes do not support ownership management. Git repo volumes support SELinux relabeling.\n\nDEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir into the Pod's container.", "type": "object", "required": [ "repository" ], "properties": { "directory": { "description": "directory is the target directory name. Must not contain or start with '..'. If '.' is supplied, the volume directory will be the git repository. Otherwise, if specified, the volume will contain the git repository in the subdirectory with the given name.", "type": "string" }, "repository": { "description": "repository is the URL", "type": "string", "default": "" }, "revision": { "description": "revision is the commit hash for the specified revision.", "type": "string" } } }, "io.k8s.api.core.v1.GlusterfsVolumeSource": { "description": "Represents a Glusterfs mount that lasts the lifetime of a pod. Glusterfs volumes do not support ownership management or SELinux relabeling.", "type": "object", "required": [ "endpoints", "path" ], "properties": { "endpoints": { "description": "endpoints is the endpoint name that details Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", "type": "string", "default": "" }, "path": { "description": "path is the Glusterfs volume path. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", "type": "string", "default": "" }, "readOnly": { "description": "readOnly here will force the Glusterfs volume to be mounted with read-only permissions. Defaults to false. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", "type": "boolean" } } }, "io.k8s.api.core.v1.HTTPGetAction": { "description": "HTTPGetAction describes an action based on HTTP Get requests.", "type": "object", "required": [ "port" ], "properties": { "host": { "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", "type": "string" }, "httpHeaders": { "description": "Custom headers to set in the request. HTTP allows repeated headers.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.HTTPHeader" } ] } }, "path": { "description": "Path to access on the HTTP server.", "type": "string" }, "port": { "description": "Name or number of the port to access on the container. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.util.intstr.IntOrString" } ] }, "scheme": { "description": "Scheme to use for connecting to the host. Defaults to HTTP.\n\nPossible enum values:\n - `\"HTTP\"` means that the scheme used will be http://\n - `\"HTTPS\"` means that the scheme used will be https://", "type": "string", "enum": [ "HTTP", "HTTPS" ] } } }, "io.k8s.api.core.v1.HTTPHeader": { "description": "HTTPHeader describes a custom header to be used in HTTP probes", "type": "object", "required": [ "name", "value" ], "properties": { "name": { "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", "type": "string", "default": "" }, "value": { "description": "The header field value", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.HostAlias": { "description": "HostAlias holds the mapping between IP and hostnames that will be injected as an entry in the pod's hosts file.", "type": "object", "properties": { "hostnames": { "description": "Hostnames for the above IP address.", "type": "array", "items": { "type": "string", "default": "" } }, "ip": { "description": "IP address of the host file entry.", "type": "string" } } }, "io.k8s.api.core.v1.HostPathVolumeSource": { "description": "Represents a host path mapped into a pod. Host path volumes do not support ownership management or SELinux relabeling.", "type": "object", "required": [ "path" ], "properties": { "path": { "description": "path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", "type": "string", "default": "" }, "type": { "description": "type for HostPath Volume Defaults to \"\" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath\n\nPossible enum values:\n - `\"\"` For backwards compatible, leave it empty if unset\n - `\"BlockDevice\"` A block device must exist at the given path\n - `\"CharDevice\"` A character device must exist at the given path\n - `\"Directory\"` A directory must exist at the given path\n - `\"DirectoryOrCreate\"` If nothing exists at the given path, an empty directory will be created there as needed with file mode 0755, having the same group and ownership with Kubelet.\n - `\"File\"` A file must exist at the given path\n - `\"FileOrCreate\"` If nothing exists at the given path, an empty file will be created there as needed with file mode 0644, having the same group and ownership with Kubelet.\n - `\"Socket\"` A UNIX socket must exist at the given path", "type": "string", "enum": [ "", "BlockDevice", "CharDevice", "Directory", "DirectoryOrCreate", "File", "FileOrCreate", "Socket" ] } } }, "io.k8s.api.core.v1.ISCSIVolumeSource": { "description": "Represents an ISCSI disk. ISCSI volumes can only be mounted as read/write once. ISCSI volumes support ownership management and SELinux relabeling.", "type": "object", "required": [ "targetPortal", "iqn", "lun" ], "properties": { "chapAuthDiscovery": { "description": "chapAuthDiscovery defines whether support iSCSI Discovery CHAP authentication", "type": "boolean" }, "chapAuthSession": { "description": "chapAuthSession defines whether support iSCSI Session CHAP authentication", "type": "boolean" }, "fsType": { "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi", "type": "string" }, "initiatorName": { "description": "initiatorName is the custom iSCSI Initiator Name. If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface \u003ctarget portal\u003e:\u003cvolume name\u003e will be created for the connection.", "type": "string" }, "iqn": { "description": "iqn is the target iSCSI Qualified Name.", "type": "string", "default": "" }, "iscsiInterface": { "description": "iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp).", "type": "string" }, "lun": { "description": "lun represents iSCSI Target Lun number.", "type": "integer", "format": "int32", "default": 0 }, "portals": { "description": "portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", "type": "array", "items": { "type": "string", "default": "" } }, "readOnly": { "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false.", "type": "boolean" }, "secretRef": { "description": "secretRef is the CHAP Secret for iSCSI target and initiator authentication", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "targetPortal": { "description": "targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.KeyToPath": { "description": "Maps a string key to a path within a volume.", "type": "object", "required": [ "key", "path" ], "properties": { "key": { "description": "key is the key to project.", "type": "string", "default": "" }, "mode": { "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", "type": "integer", "format": "int32" }, "path": { "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.Lifecycle": { "description": "Lifecycle describes actions that the management system should take in response to container lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container blocks until the action is complete, unless the container process fails, in which case the handler is aborted.", "type": "object", "properties": { "postStart": { "description": "PostStart is called immediately after a container is created. If the handler fails, the container is terminated and restarted according to its restart policy. Other management of the container blocks until the hook completes. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LifecycleHandler" } ] }, "preStop": { "description": "PreStop is called immediately before a container is terminated due to an API request or management event such as liveness/startup probe failure, preemption, resource contention, etc. The handler is not called if the container crashes or exits. The Pod's termination grace period countdown begins before the PreStop hook is executed. Regardless of the outcome of the handler, the container will eventually terminate within the Pod's termination grace period (unless delayed by finalizers). Other management of the container blocks until the hook completes or until the termination grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LifecycleHandler" } ] } } }, "io.k8s.api.core.v1.LifecycleHandler": { "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", "type": "object", "properties": { "exec": { "description": "Exec specifies the action to take.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ExecAction" } ] }, "httpGet": { "description": "HTTPGet specifies the http request to perform.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.HTTPGetAction" } ] }, "tcpSocket": { "description": "Deprecated. TCPSocket is NOT supported as a LifecycleHandler and kept for the backward compatibility. There are no validation of this field and lifecycle hooks will fail in runtime when tcp handler is specified.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.TCPSocketAction" } ] } } }, "io.k8s.api.core.v1.LocalObjectReference": { "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", "type": "object", "properties": { "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.NFSVolumeSource": { "description": "Represents an NFS mount that lasts the lifetime of a pod. NFS volumes do not support ownership management or SELinux relabeling.", "type": "object", "required": [ "server", "path" ], "properties": { "path": { "description": "path that is exported by the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", "type": "string", "default": "" }, "readOnly": { "description": "readOnly here will force the NFS export to be mounted with read-only permissions. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", "type": "boolean" }, "server": { "description": "server is the hostname or IP address of the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.NodeAffinity": { "description": "Node affinity is a group of node affinity scheduling rules.", "type": "object", "properties": { "preferredDuringSchedulingIgnoredDuringExecution": { "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PreferredSchedulingTerm" } ] } }, "requiredDuringSchedulingIgnoredDuringExecution": { "description": "If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to an update), the system may or may not try to eventually evict the pod from its node.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelector" } ] } } }, "io.k8s.api.core.v1.NodeSelector": { "description": "A node selector represents the union of the results of one or more label queries over a set of nodes; that is, it represents the OR of the selectors represented by the node selector terms.", "type": "object", "required": [ "nodeSelectorTerms" ], "properties": { "nodeSelectorTerms": { "description": "Required. A list of node selector terms. The terms are ORed.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorTerm" } ] } } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.NodeSelectorRequirement": { "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", "type": "object", "required": [ "key", "operator" ], "properties": { "key": { "description": "The label key that the selector applies to.", "type": "string", "default": "" }, "operator": { "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.\n\nPossible enum values:\n - `\"DoesNotExist\"`\n - `\"Exists\"`\n - `\"Gt\"`\n - `\"In\"`\n - `\"Lt\"`\n - `\"NotIn\"`", "type": "string", "default": "", "enum": [ "DoesNotExist", "Exists", "Gt", "In", "Lt", "NotIn" ] }, "values": { "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.api.core.v1.NodeSelectorTerm": { "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", "type": "object", "properties": { "matchExpressions": { "description": "A list of node selector requirements by node's labels.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorRequirement" } ] } }, "matchFields": { "description": "A list of node selector requirements by node's fields.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorRequirement" } ] } } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.ObjectFieldSelector": { "description": "ObjectFieldSelector selects an APIVersioned field of an object.", "type": "object", "required": [ "fieldPath" ], "properties": { "apiVersion": { "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", "type": "string" }, "fieldPath": { "description": "Path of the field to select in the specified API version.", "type": "string", "default": "" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.ObjectReference": { "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", "type": "object", "properties": { "apiVersion": { "description": "API version of the referent.", "type": "string" }, "fieldPath": { "description": "If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: \"spec.containers{name}\" (where \"name\" refers to the name of the container that triggered the event) or if no container name is specified \"spec.containers[2]\" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.", "type": "string" }, "kind": { "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "namespace": { "description": "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", "type": "string" }, "resourceVersion": { "description": "Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", "type": "string" }, "uid": { "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids", "type": "string" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.PersistentVolumeClaimSpec": { "description": "PersistentVolumeClaimSpec describes the common attributes of storage devices and allows a Source for provider-specific attributes", "type": "object", "properties": { "accessModes": { "description": "accessModes contains the desired access modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1", "type": "array", "items": { "type": "string", "default": "" } }, "dataSource": { "description": "dataSource field can be used to specify either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) * An existing PVC (PersistentVolumeClaim) If the provisioner or an external controller can support the specified data source, it will create a new volume based on the contents of the specified data source. When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. If the namespace is specified, then dataSourceRef will not be copied to dataSource.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.TypedLocalObjectReference" } ] }, "dataSourceRef": { "description": "dataSourceRef specifies the object from which to populate the volume with data, if a non-empty volume is desired. This may be any object from a non-empty API group (non core object) or a PersistentVolumeClaim object. When this field is specified, volume binding will only succeed if the type of the specified object matches some installed volume populator or dynamic provisioner. This field will replace the functionality of the dataSource field and as such if both fields are non-empty, they must have the same value. For backwards compatibility, when namespace isn't specified in dataSourceRef, both fields (dataSource and dataSourceRef) will be set to the same value automatically if one of them is empty and the other is non-empty. When namespace is specified in dataSourceRef, dataSource isn't set to the same value and must be empty. There are three important differences between dataSource and dataSourceRef: * While dataSource only allows two specific types of objects, dataSourceRef\n allows any non-core object, as well as PersistentVolumeClaim objects.\n* While dataSource ignores disallowed values (dropping them), dataSourceRef\n preserves all values, and generates an error if a disallowed value is\n specified.\n* While dataSource only allows local objects, dataSourceRef allows objects\n in any namespaces.\n(Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.TypedObjectReference" } ] }, "resources": { "description": "resources represents the minimum resources the volume should have. If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements that are lower than previous value but must still be higher than capacity recorded in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceRequirements" } ] }, "selector": { "description": "selector is a label query over volumes to consider for binding.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" } ] }, "storageClassName": { "description": "storageClassName is the name of the StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1", "type": "string" }, "volumeMode": { "description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.\n\nPossible enum values:\n - `\"Block\"` means the volume will not be formatted with a filesystem and will remain a raw block device.\n - `\"Filesystem\"` means the volume will be or is formatted with a filesystem.", "type": "string", "enum": [ "Block", "Filesystem" ] }, "volumeName": { "description": "volumeName is the binding reference to the PersistentVolume backing this claim.", "type": "string" } } }, "io.k8s.api.core.v1.PersistentVolumeClaimTemplate": { "description": "PersistentVolumeClaimTemplate is used to produce PersistentVolumeClaim objects as part of an EphemeralVolumeSource.", "type": "object", "required": [ "spec" ], "properties": { "metadata": { "description": "May contain labels and annotations that will be copied into the PVC when creating it. No other fields are allowed and will be rejected during validation.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" } ] }, "spec": { "description": "The specification for the PersistentVolumeClaim. The entire content is copied unchanged into the PVC that gets created from this template. The same fields as in a PersistentVolumeClaim are also valid here.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PersistentVolumeClaimSpec" } ] } } }, "io.k8s.api.core.v1.PersistentVolumeClaimVolumeSource": { "description": "PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace. This volume finds the bound PV and mounts that volume for the pod. A PersistentVolumeClaimVolumeSource is, essentially, a wrapper around another type of volume that is owned by someone else (the system).", "type": "object", "required": [ "claimName" ], "properties": { "claimName": { "description": "claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims", "type": "string", "default": "" }, "readOnly": { "description": "readOnly Will force the ReadOnly setting in VolumeMounts. Default false.", "type": "boolean" } } }, "io.k8s.api.core.v1.PhotonPersistentDiskVolumeSource": { "description": "Represents a Photon Controller persistent disk resource.", "type": "object", "required": [ "pdID" ], "properties": { "fsType": { "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", "type": "string" }, "pdID": { "description": "pdID is the ID that identifies Photon Controller persistent disk", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.PodAffinity": { "description": "Pod affinity is a group of inter pod affinity scheduling rules.", "type": "object", "properties": { "preferredDuringSchedulingIgnoredDuringExecution": { "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.WeightedPodAffinityTerm" } ] } }, "requiredDuringSchedulingIgnoredDuringExecution": { "description": "If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinityTerm" } ] } } } }, "io.k8s.api.core.v1.PodAffinityTerm": { "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key \u003ctopologyKey\u003e matches that of any node on which a pod of the set of pods is running", "type": "object", "required": [ "topologyKey" ], "properties": { "labelSelector": { "description": "A label query over a set of resources, in this case pods.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" } ] }, "namespaceSelector": { "description": "A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means \"this pod's namespace\". An empty selector ({}) matches all namespaces.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" } ] }, "namespaces": { "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", "type": "array", "items": { "type": "string", "default": "" } }, "topologyKey": { "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.PodAntiAffinity": { "description": "Pod anti affinity is a group of inter pod anti affinity scheduling rules.", "type": "object", "properties": { "preferredDuringSchedulingIgnoredDuringExecution": { "description": "The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.WeightedPodAffinityTerm" } ] } }, "requiredDuringSchedulingIgnoredDuringExecution": { "description": "If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinityTerm" } ] } } } }, "io.k8s.api.core.v1.PodDNSConfig": { "description": "PodDNSConfig defines the DNS parameters of a pod in addition to those generated from DNSPolicy.", "type": "object", "properties": { "nameservers": { "description": "A list of DNS name server IP addresses. This will be appended to the base nameservers generated from DNSPolicy. Duplicated nameservers will be removed.", "type": "array", "items": { "type": "string", "default": "" } }, "options": { "description": "A list of DNS resolver options. This will be merged with the base options generated from DNSPolicy. Duplicated entries will be removed. Resolution options given in Options will override those that appear in the base DNSPolicy.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodDNSConfigOption" } ] } }, "searches": { "description": "A list of DNS search domains for host-name lookup. This will be appended to the base search paths generated from DNSPolicy. Duplicated search paths will be removed.", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.api.core.v1.PodDNSConfigOption": { "description": "PodDNSConfigOption defines DNS resolver options of a pod.", "type": "object", "properties": { "name": { "description": "Required.", "type": "string" }, "value": { "type": "string" } } }, "io.k8s.api.core.v1.PodOS": { "description": "PodOS defines the OS parameters of a pod.", "type": "object", "required": [ "name" ], "properties": { "name": { "description": "Name is the name of the operating system. The currently supported values are linux and windows. Additional value may be defined in future and can be one of: https://github.com/opencontainers/runtime-spec/blob/master/config.md#platform-specific-configuration Clients should expect to handle additional values and treat unrecognized values in this field as os: null", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.PodReadinessGate": { "description": "PodReadinessGate contains the reference to a pod condition", "type": "object", "required": [ "conditionType" ], "properties": { "conditionType": { "description": "ConditionType refers to a condition in the pod's condition list with matching type.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.PodResourceClaim": { "description": "PodResourceClaim references exactly one ResourceClaim through a ClaimSource. It adds a name to it that uniquely identifies the ResourceClaim inside the Pod. Containers that need access to the ResourceClaim reference it with this name.", "type": "object", "required": [ "name" ], "properties": { "name": { "description": "Name uniquely identifies this resource claim inside the pod. This must be a DNS_LABEL.", "type": "string", "default": "" }, "source": { "description": "Source describes where to find the ResourceClaim.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ClaimSource" } ] } } }, "io.k8s.api.core.v1.PodSchedulingGate": { "description": "PodSchedulingGate is associated to a Pod to guard its scheduling.", "type": "object", "required": [ "name" ], "properties": { "name": { "description": "Name of the scheduling gate. Each scheduling gate must have a unique name field.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.PodSecurityContext": { "description": "PodSecurityContext holds pod-level security attributes and common container settings. Some fields are also present in container.securityContext. Field values of container.securityContext take precedence over field values of PodSecurityContext.", "type": "object", "properties": { "fsGroup": { "description": "A special supplemental group that applies to all containers in a pod. Some volume types allow the Kubelet to change the ownership of that volume to be owned by the pod:\n\n1. The owning GID will be the FSGroup 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) 3. The permission bits are OR'd with rw-rw----\n\nIf unset, the Kubelet will not modify the ownership and permissions of any volume. Note that this field cannot be set when spec.os.name is windows.", "type": "integer", "format": "int64" }, "fsGroupChangePolicy": { "description": "fsGroupChangePolicy defines behavior of changing ownership and permission of the volume before being exposed inside Pod. This field will only apply to volume types which support fsGroup based ownership(and permissions). It will have no effect on ephemeral volume types such as: secret, configmaps and emptydir. Valid values are \"OnRootMismatch\" and \"Always\". If not specified, \"Always\" is used. Note that this field cannot be set when spec.os.name is windows.\n\nPossible enum values:\n - `\"Always\"` indicates that volume's ownership and permissions should always be changed whenever volume is mounted inside a Pod. This the default behavior.\n - `\"OnRootMismatch\"` indicates that volume's ownership and permissions will be changed only when permission and ownership of root directory does not match with expected permissions on the volume. This can help shorten the time it takes to change ownership and permissions of a volume.", "type": "string", "enum": [ "Always", "OnRootMismatch" ] }, "runAsGroup": { "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.", "type": "integer", "format": "int64" }, "runAsNonRoot": { "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", "type": "boolean" }, "runAsUser": { "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.", "type": "integer", "format": "int64" }, "seLinuxOptions": { "description": "The SELinux context to be applied to all containers. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SELinuxOptions" } ] }, "seccompProfile": { "description": "The seccomp options to use by the containers in this pod. Note that this field cannot be set when spec.os.name is windows.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SeccompProfile" } ] }, "supplementalGroups": { "description": "A list of groups applied to the first process run in each container, in addition to the container's primary GID, the fsGroup (if specified), and group memberships defined in the container image for the uid of the container process. If unspecified, no additional groups are added to any container. Note that group memberships defined in the container image for the uid of the container process are still effective, even if they are not included in this list. Note that this field cannot be set when spec.os.name is windows.", "type": "array", "items": { "type": "integer", "format": "int64", "default": 0 } }, "sysctls": { "description": "Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported sysctls (by the container runtime) might fail to launch. Note that this field cannot be set when spec.os.name is windows.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Sysctl" } ] } }, "windowsOptions": { "description": "The Windows specific settings applied to all containers. If unspecified, the options within a container's SecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.WindowsSecurityContextOptions" } ] } } }, "io.k8s.api.core.v1.PodSpec": { "description": "PodSpec is a description of a pod.", "type": "object", "required": [ "containers" ], "properties": { "activeDeadlineSeconds": { "description": "Optional duration in seconds the pod may be active on the node relative to StartTime before the system will actively try to mark it failed and kill associated containers. Value must be a positive integer.", "type": "integer", "format": "int64" }, "affinity": { "description": "If specified, the pod's scheduling constraints", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Affinity" } ] }, "automountServiceAccountToken": { "description": "AutomountServiceAccountToken indicates whether a service account token should be automatically mounted.", "type": "boolean" }, "containers": { "description": "List of containers belonging to the pod. Containers cannot currently be added or removed. There must be at least one container in a Pod. Cannot be updated.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Container" } ] }, "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge" }, "dnsConfig": { "description": "Specifies the DNS parameters of a pod. Parameters specified here will be merged to the generated DNS configuration based on DNSPolicy.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodDNSConfig" } ] }, "dnsPolicy": { "description": "Set DNS policy for the pod. Defaults to \"ClusterFirst\". Valid values are 'ClusterFirstWithHostNet', 'ClusterFirst', 'Default' or 'None'. DNS parameters given in DNSConfig will be merged with the policy selected with DNSPolicy. To have DNS options set along with hostNetwork, you have to specify DNS policy explicitly to 'ClusterFirstWithHostNet'.\n\nPossible enum values:\n - `\"ClusterFirst\"` indicates that the pod should use cluster DNS first unless hostNetwork is true, if it is available, then fall back on the default (as determined by kubelet) DNS settings.\n - `\"ClusterFirstWithHostNet\"` indicates that the pod should use cluster DNS first, if it is available, then fall back on the default (as determined by kubelet) DNS settings.\n - `\"Default\"` indicates that the pod should use the default (as determined by kubelet) DNS settings.\n - `\"None\"` indicates that the pod should use empty DNS settings. DNS parameters such as nameservers and search paths should be defined via DNSConfig.", "type": "string", "enum": [ "ClusterFirst", "ClusterFirstWithHostNet", "Default", "None" ] }, "enableServiceLinks": { "description": "EnableServiceLinks indicates whether information about services should be injected into pod's environment variables, matching the syntax of Docker links. Optional: Defaults to true.", "type": "boolean" }, "ephemeralContainers": { "description": "List of ephemeral containers run in this pod. Ephemeral containers may be run in an existing pod to perform user-initiated actions such as debugging. This list cannot be specified when creating a pod, and it cannot be modified by updating the pod spec. In order to add an ephemeral container to an existing pod, use the pod's ephemeralcontainers subresource.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EphemeralContainer" } ] }, "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge" }, "hostAliases": { "description": "HostAliases is an optional list of hosts and IPs that will be injected into the pod's hosts file if specified. This is only valid for non-hostNetwork pods.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.HostAlias" } ] }, "x-kubernetes-patch-merge-key": "ip", "x-kubernetes-patch-strategy": "merge" }, "hostIPC": { "description": "Use the host's ipc namespace. Optional: Default to false.", "type": "boolean" }, "hostNetwork": { "description": "Host networking requested for this pod. Use the host's network namespace. If this option is set, the ports that will be used must be specified. Default to false.", "type": "boolean" }, "hostPID": { "description": "Use the host's pid namespace. Optional: Default to false.", "type": "boolean" }, "hostUsers": { "description": "Use the host's user namespace. Optional: Default to true. If set to true or not present, the pod will be run in the host user namespace, useful for when the pod needs a feature only available to the host user namespace, such as loading a kernel module with CAP_SYS_MODULE. When set to false, a new userns is created for the pod. Setting false is useful for mitigating container breakout vulnerabilities even allowing users to run their containers as root without actually having root privileges on the host. This field is alpha-level and is only honored by servers that enable the UserNamespacesSupport feature.", "type": "boolean" }, "hostname": { "description": "Specifies the hostname of the Pod If not specified, the pod's hostname will be set to a system-defined value.", "type": "string" }, "imagePullSecrets": { "description": "ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. If specified, these secrets will be passed to individual puller implementations for them to use. More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge" }, "initContainers": { "description": "List of initialization containers belonging to the pod. Init containers are executed in order prior to containers being started. If any init container fails, the pod is considered to have failed and is handled according to its restartPolicy. The name for an init container or normal container must be unique among all containers. Init containers may not have Lifecycle actions, Readiness probes, Liveness probes, or Startup probes. The resourceRequirements of an init container are taken into account during scheduling by finding the highest request/limit for each resource type, and then using the max of that value or the sum of the normal containers. Limits are applied to init containers in a similar fashion. Init containers cannot currently be added or removed. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Container" } ] }, "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge" }, "nodeName": { "description": "NodeName is a request to schedule this pod onto a specific node. If it is non-empty, the scheduler simply schedules this pod onto that node, assuming that it fits resource requirements.", "type": "string" }, "nodeSelector": { "description": "NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node's labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/", "type": "object", "additionalProperties": { "type": "string", "default": "" }, "x-kubernetes-map-type": "atomic" }, "os": { "description": "Specifies the OS of the containers in the pod. Some pod and container fields are restricted if this is set.\n\nIf the OS field is set to linux, the following fields must be unset: -securityContext.windowsOptions\n\nIf the OS field is set to windows, following fields must be unset: - spec.hostPID - spec.hostIPC - spec.hostUsers - spec.securityContext.seLinuxOptions - spec.securityContext.seccompProfile - spec.securityContext.fsGroup - spec.securityContext.fsGroupChangePolicy - spec.securityContext.sysctls - spec.shareProcessNamespace - spec.securityContext.runAsUser - spec.securityContext.runAsGroup - spec.securityContext.supplementalGroups - spec.containers[*].securityContext.seLinuxOptions - spec.containers[*].securityContext.seccompProfile - spec.containers[*].securityContext.capabilities - spec.containers[*].securityContext.readOnlyRootFilesystem - spec.containers[*].securityContext.privileged - spec.containers[*].securityContext.allowPrivilegeEscalation - spec.containers[*].securityContext.procMount - spec.containers[*].securityContext.runAsUser - spec.containers[*].securityContext.runAsGroup", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodOS" } ] }, "overhead": { "description": "Overhead represents the resource overhead associated with running a pod for a given RuntimeClass. This field will be autopopulated at admission time by the RuntimeClass admission controller. If the RuntimeClass admission controller is enabled, overhead must not be set in Pod create requests. The RuntimeClass admission controller will reject Pod create requests which have the overhead already set. If RuntimeClass is configured and selected in the PodSpec, Overhead will be set to the value defined in the corresponding RuntimeClass, otherwise it will remain unset and treated as zero. More info: https://git.k8s.io/enhancements/keps/sig-node/688-pod-overhead/README.md", "type": "object", "additionalProperties": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" } ] } }, "preemptionPolicy": { "description": "PreemptionPolicy is the Policy for preempting pods with lower priority. One of Never, PreemptLowerPriority. Defaults to PreemptLowerPriority if unset.\n\nPossible enum values:\n - `\"Never\"` means that pod never preempts other pods with lower priority.\n - `\"PreemptLowerPriority\"` means that pod can preempt other pods with lower priority.", "type": "string", "enum": [ "Never", "PreemptLowerPriority" ] }, "priority": { "description": "The priority value. Various system components use this field to find the priority of the pod. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority.", "type": "integer", "format": "int32" }, "priorityClassName": { "description": "If specified, indicates the pod's priority. \"system-node-critical\" and \"system-cluster-critical\" are two special keywords which indicate the highest priorities with the former being the highest priority. Any other name must be defined by creating a PriorityClass object with that name. If not specified, the pod priority will be default or zero if there is no default.", "type": "string" }, "readinessGates": { "description": "If specified, all readiness gates will be evaluated for pod readiness. A pod is ready when all its containers are ready AND all conditions specified in the readiness gates have status equal to \"True\" More info: https://git.k8s.io/enhancements/keps/sig-network/580-pod-readiness-gates", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodReadinessGate" } ] } }, "resourceClaims": { "description": "ResourceClaims defines which ResourceClaims must be allocated and reserved before the Pod is allowed to start. The resources will be made available to those containers which consume them by name.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodResourceClaim" } ] }, "x-kubernetes-list-map-keys": [ "name" ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge,retainKeys" }, "restartPolicy": { "description": "Restart policy for all containers within the pod. One of Always, OnFailure, Never. In some contexts, only a subset of those values may be permitted. Default to Always. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy\n\nPossible enum values:\n - `\"Always\"`\n - `\"Never\"`\n - `\"OnFailure\"`", "type": "string", "enum": [ "Always", "Never", "OnFailure" ] }, "runtimeClassName": { "description": "RuntimeClassName refers to a RuntimeClass object in the node.k8s.io group, which should be used to run this pod. If no RuntimeClass resource matches the named class, the pod will not be run. If unset or empty, the \"legacy\" RuntimeClass will be used, which is an implicit class with an empty definition that uses the default runtime handler. More info: https://git.k8s.io/enhancements/keps/sig-node/585-runtime-class", "type": "string" }, "schedulerName": { "description": "If specified, the pod will be dispatched by specified scheduler. If not specified, the pod will be dispatched by default scheduler.", "type": "string" }, "schedulingGates": { "description": "SchedulingGates is an opaque list of values that if specified will block scheduling the pod. If schedulingGates is not empty, the pod will stay in the SchedulingGated state and the scheduler will not attempt to schedule the pod.\n\nSchedulingGates can only be set at pod creation time, and be removed only afterwards.\n\nThis is a beta feature enabled by the PodSchedulingReadiness feature gate.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodSchedulingGate" } ] }, "x-kubernetes-list-map-keys": [ "name" ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge" }, "securityContext": { "description": "SecurityContext holds pod-level security attributes and common container settings. Optional: Defaults to empty. See type description for default values of each field.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodSecurityContext" } ] }, "serviceAccount": { "description": "DeprecatedServiceAccount is a deprecated alias for ServiceAccountName. Deprecated: Use serviceAccountName instead.", "type": "string" }, "serviceAccountName": { "description": "ServiceAccountName is the name of the ServiceAccount to use to run this pod. More info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/", "type": "string" }, "setHostnameAsFQDN": { "description": "If true the pod's hostname will be configured as the pod's FQDN, rather than the leaf name (the default). In Linux containers, this means setting the FQDN in the hostname field of the kernel (the nodename field of struct utsname). In Windows containers, this means setting the registry value of hostname for the registry key HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters to FQDN. If a pod does not have FQDN, this has no effect. Default to false.", "type": "boolean" }, "shareProcessNamespace": { "description": "Share a single process namespace between all of the containers in a pod. When this is set containers will be able to view and signal processes from other containers in the same pod, and the first process in each container will not be assigned PID 1. HostPID and ShareProcessNamespace cannot both be set. Optional: Default to false.", "type": "boolean" }, "subdomain": { "description": "If specified, the fully qualified Pod hostname will be \"\u003chostname\u003e.\u003csubdomain\u003e.\u003cpod namespace\u003e.svc.\u003ccluster domain\u003e\". If not specified, the pod will not have a domainname at all.", "type": "string" }, "terminationGracePeriodSeconds": { "description": "Optional duration in seconds the pod needs to terminate gracefully. May be decreased in delete request. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). If this value is nil, the default grace period will be used instead. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. Defaults to 30 seconds.", "type": "integer", "format": "int64" }, "tolerations": { "description": "If specified, the pod's tolerations.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Toleration" } ] } }, "topologySpreadConstraints": { "description": "TopologySpreadConstraints describes how a group of pods ought to spread across topology domains. Scheduler will schedule pods in a way which abides by the constraints. All topologySpreadConstraints are ANDed.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.TopologySpreadConstraint" } ] }, "x-kubernetes-list-map-keys": [ "topologyKey", "whenUnsatisfiable" ], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "topologyKey", "x-kubernetes-patch-strategy": "merge" }, "volumes": { "description": "List of volumes that can be mounted by containers belonging to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Volume" } ] }, "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge,retainKeys" } } }, "io.k8s.api.core.v1.PodTemplateSpec": { "description": "PodTemplateSpec describes the data a pod should have when created from a template", "type": "object", "properties": { "metadata": { "description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" } ] }, "spec": { "description": "Specification of the desired behavior of the pod. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodSpec" } ] } } }, "io.k8s.api.core.v1.PortworxVolumeSource": { "description": "PortworxVolumeSource represents a Portworx volume resource.", "type": "object", "required": [ "volumeID" ], "properties": { "fsType": { "description": "fSType represents the filesystem type to mount Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\". Implicitly inferred to be \"ext4\" if unspecified.", "type": "string" }, "readOnly": { "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", "type": "boolean" }, "volumeID": { "description": "volumeID uniquely identifies a Portworx volume", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.PreferredSchedulingTerm": { "description": "An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).", "type": "object", "required": [ "weight", "preference" ], "properties": { "preference": { "description": "A node selector term, associated with the corresponding weight.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorTerm" } ] }, "weight": { "description": "Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.", "type": "integer", "format": "int32", "default": 0 } } }, "io.k8s.api.core.v1.Probe": { "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", "type": "object", "properties": { "exec": { "description": "Exec specifies the action to take.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ExecAction" } ] }, "failureThreshold": { "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", "type": "integer", "format": "int32" }, "grpc": { "description": "GRPC specifies an action involving a GRPC port.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.GRPCAction" } ] }, "httpGet": { "description": "HTTPGet specifies the http request to perform.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.HTTPGetAction" } ] }, "initialDelaySeconds": { "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", "type": "integer", "format": "int32" }, "periodSeconds": { "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", "type": "integer", "format": "int32" }, "successThreshold": { "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", "type": "integer", "format": "int32" }, "tcpSocket": { "description": "TCPSocket specifies an action involving a TCP port.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.TCPSocketAction" } ] }, "terminationGracePeriodSeconds": { "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", "type": "integer", "format": "int64" }, "timeoutSeconds": { "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", "type": "integer", "format": "int32" } } }, "io.k8s.api.core.v1.ProjectedVolumeSource": { "description": "Represents a projected volume source", "type": "object", "properties": { "defaultMode": { "description": "defaultMode are the mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", "type": "integer", "format": "int32" }, "sources": { "description": "sources is the list of volume projections", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeProjection" } ] } } } }, "io.k8s.api.core.v1.QuobyteVolumeSource": { "description": "Represents a Quobyte mount that lasts the lifetime of a pod. Quobyte volumes do not support ownership management or SELinux relabeling.", "type": "object", "required": [ "registry", "volume" ], "properties": { "group": { "description": "group to map volume access to Default is no group", "type": "string" }, "readOnly": { "description": "readOnly here will force the Quobyte volume to be mounted with read-only permissions. Defaults to false.", "type": "boolean" }, "registry": { "description": "registry represents a single or multiple Quobyte Registry services specified as a string as host:port pair (multiple entries are separated with commas) which acts as the central registry for volumes", "type": "string", "default": "" }, "tenant": { "description": "tenant owning the given Quobyte volume in the Backend Used with dynamically provisioned Quobyte volumes, value is set by the plugin", "type": "string" }, "user": { "description": "user to map volume access to Defaults to serivceaccount user", "type": "string" }, "volume": { "description": "volume is a string that references an already created Quobyte volume by name.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.RBDVolumeSource": { "description": "Represents a Rados Block Device mount that lasts the lifetime of a pod. RBD volumes support ownership management and SELinux relabeling.", "type": "object", "required": [ "monitors", "image" ], "properties": { "fsType": { "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd", "type": "string" }, "image": { "description": "image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", "type": "string", "default": "" }, "keyring": { "description": "keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", "type": "string" }, "monitors": { "description": "monitors is a collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", "type": "array", "items": { "type": "string", "default": "" } }, "pool": { "description": "pool is the rados pool name. Default is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", "type": "string" }, "readOnly": { "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", "type": "boolean" }, "secretRef": { "description": "secretRef is name of the authentication secret for RBDUser. If provided overrides keyring. Default is nil. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "user": { "description": "user is the rados user name. Default is admin. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", "type": "string" } } }, "io.k8s.api.core.v1.ResourceClaim": { "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", "type": "object", "required": [ "name" ], "properties": { "name": { "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.ResourceFieldSelector": { "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", "type": "object", "required": [ "resource" ], "properties": { "containerName": { "description": "Container name: required for volumes, optional for env vars", "type": "string" }, "divisor": { "description": "Specifies the output format of the exposed resources, defaults to \"1\"", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" } ] }, "resource": { "description": "Required: resource to select", "type": "string", "default": "" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.ResourceRequirements": { "description": "ResourceRequirements describes the compute resource requirements.", "type": "object", "properties": { "claims": { "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceClaim" } ] }, "x-kubernetes-list-map-keys": [ "name" ], "x-kubernetes-list-type": "map" }, "limits": { "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", "type": "object", "additionalProperties": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" } ] } }, "requests": { "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", "type": "object", "additionalProperties": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" } ] } } } }, "io.k8s.api.core.v1.SELinuxOptions": { "description": "SELinuxOptions are the labels to be applied to the container", "type": "object", "properties": { "level": { "description": "Level is SELinux level label that applies to the container.", "type": "string" }, "role": { "description": "Role is a SELinux role label that applies to the container.", "type": "string" }, "type": { "description": "Type is a SELinux type label that applies to the container.", "type": "string" }, "user": { "description": "User is a SELinux user label that applies to the container.", "type": "string" } } }, "io.k8s.api.core.v1.ScaleIOVolumeSource": { "description": "ScaleIOVolumeSource represents a persistent ScaleIO volume", "type": "object", "required": [ "gateway", "system", "secretRef" ], "properties": { "fsType": { "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Default is \"xfs\".", "type": "string" }, "gateway": { "description": "gateway is the host address of the ScaleIO API Gateway.", "type": "string", "default": "" }, "protectionDomain": { "description": "protectionDomain is the name of the ScaleIO Protection Domain for the configured storage.", "type": "string" }, "readOnly": { "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", "type": "boolean" }, "secretRef": { "description": "secretRef references to the secret for ScaleIO user and other sensitive information. If this is not provided, Login operation will fail.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "sslEnabled": { "description": "sslEnabled Flag enable/disable SSL communication with Gateway, default false", "type": "boolean" }, "storageMode": { "description": "storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned.", "type": "string" }, "storagePool": { "description": "storagePool is the ScaleIO Storage Pool associated with the protection domain.", "type": "string" }, "system": { "description": "system is the name of the storage system as configured in ScaleIO.", "type": "string", "default": "" }, "volumeName": { "description": "volumeName is the name of a volume already created in the ScaleIO system that is associated with this volume source.", "type": "string" } } }, "io.k8s.api.core.v1.SeccompProfile": { "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", "type": "object", "required": [ "type" ], "properties": { "localhostProfile": { "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must only be set if type is \"Localhost\".", "type": "string" }, "type": { "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.\n\nPossible enum values:\n - `\"Localhost\"` indicates a profile defined in a file on the node should be used. The file's location relative to \u003ckubelet-root-dir\u003e/seccomp.\n - `\"RuntimeDefault\"` represents the default container runtime seccomp profile.\n - `\"Unconfined\"` indicates no seccomp profile is applied (A.K.A. unconfined).", "type": "string", "default": "", "enum": [ "Localhost", "RuntimeDefault", "Unconfined" ] } }, "x-kubernetes-unions": [ { "discriminator": "type", "fields-to-discriminateBy": { "localhostProfile": "LocalhostProfile" } } ] }, "io.k8s.api.core.v1.SecretEnvSource": { "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", "type": "object", "properties": { "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "optional": { "description": "Specify whether the Secret must be defined", "type": "boolean" } } }, "io.k8s.api.core.v1.SecretKeySelector": { "description": "SecretKeySelector selects a key of a Secret.", "type": "object", "required": [ "key" ], "properties": { "key": { "description": "The key of the secret to select from. Must be a valid secret key.", "type": "string", "default": "" }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "optional": { "description": "Specify whether the Secret or its key must be defined", "type": "boolean" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.SecretProjection": { "description": "Adapts a secret into a projected volume.\n\nThe contents of the target Secret's Data field will be presented in a projected volume as files using the keys in the Data field as the file names. Note that this is identical to a secret volume source without the default mode.", "type": "object", "properties": { "items": { "description": "items if unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" } ] } }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string" }, "optional": { "description": "optional field specify whether the Secret or its key must be defined", "type": "boolean" } } }, "io.k8s.api.core.v1.SecretVolumeSource": { "description": "Adapts a Secret into a volume.\n\nThe contents of the target Secret's Data field will be presented in a volume as files using the keys in the Data field as the file names. Secret volumes support ownership management and SELinux relabeling.", "type": "object", "properties": { "defaultMode": { "description": "defaultMode is Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", "type": "integer", "format": "int32" }, "items": { "description": "items If unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" } ] } }, "optional": { "description": "optional field specify whether the Secret or its keys must be defined", "type": "boolean" }, "secretName": { "description": "secretName is the name of the secret in the pod's namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret", "type": "string" } } }, "io.k8s.api.core.v1.SecurityContext": { "description": "SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.", "type": "object", "properties": { "allowPrivilegeEscalation": { "description": "AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.", "type": "boolean" }, "capabilities": { "description": "The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.Capabilities" } ] }, "privileged": { "description": "Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.", "type": "boolean" }, "procMount": { "description": "procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.\n\nPossible enum values:\n - `\"Default\"` uses the container runtime defaults for readonly and masked paths for /proc. Most container runtimes mask certain paths in /proc to avoid accidental security exposure of special devices or information.\n - `\"Unmasked\"` bypasses the default masking behavior of the container runtime and ensures the newly created /proc the container stays in tact with no modifications.", "type": "string", "enum": [ "Default", "Unmasked" ] }, "readOnlyRootFilesystem": { "description": "Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.", "type": "boolean" }, "runAsGroup": { "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", "type": "integer", "format": "int64" }, "runAsNonRoot": { "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", "type": "boolean" }, "runAsUser": { "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", "type": "integer", "format": "int64" }, "seLinuxOptions": { "description": "The SELinux context to be applied to the container. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SELinuxOptions" } ] }, "seccompProfile": { "description": "The seccomp options to use by this container. If seccomp options are provided at both the pod \u0026 container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SeccompProfile" } ] }, "windowsOptions": { "description": "The Windows specific settings applied to all containers. If unspecified, the options from the PodSecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.WindowsSecurityContextOptions" } ] } } }, "io.k8s.api.core.v1.ServiceAccountTokenProjection": { "description": "ServiceAccountTokenProjection represents a projected service account token volume. This projection can be used to insert a service account token into the pods runtime filesystem for use against APIs (Kubernetes API Server or otherwise).", "type": "object", "required": [ "path" ], "properties": { "audience": { "description": "audience is the intended audience of the token. A recipient of a token must identify itself with an identifier specified in the audience of the token, and otherwise should reject the token. The audience defaults to the identifier of the apiserver.", "type": "string" }, "expirationSeconds": { "description": "expirationSeconds is the requested duration of validity of the service account token. As the token approaches expiration, the kubelet volume plugin will proactively rotate the service account token. The kubelet will start trying to rotate the token if the token is older than 80 percent of its time to live or if the token is older than 24 hours.Defaults to 1 hour and must be at least 10 minutes.", "type": "integer", "format": "int64" }, "path": { "description": "path is the path relative to the mount point of the file to project the token into.", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.StorageOSVolumeSource": { "description": "Represents a StorageOS persistent volume resource.", "type": "object", "properties": { "fsType": { "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", "type": "string" }, "readOnly": { "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", "type": "boolean" }, "secretRef": { "description": "secretRef specifies the secret to use for obtaining the StorageOS API credentials. If not specified, default values will be attempted.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" } ] }, "volumeName": { "description": "volumeName is the human-readable name of the StorageOS volume. Volume names are only unique within a namespace.", "type": "string" }, "volumeNamespace": { "description": "volumeNamespace specifies the scope of the volume within StorageOS. If no namespace is specified then the Pod's namespace will be used. This allows the Kubernetes name scoping to be mirrored within StorageOS for tighter integration. Set VolumeName to any name to override the default behaviour. Set to \"default\" if you are not using namespaces within StorageOS. Namespaces that do not pre-exist within StorageOS will be created.", "type": "string" } } }, "io.k8s.api.core.v1.Sysctl": { "description": "Sysctl defines a kernel parameter to be set", "type": "object", "required": [ "name", "value" ], "properties": { "name": { "description": "Name of a property to set", "type": "string", "default": "" }, "value": { "description": "Value of a property to set", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.TCPSocketAction": { "description": "TCPSocketAction describes an action based on opening a socket", "type": "object", "required": [ "port" ], "properties": { "host": { "description": "Optional: Host name to connect to, defaults to the pod IP.", "type": "string" }, "port": { "description": "Number or name of the port to access on the container. Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.util.intstr.IntOrString" } ] } } }, "io.k8s.api.core.v1.Toleration": { "description": "The pod this Toleration is attached to tolerates any taint that matches the triple \u003ckey,value,effect\u003e using the matching operator \u003coperator\u003e.", "type": "object", "properties": { "effect": { "description": "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.\n\nPossible enum values:\n - `\"NoExecute\"` Evict any already-running pods that do not tolerate the taint. Currently enforced by NodeController.\n - `\"NoSchedule\"` Do not allow new pods to schedule onto the node unless they tolerate the taint, but allow all pods submitted to Kubelet without going through the scheduler to start, and allow all already-running pods to continue running. Enforced by the scheduler.\n - `\"PreferNoSchedule\"` Like TaintEffectNoSchedule, but the scheduler tries not to schedule new pods onto the node, rather than prohibiting new pods from scheduling onto the node entirely. Enforced by the scheduler.", "type": "string", "enum": [ "NoExecute", "NoSchedule", "PreferNoSchedule" ] }, "key": { "description": "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.", "type": "string" }, "operator": { "description": "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.\n\nPossible enum values:\n - `\"Equal\"`\n - `\"Exists\"`", "type": "string", "enum": [ "Equal", "Exists" ] }, "tolerationSeconds": { "description": "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.", "type": "integer", "format": "int64" }, "value": { "description": "Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.", "type": "string" } } }, "io.k8s.api.core.v1.TopologySpreadConstraint": { "description": "TopologySpreadConstraint specifies how to spread matching pods among the given topology.", "type": "object", "required": [ "maxSkew", "topologyKey", "whenUnsatisfiable" ], "properties": { "labelSelector": { "description": "LabelSelector is used to find matching pods. Pods that match this label selector are counted to determine the number of pods in their corresponding topology domain.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" } ] }, "matchLabelKeys": { "description": "MatchLabelKeys is a set of pod label keys to select the pods over which spreading will be calculated. The keys are used to lookup values from the incoming pod labels, those key-value labels are ANDed with labelSelector to select the group of existing pods over which spreading will be calculated for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. MatchLabelKeys cannot be set when LabelSelector isn't set. Keys that don't exist in the incoming pod labels will be ignored. A null or empty list means only match against labelSelector.\n\nThis is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default).", "type": "array", "items": { "type": "string", "default": "" }, "x-kubernetes-list-type": "atomic" }, "maxSkew": { "description": "MaxSkew describes the degree to which pods may be unevenly distributed. When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference between the number of matching pods in the target topology and the global minimum. The global minimum is the minimum number of matching pods in an eligible domain or zero if the number of eligible domains is less than MinDomains. For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same labelSelector spread as 2/2/1: In this case, the global minimum is 1. | zone1 | zone2 | zone3 | | P P | P P | P | - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) violate MaxSkew(1). - if MaxSkew is 2, incoming pod can be scheduled onto any zone. When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence to topologies that satisfy it. It's a required field. Default value is 1 and 0 is not allowed.", "type": "integer", "format": "int32", "default": 0 }, "minDomains": { "description": "MinDomains indicates a minimum number of eligible domains. When the number of eligible domains with matching topology keys is less than minDomains, Pod Topology Spread treats \"global minimum\" as 0, and then the calculation of Skew is performed. And when the number of eligible domains with matching topology keys equals or greater than minDomains, this value has no effect on scheduling. As a result, when the number of eligible domains is less than minDomains, scheduler won't schedule more than maxSkew Pods to those domains. If value is nil, the constraint behaves as if MinDomains is equal to 1. Valid values are integers greater than 0. When value is not nil, WhenUnsatisfiable must be DoNotSchedule.\n\nFor example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same labelSelector spread as 2/2/2: | zone1 | zone2 | zone3 | | P P | P P | P P | The number of domains is less than 5(MinDomains), so \"global minimum\" is treated as 0. In this situation, new pod with the same labelSelector cannot be scheduled, because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, it will violate MaxSkew.\n\nThis is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default).", "type": "integer", "format": "int32" }, "nodeAffinityPolicy": { "description": "NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector when calculating pod topology spread skew. Options are: - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations.\n\nIf this value is nil, the behavior is equivalent to the Honor policy. This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.\n\nPossible enum values:\n - `\"Honor\"` means use this scheduling directive when calculating pod topology spread skew.\n - `\"Ignore\"` means ignore this scheduling directive when calculating pod topology spread skew.", "type": "string", "enum": [ "Honor", "Ignore" ] }, "nodeTaintsPolicy": { "description": "NodeTaintsPolicy indicates how we will treat node taints when calculating pod topology spread skew. Options are: - Honor: nodes without taints, along with tainted nodes for which the incoming pod has a toleration, are included. - Ignore: node taints are ignored. All nodes are included.\n\nIf this value is nil, the behavior is equivalent to the Ignore policy. This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.\n\nPossible enum values:\n - `\"Honor\"` means use this scheduling directive when calculating pod topology spread skew.\n - `\"Ignore\"` means ignore this scheduling directive when calculating pod topology spread skew.", "type": "string", "enum": [ "Honor", "Ignore" ] }, "topologyKey": { "description": "TopologyKey is the key of node labels. Nodes that have a label with this key and identical values are considered to be in the same topology. We consider each \u003ckey, value\u003e as a \"bucket\", and try to put balanced number of pods into each bucket. We define a domain as a particular instance of a topology. Also, we define an eligible domain as a domain whose nodes meet the requirements of nodeAffinityPolicy and nodeTaintsPolicy. e.g. If TopologyKey is \"kubernetes.io/hostname\", each Node is a domain of that topology. And, if TopologyKey is \"topology.kubernetes.io/zone\", each zone is a domain of that topology. It's a required field.", "type": "string", "default": "" }, "whenUnsatisfiable": { "description": "WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - DoNotSchedule (default) tells the scheduler not to schedule it. - ScheduleAnyway tells the scheduler to schedule the pod in any location,\n but giving higher precedence to topologies that would help reduce the\n skew.\nA constraint is considered \"Unsatisfiable\" for an incoming pod if and only if every possible node assignment for that pod would violate \"MaxSkew\" on some topology. For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same labelSelector spread as 3/1/1: | zone1 | zone2 | zone3 | | P P P | P | P | If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler won't make it *more* imbalanced. It's a required field.\n\nPossible enum values:\n - `\"DoNotSchedule\"` instructs the scheduler not to schedule the pod when constraints are not satisfied.\n - `\"ScheduleAnyway\"` instructs the scheduler to schedule the pod even if constraints are not satisfied.", "type": "string", "default": "", "enum": [ "DoNotSchedule", "ScheduleAnyway" ] } } }, "io.k8s.api.core.v1.TypedLocalObjectReference": { "description": "TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace.", "type": "object", "required": [ "kind", "name" ], "properties": { "apiGroup": { "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", "type": "string" }, "kind": { "description": "Kind is the type of resource being referenced", "type": "string", "default": "" }, "name": { "description": "Name is the name of resource being referenced", "type": "string", "default": "" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.api.core.v1.TypedObjectReference": { "type": "object", "required": [ "kind", "name" ], "properties": { "apiGroup": { "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", "type": "string" }, "kind": { "description": "Kind is the type of resource being referenced", "type": "string", "default": "" }, "name": { "description": "Name is the name of resource being referenced", "type": "string", "default": "" }, "namespace": { "description": "Namespace is the namespace of resource being referenced Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled.", "type": "string" } } }, "io.k8s.api.core.v1.Volume": { "description": "Volume represents a named volume in a pod that may be accessed by any container in the pod.", "type": "object", "required": [ "name" ], "properties": { "awsElasticBlockStore": { "description": "awsElasticBlockStore represents an AWS Disk resource that is attached to a kubelet's host machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.AWSElasticBlockStoreVolumeSource" } ] }, "azureDisk": { "description": "azureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.AzureDiskVolumeSource" } ] }, "azureFile": { "description": "azureFile represents an Azure File Service mount on the host and bind mount to the pod.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.AzureFileVolumeSource" } ] }, "cephfs": { "description": "cephFS represents a Ceph FS mount on the host that shares a pod's lifetime", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.CephFSVolumeSource" } ] }, "cinder": { "description": "cinder represents a cinder volume attached and mounted on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.CinderVolumeSource" } ] }, "configMap": { "description": "configMap represents a configMap that should populate this volume", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapVolumeSource" } ] }, "csi": { "description": "csi (Container Storage Interface) represents ephemeral storage that is handled by certain external CSI drivers (Beta feature).", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.CSIVolumeSource" } ] }, "downwardAPI": { "description": "downwardAPI represents downward API about the pod that should populate this volume", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIVolumeSource" } ] }, "emptyDir": { "description": "emptyDir represents a temporary directory that shares a pod's lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EmptyDirVolumeSource" } ] }, "ephemeral": { "description": "ephemeral represents a volume that is handled by a cluster storage driver. The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, and deleted when the pod is removed.\n\nUse this if: a) the volume is only needed while the pod runs, b) features of normal volumes like restoring from snapshot or capacity\n tracking are needed,\nc) the storage driver is specified through a storage class, and d) the storage driver supports dynamic volume provisioning through\n a PersistentVolumeClaim (see EphemeralVolumeSource for more\n information on the connection between this volume type\n and PersistentVolumeClaim).\n\nUse PersistentVolumeClaim or one of the vendor-specific APIs for volumes that persist for longer than the lifecycle of an individual pod.\n\nUse CSI for light-weight local ephemeral volumes if the CSI driver is meant to be used that way - see the documentation of the driver for more information.\n\nA pod can use both types of ephemeral volumes and persistent volumes at the same time.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.EphemeralVolumeSource" } ] }, "fc": { "description": "fc represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.FCVolumeSource" } ] }, "flexVolume": { "description": "flexVolume represents a generic volume resource that is provisioned/attached using an exec based plugin.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.FlexVolumeSource" } ] }, "flocker": { "description": "flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.FlockerVolumeSource" } ] }, "gcePersistentDisk": { "description": "gcePersistentDisk represents a GCE Disk resource that is attached to a kubelet's host machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.GCEPersistentDiskVolumeSource" } ] }, "gitRepo": { "description": "gitRepo represents a git repository at a particular revision. DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir into the Pod's container.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.GitRepoVolumeSource" } ] }, "glusterfs": { "description": "glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.GlusterfsVolumeSource" } ] }, "hostPath": { "description": "hostPath represents a pre-existing file or directory on the host machine that is directly exposed to the container. This is generally used for system agents or other privileged things that are allowed to see the host machine. Most containers will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.HostPathVolumeSource" } ] }, "iscsi": { "description": "iscsi represents an ISCSI Disk resource that is attached to a kubelet's host machine and then exposed to the pod. More info: https://examples.k8s.io/volumes/iscsi/README.md", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ISCSIVolumeSource" } ] }, "name": { "description": "name of the volume. Must be a DNS_LABEL and unique within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", "type": "string", "default": "" }, "nfs": { "description": "nfs represents an NFS mount on the host that shares a pod's lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.NFSVolumeSource" } ] }, "persistentVolumeClaim": { "description": "persistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PersistentVolumeClaimVolumeSource" } ] }, "photonPersistentDisk": { "description": "photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host machine", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PhotonPersistentDiskVolumeSource" } ] }, "portworxVolume": { "description": "portworxVolume represents a portworx volume attached and mounted on kubelets host machine", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PortworxVolumeSource" } ] }, "projected": { "description": "projected items for all in one resources secrets, configmaps, and downward API", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ProjectedVolumeSource" } ] }, "quobyte": { "description": "quobyte represents a Quobyte mount on the host that shares a pod's lifetime", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.QuobyteVolumeSource" } ] }, "rbd": { "description": "rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.RBDVolumeSource" } ] }, "scaleIO": { "description": "scaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ScaleIOVolumeSource" } ] }, "secret": { "description": "secret represents a secret that should populate this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretVolumeSource" } ] }, "storageos": { "description": "storageOS represents a StorageOS volume attached and mounted on Kubernetes nodes.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.StorageOSVolumeSource" } ] }, "vsphereVolume": { "description": "vsphereVolume represents a vSphere volume attached and mounted on kubelets host machine", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.VsphereVirtualDiskVolumeSource" } ] } } }, "io.k8s.api.core.v1.VolumeDevice": { "description": "volumeDevice describes a mapping of a raw block device within a container.", "type": "object", "required": [ "name", "devicePath" ], "properties": { "devicePath": { "description": "devicePath is the path inside of the container that the device will be mapped to.", "type": "string", "default": "" }, "name": { "description": "name must match the name of a persistentVolumeClaim in the pod", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.VolumeMount": { "description": "VolumeMount describes a mounting of a Volume within a container.", "type": "object", "required": [ "name", "mountPath" ], "properties": { "mountPath": { "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", "type": "string", "default": "" }, "mountPropagation": { "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.\n\nPossible enum values:\n - `\"Bidirectional\"` means that the volume in a container will receive new mounts from the host or other containers, and its own mounts will be propagated from the container to the host or other containers. Note that this mode is recursively applied to all mounts in the volume (\"rshared\" in Linux terminology).\n - `\"HostToContainer\"` means that the volume in a container will receive new mounts from the host or other containers, but filesystems mounted inside the container won't be propagated to the host or other containers. Note that this mode is recursively applied to all mounts in the volume (\"rslave\" in Linux terminology).\n - `\"None\"` means that the volume in a container will not receive new mounts from the host or other containers, and filesystems mounted inside the container won't be propagated to the host or other containers. Note that this mode corresponds to \"private\" in Linux terminology.", "type": "string", "enum": [ "Bidirectional", "HostToContainer", "None" ] }, "name": { "description": "This must match the Name of a Volume.", "type": "string", "default": "" }, "readOnly": { "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", "type": "boolean" }, "subPath": { "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", "type": "string" }, "subPathExpr": { "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", "type": "string" } } }, "io.k8s.api.core.v1.VolumeProjection": { "description": "Projection that may be projected along with other supported volume types", "type": "object", "properties": { "configMap": { "description": "configMap information about the configMap data to project", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapProjection" } ] }, "downwardAPI": { "description": "downwardAPI information about the downwardAPI data to project", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIProjection" } ] }, "secret": { "description": "secret information about the secret data to project", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretProjection" } ] }, "serviceAccountToken": { "description": "serviceAccountToken is information about the serviceAccountToken data to project", "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.ServiceAccountTokenProjection" } ] } } }, "io.k8s.api.core.v1.VsphereVirtualDiskVolumeSource": { "description": "Represents a vSphere volume resource.", "type": "object", "required": [ "volumePath" ], "properties": { "fsType": { "description": "fsType is filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", "type": "string" }, "storagePolicyID": { "description": "storagePolicyID is the storage Policy Based Management (SPBM) profile ID associated with the StoragePolicyName.", "type": "string" }, "storagePolicyName": { "description": "storagePolicyName is the storage Policy Based Management (SPBM) profile name.", "type": "string" }, "volumePath": { "description": "volumePath is the path that identifies vSphere volume vmdk", "type": "string", "default": "" } } }, "io.k8s.api.core.v1.WeightedPodAffinityTerm": { "description": "The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)", "type": "object", "required": [ "weight", "podAffinityTerm" ], "properties": { "podAffinityTerm": { "description": "Required. A pod affinity term, associated with the corresponding weight.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinityTerm" } ] }, "weight": { "description": "weight associated with matching the corresponding podAffinityTerm, in the range 1-100.", "type": "integer", "format": "int32", "default": 0 } } }, "io.k8s.api.core.v1.WindowsSecurityContextOptions": { "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", "type": "object", "properties": { "gmsaCredentialSpec": { "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", "type": "string" }, "gmsaCredentialSpecName": { "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", "type": "string" }, "hostProcess": { "description": "HostProcess determines if a container should be run as a 'Host Process' container. This field is alpha-level and will only be honored by components that enable the WindowsHostProcessContainers feature flag. Setting this field without the feature flag will result in errors when validating the Pod. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", "type": "boolean" }, "runAsUserName": { "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", "type": "string" } } }, "io.k8s.apimachinery.pkg.api.resource.Quantity": { "description": "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` \u003cquantity\u003e ::= \u003csignedNumber\u003e\u003csuffix\u003e\n\n\t(Note that \u003csuffix\u003e may be empty, from the \"\" case in \u003cdecimalSI\u003e.)\n\n\u003cdigit\u003e ::= 0 | 1 | ... | 9 \u003cdigits\u003e ::= \u003cdigit\u003e | \u003cdigit\u003e\u003cdigits\u003e \u003cnumber\u003e ::= \u003cdigits\u003e | \u003cdigits\u003e.\u003cdigits\u003e | \u003cdigits\u003e. | .\u003cdigits\u003e \u003csign\u003e ::= \"+\" | \"-\" \u003csignedNumber\u003e ::= \u003cnumber\u003e | \u003csign\u003e\u003cnumber\u003e \u003csuffix\u003e ::= \u003cbinarySI\u003e | \u003cdecimalExponent\u003e | \u003cdecimalSI\u003e \u003cbinarySI\u003e ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n\u003cdecimalSI\u003e ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n\u003cdecimalExponent\u003e ::= \"e\" \u003csignedNumber\u003e | \"E\" \u003csignedNumber\u003e ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation.", "oneOf": [ { "type": "string" }, { "type": "number" } ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", "required": [ "name", "singularName", "namespaced", "kind", "verbs" ], "properties": { "categories": { "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", "type": "array", "items": { "type": "string", "default": "" } }, "group": { "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", "type": "string" }, "kind": { "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", "type": "string", "default": "" }, "name": { "description": "name is the plural name of the resource.", "type": "string", "default": "" }, "namespaced": { "description": "namespaced indicates if a resource is namespaced or not.", "type": "boolean", "default": false }, "shortNames": { "description": "shortNames is a list of suggested short names of the resource.", "type": "array", "items": { "type": "string", "default": "" } }, "singularName": { "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", "type": "string", "default": "" }, "storageVersionHash": { "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", "type": "string" }, "verbs": { "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", "type": "array", "items": { "type": "string", "default": "" } }, "version": { "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", "type": "object", "required": [ "groupVersion", "resources" ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "groupVersion": { "description": "groupVersion is the group and version this APIResourceList is for.", "type": "string", "default": "" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "resources": { "description": "resources contains the name of the resources and if they are namespaced.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" } ] } } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "APIResourceList", "version": "v1" } ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { "description": "DeleteOptions may be provided when deleting an API object.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "dryRun": { "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", "type": "array", "items": { "type": "string", "default": "" } }, "gracePeriodSeconds": { "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", "type": "integer", "format": "int64" }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "orphanDependents": { "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", "type": "boolean" }, "preconditions": { "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" } ] }, "propagationPolicy": { "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", "type": "string" } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "DeleteOptions", "version": "v1" }, { "group": "admission.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "admission.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "admissionregistration.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "admissionregistration.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "admissionregistration.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apiextensions.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "apiextensions.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apiregistration.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "apiregistration.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apps", "kind": "DeleteOptions", "version": "v1" }, { "group": "apps", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "apps", "kind": "DeleteOptions", "version": "v1beta2" }, { "group": "authentication.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "authentication.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "authentication.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "authorization.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "authorization.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v1" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v2" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v2beta1" }, { "group": "autoscaling", "kind": "DeleteOptions", "version": "v2beta2" }, { "group": "batch", "kind": "DeleteOptions", "version": "v1" }, { "group": "batch", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "certificates.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "certificates.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "certificates.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "coordination.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "coordination.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "discovery.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "discovery.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "events.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "events.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "extensions", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1beta2" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1beta3" }, { "group": "imagepolicy.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "internal.apiserver.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "networking.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "node.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "node.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "node.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "policy", "kind": "DeleteOptions", "version": "v1" }, { "group": "policy", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "rbac.authorization.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "rbac.authorization.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "rbac.authorization.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "resource.k8s.io", "kind": "DeleteOptions", "version": "v1alpha2" }, { "group": "scheduling.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "scheduling.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "scheduling.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" }, { "group": "storage.k8s.io", "kind": "DeleteOptions", "version": "v1" }, { "group": "storage.k8s.io", "kind": "DeleteOptions", "version": "v1alpha1" }, { "group": "storage.k8s.io", "kind": "DeleteOptions", "version": "v1beta1" } ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector": { "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", "type": "object", "properties": { "matchExpressions": { "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelectorRequirement" } ] } }, "matchLabels": { "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", "type": "object", "additionalProperties": { "type": "string", "default": "" } } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelectorRequirement": { "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", "type": "object", "required": [ "key", "operator" ], "properties": { "key": { "description": "key is the label key that the selector applies to.", "type": "string", "default": "", "x-kubernetes-patch-merge-key": "key", "x-kubernetes-patch-strategy": "merge" }, "operator": { "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", "type": "string", "default": "" }, "values": { "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", "type": "array", "items": { "type": "string", "default": "" } } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", "type": "object", "properties": { "continue": { "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", "type": "string" }, "remainingItemCount": { "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", "type": "integer", "format": "int64" }, "resourceVersion": { "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", "type": "string" }, "selfLink": { "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", "type": "string" }, "fieldsType": { "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", "type": "string" }, "fieldsV1": { "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" } ] }, "manager": { "description": "Manager is an identifier of the workflow managing these fields.", "type": "string" }, "operation": { "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", "type": "string" }, "subresource": { "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", "type": "string" }, "time": { "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", "type": "object", "properties": { "annotations": { "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", "type": "object", "additionalProperties": { "type": "string", "default": "" } }, "creationTimestamp": { "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "deletionGracePeriodSeconds": { "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", "type": "integer", "format": "int64" }, "deletionTimestamp": { "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] }, "finalizers": { "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", "type": "array", "items": { "type": "string", "default": "" }, "x-kubernetes-patch-strategy": "merge" }, "generateName": { "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", "type": "string" }, "generation": { "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", "type": "integer", "format": "int64" }, "labels": { "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", "type": "object", "additionalProperties": { "type": "string", "default": "" } }, "managedFields": { "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" } ] } }, "name": { "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", "type": "string" }, "namespace": { "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", "type": "string" }, "ownerReferences": { "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" } ] }, "x-kubernetes-patch-merge-key": "uid", "x-kubernetes-patch-strategy": "merge" }, "resourceVersion": { "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", "type": "string" }, "selfLink": { "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", "type": "string" }, "uid": { "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", "type": "object", "required": [ "apiVersion", "kind", "name", "uid" ], "properties": { "apiVersion": { "description": "API version of the referent.", "type": "string", "default": "" }, "blockOwnerDeletion": { "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", "type": "boolean" }, "controller": { "description": "If true, this reference points to the managing controller.", "type": "boolean" }, "kind": { "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string", "default": "" }, "name": { "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", "type": "string", "default": "" }, "uid": { "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", "type": "string", "default": "" } }, "x-kubernetes-map-type": "atomic" }, "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", "type": "object" }, "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", "type": "object", "properties": { "resourceVersion": { "description": "Specifies the target ResourceVersion", "type": "string" }, "uid": { "description": "Specifies the target UID.", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { "description": "Status is a return value for calls that don't return other objects.", "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, "code": { "description": "Suggested HTTP return code for this status, 0 if not set.", "type": "integer", "format": "int32" }, "details": { "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" } ] }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "message": { "description": "A human-readable description of the status of this operation.", "type": "string" }, "metadata": { "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" } ] }, "reason": { "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", "type": "string" }, "status": { "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", "type": "string" } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "Status", "version": "v1" }, { "group": "resource.k8s.io", "kind": "Status", "version": "v1alpha2" } ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", "type": "object", "properties": { "field": { "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", "type": "string" }, "message": { "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", "type": "string" }, "reason": { "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", "type": "object", "properties": { "causes": { "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", "type": "array", "items": { "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" } ] } }, "group": { "description": "The group attribute of the resource associated with the status StatusReason.", "type": "string" }, "kind": { "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", "type": "string" }, "name": { "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", "type": "string" }, "retryAfterSeconds": { "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", "type": "integer", "format": "int32" }, "uid": { "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", "type": "string" } } }, "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", "type": "string", "format": "date-time" }, "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { "description": "Event represents a single event to a watched resource.", "type": "object", "required": [ "type", "object" ], "properties": { "object": { "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", "default": {}, "allOf": [ { "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" } ] }, "type": { "type": "string", "default": "" } }, "x-kubernetes-group-version-kind": [ { "group": "", "kind": "WatchEvent", "version": "v1" }, { "group": "admission.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "admission.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "admissionregistration.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "admissionregistration.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "admissionregistration.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apiextensions.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "apiextensions.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apiregistration.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "apiregistration.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apps", "kind": "WatchEvent", "version": "v1" }, { "group": "apps", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "apps", "kind": "WatchEvent", "version": "v1beta2" }, { "group": "authentication.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "authentication.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "authentication.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "authorization.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "authorization.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v1" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v2" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v2beta1" }, { "group": "autoscaling", "kind": "WatchEvent", "version": "v2beta2" }, { "group": "batch", "kind": "WatchEvent", "version": "v1" }, { "group": "batch", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "certificates.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "certificates.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "certificates.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "coordination.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "coordination.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "discovery.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "discovery.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "events.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "events.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "extensions", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1beta2" }, { "group": "flowcontrol.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1beta3" }, { "group": "imagepolicy.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "internal.apiserver.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "networking.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "networking.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "node.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "node.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "node.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "policy", "kind": "WatchEvent", "version": "v1" }, { "group": "policy", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "rbac.authorization.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "rbac.authorization.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "rbac.authorization.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "resource.k8s.io", "kind": "WatchEvent", "version": "v1alpha2" }, { "group": "scheduling.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "scheduling.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "scheduling.k8s.io", "kind": "WatchEvent", "version": "v1beta1" }, { "group": "storage.k8s.io", "kind": "WatchEvent", "version": "v1" }, { "group": "storage.k8s.io", "kind": "WatchEvent", "version": "v1alpha1" }, { "group": "storage.k8s.io", "kind": "WatchEvent", "version": "v1beta1" } ] }, "io.k8s.apimachinery.pkg.runtime.RawExtension": { "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", "type": "object" }, "io.k8s.apimachinery.pkg.util.intstr.IntOrString": { "description": "IntOrString is a type that can hold an int32 or a string. When used in JSON or YAML marshalling and unmarshalling, it produces or consumes the inner type. This allows you to have, for example, a JSON field that can accept a name or number.", "format": "int-or-string", "oneOf": [ { "type": "integer" }, { "type": "string" } ] } }, "securitySchemes": { "BearerToken": { "type": "apiKey", "description": "Bearer Token authentication", "name": "authorization", "in": "header" } } } } kubernetes-kubernetes-40e1192/staging/src/k8s.io/kubectl/pkg/explain/v2/templates/plaintext.tmpl000066400000000000000000000350161504711711200327440ustar00rootroot00000000000000{{- /* Determine if Path for requested GVR is at /api or /apis based on emptiness of group */ -}} {{- $prefix := (ternary "/api" (join "" "/apis/" $.GVR.Group) (not $.GVR.Group)) -}} {{- /* Search both cluster-scoped and namespaced-scoped paths for the GVR to find its GVK */ -}} {{- /* Also search for paths with {name} component in case the list path is missing */ -}} {{- /* Looks for the following paths: */ -}} {{- /* /apis/// */ -}} {{- /* /apis////{name} */ -}} {{- /* /apis///namespaces/{namespace}/ */ -}} {{- /* /apis///namespaces/{namespace}//{name} */ -}} {{- /* Also search for get verb paths in case list verb is missing */ -}} {{- $clusterScopedSearchPath := join "/" $prefix $.GVR.Version $.GVR.Resource -}} {{- $clusterScopedNameSearchPath := join "/" $prefix $.GVR.Version $.GVR.Resource "{name}" -}} {{- $namespaceScopedSearchPath := join "/" $prefix $.GVR.Version "namespaces" "{namespace}" $.GVR.Resource -}} {{- $namespaceScopedNameSearchPath := join "/" $prefix $.GVR.Version "namespaces" "{namespace}" $.GVR.Resource "{name}" -}} {{- $gvk := "" -}} {{- /* Pull GVK from operation */ -}} {{- range $index, $searchPath := (list $clusterScopedSearchPath $clusterScopedNameSearchPath $namespaceScopedSearchPath $namespaceScopedNameSearchPath) -}} {{- with $resourcePathElement := index $.Document "paths" $searchPath -}} {{- range $methodIndex, $method := (list "get" "post" "put" "patch" "delete") -}} {{- with $resourceMethodPathElement := index $resourcePathElement $method -}} {{- with $gvk = index $resourceMethodPathElement "x-kubernetes-group-version-kind" -}} {{- break -}} {{- end -}} {{- end -}} {{- end -}} {{- end -}} {{- end -}} {{- with $gvk -}} {{- if $gvk.group -}} GROUP: {{ $gvk.group }}{{"\n" -}} {{- end -}} KIND: {{ $gvk.kind}}{{"\n" -}} VERSION: {{ $gvk.version }}{{"\n" -}} {{- "\n" -}} {{- with include "schema" (dict "gvk" $gvk "Document" $.Document "FieldPath" $.FieldPath "Recursive" $.Recursive) -}} {{- . -}} {{- else -}} {{- throw "error: GVK %v not found in OpenAPI schema" $gvk -}} {{- end -}} {{- else -}} {{- throw "error: GVR (%v) not found in OpenAPI schema" $.GVR.String -}} {{- end -}} {{- "\n" -}} {{- /* Finds a schema with the given GVK and prints its explain output or empty string if GVK was not found Takes dictionary as argument with keys: gvk: openapiv3 JSON schema Document: entire doc FieldPath: field path to follow Recursive: print recursive */ -}} {{- define "schema" -}} {{- /* Find definition with this GVK by filtering out the components/schema with the given x-kubernetes-group-version-kind */ -}} {{- range index $.Document "components" "schemas" -}} {{- if contains (index . "x-kubernetes-group-version-kind") $.gvk -}} {{- with include "output" (set $ "schema" .) -}} {{- . -}} {{- else -}} {{- $fieldName := (index $.FieldPath (sub (len $.FieldPath) 1)) -}} {{- throw "error: field \"%v\" does not exist" $fieldName}} {{- end -}} {{- break -}} {{- end -}} {{- end -}} {{- end -}} {{- /* Follows FieldPath until the FieldPath is empty. Then prints field name and field list of resultant schema. If field path is not found. Prints nothing. Example output: FIELD: spec DESCRIPTION: