kubernetes-kubernetes-9bda076/000077500000000000000000000000001476411216400164525ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/.generated_files000066400000000000000000000013561476411216400216000ustar00rootroot00000000000000# 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-9bda076/.gitattributes000066400000000000000000000007761476411216400213570ustar00rootroot00000000000000# 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-9bda076/.github/000077500000000000000000000000001476411216400200125ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/.github/ISSUE_TEMPLATE/000077500000000000000000000000001476411216400221755ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/.github/ISSUE_TEMPLATE/bug-report.yaml000066400000000000000000000042441476411216400251530ustar00rootroot00000000000000name: 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-9bda076/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002161476411216400241640ustar00rootroot00000000000000contact_links: - name: Support Request url: https://discuss.kubernetes.io about: Support request or question relating to Kubernetes kubernetes-kubernetes-9bda076/.github/ISSUE_TEMPLATE/enhancement.yaml000066400000000000000000000013561476411216400253530ustar00rootroot00000000000000name: 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-9bda076/.github/ISSUE_TEMPLATE/failing-test.yaml000066400000000000000000000021501476411216400254450ustar00rootroot00000000000000name: 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-9bda076/.github/ISSUE_TEMPLATE/flaking-test.yaml000066400000000000000000000025611476411216400254550ustar00rootroot00000000000000name: 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-9bda076/.github/OWNERS000066400000000000000000000007021476411216400207510ustar00rootroot00000000000000# 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-9bda076/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000055321476411216400236200ustar00rootroot00000000000000 #### 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-9bda076/.github/SECURITY.md000066400000000000000000000011501476411216400216000ustar00rootroot00000000000000# 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-9bda076/.gitignore000066400000000000000000000041071476411216400204440ustar00rootroot00000000000000# 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-9bda076/.go-version000066400000000000000000000000061476411216400205370ustar00rootroot000000000000001.23.6kubernetes-kubernetes-9bda076/CHANGELOG.md000077700000000000000000000000001476411216400230242CHANGELOG/README.mdustar00rootroot00000000000000kubernetes-kubernetes-9bda076/CHANGELOG/000077500000000000000000000000001476411216400177415ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/CHANGELOG/CHANGELOG-1.32.md000066400000000000000000012476601476411216400221530ustar00rootroot00000000000000 - [v1.32.2](#v1322) - [Downloads for v1.32.2](#downloads-for-v1322) - [Source Code](#source-code) - [Client Binaries](#client-binaries) - [Server Binaries](#server-binaries) - [Node Binaries](#node-binaries) - [Container Images](#container-images) - [Changelog since v1.32.1](#changelog-since-v1321) - [Important Security Information](#important-security-information) - [CVE-2025-0426: Node Denial of Service via Kubelet Checkpoint API](#cve-2025-0426-node-denial-of-service-via-kubelet-checkpoint-api) - [Changes by Kind](#changes-by-kind) - [Feature](#feature) - [Bug or Regression](#bug-or-regression) - [Other (Cleanup or Flake)](#other-cleanup-or-flake) - [Dependencies](#dependencies) - [Added](#added) - [Changed](#changed) - [Removed](#removed) - [v1.32.1](#v1321) - [Downloads for v1.32.1](#downloads-for-v1321) - [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.32.0](#changelog-since-v1320) - [Important Security Information](#important-security-information-1) - [CVE-2024-9042: Command Injection affecting Windows nodes via nodes/*/logs/query API](#cve-2024-9042-command-injection-affecting-windows-nodes-via-nodeslogsquery-api) - [Changes by Kind](#changes-by-kind-1) - [API Change](#api-change) - [Feature](#feature-1) - [Bug or Regression](#bug-or-regression-1) - [Dependencies](#dependencies-1) - [Added](#added-1) - [Changed](#changed-1) - [Removed](#removed-1) - [v1.32.0](#v1320) - [Downloads for v1.32.0](#downloads-for-v1320) - [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.31.0](#changelog-since-v1310) - [Urgent Upgrade Notes](#urgent-upgrade-notes) - [Changes by Kind](#changes-by-kind-2) - [Deprecation](#deprecation) - [API Change](#api-change-1) - [Feature](#feature-2) - [Documentation](#documentation) - [Failing Test](#failing-test) - [Bug or Regression](#bug-or-regression-2) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-1) - [Dependencies](#dependencies-2) - [Added](#added-2) - [Changed](#changed-2) - [Removed](#removed-2) - [v1.32.0-rc.2](#v1320-rc2) - [Downloads for v1.32.0-rc.2](#downloads-for-v1320-rc2) - [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-rc.1](#changelog-since-v1320-rc1) - [Changes by Kind](#changes-by-kind-3) - [API Change](#api-change-2) - [Bug or Regression](#bug-or-regression-3) - [Dependencies](#dependencies-3) - [Added](#added-3) - [Changed](#changed-3) - [Removed](#removed-3) - [v1.32.0-rc.1](#v1320-rc1) - [Downloads for v1.32.0-rc.1](#downloads-for-v1320-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.32.0-rc.0](#changelog-since-v1320-rc0) - [Dependencies](#dependencies-4) - [Added](#added-4) - [Changed](#changed-4) - [Removed](#removed-4) - [v1.32.0-rc.0](#v1320-rc0) - [Downloads for v1.32.0-rc.0](#downloads-for-v1320-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.32.0-beta.0](#changelog-since-v1320-beta0) - [Changes by Kind](#changes-by-kind-4) - [API Change](#api-change-3) - [Feature](#feature-3) - [Bug or Regression](#bug-or-regression-4) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-2) - [Dependencies](#dependencies-5) - [Added](#added-5) - [Changed](#changed-5) - [Removed](#removed-5) - [v1.32.0-beta.0](#v1320-beta0) - [Downloads for v1.32.0-beta.0](#downloads-for-v1320-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.32.0-alpha.3](#changelog-since-v1320-alpha3) - [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) - [Changes by Kind](#changes-by-kind-5) - [Deprecation](#deprecation-1) - [API Change](#api-change-4) - [Feature](#feature-4) - [Bug or Regression](#bug-or-regression-5) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-3) - [Dependencies](#dependencies-6) - [Added](#added-6) - [Changed](#changed-6) - [Removed](#removed-6) - [v1.32.0-alpha.3](#v1320-alpha3) - [Downloads for v1.32.0-alpha.3](#downloads-for-v1320-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.32.0-alpha.2](#changelog-since-v1320-alpha2) - [Changes by Kind](#changes-by-kind-6) - [API Change](#api-change-5) - [Feature](#feature-5) - [Documentation](#documentation-1) - [Bug or Regression](#bug-or-regression-6) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-4) - [Dependencies](#dependencies-7) - [Added](#added-7) - [Changed](#changed-7) - [Removed](#removed-7) - [v1.32.0-alpha.2](#v1320-alpha2) - [Downloads for v1.32.0-alpha.2](#downloads-for-v1320-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.32.0-alpha.1](#changelog-since-v1320-alpha1) - [Changes by Kind](#changes-by-kind-7) - [API Change](#api-change-6) - [Feature](#feature-6) - [Documentation](#documentation-2) - [Bug or Regression](#bug-or-regression-7) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-5) - [Dependencies](#dependencies-8) - [Added](#added-8) - [Changed](#changed-8) - [Removed](#removed-8) - [v1.32.0-alpha.1](#v1320-alpha1) - [Downloads for v1.32.0-alpha.1](#downloads-for-v1320-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.31.0](#changelog-since-v1310-1) - [Changes by Kind](#changes-by-kind-8) - [Deprecation](#deprecation-2) - [API Change](#api-change-7) - [Feature](#feature-7) - [Documentation](#documentation-3) - [Failing Test](#failing-test-1) - [Bug or Regression](#bug-or-regression-8) - [Other (Cleanup or Flake)](#other-cleanup-or-flake-6) - [Dependencies](#dependencies-9) - [Added](#added-9) - [Changed](#changed-9) - [Removed](#removed-9) # v1.32.2 ## Downloads for v1.32.2 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes.tar.gz) | 5bb3ac1622ea58940f24cba80d8697f1a4924d6be5329745ec3caadbf332de1dd17728f549df2b44c39e67a93dfb93898c9247576e0dd554b9ca1f822c02b8fd [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-src.tar.gz) | b3cc597b924333f695c8789ed3549f565347c5bf0cb18a5fff87c5ad67843cef8342622e4860b443d8bc94daac6ee42e2d89053ea9ca3b5c235db2173e8715f3 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-darwin-amd64.tar.gz) | ec277b6cb932d7827ee652ba8645f8f69a54df6cb1411a6b7e3c8a8527cc4f01ecc8bee379bd99997d0f5b860521acc36d0b48b83401d4b85816d047b6fe1ab7 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-darwin-arm64.tar.gz) | d65282f7c1af50ee584c70bce5a6dd52858531a627b883d695fda3a04845043cab09f6cecefb8eb25c95fd5d6e0f51817d3b642f01459920f08c59c7d6d701e8 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-linux-386.tar.gz) | 3f228cb3342b28cd2884450a42d7cf8626acbe5773bd770c80526a5d2579babd6c5af7137a497c9c407d029e0acb8d5aa6cd1a1e9a85d57dddf0034c3e4bdcc0 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-linux-amd64.tar.gz) | 0f27d1918088df6a672f42b13cf213acb5e7499db1b9db5191478adb2ca0c350ba8f5004ceee3798b0ff47fc358bf2fb37097c1113f603dbedd0d00ae0dbaf7f [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-linux-arm.tar.gz) | c45d0804cf74edb31944fcc0451e498cf13a9115927ecca4bb32369ca136f96ad746116047c75b8d76a60da7bce95ea9ca39cd0fe1b19db17c2179da85405c18 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-linux-arm64.tar.gz) | ad0af31c2845e80fcc1916b550b6047a42bd01971f5a20256d98bdd59b51d03061607898cf190365a484a169d411a5b3d46aa8365ec3e035fb98fd345fb04c09 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-linux-ppc64le.tar.gz) | 471b788c71b158346e18767ec74a3e27546ae270285d64561ba47dcc632423ea936817e8c071407919cbacfc0183211ff69aa8f1a4c6442506dc60c9bae24933 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-linux-s390x.tar.gz) | 3d4778a33aa4c3a9cad2ed36942e105171596f7f5b864c33897d8df42fdadfbe905f2a9be8f99855ddb7eb8dac7b0d32cd30cf33a6ee39d15d3b184cc670db7e [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-windows-386.tar.gz) | 1fffe7792d46d173a9e8d74515d86db8bc75834caae796588a222ad04ac41776a27a1d3dcc28f1b4fbd8ae856dcc59776389c59ddc0f02ee69ac40e1bd2d8f02 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-windows-amd64.tar.gz) | ea835ba701849dc2f9d0b987f72020c1d74bf3e3e528edca22cce6bd762231ddedf76322d0129d85dbe776020ddcd4e182f65565ca7a91fbb6f351226f976c49 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-client-windows-arm64.tar.gz) | 9fe659e162cb8f067a783a52b4c68179bde333ec46d8f694c0d790121cfea7a91ad415197233f217e93fc68a30820057568c16e33a7852f98c92f891a57723c4 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-server-linux-amd64.tar.gz) | 35fc5ddaec31a9165aa332161d8632a3b5e6d77ba1f2243561af00f9115e0f085f297ad9c28da844e47d03de2b001fd9a11709cf5bdd76847597c96a2c7dfe78 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-server-linux-arm64.tar.gz) | fed886acadca24457cc852b224b951c4472efa3847b1beebe99168692a0292922e105100d5aa6f41d47eae8cda936399d73e06a3435d33fb2178954cf9e6d9fa [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-server-linux-ppc64le.tar.gz) | d2bc74a741ff0471f88b3b5ec5cc05e8e8c62503837b0744496381453229993a633c1e722c2107b2bb1c03f3284217ed0838ca4936cf67b6c1ed502cf1b5b210 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-server-linux-s390x.tar.gz) | 9abf035bd10d543438e91d459eb689f24275cd5657c0eee5abce5adbc5e2c8a68635e5cff6845988fdb8c168f15459dfbb61b801d9e505ac95be227936a37261 ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-node-linux-amd64.tar.gz) | 92df813a32e157827c69c8c5c4843c6a994d7a52750ae5d3b06d136bd2d61386a55a878a425f4e29f10a9de56c0638d49d34c7b96c8cf391924c76e225ed78bb [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-node-linux-arm64.tar.gz) | a46184f62f2301ea8d6c88c22557365d0480ba87db98e36fed56f2ac88fffaf7d343654c05c76ab71ae6d6d43323b7f9f9f1c8b3ec7ab1c7f216c53b42ec0793 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-node-linux-ppc64le.tar.gz) | 21745d0a482e7cfc4a38b3342c84b86436fb08265d104c3c007f60f9e2cb268bbc35e78ebdd042391389206bf1294284f133a334c5f2036f48e544715a8aac9e [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-node-linux-s390x.tar.gz) | 71d686f7b3035ebdd58be58241b56974a5ad8974f53b0c0340355611ffb9b87b83e6b394146255ae9a203c424662451e9db8403e25d44aad860b410b71de1b18 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.2/kubernetes-node-windows-amd64.tar.gz) | 6ea1039891f77aec84f7ac8c4b4bde9d6dcfd213e18a556cf8becfc354b50be341391dd43c79443bb428868d71f8dbcfaec18e91cce8068702216472e93913ce ### 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.32.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.32.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.32.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.32.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.32.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.32.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.32.1 ## Important Security Information This release contains changes that address the following vulnerabilities: ### CVE-2025-0426: Node Denial of Service via Kubelet Checkpoint API A security issue was discovered in Kubernetes where a large number of container checkpoint requests made to the unauthenticated kubelet read-only HTTP endpoint may cause a Node Denial of Service by filling the Node's disk. **Affected Versions**: - kubelet kubelet v1.30.0 to v1.30.9 - kubelet v1.31.0 to v1.31.5 - kubelet v1.32.0 to v1.32.1 **Fixed Versions**: - kubelet 1.29.14 - kubelet 1.30.10 - kubelet 1.31.6 - kubelet 1.32.2 This vulnerability was reported and fixed by Tim Allclair @tallclair from Google. **CVSS Rating:** Medium (6.2) [CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H](https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) ## Changes by Kind ### Feature - Kubernetes is now built with go 1.23.5 ([#129966](https://github.com/kubernetes/kubernetes/pull/129966), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Kubernetes is now built with go 1.23.6 ([#130078](https://github.com/kubernetes/kubernetes/pull/130078), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] ### Bug or Regression - 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. ([#129674](https://github.com/kubernetes/kubernetes/pull/129674), [@gohilankit](https://github.com/gohilankit)) [SIG Storage] - Fixes a 1.32 regression in with the ServiceAccountNodeAudienceRestriction feature where `azureFile` volumes encounter "failed to get service accoount token attributes" errors. Reverts the `ServiceAccountNodeAudienceRestriction` feature to disabled in v1.32. Refer to https://github.com/kubernetes/kubernetes/issues/129935 for more details. If you're using in-tree inline volumes or in-tree persistent volumes whose CSI drivers depend on service account tokens, do not enable this feature in the 1.32 release. ([#130015](https://github.com/kubernetes/kubernetes/pull/130015), [@aramase](https://github.com/aramase)) [SIG Auth] - Kubeadm: fixed a bug where an image is not pulled if there is an error with the sandbox image from CRI. ([#129608](https://github.com/kubernetes/kubernetes/pull/129608), [@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. ([#129862](https://github.com/kubernetes/kubernetes/pull/129862), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] ### Other (Cleanup or Flake) - NONE ([#130010](https://github.com/kubernetes/kubernetes/pull/130010), [@tallclair](https://github.com/tallclair)) [SIG Node] ## Dependencies ### Added _Nothing has changed._ ### Changed _Nothing has changed._ ### Removed _Nothing has changed._ # v1.32.1 ## Downloads for v1.32.1 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes.tar.gz) | 8ed533785ea6016d8ed87d87e22292fdd06042544431b98066d1f436d3534f044a1ac8fc9eca273a44d1e4ba07ca7111c6183c3107355010a20a80b407488ba2 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-src.tar.gz) | 737247c5c00111b83569f409bc2f759dc47b0047fe1b09ef898cf740eb74393caefa5527678223b08111c27053d3b231ca9ea1d16144514d6505a2c9582a85c4 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-darwin-amd64.tar.gz) | 7035cc3a12eea055cc35631238360730145cee63139e9ab35cedca84929984252b8a0a79567cbdcc2860a41de04fe1ffb041cf8facddf7acfe91986cba95b578 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-darwin-arm64.tar.gz) | e40a502b1f94600544d93a47e3f2e5511e004c388ad13870786299570140ad4f3237f57997ba33dc8c56507ea22446f088923b07e2e4088fef3d5ac171eb051e [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-linux-386.tar.gz) | a56102f4691f5ca99bfe862e648fa95b605ff5d30a9af1bdeea25ba0e3fcc697d7b39689f24e86707c9f78953abd20f22af7ad088f0ed0bc61f1386d3e405d32 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-linux-amd64.tar.gz) | 3eff144cdc8db4681fcf9b2205fa732ea7836e7878d9cc5617171970bbf80813eea45d08e1d00f6d652b6364c4a099e3e40a2a6a3ddad11a9896c73cda3118d2 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-linux-arm.tar.gz) | b1b6a8c298ab47b1fe2ceaa5deddddc5ee0faec681274c7241dd2eea573388aac1d37bf8a782bd8adff331483056464b6b9e87487dbc676d78b2e25beec58d0a [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-linux-arm64.tar.gz) | 7a0243dc6f643c911238956acb9181ff033970f1152c52acd8ca4aae1f73f978fe815f7c85526919384e12b9e2ecbf9ba56620ade463591d6f6b5b68511d4c6c [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-linux-ppc64le.tar.gz) | 8bf9925b287769a2fef518ac2c9fc24eba99b1c19b54a269c9cce9cefa1efaa400924c101fd50377fb5c6cd4b57a28266d2b2e765f0ecd1fc7965268b5227f55 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-linux-s390x.tar.gz) | 5c1ac45076db6e819c2a86289d74b2dd7e65523e374252799c159cd36846033de44be796439a255854ac993d83a336ff76638db7e14b87e3508c6468ca3e0931 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-windows-386.tar.gz) | 7bb021267376bfe1b081e40134b9046135cd10ad894e4bb691a416ca3e82aa676a0f022ca6071de4f2151f912272b9f61043ed21587350c7664870cb688c2f3f [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-windows-amd64.tar.gz) | 0042490cd17d351f2f868c81f3ec7a5dfc9f698bd256aed55913b4adad3ed96f6335a26222289e410676452aede46a25259e34dc76cd2b4ccf003f88ff7e49e8 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-client-windows-arm64.tar.gz) | 1f0e41b1f9f557e6e11c86ba8c9d4de4898dc7c7d92a03ca9ede4c10809206ea1c3183798af1dacbf2306d6104305bad42738ba9fd07aa2cecff67e96323d1c2 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-server-linux-amd64.tar.gz) | 564105bcd68a6b080e02ace75a66749bea67785200c922de5499049ed6ba5b07a246903de00966c83bea6e5a9ad44ff8224c96ed37c3fee96c0f6f42c82f3f49 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-server-linux-arm64.tar.gz) | 42fc09fcc6b7169c01687de3e1978ab0dd171a7733cc5d29c22f083c74ec8b53d9b4515f8811e5d1b367bb6944b9bc28d20ac269bd55561313077009820f11a2 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-server-linux-ppc64le.tar.gz) | 648f3272d33f8edfd1f0aa2da4f4d36e9a998eeec1506fa51a2af34c662803888ab27f0942d6c79b3b76e19e14e786b504f3b713e979dae126c42bbc24e2d9b5 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-server-linux-s390x.tar.gz) | bae5ddced7a2d2c8eb8e16580ec8579ac426483927ef6db2bb672014985cf60ba7fb0739f556229b33a51d2605f6a0efc1e905d50a5da7d5b7723351b9bf792b ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-node-linux-amd64.tar.gz) | 843756e63bc68d46520e81c3e0d476fc13a6c1739a61879bf27895d38e1d276162c9f388f66a474d70ab763142b5b61439805a662ddd448b361cc52d09e7d9e5 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-node-linux-arm64.tar.gz) | 7dfb4528225791986195906d272da1e3188eeb91ddcf3102cd186887a453aa8a906dbac1412401fd4097e5490f259b81a08281dc7aea06f34505e7fd072ce4de [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-node-linux-ppc64le.tar.gz) | 8e67bdbdb99353cebfe43a190320921565a84f2e20214d1fc05f99334fc01eb01f5f146b860edb45e298a7080323c9f9b3c5bc174665071d51ff0692259842bb [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-node-linux-s390x.tar.gz) | 0f8815d89d83496b14ce650d540a2d3e02b1d2045347a2eeb2506dc9add49f75fdc721ba0a80a728b03266d6a187aaced0007f5aec17f216bd0e0c3818a93ca3 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.1/kubernetes-node-windows-amd64.tar.gz) | e4aaf16ba64658ecbc3d4fb3b5df1463fd4c4ae84b8c758c4c05720a6abd843f453ed9577c0ad0a321b626d03e5bd8b218aa7318579e9ebee05a76be50f5247a ### 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.32.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.32.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.32.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.32.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.32.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.32.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 ## Important Security Information This release contains changes that address the following vulnerabilities: ### CVE-2024-9042: Command Injection affecting Windows nodes via nodes/*/logs/query API A security vulnerability has been discovered in Kubernetes windows nodes that could allow a user with the ability to query a node's '/logs' endpoint to execute arbitrary commands on the host. **Affected Versions**: - kubelet <= v1.29.12 - kubelet <= v1.30.8 - kubelet <= v1.31.4 - kubelet = v1.32.0 **Fixed Versions**: - kubelet 1.29.13 - kubelet 1.30.9 - kubelet 1.31.5 - kubelet 1.32.1 This vulnerability was reported by Peled, Tomer and mitigated by Aravindh Puthiyaprambil. **CVSS Rating:** Medium (5.9) [CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:N](https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:N) ## Changes by Kind ### API Change - 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. ([#129544](https://github.com/kubernetes/kubernetes/pull/129544), [@pohly](https://github.com/pohly)) [SIG API Machinery, Node and Testing] - NONE ([#129598](https://github.com/kubernetes/kubernetes/pull/129598), [@aravindhp](https://github.com/aravindhp)) [SIG API Machinery and Node] ### Feature - Kubernetes is now built with go 1.23.4 ([#129423](https://github.com/kubernetes/kubernetes/pull/129423), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] ### Bug or Regression - Fixed a storage bug around multipath. iSCSI and Fibre Channel devices attached to nodes via multipath now resolve correctly if partitioned. ([#129180](https://github.com/kubernetes/kubernetes/pull/129180), [@RomanBednar](https://github.com/RomanBednar)) [SIG Storage] - Fixes a panic in kube-controller-manager handling StatefulSet objects when revisionHistoryLimit is negative ([#129322](https://github.com/kubernetes/kubernetes/pull/129322), [@ardaguclu](https://github.com/ardaguclu)) [SIG Apps] - Kubeadm: fix a bug where the 'node.skipPhases' in UpgradeConfiguration is not respected by 'kubeadm upgrade node' command ([#129455](https://github.com/kubernetes/kubernetes/pull/129455), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: if an addon is disabled in the ClusterConfiguration, skip it during upgrade. ([#129429](https://github.com/kubernetes/kubernetes/pull/129429), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] ## Dependencies ### Added _Nothing has changed._ ### Changed _Nothing has changed._ ### Removed _Nothing has changed._ # v1.32.0 [Documentation](https://docs.k8s.io) ## Downloads for v1.32.0 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes.tar.gz) | `6ff36174fd78b83b7cf2a05ff991725efcd3529f2c8c9924586258d359af5049062c1f4aff6d8e9044981781c80de6cc738365b85e47fd2e2971cd53a36882c2` [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-src.tar.gz) | `3c401843abef2e74c2e20557f1a7165623dc98c1e290cd629035ac323a491125c666966c638e8baf9f1cb039f330e1b80a4795551145dc04c323c487c25ced22` ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-darwin-amd64.tar.gz) | `adab0d3f2947323dc8690aebe8bf9aca0179a460ee43dd4144677d293d9d75cfe8c363d1f377d03533758aef891bba3fe4c884ec16e94b84dad83c5de1570a98` [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-darwin-arm64.tar.gz) | `155376003480f5689a503bd3857606813882bc45bdf7d3b07a002d282cbb74fc585844ccffd00ca5f49ed3e65721c9f63d25d67a19f09a9f3257416017e83e83` [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-linux-386.tar.gz) | `96716dcadf056057f9e9e7cda99935a95381333b8dfa101c3c168903c7dbdef2994d59585e8ee2d362c552f04038c3a0b47077ab7506a2a98ccbd1c1d91f183c` [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-linux-amd64.tar.gz) | `302e02599f0bdd3665aadb9e16a2f1f50712bf875f7525a0184450c0dcd59cefbfa67c3211aaa4d4eca197bd9fb49e1de35ffb9d579527ed4830d04400b09ef7` [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-linux-arm.tar.gz) | `b104c1fcea77ee2c614ee9089e94accc8aa5f915315711a974a51f0f5e0899e4741dcd6a046fea69264cb6933ce5c84ccaa9f7c9c1849def7da098ad5d2cc845` [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-linux-arm64.tar.gz) | `378face3b06a2d062aa734ba0b9fd13f20f877bd611556c352be6246fa70067e60ee44fe55c4c0f064b5715b311075b4db540c7cc52d1a2af4b96a563625f4f1` [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-linux-ppc64le.tar.gz) | `6172956799cdf4a65fa5450f26ed4e491a935473418daacb51686d93745445747b893eea701af9d8c508ac8cbd3f4cbabef6cb17b94448e5c2732dc13d35e046` [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-linux-s390x.tar.gz) | `a3ebc9175317aa93acc26edee8b7f5502a0d9405c1b04b39d907bbebe022e23c9fed058f5cede7045e388d1706c2657b0282d14862e01a7b34002e88e7224d8f` [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-windows-386.tar.gz) | `affcae9e4065cdfc130c6bb690539a631c1be0d992e9b02efbd49e0d519275d23e77c2ba5aa563ed5b89498e8bb26ce73019575bd557152b0d554578a96bf945` [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-windows-amd64.tar.gz) | `9619c05daa723c7853daae3432f771a31a6fe57887c32e5e592eb3bf619a636a8c3c2dec0eba2ec60382dc3d2ea8b0dc58e5b5f15fa43cdb7371a3ec0a7e4f55` [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-client-windows-arm64.tar.gz) | `e4c8c0d70d5c825dccd3dfa4517baf07f863deac440560c176206b626b0ebd585f0c0601e8956e4a73eda41d31d963895fe337a700a9c1853b7ee3ff2bd568e3` ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-server-linux-amd64.tar.gz) | `09ffc69de339bb507a9f8fdd2206dcc1e77f58184bfa1f771c715edc200861131e5028ae38ec1f5a1112d3303159fb2b9246266114ce0a502776b2c28354dfba` [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-server-linux-arm64.tar.gz) | `56b04497a022b3cd4efac6d1771ead89aef9e6e33639209bb2c1eaa95f4c01cf6ac5f3fa6e66b5edcbd0cab1c164522ad0585daedf271b27b53a8e2d573f6a82` [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-server-linux-ppc64le.tar.gz) | `75d09f92b6756f1ef96868dd3b83241154729033015544de5e4a881f0ea8bb62bebc326df8c199ab98cb29b7171ff2fce4d4ee15f26d8d68e4545067bbdfa5bb` [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-server-linux-s390x.tar.gz) | `562b42b297a161eded117b5fb0f346c9531e959d4d798e623521703960dccf8841aa261b2678b40d1efc11123af85be1b769ac197a3f89246479486efef85d5b` ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-node-linux-amd64.tar.gz) | `37b1c6da21d0b915a8dd372caa2c48715dcc9071191f753b2ebdc812643265b646777ecf781c4d269d5490066968648c3321ce0d56b3ac8d3c528c6357de2e67` [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-node-linux-arm64.tar.gz) | `d6708bf5e5c9e70242af57b20bf64396d419fc6654c090741c508d4c265717b0a1d6e8948de5d6927dd356f22c2085607f7b9549bb0f4ee7aafcb3b2f4b862b3` [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-node-linux-ppc64le.tar.gz) | `c26df8571204a0ae5b18a126c21cd8985b6fd0a8df50c8da4cfd86006b3974fa452ff30de0c4f6ed5cd54e59705a2f639a8ee4201fd681048968cbea416e7e40` [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-node-linux-s390x.tar.gz) | `d5a13e1d13a6d9ff081f691b06ca66b8e9bff7cd12591b1281e7c05382aeeee4cd3ec83a23176e07d21c018ca29795b3944cbff7af5f62700046bf2062912959` [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0/kubernetes-node-windows-amd64.tar.gz) | `57f4b842d1637a67ae59e400d237c8d63aea9a7dc018384e3fca9804d457b9125f46bb5776d36f2150642bb70f6f2e8781b4e62e8de84627c076004d1244212a` ### 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.32.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.32.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.32.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.32.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.32.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.32.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.31.0 ## Urgent Upgrade Notes There are no urgent upgrade notes for the v1.32 release. ## Changes by Kind ### Deprecation - Reverted the `DisableNodeKubeProxyVersion` feature gate to default-off to give a full year from deprecation announcement in 1.29 to clearing the field by default, per the [Kubernetes deprecation policy](https://kubernetes.io/docs/reference/using-api/deprecation-policy/). ([#126720](https://github.com/kubernetes/kubernetes/pull/126720), [@liggitt](https://github.com/liggitt)) [SIG Architecture and Node] - ServiceAccount metadata.annotations[kubernetes.io/enforce-mountable-secrets]: deprecated since v1.32; no removal deadline. Prefer separate namespaces to isolate access to mounted secrets. ([#128396](https://github.com/kubernetes/kubernetes/pull/128396), [@ritazh](https://github.com/ritazh)) [SIG API Machinery, Apps, Auth, CLI and Testing] ### API Change - **ACTION REQUIRED** for custom scheduler plugin developers: `PodEligibleToPreemptOthers` in the `preemption` interface now includes `ctx` in the parameters. Please update your plugins' implementation accordingly. ([#126465](https://github.com/kubernetes/kubernetes/pull/126465), [@googs1025](https://github.com/googs1025)) [SIG Scheduling] - Changed NodeToStatusMap from a map to a struct and exposed methods to access the entries. Added absentNodesStatus, which informs the status of nodes that are absent in the map. For developers of out-of-tree PostFilter plugins, ensure to update the usage of NodeToStatusMap. Additionally, NodeToStatusMap should eventually be renamed to NodeToStatusReader. ([#126022](https://github.com/kubernetes/kubernetes/pull/126022), [@macsko](https://github.com/macsko)) [SIG Node, Scheduling, and Testing] - A new /resize subresource was added to request pod resource resizing. Update your k8s client code to utilize the /resize subresource for Pod resizing operations. ([#128266](https://github.com/kubernetes/kubernetes/pull/128266), [@AnishShah](https://github.com/AnishShah)) [SIG API Machinery, Apps, Node and Testing] - A new feature that allows unsafe deletion of corrupt resources has been added, it is disabled by default, and it can be enabled by setting the option `--feature-gates=AllowUnsafeMalformedObjectDeletion=true`. It comes with an API change, a new delete option `ignoreStoreReadErrorWithClusterBreakingPotential` has been introduced, it is not set by default, this maintains backward compatibility. In order to perform an unsafe deletion of a corrupt resource, the user must enable the option for the delete request. A resource is considered corrupt if it can not be successfully retrieved from the storage due to a) transformation error e.g. decryption failure, or b) the object failed to decode. Normal deletion flow is attempted first, and if it fails with a corrupt resource error then it triggers unsafe delete. In addition, when this feature is enabled, the 'details' field of 'Status' from the LIST response includes information that identifies the corrupt object(s). NOTE: unsafe deletion ignores finalizer constraints, and skips precondition checks. WARNING: this may break the workload associated with the resource being unsafe-deleted, if it relies on the normal deletion flow, so cluster breaking consequences apply. ([#127513](https://github.com/kubernetes/kubernetes/pull/127513), [@tkashem](https://github.com/tkashem)) [SIG API Machinery, Etcd, Node and Testing] - Added `singleProcessOOMKill` flag to the kubelet configuration. Setting that to true enable single process OOM killing in cgroups v2. In this mode, if a single process is OOM killed within a container, the remaining processes will not be OOM killed. ([#126096](https://github.com/kubernetes/kubernetes/pull/126096), [@utam0k](https://github.com/utam0k)) [SIG API Machinery, Node, Testing and Windows] - Added a `/flagz` endpoint for kube-apiserver endpoint. ([#127581](https://github.com/kubernetes/kubernetes/pull/127581), [@richabanker](https://github.com/richabanker)) [SIG API Machinery, Architecture, Auth and Instrumentation] - Added a `Stream` field to `PodLogOptions`, which allows clients to request certain log stream (stdout or stderr) of the container. Please also note that the combination of a specific `Stream` and `TailLines` is not supported. ([#127360](https://github.com/kubernetes/kubernetes/pull/127360), [@knight42](https://github.com/knight42)) [SIG API Machinery, Apps, Architecture, Node, Release and Testing] - Added alpha support for asynchronous Pod preemption. When the `SchedulerAsyncPreemption` feature gate is enabled, the scheduler now runs API calls to trigger preemptions asynchronously for better performance. ([#128170](https://github.com/kubernetes/kubernetes/pull/128170), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling and Testing] - Added driver-owned fields in `ResourceClaim.Status` to report device status data for each allocated device. ([#128240](https://github.com/kubernetes/kubernetes/pull/128240), [@LionelJouin](https://github.com/LionelJouin)) [SIG API Machinery, Network, Node and Testing] - Added enforcement of an upper cost bound for DRA evaluations of CEL. The API server and scheduler now enforce an upper bound on the cost and runtime steps required for evaluating a CEL expression. ([#128101](https://github.com/kubernetes/kubernetes/pull/128101), [@pohly](https://github.com/pohly)) [SIG API Machinery and Node] - Added the ability to change the maximum backoff delay accrued between container restarts for a node for containers in `CrashLoopBackOff`. To set this for a node, turn on the feature gate `KubeletCrashLoopBackoffMax` and set the `CrashLoopBackOff.MaxContainerRestartPeriod ` field between `"1s"` and `"300s"` in your [kubelet config file](https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/). ([#128374](https://github.com/kubernetes/kubernetes/pull/128374), [@lauralorenz](https://github.com/lauralorenz)) [SIG API Machinery and Node] - Allow for Pod search domains to be a single dot `.` or contain an underscore `_` ([#127167](https://github.com/kubernetes/kubernetes/pull/127167), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps, Network and Testing] - Annotation `batch.kubernetes.io/cronjob-scheduled-timestamp` added to Job objects scheduled from CronJobs is promoted to stable. ([#128336](https://github.com/kubernetes/kubernetes/pull/128336), [@soltysh](https://github.com/soltysh)) - Apply fsGroup policy for ReadWriteOncePod volumes. ([#128244](https://github.com/kubernetes/kubernetes/pull/128244), [@gnufied](https://github.com/gnufied)) [SIG Storage and Testing] - Changed the Pod API to support `resources` at `spec` level for pod-level resources. ([#128407](https://github.com/kubernetes/kubernetes/pull/128407), [@ndixita](https://github.com/ndixita)) [SIG API Machinery, Apps, CLI, Cluster Lifecycle, Node, Release, Scheduling and Testing] - ContainerStatus.AllocatedResources is now guarded by a separate feature gate, InPlacePodVerticalSaclingAllocatedStatus ([#128377](https://github.com/kubernetes/kubernetes/pull/128377), [@tallclair](https://github.com/tallclair)) [SIG API Machinery, CLI, Node, Scheduling and Testing] - Coordination.v1alpha1 API is dropped and replaced with coordination.v1alpha2. Old coordination.v1alpha1 types must be deleted before upgrade ([#127857](https://github.com/kubernetes/kubernetes/pull/127857), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery, Etcd, Scheduling and Testing] - DRA: Restricted the length of opaque device configuration parameters. At admission time, Kubernetes enforces a 10KiB size limit. ([#128601](https://github.com/kubernetes/kubernetes/pull/128601), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] - DRA: scheduling pods is up to 16x faster, depending on the scenario. Scheduling throughput depends a lot on cluster utilization. It is higher for lightly loaded clusters with free resources and gets lower when the cluster utilization increases. ([#127277](https://github.com/kubernetes/kubernetes/pull/127277), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Architecture, Auth, Etcd, Instrumentation, Node, Scheduling and Testing] - DRA: the `DeviceRequestAllocationResult` struct now has an "AdminAccess" field which should be used instead of the corresponding field in the `DeviceRequest` field when dealing with an allocation. If a device is only allocated for admin access, allocating it again for normal usage is now supported, as originally intended. To allow admin access, starting with 1.32 the `DRAAdminAccess` feature gate must be enabled. ([#127266](https://github.com/kubernetes/kubernetes/pull/127266), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Network, Node, Scheduling and Testing] - Disallow `k8s.io` and `kubernetes.io` namespaced extra key in structured authentication configuration. ([#126553](https://github.com/kubernetes/kubernetes/pull/126553), [@aramase](https://github.com/aramase)) [SIG Auth] - Fixed a bug in the `NestedNumberAsFloat64` Unstructured field accessor that could have caused it to return rounded float64 values instead of errors when accessing very large int64 values. ([#128099](https://github.com/kubernetes/kubernetes/pull/128099), [@benluddy](https://github.com/benluddy)) - Fixed the bug where `spec.terminationGracePeriodSeconds` of the pod will always be overwritten by the MaxPodGracePeriodSeconds of the soft eviction, you can enable the `AllowOverwriteTerminationGracePeriodSeconds` feature gate, which will restore the previous behavior. If you do need to set this, please file an issue with the Kubernetes project to help contributors understand why you needed it. ([#122890](https://github.com/kubernetes/kubernetes/pull/122890), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG API Machinery, Architecture, Node and Testing] - Graduated Job's `ManagedBy` field to beta. ([#127402](https://github.com/kubernetes/kubernetes/pull/127402), [@mimowo](https://github.com/mimowo)) [SIG API Machinery, Apps and Testing] - Implemented a new, alpha `seLinuxChangePolicy` field within a Pod-level `securityContext`, under SELinuxChangePolicy feature gate. This field allows for opting out from mounting Pod volumes with SELinux label when SELinuxMount feature is enabled (it is alpha and disabled by default now). Please see [the KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/1710-selinux-relabeling#story-3-cluster-upgrade) how we expect to warn users before any SELinux behavior changes and how they can opt-out before. Note that this field and feature gate is useful only with clusters that run with SELinux enabled. No action is required on clusters without SELinux. ([#127981](https://github.com/kubernetes/kubernetes/pull/127981), [@jsafrane](https://github.com/jsafrane)) [SIG API Machinery, Apps, Architecture, Node, Storage and Testing] - Introduced `v1alpha1` API for mutating admission policies, enabling extensible # admission control via CEL expressions (KEP 3962: Mutating Admission Policies). # To use, enable the `MutatingAdmissionPolicy` feature gate and the `admissionregistration.k8s.io/v1alpha1` # API via `--runtime-config`. ([#127134](https://github.com/kubernetes/kubernetes/pull/127134), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery, Auth, Etcd and Testing] - Introduced compressible resource setting on system reserved and kube reserved slices. ([#125982](https://github.com/kubernetes/kubernetes/pull/125982), [@harche](https://github.com/harche)) - kube-apiserver: Promoted the `StructuredAuthorizationConfiguration` feature gate to GA. The `--authorization-config` flag now accepts `AuthorizationConfiguration` in version `apiserver.config.k8s.io/v1` (with no changes from `apiserver.config.k8s.io/v1beta1`). ([#128172](https://github.com/kubernetes/kubernetes/pull/128172), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Auth and Testing] - kube-proxy now reconciles Service/Endpoint changes with conntrack table and cleans up only stale UDP flow entries ([#127318](https://github.com/kubernetes/kubernetes/pull/127318), [@aroradaman](https://github.com/aroradaman)) [SIG Network and Windows] - kube-scheduler removed `AzureDiskLimits` ,`CinderLimits` `EBSLimits` and `GCEPDLimits` plugin. Given the corresponding CSI driver reports how many volumes a node can handle in NodeGetInfoResponse, the kubelet stores this limit in CSINode and the scheduler then knows the limit of the driver on the node. Removed plugins AzureDiskLimits, CinderLimits, EBSLimits and GCEPDLimits if you explicitly enabled them in the scheduler config. ([#124003](https://github.com/kubernetes/kubernetes/pull/124003), [@carlory](https://github.com/carlory)) [SIG Scheduling, Storage and Testing] - kubelet: the `--image-credential-provider-config` file was loaded with strict deserialization, which failed if the config file contained duplicate or unknown fields. This protected against accidentally running with malformed config files, unindented files, or typos in field names, and it prevented unexpected behavior. ([#128062](https://github.com/kubernetes/kubernetes/pull/128062), [@aramase](https://github.com/aramase)) [SIG Auth and Node] - NodeRestriction admission now validates the audience value that kubelet is requesting a service account token for is part of the pod spec volume. This change is introduced with a new kube-apiserver featuregate `ServiceAccountNodeAudienceRestriction` that's enabled by default. ([#128077](https://github.com/kubernetes/kubernetes/pull/128077), [@aramase](https://github.com/aramase)) [SIG Auth, Storage and Testing] - Promoted `CustomResourceFieldSelectors` to stable; the feature was enabled by default. The `--feature-gates=CustomResourceFieldSelectors=true` flag was no longer needed on kube-apiserver binaries and would be removed in a future release. ([#127673](https://github.com/kubernetes/kubernetes/pull/127673), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery and Testing] - Promoted feature gate `StatefulSetAutoDeletePVC` from beta to stable. ([#128247](https://github.com/kubernetes/kubernetes/pull/128247), [@mattcary](https://github.com/mattcary)) [SIG API Machinery, Apps, Auth and Testing] - Removed all support for _classic_ dynamic resource allocation (DRA). The `DRAControlPlaneController` feature gate, formerly alpha, is no longer available. Kubernetes now only uses the _structured parameters_ model (also alpha) for allocating dynamic resources to Pods. if and only if classic DRA was enabled in a cluster, remove all workloads (pods, app deployments, etc. ) which depend on classic DRA and make sure that all PodSchedulingContext resources are gone before upgrading. PodSchedulingContext resources cannot be removed through the apiserver after an upgrade and workloads would not work properly. ([#128003](https://github.com/kubernetes/kubernetes/pull/128003), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] - Removed generally available feature gate `HPAContainerMetrics` ([#126862](https://github.com/kubernetes/kubernetes/pull/126862), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Autoscaling] - Removed restrictions on subresource flag in kubectl commands ([#128296](https://github.com/kubernetes/kubernetes/pull/128296), [@AnishShah](https://github.com/AnishShah)) [SIG CLI] - Revised the kubelet API Authorization with new subresources, that allow finer-grained authorization checks and access control for kubelet endpoints. Provided you enable the `KubeletFineGrainedAuthz` feature gate, you can access kubelet's `/healthz` endpoint by granting the caller `nodes/helathz` permission in RBAC. Similarly you can also access kubelet's `/pods` endpoint to fetch a list of Pods bound to that node by granting the caller `nodes/pods` permission in RBAC. Similarly you can also access kubelet's `/configz` endpoint to fetch kubelet's configuration by granting the caller `nodes/configz` permission in RBAC. You can still access kubelet's `/healthz`, `/pods` and `/configz` by granting the caller `nodes/proxy` permission in RBAC but that also grants the caller permissions to exec, run and attach to containers on the nodes and doing so does not follow the least privilege principle. Granting callers more permissions than they need can give attackers an opportunity to escalate privileges. ([#126347](https://github.com/kubernetes/kubernetes/pull/126347), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) [SIG API Machinery, Auth, Cluster Lifecycle and Node] - The core functionality of Dynamic Resource Allocation (DRA) got promoted to beta. No action is required when *upgrading*, the previous v1alpha3 API is still supported, so existing deployments and DRA drivers based on v1alpha3 continue to work. *Downgrading* from 1.32 to 1.31 with DRA resources in the cluster (resourceclaims, resourceclaimtemplates, deviceclasses, resourceslices) is *not* supported because the new v1beta1 is used as storage version and not readable by 1.31. ([#127511](https://github.com/kubernetes/kubernetes/pull/127511), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] - The default value for node-monitor-grace-period has been increased to 50s (earlier 40s) (Ref - https://github.com/kubernetes/kubernetes/issues/121793) ([#126287](https://github.com/kubernetes/kubernetes/pull/126287), [@devppratik](https://github.com/devppratik)) [SIG API Machinery, Apps and Node] - The resource/v1alpha3.ResourceSliceList filed which should have been named "metadata" but was instead named "listMeta" is now properly "metadata". ([#126749](https://github.com/kubernetes/kubernetes/pull/126749), [@thockin](https://github.com/thockin)) [SIG API Machinery] - The synthetic "Bookmark" event for the watch stream requests will now include a new annotation: `kubernetes.io/initial-events-list-blueprint`. THe annotation contains an empty, versioned list that is encoded in the requested format (such as protobuf, JSON, or CBOR), then base64-encoded and stored as a string. ([#127587](https://github.com/kubernetes/kubernetes/pull/127587), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery] - To enhance usability and developer experience, CRD validation rules now support direct use of (CEL) reserved keywords as field names in object validation expressions. Name format CEL library is supported in new expressions. ([#126977](https://github.com/kubernetes/kubernetes/pull/126977), [@aaron-prindle](https://github.com/aaron-prindle)) [SIG API Machinery, Architecture, Auth, Etcd, Instrumentation, Release, Scheduling and Testing] - Updated incorrect description of persistentVolumeClaimRetentionPolicy ([#126545](https://github.com/kubernetes/kubernetes/pull/126545), [@yangjunmyfm192085](https://github.com/yangjunmyfm192085)) [SIG API Machinery, Apps and CLI] - X.509 client certificate authentication to the kube-apiserver now produces credential IDs (derived from the certificate's signature) , for use in audit logging. ([#125634](https://github.com/kubernetes/kubernetes/pull/125634), [@ahmedtd](https://github.com/ahmedtd)) [SIG API Machinery, Auth and Testing] ### Feature - Added Windows support for the node memory manager. ([#128560](https://github.com/kubernetes/kubernetes/pull/128560), [@marosset](https://github.com/marosset)) [SIG Node and Windows] - Added `--concurrent-daemonset-syncs` command line flag to kube-controller-manager. This value sets the number of workers for the daemonset controller. ([#128444](https://github.com/kubernetes/kubernetes/pull/128444), [@tosi3k](https://github.com/tosi3k)) - Added a `/statusz` endpoint for the kube-apiserver endpoint. ([#125577](https://github.com/kubernetes/kubernetes/pull/125577), [@richabanker](https://github.com/richabanker)) [SIG API Machinery, Apps, Architecture, Auth, CLI, Cloud Provider, Instrumentation, Network, Node and Testing] - Added a health check for the device plugin gRPC registration server. When the registration server is down, kubelet is marked as unhealthy. If systemd watchdog is configured, this will result in a kubelet restart. ([#128432](https://github.com/kubernetes/kubernetes/pull/128432), [@zhifei92](https://github.com/zhifei92)) [SIG Node] - Added a kubelet metric `container_aligned_compute_resources_count` to report the count of containers getting aligned compute resources. ([#127155](https://github.com/kubernetes/kubernetes/pull/127155), [@ffromani](https://github.com/ffromani)) [SIG Node and Testing] - Added a kubelet metrics to report informations about the cpu pools managed by cpumanager when the static policy is in use. ([#127506](https://github.com/kubernetes/kubernetes/pull/127506), [@ffromani](https://github.com/ffromani)) [SIG Node and Testing] - Added a new controller, volumeattributesclass-protection-controller, into the kube-controller-manager. The new controller manages a protective finalizer on VolumeAttributesClass objects. ([#123549](https://github.com/kubernetes/kubernetes/pull/123549), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps, Auth and Storage] - 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. ([#127483](https://github.com/kubernetes/kubernetes/pull/127483), [@jingczhang](https://github.com/jingczhang)) [SIG Node] - Added a one-time random duration of up to 50% of kubelet's `nodeStatusReportFrequency` to help spread the node status update load evenly over time. ([#128640](https://github.com/kubernetes/kubernetes/pull/128640), [@mengqiy](https://github.com/mengqiy)) - Added an option to enable leader election in local-up-cluster.sh via the LEADER_ELECT CLI flag. ([#127786](https://github.com/kubernetes/kubernetes/pull/127786), [@Jefftree](https://github.com/Jefftree)) - Added kubelet support for systemd watchdog integration. With this enabled, systemd can automatically recover a hung kubelet. ([#127566](https://github.com/kubernetes/kubernetes/pull/127566), [@zhifei92](https://github.com/zhifei92)) [SIG Cloud Provider, Node and Testing] - Added metrics to measure the latency of DRA Node operations and DRA GRPC calls ([#127146](https://github.com/kubernetes/kubernetes/pull/127146), [@bart0sh](https://github.com/bart0sh)) [SIG Instrumentation, Network, Node, and Testing] - Added new functionality to the Go client code (`client-go`) library. The `List()` method for the metadata client allows enabling API streaming when fetching collections; this improves performance when listing many objects. To request this behavior, your client software must enable the `WatchListClient` client-go feature gate. Additionally, streaming is only available if supported by the cluster; the API server that you connect to must also support streaming. If the API server does not support or allow streaming, then `client-go` falls back to fetching the collection using the **list** API verb. ([#127388](https://github.com/kubernetes/kubernetes/pull/127388), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery and Testing] - Added preemptionPolicy field when using `kubectl get PriorityClass -owide` ([#126529](https://github.com/kubernetes/kubernetes/pull/126529), [@googs1025](https://github.com/googs1025)) [SIG CLI] - Added status for extended Pod resources within the `status.containerStatuses[].resources` field. ([#124227](https://github.com/kubernetes/kubernetes/pull/124227), [@iholder101](https://github.com/iholder101)) [SIG Node and Testing] - Added support to the kube-apiserver for an alpha feature enabling external signing of service account tokens and fetching of public verifying keys, by enabling the Alpha `ExternalServiceAccountTokenSigner` feature gate and specifying `--service-account-signing-endpoint`. The flag value can either be the location of a Unix domain socket on a filesystem, or be prefixed with an @ symbol and name a Unix domain socket in the abstract socket namespace. ([#128190](https://github.com/kubernetes/kubernetes/pull/128190), [@HarshalNeelkamal](https://github.com/HarshalNeelkamal)) [SIG API Machinery, Apps, Auth, Etcd, Instrumentation, Node, Release and Testing] - Added the feature gate CBORServingAndStorage to allow CBOR as the encoding for API request and response bodies, and as the storage encoding for custom resources. Clients must opt in; programs built with client-go can do this using the client-go feature gates ClientsAllowCBOR and ClientsPreferCBOR. ([#128539](https://github.com/kubernetes/kubernetes/pull/128539), [@benluddy](https://github.com/benluddy)) [SIG API Machinery, Etcd and Testing] - Adopted a new implementation of watch caches for **list** verbs, using a btree data structure. The new implementation is active by default; you can opt out by disabling the `BtreeWatchCache` feature gate. ([#128415](https://github.com/kubernetes/kubernetes/pull/128415), [@serathius](https://github.com/serathius)) [SIG API Machinery, Auth and Cloud Provider] - Allows PreStop lifecycle handler's sleep action to have a zero value ([#127094](https://github.com/kubernetes/kubernetes/pull/127094), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) [SIG Apps, Node and Testing] - CRI: Added a field to support CPU affinity on Windows. ([#124285](https://github.com/kubernetes/kubernetes/pull/124285), [@kiashok](https://github.com/kiashok)) [SIG Node and Windows] - Changed OOM score adjustment calculation for sidecar containers: the OOM adjustment for these containers will match or fall below the OOM score adjustment of regular containers in the Pod. ([#128029](https://github.com/kubernetes/kubernetes/pull/128029), [@bouaouda-achraf](https://github.com/bouaouda-achraf)) - Client-go/rest: contextual logging of request/response with accurate source code location of the caller ([#126999](https://github.com/kubernetes/kubernetes/pull/126999), [@pohly](https://github.com/pohly)) [SIG API Machinery and Instrumentation] - DRA: The resource claim controller now maintains metrics about the total number of `ResourceClaims` and the number of allocated `ResourceClaims`. ([#127661](https://github.com/kubernetes/kubernetes/pull/127661), [@pohly](https://github.com/pohly)) [SIG Apps, Instrumentation and Node] - Enabled graceful shutdown feature for Windows node ([#127404](https://github.com/kubernetes/kubernetes/pull/127404), [@zylxjtu](https://github.com/zylxjtu)) [SIG Node, Testing and Windows] - Enabled kube-controller-manager '--concurrent-job-syncs' flag works on orphan Pod processors ([#126567](https://github.com/kubernetes/kubernetes/pull/126567), [@fusida](https://github.com/fusida)) [SIG Apps] - Ensured resizing for Guaranteed pods with integer CPU requests on nodes with static CPU & Memory policy configured is not allowed for the beta release of in-place resize. The feature gate `InPlacePodVerticalScalingExclusiveCPUs` defaults to `false`, but can be enabled to unblock development on ([#127262](https://github.com/kubernetes/kubernetes/issues/127262), [@tallclair](https://github.com/tallclair)) [SIG Node]. ([#128287](https://github.com/kubernetes/kubernetes/pull/128287), [@esotsal](https://github.com/esotsal)) [SIG Node, Release and Testing] - Extend discovery GroupManager with Group lister interface ([#127524](https://github.com/kubernetes/kubernetes/pull/127524), [@mjudeikis](https://github.com/mjudeikis)) [SIG API Machinery] - Fixed: Avoid overwriting in-pod vertical scaling updates on systemd daemon reloads when using systemd ([#124216](https://github.com/kubernetes/kubernetes/pull/124216), [@iholder101](https://github.com/iholder101)) [SIG Node] - Fixed an issue where kubectl doesn't print image volume when kubectl describe a pod with that volume. ([#126706](https://github.com/kubernetes/kubernetes/pull/126706), [@carlory](https://github.com/carlory)) - Graduated the AnonymousAuthConfigurableEndpoints feature gate to beta and enable by default to allow configurable endpoints for anonymous authentication. ([#127009](https://github.com/kubernetes/kubernetes/pull/127009), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) [SIG Auth] - Graduated the kubelet memory manager to generally available (GA). ([#128517](https://github.com/kubernetes/kubernetes/pull/128517), [@Tal-or](https://github.com/Tal-or)) - Graduated `SchedulerQueueingHints` to beta; the feature gate is now enabled by default. ([#128472](https://github.com/kubernetes/kubernetes/pull/128472), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Graduated the `WatchList` feature gate to Beta for kube-apiserver and enabled `WatchListClient` for KCM. ([#128053](https://github.com/kubernetes/kubernetes/pull/128053), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery and Testing] - Implemented a queueing hint for PersistentVolumeClaim/Add event in the `CSILimit` plugin. ([#124703](https://github.com/kubernetes/kubernetes/pull/124703), [@utam0k](https://github.com/utam0k)) [SIG Scheduling and Storage] - Implemented new cluster events `UpdatePodSchedulingGatesEliminated` and `UpdatePodTolerations` for scheduler plugins. ([#127083](https://github.com/kubernetes/kubernetes/pull/127083), [@sanposhiho](https://github.com/sanposhiho)) - Improved Node's QueueingHint in the `NodeAffinity` plugin by ignoring unrelated changes that keep pods unschedulable. ([#127444](https://github.com/kubernetes/kubernetes/pull/127444), [@dom4ha](https://github.com/dom4ha)) [SIG Scheduling and Testing] - Improved Node's QueueingHint in the `NodeResourceFit` plugin by ignoring unrelated changes that keep pods unschedulable. ([#127473](https://github.com/kubernetes/kubernetes/pull/127473), [@dom4ha](https://github.com/dom4ha)) [SIG Scheduling and Testing] - Improved performance of the job controller when handling job delete events. ([#127378](https://github.com/kubernetes/kubernetes/pull/127378), [@hakuna-matatah](https://github.com/hakuna-matatah)) - Improved performance of the job controller when handling job update events. ([#127228](https://github.com/kubernetes/kubernetes/pull/127228), [@hakuna-matatah](https://github.com/hakuna-matatah)) - Included an additional resource labeltransformation in on_operations_total metric which could be used for resource specific validations for example handling of encryption config by the apiserver. ([#126512](https://github.com/kubernetes/kubernetes/pull/126512), [@kmala](https://github.com/kmala)) [SIG API Machinery, Auth, Etcd and Testing] - Introduced a new metric `kubelet_admission_rejections_total` to track the number of pods rejected during admission. ([#128556](https://github.com/kubernetes/kubernetes/pull/128556), [@AnishShah](https://github.com/AnishShah)) - JWT authenticators now set the `jti` claim (if present and is a string value) as credential id for use by audit logging. ([#127010](https://github.com/kubernetes/kubernetes/pull/127010), [@aramase](https://github.com/aramase)) [SIG API Machinery, Auth and Testing] - kube-apiserver: Promoted `AuthorizeWithSelectors` feature to beta, which includes field and label selector information from requests in webhook authorization calls. Promoted `AuthorizeNodeWithSelectors` feature to beta, which changes node authorizer behavior to limit requests from node API clients, so that each Node can only get / list / watch its own Node API object, and can also only get / list / watch Pod API objects bound to that node. Clients using kubelet credentials to read other nodes or unrelated pods must change their authentication credentials (recommended), adjust their usage, or obtain broader read access independent of the node authorizer. ([#128168](https://github.com/kubernetes/kubernetes/pull/128168), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Auth and Testing] - kube-apiserver: a new `--requestheader-uid-headers` flag allows configuring request header authentication to obtain the authenticating user's UID from the specified headers. The suggested value for the new option is `X-Remote-Uid`. When specified, the `kube-system/extension-apiserver-authentication` configmap will include the value in its `.data[requestheader-uid-headers]` field. ([#115834](https://github.com/kubernetes/kubernetes/pull/115834), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Auth, Cloud Provider and Testing] - kube-proxy uses field-selector clusterIP!=None on Services to avoid watching for Headless Services, reducing unnecessary network bandwidth ([#126769](https://github.com/kubernetes/kubernetes/pull/126769), [@Sakuralbj](https://github.com/Sakuralbj)) [SIG Network] - : `kubeadm upgrade apply` now supports phase sub-command, users can use `kubeadm upgrade apply phase ` to execute the specified phase, or use `kubeadm upgrade apply --skip-phases ` to skip some phases during cluster upgrade. ([#126032](https://github.com/kubernetes/kubernetes/pull/126032), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - kubeadm: `kubeadm upgrade node` now supports `addon` and `post-upgrade` phases. Users can use `kubeadm upgrade node phase addon` to execute the addon upgrade, or use `kubeadm upgrade node --skip-phases addon` to skip the addon upgrade. Currently, the `post-upgrade` phase is no-op, and it is mainly used to handle some release-specific post-upgrade tasks. ([#127242](https://github.com/kubernetes/kubernetes/pull/127242), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - kubeadm: added a validation warning when the certificateValidityPeriod is more than the caCertificateValidityPeriod ([#126538](https://github.com/kubernetes/kubernetes/pull/126538), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - kubeadm: added the feature gate `NodeLocalCRISocket`. When the feature gate is enabled, kubeadm will generate the `/var/lib/kubelet/instance-config.yaml` file to customize the `containerRuntimeEndpoint` field in the kubelet configuration for each node and will not write the same CRI socket on the Node object as an annotation. ([#128031](https://github.com/kubernetes/kubernetes/pull/128031), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle] - kubeadm: allow mixing the flag --config with the special flag --print-manifest of the subphases of 'kubeadm init phase addon'. ([#126740](https://github.com/kubernetes/kubernetes/pull/126740), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: consider --bind-address or --advertise-address and --secure-port for control plane components when the feature gate WaitForAllControlPlaneComponents is enabled. Use /livez for kube-apiserver and kube-scheduler, but continue using /healthz for kube-controller-manager until it supports /livez. ([#128474](https://github.com/kubernetes/kubernetes/pull/128474), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: if an unknown command name is passed to any parent command such as 'kubeadm init phase' return an error. If 'kubeadm init phase' or another command that has subcommands is called without subcommand name, print the available commands and also return an error. ([#127096](https://github.com/kubernetes/kubernetes/pull/127096), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: promoted feature gate `EtcdLearnerMode` to GA. Learner mode in etcd deployed by kubeadm is now locked to enabled by default. ([#126374](https://github.com/kubernetes/kubernetes/pull/126374), [@pacoxu](https://github.com/pacoxu)) [SIG Cluster Lifecycle] - kubelet: add log and event for cgroup v2 with kernel older than 5.8. ([#126595](https://github.com/kubernetes/kubernetes/pull/126595), [@pacoxu](https://github.com/pacoxu)) [SIG Node] - Kubernetes is now built with Go 1.23.3. ([#128852](https://github.com/kubernetes/kubernetes/pull/128852), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Kubernetes is now built with go 1.23.0 ([#127076](https://github.com/kubernetes/kubernetes/pull/127076), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Kubernetes was built with Go 1.23.1. ([#127611](https://github.com/kubernetes/kubernetes/pull/127611), [@haitch](https://github.com/haitch)) [SIG Release and Testing] - Kubernetes was built with Go 1.23.2. ([#128110](https://github.com/kubernetes/kubernetes/pull/128110), [@haitch](https://github.com/haitch)) [SIG Release and Testing] - Label `apps.kubernetes.io/pod-index` added to Pod from StatefulSets is promoted to stable Label `batch.kubernetes.io/job-completion-index` added to Pods from Indexed Jobs is promoted to stable ([#128387](https://github.com/kubernetes/kubernetes/pull/128387), [@alaypatel07](https://github.com/alaypatel07)) [SIG Apps] - LoadBalancerIPMode feature was marked as GA. ([#127348](https://github.com/kubernetes/kubernetes/pull/127348), [@RyanAoh](https://github.com/RyanAoh)) [SIG Apps, Network and Testing] - Locked the custom profiling feature in `kubectl debug` to true. ([#127187](https://github.com/kubernetes/kubernetes/pull/127187), [@ardaguclu](https://github.com/ardaguclu)) [SIG CLI and Testing] - Output for the `ScalingReplicaSet` event has changed from: Scaled replica set to from to: Scaled replica set from to . ([#125118](https://github.com/kubernetes/kubernetes/pull/125118), [@jsoref](https://github.com/jsoref)) [SIG Apps and CLI] - PodLifecycleSleepAction is graduated to GA ([#128046](https://github.com/kubernetes/kubernetes/pull/128046), [@AxeZhan](https://github.com/AxeZhan)) [SIG Architecture, Node and Testing] - Pods were allowed to use the `net.ipv4.tcp_rmem` and `net.ipv4.tcp_wmem` sysctl by default when the kernel version was 4.15 or higher. With the kernel 4.15 the sysctl became namespaced. Pod Security admission allowed these sysctl in v1.32+ versions of the baseline and restricted policies. ([#127489](https://github.com/kubernetes/kubernetes/pull/127489), [@pacoxu](https://github.com/pacoxu)) [SIG Auth, Network and Node] - Prepared Pod validation to handle version skew for InPlacePodVerticalScaling's beta graduation. ([#128186](https://github.com/kubernetes/kubernetes/pull/128186), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) - Promoted `RecoverVolumeExpansionFailure` feature gate to beta. ([#128342](https://github.com/kubernetes/kubernetes/pull/128342), [@gnufied](https://github.com/gnufied)) [SIG Apps and Storage] - Promoted `RetryGenerateName` to stable; the feature is enabled by default. `--feature-gates=RetryGenerateName=true` not needed on kube-apiserver binaries and will be removed in a future release. ([#127093](https://github.com/kubernetes/kubernetes/pull/127093), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery] - Promoted `SizeMemoryBackedVolumes` to stable. ([#126981](https://github.com/kubernetes/kubernetes/pull/126981), [@kannon92](https://github.com/kannon92)) [SIG Node, Storage and Testing] - Promoted the `RelaxedEnvironmentVariableValidation` feature gate to beta and is enabled by default. ([#126897](https://github.com/kubernetes/kubernetes/pull/126897), [@HirazawaUi](https://github.com/HirazawaUi)) - Promoted the feature gates `StrictCostEnforcementForVAP` and `StrictCostEnforcementForWebhooks`. ([#127302](https://github.com/kubernetes/kubernetes/pull/127302), [@cici37](https://github.com/cici37)) [SIG API Machinery and Testing] - Promoted the `ServiceAccountTokenJTI` feature to GA, which adds a `jti` claim to issued service account tokens and embeds the `jti` claim as a `authentication.kubernetes.io/credential-id=["JTI=..."]` value in user extra info - Promoted the `ServiceAccountTokenPodNodeInfo` feature to GA, which adds the node name and uid as claims into service account tokens mounted into running pods, and embeds that information as `authentication.kubernetes.io/node-name` and `authentication.kubernetes.io/node-uid` user extra info when the token is used - Promoted the `ServiceAccountTokenNodeBindingValidation` feature to GA, which validates service account tokens bound directly to nodes. ([#128169](https://github.com/kubernetes/kubernetes/pull/128169), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Auth and Testing] - Realigned line breaks from `kubectl explain` descriptions. ([#126533](https://github.com/kubernetes/kubernetes/pull/126533), [@ah8ad3](https://github.com/ah8ad3)) - Removed attachable volume limits from the capacity of the node for the following volume type when the kubelet was started, affecting the following volume types when the corresponding csi driver was installed: - `awsElasticBlockStore` for `ebs.csi.aws.com` - `azureDisk` for `disk.csi.azure.com` - `gcePersistentDisk` for `pd.csi.storage.googleapis.com` - `cinder` for `cinder.csi.openstack.org` - `csi` However it was still enforced using a limit in CSINode objects. ([#126924](https://github.com/kubernetes/kubernetes/pull/126924), [@carlory](https://github.com/carlory)) - Reverted Go version used to build Kubernetes to 1.23.0. ([#127861](https://github.com/kubernetes/kubernetes/pull/127861), [@xmudrii](https://github.com/xmudrii)) [SIG Release and Testing] - Support inflight_events metric in the scheduler for QueueingHint. ([#127052](https://github.com/kubernetes/kubernetes/pull/127052), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Support specifying a custom network parameter when running e2e-node-tests with the remote option. ([#127574](https://github.com/kubernetes/kubernetes/pull/127574), [@bouaouda-achraf](https://github.com/bouaouda-achraf)) [SIG Node and Testing] - The Job controller now considers sidecar container restart counts when removing pods. ([#124952](https://github.com/kubernetes/kubernetes/pull/124952), [@AxeZhan](https://github.com/AxeZhan)) [SIG Apps and CLI] - The `TopologyManagerPolicyOptions` feature-flag is promoted to GA. ([#128124](https://github.com/kubernetes/kubernetes/pull/128124), [@PiotrProkop](https://github.com/PiotrProkop)) - The scheduler implemented `QueueingHint` in VolumeBinding plugin's CSIDriver event, which enhanced the throughput of scheduling. ([#125171](https://github.com/kubernetes/kubernetes/pull/125171), [@YamasouA](https://github.com/YamasouA)) [SIG Scheduling and Storage] - The scheduler retries gated Pods more appropriately, giving them a backoff penalty too. ([#126029](https://github.com/kubernetes/kubernetes/pull/126029), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Unallowed label values will show up as "unexpected" in scheduler metrics. ([#126762](https://github.com/kubernetes/kubernetes/pull/126762), [@richabanker](https://github.com/richabanker)) [SIG Instrumentation and Scheduling] - Updated the control plane's trust anchor publisher to create and manage a new ClusterTrustBundle object, associated with the `kubernetes.io/kube-apiserver-serving` X.509 certificate signer. This ClusterTrustBundle contains a PEM bundle in its payload that you can use to verify kube-apiserver serving certificates. ([#127326](https://github.com/kubernetes/kubernetes/pull/127326), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Apps, Auth, Cluster Lifecycle and Testing] - Vendor: updated system-validators to v1.9.0. ([#128149](https://github.com/kubernetes/kubernetes/pull/128149), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle and Node] - Vendor: updated system-validators to v1.9.1. ([#128533](https://github.com/kubernetes/kubernetes/pull/128533), [@neolit123](https://github.com/neolit123)) - When `SchedulerQueueingHint` is enabled, the scheduler's in-tree plugins now subscribe to specific node events to decide whether to requeue Pods. This allows the scheduler to handle cluster events faster with less memory. Specific node events include updates to taints, tolerations or allocatable. In-tree plugins now ignore node updates that don't modify any of these fields. ([#127220](https://github.com/kubernetes/kubernetes/pull/127220), [@sanposhiho](https://github.com/sanposhiho)) [SIG Node, Scheduling and Storage] - When `SchedulerQueueingHints` is enabled, clear events cached in the scheduling queue as soon as possible so that the scheduler consumes less memory. ([#120586](https://github.com/kubernetes/kubernetes/pull/120586), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Windows: Support CPU and Topology manager on Windows. ([#125296](https://github.com/kubernetes/kubernetes/pull/125296), [@jsturtevant](https://github.com/jsturtevant)) [SIG Node and Windows] ### Documentation - Clarified the kube-controller-manager documentation for `--allocate-node-cidrs`, `--cluster-cidr`, and `--service-cluster-ip-range` flags to accurately reflect their dependencies and usage conditions. ([#126784](https://github.com/kubernetes/kubernetes/pull/126784), [@eminwux](https://github.com/eminwux)) [SIG API Machinery, Cloud Provider and Docs] - Documented the `--for=create` option to `kubectl wait`. ([#127327](https://github.com/kubernetes/kubernetes/pull/127327), [@ryanwinter](https://github.com/ryanwinter)) [SIG CLI] - Fixed documentation for the `apiserver_admission_webhook_fail_open_count` and `apiserver_admission_webhook_request_total` metrics. The `type` label can have a value of "admit", not "mutating". ([#127898](https://github.com/kubernetes/kubernetes/pull/127898), [@modulitos](https://github.com/modulitos)) - kubeadm: fixed a misleading output (typo) about control-plane joining instructions when executing the "kubeadm init" command. ([#128118](https://github.com/kubernetes/kubernetes/pull/128118), [@amaddio](https://github.com/amaddio)) - The kubelet, when using `--cloud-provider=external` can use the `--node-ip` flag with one of the unspecified addresses 0.0.0.0 or ::, to create the Node with the IP of the default gateway of the corresponding IP family and then delegating the responsibility to the external cloud provider. This solves the bootstrap problems of out of tree cloud providers that are deployed as Pods within the cluster. ([#125337](https://github.com/kubernetes/kubernetes/pull/125337), [@aojea](https://github.com/aojea)) [SIG Cloud Provider, Network, Node and Testing] - Added request header UID propagation, behind an alpha `RemoteRequestHeaderUID` feature gate. ([#129081](https://github.com/kubernetes/kubernetes/pull/129081), [@stalz](https://github.com/stlaz)) [SIG API SIG API Machinery, cluster lifecycle, testing] ### Failing Test - kubelet plugins are now re-registered properly on Windows if the re-registration period is < 15ms. ([#114136](https://github.com/kubernetes/kubernetes/pull/114136), [@claudiubelu](https://github.com/claudiubelu)) [SIG Node, Storage, Testing and Windows] ### Bug or Regression - 1. When the kubelet constructs the CRI mounts for the container which references an `image` volume source type, it passes the missing mount attributes to the CRI implementation, including `readOnly`, `propagation`, and `recursiveReadOnly`. When the readOnly field of the containerMount is explicitly set to false, the kubelet will now take the `readOnly`as true to the CRI implementation because the image volume plugin requires the mount to be read-only. 2. Fixed a bug where the pod is unexpectedly running when the `image` volume source type is used and mounted to `/etc/hosts` in the container. ([#126806](https://github.com/kubernetes/kubernetes/pull/126806), [@carlory](https://github.com/carlory)) [SIG Node and Storage] - Added warnings for overlap paths in ConfigMap, Secret, DownwardAPI, Projected. Added warning for cases when ProjectedVolume with sources is provided. ([#121968](https://github.com/kubernetes/kubernetes/pull/121968), [@Peac36](https://github.com/Peac36)) - Apiserver repair controller is resilient to etcd errors during bootstrap and retries during 30 seconds before failing. ([#126671](https://github.com/kubernetes/kubernetes/pull/126671), [@fusida](https://github.com/fusida)) [SIG Network] - Applyconfiguration-gen no longer generates duplicate methods and ambiguous member accesses when types end up with multiple members of the same name (through embedded structs). ([#127001](https://github.com/kubernetes/kubernetes/pull/127001), [@skitt](https://github.com/skitt)) [SIG API Machinery] - Bookmark events are now sent immediately after all items in the watchCache store have been processed, improving consistency in client behavior. ([#127012](https://github.com/kubernetes/kubernetes/pull/127012), [@Chaunceyctx](https://github.com/Chaunceyctx)) - DRA: fixed several issues related to `allocationMode: all`. ([#127565](https://github.com/kubernetes/kubernetes/pull/127565), [@pohly](https://github.com/pohly)) - DRA: when a DRA driver was started after creating pods which need resources from that driver, no additional attempt was made to schedule such unschedulable pods again. Only affected DRA with structured parameters. ([#126807](https://github.com/kubernetes/kubernetes/pull/126807), [@pohly](https://github.com/pohly)) [SIG Node, Scheduling and Testing] - DRA: when enabling the scheduler queuing hint feature, pods got stuck as unschedulable for a while unnecessarily because recording the name of the generated ResourceClaim did not trigger scheduling. ([#127497](https://github.com/kubernetes/kubernetes/pull/127497), [@pohly](https://github.com/pohly)) [SIG Auth, Node, Scheduling and Testing] - Disallowed label values will show up as "unexpected" in all system components' metrics. ([#128100](https://github.com/kubernetes/kubernetes/pull/128100), [@yongruilin](https://github.com/yongruilin)) [SIG Architecture and Instrumentation] - Discarded the output streams of destination path check in kubectl cp when copying from local to pod and added a 3 seconds timeout to this check ([#126652](https://github.com/kubernetes/kubernetes/pull/126652), [@ardaguclu](https://github.com/ardaguclu)) [SIG CLI] - Fixed 1.31 regression that can crash kube-controller-manager's service-lb-controller loop. ([#128182](https://github.com/kubernetes/kubernetes/pull/128182), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider and Network] - Fixed a 1.31 regression starting kubelet on Windows: Revert "fix: handle socket file detection on Windows". ([#126976](https://github.com/kubernetes/kubernetes/pull/126976), [@jsturtevant](https://github.com/jsturtevant)) - Fixed a 1.31 regression with API emulation versioning honors cohabitating resources. ([#127239](https://github.com/kubernetes/kubernetes/pull/127239), [@xuzhenglun](https://github.com/xuzhenglun)) - Fixed a bug in the endpoints controller that failed to reconcile the Endpoint object after it was truncated (when it received more than 1000 endpoint addresses). ([#127417](https://github.com/kubernetes/kubernetes/pull/127417), [@aojea](https://github.com/aojea)) [SIG Apps, Network and Testing] - Fixed a bug in the garbage collector controller which could block indefinitely due to a cache sync failure. This fix allows the garbage collector to eventually continue garbage collecting other resources if a given resource cannot be listed or watched. Any objects in the unsynced resource type with owner references with `blockOwnerDeletion: true` will not be known to the garbage collector. Use of `blockOwnerDeletion` has always been best-effort and racy on startup and object creation. With this fix, it continues to be best-effort for resources that cannot be synced by the garbage collector controller. ([#125796](https://github.com/kubernetes/kubernetes/pull/125796), [@haorenfsa](https://github.com/haorenfsa)) [SIG API Machinery, Apps and Testing] - Fixed a bug that occurred when the hostname label of a node did not match the node name, pods bound to a PersistentVolume with `nodeAffinity` using the hostname may be scheduled to the wrong node or experience scheduling failures. ([#125398](https://github.com/kubernetes/kubernetes/pull/125398), [@AxeZhan](https://github.com/AxeZhan)) [SIG Scheduling and Storage] - Fixed a bug where `podCIDR` was released before node was deleted. ([#128305](https://github.com/kubernetes/kubernetes/pull/128305), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps and Network] - Fixed a bug where the kubelet ephemerally failed with `failed to initialize top level QOS containers: root container [kubepods] doesn't exist`, due to the cpuset cgroup being deleted on cgroup v2 with systemd cgroup manager. ([#125923](https://github.com/kubernetes/kubernetes/pull/125923), [@haircommander](https://github.com/haircommander)) [SIG Node and Testing] - Fixed a bug where the pod(with regular init containers)'s phase was not pending when the regular init container had not finished running after a node restart. ([#126653](https://github.com/kubernetes/kubernetes/pull/126653), [@zhifei92](https://github.com/zhifei92)) [SIG Node and Testing] - Fixed a bug which the scheduler didn't correctly tell plugins Node deletion. This bug could impact all scheduler plugins subscribing to Node/Delete event, making the queue keep the Pods rejected by those plugins incorrectly at Node deletion. Among the in-tree plugins, PodTopologySpread is the only victim. ([#127464](https://github.com/kubernetes/kubernetes/pull/127464), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling and Testing] - Fixed a bug with dual stack clusters using the beta feature MultiCIDRServiceAllocator which could not create dual stack Services or Services with IPs in the secondary range. Users who wanted to use this feature in version 1.30 with dual stack clusters could work around the issue by setting the feature gate DisableAllocatorDualWrite to true. ([#127598](https://github.com/kubernetes/kubernetes/pull/127598), [@aojea](https://github.com/aojea)) [SIG Network and Testing] - Fixed a possible memory leak in the QueueingHint (alpha feature). ([#126962](https://github.com/kubernetes/kubernetes/pull/126962), [@sanposhiho](https://github.com/sanposhiho)) - Fixed a potential memory leak in QueueingHint (alpha feature). ([#127016](https://github.com/kubernetes/kubernetes/pull/127016), [@sanposhiho](https://github.com/sanposhiho)) - Fixed a race condition in the kube-proxy initialization that could cause UDP traffic to service VIP. ([#126532](https://github.com/kubernetes/kubernetes/pull/126532), [@wedaly](https://github.com/wedaly)) - Fixed a race condition that could result in erroneous volume unmounts for flex volume plugins during kubelet restart. ([#127669](https://github.com/kubernetes/kubernetes/pull/127669), [@olyazavr](https://github.com/olyazavr)) - Fixed a race condition that could result in erroneous volume unmounts for flex volume plugins on kubelet restart. ([#128495](https://github.com/kubernetes/kubernetes/pull/128495), [@olyazavr](https://github.com/olyazavr)) - Fixed a regression in 1.29+ default configurations, where regular init containers may fail to start due to a temporary container runtime failure. ([#127162](https://github.com/kubernetes/kubernetes/pull/127162), [@gjkim42](https://github.com/gjkim42)) [SIG Node] - Fixed a regression in default 1.29 configurations with the `SidecarContainers` feature enabled, where init containers may fail to start due to a temporary container runtime failure. ([#126543](https://github.com/kubernetes/kubernetes/pull/126543), [@gjkim42](https://github.com/gjkim42)) - Fixed a regression introduced in v1.29 where conntrack entries for UDP connections to deleted pods did not get cleaned up correctly, which could (among other things) cause DNS problems when DNS pods were restarted. ([#127780](https://github.com/kubernetes/kubernetes/pull/127780), [@danwinship](https://github.com/danwinship)) - Fixed a scheduler preemption issue where the victim pod was not deleted due to incorrect status patching. This issue occurred when the preemptor and victim pods had different QoS classes in their status, causing the preemption to fail entirely. ([#126644](https://github.com/kubernetes/kubernetes/pull/126644), [@Huang-Wei](https://github.com/Huang-Wei)) - Fixed a suboptimal scheduler preemption behavior where potential preemption victims were violating Pod Disruption Budgets. ([#128307](https://github.com/kubernetes/kubernetes/pull/128307), [@NoicFank](https://github.com/NoicFank)) [SIG Scheduling] - Fixed an issue in the kubelet that showed when writeable layers and read-only layers were at different paths within the same mount. Kubernetes was previously detecting that the image filesystem was split, even when that was not really the case ([#128344](https://github.com/kubernetes/kubernetes/pull/128344), [@kannon92](https://github.com/kannon92)) [SIG Node] - Fixed an issue in the kubelet that showed when writeable layers and read-only layers were at different paths within the same mount. Kubernetes was previously detecting that the image filesystem was split, even when that was not really the case. ([#126562](https://github.com/kubernetes/kubernetes/pull/126562), [@kannon92](https://github.com/kannon92)) - Fixed an issue where eviction manager was not deleting unused images or containers. ([#127874](https://github.com/kubernetes/kubernetes/pull/127874), [@AnishShah](https://github.com/AnishShah)) - Fixed an issue where requests sent by the KMSv2 service would be rejected due to having an invalid authority header. ([#126930](https://github.com/kubernetes/kubernetes/pull/126930), [@Ruddickmg](https://github.com/Ruddickmg)) [SIG API Machinery and Auth] - Fixed data race in kubelet/volumemanager. ([#127919](https://github.com/kubernetes/kubernetes/pull/127919), [@carlory](https://github.com/carlory)) [SIG Apps, Node and Storage] - Fixed fake client to accept request without metadata.name to better emulate behavior of actual client. ([#126727](https://github.com/kubernetes/kubernetes/pull/126727), [@jpbetz](https://github.com/jpbetz)) - Fixed the ability to set the `resolvConf` option in drop-in kubelet configuration files, which validates that drop-in kubelet configuration files are in a supported version. ([#127421](https://github.com/kubernetes/kubernetes/pull/127421), [@liggitt](https://github.com/liggitt)) - Fixed the bug in `NodeUnschedulable` that only happens with QHint enabled, which the scheduler might miss some updates for the Pods rejected by NodeUnschedulable plugin and put the Pods in the queue for a longer time than needed. ([#127427](https://github.com/kubernetes/kubernetes/pull/127427), [@sanposhiho](https://github.com/sanposhiho)) - Fixed the estimated cost in CEL for expressions that perform equality checks on IPs, CIDRs, Quantities, Formats and URLs. ([#126359](https://github.com/kubernetes/kubernetes/pull/126359), [@jpbetz](https://github.com/jpbetz)) - Fixed the incorrect help message of a metric "graceful_shutdown_end_time_seconds". Fixed incorrect value set for metrics "graceful_shutdown_start_time_seconds" and "graceful_shutdown_end_time_seconds" in certain cases during graceful node shutdown. ([#128189](https://github.com/kubernetes/kubernetes/pull/128189), [@zylxjtu](https://github.com/zylxjtu)) [SIG Node] - Fixed the reporting of elapsed times during evaluation of `ValidatingAdmissionPolicy` decisions and annotations. The apiserver_validating_admission_policy_check_duration metrics will now show elapsed times and no longer be zero. ([#128463](https://github.com/kubernetes/kubernetes/pull/128463), [@knrc](https://github.com/knrc)) - Fixed the wrong hierarchical structure for both the child span and the parent span (i.e. `SerializeObject` and `List`). In the past, some children's spans appeared parallel to their parents. ([#127551](https://github.com/kubernetes/kubernetes/pull/127551), [@carlory](https://github.com/carlory)) [SIG API Machinery and Instrumentation] - Fixed: dynamic client-go can now handle subresources with an UnstructuredList response ([#126809](https://github.com/kubernetes/kubernetes/pull/126809), [@ryantxu](https://github.com/ryantxu)) [SIG API Machinery] - Fixed a bug where restartable and non-restartable init containers were not accounted for in the message and annotations of eviction event. ([#124947](https://github.com/kubernetes/kubernetes/pull/124947), [@toVersus](https://github.com/toVersus)) [SIG Node] - Fixed a kubelet and kube-apiserver memory leak in default 1.29 configurations related to tracing. ([#126957](https://github.com/kubernetes/kubernetes/pull/126957), [@dashpole](https://github.com/dashpole)) [SIG API Machinery, Architecture, Instrumentation and Node] - Fixed the bug in PodTopologySpread that only happens with QHint enabled, which the scheduler might miss some updates for the Pods rejected by PodTopologySpread plugin and put the Pods in the queue for a longer time than needed. ([#127447](https://github.com/kubernetes/kubernetes/pull/127447), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - For Dynamic Resource Allocation, labels in node selectors now are validated. Invalid labels already caused runtime errors before and are unlikely to occur in practice. ([#128932](https://github.com/kubernetes/kubernetes/pull/128932), [@pohly](https://github.com/pohly)) - For Dynamic Resource Allocation, the new "v1beta1" kubelet gPRC was renamed so that the protobuf package name is unique. ([#128764](https://github.com/kubernetes/kubernetes/pull/128764), [@pohly](https://github.com/pohly)) [SIG Node and Testing] - HostNetwork pods no longer depend on the PodIPs to be assigned to configure the defined hostAliases on the Pod ([#126460](https://github.com/kubernetes/kubernetes/pull/126460), [@aojea](https://github.com/aojea)) [SIG Network, Node and Testing] - If a client makes an API streaming requests and specifies an `application/json;as=Table` content type, the API server now responds with a 406 (Not Acceptable) error. This change helps to ensure that unsupported formats, such as `Table` representations are correctly rejected. ([#126996](https://github.com/kubernetes/kubernetes/pull/126996), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery and Testing] - If an old pod spec has used image volume source, we must allow it when updating the resource even if the feature-gate ImageVolume is disabled. ([#126733](https://github.com/kubernetes/kubernetes/pull/126733), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Node] - Improved PVC Protection Controller's scalability by batch-processing PVCs by namespace with lazy live pod listing. ([#125372](https://github.com/kubernetes/kubernetes/pull/125372), [@hungnguyen243](https://github.com/hungnguyen243)) [SIG Apps, Node, Storage and Testing] - Improved the scalability of the PVC Protection Controller by batch-processing PVCs by namespace and implementing lazy live pod listing. ([#126745](https://github.com/kubernetes/kubernetes/pull/126745), [@hungnguyen243](https://github.com/hungnguyen243)) [SIG Apps, Storage and Testing] - kube-apiserver: fixed a 1.31 regression that stopped honoring build ID overrides with the --version flag ([#126665](https://github.com/kubernetes/kubernetes/pull/126665), [@liggitt](https://github.com/liggitt)) [SIG API Machinery] - kubeadm: added "disable success" and "disable denial" as parameters of the "cache" plugin in the Corefile managed by kubeadm. This is to prevent conflicting responses during CoreDNS cache updates. ([#128359](https://github.com/kubernetes/kubernetes/pull/128359), [@matteriben](https://github.com/matteriben)) [SIG Cluster Lifecycle] - kubeadm: ensure that Pods from the upgrade preflight check `CreateJob` are properly terminated after a timeout. ([#127333](https://github.com/kubernetes/kubernetes/pull/127333), [@yuyabee](https://github.com/yuyabee)) [SIG Cluster Lifecycle] - kubeadm: fixed an issue where the wrong member list was being reported when removing an etcd member. ([#127650](https://github.com/kubernetes/kubernetes/pull/127650), [@SataQiu](https://github.com/SataQiu)) - kubeadm: when adding new control plane nodes with `kubeamd join`, ensure that the etcd member addition is performed only if a given member URL does not already exist in the list of members. Similarly, on "kubeadm reset" only remove an etcd member if its ID exists. ([#127491](https://github.com/kubernetes/kubernetes/pull/127491), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - kubelet now attempts to get an existing node if the request to create it fails with StatusForbidden. ([#126318](https://github.com/kubernetes/kubernetes/pull/126318), [@hoskeri](https://github.com/hoskeri)) [SIG Node] - kubelet: Fix - the volume manager didn't check the device mount state in the actual state of the world before marking the volume as detached. It may cause a pod to be stuck in the Terminating state due to the above issue when it was deleted. ([#128219](https://github.com/kubernetes/kubernetes/pull/128219), [@carlory](https://github.com/carlory)) - kubelet: Fixed a bug where kubelet wrongly drops the QOSClass field of the Pod's status when it rejects a Pod. ([#128083](https://github.com/kubernetes/kubernetes/pull/128083), [@carlory](https://github.com/carlory)) [SIG Node and Testing] - kubelet: use the CRI stats provider if `PodAndContainerStatsFromCRI` feature is enabled ([#126488](https://github.com/kubernetes/kubernetes/pull/126488), [@haircommander](https://github.com/haircommander)) [SIG Node] - Made kubelet's /metrics/slis endpoint always available. ([#128430](https://github.com/kubernetes/kubernetes/pull/128430), [@richabanker](https://github.com/richabanker)) [SIG Architecture, Instrumentation and Node] - Node shutdown controller made a best effort to wait for CSI Drivers to complete the volume teardown process according to the pod priority groups. ([#125070](https://github.com/kubernetes/kubernetes/pull/125070), [@torredil](https://github.com/torredil)) [SIG Node, Storage and Testing] - Reduced memory usage/allocations during wait for volume attachment. ([#126575](https://github.com/kubernetes/kubernetes/pull/126575), [@Lucaber](https://github.com/Lucaber)) [SIG Node and Storage] - Removed unneeded permissions for system:controller:persistent-volume-binder and system:controller:expand-controller clusterroles ([#125995](https://github.com/kubernetes/kubernetes/pull/125995), [@carlory](https://github.com/carlory)) [SIG Auth and Storage] - Reset streams when an error happens during port-forward allowing kubectl to maintain port-forward connection open. ([#128318](https://github.com/kubernetes/kubernetes/pull/128318), [@soltysh](https://github.com/soltysh)) [SIG API Machinery, CLI and Node] - Send an error on `ResultChan` and close the `RetryWatcher` when the client is forbidden or unauthorized from watching the resource. ([#126038](https://github.com/kubernetes/kubernetes/pull/126038), [@mprahl](https://github.com/mprahl)) [SIG API Machinery] - Terminated Pods on a node will not be re-admitted on kubelet restart. This fixes the problem of Completed Pods awaiting for the finalizer marked as Failed after the kubelet restart. ([#126343](https://github.com/kubernetes/kubernetes/pull/126343), [@SergeyKanzhelev](https://github.com/SergeyKanzhelev)) [SIG Node and Testing] - The CSI volume plugin stopped watching the VolumeAttachment object if the object is not found or the volume is not attached when kubelet waits for a volume attached. In the past, it would fail due to missing permission. ([#126961](https://github.com/kubernetes/kubernetes/pull/126961), [@carlory](https://github.com/carlory)) [SIG Storage] - The Usage and VolumeCondition are both optional in the response and if CSIVolumeHealth feature gate is enabled kubelet needs to consider returning metrics if either one is set. ([#127021](https://github.com/kubernetes/kubernetes/pull/127021), [@Madhu-1](https://github.com/Madhu-1)) [SIG Storage] - The `build-tag` flag is reintroduced to conversion-gen and defaulter-gen which allow users to inject custom build tag during code generation process. ([#128259](https://github.com/kubernetes/kubernetes/pull/128259), [@dinhxuanvu](https://github.com/dinhxuanvu)) - Fixed problem with named ports not being available when specified in sidecar containers. ([#127976](https://github.com/kubernetes/kubernetes/pull/127976), [@chengjoey](https://github.com/chengjoey)) - The scheduler started considering the resource requests of existing sidecar containers during the scoring process. ([#127878](https://github.com/kubernetes/kubernetes/pull/127878), [@AxeZhan](https://github.com/AxeZhan)) [SIG Scheduling and Testing] - Tighten validation on the qosClass field of pod status. This field is immutable but it would be populated with the old status by kube-apiserver if it is unset in the new status when updating this field via the status subsource. ([#127744](https://github.com/kubernetes/kubernetes/pull/127744), [@carlory](https://github.com/carlory)) [SIG Apps, Instrumentation, Node, Storage and Testing] - Upgraded coreDNS to v1.11.3. ([#126449](https://github.com/kubernetes/kubernetes/pull/126449), [@BenTheElder](https://github.com/BenTheElder)) [SIG Cloud Provider and Cluster Lifecycle] - Use allocatedResources on PVC for node expansion in kubelet ([#126600](https://github.com/kubernetes/kubernetes/pull/126600), [@gnufied](https://github.com/gnufied)) [SIG Node, Storage and Testing] - When entering a value other than "external" to the "--cloud-provider" flag for the kubelet, kube-controller-manager, and kube-apiserver, the user will now receive a warning in the logs about the disablement of internal cloud providers, this is in contrast to the previous warnings about deprecation. ([#127711](https://github.com/kubernetes/kubernetes/pull/127711), [@elmiko](https://github.com/elmiko)) [SIG API Machinery, Cloud Provider and Node] - `StartupProbe` was explicitly stopped when the `successThreshold` was reached. This eliminated the problem of executing `StartupProbe` more times than the `successThreshold`. ([#121206](https://github.com/kubernetes/kubernetes/pull/121206), [@mochizuki875](https://github.com/mochizuki875)) - kubelet: on Windows, consistently resolve filesystem links to volume identifiers instead of inconsistently normalizing to drive letters. ([#129103](https://github.com/kubernetes/kubernetes/pull/129103), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Cluster Lifecycle, Instrumentation, Network, Node, Release, Storage and Windows] ### Other (Cleanup or Flake) - Added a short output format argument for `kubectl explain`. You could now use `-o` as an abbreviation for `--output` in commands such as `kubectl explain --output plaintext-openapiv2`. ([#127869](https://github.com/kubernetes/kubernetes/pull/127869), [@ak20102763](https://github.com/ak20102763)) - Added an example for kubectl delete with the --interactive flag. ([#127512](https://github.com/kubernetes/kubernetes/pull/127512), [@bergerhoffer](https://github.com/bergerhoffer)) [SIG CLI] - Added: Log Line for Debugging possible merge errors for kubelet related Config requests. ([#124389](https://github.com/kubernetes/kubernetes/pull/124389), [@holgerson97](https://github.com/holgerson97)) - Aggregated Discovery v2beta1 fixture is removed in `./api/discovery`. Please use v2 ([#127008](https://github.com/kubernetes/kubernetes/pull/127008), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery] - Append the image pull error for the pods `status.containerStatuses[*].state.waiting.message` when in image pull back-off (`reason` is `ImagePullBackOff`) instead of the generic `Back-off pulling image…` message. ([#127918](https://github.com/kubernetes/kubernetes/pull/127918), [@saschagrunert](https://github.com/saschagrunert)) [SIG Node and Testing] - CBOR-encoded watch responses now set the Content-Type header to "application/cbor-seq" instead of the nonconformant "application/cbor". ([#128501](https://github.com/kubernetes/kubernetes/pull/128501), [@benluddy](https://github.com/benluddy)) [SIG API Machinery, Etcd and Testing] - CRI client now used the default timeout for `ImageFsInfo` RPC. ([#128052](https://github.com/kubernetes/kubernetes/pull/128052), [@saschagrunert](https://github.com/saschagrunert)) - Clarified an API validation error for toleration if `operator` is `Exists` and `value` is not empty. ([#128119](https://github.com/kubernetes/kubernetes/pull/128119), [@saschagrunert](https://github.com/saschagrunert)) [SIG API Machinery and Apps] - Device manager: stop using annotations to pass CDI device info to runtimes. Containerd versions older than v1.7.2 don't support passing CDI info through CRI and need to be upgraded. ([#126435](https://github.com/kubernetes/kubernetes/pull/126435), [@bart0sh](https://github.com/bart0sh)) [SIG Node] - Dropped support for `InPlacePodVerticalScaling` feature in Windows. ([#128623](https://github.com/kubernetes/kubernetes/pull/128623), [@AnishShah](https://github.com/AnishShah)) [SIG Apps and Node] - Enabled `CBORServingAndStorage` feature gate – built-in APIs can now be served in CBOR format for clients that request it. ([#128503](https://github.com/kubernetes/kubernetes/pull/128503), [@benluddy](https://github.com/benluddy)) [SIG API Machinery, Etcd and Testing] - Fake clientsets now use a common, generic implementation. The corresponding structs are now private; callers must use the corresponding constructors. ([#126503](https://github.com/kubernetes/kubernetes/pull/126503), [@skitt](https://github.com/skitt)) [SIG API Machinery, Architecture, Auth and Instrumentation] - Feature `AllowServiceLBStatusOnNonLB` remains deprecated and is now locked to false to support compatibility versions. ([#128139](https://github.com/kubernetes/kubernetes/pull/128139), [@Jefftree](https://github.com/Jefftree)) - Feature gate "AllowServiceLBStatusOnNonLB" has been removed. This gate has been stable and unchanged for over a year. ([#126786](https://github.com/kubernetes/kubernetes/pull/126786), [@thockin](https://github.com/thockin)) [SIG Apps] - Fixed a warning message about the gce in-tree cloud provider state. ([#126773](https://github.com/kubernetes/kubernetes/pull/126773), [@carlory](https://github.com/carlory)) - Fixed spacing in `--validate flag` description in kubectl. ([#128081](https://github.com/kubernetes/kubernetes/pull/128081), [@soltysh](https://github.com/soltysh)) - Fixes a bug in the `k8s.io/cloud-provider/service` controller, it may panic when a service is updated because the event recorder was used before it was initialized. All cloud providers should using the `v1.31.0` cloud provider service controller must ensure that the controllers is initialized before the informer start to process events or update it to the version 1.32.0. ([#128179](https://github.com/kubernetes/kubernetes/pull/128179), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider, Network and Testing] - Fully removed `PostStartHookContext.StopCh`. ([#127341](https://github.com/kubernetes/kubernetes/pull/127341), [@mjudeikis](https://github.com/mjudeikis)) - kube-apiserver `--admission-control-config-file` files are now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now cause an error. ([#128013](https://github.com/kubernetes/kubernetes/pull/128013), [@seans3](https://github.com/seans3)) - kube-apiserver `--egress-selector-config-file` files were validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now cause an error. ([#128011](https://github.com/kubernetes/kubernetes/pull/128011), [@seans3](https://github.com/seans3)) [SIG API Machinery and Testing] - kube-apiserver `ResourceQuotaConfiguration` admission plugin subsection within `--admission-control-config-file` files were validated strictly (EnableStrict). Duplicate and unknown fields in the configuration would cause an error. ([#128038](https://github.com/kubernetes/kubernetes/pull/128038), [@seans3](https://github.com/seans3)) - kube-controller-manager `--leader-migration-config` files were now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration would cause an error. ([#128009](https://github.com/kubernetes/kubernetes/pull/128009), [@seans3](https://github.com/seans3)) [SIG API Machinery and Cloud Provider] - kube-proxy initialization waits for all pre-sync events from node and serviceCIDR informers to be delivered. ([#126561](https://github.com/kubernetes/kubernetes/pull/126561), [@wedaly](https://github.com/wedaly)) [SIG Network] - kube-proxy will no longer depend on conntrack binary for stale UDP connections cleanup ([#126847](https://github.com/kubernetes/kubernetes/pull/126847), [@aroradaman](https://github.com/aroradaman)) [SIG Cluster Lifecycle, Network and Testing] - kubeadm: don't warn if `crictl` binary does not exist since kubeadm does not rely on `crictl` since v1.31. ([#126596](https://github.com/kubernetes/kubernetes/pull/126596), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cluster Lifecycle] - kubeadm: increased the verbosity of API client dry-run actions during the subcommands "init", "join", "upgrade" and "reset". It also allowed dry-run on 'kubeadm join' even if there was no existing cluster by utilizing a faked, in-memory cluster-info ConfigMap. ([#126776](https://github.com/kubernetes/kubernetes/pull/126776), [@neolit123](https://github.com/neolit123)) - kubeadm: make sure the extra environment variables written to a kubeadm managed PodSpec are sorted alpha-numerically by the environment variable name. ([#126743](https://github.com/kubernetes/kubernetes/pull/126743), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: removed the deprecated sub-phase of 'init kubelet-finilize' called `experimental-cert-rotation`, and use 'enable-client-cert-rotation' instead. ([#126913](https://github.com/kubernetes/kubernetes/pull/126913), [@pacoxu](https://github.com/pacoxu)) [SIG Cluster Lifecycle] - kubeadm: removed `socat` and `ebtables` from kubeadm preflight checks ([#127151](https://github.com/kubernetes/kubernetes/pull/127151), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cluster Lifecycle] - kubeadm: removed preflight check for existence of the conntrack binary, as conntrack is no longer a kube-proxy dependency in version 1.32 and newer. ([#126953](https://github.com/kubernetes/kubernetes/pull/126953), [@aroradaman](https://github.com/aroradaman)) - kubeadm: removed the deprecated and NO-OP flags `--feature-gates` for `kubeadm upgrade apply` and `--api-server-manifest`, `--controller-manager-manifest`, and `--scheduler-manifest` for `kubeadm upgrade diff`. ([#127123](https://github.com/kubernetes/kubernetes/pull/127123), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - kubeadm: removed the deprecated flag `--experimental-output`, please use the flag `--output` instead that serves the same purpose. Affected commands are: `kubeadm config images list`, `kubeadm token list`, `kubeadm upgrade plan`, `kubeadm certs check-expiration`. ([#126914](https://github.com/kubernetes/kubernetes/pull/126914), [@carlory](https://github.com/carlory)) [SIG Cluster Lifecycle] - kubeadm: switched the kube-scheduler static Pod to use the endpoints `/livez` (for startup and liveness probes) and `/readyz` (for the readiness probe). Previously, `/healthz` was used for all probes, which is deprecated behavior in the scope of this component. ([#126945](https://github.com/kubernetes/kubernetes/pull/126945), [@liangyuanpeng](https://github.com/liangyuanpeng)) [SIG Cluster Lifecycle] - Optimized the code by filtering out empty strings for podUID when calling the `getPodAndContainerForDevice` method. ([#126997](https://github.com/kubernetes/kubernetes/pull/126997), [@lengrongfu](https://github.com/lengrongfu)) - Output a log as v4-level when a probe is triggered and shift the periodic timer of ReadinessProbe after manual run. ([#119089](https://github.com/kubernetes/kubernetes/pull/119089), [@mochizuki875](https://github.com/mochizuki875)) - Removed generally available feature gate `ValidatingAdmissionPolicy`. ([#126645](https://github.com/kubernetes/kubernetes/pull/126645), [@cici37](https://github.com/cici37)) [SIG API Machinery, Auth, and Testing] - Removed generally available feature gate `CloudDualStackNodeIPs`. ([#126840](https://github.com/kubernetes/kubernetes/pull/126840), [@carlory](https://github.com/carlory)) [SIG API Machinery and Cloud Provider] - Removed generally available feature gate `LegacyServiceAccountTokenCleanUp`. ([#126839](https://github.com/kubernetes/kubernetes/pull/126839), [@carlory](https://github.com/carlory)) [SIG Auth] - Removed generally available feature gate `MinDomainsInPodTopologySpread`. ([#126863](https://github.com/kubernetes/kubernetes/pull/126863), [@carlory](https://github.com/carlory)) [SIG Scheduling] - Removed generally available feature gate `NewVolumeManagerReconstruction`. ([#126775](https://github.com/kubernetes/kubernetes/pull/126775), [@carlory](https://github.com/carlory)) [SIG Node and Storage] - Removed generally available feature gate `NodeOutOfServiceVolumeDetach` ([#127019](https://github.com/kubernetes/kubernetes/pull/127019), [@carlory](https://github.com/carlory)) [SIG Apps and Testing] - Removed generally available feature gate `StableLoadBalancerNodeSet`. ([#126841](https://github.com/kubernetes/kubernetes/pull/126841), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider and Network] - Removed generally available feature-gate `ZeroLimitedNominalConcurrencyShares` ([#126894](https://github.com/kubernetes/kubernetes/pull/126894), [@carlory](https://github.com/carlory)) [SIG API Machinery] - Removed legacy cloud provider integration code and the "service-lb-controller", "cloud-node-lifecycle-controller" and the "node-route-controller" from kube-controller-manager. You can now either set the `--cloud-provider` command line argument to "external", or to the empty string. All other values are invalid. ([#128197](https://github.com/kubernetes/kubernetes/pull/128197), [@aojea](https://github.com/aojea)) [SIG API Machinery, Apps and Cloud Provider] - Removed support for removing requests and limits during a pod resize. ([#128683](https://github.com/kubernetes/kubernetes/pull/128683), [@AnishShah](https://github.com/AnishShah)) [SIG Apps, Node and Testing] - Removed support for the kubelet `--runonce` mode. If you specify the kubelet command line flag `--runonce`, this is an error. Setting `runOnce` in a kubelet configuration file is also an error, and specifying any value for that configuration option is now deprecated. ([#126336](https://github.com/kubernetes/kubernetes/pull/126336), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Node and Scalability] - Removed the GAed feature gates for `ServerSideApply` and `ServerSideFieldValidation`. ([#127058](https://github.com/kubernetes/kubernetes/pull/127058), [@carlory](https://github.com/carlory)) - Removed the `KMSv2` and `KMSv2KDF` feature gates. The associated features graduated to stable in the Kubernetes v1.29 release. ([#126698](https://github.com/kubernetes/kubernetes/pull/126698), [@enj](https://github.com/enj)) [SIG API Machinery, Auth and Testing] - Removed the feature gate ComponentSLIs, which had been promoted to stable since v1.29. ([#127787](https://github.com/kubernetes/kubernetes/pull/127787), [@Jefftree](https://github.com/Jefftree)) [SIG Architecture and Instrumentation] - Revised error handling for port forwards to Pods. Added stream resets preventing port-forward from blockage. ([#128681](https://github.com/kubernetes/kubernetes/pull/128681), [@soltysh](https://github.com/soltysh)) [SIG API Machinery, CLI and Testing] - Short circuit if the compaction request from apiserver is disabled. ([#126627](https://github.com/kubernetes/kubernetes/pull/126627), [@fusida](https://github.com/fusida)) [SIG Etcd] - Show a warning message to inform users that the `legacy` profile is planned to be deprecated. ([#127230](https://github.com/kubernetes/kubernetes/pull/127230), [@mochizuki875](https://github.com/mochizuki875)) [SIG CLI] - The `dynamicResources` has been refactored to `DynamicResources`, now users can introduce the `DynamicResources` struct outside the `dynamicresources` package. ([#128399](https://github.com/kubernetes/kubernetes/pull/128399), [@JesseStutler](https://github.com/JesseStutler)) [SIG Node and Scheduling] - The `flowcontrol.apiserver.k8s.io/v1beta3` API version of `FlowSchema` and `PriorityLevelConfiguration` is no longer served in v1.32. Migrate manifests and API clients to use the `flowcontrol.apiserver.k8s.io/v1` API version, available since v1.29. More information is at https://kubernetes.io/docs/reference/using-api/deprecation-guide/#flowcontrol-resources-v132 ([#127017](https://github.com/kubernetes/kubernetes/pull/127017), [@carlory](https://github.com/carlory)) [SIG API Machinery and Testing] - The alpha Dynamic Resource Allocation gRPC API is still available, but might be removed in future releases. Driver authors should update their DRA drivers to use the v1beta1 gRPC API. ([#128646](https://github.com/kubernetes/kubernetes/pull/128646), [@pohly](https://github.com/pohly)) [SIG Node and Testing] - The feature-gate "PodHostIPs" has been removed. It is GA and its value has been locked since Kubernetes v1.30. ([#128634](https://github.com/kubernetes/kubernetes/pull/128634), [@thockin](https://github.com/thockin)) [SIG Apps, Architecture, Node and Testing] - The getters for the field name and typeDescription of the Reflector struct were renamed. ([#128035](https://github.com/kubernetes/kubernetes/pull/128035), [@alexanderstephan](https://github.com/alexanderstephan)) - The kube-apiserver `--tracing-config-file` is now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now result in an error. ([#128073](https://github.com/kubernetes/kubernetes/pull/128073), [@seans3](https://github.com/seans3)) - The members name and typeDescription of the Reflector struct were exported to allow for better user extensibility. ([#127663](https://github.com/kubernetes/kubernetes/pull/127663), [@alexanderstephan](https://github.com/alexanderstephan)) - Changed the percentage marker in `kubectl top node` from `%` to `(%)`. ([#126995](https://github.com/kubernetes/kubernetes/pull/126995), [@googs1025](https://github.com/googs1025)) [SIG CLI] - Updated cni-plugins to [v1.5.1](https://github.com/containernetworking/plugins/releases/tag/v1.5.1). ([#126966](https://github.com/kubernetes/kubernetes/pull/126966), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider, Node and Testing] - Updated cni-plugins to [v1.6.0](https://github.com/containernetworking/plugins/releases/tag/v1.6.0). ([#128091](https://github.com/kubernetes/kubernetes/pull/128091), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider, Node and Testing] - Updated cri-tools to v1.31.0. ([#126590](https://github.com/kubernetes/kubernetes/pull/126590), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider and Node] - Upgraded etcd client to v3.5.16. ([#127279](https://github.com/kubernetes/kubernetes/pull/127279), [@serathius](https://github.com/serathius)) [SIG API Machinery, Auth, Cloud Provider and Node] - Upgraded github.com/coredns/corefile-migration to v1.0.24. ([#126851](https://github.com/kubernetes/kubernetes/pull/126851), [@BenTheElder](https://github.com/BenTheElder)) [SIG Architecture and Cluster Lifecycle] - Upgraded the functionality of `kubectl kustomize` as described at https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.4.2 and https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.5.0. ([#127965](https://github.com/kubernetes/kubernetes/pull/127965), [@koba1t](https://github.com/koba1t)) - `ComponentSLIs` feature is marked as GA and locked. ([#128317](https://github.com/kubernetes/kubernetes/pull/128317), [@Jefftree](https://github.com/Jefftree)) [SIG Architecture and Instrumentation] - `kubectl apply --server-side` now supports `--subresource` congruent to `kubectl patch`. ([#127634](https://github.com/kubernetes/kubernetes/pull/127634), [@deads2k](https://github.com/deads2k)) [SIG CLI and Testing] - kubelet: fixed an issue mounting CSI volumes on Windows nodes in 1.32.0 release candidates. ([#129083](https://github.com/kubernetes/kubernetes/pull/129083) [liggitt](https://github.com/liggitt)) [SIG API Machinery, architecture, auth, cli, cloud-provider, cluster-lifecycle, instrumentation,network,node, release, storage, windows ] ## Dependencies ### Added - github.com/Microsoft/hnslib: [v0.0.8](https://github.com/Microsoft/hnslib/tree/v0.0.8) - github.com/aws/aws-sdk-go-v2/config: [v1.27.24](https://github.com/aws/aws-sdk-go-v2/tree/config/v1.27.24) - github.com/aws/aws-sdk-go-v2/credentials: [v1.17.24](https://github.com/aws/aws-sdk-go-v2/tree/credentials/v1.17.24) - github.com/aws/aws-sdk-go-v2/feature/ec2/imds: [v1.16.9](https://github.com/aws/aws-sdk-go-v2/tree/feature/ec2/imds/v1.16.9) - github.com/aws/aws-sdk-go-v2/internal/configsources: [v1.3.13](https://github.com/aws/aws-sdk-go-v2/tree/internal/configsources/v1.3.13) - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2: [v2.6.13](https://github.com/aws/aws-sdk-go-v2/tree/internal/endpoints/v2/v2.6.13) - github.com/aws/aws-sdk-go-v2/internal/ini: [v1.8.0](https://github.com/aws/aws-sdk-go-v2/tree/internal/ini/v1.8.0) - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding: [v1.11.3](https://github.com/aws/aws-sdk-go-v2/tree/service/internal/accept-encoding/v1.11.3) - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url: [v1.11.15](https://github.com/aws/aws-sdk-go-v2/tree/service/internal/presigned-url/v1.11.15) - github.com/aws/aws-sdk-go-v2/service/sso: [v1.22.1](https://github.com/aws/aws-sdk-go-v2/tree/service/sso/v1.22.1) - github.com/aws/aws-sdk-go-v2/service/ssooidc: [v1.26.2](https://github.com/aws/aws-sdk-go-v2/tree/service/ssooidc/v1.26.2) - github.com/aws/aws-sdk-go-v2/service/sts: [v1.30.1](https://github.com/aws/aws-sdk-go-v2/tree/service/sts/v1.30.1) - github.com/aws/aws-sdk-go-v2: [v1.30.1](https://github.com/aws/aws-sdk-go-v2/tree/v1.30.1) - github.com/aws/smithy-go: [v1.20.3](https://github.com/aws/smithy-go/tree/v1.20.3) - github.com/checkpoint-restore/go-criu/v6: [v6.3.0](https://github.com/checkpoint-restore/go-criu/tree/v6.3.0) - github.com/containerd/containerd/api: [v1.7.19](https://github.com/containerd/containerd/tree/api/v1.7.19) - github.com/containerd/errdefs: [v0.1.0](https://github.com/containerd/errdefs/tree/v0.1.0) - github.com/containerd/log: [v0.1.0](https://github.com/containerd/log/tree/v0.1.0) - github.com/containerd/typeurl/v2: [v2.2.0](https://github.com/containerd/typeurl/tree/v2.2.0) - github.com/moby/docker-image-spec: [v1.3.1](https://github.com/moby/docker-image-spec/tree/v1.3.1) - github.com/moby/sys/user: [v0.3.0](https://github.com/moby/sys/tree/user/v0.3.0) - github.com/moby/sys/userns: [v0.1.0](https://github.com/moby/sys/tree/userns/v0.1.0) - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp: v1.27.0 ### Changed - cel.dev/expr: v0.15.0 → v0.18.0 - cloud.google.com/go/accessapproval: v1.7.1 → v1.7.4 - cloud.google.com/go/accesscontextmanager: v1.8.1 → v1.8.4 - cloud.google.com/go/aiplatform: v1.48.0 → v1.58.0 - cloud.google.com/go/analytics: v0.21.3 → v0.22.0 - cloud.google.com/go/apigateway: v1.6.1 → v1.6.4 - cloud.google.com/go/apigeeconnect: v1.6.1 → v1.6.4 - cloud.google.com/go/apigeeregistry: v0.7.1 → v0.8.2 - cloud.google.com/go/appengine: v1.8.1 → v1.8.4 - cloud.google.com/go/area120: v0.8.1 → v0.8.4 - cloud.google.com/go/artifactregistry: v1.14.1 → v1.14.6 - cloud.google.com/go/asset: v1.14.1 → v1.17.0 - cloud.google.com/go/assuredworkloads: v1.11.1 → v1.11.4 - cloud.google.com/go/automl: v1.13.1 → v1.13.4 - cloud.google.com/go/baremetalsolution: v1.1.1 → v1.2.3 - cloud.google.com/go/batch: v1.3.1 → v1.7.0 - cloud.google.com/go/beyondcorp: v1.0.0 → v1.0.3 - cloud.google.com/go/bigquery: v1.53.0 → v1.58.0 - cloud.google.com/go/billing: v1.16.0 → v1.18.0 - cloud.google.com/go/binaryauthorization: v1.6.1 → v1.8.0 - cloud.google.com/go/certificatemanager: v1.7.1 → v1.7.4 - cloud.google.com/go/channel: v1.16.0 → v1.17.4 - cloud.google.com/go/cloudbuild: v1.13.0 → v1.15.0 - cloud.google.com/go/clouddms: v1.6.1 → v1.7.3 - cloud.google.com/go/cloudtasks: v1.12.1 → v1.12.4 - cloud.google.com/go/compute: v1.23.0 → v1.25.1 - cloud.google.com/go/contactcenterinsights: v1.10.0 → v1.12.1 - cloud.google.com/go/container: v1.24.0 → v1.29.0 - cloud.google.com/go/containeranalysis: v0.10.1 → v0.11.3 - cloud.google.com/go/datacatalog: v1.16.0 → v1.19.2 - cloud.google.com/go/dataflow: v0.9.1 → v0.9.4 - cloud.google.com/go/dataform: v0.8.1 → v0.9.1 - cloud.google.com/go/datafusion: v1.7.1 → v1.7.4 - cloud.google.com/go/datalabeling: v0.8.1 → v0.8.4 - cloud.google.com/go/dataplex: v1.9.0 → v1.14.0 - cloud.google.com/go/dataproc/v2: v2.0.1 → v2.3.0 - cloud.google.com/go/dataqna: v0.8.1 → v0.8.4 - cloud.google.com/go/datastore: v1.13.0 → v1.15.0 - cloud.google.com/go/datastream: v1.10.0 → v1.10.3 - cloud.google.com/go/deploy: v1.13.0 → v1.17.0 - cloud.google.com/go/dialogflow: v1.40.0 → v1.48.1 - cloud.google.com/go/dlp: v1.10.1 → v1.11.1 - cloud.google.com/go/documentai: v1.22.0 → v1.23.7 - cloud.google.com/go/domains: v0.9.1 → v0.9.4 - cloud.google.com/go/edgecontainer: v1.1.1 → v1.1.4 - cloud.google.com/go/essentialcontacts: v1.6.2 → v1.6.5 - cloud.google.com/go/eventarc: v1.13.0 → v1.13.3 - cloud.google.com/go/filestore: v1.7.1 → v1.8.0 - cloud.google.com/go/firestore: v1.12.0 → v1.14.0 - cloud.google.com/go/functions: v1.15.1 → v1.15.4 - cloud.google.com/go/gkebackup: v1.3.0 → v1.3.4 - cloud.google.com/go/gkeconnect: v0.8.1 → v0.8.4 - cloud.google.com/go/gkehub: v0.14.1 → v0.14.4 - cloud.google.com/go/gkemulticloud: v1.0.0 → v1.1.0 - cloud.google.com/go/gsuiteaddons: v1.6.1 → v1.6.4 - cloud.google.com/go/iam: v1.1.1 → v1.1.5 - cloud.google.com/go/iap: v1.8.1 → v1.9.3 - cloud.google.com/go/ids: v1.4.1 → v1.4.4 - cloud.google.com/go/iot: v1.7.1 → v1.7.4 - cloud.google.com/go/kms: v1.15.0 → v1.15.5 - cloud.google.com/go/language: v1.10.1 → v1.12.2 - cloud.google.com/go/lifesciences: v0.9.1 → v0.9.4 - cloud.google.com/go/logging: v1.7.0 → v1.9.0 - cloud.google.com/go/longrunning: v0.5.1 → v0.5.4 - cloud.google.com/go/managedidentities: v1.6.1 → v1.6.4 - cloud.google.com/go/maps: v1.4.0 → v1.6.3 - cloud.google.com/go/mediatranslation: v0.8.1 → v0.8.4 - cloud.google.com/go/memcache: v1.10.1 → v1.10.4 - cloud.google.com/go/metastore: v1.12.0 → v1.13.3 - cloud.google.com/go/monitoring: v1.15.1 → v1.17.0 - cloud.google.com/go/networkconnectivity: v1.12.1 → v1.14.3 - cloud.google.com/go/networkmanagement: v1.8.0 → v1.9.3 - cloud.google.com/go/networksecurity: v0.9.1 → v0.9.4 - cloud.google.com/go/notebooks: v1.9.1 → v1.11.2 - cloud.google.com/go/optimization: v1.4.1 → v1.6.2 - cloud.google.com/go/orchestration: v1.8.1 → v1.8.4 - cloud.google.com/go/orgpolicy: v1.11.1 → v1.12.0 - cloud.google.com/go/osconfig: v1.12.1 → v1.12.4 - cloud.google.com/go/oslogin: v1.10.1 → v1.13.0 - cloud.google.com/go/phishingprotection: v0.8.1 → v0.8.4 - cloud.google.com/go/policytroubleshooter: v1.8.0 → v1.10.2 - cloud.google.com/go/privatecatalog: v0.9.1 → v0.9.4 - cloud.google.com/go/pubsub: v1.33.0 → v1.34.0 - cloud.google.com/go/recaptchaenterprise/v2: v2.7.2 → v2.9.0 - cloud.google.com/go/recommendationengine: v0.8.1 → v0.8.4 - cloud.google.com/go/recommender: v1.10.1 → v1.12.0 - cloud.google.com/go/redis: v1.13.1 → v1.14.1 - cloud.google.com/go/resourcemanager: v1.9.1 → v1.9.4 - cloud.google.com/go/resourcesettings: v1.6.1 → v1.6.4 - cloud.google.com/go/retail: v1.14.1 → v1.14.4 - cloud.google.com/go/run: v1.2.0 → v1.3.3 - cloud.google.com/go/scheduler: v1.10.1 → v1.10.5 - cloud.google.com/go/secretmanager: v1.11.1 → v1.11.4 - cloud.google.com/go/security: v1.15.1 → v1.15.4 - cloud.google.com/go/securitycenter: v1.23.0 → v1.24.3 - cloud.google.com/go/servicedirectory: v1.11.0 → v1.11.3 - cloud.google.com/go/shell: v1.7.1 → v1.7.4 - cloud.google.com/go/spanner: v1.47.0 → v1.55.0 - cloud.google.com/go/speech: v1.19.0 → v1.21.0 - cloud.google.com/go/storagetransfer: v1.10.0 → v1.10.3 - cloud.google.com/go/talent: v1.6.2 → v1.6.5 - cloud.google.com/go/texttospeech: v1.7.1 → v1.7.4 - cloud.google.com/go/tpu: v1.6.1 → v1.6.4 - cloud.google.com/go/trace: v1.10.1 → v1.10.4 - cloud.google.com/go/translate: v1.8.2 → v1.10.0 - cloud.google.com/go/video: v1.19.0 → v1.20.3 - cloud.google.com/go/videointelligence: v1.11.1 → v1.11.4 - cloud.google.com/go/vision/v2: v2.7.2 → v2.7.5 - cloud.google.com/go/vmmigration: v1.7.1 → v1.7.4 - cloud.google.com/go/vmwareengine: v1.0.0 → v1.0.3 - cloud.google.com/go/vpcaccess: v1.7.1 → v1.7.4 - cloud.google.com/go/webrisk: v1.9.1 → v1.9.4 - cloud.google.com/go/websecurityscanner: v1.6.1 → v1.6.4 - cloud.google.com/go/workflows: v1.11.1 → v1.12.3 - cloud.google.com/go: v0.110.7 → v0.112.0 - github.com/Azure/go-ansiterm: [d185dfc → 306776e](https://github.com/Azure/go-ansiterm/compare/d185dfc...306776e) - github.com/Microsoft/go-winio: [v0.6.0 → v0.6.2](https://github.com/Microsoft/go-winio/compare/v0.6.0...v0.6.2) - github.com/armon/circbuf: [bbbad09 → 5111143](https://github.com/armon/circbuf/compare/bbbad09...5111143) - github.com/cilium/ebpf: [v0.9.1 → v0.16.0](https://github.com/cilium/ebpf/compare/v0.9.1...v0.16.0) - github.com/containerd/console: [v1.0.3 → v1.0.4](https://github.com/containerd/console/compare/v1.0.3...v1.0.4) - github.com/containerd/ttrpc: [v1.2.2 → v1.2.5](https://github.com/containerd/ttrpc/compare/v1.2.2...v1.2.5) - github.com/coredns/corefile-migration: [v1.0.21 → v1.0.24](https://github.com/coredns/corefile-migration/compare/v1.0.21...v1.0.24) - github.com/cyphar/filepath-securejoin: [v0.2.4 → v0.3.4](https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.3.4) - github.com/distribution/reference: [v0.5.0 → v0.6.0](https://github.com/distribution/reference/compare/v0.5.0...v0.6.0) - github.com/docker/docker: [v20.10.27+incompatible → v26.1.4+incompatible](https://github.com/docker/docker/compare/v20.10.27...v26.1.4) - github.com/docker/go-connections: [v0.4.0 → v0.5.0](https://github.com/docker/go-connections/compare/v0.4.0...v0.5.0) - github.com/exponent-io/jsonpath: [d6023ce → 1de76d7](https://github.com/exponent-io/jsonpath/compare/d6023ce...1de76d7) - github.com/go-openapi/jsonpointer: [v0.19.6 → v0.21.0](https://github.com/go-openapi/jsonpointer/compare/v0.19.6...v0.21.0) - github.com/go-openapi/swag: [v0.22.4 → v0.23.0](https://github.com/go-openapi/swag/compare/v0.22.4...v0.23.0) - github.com/golang/mock: [v1.3.1 → v1.1.1](https://github.com/golang/mock/compare/v1.3.1...v1.1.1) - github.com/google/cadvisor: [v0.49.0 → v0.51.0](https://github.com/google/cadvisor/compare/v0.49.0...v0.51.0) - github.com/google/cel-go: [v0.20.1 → v0.22.0](https://github.com/google/cel-go/compare/v0.20.1...v0.22.0) - github.com/google/pprof: [4bfdf5a → d1b30fe](https://github.com/google/pprof/compare/4bfdf5a...d1b30fe) - github.com/gregjones/httpcache: [9cad4c3 → 901d907](https://github.com/gregjones/httpcache/compare/9cad4c3...901d907) - github.com/jonboulle/clockwork: [v0.2.2 → v0.4.0](https://github.com/jonboulle/clockwork/compare/v0.2.2...v0.4.0) - github.com/moby/spdystream: [v0.4.0 → v0.5.0](https://github.com/moby/spdystream/compare/v0.4.0...v0.5.0) - github.com/moby/sys/mountinfo: [v0.7.1 → v0.7.2](https://github.com/moby/sys/compare/mountinfo/v0.7.1...mountinfo/v0.7.2) - github.com/mohae/deepcopy: [491d360 → c48cc78](https://github.com/mohae/deepcopy/compare/491d360...c48cc78) - github.com/onsi/ginkgo/v2: [v2.19.0 → v2.21.0](https://github.com/onsi/ginkgo/compare/v2.19.0...v2.21.0) - github.com/onsi/gomega: [v1.33.1 → v1.35.1](https://github.com/onsi/gomega/compare/v1.33.1...v1.35.1) - github.com/opencontainers/image-spec: [v1.0.2 → v1.1.0](https://github.com/opencontainers/image-spec/compare/v1.0.2...v1.1.0) - github.com/opencontainers/runc: [v1.1.13 → v1.2.1](https://github.com/opencontainers/runc/compare/v1.1.13...v1.2.1) - github.com/opencontainers/runtime-spec: [494a5a6 → v1.2.0](https://github.com/opencontainers/runtime-spec/compare/494a5a6...v1.2.0) - github.com/opencontainers/selinux: [v1.11.0 → v1.11.1](https://github.com/opencontainers/selinux/compare/v1.11.0...v1.11.1) - github.com/stoewer/go-strcase: [v1.2.0 → v1.3.0](https://github.com/stoewer/go-strcase/compare/v1.2.0...v1.3.0) - github.com/urfave/cli: [v1.22.2 → v1.22.14](https://github.com/urfave/cli/compare/v1.22.2...v1.22.14) - github.com/vishvananda/netlink: [v1.1.0 → b1ce50c](https://github.com/vishvananda/netlink/compare/v1.1.0...b1ce50c) - github.com/xiang90/probing: [43a291a → a49e3df](https://github.com/xiang90/probing/compare/43a291a...a49e3df) - go.etcd.io/bbolt: v1.3.9 → v1.3.11 - go.etcd.io/etcd/api/v3: v3.5.14 → v3.5.16 - go.etcd.io/etcd/client/pkg/v3: v3.5.14 → v3.5.16 - go.etcd.io/etcd/client/v2: v2.305.13 → v2.305.16 - go.etcd.io/etcd/client/v3: v3.5.14 → v3.5.16 - go.etcd.io/etcd/pkg/v3: v3.5.13 → v3.5.16 - go.etcd.io/etcd/raft/v3: v3.5.13 → v3.5.16 - go.etcd.io/etcd/server/v3: v3.5.13 → v3.5.16 - go.uber.org/zap: v1.26.0 → v1.27.0 - golang.org/x/crypto: v0.24.0 → v0.28.0 - golang.org/x/exp: f3d0a9c → 8a7402a - golang.org/x/lint: 1621716 → d0100b6 - golang.org/x/mod: v0.17.0 → v0.21.0 - golang.org/x/net: v0.26.0 → v0.30.0 - golang.org/x/oauth2: v0.21.0 → v0.23.0 - golang.org/x/sync: v0.7.0 → v0.8.0 - golang.org/x/sys: v0.21.0 → v0.26.0 - golang.org/x/telemetry: f48c80b → bda5523 - golang.org/x/term: v0.21.0 → v0.25.0 - golang.org/x/text: v0.16.0 → v0.19.0 - golang.org/x/time: v0.3.0 → v0.7.0 - golang.org/x/tools: e35e4cc → v0.26.0 - golang.org/x/xerrors: 04be3eb → 5ec99f8 - google.golang.org/genproto/googleapis/api: 5315273 → f6391c0 - google.golang.org/genproto/googleapis/rpc: f6361c8 → f6391c0 - google.golang.org/genproto: b8732ec → ef43131 - google.golang.org/protobuf: v1.34.2 → v1.35.1 - gotest.tools/v3: v3.0.3 → v3.0.2 - honnef.co/go/tools: v0.0.1-2019.2.3 → ea95bdf - k8s.io/gengo/v2: 51d4e06 → 2b36238 - k8s.io/kube-openapi: 70dd376 → 32ad38e - k8s.io/system-validators: v1.8.0 → v1.9.1 - k8s.io/utils: 18e509b → 3ea5e8c - sigs.k8s.io/apiserver-network-proxy/konnectivity-client: v0.30.3 → v0.31.0 - sigs.k8s.io/json: bc3834c → 9aa6b5e - sigs.k8s.io/kustomize/api: v0.17.2 → v0.18.0 - sigs.k8s.io/kustomize/cmd/config: v0.14.1 → v0.15.0 - sigs.k8s.io/kustomize/kustomize/v5: v5.4.2 → v5.5.0 - sigs.k8s.io/kustomize/kyaml: v0.17.1 → v0.18.1 - sigs.k8s.io/structured-merge-diff/v4: v4.4.1 → v4.4.2 ### Removed - bazil.org/fuse: 371fbbd - cloud.google.com/go/storage: v1.0.0 - dmitri.shuralyov.com/gpu/mtl: 666a987 - github.com/BurntSushi/xgb: [27f1227](https://github.com/BurntSushi/xgb/tree/27f1227) - github.com/Microsoft/hcsshim: [v0.8.26](https://github.com/Microsoft/hcsshim/tree/v0.8.26) - github.com/OneOfOne/xxhash: [v1.2.2](https://github.com/OneOfOne/xxhash/tree/v1.2.2) - github.com/alecthomas/template: [a0175ee](https://github.com/alecthomas/template/tree/a0175ee) - github.com/armon/consul-api: [eb2c6b5](https://github.com/armon/consul-api/tree/eb2c6b5) - github.com/armon/go-metrics: [f0300d1](https://github.com/armon/go-metrics/tree/f0300d1) - github.com/armon/go-radix: [7fddfc3](https://github.com/armon/go-radix/tree/7fddfc3) - github.com/aws/aws-sdk-go: [v1.35.24](https://github.com/aws/aws-sdk-go/tree/v1.35.24) - github.com/bgentry/speakeasy: [v0.1.0](https://github.com/bgentry/speakeasy/tree/v0.1.0) - github.com/bketelsen/crypt: [5cbc8cc](https://github.com/bketelsen/crypt/tree/5cbc8cc) - github.com/cespare/xxhash: [v1.1.0](https://github.com/cespare/xxhash/tree/v1.1.0) - github.com/checkpoint-restore/go-criu/v5: [v5.3.0](https://github.com/checkpoint-restore/go-criu/tree/v5.3.0) - github.com/chzyer/logex: [v1.1.10](https://github.com/chzyer/logex/tree/v1.1.10) - github.com/chzyer/test: [a1ea475](https://github.com/chzyer/test/tree/a1ea475) - github.com/containerd/cgroups: [v1.1.0](https://github.com/containerd/cgroups/tree/v1.1.0) - github.com/containerd/containerd: [v1.4.9](https://github.com/containerd/containerd/tree/v1.4.9) - github.com/containerd/continuity: [v0.1.0](https://github.com/containerd/continuity/tree/v0.1.0) - github.com/containerd/fifo: [v1.0.0](https://github.com/containerd/fifo/tree/v1.0.0) - github.com/containerd/go-runc: [v1.0.0](https://github.com/containerd/go-runc/tree/v1.0.0) - github.com/containerd/typeurl: [v1.0.2](https://github.com/containerd/typeurl/tree/v1.0.2) - github.com/coreos/bbolt: [v1.3.2](https://github.com/coreos/bbolt/tree/v1.3.2) - github.com/coreos/etcd: [v3.3.13+incompatible](https://github.com/coreos/etcd/tree/v3.3.13) - github.com/coreos/go-systemd: [95778df](https://github.com/coreos/go-systemd/tree/95778df) - github.com/coreos/pkg: [399ea9e](https://github.com/coreos/pkg/tree/399ea9e) - github.com/daviddengcn/go-colortext: [v1.0.0](https://github.com/daviddengcn/go-colortext/tree/v1.0.0) - github.com/dgrijalva/jwt-go: [v3.2.0+incompatible](https://github.com/dgrijalva/jwt-go/tree/v3.2.0) - github.com/dgryski/go-sip13: [e10d5fe](https://github.com/dgryski/go-sip13/tree/e10d5fe) - github.com/docker/distribution: [v2.8.2+incompatible](https://github.com/docker/distribution/tree/v2.8.2) - github.com/fatih/color: [v1.7.0](https://github.com/fatih/color/tree/v1.7.0) - github.com/frankban/quicktest: [v1.14.0](https://github.com/frankban/quicktest/tree/v1.14.0) - github.com/go-gl/glfw: [e6da0ac](https://github.com/go-gl/glfw/tree/e6da0ac) - github.com/gogo/googleapis: [v1.4.1](https://github.com/gogo/googleapis/tree/v1.4.1) - github.com/golangplus/bytes: [v1.0.0](https://github.com/golangplus/bytes/tree/v1.0.0) - github.com/golangplus/fmt: [v1.0.0](https://github.com/golangplus/fmt/tree/v1.0.0) - github.com/golangplus/testing: [v1.0.0](https://github.com/golangplus/testing/tree/v1.0.0) - github.com/google/martian: [v2.1.0+incompatible](https://github.com/google/martian/tree/v2.1.0) - github.com/google/renameio: [v0.1.0](https://github.com/google/renameio/tree/v0.1.0) - github.com/googleapis/gax-go/v2: [v2.0.5](https://github.com/googleapis/gax-go/tree/v2.0.5) - github.com/gopherjs/gopherjs: [0766667](https://github.com/gopherjs/gopherjs/tree/0766667) - github.com/hashicorp/consul/api: [v1.1.0](https://github.com/hashicorp/consul/tree/api/v1.1.0) - github.com/hashicorp/consul/sdk: [v0.1.1](https://github.com/hashicorp/consul/tree/sdk/v0.1.1) - github.com/hashicorp/errwrap: [v1.0.0](https://github.com/hashicorp/errwrap/tree/v1.0.0) - github.com/hashicorp/go-cleanhttp: [v0.5.1](https://github.com/hashicorp/go-cleanhttp/tree/v0.5.1) - github.com/hashicorp/go-immutable-radix: [v1.0.0](https://github.com/hashicorp/go-immutable-radix/tree/v1.0.0) - github.com/hashicorp/go-msgpack: [v0.5.3](https://github.com/hashicorp/go-msgpack/tree/v0.5.3) - github.com/hashicorp/go-multierror: [v1.0.0](https://github.com/hashicorp/go-multierror/tree/v1.0.0) - github.com/hashicorp/go-rootcerts: [v1.0.0](https://github.com/hashicorp/go-rootcerts/tree/v1.0.0) - github.com/hashicorp/go-sockaddr: [v1.0.0](https://github.com/hashicorp/go-sockaddr/tree/v1.0.0) - github.com/hashicorp/go-syslog: [v1.0.0](https://github.com/hashicorp/go-syslog/tree/v1.0.0) - github.com/hashicorp/go-uuid: [v1.0.1](https://github.com/hashicorp/go-uuid/tree/v1.0.1) - github.com/hashicorp/go.net: [v0.0.1](https://github.com/hashicorp/go.net/tree/v0.0.1) - github.com/hashicorp/golang-lru: [v0.5.1](https://github.com/hashicorp/golang-lru/tree/v0.5.1) - github.com/hashicorp/hcl: [v1.0.0](https://github.com/hashicorp/hcl/tree/v1.0.0) - github.com/hashicorp/logutils: [v1.0.0](https://github.com/hashicorp/logutils/tree/v1.0.0) - github.com/hashicorp/mdns: [v1.0.0](https://github.com/hashicorp/mdns/tree/v1.0.0) - github.com/hashicorp/memberlist: [v0.1.3](https://github.com/hashicorp/memberlist/tree/v0.1.3) - github.com/hashicorp/serf: [v0.8.2](https://github.com/hashicorp/serf/tree/v0.8.2) - github.com/imdario/mergo: [v0.3.6](https://github.com/imdario/mergo/tree/v0.3.6) - github.com/jmespath/go-jmespath: [v0.4.0](https://github.com/jmespath/go-jmespath/tree/v0.4.0) - github.com/jstemmer/go-junit-report: [af01ea7](https://github.com/jstemmer/go-junit-report/tree/af01ea7) - github.com/jtolds/gls: [v4.20.0+incompatible](https://github.com/jtolds/gls/tree/v4.20.0) - github.com/magiconair/properties: [v1.8.1](https://github.com/magiconair/properties/tree/v1.8.1) - github.com/mattn/go-colorable: [v0.0.9](https://github.com/mattn/go-colorable/tree/v0.0.9) - github.com/mattn/go-isatty: [v0.0.3](https://github.com/mattn/go-isatty/tree/v0.0.3) - github.com/miekg/dns: [v1.0.14](https://github.com/miekg/dns/tree/v1.0.14) - github.com/mitchellh/cli: [v1.0.0](https://github.com/mitchellh/cli/tree/v1.0.0) - github.com/mitchellh/go-homedir: [v1.1.0](https://github.com/mitchellh/go-homedir/tree/v1.1.0) - github.com/mitchellh/go-testing-interface: [v1.0.0](https://github.com/mitchellh/go-testing-interface/tree/v1.0.0) - github.com/mitchellh/gox: [v0.4.0](https://github.com/mitchellh/gox/tree/v0.4.0) - github.com/mitchellh/iochan: [v1.0.0](https://github.com/mitchellh/iochan/tree/v1.0.0) - github.com/mitchellh/mapstructure: [v1.1.2](https://github.com/mitchellh/mapstructure/tree/v1.1.2) - github.com/oklog/ulid: [v1.3.1](https://github.com/oklog/ulid/tree/v1.3.1) - github.com/pascaldekloe/goe: [57f6aae](https://github.com/pascaldekloe/goe/tree/57f6aae) - github.com/pelletier/go-toml: [v1.2.0](https://github.com/pelletier/go-toml/tree/v1.2.0) - github.com/posener/complete: [v1.1.1](https://github.com/posener/complete/tree/v1.1.1) - github.com/prometheus/tsdb: [v0.7.1](https://github.com/prometheus/tsdb/tree/v0.7.1) - github.com/ryanuber/columnize: [9b3edd6](https://github.com/ryanuber/columnize/tree/9b3edd6) - github.com/sean-/seed: [e2103e2](https://github.com/sean-/seed/tree/e2103e2) - github.com/shurcooL/sanitized_anchor_name: [v1.0.0](https://github.com/shurcooL/sanitized_anchor_name/tree/v1.0.0) - github.com/smartystreets/assertions: [b2de0cb](https://github.com/smartystreets/assertions/tree/b2de0cb) - github.com/smartystreets/goconvey: [v1.6.4](https://github.com/smartystreets/goconvey/tree/v1.6.4) - github.com/spaolacci/murmur3: [f09979e](https://github.com/spaolacci/murmur3/tree/f09979e) - github.com/spf13/afero: [v1.1.2](https://github.com/spf13/afero/tree/v1.1.2) - github.com/spf13/cast: [v1.3.0](https://github.com/spf13/cast/tree/v1.3.0) - github.com/spf13/jwalterweatherman: [v1.0.0](https://github.com/spf13/jwalterweatherman/tree/v1.0.0) - github.com/spf13/viper: [v1.7.0](https://github.com/spf13/viper/tree/v1.7.0) - github.com/subosito/gotenv: [v1.2.0](https://github.com/subosito/gotenv/tree/v1.2.0) - github.com/ugorji/go: [v1.1.4](https://github.com/ugorji/go/tree/v1.1.4) - github.com/xordataexchange/crypt: [b2862e3](https://github.com/xordataexchange/crypt/tree/b2862e3) - go.opencensus.io: v0.24.0 - go.starlark.net: a134d8f - golang.org/x/image: cff245a - golang.org/x/mobile: d2bd2a2 - google.golang.org/api: v0.13.0 - gopkg.in/alecthomas/kingpin.v2: v2.2.6 - gopkg.in/errgo.v2: v2.1.0 - gopkg.in/ini.v1: v1.51.0 - gopkg.in/resty.v1: v1.12.0 - rsc.io/binaryregexp: v0.2.0 # v1.32.0-rc.2 ## Downloads for v1.32.0-rc.2 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes.tar.gz) | 65d2677a56a980f699a7241042a9931025fe5e835fa5e303111ecd5cfec6a28447a875dc442777c94271feaf865e5c0db30667ab642b3401dca9c476cf840eb5 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-src.tar.gz) | d3e3f81d22ad58a03c2a2d995edfef01466e31eeff15e5bd329250344c85c87fc66869390c5d652fc53e3a42b9210736ea440f52068c2d76df7a39640b1a060f ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-darwin-amd64.tar.gz) | 4120647940d8effb671e75647bc3be549c5cebce342ca79dd06c352ebf6fd3b1c97adaffbc96e80fa8143d1258771d8ae7d47d3f280e6a422675648312165ecd [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-darwin-arm64.tar.gz) | 69a3d0c3605e0fc0cab9570143353958fc8c95cec8fb13bd07fc4057371b51f069d5a226e3f386068e9485df6bdbf2a13cd0e3afeff150cb637d802bcebf94a2 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-linux-386.tar.gz) | ea9429d795df3897975ebe9404cd3770715c791197861f6399ba3099e9440167fe8574365d63bf29073819feb735f29b0cc42a5d3065d76285932bdc6bbefc10 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-linux-amd64.tar.gz) | f83349dd9626a7c4417672c089c5d80f117b98dc5f66772d8bcd5f9b260fb9be38ec7525e93ebfa66138c5a22eceb8beca40ee8b19fa4de8743d1a0e06307895 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-linux-arm.tar.gz) | 5b42004e3aa37fdb7025584b62c754f49a936af87ca008e15632e60c5dfec1758940c47b4d898c77307df5fbee15c154883a83e289a93e84dab9b2511dd1b428 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-linux-arm64.tar.gz) | 39972ffd9ec4e0cac0b0ac10ad42e428ffdd2eede0e10b6ccae49940542448bc4b01b0c2a492f210f3108fb0cd995d183fedb35de8a8c5de630fa2919052116a [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-linux-ppc64le.tar.gz) | 123765dccd7c1497111a073d7fc26fc1d896fb1e6504d4cb7d167ff0b6f40ad756322d8a7d794c953a1f23b9115d7d5d138e6e97ffa7097d110db3ed2c783081 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-linux-s390x.tar.gz) | 1bc211dd712008f8a387dac8f662ac7b9a036bf2bd274b1b157b4ffedabeb99d44d2842a82c26a4cae6f4f123ae021b9ed76221968657c769cdf94db9ca2cdf7 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-windows-386.tar.gz) | 54f003297bc06a704c19f039f9f834e1010d1c39226a11f08e1a5175877311bb6b5a2fd32c11f835f35204d34950771f72ed0ab745cc08e0c52898883aa25eea [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-windows-amd64.tar.gz) | 7db4e4b0a439e5738a36a49e9db6cc1783d5f3c894d9038cf967af5f5345cfc57b2d5dbe23773022dc14823112681d8f61af833bd1456760bd321d098e92f905 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-client-windows-arm64.tar.gz) | 79c807f76ab6f376f7e90eb9c01e247be85ca73e4d229a8ead34f9a2a01fc04eae7fa582db412d510b16971eccb8d5c57344d9cf687e337f996b5730a89a1b20 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-server-linux-amd64.tar.gz) | cd37ab7199c051b67395e43b4d4bd3c33f8985daaeb65528388b7e257857679dba490e3672da3b6e0b08e604f22134e480be4ba8562b052a781a79938a24739b [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-server-linux-arm64.tar.gz) | 9a7b89d6d9a52e659fd41f0398da9e8d1ae34a9ecf714db656f2b9ceccc17b0438fde777f9bd2729d741dee4fea546af5ee2358752780fce14921e8b2095bbed [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-server-linux-ppc64le.tar.gz) | 06f84e96b541a79e2cb4d25052fdae8ca5bb4561155306a1ee3179137f326c8d4abeaca1d30e21d45e8a7e0e1ae258b51bf4e005434fa09875e4b1a9f319705b [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-server-linux-s390x.tar.gz) | 4f626085276636bd8f0e0e2b6231dcdbe9e8af6897c9e6d01c1c7da71998269fc4dfcc1125007fa87c4b27a6d4023333070ef8da1f2fada60ec8c42ca057e24c ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-node-linux-amd64.tar.gz) | fed50efd8aca7a17ad70898a7a577f001c94880d7abe020ccaa1b3a182700e193d9bdc4e413529cb7e66b2e5bd3b75dd6287cba7fc3b2fc8cb5b93ae96e2a4a3 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-node-linux-arm64.tar.gz) | 25ec7c07b03bd1d400dcba0b7d885a5cc606ec9d294fed7ab67cd888d6faaed0ae47cbfacfff91325d08818461f0f09f9732aa3720a56082db81c2730b0df42c [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-node-linux-ppc64le.tar.gz) | 134dcdf14b2c77b04c8e811f65553270e975159fdb92af3f556092c8cfbb6bcf467302e477c6817eff7a1d6e5edfd69600bdbcbaf679c60e2be52aa01bb58bfb [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-node-linux-s390x.tar.gz) | f6ba4ccc72b8631848c0e031c78201ffeaa4370e02301b0434d60d680a118fe02cbb5297c1f7870010f8e6187dc46fb8c330f3f0488031857c275aace3c671ec [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.2/kubernetes-node-windows-amd64.tar.gz) | e7e8ec9047aad1c8853f8f3daae6f8d4e1b3bbfe21c6acf1eca0ca2faf6ddab5470accd8398c991b4c303af6041d221bde62c1a88f99aa8d8a78beed3b1b221a ### 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.32.0-rc.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.32.0-rc.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.32.0-rc.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.32.0-rc.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.32.0-rc.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.32.0-rc.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.32.0-rc.1 ## Changes by Kind ### API Change - Request header UID propagation is gated behind an alpha RemoteRequestHeaderUID feature gate. ([#129081](https://github.com/kubernetes/kubernetes/pull/129081), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Cluster Lifecycle and Testing] ### Bug or Regression - Kubelet: fixes an issue mounting CSI volumes on Windows nodes in 1.32.0 release candidates ([#129083](https://github.com/kubernetes/kubernetes/pull/129083), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Architecture, Auth, CLI, Cloud Provider, Cluster Lifecycle, Instrumentation, Network, Node, Release, Storage and Windows] ## Dependencies ### Added _Nothing has changed._ ### Changed _Nothing has changed._ ### Removed _Nothing has changed._ # v1.32.0-rc.1 ## Downloads for v1.32.0-rc.1 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes.tar.gz) | f3a100cb16f7b0298e1abb1d79cae29da9f6c0318a8c75e76c4b9b3f8b0b1b0518dad9c9964d3b29af4b2830dddd7d45e933b38fe5427bc97e747f8c46f51e87 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-src.tar.gz) | 5287c28fad700f41faebf1b00f9166ae4e553368a0ceb480cf17f8afe7afb4819a82f42242bf50da12583a6b096a332ae235b301a6a50cae8b7475f7e38131f0 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-darwin-amd64.tar.gz) | f821d8aee23d1509995aa928e483992761c0401cae9dc8e0fa8c75a26562b51ddca785eb95497a0c3fe60dabf23e8497e03bcab1071dc5ec5c3c35f782d7f52a [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-darwin-arm64.tar.gz) | 01fe12bd64028b8b051ba1664310533e6fb0fc95605dd79622129becdb02ee7156462fdc12ae969f55b47a1118de1ec0264aa7d43387d77e15b3ee935eba48f1 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-linux-386.tar.gz) | a0e41a46f55ac9e345787c35181fde79abfed1ee0a79dc9c3229312ffaf72a54bc7c90416d7c63ba7f6e08666a4757b2223debc2aa65b60c6f70fc8b905befc3 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-linux-amd64.tar.gz) | b338712c83ff84109e21ea9ae5b16b5f53d6a3667fd31e712021f453acda74e58370e8b5cf517149077e9c70f908098de0a579b8879b9022178875da3279ab39 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-linux-arm.tar.gz) | dedffc2a8a8992b38a95e2ac594bb811242070b8df8b731167eb7ba4a600c87e22b4134e46b857054087d59537f89ef5602094d5f881a9e78e957abe2e995231 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-linux-arm64.tar.gz) | e6510a53e22eb27b12ffb6a35fc37e212d3cd81174ad2c092bbca4ee877a502dce42937f5b826dd95b586471645fb91a4b49ec064fae31dc6a1d5ff6436598c2 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-linux-ppc64le.tar.gz) | b3a3865ea43223caf0a250c8ee060d3540b4b85495ae6ef4aa5de52b270c66cc6a50f0ff90ca81c4f1e7651b932ef3935c572a66b4f498e550d496b34ce689e8 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-linux-s390x.tar.gz) | 33a1d656175eb199ea1c806df3281b7045f66aba5b260d9cce6f255bb6b0b313dff40df57ab5dbab9dbd0241d9945dda5770fffad13664325729b843d987dbff [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-windows-386.tar.gz) | 21251ff75d69a79abf9133a141012b3666cc3162d1e5688258a22c7520e2233f14012f85325d0bd7056329d8ccb135274ffb9341f7d87f100d19e5e945bced0d [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-windows-amd64.tar.gz) | a27f66a19e88dc0ffbf82e701cfdd2ea4600d3af5684c6a7e1a591eaa296c7b8672a54a7fd5b0e9b889ad30fb52d823ccf370e45e59e774b1245f4c791f837e7 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-client-windows-arm64.tar.gz) | 270b0ca109acd12322fca8da871e7121a45147cd629beee48ef115ce105d6db1447437184bb6ad41f1f6f9c20f5a4380d7d80be8141a1b0d993010e8f2a550d6 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-server-linux-amd64.tar.gz) | 28747b06fdb9e5cdebb14d2dd715e6489542be416f8af72625fb7cf52fbfbd2bbbba8158c3b574a515b97a74df89f61fcb3e6b5358c4c797aa3b8d8e8a5a26f2 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-server-linux-arm64.tar.gz) | 4bda9415781523185606f3087546ee58fea8c84c640b5f1297c517121b697083cffd28ebe48ed74af06467c0db436b01cb672c23f3e933eed82f34740a6d70a3 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-server-linux-ppc64le.tar.gz) | 6a6436438e1449687067378a524e87e4ef81d9282bfb61a1c6c3c95422f469f29e911bf2d78bb49868e922062a4e985ced109501a45ff5b62d0f5567a632d436 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-server-linux-s390x.tar.gz) | 56cec0289c87f39a39643cd56e495f2ae4df4d2dffde0c01f96325815d2ca2224f48a626d250313a4b718bb1748d214d3773c3c3e07932dc53459249db36c325 ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-node-linux-amd64.tar.gz) | 0802fb296f9a1eac4baf2876ea364bee08771595fda97f30e1679b2cc04314a49617fbec7f0f233821a2c58023705bd1287cf5030ada9674c5f8ceb59beeb019 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-node-linux-arm64.tar.gz) | a370a3e7ea702780446e7e3ebf9ca487c43a9be512ddcb088722218ea396c5ed0be7d49116248380c7c00a9e5709f38b75bda7ca6999502b7e7459a12192bb87 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-node-linux-ppc64le.tar.gz) | ae3cc129ca467bd81037ef936032c8d3076bd7233fefd695b0060c82bc1933e715c82b10f5321d2f35900308d53c1b68d9112137d37f900da555d71b2e765c1f [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-node-linux-s390x.tar.gz) | cc6cbe61a65044edd5c94830bb7a6d70e181e51ba57e3f7125faef828afa8e9ce638eacaa39a9631f76952ac49c3474f27f315d3492cf2e9a2a6e601665ad0ff [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.1/kubernetes-node-windows-amd64.tar.gz) | a25497bff7274723c4f2d6d567c0b3b3fbabff5a305c5a2d63ca6c6d3f584472b4c7b71e53e1a903c975b5ea3173b47f9f16e653859019635f156facb207eebf ### 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.32.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.32.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.32.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.32.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.32.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.32.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.32.0-rc.0 ## Dependencies ### Added _Nothing has changed._ ### Changed _Nothing has changed._ ### Removed _Nothing has changed._ # v1.32.0-rc.0 ## Downloads for v1.32.0-rc.0 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes.tar.gz) | eaa85d26d9315bfe43b2d0e25c317c6a756b031f9c63b14ab1c06a1970b9e2498ecde4dc6c431b926f1b700c02f232e8b63a4e1e02cd3af8cba45a140feba002 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-src.tar.gz) | c7589b72811610703d7ac405f6cbfc20d319015f09a0dc9809bc88db706c95eca2b1329be45f370b185e346393aef823f50dc79a5a7151ba6ca168e7ffbd3b09 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-darwin-amd64.tar.gz) | 6294ea5125483ae5c9273a29cff85cdd2322f1ca240f6f3eb03455314d01c55b1869a4d6ff496522b5b76823760cad28c786ca528883bc54b3cdb4e85c5063c8 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-darwin-arm64.tar.gz) | 4ba6e849650b19a3bf98ff978b26bb6ff2c5539aeb6766048b2fb36c5fce98d84f482607230df43553263d7def611e467dfdaac64282b99d59d585eb54878d33 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-linux-386.tar.gz) | fe2aa6e4b8aa963b37b19fbe4c235e5e19c1c374da6b33723d36081bc5e13348a9ba4c2ceb01b4729a514995e9f3ff8dbe8c34576b3620634dfc15e7031dcda6 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-linux-amd64.tar.gz) | 38a9c36075c1f75cf9dc36dedd1d4d7c37dc5f7d012d427ebaebee2b7a54a816aac73d6054e936f4168b272156975b4addec2224902bd15bf64b74885b6d3a86 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-linux-arm.tar.gz) | 05f76c05874aee0b1c76c0be855efd1e56241b3cd8b1ae371856052a85de2fed69705438cefd616e85e7d2af512882a7de7fb5cb065f1b14b1877bb4bc5552db [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-linux-arm64.tar.gz) | 2021324d205a091d1c06cf913dc7207d322e9a6fb4b5befa453ecaf740e6438ed1ed7f81c8140e78ac1d5e69f657af13fe0c1334f3adafebf7fcec9996d6bbe2 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-linux-ppc64le.tar.gz) | 87bee10e358781a63345d67f86184a2702ee9fa9cd81b6647fc852b56160a28faf3c008c7a43ca78cc5d675b23d4952f4ca64382fe16930313eec2d381ddc636 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-linux-s390x.tar.gz) | 734d62b86165aeda36a994b7493a8514565d3ad12fea67fff231d161021fbeddbf1e694c18f597a4f873b00fc2d0d2c2d6e1a60f74714fb9959d4989e5e94f31 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-windows-386.tar.gz) | 80faf17e8aebbf682f577cac4968dd472108ce6f9f16ecc8167fa13d6a31928fb4f87ba51fe2becabea73296dbd2b7a551dded4d4f172066576533c3eda46d78 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-windows-amd64.tar.gz) | f97ca8359f4c466d43bbc824f508ea8668f00a73f348abaf4b08743d7c7ac05624b927f1a572f7f11f28861c9bf4f7d4c37c052e57c360062b529791603e820f [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-client-windows-arm64.tar.gz) | a26953011fbd955fd9a8faeaa350a44b42e7adb99daf4ba0eaa7f738c2c4ddbb1d43f8f09b80926e1239466d81340978dd70f8a4657847059e074cc801bf9267 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-server-linux-amd64.tar.gz) | 9e9a615e67971410ca4094e3521908cc929f40a38a7939cec09411f80e6b6d34273af3f5a9e18b3cc3e4b9a94cea4ffb414581c25a9d61d905e9dc1d98bd0e15 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-server-linux-arm64.tar.gz) | 352d53b50b0931cf8f9e447de26aa00cbdb4883104ef769264bf1068b65fc7997f8fce19b97145c0288894791f724f7048c220dd08589393d713c527cc23ed75 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-server-linux-ppc64le.tar.gz) | 3b675db6bc25b36e1be5f753d7e37c44062ed04d06303461919fa42ea1ac1a5b65ee90f081db2095086e5f7a5bc5ba875feca76da5bbf1a7d0de56e351de07e9 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-server-linux-s390x.tar.gz) | 9871b11b070edbe28d9aee8ee75079a748ac0b82f7f8e65cfcbdc078730585111eb437762d152c7a2d7be883e4c89edcbc9e036559316fd32361571be082df9e ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-node-linux-amd64.tar.gz) | daa150e2b95822f9444fd278c2561f14b55ae69bf34c442c7aee52a48979dbb61a14da476a9d0aaa17ab557a46b75eea43342b173f001c1d04a520bae9ea2c2b [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-node-linux-arm64.tar.gz) | 57166c47374c28b7c3ad0214edc98a252f1f3b5390cd2d4ad9a043bc5ed7a5819d1e5503607277492b7a1d405ace3a06d9803464018790a3a761368184230241 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-node-linux-ppc64le.tar.gz) | a077fcf0579f4631fca7a07f7a972971bdf29f46faca2a96de84c036a237b1523306c9aa46e395d11c1fc18bde8d9700c87ca658c4e3abd4be75ec231ad72c42 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-node-linux-s390x.tar.gz) | 452721c3c39d6800d335a5f4cbd672f8cf52555c97850497530951e979a742fcb045963e7d7b88ad436f258bda1ee42b8fbc3cad57dc9f5ff92f55be4edc0ae6 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-rc.0/kubernetes-node-windows-amd64.tar.gz) | f09db4e3c81b8dea49d05efa7de6f5ac2c783c93b22f939707811a3f295c770a8b900cd83d91a3fda37c01b22d2c39e6c7710d3f8fad3d4ffc8d1117dd7b09e1 ### 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.32.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.32.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.32.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.32.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.32.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.32.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.32.0-beta.0 ## Changes by Kind ### API Change - A new /resize subresource was added to request pod resource resizing. Update your k8s client code to utilize the /resize subresource for Pod resizing operations. ([#128266](https://github.com/kubernetes/kubernetes/pull/128266), [@AnishShah](https://github.com/AnishShah)) [SIG API Machinery, Apps, Node and Testing] - A new feature that allows unsafe deletion of corrupt resources has been added, it is disabled by default, and it can be enabled by setting the option `--feature-gates=AllowUnsafeMalformedObjectDeletion=true`. It comes with an API change, a new delete option `ignoreStoreReadErrorWithClusterBreakingPotential` has been introduced, it is not set by default, this maintains backward compatibility. In order to perform an unsafe deletion of a corrupt resource, the user must enable the option for the delete request. A resource is considered corrupt if it can not be successfully retrieved from the storage due to a) transformation error e.g. decryption failure, or b) the object failed to decode. Normal deletion flow is attempted first, and if it fails with a corrupt resource error then it triggers unsafe delete. In addition, when this feature is enabled, the 'details' field of 'Status' from the LIST response includes information that identifies the corrupt object(s). NOTE: unsafe deletion ignores finalizer constraints, and skips precondition checks. WARNING: this may break the workload associated with the resource being unsafe-deleted, if it relies on the normal deletion flow, so cluster breaking consequences apply. ([#127513](https://github.com/kubernetes/kubernetes/pull/127513), [@tkashem](https://github.com/tkashem)) [SIG API Machinery, Etcd, Node and Testing] - Add a `Stream` field to `PodLogOptions`, which allows clients to request certain log stream(stdout or stderr) of the container. Please also note that the combination of a specific `Stream` and `TailLines` is not supported. ([#127360](https://github.com/kubernetes/kubernetes/pull/127360), [@knight42](https://github.com/knight42)) [SIG API Machinery, Apps, Architecture, Node, Release and Testing] - Add driver-owned fields in ResourceClaim.Status to report device status data for each allocated device. ([#128240](https://github.com/kubernetes/kubernetes/pull/128240), [@LionelJouin](https://github.com/LionelJouin)) [SIG API Machinery, Network, Node and Testing] - Added `singleProcessOOMKill` flag to the kubelet configuration. Setting that to true enable single process OOM killing in cgroups v2. In this mode, if a single process is OOM killed within a container, the remaining processes will not be OOM killed. ([#126096](https://github.com/kubernetes/kubernetes/pull/126096), [@utam0k](https://github.com/utam0k)) [SIG API Machinery, Node, Testing and Windows] - Added alpha support for asynchronous Pod preemption. When the `SchedulerAsyncPreemption` feature gate is enabled, the scheduler now runs API calls to trigger preemptions asynchronously for better performance. ([#128170](https://github.com/kubernetes/kubernetes/pull/128170), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling and Testing] - Added the ability to change the maximum backoff delay accrued between container restarts for a node for containers in `CrashLoopBackOff`. To set this for a node, turn on the feature gate `KubeletCrashLoopBackoffMax` and set the `CrashLoopBackOff.MaxContainerRestartPeriod ` field between `"1s"` and `"300s"` in your [kubelet config file](https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/). ([#128374](https://github.com/kubernetes/kubernetes/pull/128374), [@lauralorenz](https://github.com/lauralorenz)) [SIG API Machinery and Node] - Adds a /flagz endpoint for kube-apiserver endpoint ([#127581](https://github.com/kubernetes/kubernetes/pull/127581), [@richabanker](https://github.com/richabanker)) [SIG API Machinery, Architecture, Auth and Instrumentation] - Changed the Pod API to support `resources` at `spec` level for pod-level resources. ([#128407](https://github.com/kubernetes/kubernetes/pull/128407), [@ndixita](https://github.com/ndixita)) [SIG API Machinery, Apps, CLI, Cluster Lifecycle, Node, Release, Scheduling and Testing] - ContainerStatus.AllocatedResources is now guarded by a separate feature gate, InPlacePodVerticalSaclingAllocatedStatus ([#128377](https://github.com/kubernetes/kubernetes/pull/128377), [@tallclair](https://github.com/tallclair)) [SIG API Machinery, CLI, Node, Scheduling and Testing] - Coordination.v1alpha1 API is dropped and replaced with coordination.v1alpha2. Old coordination.v1alpha1 types must be deleted before upgrade ([#127857](https://github.com/kubernetes/kubernetes/pull/127857), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery, Etcd, Scheduling and Testing] - DRA: Restricted the length of opaque device configuration parameters. At admission time, Kubernetes enforces a 10KiB size limit. ([#128601](https://github.com/kubernetes/kubernetes/pull/128601), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] - Introduce v1alpha1 API for mutating admission policies, enabling extensible admission control via CEL expressions (KEP 3962: Mutating Admission Policies). To use, enable the `MutatingAdmissionPolicy` feature gate and the `admissionregistration.k8s.io/v1alpha1` API via `--runtime-config`. ([#127134](https://github.com/kubernetes/kubernetes/pull/127134), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery, Auth, Etcd 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. This change is introduced with a new kube-apiserver featuregate `ServiceAccountNodeAudienceRestriction` that's enabled by default. ([#128077](https://github.com/kubernetes/kubernetes/pull/128077), [@aramase](https://github.com/aramase)) [SIG Auth, Storage and Testing] - Promoted feature gate `StatefulSetAutoDeletePVC` from beta to stable. ([#128247](https://github.com/kubernetes/kubernetes/pull/128247), [@mattcary](https://github.com/mattcary)) [SIG API Machinery, Apps, Auth and Testing] - Removed restrictions on subresource flag in kubectl commands ([#128296](https://github.com/kubernetes/kubernetes/pull/128296), [@AnishShah](https://github.com/AnishShah)) [SIG CLI] - The core functionality of Dynamic Resource Allocation (DRA) got promoted to beta. No action is required when *upgrading*, the previous v1alpha3 API is still supported, so existing deployments and DRA drivers based on v1alpha3 continue to work. *Downgrading* from 1.32 to 1.31 with DRA resources in the cluster (resourceclaims, resourceclaimtemplates, deviceclasses, resourceslices) is *not* supported because the new v1beta1 is used as storage version and not readable by 1.31. ([#127511](https://github.com/kubernetes/kubernetes/pull/127511), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] ### Feature - Add a one-time random duration of up to 50% of kubelet's nodeStatusReportFrequency to help spread the node status update load evenly over time. ([#128640](https://github.com/kubernetes/kubernetes/pull/128640), [@mengqiy](https://github.com/mengqiy)) [SIG Node] - Added Windows support for the node memory manager. ([#128560](https://github.com/kubernetes/kubernetes/pull/128560), [@marosset](https://github.com/marosset)) [SIG Node and Windows] - Added a health check for the device plugin gRPC registration server. When the registration server is down, kubelet is marked as unhealthy. If systemd watchdog is configured, this will result in a kubelet restart. ([#128432](https://github.com/kubernetes/kubernetes/pull/128432), [@zhifei92](https://github.com/zhifei92)) [SIG Node] - Added a new controller, volumeattributesclass-protection-controller, into the kube-controller-manager. The new controller manages a protective finalizer on VolumeAttributesClass objects. ([#123549](https://github.com/kubernetes/kubernetes/pull/123549), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps, Auth and Storage] - Added the feature gate CBORServingAndStorage to allow CBOR as the encoding for API request and response bodies, and as the storage encoding for custom resources. Clients must opt in; programs built with client-go can do this using the client-go feature gates ClientsAllowCBOR and ClientsPreferCBOR. ([#128539](https://github.com/kubernetes/kubernetes/pull/128539), [@benluddy](https://github.com/benluddy)) [SIG API Machinery, Etcd and Testing] - Adds a /statusz endpoint for kube-apiserver endpoint ([#125577](https://github.com/kubernetes/kubernetes/pull/125577), [@richabanker](https://github.com/richabanker)) [SIG API Machinery, Apps, Architecture, Auth, CLI, Cloud Provider, Instrumentation, Network, Node and Testing] - Adopted a new implementation of watch caches for **list** verbs, using a btree data structure. The new implementation is active by default; you can opt out by disabling the `BtreeWatchCache` feature gate. ([#128415](https://github.com/kubernetes/kubernetes/pull/128415), [@serathius](https://github.com/serathius)) [SIG API Machinery, Auth and Cloud Provider] - Considering sidecar container restart counts when removing pods by job controller ([#124952](https://github.com/kubernetes/kubernetes/pull/124952), [@AxeZhan](https://github.com/AxeZhan)) [SIG Apps and CLI] - Enabled graceful shutdown feature for Windows node ([#127404](https://github.com/kubernetes/kubernetes/pull/127404), [@zylxjtu](https://github.com/zylxjtu)) [SIG Node, Testing and Windows] - Ensure resizing for Guaranteed pods with integer CPU requests on nodes with static CPU & Memory policy configured is not allowed for the beta release of in-place resize. The feature gate `InPlacePodVerticalScalingExclusiveCPUs` defaults to `false`, but can be enabled to unblock development on ([#127262](https://github.com/kubernetes/kubernetes/issues/127262), [@tallclair](https://github.com/tallclair)) [SIG Node]. ([#128287](https://github.com/kubernetes/kubernetes/pull/128287), [@esotsal](https://github.com/esotsal)) [SIG Node, Release and Testing] - Graduated `SchedulerQueueingHints` to beta; the feature gate is now enabled by default. ([#128472](https://github.com/kubernetes/kubernetes/pull/128472), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Introduce a new metric kubelet_admission_rejections_total to track the number of pods rejected during admission ([#128556](https://github.com/kubernetes/kubernetes/pull/128556), [@AnishShah](https://github.com/AnishShah)) [SIG Node] - Kube-apiserver adds support for an alpha feature enabling external signing of service account tokens and fetching of public verifying keys, by enabling the alpha `ExternalServiceAccountTokenSigner` feature gate and specifying `--service-account-signing-endpoint`. The flag value can either be the location of a Unix domain socket on a filesystem, or be prefixed with an @ symbol and name a Unix domain socket in the abstract socket namespace. ([#128190](https://github.com/kubernetes/kubernetes/pull/128190), [@HarshalNeelkamal](https://github.com/HarshalNeelkamal)) [SIG API Machinery, Apps, Auth, Etcd, Instrumentation, Node, Release and Testing] - Kubeadm: added the feature gate `NodeLocalCRISocket`. When the feature gate is enabled, kubeadm will generate the `/var/lib/kubelet/instance-config.yaml` file to customize the `containerRuntimeEndpoint` field in the kubelet configuration for each node and will not write the same CRI socket on the Node object as an annotation. ([#128031](https://github.com/kubernetes/kubernetes/pull/128031), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Cluster Lifecycle] - Kubernetes is now built with go 1.23.3 ([#128852](https://github.com/kubernetes/kubernetes/pull/128852), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Updated the control plane's trust anchor publisher to create and manage a new ClusterTrustBundle object, associated with the `kubernetes.io/kube-apiserver-serving` X.509 certificate signer. This ClusterTrustBundle contains a PEM bundle in its payload that you can use to verify kube-apiserver serving certificates. ([#127326](https://github.com/kubernetes/kubernetes/pull/127326), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Apps, Auth, Cluster Lifecycle and Testing] - Version skew strategy update for InPlacePodVerticalScaling for beta graduation. ([#128186](https://github.com/kubernetes/kubernetes/pull/128186), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) [SIG Apps] ### Bug or Regression - 1. When the kubelet constructs the cri mounts for the container which references an `image` volume source type, It passes the missing mount attributes to the CRI implementation, including `readOnly`, `propagation`, and `recursiveReadOnly`. When the readOnly field of the containerMount is explicitly set to false, the kubelet will take the `readOnly`as true to the CRI implementation because the image volume plugin requires the mount to be read-only. 2. Fix a bug where the pod is unexpectedly running when the `image` volume source type is used and mounted to `/etc/hosts` in the container. ([#126806](https://github.com/kubernetes/kubernetes/pull/126806), [@carlory](https://github.com/carlory)) [SIG Node and Storage] - Add warnings for overlap paths in ConfigMap, Secret, DownwardAPI, Projected - Add warning for cases when ProjectedVolume with sources is provided. ([#121968](https://github.com/kubernetes/kubernetes/pull/121968), [@Peac36](https://github.com/Peac36)) [SIG Auth] - DRA: labels in node selectors now are validated. Invalid labels already caused runtime errors before and are unlikely to occur in practice. ([#128932](https://github.com/kubernetes/kubernetes/pull/128932), [@pohly](https://github.com/pohly)) [SIG Apps] - DRA: renamed the new "v1beta1" kubelet gPRC so that the protobuf package name is unique. ([#128764](https://github.com/kubernetes/kubernetes/pull/128764), [@pohly](https://github.com/pohly)) [SIG Node and Testing] - Fixed a bug where the pod(with regular init containers)'s phase was not pending when the regular init container had not finished running after a node restart. ([#126653](https://github.com/kubernetes/kubernetes/pull/126653), [@zhifei92](https://github.com/zhifei92)) [SIG Node and Testing] - Fixed the incorrect help message of a metric "graceful_shutdown_end_time_seconds". Fixed incorrect value set for metrics "graceful_shutdown_start_time_seconds" and "graceful_shutdown_end_time_seconds" in certain cases during graceful node shutdown. ([#128189](https://github.com/kubernetes/kubernetes/pull/128189), [@zylxjtu](https://github.com/zylxjtu)) [SIG Node] - Fixes a race condition that could result in erroneous volume unmounts for flex volume plugins on kubelet restart ([#128495](https://github.com/kubernetes/kubernetes/pull/128495), [@olyazavr](https://github.com/olyazavr)) [SIG Storage] - `StartupProbe` is stopped explicity when `successThrethold` is reached. This eliminates the problem that `StartupProbe` is executed more than `successThrethold`. ([#121206](https://github.com/kubernetes/kubernetes/pull/121206), [@mochizuki875](https://github.com/mochizuki875)) [SIG Node] ### Other (Cleanup or Flake) - CBOR-encoded watch responses now set the Content-Type header to "application/cbor-seq" instead of the nonconformant "application/cbor". ([#128501](https://github.com/kubernetes/kubernetes/pull/128501), [@benluddy](https://github.com/benluddy)) [SIG API Machinery, Etcd and Testing] - DRA: DRA driver authors should update their DRA drivers to use the v1beta1 gRPC API. The older alpha API still works, but might get removed eventually. ([#128646](https://github.com/kubernetes/kubernetes/pull/128646), [@pohly](https://github.com/pohly)) [SIG Node and Testing] - Drop support for InPlacePodVerticalScaling feature in Windows. ([#128623](https://github.com/kubernetes/kubernetes/pull/128623), [@AnishShah](https://github.com/AnishShah)) [SIG Apps and Node] - Fake clientsets use a common, generic implementation. The corresponding structs are now private, callers must use the corresponding constructors. ([#126503](https://github.com/kubernetes/kubernetes/pull/126503), [@skitt](https://github.com/skitt)) [SIG API Machinery, Architecture, Auth and Instrumentation] - Removed support for removing requests and limits during a pod resize. ([#128683](https://github.com/kubernetes/kubernetes/pull/128683), [@AnishShah](https://github.com/AnishShah)) [SIG Apps, Node and Testing] - Removed support for the kubelet `--runonce` mode. If you specify the kubelet command line flag `--runonce`, this is an error. Setting `runOnce` in a kubelet configuration file is also an error, and specifying any value for that configuration option is now deprecated. ([#126336](https://github.com/kubernetes/kubernetes/pull/126336), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Node and Scalability] - Revised error handling for port forwards to Pods. Added stream stream resets preventing port-forward from blockage. ([#128681](https://github.com/kubernetes/kubernetes/pull/128681), [@soltysh](https://github.com/soltysh)) [SIG API Machinery, CLI and Testing] - The feature-gate "PodHostIPs" has been removed. It is GA and its value has been locked since Kubernetes v1.30. ([#128634](https://github.com/kubernetes/kubernetes/pull/128634), [@thockin](https://github.com/thockin)) [SIG Apps, Architecture, Node and Testing] - With the CBORServingAndStorage feature gate enabled, built-in APIs can be served in CBOR format for clients that request it. ([#128503](https://github.com/kubernetes/kubernetes/pull/128503), [@benluddy](https://github.com/benluddy)) [SIG API Machinery, Etcd and Testing] ## Dependencies ### Added _Nothing has changed._ ### Changed - cel.dev/expr: v0.15.0 → v0.18.0 - github.com/Microsoft/hnslib: [v0.0.7 → v0.0.8](https://github.com/Microsoft/hnslib/compare/v0.0.7...v0.0.8) - github.com/google/cel-go: [v0.21.0 → v0.22.0](https://github.com/google/cel-go/compare/v0.21.0...v0.22.0) - github.com/opencontainers/selinux: [v1.11.0 → v1.11.1](https://github.com/opencontainers/selinux/compare/v1.11.0...v1.11.1) - google.golang.org/genproto/googleapis/api: 5315273 → f6391c0 - google.golang.org/genproto/googleapis/rpc: f6361c8 → f6391c0 - k8s.io/kube-openapi: f7e401e → 32ad38e ### Removed - go.opencensus.io: v0.24.0 # v1.32.0-beta.0 ## Downloads for v1.32.0-beta.0 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes.tar.gz) | bb901478a959462a53748044c13fc4bd724ee8ac778c2c474446ce4229c925664e45744f37f16d278926348528076051ecd5b52035fe4deddd87a6dc7399a691 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-src.tar.gz) | 9c3d0ab91df95d62801501de594d988e296061ba8eb48172aa11c54a851915e7090b8beeb54890fa1dbc4068f9f255c5daa5f0f58b399b065ab40b13397956d1 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-darwin-amd64.tar.gz) | b3241c51e8dd477e4fea33bfbf6fb4703d7496751af3694908477134401a42f10c6fb94335821b0a8ee674e33ef61cbe34e095561d479ba9178470e6b07fbec7 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-darwin-arm64.tar.gz) | a8cf6c966a74e17d94ba237b305abe7731429c5cb1b937a7aaa97b28e3e65ce5b4dc386095fbc6929a61f04159c72857dce937f737630e7f9f9acbcf3e7d4621 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-linux-386.tar.gz) | e95240b371c4bc1076fc1fce8b09e1997068b7dd238a037b4940b3b970024b83146f528d562b9d9522acdd24a16bfacae45079c92eaafe8fa052b380c4e46d68 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-linux-amd64.tar.gz) | 9dd52cd0e433ee9d4045495288da615281980fbf22c2a889494e7811bacc9fe5269aa475c34421671090fec3a14e16c41a254e2047b4363731dc7e390e0c747c [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-linux-arm.tar.gz) | c31a8d7046cf87b7b10100dc185d793cb46ea6c15822feb05b0203bd463714627c4722f048cff6d1128e323847df167aaa8659c37a2c897576feadb74898ca8e [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-linux-arm64.tar.gz) | cce0c249dd0ea45b7a39ca3c3a45b2779a105c6422f0c6b90d5085b3a2f3f926180735efdcabc1f17076d7f3858429bc69f2c2c623047e9bfc96d3aebc9d7b65 [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-linux-ppc64le.tar.gz) | 12e41f7b22ad3303b97a05988e2fe53d783ca76df6c2c01d6045c0d3503e5abe62dc5dafe2f04fd1b9f83467b5b31e94da15b4034f1efdfb8a24f61d71f5fb7a [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-linux-s390x.tar.gz) | 6c6987962d7b4919f560a0242eaf948b739fc5dc0a992dfc410e39cb75da6ca869a08c51e6b3fab0b341cb00da3a6eb36842421b16f3f1b6119334282cf56043 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-windows-386.tar.gz) | 0f2adfe62d917d405bf7d238adfbf945b6aa898c7d9d536afd457f7b71727dd99853b42cc8ecd61435f6e1816689afed359bed88492906f4607a2cfac1bd8076 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-windows-amd64.tar.gz) | d26970c2331a18ededd36b4bbf3ccd1b4b9d27dec4bce5ef5b84a78c55a698ea2a898deaa2d12f8093bcca9c5f4e9d53cedd3eebed81be44e40ff4a88a9b2751 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-client-windows-arm64.tar.gz) | e80c1a02d23c156c9c448e33e405f5b7d9a8919236219efb9bfac34a4d0bf3935063d5e0570359bf3260f167ab443e49b46bbcfcee61ac160d2f513fff56e7e2 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-server-linux-amd64.tar.gz) | 0f7150b39e607e8543296b46b32c7b90a8afe4980051f3d15a447091d6019db501a6de37ecd94e24cfc943b6edb3e555f09ed5098dae070f38fbf439720a69c7 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-server-linux-arm64.tar.gz) | 925964b3dbbb96cb4f8e78a983d49926304a63b216a0163d6602c564614f090fe0db55da31b808643ed77e238c03775e91664c614f4a05fb6309119106585f22 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-server-linux-ppc64le.tar.gz) | 8b1c42c01db9687b948082aa93ef3ce9ea33aa36c4c55de471c12e06f71a2f4af4c1942f8a8f7744fc5cb28fefdf77d8784ff33d9af8d401c3bed2fa835142ae [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-server-linux-s390x.tar.gz) | 8833ad6e984ffa427cb125cdc15759d1f03cebecd4f723209481d7ffcc1abc259851d7e8ffbf531af2bbd9166c1594e9730197edff157b8719b93e62af71bbcb ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-node-linux-amd64.tar.gz) | 40d539f90ec3c3d9a8bc9df533dc6185a8313a0fb83045b77294e5896c6d9517941ceb5aa58012364136490b5c2ad73df59deb1f5e5a526177137cd08bacf360 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-node-linux-arm64.tar.gz) | d2edaba95fda9f658b16dfc127451ad3f2d89a2ddc832caa1bf8d97c69931820675264593803042584dd7bcb1ea881c6b53e588e50a414d32fb9f643c36c5c90 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-node-linux-ppc64le.tar.gz) | 32bbf383c9d3f1386313f57096c51e5cb21fdd7842758abd99cf7e3275f78da70208534ec417d1ad2af1b54dc976416d1a007eb4e501db5b8a4757fc0cd7ccac [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-node-linux-s390x.tar.gz) | cfc11d4d2d26df6c4504f620691e01a47250cf3b23a7337ffa63d36da91fca89b191f59e7f0d77395c91fa687829ff8bf228ee1cfb0c939f1b810756f0ae2ded [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-beta.0/kubernetes-node-windows-amd64.tar.gz) | b635f0e8a033ef48d519e1da6803a328aaacc0ddd8ae59e7b6b9b8908143c470e4a553a6723f13e795ba1d71ec3803bb976ec0a30896d4df0cc85178463b66a9 ### 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.32.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.32.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.32.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.32.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.32.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.32.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.32.0-alpha.3 ## Urgent Upgrade Notes ### (No, really, you MUST read this before you upgrade) - Fix the bug of InPlacePodVerticalScaling state un-marshalling. State stored in `/var/lib/kubelet/pod_status_manager_state` is now can always be read back after kubelet restart. Since the checkpoint format was changed to fix the issue, if you are using the feature `InPlacePodVerticalScaling`, please clean up the state file `/var/lib/kubelet/pod_status_manager_state` when upgrading the kubelet as failrue to do it will lead to incompatible state formats and kubelet's failure to start. ([#126620](https://github.com/kubernetes/kubernetes/pull/126620), [@yunwang0911](https://github.com/yunwang0911)) [SIG Node] ## Changes by Kind ### Deprecation - ServiceAccount metadata.annotations[kubernetes.io/enforce-mountable-secrets]: deprecated since v1.32; no removal deadline. Prefer separate namespaces to isolate access to mounted secrets. ([#128396](https://github.com/kubernetes/kubernetes/pull/128396), [@ritazh](https://github.com/ritazh)) [SIG API Machinery, Apps, Auth, CLI and Testing] ### API Change - DRA: scheduling pods is up to 16x faster, depending on the scenario. Scheduling throughput depends a lot on cluster utilization. It is higher for lightly loaded clusters with free resources and gets lower when the cluster utilization increases. ([#127277](https://github.com/kubernetes/kubernetes/pull/127277), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Architecture, Auth, Etcd, Instrumentation, Node, Scheduling and Testing] - DRA: the `DeviceRequestAllocationResult` struct now has an "AdminAccess" field which should be used instead of the corresponding field in the `DeviceRequest` field when dealing with an allocation. If a device is only allocated for admin access, allocating it again for normal usage is now supported, as originally intended. To allow admin access, starting with 1.32 the `DRAAdminAccess` feature gate must be enabled. ([#127266](https://github.com/kubernetes/kubernetes/pull/127266), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Network, Node, Scheduling and Testing] - Implemented a new, alpha `seLinuxChangePolicy` field within a Pod-level `securityContext`, under SELinuxChangePolicy feature gate. This field allows for opting out from mounting Pod volumes with SELinux label when SELinuxMount feature is enabled (it is alpha and disabled by default now). Please see [the KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/1710-selinux-relabeling#story-3-cluster-upgrade) how we expect to warn users before any SELinux behavior changes and how they can opt-out before. Note that this field and feature gate is useful only with clusters that run with SELinux enabled. No action is required on clusters without SELinux. ([#127981](https://github.com/kubernetes/kubernetes/pull/127981), [@jsafrane](https://github.com/jsafrane)) [SIG API Machinery, Apps, Architecture, Node, Storage and Testing] - Introduce v1alpha1 API for mutating admission policies, enabling extensible admission control via CEL expressions (KEP 3962: Mutating Admission Policies). To use, enable the `MutatingAdmissionPolicy` feature gate and the `admissionregistration.k8s.io/v1alpha1` API via `--runtime-config`. ([#127134](https://github.com/kubernetes/kubernetes/pull/127134), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery, Auth, Etcd and Testing] - Kube-proxy now reconciles Service/Endpoint changes with conntrack table and cleans up only stale UDP flow entries ([#127318](https://github.com/kubernetes/kubernetes/pull/127318), [@aroradaman](https://github.com/aroradaman)) [SIG Network and Windows] - Removed generally available feature gate `HPAContainerMetrics` ([#126862](https://github.com/kubernetes/kubernetes/pull/126862), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Autoscaling] ### Feature - Add `--concurrent-daemonset-syncs` command line flag to kube-controller-manager. The value sets the number of workers for the daemonset controller. ([#128444](https://github.com/kubernetes/kubernetes/pull/128444), [@tosi3k](https://github.com/tosi3k)) [SIG API Machinery] - Added a kubelet metrics to report informations about the cpu pools managed by cpumanager when the static policy is in use. ([#127506](https://github.com/kubernetes/kubernetes/pull/127506), [@ffromani](https://github.com/ffromani)) [SIG Node 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. ([#127483](https://github.com/kubernetes/kubernetes/pull/127483), [@jingczhang](https://github.com/jingczhang)) [SIG Node] - Added metrics to measure latency of DRA Node operations and DRA GRPC calls ([#127146](https://github.com/kubernetes/kubernetes/pull/127146), [@bart0sh](https://github.com/bart0sh)) [SIG Instrumentation, Network, Node and Testing] - Adopted a new implementation of watch caches for **list** verbs, using a btree data structure. The new implementation is active by default; you can opt out by disabling the `BtreeWatchCache` feature gate. ([#128415](https://github.com/kubernetes/kubernetes/pull/128415), [@serathius](https://github.com/serathius)) [SIG API Machinery, Auth and Cloud Provider] - Allows PreStop lifecycle handler's sleep action to have a zero value ([#127094](https://github.com/kubernetes/kubernetes/pull/127094), [@sreeram-venkitesh](https://github.com/sreeram-venkitesh)) [SIG Apps, Node and Testing] - Fix: Avoid overwriting in-pod vertical scaling updates on systemd daemon reloads when using systemd ([#124216](https://github.com/kubernetes/kubernetes/pull/124216), [@iholder101](https://github.com/iholder101)) [SIG Node] - Graduate Kubelet Memory Manager to GA. ([#128517](https://github.com/kubernetes/kubernetes/pull/128517), [@Tal-or](https://github.com/Tal-or)) [SIG Node] - Kubeadm: consider --bind-address or --advertise-address and --secure-port for control plane components when the feature gate WaitForAllControlPlaneComponents is enabled. Use /livez for kube-apiserver and kube-scheduler, but continue using /healthz for kube-controller-manager until it supports /livez. ([#128474](https://github.com/kubernetes/kubernetes/pull/128474), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Label `apps.kubernetes.io/pod-index` added to Pod from StatefulSets is promoted to stable Label `batch.kubernetes.io/job-completion-index` added to Pods from Indexed Jobs is promoted to stable ([#128387](https://github.com/kubernetes/kubernetes/pull/128387), [@alaypatel07](https://github.com/alaypatel07)) [SIG Apps] - PodLifecycleSleepAction is graduated to GA ([#128046](https://github.com/kubernetes/kubernetes/pull/128046), [@AxeZhan](https://github.com/AxeZhan)) [SIG Architecture, Node and Testing] - Promoted `RecoverVolumeExpansionFailure` feature gate to beta. ([#128342](https://github.com/kubernetes/kubernetes/pull/128342), [@gnufied](https://github.com/gnufied)) [SIG Apps and Storage] - Realign line breaks from `kubectl explain` descriptions. ([#126533](https://github.com/kubernetes/kubernetes/pull/126533), [@ah8ad3](https://github.com/ah8ad3)) [SIG CLI] - Vendor: update system-validators to v1.9.1 ([#128533](https://github.com/kubernetes/kubernetes/pull/128533), [@neolit123](https://github.com/neolit123)) [SIG Node] - Windows: Support CPU and Topology manager on Windows ([#125296](https://github.com/kubernetes/kubernetes/pull/125296), [@jsturtevant](https://github.com/jsturtevant)) [SIG Node and Windows] ### Bug or Regression - Fix an issue where eviction manager was not deleting unused images or containers when it detected containerfs signal. ([#127874](https://github.com/kubernetes/kubernetes/pull/127874), [@AnishShah](https://github.com/AnishShah)) [SIG Node] - Fixed a suboptimal scheduler preemption behavior where potential preemption victims were violating Pod Disruption Budgets. ([#128307](https://github.com/kubernetes/kubernetes/pull/128307), [@NoicFank](https://github.com/NoicFank)) [SIG Scheduling] - Fixed an issue in the kubelet that showed when writeable layers and read-only layers were at different paths within the same mount. Kubernetes was previously detecting that the image filesystem was split, even when that was not really the case ([#128344](https://github.com/kubernetes/kubernetes/pull/128344), [@kannon92](https://github.com/kannon92)) [SIG Node] - Fixes a race condition that could result in erroneous volume unmounts for flex volume plugins on kubelet restart ([#127669](https://github.com/kubernetes/kubernetes/pull/127669), [@olyazavr](https://github.com/olyazavr)) [SIG Storage] - Fixes the reporting of elapsed times during evaluation of ValidatingAdmissionPolicy decisions and annotations. The apiserver_validating_admission_policy_check_duration metrics will now show elapsed times and no longer be zero. ([#128463](https://github.com/kubernetes/kubernetes/pull/128463), [@knrc](https://github.com/knrc)) [SIG API Machinery] - Kubeadm: added "disable success" and "disable denial" as parameters of the "cache" plugin in the Corefile managed by kubeadm. This is to prevent conflicting responses during CoreDNS cache updates. ([#128359](https://github.com/kubernetes/kubernetes/pull/128359), [@matteriben](https://github.com/matteriben)) [SIG Cluster Lifecycle] - Kubelet: Fix the volume manager didn't check the device mount state in the actual state of the world before marking the volume as detached. It may cause a pod to be stuck in the Terminating state due to the above issue when it was deleted. ([#128219](https://github.com/kubernetes/kubernetes/pull/128219), [@carlory](https://github.com/carlory)) [SIG Node] - Makes kubelet's /metrics/slis endpoint always available ([#128430](https://github.com/kubernetes/kubernetes/pull/128430), [@richabanker](https://github.com/richabanker)) [SIG Architecture, Instrumentation and Node] - Tighten validation on the qosClass field of pod status. This field is immutable but it would be populated with the old status by kube-apiserver if it is unset in the new status when updating this field via the status subsource. ([#127744](https://github.com/kubernetes/kubernetes/pull/127744), [@carlory](https://github.com/carlory)) [SIG Apps, Instrumentation, Node, Storage and Testing] ### Other (Cleanup or Flake) - Removed generally available feature-gate `ZeroLimitedNominalConcurrencyShares` ([#126894](https://github.com/kubernetes/kubernetes/pull/126894), [@carlory](https://github.com/carlory)) [SIG API Machinery] - The `dynamicResources` has been refactored to `DynamicResources`, now users can introduce the `DynamicResources` struct outside the `dynamicresources` package. ([#128399](https://github.com/kubernetes/kubernetes/pull/128399), [@JesseStutler](https://github.com/JesseStutler)) [SIG Node and Scheduling] ## Dependencies ### Added - github.com/checkpoint-restore/go-criu/v6: [v6.3.0](https://github.com/checkpoint-restore/go-criu/tree/v6.3.0) - github.com/moby/sys/user: [v0.3.0](https://github.com/moby/sys/tree/user/v0.3.0) ### Changed - github.com/cilium/ebpf: [v0.11.0 → v0.16.0](https://github.com/cilium/ebpf/compare/v0.11.0...v0.16.0) - github.com/cyphar/filepath-securejoin: [v0.2.4 → v0.3.4](https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.3.4) - github.com/google/cadvisor: [v0.50.0 → v0.51.0](https://github.com/google/cadvisor/compare/v0.50.0...v0.51.0) - github.com/google/pprof: [813a5fb → d1b30fe](https://github.com/google/pprof/compare/813a5fb...d1b30fe) - github.com/onsi/ginkgo/v2: [v2.19.0 → v2.21.0](https://github.com/onsi/ginkgo/compare/v2.19.0...v2.21.0) - github.com/onsi/gomega: [v1.33.1 → v1.35.1](https://github.com/onsi/gomega/compare/v1.33.1...v1.35.1) - github.com/opencontainers/runc: [v1.1.15 → v1.2.1](https://github.com/opencontainers/runc/compare/v1.1.15...v1.2.1) - github.com/urfave/cli: [v1.22.1 → v1.22.14](https://github.com/urfave/cli/compare/v1.22.1...v1.22.14) - google.golang.org/protobuf: v1.34.2 → v1.35.1 - k8s.io/system-validators: v1.8.0 → v1.9.1 - k8s.io/utils: 18e509b → 3ea5e8c - sigs.k8s.io/structured-merge-diff/v4: v4.4.1 → v4.4.2 ### Removed - github.com/checkpoint-restore/go-criu/v5: [v5.3.0](https://github.com/checkpoint-restore/go-criu/tree/v5.3.0) - github.com/containerd/cgroups: [v1.1.0](https://github.com/containerd/cgroups/tree/v1.1.0) - github.com/daviddengcn/go-colortext: [v1.0.0](https://github.com/daviddengcn/go-colortext/tree/v1.0.0) - github.com/frankban/quicktest: [v1.14.5](https://github.com/frankban/quicktest/tree/v1.14.5) - github.com/golangplus/bytes: [v1.0.0](https://github.com/golangplus/bytes/tree/v1.0.0) - github.com/golangplus/fmt: [v1.0.0](https://github.com/golangplus/fmt/tree/v1.0.0) - github.com/golangplus/testing: [v1.0.0](https://github.com/golangplus/testing/tree/v1.0.0) - github.com/shurcooL/sanitized_anchor_name: [v1.0.0](https://github.com/shurcooL/sanitized_anchor_name/tree/v1.0.0) # v1.32.0-alpha.3 ## Downloads for v1.32.0-alpha.3 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes.tar.gz) | 8e63fb26192ea5fcb01e678aefad000b24e4a3dd0c22786e799f32cb247b356acff608112e8da82265475a743ad6f261f412b0b6efbfeb2919a4cfa00ba9410d [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-src.tar.gz) | ee32a2c0404876082b4bbc254692428cb149a14a1c2525053ce1ea95ea5de25513d694f035efe7c38902e0982fd92d130a3164e9e53b8439b3dc74b72a8faed0 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-darwin-amd64.tar.gz) | bd0f891706174cf4a6b4c201e24861d5e200c86e188eeb7fb61708164c64814826f362a425c01e687fc92124ed25b145cb5fc9b9ffa7e495d43c91247832f042 [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-darwin-arm64.tar.gz) | 315c8b6cf7e8e2c677139bc89d717fc2c60e3ac44cc51dc90716c06f45ba534269fbdbe624781f20e3d785b24c6d9d4ef399b4ffc7b6392610c4d0531c24f707 [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-linux-386.tar.gz) | 5128751b6e2be1cb2e84e326ffe4f356c05256b7afdb46c3d8378750b005be368364b6cc588f9d91fcc8ae30c1085f0cdd88889f48cdafa13dbb2c833d0f340d [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-linux-amd64.tar.gz) | f73f8e6039b483f3427b379b109f574f06c075d6c1c9f7494d379f4408cc64445b7af3f7b269b693f0c55d3fb9c9239b7bb9b0040d71cf300123503178778544 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-linux-arm.tar.gz) | 21648d86c8b1862ab3ce4fbe4fbe051a918b86cbfab226c0643748d1fe67fea9827aa009a1d37e832fd7ca6d8744f5a3531cd478ab51b7ef7a52e08cda5e26a1 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-linux-arm64.tar.gz) | 07d884142a8626db828422b85d6f4518a5852b76f4e598fdc23ad3fae589c8ab4d5e47bc9d8b05f02892519ab08710a38f65743020200e6f58ba2201b6885f4c [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-linux-ppc64le.tar.gz) | b952e4c58c168136e5d9458c5ea7888bfe46a963077d0319ef8588018b9d64ec6a06916e70091352d516223313e00a4e5e6480da7c6ef332bb8d2a6c04874b35 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-linux-s390x.tar.gz) | e672faf92802a0f62c5e47209d756e3832541720cf4992516b41ae4eab3b992b8d650ba104304e3109dfe2a10e4af923fdc56bac86da7ef485c24cf0b6948e19 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-windows-386.tar.gz) | faea07933885a63737853aed53878a4abd0a3582254122c847fc63b1e728e6d3fe6d2785aaa3b467c6aa98271bb2785cb94e4b216fff60f66c052331e0e3e70f [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-windows-amd64.tar.gz) | f6e202365fd3fa33f28526dae6c750c15d4784bfb4c4a011e3cb07a8bb817ed29a43d76b258e0be31075f82f2f8a030f364b2b91612d54d3508fffd8d0e2fd3d [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-client-windows-arm64.tar.gz) | 048c9deff34a349409d08b0e6889b82c1dfb49af09f00c0b77f88a5ea459348d5206f9a12a869cc8264ca328b58095adaf2ac508f08bfda2d6dc1b8735987fd6 ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-server-linux-amd64.tar.gz) | 9c7dea0269e894f6ca9410667720d6d1d1bc9e690b9da5d34e7c775a0f6fbcf22c51b6bd2805ea6fb0e61eca815aea2fb675c4827d1bc14cbecb604220d18ed6 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-server-linux-arm64.tar.gz) | b871099bd869adcf4180bbddf1258e088172d1e90da7ade3d8af58866fef73d0bd928b4643bdf6f061042859d123ed86b1177b84aaef5f81b1eee302d7b8e1ff [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-server-linux-ppc64le.tar.gz) | da51792904eb2f06e5f84ef20e91e6f5e1f128af6f61f0492054739780178d1ab56e84a344dac9f6b3ba82bf4553a1ffa8c9028db08ecc9657125671b28c68e3 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-server-linux-s390x.tar.gz) | 20f3c235d2218c4f8251458de153535fbf529a3583ab687abc48f48df72ab423fdca7b8961fc5dbf25877e695ff6572bd7564931dc444c98081f4ff02f724ef9 ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-node-linux-amd64.tar.gz) | 0188737cde5aebc4332a6fc78959c47a0db187b6ed5b28f749a9f7a20111e507539399290aff1cb88a257a72d337dd4e60f19dfcb029995cdadb4d1370ad2ac5 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-node-linux-arm64.tar.gz) | 28d59f3a211ffac196ae94864a8c5d547a34a5f89777d3c4a0d964d43a5cc352945af68e09e780d4e6ec230f64e91c52faeb3019553bea24a14c18e284746166 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-node-linux-ppc64le.tar.gz) | c055f42aa3345a01e73df4131ed9409cc99e1828ea1c98307d394b7eddc6f913c13a24f4e101c67eb8551d2cfb4d69464e6d10670657ce39aca0aed52559b38a [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-node-linux-s390x.tar.gz) | 559789272cb8ddb77e2600034b330f588dd3d0054c7da07b9e7f37c0cc6175f63aec987c8cf7d309145394687422c1a5a635e7a82727af8713928d76e4b03ee9 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.3/kubernetes-node-windows-amd64.tar.gz) | 9c53bf29311542c814524413f4839c07aa87159be5a166883bdabf4a8cb98b648812384be20d93cc63b20b3357822a84f85aa7d47350ff7d36c7930980b27c97 ### 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.32.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.32.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.32.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.32.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.32.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.32.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.32.0-alpha.2 ## Changes by Kind ### API Change - Added enforcement of an upper cost bound for DRA evaluations of CEL. The API server and scheduler now enforce an upper bound on the cost and runtime steps required for evaluating a CEL expression. ([#128101](https://github.com/kubernetes/kubernetes/pull/128101), [@pohly](https://github.com/pohly)) [SIG API Machinery and Node] - Annotation `batch.kubernetes.io/cronjob-scheduled-timestamp` added to Job objects scheduled from CronJobs is promoted to stable ([#128336](https://github.com/kubernetes/kubernetes/pull/128336), [@soltysh](https://github.com/soltysh)) [SIG Apps] - Apply fsGroup policy for ReadWriteOncePod volumes ([#128244](https://github.com/kubernetes/kubernetes/pull/128244), [@gnufied](https://github.com/gnufied)) [SIG Storage and Testing] - Graduate Job's ManagedBy field to Beta ([#127402](https://github.com/kubernetes/kubernetes/pull/127402), [@mimowo](https://github.com/mimowo)) [SIG API Machinery, Apps and Testing] - Kube-apiserver: Promoted the `StructuredAuthorizationConfiguration` feature gate to GA. The `--authorization-config` flag now accepts `AuthorizationConfiguration` in version `apiserver.config.k8s.io/v1` (with no changes from `apiserver.config.k8s.io/v1beta1`). ([#128172](https://github.com/kubernetes/kubernetes/pull/128172), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Auth and Testing] - Removed all support for _classic_ dynamic resource allocation (DRA). The `DRAControlPlaneController` feature gate, formerly alpha, is no longer available. Kubernetes now only uses the _structured parameters_ model (also alpha) for allocating dynamic resources to Pods. if and only if classic DRA was enabled in a cluster, remove all workloads (pods, app deployments, etc. ) which depend on classic DRA and make sure that all PodSchedulingContext resources are gone before upgrading. PodSchedulingContext resources cannot be removed through the apiserver after an upgrade and workloads would not work properly. ([#128003](https://github.com/kubernetes/kubernetes/pull/128003), [@pohly](https://github.com/pohly)) [SIG API Machinery, Apps, Auth, Etcd, Node, Scheduling and Testing] - Revised the Kubelet API Authorization with new subresources, that allow finer-grained authorization checks and access control for kubelet endpoints. Provided you enable the `KubeletFineGrainedAuthz` feature gate, you can access kubelet's `/healthz` endpoint by granting the caller `nodes/helathz` permission in RBAC. Similarly you can also access kubelet's `/pods` endpoint to fetch a list of Pods bound to that node by granting the caller `nodes/pods` permission in RBAC. Similarly you can also access kubelet's `/configz` endpoint to fetch kubelet's configuration by granting the caller `nodes/configz` permission in RBAC. You can still access kubelet's `/healthz`, `/pods` and `/configz` by granting the caller `nodes/proxy` permission in RBAC but that also grants the caller permissions to exec, run and attach to containers on the nodes and doing so does not follow the least privilege principle. Granting callers more permissions than they need can give attackers an opportunity to escalate privileges. ([#126347](https://github.com/kubernetes/kubernetes/pull/126347), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) [SIG API Machinery, Auth, Cluster Lifecycle and Node] ### Feature - Added a kubelet metric `container_aligned_compute_resources_count` to report the count of containers getting aligned compute resources ([#127155](https://github.com/kubernetes/kubernetes/pull/127155), [@ffromani](https://github.com/ffromani)) [SIG Node and Testing] - Added kubelet support for systemd watchdog integration. With this enabled, systemd can automatically recover a hung kubelet. ([#127566](https://github.com/kubernetes/kubernetes/pull/127566), [@zhifei92](https://github.com/zhifei92)) [SIG Cloud Provider, Node and Testing] - CRI: Add field to support CPU affinity on Windows ([#124285](https://github.com/kubernetes/kubernetes/pull/124285), [@kiashok](https://github.com/kiashok)) [SIG Node and Windows] - Change OOM score adjustment calculation for sidecar container : the OOM adjustment for these containers will match or fall below the OOM score adjustment of regular containers in the Pod. ([#128029](https://github.com/kubernetes/kubernetes/pull/128029), [@bouaouda-achraf](https://github.com/bouaouda-achraf)) [SIG Node] - DRA: the resource claim controller now maintains metrics about the total number of ResourceClaims and the number of allocated ResourceClaims. ([#127661](https://github.com/kubernetes/kubernetes/pull/127661), [@pohly](https://github.com/pohly)) [SIG Apps, Instrumentation and Node] - Kube-apiserver: Promoted `AuthorizeWithSelectors` feature to beta, which includes field and label selector information from requests in webhook authorization calls. Promoted `AuthorizeNodeWithSelectors` feature to beta, which changes node authorizer behavior to limit requests from node API clients, so that each Node can only get / list / watch its own Node API object, and can also only get / list / watch Pod API objects bound to that node. Clients using kubelet credentials to read other nodes or unrelated pods must change their authentication credentials (recommended), adjust their usage, or obtain broader read access independent of the node authorizer. ([#128168](https://github.com/kubernetes/kubernetes/pull/128168), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Auth and Testing] - Locking the feature custom profiling in kubectl debug to true. ([#127187](https://github.com/kubernetes/kubernetes/pull/127187), [@ardaguclu](https://github.com/ardaguclu)) [SIG CLI and Testing] - New implementation of watch cache using btree data structure. Implementation is not enabled yet. ([#126754](https://github.com/kubernetes/kubernetes/pull/126754), [@serathius](https://github.com/serathius)) [SIG API Machinery, Auth, Cloud Provider and Etcd] - Promote SizeMemoryBackedVolumes to stable ([#126981](https://github.com/kubernetes/kubernetes/pull/126981), [@kannon92](https://github.com/kannon92)) [SIG Node, Storage and Testing] - Promoted the `RelaxedEnvironmentVariableValidation` feature gate to beta and is enabled by default. ([#126897](https://github.com/kubernetes/kubernetes/pull/126897), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG Node] - Promotes the ServiceAccountTokenJTI feature to GA, which adds a `jti` claim to issued service account tokens and embeds the `jti` claim as a `authentication.kubernetes.io/credential-id=["JTI=..."]` value in user extra info - Promotes the ServiceAccountTokenPodNodeInfo feature to GA, which adds the node name and uid as claims into service account tokens mounted into running pods, and embeds that information as `authentication.kubernetes.io/node-name` and `authentication.kubernetes.io/node-uid` user extra info when the token is used - Promotes the ServiceAccountTokenNodeBindingValidation feature to GA, which validates service account tokens bound directly to nodes. ([#128169](https://github.com/kubernetes/kubernetes/pull/128169), [@liggitt](https://github.com/liggitt)) [SIG API Machinery, Auth and Testing] - TopologyManagerPolicyOptions feature-flag is promoted to GA ([#128124](https://github.com/kubernetes/kubernetes/pull/128124), [@PiotrProkop](https://github.com/PiotrProkop)) [SIG Node] ### Documentation - Fixed documentation for the `apiserver_admission_webhook_fail_open_count` and `apiserver_admission_webhook_request_total` metrics. The `type` label can have a value of "admit", not "mutating". ([#127898](https://github.com/kubernetes/kubernetes/pull/127898), [@modulitos](https://github.com/modulitos)) [SIG API Machinery] - The kubelet, when using --cloud-provider=external can use the --node-ip flag with one of the unspecified addresses 0.0.0.0 or ::, to create the Node with the IP of the default gateway of the corresponding IP family and then delegating the responsibility to the external cloud provider. This solve the bootstrap problems of out of tree cloud providers that are deployed as Pods within the cluster. ([#125337](https://github.com/kubernetes/kubernetes/pull/125337), [@aojea](https://github.com/aojea)) [SIG Cloud Provider, Network, Node and Testing] ### Bug or Regression - DRA: fixed several issues related to "allocationMode: all" ([#127565](https://github.com/kubernetes/kubernetes/pull/127565), [@pohly](https://github.com/pohly)) [SIG Node] - Fix bug where PodCIDR was released before node was deleted ([#128305](https://github.com/kubernetes/kubernetes/pull/128305), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps and Network] - Fixed an issue in the kubelet that showed when writeable layers and read-only layers were at different paths within the same mount. Kubernetes was previously detecting that the image filesystem was split, even when that was not really the case. ([#126562](https://github.com/kubernetes/kubernetes/pull/126562), [@kannon92](https://github.com/kannon92)) [SIG Node] - Fixes 1.31 regression that can crash kube-controller-manager's service-lb-controller loop ([#128182](https://github.com/kubernetes/kubernetes/pull/128182), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider and Network] - Kubelet: fix a bug where kubelet wrongly drops the QOSClass field of the Pod's s status when it rejects a Pod ([#128083](https://github.com/kubernetes/kubernetes/pull/128083), [@carlory](https://github.com/carlory)) [SIG Node and Testing] - Reset streams when an error happens during port-forward allowing kubectl to maintain port-forward connection open ([#128318](https://github.com/kubernetes/kubernetes/pull/128318), [@soltysh](https://github.com/soltysh)) [SIG API Machinery, CLI and Node] - The `build-tag` flag is reintroduced to conversion-gen and defaulter-gen which allow users to inject custom build tag during code generation process. ([#128259](https://github.com/kubernetes/kubernetes/pull/128259), [@dinhxuanvu](https://github.com/dinhxuanvu)) [SIG API Machinery] - Unallowed label values will show up as "unexpected" in all system components metrics ([#128100](https://github.com/kubernetes/kubernetes/pull/128100), [@yongruilin](https://github.com/yongruilin)) [SIG Architecture and Instrumentation] ### Other (Cleanup or Flake) - Added: Log Line for Debugging possible merge errors for Kubelet related Config requests. ([#124389](https://github.com/kubernetes/kubernetes/pull/124389), [@holgerson97](https://github.com/holgerson97)) [SIG Node] - Append the image pull error for the pods `status.containerStatuses[*].state.waiting.message` when in image pull back-off (`reason` is `ImagePullBackOff`) instead of the generic `Back-off pulling image…` message. ([#127918](https://github.com/kubernetes/kubernetes/pull/127918), [@saschagrunert](https://github.com/saschagrunert)) [SIG Node and Testing] - Clarified an API validation error for toleration if `operator` is `Exists` and `value` is not empty. ([#128119](https://github.com/kubernetes/kubernetes/pull/128119), [@saschagrunert](https://github.com/saschagrunert)) [SIG API Machinery and Apps] - Feature `AllowServiceLBStatusOnNonLB` remains deprecated and is now locked to false to support compatibility versions ([#128139](https://github.com/kubernetes/kubernetes/pull/128139), [@Jefftree](https://github.com/Jefftree)) [SIG Apps] - Fixes a bug in the `k8s.io/cloud-provider/service` controller, it may panic when a service is updated because the event recorder was used before it was initialized. All cloud providers should using the `v1.31.0` cloud provider service controller must ensure that the controllers is initialized before the informer start to process events or update it to the version 1.32.0. ([#128179](https://github.com/kubernetes/kubernetes/pull/128179), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider, Network and Testing] - Fully remove PostStartHookContext.StopCh ([#127341](https://github.com/kubernetes/kubernetes/pull/127341), [@mjudeikis](https://github.com/mjudeikis)) [SIG API Machinery] - Kube-apiserver `--admission-control-config-file` files are now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now cause an error. ([#128013](https://github.com/kubernetes/kubernetes/pull/128013), [@seans3](https://github.com/seans3)) [SIG API Machinery] - Kubeadm: removed preflight check for existence of the conntrack binary, as conntrack is no longer a kube-proxy dependency in version 1.32 and newer. ([#126953](https://github.com/kubernetes/kubernetes/pull/126953), [@aroradaman](https://github.com/aroradaman)) [SIG Cluster Lifecycle] - Output a log as v4-level when probe is triggered and shift the periodic timer of ReadinessProbe after manual run. ([#119089](https://github.com/kubernetes/kubernetes/pull/119089), [@mochizuki875](https://github.com/mochizuki875)) [SIG Node] - Removed legacy cloud provider integration code and the "service-lb-controller", "cloud-node-lifecycle-controller" and the "node-route-controller" from kube-controller-manager. You can now either set the `--cloud-provider` command line argument to "external", or to the empty string. All other values are invalid. ([#128197](https://github.com/kubernetes/kubernetes/pull/128197), [@aojea](https://github.com/aojea)) [SIG API Machinery, Apps and Cloud Provider] - Updated cni-plugins to [v1.6.0](https://github.com/containernetworking/plugins/releases/tag/v1.6.0). ([#128091](https://github.com/kubernetes/kubernetes/pull/128091), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider, Node and Testing] - `ComponentSLIs` feature is marked as GA and locked ([#128317](https://github.com/kubernetes/kubernetes/pull/128317), [@Jefftree](https://github.com/Jefftree)) [SIG Architecture and Instrumentation] ## Dependencies ### Added - github.com/moby/sys/userns: [v0.1.0](https://github.com/moby/sys/tree/userns/v0.1.0) ### Changed - github.com/vishvananda/netlink: [v1.3.0 → b1ce50c](https://github.com/vishvananda/netlink/compare/v1.3.0...b1ce50c) - k8s.io/system-validators: v1.9.0 → v1.8.0 - sigs.k8s.io/apiserver-network-proxy/konnectivity-client: v0.30.3 → v0.31.0 ### Removed _Nothing has changed._ # v1.32.0-alpha.2 ## Downloads for v1.32.0-alpha.2 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes.tar.gz) | 12fa6fbea15ce6c682f35d6a1942248a6e3d02112b5d4cd8ad4cb71c05234469a61e0a0a24cd7c0f31d03dbbfdba0c1f824b3c813ffade22c1df880d71961808 [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-src.tar.gz) | 41a87e299da2e0793859bf2ce61356313215f23036b1c15a56040089d0a6a049a38374cc4d55c25f1167f7b111c0b23745ebd271194392f67d57784f6b310079 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-darwin-amd64.tar.gz) | 5eaef34ed732b964eea1c695634c0a2310fc7383df59b10ee5ae620eea6df86ac089c77e5ea49e0a48ef3b4bbeeee5f98917cc1d82550f8ffd915829aa182c2d [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-darwin-arm64.tar.gz) | 2d25f8d105a2bb1cf5087e63689703a9bcaf89c98cd92bf9b95204c5544c7459ffcc62998cbb5118b26591ee56c75610b2407fa14e28af575c55d7f67e3f005f [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-linux-386.tar.gz) | a6626f989b0045d8c12cda459596766ba591dd4586a1d2ab2de25433f9195015b46b4cf1cc9db75945e0ca8e5453fd86b4f6dd49df8ec2ac0c40edcb4d7f21c9 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-linux-amd64.tar.gz) | d80eebb21798b8c5043c7b08b15d634c8c9e9179b44ef1cd9601fa05223c7ba696e5fe833f34778c457ae6e20b603156501122602697a159f790edb90659fa49 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-linux-arm.tar.gz) | d3a90dd1e38f379a5433023f2d10620a96a8b667baf51bc893b8ebb622ea675e7f965b13e5f94d0c0346f426ba7912ae80e31e36982bb30c3efd0f9e2dbd44c3 [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-linux-arm64.tar.gz) | ab7f0dca923cfbca492cf02c4625e946d4d9013d00ceee91c8adbb66cd0c42c305b2a0912fee65fba6f93d4ac7180729afbe65e02a98453334489fbddcfa81dd [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-linux-ppc64le.tar.gz) | f669e9d18a6d36462a13c5b1e3f71fd812554671b27070445275852788ad927d5f5a95964a6e2f035fc7cdcaeab68f130c97b256a1a3101877883f50b89d4a56 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-linux-s390x.tar.gz) | 870a52113f5c678271db4adbfd86c42710b9299d2d6f94581288ee5bce619723f3317bb0f36fa964d972c22d0a4539caee9a7caeb342fe1595f845de1b222812 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-windows-386.tar.gz) | caed3c909f1edb95d26e8ba1fd4a4dba8a2b377c22e9646cb85d208e4eb15dedf829b1a9f4b3c2afde85177b891d0482e3213668f8db0dcb549b40d209ec7ae5 [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-windows-amd64.tar.gz) | f020c3de77e4a6b34d3fc529932daec3bfafcf718e229fa111903a79635cae1012fc62225e5513c28fb173a0c52927ad152419fba6ff4c8afb148ea1a6ceba6f [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-client-windows-arm64.tar.gz) | a0e1c0f0dbe19ff8dcffd3713b828088b30c9f0ede4f7e65e083e3714e15da26bb361f2924a5edc7cf4f97c23cf9eab806cd11d8a616cb77df097a5ca1812e0f ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-server-linux-amd64.tar.gz) | d40f6a3dc056b68eb78788bb91e6f1d07f81b8b58ae0bda787be99c0f41c0ec87d2f652eb15aba0df5ab41f5c96144980415856155a7011d3f6195aba8030ff3 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-server-linux-arm64.tar.gz) | 7a56e4537b3d61875e8d61645383b82c4609b26b0eef17a1d6967cb52d990ad64a2f0c39910b0a2188930dc28ce1cec44f6aec86eba0dc4bdfc7329553d5b3d9 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-server-linux-ppc64le.tar.gz) | afdd9540cee13f8196fdaf5edbaf5f2ae5c792b94dbfaab461345a62d709591f13a06a037d3dd9374775fb1a3db82bf337a873391c989ed864790089f332f3a8 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-server-linux-s390x.tar.gz) | f5d8998bc1be3a31bf510af6dd5aa43d165d4424faa5157dd9fc6640f34e75c967379f3ea51f2049675843f8f3222d42cdb8ad61da0ccc5b35b21925f7318d02 ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-node-linux-amd64.tar.gz) | 0414c3d74019d5f932b3effba27580bd86ae6d8a6ae9f4c2a8967f70f15167f8c2805451fb4f18aaab8b9e1c0e47eaf627e4ea5844311ba095ddcfa2383ba4ff [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-node-linux-arm64.tar.gz) | 96a13271ab2cd2a3c5fe556de71f3b862b6263abe793a87ed123ac4bb928dc22ff9ad0219a0dc21669cc5fc333000091185fbc4bd8415f370870b56491f0fed4 [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-node-linux-ppc64le.tar.gz) | 2c92a70ca1285b3146b743dc812323db3eb1f52e0978ab4c42af9d4218260a4eb445928453298d264166768eb87f4b0db997e3cfd370112685e9836e890562bb [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-node-linux-s390x.tar.gz) | 65a84611fe4805c7937b0406a3818be923036402339a61cf1f0ce580229186bd520c65e083af8f9c9fce5dba15c4786c146d4d5254c878bc3d989bfc9b21db49 [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.2/kubernetes-node-windows-amd64.tar.gz) | f29148bf2230b726d57120cb62ebaf2f0d47b46fc4e5ad5d5a332c79a93e310bfacb471e7e95a79ba850933c47471bd934415fa1aec3cb655433fc034ed54296 ### 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.32.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.32.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.32.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.32.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.32.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.32.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.32.0-alpha.1 ## Changes by Kind ### API Change - Fixed a bug in the NestedNumberAsFloat64 Unstructured field accessor that could cause it to return rounded float64 values instead of errors when accessing very large int64 values. ([#128099](https://github.com/kubernetes/kubernetes/pull/128099), [@benluddy](https://github.com/benluddy)) [SIG API Machinery] - Introduce compressible resource setting on system reserved and kube reserved slices ([#125982](https://github.com/kubernetes/kubernetes/pull/125982), [@harche](https://github.com/harche)) [SIG Node] - Kubelet: the `--image-credential-provider-config` file is now loaded with strict deserialization, which fails if the config file contains duplicate or unknown fields. This protects against accidentally running with config files that are malformed, mis-indented, or have typos in field names, and getting unexpected behavior. ([#128062](https://github.com/kubernetes/kubernetes/pull/128062), [@aramase](https://github.com/aramase)) [SIG Auth and Node] - Promoted `CustomResourceFieldSelectors` to stable; the feature is enabled by default. `--feature-gates=CustomResourceFieldSelectors=true` not needed on kube-apiserver binaries and will be removed in a future release. ([#127673](https://github.com/kubernetes/kubernetes/pull/127673), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery and Testing] ### Feature - Add option to enable leader election in local-up-cluster.sh via the LEADER_ELECT cli flag. ([#127786](https://github.com/kubernetes/kubernetes/pull/127786), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery] - Added status for extended Pod resources within the `status.containerStatuses[].resources` field. ([#124227](https://github.com/kubernetes/kubernetes/pull/124227), [@iholder101](https://github.com/iholder101)) [SIG Node and Testing] - Allow pods to use the `net.ipv4.tcp_rmem` and `net.ipv4.tcp_wmem` sysctl by default when the kernel version is 4.15 or higher. With the kernel 4.15 the sysctl became namespaced. Pod Security admission allows these sysctl in v1.32+ versions of the baseline and restricted policies. ([#127489](https://github.com/kubernetes/kubernetes/pull/127489), [@pacoxu](https://github.com/pacoxu)) [SIG Auth, Network and Node] - Graduates the `WatchList` feature gate to Beta for kube-apiserver and enables `WatchListClient` for KCM. ([#128053](https://github.com/kubernetes/kubernetes/pull/128053), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery and Testing] - Kubernetes is now built with go 1.23.1 ([#127611](https://github.com/kubernetes/kubernetes/pull/127611), [@haitch](https://github.com/haitch)) [SIG Release and Testing] - Kubernetes is now built with go 1.23.2 ([#128110](https://github.com/kubernetes/kubernetes/pull/128110), [@haitch](https://github.com/haitch)) [SIG Release and Testing] - LoadBalancerIPMode feature is now marked as GA. ([#127348](https://github.com/kubernetes/kubernetes/pull/127348), [@RyanAoh](https://github.com/RyanAoh)) [SIG Apps, Network and Testing] - Output for the `ScalingReplicaSet` event has changed from: Scaled replica set to from to: Scaled replica set from to ([#125118](https://github.com/kubernetes/kubernetes/pull/125118), [@jsoref](https://github.com/jsoref)) [SIG Apps and CLI] - Promote the feature gates `StrictCostEnforcementForVAP` and `StrictCostEnforcementForWebhooks` to GA. ([#127302](https://github.com/kubernetes/kubernetes/pull/127302), [@cici37](https://github.com/cici37)) [SIG API Machinery and Testing] - Removed attachable volume limits from the capacity of the node for the following volume type when the kubelet is started, affecting the following volume types when the corresponding csi driver is installed: - `awsElasticBlockStore` for `ebs.csi.aws.com` - `azureDisk` for `disk.csi.azure.com` - `gcePersistentDisk` for `pd.csi.storage.googleapis.com` - `cinder` for `cinder.csi.openstack.org` - `csi` But it's still enforced using a limit in CSINode objects. ([#126924](https://github.com/kubernetes/kubernetes/pull/126924), [@carlory](https://github.com/carlory)) [SIG Storage] - Revert Go version used to build Kubernetes to 1.23.0 ([#127861](https://github.com/kubernetes/kubernetes/pull/127861), [@xmudrii](https://github.com/xmudrii)) [SIG Release and Testing] - The scheduler implements QueueingHint in VolumeBinding plugin's CSIDriver event, which enhances the throughput of scheduling. ([#125171](https://github.com/kubernetes/kubernetes/pull/125171), [@YamasouA](https://github.com/YamasouA)) [SIG Scheduling and Storage] - Vendor: updated system-validators to v1.9.0 ([#128149](https://github.com/kubernetes/kubernetes/pull/128149), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle and Node] ### Documentation - Kubeadm: fixed a misleading output (typo) when executing the "kubeadm init" command. ([#128118](https://github.com/kubernetes/kubernetes/pull/128118), [@amaddio](https://github.com/amaddio)) [SIG Cluster Lifecycle] ### Bug or Regression - Fix a bug where the kubelet ephemerally fails with `failed to initialize top level QOS containers: root container [kubepods] doesn't exist`, due to the cpuset cgroup being deleted on v2 with systemd cgroup manager. ([#125923](https://github.com/kubernetes/kubernetes/pull/125923), [@haircommander](https://github.com/haircommander)) [SIG Node and Testing] - Fix data race in kubelet/volumemanager ([#127919](https://github.com/kubernetes/kubernetes/pull/127919), [@carlory](https://github.com/carlory)) [SIG Apps, Node and Storage] - Fixes a race condition that could result in erroneous volume unmounts for flex volume plugins on kubelet restart ([#127669](https://github.com/kubernetes/kubernetes/pull/127669), [@olyazavr](https://github.com/olyazavr)) [SIG Storage] - Fixes a regression introduced in 1.29 where conntrack entries for UDP connections to deleted pods did not get cleaned up correctly, which could (among other things) cause DNS problems when DNS pods were restarted. ([#127780](https://github.com/kubernetes/kubernetes/pull/127780), [@danwinship](https://github.com/danwinship)) [SIG Network] - Node shutdown controller now makes a best effort to wait for CSI Drivers to complete the volume teardown process according to the pod priority groups. ([#125070](https://github.com/kubernetes/kubernetes/pull/125070), [@torredil](https://github.com/torredil)) [SIG Node, Storage and Testing] - Reduce memory usage/allocations during wait for volume attachment ([#126575](https://github.com/kubernetes/kubernetes/pull/126575), [@Lucaber](https://github.com/Lucaber)) [SIG Node and Storage] - Scheduler will start considering the resource requests of existing sidecar containers during the scoring process. ([#127878](https://github.com/kubernetes/kubernetes/pull/127878), [@AxeZhan](https://github.com/AxeZhan)) [SIG Scheduling and Testing] - The name port of the sidecar will also be allowed to be used ([#127976](https://github.com/kubernetes/kubernetes/pull/127976), [@chengjoey](https://github.com/chengjoey)) [SIG Network] - Unallowed label values will show up as "unexpected" in all system components metrics ([#128100](https://github.com/kubernetes/kubernetes/pull/128100), [@yongruilin](https://github.com/yongruilin)) [SIG Architecture and Instrumentation] ### Other (Cleanup or Flake) - CRI client: use default timeout for `ImageFsInfo` RPC ([#128052](https://github.com/kubernetes/kubernetes/pull/128052), [@saschagrunert](https://github.com/saschagrunert)) [SIG Node] - Fix spacing in --validate flag description in kubectl. ([#128081](https://github.com/kubernetes/kubernetes/pull/128081), [@soltysh](https://github.com/soltysh)) [SIG CLI] - Kube-apiserver ResourceQuotaConfiguration admission plugin subsection within `--admission-control-config-file` files are now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now cause an error. ([#128038](https://github.com/kubernetes/kubernetes/pull/128038), [@seans3](https://github.com/seans3)) [SIG API Machinery] - Kube-apiserver `--egress-selector-config-file` files are now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now cause an error. ([#128011](https://github.com/kubernetes/kubernetes/pull/128011), [@seans3](https://github.com/seans3)) [SIG API Machinery and Testing] - Kube-apiserver `--tracing-config-file` file is now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now cause an error. ([#128073](https://github.com/kubernetes/kubernetes/pull/128073), [@seans3](https://github.com/seans3)) [SIG API Machinery] - Kube-controller-manager `--leader-migration-config` files are now validated strictly (EnableStrict). Duplicate and unknown fields in the configuration will now cause an error. ([#128009](https://github.com/kubernetes/kubernetes/pull/128009), [@seans3](https://github.com/seans3)) [SIG API Machinery and Cloud Provider] - Kubeadm: increased the verbosity of API client dry-run actions during the subcommands "init", "join", "upgrade" and "reset". Allowed dry-run on 'kubeadm join' even if there is no existing cluster by utilizing a faked, in-memory cluster-info ConfigMap. ([#126776](https://github.com/kubernetes/kubernetes/pull/126776), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubectl: `-o` can now be used as a shortcut for `--output` in `kubectl explain --output plaintext-openapiv2` ([#127869](https://github.com/kubernetes/kubernetes/pull/127869), [@ak20102763](https://github.com/ak20102763)) [SIG CLI] - Removes the feature gate ComponentSLIs, which has been promoted to stable since 1.29. ([#127787](https://github.com/kubernetes/kubernetes/pull/127787), [@Jefftree](https://github.com/Jefftree)) [SIG Architecture and Instrumentation] - The getters for the field name and typeDescription of the Reflector struct were renamed. ([#128035](https://github.com/kubernetes/kubernetes/pull/128035), [@alexanderstephan](https://github.com/alexanderstephan)) [SIG API Machinery] - The members name and typeDescription of the Reflector struct are now exported to allow for better user extensibility. ([#127663](https://github.com/kubernetes/kubernetes/pull/127663), [@alexanderstephan](https://github.com/alexanderstephan)) [SIG API Machinery] - Upgrades functionality of `kubectl kustomize` as described at https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.4.2 and https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.5.0 ([#127965](https://github.com/kubernetes/kubernetes/pull/127965), [@koba1t](https://github.com/koba1t)) [SIG CLI] - `kubectl apply --server-side` now supports `--subresource` congruent to `kubelctl patch` ([#127634](https://github.com/kubernetes/kubernetes/pull/127634), [@deads2k](https://github.com/deads2k)) [SIG CLI and Testing] ## Dependencies ### Added - github.com/Microsoft/hnslib: [v0.0.7](https://github.com/Microsoft/hnslib/tree/v0.0.7) ### Changed - github.com/armon/circbuf: [bbbad09 → 5111143](https://github.com/armon/circbuf/compare/bbbad09...5111143) - github.com/docker/docker: [v27.1.1+incompatible → v26.1.4+incompatible](https://github.com/docker/docker/compare/v27.1.1...v26.1.4) - github.com/exponent-io/jsonpath: [d6023ce → 1de76d7](https://github.com/exponent-io/jsonpath/compare/d6023ce...1de76d7) - github.com/google/cel-go: [v0.20.1 → v0.21.0](https://github.com/google/cel-go/compare/v0.20.1...v0.21.0) - github.com/gregjones/httpcache: [9cad4c3 → 901d907](https://github.com/gregjones/httpcache/compare/9cad4c3...901d907) - github.com/jonboulle/clockwork: [v0.2.2 → v0.4.0](https://github.com/jonboulle/clockwork/compare/v0.2.2...v0.4.0) - github.com/moby/spdystream: [v0.4.0 → v0.5.0](https://github.com/moby/spdystream/compare/v0.4.0...v0.5.0) - github.com/moby/sys/mountinfo: [v0.7.1 → v0.7.2](https://github.com/moby/sys/compare/mountinfo/v0.7.1...mountinfo/v0.7.2) - github.com/mohae/deepcopy: [491d360 → c48cc78](https://github.com/mohae/deepcopy/compare/491d360...c48cc78) - github.com/opencontainers/runc: [v1.1.14 → v1.1.15](https://github.com/opencontainers/runc/compare/v1.1.14...v1.1.15) - github.com/stoewer/go-strcase: [v1.2.0 → v1.3.0](https://github.com/stoewer/go-strcase/compare/v1.2.0...v1.3.0) - github.com/urfave/cli: [v1.22.15 → v1.22.1](https://github.com/urfave/cli/compare/v1.22.15...v1.22.1) - github.com/xiang90/probing: [43a291a → a49e3df](https://github.com/xiang90/probing/compare/43a291a...a49e3df) - golang.org/x/crypto: v0.26.0 → v0.28.0 - golang.org/x/mod: v0.20.0 → v0.21.0 - golang.org/x/net: v0.28.0 → v0.30.0 - golang.org/x/oauth2: v0.21.0 → v0.23.0 - golang.org/x/sys: v0.23.0 → v0.26.0 - golang.org/x/term: v0.23.0 → v0.25.0 - golang.org/x/text: v0.17.0 → v0.19.0 - golang.org/x/time: v0.3.0 → v0.7.0 - golang.org/x/tools: v0.24.0 → v0.26.0 - k8s.io/system-validators: v1.8.0 → v1.9.0 - sigs.k8s.io/json: bc3834c → 9aa6b5e - sigs.k8s.io/kustomize/api: v0.17.2 → v0.18.0 - sigs.k8s.io/kustomize/cmd/config: v0.14.1 → v0.15.0 - sigs.k8s.io/kustomize/kustomize/v5: v5.4.2 → v5.5.0 - sigs.k8s.io/kustomize/kyaml: v0.17.1 → v0.18.1 ### Removed - github.com/Microsoft/cosesign1go: [v1.1.0](https://github.com/Microsoft/cosesign1go/tree/v1.1.0) - github.com/Microsoft/didx509go: [v0.0.3](https://github.com/Microsoft/didx509go/tree/v0.0.3) - github.com/Microsoft/hcsshim: [v0.12.6](https://github.com/Microsoft/hcsshim/tree/v0.12.6) - github.com/OneOfOne/xxhash: [v1.2.8](https://github.com/OneOfOne/xxhash/tree/v1.2.8) - github.com/agnivade/levenshtein: [v1.1.1](https://github.com/agnivade/levenshtein/tree/v1.1.1) - github.com/akavel/rsrc: [v0.10.2](https://github.com/akavel/rsrc/tree/v0.10.2) - github.com/chzyer/logex: [v1.1.10](https://github.com/chzyer/logex/tree/v1.1.10) - github.com/chzyer/test: [a1ea475](https://github.com/chzyer/test/tree/a1ea475) - github.com/containerd/cgroups/v3: [v3.0.3](https://github.com/containerd/cgroups/tree/v3.0.3) - github.com/containerd/containerd: [v1.7.20](https://github.com/containerd/containerd/tree/v1.7.20) - github.com/containerd/continuity: [v0.4.2](https://github.com/containerd/continuity/tree/v0.4.2) - github.com/containerd/fifo: [v1.1.0](https://github.com/containerd/fifo/tree/v1.1.0) - github.com/containerd/go-runc: [v1.0.0](https://github.com/containerd/go-runc/tree/v1.0.0) - github.com/containerd/protobuild: [v0.3.0](https://github.com/containerd/protobuild/tree/v0.3.0) - github.com/containerd/stargz-snapshotter/estargz: [v0.14.3](https://github.com/containerd/stargz-snapshotter/tree/estargz/v0.14.3) - github.com/decred/dcrd/dcrec/secp256k1/v4: [v4.2.0](https://github.com/decred/dcrd/tree/dcrec/secp256k1/v4/v4.2.0) - github.com/docker/cli: [v24.0.0+incompatible](https://github.com/docker/cli/tree/v24.0.0) - github.com/docker/distribution: [v2.8.2+incompatible](https://github.com/docker/distribution/tree/v2.8.2) - github.com/docker/docker-credential-helpers: [v0.7.0](https://github.com/docker/docker-credential-helpers/tree/v0.7.0) - github.com/docker/go-events: [e31b211](https://github.com/docker/go-events/tree/e31b211) - github.com/go-ini/ini: [v1.67.0](https://github.com/go-ini/ini/tree/v1.67.0) - github.com/gobwas/glob: [v0.2.3](https://github.com/gobwas/glob/tree/v0.2.3) - github.com/goccy/go-json: [v0.10.2](https://github.com/goccy/go-json/tree/v0.10.2) - github.com/google/go-containerregistry: [v0.20.1](https://github.com/google/go-containerregistry/tree/v0.20.1) - github.com/gorilla/mux: [v1.8.1](https://github.com/gorilla/mux/tree/v1.8.1) - github.com/josephspurrier/goversioninfo: [v1.4.0](https://github.com/josephspurrier/goversioninfo/tree/v1.4.0) - github.com/klauspost/compress: [v1.17.0](https://github.com/klauspost/compress/tree/v1.17.0) - github.com/lestrrat-go/backoff/v2: [v2.0.8](https://github.com/lestrrat-go/backoff/tree/v2.0.8) - github.com/lestrrat-go/blackmagic: [v1.0.2](https://github.com/lestrrat-go/blackmagic/tree/v1.0.2) - github.com/lestrrat-go/httpcc: [v1.0.1](https://github.com/lestrrat-go/httpcc/tree/v1.0.1) - github.com/lestrrat-go/iter: [v1.0.2](https://github.com/lestrrat-go/iter/tree/v1.0.2) - github.com/lestrrat-go/jwx: [v1.2.28](https://github.com/lestrrat-go/jwx/tree/v1.2.28) - github.com/lestrrat-go/option: [v1.0.1](https://github.com/lestrrat-go/option/tree/v1.0.1) - github.com/linuxkit/virtsock: [f8cee7d](https://github.com/linuxkit/virtsock/tree/f8cee7d) - github.com/mattn/go-shellwords: [v1.0.12](https://github.com/mattn/go-shellwords/tree/v1.0.12) - github.com/mitchellh/go-homedir: [v1.1.0](https://github.com/mitchellh/go-homedir/tree/v1.1.0) - github.com/moby/sys/sequential: [v0.5.0](https://github.com/moby/sys/tree/sequential/v0.5.0) - github.com/open-policy-agent/opa: [v0.67.1](https://github.com/open-policy-agent/opa/tree/v0.67.1) - github.com/pelletier/go-toml: [v1.9.5](https://github.com/pelletier/go-toml/tree/v1.9.5) - github.com/rcrowley/go-metrics: [10cdbea](https://github.com/rcrowley/go-metrics/tree/10cdbea) - github.com/tchap/go-patricia/v2: [v2.3.1](https://github.com/tchap/go-patricia/tree/v2.3.1) - github.com/vbatts/tar-split: [v0.11.3](https://github.com/vbatts/tar-split/tree/v0.11.3) - github.com/veraison/go-cose: [v1.2.0](https://github.com/veraison/go-cose/tree/v1.2.0) - github.com/xeipuuv/gojsonpointer: [02993c4](https://github.com/xeipuuv/gojsonpointer/tree/02993c4) - github.com/xeipuuv/gojsonreference: [bd5ef7b](https://github.com/xeipuuv/gojsonreference/tree/bd5ef7b) - github.com/yashtewari/glob-intersection: [v0.2.0](https://github.com/yashtewari/glob-intersection/tree/v0.2.0) - go.starlark.net: a134d8f - go.uber.org/mock: v0.4.0 - google.golang.org/grpc/cmd/protoc-gen-go-grpc: v1.5.1 # v1.32.0-alpha.1 ## Downloads for v1.32.0-alpha.1 ### Source Code filename | sha512 hash -------- | ----------- [kubernetes.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes.tar.gz) | 86532c5440a87a6f6f0581cdddfdc68ea3f3f13a6478093518d8445c5ade8c448248de3f2102f29dc327f2055805a573cb60c36d7cce93605ed58b8b2ab23a5c [kubernetes-src.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-src.tar.gz) | 9cdce49ad47d92b14d88fbe0acdf67cce94dfd57f21d2a048ed46b370ff32f3b852ebbd1dfc646126cf30d20927d8e707500128c2ff193810ba7d7b68f612e94 ### Client Binaries filename | sha512 hash -------- | ----------- [kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-darwin-amd64.tar.gz) | 742727920beab9ac9285ea98238be4e7a9099205ca95a52c930f2ebff2ded5617b13d5c861c4579c2316b3cb8398959ecb66c72f061724df6079d491c0f4fa5a [kubernetes-client-darwin-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-darwin-arm64.tar.gz) | 7bd4af634ccbf510d83a3468f288a3d91abf20146fd54e558324cb0dcaaa722a9e07f544699c2c73f033a5cf812cdfd9b8b36e3c612c0148792e1f8370a5d33e [kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-linux-386.tar.gz) | 39d34eca859b53fda63bda7df3ed45ba5e7e6cf406895d454da0291c6dd403139b4bfc46584595ddabaee890511df76d71252ebc1e1dda42f0ba941cec296cd9 [kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-linux-amd64.tar.gz) | f71a38447431dc7289caed55fd4846a4990247e4996c22b7c98aa9304959a5e25bf5aeb117d443481c411e6cc497051d8c75bde1ef3a7cb4ab8ff6f2abe43a39 [kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-linux-arm.tar.gz) | 21b75e8d69e98842704b2d1e468bbdaa62031d8570d35398095e6b7c96825af0276f668064722d6043788e7f2b8b0d093bbaed8fa93126f3e2d8720bc3fecf9b [kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-linux-arm64.tar.gz) | 498fc9962c02c60823832207f85ce919bb0c405b73feb931a7186babd644c928cee377c4ae0286f3e981328995d96586e4ae4783e38b879eb3caab8f9c9d0a5b [kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-linux-ppc64le.tar.gz) | 9bed5cf8bb05dc529f9ac7a637a657e1312065a2ee39c1d809f926b542547b8ddc674addae84cb523569a8a5a7f183a598b2d0566d9e58317bccd61558ca7192 [kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-linux-s390x.tar.gz) | 6c5aa276aa65d969826ad49d901bc95fb7290cd00778c03f681ccdc12f3dc7cd77752e2895400250875a3c0a7548e20fe6f958bace1482f9a9b88c8581c10d95 [kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-windows-386.tar.gz) | 5d45f1c1e0e984fa85ed99ac58dda6c475c3a2120a911425272187fde03b8017cdb14d71b2d6d9a23c946166fd2c374c42ffa32186c74546d7ea0146271cd50f [kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-windows-amd64.tar.gz) | f0e3b6e845053c753640a46c3258eec96b04e7c95f044e8b980300ad32dadab2f0fef735213ba3de9b98dca2d7106a7f51e0f08c28a75cbe89f5a9f36f7e29a4 [kubernetes-client-windows-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-client-windows-arm64.tar.gz) | 1a86995fc7284db06c23af66d82d836be36a6efcba7e2ef296c14bff56d39392a444cb399ce1f999181ec1ff7ac3edfdff84c3ccb63b0c6564550a8c0c948cef ### Server Binaries filename | sha512 hash -------- | ----------- [kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-server-linux-amd64.tar.gz) | dd0cfd5d57ad9c82ea52c98c80df8fe63a349bfbb16e42b30b1fe4c3b765327250397438e75e49014e6afffbaa7514daf830b8f7c781362241fb527196d8dc86 [kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-server-linux-arm64.tar.gz) | dbd29ab7bdfe97b8f9261cf3e727065f301bced78c866ead01d932de92e26476d3824c8f1023a8ebc63a63a3a79001dd2493c0f70118580841922b59ab1632c1 [kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-server-linux-ppc64le.tar.gz) | f37b92ed3ef9eeb3c40973068ef6131441abd6f4eabf1f1b4845f5774f116efbdf7d73f870f5268137d0ff4f406f443522f8adf63a043aaedcb67672246f0b55 [kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-server-linux-s390x.tar.gz) | 58531d380dc3ddbff5b8e6e3cef8cc58f6c47aea0b4a3c907805836e35f571dc1e231e4dbbf635115bb70357408cf23ad68a86dd725a5abbe5025b2945cf1ddf ### Node Binaries filename | sha512 hash -------- | ----------- [kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-node-linux-amd64.tar.gz) | 4273a6fc9fec18f408c0e559d3680270572250fc3d4c997439dfe844dca138a1a7277852882184601c4960a52525a6594b274f251bcca78df02104d296302e12 [kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-node-linux-arm64.tar.gz) | 931eea6e9e6809a13a28519b03022bda056ac6215cd2b1bcd4186efa8204bc1b9245c3893292ad0ba823dc9cf008afd82dc4988cee2ea09eef3d5bb073945b1d [kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-node-linux-ppc64le.tar.gz) | a35ed30cafb4aebb541d6a7a8d1995e773877cdda3e8b413a81eddc1eeb989b086765c6396df3d1d1dde86fb62ae7684401aa6dcedfcbe6940ada470549fe6e6 [kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-node-linux-s390x.tar.gz) | cc9b57d9fa7561d015288789cf7949dc7a68d4e6f006aa5b354941e736490b92480bd65f36090c53ddacde00f5a6a34b7a7a2b8c4912dfed3ec36e4c37759e9f [kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.32.0-alpha.1/kubernetes-node-windows-amd64.tar.gz) | be118da99917ca00cff3f5ba9bb1a747c112c26522c4cc695d6cd2b2badfdf2ebcf79cb8885dbcf9986fc392510ec8a6c746cdf4ea7c984ed86a49f206ba68c2 ### 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.32.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.32.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.32.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.32.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.32.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.32.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.31.0 ## Changes by Kind ### Deprecation - Reverted the `DisableNodeKubeProxyVersion` feature gate to default-off to give a full year from deprecation announcement in 1.29 to clearing the field by default, per the [Kubernetes deprecation policy](https://kubernetes.io/docs/reference/using-api/deprecation-policy/). ([#126720](https://github.com/kubernetes/kubernetes/pull/126720), [@liggitt](https://github.com/liggitt)) [SIG Architecture and Node] ### API Change - **ACTION REQUIRED** for custom scheduler plugin developers: - `PodEligibleToPreemptOthers` in the `preemption` interface gets `ctx` in the parameters. Please change your plugins' implementation accordingly. ([#126465](https://github.com/kubernetes/kubernetes/pull/126465), [@googs1025](https://github.com/googs1025)) [SIG Scheduling] - Changed NodeToStatusMap from map to struct and exposed methods to access the entries. Added absentNodesStatus, which inform what is the status of nodes that are absent in the map. - For developers of out-of-tree PostFilter plugins, make sure to update usage of NodeToStatusMap. Additionally, NodeToStatusMap should be eventually renamed to NodeToStatusReader. ([#126022](https://github.com/kubernetes/kubernetes/pull/126022), [@macsko](https://github.com/macsko)) [SIG Node, Scheduling and Testing] - Allow for Pod search domains to be a single dot "." or contain an underscore "_" ([#127167](https://github.com/kubernetes/kubernetes/pull/127167), [@adrianmoisey](https://github.com/adrianmoisey)) [SIG Apps, Network and Testing] - Disallow `k8s.io` and `kubernetes.io` namespaced extra key in structured authentication configuration. ([#126553](https://github.com/kubernetes/kubernetes/pull/126553), [@aramase](https://github.com/aramase)) [SIG Auth] - Fix the bug where spec.terminationGracePeriodSeconds of the pod will always be overwritten by the MaxPodGracePeriodSeconds of the soft eviction, you can enable the `AllowOverwriteTerminationGracePeriodSeconds` feature gate, which will restore the previous behavior. If you do need to set this, please file an issue with the Kubernetes project to help contributors understand why you need it. ([#122890](https://github.com/kubernetes/kubernetes/pull/122890), [@HirazawaUi](https://github.com/HirazawaUi)) [SIG API Machinery, Architecture, Node and Testing] - Kube-scheduler removed the following plugins: - AzureDiskLimits - CinderLimits - EBSLimits - GCEPDLimits Because the corresponding CSI driver reports how many volumes a node can handle in NodeGetInfoResponse, the kubelet stores this limit in CSINode and the scheduler then knows the driver's limit on the node. Remove plugins AzureDiskLimits, CinderLimits, EBSLimits and GCEPDLimits if you explicitly enabled them in the scheduler config. ([#124003](https://github.com/kubernetes/kubernetes/pull/124003), [@carlory](https://github.com/carlory)) [SIG Scheduling, Storage and Testing] - Promoted `CustomResourceFieldSelectors` to stable; the feature is enabled by default. `--feature-gates=CustomResourceFieldSelectors=true` not needed on kube-apiserver binaries and will be removed in a future release. ([#127673](https://github.com/kubernetes/kubernetes/pull/127673), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery and Testing] - The default value for node-monitor-grace-period has been increased to 50s (earlier 40s) (Ref - https://github.com/kubernetes/kubernetes/issues/121793) ([#126287](https://github.com/kubernetes/kubernetes/pull/126287), [@devppratik](https://github.com/devppratik)) [SIG API Machinery, Apps and Node] - The resource/v1alpha3.ResourceSliceList filed which should have been named "metadata" but was instead named "listMeta" is now properly "metadata". ([#126749](https://github.com/kubernetes/kubernetes/pull/126749), [@thockin](https://github.com/thockin)) [SIG API Machinery] - The synthetic "Bookmark" event for the watch stream requests will now include a new annotation: `kubernetes.io/initial-events-list-blueprint`. THe annotation contains an empty, versioned list that is encoded in the requested format (such as protobuf, JSON, or CBOR), then base64-encoded and stored as a string. ([#127587](https://github.com/kubernetes/kubernetes/pull/127587), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery] - To enhance usability and developer experience, CRD validation rules now support direct use of (CEL) reserved keywords as field names in object validation expressions. Name format CEL library is supported in new expressions. ([#126977](https://github.com/kubernetes/kubernetes/pull/126977), [@aaron-prindle](https://github.com/aaron-prindle)) [SIG API Machinery, Architecture, Auth, Etcd, Instrumentation, Release, Scheduling and Testing] - Updated incorrect description of persistentVolumeClaimRetentionPolicy ([#126545](https://github.com/kubernetes/kubernetes/pull/126545), [@yangjunmyfm192085](https://github.com/yangjunmyfm192085)) [SIG API Machinery, Apps and CLI] - X.509 client certificate authentication to kube-apiserver now produces credential IDs (derived from the certificate's signature) for use by audit logging. ([#125634](https://github.com/kubernetes/kubernetes/pull/125634), [@ahmedtd](https://github.com/ahmedtd)) [SIG API Machinery, Auth and Testing] ### Feature - Added new functionality into the Go client code (`client-go`) library. The `List()` method for the metadata client allows enabling API streaming when fetching collections; this improves performance when listing many objects. To request this behaviour, your client software must enable the `WatchListClient` client-go feature gate. Additionally, streaming is only available if supported by the cluster; the API server that you connect to must also support streaming. If the API server does not support or allow streaming, then `client-go` falls back to fetching the collection using the **list** API verb. ([#127388](https://github.com/kubernetes/kubernetes/pull/127388), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery and Testing] - Added preemptionPolicy field when using `kubectl get PriorityClass -owide` ([#126529](https://github.com/kubernetes/kubernetes/pull/126529), [@googs1025](https://github.com/googs1025)) [SIG CLI] - Client-go/rest: contextual logging of request/response with accurate source code location of the caller ([#126999](https://github.com/kubernetes/kubernetes/pull/126999), [@pohly](https://github.com/pohly)) [SIG API Machinery and Instrumentation] - Enabled kube-controller-manager '--concurrent-job-syncs' flag works on orphan Pod processors ([#126567](https://github.com/kubernetes/kubernetes/pull/126567), [@fusida](https://github.com/fusida)) [SIG Apps] - Extend discovery GroupManager with Group lister interface ([#127524](https://github.com/kubernetes/kubernetes/pull/127524), [@mjudeikis](https://github.com/mjudeikis)) [SIG API Machinery] - Fix kubectl doesn't print image volume when kubectl describe a pod with that volume ([#126706](https://github.com/kubernetes/kubernetes/pull/126706), [@carlory](https://github.com/carlory)) [SIG CLI] - Graduate the AnonymousAuthConfigurableEndpoints feature gate to beta and enable by default to allow configurable endpoints for anonymous authentication. ([#127009](https://github.com/kubernetes/kubernetes/pull/127009), [@vinayakankugoyal](https://github.com/vinayakankugoyal)) [SIG Auth] - Implement a queueing hint for PersistentVolumeClaim/Add event in CSILimit plugin. ([#124703](https://github.com/kubernetes/kubernetes/pull/124703), [@utam0k](https://github.com/utam0k)) [SIG Scheduling and Storage] - Implement new cluster events UpdatePodSchedulingGatesEliminated and UpdatePodTolerations for scheduler plugins. ([#127083](https://github.com/kubernetes/kubernetes/pull/127083), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Improve Node QueueHint in the NodeAffinty plugin by ignoring unrelated changes that keep pods unschedulable. ([#127444](https://github.com/kubernetes/kubernetes/pull/127444), [@dom4ha](https://github.com/dom4ha)) [SIG Scheduling and Testing] - Improve Node QueueHint in the NodeResource Fit plugin by ignoring unrelated changes that keep pods unschedulable. ([#127473](https://github.com/kubernetes/kubernetes/pull/127473), [@dom4ha](https://github.com/dom4ha)) [SIG Scheduling and Testing] - Improve performance of the job controller when handling job delete events. ([#127378](https://github.com/kubernetes/kubernetes/pull/127378), [@hakuna-matatah](https://github.com/hakuna-matatah)) [SIG Apps] - Improve performance of the job controller when handling job update events. ([#127228](https://github.com/kubernetes/kubernetes/pull/127228), [@hakuna-matatah](https://github.com/hakuna-matatah)) [SIG Apps] - JWT authenticators now set the `jti` claim (if present and is a string value) as credential id for use by audit logging. ([#127010](https://github.com/kubernetes/kubernetes/pull/127010), [@aramase](https://github.com/aramase)) [SIG API Machinery, Auth and Testing] - Kube-apiserver: a new `--requestheader-uid-headers` flag allows configuring request header authentication to obtain the authenticating user's UID from the specified headers. The suggested value for the new option is `X-Remote-Uid`. When specified, the `kube-system/extension-apiserver-authentication` configmap will include the value in its `.data[requestheader-uid-headers]` field. ([#115834](https://github.com/kubernetes/kubernetes/pull/115834), [@stlaz](https://github.com/stlaz)) [SIG API Machinery, Auth, Cloud Provider and Testing] - Kube-proxy uses field-selector clusterIP!=None on Services to avoid watching for Headless Services, reduce unnecessary network bandwidth ([#126769](https://github.com/kubernetes/kubernetes/pull/126769), [@Sakuralbj](https://github.com/Sakuralbj)) [SIG Network] - Kubeadm: `kubeadm upgrade apply` now supports phase sub-command, user can use `kubeadm upgrade apply phase ` to execute the specified phase, or use `kubeadm upgrade apply --skip-phases ` to skip some phases during cluster upgrade. ([#126032](https://github.com/kubernetes/kubernetes/pull/126032), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - Kubeadm: `kubeadm upgrade node` now supports `addon` and `post-upgrade` phases. User can use `kubeadm upgrade node phase addon` to execute the addon upgrade, or use `kubeadm upgrade node --skip-phases addon` to skip the addon upgrade. Currently, the `post-upgrade` phase is no-op, and it is mainly used to handle some release specific post-upgrade tasks. ([#127242](https://github.com/kubernetes/kubernetes/pull/127242), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - Kubeadm: add a validation warning when the certificateValidityPeriod is more than the caCertificateValidityPeriod ([#126538](https://github.com/kubernetes/kubernetes/pull/126538), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - Kubeadm: allow mixing the flag --config with the special flag --print-manifest of the subphases of 'kubeadm init phase addon'. ([#126740](https://github.com/kubernetes/kubernetes/pull/126740), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: if an unknown command name is passed to any parent command such as 'kubeadm init phase' return an error. If 'kubeadm init phase' or another command that has subcommands is called without subcommand name, print the available commands and also return an error. ([#127096](https://github.com/kubernetes/kubernetes/pull/127096), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: promoted feature gate `EtcdLearnerMode` to GA. Learner mode in etcd deployed by kubeadm is now locked to enabled by default. ([#126374](https://github.com/kubernetes/kubernetes/pull/126374), [@pacoxu](https://github.com/pacoxu)) [SIG Cluster Lifecycle] - Kubelet: add log and event for cgroup v2 with kernel older than 5.8. ([#126595](https://github.com/kubernetes/kubernetes/pull/126595), [@pacoxu](https://github.com/pacoxu)) [SIG Node] - Kubernetes is now built with go 1.23.0 ([#127076](https://github.com/kubernetes/kubernetes/pull/127076), [@cpanato](https://github.com/cpanato)) [SIG Release and Testing] - Promoted `RetryGenerateName` to stable; the feature is enabled by default. `--feature-gates=RetryGenerateName=true` not needed on kube-apiserver binaries and will be removed in a future release. ([#127093](https://github.com/kubernetes/kubernetes/pull/127093), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery] - Support inflight_events metric in the scheduler for QueueingHint (alpha feature). ([#127052](https://github.com/kubernetes/kubernetes/pull/127052), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Support specifying a custom network parameter when running e2e-node-tests with the remote option. ([#127574](https://github.com/kubernetes/kubernetes/pull/127574), [@bouaouda-achraf](https://github.com/bouaouda-achraf)) [SIG Node and Testing] - The scheduler retries gated Pods more appropriately, giving them a backoff penalty too. ([#126029](https://github.com/kubernetes/kubernetes/pull/126029), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Transformation_operations_total metric will have additional resource label which can be used for resource specific validations for example handling of encryption config by the apiserver. ([#126512](https://github.com/kubernetes/kubernetes/pull/126512), [@kmala](https://github.com/kmala)) [SIG API Machinery, Auth, Etcd and Testing] - Unallowed label values will show up as "unexpected" in scheduler metrics ([#126762](https://github.com/kubernetes/kubernetes/pull/126762), [@richabanker](https://github.com/richabanker)) [SIG Instrumentation and Scheduling] - When SchedulerQueueingHint is enabled, the scheduler's in-tree plugins now subscribe to specific node events to decide whether to requeue Pods. This allows the scheduler to handle cluster events faster with less memory. Specific node events include updates to taints, tolerations or allocatable. In-tree plugins now ignore node updates that don't modify any of these fields. ([#127220](https://github.com/kubernetes/kubernetes/pull/127220), [@sanposhiho](https://github.com/sanposhiho)) [SIG Node, Scheduling and Storage] - When SchedulerQueueingHints is enabled, clear events cached in the scheduling queue as soon as possible so that the scheduler consumes less memory. ([#120586](https://github.com/kubernetes/kubernetes/pull/120586), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] ### Documentation - Clarified the kube-controller-manager documentation for --allocate-node-cidrs, --cluster-cidr, and --service-cluster-ip-range flags to accurately reflect their dependencies and usage conditions. ([#126784](https://github.com/kubernetes/kubernetes/pull/126784), [@eminwux](https://github.com/eminwux)) [SIG API Machinery, Cloud Provider and Docs] - Documented the `--for=create` option to `kubectl wait` ([#127327](https://github.com/kubernetes/kubernetes/pull/127327), [@ryanwinter](https://github.com/ryanwinter)) [SIG CLI] ### Failing Test - Kubelet Plugins are now re-registered properly on Windows if the re-registration period is < 15ms. ([#114136](https://github.com/kubernetes/kubernetes/pull/114136), [@claudiubelu](https://github.com/claudiubelu)) [SIG Node, Storage, Testing and Windows] ### Bug or Regression - API emulation versioning honors cohabitating resources ([#127239](https://github.com/kubernetes/kubernetes/pull/127239), [@xuzhenglun](https://github.com/xuzhenglun)) [SIG API Machinery] - Apiserver repair controller is resilient to etcd errors during bootstrap and retries during 30 seconds before failing. ([#126671](https://github.com/kubernetes/kubernetes/pull/126671), [@fusida](https://github.com/fusida)) [SIG Network] - Applyconfiguration-gen no longer generates duplicate methods and ambiguous member accesses when types end up with multiple members of the same name (through embedded structs). ([#127001](https://github.com/kubernetes/kubernetes/pull/127001), [@skitt](https://github.com/skitt)) [SIG API Machinery] - DRA: when a DRA driver was started after creating pods which need resources from that driver, no additional attempt was made to schedule such unschedulable pods again. Only affected DRA with structured parameters. ([#126807](https://github.com/kubernetes/kubernetes/pull/126807), [@pohly](https://github.com/pohly)) [SIG Node, Scheduling and Testing] - DRA: when enabling the scheduler queuing hint feature, pods got stuck as unschedulable for a while unnecessarily because recording the name of the generated ResourceClaim did not trigger scheduling. ([#127497](https://github.com/kubernetes/kubernetes/pull/127497), [@pohly](https://github.com/pohly)) [SIG Auth, Node, Scheduling and Testing] - Discarded the output streams of destination path check in kubectl cp when copying from local to pod and added a 3 seconds timeout to this check ([#126652](https://github.com/kubernetes/kubernetes/pull/126652), [@ardaguclu](https://github.com/ardaguclu)) [SIG CLI] - Fix CEL estimated cost of expressions that perform equality checks of IPs, CIDRs, Quantities, Formats and URLs. ([#126359](https://github.com/kubernetes/kubernetes/pull/126359), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery] - Fix a bug on the endpoints controller that does not reconcile the Endpoint object after this is truncated (it gets more than 1000 endpoints addresses) ([#127417](https://github.com/kubernetes/kubernetes/pull/127417), [@aojea](https://github.com/aojea)) [SIG Apps, Network and Testing] - Fix a bug when the hostname label of a node does not match the node name, pods bound to a PV with nodeAffinity using the hostname may be scheduled to the wrong node or experience scheduling failures. ([#125398](https://github.com/kubernetes/kubernetes/pull/125398), [@AxeZhan](https://github.com/AxeZhan)) [SIG Scheduling and Storage] - Fix a bug with dual stack clusters using the beta feature MultiCIDRServiceAllocator can not create dual stack Services or Services with IPs on the secondary range. User that want to use this feature in 1.30 with dual stack clusters can workaround the issue by setting the feature gate DisableAllocatorDualWrite to true ([#127598](https://github.com/kubernetes/kubernetes/pull/127598), [@aojea](https://github.com/aojea)) [SIG Network and Testing] - Fix a potential memory leak in QueueingHint (alpha feature) ([#127016](https://github.com/kubernetes/kubernetes/pull/127016), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Fix a scheduler preemption issue where the victim pod was not deleted due to incorrect status patching. This issue occurred when the preemptor and victim pods had different QoS classes in their status, causing the preemption to fail entirely. ([#126644](https://github.com/kubernetes/kubernetes/pull/126644), [@Huang-Wei](https://github.com/Huang-Wei)) [SIG Scheduling] - Fix fake client to accept request without metadata.name to better emulate behavior of actual client. ([#126727](https://github.com/kubernetes/kubernetes/pull/126727), [@jpbetz](https://github.com/jpbetz)) [SIG API Machinery] - Fix race condition in kube-proxy initialization that could blackhole UDP traffic to service VIP. ([#126532](https://github.com/kubernetes/kubernetes/pull/126532), [@wedaly](https://github.com/wedaly)) [SIG Network] - Fix the wrong hierarchical structure for the child span and the parent span (i.e. `SerializeObject` and `List`). In the past, some children's spans appeared parallel to their parents. ([#127551](https://github.com/kubernetes/kubernetes/pull/127551), [@carlory](https://github.com/carlory)) [SIG API Machinery and Instrumentation] - Fixed a bug where init containers may fail to start due to a temporary container runtime failure. ([#126543](https://github.com/kubernetes/kubernetes/pull/126543), [@gjkim42](https://github.com/gjkim42)) [SIG Node] - Fixed a bug which the scheduler didn't correctly tell plugins Node deletion. This bug could impact all scheduler plugins subscribing to Node/Delete event, making the queue keep the Pods rejected by those plugins incorrectly at Node deletion. Among the in-tree plugins, PodTopologySpread is the only victim. ([#127464](https://github.com/kubernetes/kubernetes/pull/127464), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling and Testing] - Fixed a possible memory leak for QueueingHint (alpha feature) ([#126962](https://github.com/kubernetes/kubernetes/pull/126962), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Fixed a regression in 1.29+ default configurations, where regular init containers may fail to start due to a temporary container runtime failure. ([#127162](https://github.com/kubernetes/kubernetes/pull/127162), [@gjkim42](https://github.com/gjkim42)) [SIG Node] - Fixed an issue where requests sent by the KMSv2 service would be rejected due to having an invalid authority header. ([#126930](https://github.com/kubernetes/kubernetes/pull/126930), [@Ruddickmg](https://github.com/Ruddickmg)) [SIG API Machinery and Auth] - Fixed: dynamic client-go can now handle subresources with an UnstructuredList response ([#126809](https://github.com/kubernetes/kubernetes/pull/126809), [@ryantxu](https://github.com/ryantxu)) [SIG API Machinery] - Fixes a bug in the garbage collector controller which could block indefinitely on a cache sync failure. This fix allows the garbage collector to eventually continue garbage collecting other resources if a given resource cannot be listed or watched. Any objects in the unsynced resource type with owner references with `blockOwnerDeletion: true` will not be known to the garbage collector. Use of `blockOwnerDeletion` has always been best-effort and racy on startup and object creation, with this fix, it continues to be best-effort for resources that cannot be synced by the garbage collector controller. ([#125796](https://github.com/kubernetes/kubernetes/pull/125796), [@haorenfsa](https://github.com/haorenfsa)) [SIG API Machinery, Apps and Testing] - Fixes a bug where restartable and non-restartable init containers were not accounted for in the message and annotations of eviction event. ([#124947](https://github.com/kubernetes/kubernetes/pull/124947), [@toVersus](https://github.com/toVersus)) [SIG Node] - Fixes the ability to set the `resolvConf` option in drop-in kubelet configuration files, validates that drop-in kubelet configuration files are in a supported version. ([#127421](https://github.com/kubernetes/kubernetes/pull/127421), [@liggitt](https://github.com/liggitt)) [SIG Node] - Fixes the bug in NodeUnschedulable that only happens with QHint enabled, which the scheduler might miss some updates for the Pods rejected by NodeUnschedulable plugin and put the Pods in the queue for a longer time than needed. ([#127427](https://github.com/kubernetes/kubernetes/pull/127427), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - Fixes the bug in PodTopologySpread that only happens with QHint enabled, which the scheduler might miss some updates for the Pods rejected by PodTopologySpread plugin and put the Pods in the queue for a longer time than needed. ([#127447](https://github.com/kubernetes/kubernetes/pull/127447), [@sanposhiho](https://github.com/sanposhiho)) [SIG Scheduling] - HostNetwork pods no longer depend on the PodIPs to be assigned to configure the defined hostAliases on the Pod ([#126460](https://github.com/kubernetes/kubernetes/pull/126460), [@aojea](https://github.com/aojea)) [SIG Network, Node and Testing] - If a client makes an API streaming requests and specifies an `application/json;as=Table` content type, the API server now responds with a 406 (Not Acceptable) error. This change helps to ensure that unsupported formats, such as `Table` representations are correctly rejected. ([#126996](https://github.com/kubernetes/kubernetes/pull/126996), [@p0lyn0mial](https://github.com/p0lyn0mial)) [SIG API Machinery and Testing] - If an old pod spec has used image volume source, we must allow it when updating the resource even if the feature-gate ImageVolume is disabled. ([#126733](https://github.com/kubernetes/kubernetes/pull/126733), [@carlory](https://github.com/carlory)) [SIG API Machinery, Apps and Node] - Improve PVC Protection Controller's scalability by batch-processing PVCs by namespace with lazy live pod listing. ([#125372](https://github.com/kubernetes/kubernetes/pull/125372), [@hungnguyen243](https://github.com/hungnguyen243)) [SIG Apps, Node, Storage and Testing] - Improve PVC Protection Controller's scalability by batch-processing PVCs by namespace with lazy live pod listing. ([#126745](https://github.com/kubernetes/kubernetes/pull/126745), [@hungnguyen243](https://github.com/hungnguyen243)) [SIG Apps, Storage and Testing] - Kube-apiserver: Fixes a 1.31 regression that stopped honoring build ID overrides with the --version flag ([#126665](https://github.com/kubernetes/kubernetes/pull/126665), [@liggitt](https://github.com/liggitt)) [SIG API Machinery] - Kubeadm: ensure that Pods from the upgrade preflight check `CreateJob` are properly terminated after a timeout. ([#127333](https://github.com/kubernetes/kubernetes/pull/127333), [@yuyabee](https://github.com/yuyabee)) [SIG Cluster Lifecycle] - Kubeadm: when adding new control plane nodes with "kubeadm join", ensure that the etcd member addition is performed only if a given member URL does not already exist in the list of members. Similarly, on "kubeadm reset" only remove an etcd member if its ID exists. ([#127491](https://github.com/kubernetes/kubernetes/pull/127491), [@SataQiu](https://github.com/SataQiu)) [SIG Cluster Lifecycle] - Kubelet now attempts to get an existing node if the request to create it fails with StatusForbidden. ([#126318](https://github.com/kubernetes/kubernetes/pull/126318), [@hoskeri](https://github.com/hoskeri)) [SIG Node] - Kubelet: use the CRI stats provider if `PodAndContainerStatsFromCRI` feature is enabled ([#126488](https://github.com/kubernetes/kubernetes/pull/126488), [@haircommander](https://github.com/haircommander)) [SIG Node] - Removed unneeded permissions for system:controller:persistent-volume-binder and system:controller:expand-controller clusterroles ([#125995](https://github.com/kubernetes/kubernetes/pull/125995), [@carlory](https://github.com/carlory)) [SIG Auth and Storage] - Revert "fix: handle socket file detection on Windows" ([#126976](https://github.com/kubernetes/kubernetes/pull/126976), [@jsturtevant](https://github.com/jsturtevant)) [SIG Node] - Send an error on `ResultChan` and close the `RetryWatcher` when the client is forbidden or unauthorized from watching the resource. ([#126038](https://github.com/kubernetes/kubernetes/pull/126038), [@mprahl](https://github.com/mprahl)) [SIG API Machinery] - Send bookmark right now after sending all items in watchCache store ([#127012](https://github.com/kubernetes/kubernetes/pull/127012), [@Chaunceyctx](https://github.com/Chaunceyctx)) [SIG API Machinery] - Terminated Pods on a node will not be re-admitted on kubelet restart. This fixes the problem of Completed Pods awaiting for the finalizer marked as Failed after the kubelet restart. ([#126343](https://github.com/kubernetes/kubernetes/pull/126343), [@SergeyKanzhelev](https://github.com/SergeyKanzhelev)) [SIG Node and Testing] - The CSI volume plugin stopped watching the VolumeAttachment object if the object is not found or the volume is not attached when kubelet waits for a volume attached. In the past, it would fail due to missing permission. ([#126961](https://github.com/kubernetes/kubernetes/pull/126961), [@carlory](https://github.com/carlory)) [SIG Storage] - The Usage and VolumeCondition are both optional in the response and if CSIVolumeHealth feature gate is enabled kubelet needs to consider returning metrics if either one is set. ([#127021](https://github.com/kubernetes/kubernetes/pull/127021), [@Madhu-1](https://github.com/Madhu-1)) [SIG Storage] - Upgrade coreDNS to v1.11.3 ([#126449](https://github.com/kubernetes/kubernetes/pull/126449), [@BenTheElder](https://github.com/BenTheElder)) [SIG Cloud Provider and Cluster Lifecycle] - Use allocatedResources on PVC for node expansion in kubelet ([#126600](https://github.com/kubernetes/kubernetes/pull/126600), [@gnufied](https://github.com/gnufied)) [SIG Node, Storage and Testing] - When entering a value other than "external" to the "--cloud-provider" flag for the kubelet, kube-controller-manager, and kube-apiserver, the user will now receive a warning in the logs about the disablement of internal cloud providers, this is in contrast to the previous warnings about deprecation. ([#127711](https://github.com/kubernetes/kubernetes/pull/127711), [@elmiko](https://github.com/elmiko)) [SIG API Machinery, Cloud Provider and Node] ### Other (Cleanup or Flake) - Added an example for kubectl delete with the --interactive flag. ([#127512](https://github.com/kubernetes/kubernetes/pull/127512), [@bergerhoffer](https://github.com/bergerhoffer)) [SIG CLI] - Aggregated Discovery v2beta1 fixture is removed in `./api/discovery`. Please use v2 ([#127008](https://github.com/kubernetes/kubernetes/pull/127008), [@Jefftree](https://github.com/Jefftree)) [SIG API Machinery] - Device manager: stop using annotations to pass CDI device info to runtimes. Containerd versions older than v1.7.2 don't support passing CDI info through CRI and need to be upgraded. ([#126435](https://github.com/kubernetes/kubernetes/pull/126435), [@bart0sh](https://github.com/bart0sh)) [SIG Node] - Feature gate "AllowServiceLBStatusOnNonLB" has been removed. This gate has been stable and unchanged for over a year. ([#126786](https://github.com/kubernetes/kubernetes/pull/126786), [@thockin](https://github.com/thockin)) [SIG Apps] - Fix a warning message about the gce in-tree cloud provider state ([#126773](https://github.com/kubernetes/kubernetes/pull/126773), [@carlory](https://github.com/carlory)) [SIG Cloud Provider] - Kube-proxy initialization waits for all pre-sync events from node and serviceCIDR informers to be delivered. ([#126561](https://github.com/kubernetes/kubernetes/pull/126561), [@wedaly](https://github.com/wedaly)) [SIG Network] - Kube-proxy will no longer depend on conntrack binary for stale UDP connections cleanup ([#126847](https://github.com/kubernetes/kubernetes/pull/126847), [@aroradaman](https://github.com/aroradaman)) [SIG Cluster Lifecycle, Network and Testing] - Kubeadm: don't warn if `crictl` binary does not exist since kubeadm does not rely on `crictl` since v1.31. ([#126596](https://github.com/kubernetes/kubernetes/pull/126596), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cluster Lifecycle] - Kubeadm: make sure the extra environment variables written to a kubeadm managed PodSpec are sorted alpha-numerically by the environment variable name. ([#126743](https://github.com/kubernetes/kubernetes/pull/126743), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: remove the deprecated sub-phase of 'init kubelet-finilize' called `experimental-cert-rotation`, and use 'enable-client-cert-rotation' instead. ([#126913](https://github.com/kubernetes/kubernetes/pull/126913), [@pacoxu](https://github.com/pacoxu)) [SIG Cluster Lifecycle] - Kubeadm: removed `socat` and `ebtables` from kubeadm preflight checks ([#127151](https://github.com/kubernetes/kubernetes/pull/127151), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cluster Lifecycle] - Kubeadm: removed the deprecated and NO-OP flags `--features-gates` for `kubeadm upgrde apply` and `--api-server-manfiest`, `--controller-manager-manfiest` and `--scheduler-manifest` for `kubeadm upgrade diff`. ([#127123](https://github.com/kubernetes/kubernetes/pull/127123), [@neolit123](https://github.com/neolit123)) [SIG Cluster Lifecycle] - Kubeadm: removed the deprecated flag '--experimental-output', please use the flag '--output' instead that serves the same purpose. Affected commands are - "kubeadm config images list", "kubeadm token list", "kubeadm upgade plan", "kubeadm certs check-expiration". ([#126914](https://github.com/kubernetes/kubernetes/pull/126914), [@carlory](https://github.com/carlory)) [SIG Cluster Lifecycle] - Kubeadm: switched the kube-scheduler static Pod to use the endpoints /livez (for startup and liveness probes) and /readyz (for the readiness probe). Previously /healthz was used for all probes, which is deprecated behavior in the scope of this component. ([#126945](https://github.com/kubernetes/kubernetes/pull/126945), [@liangyuanpeng](https://github.com/liangyuanpeng)) [SIG Cluster Lifecycle] - Optimize code, filter podUID is empty string when call this `getPodAndContainerForDevice` method. ([#126997](https://github.com/kubernetes/kubernetes/pull/126997), [@lengrongfu](https://github.com/lengrongfu)) [SIG Node] - Remove GAed feature gates ServerSideApply/ServerSideFieldValidation ([#127058](https://github.com/kubernetes/kubernetes/pull/127058), [@carlory](https://github.com/carlory)) [SIG API Machinery] - Removed feature gate `ValiatingAdmissionPolicy`. ([#126645](https://github.com/kubernetes/kubernetes/pull/126645), [@cici37](https://github.com/cici37)) [SIG API Machinery, Auth and Testing] - Removed generally available feature gate `CloudDualStackNodeIPs`. ([#126840](https://github.com/kubernetes/kubernetes/pull/126840), [@carlory](https://github.com/carlory)) [SIG API Machinery and Cloud Provider] - Removed generally available feature gate `LegacyServiceAccountTokenCleanUp`. ([#126839](https://github.com/kubernetes/kubernetes/pull/126839), [@carlory](https://github.com/carlory)) [SIG Auth] - Removed generally available feature gate `MinDomainsInPodTopologySpread` ([#126863](https://github.com/kubernetes/kubernetes/pull/126863), [@carlory](https://github.com/carlory)) [SIG Scheduling] - Removed generally available feature gate `NewVolumeManagerReconstruction`. ([#126775](https://github.com/kubernetes/kubernetes/pull/126775), [@carlory](https://github.com/carlory)) [SIG Node and Storage] - Removed generally available feature gate `NodeOutOfServiceVolumeDetach` ([#127019](https://github.com/kubernetes/kubernetes/pull/127019), [@carlory](https://github.com/carlory)) [SIG Apps and Testing] - Removed generally available feature gate `StableLoadBalancerNodeSet`. ([#126841](https://github.com/kubernetes/kubernetes/pull/126841), [@carlory](https://github.com/carlory)) [SIG API Machinery, Cloud Provider and Network] - Removed the `KMSv2` and `KMSv2KDF` feature gates. The associated features graduated to stable in the Kubernetes v1.29 release. ([#126698](https://github.com/kubernetes/kubernetes/pull/126698), [@enj](https://github.com/enj)) [SIG API Machinery, Auth and Testing] - Short circuit if the compaction request from apiserver is disabled. ([#126627](https://github.com/kubernetes/kubernetes/pull/126627), [@fusida](https://github.com/fusida)) [SIG Etcd] - Show a warning message to inform users that the `legacy` profile is planned to be deprecated. ([#127230](https://github.com/kubernetes/kubernetes/pull/127230), [@mochizuki875](https://github.com/mochizuki875)) [SIG CLI] - The `flowcontrol.apiserver.k8s.io/v1beta3` API version of `FlowSchema` and `PriorityLevelConfiguration` is no longer served in v1.32. Migrate manifests and API clients to use the `flowcontrol.apiserver.k8s.io/v1` API version, available since v1.29. More information is at https://kubernetes.io/docs/reference/using-api/deprecation-guide/#flowcontrol-resources-v132 ([#127017](https://github.com/kubernetes/kubernetes/pull/127017), [@carlory](https://github.com/carlory)) [SIG API Machinery and Testing] - The kube-proxy command line flags `--healthz-port` and `--metrics-port`, which were previously deprecated, have now been removed. ([#126889](https://github.com/kubernetes/kubernetes/pull/126889), [@aroradaman](https://github.com/aroradaman)) [SIG Network and Windows] - The percentage display in kubectl top node is changed from % -> (%) ([#126995](https://github.com/kubernetes/kubernetes/pull/126995), [@googs1025](https://github.com/googs1025)) [SIG CLI] - Update github.com/coredns/corefile-migration to v1.0.24 ([#126851](https://github.com/kubernetes/kubernetes/pull/126851), [@BenTheElder](https://github.com/BenTheElder)) [SIG Architecture and Cluster Lifecycle] - Updated cni-plugins to [v1.5.1](https://github.com/containernetworking/plugins/releases/tag/v1.5.1). ([#126966](https://github.com/kubernetes/kubernetes/pull/126966), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider, Node and Testing] - Updated cri-tools to v1.31.0. ([#126590](https://github.com/kubernetes/kubernetes/pull/126590), [@saschagrunert](https://github.com/saschagrunert)) [SIG Cloud Provider and Node] - Upgrade etcd client to v3.5.16 ([#127279](https://github.com/kubernetes/kubernetes/pull/127279), [@serathius](https://github.com/serathius)) [SIG API Machinery, Auth, Cloud Provider and Node] ## Dependencies ### Added - github.com/Microsoft/cosesign1go: [v1.1.0](https://github.com/Microsoft/cosesign1go/tree/v1.1.0) - github.com/Microsoft/didx509go: [v0.0.3](https://github.com/Microsoft/didx509go/tree/v0.0.3) - github.com/agnivade/levenshtein: [v1.1.1](https://github.com/agnivade/levenshtein/tree/v1.1.1) - github.com/akavel/rsrc: [v0.10.2](https://github.com/akavel/rsrc/tree/v0.10.2) - github.com/aws/aws-sdk-go-v2/config: [v1.27.24](https://github.com/aws/aws-sdk-go-v2/tree/config/v1.27.24) - github.com/aws/aws-sdk-go-v2/credentials: [v1.17.24](https://github.com/aws/aws-sdk-go-v2/tree/credentials/v1.17.24) - github.com/aws/aws-sdk-go-v2/feature/ec2/imds: [v1.16.9](https://github.com/aws/aws-sdk-go-v2/tree/feature/ec2/imds/v1.16.9) - github.com/aws/aws-sdk-go-v2/internal/configsources: [v1.3.13](https://github.com/aws/aws-sdk-go-v2/tree/internal/configsources/v1.3.13) - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2: [v2.6.13](https://github.com/aws/aws-sdk-go-v2/tree/internal/endpoints/v2/v2.6.13) - github.com/aws/aws-sdk-go-v2/internal/ini: [v1.8.0](https://github.com/aws/aws-sdk-go-v2/tree/internal/ini/v1.8.0) - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding: [v1.11.3](https://github.com/aws/aws-sdk-go-v2/tree/service/internal/accept-encoding/v1.11.3) - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url: [v1.11.15](https://github.com/aws/aws-sdk-go-v2/tree/service/internal/presigned-url/v1.11.15) - github.com/aws/aws-sdk-go-v2/service/sso: [v1.22.1](https://github.com/aws/aws-sdk-go-v2/tree/service/sso/v1.22.1) - github.com/aws/aws-sdk-go-v2/service/ssooidc: [v1.26.2](https://github.com/aws/aws-sdk-go-v2/tree/service/ssooidc/v1.26.2) - github.com/aws/aws-sdk-go-v2/service/sts: [v1.30.1](https://github.com/aws/aws-sdk-go-v2/tree/service/sts/v1.30.1) - github.com/aws/aws-sdk-go-v2: [v1.30.1](https://github.com/aws/aws-sdk-go-v2/tree/v1.30.1) - github.com/aws/smithy-go: [v1.20.3](https://github.com/aws/smithy-go/tree/v1.20.3) - github.com/containerd/cgroups/v3: [v3.0.3](https://github.com/containerd/cgroups/tree/v3.0.3) - github.com/containerd/containerd/api: [v1.7.19](https://github.com/containerd/containerd/tree/api/v1.7.19) - github.com/containerd/errdefs: [v0.1.0](https://github.com/containerd/errdefs/tree/v0.1.0) - github.com/containerd/log: [v0.1.0](https://github.com/containerd/log/tree/v0.1.0) - github.com/containerd/protobuild: [v0.3.0](https://github.com/containerd/protobuild/tree/v0.3.0) - github.com/containerd/stargz-snapshotter/estargz: [v0.14.3](https://github.com/containerd/stargz-snapshotter/tree/estargz/v0.14.3) - github.com/containerd/typeurl/v2: [v2.2.0](https://github.com/containerd/typeurl/tree/v2.2.0) - github.com/decred/dcrd/dcrec/secp256k1/v4: [v4.2.0](https://github.com/decred/dcrd/tree/dcrec/secp256k1/v4/v4.2.0) - github.com/docker/cli: [v24.0.0+incompatible](https://github.com/docker/cli/tree/v24.0.0) - github.com/docker/docker-credential-helpers: [v0.7.0](https://github.com/docker/docker-credential-helpers/tree/v0.7.0) - github.com/docker/go-events: [e31b211](https://github.com/docker/go-events/tree/e31b211) - github.com/go-ini/ini: [v1.67.0](https://github.com/go-ini/ini/tree/v1.67.0) - github.com/gobwas/glob: [v0.2.3](https://github.com/gobwas/glob/tree/v0.2.3) - github.com/goccy/go-json: [v0.10.2](https://github.com/goccy/go-json/tree/v0.10.2) - github.com/google/go-containerregistry: [v0.20.1](https://github.com/google/go-containerregistry/tree/v0.20.1) - github.com/gorilla/mux: [v1.8.1](https://github.com/gorilla/mux/tree/v1.8.1) - github.com/josephspurrier/goversioninfo: [v1.4.0](https://github.com/josephspurrier/goversioninfo/tree/v1.4.0) - github.com/klauspost/compress: [v1.17.0](https://github.com/klauspost/compress/tree/v1.17.0) - github.com/lestrrat-go/backoff/v2: [v2.0.8](https://github.com/lestrrat-go/backoff/tree/v2.0.8) - github.com/lestrrat-go/blackmagic: [v1.0.2](https://github.com/lestrrat-go/blackmagic/tree/v1.0.2) - github.com/lestrrat-go/httpcc: [v1.0.1](https://github.com/lestrrat-go/httpcc/tree/v1.0.1) - github.com/lestrrat-go/iter: [v1.0.2](https://github.com/lestrrat-go/iter/tree/v1.0.2) - github.com/lestrrat-go/jwx: [v1.2.28](https://github.com/lestrrat-go/jwx/tree/v1.2.28) - github.com/lestrrat-go/option: [v1.0.1](https://github.com/lestrrat-go/option/tree/v1.0.1) - github.com/linuxkit/virtsock: [f8cee7d](https://github.com/linuxkit/virtsock/tree/f8cee7d) - github.com/mattn/go-shellwords: [v1.0.12](https://github.com/mattn/go-shellwords/tree/v1.0.12) - github.com/moby/docker-image-spec: [v1.3.1](https://github.com/moby/docker-image-spec/tree/v1.3.1) - github.com/moby/sys/sequential: [v0.5.0](https://github.com/moby/sys/tree/sequential/v0.5.0) - github.com/open-policy-agent/opa: [v0.67.1](https://github.com/open-policy-agent/opa/tree/v0.67.1) - github.com/rcrowley/go-metrics: [10cdbea](https://github.com/rcrowley/go-metrics/tree/10cdbea) - github.com/tchap/go-patricia/v2: [v2.3.1](https://github.com/tchap/go-patricia/tree/v2.3.1) - github.com/vbatts/tar-split: [v0.11.3](https://github.com/vbatts/tar-split/tree/v0.11.3) - github.com/veraison/go-cose: [v1.2.0](https://github.com/veraison/go-cose/tree/v1.2.0) - github.com/xeipuuv/gojsonpointer: [02993c4](https://github.com/xeipuuv/gojsonpointer/tree/02993c4) - github.com/xeipuuv/gojsonreference: [bd5ef7b](https://github.com/xeipuuv/gojsonreference/tree/bd5ef7b) - github.com/yashtewari/glob-intersection: [v0.2.0](https://github.com/yashtewari/glob-intersection/tree/v0.2.0) - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp: v1.27.0 - go.uber.org/mock: v0.4.0 - google.golang.org/grpc/cmd/protoc-gen-go-grpc: v1.5.1 ### Changed - cloud.google.com/go/accessapproval: v1.7.1 → v1.7.4 - cloud.google.com/go/accesscontextmanager: v1.8.1 → v1.8.4 - cloud.google.com/go/aiplatform: v1.48.0 → v1.58.0 - cloud.google.com/go/analytics: v0.21.3 → v0.22.0 - cloud.google.com/go/apigateway: v1.6.1 → v1.6.4 - cloud.google.com/go/apigeeconnect: v1.6.1 → v1.6.4 - cloud.google.com/go/apigeeregistry: v0.7.1 → v0.8.2 - cloud.google.com/go/appengine: v1.8.1 → v1.8.4 - cloud.google.com/go/area120: v0.8.1 → v0.8.4 - cloud.google.com/go/artifactregistry: v1.14.1 → v1.14.6 - cloud.google.com/go/asset: v1.14.1 → v1.17.0 - cloud.google.com/go/assuredworkloads: v1.11.1 → v1.11.4 - cloud.google.com/go/automl: v1.13.1 → v1.13.4 - cloud.google.com/go/baremetalsolution: v1.1.1 → v1.2.3 - cloud.google.com/go/batch: v1.3.1 → v1.7.0 - cloud.google.com/go/beyondcorp: v1.0.0 → v1.0.3 - cloud.google.com/go/bigquery: v1.53.0 → v1.58.0 - cloud.google.com/go/billing: v1.16.0 → v1.18.0 - cloud.google.com/go/binaryauthorization: v1.6.1 → v1.8.0 - cloud.google.com/go/certificatemanager: v1.7.1 → v1.7.4 - cloud.google.com/go/channel: v1.16.0 → v1.17.4 - cloud.google.com/go/cloudbuild: v1.13.0 → v1.15.0 - cloud.google.com/go/clouddms: v1.6.1 → v1.7.3 - cloud.google.com/go/cloudtasks: v1.12.1 → v1.12.4 - cloud.google.com/go/compute: v1.23.0 → v1.25.1 - cloud.google.com/go/contactcenterinsights: v1.10.0 → v1.12.1 - cloud.google.com/go/container: v1.24.0 → v1.29.0 - cloud.google.com/go/containeranalysis: v0.10.1 → v0.11.3 - cloud.google.com/go/datacatalog: v1.16.0 → v1.19.2 - cloud.google.com/go/dataflow: v0.9.1 → v0.9.4 - cloud.google.com/go/dataform: v0.8.1 → v0.9.1 - cloud.google.com/go/datafusion: v1.7.1 → v1.7.4 - cloud.google.com/go/datalabeling: v0.8.1 → v0.8.4 - cloud.google.com/go/dataplex: v1.9.0 → v1.14.0 - cloud.google.com/go/dataproc/v2: v2.0.1 → v2.3.0 - cloud.google.com/go/dataqna: v0.8.1 → v0.8.4 - cloud.google.com/go/datastore: v1.13.0 → v1.15.0 - cloud.google.com/go/datastream: v1.10.0 → v1.10.3 - cloud.google.com/go/deploy: v1.13.0 → v1.17.0 - cloud.google.com/go/dialogflow: v1.40.0 → v1.48.1 - cloud.google.com/go/dlp: v1.10.1 → v1.11.1 - cloud.google.com/go/documentai: v1.22.0 → v1.23.7 - cloud.google.com/go/domains: v0.9.1 → v0.9.4 - cloud.google.com/go/edgecontainer: v1.1.1 → v1.1.4 - cloud.google.com/go/essentialcontacts: v1.6.2 → v1.6.5 - cloud.google.com/go/eventarc: v1.13.0 → v1.13.3 - cloud.google.com/go/filestore: v1.7.1 → v1.8.0 - cloud.google.com/go/firestore: v1.12.0 → v1.14.0 - cloud.google.com/go/functions: v1.15.1 → v1.15.4 - cloud.google.com/go/gkebackup: v1.3.0 → v1.3.4 - cloud.google.com/go/gkeconnect: v0.8.1 → v0.8.4 - cloud.google.com/go/gkehub: v0.14.1 → v0.14.4 - cloud.google.com/go/gkemulticloud: v1.0.0 → v1.1.0 - cloud.google.com/go/gsuiteaddons: v1.6.1 → v1.6.4 - cloud.google.com/go/iam: v1.1.1 → v1.1.5 - cloud.google.com/go/iap: v1.8.1 → v1.9.3 - cloud.google.com/go/ids: v1.4.1 → v1.4.4 - cloud.google.com/go/iot: v1.7.1 → v1.7.4 - cloud.google.com/go/kms: v1.15.0 → v1.15.5 - cloud.google.com/go/language: v1.10.1 → v1.12.2 - cloud.google.com/go/lifesciences: v0.9.1 → v0.9.4 - cloud.google.com/go/logging: v1.7.0 → v1.9.0 - cloud.google.com/go/longrunning: v0.5.1 → v0.5.4 - cloud.google.com/go/managedidentities: v1.6.1 → v1.6.4 - cloud.google.com/go/maps: v1.4.0 → v1.6.3 - cloud.google.com/go/mediatranslation: v0.8.1 → v0.8.4 - cloud.google.com/go/memcache: v1.10.1 → v1.10.4 - cloud.google.com/go/metastore: v1.12.0 → v1.13.3 - cloud.google.com/go/monitoring: v1.15.1 → v1.17.0 - cloud.google.com/go/networkconnectivity: v1.12.1 → v1.14.3 - cloud.google.com/go/networkmanagement: v1.8.0 → v1.9.3 - cloud.google.com/go/networksecurity: v0.9.1 → v0.9.4 - cloud.google.com/go/notebooks: v1.9.1 → v1.11.2 - cloud.google.com/go/optimization: v1.4.1 → v1.6.2 - cloud.google.com/go/orchestration: v1.8.1 → v1.8.4 - cloud.google.com/go/orgpolicy: v1.11.1 → v1.12.0 - cloud.google.com/go/osconfig: v1.12.1 → v1.12.4 - cloud.google.com/go/oslogin: v1.10.1 → v1.13.0 - cloud.google.com/go/phishingprotection: v0.8.1 → v0.8.4 - cloud.google.com/go/policytroubleshooter: v1.8.0 → v1.10.2 - cloud.google.com/go/privatecatalog: v0.9.1 → v0.9.4 - cloud.google.com/go/pubsub: v1.33.0 → v1.34.0 - cloud.google.com/go/recaptchaenterprise/v2: v2.7.2 → v2.9.0 - cloud.google.com/go/recommendationengine: v0.8.1 → v0.8.4 - cloud.google.com/go/recommender: v1.10.1 → v1.12.0 - cloud.google.com/go/redis: v1.13.1 → v1.14.1 - cloud.google.com/go/resourcemanager: v1.9.1 → v1.9.4 - cloud.google.com/go/resourcesettings: v1.6.1 → v1.6.4 - cloud.google.com/go/retail: v1.14.1 → v1.14.4 - cloud.google.com/go/run: v1.2.0 → v1.3.3 - cloud.google.com/go/scheduler: v1.10.1 → v1.10.5 - cloud.google.com/go/secretmanager: v1.11.1 → v1.11.4 - cloud.google.com/go/security: v1.15.1 → v1.15.4 - cloud.google.com/go/securitycenter: v1.23.0 → v1.24.3 - cloud.google.com/go/servicedirectory: v1.11.0 → v1.11.3 - cloud.google.com/go/shell: v1.7.1 → v1.7.4 - cloud.google.com/go/spanner: v1.47.0 → v1.55.0 - cloud.google.com/go/speech: v1.19.0 → v1.21.0 - cloud.google.com/go/storagetransfer: v1.10.0 → v1.10.3 - cloud.google.com/go/talent: v1.6.2 → v1.6.5 - cloud.google.com/go/texttospeech: v1.7.1 → v1.7.4 - cloud.google.com/go/tpu: v1.6.1 → v1.6.4 - cloud.google.com/go/trace: v1.10.1 → v1.10.4 - cloud.google.com/go/translate: v1.8.2 → v1.10.0 - cloud.google.com/go/video: v1.19.0 → v1.20.3 - cloud.google.com/go/videointelligence: v1.11.1 → v1.11.4 - cloud.google.com/go/vision/v2: v2.7.2 → v2.7.5 - cloud.google.com/go/vmmigration: v1.7.1 → v1.7.4 - cloud.google.com/go/vmwareengine: v1.0.0 → v1.0.3 - cloud.google.com/go/vpcaccess: v1.7.1 → v1.7.4 - cloud.google.com/go/webrisk: v1.9.1 → v1.9.4 - cloud.google.com/go/websecurityscanner: v1.6.1 → v1.6.4 - cloud.google.com/go/workflows: v1.11.1 → v1.12.3 - cloud.google.com/go: v0.110.7 → v0.112.0 - github.com/Azure/go-ansiterm: [d185dfc → 306776e](https://github.com/Azure/go-ansiterm/compare/d185dfc...306776e) - github.com/Microsoft/go-winio: [v0.6.0 → v0.6.2](https://github.com/Microsoft/go-winio/compare/v0.6.0...v0.6.2) - github.com/Microsoft/hcsshim: [v0.8.26 → v0.12.6](https://github.com/Microsoft/hcsshim/compare/v0.8.26...v0.12.6) - github.com/OneOfOne/xxhash: [v1.2.2 → v1.2.8](https://github.com/OneOfOne/xxhash/compare/v1.2.2...v1.2.8) - github.com/cilium/ebpf: [v0.9.1 → v0.11.0](https://github.com/cilium/ebpf/compare/v0.9.1...v0.11.0) - github.com/containerd/console: [v1.0.3 → v1.0.4](https://github.com/containerd/console/compare/v1.0.3...v1.0.4) - github.com/containerd/containerd: [v1.4.9 → v1.7.20](https://github.com/containerd/containerd/compare/v1.4.9...v1.7.20) - github.com/containerd/continuity: [v0.1.0 → v0.4.2](https://github.com/containerd/continuity/compare/v0.1.0...v0.4.2) - github.com/containerd/fifo: [v1.0.0 → v1.1.0](https://github.com/containerd/fifo/compare/v1.0.0...v1.1.0) - github.com/containerd/ttrpc: [v1.2.2 → v1.2.5](https://github.com/containerd/ttrpc/compare/v1.2.2...v1.2.5) - github.com/coredns/corefile-migration: [v1.0.21 → v1.0.24](https://github.com/coredns/corefile-migration/compare/v1.0.21...v1.0.24) - github.com/distribution/reference: [v0.5.0 → v0.6.0](https://github.com/distribution/reference/compare/v0.5.0...v0.6.0) - github.com/docker/docker: [v20.10.27+incompatible → v27.1.1+incompatible](https://github.com/docker/docker/compare/v20.10.27...v27.1.1) - github.com/docker/go-connections: [v0.4.0 → v0.5.0](https://github.com/docker/go-connections/compare/v0.4.0...v0.5.0) - github.com/frankban/quicktest: [v1.14.0 → v1.14.5](https://github.com/frankban/quicktest/compare/v1.14.0...v1.14.5) - github.com/go-openapi/jsonpointer: [v0.19.6 → v0.21.0](https://github.com/go-openapi/jsonpointer/compare/v0.19.6...v0.21.0) - github.com/go-openapi/swag: [v0.22.4 → v0.23.0](https://github.com/go-openapi/swag/compare/v0.22.4...v0.23.0) - github.com/golang/mock: [v1.3.1 → v1.1.1](https://github.com/golang/mock/compare/v1.3.1...v1.1.1) - github.com/google/cadvisor: [v0.49.0 → v0.50.0](https://github.com/google/cadvisor/compare/v0.49.0...v0.50.0) - github.com/google/pprof: [4bfdf5a → 813a5fb](https://github.com/google/pprof/compare/4bfdf5a...813a5fb) - github.com/opencontainers/image-spec: [v1.0.2 → v1.1.0](https://github.com/opencontainers/image-spec/compare/v1.0.2...v1.1.0) - github.com/opencontainers/runc: [v1.1.13 → v1.1.14](https://github.com/opencontainers/runc/compare/v1.1.13...v1.1.14) - github.com/opencontainers/runtime-spec: [494a5a6 → v1.2.0](https://github.com/opencontainers/runtime-spec/compare/494a5a6...v1.2.0) - github.com/pelletier/go-toml: [v1.2.0 → v1.9.5](https://github.com/pelletier/go-toml/compare/v1.2.0...v1.9.5) - github.com/urfave/cli: [v1.22.2 → v1.22.15](https://github.com/urfave/cli/compare/v1.22.2...v1.22.15) - github.com/vishvananda/netlink: [v1.1.0 → v1.3.0](https://github.com/vishvananda/netlink/compare/v1.1.0...v1.3.0) - go.etcd.io/bbolt: v1.3.9 → v1.3.11 - go.etcd.io/etcd/api/v3: v3.5.14 → v3.5.16 - go.etcd.io/etcd/client/pkg/v3: v3.5.14 → v3.5.16 - go.etcd.io/etcd/client/v2: v2.305.13 → v2.305.16 - go.etcd.io/etcd/client/v3: v3.5.14 → v3.5.16 - go.etcd.io/etcd/pkg/v3: v3.5.13 → v3.5.16 - go.etcd.io/etcd/raft/v3: v3.5.13 → v3.5.16 - go.etcd.io/etcd/server/v3: v3.5.13 → v3.5.16 - go.uber.org/zap: v1.26.0 → v1.27.0 - golang.org/x/crypto: v0.24.0 → v0.26.0 - golang.org/x/exp: f3d0a9c → 8a7402a - golang.org/x/lint: 1621716 → d0100b6 - golang.org/x/mod: v0.17.0 → v0.20.0 - golang.org/x/net: v0.26.0 → v0.28.0 - golang.org/x/sync: v0.7.0 → v0.8.0 - golang.org/x/sys: v0.21.0 → v0.23.0 - golang.org/x/telemetry: f48c80b → bda5523 - golang.org/x/term: v0.21.0 → v0.23.0 - golang.org/x/text: v0.16.0 → v0.17.0 - golang.org/x/tools: e35e4cc → v0.24.0 - golang.org/x/xerrors: 04be3eb → 5ec99f8 - google.golang.org/genproto: b8732ec → ef43131 - gotest.tools/v3: v3.0.3 → v3.0.2 - honnef.co/go/tools: v0.0.1-2019.2.3 → ea95bdf - k8s.io/gengo/v2: 51d4e06 → 2b36238 - k8s.io/kube-openapi: 70dd376 → f7e401e ### Removed - bazil.org/fuse: 371fbbd - cloud.google.com/go/storage: v1.0.0 - dmitri.shuralyov.com/gpu/mtl: 666a987 - github.com/BurntSushi/xgb: [27f1227](https://github.com/BurntSushi/xgb/tree/27f1227) - github.com/alecthomas/template: [a0175ee](https://github.com/alecthomas/template/tree/a0175ee) - github.com/armon/consul-api: [eb2c6b5](https://github.com/armon/consul-api/tree/eb2c6b5) - github.com/armon/go-metrics: [f0300d1](https://github.com/armon/go-metrics/tree/f0300d1) - github.com/armon/go-radix: [7fddfc3](https://github.com/armon/go-radix/tree/7fddfc3) - github.com/aws/aws-sdk-go: [v1.35.24](https://github.com/aws/aws-sdk-go/tree/v1.35.24) - github.com/bgentry/speakeasy: [v0.1.0](https://github.com/bgentry/speakeasy/tree/v0.1.0) - github.com/bketelsen/crypt: [5cbc8cc](https://github.com/bketelsen/crypt/tree/5cbc8cc) - github.com/cespare/xxhash: [v1.1.0](https://github.com/cespare/xxhash/tree/v1.1.0) - github.com/containerd/typeurl: [v1.0.2](https://github.com/containerd/typeurl/tree/v1.0.2) - github.com/coreos/bbolt: [v1.3.2](https://github.com/coreos/bbolt/tree/v1.3.2) - github.com/coreos/etcd: [v3.3.13+incompatible](https://github.com/coreos/etcd/tree/v3.3.13) - github.com/coreos/go-systemd: [95778df](https://github.com/coreos/go-systemd/tree/95778df) - github.com/coreos/pkg: [399ea9e](https://github.com/coreos/pkg/tree/399ea9e) - github.com/dgrijalva/jwt-go: [v3.2.0+incompatible](https://github.com/dgrijalva/jwt-go/tree/v3.2.0) - github.com/dgryski/go-sip13: [e10d5fe](https://github.com/dgryski/go-sip13/tree/e10d5fe) - github.com/fatih/color: [v1.7.0](https://github.com/fatih/color/tree/v1.7.0) - github.com/go-gl/glfw: [e6da0ac](https://github.com/go-gl/glfw/tree/e6da0ac) - github.com/gogo/googleapis: [v1.4.1](https://github.com/gogo/googleapis/tree/v1.4.1) - github.com/google/martian: [v2.1.0+incompatible](https://github.com/google/martian/tree/v2.1.0) - github.com/google/renameio: [v0.1.0](https://github.com/google/renameio/tree/v0.1.0) - github.com/googleapis/gax-go/v2: [v2.0.5](https://github.com/googleapis/gax-go/tree/v2.0.5) - github.com/gopherjs/gopherjs: [0766667](https://github.com/gopherjs/gopherjs/tree/0766667) - github.com/hashicorp/consul/api: [v1.1.0](https://github.com/hashicorp/consul/tree/api/v1.1.0) - github.com/hashicorp/consul/sdk: [v0.1.1](https://github.com/hashicorp/consul/tree/sdk/v0.1.1) - github.com/hashicorp/errwrap: [v1.0.0](https://github.com/hashicorp/errwrap/tree/v1.0.0) - github.com/hashicorp/go-cleanhttp: [v0.5.1](https://github.com/hashicorp/go-cleanhttp/tree/v0.5.1) - github.com/hashicorp/go-immutable-radix: [v1.0.0](https://github.com/hashicorp/go-immutable-radix/tree/v1.0.0) - github.com/hashicorp/go-msgpack: [v0.5.3](https://github.com/hashicorp/go-msgpack/tree/v0.5.3) - github.com/hashicorp/go-multierror: [v1.0.0](https://github.com/hashicorp/go-multierror/tree/v1.0.0) - github.com/hashicorp/go-rootcerts: [v1.0.0](https://github.com/hashicorp/go-rootcerts/tree/v1.0.0) - github.com/hashicorp/go-sockaddr: [v1.0.0](https://github.com/hashicorp/go-sockaddr/tree/v1.0.0) - github.com/hashicorp/go-syslog: [v1.0.0](https://github.com/hashicorp/go-syslog/tree/v1.0.0) - github.com/hashicorp/go-uuid: [v1.0.1](https://github.com/hashicorp/go-uuid/tree/v1.0.1) - github.com/hashicorp/go.net: [v0.0.1](https://github.com/hashicorp/go.net/tree/v0.0.1) - github.com/hashicorp/golang-lru: [v0.5.1](https://github.com/hashicorp/golang-lru/tree/v0.5.1) - github.com/hashicorp/hcl: [v1.0.0](https://github.com/hashicorp/hcl/tree/v1.0.0) - github.com/hashicorp/logutils: [v1.0.0](https://github.com/hashicorp/logutils/tree/v1.0.0) - github.com/hashicorp/mdns: [v1.0.0](https://github.com/hashicorp/mdns/tree/v1.0.0) - github.com/hashicorp/memberlist: [v0.1.3](https://github.com/hashicorp/memberlist/tree/v0.1.3) - github.com/hashicorp/serf: [v0.8.2](https://github.com/hashicorp/serf/tree/v0.8.2) - github.com/imdario/mergo: [v0.3.6](https://github.com/imdario/mergo/tree/v0.3.6) - github.com/jmespath/go-jmespath: [v0.4.0](https://github.com/jmespath/go-jmespath/tree/v0.4.0) - github.com/jstemmer/go-junit-report: [af01ea7](https://github.com/jstemmer/go-junit-report/tree/af01ea7) - github.com/jtolds/gls: [v4.20.0+incompatible](https://github.com/jtolds/gls/tree/v4.20.0) - github.com/magiconair/properties: [v1.8.1](https://github.com/magiconair/properties/tree/v1.8.1) - github.com/mattn/go-colorable: [v0.0.9](https://github.com/mattn/go-colorable/tree/v0.0.9) - github.com/mattn/go-isatty: [v0.0.3](https://github.com/mattn/go-isatty/tree/v0.0.3) - github.com/miekg/dns: [v1.0.14](https://github.com/miekg/dns/tree/v1.0.14) - github.com/mitchellh/cli: [v1.0.0](https://github.com/mitchellh/cli/tree/v1.0.0) - github.com/mitchellh/go-testing-interface: [v1.0.0](https://github.com/mitchellh/go-testing-interface/tree/v1.0.0) - github.com/mitchellh/gox: [v0.4.0](https://github.com/mitchellh/gox/tree/v0.4.0) - github.com/mitchellh/iochan: [v1.0.0](https://github.com/mitchellh/iochan/tree/v1.0.0) - github.com/mitchellh/mapstructure: [v1.1.2](https://github.com/mitchellh/mapstructure/tree/v1.1.2) - github.com/oklog/ulid: [v1.3.1](https://github.com/oklog/ulid/tree/v1.3.1) - github.com/pascaldekloe/goe: [57f6aae](https://github.com/pascaldekloe/goe/tree/57f6aae) - github.com/posener/complete: [v1.1.1](https://github.com/posener/complete/tree/v1.1.1) - github.com/prometheus/tsdb: [v0.7.1](https://github.com/prometheus/tsdb/tree/v0.7.1) - github.com/ryanuber/columnize: [9b3edd6](https://github.com/ryanuber/columnize/tree/9b3edd6) - github.com/sean-/seed: [e2103e2](https://github.com/sean-/seed/tree/e2103e2) - github.com/smartystreets/assertions: [b2de0cb](https://github.com/smartystreets/assertions/tree/b2de0cb) - github.com/smartystreets/goconvey: [v1.6.4](https://github.com/smartystreets/goconvey/tree/v1.6.4) - github.com/spaolacci/murmur3: [f09979e](https://github.com/spaolacci/murmur3/tree/f09979e) - github.com/spf13/afero: [v1.1.2](https://github.com/spf13/afero/tree/v1.1.2) - github.com/spf13/cast: [v1.3.0](https://github.com/spf13/cast/tree/v1.3.0) - github.com/spf13/jwalterweatherman: [v1.0.0](https://github.com/spf13/jwalterweatherman/tree/v1.0.0) - github.com/spf13/viper: [v1.7.0](https://github.com/spf13/viper/tree/v1.7.0) - github.com/subosito/gotenv: [v1.2.0](https://github.com/subosito/gotenv/tree/v1.2.0) - github.com/ugorji/go: [v1.1.4](https://github.com/ugorji/go/tree/v1.1.4) - github.com/xordataexchange/crypt: [b2862e3](https://github.com/xordataexchange/crypt/tree/b2862e3) - golang.org/x/image: cff245a - golang.org/x/mobile: d2bd2a2 - google.golang.org/api: v0.13.0 - gopkg.in/alecthomas/kingpin.v2: v2.2.6 - gopkg.in/errgo.v2: v2.1.0 - gopkg.in/ini.v1: v1.51.0 - gopkg.in/resty.v1: v1.12.0 - rsc.io/binaryregexp: v0.2.0kubernetes-kubernetes-9bda076/CHANGELOG/OWNERS000066400000000000000000000014411476411216400207010ustar00rootroot00000000000000# 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 - AuraSinis # 1.24 Release Notes Lead - cici37 # 1.23 Release Notes Lead - csantanapr # 1.25 Release Notes Lead - harshanarayana # 1.27 Release Notes Lead - ramrodo # 1.26 Release Notes Lead - sanchita-07 # 1.28 Release Notes Lead - fsmunoz # 1.29 Release Notes Lead - rashansmith # 1.30 Release Notes Lead reviewers: - release-managers - fykaa # 1.30 Release Notes Shadow - npolshakova # 1.30 Release Notes Shadow - OrlinVasilev # 1.30 Release Notes Shadow - rashansmith # 1.30 Release Notes Lead - satyampsoni # 1.30 Release Notes Shadow labels: - sig/release - area/release-eng kubernetes-kubernetes-9bda076/CHANGELOG/README.md000066400000000000000000000024631476411216400212250ustar00rootroot00000000000000# CHANGELOGs - [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-9bda076/CONTRIBUTING.md000066400000000000000000000010151476411216400207000ustar00rootroot00000000000000# 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-9bda076/LICENSE000066400000000000000000000261361476411216400174670ustar00rootroot00000000000000 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-9bda076/LICENSES/000077500000000000000000000000001476411216400176575ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/LICENSES/LICENSE000066400000000000000000000265141476411216400206740ustar00rootroot00000000000000================================================================================ = 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-9bda076/LICENSES/OWNERS000066400000000000000000000002271476411216400206200ustar00rootroot00000000000000# 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-9bda076/Makefile000077700000000000000000000000001476411216400236272build/root/Makefileustar00rootroot00000000000000kubernetes-kubernetes-9bda076/OWNERS000066400000000000000000000015161476411216400174150ustar00rootroot00000000000000# 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-9bda076/OWNERS_ALIASES000066400000000000000000000266721476411216400205700ustar00rootroot00000000000000aliases: # 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 release-managers: - cpanato - jeremyrickard - justaugustus - palnabarun - puerco - saschagrunert - Verolop - xmudrii 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 # 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 # 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 - Huang-Wei - ahg-g - kerthcet - sanposhiho # emeritus: # - damemi # - bsalamat # - k82cn # - ravisantoshgudimetla # - wojtek-t sig-scheduling: - AxeZhan - damemi - denkensk - 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 sig-instrumentation-reviewers: - dashpole - s-urbaniak - coffeepac - logicalhan - RainbowMango - serathius - dgrisonnet - pohly - mengjiao-liu - rexagod # 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: - CecileRobertMichon - 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 - 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-9bda076/README.md000066400000000000000000000104301476411216400177270ustar00rootroot00000000000000# 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 [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-9bda076/SECURITY_CONTACTS000066400000000000000000000012311476411216400211370ustar00rootroot00000000000000# 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-9bda076/SUPPORT.md000066400000000000000000000020651476411216400201530ustar00rootroot00000000000000## 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-9bda076/cmd/000077500000000000000000000000001476411216400172155ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/cmd/OWNERS000066400000000000000000000005551476411216400201620ustar00rootroot00000000000000# 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-9bda076/cmd/kubectl/000077500000000000000000000000001476411216400206465ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/cmd/kubectl/OWNERS000066400000000000000000000002341476411216400216050ustar00rootroot00000000000000# 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-9bda076/cmd/kubectl/kubectl.go000066400000000000000000000017031476411216400226270ustar00rootroot00000000000000/* 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-9bda076/code-of-conduct.md000066400000000000000000000002241476411216400217430ustar00rootroot00000000000000# 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-9bda076/go.mod000066400000000000000000000267311476411216400175710ustar00rootroot00000000000000// 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.23.0 godebug default=go1.23 godebug winsymlink=0 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.0.8 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.24 github.com/coreos/go-oidc v2.2.1+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.3.4 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/golang/protobuf v1.5.4 github.com/google/cadvisor v0.51.0 github.com/google/cel-go v0.22.0 github.com/google/gnostic-models v0.6.8 github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.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/runc v1.2.1 github.com/opencontainers/selinux v1.11.1 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.55.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.9.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.16 go.etcd.io/etcd/client/pkg/v3 v3.5.16 go.etcd.io/etcd/client/v3 v3.5.16 go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.42.0 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 go.opentelemetry.io/otel/metric v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/trace v1.28.0 go.opentelemetry.io/proto/otlp v1.3.1 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.28.0 golang.org/x/net v0.30.0 golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 golang.org/x/sys v0.26.0 golang.org/x/term v0.25.0 golang.org/x/time v0.7.0 golang.org/x/tools v0.26.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.35.1 gopkg.in/evanphx/json-patch.v4 v4.12.0 gopkg.in/square/go-jose.v2 v2.6.0 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-20241105132330-32ad38e42d3f 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/structured-merge-diff/v4 v4.4.2 sigs.k8s.io/yaml v1.4.0 ) require ( cel.dev/expr v0.18.0 // 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/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // 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.7.19 // indirect github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/ttrpc v1.2.5 // 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.2-0.20180830191138-d8f796af33cc // 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.0 // indirect github.com/google/btree v1.0.1 // 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.0 // 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.20.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/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/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.16 // indirect go.etcd.io/etcd/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/raft/v3 v3.5.16 // indirect go.etcd.io/etcd/server/v3 v3.5.16 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.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.19.0 // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // 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-20240911193312-2b36238f13e9 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/kustomize/api v0.18.0 // indirect sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 // indirect sigs.k8s.io/kustomize/kyaml v0.18.1 // 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-9bda076/go.sum000066400000000000000000001764251476411216400176240ustar00rootroot00000000000000bitbucket.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.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= cel.dev/expr v0.18.0/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.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 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.0.8 h1:EBrIiRB7i/UYIXEC2yw22dn+RLzOmsc5S0bw2xf0Qus= github.com/Microsoft/hnslib v0.0.8/go.mod h1:EYveQJlhKh2obmEIRB3uKN6dBd9pj1frPsrTGFppKuk= 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/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 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/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= 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-20240423153145-555b57ec207b/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/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= 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.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= 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.24 h1:NL/zRKijhJZLYlNnMr891DRv5jXgfd3Noons1M6oTpc= github.com/coredns/corefile-migration v1.0.24/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+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.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= 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-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 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.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/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.1/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.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cadvisor v0.51.0 h1:BspqSPdZoLKrnvuZNOvM/KiJ/A+RdixwagN20n+2H8k= github.com/google/cadvisor v0.51.0/go.mod h1:czGE/c/P/i0QFpVNKTFrIEzord9Y10YfpwuaSWXELc0= github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.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.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 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/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/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.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 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/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 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/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.2.1 h1:mQkmeFSUxqFaVmvIn1VQPeQIKpHFya5R07aJw0DKQa8= github.com/opencontainers/runc v1.2.1/go.mod h1:/PXzF0h531HTMsYQnmxXkBD7YaGShm/2zcRB79dksUc= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/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.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/seccomp/libseccomp-golang v0.10.0/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 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/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= 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.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28= go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q= go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E= go.etcd.io/etcd/client/v2 v2.305.16 h1:kQrn9o5czVNaukf2A2At43cE9ZtWauOtf9vRZuiKXow= go.etcd.io/etcd/client/v2 v2.305.16/go.mod h1:h9YxWCzcdvZENbfzBTFCnoNumr2ax3F19sKMqHFmXHE= go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE= go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50= go.etcd.io/etcd/pkg/v3 v3.5.16 h1:cnavs5WSPWeK4TYwPYfmcr3Joz9BH+TZ6qoUtz6/+mc= go.etcd.io/etcd/pkg/v3 v3.5.16/go.mod h1:+lutCZHG5MBBFI/U4eYT5yL7sJfnexsoM20Y0t2uNuY= go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE= go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= 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.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= 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.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 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.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.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/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-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-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 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.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/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/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/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 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-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4= k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 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.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/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.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= sigs.k8s.io/kustomize/cmd/config v0.15.0/go.mod h1:Jq57b0nPaoYUlOqg//0JtAh6iibboqMcfbtCYoWPM00= sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 h1:o1mtt6vpxsxDYaZKrw3BnEtc+pAjLz7UffnIvHNbvW0= sigs.k8s.io/kustomize/kustomize/v5 v5.5.0/go.mod h1:AeFCmgCrXzmvjWWaeZCyBp6XzG1Y0w1svYus8GhJEOE= sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= kubernetes-kubernetes-9bda076/go.work000066400000000000000000000023421476411216400177640ustar00rootroot00000000000000// This is a generated file. Do not edit directly. go 1.23.0 godebug default=go1.23 godebug winsymlink=0 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-9bda076/go.work.sum000066400000000000000000000400011476411216400205610ustar00rootroot00000000000000cloud.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.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 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/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3Z5Odx4wjKOASzA= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= 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-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= 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-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= 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.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 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/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/seccomp/libseccomp-golang v0.10.0 h1:aA4bp+/Zzi0BnWZ2F1wgNBs5gTpm+na2rWM6M9YjLpY= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= 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.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 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.15.0 h1:WkdY8V2+8J+W00YbImXa2ke9oegfrHH79e+kywW7EdU= kubernetes-kubernetes-9bda076/staging/000077500000000000000000000000001476411216400201065ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/OWNERS000066400000000000000000000006051476411216400210470ustar00rootroot00000000000000# 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-9bda076/staging/README.md000066400000000000000000000134131476411216400213670ustar00rootroot00000000000000# 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-9bda076/staging/src/000077500000000000000000000000001476411216400206755ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/000077500000000000000000000000001476411216400220105ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/000077500000000000000000000000001476411216400234415ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/.github/000077500000000000000000000000001476411216400250015ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/000077500000000000000000000000001476411216400271645ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000016371476411216400316030ustar00rootroot00000000000000--- 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-9bda076/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002161476411216400311530ustar00rootroot00000000000000contact_links: - name: Support Request url: https://discuss.kubernetes.io about: Support request or question relating to Kubernetes kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/.github/ISSUE_TEMPLATE/enhancement.md000066400000000000000000000003531476411216400317740ustar00rootroot00000000000000--- 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-9bda076/staging/src/k8s.io/kubectl/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002251476411216400306010ustar00rootroot00000000000000Sorry, we do not accept changes directly against this repository. Please see CONTRIBUTING.md for information on where and how to contribute instead. kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/CONTRIBUTING.md000066400000000000000000000011301476411216400256650ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/LICENSE000066400000000000000000000261351476411216400244550ustar00rootroot00000000000000 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-9bda076/staging/src/k8s.io/kubectl/OWNERS000066400000000000000000000002341476411216400244000ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/README.md000066400000000000000000000031561476411216400247250ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/SECURITY_CONTACTS000066400000000000000000000010551476411216400261320ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/code-of-conduct.md000066400000000000000000000002241476411216400267320ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/doc.go000066400000000000000000000011461476411216400245370ustar00rootroot00000000000000/* 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 // import "k8s.io/kubectl" kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/docs/000077500000000000000000000000001476411216400243715ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/docs/maintainers/000077500000000000000000000000001476411216400267035ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/docs/maintainers/MAINTAINERS.md000066400000000000000000000054451476411216400310070ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/docs/maintainers/issue_backlog.md000066400000000000000000000176551476411216400320550ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/docs/roadmap/000077500000000000000000000000001476411216400260145ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/docs/roadmap/template.md000066400000000000000000000004741476411216400301560ustar00rootroot00000000000000Use 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-9bda076/staging/src/k8s.io/kubectl/go.mod000066400000000000000000000102221476411216400245440ustar00rootroot00000000000000// This is a generated file. Do not edit directly. module k8s.io/kubectl go 1.23.0 godebug default=go1.23 godebug winsymlink=0 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.8 github.com/google/go-cmp v0.6.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.9.0 golang.org/x/sys v0.26.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-20241105132330-32ad38e42d3f 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.5.0 sigs.k8s.io/kustomize/kyaml v0.18.1 sigs.k8s.io/structured-merge-diff/v4 v4.4.2 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.2-0.20180830191138-d8f796af33cc // 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/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/gofuzz v1.2.0 // 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.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/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.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/kustomize/api v0.18.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-9bda076/staging/src/k8s.io/kubectl/go.sum000066400000000000000000000566251476411216400246120ustar00rootroot00000000000000cloud.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/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-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.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 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.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.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-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/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-20240911193312-2b36238f13e9/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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 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.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= sigs.k8s.io/kustomize/cmd/config v0.15.0/go.mod h1:Jq57b0nPaoYUlOqg//0JtAh6iibboqMcfbtCYoWPM00= sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 h1:o1mtt6vpxsxDYaZKrw3BnEtc+pAjLz7UffnIvHNbvW0= sigs.k8s.io/kustomize/kustomize/v5 v5.5.0/go.mod h1:AeFCmgCrXzmvjWWaeZCyBp6XzG1Y0w1svYus8GhJEOE= sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/images/000077500000000000000000000000001476411216400247065ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/images/kubectl-logo-full.png000066400000000000000000050572671476411216400307710ustar00rootroot00000000000000‰PNG  IHDR < îm¢k pHYs.#.#x¥?v IDATxÚìÝ]l݇}æùǶDÚ”IiŠþCT#ŠAb²#ÅÄ´zÁ €µâ(°‹éÀŽvï¹Xw̃pwÝ›-ÖL¦Û«*:ÅÎvÊÓ`Òf)Km·‘Åt— •PŽÇåêSr ‘mR²¼‡)E¶õ—s?€ Ä×£çÐE!è›ß#üq–*Š¢/É–ûü´®ù—z31ÿr_ʲ<î'VÎ#‚¨MEQlIÒ÷ ï>ð oÿ´ÏYxÿ³Ö]5çóé1ÅH’+÷ù¾‘²,¯˜€F'x€eREW~ùÂÁ»|èÝÞÖ—d³y'îò¶»EÇïøý•²,GÌ@­<À¼¢(Üñ¦ÏúýsV£AÝOÜNÜöû²,› €å&x áÜ.ô%Ù2ÿë-ó¿_ú>W`y]M5ˆX°4ޏrçûʲ¼b2îFð@M+Š¢+I×üo?é×[’öØcŸ»~ýúöëׯo³@Í[¸qåŽ×)Ëò¸y€õNðP£î îxí:Àú°p%âø’×Wʲ1 Ðèk¤(Š®$w{4p/>éBÄDY–æêà`-¹Òp ·_ixÖ:¬°óI&²CO’²,›¨‚€‡TÅ,Æ ]K^vX€µpb"K¢1PK÷àŽK ]ó/}I6[€#†j‚à`^Q]Y –¾v©ªÎgI‘äx’‰²,'L,7Á°îEq Õk }ó/[’ÿsöf’÷L°<À²Zrµa!nxÎ*@ 9‘ùKq ‚O×’¤þå‰ûø¼ Iþ>ÕâÍðàÀ)ŠbKªAøÚÔ·;¯AŒ”e9a–u­?É?Ïý…wóÞüÏÔÂîƒàøLEQteñbÃù×›-4°«©þõ㙿Q–¥°ÞøZ’ÎÊ„¼d1|I2cn€O'xnSÅBذðòœUn9‘Û/Aˆ GK’o%Ù¾JßïtÄŸJð똸`Y,ý‡ë#eY7I]úƒ¬^ìp§-ù`žàÖ qÀªAÔ—¯&é¯Çñ^ªñÃP\}<@#7Ô¤ÓIŽg1‚ð¿æ_¾”ä[5ö˜>˜ÿ9ù©Fë’àê\Q]Y Ì¿Þl€ºp"‹— Ž—e9a’U÷­T£‡Z5”ä/ãâ°  ŽE±%·‡ "nh$W3?d1‚¸b–ó+I~¿ç©^{ò”ë‰àjXQKㆾ$ÏZ`Ý9Ÿ%DY–ÇM²l¾š¤¿ŽïÏ“üq\{Ö ÁÔˆ¢(ºrûõ†ç¬À'8Û¯@L˜ä|+É—êì1ä’ü£§ht‚X#EQÈíÃ«ð€®æöâ¸IîÉŸÔéãþ É‘ùç a `¸ÞÀpâ³ýI?v—€†'x€PÅÒ¸á@\ojHkǶ´ut~æÇuîÞsß_{ëΞ4oj««=.œ9uߟsyülf¯MêÇLMV2=yÑPK®@O2â D¾”ä[uþgø É¿J2ãÇhD‚xHEQlÉbØp ®7¨}ç3w>)<ؾkï]ßÞ´©5[»{ Zã.ŸÍÜ'D—ÎÝ5¨¸[h1{m*—ϽiPàAÈíW ®¬£?{#IòóT/=4Áܧ¢(ºr{àð¬U`ýºóZBó¦Ö´ß´utþÒE…öîž4oj5 +¦2:|Ûïg¯MåÒ¹³·½í΀ÂU ÉéÌÇ© ügmIò¿7ÈŸåR Šà>CQ}¹=pØah,»/(Ü,Ü+´>½í—âhT³×¦syüì’ßßML¿[ÉÔ’@¢rfØhÐxÎçöb¤Áþ|Ò ŽÓIþ­W ÑàóÃ%/›­õaéµ…;Ã…í»ö.~œhVܱą3§nýzée W% î\M5~I5€8^çž?H²½Až›ßM2ãGh$‚Ö½¢(d1nè‹ÀjÆÒ€¡}gOšŸlM’lÝÙ“æMmÕ/@ÃXI,½&1ûþt.Ÿ[|ûåso jˉ,^€8^gý«IúäyøãTC€†!x`ݹ#pxÎ"°º–F »÷$©^cغ³·ú~p¦&+™~·zâÒ¹±[W#*£ÃI݆ê)€øR’o5Èî™ä?úñ‰à€†'p€ÕѾó™4ojKó¦Ö´w÷$Y¼Äд©5[çß°.ŸÍÜ|qáÌ©$Éô»•LMVƒ‰Ê™a#ÁÊ9ù"ÕâJ=¾ÿ%Éç`gÁÐp4,¯»]d2jájÄ쵩\:w6Iryülf¯MWß7HåVQ–å` <ž–ä °«àh8‚êžÀÜB̰ô*Ãö]{“$íÝ=iÞÔj$€»¨ŒV/B\:7–ÙkÓ·®Eˆ"àœÈbq|÷’|©Îw< Gð@Ý)Š¢/‹Ã!‹À'ëÜU½Èpçe1ÀÊš½6ËãÕ ΜJ²$’?›¹™i#Á'[‹âsIþ Éu¼›àh8‚jÞÃ$›­ISKk¶Î‡ ÃÖ½iÚT};µíÒøÙÌ]›¾u%âòøÙÌ^›DÀ/;šÅbd¿O_’ÿ®Žwú£$?÷ã4Á5§(Š®Ü8ì° ëUç®=‚€uJwu5óñC’Á²,'–ùëÿ³$_M}^zøÝ$3~D€F"x`ÍE±%‹qÃ@¬#­ÛÒÖÑ™ö=i~²5[wö¤yS[:wï1Ÿª2:œ$¹pæTfߟÎåsg3{m*—ϽiÖ“ó¹=€¸² _óWR½ôð¹:Úát’ëÇh4‚ÖDQ²8÷¦qhW3?¤@LÜÇ綤=ô¥ö.Ôýq’O/Ј,›¢(d1rxÖ"Ô£¦–ÖlíîI[Ƕ´>Ý™­;{ÒÚÑ™­Ý=Æ`]»4~6sצsá̩̾?ËçÎVß63mêÕéTã‡Á²,ßÇçÕRüð£$G<•@£<ðÀŠ¢èÊíW6[…zq·k íÝ=iÞÔj¸ W!¦&+™š¬¤2:œ©ÉJ¦'/‡zs4‹ÄÄ=~ÎZÆ’üÏž6 ‘ ¸/óWW¨yí;ŸI[GgÚ»{ÒÖÑy+rVÞ¥ñ³™ž¬äÒ¹³·¢ˆËçÞ4 õà|’ÁTˆãeY^¹‡ÏYÍøÁe`]<ð©æ¯8,‡,B­Z6lÝÙ“ÖŽÎlíî1 Ô !uèD戲,GîáãW*~ø ÕÐaÄS¬‚~IQ Ã@’¡– q !¨çS½ü0X–åà=|ürÄï¥zÕa(ÉŒ§X/,\q8ÅK›­ÂZkíØ–¶ŽÎtîÞ“¶ŽÎ[¿ÖŸ¥!Det8S“•LO^4 µâhˆ‰ÏøØ–$_šù•ù—'îòqïÍ¿ü<Õkÿhf`=<¬SEQô%9œjàð¬EXK»ö¤­cÛüÕ†Þ´w÷¤yS«a€OUÎ¥sc™z÷b.Ÿ;›Kãg373mÖÒù$ƒIŽßãõ>…à`(ŠbK/8 ÄÖ@kǶlÝÙ36T_·utX6S“•L¿{1ΜÊåñ³™š¬äò¹7 ÃZ¸šùË©&¸?‚€VEWªqÃ@’ç,Âjrµ¨%w^ƒ¨œ6 «ítªñÃ`Y–#æøl‚€SÅ,F;,ÂJkjiÍÖîž´ïì¹í5@­»4~6Ó“•\:w¶DŒŸÍÜÌ´aX ç3ý¡,ËAsÜà ÎE±%Õ¸áÀüëÍVa¥,Ä »÷dëΞ´w÷¤­£Ó0@Øš¬dúÝ‹¹pæT.ŸÍ¥sg3=yÑ0¬´£©^8^–å„9ªu¨(Š®,^qxÎ"¬„ÖŽm·¢†í»ö¦½»'Í›Z ¬;³×¦syü¬‚Õr:É‘Tã‡së™à NEÑ—äpª—žµËIÜpD¬’ó©^~,Ëò¸9€õFðPÊ¢X¸âp É‹°šZZ³µ»'»÷ˆ–‘‚v5óñCª×®˜ht‚€RÅ–,I6[…‡±4nX¸àÐÖÑi€U²4‚¨ŒçÒøÙÌÍL†åp4‹×Ä@C<¬±¢(º²8²£sמ´ïìÉÖîž[¯¨-S“•j1:œËçΦrfØ(<¬ÓIޤ?L˜h‚€509 $9œäY‹ð Z;¶eû®=iïîÉÖ½éܽÇ(uêÒøÙjü0z*—ÎÍåso…u:‹—FÌÔ3ÁÀ*)Š¢/ÕÈa "îSSKk¶v÷¤s÷žlßµ7íÝ=iÞÔj€5{mºzâÌ©[× æf¦ Ãý:ŸjüpDüÔ#ÁÀ š§9ì°÷ª}ç3Ùº³'»÷¦}gO¶v÷`›š¬¤2:œKãgS9sÊî×Büp¼,ËAsõ@ð°ÌDÜ/×xP•ÑaW xWSÅ@-<,ƒ¢(R ’l¶Ÿ¦µc[¶ïÚ“öîžtîÚëzËfá DeôT.;ë ÷BüÔ,ÁÀ9p¯:wíIûΞlß½'»÷ºÞÀª™½6}+~¨Œ§rfØ(|ñPS÷AäÀgijiÍÖîžtîÞ“í»ö¦s÷£PS*£Ã¹tn,•Ñá\ÎÜÌ´Q¸ñ°æŸAäÀ§iíØ–­;«C箽ÙÚÝcêÊ¥ñ³¹|îl*£§ráÌp¦'/…;‰€5!x¸ ‘Ÿ¤µc[¶ïÚ“ÎÝÕë mF ¡LMVªW ÆÏ¦ræT.Ÿ{Ó(,%~Và`žÈ»ißùL:wíÍöÝ{ÒÞÝ#p`Ý™½6]½þ0:,€àNâ`E €u­(о$‡ç_DÜ8tîÞ›æM­F€%–—ÏMå̰QHÄÀ <ëÎ’Èa É‹¬oxx•Ñá\8s*•Ña‰øX&‚`]9°@à+¯2:œ·O¥ræT.Ÿ{Ó ëÛùTã‡#eYŽ˜¸‚ aEÑ•jàp8ɳYŸ°¶f¯M§2z*F‡ˆ€û"xŠÈÖŽmÙ¾«7tïï8@@0ït’#I˲œ0p7‚ îE±%ÕÈa É!‹¬/K‡ÎÝ{ÒÖÑi¨#³×¦3~r¨AœÎôäE£¬? ñѲ,¯˜X xêVQ —DëHSKk¶ïÞ“ÎÝ{Ò¹ko¶v÷ÈÔd%•ÑጟÊ…ÑáÌÍLe}9šd0ÕËâXç@])Šâ@ª‘Ã@’ÍY:wU‡í»ªW€õãÒøÙTÎœÊøÉc©œ6ÈúòTÇASÀú$xj^Q}YŒvX¤ñµï|&»öÎ_rØ›æM­F’$•Ñá¼}r(•3§rùÜ›Y®¦zõᕲ,GÌë‡à¨IEQt¥8Nò¬E[SKk¾°ÿ`:wW/8´utøL³×¦3~r(•ÑS¹pf8Ó“ÒøÎ'9’äHY–æ€Æ&xjFQ[R’²Hcëܵ'Ýû¦s×Þlíî1ðÐ.ŸMåÌ©TF‡3þÆ1ƒ4¾ÓI^I2X–ås@ã<k®(Š…Èa Éf‹4¦ÖŽméÞןí»÷¤s÷Þ4oj5 °¢*£ÃyûäP*gNåò¹7 ÒØŽ¦zõaÐÐ8Àš(Š¢/ÉáT#‡i9PUÎÛ'‡2þÆP¦'/¤qœH5~,ËòŠ9 6€RE_ª‘Ãá$›-RÿÚw>“ÞþtîÚ›­Ý=ø S“•ŒŸJet8ão3Hc¸šd0É+eYŽ˜Ö–à¸gEQlI2ä¥$ÏZ¤¾5µ´æ û¦s÷ÞtïïOó¦V£< ÙkÓ©ŒžÊøÉ¡¼}òXæf¦RÿÎ'y%Õ«æ€Õ'x>SQR½äð kÔ·ÖŽméÞן/ìïOçî=X!—ÆÏflh0•3§rùÜ›©G“)ËrаzÀ]EÑ•jäp8ɋԯöϤ· »öfkwAVÙÔd%ã'‡RÎøÇ Rß®&9’äW`å €ÛEq8É@’CÖ¨_Ýû¦{º÷÷§yS«AjÄìµéùøáTÞ>y,s3ÓF©_'R˲¼bX~‚`ášÃK©^sØl‘úÓÔÒš/ì_Œ¨ã'‡rat8ão ezò¢AêÓÕ$ƒ©^}1,Á¬SEQlIõ’Ãá$ÏY¤þ´vlK÷¾þôödkwAêÜ¥ñ³LåÌ©\>÷¦AêÓéT¯>qõžàÖ™¢(úR½æ0×êNûÎgÒÛ?Î]{E lj²’ñ“C?Ô¯ï¤>7<Á¬K®9¼”äY‹Ô—Î]{Ò½ÿ`º÷÷§­£Ó ëÌìµéŒŸª¾¼qÌ õç|’WâêÜ7Á40×êW÷¾jàн¿?Í›Z @’Û㇠£Ã™›™6J}qõîƒàŒkõ©©¥5Ûwï9p_⇷O?ÔWà A¸æPšZZó…ý‹—àaˆê–«ð PÇ\s¨?"VCet8oŸÊøC™ž¼húàêÜAðu¨(Š®$/Ç5‡º r`-]?›kÃßϯÿU&/V R¾“䕲,GLÀz&x€:RÅá$‡“Œ˜€µ&x€ûTÅTC‡CÖ¨ í;ŸIßó/¦{š7µ>Åí-iÙðÈŠ|í™çø…™ûúœÙkÓ·â‡ñ7Žy‚jljTÇAS°VpŠ¢8œäå$;¬±öZ;¶¥· ½ýiëè4Ü£§,û‹ÇWäkÿÃälÞ¹ñÀŸ?5YÉøÉ¡Œ æò¹7=Yµá|’W’)ËòŠ9XM‚øEQt%9œêE‡ÍY[M-­ùÂþƒyöù³µ»Ç ð€¾¸¥)_ܲqY¿æ¹©ëûÅܲ}½Kãg364˜ñ7†2=yÑ“¶ö®&9’êÕ‡ s°pEQô¥9|Ãk¯{ßÁôö¤{¿1`™|¹½9۟ܰ,_ëÂû7ò“˳+öXÇOeüäPÞ>y,s3Óž¼µw4Õðá¸)”° Í IDATXI‚X¢(ŠTC‡ç¬±¶Úw>“Þþôö%Í›Z +`9.=¼uåzÞº2·*wöÚtÆOelh0•3ÞÀµw:Õðáˆ)X ‚Ö½¢(¶$Hòr’Y;­ÛÒ½¯?}‡^H[G§A`<õøcùr{sZ67JðÀºQÅ$/'yÎk£©¥5½ýéíÈÖîƒ+fj²’±¡ÁŒ fzò¢AÖÎéTÇ#¦à~ hxEQN5tØaµÑ½ï`zûÒ½¿ßÀª«Œglè{;vÔkç|’W’)ËòŠ9¸‚RQ[’¼4ÿ²Ù"«¯µc[úž!½ý_Ió¦Vƒí‰ äé– ikz4OlxäÖÛoÜü8SsçúÍ35wsþ÷7 ü’ÙkÓ?9”‘×^Íåsodm\Íbø0a>à€†REWª×"tXuM-­ùÂþƒyöù³µ»Ç À²Øþä†t>¹!Ÿ{ü±ûþÜë7“©¹25w3¿øðfÞ¹aP IriülƆ364˜¹™iƒ¬ï$yYøÀ'<Њ¢èKõšÃ7¬±úÚw>“¾ç_L÷þ~×€eóÔãåËíÍiYrÍáa]¿™¼;s#o]™Ë7üÝ(P564˜ñ“Cã˜1ÖÆ‰TÇã¦`)Áu­(Š©^txΫ«©¥5½ýé;ôBÚ:: ,›>’/·7åé– +ú}ÎM]ÏØ/æ Ü25Y¹uõazò¢AVßé$¯”eyÄ$‚êTQ‡S vXcuuîÚ“ÞþôöXvmMæŸv<¾¬W>Í{~”ÿwr6×oú{Ràvã'‡ª—\}X çSý»¿Á²,¯˜`ý<P7Š¢Ø’d B‡U×Ú±íVäàš°RÚš;â‰l|tu¿ïõ›É噚»éI~ÉìµéŒ }/#¯}×Õ‡Õw5É+©^}>¬C‚jÞ|èðÒüËf‹¬žî}ÓÛ?îýýÆVÔZÅKýäòl.¼Ó|¢ÊèpƆ¾—±cG±º®&9’jø0a€õCð@Í*Š¢+Õk:¬×€Õ¶ñÑG²¯x¬©ï$yYø°>¨9KB‡oXcõ¸æ¬•/niÊ·l¬‰Ç2sããü??Èõ›þÞ¸7®>¬™£©^|8n €Æ%x fEq ÉKIYcu¸æ¬µ'6<’ÿr{KM=¦·®\Ï[Wæ<9À}qõaÍœHõâÃqS4Ákn>tx9ÉsÖX®9µâËíÍÙþ䆚zL×o&Ç/̸ò<0WÖÄéT/>1@ã<°fŠ¢8œêE‡g­±òšZZÓÛ?¾C/¸æÔ„Z¼î°À•`9LMV264˜±¡AWVÏùT/>1@ý<°êæC‡—“ì°ÆÊëܵ'½ýéí0PSºÚ6æWŸjªÉÇ6sãã¿0ãI–ÍøÉ¡Œ fücÆX瓼’äHY–WÌPŸ¬Š¢(¶$ˆÐaU4µ´æ ûfï׿éšP³~cÛikz´fßëfòÁ  ,¯…«#G¿›¹™iƒ¬¼«©†¯êà€5:¼4ÿ²Ù"+«}ç3é{þÅtïïOó¦Vƒ5kã£ä7?ßRÓñ'—gsáýž,`ÅŒ flh0•3ÃÆXy€:$x`EVWïÁCéíÿJ:wï1Pžzü±ì/¯éÇxnêzÆ~1çÉVÜ¥ñ³9ýÚ«yûä1WVÞÕ$ƒI^.ËrµMðÀ²:¬žÖŽméíHßó/ºæÔíOnÈ—Û›kú1¾÷áGy£üГ¬šÙkÓú^F^ûn¦'/då}'€š&x`YEÑ•äå$:¬¨Î]{Ò÷ü éÞßo  n}qKS¾¸ecM?FÁ°–*£ÃyíÕŒ¿qÌ+OøP£<”%¡Ã7¬±ršZZó…ý³÷ëßL[G§A€º'x¸7S“•Œ fäèw373m•%|¨1‚ˆÐau´vl˾¯}3ÝûûÓ¼©Õ @ÃxºeC~­£¹¦£à¨5cCƒyíÕ\>÷¦1VÖw’)Ëò¸)Ö–à€û"tXÝû¦ïùÓ¹{1€†ôÔãeñxM?FÁP«*£Ãú^ÆŽ5ÆÊ:‘êŇã¦X‚î‰Ðaå5µ´¦· }‡^H[G§A€†¶ñÑGò›Ÿo©éÇxáýùÉåYOP³¦&+Õ«G¿›¹™iƒ¬áÀ<ð©„+¯µc[ö}í›éÞߟæM­ÖÛ[Ҳᑚ}|?¹<› ïßðDualh0#¯½šËçÞ4ÆÊ>¬2Áw%tXyÝû¦ïùÓ¹{1€uéËíÍÙþ䆚}|¯_˜É7üý)P_*£Ãú^ÆŽ5ÆÊ>¬Á·:¬¬¦–Ö|aÿÁìýú7ÓÖÑi`]Ûþä†|¹½¹&Û{~”7Ê=I@Ýšš¬T¯>ýnæf¦ ²2„+Lð@¡ÃJkíØ–¾ç_HoÿWÒ¼©Õ ó~óó›²ñÑÚ{\?¹<› ïßðÀ®^šÌð¾ŸÉóç²¹½#_üõ½ùÒ¯ï5LÌþíLO^4ÆÊ8‘äåk­½Ir8IW’‘$G¦ßz}Ä<Nð°Î VVç®=é{þ…tïï7À]|qKS¾¸ecM=¦©¹›ù»‹xrà£óz¾ÿ'ßþ¥·w|¾+_ÿƒ?Ìã-›ŒTã*£ÃyíÕŒ¿qÌ+à£ÇZr½yk>z¬e雿3ýÖ뇭ð`ë”Ðaeõ<”gŸ1[»{Œð)6>úHlo©©+'Ëó‹?òäÀŸ;,èø|WþÛÿõ3T˜š¬äÔ¿ûvÞ>y,s3ÓYfw þõô[¯¿d€û'xXg„+§©¥5}‡^Hoÿ@Ú:: pºÚ6æWŸjª‰ÇrnêzÆ~1çI€%>+vXð;¿û/ó¥_ßk°:2{m:#¯½š±¡ÁLO^4È2»#|ø/¦ßzýŠUîà`:¬œÖŽmÙ÷µo¦{š7µàüZGsžnÙ°¦ajîfÞ(?Ìõ›þÎüßßýÓüøß¿§ýßùj~ã_|Õhujlh0#¯½šËçÞ4Æ2ûè±–Ü|ìñß}b €û#xhpB‡•Ó¹kOúž!Ýûûð6>úHö§­éÑ5ùþb¸Ý‡3×2ôêŸfôoßóçCet8#¯½šñ7ŽcùHòrY–ÇMpo Jè°rzʳϿ˜­Ý=ÆXF}$ÿ´£9Ÿ{ü±Uý¾b¸Ý»ç'òýÿãßdò‰ûú¼ßùÝ™/ýú^6ˆ©ÉJNý»oçí“Ç273måõTÇ S|:Á@ƒ:¬Œ¦–Öôz!½ýiëè4À ê}ª);Û6®Ê÷:7u=ÿùÊu±ÌþÁ_æïþÿÏìÌÌ}îïýÙ_°Í^›ÎÈk¯flh0Ó“ ²¼„ŸAðÐ Š¢Ø’jèðß[cù´vlKßó/¤·ÿ+iÞÔj€Uòtˆô>Õ”– ¬È×ïÃòÖ•ëùŇ’\½4™ïÿÉ¿É;c?{ Ïÿâ¯íÉ¿øþGC6¸±¡Á¼ñçß>,?áÀ'<Ô¹ùÐá¥ù—ÍYí;ŸIßó/¦·Àkd㣤«mcºÚ6fã£ÿõ®ßLÞ¹‘·®Ìåƒþ^’äÙkùñú~†ð—tÕaAÿ ‡³ç·~Û ëDet8oüù·S93lŒå%|¸ƒà N VFç®=Ù÷µo¦s÷cÔˆ…ð¡óÉ ÷tñajîf®ß¬þ½ç7>Î7>Î{~äš,±\¡Ã‚¯ÿþæó½ÿİëÌ¥ñ³9ýÚ«;vÔËë_§>\1°Þ êŒÐaeô<”½_ÿfÚ::PÞØðHÚšK[Óâɇ…ÀazIèÜÝÕK“þÁ÷3ú7¯/Kè°à÷þì/Œ»ŽMMV2rô»ÌÜÌ´A–é?×$¯$yEø¬g‚€:RÅá$/'Ùa‡×ÔÒš¾C/¤·@è4¬«—&óÎØO3ú7¯ç±Ÿ-û×ÿ|ï¯æë¿ÿ?šÌ^›ÎÈk¯fäèw…ËøŸp„À:&x¨B‡åÕÚ±-½ýé{þÅ4oj54¸«—&sõò¥¼{þ\f¯ÍT_Ï\K’û:>ß•Ç7µÜúýç{w-¾oGWß´iþíÿdÍþ¼Î\Ë;?ûiÞûiÞùÙO3ùÎÄŠ~¿_ÿ­ÿ:ÿÕ ÿ´:õîù‰ÌÎÿÌTÿ›8së}÷ûßÇç{õÖ¯?š*3sáLæÞ¿jäeú?eI^.ËòSë‰à †E1êÿz—Ða´vl˾¯}3½ýÆ€öóŸÊäù‰¼3vfþsϬÙciniÉÓ;º’$›Û;²ykGõí›ZòôŽ·>®cGWoÙtÏ_÷Ùk™>ÍcÍdãì%áÃò>ëÂÔ¡Ãòêܵ'û¾öÍtîÞc h0Î\Ë[?>Uý‡Ü"‡5µ¹}«jÌBôãÿôýU¿øñI>z¬%µìÈ£7?ÌÆ¹_dÃõ«ž¨‡³#ÉŸEq8Õðá¸I€FäÂ@ (Š¢+É+IYãáõ<”gŸ1[»{Œ æê¥É ÿàûý›×3;ã)¾üÞŸý…jÄ;c?Íè‰×3ú·Çkþ±>rózšæ. –ω€äÂÀš^Nò k<¼Þƒ‡²÷ëßL[G§1àݘ›Ë￟֧ž2PÓ®^šÌßý‡_ÿ{=ini1B xgì§ù»ÿðÖÕµ“ݘÙÇ·e®ik6^¿’ בG>¾éÉ|pÏ%y½(Š£I^*ËrÂ$@#<üÿìÝtÔõ½ïûWÂd&b~|$@êDÒ*ÄÝÊA»ÚãV8îÓ®÷ªì»tÝ­ì»H{½g£í)ñlµö¬®6œŠ¶Ko¥p7ty¶n°²õTQ@vwÃ6€$-($À7 †üša2™$÷  !“d~|'ó|¬Åd2ßO^Ÿï_ß×÷Ƙ,Iå’Ö“Æø8]n•.X%ËVPtFÐÙÚª–Ó ºÐd©«õs=×<&+Ï(oF¡r •–žAhbÎçõh׿×):ØT~ÑLBˆ¡¦ú:íÚò«¸*:\­?9EþÔ\õ8³•âo¥ø0~Ë%-7ÆüZÁ‰uD ž%õ÷÷“DÉ ¢C¹¤L»KE‡ÒQjº›@€aø<]j¨­QËézù<žQ½mÎô*¾ý+L°‰)Îde§MÒg²&;’®ø»V_Ÿ:ü}jò Ê÷ÞѾ7ßP·×K6UX2[«~ðß"Úß½í{ó |oç„{ß’ú{åèiWJO«’úzØìñ{VR¥eYmD >¿N¤ðQaŒY-©BRiŒ;ošJ–­ èŒ åtƒjkÔÖlû¹f|y¶Šo/•Ãé$ؘžáÐMYN¹®*9 §ÉP“·WMÞ^õôñ³@ħ¦ú:íüÅKjn¨# ››»p‰îÿ»¿'ˆ(:~p¿vþò¥„(9zÚåô·P|¿vI•¢ø Qx€3ƬPð‡IÆÁ7Me+רd٠†ðûÕrºA'üiÔÓF’‘5U³¿¶iQ”6I·ç¤†\tJ“7 Sµúz qãÀ{ïh×–M'î~è;ºûo¾CQàóz´kóë:úñî„{ß)>„M½¤ ˲øG@Ü ðbŒY¢àD‡Å¤1v€‘ü~5ÔÖètí1‘»ÐáHÑW¿ñ-JQP’íÔ¬))a{¾Ï}½ªmõ«ÃßG¸°-Ÿ×£¿xI' Œ8Bá!:˜z4)Щ«&õz9)Ƨ^Òj˲v»£ðafŒ™©àD‡å¤1v€‘E«è0¥‡È»='UÓ3yîšV¿ê:¸;6ì§½¥Yoþô¿'üÅÜñˆÂCä5ÔÓ›?ý±º½\äɤ^¯Rº[(>Œß'>ì& vEáÂd èP!éQÒ»‚Ûæ«låÌOÀ0bQtÌáHÑ×ú¶N'›f‘,;\r¦+ #ç» ¶ÑT_§­Ïý‹¹ã…‡È:º÷#íüåF‚ҰÙ!©Ü²¬:¢`7`œŒ1Y’Ê~e’ÈØPtBsî³OuòÈŸäóxbºŽ¬<£;¾ñM6$Œ¦g8t{NjTŽu¢­G'Úü„Ž˜£ìÿVýàY–Ì!ˆ ì:Ša³AÁ‰mDÀ.DcgŒY-©RÆŒ¢».ik¶tî³Ouã—nbs %9I%Ù©Q;ÞÍY)úÜ׫V_/á#f(;ãì0:½“\êu)¹Ï§«=í„26k%­6ÆTJª¤øÀ(<ÀcV(Xt("±¡è„æB“¥šßl›¢Ã`'ü‰ÂC˜”d;•’ÝcÞž“ªÝg¸Ð±áózôÖOLÙBS}e‡1êKNSwÚ4ù¹rú[(>ŒM¦¤õ *,ËÚD$b‰ÂŒ‚1f‰¤ I‹Icl(:¡;~p¿Nÿ¹Æ¶ëóyŒ_‘¤×1å’Ê-ËÚM$b„À3SÁ¢Ã£¤16€Ñ©ùý>;ù©í×Iáaüò]“bvì™SR(< êö½ù†jk“O‹âCXÌ“ô‘1f‚Ňj"Mà:Œ1Y’Êá1 èŒ^¼”$éBÓ96lœbYx˜âLÖdG’.úÙDES}ö½õAC8ðÞ;”"„âCX,–ô'c̯%UX–UG$¢Â c`Tw…¤LÒ½’¥Ë5ïÁG”[üeÂFáÜgŸÆMÙA’|›6NÙi±ý‘Ý i“˜ò€¨ÙµåW„ õñÔëѾ7)EҰxTÒ cL¥¤J˲ڈ@$Qx€«cVHª”TD£W²t¹¬Z£)y„ŒRÀï×ñUq·îÎÖV¹³³ÙÀ1JIŽíñÝÎd6QqtïGܽƾ7ßP·×KQBñaÜ2œŠ¼ÚSaYÖ&")`€1¦TÁ¢ÃbÒ½‚Ûæ«låÌOÀ?¸_@OÜ­;ÐãgóâØ ˆ’ïî$`>¯GG÷~D1@ñaÜŠ$½>0-¹Ü²¬ÝD Ü(<HxƘ™’*ÅQ¢è„GÀï×¹“Ÿ€ ©©¾NÍ u áèÞ˜îcÆmž¤Œ1{$­¶,‹ð„ … Ë“%©|àW&‰ŒE ¼Î}¿e‡©ù† Œcþ>B@Ä|÷B†}}0ýÄ.(>ŒÛbI§Œ1$UX–ÕF$Ƌ€„dŒY­àT‡"ÒŠ@dœýì!$¨ž>)%9vÇï¤ð€(8~h?!ChoiVûù‚°Šã¶VÒjcL…eY•Ä`<(<H(Ƙ% “ÆèPt"'à÷««íB\®=+éãÕê (ß»Û}îëeQMõuêöz e {£ø0.™’~fŒ)—´Ú²¬ÝD`,(<HƘ™’*%-'Ñ¡èD^ç…Ö¸]»;;› §&ooÌ Ÿûzu1ÐÏ& ¢šëOÂÖÞÒ,•Ì!ˆ±¾>êê!\*>R²”ÒÝ¢I½”¸F¡HÒGƘ=’Ê-˪&£AáÀ„fŒÉ’T.i=iŒE z.XVÜ®}j>ƫɻ ]6×ÞÒBì/†Ëï|3!Ä‘ÞI.õºŠ4©×KñaôKú“1æ× Úˆ@((<˜°Œ1«œêI¡sçMÓ¢ÇÖ©ø®e„ຎåÎ($ˆqêéë×™®€¦gD÷GwÞ@ð¸@¤51á¸Î룎âŇqyTÒ cL…eY•Ä`$L8Ƙ%’*¼cBäΛ¦²•kT²laÉŒ’9„&µ­~å»JIŽÞ1œï&xDE·×CÀ°¯.”gÆ,SÒÏŒ1å’V[–µ›H ‡Â€ Ã3SÁ¢Ã£¤:ŠÆÂáHQaÉl‚“ž¾~Õ¶vëöœÔ¨ïTGZ}½@\.>:•Úݤ¤¾B M‘¤Œ1{,>Ô €«Qx÷Œ1Y’Ê~e’Hhœ.·=¾Ž¢€1™Q2G§“ ÂèLW@Ùi“4=ÃñãÔ¶ú €0ëu¸åu¸åèi—ÓßBñ!t‹%2ÆlTaYV‘¸$™Ä3cÌ IÕ’Ö‹²CHœ.·¬|R«ÿß÷);6‘gŬ<£ây¥l\9ß­3]ˆ=ÿ‰¶9ßMЈªÌœ(÷õêöœT¹Ic~ž&o@§:jõõ*b† º#¯°dv0ëœ IDATzIå–em' qPx`[Ƙ™ Ntx”4BSpÛ|•­\£‚¹ó ˆC‡~÷žÚš-Û¬'+ÏhÎ×ïVZz›`\vþâ%ýx7AŒRaÉì ¹ï ëóú¼½õÓ«¡¶fÜÏEáaü(ÍÜ…K4wñ=aŸ"ÓT_§¿xIÍ uã~®ë.éhnÔǯ¾¨“U²©¡Û#iµeYuDL|Ø’1¦BR¹¤LÒYά[µè±§):q®³µUûÿõ혯#-=]Å·E7~é&6@X4ÔÓÖçÖDˆæ.\–i# G…ÂÃøí{ó í{ë ‚AªË¥ùßüëqOs‰ÏëÑÖ\?îÒÃCßý/ºåÎ!=¶ñèUmÛ¨ÆO°Ñ¡{VR¥eYmDL\ØŠ1f‰¤M’ŠHcdî¼i*[¹F%ËV0Aœ®­ÑñCûcrl‡#E3J樰d¶N'› ¬¸‹ýÈæ.\¢»ÿæ;½ûjã-=Px¿ö–f½Rþ$A\Ç¥¢Ãߺ_i®ô¨3¥‡±¼>ÐÞ×~¤ó§þÂÆ‡¦^R…eY›ˆ˜˜(<°cÌLI•’–“ÆÈœ.·ÊV=©Ò! `ªùý>;ùiÔŽGÑ@4Ýû‘vþr#A !E‡Á¶>÷C5ÔÖŒém)<„G8¦mLD±(: æózôÊÚ'ÔíõFýõQ»k»ª¶mTgóYN„Ðì‘TnYV5Q …1gŒ©T.)“4®Ïér«tùÃ*}𥦻 ˜À¢Qz è Ú~õôSãº[úDë¢Ã%>¯G¯?ýÔ˜&pPxö–fýê™§Æ|aýDë¢Ã`MõuÚúÜÇ´7«~ð¬ KæŒëøÕooVÕÖ—å÷vrb„fƒ‚Úˆ˜(<ˆcÌI›$‘ÆÈJ–.×ÂÇŸ¦è$ã÷ëôŸkÂþ¼ÄJS}^æ©„ÏÁ.E‡«÷f,uSxŸï½£][6%tv*: 6Ö 5á(Pt`7‰tQwªË¥¹‹îÑüoÞoÛ¢Ãxö†ÂCxù¼mýÇõjn¨Kˆ÷73'Ww~ë~Í]tm‹ãÙ›p.éhnÔǯ¾¨“Uò¢ ÍI–eµ(<ˆ cÌI›$‘Æõ¹ó¦iÑcëT|×2Âp…€ß¯–Ó :ûÙ§jk¶®ûØ´ôtMÍ¿Q¹3 •;£ðØÎ¾7ßо·Þ˜°ïßÍwÌ×-w.ÐÜE÷ÄÕº}^^ú)µŸo éñ™9¹úÛýÄö«ÇÛìÚüú„-¥º\ºåŽš»øžˆ”"©©¾N¯?óÔ¨þø›ïýCÄÖÓxô€ª¶mTã'xጬ]Ái›ˆˆ/D”1f¦¤JIËIãúœ.·=¾N%ËV€t¶¶*Ðã—¯«K»º4ÕIÒäŒ ¥¥gÛ;~p¿vþò%u{½âý¹Tr(,™Ó†ÓP{L[Ÿ[?ª÷;’u'ª‰T ºTr(œ=G7ß¹ ® 2£Ý—e¯ÖüoþuD×T»k»ª¶mTgóY^8#Û£`ñ¡š(€ø@á@Äc*$•KÊ$á9]n•.X¥>¢Ôt7€„Ïw³/,™­Â’Û”W4S·Ü¹`BíË[^×Á÷v†üø'*_Žë’‡]5Õ×i×–_©¡¶&®Öêr)¿h¦ KnÓÍw.P~ÑÌ µ/[ŸûaÈ{’™“«'6¼•uU¿½YU[_–ßÛÉ‹gd$UX–ÕF€½QxvƘ%’6I*"ë+Yº\ š¢Hxí-ÍÚ÷æ:~h¿í&>\ºx;¯hÖÿè~õôSjn¨ 鱫~ð¬ Kæp"GHCí1x÷8tÀvkËÌÉUfnîåòO~ÑÌ _~ioiÖ¯žy*ä«ÖýÓ?GmmÝžNU¿½Yû·½Ì gdõ N{ØN€}Qx6Ƙ,‹ËIãú n›¯{˟ה¼ÂÄçõèÄÁýj¨9ÕòCaÉlIR^Ñ,¥¹Ò•W4Siéé }S}^æ©û·/ü$!J ±ÖÞҬ㇂¯h•.~‚¯“Û‚ÿ=G©®ô„Þóï½£][6…”ßw_Ýõõu47jÿÖªýp/œ‘í‘´Ú²¬:¢ì‡Â€°0Æ”Kª”IÃË™u«=ö´ æÎ' €4Õש½¥YÍõujª?¥n¯Gí--j?ß2âÛ¾P[úâbíÌÜ\eææ%üÛ¡å¢îÌœ\=±áІÚcjoiV{K‹j?¹üš ¥(ti2Cðµ’®ü¢Y’t¹ìÃëcdoþôÇ#Oæ.\¢ûÿîïc¶ÆÆ£Tµm£?9À†ìY˲*ˆ° ÆÅSªàT‡y¤1<§Ë­E¯Sɲ„€¸²õ¹ª¡¶fÈ¿Ku¹´êÿ ã‘|^^Yûݓ̜\ýí~¢4WzÌ×zò»´÷µÕÙ|–»¾z§=ì& À(<cL–‚Ö’Æðœ.·J—?¬ÒQjº›@w|^¶þãz57Ô]ñÿ3srõÐ÷þ²ZS}¶>÷ÃkJ…%³õÐ÷þÁe‡ÁªßÞ¬ª­/Ëïídó®o‡‚Ň6¢b‹Â€Q3ƬPpªC&i ¯dér-XµFSò „,;mÒåßúúÕáï#ÄœÏëÑѽ©¹®N’T8{Žn¾sí.æb¡½¥YG÷îVSý)eææ©°dŽn¹sm×ÛíéTÕÖ:üÛ-lÞ[+©Â²¬J¢b‡Â€cf*XtXLÃ+¸m¾>¶N¹Å_& 0¢ÉŽ$Íœ’¢Ò&iŠ3yØÇuøûÔ3P€ô}ñçV_/!F­£¹Q¿ú¢NV}H×wXÁiÕDD…!1ÆTHZOÃsçMÓ¢ÇÖ©ø®e„F4Ù‘¤ÙÙNå»ã~®Ÿš¼½:ÓÕ£‹¾ç ]ãÑÚûÚtþÔ_ãú6(8ñ¡(€è¡ðຌ1KœêPDCsºÜ*]þ°ÊV®! 0¢”ä$Ý”•¢YSR"òü'ÚzT×Ñ£ž>¾÷ ]í®íÚûê‹ò{; cxõ’Ê-ËÚN@tPx0$cL–¤JI’Æðæ=ð°ÊV­Qjº›0Àˆ¦8“õÕ¼4¹I=Ž7Я?6ûÔáï#t@Ⱥ=ª~{³ªwl¡øp};,>ÔY\óZÁ²C&i ­à¶ùº·üyMÉ+ ’é•d§*%9zÇ´>€gŒY"©RÒ<ÒZÉÒåZøøÓJMw•”ä$-™îRJrìײïìEuøûØÀ˜t{:Uýöfíßö2a ¯^R¹eYÛ‰ @‚2ÆdIª´–4†VpÛ|-|lr‹¿L`LnÏIÕô ‡-ÖÒáïÓ¾³ÙÀø¾¾lnÔǯ¾¨“UÆðvHZÍ´`ü(< ȳBÒ&I™¤q-wÞ4-zlŠïZF`̲Ó&é.“f«59ß­3]60nGhïk?ÒùS!Œ¡µKª°,«’(€±£ð$cÌLI•’–“Ƶœ.·J—?¬ÒQjº›@À¸”™4Ý6ÉVkòúµûŒ—Í„MõÛ›Uµõeù½„1´= N{¨# `ô(< ÂS.©BLuRqÙR-||¦ä·ÉŽ$Ý3Ýe˵ýÁò©Õ×Ë&¦ÛÓ©ª­uø·[cxÏZ–UA ÀèPx&¸©›$-&kå̺U‹{Zsç›’l§fMI±åÚÎttä|7›»–“Öǯ½¨ÆOÆÐK*·,k7Q¡¡ðL`Ƙ IëIâZN—[e«žT郻û Ó•’lϵyýÚ}ÆË&"¦v×vUmÛ¨Îæ³„1´ ’*,Ëj# àú(<1f‰¤JIóHãZóxXe«Ö(5ÝM ì²Ó&é.“fë5~tÆ«‹¾7 ˆœnO§ªßÞ¬ýÛ^&Œ¡ÕKZÍ´àú(<ˆ1&KR…¤µ¤q­‚Ûækácë”[üeÂss–S7g¥Øz°|jõõ²Y€ˆëhnÔ•ßWã'ch;,>0í…`‚˜ê°IRi\ÉérkÑãëT²la€ˆ»#/Uù.‡­×x¢­G'Úül jÐûžQgóY¸V»‚¥‡íD\‰Âç¦:TJz”4®5ï‡U¶jRÓÝ„¢¢Ì¤é†´I¶^#…@¬TmÛ¨ê[ä÷vÆÕ’'íR_ïc–eÕDáˆcƘ NuÈ$+Ü6_÷–?¯)y„¢ŠÂ××Ñܨ_}Q'«>$Œ«%%w÷;Ò~ÞtúäÿC… . LuØ$i9i\É7M‹[§â»–ˆ  ­­j9Ý  M–ºZ?W У´ôt¥¥»59#CiéÁÿfdÈ=5[§“ÐNãÑzÃ3êl>KW›”úYrÊM§?­% $2 @œaªÃð¬|R¥>¢Ôt7a€˜)ÉvjÖ”[¯±¦Õ¯ºŽ6 €8÷Ù§j¨=¦®¶ czû¬<#‡3EÙù7*+ßÈM¨€¸Wµm£ªwl‘ßÛIƒ%%w+9åE«±¾‚0°ŸSxâƒ1f¦‚E‡Å¤q¥‚ÛæëÞòç5%¯€0@Ì建#/ÕÖküƒåS«¯—Í Š:[[uüà~µ5[a}ÞŒ¬©*,™£¿t!âZGs£>~õE¬ú0®–”üõ÷=aYV5a á>¦ðØŸ1¦\R…˜êp§Ë­ûÊŸWñ]ËØÊ}…éJI¶ïúþµÎÃ&E§kktüÐþˆ#+ÏhÞ’¥r8ˆkGèý Ϩ³ù,a\ëY˲*ˆ‰„Â`cLuÞ¼VÙª5JMw°’l§fMI±åÚš¼jîf“ˆ’šßïÓ¹“ŸFåXGоúoÉMð€¸Wµm£ªwl‘ßÛIW:,i5Ó((<6ÅT‡¡Ü6_ [§Üâ/°­ÉŽ$Ý3Ýe˵9ß­3]6 €(8y¸Z§ŽFÿZÄÙu·nüÒMl îu47êãW_Ôɪ ãZL{@B ðØ S†æt¹µèñu*Y¶‚0@\¸='UÓ3¶Z“7ЯÝg¼lQp¡ÉÒß/vŸ‹,^ªÜ…l`B8ù‡]ÚûÚ‹êl>KWbÚ&< €0ÕahóxXe«Ö(5ÝM nLv$éîi.¥$ÛgMLw :~¿ªvîÏã‰ÙŽ}ý¡oËát²!€ ¡ÛÓ©ê·7kÿ¶— ãZL{À„Eá°¦: -gÖ­ZôØÓ*˜;Ÿ0@\º9Ë©›³Rl±–Ÿö½È¦'WëÔÑØßhùÆâ›4ûkw³!€ ¥£¹QT~_Ÿ Œ+1í… Ƙêp-§Ë­²UOªôÁGĽ»§MÖglÇ<ôôIUÖEuøûØ¢`ÏoþI@-ÖòõÿøŸ”–žÁ¦&œÚ]Ûµ÷Õå÷vÆ•˜ö€ …Â#LuZqÙRÝ[þ‚RÓÝ„&„ÉŽ$Ý=Í¥”vŽœïÖ™®›@œûìSÕüû>Û¬ç–;hFÉl60!u{:Uµu£ÿv a\‰i˜0’‰ˆ¾©Õ¢ìp™;ošz~“îÿþÏ);€ åb _UÖEõÄh¸Â©ŽÊDQóéz[­çìg'ØÀ„•šîÖ¢Ç×é¡ç7)gÖ­ò…y’þdŒ© Ä;&<QÄT‡¡-Xù¤J|„¢˜Ð¦g8T’ÕI'Úzt¢ÍOøDÑžßü“[­iÙÿ¾š$„ê·7«jëËò{; ã L{@\cÂ%Lu¸VÁmóõèk¿SÙÊ5”À„w¦+µI=}Ò‘óÝ”ˆ2Ÿ§ËveIºÐd±9€„Púà#Zù?ÞTqÙRÂøÓטðDS®åtGL—,[A áLv$éöœTÝ6)"Ïßä ¨¦Õ¯‹¾÷ @´]h²ôÇ÷߳ݺ¾zß755ß°A€„rò»´÷µÕÙ|–0¾À´Ä @cVKª””IA%K—káãO3Ñ$¼éÝ”å”Ë‘4îçêð÷és_¯ê:z(:Cv-<Ìš[ªây¥l át{:Uµu£ÿv a\éY˲*ˆñ€ÂƘ,§:,' wÞ4Ý·öÌOƒLÏp(ß5Iù.Çu×áïSO_¿:ü} ô}ñçV_/!`°§–“ÖžÑùS!Œ/ìQpÚCQÀÎ(<afŒY¡`Ù©¬|Re+×À¦8“åHþbâC` àâÇ®ÿo“íÖtûâ¥ÊQÈæ^õÛ›Uµõeù½„Ô.©Â²¬J¢€]Qx„©×*¸m¾î-^Sò $„=¿ù'=¶ZÓ‚ÿð ÜÙÙl’:šõñ«/êdÕ‡„1èS1í6EácÌIÛÅTI’ÓåVÙª'Uúà#„ÊáÝ»tþÌiÛ¬ÇáHÑâÿü¿±1\åäviïk/ª³ù,aµK*·,kQÀN(<ã00Õ¡BÒZÒ*.[ª{Ë_Pjº›0@Â9][£ã‡öÛf=7ߤÙ_»›`ÝžNUmݨÿÝB_ءഇ6¢€PxÆh`ªÃ&IE¤!¹ó¦iÑcëT|×2 ËçéÒ¿ýË?Ûf= þÃrgg³1\GãÑÚûÚtþÔ_#¨]ÁÒÃv¢@¬QxÆÀS!i=IÍ{àa•­ZÃTI‡wïÒù3§c¾Ž¬<£;¾ñM6€UmÛ¨ýÛ^&ˆ/üZR9ÓK€Q0Æ”*8ÕaiH9³nբǞVÁÜù„0 ³µUûÿõ혯ƒéŒ^Gs£>¨ü¾?9@Aõ N{ØMˆ @ˆŒ1å’~FA V>©²•k`Çî×é?×Äìø³æ–ªx^)ÀU¿½YU[_–ßÛIA,Ë*'D…`Ƙ™ NuXLRÁmóuoùóš’W@Ãøý:ô»wÕÕv!êǾ±ø&ÍþÚÝlãÔÑܨ_}Q'«>$Œ Ã N{¨& D …à:Œ1«%UJÊLô,œ.·ÊV=©ÒáÄA,J”{òy=:øîN5Ô~¢ö–eææjî¢{4wÑ=„ēإ÷+¿Ï´‡/|ײ¬Jb@4Px†`ŒÉRpªÃrÒ`ªÀXü~Þý¡Úš­ˆÇáHQñ¼¯hFÉlBlÆçõhë?®WsCÝ5WX2[}ï”æJ'(Àæº=úøÕ©öÄ´GÁiuD€H¢ð\ųDÒv1ÕAN—[÷•?¯â»–qbŒÃÉÃÕ:u´:"Ï}cñM*žWª´ô ‚læze‡Kîüæýº÷á¿%, N4= ÷7<£Î泄!µK*·,kQ R(<¦:THZKRqÙRÝ[þ‚RÓÝ„>O—ŽýÛ¾qO{ÈÈš*wö ššo”;£P§“pjª¯ÓÎ_¼tݲÃ%å¯þš)@éötªjëFþíÂÚ¡à´‡6¢DÐ-Wýù¢¤ÓÄL|IƘRI›$ÍKô,ÜyÓ´è±uLuˆÎÖVþsZêô ù˜¬<#Išš?ð_cäHqÊM€@hª¯ÓÖç~¨n¯7¤Ç¯úÁ³*,™Cp@œaÚÃê,=ì& @¸$•üºEÒäë<öŒ‚ŇK¿>ø`‚ ð€„gŒ©´ž$¤y<¬²Uk˜ê%>O—.vu]þ³{j6b´,K»º®Ý“ìl9Rœšj ûƒÝû‘>ØòzÈeIúÛ~¢ü¢™„Ä©ªmµÛË´Á²¬rbŒÑ ’ôWax®ã ŽKª–ä%^ >Qx@Â2ÆÌTpªÃâDÏÂ7M÷­}Asçsb !ü~5ÔÖèÜÉòyØðŒÎŸú aH‡œöPM€¹$}Gá): 墤]¿(>q†Â’1f…‚e‡ÌDÏ‚© T¿_ZuÁ²®¹ þÔ|£ÉšjŒÒÒ3 ¶>jktºö˜ž1?OZzºn¹³L¹3 jª¯ÓÎ_¼¤æ†ºQ¿íÜ…Ktÿßý=!Ó.k—TaYV%QFp‹¤'%MŽÂ±>WðZ‘ãÄÄ H(Ƙ,I•’Mô,˜êBu¡ÉRCí1?s:¤ÇgdMUaÉî€ÛélmÕ‘=»F5Ña$9ÓghÎ×Êátp‚Ú÷æÚ÷Öc~û‡¾û_tË ˜@˜öp…=’VX–ÕF€!|M±¹~ãI¿%~ >Px@Â0Æ”JÚ.©(ѳ`ªÅ…&KÇT©«í˜Þ>-=]³¿¶PSó a"æÎ}ö©Ž¨×T‡áddMÕßø¥‡stïGÚ÷æj?ß2®ç)õ×Js¥(01íá²vI«-ËÚN€AbUv¸äßœöÀæ(< !c*$­Oôrfݪ{×¾ Üâ/sR€ë:~p¿Nÿ¹&,Ï5ã˳¹{9bªåtƒŽìù0¢Ç ô8ÂUt¤¼Â™ú?~ôB&°ŽæF}Pù}5~r€0¤ ’*˜öT*é ¬ƒÒ(<`B3ÆÌøâtq¢g±`å“*[¹†“\WÀïס߽;æ©ÃábpÄŠÏÓ¥ªßîˆÈd‡«eåÝñoúÔÞÒ¬ã‡öëà»;ÃRt¸dîÂ%ºÿïþž€PýöfUm}Y~og¢GqXÁiÕœ°nô_%M¶ÉzvIzƒmì‹Â&,cÌ Ë™‰œS@¨"Uv¸„Òb¡ê;§‡2kn©Šç•&D¶í-Í!_ü_X2'.ß¿†Úc:~p¿NŠÌÙ—=¼Zó¿ù×¼PÁ´‡+|ײ¬Jb€„ôKºÅfkzEe<À¦(<`Â1ÆdIª”ôh¢gÁT¸–ÏÓ¥–†5Ÿn$µ5[JKOWZº[îìlÝX|“ÜÙÙ…„tx÷.?s:¢ÇÈ™>Có–,#lDŹÏ>UÍ¿ï‹ê1Ž•=°\iéqŸ_Cí±`©¡¥EMõ§Ôíõ¨©¾NÝ^︟»°döåߟNÅ IDATçÍRš+}à÷3•–ž>ð˜è–$Ú[šÕT_§†Úcj¨9¦æ†ºˆsÕžË2€ñaÚÃe{$­°,«³Æ×dÏk9>—ô ÛØ…L(ƘRIÛ%%rLu€kü~"¦ùt}LßrºA7~é&[fsüàþ`ÑáÐN”º4µ@b+}ðßµL;Ÿÿ¿tþÔ_9ŠµÆ˜%’V[–UÍ™Ò2›¯ïI¥’ø8ØL2 žcVHªS‚—æ=ð°Vnx‹² áäáêQ—. zTóû Þ¹“'¢ÌÏ>%xDLÀï×ù3§cº†X.®æóz´ïÍ7ôÊÚ'ôÖÏþ;eHu¥IÒ”¼­Üð–¬|2Ñ£˜'i·1f5gL87Hšë,e«ûaÂâ–1¦RÒÚDÎÀ7M÷­}AsçsBÀ~¿N××stµ]йÏ>µí]ºñòyºäóx¢~Üs'?Uñ¼R¥¥g° »Î ­1_C¬ —_ã^¾»SÞ{GÝ^/'‡äÍ"W([¹FÅeËôÁ†gyÚC¦¤×nz¶Ú²¬6Î ˜n“uÞÂVöÄÄcL©1¦Z ^v¸4Õ² ¯åtƒžq?ÝîÒ „SgkkBÛËJø×—$xï½²ö í{ë Ê'r‹¿Ì´‡ å’ª1Üi&†x)Ü ÉÅvöBáqe`„ínGÚ&$wÞ4=ôü&-z|RÓÝœpšÂsÁ«]îÒ DB,/È>ûÙ 6áótÙbLŽÛP{L¿zú)íÚ²‰¢ƒåÍ$Ã*[¹Fÿ¹òMå̺5‘c(’ô'cLgĽâh­ÓÙ.À^D€x`ŒÉ’T)éÑDΡdér-|üiŠ$ˆÞ¾>©?|Ï××ׯþ¾þˆ®9x eØÛlj”@’’“”œ”dÛõ%OJR´—] ðµ?¢£låM¿mÞßðŒ:›Ï&j ‹%ÕcVX–µ›³âN<\l`/`kƘrI?Kä ŠË–êÞò˜ê»þþ~õ…p·ÿþþÐ/ÞíÝý¹û> âú¶šïb@==1Íf¼“LÆ3idÌC’¤IÉÉœØAG÷~¤¶¼®n¯—0€QšìHÒÌ))šž‘¢”!>e¹!mÒˆÏñ¹¯WýjõõªÉÛ{¹„SÁÜùÁi[7êðo·$j ™’>2Æl°,«œ³ 1Px€-c²$m’´ÒÇ¿\/¿·+!¿ä“ô/Ƙ –e•s6€­}Gk=ÍvöBá¶`ŒÉRpªÃâDÍ gÖ­ºÿû?gªÛüƒºÁ®WDnB(w%ˆ„¦ÍÐ)Ç!ã»Àì†îB‰+=+[‡sT¯“þþþwý¿ONNÓEOOÈÇ4éÚÄ50’®}ÜPo‡‰Íû©<GJÄÖAÙ›h–»9+E“I:r¾›Mˆƒ'1 Ub°ÓMW ï¸Gß®¼S»~ö=Yµ‡uËÖL_aYVg0ØÒqIGk`#s߀ܮà]XÒ‚•OªlåN£ÖÛ×7äÍa{×6úû5äá(#€‰ÌátjÚ-·ª¡æè˜ŸÃ•9Uù3¿D˜˜Ð² ¦‡eʨ^Ÿ§Ò³Fw1øPwGzÆõ'V$OJRÒ ÑÉ“’4xhÅ$Ç ¦Mħ©ù&ækÈ2‘YÃ÷Þ¡ìŒA¬Ê—LÏp(%Y:ÔLé!–.}?uðM]z{û¿(4D`C48]n}ëû¯êØ{[UýÖ/uÚïGoþôÇl,0J)ÉI*ÉNµÍzfMIQ“·W­¾^6gw½È0¹Ó´ü…ßèOoýRÕoý2QcxTR©1fµeYÕœ`»þ¶«Ï%ñq°! ˆº»ªlWð.+ Çér믿ÿsÌÏÉ„Ù%…«&(ô÷ëšI ü` `âs8º}ñ}j¨=¢³'þ<âã3sóU<ïÊH(…³o×Ñ=ïGéXs•–žž0Ùž(ÑÂõ­­Íj:uRíÍMê¾Ø¥®„’“¾˜&1T9â’´ôt¥¥»59#CS󦣴ôŒ„;§oüÒM:yäOòy¤noWèo”tiv„‚“#†)GddMUaÉÝø¥›*ÓsŸ}ªšßÕc:)*{`yXK& µÇ´õ¹õ¼H&˜uÿôÏ„a“IºgºË–k;r¾[gº.óþþ~õ]žÄ0x:ƒÔ¯þknFƒÈó{;õ§7©šÿµ5‘cøµ¤rËúÿÙ»ÿèªê;ÿ÷¯½Ï„“`0ø ˜(B0XK,µ× -:•¶ˆ¥k¾ß~]µÐ;«¦·÷®Ua¼³¦£SàûUç»:Ø5s;2¬ï«·Ã·ó-ÚVEs§(…JüAD!ŠìðKÉIν÷ýã$1@NÎ>{?kQ$œœ³?ï÷çМ}Îk¿cì(¹y*„‚f—¤ =@0xÀˆ0ÆŒQaªÃ Q\25Zs¾ý×jšw+›¡rê›Wƒ>ŸÑÇ×0Y“gÌ:m?,K²íB² AIŠÅOùše—MO=ª?þûSÊçsE¬1ãŒfþÙÍÃzŸLw/Å7wRJ©xpƒ\¿íL+Àà ž'>^Ùô µ­Y®Îm›"û£— “Ö± ¤.‘tO€ŽçŸ%ˆŒÀŠÊ³BRdß›ý_¾«kþËÿÉF@Ñœm`7¦C1Üê§_¥úé3(ì‡tíÝ£Ý[7—ìñ‡câÆàtˆ #½Ó©Wž¶¨Q=æBÍü³ùÃêyò¡Ò«m›x„‡âº iëú‹Gú·̨+/úã Nê%Ì€A¯?ýSmßðP”§=<â8Îv”Ôç$-Àql–´ŽvÁFàEaŒ£ÂT‡¢¸þÑã.Ö—ÿæ5vòl|¢‡\דNy©6øÆ“Ä„ÀÈÚ¿{§Þyý•sž>P3v¼&Ϙ©ª1µóCŽìG›Ÿ/ùq Gèa(‚Œ8°çMíúÃKE™ôP¬°ƒ$­þö7•I§yÒ„‡âºô‚„¦×{ªÐîc9í>vþ~\·p‚ÙÍüîú’/yþ/œªçÐ{Ú¸z™ŽvîŠj Ú%Ýê8Î^v”L©C„€2AàÃÎ3W…°CM×ß8÷ýoKþJɪÑEø þOo#Ø>Æ#ý®^rúÏx›Á æ žsö}És}Î3cؼ¼a¶oXÕåwKZâ8Îì(™R…6JúåÊ +cÌ’VGqíÉTµZZWª~æçÙp ß÷åûžü+Íyž7ðuO:Íy ˶%Y²lK–,Y1»p¥ÏóüP €ó×ßÛ«îCŽ2éõ÷ö*“.|h¼fì¸ß !‡!øãoŸTºûý@Sýô«T?}FYÔϲ$Û>÷`D>›Ugǽýêöó:ŽÊª*5κFc/©/ÚZ÷èÃÚúô“¥èkøé½?PgÇž8!Eࡸ®1•º¨2VÒcðOþä œ#œ Œ1K$=ŵ×Ö7ª¥u¥j¦±”§i ’ä»…ß a†‘0œ-Ïuå¹®,;¦X‚àCØÕ¥âª©Œ)9pÖcý®rWCÌy¾ºû]Š€²Ðµï­À†$©ûP—ú{{C5åaH/=_‰ÊQ[ÙogÙ–r9K¹\NvÌ’eI²¤X¬°mkØ'vÎ]~No G áŒÿîöçigêœ[4¡i–6®^¦£‘»v¤çŒ1+ÇYÁn€’8"i¤ŸIjøÕ(iÔ¾ç]‚ ƒ¿Žˆ€*pNŒ1c$=(iq×?¥e®¹ý.%S£Ù gp2ƒïù'ßyõ}ïä›°¾ž‡ûž«|Æ•‹)žHJÃüÁ”N]*®)µš0úì®Äx¸·ðAÃé¼¥ó:œæƒŽ÷vï þ1¾Ù¡É3fѬӾõå>þ,÷/­?ú:{0aÙ… „¤„UIØ6…ºTÍØq¢Hú†˜xáÃÌ–mɶN D¾‹ˆˆ‚îÇ<ÑñìÀdÓÿ)E á“p¡ÝÕ‹Z5¡i¦ÚÖ,WÏáQ[þ ’¶cnug;»a%¢‰³$8+Ƙ[%mRæi¦Þ·ž°€Òñ}¹ù¼òÙŒ²ý}Êõ÷ÉÍeåæs…© ¾ùúä³¹¹{¥L]5~”ZªÏ9ìp:‰˜­+ÆVê —U«©®’" dº9eqœ™tú{{iØH½”õ|¹®'×õ”˺Êf ¿úzsêëåõ-0¤ç‘_xår®²™¼úû ÏŸGû•îÏ+Ÿó”Ï{ò\_žëË÷ƒv¤÷zxž#øLÓ,-¼½êgÎâò$½lŒ¹“P:LxÀc”ô½(®½yQ«®^ÔÊ&PžëÊsó…iøDn>'ÏsO$eÙ\ë¡$lK×NªÖ ÃGc ø0at\mûz•ó| €UÓN=Öʪ*š <ß“ïIžçË÷ Ïõå«`8“=9Õ×Tr]霧î~Î÷¡<$S£5oé*½þôOµ}ÃCÊ–ÑÏ5Ãdµ1¦YÒŽãcGŒ,ÞõÀ'2ÆŒ1ÆlRÃÕu´ð¾%ì $Ü|^Ùþ~å³ÂgÉ÷<å²¹ù<Å(Å;œª¦2®–†*%l‹Â`Dõ¼ÿ~Ùk¹L£ˆ‚‰S›(Bíä„“é ™þüÉ '½'2ê9žQúDáÏ™¾¼²W¹¬+×õ>1ì IûŽw‚›G3l”+o¾Móï^«ÚúÆ(.±¤MÁŒ &<àŒNÚm’Tµµ×Ïœ«–Ö•J¦F³Œ(Ïu•Ïå$ߣçÃ÷åæ²’¤XœS AuÕøQ#v4z`ÒF’›ÏQœµÑÕI»©CQ^ÖUÍØ±ªoúÔiÿ¾kßÛ:¸o¯º¢Xgapƒô§‰ úïÁ¯{òGèåøát^霧T"X×͹ž:eÙ0ç ŸÍª·û}urÔßÛ«LºW’ÔûþûÊç 5­;^©*]tñ%ºhâ%m˜Õ6LÓÂû×ë¥GÐŽg~µåÏP!ô°Äqœ'Ø #ƒwûð±Œ1wJZµu'SÕšý»4uÎ-l#Ë÷•Ïe™æ0ÌÜ\V¶m˲t4u©¸¦ÔV”ä±k*ãºbl¥^í꣩*Š€³6ö’KµóÅ6 1,ÏÁ”gÎÖÔY³U?ýJUñ9Ù}è vmÛ¢WŸß¤ƒ{#Q+×ó¤S ¾ï`šÂ©á…ŸG.Àp.^qútí%Áú7øÕ®~.ÂpùlVGÞ{G½Ýï«÷ØûÏÅ®!ÿàmî{K©jMž1“àC\sû]š0}¦ÚÖ,W6Ý¥¥×HzÜó#Çqîd'|„1fŒ¤UÍ)µõji]©Ú†il#Ê÷<å²Y¦:I.›U²¢B²,Š Mu•%}ü)µ:p"§Ãé<Í@ÑUVxÀÙ›ØØDÎCŨ”&7ÏÒåWV—7Ï’³NžÈf>úZ°ð÷ξޜzŽghØØö^Z_˜uŒ†1ÁŸÔUÅKþaDCõ˜ ËæX«jjiX€4]7‡" ÑÄ©MúêÒ{´ø¾©éº9'ÃébTJ_]ö·ª›TOÁËHÎóõ⻽ʹ¥›®šs a‡œÇù)©tèÜÑ®­¿yB;^-jØaÐÁ}o©sG;Å/’«µjþÝÿ¢êº Q[z¤—1KØÅÁ»¹$c”ô¸ 'å"£¶¾Q ï[¯ú™Ÿgq'Ã1^>ObBup¦mLÍä_Õ˜ZÅãɲ9VÇ>Ñ`Ðá«ËîÑÄÆ¦¢?¡‡òÔÝ節³WÝýù›Õû¤:w¼ˆã9¸ï-ž#àÊ›oÓÂûþUÕu¢¶ô’¶cæ² †€ˆ2Æ,‘´IRC¤ÖÝ4Sþà“2M³ØJÃ÷ aß§%ây\M¯ÔR <íx^UcjU3v|`/Oꢋ/¡QT1*¥–?¿=òuJÐáTu“tãb.®SŽôäôÌ›'´óP¿Ò¹á >îÍë•®>=óæqm{¯©*„^yþ·Jw¿¨ãê>ÔÅ“`Ô6LÓÂû×kJË‚¨-½FÒsƘ;Ùç/N ¢Ç³N…‘ª‘Ò¼¨•©JÎÍç™0Pb>a%T?ýÓzõùßòØ.kž©x2I“ªéº9Ú¿k‡v¾Ø¹µOœÚ¤Ù_ùZ`B6yÆ,ÍøÂÍjöi6j™Éy¾:÷«ãp¿&T'tñq¥â1ÕU}ðã4Ýýyå² Ç2®ònáüR:ç©7ç)=ð Õ±ùùÀ…$©çØû!†I25Z-­+ešfjËc(›î‰ÒòWLzXâ8Î1vÀ¹!ð!Ƙ1*Lu˜¥uW×M8‘ÊT¥åy®Ü|ŽB”˜ïx(µc\ÝV3v¼j'LÒÑïê¸j'LÒøK/§AwãâïHRdBW\Û¢æyóU7)øƒŠ[þüvíßµC‡ßíd£–©=9èáÜÝpÚ¿{g`')¸¹ aSçÜ¢‹¦©mÍríÜ¥¥/”´É³Äqœí쀳gS€h0Æ4KÚ«ˆ…êgÎÕÂû×vnŽ7L©põÄ@Ë0²?û9U¤ªs<©š ÕøÙÏј2qãâïhÆníúê&ÕkÞ7[õíUkuãâï”EØáÔÞ$G¥†|û-¿þ9¡•ÏfõÎë¯öøjÆšTµ Ó4ÿžµšÒ² jKŸ¡BèáVvÀÙ³|Ÿ«„1f‰¤‡£¶îÙ߸KWÞ|@ ¸ù¼Ü\–Be)Y9Š:”ØW›ÆæX÷æÕÖÙCS0¢zÕ«›~§|¾´¯+RպꆛTYUESÊÌ[í[õ»GÖ(Û—.ûµ\6c¦&5N×e3fê‚‹Æ–õZÚŸ}Zmÿöèo?ûË‹4û+_cC#töïÞ©·Û·öøfÍÿ*ÿßWb»_ø¥¶<ö€²éÈ—[é8Î vÀÐx9cÌ:I‹£´æêº š·t•j¦±F¶¿_ò¹Š|PœÍÕQ×NªÒ„щ@Ë+]}Ús4CS0âŽìG›Ÿ/Ùã§j.Ô§o¸Iñd’f”©L_Zm?û‰v¾ØVV¯ÉÇNjÐÄÆ&Mlœ®‰M¡ëËã«îÕþÝCºíèÚ:-¾ïGlf„Îû¤ÒÝïòØjÆŽ×U7ÜD“àè¾7Ô¶f¹ŽvîŠÚÒ!i‰ã8ÇØŸŒÀ@HcÆHڤˆÔȨŸ9W-­+•Lf Ïs•Ïðaê !ðPz 5I}æâ`ôá™7+#€Ò(Õ¤‡‹§^¡ú¦Ov‰ãGiçæÔ±ù8z80Ç5qj“’©BÀ¡î’ÕMj(û CíÇúûþfÈÓ7þ¯þÙÄ•þÞ^m}êñÀßU7ܤš±ãiT@dÓ'ôÒ£èͶ_Eméí*„¶³ ÎŒÀ@cšU;ÔDiݳ¿q—®¼ù66€ÀÉç²òòy  ‚á‹S.P*a—ô:»3Úö^Í@Iõ÷öj÷Öß«ûPWÑ+Us¡.ožÅ=Clÿ®½Õ¾UûwíÐáw;‹úX§¦3Ô]Ò ŠQ©ß«Nþ9ÊÞjߪß<´úoW7©^_¿ûïØ¸•RO0:“‹§^¡É3fѤÚýÂ/µå±”M÷DiÙÝ*„ž`|<!cŒY"éá(­¹ºn‚æ-]¥Ú†il”íï“xý(‚!S˜î€ éÚ»G;^U¦ô«0I›:DÐþ]:~äN9¤L_Z‡ßÙ7äïØØtò¿à ©”ê&5PØ!xò¡Uz»}Ûo3ï›­jºnÅB¨tîhWçŽWw\©j]}ã—˜n`G÷½¡¶5Ëu´sWÔ–¾Òqœì€Ó#ð"Ƙu’GjÍM35oé*%S£ÙÉó\å3 0‚ã —U«¦2^’ÇÞy¨_‡ûi§kïÙÿŽŽ:¨|>{Öß_‘ªVeU•ªÆ\¨ªš uÑÅ—ðáN 2}i­¿÷û:qôðiÿþŠk[tãâïP(„Î[í[õÞî;®«n¸‰à_ȦOè¥GЛm¿ŠÚҡ´‡cì€"ðƘ1’6Iš¥u7/jÕÕ‹ZÙÍÍåäæs"`<GMeL-õUJÄì}ÜýyµuöÐ^oï'>ðÁM Ø¿»OOþóª„fy‘fåk¡ôêó¿U÷¡®@Sýô«T?}Í)#»_ø¥¶<ö€²éHÃk—t«ã8{ÙBà Ìcš%=!)23Ä“©jÍ[ºJ¦i@àå³y®K!‚IJ”¬E¤¡&©Ï\õxI¡~úUªŸ>ƒf„ÌÑ}ohãêeê9| JËn—4—Ј2eÄÓ¬BØ!2ghkë5oé*U½˜  ,xËR²ru(©„­†š¤&ŒŽ«¦2~ÚÛ¤sžÒYï䟧ó'¿Þ›óNþ#ë¿yB™tOI;Us¡>sÓ—iBHeÓ'´qõ29Û¢´ìnBÛÙ Š<” cÌ\IOHª‰Êš§ñ6]sû]4@Y#ð,±xB±D‚B”¡„m©¦2¦î~W9óYAÖµwvoÝ<â'Õ|Ó—UYUEBîå k´}Ú¨-û[Žã¬£û j<”cÌ’VGe½ÉTµfã.Ms ÍPö<K¢r”,Ë¢@‘ô”‡x<©«æÞ¨ª1µ?"œŽ­Ú¸z™²%š&R"?rçNº¢„À@ÀcÖIZ•õÖÖ7ª¥u¥j¦Ñ|¡íï“xív<®x"I!€Ð{ì¨^þÝoFä±jÆŽWÓu7(žäü_ÔôzOW/ÓÑÎ]QZöó’nuç;D€€2ÆŒ‘´IÒŒ¨¬¹~æ\µ´®T25š 4r™Œ|Ï¥¥fYJVTJLwFL×Þ=Ú½usÑî¿"U­úéWiü¥—Sìˆ{éѴ㙟FiÉí’–8޳#ð@ƘfÂ5QYóìoÜ¥+o¾æ7—“›ÏQˆ‹'+dÇbaÅ=ÄãI]Ü8MõÓgP`œ´û…_jËc(›î‰Ê’»U˜ô°‰î€0#ð0Ƙ%’TDÂÉTµæß½Vµ Óh>€Próy¹¹,…(¡X<¡X"A!€éÚ»Gooߦ|þüΕ.žÒ¤x2IañG÷½¡¶5Ëu´sW”–ý-ÇqÖÑ}VijBÒòȬ·i¦æ-]¥dj4ÍZ¾ï+×ßG!JÄŽÇOðÆ'Pj½ÇŽê­ömê>ÔuNß?®a²ê§ÏPeUÅÄeÓ'Ô¶f¹:·mŠÒ²qg ÝaDà Œ1cT˜ê°8*kžþÅÛtÍíwÑ|‘ËôË÷< 1Â;ÁÓ}¨K;^Rð!Oªvâ$‚8'¯?ýSmyì(-¹]Ò\ÇqŽÑ}&Jl ì°IÒŒ(¬7™ªÖìoÜ¥©sn¡ù"ÃÍçåæ²bÅâ Å T>›Õ‘÷ÞQ&Ý£ž÷ß—›ÏI’*RUª¬ªRÍX£š±ã)΋ӱUW/S6Ý•%ï“t«ã8Ûé> %dŒiV!ìP…õÖÖ7ª¥u¥j¦Ñ|ÑâûÊfú%^ƒŸeÂñ8µ(›>¡§îý¶ŽvîŠÊ’»%-qç º€À@‰c–HzP ;ÔÏœ«–Ö•J¦FÓ|‘äær'¯P†â°l[ñDR–mS À´­Y®7Û~¥%/uçA:Ê€0Ƭ´<*ëm^Ôª«µÒxÑÆ”‡¢:9ÕÁ²(à´v¿ðKmyìeÓ=QYò#Žã,¡ó œxAƘ1*LuX…õ&SÕš·t•LÓ,š’Ü|^n.K!†‘eÛŠ%²íÅ|¢£ûÞÐÆÕËÔsø@T–ü¼¤[Ç9F÷@9"ð0B›$͈Âzkë5oé*U½˜æÀ)r™Œ|Ï¥ç˲‹Ç‹'¨à¬dÓ'´qõ29Û¢²ävIKÇÙN÷@¹!ð0Œ1Í*„j¢°Þ)- tÍíw)™MóàÃ|_ÙL¿ÄëñscYŠÅâŠÅã’eQÀ9{éѴ㙟Fe¹Ý*LzØDç@9!ðPdƘ[%­SD³¿q—®¼ù6gà{žrÙ ¡‡³AÐ# ŸÍª·ûý“®ª¹Pñd’Â!¶û…_jËc(›î‰Ê’¿å8Î::Ê€"2ÆÜ)iuÖšLUkÞÒU2M³h< çºÊg3â“X¶b±AU×Þ=Ú¿{§Ò§„N•ª¹P‰dRUc.T<‘PUM­ªÆÔª²ªŠâ!ptßÚ¸z™zˆÊ’ä8Ît”EbŒY'iqÖZ[ߨù÷¬U25šÆÀYð\Wù\–I§aÙ1ÙƒA Hºui×6+sŽWr­HUkü¥—éâ)ML‚Ê\6}Bmk–«sÛ¦¨,ù’–8ŽsŒî€ #ð0ÌŒ1c$=!é†(¬wJ˵´®¤ñpŽ|ÏS.›!ô œæ`Ç㲘æ€"ëÚ»G»·n–ûªHUkúçæ¨jL-…ÊÜËÖhû†5QYn»¤¹„@xFƘKU;̈Âz¯¿c…¦Î¹…ÆÀùò}å²Yùž½µ[–ìXašƒmÇØ Ãv'uÕÜ =!йí9µ­Y®ì9Nƒ+3Ý*„¶ÓyD†‰1¦YÒ&I5a_k2U­ùw¯UmÃ4ÃÈÍçäæó¡Ÿö`Ù¶l»r°l›Æ`DuêÒ«Ïÿ¶(÷Mè£ûÞÐÆÕËÔsø@$^*IºÓqœut Ÿ*Ƙ%’^VµõZxßzÂP±xB‰ŠJÙñx¸60Å!–H*Q9J‰ŠJÅ Â(‰]Ø\´ûÎç³z£ˆ÷@Pø¾/Ïså{ÅZµ Ó´ðþõ2M3£°ÜIcî¤ó h˜ðpžŒ1+$-ÂZ§´,Ð5·ß¥dj4€"ó}_n>'/Ÿ/»c·l»ð˲ S,‹† ºöîÑî­Å$LuÆ_z9„‡ïËu]ùž+ÏuO{“ÂÅ- çìÁ ]XçŠdqñ eí¥GÐŽg~•å>â8κ‚‚ÀÀy0Ƭ“´8 ký»tåÍ·Ñta¾ïËËç庮äïŠy–“e[²¬Â›Ö¶£i¬ÿ±IG¼[ôÇ©HUë³_º•‚ÊŸïËÍçåºyi>_bÙ1Ù1[v,ÎE2”Ý/üR[{@ÙtO–û¼¤[Ç9Fç@©x8Ƙ1’6Išöµ&SÕš·t•LÓ,%æ{ž<וçyò=wäزdYv!Ø KV¬0½7¥Pnþ¿ÿõ؈=ÖÕ7~IUcj): ly®«|.;,A‡Ó±ãqÅâ Î1(+G÷½¡«—©çð(,·]…ÐÃ^:J‰ÀÀY2Æ\*é E ìP[ߨyKW©zìÅ4È÷Ô¥WŸÿíˆ=ÞÔY×iü¥—Sx@YÊç²òòùâ?e)6|€r‘MŸÐS÷~[G;wE⥔¤¹Žãl§ó T<œcL³ “j¾Öú™sÕÒºRÉÔhÊÞHê§_¥úé3(< ìŒXØáv,¦x")1í@i[³\o¶ý*/§$-qç ºJË4 ‘1f‰"vh^ÔªyKWv BJv$Ïu•Ëf$.Ú  Œ´´®Ôõw¬ˆÂRk$=>ð~9Àˆ#ð0Ƙ;%=¬‡’©j]Ç ]½¨•¦€P©HU‡úñ8_žë–$ì0È÷<åó9 ¬Ls‹Þ÷¯JFãçÿ‡1ëè:i>ÁÀI›Õa_guÝÍ¿{­¦Î¹…¦€Ðò/7—+üÊçäy.W *«ª'GìñªÇ\HÑåÃ÷•ÏeKÿº=Ÿ—çºô@Y©m˜¦…÷­Wm}c–»Ø³Î3†Î€‘bù¼™ pZ'iÖIZöµÖÖ7jþ=k•L¦ñ t<ÏÒ&ìXLv<.ÛŽQ4BjÇlÒÑïýq*RÕúì—n¥à€²1xQ€@°l%++i €²“MŸÐK> 7Û~…å¶Kšë8Î1:Š §1vؤ„¦´,ÐÂû×váãûÊg3Êg2Cº:¤çºÊg2Êe2â"!„ÓE/‘Ç©Ÿ~Å”Õëg×Íèx<¦<(KÉÔhµ´®Tó¢Ö(,w†¤íƘf:ŠÀÀ‡ œ”Ù®ÂIšP»þŽji]IÓ@øø¾rÙÌ9}@Â÷\å2ýò=:2ã/½\©ê¢>FªæB¿ôrŠ (®ëJ þ{A `ÀYºzQ«®¿c…’E~í ’6zÅFàà'c6©pr&´’©jÍ¿û_4uÎ-4„Ï@Øá¼ ƒ÷Á¤BgòŒ™E»ïx<©iŸ½Ž"Êëe´¼i !Pæ¦Î¹Eóï^«êº a_j¤—1Kè:(N¼¬ÂI™Ðª­oÔü»×Ê4Í¢é ”Ü|~x¦3ø¾òÙ, d.šx‰ê§_5ì÷'uÕÜU5¦–"ÊJ þÏäEe¯¶ašÞ¿^µõQXîÃÆ˜;é:(‹«ÔH'_V‡~M35oé*%S£i:%ß÷•ëïÖûŒ%’ŠÅã€éÚ»G»·n–ûªHUkúçæv”¥l_:ÇÅëqaÒ¶f¹ÞlûU–úˆã8Kè8N@äcÖIZöuNiY –Ö•4„Z>—•—Ïë}Z¶­DE%Å „zÕŽß¿ Lºçœ¾¿"U­ñ—^¦‹§4)žLRP@Ùñ=O¹L -O(–HÐ$¡ñúÓ?ՖLjÂR‘t§ã8Çè:@dcÆHZ'iaØ×zý+4uÎ-4„^¶¿O*Âù®Då(Y–E©®½{´÷N¥»ß?íß§j.T"™TÕ˜ O$TUS«Êª*&:ÊYÛžSÛšåÊžc躌´KšKè  ’›$Íó:“©jÍ[ºJ¦iM¡çy®ò™LQî;–H*SdB®¿·÷Óªj.dr ô²}é@autßÚ¸z™zûRÛ%-qg;]çƒÀˆcL³ “Bv¨­oTKëJÕ6L£é Ü|Nn.W”ûæC€° là!‘P,ÎkqÁOÆ>þïâ¶,û“'ÅfzOèw/Ñá½o„½\Ý*Lz ôÎ)a‡M’jB½Î¦™š·t•’©Ñ4D†›ËÉÍ'ð`Ù1%**(2 tr™Œ|Ï ÜqÅ+*dÛ1`øÿ}9%°pj@Á²¤xÂþÀmÉâþ;ôÔÿZ¯ÿûãa/y· “ž`÷€sAàD†1æV&;„:ì0¥eZZWÒp9Öëés!n)Y9Šæ²Áƒ³‹B ±¸%{ ÌPìàÂùØöóGôÜ?ß…6}ËqœuìVpÖ/ <€(0Æ,‘ôpØ×yý+4uÎ-4DΞçºÊg3:&;W<‘¤9$¦0ȶNNcˆÅ,Ù1K–e}d"C¹zí™ zîÇ÷+Ó{"ìí\é8Î v58@ècVHZæ5&SÕš·t•LÓ,"«˜‡X<¡X"A‘¡”íï“ôù‘xE…l;Fc€ˆ°c–ì˜ý§@ÃÀd†Á?GÅÁ=úŸËnBèáÇq–°óÀPx¡fŒY'iq˜×X]7Aó–®RmÃ4"Íó\å3Ź"e,‘P,NàNżˆÀÙbÊ"NƒSɘ,KŠ'lÙ¶¥Xܦ8§Èô×ú¿¼]‡öì ûR‘t§ã8Çè:øÄ׉@cÆHzP!;ÔÖ7jþ=k•L¦é’²}é¢Üo¢¢R–Í›¯€ò}e3ý˜òÀkp |Y–¥X–³‹ÙJ$mY–¥x‚çôÙÈô×S?ük½ùûa_j»¤¹„À'þœIà„Í@Øa“¤a^ç”–ji]IÃN‘Ïfä¹îðÞ©e+YYIq¡æ¹®òÙLI ‹@y 6Äã…pC|à¿-Û¢8ÃèÙß§?nøIØ—Ù.éVÇqöÒqð±?xa•°Có¢V]½¨•†|ˆç¹Êg†÷ñDR±xœâBÏÍåäæs%yl;W<‘¤ vøÝ}ʤÓÚ¿k‡$éø‘C:qäðÇÞ¾î’UŒJI’&6NWE*¥ºI r˜Å“1‚ %ôÚ3ôôß?ìËìVaÒÃv:N‡À cL³ a‡š0¯óú;Vhêœ[h8ÀÇÎ)–m+QÁt@„^Wç²òòù}LÂÑ“é+öïêÐáwöiÿîŽa»ïºIõª›Ô ±—\ªºI šØØDÁ‡ò<ŒYŠÇcŠ%l%’¶lÛR,nS˜x§}‹žøÁw•é=æez‹À…(„’©jÍ¿{­j¦Ñp€3ñ}e3ýÒùž÷²,%’²lÞØD‹›ÏËÍe‹ÿ@–¥X<ÁdňÈô¥µsó êØü¼¿Û9¢=qj“&7ÏÒÄÆ&¦@è£SÉ4àîéÐ?ø®Žw½ö¥~Ëqœut|à¥#PîŒ1K$=¨‡jëÕÒº’°Àùž§\6s^¡‡X"É.‘~mÏeå{Þðß¹e)‹^w[Źý»:Ô±ùyí|±-Ç“•Òä35¹y–&Ϙúú†â [±ßQž2=ǵþ/oס=;þTBàƒ/! <€r6vx8Ìk¬­oÔü{Ö*™MÃ΂ïûÊg3gÿÁ ËV"™d²˜öÏKþù,Û–eÛ²í˜ìW“‚ý»:´å×?×þÝ=ư…ì˜u2àHÆ7„P¦ç¸žýñýzýßûRW:޳‚މÀ(cƘ;%­ó§´,PKëJš ÄJè IDATp†üÁ ËV,ãê’œ†çºò'/Ÿ?ý ,Kv,¦XwÞaIrsYyÃp?0hÿ®Ž!Ý.Û—Ö[í[)!‘éKëw¬¡’öïîÐã«ïÕ“­Òñ#‡†ü}ÉQq©¥Ñ5„0¬æÿÕ×çÿ¿ ók$m2ÆÜJ·ˆ~Z…°Ãõw¬PKëJš 8ÉÍç•Ëü)ä ßöÇÈgsÀ°Ù¿kÇoûÖv„EÛÏ~¢l_šBœâíömúÉ=wj˯®Ìj“Wí¸AÕ̯-ÖÍÿ÷ß©¢*´Þ«‘ô¸1f Ý üø©ÎÀI‰—Ò°C2U­ëïX¡©sn¡ÙÉ÷¦9ôÉÍeå{^‘Ï+þcˆ„ãGéð»C¾ý[íÛÎøá?PöïêÐÎÛ(ÄÇØòä=r÷÷Ô±ù…|ýÔ‰–mQ(ݧ¾¸HÿyÕ£a=HÒÄ? PNF<Öõ%SÕš÷ZÂù¾¯|.[:äs’ïØc{®Kœ··Û·Õí³}i¦<ÛŸ}Š" áçž?Y£ÇWÝ+çí7N˜è€‘6îò&ýçUê‚ñ‡y™cVÐm‹Ÿ¢@`„=ìP[ߨùw¯UmÃ4š Q6tÈõ÷ÉËçKtLxpþ:6?Öß³óCW:ååø‘CgzŒ²ý»;ô¿~ø_õ«ÿ“Ž @™jöiŠp.u{îYý¨õ=ý?þ‡ú{{)F\Eõúú?<ª)Ÿ›æe.&ô@8x%7pÒá{a]ß”–šÏZ%S£i6D”›Ï+›ÉÈÍçH…óù°ãöOQ@ÊÔ[Û·R„óðÒ¯¥ïø¶6­_øàCo¯ö¾öšö¾ö ‰Šê tëý±®ü³¯†y™‹1ëŒ1cè8áaù¼±Jh ì°8¬ë›Ò²@-­+i4D”ïyÊçrò=7PÇ‹'K$h€sö/˾­l_úœ¿ÿ›÷>¨ .K!(#‡ßݧõ÷ý …&©”®]p‹®]°@•UU9.çí·õâ¯~¥öçžýȱÎýú×i\Hlûù#zîŸïóÛ%ÍuçÝ üx%1pE…u’†u³¿q—®¼ù6š QäûróùÂD‡Š%ŠÅ <87›_ÐÆŸ¬9¯û˜ýåEšý•¯QL"ö3NoÆç¿ k,¹ì²’ÃΗ^Ò‹¿ú¥ö½þúÇÞæš¯,ÐÍñ4,$^{fƒžþûï‡y‰„ `Ä „6IšÖ5^Ç Ms Í€ò\Wù\Nò½Àc¢¢R–mÓ,çä‘»¿§GŸ×}$G¥´ø¾©bTŠ‚P&ÚþíQµ?û4…(¢†+¯TóæéŠk®‘©ÇÔögŸÕög7ªûС!}Ï÷Öü‹ÆŒG³Bâö-zâßU¦÷DX—Hè€ ðFTØÃÉTµæ-]%Ó4‹f@Ôø¾ò¹¬<× öqZ¶’••ô À9Î+;3å€òòøª{µw…!3>ÿ]ú©O {øÁyûmí}í5mv£ºöî=ëïÿâÿþºvÁ"÷tè.»=졇%Žãl§Û”§8%#% a‡ùw¯UmÃ4š S˜ê•Êà"C±XŒ†8'™¾´¶üúçÃvÛŸ}Z3æÍgÊÀi´?÷¬ÚŸ{V¿øGiü¥—êŠk®•¹ì2™Ë.;« {_{MÎÛo^ò$‡ÓßÛKsBfÜåMúæš_è‰åßÕ¡=;øÄ’6cæz <x#ÂÓ,iBv¨­oÔ¼¥«T=öbš QR.SY–bqÞpnÚ7>¥GÛýeûÒjûÙOtãâïP\€3èÚ»÷#ÓÆ_zé'?8o¿­L:=ìÇ2œÓ&5f¢¾þjý_ÞÖÐC=P¶xWÝ@Øa“ 'B§¶¾QóïY«dj4Í€(ñ}å²ùžW6‡O$%Ë¢wÎÚñ#‡´åÉ Ã~¿;_lÓì¯|M\4–"œ… FÊ×\CñCª¢ú}ýÕ³?¾_¯ÿûãa\"¡ÊPTa;ÔÏœ«–Ö•„ ‚Ê-ì‹'dÇb4À9ÙøÈš¢ÝwÛ¿=ª/gEpÞº»ÞQ·óîÀïïH’ºö¼®LÏñ“·éïéÖÁ·vœöûÇMž®Êê¾Q?ã:I…Ž¿üSø5ÉTŠ"DÜ5_Y 1ãÆQˆ«¨¾@óÿê¿KRØC·:޳‰ŽP,ß÷©(а‡¦´,PKëJ œ‡ý»:Ô±ùy8rXÉTJ“gÌRÓus( ÏÍåäæses¼±DB±x‚Æ8'[~ýó¢Lw8ÕW—Þ£‰MÀtíy]÷¼®nçu¶o.ºÞñã¨ÿôuAˆ+5îò+Uc.ÑøË¯¤Aàg„ÒøK/ÕwV?H!"ä÷?ùGýþ'ÿæ%~Ëqœut€à#ðŠ"ìa‡é_¼M×Ü~ÎÃïyH;_lûÈ×ë&Õë«ËþV£¸R‚+Ûß'•ÃùuËV<™m3ÙÀ¹Ù¿«C¯¾·èS7©^_¿ûï(8€ 7t½ùººÞ|Uï¼úRà¹þÓ×iÜåWjü”B‚Ââ­ö­úÍC«)DU¤RúÎê™îA¯=³AOÿý÷üDB”`Øc–Hz8¬ë»þŽš:ç œ‡ ; âC2Ïs•Ïd‚}–¥X,®X<.ýÿìÝ}|”õïÿ÷5sÍLf&É@ÈÀ”*J(ìÖ±ÒjÚµX¥[ñ¨œÝõf·uížßY[qÏùՕݶjÝÇî‘v»çq*ÒžÜö¡xD[uEŽ¢+*‚ø+1`hP!ì„„Àäf&sÿûc@ðÈÍÜ\sÍëùxäÄ0×õý|¯É\wïëcL€1‰Ç¢Zw×·•ˆE‹²¼%×Þ -K)dx°_¿Ýªww¾¨Cm;tôàÛ3öYE3,ÖìÏ/U`Ê'ØP6ž¾µö·¾A!*ȧ5kÅwRˆ wäv=²òB $<€¼±sØÁí«Öw­U]ç˜h`Fv¤EW.×¢«®¡h°Ë C†áÃááÈ}§›€|(EØá¤ÆŸÕ•ßZÉ$6ÖñÊ&øíËêÚõŠz;;(ˆ¤À”éšuñÒ÷€•µo{Iϯ_C!*„ÇçÓwX«*¿Ÿb B?‡Ãßg¦° /;8›x,ªÿÙjÞ×>âCàaôNC§cD?kšŽüÜ ç4 9ù½±.™Èäíµ²™¬R©¾Þh~T¬Rr¡§Œƒp€Ã?·îþ’>¹ùêÛWiÚì¹L`ÝïìQWë«êxy“¶½FAÎÁã¯ÑìÏ/ÕŒkÖÅKUU]KQ`9ëîú¶úz)Døã¿¾M [Z(Þ Ößû/êyg¯mÅ…Ãá›™i¬…À7;‡êfÌÖ’[@؇ÞCzúg«G}ñÃ77œ)€p¦°aH¦ëÌ—ÛÉUbg OdÒY¥Ó=ÆN'3Ê|èØ;“Î(“æx€r•N%•N'ð`8r8r8M ®”NWSW¯«î•ÇëcR€24<د®ÖWµïÕMêjݦH÷!Š2³.þŠf~©æ_~Å€eÐå¡24Ì›§›ï¾‡Bà£Ç ƒýzøŽ=€¢!ðÆ% ýDÒ·í8¶º³uŪµrûj˜h`Œv<õ˜v<½qÔÿÎíõé/W¯-É:› 8œ†œ -|\Gƒ|vL@eJ%3:ýýÊô‡B©Dš¢P"ÙLFÉøpá`8ät:å0 9(«„N¢ó#P^"ÝÕÕºM¯lÒ¾WŸ¥ p²óì‹—jöç—R”ÜÃ÷Üi™ýÆM?¼[3›š(>þøa°_Oüýÿ£ƒoî°ë =`!À˜…B¡%ÝdDZvƧ÷P§ž[wÿ˜/v4.ø¬®üÖÊ1ý[ÓtH§NïŠðá †aœµ£P.Ò©Œ2™SÇ÷ÉÄ©.%(Œd|XÙL&¯¯i8œr8rš&Pôãø«ïV"µÔz­¸ëGªŸÞÀé>¨ŽW6©íÙ :òî[¤ˆS¦kÖÅKõ¹k¾©À”OP”lÿáá{þ–BØÔ§5kÅwRœÓ3ÿø]íÙü¸]‡Gè‹ ðÆÄÎa‡ –,Ó’[À$cEµuÃzí}më¸^gɵ7èþè 9NtVp†œ§Nï°@h»‘%2 M)“N+•ˆçåµ ‡SN—)‡ÃIa]û¶—´õч,v¤úé3´â®{™$ÀBºßÙ£¶g7hß«›é>DA,`ÆgkþW®Óü˯£(É~Äóë×Púöš4aòd !ô À5Â>,‹ªõùg´k˦¼Ü qëê+ÔØHa :=$‘Ig•>„H'3Êd³R&«T*C¡!•ˆ+“{÷$‡iÊé4e8ð(ͱüsëî×þÖ7,½ž‹®\®EW]Ä%DÈ¡}÷—¿¢¸€ ¤’e³Ù÷ƒÙ“aB»Èf•Lĕ͌îsÍašrš.†A ”Ä»­;õܺ5–ìêðqVÜõ#ÕOo`â€"ŠtTÇ+›Ôöìy÷-û Ðå“zàJd¿”ŒÚbxt}@±z°º;`¬v?»Q›þéN»¯UÒ¥„( `Äìv¸ä/¿¯Y_ø“ H2 CNWî ³¦éá0d’yâïzº¨«ý-½öäoééÉûòæÍÓÍwßÃDàd"“É*Ê*ÎäÂ'‚”ÇZVéTJéTòÜûÚ§L—‹ŽJ¦ÿhž_·F‡÷µ—Õz×OŸ¡wÝË6<د¶Í”oÈádˆÁ?Yòö%IÁ¦±½fÏîÜ÷äЉ@ÄtüÄ÷ÈË—$0eºæ_~.\~‹ªªkÙÈQP„ìî/B <€!ìœr¸£]ñØzvªÿhŽöæþ~Œ7 ÔÔÕ«vRðýÿž6{îiþ´¤\׃|<ÉÐt;%I§!§Óñ ÃÉpÇ…÷ïWxÿ~ؽ[v·$äpºæ«–ié7¾Á†T¸“aˆT2‚H¥2!––ɤ•Iç¾túç•áÃéÓitP2ñXT­Ï?£Oo,Û1,ºr¹]u “ @Ûæ êxe“ö½úlù¬´/˜ 7Lh”ê›rß]þâ¯Gd¿4t$÷ýøþÜ÷h%K6ÿòkuÉw(0ålô(Båï¦Þ­™MMãr°u‡žøûÿ¢øÐ€‡Gè€ ðÎ* Mô„¤/Úmln_µýÙßvÀYõíÑáŽvîxK½‡:Õ{¨«ôÛ®×§à‰ðCͤa Ã·Ú¯àŒ™2ÍÜMTÞÚjר(Ã0Þ5|œãGŽèø‘#øsî«[{ö}|_ù‹oè¢eËØø|¬“Aˆd"“ë‘Ì(•ÊP€?³2”Üɠî-›”ˆEËz,n¯O+îúÑ`ìºZ·©mó#êxe“õoF<Ù¹!ØTÚpÃH%‡r!"ûsß{÷Xjõf|f±.¹ñÍX°˜7 ¢}ÛKz~ý Q†èÀ|:òN»Yy¡À;¼(iÝÆæöU늻֪®áSL4>¢÷P§Ú·½t"äÐEAŠŒ§‹d" A$3J¥Òʤ9߀ÊÕ´G{·½d‹ Ãé¦Íš««W®b‚1ŠtTÛ³Ô¶yƒ"݇¬»¢¾àà Æò/~Ïn©w·ôÞv)rÀ«4ù“ŸÖ箹Eó/¿Ž7ò®÷P§ž[w?×XÊÌõß½Ssš›)ò†ÐÈàcv@¥‰Ç¢¹žF}½¤„þßû¥ªü~ `\Ò©ŒRÉŒ‰4] P1w´«}Ûhïk[m;Æ%×Þ -K™l`Ú6oPÇ+›´ïÕg­¹‚.Ÿ4µ9p6I¾Éöž“ ~¿=€H–6˜˜2]—Üx‡f]¼TUÕµ¼aW;žzL;žÞH!Ê@ ÔwXK!w‘ða=ñ÷ÿE=ïîµãð=P$ÀGØ9ìP]ž¾tûjÂx_ï¡Nízþ[ßPn¾÷ø@Þe3Y%âi%i¥t€€}TÚCÜ^ŸVÜõ#ÕN 2ùÀYt¿³çD7‡G¬ùTåóå Á&{tpÈ~©sK.üí)Ùjxü5úÜò[táò[> ¯úöhÇSqÆâ¾òßÐEË–Qæ˜e°_¿¼íOÕ×ÕaÇáz <€°sØ¡nÆl]±j­Ü¾&:ÜÑ®O=¦ÃûÚ)†…ð!Å’Ne”ˆ§•Jd”ˆ§(ÊN%ts8“i³æêê•«Ø€ì×¾W7éõÇÖêÈ»oYkå|ÁS]¦63YgbðÁ Ák£û6 ­ç`žüÞ_zcB༰*AÿÑm}ô!ío}ƒbXPüyºùî{(€¢Êf²J$ÒJÄÓJ§Å¹XYû¶—Ôºåõêªè:,¹ö-hYÊ(×ÍaçÆµêxe“µº9fJ -¹€ƒo25Z'Ã[¤d´è‹÷øk4ûóKuÉw(0åÌò¦ÿhön{IíÛ^ªˆîTå`Áe-úúm·QÔ@$®Á£Ç´uÍ÷ÔõÆ‹v"¡ ˆÀDØöEÕúü3ÚñôFŠaaXâ3c8EøÖÚG=q^ƒOq{}Zq×T;)H1P‘,ÛÍá¼E¹€ÃÔfÉÅ“Âóæ½í¹àÃïw”dñó/¿–à âÝÖzw×N½Ûú†±()‘›~x·f65QTt0©Ø`B’´uÍ÷ôöÖ'í8̈r¡‡]Ì8ùEàØ:ìpÁ’ej¾áo;T¸ÃíznÝýÜP¾xý ]ºb…`'ÉXŠb øû£'‚»¶lâ&ÀѸ೺ò[+)*J¤û ^ìçjÛüˆuº9r(žäP.øðö“R´§è‹'ø€B"üP`Pßy`-…@á?Âiõ÷ ¿ÿß„Àhx ÂÙ=ì°äÖ0ÉÈá4äp:dšmûõ£ze㣥Lx`UÙLV‰xZÃCI¥R T°£‡j(Ò§HÏ‘ü¿@p²&Mý„üê(`\:ŒÜW¿u»>¹àB ÛkÛ¼AmÏnP×›Û¬±Bç-’Zr!”FÏn©k‹ÔùBÑMð…Ö{¨óDøa§zuQúÊ_|C-[F!Pp©dF‘£±ü¡0R¨`„PîL·S§!§Ó!§™ûnº’¤á¡!=¸ê.u8@¡Êå Êh8šR<–çU€ÊpôðAuxG}¿?4¢Ÿ÷øª5ãÓó5eæù0j­[6iûSt¡šºz­Xu¯<^Å€íDºªíÙ jÛ¼A‘îC¥_!:9XSr(×ñ¡sKÑ»>|@1ÄcQîxK‡;ÚÕ{°S‡÷µS”<úöš4aòd ¢8úÈßíÙô+íø·ÿaË]9z o<P¡; \œìÖàr;e’éÊun0ÆÿMxÿ~=¸ê.Å£ÜPn<('t}ì-•H¨»ó]½·o¯âÑÁ1½Á ?ǃ'NC§qÖ“Lä>“ÓéŒ2é¬ÒÉ E”w[wj놇4Ð×K1FiAËR-¹ö Ûèjݦ¶Í¨m³:×fJ,#äP.:·ä¾z÷u±ó/¿Vó/¿^3,fP‡;ÚÕ{¨S=hàhoQC óæI’f6ÍÿÀßO˜ $zuªÿhzæ¾ôõjðX¯"=£ï|5aòd…?yâ{£Bã>®ÒÝ»u`÷níÝþÚ˜Ö-Ÿþø¯oÓ–6M¤7vƇzgCà€ cç°Ã%ù}ÍúÂטd‹û@°Á4ätænfÉÂåï¦Þ­™MM@ÙJ§2Š%•ˆ|ÊM!‚Üvkî翨@p ÅNp8 ¹=¦<^3oÇ…#‘Íd•H¤•ˆ§•NÓ%EÕúü3ÚñôFŠ‘ÓfÍÕÕ+WQ”H÷A½¼þ>u¼²Iñ¡Ò­ˆË— 8\°L 421v="½ýd®ëC²xçÍ >À ª|.ùkÝïÿ÷Ý»ßÿs®ëÂI§:3HR•߯Pcñ~ؽ[»¶lQë [Š^ϧï<°–‡3 ¸û=}Ãg}¡p&¨ „PL§!Ót$Øðq;ØvAð(ÃCCzïívÙÿnA‚6ëÂÅš2ó| Še†<Þâ‡Î&>œR"žæsEu¸£]Ï­»_}½#]¹\ ¾t…<^Å€åu¼²I;7þ\]on+튜·(thà ß¶–:|ˆï‰òPJI^Ësõm¹† IDATœKxÿ~múÅÏÕ¹gOÑ–¹à²}ý¶ÛØPPTç Tºñs¦6K¾ÉLL¥éÜ"µ?Lð¶æpš,¿äkO>©gÿ÷/в¬ë¿{§æ47³± ¨Fx¤¾Îßé™{n!ôÞGà€ `×°ƒÛW­Eö7„ŠÈt;OtnÈuk0M‡ ‡QÒuÒƒ«îR÷L x`WÉDZƒ‘87OPª Ãé<¾jýÁ—¿*ÓífBPÇ‘¾jWуñã¥LðÙ¼¢«Cq]}û*M›=—BÀ"ÝõòúûÔñÊ&ŇJ·" —I3Z¤ çß ‚°µ*ŸKþÚò<æ.FGsϧïþòWl((ºH|ÄÝ =€ÓxÀæìv¸â®µªkø“\¹PƒSN—CN3ppšÖlû»é¿Ðö§ždÒl‚À»‹&LP "=Ý:ÜÑ®¾ß²ÄúÌøô|Íøô&¶ešùjÝetø0‚È—O=¦Oo¤E4mÖ\]½r…@Iu¼²I;7þ\]on+ÝJfJ -¹/—ŸIÁGõìÎz÷ïxˆà ¬¶®ª¬E zh¾j™–~ãl((ºÑž'ôN"ð€vÀHX±kÃHؽ[ëþŽ ×vBà@% ÛP\‘žnu½õ¦"=ÝÖÚ7ݺð«_§ËlÇ0 ùkÝòxMÛŒ)›É*Mix()®©`´úöèßï_­ÞC]£þëÏ~IPtÃýÚ¹q­Ú6oP¤»„aۆˤ –IF&#S¢àÃü¯\§ù—_Gý‘W“BåðÚ»}»ù‡{ òÚ·®þ±B|> øÆò@ B@"ð€mvÀGvü CÎÓ•ëØ`ºe=¦Ÿüå-Šôô0¹6Bà@¥Èf²Œ$”ˆ§(P@ï¶îÔ{ûöZvýf]¸XSfžÏDÁ6¼Õny}fÙ„èG+Êh°?¡T"ÍdcDw´ëéûW+‹RŒ!ð€bê~gω Ã£¥[ _0r ›Æ£Á‡À”éºäÆ;> /L·Sº*[Œ¥]ÎÁ ¾óÀZ6”ÄX; ö¼§ç¼R}]v+ ¡FºŸO °Â8np¹rš¹î NÓa«1îÚ²…° |?«†j&zr(:  @ž¥ u¼þªú~ÈÒëyôðA°ÇñºÇ”¯Æe»ãÎsšêªNi(’ ÛΪuË&m}ô! !©~ú M›ýiM›=Wµ“‚ªŸÞ𑟉Ǣê=Ø©xlH½;Õs¨S½;5Ð×Kaym›7¨íÙ êzs[éV¢á2iF‹äA"ȃ`“¼»¨Á‡H÷!=ýO·ëåõ÷|À¸™6:.¹tÅ íÝþZ^¯‡Íi¾ˆe§:8UW¬Z«gî¾Ån¡‡€¤C¡¡ÎØ a‡Êãp2M牀C®ƒƒ]Ÿ¦y:º;ØT"nšò¯ãõWu¤ó]˯§iºuÑ×¹‘åË4òÕºår;+nìt{ÀÙ<·î~í}mkE× ¦®^ ¿t…|Vµ“‚c~þ£=:ÜÑ®ÃoéÝÖ7FÕ-£~ú ­¸ë^6HÄð`ÿ‰né.QÈÖÌur¸`ÝPX=»¥®-Rç E[$0Õ<^û<ÿõÀîÝZ÷w«òözß^ó€&LžÌ†‚’ˆô ë8:°cèA¢ÓçD‡lÄ®a‡º³uŪµrûj*~Ž+5Üða{·o'ì° O•)§Ó¡þ¾aB@t½ÕZaIJ¥èð‚òd†üµn[ÝD4Z'»= õ'4M²Q@R®KÁ㫨ÞC][ƒšºz-ºêÍ]ü…¼¼^í¤ jß½ÞCjßö’ÞݵóœÝ]u %ò®û='‚–n%êçåBS›™G°)÷5wE®ãC‚t|Àx8œöºn6³©I óæ©sÏø»­L™9“°C¹üê­rªÖåÐD·!—ÓP͉Î%©Œ’é?‡<œÉ*–’ú“Kd”ÊØï\³ÛWC§*° ÂöC¸áÌžøéOÕú aCtxPÉRÉ ¡`œ†‡†´ó™ÇËj/¼âjUùy"/ʃaªò»äõ™Ÿž&Ki¨ŸnMl„´,Õ¢«®‘Çë+ÊòÎ~øÒ·æ-tHRÛæ j{vƒºÞÜVšpùNusðq£*J,z¤hÁ‡“èø€Ñ˜²ß1v¾º<|å/¾¡‹–-c#±(¯ÓÐùµ¦‚§\y8æHeÔ9˜Ò‘aë„ÆÛáá$:=Py<`„ÊŸi:äp9ätn‰øÓ?Q<¥6Dà@¥#ôŒOÇ믖Mw‡“æñNaò`yn¯)Ÿß%牧J‚ÏpœÒ{¨SOÿlõ9;Ø÷÷ƒOW~k¥¦Íž[Ò98ÜÑ.·×§i³çªvR ã6<Ø¢›ÃEº•f%3Ousp’…Å|€†¡º)>[ŽíþÛ¿£îÆõß^ó,Èt:¿ÆTƒ¿0]“™¬º†Òzg ôÝ óx=Pqûúœ| ¼v(?'à ¦éérÈåv²!Bxÿ~­Yy;…°)À “ÀX•cwIºèk×Ét»™@X÷Ö픯Úűë¤S +•ÊPŒ Ò{¨SWß­D¬2ÌP?}†¾ú­• `+‘îƒzyý}jÛühéV¢á2iF‹ä<ÊÁXìø%PWe˱íÚ²E¿þ—ŸŽùßjQ³VÜy'‰ÅLt;Ô4Ñ%¯³ðHe´ûXRÉÒ³öuGózÞ;ÐÖ5ßS×/Ún—T„øÅ@#ì`}¦éÈ=³Æ­Úº*M ù¨÷ª&à‘×Ï #cq`÷nа5ÓåPm]• ƒnOÀh¼÷v{y¾ç ;À¢NCÕuU»ŽÓÌ}†›tÁ¨ñXTOÿluE‡®^ùw„`]­Ûô«;þ“~ög•&ìàòIs¯—–®‘>{a”ßäÜ6»tM.¬S‘îCzúŸn×Ïþ¬Ym›70¨sš›åñùÆõïa-S}N}®ÞS”°ƒ$Õ˜-z4ÕWºcü|?äÇí«Ñ—n_­ –,³Ûæôâ‰ûA€<P¶;Xá†â¢6Þ¿Ÿ"€N…ŒÜÑÇÊð‚°¬Ç0 y«Ýš0É+פ £­ŸÃ ôP!ⱨ_ýC ôõVäøO†<^Ê^Ûæ úÙŸ5ëWóŸÔõæ¶â¯@`¦ôÙ¿––ýRš»"wó8PŽ>À6~€H•߯9Íéßz|>3ÕçTÓ„Òœjšà.Iè!]ÀnˆKný¡lŽ«”!Â¥çp2ÝN™¦C¦ËA˜¡ˆìn£6F N1]U<ŒÄ)0‚}ˆxt°ìÖÛ?q"“Kq{Mùü.9¹Y\N†úû†•*à (­ÇWÿP½‡º*rì5uõ„PþûƒýÚ¹q­^߸Vñ¡Ò¬DÃeÒˤ@#{9|˜»BêÜ"½ý¤”,l7¤“Á‡çþ×ßësËoÑ…ËoQUu-sQœ.{Ë,liQë [Fýïæ4_¤*¿Ÿ Ä"Jv8)·ü„Þ‹¦‹¶ÌL&[Ð×_rë$Ioo}ÒN›ËåB—†Ãáã¼{•ŒÀeÆ®a‡Ÿ½TKný%ÆaÈy"Ôàr;dšƒœÇk*•Ìh8š¤ÀY ï+Ëõ®›ú &–`ºòUÓ™0Ÿ ‡¡ê EŽ+›ÍR›ynÝývp{}ºò¯Vv@ÙŠtÔËëïSÇ+›Jt𥆖\ÐÁŧ°9ßä\èá‚e¹ÐC‚ñ¡½üÐj½¾q-ÁØÒ̦&‚AEzzFõï¶´P<‹¨q9Jv8éSµ. $³H'¨Ÿ*Âr=`_(#v ;\°dÙû'¬àd÷·Û)牥â¯u+•Ê(•HS à †"åx˜Dà8þõU»åñr© œ¦ãýN„ì£uË&í}mkÅŽÿË7ݪúé l(;]­ÛÔ¶ùµm~´4+P?/thà†ST —ŸàGsš/Òö§F~3w Ô̦& g ë\Öùõì0Ô4Ñ¥mGŠÓa8“.Îq1¡쉫” ÂÜ!22ÝN™n‡Ün'Ý€åÔNðèXOŒ›%©;oºªü<Õ¥a†ªü.y}&ÇÀfºòÜ<§6p¸£][}¨bÇ¿ e©>¹àB6”•ŽW6içÆŸ«ëÍm¥Y†Ër7y™ €à [ZFx¸hÙ×(šEœ_ã’×i­íÕ˜Mõ9õ^´ðÛI¥2E¡ì‡Àåã'"ìŸ e­Ê_MÇpª™èQß0Ål¢aÞg(JÂí5åó»ä4éfX,ž*Séê¬bƒ ŠQÆâ±¨ž¾uÅŽ¿¦®^‹®º† e£mó½¼þ>Eºá¾`®“ÃËr7xø ‚À¸„åñùŽì}³°…îBV`: Íð;-¹nçטE <¤“™¢Ž‹Ð6ÛŸ¢X_(zPÒMvS1ç!Óí”Ûí”ÛCÀ¡ìßúÝŽíPq\n§ª|. G“(sSgÍ‘B…@Q™n§|Õ.¹ÜNŠQ¾j—’‰´R‰4Å(SÏ­»_‰X´bÇ¿äºäñúØ`iÃýjÛüˆvnüyi‚õórA‡n,F„à ™HKrÙ~œsš/Rë [Îùs óæÑ]Ò"¦zrYôµ×Yø.éT¦$Ý‹ =`°8ÂcÜÉ9np¹2]<µÒN&LžL ( jÂäÉš0yÊj}üÈ?Ò­ðþý#~j ÿüµn¥颶ÿ_¾ÀD}rÁ…EãpòU»åñr9 Ôj'xt¬'V’›<0>ï¶îÔþÖ7*vüÓfÍå³ –6<د×êõk(îÂ]>ijsî¦mç-±½>£5§¹yD‡ðþýË*ûÔ~k?|`rUa©déÎgzÀ¸Â€…v9Ã0äªÊ…Ünº8Øú}ÑØHò( jNóEšÙÔ¤™MM#zÒOxÿ~ؽ[»¶<¯î("™¯Ö­þ¾a œÆã«.›õüÌÿˆ CQ†¡*¿K¾jŰʜ8 ùn SŒ2Eõܺ5]ƒEW]ÆKŠtÔËëïSÇ+›Štðs.X–»YÀø|FlNs³<>ß9PFuüÈ&Vb^§¡ÓÚè›\UØ@F¢ÄÝ =Pþ<`Q„ÎÍá4äö˜òxMº8TÒ{£±Q`P‘žŠ1F`P [¾¤…--c:ÉjlT¨±Q-[¦»w뉟þ3óEär;Uåsi8š¤À Õ&Z~=¾j}úâ/Èt»™0œÛkªºÆÍìø» ÊT“V"ž¢ebë†õJÄ*·Óá´Ys5mö\6XÊÉ CÛæG‹¿ðúy¹ CC  ÁŒCªÄ7UÓ̦ùúÝŽíLz¨.“ëØÝKdlûÞ\rëšûY½üÀ÷í´y-ôI7óNØ,ˆ°Ã™r€”;‰;’V½ø —µhaK‹f65åq.šô­ÿD›~ñ æŠÈWíR<–R6›¥€$ÿ„:y|ÕŠG­ùž LÔg¾øG„Pp¦Û)›ãe‹«¸u¬'Íçxè=Ô©½¯m­èÐÝVÒýÎíܸ¶4A‡†Ër7_è> ÍéÁ‡÷¶KíKÑÂ>x‡àÊÉœææsÁ Ý, ¶LŽÑë<΂Ò©Œ2ikÿÎúÂ×$Én¡‡›B¡ÂáðͼÛvFà‹!ìðQ„ða [Z¸¹~„<>Ÿ¶|I-[V°“ÚU~¿¾~Ûm’ļ@‘CþZ·#qŠœ0iÚt½·o¯åÖkê¬9úä‚ ™ ”ÃiÈWã–§ŠSþåò9î­v): ·uÃC=~º;À*ºZ·éåõ÷©ëÍmÅ]°Ë—»Ñº¡Eòq³(P2.ÿ©Î*[>àœ’‰´\n§íǹ°¥E¯=ùu8pÆŸYúo²A`ÄÌ]†OÄ­Õy…Ðeº¯B °ŽP(ô}v$†!W•SU^³"NJbtf65) *ÒÓC1ÎÀãóé¢e_ÓEË–©Êï/Ê2 =@‘×{ME–y:PjS/˜k©Àƒiº5ës‹5iÚ'˜Œaªò»ä«vQŒ2ãõ»4Mò9na‡;Úux_{E×€î(µ’|ÁÜSå§6çn´`0•´}óÝ÷èá{¤Î={>ð÷ŸOK¿ñMÍinfƒÀˆÕ˜FA^×j‰ÐåÈ e2Ö …n–ô¯vÓX¦;rp{œ2Îh×–-úõ¿ü”B|H)‚öð½÷ž³ò™|ñúºtÅ &F(™H«¿o˜B't½Õª®·ÚJ¾SgÍÑŒ¹Ÿ‘év3)(·×Tu›cç2¥èÖdaOß¿Zû[ߨØñO›5WW¯\ņ€’èxe“vnüyñƒõóNÝL  <)øp’Ç_£ÙŸ_ªKn¼C)„Û­ªÊç’¿¶²ŽÇÃû÷kïöÜu™Pc£f65•ì>êü—ί±þ3‰ûâií<šßN„ÙLV}G¢–ó¾—~c·Ðƒ$­#ô°#X@¥‡ ÃÇkªÊgÊY¨^™°¥ûoÿÎY[õV+NÒƒ«îÓÜx€Ñ‹ô +•HS@R*‘Лÿñ+ÉòÁ)úä‚ÏÊ?¡ŽÉ@Á˜¦C¾Z7ÝmâXO”.Ô´GëW}§¢kpõí«4mö\6UÛæ zyý}Št*î‚Ï[$]°L 61 @¹*rðA’æ_~-Á«3¹ ÔUQXF%bCIE–7¡ÊJ¬’æéP•ß%×dCÀ˜„÷ïך•·Wt ¬tøðÜ<¸ê.Å££{j =º<4t¼Om/>§TªxSÁ)šñéÏ(œÂ ` ÃP•ß%_µ‹bØ]¬i룩u˦Š?ÝPl% :4\&Í]!ù&3 €]|À “Bt7€u«œúƒ:ëwéJéw‘d^_3ÒS*•±üØmz¸=ÿ„w À.<PB•v0 C®*§¼>—LÝ0~»¶lѯÿå§7n«N·wûv=ò÷Žêßx€±¡ËðAÅ =t@±˜n§ªkÝtE´¡l&«c=1q­ÆZXy‹±hÅŽÿÆ»¢ÚIA6\I‚._®›ÃË$7öEð¡âÕÖUÑ–QãrhqÐcùõÜIªk(•·×K%3Š•Í<Ù4ôðçápøAÞ…;àqÊ”H¥…NCU>—ª¼¦ ‡Á€¼YØÒ"IzƒºhÙ×´°¥Å²A‡“æ47kÁe-j}a *X•ÏÔ à}þ ušé— z è€b1 CÞj—¼~º:Øv޹C$b)Šaï¶î¬è°Ã¢+—v@Á•$èà J -€JÑÐ’û*bð¡mó£jÛü(Á‹H § <À2’%3Y¹,~üX"¿bÑdYÍÓ¬/|M’ìzø×P($B; Ã% …JzQRÀ.c:SØÁí1Uå79©ˆ‚³s§ϧ9ÍiNs³æ47—Õº éþÛ¿£HÏÈ.(‚A}ëÇ?±|˜¬èXOT™4çy€õꋊFŽååõ: ˜èêP9Êí©—v·õчԺeSE޽~ú ]½òïäñúØÿý²Á~µm~D;7þ¼øA‡¹+r7>¨\t|¨¼ã)Ó¡@½—BÀ2Ö¹5¹Êº×Ëc錶vÇóözåÜÍNXЬ†a¨Êï’§ÊÉ(ª»w뉟þóˆo®·²ÓC3›šÊ:Þ¿_kVÞ>âŸo˜7O7ß}4ŒRl(©è@‚Bãð¾½:¸çÍ1w{ðøª5ûs‹ : h¼ÕnùªéêPI.ZǺ»¾­¾ÞŠ·ÛëÓò•«T?½y5<د×êõk(Þ‚ëçzÂ;œDð¡¢L¨÷r–1ÕçTÓ·e×ï”ÞÈ_G†è`R±Áò=WMèë!ð@Ù=ì`ºªòšòxM&%3<4¤×ž|RÿñÈÃeµÞŸO3›ækfS“f65)ÔØh«yyñá‡G5'ß^ó€&LžÌ £PÎOÍŠ!•Hèè{uxßÞw|¨;oº¦ÍžKÐEãpª™P%ÓÅM9•f¨?¡áh’B”X<ÕÚ•·TäØ¿tã­š»ø lÈ›’殂ML€3#øP|5nyýÉa—…ªär–[¯d&«­GâJeòs^Ù.ç©mz¸,¿È»PŽ<P$v;¸½¦¼>7dÀRŽ9¢][¶èµ'£x4j©u›2s¦&Lž¢Pcãû_•psÿý·GÝŒègoúáÝšÙÄ…i­H\‰XŠBç0<4¤¡ã}ŠôI’"=Gä4]ªž8QN—GÕ&r@ѹ½¦ªkÜ2,xó />œÒàñ8…(±ÃízüÇwWܸ]¹\‹®º† ùÙÏ*UÐá¼EÒË:‚¶fšê½–q~Kç×XïÁùîî`§NÄ6 =D$]‡wñŽ”A(š)i—lv˜õ…eºüŽ{Uå5¹–·wûvؽ[{·¿¦HOá/\‚AM˜Î#‡žÝ¹àC-’àCqL¨÷ÊiòÀ6Xƒé0´8è–×im’îçFèk ð@…B¡ ÊuvX`—1ÍýÒëÊ;ÿ‘ÉEYRxÿ~ؽûÄŸß•”ëq¶0ÄÉÃI3›æK’ªü~…sï÷ÆFUùýù,^{òI=û¿qÖŸi˜7O7ß}Å€1:ÖU&Íù(¦é?à¡c"$I‘¾a¥i QB•x ì€| èÀV>ØN•Ï%­›BÀ2‚UNýAu¶Éßö%Ô3œ¿ãÐè`R±Á„íæmϦ_iÇ¿ý; ‰Ð ìx €ìv˜wùÕºâ¿ÿ“ `̾÷^ýnÇöýŸO7ß}Ïû!ÀèÙ©e8Ø™Ûcª:à¦k"Þ7ÔŸÐp4I!JèñÕwëð¾öŠ+aŒWI‚.ŸÔÐ"]°Œ €Â*Qðaþå×kÆ‚ÅÔ? ÃPÝ…€¥œ_ãÒù5¥ïòøÎ@Jï äï4Ê(rtXv½qëšïéí­OÚiHI ÃáðÞ•€²Ø·'ð@av€7<4¤WÝ¥î>ð÷SfÎÔ×oû6a§t*£ã½1 æ«qËëwQ|@<–Ò`$N!J¨R„0% :\°,÷墻*€"*AðaÆgë’ï øGÕ<^“BÀRš&º4µ„Ûå{±”vËoà~àX\‰xÊÖófÃÐC«rŽó®X $ ía8£][¶èø‘#¹ß™šÓÜLQ O"½1¥R c†j&zär;)>"™H«¿o˜B”P%;`¬:¨hÊšÃihb.°ž©>§š&¸‹¾ÜΡ”~ÉoØ¡’Žg =P(€P(ô ¤›ì2Âå%:˜Tl0A!ÀBLÓ¡ê 9MÅÀ Q„²{à°Æ‚ œ†àCÙª­«"xKšèv¨i¢K^gáÏ$3Yí>žTÏp:¯¯›ÍduühL™tå܃Hè€â#ð@žv@©¥’EŽÆ(X„Ûcª:à–á0(Ϊ¯;*®Û”ŽK®½A Z–2ɱ’|A©¡… ë#øPvL·Sº* knŸC ~Sçטyýd&«®¡´:‡RJeò¼9‰+KUܼÙ1ô‡òŽXòˆ°¬‚&Àª|.ùkÝ#éV*‘¦%²ã©Ç´ãé¶×—n¼Us ƈ”,è0wE.ìå„àCY¡Ë¬Îë44£ÚÔT¯S®<<0a •ÑËã.k IDATá¡´Þ‹¥ t¤øpJƒÇã;g6 =¬ ‡Ã7ónXò$ }_Ò÷ì2žàùstÓš_3±eªRŸ¬VRðÈã5)FŒÀCiµnÙ¤­>d›ñ¸½>-_¹JõÓ˜\œA‚eÁ4 Ô{)ÊB°Ê©Z—C݆\NC5¦CR.ÄLü½~Ù¬b)©?™Ñ±D¦`!‡“RÉŒúû†+þ¡;„(äA(ºYÒ¿Úe<ÁóçhÅ}ÉS]Ë䔩x,¥ÁHœB@ †¡š‰žŠQ‹&LPˆ9ÜÑ®Ç|·-ÆR?}†®^ùwòx}L,Ί äÁË#”äG6“Uß°R© Å¡ŠÀãDØV”Net¼7F! ÈNC5ªdº£Fà¡ôþç_ýiÙaÎEK´äº ;à¬:@|°ôñÚ„I^ƒbã0p,®Dœî§³aèáöp8üf`°²c=QeÒœû€b1M‡j몸ycFà¡ôž¾µö·¾Q¶ë¿äÚ´ e)‰3"èET‚àC`Êt]rãšùuÔÿ ¼Õnùª]£H\‰a‡cÃÐ߇Ãá™Y€x`ŒB¡ÐBI/J Øa<„ì‡'m@ñ¸=¦ªnÂ¥×¾í%=¿~Mùýòú´|å*ÕOo`ñ±Jt/_PòM>õßþɧþ;Ð(¹üü;°ìNÞ‘\ð¡ó…¢-’àÃ9ê3ÉKW>` bCIE8f=›gî¹Eáö7ì4$BK ðÀv@9à¦I(·×TMÀC!Àg· ÄcQ­»ëÛJÄ¢e³ÎÓfÍÕWÿj¥<^ˆ(Ë ÃXœ GLhÌ}4æþìò³°ÐÎÁ«0ÝNêª(0ªc¥”#q q‰è€ž¹ûõuuØiX—…Ãá™]@)x`”; \$iõ÷ S( *ŸKþZ7…@^x°†­>¤Ö-›Êb]]¹\‹®º†IÃGDºëéü¶îÞ®l&S¹…ðO…ê›A°ÈNÁ+ðV»å«vQ`;ŒŽ CI—†Ãá]Ì. T<0 ¡Ph‚ra‡va{Ëf²ê;¥P Õ<^“B o&JÅÌZ$ßdê 8>UuÀ#×d»CE‹ %LŠû ó¯¯ówzæž[”ˆÚeHÿ‡/efÅBà€³ ì€rNiðxœBÀ8˜¦Cþ€‡° îXO”§hZÐsëî×Þ×¶}¹ÓfÍÕ—nºUµ“‚L:XIý¼Sá(‚E˜äå¸)›Éj0’P"N§àB²aèa]8¾™™Î  Ý,é_í2ž×ÐÙ•(Ki¨?AW‡"!ôÀ÷ÕÙYà£ìvXúßîUÓW–3±(•Ì(r4F!` ÃP`R•œ&7» ðÒ©ŒŽ÷ò™me;žzL;žÞX°×¯Ÿ>C Z®ÐÜÅ_ Ø èPnèú ˜J|˜ùõúÜ5ßT`Ê'lHè• •Ìhh ¡T"M1ŠÌ†¡‡ÛÃáðO˜Y@A÷Ó <ðA¡Pèë’·Ëx;€§EÀèq“ Š®LåápG»ž[w¿úzóöšs.Z¢¹‹¿HGH"èPö|ÁS]\~ê À;Å>HÒü˯Õ%7Þaëàǃ°³t*£èPR‰XŠb” C‡dfÛG'ðÀ)¡Ph¡¤%ì0Â<ÀXTOðÈSeRMt0©Ø`‚B”‰öm/iÇS9ø0mÖ\}rá…š³ø òx}êjݦ—×ß§®7·•ÿ`B‹òó:áå[—OšÚ,Í]!ù&³(,‚a†ü7Ç…°l&«X4¥á¡¤¸_Ð"Ço¼ ç|‡†DèP¸ýsv`È!ì»"ð£SåsÉ_ë¦(ªcq%â›ØàÁ‡Â|,<òx = |ÑÑÁÚö½ô½üÀ÷í2œˆ¤KÃáð.fo …&HÚ%©Áã¹øÆÿª‹oük&’<Àh˜¦Cz/…@Ñë‰*“æš P)Jtp×H¡ ¥úÜ÷ÆéÁ†êé§þüacþÕTäßi¿ß! Röh»tt¯õ:DÔÏ“Zr_PH% >\¸üM9žíJêöšª xضPVâÃ) GSJ%ÒÃâ=pnïDØáEI ì0žy—_­+þû?0±x_¤o˜ [0BI^™.…@Q¥SïQ ´mÞ ×[«#ï¾U¼…ž·HÆþµt^óÈ~~\—-víùh{®#Äï·ŸD¼Wúuò¥¹+>(¼“Á‡Î-R´§h‹ñ™ÅºäÆ;4cÁb[•Ó4ª­«’á0ض`Y©dFñXJñXJÜX^lz˜‡3³€|!ð¨x¡Ph—;ÀÆ<ÀÈx«ÝòU»(Š.Ki0§€µmÞ —×ß§H÷¡â-t4A‡réâ0žuËJ<,u>§ìïwH]Ï—võ\>é‚e¹/—Ÿ7 €ÂêÜ"µ?Lðaœ Ã?à–§Êd›‚uŽ'‡SJ%2JÄSt ,s¿Ý¸F»6®±ËpZ•ëô@èŸ}q€J …”t“ÆBØgBàÎÍ0 M zyZ'Jb W"–¢€Í ökß«›¬t°{Èa$«Ùùœ²]ÏIÏK‰Ò¬2ÁÅDð!/ª|.ùª]?¢$’‰´’‰Œ’‰4ç|mhëšïéí­OÚe8„yCàP±ìvøÄgéúÕ1©øXàܪy¼<¥¥Ñ××kûì×ÎkõúƵŠñ&ús8œÂ —IsWH¾É¼‘V‰‚ó¿ræ_~-Jèpªxär;ÙžP¸=LV©T†€C…±Yèa]8¾™YŒ@E …B7KúW;Œ%xþ­¸ï!yªk™X|,pv§¡‰A…@IćS<§€ X2è0®KÁV½Žœ-üjv>§ì¾Ç¥®çK3D‚Š¥Á‡À”éºäÆ;l|p{Lùj\rš¶'Œ[*™Q:•Q"‘V&™Q*•¡(ꕾ§Ž—=p@Å!ì€Js¬'ªLšs@p&tw@) DâJÄR(c‘îƒzyý}jÛühq|¦ ò'Ñ/í{\Ù=ë¤Á÷Š?ìúy¹àC°‰7€Â"ø0.†a¨Êï’×gÊplO‘t*hH§²J&ÒJ'3tþƒ$in}•ίsë{õ§ÚýÆv» ëöp8üf0æ}nv”•$ ]*é;Œ…°Fêhxˆ"À†¡‰A/7¥ dúº£ÜÔ”)K9Þïw(ûöFißÅ_6ÁÅÒ³;|èÝS´Ež >̺x©ªÊüº'Áœ áŒè÷a•SŸ=Ï«@Uî¡CýúÛ¿üÏÚßÑn—!þy8~™Œi_›'@¥…B %½()Pîcñøktãš_+šÆÄàœ<À™Uù\ò׺)J">œÒàñ8…ÊLWë6½¾q­ö½úlq<ëj³—ç‚ãºÄkÕëÃY ¯æi+3xXÙ}Kû/~×_0|hhá °J|ðøkô¹å·èÂå·|@YK&ÒJ%3ʤ³J¥2J%ÒçÔpkþ”*¹œŽü½ C—…Ãá™qÀ¨÷± <*A(š)i—lv¸~õCš|þ\&#BàÎ,0É+Óå (‰cq%â) ”‰®Ömzyý}êzs[q<ëjx›T=ÖàÐÅ¡`uÛ÷¸²{ÖK}{‹»j Á‡q1 C®*§|~—œ&Çv“L¤•Ig•NgOü9rF«!àÖNõñÿÛ,ô‘ti8ÞÅÌFµoMà`w¡Ph‚r”ûX;`´Ò©ŒŽ÷Æ(| Ót(Pï¥à3ÀYµmÞ ¶g7”YÐCQköûʾý¸´ï‰â®²Ë']°,÷åòófP8% >Ì¿üz}îšo*0ååüévªÊkÊã5ÙžÊì¸-“É*™È(›ÉulH'3â~;äËyÕ.]ô‰sïǽû»·ô·ùŸ°Ã°#’†Ãál€‘"ð°½P(´K6;HÒkž ì€QI&Òêï¦ð1¼Õnùª]%1‰+£»`em›7èåõ÷)Ò}¨¸ sÐÁª×}³^Í<¯Ìàaeÿ¿ÿ)u=/%Šx3ÁÅÒ³[êÚ"u¾PÔÅοüZ]rã¶>œìúàö8å©"ü`©†Ü÷ÜùT7…äs9ÔÒX-—sd`lzhU®ÓÃq¶Àˆö£ <ì, =(é&;Œeé»WM_YΤ`TâÃ) SøI^™.…@Ñe3Yë‰ñTPÀ‚†ûÕ¶ùíÜøóâÜ5Ò¼›eÌ^>Š C™tq°Üªf ÿ’‰e÷¬“ÞZ_Üàƒ$5\&Í]!ù&ó†P8Ñ#¹ŽÆåôðƒÛí”á0ض àd—†L:«t:«t:£L:«Ì‰ï@©,™Q­zÿè‚Ov =„Ãá…l €í;s2`W„€ÿŸ½{¯ª¾óÿÞ—\È…PJÈ!Á V"X¹H¥¿öð´L!=ýýfÎÌ©S¦_ÕyhOµö<:Xµ¿vj‹Ð*êÁô7 §FZ5‚Ç A@@¼­J ÉÞIöm?6PT.¹ì½öZk¿žGHÖú~?Ÿ•dgïýÞ)ÜUwWÄ››³,%¬O¾Òšß ñ.ÊçóixY…?ŸHJvnX­V«7d㓇N&ÖJ¹Cûr#ØÉ7кL+s‡tJ­¿—Õô¿¤®ìÝ6Á¶Ü°ÍLðaüæjjMÊ'ÍðT9ƒ¹å䔓ëWN.÷oõéÇñY“¢‘äŸñhB ËRüÔäÀ‰*JrõùQ»_êÕm[õÃåç•R9 j]N9äg3|Q™ >”Nä @úd(øP~Õ Í\ºÜsÁ‡Ó‚¹ƒ~sý ý ³kBa4—¤3“Îþ7 p»¹ã†ª`SG_¨ÿ=|ïw½RB€‹"ðð/…*«ê†ïþˆ¦`À:Ž÷(vê@·‹Ç¢ŠÇbRªîÏúx BúK(B>ùü~. ÀÆ媠(‡BÀvž #n½Üö®¶¯]¡Ý[Ÿ±÷ÄE—Ê÷ùÛ¤ñ{‘§>†k9x™–ƒyÖÌ×d½ñ3ÉÜaoy>°C¸]jmÖKѰm§-¿j†¦Ôܬ+®›çùŸAø>süòû}® BÄÎ *œžÈpö”‚ ȃ™îp6…¾išæ®ÀùxxŠa“%m“Tâö½v@*üÉ ¹–¥h$"+apÃç÷ËïÈHþ À;†ÏWN._×°W,šPÇŸº)Am‡öhç†Õ :0ÅaÐksÔ!ûpLJÆJãæKsø† }¢¡dèÁæàCIÙhÍ\º\UÕ‹²®äþ€Oþ€_~ŸOS¯úä÷'_äÃçK$RýûÍÙÏ;Kþ=ù~<žPâÔTB ÀG͹¬H%ùÁ”ëÉ•ëÉU{¥4„çEàà„€ŠÇ:qÌåOª´,E#½²‰Ì¯ÅçW T äâ<`øÈùN=ñ°KDZî3¯\ À^Gv½¢íkWèÈ›¯Ø{bcZ2èpÉôsÝØuò q‡.Órðax®÷“Á‡ƒí-eAirâÁéDðÁ‚A¿tžß +)¾ –ã×ÜqCSżîùŽ~÷[/”§CÒ,Ó4›¸RGàà †a S2ì0Éí{)½üJ-Y±NyECi,¥·'¦®½îÝ€“Âg#ø¸žÏçÓð² [u‡¢ wF(`³·ÿk‹vnø…ƒ‚N}lÖrð2-2…kËdð¡bNrêCN!ß4¤G4$µ6$ƒ᣶6¯°XSkê4¥¦Nù<ö À.ž§«Ê†¤ü¸„^Gààz„€s Œ¨'uíúãѨâ1¯ßçS ”?”ÏÇ«ÄnÌ ¨dx>…€mbÑ„Nïá•QíÞú´¶¯]¡Ž¶÷ì=ñø…òU.“>=áÔ?¸dŠƒã–êÔƒ EŠtÊÚ³Vzk­é´¯ä9ÉÐÁéÖÚ í]Oð@Vºvt¡.)ÎI˱¿ý¯èðÛ{½P¦]J†NpÅN#ðp=Ã0ÖHªuû>†–RíÊM„2Ç{‹Ä]¹öD"®X¯{¦S‚9ɉW ðÛ&ëV,– @šõtÔ—·d.èðù”Š.•kB^8¤ì°*T&ƒ£¦K–H#ùÆ }2|¤ªê5sér•”¡l7ç²"•ä§grp¨ó¤î¼åž =˜¦9™+p€«†ñ¤o»}y…ÅZüà:¼|M2È<|X[û…Z÷ìI~mhòœë5kÉåòªf÷ù“ríÚ£½½²î køü~só˜ö¸À¢\åPØ¢³£W‘î…Ò¨§ë¤vnX­V«7dãÃs‹¥ñ5òM\&rp…œrHÓ‚Ür8ŸÏÊjú™Ôõ½ç­˜Mð@úµ6$ߎí±õ´d ÃÒz|…7MsW @"ðp1Ã0–Iú•Û÷AØpóða­¹û.õ†ÃŸø¿²±cµì¾û =\¥·'¦®½®\»Û¦;|„ϯܼ<&=Gà¶ý<«£—BiÒÑö®v?ÿtf‚•µòM¬•r8±×ºà_µ6ÇÒáS9N/ïàÆÌÊçH¥ùÆ }Ž6''>Ø|(¿j†f.]®òI3耴KwàA’ÞÙÿ–î¼å wuz¡d›¦y;W€ÀÀ• ÃX éY/ìeñŠu3iM2èBa‡Ó**+µì¾û)À5B'#ê G]¹öX4¢D̽¯„íóû•“—ÏE8Ø!‰ëäñ ¤AGÛ»Ú¾v…vo}ÆÞJNs¿ÐA§Nq°|X?N~±Çð¼ ë­u’¹ÃÞu¨LN| ø 2|˜Rs³®¸n=6v$Ï…¾išæ®Èn®cÆdIÛ$•¸}/ó¾ó€&έ¡©@õ%ìpÚ¿üú7Ly¸ÆŸ†•ˆ»ó~ŸHOd%\]ÿ@0GžL 8¤[,šÐÉã=â1 µÚíÑÎ «3tøümÒx§Ý—Ë$G¬+•ìïòÌɉ™>TÌI¾@ºt–ÖK­/ÚzÚ’²Ñš¹t¹ªªÑ)gWàAJ†nÿï_õJéf›¦¹+²€«†1LR‹;H¦†múé#}þøÚܧ±yõ2€óÅc 8ÖíÚõGºÃîo‚ϧܼ|ÉçよÀÒ‰°zGv½¢íkWèÈ›¯Ø{âáWÊ7±ÖAAŽY[ªÖ•ª¥ß'ë­µÒÁMön¡ 49ñà€t ·''>d ø0¥æfUU/V~ÑPú %¾X^¤…AÛÎ÷Býèá{¿ë…ÒuHšešfWd'×8vØ&i’Û÷RY½P7|÷G4È -=¦ÆßÕ÷ës¾½r•†IñŽ×Š*ÜqåÚ‰¸b½½žèC 'W` p HÂ@j½ý_[´sÃ/ì:Ó’.™æ€*Xç|טâÐÿ¥¥ym]ï''>d*ø0jº”Ä\ir:øðA£µïÅ2ò ‹5µ¦NSjê>´kGê’b{ï“òXèa¬iš'¸’ ûx¸†a%}Ííû ìdVO(¤õüP­{öôëóÊÆŽÕßýä! p…ŽcÝŠÅ®\»—þ@@ÁÜ<.HÀ< z{b uD;)°{ëÓÚ¾v…:ÚÞ³÷Ä×ËW¹ÌA§†,6‹CçÒõ¾¬ƒ¥·ÖI‘NûΛS ›Ÿ|#ø ]¢!é`}òÍæàÃ×ÍÓÌ¥ËUR6†> #òuei¾íç]ýo?Pý“¿òB w)9éÐdW0 c¤Z·ïcÌUÓ´øÁu4È}ÚøÈÃê ÷ÿðé_¯y7ÝDŽ%tâX·k×ï¥Àƒ|>åæá¢ˆÀRÍÍÓ•§èé:©/oÉLÐaüÂäD‡¢K3´{ë‚Í,ËÁ‡têãÜ–s–é”õÖÚÌ*æHLÌ& >HRUõšRS§²Ë+é€~QÔ+Š2rî‡îùŽ~÷[/”q“iš ¸š »x8žaË$¹>j^zù•Z²bòu ØîD{»¶<ö˜ö¿Ö8àcÔþà>8‘b/t2¢žpÔ½°,Ezº=ÓÜ!\”€#¿6ƒ*.a Rðc+a©«#¢HoŒbÔÓuR;7¬ÖŽ «Õ²ñ Ù¹ÅRÅ—3t äàˆu¥rƒ–ƒ×锎¼ «éçR×öž¾b¶4a ÁéÕÚ í]/…ÚzÚò«fhæÒå*Ÿ4ƒ賯^1T9FÎ}ÿò[Õ¸íÿõB7MsWdG3 c¤gݾ¡e£T»raÀf=¡^­¯×žZ?ècÝóìF p…? +w÷ý=‘žnÉ#÷Yxœ)˜PÉð| A‰FâêêèuýÏ] S:ÚÞÕîçŸÎLС²V¾‰µR®Ý÷×Zç|7ó,ÖÁßc-O˜¸ƒ >ð® F~æsšúõ:UU/¢.êšQCTž¡âužÔ·|C‡ßÞë…R~Ó4Í5\Q<Ë0ŒÉ’¶I*qó>ò ‹µøÁuyùš ØäD{»šôjýsê ~ŒqEe¥–Ýw?…8^oOL]'z]¿X¤W‰xÜ=!ð8S0èWɈ!b%,uuFéfª0mïjûÚÚ½õ›o˜e*èàÔCšDÈÁ=ý<¸QÖÁ’¹ÓÞeލLJ™¦ Ž6'ƒÇöØzÚ’²Ñš¹t¹Æažòy!<ç»9TÔ+Š2v~…š¦É+@ ðp$Ã0†Ijaý°¯±Qûµëņ”wúWçkÞM7Q`€ãuïQ,âþ @"W,âþà†|~åæó ò€S}Ú(¤è+a©;SO(*[S˜ IDATp[5SA‡¢Qò}þ6i|]ß-.ø×´hý½¬ãû’7AiÒ%Óú¶¶4l×Jýû-?E ûp‡¬¦ŸImxP†‚y…ÅšZS§)5uœÓË‹4¢0˜±ó¿³ÿ-ÝyË7îêt{);$Í2M³‰« ¼ÀÀqN…¶Išäö½,øþÏ4îº/ÓTdDKsó™÷{B!™‡Ÿÿëî²Ë”_XxÑs¢Ó!‡–æÝê8šžÅ_»í5yÎ.*€£Åc 8Öí™ýDzz$+áê=‚9 ääpqUòé! æø).Š 08Gv½¢íkWèÈ›¯Ø{b[ƒ˜âÐõ¾¬ÏJ{—"{’Òð+åûoHÃ'ˆ)YšƒÃé<”¹#9ñáÐ&{·UPš >Tpÿ+€4 ·'ƒ­/ÚzÚ¼Âb]qÝ<Í\º\%ecè€32=åAòTè¡UÒdÓ4Ope€wx8Ža%}Ííû˜÷4qn EZ™‡Ë<|X'ÚÛÕÒ¼[=¡ÚZZR~ž’ÒR 9R’4ldÙ™÷ÏEŒ8Ñ–ýžho—yø°Zšw«u=¯ÈSûƒûlÙƒÑÙÑ«HwÌ3ûñ”‡œü!òù|\œ€C ËS^~Bà¼â±„¡¨¢=q‚Àd,è`LKÎ;å U20ÉA’Žï•µçqéÀÆ \n±| ž•Š.MËv3xôõÓë“úz˜®÷e5ýœàoÊPðA’ªªoTUõb•OšAH’®]¨KŠ3û‚9¯nÛª.ÿ;/”s—’“=€Gx8ŠaIú¶Û÷ñ…¥ßÒ–ÞFC‘r-ÍͧÞì{²ÿ@TTVþåëú²Ï|bRÄ…ÂgO¦8ÑÞ®ími rôÕ=Ïnäâ8š•°t¼=ì¹}Å¢%bî q0Ýp¾ü‚Í¥ø„ÓA/ ;íÞú´¶¯]¡Ž¶÷ì=qÚƒ 8œÖúûdÐÁÜÑšL•ïÿX›’í¦¬nÎù-Š€ÃÅœ>¼û‚é²oû¥ÉÐøùRŽó'p©hH:XŸ|‹Ú{ŸZùU34sér‚”ã÷é‹…*Éð r¼PÿzøÞïz¡¤›¦¹Œ+ ¼‰ÀÀ1 ÃX&éWnßGeõBÝðÝÑP¤Ì¾ÆÆSo¯ª7¦ 6+)-Õí«VS€£…»¢êîŠxoc–¥h¤WV"áªeûü~åäæILw-ô«dÄ 3¢‘¸Â]QÅ"qŠ @Æ‚ã&ƒ©šdðѤç|×6‘“Rë ²Þø©ÔõÁÀn›þõkRnq¿·›’š9ï B9`¤SÖ[뤽ëì >ä$C¤õ—€SÁ‡Ö)|ÔÖS—”ÖÌ¥ËUU½ˆ>Y¬$? /–*'àÏè:ž\ù°ž\õ°Jú°iš·se€÷x8‚a³$½èö}Œ¹jš?¸Ž†bÐN´·«©¡AM /¨ãèQ ’A••ZvßýàXVÂÒŸv˳÷ñ¸,ôàÌÉ%ì¸Äð‘òùùzÍv€ëé:©VkdžÕê uÚ{ò´2r’¯¬¿çqéÀ³RdpuõÝðø…§^x=ä`9x„uÁž>Ú4à΀äüeâCÁH¾ñHŸÖiïúŒªªiJMò‹†Ò •ätÍ%C2>éá¡{¾£†ßýÖ %ý¦išk¸²À[<2Î0ŒÉ’¶I*qó>J/¿RKV¬SwDaN´·kÛúõÚõbÅpˆI³çhÁ?þ#…8VèdD=á¨ç÷F9xŸ>¿Á Á %à"EÃò”—Ï×m¶êíŽ)ÜQ"Îã$@e,è[,UÖÊ7±VÊMåý°9HÒ‡¯É:¸A:°1u7SÏx ä©…9òP:àÁM²výÜÞàƒ$UÌ–&,!ø ½Z’oÇöØzÚ¼ÂbUU/Öԯ߬’²1ôÈ29~Ÿ®]¨…™½ŸêÎ[¾¡æ×½PÒ«MÓlâÊï ðÈ(Ã0†)v˜äæ} -¥Ú•›;`ÀN´·kã#«uÏŠá0_Z¼D³–,¡GŠÇ:q¬;kö›HÄÆd%ô ÜWËTqI…È"VÂR¤7NÐ Ž¶wµ}í íÞúŒÍß°St°.øWÛ}øš¬7~*™;R^7ß_¿æý€ƒ,/ÏrÔaR~ÀLÊçH¥ùÆ }Ž6''>³ÿ±»ªê5¥¦Ne—WÒ Ë\>}ªtˆ|~ÅðüÏKÝá˜zBQñ¸Ð :’ïó·IãkRñàœïfÜ ²^º3=ÇþÜRù¦/55sÜ7v¦88ªŸæYM?—ÚvÚ[î•ɉ¤S¸=|h}ÑöS—_5CSjnÖ×Í£@Éñûtei¾*Jrl >„£ í;Ú£þI›î\¢®cº½”»$Í2MóW¸@Ɔ±FR­Û÷±tåFÂWëëµmý“ê ‡)†ƒÕþà>Èfç‰Fâ:y¼‡B(9ùAJ>™õôŸ§‚–•HÝ“¡|>ùAùü~ xDQIžò†^òªx,¡žpL½Ý1‚ÀÙõж¯]¡#o¾bó7çTr8½ _“µ9Mw‘¿R¾Ö&§c ´fr`͘äàì~f2ø0n¾4j:߸¤O¸]:X/µ6HQ{Û+)­™K—«ªz}²HŽß§QÅ9*/ÉÕˆÂôÝwõagTvFÕÚ9óoÇ[÷kóýuŠ„»Ü^Æ?˜¦9‹« ÜÀ # ø]ÒOܾyßy@çÖÐPôˉövm|äaµîÙC1\€À첯±QM ê %ï8¼rúµšŸ_þ@€àQÁÜ€J†çS‰Ç ‡¢Y9 H…ŒŒiÉ Ã%Óz«ï‚Íð-ÒOþËï¿%y!µ§É-–Æ-”ïêoõ#ìàài œrÐþéú |8´ÉÞó”&'>TÌá9€ô‰†’Á‡ƒõ¶ò ‹5µ¦NSjê”_4”^Y$ÇïÓˆ‚ †å4¢ ¨œ€T’?°D4žÐ‡]QµžˆêXøü÷›oݯMw}à å{Ü4Íe\Eàn¶3 c¤gݾÙ§®ùz- E¿454hËc¿`ªƒ‹x€6>òˆv½Øð‰/)-Õ’ïÝ)ã²Ë(€èEîŒPH‘’OQ0‡P“D#qõtÇ:´{ëÓÚ¾v…:ÚÞ³÷ă :8|ŠÃ…þ÷×Ó¤HgjNU4*t¨¬íCÐÁÁ[_8ðPŽìçÙËéz_Ö®ŸK‡ž³w §ƒ£¦K9¼˜€4jmö®—ÂGm=m^a±®¸nžf.]®’²1ôÈb9~ŸJòÊñû4,? `À§ayO|\8׉ž„Ž…cêè‰÷ùøþøœ¶¯º× ¥ú¦išk¸bÀ½<leÆdIÛ$•¸y•Õ uÃwDCÑ/[{L¿«§.óí•«4läH ´9_Øá´’ÒRݾj5…pF<–PÇŸzÄ}::¹C‚*.É£.ÄîŠ*‰S `2t¿0t(º´ŸŸè®IçýÈ_Nü錩ò[(_˜²ue¤fLrÈôS¸¤s¬­ëY‡6I{×I‘.ûÖ”S ›Ÿ|#ø Z’oÇìŸî>þ s5µ¦Nå“fÐi±gËzí×ÿæ…­Ì6Msw"ð°aÃ$5Iªpó>Æ\5M‹\GCÑg=¡6>òˆö¿ÖH1\èžg7R¤Íŧ}í¶Ôä9s(IRÇñžÌ i0lÄ‚Lyp›Þî˜zBQÅb ŠôSO×IíܰZ»·>í’ ƒS'9 |1ÖÆ…Òñ}ûäq ä¿P2¦¥emé/“pÀ.§k‹tÊÚûk‚¼ëh³t°^úð5ÛO]~Õ UÍ]¤ªêEô@ʽ´ò|Éõ/îØ!i–išMt܇ÀÀ6†a4Išäæ=”^~¥–¬X§¼¢¡4}Ò iÍÝw©­¥…b¸¤K_Ã’ô¥ÅK4kÉŠ@Ý¡¨Â i›Tñ§˜òà½Ý1…»"JÄyŒè¯ÓA‡V«7Ôiã7Úb©²V¾‰µRn_î_õƇ åÿ%½ñ³þÕpÜBù*—ž',‡Lö3ë§8ôG¤Sz·AVÓ¿K¡ìÝCÅli©€©¾Ò(Ü.í]/µ¾hû©KÊFkJÍͪª^¬|ÓB›ï¯“¹÷u·oc—’¡‡tÜ…ÀÀ†a¬‘Tëæ=äë–ß4v@ŸvðpÂ÷$)Mèäñq_¤ÏÐáùÊÉ P‡²–ºÃ1õvG :ÐÑö®¶¯]¡·ÿk‹ƒƒÞ9|Do§¬ÍK/>å¡h”|Ÿ«•Æ/LÖÓŽµ¥ªf„œrÐ,'Më:´‰àï ·K­ É©Ѱ­§Î+,ÖÔš:UÍ]¤’²1ôÀ EÂÚ|_ŽyÛí[ùƒiš³è(¸ @Ú†q»¤Ÿ¸yy…ÅZüà:¼| EŸ=zÇí„<€ÀR©¥¹Yëø¡zÃý{`£ö÷iìĉÈrǺ‹%(¤‘?àÓ§J („Ü:ô„¢ÿ€ÜŽ<tؽõ{O\4J¾Ïß&¯¹ØWù9ßuÈw ô²ë}Y/|ëÜ¡cª|ã&ƒé^WÊöçð)޼,,—ÉÆµÚ$ëà&©m§½ûQ™ >”r߀4Іþ|µýôUÕ7jJMÊ.¯¤%îÔ3·E‘p—Û·ò¸išËè(¸@Z†±@Ò³nßÇâë4fÒ4Š>ÛøÈ#Úõb…ðH•më×ëO­ï÷çåèþæ d¹ÎŽ^Eºcl0¤(WE9Âⱄ¡(?:²ëíØ°Z^~ÞÞ_4èeSúzÈ#/ÈúÓ^I’¯èRéÓ¤áWf¦Hæk²l”ºÞïïÇ-”Œ©Þê§×Y’Öfîµëß >ð®ÓÁ‡ŽÛO]~Õ M©¹YW\7>°ã­ûµùþ:/„¾išæ: î@à6†aL–´MR‰›÷1ï;hâÜŠ>{µ¾^Ïÿò1 á0X-ÍÍÚòØ/<ñå³Ó¦kÉ÷¾G!,ÖÛSWG/…•|zˆ‚9~ ‘!±hBÝa‚À@Ùõж¯]¡#o¾bï‰iÉ Ã%çzá˜,Ÿâà”u]L¤SÖKß“Ž ò…\Æ}M¾™?tw/™âyæY‡6I‡ž³ù†àXiÜ|©b?P¤×ÑfiïzéØûç-­™K—«ªz}0°ß;_Q/üd¹¶rµišMtœÀ - ædØa’›÷ñùš¥šówÑPôÙ‰öv=zÇíê ‡)†GxÀ`¾l[¿~ÐÓ^æþíMºvþ| d©X4¡“Ç{Äý7`/À§aŸ"ŸßG1lÄîŠ*‰S `vo}Z»ŸÚAA&98b]ý9û ß|Øá´Ïý|Óþ§»úÉ$g~}v}œø`wð¡ 49ñà€t ·'ƒ­/Ú~ê¼ÂbM­©Ó”š:å ¥úåÀŸÓöU÷º}’&›¦ÙBGÀÙ<ÒÂ0Œm’¾äæ=TV/Ô ßýÍD¿¬¹û.µîÙC!<„Àú+UA‡Ón}ð'2.»ŒÂYÈJXúóÑnÂ!¹yA*BØ ·;¦žîA`€vo}ZÛ×®PGÛ{öžxüÂdСèÒ³oÅžó]ܺvðaR¨ãûdmJí¤cßÿ¹õcׇûÉ$÷|}ž>¼Û Eºì[^Ai2ô0n¾”SÈén—Z¤ƒõRÔþV«ª¾Q3—.WIÙz Ï^Zy¾Tïömì’4Ë4Ítœ‹À å ÃX#©ÖÍ{(½üJ-Y±Ny¼’ú¡©¡A›~ú…ðè«–æf554¤,èÀ5d7+aéäñÅb Š”_£Â¡¹"M?ë"½q…»"JÄyœ诞®“:ðòœpHÓ‚¼pøøªÞø™Ôô³”Ó7í_¤Ï-uV?™âàþ¯ÍÞNY{-íýµµ1øS =|nÑôAcrêCø¨í§/¿j†f.]®òI3è€>yá'ÿ¤#¯osû66™¦¹€n€sx¤”aË$ýÊÍ{Z6Jµ+7v@¿=tK:Ž¥ÓÍïD{»N´·Ÿóÿ†©a#GºvoæáÃjjhоÆWÓòõ_QY©e÷ÝÏEd¡Žã=¼Ê58DQIžò†)DŠÄc õ„cêíŽ1Å€ž®“Ú¹aµvlX­ÞP§}'Î-–*k廢F*õÑÿóú$‡ά͒¬¦ŸIM?Oí¡'ÿƒ|“ÿ¯Ì׃û¿>Ïõ©‘ FM—&,‘ Ü{¿&—hmH¾³º|IÙhÍ\º\UÕ‹è€ Š„;µù¾:?ò¶Û·ò}Ó4北àL)cÆdIo¸yy…ÅZüà:¼| E¿ìklÔS?z€Bx‡óë …d>,óðaõ„BjiÞ-)pÌ“ÿKJKÏ„ ò ‹d\vÙ©÷ ϼŸ© DKs³ÌÇÕÒܬ–æÝê §w¬ôô¯Î×¼›nâb²LgG¯"Ý1 Bèaðz{bŠtÇéåg0mïj÷óOg.è0q©”{êb˜ä¹u¥lŸ\[:ýŸð`9ø²°|Ù{t’C>ýÐ&Y»þ] }hï+f|`£ÍÒÁzéÃ×l?uIÙhUU/Ò”š:åó‚Î#îÔ3·E‘p—Û·òMÓ4×ÐQp€”0 c¬¤&I%nÞÇâë4fÒ4Š~[ÿÀÚÿZ#…ð q¢½]ûO=éÿÇL4);Vù…É1òÆeŸ9óþ¹Bc'N¼àþΞDÑÒÜ|êÏÝê …ÔÖÒbûÞæþíMºvþ|.> ‹vç"ôÐñXB‘Þ¸zÂQ%â< DGÛ»Ú¾v…vo}Ææoz£äûümRÅõÉ S2³®TnðbË3wÈÚ²,¥gõýÕo¥áWÚS7¦8¸ÿë3ÛÊdð¡|ŽT:‘\Ò+Ü.í]/}Ð(Eöž:¯°XW\7O3—.WIÙzàŽ·î׿ûëÜzè4Ë4Í&: ÎBà0h†a “´MÒ$7ïcÞwÐĹ54ýÖ éÿùëÿA!<*ÛæáÃjjhоÆWpÈ&µ?¸ï‚! ÞBØœ¯ 8WC s(ÄE0ͼŒÆ/$äɵ¥j]ý\šµe™dîHÍéËçÈ7ç§é­™å’k-%Ë!äÐ/m;“Á‡¶ö–bDerâÁé %'>¬·=ø Iã¿0WSkêT>i½ðG^Q/üd¹Û·Ñ*i²iš'è(8À †±FR­›÷ðùš¥šówÑL HSCƒ6ýô áQÙx8ÑÞ®¦†55¼@È!þ½rÕ'¦Tð&Âà¹C‚*.É£‹&ÔÛSowL<î Ü‘]¯hdžÕ:ðòóöžØ˜*_e­TñeUƒIý_Ú ×锵¥V:¾pÇþYùæ=.å§¾nLrpÿצeãÚÎ^··D#*¥Š9É7H·Ö†äÔ‡°ýçŒüÌç4õëuªª^DœqàÏiûª{ݾ]¦iN¦›àƒbÆí’~âæ=ŒûÂõZðÿœfbÀ6>òˆv½Ø@!<*›-ÍÍzµ¾^û_k¤ñ\lb%,uuF;€Ëƒ~ ËS èÏê:Äc EzãŠtÇ‹%¸0€A8²ëm_»BGÞ|ÅÞSå»ú6é’iN¸uìàÃz8äðq‘Néà³²Ž4ôÚƒ1U¾q ¤q R[3¯‡¼>Å!í[ëÃÁï—µï×Ò¡z{ËVPšœø@ð€Ž6'ƒÇöØ~꒲Ѫª^¤)5uÊ/J/襕÷èàKõn߯ã¦i.£›à fÆIϺy¥—_©%+Ö);^0kî¾K­{öPʆ'œ·47kÛú'¹Ž¦¤´T·¯ZM!³–Nïá ¢àR>ŸO…%¹ÊËfÕ¾ 9©µ{ëÓÚýüÓöÆ/o\‚LqèÿÒ<úªÿi-¿åàR1ÅÁÖƒw} ëÍÏLðaÜüdð!§~Ò+Üž >´¾hû©ó ‹uÅuó4sér•”¡@–Û|̽¯»}ß4Ms Ý€Ì#ðÃ0&KÚ&©Ä­{È+,Ö-¿i ì€AûþÂÁüx0Ö–Ç~AÐÁ¡**+µì¾û)àQ±hB¡Ž^ž( ›TAqާ§=rRo÷Ö§µ}í u´½gï‰Ç/HNt(º4ƒ»'ä0 µY^›#Ëo9¸Tä`¹à:;Å×õ¬CÏÉÚûk)Úe_ys ’Á‡qó >H¿p»ÔÚ ¬—¢aÛO?þ s5µ¦Nå“fÐ KEÂÚ|_ŽyÛí[¹Ú4Í&: ™EàÐo†a S2ì0É­{È+,Öâ×iäåh(Àƒ·y1ðÐ iÛúõjü]= v0€wõöÄꈈûdÀ;|>Ÿò s4¤ (Ÿßç‰=E#qEzâŠôÆ”ˆó3 HÉïã]'uàå-ör‹¥ñ 嫬ÍPÐÁrða™äÑš1ÉÁýýtIÈá¼·Î"²öþ&3Á‡Š9ÉàCÁH~@Hó/w!éƒÆäÔ‡ðQÛO_~Õ UÍ]¤ªêEôÈB]G?Ц»–(îró6:$M6M³…Ž@æxô›a%}ÍÍ{˜÷4qn ÍDJxð¶ùõo”_èWÛjinÖÆGVÇÑ£4×á¦u¾æÝt…<&ÜUwW„B€Gù>å*oHÐukÇŠEŠôÄÄ æ)ÔÓuR;7¬ÖŽ «Õê´ïĹÅRe­|•K¥\»§Ü2Åa@kcŠƒûûéø)Ùp.r8—H§¬w_”µëߥЇö¶¤b¶4a Áöø 19ñá˜ý“¿KÊFkJÍͪª^¬ü¢¡ôÈ"Ç[÷kÓ]ßpû6vIšešæ : ™AàÐ/†aÜ+é7ïaößß©k¾^K3‘2¼­ö÷iìĉžØË–Çcªƒ‹|iñÍZ²„Ba%,<Ñ«X$N1 ¸a⃕°‰Ä‹$˜â¤IÆ‚E£¤ñ5:X>¤ƒŸ|NÈÁý½t|ÈakójÈá<˰='ëÍ ÊçH¥Þ¸/€Ãm–Ž4H­/Ú~ê¼ÂbUU/Öԯ߬’²1ôÈþøœ¶¯º×íÛxÜ4Íet2ƒÀ Ï ÃX&éWnÞCeõBÝðÝÑL¤oóBàáD{»Ö?ðCµµ´ÐP!ðxGoOL¡Ž¯” Y*wHP¹yåågvêÃÙ‡X$®X,As€4éh{WÛ×®ÐÛÿµÅö ƒïêÛ¤ñ íüîâÐC:ø¶·åÑWþOkù-—ŠI™<¸/åËøË?Z‡ž“õÎsRÛëö¶nDerâÁv·''>´6HÑ°í§¯ª¾QUÕ‹U>i½²ÀVªiÃJ·oãÓ4¢›`?€>1 c²¤m’Jܺ‡Ò˯TíÊM4)GàÁÛÜx0Öš»ïRo8L3]†Àà~VÂRWGD‘ÞÅÈçó)'? ÜÜ€‚9~‚þ´ž/M(Kœ 9Ä™àØàtÐa÷Ögì=±­AËÁ‡%äÑšeMÈÁ£SÒ¾5„¬­ËjÛ)ëÍG3|¨˜“|€t‹†’¡‡ƒõRø¨í§/¿j†ªæ.RUõ"zxÜK+ïÑÁ—êݾ٦in£›`/€‹2 c˜¤&InÝÃвQª]¹IyECi(RnÍÝw©uÏ áQn<ìklÔÆG&ìàRsÿö&];>…\ª·;¦ÐI¦:ÎÏð)˜P0èW0Ç/¿ß7àDôT !·=p`Ÿ¶C{´sÃjûƒÆÔdÐá’ii>‘åàC:8HàØ_,—žƒ­5³\r­ÉþÃ9?«m§tè9YïØü½‚ÒäÄ‚ìÒÚ|;fÿcŸ%e£5¥æfUU/V>ëž wjó}u:~äm7o£CÒdÓ4[è(؇Àà¢ ÃØ&éKn]^a±?¸N#/Ÿ@3‘yD»^l ©þÚ-(ÐØ‰U2.»LÃFŽÔ°‘#?òÿ=¡ÌÇe>¬–æÝi{R¿[M ÚôÓG¸\ÌíÓE€lÄîŠòDSÀ€ù>ù >ää&”8ûþü„¥X,AÁ€ :²ëmi7’¤ IDAT_»BGÞ|ÅÞÛtpjÈÁáO<'äàþ~rpL?r8§®d½ùhf‚ãæ'ƒ9…ü~G›¥# Rë‹¶Ÿ:¯°XUÕ‹5õë7«¤l ½<&îÔ3·E‘p—›·±KÒ,Ó4OÐQ°À†ñ¤o»y ¾ÿ3»îË4iÃËSç³Ó¦kìĉºrúôO.f_c£ö56¦<|âÆ'›‡kÍÝw1ÙÁå<î%EéŽQ ËXÐaüù*k¥áéxa—4=^èõƒåà ޽4˜â`{Í\rplÀá|º>µï7²='Em|²^NA2ø0n>Áö·Kë“Á‡hÈöÓWUߨ)5u*»¼’^r¼u¿6ß_çöÐÃã¦i.£›`€ó2 c™¤_¹y³ÿþN]óõZš‰´:ÑÞ®‡o½…B PÙØ±š<çzMž3Gù……)éÇú~¨¶––”¬ÏmO:'ìàw è=vo}Z;~»Zíï¼eï‰Ç/HNt(º4Åvê‡4­-Uëòú‡´”Ÿƒí5#ä`ŸHg2ø°÷7™ >TÌ‘ F Ò.JŽl“Bm¶Ÿ¾üªšRs³®¸n½<âÀŸÓöU÷º}w˜¦ùÝ€ô#ð8'Ã0&KÚ&©Ä­{¨¬^¨¾û#š [­ Ò;›¥?´ýÔ%e£5sérÿÂ<å ¥€Ë½±a¥š6¬tû6f›¦¹n@zx|‚aÃ$5IªpëJ/¿Rµ+7ÑLØæÕúz=ÿËÇ(ÄEähòœëuíüù62ý¾¤"ôà¦'oyì15þ®ž Í#<ÎÔÛSOwL±Hœbx\Æ‚•µòU.•rSñ®4=èõƒåà <”#ûi¹áZcŠÃÀ–àü~Z‡ž“õæÊÌÆÍ—J.ã‡8{m–Z¶Jï¾dû©ó ‹5µ¦NUs©¤l ½\쥕÷èàK®~Œ»CÒdÓ4[è&¤À'†±QÒ×ܺþ¢—h飛T0¬„fÂ6=¡º¥N½á0Å8‡¼‚];ÿ¯tíüùÊ/,´õ܃ =¸åIç-ÍÍzü_ïæbóÓ×^,šP_~oK$,Åcƒÿý.jÓ“¸ƒA¿|~ß Ž‘“ëïãǸ 0(§§9D{ââ~oëé:©VkdžÕê uÚwâ¢QÒøš˜â0 µ1ÅÁýýdŠƒcúéÙ)ý\—uè9Yûžþü¶½KQ™œøPÊ‹©°I¸]:X/ùƒé´ýôUÕ7jæÒå—Š„;µù¾:?ò¶›·±KÒ,Ó4OÐQH€0 ã^I÷¸uý¹Eºá®Õ=±JC sh(lµmýzýá©õâ,™ :œmÍÝw©uÏž}®[ÝR§Ž£G¹èU€iéa%,Åb E# E#ɲƒ»ƒLrèÿÒ<ú¤ø´–ŸI¶×ŒIîïçy>ÅjÛ)í{BÖ{ÛìÝBAi2øP1‡üìáHæëÒ¾ÿŽí±ýô%e£5sérUU/¢8¯D<®D<¦Dü¬Ç|>ù‚9òùxa+»oݯMw}ÃíÛ¸Ã4͇è&¤€$É0ŒÉ’¶I*që&×ܪ«kn=ó÷a#†(ôÓ\ت¥¹YÿëÝY]ƒé_¯YK–8"èp6óða­ü§;úýyn<0ÝÁ›< ¯>¤øø„‰³'Qø|>s²ûöÑépC,šP,–PâÔŸÈ.mïjûÚÚ½õ{OlL•¯²Vªøò@nͦéF²c’¦ýyôIñi-?Sl¯™KBLqHѧ„>õæJYïÔÛ»-‚ìÌ“B¦´÷ié-¶Ÿ¾¤l´ªªiJMò‹†Òœ‹F”ˆÅ.p£Ç§@0G`bÙìÀŸÓöU÷º}W›¦ÙD7 µ<dÆ0%Óܺ‡q_œ¯/Þúýü[Aq®†æÐ`Ø._mßiÎçÕúz=ÿËÇúõ9N<ìklÔS?z€/<"ð;œ–8;(ññ nœ,‹&dY–¢‘Ä™C"žP"Îý Ù,£A‡«o“.™ÖÏOtê‡4­-UërìÍ~ËÁ¥'ä`{Í9¸¿ŸƒÝÎéàû/JÑ.û¶šS ›Ÿ|Ë)¤]0Oò¤7)Ú,ENÚzú¼Âb]qÝ<Í\º\%ecèG–‹G£ŠÇ¢}û‘™—/ŸŸÙ´[ãºÓ[Ï?áê»$5MóÝ€Þ¤¤Ikäâ°Ãðò+4ýoþùÿÞŽx@FLž“|u¤l=””–êÚù¥Ésæ8:èpÚµóç«¥¹Yû_kìóçô„BŽÞÓ¾ÆF¾è X,?çûâøäø?ò÷@à£wøŸ=UâlŸ>ѧà üåßâÑ„§^С¯û@vÉXСüúäD‡~œrpø• 9¸¿Ÿ^9¸$à eKÈÁÊ| GÉ7ãûò]óϲö=!kßoì >DÃÒÞ§¤ƒõØ#Ö›üsÒßJ×~Gzk½ôÆ*Û‚½¡NíÞúŒvo}FUÕ7ªªz±Ê'Í /ÙȲúv¤X4ªœ¼<êf³éóÏê:ö޼¾Í­[(QòEg'ÓMH&<@–3 ãvI?qëús ŠtãCÿ[¹Åçüÿ¡Ãó]ùŠÄð¯Nz()-Õ•Ó¯Õä9sd\v™ëÖß é¡[êÔ÷éãËÆŽÕ²ûîwl ãGÿã¿÷y/p&<ÀàÙõж¯]¡#o¾bï‰Ç/HNt(º´œ¦Çê¼r°<ô$jÛÊo9¸TígVNqðX?-×éLÞ©—BÚW–œiÔti©`$7¤_ÞPièéÍÒ«?–:ß³} åWÍPÕÜEªª^D?²H<U<í×çää‘Ïç£x6‹„;µù¾:?ò¶›·ñ¸išËè&¤Èb†aÌ’ô¢›÷ðµûŸÔðŠÏž÷ÿsó‚*þ‰{dŽyø°6>ò°ÚZZ\½²±c5vb•kC·¯±QOýè>ü—/Ѭ%Ky}­ü§;øBó(0pÎ:8uŠCšÖ–ªuy}ŠCZÊïÔƒG§8¤}kN 9x¬ŸVæ×f½S/ëÍ•ö$©b6Áö9|8ºGjü±ôþ˶/¡¤l´f.]®ñ_˜§ü¢¡ôÄãbш±X¿>'˜—'¿ŸÙÌ„ã­ûµùþ:EÂ]nÞÆ7MÓ\C7`ð<@–2 c˜¤%G©¹ÒÌ[îÕøÿöWý¸a#†(ôÓtdTSCƒ¶­RG:~­y2.»Lc'Vús¢c§ Æ–ÇSãïêûô±%¥¥º}ÕjÇíáÕúz=ÿËÇøó¨/Þø7š4g…€~ÈHÐ!·Xª¬•oü‹œrpø•½rðzÀá#KòhÈÁ%‰Cæ¿lú"ëzYûžþló«WÌ–ÊçH¥¹q ýò†JÃ.“º'ƒ{Ÿ² …ÅšZS§)5u<,ÚÛ++ï×çxÈ,sïNm¾ÿ7o¡CÒ,Ó4›è& ÈR†a4IšäÖõûâ|}ñÖï÷écs‡U\”8CSCƒö56jÿk_Ëé`ð‘e6r¤ÆNœ¨a#GjØÈìx妞PHÞq{ŸC(÷<»1ckFâ§þLH’âÑ„–¥W7>£WëË–GMûJ¦}õëú`÷Ö§µ}í u´½gßIO*—J¹ç{R!‡þ/ÍÁ&x(GöÓrõƇ-ÁCý´\r­>JÛëɉí¯Û[Ú•ɉØ¡°TZ.Åz¤¦UÒ«¤ÈIÛ—QU}£f.]®’²1ôÄc<¸Óž-Oèµ_ÿ››·Ð*i²iš'è& ÈB†a¬‘TëÚõO¸F7ÜÕ¿WZgÊœ¦'Ò¾ÆFµ47Ë<üŽÚZZR~޲±c•_Xx&Ð_X(ã²ËÎü ©¥¹YÿëÝý¸ÏN›®%ßû^ZÖ‹&dYÖ©?¥x<¡DÜ’–b±Ä?÷ÙïÓûöÒH"ðç¼ CšwótÈ! ¦8¤¥üN 9xtŠCÚ·æÔƒÇúi¹à:»ØÑÛ^—ö=!ë½mö–{D¥4n¾4j:7>¤ßéàC OÚ»^zõÇRç{¶/£üªš¹t¹Ê'Í 'FEûõ99yùòùy®I¦½´ò|©ÞÍ[Ødšæ: GಌaË$ýÊ­ë/q‰¾öÃõÊ-(î×ç1ån`>¬žPH-ÍÍgþ­'’yø|ÜéÃÙÆNL¾ºa†þÛ¶~½þðÔú ~Líî;Sãþ°Î -œžÎpzZC,ôÚ ¼ÿ²íËù™Ïiê×ëTU½ˆž¸œ•H(ÚÛÓY~åæçS8‡Øtç?ò¶›·ð}Ó4肋00 ‹†1YÒ6I%n\nA‘n¸kµ†W|v@ŸÏ”çóè·ŸwÊÆ—/Ѭ%KÎù§ ‰„¥xÌúKÀ¡ÓRÀƒ·x€êé:©Ý[ŸÒÎ ¿p@ÐÁ©!?îgyø•ÿÓV~ËÁ¥²¼ÙK—Lq²%ä@Àᢧ} k7Áwvðáhs2ø°÷)Û—QR6ZSjnVUõbå ¥/.íí••èÛ Órr)šCDÂzæö¯(îró6f›¦¹n@ÿx€,aÆ0IM’*ܺ‡ëïX¡òkføóƒ¹• '}à“zB!­à‡jݳçÌ¿•”–jÞM7ëò«§HJNh°;Ðp1¼Àœú½½ë¤vnX­V«7Ôi߉‡_)_eí© CšO#ä©…9òPŽì§×§8¤}kLq°¥f^9X}ø€H§¬ýOÊÚ÷„µñI€¥ÉÐøùRN!7Z¤WÑ%ÒÐ1’?(|WÚ»^zc•9ië2ò ‹5µ¦NSjê>¸‘e)é••¸ðc½þ`PÁœ\êå0Ç[÷kÓ]ßpó:$M6M³…n@ÿx€,aÆFI_sëú'×ܪ«knôq†ÏWNn€ ÀñSbÑ„N=ªãšÊR Oc¿öß?þ¨ö½úMô(²]Æ‚ÆÔäD‡K¦¦þØ–ã”ú :ö¡GBý_!‡Lœƒ¾lr8—Lr ’¡‚ÒÍŠF%Ãþ ÔÛ‘œöðÆJ©ó=Û—SU}£f.]®’²1ôÆM,K±XT‰Xì7À| ‚ ääP'‡:ðÇç´}Õ½nÞÂ.Ó4'ÓIè øWÒ=n]ù5³tý¦äXþ€OŸ*-ࢲÌéÉ ‰¸¥xÜR4wÌ”†Áxíw¿Õkÿ{ ö(²Ufƒß’.™–Úãz}ŠC6„¼pøÈ’<rpIÀAÊ–‡ºk‹tÊzo›¬Ý«¤Ð‡öíátð¡bŽT0’5ÒçãÁ)9ñá­§¤÷_¶}9ã¿0WSkêT>i½q˲d%g¦=ø~ùý¼€¦¼´ò|©ÞÍ[xÜ4ÍetúŽÀxœa³$½èÖõ/¿B7ܽZ¹Å);æ¢\‘Ƽ(‰Ÿ 5Ä£ %,K±Hܳû}g×Nýç£?¡ñi2bt¹ò†êÒ+&|âÿÞ{¯Nþé¨:KÛù <È6mïjÇo¡Ý[Ÿ²7èP~½|•KStðzÈÁòГ¨m+¿åàRy´ŸY9ÅÁcýÌú)©Y›õN½ýÁIª˜-MXBð@z+øðÞËRÓJé-¶/§üªªš»HUÕ‹è f›î\¢ãGÞvó¾išæ: }Cà<Ì0Œa’Z$•¸qý¹Eºá®Õ^ñÙ”»äÓCÌñs‘.%”HXŠFg&7Ä£ eãíÚ“:ªµwßÎE‘"#F—ë3“¦èÒ+>wÎÃùzðþÛ{µï•?êý{Sº²EGÛ»Ú¾v…vo}ÆÞ_œèPtijŽÇ$‡Ì®Í‘åg’ƒí5c’ƒûûÉ$‡´­Ízo›¬}OHíÿŸ½û%øÀç >œ|Wjü±th³9iërJÊFkæÒå€4Š„;µéÎ%ê:ö¡[·Ð!i–išMt.ŽÀx˜aM’&¹uý×ß±Bå×ÌN˱ƒA¿JF á",M(O(;5­!žP,– 0óø]ßN딯1º\f|I—MºFC?]:¨c½ÿö^½ôÌZ{ïHJÖFà€×¹>è@À!³ksdù 8Ø^7îï'Û×fµ½.k÷Jûƒ—L“ÆÍ—J'r#@úœ+øÐÛ!5­’ÞXe{ð!¯°XSkê4¥¦NùECébÇ[÷kóýuŠ„»Üº…]J†NÐM¸0àQ†a<$éÛn]ÿäš[uuÍ­i=Ç¢\åp±Ä•ˆ[ŠÇ-E#qéÔÔôÍKϬӮ†-¢Ÿ®¼ö‹š|ý 1º"åÇþýãjß«/ ú8x•«ƒ^9X}Õÿ´–ßrp©˜âɃûR¾ ¦8¸êëÓrðÚN¯ íuép½¬w~gï‰GT&'>|Nç >HÒÞõÒ«?–:ß³u9y…Ūª^¬©_¿Y%ecèBþøœ¶¯º×Í[Ødšæ: Fà<È0Œ’žuëú˯™¥ëïxЖs ž¯œÜ `ƒñ„qn‹Ö±÷Zµþþ;)Dä)Ðä9ó4éú”7¤ ­çJEèÀ¯ÉHÐ!·Xª\*_emòý ä©…9òPŽì§×§8¤}k9x}ŠƒmÛ!à0 ¡dí^•™àøùÒ¨éÜH>ç >Úœœúðþ˶/©ªúFÍ\ºœàBëþMo=ÿ„›·p‡išÑI8?à1†aL–´MR‰×?¼ü Ýp÷jåÛr>ŸÏ§O•‘ÏïãâR$M(O(³&”ˆ'˜ØfÏ>xŸÞ?°—Bœ‡A‡³ 6ô@à€WÙõж¯]¡#o¾bã/ƒƒ:¤ì¡3 ûð åàÒr°µf–K®5rpÆ—åÐ’¸¬Ÿ¡3|((MN|¨˜Ã&és¾àÃ{/'§>ì}Êö%•_5C3—.Wù¤ôHÍ÷×ÉÜûº›·pµišMtÎÀxˆaÔ ;Lrãús ŠtÃ]«5¼â³¶ž7ô«dÄ.  Ÿ¬„¥X,¡h$pHD 6dÊ;»vê?ý …øøÏ• Î6˜Ðn窠ƒ×§8rðF? 98¦ŸÙr à0°S»¼Ÿ–¤H§¬ýOÊÚÿ¤í²o‰Øá|Á‡“ïJ?NN~ˆœ´uIåWÍPÕÜEªª^D€Aˆ„;µéÎ%ê:ö¡[·Ð!i¬iš'è&|ðÃ0ÖHªuëú¯¿c…ʯ™‘sç ª¸$‹8X4¡x,¡xÜR4W<š·#…)gOÏ|ÐálëïÿžŽ½w¤ßŸGà€[e$èP4J¾«¿%U|¹ïA¯‡,=‰Ú¶ò3ÅÁöš¹$äÀ'|Ù0Å!m5;߇e2ø0n~2øSÈ +éáH#¥âQRà¬ç ôvHM«¤7VÙ|()­™K—|áxë~mºënÞÂLÓœE'à“<€G†±LүܺþÉ5·êêš[3º†!E¹*(ÊábBÖ‹FâJÄ-ÅNMlˆEâÅNþé¨Öß§"Ýᬭӂƒí n“Ñ Ãø…}ûxB™\œ£ãØ~Zn¸Ö²#ä 1É!ó_6LrH[Íú³üH§¬÷þ «y•²ñ“s ’Á‡qó >H¯ÂRihùGƒ’´w½ôê¥Î÷l]NIÙhUU/Ò”šÿŸ½»²ª¾ó}ÿYû±{÷n[%jcÒ‡L L4Æq@Î1C¬Èdê†XwæÜÏ©:bUôVÈù#*äÜ[uG‰“L’cPç˜hÐdˆ9&&À§ˆ ­â‚ÆMwïîý¸îP~Øëá·ÖûUe™ðð[ßýûþÖÞ»Ûýéïr5å'Ð`”^ÿå¿êWÞmòC¸Çqœ»é$œŒÀD€mÛ’¶Hj3±þŽÙóµpÅý¡¨%ß–U¶9Å¡Bl”KÕãÁ†ÚÑÃ\oî|Q?ûÇ5±{Üa :œèÝÝ]úÉšoŽêïx`ŠP¢pâò8Œ¾¤ˆV8˜ßOŠUÀᣖyó ‚¢ëtÁ‡76Õ§>¼û¼¯åd[Zuå’å€1xîohÏs?5ù!,pg €Gà gÛöDÕóL¬¿½ã]w×:er­¡©‰Ð¢ŠpC“‚­-”ÛÏ$ß÷Iæ÷’ƒ9Œ²ªƒÛä¾ü tð÷þ^xÚ‚¼wºàÑ·¥¾]ŸüP:âkI€‘+úõØm׫T0õ!ìt§“N@0˜mÛ·IZcjýó¾v·.þô_‡¶>B0ÅIá†J=àzgŸþíŸÿqÄ®³ÖöIúxçwÉt}|Öãû²~õ#þóç]<]7Þ~@(t¸â?Iç~êä_oØÞ ñ[ 8˜ßO¡ée<c¨€C4úé_W=ø°Žà€hÊN&\ eÛNþõbŸ´ãAiûƒ¾:.Ÿ«yËVªcÖ\úƒPT±0 ¾G’”Lg•ŸxÖéo«\^M--ž×Õ»o—6®ú’É[ûÇqnã„0–mÛ’¶›ZÿŒkoÖU·Üú:›riµLÈpà„0Z¿}âidzO©4T0¦æÖöI:ï’é:ï’:ï’é‘›p°óÙ§ôÜc?ñŸ_ºê[FN³¡ :D}ŠƒÑÅ{ºýLqð}Ï9˜ßOBbŠƒ‡µýq·Ü]?–ûÖ“þ–7é²zðaòŸðÆ €w>*øðæ&é7ß–úßñµ$‚Òðà îÛ£{ßRq SR©Œ&L>GgŸw¦\ø Ïê|õ©‡õÛÝkòVßè8ÎN€¸#ð²m{¢¤’Œüô£=}¶®[µÎ˜z3Í)å[3²‡¾"Ü€F)ô‡_ÿR;žÙ¤þÞC!z~ÍiòùÓÔzö$M¾àBM:š&]0MÙæ\ä{ò“û¿©w_ïÑŸ½qÅ]:ï’éd¾ EÐ!ê!&9D£ŸLrM?™äômCÈÁ³=s 8g'|OîË|MÙ R~ªÔÜþáßëZ_ŸøpèU_K:çã3tåX®™×|‘þÀݯíÔþÝ»T©”²^*•Ñ—]®ó.þ¤'õ>÷À7´ç¹ŸšºÝ}’:ÇÙËÉgÀ@¶mo´ÈÄÚ3¹¼nZû¤2¹V£êN¥ÊOÌ*™Jpá‰r©ªZÕ%ÜÏzgŸº~ýK½»û5z§ÛóëwqýƒúÇ>°_4´ÄþüGÞïÑúÕwŽhò Aïé3Éãÿ;aYJ¦GþÞ¾|â{³š«J¥Æ†"- ÃÅ‹e]tc=èÀ$‡  åR¡ì§kÂY‹Ç$«á%0ÉÁ¨û“Cøûy,øðÎV©<àßà øÀ©l}âCîœÿÞ;½¦·% IDATÏK/|[z÷y_Kj›r¾æ-[IðžÔkÏoQ¡ïÞœáÉS4}îg”Êdºn©Ð¯Mß\®ÞîݦnýNÇq:9âŒÀƶíÛ$­1µþE«¬öi—šù¢iYjiË(Û”â bÌÜ£+—jªVkª 8Ayww—޼ߣþ÷{ÔóÎ>• õà‡Oˆ8`8¦õìIšpödIÿfÈær|@º~ýK=óƒ>òÏœwñtÝxû]lœÁ±0C:“”eI©£a†T*á餶cAˆr©vü}^µ\ßo˜*° Cç’òçs!¦8ºoLq0¿ŸLq0¿—DÀ!Äý,õËݵ^î®û|ÈM®¦}–7z¼óQÁ‡žW¤J]øZRÛ”ó5gÉW5óš¿QS~=BC îÕË[þ­aSN'›ËkÆŸZ-Ûºî@Ï~m\µT¥Â€©-øŽã8·qÄ0ˆmÛ’¶›Zÿ¼¯Ý­‹?ýׯ÷!ÓœR¾5ãé§ Ǧ6T«®Ê¥*~ð!ÿöÏÿ¨?üæ¹Ó¼Þä´äö»À R™¤IKÉdBéLB‰„Ê)l'†\y0A A‡‹˺b¼A¦8ºgLq0¿ŸLq0¿ŸQ9pEmQ9œêR¥~¹» >ˆ¨DRÊO•òçJ‰üÅ#o×'>¼±I*ñ­¤lK«®\²\s–,'ø€q©”Jzñg<;én;K—æs Ÿôнm³žY³ÒäVÜè8ÎN$€8"ð†°m{¢¤’ŒüÔãEWß «o½'2ýH$-åZ™ö€ºjÕšjUÞc8³âPA?ûÿî×»¯wôëç]<]WñÂb-•J(•I*•N(™JŸÚ`ªJ¹~(«ª @ÐÌ :rtߘä`~/™ä`~?™ä &9ÐÏ3]®Ô/÷­'äî^/ ¾ç_]øáXð¡å)™=ù÷Š}õ‰Û$ø³¾°õêë9àë5ÛÏ=_3þb~Ã×ÝþøÚñø¦¶¢OR§ã8{9•â†ÀÂ¶í ’™X{{Ç%ºî®uÊäZ#×—L6¥\k:”?UU9úSyË¥ÚñŸØËOêÐ(oî|Q‡ÞÞ'Iúxç‚b)•I*I*I(IFú±º5W¥bU¥áªJÅ Íøjxàˆ^þù#zññïtyˆÀ qm!Z&Ôýt 9k¡Ür¾ìE"°â†üœùz¹“ÿ°ûÖr_Y' :þÕ›ÎIÝPÿ'ÝÂDÞi™,Mè8uð¡ëiûR¿_—e[ZuÉ_|^ó–­TÛ” èFäÀÞ7ôú‹¿äÚÓç~FgŸ×ø³ºiõr9]ÛLmÉNÇq:9™â†ÀÀ¶íÛ$­1±öL.¯E«×+?yj¤{ԜϨ9—’•°8°†:h86©¡ztJÓ¼§€ÃG9~ª0ùà©á#zññuúÝãëTì÷碙V©cá‚LqCa¡\*”ýtM8kñ˜â`5¼¦8u2ÅÁü~ºýÃDZËd)wŽ”mûðïu­—~óm_ƒ’4óš›>`D~÷³ *¹v6—ו_XÜðuK…~m¼s©½gj[¾ã8ÎmœNqBàBζíNIÛM­áŠûÔ1{A<^T-KM-i‚!S9a C¹T“¤ãaI|¨ À'‰¤¥T&©L6©L&É{æS¨Vj.TTª0I Ð0f,“uÙßÖÿ÷ˆ0Éa̵…h™P÷“I¡ég<&9rÛ¥ 9„¾Ÿ®÷µ¹_>ü½‹à¿d'H.8uðáMÒŽ¥wŸ÷µ$‚ø(ANw8抿ü‚Z&¶7|ÝÞ}»´qÕ—LnÏŽãlà”ˆ b¶mO”´CÒ4ëï\r«®Xrkü^\²MI%S ²Nbpk®*•úÿf*@8¤R ¥›RÊd“J¥yoJ¼¯Œ™A&9Œ¡°P.Ê~2É!4ýd’Cn7Ä[BÈ!Ôýtƒ«+°àÃÔ«¤éKë?‰¼’ÊÖƒ§z®yçyé…o|@(ìþÝó:¸ïÍ@k˜zñ'õñYs3…T&©t&©dÊR*•üÃbnÍU¥R;>•áØÿ—¤ÊÑPâɲ,¥›’jjN)I²!©”kì+Ÿˆ§p˜â0æÚB´L¨ûɇÐô3S8ŒíÒBßOS§«kð=¹¯|×ÿàäËê>ðZvB=øÐÜ~ò¯û¤|€/âx¤îm›õÌš•¦¶ªOÒ…ŽãæÔˆ*2¶m_ÒßšXûEWß «o½‡&Ž÷ÅÙ²”L'”J%d%,¥3õD£>Pvb€¡\:öïªt¯'J¥jjI+“5sBYT –Uè/±3á :0Åa ……r©Pö“)¡é§Õð9sº!®­Qur0 —g¨Í•T빻×Ke*3Á~Ie¥ HMíR"õï¿^쓺‘¶? õ¿ã[9âå7U¥ü÷ ýˆ ÃûÎtSR͹´RéRL{€ø$èŸ*]tã A&9Œ¹¶-ãá‚ ,‰CЋ3É!èÛ†Iží!óûyºß"ø êÉú×h-çHÉìɿ׵^úÍ· > á^Þú õõ¼¿’´iõr9]ÛLmÙ=ŽãÜÍÉE $lÛ¾PÒIm¦ÕžÉåuݪujŸv) —HZjÊ¥ÕÔœbšƒ!Üš«þ’JC6"(¨ ƒÕùŸ¤‹‡ù€Cð 6°$C²x [NÀÁ—}#à~º!?g¾^ÎàI#Tð¡íB颤iŸå ,ïµL®‡Ò-'ÿ:Á4Ø»¯ÿAoí|1ÐÚ&OÑÌÏ|Î·ë• ýzì¶ëU* ˜Ú¶+ÇÙÁé5 $lÛÞ!i–‰µÏûÚݺøÓMƒ¥2I5åRÊ6¥Ø C‡*ˆI!X°%1É!èÅ™äômCÈÁ³=#ä~zT6Á‘—ÊJùs¥Ü9Râ„hóÎóÒ ß&ø€qyyë/Ô×s ˜£ÊhÎ+•Éø~íí? ?`jÛ6:޳˜Ó J<@ÀlÛ^,é'&ÖÞÞq‰}k=M cY–ÒMIåZÒJ¦:DMµRÓÀá¢*•›!èp&9„aÁ–Ã$‡ ·^“Œ¹?Ý×Ö¨º9ÐË3Ôæ÷v|u‰d}ÚÄ)™ý÷_'ø€qèë9 —·þ"kw̘©Ž³{ìϬ¹]ÝÛ¶˜ÚºŽã¬åˆ  Û¶'JÚ+©Í´Ú3¹¼­^¯üä©40„eYjjI«9—’•°Øsk®Žôzôx[¿úÁ}Úý¿ŸŠqÐ)!X°%1Å!èÅã1Å€ÃØ.MÀ!ôýŒú7µ¹/÷•ïJ=Ûý»t:']tCýŸt o‚x¯¹½>õ!{ÂÇQÞy^Úñ€ôæS¾•Ñqù\Í[¶R³æÒƒ½¹óEíý¾^3×v–þôs×ú¸K…~m¼s©½gbÛú$Íwg'@x€Ù¶½EÒgL¬}áŠûÔ1{M @Ð!ž=@¸ :¼üóÇü»h¨‚„B°`K"äôâñ9Œ¡6BÑè§òsæëå˜äÐȺ>ˆ…TVšpÔÔ.%Rõ_;òv}âC×#¾•AðÁl•RI/mý… }ôçØ¦2êüÜõjj þµ²wß.m\õ%S[·SõÐÃaN1Óx€€Ø¶}·¤o˜XûŒkoÖU·ÜA€#èB>±:¸ýP¼Ëx¸`Kr£ÙKBæ÷“ƒ˜ä`@?™äª~| ‰d}êÄ)™­ÿÁŒÂàá^½¼åßT©”<½N6—׌?ÿ´Z&¶‡æ±¿úÔÃúíî5µußqç6N0Óx€Ø¶Ý)i»‰µ·w\¢EßZO€Ë4§”kI+™J°1GèÂ!žA¦8„`Á–ć gŠCз ÏöŒ€C4úépÖδÊÁßKo=)wïÏü+ýXðaÚg¥Ü9¼iàì)?µ€>`ĆõÚó[<›ôÐ6yЦÏýŒR™Lèû3knW÷¶-¦¶îFÇq6p‚˜ŒÀøÌ¶í‰’vHšfZí™\^‹V¯W~òT ÏuUsëÆu«§þP®•HH–”H$Ù/à4IKù¶¬Òîœð[suøý!Õª|/ü¿ !‡-Ú€r9}Ba¸mÜnIúõ C$Bg¨Í°I#6øžÜW¾ëoðA’¦-¦/%øÀ?©¬”?·þ¼“H|ÀˆTJ%uw½¤ý¯ÿ¡akfsyu̘©)~"´»Tèׯ;—jàÐ{&¶­OR§ã8{9ÁLEà|fÛöI‹L¬}áŠûÔ1{Mã纪Õjrkµú¿Ýš4ʯO­DB‰DR‰d²„ L6¥|[FVÂb3ð!•rMGz‡Å÷Àñ :rÁ‚ ,‰CЋÇ#ä@Àal—f’CèûõIQ 8œîƒïÉ}•à€˜h™\Ÿún!ø€Ôþ=]:øÖ›ªTJ£þû©TFí篳§^ ³Ï»ÀˆÇÜ»o—6®ú’©-Ûê8Î|N.SxÙ¶ýIß3±ö×Þ¬«n¹ƒ&€1«Õªr«µú¿kµu›P2•R2•b£[ÍùŒrù4Tªh ¯ÈF€‡btp#ô!j–òhÁ–Ñ~r°^!cîO¦8˜ßO¦8D£ŸgºÁq’ÉÕƒMíRyPÚñ ´ýA©tÄ—Ë|0Óàá^UÊeõõ8’¤d:«üijNûç[ÚÎR*“1ò±¾úÔÃúíî5µU÷8Žs7'€‰<€OlÛ¾PÒIm¦ÕÞÞq‰}k=M£stŠC­VU­ZõçQVB©LZ‰D’ýG¬äÛ²Ê6øÁÈô÷Uª°Ð`‘:¸ý©ÿ,ãá‚ ,‰€CЋ3Å!èÛ†)žíSÌï'‡Ó+÷ËÝýˆÜÝHåÿ¶„à€ $’õçÖ©Re˜àp‚gÖÜ®îm[L-ã8[è"ÓxŸØ¶½CÒ,ÓêÎäòZ´z½ò“§ÒDpf®«jµ*÷XÈ! ÉTº>íÁ²è "°FýT]suøý!Õª|_!ÚA7Ä?ŒšÃèKŠhh…ƒùý$ä Bô3ê!‡ÀËqÈáT±<LðaÒeõàÃä?áM8e'H-çHÉ,Á@R©Ð¯w.ÕÀ¡÷L,Ÿ¤NÇqÓI&!ð>°m{­¤¯›XûÂ÷©cöšN/$!‡}Á›H(Éz@¤vÀX•KUéf#` :ØWʺâ$ûSž¾¿é¡\Ê£X“‚\Üjx Lr0êþ$ä`~?™ä`~?]ÿÁqslêC&/½ü}߃×ÿ×5j›r}@(ôîÛ¥«¾djùÇYL˜„Àx̶íù’6›XûŒkoÖU·ÜAÀ‡¸®«Zµ"·V UÈáC_ôz@„5ç3ÊåÓlƬ¿¯¨ÒP…€QŠ^Ð)!X°%1Å!èÅã1Å€ÃØ.MÀ!ôýdŠƒùýtª­< ÷­'ëÁ‡‚ãß–|¤ì)Ý,ízÜ×àÃÌknÒ¼e+ > ^}êaýöG÷šZþß9Žó}ºÀÀC¶mO”´WR›iµ·w\¢EßZOÀqµZUnµVÿw­fξ„A™lJ­geÙŒKµRSßûÃâûƒ02Ñ :rÁ‚ ,‰CЋÇ#ä0†Ú9D£ŸnÈÏ™¯—3´Ÿ„|­ÍÝû¤ÜW"ø >I)Õ,½ñ3içC;Ϭ¹]ÝÛ¶˜XzŸ¤NÇqöÒE& ð²m{ƒ¤E¦ÕÉåµhõzå'O¥‰Ä•ëªæÖŽjrkU£Ž•H*åÃሆDÒÒij›e%ñ`ü e ”Øø‘ :¸ýP¼Ëx¸`Kr£ÙKBæ÷“ƒ˜ä`@?£>ÉÁ5ä¬r ÿ6'°àÃE7HS¯âM<€`X é­§¤Wþ'ÁÄF©Ð¯w.ÕÀ¡÷L,§ã8t€o3<€7lÛ¾MÒk_¸â>uÌ^@ˆ™j¥"·VU­V ù‡¡Æ&™J+™NÓhoB{“Ò™$†pk®þØ3Ä”8…H܈~(Þ£¥ä&×'>Lû,oê£:,í}FÚù]ß.9óš›4gÉrMùÄeì?|×»o—6®ú’©åßã8ÎÝt@ØxضÝ)i‹¤6ÓjŸqíͺê–;h"1R­”U­T"rø t¶IV"AÓa¬LsJ­mL+Ac1åNvàWõâãë :¸!þœfX§8x¶hʉhÈÁ5䬉C8nBžíYÔ'9r0¿Ÿ‡NYÕÞ'å¾õ3©g»%ø h…ƒÒ®‘Þú…o—ì¸|®æ-[©ŽYsÙøêÕ§Öot¯©å/pg ]fÀ¶mï4Ë´ºÛ;.Ñ¢o­§Ä„뺪”Šrkµø|œH*åÃâ0ôüZ–ΚÜ,+a±hìëAÍUïÁ öºwþZ¿úÁ}ê~é×þ]´!AB!X°%rzñx„8ŒíÒ†÷3ê‡Q_ÎÐ~2ÉÁø~º·Ë}õ!‚â¥pPêzDÚ÷¬o—$ø€ <³ævuoÛbbéû$u:Žs˜.+Ð`¶mß-é¦ÕÉåµhõzå'O¥‰Ä€[«©\*ÆbªÃ¥²Y%IŒÓœÏ(—O³ðD_Q¥¡  –:xòG¨–ñaєѠƒçi¾ìYì§9„¹§„Æv9ûõi㺄Aý<á—Ü‚b¨pPêZ/íÛìÛ% >ÀO¥B¿6Þ¹T‡Þ3±üŽã,¦‹ÂŠÀ4mÛó%m6±ö…+îSÇì4€ˆsØAbÊ =·Lw€ÇÊ¥ªŽô³bÅÈ ƒ¡ŸïÃR-ØÀr"ÚO¦9˜ßS¦9D£ŸQ9D"àp†Ú9ØSwÔe|˜z•”ná þ#ø€sº^Ô¦Õ_3µü¿sçût@x€±m{¢¤’¦™VûEWß «o½‡&®ëª\ŽmØá˜t¶IV"Á€1šriµLȰðÔ{ ªUù^!€è3.èàFø'ÿ7xl`Ií'!‡hô3ö“9ÑO&9˜ßÏH…܆•èöl—v?"wÿsþ•ŸÎIÝPÿ‡à€ |@DmüíxüKï“Ôé8Î^º l<@ƒØ¶½AÒ"Óênï¸D×ݵN™\+M ÊÅ¢ÜZ5öûL¥•L§90ÆÄIÍJ¦éÀ[ƒGJ.”Ù‘eVÐÁ ñçnÃrðlÑ”áЊ!A«á%¸f÷‘ "Ü`HO£>ÅpC4úÙèRß“ûÚCr÷nòï!|4‚ˆ M«—ËéÚfbé[Ç™O„ hÛ¶¿"é{¦ÕÉåuݪujŸv)M ª•²ªe>È*IV"¡t¶‰€ïÛ³)µž•e#à¹J¹¦¾÷‡Ø‘cLÐIaX°%1É!è 0É!ȇÁÏöŒƒ!=uC\6!‡†ÔæG©ÄÁDH©Ð¯Çn»^¥Â€‰åßã8ÎÝt@˜x€q²mûBI;$µ™Vû§¾|‡.ûüÍ4€8p]•ŠÃ!ÿ•¿2MÍ’e±½|[VÙæ_ü±§ Z•× ÑÞ ÓB´hÊazCЋ[ /#B=e’ƒ9ÒÓ¨‡˜ä{4¨R >ˆ#‚ˆ§ëEmZý5SË¿Âqœt@Xx€q²m{‹¤Ï˜VwÇìùZ¸â~@LTËeU+Lw8Q*›U"‘d#zíçäd%çÀý}E•†*lDÌ$’•°”LÕŸkIë¤L [“jµú÷‘«WnÍU­Æ¾!¼Btp ý)Ô-åÑ‚ ,‰ A_€ a¸u:x¶g„¢qF=è@ÈÁ_ƒûå¾öOÄKÁ‡¶)çkÞ²•šyÍÙ4Ä ?¼W¯=ý°‰¥ï“Ôé8Îaº <À8ض}·¤o˜Vw~Ò¹Zô­õÊäZi"1Q®ZÇ%Ó%SüÔ|„[&›RëYY6¾)U4ÐWd#b •¶”J'”LY²£ÿû®[?T+®ªå„B A‡‹˺l™ÔþÉÓß,¡DÈal%rzqBAß:<Ý7Bæ÷“Iæ÷Ó5ଠ¾'wï&¹¯?*•ü)%“¦}¶|ÈÃüGð†ÛxçRõvï6±ôï8Žs`ŒlÛî”´ÝÄÚ­þ±Ú§]Jˆ‰ZµªJ‰¯~P2•V2f#j¹ÖŒš[8§ðO¥\SßûClDDY–”Î&”Î&NšàИ÷®Ê%W•RM|Ë~ ,èpÅ?Hùó>ðnˆ?ÓGÈalåD4´BÈÁüž2ÅA„ è)S¢ÑOBáìgy@îîGý >HÒ´Òô¥ƒà 5г_W-U©0`bù7:޳.Û¶'JÚ"i–iµw.¹UW,¹•&#ÕrYÕJ™ø0Á„ö&¥3I6¾zßd"ÆË Ã©TJ®Ê¥šª¾÷ o…*èÀ$‡0,ØÀr9½8!‡ oBžî!óûIÈÁÀžF$äp*ÄÁèõ_þ«~õàÝ&–Þ'éBÇqÓEA"ðc`ÛöZI_7®îé³uݪu4€˜)‡åÖjlÄx€ ζ[Øø®¯wX•R•ˆÌ륦\RVÂÿkW+®JÃÐxLt)7TËx¸`KŠÐã}Ûr‚¾ìAt0 ŸÌïgäB§¨Í5àœÁqPðaÎ’¯jæ5£¦üz€QyîohÏs?5±ôŽã,¦ƒ‚DàFɶíù’6›Vw&—×MkŸT&×Jˆ™ÒPM8…T&«D’ŸœŸÑTBm“šÙø®¿¯¨ÒP…ˆ€LSB™¦DàuTÊ®JCU‘¿Äx…&èàFôCñ,ãá‚ ,‡iA.n5¼„õ3ê!7ĵ5ª.&9˜ßϨOrp 9kã©+J“Fºdy@îëÄÅÑ'ÂÂA©ë_ƒÙ–V]¹d¹æ,YNð#V*ôkãK5pè=Ë_á8ÎZº (`lÛž(o€¦ IDATi¯¤6Ój_¸â>uÌ^@ˆ·VS¹8ÌFœB*›U"Aàá•ɦÔzV–€ï e ”؃Y–”Í%•J[¡©Éu¥r±¦Ò0©Œ^(‚nD?ïÑR-ØÀr9¹8SÂpÛ„5äÀ‡Ð÷“)æ÷“)æ÷Òáëly@Tíµ’ Ž›à€ Ÿg >À½ûviãª/™XzŸ¤NÇqöÒEA ð£`ÛöI‹L«{Ƶ7ëª[î ÄP­VU¥Xd#N!ÓœcjÍùŒrù4߇*èãµÃT‰„ÔÔ’T"i…²¾ZÕÕð Ó02Á¦†øs}aâàÙ¢ ('¢S9\CΚ9„ã¶!äàÙžr0¿Ÿ„Ìïg'9ŒâuÖÝû3‚"dϳr¯>õ°~û£{M,}«ã8óé € x€²m{±¤Ÿ˜Vw{Ç%ºî®uÊäZi"1DàáÔɤR~r>Â-ךQs ø¯\ªêH/ÓÌ|}³ÔœOʲÂ]§ëJ¥¡šÊ%R8µ`‚‹duž0Ñ!\wM(—òhÁ•Ñ€ƒç€ƒ/{FÀÁü~p0¿Ÿ®ç,ËpÕž¹ã}ýðîÞMþÎý”tÑ Òä?á þ?ç|¸ä/>¯yËVªmÊ´ §µiõr9]ÛL,ýÇqüFàFÀ¶í %íÔfRÝ™\^×­Z§öi—ÒDbŠÀé%Ó%S)6¡6¡½IéL’€ï<˜)•¶”Í…?ìpÒY+ÖT"ô€÷òÏÕïþ×:|ó5ÿ.Ú !‡±•ÖÿæEÈal%D¨Ÿ„BüêCÈaô—"äÚ~r0¿Ÿ„Nù§‚>Lº¬>ñà€ žs>HÒÌkn"ø€Ó*úõØm׫T0±ü+ÇÙAø‰ÀŒ€mÛ[$}Æ´º?õå;tÙço¦Ä‡S}%œP¦©‰}@èx@P<˜'I(›KY{¥äj¸P¥‰1÷òÏÕ¯~pŸú¼ãßECt ä0úr˜äôâLrÃmCÈÁ³=#ä`~?£rp 8gã­ƒ§u|~ ~î-”ö‡IRµ\VµRf#,K™l“dYìŒ@àA"ð~Q ;S.ÖT"¤eñ :r}9âàùC AÈ!êS|}8nH·„)¡ï'S¢ÑKBæ÷ÓÃ)¯Lð@”ß+ Á¬wß.m\õ%Kß'©ÓqœÃt€×<Àiض}·¤o˜V÷ŒkoÖU·ÜAÀqn­¦rq8æ_ýZJg²² ŒAàA)—ª:Ò;ÌF„XÃÇ 5•K„¢&žA7TËx¸`KŠhÈÁ€ƒDÈ!øÛ†)žî›òsæëå9„²Ÿ®!gm<µE=äà6âu6\ý$ø òïƒ?(ÀàÃÌk¿¨™×|‘ãc¯>õ°~û£{M,ýŸÇù à5p ¶mwJÚnZÝí—hÑ·ÖÓ@ð!¥áaÉé‡ ;ÀPáå°ƒ$¹®44PU­Ê÷­£ ~AB£/)¬÷:SÆVF„úIÈÁü~F}ŠÃ¨.GÀ!´ýdŠƒù½4xŠÃˆj;ZžÛ³]n×÷> šúÞ’JG'rÞ¢ýÞi4ʃÒþ¤®G¤Bo—m›r¾æ-[Ið!Æ6­^.§k›‰¥ßè8Î:ÀKàlÛÞ!i–I5gry]·jÚ§]JÀ‡T+UË¥ø}Ñ›H(•Éʲ,Œ“oË*Ûœb#à;á•J[jj‰~ªVuUè¯ÒpƒÅ'èà†r)l`9„‚\œ)a¸mÜn SBßO¦8˜ßO¦8˜ßÏNqøPmu‹|˜öÙú?@#íAzé¡“?ÌžÎIÝPÛ zï‡ÇZã¾Íà›R¡_Ýv½J…ÓJï“t¡ã8‡é"¯x€°m{­¤¯›V÷§¾|‡.ûüÍ4œVܦ<$Si%S)‰° ÕœÏ(—O³ð]q¸¢ÃE6"dIKÍùdl^ÖJÃ5•†k4Þ0¾2­ÒŒ[d]´ØÇ CX§8x¶hʉ؇â}ÛrB¾ì!óûIÈÁü~r0¿Ÿ„Ìïç(Ë $ø›\ÿ:Á4¾g¥mÿÏéÚiöfŸ¢þ5ëhk (ø0gÉW5óš¿QS~G(&º·mÖ3kVšXúFÇqÓA^!ð'°m{¾¤Í¦ÕÝ1{¾®¸Ÿ€T«UU)Æà¬VB©tZ‰d’¦ÃhM¹´Z&dØø®0PÖÐ@‰‘¸…$Éu¥¡þŠjdŒXÐaƲúÿöþD†jl`I 9pâr à0¶KÞϨF}9ûõ€Ã˜/AÀ!TýtÇû­€ÃiW:üºÜ=ÉÝ÷”å|ÀxJOÝzæ?7ûÿâœEýë×±Ö@ð!ÛÒª+—,ל%Ë >ÄÄ ?¼W¯=ý°‰¥ßè8Î:À à(Û¶'JÚ!išIugryÝ´öIer­4œQµ\VµRŽìãcª¢$•Iª­½‰€ï”4\(³!aYRnB*–/m•’«áB•CbÑ :¸¡\Ê£XN„>ïÛà 뇈õ3öS"ÐOBôò µr0¿Ÿ„Ìï§—åÕ^û'‚0ÃKI{žÙŸ%ô½¯aYcÏ«õàáW}«”àC|” ýÚôÍåêíÞmZé}’.tç0]Ðhà(Û¶×Júºiu/\qŸ:f/ `ÄÊÅ¢ÜZ„>¯yËVªmÊ·ˆêÝ·KW}ÉÄÒ·:Ž3Ÿh4 ɶíù’6›V÷ŒkoÖU·ÜAÀ踮ʥ¢\Ã?=h%õ C’‰ˆ®‰“š•L%Øøª÷@A|Ï0¯s„$1å!l¢tpCµŒ‡ 6°¤ˆ† 8H„‚¿uÜo !‡Ð÷“ƒùý$ä`~?ÝF¼Îr8#‚³Ñ$B‘yÍó Æ—:äðA’f^sÁ‡ÛþøÚñø&–¾Âqœµt@#x{¶mO”´CÒ4“ênï¸D×ݵN™\+M£çºªTʪU*f}›H*‘L(‘HÊJð!pD_¾-«lsŠ€/5W½ lD4å’Jeô¹n}Ê߯V´‚„F_RXo@¦8Œ­Œõ3êSFtiÃûérÖ|»œýt 9k\Â~º†œ5–%ààqmGµ×¾GðáòÒCÒž'Fÿ÷¦-.ÿ?¤t {häó¥5žn©¾·êg¬{‹¯ŒàCtmZ½\N×6ÓÊî“Ôé8Î^: Q<ˆ=Û¶¿/éoM«{Ñê«}Ú¥4ŒKµRVµRQ?Eh%²,K–•u4äÄM¦9¥Ö¶,߇+8\d#FØáç²PS¹Tc# ƒfÏ‚ *'¢S<h!90ÅÁüû“)æ÷“)æ÷“)æ÷’)áë'Á„Iß[Ò3·ý\ÍþÏÒä?ayÎ ð{…ƒõ‰>.þókuå’åê˜5—£=ûµqÕR• ¦•¾Õqœùt@£xk¶m/–ôÓêî\r«®Xr+ ä´ëh¨!aÉ’õï!¦7õ[IJÔ>%ÇFÀ7ƒGJ.”Ùˆ¥3 es¼ž¨Rv5 ,Æ:åá˜i êç*wNÌ7’ ÃÈžû‚ >t\>Wó–­$øÝÛ6ë™5+M,}…ã8ké €F ð ¶lÛž(i¯¤6£êž>[×­ZG@ù®«Zµ¢Zµ*·Ö Ÿ¢|,Ð`I–•¬£“D¨©¶³›•Js¿À}‡†T©ð“ôƒBØáôû*â[ÙÞ3;èÖCnèCñ˜â Å%ä@Àal—6¼ŸQ8ŒêrBÛO×€s6žÚ\CΚGËpy/ > ~y—tèÕñ­ÛàA‡±=÷¬mº7Kå‚o;Að!:žYs»º·m1­ì>IŽã쥃ƋÀ€Ø²m{ƒ¤E&ÕœÉåµhõzå'O¥À[®«š[“[á‡^ŽI$’ì!Ш¯šSjm˲ð\µRÓáCClD@’)KÍy^?Ogx°ªJ™ïe{ÅÌ C?¼àÝ‚ ,‡C‹[ /ƒ1÷'SÌï'SÌï'S¢ÑOBæ÷óĥʪíyLîžÇ¤ò ?…àŽŸ¿Ázè¡oïø×j»Pºè†˜œ+ 9{{žÞxÂ×àCÛ”ó5oÙJͼæ‹Üÿ†*úµñÎ¥8ôži¥oug>0^Ä’mÛ‹%ýÄ´ºç}ín]üé¿ìúmMIMͧ5)—R[SBéäØ~êd¹ZSßðÉž<\¬ªRuUª¹ê®ª|ôß@ÜY–¥³&7ËJXlýM)ÝÁÍ"èÐð…˃Ҟ' >`Tœ®µiõ×L,}…ã8ké €ñ ð vlÛž(i¯¤6“êî˜=_ WÜȵ§µeôÉÉMÊ¥¾_»o¸¢¾bU‡«ê)TT(óáÄO¾-«lsŠ€·_õ«R"xî7Â#S­¸à|6ÂðÀ½üóGôâãß5$èÀ$‡Ñ—CÈ!èÅ 9„á¶!äàÙžr0¿Ÿ„Ìï§kÀ9ópYBôs¬K|8ö“ù#ù!uœùÜ58ô0é²zè!2:x¾xyPzï·R×£R¡Ç·]#ø`®~x¯^{úaÓÊî“Ôé8Î^:`¬<ˆÛ¶7HZdRÍ™\^7­}R™\«¯×=7ŸÖåvs A‡Ó¾®èÞ’ö÷—U®ñ€xH$-59ÇFÀ3ÕJM‡ ±>³,)ך’•`/ÎÄu¥Á¾ 1ÃGôâãëô»Çש8ØïÏEÇt à0¶rÂú}2c+!BýŒzÀaD—6¼ŸQ8Œúrö3ê‡1_‚€C¨úéŽ÷u–€Càýläc,÷«¶ç_ü >¤sõàÃE7|ˆ£ò ôëÿ.zµ1ë-¼_jû˜á›BÐ!…»7|˜yÍ5gÉr5å'ð|`ˆw.Uo÷nÓÊÞê8Î|º`¬<ˆÛ¶Kú‰iu/\qŸ:f/ðízé„¥ÙSs:·5Ú=)Wkê:TÔ½E6b)ðRa ¬¡á#Ë’šóI%’Œv©ÃÆÂŒ !‡Ñ—Ç gŠCn¦8x¶gLq0¿Ÿ®ç,KrÕžr0¿Ÿ®Ç‹•>À_/=$íybüëLÿiúRƒ7‚°Cà |ȶ´êÊ%Ë >¢wß.m\õ%K_á8ÎZ:`,<ˆ Û¶'JÚ+©Í¤ºg\{³®ºåß®×֔ԟߪ©¥o¸¢ß¼SP¡\ã Ò˜ò/ý±§ Z•ïú…°ÃØú«œÓQСA½ŒzÀᤒ"r0$à rþÖaŠƒ§ûæpÎ|»!‡Pö“C4úIÈÁü~º,Fð~Úÿ‚´íHåÂØ×06ð@Ð!t ¿÷ÛzçÐk¾í0ÁslüíxüÓÊî“Ôé8Î^:`´<ˆ Û¶7HZdRÍùIçjÑ·Ö+“kõåzmMI]ÝÑ¢t2aToËÕšžëTßp•ƒ€H˵fÔÜ’f#ÐPÅ¡Šú˜žçÂc74PUµÂ÷³Ï$ÜAB£/)¬gž)c+#BýŒú‡]Úð~2ÅÁ€^ž¡6׳æûòB³gn#^g 8Úϰ½ï&ø¿J/þéЫcûûÆ:„~½*u=Bð²iõr9]ÛL+{«ã8óé€Ñ"ð lÛ^,é'¦Õ}ݪeOŸã˵L ;Cèq`Y–ÚÎnR2•`3Ð0Lwðó&ì0>Z8ƒþ°•§årrq¦8„á¶qCº%Lq}? 9˜ßOBæ÷’ƒùý4á}7Áøeÿ ÒKI…žÑý½…÷Km3àºñ«Ñ5|/ >àzök㪥*L+}…ã8ké €Ñ ð òlÛž(i¯¤6“êî\r«®Xr«/×J',]=­EmM)£{]®Öôì[*”k|DV*“T[{†`ºƒ;Œ‡S _Ð!¬S<[´åDìCñ¾=,B¾ì!óûIÈÁü~r0¿Ÿ®!gÍ£% 9ÐOSÃÅà—}ÏJ]ëG|8÷SÒÜÿfÀƒbªƒÑ{Y8(u=*uoñí’ÂëÕ§Öot¯ie÷Iêtg/0RDžmÛ$-2©æöŽK´è[ë}»ÞÌ)ͺ¨=‰~÷ Wôì[|DZ®5£æ–4qcºƒ?;4‡“tÈO•.Z|Š “F_NDCž?4B¾ì!óûIÈÁü~ºœ³@.CÈ!T{æŽ÷u–Càýt 9kgBð~Ù÷¬´ç§RßÞSÿþ¤Ëêa‡PŸ ¦:Dj/ >à¨gÖÜ®îm[L+{«ã8ó逑"ð ÒlÛ^,é'¦Õ½hõÕ>íR_®5)—ÒÕÓò‘êûK†ôF/?¡Ñ6¡½IéL’À˜ –Uè/±K$-5å„qf ÄN©Ð¯Çn»^¥Â€i¥¯pg-ðQ<ˆÛ¶'JÚ!išIuêËwè²ÏßìÛõÒ Kui[¤ÏÂÓ{ލP®qS ò,ËÒ„ö&B8£J¹¦#½Ãâ{‚ÞiÊ%•Ê0Ö¡ñg×Õð`<FbrˆØOý÷mË™âàËž1ÅÁü~2ÅÁü~ºœ³@.CÈ!T{FÈÁü~rÛ²ib±—c-­{³ô‡G¥B/e¶M9_×ÿ—µê˜5—[ÓgN׋ڴúk¦•Ý'©Óqœ½tÀi¿Vå?nˆÛ¶×JúºQ5OŸ­ëV­óõšçæÓú³ ¢ýM•— é^~j)âÁ²,µLÈ(Ûœb3pJnÍÕ‘ÞaU*ý¹¥æ|R‰$a/”†k* Gûìt`’CØŠ Ù–‡5ä±~Æ>ä~r0 —g¨Iæ÷“Iæ÷“IæßŸ>Œ > Z:Äb/UšÏÁ‡ŽËçjÞ²•|öÂïÕkO?lZÙ[Ç™O÷œöëV¢Ä¶íù’6›Ts&—×¢Õ땟<Õ×ëNŸÔ¤ONnŠôyèî+jÛþ!n ÄJ¦9¥|kFV‚]ãdý}E•†*l„IKÍ-IY YñÌÐ@UÕJ4¿—mdÐIa*2„[Î$_öŒIæ÷“ƒùýŒzÈÁ5àœ·6Bæ÷“ƒù÷gÀï½ >À|1 ;¸1ÜK¯Êò9ø0óš›4oÙJµM¹€ÛÖ¥B¿6Þ¹T‡Þ3­ô¿sçûtÀ)¿†%ð *lÛž(i‡¤i&Õ½pÅ}꘽À÷ëþÙù-:·5é3qh°¢çº|½f_Oý›a‡ÿ¨j¹¨¶É¶$©mònRø&‘´”kÍ(ÛÄ´ÔvðN:“P¦9!‹Œ‘§Gïüt`ŠCØŠ Ù–pðeÏ8˜ßOæ÷“€ƒùýŒzÀÁmÄë,‡À{IÀ!˜²>À8LuˆÅ^úQšÏÁ‡97~Uó–­TS~·±Çœ®µiõ×L+»OÒ…Žã¦ƒ>ô5-QaÛöZI_7©æŽÙóµpÅý\ûꎼ&µDûÃO…rMOï9âé5*¥’ì{S½ûß>v8\ÛYÊO€CnBží!óûIÈÁü~r0¿ŸQŸâõp±aï½ëÁ‡&ø€h~hjüácð!ÛÒªyËVêÊ%˹¥=R,–µaÃóêýÍÃjêùiåotg1]ð¡¯o <ˆÛ¶çKÚlRÍ™\^7­}R™\k ×CàA’~ÒåMè·R*iÿž.íß½K•JiLk¤RµŸw¾¦\ø &@ÀS©LRù %S|0;n;xò¤æ|J ²DþœãBMåRÍøÇÏ !ß÷ŒIæ÷“ƒùý$ä`~?£ró%9„jÏÜñ¾Îr¼ŸLru? > òÏA±{ž4d/ÃPšÁ‡¶)çëúÿ²V³ær‹7О=ûµiÓïT,–eU‡5q×CJ”úL{7:޳n8ék]¢À¶í’f™TóÂ÷©cö‚À®OàaìÞ÷m½þ»_9èpÊ/ä&OQÇŒË >ÀSL|ˆ—Â@YC%6¢AIKÉ”¥Tºþoøg°¯"“¿ê Cl!¬­QuÅ2à±~Æ>à~F=à0ªËpm? 8˜ßOæ÷“€ƒ‘ý$ø€H¿¶ÄâùÒ½ cY>:.Ÿ«ëÿëµM¹€Û}œ6oÞ©mÛ^?ù¥e`Ÿ&ìùŸ¦=”>I:Žs˜®8þµ/¦³mûnIß0©æŽÙóµpÅýÖ0{j³:Ú²‘>…rMOï9ÒØ¯é^Û©î×^ö¬æ¶ÉStñœ?WS ßÀ€w>D__Q¥¡ 1É”u<äLY²È8¢Rr5\¨Y{hƒLqS‘!Ür¦8ø²gLq0¿Ÿ®çÌ·Ëre?]CÎÚxjcŠƒùýdŠƒù÷§áSF³„Ûý”j ø€ˆ¿ŒÜó¦!{ö6û|˜sãW5oÙJ5å'pûÒ‘#mØð¼ ç<§fç9ÓÖ?;Žóº àø×Á˜Ì¶íNIÛMª9“Ë릵O*“k ´Žé“šôÉÉM‘>‡+z®{ aë½¹óEíýž×Jetñ•suöy¤×àñ{ÏLRMÍ)e›SlF¸& … IDATD¸5Wý%Âcp,Øpì„ÃÐ@UÕŠYßÃeÐIa+2d[Î$_öŒC˜ßA…xkÝ^ŠC(ûIÈÁü~ºx%äx/ 9˜ßÏz)!ø€(¿§Ìs§!{iÚG6} >d[Zõ—ÿkæ5_ä©`„^}uŸž}v‡ŠÅòGþ¹¶]ßUjè ioã8[è2‰ÀÃÙ¶½CÒ,“j^¸â>uÌ^xçæÓú³ ¢ýM‘î¾¢¶íjÈZö¾¡×_üµ¯õ_Åá Ër0¤ŸLqà½÷G,êv?MðÑy>ðºF7†{éFô\–ŽÞô6øÐ6å|-ü?ïÑ%ñyž2$íÙ³_›6ýNÅbylû¹ë»J 4ía/pg ÝâÀ#Ù¶½EÒgLªyáŠûÔ1{A¨jšÖ–ÑŸNÍEòŒÊ5=½çȸ×yÿÝ·Õõë­>–Ž3Õ1c7>‘HZjÊ¥•É&™ú2•rMƒ}EU*5ÎiBJ¦ÇGÕxažîxÐIa*2„[Î$_öŒIæ÷Ó5àœùv9B¡ì'“¢ÑOBæ÷“ƒùýôùë'‚ˆÆk¶ Ï©†ì¥ásyâ-ôHxTz{‹§Õ]üç×ê/ÿþµM¹ ¶Ï›7ïÔ¶m¯kÔеízÈ´‡¾ÓqœN^;€x#ðÀ8¶mEÒ÷Lª¹cö|-\qèêJ',]{Q«ÒÉè}ä¥Cz£·8îu‚œîpü‹TFs¾°˜)SÂ#îS8D[¥ìjx0|Ó :Ì:a¢CC0ÅÁ÷=cŠƒùý$àâW ¦8ŒþRBÙOÑè'óûIÀÁü^ºá¨‹àÌ|®7á¹ÕýŒKÐრ=ÒöÿW:ôšg•f[Z5oÙJ]¹dy¬ž9ŠÅ²yd«<ÜõrÎsjvž3mîqçn^G€ø"ðÀ(¶mO”´WR›)5gryÝ´öIer­¡¬oú¤&}rrS¤ÎI¹ZÓÓ{úU®ï5nðp¯¶ÿÛÏBñ˜>6kŽÎ»ø“< ,ËR¶9¥lsJ©4Ÿ4÷õëRU…#¥ØMuH$­ãá†d’€C”¹®4Ô_Q-DG¼ïÀÛúÕîÓîÿý”ÁA¦8ø¾gLq0¿Ÿ„Ìï'!‡ð÷³Ô/÷ðnéàï¥?î–[î—n?ýŸo±¥Ü¹²Î™-u±¬sþTJ·šÛOBæßŸn#^g 9ÞOB¼÷öpQ‚0ç9?ìϯ†ì¥áséŽb½C¯J»ó4øÐqù\-üû{4å—Eþ™£§§Oë×oQ±Xnèºm»¾«ÔÐAÓ¶ã ÇqvðzÄF±m{ƒ¤E&Õ¼pÅ}꘽ ´õEqÊC£¦;t¿¶Sݯ½ŠÇÔ6yŠf~æs< tIKM¹´2Ù¤’)>…î•j¥¦BY¥b%6çêxÀ!eɲ8qQª©\ GÚáXÐáåŸ?æßEt äàûžr0¿Ÿ„Ìï'!‡ð÷³Ô/÷Ý­rßzâ£Ã #}N;ïÓÒyŸ‘uþ§~ ä0îÚ\Þ7¼,!úérÖ"ýÞ;¬ï»½Û/‚qçÆ¯F7†{Õ©î8ÖëÞR>z<{ón¹]ó–­Œì³Çïÿºž}v§'k§†¨m×C¦mÉVÇqæóºÄưm{¾¤Í&ÕÜ1{¾®¸?ôu~¢=«Ë§4Gâœô Wôì[ Yëå­¿P_ÏÐ<¶yÿñË< ÔR©„2Í) T­ÔT,«4í Ã‰á†D’€C|Ï»«¡jàu˜t äàëž¹!®í9„á¶!äðÿ³÷öQR”wÞ÷·ªºzfº{èqœ‘tf4"Q4Y ƒ F£Yû¨{³²yöYï¼ìÜ5Ù?r'brïý²‰ÉìÙ˜Ý{“ì]ŸU7‰E„צhÀ$ä€7ö¯kØÑE¥ÛC¡àà±ÇÖáСÁ@ÆËnû!#‡Tš¢€^Û¶÷ðó†xAᢖeõø’J5ôžÐ5w¡2õšº†ùÝid›Õ”œ²‹çö #7êßO‚|þ'ÿ"Õ1Rx „B!ª¢IÉ&MÍ NÈÙîmŠeŒŽ”"%:hNtn à@ÎÄÈPåR8ßU«':È*9HX›_uÅRrˆXž”$þ äà}(Eò,«ttxí‡á”bf }ðO¡]öGÁÌ%‡hœŸ”ÔÏ“’ƒúYFXr8me"H̺:PtˆÖºLÂá-Àk+ÜÞ†i׬áæ¯|Ù‰)ù20ã®E¡à6fbäd·ýHµ©úÿlÛ^BH¬ ð@‘˲z¼ªRÍ]sûðÑ{¾£Ü\g› ÌïJÃ4Ôúé« „B!„4Š1ùÁL²ûW`t¤„Ѽ·¬þwuc‚ÃØ/Ý à@ÎN!ïÂ)ºŠèÐþhÓ?SƒèÀNuÊæAíœd8m(94lÎ(9¨›çï·ÃýÝ7Á7Ã/¯sôk¾¤'I´\(9H3oÂÏYJ¡çIÉ×ÞJ—g¨˜âCñ«QÄp.ED×¥jÆm¿óçÀ¶œ¼ïGÝ”nÅÇ>ÿ ̼áv¥ÒÚ²e/žzj}(c§ìçÐb?§Ú¿Õ¶íUüü!$>Px „HeYÌV¥Þd*ƒÛúW#™jUr¾³Í®½0”"åFKøí[yäÿŒ ð@!„BHãÑ4­">$õX Â(Ê(Ž–Q,¨ÝÍA×qR÷ Ä ¥¢Àh¾蘡ˆ¯‚ÖûÀººÚw ÉÞ´$®Í¯º(8¨Ÿ'õó¤à ~ž»WC¼øM¹J63Юù´) i¹PpfÎ(8¨ŸgŒg‡¾×V_¾Ø÷K¸Û(>(^œj¤è­Ì‘ΰ­3 lþ1°]CfáÒ7¹Í™ Ò'¶fÍF¼ür¸Âøy[„^Ì©´ÐszlÛäg!ñ€Â!Dj,ËZà>•jþè= kîB¥çÝÔ5\{aé„´5:e¯.`çÑBÃÆ ð@!„BH8$’•îFBC2i@Ó£ñ}ÉqáË(Ê(Ëʇ®FB?!8hlÒAj='–ä(9ÔYdà›µsÍ÷2"”gÔ%‡ª†V<ϨKž‡S0Ïq%^ý.ÄöǤ½öÐæ,‡vÙ-JRŸ¢ÞÏYJ¡çÉ.¼öVîܬ§|q\|x(XñaÖÿt_Ï/+"·þ„"‡,³´÷b¢ñõÞ ¼¶Èíõ}6²/ÄÍ_îG×ìI™V¡àà©§ÖcÇŽ·C¯ÅÚ‹ ;QmÁÿ­mÛËù™DH< ð@‘˲zlU¥æ®¹}øè=߉LSÛ›0½£ ¦!ÏÓ+¹Ñv-âíc·±Ÿa¯<³ùÜï¥9ö«nºÍiþd B!„?tCC"aÀ0?doèJt‚JE¥RnYÍïá(8†œÊr‹²J|Ð*쳓ƒ § ;94lÎØÉAýð šÖ«¶øÚ¶½–ŸM„D „i±,k-€ªÔ›Lep[ÿj$S­‘ÊÁÔ5LkoÂÔöd(âCn´„\¡ŒÃÃe äKÈ;n`co_ÿíÝ%EM© ®þľ1B!„2ŽDB‡nèÂРt]ƒ‘öÞ¥\ráºNÑE¹ìÂ- µ;8Ú ¹ÁHhÐ4®5â/AÉrŠìâPg‘oÔÎ5ßK`‰ò8,éóŒ©àpÒ_+";œxÏ=ôÀ.꟟ÔÏ“‚ƒúY²‹Cî×)>Dü3¬Zò‡€ÁÝ@ní:?˜éÆÖ(":—‘zÛ‰€èpÒ:^ý~¥ëƒÏtÍú–ÞÿOhÎL=µ}t- G®{¥ò(Ú¶ýz1§ÒI°Ñ¶í^~F}(<B¤Ä²¬eþY¥š¯û³¸ô#é\&eLt¤hk2БNÔ¼§ì"7z²¸0X(£T(º¹Ñ2œãÿ “#öãõ߬“bî/è¾—]ýa¾9B!„R%š¦Á8ÞB7*]!Æ0“Þ„×(—ÞûÍ9.3¸ÇåÕ¡à@‚$ÙA>Ñ[ÕQ`(›¹óxtr äPÛЊçuÉÁÓpÑ”N¼L1ÙáÄûoÏ' ÍûšZï^k£ä ~žìâ þùIÉ!:YúR~ýÇEñ!bŸcÕâ ¯?ìxòä?7SÀ´OÓïE‡8¾õˆ¼TÈ3×WÄŸ»=4¥[ñ_îÿgtÍþPhÉíØñ6žzj½t²Ã‰·–¡½˜°ãÕNˆûmÛ^BH¤¡ð@‘˲ÚìU¦æésqÓWÀð"F©XÄK?_…R©z-3,B¶s"C!„B!„ÔÍx¹ÁHÐn AÞg7VvKt`'‡:‹ |ó vÁ¡ÆÚØÉAý<ÙÉAý<=–-v¯†xñ›Ê^›hóþ´ž›£•gÔ%áÇç,%‡P³ ¬3ßJ¢ä |–¾”ß˜ã¢øñ{‡SyökÀá-gþûi7³>«À! Æ,í=škÇvç W¤û%ßgðºÏü®ûã¿ <¹-[öâ©§ÖK¿ÂÒžAóÀzÕNŒ‹mÛÞBHd¡ð@‘˲V¸K•z“© ÿõ£ÈtNfxdûúphï®PkÈvNÄÌ‹!„B!¤&(8(Œ¸p nCö-è «ä±‡â;,JÌ%õó¤ä ~ž5–-½±æó ©vöìÙhkk;éÏ6lØ€\.çûXZ߃Ð.¸Rí<)9¨~ ß;B]”B™·8äèKùÁWàâÃXGiŸÌtÄ¿Édîý5ðò÷Îýºùß:gHz¸ä½OrMà™vi7¦ÛÃ¥þ8nþr?š3Iï¿x ¯½¶G‰•¦•GѶíGЋ9•Nu¶m÷Y(<B¤Â²¬>kTªyÞ÷bÆŸbxetx/=õD¨5°»`ê:R t¤hk2Þ÷÷ùRƒ£.sw\.\B!„[4í=ÁA7(8ð(ޏpŠþß«…/:Pp¨£ÈP6jçÂÊP‚y£à ~žªÃ9÷ßoœ!_ªíîîÆ²e˰dÉôööžñu{öìÁªU«°råJlܸџ©23ÐoyBò‡6eD(»¦à HžB‘µÙkovqvZÃ?.Š^§çêî0FÇ à#ßìc–ö~MÒŽg½iL·‡ .¹7¥§Îhh‚*Éc$sÛѺû'ª,bÛöJB" …BˆTX–µ@·2õNŸ‹›¾úqöm݈}[7‡2öäK/Ç%³¯ŠíÜw¤˜ÖÞ„I­fÕÛäFKØy´ˆ·9p\^çB!„h3^p“‘á#Ãe¸eïÍÂ$»Ggü¬E¾yP;×|/!ByÆ^rˆ@žQ—< cÉaüîžÿ ÄgëÞÏ‚ °bÅ ôõõyÞvíÚµX±bÖ­[WÿuξðA¹óŒz‡sì–’ƒ"yRr൷rçfô®×)>Dp­þliõ¯½ö¯€Éó$8dŠ¡eÙaü‹íõÀ«ïk·‡¦t+nþr?.ûƒ}O°PpðØcëpèР’»‚dn»J%çôض­æ„BÎ …Bˆ4X–µÀ}*Õ|ÛwŸD¦s2˯<³ùÜï3•=W.º9–óÝ‘J`zG3:Ò‰š÷á”]¼~¸€G \À„B!$2è:`$tè(8©)9…|~~ýžèðùã$ZA–³“CXJ0oìä ~žìäPûˆÛ…xµ¿®}d³Y¬\¹K–,©»ž•+WbùòåÈårõ½§÷~ ÚewÈ•gÔ%áÇç,%‡Ðó¤äÀko¥ÎËzÊWë¸(>DhÍþûÕ?Øm¦*]²Gâž;nQ7þÞMqÑa<ÎpEzð¹ÛÃuŸù \÷ÇéÛþT—@+â¼­B++õÜÍmÛ^BHä ð@‘˲zìV©æÞ¥wcÎÒ»^LƆgV£T*2^*{f-X„D2»¹ž9±ÓÚ›|Ûßáá^>˜GÞq¹ !„BˆrŒ c4sBä§0âÂ)øw{Ñ] ËÎ)8„•¡óFÁAý<)8øƒs î¿ß 8C5ïböìÙX»v-ÚÚÚ|+kpp}}}ظqcí;13Ðoù`fÂË“‚ƒúç§Pì}#ôÝQpeÎØÅÁCùê?O¸øÝ é¤.P9|yøÍÿ¾èášæ,ÒCE%OcŠU¿x×Ïm?ñµÛÃÌnÃG?÷ 4g&ÔµŸžzj½Ò²ÃÉÜv´îþ‰je/´m{-!‘‚Â!D ,ËZ `*õ¶w]†ÅÿãQ3†bóÚ_5\z˜|éå¸döU±›_S×pí…麺:œ §ìâ¹}ÃÈ–¹ !„BˆÔè†ÃÐ(8%.02\†[öç;gŠïûÙuó v®ù^F„òŒºäPÕЊçuÉÁóp æôtþî›{V×¼ý]wÝ…•+W6¬¾eË–áÇ?þqíïù=Ÿ€6ï¿›gÔ%‡sì–’ƒ"yRr൷rçf=åGó*Š*æ|¼Æ½k€—¿çmS3L¿˜vK‡ÌŽòÞÇEXtOnO¥Ûû{}+õ‚K®À§øiÍÒÃÀ@>º…‚™Ï‘ÖÝ?A2·]¥’÷Ú¶ÝBH¤ ð@ ˲–øg•j^ü×ÿŠöî0¼R*±iÝ3Èç~ïû¾³ÑuÅ,d;'Ærnçwe";ŒAéB!„Ȉ>Nn04sBÔÄ)¸(ŽºðãëæØŠ|Ð ²ìœÂÊP‚yc'õód'‡ÆUtèˆ5Ÿ¯yûFËcÔ+=è7ÿHOjlžB±÷ ŸwMÉA‘<)9¨Ÿ'%åGõ¹©÷—Ø÷t8âC×õ@ç£ýÙ×Èÿã/+u{%ÕYºÆk.£*:xziLd‡ñ/ßòc`×S¾•œx!–ÞÿO˜8u†§í¢(;€^Ì¡mÛ¡• *•}¿mÛ+@‰ !¡bYV€=²ªÔÜ»ônÌYz7Ë9û¶nÄÛÛ·ùÒí¡}Ò…˜rÙôØŠ0½£—w67|œÜh ¿Þ=ÄL!„BBÃHh'IˆêÍ—Q.Õÿ=s,E‡¨KŠ%‡ðr”`Î(9¨Ÿ'%‡@êrýy`àÕšö°xñb¬Zµ*°Šë‘üëò@ÉÁûç,%‡Ðó¤ä ~žBò5&Í”ÆGp8ã+Ã:fT:>„">µ3Ø<÷õÚwÛÕÌýóxÌ'»:È5yAˆãc¿lø{ÀÉûR~SºŸzà§UKQ•Æhxé¿R­ì‹mÛÞBH$ ð@ ˲ú|I•z3“°ø<Šdª•ᔊE¼½ãuÏâC"‘ĄΠ½ÀÂù“/Bs:ëyL™:>>mB`ã½10Š×rB!„@ß½ÁHÐn ѯ®±(9@–ÇCr`‡Ú†V<Ϩwqð4?j«twøBM{š={6Ö®]‹¶¶¶@ ··7n¬iÛÚº<‰—ž|·É’‚ƒúy²‹ƒ‡ò£ülTíÇšøÐ}}åWÔï;ü¬qÓ?;V×>ÄÜ/úÐéADíˆÐ½] ;:œéòÀú€w÷úv87ù»˜yÃíg}Í–-{ñÔSëu&ìø˜CûT*ymÛ} „D „а,«À•j¾é«ÿkúU ¼ÜÀ;È ØÈ  ÿþ÷(•Š'º6d;/@:ÛŽt[{ì‡S™;¹]Ù¦ÀÆsÊ.~¹ã—×@„B!Ä(88àWW‡Øˆ⌿‘)ÕP7jçšï%Prx ò‹ƒôy²‹C¨µÕÚÝ!›ÍbíÚµèíí ühöìÙƒÞÞ^är9ïŸUwy äàýs–’CèYRrP?OJ¸^—(¯ã»ûŸ†ûÆCÀH€âCª³Òñ¡a⃈V–Î0ðì×ÜžÚ†Êö×?½¹dGù&°‘µ{ùŽmËCÀ®§|;¬³Iq‘@/æpÞÖU+ûVÛ¶W¢<!¡aYÖ³U©÷Š ×|æ^GˆÝÝaŒWÞÎco®È!„BH]hÚÉ‚ƒnPp ѧ8ZéêP±ØÅ!¼ÚN}¯öµ„ˆåIÉAý<)9¨Ÿ§­ÕÓÝá»ßý.–/_ÚÑõ÷÷ãž{îñ¾¡™~ËÏ3sî9‹ºä üøœ¥äjžBuÆkoæè[ùìâPïnBº¯¦}0Ó1Yß5Ö˜Û]‘œ|mÛßúSyæÒ¾ T~Ÿî&Í«~ ˆŸÃÈ"äe]OWÛî_Wj=gNᣟ[«—þß'ýYœd‡oÛösh±ŸS©ä€Û¶AQ „P°,k€ûT©7™Êà¶þÕH¦Z!>2½£—w6>nÞqñËï2B!„â $ΔK…|n®C¤E>dYvNÁ!¬ %˜7 êçIÁAÊ<Åï¾ ±ççž·›={66lØz}}}X·n÷Ï“y_…Ös3äD(»¥à @žxÝ­ä¹YOùìâÐ[‡0Ä3U‘º¯RDx×Yg=Òƒ'á¡óyx ðÛÿýþc0SÀ¬?ºFìí,â]j¬ýݽÀ‹ßFûr¸3o¸ 7¹@|ôžï08B|&¬îc°Ë!„B9•ñ‚ƒ‘РéœoŠ£.œ‚‹Z¿BޤèÀ­ ËÎÙÉ!¬ %˜7vrP?OvrP&OñÚ!¶üÈóv‹/ƪU«¤IªÖ.ú'~¤'©ÿÙ^Ãn)9(’'%^{+wn™]Ž IDATÖS>%‡0£M|˜4¯Òõ¡óƒ ¯‹=dž?Té’ÛSÝv]}ÀÜ?ÿ½ã—Ÿòg™æÈö(zúStðï; ŸýÕÞzÖ—iÐ&σ1óÓˆ3éÏ y@)éc£mÛ½ „( …BH X–Õ`*õ&S,þëG‘éœÌðñ‘°»;ŒÁ.„B!ñF7´rƒaPp dŒ’#P)ÃukÛ>r¢CÔ´RDp(9„“¡$sFÉAý<)9(™§ûä­À°íy»Ý»w£§§GšÔjíò õþ?Ð.½#2yžk·”É“’ƒúy Éט4SJÁAÆèC:fTćÉ×(´>zÿý1àÇϾ­™®à 3œËÜà×÷V÷ÚT'°ðÛ€™Žæùˆì B^Öõ|·á£èpê®ö? lü_¦#îÒƒVEÛ¶A/æT*ûÛ¶ûAQó}‡Â!$(,Ëj°@·*5Ï»ó^̸ñS ŸéÎ&qåäTèu<æà·o 3B!„˜p’àРiœBÆ#\`4_F¹TÛwÆ‘¢þ UÔ»8œ±ŒåIÉAý<£ÞÅÁóp æ‘.gÜûg!žÿ+ÏÛÝu×]X¹r¥tIöööbãÆÞ>‡&χöÿKí<ÙÅAý󓂯½•;7ë)?ÊÏ/‰HE/o„ØöÄ‘MÁœê¦ßt_¼«ÝUþPE|Ø·öôsví_Ù‹Ã_P‡·ÏÝWýë;®®ûF´Îqvu¨âå>JgÚÕ‘­Àúï¥|ÝS£µ_ ãê/"®$sÛѺû'*•œÐcÛö !ÊAá–e­pŸ2õNŸ‹›¾úGH¸öÂ4&µšRÔòä¶—×C„B!QdLl(8rz„Š£.œBm-"!:ðA+Ȳsvq+C æ]ÔÏ“]ÔÏsüB}ñ›{~îykÖ¬A__Ÿt©ö÷÷ãž{îñ¼~Ûª•%»8¨nÆá§ÿS.ŽN–¾”ÏNjEr1ñáápćîëi·HÖ äÜó‡*]wW~ßv10iž<‹Ê«ðSofþIŒîc$:¼oŸý\»{w/ðÂ7}‘Ð|ðW@¢q¤u÷OÌmW©äÛ¶½ „å ð@ ˲zìV©æÅý¯hïþÃ#¤Ü:½MšZ~»‡†B!„Æwo0´©§à¢8ꢖ¯‰•¢.9("8”ÂÉP’9£ä ~ž”ÔÏó C¸O,œ!O»š={66lØ eƒƒƒ8ï¼ó¼Fõ}Zç¹ó¤ä þùIÉAý<ÙÅÁCùÔ‹þ܉Ã!ö? ±ÿ™`K3SÀ´OÝ Ôѹf‹ª8å OÞå}»+¿t-Œøý ;:øR¬×]9yà7߬Èõ’lEbáGÑÊ£8oëƒÐÊ•Ê^hÛöZB”BçBb¥JÅö.½›²! "ÛlHUO›dõB!„êдŠàlÖÑ’1iK %c Ù¬Sv ¤ Ê%ü±2 #Þe‡Ü;û±ú[Ëñ÷w^œì0ñ*hÿ'h7þs}²ƒû%ÆýF6ꨭ¡‡åïεq¿ê/CÖ<…÷Ú;”çìœC+ž§ãÐÓp æ)Â.;€ÁÏ1„8ð¬gÙ–/_.íµO[[,Xà}ÃC¯È™çYv«Uõ9+ä>?Åø_ ®¶4$Ìsüu·”×Þ>ÌYCKæû”zÊWü¸ª:è¨Eï­ ­c6ô9_†ñ±‡¡]´(¸2<ðúcÀ/þ+ðòßU:¨˜ƒvÀ޽a¦+¼òʃ•Ϊó”ªx¹ð/‹Zve¦€} ˜Ð]ÿ¼¡üÒ÷G„ÑŒ¼5_µ²W‚¢! Dz¬%¨Ro¦cfÜø)GHƒ0u¹>ËRx „BQM¦†¦©Vé,BjA¸@!ïbd¨ ·ìí_•NzŽ)BÅŸió†ÖVçû8j”Î8=*ä)C†ÌÙ9—¸P?ÏPÊpPO)šgè%‡/9œÄu5 ±dÉ©¯ƒj©O ¼*OžUJÒ}T{pÒ §Î›l§º„y*uÝ-Ó´SrP÷€E„¢÷é¸Rß.¹µò€{Pì]Sžý0ðZ@s&ßGMCëôƒé·ÙïÛ=ÿõJ‡ˆÐÏ?ó¬7ø0…›¿÷ðåÍï4¢C=Ó`¦€üO`ò‡ëŸÂ#ÛàøâÈhç<8™.•Jî¶,k!J¡ !8 „†aYV€ ºU©ù¦¯þ#¬éW1„bÌ ô|¸äVû¸o< Œ¼Ì؇·T~½þ0ýÄŠ¾•–º¸æ¯€çïó¶}n°o еPÑûDо*±è0Fyó#0æý9´Ö)ˆn2‹k>ZìçT)9  ÀB”€! ò¬^¯ªRo2•Ámý«‘Lµ2ëú¾­sÂ(8(r~²‹¯½•;7ë)Ÿ’ƒzÑËUØÿt°âÃf ˜öI`Ú-€™oþâ,:œÊÎ'Í+½í«ã àºoÈsÎPv¨âåÊûŸ¶<ä»èpÒu|ëóþH´ nœ·õAèÅœJ%/´m{-!ÒÄFÒ¯R±óî¼—²!08Zæ$B!$tŒ„3©Ã05hu6 Ò´ÊþN7ƹpËBœ,Aœø3WÀu£3ç'Ä’1Á¿i(%G /ÃËÏ»‘^tàƒVeçìâV†Ì»8¨Ÿ'»8¨Ÿ¥hð†¯xÞóâÅ‹•€Ú„zèì ,GJŠœŸ”ÔÏ“’ƒ‡ò)9¨½ÜyiÝ㢠o„Øö0Ä‘MÁ ìä+Ývü{E|è^Xé4‰5"Ô<ͦÞr¼kÃÚê÷wx«b÷|)ÖOÑaûO€‘Ã_úÇT:=ÌùSÄ¡®[0aÇ#*•¼@!ÒCáÒ,ËZ`2õNŸ‹K?ò‡ Žp\¹¾p9œ/1¢6B \.C­ü:¦C×5hºÃ0À'K !H˜šZ iº œÔIÂÔÎôVB‚8Ýe…‚!!]ºÀh¾|R—™s!µèuÉA¡(9„‘¡$sFÉAý<)9¨Ÿ§nCqÈ{ãî¾¾>e®•z{kœc Ï’ƒ"ç'%õó’¯1i¦”‚ƒzÑ åbÐΟ íó!Žl„Øÿ4Äþg‚L|xý±Šô0ýŽÓˆìêXiW~±"=äöT÷z3¥È½#E_ UPt8ùÞj3ܽk¡w«s¿äËÛl¦…ö™h:ºY•’»-ËZaÛö B¤†Â!Äw,ËjƒbÝæÿÙý Ž€M0È;.C!ÊR.•P.98ãí.Ü2€rå’ÃHÀ0MN!$–è:Д2ªêº 'u8!‹1^nÐ „„AqÔ…Sp«îê ­è@ɲì<’»8Ô6´âyF½‹ƒ§á(8H›§¡®Á7gÈófK–,Qæz©–NbðÍÚ>ÙÅAýs“‚ƒúy²‹ƒ‡ò£*9øxl”RªvþlhçÏ.ûc¸Û N|€½k*¿&ͦÝtÎP$"­Óíºû§?W‘QÎEÇŒp ¦èPÅ&>»S’è0÷' µ_ ­u âÄð”EHæ¶C+T)y¹eY+mÛÞBˆ´Px „4‚²ªÛ»ônd:'35B$7ZB¶YŽËvx *"J%n©äi›rÉA¹\†™LBÓuÎ#!$6˜IÉ=²àŸKˆpâ'½Ÿúß11¢Æ *Š$„D‰rI 0âV-9I':øú£ »uó vÎ.ae(Á¼±‹ƒúy²‹ƒúyŠðëƒÛ=oÓÝÝžže®›Ú‚]¢qnRrP?OJÊg'µ¢ŽA©  ÷Þ Ìø¯pw=±û À¦Öƒ/V~ųt|¨W| èà 3 \÷ `ͽç~íÔ[‚/8ÙA„Öür Eû%`ËC¡Šãq7?ãÃ_AœF3†§,Bfß“ª”œEå‡;/!DZ(<B|Ų¬^_R¥ÞLÇ$ÌYz7ƒ#$`öæÌ’@xÈ;.;<õ¾¥b­qí N±#aÂHðv€}šZt˜M”¼Æ„„3‰ Â\÷dbüŸ¹®€aT:7Pp D¦kC 8âÂ)Vwm(•èÀ.áÕv ”ÂÌ1äy£ä ~–”ÔÏSHVÛ¡W=oÒP@6Ô%ÎO!qm¾—DÉAù,})Ÿ’ƒZÑGUr8Ë Í ô|¸äVˆýÏÀÝõ3`äP0õÞ<÷u ÛSéøÐ½P¢¸D´O¿lpå€W<ókºú|êðÀ®5-ü¬½¢Ã‘­ÀöŸG^—ë9v O» q¢Ð> MG7ÁÚ§JÉ‹-Ëê³m{-!RÂ'œ!~Ó¯R±ó゚‰9˜5±%ô:v-0 ¢Â­È 5ÿ(î;(;Ehº]78±„ÈÒœ2HòÁüjÐtÀÐÏ.EBä¢T(Œ”«º4”FtˆºäÐðC£äÈœQrP?OvrP?ϨK5ÓøÚÄðAÏÛôöö*wµ`Á¬[·®aRrPäüd'õ󌃬âKùQ|<6ÑãRýÚÔÌ@»äV—Ü ±ÿi¸ÛN|Èí^þðúc•ŽÕˆìêP?] ÔÀæ®d0Fª¸üöÊßKuOIÑÁ—B#,:ŒÇÝù hgAk‚81ÔõIœ·õA•J^  „)¡ð@ñ ˲–X J½]sû`M¿ŠÁyÇÅÁc&µš¡Õà”]ì,2 ¢ åR eÇß5[*:H6逯[ !ÑÃlÒ);B"‰[(Œ¸'º±œÐEè?hKÁ!byÆ^pˆ@žQ< GÁAÚ<…ë Øáጇ3SÇç,‡ÐÏO ¼öVîÜŒâõzTßb¶Cpµ‹n€qÑ G6Al{âȦ`Ž5?ðžøÐ½°ÒõÁL]ŒD‡ñtÌ~Ȫ̪³"AHuoIÑÁ—bU&\ŒÅc5mîn~Ƈ¿‚8á&³±æ£Å~N•’»-ËZaÛö B¤CBp!ucYV€=²*Ô›Lep[ÿj$S­ È6¸þâðÎÁ7FñúáQA”À·Î§ÁH˜0L““L‰FBCK†l!»&@qÔ…SpÏùÚPE‡‰cø UØ;g‡°2”`ÞØÅAý<ÙÅAý<…ëìTß„ûô]5\£¨÷oÍË–-ÃücoŸ« þZ石”B?(9ðÚ[¹s³žò)9¨½ˆh Á—xw'Ä®' ö?ì<˜©Šô0í ‘ŽßÚPòm†²CÍcËÜÕ!hÑ!=ÚÔ¥ÐÚ¦Cànú; 4RÓ®ôËo…Þ݇8¡•GѶíGЋ9UJÎèµm{!RÁ„¿XEdè]ú_);öÂh;Ž0­½)ð±óŽ‹G ¨ “ \.Qx „D MšS”!ÑÂ)¸(Žºç¼$ Mt˜}¼£CåVÖ ëP7jç”ÂÊP‚y£ä ~ž”ÔÏSEÉa|Ã=o³`2M¿O¢§§§Ÿ³”B?(9¨Ÿ§|I3¥ÔŠ^D8†àM›0Zï½ÀeŸ»ýaûÀnüÀNxýq`Ç“ÀÔ[Nßñ!ªëD¹·Š5MÑá=šÛ¡ußmâ5ïýYf ´©·BlûkÚ¥»ãЧ\$Z„ÑŒ¡®[0aÇ#ª”œÐ` !RAáR7–eõø’2õNŸ‹7~ŠÁ"o Œ¢3e Ûì%ÉËoçá¸ìrEÔ Tr&;„€ë–¡ë|8˜ Ì&šÎy „DƒrI 0âÂ-Ÿýz0\Ñ᪱ KÉf‚CmeDèÁ¹¨ U ­xžQ<§`žQêBÂ<ßô¼I­â€ŠhÊ›ÇkcõÏMvqˆN–¾”OÉA½èEDcä¸R¡÷Þ 8Cpw=±û‰àć7v> LšL¿H]ÍuBÑA® TFt8eûzw•6þC¸¢Ãø{ƒ ®w@¼ó¢÷}—Fàîx úåK'œL7ŠÙËÌmW¥äÅ–eõÙ¶½„i ð@ñƒ•*{Í÷21Bd¹©q^>8‚ù]i˜F0O&¾òv‡ó%N>Q!ÜRã׫(»…BHдŠð@!Ê_º@qÔ…StÏúºðD‡Ïëè ÕÌ…ºyP;g‡°2”`ÞØÅAý<ÙÅ!y É×Y­µ ¿íyË8 JåÉ.êçÉ.<7ë)Ÿ’ƒZѳ“C(˜èø pÉ­ö p·= Œjü¸NØ·¶ò««¯ñ¢ƒ¼÷œ|)ÔÑaûO·ž fCtLÚ%K!w…£ž‡q÷®ƒÖÝ­¥=VwQÃSÁÚ ­\P¥ä•âzÓKˆ”Px „Ô…eYK(Ó³øŠ íÝ`p„HDn´Œçö "=¼òv{sEN:Q† dp]Ô9;º¡A7tèšÃ|ïóÊHhÐumÜù$P.½÷ nÙqá ²ãBvj4f“Mã<BÔEÀ)¸p îY›|Qt8iÖBÙ4èSr+G 挒ƒúyRrP?Ï(uq8Smöç½ôõõÅéJ R?|NÉAýó“’CÄÞgë)Ÿ‚ƒzѳ‹ƒ4˜hÝ㢠ö?œøx(:È}ïIÙÁ¯{Œš ZtH´TD‡)}ÕÏU¢Ú>±é{5 é¾öŒ«ÿqÂMf‘·æ#}àWª”ÜmYÖ Û¶W€"!5cYV€~UêÍtLœÿr7ƒ#DBƤ‡k/L#e6Fz ì@”¼éwËœB¸QNH$t$LÆñÿúpJ%%ÇE©è¢T*Ã-S‚ð3Éî„u)9Å‘2ܳ4u èpâS5”MƒÞy<$vq¨mhÅóŒzÏÃ)˜'»8(–åij;]©Ã=ïUÕ½½½ÞgpàUhsäÉ“‚ƒúç'‡èdéKùQþŽPD4zJ²sB|8² bÛCG63ð9ÅÊòÞƒÆUtðù¸U¦,¨ˆ‰–*§ö½?вӀ) ¬ó>kGw@Ý­}âÄhç<4Ý„DP2Zý,·,«ß¶íABB‡Â!¤®uݪ{ÍgîE2ÕÊÔ‘”Üh¿Þu s'§0©Õôm¿yÇÅoßFn”Žõg{ÚâßqÒ€™4`&u˜ÉÆõ;Ñtíø8®üYÉqáË(Ê(ùYUº¡A£ï@Q·,PqOêô¾û¥Ø‹"Ô̓ڹæ{ ”$Èã°ìâ }žìâ ~ž1éâpVòÞ;<¨*<´µµ)X5%‡HœŸ”"ø^Åëu‰ò=.^Ÿ: Úù3¡}ø[ñaÿ3ûŸ fø÷‰±8mÕ¼eG_ŠšèPå|i]7BÞ Žz.ÃÝùŒöxuy€ü”E˜°ãUÊÍ¢òà—:!5aYV€û”©wú\tÍ]Èà‘Çøí[ÃèH%0wrª®nNÙÅΣE¼~x”K!ä$4MƒÙl Ùd ™4 éZhµ$ÌJ‰–´ á ŒŽ”P)¡T¢ðä}.5N!D)„ G]8Å3¿çÇ[tµ‹ƒ¿Ä£‹CµQrP?OJêçIÉAý<«-µ†î³gÏæ]y EÖš4§%‡Pæ’ƒ‡ò)9¨}T%‡x®CíüYÐΟ\v'Üíÿ’øpûi:>(²Ä÷­oò‡*ÇÐ1èZ(gÁªØD’® ‹'ísꈭÿä}cÚåÁÉtc´ój4¬W¥ä»,ËZiÛöZÞ£.!µÒ¯R±óÿì~&FˆBΗðËïbRÆDw[ÒSLJÜh {sö Ḃ“I!ÀÉ’CS³œ·Âš®¡%m¢%m¢ä¸É;pFË‚ŸgÕ`$(<BÔ@À)¸p .Îô_Ѽ—@ÉAÂÅRǰ”¤Ï“’C4ò’¯³zk«åí·†îjvIPâj‘¢p~²“CÄÞgë)?Êß뉈F/"e›¤&BïýKàÒ;á¾õ Ä®'€ÒpãK•I|ð2mÎ0ðü}@nϸ?ÜZ9–«ëîÌ´‚÷¤qíê@ÑÁ7ÑaÜßkçÏ„ÈNr;=—Û.Ö|4Ý­\P¥ä~½¼_%$\(<Bx¾÷ÉãÈí©ü}C¥Š5-x쪈´‰ó M]ÚÑa<úÔ[á¾òmï³Ó.ÂhÆð”EÈì{R•’g[–µÌ¶í•¼‡%$<(<BjA™ïLÇ$̸ñSLŒÅq\ÃùçKœ +4]‡pÝ@Æ!$RçÎñn©´ #¡+þ> !•1Ñ’JP|8gîœBˆ¼”KÅQåÒéßÃã%:Prð^»8H¸Xê–]¤Ï“]ÔÏS(°Îê­ÍïR‡zÞ„ÂCyRrPÿü¤äÁ÷ÚzÊçƒåjE/"×aM›š™âƒx븻žF5þ°‚jÂ}kÎ,;ŒÑ0é¢CÍcû*:(ÚÑÇE‡î›€ævŸçë ŸžmâÕï¬÷\«{àw0b&<@¡}šŽn‚9´O•’û-ËZeÛö ïi  „OX–µ@·*õοû~$S­ ŽBˆ’" L½¥6ñ!Ì¥R³ôqÑ¡Ñ绯²C÷'JŠg;Pám‡Ð.ª­Ëƒ{àEèÓn-±»?Ë[óÑtt´rA•’Wèã³³ü¯ IDAT5!ÁCáR–eõ¸K•z¯¸ñÓÈtNfp„B”Ç0(—ß÷« “݈¢7² © ÉXŠ§Ò’6‘l204X@©ärqBHˆ”ŠÅÑ2ÜÓ¼GWt äP[ìä ñ@5 ÍNÒç‰ÉØÉA©÷ ¯µQrP!q†²>ÐLÉ!:×Þ”"rÀŠGUÉëP¦)ÑΟíCß N|pò'‹Óogn:>ÞêmOÒE‡ºÆ OvrP?Ovr¾¶Á7=oÕÖÖã+@J‘8?)9Dì}¶žò£lž‰ˆF/"%Ù§$4ñaßšŠôе0Ø9šzsEºpòÞ¶«Jz>æÏŽ¡Ý«8y`÷SÀ®§€R>˜1; úÔ¥@fJS+|Èãìs¦u}Ü»ðÀÝù‹X PhŸ…¦£›`íS¥ä~ >%B9'–e-°@•zçß}?C#„­‹v3 §X„_¸iZEvÐ4N,QMÓž@Ñá\´f›PHÊbwìn™?•< #e”œÓ¿m~úqüêû_€è@Á¡¶ØÅAâ<KÁAú<)8D#Ovq5Oáx¿^éííÙÅŸðç»9™ÏO êgÉ.¸^ê{BÌÖ!%‡ÚKòéÐŽ¨?øÐß@Ùwû¿4^|È/?¼þ80÷‹@ÇŒ`æÊL×}Xs¯÷mǤ‡…ßnð}¬Ê²C½ßÑÄLth›­ë&hmÓjœÞÆ‹'¨µËÃÈQˆc µNAÉOY„ì¶©Rn·eY+lÛ^BH`ðiBÈY±,« €2Î]sû`M¿ŠÁB‰š®ÃL6Õ/=hÌd4]ç¤5Ö¾¦¡9m¢%•€¦SÒ©†¦–Œ„ŽwŽBˆøH‚¾!$à÷œâˆ §xú\›Ÿ~Ï?ôrï¼LA (9x/]$\,u KÉAú<)9¨Ÿ'»8D÷ü$jåIÉAý<)9x(Ÿ–«}T%®Cµ¦Dœõ»íüY0‚ž»è¸¸üŽŠøÐh²=À•_^yÐû¶¹=À+ß®ü"(:Ô9~#»:(ÐÑAën¤èPÍkj¸×›x5PK—‡=kaÌüt,ïrJ-1Úy5šÖ«Ròr˲úmÛä=*!Á@árÎgÝ*šLepÍ÷21B!‘dLz(9E×­iûD² ;;EH6%j5a$(èx¾Ñ7uLhoŽôP.  ¾ÇB‡€SpáÜÓŠVû6þ¿úþ×qh×Ö` ò]t äà½J.–:†¥ä }ž”ÔÏ“’C4ÏMY»v­÷ÌL4ò1Xkìä¿÷ŒØ >›ˆèqIW.%Õ¦DóX`àâÃá­•î ]}Àå·© ;^×ÂÊk‘ö¿îºò ’œOqê¸wÉÛ Ø/Qt¨ö}!; "=~Û[J‡6¥ ÑË{µ¼5MG7A++ÑÍ>  À2Þe !gIJ¬T„%¸âÆO#Ó9™ÁB‰,š®ÃljF¹TB¹TD⃦ÁH$`$LN Qã&5¡#5! 3ip2ê™ÇJn™Â!¤q8ÅÑÓ‹ïìÜ‚ÿøþ}Ø·é7Áã›è@Á¡¶2"ôà\Ô‡ª†V<Ϩ ž‡S0Ϩ 5AÁA¹÷Û µ]ªnžìâ þùÉ.ÊçƒåêE/"×¢JÓ¡ùT`àâþµ•_—ßL½0Ó«QÒE‡*_îóñW»»1Ñá­gƒ;!Û¦Aëò :Ô<_“N¼'L^ñæ¿zÛ¨4÷MЧ\Ëû.a4cxÊ"dö=©JÉwY–µÒ¶íµ¼k&¤ñPx „œ¨ØˆÒ“阄9Kïfb„BbAE`HÀ-—!\®ëB¸å¯ézå—¦ÃH𒟨¦ihɘhISÎñí†?fÒC¹$`61wBˆ¿”ŠÅÑ2N×`+÷Î~<ÿÐØüô¿SŒ/¢%ï%°‹ƒ„‹¥ŽaÙÅAú<ÙÅAý,ÙÅ!šç&Q/OJêçIÉÁCù|°\­èÙÅ!6ëPÈ}\üÑ9‹o<ì|ò¸øpKãÆ©Wzè˜QéJøù%Ôzo ÏHOvrP?OvrP?Ï{7nô¶AÊ’wò)9¨~ žÜÕ•å7-Ñè)8ÄfJ<%š$Å*>ä€çï:®¨ˆ© ü£Vé!?Ð-ˆ³èñŽ5ÍW¢Ã‰íÞ¿±6ù#ž…‡6Aëî‹í=[©e"F;¯FóÀzUJ^aYÖ*Û¶Ai!ïò¬ef+qÛm4á¢~–¡‘ªH™:º³ILjM Û\ýGàÁcsðö1ŽËŸžE!„øAK&‰–T‚]‚ºù7ud²MÊ"}œÅ‚‹BH œKtxgçüÇ÷ïþM¿i|15‰¼—@ÁAÂÅRǰ¤Ï“‚ƒúyRpP?O~µ]û5HÚ’gâ)8¨n²‹ƒ‡òù`¹ZÑ‹ÇÀµ¨Òth¨µÏ‚qíqñáÍ‹‡·O¸üv`êÍ€™öwÿõtzhØâbGÏ» Ktè¾ ZvZóNW‡“hjÒ“á·½íöÀ‹@Œ…È[óÑtt´²ÿ¶Ù `9€¼ƒ&¤qPx „œ„eYmP¨»Ãhç<¼úú\6‹Ù‘3“2uLïlBW¶©¦í'µš˜ÔjbfÙÅΣEì8Z ø@!„ÔzšÐ‘Î6!a꜌€ijI X,£8ÝVnY \0i!Õq.Ñatè]<ÿÐxé‰6¾Ï¢%‡ÚʈÐÃsQ—ªZñ<£.9xNÁ<£.9Ô<%©òU|Îîð´Ël6«ìõßž={ÔË“’ƒúç'%åóÁrµ¢ªäÀu¨Ò”h²çušò´ógÁ8ÿo ìß ¼õ€‘CÿÇO3ÿä=IÁ¯ëê«ü¯/҃ʲC½ßéPtð>ÅtuÕo¨MþÄ›zÛý±#G¡µ´#®£ÃS!³ïIUJ^nYÖJÛ¶÷€Ò(<1:{?ÙWÏöº™^¦9ÃJ|Sí&³é¼û÷`ÿþ\tQ'yÓ;šqyg³/û2 —w6£«-‰—ßÎãp¾Ä &„BªDÓ44§M¤2&'#D2­I¼ë¸(•ÜÈcqÔEK†]!gç\¢l~úqüêû_GaøXc‹IO†6ï+ÀE×WñbJÞK`' Kò“ƒôy²“ƒúY²“C4ò¬Fr3äi÷½½½Ê^Ö$<¤¬àó¤ä þù)$¬IÊ)ò^D4J*M‰Š’ÃiÃúÖ‡à¾õ ÜíÿÒ8ñÁÉW¤„}k€™Ÿ²=þ`Wpx °oí¹7»¨¯ ]<íJéŽÕdP¢ƒ·µöµ uh´˜wy(´ÏBÓÑM0‡ö©Pn•Ë@i‰þ}m­ÛZ–Õ`‰*Ç:g£üSÕÏ̆ äÉ“’ƒúç'%åóÁrµ¢ëP­)r‹› ¸¦Ö/^ýÂEña÷ªÆ‰;Wïöð'@×Bv΀–N`Û¿½ÿï²=•¿Ou‚¢ƒÏǮ݅*:\ZùbVÔ;½²tu¨ÓÚhÿ àUx8vbä(´–vÄ'ÓBûL4ݬJÉ+ôâ;!˲úÜ¥J½C]Ÿ|ߟmÙ²—Â\{aãe‡1º²M8<\f§B!äÔ›MvušTÆDaÄ[Žæ?ò Q‘šÓÃ&$Æ8…Šè Îñv·oão°ú[Ë‘{ç­Æ”ÌÓ?möçNWmÛЙômO”ÂÊP‚y£ä ~ž”ÔÏ“’ƒúyRrð…ÁÁAï×0½þÍ%õÏMJÊçÃåjE/"×¢JÓ!u'ÀÆfúeŸ†vÑÇànâ­_5æXœ<ðʃñáÊ/© 꿾üöŠ@qðEÀ9.kd{€Ió|Z|aÊõ~×3ÑçüBöœSÑáøoµöBà1ï%Ú]0ð+UJ^`%ïÀ ñ „X–µ À5.`š·æŸñï_xa+n¼ñ*†S:R Lko eìk/Lã—;Þe„Bâ}ƒÉ®Ja& $’JÅèJ›…š¦!‘Ô8!1 \ªˆN%§ºŒ ¤«ÃÔ?„6ûó@f2*ÿÊ}É‚CXJ0oÔÏ“‚C4ò’¯³zk‹ºàpŽ]Sp¨ŸZ„-m…¸<(8„2gìâà¡|>X®Vôì⛵HÉ!¤ÓÄÿãÒZ&¸öCÙwë?B¼»«1ÇýÆãÇ»=|è˜áÓaRtì>èÈV`ÿ³!Šˆ†èpb;ј¬Ú?xŽîJ#@¢%ö÷r£óÐ2°z1§B¹Ý–e­°m{ï ñ „Ä˲ڠPw‡á)‹ Œæ3þýk¯íÁܹ—¢³3ËpcÈôŽæÐÆN™:¦w4ãõã ‚BH,iÉ$‘ʰ«ƒjd&$1xx$ÒÇ8š/£¥B"L©(P,¸pËÕýCT ]&^íê¯í8þ2>  «äÐðv΢.9T5´âyF]rð4%ió¤ä ~ž”cppÐûF)+à(9„2o”"p½.Q^ìäP©\‹*M;9Ô9çÏ‚1ÿ{pßzî–JÃþ’ž_LýD¥Ûƒ™éÞ‘ªÞÝ‘­•ŽG^î\9Ut€¨»Bd»:œò—ZûŒšJsßÙ}Ê5¼™0Ôu &ìxD•r—[–ÕoÛö “#Ä(<B–èV¡P'Ó…Bû¬s¾î׿ހ;îXÀdcFG*Žt¸kSÛ“Øq´Çå?.B‰ÑMeBG:Û„„É®*b$t$›(J‘>NJ„D!PéæPtáºÕo÷ÎÎ-øÙ}Ÿm\W‡ôdhðMÀª£û$;9ȶÚ$ËP‚yc'õód'õó ¬³zk‹ºä üøœå÷е°qãFoœú _Ã" äʼ Éê‘v:£ü~#"=‡Ø¬C‰§„‚CcÐ/\}â‡áî^w÷ªÆˆ;ì[[éö0ižÇÃVYv¨÷; ŠÞ§8¢Ã -é¡–.N¦ÅìeHæ¶«Pn@?€eLŽ ð@HŒ9ÞÝa¹*õŽXó«zÝþýØ¿]ÔÉcD˜ÝÆ0 “[MìÍ!„XÀ®Ñ 9}á¨HfYGS åBT¦\(œ¢ëyÛçzÏ?üÆ–Ì@›ý9`úÞ·eÉ àà}X ÒçIÁAý<)8D#OI»8ˆÁ7=oÓÓÓ£äµd-ÝÞ{  ¢pn²‹C®×%Ê+Ÿ]¼>•/3vqé4‘ì¸Ì4ôË> í¢ÁÝò Þù­ÿc8yàw߬«+âÃÙ$P_æˆ]ªÚEÿî'ƒÆÓþAÀ«ðph3È{ OY¤Šðwïò°ÉR?‰7ý¨Ø„ÒShŸ 'S}#Š^ØÊ.1"eê¡wwcj{’Â!„èßH²«C¤0“ ¥’ùcu .Ê%–´Ë—e(;Å‚ ·ìý_¯rïìÇO¿þYÚµµ1NÿtEvH¶z8¨†Î˜o{Ò|/ƒ’ƒÄÕ0´âyF]rð<œ‚yF]r¨kEò¤ä G–ÎçMT6l¨á“DÆÇ(9„2o”<”ÏËÕ‹žb³ÙÉ×Ô§›û–‰0®ú:Ä‘Mp·þ#Ä»»üÄ^<óy`θn¾ÎSœEvtðž‡O÷”aÉ´ìTïe—F Žî€Ö> p“YŒXóÑb?§JÉýú˜!õCá˜bYV/€»”¸í7š·>âivyˆ“ZåùÉÒÙæR¦Ž¼ã2B!‘¤9e"=!ɉˆZ®iC¹B,ŽÕ- ä•Д205†OˆÌ߸@qÔEÉq!jüÇ«íÿù ¬þÖr†ù_àÄ« ]ý ýUPCgË·=ùÛÉ!bᓃúy²“ƒúyRrP?O¡À:kà®5åóT›Z:< mš¼k­î’ØÉ!:ïµõ”ÏËÕŠžb³ÙÉ!¤ÓDÍu¨? ÆüïÁݽ îöG€Ò°¿8yàÅS»=Ptä>I ÑáxìêàÏwgMçéÉÀðÛÞª8ú&…‡qŒt^¦£› s*”»À²¬>Û¶×29BêƒÂ!ñ¥_•BG;çÁMzoDÁ.ñ¡3•®vy „5tCC&Û3ip2"H²É€¦i"ÖŒ—‘054¥ hô‘êü,;N±Ò‘¥~õýûðÒ?lÀ›f¦":L]|ŽƒièLùº7Jad(ÉœQrP?OJêçÉŸ83ÉAøñ9KÉ!jéð ™™hœŸ”"ö>[Où|¨\½èÙÅ!k‘‚¯©}@¿x ô ¡¼õ Þúÿ8©ÛÃÕr½¯‰Ž-ü¬½Ê{¥ýÏÛŒnQt¨c\oûÖÚg@xm¦ÝÄ›º±ù0š‘·æ#³ïIUJ^  ‡ÉR‰!–eõPÂp“Yä­ù5m»ÿvìxÓ¦Mfè§#%׃—š Ç\!„D‡æ”‰TÆ„¦ó©ð¨¢éÌfÅ‘R¬Ž»ä”ße·B¤¸ÿ/ 8QW7‡1rïìÇO¿þYÚµÕÿB§ÚìÏÉÖÓÿ=%É`‡Ú†V<Ϩwqð<œ‚y²‹ƒbYž¦¶8ü$lvqP’š:X®VôQíâÀu¨Ö”h‘œö¬C3cö_@\¸îÖÿñîn‡ëöpÉ'€Ëo;Þí!ÄùWµ«ƒô¢àùKZŠU£µbÿ3Þª:v(‰ÞاÐ> MG7ÁÚ§B¹Ý–e-³m{%“#¤v(<O”ù𞲨®í׬ÙHá!˜†.U=mMüÉׄB¢¦iHg“hjæ­cH6ÅOxNéöÐb@Ó¹ òü+]8E·ìÏ?xnÿÏ_`õ·–£0|ÌßbÓ“ ýÁ¬«Ns %ÿ>×}/’ƒÄy–]¤Ï“]¢‘'%‡ÿŸ½7’ëºÎ<¿—Kí@Š,T €"¤ˆÅæ’- ,8Z²)±Û3#ËV ÇÒtÄXð¸ížp{¬’äîñˆ# Ó^4´ÝrH²EÁ&ÁE‰[ b!A±`-@¡U™Y™ùòÝù£P`¨ªÌ·ßsß÷‹¨ õÞ½çžsß–y¿÷ɯ'Eâñâð€¦9õ¤ÈAÖ=Zèásq¹¬Ò+ƒËÀ¹()Z;9¨cŒËºi Òÿλÿ çè·»l×'Ÿt|ø…Üü¡èó/Fè ÍÑf®îçCìæçií‹€t P›pµ›3x©Åf?î*RµRN©+Ǽ“n²šPË΃Je; oAç;+%Üí¹\nÇÀÀÀ(!žàªBF.—{ À2 ±V;–¢Òy»¯6òùÞ|ó4>ô¡e,¾¡Üܦߥ,K½!„3)tt5#áêï¤ÐԔ웻ªP³md›Shjá¼'$ìãÍ®8°«Á~ÙùÊS_Ç+ÿçÁ¼î “®S„þ¹®"ÅÇ2ŠBËEòëiºÈÁÈ·Î&Lä ‚¸Îj^Oø¶×„‡«s¥Þõ¤ÈÁ°ó¬Ÿð¹¨\^é•¡eà\””K÷zÑÉÁû¸®lšêûR‹?†Ú¡?‡üïÁ†S^ý2pÇÿ8éö Á=º¯þÃtt˜­9„]ê7˜ymÝt7ÔÐ>wQŽf<¤ì"²å!¤k¥þvõßʃ¨¥[Qm^'ÓfÌYÑníE¹{ šGK·À6ý „x‚‚BD.—ë°]J¼EŸîSìÙs+W.Bss–“€Ds‡Ê·`BNSsM°R“‘ ¬”…LSv¥–Ø(T&ØM­id²< §¦P-+ØU'ð5uã—ñÌãÛplÏóÁ6¼àXÿê«@÷ uq À!îzÛ-Úדùõ¤ÀA~-“"pH0t·C¶]¿šRà`à¹ÖOø\Ð+«ôtqHÌ\¤‹CL‡IÂ硚ûž&}Ïÿ uñ0j‡þ( â;ÿ ü ¸ÿ¶žðê¡Bœ *ȹTç9ªZÞÝ œÝ©ÐÁê½Èm˜Aè |ë(tðÀü€[ÁÃ¥ã×ÖTÕ-GÆohÿt­„tñ4ìLª­‹ ,3^„VX¼Mù£°je ánËårO œ!Ä5\‘IH²Ø†Iµ ö”»×Àní ¤­|¾€ýûᡇîâ „B©CSkó:›™ˆ„’M¸àa Ç& 5¤3šZRHg(| ÄÓ±TS¨VjUŽNùÁ³øþŸü†N ¶áu_€µöŠ«ƒæ ®¬ÀÃ0hñœé"‡†º^OÓE®»XOÓEž» ÈA«z*¿×YŠ$pêÔ)÷÷Y׸;è0õ ¤†~Qä`È€ (=1)rˆé0áü—pNí€sì‚ 9ØõÀ/ü;à–û䜛Âtu˜Mèpr'`£;þ{ï‡Õ÷  ¥{†péèP¿ßpÎ3Vç ÷-—F J#°Z»a©š §‘vÜ/òÏØãHC¥½NºEþ3Ý‚‰žûÑ:ð ávbÒáá1>¥âžS@H2Èår}˜<¸iF! w‡)öï?†r¹Ê‰@!„2;l?&˜NÍV(×0Q¨A9Ì! =Ó;@µì 8VCq¬†j9<±Ãà‰7ñwŸß¬ØaÁ°~å{ˆ‚ÍδXÓ~<‡ æüG]f”û¸"JL9«[.êKøvêªDBë{È!à¹,ºÖr†Ø´ 5¤`æh¶±ë¬æõTS?¼?‹àí·h0õ;´8¼uΗŸð…+Šz¤jí< IDATi{íR•A:}ŒKët(XÓ~ÌI;ç¡ïdÛ‘Zõ›Hø/`Í¿-ØaT‹ÀÏþàç T Á¥%øgØÜÏœšåyjz^Ž~xéw'ÿ‘ØÁ꽩 ý°VöF±ÃÔóBhùrùw¯éw-vpÛ¯ ï˜oîòô¼¡†ù;L‘‚BKá]¤«£FœE‹¹pš:¥„»5—Ë­çS:!î¡Ã!É¡BÜйP+HËå*^~ù þõ¿¾—3„?‡«\ G!D;Èd(x˜ »ª`Wmd›RhjIÁbš¹åvÕAµ¢àÔ¢ù²ùð ßÃ3ÿ^°®þMX÷ýa™ 4<+Ð0 z;0]̨§Ò|žEÚÐz*ó,–.èä MÎT×Y:9HæÀîwjËÅ4ý rÜ ý0¢‹ƒ!^zep8%¥Ck• cLÈó²59ÒþK8Ǿ¼ÛÃÙÝÀåS“n}z§Âtt¸þW¼<7ÐÕ!´cßš¿ªð¾»½GŽ#Ó»Ú—Øa:Í¥÷aÛET[z¡¬´è³jañÌ{÷Ÿ¤„»Àf>©â IWܶJˆÕiêÄDÏý¡´ýƧpÏ=«ÐÓÓÉIa:Š Š !„{0̤(v +eÁ²,(Å•23Q­8¨V¤3šZRHg,&…$§¦P³U¤"‡)ö>ý^úfp f;`=üÿ½A¼$"‡HrF‘ƒüz*!s-²îÖS ™k‘wAƒVõT~¯±8˜Äè¨û7—Za (pwjø\X.¯ôÊÐ2p.JJ‰¥{½Tޝ°ÆQ R«~Ö­ƒsðP#‡ƒk8xõËÀÝ[¥›C·ßÏ“B:‡'Þûq´çƒ9…pçè0cŠ(tlÎLýïüÀû¯¸ke䲕KF–©æ‘² ¨¶äPËÎ{†­tÞŽjÇRdÇÏHwS.—Û<00°‹O넸8_1„$‚'¥:¾ôWCmÿG?:€Ozg„Ah)x°k, !„1X–…ùÝ-L¹J:›‚]áýÌ\Ôl…Òx 錅lS ™& H2pj“‡ZÕÓ£Ø3oÃáþ1¸—< 롯M~¾È î‹0º8D’f]:rÙ-]´¯']äד.òëI2»vír¿S‚Š <×ú Ÿ ze•žN‰™‹trˆé0¡“Clsªµéþ λÿ çØ·»LÃÕ"ðú7 G€5[l{<ç¬0](t…ç &V\÷lO V¸€tûÍF™R6šKï¡Z[€jKR).Þ‚ÎwþVJ¸OèãÓ:!CÁ!†“Ëå6±Â¿Ú±ÕŽe¡öqöì0Ž?•+qrÄ…‚›Ûõ¹¤NÐáBˆæw·ÀJq±6ù€”ÅùÐ(5[¡fך2M)d›R°RÌ 1¥®Ìóª‚]u§ùËÄøe¼ôÍ? Tì`ÝûÀŸõ’™@ÇF‘Cd©Ž³]Sä }=)r_OŠ̨'üõ0|Àõ>]]]âîm=9ŽÔ†~X}Ÿ]ìàö™B¹Í™Ë¿{)Á5÷Ô>kù5†çôzçýή{±GN…>岕KHWe®ÁWéLôÜ/%Üe¶ñ©/‡L!FÓ/ãF£…ˆÜ¦Ø³çî¾»óç·q–@~¢†bÕA›o¨>?VeA!„ˆ m~“@H˜Ï9 ¨–TË@*5霑m²JÓùÄOÍV×üèJàb‡Õ¿ ë¾?¬wô?]"I³.¹ìÖ€zšîä`Ì›rìä`œ‹Ã ±™þ6ìº C ¨'Å ‘áEðлƒ2×´:¼éâ`È€ (½2´ œ‹’Rbé^/•€ã+¬q)3ÆeÍ_Žô†?›t{8õß‚ëòoùSÀÝ[l{0I ÓÕA{G\„™/::¸ž/ žû­ùË=õh_>ÌüE¡N¿liNº*%ïzÅÜF4Bª’—î¶\.·.„4pYd 1“\.×I öLôÜ•n‰¼ß;÷âÓŸÞÄÉbgF+XÝÓ{'FÊ,!„íijÎ Û”f"ÈŒ¤¹ ?pp(~ qÎÁÚµ%àK×@ÅÙŽI¡ÃŠOÍðG  8ð]"‹-’n)pоž8ȯ'òëICŒµ4_yáIðеjŽ)¦ôgZž®)r0dÀÂK¯ .碤th-rP :ÆŒxfލ^Ùv¤îúX½¢¶ÿO»LgwOŠîÿ ­Ç{2:Ìò vöÇÀÑJ¢+Q’„W÷KžØáê¿Ï_uù¤«^kùð)(4•ΡÜÞ'ò ^ÌmDÇ™‘j'&_jM§Bêž—!Æ‘Ë庤\¦NscéûìÙa?~žÆŽ”Q­9±Æp&_F±ê°„B´§m^–I ³PÀEøá>9“Îű y墻"c:‘CÍV¨–L&çYq¬†rÉ]M¨Øá—ÿî:±ƒBP‹û¬i?®™1]zÈ™Šj8*¾¼ÕíVx=cImĺêJ`=UÜaG€çæ…ÔSéjHÁÌÑdc×Y¥w=ÕÔ®s-xˆr¹ÊcUGá­ ñ¹+TkÞ¦»!„ýijΠᣠ™›ÎÈP ¨VL¯]”^³©~ îæQÍV¨L8(×0>j£4.Kà0@Å î€õoŸºï€"5S ú. È¡±ºÚ@ÿzš.rpU"¡õŒ5äræ¹ AõÔ&Ìs6G³9>>5æÀîwjËi,r ž9¸Ÿ ze•^ØùŽ"Pä í0Iø<wKp°Ùv¤ø3¤îü_‚ ±Zvÿ‡IÇ7úaŒ~st…J8¹3>¡Ã†~X¹û넬܉\‹C\þÝK ü €_B ÐáJÖ‚¹Ž¢v9ºüf'ÄžÝK1½„Ù#ý „ÌI†) Ä,®¨ý¾$!ÖjÇRT:o5†|¾€={Žàá‡×qòÀ‰‘2udqs{ô—··.ÐÝBˆ ZÚùHæ†NñáÔœšBõŠŽ6±J[Hg&,šo«ódRäP»2gL!P±Ã’Ͱú*ÐÔ DÞNª ÿ£.WP7,®H»6 žJóyiwBë©̳XºTO%`ž…جeB=•€y–`FGGÝßÿu®0¯žŠs¬±ðM=v”Á¥W†–sQR:,ÝT 8¾Â—JÀñå’ÔmŸ‚uÓÔöÿ)P ¦Ñ׿ ‡;þÃSÁå«RÞÝ9)v°‹Ñ•¬¥Ö²OÔ9æ8:xRG4Ú¯Šæøj ›F„W·m¿H·µ WÙ—Ï#3QøÇ¼²‘²‹p2mâÎöÕŽe(w¯AóÈa ánÍårO ìâSz åâøÕknë@G×´wu3A„XI¥-d›ÒL!B¨Ù 5ûD*¤3)¤Ò¸*„ f£\5ÔìÉÿš*J Tì°â‘I±ƒO’!rP‘ìYl‘u-¼žJÈ\‹¬;õTBæZä]Pà U=•ßk,⮟³{÷n÷;e;ä×RqŽ5>ôÊ+=E‰™‹§„"ƒç!EõçÿüåÈ|ø/P;ô ¨ÁÿL£ïüÓ¤èáþ×:C4@èбÖ⇓#t¸ºŸ±C¬Ÿ×5‡¡Ãô_¬ùË¡.¹ûl¼–Fðiû²HÁs‘"x&×}næS÷1N"(VüäL—¶G"zÈOØØ¾ÈÄ'œÂèOŸÄÅsï]#t¸žæ¶Ü´øV,Zy'ZÚÛ™8BHä45óÉ8àT®uK¥-¤ÓÖUD*M'±õ­©É'@Üp=ŠÖ~Öº/xÚ•.ÁîYl‘tKíëIùõ¤‹ƒõ¤ÈÁìs­P<¹;ܼNn=)rp>ôÊ*½2¸ œ‹’Ò¡µÈA%è3â™Ù€zeÛ‘þÅ?†sêŸá¼õD0mžÝ=)z¸ÿ÷L{Àãž¶qxçûÀ{?Ž6g]+'ºV56º:4Я¡Ã m\»½Áƒ}é]4/¹7’©›ré>¡NS'J¹hø‰„p7år¹Íty df¸Ú…³xRJ ÅÜG´Šghh{öÁCÝÅYdù‰Z$¢‡ü„Ÿœ. êðK¥¤2xêÎ{Åü¥†¶/ÇqþØÛ8ìm,½k –ÞµŽI$„DJs+I}jU‡I„såíÿÓ±¬Ä©…º¡Ôusj€ãL ’ÌKßü“@ÄÖC_V|ÒÝ>ž Ùð?ê2ó"ÙEÓN ¨§é"MyÄÅtBÐð§ÀÁPzº8$b.RàÀ{ê¸B‡ÀÂIõ} Öüå¨íÿSÀ.øoÿâàÕ¯ý m‡yB‡jJ¡CýJtt˜£ÁùË]GY+\„²Ë°2Íá_  ÔsZ†«V–î“ú@¹®v!Är¹Üf›DÜDä6ÂiêÔ.®={Ž`ÕªÅèééä„2€üD ?zwÜچΖà/wÇGÊ8*â~Ó“8ãæõzד..Â7ùLZzŠ35N EÏE:9„ŠÕ½™Í‡ÚOÿ¨±wý÷wù4°gJôÐæ?ð‹G€“;ýÑæB‡ž­õ:4t½PÇmµ/Ò-€KaAíòydºo }*§„_CTº…Å[Ðqæ_$„»,—Ë=600ð$!×@Á!æÐ/㢥žû´oçνøÜç>ÆÙdŪƒ½;ŽÝ͸óææ@Üò6 NàBÑf‚Æ”ÐáüÑw`Û•`æhþ^ÿá³X³i ZÚÛ™dBH¸Mi&4 ¯ï”¢zã‡ÓS.× !¦äF¦ Sÿ­ÙüNê³÷é'ü‹²°þÕW%ϺI°‡9ÿ3ºº8Äœ3Šä×S ˜g~b£ÈA~=)r_ÏŠ®¡í£ï9½8±L-Ò¯Íñ|:3¹¤m&1„iNS ÀB‘ëÄ;G_}/}³ß_#ÙX¿ü·@÷7ü‰.ÁîYl‘tk@=M9³ˆ,Á"#c%LäPïí­&Ô3é"‡qêÔ)÷;u­Ô£9¸Ÿ ze•^ZÎCY)Q°ŒL;ç!]â%½ö÷àÜ´Ρíþ«?pzhHôpeg<éèpùt´9Ìm€µì@K÷´s ]ü÷«‡«CCâ8å=n¯‚‡(¨¥[¸šŒ/ýUÌ?þm ¡.Ëårýý|ª'ä(x Ä ¶KÒiêÄDÏýÚǹgϬZµ==œYQuNŒ”qb¤ŒÎ–4nnË «%…¶LmM©kD “î £å.l\(Ú¨:üv(‰ä‡qtïk¡¦SÌ_™·aùº{™tBHxY H}l›îdn¦þ×ê|P?%Œ¸Þbêßgû=´çÑë ÓÅ ÓÇEw†h<ñ&žy|›¿Ff;PäÌæ‘Åi×ÂëI3ê©4Ÿg±uA‘ƒ69£ÈA~My;#^V[.¾B(Mç—v‡ôÊ*½2¸ œ‹’Ò¡µ‹ƒJÐ1ô¸(pÐ$”kK-þ%XónCí§ØM7$z˜&tx矀҅èò˜in^K¡C(ÏÚzº†(ÿq[ >œþWû8…‹pÊcH5Ï ÷t‘2ãåÕŽe¨v,EvüŒ„p·år¹í£|²'äÊ%—) D6¹\î1Ë$ÄZX¼EL^wîÜ‹Ï}îcœ`†’Ÿ¨!?Qc"Èìç«Ñœ<¸ùáÁÈú<ìmÜ´h :{zYBH8tx P­Pð@‚aJ@09±Ü¸ŸJ[°\®^§XAã—ñô—~ å˜÷F¦‰’!r ‹ƒ·®…ד"µ¬›é.ž» ÀA«zÎÑ,BjÉÛߺxrxhXð 4­!E† Ø€Ò+CËÀ¹()–îÒÉÁû¸TŽ/íÙ»!kþr¤?ü_àìÿS¨±wýu5—è¡Z˜ts8»;z¡Ã­›a-~È´Rèè3w2nØ¥yÁäOù’»ÃclÍ! j™6c®4ÅÅ[ÐùÎßJµÀ6ý|²'d®x!D>".jÕŽ¥¨tÞ.&©CC£Ø³çg! îTptï¼þÃg#;LqæÈ!VÊbHýûö E¡DœšBÍv÷C±ƒ žþÒo!?øž¿kÚÃÛa¹;¨ë~fÿGMðWdC‰)guËe@=c ?ÂN]•H×ZÖ‰-ö°#ÀS‚ê©M¨!žÓæhÖšö#¶žjêGøu=²æt­epœ>}ÚÝm½á ”ÃHÉ®ç¬á+Cçi€ãÒ*EJVÍ\…ʹ()ÔÕ­S®Dðñ>3+ƒ/íOÏî²Z"½áÿ‚Õ»Áÿ8¦D—¯ÜßU “n/ý.pôûщ2­@ßÇamø2¬eŸ€uUìP'/SÏ*žoÕëåÝå߽Ίfi#˜—ÖXó–»?4 #pÞ÷À,Ô²]Æ\uìÖ^”»×H w[.—3'ù„ø½3„È%—ËõCˆ»C)·Q\~÷ì9‚%Kz°dI'!†cW*8ü-œ?úl»[ùáAL hiogQ!bY;F¯‰<BÂã‡ý%œ9ôš¿kÚC_Õ{oc‹rqð]̨§Ò|žEÚ´¬§‘oœÕÕÉAÅÒ,„ÔS ˜kâÅÝÁšÑÝAG'º82`JO‡DÌEº8ðž:®„ÐÅAƒph(ÛŽô/þ1j‡¶C{É_[Õ"ðê—Þ_†Lþ-ݰnÝ äÒ­×=OÑÕÁ¿B„*€˜çr[œ¿êÂ~WÍ9Co¢ºúQ4—Þ %{µæ›Œ»sAóÈa ¡vØà1Bèð@ˆT®¨÷¶IˆµÜ½ÕŽe"óüÜsûP.W9á1˜sÇÞÆ¾gwà̑ñЦ¸xþ,‹B œt–~¤ûö ›I „„Æá¾‡}?ø_mX}ÖŠOνь/™ú¦ø6,®H»–àâ |o&ºžF¼)wŽBÅþVQ .š×S«Pãuq°¤ÖS%ÀÉ!´·ð&ËÍ‹àm¹`rJÚéâ wЦ•ž.†|‘Má0ÐÚÅÁWÚyN”—‚ÕÜ ,½vRkXæd—€s{¢;´tÃZýYX|Xü0nö<•Wue®vtÓo˜ÇÐ ŽŽu®+u››ù—À+«û.OÇ„=qåÖ[Ï ce`7uwErš:%½¼yk.—ë!„‚B³ “*>í)æ>"6Éù|;wîål#Ä@ìJG^Ý…wîÓBèpõ¼34ÀâB‰…J™î„p<ñ&~ø×â« kíf;̺Lø¢øX†SÎê¦DÊ"jÝRa§®J$´ž±‡,Aä½ë©U¨ñŠÄÖSKCÀy eZ²×3::ê~§âL;Er¬ *½0áEÞÇ¥y©µ9øÊ]Âωⴙ,P$›ZüKÁˆ¢`ºÐ¡w @¡ƒÛ˜$tP>âvó™YºV›{á‚<„Z¶ åÖ[à4è娕ÖÅPVÚÈçÅRÏ}Péf)áöƒBÁ!‘äîPÊm„ÓÔ):ßÇŸÇ›ožæÄ#Ä0í~#ï¿§]\5›®2„Bâ¡:AÁ!$x&Æ/㙯mC¹0æ¹ kÅ#°Ö}შ+pº(Þç.‘ÅI·¸rÄêâ 4›ÖBë©‹ƒÒ¬ Š´©gf)rPÏÀ§FtõôäœpïÙÞ«AÚ)r0dÀÂK/¬^Fpcº¨h%Gä óó›ŽóP¬‹ƒ2,”xÆ•ZüKHoøÏ@¦]Ïrw­„µþw“+t˜ÚÏ“Bbö_]ü1€yÝX7¡ êuÍ_î¾›¡Ã€Z¶ •ö>ÔRþò—[o“iƒ©¨t Šty D<"“íàî ÒÍ(õÜgDÂô£ÎsæbG÷îA1‰‰ „B®P.ÙPJ1„Ày婯cèäï ôÞ롯 sqðE ÈÁOlºMkŠ´­§qoœ• rˆ®†9©g é÷›ÕµÒõ>R¡9<¨0ë(ðÙ¼!×5Šd”^¨‹ƒ2h\QÕKˆ‹ƒeTÚMv_jp\9hŠ㲺×è'z˜:¬ÿ"еêjЬkr7Wj=Îæþ÷*t «Ã ùS¾Âq[WkÁ]î»;Ø%€“nA¹c9ªÍ7{ʤ åÖ[QËvÁt&zî—ô"ç'AH¡àa\Qëm•rS Ò-Fä½\®bçν(—ùæuB¤“ÄÐé“L!$Qؾ¹ŸÔ¹w/ÙL!$p޾úöýào¼7°àv¤6m"rð°R)²ÅM1®¢+rh0g±¤–Næ,öE†º:9(9õÔ*Ô™cŒ9h^O•‘C %¡žÙcïG½8<̘P#º8Ȱ2¬ôBE¦cAKsý‘Ö.¾¦ω²R Ù¢óýcPÇþüåzˆr`Ýû®:$ÍÕB‡YBõqÍõq€[ó—ÖÛ©‘ã×ü^mîA©c%ìlã úíl'ÊóV¡–‡¤ ÈåaS.—Û B „È£_BNS§¤‚†ÅË/ä $D8G÷¾Æ$B!Ó¨ÙE1„À™¿Œgßæ½lR}hꀌE·áíYlAw«š ”Aãªw.ˆSôÛë/ÃZýY ãV ¼D€ÐaêºÓø°|8(³Ñ¤èÁ%Îà¡{KeQi]„Ò¼ÛQn½v¶µtë5?Õ¦(·Þ‚Ò¼ÛQi]e¥õ Xî^‹jÇR)áöó©$™ S@ˆ$¹;˜&v˜â7NaÉ’|èCË8! ÈÅsgQ.Žkc{׊ 5ÛA:CÍ;™áÞ½@3BHð<ý¥ßB¹0æy롯 n×pd*’]"‹-’nuþr]iœZ¥iwB멄̵ȻTO%d®…Ô¬eB=•¹¦ÍÔ ¸!<9<°Ž „oêüT†–^\ÎEIé°tP%è3âyÙzñžãƒóÃÑCí§Ø…p;Ë´N n}hé¾&}V£ùTÊgy”Ïzô¼¬¼ì¬\ô«¢9€‚:Dr v'*±Üu鈻0.ŸýoVµljÙ.>(Î@)·Ùãß–ê¦\.·y```«F’W»"‹í‚´[¢Ü½ÖØ"ìܹÃÃyÎFBrñüYícì¼¹—…"„„‚ãp±¹‘ší R²™BH ì}ú œ9äÝYÍZûyXKÖd4^¯ÙÛ c|mbÝn%¼õ_i–Úˆ;õìä ¤ž±¿U4‚žÒÔØ9÷T;–IryØÎŠ‘¤BÁ!BÈår›|JB¬ÅÅ[Œ¯ÇŽ{P.óM¸„HcäÜ{ZÇ—É4á¦ÅKX(BH(T+“@n¼w§»!$`òƒgñÊS_÷Þ@ï=°Ö~!æQxøÂ8²ï˜58(Oè_ÏX— pTÏØCÖQàè½æºØ´ 5ZЈÀA@=µ8„³@›Ó¨žmî^ÂrðàAÞøŠ96ý„Ͻ²J/K8E‘ƒqQäÓa’ðy¨¤¥@£`CÉš7­ù·!½á?™¶àšn醵ò×&…}Ÿ>LKåÏ`õ^ޡ܉nh®^Ý\þÝË4Jè ¼Ž!ˆ¹ÿB‡k#Ÿ×bÿ;}ó Ù½ƒ9ÆÇ3Œ/}DJ¨ër¹Üc¬I"<"‡~ AV;–¢Ú±ÌøbäóìØ±‡³’AL °íŠÖ1.ºýе*äº9AwBH<óµßC¹0æmçlR›¾SätqpŸA‹¨µBŒ.AºÔ3I.Ñì}=“äâ09hr]´9 ë©«-gÜ}é®]»<Ýšq®õ>–Ë*½°z!Àé~Cët(9")ÏãºÌCqÃ×Tä´s½ÏqYó–#½f›ÿæÓͰVÖ_nÝL¡…´1ó/îB f£ºù³Üå>¼‘ã Þpš:Qî^#%Ü~VŒ$ Àw‡Mb-å6&¦.gÏãå—ù#B¤P.Žk_&Ó„E+ïd¡!¡Q­Ô˜r Å1º;B‚eïÓOàÌ¡×<ïomþÐ4/ˆ}89D[ÄPä ¿ž9ȯ'EfÔ“"ùõL€ÈÁÛÂ'Ãé\Ù`ælÖðM]Ðà±§]Š„º8(ƒÆU½4N‰uÈÁ¤´›:]Ü'QäÀûÇ8’tÝ?[½ÀZþkþºª•gì#QB‡©ý<)$fÿÕŘ/uc5z~ówh®sŒkþr÷aæsй@¥›%„ºŒ.$‰Pð@ˆ ú%Yî^“w‡éìß o¾yš3”â›U÷=ˆLSA ¥lº<+T+5TÊtw „G~ð,^yêëž÷·Ö~Vï½a_ A'/Ýêú%{ƒ9‹%µº:9(™õŒ=l]ÕS«P#pr¸®Y ˆ4¯§’àä Ó‚f ë9]à ‚‰íÔ©SIø4tq:`eXé…ŠL?Æ‚—æú#1.9xf6ä>Ëôûǰωu†›¾}+¬wûëùø÷ñ÷>R\Óù\;z8ÏýþþîUè@W‡æ™ë³ÿ\oïÅá Ëƒœ¦NLôÜ/%Ü~VŒ$ ÑIîÅÜGY£;÷bx8ÏÉJñÌ¢U«qÓâ%L!$tÊ%.p'€rÆóe&‚(Ï|í÷P.ŒyÛ¹÷Xk¿ÖYž]Bÿ~9¦/²] t^D­[jc8ˆ~S®¥A …ÖÓô7aש!]„Ô3q.sÄ6§³ÁÌè.xصk—ë}¬¶^Pä`Ì€…ÆÂ¾!À©^Z§CÉ9˜||…1.º8ðþ1޹èáÐKoøÏ@‡ïÐíÔÛÿ–]JŽ«…×6ç7nØFãP°æÝæ>ì¡C Þ)õÜG—B4…‚Bôg»„ ËÝkà4u&¶HßùÎ.\¾\äl%„¸fá²åX¾î^&‚ |£?€RцSSL!$0޾úÎzÍÛÎÙ¤üJÀQäà¾[¡‹âc‚Ž.‚ë™$‘ƒ„ó†—Ø’$r˜Š¢¨%E‡ä2¶loj -'/æY‘¾):æÓ‹‘ç”@rÇy()%Ö5"“Ònê7òÇìÏéVÛ-€‡5ytyðO1·QJ¨Ûèò@’„hÊw‡eÚ?Ò¤›Qê¹»ÂÙ³Ãxî¹}L!šÒÙ³Pƒz±~˯ ·o B‰…JÙFÍv˜ˆ2ž¯À©)&‚ƒ'Þľü·³°üŠË(r˜³kïÄEîK¤dÖÓt‘ƒçÃLP=)r ÈAB=)rðG[¦qàÀ×ûX7¯rˆpa¹¼ÃXP½(rEÒÎCYÃ×Ô-ËÄûǰ“ñ­ùË‘þÅÿè½»çÿj†«…×67ó/ŸBtt@ýgu˃˃3xÄåîµty D3(x D_ú%YÌm¤»Ãu¼ñÆ)üüçǘB4dé]ñ}aÕÜÖ;Ü„5›¶ ¥½Å „Ä{W¨2 I«ùx•²ÍDB奿þ’ç}­‡¾4Ík`KŠæì–N2ëéÙÉrêI'ùõ4]äP§†9©'E±âEP@¼ž›Ä½²;ì ª€[`A53Æe,¦ziœŽé³œ8e¥@³` ÇÔy8ÇØ4²Õ½©•ŸñÞÀèq¨Ó;Ý¥aö¨·ÃܧСî5¬ñayŒ[yšm;õÜÞØ.Ö‚»Üï‚ °x‹”Péò@„hˆw§©=÷³`3ð£Ä›ožf"ÑŒ–öv,Zµ:Ò>›Û:°êÞqß'ÅM‹—°„-¨”lT+5&"!”K6Jã&‚(‡_øÎzÍÛν÷ÀZòð,ôðíid_¸ÆôÍ®1¥Yj#ìÔ˜EdsÄ{Ø!`äb¬ëbÓ*Ô‚iPà`I­§ÒQä  ï‚f ë9]à±ÈÁê\ézŸÑÑQÞ‡znâË£lJì³B$¹ã\”Tj3œ‡òÖÖ›(pHÊÚÓÏãçê§cö¨·ÃÜ÷šOxSHÌþ«‹?0Çë¦î5,(¡ƒ d#q¸n·æ¹wx@iª4Âç-ŸT:oGµc©„Péò@„èI¿„ ‹¹¬ÔìܹgÏ3„hÆÒ;×¢­sAèýL:ôö­`â !úÝËÓå! ØUãù2A ”‰ñËx婯{Û9ÛÔƒ_¹îuuqˆqEAÝn…¾õ?Ö!D\Oc‘éêâA=\Œ¥«‹Cˆõ¤‹ƒ¾×Xš£‹Ã\÷hn¡à!Œ©È…å²caõ¢‹Ltq€$)ÏãºÌCqÃ×Tä´s½ßq 9ôÒk¶­ =ïï¼óíÙÓ2÷?øû»W¡C\ºŽ)ŸÉtõ9›F]à%ÝÍ €f÷/ïW#Çøì%9ë#éò@Œ‡‚B4C’»C¹{- V‡;ö`x8ÏD¢™¦&Üq߃ÈdšBi¿³§—BBˆìJ ¥EF׸êàòÈA œ}O?üà{žöµÖ}èX}E@lßêRä ¿ž9ȯ'EfÔ“"ùõ¤È!²í®6?xð oŒ½LCÕÐ t¹>®XçðqEU/!.–Qi7uº¸O¢È÷q$Iâp³íHÿÂyß¿pêôÎ9Q \(t˜%TÂÅ@{—Û*å;åÖ¼åî+1rœÏbPíXF—B4‚B4âŠÊn»„X ‹·°` P.Wñïì¢èÍhïêÆšÍ Tô°pÙr¬Ù´k6m¡Ð"†ÒxvÕa" dJì ”b2!’<‹½O?ámç·ÃZýhø›È7ÑÉ!´œÅ’Z:9š³Øêêä äÔS«PC fŽf-4"rмžJ‚“ƒN š5¬çtƒÒw®Y+]拾˃ëØ\ >¼__’°ˆR…”;Æ&¨ &+Šzi®?ÒÚÅÁ×tâ9QV 4;PL¾ ;I⇫`Í¿ ©Õÿ³÷N?”F(te¾A¡’£ƒËíU0©µºît¿Û% ‚¢(gä¶\.×ÇŠS¡àÍ.:˜TÛiMµc)*·³Z R.W±cÇ”Ë|ƒ2!:ÑÞÕõ[~=½žÛÈdš°pÙrÜûñƒÛï{ÈW[„J)òe(‡‹âM‚bBH˜¼òÔ×Q.ŒyÚ׺÷ß7pqBÄ.1 ”§ â¼s€þ.JƒPOm\”5Ô*1Çfú›°ëÔ.BêIýê™ípÝ´½ouë@áEðÑøTäÂrY‡±°…¯9 ‘ƒ†h-rðu˜$|Š[[¯©ÀA4®(’$^Ó1óR}Ÿ„Õ}·çV£ß¾®ýzý7øw¯yV€7…D£ýªkÔX7‘ T yŒ# gúiÝZóÝ;< 45vÄ?vk/ÊÝk$„Ú  Ÿ#¦BÁ!špÅÝA„­P)·‘sI>_Àw¿»›¢B4£¥½k6mÁª{Ds[ã_èe2MXz×Üû‰Gqû}¡¥½É$„ˆÅ¶ŒU˜SêI±!$ÌgÛÁ³8üÂ?zÚ×Zþ¬Þ{gþc’D:ÅdÎ’$rÐužùˆé"ÀÀÅX 9Ô©!EBêI‘ƒFµœ!6Ãâ?D¸°\Öa,¬^9€"i‡ E²†o¢‹CBç¡p‡FZóE Óæ­‹üq¨ÁŸ6‡‹¿{:áêì"îãÊÕÁO2:q ®’n†Õ–sßÔÈ1>³D1÷)¡n¥Ë1 Ñ1îÕŽe¬–††F)z DSzûVà¾O<Š;Ü„E«VÏèÔÐÙÓ‹E«VãÎ7áGÿ',½k2MML!Ä*%ÅqÞ£H§kTƒÛ†R+'tÂÕ!˜<Æг½š#Žy·¹onä8H08MR\º<CÉ0„ÄÝ’ÔèáÓŸÞ„ææ,BˆfÜ´x nZ¼„‰ „$’Òxé´…æV>&Ь_¡Š":!!ræàk8sè5OûZwþ&Ñ­1}›«4+Èœ)c‹¼+¡õTBæZä]ª§0ÏBlÚ2¡žJÀ<Óª9¥qº”¾óÌ#VÏ:¨·Ý퓇%ñœ£É±§ ïOõ®™æé°tP%è3â™Ùz™~ÿhü³~üõ²z€µpÔÐOÝïlO@yÖòÓ`L9:xRG4Ú¯Š¦f*€ë]ŽÏ5Ûª0…ÿÑš·jÐÝgô<K1÷4–êÖ\.×?00pŠU#&A‡Bô@„»C¹{ Ý€N„BÑ•ñ|å’ÍDB9 cù2Å„Ðy婯{Û±ýXk>öÙ±½º®n·tqоž®º¢‹ƒ–õ¤‹ƒN'½ÀkH!õ¤‹ƒüz*™×»íð0k¹ö¦h#cAõ2Æe,† ¤y:´vqðu˜pʾfõ2ùþ1ì$‰rðÁ§×|È´y‹æÜn ðBwt˜Úb‡ÚÓÑÁ ±X îtß¾]‚; NS§¤—E÷³bÄ4(x $f$¹;saÁ‚¢B!„èÊx¾ŒR÷(°«F/–P¡H…2¾Üîù÷!EÓ7»ª^× ú~ëL‘ƒû ­§é"Ïe¡ÈA›zÖ©!EBêI‘ƒüz^·»Õ¹ÒuÆ fL©Î÷±\PÜ2 ª™1÷¦1ÕKãtL8Xº:ù¾§Nè<•Í‚ ,Sçác?ämŸ=xÄ9ñƒ:ñÏþkã)ð²ø¾Q‘E˜“c†Ï+ê\ÿ»vû¼þûßÈãöÓž÷U0iu‡5ï6÷] ŽRÏ}Péf ¡nÍår}¬1 ‰1îNS'« CC£xùåƒL!„B´£8VÁX¾ å(&C×W‘¿X‚Sc!áãÙÝaá=°–<P1}³kŒÀAi–Ú;5Rà 4 ;‚Œ[Œu]lZ…R0 ,©õTºŠt]Ьa=§ ´9(ÿykd÷¶^WMæóyCï€ùÆrY)¶ ”.Dà ¹ÈAë”+Ñ|ôóPÜBsI™‡Ê !ÇsàX½ÀZ¸ÁÛÎùPƒ?›e3ÿÚx*(t¸ÚÆÌ¿xJKƒÕ赟á”VOqx< ðÈH·`¢ç~)áö³bÄ$(x $Fèî@Þxãž{nA!„í¨”l\™@Ív˜ °«òJ(W˜ BH$ørwXûyŸ½ÇøÍnÝn…¿éÙt×%XOÓ]}Ì—'…Ä쿺øcIm¬wBqîèàv{ÌWÅÀòàð»5vŽIA)ty $2L!ñ ÅÝA¥›éîo¼q CC£øô§7¡¹9+~<ùáAä‡0~éjöµ $›ÛÚÑÞÕ›-AK{;‹O!„è~?¨ŠcTÊ5´ÏkB&KÝ|¤ùwJE…*”RL!$R¢qwˆñܦ4Ž-ˆ¸”ƱÅÒÀz*!s-ò.t¾'R‡ª"oÖ2¡žJÈ\Ófj(S%èÜcÚ­¶œëæ(xà9Á¨ûËÄß›ÆT/ÍÓaé JÀñÖ¸TŽ/íÃ1ù3oeè•ö]§–}êÜ ÆÞußÅùݰH·zŒB‡ÛP!N3ÒÜUÁ¤\PǾ´æõA¹{S#Ç`Í[ÌG§)õ܇æ‘CHUòº‡º5—Ëõ ðሇ‚BâC„»ÃDÏýtwˆˆ¡¡Q|÷»»ÅŠìJç¿…óGßmWæÞøôI¼{pÚ:`ñªÕèí[Á @!„h­¯!±„¦Ö ÚÚ³Hg(| !:àÇÝí‹æ:ËÅtrõ½AœW†@7‹%¶È»ZOÓEF.Æ¢Èášk€ õ¤ÈAþñI‘ƒ÷F;ÝV¿k×.Þ8óœ`ö9%÷§1ÔŒ"‡˜“„ÏC•ãKûP6)pˆ¥ëÔêßFmït¿£=ub¬Û?ã2N nlC¨£C)ÐÑaFæÝ¸<–mæcTGOºÅÜFtœù áöxŒU#Òá Bb@’»C©ç>,B¦DårUTÜgŽľgwàÌ‘ÃõÅÓ(æ/áØ¾×°÷ÙÈrB!¨”lŒ^(a,_†]u˜€©Ù —+¸4\Bi¼B±!$¾ç¼ÀÝb²¸o¨Ûâ .ø˜Rq§ w¥dÖ3ö°#ÀSó‚ê©U¨!3G³Ö´±õTS?B¯‘O ëy5$]çšòŸ·P†vm£–ÁÃxóœøs‚ç”ÀÂ6¶°k¦y:,¨«?fþ Ÿ‡â†¯QÀ¦ß?Fõ¬Ÿô{ŽÈ>C¸îœÞ}7¬EõÖýÐ^`b¤Á8Ý{Ýösîf føLcŽm­Fϵ~â\ìà¡6A¤\ùŒ£Ï&¬y·¹kä8§B Ü½VÊ‹¤·ær¹>VŒH‡‚BâAŒ»ƒJ·°Z#Iô`W*øù‹Ï¸:ÜpXÇáÝ/âäÁ}œ„Bˆ*%ù‹%ä/”P.ÙPæû¡Ûñ8.ÍÓ1]à`–ÈóPÖð5«W`¡˜:çH’ø!Ç|H¹KßùÛ@¦Í[Hgž¯sB\¬Fjwã/îëï#qôÙÀŒÝº:4¶½5Ïý³ìÔØ9à)æ6J µŸÕ"Ò¡àˆ¡»i ¢‡Âèö=»Åü¥ÀÚ<ìmyuìJ…“€B‚m;Ï—12TD~d¥B•Î Rž°1–/cd°ˆñÑ2*e›I!„h?w‡ßA¬åiƒi0¶X† Aà ¨ž±‡-Aà y=µ 5^ƒ%µžJG‘CÀ9 Mà 4K—ÒTä°‹ƒŠçœau®t݃Ž.^úÍ#9·s²¯ù~sGIé#p0Ê-‚y(n¡¹¦eиâxÖ7i\¢ƒi gÚZöIo­ÌæòW«QW‡™ñ”–`òá¡.¡ \4”û1z=¨‘c ÁC—B¢ƒ‚B¢‡î¤!††FñÔS?Äðp^»Ø £#8¼ë‡¾\fcäý÷ptïNB!D v¥†âXù‹%\( ?2Âå ŠãUT+5T+µD;AÔlåÒ¤“ÃU‘Cɦ›!D;ö>ý„§ý¬å¿zÅÝ!"°[÷Ö›Å[d5^ÏØ>D€çæéâ M=çh’.Úœ#œtqˆ¥ž¡ ÍG£7¯s½‹Ž‚Y˜*rÐùšï7w9HJ‡Ö"‡@î©<E _S!©‰÷a'Iüc >ÔÜÍÜpªï µÇ[‹Ó](t˜¥†>î§~S”ÐÁëg£ónsòÈq>z…]‰ ‰º;·äó|ç;»´=„)v˜bäý÷pæÈANB!D8v¥†‰b¥ñ .LàòÈF†Š¸8PÀ¥áIGˆüÈŠãUÇ«(OبVjÆ8DT+5” UŒå˸4\Äè…Æó“N9B´}<‹c{ž÷´¯µöóágŒ“ƒÿÍb‰-’POÓEÆ;9 Y"‡:Nš¬œö"tK•‘ƒÒ$íÁg½8<ìÚµK» uuu¹Ïâ…‰bÑu IDATƒ1Ì¥MSÚ÷¦z×KãtL8˜åäÀy(+šX8¦ÎÃ9Æ&~È pr˜«ñL;R+>ã­‡)— >hÃOÜ®„¥7ŠÀf Ñe>ü¦jæÉႇ° Ë!Ña ‰º;÷7Eå*¾ó]øõ_ߌžžx§Ïà©x÷ÀþPÅSœ9r7-Z‚ö®nNB!Ä@œš‚S«˜FÌúКI) )ËB:;©ÙOg,¤RÖÕ¿[)+¶q(GÁ¶8ŽBÍV¨U85¶í°È„‘¼òÔ×=íš»ƒò½A\Wˆ@7‹%¶È»ÓyÕ€Ò8l¥i‚ꩄ̵šµL¨§2×´™JãT ½Ä–öpòeu®p½Žëׯç9ÁÄ{ËHÃ5õeÊÈR[:¨tŒñÌlH½L¿4þY_p½”~ §êôƒ;å¾×3ÏÃZõë!}æ ¢)„ àª|ƭ¨±r¿­ cºŒ# ûQ˃ÃìÔØ9XóƒO1·gþEB¨ýcňD(x $"¤¸;8M’l–C¹\Å·¾õ">þñûð¡-‹%†ÁS'plßk‘öyòà~¬Ù´…€BI0×Êso›J[H¥?02œM ‘mªov8%`˜N­æÀ©Mþ›3íÿ !Ä&Æ/ãð ÿèiß@Ý(rˆ/¶XºXOŠ„Õr†Ø(r_OŠäŸÚ‹”¦i _m9 ÛT ï’ÏçqêÔ)ôõõɾ!¾xÀçpN0ðœbü½iL5£È!¦Ã„"‡D_Ú‡’°y¨r|‰ë:¸ÆS«µ½ì>‚¡½°–þ2ÐÜí.V \ì²ÐA…5ã:\SÃy}®…›#ÇA£ڱ ÕŽ¥ty $¬k9S@H$ÐÝÊþýÇðÜsûBïçèÞ=±Š`äüYœB!„™(px÷‹xý‡ÏbèôÉų‘ÄÐé“x÷à>ìÛùyu& &Õ‡_øÊ…1×ûY·nnÌÝA]÷ã~ƒi0¶X†a‡®Æ'´ž±‡rçžÇ'¨žZ…R s4k]÷#²žjú®s-®cQH=¯†s¥ŽÚŸ;tI»^¹²:WºÞgttT»{ÜM›6¹¯Äû¯ÆwNrß+òP4®(&æé° ®þhr%ú€~*i)Ð(ØÐîM‡Ê !Ç|à„Öu<ãšryðñ™®‹ö_]ü1€<6ÖMbÄAL)!b°æÝæ~'»5v$ Ïfì2¬8‚;Ьy}î‡:r $<¦\°5—Ëõ±bD<>tw ¡144Šï~w7†‡ó¶«‹ØB!„bv¥‚3GâÀ‹Ï„ö¼ñîÁ}8ºw“í’3_C~ð=÷;.¼Vï=×þ›1Nþ7‹%¶ »RÅdÎL9âä y=M9Ô©!EBêI‘ƒüz†êâ ùê»ê¸÷¹ž]qy°^Ú•^Ð\4Æe,†zižŽé³œ8e¥@³` ÇÔy8ÇØÄ9Ƅڵ~…ñåòpþ'×kÎ1‡Y,ø:Á8:îêàR`iB??<¸<¨‘ã áB—B‚BB„î$ ††Fñïì Dô`W*øyˆ‹¼B—B!„BdsîØÛØ÷ìœ9r¶] ÷éôIœ<¸IwÁÞ§Ÿð´ŸµâƒÊ÷f±ÄyWBë{Ø`Üb¬ëbÓ*Ô‚iPà`I­§ÒUä ë‚f ë9]à ¥ÈAùÏ[(i—¹òNø¾÷¹›7oö´Ÿóú× ò' *½°U tq]¤& Ÿ‡âš›(pHʬ[·ÎýŽÕœWß½èA»Ò š‡9€"i‡ 硬ák*$Mø=‡§$‰rŒÁ‡š;YEñìòP›€yc–ñ‡Y¸Æº©{½ Jè ‚žn…¹:Ä}|(«ÃÃs˜]‚*€„ ]   º;HÙ¹s/^~Ùýº‹šÛ:X\B!„B‘ÄáÝ/â­×v£\%†c{_ƒ]©°u8üü÷<ígÝñ×ý‹”7Åûß,–Ø‚îÊ'jažË"¨ž9Pä ¡ž9ȯ'E³†¯N?uòiOM¬_¿^Ë¡=öØcÞv¬àìúÔ™•žNb>Š„&tr “ƒ§g:93.ч?=»<œ}a†<„Y¼úÝ4tÍU³þâ:Õ(®q (­žëèÚÕ!¬éñAÛÞ\Ž„‹0—‡.VŒˆ¸~3„„C.—{ tw 1°ÿ1ìØ±årµ¡í £#xý‡ÏjíìÐÒÞÎÂB!„"€‰BG^݅û_D~x0ÖXl»‚“÷±(uØûôîwÊvÀZþ´_t«¥È!â/„YD¦«È!‚z¹KW‘CHõ¬Ó,EÚœ#œ9Ä’·À‡&|‘× á«3ÏÃyýkžš[¶l™y‚‡+8¯?uâiK/Tà  ?Æ‚—æé˜.pÐ^ä¤s}ã+pP‚ç]RæáI?dMÊ q…qíê¾ÛÓ"q”/AåC”ÐÁ«ƒëû¶À½v[¿S/(G‡¸]®Æp]Û­9÷M A.ÛX-"  ~ݤ»ƒ¹?~ßýîn\¾\œs»Âèïúalo\m„¶Î,(!„B!8wìmxñŒ¼ÿž61 >‰‰BÅ™…Ã/|å˜ëý¬%›&ÝœøüF7–/Í#îБCƒovŒ]ä [Bê©Õâ•x],½NVNé*r0o¡ ”f©R‹|æ-”´›áâ0ÓBuá€g±<úè£Ú»«« [·nõÕ†óÆ7áüüqJOC>ÑéÐZà`ä=u„ ¡‹ƒá˜:3]b@¨]›}N´–=âm÷÷b®!Hè„çè0õù‚quð‡kG‡°^Š2{»Ö¼ÛÜ7w‰‚‡(äò°.D<WÜ–é'ÝÌfhhßúÖ‹8{vxÆ¿O‰l»¢õ8:º(x „B!DwŽî݃wîÓòùbè4?¸ŸÃÏÏÓ~ÖšßÑd:¿‘2&ÑoÊÕÕÅ!¢XDŸZôQ µÇ}k#oåKÇwãÿÎv-nh¨7þâ)œ; p[\[§ Êî~pW6ÕÀÑ¡Á8<¹•”F J# á#Äå¡ty  àpè×=@º;$ƒr¹Šï~w7Þ|óô5ÿ.Eì7-ZÂBB!„¢1G÷îÁÐé“ÚÆwáÜ{,Ò äÏâÌ¡×Üï¸ðöE1F®«È!â/„YD¦«È!‚zzîBP=µtqP‘5K‘ƒzRä ïu ¶´ _ä5kø³ü¡:çÕÿ¨zwEÛºu+úúú´NK__úûûý§7RуPƒ2ü z\š§cºÀA{‘C’ÎõAŒK¬ÀA žwI™‡s$Iü58(ƒÆEÒ”šsˆ©¥Þ\ÔùŸ˜{çê0ó/®Ãñ¹‘Ç8¦9ƒ”Voq»:q ®’nÕšsßÅÈ1ð¡Ë!ÁAÁ!Cw¢#;wîÅsÏíØ•Š±C&Ó„›Sð@!„Bˆ®œ;ö¶Öb(æ/Á®TX¬ëØûý¿ñ´Ÿµú7bˆ¶/^cùÒ<â94øfÇØEºu!¤žZ-^‰×ÅÁ’ZO%AäÞi(æÆN•2WäJÚÍwq˜ çÕß÷%vèììÄöíÛE¤iÛ¶mØ´i“ÿt‡&z ‹ƒAŸètÐÅÁÐyH ïM‡tqÐÿ0H–‹C=R‹? dÚÜ÷6´7€XœÐÁ«ƒ«{ºÀö™‚;ø‰ÃÕç!!»:¸Íµ—‡±s Ñ@—B‚‚B‚§_÷éîLÞxãžzê‡xó§{Dˆ`Ñíw°p„B!„hJatïÜ'#Öü%ì:¿ð]÷;µßëÖÍD×௱|7K‘C ƒˆ=d]EJN=µ 3^‘ƒØzj)p¸žtq_ÏP†–\‘ÃÎÏ¿•?á+Œ'Ÿ|]]r^¹cǬ[·ÎúϾç§.¦äygà1ô¸(rˆé0á<”5|M…¤I¿çð’$ñCŽ1øPsgø<ôš»L;¬…Üw[›€Úç½â„*„yv@%ÐãÆ‹ÐAÅ{,)ïqXRjä8H4Ðå` à¡»Ñ çÏãµ—^Åøø„ö±6·u`ÑÊ;Y4B!„B4åäÁýL‚P¿ð=” c®÷³–?bTÞJvêj]´ Eñ±×0Â<—EÐBeÓEuj(ÛÅáJl9è{pEâkHøîÇ¥Î<uö_á|ñ‹_Ä£>**…]]]رc:;;ý—ã¿÷˜C:9tð‰N ‡trˆiÞ%q*ÁóN£'Ô® wrPîœæ"µâ×½EâÊåAÍø¿³]£Ý4ç) àmÛÀ<ġ£Ûf½Å1õù”ÕÑç¾Û±s€]‰º<â  íOøtwH6™ê(ÇÁÉ“ïãÂ…¼Ö±._w2MM,!„B!’D~x‰ÊÑWŸó´ŸµüWŽÄ¥‹CdßÍFÜ©1‹Èt9DPO#cé*r©žuš¤ÈAH=)r_ÏÀ‡&|‘׬áû—ÊŸ€óÆ_û më֭ؾ}»È{á¾¾>ìÚµ Ë–ù™óúã ¸d8(ñ Ç¥y:¦ ´9&pHà<-pP…’ÀyHƒ† ˜‡ n¸F¶.„µàCîw¼|(_j0~øwu¸¡¼a¹:x:¸p%¢Žs Á]ÆøÝŒCM{ÆwÇÕÏ©¦þ)Ý4¹1?]¢C’Ë«Et…‚B"—Ëm°N÷8éî\,UC¦úÈáüùœ:5ˆZÍÑ.Ö¥w­ÁM‹—°h„B!„hʹ£o1 Bɞű=Ï»¦¼uоÈgïlDFÄ!rhpáCì"ºPrê©U¨!Ò ‹ƒ%µžJ‚È!šËŠØãsºÀAK‘CÀ¥ù9#öC$¸q©×¿T ž÷ß´iž|òIÑ÷Äëׯǰnÿ¯õœŸ} ¨ŽÏQLÉóÎÀc,èqÑÅ!˜´'í\ĸD _Ó{¬¤ßsxI’ø!Ç|¨¹3|F4ïR˼9Ûªó?©_¿B`tÓ$D¡T0ŽÊç1 …М%f:LŸKóú܇2r $:¤¸<är¹ÇX-¢#<ýºHw‡„Ÿðíâ ÿvùr'N¼‰‰Š6q.\¶KïZÇ‚B!„¢)…FÞOTÌ™l–…»‚Ww,Äc ~+iºÈÁ˜7åêêâQ¾EлžZ…¯ÈAl=éâ ïuÀuH¯±¦"‡FqÞ~ªG‚ÙY·nvìØaÄ}qWWvíÚ…M›6ùk¨4çõÇ!ÚÉÁôc,èqQäÓmAÂç¡h'“nG“0éä ÿa@'‡P®Ÿ 7­=î£yc–q 8¡CdŽ*ÈF¯ÝVG‡0Æèv9:̰­ÕáÞÅN‰A.ý¬Ñ  €+î›t“î ?á×f~cÓÄDÇ¿K—Æcqá²å¸ý¾‡X,B!„B4&?< .æö®nî ûžþ ¼Ö­›]ì «È!â/„YD¦«È!‚zzî‚"mêY§YŠ¢¨iÔçSǧö"‡êª‹ƒÀE^³†î¸TþÔ;OyÞݺuصkºººŒ¹7ž=lݺÕ_nö@|Zè¼3ð z\š§cºÀA{‘C’ÎõAŒK¬ÀA žwI™‡s$Iü58(ƒÆEÒ”Òbˆ©Eu¿SùÒ4уºvhu®ßîÁ”·ô³‘Çí®¨€âeŒnšUîã¾:_ê…vEáÉáá8H´qyXF—¢#< ýºHw’ªMÌ>?gÏãìÙaÔjN,ñQì@!„Bˆ òââí¾åVí ƒ'ÞD~н;‡Õ»CßJÆò¥yÄ!rhðÍŽ±‹tëBH=µZ¼¯‹ƒ¥×ɪñÁ©89„ö^¥Yª”'¥IÚéâà;„7þÊó¾xòÉ';LçÉ'Ÿô-zpÞù{ 8(dÞxŒ…1.º8ðž:Ž„ÐÅAƒpL‡€™. ´®pNÔlx–ÁŒ¼À]üL0W÷aLôiõ üØr+\uGØ®îã¨ïêpm­¦. ©Ó}ˆ=D ]ñ„ø„îÄ$.]ljïcb¢i¿;B!„"‡ñÑK¢â½iñí ûž~ÂÓ~Öò_á_\ÉËw³ºŠ4_D­¥‹CDøvqмžZ…¯ÈA³“UãƒÓÞÅAi65èâKÞBE…ræy¨ ‡<ï¿cǬ_¿Þè{eߢ‡jÎë ;”¹¸\Ìí ÇÅ"wã÷6}M](rpŸ$c\LrrH€³&N³^k[ÂZx¿ûíª¥`„jÆ_Ü¥:ÐóG¡ƒ `Êø‰ãê ‚£ÛqxËu}¡Ãìu´ZsîC9-ty Ä<âíOìtw `Íáð0‰‰ Ž—.GÅ„B!„È¢˜&xXDÁÃG_}Îý³ä­›€öEW~ó°`#"þBØUwB¶jãâ 4ëBP=µtq ÈÁÕàLwq¼9ÄROŠ\„㸪ãpÞyÊóîßøÆ7°yóæDÜ/û=¨‹‡ öhu[gÜ1ô¸4O‡‘CÒÎõ~Ç%nøšŠ’~Ïáv\â×Ò›(pøÿÙ{÷è8®ûÎó[Õ/ ° AŸhJ¤,Q"Ë&%Q‘ÉÒÄt”Î&묭,ÛqfvfœÄ“Éœ33BÎl&NöL Çç˜Ó~)TÅA:‘ƒcUÊp*]ÅAxÈ”2‡¬â áV£Ìª8(ˆ¶ú>À_e=Þ\ò¾žÿG'„#]w¼ÕÏ2B…Úá\ÐlúaÏŽü…Ë·­UǬ›}åHñQ¤ÊC£a-Ì‘?C@HA´Ën «;…ªrYKçŒMbzz›6Õ£¢ÂÙ‡„(v „BJÇÚêÖ­ð#ò!Rqã¶ðÒTã铳HÎä0Bˆ²¬Ûr'ƒp;Õ¨†vÛã7Ùd–dg+qw²~‹+$6YHÜ…"ùŠŒ5—šÕ”ÎåUÛÔkÒ !q¨$kÒ…]ñ‡»„Z~‰3ßµu^$AGGGY®;::ÐÒÒ‚žžë'O@œyÚm¿Ìµ©,×ÉáÉn çÖÔE ˆ(ƒù%½9^‡·ðM”ÑSªk_=枾߷vÒ•3@z ÕæwoŽáƸÅϽpÀÖÉÂfü°èó-'ö™Ð* ÀréüÏËNCLœƒV³¤xÌWyLÊnj;€fŒHqf±«;raff§NÃ¥KIÇÚ¤ØB) w­ªÀãw¬ÀýÃØ -)v€Ua?¶Ô…ðÈæ<´©«ª¨•'„¨G¨ª+×od ÌL^Aï‘—-Ÿ§ml¹ö%­â ŠÛ•ÒoÊU¡Šƒ ‡ŠæSÊ*¢hÍ²Šƒ"ùdõóéŠkŠ¿Éö¦æËí—|ÈLÙ:·³³Ñh´,×ÏÑhˆD"¶Î7ßÿk 3YäiãÅ·Eà—äá¶ŠCÁKã2‡JWrPyÜ•ã8d%ù§+9¨<µõÛkfô¸ûU\«è`¡*¹¿åÞÊ¢Žûhűè³kv,_Õ!Ïv—¸Ôh•†uWF{AŠ"UšYåÈ„ا]vYÝ\3üá‚ÎEÿr9³ v(v „BŠÏª*?~~Ë ÜY_€ÏÚ6pU؇ªqÿ†0ºÆ`Bà÷•°sÓ¶íLÖUz¼dë<íŽÿ£ßÍù aÏ)rû^P’°+þ×MÍWË/óýçl·oß>´´´”õº©© û÷ï·wrf bè—§M•2'\[+½U‘Z¬‰ÝÎd]åïþ¥D¯jï(’…Eþ¶Ô"™|(Bçž9\—4©ž“(mÍÎ!ŸB‘ƒ{—¡7æp¨„¤"QxÜ\ ;«8HåNò4±|^$±ÿ ¿ÇˆÇãØ»w¯­sÍÓ/”ÑÚ´D×Vqp&ìåv­wÂ/¥Ü—tUîk;ARÞåïjì<>=7ÕòwF[÷ˆõÖ§Îé±¥»]úù›íøX´q¬b‡BínùhuÙ³C[Ö4 y¼Åa¶;ç¸,3õ»T0³Ù0Œ³EJ „Ø£ @Df…/„éúÌYÀôWÁDáoe6MgΜÇÈȘ¥ó(v „BŠOC$ˆ{×U9Ö^U@ÇC›Â=RæDêå~ÛŒßÄGv>ÀD]%92„ gNZ>OÛü¸Ë–•¨ŠƒÒoÊ•µŠC‘ °Õ…Bù”ÊÌÒŠ¤¸nØqŽUä½X6IÁû@IÃN‘ƒ´® ¾lë¼öövD£Q.¤¯²ÿ~D"6¾œ¹y•Š@‘ƒjË‚2‡JWrðÒr´Æ¡ðËUrð’_ÅØâJ^ž_ËÝÓWÛ{YŒž¸±ûÿaÍÉUÎ IDATü²y¼Cã¡àªBŽª×¼ÁšËWu°(º\æP;0= 1=Ê ` H×퀌¨`j;³EJ „Xäju‡6Ù휩ßá«`ÂÈ5ä‚Î}I122ŽS§Îav6»ì±;B!ÅgmuÀQ±Ã<ßœè! k 2!eÊÊu¥¶osÓÇŽÖ1QW9uø%[çi·ý¢Ã–ù aÏ)r°`¾Ú~‰äiˆK=þ$[n£¡¡mmm ‹ÅlÇDœ~~.ÉÓà–hý(u8„:"™÷o2ŽCåÜ—TäPîk«~)?õ$8Ï/7ü^«äàŒ3Zåj[¢qñÍ" W…ÎTt(Hè9„óvØp|y¡ƒ……"cMš¡!$•¢÷‚’„]ñÏ"„Z~‰K=×ü ¸Ü}íA™©¹‡è¦½½ è%hkkCGG¬åòʈ#ÿæÆûáŠÛ€@õÜ?V6Ð"·jh•k€ª5‰œ(É©n£É~Mô䚺He2¿¤6ÇËߺ,<Ú}\E̯Bïù«wA\ø™5K¦Îé1 uÉ ábl„3áØ!Ãç¼Âfü°èó/'ö¢Â^´JbÒÚþç¸ù+麨JüúlRvSÛÄ™1R*(x Ä:Ò_´YÝÜtY« ­„ž¾äX›¦ibxx““3ظ±>߇Ń(v „x MÓà Ì]çü~ÚÕ7Üûüô«×u >q ©å²&LS\½ ä²sÏeL˜BP QÆì0*ð¹;×Ö°¶:€ó“œ2¤aÛ¼ûú!iìñûƒ¸ëÁfDê×09‹9}Μ´¾æùÈç Ùyy£+±mNØåuƒí.(p*Ÿ8¨ŸO Ôϧ|ŒIR9üÉÓ@*±ð'¦¯þ=Sº D"´¶¶r½ÑhíííøÂ¾àLþ¯œùð—.=2+WC«2æÄ•k>D¬Ü¡êäSú’C‘ƒgnЇ€"ÏŒEŠ$íÚã×DQ&óËAôÕ÷ÁÄ7¬[6zÚÚŸsÁ—Å­´¨(v°kÇÄ~fªÕ4jqìöqóWBRÆC¨ü¾ìf¶†M$ãÌ)<bÃ0âd¶‘ÕÈrdBõгSðå¦m÷Ê•Þ}w›6ÕcÅŠ*Š!J¢û4è>ºOƒÏ§ÃçÓæ~WDƒ|~ùÔŽ˜F˜9\N 3›LlÖdÒ=HU@ǦH¨(}ÝU¢à2eåúˆÔ¯AòâHÉm©[»wìÜ 0ÈÄ\‡­ê´ ÍV?(žS¬â [‹Ø)tÈ‘Ó'ðì¿ügÖ÷Š÷=í¶Ç—øŸ"¯>ŽX,†d2©ŽÑ0´·ÏUƒXÕmÅm@ ºx×Vq(Ñ%»Ì«8(áQsÊPlC‘ƒ¤]³’Ça]e§=øë–›Ö¿y•ÇÅ6„®Tu°Ø¨rŒ‹B«:ìvésÔÜÀ‹£=ÖÆîퟂ¾e7%¤îØ…–KËn擉Db?³EŠ +<’?qÙ L×m§Øä¿ôÕ|H‡c¤/"¾Tp{&4d*×#˜{h29…^ÅîÝÛ°{÷6œâ:‹Å  ¿_‡¦k ŒMB`®âE¨rn»Í˜HOg1›ÎRü 9U½¨bˆ†| :W âÔÿ˜û]åê«â‡ÛU;æþtrŽQäP¢Ëu™‹XÅëDzÝë+œ/Vq°ï—(“ù%Cwþ0´zU.¾u£àA†ª­ªXÕ¡@¡ƒ{U—_£Õ4X<ˆ±>nüJÌLý.T&~,»™m(x E‡‚BòÀ0ŒͲۙ2>ÁdËdBõÈ¢NŸ³]í!ˆ S±B»ñÇ#GN¢¯o{öìdµBˆchšvUØà£¸¡›†€ ˆ0‚ÈÌæ03Åìt–‘b‹`U˜ÛJBÊ5±ÛáÑûÆëÈfgoŸB{ôyÉúkC3¬…:ÁJŠår Û(rP?Ÿ9¨??)rðØu¶óÝõkAØp©ÇóâbŸ¶¶6µK1}bèU|X­O[¹ãjˆs÷Ð%‡UäÈÖOÜüx6 ®\Ì]´Ä& ‰»P$Ÿ8(œË«¶Qà þܤÀÁc×ÙBÍwɯTbNØ< \¾úg‰ðû¥|ijjBCC<åׇU ®® VnV6Þ\A‘×Ô¥ E˜C‘ƒ§ç˜R]SäÀqXœ®ôÕ÷Á<ñ ëfŒž„Vÿ1‡¥ÐÁ¾öcí˜ÐÁõÇ\{¬Œ¾`ñÁy1Ú­n 7¥ºZú*T©òРƒ#Å„‚B–Á0ŒXݔˢI Sa Sa@33ÐÌ |¹©kŽÉùÂz`Y‘ÃR9rCC±gÏN¬XQÅ€_G´Ò‡ß½fffð·G/¡o*À ²e^à¼*nðùù·lhº†ªê*«ü>HD„ŽÖáÞÇ~çzßÃЉ£¶«=PèP8Ç^þŽõ“Õs\Ùl:~`)vÌ›Í*ÛF‘ƒúù¤ÈAýù)½ÈAHvŠòî*y¸ÔÍê 7¡µµ•A°@[[ž|òIOû(..»Q±ÒFˆ"!µÈA”ìd(áQ¿”3¥ Ç¡(ƒù¥\÷e0E™Ì/Õºó‡¡ÕÛ¨ò0vbÁƒä–lÃb£BÈ‘DáVE·×4…ÇO«ŽA$ß·ÖëÄ<”…ª<´&‰NfŒ YžvÙ duâÊzÿª¨Áô;+Lºˆ^Ńný÷ne 1»¨ªÃƒµÓÇ¥@=CÊMÓæ*7uC> TÊÝUáC¨Â‡ÔD³é,ƒRBR“A „”œõ[ïÄú­wb¤ÿ4.ŸÂèùå+ TEj]½ë¶Ü…Šp˜A,Þ#/Y¿§;)vðD‡el’ÚUò.(r&fË4K‘ƒ"ù¤ÈAý|Rä`Á|gý—z .õ—»!.åñD"´··3ˆÇãØ¿¿çª<ÜrNÍ €¹‡þ®V~ÐÖìªJ'V§ÈÁ37ÅݧÈÁùbI»fŽC††€Vw·uÁÕ3ïf5+]ºe…ÇúVëåê낇Ñ> ¡¥lö:z6=7 9èÙ©þßô‡!àƒé«püº›æ@*m(x Eƒ‚BnÁÕêûd·3]·ƒÉ"J‘NgðÚk=èífµ‡ELeL<ÿî|zsÐÀåY"¼’Á!Þ\ˆúu*üu‚>Dq|~5µ!dfý˜L¦aæXí¡T÷B‘…5±Û±&v; yq3S“H§&?¼wB¨ŽÖ"©…?dÀbäô $G>°~↖Â:¦È¡´¶•´ ФÊ'+9¨ŸOŠÔϧx|IN d ¹¹û÷ïG,c0,FÑÙÙ‰––$“Éò @v "ñ:DâuàÄŸ•«¡­l„f<0Wý!PíZךì×DO ‡‹áQ¿”2ÇËŸéSä V÷¬äÀq(Cw×v¤ÕïÞÖZ¹™¹*µwÛ4¾UŠ*tp1¡Â~¬—;¨!tXð§Ò°nÅhŸç·4¾Ì|™qèÙ)èËÄÒ—›^ø» ¦?Œ\ Š\ ÆUgêv¨ xh6 £%‘Htñ“R (x äÖ´Én`¦z2Õ ÌQV{¸‘·ÓÈårØ|"‡àñï!¹åQT¬ÚÀà噯â¬ð!òAÓ5Ń‚>DWV"5™ÁL*À™RTx¸4Ū„å‰Ô¯A¤~ Q޽ü7ðj{ Áþ ŽPµ¶åFz&‹©ä,¸ç(.?¿eªzÑúëMãØÈ4O!’ðÌ÷Y®ð mþh÷?µü¬âPZÛJÚÁ©\ ÍØ måŽ9D¾{™¯‰¢d'«_õK9SÊpŠ2˜_ÊuÏ*‡2tg­#óô·až±ø²™P¾¦ë‚=²Tt€Ba3~(µÐÁ…ªK|&“ëûkˆIk{ýžÏA_Ÿg®bznéaøÌ´ãm›š™ŠzäQçížM¢öä7UñæD"ÑÏOˆÛPð@ÈM0 £ÀS2Û˜©Þ„+[ž`²ˆg…¬ö°ˆ*3…ìO¿£o¿‰äÔ îøµ‹ÚMw00Dzü~þ ¡J?üE|èšÈI.kbr›1ÿ3ñ–¬ÓÖí‚oûç½± I"˜q½·„ÕƒßS¡ÊÃD"ç.Ÿ¸>ŸBnŠôá™ú]Ìñét¯½ÖƒÞÞaV{Ò« íø%èÇC»r¯ý—߯C_þ:Vß~7 ‘ݧ!T@¨ÂŸŸ"ò!>¿Žu˜œ˜Åì4Š/}£³E<¤2&Å„"ƒ=G¬Ÿ¨¾Qìà‰J8Hk›vQà p.¯ÚFƒúsSzƒ4ì9Üz“™X9ˆÄx•H$‚¦¦¦kD ‹«1òaqEŽ¥˜¯ ÑÕÕµP9¢»»ÉdRM‡³S‰×!¯=€¶rû\õ‡5Uk)rP?Ÿ9X0ß¿Dò4ÄàËž¬â°X؋Ů9R‹ÅýýýèîîÆ¡C‡”öI[q´A[ó´R‰DÉNVÿ¾Ê*\?–í^_á|±Šƒ}¿D™Ì/¥»s¯£Ü‰ÿb¸ËÚ:§vô;~½e©è‹"+:¸¹þΟïç4¹dýË¡öò«€¿RÙ+\1ÅK†ÝW‰L…ÓWøó]+úþ“ƒ²‡ü‰D;wìÄÕÏ(x äF ÃèÐ ³“›Gºn“E<ÏÆõ¬ö`úâ^ýê¿Â{Ç{Ðúÿw<¸‡ƒ£ ¹|n—‡‡¼xéÔd^焪ªQ#R¿áHV®ßXøRÓP ²ÊMטb‰ôt“I> ï6«ªüx¨¡ÚÕ>R¯™@Æäž’Bd 92„gž¸ßúÚîç¾vc…‡%a%Gm+y¬ä MÌ(rP?Ÿ9¨ŸOŠ,˜ï€_™É¹ çC$™)O¬Å 6R±XÑÕÕ¥n5ˆÊÕÐÝsˆ·q=-ë=•"®K$Vq´kVqà8”ah§3qágÈõü±åó|÷ý‘ ;e:ÈRÑÁfüà`E[î•Pè°ˆÜû 1=bmÜ~ô7¡­Þ®ä•ΗGhú¼¶d‚µÈ†ê!4û/ L`Eß·d{@ŒUˆ›Pð@Èu†Ñ à™mduRn°ÚÃÕ H&3?;ˆÛï{ºßÏQ&dgg1Ü÷.†O½lvÖ‘6ëÖnÀú;îB¤ÞÚ›²(t NAÑCqxds5"îÝ/ÞNa 9Ë@Bˆ$¼ñü_âGÏ´[>OÿåÁš%þ‡•·­¤Í³’ƒTq£ÈAý|Rä ~>…ÄãKªp:äWfrNàpþ0Dâˆòë®æææÄ „ÈÊ|%ˆyDOOZ¸!~ ÈÁ¾_£~)eŽ—Ÿï¡ÈA­®)rà8”ah” QÙ)dþŸ–OÓïøuhµwYóM¸"ÅöEމ\¯®kÕk™ç^yñgÖÆmC3ô;Y¹+žž›AÅÔY©l25?f+×ÃôÛÙ¯"U¾H$:¸'nÁ§% ¹‘6Ù L1K¤¬H§3xíµôöã‘GšP_)Ë8ø!l}ðSeÄåsC8ÓóVÞÕòeôü=ÿ"õk°iÛŽe…:§ UÎmC(zp—óYWS“A&„‰8öòw,Ÿ£­o^$v ÀÁqÛJÚRå“õóIƒúùd æ;äW*±PÅA\:ªl˜(n ª³Ô¸íêêZ¨ÑÕÕ%wˆé 0Ïvg;í‹ º¬•¹ÈU¸~,Û½¾Âùe0¿ÜòK”ÉüRº; ’äC«½bì„5˯œÎSð KUX¨XÕÁÍ5‘U®¡rõðö)y Ì$¤³IYT¤®\‹\À^5È™ú]*Útp'NÜ‚Y„aMÞ‘z[â alÛ— |L)[vïކݻ·1ÄÓœéyý說u[ïÄm_òÿ‚•~T×)t ®0=•Aj‚Übmu÷o »Öþ ï²%!„ÈBrdω» ÈAš˜-Ó,EŠä“"õóI‘ƒó9 ½‘<­\xÐÒÒ²ð€xKK š¤,˜>ÌÿH-€˜g^ü°¹ZÕ‡/k9xÒ/åL)³qÈ*’vÍ*‡2 ùe~æûÏZ:G«Z }ûo»?.=SÑÁ†Ýó±vrÿë¡Ã|ZgÇ‘{÷–Oõÿüÿ«Ô•PϦP‘ÚÆBDµ'¿ }Vúý«<×`…B®Eúê3õ»(v eÏ‘#'Ñ×7Œ={v–mµâ]²³³x÷õCH^)ZŸÃ½ïaüÂv4?08·ôi¨Ž„ú˜â•á²Y³ÓYÃ.¥Ü‹ëù‰ L!1Øóº­ó´õÍ’yÂJRÚå”C¬ä ~>)rP~Räà±ël!æ;è—Â"‡ùê ---hiiA4!åȼȧ­mîkR%‹*?h+n›«ú°æh6Þ\[öåB hu[”¹*Ò¤·14}iÀ–è!e<„êÁïËîb¬ò@ܺ³Â!s†pVê­ «;r¬ö@¼DvvG½ŠTr¬$ýWEjqïc¿€Êê ªªL)ci̦)zpƒ­«Ä¦HÈñvßNa Éê„" ß}ê7Ð{äeKçhë?í¡¯•Ør ¤µÍ »(pP8—WmPc¬I34(p(IÜXÅÁ‚ùú•™œ9œy^‘C$¹FÜÀê „äOww7:;;ÑÕÕ…C‡Im«¶â6hƒ¾á1 öîµÞ ¿(rÀŠ<=ǔꚕ8ej%*{ð×lÊÒ9úm¿­þÞk}n…έª²>´…B‡ü»Éõ"yÊÚ¸½ýSзìQb^jf•“}Ê\Gfª`ú«,Ÿ§H•‡‡‰DwÓÄiXᑾºÃl䊹Ž#GNâĉ|êSÇÆõ Qš3=o–LìÓWÆ1ôîhz¸…É E¥:Ä•QÙ¬É`8ÌÀxÆqÁC*cRì@!13yŲØ°¡TÕÊXäà9öQä ~>)rP~RäàÁkm!æ» r8"qDúD"‘kÄ MMM\8b“ù 0>>¾Pù¡³³r]¯œ8ùç0Oþù\Åãèkv_?”¹ÈAxÔ/åL)Ãq(Ê`~)×=«8pÊÐÚIÒVï‚î²¼N™<8$Æ)ªÐÁÅœ vÏçÁÉ=±p{ —VìZ8fYð ÆÔøg/+u ¦‘®Ù ¡ù,7]¿ás?”ݽ8€.âôý— Èè‘Ùαm_‚Œ0a„Ü„}l+vïÞ†Pˆo¦'êq®÷=œíy³dýë> MÓp{S¶Ü{/“BŠJ6câÊè ¸?qáþèp•‡Ÿ Máüd†%„I8öÊwðƒ¯=i}ý÷‹@xm‘¬d%)írÊ!ŠÔÏ'EêÏOм“KGÌwÖ¯E‡Ì”Ô!innFKK Z[[)p ¤Hô÷÷/TxñÅ¥µSÛð(ô«o@‘ƒZ¦PäÀq(C÷9pÊÐw’d„yâÖN Eákü7.…QE¡ƒ}Á€cB*:\s˜pijÍ ¦G;õ߬µí¯„ÿ“_Ub^†&ÏÀg¦•º–dÌV®³¶·ÉÍ öä7¡å¤÷us"‘èç®™éæ\n IDAT8 +<2G’‹ÒuÛ)v dÞz«}}ìö@”cfj C'Ž–nAðÁÐþ}º»kP³r%“CŠ8u„W1™L3s41ƒUUT-šçvL¦)v „Éì±ñ†åèÖ"ˆd}ðAHÜERÅŒ"õóI‘ƒúù’1iBê¼_"ybðå9‘CjDÚ466.ZZZ¸0$¤Äb1´µµ¡­­ ÐÙÙ¹ €©úƒøà‡È}ðCÀ†¾ñQhƒ¶â6/Ü¿Ý šãå—úº,<ÚµÇE¢Læ—òCCxrjÑmÖÏKé1 Të`(-ÆWˆÒçÎÕŠÚ.ƒŠ7įr à V”ÏNCLœƒV³^êi©‰œrbðg’È„ê!ôü_ì+|˜©ß…ÊÄew¯sÏäâà\çT aýd¶1ù‘/"[¹†É"$Oî¹'†‡ndµ¢§Þ8‚ gJÒw èƒÏãCе†]Ÿþ4“CŠÎÄX³é,á0‘ ÚFÀg_ôœÉâµ³“ &!„HÆÓ­w"=5aéí£OBûȯ9l «8Hi—S±Šƒúùdõç'ÞÉ¥#æ»àWfrNä0ô Dò´”áhhh@KKË‚È!r1HˆÄtww/ zzz¤³O[qÛ\å‡ °º÷VVqàú±TAb »gŽCº‚7ú•ûéïALô[jJ¿íƒ¶ê^‡ÂªbUvϯݜÜ'—¡ØaaÜžþkˆÉAkãöÎÏ@oh‘zšêÙ*RJ^bìVy¨;öuÙ]Kˆ%‰qcûx H¹cFÀ³2Û˜©Þ„+[ž`²±H(Àž=;±eË:ƒHËÌÔÞü‡J± D°Â]¿ù£;;?ýiÔ“DŠŠ0Æ/OÃÌqŸâ4…ˆ“i¼5<Í Bˆdœ:üžoÿ¢åóô_ìt¨ÂEÒÚæ„]9(œË«¶Qä þÜŠŒ5éÂN‘ƒ¥îξZÍሔáhnn^¨àÐÔÔÄ !ŠÒßß¿PùáÅ_”Î>mÍýÐ7<Íx@û*+9H`+9xh¡¥x׬äÀq(ÃÐ(¡Ã<æ©gaþÀÚš£ö.è[Ÿ(0¬* ì :¸þ«v—¦Ú­0ÿsÄZemõvø>ú›ROWzÁôTeºz‹¥*P=ø=„FÉîÚ$‰vcs! DþÒ9ÓÆCÌ!6H§3èì<‚-[ÖaÏž¬ö@¤d¸ïÝôº¼ØŽ§àtêª#!\a0&9“ÃË}ØaT`S$”×9©Œ‰·†S¸”bÕ B‘‘Þ#/Y?)ºµ@±C‹<'pXÂ6ŠÔÏ'EêÏOŠ>¾PùAñƒù r#?*WÏ 6> ­r\÷T $0‡-´ïž•8eè®ü×£ÕÞ X<ˆ+g ¯Å˜ Qú|ºZÑÁBÛe\Ñá†qn`Mð &ÎI?}5䔾üø2Ȇê,“2>¡‚à! ;bâØ\g…RΆÑà Ì6f+W#ù‘ßd²)P(€܆{ïÝÊ`©xãï;‘NMu«—ØažO>ñüÁ EŠÎÄX³i>dïU·×…P_åC¤âZü¥©,ÆÓ9 &g‘œÉ1X„"1ÏÈ ~—A\>óÄ_@ßø(´Í­T} ÈA-SÊàíùžr—%óEƒ"Cƒ‡[âC«‰ALô[;ïÊYàf‚á‘œÉ v%~B6»´ŠÕ3¬õ0Ú mý}ÜÀ¸„/gï{û™ú*Út0ËÄ‘[/C@ÊÃ0böÉl£Œ ]·ƒÉ"ÄA††.âÀWYíHÁäøX{³.v€Ñáa HIÐt á¦'g B!d ޽üë'…  öŽ›ü'ERÚå”C9¨ŸOŠÔŸŸÒ‹„¤a§ÈÁv×çCœù®TÕš››l¦È²RвS0Ͼœ}Úšû¡ox šñ€s÷0Џ~,U„GýRºû2‡¢Læ—ÒÝ AwüÒjï¶,xg »ó»ÅJB‚—EU‡G[ß|Ýod}ðU±MHjW‘š¥ÈA‘œRä ~>…ÄãKªpɯÌä‡ÕR#R„dïÞ½ 0G£Q.â!ys½ø¡££/¾øbi/ó#?Anä'@åj胾¹„­_ë•ºÝ šãåÏœYÉA­®YÉãP†¡A‘ƒ]´Ú»ÁX3íÊÙeBï–ÐÁŘvÏÇ0/Ód;XµC¸8%퉀9Á.½aͦ‰sÜ ¸}=9[çÍÔï’]ðÌ=§ÛÅ,“B¡à”%†aDÄ¥^{ûB¬î@ˆË\¸0Ž^ÅîÝÛ°{÷6„x–@П_·uî•ÑQ””ªê &“i‚BYDrdΜ´~âê{!ïƒBÒæ)p*n¬â ~>)pP?Ÿ¬â`Áüâù%.õ@ ¾ 1ôŠ!¡Èâ4ó×”ñññ…ª%?L_€Ùû-˜½ß‚¶á“Ð7·B[q›‡nu¬âà™|±Šƒ„ݳŠÇ¡ ÝQààZíÝÖOÊÍ@L‡Vµ¶€¼PèP¸{ „ñűC o²nÚÄ9 ; ø+¹9q ÝœA5–Ï›Ü3>›”Ù½½†aĉD?3M ‚R®ÄHýJ÷™ú]¾ fŠ"päÈIôõ ãᇱqc=B¼µ)òë¶Å„È@¨ÒÔä,«<B!‹8uø%ë'ª¡m¸®Âƒ×E¶» ÈAª|Rä ~>)rP?Ÿ9X0¿¸~‰Á—!Î<‘<]ò466"#Sä@qh4ºp­™?tttàСC¥»%|ð#ä>ø´•Û¡mxú†G½Ý±’ƒg$¬ä i׬äÀq(ÃРÐÁqüah51ˆ‰~kç¥Î×(v°m·p{X§§ƒ¢ _Œ£}ÐVo—òRó…À¥²Ý'¥Œ‡P=ø}ÙÍl»úCˆmøô)W¤¿xΰº!EåÂ…q|ûÛ‡pð`Òé B¼±ÐÓ5\îõ©ª2„BÈ"z¿lùœ±ƒXôS\6@Øí¢äÉß6©Luɘ[4«-úQ6ŸbþGÖq&$æsÁ$YÇšùtÜ5!ùÜ´k~ üÊLÂ|ï9ä^ý<Ìw¾VR±Ccc#ž~úiœ={ÝÝÝhkk£ØR4æÅ]]]8{ö,ž~úi466–îVqùÌž§‘}í 0O} ˜Rõ榸9Н9ìø¦¼Ë%tÀÕ®½:ÅûZááù¥ü÷à8œß‹ÏÿHà—*bâìu9ÊÓyKŸ'¹Ÿk>±fÇÂgz7=ÝÂxµýù¯ƒÇ/âÀç/ŽØ‘¿-¶ª<ŒöÊ{iÐe½7J×í€ð…d73n?4"Á'àHÙaF@ƒÜ7¡í0ƒ&‹ðÖ[½xî¹bhè"ƒAGƒ?胦i QžP¥ºc™B€™É+<úºõëï-Ñ÷Eøbµ`ƒä*KeªKÆÜ¢Y ùˆ$ϧPAä ÓÐ0Ÿ‹RŠ8 ǘTS¤D~¥0ßþäþ¾âýç€ÔHIÂÒÐЀ/ùËxçwD±XŒ‹4BHI‰ÅbhkkCww7Þyç|ùË_FCC‰¾ž¾³÷o=øäŽ~bzD¢HyQàà5‡ Qà ápñ¸À8¨q™¢À¡Ø<\9cÍ„óvبêpÍçz…VL´åžÃB‡kš”¥ªC¾™;N«¶!x˜8'ï¥BÀÔüe½'š©ß%»‰qî^I!Pð@Êé/œ)ãÌ!%$™œbµRœÕ|½áZÛþ€]/üñ@oÖ'rª 0„B€Þ#/Ù:OÛPÌÏŠðŪí.XÅAš|²Šƒúùd‰Œ”)ì98nÒ¥˜‡¿‚Ü«O@ ½R’°D"ìÛ·/¼ðúûû±ÿ~455qaF‘’¦¦&ìß¿ýýý8xð öíÛ‡H¤/šËNA|ð#äþroý'ˆËǼµž/ÑòÅ3k;ARÞåïjì<>=7ÕŠèLQcçÁqh¹Š@é°#xÀì8Ë#óB[vˆ+:ˆìvý³`«v”¨ªÃ5º&+škÓÂÖÅÁb´Oê¹húÃJ_ör¾Â쟮ߩ‚›mܱ’Bð3¤œ0 £ @³Ì6fª7±º!’ðÖ[½èëÆÃ7bË–u qœPUµ+íêºÀ]kÝÚµL‘‚Ê*?¦'gB!eÏ©ÃÖÚúO—-’6/ó—¢BbSEћռO¡ÈX“fh‰C¥ÐµCа+þ`Ó/1ø2Ä™ç!’§KfCss3âñ8Z[[F¹#„(GKK ZZZ0>>ŽÎÎNtttàСCÅ¿¦ü¹‘Ÿ@«ÛmãÐ7<ªöþŒëÇâIxÔ/¥»/ƒq(Êd~)ÝðèTÔ/ZM b¢ßšËg¡…joòŸBŽñ „í±éhEWçˆU;„KÓVXøoQpçZ0#ÀlÒZK£}Ðê¶H9sþø3I(‹VØ3>ÂWtÝv„FÉìeƒañD"ÑÁÝ*±+…×*9Ñ™¢ÎYŽÃù½¸Pß/;UÄÄÙ¥ƒ"ƒØaás«ã7Vu(ÄnÕÅyMÛ<Æ¿(Ä¿›¯U¬¶±ží•væ5055Ÿ'0¡ÁôUÜNÊø„ îÆ¹C%v¡à” †aÄì“úæŒ SÝÀd"!}}Ãø‹¿ø{ôõ 3ÄQV®ßàh{þ€º®9ÒVMm-jV®d’ˆ4C>BHYsêðKHOMX>O[ïT±Ë"|±ê9‘Ãuß|KõðŠK†ä)pÐTͧPAäàì°•¨1‡C%9ÈvÅ®¹©ù’ø•™„ùÞssB‡ãÏ©‘¢›°wï^¼ð èïïÇþýû‹Å¸ø"„x’X,†öövô÷÷ã…^ÀÞ½{‹oÄô…9áÃ+ÿ;ÌÞo™)µïÍ®­½Ä-‚äC‰E^ò«[,rðòüRjÜ•Á8\¼Þš_vH¿68–>kr)~×¼šù „KîÙ³{ù)hs º¦[«6,¬fãE1Ö'õ\ÌTÔ+y 1ýagÚ F0¹Cvw› ÃhâΔ؂RNÄe70Åê„HM:aµâ8kns¬-MÓàók޵×p÷ÝL‘ŠP…ºOc !„”-½G^²~Rt+^[@¯²ŠdþÒWÖ*.ãh™E{¬I34XÅ¡$qsÅ5Š\'•X:ˆ÷Ÿ³ùÀ«}ðôÓOãìÙ³èììDkk+\„²¢µµ¥«ú‚Ùû7sò>x±ŠƒÖvüb »/ƒqÈ* tÇ*ªŽE­v›õ3S 7#‡Ð°‘£Õnü•}»]:äÑA¡ŸåuÚ2,tÈïx-¼Éº{£r r¨’UrÇÚš©ß©‚ËmÜ;Pð@Ê ©/”f0‚tÝf‰˜¯öpâăA &­CU¤Ö‘¶üšæ\u‡õwÜÁé†ü !„²åÔaë‚ëÕŠðà¹í.zPYÊ*¢hͪ]Åáªm9”²1‡CE‘ƒT÷€’\ò$ô+•€ùöŸ ÷ê%:ìÝ»D?ÚÚÚXÍRö”¼êÃuÂ1="ç=ÌËUÀÜ’ò.KRÅ"k~ ŠÔ¸LQäà‰§? Øx³¼¸rÆbßn¸4Ÿ+«1ȧªƒE¡ƒ­ÏŠ:~á¿EaãP›…Ýœ[ŸoZåÀ²îæÄ9©gélåz¥®*&4˜~ç™êd+WËîö>Ã0¢Ü‰«Pð@ÊÃ0â"2ÛH±!j‘Ngðÿð¾ýíC¸r%Å€‚hØVø=`®ºƒsK»;x€‰!R¬ð1„BÊ’‘Ó'žš°¾NÜð‰<Žb‡Â’™ZÚ*šªù²ŠŒ›£)0Ÿ‹B‘k‡ag‡¢²Xè0ôJQ»nhhÀSO=µPÍ¡¥¥… ,BY‚’V}¸*|Èü äŽ~}‘ðAý¥hy„‡\.ñz¿(áÍq¸Xäàåù¥Ô¸+ƒq¸x/.ʬbÏ"´Ú»­·œ:o¡7\6bqÝg€âÖǺãžp.w× |!E!6\ÕÁZÅëîŽöJ=cM2¡UÊ\aÌÀ ÍÙg fêw©à:«<ë×,!£@ }ëçæÞÀ«ÜrÔËŸá º,<Úµðö8œÿcú0}aîïcÇo~Zåjh«çþ”ò УC^xtz|~YÀî‚yò›Öîó51èùbñÇŒ°÷0½–—iÂ¥» pâs%á€-Âe?o5nG~ s䟬ÛÕÛáûèoJ?‹ƒÓÃðg’ÒÛ9]½B8ÞníÉoBŸ•ÚÿD"ãn“XºþðA!âu ÃhpPfÓuÛ1¹é™,BgãÆzìÙ³+VT1Ä23SSè~õÈfg­/è4 ¡J¿#vÔÔÖb÷g>Ä©™Kc6e !„”Ïh5›½=)ppÀ% n¸GÏ·0}ÙÃÿÚòù¾ÿ§âË9”Aì@¡ƒ›ãCL"wæ[ï•ðò«JÌlÙE™Ð*dBõ®´]•ø1*?–=E_H$Üi’¼ï¹<¯cF€}2Û8¶íK0ƒ&‹Àj¤¦ÆGq¬ë‡–Eþ€þ@áÕüv·¶¢²¦†É R“žÎb2™f !„” É‘!<ó„õ \ÚÃß„¶ú^wE;©HPàpÍòB>ÝÖù\|HöAÌN—ÞYôûîª2 ……¿£j-´èíТ[çþ-ÕРÀ¡$qc æ+âW‰„ hkkC<G4å"ŠB\b||û÷ïGGGŠÚ·#¯¯ÝöKxÔ/¥»÷þ8© cÇ!.ütNäM¹Ó¥¿ úºG ­ÄEñƒðè°§À¡\®‰·ú¼,ÓµÏòüÔ?ò‹æ›G…®†¬ªÐAeÌ.×TöØY>Ý·û÷¡Õ¬Wb¦ûÓ£¦G¤³+§‡®¾Í½}Cnµ'¿ -'õs=‰D¢‰;L’÷¸¦àxÃ0bÎÊlãläLlþ&‹ÁjÄ.vD¡J?4M+¨_ €]Ÿþ4jV®dˆôä²&Æ/M3„Bʆ7žÿKüè™vk'ª¡ÿò«îä¹J9,†"‡eZï…þ'`øÇɾ „¡ÕZ}´uå/€ ÈAýùI‘ƒóò«DB‡ææfÄãqÄãq.œ!¤Èttt ££‡*j¿zì— oý<—`©ÀJZh)Þµ·…"uaNà0üÄDÑ­Ðjï†~û¯A«»G½|±’ƒ.±’Ã5ó!Ïãr=óâÖîé÷@[ó€$¹´"t.†œbW¯5×5•;ó-ˆ©AkãöžÏA_Ÿ23_ÏÍ 0“€/'Ç3&4̆c0}®öS=ø=„FÉžž‡‰Dw–$¯û1ĈÑà)™m¼²åóÈT70Y„xV{ v™Åûo¼ŽTrlù™_G0è+¨¿ŠêjÜûÉORì@”bt$îe!„” ó»¿‚Á£¯[:G‹}Ú}ÿÁ9#XÉ¡t¶¡YŠ–!3 ³÷A üJ¸ëJÕh낾õWo?Pä þü¤ÈÁ‚ùŠùU"¡Ã¾}ûÐÖÖ†¦&¾ BJMWW:::pàÀâuêCß¼zlïÒÂVr(Ì/Vr°û2‡0‡_ƒ8÷ÄØ )¬Ójï†~çmT|`%µ† +9Ü0öm4mý¹SÖú‰Þ }Ëç\pÛºhÀ1‘ƒí°{Tè Œpq*ÌýÂù'˜þÉÚ¸]· ¾íŸWîj gS¤/”\ø®Ü€\ Æ}g“¨=ùMÙÓr ‘HĹ£$y]{øñ2†aŒˆÈjŸŒ`lÛ—˜(B<«=;dgg1øîQ ÷¾wËãA|~Ýv?ë¶lÁ]÷ß0È ¥HŽÎ ;›c !„xž™É+Øÿ™»,Ÿ§ýÜC[ÿ û{®ŠÃ¶ Ií*R³9äA*óä_A ¼TµÈh[~e®òC ZÞ±æˆI9xã:[ˆùŠú•™„yúyˆ3ß2SEé2‰ £­­ ±XŒ‹%B‘Œþþ~´··£³³Éd²8.>øÃ5Ê*Zh)ÞµÇEóL_€øÌá×€lJJ‹õMCßòkË\gXÅA­!È*‹Ñh^Œ@öívk£ðíøŠƒî—¸¢ƒ­¸ ®¢ƒU[Ü:,üëÊ)äž·ÖfeüŸxJÙ«„žMÁ?{þìdÑûNW®E.-Z5gÿÁä)ÙS²9‘Hôs7I–½WSð@¼ŠaqÏÊlãä¦Ç‘®ÛÁdR„BìÙ³[¶¬c0ˆ%’G0xò(’G–ZÊ¡¢Êo«Ýu[¶`ËG?ŠÊš™(Ij2ƒéÉY‚‡ÉÎÎ"yqSÉQd3dggQ#©ÃÊõ BJÀ©Ã/áùö/Z>OÿåW­? í9‘«8\»{ð@>‹eZfrNèÐ÷wr¸CÛò«ÐöÜXõAµ\Ràà±ël¡æ+ìW „ B‡h4 B!r3>>Žýû÷cÿþýÅ>lûçÐ×?Zžk;¾Qä i×å!r1zæà÷ .üL ´šô{~çºj9¨5)r¸a\;Ü|æG¿jÙßö¯¡öyž`@srï^” Á:æÇMÚÎÍ {r¿õqû‰§ UÖ)}åÐÌ é‹ðgÜ_¯›Ð©\_”Ê‹ L`Eß·dOÅ$‰vî"ɲs–‚âU ÃèÐ(íRÛÂèöße¢)3¶lY‡={v" 0ÄÉ‹#é?Ñs ›{È[÷ë}y_S[‹ºµk~XѨNz:‹Édš Ä!f¦¦0x²ÎÜô¿?ˆuw|›¶52`„‘|­ Ç^ù_–ÎÑÖÚÏýq~勨bB‘Ã5cÁ ù,²ybøÇ0ßüÏE{€Ù*Zç ßõ…[(r(Éü¤ÈÁ‚ùê'%_†yüÏŠ*thooG<çˆBd||hooÇÀÀ@q:­\ }ëçò>”ჯ£~)Ý}ŒÃë?®=óôßBŒPÏ-|÷ü6´Õ÷ylØSàP.×DÍåáÖÛBN IDAT}û)ˆ±“–ÎÑcŸ¶ê£6ÃàVE  Øá‡H.v¸J®÷¯ f.X·÷|úúûÚíŽÏ.ýŸž«â°„m^9,Ó$Eö1{þTžªËÍó-¿2'|Tƒ"‡ÍO¡ÀõBŠpzç;(qþðœÐ!5R”þÑÖÖF¡!„xˆŽŽŽ¢ ´šÍзý huÛ=yoÎ{ÝÁ*’v]>U®ù_•…סßó[Ð×=¢øÐ È¡®‡Z‘‡CîÔ³0‡þÞšk€¾qÅpPèpã!:X:Ìcžÿ!ÌKoZ·ëvÁ·ýóžºÊh"ÿìèœðAd nÏ„†\h%2¡ú’ú=ŠêÁïËþ/$‰îÉ­ç(ă†Ñ`ŸÌ6Žnÿ D‰T{„9`µR(+0ƒ@Êa Œ^H1„È©7ŽÜ²ªÃÍ èâÂ3OÜoù<ýñçðÚE7N[w[™W›Ê*¶œ+µy™I˜‡~"Ù§ÖE"†¶åW¡ß—hèSààkm!æ{ë{'q©âý—Ž¥¿ææf´··£¥¥…‹!Bñ(èèèÀ¡C‡ŠÒŸVwôOB«\í±H²ŠƒZÝ—_‡kŽš8 ó½ÿî ¡Ãb=°ŠC.±ŠÃ ÷¾óâÏ;ú5köVзýk—r®ªØB‡Â|±Ö¶¸r ¹ç­Ûšõðíþ}Ï^}|™qøgÇáËM[¿h~ä‚QdƒušO êŽýWh¹´Ì!?”H$Z¸[$·\{2Äk†ƒäb‡tÝvŠ!èëÆsÏýCC bÐÇ ²FÓ59×ûž-±¤’cx÷õC "!.sêðKÖO sb,aë¤"pCB&S]2æÍj‹~Šn—SΉùŸ›’J¨)v€ÌÄ»ȽôYˆ‹Ý%ú²Ž5Qø„ƒ{Ng’7 ¦Qô@ˆí=ÉÔ†NöÐVòâFúO3˜„¸È`ÏëÖïëš øÂI懕A‘(rpœÌ$rªŠ“ùã6˜o}ÈLièK>Ö }³EyšïA‘d&`¾w¹WŸ€Hq½; !¤|iii)®ðáÜíú"ÌÞ¿QLøp‹}«ÒË‘ïÇ]ëZÀ›ëÄE> ‘—{æÀ÷ýÇ1|ÐÓ×2óø7 FK¶5òà8œß‹[~è]ùç-Éëó²bZE½õ¤Îß:÷6>Y^èXªè`ëå:ÖíίIVu¸1dÂzó§øB@`…uFûàuL_f+×a¦f+R+îš4T5Ìý„7ÏýnÅH‡cÈ„êaJú왺*„»»Dr+(x ^$.³q™êMÈV®a–!×ðÖ[½øö·áâÅ$ƒAòÂççƒÞ„øÜÎb—Á“=ÈfghçƒIˆ›sµÇÆC–«ïÍã T–MäàÒ7£·hVE®Ú•™€yèw€TÂ3× 1ðÒ\µ‡áriø+tí(òéE»f”ü’çý‡×ÄéïÎ Þÿk×{¥ÐBÈd§`öýOdÿÌs?RdAâ¥åˆ$á!¿Š4!¬½ü|ô8r¯? óý¿²©²¸Žåº¿ 1}¡Ä[#/ P&‡ü}Ëëó²’Þ+æ:Õj·Y?s¢‰_Úx[ÿ¢X]÷«›ÊR³ókÝîå›dU‡›^c¡%>¯ÕÂ6ª<Œö–ÝÚÝôUÀôWÍýH*nXÒî`éºí²›¹Ï0Œ(wˆäføâ% ÈhÙÆ´j9BH ¸pa¼ŠÝ»·a÷îm ¹õ"ÎϽ !„ØgôÜÎìoR“˜E8ZÇ â0ƒ=¯#=5aù<í¦‚Y¿›)ŠÞ¬VjÛœpNÚï߯5Ì|óԯ찙)˜?ù÷ÐÖ>ýãÿ7¨. T2?L!$œÞŠ?|"<êWž‹K=0ßùq½çææf´··Sä@!äæ…]]]hooÇ¡C‡ÜëlúÌ£û!>ø!ô­Ÿƒ&ÅÃO£ËáÑ®=¾N´ã^v fßßÂü~I,D"hjjBSS¢ÑèŸ󌣻»ãããèêêBOOsgS0»¿ ß_/òЂ屳‚&õp¸±c­&œ·xŸ8 àá«M Û6ä'tp+eÂùãYÑáMPÑa©yÞ1~ÜZ‹ePáÁKÌÔïBhTú—ÙÅìg¶ÈRPð@¼F\fãæ”r<BnÍ‘#'14t{öìÄŠU ¹q£©iðQð@!Ä&S㣎Tw˜çòð„¸€­êõ]ôp³B*SäP:»œrN‘Ã<æÉg¯‚póšŒ;ûI¾žŸ?ŒÜKŸ…~ÿB«o²*¡T.K?½)rPÞáÔÌc‘8âzï:BÉ—b Äèqä~úï ­ÿ$|wýs –c§ôrDx´û2X#ŠçÒ‰?¦/Íò††´´´ µµMMMˆÅbËžÓÚÚºð÷ññqtvv¢³³/¾øbᑜè‡Ù÷mè·Ö»óË5—¼:¿ìû¦I?nݹ½Ûz‹©„ñ`Eè`¡máv®­Ú!\~Š ²ãêñyœb«ÂÃÄ9 ; ø+¹ˆW€lådª7!09(³™m àÜì:%„`ˆ'0 £ À;2Û8m<„”ñ“EÉ‹P(€GiÂÝw70äüA"u ){’£3ÈÎæB,2Ò½o¾îX{‘ú5ØÞüKˆÃüÍïþ Z›«ÚÝ_„v÷%õHV‘ƒ(I³9”.Ÿb¼望'‘H---hii¹æMšKÑÝÝþþ~tww£««ËÝ7ö.owš߿E¸XÉ¡ä׌’‡³ ßš™„yúyˆ÷ÿÚu+(t „R(]]]hkks_HëCýô­Ÿ+þý™U$íšUòÁ|ï¿­ªCCCZ[[ÇÑÔÔäX»ýýýhkk+\øà¯‚ïþ¯C«\Íq¸¬K¬â°Múá`½ãÌ¡8MY:G¿ë_A«2,Ù¤9¹ïň «:æ‡;b‡y²ï?d®XêÁ·ó·¡Õmá¢]B£GQ]¢jTøL"‘èd¶È ë ˆW0 £À>i—ô¾ƶ} ÂÇT !ÖØ²eöìÙ‰P(À`@°ÒšHˆ e„ØcðdO:W®”‚BÜá«­·|ŽÖò h«ï•ÄVq¸&7¥¶Í ç«âp3Ìü2ÄÅî‚{mnnF<G<·ÝÆøø8ºººÐÙÙ‰¸%mU#ôûÿðj ¤¸f”<¤åûp¸Ôó¯©W-illÄþýû)t „âhooÇÀÀ€»U®†oG´ºíîÞŸ)r´kŠònqâ,Ìã 1Ñïªå‘H­­­hkksTä°]]]hmmE2™´¿ÿ\÷0|wÿ6Çá’.QäpÃx‘z8Öqöívˆ±“–ÎÑ7~ Úêò¶ËsU(tpÈŽ«Ç ëæ>ø{ˆñãÖÆíퟂ¾eë Q{ò›Ðg“2›x(‘H´0Sä†ë C@¼€aQ­2Û8¹ƒbBˆ-úú†ñÜs?ÄÐÐEƒ|>.á!„B¼Ì`*,j ÄbÑÏÿ”ʶ"4«-úɯÙ¾ô€˜ÿ|¶Ùˆ™ø‡‚Å8x𠺺º ;@4Ekk+:::066†gŸ} îTy—z{ù³É^ ¯zÎW¦¬ó²Pó÷«Ð|e&aû3˜‡ÏU±CCCž}öYtwwSì@!ÄQâñ8úûû]]7¦/ ÷Ó‡ÜÛÿÄôgïÏÊ/GJ耫]{|8¿¯uÐ=sø5äÞø÷®Šæ×•ýýýèèèp]ì---èïïGcc£ý¨´yíðà8øð³•r݇]‡†ÄÅðÏcÞyÑk;Ÿï¹³ÚVzØèZ¨›ŒTý¢»Øát:]TSÄ|è´a¼‘œt¾Ÿj‰ ˆ¼‰Ç'°wo|ƒ ƒ ‚ ¡˜¥B Î öÔýŽÔ´¹DÞŠ*r0PHÀUä ¢ŠøÔ§öÆî‚¼èîî6ìs}}ý̶ÞÞ^tttð/ÆÌ´þlð9ó×'‰t¸_æ"‡é§ß~ ÙП€Ûg˜G³¤*ˆ"‚ ˆÅðz½‡Ãèî÷9»ü ²/~Úé¶Æ3õr¤Ä"uÃ’f°æ:qVž 9Ô dÿ-´ãßÔICrÑÑÑ}ûöͬ+ëëë‹ZŠõõõ…B…‰Ržíp¶ÀD3è8˜Xä0yÅFýž,(¢Ò#ttEt`ù”•ÞrÍÕ¤QB‡l—­Ðáæ²‘jVëw{„f#ѸŠnú¨¦ˆ›æV*Â"=ÀejWCs(TKAÌÁƒo`ïÞ>\»6I…QÆT8h GAˆƒª¦©‚3yExh,ft3Dr06˳!‘ƒ9ê“E~š÷ ··~¿¿(¹v»Ý…B† ´Ã_…öfÀ|õɽ™üpÍ-Ý/ƒÃkzò•‡öJ7´C~â:(Š2#ˆ"¡AQ,êëëá÷û‰DÐÝÝm\Bê´3ÿ5ô)°‘c¹Íͦ^Ž$p`ÊW1 l¶ÀÁ ,²±©¨—z ±ßÑÑ1QÐãñ”|Œ)Dô ]z¡<ÚaY rÏ[NQ„8—¸TçÒÿR:d“ó|½Q®óþèùʱò*3=Ïçj²À 1 õ›å[0F ò±Í¸Õ©´¬ °UêωÌ5ºÛ–!Õ°It7½N§³žj‹˜ –#LÓéthÙÇdãýTQApãÂ…+سçç8sæAešÎR!Dį Q!„à Õ/xšŒ<ÌûP¨ß§ rf³9ýh+ôølîñŒä°)íÍü¢;(Š‚P(dHT‡¥˜>ìÞ½­­|?²“{ ~Rìú4¤Q‹d8··ß~ ÙŸ,zÐ0·mÛ†H$¿ß_ô›w ‚ ¸!|@WW—q %†}åKȾþ߀̸å–Y%uÞв³â Ÿ0íÒ Sb‡o_/Œ¶¶¶¡C)öž‹/¡P(¿H2ê$ØÐ+Öm‡ÓB+÷/#¢8”´9”`¢²×@ªÕÿ=gjœ¡¨…%“£ÐA”¨yÛÎçûîÒiäåá4-ÌM† γ*ª)b6$x LÓétèÙÇTýTQAÆáç±wo®\‰SaQV0Q!A–d°_ÿ-ÔRÓf^3,Ä9ˆÅA¨BZØ7«Gqȱ ØÛ/æeº³³^¯WˆR›¾¹—÷66ÜÏAô šÈÁ䇼né~^ãÙÅè¨Ó·ïƒA¸\.Z¸AÂÑÞÞŽP(„}ûöq6ƒ: íÍï#{èK`×ΙtÝam­øûRùb¬dYd‰!d_Þmð'\í*Š‚îîn„Ãaaöœ‹áñxòÚ‡²¡Ch‚å pÈ=o9/Bà FIuyìQk xDåâ‡yÌ;/Å:@_,ß$x0#&8×Úæt:ÝTSÄ4$x ÌŽð»¿Dãª%‚ eh(†gŸ áĉóTA” ªªQ!„@$'&¨‚—ÏžÐÿRã}y¦6ïG@¡~5Š îa6÷¶AaEMåhŽ]: ;EQò¾ÅÒH¦°íܹŠ¢ð©øY¢‡ CÆŠâ`‘ ço*=íqQÌpû.AAÌÆãñ ‰p]7Þ4 Gö Ú™4,ª’©×‰†&]F"‡Ry2ô ²/o‹pµÛÑÑp8 ¿ßoªHa~¿_ÿKêäÌMõæj‚Åa>9}/+ã(‹–]^"9îϱhò*K=Ïçú+¬Š¹ùþyÌ;/LÿËFÖé¬G¥eM€­2¶{‘á&#Õp/4‡"º›^ª)b<fGè-Õ° ̶Œj‰ ŒÞ$rjÊEh*ƒŸþôU<÷ÜkH¥2Ô(‚°P‡Ž‡Eâ` 3‹D¾U]Tçú,älý•p^‡¯|>ŸÐ‡O|>B¡ÚÚÚøÔÖ’¢Ñ¢8$r°L† 6Å¢/!û c¢:˜íö]‚ ‚Xh݉D°mÛ6ÃÒÐÎü#ÔƒÛÀFŽ ¶æ°’È¡ ¢} r˜iÓgŸE6ü$Àñ7hEQ°oß>„B!SF s»ÝyEá-1¦ 29Ì#§H%–Ì3JË{5‡¾£#Ϻ‹&¡ƒÞ¨T3?ôúbö¨ù æ¶Ý&ýÙ9M‹ob‚(]N§³žjŠHð@˜§ÓéÐ*²ÉÆû©¢¨ ,‡#úK,;»•çŸÇ²³ûà¸tr*^Öårüx{÷öáÊ•85‚ ,M–!5Ê *‚àÀå³'šÓýžÔ´T„QEý2º„Yý"Ñ ‘ƒ.3ÃGt¿£( |>ŸðcF{{;Âá0·Ãk3¢‡É(ŸJ ‘ƒ÷Iä—©Œ±Q:;;Myû.AA̧¾¾===@GG‡1‰$†=ôeho~¯Ñ80 å«…ƘxYT'}õÏ¡ÝËÕlWW"‘<©k./ÿE‹ð0Óæ¬*pÈoìÈIàPòaɼc¢œO”‡Dt^Þu‘îrÕ[¹š4Jèƒm:ü¨T½ZvGÎÐÂÛ„$Å<嘞S©#ô@–©] µjÕA1y%†áxëÈoßüç—”}ć¡¡ž}6„3g.Qc±(šF½ "“ÎR!DžÔÔó'ØíØ*X‚à±–?{BÿK›oñfˆäP<³$r(F}2¡Lͺ¢_ðàñxLu¸¹§§ûö탢~œÅÏ"û§Á⧨ÃùM¸¾¥ûepCoÁj¥ÅMÕ¡­­ ½½½ƒ¦¼}— ‚ n…ËåB(¾}ûòº©=´ó?†Ú÷)°Ë¿4Ï>D mmYaÍ9õßþlô7›ÓQ€%´ù„‰ð0-r°¼Ð<¿HbKÙ73@ªß˜gß1*¢ƒÞ—ôú‘o˜VäÑåhƒ„‹>¾ÔøPs{s) ̈æPjØ$º›>ª) ÁaRœN§ @‡È>¦Ì¡~#Ó!iT ^d%–=F‹èT*ƒ`ð z{û©ÑX¬J‚‚Èf4*‚Èe%_avCË;©P ‚ƒýúmÞˆî0ïG@¡~,~$‡Ün¦ù‡ÓÙÑDÅ;ÐÌËëÿF`Æ[7=Âá0ÚÚÚ 7–™€v`;/Q÷¦(ɰ1¦ Œê ( vî܉p8 ·ÛM ‚ ²L¯»»»¹ˆfoBDöÈGöõ¿â<_S$S®EŒä0íì³È¾ö€ã¥z–ˆê0›ööv6A«Fr(Là n$kægCªué73>{Zy9ÈñYÃ…€qs tú¯Ã%©F„¨ °±‹´Ø6!&8çÚêt:ÝTS ³"´j‹Ù*Ið@aysÉrb˜ ê:‡ŸÆÓOÿ©T† ƒ ËU50F‚ȗÛZnGeu-7{«\ë¨P ‚—ÏäáAä(¬hf)ŠƒIêÓˆ(‹Ë〕Y¤LߨÛÙÙY¸±ÅD†t#9X$Æš22ªCgg'Âá0|>º0Ž ‚(êëëá÷û‡ù¬š»‡^)0ÚC‰EêE8X4ÚcæÈž:lø¯¡ÝËÕìÎ; -Õaþ¸¡»UpŒ˜‘[d$r˜‡n‰8 é7þPZ¡?²)°tlñtór–ã³7î7Â¥Ú+$œÅyW3V¼’gVó‹òp„ùÈÔ¶B­jÝMúhGà0-^‘K6ÞO5DFLZ‰aØâg—^t§ãTX³Šáïþî_páÂ* ‹@7ÛåŽJ}€ fmÛ»¹ØixÇ;¡4®¢%$ǯaèܺߓêï ‘Hä |}[ä0ýdѸDH(!õõõƒèêê*¼¤ãgoˆ â`ÂC 9h Ã5«Þà¨ûöíC0„Ëå¢ AQv¸\.ƒAôöö¢µµ•º£=”x-EQòÏ›É²ÈÆ ¾¼lè7›­­­8r䈥E´‚nS,Þ¿Œ9Xg°-~rü ©^¿èEt¤›—³ù?Ë r¨ÄQ ŠBœ—í|¾ /t˜i·Õú£<°‘3 ̉ λv:NÕTyC‚Ât8N/Ed“Ý ¸#i8¢¹ßCQæ’Je°wo^ÔÔV@£›í‰2'ÌR!DÜÖr;šïØPjeîÜò &Apbèl7ÞÕ¯*jKèµA{1+Áì"6÷¶A&˜o¥<Ðl„±ô¸þne‘8vïÞ]x«˜-zàÚÆLHYGq`%«z#£:tuu!‰˜6ª AAðÄív#£»»Û˜•Ţщâ@"}ùb&Šä0íÒ È¾ü9 Áﲸéˆaííí4 ¥ ÎŽâ`ÑH):ò•ó÷2!¢8X7’ÃbyË+ÊC"z³y9­çù\MP—…Fu(T`À»Mäë‡ÀB‡™v›W„<˜•´r'˜­Rt7½TSå 3"´>Õ° šC¡Z"ÎT ´Lî\‚¢,Ä /ôã¹ç^C*•¡Â01jš{åM†úApamÛ{ÐÔº6¯w«•¸·ãC°;TÁ‰Á~ý9¥ú;Jà©¢8ˆ,r€x¾™<ŠoÜn·eƯ×ËOôðb¾¢9X$ÃÅ7•‡väk†DuhmmEoo/€eDNAÁƒúúzøý~ s‹ûœhãBä`˜a‹À6©ÀáFœ@öøßB;þ-®f»»» imihdóD´Ó-p`æÈ—ÕÂ!²£TëÒïÂt„ÝŘ ×h  X,t`|ÛF¾~äõ˜òè’í¶F„¨ °±‹4w™qس-CJüK¾½TSå Sát:ÛW>EÑ‚;¶‰·!O¼­o‚KÇ©ànÁñãìÝÛ‡k×&©0LŒšÑ¨ˆ²$•TÁ(Ê ApãÎ-bõÆMºÞi¾c‰Âû_ÖÿRã}EðŒ¢8äA«Gqà^V=@^Z¦EŠRØ-úD$r°H†KfŠ ÷#ú° ?ãžÛmÛ¶![JÜDA¼q¹\…B\Ö‘ ÎõC¯@íû4ØÈqSnƒÊj8[à`ò,²Ä²¯þ9Ø¥^n6EAoo/ü~? †t+« ò;Äâ@ßCÎ#ÂK\Ôd™àü<Ë×¶žd˜Ž¿f—‰~¥Ã ®—W”‡koÑ\fR÷‹îb«Óé¤P­e ³!ttÍ¡ SÛJµD<ÏZf*ºƒÞ÷R$xXŒ¡¡öìùÜÙ11 IDAT9.\ Hf%«’à(OÒ)Šî@¼Y½± ïyô£KF{hj]‹MÂÚ¶÷Ø àòYý=Œð`Ð/$‹˜]Zà0Û‰JVŸ9Äb1Ë/^¯¡Pˆèáõ¯.Q&<ÔÀùÖFg¸¤¦LEu8þ]h¿$.sÍõtT‡žžºy— ‚ t¬##‘ºººøW'‘=ôeho~Ÿ{4'ã·e$r°H¾ØÐ/‘}yûÛÒ9ÐÑÑH$RvBÚ¾¾>ƒ›`9Íó‹ä æ°d}s±.=°×@Z߽֨DTgFÀïyŠêÀÏÝß‹K/t˜ƒªõGy`#gh!mR¦Î¾®ÝM/ÕTùB‚Â48ÎzB+´[¨¢‚3C‡-£Ñ­NBÊã½r"•Ê`ïÞ>¼þúi* Bˆr%“$ÁAÁ²šܹåA¼÷ß} ïúµ¬Þ¸ «7nš¶÷`SLJðë}wnyJã**,‚0€øå HMŒéÜ,Õ\¢Frù‡ÓÙ GràZâÿ‡-9δ··ó=¼ýÒuÑh°H†KjjŽÙé¨çöqÏ=Eu ‚ ˆü©¯¯G @oo/Z[ù_ü§ÿ1Ôƒ>°k®SÊBk)‘ÃÜúÒÎîE6üU@亾 …B$¤ÍiÅÝKT³p$‡Â‹~/+é°d¡ïE¾ô@Ê'ÊÃ’B-:^&0Î fÚmU“þb%Áƒ™IŠå¡Óétº¨¦Ê<fÂ@Õ9f«Dªá^ª%‚àˆmlòÄÛù/¼)ÊCN¼ðB?ž{î5*“¡¦éÐ7Q~¤*cTa v‡·µÜŽÕÛ°zcZîØ@"‚(—ÏœÐÿRÁb‡â €\EqàQøâ˜*e}J5NÝïX1ÂÃ4ÜDƒÏß"ÒƒàÈAØn¬zš¢:A„à¸ÝnD"twwó7žBö Ú™hRkDKeq̨Ⱦúhg÷rKEQìÛ·===e9„B!NUeUCþcGNßËJÚgéÒHõyÆ#KdLo!äò Š/tÐYŸEìŠRõíy¬íFÀ#´€6)iåNhEt7½TSå 3á}°g¶eTKÁk‚JÅQ1|´0édŽ?ÁÓOÿ©EÅ0 ªª!«R”¢¼H&T*‚ Â’ ö¿¬û©qsž©ô#Þ"fIä`°o$r¸5Õúýýý–oø‰~íä±3[Â[ËpIM-šLü,´Ð;õ wÛÕ ‚ ŒÁï÷ãÈ‘#hkkãn[;ó,²/ùÀC%ÚnÈÁ¼ëßy36õåÏžà–Z[[Âá0<OÙöÿ|""Ju.‹‹òß<‰-r°È¾Y°K¤:—þ,$¢¹fl‰‚ÈõVXyòÓé3 yÞ@?—†Zð£Ü:­Ò²<¢<Œœ¦…³‰1Á¥ß>ª¥ò„„)p:íÚDöÑá|ÂTT ´ÂßS„} ÅðôÓ¿À•+TnfAÍà(²ªF‘M‚ ë®ÅÏá¡ø‘$äò£­È?œÎ8ˆ&rõ@³€õ9íJµþHEùâ0ÜD'Ÿ|^¸îKQJo*´SOCëû°kç¸Ú¥¨AQœõd8FwwwÁkÊ›–$cȾ´ ÚÅ-ÒvÃâëDV>"‡™uæ¥}õ+@â ·T»ºº‡ár¹ÊºïçáaY#íî“Û÷2"r°@Õ0ñò&Õº{µ¾—²©Y¢"PT~~èþŽ,¸ÐavÛ­Ñ圡E³‰I4nÝEÅétz©¦Ê;a„VeejWC­ZEµDœ¨: )]ø¡{y¾àAM€]»8µ¸{ È$®ÿûE@M¶ð®j€TÕ0µØ¯kìU–¿sêÿÖ›¦ìãñ <ûl>ºë×7Scœt2‹Ê*ZÎåÁäE !‚ ¬ËàÑ<"<,*x0è×EÌJ…(9L`÷˜¦„,°n£“ª`“—u™ …Bhoo·ô¸3-zp»ÝˆÇóÿ£½þ7H«?"`a­=fîn™™ËùDw¨v5#2ÆA9”Î7M™Eä0‡•íÀp¿.“@>Ÿõ£b···£§§[·n-ÈŽöúß@VÖARÖ ÐEHä r7fç~íÔÓ@f‚«]EQàñx@ADqq¹\…Bèééßï/HL{ÓÚaè¨}°mþR‡¦Iä`ùµ¯:ì‘'ÁFOpó¢­­ @Àòbø\ ƒy½'­¸»ìöa$r ï‹÷‰ú38Ô- øùÁ îLœqAª^­ßäõ3TÓÇæ#Õp¯è‚‡§ÓéŠF£ª­ò„ðPDuŽÙ*Ið@œSqT /}»Û´°!›ˆÝ7¤ÆÁ4Ã^_̳¡cÀÙç¦þ¬ªÒŠõÖO‰ [èÿô§¯âÂ…+xä‘÷Pãub ©„JQËCÑ‚° v» G•ìr~6®¿Wá°ÍZ3¤SY¤ÓYd’Y0ƨ°MÄP‚‡©èÅ8$r0Ü7M™Rä0§¿¬×ƒþþ~D"¸\.ËAÓ7•,zxñsý›Æ‰Hà`î.œ‡väk`уÜMwvv"PT‚ „% !‰ ‰ÌùóöövÔ××£¾¾žÓ–ÀçóÁãñðöBö òºÇ!¯ÿ=sïcx.î˜Eó•Ï›cȆŸW¸yC‘Ãn¦§§G÷;ÒŠeÑ%㛹6…ôM`¹þnhø¡¾ìGøÔ1‰øùQQæ`«„´¬ ,9¤ÏúÈiH-Ð$jR2µ­Ð ät\d7}×ÿ!Ê:G˜¡%;$-Ç¥€6÷P«–IBKß8ˆ.lÐCb,qìÒ¡©2¨k™?´<©®E ™LãÑG· ²²‚ª€¤“Y<–FÍPt‚ L¾Î•$,«©@å2lvÙ˜4d •UvTVÙÁê¦Äɉ E~0 —Ïäqû`ý| (¥óM@Sf8ÜÔ†Wæw€0 –E”€“è!3íÅÏÁöðßME¡1´)’ÈÁLݘE_‚väkÕ `ú}$A,C{{;\.WYöÌ€ßïGOOOηÝ+Š2§Ýn÷Œ(‚ Ì‚‘Ñ´³Ï‚‡mó—{My®Ë=ŠÃBíâÒ ÐNþ NrólçÎe³Ì•P(„þþ~ÝïI÷[¶-R‡R¹oþ0/!P:d“€mY~eBB~~”›ÐavÛ­¹=ÁÀ¦fÒùj"²‹࡬èÖ?BdœNg;€#"û8ºñ h…*‹ ™Œ´ É«3Âˉôb¯‚Ô´ òª{!5m*¹;MMõøøÇ;Hô ( MÕd‰ ‚°$ñ‘$Ôt– B@VVÛ¡,³Á!KXY=Wx5<95_™TOf‘ÑhßI”²MBu­£¤ÂÄL:‹Éñ £‚óƒÏ?†Á£/ëÛC¹¿©qsa “È¡t¾ hÊj"‡ùh|`Ãúg´¶¶Þt³Õ Gz”uý›@E-ç¦H"suc6ÕáÔ3`çöq·ÞÑÑ@ @‡º‰²`ZЇ‹Å …‹Å=tØÚÚ ¯× ŸÏG‡åK@,ƒÛíÎë`è­ê³½½ííí$‚ LE8†×ëåÖf°WöùËîEq(ãµ/íä?@ãxOQ„B!Šº³n·;¯È-¶÷}RU“%Ú¢øQ,0h”Ñ÷õÐÁÆÏëëOk?IÙ ¯\HèÀÏ2:̼9vÙÁ ¾±³®¶¿H©‰‘²I¬xã;²)‘Ýüh4 Rm•I›$Á!2N§3 KTÿ2µ«qmý'¨¢"ï\lä ¤ }ÐÆÞ†–§2YˆªÈM›JùAQjàñ<ˆÆFy‰FuU5$F!¬G*©b<–¢‚e¬©ñŽº 4×V`e¾Üñ¤Šóñ ci?–g:¢CUµ]Ab&Åx<-KýODžüþ5¾ìy.¿ÃÄ$r(oš²ºÈaŽÙ3?„vì;ºßÛ½{÷LôƒrÁçóa×®]…Í…zD$p°P¾á‹Ÿ…vÈ$.sMAQøý~ºq—°$¡Phޏ!‰|H¸µµ@n·› Ødsi.uëv»gt8—¿ß;vp·+¯{òúÇ­·N$‘ÃÒ¨ÈÿØÐ!n&;:: IPv‹5ÊÃ?¬_Xç‚í¯›¾ŠÉYxˆ°ö7ìé=Ð.üTß¼×øäw~$t(ÄT1„:Óa&3²)¨'ÿV÷kö> Ø«hB51µƒ?FåÈ1‘]ÜF)l™@‚BXœNg=€aOÖŽ­y iåNª,‚È•ë6rzêÿÇ.R™è¸ëZ µv@^uoI6••xüq7‰C¶IXÑXMAX ¦1Œ^I€ö+¥geµ­õX­Tr±7OáÍ+)Lf4*\ÂrØ6Ô.wÀf—…W'Ç3HNf¨¢âòÙØý'Ö÷RµòÿõÃ+~‘½E¡JߪIàPZcœ]2Ø·É(²Ïÿžî×::: …Ênlòz½Ø³gOA6¤•mS¢]M‘DæëÆ7;¤zìÔ3ÜSjkkC0¤¨„é™-l˜þ÷óçÏšf9 øJE$Áš5kŠž®¢(3ˆiAˆ„QѤ†»aÛü%À^cîu"‰r·œ‚~l,ÂÍæ¶mÛÐÓÓCõ´··çÕwåO@n~ØtíPüH$r°B†µ·û}ó)}m³¶¶;þPGr$v ¨üSõ?Ì5]ïØ6RÓ&šPMŒ=qÊ©¿ÝÍ5Ñh4BµU푊€;h…Ä‘ËÒyì"Øå£`CÇHàÀ«<ÿÚÉ}š6A^ÿ(¤ª†¢¥ŸJeðì³!|àí¸ûîVªQæ¤,C*¡¢²Š–v„u§IìPbª+dÜ»ª ï¨ãAfµR‰ÕJ%ã)&)âaªj¨®7â’$K¨Yî€c™ c£)caèì ýuYÇ"†%ÞÍm×!òŽˆD¥5ÆÙ¥"úV턤¬‹ŸÕõZ__B¡PÙÝŒ  Ñî‡öúW!ß÷Ÿ–¨n9˜«/âÌäeh‡ºÁ®ãžjww7ü~?-S1¥!MØp+¶nÝ $z(âZlâñ8öïßýû÷˜+€ðx<$#JN{{;B¡ü~?×(läÔ¾?‚mó— 5Üc®5¢¥–ÁÅÉ @öÕ¯ê${Š¢ Àã¡ o…ßïÏO¨d¯†Üt¿iÚ¢ØQ,0`Ð÷€›ÛÜŠú­ŽŸÏ1Y:ÐÁˆÄ¦lI5·ƒÅôý¦ÁFN“àÁä¨U« V5ÁžÙM/?Õ–õ¡„°8Î0€6QýK8¤ó!ª(‚¸i¥3ÅA».r€š 21z2oXyÝ£Ö5ÝGÝB¢°;lP–QA– •T1KQA”u •x×ÊJTØŒ½¥>“ÕpøRoÓó„‰×b’„ÅÊeæ2áÚHªJ‘VJÍ‹O/>óM}mnã'!ÝýÉy•ºÈó¹µ ‘[,‰JkŒ³K¥ó >íðWu¿×ÚÚŠH$R–cT¾7y΃nÿð уðãǃâ8Ä.ü Úñ§€Ì×”[[[ é¦rBxB¡ÂáðŒÈ¡¯¯O8EA8¦ƒïãv»…¬ÿÖÖÖñƒÛíF}}=UQÒ1Óãñ sµ+¯{òúÇÅ^'’È!o´‹/@;ñmnöÚÚÚh¹ápn·;¯¾*½Ã ÛÝ*t;[äÀ,Іû _–7,C…ÃFA˜š¬ª!~5I7—ˆ Y»›«¹GuX Šö@˜vý%IXÞ° ö Ùt¾“èA ~ðùÇ0xôe}íÎý-H+7ßúïso¢¶N8”Ög—ò-3Žìóçu {çÎðù|e7FÅb1¸Ýî‚Eò=ÿ¤uÿ·K¨¼EÓýJ;òu°èAîlÛ¶ ~¿ŸåÂ1[܇ #‹IGGB¡U¢ð ƒ¶¶6x<x<:ìK”léõzg¢’ðBjz¶MŸì5b¬¥,÷©“•$)íì^hçör3ÝÙÙ‰@ @ëLç4Ûû¾ ©ªI¨v( ØÌßr̉"{ôëІ_ÓõŽÜòaÈMÌKž„Å:èLÇ"b`™8²¿úžnKö> Ø«hr51R6‰o|RVèK+?FƒT[o‹tˆ§ÓÙ`›¨þ¥•;1¶æ1ª(¢¬a‰°¡£`]¤‰ªÈë)šðáž{\xä‘÷P¹ Ey L?·ÐáÛ’R!Kx¨µJ‰n©'U8?A¢Â4˜Yì@ã®8ìôl@jbL×;rçs@EíÜö˜[‹ÜIäPZcœ]·­iG¿ vöŸt¿§( "‘HYˆá&zØügVÄ »‹vcýΰá~hG¾$.sõDQƒA¸ÝnZ(%g¶°! ™JÜp+z{{©¼O4Š¢ÌD~ðxŸ»víÊo.4$ºƒ"VF}¬hî“À!SÚ…AöÌÓúÚ´rlk?Fbž~PTݶÔ_ý¹¦Ë¢mó§!5m¢ ÖäÔþ•#B_‚¼?z¨¦¬ !q:1†O[óÒÊTQDù¡& ]> v¾Df ˆÂ=ˆEy ÌÌX<…tB¥‚(¥;LC¢Â,XAì0 ‰J8æ]¾€§>ñ^ví”à$r0Ü7M‘ÈC¹MF‘}þßçe¥³³Á`y^‡áv» >ˆf^ÑEr˜vê°SÏp÷†nÛ%JM(šù'ó=€+]]]TÙîÍN[[¼^/Ün7E жÖôz½ÜEeò=Ÿ…Üò⬥(’ÿ¤Ô d_û ØX„K2Š¢ Àã¡3pK°uëÖ¼ßçÝ"9˜oˆ ¡ƒS,öÔ#¡³ƒUÂ¾é‹ ¡Hè`Hb¹ÙÊ^ü)X섾uYkä ¿C“¬É±'.C9õ÷¢»¹&F¨¶¬ áp:^»EõOs(ÝøUQVh_:6t¬,òÛZWW]]ÎÏÇRiô_7CU °møÃÓë×7ãÑG· ²²‚:M)7vÊJ H˜ÄD“ci*ˆ ŠØa=f ¶¾•‚ô0!v5-Ký®˜ ö¿Œ|A_ôH©±rÇ·—ªQ‘[‰JkŒ³K&9ÌC;üU°Áçó²ºoß¾²= ÃMôð¾o@ZÙ&úÌhá.\ C“—¡ê»vŽ«WŠ¢Àï÷ÃçóÑ(±XlŽÀÁ Ñrett”„E …ððÃ[*O­­­s¢?„‘c²ßïÏûFù[î¡›?Û¦Ïò_K‘ÀÁФXbZÿ“ÜÄmmm$âÊqß·yóæü÷{k?yíÇŠÞÅŽä@‡²—0•é}\·IÛÆ?…ä¨7 [$t(Øw‹ ¦ÓbÇ¡]zNß8]×Ûƒ_¤‰Ö(§¾{bHd·G£Ñª)ëB‚B8œNg@‡¨þ%œaÒùUayØØEh‘0tLMšÑåp }åJÔWV¢ý¶•€¶ëÿ îæ.鄇‡K§Ðwñ"bé4ÂÃWÐwé’“~ÃzÈë…Ô°Þ°4ššêññwè¡ÄÔ*•¨¬²SA¦!•P1OQA”ˆw7WaµR)”OÃ* ŽSåBRUë@u­õÖ:jFõ‘$è;QñxñéoàÅg¾©oM¿ñ“7~rþîMä% JkŒ³Kæ8ÜDQEA$)ÛC¢\rVÔ@~ß7!)ëhL0Y¾Ø…ŸA;þ™àêa[[‚Á \.-C)gÃ|vïÞ ¯×KBÄyR`E™>x<͆ áõz¹FØ‘ê\°ÝÿW€½¦°µ”¥>—0a“ccȾö@ä’4EË‚EîËaï×uö5£8°2êcEsŸ¼M©Gþ,ö†®wäÕ¿ ¹¡£O$tà3™uŒaºc™8²§¿§;%ûŸìtq§Ù©9ŠÚÁŸˆìâùh4ꢚ².$x „Âétº ˆìãèÆ' 9ª,š¨ h_v¾HŒ˜ÎýiQCûÊFÔ;èhiA½£í+W–Ü·ÈØB/¢ÿêðÌÿ—lòoÚyÃï@ªj0Ä>‰XàIV4VA’%* BxHìPZÞQ[÷Þ^#¤oG/'pv„Ú!v‡ JÃ2Ëæ¢í—êþ$NÔw˼üà_Cjž¾„AÔoz$r(±1Î.YDä0B¢ ¹å·^K‘È¡dIi—^€vâÛÜÜ¡hFú毂";·,"vÈ¿qˆÉYxˆ ‘C±LIu­ú“?ÏÁ?:ì{™ fÚnÍí`1}¿o°‘Ó$x°ÂHj[†´r'*Gމì¦@ˆjËšP„B(œNg@«¨þ­y iåNª(Â2h_;ß6vQh?Ûn›5t4·\àPXÄ5«A’$00d5 €¦1¤Ô©ƒ]ÓO’jª¦-ð1EBe…Œ › K*ì6Hì²IÒ«~èÒEì@pàίpíU7|rËÜM75ÕÃãyË—WSg+Ë–¡Âa£‚ „„Ä¥ç]+—aC£Ø7ÕÆS8|)A•EAuU5ÖsfU ±aêwÅàɵè~ÇöØA¼'C‰qvÉú‡…ÐNî;¹'oÓ½½½e}X›Ë´ŠØÜÿ¨v–ᘠ¾CìÜ ÿÜívuu¡§§‡Äܘ8ƒAœ?¾|2n¯†´|-PÕ6rH ÑQýÓ F7>AEˆ; kH©©~râÊÌŸËÉṎG¡Æß‚{ 𦠙¡y IDAT—Žæf¸›[ÐÑÒw³þC@ªÆÆÉj`Œ!ºz,‘žùR2ýÁd¾8Aºé_æ\‘°žaþ{€êÊ Ø$66YF®:ˆàÀ¹ñC±"?H ë!oøHu-\íVVVàñÇÝhl¤è8¥@¶I¨¿­ ’,QaBAb‡ÒS!KøÈú:TØdá}}þÌ5Šò@1§®h,çäx‰ñ4U¼ ö¿Œ|á1}köÆvÈß*¡×$r(±1Î.•§Èa>ÙŸý0™ßÍõŠ¢ Ãår•íXæõz±gÏž‚lHÊ:ÈïûPQ[c‚IœÉŒC;ä»z”«YEQè,Q0±X Á`Á`°,"H ÷öHË×Bª[TÔ@j˜{+¦vñ_¡Ó'BÛ¹sgÙF+2 Ç£»MÎ<,8ú96j®(Š¢ÀëõÂçó•õz‰(œH$ÇSØìùck ¶-ÿmÞm³ÂLœvê  þ„‹­¶¶6ƒAsr$ÃëõÜ·lïÞiÅÝ0TäÀʨÍ}8ˆRõêË 6ÿLÏRýný@ªmÕá+ ¸øOb‡™‡Xæ²§¿§ÛûŸìU4 [åÔ÷a×yñB‘ÙFTSÖƒ„08Î €NQýK8¤ó!ª(¢ô·: )3 9qr:hȉ¥7@Z&‰ôHêXT¸-ªo );ßÙùþ}&±CÁ¾“ÐaÁ‡ÔÓß2×tydÛòH ëi"¶•#GQËI´jýÑh”B~Y<Bàt:댊ìãèÆ' 9è°.Q‚Z„œž8$†!éüxMĉ ›'RSÛm+ániAçš59 cP5†´ª!™Qq-‘ÂŒ˜a!‚H‰z¯Â&£ÊQ »mÉ;ááaì:Ú=§N_AU °Ýóû\7$z(-åvH“—±x é„J!Y¿Õ²)|ÌhxþÌ5ª4"'’ˆ_‰"~å2R“˜…ª.©Ànw fÅ TV× ¦¾·5ߎe57nø+·è3}¢<Ê/¾Û×ö}_×;rÛg Ýñ±"x'ªÈ lŽD%)7NYÓŽ}ìì?åý~gg'‚Á`ÙŽg±X n·»àÈúEÌÂ]¸4i§ž;õ w»ÝÝÝðûý4ùº±¤ÈaaƒT·¨(ü†ñìë6ôJÎÏ·µµ!SCãÁCSÀŒbè°ÑB—‘¢(ðx<ðù|ho§³'„~|>_Á‡´gËò†OAnþ€ rÏ,•O±‰øôÁKì 5n­í‹¹=+üÞÌçæHä`Úê×¢}Ⱦù”¾þWÛ Ûº?(,c$t¬m˜Gè03—_Ø6vF—gòºG ¯”&c e“XñÆw e…¾èrs4¥Vk{$x DÀétúìÕ¿´r'ÆÖ±ão!5n6¢¶)ŠCiqv‰ºfÆ‘íýc`òrÞVÊý€MñDÅÁ2ãÐùÁ®åj¶µµÁ`´º°’ÈAª[T7Aª[ ©áÝô¢EþÚI}‚ZZëó%oÁCAÕpóË7¢?œZ1õÁëõRã!t‡áõz¹ÎòºC^÷¸u÷ÃB%Ç®‹B\¬íÞ½›Ælݺ•Ã:ÇÛ»ý€ýÖ¿3P‡R¹O³}`ã硾úŸõ½f«„ýž?Ë/ƒ$t¬˜Oè0³9 -ªo>—Ööå34![„ÚÁ£rä˜È.îŠF£>ª)kA‚BœNg@«¨þ¯þ-¤"ŒÕIØ®‡ml0o‘ ŽÐ¡µ®ž5kÑÑÜ ÏšµK>ŸÉjH¤UL¦3ONÝþ}“˜a¾ØáÆ¿šZì0ŸJ» U;l²|«TK¥°ëh¿ñÂ{l›~RÓ&.æHôP:d›„úÛª ÉQTÔŒ†k#Iú] 6­ªÂú†JSù|òJo'©òˆ›˜ˆà\ÿaį\6Ä~Mý Ü÷ïGƒÓY–åK‘yŒãɵè~ÇöØŽÈ¡ÄÆ8»D"‡B²á~h/~® «;wî„ÏW¾¿Ÿ'z H†z0ÜíU?™àj·«« ===¨¯¯§IŸÈiü¦9Huk€åk¦¢7,_iŦböä©ÿ9Žì¡/ëzóÈ‘#$JâÙ$ß]íÕ°?üÿ;‡¨`#'ÀFC:$¯WnŠ¢ÀëõÂçóÁårQC"rž;¼^/öïßϯ7ÝÛ=Ÿ]ô·åÖˆ%8ÐÉKì ( zzzHì ¿ß;vnÈ^ û¯wÁ¾B‘J™:˜«úç:“9ð)@ç!Û©jUî,[¡ƒÎtHì³-–BöœþH¡öì¢IÙ"Ø—¡œú{‘]ŒG£Qú8i1Hð@”§ÓÙàˆ°Ëg[%F6}ž*Š0 ÛÄÛ3Ñ A¡Ã´È¡ë® h_¹rÑgUMC2“Åx2±Df–˜˜/2]ì0Ï«›Ò_Ðö-ijߓeU68ìö@ñ„<£=è¡„» eeQ4&Ç3HŒ§© ã#ë—£Úd_†'T§Ê#æpñôI ô¿fìZÝ.£Âaúöv¬¿ï¾²+c5£!~5A3ñËðÔ'Þ«ï¥j'l¿ù¿ ýÂA"‡Òãì‰xÔNî;ùtA)•ûí¢‘HíííˆÇã…}ƒPÖ.éÁ¬]X,‡´SÏ€z†«M:xFè!  r=¬Zªš 5lš6,_©aS ºøÂ‰©Ïuê²ÒÛÛ ·ÛM‘zÒŠ»a{Ï_ua‰¡©ÈC‡ÀFë>Xg4ðù|Ô.‰œéééÁöíÛùõã:ä{>;%d³â±”s•:lø«\"Ï(Š‚P(D¢½‰Åbðù|سgOáÆìÕ°½{¤:×~#ôVˆʰɫqgÔ#{S—E¹ùÃïÏ-³ÕAì9²¶˜1~©'¿ h)]ïØ¶|RÃzš -‚rêû°'†Dvqk4 PMY<%Çétt‰ê_²q &Z>DEpÇ66ûÈ›EsJ/tÈUäÀCZÕO¦›˜ZðÎ*Üør;\Oáægnñ^e…U·üѤ(ÂŽÑHôP:UvÔ)•T„¡dU ã×ÒPÓY* Á¨%üÖ]æ{3Y ?ùÕ5ª@b†_½zCçÏ?o.³C¾©yýzlzÿûË®¬G¯LBËÒ·#®í÷¥çð#ÿ§t½#5ÿ:äÿZgJ$p(±1Î.‘À¡Fµ?6\Øíâå.z‡Ãp»Ý‹P½ òý; )ëLÜí·™qh‡ü`Wr5ÛÖÖ†`0H·sKŽ===ƒ…ÅÀ^3#jnÛ©n-PQ3·½«ß:Aµï Ê=:oŒ<×ÈØè °‘ã`WE„)ËÖÖVøý~x<ŠD,I(‚Çãá7¯Ø«a»ç³š°Æ‘ :ìkÿ•Ë8ÓÖÖ†@ @b‡‰D"ðx<|¢h];Èu.áš¹)öa»O+ÈüZäŸô­%î…íößÖá Jßn¬#t˜i»öƒÑõ޼îÈë¥IÚ"TŽEíàODvq4õPMY<%ÇétÆ{êjtãÐt —à4èjØbg`,X蠥ơŸ)‰ÐAq8àY³kÖÀ³fí-ŸS5 É´Š‘‰inñ×/v¸þ÷³^ÎUì°P4…ùÂ……b-ä"xX(ò‚”¯ØaÞà 0첌êJäE„;^=„]ÇŒ ÿÎ+Ú‰J‰#ILdÏ€öb²²ÚއZkMéûONőѨ]Å;H’„Ê*ûœ?+GÑÃĵ4’“jxyñéoàÅg¾©¯=nÜ yã'sx’D%6ÆÙ%9ÝhfÙŸý{ 3Q=p=TÔ@~ß7–=P$‡Ü=î‡öª¿à6>ŸmÛ¶¡§§‡&ybAb±zzzpþüy±­jº!phØiùÚ[÷mÆd}läxÎV»»»á÷û©‘rÜCêz~IÁC‘çuÚå©ÈìÊ!!¢?(ŠŸÏ¯×K‚:bQ¸꾎|×'!·þ¶9׈"ÍQœÅ¡Pˆ„P9 áõzùˆìÕ°Ï‹ì ÖVˆ"9X(Ã&¯þüœa±7 ùK}/9Øßõ§9¸ABáçJ#l±âø¥† éÛ5¬‡mËgh¢¶Ê>8›DñoŠîæšh4¡Ú²H›£ƒHD)q:^»Eõ/S»×Ö‚*Šà‚=~ö‘“€VØa!-“Dz$u,Zô,­jÆ“\O"“Õ–3Üø¿ëÿ» Xáæ¿ŸýŒ$ͳR¤è¼ÄóýÎÅŸ YFÕ­„ ˆŒ]Ãö—`dÀ Í'Ú‰J‰Þ¨ cÕAtÌ,x8p~Ó*Ub™S,±Øì2*¶›þ|]{;Ößw_Yïñ« j|ù§îOâôÁçu½#?øß!5?t‹¿%‘C‰qv‰D¥6Êâg õþ‡‚íèÁ`ÑErÐïå¹A;þ?¸ÚTÁ`n·›&xâ&B¡öìÙ#®“UM7¢74l‚TÕ´tø`LöØ.°‹/äü|gg'‚Á 5VNð<”hNa7§ÍÆ"`o÷NE úCWWü~? ˆ[‹Åàóù¸Î;Róðmø`¯ÈÄL(ûÚ=Qpª$vЇßïÇŽ;ø[Lì@‘ pŸæªz~ÎdzO÷¼çó€mÙ-\*'¡ƒÎtHìÀÍ/–BöÜ3úÛîGvÑdm!jŒÊ‘c"»¸=ÒM-DIq:!¢ú7¾ú·j¸—*Š(ÛØ ì#oÑi*2±·Àâo!“-Þa¿Öº:xïÚ€® ï‚«®nÁg¦EÃãIdTmA1CÙ‹fòS¸Øaö;vYF•cžðaÖÔºtÛ_:€þ«ÃÆ,$8D{ ÑCé ÑÁåC†Æ09ž¡Û¿Mº†JÜ»ªÊ”¾“à¸9‹Ó¯½\´ô*6Øìò‚÷`g'ên»­lÊ~äò$EîáÈSŸxñËoéÛW>ú¿€'æ,ú…¬&°99”¤Ü˜àmìV© >íõ¿)؉8Šîùnÿ°uÇ;#IC ,z«ÙŽŽƒA:tFÌ!‹! Âï÷‹ÍÁ^é¶{ ­úµE ôq“ˆÑÎü#´3ÏêêÇ¡Pˆ.'ò<ˆ#r¸å£‰!°+‡¦þ}£¤åÜÑÑ¿ßOb;â–ôôô`ûöíüúv rûYdÎ(Ñ„a‚¹){â[`— Ÿgººº¨qç¸óx<èëëã´vZ@ì@Q ȉÌUýÆ8£¾úŸÁÆõí¡l®Ç )w-àEu0ëÜY­" f›QO}ÐRúÚî–Ï@jXO·E¨?ågþ?‘]<F]TSÖ@¦" J…ÓétA`±³U"­ÜIEä?À&†á¸tC‡ ;d®E¡^x é‘HÑÄÍÍøÑ#bàˆî-÷ß$vÐ4†ØD §‡â8ãíØdÑÅXì°Þb`Zl’D"YpkânnÁ‘ß}ß|ð!(ÿ±rèÔƒ6r&o©TÏ>•+q8ŠL:¡b,ž¢‚ ò&1‘Á蕉ˆâ|,‘%*„2FM§1>\Ô4o%v€cÿöoeUþ¶ ú|Ľb@Í*Lýà3ýP_PfýÃÑÊ確^"ùÆ«Ü )öâ—•´ú#V~¸~ëÖ­e}0§½½¡PŠRà™ hG¾vágæí%r—ÅÏB;øîb‡;wÒ »Ä"‘|>\.¶nÝ*”ØAj¸ò»> ÛûvÁþ¡ga»ïÏ!·|p‘h¬Ó§ÄH£lBXi’dúÚ›TÕyõoÁöݽòÆ'¾é»ôõõáᇆÛí¦ÐÄ‚ø|>ìÛ·¯ð5èt—‹ ûòv°‘ã¥_#-©ÂóEb‡â …àr¹ø‹j]%Ü ™h¦«+1óçÍè}³PETg¤úú=œç–^9æ©P?tóÖ‘FQÛÏÄr´•ÓcœëzvÛ­¹]¿‰‘Ó4q[ˆLm+4‡Ðܶ:N7Õ”5 _¬‰RâÙ¹´r'ØüÐ_‘ËFDË bè0—@Nv«¾–GöR?RC'‘Ê$ ÷]q8°íÞ{1ðˆÞγfíMëÖÉ´ŠÈ•k8ùö(.Å&¡ªÚ¼ÏüÏ"bÌý{ ±ƒ$ŠØ‹¼7Ï×›ž)@ì0»3Ù,ÆI¤2 b|÷¶aà÷»Ðu×þ '1‚ì«ß~µPy™ ÑC ç»„ŠøpL£_D }6¡bôÊ$&ÇÒtã·Éˆ'³¦õ½~™*°Œ9×ÿT5]¼%KlÆFGqñW¿*›ò¯pPÿãÅ`¿þ(%Rc»€¿$rËç¢ù‡ì}3$k¥//ù¾ÿéï+ØÎÖ­[áóùÊvÜkooG0äbK;ò5hG¾fα®î² ?›;\;Ç-‰ÖÖV9r¤¬Û41—P(ǃ5kÖ`×®]…GtáAU¤–Âvß—aÿga{à¯!»:!-_›cçùÃ\7#-_£ëµp8L˜c_ÐßV‹lݺ.—‹C7áñx …ÐÖÖÆÇ :‰ìk_vþÇÅ_#–DäP$v(>>Ÿ?ü0·µ˜´¬öû®‹ÌöBØO,V8pΗPET‚Ë7êZõ{™ˆæ¹¯ ¡ƒ°{;=¶J!tXÀ”TýNý¦FÏ”¼—K, Y„-3†ŠÔ•9ÿØS#ÕIÈê$$fÞßÖ‹I¢q‹è.z©–¬D”ˆRát:#ZEõ/~×§ V­¢Š"taŸ}ä$ vÓ5ÓÔ©h±·Šâwk]¶Ý{/¼wm@}e%$IÂìé!­f12‘ÂÕñ)ÑÅB¢‚…DnóÏYÌ0ÿï´+˜ØAºÅ{ó|½é™[¾—›?ןœûÌõÿ¬ªpÀ./¬k ]ºˆí/@ÿÕaîmiYc+V}ðO +ïDb2TRÅäD ÙlnëÊÊ <þ¸ (EF¶I¨«_;ÝàL,B*¡br< -K{³²²ÚއZkMéûóãžTM_±¨™¹kÅäÄ8R“ãKϓյXVS‹eìDn5Æ/ÿùÿ/î¼h—áXâÝŠxð£-ñ?©b+€÷¢†¡p]ÂöW¹1ÛO2ãÐ^üXülÁ¦Êý°N ÀÖ­[¹Ø’œBÞüg@E­x}Cn¬x ìÜ>®Éuvv"PTb¦O÷ôô ¿¿_¤º5ÞùAH ›6,щ˜·@3lä8²¯~YŸú˜ ¡P?ü°¾}çÚA^÷ñ"4·âÔ1»rÚÐ!°·C%©ƒÖÖVøý~x½^jÄ ±X ‡ß÷¤æ‡a»ç3¦Øz—"1^b‡îînøý~jÄK‡áõz¹®É¤ZìïööÓ¶CqöfV]g1‹ViaÉ+P_Öÿ Ø~ï—¨;¦ÿefp™v>ecEq‰%‡xFÛýÈ®¢÷3Y„=ƒ¬N@fú~sÎÚªÀd4y²u`r-Df—m:Žo|Gdã\Ñh4FµenHð@”„ëabzEõO­jBü®OSE¹¦ê$*†ÑÔ‰a°á3E‰èÐÑÜŒ®»6À»a[ÿ% ×&ÓˆÆ'‘É΋â€BÅSÿa&±Ã<¯nJAÛy‰nÎCÎþÌ{M†„*‡²´ð Å;^;„¯2fœwwá¶û~sæ¿“‰ &ÆS»–D*¹øÆ¡²²üÇ¿‰ÊJÚ ”‚ê:ªj¨ì‰Y)4†t*KB«ôñ Y¿Ü”¾›Uðœ˜ÀÕK0réâW.s³[Y] ¥± ·5ߎÛZn·t»½xú$ú_+jšö [N"À;;QwÛm–;ÔŒ†øÕˆÂùÅw»ñÚ¾ïëzGnû ¤õ¿[ŠU€¦HàP¢rc‚·1#á(zèèè@0,ÛãÓ7Áó¸uTZ¾Òæ/BRÖ•G;ÌÕÝÌ8´Wý`WrKRQøý~Šê@ ‹! Âï÷ãüùó%÷GZõ¤U¿6%r¨jʯ#™ñÌ"fHðPÚ9NÁ+zãž·‡v)övl,RôäIø@,„Ïçî]üÕIu.ضü%¿áfÝÝmð'ÐNí.ØÎîÝ»©ç@OO¶oßÎզܸ¶OAì`òõ ,RõâÕ—úògÀ’úÎÙÖþ>¤ÚÖÖc1Ä$tÐ÷Xñû¬zêÛ€¦ïÒ,Û–Ï@jX_”š²eÆP‘Œê9,FÖV…Le4{5-J®S7ðC8â¿ÙÅ­Ñh4@5enHð@”§ÓÐ%ª-¿dãýTQDNðŒê Dj|ØpŸ;š›Ñ½e ÜÍ-7/D³®Œ%qu|j1º”¨€¿Øáúß/h×\b‡Y9ÑñÞ͉\ü™[~s·Ë6TVT`!ÙCdì¶öþ+ú.]äÞÎjÞ¹-üGT,ŸÎ:™È`tdcÉ[F~hjªÇÇ?ÞA¢‡Rkj—;`³S´‡r&«jH%³HNdèÇo‹ñÑw™ó€Ýóg®a2£™ÆßäÄßèÇÐùsÆÛvšï¼ ÍëßeÉÈÇú~ÎU,’ [Nó`ëÆØðÞ÷–ÅØq5:A(~ðùÇ0xôe]ïÈïß©qs‘<$‘ƒ~—Hä`úºÌŽ¢‡¶¶6´··—å8‡áv»¹ˆPQyóŸAr¾ÏÚm1×ß¼¯…v¨H qKºÜÛ+1E,COOzzzøôݘ9ÈMï*jòëHLÐNÌc ÁCÉBðPj¡Ãi³±´Áÿ]’¨Š¢ÀçóÁçóQt"ÀTt"ŸÏÇo.«j„­ý?Cª[c‚­‘ñ‰i—z¡øvÁvHì°4‘H^¯—käoÿMØîÜjêvXº®DBsU¿¸õ•=ö hÃú.’›òÊûKP$t(ª-&vŸÍ¾µl쌾¶»îÈë5öËÂ1y¶¬qze+ V6‚I¶²_£8â¿BÝÀEv±/ºi5inèQtœNg=È>¦"–^i8¢¿„}øXÁbubéó¿4\ìð‡wÝ…×÷cèíôÜ$vH¤³8wåN¾_T쀥݈Øa‘?3“ØT–ÅD:…¬vó!QWÝrôþ»â›>…óɉ·ÞÀÙg¾ˆ±3¯ÎùóeUxG‹‚µw4aeS-l¶›K}h(†½{ûJeh°)j:‹øÕ$&Ç3`ýèYn¤’*ÆFSˆ 'OÓßdxÂ|Q2YÍTb‡‹§O"üóÿ]±¨jƒoÃkÿÄÅÓ'-×f‹-v˜·ä]”k##å³ÿɵPˆEÑ+vP±›õ8¦8ã\TúÆ«Ü ÉšÈå•£ûöZH›¿¨ópíÂô÷÷Ãív# –å8ØÞÞŽP(EQ8,' òC;þ]홣Ýåì*»ð<´ƒ_à*vèêêB("±C‹Åà÷ûár¹°cÇŽ’‰¤U@¾×ûo< Û}¹åƒ9ŽÇó:RQ»?§ñF· ýé…B!jìfgÎúT¬ K®sÁ~÷¨p`»Ó iYcñ¾%ÄãØ±cÚÛÛ¨ðz½üÖ¡¸‚ì«_zEÀ­Qq×¾$v(===hooç.v°m| ±ƒ¾,˜fþ¼=vUDOñ" IDATæ©/©~£þÜòÚLä¸/Ñk—M­UYÞ rÞ3uƒV¸­œ3ª®u´ÝêwêOjôŒ¡µ%g“¨;m¨Ø*Ò£pLœ‡Ä²e¿NI+w‚Ù*Ev±ÃétºhEinþ{oÞÆuÞûg’)‘¢DQ„V˲)RµÇRBub;Ê­E¹V§õ‰»8mE·‰›Þ{cQ¾}ž&7v ýšÚéÓ8†âþîãÆVLõÆñ’EPJ®%Ç eI–%J\´€‹(€ –Ál÷p €`çû<IÌœsÞ³Î9ç3/ˆ ¡V­ÇÖ¶@bL¤–ˆÒžl†Á_ƒ]Ïm®(ò|g½þ!8A½Ã‡ßr zÿè+xá÷îFÛâÅ sÕ@$†ó×ý¸8@˜™€eä¹!ìää}QÂIò67ýôá¨Ô÷$ §ì0e0 ÊsˆÄؤ‡—;7¶âýÝ_B{¯¹H`Ãø÷§põg!°‰o¦ ‹ê*±jm=ªmóÂè¡°’$ ‘‰ü7"`#<)ωÇps$Œ ?‹Kê¼”åg‹oÃe4\<6|ò8.÷œÏÇòß—ù.÷œÂéc¿‹•D{ ù dx¸ÿ¦ÏW6c£'[H¹*04(?¹A™äIr(X+dqÎ2Ÿ²®½í{Š@@»víBWWWYއŠB¤K¯BôüiE 8HònÏ¿Ñû4À)ãÉjµâ…^€Ûí&oÄ.×5£@ªöö,!ÌïHÅx&«h$PÕ+I.MÍO :¤`Q³þMKgÝôyè¶= Ýït®Û’7kûûû±wï^Øív>¡­­ }}}hmmU&B> Áûˆ×~­¥Qaæ¾Rð2Äó?Ê9;¤W__{ì1eçf:3tŸø.襲° ù¥8(˜/ÍU}qîP•Íòƒ‡úÕY“Ì‹* @¼:Ƚ­° ÃôÀÜ$?É1õ€ZˆÂê§Fˆ,ŒÁ  …hÙÏYŠà%ãdÂYä"ßVå¬Ã`u‡ÁZ'ÿ-; vÅ俵‡ÁÞ>ë_Ûa°Û,6Û>-ç'f½…T*Qú‰Jp†+¿ŇsŠgÊ«ChbD5[Û—-›ìUU óÔ±‹Óƒc@ŒöÜ vH 3¨;@ͦ€’¥—d ÒÆ;/E¥ÎF:Ø™ÃS„¹8qþÁQ5½=øÏCï‹‹èHßüÉCai£Í«ÁhÒ%\#ÐCá% &,nŽ„ÁFxâñ¡„$ð""!7GÂ܈ æ ¤~ËAÅèááz°8ž§ý"o^Ò)02„Jzà¹ÂÔ=Mg>É Þ¸A¢Ìú¦ïŠü¥‰E)àA%ÀAÒTd •¤aÈ!ÇrS¥ØKè­i.LCæ%Š${àÀtttÀï÷—ݘ¨8ô0~ â±?ƒté0JՋü›¹ ˆÞ§ }ü¯Š™ÔÚÚ ÇC›•© :TÔƒ¾õ«`σ¹ó²„¤<–T8$–m …4äRVA!@6ä¢]S5·Ùø8t۞ݴЙób=ˆ¦d³Ùàõz±gÏÅâ?ü>„ÿ±xŸAÙ¦†pê ÇïÌ ì^jyu j6@¿íYPUöòÞHûˆ#CÓƒâÎWÚ]6 °bY‘*h_ò?2 ,©Ø˜ˆW(¶äúL1Õ´ü7û«=P’WØaJ4$è#×ÊÞÓC¤îZ7‘L:‹\x ZP³€†)˜áÖI€á“‡Ánp'€Û'ÿ­Ð4ù¯qOSÿ,'^úÉÒ߯YØ’ÑݲÛÁ0©x¢¤Ò¿ýð{¹µ3‘5r^U¯íË–á×÷ïįïï˜@%\÷‡qzp WÆBIa†øß ?æÿÌõ$0ü“þ•4ÁÌ'«PØ»CØaÁýRÉKŠJgO Ø!y˜99¡ÒÛ’ªÞ( àQŽ…˜GoÜøz_ü[ Ÿx9éuS…öÕ‹±¸¾2áóáa?^ý$€ ¬ð!‚`€ω¤PŠP³=9øG#cr(C]ŸàÀ ÅÕ‡¯ððñÉ㌠iÆžpàfÉ@ZÇ0“(3 ô—hñ¦\VŸ ^äšD¼8}]*bþo-¶®ãøgPÖÕŠ˜qäÈ8x½Þ²Ã.ñÃ@ôüYoÅØ•ÓÜÌM@<þ Hƒ¿PÌ´;wÂãñ ­­ Då'—ËUÐAgÕx7˜m¡s<Ú¾TE}*ùŸùéÐ…ˆ¦Þ¥¯îBÖyú´©,›(eª³n/ôÛž³ák LuyÉÍlð¡»»›´¯2–ÛíÆ /¼ \O¹v4=ð¡<ôG <øÄžïØAEy½^´µµ)ïÕÝ´ºß9,YBû’Fû˜VÇMQ‘ÕW†æR¶[åGêCñzu  ƒ¼ÛÔòà‘{d”e¹üPc¯9Cx0ï°Ã”‘…>r­¬ç/¢Á ^Ö¾NÞÕÜÐÐà 3Í∦5 l˜ía6Ð03Ô"00rÓ8ú/?úœ–ËÀ¼² MöZ¬Y¿·ÜÖ€æU‹Ðd¯E}C×W¢ªÚ³Å@KŠ9 &8Ûƒ€tå=×Õy*WUM‚;«Og¯ÜÄh0îB+у¬¼fè¹aæG’ëH¼ž4^*ùõù÷¤öî@Qy„&Ü[çݳìÀ "<Ÿ2­LajN¢©`‡éö€åcIÁ5½=Œœx}?9n<¹w“Eu•h²×&Àg/^Ãoœ"ƒ‘öb$ ±ÀnŽ„ °`#<žZ”À‹`#<‚7YŒ OD3º>Q<´,8{—¹quPžæ*¸‰s'Ž‘OD¤ õž‘¿µ­‘3S²1‰@kc…,Î\4è+Ao{Z1è¡§§‡£,ÝÙl6x<E¡‡¸·‡?‡øás7¡Ñv—ýÍÒx/„_= i\¹ùç3Ï<ƒîînØl6òÀ.3¹ÝnØívUÒ¥ãÔÞzc'tŽçÁlìU½*ËÎ…òõæ@Tú*(è°Š$²m¢: è¥è¶= fã7@ÕlÈKûûû±k×.8x<ÒÞÊTN§ï¿ÿ¾r^Ç®…pê[ CÚÿïw ûrŠcÿþývH¢)¯[›6mBOO²‘ëÌ`6~̺½åµ¶+/Å5 ©ûYyyˆ +gëü?2 L@‡ìã*rÐaÚ3®¹I~è›Êzx`¸ !RÐn¯ã'ÀpÁ²žËD‰—"5û)‚òÔa°:Lz^˜ügT;Ýá‹—vh¹\Lë‰Wè )ä ¢Q'€ç„C1‚6Ê“Vb¢D†«oƒŠåö…¸‚ðÈEUl´ øÞ¶mØsËú„ fA”0Œ`t|rH ¤÷`v@òÃüi`†¤×‘* ÁTŠpIÊ1U8Q’äbˆ "8I‚ ‰àÅÄCâÕzjŒFè™ÔL¢°Ãì0¼(B”bÐ3zÐsnêÜØ DzFì=úKôÜU¬Í†®œEï‹£ñÞGQµf˼ëf‹Í«ãêÀÍéqõÃû÷Ý·™ L‘(Äá‡X„Ÿî—ŒžÍP` Cf(èt4(šxOʇxN„À‹ˆÅð1€ D)Õïç°Âj, [/ŽiÛC‹áÂÉšµ/02„¡¾^,±¯.ʶªÓëI‡%*|ƒò™¸A-7嚌P!s´Ÿ”TÌ×$ô ¾û¤Ñr#ìÚµ ûöíƒËå*«ñq zèììÄ¡C‡”«þK¯B| ô-_µêveymQºñÄ“]§Ì!ºææftww¯e¨©>§ø!º´“{ ¨%Ÿ½öË2½8$é/y} •—¦¼Ž9Ež‹R°©onýfîÏ;J8ónžÉ)Ž={ö ««‹4À$s4§Ó‰þþ~Åã¦*í`Z—áQ‡ì”H†‹|’J´Ò̹*›å';ѯ͒üÀj¯s¤bmo’‚·iµ'F–ð0¦ì:}Ô§‰¡@ÇŽ@ÐW•í|&f]‰1‚X­šØAfÅ+<”‰&+€jÌ@yÕkO=³–cÙ­–‘¾~ KmÆ÷Ó •B,šµ.ŒF8ð¼6ʃòˆÅxB©h6½ïP9¸ã”Dº‘óGT±ñ‰Í›ñW-a3¦•#ã\„g °C2¨ 9ì0ÃB#fÇ1vHc[>a‡d¡•‚‚‡ÑDIJiŒs1Lð-f"Ú@ÊpTb ˆ t4èÀ§mñb¼¿ûK8pê]8õ®bmW`Ãø÷§`ÛÐŽ†í{ÀMz=ƒöE軑=TW›±uë2@iqG’ÀÇ„ÔO Ð3££@ӈȺñ"x^„ÀKàbN„$‘/‰2Óh˜ÇhˆÇb‹¶—„£!¨ i¯]<ž×6”1pötÑ[­æmÔçÊ$ ¢(Bˆ“à¬4ù“¢ãÀ,MÓ  ÀH[¾tVvʶ6YKP°Q)ÚB58AÕ°mJ•d˜¯B¾ô• ·}âoÿ7¤Á·‰òàÁƒèëëƒÛí.«·íÛl¶é<2!-B1„š.]»è„†sìVX   €¡ö¼øWóç‘ñàZ­–—yãç «Y®H\:=ƒQ³Å€*« ¶Z3×W¢²ÊˆÊ*#ŒÆ™ƒe¢H6®µ*š Àpí휈C‘u½ÁˆòîÀ?½l~uÿNì\¹&nzZys‚Åß8&XÔäÁúØŠ³‚Ù×ã÷PIa‡ÅÔd‰×gà ԬCýRÀ³|ÏÀ y†æ¥!v˜Êí¼g}â9 GÂSŽÜæA!sKXA€Í`€|Ø! |‘25¯|EI‚ `hz^\Žeh_Öϵ«Ä”;ØéGàüqXš6@gIü’¢)T[+š`!ðñÃ|ƒƒ#°Z-¨¯'_È›DQ‚(Hà9|L@,*€ðˆ„8D&8pQlTˆÞçã÷qOôœq©œ$ðq¯ lD@4Â#æ °ˆ†yĢēQö s"šmMÛøöÀ8Ï—?>ùNÛÀƒÀÅ`4[PYð@2^ÇFóš¦NŸ9,°aÛ6Õí‘$ <ÇÅ Br¤ÄC1“K¢Q p I hZ‘g(áÉó& ôœÀé·~"+ UתùsS¬`ƒR´uj¯° äP:u©ˆùùɵtòYpC™7¥Ÿ?/½ôPNºï¾û`·ÛáñxÀ² ¾ŒAò‡té§€È27úÊS”IWì}IÖýN§“ìV@v»×GtÐKš“ÎúúF…¾(e^FuŸ³ü³m€4шœª¥ÒÓÓ·Ûh4ж¶6ò¬,3uttÀf³áÍ7ßT`@æ ù~jñ&PÆš’XƒI7Ï@ìùNNqØa¾\.:::Tñ¼E™ê Ûø8èeÛÉ~@ɈÅenù¢õ|ÇdÞTÕjPkfÐ!¯q• èpGä:»)¯íVÔ‚ªÍý8©ž-jçûZ‰¢ ê*Q®’“– ¾þõ¯¿¢¢EÞ[:: Öˆ¸÷†E4w’å/W½! ‚&GrJ_ÚŽ.P†Š¼§- ¢Ql”C4ʃrĄڄÈÁØÿfN§ôÄb#”Ý|m®ªÂ÷¶nÃΕ‰o ±<oL &ˆó¼:Ùym˜}ÏìPɼ6ÌüHçQbþõDÛæÄ’wØ!yúéÃ¥‡``"^‘ ì0û×&KŒ“oæÍì07œŽfÀPóûùY‡Î¤xß«»ëAÔßµ;éX9ÛÓ|îs[pÛmÍdÀ*·ñ™¢Àèão°žíbÊ[ÄÜÏ‹AܤW Q $QÏ‹ÀÔO""õÉå,­ÒkÒ¶F¢87Õtù…ücxÿ—?/Šº®]º¶9в^ê9…k>Êkš#𡼝¦¡ŸØ±CU[žŸ#lª3söøçR:ýÖOðÚw“7ïi¾ôæ¿Ë=qÅ·þäPr+‡zTÄüÂæKxâ‡Ï\H‘ø¬V+\.œNgÙ›^¯ª̦¶‚ZºTÃÖÌà©0}Lô>iðе©îîn8•\.ºººÔOLg½ünPö *êsàó>¤—†7þÍY÷=z”Œ J=Wd‚æTÍ0w<©©vN©ÚLˆ„Aø9ÄÁ×T÷øPîs±r—ÇãAGG‡2ÏNyÒÓƒ½¸—`ÁËN=‘Sßkmm…Çã!Æfµ³ÎÎNU@ ë¶€Ùð5@g!û¥™áBO;KÉÍU&_ÂG?€èûy}~Éï‚^ò)æÐ!¯q•2è0ë²8zâè yë¢Ú5`¶üeÎVšÆ?­¡±E¤tˆViö½àyQÍÙ hÙÄ•>Ÿ¯ÌJ‹K:RÅ­IÈa€z­Úùý‡Þ¡UØ M-€f(˜-˜-‰o7 ‡b‚((‘ƒáêÛ9ÁúÑó¸é¿®¸mÕ²OlÞ ›Ñ8ýË ¸r#„ËOžaO €ÀêÃs„¬`€BˆçãÀÃBùH™:²¼+.^ÀC€qÎÛ×lF#^Øþì´¯ÂÞ£¿TÔÛÃȉW¼x+v~ú꺄±r…}.]†0ùfá×_?‰êj3ššêÈÀUF’$ ü$ 0õsÁ ¯!ñ€§Þ0ÿÀ'E:=­ˆ¢(AàÓÒxAàDð˜H zïZ÷®©‚ž¡5eW ÊkvÿÈpÑÔõxÙ:WKšWåxÈÜ6uáK.ƒÈç¾äY0Àè²ß"°CnºxFþÚÔ–ƒgâÅAKFj¨ØÉ¡µE­¸´u ÄwŸÂC¹Ï‰ìÝ»^¯.—«¬ÆÍ¶¶¶ièáØ±cê4)ßqH¾ãñºkØ jñF z5¨Å­Y6/åÛ¢’°Ckk+º»»É›ÜËHN§3?tÐöûAÛwz¹_U•£7‡x.•ÊDJûܱ†YµÌŠGNB¼ô2¤èˆj¥65s¹\p¹\*#9Žéçh·Ñù0„Sß³ùIPU+‹s!°ƒ¢òûýèêêÂÁƒUš—™ÁløèºO”Ö¼ƒ€šy—1 š«l¾(Û@&ð M K21/ ƒÌtì€R€€2/—ŸÂØÅœ­¤…¨¦` %”$@¢”«ØÚ¨ð½­e;¸@TT"ÀCê0Xâ–AÃÃl]=söSZ¶Ï´êNÍÙ” ‚ˆ„c`£<Â!vúÐ/‘²2\}T–„¡$ò¨>‹Ñ‰1EmÚ¸h^Øþ{h]¼xú3Q’à D02&¤‡¨ÙŸe;Ìþ„ÀHëM!™M¢$!Ø!eúIí¡RziHnaï³J1ƒŽf@S‰‡a;ì«Ðöà—°÷è¯pìúUÅÚ{t¤½/>Žº»vcÑï̼1™f(,o®Eÿ¥ÓŸuwÇ—¾ä@]• ^D)5ŒÈ” "*q¢„·Bø½•UÚ±IñÞõHQ”ŸÀ±Å3ò1DC!˜,–¢k§[-ÌÖ„7ó–f¦LZãZõÞ΢ì0óü‹CªÙ@’HÖž¹j¸W>ð«ÌöE -©¡b'C¾EYWƒqü3ÄwŸ€4ú"qôôô`ûöíØ¹s'\.ËDmmmðx˜µ{•ߢzàÀØíö²œ“•ósvÿþý¹G4ééa¶Ç…¬ºAð2ÄŸA8õø_ü„w¾ñü ®y4;L•]9Ã~¿Ø´i“j°]·úmÿT¼°C ï ¨þÜÔTY}elnáò••—‡Ð@й’Œ‚‘¤,×=ùYšåu(7.YmJcYÌ$² Ó¢ÌËå§>v1GëM5´H^$ÆÖnÔº‰N²Ú).àA£: ÖxìZwX ÀX¬y¹vî#MÆÕw–N‡f(TV™°¨®MöZÜr[šìµ¨Yd†Ñ¤#+Ý„‹C?úAvå>1„ý'”;lý­;6ã½wO—$`x<гWýˆññÃFéd@AÂç ™Ÿókšñ©"¢R_‚Œh’\—ëÝB²—(¥,Ÿ¹¿-àÝ!SØavz EÁj0Ê.€sè´Þ-ò ;Ì'ð$aÞçm‹ãèý»°¯¥UÑþ)°a üûS8ò]l(^>‰Þ†‡ýø·;F """¢t}‚ÃÛ!„ p°™Düúrý©¢”ºeË]yKK\ ¬¿óNè ê@æ<§ò|F’ ðòÀ"ž'IE IDATÀC.º(ÿ°eY:«Î à AÈA³_Œæh›*Y#C±ˆZq/èmOÇA&°wïÞ²<Än³ÙÐÝÝW_}Vk‘{yäB|' zŸ†ð«‡!zŸÂCŠÁ­­­ðz½ä­íe ©zÞ»w/€z e:ÌÓó:Ä+”XVQò9&‘NQvJßÞÒB’ºi«×¥Óg€nÚýÖïƒ^ù ªàÃÔœl Ê$*}uuuá…^È=¢,¡iø]ˆçþí?›47ÌÕ3Ï<ƒŽŽŽ²m3S@êÁƒÕI@g³ñ`6~ÐYЧ`RnMipÏJÝ 4*MåKSæj'_”mƒülF†²œ§e:È\¯Ð&è]²Ò“@™›ä[#ð 2&2YÕ¨bÖuM{î µT\"' 5¦Ã`­šXK!?'^úÉÒèDhVíÓׯc©-é6e¶¦ß„. ÂaáP ã,8N nª- ¿ˆòÕ„‡1è;§˜ÍUUxÞ±íË–M‰ è &¼q9LHô v˜wÏÌÉa‡ ¼?$waïÔž)(JØ!YèÌ`‡q§ ¦~¯5áyÄD!i³ËŠ…zs,z}Ú| v˜’ ‰$zJ—Æf0â™­ŸBû²Fì=úK|;q°÷.üð/ÐxZ³‹ë+1:<óÄáa?^ý$::¶’A¨(Çpï™ÉßÓ¿'»>Wõ«oƒ©²:á³­ñ¾`¬¬Æ’Õ·‘&ÊH¨€__ âŽef,­Òç%ÍÑ÷®‡ Zä*£¹’4š<Êb«ÅŠ -8{:©IE 4=n´lÍ4®[§Nª’‘WßË Ïq`t™o ‰ÄÃCNJõüN«Åm ¿Ic’4l›Rå&iÐ&Íi©ΜÉe] Æñˆïå 7æêàÁƒðz½èîî†Íf+«qµ££}}}èììÄ¡C‡Š?C\Òà/ (:ÀÎ;áv»Ë®]”›ü~?ºººÔ;@7¥ŠzÐk¿œ…7‡B ñR£)ä³lÖÁ$Â;”‰ÒW4¥j3• ”M™éê,`VîÓ´ÂàÏ!þàê˜yìØ1lÚ´ ûöíCWWyþ–¸œN'l6œNgn !†àýtw=ö º†4ð3ˆÃïÑ‘¢*«={ö”­§1¯×‹ÎÎNÕ<:q¸‹Yµ»x@‡2Þ(¦Go‘£ ¹ÚÌm»rw奉,òC@‡œâ’ŠyLTéGÙxx¼ ªª±¤žcA¹KbLˆY×Á8vZ«&6744tø|¾n²Ú)Q’Dv¿´ R¦ôä¶íû†z/íÖª}•wý!L«î,ÛvÇq¡BAá A(Ïñ€ ęҞÇMÿuÅì¸ßnÇóÛ·Ã6é @pÝÁÈx4)00hH%ÈÃÍ|R2°C2;“¤Ÿ>\› æšt“eäbà¤øÒ”Ã@G3ÐÓ4Œ4 ³^·€g‡ž&Ræ_yØaîm:0 ©ùN£ü1»Þø9Ž]¿ªx¿­Z½K?ûçèÍ»n¿ÝŽûîÛL&DšÓ°DÀ7ˆ¡Þ3`'Æã]Q=}£¥ KVßkÃrX—4¡~õmX²æ6X—4‘Ê!Jª¥•zܱ¬zFÇ€œ âÜ(‹Þ1¶xûu(„S¯¿Z4öþîƒTmóã“Ç1ÜIõtôŒ.±ý/[³-Ÿþ´ji <>–O'z“ 4½pÿ–D cÃaå°ç³ÿqáø›²ÂПü{PË>•Cªr(H¹I³G³ÅYÞÄÓÏBº¤ÜüÁjµÂãñ ­­­,ÇXǃÎÎNôôô€}ûöÁår‘‚(q¹ÝntvvªëÑAgm¿ôÚ/g7Ð!?yž“¼4ö!„Sß’ÓÑ£G‰7…DQ”¼ûk6€¹ãIÅÚ\É€ó’S m>áò+qðAEY­V¸\.8NÒ!J\S–r}SUv0›Ÿœwh]¼vÒµ£š÷àJ­­­ðxŸ,âŠDx(°Jt˜RgóºW8–mÐdã×W ¶£ ”¡‚4ÄIM£eçý9ûß”íÝ¡vìc Œ]SÌŽ§·nÅ_µlœY¸".ãÅ„ƒóÁ“¦õÜ ì0;m;, ;¤ÎǬx¨Ðì0õ'  Å$ â:݃¿>þ¶âý—1šQyÇ—ªjwmëÖ Øºuˆˆ ¡)°a çø$ÜpœÐ¬½S ÄŠÖ»P¿ú6¬hÝ:Ï[Qo@ÐÖÔ±ºÖ øÀ "zÇb¸8Æ‚‹=zòçÝ`Ú·ÓZ·-íŸ-™¶™èfhŒ3óµaàX¢Ÿµ£ÓAg0,xá1`A”½žû£;eÃÌ}ÿ˜ål§À!G# ¼hêRóɆ„PoAüðY€ )fÉ3Ï8NUß ôš‡@ÛwúLÞD(p¸/èP`oiL ÀCaÕÖÖ&ÀÓ}敜Úd&xùeˆ×ÕGÛÛÛár¹ÊH-)=Ô}LÛß|bÿÏ üL5$ùÕjE___ÙÁùRé•»ã^Èž@Ñï h¿ˆä ñ§þÒ„¼}¦ùPÕëÒ—²‹xtÈèFqôÄÑwä͉ê[ÀlújÖ¹3†úÀÍô_‘Ò!Zµ–Lš'UsöŸ@ÇZ5/Àîóùü¤¦´/)‚¨ÔAxéoÿÇ&­Â`hj!°ÃUV™PYeB}CÜûÃÄxl”/Ù<3þ‹…VTUáð½÷¢uÑâéφƣðùg&av€ª°C„ç!BBLÁ‹"8I„™ÑÁ¤cP¡Ó%‡’¦—t˜›×¹åˆyåŸ*Ԃм|æ v"DI„Žš_f-­p,kÄÞ£¿DÏQÅú¯À†8þ#еk@Ýþ‡ *j§¯?~V«·ÝÖLw"uW>Cƒºxfp8¡ÞÁ†‚E•6ÄÀ' ŒúU°¢u+V´Þ…uÛî#]ÆâD çF£¸8ÆbY•«k °š²[F¢ÓW8¥j• Ô¥ÕO—2Õ¹õQÐM; ||’ÿ¬*é;v ›6m¾}ûÐÕÕU–s³rP[[<OÎЃ4ò.„SO@ ^.jÐaj^Znž¼^/:;;UR©š `6| ”©Žì”À~€6‹ˆZe»U6ð …R’úë ;€À“m×Ü@ð ]Ì)‡‚®ZSÀƒ ¯"æYŠY×Á4rR³ÓXܤ¦ŠàÙH<<äW‡Á¬P_êyýÖæ­ÿcìÊÕÏiÕ¾êO†¦¤Qf ŽÅ ²ŽGK*o¦Ë?“<( ;ü¾ÝŽç·o‡Í`ŒOÞ$ ½ÃA„Ù™EÉÀ5`‡™êÁ ¶'±MqØ!è<‡›l4á°å\À€P¥7`±ÉšJîb!¯©ÃÌÉÉ‚ùPvH;Ò…´!Ry{ðÇX8õ.žîQ¾3ë*@7·ƒ^“øØùâÛÑÔT""%4ÛsC±Â ÙjíÖ{§áë’&ÒÊ\f=:³Õ&6#«‰žçýD¢"ü¬€ñ¨€‘00'–æØ áÔë¯jÚF£¹[vt”dùF†Ðë=…pà¦*ñ×­hD˶»PµhQ^òÆóøÅ6EÁX‘þ%’(al8 ¢ì5ÐsÿçëÊ«šÅm ?}0U­h/“rÐB„)Nr AvTÜÄw÷CºñbI´¶¶Âív—õ…ý~?Ün7\.WÉ{|˜:TFÞ ]ºòx®R$õ ã?zF¶µÌÖÇAU5f··! ¨~¬™þ©\‰Ö“Éò¤èX5gÿIË&ñù|dÑV"ÀCžt¬À²ÉL9äù/W½! B¥&QK-j;ºHÃÌfB-H£%?0Áè‡ßËø~%a‡oݱßÚ¼yf¢ð±/pð<-ì0ý_˜!Éÿ…`†Ô Â¬»Šv‰Dä¹äáòW•Þ€úŠŠìa‡¤á’×sêpTròîaˆ¤ÞÀsí*v½ù±˜ò»¢Ìíªv ÀhÔãK_r ®®d*©¨¡Þ3Ó€ÃÐÅ3¾t– âÞZîýˆˆféRÏ)M{y¸õ®v,j,íþ:Ô׋«>R |¨]ºënEÝòFTÕó³ÖEpÑü®±ŒfsÚël„ÇD€%<ýæÇOã7/~OVj̓ 7þåä_pÈÑÈ‚Ïc¤(Rr¨A‰¨ÄŸ…tI9€ÒjµÂív“ƒuÜn7Ün·ªo\-”Z[[ÑÝÝ »ÝN¸%(¿ß§Ó‰#Gލ—HE=èµ_Ýxwæƒò“ï,“{_‚Øûo²ÂàA9eéŸwƒçcš³mÙÚõXÕº¹lê" áÆµA„üc`Ã!„nÞ\°^t:,550š-°Ö-µ®&ËÌ—‰¶Å`ttÖW86¿pÁBÀC`4žI'ÏA¯}·§ßzYVúŽo‚j¾O[!ƒ–"Õ@‘’ jD%ùþâû߸bfìÛ·.—‹ Æúúú¦á‡RðúÐÚÚ Ç›ÍF*·år¹ÐÕÕ…@@¥/éuÐöûAÛwzKfYY€Å 9$¬)ðPPe<¤^+”¦7‡÷³tâC_‡xùÕ,°Z­èììDWWé0%(¿ß»Ý®Þó[£jooGwwwYÌKûúúÐÙÙ©*JÕl³Ö ªÊNöÊh? (ò¥)sË«òï샕³ê!Pæêµ:€€¬OGO@}GÞs°¾̦¯fÿ•ƒ@pœA­Z ‰*‹÷¡Ë’qìTüLË&>æóùÈfºÆE€5éÕa-€ÚrËûßmÜüãÃ#ŸÒª}5]`,µ¤‘*(Ž01ÅÍapœ ýÁOä`¼œÙCTIØá—÷ßÖE‹ã“IÂ¥¡ B1>¨ Ìâúì{ÒÃH8´Ÿ5ì4ÞÂÀs¬šþm 'JÉãN㥡ÆdB­Ñ˜$\öd ;$ ›/Ø!U¼ ¤AI1™wŸ?‡ÇŽ¿­Ž·ts;è5ŸCý²¥øâÛ ô@4­ÀÐ †.ž™†ˆ÷†§–*¬Ûv6?𖬾QyŽ+#C8}ì𲩾yÖmÙJ*'G™ÌzXª ª§£5à‹ ‹’£~ô§Ÿ•=Ï ïþ!(ëšÂO -D¨‘â$ò•è…ôþÿ†4~I1Óˆ€ùêëëCww7º»»‹ÒóJ»m:NUÛ%µäNзþ ¨ŠúÌ´b;ÈRFÞ’®)²._¾Lž I à¡d¼9HZégòÓ•¢#?>qô”ªÏr·Û]6oÃ/'y½^8޲€š››ár¹ÊÆ«œú@ªÌ:'襲'PÆ{%hŒ‚æ–o;>üÄÑ÷dÅJ/½ô¢Íê´; ha)¿†Iá+ä½ º èîþvNé2\ÆÈ•‚õj¶b9}™'%DQ{ú{Z6±Ççó‘EšÖÛÔÑa°‹‡Ê׺|ê·•Oý·]ohÕ>]M#l;þ–4Rp¸9F(… hsŒaBס÷-L’*;l\´¿üýûa›<4Ïò. Ä=$$À Á³.*;ÄÿPv˜kûÜk §°eápԬ鸄ËÁ`’+©Ó›²ÛÄèÐ8ù–ß¹pµ€-sJ)Iù¦²§4`‡ÙaŠIšv_p{þ Ç®_Uià¯ÝÜŽÛ>÷0>¿³ Òeª¡Þ3q°¡79†®BQIõ«6`Ë<‚–{¾@ ƒ¨üÆš¾^\8uB}‘ÀÊmœPjê*@Ñ”êi±áp^ó–xŒEÁÇÒrÔ·?Û(½ú€§0ÆÀAK‘j HɆ‚U=7ñýïBòW,J«Õ ·Û]6‡‘äÊãñÀëõÂëõ¢¯¯^¯W³‡ÔìPºêêêÂÔ›ÓV­½áPµ- hÄ›ƒúyV)ùl€ò=±rÊ xp¸…xsÐÂØ2w(ôŸ…pᤠõGTVêëÅeï{àùÂÌu:V¶Ý%öÕ¤2TE¥æJõ=Eåx h“)é5âÝA ôœÀÿùúƒòêźôÝÏçÏH9h)R )ù"YKU/žÿ1¤ó/*ç¾}ûàr¯Ü™Êï÷ÃëõÎûÜëõÂï÷gNIxÂjµÂãñ·A—˜T?D§³€^ûhûÎ…µ² ù¼“TO^üèyˆ?“gùžX1uvvâàÁƒòºè] jnS¡‰JiÞ…ŸS*hOü9„¾W^õzss3Ün7éD%ö|ß´iSAmhmm…ÝnG[[ìvû´7Ÿ¶¶¶”ÇãI_ºp¥*ÕÔš `Ö:AUÙÉ~Ù(fc4—´Ã¤¡ožßó÷ò1FèníÌëœI{m›xt(\ÿ›‰KxRXÞ‹(éõ»@7ç>7Î7ôÀë­ˆU,#á…ê%ð1ª.¿¢eú|¾NRSÚÔa°V·¢ ½:ÌÖãë[¿òû5û­Ë¢Ýße¨ 6Ïâ87o„0îhÂëƒáÚÛ #£)¯ßÅ{æœÎWÖÝ‚:¶OúÂçç;L~˜à™À)a‡)õŽg;Ìú¥¾ÂŒj½>¡ì°p@*Íeòö@oªDë羄-ðUX—4‘ºÈŸŽc¨79ï ÚSË=»ñ»ÿ ésDe£ çOž@8p3¯éÖ7¯ÂŠ ­0Y,¤Tmq­j±h’(æ%?ŒNÁôZ`4žI¥ç¨“?ýüê¹.Ya¨æû@ßñMu #ƒ–"Õ@‘’/“µ\õ’ï?!¾ÿ]€ )gkk+º»»§<åGn·ýýÙ½`¥½½=í!4¢â’ßï‡ËåR÷]ãÝ`n}Ð[RjrÈOžó”¼pòBºyFž…ä{bÅ”ÍÁØiàxsÈOž³5“A¸ü Ä+¯«– âí¡ôäv»±wïÞ¼¤eµZáp8àp8ÐÖÖFæîN§3ëyûƒ¿Ì:'è¥yª'²P”ûE__rP48wìeGÁ¬û3PkþçMoßt(\œ—8úÄÑwäíeÔ·€ÙôUE,Ò³#г£ª·`âÙAžjO? J`µj^¿Ï糓ZÒ®ð ƒ] ìOpxé'Kÿµó/kÕ>ÃòT·?Bl5åõáÆð8N(˜ÆÁ_ƒŠ%'9"#¸xõLÎi|eÝ:üÐñ{ñÁ@ß B±y°0*P v˜ù¡]ØaίP v€¡H!ŽOˆ$)$0+MÃ^Y•`¡0sË1UZ¥;¤ª…ÔÑ( ·‡IµÜ³-÷|+Zï"tˆÀ Å/>•›†úzqõÂGª‚:µË èé ¬µ&UÓàc1<ŸŸü`˜ùïkˆ„8„ƒ1Rá èµïvâô[ò¶Hèj̓ÊC -D¨‘â$_$]Õ‡}ßÝiü’bQZ­V¸ÝntttÁºòx<èìì”ýFâ¡£´Ú€š‡è¨ª• 7<ª¶%ùàF ‡üä¹Éà¡°Ê xø$´ÞÖ% õ³|O'ú \ø1$ÿYUrEæh¥§l<¿dªææfttt ££ƒ Éï÷£««Kµ:ºi˜U»Êû¸dO DöäP6íp |ÏßCòŸ“7Þ4î]ÓRðùS~Ó ìï=kI[e/…¯@ù6]tw[¹g/†!r´¤üwoSÎXQg&)ªø¿0ŽÖ²‰›|>Ÿ—Ô”6E€‡u¬ÀzVRÀÓ¿ÿÀ.|ï¯4;`Þõ‡0­º“T”FÅà #8Í{Ú¦ÞW“~¾TŒàÚÀo⹜âÃÛP% —†ƒ³|ZØ!þ¹<Øaö=Z€â÷hv^q%4QBFàMQh4[`dä;¤°‡šA9Á ëè’~žoS².YŽÍ|-÷|¦Êj2@k@¡A ]ŒC n(=ð¨ÜòáÆµALܼ çÊ:.ÎKM ,¶X/Á¢FÒòº¦´a4éT‹_’$Ä"‘¼äÅhž¿+ð"7¢äГBúÑŸ~×ä„¡?åU§€ÓL8h)R )ù"¹øª~î«ü& ~ø¤Á_(šÊž={àr¹È›„ $§Ó‰C‡e|ÿåË—‰gŽ"—ê‡ètÐkmß™|2PçIC½gð Nzo8¡ÞÁ†‚eU­Õu°éM°›«a¯°N~V›Þ8Ý™lz#Úªê“î#xLJáçfÜþyLJà¢ðóìôµžà°fò{ú­—qú­—ÑrÏnÜýçOЈ¨äe±ÕÂb«]ð¾h(6<‘ôšÑ\I<8h@¡@ : FG«³n¥(P4 IUÍ£K¾41#ž”\Ø@n°´©Š”|‘\\U¿€1úJЛ¾iq+Ä÷ŸR,ÕC‡Áãñ »»mmm ʯÜn÷t=,¤ýû÷ءȥºW‡ÆßsëŸzËü1¥Ø­È!û<ó!Y1´··“ÎYÎs!ÙÉŒ–Ü¿ IDATIÅ‘oI}è†vЋ·@¸ü Ä+¯+žÒ‘#G`·Û‰·‡šóµµµå4hnnFgg'œN'aTP6ÀZÆÒ™Á¬Ú ºéód?€ì”F}À¡ Á©ÊfùI… , Å6O@­ytHÚ~ÍË!…彨R» 8ð èmô6ÐB ͇@ QÐiò"R:H´m€Dë!0HŒEŽ ç"¾b Dƒt, UÉ"LÃ"ÀC–: ¶ÀZR3:ñÒO–F'Bk´jŸ¡©…T’F¥×3XÚhÅ’†jÜ áæ!¿ 3‡Ïb4GØá_<¼î–øÄG’pþúø ì@-;Ì‚¦ÿK$†OöYò8¨4 Â¬»æP/FØ!Íá}๲!ž+Ì$314hŠBÅäá/*i¤Žwv9#E$χz°C¶Ê'ì0u‹€&ÉÔ¤³¥Î[Öc¯ç—8ÒwYõñ`ê ¶ÑR…uÛî#ðƒÂš¾+qÀáƒe“÷Öê:ØÍV´YëÑ< 6´%@ éö¨´{mÕ‰ „cQSÒ{û#ãð†á Ã;> ÏØ<[°29ýÖËøø?ßÀ–Áæ!àQÙËd±¨Aã’$ ~Õµ&P4¥J:½«îØÌèõó> ÇÀÇRÉ ÎydÏí«³h”)'Zê9 ž‡5Rœä‹äâ«zùQM÷€®^ ñÝ. 2¤ˆýýýØ´iöïß®®.2€çY™@ûöí#uSÄRÝ«CE=˜û@Õ¶$Ž-Ä›C~ò­ÐaêJ°tºÒ]0©"ðæPˆ5ƒÎ fíàë6C¸ðcHÊm@»ví"ÞJ@6› ÝÝÝØ´i“ì°ÍÍÍèêê‚Óé$©‚¼^/œN'zzzT‰Ÿ^ÚfÐ)´ß+Õ‚·æ™%ò\Ï«©¤ªU$´mäîÜKÑa@`ƨ‘*# ƒâ‘I.ûYÊx¸4;Ti‰"cŠ{f0Ö%ö3! H"D™L’ò ¶v#*|okÕa‡dÀƒžëíl#^½çóð\»Š½ž_¢Bý·ÿ³¡`ü°¢uë4Ae/¬Ù^†zÏ`¸÷ CWÊ"ïV½mÖ:85¡yph³Ö'_ÿËÚˆ RwÅ ã±WXa7Yѱd†Ûí‹à„glž±ôGÆó»˜ ñ›¿‡“?ý|æÑ'ÑrÏH"""Ò´x^ÄD0†*«Q¥uF§ƒÀóªÄÏèõóæèl„G4Ì‘ÊUP=Çå²eð^ 8h)R )ù"¹øª>wƒ(ëj0Žç ¾ÛéÆŠYvàÀx<¸ÝnâI Ïr»Ýp:p¹\ðz½èïïGss3œN')¤"•Ú^è5^ûP|hÑØÁ{õ£)ðìb.o¢òœÉNŽ@ÏÍl Ûòmˆƒ¯Cè{àÊZE¼=”†ÚÚÚ°ÿþŒ½477Ãår‘:WQ.— ]]]”³0UÙ fTÍmdO€ì o}OÚªj”i1¤è¨Õ¯Ú€­[±¢õ.¬hÝZÖÄ\°!àÄð¥³e“ÿi¸aqZ'Á{Euò–.e¸?@ÏúL'Ô,Ø!]ˆ“÷‰Éï¥@-¸1åù¡{øŽ ¤}æÑ'±¢õ.2"""Ò´ :Õ I’À±,$QTvmÌ0ÐmæbÆÇ¢¤B•ÞCÚÿǸpüMyõ³ñ/@­y0Í\@ˆTEJ¾H.®ªW×ñü‹Î¿¨ìÇj…Ëå"o‘%"ÊQ]]]j”+ªövÐ;A™ ñ^.9h¹¼ù·u{{;<é° ©»»»ví’·X¹̪/¶­kt™¶T¸~–6¦èÄ ?†8zJ•"ÞŠ_N§3­g/âÑA}ùý~8N9rDùÈufÐMŸ³j7Ù û €C™µÃ<‹ðÑ É;L×o]¿­€UH¼:(™¤ñ²O·VýÈ%; ³õqPUdòR²žÿ!t‘a­š`÷ù|~RSÚ2Ô$ìp; )ùzí©gÖþü)× Zµ¯b½–; U¤R|ЃîæGX/NàÔ¥Ü6‚â?äÁ)¯'Ä9FMØa–õ…†’Ù$ýôá´;°‚_8 nò0[²2¨Ôë±Ôl)+Øaz:åí¾à8;ñ6Žô].ø8U¿j–¬¹ õ«oÃ’Õ·—ÜAíÀÐ ¾+êýìÄ8zN :(+°aJ­Ö:´YëÑ^ׄ¶êøïé×üI€-ÅÿéD@@/F‡Z3}²ôÌ?žÅ3ãóÔϱè¾€î¡ 82|!¯eÝrÏnÜýçO*DDDšV1AMCo4&ÌÓ¹˜€àMdoHy=÷GwÊönEßýCPÖ5sžõrÐ@¤(RòerqU}~‘F{ žì¸¢ñ’uDDÙÉëõÂét¢§§GùÈufнìîâÛŠ r˜L[*`ÚrCD†!¼ýg²ÂìÙ³n·›t\…äñx°}ûvyk€àA*P+GÈAÝ|‹£§ œ{Nqo@N%ÞŠ¬p¹\ î›››át:ÑÙÙIæß*—}GG‡*^èº- ×9A™êÈ~Ù(Áj íP Å"úþÂù–†²4YùPì% ƒâ‘I.û $ ¼),óû’õ»@7;Ȧ„eû•?Ó²‰{}>Ÿ›Ô”¶¤#E°°ì°°Þ{õßwhz€\u'©¤"ÍPXTW‰šZ nŽ…0:<‘û´ÌhC%âÜ@n_<ý‘ Ø*ÀXv@a`™§ç3\ÍÏR¤— ì"ú„BRvq†"ˆ“̨&Np<³NWV°ˆTü€#1óî´WUãÕ{>ϵ«8ðÞ»8výjÁÆ©áKg'ÿ¿<ýYýª °64aÉê8aª´j„ê=vb|lœCƒ²ê•’¦½7Ô5¡}qÚ¬õ°é™¯ñ)ÄF "`ãgÓq²Ý[0&‚ÓA£40¡£ŽN§Mo„³ñv8o‡Ÿcá¾zî«¢'¨>iú­—ññ¾Ï<ú$Zîù™iR±   ô@QôF£"ЭÓAo0$|ÆFxLXR‰*(:1žÕŠª^3ùL&C#ÔHq’/’‹¯ê gµ¸Ìg^„øn¤(ï‘#G`·ÛÉ:""RÕ«Cý`Z:}¾¿¦" CÑ”uoE´Ûí¤ã–Ó<‚@yË7½x3è»þÂåW ^y]ѸvíÚ…}ûö¡««‹Ž/B98R¥2OÓ™Áløèº-dO l÷äP6í°ÀEBU6Ë79:L@M?¥:L·_órÙÀƒ4v ÀCI+f]§u;¸IMiKxX@vÈLcW®~J³¼¦ºââ¨4>TÛ*0:ù"¹øª^Cé+Ao{ âù!Q±h§ÔoDDéÕ××§Ó‰cÇŽ)yE=˜–} j[Šo|+*ÐA*`òÅõfY¢bŸŽ•#è &›ŒÌš‡A/Þ á£ç EGMæàÁƒèîî†Ûí&‡ç‰ˆÒÈï÷£££C•yÝ´̪݀Î"Œ {E>ç!CÙ´C  UÙ èÌò‡§ÞÕ øLœHøITµNyo¨[Ž6[=ìæêYkx*}se¤IÀA*Å{D÷âš @ÜÔƒŠ0@ŒNz{[u=Ü-;à_Ï¢{èºzƒþȸª}æGúYüîÃ-hxˆ.8 è`,=ç€éÉ0oøÚcÁÏðP㊵†oîïÀÔ˜]M$àÁf"à!‡^@¬@•Da…>˜¿×®¶Éû úZ¨’j\Í46zpýÚ –näwßæ‡‚Å7ÿ"J¢äô~òo>‹ÞÆFȆXaØ!0°ýÀ=Ó9øßsôµçtï#WU€„äÛÄ™(dü 0@`[%¨F^·2>ÃÓ…¥ãŒs`Û0¦G °p¹ ØÁ#J9Ú€qØÁ˜ª ;07>šþÁ¾ø0ùþN¾Äéé÷hÀ¬#Mƒš;Ðçk*ný.`<é¹Á£&ÿ7]6óîPH)ø!!a) ?èØ3Ô±Cû1¹8‹Ñ‹¯T | o$É%)ðú]p7TnÛ…1&ö&‘ˆ«X[MW‡*ª/^¬­Ú‡RÈ‹Cõ̧/’WõÜùÕ û!ÜýuðK?‚vö[¦&yæÌ `tt###4è“êR“““r WnîXGÞª“ï ÚLÀC­NÍr°f¾Ï ÚÁü½îøOñö055…Ûo¿ÇÇèè(õ R]kdd'Ož45Næï…xë¿kì£=šÞ OuÓ¹3ó%B3ÈMòȬû µêÑA/ÖP ðpè¤ÉM+Þ|îÅ7íjÞ纺ºúæçç§©¦ì!tôb>{¨$ 믿ü佚ªúíjŸ{ï]TIu"AdèèjBSsæß#Õ÷翟®†JNç/¢¿µ0{#‚¸’v–°ípº.ì Kl]Ûvø¤{{øü‰oÃão¢Ž@"‘l%MåX ǰ¶¯8øK:X£«¿Y²ðvUén]pûNt*a>}‘쬪ç5[ lÏ#ÚCû—Q`ݼ7ú…Ãa<ýôÓ§7 “êN•8@¬q7„CO5í±và¨Ht9ÔÛ‰dEãv1¤Šm›[˜=^–=bߣºîKz{0ø–æB:qâĦ·‚šHõ¦P(„¡¡!œ9sÆÔxu½:Ð~íØÚ\j‡Î*–â cƒÆ£O,j Ý6ë3;Ôì$=<à†Á(/Чoþ éåaŒjÊ"àA_ûˆT …5ý«à}v¶ÏÝs˜*©ÎäiÑ·· K7"¸qmªº5ƒºcí"~zé_KŽûñð;>XXŽ"I˜Î<€ŸíAa *ȆrÂ@ÖïåÃÌ:ر$Ü € ‚ 9€ÝkÆ`äŠ'|‘íÆ%ó•2R—9çÐ’0CÛÊiMQ²ê_×R棽ÁYò„+^Ì¬ÎÆ*I™qò ðåú›ðüÝ÷âù»ïÅø»¿Æ©wÞÆ™æhàtše7íØÑŽþ@;:pC†$p@Þª}ÔÁ°ÃvùpŸ(ØuÍ&Eª>̾ñ¾ùøxøÙ1¸ç!ê$$ÉvJ‘å8Ü Ü $¹rtªhˆÇTD×ÐT:%e…f§^5>C¨¨w‚ªg>}™ì¼ªç5Z Ûö)šöB<úMhïþ ø¥M5+õ&á§žz £££ô Õ¬‚Á †‡‡155ezÜÂ¾Ç ì{Ìþcœ£<:p “·tàKoCãv-LÏêt¨R¾yeÓežvHÿ´+ÿêô €²fZÜ)\ccc¦>Cª MOOchhÈÔ¹ó´C¼õI°·Ñž@Í-Á t¨›vÈk/_Ì¿ |՘׾6 Ö¸ß&}†@ëú5·´i3oñ¸”uð•9°ÆnšìÔ°âÍÀE7˜³«‰Ã àÁ6bœÓàéz±›즒(Nÿ¡{Ïÿ²«‡WÏ!4ý2UR+‘P1?ÆZ$?ìü—ÿŒ7—J{›ÝáÖVüË翯Ç1s=’D7ÿIƒŠ„rÞψ3¦’°CšõfÁÂàÀD1ãy="èÙ “~þp:6€X.{²Â±íAÎÁUàLÓ¶–iÍ•HÑ è¡XàA` 7y½ðJ’NÛ1îÝÁØQë<ÞJs,‘ÿ.+ÅÆ<õ œ8ü>½²ŒSï¾ñw~™Õ8m¤fÙílïAÿŽ$ØÐçk*¼çÛ½¸7¸´äÿÖM»m»ç`š ×]`«¹yêÉÅYŒ¼ý3L­,TÄÔ;ù>ùä ê<$ÉöDÉ%Âå!JBY×8âqJ\ƒW¡(°Åzék#xó'?46K¸å ‡-z€WcB€Cd¸ª¾>‡¼ŸœZðë@"bþ®¹™Õ‘jVãããA867ↈ·ÿ‘É^¬öæ`åxëPÈ¡ìàÉÀêëÿ|ɘ·1úŽØ\MNNâþûï7¶>Üý›É·†j#9ÔÚÜŸG¯UÄÛ|îsŸÃøø8N¤šV0Äàà ©s5ᦣ ¢öjb N€CÝ´C^ûýK½ð=hs?66¦µ~B×(õæÑA·ý^þïàkWŒµß[Ð;Hž—oîïà¹ö ;›¸{~~~šjÊz‘‡‡4½€˜`•Dqúûâgì ;€k'yw¨wɲˆ}-XºÁ=¯?,vhv¹ðÃ? ˆ+Z^Ø€PvÐ;únìÀXnÄM›·B·vÈù·>ì ÷ᜰÃFý0IÚú›sp®Z&! Q;¸E7y½A§íì·^7W>Ðrƒ}M8þÑ;qü£w"xý:N½ûkLL_"ø¡Êêõ5a`G;í8Ú±}¾¦-¸hO.pÛéÀgÀÐo‹ç[v!x÷0ÆçÞÂÈÛ°b.¡ÿú‹…Ù©WñèŸ|Í;©c‘H$ÛJS9âë âë[À$—1ˆiðƒ(2"ƒ¦q¨ÊÖ ¬ª4•CMht@ɆšzÍøL¡½\äÅ¡zæÓÉΪz^ÃÕPâ[»î†øÀw¡ýâø7L5?ãØ±cÇèè(é¡@r¼B¡†‡‡qúôiÓãzÿ-„}¿ È>ûŒy|c ÃQÃFÚu 9ê@u:Xy8«zå]Io§OŸÆÀÀÆÇÇi~FªIMNNbhhÈ<ØAòB<øï!´Œöh?À†æR;tV±TÆ0æï5nItÁBÛ t°®ŸsÛ5uÖÐcxà‹j^±–Ãv†@^l!2µ€HÅPœæÎž»×®¶1¹ž=wQ%‘a7ð?ßy¥äðyô~ô66‚sŽK×’‡ ua ÓËB!ØÅÁ,ßýb`†JÂE@È”;èDVì;Þ­B`¹>žv@B``ÀRO“ ^— «‰r™f§_–ÑÙÐéy !Ø!_;Ó½‘05÷”ZÛ0pä^<ä^o\Çäs8õî¯1uã: ¤&êhGv´oBƒ=[•fn6¼7¸íü6ë:ÒU|îþ0†:÷clúuœ¸øŠ©f/\:‡oÿÞƒxøÙ1¸ç!êp$É1Râjò—•…“]]FøêÃáX[)ÀAÕ3Ÿ¾LvVÕäPP²ÂÝ_¿ô"´w¿gº·‡3gÎàþûïÇO<ÑÑQôõõÑ‚äHƒA affÆÜˆ: z ¬å}Æ<^N”:T7xîÀ|å=ê¸59M#oÕÍžµsI¡ç Lz{X5-Þ™™Üÿý8~ü8FGG©‘jFããã8vì˜iñ1/ÄÃæi§ýÚ°‘¹ÔU,•7Œ·*rÙ¢<Øv¨ö¼•¼:d¶_o7pÃ`‹hÒSR:¡¹š!ÄÃv5q<ØBC‘D"‘Höš¹~ IDATUÓìÔ«Æg Í{«÷vâ!$Kæ<ôE²óªž×h5T6_lÏ#»î†üºéÞàÔ©S˜˜˜ÀÈÈFFFèAArŒÆÆÆðôÓO›¯y^¬>èQC™ù®†7ƒo„?zô(uv[Ït¨nöì³)Ëü½îøOP§_€6ý#SS8qâ&&&011AP*Éñ2vv~âþá¾X‹û98Ë\Ú›rV‘T×8æió´G½´‘GfÁ|»ª”òê@ C޹®·ÇxZÊ:øÊXc7M€j\ñævöòÐßÕÕÕ7???M5e­xØÒ.*‚âõÒsc÷ÙÙ>wÏaª$àÞK¯à[Ó¿.mPðûñõ#w–£­Å‹†2z¿3VлA¡ƒã,ïùnVÀ†Nš3&‰`’\ðl¹!ïù`‡Bq³Â°CÖVŒÍÅÃl[¢L¿è]>>ˆD hÉÛœà* j´7xÐ$»tÓ³v`†’.|ר!ã¶¶aY~¸ÜêÛƒ¡¾=€é•eo\Ç™æ6ÿ¯W5Ën ìhÇ@K;²G;ËR$r@ÒyãǪÎa‡ÍÕ†ÞlÞ (™åÒ×ÐŒÉ=†‰…ó~óe„ó^mþóï}W/žÅÃÏŽÁão¢I‰D"‘*®…‹gÊëݼ8TÏ|ú"ÙyUOƒ)òvVÔÛC8Ɖ'066†‘‘z£0Éö …BÆéÓ§M^{!ëü¸õã·<‚òÒušGîIJ&YÛĸŒ¨rÓäfϾ²bߣÚî0ÝÛÃÔÔ066†ááaê$GÊTØAòB<ø$„öÕà³— g™K{SÎ*kcþ^ãÀCt!ð@ ƒ©‘èP0óvƒ¯;cÃÏðPе¶3ðC /–‹€w‡R4wöœm½;0¹®<¯šÀ«ÿðÿ–þ/Ñìr!®h˜¯—åÙAÏ{Bz˜ÌÏ”æÙaë¿|žÒ>¥sFœ±ôX6þa ‚$’Ærƒú×Ê„òÀ#ÅÂÙa Û£›“2ap‹"z±c5‘Ø'Á#Jhv¹ s¨*ÀyÙ°ƒÁ‘3¶†Š·3çítøAÍ?@_cú›6˜ü`Ó+˘ºqÁ×¼q áx¼&ÆÒ^_#úüMØÑŽ€Ëþÿ;7è{ÎJ«DÆ“Þd-ù#ÔDqaMQ0¬+ 4ÎUU(š†„¦A4HZ=ÈÛ=äÔ"ì±êÐÀ{ÖˆvÃlcY†:öcú¾¯`ø­—qzá¼iÉžõÇøþ3âá?CçÞÛhrD"‘H¤Šjvê5ãÓ¢À>sÈ90Ÿ¾HvVÕó®ëóVio)ða||£££t¸ŽdKƒA affÆÜþÕqÄC#exu0aŒà¥k–á9µt–:1Íß*Ÿ6w`ž¹Ë›g˜¯ÒG7¼=̘çí!ãØ±c˜˜˜Àøø8yâ"9JfÂÌß ñÖ'Áü}ô<¡ý Ì¥})g‹½ cÍëÿj,‘Ë@ëÊÖõç€›í·¡§àáÐ;H¡—ÒÐ ¥¡Òú‚]M–‹€‡¤n¦"(^ï½þKÿòÂ5Û®‡¨’H€NýøÖbiÁß½å î»éfp—®­¤Á ¿ ˜AÇe€£`I˜$nܯ ì º8Ø!GÜ,=(Ξ¬ª3vH}”1†€Ûƒ€Ç“ãÉ,a|PU°b ë§PÀÊÀæ…ÑW=QtzâüÀ4ÎYQ‹·Á›º›2‰òP<†àë˜^YÆÌÊ ¦WV0½ºœü{uÅãdÿŽ6\n\n ´´o\K )À¡àš´Ð÷3 I¨Dä€Ä“pC h5‘@LU‘Ø€bª ó¼-.¡iHlO;ý~¸E±þÎ>ܧ×]`«™K‘€äÆÄÀ#˜\œÅðÙ—1³¾lJ’ —ÎáûÏ<ŠGO|»úЉD"‘HÓì%m È¡šæÓ—ÉΪz‚ªª {{€™™;v £££>l¥±±1<ýôÓæF*y!ìûm}Ÿµn t2èà4È¡ìàÕÏðàà uþºï7kË›¶]ì{BàV¨ï|ËðÛœóéôéÓÀÄĨ;’l/3aᦣ÷?H>‡— í8Ë\Ú—rV±Ø·¾Xà ñܬÍV(o;X78væínü³±/ÐD¨Nk9 iî§v5¯¿«««o~~~šjÊ:ðT'Añzé¹±ûìlŸgÏ]TI$´Æ#ø§©Ÿ•Ö'IøÃÛó¡u$-ëT¼ØA×óCZ`=Ø!c²—ö[!غñ„“·@‡Ô5}›r]+;ä¹V ì Ÿ"òÂ(¾`ÛK݇ ä —Ïl¯ I“d€oÁàÀ[â–”Y2—¡/’Wõ¼F«Á9ùª´·` |ÃØØº%Y¦P(„ááaœ>}ÚÜ~Ô¸ÂGþ¬Á¨cq“Æ kÒ5ÃpJS 5RÏ! t°]y³ÀÁ¤·‡ ß…võM—Ý~ûí8~ü8FGG©›l+Sa‡Ý_€¸û7iOÀvKU‚ê¦ò:é_4ùz‡Ucà‰˜Ü\ݲªeÐÁ²½Ag‚›óZoq[”uð•9°ÆnšÕ¸âÍ‚ϾÀŒlü,RÝ/ Ö@¤¦P¼æÎž³­wÁ×¹s?U ·¾õc|kéZIaŸé@“Ë…õ¸Š+±´Cí%Âz¿€2=Jl¤^ìÀʃ ‰`r棡"°Ó/Ÿba†ü6éÁ9?S6ì ãs g¸b`‡mñ²$üI4- >hÚü`ìÀòß.QúÆ™ãÝm«ÑôcÙl KÍÒnhHBÅ.Òu¶¦¼Bð´ß‹Y…ô®3®O. éPôÖ jlÐSBÓpyu5Í“Cé­MãK±ZÝë3fÕޤߖe°E9£í¥¼=L,œÇð[/#¬ÄLIò¥¯=Ù©Wñð³äiD"‘HæêêÅ·ŒÏoÛ ¼A“¼80Ÿ¾HvVÕ“[*åíaþUhÁ¯Wì íÔÔî¿ÿ~|îsŸÃøø8=PHUS0ÄÐÐfffLWØ÷„}U¬ oÕͻͼ9ð•÷¨S“Êoo9T'ßå˜&y!Þò°¶; ¾ó€²fšÕ'NœÀää$&&&hNF²&&&Ì$/ÄýO@¸iöh? ‚æÒ¾”³Š…;¶èYóAðð¯E¹ h®N™‘G‡ ælÐ!£ýz»Á׿Œ¥²xž€‡:æj†ÒÐi}Á®&€KE€V*‚âõÞë¿ô//\³-ðàÞy˜*‰„«×Jöîp¨µwwu¡ÙåÂùù弇Ú7n†Ò`Û¢A–Á„mÙsì;Þ­BpìPL„äæõA3G1i³Ãå»ëdØ!g}è,Þ´Ô%æÌý¬t¨!é­!嵄Ѩ)°CJk ­n‹3e‡ªmL€7¨`×]@4óA4Ô±Ó÷}C¿úÎ,]6%¹7òCDW—ñð³cðø›ªžÝ«Ï"¶ºœö÷[—4ö7¡sý}uZ‰Dª¢JñðPx ÈÁ€ùôe²óªž<98b‰Øu7ľ íÝï_š¨X:§OŸF__ÆÇÇ144DRÅ5>>Ž‘‘„Ãaó"mè€xè)°–CÕ/ :p ͰêM´ö{>wZW=‚ÜÞåm¢yBÛ·B9û ðЯM‹÷Ì™34'#ÙNÁ`ÃÃÃåG$y!}ä8˜¿öh? BæÒÞ”³Š„;¾ØYÀ8ð€èBåËŒ@‡ V; Ãfûmè)x¸ôÒ:¶k9 ɾ^z»ººæççƒTSÖ¨®‡“´P3(^/=7vŸís﹋*‰„Þ_ÿ /–èÝá+·&·ÖW“Éõ€†-(¡2°Cú•ŠÂLsÉ!û|za½Š…tË5­rAÎ; `^ ÞØ§¶ÛSD(Q$1ééAUÁTÅŸÔg¹í#Ø¡< ywZÚ­”g…jí¥°m’¯+ åj97©µÙDÜF6HxWš·‡4$7&?öF/¾‚_1%Ùó¯þßæQüös/˜=DW—±pñ,`vêU™`Ãì¯YVÄn_ã& ѱ÷6xüMÿ7oþM"‘H¤ò•zR`ožÏ8Ôí‡*™ZmQöA¸í+à;ÿÕsàË—*’b8Æ#<‚ãÇctt”,¤Š( add§N25^ÖqÄC#€ì«Î¸ádo–™RÛ .P'¼9ذ¼+išä…ÔÿÇЮü/¨3?2ÍÛCjNöÔSOalŒ¼ä’¬Ÿ» — ©2/Ä[Ÿtì@€ƒ³Ì¥½)g ¯¹bgþ^ãÉD.WÖÀZ†,Û/¬=ØHÀ?KqñMŽêDñæÁg_à†A^,S½{xh¦&`LsgÏÙÖ»ƒàk´ƒ\Õ»ZãœyëK û`Oö65AÕ8nDbÉIVØ!סñ\0ƒÞaÿ\°Cê~¥`0€‰ ˶„F)°Ê‚Cé凲î°g,šÅó<Ô‰b-‡í <ôvuu ÌÏÏ©¦ª/òð@*Z/=7vŸís﹋*©ÎÕàWÁŸ”ö‘Ý{à—eÄ*¢ŠšVØü=ûðu&ìû@ÿök¹€‰¬_ Á ÛïC'Ap¹Á¦ï¡¤³»Žé³ÜXᨠÂ( ; 至ì7bQLþpžÔ-¯΃Š·³fa’å’cÐ6Ly°ƒ[ÑìrU?#v‡Òµ#Þ¨‚½ïIz8ÙP@rcòcaôâ+8qñ•òË‘|÷?ütßzÂó³%½¡»&76ì`ľ¢ÂGOì@ª´v¸Ý¸ ØáfŸ/‡×»l^ØÄIßµ6ï¢Bƭѽ÷` ±Ão½Œ°+Ë,5ÇìÔ«ÔÈ (|õ ÂW¯àü«É7Ź}8pÏCØÕ7võ!$©n5;õšñ9o[¿C敘ÐÉΫz^£Õ@mÑp0o'„#_¿ñøÙo/_2ÕòááaLNNÒƒ†T’B¡†‡‡qúôió"•¼~B÷•?r¨nþëÈ›Ãvõ÷÷Ó QËÏsòæPùo­ßv Õ¢Z=øåÒ¼24ˆü’Œ®/z! Už†;vHÚoId¯";öcòc¡·¡‰¨ŠEVðæO~ˆ—¾ö4¾ùøÇñíß{?ÿîs¸ZÊ›ÎI$ÉÁJ÷†Sô|´h<íÇÊi¾ÃóUú²UqgÕ™!S©-šŒµ†pߟCèhè0-gΜA0HžÁIÆ 188h*ìÀwC¼ó«•J†¬»¸ÅCg™cwY¶[qޤç^®Ÿ6__0%½¡½Ÿç¦OiªÐÖs&aÕSDº3Íäˆ%/ÄÛž†¸÷q@òšíÔÔ1>>NÝ“TQ•ÓÆ¬ƒL\7Ûj îàý^CùªV}Ù¶HP_%™W~¾˜o—qS#—K›‡õ1ë¨Ús[n…aUjÓ%·Í2Ò+".æí1·²¾2R}Èæ^z»ººˆ·@õ <ø¨ú‹×KÏÝggû<· R%Õ¹–çðâôÛÆIÂPßnp Ëë[‡ÏÂz¿;0ã°3;0&Ëd åÀ %xv`éáX¦},GÜl«¬ÓKk%G\UsœjO^Ô4Žåõ8"±D¶-È.Gd•¿NÞ;䪖ôøý8à@s;ý~Üìó¡ÉåªÏ‚3c¯¦)ÞÍêô9†þÆj káÒ9üü{ßÀw¾ò)|óñ»ðÓ??Nð‰Dªy•4ÎÉ>ÀÛUàÁIC dØÒ¨ÌÍ[­UµÅJÛù ľ›$s¶ßé@ɨ&&&088ˆ©©)ÓâdÝ¿ñί‚5í1·ã”u Æ‚1&uèÞrСÚÁ-*÷èP(Ýè5ÃQðà”gºÓ5«@‹Ç·Z¶Iè~Òá?.é0d.…Ãa;v ÃÃÃ…BÔ]I¦+ áÔ©S¥ÍÙü½÷?áÌu3íTy? ŽçŽÚ"±y}•\væå‹ù{K‚yt:X9~Ô0èÀ Ä%¸ÁÜmÆ“Y¾B¦:‘ͦZª¾¤:Î;½Ö€æÎž³­wiG7D_ URëæwþóÑ5ÃáŽtuÁ/ËX%²½3¤ýQvHƒŒÀéW*;¸\ÄLì ÏÛB†1àÕ!¥hB¢i9mÙÏz\c€Ï%ì«M§_Åä瀢š†t×¥Àæ•ÃÈ»©ÜvÙêñ ÕãABÓ q·(æ_ØçÐj"‘1¾…ⱬτbñ¼é…ѳÁ/ËòÀMm †G”à‘Ä´ûîò67L›øià½k`WekœHn cø­—qêý·¨©Ú@á«Wðú‹…×_ü+4wö`ÿÝác~ Í;©pH$RMivêUã3ж >,-¯Æ$À¶vžT›™d*µÅjÛù Ä®#ÐÞ›¿ô" DJŽkbbcccôÀ!¥ÑÑQœ8qÂÄ5¨ÂÁ/§yuàu] =9X>Œr ƒs ³\Ù´è%„Ž~®s›õ3G®¸…ÅRÝòfþ^Hýÿ'Ô™ ÍýØ´$N:…`0ˆññqSH¦jrr²´5ˆ§ÒGŽ›^×ßž€ÃÖͼFóUúâ5š/[›WÙ| Í·@»ñKcEfÁ<í&˜m×1Ñ̹w^çU¶‹—óö€Ç®KnñÐ}Mšêa•,zo>Wø]»š8`„jªº’(ï¤Bzïõ_ú—®ÙxpX½kg4„_L—v¸ññýÀÜXÝ:Pʬ$h’eðCúb`‡Ô!üJÁÌØÁ°g”;lÅžu-¡;¤´Sà’DÈ¢ó“yÓ× ì°Ý@YNš( XN*À|Ø!gí˜ìÝ`RéÊnmÛ½¼û7%E333ƒééiôõõÑC‡”{M add¤ä·ëî4î†pè)°¦Ý ÐÁaϨ:ƒøyV¬‹ç;A-ç*ç9W2’âÞÇÁ¡¾ó_e͔䦦¦088ˆ±±1 SW&™¢`0XR8ñðTv ýg™K{Î*^£E_å|ù{£ÀCtÁ„,p›6nbzµ;X:¤Äº%cÏ~¾t&Lu$›½]]]óóóAª©ê©žýû¨ú‹ÓKÏÝggû\;S%Õ¹îYšÆ^xßp¸#]èlð"¦¨[“©ôÿ·YÐ 3èø·v`v Îbõ!ÆU×»Ëo3V¢q´ø<ú°CÁ|Ôì°ÝXQE€k€¢nóú@°©Å hIXA᫉$$—ò¾¼®Y²gažI!8"@HŒÁ/É›Þ"L‡!Úâà.¶(g\é½Ùƒco½LM׆Z¸t/}íi¸}ÿîyŸøâ3äõD"9Z³S¯ŸY´õ;3³9Øn^VC™d*µE[G |èyÚë'À—/Ž" ð@Ê©ééi ajjʼ]€î߀xË—ÙgA÷­gÈ¡L :X“.½½>§¤Ukë¶\7p‹Š…[œµÌ›BëGÁ>ògPÏ>™5%ùp8ŒcÇŽarrcccÔoIe©ÂMGÁü}öí¿:TÁ\ÚpV‘'3%4„†Y¹\FêÕ@sí2v@ÒÃj4àú"øú"XC Mœê@ñæv7qä塪ªKàáÄÈ»ƒÍ=g[ïÒŽnˆ>z€Õ»¢ïþVãoÌ~°§À–w‡|^˜ÎµígܙΡïB0Ëq¶;Ì™®ƒ`‡|‡÷óÀ e}éʲYÓ8b YÖ7w>*;”ªBQ¬) ±bª UãðÊ2|²Œf—«¼ô˜ÈBò‹7UT-ûK8‚H5¤”W†P<¾ p0øŒ©aØ¡)š–‰Qú IDAT„"8pÑŒ{I„G”p¹à%øeyÓ;DIjJ€‹ìº+#ÏÃ7}ž& _DX‰Qã¶¡b‘¼ù“âÍŸü»ÁŸÿÜó ‰DrÖZtuá«WŒÏ<äàÈù˜ãë‹ 8rГ·â‘ÿÊ?< ¬/  144D’nÛD86-Ná–ß…Ð÷Y º°… ƒ“!‡²ƒs‹²k^ºÜà˜ €$×Ù´ÔÚCVÜÞ…èDСÌÿÌÓé£uæGÐf^4ͬS§N! bbb‚@URYšžž6>Ûý›´Pwû´à¬b!O• 4(± žƒÉͲâPÐÁ²±…@‡¼ívó!îs·Ç®3cñ‡6õ Cñ„B!zè²4>>ŽcÇŽ™¡ä…xçWÁšvW±?Z|˜[”®åÑ8rÈP”€šXØÖm·~ È¡X‰½Ÿ‡Ð|ʹ1@Y3ÅÌ©©) `rr’<ÉJÖÌÌŒá9ó´Óž€“×Í9ÔhU“‡jŠùv÷Þ½HÍ+ t¨ª]&ytЋ˜y»K.5 <Ê5 LK€iqhRòÜ—&x I^p&ÖÕüÐæÀCoWWWßüüü4Íä«#òt@Ê«—ž»ÏÎö¹v¦JªsÝpùðöœápvï,®Æu'UZÚüŒ€6=B„X¦÷ˆÍx‘;^=Ø!-^&Ë`¢`ì°Á(vÐän»î¬oúÞrÙ£i@\Qá’D]G ÙöTλC©°CÒËÅ•ÕUĶÈŽ© bjî]‰Ç!2í ¥ÃÛï BòG’MSÕ’\¯³\Wv ™¤ȰšHlü®ôÔ@°C5mHÕSºn7.ü² — ’ äYÕhà½k`We«î;0yÇcøì¯^ÀåØŠeEâ“dìkNn†b1ôù›6ïͯ¯Á#–¶)á—dìk `~}mÚéó7!¸x ’ À/É83?ç˜~¾z/}íiüü»Ïá_|ûï~ž´²"‘H$»ivêUãìèÝ¡î óFC•L¥C N*VÀ@Öj|?7 ÒC‡”¡‘‘œ4w>GH$’­5;õšñYFó>{Ožœ=çrj}ä€z…2>ëí¤©ôub(„ááaœ>}Ú´8…Þ áà—ªÐ'É›ƒ5ÑÔ(äžÔÊ4 4ME}zs("m KòBêÿc¨3?‚6ó¢)Q†ÃáM/L=h?€öŒ™K{Î*^£Eï¼vȹËeôšÇN¬{ÌR¼ÝPF¼¾¾¾Öàüs£rìdƒ.$e’² Ul@¼¡\k{6'zìîåaH$»éêÅ·ŒÏ4¬òð@^œ=Ïrjàr¨¡X_(úãÓÓÓôÐ!! appSSSæD(y!ü„î*Ü7É›Cõ£¨}È¡=z”šk˜·wž t0,±÷óZ? õÜxÔœ¤;v @CCCÔUIEirrÒøÚ¤¡½²ý… ‡*˜KûÎ*òâ`G1_¯ñ¯]vÐøiæü@cÁ+:l}Ø æn7xèŸ/žëvîÙQÆU¸Ö.gyo04‡W×á^½ˆ¸¯šè©éy¢Í‡þ®®®¾ùùùišÑW^õ Ù³@x]ɘm±öszfÈã¹aûgRWô<7lýÇ2ÒÎöþÀ²mc ÌåÚ¸–Û»c…@ŠìégE`‡­Ø³?Ãr…apÉ"¢‘X EË™Ó ¨šQ¡S%°3ìëŠRtà\©ÅT~Y6¸ØÈÝX¾ÌKÛ¼>hÚ&üÀòWœAûòÕÁµ P,–<à®$6ºWaYiÛ} ÓmXKËwœi,¯-Ü£Ÿ[ÄÒ q³]¬¬@bnÚÜ hóx µÆÁ°•­åN¯§ ¿¾çKøð«Uµôö¤rž;¤Ôìra 5ùåÊàMÝÉ•mk{Æ0ØÕ]~]rVTé@Ä™ù¤Ç‡àâ5„â1¯eÀ zš]nù'ØÕ„I‰d¹®^<‹XÄ ÇÙx»l0G /’1Çsj}䂊ˆËÛ nx˜™™¡O½ï˜ ;4t@¼ýÀšvW¨’7ëÆ@nQÖ­©s¾t–ˆºT=zs È¡*ó=/¤üÔwþÚ_šçðð0&''100@]—TyÚi?ÀqæÒ^€³Š…¼88AÌÓæi3 -òÈe0ßNG›¶ŸZÒ.³†î€‡ €ƒWd¢Vþ >p¸"Ó5=Ä›ØÝÄ!—‡ªH¢" åÒÜÙs¶õî íè†èk¡Jªs%–¯!±|Íp¸#P4 ªªmθ¶Ãk‡ÌsÁ F`‡Ô!|ÓaÌ%ƒ Ûb)vØþ ³`½HŒÂ©2Í n,F¢Ð4ž#ܶÐ*šDè%l'ØA/DBÓ ,”’[ ,6rÜ)ÎCÚ¬zÃë瀦)Jš×‡Ò`‡˜¢BÑ4ĕ܇©EApŒAÈËê${)ªª›€C(«Üà„”4ï,Ûz êxpÙðû8‹`>É™Äŵ%¬*&–·Á½.·]nŸî½¦ŽmÀ­È¹À×ÖÛ*Šë<ŠëÑ(.7Ú<´)MhXßZØ÷zšpù¾'qÇ?ŸÂŵPYYmó4àÛG@Ï–öòËÅ$ØúüM›$»z²î§À‡éÕ̬.crþ ‚K[ D8Ãß}0›õÑÎnt5ø, .Ã÷ÿà ØuøþÃçÑܹ“Æ ‰d™.?\Æš÷UÞ0òä`y4¶Ë—íL¥ƒ N*z›É1 v`-†xû%ADÓû'ys°¦¸ê rÈ̳qèÐ1Í{'AÞª­Åó/É ñ¶°¹C½ø·eG‡144„`0ˆ@ @ݘT×ClÍ®› t¨Ñª&ONk‡Ì·Ë8ð°vÌ×cÓfC°CUí²vÖЄŒíû𥠎í±rtÞØ!¥ôkÜÎÄÚåDݽ< ƒ€‡ê,Y©H¹´zcñv»ÚæÞsU ‘ËÆ˜ìilBgƒ+ÑxÚŒ+ûà¸Êyn˜AçÀ¿aØ™÷uã-;Ș(l¦n9ìP(î"`‡¬Ð:|[+EùóaØÈ+;䊣¢°Ã¶‚a¢ˆ"À5@Ýøá¼hØ!O`-®åµAÑ¢ M·~$A€K ‹"\¢@ƒ¨… ÅbÅãIÀ!ƒÂ5XUAï ¨ñk PÒÒJYñ]ˆ„°ª¤½y!ã~&ÄÀ6¯]Í6Ô‚|¢Œ}Þ›û·ý½Ï€_tº<>„ÝÀE…_p¡Kò¡ÍÕ ! ¹ñú]O`ðõ`je¡d{®G×ñƒ ïâoãÓåïm™±7Æ‹¿p¹3@ˆãHέ§WV\º†©Åk˜œ¿‚3Wç6?“þ{³ìÆgºûpSƒ³‘üËõ«'bUk ³o¼†o>þq|âw~w|þËðlÀ$‰TM]½PÂÛtÛúÍ7„Ë£±]¾lg*hpRqà@²åB(„S§N™Óƻ⡧*ÐO t¨~qq ‹Ü>‡¿ùÊ´áèÀqÎ}KJ†@‡êFÍmÕÖ„îOƒ5„zvÌðÛv·kffÃÃØ˜˜ .M¢%x-¬› p¨Ñª&ÀÁéí=4ñè‚M›ÁUµ‹W¹¼t. ÞîÔû‹×ú"øú"Xƒ³^˜-(kãKæÇ yý}ĽµûÂ@›ý]]]}óóóÓ4Ñ®¬êx ×æÑ_ùÉ{5UõÛÕ>×ÎÃTI$D.Ÿ3æPkkrXKdâÞ*¬) ¼²Tv@AÏ º°C˜¡Ð}¶qÉ’½`–'î"a‡ípžÍyx@f9#GÉ7ýÛvÈ'¯$AƒŽÜ5[ ’YJIºì0Ùí@$19ûPÕ BÍŽsŽ¥õ8µ¸ÃðŒ±œPç@BÕP5Éöã–D¸$ 2ñŸ•V pHþ³¡…%´ðD¸Oëc)°a£-Ã[°ÂTxË+Q0 b˜­Õ<¬`Ês_Md ¯„æ †ëtmy‘¸Í׆›Ü>ÜìöãO÷}#ïü¬,O?¸ø.:ðüÇï3¼—chߨw“ÒHy†Ú¹Ç7Îä¯aòꜙ¿‚Éù9„1„1¼<·µVïßц´îÃr<†Ÿ~p¹jðÃÏ¿÷ ¼ù“ÿ†‡Ÿîþ#ÔIH$RU5;õªñG›IoÓ­;ÈÁ®.ÕkÒ “L¥¶è¤â ÈdwMNNšpèÿ€Ðý€‰ý” kŠ«A‡<é*Æ÷“úúúh`©§ù¯ádr¨~ÔܶmŽùvAúÈŸB}÷¿B3x€r»NŸ>ÉÉI R'åT(²°/Ò^@ùæÒ^€³Š…×hÑ×o;dÍǹl³fD ƒuãSõ½:dHpƒ¹Û ƒ¶|ñÛ6ÚÝ}-TI$D®ŽtvAK;Ít^±¿éåZÆ}]€éúg:³»b=7dÝ×™,2·ËØA/tq°ƒÎ}ƒaRŸ‘‚È i96`uºRl ;ä %0†€ÇƒÅè:ŒÂÐîõV.ï¦ÀÛ¯H"$@Û4móSf¹STÄ«±¼.) 81RùJyn¸Úp(Ð’Í5$À¾åaUIàâjr~>Á|4ù%s:Ø@²^Wã\oÔÍŠù'ßšBK;†4¶qSÌ} j°C. ´´c`G;FnI:KÁ—/bj)¹é4µt}ó÷üUü`úŠ×oøê|ÿ¾€ýw?;FÞH$RÕ´pÉøz”5ï-=Aòä`y4¶Ë—íL¥ƒ N*‚HNR0,/É ñί‚5í6¡¯Úà °Ó@‡²ƒs‹‹Ü†Òv³ÖïAðPóß’’±1è@Cuä:ÏÑ[GÀæ~ õÒß–•âèè¨i #‰æ}›ÏDO»ƒÖf:ÔÍs™<9X`µCæße<²Q`:*3\)°C©*&ŠVë‰ÖUcNÜ:}>¸S°G)–˜zöŸéÇË ŠÉMÛ„Bk±ŠÂ“bDb Öâ |.^MŒJÑ´M¸áúz ×ìk¬ !T!¹¸Ö¶î]X %A† ¨a>ºF © þÝ?ü}M¼©ÛÐÆMáªLØ¡i vö`°³Çû?ŽéÕeL^½‚Ó—/âôåKÉ~’?|¦»]þ~þ2f"Ë­ƒó¯þß|üN<üìÜó5J‰TQÍN½f<· :Ò$O¶ˆªÆŒ1Ù\j‹N*[A²qÇÂÁ` ‘ ©¡âÖÐQfµø 0AdÝž‡¿·¯àyÔø‹ ¶›wp‹šys¨nÔv(o†R¡ ÝŸóï‚rî$ ¬•dÅ™3gÈËÉüuÎvàöª`.í8«XÈ“CM·ÃmgÍ·€‡ß6Eä2Xà6‹ŠÞì9!yu0Ü Ãf¼ =@hÊXv–.8ª‡K‰PÒ#áég"jQ6ú»ººóóó!*ׯë4ß U}nýõ—Ÿ¼WSU¿]í“;÷S%‘]˜1æPK+`5–(;ä: oìÀòÀyâ˜$ë™™eW®‰¦aØ! ÈãYB73dŠð4!‹=2V£JÞÌÊ"ƒß-œz›¡R¼;±æf#®­¯a9/N` >ü²\ÄÂ#ÇV^‰±R2-`‚€8‚˜ìœª†|4C¹°CÆbˆ'LJ˜¢"Ðà"o…ÆaUÅõõu̯¯a5‘°Ù*gdHÉF—Æ CIhaj)ù…ppb˜ Ì@*OŸ|ùEniŧ{zqWG²;€Èt J&ŒAå¦Q |Ÿ¿ þ[1¼çV„â1L\¾ˆS—ÎáÌÕ9ÀËsÓ€^_žØs+âšZQ¯±È ~4ú»8ô©ßÄÿûŸ·‰T1]½ø–ñùq[ã6yq¨V4¶Ë—íL¥/“TvöâÀšö€ÏƒÇB!úž¨^Uè²¾¾ø&X÷9T#ß9T$ÿ,_ÄZuû©F¦9:T?j»x‹áe™ÅšBºýO¡žÌ–dÑøø8¤œ*ÙAU0—öœU$äÅ¡¦Û!Ïÿ¬6 <¬]Šê£CÛY¼ÝPZ___khqDo”H•ÒYƒ*7Öäˆo>`w‡ŒÓ̾rªKàáQ¸#/ FµŸCÓ¿ ÞgÛß×iG7U ‘+g ‡9ÔÒ žØzó8Ëã²`Y‰£>]˜Açã™0Ãö Ÿ!Øå€(A’Á˜3a½êÁ¬ˆÙ3à‘eLÀJ4®“_¯K‚×%%Ë…å´Æï•†^š\n„c1¬&âYŸ’E.v¸ÝŠh9ïX;¤Ý^+€ I¯\˂̄ҕP5\D±Ã놴áM…”Ôj"ùµ5\FU-æGiPGÒcV•.¬.b>ÁÕhdÓcÃ…Õ"J‚*‘T1iœ#xã:‚72]m6»Üèó7bð¦nô·¶£Ïß„Á®BóÙ"F`^æ}“!À€Ëá½·bxï­˜^]Æ©Kç0~ñf"+˜‰$ÿ€GwíG×ñ‹çNTfMöæO~ˆÙ©×ðð³cØÕ„'‰D2]¥xx`Íû Ž×9T+ÛåËv¦Ò—ÉN*æ”C µ<Ä‘LW¹‡³µ7ÿ À¡ûtvnaò:Ø­ÌYÁøê´¡›››iP©ÙnAƒmÊÜ“˜§ Òá?‚òÆWK‚&&&¨’rªÈ™n¥ý€Š™KûÎ*‚jºùqæßeÜ”B^ãx•óN̵‹›«Æ|Spƒ¹ÛÀc×eqñC ‘D"™ª…‹Æ|¤u9p[FU£™d*µE'‡­!M ƒô6à:U__žxâ œ:uªä8´·þ øâ[=eÃÎ^ƒ W§&™JÞjX9T'Ïu 9dIò– =„ÃaƒAHºššš2Üi/ÀLsi/ÀYÅÂk´è©–S BóAÃoÈçÑk€Dw…«‚@ëÆ.›ƒéé5t—<\œ<¨Õ{9;ÓjûE 6>×ÕÕ˜ŸŸ'wÅR=«èU&ÛôÒןߟˆÅºìjŸ{Ï]TI$@bÙø¡ÖÃ-­H¨…ç?ÃÉ·…±·…p"†W®½W®½Ïܼû¿tÎtðaö×ð¯<ˆÏŸø6:÷ÞFŒD"•7¦L½j8 kë78Ð:QäÅÁy¦R[tRqÔœÙg8H0¤‡P«¯¯ãããxä‘GÊk®ïÿ êÊ{ïüS@òY×it¨r¶í{ç3qW IDATð›U¹¸ÈSN­Oyl 9TÔ¼z¸Å&m¼\η Bç½Ðæ~b(ôää$G$SæûÌÓN{†Í¥½g yq¨évX¡b`þ€‡µËIà@‹ûGý‚) ÞnÃ^J°¾¾¾ÖÐBªzY‹»{y à¡‚ªgà!BÕŸ­Å+s÷ÚÕ6÷ÎÃTA$@ôÚ´á0{š’]"QEvПÀ1q5 <äòÜvÈ3ºŸ“%{À,OÜEÂL· òO¢;(ª†ÐZ ¼”Y=çi¿r„V£Øáó@˜>€J¾h¡@ ›êÙ¡˜¹okÜ~‘˜$AjÓËC(Ãüú®¯G¡pÍÜÈ• ¯ *K‚ ž¸¸ÚÈc‰”_zDKêîÅ]í]hiGŸ¯ïÜ„q¬L "àrãø¡ã©ÝŽ“ïl)õ}àÀfÞ1µ ÃW¯à;_ù~öyúÔoQ£"‘H%káb oÒmÚ ú"¯:ÑØ._¶3•Ú¡“ŠƒÕò›½†CMOOÓC¨Î544„ãÇãĉå5Ý•÷ þËC8ôXãîêu‚,ʾý#3“ŠL#õ'‚ª“o'zsàÖ¥ÏÜmÔ7I¦¨$àÁß[ßû†Ì¥ýg yr¨évXáb`Í«?7fRä2ÐVå2±düâ6î';7˜» y‚éÿgïÍ£ã8Îs狀gÅ6C F$@€â*QF¶äM’ o‘cç^Á¡ãsäø„r’g$ßÄù¾|1!ŸÜsnn¢Ê]âDN :×–cÚoìkZN F´h[”ˆÍ$±’ f03½Ô÷Ç`ÇlÝÓ3S=ó>çàtU¿U]U]ÕS¿~H$S¹ýN<õúOqüÚy„”8þnèZªëðŻ߇§ñ BŠuk·³ßù2B×ññ?ꧦމD2¤‰+oŸ%×·Ù¸Ä8Ø/Tú2ÙNÕQ)€Cʲo9>sÎP®ýýý´Y—„ÞÞ^Èz€…öÒîúCHÛ?h}ç±è`G7!@‡ÒA|–ÊZ\´É ¹9Uç\Œë®]ýšáÍhà÷û©“Öh``###Æ×Sþ; Ññmº.£göªʺ–¸XõÀQ¨Æ^$Ëc“`ž­…«'‚ 8¾™ÏK4ؘ· ˜5öâ~kHø‘DwToÔb®²Ÿ?ê.TïV8&E ±£ÂËà£fÔÛ¿¸ð1Qcs7ßMȆŠM cæ•ï#|åehñÜ'ÓZ<š„#ÆÎc ß‚ì®Bí®ûà?ÐŽêæ;¡Å´Ü^WU_¿=3ìÀUÕ ‡[; C¾+8c Ì)¯ÜvØ °TÕ”!3rtš0;¤ð`¦ï ຶ9èzÊté`‡˜¢AIçì€üœ" NY†ÀáXë|°„Èé|é+Ïw‡Üa‡LvHø¡Ô°hœCÓyZ Cè±XÓ06?ñh*×M¬èK€C²üã ѤsÃÜ,nMÜ ˆÚê¶Âïtoxø\ÿ9O•Γì›Võµóé¿y=åçáI̪±5ùœºu.²A­¸@ü,y­75 cÇíhÛÜ€öÆ&ø]îÜ®UÖcòwh­ªÃWÞóKx|ß=xâÕS859†‘ȾðúKøØ¶ØQU‹/ ½nYÝ\~é$¾þ¹ÃøôÓ'z ‘H9kâÊ›ˆGÂÆ9«ª€ KË…ÊF¸r *}¡l§ê`¢È‹”ÐÛÀðÐ××GÀ €…Ðý¿Â× íÿÍü;¹9©¸•îæ`>Cí:ý!7‡âg]‰ŽÆN O¼}â4E$KdÊÝÁðÌ t¨ˆçÂW =(ëv(P5H¾ýÐg^5~ôºIà@‡Ò­á­¸0íZªÚÍh¢…›à 7Á¼›…U4É Y/ü Ú‹ W”RñÍwÃ!®ËCÍò £Jæ@ÀàÌ7¾y[l>²[Ôø\Mé"ÙH±©aŒÿø8"cç­¹áÇ£˜= ³çOÁÓÐÉiÎzIÓùª ü™a‡¥_¢ªšJ@¾°Ë;¶ƒR˜ v`é/ìÀ°¸¡^S]K±›žå¸¯=¶Åä¶HÈmI±ôÛ’Ë ±1…Û)£Úå\Þ„owØaI^§Ñ„ºÜæ¹ T]‡,ɶ‹Ç£QŒG£˜M\0é P“® K€ÃÀÍ)\ ‡0pk CáYL,DA²Vmu 𯺿µ×7¯y€p¨¾y̓…µËüÀçö`‚xhß¼Ãt>ý7G—ŽÍax!˜Ub'IþY5ŽÁð$5°E ÞšZ(íªõá3;÷ãP  íM&¯%³Àbå×à¦üøCŸDߨ<úÓ"¤Äñý·¯Áçtã‹߇¸úF"s–ÔÇäÕóøÊï|¿úÔ?¢q×j $)û¸qÅø[t™o—MJGNö •¾L¶SuT²“CÚ:©»Ýpê¾¾>ôôôÐ ‰Àbèaäÿ€/LB>ø‡€£Úx 7‡"צ C ¦ ì§?äæPܬ+r0w}âEh—¾lêl‡¢>OJ;¯3´žÈêî@€CE< ¾ZÈÉ¡¬Û¡ÈÕPÝbSÖÖçÊtX–äs×vã7/ƒm·Ð£Œî¬…/,ð 3tÙƒJP·Õâ¾@ Ð>>>ÞO³}kUéÀà €fjÀK_ûƃ¢ÆÆœ^8÷ÐE²´xSg¾…™W¿_°sĦFL¥;¸y æ” S¸t°ÃÒï×SÃ)vì[ ;€Xµ¹Ú8ìÓ°CªÌrXÆ,X:s‡4§f'â9nÝ×4pUMNÐsL’ ¤àœ#K¤uwÈvÈEqECBÕá¯rÁ)I9הȰ€8°ÌÇD%ÕTZ§,A–¼ÎÂN½æc‘yL/ÄrwsÐXògp˜W•EÀa7ɽÁ¬Õ'7û} ÉÓ`ÝÊ›1ÚëwdhÀsùŒëÑ eñ,2+,±®Š–\#f•ø2±ôÙðB#±¹Šk§WÂ!<õúπדïܲ¿²}'nÞ…àæ†@…¹Þt4íBû| O½ñSj°hÑëÖUBIÆ7.p?*=ìÀr‰‹‹Ó'™w» àaxP~8 –˨4—¿b摺ËÕ»Ža_€Ù ŸfüÖª¢‡ÃpGN à®ô†0qùÊûEÍÕLîvPlj£Ïÿ”9q7ƪš¾†È;,ý?¬$Pçt-f° ve°¥7÷›‚² `ÆÃù¬R§[—še›™³Ôy¯úk˜ª.Z;°Œ›þ×^ÃÔ'Ÿ+%…–ËÅ9BѶT»S\sûÁ K 5nÂ1E¨1Dãšfn•·ÔV¢qµ^\²dÝØ¦ëI7‡…(æ•êl5àÀ± 8 Þœ^vr ¥–Ïé^†‚þ¤ÃBò³äFˆ o•ë‚€¡˜’[|N=¿ó¬Ï_ÉÒæõÂ=X»²a¥cëž”#Õ¬Ç@xbŠ˜Xù…8E¼23‰Wf&ñÔë?ƒGvàÁ­ÛðHë>´76¡µ¦ÎÜÈž‡ûƒß寱{ÚñðöÝxôg'1™Ã÷o¬¸=<}á„”üß‚„ z ‘H9É”ÃC}›`¥ ÈÁ^¡Ò—Évª‚ ÖWÝíÉ7é«CézzzL½–T¾²zC{© ò»þ XíÎÔý@‡"צn–…γ¶UÍŠvnnÓrsÖ7»éo„võë¦Óû|>tttPw'm˜ß›‘TŸ=×Í9ØóVY׋œÌ—‹Û¯lÌ·ßxReÐâ€ì.Ò¼‘\Œ%7ž3«_2ïv`ÖÉo ‰ßC%'T§¥0{rt0¨ÎÊ ¾ùn8ÄuyèÐE3~kå *À €m•\×ξZ™öµX©• ®Ù7ûñÖÉ¿6¾jGr¨K¬Ú¨ÌRLðÒíOh:àL2,þ²zƒ>c¹ý}ã1ë`ÒÒûMíLÏm÷yJØ!‡¬³Á)X ;°ud‚¸®Š¶l?ìP5ÄT-euvXJÇ9GTÑPírdn6€’× é„ h:bŠVVã¥Æ9f£qø¼.¸r^yMÇb˜Ž-`<Ͷª4ipPpH§C IW†%p¡Íß¿ÓÖjZ«êV­³Yq¬ï<°U›+$G ^âç©N—€øÚ~¢«þ¯1@gkÛzžò;Ühß”tŽHE GCŽ…ðO7ÞÄ•…[^!¦k˜HDÊ®?Ä4/ÜÅ 7’¯€i­©ÃÃM·ãáæ]holÎ dÈù˜E‰ö­Mxí¡Ïà©7Îà™K+n´ìC(‘À÷o\ËÿaA$)¾~ÆøíÜ·«ÄQs!³*Ó€, •¾L¶Su 9Ø`ƒ3 ¼|ÌØRÇGww7Z[[éÆDZVoo/ÚÛÛñè£æŸ™…öÒîúCHÛ?‚ŠY\QÆTžaÜ/d•ÈD5þ¢½½‹ò¼iîœäèPœÀ¸=Ú—véYè§óÊ£«« ~¿Ÿº¨v‹i¦0¾ż’f³·Æ’?‹š á'o/;9T’54Áïr#èo@Ku]rƒúâ¿–?wXªrÇb&\úÊg.]˜g1ÂÆP¨ó¯#82@sÎd q Lc‹°3åÑêM C¬††c! „'pêÖu[÷³õî»j}øLëx¸y‚þ†ü¯;OYr{xôg'ñü[W08;…áȺïz/Ž]|!%?À„ ‰”NWÞ0>U¨+–»ö •¾L¶SuàP8Iïƒîø{Ão*?uêzzzÐÕEŽà¤µêììD0D{{»©ÍszÉÛ?†¾ù¾?Õâvh VfV°*Ë/®D §!‡‡JV%º9p1ëÛ&n˹Ŧ¡<2šw^fÞâO*™…`¤úûlº.£göªʺòòï_¬v'`p?5MhþhCØ¡XŽÜ¢ëkLÜ>íšy·›†ÁМ~(Z ÎÄ-kò“ÜHT· R•ðí…gêeQÃëÐC3ëDI½ `O¥~~ææ=¢ÆælÜC­SPÙ vÐ×l|Ï;l˜ 2$V9<¤‚Rü“ìÀ@–­‡2|–+ìÀŒi*7'ø"‡tia‡EWpž2–|`P—À `‡Â(uå‰;À‚¢"SÖ^º2V\MùïHlΖ}òJ8„§ÎýOû)<^nÞƒ_º­M»Œ_wžýâwºñþ#z.½Š'_;…G÷gð;»ïÆ™éœÊo|&èD"¥Ðä•7ßiêÛìq3%È¡H¡ÒÊvª&z€¼Lú—³,ð^S.O<ñÚÛÛ é&EZ£`0ˆþþ~ttt`dd$ÿî†úï¿ ù¾?OnåžGCÁÊÌ ZeedÂáÁï÷ÓQQªDÈ¡Ôsò€Ï^€ú‹@]È;¯cÇŽÑœ´A½½½¦æjRàý€£Êfë2z`¯*¡çeÝy…ô1Ìw‡ñ3FDzŸ—@‡…VÂ\Ðöo,æÝÌ;í!ÛôpÅg.¸âyå£É^$ªšÁ™\±óÊøæ»EÚ@ëøøø0­¬Ã=yñÜ•Vöø­ß}P×´Qãs7ÝM T@Ŧ†1Þܶñg„ØZØ1 ¾<¤ƒ˜Õ°æ³LPMÀö©o<ætÌP<©ê×:Ø!Å6ü%ØaÕæp¶î¢æ ;¤«ë`‚|Ó1ëNbiÄÙŽ+XH”·«Ãz­uåÈr–7ZÍ…pò­ ÌLáJ8T–uÔè©BÀS{77bO­ý øåÛZó_”/5ey1§žüÌ­%‡J)§É¨žüIUeq ˆ­ƒ! €­Z=>t4¬¾Kný·Fm ALÅð¥Ë¯ãK—_‡Wv £i~­eojø!Ouí}Ú·6ã§¿‹‘Ⱦ4ô:î¯ß†GvìÃs£ó{0BЉDZ§‰!ãÀ|»Å½äP¤PéËd;U99”FÒÎh&€ ù¶òþþ~Ú@GÚ `0ˆ´··cpp0ÿ Õ(´—ÿ?HûÒ¶–¶Sè`i¹YQª¬‘ :ã,õ R%ˆ@aê܆ÃR–úÛ'¡]ýº%Y9r„\¹H4;;kÞÝá¶C6X—ѳ{U 99”u;äÒ¿R­u<õ€Ë$fE½VÕlA˜å ;X9’«CÚò¹ëŸfáfØuxmÑóU÷fèŽ*8cã5c ±ͽŠ»¡âç–ª·ºË)!쾩˃e"àaE“š+­Ðï ¼_ÔØ\MÁ\^j™ê­ü/hñ¨-cÏ;¬þÏêða%:—kÕaå;¤:A6Ø!{6‚ÖÖ¢©U3×-‡\GÚƒ­9˹ÐFÏI¨;€Æ9æÓ±¦c ©!mÅÅa^Q0ps?¿Qv.Õ'v×ú±»Æ]µ><ÕxÏ–êÝ^<5¨q85\¾ø¯Ä“ÿÊ<ùãÖ o²"Ð3¡%‚»Ï3F—žüIuèœP˜"!R¹A¬‡ úo"¤Æmq™4Ï\Äs#á‘x°aiÙŽ¦]ð»R°æ9¸;¬¯ì ¿¯=ô|âÅïâÔÔ~2ý6æý øO»îÆß]y=¯ø— ‡ÏþïŸÃSSGý–Dª`Åæç0yõ¼ñ»%äâ`¯PéËd;U¹8rênÛr|æœá´¡PíííèííEGGݰHk×W~?úûûÑÕÕ…ãÇ-xy…þÆæ®AÚÿ›Åï›v†Ÿ ¤N¬¬²Â•Ÿ‡‡ ïóùhP(‚fgg §a–¼¥œ aÆ^Âs[‘…vùYè3¯Z’õ¡C‡ÐÛÛKƒiƒzzzL¹;0O=˜ÿNA×eô<À^UBÏʺV‹CÖq³ª Ü(ð›Ú<”l,ä‚U¿¨ ¸/äQNg˜£\ ;ãÍ!°­m3 è²ñêVHje’2)C½i’šÓÍå«hW‡õRjvÀ}óœ¨áµƒ€ËDÀÊÞ°æòžš¾GÔØ\Íäî ¢f^ý>bS#¶Œ=Øai“üzPA[œLv` L’3LTS€yÀ,‡Íû,Õ¶÷Á0; ;ì èHÓˆ’Yh¡p°ÀàvHÚt¾ç*ìÐtDâjÅŒ¡š®ãV"޹DM…ìHQcKÖ¤Œ/Dñ“ñ·1ps ?™¸QuÐè©Z†Ú65 àMº8€ƒI¨w{Pïö¢Þ½=fÚÍ0žteX‚ر~0;,בÓà×ðèÏOâøðy ÎNa82‡î»Þ‹î7Îä÷j§‚H¤ÊÕäãîÌ—³ Aö •¾L¶Suä ¦¤=¿mæOL¥ …BøÄ'>£Gš~K,©|å÷ûÑÛÛ‹ÖÖV<õÔS–ä©þ xøä{þÀQ]ؾiK7B–V[‘ʯ{!¹;GÆç&5-…mo:'0»9,ÿ72 íÒ³à‘QK²okkC__ ¤”c¥Ùy™´ó“‚­Ëèy€½ª„ž”u;$È!õ¸éÛ mö cÑE¯›ßQâñ xJ;0#qñòhç¬j;øÜcg¾yÙVÀÃ’tGŽ*À» LWÀt%å1¤ÔJøö‰ <<üããã³t¥ò‹: ·zñQ{*¥Ìßû«c{”x< j|ÎÆ=Ô0“`êÌ?[’WuýøwìÀ¦–2xòqktJt¡ å0 ;@TU±ÙåF>°r€’«)íducì,ó1ùÀL<Ø!ì‘v`™ëÑ$ ÀƒC’ éº©VWØÁéàq:2\÷|ÎU8Ø1†ðBù¸¤Ó\"[ñ8攢ê Üá”d8’žù"äÀW ‡ŒàÊ\ÈÖeoôT!¸©o5Ú65`w­?éÖ°n}[ïö¢ÞãAÀb£[|½Þ’[ƒKdÝX0ˆC¹ÀYóH3"Öª+É—òˆK@U( IDATL‹çA´z|hõøÐѰg9žþ[£èŸM‚³â»@¬†7ïÁ¯·Üަ]æê|ño_y×C8´µ ýü‡)q»ø*>¿ÿ]øÒ•A„óõ1yõ<¾}ô1|úéoÑ‚€DªP¾d<Ñ#îÞ r(R¨ôe²ªChÈWHÿÊv¶Ü Öøð‰ŸšÎã©§žBkk+:;;éÆEÚ îîn´¶¶âÑGµ¦ëÞzê¿ÿ6äûþ¬v§õýÓ– C)Ç% @ÍyøMã0nkk+ •4gã%E ©Â4ix€owánÈ8)Tú2ÙNÕÁDW@ÿʳ\ÌÛiï¯C?ÿ÷¦s …BèëëCGGݼH©×IÁ †‡‡ÑÞÞnz³Ýzéoüwð›o@¾ëJÐÍ+tÈ|î‥„8ôYrx¨,U"è@nE)NdÚùgÀãÓ–îÈ‘#èíí¥nKJ©ÞÞ^<óÌ3æ;ª 7¬ˆÝŒžØ«Jr(ëvÈ+¤0VÓ>?b,iô:˜ï@Ëʽ,Va:ä“sÖ‚9jÁÕ°±\nm=H° “RÓ":ð@²@<¬Óa¸C'À¶r.ç™o|ó¶Ø|d·¨ñ¹šï¦Æ(˜fßì7íî°ý÷àÝÿé1¸ªŒ½uÁUU…»~õaì|ÿýøÙßý#&/\Ì£©wë¯l¬_ÙúžjâQÕÎ ©`‡”î™ÿ¾F’”SY3Wö¬0EªjÊ:¤qvH‘°õI³ÄÂÒB <¡‹îÅ„–þ[ëqb6šO;y7¶1ÛéX{í²¼ qÊÉc$Æ §i+ÖÁ¹ÑfaP5{q]CTQ—Ý¢ª5‡ må×ÉØ~>=ïŽ^³äÐèMº775 ms=v×ú ­aýnUÕÔzV¹6(–,ëExNB1ˆ^6ƒÒÉ«^ |õyæ` ¹¹@Ø €˜‰/à™K¯á™K¯¡ÑS…Ïßq:¶ïFku]Néƒþü¸ýSø@ÿ7-…¾÷—O`ë®hÜu€$R…(4q¡‰1ãwŸe‡‚ì*}™l§ê È¡üÚ¡´óað™×Á'~jú,<2?£ðûÑßß®®.?~ÜšÖýö¡…¯A þ ˜wk»9AiVþª*.@qó€‡2œ»‘›Cq+3ÈaIúÛ'¡]ýº¥§=v캺º¨û’ÒÎÑóirë'Í»;ä`»iPY\/z_¹r°4V»Ë0ð€Øà+dy+v Ð!ïü– ïvððc9ݼLÀC*áÛ+rx¾@ Ð>>>ÞOW*?ðZב¼uW—k_úÚ756©z3äêÍÔ ÓÜÐ˦Òùw4ãÁ'~?¯sW××ãƒúǸøƒðÚ×¾a8ý¼¢d™(fÛúžükTUQåpz ~¶­œÖmbOéìÕ!7g‡ÔŽàˆRª ,º+”v’oª÷W¹ZH@ß°éåÔ–\<²Ä ´ŸÔÀN°h\ܧKÎ qM77,I_Y€MÆðÓ©qüÛ1\ ÏÙbÌoôV- njHíÞ©a0‡“!PãAS] <²¼ªb¬[Ú‹ðœ„b½ÊvHwž¼fÕÿd "ƒ% gŽi=±>ôM]Æàü¤PcÒD,Š'_;…'_;…ZðÈŽ}èl=µ® =|ýs‡ñÙÿýsxjê@"‘Ê_CÆß Ëêï¶æ&HC‘B¥/“íTBC¼BúW!ʵê0ùî'¡ýìóàs×è&D*˜ü~?z{{ ñÄOXÓÚÃÃÐÎ< 9ø'`›ï*q¢b!‡–;‡ 2üÖyÃÙƒAêøårß$С8Á•)è5 õÏ€‡.XvZŸÏ‡žžtvvR&¥ÔÀÀÚÛÛ …Ì­=õš¹Ý‹žØ«ZÈÅ¡¬Û!¯þU¢Xu‹ñl£× Tn ­o+ÐÑ!ÕGÌ»0 < Ñ$¬WÔ² ß^¸B—D ±@?]©üDÀC †[=øeeûpzdôQcs“»ƒpÒℯœ5œÎYåÅÿô-‹cßG?‚­wìÃÿì‹à6_ ÏmØL¿v`™&‘‹[õ r}sÃÚ¿¯Ïƒ­²GHåîÀÖŒ1@bc)ìê@a`ÎMÛXÖ"ÂË7QIÂæ*7⪎¸ªƒCÕÖz>°UçuÈ ’Äà”$8=ŒÂfU*ØAÅu qUCXQ×TDÔ¤ƒCª6”Ja€¼ÔÀô•š™×œ™ÇK7pfr\øq~W­ÁÍ+î æSâÉb;’Î ~—ª*ªª,ŠŽ`ÛÆ@°ƒµu‘Ëy<àÑVN—€¨ —5súöM;оiº[ĬCßôeôßEßô%¡Ü^Á ã#ø½Wþ Ûwáö߇ ¿!m]¥„î|Ž]6=Ä#a|ûècøôÓߢ…‰Tš¼bx@Ý.ûÎÄÈ¢PéËd;U¹8”q;Lw¨³ÒÝOB;ý¦"ðûýtó"嬮®.´¶¶¢³³Óô&¼5R£ÐÎ~Ò¾Ç µüŠE]¾Tï t(Mqs8¿¡±±ÒîŸ9'8.Àµ.XvúÌ«Ð.? ¨ –Þçó¡¿¿Ÿ *RZå ;€|Çg-í ô<ÀNUBCY·Crq(Z8’ÿ 4£§ˆO¡àoïærìPºëÈsúˆy·Ï9üVrÞëðÒ„¬Â¤Ôìx [¼ =tŸÉôùG_?ƒÓ_}üÆçh±@"•¹FÏ¿ Õ·ÙkÞ v@…J_&Û©:r(ãv˜«áÃØLEâóùèíÂ$Ãêèè@?:;;188hIžúÅ¿ùä»þpTÛg¼(©£ƒ ÈÁ²pÅusH™lÞØw#‡¢Ž^AÓ¼’¶w‚ŠWn#›»Õ(´Ñ>èoÿÐÒÚÚÚÐßßO@)­z{{ÑÕÕ•ì Õß æ¿3ÿ~@7 U =(ëvHCiÂqû§PŒÇ<:VÕdŸz$ÐÁF}‚Êž9kÁµàjØØYnm=H“² S·ÕoýHÔðZ@ëøøø0])ó"à!ƒÃ=yq€­åT®û»/ ëîÀœ^86mÏ+EÑ $´5ÿW-¯<ݤUob—$×Y1}!rݸEruýì|ðþ \nñ2(ʺ½ÜÆ` ®é9Á0;`²”&žÁ+¹o<Æì:Ýz‹l³ṵ̀×u@×…2%v0ræ¼sNãîàtHÐòŸWKÓuDT ]C\ÓQÄ5-¥cÃzÉR\‘r…t,o@Fqrl?¸>Љ…¨ãxµÃ‰àæzÜ߸m-àm]*q@BnHaHâ‘h­­E½Ç“ѱ„dãç v<¿HåªXú»K6'V_…Eˆ yk¬iD÷ΖÝú¦.¡vT÷‡Ká[xràž8…GvìÇoÝ~í ÍkËàoÀ} 8µ=üñ¾ûðß.¾lú¼§ÿ鯱çþ¢q×wH¤2ÖÄ•7ŒÏ}»mv¯$È¡"&)9”¨‹Tx;4Q|¾0}øySQuttЦ;’)ƒÁeèáù矷¦—LýÚÙ?ƒtàÀjwŠ;f”rÈ~î¹9pAŠk0rwZ–Ô5Å Ž—è¼EÉrå@…véYðÈuKC9räz{{©Ó“Òª··>úh~™8ªÖº;à`Ë©OY\/rq0_.^!ýKØ3aXm+øMc€?^Ïx Gck].pó, ìcÖÌ»<|ÁØ™n^&à¡¥»|P½[áX˜5Ä=t¥òX&PdÖa¸/Ÿ@Ü ÀW.e~màý¢ÆæjÎ|£‰-(ÐuŽ…hò­ÜÑHò_%¡A±pÓ¬QUU»²,ÁíIv«%HÂé’átʶn3‘1ãÀÃÞ‡>R°xª¶Ææ1¸žC³§ª¦Ã ì ]Ë v`&`0‹›…-ƒr˜AgƒR$°vX‰dƒ ª;¤½öù, Xα=_:ØÜ1c÷\"sDUuÙ­!ª*PM¸E0–ŒÃéÈa³>Çš7¢ÿàú(~2ñ6~2~Cȱ{W­4nÃýÛ°»6ÇiŒ´øãÈ|]ê=^4UWÃïv°äî@Ê£íäëî¨À,È#Ïsä[¯xõ•q:ì‹É€’«ûÐ7} }S—Ñ?;Š‘X¨äWÿ¹Ñ xnô=Uøüþw¡³õüN7À7Bwõõ¼¾}ô1<ú¥à©©£®G"•¡&®¼‰xÄØ[ƒà¬ªm0G ÈÁ^"ÈÁ^]„ ‡|¤3•Îçó¡»»›n^$ÓòûýèëëCww7žzê)kzMx8 =ì{ Ò¶Š5f :›C¦65b8M0¤^$™©k}öidê½´ÖÖÁ#ä#ØÁ¶1”¼v0ǺúªUÁkçÜ ˜w€%ØØkʼ~/:ê÷æ'Ð{ãú¦/—~˜ˆEñä@?žèÇ#Í+®KÐÃ;~ôO)q»ü iÞç®_0užÐÄNõi|øwŸ‰D*?M^yÓøÈ·Kœyp•B„J_&Û©:˜èò è_…*—EUÀgÎßÿbpN1%nÝÚv`FââåÔO¸%Ù3ïvãg¿¨ €ÃK´ S·ÞñE ïP ðÏÒ•2'rÐa¸Õˆ¿2€¾÷WÇöèšV#j|aÏíˆß\”‹%½ …,3¸=N¸=82Ü'<'$™•<îØ¤ñ7ùw4›† ¥«ss¸½Ê—eow˜­lâ×8‡ƒ¥ØÜž7ìÀLÁ©Jc=ì€ì°K?U·ÄÙSÕ Áä’®ð2àªahq“ù`ËÏUdØT]‡.qŒ‡£Ð8Ç\"¸¦!®αÇ!38e ÙèpzüN\ÂàÌ´PcÛ‡:_n @ éÉsGv PU…¦êj8$©¨}JÄç'ƒÈu@°ƒ±8²Ô—K6%V ;Á$@K[°¦={ѳçÃËðCÿì(çKû…ç®_Às×/`oí&üɾw¡cûnüã½á±³'Râ8?7ƒû·lÃOfÞ6•ÿÙï|{ïÿ(v´½—Ï$R™ibÈ8ð€-m}oFƒ½Dƒ½º¹8BÚëm*]KK ¹;,UGGúûûÑÙÙ‰ÁÁAKòÔGÿüÖïý"à¨.A×.Õ¸er°,Ül¸a°rX“[ÌøË©x¨$äPš°ÄquÐ'NC»öµäÆ. õðã··~¿Ÿº)£†‡‡qüøñ¼òêï…ÜúIZƒÙ~iJ€CY·Crq(qHÆ3‘êö@“Ý€7°ˆƒÇ¦À< âÔ56ë3ܲ왳ÌQ ®säæ7‡À¶¤IZ…Iõ6Bwù %B¢†Ø —®”9ð£Êzxå;ßý˜¨±%|{žœ¯Èö¥iÑHbè±BTU»àpÊp:å¢;BĦ† §ÙzǾ‚Æ´õŽýÀw¾k(ͼ¢ÀéSµ “Ḛ̂DUu.×Ú<ò…6“vÈðYA`‡4µ•²9mf£>×u0oö/¼»Cþ”3ñWë`fU1Öõ£µ°CLÓSUÌ&ˆi*bª†ÙÄÊb:¡êˆ+…ƒ\²‡C‚Ärب¼ø&óyEYvs˜Xˆ sϸ¿ñ677àþÆmx«rx‚C€Ã’<²­µµTU±t;Ø6‚ ä‘ç9,+‹Á¿×*ോ¿/È`QP¤´ùkѳ»0 ¡oúzÇÏ•~¸¾…ÇΞÄï½ö¯øTÓ^<¾ûxfèU †¦ðHó>Œ-Ôa$:g*ïïýeýÒ ðÔÔÑâ™D*#™rxØÒF“‚„J_(Û©:r(ÓvXàâëc?LZì@*„‚Áà2ôðüóÏ[ÓÛÂÃP_üÈÁ?Ût Ý›ÜŠ^LΑ‚z°NáaSm™TÎâ%ξRAq €…vùËÐg^µ<¬cÇŽ¡««‹º)' ä·–¬i¼ÿ³´³íÒ” ‡²n‡9”8¤<3‘=`žFðȨ±³Æ'³:ZóèW^Ì» <|ÑXê›— x¨P%|{á™zYÔðÚAÀƒið`@« ‡ý|v,Chbâ‘ÒZ¥!Ü\.Üž¥'œN¹ 1(sÆß´õŽýÂÕ弪.Cé¶gƒ,¿yÞRØ[~Û|°Ã†rl<ºt,”aÃ, X‘¢ë?`YËQ8w‡bùÀy/˜Ø°CLÓ×4ÜŠ§ÒÉåH¶«¡§ÌàtÈ¥J±ÊÍah.„W‡prlTˆñ«ÚáDpK=îo܆·¡Æé̼0d$ÈæVŽ~—M55¨÷xŠ\R‚lC9À9•“¡®Xqê;_pë{Çì„D`‰ôðC«Ç‡®¦ûÐÕt_Òùaüú¦/c$Vš·*,h*ŽœOŽyN7f•8ž»~ŸÛ{/¾|íBJÜpž¡‰1œþêÓøðï>E‹©Œ4úúãw%ß® œŒ"Tú2ÙNÕÁDW@ÿ*T¹ŠUJúù¿7•ôСCèì줛© òûýèëëCww7žzÊ¢¹¾…vö ö= iǯ¨‹“›Cq‹Ê‹ W£¦Ú1‰æ•Öf/èÃÈ ssXþ4tÚ¥gÁã3–†ÕÖÖ†ÞÞ^‚¦H†”ðÀ<õp¿8¼eºã%IjûûQÉB¤çö«^¦áXW.V·Û8ð|JWß6Ò¯}¹ Í–./‹_<üÛÃÀÃMÐ*TJM‹ÈÀC]!ó"àÁ íxãâ{lµSìg¾ñÍÛbó‘Ý"4¤Ü©ˆÇT„×½€vÉ Â³Axô)4î:‰dM˜pw€·pÖTÈ$¤¡Ò—Évª‚ʸ–  ôá>@˜JKî¤b¨»»íííèèè@(dÍÚE¿øð›oB¾ë÷Gµ]œÜŠ[ÌÒÔ7Ÿ6tü¡C‡¨Ó¼ÒÂìÉÍA´u’6Ú}´ÏòðŽ9‚žž¦HE•´û7a·Å_šÒó€²n‡9NaÊÅ|ûÿf,QlªtõN®6ê?Vé3ïvã9…ߢIV…*áÛ .»Á´¸ˆáù@ûøøx?])ã"àÁ¤Ã}ùâ!{ìó+Ïÿ‹°îªw+t—VžZr‚XÍAÈ2ƒÛã„Ûã€Ó)Ãíq¢ªÚUÐ86µì(hþÕõõ¦ÒÉ’”uÊ™í-øsJ"ËduuÊìîË’’8FFw‡ °3;¬w„ÈvH.s=švvÀ5 …†ŒmµÎ€©ä:¤ù‹°Ãܰô“ʵY°ÝE–jz{{ÑÑA/%™Ÿ/™uÅÒ‡O@ª¿·²Çk^¦å6Dz&e¯jàeRñÊ%Õíƒf4ºø ÅÙ 2‹ 5-f»+¬«Ãš:vÖ‚9jÁÕ°±\omöýà¤J©i+tIÔð:ôÓU2.òÐa¸'O p·èñ¾õæù…`|û¨AHšÆ$–aˆ%92œ.yÙÂé”S‚‘±ó†Îç¬óÍ ×æBi§œ,…3ÚMü©öˆ¯ÚõŸÊݱì°ÃZ@* ì€À,Û,?wØ!Ùˆu‚,YTÉÔ4E IDATåVhU×QÌ&ËÎ *׳œÇÚm/N‡'¤ÅËÏ¡éëm$) 7H’ÁÚYÆ£ÑeÐ!¢*%›’Ãm¸?°-»“ƒÄ“N’5çTU¡µ¶Y.áèL°ƒmc ØÁ@¹ÄÁ _éKÇH|Ê¢ó€ùÌðCGý^tÔïðñEׇKx~úrIš¶YØF_?ƒs?ü&þÒ§h!B"Ù\£ƒ/Od9ð@ƒ½Dƒ½æ¤9ˆ"ýò×ÈÝdµ¶¶¢¿¿]]]8~ü¸5™Æ¦ ý¤}AÚö}¸TÙ„›ƒeáò³ô'¿eÈ ƒÔq+q~iIÖäæ êZIŸ8 íÚ×uÁÒ0:„ÞÞ^´¶¶R×#™V>÷>?møäÖÕ5VäPäé™”½ª KåòyÁ&ŒEŸ«j°n t(mÛ+è°¦¾Ýõ&€‡Ë OM-:H$kbÈ„ÃÖ¶2˜h"Tú2ÙNÕÁDW@ÿ*T¹DÜ'0w|ì_M¥=zô(mÈ#•D~¿½½½hooÇ£>jM¦jú›ÿüÖ÷=8ªÓôa‚Š[T±qp“4Ç´$k‚„^/©Qh—¿ }æUËÃ=zô(¤$ËæIGŽ1 †êÃ' ùïóßY¾c5/ÓrÑóñ+„—qÙlŽåªÚ¢c…Š ;X·³\ã"ÐÁÒ¸˜w¹fì,·†h’V¡|?r[ h¦+eLÚ¼5µ¬…6TªµNKt ó<¢˜ç—V5h‰­ŒTœ¯ý±S=Z’G± ŠBŸƒå‡ N¯€¨ –H ?´z|èjº]M÷a <ž±³è›¹„º›†&ÆpöÛÏâßøY$’5yõ¼á4Ì”ÃA1#È¡Ds.rq]úù¿7•Îçó¡««‹nV¤’ª³³Á`±¦w¿Ý-< ©íó`Þ­%†²Ÿ›tì©dÐ!õùù¼ñvFÀCÌ3-Éš@Ñ×L<2 íü3àñKÃmiiA__¹Á,UOOúúú ™{váKpÜû_GUùŒÑäâPäé™”½ª\Š)Éw'4ƒð$Ž T×¥wu`FââåÖ¿J ;€̳Ýx²›`®KŦ¡ }òþß±÷ØLC‘C¤gRöª‚J%Vgü…=t°Y+=è°|Üõ€äô„±,nmÞMµ T|óÝp¼õ#QÃk†%QX¯ÃpO8 à:M„˜n޽%¬ÃC·¨20f¼iÈR2O–æ®T·ƒb¸.R\*–þæÖÁ7'À·ÆÁ«´äؘjlsxÐ8ˆþ¶OãÚ»?‹Ç·ß ŸÃ-ÞÓHg¿ý,­'H$›jâÊÆùvgìVÿ,C¡Ú¨\…«ÛT_ó#tµãz•S;´UñW‚Õ^ÿkS9´´´ ³³“nV$aä÷ûÑßߣGZ—©…6øß _úŠ01[õcýøSÄAlMÌ¥@WŸ7·øÂ”©öIª0emN¥œ4¤9wQºbNs–& ¨F¡ýâo,‡|>¾óï@J*¨ÚÛÛóšéãÿ}ú¬¦‚/Mm°h4b9> 0Q.[>(·®aŸvÈjv²Çx MÀ×ÖTUžu»&¹ù¼˜ÑymÉvúl^å3‘8MÉkÆåá2MÐ*TjM‹Èá=LWȸÈá¡@: · `Àè¢ãÃ6Õ¥ˆå{ulD­«„o/5RQ4±°€Ý5þµ“Rƒ°×µµŸå;¤$oˆ­‰9Sº”°CÆ 9Ël2쀜`‡T±0]7.ŸÅˆÉt‚Áª®c&Ãt,†™XÌšB§\°í1k.–ÆÑ¡Ñ[…OÞ¾ 5µ ÆéÌP¼ È©ßåFkm-ünñ6ì`Öœ˜´èÚÀ âD²¡™8@"±öC‚¬-‹õ•îo5*x X\Jy|«Ç‡žÝFÏî£wüœp®äò@"ÙW“WÞ4~ûòípQˆy½1ÏNÕA.eÚ¹ýË¥ý0i%ÞÛÛK7*’êîîF0Dgg'B¡%yê£ß¿õ&äw>8ª‹ÖG×/Ñ 7q@ÆÍ¯ƒÅCøü°¡3:tˆ:f¥ˆÜŠWö"­xdÚùgÀã3–†èÐ!ôõõè@*Úܨ¿¿ß´ –váKÞó7€£JÜ1‚\Š"=“²W5‹ƒ¨bµ»Ág½ð‡GÇÀ< E¬¶Ò;:¤^s›/\^%ttHy]¼Û€È5cYÞ¢ÉY…Jõ6Bwù %BBÆ:ÆÇÇûèJå.Š ELJɈW# >lAò]ÎEÑ…S/Þ#jÝì@2+ËÃi&¢QHl-8n{:Ø ˆkšµ°“r‚Rͨ³ÁÈ!dÚ»švXú5¢(ˆkIÄ%I¨v:—ë9×]±)J<¬«Iëc{tYút‚Àª®/ s…Ρ\m™tx¨ijnApK}†Šã÷Õªq:±»Î'(è`²1ìPü–Ü$iÅ¥¡u -Æ¡ë(/Ø¡HyÀưÃêü%µ*x-€¸6/'!ˆê Dgà †c!tŸFßÌ%„ÔxI»î’Ëÿñ9š¼“H6Óèàã‰êv »‰ {Ík r(›r)èçÍ9a:tííít£" «ŽŽ  ££ƒƒƒÖô¦ð0ÔÓŸ…Üöy°MŠ2ö°‚ŽE¥‚J9˜Z4çP£†’·¶¶R§,gäPܲóâ[Ÿ8 íÚ×uÁ²ð}>º»»ÑÕÕE}‡TTõõõ¡µµÕ ªF¡  òîßkŒ È¡È!Ò3){UAv«ÝixˆOÙç¢Yt¹˜­šGåÀÀKíÐ^U{°úðó€1u–žžºQ‘„Wkk+úûûÑÕÕ…ãÇ[“©…öÊQH· ÒíŸ*H?-ÈaÃéÝômðP>{ÞT[$UâT‚@‡Òdi͹µK_†>yÚÒ"´µµ¡··Á`ú©èòûýèííÅ'>ñ Séõ±@ª¿Ìg鯡×e9Ð3j‹… §<Û¢ä¿úõÿc¬&¢cEªÂÒ;;0#qñrê{âƒË×È]H.@O;ÅÍ!°Í»i‚VRjZD:™nä>FUP†{æ0Ü—Ã}ÀY$AˆI+Ïsíì«5‘ÙYaŸ`(5-ÔHEÓÕ¹¹åªYØ4›ÜHž&‘$¥‰!=ì°>ۢú†·æç—a‡ÕÒÁq#ETQ6æ 싎©ÒU*ì0¯(¸0;‹ŸŒßÀÅÙÙÊ…tè ãÑ(þbà|ú_OvhÛR/Þû<÷¡‡pdï©aƹ𰃃Ih­­Ã{ výù€H10– œNÀíÜ.ÀáX„˜XMG’+vÈû<6€¬P­ ^Ÿ÷«€#õÉü:1ðÎÇðã¶OãHãÁ’4ãx$Œo} ±ù9šŒ“H6Ñä•7ß^ën³0|ÕµÛH&Ë%xu0ðåŸrªvj‡v«ãÁò… èמ7u¶#GŽÐF=’m´´¹ï+_ù |>ŸeùêW¿ í•/˜€†x¦Udê'7–ŒEEÐÖœ®”ƒ©sçz{0ŠÑ˜Y¦Ó !'iÎ]”° p’œ³´îÜ<6 õµ?³vxüñÇ100@㩤êèèÀã?n:½váKÅ„^— ¾häfC¬„çôLÀá”[d¾ýÆ“+s€7~ºb­-X ¦^g™çй8È/¯¼²Îã9¼AI^3.—ibV¡ü…ì-@ •®Rî"‡tî8’°Ãò«GO ^½x}–ž†»‘›ÄÒNš€ø3Ÿ|äâ–[õn…îòQ ™’¿¥ÙpšyU]çÊ`ðµú‹Š¨Ê†M÷ÙÝXfw‡L°2§ËvØð×<`Æ€©…è«غ ’ÇT¯Ú ž3ì«@ a`c§Íí¯Yš ªë˜ŽÅðV$‚ùT‰¹@L”K€m4‹ŽóŠ‚W‡püÒ…¢ºÚáÄÛpdïÀ‚â¸9,©©º­µµpH¢³«;”þ°$X'¥qp¶ØJü<·fˆ²y¿hPEÓ[uƒÉ÷š¬NïÔÁ7-ÎÂé]Úý;ÐîßîÖÐ;~=o½Œ/Zk}ý þö3ïÂ}¿ú[äö@"Ù@£ƒ/OTß&NÈÉåúæFrr(ã¶È+§é—¿nÊÝÁçó¡»»›nR$Û©³³Á`±¦Þ:õôg!·}lÓS}•ÜJ<6šÙw6Þ~Èᡦ\ÌàÈÍÁØiC þâ@]°,OŸÏ‡¾¾>´··S" ¡žžô÷÷cppÐx‰MC>¹õpE?@Ù>(ççäæ`Ÿp*ïÙ«nsmàÑ1°Ú]W%9:”®­ÚÇÑ!åuón"׌òÖMÊ*X ß^¸B—D ¯Yç(Õa¸—¾ù å“ÏïÅbÿAÔ2ªäî@ÊC.“o3—[»‰?ÍÆøtîÖAyÂŒe‡Ò9B rdY Š5ÿß;,¨*bªš¦ü+H‚ªëˆª*ªc°°¼¹5_ØÁìR†™È—™<]ªt1MÃp8Œ™ØT;_¹ÁË›_O\Bï¥ ˆ¤€? ¡Fo>Ú¼‡wîNíä·¼Î3©ÞãÅnŸY†ø"Ø¡d1,f]D€øª²¬sýÉ9F!œ˜.VÁ…†.ò)G­ ^ .Ee@Û˜W«Ç‡îÖÐÝòz'Ρgìe F&‹Òªã‘0NÿÓ_ãåo?Kà‰$¸&Ì8<øvÙhþ@_"Û­J„hCƒùrUä°œÃÜUð±5•¶««‹6î’l«`0ˆtvvâù矷&S5 í•£övBÚñ+9÷WJ86æâü°©¶G*×é„€pA¦¤öAí³4χ~½½½ðûýÔ—HB©··÷Üs©´úØÿ…ÜôË€£ªpýTØ5š ô„ÒÍ+½-ò è_…(˜ïãÀC|*3ð@°ƒú ½a`3ôŽì½#ìÀyñ§Hu^ãt"¸¥wmÞL°ƒ]ž3Ƈ 8€Ç ¸œÉÿÛvX}ó1#ÁÖžCtØaõßÝ:ø&|sÜ¥§=¼³ñ Þù~|÷§q¤ñ`ñÖ@‹àÃß~æ]8ýÕ§›Ÿ£I:‰$ÚºÑð€º9:¬ç| M yØd Z% |ͰU΋šØ&í¤ÉÚ®¥‘~þïM¥óù|èêê¢ÉÖòûýèëëñcÇ,ÍW¿Ô mð/u>ceHñ¤Æ².^ÄmMÌ¥T œ×¢¹5t|K ½p¬ü¦¶"´y^‚! å6”e ¨F¡žû¯–Â>ŸÇŽC__Á$! qôèQÓ}Fúªõc„Ðk4Aãf—÷¦–ïs^Få²]Håøœ4Ç9ÛâŸXõã¹GÇ,ªÎ<ëMrsy¥\#—l¾[Ìöº.¯¼²Îã¹¼bîz@r:üMÊ*TŠØ/f˜®Pî"ࡌ‚„ì­\vCõ6ÒE"å%ÿŽfCÇO.,¬Ý7™â ûEsvÈCÚóoHcvHƒ ¤/Ç*`ñ×Õî,KÀ!¥©³t°èzÉ`3Î~ËúÑx4ŠW¦¦083ƒéU@I©U2w‡EÐah.„'^z_xù§˜Xˆü´m[êñ×ï}ϾÿƒøhsŠE>[ŠW&aw÷6l…ßíÎ9]TU—â©ÞŽOSF:œ$ýÿì½kp[Ç™-ºÀƃ H¤$ĶDFv,¹Æ6¡Dqrû αãTeꊙøÇ®"T&9žsŽåÄqæÇMÈäÜyÜØšLe|ïIΈJj䪙I$Ÿºžø‘‰àXŽã§ ˱äH²IJ²Eñ!>ðÞ¾?@R$°ì7zoôªbI$vwýõc÷×èÕ €P°òTœ*ß+~Ô‘‡B)Ä2fJE\.—š-ê@]8Ù¡þç>]hO´C®(ò¨ ߊÑíŸÇ{·üg ÷ÞŠX äH7åÄ6QÌ.`~ò¼îtŽ)~hÃ3Ø»w/Ž;†X,fÝøšzÒ¿ º8VÉ5&:08Wh+ŠÁß¶»‡‚ν­+WÅqáÒÂú…˜}‹@G¦ ÐMr°§‚47騷AçOY–g?Òé4'‰r0‘‘ô÷÷J«\ü=hñ,»ü€©ÍdÓxñåíÅ}µœ¨ÝF{T >"±úKª&‰_½{Ö‘bïºf+¾¸í:\×UçËfk"Ùk¢è[·Ÿ¶Cì3Å"ÊeˆJíæ‚Ï‡Íè8±ôãê¶”OH…Ð@|æ‰ ÌÂEMóõ®Ÿ)1S*Ö<ÓˆtÀWýÒwJAÂö~ïR²Cõç4¢’$ë¤Ú2ûÂ1ŒôÞŠ½WïÂá™Ó?Šñâ¼í½~™øðꯆÿÕW°ë¯¾‚pg_èsp´FÔH·Íê•Ôò]êh2çV>Ôs.÷v_¤vý+'©;ôööbhhˆ¿ 8<…d2‰±±1 àù矷&Óâä—¿ ßõCðoý¼CÝá Ž²2¹¶ê°”™¤ÿ"˜T*Å[;®í.›º°Þ”-¿+ï? ù݃–æ988È ¢®Âèè(vîÜi(­|êÿE ùz$6c›E~I¡Åö –¡@×56Ô›­Vq`Öì’¦ŠU²d%ã¹ÅµjN‘(e4ˇ²Ãê¹) €Æ%Ð "hH}¾ŒÂÚ|ÆnùÏØ¿ýóØÛêÈàŠl`âøô'²ZÝA×…~íp3™w”V«8–okäJÆêåÚ8Øwþ·  ïJ;::Ê_NžD<G:Æðð°µãíÏ£ÿrÎMOpkŠkåäÚZE‡Õ™Ñ옡>Æá<Œ(kй“uÛ¾5}¾S€wÕV å!Ÿü‰¥d‡X,†ýû÷ctt”yW!™L^ѹ“Uó¦Ûb3ÆGÃæñ=Ö:˜7–«8¯—‰æôY¡ò Év“U7™';X‘5{dðE®ÒoÖì¾kc0~A;WxÐ:ö¹ ¼‰D"°›O ^†`@ááÍÙ™5‹Zýd‡êUq-Ù4^D¯&;µ¢i„4N§F í±†ìs¥’f²l‡kó& j|åË:Ùa®TÂ+ˆ Ës;ÙA!%8³0‡þð~˜y9Q´µÈÕD‡Dõ<²LthÁ )@|Ø_dO:Asº™bD†É|ÞÆp²ƒi|>@€Ph‰äàWgÑyÊ Ö/“xZDv(É2.—K Ÿã¹E,ˆe‡Èàd]Ÿõ(¸SíA;äºÓ×Ðæ›‘î¿Gn¾{º?âÈhàÄŽÖbâøKúßbVt})Å¿Dv›K˜%8TûÏkŽw¢/ºÎ­1V9mì`ßîÝ»ù åžÇÈÈŽ9‚X,fݘ›zÒ¿ º0æž9£f>mg’Cm†´8¥;·d2ÉX `„ðÀ,±Ç1’mQ–Îùœæ& û6”™7,˳¿¿étš+aq¸{÷îEo¯± 2å3¿pYlÆI|O ½÷ì÷_›“ ‚ Qý~ÑÒ”3ýÍ‚øpíI¬&yµ¼ Yi@U^¦²6q!‘ ‘«õ'’  ‹øb¬M!vö‚úC¬š×›H$úx+5'hr°žRkl¤Æº –5ûêb!¬$Úïo³D«ú…[ÉÕé#JEñ!&×UõIÅ·âð_Ü÷nyƒ›orddpâGk0yö-ý‰º 8Éí@r ü@ƒ÷ú¢k4´Æ`åôAÀ Lø¾}ûø‹‰£-J¥ÉdÐßßoÝÈ/NAzå›PÎ=ebÞppŠjù|¥£\ËMl·´a& IDATž!-p‡CkÛ‡!+$çæåýg!ûhiƲ<÷ìÙƒt:ÍÇ9‡«Ç ¯÷ivÊÅßs’ßà{ºŒåJí¾N#U?@lQxàD‡ÖÅßíCtX] 1¤òpš/ÆÚ\åÁýà„ï‚Ù ƶóÖá`f”$EQ¥ÔURXõ‹ÚQ{R_(A3Ù¡æÓ:du’DU}«æD±Êu( Uê¤ÊžºÖ.FiëÉDs±M?•ïÌÏáõ©)̗˦ú©^;õ—ã0ÙÀÑ‹à+Ïÿ¿z÷¬­Å±LtèìÚ¸ ×Åbøô‘—$(:UQJH7fFEÝø³Õh¥ „ .©8*$‡vòÁrßivh^¡æëaBu¡(KÚ}E©bÁœ¿í& Øì/Gê`%Ùa5 h\‹@@ý¡¾p £ÛÿïÝò†{oE§?hû(áÄç0?y¥Ü¢¾DBT¿ÂCÛ“`®^Œ»Ä[$ &¿·ÜÔC.`È`  œƒòÞ“†’òC|m…¾¾>d2<øàƒ–æ+ÿyò›?¤;ëŽ5E1z³½íîÑ—!{[Wî±X ñxœ,í iG†!u¬:LRòÉŸ@~÷ ¥ÙãðáÃ|Œsxؽ{·¡´ÊدÜcºjOÀ%õråZ¹†vÁÐhÓý) Ijð <@\”’õë-Nt°./ÏÖÆ$l„ðp†/ÄÚbg/Ëæ¥x 5'P­ì°¬î ›ì€ÊÙsÍÑŠá°®AĺbÏç²xyjóyÃæzŽì@ @+ªß~åøÎ«Äd!o[q,ćëºbصq:ÁÅ“*';h_uû*ĆÐÉÁïo"íãuæí +æÛÒäá}?ñé"ˆŠ¼¢¥»Ïyì ‚’"#/ë'gµ2B¦]â ©÷Á¾p #½·áÜ'þߺæ“¿íf-öíNœxö_øšžƒÃLžù“þ·Y3uÍg½z°œ¯eÛ-«˜Ss0å»v8È@=äÆH•)ÊØ“Y×"‹add„¿˜8ÚûöíáC‡‹Å,ËS™zÒËß]kí„gÁ ±ìQ[ß±’¾ýSNãÐÔï™lTt`x]KsãŽ}ÊÌ–å‹ÅpäȾFâðäÚÇÐ8+NWTXc].ò}w_|ÀöòÃÕõrz}fÉ¡æùèý5¨Qy æ]b2/Ò<ãÖ‡¢Vù«Q^Ôa»h«ÆÏø"WëÏ…ÚŒŸ[ÞÃ[¨98áÁƒH$}˜¤#QRd3o$Ž–a²Pаn¬îå57¤“Æ 5#k˪G\ ìiHv0F¢XJÚ$iJˆƒÆÈ €¢áp+Ѽhs dÔ0ƒ¬(âõ©)œ_€¤PNvXy\Qu¸ç·ÏàÅ‹ØVTwþÇîϨü­%:@O8‚]›6ášÎNÓy T!B~¿e½‡ý ·Å6ø|€ ¡€#$‡–oæm·ô7S#qàð~Ä0ß§ÜBv°âC¼,áýBg²søóâeŒçp>¿ˆ3Ù9œËeQRdû|iTÝAís€¨º¡\!>¨d„ñým·£të#Øýç±YˆÚ>zæ'Ïã©=„Çïû'>ppXŒKgõÐÓÏöºÇ­`ÜwijËëå.P¦Í¡…É áÁöîÝ‹¾¾>>r´-ÉdÐßßoÝ-NAzå›PÎ=åü¦':˜ÎŒfÇu=ÏçP6ÔZ¥èÐÚyFž8éØ0hiƲ\ûûû166†T*Å»6‡çL&188h(­|ö—ºIÖMF †Ìã*£-…°ð®ebhx½Ú«â`ä[>#*´8eM{µBÕÁ3ã¶ÝTêH"ú @ ³|!¶ ~qBñ"B¹1„N¡cá䚟Pn Bi DÝ?#ûÃ"›˜µ/‘H ðÙäÝÅ]àI0Ûñ¹º‡•ØtƒþÅ÷¥B>ß*ÚAõáyÒœì@€U‡õ4ˆÊßЄ¸`”ìíd‡òEƒzDƒ‚jªÿ(ø|è ¯| ‡ìÍn¶ˆì@šä«¡hÕOÇñúÔ²¢Ø¬úÆÊr#ÙaIÕáb>‡^|ßyåȉö,‚û»{ðè§nÃcŸº ×uUÝ„ç[":´âúqㆠ[D:|>D]Ï[£(ÁÉu'6¿-ÉÁï[ë®VïÕµ¼4’(mL‚sB©€1!±ÙŸVÔÅt»ó¤ L• 8Ÿ_DVk(AA–p.¿¨Nz`‰ìP¨/ƒvÈu³Ú|3.~ò¿aÿõŸGo(fûHZ&>üÓWïÄÄñ—x`ÀÁaŒŒ%»Våæoµ±a°^ »ƒ9ËÜÎû¡{Ø6G9}аºÃÞ½{ùK‰£íÑ×ׇL&ƒ|ÐÒ|å?B~óG†Æ§ñù¡•sW+ßÖd¦—ì°Ü8ÚUs -([¯‘RÒ‰ïC™xÒÒÜ|ðAd2ÄãqÞ½9< ÃÊ%Ròùß8¸–àJîÛàJì›ãU%‡õjŠCü¢[õ×®p®#:Púƒ yµ1ÑaõÇ$¬Ÿô@gO·ýŒ("„âE„N!T8¡|~¹ŸŠÏýrBi‘ì„rcð9N:µbl;Ëæ¥x„МðàM0ÛñÅÎ^Þ:­Ÿø– ’Vÿ¦Ÿì°Šfàk\ivH_Å4Uh¨±b©îÓ…ü~DB3Љ4´E­¼5õlDx0Lvh’βòªÃØâ¢–ê+‹¸p°-ÒýÕ»gð•ç‡ã3Ó¶³9ÒG’ÃcŸº Éîž*¿-Zì¿DG>¹y3zÂaËóÞ´2îšÏ}WE­¸}œ“j&´’Cµ$Vüà²È-&;¬7›Âšóð²–Ðdú¿5Ä ÓíÒ, d%—ËŦõP(Åû…¬µu°“ì°úó\!>Då ‘NC›oÆØ-8F|¸ôîÛ8øðÝ8ø»9ñƒÃ$&Ͼ¥ÿÍÖu­ÁC8mòžË]Â4ÉÁ”ïÚœäມè’ÃÊ#…IÐ ÿa(û‘‘~ ƒcöíÛ‡C‡!³.¶P¦^…ôò# ‹c6ÏíLr°®ÞWnOÕ~ |»AÃa:7­S ‘È[éw€ÎŸ‚øêàóïX–{,áC‡°oß>ÞÅ9<¾¾> [×\xÚ•©8&8´ñ¾'8´xh´q?4Ye+Ikò5¢ð¿`vùNt0™—牀¦=Þå~êÑŸûì™¶^J³eÏB(_V%84‚_. œG(7æZÅÆ/lOñ¡18áÁ›ØÃ' Ž‹` È0Lv¸²'õó®ª4 _¨²·‚ì°lÏUÑ(‚«qV¥Ktt` À0Ù¡ÑMÞ¦ÈõýlÙaªƒ~ãZ7ì<¶S¥êðÓ·NØ¢ê ^¿OÜq>·ekí@÷Ó–¯tÂþ’Ý=Ø_€ÏcB~?®ŠFá#õ;_$À–ÎN„,R–à/TH&bòVï ØIV 5".4²U–™ñW—Ä5•–°>¾2Íúܲ,!;ÀT)¯¹¢¢ +‰ÖÔÁ)²Ãê•API ë´ú£öËbN¼ù>|7žúÑ^ÌOžãó4‡NLžýJ¹E}‰„(Ù¬aá*nqÉj‚ó$à ÛôPƒk î¼µQ9}ÐPQ½½½\݃CH§Óèïï·nh§ ½ò”sOY7O´|ÂÕQ®å&ÚWg#Ä®ðÐZôöê»øÎŸ²¶ß92m(ÄJyUeS@~÷ ¤?ä‚e¥ô÷÷#Nc``€(޶ÁÞ½{<-Syh’ƒ×``íG=V/fû êåD{1¦äP·ŒPàè¯yiJ¿«ÔÑå&›-Ââ¾aS^Ôa»(CãªÎdž.·'áÁ/Î!¼xÁÒ¤n¢CM^r‘ì ﻎø E6ƒúC¬š×ŸH$úx„Pœðà1$‰»“Å&P˜7’›–ç‹çu§‰nìq]=šþ¥ÙÆéH½¼[Hv*·Vo]·=‘ºBA>|„ Spug'º‚A4%;¬ùÕƒõÔ%;H Åñ™é+ªªínEf}kÙa‰èExç$þú·Ïئêp×–­xâ?Ý…Áí7TUŽVŸ2@<¹&Ú‰]7"²¡Ü)ØÖÕ…DGºÃᕟ«¢Qô®[g!Ù¡Õ–I¡`å' äÀ’€¬P,J˜Épi1Ù|³ùbåÿ¹²Å2d…6÷“,Õ'´H¡#À¶Î"ËãHå™.!„îPX{›S›ë¡uÞ6egåQQ ªä_Rdw’Öìä( ]͉™~Gn¾»c[m‡'žýW<~ß'ñÔö¢˜]à ‡F\:û'ýKƒ®kLœäà&—0Kp0í»6?Ôມè[ͪ;ppp¨#™L"NcppÐÚ8þÏ ¿ù#@Ê™œ¶Ø¸eÝÙ©Ö¡…Nxh-ìõƒ~ÇÕœ›g(@³2ßòþs––488ˆt:d2ÉG[!&?›Syð"É¡ö¼¶'àE%×5‚µë1 NuðRy8¯Ï]&æ&®ê`EÖnPuÐOtXéÃý„fA ³m³ÖòÉE„rc>€J–æçÉžPšr•O¸Êƒ‹û3wçÀìubl;o·AÔ#I´Ç]„3d‡œ$ª?£‘ì@ªo{gì Vûõá6wt ¯« ׯbøP4Šh PïqÕz¨*X¨ne”ì0W.ãåK“˜+•´; Z 3•ÖPŽd™éi|%ý;xç”-c¶¿»~ê6|+ù1t ÂªŠ±CtXVu¸.³MÕAuQGº‚Á5„‡NA°PÕ¡ Éj$âÛÒ%¢C®€¢$C®™Ï $YA¾,a&W@võÜYm#¥€$«‡ÍŠ>B°%Ú…D8Š?°ò÷ˆ?€D$ŠD¤C»¿©É±f…rƒEd©¢?·“VXR|ˆ‰@@=ãTl+Ò7ßë0ñáýÅ9ñƒC&Ïè'< »¿jRà$Õd\ÉÁAßµ¹’ƒëªÏ°’ƒ UwèïïÇÐÐ!qp4@<Çèè(öïßoìäzãvêUH/?¢MI`Í\ÁÐ-ëÌoŽ®ùVCuvïÞÍ‹'aÏ¡:Se;’%[jË?ÊûÏB:ñ}Мu ›±X û÷ïÇèè(âñ8ïòm‰‘‘Ý*9**c¿2ƒ1®ìH&òÊ~€Îz¹Æ ¶—%æxµ6qKTšÚÕÿUS…Nt°./Nthn/ìÖ_òìé¶XcJ³æÆà·P•N Bi¡ì»ðÉEWøEììeÙ<.÷×hÈsx)V cœÅц0«ì°|KµQe‡ê2$;@#Ù¡&ï†éHÃ`¤i”FiÓ€F_øc$]ó4çs9Ÿž´ú–òv&;,©:\ÌçñíWþˆ¯ÿáLò–ר à‘äÇðاnC²»ŠXÅÑpVÕÁáÓXlÚj±Áj’u¡t@’\ΗP¬GR©±!_–0›+‚RºÖF  ,«‡Íd‡ÕÏt AôF×áú®õ¸¾k=¶DסKêÊ£áX³ÈN[ýUõYØç×—?â³ ­œ";hüÜÐuhćRnGù(öíNœxö_øâŸƒ£ )<ôÜŒ¶=XÞ,)s+Y† ¦|çÕ/’u8ŵ*^9а63:û¦au‡}ûöñ—‡F !N£¿¿ßº)¡8é•G œ{ªñœQûK‹&/kebƒ¥ŽÍt,¸ºC›¬‡¸šƒsqõ¯RòÉŸ@~ï ÀÂÃBýýýH§ÓœüÉÁãªoÊ…§¹Šƒ;ßî ±]×^–«8´á:ÌDµY 9¬±ÇJ…Nt°gê„]-ñ%5õñš~¹Zñ‹<¿¶ ÞG°4 ŸCëWJæÆà“òÌû†qÂCŠGõÁ B"‘èÐÏ¢mÔ‚Ù̉ƒ˜%;¨þ¥Ù¡ÎB}yµNj© •óµ6‘ˆ†ši °4Ó ˆ†œ« †#¼&Vi7u $EÁ;ss8;¿ š íÈKD‡¬(âÀ;'ñ׿}/^üÀ–ñúÅm×â‰ÿt>·¥ê`¨oIÕˆ7nèv\ÕÁ¡ÓòØ”…ø¸ê¥`’ƒÇÉ”R\Î!)Jý¾CëÏ©—ó%Èòª$IüÆ ÙÁî>áN²PQ¿XQ“Ñ@vð‚΀`O[Ùò¹Î¶÷´SñaÿõŸGo(fëXŸ<§~ôþé«wbâøK<ààPÁÄ›úÇéÚæ!ü6ƒñ/–™%8˜ö]~™ì¢~ǼÁ6ÞrnTÝa÷îÝH¥RüeÄÁ¡Édétƒƒƒ–æ+ÿùä7H9®æàÔ^· Z¼¤;;Nxh=ŒÜÎO³ãÍûœíÝ‘’[j+OÌŸ‚øÚÃPfYZòàà Òé4’É$<¨; ©<PVT\p9€!óÚ\ÕÑu{Œ¶—isÚ¸Z¤â@¬½Ïáâ" ”j}¤þ‹._©;_ûŸí76åE¶‹¶jÌY7W‘ðUú­˜=ãéuU°ð>â¼ós (ÂùqøÅ9¦ý£c"›X5/–H$xX·qx )V ãê.]Ö·›qÍ¢¹¹À Ù¡q€ß·ô;iºˆwŒì@Ô,ZúˆÔKGFfD ÙX{È•h l´‡@2ZIQp|fó+³u7ÙÀÓç&ð•ôïpàS¶ŒÍk»bxôS·áoÿâft «ÆvˆÐŽà“›7£'öò Ép@lлH¬øÁòç ¥Å4?¼/É fsD d@–õ׃%²ƒ™r(±æ ~SB…õd‡el uh";À¦P|jãM—ˆCýž·QñahóÍ»åGˆ—Þ}¾¿q7æ'Ïñ`‡ƒc “ÔÙnŽöaèËIÆ¿ÓäJ¼µÑkJ3¢³'@gß2”³Ñ[\98Úñx£££Ø¿?b1ëb eêUH/?š'98]½ªêÞT]œ@Öz:¸.å`Çá:}²UY2ªæ° ò{!½õKUb±öïßÑÑQC$/ðÊÃä †È‚ìÆe|/À}n`ôÂÓfµ¡º¨¾c™à jot‹~.«<8©êÀDw䪶ùÎ ±(ԣߚŠ€T€Ñ*²Ãj„ À'™ö“ĶÊà ÔÁ ÞBŠU×á¨;³ë±oºa»ë«Mô>Ó@Ù¡îÁý•4z•&+ŠxùÒ%dEI5A[‘–T2ÓÓxèÅðÃc¯c²`½YT0xýül÷gì® L|”™ñaG|=nܰÁƒª;#ëdB¿ÚDr`É6£ JeýÊÕ>¢sE,dóú74´®7M" æËiú¹¤ b^¢IúŸ€D8ªNdXJï#‰p]BÐd‰=c“6™¶Ã2ñaø›>b?ñaâÍ—ðø}ŸÄoÿqÅìy8Ú— HìZ—ÕÒÄ7t.Qq`žà`Šäà% 8PÕ‹ù~g¼^\݃£uB:F¿uâß´8é•G œû÷MbÖ>Ú¢ u¡þ2wRwüàt{-™˜êóRsXy27)ó(ï?g©ýýýH§Óâ}ƒ£ÎÚÆèºFûµær¯«:º+Äv±-ØðÌ"Ë‚*»ä°ºâFThqÊ9¢Àˆ¢':Øb£öÀ:À:ý–yPå²ÃŠ-¹1EdÖWŒ_àžâ‘:8áÁ[`–Ùà .]îf=_ÇZBÝ'f Fv :ÉjEj!;ÔO×ndR? ²Ãñ™H UMÐ6d‡¥¿óyüàØëøú^Àñ™i[Ædw~ö¿ƒÛo¨Z±°¥ê†°kÓ&$::¼>;ê‹SYØgP›Œü~@*$!ø< ‡6(ò%±~ßÑs(\VIBQ’1›/‚R­ÞÄžþ¢w˜õ5%ÎŒ-êÌî‚èíèBw(‚ˆ?€ˆ?€.!ˆîPWE:±-S';X1?Y¡Ñ¨+ò×J|øx…ø „lǯú9¿ï¼úëŸñÀ‡£­1qü&<7, 8pÇÜÎonWqhI¿³¦^fÔöíÛÇ_D ™L"NcppÐÒ|åÓ ½ñÝ¥ÛçÙÌp›šC]›5”mÀï†Ô8Zÿ*ÏMØÐ¿DÚöDì,Ûh½›˜¢¼ÿ,¤Ì0hÎZ…Ì|étšU›â„ŠÊÃTë 7´´àûîÞð’9œä`n&9¬®8‰nÕŸKþ¼)ê":pEkìj‰/©©uõ©ÈUú­›=í©Ï/.2Cv(‚ùsÌúKììõ‡X5ow"‘à·Z¨ö+O ‘H$ÄX´MŠl‚ŒñFr#t¢=Ý®ªžQ²CýtÈ+¿ êFxRõ V²ƒz:Ò¤üÚ ZFv*„ÆÈóù†dÓýÎÂ<Õ³³âøO…èExç$¾òüïð̹ [ÆcTð·qûÔmkI„-¢ô­ëB²§a¿ß£“¿GÈÕ$¿Ï(C>° %I†¬JLÐIvd@º¢ž#)Óù"¤ºÊËy0¢ˆ`šD`ÙÚ0¦uûóJ‚χî`[:ÖaKÇ:$ÂQt at„†êÚÊw1Ù¡* §2è:©1ña×Þz«­Ä‡Rnÿñø¿ï˜8þ8Ú“g (<ôô3ZßÒq‡¸Ý«_$ëèP®s4˜¯—Qu‡ÁÁA~؃ÃBÄãqŒŽŽbÿþýˆÅ¬û>†Î½ ñÿôòÛ­›È,_8Hr Æ+B³ãºŠÜ½{7 Àˆró©9´BÑÁj+(MCzëûß{ÂRKb±:„}ûöq%s©Ñ÷šã*\ÉÁx½8És8ÉA/¼CrXS¯Øvý9.˜òa­},N“Ô¾¼¨ÃvÑVõ9ç^ƒ$l€ð°x^QDöêãWJJSÌúñKÜSà¨'ßÉ!äÉ?8X¾(Éê}Gë¡o @¹6ª— %D©NNà ÙÁ ;µ”Ó,d[|É2ÙýüÊb‚FeÐÎUćUÅÇaŒl½ÍâÃüäy|ønüÆÝ˜Ÿd÷¦ ;pé]ý‡÷H×6–¢wx]ɽ p’ƒQ§¸îâJ®äÐ4'ê###ü%ÄÁa†††N§Ñßo!ASÊC:ö]Èïý«…“™µ¶(CE+W/ᡯ¯·Bγ¼p0e«×OúnÎ]>tG/…xì; óïXjM?2™ x_çàp ^°]åÁP8EáM%õr m/Óæ´q?´äà^ß4©c¨ðGô— ãs}¢C6cÂwίM-·Ëñáî<Ña¥E®ÖoíìϬ„âEøÛ…Ò4ˆ"2i›Ø¹•åfMñ¨ œðà0»KRŽ]Ï[ÇЩîÑîRxhH0hôxƒSõÍ”V¤*M£“ð“HU-';Tz¿y#ZMvPýÔÙÁÙ®mæXP%ÔÌLOãžçžÁ½nÑ!*xdçÇjU|ì©:ô„#صq#â¡8Xëð*JA¡Brpz¶ÙDE1žXQ± (õ7¯(Kb}҃ݾ v+"‹ìÔ0Ÿs²ƒÎωuý‰6Y}еQ9Ê ²j„¾L|È$¿ŒÁM7Ù:¾'Þ| ß÷IýÅQÌ.ðw ‡çaDÙ„tm„οø- 90ÃP÷œj/¦að@ƒëêæ7·ª^fÔøÁ\ûL&‘N§188hí˜ïß ½ñ]@ÊÙ7Ù¦æ`ã¼^·åJùÊðy• ¹½_/¹E[‡lE–,´«9¬º“òNþÒéÿ ÈK­F&“á㓃ØRyà*ÆëåÊKÜž ª—íeïÜ¯â ¿â¾Øý%–¦5ûRSûµ¼»r¢ƒm¾sÀXÐy©H>)€”eÚFVUÊngÍ«õwîÏ€Y­Z‘+<¸3<˜=­;›Ì’T•å[­ì@ˆjšš‘j~w)Ù¡©÷›'¶JmÁN²ƒSêÆÉWˆ½ø¾þâ ˜Ìçm‚ŸN|OÜq>·¥Š-ËÑ!@|¸®+†7l@Àçõe“‹Ôþ%’C°òÒ¢-' -ëeZQu%õgUþ¶XQ–W‘+Ì®×òŒ Ž(Lp²ƒídC¾#ÿjŠ IDAT?PÐN©.ñ¡/Ãèõ‰÷v=€=ݱuTýå£xü¾[pâÙáS‡§1yÖÀ­ä±k[´à0øã_,3Kp0í»6?ÔÀIŒ˜bo½háWwàà`ñx£££Ø¿?b±˜ucîmˆø/ —µ¨dµJÍÁ¡y½!ÉÁ¤‚NvLwšT*Å;>H&“ÞXËx\ÍayH™yå׆2{ÌR«z{{qäȾæáà0 3*z‰ƒÖMoí ìè%xMÅ¡Íû¡EâÕ±ÙÌQý·œÓüù¦>ÕÔHœè`]-ñ%5õ±• !ýçœd ò û6ŠóLª<(Á”`ŒU·õ&‰>¬'¼ñ:¾þâ 8>=mÛðÛéÀ£Ÿº ÿý–O¢SV­HØSuè${zpMg'¼—ü~@*D‡@nuzÿÀ‘¾ÓÈ…e±BvP¨î:,KKÏx…ìà@œìàN²ÃêôËćˆ¢ê‚¾p ‡o¸Gnº»cöÉl–r‹xêGáà7îÆäÙ?ñù–Ó¸d o“.§¨8p%}Ç•Ü{ ÁK$gëÅÕ88Ü¡¡!¤Óiô÷÷[—©”‡tì»O07©Ù¦æÐŠ9ÛÚ²ÜøÏçV¯¸æO±Óç=¨æ zxqYÕáÔ?X®ê°gÏd2NBâà°fTäóO;NQî謗k\À`{Y®âÐFûR4§; °­­‰‘[ÎëÜØNêÚÍÐ’²‘]VåE¶‹¶ª/²2ðUúkqÙÝ D™WwXF@œcÒ.ÆÏ7óÀ² œðà 0+_"vnå­ãFHÐÅ º“E7v»ë¥¯aÉ݈pöû¯üÞ„ì@j ¤‰ºÑLv¨Q{h!Ù¡-gÅÇYQÄÙ…õÖãÊYQÄS'ñ×Ï=ƒgÎMØÖjQAÀàöxâλ\­CØ#:@¢£Éîžµ¤ Ï‚q²!K$‡ ¿¯E; ›Œ•_ï`= +@¹\!;(Æ •) ÉÔ_ûýM‰3mÊÉ:?gŒì°‚ºN ©R±­Hßt/ÝðEô†ì»ybâÍ—°ÿkŸÅoÿqÅì•8<…É3È<¶)<˜ø†Ž¬q»³‰†×40Ø^–ú®5õ¢…K þÃPZ~Ó1‡óH&“Èd2xðÁ-ÍW9÷ï^y´8…Ö©9Ø’¡Æ"웃9áÁÝ0t8WóMäíªæ äP‰EÔw—ìRuˆÅbØ¿?>Œx<·E0¬òpáéæs«éK¼ë9ê±z¹j{¢ ÷¥,¨²ûUìkoŸ…‡Ò4 ”š¬¿´§gæ†vSu`è°Ò#³î&<°J"Pƒ¿Ì¦­bg/ËnÇÚ÷w'bÕ0‰í £ÞÀ䛺ÓD{ºíéqMu“j½„ü~Cd`‰ðÐÈž¦¤…Õÿ% «Bõ«~u€Pª»åˆš¿ ’ŽÏÌ@R;ëq²ÃS§pÏsÏàÀ;§líŸN|?Ûý n¿¡Êp67`vÄ×cG|=¾vX&1Jv ø+J¡`…ä@XÚA[“Ÿ5d‡e’ƒ(Uˆ’(ÔƒeY1GÓq@-ðwÃð‘ìVàdû|g¡%âCP}, t_±?€Ç¶ÝX d›¯ú9¿ïœxö_xÀÄá ³ ¸ôîÛú—)Ý7ÛðR§Æ“1ú&³Ó¾ã$wUß‹*lÔ‹«;pp¸ûöíáC‡‹YG˜¦ÙqH¯<eêUÛ–=- PsP-¶8¥ëy£·_s0´jÉ;ÛßueÉ‚šƒ6Sb”òß;h‹ªC?2™ †††xgæà°†U¤¼ºÊƒ¡)•«:ºkO€Áöâ*ÆëÕ¶$‡¬Á Ñ-†bÝDê¿r¢[m `à ꯙK¡YO\t­T‚O.2g㺧xDPÕ¸ ÜD"ÐÏ¢mJ0)²™7’CŠK't§‰ou~ò/çó¶åÝŒìÐä¿«þFÔ󮛎4ThBv MëaŸºƒQ²C¥ÓQ] IÃÓЦK¥-ÉOOLàžçžÅwN!'ж£Í‘<ú©Ûðßoù$«Œ¦L’ÂþvmÜ´ÖVOƒA²ƒÏ·¤æu Nvhyù‘`piÂ’ ˆâ*’ƒ¢ÝF‡Ë)¥¦ó°ÂãÏp²ƒ¾Ï]HvÐ2×Zaÿ2ñAPxïUÇØ®0¼õVÛÆ)·ˆ§~ô~ãnLžý88ÜŒKú0éÚfÑ‹œ+9´dýdêÖÆ6>ÔÀIŒ˜ÂN½¸º‡»100€L&ƒþ~ ¿V’òOüòÉÇ)góôå ɶn¦s'u=ÏÉdl!•Jéoóâ´3ý]7ÉÁÝjËPæOAÌ|òûÏYníðð02™ ‡6´ÊWr0^/®äÀ@߆ýФ︒ƒQ#*… ÚÚ–¬³«%¾¤¦>v¾ÈÕúk7{Ú•3)¡2ü«TVÜ¿ÄAƒúÃ,“b‰D"É#‚Ucœ»ÀõH±jãì'Žº‹ÑYC„‡kvítÜÖ¹ñ ÝinÚ°A%Üi Aý¨=iDv MÈK7Çk%;¨“$ëT¸†ì 3!';¬õ»ž¸!3=‡^<Š{“6†¢‚€¿½ñ&)ïB›sLÚ%uö2½æÁª>Ä]àz °j˜ÈöDÀQÊ™ßJ·é†íî VnŠ&uƒ¡ÕÿQ;jð‘†Ñ”•ʚȤItÇ2Ùaù6p ­";ÀÙ…dE©n/‘.æóxèÅ£øú‹Gq|zÚÖ1öÅm×â‰;î·]We0e–ìз® 7nØ€€¯]–EŒ´¨90µÂÊK̉rè¹A®Je Tªü_¬¨8têïB#åXAvÐ’…BQ$ÌŠ˜)0_,!'Še¢¬€®"ƒHŠQVP”$äDsŦrÌ—J(-“ ù‹´f ·Û<¨Û·6‘VGò Ð!«V§/Ãèõ‰#7Ý‹Ý1ûˆè¯ú9¿ïüùŧy·âpŒ¨”î›íq3þ}¦7U\àx'œÂUX§» Š9ÐÉ?JÊrp°‡}ûöáСCˆÅbÖM‰ÙqHo|ʹwÇFHÝ"Z8 <Qà°ɤËsãàj°DÍaÊÌ(¿ö0”Ùc–[=88ˆL&ÃLJƒ0OÐÉßkœƒÚ”àp‚a|îKYМî'9°Ý¢¿F¥)õ:1QU®ê`›ïž²|a# .%<(E×ÙLd6m.Ç®gÙmà¸Òï¹ \«†1>p¨½À ³ ï¿¢;]|ëD«oqg•³‡ÆÉ•ìk?f²Ö¬¶žì@ª2𤲃¾b×`|qó…º ¼BvÈŠ"~zâþú¹gm':ôw÷ààwáoo¼y­JÃD‡ñáÆ Ýè[·®fú“ô¨9Øe û!FË·%_ È I«È e \®üMQ*Ï€ÔØ°>B(àÓg£ÎÃÝa!Rý²Â&I”RäJ"¦ E,–ÊÔÈ{Z¾G %IÆ|±„™|9Q\EÌÔ’±¿oyNÝX—¿ ²C^q¹\ÄL¹€©r3å²rYsÙdyLúDe ¢N|HŶ"}Ó½Øÿ‘Ï£7³e)åñë‘/ãà7îÆüä9Tq¸— +pêîyîYüêݳ¶–smW ~ê6<öéÛè¨_„ÝH§S°kÓ&ô„Ûé]ÕB²!úÔ˜ÚaÀËöcªÈ ÅR…Ü Š5‡rƒJß¡jÍJ ‡°.$Àß é£Á¿îƒå„¬ ú}a³"BI–1/Ö’L–!SŠ\¹B¢(H’5umö ';Øà»Æd‡™rgr—q¾ÅT¹€±ˆËb 3bïs/,@¡TÙa5ü¨Bê ‡6ߌÌÎ/axë­ˆìÙ¬™xó%<~ß'qô?æó(f0?y^ÿ’¦ëZ•ÉÀ$Áq’s0å»6¿¹ÑÕê!S\ÜÅ”±ÿe(éÞ½{ùˇƒƒaôõõ!“É`xxØÒ|•é× ½ú-Ðì˜ÎùÑùÜÉrõÖ9;®+—X,Æ^38¦t÷†Ò´5݉…±¦µl ÕVæUvïÞ±±1 ðË598Z£q…2þkîP}ñ¥kBQÊž±–˜CáÍ}©&õj[¸º­I§~ÕpZ¸ sMj·ßmÈËóDh{—¸¥‡õ“èÂy×WŸì>…–í;{™õY"‘Hñh`©ÿp¸Ìvd‰á €£^ Ÿ6,ÑtÍ®®©ç¦HDI@õ“5Ä…ß_CvPË@•ì°–5И접| žŽ=²ƒö@ÔÙ¡îܤ(øÓìåÚE‹¢`²ÇT¡€…%ÂÃl±ˆ÷óyä%‰­…y“cGG?ø÷<÷,¼s 9Q´Íލ à‘ÃÏRŸA²Zá…aUHtt Ù݃°ßÏ'»á÷Á`…èàwé²ÓÍd‡e‚ƒ(]QnXMn°!€îh:*ä‡h0€Î€x$„Ñ¢A]á â‘ý€‚õ‘P­ºƒi™#;dË"拥Ƅ8 Ô#Ke\.m-‡“ìð]c²ÃT9±¸¤6¦Ž’"ã\a±Bz¨³hh› 2¨}(cdëmÈ$¿Œ=ݱmÚ:úËGñø}ŸÀÄñ—ø{ƒYQw Ý7ƒ«8´p=Ä Æœâºê3z ÁKõ2åÂo ÝÔ¾{÷n¤R)þòáàpFFFpäÈÄbÖ©ÃÑâ4¤Wÿò{ÿÖºù±¡šC+_2õ|6¥+7®îÀ&vïÖ#?euwb³Ï[©æ°œeiâ[ß·MÕá±ÇC:æä"Žchh½½úÏ¡Ðâ4”™×=¶àµPÔë*^CƒzYà;÷«8¸»Í}]; ̳SŒ¨:Ø”uØ.ÊИvéTfDåÁè™I#]NaÒ,ÑáËA¤xÇYzOq¸ÌvärìzÞ:nzf¡œyÚPÚ«?ºÑêÃÎ!7mìÆI¦5“ZUóûª?F|þš‡‰²°Š9@4“ªÕÚ…ì`äãFüéò,¤ªS†’¢àR±QVjÒQJ1[,6%=8¥îЈìpf~½xßyåeLæó¶»¨ `pûª§fÀë¨ TÞ—‹%äE±IÄš~EQV0](BR#§p²ƒÎÏ ;4þ—Ã\©Vñ`¦\¹A»QÖ—Kõoón5Ù!+ŠøÁoàoÒGp|zÚÖ1w×–­x⎻0¸ýt Â*ãØ':ˆ7nèFߺum8Ë;DvðùA¨¨9üæÇ';h·¡ZÅA2Cp¨ê;Ô«ž üèŽF°.$ ðCðù!øý ÄÂ!›Èæò¸\,A”åæd‹í¤”b¶PDa5ñŽ“t~N¬ïKæÚ©¬+·¼,Õ¬tÛæ‘+?*æ¥b[‘Ùùe<¶íÄ![¦±Ïþ+¿ïœxö_xÐÅÁŒ(®kµ¿¿¹’ƒñµWr0V/WÞÚÈInê‹Ê…ß…KºSruw"#NcxxØÚÙdîmH¯~ ÊôkÏé únò1;©»Nx`FÚ¥©ÂWsX[Rnâ[߇üÞ–«:XQu0B^áàà°†UæOz϶>XõZ(Ê•ܶ ik ê\ÉÕjRx(M»|>áD¯Mi¾È‡ô'*Ì‚fÝ5t‰Ÿ/-„ŒA ÆX5¯?‘HpÉApƒk‘H$’˜aŒË»pTOÖg~ºhŒm»iÇvlº¡5ì¶Ëã¦óÐBv¨}°‚ßߘìШ¬%‚©÷Œi²CíCõÓi';XâgÕU}ÚJ²CV1¾¸Xó÷y±¤ªìPk"EA–µ×Í!²Ã¯ÎžÅ=Ï=‹gÎMØ:Þú{zpðλð­[Kt˜':@Ø@²§=ápÎòüþ É!(~‹––œì áPö*%Ó*uúŽÍ$ƒˆ@W¨BpX ¡+D(à7à MѾaO,”Ê•ÚÈWÄšƒúužY,•Qd‘Zô¹mã’4-_l؇jó/È’uvûDe ¤nÀÞ«>ޱ]`pÓM¶¸­”[ÄS?z¿q7æ'ÏñŒƒ Lž}K¢z  Çå*‚ƒ)’ƒ—`€à@=T/æûëÂp½è{ÆÔømÈÖcnnétºæ‡ƒÃŒŒŒàÈ‘#†Ö…”‡|âÇOÿ,T©¥:mÇæR}6W0ôÆf†jê\ÍAòÄaˆ™a[TúûûqìØ1¾Žáà`Fǧ2ù‚«â0o„¢ _zÀ•ôÕË‚*s‚ƒKªè uëÏÒ1•Nt°!xÖü±k:·/Ôß±xÁ]µõ·ã({Áø¹ço!Nxp3˜íÀåØõ¼uÜòâ[¼å¬qùÆÿjOë^0yý·µl[×µ&˜ªþEí¨}½ÿQ!°ô$©­©•â÷i:(OTR7x¼î_´¥Ó’F¿ºƒ¦+tí!;ÀÙ…HÊÚ•yI‘±X5ûBªR¢h%Ù!3={ž{?}ër¢hÛ8ëïéÁ£Ÿ¾ }ú6$::ª £® ;t vmÜXKÔh ØHv €PÖ°£X ¢Y&;PTÔ,WrPé;6Þ¿ò ±  e˜('/J(Š’9,¨²ËÄ £ù0Ev 6øHÃ÷¤-5F&“áÊ)Œchh±˜þ»A•É*ó,s1Õ÷¸kbKÊžI^ª—² Êî$9À›m­£=ÏÊÔC•°)/ê°]”±>í‰.¿¶$r•þfO»¬ÆîTxPÌÚ&vö²ìº88áÁÅ`¶Kl|Ž•†*@>ösÃɯþèΖ©;À¥“§t§‰PjžìŸO]IamêÊDkºªÔ¤I¤X‡’PŽTÙS7Ë5ö¸—ìp>—Ã\©\“ +ІëV‘.æóøöË/ãë/Åd>oÛøÚÜÑGv~ }ú6${zªŒrÑصq¾v\îØDv „Š¢CÀoýÈyIDV1S, +ŠP(5n¥€(å%5Jíí;Nܲoöp½&;Ì©!È E®,ÚZ†¶º’•ÇæK%PJõçÃ:ÙÁ–ñËÙñéÊ?â[E®µzȇ CVmŠdt3Ò7Ý‹ýùù$æç禙ŸŸÇ“O>‰‡z;wîD__FFF066ÆÊa ñx‡Æc=fí,”‡tì{PÎýÆäÔån’ÚGsúýýý¼ƒ2 #íCçNêèw ¬PsHyH§é­€–fli«cÇŽñõ ‡‹Ö&†U.<ÝbëuÌá®TvôÚö„÷š8ÈB®äàÞ±@¢ú/Æ¢ù Tü<¯êÐ.D‡ÚJ`þœfϸªæŠ `Ú>Æ/zOñH€Ü &;0õ‡ E6óÖqà äÌo€Â¬¡´BGŸøê—ZjnZÿí 7mØPm¡Ù¡A>±`PÙ¡æÓe~! Ói%;ª \Gv–íýé ­¬¢¬`|qQ5AI–u•òûѬ‰­D5ÙáÀ©SøJú^¼ømã**ܾOÜy>·ukm@Üéô­ëÂŽøú6ám ;ø|@0X!:ømZ>zhïHTœÏfñ~.‹™bïç²83?‡™¢Nu"…e±¢æ Ë6–úu (q ¿˜'"dËåúä­õ°¸ÉdJ±P.ë+§Õd-íb9ÉÆ"²CÃM>¢+ï®@PWÙë…°=d‡Õ;Q¹B~Pi–¡Í7cl×xðªÛRü¥w߯þ¯}–«=p´—ŒmºÙ;H悃)’ƒ— Ñ)®ûn•1ƒm!8Ðöè‡õþ\¸zéeÝ%õööbhhˆ¿t `nnCCC¸ýöÛqàÀSyã»ßý.>üáchhétš;˜ÃöîÝ‹cÇŽ¡·×ÂK¯¤<ä3¿€|âÇ€”o>…ÙxëžSÍ=R“DÓa÷+èëëã“aRy˜}ƒÑu²ójËßå׆réE[jÆU88Ü».1eò…Æk[c0ªïq×Ä–^¹ô€Â›ûêe!ÉÁ½¾ñhs騢¿h[8ÑÁ6ßy˜è°Ò(<,^¤‚k<À²RB}›£l÷*Rd«æõ'‰x»ÇœðàB$‰$€‹¶‰\ÝÁ¯üK' Œ?o8ý'þæËv´ö¥yyüœî4QA¸rp±Œ@CâÂ~¿j‡&䂺d‡šßÝMvнþl”1WìÙùyHJm!h|ˆµ ‚߇ßß²Cfz_IÁwN!'ж©e¢ÃàŽ\é숯Gߺum:Ã[Hv ðû+$‡ ølܦb¡‹YhƒPGUd¦XÄÅ|®¹ ²R!9”Ë€¢8×w,Qnhö¹CŠó ”"/J˜ÉP’d{롵.U(É2Ê«I{¬OÑ´ÉüdVy¡ÙI¯Å.¢Ûö¿€õBHS‰Pa_À™¶õÓŠÚC ¶°x Œ}ÛîÀ‘›îEÔž£¿|ß÷ L‰iŽÁH#]ÛYé1Lr0:´¹ŠƒëÂ.ÆÚ‹«8¯—†*+g*•“ŒáðáÃèëë3MtPÃpûí·#•J!“ÉpgswŸî— IDATF2™D&“Áàà ¥ù*Ó¯A|鿂ν­ažjÕœm­šCMÒ측öà`FÊüI†Ö'-RsX.=7ñ­ïC~ï @¶þÐWuààp7âñ¸±õˆ”‡2ù{‡æO¯…¢\ÅÁûM|ÇIžon#ðÅvè7AZ¬P»M»6èð$°$ ÿÜ]¸à*oHNWÙ+Ø?Ë%±}þ9…6'<¸Ìv\±s+oægåäÿl8ùÕ݉k>¶³åÕ˜›ÐOxض®«fÉ£J8h@v*ĉfiHÕ/W *jÉêéØ#;hjIãÛÊM’æÊeL‹uÓ o©'„`C(ìÙKÇ—²¢ˆŸž8¯¿xgççmKwmÙŠƒw~ƒ;n@çš¾í¾@?@|صqîc1[Õw,‰u Š¢ƒPŸˆŒ·‰‡-ÃB¹Œ¼$ªÛ°LtEÔªçc Ú‰ ²ƒ±|7~ûÃ(fÀÁa7&Þ4Bx¸¶e ®äà6xíÖFÛËRßq’CCˆ9ÐÉ?ê.=‹¾mµ122‚/|á ˜·qžþyìܹCCC˜››ãŽç0„x<ŽÑÑQìß¿±˜…wsIyHÇþ;ä÷þ!’`—šCMFÔs8Cí#@çOµx}Ò:5‡å¹@~ï ÄÌ0èü;–×.‹qUC˜››C:ÆÈÈJ¥@Yó“L&‘J¥022‚Çs§9°†6åÂ3­›ù~æðý#p'ÉÁ£*žT‹tm×oViÚ¢ÊYé(«²¦­­Š±«gTtôc#*³§]åYpÏe° ˆ+ìeüÂ÷v< r%˜í¸åØvÞ:¬¿èŽýܰü’ÐÁ'¾ú¥–×áÒIýœ7nØPyyÊtÍ-ÿ«ÿ§…ì@DÂÚ?7<ð_EXºõ[ìP­îà²EI”Q’dH²Zu@4à÷Aðû à_s;üÒÿ—n.o ¸a8Ü_\¬Ésu¶¢\nšÓúPÁ:·µÛAv€£|€{ÃVE‡þž nßdO ‘—i„ýܸaCi£`Ùa™èàwËÙÁ&„ü~”du…€¹R Ëï’e¢ƒ$9LrXÕwF^¾òŽ|~Äwe æ4ÙaeŠ éA"@ÙWóÌÞ«>Ž¡M7aèôÿ‡'g¬ßì{íÐÏqúOãóß܇­ýÿÜ8lÁäÙ?éOˆ›|33¾§Ž'„gB=Z/W™ãÕ~Ø n&ª¬\ø@ÊëN744„x¼í•µuûÌU‡F8pà>Œ‘‘NPá0Õw“É$†††püøqËòUÆ~:ýü7~$ÜÃö ÈÂW‹ÂC__ïˆ #£¿¿_÷øPf^‡ßñï[õ³ã°£2óä÷‚–fl©åîÝ»1::ÊLJfÌÍÍáðáÃ8|ø0ž|òɦÏ/÷çŸ@…`300€¡¡!NR³}}}سg¦¶Y3ŧ¡Ì¼_÷Çœ‹/ùžßh…ƒLV™xÑ'¼Jšá‹n…¼ ïl-MÛ¨d;¼>$ŒU‚»õ—tùŒ«<£ÖAÁEø\ÐЊÐå Ÿ2~á{Û\áÁØÍ䤌A Æxë°üâOƒÎ1ß¶÷¿"ÈÀÍé—Ç't§Ù®ØMëFlUibÁà•ß›j>÷ù,";@Ù¡‘-õÒi!;PJ‘+‹˜É±X(£,Ê5dd…²ˆÙl õ³H“O/æó˜+•&X'MU:'¸•»²¢ˆo¿ü2¾óÊ˶‘6wtà{·|}úV²ƒ;)Ý‚€]7¶1ÙÁìjÐ r²ƒ¥‹ìú3ÕÊ\(S ,¶@ÑA_Z’‡¾­×’,c¶PÔAv€m º_„Š¢d“Ìäo#Ùaõç~aå‡ ²Ãj(Ð!Wþ­B<ÆáîÆ‘›îEoÈúØ«=pØK¤çf›#¯ª8xôö´šºyÉ\ÅÁ½ýÚâ;eüJÇÏëC+È+k¯ùy<ôÐCH¥RãÁaÉd™L>ø µ³\vÒkåâïÙ{Úôj¡9}„‡X,Æm»F8Ó™×[ÐçÕUÛ±ãfgZš†øÖ÷!ú[ȱX =öÒé47š066†¡¡!¬_¿÷ß¿îõ«×[Àí·ßŽT*…t:Ík1ŒÆÊù§í/]µ'À˜±–™ãÕ½©õ² Ê\ÅÑjµÄ—.˜_:¸¶µÜ.ÇÛ«]ŒW˜ƒ»”ø!ÝqùŒÚèŸúÃ,“z‰D[µœðà2$‰³“Û즶]¼åÔ!Ã鯿ëlº # 7mذ²R#;­d!T•*¹-ý¦Â1 ¤–­`ŒìPûPýtÖ’Š¢„™\ù’ºt@WKÀ[%\ÎQ–”+å)J#'6j’†Ÿ®Qw¨C!„`S8‚߯šSgPÀ†PĘ!:· €_=‹{ž{/^üÀ–q nß'îü,nýЇê îC<B²»_;/i ª;øý@0g‰m‚µª6* PryEé¦e}ÇòƒçªQ¶y˜,£*‚$a¾X­Þô²° cu%šÇp=Nv0‘¿Cdõ!;¬FH©(>¨4e*¶™_ÂðÖ[m1óµC?Çþ¯Ý‰‰ã/ñ ‡¥˜ß áa àó9@vŽ~ðîyîYŸž¶e¼ô÷ôà¤nÇ·>úQt ‚§æµDGvÄ×·ù쮣3@0T!:n_µÙ"@U·¤ê Ëè ­ï;–(74ûÜ!EyHŠ‚lIÔWŽUd‹‰ ¯Vádãù·ìà •Uw’ÑÍHßt/ÛvbåEsµ+1ñ¦þ~Dº¶™x㺄à`Šäà% 8P·Õ‹zÈ”6$Û8T]Z¸:û–þ=ˆþ~~øV#æææ˜%‡<ùä“H&“Èd2¼¡8 !#NcxxØÚŒ¥<ä·…|æ¨=èxßÛ6ÏWýU§ºÀÜ#*Êìë)›0¤æ°lQnâ[߇túrÁ–2166fÈ÷í‡eRÌßÿýß;VæüüBG ÐÚ¾Ó¦dX,‹5ï³Æy°Kvq™ðÀÙR ¬È+?+ ,“Z>›3¬î †ˆ\ùQéR{¯ú8Æv=€=ݱÜ|®öÀa ‘f"›¡Sç¸ö¢ŠƒéÄ,ïšh¯Wq0oŽ×êåD{µ ÊÊ™ƒ†Ò½Uµ±wï^Œ3kßøø8R©?€Ça ###8vìz{­ý‚X9ÿ4¤×þtîmëß!¶Í¹Í36¢ðÀo¬w º— P.µ ßi[9rRÊC:ýsˆ™aÐùwl)¢··GŽÁèè(âñ8ï| ±LBý¾€ùùù–Øpÿý÷s•4 רFæ%eò÷ E·_ÝÁud‡Õ»2 Ôf„qø†»qè†/¢7d=é’«=p˜ÁäYý·•“صÞ®Ô£$¯Þ–§Ã)®Vr`È~¨A_½ZÝïÄèäu'‹Åbü –F¤Ói8p€y;çççqÿý÷s" ‡),«…ìÙcíw ´8 )óA~ëÇnÀoɺ2¦ó\áÁË0vóø…gÌ­/PsX†üþ³(¿ö0”K/ÚVÆðð0ÆÆÆ¸â‡&d2$“I&Öeàki `tÝZw®uå~€×¶'8ÉA/ÜJrðè>$Ó*ÚŒ"]F¦lšŸÚè€ÿŸ½wëªï½¿k_æ*kF–,ËN,)¶ñ-¶%›\J.õ¤$N(VÐð†çµ¤%é‰| á”ó+´œ‡$-–Z±THè[ÚD†ò®–É…$$¶„Äv,9‘cË’,Yšë¾¬÷±l]fF³¯³÷žõí#yf]~kýöÚk-­Ïú¡¨ýkO<(ÖŠµ“ ÉQ×µ¦*„ ùkœS"@ÒÓþ‘Ì¢<8R xp—먙ÈjÖ;N|‘| õ…ƒCA\{ÏÝŽ²g裈hN·þB„‡é ½ù`’ãŸ#~_®¯^ü¥ì]Uæ.Ô‰°ÃdJÂx2=÷länŸÂËê|óVŠŒ¢@RT#SÓKu–$Œ¥3ëWdmM‚ºŽÁÿ|þ9œI$L>šjjðä-À¶5kP!ŠžÓÖD«P •ùÈ>!4 t˜Š*ã„….-³n¢2jKƒ!4T,À *Külz vÐ>0ω¨ãrØäùÞ¡Ã IÂh*‰¸$åP$d#©$ÎgÒ3¿§«|bî³Z¨_ìPX>5í!Ç®BKõ*ônº÷/½Úôb§G{`bÒ´ž<þšö·gõÆ<ϰKCƒ×&gou¦²«Õñ’]vô—ƒÌU‡^,âðð\±CñÅËŒÃldA#¸åŸ¸øCªÖ‚5û%»wïFKK ÆÆÆXç1éR4Eww7öìÙ£ëÀwÁ1køUH/þ¨§«ý¥d9äP|æ4u45¬©”-[¶0çr™ôDy é¨gžÓàwÅù·}!)Ôñ7 ½ò('~(IKJÙ²e Nœ8öövæhLE©³³±XÌQ·ô`ΜcÛ¶mÚGªÔ0èØ.ZŠRx3’ƒCí²£¿L„ÜÛ6ín´3§'ÂCzxZÙfÚ‘ÿWCyY”Äœ~³{=ëÝçŸèŠòðGW¶®ä_YŒ”¼*2¡e „w­§f"«œ\½X¹Îûðà.9ÖQY„çI=ö3ЉAÝé¯ýì§®©q”M'žÕ¾·6Äò•P/®Ó ;"< ×wHq°!s¢;Ì;ÀVØRбDÉÙ‡Bs–¥}±=³°ì!ͤ$ë,k槃ñø|æk«ŸQƒ§m1LJv<ÿºŽ1ý¹X á+×\‹]×ßài€Áó8#Ç>ßLС$‹o'l”¸*2@UQéó¡Báçù·ƒ‹`‡¢úS{9ŠJgFw°:j@Qß1Þ^¥t«ó™ &3²S¬â^–)EÁh* YUõû²YýÄ`ó^MüÑ:–ߌ}î²,ÚÃã÷Ü‚3:±3•§ÎÓá+•+¦¹»ƒ!‡©gžIX¦Qà6ó½v ¡ŒýС&«Ç~¨+;œUœÚÛÛ ¬#UëÀ_ÿ-ð×þà àïW Äþ|Óƒ ‹ÌN÷îÝ‹X,Æ &CjmmEOOšššÌÍXN@9òÏ{ÿt²EGprÎd,{ÈR£Xt÷I÷Íã'Ÿ.nžá h@:ü0äÀ¦G,)%‰`Ïž=èééAcc#s2¦¢ŸÅíÛ·c||ÜquëêêBGGë¤RŒµš#ê”r}éÀå.õ]vô— mÇ öxÚÖÎBDô|ÇšÆõ|T¯ƒ(™Ä_­½¦£Ç\ÛÊ™àÒ’B*2áF¨|ÀÕÞ*ƒò~§V/V®s~<¸K[œùpׂº|€òÜòiôÔãÏèNÅ×áò÷nrÖË8‘À‰g_МîÚÚÅÙ—©:mõ—oUˆü°CxêÀ*æÂ(ZÏë„æ~‰:™ovPÔ,ì0ã@h¾ú½ÔΗîÒAÇ´¤€æ¸Z ì «*†ÓIÂ@ïð0>ùË_ oxØôçâŽå+ðÝØM¸aÉÏŽgápÕ¢Z;äsÆ)ÐÁ'œÆƒÀÞ]¯–nÑ/+YØÒÒÕ!—ï¸ v°¨œŒª˜[†|ߨü"­(H]‰æjŸK§ ¨Zi;8ÚEåŽö‹Ô£wÓÝØYƒéUzëuìùËà¹ýG¶d*¨Ôäy ½õºæt\õÉÁ]»!ÅÙåêHnö;æ‡N7™Ž’CšÓmÛ¶2,Býýý†°‘%1ðïý H°6ÿw]¾é‹à¯ÿÈ’˜©õïëëCcc#z{{Yg2éVss3z{{qÿý÷›?†½ù•/A9öý¹‘j,Íɘåãÿz€šr²;‡ßåþ5×N‡]Ñ ÈqÈü¤ÞvÐñ£–•¶mÛ6ô÷÷3è’©h¡¥¥»wïvt=wìØîînÖa6µêÈÍÑ–Ên?ÀÔê”é¾”A“äàP“¨÷ÛYð &M²ÇŒç‡ÎòÑÒHO„L ººÅ3Á¥üö_tíØaJREƒS«ÖPWW×XŽs~<¸Duuu1§ÖMvîƒ]ž’“P>¦;y¸¦›>õIÇ™õæ3¿Ô•îýK/¿4Ñ;LWm0˜ã;$'c0v@8VÁdZ]òNÞædEŹxªà˦ÁdîpÏŸ~×Ôg!,Šh]½w¬Xáù±ÌnØ!-+HËÊŒÇyŽ ðñ¥æC§9$Ïg8ƒ‡™K¼.÷Tù*$ifTÇl>¸x ÖÚr.u!‚u@{™ ;ÔÎŽ€c1ì +*FS)ý/Ëiù/ AÈ|0ƒ°ƒIý_î°Ãœ—+(¹ý¤ýä³xèäs¦WÍ^€þßÏãê~†- ™fè¹ýG<÷ý¯k{‹^~3øæÏ—¾òÔɂÄzÔ.×U§Ì|Ñ­æJqÈ¿Ö~JSS»ñ¿õôôছnÒ·ª[ÐþÚ0ôŒ©§z ¾¹gî­÷´gÏ»0ÖØØZ[[-;€J5àîW÷§Ž}Ÿ©Ã¯Bym—¦4‘HcccÌ\ªh4ª €#‘ÕÖÿ/̳³Q’y:ô<”“Ý éËJŒD"hoo×}™©|Õßß––ôõõYV©Ìž“ ñ·ÅœÃ\lžmÿXKü5®ùz jëÕý€2Ü—2Ádâµ6ñhW—[…hü$¤¾‡´ù²°ÂÛŒÙHmn/:xþA’ßùhFÛš…ßôßAj7¸¾'U ¤ÏBÌœ+êûJ|jbJ|J|ÊùSæÔcáJ °$¸dá{²¿;\Bò "G¿çÔêu>}ºµÜæú[î¸F1§VÌá$SYIè1;lºëNÇÁ@6ºƒVØ®]´8;…¢ùWŒ³*¥8réÃ…À4Ø$Èì)ÉÞÄ^4ì@rçMò-vGv° v˜ÑÐ3oþ•€  vUUì`Áp:‘Àß¾üŽë¼µ/ŸšjjðÅM›Q7û`«''6²ªâ|J‚œãöwIR’‘ç° BฒùUÑ ƒg¿.‘¬Ìêàœe«‡`˜rH¿àÍþ6Ô¡è7ª†ûsÆ%‹aP@RU˜;€DUø_ |b¾/™0ÛÑž{ÂàW@ŠŸóQ{ýh©^…Ö7ÿ }ñ!Óª—ŽOà×ßnÇɾßá¶:¨¨dï*&À=ª7–¦²Ô郅zÔ.×U‡jp£]êà¯t¥f‡‹“0€[wŸ.Ç›>Ãå—ÆÀ×^ åµoB=û{SlÚ¾}»aÛ˜˜¢Ñ(º»»ÑÑÑöövÝQPòŽp©a(GÿêÀ,ä)@‡_ÑœŒEwp·ÚÚÚðÐCiw—ñ£POýÜÒؼ«‘ÿyPÇ@9ñÃì!o µmÛ6ttt°ˆLšÕÛÛ‹X,fúû…„–[t¸ÊÕ áe3Ÿ’ôÔ³ÏCy÷W†à‡¾¾>´µµ¡££ƒu¤Îù÷îÝ»µpéa¨#¯‚«~oyí˜V-/ÿqÒÚCÉÄ«íÂLòL¥ôFî IDATDx ò ¦ίÝF:°‡É*_ö×hèùw<&·h‹+%kAùë'¼œ&¡yZwúÚ5«±úƒ·8ήL"£?ÿ¥®´[|GùspYA*­ •QLËäìèÅÁàŒDš` <€Á3þQU5–•¿ñôÂæFwÈ&:6>ŽÏôì3v‹"î]¿»®¿Áf¿ÃTç霰ÃtIʅ頻ÝK®,äàó¢àØ¡¢;P’œv(y?˜tS½QØÂ„zSñŒjçRؘ<Ø;àP“žMiúøK-~¶é<}cQt‡²…¦ï8„€Ÿ›¸9¼½›>õ7˜^Õ?¾ðs|ûS×àdßïØB‘ 0¤xˆØUMwôq†ˆ×j—ëÌwP…Mu/úa»\ÿèÍ­¼:ð͹D"´´´°—Ì<êèèÀÀÀ€®´ÜòO€,h,úY#Ó~æÌá„0ø‚_ÕjšmÛ·ogÐ “)jkkCoo/ššš¬õ.€òK÷C}ç™"¢X8ÈOËš¦ÎB=ó¬æ,b±s—û{$Ñ•6 œÌ?æÛ5o?éðÃ?b)ìÐÔÔ„}ûö¡³³“ÁLšeì@*WAX÷ˆ¿ ~ÉÍs` þjð—¾M_We PÛ½{7zzzXgêkux:ÞËÚÇS. ©‡ì²z?À„¶#¶¾Çé4®éjÖÎÓß«5§QƒÚÆC&ëHl{ÏS '-gAKµ[tÞnœˆLp)’+‘æ#H~ñ?ü“ž@òØ>È£ý–Áyë41u`?”ƒAþõÿ‚rè C—}[!©¢Á©]ÚPWW×Xns}<¸@uuu1§ÖMvî]^’“P=¡;¹ â†÷9Ò´ƒ?ø¡®èë«¢6Ì ;Ìy9É*d™Î˜†edªý‹éHÙ‘YÿJx¾¤°CÎ ¥˜He ÂÚ—æy›wæ?R­°CþüôVÀÏœ<‰ÏöìC\’Lóÿ¦š|7vîX±å ;aE¥8—H£Øs»”âÂ÷mX "à÷: v°«| “Å¡ý@,;¸=ó;Ä„< –a†-¶D¨°vq x° vÐý²¤:}Ù¬2J;0M“_JÎnm¯¿7ݦp­©E¦ãxò ï¾µ©Éó¬ÊXãgÞÆø™w´B•Ë­O˜9xI:ê6»¨‡ªR†°g‡¹ÐÑÃ@R{Ä¥––vøp¡½½]_âÀ"pË?]f%™ößÜ²Û \ó(H`‘)6îÞ½›Ey`2EèííÅÎ;­ SÃPŽÒK÷C9òÏ “‹†â©_ÇÞ€òÚ.]Y1ØÌÝŠF£†`1ùÐ×€ ЃÝssš†üÇïeA‡ñ£–•‰D°k×®‹Ö™˜´ÊlØ„–eA‡u€+êÀ'„ „ÕŸƒ°ÂØ<‰Á¥úç[¶h¿OT9šöÞ~€©Õ)ÓË7 šìNÀð<à@Y;+.ä7¯Uéá"Gð8èx?ªƒ;Æ Ô<Œz x˜²IîëBú¹‡‘9ñ[(ñçTNN‚žzù"ü y 49ZòjIõNîÒ²[43àÁr¬cJ xp„Ôc?Ôþƶ¿†Ï7ǽq'ž}AWÚ?oh̶ ͱšÄì`ôb4‡Ù_Š~ÌM1'»ü«SBrV¶s1› ¥Ofæ½­žh(AÓÇT»…þu,“6!7‚þóøqžw>ì Ë~bŽ ÷׬‡Ê>ºC®Ý‡ ˆöF{xåéÇðäçïÀ7ü3yCgŽéˆîP½ÁšçH÷¾™C‹â`Fu¼f—ýå±(ù¤þZW ºò—‘ÚÛÛu´ã¯¼¯`ÿå=,SD·“®}Ü¢«M±³««‹AL¦>7´,Ú@N@=ó,äW¿ùÕ¿úÎÏ@SCæ—“c(VÏürßW!ÿá«oê×¢††4773Gq¹ÚÚÚÐРóï«JÒ¡¯éò½êøÈü¤W„:ô¼¥m³mÛ6ô÷÷³CÞLºe6ìÀ_þç7~¹xÐaÖ3Ä-ºÎôÐ××ÇæÝÆZ]£àÈ«ÞÛ`Qí0ÈÁÓ]Í*U¤?GÖh·098ÿ\Ó0€e}súYßÇîyÀÜ!",8Ÿv ==¨ƒ/AþíCP~ÿ ÇEPÈ)9™üðÛ‡ |¬¤ýàð ácå6ÏgÀƒ;äXÇt8ÁT¢C‡ ì×~Õ­7£víjÇÙ•I$ðÒw¾§+mm0ˆ?©]œ{U9kQI)’”œ_å8‚*¿SÑrGr˜™áìèøüé.þb]t‡\9ÇÓ¤|7“窣ƅû¼Í-²6$E-®&ÓÌK§!«Tý œMôðøæaó&ƒ+"ìºþ†²‰ê0¥•‘ˆm°¥4çs_ŒR’b~”‡‹ ƒ/ûÿµú£EkÈó™ ΟǛçÎáÍsçpll #©$TJ·Žµ£|•ÉÛ°CQmMlè/bzŸ §ÝjR¿)§Àg~ž?ßrδÃü—¶åEžÓÖ×Ê—¨ E§/ø9-¢_ìPš2Dû£= ½õ:öüåðû§¾Ëe¨¡ãz€‡æ=? rÐg—«#98¨*ìPƒ6»\Q¥ŽÊKqÐÁßh.iË–-hlld/™êïïÇîÝ»õ­"] Rµ.ïL–˜ñ¨ að·ì¿™boWWš››166Æ:ŸÉ°š››-öpññ™< åø ¿´ò«_‚rìû co˜3 ©³PÏüÊÑôÂg¡ýè¸þüYto(;À|zPG̺„)÷‹D9éðÃ?b9è°eËÞ3JÀóÙb`ûË¢)ÜH*‰‘dªàw‚‚€e 8c-ku¤ÒÖa¾1É3°Cåè´%ž‘—$ûÚ Ö¶—Èq¨ò­íšß–¸4Õžúóó"Â3"û êž§_ìà¬2Ti>çÇ=ã'Ñúæa =nj±þðÜñÐÔ7½-*Ë@_»å2Íiø?ë -¶áyñêžõhPVÇË{ÃÔ£&›g€ò|èÄ Mi"‘;Ð>z{{±iÓ&]i¹å·üùWÔL"ÿ›NôC>ÐnÊ즦&tvv²ƒ«L¦ª££ííí¦du«¶lÙ‚žžæ R?š››û5¿ôàß=Œ¦q>Aã'¡Ž„:z4þ¶-¶G"´µµ¡­­Et`2eîeìÀ×½|ã–MÄä£ß‚z®WWÚ}ûö!‹±×8Î^qÅÚçä‹o¿ê³l?À‹kfL&^lv7«¹’?z^ Ë/~?¸Ê5›…º¤h¸†û ÉSßý±¶DBÂû¿æGA9ü„k£9hé®a ¸•²¥¸ÀÙ—ü•S[cûéÓ§;ËeŽÏ"<8_Žý«€ƒÉ¥²zìg†(ÂÍŸú¤#a‡Ï>¯v€O¯^;wš•‡d5ïâtq ãPár/b烲‡™ó®‚í… ‘‘K ;oGÏ÷•”¤ XØ&eIÿ¦ƒC`‡‘¾»©,a‡••Ûa‡’Š@¿_D‹×’ Yšv€¤,#-+¥_O[½¦–•°CÉ/0`°C±ßx®¸ö2åù#––ATúÖö-lKX!NwþáòÖµƒ[ÆT´~n¡±H=z7Ýmz´‡t|O~ácøÕ·v²…¥ÇuæøkÚ áùaÓ"9xI:¢8PÙewUØ­ÚÉÕ&[óàÐäfØ`ÑŠ‘îØ‹À×ß–{vlê³?wÒG4BØÜbžÿT¤‡ÞÞ^æ L¦>Wýýýضm[Ù¶»ÝÛ{jll4§_•$”·÷BzåH½_†r²êøÐø¬¿aÈ ¨ão@yÊɽ?ŒÌ‹÷Bêm‡òö^Û`‡mÛ¶¡··ííí v`2,3aaÅö<°ƒyópaE«ö²Ù{ÀÐ8«'šŽ:rã`Qܺn6ÿ1vgö7‹äP2q‘ÕÚ[&=\°ƒ×£:xË¿IP{„ÈIÛ£ èê©ä(”ƒAùý7¼;\èõø3P^xtbÐúâœ}Nº¬naÀƒósjŤŠzÖ;¥zIM fCéÔ7^‡+n¼Þqvxöy¼ôÇu§¿²j!þ¤6{0…N»¨,×ÂSU(5÷„Œã8Ô†‰Â…h3³EÁÈ4àÌÊÀnØAQ)©à"]Û’^OºlÇJ“ž¡L/dvº”¢@Vi‘55dðŒDf·.«Çwc7•סÿ ª …pyE…åå$d’:Ó¯f?×Ö¯š¦ù@(,ÒUJqj2^tEG–®«-ŸèÈ€,;h³$ǘTN°ƒA¿ðó¼IíQü;Rw>ó”Q!úÁ¦,†¦>øýùbŸãQåóço3jEÛ1ØÁeøTÀ?¦‹ t,¿O¯½Áoj5^yú1<~Ï-?cÏ¡&û5¤x 5óû­aÀÁKôÒ`—k‡AÎÊÌ¡}æ¥GÏúÊÓþëJÇ€‡ÂêééÁþýûu¥å—¸Aq™Þkô0>>ŽX,†îînæL¦)¢³³ûöíCSSSYÙ¾uëVv«·GÕÖÖ†­[·š÷~¿ åí½? ©w'2Ïo¿ôóÒ½?ùÈ?e‡ñ£€’´ÍÖ-[¶ààÁƒèììDcc#ë|&Ã2 vàƒÖ}ܢ묟 !+¶ëJºÿ~éÇ®õ‹œ€:òª‡–…e9Ü p+äàÑ}HGÞ†f<•k´·Tú¬¥ûF“˜ãN©U6zЗÚ¡:úGGÛ¤ôdþ*»¹„òÂ#PýÌÒräàbPÞïÔfˆ•SŸ3àÁùr¤CÊÁZP>Àz§$Ÿ„rð1ÝÉÃ5ÕØô©O:Î,£°ü÷5³¢;äá0çfòéß[C üŸ÷;—~Ï ;€ã.þRjØi›`‡y2#ŠR0oU¥ Ó£PüÙNJR‘55dðŒDfÂnÚŒ/nÞ\–ÃX](„5Ñ*Ëò—T§âq¼96†w&'qâüyÇH*¹ÀÇÛ4ý1t°X#©TÁ(0³×µE}×­ëjI W^YÛS§äaTA ‚`m6À^D°6ÁÀ‚*`f¤‡BÐ Ç#êó#âóƒ\œÀ0ØÁqã_©Ë(í¡¥zú¯ú¶V¿ÇÔê ½õ:¿çúÅ¿³µ¦u²ïíS¸Êå3ý•EqÈc›×šÀ‹·6zÙ½ÅÁ~ÔÁ_kNÓÔÔ„ææ²º0J³ôÞ¼KªÖ[3qHÒéOB8 =T­3Üããã¸ýöÛÑÙÙɃÉTMEÙ³g¼‰<‰°çÈãêììô4ÄÓÐЀ§Ÿ~===lÁdšº»»MƒÄu€«\mÛ|œ[Ø ®Jß³À¢‘V]]]3€ˆëæð0-Þ ýÌPè¡ÚîƒÏa·É›;ܹb%®XP™}¡Óü+Q@Q(rœ©ø‹ƒAžËq¸Æü<°rá0¢`E¥Hå¹¥ÜVØR ïéK‰/Fm …³x0qº À<Ø!,ŠøNì&|°¾<£äTˆ¢¥°CZQ0011Ç?TJ1’Ját"BÈ É\—±t°p]9)e4}_äxo:é|°ƒ3–áæDU˜×߈ >kìBϽ]{6`‡/¢Òç³°æé“éŽÃÂ@ Dü„¿?"Ç!$ˆøüX"âóÃ7cÜ`°ƒSÆÛŸƒbäS³?³è^û1ìyÏm¦F{HÇ'ðÓGwà§¶!5yž-:=¤3ÇtDxX¸‘Ayí¢†¿æ:»JQ/ÙeGy&Šƒ½Ð3/rBsº¶¶6ör) ÃÑlŒæPPBÂævpK¶˜Ò.Û·oGGGs&ÓÕÚÚŠÞÞ^ìܹS×AF7(‰ §§Ñh”u¸‡FÑÝÝí9?ŽD"صkúûûÑÒÒÂ:šÉ4uvvâöÛo7 v áËmŸóðAÍéX”ýsͳêñ# ©a—îxIÖEqäàX“äàL !µöVLZ³—Q’®¡†>vÏÃèeQ_ödƒŽ³äbT‡Â%éÝÑc÷?jQIŽ>ç+›×su0GÔõ3à¡$óÐ!¨ûu§_ûGPÕà¬Á÷À~ˆ7þ+Cy4.X€;WdozU§Á¹#2’¢Îù·©_+²ÐÄôàIÎ/ÏÊ›Ìú…ã;ù£;¿p'…Ó‘"s(v˜êQàæÍ:.éˆZ¡y·"› ëÈS`‡‘þîškQç0èÈ.Uˆ"š«k,Ë_¥§âñ‚‘Îg29A^˜é¥Xq¤Àƒ)ÖEs°xm9}lœ¯!¨Eï­¯å"`‡’¯ñM‚Š„Œåaƒ-ìðñ/<nýÿî²kÍÍÚÙç¥cºËá]Ä€g+æÔŠ9œXò¦.¼°ôªvÍj¬ÿèVǘ“I$pð?ĉg_0”OHpÿúsV¨ù`•R("ÌþNM €¾ì^Ž›ÃÌÎkš0;#AÈbVL€ætÑÜÑô.äuàªEm)PB‹ÊZVUm›:a‡gNžD×Ñ#†}¾©¦w͵¥9$î„ á°~a5κàR§ Hêü‡öÏg2ˆVø1™Ö%„̵ÁjÐÁ¦õ%GHAXdzªƒpÄæ-A«Û@Q™Ábò°ÁB¥Ï‘dÒÜö²v8•¢¿ðØYbØAûçvÀ6}Îdü* ÈÌ|ônú4ÚO>‹‡Nšb~è­×ñäçïÀÍŸû 6|à¬ý]¬“}¿Óþ¦­\ˆá2¨%_u•]®ª;ÐàÙçË®âå8è©ßhÎjÛ¶mìÐbŠî°î>Çú¿î^ P õÄ çÕÕÕèèè`¾Ädº¼>444 »»›Áe¦ææf×CÛ¶mC{{;Y‡2™®¶¶6]‡ÖçNp¦`‡e%µ‡¯{?”w(IMéöïßþþ~öœi_›ššÐ××§m¶}æ9À,à:&“²Û ^lfkç‰DV£µµpzؼ¶g {̰óøªA3áÑc µJkÍÄ ”ƒÉQËËZº¹‹Ö®F¤¾‹Ö®ÖœþbšiS©±“ocø#xgEbxIJú«‡Ÿ&Á­ù¨iyÊÁÅ ¼DI;ÑÙcåò.âÀäd9Òå`-(`½c³”COdiACAܰã>ÇØÆo¾úˆaØ>½zd£2LÓ-´h•/Üê<û;ÏáòŠŠ‹¿ó„ä\“Üÿ0íwnΡDë–ÑóEwÈuƒ=ÑXBÞtZ` ð@r&ZT'e©xÛtÂÇÆÇñÈÁ†ûéÖeõØuý e ;@sM çÒiLJÅ ’ª‚ j¯OÈ'ÌxÞ!Š€ßïzØ@aÿœV‡ Ÿˆ*¿Íó;`‡ùü‡Áò€õyè*xŽ Âç3¯žÁ@X±Ðôì`/™óâ×7cÑ,.cZ?òÈ‚9‡öúqpÓÝhðGL«n:>Ÿ>º?}´ ©Éól1êRìÓ±Þ¬\žÇ½º\ƒ]®j‡UִꔡºÚäW~žâÕ3/êÊ–Ew(,½Ñ¸%1À"gú×…"øå¿ös¦dÙÕÕ…X,†±±1æ4L–h |èïïÇž={ÐÐà¾hå÷ß?z{{ìP¦š‚Üæ»Û¶mÉ'ÐÙÙÉa3™®±±1´´´x v!ðKn¶uîYÎjmmÕ>NƒÆO_—šº—Ã~5½íÈ´¶Ÿà ³X;»^$¤ýrdš6î¶w×<zÂ}ÊáÈoñWkÏíü;%µFèòÂ#–ÂK77ã}÷ß‹;þõ1¼¯í>¬¼õ]°C>Eë—aå­·àªÏÞ}ýa¼ÿïwbå­7#TSmQ›í7t¹x.IÎòÐT.ï"<8Tuuu"N¬›\Ì:ÈîWðÐ!C¡ˆ®ýì§á …aËÐGñÌÿnǘ ¡Š>\߈?[š }F)òF˜þo9ÓàŠ À“KC¢À]@ Á9É„ç/…`¸UÑHKô”¤¬ïü[yÒi…¨z‰HÉ—ÁÔ?Ñâ&Õ²J‹«’NØát"Ï¿-øÖeõøâæÍe=†­‰VY{Œ¤RÚÞe”"ì5ELá AH.Etðù¬`ï³*˜·"Ï¡.vÊ:Ô¤‰ ƒ.}nì@,ö‰ÂíxÁœrŒä‘ç³€  &DXðYÛ%ùœØ06Óê/«*”ó;ØQɽ{áWan†ÍáÅèÝt7¶™|³Ê¡_üO~þœ9þ[”ºPzúT®˜æ¸e 9P0ÈÁŒª0ÈA›] r°©x:ðÍÙ7440à¡€ŒDwà–Bc'Û¼¿P,·$~í_™’u__ƒ˜,W4Ekk+úûû±oß>lݺÕñuž:0΢ 0577£C{å IDAT··[¶lqß2ÐÉ*õ÷÷#‹aïÞ½Æ3sì0U¥º÷|Psº®®.6—Ò(=À¨ƒ?/Á²]z G rp¨I”µ³—ÄE´|¾áÁ  àý¨å:¶‘–jÏõܱҘ#'¡zê‘§-É>TS¦»þùçÿ‹÷µÝ‡¥ïÝd›iÑúehºëN|èëãOÿæ4ÜpùÞpêeS¡'Ÿ›®««‹•Å»ˆ½Ž+Ç: ƒI%o¾†“£†ÞU·ÞŒËm|Òá§öâ7ÿçH‰¤á¼nZz>½zí¥¹J°ƒªÒ¹sÔCˆúü3GBæ‡rDw S‡Ÿ;PJ!)Jζ(nK O:­°¨EÂ60´}0‚¿}ù%Ä‹ŒO÷®ßPö°Ãåá ÔY \%d*Õ¶(9È|Ë4SabÓøC̳ÁBØ!©ÈM%1”Šc4“ÄH:¡T#©ÎKéìû†Á—1Ïó%Ò,ø0ëkQ!€ÎUÆÓkï@Dð›fÂÐ[¯ãÉÏßC¿øw¶8u™†ôÕà­Í~ ]t-àà0ÈÁKvÙÑH®7·„ètš8¡¹8;VGG‡®tÅEw°ÑÇføóÃ-‰AØ´Œïóôõõ¡±±½½½Ì˜,W,Cww7Î;‡]»v¡©É9—Þ544`×®]ìÀ8ÓE£Qôôô`çÎŽ¬˜ìÐT´›¾¾>Ãy‘Ðå7~ÙQ°CQ:;;™“hWõêÈ«%Ø(ƒý–;n…<¸ÿãxÀAfJÏ»”&ßÑ×…%ñ}»çaõ²Šï$Ô<ŒÚ<Ðä(”—¿zêeÓó^´f5®úÌv|èëcå­·@,ñEÚ‹Ö®¾ùák¦ƒfBRE½“‚X9¼‡ðÀPǃˀ;¥~õášj¬ÿhéo.Šã™ÿÝŽÃOÿØ”ü®¬Zˆÿq寫Ùù` {Kïì/KÃ3_Ú>ŽÓ;`ZtÀ M³·¤°\/Ì&§æÁ—ôÍC‡p||ÜŸ>¸i3îX±¢¬Ç¯¨Ï•ç+ªô]º5]à8T…üùüS¢€È£¦"!ÈFuàl˜>ÑÒµMCe%*}>p"‘çP `y$¿Õ-ìlJŒ4T™’®û];8ªÐÐ^•~Â>QG̵c t¨ à³ûÓû²Ôì—·(¥8—Ia"“†LÕ¹SP¤£©ÔÜ9¥ßn…¦ïdT€Ÿ[HKõ*ô6MáZÓLIÇ'ðÓGwà§¶!5yž-R] ñ3ocüŒö?¢Êå°^Ë5ï`QQ¯þá•AN,š½¨+][[{¹äQ¿î‡óGw°ÑÏòByÞ•Uë l6zG,cГmŠF£hkkCoo/Nœ8]»v•$òCSSî¿ÿ~ÿ žjÿ´6¬ÞþOv©ÅÔ9Ï¿ç&ÆVTÇ«{±Ô£&SO/ÿö3@rHSš¦¦&v ½€Z[[ÑÕÕ¥9·$~Æ>;-¡oi/›¦ÎBùã “¦TgÏž=ú 11™¤žžž‹?½½½¦°>ŽÆb1477#‹±CâLºÕÙÙ‰ööv ØZî¶mÛÐÒÒÂ">1Ù¦ööv<ôÐC¦ä5;˜kZ)ùø¨g§9ݾ}û‹Å˜ÓhP4Õüžçjo¿ê3l/@¯]M'^nfkgv¾òî/¡œøÿ´£á+À/ùú’–kѲòM­RÎüj¼_›/¯¹\ƒõs/uð¥ìyQ9iZžb(ˆ¦»ît$äPHgß8ŠW¾û8Ã#欖^~Ã]†ò¨<öˆ“'Ù^§OŸ&^êöRvžêêê¢p ì8>,‹·$'¡{Fwòõ·¤¤°C|x/ýËã:rÔ´Ï; v.E´Ð;èÙIÈû•|Š ¾b­€&% <`¨^w,_Qö°ƒ@8¬_¸ÐØB‚ލE@œKÃᜰö ø¸™ (ÚÑÁIkNZåKƒ÷ÀÔÚöòñ<HH2’|zÒý‚™Ç $Š ÈìñˆÁ6Ø>{š¢ vÈfE—%TŠ~÷^ƒ€À«@š›Sv{ýˆEÐòÆ`\N›bÞÐ[¯ãÉÏß›?÷løÀ'غա:þšv/tUtê¬çÞsb+ªRfXxwGM'ÞÒ ;`‡Ð ¨¿¿_ìLîà.Ðáâ{3°ÂæÐÜÜŒÆÆF477³Æe2M­­­hmmEww7:;;uGšO‘H---ˆÅbhiiA4eÏd‹ÆÆÆÐÖÖ¦{~5g¾âØáB”-þ²?ן>º'û^Àmt°Å«u²Oû!²p£w^ð rpHUØ¡æ‡N(þRÆtð7ºr`Ðó«³³SW:nI $PcŸßQ‹œMCؼÊëß‚:üŠáì¶oߎÞÞ^tt°ùSéFç@LLNÑT´…±±1tww£»»===º#“D"‘‹QHZZZ¨ÃT!‹¡¯¯Ï”ü ;йs2⯩\zþMMYuuu¡££ƒI¤ªQG^·øF¶`‘É,Š3«<Ÿ)—´ó¬&áeÚs–'5 pþw-˜}Ó|ÙW­½ô‰AK­S=zêeÓò D*±êÃÿ ï¹õfW÷º ¡é®;±hÍj¼òÝÇ!%ŒE¾Pö .wÙµºÒKõ:·¹bðÀTÇs¤ð`ÓëyèèÐ!ƒ|7$õzã(^úÎ÷7)ŒÐ”nZrþúÊsV¼a‡ŒškqL€š@K¡‚ hIVg|’vQÈB¹&H%‚²Ž¤e¢Grg£³èuP©¦] ޳{K#[ÞsヒçO¿«;—ëë–à‹›7—ýøU ¡®Tpu IUq>“Éùy¥Ï7r*øðÙÕ¡\ÖžN¢²âðJËn²ŸùbBË0ã™0¥žÓ‡€,øÄ3R² £°O‚¼ˆ ÀçÌè²ýœJ¯PŠ„"åýœÌó¼ñ-¨ïî7œÝîÝ»166¦&abbb*'E£Ñ‹Q€lä¡©È$.F(™®ææfD£QD£Q477_ü‰©”êííE,Ó íÌ–caZx^Æ×¼²Fົ»¤¬AÍÍÍhhhÀÀ€¶(etôx`ûºžU¶ŸÀL*·vvKçÑô¤r5èù£ÚJJƒ/+Q÷2Ð V9ü8¸T{¢ä(hr$¸Ð\Óä$”7ž2v¸ìê÷¢á†ëˆVzÆ–¾wnn؉:¾‰ñ“oÊK=ü$H°dáJÍi~~:ÀÓ·É0àÁ¡ë,G¾6x?T_„õŽÕ’“Ùè:µþö­×ÔØZåøð0|ÿß0xà éyßV߀O¯Z;gå›v ÙÈÈ ; * .¤É…ÿ•Udž8Âó9¡R( Űƒ¢¨º¶L…¦:Dãî?ð`nt‡l¢IIÂÃèöÓ‘ƒTˆ"VV–î=Q ¡ÒçÃX: å‚ïñ„ ê÷Ùã²°)Á›ÖŸ^î@i6ºƒ£û¡Ì`j±Ú‹'•~Â>’ iE¹8®“Èqðñ<ü<a¾¶ÂÄŸšZÜ«*&•ÌEÖ±BðÁÏñø1¥î²¢ê†@¥& ,²ƒùåÈ@YFÛÒ«‹Ô£åõÿÄ@Úœ?¢½õ:¿çÜö@V]ÿA¶žu€ÎM{"! \ì”Ý÷Ì]7áµ¢*ev¨zÔ.×?Ætâ<«9ç––öbÉ£îîn]‡ò¸%[@‹lô5{üž_û9 °ê‰ÿ0œWWWúûûÑÝÝÍá2111iPcc#Ùû›ÉUêììD[[›i°·è}VlwØ„Y÷ë€m7Övtt0àAÇ:g÷îÝšÒ¨#Àˉi ƒôˆA̤²kg·t¾Æ¦çÂË hÔä)ðËœåS t({I` hJÛŸtô :#䔜„òò7L‹!øýXýá¡zÕ{²6ò¼§¼!TSƒ-_z}?ø7 <÷‚¡¼”ƒ¿îA]‹¬…rbż>"p`r¢¶8±R,ºƒ=Rý õ…Þ©]³«?x‹muÍ$8üÔ^üdÇ-î[·wOƒ™!œ–PšvX\P“iÿKé<‘@D±hØábÞEÂF6 ¨¦oçh3`ìmêó-y˜ ;\Rב#ˆK’®´aQÄß]s-*D±¬Ç.pX­‚À•vj, ‡±¬¢Ë**°4.vÀçc°ƒW˧2RáÈ7 vЇÁ2Ìho a‡éâAPáó¡:Du0ˆ¨ß°(Îü²ÿ?â÷#ê÷£6BU €°(:v °v —¾s6ĉÄyœM§0’Éþ ÄÏãíÄDn0€¢¤°( PL–âÜ>n/F僚±µú=¦•ŽOà©öOã¹ýGÖîÐÉ>휤zƒ&Có ŒÅÍ9rX…M«Šë:¸]®6·ÄýeYñÚ2¦ƒ¿Ñ\B$a& ¨½½]W:nÙm6øZiüž¿âãà×þ•)yíß¿±XlÎÍäLLLLLLLÞQGG¶oßnìÀ_þç΀(²‹ Úçcü¢ë4××ׇþþ~æP¤QG^-¿ýƒ{dÖ;Û„z¯›)«Ty)G;lzÖñ:=\»õìž¾-ßµNįãBi“À¦Ã‘úe¸æs÷\„€ã¼w<\ …pÕgïÆÊ[o6ÜþêÁÇô%uî9êH]]]£—G<8Luuu1§ÖMª¨gdõ«zôÔ}¡¿ÅP×Þs·muÍ‚âðÓ?6=ï ஹ7-¹D÷’ «àü (ŠJ Â|ƒ‡dFnEˆç®8RÏ^Ot¢±cõ›çSJUÕ\9‘çŠ(kÜi ã?ß:®Û_ÿîškQ ¡Üµ2q'ôAHtJôŠÁö”/ËÞ‡ŠjkbCå.#!˘”%Œ¤SI¥(mà @EŽrxBàãùœ°CXáçyø¦ß`ÔSa+óN§ã8'¥s~žTd¼š=PgŒKù ½b¢;š‚Øig¹Ew˜3_àWçè^û1ìyÏmˆ~Óšâ¹ïO~þcHMžg‹ÛjHG„R¹¢ /Bû#¥iÕñê_ Øåz“KXyKÛNÆêЋšÓ´´´°Ûõ󨧧Úß7Uë@4Zä¥|h/•Í-Ù’…ã{e}}}hnnFoo/s:&&&&&&©µµ;vì0'3>aE+øËÿÜKcó1nÑût¥ëîîfN¥AÍÍÍhjjÒÞ½£Êo?@ÏAh¸p(õšÊ³X;—™æyÆ Š„´Ÿ¤¶óéz÷+‡gÈ>ûH`©ŽùÀ1s¬œ4v¸ìê÷¢é®;!fþÝsöï^RÓ]wâªÏl7Üê‘§4§søÅñ1/¿áðÀ®hÉ,ƒåÒ3€Niýí[®©±¼Ž'ž}þ"è %’¦ç¿®j!¾}ý\± òÒƒyaHËJÎ/ä‚HÎ|fläHþò8"ˆ¹ëDòÕ±øè%…ˆ¦bóJU]•ã™§ŽŸ3ç*v¿‘  vÐV5è6~àçfÒºx#z6Ü…¦p­iMrò¿Ãž¿¼gtºg2GgŽél‰ð ppäà ªPÙeWyp z ̱‹Nœ’g5§cÑò«££CW:þŠ[ào¥‡f‹[²Âæ/›= ‹1艉‰‰‰É#Css3ºººÌÉB\÷p:"#˜7-¢Ó": - ]®9]gg's.ÒåA9È ï혰¬`ƒCM¢¬ËK9ÚÙ"ÐáÊ ò„ öëûØ=}\~lŸ¸ àÁ@ÁLØAðû±ú¶aÅÍV–#_Ã׆Ôý CÚÎ:üâø˜—ûœÎS³S+&³Þ±Pê@îYíšÕXýÁ[,­ßèðÒwG|xÄ’2>qÅJ|eó5 —n‰çŠ„dE¥³¾gv˜ <éÿ!ˆYcv^%‚f+™‘‹Jhì픢óœÝöùËËþV!Цì˜ô£oXIÞTSƒmkÖ”ýØ%kÜxã#ÏgabÍÖ›¤ª8—Ja$•ĹT Òôh'NY‹–Åz˜f£;8V^‚€³É$NLŽc$ĤœAR‘ Þê/© ÞNL\‚tFp\{•ìV ¥“E¥ŸT$ƒc1½þš6ïÔ;„x±`ä°’¿^Šì`–D ˆêÜxx1z6Ü…mµæx?óžüü8ô‹g ]›•š<¡·^×î™Õ-zHX[«ã%»ìh$ÅÁ¡îb~Ætð7šÓD"<äQ?öîÝ«ý]XRu¥q×Èÿ6?óØ[Ñaó—AÆ/ G,c7311111¹\½½½hnnF__Ÿ)ù‘Ðå7~YסKs¦Eæ@³¥ÞèëëC?s2 Ò»ÞQGÜå¡ÈKtº±;!îÿ8p`ƒíílSÓ“ÊÕÚklI”‡rh™ø³Íâ| Âíµ5åa v€lü’iÁïÇÆ»îÄâëó~Gtãåµeô zBSŸP>Õqj“4{¹¿ðà<ÅœX)‡SIî—œ„zì]IÅP×Þs·eU³t Ú| >±|å¥É1!à¸üÀì–ª vÈ™7*üBîòŽ—ãFßRÂÓUTŠ´$—°È´´_öÍ®b.2¿|]®@ÔïwW¥EÐ{HµŽÇq.•ûÐ÷¤$apbÒkÆrX³ÊJþ?:”|Ýì-ØARhaØa%eyn+üÖŒV÷‰Ë¢;¤ÍŒ^SØaJ }Aø‰PT5C¼ˆ*_ g¤/GŒ¿,²CqUñ«?×¶¥Wã঻Ñà7oY~è?Âã÷Ü‚Ôäy¶ðµA'û^ОÈ0ðPäe®»ØŒErp— Øå™(^Šä`Ÿ]tâ<«9òK×{!nILç˜çÈÁÈ'„!lú²)Ðlß¾íííÌ™˜˜˜˜˜\¤ŽŽÜ~ûí7%?¾îýVlú;Ý ÈÁú9ñWƒ„.·gžZæjmmÕœF9È ÷¬›Ë2ŠC©×QnÛ£`íìJv€?艰DSƒ&·‰¾ÝÓïåàÛ¥ñi?sIÏið ¾dìPýž•hºëNùÏkŽ/›Ñ²é®;ÑpÃuú=rèèС¢¿/9ûóK[@‡ à›ðÅ›.0ã¹ Çþ À5Á€.Øã€ ¿ˆ~qnº©úˆÂº C`3ì )*2’R0±LUÄ% ç2iŒg2 ê)ºð§Š¢kçC¼ño–‚puM-¾uý\³¨vÚ™€çfj/v˜ýoÕÁT ;øE!Ÿ€HPDu8€€˜'²²Ôãlò1×y·‹ …ͰÄd6´¾ IDATÓRÁÄ£é''1œJb<ƹt ƒ“J&¡¿ª vPÝ“S㊆ü¼ ÛDw¸wý6pXBà\2 ðù,…ÒŠ‚³Éâ&½“R‰€‡r #9³ Ü;YN¡÷‡ñƒêv¿Sf°ÃœrŠh¿JÁ¯±l{`‡éŸó„CHÈFp¨ „õ ༚Á˜’™LoÆGq*=‰´ª8kü³« Íå8áö0«šQ!€îµîå7›Vd:>=ùúÅ¿³‰¤…:sü°ö·så s98¤*e9Àíæ–¸¿,+ºô~¨½¨9 ƒòKï­¹ÜÒ˜tBDkž~í_™=tuu¡¥¥cccÌ1™˜˜˜˜˜¨±±1Äb1tuu™“!„¸áouÚ§C´d ÃÅùc•¾óIÝÝÝÌù4¨±±MMMÚÝd´ÀÃ<ë“÷B‹âP¦r_äVâ¯ø ¶DjTž0¿‚.²Ñ¾ì«8Ÿ¶Drt¢8èá"ì`‚VÜügXýá±!smùÒCA}‰“£Pû{Šë[_”w,Pójÿ2à9Úüct°”°Þ±âU>z tô˜®´«n½U Æ"oć‡ñÒw¾‡§îùk~úÇIKí]b禫ñନÇãÈœ…u1‹oA‰Ë,@ã‚Êy󙦢9„ý|Ÿ;ÝÔY{ˆ3o&vì )*$YÉ›x8•ÄDž›å’„Ó‰x1EÏÿ©ªêòž›½Qz@óáuãÑn]V•‘HÙ]u¡¢~—PÀ„d#;X gœšœ,zí¦¨lCÈÚ‰‹ráLÖî7˜u!ç˜n3¸¡;2…üœPtT >T¢†²í‡f+­*x7Çy9=ê™”3x;u¾8èEvpž-¹Êð©7÷ƒ¶¥Wc߆»Ì›÷üôÑøé£mì=cÎ 鏯?–aÊåœÅ‹‘v›%Q¼4,`—«M.qå=É!gM&NɳšÓ1à!·úûû±wï^Íé¸EWƒÍã*N€ìúøµ~í_™Ró½{÷"‹1艉‰‰‰ÉaêííE,ÃþýûMÉ„.‡¸î ºn’Ö6%rÎâŠø«AB—kN×ÓÓÃP£ôDSGrÂ9û:ÜÖ€ƒ³ÖÜîØ£`íì:vÙ~ k?³¦=Ê5ô±{ü |ÝÁ¾ì«ÖnÕÐáy#˜ ;¬¾íC¸ìê÷jJËbYŽ®b(„÷ÝŸîôêñg@“£E}×ÁQ¶xµðàÕÕÕE8ò ~ÅÕR<¥s`býG·ê.÷Wâ7_}?ÙñEœxöËí >~Å |óº?Å•U §-¤ øÙ7ú“â"9Lÿçx¬©®Ââ`°`š‹‹wB úùЦ >ß RA7ì0ã¿Íƒ€|Ѳߞ”$Ä%©`1EÅä¬ïh†¨ªxð <òb/&î¶×ÝaÛš5e?n „ÃÊJ—@6Áç3H…üÞ k·r‰î@) (íEw Úlñ焊/£Rôƒ#ÅoŽj/ù'eã©4Î&JÌüM¥0™É@VÔ"ëfd‡/€+¢ŸüZ_Ȇºÿù|°Ã9)uhóÏã)Å©ô¤quÃøêÅÈ9Yç>s±H=ú¯ú¶DêM«Æ¡_üßs R“çÙ¢ØD MûÛ9²<Ãk\98¨*ÔCvÙÑ_®g:Å:Ú©Í­Ùào4§‰D" xÈ#½ÑÈ’X?,Èav†\ÝŸ‚_û—¦äÖ×ׇX,†ÞÞ^æ¤LLLLLLPOOb±úúúLÉT®²vpH4‡|ÒÍbïÞ½ Õ(½ëuü,rJŒêhÀUäÀL*»vvK绸é¹ÊÕÚ[ hà¡\¢:”ƒ¿;_$¸T»uc'@Ï‚Žõƒ&†L< @\øQôEWÖ µË’à¯ç…^g瓸ÊFFF066ÆF׿Ýû‡¡JV’CU<¯âÀIžÖs„ªÞ !‘/Û¬3¯ÞWøQýð@ˆ9 <Ü|{å¦s ‹ï® n^†1sælëk2IUqäį9";p‡ýìwÁó@ç/4}Îö¹êlÛ•‚ƒÀ2j*\á¡-0_ÿKGéìÇÎû>hûù¥k×ðâŸüïøóS¿—¾÷aéÚuo&ž^|íèøÒè86d$ µƒM²ÃFt+*Æú‘”ä"CL†*‰u7ô²(¢GSkQ‘¤¦/ š—›Îð;Sw¨‡å²n™ÐÅõÂ2ìÒÊ«·™_z4¾YÝFòj›ÿv+r±RÁߺĜ:!Ë\Ý@¢†‡ôáÙÁÉ®[U:gé¥}£ŽÂ ';0äa§Öv2±¸½Cí›”¶Ç7†.ÙRŠ›å2ò¥ Ó¾üjÅ01_,  ëîµ¥£ïk}ê–db ˶WÛµ.k’m²ðñAÝaìP0*L=¼dú¨*@jâJy–oàçËóx³Ç•ò*Ô„ï`ñE1¡6Á™]â#%©®©´´€§OÇùçÿ”o]ÀÕ Ò£Íã&”*\É!àJáêájZ˜]¸ÈœŽ¬155…ÙÙYöyfK6jŒ¶]Vsh”!IC:úUW1æóyd³Y<óÌ3<`988888|ÀÄÄNž<éZ~Òî H»Oº[ÈÖ×Jj$¾9_9‹_V˜óïþÆÕ…=-Wq¨[¼ž; Múx„àˆðPWáøÀç纫Ÿ=Qy0Jµk„7zí[_¯’’CƒŽóåÎUxXß= 9®9Jk¾þMŸ ø¹j®ðÀÑVdƒX(SIÁTR¼uÜ®×Ë/…yGiò[Ͻù׃ÿïëßÀŸŸú¼ù×/ ²\ðÄ·þ˜†ßR ,”W!ª¿'Å=©k£[R°+žB&–@Z‰!-ǰ-–Ä°Ö UÊçÙnèE”L=<ã_m”L³…›¸©—`®&®P7õf yk5 àú’) ×®o¥÷ajôƃ®­´´€ç¾y ?úã¯ñr (.ÞÄܯ²nÝ»¬c2”$‡…*³€¶ lS†®½G}Ôq{qpppppp°#—Ë!›ÍâìÙ³îd(j†0p‹Ë*¢CÕz²›ýFZ®ðÀGû ½Ó¶ÊC“w‹áÉIu‰òzî,XÔsˆhµ›eP}Á¢þšTo$b¤ú@H!( JšÝë›KŒÌ_<²ˆ²Ôñ£s¼¿‡ý„³¨¶©òPIîªûÙ(¶)'<÷±Pz°eWB §ê;ï»ƒë¿ØXºv /ÿÙ³øóS¿ÿä;˜{퟼› $ ÿ|Ï~üÑîÃý™j©'Iê«*Ô9Û^ì ÷¥±#ÙÕà‚˜,®â²QØHŒ¨[Œj²ƒ¢T=D²,•¬o"ΗËXª°ÝR¬ˆ"[ÁÖ>r¨î°–›" öë£ÑÓé@ÝŽïÞÝñãÖ¶D±¦ñÈ2àq9“Šbk¯ªIÒ1Í›BuÚ;+ÃXýÁ"HÙÁ;2Z{ºz°¯»ûº{±3Ùí‰. Ä4¤Õ’’Œx=夓Ëeè¦Ù²7K%ÐõX'íh“Y×"½@º%iYCZÑeFÛþ’`Q¯8ªNÁFý„rŒg´±¦aÖIh‚âJÉ'¥‡VêK Ö–y<1„©Ñx$½×µbþݾÿòµß@qñ&ß0;ÀÜëÔºwr‚«8¸Uœ`eÐ6 nS†ª½¸Šƒ=Oæþ–9 ';Ô‡“[r…þ;@bÁí›m#9°gHbŽ~ÕÒœ„m«À@‘DBc »d‡nEÅH)jÝs÷›3 ›> 6Ò )Ù¡*µd‡Fê7KEæöìÞ|p»YIÖ>2MæÃÆs‹I"Ú©ì?~÷]\]^fN÷‘í;‰Ç;zÜ’ˆ€‘®®Tôœì² ­Åníç¬\„à¶dÒ»=eöµžÙªC¸òµ:ìД$@›äßÌF˜”06ÏÕ”bY×] ÷…J¥þlè:a€°µ³#Ûþ“8RwAJR½wjãÆU‡ºû Pä*ÅpÖ—jÖt‡)†g~ ßÚõ kÅýÅ ?Äӧ㪃ÃûŽK3/°'êÞ¢÷ðúÑ€“œûzw}v m¦#‡úè<û\’Ífù„bÉÉIäóyö]á–l0;ˆëáîR†RÒѯBÈ|È•R=ù䓘˜˜àÌÁÁÁÁÁÑ&LMM!›ÍbffÆ•üH|äѯ€$¶;Ï$äjV Ý{ÙoÎWyp'pzýïíï/]$8„—ä!p‡ETßùµ'DEZzœè•>8QxÀÂe ts…ì°|­å2$1vâ×K¥øë2ÆNüš³(·¡òð 峑›wx8óÀjƒ+<¸§êû>ò ýýëÿ½QÍáÇgþ.ÿì%OýˆKŽï¿ÿ¥áøÈîªÛš I„$ wå°OvØžìÂh_$"Zr Ü#;•â‰tþ’ X‡ìP4 °ÞŸ›Rhu‹“Æ1ª;lLªJ"l9Ìúgc½ÿÍ»ï:ŠõÇèøqkO*U_¥%(E@’}3ŸŽiÐ4†*ŠØÞÕeùß׺ÝBÝ“læÑì›D„VÁÃ#Ò†?8l“uµ%—ô— cƒÊ{zûßwÙ€£1zPIÜJ×Ád€`Ù°§Q\#–„±¾j3}|ëø¯‡O %ª®˜™{ãU<}ú8.Íü„ožाHß‘€/¨¢¨äÕæø‡()9D?Í«ë(®ð` 'êâîô°ÝÝ}ÔÉ¡¦Þă¿ééáìÙ³Èf³Èår<988888\Äää$xàG¤P+wC>òU@rxùXÄH›×YB÷>æ\8áNȲ´ttiíRv 'Á¡û… ¸ÄI†&*¼ú‘iñÞ]ø3Ô'¢ËN.Ãxå?»Jvbªk>É~¹ïF Üá{ïq”¶™ÊCÀ/”Z[J<œy`5îüuÜœöç/8Rwãò”——qùï_›õ7˜{íŸ|ñ!.Iøèm;ð±mÃHHRÕ[ J"Äf»nRŸØ°ù¿²Œ½©’Ò8]¢`‡ì°z4^’@6ú'u þ“(¥(”u«’1Cô©1{Ûü‘a0©;lÎM‘D[wBvX+Õb¥‚¾u‰¹^Æúû;^Ý!&JÁ¯BÉç% zÕRŠŠÅJÓ€aRÄ% YñŠì@ý­ïmnRwð}_2²C»ó .Ô—+±GZ÷·…6)麋uI@)P¡&"¶±­HpÆ––z¤ý‹‹2õ²­gB0¨$Ð-)ÞÕO€É,(˜z¸ëK¦€IJ54›Ú食Á±ü¿1³4ײ™ÒÒžþâ§ðЗ¾…Ñ_ý4ßHÛÀÕ×_fÞî]A{cÑ¢DõÇQwiDMw@nüçÜ‹Ì9=òÈ#èééG5r¹ž}öYætB[Õ¨¡O= kñÀo‚$G`\øO-gwîÜ9d³YLMMñçàààààp8{ö¬kù‰ÃŸ†¸Åz&Ú:¿¾/Bï8ÌlJNÖ°Žññq cvv–­åò¯$v¸¶\&Œaî¯çH4>¯úúã–ÂCéZDë˜+:„:–¥.@P³Ì–Ð(·l»dŽZúä'0ûcvÅx:´0¢õY/Æ`*)å|ÝÎF­¹Â¬º8û(”hÆøª‡-GFñÒÿùŸñç§~/þÉw|!;ô«1üúîý8s×}8>¼IÑ+‡ÖeI„hc—n‡ì[ Œ§û½#;ˆ"È&¥ ë2úOv€’¾vÃskdƒu´7!;l>lÌæ&‚˜$ºNvØŒ;Tw8¾kw¤Ç¤’aÔ¿)|‚þc0©Ueñu‹Gf…:°\ÔñÞB× X*UÖuË{ËN#;ÀF•Nv`ÈÃN9Hûc ҅ÏdÝ4›s|_1L÷ꈶX7޾'·Ú¦-ù³‘ G²÷I$lus²ƒÓZêKÀŠÚÃ&÷GÔ¦ŸÀcƒ£®™zî›§ðÜ7çé&¸úú+(--°%’!<ì6$׊Õ[žøjw}n¯¶™î€8¬',2ÏNËf³|B±€#u–ûýí,mSFñn,¶} âÏ»’õÌÌ ÆÇÇ1==ÍƒšƒƒƒƒƒÃ!r¹²Ù¬{dQƒ´ï ìd‡H©9Ø[g Wyp'û"óê]Sq ¡ŒÝ+9ðzî Ô©g^õöÆ1µ5ÆÁ³ ª/xöÊ!óy4úIô!8Pyhé½{8ÙÁ#Äûû±ç#:Jk^­ÊÃý‘ë§<”ýE&“éH«;¸;õ/\^Qx`]‚KûS¼ù×/ ²\ð¼Üýj ŸÝwߺë>|ô¶·ˆ«çìI€*‰PK>°Ú±Û!;$d£}iììê^Oä ÙA–ë¦ Ù–JÔ#;h¢Ø3ݯÅ!ÁFËX|dšLê›3‹Éb[n¾ß¨îÀ‘ºÃP<Ž{·l‰ÞXD)r¥Þ^\Ä{…Þ+pyi ó¥"tÓ¬z¶GQÑ£|Q-Ë€àórf5Ø–Ë\_, X1jº…aR,•*¸¶X€n˜|Rt&]ƒø{‚6äÑ"©‚z`ÖÉ+Ó$uÉÇ:?w´EÙ¸T7M>m$bê!qQFZiüB6­hÖº¡ ¢{ýÍ‹>ÝÕñ$Ûv–J4êK®%=ôH1Lî}ßÚù kæÎ?ÿ}<}úS(.Þäs̽þ {ûFvØ”®'ª?¾6ð+ô.ûXø¶Ö]ÄI”6u‘ο èËÌŽ;Æ' 8!<ØHrÄû˜v=ü=èOuM¬|(d>iüË€Ôºòèìì,²Ù,'=pppppp8Àôô4²Ù,Î;çJ~DMC>ôE}ã kÚÂï‘AÞ“Ù«/߯lØád_D—ÞbÞƒ…“àÑ=w ßïðSö¾Õ3ç—8ŸÛì}iñ':ðq?xPÒžš=Œ÷}êQNvð‡ý䏯ÞæÎzýs»º6XŸ3™Ìx”ÚüG`*È1Œ0/:{©@}zys Õ‹ß:4†oÝu/îÚ €ÞzDU“DdÍ  ÙÁÎÇÛ“]8šîGJQÖ°C’è4²CÅ0Ÿó%@—¢65Ý­*HH’£²¶®îÄdɱºÃb¥Rçùj²Ã•åeÌ\»ÆÜ>º=z*7”RÌ –u·\ÑqµP¨"=ŒtuÛ!I Ùa±XÆb±b£ €ËEwI~ï/ý²¯GLÝÁ ’@P$à /$h›Ûƒ‰ìÐÎüëØé ²ÃÒ²†­±$ÔM‡÷“’‚a-…´¬y;îöýamLJD°íK\”£S_² ˆµ†ßz'þëáH‰î¼ ¼ô?Áӧ㪃ƒý€K3ìò³$=êa £Hp˜_^´W¤40Õz¿¼¨°$;)ç^d¶666†‘‘>¡lB.—óÏ>ËœNØþ߸î>˜±½Éaó¤ç¤ñ¯¸BzÈçó8zô(&''y€spppppØÄÙaffÆ•üH|äѯ€$¶Û\3DQÍÝÁÊ'<°Ã)ÜœÿYóØ'9p—:¬žÃÒø¼ê]нŸ½5Ê×"KQï/7D1ï†FcÿÃk« IñjäxÃ÷}=¡^€y¹þ»÷€_,ŸÔœÃØT=Xj%|Ð +L¯àÞÁ-ø_ŽÞ…}ävÜž¨Zˆ ÑA“׈ldk’ RŠŠ;1œLnx€4&;Ø$X§[Í›×É^ P6êÖåÚôª*Jýe EF_ÝÅ ‡ÔˆÅ'¢@ Hcº ÝŠšÏ“še÷ß¼û®£:þÈŽè7Ê%T0e(¥˜/•„@ÝAVØã•uËeÝ~2 ä %wmJvبîÀÉ y´hà _¼ T¸a'àdâz;Eƒìà’¢‚a­û}ë[Õä-U‡aÙ¨àz¥€•"J¦á°¯¸To’bˆ&H5¬êK¤+ćÍýÔLžÀXbÐÓso¼Š§OÇ¥™Ÿðýõ&\½à@á¡ïˆÁ =\+J‡‘vw}n/®âàÜ·\¤7^fNÃÕ¬áDÝÈÀíí¶©9x=&Øs„$‡!Ýñû .ýpxòäIœ9s†9GLNNâèÑ£Èçó®ä' Ü ùÈW«Ô¢°æwgáFÜRäè4<òÈ#ì­œͺÝÀIÜ¥«ç°4>¯z×AìJD´ðNˆã©úLˆG„/È@D~4Ü {?ò ³P¹üÓºßüby®ðÀá*²A,”©¤@EÎòr­>/¿ØPÖÆoh¢„cÛwâßÞñAü‹}‡°#Q}«» hŠU!®Ÿì'Õ»õº³gãGDAÀÞT÷õ!&Šh–©ÙØ(en„€(Šû †6«;PJQªèÖé6e2Ó0”H .Ë’ .ËŒÇ1ÓìÌê#jVß®nßÍõO4EbLw V Vd˜v î°;•B&Ô8D)År¥y›• Ó ¶º!€,û\¡·þ¹P,3'7LÊD’hV¿ëÀs¬?Q ;تkâA{…ˆìÐn;.’D+šV•(ßûIvð½G’è(/4°±h”ñf!·K ¸^)à½Ê2f‹y¼U\`#>Pgµl™蕚ï«T2"Ô&¨fMõŒ'†0uøéÛëJJK xú‹ŸÂùçÿ”o²WQ\¼‰¹7^eäî]m ®ä¢¢¯äæ¸ëÀ8dTr°Ì­0ºp‘9'æ Ù£>âýý¾÷öpY¸ ºp¹î÷¾\>¥öã„ÿH Wwpæl0oPØžHâ3{áûÀý8¶cúcÕ‡”$Q@B‘“D²Ií`í_6vëõT¶'»pÇÀ5mÓ3¤± B²C#%‰uu‡5²©ï‹u‡v“ X1øV M1¨iØ–Lb[2‰!MC¢îÍø6Ù"ºáÄMldÀh²È3Uæ7©Ô#;,V*ø›+ì ÇwíŽÜ8T0쀌‹R°ÕdÙ{Y•:›Ý0a˜Î6~ÅŠîJ:n¯KWÕ¢Bv‚ê‚yxA¨ð¢¾\Vv Ù@…+d’ ¸ÔNÄÝ¾í£ºC'‘Þ)-¢Bkçõ‚YÁ[Å›¨P³M~4Žÿ%ŽnIµ´#€ £$åȵI”ZÒCÃ3?…¯m¿×µâ<÷ÍSøÑo´\šy=’»wrÂ¥À(É!J~µ¥‚h›ê.`~…*î:0)uÝE'ê©T ã㑺àÉär9<ûì³Ìé„-Y÷;OÛÔ¨cC‹¶¥8¤ñ¯@È|È•bž={ÇŽC.—ãAÏÁÁÁÁÁ±atìØ1<õÔSîd(jvO@Üöñ:k®æ`o¡‰³ßžÍ ìÈf³ì‰Œ°t‰Çk\¢¼ž; M¼úÛÏDM"ã…WfT_‘ß׮ΠJÛÌîèc:r˜'Àž~ØYè\~±îwºKµ´ÙL¦'*mÇ >"“ÉŒH±l—Y Ì+/…ù@•郃[ðÛï{?þç±_½ƒÕÌD@–D$ êš´‘S²Cž@ZáŽAìH&!Á6ÙT›¯%;À&ÙACKv–Ë•:¾1›nþ­UÓ\ùkƒš(Šëío‘b²Bˆƒ2® W.WÕ{½¥÷ß}×Q¹wË–ÈEÝþáúÛ‰à:"‰€àãòeS°•tÃqV†I‘%:ý¥L uî¢Fv m¶’ú¢í©§ØÉвCU~n·SÛ¾çd7m\)-5Þ{€â½òrü°·ØÌ( lS»Ð-©ÐˆM–5 k©Z2DDÚ¤Š ˆµ‰žØq¾»÷!¤Dwêáï~ðm<÷ÍÇQ\¼ÙÑË‚¹×_aOÔ’ºCÀ^þsߢ沅okÝE<ÛwtîEæ4\ÝÁNÔ€ Üé^Œ‡äP·Ì.Û–â|Þ5ÒóÏ>‹l6ËI¦§§‘Íf?-!j}ÂÀ=Ë®æÀšµ•Nx`ÇÈÈÆÆÆ˜Ó­©<?N¹Š¯ç¨5n™—Z÷»€Ÿ·ŽÌ­GœðÀÉ'<´½sv Æ ß€93ˆ"¥Õ>±}'þè®ûñ™=‡p Õ[=aM‘Pe(↡¡Ù¡éj¶ö™”¬b´7ƒ½½PEÑ"ŸÉ¤qyÖÉ›n‡ÙA7L˜&u$ƒé ÙR ÉÁù²iâÊò2Þ^\Ä•å%\Y^ÂìÂ"r¥Òzž1YrPÆ[X¬TÖë½ÑZtæû郙-HÊr¤†$J©mÂC—¬`0¦#„’mcle£µÃ÷† òPàöœ~«;†ÏõÐad‡–ã!DõÕ&Ä%É5Q²šŸ{¹¹ÔΜìà‰E£ ÓFA2LJ]ôƒ-€ã¢ŒŒœÀöX7¶Çº‘–5ÈDˆd›Ô…D¹vŽŸ<‚©Ñ®‘Î?ÿ}<}úxG“.Íü„=¢ûŽ8„ª8p’{%q%‡†K‡)9´Ûê<»Âƒ£ÛK;N$9 h á¦ESC¢C{ ø<ÄŸw%¯™™Œczzš?GÇbjj Ùl333®äGâÛ ýýêÜèÐÒþ‹t±Î;ǃÛœì“ÌùŸ8F#ö8t(¢ø¾/ZLèÞÏn­|-ÀõÐ mÝámg«¤]5+©*Žœø5¤÷íõÔ])¦‚£1ö~äAöDztî¼åW•äŽ@/q£Ònœðà/Kx¨Wb%ØsãÜy/}úÿû?Á|í  —ý²¾~ü«ý£øÆí÷à‘í»jɉAB•‘P%ˆ«D‡õ#Mu$HÍw›gÿê¯UQÄÁž^Œöõ!¥* ¹Iv ›2X';4Hd²,—õÚt„Ùtóoë%0ÍÕ—‘ÖXÖu¼»´ˆ’¡WeFA‘+—ðîò2$Q¨&Õ8¨ÅJ¥)Ù¦¯; w=•vã„1ÄB–?þsX ^ÆK߆ñWOÂ|ý/¼ÿ#So?¾°o¿ô|bÛNô+±ª‰\ )"ºb24¥öwbç¿›€j¢Ã ¦Y¦Û,ÑìP‡|Ø$;È2ȦëÃAv U^WtãÖAÏ–U$hrhþ½åh“Õ"!%j´TÎ\¹lë¹™k웣ݩ2ñxäÆ+ƒÚ[Ågâ $ýTQhÔñÄà©NTŒÖÕbóйßd‡ lFÆ 3mBNvpÉÆrEǵBK• ¨Im—à‹åòzZ×|ñ€ì°M’ÐS!nì÷6È"zTš•ú';„üc°¡ öç m• ÖÁd‡v@1¡¶ð“{Æ·v>芉ÒÒžþâ§pþù?í˜=ùÕ×Ù÷V«;ì—2׊Òa$„ÝÝN 9D4ƒ6„̽Ȝ†«;Xà á$‡AbýðWÍÁë±ÂÏNPm›$‡!yµ ZÇÉ“'qæÌÞ88888"\.‡l6‹³gϺ“¡¨AÚ5ñ¶‡}|¢}ì¨ô[og'<@/€.\¶þ*¸ç®S™Lf$ mÆ >!“ÉdƒZ¶Jr˜7PƒËœ ”šÃ¶xŸÞƒ3w܇/ìÅxï­×ÖæoI$èŠÉHjѺÛ×' l 4!;ÄDû6ꥫ¶El‘,M6#;D‘kdšÂ£ìP¦X1œgÒ¬`Í` _PΗŠ(›FÓüUYDÅlíøõbÑÖÚtÚááÞÌ–H]¢CõŠ(bPÓ‚IxeëïóF¨ä‚Ú€$ò¥˜ý9XçuàÊæÝ²ƒ… J)òÅËåòžCâÆš:ÄbÔl±><$;¬µ‚ˆ¾X Y†¸^OÖvD$$}±Xˆ”Úÿ}'+;ˆDpfƒÙ—Wvh—™ríÀõøÖ;ñƒÇ‘ÝyéùÜ7OáGüµŽ˜òœ(<î]àJa[Øp%‡àÆ]‡Å!¥ÁuQ_]¸ÈœŒ¬á„ð l¹ßãuFÔü?-æøä0¤;~Ä¥[ÔN:…‰‰ Þ!88888"‰ééid³Yœ;wÎ E ò¡ÓîŽØ^4xY“®}ŽÚ›ƒNöK´­ TO ¤KQU© AãóªD<“ÄvöR–®ùX?ú­Æ²âüRÝþ2ö?ü1>…[o?Šx?;Á…^¶¾€(àÍGBåAâaËh3t®ðP;HæA/¿sö |/OŸÃxo?îÈ`G¼ke²ÝôŒ ¨¢€˜Òü`T3²ƒ¥ H) ¶&âH«Zm¾ üÛ';lVwhNv ¥æÁðjõ,ʺ‡¦›Û(i6¯úÈųнÆÍŸ³•¸| Hìä}žû(—…®0®±“ƒ<ò0äCQȰõö£¸ðñ…ÜüËÏ~Ñ|À3ao/~­°ä/JTTa*)Þ:kõQ˜‡qþ{0þêI˜¯ÿ¥¯dM”ðþ >¿÷0¾>ö|zÇl'«"€" Hi2º5É‚ìP{€ÉÙ¡ÞÅꃚ†Ã}}8Ü×çˆì€f¤…ª²’Ô¦d‡feižÎ>Ü ;P›+4WÉ”6½U½\¥ØP?3E@ â NVˆ.”+(Íoy¿²¼Œ¥J…}`Ž*áA!6! Æ5ô¨“S#|âf6én…r¥e²${²°¢2ãOFŒì@ÚÖõµP®¸JvX›AtÓ¬?Ö”ì°ù{I—d$ä[qQnÙ¸×δÉ#°dŒh %5Ÿ«e"")*M ®/då-ŒBkºåxbïøÆƒ®˜:ÿü÷ñÏ}ÅÅ›‘ì ù«o!õmö™=íáÁµ ´:LÅ!ô.û\ø¶šêU{kJásÑ áÁÉáN€uë‰ ´±ÿy”uMøÙ!šØ­W4)iìˆîs¥Ï>û,²Ù,r¹ï ¡Çää$xà×ÈBÿÝ!';´q­Ó®¬Å8ˆÚÇœŒ«<°cddÃÃìÖZWyˆàûŸ@ºÄ¥|«g^õ‘ŽgG …w‚ñž!R±Ð] ñ,÷²E7À> ØÏÞÖ —A ó–ßU\R m"q'<ø‡l p–‘wsá¢}ç§¾–åHO?þùÎøÃ÷ß‹ÇvÀXoílQ H¨z šA ͨ µŸØ$;ˆ„`k<Ž;ú°·;…”¢ØR<°";Û9_õ­(‚¨1[,'Ç>¨;´Jv€²Íþ®’€•CÆ nU' ™©òÊMýªêˆì@)+Ù»ñvúWwØ\ý±X]Òà¦aOw ²°e(ú£î`cCT¨è-›ÑdÉóM™ß›BÇehQMÃaÏ ÙÁV}¶hÇ!Ù¡d(®ç\$;¬a¹RÝÁV5éÐ ^]EiÞÛ<¾Èµ¤‡)†©Ã'ðØ ;‡òçÞxOŸ>ŽüÕ·"·¾½4óöï;ì]<¹öS‡‘B/]U’h,nð‰ÒP»g¾÷"sšcÇŽ£Žkê®ÇAX’m¹ZÄŸ‡0üIWJ533ƒ‘‘~Pƒƒƒƒ#Ô˜˜˜ÀÉ“']ËO¼íaH»'BJvhãZǃeéb?Å×1Îàdßd2"ºçœKQ}·ä†ç$‡NŒg’ppÈW_ï¼ß k›v›$v1§Yœ›ãCT±õö£ã{ÎÿÂòsC ª«÷G¡½8áÁd2™d¸Ãy3„èp[<‰6rÿöè½øüžÃø@¦¶ó‚˜,¢/¡ ¥ÉPä[ÝÙÙaó×–d‡ Ä% {ºS¸c`;»º¡Šâjº:Öêø¯Ov°NgW¥€"ƒÈŠuÞuÕ&ì«;øEv€’ ¨ëdÓhx«: ÙA–D„@ô1ª¬‘àJaÙVš×ÜN3žîôø& 2š†”ªBÅõ¿¤,ãhÿÀJŸÔn˜¬üØ5Ûsf#-Äd¤Þ€ÆÉµdêu¡BFvh·;ù×yf±T¶™˜ÉkÉŠçª0“Ü蟴Iq²ƒÿã_‹6B°=Ö…n±v=¥ 2¶Çº¡'ó'A % Ë„êôH1Lî}¿µõNWL̽ñ*¾ó¹ãêë¯Djm{õ»?¤{W{ãÈu‚CD–7"9DɯP›ï0‚CÈ]¤…9 ðs:®ðP‹\.‡™™öõYÿ#:øÙ?Ý­qä8ÄýŸs¥„ù|ÙlÖ)†ƒƒƒƒƒÃï5N6›ÅÙ³gÝÉPÔ íz ⶇxOÔw6ןÝû˜ÓLMMñÎàNöMæõŸ/hBýŽ"D³cÑ„àÀ«¿£â™¨i@d;4LõÀ,µ±Þ:!6:´K´=žÙÏåfƒs™Äx6¯Ó±õýGÙÃpþ‚åçz€Ï_g2™Ð«Öã‘·=%ƒóÏÿièׯÅÅ›˜{ãUöhïm=f¸’ƒs¿¸’ƒ;æ£ä—•¶QÉ!r}lõ“yvÅ®î` '·Þ -‘<ê Õ¨qÜÚ#Lk€žƒÆ~ÜyvòäI<þøã¼ÓpppppÏ<ó ²Ù¬kdßåèï…ˆìÐÆµŽç˨굉oc¾=X!Àp°¡§§cccÌéÌükXw‡íEä A4Qrààñ¼ZlÁÁZÁ,^ë$­5w»‚"u1'Ë]z áÃÖÛÙ @jÇ/SIŠUØÈ†½­8áÁRáA×AÅXçÌ…ó`¼ð ˜¯ýÐ žÙÕD w¥‡ð?ì=‚'ß…On߃mñä¦y™Bº4 é.]1 ò&æ]su6²ƒ*JØc<Æx?5 jsªs&ª1ÙÍÉUå±Iv¤À’4 ê§Yý ¬›vÍ2—Ó• PçûzÉ»UÝŠbù* èÅê~oݤ¦v òìê»S©ŽIx< bÜI¢Ð’øDB‘ƒ»I ZĶ0§ÉÏ^âC[!ÇãHí`'lÑ›o[~àKçÇÃÞVW8k0:EÝA/À¼ð0g½•nÜ“Lá®tGRihuóŠ„@SDhJõÍ߄ԡ2Ô%;Ô™ˆ7ýCÒj }«vH +ÿcQž†éˆ-²ƒe™­È„€( Ô£$„ƒìР͆*Žo7wP]g ;T³fúb1ÄeË• J«‡”“²„áž.È‚}~Ù!W*¡hè¶Ò³(A¬÷ÏîÎ$<ô(*b^+)4ƒ x«îàpsWd,•*ì‹ä ª;YYÂ3ÂCÔÈÄðÀ{ó˜L„öµI˜Ém##øOv€+å%Tšj*¦¼^B¯óoü ¬ð’ʦŠi@§& J¡3dˆPª(­¼Ûã‹C–BJuO aêð d_þf–Z—Ãýñÿñ‡È_} }éL(×°s¯³ßfNÒ£>ÄyT| u—FÔ|Ä!í€þUïéÂPxÙ Wx¨Åôô´£Û “Âõ1„hpcÚËñOŠCû2ôW¾šûÇ–-œ={ÓÓÓ˜ššBOOïHÀÄÄΞ=ëZ~âmCÜöñÎÝÏÑ`ú"tð æ5/;Ž;†§žzŠ)Í-…‡ŽØšvr¡"Ê«žÇsËEw¢EK×øøÀÇÀ@ºLÔ~`ñçLi¯Î#œ8¸yF…:¸í—j>דÃI~ {;qƒÇÈd2#†ƒX6½tî<ŒóßóLÑ¡WQqWßîJ!­h–ÇŠB É"ªhMpDvhð úTCš†>5Ö0#WȰOvجî`Iv$Q\%; äd‡:>l<«H)L“Mm¡î·Í g+¶r#–_ÄD±ê}o\e";ÔÕ‚}Ãën¬ÙÓ¡ \ÝÁ9⊄bE‡a²í´jÔüÞ¨™ìxDxè0²<²AWÖ8ÎíÛåêXîte‡¶ø ²ÃJ‹zÙ–«‹FÅšðÀ•‚çK%Ó@ÉÐQ2 Ð S¡*†%£‰ÐDšèòú£Õ±X¦€À¼•OÃÔáxüÍáìÜù–‹xþùx}é bÉîP­a/Íü„=êûF=Šñ#9„ÞeNrm›ÑêcRlî… IDAT9Pwã³-àHÝ!9 ð¿?Ò Ž~©¬ÙÈXŠCû]¯ýG˜Wÿºe‹333Ç3Ï<ƒññqÞ™88888|C.—C6›ÅÌÌŒ;ФáOC¸§óös4ø¾89LÊ Îàˆ0n@—.9»å<‚]*b…Š 8Édz»E' „}| ¤Û$¶•9 ëyŽà`àÀ~\øáØÂõÆËÏ+ÉÐêg&“É^¹re*¬í$ðPõ}ëi…½ãü÷`¼ômOȇSiœÜy_9t>ºe}Jõ!+BMÑ×¥ ?©´ì ƒš†=©îÄÁžÞ²iÙ¡á¹{²!D’6,¼¯{¶(|dÐëöuì`+êN¯E ˆlÓŒ•ºƒnšLª Nvw(á¡?so1) Kå –Ê +!Ø(BÒT†ŽW$kuŽzAUWq&’›nWò í·Ã`CÄö¾¤XÍGu¢šu²m‡oÁ ;T¨‰ë•‚ÿý-(6˜í„‡ì@A±¤—ñ^iùJESoì`Q_:5± —0_.0+E´4NÚ@¨.pÃäÞ‡ñ[[ïtÅÄ/^ø!ž>}ÅÅ›¡Z"\ú„‡Í ®*…GUǽŽ_¡—®÷±ðm­»ˆCJ#ä¢;Á@çÙo¸ºƒ5êª;xÐëšðs,hb;`ãŸxàswÿº+%˜E6›Å3Ï<Ã;‡/˜žžÆÈȈ«dùÐ递Ú¸Öñ|åܠн9͹sçxgqˆû￟9™ÿ§pmM#´WæpXϼêy<»8>ó-¼Ô ;Zëî6[,+iæ4z©„¢ƒ‹z9üÇÀÁýì‰ ó–g‘~ñ|¨omá„0ë¨$‡#Yátá2ô¾úÎOÛj§WQñ«C;ð»ïÄÉ‘CMÕNz²( 7©` +†.M†´z#q;ÈÒ*Éá@OîžT ƒšyýdÈhn•`@ê=ÃBvвrºNy,ÓmÌ; d ”u³™éæßÚ!;Tt›9ÛùŠc­Xv‹Ä:Qá¡?¦Ar\P¨è˜_.áúR Å –J:–J:Š\_*âÆrÉ>ñAô àÂFIôÆc¶b=®HHÆ×Ëàw´µ 4$ên("„EA‚цê¸O³*ˆ„¸ë';ÔoŸÉ°¨—aRÚžq'Jã«Ó¾ú*™®• X2*-+:ØNMÌ— (z‹¾w &jý?³óA|wïC®˜˜{ãU<}ú8òWÃqãŒuH ØP›QúÑ¡_‘!8øLrˆ’_^TÚF‚CäH.åè@á¬áˆðÐ{¨Nû±Öô³£Ø°PEaÛG!îÿ¬+%ÉçóxôÑG199É;‡§˜œœD6›EÞ¥CU$¾ ÊÑßs¤ àûš#€Y·Í ¨¨}Ìɸʃ38ÙGÑükÁÞ–’äÀái=s~ ç6Ž‚ƒu-_ Ú„íccth× (Hl sšü,Wy#äx©Ư›—k?c0•ÀžEä„¶ýP UIîˆde›³S0^øÆ ›ªMØKà¿Û¾ÿæÀ]øÕ¡Z5Q èŠÉLÅЛT ‹Hp—ì%lÇ1–N¯ºSH«± $ÚÔÄÆÁýº$€ºåq‰ì@ È+dBÚCvpcqãàib3£ÍÅÃBv eoø|²¼½´ÄTÃ_ê&dIYî¸É¨UuÝ41¿\ÂB±ݨ0½b˜˜_.¢¤Í‚À;ƒ‹›%IЗˆ!¡Ê6)šÄd餯ÉNÊ`´›ð@Ü!34õ“xÐ^Á#;¬­äÍJ?.û’Ø<~s²C|k?Ùi}ϘQLˆ¨²N„ßwÞ¬”¯Ý%:ØÌjA/9'=´«MX’&àŽ#%ª-›˜{ãU|çsÆÕ×_ ||\šy}Æß¬îà¸;HÅ!.ûXø¶Ö]TØ‹¢ŠC{ƒæ€Â{Ìé8á¡ÓÓÓŽ ýwz×/-C)Àjm-ž» ™Aºýë€w%¿“'Obbb‚w,OðÄOàäÉ“®‘„þ»!~ãñ°ëÏ—RîtBJá„gpDxXºùn±BE\ÅdzEWûÙ‹Óð@;(–:° $¶•9ÍâÜC '*tþ–ŸXå!Ô„‰‡)0‚-£Â½ãÿ¬­ª»)|xhv'S«ÇÀ(€•CÛ‚h²ˆdL‚¸‰à°ñß­’D" [QЧÆÐ£(57)[åoM{¨7k×!4LLs ªG' D-˜ öŽÚ>^Ú¢ºC;ÉP1 ›¶‚Ev`ª•d‡kÅ"ŠŒ‡Î–*¦ç;QÝhðP¨¬(8Ø^ÜQ _(£/¡ÖW•Ü3˜Áûzû°=‘DB’ZÔ%6k:Ø!;X|ÚI¢¾‘•ÿÈŠªƒª®HdÔ¤ ÙÁþѱª 2)”uF;6 f°’ˆ­|Ý&; ¹r©íãF&Þy„§ê­À …ŠQû';pl†i¶)㨑Ú|ˆß¥úÒD qYjmÙDvèUÕ[k§ÀÚØÖ­Ìº";@R”o‘ WŽ!)*Þ¿Q";øàË’^F…iÛ*cÄÛú’jI#j S‡O¸Fzxî›§Iz¸úú+(--°÷‚î]6*8Š 5Nr—é"9D¹µÛÔFaŒ/3g5>>ŽZ8!<½‡< )?Ƕ#@t¨‚¤Aºýë†îsÅÔÌÌ ÆÇÇùÁBW×-ãã㘙™q%?¢¦!: ¡×Ïu¢Oó~ nÏ>wîïHád?eæ_‹RÈk¯ÜÑhò΃Çs@âÄÜ’^~\Õ»8 ˆÔÅœ,w‰«<„NP˜ôBÍÇ¿€>´?p”0t0{ÆË/ÂøéY ­`g¢ÿbä}øg;¢WV«¾‹«"nëÕ0ЭB6Øe$;lüï„$cK<ý©^Ü90„#}iŒtu¡[Qjˆm$;f²±Mv¨q_n©:X¦ 3Ù¡Aš™T ºa:ɵIg×WþlçæÙ..Üd®ã+ËËÌi†:‘ð ilk³ŠŽùåRKd‡5u‹ÃŒB›—#œì¾2´…ð"²ƒ­º Ùa홤¢ ¡ÈÎfFd‡@xÔoˆývˆÙB°UM6|Fe (qïÆ>NvhɆNM,•ÀµIÙÔ›Û ÄŸ¶—( TÑ#Å0uøuÅlIÎÔ×kŸgŸ_ÜRx¨ÛÎ! 9„IݦiÖÕ_Šû? ᶸb:ŸÏ#›Íbrr’w8Ž–pæÌ<ðÀŽÖ.–ë™ø6È£_vt1Üó~è Þ‚¨¨}ŽÖÀìp²Ÿ¢K—:àU'9øVÏ\ÉÇsÀÇ‘GK×;$Æ:´k…$¶…9M~ö8‰TjnÖ^ÎNÅtm0¨nr‡½}P dù»0/¿óå§]%;ôÈ*Nl?€9r»©[“!H¨¶õÆÑW,f9[­CEl'«;“]èSUHBe€FŸÔ!;X%s…ìV²Ãš!AU@¥Š…Pß ù„†ì@¬Ó4Éd¹dGÝ!Ø(*•uÛ¹ùGvÐMÓy᪃4¦ð%Äl**” ׋X(VlpšC7LÐ Ší%<ð<Á©»e «‡£ÜÝ‚†‹ìÐnÅŸbFmQÍE¶";FÝ´9ÿ:v:Œì°†¸(cXKA«I4!H+¶Çº¼û8Ù¡e •r Û¤dî×±›¾ˆ°$=Lî}ØUÒÃsß|<0Ë™K3?aï }G6Tf„oÏoDp òËkÓ4B~yQi 4Âý+木; cdd„ï7ÁÉa/Òs°Mm5¯»CHÖˆ»âþϺR”|>“'Oâ‰'žàŽƒƒƒƒƒ¹\8uê”ky ýwCý zýûXTH@P6>N+œðà Îoú²;±H‚ÿÖÓzæUÏã9dE'jšÝ­òµˆÇZ‡v±€¨ýÌi¯Îña6¤H ; l-¼mù¹ÜKèCKxxˆò@1B®î°Nvp¿<°¿2P=x¤â2ºbrÕüêüžGÍG DQ@·¬ ­ªè–ĤênhG õü7 ;§d4HW‡`Ðì@ I$±i:+RBýK£Av0Lвn¸Gv0ÍU‡ˆƒDv€·—–øŒÑ&ôÇbÍb”b¡TA±b´¥ “BW£§ÅÐM7PAÙÈu¢m§e09Ù¡%;öEÈ¢€Ê:ʾ¸$!¹Y튓Úà[ðÉkPÛc]¨P•ÕƒéñNv…/5Q¡F`Û¤lPÑ FÛ‹ŒêòLî}ã‰!œzóG-ãüóß<ô¥3¾/)æ^…½7¤#z·çxÝÖE+¨_^T°o6EçÙÇC®î`©©)ö¹¥u²—4„ýºgWºDí‡þê™Öª­âÉ'ŸÄÅ‹qæÌôôôðÈÁÁÁÁÑÓÓÓ˜˜˜ÀÌÌŒkyJ»ƒ0pO4ÖØž/£‚·"ñmÀ ¶ø¸xñ"ï\ÐÓÓƒ±±1æþhæ_ƒ~È_%ðÓõ¾Õ3¯zÏ¡-:éÞÇž*’ ”wµ8ãHááÒ[|È ëºo‡¼Â¼åǺ6çƒèæXXÛ‡+4¼q¶JvÐ ³­eI¨;0Ú¿Y.ã­…üüÆ üüÆ ¼µ°€›årg®˜Ì¾lò"Úní'ˆH‚€^5vkh‡$èÅ m"ˆì`RŠe£²þWi6†¶“ìÐÚLÝÖnÒ²ƒoã`‡)ºúª˜‚ cóâ•’`T ×VôÄàüàÀq¤DµeçŸÿ>¾ó¹£¸xÓ/Íü„½Gôñ‚¨Î/¡—®÷±ðm­»Ð7Ls¿({·WLÑ…7™Óp…‡Zär9ÌÎÎ:˜_9l÷h:í¬Ý±K’ÃÞÿuÄWŠ=33ƒ‘‘LOOóŽÈÁÁÁÁa‰Ç>ú(ò."ñm†àà¦egtø²nçZ¢ì‡IùÄ9œì«Ì›¯y¾LäÚ£q=óªçñÚ¢7(¬Q‰›eP}!"ñ×].rãǦ5µ•‡YÿTLÓ‡3 ÜÏ57­ •äp] å-IœðÀ$” n’2j¿±ã}¸»/³þû­$ÜÖ£a([?,o‡ì”$Œ$»p4Ý#}ýØ¢ÅÅšGÛEv°JÝôHi3ÙAAÔˆ,¤UUê"Ù¡ºíE»È62*Ut꬛dÓÊåe›®RJ¡o´ë²ºƒ]²\\ˆÂ&˜HÊ2¤:Š ^‘ l¬nl—ºCÈ6SW–—pei … }¶ ë¸²´„+ËKá­§epmã呺uAÁV9Ú|ˆßÃú’}±²l9Ÿˆ èRôÅbµcV@È&¥¸R\Â…Å.,åðvaqåoyo.çq¥¸“Ò¶Ùwÿ{ÒÖü9ÙÁ f;$”õeR¿êˉÐ¶ß Òñô>Lžp…ô0÷Æ«xúôq_H—f^`Oë-Ì…dEØàvd>“•¹ÿmF£¦äàÁ¡ç» s€ÎvÙC*•â pr³-iö•e»ûÙ‘lØŽ,ÉÁeÛRÒí_‡0tŸ+Ùåóyd³YLNNòÎÈÁÁÁÁ±Ž\.‡ññq<õÔS®å)ôŽA>tÄÁ-Ëá˜û;|¯'j+Œë‹/òçNtñ’'{Å@n`9ê×3¯zÏ¡-:caUö[ñiéZb°»]¤œ²‰meÎmqnáD¼ŸqüÒ +V_iƒAu“8ï‚X¨JrGè*’ÎwìðÞ-øÍG‰%V&'} ·õÆ!ŠBí!{ ²ƒD¶hqŒõ¦1Ú›F&GL”n=Õn²CÍo";4:¼ßN²ƒ(BPUYYg+Ø!;€‰ì€ædO/a%Ž¾ÚøH±¢·fÎ0€JÅ–ª+•0»°€Ùż½¸ˆÙ…\+.ìW©(õ˜·Kv¸²¼Œ¢¡{Öb‰SwèÕé¹PªxBv€ÅRH{!;èÿ^¡€›¥úJ7Ke\/"]ÕéÜ*|ˆÈ¶ê…4‘Ъnt§ÍÚd£úAB–1£GU×ÿÒ1 iM«Uup£=\:Ì_2 ¼±t7õ2ÌÍ7ð¬õ]½Œ· Ö¤§k‹’88ÂŒu5š0Å·Lkìã‰!LžÀ°šj9{¿HŽºw…« Koù ‡u€…þ‡ ÏIÎýŠ”ŠƒÇí奩/3§ádk8¹ÙÖRÝ¡nû˜èÐv5?¦oê[ÜÿYú’W>ŸÇÉ“'ñÄOðÉÁÁÁÁ©©)ŒŒŒ`ffƽy+ó+ö}ãáZs’SÑI|›'ka`dd©ãû5£p‹ôÐi{åŽWqàñ¥¢;/¬Ðíà–ôòõÆbv½sŠ8 ðä|Txàh ‰þ~ö(ª£ò`÷2zNxà_€!Sw  —aœÿ^Ëù¨‚ˆc™ÝøèàÈúgŠ$`[Ÿ†nMY™¨6øßDvHHvwuãŽþAŒ$»—¤*gdbÎÙÁr‚nðªê¡DDUWX‡{ ¶õ+kÎhAÝ¡A©l’ “¢¢›Î Æ ê°–ü¥%äÊ%PÐõ )(Ë\Z\„nqË: DÙÞ^ZôtLÙ“J¡“¬Cð(éŠû±œKzÈ!;èoRŠÅbÓçnKöM‡™ì°R).À‹Ãû°w¸¾U;”4';´Úž)TÔÿJÅ•?A¼u¸Øm?\<Ì¥¸\Mt¨ƒ’iàz¹ØB¢Avàê^Ø ì6x›Ø[Ï’µq˜´ß7}‘¬IÓ㿱Dë·ˆ¬‘®¾þŠ'K‹üÕ·¿ÊNZ Ý»Vä"=p%‡¶›’_^T'9„ÂT•±…7™S;¹…´àHá¡÷ùÛ¯Ne#(ÛNó#kïë[þ$ÄýÿÒµüž|òILLL —ËñŽÉÁÁÁÑ¡8sæ xàäóy÷^ ìz âð§#6ï‡Þ`[Š.tícNÎ Îá„PN—Þ HÀð“öž×3¯zÏ¡-º;…u¤0U¾ÂFí°.Ø¡N –¸ÂChÑÐa«`MØÒƒ{>{,Œmà  “Éôä©Z=L„½ã§TWþÅ.TAÄÄöCO ¬Ö—Tp[¯I¬U"ØLvèUUêéÅh_1­Šxà?Ù¡~fVd‹25uˆ&‡Ð“|4M#ä—•¶‘à@#äWäHõý¢ ™sã Öp¤ðn¢æ@}ì^Ç­Ÿ·:û9­Ø†îƒôþÿܹ1ûìÙ³Èf³œôÀÁÁÁÑaÈår8vìN:å^¦¢yôËî‰È¼Ï÷|õ‹¾ò\áÁ[8!”Ó¥K> ?iïi=óªçñÚ¢·§°$ÁNx ¥ë!jàìŠì” €HIö5ÿ%®òF(q=QaÞòã _HŸÉd²akNxðýU)4 .‘†Ô8ßu5Š•è·÷Å‘ÒÖn='uÉý1 ã}ýØß݃îÕCþ‘È­rݲz‹¬±Ù‡Íd²ú–ùZ¦[ÉÔ²}Z@”&Mš´ Z,//ãÚµk¡ÅI 'a¼ùùnSnÞ*Ño|ò"‡xg¤ªüf IDATånßÌÞ^ybmÄ;?i²=§®=Dë,1gÙ=²w·%+X´,ËLuÚ4‡ÊCm㤥ϦÙß÷Ñûž·JK"g5u·%Ià!¦}¨Ž¥EáÁùé·}… vÌ,à“¯"§h€¼®âô\ºzˆ%xÂÝÀù#sx¡<…œªª*Œ€‚œC ;¿ø»uhŸx^°C_ >h‚š¥ txŽ õx$,aÀv\¸·ÝûÀ®´Û€e3݆NØÔÅ“Cå†ÑN6m‡[ÙAe8À¶kY¨¶[•ôÁC{”RÔÛvüÎNÛhY!âŸØèš–å0b„ E¹aÔó qý#H ìÀ3‰‡ÝnéˆtB$ì ܸ#a‡HÓÐUÈ|hP‘Wôl´/žÐÃû¯üÞ9úÚØÑÇ=p)F^”ÂIæd×ÖÖdÇä4ž}­ßMSgš@¡â -͓ú.–³¤È<<¬,'¨‹fvÜ ©=ì·þÛ­š!ÂÎsî¸6žM-2Ž_C„=§ýFÚêEÛÂÂÂ4€%}³Eìܾ9t0e¯—ŸÁ„œ<’G)ß} ¦f˜Ïåñæì<æóùžCüQÁýŸ à÷¸Žvè‰U! šbš †ìÌ•°ÃØ0' Ç¥°‡ith·›]ÑÁ/-–XBPÐø ijðéEÂñX?ðÐÛîâ\yô¶ ;­‹ìöE%CX†x›Í°}`VûH쨸tâð‡g5”¤ÿø÷¿*ôÀ£AÊg‚¸¹ º³PgDaJÈ!=IKÈA¶Cö|íÜf"¼íúõëìóKé´}"òñ3‰¨“ØF€Œ¦û2´×Ð ¡xwíÚ5,//˘¥I“&-åvãÆ ,//ãÚµk¡ÅI*ç ¿ö» ·&Ç2÷‹0Ÿ§e;Â>‚§Èõ¿ñì³èÞ1÷ÊmrH¬œ¥ë“å¬É~@œZ;”#¼n*3lÝ•;Φ¶qÒÒgFýýÝñVyùbú………T}‰ ‡ö;¢:&ºÂƒ{ëO| vÌ,àWæOï7t‚¥¹"òF÷mõÏ`SUñÊô ^(W *D<Øx… ;xL¾ÃÂ)*ˆ¡C1MMë)Ïô' v`‹7èGZÝ·ìSpœŽšƒe‡¾# ¹Íå‚ÇÛ§î )ÁÓ¹·»ZþŽ 6h^ê­$Ô€à{o’ôÞ($›ÍåGן¡ÃdP[‘s]ÆUd Ü•ŽÀ°ÃÁóy3ï 2t…¯hNåËPú8vü! Ûu±Õn`ÇjÁrGÏÇ.¥¨Û6¶Z ¡Ñ¯ÅcŸÚ3p€Öÿìë… <ð[4Àƒ=è_1 ,U*˜/ä‘×4ä5 %CÇB±ˆSå¾â³ ;°ŽpaÀq@t̼„‘HØí9 ^WÌùÎÞQ}(¨º7À”ìÓsi­áØØ±Zál™\;V OZ ´úRZg%Í@NÔ Omö2l¹4¢àˆ‘CE7QÔ ˜ûp|(pRÚ< ‡ ³ç°öÚo =ð¨;ÀœÌ†Šé¾ÍÍé@ͧr2é ‚²”/©â^Ê ö[ÀxnãºÉ¶t:‚v4ÞG"K;²¨“pIËC{ýw {+´(¿þõ¯ceeÕjUvfiÒ¤IÜ.]º„‹/†)œ„þÚïA©œK~½û´ž¥[Â£Ë )Ÿgm, `îÖnBž²O¨óIÀ!®ANº.ËÙ{Ž2çØsníÄT¾Úue¦¸MáPxØKHáÁn¶ ߦÃSx„¾œ^ÒÄoŽÀêî­?[Üáÿú‰W1¥ug_uý™îÝã:XœlågrÈòu{rHSR´ÃO˜ƒHàÁÛÖÖÖ˜Ã(Ó¯„Ø–’OãŽ:Éþ3BÍÆ—¶zîo@=÷nh±_½zËËËòFfiÒ¤IÔªÕ*VVVpåÊ•ÐâTæ¾ýµßq›lLƒTsà\†DŸ…ãölžµ±´1ö[­MYpqŽrcsj]Ï^ã%EöCôý$þw“Ð}3™±˜M1@´û>áNü*Ôu ßøîû>ø¼öÛiª ‚ù\ç°KX°ßáô1`‡a÷|%D×;jº¢(Cü v }ˆ;ç †C&¾‘t)9OÈa0ÛuÇhK~.‘Ѿr6áQ°LááÓz6ã ½Alc±qc3Ûò¡ÓÆà­¼-+à¨ìa$ì }8©Ò;$}ˆ?¶8H:Ú.Ç,Z¿°ƒøãN—QSЦcGÛ&»^°íZíÀ`@ê$¯j8bä1käQÖ äU Q~òª†‚ª¡¢˜6r(j:4E©h˜í¿E7KÊa› oèáçã‡xÔ`Nw~¼ê‚·¼ZOAë\€erÈòerHWóHO;¤ ö/Ö––0== i½Æs> ü=@{Êœš<è„Kü Ü´×ÊìçBóæâÅ‹xï½÷d—&Mš´„×§OŸv¨œƒþÚï‚NE?ÁuGû9Å{ÄÕ<Û9 ba'UøŒKáaûCYpaŒRÉAŽÇ~®K%a˜³€Êv9µw·R™Oh—–™Š,i…‡½ÇñŽeAÚxV˜c„[†TÍ {I=$ð Mä†`ç èTîÍ?âþï-œENQ¡k Né]$5??=‹‚ÖQ|ðR[ðy0zQVu‡!°ƒŸ_;(DÓALÄ4UE?ÄáëÁ,µÊô¸ r8Pr £n)ïnúÍì « TetÌ›Í&¤Åc9­4hÙ÷F€ŒƒñÙ4H“°ƒ„8ê‹d£mǦî@"Žß# ;ˆ1îÄ”F#jØÁÃöl+su¢GŒ< šŽ²n üäT ¦ªA%Ï^qT4§Ìr 3iÝ…èaZËÅ=|ôÓŸáO¿õ¿ãÿø§ÿ„}ÕP>ÍwFÍM õ4Æ RÀf(_qZ¦ ‡˜¿|MpHYEíÜf"Õ¼ x˜~u¼œi5@‡X\ )­õÕ¿åᄿÙ7¾ñ ¬¬¬ÈN.Mš4i ­/–——Q«ÕÂÙ–/üôW~P ñÌ÷ÝÿÎú¾7t×ÅË ©œc#>›žžÆÒû%£´µ) gœ€ƒSëºl¼‡sTñǘùd̲ŸÐ®-3yÒ< ÍÚ6š!킚+‡±­8Ç£PÓð}äˆ <,§¥N4Ù,#5!¿Yr”Gqo¯ íìÃìK3Ïá˜QÁÒ\±wÐÑt¼2}š2xʾû?ñ:#>BÝaîb(´ * š(Š7|1RÂcRöüwúažcNÄïÔpœÖtè˜ 3Âü€ÏÏ °äôÑ·øÛ®‹Íf#’±ä‡ÂÃGµÎó,P´–ã aÛh».4… §j‡P×@¨½oÙq«;ìÃC@˜ƒˆ¢lÂè„§Ÿ¸1Áˆ v‰> QÀQqHØ!‚¼IØAÔ±­åŸwmJ±m·Ðrm´© ·ËÑ¢¢cZÏÁ Êȼ4EMï9üŸúùhßfõ<>kׇ~F! F%Õà¿D/¯8êCàОò:€–üM¬ïw ÍöÏ¿úw/£ÕlâÖ_ü ý´ós`ÖÓ;ì+‡ÊóÑûÞ=¢F%{‹>šæÆšp¡ÑŒæ+sûŒôWÝù„9Œ¼íöíÛìóKnž¯]¥q|¥¢ö'š K4²(Õ3¿R\„óÉ7¹¿é¶«W¯VWWeg—&Mš´˜,TØAÍC[úM(ó_Lx¾—ëòÑ®‹›R8ÉÕŽ¥ñÙùóç±±±ÁÖœšO@Ì9YxAú”<.ÇáÔº/¯—)…Sp¶ÆV’íMü Yþ›m*tÒ$w´É¦¾÷è1rSSr@H‘é…<{óÙ¾räEÏgvi úî³z:-u"‡ˆ÷8":å‚É™Û ¸×¹‚5 xkæ9À‰#9ê³ó¹–JåØáèº'£®_Â…:ç}—ÂÝ?5Þý\QTBú5¼¡B@Tµ£â |Žv Ãa‡€ðw¸4ÃdH°½!pÄtÚÃ8Æ«VÕ1/BÕ¼,Ju‡àa7ED+¥Ÿ5›hwžl;@ݲQSÌårЕÞÃ%]ïrÝ6#T?4U@Á):¡i ãCŒ°ƒ(é„‘bÈ‹„ŸÇ;Dÿ\ÂüiXn0à¡îÚø¬Ýèºmϵ°×²pÌ(  è#ó²g[¨èfæêdFË¡å:ض½eŒKª£è­êØ!Îu€Àz¸ûñ³¿0˜|k´Î~Û)?m±ìÝQÏj.Ý…ÊNÀ/ 8¤¤iÐÌU• ¡—ÂCn.xÛ’C<Ž¥rè7娗AJK°?ø‡z&Mš´®+„ôW{ÿ°zDBp[ü¼(ås`½öL*<ðÛùóçqíÚ5¶¦µ}˜zIžŸj›49§ÒuÙx™É~#>m?‘u1‘YNÇ÷'ĘeªwîböÜÙL6ÅÏ~ú!nýéwQ½sõÍ'˜ù%,}ù‹Xúò—R¯©¥E<øax€°-àEõû¶´°°0ýé§ŸV…ßóÈ5[XX8 @H$Ë*- åóÓos¿ÌÿÅÙN^ †Š©ü³C½ó¹<Δ+Á`‡þ3â!ÂmÇE½m£Ö°°µ×D­ÑÂN³fÛ?­6ªÖþó6êmçÙäî¤TŠa€˜& iü°‚ÃýHÈÄÁÔlh·€V»óûذ8øhã³èpUwÈëÚ@ÿð²(‡þÃýAìVÌ’ac-û`‡ž±Ôuñ¸Ñí«,­¯NlÇÑcâ?xø"¨*Èwò@ØqÐ1Ú\6II8‡ÝéˆþœØ!¡çQµ‘Å.aþä‚%hS:vèY ´€zÄG›Ž ‡º©*¯À{m£ˆ“f%Õ€©h0ít8a–² ;Ć@éMôzx{jü÷ Õ[߇ýÑ?Ü'mßg̜̙ˆËÆÝþ gÁ&(_YÒ ç+Ž;siÆòG†bmk‡‡ÙÙÿ¥É™IàÁÛ¸€‡éW0òð}dÍ/Âv=4ê$ûÔt#w)‚DIŠ‹Ðÿ R<J’W¯^ÅÊÊŠìðÒ¤I“ñš",ØNÂxó?㺙_ìerJ×çžn§'/<í¨V«I•NãÙwѽ»\bƒR_+‰XÎÒuYÎ"ÌQ{^jíÈºÈØRKØLq$Mr'˜“Ù}ô8ÖlµëXÒ¹õ§ßÅ¿øÏ~xõͨôÙÍñ¯þéâÿúÝoÀª×'«{îÜó}&0ðz¹¿Iàa€H‡6¶@|Ÿ+ìç+ǰ˜+C!ÏÏÿÞ¦úË#6ØhX6žÖ[ØmZhZ\¿ÃF}ñÛ.Eò;`D« ‡DÓ@LŠn7·†c‡0Z@)íç}t¸ °Ã(_¼ÂÅ ;@­}ÈÁ±):°ÂãžM$~ÿ YÝ!(ìt€‡‘ãëb³Ý‚êEé¯]; Ûí¶/ìp8üPŠZ»Ýó·iÓì)ÿ·¬]¿_nhc›| ÕÚNä‡÷ìpýØ~Œ€B.BPvK¡"5°‰©ÝÇ¡î@"_Âã¥cšw žÚ­À ¬ Š»è³ ÇNUy±XAÕqÂ,a)WÁR®‚f Íä'åÁ? _èᣯ¤³ysz À)Ÿ‰©ÐíOB‚‘BY[ˆ÷™ƒÒÜîM,ƪ¢Ï ‡®|ñ(<œ>}Zî¯û¬Z­2J|¦îÀ¹g­ßŽŒ:É>å“v,]=äûÜõA­íÍ¿åè[¡¸ ¡iÒ¤I‹Î„”¹/@õoj!<…€R¶F÷t;½û R>ËÕ®¥±Ͼ‹¶6'¨„<öîY½;C´r–®ËrД »º m>P/Úíe¦Ošìª%µ;ÙüàGXÿæÿ24Ïÿê¿ýïR›¿ù—9”¹,ÿs‘®1%rv%ð0á&dp)P5'Îtµ±ÆÎTT¼5ó๙gù ;ôþ­cçá IDATëÿB—¢Öh¡Ñv‰ûø€º~Sl¢bÛlEéM{ìàDw>ì0ø!ÿp‚Á”¶ ÒêSràKzôSÂêãˆz ˜é(a‡œ®BUF§Pm·#WŠŒ*ë›â¿Xj9¶ÛA7ú¾rcÛ#ôµ…mÃÐTyÐ_”iV`‡‘ù$!Ä‘‚4B+¯1?#:ìFß CÆÁ”ÂÒÆo'A‹¶îXLiÔÝ`ŸoŽ‚­]d¦’Lšhe­Ä®š1èaý›ÿóÈÏ<øáÌå{hÓjl }n•…]Þ¦b¿#§ÓÉjŽH”Ý€{ŸOÝá—Ž,Â$*Ц†©|çpsÅ0À$2Ø¡m;Øn´{3ÊÅÐA PPPìÔÛ·²ûB¾gßÓ ;p/*wh·V ĦäÀ–ÖdÀ„e3d¥ºÃñ¨<|*¸|ÖV«|œï/¦ 3‰mjßJcxëÓTEô!}!|°C¨i„U§ã¦“4ìd6 ½Ýevêã§±cµµI7"]JaSWÖIÚM”²Š z ­ÐÖsxR~>Þzioƒ6#ñ›½bQqÈÒ7(]y’C*’ʦŠFB=Öd—9—êÞvûömöùeú•Áú‹¬aĵ€j±¸A!€ݦ{ Ú›ÿ) åÇvíë_ÿ:VWWå Mš4i!XµZÅ… B´3ï@]úMá—‚&Á#K›C R‘ qpN÷îd¬$äà&]—åœ*#æ{X;u4¡Ý_fJ¸¤‰ÁÞ¦÷=Ž-«Ö^´gáªwù$ÐgüàG©l©Åyv%4‡Nþ˜¨Ù=†:‘ÀCt&$ð`—–„ñ޽Øì‡Ÿ+šŸ+Í83_ì .š†s•™Ä`Ç¥¨·{oP;ôüAUA Š®ŠÒ7ÏRÔ[Ö€¯Qúž$Ã!°CÀé;ÄOõ‡a—Xb_Í­6`Y€ëz‡!ãæi2`(Ú@?ñ]ì´¢Wxx±’-à¡aÛ=CúKp‹ê±Ò¡î °‘°C6`‡@ù$1”UŠ`‡¨ÓAy€Ž¨—¨` ;ˆ2ö$”FñáRh¬P°¯íG§5°km×™Ð:!c¬ºòA'¬Ä=X}—}Q8˜3 ,Ô­§ñ¯Ùb²d€ÍP¾29d¶ØòÅ£ð oãÌùô¹#£tˆ¼ÛG¨æ@£I›¡ÿû ÅSc»zéÒ%yˆQš4iÒÆ´jµŠååelllŒ‘š‡væ(ó_HÇ{ ßuzÔ€ìì;zëEá¸={}}ÕjUvvãÙÑ̵֓»xßL’IÈA:›¥fL9žôE6å'3ázp<#¹ãì{‹ì€·¾óÝÌwÁÂÜ\èqÚ⩎–ÀCt¶$¢S"u÷ŸºÃ¿3Û¹¹q¾œÐ^™ž…¦ôé%Ä;{-«ç€8 zX~t ÚþqÏô,Ç…åºa‡ž’ð¼ÕZŽƒ­f[Í&jí¬u‰Þ„=ãáo™ô”+;ì0ÔçPͶ3\Í!&ØAS‚ÂvP AѦîÐt4;ò±¥¤ëÌaÖ77…L,—ífæœúìä´§Âa_jP@×Ôd X¾·ÀB‚â€Æõ#Œ| +°ÃˆÃÈAê‚yî“NàðAŸ3®–°CæÓô¶•‚ʶv)1|ž²,ìd½‹e¢æƒÀcùô@w²ª<Ÿ\5ÕN3žöé­â…@3˜½3”ˆ’CÖšàøù¢;Ÿ0‡Y^^–ÛL[[[cŸöróéèT!šd‡HÀ¥tAƒíoÚk¿åè[cÅS«Õ°¼¼,1J“&MÚvᬯ¯‰š‡þêßvˆ}JÏÀ:ÝWÍ!K Ã`[#…ç˜c“€$Ÿq©Txðžä9ñxÊ9îKg¥õ¼ë”3©¼ÄMãÁäÖ•Tu¾,‰Á~ûÿÞãÇñfÝq#‹ûÁƒ«6…|j[®Îè;ݺ5ô¹kL ›×………ó¢×‡¢©øeQ}xpïÿ9ÐØbw*WÆ©\ªB°0eBSœ)OG;=³Ö²8î³™q(ìpÆ!è „Œ€:ÿè@tð3# …ºòìRàþî.îîî`«ÕÄÓV›6¶·±ÙhÀ¥ ÛFݶQwl¸ÂÁ¡(퀭֡šÃˆ(B€†Ôû@žÇTÇHv€rÞüÙ]ËŠe|yƒƒ®¼‚±(V1üëÄP£šövî¾ !€™$ð •ð!k°‰¸.RT^Y6:b ”Êb´Ÿ„ÓhùBžƒýkF3‡«ÿfj[ïôâb¨ñY¥%‘³{ZôúÀÄU¼(„}üW¸/Nu&Š£S[ËÏV¦QІøá?ìBLÝ¡e;Á’#Tʾ¢ˆò,Èh\—¢ZoÁÞ?´4: ;€K)îï0«?\µÕÂ'µìîâÁÞ.ìîá“Z ›Þ‰KØRÀ¶;Š–Õ§æ@ü«+”³Q„©%j# ¿¿; é}\ÀËSìãÜ'ân’t%xW £çó^j$ô3€#"Tˆï^&¯k˜h“°ÃdÁqä5”:'ѧ±_VOÛMWHvp»`¯óJt šÞ/ñ 3 vè¶Z£Jý@ âwîÞ';û7+$ú?³Ónc·ÿfæ¾xålØa(èÐWî#â ˜ôè§dtÔºBB\°Lå æpMljmœy±Â¡ò°)®ÊÃÓÞŽeägÌÐbœú‡ÈIèšU!Éh’›E6«‰ûàw°š°ÕQ\°Ã¸éd Ü•C>ž¶{¥ë¶%a‡a3»„ÄßCX¡iDÁ=‡ãf ÇÎϼžÇ´–ƒÆ¹u7†A“™ºèd$¢¦1¢|“„*Ï‹SÇ@{;Ø:,r%‡,™‡ŠƒTr6©Ì)9t4|5?c"Õ¼ x0çJÄ]J¸ ~RCùrèO[9ö%(G¿4VL+++¨V«r`&Mš´!V­VqáÂ…ñ"abŸb2°f÷²¶_ä3R8É|ƒv­V“Ч½ñÆìµÜÚ«eU 4å}YשtV6e<{7FÇ/g¥|ŽÝ…öf¶ËWf*eÉöö…‡ê»±‹ððà?‚Uoþü‰Ï¿™ê­8T€íáå#Ê9n;-z}Hà!x0¦„ðÃݸΦ¢ø¹ÒtUÁÑBK¥ÊØ¡÷o]ÿ*ìà:8ÓKˆ®ƒ: Ï£ö<°C÷¿¶›^ñŽñ¨;LåM®ƒêMÇŽmœyqŠ}¼[x0UÇ O¥‡‚®áh>?0~À´¬Í!œvIü“Pwï9¤…¾)Nìu:Œñ+$*`@Â,Ï%ìÍ<¢÷Ì×$Ѽ(D¾¼ÆO‡d#/i…,!è”н{€Ûön?‘™"˜à 9d-oJ*sí0f‡¡®<ý s uìF$‘ågoÌÍ1‡¹ñDl*\W/p¬PÀ|>ù| …Ž˜9OØ¥ÍqlC~Lé]ûï[NWa„^ôšK)ê¶uøcõŒ“ìñ&Þ‡!‡« îc#/aåÙ„`ÆÈõ„ïù7w]Å;ÄwÄÏ£„ê¢í:¨Ûö, {vïOÛu`‡9> öB€ìC=$ñ¼x*]Ð;¿ÛÏê—y$䂤$ä“Ù{ÌAΟ?iƒÆ¥ðà ®]»ÆÁ(ØAª9„° ÉÒ~$Ä|ôE¥”Ï2G!>ãÙ‡Ñúdú¿<'.ÇÙÔº.o<Å äÐmÔ¯úÝk?É^Yg®e½,ý"ZP æw=ŽÅó6ƒCPûìæ‡?›vu˜Z NpŒiaó»°°°,r}hrÖÝN‹ê˜UJÀ¡÷ÿœ+ÜÏg¡€àÍ…YõgÍ6IØ P”Î Å.¥U­£è0xàÝvðˆÖ N´mš¡`,ØtåÁ' ¨`vèùv°í޲ÃÈÓúbÖ×54-Ç#œx°C^ç›6옟çÕë¸U«q©CÄiº 8шÿ犆~Øæ8¶ c­ý Цj™¸”âq½Ž]ËêËû˜¦á¹biðFù Øã‰åÈÃÕ„¤v¡<Ç*Âò3ØfJšǥЉâ?¶ ;øúK¤±‡ ;Ø®‹–ë í:°\'pù˜ª SÑ`ªjD})™ñ3¯êس­Dó¢µ=µ ãWæïÄòÁ¹æSô-ý/?ÿË8_<Š‹ùÇáŽe…〚¯Œí&hýaÇ?Ùƒå‹f4_™KŽf´ Š›/ºó s˜ééiH4žƒ[½ÀM°{QQ:KnIÈa˜+êÙwAü_€îñÝ xéÒ%y¨Qš4iÒú¬Z­bee…?‚a°CìSKÊ×ï4Ë{KT¤|ÀÿÉ"ùŒKiÏ®ÇÓ¶äÙp9¾¦ÖuÙxã)âøË™˜³€š†CØnÔÞé,—ýOfJÀwâJî8Ü:›B\íÎ]”ŽMßžéÎ]Ô7ƒCHó¯¼4™CìÎýáÚ1ªš NKD÷Ïvc ·er 1ðÒ­[Ìa^ÈO£¢™Xš.áx—ÄM/ìйAµû"Õ¨a‡ƒ`šªºb=°ƒWè¡J }¾|Æ÷¼Éx°Ð9 ìn4ìPд¡– ˆ­`ÛL°ÏãÑ v]U`jjfa°iü ]•‡õM±UX¬¤ë¾ÏT…Wå±µ)ƒjK9ªÞÅ–ãàãZ Ûí¶'ì ËÆÆÎ¶ï󉨼Š;õQØDœF¶a‡3‰Š‚ª…;DX×ÃÆÀÈÒ' åÍêb϶°Ùªc«ÝÀžÝî…5QЦc£f5±Ùª£áØ™™‡ šÎ¤À…5™µ,(;ˆ²Fe‡Ñ—a}åèëøÃ³¿®›•瓯?¿„šO€ÖÓ"ÎÚ· *YSrHHÅf*±ËŽŠ§äàçrã3æ0ËËË6hÕj•}6,-!5:ê‚k‘»AÿdŠR ®­í•¿Ù9(Âaëëë¸|ù² ¤I“&­{ﺲ‚Z­Æ^[úÍ^ØAª9Œ=&??G‘Áx¢R '¹ÖÒØxpkF×ïåEør|M­ë²ñÆSÄÑ«8Œ2RäPyh=Iw¹grLÌrY2æÑ˜eN!.…»Ù 5¾ÍŸÞ üÙŸ;½ë¬o=¥gò¦—Nqüh¨ËÎvy+r}H…‡ ©pGà6¶FÒK^öba QðË/œx¶øÙÿO÷þ^å€g¿D ;€ŠXͶǧ<þM†|ÆçLÉHH€;ŽB†$2mšØ³,ÏPAâ~v°:øÉ|fuÇl„ãÀ2ù‡aà=zU6ul9.hþ2;À®eÅ>期c¾s÷~ý…&b²*›:Z¶ÃÐƇrº:v[ê·O÷ö†ƒ û,ÇÅãF …âäm^Ó;q!OYHØ!œøC=ÌO"ªë!i¤vWÝáthŽ‚Óp)ÅŽÕBñ0¥›ƒÊIoœ7ãÝD­ÝŒ0aÓ0A®_5#KêY˜?n¿¡¤ã¡ô°rôuM锟ºMÐúC-P…"ã*R¢;%ÉÑŒ6Áç«É<,--Aš·qÝb¯åêj) Äê’TsêJÏ÷ æ´Wþ&ìÿ#®Ôß{ï=¬¬¬Hui¬Z­buukkk\ YšìüùóXYYÁùóçeÅO­­­áÚµkÜáÕç¾ eþ M-”3»?¡ÉD¥æAŒ# í-æ¾ Aj6ã*/§nCçÃåØšZ×eã§xÅ*g¥|ÎöÏØ²ÒÞЧe_œÔ þŽ\É€‹2…©Ý¹OŽ7Ôøü ø{ÖŸóð÷ÆÖSÔ·ž¢ÀqyqÒÖ m6k4ðàäAß½#b–…~"‡ðMÈÙÕ.%ÿ…}ü¯™Ã˜ŠŠŸ+Îa±X‚JzÁ…Ø!,e‡{ö’@Ñuh (:êmËãÐû`úžq†óðiˆ†® ÷{˜ÚB׳¼¦¡¨ë¨J'^ÞxÇi( *†Qäá…(í(:8ÃÑ›!ª¡\Kþ,6U!(™vš‚Òi’²/?Ž«Þd óQ­†Oëu,ð,VRf„LåMTë-î6ϲÐT•¼jv- -Ç ìÃv«?ð0ñÆpxŸŒ±¯ä<¼ßrÔ.¥È« ª6^:vˆ©œB˜}Ç*&v ”¢îØØ³ÛQ½ êì[\[­&fŒ4E‰$ÐÚɈ6i**ʺ‰«Yyù®át³wÍØ!.e‡,À!îs¸óâ=ÜnÖð»ß¿w>™yUÜö@]н{0ƒ¨ijá˜üB'%ãÍh5¥;_tç6sž[E'ÅXoj&¥Å˜»™€C,nÑ„£Lè0l•¦L½ õÄ¿çÁw¹úÆåË—ñÞ{ïÉBÚP{ÿý÷Ǿù>Mvýúu\¹rï¼ó._¾,¡  ±K—.q‡UfÞ€zò«rJ¤’C˜Q‘ò9Ð'ÿ/S˜7nHàæ¦¦˜æIºwwüF ωËq5µîËÆO‹[Î\ ÀŒlâ—©”¬¯Iî8s˜fmv³-gFž³0Óùìfp•ªùW^:Los3tø"NÓ yXõFðÖàRx.°÷1¡Ò¶·EtJ ºu‹9Ì ùiä Ÿ;Ñ‘þ v¥˜0ì!!Ñ5Ý8L#oh0uµ/¸¿LD?ì «ÊÀ-¶£`]U¡{óW©èòÌ#Þ…B†ª?kß÷G•/)ìÐnÇ;Ö§d<ØáÀòº†‚¡ò5jØàHÑL-ì/NM¡x¨>Üþï‡31å4uäg UA9§#ÜÖ¶¿èŸ4UÁL!üEzkØíâ>í½nǨ62ñê‡÷ ÇMý‡÷]Jqwo{;ø¬ÙÀ“V÷övqk§Š]¿62î!þ0êKØA¨÷)$šr”°Ãà^Âuñ¤ÝˆvxÅÓv¶ë&×FCº?¯jø€ØòRÐt˜Šš‚ò}ŒIyY‰RïoÞ[ü2Þ9úÚøÉú/A7M¾ÂRO·› õ‡>‘#cg_ =SY¤ñÕ³yÐlµÅìì·Á¬äËÞc"o¡ö6>u‡b¸í“ÿ1tž!CDÜiÇe’ãD_Ú\!öfN}þ?à:0—/_ÎümýÒÆ³ÕÕU|ík_›ءۮ^½Š••Ù&¤¯¯¯s…%…“Ðμó­³)¿u| YÚH†˜¢"…“ñ¬Ÿ¥qíÇhk“­³dl;/ð ”>שtV6e<{7FÅ/gRàÚO&j0ñ™Š5Ùp#Æ,s˜ØT\'”x>ûipØ¡07{¨æ°óðaªa˜^\ =NG€óÜ>6µ°° ìÍxÑN‹ê›+Ä<œ4Ë8ž+âè”éÿbŸúSßsu2,¢Óúo€&@)gt0ö‡ z$§ A9g`*ŸÃL1‡r΀©©#a‚RÎðÏ7#ì@(„àd¹„¢®{C }Ò'K%˜ª-ì@éDÀ)™:r†:†^uÀ;誂¹âˆ[•Sbo-°S´ß¹{Y°œ VÉëÚèaü[~MMÅLaÈAÏ,¿T˜h8• ãárðÁ{;ht«ƒÐƒgÀƒú¶­6[:qihÛQ«;HØëù8°Cñ±ÕnÀ ²˜õâ2ŠªÕ¥4²4Â͇ÛÏ«Ž¹Þ¹9¢u9UCI3RP^¬ièÇÉ8^ž ;DX¾ÛŠÕ³_ zp?þV8ÐCdß·Q õh×=2ÈrÈR¾âÈP¬ßófìKånÈ!£}úcæPòöioã9ÈMŠ‹v7ÜǺŠ ßLQ —„XWgÚÙw¹¼;Py&ÍËVWWqñâʼn.ƒk×®uó¿´tØ8J7Ú™w5Õð¬ÈÍ"ä@…Š ÀCœÆ£¸G›OF7yF<{éz¶Ê9Uí!€ÃÀüdÎjž-ÛµwÄ®LŽ•Y.ËpóÈ£ò°ûèq,9u¬p.†eQw8ñù7;?…Ýlþ½û÷ÌÓ­¡Ï­Ò’Èî {»’&¤¢“Vx ;÷»Áî߬,`:gô)7ìÿ‚þÃôèú›¿ºCÿ¡[&ØÁ0íGÐ*ùtUð¯;ŒB 93E¦öìà“¡©(ç Ì–ò(ç 7´ëªŠébš4£a‡ƒ_Bp¢X‰R eÀBåLUÅ|>¥r9:ØÁq&v8ø¿JÎ@ÉÔ9|ôª6Ø¡hj™:œþ¥ãì Êj5Üš°Ûªòº†©¼Ñ7FŒ ;ESÇT„íIá¸-» éѨ„øfwÏm¡@ƒzÜlÀê¾5Þó3õÞÃÝtL§b˾ IDATâP]™?‰‚bŒ;âçãÀ»V;V+±qÇ¥;ݪ)„L% *º‰Y3’nÀTUèJïϸ°iAÓQÑÍ”—´4¶á±Ózˆô;·ÞˆéÞ}Àmg¤Ñ ~_cf$äBv2®”2F¾¤Âƒ·Ý¾}›=«ÂCÚ!*n»/Êlª9ø†/.B=õW¹ÂJ•i^&a‡gvåʬ®®Ê‚Èp[ߨØà «.þ5®Š'bM?pÈÊZ^,5/SÊg™ÃðªLºqõ;CzöÜÒõŒ—sªÚC:!‡Á=,‡ÊC뉸Í>s -ëe~B< Õ;wcÉ­ðPÛ~ðüË/¡Y«¡Yí=–ÚDÜ6µÄ±ï<b\bïcË¢:¦AZ˜&ä·Jvþèèa|ëÜµŽ Ã>˜@ÊÏyäÈY1!Э¿dsÊ,ã¸QÂé¹ý/˜Àˆv0 <®èy.¥°ÎþI²Ÿ¾®*ÃLd@:ðƒ±¯ö`».(4Eéø2øqŸ|‡ös (h û !@ݶÿÞwT°ÃþD+:ìÀ½ÈñûK׃‚¡ASÔ­C`!JAW”sz&Tzå}y,VûÖGá?þÜç&j35sÅvZš–3v{ª”òPµhÛSI7ð÷%ÓÈä¾O,Ș·ƒÞtÏp÷@½aH.vm ͯ¼A!ÁãÖ˨x„âzNâë3‘ÆÎ;l[-4;ñ±§éØ(j:TaY}¶:T‰‚‚¢Š7€Ww,ìÙÖ ²ÅSAE7a(ªXsRhs[ ýQ*;Ä“@ŸbíêÙ¯®>þ`¬¬» 2÷¹„êaDäÔݽRy1m0{cJ¢ÍWlYÊò—äþy£O›Txð6àäæBêvTÈöt•Tt•;wG¨žø¸¿Ç|¤V«auuUÞb/íÐÖÖÖ$ìÐg/^Äùóç%`˜AãUw åsP~I®í‡ºÕµ÷lo2¤½›šŠ£ù¤‰þfC gŸé ;Xýç,}Ír(Wl~JØý9‰á݉Ðt"Š? °Ã5l;úD¸n ÿ xAÕ1·¯1jmfª*ʺ‰9³?ì×-+Y€â2Ña‡Ã¹~ðO‘*=Dv©ÇÍ”v´þ0 é@ÅfTÅÆ›ÍP¾b­¦lÜTwã’¼çÖz’›£ÛMªšCD‰H56Ó P/p½|ù²0¤nܸ .È‚ð™k¹”ƒ¤ kkkkÜêÚÒ_“k{_·¥"›HUMòì·ÑÞ¸qCŒÆ¥ð°wGž—ãh ]—*ñqVßuÍO< âtÌ5º¬—eô ­ (l—°Ú­vc€œοÕ77aÕ>[˜=‚¦Ï;Y«^Ÿœá|çÞè6PZvy+ªcx˜€Šv ÿÛ½Ü[wãz€¶ÑþÅ7àÞüv¨?½Åì÷Ï—æajJò°ƒ®ùÌàâñAÒ÷Ø¡+Pd°†À£Ò–&ØÁvày<ú#áÄñÁÁô™‚ ] >d‚fK9˜šù8TÒõÄÆÀ{q‘9Ìžeá{E?Œ4ÜrªÊÝJ5EA%g`¾”ÃTÞ@ÑÔ «ÊÀ©)(š¦ó&æKyTrFìàÌl.ÿ zð±ŠiàT© …DxhRª;`|u‡ ùä?\¿k[ÁüE¹aÌ|ÄåǨψ;„Ñ7†¥!a‡±ü³)EÍn¡ºÿSw,®ú ìOÜc^Dã4AAÕqÄÈc>WÀ´‘ø9š+bJÏ!¯jâÍG¡Ík„­’\^¢†>ûa„/ÊÇŒ¸ùho‹Øá:?™û¾2!È![‰Å˜¬~aΗ/úô/ -ã:¬å¥ð0²“l¿CÒŽ¾J"JÊ= äõ*W9úÈÔKÌá666ðþûïËACŽ™X^^F­V“…áaµZ .\à‚餉i«««\áÔç¾ R8’)?œ›Õ}Jõ“PñÂÉxÖÐn< @”ñÌ´ðöÜÒõ *çTµ‡ìC½óðÐ~’|7ëœð’Î`‰1Ë&•»Ù;ŽêÆÝÀŸ-?wÔq3Õ[æ_f×kôZÏq‘}‚ö¶¨ŽIà!\{CD§¬’÷a_ÚØ‚ûÑw˜ãs7®Ã¾þŽ"DPãP†8W˜Á\Ùì…º'‰®_¸`‡Q;øzO+ì0÷Ðp#°ØØ¿¹74`lØÁ?@x°C° :j3³Å †uX=øÀ*!(f‹9ÌLäu-¶qH#$±1ð­ãÇQä.®~x3Õð@Ú’©©(:f æÀÏTÞDQ×aôÃ8qCfÏOMa¾Ðfó9Ìò8Y.áÅéi,Švˆ~ëÎÁíQõ4Æáz—Rl[íÀ~š~}HØ!P»3¤a‡ ³vhÀ@ß± l¸ž“gý%’øÅªv w›Ûزšxºÿó¨]Çýæ.ڮ˔†K)l7—"z' 0uà'ósÎD«.p¤!€: ³E =Üùã•¿îݨ#@ãÌ¢’C¬Ò r%;rðX)r½wzûí·!-ÄîƒTsˆw\ ¥Tså›zê¯rÅ ‡É6 ;³õõu¬¬¬È‚È€U«U¾qOÍC]øÅ¤6ôâ,uc"þz^Ì­)<Ç5'Hc³ééiöÚÙ»# n¬NE¥ëÒÙ”7e:qCÏ^ÙœÔ<[ · ô@³ÚEYKf'$wœ}_rçnô%|ÀâgéèQ9ÎÖŒ)a}[XX8-¢_x¯‚Ï‹ê›ã£ðàÞúþHíœýA 8軺Ãsfpb& Æ‚†©(Ê!ì@úÒ;€H×öÏhöÁƒáýñ ÷ì$2Øìÿ¯ï3ÀÆaÜCÙƒó› ´Û*Gà¢U†ò aø+OlÁa‡nS‚’ÙQf8R4Q2uŒg·ïš M9¸}_EÑÔ0•70[Ìa¶”CÉÔ¡*ñÃqßúßoo-°/*Õë©Wy£Í ¿±Û÷AW̘¹ŽâC.3‡‚¦G :V‰¶0¶ûcÞô½k[ýPAIÓùòìu:"(;ÐUð—!Lϳ¡ê0®íZmn% Àq\XŽƒ¶ã`³UÇVËûÆ‚6uð°µ —qíçЈ2Î-‰! AçƒXÒˆáHX/T¥²C€ wðO¡@NîÍ?zˆøË<ê€î&ñ…¶à9È!Ƥh¦‹±ìhF!¾ú"ð†èÓŸ@ZxÆuXKª9Ä7æ2E)Õ‚ 2õ2—ÊÃÕ«WåÍõVøÍŸ~ø³ÅcþÀC»žÎõ^dßÒ§£ÏL ¬ð§EtJ¯`À›¢-Ðß?î¾çƒoï¼;÷˜ã=W˜ÌÍÎ"çð?!À¤÷³=ª„t`ÓðXiõÅã/Éà£"1úëÑZÞÉÄî¢4hj=°aHÁ7_„5†€OIò°C¿iŠ‚‚¡¡dvnß?RÌa:ß}û¾¢¡ÃÔÔD ‡n+q(,„i¿þ \á¾õñG“²•äÛ#øFS{Kzs)a„;tÆk¼ °‚Pëûq”4cü|ðö5 ;øƒ¡ÇŸÅÑœOÝ¡å8¨;רãRжmÃr8®‹†m¡Ön‚º.\Ûõ\P<±šLõdÓ′4È÷ïâÕG•úMè!ƒåÖhãqLDª8„–\–òkUI‡îQ8Ð!hŽ¢Z^^–ó¯±d$S¯$= °Mþ±ª9$¥Tsà-@õè[\1¯­­ÉcÂLÂ|våʬ®®Ê‚H±Å«î5‡¬¯íÅ\6…±GáQykvãSy¸+ NÈýפ¸.!‡xŠXB~¦”ϱi tHa²bŒgĘe³÷ø1ìf+rßÜ1Uö6ƒÃFÓ‹§²Ö"Í“VcYȹCNŸ¡™ ViÑ{GÝ¡ÊxðýáЃÅNfÍj9¨û­3tØað×ga1 C`2 v Þêñ=`ïn8*A<"ðŒÛ7/ÃOû“ˆÛ–°—^u@@_pk$¹)çÅ©)¼0Å.µ¾¹‰››È¶Epð-àA¾£í„ ;xµÉ0ýà‚´hRå…pËkâ`‡ì¨;ð”Rl[-®¶NXŽ}5PJñ¤oíï'‘9°öð>‡²CfTâHƒŒ­æ#Dd¥ýÆ™×¢‡xùÝx Ø{Ñ5ôÌ}_cfb-»Œ}{&!ߘ0DíJ…‡Ð,œ›êE€hnI5‡ ®Ä§æÀV.ÊÑ/˜ì_´s––Z“°ÃxvñâE>%!iBÏaneæ<ƒºCÕäÚ^¼ª–8ÉŸdŽùöíÛr `´óçÙ Q§. .íãfê\—C?ÑæÃh»JæÆØ,'-f¥ñµ;‘ûåX6wØÏ~úaàÏ–Ž âP³ˆÊôBž­…îÜV7ùc¢fù´ˆNirúÌv{@aª;ôÄûÑw@Žœ9òbï…‡³…L ®o F«;ô©a„&%§ÏÓÐWU‰Ö‹rôKp|—)L­VÃ7¸n(––«V«¸páÂØ°)ž‚þêß´BÜÍ;¼ì:¬¿ø/Aë÷¸RªÕj¸páÖÖÖøºJKĸ 5¥r.{ë~šñýKØy¢éÌ‹â±Ó§O³×hóÉ„••®Kg3Ô”e9mjÄœm±…´ñ$BvÑ2%ß¹wÖ]¹ãÀîϘÂì>ŠAáñ¬\·Õ7ƒš¹©Jf‡¬éÅE|vóÃÐãµÅÞÑ)©ðž½!¢SVÿ—ævôñÑ Ž?úÀî‚ÒJ–W:N%ÿŒÇ ªî@ÈhØ¡û‡ŸW@ÕžÃ;6Ohvè‡ 2 ;6 4Ø9†€O zvð¶¤à7¤ÊO¢L3»û ;Ä£T@ÔpÊ¡ë3¦ª¢¢‡B0kæÂÍK\ª ã¦#a‡òæ­ìp¯±ƒ§V ÛvwÛp]Êþ8°ìÙVàq‡°lÛvh¹víöð(\ÚõãBu—?´«†1·p]ô1Á°C\£dvz ²6Üçî´–ÃÚÏÿÞ(/¹ú§¥§)^½¶·@_nÓgr¦n›Š13±–]UQþúŠJÅÚ{Ì9“½Mü·‡4È»[ 0E)Õâ(R\1Ùo”³mÕjËËËX__³}1¢ŽkZ¡“5Ïòúú:VVVdãJ‘­­­1‡QfÎggÝïév–×ùéÚºFá€"‡XŒx€SŸ€’É€ú •ÎJƒ‡Ê©´Pö­sm? ¯Ëȱ6Ee™ŽqŽKááÎÝÈý²›üßímïsSK£/ø¢Ž+¿.s)a}[XX8-šOx§b…ýFÉéSxpo¯ ᎎ±$NšeÀñéÎËŨ`‡Ã¹„€ìÄ&ô‘ vð˜Ð††; ü;¥°Ãºƒm‡;–>MìFxøõ^@‘ÃGõ:®ÞÌ ô è¡7Ñýaß”¸1Á €B"Ig!_x=ô=×§ eèûjOBÁQ§#a‡òF|Ÿ[ôÙÞ¥@Óµ¹Òv ”¢å8ÛŽå8½€B÷K›]Á©¤°'ð%8†¢"~“ÊÑç%#°ƒ°i¤´ O ô@=üêzyOi†¾€¡ˆõ‹‰Ø’Š9_±V•„º‡§¨ ‡Þ#ûy£´·ñ¤´”ì?òîALQ&9žô¥r Iø¢)³Ÿc#8f×Ò0®…=\»v —.]’,ÃÖ{/åwãß0 ½ÎO~?ƒjÄ8¤V«¥ ËxöeîÞÝ –DŠß‘¤Êõ ¾‹²ˆ©„¢^cU^b¯šöæøu›¹Æšõ¤ÓSi<ÀƒÝj¡9¦ãÈt\nÐ`룃o¯M3@~›©ìizýݺ5º<ÄUx€Ó¢9$‡ŒVìá©r7®GŸæÆõCU‡ ¶ßòŠUé;Ö6ìÐuLŸhjzðˆ ÿ¨}ØÁ;5ðCþ‰ìç}T8`°íW ¡|Ä3€èBÚÔ€ð %qxUþ·?§õI¸eCÔ— ²&ª¾eü8|l!_ÀR¡‚Y3Šn¢¢›XÈñ|©SUƒï—ƒ€aìË%ì]úQÆ=ä¹Ò÷\õ›#„ é: ¦.áRê©ìp`–ë0¥SuT ”R¸^¬˜ŠÎØöBsì4‹#OƒÈ²Š4”;QCµ#tï@m ™ƒbLŠf(_±VÍ(äÀ×8˜‡0ÎUíÞfúüÒÒ¤…gD-Ä?fH5‡øÆê «9øæ‡ãðˆ²iaÁPóÁ`‡”\¤x Úé¬8®\¹‚ÕÕUÙØR`< àAª9ˆŸQá†qÖªOx`³ÉVÞ“*ÒÙ,5e 8ÄÙž¹S8ÇGYŽé^z ›Çqö—¹ãÌaj1¨<0€ÔqѬհuëc4«ÁaŒÒ±£ÈªQ¯à®›¼°å¶,šCxiï"¢SV©·“¹÷ÿ]ÅtðMɘf •>Ħî@‰ÿ8ŽÑi˜ªŠY3‡…|¡Wõ!´úˆ¹¼xã˜8ØDwðú?j¡CB€y#ÓK¹ bØ)t¥á ZŒ°ƒª(8¢™Lc›¡Ž©îØ"Cj$å%a‡hÓˆzøä[‚-6)`5@ë3ô…KUbO,ÆìH‡þá'ÐgAŠìôéÓæmbÚNRÍ!‚I^ª9¤haß1eêeæ0ׯ_—HÆ,TØáçþ®?ìF€«»¿ÌêÉ_+Ž‹/Jh(£F´Bº–j)ÛêEŸ¸R>ˆ’ÆXóÛÊ>(]Ïn9§ª=HÈ!©ö¬TαÇbïn[ÔmxfÖÉ&›î±xØ}ü8r¿ìf+Ðgv~Š'}„ŸÂ±,Ôîƒ1rS•@Ÿs,K‡}æÂ*8ŸÍ! <„cçÅì}ê}'¾ùmëhc‹{Ê2Ôý¦%ì€èz`ØD;Œ„ºõ÷Ç+œP°Øv¬°a}J$ìµMFâ>Œ£ò°¾¹‰ï=|˜ÒÒOéMÕ>´Ûžì6ðx»ŽÇÛu<Ùm`»ÑBÛq"O_„2ˆ½íÄ}xŸ†˜w–OÂÀq='ñõ—VÑ <_˜Â‹…Ìè9ÆØIhcB{¨Ð—†;dñá l ±Y=ßYÃ̇Bòê RÙA¼¼He‡ˆÓ Ê |¡‡%sjà¸MËAu¯…§õ&lŽÅwZÊ ö¶“Ôá}E ·,$ì^ü¡Âÿ?{ïWrž ~yΩ+.U¼€D·HR“­v«[€g£½+ă"¤ ·ƒˆ­yÐÐÏ9k[މX›œíX²wµkÏJ„ÆË»–šìñ}í0¡K–´ÛØmÉÍnR@¼¢q)Üêv.¹ H P—“ynyNý_DG“¬ÊÌ?ÿü3óÏSùù÷È‘-XÀõû|VH*x‡ï¦\’ F)¦ÈoÚ¡ÏÈø 1^§IÙA¬¤ì¬ØjBz¸þs_@A÷v¶—ôЄàÐêeßÛ÷'.çèõ7õ˜ÿ€ß²;<¡$¹ñbpArˆI(Œ Z<¶‰¥PæBØUF=QTUsˆÐ/MgŽ WAotN%;„º¶… cèßzºÜ¾¾¾Žññq)âàÏ–“¤¼? ¾tÑáI”>,\†!ECm¹ûæ º ÈØDHŠÆ³&‘{óú²Âû|rÎFOšNz}†–}V¸Ìv µÌrÕõul=ZBiaþË-lܽ‡ÊêZSBDiÁ=£÷ø1$…S§‚u Ê áÁ(IÓ6{ŸN2çÞ¥êøÜø2^ü¼\÷øýŽ%\îD¦oçÐ$Ù3Œ6ŠmÔš= hõ­6ö´½`߆ìàò‘…ßrÙ×}pOv·­s¹ÖLÿÈî:Ðd`G]!ëå-Í>Ú!«òð¨\Æwî܉‘דAvàœcu»Šr½ó¾bZŽwÒ‘ùåý½„¯íÙÁ¿ú}½¬ÏüŸ &;D¥îàêpï‚”ÕSè5ÒûÚHé´6„‡”¦#£ëþljß1™„ý Iä®íKö¸7to´ç8¦_þ’/¤¾üã;ßDÅ¡|ëÅ'Aˆ?<ŠƒÇ.‘ŠCãRâê’sd.ãàÖ6þ!¼ Zˆq[(…ÕX”TsˆÐ- Ð /ЂХ¸pá‚ÿd‡8¸Daä‘zñ7=']ÅÍ›7199IA˜ 8ïÅ %&’ƒÚËK´È¹¾ûÝïÒäÄðð°xdÔVºc’éÉñs¬âqˆg–?)Þb+…žTÿªóŒ!Æ …- fô !ÈÀÜ.£ôÁ"6ï?Deu ævÙߣ´Ë— Û¦BÃy3]PÕ´Âàà RbDxðˆÁÁÁaUm³+<ðÊêù@ýÇðÜg^Áçþ×1pzHnKÚ~$UîpÏÎe¨ È` l÷"e'²Cu‡¦J MÊ5j­ÑìÐYÝ¡9%¡µJ…{uY²£<]ì IDATÃN@Ú® ±uºhÖݧDvˆG³Y%ì˜xáÏË©Ø|ãÖ»xX.ÇÀÛ\|c>ütÜĆR¥&D`àX+W÷©@c'êËûó©|Ü»êkÀþrSGÐÊB}dþ·Ad‡ÀâXë°¶g4‡R­/¤5‡wIOÈ:tµís1• f>ª²gvSž²n…úÖxpŠ_7Ýô‹ôà̽á#éA’äЫ ^Y‚:ñØÈ”€¦JIB€$‡H_¤¸¿a¾9/\ƒÌ…šnÁˆÈXÏ)ï1Ijá&K¤æ@÷¤-199‰o|ãÞ*ÙKvH¨šCKø@zxóÍ7qá F1:*ñ’J»¢VZœÈ  ~t1Éá@n”ÿˆpRyè–ó‘ÈØ„€H±ŒgÖÿ¼xëÕJ'ñœ!9 …Ÿs`2*–”니MÙ‚»KûÍ”$»RVV]}Oa…@1•"<øpfQÕ°]æ—Twø^ûÕ'þÜø²Ü¤-É€3)­)ÙAæ—‰f÷³4Co³åìù“Od‡FrA·žÂîB¤û;…È*aP’d~ëçÿ•tÙߟù±âžŽèâ›WÂC¶k&LK\­s`»VÆ~œí¢Œ0.ïw\0]^Iè¤ìÐ邼›¾vlƒï/qƒ…×DvùcÁ˜Ó2c⩼ÛÏà ;$çó È©FC‡64]ë¨òì#¥²Hïޤ²ÈïîùÛÐCJ×;ä išÒœ%²ƒ’y‰j}á* KP_äº<ÚsW>öYÏÕ:so€—¸t`‡f­m-Üi®’ŠC,ÐU*Üõ’/‡W—@P=6ÃÞ[£TsHÍAÁ½…îð:À²°Cvè9Ùõ¬ ü"ô¿ì©Ž×^{ ³³³œ AFáÁY»©ÈšODõòýøŒËŸ.3==M‹†ÆÆÆÄ Y•ã“TÈØ$lDpHZ>nÃÐu¤ £mŸ ©,r2ÄH";¨Ù);¨ÃIÉgº>y쓸zæ—¼?Ky÷k-H»$„û{¥cH–ŠCè…ØRqh\˜Ò¡ ×8¯~(Ü’Ì…½n€ÌÅ,¶OáÁÉ!No<—"9ðèçNSÂ#9Jè.¡ü";ϽÙ!¾¬ŸøehŸòTÇØØ½%]!È(dñò]ðÚJ¸ë~"7û¢Œ[â3>,'Nx(•J´hМ#Ó)aWÄÅDrHr<³¼øùƒ×–Éç¾ç´N Ç.š?—Ò²â/ß­®o º¾®Tÿ¶¹'aôw§PÀm‡–Ú&°sÇU5%QÚeü8Þ–*ÿ¯~õàEþÁ<÷éWBßI²C‹ øLÓ[’šTÔ”ìÀDÉ­ -›ßcYÜÉ@ƒ7²ƒ°a»/X[0d‡nÀ‰Þ^elÌç1ùñ¤Ê>*—ñwßU,•Þå–% 0ÙPhð¹}|Zì¨Fvš ;¸/";ˆÎü Þa× ŒlÁ­?H²Ã.Òš.Ô~LXHiúÓÿt™T ™T )Ý€®iOrwÆiHé:2†ÑVµAc ‡ÒYd:¨?ø7§‰ìf^Bþ¢¾ìêcÒÃ}„©äÐæ6P]ñq€’HrPæUžt‰H{W=a%‡ôË‹E|Bï¢#9 ˜HrÂ$9IPsàÛ 4¿»~’´_LÆòÆÐ¿•z3ú.Ö××1>>N‡Áèè( …‚ø‘ñÞ_„˜JÉA½|?¦ëÛ^Ÿé9áâ¤PüùÌÙ¸Ѐóø.œŒízì%8É¡+â™É(Ñ9G0dš_µgA‘:’öþQ„ìõÉ-/]{';0ÑO}ý,¸ŸÔ’®îY]G¯¤ªBøÂsÏá9‰‡ÛðíŸÝÁ–i*çã-³žˆ3œ³—+Ðÿ¤üfD"o”÷‹ì Œ‚„Çï¨NvðW¼YýÌý¼’þüq;ÕÙxLx9@2]מþ§±'Þ×5ö„ÜM¥ÑSHëú>D3d4GÒ¹¶„Ÿ³-5÷Š$åRq c“´mÁ¡[ãY“!"\wiaQ©¾ZÕš«ïe ýÔ«^ø¸ +îgŒžQµÛC*C„,(¨h׳ÒêŸøüXËÏN&…?Éû#žŒùKvh»©±ö÷î=“šh°6^ ›ì´Pw`ÂI€XòÐÚyþ´Ç\g1¢íuÙa'zz•²çË/½,UnÛ4QyPôâ›6hí|CÏJà‹ºƒ«‹ù’—ë÷¾í{œªàƒ¿:µ£ Ù¡Åu/„„ª:<ÉÁBº”œÓ hŒÚFÇõ™1RYÒ™¶„©8‰Û~V¡´Ã’Ñ";Ä#¯l⎠Ͼ‚‰c/{ëNù!œ[_ó@2ÀÅ[ ·'I’TBo,¼ó©8˜Ö®.4'UÅÁ* }ýìÙ³t¤m¾AjnM!’ƒÇJ­mðmñˉð\¹rE!²CBòš}9ÚèyϤ‡7ß|.\ ÀUãããréÚϾáSŒˆJäd±3Ä£™,}X¨ÊõõuZ0tæ&Ó»4·‰CÞE$Šg@JQ×Wb:5My“·…Ø3*…‡-Ŷ—ÜÙ“|É0wlöÃìRÖ¶ÁÁÁ1Ul!ƒ7Œªj˜•;þÁw…ËõÀsmH ™Þœ}1œMÂ'²Ã¾ršæÙ.ìiÆ1›ìÀöØÒÚ×û¼(½©7ß¡xËo2¡ŠÜ~…ÈqÁ`>ƒ©³ =ŠÏ<%Uö;?»ƒ‡år„ÖÙ¡ÝúfèZWû cìðlôrùUssÉœ…£¨ ¥È>×Od_×þT&ð6ZõµÇHãh&Œ®#<ÐEñàûBÊÃj¸eêÌ«þ‘T]ß¾×a ˆä‹õ%Ñ?äHrPFÉȼ‰–éù€‡šÔÜššCôn ¢RguF¸†‘‘ZDb‚©©)üûÿï=×ãì}²Ñaïú”? c苞šyýõ×155E1d |ó}ØÿÁÇ©’$ hŸú¢Tú¢Ã“/d W?==M‹† ¢þ6n%ÿlJ$B³œ‹HÏ &J©…";´ü~—‘ž¬'½j©h;q 0ÇÇ0ú";ˆ}Ù!aŸG€´¦£/ž4¢¡ièKe0Í£ÇHy«Œžw¯¿’¢ì@ê$§Ä¾‘æÞP'þë@u¥!Bø!Ô‹æ‘Þj°K<¡JrãÅ Arˆ´o!µXýrQ*•Ä·“ÞSÇSU’šƒr~ RÍ¡I¥2„Rwˆ¦¦¦ðÚk¯y?«K“¦æ páNøEèyÕS³¯½öš9àŠÅ"&&&¤ÊÚ gí¦‡©’¤³A’ÕFtØ]Ãr'h£££Ä"çÒ Óø,Jå|‹HÏmLdù“âÕÕ—c2.ñNÓ”ícÀðúœŠ}`†ø½8ÕT‚€éK…CžŠw¸mu ÊáÁ†U4ÊIÁ—Þ–*û „‡“?¬ÂýÚÖÍ£ù_öüs;’À/²NÕí«Ä Ù¡I!Wsg…›žI~ wøü­\ ‘º'zz”²g0ŸÇ¯|ì9©²»¸€Ùå°SÉ';´Tgp‰lÚˆ½‹¸("píÞïÇÛÄyBü•d²ƒ I8k­p ,²‹É9Ýð—ôÐІ¡iÈ)Nçp8CN7"ZC»ø²x(m°pÔ’@vÅo˜nºòÑÏb¤ç˜·n®Ì(Dzààåû€] /g%]"‡Æ)êê¢.#9ìCEü‡,ÿ.Òb³’šC\6·šn_)¯-KÆÆÆhú*ŽèÈÝKrh„~â—¡ý”'3ÆÆÆ0??O!&''¥ËZ?ûxù®ÀTI"ÉÔÔt‡/è9á&Iá¡‹Î¥]c:âoÏ.Md} Õ1›$7›lU_ꉱÊCu}Ýõw‹§N¢pôç>.^Ètÿ›žÕ;¤j׉ðŒ©h”;~ïGÂåNŒ¾ˆþÁŽß8=¨ýÇÄòVíàæÑê¾9cí7½—Þ5mÿ?³ÎåšÖÝ¡\[EŠ}—ð®ÔwTwhNIh­RÑAÝÁ²ƒ”d“D–À$ñ‡ìàÞ";ˆÁÐ4 æóJÙ4ñ 8.iÓ7n…©òÐ>ÚJµzó³† g:¤= ]CºÝEù˜ø ؉ÙZÅŸmÄÝ_ÊÂúœ…Ô> ¼Q’v‘Ó NçÒtÏmd4=F…T™<§sè5Ò04-ð~ø˜IÙAE²Å–„»ŠFÓ/}ÉÒôýƒðð­€ÛÁ6ÅÃïWì×("94’B*<Ê~ÅsÌŠÅ"ɘ/þTIjÊù%d5‡FØ‹oJµ@„µáÙAøE²CBò›Ý|ͧ¾Ã_ËË¿)}}}ãããRªD066†³gÏʶ+0ÿå9Hzà ?~÷…Ô|6Ó}?Xßáæi aˆkË ’›˜\‹HÏ’&²Ì°Ìañ¦ª÷“›3É4ô> ¿ŸWÉTQx¨®oÐr2=£ªiÊüA„‡„ ä^Ø0„¤PvñâçÇ\}/ÓÛƒLO°”MË9¸›ü#š]ÓoIvØ£îà–ìÀš½ã¿m9æŠìÐtƒK*Ùs´UÕð%K`ÿ*S›;ö‘ä0ÜׯœMAªÜÍååTbªì \:塬‘?8œc­VÅÃò6·61·±ŽÛë%,nmb£^¸7­Æ È ?àyI»6ˆìàKüš†Cé,Šé,2š!0â YÝ@Ÿ‘Á@&B:ƒ#…Œ®ƒ1z?<ÇdXÏçHÙA½1!$‘œÅ¿_ž iòvø5È®ïün.ê~Å ~Ä%’ƒÉ!ý""KZHÍ!žs=b’ÓÜeýœ%qÂæÐЩÇ( ?ÉÆs¯‰ÏñØ.Ñ]¼ÓóH½øëRoKßÅÍ›7=© ¼ãÒ¥Kò…“œÿ©aºšƒºËJÌΦÍ” ¦K¬Y³³³´X@&—Ú!<ÄlIàd,ƒ÷äYDr xöÉD–K¼S¹¯˜#TÌb×PèêyË>+\f}aIG½\¡É3w\UÓΪb¼aDE£¬m9–×éϼâú»g†³ÿýJ û®È·#;¸"-ìþ]kU‘?d¸';4ª;$ZÙ·)#×tçO‘ℬ®+§òðùS§0rô¨TÙàUÂ#;lÔëø°RÁýím¬T«¨Ùß"+a‡®1äÓ†T¹\ÊPâléņz?ÛXLJ• 6êuT, ¦ãÀáËzB‚p\-(1»¼ßø}j!â²N+£îÀü¯ŸÈ¡ÇqZÓQHïŠé,zŒ4zŒ42š¬n<ù{!•Å‘LÙ<ú rºá?ÁÁó|dñœ³qi ꇲcBê~¥ÌE#‹©3¯¢àñÍ(Îüà›s9Uð‡s¼²ä­)RqìN7¨8ȑԈ;¥³tk^¸ )<´Æôô´x¡Ì€‡ø {é$5‡ö¶Eè*µÊ°oMªÅññqZ@Exd‡åpOò¶áéáÍ7ßÄ… (È#ÂØØÎ;'_]5÷_`/üHÍAÕe%†ëZD‡'y™„2 )¬—ͧ» üÂd‡=7àYPd‡Ž¤…½!;ÀÙMúŠå';ø ì@d‡î@ÒTþ&0¶­ûh«ÚÖÓs‡ j¶¹Í <¬”±V¯aË2±R«âƒ­M<,—åÏ?’èɤ`èb)Ko6í[û¾ž°ešxXÞîHf¨XVªÕαÙ‘àI Äœ¸áw;Dv oÝKvhÌWÒšŽ#…#…B:ƒþTæÉß3ºi ?Û¢‹âÁ÷…%ÃWDvHn 7¸p´ç8¦_þ’wÒÃí?/?ðÉ™$¨,Öv(M…Ú/¥Ö:<ý!—TöM/W?‘Šƒ˜Õ¦øy›ÞÂîóÖ‘=M|Å‚äÐdR“šC@k÷þX·¿^“»ðA—®ÕÄôôtd‡¤Ñaïz—? c苞êxýõ×155EÁ¦¦¦P(<Õa?ú˜?ù]éõ7Îç5Sò“":ìƒ IëæÍ›´Ptˆä@h™_‘Ÿ»>ž6Që'<8Õû;$ Ï"ìcg¹°ÚI.³µ´+_†N‰E•×îF„t^üe|õ}÷ßÕ³*wX#ˆðó<ôZ|óžp¹ÓŸþ¡ï÷ ¬÷j›v7䂯/7^µgL“';´ÝðX«{÷-ú!Jvhr ¿e9ÉŒÙÐ*ª<Œ=Š=øŒTÙ`TÄ¢­jÛRç‡s,nmÁtœ¦Ÿo˜uqÒƒÇóc ‡òפ‡¾\CWëü%aÇ÷~^«U[ŽY¨d‡ Ûalç?";øW?‘Z¯µ]NvP® ávù+ðvüÉ‚-î`Ûªcݬbmïõÿ¯›U”m6wºlžÙ!¨z—ôà vέ¯µ’äõw¢ò­À1Û7ÚâC$‡˜uPh*ÅKÉ!¦ÃBˆÉ܉¢JH¤æ|óþThÝþœÕ©²gÏžÅðð0MwÅ0;;ë‹òFk²Cò.ái¿ý#¯zªãµ×^Ãìì,}(‹¾Nxù.ÌŸü.ì{Ù½yNäKJ ×4×$ÿúÅò.C*bkŠpl/Æñq›ø#0‘(žÃ7‘åOŠrêà¡««mò8M:DšWÉâYYŸ¸ÊMå!I “ É«Db ѼØXFá¡´=ÒªVANº ªiEŒ ÂC̰f]|¡%;@á™@ûñ~e­é¶ÙáiíÍëîf²kï³N…X‡:]4ëîS&jc»ÝwZ´=";´‡¡i8ÑÛ«”Mƒù<>wRŽ9é¯ÊƒÜÅ7Ëò–iú÷=Ÿã1†žL }y{2(ä3èͦPìÉàHo‡òY¤õø+;z“uͶšÇN\Ü´±w|ƒh#.þR†ìÔ8ø°†µÖ ÕNd‡®îK—ûËâ–k;¤Ó±Û×ߢ²mb­^õ‡ô@ê$ûrŽ «†ë;”’YŶebÛ2±nVQzLPY7«¨4Í5b‚×Nû¤gÒ/?„sëkMHüdUÀËC ê-œ‰~SÜx©OrHÀ‡¦æ?þ‡Í9áêdÞJl#÷©Ê(c<j\‘y«X¥Væ?ÿ'²ÃÙ³g166FÓ_!øGvøÔc²åpaÀþ"Xþ„tùõõuŒÓ›Ô#ÂÔÔFFFü Õú*¬¹ÿóÝËpÖnÆþ¬ æ11†gWæß–ûMøn?êŠ!yM¾î5Që— <Ô–Cr’ªùCÌûtîƒhˆOÚ— <©® Ì²c; „“Vö™ÿ˜ FáA£J.M‰íqúÓ¿ \?VÍ*î<ÚÚ·ÙH“\ìj‰&;ìû³ÙÁ·BDv tÀ‰ždýxƒ»˜xáô¤RÂåüSy¿øæ–¼°‹-Óì¨î°‹²eEzJë:2†Ž|:…´®Cטzç°mȘ;1";@  ]“´ÓÅ\#²ƒàç,„yô¿I;ÕOd‡0Ú‘xT¥ì¥wUÇ^îq Í߈ Öãºâ´?‡‘óɹ?QϨÚVdzJÛØ´kX®—Ÿª>Ä“Ç>‰óϾâ-œÊá,ü”ø5¨¾TWX\’¦â"9´ØA™Ûªy<ú¥ä°ðöýâoô”zs(!¼äÔb”P‡Õ´ÿ•:ë·`Þ¼¾qËS=¤î ü";°ü Ͻ– %:9žGêÅ_ôœt7oÞÄää$M„ˆ0==íéøæû°nÿ¡Š¤æ vš©þ¹‡ºîq›èx ’ųÚ&Ê)<pÒH{ÄMš´x¿$H‘ö-æC“¤~Å óóóŠ¿j±Njá5P¬2ì¹oÁúÉ€×¼‘ÏŸ?Oê ÁO²CêÅßH@Êà y>Þ|óM\¸p&D(‹˜žžÆÐп¡üXñ¡þã_‡½ðgž×îÐ÷ RsÈähúÄúΗ!噀ÃÅÏ5!Vwø,JIs"9P<ÇjÊi=âŠiÜÚ·6rZDc•ÌÆÂËw Ùá‰=¤òн°*B_wÒU{2¤‚DxˆùxGLêå¹O˽Í0ÓÛh?îÖ¶°¶]{²µ$ìù!gð IDATKÓï°Îåà¢\ã‚ÛÝi0¸Ö¯`¢5ÃÙ¡…UÌ/‡1•©‰ºÔ];DvÆÑlÅtF)›¾ðÜs¨ø@lž^qj’šƒr~‰‰šÃ.ì‡ú[_ýàï=×544Dê Áw²ƒ‘O@>O°üIC_ôTÇë¯¿Ž©©)š`—ôà§ÒÃÓE¼ûÑ ˜oÿ6ÌŸüîŽêƒ]ñ{H*ÙâÙ¯ÈÅ1„ ‘”×–»hºÐ99Ü\ŠüÜõñÇ)§çÁò¤‡êýø;-´f“»«FtØ…–}F¸Lia16~ï=vLühd𱋝âÐIñÙ¶yOèû +<`pp0rÉi"<ÄtàZ¼%v™D–ð4nWÖ°]³°êíÉ-eZ”c¿ÓöÂsÚ€?ê-êniO{"Sì:“iºó§ŒÈIÆ ‡©•˜¥R!«<øsñÍ Ám™&ÃLo3Å»¯8’Ͷ&–ìÁ¡LG²¹§±Ù!èvx›6tÍÝùÛ Á«*‚Ûv<ùÂMYæ­®í#²ƒÈçDv£ Y%hL<¢jÛôƒƒ£jÛØ²ë˜«–0W)ánm·+kø º.¥\͘„KvààØ°j¾QLnc3!JSg^ÅHÏ1OÕ:ó×Ô =p{‡ôÀí”4$ø‡\ïJ>Wi¿Ôz;e×Ôüµ-B·(ß« ûÁßÁ|ë+°çþԷ˱ׯ_G±X¤¹¬æçç‰ì°›Ó%båÐ>ý#¯zªåµ×^Ãìì,M0<<éa7JÊwŸ¨>Xïÿ>¸¿ó1ò!¦çƒœÃYFü ÃDxˆcÜ‘±ÝDr xNΔÓúŸï¶/j9IzCBò,·ù ”ýÕxǾì³Âe¶b¤ð`dÅ_lìÄðʇó¬Ea•‡È"áAÃ*%Jv€ÓŸyEY'Ï—×›’öíTh~ÕžDv@GÒÂÞ?ŠàŠìÐÉ–få"%;0Lmºó§DvH<²ºŽá¾~¥l OåÁ¿‹o–ã>kn.$>F¯‘Ú£&Ð`C”Ÿu‡'IcêëÇ‘l)MÛ÷ï9Ã@:½½È…LvàÌŸ âÈm£û0a(TÀ;Ù!¨uCt¼Úµã+Ù!9ŸÙ! 0òWà}ñ¶¾8Ü ÌWKæ6î×¶`6¬…I1èW<†ÅŸ~ñÒO…¾?44‚ñà߃7R5"9Õ^[†½øæŽ¢ÃÜŸútcW¯^Åèè(ÍgP*•0>>Þd‡Ä]ÎÛßýīЎ~ÊSccctÑ8"ì*=œ;w.ð¶œÒÛOÈæ»—a?ú—k>):¨Ÿª×-}˜&x÷<. cDr xNÒ”{Ú Ö'Axð¤ð@Šq‹ZZœpº½-᡺¾AËD°Õ%<ŒE>—(<¤ ä`GðâÂÀé!dz{¤Û8ìjïWJX\­4ß ‚ ;´Ý YÓKöþ‘š\ÃoY.²ƒt"Ðìðd¥Ñ$Û"²C7c¸¯¯¹Š@DGåÁß‹›[±aŒË^x„ŒwÎßàH6‡öð|ñž/ÂéB'{û0˜ïAÞH=°Èžëð¡ ÆÐ–ÉC/ÀœöÛ_ª"{Á®ÈÂm(þfüøûK¦¦¤¯,î dU±nÖÚ~oÉ,w$DÛ÷þ5¹ƒ5«Š•z‹µ |P[Ç{•UÌUw”-Ö·q¿¾…¹j ‹µ¦ýæà¶í:ì°|àVP4²˜:ó* zF¾N» çÖ×Õ =˜àåHÔDrhÊ,˜ª#í—’C£X¿†‡‡AP5œIÍ!ÚD6¬¦ƒí¯-úý5˜oýìÅÿ曢Ã.Ο?ÉÉIš×Š`||7oÞô–ÞÆì(5‡ök‚1ü«`ùÒ5¯¯¯c||¥R‰&K(‹¸~ý:.^¼^4m¾{áÛ0ßþm˜?ù]Ø ^¾Ü>ùTŒÉZÀELVóä%ï#µÎ œŒ¥xÀS‚‘(ž3åšwBJ᡾8uÉö#Î-’ÑP¬r›Ð¡¥Á$H¥…ÅÈL®z|)C'XÕm7-`çŽ+{DŽ|*QxHaXE£LÁ‹­Ï}ú<µç…,áïWÖpwµ¼o“ÚûQ²oC.hürבZöÝ]¡¶d %áA†ìà%Éiú/DvP/)e•‡ÙåeømþžKRšû”`á!ÁDƒøØÐed‡Ý: ÝÃx…зÞy€þò•¬Àü{Þ¬~ÖæóNåÝ~ΞŽM õÙ!´uüp_Ô$;¬Z,V7±fv~æpŽ ³¦è¸»÷ïŠYÁ\µ„ëe¬XT 5§µ2WűðAuNƒaÛ ìœÂlÇQå¡ÉPŒöÇôË_òLzàóov5úþÕV€zŒ/Eí%8$’ä ÷«¤úJ øµµièâF×"$‡&‡Rsˆ¥š8«30ÿù`¾õ[p–¾H¸rå ÍoE0;;‹ï~÷»ÞÒÚ¸«æÐ¡/z©ÿG@ÏI·tóæM")EŒK—.áÆ(Â}Ó&/ß…ýèÌŸü.ê?þuXsÿÎò?y'Â‘šƒœ™mMV›äpàçû¾ÓBõ¬|á­ë±wN“Š!ÑùÅ3M9—Ñs`qE"GHå!I/eP¬!å7±³[‚ð°þÁBbW]Çq@h1sõ¬ª¦E.@„9 'a8ý™W”vòíÊV·êO6ª½VvØqPsrA×»™ì SÈUñ&©eÉÌ“µb•Å19JzS) ÷õ+eO0*Á\Ü,ÕkB}Ó\ØÑŸJCÛ]pˆì € 1";Àç6t½ù¦©qÃï(Cvhñ¸ÀMyî¶mí-Ú Pµ‚Èm²CÀþ’iÿ,8£¾ùjÕª`Ý|úfu¶³ì˜ Ž»{ÿ~hîDÛqÀ±T/ïû·v$ ?Ps¬ø:š¦Î¼ê-4ÊáÜúº¤¾}°¶ã3¤âÐ2L…<ýRrXx‚úE)÷Rä@jÁ7p¬2ìó­¯Àz÷«à·kjbbSSS4ÇÂõë×½U çÔ';t#ÉáÀ8y'=¼ù曘žž¦I!ÆÆÆ0??sçÎEc€]³üXsŒúcGýáÑ&ꪦí1ʯ…ÌT³Oq½˜ŒŒŒˆ…Üö"‘ .&’Ås’¦œä‹_ú$T*÷ìIÂóŠîZ§ãžßȶ-ÑRÝ…0{O©jÚpÔáA£*eYî/dzò8=¬´“+Ž…Û[%wd;^¢Éûþ,Ov`2…Ü|¬iû:&ÙAØ~/5“ºC0;e_ß~Eˆ!«òpsyÙ…ÊC„ c8–ËuHö|‡ÈÉ9#òêà׿fåuMÌŽ0ˆ~´£ÙÁçú‰ìµ”â¯èseƒi®•ϺÜÙGvØ­;~û§{_˜ÜÁšU•ޝ »¶OåÁâvàîšTfèyWÏü’7Ÿ”ÂYø+5Ò½­5'ÚæÕ¤âÐ’¤âbÔp*Aé¸P-NHÍÁ[âо½ëö×Pë+°çþ¼¶¨‹.^¼Hd1;;+_Xe²Ã¾/I›„‡cGþ$Œ¡/zªƒæpô(‹¸~ý:nܸ¡¡¡h£²|ö·aþä?Á¼ùÛ°æþÎÚÍöÈÈ7éMåJŸ‡‚ÎûæççiX+ºýÙA2o"’Åsb¦œ÷Nhý„‡¶ I': ÑëGˆœZöá2ë ‹‰S«Z¡9ì´²¹äPäóˆÂC %Ýv¹àÄÏ"Ž~¿¼†ÅÕJK²Ãã{ôìàA~ï_°G£U¹§a`.È;U쯨3Ù¹";0(;´!Q¸Í|QNhWÉc•&ÙˆlâÂ$3!Ñöˆì,^(RÆÿU‚½¼Yª¹WyèO§ñl¾§éÅÄœn`¨¯oçmÊÜ?ûèœæñHàåí§ßa>ÔP?t] ¯>‘‚n‡ÈôÈJ®¯IQv€ªþ’i#˜G…¹]•t·ìýd‡´¦¹RxH1]¡ñóï†Uó<îÕ=ª aœU‚&U„ÉcŸÄÅ“Ÿö*+³pÿZ5ÄßœT£D¿©.@ùê}î[Ò††.nâ0HÍ!¼51x5gé{0o^‚yó?ÂYú>`WuS¡PÀÕ«WqéÒ%šÆ BúBäc²ë9©àRÙåjm  | úGäåè²±:ØU{¸xñ" …è¯ðúêŽúÃí?Býÿý2¬÷ÿöÃàÕæP¦ºÚ}‘ÉûXî­AU>t‘‹‰à@ñœ´)çoG¤ê+ê=· ¼+ô:ÒÓߺˆäÐvB*¤äàÓШ×/¾5/\&Yo MÞü,éTFÍH–šCðýÙ«æ`Ýþ:øv8oèÁôô4&''i:+ŠÑQ¹ß‰•#;$’èLÕúG^…vôSü Á¥K—”">ìÂ)½ {ñÛ0ßù˜oÿ6ìÅoÃÙ|¿ r'?§ºú}ÏûöôIÏÑ&ÄwþÆÚÅDr xNÚ” ®#,s,sXÜ¢}*IznÑ®±ä!ID‡}ý’PyØ~´DËzœaʽèÄÊSµG‘þ0A„‡„À²,¡ïŸŒ‰ÂýÚæ×¶š_³—!;8N‡Ëþîɬá¢$;g­ÿꪠ0Ùt¬­ÑÍHdÂ.Nôö¢˜Î(a‹•‡ïܹ#mrç”R½&egÞ0p(“Á¡L™Ý·èÑ@|";„¡ˆàG.ö—Dø+Éd_vÜç íóÉ[ÇÉ_¡¶ÁÀP0¼åp©=爼žr©î ¡WO+0ˆr9ŸæÃ:™×R¡ö´ÎmØ+\æííñÓåµ'Z’ОìÀÞÔOþ“XCDvØcü¾7‹>ý´fÙØ®›(Uj(•kت™¨˜8ç2]Üÿ/DvHŠ™ †ûú•°eâ…¤ÊýãÃØ2로Wª¶—ÎDmའ««3²¸ÆÏ ½acM9$ÔÇ>ŒEÛψì äÚ§lŒüÕ¼°MÇÑL=zªíYFc y=…#é ©,ÒšŽgÓ}Ð:Hýi`L÷P7ˆc¢ád¦_˜ôЯgš–ËhF(vs6w4#=¼ì‘ôP/Á¹õu5Hõ’<éညCÒ’uoæoÕ‘öKÙaá ê¡‹¡ªš‘„`•á,}æÍK0ßú-Øþ¼¶ºëv‰óó󘜜¤éCÈ\bw‚Vxhªà÷ý65—Uóòb‡ åþÅ !úù=55…µµ5\¾|###êkWà¬üÖí?ÚQ¸uö£÷¶åÝMU¿/ry‰TB±(þUUÁè¬ÎZC$‡Øçp4åZtBްü @W% EåÁW9uðêýÿê+]·¦wÑáIŸ>0£W¸\IaÒCáÔIé²f¹LÛ\«ãY¦ GÙ8b6`­`Yî/´žôIá!ÓÛ#¾h÷}Dª­™Í%¬lÖ<‘v§¡‘ÜòOm{Tv>-×-,oU±Q©£\·`ÚLÛA¹na³jâí*Jå*¦%Ø‘½÷õ¡˜ŽžÉ8˜ÏK©_Ns":$2 °+þÇÞ¾‹{I\Õ©ZFÝaddDê-A ìª>ÌÎÎbnnN}ò¾ù>ìÅoÃ|çw`¾ý;°¿ §ôvŒ§ºúgñÜÏÃÙˆŽSBFm‹Ûe&•ƒ]c8‘B?;s21ÌçŠBFåÁ©<Þma—Þ‚¹0ëá_ìüwÿ;°îþ œò|â7ìn$:ìë¿„ÊÃö£%Úº vZÙóÿP¤{…F¼¬e€ \h•Qfð ™~)ÒÃ6à½G›6¿½qCZöäAm²Ã¿ÙA¤¢–_Ñ4€iàœ£T©a»fî¨8´©³n;جšXٮ´mÙ¡ðÒáÃÈêFävȪ<üÍâBûɱ¶ešòµp v¬êayw·¶°V«¢b[0÷(÷Tl ËÛxXÞöÿÀåQ/²BRD° ]ü9 ÉÛ!²C}#²C2Ú ²C({‹"0˜†œn ÇH£ÇH#£éûHH1 ƒé<Ÿ?ŒÓ¹C8‘éÃó¹Ãøh¶ˆ#©œÿúˆáŒ¦ãÙt/žÏÆP¦€çs‡ñ|î0Nfúq2Ó#©YäµTûþs4%‹NJ±CCwÆ §põÌ/y ­•Y8‹­FÿÚ‘¼©.Ib¹_%ÕWrHÀ¯­M»ä‹t!¥;@já4lœÕX·¿†úÿ¬Û_‡³:YD={×®]#¢C‚P,¥.>;k³>NŸ$ç{êUÍ7Å ¤î  ?œ={VíU_…ýhzGýáÿûw°nÿœåD¢l$6ã£æ Gtðžšh¯#¸È‘ˆà@ñœ¨)¿Ž°>qÂC >ºÍzøçpJolÂÚ‚½ôÿÀÙº…$“º,û¬p™Ò ‰ô…c;­|“.(kÛàààpTmáAl FUµÍ-ááÄè‹Ñ/Úùá2÷j›øÑÃ¥æ›k¾%6%;໾êDv`­·Ûn!;Èf¬Ó§†õj¦åg;k ja‘êÁÐ4¼tø0 íV6˜ÏcäèQárÊeüÍÂB냒RyðbƒãSªù°Rq¥à°Q¯c­V ĆÐz¡‘XðíÝc€‘B&;$çs";„´î‘¿îKÊ!‰†¼–Rl܃ጦ{êKZÓƒõÙnžÎø¨©ah'}çŸ}Å[ˆ=ú'ðå5ú·KzØ«âÀébùÞá"8ðxôKÉaá ê—šÁÒ5(•Jê­9†=<’@j5o/ÀžûÖÉáݯÂYú>`W"‹ª‰‰ ÌÌÌ`zzZN€ 4dÆÔ3ñ&ÑDµS.…"<$»ä‡ééi¬­­áêÕ«˜˜˜@¡PPÚn§ô6¬ùoÂ|ç"ÌŸþ'Ø‹ß/ßUä”t’Àg„¤?Û HäGDr xNÔ”‹wGdxõÿ.ô+Úz¯£}öê?[›‰šñÝ®ê°Ï2 KÉTx°j5 ˆvþÉSö(Ùž@a!%uB,ËrýÝHÕðÍ»`Ç>)UöÖðþÃͦ<æ’ì°c„0æšìp@í!Ñd‡–Ux«¨q¶˜zÛ5 –.ÃÔÔDo*…ŠÜŽ‰Ë©<|çgwB±oˬ‹ˆ:Ö›œµMÇ"1¬Tª¾ÛXøqù•+ÐÐé"(‘ÂQá"yˆ¸";„±†2òWÖ{%ö-UÛˆG ÷idµàTÒ2LG·àÊG?‹‰c/{ªÃ™¿^~¨@îÍÚøö]ŠCT"o<@óIÅàfgÅßxÎzNù?Ö.†>|’©9t¬ù1ÉÁ|ë+0oþGØþ>R’C¡PÀÅ‹177‡©©)ŒŽŽÒ$O(d.³óò]À*K:'¢C„Õòò¢ÔºB„‡ä£X,brrSSS(•J¸qãΟ?/¥êì+߃ýhæOõ™ß„5÷Çp–àÿþéj>ª¹®1éÜ/8’ˈ_´“ɳ t¦Œ{‰ä@ñŒ½§"Y/Ü`ù€ž÷‚*¸ÐÙxÇÅ—êàå1ë‰èpZöá2Õõ XU"tÝ®©gU5-²”DxˆÉ@µÝã.~Ÿý„oížüy µ³–; ­÷¸pÑn<À;÷×<";€A²Ãjë\.Þʬy²á2ûhÿµ§ŸVê&`èÒYÃ9¶ªf“‘FÝÈjãh6‹ŠÑ’FÅsoÛ¹³¾ŽÙå˜|Æ–iŠ؈‘6pÔd”-T:ö…¡îàõò¾«xð¡ îÒ_4M])Evà±ï°ãF¶`ÖOd‡Ö<òWÀ}I޲ƒšmÄ+†ûL`¤‡#܃G“a¾òÑÏb¤ÇÛÛUœ[_Ÿô°÷‡Ü½²«ôÿE¨8ìó]”ý"’CÌ;LPFÞ¿uÇ%É%9@rൕHCrddW¯^E©TÂ¥K—0<ÿ`Wƒ·?䯒ôà]ÉÁçª#í—²C“¤~%u¼Þ\I5‡¤ämÝCr( ˜˜˜ÀÌÌ fgg199IS­Ë ógãV‡`'5Õ–P¾ù^(±AH†‡‡199‰ëׯƒsŽk×®áüùóR{¶–ïÁ^üÌŸþÌ·gGý¡ôvgõW©µšk›·œ/8EB—ïoRqˆò¼L&RGüØ_ó'Ä ÕWäݪÂè:õøŽ-„¡¥ÅÕ¶¶-…jcm}=ð6Hµ¢Ã: ®ÂCd)ˆð“j· ™ž<ú¢„›÷Z{Fï˜Ä%°qÿ|wýñæÈ:n–¾ñ˜c°—6Ùí±¥åÆ/»ó»8hé%ͺúÔ´ùÓ2t©Îî–¨Y6ˆì@ØÅ ÅC‘’>êŽK´ÿ½‡ð°¼è¡É•ƒ ±î³ )Máá5$²‚&;0wø=_Roâ/íß[bAv!.y‡±'²!6§zr˜¿X2Æ„ÔIi#£é8šÎ{&*h`(Yät£;æUÒÃõŸû zF~ÈÊáÜþÿã@öMuõøÖÀU~³Ž7‚ó·êHû¥ì°ðõ+©ãEð8îîζ]¡æHóÁôGE’ áòå˘ŸŸÇÔÔFGGiªu)ÆÇÇ…Ë8«³rAÚwU[Beˆð@h¶^\¹róó󘙙ÁåË—qöìYµgq}uGýáö¡>ó›°nÿÑŽúÃÞ½8!jžsÌŠÝq|#’Cìsµ.2Q¬ÝÓ¬OBáAT!– ßr"‚ _IJ ‹¡ÚX]ß Šfï)rBˆðX–åê{~«;œý„Ä,,CßZÓ ½âä‹U³Š?¿7‡JÝn½s¶ú`/Ç ñmçmÈî·l¿¾ÕX†!°Ê;‘˜×fY“üpOvÈ4@×¥=mÙîm%²Cwàt½©TdíÿŠ*¡-é!ˆX@¾Þ›J U—Ó ¤DUdºIÙÁ âA^ðwÛV0ôpÚqS‡+_°€}Õa7$²CëŒÔnCÑG^]}¿‹ã·‹ÕIrºÃ©zõ´«^i`H1y-…‚‘ÅÑti¢iœÑà¨Ñžã¸þs¿âmè6çá̽áO,ûñ#®¹ ¾9¯éAîÇDtðYöá2ë!‚íæ%¾ÝºÛª«ðÛž1¨¶û³K…‡ÓÃÑ[~*­“>tRªŠ¿\ù~tgõÀÆÙ¸sî»Êß(¨ðø€ÓX®Ù¡³ºCsJë`OË mŸ=âd÷ „w†€(Ù¡iݽÊC£bß½PMdÂcš†Ñ#G##=|îÔ)ôH´ý7‹ ز‚•£Û ;Aäþ~7£ëBo >’ËgoPGÀ¨I®ëðØF º_T,ˆì­rE„ŸÙ!Œ6˜x4&m°àÕHÙ!qþÊë)MçÑ«§‘b )¦£OÏàH:‡C©,z42ÝFthc…S¸zæ—¼ áÊ,øòŒøÚ(«äÐ vu‡ô`W#r²Ü¯’ê+9$à×ÖŽ*I}›4§áï:¨ªæ€h)¦ý­ÔYQ’ä###¸zõê5ºÄLØ‹ááaŒŒŒˆÇüÆ­„]ê AÍ!¢½DFÝadd„Q×(‹ÇÔÔJ¥fffpñâE©µ%Ô™R_…½4 ë½×QŸýMXwþÎÊ_RLÑÁ§uÎB‹yA‡Øçh]db—uÄßý·ïŒ¸'Û©<„æâ†ñ4úÜ•²¶ÔÑAÚwF ¥…Ëm=ZJœ/"<´„•;®¬mƒƒƒ‘<¨ ƒâä*°Ý*<@xè?.¦ÒÀkOÙ‡Z¦zNÜ­«fÿíîÏP®[O6O!²Ã.Tº‹ìв ×1ÉFRz“eÇ0äÛ#²¡1œ“²—ãýBo*…ÏŸ—“Ú6MüíÂB ¶µ$<ïL¤ôö"ÓAFÃ`¾y#£¨í"²|è‡[z´þòJvðÃW¡+;0c©Ãc";ļ Rv tÙ¸Ç*+aÈë)Jeq,ݳï¿C©,rºáþ¬ÚÛ‡ÉcŸÄùg_ñT¥3 |s®}Ìî{soÀA:é!$‚C¤$‡˜®•IIÛˆàgx»ª²š‘Úî¡«3°n õþ;Xï~U)’C¡PÀùóç177‡ÙÙYLNNÒåeBKÈ`œµÙ„íÁñªZd}ã›â„‡ññqšiŒŽŽâÒ¥K˜ÅÜÜ®^½ŠsçΩ­þ`Wà”Þ†5ÿÍõ‡Ÿþìû^¾ù#y¢CœÖ0!.)‘b|ì:Å:A1Ý 2*¼ò@³ACNá’ð¼ú@Ùñ _Š|ˆiRy ¸YôŒ²ÇÍHæ …„ÚÔñ¼ßpi¿ Ï øÞ~¿DŽùôGòTá©vwUÚ“Ð’_ÀvœwðË{ÿ–h²k]βCs'0éf„MkKz`-þÅ04_“+";$†¦á¥Ã‡a°ð·¹/<÷œT¹oÿìÀ‚ À¦„‡ ãÝíd°Ac '{ûp$›ÝG|ÈézS) ärøX¡€þ´ :ÒyßedÏ}ðW;¡0úâu7õíBXd‡ loÑ‘b°¶ù™¡ˆÂ} AÙ;Ä–¸òÑÏbâØËžêpn ¼üðà¼ãýèÅmð;@½ðÂÂ…g# ¦êHû¥ÖžÒÌ|"9±_òê‡ÂMÐeéÖ”Ï×ÿê«9¨²¨Si#ÉÁYú>`W”‰ÅsçÎáÚµk(•J¸rå †‡‡i‚:Bær;ßx°Ë ؇㞎unLFá”`~axx“““¸~ý:J¥nܸóçÏchhHí¢röƒ¿†ù/¿úìW`ÍNéíÐöüÈÕè™°gNÉ!Üœ†H‰>ïǬOœðà4*<¨p60zåíWa@dß|™>"\&‰ vÌ>|÷–XÜaOí)¬òÉJPÇ /@x81ú ßÛï'Í> ¸Œ…]$‘ðàÄŒð <Î NZYu¾Q×Cß hé õÎô. ýÇi¿ð˜ø$´ö“RÅ“RmÿÅÊܸõ¨ÅÆÚì°{gζÿq €u.9ÙA:¡ðNv`¢Ÿ6é~O&…¦\ÃØçvdÆ€\Ê $‹Ð»¤‡à”XÓ¨“UyøÎÏîê'*aœ£®þYžÈ>µF’}i§ò”ψì@߈쟵Ϳ¬•ü%ÓRv¶ Ša‚ûð(YL¿ô%<ÈÌòÊC8w¾¥^_k+à›ó·%‚U^Ź­:’Ëm1~[Kßq$óu¢>÷ËǪ˜ÑCk©âË<©9DX©U†³ô=Xï~õý¬Û_WŽì044„Ë—/cnn³³³˜œœ$²ÁÎ;'\ÆYÙ~÷%T<Qw8{ö,MB(Æ… pýúupÎqíÚ5LLL¨¯þ°yöâ0ÿå÷a¾söâwvÔ<æ}¡->ôbo¡Éœ Us2±ë;¢„Yßñ¢•û!é"ßx«?¯>œz¤®'E‡à eŸ.³½´«Z#çuœ´²Ï#1Œç|%Úq÷Öòþg‚"<ˆ×Û¨æ*<¦‰‹¬šUüß ïcy³v€ì¤`¶ q²C“kø¬Í¶Ù¡ ñ¢c!‰ˆ Ù¡t¡/—iîÃpU6í_rFê‰F𤇃=zÏÄ–7——ñ°²˜][u3ús´ gù¤sPéAÓ€”qIJ罙2{/°zH¶ˆìÐèM¥ðÒáÃ>×ÊÚþý “Ty¸œÊÃV˜2`. y]zdñ‡$Ðñ\íÃñ“ü¹«ïx$!ìª<Ù!À¾Å÷s";„´æ‘¿îKB7R wo ',ÍÛ‹Ñžã˜:óª·¡^™_™Q¯¯Ž¹Cz¨­4 ÎTöVM$Í'’CÒ‡ž ·”‡Ct ’ÃvI7/Á|ë·`Ïý)øÆ-¥â£P(`bb7nÜÀüü<®\¹‚ÑÑQš8ß1>>.1‡VÀË‹ îìu‘‘¼5(£ð@„‚ Å… 0;;‹µµ5\½z( JÛí”Þ†5ÿM˜ï\„ùÓßÛQØ|ßCÞçq-Pì\Á­²p"<üÛV9‘BÏa8™˜Ô3~RC¶ÅX¿¸ÂƒS½°Áâ`oöw6Þ }(HÕ!D_KÖ?XH”œ„ß/cVÅ›Ô%: Í‚§°?ò<.ô³žªt毃oΫ¹L”€o}p ²Jçm$¿&à×Ö¦æ'õ s>÷‹~£¦%< VXM@rØ^T*.vI×®]C©TÂÔÔ]@&Žááa ‰çQª<„°¾…¾„ú× /ßlñK´ÞTC±XÄää$¦¦¦P*•033ƒóçÏ«¯þP¹·£þðÞÿŠúìW`ÏöÊ]ÎKHÊ-è°Cˆ"M ‚C¸ù ©8PG’âÆý_Qx€S¯¯(uNÐòÃî[³¶BP©Ø‡&@~ÙEiA­gg½ÇÑ@¶›Wæ6ÒþÌ‘»³g厫ڵ¡Hæ …”ÚÔ nNþü‹ÙéÉ‹åæAƒ•‡›[âÏnÍÉ‘0î ñÖ{Xdá¬âàÝ’ø¸óWäÉOýÁГIáHo}¹4RúãeÉ0Ýxâ³lZÇá|¹‘òÌç} =0ןýŠ„Êöiâø|Xyë¥ZÈ2`?`Cä‡åhÓXåÈœsTL ë•>Ü*cis{ç¿ÿ¯–+X«T±U«£þXI´ ù¾úè¯]•þ dð%\WÄ$‘òcȧ îÉâHoÅ|Åþ<Š=Y ôåПMCט/vÙ¡Ë×tO¤±hûÜ©SR­|ãÖ»þ÷ T‘ô°;шìÕÈœsl×M,oW°Y­¡fYOsŠ=mX¶Ó²Q®›(•«XÙ.£R·}°3º´¿_É \ÌåÖÀÀÈ,àúIÝ!œ6},FÒ»ÓW¤ì@ˆ õkÄÔ™W1Òãá-7vÎíov5ÂE¡ÍyÜßœ/?¸½ÏÌcÕ‘ö+kuSó‰ä T2ý"$m`D‰’Dr „!ófgífrÖ¸ÐÓ™`ä›ï…B”Æää$®_¿Î9®]»†óçÏK)Ö„š2UîÁZ|õù}Ôß¹kþá”Þ–ReQWÍ΄0ÃH4·“¶üP<‡ãF÷~–Qyð®à0•‡ê8[Á(úÑ!Z°ô@K •±j5lLz°ªáÝ-ã¶Óc­Õב¹7 ­¾.>9eU4†C÷#-jŒ«@n{I?N×™ºíÚÖÁ\3>òQ)VÍ*þðýw°¼Y;¸!»Q[°í's÷—kNIh|I4Ûû'—êÁ“Xër¾ü!4+§k iÃØù/Ÿm+‹Ad‚ äHLªÏ'=<*—1»ò¡?‡Æl™fxŽæœÈ»±£Ù¡nÛXÞ®`»V?HœìІ훵V¶Ëû„ìŒÈ_*q';¸YŸ¸ŸíÙAɵ‡ÞŒŸÉô…QüºmƒÈ…–Âé—¾„¡LA> *áÌ_ 1‰ÿU’ÕV€;`Ö¶¢J x[G"9ÄcøéÒQнXÇ×o%cl}o^¾B"9Þ ‹ÎÚl¼÷¯¨94— "<ºs»rå æçç177‡Ë—/+¯\Âë«°W~óÎÿ‰ÚìoÁ¼óŸa/Mƒ×Wc˜Ú·7Š×W(H >…'’Cè‡=N&RGàFy?³~q…GZá!¸8Ðú_ú¾½úO€S÷­}RuP*ª'²ƒDtY<`É´‘²¡ëà€cîá~} ‹µ ܯoaî%¿ã Óµhdqýç¾€‚./ïËKï¹#à…Œ uqï;nî¨=lß{ªöéï„ "8ðõK¼Ã‘V¥T¿®0<<Ñ8GbQ÷Ç*É@ð2ƒùFoý aÿJšÃÖÊw¥ÞO„BÒr³ .`zzkkk¸ví&&&P(”¶Û)½³£þðÎ%Ôß¹kñ 8›ï+žÚ»3¬-‰ƒÖ%BÇ#‚Cs–¤š(Ö ŠéàâÁ??Ë(<À©Ký‚fôeŸꃽñŽ÷vADÕ ‰ÄÁc$`–Ëñ°s[ÜN-Ó·:&2÷¦¡o¹¿hçŽ+{Ô }¾Ð’¡æÀ¸[…‡3ÁšL¢~»¶Ùò³ÌÀi[þpám¼³¸þt‡ÆÞ?¶&;ÛUy`Š·,8Ù¡- AžìÀD? Šì°× ¤";ü€;Òƒ·TôèQÏ‹Ïþvq+ÛÞ“MPª×Ãs°£À¤‹Ô„°ÈpÕÎFµv àºæó lšX-Wv¾\âgÁúB×Ý¥¿Ü‡±2n‰ì õ9‘BZ÷È_êùXE_);îÃA æØø ºŽ‡õmlÙuT [vëÛø ¶'é?"5„ÕhÏqLyÕ[H<˜_™ñ1À$Þ¢åfÆÔJà¥÷Z)¢ƒ@̤lj>©8Èû‡„dnØ*µÊp–¾ëݯ¢þ£_#’à#dâÔ?…‡€ö±½U†¾UF³/˨;¨þ|Á ŠÅ"ÆÇÇ155…R©„™™\¼x###jg{õUØKÓ0ßûßP»ùXýaå‡RÄ:{$ÓÓÓÂeXæ¨âÇv"9Ð|NÚ’CñN<ãgÖ'~·»Vy/6ôâ'؇ò>TË>+\¦ôÁ"9.æH8ãšô@ OA„EÆU +r{ÛO…0z çŠR¶Ü­mâ~{eÓÚ·aw$;ìúÔ¶ÿ½É5|Ö&% ²ƒt2åºÓ{HDv ø‰Á|§û R3¢õÙg¹‰¿ e›´ÊC›x·[¦ÎÁÖ¥Q 6Dw$ ‘ìÐ9N+¦…ªiâ'Ëq°V®úЗŽ×»*AÅ•WÒ†Rd‡ä|Nd™½Ì{fGþò» ";Û‘‚€ŽûõM˜¼y^Xsl,Ö6’èh¯ñ#ÏãòG?ëÍ·‹^~èqáò¨äà¦jnƒoßßœ¬í& )9ijÃAù.æý"„?zµeE“Á°š«ÔYuûk¨ÿè×`Ýþ:œÕ¥ÆshhçÏŸÇÌÌ ‘±…Ì›´ym¼¶ÚZ ]mBÕšZ°ù^(cO Ä£££¸téfgg±¶¶†«W¯âܹÿŸ½7Žëºï|¿wé¥PâÒS+à$JìDz@¶²«Š¨’“©ÌÔ‘çç7å—±ͼ‰ý&ñ•íÙžÔ;åçÉBP5~™J¬ˆšŠciì5Ë¡dJlˆ”nBc!Ñ@7ÐûÝÞî{nß{ûöíß·Š%ŠÝgûßÙnŸÏýrvô%5u òø·Q¼~ž yêo¡åîÖáðhp~s¨AÚ(Î×å°#»Fƒí{ªH qí±³‘(j6VŸ³P¥¹ß¿Ÿ)Ê{”Š•rÈs-Þ˾ȸ,ƒª¨MÙ÷z¡™"<¬š2œÙ1ºÎ :ßÒ} ÷aËër ÷!öçÙÅ?ów?d¸.—¸ƒ?ûÖ†[쀢€+³wÙ9Á¶À Êy½OSÉä9¬­ ÝÛ¢0˜wéí‰ýûÑêñ0g÷ÚäÊNJê°Q–›~¸ªãë&gÁ²¢b9_¨­=ÕÊPUdŠR m±Ëf\ xà8cy8v°Ëï9ûÆŒ¥¹ìàÈ>¤‹âÍ:Nȇ­RJ.ì;¬ª *XR h6 Ý÷8Nï}ÔxJê¿”¼Ngª r¨9k9m9-sP%‰‹ 7µËŽþr$à@?–;I½½½ì½™O8ßë :h™ (c…â[ÿòè7 ÎþÈQý¾rˆÅb8wîœ!_ ‘œ4—>|˜9±(6=a’‘<šU¡Pƒƒƒ¸xñ"’É$Þxã <û쳆æB[gœÜ](³?ø* #_„<þ_ Ì¿(9ÇÎkZx }•ËEQêô,ªH qƒí·3×a$Âô#Ï®ëo7ãÅxŠêÐ â½àÄ6ædÉ gDyýµGP ù¦í~=Ѓ&øœÝdBñ IDATZý°íÃ…f gvL£ÉH”9—ÜÙ1=~x»Ž®ÏŸM]Ã÷2 &Ø¥ qš¼ù Øõ‚·rµ%‡1ØÁÄ­(ÃWW"=‚ίSt’> u–j9ô­ûu›Çƒ§bÎb&›Åk,Qtúz²há…2ÍX,­ƒk,pºl‘Êåk°§ÛÞ™bJ¹¨N‚ÖN ö<;Xp1W«Ò÷–Áœ¥ùìÐäm!{éÏ¿©Ç ÁV*­è{QCJnࡌ«;ò+èiÝk<Ïb²=TÜs5uýbtî(&¡-Ý–Ÿ4¥†I«ISEq0y¡q”™èÇr§+ Ù1xë8oØ—©VH@™|Ò;¿iä(Ó?0ùò^mêééÁÙ³g r ¹V†¢<,ݨÿÜF C©6Ù)Cs&$ÒúX8wîb±ÆÆÆpöìYœ:uÊÙ•VrPæß†<þmF¾¸ýA]¾Õðóšù{l’3޶94ðÁ®éªÈÖòiëÌX;‰ðZþ^™¶ÔWœ· |Ç#º¿«ë{ СÑÄùïcN“Ÿ°¬>J^?€ÐºooíåIrCôÓÜè ö¾åŪߩ=HmŽÀm¯í=Òáa¯UF€‡JÀÛy¼Ç6•_Æ·n_ÇäB¦4qè„JS ­\Ø4 ;lú»1Ø3’ÈÀÇÕ¿²³ñÌ+8cMñx±ò‚@°‰UëЃùßž9zÔP^Ÿœ0¥üJ,ºPV®ªÍaÇê>æmº¼¯vÈI2UÛùD-Gà2yäe…½vÃÀsÛ£9vгjkf–O°ƒãæÇ—Ùe/×ô½KIìÐ`ûóT0t¡ÝÅÚâr!ÑÈ#ÿÁÞÌ¢-Ç N~oËhä`æoXšäf¡¥né\Å¡) Šä@j`ïMÖqsi‡ï3d*g¡Î¾ iä ¤w¾eò¿A+Ì;¦¯V!‡±±1D£Q ä@r­˜ÓT(`1ä`û´éÜ5ÚHt‡¾¾>r|©ŒÂá0†††pñâEhš†W^y§OŸn˜èÒ­¯£0òEHwþ ÊlZq¡®óšš¾MNÕ”‡¬9Ðs×<ê ç5ö˜ÑyvæÚÙ£<¨ùiG¶Eý4À{+‰÷V#th\±Fï€ô̬eõ±2ïrR$ɵ}+øôEïÐéÁ‰êîî¶•Š&àÁab…|íËË8øÑ‡Ù'ª €Ýäß÷ á:ý… |+:ª vضÜËrm°ÃVˆB×ÊUö¯l‹_ÕБFgÓ#Iá6D±>”é,‚HFu¬#„6‡ýðWm= г{7s}F Äs™šËߨ¼¢ möfq§:Ø <¸vÐs®ÖôƒÙrp¤‰Q¶ùÖF¿2 Ú¨ý;;”ãõåaÙe#¾M°ƒÞÏ v°£ιmqËš£qÖ·";4I*ʪº^Hô#òhÐÃìehóW™ªÀ 9Xâï;.øµµ*àà¶EÑ"ÀAsQ»H¶«ö7c×±ï5«üYÇÚ½pòè7P|ûóoÿ%´Ì¤cú´¯¯oä‡ÉÙI4Ÿ•“’Ûá¢=Es°½†Ë7íés© 500€ááaÄb1\½zgÏžEOO³+­ä ¦®Ažú[¯ŸAñúÈãÿÊü[€’uô¼F0V£Û p°ÿYU‘â3:ÛÎF¢§©Ð¥Jº6Fv†ançt¦Àœ©›$n§©%H… lƒv ‘çÑÛµ"§s‰¬xÑx³÷>ó€±(/ß¹c¬ü JMŒòPÑ6 F·ÀUÛ©?’@AV¶Gw°vEÓ ¨ªóa`=ÊCͰgmvpð¸w‘ÜtéÝU‘È·¬+ƒ`;$iúÁW'4Û±c“z[÷áÜ¿RS–jì"´l|ÇâlâÀäû ›–º -?«#âƒC绦âà¼ì\PR¤.Â=ôg¨e& ßþ ßú×G¿uáªcúäÔ©S8þ<‰Dr 5¥B¡¡K¦ÚbÔúõ@k $’êííÅÐТÑ(qþüyœ>}Á`ÐÙ'«â”ù·!…‘/¡øÁ× Oý-Ôä5@ÉY77-St«‹Åp¼%Èž%4NÙA>mËÎ\{„-?íÜöx»à9ð›à;)½íŸ÷‚†°»ž¿¹cŠêà‰ð  ȧRš¢:¾žR6ky éÁÖ€"MÎê+t ÷a[ÊÙs<Œ¥™9¶‰7—„è¬<ˆ»Ž@JMCSeæ:åTÿÏ­·ñؾ]xô`°ì²¿ã¢/ËÐxÇU ¶ï"ªç\! ÓR·s:Sv2åC\8 vXÏ^/ Ià4`RÍ^*ò<Ùµ ÑùDõ!ƒžØ¿ûÌ0n€^›œÀé“'Ñ&zk*£’…´¶Õn¶ª’mn€tµ“m.Ȳu¨þ•¢¬¢ÅÃ×6³[ ;¬ vPŠ}Û„×*ØÁýÜ;Pt;ÊhâÇfä[ ´>›è¿;’ÄpiÝÃ7áû8¸Íý>¸÷1D3³xñÞ g©Þ†ðè øõ{{Ýß ¿ñW">äç8ß.Àé0ŒÖ$ÚŠvi.mÉ1êí5ð#9ë÷Ó—©VH@½÷}¨ W¡æÕw§NÂÀÀ …È™I$”.À_ºt‰mo¸t‚Uë\Ý ‡Û=e§ ]X&àDªM¡PƒƒƒD£Q #‰`ddÄÙóFî.”Ü](³¥9Ÿk¹|û1ðmÇÁîçÝeNA47Y®ññq¦ïs'Lp :ãÒ¾¤¡·NnoˆÃÍØ˜væÛOB óº¦å§Áx›¾=òBØõq}ky¶ëÄù÷3C9™™Yø-€|å‚þ—è¶íÝkR™yxG÷Qjœ=ªçiaN㻊‚7Õ»Þ·JË>xœ Bô¸hÛ4IS…®qœdYv\ô>Ä~–®á8^DË}®×d~g®¾Ära[”†r‹ÿZ Me3VP)ºCƒÁë§N…6tçóAÄÍA"ôÒχp{{íg¾-—ÔŸ:Èb*#Iøáô´©g΄áÀôÖAµð€ìØÁ‚ˆYa´gJ¨!›ì¥ªà¹R¤ CýQ%²CµÈ º?çØÓ3Û`GÎ=ôfüæ,ìù¸YúC#ÿu²Tð•§Gó€sG~}ÁBÒ*y¨7†Ù"9Ø.…¯‚ÉQh™»€*9§“(’ƒ¶kðv™%±•9I4¥‰³‚Œ\”×2“ ê~ú2Õ (Ó߇4rÒ;_„2ýÇÀ#9\¼xƒƒƒ;H400À>3d§Ìãu[>t¥‹î`$š‰Dª¬ÞÞ^œ;wÑhccc8þkKµµ¥ÌYÐÔI$k‡188ˆ‹/BÓ4¼òÊ+xöÙgqøðᆨ¿V\€2ÿ6ä©WPüàk(¼û,¤›_‡<ý=¨ÉkЊ úòY¾MóSCÛ p¨ËÂNU¤†4¼Ýig#rXß ï¨ö’§»Zœ·‹9 ˜àt³9r‚þ Jðμ nå¥`rË>§VÕÖ€<8¬CôJ’ô½ÝnÏ1ûç{Ž…Ñ±os:=QÀÛuÄ0ñ6u /½{ge vXÝ(H2´ >‚Xr³vX›ÙxÀç+Á$RÞv²³"Ço>Ö î@Ÿ0åáN*…ÛKISê°ªD¾†Í"K¬´:ûŽCa–èæÁu·—‘¶ð\ zpÃã ‚¬Ád/§{'Š|¸ö3$'.ù!ÑáãO#(#Ž6…:uvVÉhé h©[¥73555Áêv9fâ6±]Ž2ýXÞì ‡Ãì^c6 PÐarG¿âÛŸ‡|û/ ]ºµB9HÆe$ʃºh0­KèÖÂ3šC98V]¾Åœ](&‘ìŸ[Ï;‡X,†±±1œ={¶á"­¨éÛP¦_[‹QùÒ¡Ì¿uù6 ¬ÿ¨&¯AM³FöÖͪX,fÁ:Cƒ½‹:AÔ·˜ÑývæÛ›x ¨Í!#ÀCÊ¢r¾ ë{þ`‡‰eæßGs£ö>cåä,¼Óo–æ.ÁGƒ<4îžEç[›}m­¶ÖëÀGbŸ¬Ò }˜áß÷ áºå_‹âûÄ·çÍí´QàJ¿ÞV]+ìÀ¾}Ù! a‡Úk»å_¬†6J¯·@H½Í/··¯Ÿ MÚ›Ë2åᎹQ Gx`µƒQ‰vP5 YYBZ’0ŸËa>ŸÃb!©¢ ²¬ÓNœé¶V˶ÅáÓ°åÁ1Ñ8öô;ذwI®™d¯ZÚbCdvȇmVV‘t7À{èh²A½­û0|üéš²Tc¡åâvNdöL6jÈÆ¡%G¡eîrÆüêkuh—ý ,‰â@‘;tšUF.e©©Q‡ök•LË@êÂUGôC__A$’ 2r^e‰ð`ûšÔà `Ž# e§6]0¶²ŸI$’y{Ç¡¡!D",..â•W^ÁéÓ§ EØ©«”Ü!ÿn}…‘/¡ðî³¥ˆþ¹m{ëf•àܲœh9Ðs„†}Ôáò†8ÜŒÍgg.pZØMÙ ÐÍ%Nl'¶1§KZ=dfõEŽð™¸WÖU7hÑ(ZjÊSðνëä¶ãt Øa¢Wz#<Ø­½3§‘Ósú'@'¼]G ×o2¿Œ?|ÿmÜŠ/­/e`‡µ¿­ÞÑ—åmЃ°ƒþM g KY;Ü)eÕvX›åøôàñ”ãAjÖm"ÛÜÖÖ†Ç9´\4­²ª²CFÎÅkäL<È×Ówt\Ì.( –ŠEÌçs¸—Ic2½Œ›ÉEÜN%1•Nã^:ù|óù<ær9Œ-¥0¿)â†qAVU(ªfì°å;ÊÖ¾Ö¬)ÇÔöp(AÓìÀþ9ÁŽ,ƒ¹Žìeiœõ°ƒ]"Ø¡ö3$çQ´5Ÿè:ç>QS–Êÿ u> ­˜´p’¬ãgÅ$´å˜ñ¨UW‘u° rpS»¨Ê®“‘K£Z-ÀƒÝÑ 9ôôôàìÙ³C$!ÈD2AF"<@ÉAËê¸ø`ëšäÐ¡ÊÆH]¾Éœu£½UžDr³B¡0<<Œd2‰«W¯âùçŸGOOOSÚƒæ'$p¨ËÁœªH qƒÉÎ|ûqæ4jþž£ÛD CóÊH”‡Ì̬kÚ/e³äå¶ŠéIð¹Y2‘LРûÍ™•cO>Žÿþƶ¨2ÔB¼O¡æë:%»%gìû«K³øâ[—ñâ“OàPW`Û†aío[ïèËréò<Ç9 và˜‹®þiÀì3¿Pê?Ed…&³NvvâÊÜ,äro¯×5Ùq·>wú£øÓëט³y}bÏ9fZ»ùvûýúÓF¥j€`ÂHwø9½ (ˆg2((ìóÌü |Òåo©©Y1ÇNõ†ì,G ¥õ¡Ü‹`G~N°ƒ-TÈ^–¶ƒs­vhN6IŽÞűÉe7øÇ™CO"š™Á« ·ŒåWLB¿Xʺ¥\èdéOKwC:°–‹r¾ü¿++ÿÎûÑð>ð•­>×ùp L·€ç Þ_U7™L‚TY½½½ìÝU˜‡VH€óí®ckê—€ºpZê†cà†U>|¢7“H( ¡§§###LéÔ…ß]—å´Á÷Z{›´%và¢;HÎÞWöööâÌ™3H&“¸xñ""‘.^¼ˆT*åúö‚îH͵V’}ÉÈ—m6#Ùy£¸Ž@ò=6;8ÂMîÏþû€ì8Sšäøîü§M«CžaÛ¶o¯©í/¤ÓhÙÕéȾ™ûà{ æ]Ï÷Î]…êi/-;Î6ÝÝÝýñxï†oÛf˜¶9±p—ãô ”®èÛ¢à…Àbôâ:©Ñ·‡†”~A@¸½·Mzpù‰C‡0|cƈ=ßùðž9zPÍ¥º#<ÔzfVU@¨ñ2›æß©P‡‚¢`ryjµÉ©ÂÇóù<:<~xxãìeY­}Ô!ˆ<¯Ë^µ–Ã4¦õ¢H[ö®‚ìüœ#Ø¡áËàÈ^––á¢Ç•äà ¶Ÿ±ON@ÕÏì>žMUÒðñ§ÑýÛÉÔöæ-/ÓÀ: ®«Wü`½ÓjÅ$PH® «ÿZzÜp¾ê˜Ž™¢í0 ¶‚@{À‡J@W˜h¶IYsi»Uåõ/rþÝÐò ÝE°^xmF¾<Â}¿j³[V€2P®–@‡Ì¤£l 100€ÁÁAº¨K"Ù ö9m1 áÀÓuZRÝ:èoºÌ-ÓD"×îÑ x`S$aO$Èp͸!ÈÄdF²óNâÛO€õu”Nt #<Ìšûæÿ|jIÿÆç3µl)“…¦¨àw¼,÷µÓŠk²èWå*çU'VŠxðµµÚ^¿‡žêÇ¥o\`J#§ç˜€ÞㇿûAäî]3\Ï?›|GÞîÀžx°:ì°ú©¦–.Ê‹"ìÀÝ Tˆî`,°DõO9ã›(»¢;‚69x½€¢”úS£å ‰¶†5í´¶!‘Ë#Y,Üy¬GyhóxðD÷~¼>9Á”ÅL6‹h"Þ]{L±ˆ¬ªHäó•£<˜1DjŒ;Àl6[ì°ê#i©ˆNŸñþP«bm¤ ŽãаPuÝ_mƒìò{ξñbiî;8}½l>{9TäÃd¯:«CôbI©¾on<4^7?xØä+!Ñ_‚®})¥`NÅ$ÔÙËÀìåàkœUËÅ×Àm9V‚r3õ*W€ WVöÓßÙ°? €k ƒkƒóïYù{ ’på„OQl¨n…/ú÷ ÀIŸúûûÙ/ßûïÛÍF瑳%ÀaiêüU@É9ή§NZH$’½sÚ /¼À6Ëd§9 v]¤tÁÁG«­MZvÊÐÜMÀ‰Ô¸sóêøÅbˆD"®ŠþpêÔ)ŠÞeƒ¸rјHîÛƒà@b2#ÙYÿzZ˜÷àj6>Pÿ5Ž@ÒFñþýÌiò©%äS)øƒASê è}q.±Ò]3ƒ*¤—Mk‹™Ê&êÿì^㧺n/€ˆðP½#'à¡:øÑ‡™Ó¨Rj! ÞצßyÛöÀÛuÅù1ÃuýƒÛ—áx|îãAUØaõe¹ô?zCÎì`êÆ­fØa£¡ôG–Kðn?æ˜rÈ<ÙÙ‰Ë3qSjôÌÑ£ÌÀ¼|çz»ö˜vÆMäs;f M+ýáz±šïT©ƒ¤¨ÈÉrmíX¹\_šÐqA^’•ÚÆA—ð½‚И°Ãª<"P(Ú ;X0.´*ö² Æà,ΟÔxs(Ù«Ü\ïŠ~§È4«(À{Ð!ø*Bm‚‚¤Ê~ÒÛºü4~ñú·Í/sü,Á{?Î[Û;@´b²5äâвñš"5ÔUrZò'Ð’?Ù<Óø÷”¢C´‡Á‡¶9UeÚŒÕ[ýýýxñÅÙº¶055 >xÒ6ÐAMÝ€¶4êÈ(«êééÁÐÐ …ȹH¤:ÍiÁ`ù­º8~÷Çi-gª¾ñö¨Ë7™Óôõõ‘ƒ“H.P8Þý!®E¸téRC¶ihhˆ:–QÉd’Œ@Ï\öX€žmØcJ²s-âÛCM¾ÇÖùi ŽÀ¤}ÿŸ9 IffÖ4H =£?bDë¾½¦·¿¸œv$ðIÌ3§îBëš·8ǾDζ‡Å<8¤#X¤Öú–l‹µçXûö`ifŽ)´4Íå|]G d¡äŒsŠŒ¯Üú1··á×9 a‘€ª_Ü%ØÁÔÍ›©°Ã¦™P,ŠBàƒ{·ƒ¦6ý‚€p{;bËË5×êX0ˆ£Á î0þ@õf|ñ\Ý~sÞ*Ïfq¬#‘ç­=S+jé ú¦úmò=ªÔÖÍzÁØà V#Š ;Ï•ÖxEµÐN:Ú¢™ÙOî(ºƒepÎk‡›ú„lÕ0{>ò-sÔím… qX”·¿ ¦Sô£ËÓBGêÂÙ#¿‚çÆ~`]!ÅÔÙ·€Ù·Àµì×Õ ¾«ª¿¡g5jƒ¶ƒ–ŽfE£pêT›Ÿƒ–ŸWÖ"Bpm‡Áu> .ôøÎ‡LŽaòDâ˜y‰ ‡Z•L&éây {„1öWà{^°ÌÖ‡Ô hK7k¿Ã‡¯]Ú£7û’HÙö÷ãÕW_eJ£.Ý´xhàfM{´¥›†ú•D"¹O½½½èíí]Û»¯FˆD"ÌQÈê¡Ó§OÓüd@Ñh”9 'Ò32Wí=(’‰ÉŒdg3ÄuœŒõª/u©‚xoFÿLÏÌ¢ëÄqSÊ— õý}§°œ†"I<κÜ/e³ìc7÷z¾ƒ#<„í*ˆ€‡t‹œ<ÀÑ'ÇÕïü=Ûd™žc åþǪ”7Tׄ”ÿùG´ú }Ç÷UŽî€Õ‹vP,^ïÎÐCƒÁæm vØØÉ«àÃjÄ’[Ž6¦:ÃíHäóHK’‘ÝÀ­ðÌGñµ«ï2góúÄNŸxÐ4+%òytÖž«… xhØ¡övlöQ/°çáØ_¥>v:ì°ú¹(JÑ:¿Ôª´…`‡=Œ`«Ëpèã5×ô çŽv8¶ß vpªöx‰~äT ’¦ÂÃñhá=ðp<‡ÑW†î{‘Ô8^]¸e}ur3Ц^‡:õ:¸àGÀwõ‚ ܰ·ÏCMŽBK— SÔ…éñR$‹É¿‡‚bÏã5D€ HUåÚÛÅù÷@ÃLi¢Ñ(]@Ò¡S§N1_Ö2“P¦¿aÿ¯šâ@Z!QŠÞºu᪣í 100€¡¡¡µ z$É9`£NÓA§bõÍk“ºÌ¾_§õœDr¿B¡Ö€ÜX,¶ €wVdÄžžœ;wŽ:Î&qƒd„FÞwà@b2#ÙÙ ñí'ÀzËK+Îjà½öÍ÷ÔU$=~âß,]gJ“œ˜Äa“Êg‰ð:dͦ¸œFË®NGõKjœ=*/'˜ mh:^PV'…í*ˆ€‡t‹dYv¼áþd?3ð Jy(ÙE¶ÉŠãE´Ü÷²“ïBSÙf"¿ŒsåñŸÅ_ÂÏÙµ9ÿ­°ÃÚ Rz0¼C©ìÀÕT[¶Ì;lípgsÄR#o-;ÇŸ uâÊÜlÍ5|êÐ!üéõkÈ0¯MNàôÉ“€jΑh*“Þ 2„ >?|üiô_ÿ6F2³öU/uJêàiײPòÐ2w©ßªÙm€Pñ@ €ßóx)úÞÇwˆþ@QoLšÜ6ÿ8ippùr0PŠòÀ ð{ÁP¹jjÚ»%С0ïx;>}zÓ%<‰äLº¯ä e'M¸Lé6ÐÁüöhÙ)@ÉÙÓ¯$©¡‡×"iÎ ‚Á †‡‡)šœAÅb12B3uðs3Ù,^›˜0ÍZiIZXaå˜Ó¥H«³ï0^ÌöðüÉ~\ýÎß³m$TÒÒ4<û •éÛsª”ƒœN®÷?.” ‡¿øÅ_ÄÁ]ʰÃZÅW ¯à8ÆMKØÁ¢-”yQ$\ ;lÚIð¥?‚Pº°­çÒ¶žESUQÈŠu¥Á^‘‡Èóð‰XØvl¨Á×ÙÕ…+s³õF.(£î@¿Ð½oÆÙS#‰â¹ ºý­¦X.žÉâX{ÐÚKëªZ²7g~_˜â;5\J÷ð<èb6›ÅR8Ñ'à9~ARDã·CzlQ᳈lA‚Rm51²´î ™4®5sÚ±y§,²¨5ÚR³xŽÒªØË2ƒ³4‚ì(ƒ#{YZEv &‘¬ó“®8üSø­[ß%[6¸ÔÄ qÊ­ à÷<þà'Áµ…dŽÑ\:îêÓ.®í0  544„sçÎ!•JsŸ¥P–n4lû{zz088ˆºÀF"¹@ýýýxñÅÙæ±å›€’„j—*ü@£Õ·=êò-CýI"‘Hzç‹sF,C4E4E$A4eÞïƒA bhhˆö‰&(²Ÿ…–&²FU¤†¸ÌŒdg'ù3gx(ÎjÑÐÛôw¬uÉ ?òvY¶¨QfDx ÝßmÛ·×RH™,¤lž€uphzf¹…X‘O¦Ð~_7üeÀâäø¤c|C[ÁËÇùlwww<X]:À‰õ’e¹al¸çXûö`i†-jƒœž3 <€¿û!d'ß…Z0ÖæîâO~üþÝã=8¸KǤÉa zàV ÖmŽQØcý”s>ìàh­‚¢¸> 3UÃR¾IÙ~I~õߎC‹WDÀKSµ“åëb4ÉH–jÀ­ûÎ'bàÂè(¾øÑŸ®ý,½’~*“F¸½ÃZ£)Êæ7ç»èyÝVìm ¯”Öl/À³"©v¶æËãZ<¤+E„2vð¼‚U}Z'Øa­q"Pj}¾R¹-š™þ@°ƒ‘¹¯á˰jl‘½œ/òa²©©ÇîàÞÇüûÆ )²k£KÎB¾uú¸¶Ãàþø=?ˆ­9ØRÝÆœÀ\¦iV…B! á…^hš6ä@"¹W†Ò©‹#àwÜë /Ÿ©Ë7 Eø!àD"U8F8Þ´.$“É5"™L– ×ÛÛ‹p8Œþþ~ôöö’!M”‘\à Ë­B5„ö™${ü™óuóî‚V\`ÛÇçï„k®1$3Åùïð.SšÔÄDÍ妯õçá· âØVeó2xÐɉ‰mQ–ïÅ˶ÍP„‡@'9²¢[´ &µ†7ˆ×C}²—‡ÿ†)œN@•òà=~c>/"pð§ùðGÐTã€Èžx€ÿûç ]í>];nK¤½[ó„*Ÿšº»²n«æ¸èå+Y‚6F|ÐYéœ$#]ª~]Ñ4¤ ò²‚ ß §í±%~i‚¯uH ˆØà¬ê‰ýû±/À c?ŒOã·å"Ú„ˆó 6ˆç²õ4øŽ‰·yŽC@,ñÀjØae ¼ä¤¢<˜ ;p‡Ÿ×šr,‡t|¾ º݃iUÚB°C™Ü vh궸Á^vEv è$4NH4¡éa ë¢™\œ¿‰‹ 71’™%;7º›¤Ç¡|ðM(·à»ûÀü5pþ=.]8ëYe‡µÍ@Çb10 ª5ÊC#ˆ ©ytêÔ)¼úê«Liä/€‹ÿÝ¿¼>¸m¯O{´Â<”{ß…š¸Ìœ¶¯¯œ™D"™ªP(´-É>‚ÒÅ€ -A5Ämf$;7Š?s' 1î˵ü4Pð@7¹HVˆ÷³¿,<ŸZ‚œ/@ôû —›O-éþ®/d=ð e²È§R¦Ãå`‡U-ß‹ƒãøÚÛ6|Ÿ=ÂÇ[s5_üŽŒð @Äò±AÓCÅpœEi(#>üIciiiº¦rW¡‡Z'ŽoM¼‡?ü§«È媻–µ¿®Bo³Wt소ÂæÀœîº²–×°Ãæ —ÀŸðxª‚.9IÆr^bj£¬¨XÈæ!7ðT‡Î¨ëùÿd¨mcù›ëüÔÁCÌåf$ ¯×Bén±A^Vj7ô•©mž#];èíoóËØÜŽ6ŸÏ`=ÙÚÑáóAàyó˱vàô¥÷ˆ5ö#ì éh›¶ÕV\ éõ~έû™%ùì`O9ûši—½èBzsúpƒíù\ï[¤†õ“ÞÖ}8sèID{?ƒÅŸ{ç §vGPð‘ÝYrêÔ÷ ÿÓç¡|ðMhÉŸØè¬v-Î&WW3å‹õYá ###4N …0<<ìºvõõõáìÙ³C4ÅÐÐÁ$RÈèEV-;ùà Fþ=ÔÄ?ŠHP×õ~ÇuÝþµ]+ÌC{ Ò{_6;ÔÒ$‰Dr¦š7ƒ³ÏÛRÅ&kˆÃÍHvnTæÛ³×"oüŽ"Á$+Åy»˜ÓÔå!=£ÿ…Z¡CöìaÒ3³ÐÕÔüv‚Vµ<= E’Öþ?;—`.GðµY4s:öÊÈŽB(ÂC;€Uá¡£{ô>„©(Û²òÒ4|]GjÛÄøÚÐrߣÈN]­)ŸoŽ—€?êû¼¢¾]‹¦…Bé¼ ì¸Ý!ØaË÷ vضR ›#>l¯²ªb9/[¬4`1[À®€Ÿ"=˜uŒ°À×ÙÕ…+s³úàm{ÕŸ9znŒ2—ûïà™£GÕœ ¬±ôº¿MDQJ°`]¶®qÜ|¢¿("/ˆó¨6ÖÚ}>øŒDë¨;ìÀŸð\ ztÚÒÖ¨.ûœT÷µŒìUǹÞI}N‘h,’Z!ÑÁ½apïc°ùáâÂM¤äBCµÅÏ‹8êAäx(š†.±eÛç—ÒkÿÖíiC··yUF¼˜ÙôÝ6Á‹ÛùˆYS*†¿5~ \è!G> .ôPsOfš;Ûŵ†–gJ‰Dè²$ƒpúôi\¸p¡aÛ Ñßß  QÇ’HM:Ÿ=÷ÜsÆ—ÒÂä_„¿ÐýKºZ`ͯÿÚ^KD‡­¢5œD"‘Ü%#8±¥A[KQ¨n2%ÙÙ-þÌŠó€Zx¯þr¨3I6ˆ÷ï‡RœgJ“ž™E׉ã†ËÌÌêûÍÄì°oæPT,OO£ãÀý5ç%e³È-,ê,3¾udóÌeq‚Ç{0ÌU6+lG!<ì,LÒCOõ3ª”‡´4 OÇþšÊðw?ˆ|üƒšòùf¬ô¶´5èÓ¹‘‘¤ÒMuQÔ·å!ØÁZTå5ðaÉ ì°¶`i@*_Ä®½¡³æ£„E¾æœ uâú¼¡òÛ<|âà!¼>ÉFÜÎd³ˆ&èݵÇäe‰|»ý~ëºMQKq¦¸:ûŽ)‘ª}nñåý etø}³*„£Ó^í>Z¶F>p#ì°qn—@ÕjôÌ-‡¢;ØQ†#;¸©OÜ"Šì@¾ErŸ tÀ@× €[æ?øy{<x¹D«@EJ.`QΗÎ&ªŒ÷³loÎÄÐÓº"Çc·X‚¾C¢£¹Ò™Î‰@„–ü ä«¿oø ¹x,5îälxˆF£tY’QçÎC4m¨===kõ7‰D€p8ŒÃ‡c||¼¶Œ””»ß…ÿŸà;{ Üÿ48_W}§9s}7tXÍé$‰ä.Åb1ös`CEx Èâ63’ÝæÏœ¯ œw´âS:5ßv¢zþÔ™$e$ÂCrb‡ –—œ˜ÔýÝÖ½{mµEa9|*0XS>pAÊd!e³ðHˆœÁ[á÷8ÕeÃvBÀÃÎêub¥äÕ·47Ž=ù8.}=€B&Ë”NJÕ<XËÃ è¡ Êøƒ_x]í>ý›™Õ ïÀUˆîP3ì°ss`ýijØaÓÊÅ^/ ªÈå M¯$+*2E ­^H½Ûb_Ûí÷#ÜÞŽØò²¾òUà×ÿñ‡ØxùÎôví1éÂ90•I[ <¨ZinøúùN#Àº|ºr-~,ç‹ë‘ ÕaÝ^Ç¡ÝïƒWh‚È[«&€ª×Žœú±~ÎYœÿ–r,Œî@°ƒe8ôœkú„#[YZÁ$’YZƒ°?\˜½V·úäU“…%¦4}}}Ìå\ºtiÇÏ*A ‡}Añ|ðó"b…F³óH)õ‡EŒn…Ü3!smì?GE" ÑÇ P(´éÐÃÆ(ýýý‡ÃÔq$iûÞn`/¾ø¢9™)9¨‰ËP—Áïþ„î_8à€uß ƒÑ}-‰D"‘œ-fQpzt¨!n3#Ù¹ü™ë8qï®å§ À¤ºø²ŸýÞlŠZت̌þ@µíÛk»=Ò3³}~ˆ~c/Š–óHŒ÷—3‰y´î¤lŽ)ï±î¾Æ M=.xp¹RÓs8`#º!åò(¤3P7\ìöµpôÉÇñ“×.1å¥ä’P¥¼)€§c?”lÒÒtMùüåÄûX(ðô<Œ‡÷…ô_VU (^ÀñÁåÃN˜;èË`‡r«¬‚ô¢©¥7ÛïÐhMÓPPH²dE…Àqày‡Wà‘)Èð‰DžGsk³·d²ª¢(«PU ÊŠ7Úàð,µ_¸½ÉBÉ"û%˜ÞÝ»q4ÄTŠ)Ý›ñiÄstû[MiC²P@²X@ÈkaDYo}|§Q`ÊàÀ¡ÃïƒO‘•$HŠÂØÖu{<´z=à8Î|{9vXý\JsxÕˆ\Ì%ØÁ„“ŸKÊ0igGö2õ©ÙªYÛB"5¸Ï¯Âçø\œ¿‰áÙk¸”š¨K]:::päÈtww£»»mmm8vìàØ±chkkCww7NžÌ <@q~ þîM©ãj>µBã·‘S$œ~è#è¶ äõb·ß.¿¿Âj®tÁ½P'Š€(š¼3âLÝtì`®dE-]¾çy|émþª (Ê&ð!/ÉÈJò6›(šEÑ )*²<pÀÞ¶4¯Ö½-'ÉÈä5Àa«Öì'¯^P^;»(pàÀÁ+òàÀAxxxnûenzd×.\ž¼ñbôNþ®q·þá3Å×®¾Ë\æËwîà·~Ì´sfly½]«uдÒåq[£<Ø;À°ÃÆ<|¢Ÿ(@VUd‹ ²­ê$ÌA¿WDÀã)?6œb/»ä€‚Z¡~{ý v°áôÛÌepd/KËpì@>ìÌ~!5¶ÄOB¢ƒûÃà¾Ç˧0<û†g®Ùz‰ii wïÞŧ?ýi<ñÄe¿Ç fè!¯½)}§Kf‘Hd ŒˆD"›@ˆ”R@¤N`H%©‰+PWÀø$„#ŸÄ€ ÇŠæúÉ‚ ±?G2™D(¢y—uþ[ñꫯÚ^~__ßZH$’õ÷÷# n6M]¡–oA= .pB÷/ßýq ×ç¬óVƒ« '&‘H$iõå ,â¼]v0§*RCÄŒdç¦òç-ÕàÚ³g!§¡ÉË¥ æ«ùP“ Þ¿j–-‚TÆࡵÀPô e³†Êœ¿u‹9è$çµHtö‚2iÎ^ïí€ôF°‰D"¹K†"<øê <ä@ q›ÉÎMåÏZ幕óî‚Vd{®­å§Áµµè@r”8oÀ<$'&±ï±G˜ÒÈù2³ú€Ñçƒ?¬ßð_‚î‡'`j£±€ «â=ÖFaP?x%ï8íîîîÇã–†¤æiZ(kø°ë%{õˆª(˜þÉ­ª°ÃªŽ÷}ÌP9ÅÅISëíï~žŽý5ç3²4‡¯^½Š¹åõÉp>ŸÇd?ŠÇñþÂf²¹õh“ñT@*–þ[mábý”`Ç©X dä4 n%„~ÍgòXÈt¼¡ÝU[¹µ¿-æ 5ÁÕ$)* ²ŠtAÂ|&ÅlE=åmPÈçC¸½]çùÍ#驃‡˜ëœ‘$üpzÚT;Ü6ó‡þruPU@ÕìñÛ`/×[œ‡WÐêõ ÃïCg‹íO›Ï‹€è­;8!ºƒV‡þðˆ%xmÓç;°|N°C“·Å öÒ8Xþn „Æ ‰æ kç—*õè:ÑŸþWøÇGÿ%>Ñù¼Ç–ê]¿~Ï=÷¾ò•¯¬ÇqåÊȲl«ÙÂá0pæÌD"hš†7Þx}} +g¡Ü~ ò¿-=î<¿ÓLpІœôµk;Ìœ{$¡¹·F !âôéÓ¦æÛÓÓƒçŸW¯^E2™Äðð0v ‘H¦Î_===ö¬f…Ⱦ„bôw¡Üý;@ÉÕp¾uÎz¯æ!½é½/Û;ƒA “ó’H$’Ëdxèøˆ#Ï¥ ptvÍ3R-f$;7•?3Vƒïd?#iùi‚HŽç¿9‘ ú© ýQ­ƒ‡ÖfRT$Ç'‘M$ 1ÞåcÕòtœ9 /ú-v Á©.kùCoÊ+ìÈ-çqT}ænÇ )Š~£þ\/<-ìo —L€ô ´Ô>¾V¡‡ñDfÛgóùn$“x3ÇÍd‰ü0„ i@±XŠø°Ã…u7ÂÍ(µÂenUÕ-Ê%ØAKt¾í^Õ4¤ó™d $B ÓÒv«¼Ç©.kùf"M ´à½Î™,’º#;lTøg{qëÒ[líVeHKÓ¦Deب–ûCvò]¨…Ú~YšÃŸ\¿ŠÿýÄ#xô¾Õ1»~AGQUijYijYˆ<Ý~?öµ¶ ä]?dPÀãÙô†#°ƒy²îW³Ew¥Bƒ³Å-—çyp* ­¼ù¾Búœ,Ã+ X̰+à‡À»AYo[NÒyAÞ_Ëe¨š†¿Wwù'C¸27‹<$Öàº÷ãÍø4Sýf²YD ôîÚcš bé%t3„cîUU7ìꢢ"“—vŒ"pZ<"^OmíÐ8ë}ÒŒK¶z [¢a4(ì°æ8À);ÏѦÏ?vÍïœÅ¹ìÐÔmqƒ½ì€šÞ·v ÑT÷úW†ƒûÅà¾GIM`xæ.Ì^³¼Z.\À_ÿõ_ã _øžzꩵÏçó¸rå Âáp]/Œ%“IÇw­:õ=h‰CxðsàB9`h4THʵðPo…Ãa ahhÉdÑh´bUø‰ ‰T/…B! ãÌ™38sæ .\¸`OÁJjâ2ÔÄeð»?¡û—ÁµpüÚ¯æ¡Üû®mPΜ9ƒþþ~rX‰Dr©FFF˜ÓX<4À™Û5衱=f$;7•?›T ¾ý{Ñrš¼ Nl'w 9G¼œ· Zqž)Yzv!†H ÉqýÀCðð!G™H‘$,ß‹#-ÌÂ×ÞO ‚ÇÑç'ðÐr!o(ï|*¹Àð¿åmvÚ‹ó7¨ÀE+ à46[ÕÝÝ}ÀóN«W6ø0–oþO¦4üu|ü·~ÝôºLÿä”b‘9]f!‰¿áEö¹ÛãG둟7ð«²)ÐtûZñ;üÝ_…ÐZ¹8à„ü Í³2 ‰" à8®zeþÉœè\Å¢ªÔ¤ò÷›v€Ù¥ß¢¸ÉU¶‰¦•.ƒ«êŽ—j½v¿¢À£³Å[ÅU›Û4»¬#T¶Å¾Öî÷ Å#ê.?-I¸2§ƒúÜ!šHàß¼ùCæºõìÞ³?ÿ$`"¼z,ÄÖ6ë¤øÌû2™jQ9Vê(ò<‚-¾òà°ƒÊ ØísU ²õ¯b/Ë¢/p–æO°ƒåpd/kŸ¸£Ž.ƒ`RË­~ÂUnh¬Â¹{?ÆðÌ5¤ä‚åÕçâJ³ IDAT9rä¾ð…/l»T …pòäIøý~ûMÔ`gpþÀ'!ù4 lö}?CréÒ¿`ÏŠžó“H$iu‹áܹsF*•²woÔ~ÂýO¯\0"Ð@‰DjE£Q|ô£e[3;NÀóà¿­ÿ¹´ŽÎÔ—‰òg›ª!ýä¡eï2¥w÷o;AîAr”äÄ%¨é›Li?ñó8üä/èË?_ÀÎþ'Ýyÿ/ÿ׿kÛÏß¼…÷_f»¿/´„8øS–ÖË“Ÿ…§0çD“½ÇÏXYOS‰yCË‚ uWáŸe®JyHKÓ¦·…ãEþx_[ÍyÅ |íú»¸ÛpšÉÎMåÏ6UÃH”5?MnBrœxÿ~æ4É ýRº¿dˆá¥gf™ÓNË륊­N5™å! x(¯°+%{CÌi¦¢ï›^\j¹6ãþœ1¿.ÎYbW³¡‡?úàmüÝ2‹F…û;iIÂíT ?ŠÇñþü<ËË@±Xæþ\Ùÿ5vЗ!Á&->,»÷UðÁãqø É¥ ÚÙ¢Œ¢¢ºÈJ½ø†Òð,”» _é2P{B¾*€¶\l?ý‘“†êwatÔÔU^VUÄÒËÖöƒ¢˜ÖŠ ¤ós5‹Ù<UÓÙ‚˜Ë©%]u䬴-mñl™“ vØ¡Šîдk¦ö€gOt7ŠNBj&5Ëïpk?B•¯!Ñ3‡Ÿ@ìñÏáü‰Oá°ÏÚKu¯¿þ:~ã7~¯½öÚú™E–‹ÅpåÊ$“I[ÌÒ¨€…–O@þñ— Œ}Ç€˜öÅFö$5ðƒE$¡9™D"‘H›÷h¡Μ9ƒd2‰óçÏãðáÃö­œÙ©d0ò{%È@ÉÙÞ~H$‰d— >Và jˆÛÌHvn*®C5¸öãìÕÌß#—!9NF É ÃEýÄÍÛúŸ3ðPU‚ wX!« ࡼެUÐÕ’rùšÒï9ÆžcìUU)%»hͰ=ˆm»kÎ+¯*øÖ÷ðß>˜@qõ24ÃýD>÷ðæ½{¸37‡tnõ!pùLvp¾<‚ÉS­Àï>¨þFûÆÙ²ílŽÛùd£¶E<ÐQþ#» ò|åƒÞ=±?Z=溽>9´\4µ½Sé4òz"ÐíMdÙßÉ䋆ë¨È‹æ\ð7D°ºvåѰƒn;p››% &Õ¿‚½v°nîqbtYÜb{9Pë½H¤†š³¸ŠàÃà¾GûYëÁ‡\.‡¯~õ«øüç?Û·×ȧÓiD£Q\¿~ù|ÞRS4zD 5ö2ä«¿ÈÙÊ묦g1vÛÔ5¶«F“pÁi®!‘H$’©D,ÃùóçÑ××gߊZ\€<öŠ#¿åîwmt ‘H$’Ý2@ÉA¹÷]G~òØKÐ óæ¯ä:H$©Nºtés¾õ@•ƒ¥C—®¹³N‡x{üìÜTþì´ùAh¸Ÿ½ùir)’ãdU”‡ôÌ,äBAW~¢Ï‡¶}{›ÆæùTJ·mÖú‰Á{¬¿ã­ñ§š­Çêx(¯>'VJnÙçˆz˜<ü\/»ØìVrIË¢<¬Êßý )м|ï6¾ñîO°®<ùqU>M%ܘ›Ã[““¸±0´,ìÐ@jñzv´‘WLؤó€×ˆëàCÅËÞÎߦUýÆ6ˆÄ ¾ÆP‡×‡p{»î¼>qè¡*}çÃ;€`®q’…•Þ’jFq’düÒ¯(ªV92¦Î¾¬¬i5\à×sÀ×8s.¡kÕìÕ°ƒó†þ}f(l¸`mV9;Ô¹ ޽ êgE»ö0ì8³_H-ò“u©\éÏÚ>EŸeÕ¸pá~ý×}Û[c±Õ$g!_ý¨SßÓ±‘ È¡lRšH$‰Ô`êïïG$ÁØØNŸ>m_ÁJjâ2¤÷¾\²Sµ¯æ:H$©Ž2ÝÁׯ9o»æìK‡x{ÌHvn*vxw‰ò ð@r¢/ûÙï²&uD'˜¹v]w~ÁC›Êæ26õ“¯œÕê±@&p·–¦çLÏSÊåMÉçá§ú ¥³2Êê̄¾73†¯¼{ñdù0½Õ`‡’ñT ïÞ½‹wâÓˆg2›£>T;8ïô/;X*çà÷”ü^Á”2<¢B)âƒ(  ¨ j-}ÞÖâ!¬Fµh0ØaUáö´yô]îð‰ƒìÐCF’ðÚÄ„éͽ½”,?÷˜Õš0_DZ¿X­hªñ~Úð©ð é„j±…f à <þ9<è ËÀ‡D"çž{ßøÆ7N§×Ÿ§È2:¤Ü~ Êè79»åpCƒIu‘üZêrH‰D"Ù¢p8ŒááaŒáÙgŸE0´­l5qÒû iô,Ôå›ìk&$‰Dr€ŒZaÒµ?€:÷#W©!.AäÏ ÚÝ\ûqösJ6F®Frž/ûïcN“Öqaþæ-Ýùí>q¼©l®Ù*!Ði[ýT±Õ‘vëîîîµ2¶<ìÄzÉ-+á`Zv1¥[š1x¼^Sòqr”À\èájj}ïO'7/F•—ªÿIU‘Îåp#‘À[ÓÓ]X@¾Êõ;ÔWí~ïjð…MòZºóX0ˆžÝ»™Ë˜Éf-‰ò0•N#-Iëó‘’e!¨ §ŠQT­æŒ=þ9œÞ÷¨%Åïí¡tÔXFGG‘Ïç©¿6ºzzÒå/@K7ø€5p°aÜ«‰+Þzêøßnˆ®Á®p8LÎK"‘H¤š …pæÌ$“Iœ?‡¶oõÎN•@†‘ß+ Êæß»t ‘H$’e$Âöõïƒÿùæ7%koå)Š‰ÉŒdç¦òg·t·Ð.p?s25?M®Hr–x/8ÐCzvçKû3×®ëÎ'xè D¿¯©Lž2á÷µÛV?•÷4çP Ù A¶3+ÀƒÛtÿc'áiaŸ U)iɞͅ·ó üÝš’×t>ƒÿðÁ;øûÑIdEÂJ| ø©ˆd.‹÷ççñÖô4¦ÒiȪjkRt‡Ê»Z[àÙr›ã8tø½†.i·ù<;Gž<žøÀ;yÊ7vñ­Å#¢3àÛfS;ð˜Ac·ßî@`‡6n‰òð€±(¯ONX²â&­=àjP”ªûN™:øDaó×sIœ`‡-íàjë]úw(§Rzž¯<ߺ4ªC©‚œº^6¯½œz¸#&{‘HM¦ àCØÄð‰OáÇþ9ú‚‡,)þå—_Æg>óܾ}{Ûg²,#ãòå˸~ý:’É$õךq²£5q¥ÁYGq(7<âÿåý³5k㉀‰D"™¬ÁÁAÄb1œ?}}}ö­èÅÈc/¡8ò{Pî~êâ$‰Dr¬FFFÌ9.Ž xõw¡.F­­0A$&3’›ÊŸ]ÚÝF¢ìjõo¾°½Õ'Á€¾.Ý [Ãð| zðxP·¦yÛæ–Èóè ølñÂ'Ú³¬y-&p¬#¹H[ÆÔû÷cßNpD$ˆÎÏ™n‡´$!¶¼d­±5 ä}g‡yÇ»:~jœ—8Ž«ýmߦ€ f”ã&ØÁB[²Fu(÷™W,?×Zµ¢ŽŸìÐämqƒ½4Ξè„Æ ©qç &ö•¬*a²°„›¹…µ?ñb’Æø’uç¹¶?x‘Çþ9Οøû‚¦·avvŸýìg1<<¼ãw‰¢Ñ(._¾Œ©©)Ȳ\5_×_~“³P®ÿG¨ñpøàl,Èa­øô8”ß2%/;/¡’H$©ù488ˆH$‚7ÞxÃÞ5GÉA¹÷]È·¿E ‰D"‘šCJòÍÿò æF{pÅ%f tùÞ3’›ÊŸ]ßÝ¥†qíìµ)Âɉâýû™Ó$wˆR0óÞuÈ…‚î|ºNk;Éù’“kfÞ»¾éÿÓ3³Ós|2ÅÞ?¾6rR$’ ¶)ääÊqÇ -Ü®ïä)¦æw¢ÿc¸ué2¤\)Ýj”OÇ~[ÚíéØÁ׎ìä»ÐT¹¦¼òŠ‚¿¹{ Å<þÕ£aWëj”‹2?üsŒWƒVÁNÁÿÏÞ›‡ÇUÝ÷ÿï»Í.ÍÈ’ì±±‘Œ7al, !@¤†,&ib±¤i’¶¡ä›´ VÒ4]Ê·Iø’¶±I›Ò$ÈIiIÿžB1i2Á„Å6ãcc-ËkI£Ùîöûc$Y»æÞ¹÷ΙÏëyô°Ìœó9çs>çÌ=3ç}>2Çáb<Ž‹ñ8*Ýn,÷•!àt’ØÁ8xîÒìi¤%C‰|.ÜŠ I–¡Œ9˜að;V`îl—?ÈR&Nò¿ªéÛ“Ì‚“çàä9ÈŠŠ”$C”e¤eÅðØäY—Ãà:YlXT‰½³A˜…žxãpähöÙü—¯€Ëï·¥O¤d CgÏN†æx̆Ë_ïâÅÔ\ŽÊµ«áòûÁ»œ([ºÇŸÜ£¹-œ»ÂÚçEÞ ¤zí8,µfVNfbË_0¥2<Óç1´¾\²<¤zNå,>Ð4aœ>xVl6$Óü¦· ÿïÐ[8~a†ˆ&”ñŠŒþD‡{{p ç""££s—Ój‡Ä†ãà9x™ÃãËÀ)ðp;¸\¿ Ø7ß ¼ÉøÀæócÀœƒoËÀãàáw;Qísc‘7“ýÁëäáäY«;É…“cQávfný7Ÿ  ¶¬lÁ>oYº^â‚Ã}}莚2’'¢ƒæ‡‹(Šz)v²8œ]îrÎÌv¢!žÜÙø9×õO5 뉌ñ“ÑþynŒ:VeÛa,°@ÙÌ·a·ìLE4&”ÙA» Êì@”%žÙa6±Ã8Šª¢+= EïÍúsd|ð.´ÖlAçõ_FƒÿrÃûÕÙÙ‰»îº {÷î]ð½‘Hd"ëC8F2™œñžššš’ˆ¹ãáCÞ&¢ÎëælzStøïɸÛ:Ið@AXImm-ÚÚÚÐÙÙ‰;vÀoÓƒÙ@‚ ˆ\Ù¶m›9;áø9ˆÇ¿eø]íûß‚†² XãFòsIÅsÑ÷äÜ”å( G%Àj» WJ¥fd4H i7n°•¤d ÝoÄÁŸîÆ«;€·ŸÚƒî7jêSÆÃè?ÕÓ¿~oüëñæ#ÿ† ‡#AšÛÅ{*(H3ÔšY9 fRoËfΩ»l×[Ç m‹àvÞ¿µ7@pkHHvY:¬Óï7–†æP´;€gû¦}Jå v˜þ\'Éc‡…ŒŠiœÀk.Ì>ØÁ>x¼îCù ¸ø,nØdÙŒèçÆêƒ”Ö|ãYNžƒ×!Àïv¢Â“B,.s£ÒëBÀãDÀã„×ÉÃëäá8 žË´‘c¸·~“ÄŸøeå™,,óàÜyÅ*]õ?uú´)Ÿü1IDxdØü°Åì6âc¯s,ƒE^„Ù2ž,P‡GàX(£QÎèIì`˜¯ +0ÆÕïàçx18–苉b;XaCGf›mæJÙW$v hþ2QiáLœŠªbHJåæceöµ¢ÖåGû¦ÏᙫnGËØCtÉd=ôî»ï>Äb±¬Þ‡ñÚk¯áÀ8wîÜ„ø¡¶¶¶dbB‰¼l¡è!‡_bmý#® ¹ãçPcg «qûöí%‡A„}¨­­Å®]»‡ñÀ”ð„A„Q´´´˜W¹œ€ôÎ÷¡ôþ®÷¿z6ñôÅ­a®$?S<õpk÷³ž,jò<…4a;ŒÈòÐýæÁ¬ËòN'*×®±Eߣg»ðö“ÏàÕ?Àé_¿ˆÑžCëO FñÎÿ÷ßxþk£}\XÞ°³ÄY¯Q W’s€…¶h-ÊÿÂrœá¢ÁíBým[u•»,Íò0¾HyVlç6&!È…ä(Z½†çOv!-ÉÆ‰¦?ðÉYAJ’prp¯tw#<< YQ´Ù!±ƒ¹1Æ0(wë:ù\m‡òy ³=ØçàÇ2pp, ¯C€×! Üå@…ljEׄ(¢Ü倃³Æ?uŠ™Y<¦ºcÕ*]Y^è:‹HbÔ”v‡c#ˆ¦Sæ:G5œg<>ëxò:ð9¹Å$‰²¯ÃêÌ óÖÁkŸeÂ;Øå3 0lPf“¿ ‡N» APÌÛŠ¸"fõ¾„ßÉ(̜‡¦Êµm¾ ;.»Îð>îß¿wß}7:::².‹ÅÐÑÑ1!~(µƒææŠŠWä0þ§ô„Òý‚a5ûý~´¶¶Ò‚EAä•@ €ÖÖVD£Q<öØc¶Î€EB‚ Âh±cÇSmHïíΈŠæÌ:¼7Ü*ù¹äã¹è5-¹uŽ2<ÅëZª¹LtRæ)™ÂÅ#Dz.[¹v x—3¯}¾xäþt7Ž<þúOuØr\8OÀr› ç²k˜ššp€3©µc£DŸþ/‡#=†·Çéóïø÷×óHûí/ª"!ÕsÊò1=åK ©/)Ëø§wa÷±“Õwh˜Éê…1áƒ(² YQpfx¯]¸€ðð0¤,„$v°'ÏM=¤.ƒ[൳,ÛƒMoùµQ\‡Õåó¯…>AÀ– ¾µg÷‰¦}úŸˆfµ†èŽ€ªéôìc2Ï8ùœŠ“Ï=ox6£áÜœ—05ý' fRcëÖ nÍE†#½†7ûÈUÒU[u•‡/@IÅò2$®à•pT®4¬¾_vÂÇàâpBÛšæT@–'„’¢ œ…ðÄÖâs9à²KAä8ÝY!&05Û‰²%èñ ÊåšÚyªÿ¶×ÕéªÛÌ,IYFÇð9±3yf=d1N–C¹Ë‰JŸ>·0ñWáu¡ÚçA¹Ë Žas‹Õ&1W(ët®âÃô3æÕÍxØÁ¦ql• Ívò—©6Š(³‰Šóó‘(¹ç»"0Ùí FgÝT1g¶‡zß„®ý¨ÙbxøÃâ¾ûîC,¦íû¥Õ«W—d|è=‡¹Oþâ†Xóûýxæ™gÐÔÔD‹AaKš››ÑÞÞŽ—^z)¯Â‡íÛ·£³³“„A„é´´´Lˆþ¶oßnJÆ#¥ïw&z Ã÷Ö¸‘ü\RñL"ÝP–¢`•šËŒöô@J¦ %Sè~ó`Öå\þr._ayc{pøñ'ðöS{.ˆqá=ù<Ø8˃ys€–ø(眓>|/³E›· œÃax½z³<@²çݼùÃY¹®à•†Õ÷JßyÜ÷æë8z~0»‡2Í/L{X>H2$YžSø@b‡üPîvÂëæL¼À1 ÊÜŽÜÅŸ “²=·uз±Ã^*OÔ*À³ìœmz<øØŠËuÕýÐ[‡L{ˆ$âˆÄãÆÆÎ¬Ù&‰²'EQÑŸL sxïFq.›øëBWl¢¬æYeL` ¨Ã€ùf;¥ v˜X¨ÙÌŸYõ[ø:‰¬€ŠÇ&Žb˜üEÐÜ-|x&Ï_Ê̜‡֚-¦d{Ø¿?î¾ûnttdŸ¹¾¾¾dc$;ÑCŽ© \ä0Îá§¡Fß1ÄêöíÛ‡Iì@AhooGgg'¶oßn™Ýq¡eC"‚ ¬$ ¹¹mmm‡ÃèììÄŽ; Íü`oу­Ó1 º‘ü\Rñ\ôÃm]õdyP“çi*ö‚u€Ñ!zˆõôhÎîP³å–vMJ¦pú×/âÐÏvcèlWÁ Ãò`yÊÎp¶ôI0l4m Ð*0Åѵvl—ä^’Sùs¡ã¦´Ëí/3¥ÞúÛ·ê*''¢b½y'¡|)¼5׃a9$²b IDATþÞè¾uäu¼pª iIž{ÝÒüÂ<‘Š H ëñ:T—yð8ás ð:”¹Xäu¡²Ì ·Ào”ç3†)M§çy³È³,êüÓ ey8ÜׇP¿yëeÇðb¢hÄ“ÙæU •^p¬EEWlýÉ$Ä9Ö²„(ãÌÈМ¯“u± .ƒ„YØÉ¥Ûˆ,Ûed²ç˜¾ž2&×Nb‡’îK1øKe¬Éî@ÙIhž´ . ßoÄѼq‘g_¿ÍÊöÐÓÓƒ¯|å+Ø»woÖeV­ZU²q¢D^†Üñs£>H¦-p‘ÃD‰d/”3Oë¶è÷û±mÛ6<öØcD[[‚ ¢¨­­E[[›)?'CB‚ ÂnŸ»víB8Æ<`Ü^¼ïwPú~W°ûdBÉÏ%Ï”ÅÁ(ÃQ,è< žîÔ”Ýw:Q¹ve}ê÷Þø×G5µÑ.pú®ÞJHð0m¿Q‹–M²Ð ‚°3@­­­èììDCCƒ!uJïíΣèß[ãFòsIÅ3‰LGO†(i¨é~š&„­`]K5—é}çMÙ.{ßµà]NÓû"%SxûÉgðöS{4µÏNð¾ê¼Ù–9mM‹Zìâ˜öeŸàÖ\ǹÐÛ†·Kp» ¸]¦ôùª[õùJL"Õß™ßÁÏŠÍàÜÆÌÛ¤,ቮwñ‡£w81å5ã²;ÌåÐŒð‘dˆRFøpàâEDFGib– “=ðz²HØ!×6¬.÷ÃÅOJA5í0ø:o=Üׇ½]gLëBLÑ1<”Kàe0{у(+HHÒKl˜ÙÁ*;”Ù¡|Eb‚æn±ÂÙõkÃ,²=Ô¸Ê 3×ÙÙ‰»îº séN‚‡ ò‰G¡ÆÎÌ?çlû#®9 SÎ<­»ì<€¦¦& ,‚ ¢èinnF8Æc=–õ­×$t ‚ ™––¼õÖ[†d:’Nýj¼ËÄ}2}f®ÉÏ6ˆ’jF)wPO–5yž¦a+G%À:L«¿ríÓ³;œùí~yü MB »’Œ‡ªHùYu®äâŸS)ˆœàŒ[»à¡çTØ”¶¸ýe`8s&Nýí[õMdEBª÷][Œ•+x%\Á+ «ïôè8ü:þ·ãÒ’2Kpk`¦>†ªc‡´ˆd:cý}õö Z )…=Ÿ, YˆòsðMUU$D ÃÉ4âIôŒ$&þâI 'ÓHI²iö ÙÿÍöáäpb¹Ï7ù‰eÊëÛëêt™»ã©Ó§MíÒ‰¡AÄDQÓÊ£û`¶ªi1óϱ÷(ª:ß“Ÿ)ã¥iX!T(±ŒôCÅ@fýä9ƒì›/v ìVذ ó@I ev0׉¢ð÷xÌØ>Ï3]àr'œ>àvÌþç'Ÿùã¹±LV¦Í™ía1B›¿€íK6fª¯¯÷Þ{/öîÝ;ëë>ŸøÀ(žH¡o_=Øú¦:ó'‡Ÿ†šìÓU¶¡¡­­­PADIÑÜÜŒööv¼ôÒKضmÛŒ×ý~? ‚ ˆ¢¡¾¾áp›6mÊqó™€têG€7 UtøÞHäP(QRÍ f  D± 'ËC¶Ô|ð&Óê–’)~ü œyåÕ¢ %CªçT~V`ÎeW·˜vŸ§éo$÷’©ÿC‡à!‹›Ò6–ãàö—!>5¼nï¢ÖomÀñ½û´û,Ö9>ÎS‘÷ñÊ—‚s–!ÞuÈ5×`:…]'ãÌh Ÿ­[ŸSû$3øƒqºØaú«,2ƒ¨¬ ”J!èñ¢¶¼.ž–•¢‡e3™DP”Y£G×(—pUE\”OK˜ë|»$«dI1#xpò,<ÏeôÚa8µ¾2ô%HJræFÓI祃>¶âr¼ÐuV³ÙÝ'Oàc—_Ž ÓkZ×Bý}¸añð,»àÊ“ûÁmH‰™[, '7Çš4ÏXár;¼¿@ýÙ×óý•­\ê0Bì`Ø~+ÄY¼Îs€¬dÖÐ|ØÏòu;XòUùËÔ~”ŽØAT$¢¢ ¥ÊU‰±½ >ÎJÁ a ìÅ/QèkD©îçÆ“ÿ9n–ŸXO¦.Ì%!< ø\n€{öV”Ì³Ž¬ŠÉŽVÆÖ#nªïDÛºO ©j-šO>‡!)÷‹‰zè!D"477Ïx}Ë–-Ø¿?Í-)ùÄ#à7ÝðžÒøRJ÷^]Eý~?öìÙC±DA”,hll„B!„ÃaÔÖÖRV-‚ ¢èhooGSSöíÛ§»55ñÝG \ù5{ï•‹•ü\ QRÍ ÎDO†(i¨é~S˜„æXv-…l‚ç²÷] —¨f#v±G’Ék—/®BõÊT_Q;ãµÔè(Î}½g µ)gÆÂÈ‹Ñ Ó¾ÀaT•ªÆ ƒíìÖ®áÕŸ‡è«¹ôØ0ÐùÍÖTÇòúõøôí¦´OJ‹ˆ7'£‚˜Hâ¹o킘об‚ Þ•7Ùf1‰Äù#PR1Ãê¼¹ú2üù† ¨.7V­5¿Øaöà8ð<åeeXîóeq°™( D婱`ñ^(-+I¦!çpÆÉ±à9ÇA`0LY4?šN!Ô7v;$ €¹T(ãsÿû+]¦7UUaçM;,d>^@}eÕö±á/þâ/à›”‰/‹á“Ÿü$ͯñO ßåà¯}°d'¼~Ê™§u•}æ™gÐÔÔDADAAQB477c÷îÝ9ÕÁ?îò?°í^¹è ‘C¡ DI5ƒ:¸0âɇ¡Žh»[t#¸ò 4ÝÛ $/@Š>ðПj±³O<’lj®æoÂçÝaÛ¶m$v ‚ ‚ ‚(AÚÚÚ°}ûöœê#/B -°O¦/Qu³ ÉÏ6ˆ’juP;z²<¨Éó4õ{ű G.{ßµ¦ˆN>û¼)b§×ƒk>µ_øéÃøüÄ5ÛnÍJì0^vý-7ãÎïÇß½Ë7“™!ÕsÊÐËÐBá\v ÑZÓbŸ¦¿ý™œÝ‚‡s¡ã¦¶Ñ³(`^ô¿¿Ë–èóÝ`1i›±dX®à•pT®4¬ÎÓ±!üÍ[¯âW§Î!-çö©Oì0íW–L$p¢¯¡ÞÄD‘&q±Ãq€àз?ÊxZÂH2Çøš£ ’¬"ž–0”H£7–Àp2 I1)ýF?¬.÷_Ê’0í0Ïöº:xAW3zëPÖ&õIÄž±ò˜zx_–Q®ÕNϼÅÝš2?„ù²Ó,ØN+ô}B®þ2TÌÀÌ,»Pf M¯3˜‘Ù!›ò<Ìss–g.ÍCÛ?õY€Ämh¶Ã¿LµQ:b‡~1”ªMœ©@Åàäý‰ˆR£â„e§0&ràu‰¦SÎ;Qãö£‚wÁÍòp³<|œA‡+\~°³dÈKÈ"†Ä$Ó ô$cHÇ1(%ceÈN>#‚0kŒåÙŸéw­ºÏ\u;ü¼1_Øwvvâî»ïFGGÇÄÿÛ²e ͳɟ; %ò²…öøV>·âšËùý~´µµQàAAA”(Fˆ¤÷vCMõƒÞÄ‚n$?Ûd JªÔÁÜ`t”䚆„½`` =¸üå¸ì}×ÚD)™ÂÁŸîÆÅ£Ç ­×éõà†ÏÞ/üôh¸çO²9ÌÅòëqçƒ÷ã“ßü8ÏmU$$"Ç¡*R©Ghi¡O³ ÓR¢‡T̼÷½‹`8óné«¿]_–U‘ê}×vÃç¬\ ÷²`XÞú’²„ÿwü ~~ô$†âú,ç.v˜ö,KˆŽÄpàüytD£æ'ìÇGæÆòl÷Izˆe"CÎ{µlÞ¦IQÆÀh ƒ‰¢%:üÀ³,ê³–÷ š×Õéjʨ(â‡ÇŽš}q<±Dâñ‰•Ç’L*PÁ°XYVŽj·•.×Äß2¯+ËüXá+ËMì`T6«Df÷'ßb‡‰ïaÌ*v0ÔvŽõ;ø©k§ºÀ˜˜˜õ‚ÄV@Å©ÆõE¯P1®ˆ9ø‹b˜ 5¶ðãY„ÌþÌ`†CµÃ‹.?V¸üXæ,Cù,´"£/5Š1…”,ATä‰}œ¨ÈˆK"úŬjnÆ™õ™³©j B×Þ…M¾Å†˜éééÁ½÷Þ;!zغu+¼^/Í·ÉCqòQ¨±3&Mjþ›Cv‡ÖÖV ‚ ‚ ‚ ˆ¦­­ 9]’Nýˆ©ÊâP(QRÍ ‹ž PÒPÓý45 [a¤àaÕ‡?dhv‡ØÅ¼ñ¯b´§ÇÐ>¯¿åf|á§?À Ÿ»N¯ÇкWÝp¾ø‹Gà øsªGIÅîï´n%g…’Š{<ê‚¥CðÐÛ6µMeÕ•¦Õ]½ºµ×oÒUVŠõAŠõÚn y_5<+6ƒuú «ó?Μă!2”°Ïñ,áÜÀ\8¾D‚&oQ¢0€ d/zÐAJ’1OAÊ1›‰Þ½š()IŠèM"-+yiT¹\¨r¥¥’§úûŽU«°Ä£ï¡î…®³ ˜¿^žDL”¬;LìÓU¢„ N@¥Ë=ñçó ²²a•pƒÄÚ^gL¬{zêg˜KýHìP86Š­/Åà/•éª=Õ^ã®5»CîãAb‚Ö ÛÁ “ÅÁíȈ(Y&¯ÍIÈ"¢éua‡ˆ)HŒši·ËaNÛfÖlµ.?B×Þ…íK6ÓïD÷ÜsöîÍr¿óÎ;iÎMC~ûûº2Ìýa¬Úv­‘ÃOéêkMM ZZZ(X‚ ‚ ‚ ìÙ³›6mÒ]^ŸƒÜý,9RÃ~ž²8Ä@”T3¨ƒæ¢'Ë »Áº–Rÿò¨\»Æ°v]?é=\¯ª™ødÙÜÅ9¹àÏê=Laˆ*JJì0ÍŽõs,0kæ†Äv²¡ÛcϾÏÃGIÆpLNCÖѰE¼ N–7׿$v óà9À! ýÇÃô`xo8“½8+W‚súŒ£îJÈváìèþ¸nüafY*v¸„$ËèèëE_luÕÕp EÆx¦QÄDJÁ¦ª*ã©L vUF’"dE…Ï)XÚ†å^"ñx&S‚ÊÌ%¾q#Bý}Õ‘Eaäöž;ƒ­—™¡Ž½t¸:)Ë ô¡~QÕÔlfŠ&6£*J<Ÿ94¦kð â’ünF^%“‰EQ2ב'w{6K-ðúZ‡mÓíE*"éQM6œ ‡rÎinü’Ø(•uÎò= 8øÌ^Ëf¤¢N±@Z‘ÁO³ à€´4‡4&óÙÁM­·Þ·á÷‡ÿ‡c=9›yá…póÍ7Óœùô/À®ã«)¬ ›Eóä“B=««zºˆ ‚ ‚ ‚˜úúz´µµá¶ÛnÓ¿¼D^4¤=Œg9À¹'þ›-_{鵲̿3¼'ó¾‚ÙÓÓ—Í6ˆ’ju0ÿ°>íT)Uב ÛÀ8*¡êÌ>â¿|.{ßµ9·AJ¦pøñ'0ÚÓcX¿®ùÔV4Üó'y÷ï Ÿ»Ã=½8þ›—u•O÷wB(_ Vp™·ò3‚]ÃÓ”óø”áaŒüǬÐl·v¥] Ñ7ËS)µk¿æçšÍŸþ„©í\NŒ A•eÓlT,"üÆa}›¹T ŽÀeö} rxÁ{+!'‡ ÊiCê|gxCèqùQé½t¸)_b‡É$% ‘‘X “í¡ƒÅõT5)ÓCÁ6’!É9F«Á.Ê xŽzpß‚6”;8ÍÔ?É´Oàd9¼©ó2Ô߇­¸ >ÞHAÒÌÃûiEÁ@:…Å.wæfW+Ä“Ë+JFüÀrSÏtæj#«:´ûKW\»Øa¡¾.¦`Ì«Ÿe3ñ¨ª—ìPv‡·A‡ÅM~Ø(éøu² åâou™¡ìAAAÄ|ÔÕÕöíÛ—ÿƈÃ@z`âO95ñ§ô½–ùëù-äîç w?eø](}¿ç Æ:9Uã¬Ìã~ž¾`¶8Pmî.V€= H#šª`•`•äKÂ>H#º¼Ó‰x'xWnñc{pä?ž@b`Àî8½|ü_Á5Ûnµ‹WÝpÎ=Žáž>}K”€P¶Ä´öqÒ(89nÇè<ùõ¯½ÝèJYšõÔRc™2í‡ö‡/öZÒ¶²êE¦Ö_½ºµ×oÒUVIÅêï´õزN<+6ƒ÷UVçýñCo ý½ÌœÄŸ»ª‚ŽÁ„.\@2•º” €(XàÝsdEERÌQ@eaH 'Ó–·Á'Xîó-rSý|ǪUØT¥o-EÜÿÆë>)Ì}°:&Š814hÍáýÙÊ+ Ngþ™ ;×O#æ†:O_ Iì0ŽƒÖtÿ’ØÁ’'öÒô—j… Æšì0A‡•¼ì,ñæd8¸Yœ ËV8Ëg}Å/Q”K¬|&Ûgï¯ïôŠ@RÈêå|Ʀ|ÁÎúYÒV÷ ì\}‹!&ºººh.Î6=cg!‡Ÿ´çº¡j{–Q£ï@:øwºÅ°k×. ‚ ‚ ‚ b^Z[[ÑÐÐPxߌ‰!äÈ‹»ŸƒtêH'v"ýÆ—‘~ãËßù>¤÷vCî~Êàa¨ñs¹ïéõ½0fÄíáç¢nŠg]®šÅ]l™ö,ŠÎ›ô Â,×2]åÖýþ­pùý9ÙŽ]ìÁ‘ÇŸ@rȘ‹ŸªWÖàó?x«n¸Îv~þä} §×£«¬ëƒ4o©c%ó$x¸D Ð¬Gôp.ô¶éíò,2ß•õ·o…àvê*›îï„"&í=¶,÷²«Á:}†ÕÙˆáŽÂ/ß>´l¿ßh*‰çÏãÜÀ`b†"–}¢‡Ñ´XXÝU”d}üÖúÊà¿u¾a£îzO á¡·pö«ûIœˆ.OÙÄœ®×T -iiúIì ­ŸŒ¹}(6±Cæ!8;ØÍ†f;Œ6lê¯bz†)*7V»+Pãôc¹³ +]¬u/BËÎrT ø8)Aó·`YÀåÈ<”@&Dy¾‹.#|0Å03C4-˯Ã3n‡ŸwÒ|2 ¥û¨Ñwì±^èxSú@:ümHG¾uô¬nó hjj¢€ ‚ ‚ ‚ dÏž=ðçx0ÑnŒg†CˆÇ¾“Bþ&Äw¾ŸBôýnv!‰ì2Š ‘uжîZ¦l­öª“çÉ¿„­`]Ku•ó_~yNv/9†C?Û )•2¤ëo¹w>x?ÊWÛÒÏN¯Ÿüæ×t—7ó‚v•잦"'ÁƒÍ}ó,.n홆.˜Ÿåå8ÓE‚Û…«¶6ê.ŸŒ/ˆñç}Æ.â Y£ÇððÁ†iÛõWRt EqìâEH‰Ä¥ÛÖ‰ÂÝDL, Àk;¤¢ªjîâ<ì÷¤éqkAx–Åêò±uWžz`gµßíëgjŒø IDATêt×ýB×Yì=w&‡Öex?’ˆÏ-zÈêp<“{¼È2JŠ:{_r9CùçòÆ‚ùØ;‹Qì0n‡åæ¿É™Ä6·aÓƒ©E3&Etð×`9YV€À°ÖÇ0 vˆRZëÌ„A&£KÈ졈± g^¦…ä™Ï]MUkÐ^ÿ9=˜ˆ|âG€ÏÏ:¡õ÷a)¥ÿä“B|õ‹ßÞuèDÎMimm¥@ ‚ ‚ ‚ ²"`Ïž=%ÑW55É Ñý¤÷~~Iqì;™Œçž…2ò. 'æØðÓ—É&ŽŽ=ü\ôÃMñl…»Ø²ÕÚMI1¨Òùœ°Œ£Rs™¡³ú/òé~ó N>÷¼aíoøÓ?ÆG[¾¤;ƒ‚U,߸×|j«®²r"jj–›RoF¥$x¸DmÁ-V:2< Gz-i›×‚,ko@õêÝ‹Hz°Ëöc̘áa2ÿsá xã œî¶e¿û’ ¼‰ oxH§IøP¨ŠÍf2=dIZV ª·Á~0‰*— çØaœi‡A·×ÕaU7~üðØQtÄ¢z>©4ÌžUô`„ØAK;T5³öH’þ:ôºî7k<æ}Ý ±Cž^øÙot&±ƒ}ÖxêËÔ>‹Ø¡˜²mØ =HaÁ²€s,«C!°|½(páƒY±#ÍìC½o1Â7|›|‹i~™áöÔäÎÿ²î³]ãg¼ Cé~ÒáoOˆ”‹¿5L¤±cÇ466R AAAYÓØØˆ;v”lÿÕø¹LFˆóÏA:±éC_ƒxø›Nýr÷s³ˆ ƒ<Êâ@´­«Ô sn0níg/ÕäÂVèÉò»Ø£ËÖÉgŸÇé_¿hH»^îüî}¸fÛ­ãë>w'ÊWé*kV–•áJ+ÞiÊOP[h fÊ—k.s.ô¶%msú¼Ü.ÓíÔß¾UwÙt'1iï *¸M«ûH´­¡7°¯Óžé¶$UÁ±~t Ž<‘ÛéwÂÒÍÅ\plÖ™¤\„.%&v§.P‘ù—Y\÷÷׿^A_«QQÄýo¼Ž˜¬%3 £û`öуÕb‡)A8)Ûƒ‡lUó|¦ÙN.uØFì`ÙW~ì0Œö›ŒIì`¬ UÏ2EÒ;>_PfómØ y•7 <«ƒ‹Ë-].ÇdÙoo®$vF,x'Úë?‹†À šg& œÿ5ÔÃæ=ÿh8DßÒý<ä·¿ñÕ/B:täÓ’Éa:555”Ý ‚ ‚ ‚ÐÅ®]»ÐÐÐ@Žßϧ DŒ‰ v!}è/!¾Ò©GH¡ß« ,ÔA[»Ë@?³ekt¬;ý4„­Ð“á!zVÛEáR2…·Ÿ|3¤ÍÕ+kpçwïÇòë Ê×N¯ ÷ü‰®²feyP8WIÅ; lŽä^2÷‹îEšë¾ÐkYÛ}Õ•¦Û\Äú­ú6sª"!9nï jR†‡qºã1|ïíCø¯ãd{>@Ÿá@o’©ôì7®…ÏÜÂê´¤SðçPf&ompq–ûÆÖ yêA  Çƒæuuºë¾㫯¾’å“Cî÷gÍô0k=Œ¹ñ ª@JD)7;$v0ÆOê}1\LÁ˜\ÿ4;Ó_çØ©7ëŒg;Ø•úQ’}±­¿Hì@㈪‚A)‰~)~)”"›¹y\…™ÕaÊaáäx]e–ÇhøzÒ)˜«G•X@™j`\ô°=¸&ˆ.?ùc@6àB ?zª±3P.¾ ùô/2^þ#HG¾ùôãPú–Åa.ÚÚÚhð ‚ ‚ ‚ ]ìÙ³›6m"G̵ïŸKÑùsÈ_̈ ˆé^eq »²/Èè<(”á°ŒŽ CR2…Ã?þS†´wÕû¯ÅÞê+j Òß«n¸Ë7\©«l:ÚUJ¡iŠB—§)?-eRçQà0eÚÓ* _ìE*6 §ÏkzÛÝþ2D9ª,›jçª[~#„øÀæ²r"Šô`ö½©\¦f¢HÈ9u }É$þøÊu(s ¶óALq §u¨r¹Y`I³eË G6|æ y.Yr±oÖC$“ä“Z_"£qHª’9¨Ã^rÊ«V!Ôׇý}°ÓCCxè­ƒøëúk-ñu$ž9dR篘Ëã¹ÇC¶øe9¯?sí!±ƒ5¯/Ô—b;L<­s€¬d2訟ÄVÙaЍ/vw¦8|ek$v J`ÿ‘%½bƒÒÔýw?p³<‚ÆÀ=(Ïe„Lqd°)œèS$͉½¼Cû¦Ë!dDÉf1.g§v¦­îãðN<|î Í=#IAîø9¸Õh¹‰(aCô¨rjì ;5Ù uôl^»»sçN466Ò¸AAAº hooGcc#>LÉ5=µïµ)ÿ)[Ö³LÙZ0žå`œ•¥æju°ˆÜ¥¯ î J`4^„-`ø²L<*iMåb{à[²xÞ÷$‡†ðö“{0ÚÓcH[×ßr3>Úò¥‚÷yÃ=‚Çwü­ærR¬Š˜+¸(puB‚‡K¤ü™)» êH·¦2½a,¯¿Êô¶±·¿ ñ¨é¶®ÿ|Úÿy·®²éþNð¾jÛ.$ ïL<ŒódWÎÅcøÓºõ¸¢ªÜv~TÇú±ÜëÃj¿?“íãž/š"%·ñ„Ì8ªj~움GàÁä9y†Åêr?N ^­OjÒ_oÞŒ{Ú_Ÿ¾+_è:‹ Çƒík¯œÃçÆÞŸWô ccî×§xW´8vëþØÚCb‡I¯3æÎOuž¾«Ø¸t¨/™Ö\?‰¬°ÃQ_ ïs—bØÝù‹ ¤16g6‡„"á\j5.?Ø\Ó 0È|¶sÅ%àgÀÀ/¸M'².ãâx8X‚qŽÍüÉŠy’™Ì~€SÆ-³0íZs êË–à®wþ‡&Ž(½¯ƒ©zØEW —õçƒ;Hq¨©^¨É> ™ù§:zÆô, zؾ};ZZZhÀ ‚ ‚ òD(B4ø'„Ãa;v ’$A’$Äb±™{— Á`pâ¿+**Ðй¬ríÚµøøÇ?NÎ%ÂrB¡ZZZððÓCt Žœ‚Hi‘ãÖ¤ƒ =½§ö½®óù%ϊͶŒÍT'Òý–Ù[îñáï6^‹º`…mç«OP_Yže3AÇ…DámBÅYEƒñÄl°Ød_ìsÛfôö &ÝzÊMuPÇоØþRN¦¾qÍfl½lzz1óïW¹\¨óWdæ=0ÿ߆´a¡ƒlãk—C;cbÛîb#ƤTÅ“_—d@”4Ù'Áƒ6Jô°8ev ø-t¥»ÿ˜…~)~1»Cú•‚•|Ïü옑-^Ñ~B1".üå·‹ãQžËÅªš„š½f°=¨S–϶È1= ã_ nÕ¼He~`TÇ 2¤8 æ=Kƒ¶oߎ¶¶6h‚ ‚  5´··# ! áÌ™3¦Û p:¨««ÃÕW_+®¸ÍÍÍ4(A˜J{{;ZZZ(Ûƒ G9P‹ÉUÆûY ÿ;”~mç ¹ò àÝhL”4Äȳ™ÌÓaàÝÖ·Ö0[R_û¬ã ía9rô äè!Me*׬ÆUwÞ6ëkFŠœ^>ùͯaùÆõEåóáž^üìîÚŸ-X¾ÕÆ ?\£a» VF"‘°¡Ïf$xÈ`GÁƒè»ëÿhþÏ¡3íPN<£©ÞU[Þ‡O}ç¯,ëGoG©˜ùJL$ñ«ï=‚øÀ¾Åµz +l›éÁ.¤zOYjÓÍñøúúÍø`ÍRðœ=bð ‹úª*øal5c2Øâº%³$öm²’=L#kÁƒVï¼ ¦ù!šN!Ô?v0„ÀN}ÃS§Oã‡ÇŽædòÿ^ÿ~lY¼ FLÎTàÔWT_=è±a„ØarkÆCò¥’ÙÁˆ1±Tì0ÍŽib1œ–‡Äy¶ÁدVر¬ ů©vHì@”ø€]©á9³:̆À°XéÒyP…ç+‰ …iEFB‘’gŠ57'èËì0qA¨òŒ%´-r -§~ƒ!)Es’˜—;wRf‚ ‚ “ ‡ÃسgöìÙƒ}ûöÙªm,Ë¢¼¼ë֭Æ °aÃz>$ÂÚÛÛÑÖÖ†={ö`hhˆb…‘‚ÔÁbr—ù~Vú_‡þwkA%„e·b_<ÿôìb‡IðU †ˆ²ÅúÖ‚¯j x-@”äH‘g5•qùËqýŸýŸÿ¿ÿÝS8ùìó†ˆÊWá“ßüKT_QS”~ÿÕ®Gteyp¯„P¾Ô°vØXðð{‘H¤ÝÐg1<Á`°À[vkW6‚u ò›ÿ¬m!YR»ÿ뇖õct ŠÁ³Ý–Øê>z¯þä—ú&ËÃSs=Ø\n49>ˆø¹ü„çö+®Ä¬[·Ã¾Ùêz<“žò8€ãKâIQíá$¦T‰¥DÄÓRÁì!Õù<Ìá‡cƒýèK&Çv&˜©o|èÐ!¼Ð¥ÿfL¯ `ç¶`µ¯Â¢Ãû€w ~QÕì¢K2;ÌQ?)Ó ‰Œužþ”šØȈRcYqHìg ùËT$v0׉ڃÀùt 19­¹ÜZ÷"íÆüü™ÁŠ˜ô$A‰!"‡Ùž‹^Î,¡“–ÑP¬oý'‰ˆY©©©A[[ÉAA&F±gÏìÚµ« o5w»ÝX¶l6n܈††ÜvÛm¨©©¡%ÂB¡Âá0B¡æ²ííí³®¹”Abv&g`Ë×\>Î È:XL®²ÖÏjzâÑ4—sÔÞ“³m%†Ôó¿Y½7Wу{RßÂÂ`>øû`]K)† tøß4—¹þϾ—ß?ñßÃÉçž7¤=Õ+kpçƒ÷Ãéõ­ÏõfyàÜxVl6¬Žd|ªßŽ."ÁƒƒÁF/Ùnò¯ÅÈÊ;ç“”€ô›¿Ñ\÷Ÿ=÷œ>¯e}é>zª,[bkÿOžÀù£'u•5z11ELb´óÕ¼ÙÿPp9v\½ enÁ¾sØãA] bÒÊÆds¥y ¤`÷riP.etHˆF’bÁì'ŽE…Ûi«±HÊ2^ë‰LZ䦾1&ŠøêþWp:‡>¼‚€7‰r' céâxlT^Êðb” 5GÃdÖy×;è{Ýl±ƒbŠIvr©_–”4ÏÈ“ØÁ|;%zXœ2;Ѹ“à =ˆ¨*èLFu•Õ$x`83³Æae– #z˜¦½&Ñ1;vì@kk+9ƒ ‚ Â`¢Ñ(víÚ…]»vÝ æ<Ï£ªª ë֭Ç?üa466bË–-4èAØŽP(„h4:åߣÑè„ÀÂnÙv¬„q,S¾lÙZ0ekÀ8+M²D"ê`1¹+¿~>5= í¹Ía€<ð;ÈÃDz·™ƒèAŒ< 5yaá5̵Bð÷)– lÇx2WÝфʵk+vXõþkñѯ~¹¨Åã<ù·sÇÞÑ\λò&Ã.f’=R½vt႞¦º}‘ÝK²A7à^$´}èöv„±¼þ*ËúRV]‰áH%¶®ÿ|žûÖ.ˆ í?4ˉ(RýpV®´MèYØXž‡"óƒÿ‹‘sèO%qïUWcee¹-çJ$GR’±aÑ¢Ì­ïª ˆbæP(ϳÝOØoO'@:?.žÃÄ‚ÙW:ó%°™Ç.ŽÃr¯çFcc‹3EôàüýõïÇ=í/aTu™E|õÕWæ=¨ÆÇSR–èEý¢êŒèÁb s³¬"²³eš)±CVX%v`L¬{v;Ëœ’‰³Hì`¾ :(nn?Šè@0ÅpiÆ0Q0q¢'³8µd(`À)PöA³áY@²p‘8@§,§õ¾Åh¿æ³$z Û·oGkk+jkkÉAA&°k×.´¶¶ÐabË!IˆD"ˆD"‡…†ßïǦM›°zõj|ä#Ág>ó ‚ òJ}}ýĿϗÙp\ 1ž5¢½½½è³E¨é¨}¯Aé{-³Ž;­Ø”ÉQ±)—šmÒÁbnúÁÂwÙËÏŒg¹fÁƒš<ä(xPÒÚnd—úöt‰²=¯&/@I^ ,먄¬Qð=ۅʵk ;¬¿åf|´åK%ã÷k¶ÝªKð Åzá¨XQìîiÐnèzM€`0Øà»µ+ü âÁ.ø>ù­Ÿ@í9ª©îš?ïú´e}‘Ò""ÇßµÌ^÷Ñxõ'¿Ô]Þ[s=X§Ï6±0zæ (©˜¦2ïÿâpèßÿbŸŽÂ‘è¶£k¾‰DZ¬‚Á`+€ìÖ®lJÇóPNïÕT÷ª-ïç¾óW–ö§¿³ ‰¡aËìíÿÉ8ô¤¾ †ÓoÍõ¶‰…x×!ȉ¨¦2ú»o@ð¸ñÊ®Áh_¿!íps<þzÃfÜ´b)xΞ·Tò ‹úªª©¢ “å²=Æ^O’± %²¢¢4iû=‹çPîrØvÂ#ÃÇF&}ú`§V°÷ìY|ï­C95iŠèAµ(–T ÖWŽZ_¹N&g]]q¼1þ ±CNu‹ŠQ͆ãnê-Í#vT€”8fÄæÛ`È_¦Ú°è¹’b¸4ýEÐþc¥$zŸ¦2åœA‡wá7 < p4nV¢¨@2m½]^™±¯"ÑCéPSSƒ¦¦&477O¹Õ’ ¢ÐˆF£·î†B!—žÍÆøí½ãlç»Í— Â(ÚÚÚÐÒÒRô`ÀívcÙ²eX¾|9šššpÛm·¡¦¦†CDA1] …ŠVð6!~¨ºaìÿØèKó¢þþž~œ°Æ]…ãg5~â;i.稽''»z¨ú Rϯ ÄÏdý~>øû”å¡Ð¦ª4ñÜy³ÿÑ–/aý-7—¤ï_û§ðÚ>¥¹œQ—²sÒ(œ£a;º†f`WÁÃÈÊ;‘ö/œ‚Hè€üæ?kª»|I5îþ¯ZÚŸÄÐú;ÏZfOL$ñÜ·vALèû‘ÙQ¹ÎÊ•¶ˆ…T'ÒýšÊliù ,¿ö¤ãq¼øï!z¶Ë¶¸9Ÿ©YƒÏ­_kkÑÃj¿AÏ,·¾ó6mÚ„o¼ŸøÄ'°eËr AÇø3ìøß¾}ûЧsœlÅ&pK~ŒgyþÚA™C\U˜~N‡¾ÈÚ2CëÉ´01ò,ÔäÍå—oØì/fU’ EžÍúý”å¡0Ñ’ÉÃ(œ^>Úò%¬ºáº’õûpO/~v÷íó¸bœÕkrŒ°¯àaw$i6²B<À¾‚‡áÕŸ‡èËîÆéíæî_þåÁjKûtáø)ÈiënÝë>z¯þä—ºË{–_ÎS‘ÿ‡ªÁ.¤zOi*³á¶OaÃíÛ&þûõÿ¿}Õ°6ýAÍjüѺuð¹ÛÎíº@Åì¢füà1D±ížDQQœ= Ä“${~^y<¼Áöã‰ÇqbhpÚÏÌÊ:t/tå&NËJô ïï@ý¢ªŒèÁNb‡ÉïaÙÌ­¿ c¼\ê(r±CW|)EžÝ_*°Ìíƒw˜Ð7ÆÄÌc™’b&ÛC¡¯ÿ¶=(žåü*˜¾ØmÜ)³ƒíâ·Øæò|æ3Ÿ!ÇQϾÅ&‚`<—[ò!°›Îm®18†¸«8ü,þ7(Ñ#šÊp‹nW¾!7ÃJ:#zH÷k*¦'ƒVqey(À8Ö˜É#Wœ^îüîý¨¾‚²Êý÷·ÿ §_?¨© ëôÁ[s}ζm,x؉D }N"Á ÷Øf·vi<ȯ~êH·¦ú?õí¿Âª¾ÏÒ>ÅzûíŽXjsÿOžÀù£'u•e<5׃aó›@Ž"~î-MeV~ð&¼ÿ‹wOù¿Ý×ü3ÃÚucU»ùZ[‹–{}Xí÷ÏñdFÙl½?‘d@ÊdvPƒ‰$Ù^ŸYÇ¢Âí,˜qx­'25Ë0«èážö—p:ÇÛݼ‚€؂ճ‰ÆTóüÀ3,êUÃÇ/tc?“»¿µŠ¦4”ˬ?$v˜_ì`@ý½É8Óɹý¥fÌ]á €eŒ\˜›Ý/UR—b¹þÓ­ø%<&”Ùb˜ ò'‘ô(†å™Ò³Îêy3ঠZ CH)FÄ”žs–ƒùÉO=Œ…‰ ‹šš466¢¾¾~âŸAv'" ¡½½ápápØV¼ÐÚÚJ¢1‚ tA™òC @ee%6n܈††´´´S‚(8& ÚÛÛqøðáÂìç·ä÷ÀVÝÆYi\½$r qUñùY>ÿ?/<¯© ë©¿ø£¹×!z–ÿ!¾L›ÊòPôÈÃÇ üÎ[$v˜Êñß¼Œ_ízDs9ïÊ›À ®œí{†Þ¶£[Hð`Á`°@ƒÝÚ¥Iðpôq¨çßÐTÿ5w~_i¶´OŠ,ãüÑ–ÚI<÷­]ú~`6*uLN~KÅ0zFÛø.®[‡}ó3þÿà™³xñ»ßƒOÒ¶úŠ*Ü{ÕÕ¨­,·ï÷xP˜ã¦yÊö`ï}JZäÌÍ媪"šLC”[¸…gT¸`˜Â¹zÖ,À ÑCLñÕý¯ä,z€o\³[—Ö×,«ËXîñÍñzžÅ“ßÃsÇåf'—¾ä[ì0ñsÄPŽØà¤CrÌœåƒ./ʧAöÍ;d,LŠ1Yɬ—…ú@7ã—¨ ;˜k‡ÄíA²%®ˆ–ÒÕŒ0ØÅòð®…³:™}¤“'±ƒ ŒJiŒJ2„ª*Ê$n.—Œ‹&…‰ìICCjkkQ__?ñÈ1AØšqQÃäÃ[C|wgÛ¶mC[[­µAdÍž={pÛm·‘#lÏ󨪪ºuëÐÔÔ„Ûn» 55t¨‰ ˆÂ{ž@X™Í(تÀ-û¸~á‰CÜUÜ~VFNAz÷šÊ0¼ÂòÏÔ€ìE¹Ø¥,E>½ÓýÏ?mºê•5¸óÁûáôzÈéc¤FãøÑþ©ærÎê5pT¬ÈÙ> J» úëÿ.ûϼîסûm ÏêüÑOÿÁò~ œíF| j©Íî£'ðêO~©»¼{ÙFð¾ê¼ÆÃÈ»/jz¿·ªŸÜù½Y_íëÃowþ ¢g» iÛr­›¯Ç…,zà8Êö`ÇýŠ¢fñNú¬M‹ˆ§%äóãËÅs(s %vÓ€Õ IDATç@obÒ,£Í=\V Èøaòv·«Ëýà'V³‹ØA¶Í&|È»ØaFd§0Yì * :G£³ûkZùJ§•·ö-;L|3!e2ãÚg·— Êì@ñ[ÌÏ•í?Œ†çí!Í ­Èˆ¦u\ÒL#À»à`¹<µœxyÆÞŠDùaÓ¦M¨¯¯Ÿ7ÔÖÖ¢¶¶–œC„­ …B†q‘C¡Æš ¿ß¶¶6455Ñ 1/ápõõõ#ê*U†ßïǦM›pã7âŸø¶lÙBŽ!¢ >o& ì”)m>²>À0Ä]¥ççôÁ¯h.£'Óœd)zà«ÀúÖê3¡1˃aY,ëâ8üo¦ÖOb‡¹ùïoÿN¿~PS΀gÅæœmÛTð€H$bèá< 8êH7äW¿§ÙÆW÷ý—åýJÅFÑÛ¶Ünèé½8µïu}…åá½â&0lþ3Œv¾ ELj*ó‡¿øéÜnñ8^ôgè>ô–!í[îñá‹k¯Â–ûª:ç=™Û9nç´ËÞeܾ$’4å%YQ1š‘’dK… €r—Nž+Øqˆ¦Sõ÷ÍÞ9v¦èážö—p1ÏÙîW¬ÂŸ_¹ PLôÃ,ïñ Ôù+àã{Ц¯A<° ‰  (ªŠŽØ ;€°Ì]–£ý<‰ÆI¦aØ¢H™È_¦Ù ±ƒ¹vHì@ÐÄHì`ƒéDE‡3%UUT9òñ%;s)€eÆÞŠDÆâóùpíµ×`"KÃÿÏÞ»‡ÇUÝ÷Þß}Ÿ›4#idƶlŒ|m±  –Iˆi¡An ¼=µHRÞ´¤EÎyßš“û-¤>åy“†¤‘Û4MN¡GnhÁé ‘Á$ÇÆ6F¶G¶lËÖm$Íu_ÏcɺŒ¤Ù{öÌì™Yßçу™Ùë¾öo¯µç÷Y?5‹B¡B¡Ð„ƒU(ÂÑ£GK¾Ý?þ8ÚÛÛÉ ""šUÍÍÍyu:¥Ø P|5(§`ì usý`-~qÊfU† Ic€*â4M”8¸k_“oܸwß}7zè!Ò)DDDE£bŠÁܰ Ìâ{§<³ä@dNW•w?K§÷AëÖ•&ø ­æh× °ÞìÜ\õDy05ŠQ~æ±Î(zD`‡¹uâç¯ãgíßÑÎÕxgÖ~ÉöÑS 4År}B€‡ÈçóY²ô ÿü @Ö÷BåÁ}_C]à#yoÛ•Î@Š'òZ¦OàgßøbCÆNa]^Ø­/Ø|ˆ]xJ\_dŒßßû,œ^ïœ×¼÷ÃÆéƒÿÛ”:ÚO®ÛˆÍu>°Œ5¡y¡ŠJ93 ÊZšÅêLïÄ«i²‚¸$CVrWi†¢`ã886Qr8LJ1HcƒÓ@Ý##ØõæaD%)ër·/YŠÇ>².Š7¿æøž¥i4VTÁgs䬌ë×PÙçAÑ)ºYçZa3ú*°Ã¸N g”ÞΰXâ¨,^Ø!e¤”=ô@œÅ˯¿Hd‡÷Ò™Ã*4 Ë Ë ¨×ìMQ¨bm¨bm A e²)x6Ý('R4ƒÉùë¸*CTehРЊ-*€¦¢š³Oò–sQ3'pè¡ãòq!ÀCdEàAåÝ^ó˜>cõÎsІôQ†›[Ä­<˜÷öE‡Â>1ïåöw‡ÐõÜ~Ãé…Úåà«–dN$úNBÕGßÝõ—_‚Õ+ç½îÜoâíïþ½)õ´3,>Û¸ŸXÞP¼ÐX¶<£=h¬ƒ¦¥ ‡¹’\ƒUƒ¨(Y,Mgð,ž¡Kj,Š‚ßô_…¬¥ ·@ r=Üèvcïm[à‚`^?duÁgw¢±ÒÞùÉ”ˆ&ÀÚt;ÄL³CyˆR¡åv˜¥-9€z¢£HŽŸã&GÉut‡Ò‚ b£ˆ+rFé]U )#‘',;L,$õÛ˼=Hd‡2.ƒ"ý•Ó2Jk÷‰QŒ*É9¯©døx'ˆÈ$/¢Å·7”5ª¦AR°4 TjïcaXh.à¡_Š!¢ÌÂK x h´W¡‚ò89æ˜Ä3  }ÇñÈ =èÕ—¿üeÜsÏ=S>cY›7o˲¤ƒˆˆˆòªÉÑÆÿmG§bˆˆˆ¦ÛÖ\Dw`ªo³ðwçˆäP¸©Òjü2´ØhâPÙGƒ ( n·MMM¸õÖ[ðÐC‘›ƒˆˆÈÒߌCG-X]˜¶Yt/ÀØ‹¬‰ßb~º‹ôóŒµGÚZÉà—tçÅ/Ý Ð|Qµ?Ó(¬w+h× 2aŠjpEˆç÷›–ôéÈWpè{ÿ¨+ ÍÙàl¸-«r­ <ØÖ×××eší.wàÁçóy [­^F€m¨Ê;Ïé.kסÿY6¿ˆØP¸ eÿç7¾ƒðÅ+†ÒÒ‚ Îú›ó?'F/#ÑwRWšµ;>µpÆ×GðÆÞç>Á”:¦q vÜØ§³æýŸ ô¤"=”Ëçš…Ë׈b*ÚC©ï§óT‡ßô_ED–fnë4´–ú›¤î‘|õ×oãJ,fJù_ÚpîYäGÚãY3íƒü~W%ü®Ê óÈ`+œ+Øa¢ `®T÷¾Ê<}¦ßS9Îj9ýɆÅDFéëp0œÎò©Õ±Ñ^E¢<ýGîEx€.޹&ª Š„¤*Ϻ`iv†ƒá,YÿtÀCXŽcXNÎc@@QPÅÚ°ÂQ“§É‘Á$ž=h´~ û.¼Kl€ÝxãøÞ÷¾7ãs¯×‹µk×’"""ʉ¦Gk…Bu`*Eýà?@kk+é"""´¶¶bÿ~óœr(¾ܲϲ/.ª ©= uäÔhZübÙCàñxà÷ûá÷û±uëV´µµ‘†ˆˆÈ² :;;ÑÕÕ•÷ˆoÅí8ßç§»H?ϸG2¸F<öµŒªC¬ï>ж…E×òÀ!¨‘Ós¬©‹3zEÙK!^ø'@“³ÎŠÀú5zµÿÙÇu§s6Üš³.—_+†­Ø%x0S>Ÿ¯À/¬V/#ÀÈõß,îûêÉÿCS”ÐwâtAú7|±ÿù¿3n j Ô4äµÎJl±Þ#ºÒ¬Øþ1ÜôGŸÒ•FŒÅpxïó¸zêSêý‰ºüÉÚ5p E=Ðt*ÚUÂUZ”¯¨€$•nä¹IBpp²¦^ßÞM.? ô‘$ìzó0Θôrfû’¥xlÍz¸¦Óî9„Æåâ8¬ª¬žíAW=ò;Ln E,“úÓ“GFu¤r܆9ÊÈìƒÉ8“ñŒÒûlNTr‚Žò­;Œç©É ¡ÙÁzm!‘Ș”ñ–è—2ƒ+ë„ 8hkî3ˆŠdíÉ-V$°ƒ¬©“’ÔÌ£<Ñ… N€@[ðW4ƒÉØ´¶)¸›?ñ5àÁÉðXj«DeN£ˆ[y° íìï!)L(•÷_é‰Wÿhá¨ÛÆQ•·úªÉ¢=¿Ö•fÁª•¸ë+_2TÞÛßý>νñKSê~Û‚…xò¦p ÖŒ’1ô@Q)è¦É†©å'såA+ÏqèFÐ=:r}»7½y€nt»ñW¿s |œËÇzNâ,EÃïªDÃ¥¿œLN-ϸ1| °Ã¬¯-b²„ÞØXFék;jx{†å[v˜X*€(ÞîÈÖì3Ý¡ƽ4žä’:làìAr*šxÎò°ÃxD‡„bü¤'ËÃÉZçG‹a1>Ü‘’ó'¼<¸Y*Y‹xWŽjH›À‚‚ÉÀ4¿÷c¾@ìA†Ú¾};žx≙û:–E €Ëå"DDD4¯ºººfDnÈ÷©«DSµuëVtuu‘Ž "*cuvvbÇŽædÆØÁ¯ú(¾ºÈ6¡ú7ÔJÿ!¨cg %.C“£e ‚eYx½^¬\¹·Þz+î½÷^lÙ²…Ü`DDD–zÞå~ =ëÁ6üñ,`þžWD¹î.ÒÏÓ•í}eðmÈ¡ê+Ó¶œï¾â~òÔX &Añ5 m‹èPŒ2vøxÛç±æ£w’>5¨Ÿµ'~þº®4\åBØ|« —I€‡2‘U‡¸ïÄ|wè·[ݯ@=óª®4u5xpßî‚´3:Æðù‹ëç®ç:ÐßÝclsÀÙਿTOB;ýš®ë³àø¿Àñÿõo¦ÔÝêÐCÓ…F·;³‹Y6õWö§ÕAÕQ$ý`¢Žb ‘œ½ü<@NŽÃ—7Ü„-ÞÅ€šMsw±V¹'E{°2ì0¥¹ÀÌñ!£ü3Øò›âÐ_ØTôDG2J_ÉñðÙ\”_°Ã¸$•ÂÚr2¾µÊ(СTƃÌß©&KSq.‘y¨ÍzÁ f@T>ûYS¡¦yESXÊD0¦ÁÚþŒDt˜s=Ì p0ÖˆMÅ›àÜÃrÃrrþ„¢ hØvšÅ¡2µ£²›Ä‚<ÅŒ‡å$šßý1ŽF®Û¡~úÓŸ¦\.6mÚD:ˆˆˆhB“¡†ñÈ ===¤c¦?Ùøj@¨]±`í ©ˆ9´cÉ Ç(-Ö M‰¥þë…6zš8dJ=~ðƒ µµ• Q™ª­­ ûöí3%/¶þÓ`ªoÉãËkm¦Õá#PÆ>HÙiq¨ì!Š¢àv»ÑÔÔ„ÆÆFÜ}÷Ýx衇ÈMGDDTP…Ãatvv¢££#ç‘å(Çb°7þß „K=¯ˆ@¢8˜9ÏÍšX/Ä“ÏêKDóà—î$ƒAT8ØÁR:ñó×ñ³öïè3#œ Î†Û —iaàaO__ßnÓl>J xІº¡¼óœît»ýÏ‚µõâ±SÐ¥ eG‡ÂøÏg¿)‘4”žuya_´>oõÕ <ÀÃÿøý¬Ê<÷Æ›xû»oJýo[°ÿϺ<.Á’ö`•§ >‡#³‹&=PÖ>íÓòû £å‹ ª¥Ñ¨ƒ¬jøMÿU$æ²Å³@ß:v /œ7­.Ÿ\v#v._ Åë§,lýÎJøó9FQÙ;ºgr¨‚Js1&tzl(£üí ‹%ŽÊyÊÏ-ì*ÁDàÒGÆ!ÎâÖ³Ó$²Cùõ•¡rJÖ–è—b]ËQ4l•îDƒ†„¢@ÒÈš Y›ýOƒGÓà(Í€1AìS$D¤¤éùzx;x‹@D²¦bTJ@VÕŒVRá¢YP r~ÑŠÝA€3åóùvøšÕêexùàãºÓ<¸ïk¨ |¤ míëÇh_áN² ½Ä;?:`8½Í·\å¼Ô5vá=(ñ°®4Ùpõäx£ý9H±ìO¹}ÁB´­kBËfI›  z (€ãšFQJ+âò5 HŠÅß–¨Cʱ:"IöCžË)šÖfæ÷Ͼ÷ž©ÐÃn7¾¸ Žªëeçv/ÇÆ°XUY/ÌÚ_Yi®"HÐ4Àec•ìç¢aH“!©Yò¯ì¨á퀖:Y8!ËH* äkiYšÀ0°1ìÜ”–&ÛÌiÐq·^[Hd‡òÃe”öü½E\•3º¶Šµ¡–s€¨´öヨÉHš¹€£Øhv&Ã}†TMCB•“Å´Q.ÌKÓ¨æ­u_Å —“cè熡XŠ‚K¡@]{™o#L‚€V~ªsQpì*šßûqVÐÃG?úQ¼õÖ[ˆF£%mJn¸áüøÇ?žõûM›6¥ADDTš 5Œÿ• ð¥ë©å¨Åפþ[¹"õo¡&§›-9ùÜ?@ûÐp$ÊQÛ-“ö`©è7—Ç:‹Ëâ0”‘ßB9$û¡)q@•ÊzÚív¸Ýn¬\¹---رcêëëÉ JDD”…Ãa´··£££#7‘éL…ˆã}~º‹ôóŒ5cH:ý? Žuë[‹z·‚v­ E”_ØÁ²ú§¿xýçô=Ó…Úåà«–{ÔËQÑ»‚fʪÀCdé}HV‹ ¼ó´!}ÝÍ­âÖG,H[eQB߉Óíï×ÿÿĕλ‰hŽú› ÓUzdxØþÔ×PU¿4벇{Î㵯£, ‡Mµ à⸠'•ŠôÀ0(*i%P¾$ÙD‡!°¦;ï÷Åc8ž»Ž4fæÉ?¯ž?oyÏÔÚ=¶v>Yß(”9€€Îñð v¬rWpl·8ì0ù»qð¦ôçoŒ` ØÅ8“ñyó¯wºA©@T§Óò§( –ƒ“ãMm_N`‡‰¢šŠŽ“/ÛC";X«œ¼µƒDvÈm9¥ëèê7ºÈÖàå®9抢©ˆ*’ªœ“iKƒ‚“áç, ;$UWÅ(⪌¸,!¡Êà( .†‡‡µ›^ž›·A YËõÁÙø0Def´–¢!ÐL*2EBº6”ê78ʬà L„ÆÅ¨7u½ÙÙÿ!vü¶Óp–6› Ï>û,^ýu¼ôÒK%mN^xá466¦ýÎåraÓ¦MÄæ¹&Gmƒ…B$jCº'”P ð5 +W€j@9–€²×hƒsm]wñß¡\úwCi›šš ÉÀ•™‚Á 6lØ`ÂÛaý3¥»qÎâR]v¼ÿÔÈhâ4qH÷ÉÆ¥&–eár¹ÐÔÔ„[o½÷Þ{/¶lÙBn\""¢œª££»wï6|È z Î÷ùé*ÒÏSö¼$ùÒ+P.¿¢+WÚµ¬w+8¢üÉDØaÓŸÀ–“>5Q‡^øù·Wõí?\^Øó'ÀC™ÈªÀÃhãÿÉeìĵû¨gôÝ,µõø£ïÿMÁÚ;xîâ#£+?1Á~ãï‹JO .8ësbH²ÿCˆÃt¥¹ë/¿„«WšR~t`oì}áó²ÎËÊÐKÑx½™C@ z`Y…JÅÑ?Û(e<¤w¬"›»~³@‡/_ƳGÞCT2ï &¯_nº >ÞÌa9°Ãd{PçpÁïrì0e¬¦óEv(AØHˆÜ¤Ì¦Ûà À 9H5-–fPÉ `Ç#ýXv—$’’{óBNÆ/Ó2Ht2‡³W¦ÀGÑh°y@TDšežˆª‚¨"AÒ”¼Tƒ£T²üÌHM…"ŠˆKÉÔÚ\RDä©ûÛéD`X¸9ëíÓ/$F涪 $Sß×rT±fµ!°ÃÄ„TfDÐk¿ð.v~Íp–õõõxþùç‰DÐÑу–¤IÙ¾};žxâ‰Y¿÷ûýðûýÄö‰&Gk Qfcå¨í\\‹Ü@;—Œ}ŽõVá^<*W^ƒrþECi9‚@ @Æœˆ¨ŒÔÕÕ…mÛ¶eo*«o[ÿéÒÚ4gy©YRÃG Œ}-9-~©ì!ðx<ðûýظq#î¾ûn<ôÐCäf&""2]9tAÄù>WsÒÏs‹²À©cB:ýœ¾zó5àý@¢¼I¾ú3¨±ìŸßߎæGw’5YgÞú ~úô7õÙš…«ÑX” ‡úúúšM{Fà¡ô€m¨Ê;ÏéN÷gÿþ.gAÚÃà¹óíós¿z¿ùñO §çk Ô4ä´ŽÉÁsÏéJ³¥í ¨Û¸Á´:ˆ±^{ú¦A»ÖPí,g\‡@÷ºk&¢i€³ž“ŒåöJfÖÁh”;ÌYþ©ð0úb±ù³áf:wŒà«¿~W2IŸ¡œ‡Ö•«ðÉúåÀt§Â“ûËF3h¬ðÀ+Øg®a‡¹¾æ;å6 Á¢°Ã„¹PT\JŒ¥*X¬JÍÅ_Vðì g¸}yÆë”RÅnÛHtkµ£T";ä­¿ò0-Ý–ÙÕ/Å0,'æ½®†³£&'Ûåožät˜~7U²¶ëB, ;¨š†³ña¨Ð AȘH{ËÕr¸Þ¼þ¡€ZÁe¹i$i zâ#Pg3<’ÈŠÉ@Ta‡qñ @O-£õÄ+Øù¸ñw-·ßާžz Ð×ׇ_|¯¾ú*¢Ñhɘ§Ó‰—_~yÎk6mÚ—Ë"""ë(ÏHÔ†4OŸŠå©h B ¨ŠÿÎ|=nßüä¿5ü[Ýéüq´··“‰@DTFjooÇ®]»²Î‡­ÿ4˜ê›‹¨åÖ†æÝ³FÏB=5’ýÐä ©e=—ív;-Z„uëÖaëÖ­hkk#78‘)û¨ööv´··›†Ï =çûü<ŸI?OÙ[q”8’Á/ëNÆ/Ý Ð<T¢œK85r:ë|îúÓG°þ÷î&š%£1|ûáÏéN笿´ßé(U‚}ì´»‚fÊçóu¸ßjõÊxùàãºÓ|â©ÿ7Þñ;kóåBÅ‚•ŸàÈ‹ÿÞ£' çá¨ÛÆQ•³:ŠÃìÿPWšµ;>µ`îc1Þû<®žú ë¼¶,Xˆ6‹B^@ÀëÕiU©ô ”(šM—Ë7åÀ9–ûžË&çÁ©3v IÂWý6Ž ˜ZóT´‡ðñÎkÑLr×"xx~g%<¼ LóÝ!]þ °ÌõˆºÒgò5a‡ÉßGdI5å\Iƒ‚fI& ÃãßWðì,§;}^a‡ñ$¤”ý,FÛFÅ-X‰ì@æ°y’4=ɨsØ(š¢°Ìæ CÆD³ÎESU$$2ˆä‘kU0ìgI؆¥8ú¥DœP$Ä•ô}ÆS4 •¦–]-8ÀRÖÛÓ&U—’cÒ9Ñ$%а„¯¼³˜±wʇA䩦žoïÇѱ«†&8ED IDAT³|ì±ÇðÀ\_G"øÕ¯~…þð‡8þ|I˜—¿ú«¿Â–-[fýÞåraÓ¦MÄH¡PhÜ`ꩤ% ÊQ—‚u K&þmlnÁßù”8Ä£ÿM÷)àõõõ…Bd‚•‘vïÞ={ödÿ‘ÿН.¾²I—Z¢-â0”‘ßBœ…–¸ MT©¬ç7˲¨¯¯ÇºuëððÓHDDDYí±ÚÚÚpàÀsö#|5¸ü%ÀØHçæè1^dõü5@i%žxZü¢¾g¿ï>ж…d€‰r*³`‡·}k>z'éÐêŸþâIôŸÓ÷NÔæ[ ®Ò˜qŒ¼oÅn Àƒ™òù|]¶Z­^ÃkƒÊ» §WŽ|ÚÕcºÒ¬¹g+¶?ùXÁÚ<Ú×Ѿ«í÷³çñÎ?@d`ÈPzš³ÁQ3(šÍIý”Ø0b½Gt¥Éð0®·¿û}œ{ã—Yçceè¡ÎéB£[ç½hUèA+ÑòõDy °CƎᲪ"84€ˆ$Í߆4Ð|ëØ1¼töŒ©-˜íAÊCt‡Yy}vüÎJضx`‡Éš>” ì0c#¨ªŽÇ³†Æål6ãôù‡®IUS‘ŠÑ¶‘ÈÖj‰ì`½ùké¶d¦˜*á’I =Ðe²33Q¾æŠ 1EBT±£EÁírA˜/JSÔ“O@š#bs“i^Dƒ”<¼¼Eï3UÓ0,ÇQ®C¬œFÃ.iXÀ;L„¡ò;Œ%ÈSþ?,'á?üw‘“†³}á…ÐØØ8å3—ËŠ¢ðÜsÏ¡³³Ó¼“³‹á¡AÓm¶oߎ'žxbÎkQWW""¢Ü* N]]]ƒ–°/–YrÕ_º2­aüßæ¬Á­ýûž:ðäsÿ ;ݹsçà÷ûÉä!"*™<¬¦„!‡ ¤ô‚šèƒë…&é†áJj]@QX¶lš››ñÕ¯~õõõÄéRgg'Z[[MÙsQËÁ­|œtjΞÍr˜1ç¬5@óJýÊà¯u¥a<7ñl$ƒM”»yI`‡¢Ò¡þGþíU]i¸Ê…°ùV*¯€šL+k*بêFÝizœ(h›]µ…?uÃVéªÝ–7^J•Hô°Ô\Šš|ÂúdÝòègÑpÇmYçsøêe´ b,n½S>z£ôÅb:×Ó Š™;á[zVå³LqôAÁë@érÜfij/\7$PgnO[·_ÚpœœyNdQI·Žßþ9º“ÃÙ­d´ úlõÅcxk §F†PäìÊÉ7ì¤ìSRDP´–míïGMˆì0%?1‰)0±a äqlnËÈE;t—CÁ’°ƒ;XMv°TšC½àF%#L€ E£’P/¸ ìPL÷Õµ¹"ª Ÿå`ð,Få$Mµ\÷Iš2á̯h*T2£®/c( 5œõ6V8j°ÂQƒ¾>ÞYœ°Ãøý’œº6ó°º6> 7kü`ˆ'žx‘HdÊg‘H ࣣáp¿øÅ/ðøãçÝɦ©© ŸúÔ§RuRDÜSµLw‡ž÷šP(„D"An"" ÑÑѶ¶6477ƒ¢(lذ;vìÀž={pèСò…;¨Šå`|wYú ØU»ÀßümpMOƒ[ýE0‹ïí½Uì ¥ÛCjyÚXšðÜön»ît]]]äf#""Ògo\¬•N{miӞݳ‡©Ý nÉCàWþW랆ø&8ÿsÃGA¹n4ô¬(Ú×%š†3gÎàûßÿ>ü~?ÑÞÞNnb""¢ŒÕÒÒ‚P(„ûïÏþÐSmìC(—þƒtê\=-›ÄDÀõ_k©œ Rë^±\w5q™ :QÎD`‡âSݺ5ºÓ(ñáR놀©û2­JtÁP­ÿ¡;z¥ýÝ¡‚Õ™f8ª=í7[… ®Újøoi2þp‰ @Í͆âô¿ð‰öæ´ÏÌ„þ&øbIÙr÷S÷ÈHúSîç“$¥þ¬°!+åò)jþhe;Ó¬ÐCZãGòÌq¸géRì½} nt»M­Û™‘÷¹Ï‘Û‡ˆˆ(#y<tvvbïÞ½Yç¥\úh±^Ò©¦r¦TNHËÛ| íú#Çjâ ™D9ŠSF€UJ@•Œä¤Q–<8ÐT‡A–L+øKrQ±°Wñ!]é.yµ…ëgµ±¡pÁÊg,Ï£.°á‹}8{ÁP>É«‚±Wæl¦ÖÏìüÌÒ-~pî_f•Ïá«—ñÌ{ï≛6Â!XÇ<ÉšŠãCCØT[ –ÖùrMQR8.åXSˆY9ˆeSQ5ˆfßV< z "Îð¨H9épSUÝnì½} ¾uì^8ojË^:{¯^8ÇÖ®Ç= ëSàEÖ÷…þþê‹ÇÐÁgwÀffla­;hÓl•¢Ì5'xZOPë§÷{ÊÔüãóióOȲhÇ0àh6†x~XvÇŠšzNYYù:¿žË¥ÙA+ç2Êv *rMŠê0*'­:S`‡qIª‚¸"ÁÎp–©æ¨rÂHÌÃA›_oº˜ UMîÝ7 v•Nµ…¾^‡ÖEkÑ5|û/7”ý›o¾‰_|<ðÀ”Ï{{{áõzáñ\?xÄï÷£µµ­­­Ÿuuu!# Nüw\Á`pâw·Û@àú!=@ÍÍÍðûýðûýië×ÒÒ‚ýû÷ãõ‘󨷹ѓÐw*üáDZeË–9¯ÀÀÀ¼^/±ÑDDs( Nù+g˜aŠ™vÔjRÿ­X‘ú·P“Ç5wñ/Âiï­P®üB÷|$"""Òe¯í‹­±ÎÑå–oYÏ ç2À¹ LíÖ)UQúAž…–¸ MT©äæ°,Ëøþ÷¿ýèGøú×¿Ž¶¶6rcÍ«¶¶6´´´diOý#¸5O–WçiK\zë°RXhs¬1; Ä3ÏK¡Éc Ø 29ˆL“~—ÀE*Áé@mC=úÏõèóø0hn¡~sÆØ@ÉÑÒ~îhZy?ˆ}>Ÿ%;`0ð—Ù»cÿíÒ¯u¥© ¬Áƒûv´í—O|¥€ŽË‰ÑÆ®@NŠx«ã%ÈëB .8ëo6½~c§_Óu½Ó[ƒßßû¼ôݹ7ÞÄÛßýûì°‹–àñõM° Öb²¼6;ÖVW´¶Àóù‡ iáò]¶(¥œ]¬¶×,x¨ìÏÈŠš‚2Z©iw·¯ž?o?†h¢Ÿ4y½xì#ëÑh«š½MZ~úËgsÀgsÂà ³ä‘kØaž× ó¥g˜Ô=ÌPœ°ƒ¦jèÇL¬ßüßs ƒJ^K1ÖrLÖ4 )ƒJÅY<_vº`‡LíW1´Å²ãN`¢"—DQÅ‚Îi`‡‰ý;EÁ+8-SÕÓ±Ô)P’ªL‰öN‹xŠ5µ›jWñÌ9QNE-3cßTHØa²xe ô·÷ãèØUCÅØl6<÷Üshllœò9˲ؼy3X¶pï^‚Á 6lظ¯¦/ê‹Æàt:ñòË/gÔ›6m*h[‰ˆ¬¤P(46tuu¸%T| èÊ K& ‡ÂíJ+¡tô«ÐD}‡s•ûo—DDå¤öövìÚµ+«<ß=`ÞS0;—«Ë-Ý–WG 2vZ¼7õŒÑãˆX èììD}}=1DDDó* ¢¹¹9+èYòI07l+ýÎÒòž°t÷Ñ¥°É 8éôÿ€Ñ÷Î’õníZA& ‘)R#§!dÿîŽÀ…Ó¡þGþíU]iøª%j—ë.Ë ¶ ðÐ××gÚcƒ&SÊz’\KÍÜÖëNÓ 7+**‘Hàë_ÿúÌ%¿,ãÔ©SÂ@ 0áÀCxF£QtwwgÔ½½½ÄP•¥Âá0ººº°{÷n477Ããñ ¡¡;vìÀž={Êv`ì *–ƒñÝféƒ`Wíó·Á5= nõÁ,¾tUS~`‡ïo5þ¥nnGUêwô QˆˆÊG“#…«3ûò´§¸ªC{6€[òøÿÂÚ§!4}\ãÀ,ø((ç©Ó˜©âu… ƒX½z5~ò“Ÿ#ADD”ѳ´«« î,~W.ýGÉÁc3¶YZ6‰‰¨i¹$ëͪB¿Ã±&’ CdŠìPª[·Fw96L:n‘c­Jy±QÝh(]÷ïà#¿Û\°z;ª=_ì+\¿Ñ4l.$Æ"ð,ö¡®i5zž4”—8x¬ÝÆQebý¬}Û6Üq;déá{žEQøÃ`ë8/vŽÀ#™;}OYÕj€$Ð9|ÉVèýV¡Êgh@¦RýL`˜ ;L,®AGzP¨”s,;ÕQ«ÑíÆ ÍÛ°ÿÔ)ìÿÀ|›—ΞÁ«Σuå*|²~9 Qy…&+,%–’EFá³;ᜰ1¬ñrÌ€ô´SQR; **^Ø¡PßS  iƤ$ÀÎrÖ±= cIÎ]ù²{Eg«‹èyC`‡—C`¢â–¬ªK ¨Vœ4óÀãJ*2 ì·Ìõ5‚¬Í <8hó×<ÍÑÄ+AØabB²€íúÚÌà èlÚmïþØP‘çÎÃóÏ?/|á S>ÀÀÀ¼^oÁ†±¥¥ûöíÃë#çQos£'¡ï4ÂÇψ^‘N½½½ðù|°ÙlÄh•´ººº¦Doèéé)Û¾ u©?¡TÅŠÔ… îßJiÑ­Í:z‡É MDDT6άË-ßËWGílíl˜ú±8 eä”‘ã€ØM‰ªT3.ãSŸúࡇ"· Ñœ‡ GzPâP®üÌ¢ß+»GviîÍLÚW—Â$Ëâhûbè} ­Šƒ`Èô!ÊRv(Ôdš*[ÞO8Sù|¾æ¾¾¾.3ò"ÀC)‹µƒªn„6¤/´Roðý‚4ÃÀQíAl¨p/´m•)àï¼á‹W2”WüÒ18—ÝV2(™=¼pú}ÔÚìØ¶t±¥ ‡ãCCØT[ Ö´0=°ì̈E¸7°œe™;LÌã÷is ;LÖ˜”MQ“×ÙÌS–IE!RÔÂ?gòÙ8‹ëhr[™¿DÅ­¸,#¢$­9e2„@TËT[ $3¨‹åMï.;ÃÇÄSU€‡Àz$]ÛG]SsÕ|mÙmØsö—†²{饗°eË–'èž:u ›7oËæ½Ukk+öíÛ‡9‰;ÜK ­­­ó^'Ë2º»»±víZb¸‰JF¡Phlƒå­aÜš Õ_ºr(Ç’Ø Y¯­Kt˜#ã1î¬EDDTú²þ½N@kWgž ø*0µw‚©êä¥ô¿5zZ¢šµì©æš¦興(ce =5ð@ óöØ¥°þ0±8ÚH„‡ÄeÓÊW—¡Žƒ»v ̓vøÁxnÅV [¢Rc!;”§• ¼½: óþÓ}ÈºÊØ@ËÑ’îϲ|>_ ÔÛH-X§x8óÆ;À“…­·³ÀÀg·fY¨×§W}ìvÿõ dQÔ¿Qe$úNÀ¾h½9cjÀ9òêɰ`õʼö¡YÐCû‰ *8·ÔÝ`™û*¡È¡ÑhHÀqè0z Q R§”KåÞ¹ƒ&4µžôÐΟ‡@¤V˜©¼Þ‰h/=czo\‰Çðßßy M5^ì\¹ŠÀtnÊýxN*c Ç@2–¢RQlN¸X΄rr;L|§¥ÀY >ä Ì©{šï%E1¯oæùžše\FÅ$¼6EYÇîð,RÏ)«>gŒÌýBµE+òü3µ‘¥²†!m!"2¤˜,!¢ˆÖ¬œØTMƒ¢©`(ºàU¯á¸”›û½ ͧÌ}­ç`øâ™|b¶ ûü°Ã¨’ĈœD\•AS4‡J†‡+›~ÒóXUèÔþ‰¾^ÏÝËnG×ð¾`¨ø§žz p¹\Ÿƒ«V­*ÈPÔ××£§§gÖµó\:sæ "‘È”6ͦ„Ãax<bÀ‰ŠRã`ÃøË1zU±üа”£´s ÀØ‹hPž›Új ‘å4¾,FWæ½Ì ‡ ”‚PÃA(c§¡% %.Y‚‡/^Œ-[¶ƒADD4§ÚÛÛñÈ#èO¬Ä¡¼Ú»¹”¶#ÅøàÎï>»Ö¹,бƒâ«¡‰úIV—AÛfUtÚþUjä4ÔXœï>P| ™Ä%&M4vØð‰{Šv?¨ÜVá*‰q­[·'~þº®4r<¬xÐ@—ü=B—¹°ä¯[*ï6-/j~'ûd4–‚ (ÁåÃö‡v[åuƒéª­†ÿ–&ÃyÉ‘ˆŒžqÓ ÅCh6Üq;nyô3Yå“eüÕÑwpv`ÔRmëF2;Ý~.I ((Yb_H4]Æý{Øaò¶w•» ~WeæyÈT |˜&Çá±uëðÍÛ·à‡#'=stp_üåøêÑ_¢OLZ.zŒ¬ièEð›¡+xkð2BÑQ$ÙØ˜h9„f̱kàCRLª«åh.çPЦå鎤æèr c’h1»C—Þ14_€9ßbÏv %#¤<ç0QN5*%KvÈ÷a>¹>Þn–½ OÑðrÓ»ÌÁItIÔÜ• ’£è£ˆ«©õ¹ªiˆ(".‰ô‰Q¨F ¥‘ǪḚ̀ÉM;àfCmÄ3Ï<3ãó¾¾>„Ã…;x¤¥¥ðúÈyCm ƒ_ …ˆ'* …Ãatvv¢­­ ÍÍÍ ( Û¶mî]»pàÀ’‡(¡TÅr0‹ï»üóàÖ~üÍß·ú‹`—íã» tå kÂÚôý­ftË"h\f20NÄ^•—ü~¿Åì\n.·|{,_ܶ‡öÀ-ùCðaíSšþ\ãÀ,ø((ç ¸J @‡$hš†üãÄXe¤ÖÖVÜÿý†Ò*ƒo—ÜvdjBòãzM9þ—›Ò,< ô—}±þꉃY•©&.Ïíô®Šú^T‘Lè’&š2®k>z'¶þÉeˆ±8ÎãÒÉÓ¸tò4úÏ–Æ;кukô?“cÃä¦H·o#]`=©¼ye¯ìÕºÓ]¾_ð~pV–G™NˆÕÖÀ»l‰qƒýóƒxöÄ;èS¢óïâMŽî0›²‚Pto öá7CWЋ\‡r}@ß¼žÖŽqð!1ø »~”¹íKóýÛ£þÍätÚ„"AÑTkÙŠ8Öz6~6»gE;MœÑI‘9LT•’H¨²Eo-c°€ìŸË&ª’Ð`¯‚‹áa£ÐlƒZÎÅB%hÊÜŸ£\¬`ètý¼KÕLØCÏÝaPŠO€iç¿’Ä…ä(’ª¢¯H£§®Í<¬€Î@‹ñ=Ñ›oâðáÃ3>ïîî.ذŽ#rwºõ¿{Ó<„ÃaôõõCNd9ƒAttt µµ~¿UUUرcöíÛ‡C‡•tÛ)Ghïæܰjøß×ô4¸Õ_³ø>ÐUM ¬! ­£E)9Ódߊ×÷;ˆˆÊKÍÍÍÙe óÄÝ Œ¸i—[ä¡TÄÕ)l{hgØ…¿¾ñÏÀ¯ù„õaõ»èþ!x:?‡Äãñìï""¢²QGGÜný‡íjcBKZ§!¦@Dû2är(Ýe]±\U³Fep‘%“눊BfÂoû|Qö‹ãÒÉÓÅë}060Xã[ÛP¯ß”–Ÿ±iŽàx(Ñ ÖéNSèà(0ðÀp,xçÔW}l Xƒ‘'4UF¼ïDYÎÁ›þèSh¸ã¶¬ò8Cû±£[‡N•5§Ì8Q–SàC6‹úBo2a¡:0´!ç¦âî‡ #;d{Í,Žû>»›jn›É²b<‰¤ÂD{€ƒÎãÓ¯½ŠgOþ&™~E”'Øaz9YBw$œ‚¯ 7™'ò•ݼÔ2ùŽšcÎL‹ø §üÀùø^³`\¶ ýgèëÐC¾lqâ¶X_Qù‰î0G[TMàǹD§cC8Â…ä¨þÓäIt’ò¼Oˆr8E´’…Æm•TÁ ¨·»±P¨@½Íƒ…B\ŒùÑ0†…)’è¢4¯­Ò ATˆi„¹aIS1,ÏÍ1©*¸ f=PYßx3öNÍUKñøÒ› gùôÓO#™ú²<‰ ··· ÃÚÜÜ<ñ#ºàl|šF¥‘µH!$Ê©õÉ,Š+2âª4cŽ 4;ͧÙy]\ÍFU5 —Ä14Ø<¦šüôï h€ÑúzýÛWÞ…®á 8:Ö¯;»D"§žz Ï<óÌ”ÏC¡|>X6ÿï\š››qàÀœŽëwV;sæ "‘\.WÆí…BðûýĸåM]]]¥±ÀØA9–€®\ÊQʱÔ„Ã{ ,<µ2ݬ‘=QŽòXš–“K‹Þf[¢:Åýà`jïS{ç¤=ì0äÁ_A;-q0á}Цiعs'ºººˆá(S…B¡)³í¯”Ã{~í+‘ÕÔÖÖ†öövŒŒŒèJ§† Ú»¹ždÃ1]”µ¨d†Õp„Uhƒeêø~-ÊãÙHn€býgCia åC\ ªm¨GïhcH IDATñ“ºÒ(ñaÐÜÂ̧ë’ý%}Ë”;ðP«{ªº`í€וîýWºÐüç­­»£ÚSXàÁå@d€†6ɱÀ³Ø‡º¦Õè=zÒPžâð°N/G•±›ÖîÞGœ‹Yb.š=¼xõÎ <¸² [Ge’#…¢¨¯ýÿøçãå½:€Ûù`ËÊ!€º= sèìßfÃäx(ô¶5ç§Øgæ¸ocª½852Œd<ó2D`p3_öî\µ Û—.ųGÞÃÑœô`T’°ÿƒSxñì™ëàƒz |0¼Ïv˜®ðƒÍ /oOÁfŽ»^ÐaÆ ZK9£Q×À–É =eâ|Íà~Ñr“?eà5’ ²ª‚¥u^Ë—ã3ÇŠš‚YrYN>ÚR ÊWd ´%ì0Y£ršE%+áÜ"°QñjTJZvಆ(‹ÂU¼ÃbÜtè¥iTñvCk˜¼KVfÝãÉšŠQyöù™T$UZ‹û&Iç—4£J•Œ`ŠÉŸS"ò”|;>ò»hþÍO0"'ug÷öÛoãðáÃØ²eËõ¾”etwwcÕªUyâ––8p'cƒXë¬Åñ¨¾—òÁ`pJ[æSoo/êêê w•‡‚Á :;;Ëp „êÐàXªb(¡fÜPB‹N­ÔÒt ""*œ<¶nÝjø™9„‡R‚,XÉ2f_váï  †ƒû^–Ìî·®×_2SggçÄÞ"“{öì™ñÙÖ­['¿ß¿ßŸut¢âx¾¶¶¶bß¾}ú¬r,@ ä`î¾¼Ø×E0¬”}1´øE]iTq´m¡±òX49³Sí•Ñã`*ׇ+ˆ §k°C¶An¼ecQɱ¢Ã³Ãy¬ ”Äp×­[£xP“rŸLŸå¾¾³b¥ÞmþƒwÁ:h—~­+Í™7Þ)8ð`wW€bhJa—)š†àtÌÓxçÍ_¼‚ÈÀ¡|WNÂQ3(ºünÁ[ý,†{. |þ‚á<¾{ú}ø]•ؼäSë&+êµ?-£õô8øSe»:eì Çès\¼ýÐ =JšÅë@Q©?M+ñ~0v˜·ú÷YšÆÚª„"£EF3¯ƒL2“ŠöÀLMàs8°÷ö-8|ù2ž=ò¢’”“Þ4 |Èì0]YBw$Œn„áåíð ©?–¢³›fÂã5å8OÓÖ€rfÔÒÙ‡<Ãã’T%óçF^Ÿ)ÀÆ )7ö”Dv(®çnžÚÒ/Åæ„Æ5(ÇgìP¾ó‹(g•’Ý›…Z›;H­¥,ÙD U¼cR E6%O₊vjÓm#4ÃR|SFÐS%P àdfÿáG ýk¹tD®ºUdSЃ–*#P±»o¼ »>ø…¡ìž~úiüË¿üË”È}}}¨««Ë8Z‚Yšìhá·¹uÝÝݺ€Y–ÑÛÛ›×(¡P]]]ƒƒiû  ¥¥…¾ëO‹ºzf÷!uV¹Kf¼k—ÕëN£$ÆÈ2Mä+ JÍð@ß°ŠNàaôJ?ú»C¨m,ì†ÆYíA¤°`åÛ*]3€Xõ±Ûü׃Eý!…T)Dß Ø­/Ë9~×W¾„מþFVÐÃS¿}Ï;·Â_]‘u}UC\”³òåì‹ÇP%Ð4 &Êà6Ž}^p!ÍVd>è¡Ð «ÃãbèÜFy(ØaNçwãŽû~W%<¼€ãÃóŸ<¹‘h->L+~ËÂ…x?Ž—ÎœÁþNå¬g'ƒ[| ±sùøX' dÒ¹‡f,þÅ8Ä8Ø /o‡Ï愇ËÖ ØÄ×$ª $ÕðÀ^rü:f¾ö° I³ŽÓ¦ªYùÍÍ–D]°`[¨‚÷WD1,%2ÊJRU¨šzºƒ³fá{ÉŠó‹üN@”É»9‰„*[÷Ö2 v°º(P¨äl°1 ¢²É €BS*8B±À j@2ý:DÑ;L¬õ Å€§Óƒ †MQÙ­Ïr95-‹³×ë×¶t#:¯vãаþw,‰DÏ?ÿ<žxâ‰)Ÿwww#Èo`\¿ßúúzÃŽé‚ù”(áphooÇÑ£Gç¼vÜÅív£¥¥»wï&Ž&E¦`0ˆææfŒŒŒ”ƳG7ÜPB MÍì)ˆˆˆò®–––´§’glŽÄ!P|µ~Ãdy;F¢9”ܳ—¯ßø§PÃG!õü£¡Ü8@€‡Vgg'Z[[ó²¯èééAOOÏàÌívOÍÍÍ$D‘+zߢÆzAW,/ É')ûóRXgùR®åÀ >¿ËlÙ™ÊuPFjf¿Ñ“(Å'yà´Äå¬ò¨m¨ÇýUNGÑöÃØÀ ÄX|ÖïUž’óºukt§Qâar³LMº L? Ö¬~úöýWº ^wgua g·Nóç«¶wþŽñWdÒè岜¼Ã»¾ò%8½5†óˆÉ2v¿÷kŒÄ’YÕ%))ˆ%eS®î‰\§ê$EÅXBÂ`$¸$ëß–(Jêdt«mŠv†)á~Èì`B?xx›jn€‹›cS•ÎÁV¥€È3—).ŽÃÎU«ñ£»·£ÉëÍi/G% /œÇ§_{Ïž|}ÚØŒèó¶EOŸe9²ª¢/E0|o ]Fo|l*l’ñœ¡ŒÕo¾ïHJ€8-€27 sÛÚIßséÜ ÝÁr¶']ð…vŠ´ ³x¾Æ#/åP–˜¿ýRLW–3°-;&v *^EeÑ´ˆ9y4˜ ;Ìæo©ý4Í Š·£FpÀÅ ˜ÌžÑµˆ^ÁY|°Ã,ö*ªHº`‡YŸ“DƒBkÓUMz²Ïé#õZ[dzFºÎ@ ܬ±°Îœ „Ãa ä}ÈÇ(Îću§&H»‡ºå!?åßï÷ã‘GÑU¿‘‘ìß¿ hmmE("¤"Q[[[ñÂŒtU˜Å÷]õEð¿ ®éë`—ÿ)˜Å÷®\‘ì åq“¯õ~ ´)m)¥n!""²ŒÆ2 ›&q0sÃdy;f± f]r~pÌÞnÚÓ®þ¿Þo•žÂá0ZZZ°cÇŽ‚î+FFFpàÀìÙ³Û¶mEQhnnF[[:;;É@¡ò ­6ùÙhL5é/7¤Yx>XO´£ÎàúÔh<˜Êµ™_¯ŠPÂï’§H$‚9U¥;Àpïì¾³,Ï£" ßR«Ip:P¹@¿Ÿ›šŒ”ÄrÄ4{LLH-†¬ÓæÌï¼ÞœÝÎn+hl•®´ŸûV7»l‰á|“W?„šá)¶¥&ÞáÀ–¶/€sƒw>:†ïœx јNB”!ʪimŠÉ2úbSõMÃXBÂp, QQ§mOæÑtèìët=ªDOzÍì0Ÿók†eØ›j ÎéÒ_†t |HYÁçp`ïíwà›·ß¹_À§À‡ƒ“ÀU_[æë3“á“„"£;Æá‹856„ˆ,e˜>G°ÃäïHH€(c‚6Ë#ì mlQò°ÃD§1©f”QÄÎ⪦aXJàR"‚ ‰QœŽ átl=‰Q Jñ‚DëHª©“¾‡ÅÄÄ_T‘Ìôäo‹ÀIU¤ê[—9®ÖIv *^ÅQE²î­e2ìÀÒÅõŠŒ¡h8nΆ6<¼œ'ËOü¹8ÞŽ6Üœ öÉvÓêšvP4užÈ#Ô¬Æ.9OÄ’Ö® zp1¼a“oØp‹S×eV@ÇG~×pîO=õ"‘©/Í»»»!ËùžÆ€?Ä¡RwúîînÝiz{{MmgWWZZZÐÐЀýû÷gí¤2>ìÞ½á09ÍÉÊêêêšr*ªµ"vP+RpÃòσkzüƽ:ᆬ7zÖÑ  9ä:+"""¢ÙÔÒÒbÜLÅ.–À#Ëb̺:å9ÌßvÚÓºúfÝ%Äãqb0Jp?á÷ûqàÀKÖïСCØ·ovìØ1@ìÞ½ÛP´E¢üËHôHmìÃ<ìÈ&cºr9×6ÔRcc_¬¿+äHÆÒIoÄeô84yŒÜH—~7kØ¡r·4`‡‹—!‹³ß#nß‚’ÿÚý¿’Ìü¾V[ÉßCå<øË©±Tu£î4£WúÑß*xÝ åÁVášõ»UÛ2ç÷s.nTñK¿-Û°ª~)îúË/e•ÇÁ‹çñ¿Ï÷BQõ­–EY…¤˜¿Â¾@Iã¬')*±$"II_4‰q衘"+X© ]býP\°Ãd5Vx°¶ªæº#Y¦¶‘’tÚr^/þùîíxlí:8¹Ü;pƒ»Þ}ÁØ€S³‡r¬¾D¿êC0ܰ”œ£ny€&—£(©ˆ’’ ¢v~r;d^ÇæÆ®Îg÷,Ò_EÄÙxýR ED|’³dR•1(Åq!9fô0W@hˆÊ"ú“1ŒH D ’¦LüE #RÉâsÌn¡—Цv°38œ¼s'*#ÉšŠˆœ´får;ÅÝa¾úÛn ðà`¸âl×<°€¹ŸosÀ™šóZÎE¼ ô<󬆳ƒ£èüÂ/¦®ËZ4âþÚFC% âÅ_œòY"‘ÈYôƒÙ&þ½Ô–àÁŒ(ápíííðûýضm[NTöìÙƒ@ @NÓ´°ººº,[7ÊQÆwØe;Á­ý ø{Á­þb n¨ €Œœ”VŠÑJiÑoâØÿ#""¢<ªµµÕ¸¹RâEúÈ"ÑJGÆÚÍ-ùCC¥ýä'?!F£DÔÞÞŽmÛ¶U´¸C‡aÏž=ذa<Z[[ÑÑÑA@}‹Êð±©'Cvûu”äPFÁ9h—þw¯j>£<PG“ËÂR#§¡„ßË*ÁéÀïå¿=ì * Fú®Î>ýµ5%7j—é&ë‰ð QLÉßGx°âV8G¤ }ÃzCéÞ¥«à}â(0ðÀp,X>=5É W5$%%'í‘5 ½Ñè¬ßÇDCÑéÑæ‘¢²RØ*Và‚aJ¨,;dÑO^ÁŽM57ÀÅñúóT¯E{Ó/]>yc#þùîíØ¹rU^À‡£ƒøâ¯ÞÀ®w^Ç«ƒgA)ܼÊp^„¥‚á«SÁ‡BÁ㪥 ®¤˜ÞÎåvR'"³4cØaNG:«ôγÆL‹øÔú¤ªàR2’‚´¹®“q19–ÓvÈššŠâ HÐæi° cr£ia'ó_ŸæsLpÎ"˜[$ºQqJƒ†a1nÝ©Â29‰äÆÓ,|+(ØD-÷{dã^pÞfnÐ…ÎŽÖ^¸¾RèÔ>i’:Öþ.ܬ`(»ýû÷Ïz{{‘Hä/2i €ÛíNÝꞣ}}}†Ê5å¡«« ­­­¨ªªÂ®]»ÐÓÓ“ÓþéééÁŽ;ÐÜÜüØ{÷è¶®ûLô;Oªþà XBx°„à@œì 8ØLr¨¥±óšPyH])ªL£*zbœN2‡BA*NÖåóâñ¯} k6wT|DBסkÙçihkËU_ð~ûž]†Óh)ªÜ²,íçAõ¬-MƼLëÃÉ.œzµü†Êqð4Ô—µîúì*õmhß»ËtÞòô;³±X¡ú¤g6Ý}¶~Àtú„ªâ™7FIviJ)m`D(™€¤kYŽ.€F qY)ü° ª€¦—g€*Y]‚a¬ †¢d‡óÈãK9û›Ö¢ÝgN $Y Ãó ŽîÜ…¿ìº‡7l´edF§§ðÌèkxbè$NÞ|1.µúé)¡]™ŒÄ‡Œµ¡ÏÉñ!%/ú:Rj[äM§·ZÙAÌvPsdà3¸Mܪm¦”ºŒÂq]Žüݤ®fÙߎy²ƒjP !¥«ËIv­iÊñrBÞ[´çÑ&úàš¿±œ’*o_GáxÌÈ)皊ÀYOjž;6TºÂCU@ÕÒûÁ 0ûZÈXêì†ÅW=:\ i‚ƒàA›èÃfw`‘ìPu‡ù2ååvà]è×ûL—öì³Ï.ïgUŹsçl5ƒy•ÕÄ8š ¦0¢ò0>>¾LÍa``Àö©2<<ŒC‡QâƒÃ`ù™…®auÛÓê Û>aïAØû5ðÛþ/pm÷ƒ­ßn¡/ª5‡ŒD‡j@õ):PPPÔ&º»»Í¹/yšª9Ø^ªæ`xãëÞ™3ôçJÆÐÐ:;;K¢XnŒŽŽâرcØ·o‚Á z{{)ù¡ªÎQt}0}~G«8Ôô°.6œñ˜ <£ðVy jŒN6'Z‘ \z1>pºãÈK? ËsćHâCIÈLþô„²¤@×M–_Øœñp<Ƹ²…ÕdžaÁ1lyü)Â/¹ *<ˆ['IM5TFL“KÒŽ¨"åUuȆ”®"®Êö(;˜|àÙÈç'öÖó.ÔÏß\MÉ•·¯£p<¢Šd˜TeJDv'ÐÁ/뙲š~{v*ÀÙ &$}],‡f>­èPϹÀÎûúr’t,oOwëV<²f«©Ïž=‹“'O.{/cjjÊ6sèêê\LE §5«ðäVy‡ÃèïïGww76mÚd‹šC! Ägaž¬SJdToØõé´zCc§©›K²±/ûº‚—[V£šqTV¡%9§(((œ‰ÞÞ^s®L›ò7öúìjY>¨šƒÕ`}Æ×V*ôQTÂá0z{{qèÐ!Gœ+K‰‰ <÷ÜsØ·ozzz088H Áf˜"œP’Cñgx”BÍ’ÊÙpÖÛnb:]tmŒ¨<0¼ŸN>§Y“< %ô½¢óy¨÷ãUAv€™Ë¡šTwÒÄ•úÖÃét%UéMZvv¢n¥¶À®½ÍTº_ýóPÙëîi¨SFgư,DŸ7çwv>pxQ4•¿®¤ O¿SÓöyÇïý'6š—U?yù"þ÷¥ì7ò¥ãh5[Úr#•BT–WgVCÕ¦â)¨™{3Ðm<¼8áQ´ãc+¿l9nÛÐ+HÑ…kÚÐâö˜ Ŧ_ê׿õâ+ï=€gï¼{›[léɸ¢``ì>ôƒïâés¯"Ä̼n}«Æ‹AX–0¾Ž3Ñ)¤4µ|6¶lQÔII½R’r†— ¶Âúˆ6O¦:TBà3äO-/ÇYÊ@š(à„5/®ÊE!ÇUšS™4 žE2C¦,‡VÁëðyBÉ•‹¤ªÚç󌢄d€Ê ¤÷}ª±³:¿Š0Z¸²ƒÀXô8Ô dЙôk úw¿ 9ÖÔ\xî¹ç[q‰ƒ4ó®—¤YÔsÆÚpíÚ5ÓåfRyDww7ñÔSO9öæÍyâC0D__ÂazãS9`ö6êœnÆÛžVoØ|´„ê Ù|P²~@Iåê=1IEÁƒØ»w¯9“¼\>ÛË;Us(eÖ WG' `ppxî¹çj²ý‘HxôÑG)ùÁf˜yNÀÔm£kƒÑ>C)6÷3%9äh¸)…+ ¨<0îut2:ɪÔÙ4ÙA—‹Êç¡ÞcËýUÓ/ñ™ÜëRµª;ÌcÍ&ãÄ]š-ÜîG’E,cëPÂC­÷€Y÷^ÃÉ.œzÕÕ÷4”÷°ïÊCxð¯iBðŽ½¦ó—g.AݨYó½^Ü}ì¿BðzLçñ?ÏÁÄLf'¯hº­{òËñø’£MŽ…†3 I¥Àà";IÕ–©àÊÛ¥î`G™ÇgXì4ck} CÐReèH«=ÈKúkIq--ø³»î¶•ø/_ºˆßþÑIûÅ0FR!À­ÞŸÅŽGAé—É””Ä+7¯â|,œ;¨Ú°½0æÓkÚbÉ—ž1œ¿—V«[À#c*†aàDÄd)gz¦Dm—Çyþ×ði‚KÑÊ5Ö®›ñmX†…ÀpPHáû?'Z3gç EÑ6—øaI×JÚ_V žweWz Ê•·¯£p<!ˆ*µEvЈ :4@d9Hº ža)ñÁ.è$­ä¥›ß{8qM1Hvàv©ÎìQÍvÇ]H™2ˆ‹ë|oÇí¼~Ã3ÆŸO½ôÒK8rä¶n]üorríííàùÒ>¯šWx€θJÅJuŠ|…BÁéÓ§ñãÿ¸j¦Ùw¾ó²FGG:;;ÑÙÙ‰`0¸ŒT2ßç@€ú& ÐÝÝmŠ$öüø-GË´w«Ð #©•/©Ša&³c†ÓtuuQ§BAQãkê±cÇŒ5fÏø]OI•µ§ZÆ’>§«9ŒŒŒ ¯¯´3r`žü000€††tww£··wÙóŠâmÑð9ÕÛNW”æÑ Íý\³ÃZ|Ãïz fL —ÈÓ€û–¢Ëæ[Ê®ÀŠà›~ OU£]†ú^zì‹À¾©:²øŸ‰dü¬q}[Õ›ÇšÍÆÅ´Ä м‰Î-PÂC‡Ó*¤‹ %/ƒiÝð@MJwþôÏÊNxòp×ù‘šÍýãéÎâ•þ— š¨'ÑU¤B¿†gÝm5;1Ûo߇Ý~gþñŸL¥mê:¾÷ÎÛ¾ÜÜíþºN Ç»[ËñxA„‡yÌ&(ªŽzwž4„²ˆ¢µ'ªj#;̃cWã:ºj‹ì0Ÿ‡Ÿ°¿y-ÆcQŒÇ£ê±¢¿d‘&>ðÎ!>\K&ðÌÈkø†ð:ß´‡Ûƒhc|€ÆZg›E’æ¡çãa„R lõ\å!;,Û$ÍƱz:ø‘e——c2//@Ñ4Hªjë,÷ .ðl‚*Káß>=>óA‹v‹Û(Þ,¸’ã%käÝ ˜-°š…›K¡QF IDATI׊ö,ó˜8 ”ì@QÙ˜UåÜêNe{`=ÙA!ªunÍX²®A–ÓgÇÃÏ‹”øP$tBÓä² Ÿábù9’¤ž&³íy4ð„ >ÛÊJ¨+¡Ë/ÚqZ¦Î¤_ìb>ý»`Ó©¿4U›gŸ}ñ±xFPUœ?;wî,¹ íÝ»£££¦ˆšçÏŸÇÁƒó~çôéÓ8}ú4.\¸Põsrbb9ñ=Šãǯ"CPCOOz{{‰DŒÍþÙ7mÞ»U É¡Z7½¤Š†z®Z‚: CƒØ²e‹á½‘®W®ÏvDujõa%9PX‹ÁÁAôõõaxx¸$ù3bØÆ½`¼íÀ %8ŸLï½´$ôDúo’¸\1}·”üÐÑÑîînôôôPòC‘öhôŒ ÎpnêÄæç\5,5=”Ö6žñ¬7^ƒ"ƒÞý3Äö' E߀žº "Oƒ›ÁºoëßNÉ‚:5Tô¸ßzÿ=¸÷wÿcUöOóÆv$£±UJ m­à].KʘšFb&}©º·1€º–fÇ´ß á¨):±æÀÓ.p4@šô@ ª<\8õ*¤OÄáòûÊÚGž†:ÄnL—­|ÑïÍKxà]"v>xÎ|ÿ_Í-|±)(Ñ«êo©Ù¹°ûÃàúÙ7qýÜ›¦Òÿõ…s¸­¥;[Ó}ª•'€'ªHˆÊra¤‡¹}vJÑÈ’dÀ%ZSÙj%;ÃÐ*¤Bv0ØW„(:ªë s¹<Çå¨ÍPÏ ¿Ñ…sѤ BÏÖÖyâƒK8ç⊂±s;‡Ã6âp{:ëZÓõ-jL­·ë˜&c$ríî:½õà³ÿk³F>×u@ÒÓ‚.õÍóy½àÂŒN f¨*…ºƒ›àåû}O)Ëpñ@J9v¡•VÚïBBWU¥¼ß[#z­(RŒ*C¯ZMx¨¦¥Äû¥d AÒ4¤tÕy³˜ì@@Ô$Wì-=œ°¢?THšŠ:Áµê3ŠÂ0­$0­$W¼—„‡°hcÜ`™„Ȳ¨ã\ˆiR^·Çðs®ìûÙj9 Ëà^´ó §¸åNœ¸ðÃY={'OžÄ‘#GÞ …Bhoo‡ßï/ioƒAŒŽŽ"EŒû§L ó*óJñxœNÖÀàà †††h I‘èééÁsÏ=gÌH7¡ÏŒ€mì¬ _SvIÕœ=Ô« ×MÙQ_DAAñÑ~_øÂŒ%Òä0žuµ³žR’ƒ3ÛNŸÍÕFFFÐßßÁÁALLL”¦ÎnýûÁ­=”ÝÖüÛ¿¾ôý9ILòôâßZÒ±}:11çž{Ï=÷%?¾¾>Ãiïúšï7ªâ@×÷¬¶!š&ê¬u`EpÛÁ©P§†¡'ŠÛ Üzÿ=x¨÷ãUÛG¼Ë…öÝ;1s9„d4=?|M4ol/:o]ÓpåìäÄâ'>A$tëvmË•öÔ·®Ëç…/ü‚]¡„‡?LHíž¶ÚÚÚ×xſѭO–~y¿þ´_¾`8ÝCŸûÏx×ûºÊÛGÉ®½YÞÛצ޾¢ç ?ÿo?ÃäèYs““åáíx/XÁ½ê³Ù±Ê«uçÜ÷Ÿ)y¿èš†Èäe$n΀ÔµµÁÛÜd:?9‘Àw}JÂÜA{W _?p'<¢€¸¤ \î®^paWc£á=·[àò“€Å ßj8𔪄’\ýà ²CõHª*’ŠºpKîʶ0`àxøDÁp*Ñ1>Åd"f¾-óe°HØì…ÚM|XŠ-õ xlóVië$nußXFv0˜Ë3,vÖ5¡EôP6cMÝó}ΰiÈsEçOÁŒ”ZX^*²C½è²ßïØR1Izp˜ºCŽücšŒ¨*#¦¥×áÀ3 Ü,'À[hp¬Á6ÄUqM1Ñ–Ì}ëb94dØc:×¶œº¡dŠÊ!SrÂy&óÏ[8%¢J ª¾¼¥.Žƒ?Çmÿ”ô`™ÈÐu@Ñ€elvÀ½ÇZÎxÕˆŽ¨º¨(±áŽ‹';”Œ£I¬-“×Ó¯%郧žÇD2j8«ÆÆFüõ_ÿõ2‚C (ypÁñãÇqâÄ Ü"úpU6FNØ»w/¾úÕ¯.FFF§âÀø6€]wló»Þ¿íío‚DÞ,{Ý0>>Ž@ @šIŒcÓ&ãòâLÝv»>M7ˆy«]m›Ý*TsÈåõÿ"ß4¼W¥   kª™5•[s7øõ®þõ”œ×î"²V&ú¡GÎJsï½÷bhhˆ:‹2!chhCCC¥%9ÌŸ¼ëÁoý=0®fk·Ò4H2M~Ðgßr< bþÜßÓÓƒîînªR˜CCC8tèñµô–÷[÷pÍõ%9Ðõ½PH¿ø”á4bðw©Sªh7 -z¦¨<ÖlêÀøïL;Ó2‘–ÂSçÇ-»¶;¢®ßþüW0yÆXL±·}8ocÞï¹ãã`UG^ú´) › Ux¨Q0­{O4öùÂéWËNx,U|xéíóøqèªm£!Á3#¯áÂëx|Ó<Ü¿æ4‹ÆÌ‚Ç4*Ñq&:…уuM9Æç P5@ä–-ÂM0ht¹H¥ ;xy~¡ZÉscâ©PÒƒCƒÅsÀωðs¢íå.øNC}•= Gî…((l¶ùYUvžésÖ’ ¦Ê«È xòøó˜*Ad9pŽUp¢-';è: èéçß"×åÚÄb”CWË{q ‹FÁ èt dîs \Va9©Ì–©²i…;f±Ÿúßõ>úùÿg8«™™|ûÛßFOOÏÂ{áp“““hoo/YÓ猒`ll üà9OØÖƒ`×Þ¦açò¡öm¿çs ‘sÐÆ^‘ʧl‰DÐ×ׇãÇSÇV„ýÞ{ï½6¶4ÏŽ$.ñn¨í nM,nq~[Œ’öîÝK ‚Á šššpó¦1¢G XFx $ºöÚ“5Q˜wwwSGaÂáð2RýÈÈFGGí;K¶üHIL™q5§I½ ·…/%?Ù1Ã{¹RcttÇŽñcÇ(ù!z{{ÍÙ\ක飊'9Ôôo[åk<ãY’4¦dHÔY0|uLU =6f Ùáñ?þíL3ýŸ‡ìÉÙäD¢×Söú®ÙÜa˜ð «©‚Ô]ˆs× /6Jx¨a°­{ OûáåÂéW Ý@}Ûš²ÖÝÓP‡Øòýø'ú #<ð.;¸ ?ÿÖwM•£%Ãg.Al\ü¡K—b†ó¼^{¶“š¶j1)­»v`÷£™ü'Séÿïsg°ÍÀ-õ¾²Úl(‘ÀæúÃûð‚IªšŽÚáL,ZÕNvX8uä!<Ô„²ƒ5e$U³Y32?Pu3))Mz0øè ÅåÁ5iÒÔ”,n¼ &>´ ”H`àͳxùÒEÛ,!®(;‡±s8ܾmÚ†­î ³EÎ-Ʋ¹9%'ñÊÍ«iµÁ“{üKf“+ˆ3„¤ƒìù9µÄšån‚A“Ûƒ¸"#¡(Îpõ¢ .Ž·ßïØíÛ&M>‘”Êk‹cU`üFjÂTm9vžPuŠ d]CJWU/޵F¹n ¢AÎp.õp¸Ä9@ÉÜ7m^/>»ïv|óÃxlóøÁVËxyò">vê‡8ö‹aœ ¿ ¸µÕOaH!}ÀX>7U¢ãLd çãá%Êe";,«˜–´×4Óù3`àç]¸<X®èqtsšÜžê%; å°lšôËïµËjyxg²¢[$ ª˜'Ñ‘ÔTÄ5yá%ézE¶…‚b©Ä4ÙYõ*Ù€`V‘Vï+Y®°}]JS©Í]×IM¿røIè¦c¿³«D²Ã|ÖÚòŒúvBƒ $Ixá…V<~Pqîܹ’õÌÒÀ„ÍžÊ|„Ëø6€ÛöQþÜæß^Mv YöÒ¸Ýöÿ)ØÖ»ÊR÷‰‰ êÔŠDOOŒûÒ©ŸšTø •¹Ì8*´-vOÙ»ÇDLØtWWu$€'Ÿ|ÒT:mæV-L['+¸=•ºöÚÔ­DKN³ÿ~ê$ŠÀñãÇ ñÔSOáĉ ¯ cxx¸¬dƻŽ^ÛÉëâj·öø­¿qߟB¸õóàÖ= Æ»ÞQc:O~Ø´i:;;Ñ××·ŒÌRKĉ'L¥e›ï¨Ê>1ñˤ³ÖÙš]ÖÙp3þ¤®ÐÅ·Ú ËЯA¹ô·E“\>/êý8%;˜Ä·Ç ";éËË€5›;ŒŸs3t°QÄŽ\ª9·} pÝz0uÆá_þýÿ*{? 78±¼ÈÈ"¼£þ–&s6¡«H^y½æÕÝÇþ+“ »ŸÜ¸Š×nÜ({.Çb«÷æ".©H*y‚m¥ð|k‰ìd'ºÉ÷Ì`Éç“ÉYŒDn ¶tL ¤7ö9³hg¹Òëd.ØNɬê’#=³ÄŽE–C£Ëƒ€Ëk<ÓÍ ¸<¨]àVÞÎ_ ÏÜœÚF¥´¥Êβª°tL (ž ªJ˜–“˜U%ÄUeî¥"¢¤pCŽg&³ÖhQT’šº„°è”€ì¤É ™¶>¾ðg :! t‚åé$H)é½W„0sê"”ì` ·¬˜ §½·›Êê‡?ü!B¡Ð²÷¦¦¦V½g–å¯ò÷€m½ ü¾à÷}ìÚƒ«Ï!…þFË{Ámûðû¿^6âEqèíí5çV§~jáÞ¡ÈJr¨¦5×Â6•½{Š«INµPPPÔ6~øapœñgbúÍB/ï«6’C`Úa&ÄøÅ}¤NÂ$zzzpâÄ D"GÖ[÷0„[¿àØ[öo;¸uï‡pë ìù2øàGÀnsT3‘FFFj¾GFFÐÓÓc:½H6–ØéŠWå,U~T®’†³ãþ™¨1ºW‹…ª³P§†!O¾-ü -QT~.ŸíK¦à)€oO >SØžŽå8xê¡´²fsЄí¥è€£¶ Ž„æYkï&oý{ §‰^»É‘_•½¯< åu@¢ß«n÷û7IÒÐ¥¤éwjznøZZ°ûÑGL§qü-Ì&Ë{‹éŒ$A›1±7ŸM)…“ 9#8áœbëŠÇ8³JÝ… $]ÌÊJö€/ÄD6Å’¼ã‘n‹›ãÑÙ¸[ëÖŽéRâC†|ü‚€£;vá»ïû>³ïv¬õØË®¾–L``ì>ô/ßÅÓc?ÃyrpéYÚϔȞVILU0¹Ž/¡ªƒ‰ÏuHÉiÕ‡Ò3YH;"Ë¡Á寷 ¢>^„Àrà—X0X^^@à·õ¢+ó­üÕ¿‘ ŸôÀT^;R†O]Œ0Üx†­úþ*¨Œ¬KÁŒ’Êp³;³lËQRˆªRmôEuøì9[‰;IÝ¡DdHfPGts¼a¨è:µŸLPµô^+U8Ña!©ávJv0 e¹½÷vÜŽO½©¬þèþhÕ{çÏŸG*UÚ‡ìEí[ìzÜмoQÍaûï€ñm\î{‰y3d]-à·ý›‰µz ¦•0`¢…~äý¡´ÚˆUºérPVæ /¾zâ²á4”ð@AA±»ví2á{.Z²rÖRËHTÍ¡R²^¶ÿ»qÊp¿ßOƒIô÷÷c``À‘uc¼ëçÔÞ_1ýɸšÁ¶H«?ìÿø­K+pÇÔqžü°oß>ƒAôööV-ùahh]]]¦ÉMÉ& ]ÇìTáÊ¢Íí`9Îuwù¼†=t…Jx¨y0­æ˜Ö¿ú硲×Ý×T^I{—Ï †-| ¹ëýÞ±×tyòô;Ð¥Êc|êšfY^;Ž<ˆÖ;L¥½áû—Æ¡ëåÛ «DÇT²¨=zLR æ &Ñõå¾Kœž“m-RwÈ™Þ:²!R6»ÊWΊ24BV«<ãýÕîõcóZøyÁšþšo‹ÄqجyÙ°/>x_~Ïìmn±Ýz^ž¼ˆú!Žýb'#oÍ{1ÿ¹Jtœ‹ÞÄùx¸$ù5_5Œ§é9rÏŸ?Ã0pq<|‚ˆF—Mn/Z=~´zühñøÐèòÀ/¸àá0Ù|M™ƒ¸-/§üßË8¯-Œ.°Ö¾y³yÕHð~X‘2ì·2ÛoJS³+=P²…‘ÔTèN1ž+ÙA!Ú*u–aàåjEl爲šVÖšƒÇ€"–bˆð@ÉEAc}1óïBߎûLeuæÌ™UªªâÌ™3PUÕòªß{ï½ uväãßp›Ÿ¿ÿëàv}rQÍÁ’C¦ç™%ÄnÃ#%¤þ®HƒA=zÔļMB ý(Ç©Â6€«M‰Ž8×ÛtÈ ³oNÓÕÕEÅ}ôQs[áÈNt²/íTÍ¡’²Îj«Ñ3†Ó¬]»–:“8~ü¸ó*Åy¯êPèäa·ßôˆû¾~ëÇÀ­írTýÄÄž{î9ìÛ·@===èïïG8®xÛîëëáC‡ŠR.áÖ=\qí¶žàP†Å V—ó fw0žõÆ[›ºJá …ƒrå †¾=1aiÞõ~í{n¥lñ™0¦/®*ºfsêZšÕ†5›Œ]´ÄLÍ=%<Ô8O˜Ö=†Ó]8õ*¤X¼¬u↺<øÄQ‡¥rÖaž¤Tö~°ˆìë;V–@ÖõÌêÄÜ#¹À[÷óõ—Ÿ°¿y-‚¾z lsE[v‘ø gnçÁ¶[ðgwÞçg7l´Ý’F§§ðÌèkxâßNbàÚˆ¹Ro£¯Àdr#‘ë&ní5VŽ)ç))iòà c*„ì`™ï«m‹[œx3o…»‹åÀfš”æ!²lÕ÷—Ù}ˆ¬kPVr÷ïl&•êW(êï£î pT€¬C&’¸ŸÓë¾Ñª²ôÑYºS3æÁxÄX¸j%;Xeù\ënÝŠ{7˜ÊêùçŸ_õ^,ùsçJVý”®:f0®f°ë¿ÿëà÷}캇À¸[Šú}Öè „Œ«ÜÆnˆþü¶‚iØQ’¶öõõQ¿gz{{M¥[Ty¨F’CµŽ-jSÙ»§4 ‰IÃi::è­†ËñáØT:}æÎ\ƒ¨šC9;¯,Yç-:uÅpš={öPç`ãã㘘˜pTØæoûJE©:¬öE™'¸ ܆Ç!Üöe·~Îqä‡H$‚<õÔShllDgg'Ž?^qêápÝÝÝ8vìXQùp­]¡îP%›¥jVÉ¡ºn†ô`•"… Ðehá× _€:5 "O[^ÄC½Ç­÷ßCûÚäD7Þ.|OçD²Ã|½ ›¦ZÑ*–ìŸé 0Cxâ œ?õjÙëîi¨+kù¢ßk8Íî÷ßÞ$QC—bP¢µÍúôµ´`÷£˜J›ÔTü¿Þ„¬”Aý`nÏ.if$©¸¬II $Ï!@Q–Ÿœpf(wÆý`ÙÁª±Xò5ÓíøE*äùüˆ5ýôÕcóZ¸ÍÊåj‹Â Hd'>l­oÀg;oÇ7ï?Œ£ÛwÂ'Ø{{ðµdcçð¡ÿý]<ýÖ¿#ÄG—n= ã’åó°"a$r1U)"ÿå‹ê¯j@JI+áT#`ÍΑ˜ÂË Ê™÷A|ñd_7Ëø­¡àýÕA•ùûJ'²®ÕdQTÖÚ éš3Ô°Y2ÖË ¦”rX†1E’¨*ä!:ÌÃÅZ=¦”ì`têò‚Žo¹ÓTVgÏžÅéÓ§W½?55Ué!‹! a||###H¥Ú‡äò^¸¡ìºÀï;þ= nóoƒqYGr(lëA»?—V}Øô„¥ä‡‰‰ Ñu´Htvv.¨•‚–L“*qßAÊqÀsDCËž•+`†ðÐÙÙ Š•~¡±±Ñø68þ6¨šC5­»Õ¡æ° ò  ¿<ð·~ë·¨s0ññqÇÔ…m>aÏ—ÁoúHÉüœ0io»£É0::Š'N,¨?tww£¯¯Ïш¾¾>ƒA|ç;ß).£9•'¢4›š%8 ªnƇé%š§°ØbÕY¨SÃ/@ ÿ¢d$•[ᅦ’LB•$\9;]ËûÉrÖnÛìH²Ô·®1îG”ü„ݹ{Ë€™ðtP°ëï€~îÕØaú×'‡ð®÷u•µîž†:Än”oC zÜÆ'KÄÎï™ïÿ«©2•hÈpßš{Ù6êŽ#âòk¿ÄõsoNûãWñht3¶5ì3”{÷P"F—«¸¾Ö¢) žù’&=ˆ%;,œ:ÊÍó³‰ìˆ`¢ b´Œ<åhÇ2–õ×¼ÚÃx,ŠÉDÌ€mØâHÓEÝÀ­®\›×‹£;vá±Í[q:tožÅµ¤½Ê2/O^ÄË“±·¹mÚŠƒMë€$_„=e—<écª‚‘èutÖ·ÂÏ &íÄb²ÃRß()ÏøêºµÞ‘e0‹ÿº„tß“2/üLÍÃñHjÊ¢ŠŠAe |¼P›ö[`9ˈ Ÿ+DƒŽ’(œ…¶’Ò•ò×É ÙA%âš² 4&²<¼¬P@ºEr#ϲðpæÈ¨fÓUT-ýÒ w:.–ƒ¤k}/ÿþ’,…Âü"©¯«iŽ®{®üÊpVÏ>û,<¸êýP(„T*…Ý»wƒç³Ÿ?b±Ø²W8^õp8ŒÎÎN #À»1¨½æànÓ¸Lý60bà^ Fl,Ê,K9ÜŒ«ܺ‡À­{PÐ#ç@¢ç Ç/‚DÞ4o?ºººèzZ$Ž?ŽC‡N§…~®í~ç?‘jß°ë×%âÈA+ ôÙ· §¡„ ŠL¸ï¾ûðÒK/tB ôÈ`Êx>){Õñ@£2—ÁÂŽj×ÿÅøþŸað›¿ù›Ô1˜€HÝlópëãj®ÙùÈxÛÁy·áqèáסόB¿nŠüS*D"|ç;ßY 444 «« èêê*ëY9£¿¿}}}–)–ðÁ'wöd*Äžé2^» g¼ëÈÆ©³n¡ ²¡§®B ¿’*ý%Ô·Þêý8ít3ã¤i½õvÁd‡u»¶Cô:÷Ùª)…i¶UÝ”ð@‘¶óÖ= W~f(Íäȯ Ý@}Ûš²ÕÛå÷á8M+O¿±,DŸrÜX0kËæhÙ¼So_²¥ž¾{šlŸüÖ¾' /ñ„©´ùÖ¯ðåú;àÊ㣊I׊¾SRuÄe>1GŽ®§ƒTl¾AÕ±ç*¦œ·³V.Ù”¥ E’æ?×—õq…¹þâ[ë”ò2’ IDAThqyp.z©|냙¶èH+>i⿺"~AÀ‘ qdÃFŒLOaàͳž²ÕâF§§0:=…µ/Žnß…ƒkÖÁ/{Œ)¿˜ ;,œ×u#‘ëØ]ׂ€à2h'%";,-AÕUD`ÙÒú;ü[El8 =Pe‡¼hݘ’’¦x#~^gä€[cd ­ÖÑßÑùNQ‰{ßeÿ%¢—Éa5‰u¥:žaÐ*ú 2ÙÏs<ÃBFº½fÕqX†—¯Aƒ ¢ÃÂÅ»óÞÆÏ2 xWîs%;”¦* ð‹çºã[î4Ex˜™™ÁÉ“'qäÈ‘Õó6Æ+¯¼‚ööv…÷R©R©TFrC&Äb±…ô¶€sñmS¿ lýv€s- G±Ñ9f• ¼ló»æwcÞãi ê[/&?  ¯¯ÏÞ±¨BtuuaïÞ½5–pNå[ÿŠÙoTÑÁâvG –=¥š .©àY.>GࢦÙ{lƒýrÆa¸µývq‡Š+‘c¡èšed‡R÷W@taÓZL&bGÍ÷W¾z$çæž¨§_²ìlnAçw#”H``ì,N‡®"®ØwÃòµdÏŒ¾†oð¯ãñÍ[px}m¤P’cÃ6«#ÑëØéoB›ËW`zÈ 6¶¨ö€RਲC†·™tÀ«¬:w­p80h<ˆ(Ò²Ìó>@á]ðp5zü£¶EAm>? ´Ô°„ì0w¼!W¥Ö»êÀ3\öõ€—À›¸É„a€€èIïjEd]XÌj28†ϲ«ú°Uð‚ÍEz¦d‡ŽñrÂCÐÓ€?Ür'N\ø‰á¬žþùŒ„PUµè ’Xlñ¦Jiów+šäÀø6.²•C§¥MZ$BÀYC›·­®p·<Õ„ÚC__Ž?N×Ö"ÑÛÛ‹§žzÊp:Gª<Ô É¡šÎ#eª€–‘oNF(((2¡»»ÛÜZ~üÆ'*ÄÝR5‡ê[ƒó@ž1øxàÀêLbddľÂ8ØÀ^°· ì­ÝyhÉÓÿîØ^Åèè(ÞkhhXF~˜'ó!C„ÃaŒŒŒ`||###ÁððpižxÖ§ÕÊõ¼¢ì™’jŒh\©Ç® zŠ|ã0›&:DϺ}1›¼(âÉÿñ4“¸6v¡`²Ãº]ÛÁVÀÅÐõ­Æ Z2\ó¶ÀR›‹P[[Ûqè´zE·þ(þ޲”­ýäÙËÆ&ÞÚ5øèß}£¬}–ŒÌbú‹å[uSo›+êí‹8óý-yïûÂgкkGÉË™zë<¤ØòÛ[¶mËï/Iyr"—ÿà8âSÓ†Óî¨à;ߟ[,¡qäØÈ°,noiµÆ‘3@“× .¡,ù’(–ç×w',3Kë +iå ;êN!;Q¤ªbV’-U¸Ý9¶ä}Sœ‹ÞDLU¬í³LŸq:à!“=aLQðò¥ |ûí ¸–L”eJn߈Ç6mÃV±ØÛÍ5¯—‘H‰çLAd‡ ÎÔ%X§Sƒ7ãgµ—ŒOm9顦É!fÊa@@P$4$GÁË¡ŽüÖ¨ OÉ 3±¾ðq|œXýEQÙÈb'5U>…ƒd•h¸T€$«Ÿ±Fðf^fˆŽ˜*£Ap›ªrà‚‡«Luèum‰bMúü)²\fGD‡¤¦"¥«PÈrÛ «)D4 :!^N€‹å°VôÁË yö”ðPÒ6²Ç+¬I?H‚Q.|ö³ŸÍJz(±X ŸÿüçqæÌë2åý`ê‚`|W«8WwkÙ‡³ȯüg÷dvtt”íÔjC0ÄÄÄ„átÜú÷—_åTûæ”TD–•vxÐïC=ÿ<õ9–aÇŽ3~$ÝôTéT(ÉÁ™m¯nU&ú¡GŒŸyN:…ƒR§`áp%-ƒ›ÀÔm¯’Cù-¹ ú ‡_¯ [ìèè@0\õ~©H YíճŽOÙJ°§ºnW´$¤ÑÏ›î[ ´}€ö]¹¬Xž†==6f{Ù Ëâ©¿ìC}k ˜¾8‰HèzÞïÕµ4cÍæŽŠjÛ·?ÿLž1¦úæßz6ûE—œ‡+>îÄæž…BNj̈́¥S‚bÁ¹®¯á4Ñk7páÔ«e­·Ëï-o¿±,xÑ\pSËæhÙ¼ŸIˆ^/vøSiߌ†1:=U¶ý¾ªë˜JY#ÅHIIXE`#+¾dãòŽ:÷¬¬kçÒÇØ0ZzÕ7Ç!¬¥ã!dT±ž âçìoZ‹ ¯~1à·dÐX Æ1Ð2—á<¶y+^|à0¾üžØÛlÿ¡æåÉ‹øØ©âØÈN'/Þ|çLÑóú\ì&BRÜ™d‡y?™’ÓÕàûÊêÇ ˜_\‰T5²µ¡jÌ‚ÿbÀÀÇ‹hqyÐ ¸áã,M–úyÍ¢‚»vÉ&Æ^dySå WùýEQÕ>[®²ĵÂÎ-1-ûÍ;<ËÂ/?§3 Ð º+’ì Q%…i)YEB\•^9…))ޏ*/’ätH)i¢A²ƒJtÌ()ÌjÒ*²x7:\ hýh<¨cE´ ~Jv(y™”§/ßxz;n7•õóÏ?_’*Ÿ?O=õ”µdlÓ^píïÛ¸`]Æ÷ rdY·2@ÅiÀpkEMLL ¿¿Ÿ®±À¬R†ú‘a¢Ji÷Óv°ìj±¡ÏÊ>hå©É¬ñ‚ùp)(((2á‰'Ì)5è± t·Õ²ž:dý-qÖ¥‚ýµá4‡’L¢êŒw=Øæàƒ°çËnû øMq0Ù¡}çÛrüÖAÜ÷uðÁ'T *^õ²»_ùà“¶J÷ ÅF{&¨Ý¥»vž¿KXóÇ„²…{®Ä8”Ð÷ \ù‡²8Çã_û%;BÈëo©8²¸|ÆãžõTmûJx X4†õw˜J÷«“Cå­7ÇÁå÷•µîzó ;8hš0Alºû.´î4§^1xé$R%§*p¿#™²¬HU#˜•”ÜuÐu@³QÙÀ‰dÀºÛÛ 9¾‹ú)_€¸ cÁ€…7_p²@tŽaÀ¬‹Òªa}õØßÔŠÑ[\ŸZ‡Ìò€’½]ÛnÁŸÝy7¾yÿaÞ°>ÞÞàºÑé)ü·×^Á§Nâäì…åÄ#6V Hfm¹(.pN** )æ'ŽCƒ¸íi‹ÁG–ü\lµÄÊ …èˆi2¦•$bš¼ìfî|¶íb9øx‚‚~^„—À1¬smØ‘`à6Jx Ë0Y®û‹¢RüFôò˜‘ ²Cº)…×V&jÆ÷ý¼Ë°êŠ‹ãÑ$zábùŠþ¤¦à¦œ@JS³÷+⪌™TjRJ“L(ß%53J2#Ña%<,ËC`YÌjT¢çØCP²ƒmm”ùeUí ÞŽÞe8û™™œó <øl ëáâW’1¶´ÅÍ ØÝÐŒÎÀ¸³à‹m;5§øäÒ7ªf@›×‹ÏvÞŽ8Œÿò®=Xë±W½èZ2gF_Ãô] Üx1wrn—ÈX;·IšôS•Õã_B{gŒª­Ìßxl4°æ•L@äÓćRµ¡‚÷uBpCIàdW¤¦•$®H1¼ cZI.úaÂTǸ;–”î_qN%ÃH[êL‰Ö¤_¡(›h¤ †d’ì`Z†¦¹9N€/œðÀ2 ·9¢X™‘ÔÌ*Rþc!€¢BMI˜‘â9ȹÊR1«I¦]ST•²ø_Jv°µËÎ%Þ…¾‡Lc¥ÊßÿùŸãé§ŸF*U¢‡õJd‰Jƒ©Ñ­ˆ[زÿ(͸ZÀ4ÿqmxx¸$·¡Ö"­òPÕjo¬‰ºÇá(Z$qÙp2ªð@AA‘ÏGøýÆ/ #J$yÙ|ÁTÍÁ™kU…w«zý¦Òýþïÿ>u&a†ðÀ?ᶯ@ØÑ ~ëï[÷~°-ÀÖm«Ý¹çPòƒ)0žõnýœådvlœ:5Mr 0bŒ•]¦ý[Ê¡Sg¡Ýü)äÉ¡N —dòPïÇ+RuÀiÈv‘8ËqX·k;êZš+¶m¦”Š%ùÉObttÔ ñ‹Å¥'šCHæ¯d[ïã2~ãÕÀÀÆÇÇé¢j̪<èS¯€HÓÖ›Pæ7*zçS}$TÎøÌUS7A0£ê¥ôzøu›Ü-Us¨¤¬ËõÒ·L¥3Kø¡0§îÎã`ÂCM_EŸwÜ–‘¶~ ló–ùWbŸp> >ødÑýP%›í¹f§õ%é–Zö-ƒúÔÐ÷ ÇÆS¯[ᅦ’,„¯1€u»¶ÃרO¾Æ¬Ûµ¢·òç`ýÚ5Æí^IÖ´=PÂÅj£X÷^Ãi¢×n`räWe­w±„ƒb!xÜE¥ç]"v>x5@³‹[K‹i•‡\½„”\D agIÓ0#Y8²¤ªF“ò´GÓbñA&G?(ºŽë‰Äê›'³öI ¡D³QHšfýX”Œðà²,(#Çc †aà4»=ð Bö[°3(;4yÜË¿ï€þâAo=4·¡Íí5W³uLÎ)>¤ØÅ€£ØZ߀ÏvÞŽ:üݾk=ö®w/O^\$>ˆ&ˆYÚ®’&=èz‘ãkÇä”4aŒ¢`_a,p Öøç Rۈ何;ÝQM.m[ìè/8±œkÔó.4‹Ôñ.øx!ýâ7ZD/Uv (/œø»Ç_t6. KöOF²„—ÐìòÂ/¸àâx,/¢At£Åå+)ÙAÒU¼œAHŽ-ÜæÏD’®aZIb"†¤«;ÁŒœÄ¬"å&J=­T¥h9í$^àú’ÔTË”Bd]%;CBWr*>)DGÈL¾…*§OŸ6œnddŸüä'ñÎ;ïØç:‹$<iYôò:þâçkRå¡¿¿Ÿ®¿À´Êíò÷,Þ?TS ƒ…mqÉT\÷Jx   (º»»Í¹)% "ß,‘»¥j5óÜÃèñw@LÜìÌ0 ^xáêLbppÐø¹©n›Cç}x\pW±° ·~bç×Áoÿ¸Ö.S7²W*˜º­ný¸ÖCæÒ£›í¹¦ãü©ß(µ=0¬ñKÑUx°`C%C‹ž2ù"Ô©aÔUGUïÖûïÁC½§ãd1Üu~¬Ý¶·ìÚŽµÛ¶TÙê[ˆšªi[¨eÂÕÆÍ¶ ¯¿ÃTº_ýóPYëíò—WŽÅå+>µeóF´lÞ@Ð$̪<üòæ&cqóâ"Q´ÊC†:$d²–# …ô­åV r`:•,ˆì°Š®ãÒìla¤#Ù³¥Xþ˜ÒŸ S˜²)âó…ròçÁ±iâC“ÇfnžÏ8 ÃÀ'hYFv`ʦR‘-7Çcg}Órâ)ýü ±À,—V~P3×Ù/8º}^¼ÿ0>³÷vìmn±ÕÇξôæiŒhWQ/ºí1EÁùx¸¸¾Ë:#-ž“„¤W*äTK°¸©;GZAz¨°gvÑ ò_ Ñ*Û¶àÄr ³3Žaááxø8>V„!–ŠèP6LA±8¥¬!;iÕ…B1Æð ®œs“cXx9 ‚¢>^„‹åKÚ% Ñp)Í{RˆŽœ?X]%:¦¤8]ËΓԂî:´ˆ qK‰we ;TøB7]Àm=’®!fT¦›Ð–Ï7³*ó7cèû'OžÄ±cÇLÚ|‘&È‘"'wÌfû²þ‡in݃¦Òõõõ!Ó5Ï£ò GÇ,0#ªæ`Ót³ó€î˜î'‰ÉìAÅ9@ … `ïÞ½æÖÑÈ‹ý>Us¨¤¬³ê÷ÜCo.f˜ºíÕ=ߪÙ=e[· ܆Ç!Üúy{N€> 6p[UÞÐΈMàƒOBØþ)0b“ñô(ÅÝ!6ÛsÍNê7쳈 &6¥³=®ÎB»ùSÈ“/B»ùS5æ¸:®ÙÔ{÷?ÒÁ¢0„úVc±_ºB µŠ€#¹žµåßüzšÀ4m5œî©W!™ ·å&<€héaçÁ‹¢u†¾±vŨ<üÃÅ·‘²’`3’TP‰QD’H® ]4{n+OªæûVÁõDÂbGgõ½@e‡b¾c p¿¸S‚¹¾áXõ.­>/.7îô«ÉãÆ¯>Q³ÐïRÂÈÇñ¡© m/x¶„Á×+?W™4é!ÂJö~:²a#þì7îÆó÷܇ÃímõY?]ŧÿý޽ñ¯ѯnÍÜœ™CHŠc25kñŒ,!IQYI^5‹1†–ûg«”*`L¸lDZþKÒµ²-;ÚÂ8³¿èógŠÙ [êG? ˆÖܪ<ÈD‡›ãáåÇ Ó´R8é[Ò5Ds¨þ¨DÇŒœÈÍaPµôÅàOÍCŒP‰nмžÍÿ åXã™2LF ËL¢Â˜Ü/X¤ò066†‘‘‘‚¾û'ò'xúé§Ë79•â„h%&=ØðÃ4ïÛj\e6‰P•‹`‹ÊCF3ª6E‡jÚ‡WHPJÕÔcc†³mhh@g'½£Œ‚‚¢ðuÔÔzóg’¨šC¥dí$˜Uw€¯~õ«tò›ÄÈÈ&&& §c·Ñ}a¥¸%ÝňM`›ï¿åw!v>~û'ÁÝò>0u[+ºK± Ü-çØæÂ/º¥JÕ4(Jk+2u`À}Uî¡RW¡^ÿ”ÉoA‹žtÙ‘õ\³©ÿñ—,¹0›¢¶`Få¡–I,5‡­¹œÛáuï5œFŠ'pþÔ«e«3/ <åí?Ñ‚òy—ˆÞe]¼å]H‰¦ÙZžY•‡_Lß@8iâ6D  ê:f$“,Þu ˆ¦ò´IU‹oGé½8bERS¡ç Ä1ÚÖÊ£ºMÁû°¡ +H9váůRÓp6Ùa)ÜuM8Ð| ‚¾zãćbÛ9O|Hq€ž¹ì­õ ølçíøæ}‡qtûNøxû‚ýF§§ðéWNáØèPfâÉ1gV|~>FL•-é?ƵM’JA7'—|^WR9»Up$=ØõÏâ2øLmÌà¿tR¡))I€‚Â6Ï•2Àœ òe#ªñ ‹zÁíÈ¡ÊE`ÈøL%KP;AXNfßv}Q…Ê„}¨yT„dËHwieޱùQd9L“)Oy ]1¹·e–edVåáoÿöos~‹ÅðÅ/~/¿üryÝhübñy¨V¶Ëó‹<·ÑÜó³¾¾>º[³}Ifß‚>3šßœr¿YÁ¡jSt¨ ±!…µE¿n8kªîP[G?z{{ÑÕÕ…ÎÎN0 ³ìÕÙÙ‰®®.ôöö¢¿¿ããã´ã(ÐÝÝmÎ%¯˜R ©8íÔµ×±k±}PÆÿÊTº-[¶ààÁƒtò›„Ò6#6q5—ažÑ‡Ý¥ó¹û™­ÛnÝö âíÿ£âŒg}ZÑaÏ pë., JMp ¶‡jšÔo”Ö²÷5Q¢ï7ž£Iòc-BA¹òPC߃ž˜pt]]>/>øÅOS²…)Ô¯5Nx J²fû‹§&C‘ ìú; ŸûG@569~ùíïã]ïë*ßâ÷AI–Ádá¢eóFÖ¯EøòµŠ·%%‘„»¡Á¶òæUÞ9õCé’šŠák—ñ¸o+8Φ›ô—ঔB‹Û \buT Y…WÌâî 4àM.öƒ‹ã I~Ii*¼™‚¹ÍŒ…eY Þ·„Pa—‚Dˆç7K ‹ ¯A_=B©8&1ÄT¥tý@VŒ‰Ä ð$M*àV'nózqtû.ݾ '/]ÄÀØY\K&léªÑé)|zúö6·àèÖ]èÛ•ÍncYÚ~&6…ý màóñqs^¤ËØ`MÒÁ…plImÐ.[/ØïÙ²#=H6KÊ8&Ë-¿µ¹šH.Ž-Ã9êã©Æ¥ô-ÓCáÅuC‘Å@̰šÂhüzÑeÝÛ°\ ¨kÉÿ»Ò· wúZà8NQœ-Š,Wº:‰¥Qåñp]IëTv©vã ™ J¢*kÅappÐøó‹¥{m:*Ø™OÌÖmê¶aþ©*ILB½’¸ ’˜I^.ÿ¹^l¸ ló`¼íù¿_ öLèd °£K ˘È7ÁÆ•r‰CÕ ]†}zl ¤B4\>/ÿÚ—LÝÒOA˜TxPSàj´¿(á"û†·uÈ•ŸJsãü¢¡¨o+wù}ˆÝ(’w‰`yºª×Îâç/~ª\9›Ýf5‡lØýáG àå+ñp{ê<¢íçI‚¤kpŒd qYÈgºiªpœñ@%uð BÑ„‡Œ73 Sd­ƒ” Š«ÊQ]°¥œŸµ¹}hsùV$„Rq„RqËò^þy†6¨ ›ÛºyT@ّ̜ qdÃFŒLOa`ì,F§§lñ½K‰ŸÝ³mZÝâ“·ý)MÃx"‚­ÞFSýg/ÙaþoÈs¤ž+MvµÅ‰(„ôPá}µlÝÏá»–­¬q§di²ÂHü:ÆSaŒ§"‰_CX•0¿†ˆ*Ù:†#³þÿN/û¬ÃÕ€ »Á¹;}kàÝËHe@‘óJ`8(ÄâóšÀ•LÙÁω˜É1OX~Á…署!ör«ÉÝqU†’IaAÕM« ß¹Òêõv’ªªˆìàb9H™ì`EyRž9®)M…Bôemá.……È3 Sº·ãÝ8vnÈp]_xá|ñ‹_\öÞùóçñ©O} ‰„õ„쑘ñ‹BHªx"!Ñ0šp®ŠÞ|syÆpºãÇSƒE8~ü8:dÜ¥›Ð.ܺTØ!¯L‡ G¨80•5>ÄŲ̈;æok§p>úûûqüøñ‚Hù0ñÿ³÷¦Ñqç™ðSwéh$@ФD™"!Ò’(K" É–ÛAËDK2C0N2v’ƒJ&J&ã 4ö‰ç‹Á|ÇrNÛ`Kþb:cÉ–Ù@HO$Q¤‰2m”$Abëz»ûüh€ÄÒ ô]ûvw=çàhé®[Uo½õVÕí÷©gx‡¡C‡°k×.tttÐõ¨„ñÈ#àСCºË©ñs9(É¡](Ǿg¨\]]Uw0\‰vËŽÛe[éD(ØpdI ìR:{§  “PcgaÊ„’PmðT‚@ʶ€©Øâ©\½L1vJr pÄ$ú¬I3€&Óñ°räY(‘SPC@‘Bèø j67ÒA¤0 # ª”*Y{QÂEV0M­PtàÔ·_AënÏK›½¡üßîèñûš5Ï2ô•‡ÐtÇ.œ;vÂÔsø€s6ɧºÆBUy˜R8==‰;ýõy9SL )Ôùƒ–oâ5 ˜I‰¨ ¬\"ÉéÛYm:H¬ñú0 4 /ËZ;¦D°¤.é‹õäøü Þ‹ Þ‹-¡ Œ&bKÅÓ·òÚEvXŠ$$xTÀ§dùÛ«ªÑ|ç=K$pxð ~0:âH ˜œÀS½¯âã ±ËͨSÊÓdú?šŒ¡‚ó¡Úã×e¿¼–ÆPMKœ}ŸbI=²ªB…IQÀža3䜾®x~pÒƒÕ†Y/&Iªºr%庒éòì_%Jv莠?vCBý±+y!5X…a!Ša!ŠLw Ï“!ZÃÑ\{Aáþyåc8HVÔY&Mš¶ aQÃ0.%2ÎæïE˜ó¡œóºvز&©gŸY|“5qY\~¸eK×D²J|ä @2U !Àðöª8¶mq^ÙÉôð,õIš ~Ée4D$!y&Ý @* ŠÇ!Àrh_¿çþM÷ZvìØ1Äb1„Bi9÷W_}õW…dÒyå¸bÀ7kÞ]iò,ÈŠ{4÷ÿ(Í„oñVAô]3<<ŒÞÞ^z#»hmm5®ò0ö°kïØbRè*6’CaÅ£$‡…PgÏê®¶±±MMM4 ¬$:dÂÀÀ8€ÎÎNtuuQÒL ¢½½Ýá!z:}Ë>ë/¼8]ë/¨Y3žÓ†»Íعú™gž¡4#ê`ý`*vÑ P0¡(vfcØ {bmþŸÉ‹Ð”¹wœr2»:ë ¬6â_p~˜@C†µ+3ˆ»ˆNÚq—šÅÜCí$7•ÔÔe(‘“ÐR— ²ýt|7ìý0H S(¯­.•®Z"×I Ù7ÂeëAÊÖC›Õ'Çöóï÷æðÀ°,XJUx‹ÐÐ|3ÆÎœGlÂøfiM㆒ôßM÷ÜmHåáøÕ˸µº>óáq<™áÁà¾[V4ÄE Ál¤U5¸dÃÞŸ!ÊÊp)ƒ¤ªúýØçµþPÃ0é~»öàæÙXðŒUWküÊ%d‡E) )Xަ`9&RIŒ&cˆdbÒj°enCdÒ¬€Yþ º@Ï4ïÁï}h'¾óÁ9yÿ<â²d» ÿ`t?ÁDZÓñA%«öÿñIìå×[˜¨åf²Ãõ@›Ž£>†_':HvÐ4 YBR‘ fHšdŸåä=ù‡ <\šôà´½4»»FPË0&d¿ñØË°¨âý®ï‹£>¬ÿdci_úãWÐAü úcW0¿ŠRÁ52Äň–9ÄB"…»àcYĬ ÄZe£,±0„`JJ@š[«€0ïCµ'€*>àj›Wñ\fsîëR…‡YiA¢¹¦¥÷²õªŠYù È3Œ‰;©Ó%} ‡çО¢ÈÈ`x$U9§ú$MYDxРaZJA^vö]ÒÐT .KHÈÊy/:wãÙóÿ¦ïx"Š8räÚÛÛqäÈ|å+_±Å&aΛb¡¼Õ…²Ê vcä³_Ó]®³³½½½tµÝÝÝØ´i“þ‚JòÈ?Ûô‹À ÅLt(tÓëë…§ÈS‘Hýýý€ææfTTTÐdz{{ÑÞÞnÑaÙux?ü0ZZZÐÝÝMÉ3%„ææflذ.\п„FOƒ­¼­`÷o®_¯h®æ’CÒtšhcuuuèèè 64±ö!<˜#;Ð àŒ©ÜmçkD[”B–ƒª8Ð5™šÄÙ/Uw þõÙ‰L™Ê‹“tŒ¨±Á4ÑAŽ9_9ãáÊLÅÞ'ÅÍ÷IaÞ`Pw%1 Te§¬rA0rÜm]µ$€(VÞ7¶@;ý‚®2B<óÇNà†{nËO˜Ê/áÁJÜôÑ»ðÖ·¾KQ'j·ßˆŠÑ÷²óøÕËøÍ7¢>áÁæ³FB– ¨ ¼6Ýzdx¹L·vÏA–ÆcÛ©ØË²h,+GT”eŠ’ù¡ÜãA•Ïo}ƒÆ`AÔÜBv°ä\X jfŸ¯Õ^?ª½~¤£É´êƒ¬©úýËdÌpéâà—w(ÄóØ¿m;Ý´ÇÇ.ãðà\I&lÇ?Æñ±KxlÓ<ºa+B‰Àж—U ¿ˆMaGYõªcã²Ãµïi@JJ+<)"äÐYU“‰×@š†¸,BPe”ó~pLžûÃ2iÒƒ(;²&;YG9ëãe0.%–­‹kxª8Jv0W‡y²Cot½‘aôFG–%úS¤Ñ7g›C—ÒêtÞ9ˆÐuE~çA€å݆¾tJy8ÇÖ¹Ã#à CÔdhöø]­ê°hïÅz°†óaZ^ù6y?áÎZôÿDU¹~¿¦¢b›Ò¿Ê¹È¸ï\';86fEHvÒÉ\ëó1‹ßeD$au²Ãµ8xh¢’€ßiØ©›ð/½ôÆÆÆðƒüÀ{ì¯ßÎMÁ¦ÿó7ù ÑJ DSÝ»ïÏÕ¿jïéÑ­òÐ××GU,BSSöïßÇë.«N¼mí} ºÙ¡Dšk€ì èWô±úfþÞÞ^ô÷÷£¿¿CCC+*¨„Ãa477£­­ mmm4YÞ"‘ÚÛÛqôèÑüœIûúÐÜÜŒ®®.´··Ó)Üwß}ÆÖÏ™wÁV–ê-¬”èà4Ä÷ÿÚpÙoûÛÔ€&ÐÓÓƒh4ªÿŒ´f'u~W†!j祠Jt=¦fÉÃC5š0aîªXºc¬ŠPfÞ…2s:/v \lÅhªeêßL=ëæû÷aïSÒyKa j67R#è™ËšVš i]]]/€·µk²ù¿¹«Aròk¬»Ø w߆÷ÿü×¼49>ÁôÈÅüŽãÐ(TY¶ìyçþõMŒœ1TvÛÇ?ŠÝ¿ñ¤#ý¾òÞÏ!‹‹“ ÊëÖ¢¬¾./ãðÁ±Ÿá¿ùºîrOmÚ†Ç6oËÇÏuþ ËÊlÛ‹³ AeÀ ’íyŽ[ù¶V‹í ©*E ÈHHi„:W Ï0¨òùQîñØÓ € ÷FH'’÷ „ S;Š…ì0×åeMÅ„Äh"†˜,æPÞbU€€ xVîÜ«FðÎáüLN Èñhß¶ÖoâÜŠí߬@ƒ¯l…Ü2âÞkBô‘TvUÓBZΕ¬ñú³æÝ›*€ »sÜ Õ³ØGU2G”ò1ÜâG7÷ÅAÖ½n¨ƒ¬G˜ó¢­rZÃÑn¤ yš[4Lˆ såXGÔ2-­k5ýG–ùÁ ñà†èŸœÀáÁ3˜œ€ˆË¾òówpäƒsxfç4ûë€dæ$¸sñ*8/BlþÖã§v $€g–uQ³4DD}d‡´›i˜’¨ö²æÛl°€וl1”ƒ‘ÙŽé¤PÖҾȚºH̓!ÄÚ„á"PvJEÑFÏÄ z£#ˆÊ(¬ETpøê»8|õ]À®`-ZÃh«J“ (œ™[éôùÄ{ý`(ÙÁ,Ÿ‘Ô°dYDÉöë!l÷åDz²X.MNuÌIò0ìO˜´ýWQ ©ñ\WRTÅØ\—É"ÒôÁ{\Axs^ôÜò0Z×lpÔö«ú’€E*Ìyí[ÿ1(—~¨ûfö¾¾> ÑÑ-@EE:::ðì³ÏêÊÙ³P'^S½·t67Z‘ôÃUM6ß/5òŽî2=ôáúº»»ÑÕÕ¥?‘q•¸vï½÷¢¥¥===¨¨¨ jtvvŠ]vâСCˆD"èîî¦TähkkCYYfggõǬØy0áôÅB -‡ù€<ö=ƒïAa&ÑßßohÀfÝSS§w&>P;/‹…Ä)Á“8dkUZFv XÅd‰!(3§¡¥.;_9ãh[±„Kçàiâ¤i²CͦFüêÿ:¸–£fS#Æ?Î=ò©rÉÚŠ)a?¡o*su’µ; •{ïû½yi/ï÷ä9¹·˜tÁy=زï6c޾1¿ D¬7¿‰ª›î¹Kw™I!…÷g£y9ƒŠ‚„,Ùº7Oˆ2De…$YqßYÌ®6䜸ë’ä}Jv°¶/Vr|~ïÅŽòjì­¬G/˜Nس“ì°ðóD8 Æj溚«ªñå;ïÁ_Þy>Þ`ÿºq%™ÀÓoÃÁw~Š1_à3Ǥӳ“ê\­îp­¬–NÊW”üÄ·¥uh@B–%¾¯:=Bì”ç$ðù¦s,àáÜ7æ.‚¨*ˆŠ)Œ L‰ID¤Ôµ¿)1‰q!I€¨*Ej¯Õãjì º.ž@óɯaÓ›ÏãÀ/_ÁÑɳ”ìàâWqèÒ Üûî7Qñú_¢}ðeôLRÃ80·,ž°Æ¦•‡’ìû4iRDGb¬åt¸A%ïGë]¦ÚÀ€ÀÇp¨â²<%;ØÐÇ>€rÖ›õ+už   fdQ)•áÆìœJ[¼ÏÞà+ïToÊë´ØªEïî'Ódǃ±¶²ÝäDŘ,]â`×=`è‘tݶhl4&_®\|Y7aÅ5ó(2×€œóUKŒJühkkÓ]¦»»M7âÀ–’¢¯¯MMMèïï§Á) "‘ÚÚÚ\Gv˜ÇáÇÑÖÖ†H$B«ÈñÈ#*§Îœ.â 6­Yy_‹ âù¯šd¨ì¾}ûp÷ÝwS#š€Q¥¦j/uzÇ·ËÔÎKAül/Ù!¥þìœ?8okMœ c™ÓF[„„4ú"ä«?rœì@¸ØŠÝð4< ®ºeÙA{ÙÔ³½Á{îóðtœ),õ+{öîû.×Å|—J “Ú[¥îr?µ7omηÊoCýÕ›7ÂWÒߟs¶È§šC6ÜøàÇ •ûᥠHÉùaÃ'S¶×1›³Ë…+ŠÕƒB_sY)ÙA_;(Ùa¥Ï|,‡›Ê*±·²MÁ08Æ`â n@b€(Ÿþ“³žiÞƒîû¸#ć© <Õ÷*¾2z ±àòd¡”ªà±©%=)²ÃBˆòr"™ÕuäØE„ºÕæq¤9ûÚá4ì$=Ø>&öeIŠª‚i)…ˆ˜‚ *YÕ<4hH©2"R*iÄrÝõd·oì :ÎÿMo>[O}Ïÿñ«ô­@ž1¯þðð™ï âõ¿DÛ™#¥M~p`nUð^ý?˜±¬²°Eï"Jì ª@JÕ™£Œ)þøYa·ZOðÚ_µ'€rÎÖi%¨R ;,@'ˆoÊY/ü ?Ãa çÃ&_ÊY/dMÅ´”BJ‘—ù¾É‹ýâwÖïÌ[Ÿw…jñÊ®ÇЪ±ã×þ,vÊýÇRM•ÅådÉ»ÄÔÞeèñ‡ÆÐÐÝÐX£‰Yš8eì'.t2-~ÞPYŽãpøðajD“ëTOOþW õ õÔéì;53…)Ѐ䠕ò°RvÎògkMŽC“ãtlW´Ñ,”ÈIˆ£/Bžèƒ&Çœ¥ž*pÕ-àž[±`ä/ª"ä«?TÑðó½ÁûsJv °F|KIL—¤­ê.îâq¯:S{‹î2ãç†136žŸ  æÕ^,Ïá¬O«h¨Ó]&´¶Ö±~sÞP(¯c¬®FÕ ›u—;99QRóÒæi1u}¿nW¼Q5$¤óŸ³ºáÁ¢Äj+’÷MÛÉ ² ƒìÛ‘Èøós°G4Ê ýiµ@€™•‰u€£Ä‡ï Óÿú}¼š< ø“&¤$FS³s£R`d‡…1uiÌu8¾ Šl,©} 2'Ð90™šž…ô ¨ &¥$&¥$fd!wU G|Ë.b 1YDDJARô©6ª‚ !™QM¥öC©(º.ž¸Fr8tñ†SQzøt)¢²€£“g¯‘JNùÁ¡µ€` ï×Q€¤c®“ï )²ƒ¦¥÷ )ÉÑýŸå,õ*Ç776“bŠˆI9‰I9‰E€ -?d‡%uuž 6x˱Á[Ž>ž0H*2¦¥dU5\Ö³Dî#ë°ÁWæx×?^µ ][ïCål´½ù_`5Åe*»D¼Õ†ITåÁ:´µµ¡¥¥ÅPYåÒ+ЄI—8Ÿ{U  °îÜìÐfS58¼k×.455åg>ÿ§¸õÖ[1ðî{ΞOæH4aþ:úûûÑÜÜl›º†Õ0œOQ8kgY™½ª*@ºÊUsp+¤‹G —Ý¿¿aÕ/Š4º»»êÿ»XÝÂÚØPÒÙð™Ïఛ࠹ØŠi"P¶×\bkM…&ŒÓ1ÎfqòD¤ÑoA‰œ2E*0&ЮîSà×=&´-þ_„4ö²iƯþéÓ¨ÙL÷hö¡fs5B®óžš€"§ wc«¡r§¾ýJ^Úë åŸQg‡ÊDjfVw™`M•s}^¢@ÁçYicnߣ»LR‘qbâJ^Ú+( ’d{=qA†¢f9¨* ¸à€fwV¼”8“௹„ˆ`A çÀmfL4«ÆŠ,'>¬–ÔgÈHæÏç‰S@Ì\¯“ć¸,á/Þ9‰ƒïüç¸q€»ž¬u.AT “ì0Yq–L¶äÝOæä·L¾²²ÿ‹ªâ®ù¼€ô i*.¤f0œŠ^#<Œ‰q¼ŸŠ`ZNå?FÚ@v53’€q!„"™p Ób*wÒƒkçbÚÆ9…î+ï ùä×°éÍçqðü)É¡±Pù¡éÄóèxÿÇ*æqtxëÍÞ\•¦<Î’8†A•·DȪ  -^_yb¯Í €ËÓ@“é<®*ø Á%1¶l?1ãôMþ9nb²ˆYYȢĥ«GYü?ÿ°ñÃŽvý™Æ;ðÇïÈÚ¾þY³ MþP*»€ð`Ño¿ì†‡ •ëéé¡ÉÀ¨ÊÈü}Ð]*ÐØÐdû“C´Ä(4qJw¹öööU¿‰DÐzçíxö‹ž¿sI4šS[KýýýhmmÅððpAµ»¯¯Ža‘ã‘G1v›)TÂUsp3¤án@I*[QQ¿û»¿£FÌÓ~𭦄k·“4!|) ^Å¡¤‡•ú³sfqŸ­5q Ðd:ÞK÷Ò‰!Hc/CºôÏPc_\ÆxÀ„¶ox\í`|õÙ÷fWM4wAÈŸAÃ-7ÓA§p_VK36QÂEn›o%HÙzÝåÎ;‘—öº!ÑÞ-ÉþÁêjûì_ñ¿ó…º[>ÎëÕ]îÔÔ8‚ä|ƒ5`,éÌæ3‚˜ý!çyatâ C…ôàÙÄ;8Bª …¡î Ùm=Îw‹ˆ°… ~¹fIqiâƒâÃÀÔ~çg¯á+O!æK\ëÅéÙq¤ìÞ°Ûwdó[­º½_Pä,ÉtyŽ…Ê³MÍ"™ÁGTMø˜ÈNz(Ð÷“ E”˜\ø+Õ›PÎylïvåñå­÷áÁÊM+¶/"´ø<“/ƒ ¿ÿUyˆF£¦’ô)£¹¹û÷ï7æ³g¡N8ì„îóåk€MMv®Oêä†Ê­vë~$Aëí»Ñ÷ú‰¼›üèÑ£èíí-éØ4Ov0rcvN`}`B[À„¶¬õ¿­>|˜’ŠFU<”È@õ’ª9Ôø¦”Cþú¯ÿšÑ$zzz ó˜ª;ÖO h:.Ѐ²ìœ Jr(Á‰@ÍRL¶V’Ð$zÉÇõÍŽeæ4¤Ñ!_ý´Ôegc*[y'< O‚«náVVz“'úL·qï“âæû÷ѱ§°åkkô‡(!V’¶â¨»Pä¼p4¶@;ý‚®23WÆ1Úÿš?äh[–ëñ@ÅüM.¯§ä|„aY„jk»:~íßÝÑ.5Û¶âò»ú^2¿z¿±ùF¼Þ|9·‡Ÿ Ìþê$Y… +ðrì²6@UÓÉ7L’œ<Ë0  (‹_=8Bv@arjG1‘ˆ}vÒØ+Ëçóć:oC‰Œ qõýcç€8¿ øÔe˜'>ìß¶‡Ïà£#¶MÍï ë£ÃxfçÜÜYNÏN ¹¼ÖžŸŠ;ò\¼ám܆g苺*I!÷XŸTd8ÖFcrUMAâ°Oq\L Äz'‘¨²ÃŒ$,'Y’k¤aVQÎ{ó7Otoõ IDATÖ‘Sè™<‹ÎáãE«âÐè £É6\¾?~QY({ įâÀÙWÐñÁÑ^»ën3eŸ¼#ïðý,‡¸"B]©ª;9‚\ œ£U 3æác9$T{È÷Öª;ÙA…†KâLæ=Ó‚:¯Š „ü0v’rr:öh˜–R+¨xiæëQ À¤Ÿæ¼x¢î&üÍè;¶u{­'€/nÞ‡-þŠUÛç&Ò›¦) а^›+r¦?lýÇ ^ý™îr]]]èìì…5èêêBOO¡ÄbeäÛ`Ê·Ù”°¥œO»¸649?}R#ú×†ÆÆF455e?Ãô÷£õî OºÆü%Kz°…ìÀúÀ†w‚©¸eŽä°$f)I¨±sP#ïB‰¾(æ×þǺ»»éBSdhkkC(B,¦3ÙC¡FOƒ ï(Í—4ÓrHC_7\¶¥¥?þ85¢ûh#`J]ÝAs¼`Ñ‚¸k€è°ÒŽ»Ô,bkM…šºB}€&ÏB9 %6¨Îça_=ØÐ60¡m9—Q"'M+OÜ|ÿ>ì}êQꎠ¼¶š!GPÂEî½µ;¡ê$<À{ßïuœð¿É<Ã@SUËžŸv½Ÿ„ׯC r X ˺¢MŠ$¡nçÝ„xkò*>lãDÒÿ‚}½¬ª˜¬ñxm¯6&Hð° É´.Ë€‡wvÀœ>ß,#ßO)’ý„ýˆÉ"À1éã”]EcFPÅûó³ÞXµnË¢-d‡kã«Êj<Ø¥Ä"—Ùk(EçðqôLd2ÿ®`-*8šƒkQÁy¯ý;4™$8¬†þø•k7c÷F‡¯ÙsHˆb(ŰPÄ‘¨,àÐ¥8téZÂѱî6´Um…>”sÞì‰Ã,“E ÍZ”ó>x™xE%)é3Ö*1•# xÂBÒÆÜg‘ºCñઇ *«Ö©BCLQnWr»[È@ZåÁsÝ&¿Ó°Ó6Âà þ tm½¡y2Î*íëŸM«8y6ó¸­Þú5VSR vøDö_$¸¤üFh3¿Ô·.G£èîg[„ŠŠ tvvâàÁƒúÝFœ‚2ö°ë?é>G¤$ššß>i‰Qhâ”îr+ÝÆ>4x­w݉hÂ]Šn}}}Z‘¨QŒˆD"hkk³Œì@<•`ë¾eebë¾LøpÊïöAï5M| ¤‡âÅ£>zm|õ@ô»ð@IyÜîcD=Žã ù/Å’sb?úúúô¯Mõ`ʶ––±´¼.:wRÚyš¤ðl­ ã€&—´;¨©ËPcƒ¦‰FÁÌ‘_½¾vÇ¡DN™ªû†;öàŽÏИ@áòIZš1Š(tx‹dÝíÐ.½©«Øùc'€?q¾¹¼ß‡dt&¿&óz %­{1/ë$pÔÞtc^úÍûÝ'ù¸¦q#|ár¤túÄ©ÉqÜ¿®§“þL )GŠª!!Éfêc>UÛ%“ů%,»Å¥ïP²ƒ£}±‚ì YaG¢»|ˆãÑ\^‹ 1‰s±Èò$çŒåM’"ÅI6M|¬L|ø_'109aË4ýÙ•Ë蟜@ûÖíx¬æFø’šü%*åëýФ‡ú’=!N|—UЦ.Oˆw YÏró…V!=$)Mx(PeAUP$Û}XPX§•0rûZod]OàèäY×o1æ•Zïì&3ä‚yb´†7füÎ<)¢7:Œˆ, ?~ÅÕJ}ÑôEGÐè £sãÝh_»Óý{P—¼ß÷0,| —yÁÙO`çaÞgßšâšñÖQ”Ü/D(ç<˜”¬½ÕØÇpð2VŒkq‘f”f!ç:%UØüôÑ1²Ãü£´ëßßà+þ5 ø×éQK»ýñªMøãwè*3”J¿Ûñ3œnÂñ”Ûðb% ÀÏ' Ànxò{¡»\gg'%0ÿyŒ&¼€˜y­{tÓ¼xÿÇñè¦l™ÁýSãØúÝn|G8 xTwŽuÈÊêJ3z}eþ ³èKYü2Ÿ01‰Õnõå˜EDËÄ/Uåú8:俊¦åe컯¼‹¦7¿Š¿|%¯d‡]ÁZüÙÆ»ñöîßDäΧÑsócèX%;ä€ Î‡¶ªmèÚüQœl>€+·»åøµ;f½yiÓ<ñ¡é­çÑ9r 9E*SÈ$ *y?8ÎzžåPí –ÙAU”h Ùa~–C%ï‡É}lx¢’÷6ÙÁÆ-ˤœDLuÕÉ€ Ž9ÞGES3·UÏBDL´I^¼×f³q×ü¦ÈÐs'áASÙ÷ë®Ú+äÞ(vÃC†jèêꢋª…hjjBGG‡±Ñ§ ŒýÄØAÒÂ3©ýþ\Xµ©îî Yú6AIB™|Cÿ9k×®e‰åý½¯¢ã¹C¦ÛØX—;öbúGÿ½_ý$:kwV²4o«B[‹þD†H$RÔ1§¿¿0÷ÖÏÖÿlÙáZ5•·ƒk|ÊÜY1E[[[Ñk© ¹¹Û¶m3p†“ FOç}ïå¦GSäqÒ…o.^WW‡çž{ŽÚÑtuu"3Uw€x« ³Óš™@ƒG¶}.±mÜì…êÏÎùCñÛZ“f IÅ.Pcƒ.ý3ä±—';0°»Á7<®ºÅ2²ƒ4ö24qÒÔcèø %;Pä ;¶ë‹[&Õá ”ð@¡Ó_¶ðWê.wþ¸ó„ÞïË»½8¯5 ‘Ñ1ê|F7¦ÊõD“š·zÆ[“ãPço>“U©¤c¶JÉ 5Kã¥È!é?½ã䔊%;š?¶ŒIÎ6 öŒÃ’Ï9ÂbK°ÍáZøXÎÛ¬0þ+}>ÃÓ< -ïwˆçñ{Ú‰î{»ªª-ŸÉ1IÂc?ýÚþ "˜ýþcaŒ_I¶%Öò6ÜÒÏ’_[Í¿ÃÈ%)Ÿc€¹äÝk£˜f_V¦¤*Îù¯ÕcœãØ÷FFòNtx¨j+¾±í“˜¾ó úw÷d&8P,ß³j*Š„¨”””ÄU1Ž«b“R€†W¹ÿ»é> îùOxmÇ“øíºf4xËoçR⃳‡’ÂK–0Xã"À™—„¤‰UÞÊyˆÛw¶lHI¶Œ7K”s^Ôx(c½ð1x‚# Ò/Ã"ÄzPÅû±†÷³dP|dISæâ“¾:k=kÉ“9öqF¡eÝóØLvXR G|¢f3*yýïÛ< kšìôN_øä,Ÿëº"¸4ÉÀx£˜Ê[³(¬ŒÃ‡chhˆn,Dgg§áÛ•K¯Ì)uP¢ƒ;›ZD‡e~e•ºƒ@ûþýˆ&Í]6ô¹Ç?„þxO|e¹ÿÓT¯ŸÈØßß_´±&‰ ­­Íä¦5Mv þõŽL.¶ò6pOšzÒðð0Z[[)é¡HðÙÏ~ÖØz9õ†->ZêË`1Cî4cÊÖ„¼þúëÔˆ­]F ×ìºOVg-!8Ðà±pkÁAs±?:¨?;ç%fkUЭÔPXý¡DNB}òDŸir€î¸Ë…ÀU·ÀÓð$ØŠ= œu¿ÊSÿf Ùáæû÷ÑXBAQ  „ cŽS{‹î2矀‹;ÚNÎÃçÝVù$]Ôn¿‰:ëøÂa„jõßè÷ÃK#HÉ’5Ðy˜œ½6)ea*ªµg™¼ž‹2$ﳬ¾öYAv°ÄNÄ[Ê9Ö,¡¢€È ë©à½øpx-üe6ØŽäÖ>™" ÂÊrÔøòÞ}øŸ{öb­ßz¢ÃçΠµ÷Ÿð6; °ZþüØlb¤ï–!`+_ßòv+%䈜’5ÞëA9gÓíòš½I¼Ž¨-,ñaÕÎ:<º72‚Öpï;/8Nts^ì_{ ^ºùQh÷ü zn~ íkw¢‚ó97ç IEÆ„˜À””¾)]ÐÈÚÊ·ÚïÔà‹÷áä®y#?\#>œxÝWÞql^ÆaŸ€0 BœUÞ@šP©÷Î0(㽨öQÎûÀ’xõ¤ii¢ƒdÿm*~–C9çÅÞ‡JÞOkxœ–/|›{'¤^eXÃùPn¥JMŽ}Tå:ñÑHpÑA$Èþ]íÚþ~žù‡MÖ¿ÍUC¶_ú½Þé‘ôdàö"Ü`OŒ×¸J=È¢¤¹Øu3T´³³Ö¢»»ÛpYùƒ¿w[p,lªûû²Z˜:i,Isi2}ç§ÿ=FŒ+÷„C¼ô¿>Š®ƒ{—d W 0B€_`*'+Ïð°‰>m';dž;låí`ë>nêɆÕt(ÜçÇF Îº{­¢¹®‚2~ Zò¢áòÏ<óŒa2+ÅbUw e[ Cݪ8X¾¿-X‡’VêÏΙ¥Dm­©P“—aä°hd?`e²ÿªí“g!OôA9 %r šsÔ´ÄW®îSàžÚ0Ö*€Ë}Pcæöñ7ß¿¯$Ȳ @;)Š”ð@alQjl5TîÜ1gUŠIáADêx¡~çÝeFâ1Œ%ÖœtbZ hÎý“”äÌ·Gj ZÔ·‘€µxItDu…¥º`w=%Jv˜ÿœ#ÌbµKæ$Ñß>‰¦<À —ñ½ÀÝuëð·÷Üý[·[>»¦&°û•ðåñ7¿â|ܱªIIÇ\ ᱘ àeYWØ+ÄyÀ¬ôŠzA5þ2Àá¡@s܇íVxJEÑþËWpï;/ /:â¨=ç•"w>îmŸB[Õ¶ÜÏ䢪`BL`V š0D¾ÉÃBξ‚æ·¿†^»ü°Ðü„¹¾'f ƒrÞ‡_e¼>–ϰ‹„Ñ8†ϰp<žôw+=øY¾4@Õ€”hÝYÉUȃºƒÍdËãÖ*uz5| /}œ•ãÁ…XÑžy&dú=O¦ùìÆfC]ÿ—‰s¦Ú8”Œb85c¨nžõ‚xÂÖ¹Ò’ß5U´|Æ{~”fjï2T®§§‡Þ”m1Z[[ñÐCóŽÙ³P§̹VÞ}ÛåÈi ÉaµÐ¬‰SPgÏé~þ®]»ÐÔÔtí¿û_ùÿñì7¿g¸½á½Ïm- FS.à Îͦ$tŒþ¾Ÿ]]]èëë3þGÈÙÁÕ?¶ò6S5>|˜öŠMMMسg“¡L0áŸTÍ¡d NCû®áâuuuxî¹ç¨-‚QR°«Õ(ÉÁñý­«íLIÔ‘m7 µµ–¼h²cõ.d{jê2¤±—!~Ë4!À˜Ð6ð O€¯û_½-u(‘“–èøLQû÷ìÄ$FOŸÁÈÀ{x—Ï B]íòMŠ‚‚*ÄJ®Ï”ð@alöW‚”éqyþx <ÖbSúûðSgÍ€š·*÷ÖÔ8ÅÄFßDÑi™–€¤”eƒ#ËÖTÏWÙê'äúu•¦“Ó"; ž‘ƒÍ¬Ó’ ;¸ãó Þ‹WèP{°«} LzÄòÄøÏcÿ¶íxá¾±«ªÚò™þô›ÿŠöw¿‡H 悸c$Nj€ e&=ê A·îÆ/Ë™»9Úâ÷fµÞાYç !Äy޵–ô ÛÕóá%upvÜ>7öÃÇÑ|êë8|å]Ǧծ`-¾¼ù£˜¾óà5%‡ÂÝÏäIEFDN™":dÂBòC÷ÖOâñêíŽõi ~÷¾ûM´9‚!+•F ÑO’a!ð³<ÊyÖxü¨ñ†PëKÿUzXãñ#Äyáe¸Ò!9ÌCRÒd‡¢Œ ÅIv¸v´ÕTÄQ9…iéú_TJ!.‹×”Žlð–ç¥IE΢¸¤9dË%õ¨ìÜ™¹‚÷â¡Ú-ºŸøÊäûúÛ¸à{½Ó ÷Fn´d³¢ù§UìÿQšx«ÁÔ~Dw¹h4Š®®.PX‹®®.„ÃÆˆ;ÊÈ@IºÅµÜÞ Ï·…¯æ°Ì—&Œ©;,º=_H ãé?0Üæy²Có¶¹›™eg ðftˆX¿ÇXHÚ(ô÷÷ãàÁƒÆ`ÙAÇüÑnãS¦IÏ>û,zzzè‚Sàøýßÿ}CåÔÉÿ㮃?Íñt%ÄánÃDgŽãðúë¯S#Z„îînCÊD¤l+˜²­îéˆJr°x_[Ð$ ”ä@ýÙ ¶¾f‰Ôhªñ\,M˜pUÔØ ¤Ñ!½ -uÙÙÊØŠÝðlÜ®ºÅV% 56%rÊÔ3j65¢å·ÿcQúµª(ˆŽ]ÅHÿiŒ¿? 1qý}`r6†é‹—]ÑÎÑwŽï~ñÿÅWŸø-|õ‰ß‘?ù~þÚ¿–tLj¸åfýqH‘JÎN%Ix¨««« ;#  ëo×]æüñbqçœeón'–ÏßMÂk7–¼ŸÊ©å›S_8ŒPm­îg¿r ‚lp¡0yV˜œýÑ<)e!6hZúS3g'7ƒeЇì`ÉxÂPp ÙX?ÖZ–qY¥ü5µ‡òÚ•“–5Æ?ÎS< .·O?€/ï݇ÿ¹g/‚oét>|î Z{ÿ o³£ksð±ãÐ<éaѵ±†vléðF,²qóÀM(缨ó†+=ÌÙŠ',|å(ç¼ &[XJà!Œý>¼ˆ»ÖZ+i805{t/%xT4 HÍ‘4ó~ä<àL&²š~†MïÑÊ9/6Ö ÎB@'ˆF6*`3=8ðy®+ûØ‹MûÀÜòd'á!Øøk !C©(ÚN÷¾ó†­¼=? vkñmŸÄÐm¿‹îmŸBsp­­ö*(šŠ˜"8Zg˜õâwêšqr×¼tÓ#ލ>DeÏŽGÓ‰çÑ)½f(V;dΑµH;X¼d‡¸"""§À3,øö~ÂA°êü«³¢ªdPwÈÙxÂ,ÚË·¯ÿœþD²4ܾy…EÓï£$—<Íþö«J6ùp~&Þjòõ¯©Ñ(º»»éºa1:;; 'Ê)—¾-1šÝÍòwøEA©9h…Ýbb|ÔÙ³ÐDýJÔ=ô**Ò÷EΜ@ç?ü‹áö¹c/ZwϽ?ŠàrStÈðÙÐå)0\ž­{låíÎÆUæ"¿ñ)¿ñ$¢h4ŠöövD"ºè(***ðÀ[+§ÞÈϾŒæ ºâ4ä±ï.ÞÜÜl˜¸J±†Õ<•`ª÷Ø–—ˆLÛ:bÛi.÷‡BõgçüÚzEëH3–¨3hÉÑüõAœ„<Ñiô[iµUt´~&ЮîSàžÚæhŸÍÀ àWÿûÓEGv˜˜ÄHÿiL_¼ u•ß4Ô<çü½}ôû+*9Œž>ƒ×_8BEöøCM࢕õT{‰¿¤Lÿ­-£ýï9ëänPyàÌ'ecÞeCÅÆ tR­€šIUžœ‡¢è<Xt†˜vXåaEƒ‘>åõ,•cò>!Ù“_-Iî·â”ì ¯Ÿf’™sýœØ3Z†qAšè—$L$“˜•D$d’ªBRU¤1I”Âd2‰i!…¨  –  –ó#%ÉHH’² QV ¨jæñ·ÊŸ—Bb@¦< ‰åksˆçñ…=wâ/ï¸kýÖ0£¢ˆG~úJú¶n¿ ‡7»c›¦¢‘v/÷KBÖxü† >–C÷¸ÖV !(g½¨âýXÃûà]ÄÆÀË#=8HvHÛžµ½Žyp„ŸµŽðÐ5zÍ'¿Ž£“gm7Ùþµ·à§;ý»?öµ;­QspÍ^&ÿˆÉb^Mð‘òüÕæá­]íøÃõw œµ÷–˜a!Š{ßý&ÚÎADNéóªîP¼ %šS½s÷›+ÙaF_ í[ë ®øR4ÄxPÁù © b²èxSªlï"DôgØe{ #*Ç#ÆnDí?kX!*è)ñ„WŽÛV˜Ø2ƒû~”6ªò`ôÖUŠ•a†H"q‰›PòEÎ$‡bRtÈÜ'£ê :þ.¢IckëCûÑñćÒÿ!À%¬®è°ôŸsÿ>t9†á1}„‡]»vU,éííÅ¡C‡Œ¯ •·ƒ«Ðî ¦ÿ«¬ž-¿âYc¸U¦•/(ò‹§Ÿ~ÚP9eâßV=[8»ÆP¸âp7 »ø€ã8ôôôP#ZÃêë>‘Ÿí®)% g”Üì…êÏÎùµuNPhÂUkìl€dÀøÌ]©&† ½ éÒ?C :k;Æ&´ |Ãàj0Ý]V'!½lŠØá ðØŸåµ5EãÎóD‡ñ÷‡s&2ä;öíù~ßy3WÇi¼¢ÈìÃÔ¦ëõßÜrÞa…+ÔLO4Þ\ÒXjFÿMCž@€:è ð…ÃÕÖê.wìÊ%rŽ?`[|–O:KxHÉ ´l·&êa|æýL¥3y?A)§ä~’{F¿CÉ:ûIl~þ uØDvHÈ&RIÄ%iÕßÚMƒ¤ª×n?­öð¡²*x YQ!( ²Œ˜("%Ës7ÈÚHvXˆ8 2Åòrû5WÕàoï¾6m±4<;ðÚN¾„HÀÂÛûœˆmÒÉ•’¬/îeûdŽôÀ3¬Ž§„x/Ê=>wÛÊÐ&ÍéÁa²ÃüX­ºiLÊy¯%ï]Aóɯãàù× ''æ‚0çÅþµ·àƒ95‡ÖðFGƤԠh*Í*_¼åø¯ëïÀ[ÍphóÇÐà-³µ¾£“gÑôÖóè™,þ¦â+Ä åô_Q;@ñ’–<„ÅzoBK$¶yTr>Ôð×ÒŠÅ`b‹‘>jÐRdûê"9.xK¾ça@^ü*¹­Vÿž}Vq.9½rûæêæìÑz®ž›ûú_g'ª;ؘd É þêð Aý—« S•ÐÚÚŠýû÷ó¶Ù³PÇ_Ï碊âPspïœ%0“¶BŸ”$”É7õŸÙÂa´µµ†þåk8ôƒ· õ«±.„îÏï›[œaŒìP±àÌ:8©» MMMEG"‘ˆ©„~â_®áa»&˜ù°ÁúÁoú4`⢹£G¢««‹.:¼V†ÃaýÞ)N,P²q@Ñ¢  Œƒ–¼h¸ü?þã?Vè¢XŽžž÷ª;XBp Áp‚àà0É¡¤@ýÙ9 ¶Ö U€š¸h™u«D0/òRE¨±AH£/B¾ú#h©ËÎÆd.¶b7< O‚«náÊ7òÕšV±x ã3¨Ù\{²Ôl —Ï büýaÝh‡ëjóÖîñ÷‡1s5·y3úî³r9«¥§^J æµÚºËñ„£¤ÖÃçÝN“¤‹Ô¬þàÄSÂCzƒ©dOJ¨ß¹C÷óFâ1\Näð¶ 牄,APM4K­¤òëÞ?߯Cô¶a'¾R²ƒûú¢å8öv=?dMÓÄ$ š‰ç{·”Õ Î\ôuIUŸSPÕ¶yM–WÈ42Ã-{fˆçñ{7ïÄ_Þq‚œuëøÑ ïŸðß6¸ÓñMVrŒ½«Ï/BÖxý{|ð®p“¿—åPÆ{Qí `fœ"†­Gé!d‡yX9²‘Öx|†’—¢sè8n=ù Ä®Úf¦0çÅŸ5Þ¡9¢C“/ìø˜”œÞƒæä¬OToÇÉ]ö¢²€‡Ï|mgŽ`(µ>¹Tá!3T ¤ôZ\´(^²CL3¨%¤ÁA ïÇ&oë=!4zËÑà-G˜]NþK*Α]–×¥YgÏ\ÉKmE äè¶µ[æô“%EFsj_hšOÏxZ-J6@>!ÁÎÅéœU ïi¶þc†ÊÑdQ{ÐÕÕe(‘ä G%é`k Äßsj¦ûIvyÇÐÓ¯%Õ ´ÿÑŸîc÷ÿ؇Š2 2¸‘¸ÎgDï)ý "ÍÍÍEC:;; %‹¦øÆ§Öo_° lÿú4éÁ<ˆþþ~ºè(Œ’z”ñ^ØJr ï™ â4ä±ï.ÞÒÒ‚ÇœÚÑâ}°¡åË.uªâà’=­KìLIÔ‘m7 µµa¨ÒÙAµÄκɈ§Jßh˳P"'!޾y¢šìlb3ñT«nßð$ØŠ=Æ ¦ÆM„4ö²é¾?Ðñܰ÷ÃïÆ² àò™A\:3ˆ¤Î\R†eQ³¹kÖ×ç­ýB<žówg®”žÂCÃ-Ûõ¯ ª\rv¢¿fS˜[Üü•€¿Rw¹ ýï9ÖF΄b2qDô³×4n  @²«"ÔܸÕÐ3OMCQòs€K$­OÌ–Ô£iX5ëºPÈÚòbóÉ^1I¤¤‚-ÑØŠäWWPø9„sý(ˆSQ¤‡<’€% ‚,oK>†Cµ×ošì0¯êðìðqÛLtèpûï¢sã=¨à|y“R‚  ±øð‘²õ¶Õstò,šû¿VjsJÄ4é¡hQ¼d‡¤"#‘cò¹‡°`Vh˜dDáÁ`?44‡ì©­øLßB’¬ºøÃ¶µúU^ü@W?zÆÏVŒ p^²­Îù÷Š>WØ?J3µwx«t—@oo/]S,FEE:;;V’P.½âС¶€Ô ¬/æÂôõI¹blÏ'÷>ÿ,ú~iìvìÏ=þ!´î®O7÷²xÝÈUÝð]ïoOŸþdÿb!<ôööâСC†Ës €ø×[ïg6L5&´ÜÆ'M=£­­ ‘H„.<ˆŽŽcËä”Å—öќĂ†8Ü TüóûýtlÃÖ××§¿ ë³f—õÛ\Jr°l?[°$‡’¾`Ÿú³sþ@mmÞ„*´ÔV&;è³³!ÂCŽÊjê2ä‰>H£ß‚9eZÙ@ÿ9j¸ºO_÷˜Ð¶¼4ö24qÒÔ3ö>ù(n¾_Q¸òØÙ÷ Ö¬¯ÇÆæ(«®Êkû…xî9åkkhì¢ÈìÓÔ¦¨öÝeœTxàý¾¼Ûˆóšc9ÆÆ§¨£Ù_8ŒP­~©¦ãW/Cå•÷Á6az‡-› E…–ØàêN‰©äü˜¦âÜL—âqL¦R˜L¥0›Åplv1ñÁ)Õ³uät"+• W‘ˆõc­eÿˆ(@VUËŸ_ÎyÐ\^‹rγlÎHªŠ¸(BY˜\oÙaáw"<Èlfµ‡/ì¾´se7ÜGE÷þè¾í׿Î÷»$QÎB83÷:˜ÃÂðà í†o«Æ„!€/ éÁ%<ÃZúÌ2΋rÞ{ýÖfƒè=a«ªÃ"¢C£DŠÅÛ>­0n¶¢z;^Úþ¨­Š Õ"rÊU1‚ÂʵEK¯¹¢\äã[¼dQU0«–=YÒKü2ØOYSÓ{ÿ\ƒ ±¢MÚªßó.ÜÈ‹÷Šmµú WÅÆÄÕosò1i¢E÷¥ÓéíšÃ&C[ ªÁ¾Åó£4»á!Cå 'æS¬ˆŽŽìÚe,qK¹òSh‰Q›bqø{ª9[f4S1IK\„–ÔOVصkWš$0;‰Î¯þƒ±³`ȃÎßÚ=·€@Bf’ÃRã,%;ÀÜr6t9†á1ý·C áÁh80á[ÀVÞnm,°yª±•·ƒ­¼ÍpùááaÃJùESSvìЯôU„ßš5†¾/(h(ãÇ ­óøÆ7¾Ah1Œž+ص÷êT&²jNk4 dتÙKpp˜äPR þìŒ?hÔÖ–‘ -yqÉ;;óv¶CáA B{òØËPc_¼ÅxÀ–ïßð¸ê0¾ú¼<Ñgšìpóýû°÷©G‹Â•S³1ˆ‰Ü[Ö¬¯òyïÃøû¹_>Q^[MãEfߦ& 0}(Y‡î23WÆ13æŒôŒ60çh}µÛo¢Î™6Ü®_²j(6‹ 1‘ýüa#EÁŒä,{WT²°œWJÀÎë¹Ë$ÙA’p)‡š!áUP\ˆÇÒcàÙÁ ª n ; ÷6HªŠ±Tçb ÎNcpv—’±ì*F|Ì!_ŽK$Uµ­–0تBƒ/”Ñœ YÖŸÜ•ÉZ¹Ž­À€LfV{xp}#þöîûqCYØ’¾GE¿ù³á3o>®ÂŠÄ $¸þ—Sïମƒd =hÄ™yŸc_*x¯q%máÜ$ó>øYs{ÔˆœBëÀ 8xþ5[Ì’•èPˆþUÀ(4S,T|°‹øptò,šÞz½‘ê Åun­•j‹…¬©†Õò}\H(Rî‘×!²»tï± XÛÚ-s^Ý}}mjxÕöùYÀÑñsééi`EbÊ%<@“QÌ?J3•·Jêë룷ÛÚ„®®.ãñò‹€¤æP€DcÉa ûb\åR¹úSCEçë{¿úEô½lÌÇîEE™H˜DîŠXb°y%FÔÑÔÔTð1£³³Æ|ÐS ¾ñ)ëbƒSÛø˜Ð ÆÏGšŠ·ùÃg?ûYCå¤Kß5·ÎP>ÄiÈ—.ÞÒÒ‚ÇœÚÑB˜Qw`×Þëà—&)¯´—uxc_°Õتô`«Š…µcµ”ì`"3TÊÌiH£/¦üS—5áB`+ï„§áI°•wæ¬Ba7ä‰>Ó¤›ï߇:>S4îœkþ«‰–Îj Ó‹_Ùz€ÓÿC×¹co:ÖF7¨<°¼ñd²ÈÅ1êh6¡f›±¾ONM,¿ Ù¡óÇD*騤l„MK“2Åò‘L'ç%âéϳlúTMÃX"±dÂpºÎµ¶?ƒÆd͉øA“% 'f0#‰PÜ‚“% Çg²†r&–“åsýœ@Ó4$dɦç/ÆzoÛC•à2t/%+ ’EˆA?¾¦ö°ÄäuþþöîûñhÓË\ë7ö#´Ÿ~euÒƒ›ø5 HI ¬\ ó8¶2ä¸ HšûúB@°æÿ²÷îáq]õ¹ð»¯sÑH3’,Y¶%K‰/‰‰+7H‹ƒÔZRÒ@CÊsjh¿s€öSzÚNŸRœÓöÐËy@!¥=%‰ü5ôƒ’€M @†ÐÉÎÍñMŠe[wÍHsÝ·õý1ºK3Ú{íËì­—g+Ì^·ßú­Ûžß»^9h]éaY/ N®¼­™G'ßDÛÏþÇ] ø>´õz ÜôѵŠŒìÀ` ć?ßù.Ô²ãù'´~啯 çÂsÌØ•U²JžôPñ¨Lu˜Q³ ·ã8×ÛH@3tø‰ìòêýÀXùp×VëûóŸ&.mØ‘çÕhàª=&ˆ¶ð”>þñC’¬«[RAb±  åì¨Ó†B!Föuž¨;0’ƒã{XgÁb$ÖðJ3 ³µ»}¶@vÈ:ogB§ðÀ/#<múô PFþ?èÓ/€hIOÍ÷AÜÒ©ù~5û^öM×éñ_Ø&;4\ÕŠŽßýŠri9‚(î'Q–Q¿³Ù×D‡‘S¯™ïÃ«ÛØ<ưþ\ÊLÀàÈBØx½õIlà5ïÝ“¸ðvsиï昴lñ[Å`[(Hý—/A‡ç–Î"a&—ƒN¼»\)è¼ú»2';$UÆBpÇ_°¬YUÁD!ò‰_ÊEAb£§àØ­+²bÓ]šîÙë»®3’oBÿ‡­<®jÅ>ûiªÂçÚM{w­‰•Ã!4\ÝŠíûmj¬E‡Jì?‡æ1fGië –Óœ{þEÏêç…ާnñKcÌÉh7C&n%§QyJÎá\",™ 4tša`&—ó°<B ¸ #t‹ 8“ §­ pZ³\ùšfFÉÑÏ}†Û˜ @à@{…û˜QsEÉ ˜R2ÖúkYÖÛoõû¥r²ºæÙaÑåÁa_¤͡Țﳚ¾¾} õˆs‚ÎåÕ2ke·nÇ?¼»ª£ŽLGνŽî“ßjTJ?tàE‰õ ÐJLÒ([Ñ”A8@Qðm[‚„-ªÅ@Aʼn$ÔÉ!ÔÉ!ÛªCÙÚñ8¹ô’ãæh FñÍëîEÿ£=²µrý« !qåÿr,*ðÇ;Þ—tãÎÚ«Ï05Žö—ÃÑ©7™Ã”tÈ)ë«×U$¬“’º‚ Ù8†s Œäæ0’›ÃÙì ¦´Œù"=˜¸3º•¸³7’6"<8ÐÆŒ™·#;Óy.î¸ÂYtÖµPµùùxñ›s_H\Æpv–Ú¦|ý-Þo‡õ\åO#6TìÜ,ÎP±XŒ^ACÏ@{ë)³‡$ÿo˜-‘üÕº1í æ²Ñ¯|—*ûîîCß1ªô‡Þ·7m ë¬oÅÈ« YµôçQ ²Pþ„‡þþ~;Fל\qÛt>ç³)ƒ 퀸ó~úsßà S,*C|éK_¢JgL¿äøÔËà© ÐÇH¾££÷Ýw3¤Ã {ùØ kÉÚŽØ$PhÛåì†Þc’æógïüÙÚóÎ#ú¼²CÎ50R¬ç'D ^þ´ÑoÃH{k^†» Ró‡ néÜæÏ}XòMh“Çmå!‡ÃxÿŸþaÅËËáv¶ïGÃÕ­¨Ý± Íû÷¡yÿ>To©/‹ú¼òº©çê[[ÀÀPpJc&`pä S·›*ÝÈÀ«Þ8z+øÙO£¦±¡¢=ŸTo©GíŽmᲩw.•6ý,Sw°CÛtMf„g †¨H_~Í“ê ²TúÁF©ðœœfþå¶ûØBAzøùÄ82ª ç ðÞN§³Jn)hۨłiu½Äg5αÀïˆ$¯·[D±W6†Y… ‹Áûôyx`/Síõ–ì`%œŸ-!Ùô‚ÜV~P#ÉØ_Ý€°¸ÜE èZ1ïro>Ð8pS2 ¯´UD’ðÐ 7ãûnp¤˜ãc—Ðy|éÁõÓ;å¼·r’.}j¥);¬yC"æ?›Ø^‡‡žÇ=¯~ ÍÙ[‹;¢;1pÓGq¸õöÊ÷¯2FH+®Mï¨Þ†¡[?ŽÏì<èxÞ\~í!®eËË(Æ&ºdŸ·=<7YœìÒ†Š)µ¸ŠÃ¬žCºÑÀC²F h00­epEI⊒D\ËBƒ½~–x¡0qs¦-)]õȬù@/2ÿk+mÒY×l¹6'“¿›Q³86q–º¥ñÚ[J7Ü´tåÏ(-à™n_ÉnÆv v4ô ÿïêƒQyl’7¬ªÛb=PÌf[(’Ó?£*ª§§'¯îp„^Ý¡m[P¤W­¹…€õɘ_æ£Px(wu‡¾¾> R¥ênÙíº{z=Yˆ;ºÀ…¶SçÖÝÝx<Ξ2ÂÇ?þqªtê%FnÙLP‡û=Cþ‰'ž`FtÔêõï'×3’ƒƒ{V÷”<Ü0’ƒkþÀl]rŸ¦!;PøQç@r“þž»ƒWÑ3' IDATÛ 6þ*¤æûÁGö¼ìï^U¦ Ž~Ûv>ü쟡áêV6L|ЉóC¦ŸÝ²IUU–ÓèÙ¹Mg'Fx`pnÁ¬µNx8÷üÏ=©›èƒH$”µpÙ¸)<,ÀPÍ1ÙhT&²¼06š_t$ïUDFÓÞý€^0Ž– U²à(ÎÑÀï€ °F†$ɱ2 ?ãÀk"Fv0‡Q,8¼Äd¿ÝÕ²Ãâ˜à\_½MÁ%µ¢Åíçö¨Kà2kçÜ{ÛvãÞtªDûkþàÌdžôv™ôàÙaa’Ω¥#=Tª²Ãš ¥à é¡Ììײèzõi<<ü¼£UŒŠ|óº{ÑàÃh F7‰•/Ž/ôZ¦ÐˆÃ;oÇ…[>ŽŽèNGóL£í¥¿Ç@j¬| B*#øŸ€@1ô²0Ð7¥ªÃÂúf­Á.+æöBq‡Ép4·jˆkY\ÌÍ!¡+ÈY¢cFÏábnIC¡.­Jp÷’bèP7ºÐÀub)O~^EÊlÙ][­¿WIêjA•'®œ²eW>v=[ÀÝÞ´ÜM•Ž©<¸‡ÎÎN:tˆnfU¦¡_~e¥æ@l=P²Ùz°˜$šäzú”õßo¢Ñ(º»»óêoZWwˆFä%u‡·¸•k Ù!²ô¥ ;ùàörE<§Ww‚›ïqÍ=½›(Ö© ‚´sA¹Â:‰DYûÅfÄŸýÙŸQ]LGÒ˜ñ6 Œø ŒÄ+Ôé;::pß}÷1C: GÔ,­ìE1ýžÕgvfæÏ®úa¶.‰?‹ï0Ivp ÛhÔ¼Ù iûoBjº |¸­ZC5ØU…8_]e1諸Ãê6¤psâšònÝŽÏ¿ã]ز¿.’¼Rz0;ï´Ë|'{©æî\, @@¢Ó_fÁûCÙ:ÿÇ&Ï8ZÅߨ߃¡·]õ{+Ê^•ŽjQWamZ ðµ£è¿þÃøüÕïFT 8–BÏáÆÇÑ7~²LæA”­ÊƒF $µ&s)LdSˆ+™Ÿ‰l Ó™$Ò©HIÎ.¥Þ?Xï×K¹9Ó$×Üz’¹œ›Žº“j3zá´&ÔLa%Š"ðd^°¼53ß³FJ@v€`bÏc,=ЪÁjërá?˜^|š4ôO_¤¶ëŽúý€(Ù¨³t“\9Ï.LåÁ—èííE4¥J«_þHzÄß{•2Wsp°±æ’ÛÙ_÷S¥ëêêB,=F­îÐó¡ëòêi*Ö'9¬^Ï ‘8uK†èýªõ@ÖÖÖV´··—õœH$¨ÒŠÍ÷BÈ5s²Ø`\†v@Üqu)ÇŽÃÑ£ìöÿrÂÁƒ Ä€6úf¼J‡2õâW©“‡B!ô÷÷3;ºÛêníµ*ñ|‡2WqØÔÝÊüÙ;³0;ûi?¿|¿¶!ÙÁÁ®#©óþ2/CˆÝyç!ˆ[:6Xÿ|C6þ}GÈo»ã]lèø³c¦ŸÝñ6vÑ7C‘i™€Á±CPõ@ YN7qvÈõºI¡`ÙÚ599m9MmëNæ!T*ýW.c*“Íû™ÇÄÍ0<%=߈Ÿí8׿‚€Öêš¼ÒÃòg„µÄ†/ ¼‘z‹Û3ßWÙ¡È«6Þâø)\~iÉ®ùÍ1ºE a_¤aA„j+²ó”ì°€..­²€Ý5Qüã;ïÀ®ê¨íâg&Ñùc—HnØ‹ §Á3xñ^Ï«w‡VTv BW§2 ÞHŽ¡ýc09îXžÕ‚Œo^w/Ž]÷ÄÄ`EÙkSœ÷À!*+º=ÛoÅ@ûGW{xàÌ3è¹ð\yA//•˜Q2˜Î¥‘ÖÔõôç×G-§ ©)˜TÒHéʦ¹4“]ÎБ14{ÅzŒimã³ñ´jíü,òí²ƒ´ZQ¤Äd‡…ôaAÂÛ"õ¨•ÐæU2JBvX€Î›–m¥=#’„Ï¿ý]xÏû’oƒ3“èyåG@ê\;,ÛËÂ+gÃI•+ý¡h€Zùöí=…ñšs·ßkÃ[ïøÄƪ ¾†Ì ¨.áÙ^À-µ‡G.¿ˆ®×ŸB\ËúÛFy’ZÓ¹4Ô‚Ê]Ðt@Qe{? ¥«˜V3ЉQÁÞLGvò·ë[ÁŠ}8çnϯ‡4QÁ›P\TA!æÖqŽãP#À­× ‡È]CV׊w¡•îvð¹*IZl;bèPÖk«º¤k«õ÷‚gÒ3ËêGÔU<5þ&µe›kZÀKfJ­cKê,Pyý}ª³$í¼i»•ù³7f!ÌÖ%é8J;9é‹KdºÎ˜}£ôsypĦ» 5ß>R¦¿¹.lìÁ`ߺ¿wˆ ©2ÁÄ…aÓÏV7lac(¼ßg&`pta¥ <Œ ¼êIÝÊUå!~iÌr¦ð‡–µ¤×°w‚ÑËå<óÖÒóÁ¥Ñe•‡UóY%‡´ªºnKR¤‹Ð½¸Á³¯D`*}þ5OC(ŒæHIZ,_’eÔËA´TEÀ#¸TÙ!£iˆgsPµ•Á:!H* ¦3Ù¼R‚m1Ó÷ä/X å–ßXË”,¥8{«jѨ.-Ùa¹Eã¸ìÊù7"Ixèú›!=9÷:º_~–AàA>¸SÕÝ-£Rnß'^í+ZÞ¶n¯ÃCÏãÓÏ8V­*AÂßíþ5˜K³>êWþ‚Ì7¼“îœÅT\…•ýòw@rS¥u/SþçØSs ö†¡ °¥îðå¿¡Rw°¤îp…[»vY%;@˜ ™Q‘Utô}ûŒåú´¶¶¢½½½,瀡¡!9r„îœTw+¸àŽŠ&9¬lpÒU¡N~üøq=z”-d&`pô°Tgý†ü‘oxA(½}xkCŽFÝ!¶³…9âÂfÓ°”ذ׺%çðf<ÿ㼫*6飙´wF%E ž¿EÕ½dŸì`ª+û/,JØ^ÁÞX-öÆjqU¬õvÉp¶iMCRS1•Ë.~’š="LFÓ0·Á¤f²¹<éÁͶÎTúo~ž×|µÔdƒßË mñÙaÅ|)…ÑÙº~ ˜WóÎr$pskU>ºþfÚ½Ïv5Žœ{ݯ>COzp[Ùaµ­TÊà|¿À3²ƒI(>·§Cöê~ã<<ü¼cÕ:PÕˆo¼í7ñ‰í7û¬ïÙ>Ù.d^@‚Ä eÛ„bkÚQ ´Ÿi9èX¹ƒ©q´½ô÷Hù×8º y²Cºðþ“@NËÏÛ&Î&ÀLE’èÕƇYD5B ¤d ¤/š8‹›9»T‹ò!;˜µ™…<«% 5‡„š]gl¨DG\Ë"©)ëî©:êš-[å'ñGÔê5àcûÙBíÚ8\u¨™ÿO¾æp5×PåÊTÜC{{;>ó™ÏPî2Іž,©{9t¨ödÅ-w5‡5Ed.ÁHžµœ.¢ûÞ÷£ïëtAÁ‡Þ·gIÝA-°æY!;„Àd<‹ýÉ[MZ®SOOOÙÎÔÊBâŽ{|¾¹0–C; 4½Ç–½ãñ8[|Ê_øÂÀóÖßg“Ì%@™a¬˜=mêù/Ñχý×e&u öÔê6½ýÜWrðËa“œ½\&90[—]çéódßé“ÌÅÛ=?'F Än‚¼óÄ-àÄê²÷mò¸m²C}k ~å¿<À†WaâüégõQf0†â{~fGÛꀲ>±Úö²¥ç““Öe£åp˜9¢ ´¼ýªtß¾8Œ¬–¿1>(¹øUdÿ?“ËA7<ØÌ›9ƒ¸¦òàÙÁ)ÕQ ‘¨,(HÐB5 Œ¦Ó8;Ç›³3IÏár:‰©\fñs9Âpr¹bÄŸuÕ ²DvØ =„³ŠBow‡È LþX‘ѵUi9ü©€/›L¿!‘ „d!¨%ÜÛnþÖv7æåÈñë’íÞ‡ÿ~ýͶ«säÜë8<ô“òPzÜ ÒߌÁèvìX&öŠkYt½ú4ŽŒr¬Z‡¶^/ï}/þS¬ùW…BàxÔJAD¹,Õ 7Ò›ð“Ã;oÇöQ!àHÙ =‡ÎS_ñ/éAÓ\&2SVkžì°nÕÉÏ×9Íò@\Í‚TÌaìäI f q<šäª’“„ù×›A^Üø,µoW‹„±hyvÚâÙÁŠÍ,¼ˆ´¦­TÄ[ï|~i]Í“ô•t5ZWýÑÌ[ì«;Ìl9èaX1$ªÂ$‡5J€-¿AwÆb*®¢§§­­t·¢‘¹30â'½q1+>è“•¶’ÔVƒö¦ëîîn =ó8޼@G\;ü±›ò¬Vw(ôïFˆdYEÇ7ú‡©êÔÕÕU–cßÖ­Ø €òY‹¼bÓ{À…¶Óñ z’ ƒç¸ýöÛ©Ò©ãÿÆŒW){ZÊ™^€ÐŸ=zè!ù3<11hmM\ö—bïŒT0HiûÄwepæ‰g›„ìÐ9øÏ86yÆ‘*U >õ»ñÉí7[Sad‡²EX°E£JʆøÀƒ[? ¹ˆŸtFwbè–ãn UÃõÐs¸qàqôŸôŸÈüúá«*Ä•ÌZ²!yU£œšWœ£„‚9M©€iŸìä‰ µ&­ÛåêâÊwnMÜÜúûf‰ o°îh¤°o—Ù3i3 dDžƒº®Èºù¥uÊ*’}guåÑ+¹”mu‡*1¾z/F"Ñse<˜'9¬X[m¨<°Q÷‹ÅÐ××G^»ðO€žqÝÅ,>PÒUÖ3’Jd=}êçTIÿèÿz‡¿ðeª´‡Þ·mÛ"Kê‘6RwàóêÉŒŠçG-ש££mmme9öioÅæäZˆMwúw=òÒUºß;†£G‚Áÿø‹¿ø ºsãÌKÌxe¹¯]g3ú¼j%víÚ…Ï~ö³ÌÄ.Vai3ª;8Op(Á¼icÏYà½wþÀl]¶>½úõ”:’+Ñ%RFDM¸^ n…Øt¤í¿ >²·¢<Ä ²C]ËüÖßfd‡2ÄÈ©×L?Û¶•Œ¡ø\ÉLÀàøÁªz‡å4^(<ð‚Pv¶ÌÎZ—Y–˜ÂÃÒ†)K÷3ÊCZÓðÜ¥hóA6É[ͤÝ?¼É×%„¸@zà\ºißÓâÜ ~œyÆŸ¤M/-™yE·Ì(™•{`bÌ+:¨ŽÕ3khk·Ëm$:9Ù5HaÔP5á9Û宾g1GvX<šÍ¯9U¢\”ô  Ò­c›@ÝÁ%¥6­’æ–°ðØ,ÌÖeÛyE²$ÙñÒ‘‰WÜËœ—ÁGöBjþÄÆ_ÜVqÞ¢O¿`›ì ‡‚øõ?ú$#;”)&.˜WÞl¸º•Œ¡ø´ÉLÀàøA‹âÆK/~€ Š–žONZWx¨mÝÉœpaÓK¬Ò°wÄ€õÉŸyk5ÿã¼ÀsΨ<˜<ä4“—TˆÅÅBw2Hˆs-p{å3}’ˆ¢ëdH(Š%²Ãâ\¢©–‘Ñ4Ìds „P©a¨«oÔu›ìP S7̯¿ç¨Ò q½yćd‡l‘C¸%Ú„ˆ »;fM½y˜'=+íïéážã߯Ëü%oÚb{޳Iz¨¤÷Ä‚b‘ªù×^>$;Ü»å|yï{d´GäÅŠ²ƒÙ·¨øP'…äEßQ¼´¾Š’?9¼óvühÿ‡u(°Ö·¤EuN%Èf”Ì"±ºä´üÇpž‘5´²}nLvMršÕ¨ƒñ"B¼ˆ)Œ«ƒ1DD7e´­‘€•JoU¢Œ°°þüc¬s~ˆJÁÒ’¬vµ Ï… š¡ƒPØ>gèköÜ4*ÿ:yŽÚ,Ubsµ·€Áʳ®â°ì¨<ÐÞFÎ`½½½ˆF£tGëøIñ“޹Zá‡ü·²zªèà“xcêgTé>õ‰ßuFÝA[µîР† «èHfT<õÃ!ËuŠF£eKx OùÈ.uo÷ϺTB[Þ>²‹*m"‘`kZ™àÿð©Òé?dÆ+‡½m¨ç¿d«´'Ÿ|­­,ËwëX…ª;¸CpðxíÝÔ±ç,èÞ;`¶.KŸ6ãÄÈ“´ÙÒž•ã§œŸãÅ„º_‚Ü|?Ä-àÄêŠô#ù&ôY{„)Äÿê3,¾L1q~¹”¹Kœ£LÝÁÌÞŸ™€ÁñE¹†FáaØõzI¡`Ém#H 3–ˈµ¶0'´ 1@Ã^ âN6ƒï\\üï ló¶K‹ç„+n¨<Ì×A,,†‘®µ?›ØWfp›ì°8¸@–€BÁöÄ™ ü¤¦PÙk‘$±Î÷„$s9…šìàxŸ€¾OBØ’ Êð Ùa±=«¯JDv°‚ /¢½¦M*œ nFZ€Å9CzH¨9Üó“oa&2çÔ –_Y[é/ZÒƒWÁèž”cA­HQK6†¼(c 9†¶Ÿýƒcd‡‡ZnÃ'·çƒ ¯ ×›'=1²CÙ"®e‹~’º‘ãQ#Ð W¡N !ÌKΨÙ\£Ö Ò¦ð“ÎèN ÜøQ¨jt¤nœy½—_ôWG9¥¤¤‡”¦@Óu@ÓòuQõ¼ºƒKÈZá€oÿ¾…qu² ó¤0Z5h Ô V š#;=qoP¤Ä­<xQ)€° ®™{2DƒÄ ¨¨—Cë“ ‚)²ƒ#$b=¿…w *Q*@ø!&zŒ¬CxhötøRÝÁP}6W¸CrXóz„©<ø±X }}}Ôéµ ÿèëîæ‹ƒ Û's›ÉGfЧN€(Ö/`jmmEÓÔkΨ;ZǬ‚D`èÊžÃØ´õK‚˜ºƒ×k“ÿ–I±å~@ ûMñ‘GakZ §§¢ÅKê€hs0R˜}5pÍC9Ó ú}zGGî»ï>fz—Ð×ׇáaº˜‘JRwpŸàà1ÉaSÎK,ðÞ] ÌÖ%ñgâ½?$s©ôd‡Ù7@ô”só|pÄ-šï‡P³àåŠõ"#ù&´Éã¶ò‚A|àý)#;”1FN½fúÙÆ«Z™­6„ÈLÀà¼W…ÀUï™»dmÐ¼Šæöë\«/”¿GË)ÐÅÚBA3ù¦Åý¯NÓòö[på”u–é®\Â-;Q%Ëà9²(@Ñ((Î iUŬ¢ FvhC¼¬¢Uµ ÝD;A#Lµ‘sÎN<Ÿ'=(êJÂñ@q€2E×1—S !D¬é;õp›€B€`±`¥eiùù¨í¾‰"Òššï£’8‹j+"ÇãÚªzÄÄ ÞHM¹:÷™šQf$Zà— ¸sGþ°ô7¯ü‚:ßáÔîùé·ÐÿÎû€¤èz;lÛjô,BÐò ?J³)°0ß+¥}}Úï«0Cçà?#áÀMÓU‚„Þ«ßÝ¡üíYMršäªŠ²×fB\ËÎÿ›÷¸žÿoHêö‚6ƒ¼°¨úQÍ@@ sït;5¢#©+¦Ëá9®°¢Ga7}ÉC(ž/…-h¨÷#~Uw0T€—J<{¿7âk®_×czÀrÚžž €Átuu¡££ƒ.ZÏ@»ðOwÿžÍ©ÈŸtΩ1V¶ç”|…ôqº „ÿý™ÿž¿þ,UÚEu‡ òê…H fÉ€ZM70ÏáÙF¨êÕÓÓS–ãÜŽºÙ]ºõÉGKå¢KÉu·Þ íòQjbkšÿqèÐ!<öØcÖ÷ù—BÞó 3 o¨#_É\¢.5‹12“‹ˆÇãÔëo%¨;p>+•³Çe ¯<³0[—mçÑdiä`dFm‚1ýsGòá#{óŸà¶MáIN ó÷~[w_͆fÃJt{^áA ˜áLB×nº63ƒ;¨ÞX$<$®L ¹™nÉIë·)ÕîÜÉ · j6K6²µ5Û·aöòKé^›™ÆLLàöy¥Y êFþæ|ΗR)g«êU5¬tÝás-Àå÷.8È€6 -œ-Câd YnËêK !˜Uä–“q*€ì䃄j¥ fÔl‘´jå€Ãþ´Ílä%ÌmD€+Ùa£@±¦@"¢„ÙqhÝÆìò{*nF‰©€à,éáøØ%ô¼öCôîy7£ åí¯Ó k‚ôPIÁèVȺ‘ÿø±->#;ì Ö¢w׈Ìߦ$\®¯({U2²††I5ƒ¸žWcȺËåé‹eÄ‘[1î”ó IDAT³ÎyND˜“xç^UðàP#Ö'V8à'11ˆ¾=w¡-Åß·ßq¤užà+¼K8ÃÀ¬šõœìŒ2ÉŒì°!AÄzG¥õö¯^‚Y-Wü]ÀªrB¼ˆŒ¡&=˜±›Eµˆ)‘ã×Q61Ov9~ÉçŸo¯iôÌ-åú[Ø ÇQ öDBÛ‡¨ƒƒƒèëë³|³úÀÀâñ8`hhCCC‹ßÅãñuN‡††¨oò·û®F[[ÚÚÚÿ»³³sÝ¿K‰¾¾>´··#‘HX_ã'aLž¿å6‹>ÆHþ4ÃÊ sg©‚@£Ñ(bSoâØÝxZTw¸Ì9Cv€0r%‘ñ~zrŒj|/Ëåÿ«;”Ña…]ÞcöŒä9ª5íðáÃÔ$oðéO?þ¸µßï€ü|©Ìr-3¢ßnÁ}Ì ­`EŽã‰ÉeôööRíQ!„ÊVÝ‘6ïœÄÌÂl½):ÏÎñYKäÆ\Uz6½‡˜}D›£Ï€—!Dö‚¯ÙN¬Þ4åÙáWÿà÷pݯv²!ZæyåuÓÏ6Ì+<ð‚À ÇPŒðÀàÎ-Tgyÿ2;:Á · ñ‘QËi÷]à ç ÷]c™ð_»p754 J–óñ¬ª™Kló,1«äì«<¬S‡Üÿ÷2Ÿ˜Q³“%]‚b¨òý»Ô|¾Á ó¼€Üó[ÍFñ‘ÒººòÌe¢œT!5ΤÝ,’‚´8Gróÿ#Ë5åMæXPD!À-Õ©£®ǧG\õœ­ÁLùQÝ¡Ôã§ÄÓØ¾á—aLü»õõôðáEÂຠ„†åd†º€(‡°^Pñêÿïá‡^óLkk+ÚÚÚ‹ÅÐÞÞ¾øïj²„[hkkÃáÇñàƒt·RkŸ†T½\ ¾l‰tS=q|˜úmÍÕÇûéöÀÿí÷ñÿøUÚŽ›òê9¬Tw°Cv¨4ÝÀÈx OýðU½¬’®üÚÀz¡îV—ÕÊè°âlÙr?”7ÿ7 [¿\«··ÝÝÝeI Ù,hmmŨÙÕñƒÔü[̈epV„2õâ×leñ¹Ï}­­­¬;\B<Goo/Ý:ÖØY6ꜟÇIYîkËp>b&a¶®ät(;¢ÆAr“¾±5a’BˆÝ!²àåMåYN‘îøÄGÙ¡0q~¹TÚÔ³áXUµ1f4† Á îØêöçžµ”fdàUt­NHUYÙFᡪa s>Ñxí5xëÄ‹PR)KéV«|m¸A³7ò›h‹F du *1 :D>¼äEss{¾t¦ð)É¡xŽƒFâzñyâÀ)„-bQA†P„dÉs\þ¦ñÝîÆÐ^µ§¾âé¡-Åá·û¯“ PŒ£çç‚öý„äIXø·0JIv`ðÙ¶Åb™MRs³IR5BõRÈå6æËNëª3å¸Dvð""âÊsQ€5TKùq‡àáÁàVì±;ëZ\'<†ƒÀ¦TwðÉa5„–ß "< ›¿l£ 1<<¼¨,qìØ±5ß8pmmmhoo_$B´·;+‰ÜÓÓƒ£GÒݯg  = iïÿ]vñÍMr(^!¢LÃHœ¢Êu[ú"Ž¿y…*íáßWwãœ!;@˜`d, M7ðì ëkP4-KƒÿÔÊ›ä°bîk!n}´ËÇ,§M$‹s.ƒñè£âöÛ­Ÿ»™—FxðçÀ]å죡¿ä¦££===¬[\ÄáÇéÕ¶þÊ&}•ÂH¬ñ•ffë²ì@§³ËŽƒh³¾±µº'BÞùŸ7§‡)SЦ_°ÏÁîûqýw°![9õšég®fcs`„wo5;,§™½Â–#;›²œ¦j #<¬Ø€šUU(‚º«¯Âè©W,§ûÚ…³¸¹¡áyâAPÊ9wÓѱD«ò°A«„‡…ýPíÙÁ+b±-¼ÚÃrâƒrv„"¸”I"£Z êåÐ ²CJÝà–Qˆ½È¼Íhí]äûIFÍbçxþëϹ’¿ÀqˆH’ªêìÜA yñÆc2À[ßF^[U˜Ä©)W^„˜Þ§ÌH uÊŠîzèú›€šôPsxàÄ÷Ðßñ[ˆ¥"ææ=çhºþ_‡ôP)dbѾhö eBvèüg gí߀û‰í7á[®]áÃMr¶H!ê¶(†èÐ :ÈšµJ3 hÈ“ $^@X$è½Ìß‹¤Æ×rÊÆ1”Ë÷Yâ­ý9˜·œïªÆÅ¿›äš¤<|w¨AF“A“ì_‚ø¤šÁ¤šYò91lÞï<ö“öª­è¿þÃèzýi ç컇/>¶`Ý7øøÐµ@bpf™ÔFv0µ‡ ÞéõBGQf€pu(†q%Ùe¤#‰ã=%;ä q ].‘DŽ_KP%JÈ)gÞ¿"¢ ¡À™¿½ºÑU·Ü¬ÁXÍþM2îýOpXãn6T63188¸† ÑÑѱH‚XøØA__ÚÛÛ©ËÈÜèã?‚Ðø+eáLÍacèW¾K•ûgÿà<ô_§JÛqc:oÚ(²«:‹–ìÌ“çGÆSxöÄRëgür hµ£îàÜ­Ø>›ˆs ï‚>ó"Hæ²å\Ž;†þþ~tvv²EΧ8xð ššš0::jÑE è?Ðp;3¢î(çþ.Pq±XlQuŒÁ á‘G¡[Ç;!ä»6qe2>ÊiYg ¯D³0[—mç¹ò{µ’¹bä|e=Zu!zÃæô6e êè·C±•ϵïÄ-÷¾Ÿ ß Á¹/™~vð ƒÌp EÁ .yV(ÿÑ2¦“ÌŽU>á·p:ÂCã¾k˜ï-ƒ®ª¶óh¼v/&Þ8m9¯•‡ƒó*<Ï! Èiº'‡Ë*&ê èGšlH’ð ÙÁ;iË"ñÁÈ“t:L^½ iICJW‘Õ5<8<8„EqÝ<Ò«ƒyRw âúA.f•0hímúûò&;, ,JP 9]wfî°Av๼¿Ñ )P…ˆ(a 1 .4i—A¢+IŸØwgçâ87G :83‰ž“?Bßuï²¼½±åÙÛ‡e¤/nRõë;LM÷g["; &ÇmWå¡–ÛpgíÕ+|8È Øª¥j AJS‘1T íT ÓjµRhíMþeòŽ<®e1Ç@j CÙÄâ¿vƒã‹ÎOËHÅ[¥*4ÉUتC“T…Ý¡ZìæI~Á¨’¨’BÐ,× I®2¯êà‘Ÿ´WmÅ@ûGÐùÊW¨*ËñÀ™gÀߤ§¦bb m¨%¯‡dÅŸ<#;løÞšä*4¡ *1Ìõ'çl[r†f¿Îç8pˆJüžõ‘•ã‘dÌ™T„ "B‚¸Ö óYwÖµ¸êšá–÷Vø¬X^$cö4›ÉN-ý›ƒ38~üøšÛÛ;::ÐÙÙ¹ø±‚¶¶6>|>ø U}ôËßÙ .ÜìÛÕÒÓƒW©9¬íÌ ô8ºÃì¥óžJR¥í}p^5pŒ[ÙivÖäz‚‘ñ¼ºÃS?¢Ê‚©;”ù îe µ|Ê›Ÿ£ö«¡¡!0ø=ôÕš¨Oü|<‡h£ßI§ßOpX¹ j²¡Ô˜Š›‡˜I˜­+ºÝî6##s ðÙ%HÔê¼ ¡æúÍçu‘®éøeÜùß>Á†q… —Jcä•×M?ßpUžðÀSÆ!1l0ƒ{‡»š Óg-¥™8;„†ÝmkÁäíøñK£–óŽílaNçô¦Ì0 È2êv]…‰7Þ´œþkÎâ¦e*²$äo.6ˆëK*&ë@¿aYä-é: öƒhmvsöËpBýàùüG$KäbÅ'òm ‹bžØ`²Š®ƒP”c¦½U²Di/Fv°ú}À ÉBÓ WÆg’€T%Ú |ð2n‹mÇÀÜ8’š‚’@_Kzˆˆ">k~÷…ç0–ISe{äüëèØÚŒjn„c|·_&d@ósvÙ/àêV Œì¸6\o.Ð|U[4b`FÍX":¬vÙ5ƒ-rx)`Ó§ïÊÈ ý‰a ¤Æ0w•Ø`cj cjjMþÖeä‡öÈV_ ²†Ž³Ù åh–«Ñ¨ÞØ=ô“˜Dÿþ3Òƒ¤tÅõà8¿Zˆ‘¬¢d‡…uÎV9œÉ²(ȵr°€J8,’šRÄß‚‚ˆ)°þ>l^!"&ЪÁpÆy)úöX ^‘wV⛨’®]¦j˜º’›I]dĆcñððN€èééÁÑ£Gé¥õ ´¡'!½íS¾Z%==t•3Éaùš5ÞèËéþûïÀg¿þª2½oÚ÷Ö*€ Ö’hÔ¤üù`èÊÎŽÌâ܈õµçî»ïF[[[Ùͽ½½Téì«;l¢Ã¢Û…v@Ør;ôÉŸXÎ}xx‡¦Vâ`p===øã?þchš5e¢ÍÁH]_u3¢Ïæ#>}üG¶òøÜç>‡ÖÖVÖU.¢¿¿²™Yˆ-÷–\Ý)9°9ˆ™…Ùº¢;σ®#êˆ2á;²ŒÕ¾„šý/o.ïsˆì°÷àmøõ?ú$Î+êÑm[QUcFc0w`&`p Aë/KsÉ”»OAÑuß›.9a]Ý¡¶•œ†®äom¼ö*Âë3Óxöâ[¸çê«ÁÍ%©œ7·–ZVy0E·HxæI…–εÀð¥ï½";pÖòà¸|`±(ä#7u#ÿ)z £·—jÚcì IkÕÜ$;Xj?ç‚?m`/Wý9Ö[+1idVXx9±œì`‚œ¡C∫úWâ…µ7¹RÔAäx´W7âlz£9Ö3æÑnN©Yš—#¢„?oÿ%<øâ‘Òèæë_ú1ÚïØŠ±ÝúØ¢i‡S¶R´üß’Pšþp¤ W ÙºO?c›ìP%Hè½úÝËT–ìÛ¨FL Zn‹]²Ãb¶˜ÓÔˆ_½3kYô'Þšÿ Ût÷ ˆ?Á‘ñü °»‚µØªE{ÕV´WmE“\U’ºiÄÀP.e®8ñ¡~²@zè¹ðÜ¢ÝhñÀ™gЈ¡3Z‰¾yÕ—œá³ºÄù‘üÇÈþmãÚ¶h†A_ŽKd‡ü>W.@ #+ž "d^@JS3ôÄžã%„©È^léùöšFW§ß ßr“L¡§Á‰aëcÁGûcö4Hê­y’ÃÈìi0øË ÑhèêêBWWb±õ0ìëëC{{; ë„\’¹ýòw l/­Ú #:بž>nð Ë8qòuêbì¦ü£œ3d@ 0t%û&­ºõ Ó%ÄÐÐu (ºÃæ"9¬†Øô³¯€(3–Óööö¢»»»,I5›‡Âc=fýÄůB¾ö0úiQf ^üš­,:::Êr](7ÐÚ˜“ëÀ׿£Lö>[[ÁÁu³0[—mçy=å&AÔ¸/­«Çé‚÷7¡ºƒ“d‡÷>ôlhWνð¢ég¯Z""áMk³ÙñIæ8f¦[f×}!넇‹/¿F]ÞÈÀ«‹ŸBCÁ²°]rÒ:á!¶s'sºePÒiÇò’«ª é|çkçÏblY]xžC`áöl— *N\ ƒ¢ÒºQ8H|³6œ09@Y‚@–ó$_vÅ,ç …²ƒÄó¨’$ëö$ETÙ¡p¿wÉßÌÊY½~ØÙA1 \É¥0œ™Åh.…‹™YŒdçÐr‹ÏÕH6ˆT«ê r<®­ªGs°ÚáÓµ…gU\r%cwu í¿…ºø„šÃ?ûP­Y[6çhGl¥jyâCY¾U¡±1ÉóüÜ'è~㛹˜€ ó~#<0²ƒÛèe¿Ø++"ÊHËëç+pj¤aĤ bRur[áÂd‡u²k¯npÜïmùepRå ²öðKLœ‡]†1{ú•ƒvöq¨'†òÂÇ ½ú·Ð‡¾câßÙ¡ÌH$pìØ1<ðÀ¨­­EWWúúú¯ &hkk³uã¸~å»0æÎxÞ>nÙÇÖØó ©góMÿÅOR©;¼¿ý*üøôeª2½oÚ¶EòêÙUk2-Ù@œä0:•A2£âùAëêÙ­­­¦”QüÚñkMÝÁgÀvUld „ n—™Âƒ¿ñéOÚúûv䃼@A‚Ù\ðvQÎ> ú‹çšššÐßßϺÍeôõõappn:n¹×ó='W®c‚øq/[™sOÙ™„0[ozŸ.Õü@ ôEß’ˆ: cúEª´›MÝÁ)²CýÎfÜñɱ!^yÅüeÛßv 3€Ù± ësO ²éì´Y†|ÙÉa·WŒ‘¹šfOÊyቯãå¯?ƒ\j)¨¼fk:¿»n¿Õ_ ¾aNŠ+>b=ˆ¥¶•ÜDÍŽm˜>wÁrº‰lG‡.àcû®[TF%šN { Ív~.öú†Â‡‹P Š:/“ŠË‡< ;À²ƒ©ö¹@ªà9€KmÑI^ýÀ ù ±Öm&òÆvsr˜>Hë_ΟŅåÒðÉ"Õm1V‘ÓtLf2¥?Ìèú²2½ ;x•‡ óöâçU D1¯ójÁ ÉyUY$qåGàž‡(ˆÏþ˜Ù,Sv¨ Vú°]²ƒc>Á¹œr6H?•ËàB& %ƒ)5‹)5‹Ñ\ çÓ ¤uµHz®`þ ¤‡2mû8p0 ‚ %³A*lܾ¼šUh¯Ùºqµ[/j’ ­,ûЮ·áÛ©³|øÔ ¼L®xÛ§Ê ȪyB€ïߪØXO5£|úÄúFOáááçmå±Ù!&°E Yn AV/Sõy ¤ÆÐsþ9´½ø÷¸ñåÇñÈåÙ¡~:;‚/^ùî?};ó<5ùF•”gåg Éq¼’ž€æ“èN(=$ôº^ q-[Qþ¢þ裠ৃ%";p¨ü2½Rwð Ù¡F ΗsØfÜÊú¶W7:Úu|Ó€(ƒYmkòJtsÉM˜~úÐW¡¾ú·P^øÔ“C;ûD^¹‘6ˆmmmèîîÆÀÀŽ=Šh”ŽLI”ihCOº>m35‡ÉS?Q¬©L_ÓÅÉ‘iê2{>tbÕ2 Á²€Q=x2ÿ»ËS?²\¯h4Šîîî²˽½½TéÌ©;°@¶b[>Pë™Êƒ¿ñè£ÒÍÔé Ìx>XHÕ‘¯ƒd.Ñï78O>ù$Z[[Y7z°† Ó­cÛÞëÚ^“+ç±ÀTØÀrÝ,ÌÎÞv^å ¢Æa¤/ú„ì°¾AHæŒݾn3©;0²ƒœ;ñ’égW«;ð‚À ÈP<3ƒ[àjvXN“Kš€yõ»ýxíÙâ7O}ÿ¯þ³£ee·ø%ë’˱-®Ô%›H`òÌYLž9‹™á·`èzÙØQËæÍOíÝôù…WO"£.5s\žôà†“sЗÙ8Ìh´¯ *fÈfÎ>Ã9‡Í2Lå±áLj.ž[úÂÊ$’„@(N–Ö"$1ÿL`žDQè#çÿ WW¡¶6 .Bü'¸ìßby¬[v"•ýKv˜R×J4@0’Ibj]²·aþÇ¡N¸±uãúqó>6·Lybm@D’ ¦”¬{ãù@êöšF:Òƒ/j¸„+û÷¡ý·`kˆþ ýÀ‰ïQÕÓv8÷Æ‚9PuWûÝ^mü aÀL€m™¼ÃHŽáÓÏØÊc#²Ww ±—_‚™-ï͵,z/¿ÈH6q.;³H~è9ÿoxvæ<’ºâIÙ“j'æ.cRÍøÂNSãèzãiæXCâ{ÄKgߨ¸²)†Ž´®"¥+Hé r†²¼W‰¤pS½6­[m™‡Èó>';Ðågú¹eû³˜@k¨Æ‘®».Ú‚Ká=>žEÖùÙ땵tžÜpñ[yrÃÏê| Úé/B¿òÈìi00, ‘HàÈ‘#¸ñÆÑÙÙ‰ßùß¡?ZÅO˜ú™ãÓµµÀ3›Îwñ;îVH·¦èQ”pi&٠ݾµ)‚û)ÿ£±aÝÿO†Æó7yŸ™Å¹‘YËu[P;)«sj<޾¾>ª´BC‡;cȃeÕã ÏOr„-ï¢J{üøqê¾cpÄ®]»(ÜÍ€:ò/›Ør¥Ÿ?Œø ŒéŸÛÊã#ùî»ï>6\ÆÐÐ~øaª´|ìðÕΜ ÉÍ9Ì,ÌÖÝy~ê:b€d¯€ä&ýmg#}ì‡tYo"uFv`0 +„‡«R0À ÈP|Úe&`p  V¯=Ûoê9¿©V(Yä9TK2$>?Wå Íõ‰#"ȸ%Ú„ˆXš[¸¹•d´ˆ(áÏÛ‰:¿Á™ >ýï€ä“Àzš\ª–W{0ˆkýN×›?Gø)ߦ½†² tþ³­<Ö’Ö¢-ERQ#îg—³úo¡ûÍo£öÄçñàùçÉÁA ¦Æñ×#/àý¯}5òRc®+x% Cû^ IDAT=72S¾P{p‚ôp<ñºÏ|›9”ƒ¨$ŸÔÄY²AJW0¡¤ײHê RºŠ”®"¡e1©¤‘ÒÎÍÅ{ó˜'ή &Ÿ[¿Å•ˆKÊóu^5Ͷ9@xˆŠ¼±õ½>œ9 ÿXÊñ.Koki³§¡_ü´Óõ?‚òâäÉ #ßÊ“ô LíÏñè£B’è×BíâÓ éG¦jŽj,zxFöÝ¡ÝâQx°«ƒ’9•ºÌ¾?›Ÿå°(j…ìPàû‘\ Y%Ÿá³/Ðù_OOOÙÙÞÞ^$ÖϨ|d¸ÐÛÕñ¦=bûÀÉt¿>|ñxœ->>Å'?ùIº¹uæåMh-ÌFjê[öÔ¦ÚÛÛñå/™ `gÝZîµ½Ç,k’C‰Tý~öÞ´p͘­ËÒ§ý:?9Ì%-å{;ëñ“ ÚÝÙ î—6…º#;0˜Åk?ø1r©´©g¥`€)<,ÃÄù!æ@fæ]fw=,hæ\›8k^¾pdà5“Ͻ àƒec2…‡Æ}×:^äøZe 5“E6‘@Râ¼Ü f³Žæ÷ÕógñŽÆ&ì‰-½—E†A êî^¦RhƒK¥ âáò²šžR/væ°µËvà5U…’ ,IÐ YU[*ÇDaYB•$£ *-f/+mäW½†äW§çÖO¿< y!X›üßdþ¡ÊN%’ºj:}ÆÐ0¥fÐ ‡©Ê—yõÁ0RšŠ´¦‚RÄø5 +‡ B^ç0¡’Ù .Ê1äE´W7b`nIMq¥ŒÂo4.%‚T-‘;vWÇð‰kà‹§©²|øÔ Üݼ 7¢Ù»v8 ÃÈ«=ˆ°YÏ+â†í¾ö ÅÎÞQË¢ëÕ§‘Ðì©[­%;¬œWEŽGs ºôöZU'ÁáÛØûÆN¢÷ò‹L³³œøÞÌy|oæ<¶JUøÀ–kqgíÕˆ²kãjTI!©+¸6TïL9v|mÏ]€#㧨ó82~ Ñèn¼¡ì}Aæy¨%T ód¾ò^¢̨٢D e¨È©:j¥`~ŸèÅBW¡dp@@‘݈(̹° Îç¤äabÍ–È…ÑYׂãÓö‚ wm{'NJQߌ,spvbÌžI½’º2{$7Å66 ÈhoØbmlÌÏ*¯Lû«*}@;ô ´¡'!½íSMÑÄýaì»yÅ¡×W¾k~$‹˜Ëª¶Ê»û]­è¼i[¾™ãët:%ÙAÓ Í,¤Ìd¢§§ò'‚LÆ"q”¨Ð'~¡áö ·Ïæeêù/ÙÊ"‹áå—_fÎïúûûqìØ1ºiwÛ¯ƒ“ëJó* ”ã`SÇœ³€{ïLÂl]–èón#êˆ2x~•uÃÜ$Œé©Jãäz𑽕qªêÙaàÜ æÇÓö5êÁMm;³D‘åàƒÕ›ÎNŒðÀà*¸š ÓþQ(Xü’õó±-žÕOMgÊ‚ð +ª£ùÉ‘*Ûy¤5 _xõ$þ濌¸D<H" ¢B7Ü=• '“ØgS©Bäi^ - ¬Öõ|°,ÇY?{ø‚ˆPþ¨ äoù\"=ëoY^7èÝvŸ8ª¼PÄŸ–+>ðä¿@ŽX C,#¿ã,×O-¤6S ýŒšCL A½RI•(!Ì‹È:2šº&À[‡PDdu ÏCæ…Â}>ß&Iàí·üãÍ“?Ipà$¸d»{[wc`f?¿L•å'¾;þ3J3÷8Q!yµÝdqžˆTж8ô3ÅF eÒ/ݧŸÁ`Ò^pþC-·%;yu‡¢7V—` ñÎ9ãZ½—_Dߨ)¦äP"Œ©)|ñÊ/Ð7~kZÐÝxšä*WÆURW1Çî`-}¡oÏ]HÙ"Ø=¯î0ѰLv€‘é´yY¡Ñ© Ʀ­×µÕúúú0<² ¾Ss(³1´Â¦Ñýà#»`$ÏYNÛÛÛ‹îîn´µµ±݇øíßþm<öØc–Óé?¬pƒÿ~@SÎ>úÿ³÷æÑq÷½ç·îÖÝèÐØÁM€P¦(JlQV$E ŲEGvDGJüBxIžs¢IÀœ™'f&Ž¡¿ØYŽ Ç3~3‰òfœÈïŲAGŠdZ¶A‰Jd-(j# ’ ’ØÑ ôz·š?º4€^îÚëýƒ²q«êÖ¯–[uû÷©/@¿Kà8###N§/~î²°m÷nû_ì¾_µ±çNÐ}áÜâøºl¯šŽª ‰YPy±lü¬Ìž4œ–m¼»ò{²E°ƒPãÁ¯ýÉÿæÀn‰Hçþ¦æë·¬8wœ¨ÓS}áÿðàXÙZKw‡&EˆÅk«J¼ÇXh±dëdDݯñ ¡Ãúà–lp_ã)‹þ¡H^k‚¡Þ]˜Ç?žý_¸é敉 <˜h/ô°(%0 ½Æx]ôŸðŸA©@’õ‹”R€4­æËWÝ!Ýê\.¸9QÊx’.Kj޳±ž–-ß,œøR¼L¦ûO•CSŠÊ2AW?£Ô’ú„e œ9r™¤<,…RˆªEU!ÓÌA–«JÀ1 ((¡š‚Ûª‚–/Hûç„ìîcQà)À®ôÄ®ãÓ/=ˆ¬ÿsjaýgþýÛî$¦cÅBSU .®ª=,?ÊÜX»EžÃ,(£ì$ŽÍž3•ÇÛî†rΫn†Í­î ¡.ŒÅ§s„Û$̼ : \}Ý´B†cÖXD‘VTî©ÛŠGšw àm³|\ÉTű9Ä©ŒNWqaîá]¡ç2=|ÿ{¹íóE¯‹©ýÃ¥\ ŽsÙ j`ôn¬{ÅY3ì>6¢Š¯% (Õ ;,›—°”íÙB4–£N hÜYàÄâÁÐé©3åý©Öû‹0 ¸H—£)õ†Ë+*Pb¨f«š›áw¹hiôlÙ’ìOuµè¬«ÍÜIñæ¿[@OG€Ñ¹9í>èF8bjc aŒ…–0r- Dœ»Ví§LƒÔnã¿ÕÂiÙ,kêÍ>·iØ¿´þZHZ÷ —±úªbäœ1Õ•ƒ–Ý0”ŽmÙW!C ´5·í· ¾ÿ_u§ …Bèïï7¬ÖᘽöÔSOáèÑ£eY_ï”— F.‚ñ^_AÞ(Ý—×â_•—Œo Áw¾óttt8¾@ϯS§Œ)h³›?°ž¿bpTœÊWšK_—e–[³© Ðø4¨š(?«ÁS 1c‡25`Ü›*»G[;üÆWÿ-78ë®J·÷^<¡ùZÞíÚ¨ðàq;Nt,¯9Àƒc6÷0ýÁñ#ïbkàæ¼×¹4ž¸¿85S2îPòœ¨ž™×gÃuöœäémmÙ‡ðwY¨;”ºý £¸³µ æÖ•Ï܇hB¥öí\®D"hq{À (b‰¯ŽT5y:øò ñ…œ§\S!°Ãr9ËBð°PT ¥ aÀ³ 8†1LSu±ËO4Í_¶æŸ¡B’ÿ϶‚ZV…H>âÆœœX CÐí@Í>é¼6?6{ÿ"šƒ}UPûÇlúR"=ŒF0™ˆt¾&!´qnðq<þ,p7þè— å÷äéWñÐÖ.܆­~aS¾²’œÏ969§“BD@Ý¡L`‡áà%<9~ÒT¿¿yO^Ø:Ý~Óu±2˜Ùl€r9ƒmmmhoo×ntt‘H¤¬êúÊâ^YœÀno+zÛnÍ>Wcñ⪌nwCnÍϹMC!%ƒïûƒŸ+Ï—n†C\• X&ÐÀ{ŠÖöïÆÚPD =R#Š„–7 T7ì¤ÔTy£â› °OXø…lÏÅ"Á”¬\ÓÓh\•´§ã>œä[Ë|Ѽ®”tõ†Å3 ‰9T£ínj‚ßåBÏæÍI°¡¹~WtÈ .ò¯»ß-$ˆNl¨ëXp cÁ% _¼†‘ks¹6‹ñ`ù©BÈcß¿óˆÐh¢ÙÈÁò»ç5©;´Ôº1³7]Þ>z3îO3\Ó©îGña6œ€Ì­îëGÎêÿ^åСCðûýe5¶†‡‡ ‹¡Lý-e> J3Ú‹`›ï…2û²î´GE__+=»çž{pâÄ ÝéäËß…°ã˼ö¥])Mü ¨hný¹Ï}>ú¨ÓÙ `Á`ýýýÆçÙÖû ôzÁQrp*^inq|]¶W†MG¥ ¨8ä;€§„œB¥%(ó¯K̯îàÀޱ·~ø¼æk×ÃÀòÕ­ð0sqÜéD̳ÕHíÐéÓŽ#R¦ä9 ĈÂCëM²å^]>š·w!:7Eáòùàmm)ŸÅ—jýBÚÓàGl!hI^òÆÏñ÷ûîC[ouMLjQÑ>èA¦*Î/…pc}ƒ±‡«'(Gð¾,Œ†SC5÷ ò0Y†¦c*nÀ#Í;òÎÝn†E»à5]–0àvcà§«ã܆”§ÞFÿ¥“O„JnýÚÕÕŸÏ·ì°ü»½½ÝäÍ&''19™ÜwŒŒŒ¬ü‡Ã8þ|ÉùåTd‡/¼˜|01®&ÅŠˆ€·­¨ÐÃàöO çô?!¤$ û©ïâ‹Üþ‰²Ý/{Y¾`ÀG4ðîŠTv’ê9!Õ<ÕŽ+ <¬ÑW†ì°2¶yf1мÐ1Ø/ÇçP_+®²Cº QçàvÿVœôÝQÆ åÔ0¥Ú@Ï@ ©*õ†zA@ ©9 3.ôlÙŒÎÚ:tÖÖ®*äë4Wß\7¦7©ºnLˆé×ëþHrŒ‰l×®”rÃògÅŠ¨™Á9¨Ó_‹Î†Zô\¿y%`BÄÈÕÙbøÂU„âbi7¾ƒ<ú·Iè¡Ðc³¤CJ+J%ŸºƒG`K`‡ÝÛ1pø®äf Ã2Ø›^Ò„F'ô«zW•ºCû2åéŵ?eáu@Ñ?vúúú0<< ÇJÏŽ=Šë¯¿^÷wuTœÄ@h(ÃZ—ǘ“'ÿ êük¦òxê©§œŽ^ ëëëC(dì],Ûù_)8€ƒSñJs‹ãë²m¼rn:ª‚&¦@åHÙ9E™þ‰á`~¶nW[¹½Ü2Ø¡¿ñÕ/9°C•ØÄé÷°8=«ùúÎ=»7ª}˜ˆDõÍEUúÉ+[kéîÄÄÈ{U§àÄ”î4­7í°í~\>\>_YúRŽ›;­Wͧ°¼uÖ¨,ã+o½‰¿ºë—áN N`û¡‡…D ‰\.ý ͧ3ç Þ§4 =pœ¹=Œ;è/ÇLfÛ¤Òa‡,owû0[„¤¨aV³eÓÛ~ð¹kV¡MʹÿN4ô/^G€§’mn*À˘îš„e a¹€Á"Pa¤;Ô½'g®b*ÕÝxd‡O cð¦„͵… Þ§d@&«ŠŒ•_CX˜—J“êÅô—I;øî3¦” î©ÛŠ#ÛîÒä߬êêRË ˜ÍùyY.Ö² ‡.¡÷ì³%:x½^tww# »»íííèîî.XùéE¦“$GGG199‰ÑÑÑ•Ÿ©©©¢û-+ø`Á¸ +F"SE…Þ6 ßò˜)èáèôiôÔ_‡ÞÖ[ËrÇ>V@X±÷9ïeyxÙRyij=ì 78¢á‘ªÂèU;ä½ ‚Á1ªÐxï<âŽÀ曳,‡´®·Òö;hà]˜Jè[;OÔßV– d¹ ¹%> º4Šj°°¡©µµk ‡¬ËTc¢i¿ÕÔ;‡å)NaVÿ­f¨Æër|ÍUfZþ>5ù™@“ã‚K—~·€ž6£§kóJþ#Wç0|ñ*†Ï_+Y‚Æ®@¹ü ¸mÛ?6E‡üw$ÎC™ËZïŠYÓ:Ú}þöƒÉÿ$a)쌈ˆKÊš1tÞðÐÓÓSVsíØØŽ;f`‘íÛxG™ 2‹øb=àÚ€|Uûœ8qÃÃÃe׫Á:::°{÷"ô˜xù»º~¯LjZ^ãM ž‚2ý3Sy´··ã­·Þr:ylxxG5”–ñß ¶v{ùöùªŽ=wï çÇ×eÙ€•ÒlJ j| RÙ9F\]5”–p>°þÛ+··;°ƒcí½Ÿ¼¤ùÚ}ƾáòÖ8Ž´È5^ª·fú¤qxp¬lÍåój¾vft -Ý%]ŸðÌ¿{÷nøýåu ^¿¡tlËþ2åõŶìƒ2û¨¸ ××g(¨Þ1ûí[ßúî½÷^ý½9r¾ TÊoÌ©‘1H—¾c*ǃW_}ÕéÜ´¾¾>Ãiµ³%Øß%Çlw‹ãë²m¼ j:*΃Šóåé5eê'Æ×þÍ=•Ûë-‚\Þ<òçìPM–ˆDu·oTwàÝîªöáâôŒ¥ùZšH“““¦_€8ÀƒcÕ1±†#%qR,;=5{á’îüZw|¨ìÛ&:7YL½-NN• ðÉ<~„§§-ÍógW¯`WCv^’xÃ0GL”ldªâÂâ"njÐþÂÕÅi#tïK ¿ ¶D0½wÒÌÅc˜K¤\˜T{“Ëcì>Ø¡"þ΂͞Z,ˆq̬?Ù4•ÞÃrh<™ÛÝ ‘ R?ŠºQÚÔV6 ƒ„j×x²Î8Rè„8Pÿjv ±_×g.;µõ³¯GàV\o¶ámD!ÞxhyؤàB’Ðgr³b¾Ï:”’¿4ØÐìY|óʆÓ{Y]oÈíߌê&ëâeP D}ÐC ÃÃÇéƒú/½Œ«¯›RÃ0TG¯@{÷îE XQS(g[V…H‡ Nž<‰‘‘ŒŒŒàüùó¿§-\À.àPë-x¤iGZ¿6n¥=ô¶ÞŠ œÀá‹/JRè=÷,†w=?Wž/ë8 Ã4ô@RsŽÀ°EkÏÜwgÏ(®È†·«ë-½J:%;JÝA‡"AïFL‘–EÐ5GÖkÜ3<,a5”Muߣ¶ëŒ«TêZpôŠÎVˆMƒ>ÍÓÆž’€ºt. 9,«hÀawcRµ!úݳiKZS}Í‘6¨$fþ&9Ú #X™? ä/3¯sQÀC“¿ºV "ƒ:FO×fôtoBÿ·cl~ CïŽcðõ³%?È—ŸãÙR³Õš1ê¨9è¾5*ÎC™ß¨î°¬2œ°èàŸzŸ€áoÿ*76%?·v“¡vàßä¼~ÁLJw¥lÁ`CCCú²np-ûJ|TNÔ·íÓÎ[wºS§Napp½½½p¬´lïÞ½èêê2ôÎCºv |G©µi7qÒ…¿5·U$ÇGG‡xW(0|h »éã BcyôwGÉÁ±‚¸ÄñuY6`%6›*ƒ&¦@•XÙ:F™ú©á€~Æw#÷¦Šìýjüäéãìà˜!{ëØóº®ïسQ©ž÷T9ð0¥x`ݵUé+xp̱±à•IÝiÊ]ÝÁ.£Q[òeyÁ–|¿õîÛðr>ºuÛèÀ#ðHH2$ÅzònQJàJ$Œ-^Ÿ¦ëÝ|¾G†¥JSЯ}CMïkÚCé¯ËåÈbN¬V)Å\<IUÑîñê«K%À–Zù—Ó ¸áã̉QH)˜‰PÇ»tÞ&I3H’¯tº¤ ¡H)Ch›?|¬  ð±|áßI¬+£(Ѓ êYõç¡î89sS1ýÏ”À§^ù!Fzþ°ÄÛæ«’0J7Â,“쿹pÌÌ-Ë ¬lTD)rÿ5¼V”ãè=óœ©<n¸_3ìQÝÁ¢ºø8<ÃbQŽçl`@Pǹ’žm$2…Þ³ÏâTdº`ÝÜëõbïÞ½+?Õ`éuœœÄÈÈNž<‰W^y¥ ÷qtú4^X¸€Ç7ÝŽ½uÛLçW ÐCßæ;0™ÂÑéÓ†ÒŸŠL£ïâ‹Üþ‰²í_uœ œÂ ¢ˆ†¦ž°¨ã„RsX¿Ö´ï”!úïÐôƒÎ6˜‡MB 1E†¨ÊkÕÕ2äÉ3,\©Ÿ•¾\†°êZ ¬çâÖ¶›™^Ÿ]<º4 ŸF%ÚîÆذnÐÛWT¬*5¨Hîô•; óIògÍ"ZMBBJ "‹u6Ö¢oß.ôíß•„ÞÇÀ‰Ó_§s)1Hcßð¡?XO…ìsKrXÓ×6~éìâXDEëµvooÄà—ö­Â“P2ôs°£©w@üjE'ç*xD(ÒŽ­¿Åø¸³uTfÀãëãë‚Ößßßï%j?þ8>¬)»øžó̲ÊÄ$Îü@%SÙ<ýôÓUóήlll̰:ÁµÝWÚ}ÝQqpÌv·8¾.ÛÆ«à¦£r41¥9ö ¢F.B\4¸àÀ5Þ]‘m«†ÏBž=a:v¨^{ï'ÚûÏæ7ÂÛ°ñÐE¡ÊŒÍKÕúïŽ9V@S¤ì_"¯Léίõ¦ŽS+ÄþæÝ·Ñ^ãÁ­MkƒÜFV‘dËˈ„Ñ ¸PÃç¶åÍ,DŒCªš Jeò#‘¨!*Eq-졌E1:^@ Ç®.¥;X¦ž@lÎ?K96¨?ð ƒöeÒÖŠ~°aQË€0©>¶¼Ò£t„XQƒXguœ€‰[xµ~“Êp¨ç\%ñ~‚# vx1²4 Y-[ŒÜêJ7ñq<ž¸ùÃø£7^2”Ý©…ô½÷3 tß$,È,T{˜)‡®S[X –žåg^ØAÍßÇËݾû}SJOl» ÝíÊNÕ,4âY¨A\Q ª2Dª¬4aáb8¸}§Œ÷_zO^:Y6©FÈ!›µ··ãÀ8pàÂá0Nžƒ7â`ÓeÛ¯jX.†EL‘S%MÃÂÃ𺠩š½°ƒLU¨ä¯Ý´´\[ˆE1O–…‡e¸2¶CHæ¹§Laðs.ý­‘,°rX:uq4r +ê¹Ýá«E ! 7ôlÚ‚žö-ÛæhÓe׫dU±A&úû†;èË?B’?Ëæ£@ Ütdº;kÑ·? ?Œa~èz IDAT\ÃÀ‰w0tz ¡˜XÐ>GcW _~\çgJs[¾7”÷ÖÖ«;QV-Su’°Ãð·„¿6×O ’¡O}?KšÙ¥ —ÖçG/ëŸ£Ë x0”Žm Ä†@åîq›‚xöëºÓc``}}}p¬´¬¯¯O>ù$‚Á Îî®BšøŸà·þ¦óÜ2iâ…ÿ×4ìðùÏ>ú¨Ó¡ ŸŽ­››}¾ ðà /¼`XÂ^ŠLã çŸÃ#M;ÐÛz«©¼JzÞõ:ßø6BŠ1Ø©÷ܳñ}®ú²íO,aàãø @THTIˆ§ÍÀ7Ë&×AUlR&´l• ]^a`‡õåp cQKv d%½!…‡ŒsŸ 8%uéhä2ÈÒ9¨J¢"æ…z^@ ±=í[’ê Í謭ÍÜvÙ Š$Ø P˜û–\ý¤°ƒ¢RÈT…¬¨ažcÁ3$ý”•°ÌÝ¿¦ü Ö€:5©‘åŸÀ–& >¶ÁØÝ:=†þçßÄø|áT”¹×@j¶‚mí)þ>­ä6ŽöÝÚ²ºÇÈ*…([ûâá¡}üÒ>ã°ƒNå‡ÙÅø†¿…cúƒ`Ë xÂø¸þ/½™ú] Bc ê Ü#ž-`ï€2ÿºî´Ë*~¿Ž•–}ñ‹_Ä×¾ö5ÝéÔ…7€‚Ë ¹Êsâ_çLå±ÿ~<õÔSNG.  ãØ±c†Ò2µÝ`ü·–ÎsÅQrpÌv·8¾.ÛÆ«–¦SbPãÓáÃÒvŠ|íy@5vqoSÓYqÍëÀŽYaィ½Õøë±y燲ö£j¶D$ª; ëªÎ8xp̱B-¡sœ>œÐ¨ÙºÃ²™šÎC‘6.tkl½ï|ÐÃx]‰EAÛ%@²JؾéÔDe 1YFBѦ¾!©jùl~ͪ;8°Cî~VHØ!Û}¤Š ¨*YA‡§aEDL‘Wð„A Ë£Ž û¾Bc>VÀo>ˆÌflÈ H‚u­>×uïÄÈ Î/;±èð/N ð+­¸ [l÷W!ÚZÃÊÛµ<ëbQ#á)>oüÄ‘.wŽl»K×ó®]ð••¿®¾ŽÃ^´õö–Õz{{ÑÞÞî,¸5Z:ü099‰^x/¼ð¦¦¦l+3¢H8:}''pdëÝèv_Ç ïDgð¶Å~Îá[Ãm#ÿÝPú’@ï¹g1¼ë±ŠèOÃB[Æ5°?øD]…l+x@ o«,wk!B6ÀÖà ¥}]=çÒ¥V•Tx°B—ÎA]xti´ðË^lÛ–Uõ†¶-èôÕeh#š{8¨)¸Abò·u‰À²ª"*Ë*M€2Ü_JAÕűX3TfÖú£y™øL°À$ 1?d8¿ÄïÐ{çè½óF ^CÿóoâĹk…ÙN_þ>¨´n˯q¿Yr`ÛnÆ®®¨;ȪõõùÃGoÆÀá´½¦Í°C0"®Ö#íï£úÊ) Ü°ºC˾"j Þ£`Û> %tPâúöo¡Ðßßï¼L(1ûêW¿Š¿þ뿆,ëTa§*”™—Á¶Ü[ B…Áçÿ›iØ¡«« ÃÃÃN'. ƒAôööNÏm}¸¸ýÚ³Ý-Ž¯Ë¶ñª°é¨8*ÎW„SÔà)ÐØUc‰!©îPaæÀŽYa§ßÃÄ;ïk¾¾óö,ên·ãLF˜ê ý¯fà! ¤Ž9äל‘XÁ&'²“¢³.ëÎoËí·9N­@Ë=¸x<Ë .)P, ]”/¡ÃW»¶<u®l²lÄ¥‚•k) Š¡jR%êéí$a*…¤*…¿Mטô™ØÁr· µ¯/k‡2Ë$xUøÊæÝE»Ë‹°"b"¾d÷–!ù+¼0Igø8OÜüaüî«Æ‚ÒCRŸ}íG¾÷7á °+v Š,3õžyÎpZ/Ëc ë#ºža°ÕU[þ Êq|ÿœÙ·jkkÃ#<‚8jfçêövôöö¢··w|°Sõá||¿3úo8Ôz‹)µ‡ œÀ±9ìð4Åoo¾qýý8|ÑÔs"t W_Gßæ;œNXT+ÂI›ÄÎdì`úZK¯£6•kÒ7éjÔµàÄüDIŒHºtêâ(˜¥sPÊXÉaëªjC ±†f}mMS?2Y…tA ŇDEED’6*Ühèà YABQ‘dÔ¹,£©LÝŸ™Ê‹K» ?p –®>ŠÒ®ïÙ¾ Ã7~csKèþ8úêYÛû­2ù"Ô¥Qp[> ¦v{‰½c(ÈH¥(מ_¬¶zŸ€Ãw¡÷Áí«N lÃs0Íf—2GbúwïÞ]6óüØØNœÐ€B<›Áøº‹0ª9€oµîDhÛ¼ÊÔqݹ  ¯¯ÏQy(A;tèþþïÿ^w:yú¸ ÀCå5iâ_@#Låáñx0::êtÞÛÀÀ€!e"`7}¤fkaûxUÇž;÷…s‰ãë²lÀjn65ŸUá*-R`[y>ùoáj+ª‰åÙPÃæß9°ƒc¯þó3º®ß~Ï/eîK>oÕûrâô{N‡ÒhÕ <ŒØïtÇ ¶&̘_ #¾¤_>¼õ&Gá¡Rmz¸ ·6µd¼†aj\•"&Ê ÔüÆb2—åÐìñH~ïÔPãËŒ"1¸O) ËŸz< v šóˆÊ&"KÊ!öøÌêr¨ _Q›ÛÊù{ŽV'½_ ÇÈ* +ÉqYÈwËè®i@X‘”â6ÝØÚ¾O9Pÿª|gw¿ÿ¡Ýø¿Ï Þ=µ0ƒÞ_¼€¡=Ÿ¢¬íþ*D›”¤•™¿úÇNâTxÚpú¯tìKƒ—´=Ï›y8”¼¿F"Sè9ýOºNŠÖcmmmèííŜũ ¶¬ú0::Šï}ï{øÑ~d[YG§Oc$2…#[~킱f“b>VÀV¡8/µû6ßáÐ8ŽÍŸ36—\z›nD§«Þé|E±ò€#ùÖ!ì`ìZª?ÏR‡tšŸ×J\ñn³fÉ!…@çÞ„ºx’§+e4“ìnhF ¡pèiߢ¿}U¤Ô@ÕÑGˆÎ¾G ŒEü¢ªBR”Ôo5÷ýi*¥Æð ¼.^[}ÌÀvçŸþ™ `†f‘T|hTa£«:›j1øŸö£ÿW÷ ÿßìhd ÒÙoñuƒÝôñ$øPñEûoM B™†:mÛít´û0ô—÷#pcÓêý“d_3¡Þ %Í ð ˜|•Q ¹Ñ“þÙ–ýîþÕ«æuMÝr/”Ù—•‡ ²§žz GÕ¯ò Ä,Ty¨Ž±&Mü T“ОÇãÁûï¿ïtÜÛÈÈž|òIc[|¡\[Oáú¹£äà˜ínq|]¶WåMG¥`RÕªãeú'€*{>¹7­ÛUQmìÀŽYe‹Ó3ºÔ:öÜšUÉw»‡0¶¦¡*ëÍ8Mï˜cZ4dQx˜½ ÿDZÞãACÇuŽS³˜‰Úó xÍ×ÖmÞdª¬$ôð*Þž›É}O ÏÍ£ÆÅƒeÌOéç—BˆJÒ ìÀeÍ“X mÇ¡’\:°Ã:›ŠE ”“ô—ìñYz9fò EJ»áïÄæü3”c+¬@ÊvXwÏ ¸ø¤òC™¼ËØåkÎ1oYl*‰­-ëá뺱»¡Åp–Ç&ΣïÌO—FõžŠ %Yf°ÃHx OŽŸ4œþPÛ-øÚ´?RÖé®/y \}·½õßmÚÚÚðÄOà»ßý®;Àº»»qäÈ<ýôÓxàl+çTd_8ÿN.^6œÇhl³R¬h¾Üþ tBJ½çžu:\Q¬°°ƒ‹áLƒ.†Õ?q v© QU ª dªWýÐL? u¶{ ¶¥(£P ¾eì»PÎþ-Ô¹7W`‡R¶z^ÀþÖ-øò®_ÂÏ>òëXxøw1òñOcð®ûÑ·#€ž¶­*ޤrƒÈ$ê( Ä@"Ńôä•ö™¢R„E 3Ñ‚ñ"’œv ˆ±ñ‘d,ÄÓ­Éxotã¸Îx?Ë—?Læ¿üY˜.qÀ8 „3«Pv6Õbð·÷cáë‡ðå÷ Þ#Ø;>ãÎ} ÒÙ¿ºt®£–îÆZÇ­)ó?OúñÜ·l…ÚבÿïS«°ƒ„‚ÁḄ¸¨$?K[ªŒNèŸÓËxƒÒŸuƒm¼£ÝŸ–ö*öe=`›÷ÊýÉ'ŸÄØØ˜³•*A»çž{ ¥“§;cMóÚý”iØ‚ãÇ££Ã ¾+´õööNËmûu€õØ?uW士šŸÙ…˲OS§é’a4v41›‚*Ã)jðh쪱Č®¹²ÎÑv`Ǭ4ýêwæèSŽÂÃÌÅq§Siž8V ÖÒÝYü…””ùÔà•IÝymÜZ±m%Åb%qj†SZ< ÚÉ8O}=6ÝbŽô]…¦Aóo°)ů›‡‹çŒ«28·‚[`‹;,›¢&¡˜,'ï}袲IUtÞǪ¿|¼`ŸÏ ;"’©PÑR•QŽ=°CzøUÕ»Íd]8Â`—¯Å†6ÌÒŽ1PÖþíÏwÃËñ†Kûæ™·ð³§ò¯Ò Œ^rØ!“õžyÎpÚÝÞVô¶Ý¢{òs.¸®dý”ãè=û,_xÑòÛñz½èPDkooÇ‘#Gð¯ÿú¯8tè¼6¼ØŠ(¾té%ü_×Þ4œÇ±9„±(>òsn Ýô°áô'B—0põu§³Ô ¯ìÀfU¥Ç eJçË-QU0/Å0/Æ”âJqÌ‹1̈QD4ÑBÖ…–W¾ÄBØÁ‚%Sº¢RêäO¡~ð7P¯<¹\Ò³Ån3þðÆÝø‡;ïÇÅOBð‘ÿŒáü:úo¹=­[à\ù»Ã2Üg“? ˜´ýŠŠ 4ÙGôæ“uK•Ú¡4 :ÌÅâˆJòÆ^oÃ6BRTÌGˆI²qÅ3¯ ¬,3L!˜d€QXL{§öÛïÐÿ‰Û1ö_??üûO2,,øPÂA:nŠó¯=ñí' ÿ3Ôð¨m·Uïð_Ú‡¡¿¼þÚÔ»Í0)ìs‘Œ}:•t×'”ÅJrpp¡PHwº¼ê–Õfúç®ýc ‚±Ó…‡Ò´£G‚f%5rÑkùÖÁS.}ÇÜòŠ<ýôÓØ»w¯Óa l8uʘú5SÛ ÆoCìƒ8À ¼·»?8¾.Û>í4ÛZwHA¨±K J´¢C¥%(óÆ¿aý·ƒpµ•ÑȪiòYK`‡–ë;ØÁ1,NÏཟ¼¤«ßø7µek‚Nà«Þ§ ‡{3._ÕúŠs† c¶. –®èNãòi nYœœ¶DR °ƒeêÄæü³”c›ú±9ÿËnHHµé†td+Q1E†Dð„…‡åÀ§‚ûüœ žzŒÅBq Yâ@륕æöq<þ,p7þè— çù¹ŸGà܆­¥Û',kwvÈdýc'q*llýàeyÙv·öçCšµ ¾’†zNÿNE¦-¿C‡á‘GÏW½/Šmáp££É ­@ ǃÿøÇ8þ¼åe=3÷F"SøÊuûÑ.èÛ'ÊTű9¼m¦ƒÊXÀÛ†o\?_4ýô_z›nD§A¥Ç2ì×T *¥HP’ªB¢I¥‰Rð„ÀÃðhâ=+ëû&€”㆒×0<ØŒ÷JMmE,/Q%sp#¥YBBUÐÀ»“p° _š¿–Ë3ïuÔâü–¯³vÔµêoE__¦‘ËPçÞ]-é¹cËô´nEOëZà„MLù3šZC«F[Ûê,Ä‚ëMŒ?YUŒ‹Pi–þžõþ ª·¬ûlILîñ<é龜Æq­å3­÷oiþ|3Ã3ü*ÐDלœ~€ß¼}Ù…þg£ÿqÖÖ±¡†G¡žû_7ØMS»½Ì6†ößš2ÿs¨s¯Ù 8¬™¯nkÇàŸîG禴½Ò BúšM°Ì.¥=ØêXg J—QÝVðø)ñ>lÛÇ _þºÓ=zýýýèììt6]%dØ·oNœ8¡;­|ù»vü±3Þ²­,€à‰'žÀ£>êtÖÛØØ˜qP‹õ€ëüLU,ùJùyå¸ÅñuU4žÓt|¢€&¦@åHEVO™þ  ;ÀЏ7­ÛUŽHÁTœ3UËõxä«_‚Ë[㌟*7½ê;?’]ÑéOI€D÷<ÅToØ¿<8f¯ÉúOëתư89S^®Hl\HQw€­wìqúV“¢¥¡AU{>óiüâ;O›†þæ·‘$<Úµ£í[†0,Ï®~®ª€J)(¥ „€!dƒ"DX–027»z … ÜN~U”dp5ϯ~ÉU$ØA]VýÅ‚vÏ ?4”c&M›{vÐÿwûaÛÕÖß!€‹·zБ݌ÅB† ¾Î!¹êôÔcVŠ!,›9…[cÄŽJ@"¨ol 4´àÐ 7áè…÷ —~ßO¿‡ŸÝ÷¸MÝbØW…hså8°C&‹‡ðäøIÃél»;Ä­Ï¿aÐÎ{KÒ_#‘)ôœþ'„ä„¥·rÏ=÷àñÇG{{»³€6`~¿?Ùw8N3,rþüy¼ýöÛ8sæ Μ9ƒË—/Û5ä½ø¾pþ9|åºýxÛt¥ +>ˆÍaWMKQüÞ·ù ͟ʼnÐ%ÝiCJ½çžÅð®ÇœlŠˆ99†„ªäXCPH”BR«"ZùÔ±.{o,5í ‹ZÖ…%EßœÉF¿JUa‡ˆšvXóþEU‘%ø8Á؃ÈÌå—å?oßPƒï€›blº$çÀ!õ{£hîn¤äÉáh»a£àS®1’%p_VU,Ä«ÛÎÃ˶”À2 –±F€Æû·<ÿŸ™d{𡳩ƒ½ûÑÿk{ÐÿÂLÓ`›î,ñÍ­½·¦†NC ¾ 5ô6¨/ÈíÕûôaú~ëæ´ARÕAÉЇl†d%íùÇV~¤ÒÐÐÆÇÇu§cï-Õf]½ÙÆ; Lt§íïïÇàà ³+1ûÊW¾‚{ï½W¯ç F.‚ñ^ kyk`‡ÏþóøêW¿êtÒ"Xoo¯!U"`[{Ö>»œG•Sù’t‰ãë²l@§Ùr:†ÊÐÄ4@•Ь¥<»j,1#€kÞ_­-/Ažþ±;8f©QwÈ¥âô)`qJ 4Ã{rþ¨bÅúËs¬‹UUÕ ŸÏ^ФRÛÞ¡Æ™ìËÁÄpµ›j±ó“¿Š_|çiÈ sAzæ}\\ZÄïÝ´ Mc}€I ¬†oC×Bláa‡•݈ ˆ" Öl^MÀ5Ë@:`hõÔlTw(Ø:°ƒþ¿W ì°R0Iª®ˆ’}eä°ÉD‹Y‚÷ä8$ªbsJºm‡·o„&Q8êZ}Öêډѥ^™1ö2%$%ðÙ×~„á{þ˜O·¯JqäÀÙ¬÷Ìs†Ó>ÐpöÖm…‘ÔR…†æÎ¢÷ܳ–ÂmmmxüñÇyûæóùV@†ôßn·n·[s>Á`CCCÆðð°¡ »,¢H8|ñE<±ånh¸AWÚY)† q […âÈíxo|!Eÿ¸8º„¡¹³8Øt£ÓÑ ØŒ”¼\»\;Ù©”bZŠÂÃðö)=¬›ö=,†,ÊqMS/OXøyWEÁÒ6X‚*ˆÈÚ× QE‚‡åÖ)V v¨âëHv_û9ýÀæVEUƒï€™yª¸ˆRz}¿¿e zZ’ =­[õ­•(Ô”‚ƒVÿë…t·3ÍQ†‘>“vPTZ|Ø!í³Å¸ˆfŸ–ÂDÃý[™?4ä¿üÿ 4¨@3Åz¡ÙΦZ ~v?úÚƒÞ8g®Ù:–Ôð(Ôð(”kσmíI‚¬§6‚ö!%ô¶nµÓsX&U‡€ ¢l0 ;€“ÁXö¿W¨Uw`ïp ‡¬;·ù!HcƒºÓ9*¥i{÷îEWW—¡Ã6ª<8‘˜ PAÙðìÚ¿O=õ”ãÏ"=³Œ¨žñl·ùãΣʩx ºÅñuÙ6žÓtùBÕŠVu*-A™ÝpzÖ;W[þ~ç M>kXå"ÝØÁ±t;ñ·ÿ¨ëúŽÛoÍùwO]mÕûtqzVÿZšÏý}=Q¥R¬ê)+2q€Çì}€.^±-ïDXÛ¬¥{•S•âЩ™Ô`öüeÝymÙ¨è>Ã{<%}‚× 1¢­ïÉ¢U–ákk]Qz0 =üôê\\ZÄŸÜv®¯«·½¾aYÂèâ"vÔ7Ø¿yÌXO“ÐÏ'®Þ‡IeËÂÃòˆå ´YˆÞ^ãEÝú“,Ø!ÿßÕä†;ùï´‹Ušì4CYŽˆ $³fµÿ,ÿf˜âÔ¯a‡•]>p, +ö•‘Á¢Š”vX™³‹²ˆ:N€Ðé©ÇXÌÈÉB¾=p/Ìj¥žØõaüμˆ©xÔØj?8ƒž—ÿ'†÷þ&üq bÎËÀ¢ÚÐìYœ^2”¶MðâñÍÆUÀ¶ºJïeÂàÔÛøì¹ç,Íóá‡Foo¯fE‚J6ŸÏ·Û½ò{ùßgî5Á2ä044„cÇŽ•¼þâÊ`$2…#[ïÖ•n4¶?ë‚ ~Ï~ÎÁíŸÀ§>xÆPú¾‹/¢§þ:ø97Ó±Q%ݰÃê–"(ÇÑÂÛð…@–e‹‹aÑ,Ô ªHˆ)2Ô ÷Ɔ‡‡åô- hH Å¢¤/Sä4• a¢q1e)Ô¶ZÖ'Ôà;f^A\\„ZóÃîú懃[ºô­»U(Œ¾öÐÒ6Ä€"C‘a‹bvØ!k9öÀÉW²ª¤©Ÿ¢ð0‚éÏ (Î,ä†ÿ÷O`øÌ5ôÿðMÛÁ*ÎCžø>äkσõß¶õ>Ï–òÙÌj¹5%%ô6èÒ(”Ði@)¼‚qG»‡ïÂÁýkï}ŠK( ì +*fãi_ëÌщEÝõ,õàñ±±1C£Ä³Œ·Ûy!TJõ^V®ÛÆÛ5¢?@¾¯¯CCCÎf¬ÄlppФÊC§ãD8óW5ˆÓÕÕ…ááaÇŸE°`0ˆþþ~Ãé¹m¿î<¦œŠ—[_—mã9M§Ù)•®ê°\uåÚó†ƒü‰{غ]eï5~ òôqvpÌr›8ýÎÿüMÍ××øëѹgwÖ¿ón7–­z¿Qx LY†ý­Èij×d}/æ·vj¾vfTÛé .ßêiµR,^7df/\‚,ê_\´îøPEw³21µä>TYÎø¹VàA‘ÄTŸ‹ÁU[k)ôpqi ÿë¿¿„¾]·â¾ÍÛÀ2Œ-B0Øáo0¾‰´DP)ŽK[ë¾bÁ}­î\Ž,B¥4g<âÍS³ª a¥¿¬¨‹™ôVó«j²í©š2е€ƒ‘üUšá"¨Jæô„$˜ÔaL€5¤²a‡• (ê*dRˆ•¯ÆÓÝÃJx€Nw=&ÄUY×¼gÔWd‘­—V²ðq<þ,p7¿ñ’®‰ÓíTp}ïþ ƒ»~ˆ²ÅmwKÊ(P„dª;å¸)u‡#[ïJ^ë÷±åá¶{S¬Ó_VÃmmm8räʆ‡3¶oØþo«mhhƒƒƒe9¬·/ ¬Š8²ån]ñ9¼màSð{>Øt#jÜŽcóçx_u IDATt§O„Ðù$®¿ßygaÃZ$›é[X³l! 𲼬™ªkö0,!ëÔ4NÜ…>U™$Áj`Ý)-ÔÅ€,lDz…l°tÐ!^Äù žw%á†Í]èiÙŠNo]þ~°¬Þ@3¨7mbhS©ãsjlÌ„dUEBQ³ç¡EAëýè€DE]Š#X™¿^ÿ,0À’àCëÆöëùÐ& ÿ—1ôÖ8úžþŒÏ…í|J ÊÜkPæ^ñlÛt'ÿ- Bci.ò¸_ B]:5t4v¥xsšO@ßoÝŒ¾GwÁ_›¶^VuHïvÂúàd0–v€pLÿúªÔ££lóþ}yRªF š5Ûö1¨þ›î¬Ž;†ááaôôô8²2ó*G'Z;ŒŽŽ:þ,’õöö" J˶ö€©Ýî<®4-Öhn§?8}Ú1“¤ T–«®Ì¿*Î˃À·~¬ì]¡†ÏBž=aI^]wÞŽþ=vplÅ^ýg}±í¼_ο§ÇôV³-NëXWõîèŽÙ·žˆÍ;NHY6àA¯¹ëëP¿m‹ãИ"› F‰-$¡49ž€«6yb²•ÐCT–ñç#¿ÀéùyüþÍ·@`í˜ÎW«'cIdôP(Ø!=YNÊó\î t½÷¡Ñ\,‹_=&c‘ J.–CƒËµQÕ¡ öºkÿna$“ª®B JÜP°h©<嬀iõ_† X&ùCˆùrLׂ”P7I‚G’lcëæeAn‰u`ËoF–¦¬é+yû:‰r ÞU¿t×úñûÚ¿|÷ ÃÙ½ø`ð¦SÄv/—Ehy–Ñ?v!ƒÁ´7_›á>¼U¨+)õž}G§O[V|5©:,C Ë?~¿ßÖò–Õúûû1>>^Ö¾{eq}⋸þ~ÍÐCX‘0–¡ÛÝP”{Üþ t¾ñm„ýsÇ7¯¾Ž¾Íw ÓUÇ ±½Å‹.ï/-€dªBRUˆT¥¢ª@¡@\‘ÁÇ¢–VnrÀ+ÀU©ÁFò'ó ¦ÚU ~c[¼­÷t`ð•³èú…ýà»yâûÀÄ÷ÁÔߦ¶ŒÿÖâÃ9šup áQ¨áÒÎ<ôàvôa:7ùÖÖa’áu}¸ )M†¿OÌEô=·ÊÜ–÷Wºuƒm¼£J_•È £Ö¸ªP;ïÍÿL/¿¬„ºººð•¯|íí핳yOË?ÅR¬¬E‡l–„~Œë?ªzø 6‡»¸Íö¥v°éF<Ô¸ÇæÏéN{"t Csgq°éF8–ßxÂ@Ú°–Ð.îU9ÄÌrݶ‰»ˆ°ÃŠï¡è®KÞõ£;XØÏì[ˆÅ— Úý:jêppSz;oJBùª­jTpÐÛ&Vzó*ì ¨4³ŽFØAT\‹FWóÐØ!I„›ãPÃqúÆI!`ÄM°Cºÿ' 0G€Í*àY{¿F@ÿÁ=è{`W|øQáÀ ©ü Ä®@™>`|Ý 5[’ DÍV…êÉtµL*Î'á†Ø••ÿ—¢í¿­ý¿³={6­D& ÜRƒUŠ˜\ˆê{vU€ J—]Ý¡š£ÃhÉemF塯¯###Φ¬ÄìË_þ2>¬¿ E.T­Êƒ•°CGG‡Ó ‹d½½½†@aü·€ñßêÀ ŽÈ%Ž¯Ë¶¦3嘊SuÐPmeþuPqÖàB]×ÜS¾þQEHÓÇAã×,Éî¶_;€ý¿óÛÎpsl½÷“—°8­}ŒÕojË«î஫u›2=¾†¯nP¤š‡ 3\l6¦Ïiγµ»³¨nb!–Ù —tç㮯ƒ¯­B…)<°®Õ —Ï[‘CA GÖ€õÐCT–ñÔïãÕé)üMª=ä®KFæf“ÐC6¥»a‡ôk¨ $ÄUµ‡ô a­þÒU—|å˜ÉÃl›(t­zCVå†|å—!ì°þ’ü!L²ß1LvèÆDù% ;¬LâL ±ß\ ‹°Æ¢$UŸ¦ÄÁ;¼Mx'ŠáÐ%„ýûо‹/:ÀƒÖ÷œ sRlÝBÛdÇõœËüMT!ì¬n˜ˆ‚o.¾’†Êíºòÿºžw¡÷º«J@æÛeED5Cµ AVÂÔš9%W>6ÁP•ü×eTPC°Ãòuó‰8j8_öú[ ,wJW’õu³,†ÏÏH:³°Ãò¿e—€°M\Ø>|jzïÝŽþüGOž+ÊøVãÀ:uâÙÂz@j¶¬'ùoÏV y¥ê Ç’Pƒ’ü]vèÁíè}pûFÐAE` ghwbÁgó‰‹ &ƒ±ª‚ Aålã Bc^’”¢Ñ’ÏžÛüÄs_×îÔ©S4u¢ºcÖ[__ž|òIGåAk?øšiØ‚ãÇ;°CmhhÇŽ3–˜õ€ïøL•>¦œèí¸ÄñsÙ6 ÓtÖ8F•“ªJ¬ªªN³P^7¾FoÞ0ByºI^‚<ýcPqÎ’ü>Ö÷EìüÈ>gè9¶Æ‘(NüÝ?êJxðcy¯ñ8À`æ‚þwA„s€Ç³çÁº _¦¹~“¶‘DXû)þ®Úâb&àá¼þÓé®»„e*®Ÿ0«PÅ|ô§’C}ƒxÝùIñ8TYír}m­øåßÿ"~ñ§žž¶¤þïÌÏãþýe|ºk;ݸ«wš×¼–$ŒÌÏbG}|<¯óaì°¦áRèËàƒàÃ2·‰J“AýË€C¾qUM°Ã†ö§€(§VU ù—<ìä†<¬,OXÍù'T5ëÜšyšùÌJQ}cËD=È"Z/¥¾ 'ðq<¾q{>}òß‘¹ó¹×,€ õòÐòšQu/Ëãȶ»Ím ƒfÞStY ;<ñÄ8pà@ùnÐ9ÍÍÍ+ÇË Ñßßo~ó›UµŸÕ =ÌJ1ÌJ1{ÆTëtÕ£ÿº{qøâ‹ºÓŽ'B¸ú:ú6ßá¼ÄÈ·'çÜ+b*(W;ìàbX´ó>0fÉ*…€d@pX! TûæÊÇ @ >°ô:j“ï© mEuÕ×_b'=´© ½×íÄÁM]¹}N  Â Vú …ÀYLVUÃóOT–3«Ch4IU!«*¸Lï]I2°Î SA1b1ý½#|<¿òÃb¨0 bió€’eðá" ÔØ”Ò×)͵üÝýèÿõ=IðáåsEû4v%Y›ð(*Ùê}îï@ÿö s“o}‡f ° cŒv€É`T»J…Øàà ¡tLÃ~ÑSŠFË&kâÙ ¶áÃPÞж¿¿ßJЕm&~ð5Óx„<ýôÓØ»w¯ÓñŠdccc¦æ!nÓÇÖSEs"¸ çÇ×eÛ€NÓYæ*AÅ…òVu0Ø”éŸ.’©éSÓYžîç M> ¨Ö¨j:°ƒcÙìÕþíqº-×w å†Ü€2òð²D$¢îâ=Uí3Æé6ŽÙf’~j´E£ÃÄÈ»šóܸ¹¨nk³.Aõ/8š?´œËUqÝ„÷¬NÂ. N­•âqó C5ûJÚÓÐ`p8dœÛ…=Ÿù4ü×m³Ô¯OŸ?‡ß~?Ÿ¾ EÕ «/x? =Ì ,é Úµ"ø5×}* IÞ¨B gÃd¨P.°ƒB“§ô‹“Š¢”üÌr·Kúße%é¿ô¾gE©³â4 ‚Lvxáf8ícËl=T²”/R_Ô'¡‡ýðr¼©º|îµãø‡Ð[¥½šw`‡¼68yãqcнm·ÂÇš;q¤™+>ìÐéeK`¯×‹¿û»¿+KØÁívcëÖ­øð‡?Œ½{÷bÇŽhnn. Øaxx@ ê`‡e[†Fã š®ÿ 6™G~§oóØím5”¶ÿÒËÊq8–ÛlsÕÁÃð9';†xu¬ í‚®z¸“ 5À9«ŸG­aý¼Êrðä‚ÿ-haÛÃt]¨uù„ PÛRôþÔQS‡oìÚ…Cw~2 ;dªŽÊrê§P°,ÎK/ì@ 2 3~¦P-ý)ó51E6=¾%šA¶#uB„TJ1¾´¸v’ï×&£QŒ†B¸¬½FW™Ô°ÿs~– vHÿw À¸Êdó—Á‡‹ßx‡îÝî,,ìœçÚ}øFß]ûÁ£üÒ¾µ°0G€³i°C¶6-4ìfs‘’^›Ø±ï:qâ„þ)Õ³Œ¯ Õ)FÓ~Ê'k`Û>f(Ýøø8úûûI®Ä¬¯¯~¿ßPZéÚ«ÂGVÂ>ú¨ÓéŠh½½½…Œ½Ëf|Ý`[{œçS5š-.¡Ž¯ ÞxÔ&¥:PT9 õ'fËv0Ù”ù×AÅYc‰\sy>›ÔðYË`—·Ÿü?ÿÈËh3ÆñÖ_ЕFK_r;°ÃŠMœ~_ÿþ(ÏÁRD•*Úgðà˜}ë’%ýrÎ.ŸWÓu‹×f4?˜Ó-“Ú‚Ý&­+sò}ý'Gq.ZnÜ–ç+²¯ÔoÙ O}ÜþúŠb8;‘· =lºe—¥eNÇbøã×~ŽÃ¯¾Œ±p(ø@òÍgø»¬R¼1;ÉX4ÿF„mÊÔÄßWÊA |Hî+ÊÆ|òÝGîå=*™Ê1“G¦¿-+6Hr ‰&VE] ‰Ø Tøß—ÁÑ8tSê.cTÈ_FM–€8Ž0Øám*l=dY{ïݵ~™×Ô¿5°Sû`ŸÒ6Ø[¶w®½/¥°D«œAÓ^ÎË ³.ëÎgÓ­É@xV¨LàÁ×Ú‚Æ®_£ö`¸ï©jÉÔ+¶°ì$‹"TYÎyýMŸüUl¿ÿW,¿wæçñ¹áŸâk§ÞÄ\< ºaç@L+|°°€ÑÅ\§z˜ Ü×Ü2”£.÷‹«Aý¦îƒ”²EpÈ©ÞP¬#« 9¥6"+:s¯ ØÁBcÉ4¸®^³Vêy·5°ƒ¿9°Cñ¶e¬žwáж¸xÿçת9¤WcrPIÆSéíPcP(ÅB"`<ޏ,'{ÓÒPJ!) "’„`"ŽÙX ‰ ï4©!Kß+ØAÈü}Îò¦ÆŽÀ1`ÒÓþéáYM÷Ÿë3‰*ÕQóÀ EÁåHXc™%;¤ÿ{žY2)>´Ôbð‹ûqñ›âÐ¾í¨¯)¿ ƒR°e5‡‹ßCq?î[÷¥²ŒUÐa!K›—ìB16½”§_V–áèÑ£ºÓ¡lãUÒÓËWÍ!›±mX·ît¡P}}}ÎäWbÖ×בï7© yòß*Ö/ÒÄ¿@Ít>_ÿúר¡žUff¸M+ô¹äDoÛ÷Luü\œþLæ+‡¹CM€F/ƒŠóå¡ê`GPP¦j89óÿ³÷îÑqU÷ýèç¼æ¡I#ëiYFcKvcl0˜Ä`%L’Ô`Jš4?†®_~¿¦%]·áÞ.b]VB›^Múkû )rÚ4Ï“Ð$$!H‚I_ØØ²=²e½3Ò¼Îûþ1’=#ÍhÎ9sÎÌ™Ñþ¬¥åñ™½¿{ïïÞûìÇìÏþx·‚®ð—VRHÓC¿7Å\UC~ùQM‡Ó Ö&Þyù׸|ò´®8ZÔh†!„‡h=æC3Ïxxia†Bx °f¾×[CU“6Yû‘Á“¦Û´ ËÕ¦Ï_4dgýÎä¡Öé"+¤o›¼ÈBú™B4š3ÎÆ›nÄ{Üa‰šÇË—Gèß<}³Wˆ&¬^Œ?`pvjå \@a9Rþó .*?èQ4°3ÙaI¹!•ÜàÓ‰ªª=ÓÔ(‹ígIÇ2uJ_|UMú?!¤“m²^Zbd‡ÕHflb58*@§Z>±¥ÐìÌ}xØï®†—qÖWQÒÛé]Í~<°y{Þ¦ +=عm•9BR½—Ý:r f3:¼y硎-ÞBx0:a ÙáÀèííµ%i .— íííØ·o¶mÛ†ºº:ÛæuppÆol+cÄ _ADÎ}+ÆéøLÑòÙ»éPm@U ,óè¹ø*©èUççjq’-ô€MÙ»ŒNšÁ:ÎZ‡>ÎçB­Ã…:GŪ¤W]éh½Mß&d^‘‘Ì Ì Lñ±äg1Žˆ$€Wäüë ÄÈÕœ½g/‚ðúv€¿"áYIQrРƠ¹¬9lIŠ‚ÙDb*!‡-EUæyÌ ÂÕz0Bv LŸ«$Ð4eˆìk¼/R*Ö9\YƒWplžjêJe}Ž—å”:Gi‘R?ÏÒÀ» ʼ߱D|~íãxìàõh­³÷ÜÞhmòâá?¾oêBð?’jþõËü&¦€³0‹ì䛑BQ QÎݶÊFÒ5kìPújY_óŽu†::„`0H^†6Ã'>ñ Cñä™×ËÒÒøOL!;<ôÐC„äctuu!6vIímÓÐIÆ¥r¦-Sr (L%Ú½=wGº)%©ê»Uá×àû!e5{ª0ml.ÎzÁ®»¥´š‘"@JäŒ)æê7µâ“÷$!;déVwØ~ûmðÔør†s²CFNœÒ?Çv®íýSBx°[…åq£¢…‡úv¿¦pá1í̦ª¦†¢úAXFxÔÿ’rUWÁÛ˜,‡‡à VBâ­¹‘Uˆä&<Ñ(*êjqm×GáÖ0Ћ¨$á߆Îâ+ć(T(Æ×¹Ë¾ñÞšžL¿¹.'Á„õ¶ª“  .’–”x¤$Y@É–˜MÈŠ HKÄ)© ç¯*7ˆRò{U]ÝW*elÍ­ù{*sYL³Ÿê/ÊBûËÒ1_UÉ6bÖ6V’ÊJaw­hŠÂf·nš[Q7Ía£«j•ƒnWÛ KÑØæYWX_ ØäÁª<°y;¬Ï3!'é¡`ÊDÝA ž9‚°¤3ÒÃp4î4%u\EQü5@çñoçäðÈ#ØzNÙÔÔ„ŽŽìÝ»---`YÖÖù]";=z”,²`FŠãÿ8ÿ‹œ¤‡„"M-ÁǺÐÝlìðÓ¡ÉãDåÁN Ö@šy¤ÇP44Mƒ¡hóÒ)²ƒ QIÄCXH &‰¢"'ÕT¢¢ &I <&1Ì‹rJ4XV¾PBG!O¾’·‡zÏ>û,i\EFOOñ½BÆ Öÿ§d\*סšø¹ÄÛ³jãö@üœyÎO„Ù5ö~ÈLü2”ð1Ãñ™ºN€.•IU˜0ò¨‚9mµìx/~åQ8=¤»dÅëßþ„X\sxÎåÄ–÷ߤ),Qw¸ŠùI#êä²ôµLx°åîS&„,\Ö¥a‹_S¸©!íU·q÷ö¢º!Uá!1AdZÿä³~ë–+ŸY—«Ìñ—ËÃÛ-’YÈq8*”|0¶}ø.4]·Ã’¼¤Þ=‰Y> eù¯# YÆ[Ó“‰F´´,Œ¬¶¡ªIµI„Te1I,e@V³“Ì„¢.’ätRCBXIlå•7.‡3ì IDATìk"]X­º°ÆUVû^V’õ)ÉËR(A²ƒœ…XcñfMQØè¬Ä&·-ÎJ´8+ÑêªÆFWåê·ú.ƒ—qÀïª.Œ¯Rë:Ì®èƒ_ܾÇZÒCÊ©,!)§GŒ©;¬Û†&‡'ï<°:³¤U}eœyÑÙ#v&;°, ¿ß½{÷bÛ¶mðù|(ôõõ¡³³Óðmmk ø¾t1·Æ¿€„"%=×ÜŠVgµ±¸Då!Ë\P-|’…èJˆì k¢,ÈS‘ÃÅe 3|QIH?8ŸÃ^B–ã‰ëÀ¾êWˆ·g#: IrÐÓ•)}%‡ºÃ¼ h';PÙös„ÌÊšºëÆ>dPEQW•(ýý»ÞéF5çÈÎÁ0p±,j\l¨ðÀËrÃUp,œ,“W™–ÀP”ḲºZ;ÌA`Ê‹$a’ºC¦Ï㋊ã‹ÄuÙ^‚ öoEÿcÁÛýGx`ÿTW”ÎA3±«}]’äз ÉHª9ŒPÀ)  æ¨k›’" ¡¨°z-3•£ê̺=ã.³_¾jÙ+Ò ¶ñ€¡¨èïï'K5›á0O=\6>PBG!^ü×¼í²ƒ=088ˆÇÜp|vý‡@9Ö•X©É5õ…q ñsq*T]Éø9Í´51 %~PÅ5ð~È5Ùà!OþÊø¼jhWé\° DÎ@ý@L±·ýöÛÙ 'FŽ¿ƒÁ¿¤+ή?¼œ+÷A|Æá „‡ÌOè'QwÈ »€_X@EmmÆï„h²”~(jýΨllÀùW-)ßñá߆Îâö -øÔ–­Øà©£j84œcÑ24Fˆ°­º,M²‘×á}]62=S“$(HÞìŸ~@©e¢P´¶tT5ýV|5…H¡W©B·?©<â’ïMû^U•8dÀÁ‚¢™Ò<ĽŒ´Qhp ŽÑÊÝÍÜöýîjL‹ñœ7u› *ÌB­Ó~üÿâöämÜ/ çeûÓoþ¸ x°nÀÓÜp$ÊZñ¥ ¿6Ôæ< ‡ƒuÛLÉCë.Š¿ºNý;ŽF'óJÒ®d—Ë…––455Ù^Éa9úúúðàƒ’Å„NâÉ‘×ñHKvÙcIU0”˜ÃŽŠú¢ä±çš}xðìêŽwhò8z®¹~ƒ„‰ò!;Ø·Œ…$;¸ŸÒXöⲄ¨$@QÕ¼ÒJ"$UAçEQËaÙ¡£Ò|%ÖǶîE÷æëÓIêbžäEPÐUζ⒤¨ÃVL’PåphŒ£³íì°„ ŽE\–¡¨Š!­s¹Påp "]Ý?£( †GÑW÷¦r<—'àª}'ÃÀͱˆ/ÛóËO}¡DÉ©Ÿçh`À:X§8 þZôýùþäœuà ¿9ŒŽ £\Qíu s÷ztÝÖŠÎÝëá_¿Š<½ B£ÔÕw–Òó &)åŒÌD ô…ÒE>êLãeâµ$M› ¦îVÈÓ¿†*ÌéŽ É’ÍFxöÙgqèÐ!H’¾ËÔÄXy8@˜ƒxé{y›!d{  ¡««Ëp|ÚÛ¦¡“ŒIdˆ&~.‡ $ÕV4ǨR*?¹òœÊ~]ʳG J Æ–®¬Œï†’iaòìëçO˜foïŸÜ‹½Ÿ¸—t]‚UÁGcøÙÿ÷¿tÅ©ßÔ ÿõ»4…%d‡tŒ?¥¡¢fÍû%M‡ÀôùÍìî8UõpzµÝV«•8àôT¤ÙÌu³¾ÙHUw€ñSçtÛð64ÀUŠá׋ðÈeËòöòå¼|y×­«E—nmj›m˜Ð¸™æãxcŠÇ¶êÔ¹Üúl˜Av0eQ–åÈÔƒ *(ù¥c9ÙÁ¢¸+¾§,¶Ÿ!KÉ ”uöU/Œ p ,ýåÖìÍEY©.RˆMCöW÷ëoÞš‡´t¨¦@‡ê©0—Nzðç[;0´¹H~7¬úÍŸãèÖ)ô¶ýA’ô`y²ƒV¼FßÄqCq?×|#¼Œ97›úÌ`þëôW÷ù_b |1¯$íHvp¹\ðûýhjj*ɹ)!;ÇK¡óhw×à`mv"Ò´GHJ˜Óçt"а½£G ‘Œz.¾Š¾-H*™l\ƵEvTó¿:ÑA'xEƜȣ†sf'=˜Lv°Ry!<в=[o¿¢je–n–³ ÙAUUD—_bAÏWB’PÉqWëÎj²ƒÞvlðESð¹8ÌÅùœËôlÏXš†ÏáÌ.Ѧ(T:98¹|”2Ûop»q)Iöcþñ:8MöMyVH²C&âC€&È2m tnE s+B1‡ß âð‘a¼ðfé“öw4¡ë6?:¯oÊ¬Þ ÀLSI‡Ô6¡•ì€U¾7ë™®8Éü'DãsqmqÊFÕèê%x[v7`Jð@Û|Ä`ŸîxÃÃÃèéé1Ü–¬Áûßÿ~ èl· ¤ñŸ€múpé\˜ÿîWó¾yº««‹l‚îîn œk1n°þ?]»cž‰ŸË¡òHÕ×1Š•Ÿ€*ÇÉ+35ñËPÂÇ Çgê:º$âäÏM%ÅÞÙýYl¿ý6Ò… râ¿}‘™Y]qvý¡öK¼µëˆ“S0?©_áæˆÂ9AM`þ$cö¬î8-»·k §KÝa‹¿¨~ࣱ+ŸÇO A2@¸X¿sÇ•Ï")•R‚7§ k½qÏ m1‡Ã“Nðcñêi“o‡›÷ߊ𥠿þ[KÕ,ŽÏÎàøì Ünü‘3îhiA5çÆÐ‚FRœÍ Î龪ö`Æ¢(_‚€¦²P&3,.‹¦<â—ÁBýúXéPKíK’Y8`,8œ®Z`Pl»Ù¡·]4 ¿«Cñ¹Âekñæ ú®¾Ï½,‡§oèÄçן7éá™3o#$ðè{ïG,&=²ƒVL‹qüÍ¥ß"*ëÃÜU³Ù´¼Ôqî‚ú«oâž=’W’v#;”:ÑHÊÒ²C~øû±ß¡‰ó`_ÕÆ¬aNÇg°·rCQò×»é|àÄ·uÇ#*Kã!;سŒk‡ì Bż$€—%sý»NR,Hª2‘, ;˜…]Uõè½¶µ-«7‘æøe„#* Ë ** U²ƒ[y¶giëÜ.„y!³"†ª€ ‹ ŽESy¤™½¿8½^ŒÆ¢——+‹}š¢PªäQŽd‡ÔÏq€Szðenc>lEàƒ[Š è?9†ÃoÑb ÃS[Ï.ª=I‡ä_:¶ÖæîƒK$‡ –…¥ÔÜu¬õ_³žéŠs5ÿÁÉmqÊy©;Ô•ê!BrȺjhO”¨þKÓz{{ÑÝÝ ŸÏGè6ÁO<[o½UwÖ –¢ æ¯Áèº/ü2¯<Û‰ìÀ²,ZZZà÷ûQÊDgg'Y@˜€'/¿Ž^‡í®Ì¢ EF…<ÐY} öW_cH]¥oâz®¹uÖjÈ%?˜–cYÔ¢Úåó"Uk> 2² '-ÃÉ0ÂØ‹ìPÍ:ѽézôl½eåÁoUGÞô’Ÿ­¨$¥Ü7BvÈð¨Èp°´övgÈ'íåÙ^šÂ:·qIBT” (jþö3´µµáÜ9}UZ„9ÀQSreN?™ÌhkkÃÐÐi@6@0D 0Ÿ®¾´o§MJCNoÆ%ÄÏ%[y¤êìå…‡š˜„ªðk®èZ Ï1<ß X/ß ¶o}Jä ¤éÓìÕojÅÝŸ%dMà£1¼ôÔÿÒ§ÂWí ýR†Ššjâèe9qJÿþAEî5#UlÒ\v ša„ÌŸÍêß¨ÙØ¬)œ…‡†e$ EV æ!…ì˜ tyB·õ×íëºzËMe1‘€"I Ùä+XJðÚÙ.©=D&&1üúo!D£–çùõ‰q¼>1ËâŽqGËFl­ôQõ #’rUí¡½ÊWê¡ ­‹)BvЙBvÐÿ=e±ýÕV² ’j, Ûý²+JI5 Ûo€è÷[{…oÍ[Ö¯òÐ;zÝÍ{àcך4j‘È– «jQ‡ò¢¤gŠºƒjÌ&¥±>r„‹Hbzæ yæoAàdÜ&—53:×mD5ëÔ=7Ø_Û‚¾]wÁï®ÊÝW )˜¯ì²ª"¶¤Ði¶âeRÝP÷?Jk˜•ýÎͱps,$EAB’!ÈÊJÕö9†GÓ`h –³\ÍÁäüg³åå8x÷y#R’ø * â’'Ë ‚eQër™Fvˆ‹2Q†(+PRòÉ24X†ÇÐpqtººE±È˟Ϙ§“ŸT JœÙ› ¿¡ÆJnß’œ÷Æ žŸÁà…Y'0x~ÁɈ©Dˆjíëàoª„¿É‹Ž-µèh¯…½W{?“D(`Žb«øÈ,²…ª8dz–®xœ\ÐNv(D^êJ¤”„ä ´· LÍçÞÒ÷СCäRá¾ûîÓO>©;ž8ùKp-÷•TY…sÿU˜ÉËFSS!;Ø@Àøž!ãçÿÓòƒJ„ä@*T½£*P…Y¨bˆ¼2³e3~Jø˜áøL]'@;ì[@E€4û:”ÈÓL¶ìx/>ú¥¿„ÓSAº7& |ã[ˆÎê{uüáà\Ú~C¤ž¢L˜Š©óÃú÷¸’ÿÍÖ”ÁŽl†R—1Bv€ªÆ:MáFÞ~Gû¾{{ÚÿS¬†‰]Íóà;†lÔ½gKÚÿd"’ÓïÑXÉä•_X€»&ɺKÜ8ò66`ËÄÉÃ?.X¾£’„Ã.àð… hp»ñ¾¦¦+äZe’·ÖkX0Móq„¦y´x¼ð{«´/ªÙAgÙAÿ÷Ö“(-mL”‰8ÈD *Æ&…(’\ø¥áeð»ªL„ _ž¥Z§ôp44…Îß|ÏÝpv«Í&”…ôàDd EBßÄqCñÍTw€:Î]0uŸÿ%ŽF' çµ­­ÍdŸÏ‡mÛ¶Áå*ƒßy8—˜Ã×Ç~‡GZnÉø½¤*JÌa›»¶ðõmPå!,óè=²ÆTÊì`£4MIO57›’æE E2ß·«„STqY‚û ¡O5?Í“ÝþëñøÐëš‹øô{;ѽùzmíÁFddÙ4[ÆëY5è—UÚr^J™ž­ÞïXš†×y•4() 59¾ªjæ¸ÜÒ:šŒbƒÖ¶®“ì°üY*ùA“ÏRþ¯ª*Ä ê Ma(𒌅xŠŠÄ2[’¢@RT$D`\“Ãø æ3IS‹ÿiPÊEòÃ*õäó8ÐyÝztî\Ÿn‹‚‘äA{ÁÉ«ŸW󽿱ò ‘a‰à5¿j–ÏÊâ_ŒÂFnrBÚ¿:ÈÙüi#²ƒ$+ÅWSf(ouBtÈLãçO~'îééA?YœÛ_ùÊWð·û·$IW…ù‰)M6«ëáôzŠæ‡ÔòŒŸ:§;¾«º õ[¯¢îPvà#Q¸kjt«;,ÇØ±E+Ãd<žF~ØU[‹÷55a_c3(•­Ò«.œ$UA02ñx í•Õ¨sæØ#dy(Ù|¯÷{Md‡+vT@ZIh·B›ºY¡ 1™Ÿ‚N*ŒD¢òJÇï®Æ¸ÕwÈ̬²‹J’>ðëïã•ýŒÝhN°0TBvеA›CHâñ³¹ó˜ô+2™­îࢸh¶ þê›8†C“Ç çµ­­ ½½½Å],³,¶mÛ†ºº:” º»»qôèQ2·/…ΣÃÓ˜µÏŽ QøÕÆú`ž0ªòÐ7y| Ê‘ì !M«Ò3‘ì@iLÇT¥US¸˜,jŸ‡š¬Ä (2Ü`a Ùaù;©ý}ÆçqèòÉUM쪪GßλÐQU¿Ì¶‰mÛB²$“˜NvX:ÐO3l•.Ù!&IPT¼"C QQàdTqptòÆh iæ§ŽPTûYÞqQFB” JŠN[Yò¾6!ÊHˆ2<.¦€Šªþ¸S)ä€Z¨PFû»ÀßèM',hUHÐ{ _ "©à¡€¨»ÙÈX-̲ïŠMvÈön[ü~d6¶º@û÷%€òTw $Ӧ͎u`ênƒ<ñsÝqÐ×ׇ@ @ç6ÁŽ;088¨³Í‹P¢AÐ¿í»»8ò(soæeÎívãÔ©Shmm% ÆèïïÇã?n8>ÓÐ ºrKé=¥ Bp •Gª®´£*Pù ¨RtMÛäÉ_Š`p~] fÝ-¶-›9iöuÃåË„;»?‹í·ßFº;fDffñÒÓÿ +çrbÏÁ»uÅ©ðu‡å˜:Ô‡vVÇ ×jÁÇÇÇIõ[0W2p¨©¾ÝYÈ=€OÕÞÑ—«;/@Y¼µcüÔ$AÿädýÎëÒ;*GÄX´­ [ÙÉ•†Vw€ÈÄ$fÏ_°…ï'ãqübd¿õîøÏáÿùýøþÅw1ÊÏC¦e¨«üXŸ%œ˜›ÁàìBŸm9’ÿbÍ¢‚6ʉì@Yl?C:–’(ûÒV» À‹É?-ï'37.d%3ÙAEyl˜e(Ã6Omñò#P ¢écþé¡Í[·ù°ÈãßÇsá·­È&©.L qŒðÉ[B6kì¶3³Õ|La ‚‰0º/üÒp|ǃ'žx^oñn)hiiÁÞ½{ËŠìpøða<óÌ3¤sZˆ¯¿…¡Ä\ÖïOÇgŠ’¯%•½æÃè›<¶jŽì]F“Lœ@Iª‚ˆ$­ Š ëÈ+íöí¼ û¯Ïjâaÿõè¿ùÍ!;è%UšÈ(pÙ£§ú2³ßª–)#Ä$ ãñ.Å"šãÌ|#±FãQÌ Ì <"’ˆ> ‘yLññÜiƒŒ`¶ýemARÌFy,Äýd*‹o2„&DÌÅH²’%®™ŸÕüíÄ\¢€34pŠ‚t’ C’lPHÈâæ(à _ÌÓ L˜HvÐßd‡Uò/) F¦£ÚÞT–r”ÊKÝÁÂÍÁrÙw4¶ñNPŽCq»»» …@`|ík_3/ŸŒ-í)}R Í›ì@Q~þ󟲃M …ÐÕÕe¼>ÝÀ®ÿPi=¥Õ*—?—ä\J%UW*ŽQÅ”ذµd‡2iJô”¨ñóPlÝ~›L€4=izÀ4²ƒÓSƒ_þ!;èkвŒŸ~õë¼®x{Þ Î¥ýÌAE¬ƒ\ô½Sçõ+<°nBÖ¶ÂÙâP.ëŽÖ°Å yãýjª —Oj¶Yßî_™=¡0?'"W> ž2dcýÎiÿwx*HûÒÒuÂYíª­„Â%ã·ˆSÝ!~3>ŽßŒãOžD£ÛµuØU[‹ŽÚ:4º= ÔÒ¡üÅ…VHà18;…:—í•>¸˜¥Û MR]ȹð£òOÃj²ƒå0 k0bÒV À+I¥–Æš¾°”–(k#XXµxmÅÇ:ác]I‰â”Åb¥‡°ÈãÓG~Žákç“·vó´½úc™¨;Dd§cɃÍCñ9Nê¶a¶ºÔqñWàì‹KÆçe½½½hjj*ÊëÇåraÛ¶mð•ÙM¡PˆÜôXDeO޼ŽgÛ?œ¹$!)ë*xÞòQy4ì$•[RS\BvÐNµ¨Œªæ<.ˆ‚ù¾¥´‡Q¡BR°ÙTåò%;Pꊵjï{?€nÿ 80#¯YŸ|?åAv˜‹ PUÕû9 É æ¢j<°¬MïÌÊvÀ> F¯|^§$ ó,úЉ«PÈþK™Š¤Bâ’-IÅ ÀüâÙeýQ‘a•²é!;,os²(`d&–$ÖPÛp €rQw j…Û|Ä`Ÿîxáp===EWË$Hbß¾}p»ÝˆÇãúºB,hëB¼ø¯ù á…ï|ç;Ø·oi(6A @8 ú8ÿŸŒ› %;?—l呪+-§(yZWœæí[Ѽý=ºâTÖ­#Î΀‘úÏSœ¶97¥¬}GæÍ7&ôßüȹWÈ Š¼zgÑAxظûÚÏdA,ˆøHR†9ty‘éYÝñë¶n«:ýg†#L·rD>d‡ÈÄ$"““%QΉx¿¹„_Œ\€+ˆŽÚZ´UW£­ª”BRiP …éDÓ‰8šÜø½UpÑ& U9x%ò‹Y¾„ ÓÔ (‹ígIÇ2õÊÒSÈi® @ÉC,P&¥¡…è ¹›Ñ‡s¤±Í³o„G‹Td%=|ãæ;ð×ïÁKcÃygíñ“o Gïö—ðh(!;h…¤(8…¤&ûѧß5dÇlu Iè±Ú_½£G0¾h8_üâÑÞÞ^”a¶¥¥~¿,[~Ëä|¸$ÐŽs‰9ôMËJJÌáFïú‚竳ú´:«1ÌëkዌN ÃÓX¦5VuBv0o²"ÓÈÚÉ‚"CTes}Ké£XÝRIj2}¿» ÝË•²œÕÙ¾©y)€-'à *Šm©šÒ§@¡rÕ}ÂÒ!;Ì ¼n²Ãæx>‡C箓B(ëì›Jv 4¼Ÿ–º©ªb.¶Hz`hû©;è7³ŒA-ó‰R‚Þ0Ùòª‰äaœÒšŸ ;Hò¢ºCžd‡¡Kó%3ë,muBt($誠=mP¢çtÇ}æ™gÐÑÑAi|âŸÀ7¿ùM}B<õ*˜ú[í×Ý…¹¼ÉðÔSOáþûï' Ä&èííÅ /¼`8>Ûò1Pî dP(©a˜ø¹¤çP¤úJÒ)ª0 U˜%íAäÉ_V? µ`|7Ø®LJä ¤Ù×MSu€¶›oÀŸÿp’K” tbè·p짿Їs9±çàݺâ8=]jkFÔ(šÍió%-'ÊÚ4iB¦Í¥f‡tÇIUbãÙ;Ûüø¦†´uv§§"£ÂCA&]¢eñûÈà;†llܳrâÅ:ÉË?·ïÅ5UÞ‘ßý¾dó¾D€øêÑA|ö׸ãÅá ¿} }çNâ×3—0*†!s2FÅy¼>3Š¡„ÞøBN ÙÁ  «ÕÙÁ¢òQ–Ú7ì–¦ H2€¸ülD‘AQQJÚáÅò';h€‹fÑäð¡,)(PÑ•‡®¿¸}¬7熆CÁæ‘ IDATwÐù›ïá‚c¶à>.xÛ*TH*:DääfUDðÒÜyÝ6< gººƒu‚¥hKýL„ÑsñUÃy—®Ûo³ &izÀT²ÃÞ?¹ýÒ_²ƒ•U'˘»<†±Sg0u~B,^åš½x¯þó·!&x]ñö¼[7y¡ª±ž4¤ ˜º Ÿð@;½%_îñññ~3ì…󦔓ÇuÇiÐHL¸ô¶vu‡– êK‘Õ¢Iu‡Ä|Óç/éŽïª®BMë5+;ªËIX®µÂ"<Ìž¿€ø\ÈPÜ÷}æ~\|ë¸aBŽU863ƒc3é²m;kkÑVU¦Š ´UUáúúzl©ô¡Éå¥Ò ÈÕ­ ÙAc|ßÓ\«ÆaU€±xu-Ò€” ‘ ‰ÖY¾&;¬H_M’®Ìvi€¦’Ê’ÿ€²˜!UM~ÖCXÊ©ð»«H5ËW”€$é€)JGCSØýò·ðüûîÆ¸ÍHS·S tBR-Ÿ4Æ…hZ»ù™²ܳn+EKi=$—>Æe¹¿g_DXâ ÅmkkÃç>÷¹‚ÏA¼^/vìØW™Þ. …ÈÉE“#¯ã»ïéÊø]åO¬3ÒGv¢çâkºUÏœAhS>¶œú !;Ø·Œk‹ì àÙ¼2ëW@²ÃR8J]]AlÅÝ"’¿­J‡až7–ç”ç,M£Æ™‹ì ê¯Ë"‘b’”YUDG›[¿ÀÊ ºâ±OqQ†$+æú_#ÙájKPŠ ¨ñ8íGvÈ¥2)L1ÉÐF5f:òe䙿8jÖp’¬`d&ª½ S:ß6Di©;5[¬VÜÍ`ên…<­ÿR‰ôõõ!G­­­hjjÂøø¸¾®"ÌÂ਱G\";¨ùýÛÕÕ…gŸ}–4 ›  ¡««Ë¸Æ ®íÏÈ€`ë1’ø¹d+T]é;FU òÓP¥yÒôBá!O¾l|xò]ÊQkŸâ$Æ O÷C•"¦Ùtz*pg÷gѶ÷FòZ°ѹ¦Î_=kºAt.„–ÛÀ:%]®oþ Âcºâ5oߊæíïÑ×BÈÉ‚©óAýï·Šâ¸EƒÍÀE.–æÔrò8 ég²5ïÜvå3‰ÈÌìÔAxèØžñùj f!1Ÿœ¤ß4ãž•Bv(ð„S’lŸÇ±c' ÅkÛ·7ê^Üü©{12x'Úw~6`Ûrf"Ax8[ª«±¹ª ïñùpC}=ªNìon¥$õ¢dJ¡@I«Ýd]æd§Ðjz|nñYÚ¬Y´., ¸²—ˆ@ˆçqtz:k–B‚€Á驌ßM'˜N$À/ö5Ç¡Îå‚Oã¢À_YeåŠçÕN':êêÒžu6¥È芋mA¡~ñsœ¹úܰÿ³µ.ª¸‡¸%ýàºÝ7Sl¢ìŠ%•ݤ³}%P Tªw%é¡Ý[ƒ¿?;˜wa‘Ç~€§::ñùõ{®ö± —ZHÞÎ<ŸK{öÃéw ÙúhM;xY€£i8h6oâƒuZê¯ÞÑ#[Ëx<<ñÄðz {3AKK ÚÛÛËznÛÛÛ‹ááab}“ÇhØ™á}!#ȇáwV<_†ëðø¥×ô2óg2–¥4AÈö-c¡Èö '«júÍíÍ_ÈiŸ5CŠ@PÐô[N†AÇ!&‰úl¥¬-+8ŽË‘¯Ò!;è²¥5ß–+/˜üLc˜¸`Â>ed‡¥Ï¢¤@8XZSøìŸË”ìa ¥ Qt²ÃêÏFfbW‰©ÙfÛ­{2>ç#QËý˜@⌠ž2ãM7f|ÎUv¦¶wµRöeœ<}²¨ÿ‡§§·<øÇ9Ã]û¡N\û¡NÌO%É?í×L6*5ü:…,ðëŒ!Þ"jb‰1Ac‰ ÑÙ¼­••設G‡¯>©ÁÓÀ<[^êkXÙ!MNonÂC¡|µDz¨’új¢5õxúúN|þ÷ý¦†có¸þåÁS»:ñùõ7­®t²ÖÛ¯šT7&Ò•Gd¯ÍëWüÚQQͮ췈Š@ÔMzð1.ËüÕ¾ˆfÎŠÛØØˆÏ}îs{x½^ttt€eË qèÐ!]qºººpÏ=÷ä »ÿÕ›rŽ=ŠÞÞ^>|¡PˆL&–áht?›;»j6¯ø®˜*z Ð7y½›J•ð@Èö.§ZœtL#”6ÙÀU)Bv°„ì°†¢àá8x8Ö$EÕ`YViËfæÏAF¨q:1Ã't·;ަÑä®ÐÞ¯Í$#Àbûfåßd²HŠb0.!;¬š÷´ïTýi,›/±:ãP9úKÊ÷ QNª;PÛ#ö‹æÐ%}êÅ@oo¯!º5ê„èPª`›ïpö)Cq€þƒö¦ã‘GÁÇ?þqÝñä™ÿ2Fx0¡O ï>™÷­Än·§N*ŠºAfƒAtuu7À¸ÁµýŠ2.?—l呪+o§(<ÔÄ$T…'í!OÈ“/& P®õ`ªv·UK ¦ &ÆLµëôTàà—5tàœ@?¦ÎC‘Ëë ¸"˘:?ŒWÿùÛˆ…ôíOÔojÅöÛoÓ?e$ê›@l.„© úãÒN}9Òr¢¬}I“æD÷À}ù·ºãpn'|šÒ'0YTνvD³Ý–Žk‹3ù%H‚€ñSC†Ôê¶n«:óÍžœÓE™ˆ‰ò~YË‚€©Óû¾dTwȆ%Õ‡‡¾ÿ÷øÓoþ ¶ßµŸÈ¢Wˆ¿õ&>ýÊ˸þ‡ßýì×°û?¿ŠÞðx…= µ9Ô €Ë""Öš&TP/‡uÂEÛàpôRY€ ±€œî‹v¯ßyßGÐæ5oñø…£ýè:öï¹¢¥Õ¶ X'’ªàtl%ñ÷µùDeýä“Û}¹7¬DE HºÛ±UuÒ}þ†ã>òÈ#SZhjjZ3d ©î @Á`Ï=÷œ&²ÃrìÚµ Ï=÷‚Á zzz2*B¬u|}ü­*0@òäÃÏO‡§»< ºãõM+Ñ(Ù¡ä'jÖLÍ —‡2#Oö$;p4_™M ;€‹aó¨Ï#;Àd[f‘ tÛ*m²Ô:]¨r84× GÓ¨u¹Ðê­LöªdÊ`;*q²ƒÞÏ¢ª`.Áct!Šñh s Šª²Ãªßـ엄š3Npr!8£§þ3 ×·.€ÎÎ΂M“ì¡î ¦üY0%µÈ4Á².ânSgì–ÿ¥ Š‹ûï¿n·[7f Dƒúûež0ƒì@Q!;Ø]]]†ˆxK`[>ÊQCÕÆDÕÖF ágRukcò¨*Pùi(±K«“H{Ð%|j|ÔXdÚ¶nQó/ÏŸ€8ú¦“Zv¼Ÿþæß²C°0=ƒøB$g8WUeéô­E²ÃoþõûºÙs.'n<øQCéu‡•ˆÎ…™N®±¦Îë'<0îšrpÀY†áÁ† JŒe£\~Swœ ×mÓŽD12øŽf»w_›å%nííÿ‰ÅAϰºÃž2¿°8CºiA'£¢`Ë|å£îpý}6œn}»þ¯?ÇÿüIî~âÿ$ä‚ 8:3CïžÆ~ó*>øãçAÿóß¡óW?@÷¹_àyõæCæ րꂢª˜¸”X¸ò7#Æ!ªjÑ|Uǹ‹['™ÎÙ„Y`Ùoû^–ÃÓ×wâýuͦ%ýÂè9t ÂÛôåÒY9°N‚‰0È?›=¯ÛdÍáöj¿¶9²,' i„µ†@Û;zG£“†âÞ{ï½èèè(H“hjj¶mÛÖ Ù! áðáÃ9Ãutt`ppÏ=÷œ)?@WWWã±ÇÃàà`A•¢²ˆÎœÎøÝ¿ñ=b5º›÷莖yž9Sj;,Å™@Y¦´ !M+Ó[Ûd‡ä# 4EY”?h";Ð'Ëôs ’¨<ÛO¾¶(“l•8ÙaéY“»-^xYNš“aàfYÔ8¨u¹Ð\áA‹×‹­Õ>lª¬B­Ó•ì3…V^Ð׈ý ašÒŸ É(Máçxæ0#"Š˜çELE8ZÀL,‘Wò";@CØœv4|g(ò';$㡸ö¾’ͧ)ÃÕЈ~…‡B’³ûúúЍî`á‰*rX«(`ïc{;===DÑøÄ'>a(žté»í—f‘¾óﲃÍpôèQÃñ™Ú›ÀÔÞD¹¼ß©V&(LÚ½=ŽbKÈñ$ÑA ‘ö`F+‘ Ï1ŸñÝŠ­,ZÞÅñ!ϾnX"öþɽ8ø•GÉÙ¬bnDa¥T” –Ègó[ ÿ^ÿef{Þm¨¬N‡¨;,Ct.„¹‘«¤®Ðظþ=ŠâȬõ“Ô¶Üáaã¥3ù˜<HqÝñšwj#<Œ¼­ìPßÞšõ{1n-‰$1Áø©¡+Ä=ð]³5­×dn .'yKi¬Es ©Šb»²å«îàôzLÉGÛ­{®î{æ1ì>øaxëkIã# È€ÑËxæøQ|쥟`Ý¡ÿŽŸþëUDC¨Vçû¦P‡¸Õ¤‘¼"ã|<„)1†¸"^ù›¸aJˆé:ämšœžâÖIPó,(>ý€—åðÿî|?îmÙbZ†có¸þåAÏØà–-)K!üeúBBJ`„_Xñ|\ˆ"Üîóë ¯UåÁ*u‡”@ÏÅW ÅmllD (H=µ´´`Û¶mXKèííÍy°&àí·ßÆ®]»LO¿µµ¯¼ò ¹Er~8sãBfÅœb¨FiÉxH®/|å©6nÄÏö.ž51 %~PEÒL‚<ù²a²åZ¦jGqòm‘ªƒÓSƒ_þö~â^Ò8 ˆ…éHBîvÈ:pT¸K¢L¡± L]Æ[?ü±î¸[Þwš·¿ÇPºUõ¤A¥@L$›H«1Áë¶C;½Ä™©þXãå$M Ï9Ý„~rn§f…‡¡×´«G´t\[H¼E’ü­±[Öï¼.û`IÚ'”ÔJ#¿{ÛPùªëq˃÷Y’§–ŽkÑù|ì«ÿ7îø«ÿŽŽ?:€úvr[ A6™¾J€øÖÿFçÀ÷Ñ3þk¼í¹Ôó€3ùa(;\JÌCYèêAsR—ø…ÕIl(yXŠ.|hI#Æ€Š¯œÒÿù–üÕ{÷ÀÃr¦eçñw^Gç‘ïà7£Q.d‡Åö%© ND§3ymþ’!Ó·WëGEEÑTd_¶óôWïè„%ÞPÜGy^¯õ ómÛ¶¡½½}Í9}}}«×]o/ž{î9ËóñðÃçÌËZBTÑ7™ù•q!Zp•ëBWíVÝñMGH*UÊ"¨;XzðT-BšV¥W(²Ci„s1¬Åéf÷wËÂÁ0…)¯ÙêfÚ¢ŠhK·¿Ë‹ì`Ù³¼lY\cS…ƒÕÙf¬QwˬFQULÆâ™Û•¾Ž™b4ƒ¸¤Ê¬%/ÐC ³ YÀÎd‡LdUâ$ãsqm>ËEvH%<Pxðûý[“ ëÂ5ÔˆšÃZS³´Ç˜úÇ¡C‡ÐßßOœXD´¶¶bóæÍ†â çÿÉÚ~)Ì™Bv€§žzŠl†ÁÁÁüHOŒœÿ“ã^›´œä@PR•GªnÍ:E•¢PbÃP¥yÒL„> 5>j,2í[·¿maâøZ¢êÐvó øô7ÿ-×m'£ÀЪîPY"ÏŽŒbôw1ðoéŽ[¿©»þðNCé:=¢J’úŽ“eL@‘¯^(:u^ÿ^ã&Š+†âCkÛØ¸ÑW¡Žë'©)œ§¦¬Óië²ÌŽŒ"6Bÿ7þá± Ýñ÷ÿÙç2v^×~ IDATNµ¢Æg8n9b~r ñù•çIŒ(<°:(U¶«[‚f"„‚Õ²ËHLä(%¹ñ!†ÇtÛãÜNÍ 'Ú¯ÙnËîk‹â>Ãìðˆ%ê ÇbH-øÄÚBrŒ^Ø]Ý!ž×ÐçÓ ÷=ó:ÿâl¿k?š¯{*ÖU“FG@º „«ä‡ûGN½ˆW¸³@°‚,„²ƒ†4ø4ÂCîÓ 3b¢ åðq.û«m(b%Ý…^–Ã7öÜ{[¶˜šÕFÏaÓ/¾çãïdV{('e‡Åt$UÁéØlÖ ¯Í_2”„QÂÕù‹¢ *ñWoF]šoÒ XŠ6Õ_ÁD‡&Šàõz-­®–––5Kv’‡k2¡££Ï=÷\Ñòõðã««‹ ôÈ®ò © F„…‚ç§«v«þ÷ÿìY„$»ÞBÈö-#!;d«‚¥èܪ\&hŠÂ:‡ W˜6Yl²L¶e„ì 'NVr‚²…‘ ©ŠPûYËYœü;X•K¤-þ7} àqå~gp´ñÁB”Œ/Äq)/ÉéEÒz¿ÜÉzÒ4ò ý¬ƒì N.è°ã»”¥íàÙYÝí¬Jƒ “½³«;Xx°Œæ+I°wŒËpû ƒÄ‰EÄc=f,¢œ€4þÓò¡„‚÷Ë„ìPÆèîîÆÀÀ€ñ9 {Ø–•¯ƒˆŠC™T!9””ŸKŠ5>5>Ø÷Ðhi»8|jÜ Jí[W2;E³ÃÇ þT~Âtû»ï¾ ¿ò(ê7·’FQ$$"bÚÔ#«›ì{Á¯"˘¾„Ø\G~ø#Cd‡~¾õÆÊH3Œá¸å>ËxÁ{hlB·ê _áÁÆ0m3‚%ÍÌ~à"!z‹7 1±10Ñ1бñ+ä†/K19ÒmÛ“öMå‘Á“šÃ¶íÛ³úË$µÄWB$†ào ÅݸçÆÕ;§ËI:ƒÖå–lÞ!`U±Ïmê“§ß5¯¥c»åêÉ~eLY£¾Ýúvÿ•ÿ‹ñB—Ç1y6ˆÐåq„.#6& ›`Íc‰üpèÌi´VV"°õ½x ý½Ø¤¬æ9s±ÑG1UZO‡ñŠQUÀQty¬7T˜*d¨Žtƒ¾¥m•>üýÙAD%s~a‘ÇÇÞøîi>‰§¯ý 6)µIÂE*;ÉÃþ‰UÎ~6«Ÿ¨xse3²C$.¦«;è%;,G*ááÌŒî~ZÂC¿¡Ã¥+Õ,œ;‘ƒ|¥Æ ¶ùH—¾§;j8FwwwÖË °ïîÆã?nhoDžzìº[G~^¤ñŸ@žzÅ”ò²ƒ=Ñ×ׇgžy&¯÷ ×ö™òrŠZ2F ágRuÄ)YŠ®JQ¨ü$!:Xén~òìÃñÙu·€b+-Í#ÅVB‰!LüÍ¿øÉé©ÀÝŸEÛÞIƒ(2ÂãÚˆîJ/\•^[–A‘eL†˜Hàè‹?Çðïé¶ÑzýNø¯ße8U õ †4(\%Ÿd‚uÆí#NÍ€µ~}ü i¥ˆ«*9d‚8?f(-ÿÍÚ6•çǧ05¤½£·ßº§8ƒßØ$B—õ3ãX§ëw®.IÚ!ñ‰²+“,˜=gLÝá–¨;ˆñdA0Åçv¡¾Ýk?Ô‰÷æãøÈcݤQ,ÃðÂÿÝ›Øü½Cè|íûxŽÿÐÀç§ú`e‡+ E†…Þ“%¢"ì`½uZï/3c@ÅV.,ïjòãéŽN´yÍ]½0z»û¿…§§ÞœrAê¤ i¤¤‘ŒðÙo_¢8—˜ÓÌÞÊfó²¬qéêøìe¦ú+u‡GyÄÒêjjj¶mÛ°–Ñßߟñy À®]»Šž¿êêj÷–VSy£Í‹ßY]žýí-|Ñn;(ÊsÊ?MʆéP'¦©1¨&Û[ ·²ÍVqΕJT~iҟÉ*Î ŠZãd˜lËrRƒZ˜¾©YÉ!Ó3“ÔJÐ>/ËFq&Â……yŒD"˜ŠÇ1“H "Š˜áÆ0¼°QQòV¦p;ÔT8À1tÁÈ•nÜ&'Ù8†F•“Ón•|Îó"ÎÏ-$Õ²Ù°šìa¬ TPÛ²Ù*FÚæ*›7ïÝ´Ì/Gmªð`XÝ¡î¶Ìîq”6˜š= =m†â¾ð Y÷ Ã*„s_Ë+máü?˜FvØ¿?!;؃ƒƒxðÁó²Ámþ (ǺÒw†e—Ó“Aµ°h÷ö@ü\ïUÊOBMU«!Oþ PŒ3¢+ZA{·Z“1Šå¨hâØóGÿòCÛÍ7àÓßü;Bv° ¢sÚ.â­ii¶eþSÉÁßÅÙß¼©ÛFõúFì9x·ñy£ËoÝ:Ò˜1u~Šœy}Gÿ¥×e¤î`*ÖºÂÚ½Z’RDЋJLl\w|Éá¡b]5|š²ç)…í5ôªö—p}{+œ^Oö¼ ¢%>LÌGpáߊ»ñ¦s¸Š ò†ZØ<}²¨¿í¶tlGKǵ–çÏ*Õ#ïϺä¡ÝÐåqˆqž4ƒpz*P¿i¥ºPËuÛ3¿{7·ÂéÉþžªj¬ÇŸx S†MÍ瞆üÉ–ô…lDqnÙÖƒ3ÓW>Çb˜ˆÅʦ®Æ.c`ì2¯|­ïÅÃÛ;P3_$tðXm¸wUËU "‰PôdŽ(;¬%±P+¥´ƒí^žîèÄß â¥ñ iÉ…E_8ÖF‡ðôŽb7ÝœT{(Å:É€Ó±ÕT F' ÙÝëÝ`j>eU… HpÐ,¼ gª¿Œª;ìÚµËÒÃ%^¯wM+;,!Û ===¶Éã<€žžƒÁ5__ÙUæÑâ¨,h^ ;ñù ¿Ô×ÞfÎ[l³Ãò#;¨EHÓªôTsÓ¡Ê?œ‡åÀÑ4æEAÛÜ8‹²MQð°Ü"±8Ÿ¼­‚Blén ª¹ªÙìÙŒûÛWU1AB\”¡@ED1ƒ¢(p ·ƒÉªFÀË F"Q´VyASù½XY†F׉¸ !.JdŲEQ¨ªààä]qÚ\)“5ôœ[Õ®';¼öÜOˆãDÂèúÒg+ÊGdc3mLW×âcÅâD)3¾s¼ …FW¼­yÚÜV%.3.=Vœ6’š‚&“wŽmû÷”üÝã÷]û|ñŒq «íK/j¬º…þz¤gbï"y§óz±yWy¶¦àõÑFk´MŠÎ9¹§&&mOC•$LßTX'Ô —r‡óxpm]Ö½´oÎÆ‘™CÊŠˆç ]™Ù8Ò³ñE¿'j¾Ý×­o@ÝúÆe¥Æ­m¾[Jd¨kj\Ö´Üz³å„‡W''±{}>Õ¾ô«½6,ùû±"á ¤dy Iâ*9b ‘@Z®ÃŒ«ªG_m» _»ýnlÑÖI¡t@—:ð ‹Í¾:\“Æ»èA¥ÁÝ.À$æI‹B¼€¯Ü´ ;#øî@ÒŠumÿôô0î8õ=|íæ£gó.D¤@UÛ–é çæRK­)œI\!Næî5äëi4<Ã!ÂY·žŒ+"NÌ^0¶»»Û¶ªòù|èììϯî­m4ÅÐÐPAÛ·¶¶º*¯===è顊^gæ® ¥Þ¹ Ä2QS1.¥Ñì :–—Cë¶j§—Ѿ¡Ú' d7—ÑB²c0K‰ ºõDÀC¼‡åÐàó#«*T9M-“®~íOËÁÇñð.–‘®U²ƒ fã"rÌ×M’@ãs ÙqwüŠª!ž• éy;*š†©lþ¥¨®ëTRVEÐËÃÖo—5 ±\ë|¾Êò?Ÿw¿‡ƒßÃAÑ4d$’¢åóW4Œ±Ï ø½‚>>¯S6ìҶŲ 6‡ƒˆesˆå¤…<’Û-!J¨÷{­#;0ÕxF/_¿åⱌ¸`&ŒN&:1g< ”uÑv¯Ï¥êf‰æ\Ã}çwd9I±²ÀxÖ‚kzêÄ/ˆÃ áÈ‘#®º aµák_ûüqs]<=yø‡ZŒ½T§_„2þS@·æfšýû÷S²ƒ q•ìH˜ÿɆo·¾«ö ¯»>B 'mM«ÅhÑu º4 ]ŽÓ&âDU䦡Å^5žoذë2ÄùÁð!0ü(3g¡LýÊEhÙq3xüOñÏ¡ hS^cíÉê²(^SˆM˜";>/îùì@ð™÷%ÔGJ^’»šKgš.~A‡uÀœÂã^¥"Ë&\Jxp!¬dÚ\#9Ì]+Yãà+›Pw€·ÝdlH¥1Ü÷®áx7ß~‹ãu¤k.¿þ¶©°FÔxŸ ÇÒÎ`Åä€Ì. ª ·«;€è…‡«ê×wõûM·–w~vô˜¥ä‡úMÀ{ /Œ¿‘ ¥Ù­… ²± ë;¶"X©©¾Ùrëv¼ùo'-÷»çÞFS °Œä`áðµÏWÃçÉ oSû¦§ç qŒg2ÏfÐ?=íj[¿pÇ/œÇþ ›ðµ;wã#þ¶òÄÛ&IS+€—å°ÞÀ¸”6”†å+‡¨).²¹}™9ð«Ð½K_llnCG(‚'Î¿Š‹)kõ޾÷[ô½ƒ¸ó`¾MJ5²¶)P÷Q±ü\q69LœÔž5ö’è: 0ÖzÆ} …œdzàÀÛœKxžÇŽ;V=Ù@Ñ͇r]^:D ÒªŒçfΣ{ýmË~—SŽÚ¼a´zÃÊ‘­‹OÌ^¨2á’Ü]Æ*ja‘fÔ¹~¾Ì~އŸã¡ë:d]ƒ¬iСCÖnu÷°ù»Ïy–…‡å,´ó*';06Çe–ì@lÝÝÊ N“@¿¤jHd$è‹ì˜” ßZŸÎå÷Œ¡0é!%Ë „‡ È‹ÿæ9u~Ožˆ¡jT ЦAÕtÈŠfˆìÀ²y• ÀÁçaóD‡Ï#;\ýÌ2 Ö}¨óy0•‘*vÆiÐq>%ɨx ?ç6²C¡ò";0„iU“ìr²ÃtR\Pw %;û{Ñq™þ âé·ÒÛ®Ë!âøñãä9øÆ}5t¾Dá6ð û Å^….ňÃ=zÝÝÝhkk£†¬zzzðÄO`||ÜTx-ö d 4éAŠAº|zvIJ|ñ‹_Ä3ÏF œ5N: ¬5?ÁÛ¡ôpóGöbãöÙb19Ws„‡ö=wÙ÷o¾æÀ^t¬™'0XèGÝÙÐ`¹jÄx:ƒñLý3SH$0H`"›q•ÍOà£?ýqaâƒK•®£¿é:]6 ?˃eœóþ5Õ%öªYŒÂ@,-KG(‚§ïú8¾;Ї `i’C™$>úâ¿à‘ xò–`‹¶Ð\l¯i dcPJÞ€ÆàLòŠ©äö„6ÙVtè`,ò’=6jîæ;Õ::: ¹OF´*'}ËÏ"‘yä×åµµµó¼Úp26XðWrH©Ò2õ;qhÝ6x¶dœÅ?ëeŸ8ëÈÈ &RÙèº¹Ž¬Å^”¾¾åÁÛ®}¯N¿5þº¥D€’ÜŒ#GŽ˜#Þ-K„¶G­WrèØ„N¢nUq¨ñŽ²âŠ­K³Ð¥YÚ\„:û*tÉäå’¬|C…û9†#„ód†‡&ŽA¾òhéK¶•™ª:ÔÖµ¶`âƒÁÂMã°®u³«ò›ŽÅ€kdY$'nÝuø“hÜÚZQ^ê[6‚å8ÚˆÌ —½¨{Ą¢ãGQÛ¬òò¯ ;I‹aVÝ¡íîò·º^/ž1îdѾoWUì=öÎçRÄá6ܺ¾E·‹ƒ§ ’?™™Y¨’Ïš¼5æ8&g²+fPš¼)M®žà¤ºCÎfu‡øÈ¸#å˜ú j}ÞÇ&l#$¶åo¼Y†Á:Á¹ôؾÞΛ¯ Ž¥TÉö4±±Ì‚I°ÐC2pÝpòåŽNÜÛ° yî,ÒŠµíøù±>¯e'F“ÁfÆÀ'm%9,†b‚ð ø½ØtëMžó!—Jny°«ì3$·Å*+yÛœºÃæÝÆn;ç½>GÛÒô Nìã­oDxÓF:BVco3îŽÃ9–G«ûTµ0òöyËãŒÙKÖÈÖ¢ÊÇwÙBxæI/¿ˆ'ïÜPæ:çé4oìÀãåINË;€3ºaBDHÐÙа„1H` ™@ÿôú¦§«ª±„øpÇn|ijeÁ6–žV˜ZÉýeß<.!=̧Á‚A³7/kù§HYâ²Xe{Ykc&%>ºw©rAg¤ÏîyOœg§G-ÏÑÑó¿Eïåwðµ›ïÁçnr¬;ìU$ó™YCöíK‘;]Ün³µHuœ¢f áÁmê¡Ptá¸x¼?}zÙw®Í¯›óæ4NÆ ¦å,Ÿža©“`œ •ì€ôTb¨ J…ýóÛJ";èUHÓ®ô,&;½Mµ‘ª¥îà²CZ–—’sç‡YÓQyAA0W'N¨A0&ÚÉj$;Àø™B–ÓÏš#;\…¬h@ñÞ€‡¯Üþ–àÙañçz¿a¿“é,’¢l¸¿…¼|qu‡ù¿u]G&§"#* -•YÑr²†œ¬´-àŸ/\¦!tãñ”#%Ø5‡3Ö÷7EÓHc†ì?º†3}ä{ï;w"^û›çy444 ¹¹‘Håç¬'Nœ@"Aþ~Ž[{ÏZ—ž'QÔò‚ßü‡>ø ’Ÿ_öôô ««Ë’þ@AŽÿøÇ¨¯¯G<weþ†Á¾ðJvp)úúúÐÓÓSQ܆ƒ`#·ºmX«…H)ì¶3­6j ‹­Ëqè¹iÚ|ªuòÓaÙ@+Ø@ùú…| —÷»ÓUÊÔ¯ Îœµµ¬íw߉ÿoðVá‚c óXÓ°kÖ]»ôšå8W4UE|l™X~¿P Ù¡õŽÛ°óT”–ãPßBýI@Åk$”R}÷ù8Æò`½æ.ì`5qÅÛž¥Í¯öÀeÆà™ø|CÿÏÔ›Ž‘ää4Î~m»;³ ¿ù®ág½Á;Ê/nÊÉÆ"91…ñ÷.‡‹Ü°¡¦õå-Žïó:ÖžÄDbÙý©É©šéª…7©ëšVÕ²¤&&M©;Ô55:ªv²RSQËãLÇìÅä\ÍÙyûý÷Ùº©»˜Hà‰w_BйÉ2<àY0å3é3æ3é“àÁdY@fƒCDG8Œƒ›oÀWn¿Ï~üþnÿGñå·âÞæ U«‡Óc#øèÏ~ŒCoþ—"€O«rË(ÿ6~àÇë?êx/êx/…¶ú#q{²U¢ÍÄ•œíi8mcˆ˜ÔrâHˆðõ÷âÞ´ AÞz‚¡L_xý$º^ÿ'üZÍöZލ˜€¨)eí;.¥qQŒÇ¿#`¯ a èÚ‚³–Iô¥'П&W°Ø¹s§- <Ï㦛nÅ¢¶-¼¦q³ÂC[[­¸yœMc\Z¾Pt­ŒÂŒõ8´nq˜S‰ËUXCP²ƒ;ËHÉ…Ÿ£d+ãJJÒr²ÃõûrEFJ–ȉ¦Ô,*#%;̇ñÏ—‘”¥ žYs=¡†¹¾t¨Úò8"^ç½+„ìpõ3Ë0hÐZ‚_àÊ>ïå8Ôù>޾¾>¼üòË;9rÄT8¶Þày¾¾èE‘FÁxêÁ5ì3ÛÐÐi• kð“Ÿü ã>©7†aðì³ÏR²ƒKFÑÕÕeŠtwm. ß ~C•o Öí˜ël‰”‰… ­6j«‹­+г#”ìP%¨³¯B—LÚžõ€oè"X¸°`„0ØÀ `¼M`8_žè0ù+ä>xÂV²CÝú|òÿþs|ò«ÿƒ’j¾5!øÖ„\EvP$SƒC×Ȳ(â¥ïÿ‹)²Cã–Vì:üpÅyªoÙ–ãV}{ÑTÓCÆžº%ŽŸ™÷#atÍ­f‹Z%<¸\£d ̼ ß•_Â3ñ ¸Ì¸ãù’c¦ÂÝØµÇXcäX œyÅp¼F¼I¶ÔC¯¼e*܆یݎÀ{½ŽÖk6ž¨éþb%áAµ¸­bòüû¦Âíùüï;šOÉfÂCzÆøK¦õ7¶™J#>29k½\ablÂÞþšœ³œÄåÚ÷ÜekügÇÇðÄû¯>ué!ˆ™Åßâ°*d8 .€™ž'BŒ{ÁÌzÀ¤y@2F‚è‡ñ©­øúî=xááßÃ_íÞƒ›o@“ßù ïóÑKØú/ÇÑ}þ§ˆ­MäÕ-*©ƒ&ã/oá &/ IDAT†Å:Þf!ˆfOõ‚¬]/J”c\JC±bcà&²ÃµÉ“€CËÁæ6<}×DZ3bcþééa|ôì¿àйÅ%~fy{tJÙ¡@:¢¦`87gȾ}iò±½ÀVŸ}7çq`ÁÍß /UHè<6â.u‡¶¶6„B!P,:ˆFk.Ï;wgæ®ü~XJ:š®ð Äaœ%gÈÞµHv@ù¸2ŠQU Å•QHšjM¾ÌˆÒXíd“íĦ4³²Z6!Ë•|F¿Ž4Ñò#Àódy[ad‡ÅŸ½<‡Íášëü8¶ð3‡ÍõÁ¥¿PvHfd(ªN¦˜PàQR‘“T“ŠìTΙßH¼f¾«8Œn*QR1<6Ov(¾¹]X3÷›;wÝ»woÑßDQD__Î;EQˆãîííÅÐÐq86Ô6Ôn꼂b5£t£à›ã©7óÑ£Gkòla¥`ïÞ½øÂ¾àª<ùý~<ûì³øô§?M+È…ˆÇã8tèPEdÆ¿ BÛ£ÕÎt×GJQÒΔäPSv®Å¢W•‡–¹]ÍÒ&UªÌŽ@‹½j:<ß°` \€Èò`¼`ý7€ñ¬~ ÑA™úOS*hFqûÃñè·ÿÆvŠÕ‡\:ƒÉAÈb¾ýÊ¢ˆSO>žá M¸ç³•ûúëÖÀ_·†V€Øð(TI*ûœ,Ц¸ÀÊS`·ìÐ×y{/Vlà2cðŽ…ïÊ/Á'Á(™ªäIË¥ fÉoºiìhEp­±N(ø}¸ø¢ñOÇÞ݆ž32À…œ1öù@Ä{½ØpÛCÏzf|ЉåN4þp†”N#1žJÉ VÙÎÇ+#;ëç‹ }äkÎöövCʃÓÓÓxíµ×J¥ˆâïíí552½‰Ÿ:üQTØ(øóÎév]\Aa Ï<óŒ-gif‰DðÞ{ïQ²ƒ‹qèÐ!ô÷÷›€óChÿ€sðcJrX5s­:+ C›Xeñi ªºJ›U5 å N¾`~šªÛ6ÐVò†‚ñmÌø5Ã:JthÙq3}êØÿ¥ÏQU Ë‘šžÅÔ`ôÚe¸WÉf.à ohB×—> Áç«(O,Ç¡¾e#­ä}öŒúíM ™J£…‡Õžš}ö»®b’Á'ªGp¸R슩pFÕàÒoß@.m¬¼Þ`ÀƒÕ7¡O~Ejz–8œQ²çCb"QÐF¾p¸f:°"æ°`VÝáößÿ/ŽæS¶YÝÁ)Œ¾m'áa‘ M¶.nƒõµÅælÜÚŠÆ-­˜º4dk:ß|ó àvà ¿PætŠ r ïg=:àW¡{5€-~BÓ£#|¾¼ã6 $øù•!œ¼|iÅ~Å™„$áèë¯àÞÇ>¼¿þ$phwêöý*§Wrˆ+µ2öUèá$³`zP¸¥ŽŸj¹÷6lÄw/öáìô¨-¹?~ùœ@Oûø¿n¸ õrЊŠÕu?œ›C\ɶ¯Gß[ƒönRÜB_–+P'91s …|­uøðaë÷G<›nº‰î^ €jrb ¾å·qŽËi´yÙŸExv×£?=Iv¨’ž@g°ÉÆœQ²CÍÃr‚ͤËÕ(ÙÁx˜Òq)š†X.G—só$ ¿À™HßD[dÛcc*õ¼[ÔlW ‹_ÖtÃñ³LžôUUä)0 Ç¡9€À²æòoø6'`a>J8·<B¡ a`9ñ"#*–‘À²¬Q²ƒ‰t¦“"âi©¼ýÖӒΖÿ/••qö-ò½7‰óðUµ‡ÎÎNCª‚§NÂéÓ§É»‚§lx‡3ËŠ…ùÁ†ÚÁÖÝ-ùqØÓ§OãÔ©Sèêê¢UP%¼ùæ›Ø°aÆÇÇ«–‡ÎÎN¼ùæ›´2\ŒîînSóÏb[ÿŒgmÍ,#é¤YãûZmÔ0[WÒÐs“”èPe¨³¯BWÌ]âÉð!p‘;‹üÈ‚á×€³à"Ô™3PfÏÚJrò¾Š{þèS¸ý‘iESXMU]âLŸŽÅñÒ?þÐÙAðyqÏÿ~ÅdX׺l‰Ë_V IFlظ¯ÌȻ侠œ?†­À¥ÌTáÁ¥fι†ì k ääq¸ÀÚ06ÝjÜ))úªñ›Œë³/¿þ¶©p›w—¯¼>Çê6[DêÈ©ƒ®Õþ@­Jf/^2µ˜vRÝÁŽ>U…‡È¦fâøÓ³qÄGì»…6³W8HE(’\síÜ©Mß7ß|Âùæ°ö€Eb€fÒsMS2Žp_¾å6üäÁOà¯víÁ½Í±ÙåÔþë/ÿûÏþ3.E&¾Œs´©[6®«od€wb)ÿÈùôLåi8rfÉXf&ŃÉ.ß°6û‚øú-÷â¯n¹Ç6µ‡„œÃÑó¿Å–_ÿŽŒžFÌ›¶gÇR¤NDMATL¶ï¸”Æ„D®²'´ÉÖÖà]ÄkW*ØÔöN’¯}ƒÁ ud‡ªª;X¨0aæÙ2dEÑ,eÙ¡lý0%ÚƒÑxŒäÏh^I/F7f~¿©jK’µu£d‡ÅêýæÎrI÷¦Š¢ ¯¯ŠR^eÍ´ºCÓKÏ‹¨ Å’ù¥òÁo|àÌŸP•‡êãå—_†ßïw<]†aðÅ/~‘’\ŽcÇŽáøñã•­Ÿ»¦ÃÞaÌ6:iÚ?YhgZm´=;Ùt º8]£d‡jWuvZâ-ÓṆ.€½N%“õ€ñ6‚ ´ñ¬»FvÐå˜cаýþûð…¿ÿ6%;PØY11pi Ù!>6_}çiÓd‡ý_úœ%—é†ÖQ%“yĆG‰._5Ax¨TÝUÅ_”ðàR0.Z„™UwhÛM&½9xæ5Ã϶tÞbè9MµîZ`1™ÂôErGŽÈ › +&ð>/ιn)&–¿Œð‡ëjŠ•§[XÇšªT¥ ‰á¨2¹ûö»à Í«•}ªÒ„ÁO~p?õAÔÖü›•¤"Ajfµ†í÷߇ºõ ޤõøKg0à5~3±#—'Š˜™yòÃŒŒÈZñ·º{›7àë»öàŸî?€Ç¶Ý„&¿ýˆßŒbë¿ÇWÇN!^7WÝã²CTL@Ô”èa6xIJ,˜¤¨ËãÞÛ° ÏÞý4µÙ77Ê9}1ñ!cÝÎ¥HÝ+º†séi(ËŠÛw@$·øÂr‚­-"Àñw§¨˜ÀiNÌvB¡ZZZè&±úúúj.ÏCCC´â®Ã™dá}·¨©H©’cù誻8Œ}„‡*j~ãäÒÁb²ƒÑÛô)ÙÁB;»›ìIé:²ƒI5†¤$A×õ ËiÙ”ìP ²ƒ~"pŒpåãöyø@v°ð³ d`‘ Ù¥Ód#ù©à™be·›ìP2NÝD˜EûÆÉDI-]^£õtýó‹.³{î…(ñJ¥©© 䊢àܹs¥÷ËѨ9‡Sή~uü£¸n^±ÖûñÔƒkØgz¯~äÈZ-UDkk+Þ{ï=GI~¿Ï>û,žyæZ.Foo/üñŠâàÖí·în·c žòµ;Ѫ£F©JÑÕ,´Lº’¦Í­ÚÐrP'_0?OÕíë[¸’á×€õ·€õ·€á×,4-9yä‡È]ø¦#D‡–7ãѧ¾zþ”:}SØ‚Ôô,&>„*-¼ ŒMàôÓ߃,æˆã»Jvˆl¨\™]ðù,‰g% 99…\Úø\3úîû¦êÔ¯Dó¶22Jxp)ÜĶQL¨;À¶®=†Ÿ‰Ž —6®hÑQ…‡éÁËçÈo×ÜpÛ­†Ÿõœ[‰‰DAÖ™Qr†[ ‹ÖÕ±¦TÇ©uòüû¦ÂÝñû9ž×\Ê=EÁï5näíó¶æ+>f¿ÜðbVo-aûýûI'-Ëxü·/")?f;Bv¸> ‰b˜ O^Bd‹æ¡9ÀcºÏ~ìþj×ì\g?iäÿyó5Üòïßÿ3ïA¥x9 ƒ©Éö:-gÍ&¬«÷ZD µ‡/à+7í·nëB“ϾõËñáoqdô7Ö®Ã@6FìTÜ—š$NgG`½íUw=¡B2¡ˆubö‚©´>lyyÌ8©¬&Äã…É¢§Nrmž£Ñ(­¸ë0!§1 Æ þ6.;·ï “†r DsV«œU‰ìP³J N§GÉ…Ÿ³ƒì`WÝ»Ÿì–eÈšVAÞâÒu™Åg>U%;è&ìB˜¯Z";Àø †eÂâ‹hò¾ê8xx–<®#;è®&;€ž!h×FÒ¼ŽÈ x¦L<–©4˜ø½B²CJ”1<.ÆH¾K=7ÿÿøL‡“ÄÓÞÞ½{+Úc ýÝ´ºCÃ}tãC'¼ÿø¦ÀxÌ9I;vŒîÙ«Œ«¤‡ææfÛÓÚ¿?2™ >ýéOSû§NÂç?ÿùŠâ`Cà[µv£$:QÚž«Ùtzn ZvÐ5Úì\uöUèŠ9?†‹Ü °<Ϻ¼šƒ·q‰Úƒ–„ý;ä.|jü ÛËS·¾ôü)ã/Ѹµ•V0…åÐT3CW–ù˜¹…ìÀrÖµn¦…¼ojrbŠ(̈ uVðõ†¨ÁËÙ‰š}Ô%:lr šLîPÞ¶{'ÑÍë£Èí{w¾ÕžDF¦äZYÓpåsÄáx¯Ûn4þ¼ÏëXÝfã…R|‘0mø"51‰l,NnûÁý¨knt<¿Võ©R˜0vãodùa¯œ1úöû¶æ_sH›¨S¢Í¢$Õ$éáöGtŒõž–e<þò‹H…Š;V…ì°¬2çÉóÊJ«>üÝþâÀælÍòh&‡~þ|äwÿŒ~ï0ÀkÎt¸BÝ!¥J8Ÿž±5 ëà€—dn^íAYžVg¤Ïî~µnG·Oµ O|x [~ý·èú.q3¯[Ö¾Îgf0.¥‰í;%WxØê³­Å1•oóz'È%gwîÜiùËØ††D"PÃÍ n&cT'cƒ¿+Î]ˆá}Ø$'fõ¥&j¿lRõ*¤iWzµNv¨âsŒ ö6•?Ýõ}-£(H+²¥m÷áq ï0&êsµ’Ìö›¾ zãýñºxxŽÅ¿`>…~£d‡¢Ï2,–-‘&QZyu‡`€/ýì’ïôòõk%ÙÁjå3d‡ëÊx~8QÚþåÊZªý/Qw¸djܯT}0B)pAR<DZcÇLÅÉ›¼uŸb¥ÀYÇG¾Åœ{"‘ *.@kk+ÆÆÆ°¿=—HE"üà? ç35€¾¾>:t¨²-˜„ö?©|ø²à@=åíŸ{l"9PPÃ8^ìùÄÔ ´ìèr‚6=·4…ì´Ä[¦ÃóMÁÚÀúo#„Eï9ÕøëÈ]xRôihéK¶—Å `Ï} ~ûo°ý~JZ§°¹tãï,óûr Ùê[6‚÷´²Ì^%3j‚ðÀ‡©± €€85AqH±+¦ÂÝH îÃ}ï~¶}ï.ÃÏZ¥ð &S˜$·E㶉H ‚£ ËoEò‡ëÀr\MµQ%—«é>63hnA~˃]Uɯ•ª)ÕÀä@Ô‘t2±„iÔÞôå pûÃ:–ÞÅD¿ö¤‚Ë„\AvX6é2`f=yòß'C@G]_é¼ÿtÿÛ‰§FGpïü~åW÷¶9—*; ;ôÍMB©ä6•DvÐþgÒ<˜ W°|µÞ‚§ïø8î]·ÑÖì$äž|[_xÝ~š'>x+»¹Å,ÙúÓä ·ìݨÖqžeß©:Y£ŒŠ Se«Ô¡¤à˜GÕÌá.~iÝ×Gï(h—ôD‘¹I†¨9§F×nµ,ïæç7‡_R²ƒu‹Ɔ¡I¨|Ü!Ÿ†a*³¿ƒ:$Eƒ¤hÈÊ*Ò¢‚D:‡XZB,•ÿ—ÈHH‹ Ò¢IÕ ¨š±øIóeO¦,Pä†êÃpc0½|ÙÝLv€^Yý1@t"…TV6Fv0<§,ú¼èÅÉ—‡‰ÛF{{{Å{IEQ ª<œ8q‰ùÙ/WÀùé¦gU¢:Îl¨lÝ-¦Â?~œ:»èlç?øe—øý~|ñ‹_D,£ª5€h4Š®®.SóÎÂäÏ“Hç ªâ°æVcv¦$'í¬ç¦¡eGÏË)Ê@ËA|ÁüTµöÃà"w‚á.UÖå”É_A<òÈsÐeg|c¶ßýö7°ç3Ÿrì"OŠUÖ]Tñ± L F—]8<úîû®!;Ô55Â_·†Vò$Y$ó•}÷}sõX·¡¢¼rJzUÔ %ºêÈ×§’—-œß(ÙÁet’ì “Çév²qºv‘Û¤Õd‡2¤‚9YFnñÙƒEd‡Åë?3ù2ÆJ²i;v+Ù¹jÇëúuÁç̦YAü¾ó{x¬ñ `ŠÍIòà8xx¶2ûÛDpP5YIEJ”Kå0•1™Èb*!"žÊ!žÎa.#!-ÊÈ)ä«ÿT 9YËr â) ³s&"¦bž±ˆ‘Î-#TM7Þ¾M¨;\ý?àçÁs A¼K?3,ôóhXëϳ ºñ´Œª%X=‡—k{La ••˜+Ÿ·Rv(•Ö¢®tòåa¤³äŽLV‘ñ ÌÞ|Ï5=@7<«Õw€ä7>pæÎUzzzhºŸþô§‹Åðä“O¢½½|Z`´··ãÉ'ŸD&“Á3Ïd ߬ïñ¡fþ%¾:r¿R.â|z¦ÈmÓµ©ì0.¥)Ù¤,LR´åyéŒ4âé;>Ž/oíD·ŸÌyzæ >úÒ?£í7‹Hô!æÉ”ÝåįÍcZΚ¶ï€HNxØâ³Ÿ`+·yï¹ììÞ½{ Y» okk£ÃJë²·×uy¢d‡Ò(¦”Wœ[‹w†ÈEûRV(Xæ;¿À£>(€çÊ“E_¨Ìþ“TMGFR0;—ÃÌœˆ¹¬„LN¬hÐËÝîo೦#OŠçÿ)ÒÙ<á!>'a&‘ÃT\D"%!›Só}ϰ‚N¤œP_çÏQÖ(àhϰ€ÏËaMH@C½Á ŸWæ %;æ‘Í©H$$LÏæ09%bjZD,!!+ª…ëÅ*ŦHŸ1£àüpÂ8‚11n.Vwø-¹ºÃÕý©Pãã ç¼§NÂÐùE7l°Ý¸³)EÃ]ŽŒ§\Ã>Saûûû]y¦°šÑÓÓƒD£QüÅ_üöïßH$Rð_{{;:„üàÐ4 ”ÄRCˆÇãèêêªø"¾å÷Àø7²(ÉÎ9:­:Úž«ÙJ'¨K³Ð2ÃÐ5‰6I—5-=-ñ–é„>—ß{ÍœEî¢OC›{×±´ì¸‡ÿú«8ü¿DãÖVZ¥¶@SU$'§05…*-Ç¢oôãµç~b®YLv|>Ô·l¤•6_o³Ã£¦ÂŽš <˜ñM¬!D­ŒŒ§ÍÓZƒ®˜N+‹P³äŽ´µa´ÝMFx q@néÜN4ðX)Áôàâp$dÎÈaiª 1‘\ö½?\–ãjªJ™Œµ¶QTGó?yž|‚óD*'µ†ÉŒÉ‘2…‡‘·ÏCÎ’³a=uÍPæÈ¸¦‡±W:–¨Ifí­îǹ_üÚ0;95CdCâcæé~~%ëð_l¹mëì:øIð`<T¡‡”e/‹¯Ûv3Ž_xïZ™­Æñ çqòÊe|eç¸=»Íß4~wÙË`Š®a SüFm÷Ád‡EÏ1)ðjÐ=견}jÓ8д?¹€ã—ß±=ÛCÙ$¾Ðw ¿FÏ–;q¤m/ .uPŠ+"¢bq%W±}Í(µ¶—W|Hò%¾uÏ>ì\gÏ¢x"›ÁŸ¿ü"þß±×0ì›Å¹Ô^NŒa /¢úPþLÄ6”HcZÎâµäxådÇn8qÙaÉÁ‚™À(Ë·!^Àc­·àŸv=„MmŽt‘„œÃÑ /¡û½Ÿ> )UÂpn/'GÑ—š´„ìYrÇ‹AûIj^¶²Cˆ3ÈÇõ`ÐrÂUw°ÆVñxÜUƒÓ§O£¯¯VZ SxPôüxæœUy¨ÙÁ¶iU¯BšnZ¸--Ýžváv²cÀN³‡D IDAT"ˆê¢ÛÍ‹…)W©2•SÏç‹$ýj’ܲ`lN“±9Í2$ /Ï!ð`}ëB>D‚^4®ñ¡aÏ5d:’ ³©œ+È…Š’†™d©Œ¼´¿/¶ !Ùáêg†e h\çC¤ÎƒHØ“ÿþóºˆk#ÞÂdHæÉÉ9‰¤¼Œìp}úšª#‘”1=“ƒ$k¥ë¤°lL2LXú9ž–0<“6†1Ø7¯ÿmQ—zî…¨©.}ðàAK‡¸ø¼Ân4ÅóÏ?O>ÄxêÁ†ÚéFgE¢vn|æ[>m*\"‘ —PP8ŒîînSóÍ’étÝnpë»l®¨€óó ­ºš²3]ÎTlg]N@Ë\¡dW5’¥u§Nþ0[? -ñ–ãd‡ºõ x çOñ…¿ÿ6%;PØ MU›(ªê¸‹ìëZ7C —ÈURÓ3¦ÂŽ˜Pw`X|¨r?VI¯Šú¡„Šåƒ®,BNŽ‘¦~/±º‰rcG+êšwnE’+¶…’“0%Wwà½^„šÖ¬åXð>¯#õ+& L|‘p ¶U¹fûÙÌ 9RÑ¿ÿЊ¦¢†Ÿl"Wx îËó ÎO~ƒQ¢5¤¦gk®žYŽÅ÷î†@0îM]ÁŽw!PáXõó+—ñsí#=8y–.M|è\×€'ïÙ‡¿ÚµM~{Hu?ºtxæ?ð"¢=¹¼Sybô‰@ѵêÚªHq%‡¾¹IœKM‘4ª —’#Ùmy^›}A|eÛn|ë¶.ì ;£LsüÊ;è~ïgx-3†l¬L]“Û׌ ‚# N%ÈÕa¨ºCuQŽrìØ1$ Wäµ»»›VX¹é]•1 ¾y3®:§òÐUwq˜bdòóÛJ";¸(MÆ…éuV·LÝA·8¾«Ï &T‹ì°,í*“ Æ•U• Ú­ñ| g.ÏL…mÚ®¾iع¾Ðw©/Ôzü„õDZ < †a*¨3kÉЦaf.QVµU«É„ie$3ÉM[j“d‡ëÿ÷xXx„ùóŸ9ž!ŽgÉøl”äP€ì æTc6ÿNÓtÄã²Yµ:d‡"¿+ª†sC1cýË,ÙÁµ7–ã3Yüüåaâ.ÖÔÔdùþTQ”ŠˆäTÝa%¢ö"ÙP;ØàVSa=Šh4J«‚Âtwwãøñã•õ÷ð­àox”’VÄ=ƒ3ñaœKMa\J/u6¯ÙaZ΢on}sˆ+¢}ub9j€ì°(&Ń ¯+:ÃëñämqŒøpüÊ;˜ãr–Û׌*Èz!€ 'Ø^f/ËWþÄ,¹ÂCgg§¥ehnn…uˆÇã® P' ã(FpTá!ØdY¾K¿+ì W!M»Ò³XÁÀˆc¸Q¢€¡çt‹ã»úœnC=YHv¨¤=ZIvY\Ц‘9Åš(#Ã0ð°¬9EÒ<1Vö[½¶Èµ?c X¿Md‡XJ‚¦ëÕ#;|¾æÔÄ’²’ +ÉÖ?S!ÙA$#;,þm.%C’4ãy/ú{åd87ƒ¢jDe &;,ÚÊž4Av¬Ww¸ŠÑÑQôöö’ä|àêvРΊAm;Dò›ÿÐtØ#GŽÐê§ °ÇŽ«˜ìÀø7Ah}Ô†qzÊ;3¿XhgZm´=Wµ=˜OPWæòªªH›¬›Š–ƒ:ùBM”Š(œ„"ɘº‚™¡+EUâc8õô÷M_ kÙ¡®©Áú­ÀyĆG¡©æHY#ï¾Y4㛸â}#¢VFF ´æ—0š9fŽ¥tc×¢ç嬈øˆñ¼cßn‰¤rgôLÜÔíé "#Œ]‡®8–‘ÜvN–àÁLziùÛåæ@_¿k¾õá}hò[?÷¤ß}çmô¼ókŒbËl›R%DÅúæ&p*v}s“ÈÄ0-g-sâL©Æ¥ô’Ű8·TaÂ’Iƒ®MÖ,˜9Œ\xÛq°© Ïîþ„­Ä‡ 1 ðÖÎñä·˜[½ö;ñ¹ÂêIÉî.cÈÕ\¤. %;ã@\ЉÃ=yÝ2„mÏMd§ÉN˜Bv°Hq!#©ye£íËju‡ È×l‰”UÕí'2€ä½ü3%â›S*&;€¦ëÈfUãñ,‰S7fùwÓIÑÉ9Óe06vX´e}î…(ÒYò3œئøï|ÇT8®aÝÜÔ$V®Ç(ß°à|¦ÂÒK ((ìAoo/>ÿùÏW 燰õO* ;P%ççªäPSv¦K[í¬+ih™!èJš6ߨ7]‡–¾äڶ츇ÿú«xôÛß D GKg0ñÁ âcã%}¶¢oôãWßyÚÔíÿÞЄÿ÷/YNvXÛ²‘Vâ<4UElxÔtøt,ŽÑw/ïÓCÖN85³*ꊧÍÕ­ëŒê8®:¥î9 ·›pô–²•Ýà,Î¥0=Hîôi½8ŒÇ!ƒœY>°ÖÖƒå8Úç‚*IH ‡kß· ÞPpEÛ&=7ÞÏ6y5òöyÈY3’Q ·à³‚ ËÂâc–.8K-¼R3³5żü>äÒi´Ý±C¯¿e˜ "‹9|pöl¿ÿ>Üuø“xí¹ŸT”o¾ùp;ð ïF@5á!æe‡‚ƒ fÆxtèõ2À.¨s]ž½ÿŽ_xÏ ^DZ±Ö²f_zé?qxk;º7Þ ¤ø‚e‰+"⊈áÜK:ÄyÀ3,BœžÉ;Éû8>vé²5.‹‹âÉ]‹Ï‘ƒ†/ß|ng7T¸W•⊉µÐŠ!;85‘:Ÿ“æÐý%ˆMm833‚^@bªâ,4y +óÖ—RÄaÖ ö\×pފ›QxØ»w¯eùonnÏÓ-*)"‘Âá0‰DùõV<Ž®®.ôööâ±Ç³5_§OŸÆ¡C‡(ÙÁ$ÄÙ‚„Ewn"jó†qštM¯ähåUn$;½™5‘ªY÷N¨;˜Œ‹–anª/[.²|ùxAA(c‘½ì$;hL¾ÈÌu¿³€¨©Õü>/¥ÈP4Íð-ô!^Ïæôq<|_ÕêÜ®ƒ7T»§"Öž3Mü ù UuMhé¼eÅASÆç·àڈ᱀„\umr, Åzפ„‡KQÜxïnGì§JÒ±8‚õ‘š¨ïÅc_ãÖVlܾÍ0ãUsx÷?ƒ]‡FÛ;‘¨ˆ ˜ =Ôšc}𓿠¯“aiÄÍžüð>œÃ}¯[®öp1™ÀŸÿîE|jK;ºo¸¡TÐýöª:˜Új_¤ihå‰{×mÂÞu›Ð—˜ÄñËï˜&>9{×¶Xnß1Ffk–-ûLTL aÂQ¹££Ã²|RuóèììÄéÓÆ]Ó»»»qêÔ);v ápØòü=zGŽ¡Sá8³·nó²ïSš„8Cšoó’·S‰!t…o(1¿Quw–‘’ ?g—],Rw`*l“. ;\…ãQËûçQçõXSÆD;©„젲׊û·߯‰‘AÄ$Š®£ÉÀ‡×5coÓ†%·¢NÇ8gX„„¼Ò^È#ÀÇqðq"o‰°6*/‹Ÿ1Ùo‚6eÙøl=Ù@q't+Ó*'S¡Í¯SwXüL6§ÀãñدÒPòY½ô3Œ6!lË˸®•( †QT }ƒ³%Õze“B/zKÙûÓLu7»ÔÆÇÇqöìYòîïÛ6ÔN75®Æêöå7> yðÿ‡Boo/º»»i¢ ¨}}}èêꪼ/·~løV:Ö­Ä9…V5LU‹mc‚º]š….'hsvk±<~ ¡`xèÙQ(¿‚{ºšqEiëÖ7àö‡Äöí‡7 ÕOá²É9ÄÇ& JRÙgãcxõ¹CblÂtz­w܆]‡¶´ ”ì°šª"6}ÝÝ݈F£´R*„™>iy» ·âè•3Daâj®ÄüFÉî,c­“ªü±](Ù¡P?/,kú]Pôdùª´]Wd¯ENà2 0ÀKÓcø¯g† qù™Ì¿F±s¸Ý[nF纆ÒÊ¥Ò^ô¢kˆKùq|:·ô†uÏçÉ^/B¼€ ÀdzæÚ0S¥ïŒ´«Ò´‘ìP²åU‹ìPްØ&EžÍÉTMÇ1ö‘J•«ÙÁ 8Ž1ÖN ê º±6Qæ»óà ¤DÙx«dδ@Ý¡©©É6ƒYu®qÝиÔ{ô*ØP;ØàVhéAâ°WU"‘5$…™óžy²ƒ¥ÖRà[?níÝt¬[)s ­6j˜ªÛ¡µ4qœª:¸´Þ«9h‰sP§^„–<]й¦Äíw߉íÛö=wÑê§p¹tɉ)äÒiCϾû>^}îß Wp‰óöûïÃöûï³´”ìP±áÑŠýŒ£oô›[Ó‡¬U§a]¤Âc'XÚlÝ F“KKŠ]1î–ƒ]¦ÂMÜäÞ±—üvôJ³¥Tñ‘qâpõ­äÌ+ƒ„‡Hë ¬­‡7DÃí5=‰­uÀœŠŠåûOÕ^ œ‘™5v¸ÙÔd|QñJùÄXD2Š Ô“—KÌ!‹;VOª$!—ÎÔD¸žm¬àÆ{ÈÆûwÿó7×>ï:ü0Z︭â|}óÍ7ðâ@Áç¯Ø}îcwüfÒ&¹œp|ù–Ûð­ïC“ßúù)­Èø_¯¿Œžw±5±âvv“½œJ#¿ú[e!McžøÀ¤ù¢d£Ž`_¹q7þé®OàS·!È e£=°¾ mÞ1ßîYËì;%?Ü[/iA^vùÚŽgŒmùN%Èɾ֭Sé‹òŠ`¶.âñ8º»»±eË<õÔS¦^¶& ?~ùÈGÐÕÕEÉa\*|pWœ;¬Šð^â0…‰”ìàÞ2:Ev°ó9ݦtuʱɰ&.ŽaäKÈ à ìõš#;9ÎëÖ©D\OvM'ñ_~ó|A²ÃUôǦñø/â«}/c<]0“ ²C¹6'ª âRѹ$ÎÅfðòÔ8ÎŒá\lѹ¹ì˜;˜<áesd‡¿ók0}ÝX:eÉqL'Åòý×ìœeƒºƒ7ÍŸs×·q'ÉÎúEmRäYÖà÷q¥Ÿ5û[Égʨ;$;\ÏB<ææäòö4¯ÏÇÙKvX„óÃqŒÇ²æêÝÈxxýß‹î%0«î mSw8yò$Òiò59W¿ àütCã PÒ’Ó¹§\Ã>¨Ó/‡=vìzzzèå°’ìÀ·~†Žqµ:—Ðj£†©j±«`g-]œ„®I´™»¨ÞÎ*ôTjzZâוºeÇÍØþ±ý–;}SPºT©™Y$'¦ ‡‘E/}ÿ‡˜º4d:]ÁçÅ=ü–û{Q²CñzŽ VOôusþiBxÃj1µåN”ð0¿Ç°µ>7sÉT8³êSD ?Û¾w—©4*qΖÒ¤¦f¡Hä‹nR2 ߦ—õªöÿ³÷æÁq]÷½ç÷n½ÝØA$@Ô‰BK&)Ú₱š.Û 'bR‰ßh¿Š^%#y åUM4yñ3ßēȮJ ÊD¯<£$íL½¼˜ªP¶<&/ B:’,*%C\@ ±4€ÐÝèåvßmþh4ˆµû®Ý·ó­blܳÜß9÷œ{oŸÏùÚ¾ŽRFXÒ¯HéØé¿]çuf¶2SƵÀÃø}‹´¹ÀúN-Œ«B;ðpk-uoLO$N$W9(ØQœËµŒ„æ\.ì~âñeÎ …4ø³wpd׳‹ÿßwâi$碆$€u ‡rwvXs ¢@Í:—%°|a‘ãpò¾ñDãV|«ÿ2nÆ¢¦úúUœ»{'ï{Ÿ¯Ü$Y{ÅËN°­@ñ‰« . ¸8=†ÉTCñÚ|´VpÈפØò— €§Añ4à’¡°«ç|ËáXýN«ß‰¡DqñÞ}[Ð_¿¬NTÊ܇÷!^ûŽ&¾"@ÌÖt¾`iu‹Ìômmm¦Õ|In<~0¾IBb ²Á‹ŠÄ¤.èÀLù'¢’úhS+æ7;”µLqNP,*[1±ŽK#°ƒú4æçUép€i$J¡ë–äÇ1 ¼Mkw‰Ð|+:/Ô3ý‘i\˜Ó\Äù‰œŸY>¸V/Ö6åú^FˆdÒˆdÒMÄŸ+N'j].œNuùë½7 ìÜseeÃÀ )JÁc,ƒ´œØàv1H¥Dˆ¢R¸yþNÓ\nZýõ¢vXø½è°`áQ=žð×?Ôu»pâÄ ËnƒÎœ9£+S{ˆ<È”Td%©± ¿iîW€¤íûŸœËéS§H‰ˆÔ¼ß1v *«y„LM$(%;õÒÆYÉÌÚÂ!€t’Ü3 %3%996h˶qz=hò0ýÍÏ£²¾Žt¢¢+:Äóš6ÚŽLLâÂëß3äêà øñ™g-æ~'H`‡õ57:nhCõœn\zOÇxJèJ IDATÌ‚«4x %~Ó´ÆžÍzâRrNóâ]àÜNl{ä]eŽ}¤~rS°]WbFГt"‰xxVsºÀŽíÚ/@—‹\}yn$"#wŠÆàôyQ½kç² XL›;P+²µEätm‡öÛã±J²ÖQC“ÃC“:àáú…wµß8}y-£w˜»«)Ïé[#Eo¯rqy`Üâ.9µ?y׌¨{ñ;}{Ó·F–ïgžýmô½þ}D'& Õoô nä•lxTˆ×òñ°­Ò¿èöÐ{í*¢`jÑ“©$¾=pçkFðüØ-×™ ïå3…qÈYØÀ¹‰a¼v}`Y;\šÎ’ç .þìá'°[Zñ¨œážàd(yÍpµyëÖ‰J™ÿ¸cg‡ftYªð Œa>Ѝ¨í%LCC|>Ÿ)õv¹\p‘ûbÃêìì4x ²B™à-m‚¾\ˆÞÑvo/ò°nl¬//mt.Årw°¢Óà„M;¨mC»ÂbàaY¸¼,"#IH¯õ>‚ÊBš†‹eÁPT:ëpdÐK­i–Ôk‰»`ßÔ˜¡+ïø°]­¢ÑãÎî¤nƵHi»®ã‚€¸ ,µ.7NGÖ ÂÁ©ëC”šz©ølƒÀ¹ß=Nq^0'O­ýØØ Éòúù©HŸ·>k£N_¨ÌuêPYÉa.’AŽáÐ ;@ ŠµÖø¦¦þªûm‰`‡%îg~>ŒDJÔ<Öy½^Ë€‡¡¡!ܼySs:ºò!PŽjò ³Ùï×ËIŒ;ëò0ùÏš“öôô «« ---$ŽDDydì@¹·Ø¡\æ2%‘À”ô´mgE„ÂOBÙD /íÚnŠ8¤§!'†!'G4C®ÅRëãŸBûSGÐz`/é D%Sb.‚ØT’ÆÍ±o\z?~ÛPÙþ- è|îYp&WN`‡õ•ŠÍ#›7œÏô­ÕkÛ–Šõ™uQŠ´iÚ&]ؾ¢Šp³‘‰ÜÕ•n÷‘àÜúÚé!õ‹õ.ö–2ú,ÑY†â5-¾ÎI«»8<ÒÑ×Qrf©h Ž'œ™µ´Ü`¦—Ÿ Š/«%Рâ,¨$­®<‰•`³n&+”Ñ>/®å¼P´±–*ÜφÓÚçLâî`?uuu‘ l0…„xÉë`œšÓô'¦Jssc/©” L+Ë3vP»›>LŒsÂëFQ€›eáw:Qïñ ÆíFÀåBÀåBÇzUN'¼WdØÁ¼s\U/åÞ"BÚ”!ãüÄüÞÅóxù£Ч²÷ÆfÂ:úo˜Oa(Åá)¼;9‰«‘9„y¢,«TÕUÇ1z?+ìn'“1¼ÚßaB}L€@VT”ašëƒu°°,ª*( šaŠU°,'Ø!ÏßK;ËÜÎü\Ÿù‰'LƒðWJ¿»ÃaS ìõbµ<ÅÖåÐþ½G4%DD…Þ˜;8v¿@jÉB¦$2Wo„Ó¶Oœ! 9y—À%l7%5iæ—G߀8ò÷Cç!Ï_³ìP·³G~ÿYüû¿}_úÓÿH`¢’)1Áĵ!ÌŽkZk*ð<~ù÷ÿhvh~ìüÆWŸ3vpWVØaÉ’„¹ÑqSòþPßF„Žªí›)ä}¦¿Ç Ý€M¬&od‡kNǹ¸¯ó€®2µ¸;T6ÔéZì-¤ôߨeÒFFµï ®ÏáÁI®¾õnÄWìà·òÿ"Ÿ.I½œ>í[¬J™ âSSšÓµÜg‹¶0rM©ºœ@H©kÏÀ6u Øoô½««.œ??ð@Ñ,h§rZÛâ³ñÁëØýDqÝ:bSÓ¨nÚjëë|½›ö–Ç:0øÓw4¹iq¡}N^ùô!œ»{¯ýúŠénp~ô.†&pbW+žiÚŠ¸÷Þn§ê¨JØ¡B”{°CÁ1Uðõ/á¿íÿ¨XÙ«MråH¨8“Ŷ2fÉB1 $ ÈЀlݵÊh_€ÜÀY¹2k°ì4¨5]Vª?¡}œ$Àƒý ÑÜÜŒ‘‘‘²¨o ÀñãÇqüøñe}`xx}}}èííÝôm:Ä—ÞV:èmÀ›³76Ä´ZöeØå;XÕövX P`(* 6P&^;”½ÎqÙÉ™û¾ðüÄœŸ¸ƒ'ê¶àDs‚55ù·AR ;„xID()"”LPër!àp Ö톋aÔÃ…Ž¡TŽKe;€(P𺠺<À„ú˜;€hÅ`-ìûeiT×8‘HˆàyIU—‹×Ç‚aLrv°#ì°düé}ë†íÜâñ8.^¼¨}ºqTöµ’Ó¢¾$"2ç…’LÃQˆwÿ»æ¤§OŸÆ©S§ˆËÑ2v`Ü$¨v›;ÈtD‚RÒS·Y¬ ?A@‡´™"ÎCIAI CNrÚ¶¨¬¯Eë½hòêv5“.ATRéut€ÈÄ$~ùýÔµ³ÿRu|á¨%ëÈÎZ=øv¼‰´mßýQ@œ•b ÔdzyEm߃[ð­Ë¸š0ÿSpúúUœ¹u'vµâDãýð%ŠäŠd—6ÉÁP;ä4É'qnzŸwï¶ÄáÀ7xå‚ Õs^ËËð2«ç0ŽRgè7Ì—ÖáÁª]97£Ž?ŽW_}ÕöõìêêBOOü~ÿª¿9r'OžÄ©S§ÐÝݳgÏnÚöŒK™²¬w_tþÅ+ÐÒÉ@)A™V•W,Øaƒgv¼uÕ¯ PÈ›Æ,@¤x°tÖo³¤9.MOàÒô:ªjqlk3ŽmÛ¾¸ËzÞz[é¼°ä³0Ï#ÌóŠÅàbÔº\YÂéÔ–_¡ö3Ûí¡„ò8Y¤E ‚([sÝvŽ¥×/#_{h…´B:`‡ÜO†¡PYÉÁëcÁó2¢$CQ–œ7GÃá¤ár1`Xª@Ìâ$ãã‘9Dmã«°°8î„fRxãúº©•îçÎC"¡ÝÙ‘¸;X-²ªÔJ1U{!M¾ %£º?uêÙ°€ˆh…ì°Aç2‘À”ô´ígEL@IOmª…–%m79 95%5%qŠ8oë8½´Ø»øˆ¨ÔJ'’˜Õèæ°Tƒ?{ƒ?{ÇP8—ŸùŸ~ÇðÇW[³l£X¢åJÅæ‘Š™3nêëÜh)µiÚ‘&]Ù¾¢Á²¼e‡Ó·@±åñ îr§‡†U»=ø®2Œ:]nVÈj‡-×\][KÁcÆ®\Uí±T¬O£ ãÖ¾ õô­Òì´›š¶ýîºsÍcðk¸ùð s‘5²Ðƒß„›úoÿÛeü$~ p™\ÙvÈI¢@M9AÅV/¤öqþlïü{ ÁmÍœ–~÷—ÿzçÿ q_ÒÚSWJã5ŸDÅEØáµëš`‡œ"Ó€[,ÿxérsµv’v\W}¿I«ãÛõ8<466šVO<˜§®®.Û×±··ßýîwׄ–ª¹¹ÿôOÿ„S§NmÚö\k¼a©â¾ÆéôÛ|§%;˜7i›;(ÚóT»`ÙØÁ w+`Ç•ÚÝÁl§«ë¥P t\‹*Ñu8R·Í²ae`.Œoýú2~÷·Ñ{í*â鬫œå‹þ5äÅ‹Fã ô‡gpqbW#„’IˆKßt_PV÷3a‡º;,ý=àu€e(sÜŒäãÇp½~}òÕUõ1JaÂDØaég CÁëeQUí@] õ .Ô×gÿUU;àõ.quX7OECÿ_û3>#¡ÿÖl~ØÒØî:`øë êJ¬tw€3gÎhOĸÀT‘Å;Ö½"+L‹!¦á¨®t§OŸÆðð0 Ñ‚ì`—yC1?K"ëâLnMÊ3Ί …AáCv°¸£(é0äèˆãoB¸ýwBç G¯Øvh}üSøÒú#üá?ü ŽvÿˆJ®t"‰é[#˜¾5¬ vHÌEpáõï†ü[ðÔWŸ³v¨jÚJ`‡<’% s£ã¦åwãÒ{ÚïñiVõÚÄ $Ó_àÁÆ¢eë€!rWWº–ýðVô•™âS¿˜J¿Ãƒ> OLg ‹"âÓ³Ú'fí»W²NéäyTèCä͵b³ ÐãðÐl·M[X L¤š~/NV–[LÕ^ÐÞ]ºŸõ‰ˆˆÌƒÀ¸À5™Ì-šæ ÅÖY’ù™t±gEL@NŽ@äÒ°NQÄyÈóW!…ÎA¸ý·G)| JjÜÖ‘hÚó Žvÿþðþ_úÓÿH "[h)èNè³Æ¯á§ÿ×똾ml£ÛæÇAçsÏÂ[eîwã4à¦y»éùn4ÍŽC–ÌÙÜvøÃ¼¾˜)•Vn ›!ž?7Õí–,Bˆêswhÿ|§îrÇ®\U#d`±·¤sqonQ·‡_C½æ4qxÐÖ®‹óW×o§)ߨwh;´ß7qkRµ€PœÛ‰À¶üÀCb6‚é!í7™ZJŠfA;µ©6>x­$mhw—ε>üU·«ž€_ãM%¿n9fAçïÞ1=”ëBq‰^ßíáù‡Áÿsø³h­ô[VÃ,øð ~ï_~‚ÞÈ&¼ÀaB@í;8d(5i€UÐ?7çÞýg\š·´Im/:;”›hP`Tî?˜Ò”wCƒy»C¸\6[ÝÝݶ¬W0Ä7¾ñ ]i{zzÐÒÒRÖírüøqÔÔÔhNÊ,¿wQÅ}ÖâÒ>¯ëq±Í—o¢#°ÃÆ‚(ÃFûd©a˜œ—HÁp> ΔŽ:é€U@w0P‡³¿h9ô°øœ;~ÏýëÏñû—~swî"΋÷à£×µØa­Ï"é †¢1|05wC“ŠE^ºYer½l ;äúEQ¨®pÂãb §]ëܨBç›çø¥1Q¬“·ËÉ€a©ÂéuÃJþc´öIÊ„Ï4¥Q åšKáƒá{.)V;º¬¥%·±}æ]Y444XêîpîÜ9]é˜ÚÃäA´t/ƒˆÌ »B\ˆˆŒÈLØÁ±û« ÜÛHPÕ \fgG¦!˜¢Ÿv™ÅY‘¡¤ÃÄÕÁì6“Ó·!…/Aù{ˆ#iê·9cëHx«xøèÿ€?ü‡¿Á‰?ÿ:ÚŸ< §—¬…#*½sLÞ¸etx¿:óCüòï kqûRu|á(öx:ïú(]·Žêv5Ã]YA=âáY¤bæ¹â þTŸÓ‡£f§eçHËü¦iO<…B‘Ít¾™¹»PdQs:#î0ucXõ±­÷é??»ÑçÒŧç4§­Ú¡Íáá8P ¹üòiåBû¥»Ô›íî Eî*õ»û§ææ0?5¥¹Œº¶fT6ÖÙ¤’–æ?¥ÁÝ¡®­¥à17úÞÕwSQµ]Óñz,¦ÆJ<ØÝå¡DÔþ”ú/ >7Ζ@~óéFØ?É‚ ¹€Ìê4m•~¼~ø³xþ¡=ð²œeµMˆNßø_þ—ŸàåÑÅ ÷P!êˬ԰#g]*Ä%¯]À]¾€IÞØø¬²hÙÄÎÀê…Çv’‡Y~͹,Ü ±±Ñ´¼ð`¾ºººà÷ûmW/# †ßï·-È¡V§NÒm„„øŠk›)j½[œþMvÙd¢#°Ã½|­˜âí;P*Ú±°C¾Àùò¢J•×:}àÌJ£¨_D¼zè¬oÂ𗾂¯Ý,Úðss>Šo}|¿{á<^ø7 ÍÍ/´ös3P¯‘Ž—%ŒÆøxv}cãøxv£ñ8ø¥»umpØaéç>7‡€ÏŽ£Õ÷ÙbÃk|FQ@…-œÞ,Ø!ß¡ˆ(#ØA”e\½ÁÕ»Õð‰î¹¼Ðعð÷sïŽâæhL÷³ŽUŠÇã8þ¼ö¡Éµ”{+y5tÏJV˜Ú%ì´·•¸<éŠ9hY9Ygr;²±: ”‚œº Eˆ’kÄŒÜRãfqüM·ÿRèäè(â¼­£àôzÐþäa|é?ýþðþÏ}ï¿â©¯>G "Û(1Áĵ!ÌŽ¯»YªMßÁ?ÿÕëùðŠ¡úp.'žúêsØý„ù›s.ÚvšQl4‰ÁÔ‚§o Ñ>2îhκ¶¢6ˆÈ’nm_ÑØ)²aî®®´-ûrmêú-ÕÇnô!]eÈ’E§ýŒâÁÇâ5: ¸ü•`]NmžÆã‰îµ/Í0e©,ê+eDïŽiN×~¬Ó6ç`µÃøç—zÀÃðûÚ {h§O³cë@+K˜„Àó%¹ÙŒMM£ºÉž_¼±€‡–Ç:0øÓwTß0Þ¸ô>ZëÈ{Óßùܳè{ýûˆNÛ}øüÝ;%“øæž'àKl2ka fœ€CRYµHé™mø\S3^ûõœ½ciUÎŽàüèZ+ýxfgÖlCïxÆF[ã[F†â“.ûàqn|¯]@B4PúÜ–óß•*Åë_†ãk‘B™¸æ4;]űdV°ì.FÝ£^_Tû5êóùL«·™yÝSww7þËù/¶ªÓÉ“' ¥?~üxÙBGŽAKK‹.àaˆŸƒq Í•¯¬ý_\FD‹w)…¡OÙ™mBØAuÙ›vÈÈ„…ݽ)Pp04ص¡Ìv‰(J^&o¶Û„–z± SËš>À9ÑóèatßÄ©ßÃéáOŠ2Š$DçÇïàüø´VøqlÛÛ¶>'«~ %ÊŒëKÛ1áT áT C‘(\,ƒZ— —µnæO›Ã998ÎŒ(#•‘äõc§î±v¥EåO¯×=",P ØAKLóÁ:ò‰ó®ŽFO ÚÛZKÿPs¾ §ñ”€¿þÁ ®K°µµÇ޳lÜ;s挾gñºCäÔÖ/™ˆ´†i8 ùÖw4g}öìYD"g¢M%;”Ñ\A¦˜’žvÇY‘¡df7)è`^»e1Æ¡¤Æ!§ÆlïܰTN¯­ö.þ#"²£sĦÂ2Æ®-ç1øÓwpã—ï®SÝÎf|æÙß¶d˜§*`Û5`vÓÜè8dɼu¦ƒ?Óéî q#æ¢P(Ôgvžx¸§(€ ¿u¡ÐåîP×Ö¬jwõõ”œ"UG¢:½Ýe :ÝÄtŠ,#žÕœÖ× }§p<äW:_§}Spú|¡,ÎCEdtXcé~L¯¿$YÄdéXI´ô\2¨PÜË|‡?~ ŸÛžnƬ}v3Å·.ãÛ¸Œ'¶àsMÍ8X³ TœY~(•³'Cñ,Nß4ìè°TÏlßF—”`âêL;è–.þØKƒZ{ñ¢Ijkk3ï”%¤V¨»»===Æ¿l5I†óhnnFKK †‡‡Ë®=ŒìtùÚÄe€—áð”¿Ao}Ñ¡‡f§#iõ}i 1e]e,ö•”iEyżñ,3G ÊŠ¼‹êf.ì–$Ì ÈÊ @E8šFçKÓªòÒ^¾ ó¢Jܧó- vÈ@†¾·iàÂç-ÞJô>þèÚõ N}ü.Lí»9ÅkW?ÂkW?Âõ[p°a Ž5íhEÝÂy3®UU;É/o?^”0šH`táÝ\­Û…€3 ?¸XF[™e;¬>¤2æ“ÂêcÔ¸9<^Y; °CEGN¯§ ;À†Ü ÝùŒÎ$0<‡(ÉÚ]<´ŽÅ…®Ÿ%¦½oÝ@"¥Ï¡ó…^°t¬;wîœöDŒ LÕ>•Ù½# {þwJ .râ–¦tÑh===ÄéhS‰Àe0O©‡¥d§¾Aâ,§¡ðSPÊhq¾mÚMNCN܆’ÊBvwnX©ºÍhÚó ÚŸ<‚º]Ídè$²­Ì€ìÎý¿:óC]»÷¯Tû“‡ÑþäaKιªi+¼U4W£xxé„yë#“˜¾=¢9͹Àúê,=WjÓÌÕê÷cÚϤ›á$…ˆ>w‡‡>ßi¨ÜÑ+êwk2°Ø[Ìè[Ÿ%âÓÚ‡Š†zÍi8±óÒõx±@ÜI²¨þ‹·Ê^Ç ®Ê†:Cp‘™JÇ“–柘 9«.FœÛ‰À¶Æ¼ÇèqwοEW:Æ£ýrúÖHÉÚÓL‹.3¥ÂØÖ~?8  ØÈåÂí¾=ø·4>‡›±(º?¸€ùª6¼Ö{Ïá@M9iõ·ÜÁšZ¼~ø³xþ¡=ð²ÅYø}irÿùò»øÒψ?½ù/ø‰xÞ9 R¸b¾d\ˆ#>JuJ@@HŠãµkøÒ/~ˆoÿúSa‡ÏmiÆó÷ud‹N°Ö¶{±úW¡ø­’‡Q­Y¾{¹ˆÍ¦% eë†OzJ­ßüÍß\>Œ€ IÀ›³7ÐòÁEb²¸qwÙdo;¿ ê†SP¿Ø™RÑ”bb~¹ãTXÎoPØ!%‰ˆfÒ«a‡ ²Œ¹ Q.`y¼`]åkÌOÍgNy^©ÎúmèûìoáŸý-©/þ‚¨KSøÖGâ‹ÿüc¼Üÿo¸8ä5º-eÖõ]°ÃZÇ…S<†¢Q¼šÄ»“ŠDNñvXú»ÛÉ PáMÃØA#Š~ÜNF{z-°Ôž6ìÀg$ôßšÁÐx̰,4ñÐh oübXרÓÑÑ`0hÙØvñâELNj¿Ç&°ƒ–{m²â´d¡×!¦á¨®t===ˆD"$îD›B½½½xôÑG ì`Ú¡˜Ÿ™z¬‹s9ž¶Bâ¬ùl2³“£›v0¯ÝäÄmHáKïþ#„Ûiê篕 ìÐôðƒ8òûÏâßÿí«øwõ8òÜÿL`"[J–$Ħ¦1>x s£ã¦¸: ¼õ6.üÍ÷ Üˉ#¿ÿ¬%°Í0hؽ‹À*%fÓ×Éݸôž¾~°ÞÝ’…MÓ¶d;M›‹–xÈŒ9‹€„ØdAû‚&£îBŠÇÌ­;êo¢‚íºË’t™ð Ãá!°Cû Ä9É®¼í1¿ŽÃC2—ßšÅ6’àáªo²´ªõ}¾$IEc–æ?~媆q(ÿ”˜`zH;LÀUnEë› wÄxXÛ9^+Y{ÚÕåAÃçr¡å±Õ¶qcƒ×ÐñÅ£ªò5Ëéáf,ŠßûÅ9¼rà0vóuËß‘ØRÖH©,ôà¡T®SŸÙÙ†Ï55ãôõOðÆí›E9Ë„(àÒä.MNdÇØ ?‚5u訮E°º>8@¥@ Œ‰+ JÖÉÁ¡da šàÒø8.Nãæ¼5;®·úüxþ¾…² ŒŠÀe)'ͨ>VÏÂ鯯FÓêJ€ëd7—‡ÍªžžSó‹Jit~ôÿbxïÿRt§‡r»u1< •%ì ˜W¥²3Ý,ùÙj IDAT»ñ°".å;HŠ‚¸)˜FQ€H&— ÔZP€‰yQ:úˆ]`‡Å—È>KH ®>(>ôMÝñ!÷¬u~ìÎÝ—åp°!ëüppK£zIO|tÂ+?ã% £ñ÷‡Ù%î.\³"Mù¹ß,Ú€ ^D*-BV4äc2ì@Q€ÇÍÂãf@Q”æôšaJãÏÜïņÖÛ4ä3^puÈlv€–¼výë ês^zé%KÇ4]î˜ÚCä§ä/”ˆ¬=qy "ʯÞÞ^|å+_1žÑ¦…[gGâLN{ÓÇyS¸:˜ÓnJ: …_tr(79½´Ø‹Ö{Ñ´§N/Ù¸—ÈæÃ“$!>3‹xxV×z¼µd¦«CÝÎf|æÙßVµJûõêEMsh†!A¥æFÇMë'@ÖMdäÃ+šÓQ4«{#æ  +2%ÀƒÍE)æ]x™9}î-û휓˜ :>¥úøíÒq}»kçø˜öô Úvg8CÌUtݼ,|Y“I$7ì9n>d›º¤¢Ö’æSCê­/<Üè{WW¸Jý 6Y_ÒÓ7´5|‘‰ILpУØÔ4ª›¶Ú®ß3GAêºí‰ýª‡d$ª:Î9èá—ßÿ.ë±eó àÅwßÁ+Ÿ9„Ýéº5ÝJ6~* ”…—7(Дƺiyï“`A%X â^¾âÞÇqxþ¡GðÌÎ6|kà2fÂEÃÍù(nÎGñÆð ÁíA[¥m´VúÑèò¢­ÂˆÙyš©Õí¸"ŠsÉB:»%”J"”J``bCóQôÏN#!ZKT·úüxåSð±\vLÜ„Û|;¬R¼ ^ðºh eí½¦™À‘u èéé1ç Xƒ2k‡Çþþò2füÆ7¾a‰+ETJ£güW8µÃ¾ ¬""oA`ó&T³a+Ú‚29fÔæ… % Puid(à% n–UC³a=P)õ²ì°j±°°rÒ–Öž\>*>øäƒ¶dÛœÑyM[;¬õY8Åg …‹e²ðƒÇ…€“KÓe ;,ýÝëbáu³H Q† )%9;N,ñ4àX KA’”E‡˜Œ”³ó  ˆ+Õ¯øÉ2(šM,Gƒ¦)p,–¥ó¦S; ß1e ;¨©ÿ:1ˆó†&bˆÄ3êÒ vXâ*ræç·1pcV×óÌ3ÏXú  …péÒ%Íého+(G5yð,ÙË$"«ÃÎ4…|ë;šÓõööàhC‹À6¤ÈtC‚RÒS߸±V„(”Ì, È¤“¬%9½7ȉÛ@B!u;›!âÞ@T.ÊíÔÏÇæM[À.ð<úŽê5H…Ôñ…£ØýÄ~Kο²¡•õu¤#hP<<‹t"ajžƒ?{GW:ί#fµ2s}¹É²Ä’÷ÔàÈF=9)99לÎSíGËãú1â1?=£þË€›„¨ÃáALg ,,¤×êðàòW‚u9µ]tߌR©µ?OÚtð5Ô#>5eIÞvqxHEç¡HÖNˆã©w;ØúÈyú á÷µ/~£9O•îúÓœ ´Ó§yl¹<€À–¤]íêòÀr\AàÁ[€Kƒj'-qæ\.yîYüêÌu¹K•ü‡ ?Çÿü>ïl+ô + DY†¨È °ÃŠ^Pp2 8ÚB<â5¯@©ÉÌò4z>™Jb2•\t€Xìo,‡¶Ê¬£P[E>nÉ5³Æ;±,àD\ÌXæÞOŸÛÒŒ?nß·X?*Â2q@°BC©9ÍivºŠk+é¢É#Ñ=uuu¡§§%­G?¢Ñ(üÜÚFFFL'Š¡ææftww/ûÌLø¡w꣢Ao.DïhkóÄ:ý;ŒnÙtfÐÁ´2•Ò”cœ° a=m¨C¼Úçú…zgd n£¯‹)Ó˜è.¥‡–î¤Ïɇ¬ÛCð¡n=×púö'%™ÿVÁ[¬®ÅÁ-Ùç*ÆH,T\7F5𢄘D(™ Üƒ\ŽåφvVíNŽÓÁä?>÷“»÷¯gã® jÒo"ØDIÆÐD ¡¹”º4jû‰žëq­ÏºX<% ÷Ç7tue¯×‹®®.K/½îtõ^òÀiñ} QiC¯×åadd½½½–_»DD¥POO^|ñEãm ظ89x#ö&ˆ³"Bá'¡H<é(+sI寠dfÊ. 9‡¦=íh=°—¸8••r CrÎÜïÍtuðüøÌ³¿cɦ·4à¦y;¹nuö3¥×ݸÀvËÏ™ÞPówa‘Õ06%ñë5œOzæ¶®të4Tn*Cd,¤úø¦`»¡ò -˜]s KgÓh©gN. vðPX…ˆL1ÞçÝzpŸmê’ŠÆ,Í죫êo«ýðV¯¿XtìÊU)í}ÂŒ› ÖS…ŒFàalð:J<YÛ.»ÑúœÛ¥Š®my¬?~Û²8ï;ñ4†àÛý—‘x8ƒU©âØÊIŠ AÊB…^á(PÀK"$Y‹eóh°R¨)'à¡THY„% ÖÔâõߏ»wpúú'˜LÙlKˆf³î¹ŸvÕ3Ûwãùû:ÛË4Ø¡ïou•Q~ ‡—)Þ¢(Žbà ËÇÊÒçó‘ß"¨··>úhÉëqöìYœÁHÂÚw=…40ÆÀL¯ ~Œ·¬©ÅÁ-[ZkÜ­qwXÿ‹ÂyIB(‘D(‘}fõ98Ôz\¨u»àspö€( Ǻ¨11Ë¥Áôcíep\3­¯­èÏyò%£3IŒ†eYß·v²×4€þë3¸teRWH[[[qìØ1KÇ¥‹/brR{ýì@V n–°Ó•ƒâª ÚOÐ×׷치ˆ¨œE`‡"NdŠ!)éio²8+2”ô1±é;J¹»8TÖ×.:84íi'»Á•­Ò‰$b“Óª6KÕª—ÞÇàÏ.@ào°Ì¹œØwâilm¿ßškº¡•õu¤CèPljo®ÛwGÕöÍÞ$ÃVdJV˜ÜSÿF=±ŒNw‡–ýACåævˆÏ¨‡-¶?ªÁ·˜ô¥[âÓ³šÓêqx`8Ž\mÚRÓËSdmAŒÃaÉyoô![Ä?‡b2í¸RZ¶>òÀº›FdLû—R\åP´ñ)ñT¢Y(²¶]ʦo” x€Øä´­h~έŽhõVàßÒ€èĤ¥q~ôéc¨¨«AÿÎ>·ówï .xéýðÅݦÅLVd$ ‚ »mdd ,MƒÉíL¹88ZÐØQT”ƒR“Î.ÎY!>¨S«Ï?n߇¶ŠÀb[•ì ¹;|`Öéîн£9M[[ x™éÔ©SèëëÃÀÀ@éÞh £»»ßýîw5§=~ü8"‘HYĺ¹¹§NZ<çèFË´÷P(ú—}v0o2¥,¨Ó¦=nÂ09/ʆyQ:®Ÿr€–~¶žãƒ’=&àpâÔžÇqjÏ~ôÞþ½7¯âÂÔXÉg—ÉToܾ…7ngw~b~ÖÔ¢­já}ëºDi`‡µÄ£†£ó`zÑù¡Öã2Áý¡°ƒj¨ È°´£hƒÔ\û” Ÿ©Î[)˜wœ0N Ii¯·Öºšq?³ðÊ5žðò÷ô»¨¾ð –EºÜÝõ~ó‰¬@ݬagŽBýïšÓõôôà¨ì‰DÐÝÝM`«&2Å ”ìÔ7oœ1%=U¦®&´›œÎeìâÐúø§Ð´§M{Ú‰‹QÙ+1Al*¬y³bU÷s“xëmLß1%¿­í÷a߉§-quà\.ToßjIÞ›AÏ#69mz¾zÝw´ÓW”s§Ë^Ô&<,ãìX)J1¶¸[‘E± 탨ۉû:(W‹g'¦°z ®ÀNîy.â„ë*µµ'CàÁè•(ýâW‡Ïkzž• u†ú¿™ŠO[K­GÆBRê©Ùm{Ö†ßÓǪq•æM¤¾:Íãìð‡ØýÄþ’µq:‘€Àó¶¹QÖ2.¶<Ö¡ÚåÁHœ÷þÖ—Pߺo÷|Çðù] M ;Ù‡WöFEÄØø!)2Ò’I1÷%\F’àf—¿ã£fœ­@©ÊðA£žÙ¾Ïß×±¤SP æY;lrQÜEtwðù|$èe¦@ €ÞÞ^<úè£%­GÎá@-ôFÑÝݾ¾¾²‰uoo/úúúÐÛÛ‹7ß|Ó²r~³zw‘F;Øó‹ ;(Úó¤Tžƒ%p‚br~*ò´1ìàd¤×ÛÐ`ü8š. ó2 P03/C‹ï}éJ6­@âÚîÚõ ºv=ˆþ¹0z®öãô­«°‹.…B¸ʺ7¸=ÖÖdˆºZ4z<Ùs¦5ŒåE€V#ÊrÖý!™fǡ֫×ýaƒÂPyŒ@…@Á ØÁ`>¢$#Kct&8/è) Þ{PÿŸsïo½oÝÀälJטð¹Ï}Á`ÐÒq' áÒ¥KšÓÑ•rTo’'I²•„`ªöBš|[³ËÛo¾‰ááa´´´6%*KE"tvvš²yåÞ®ùËe ;ÀÌ»í´7y,[W\ÒáÈávYº8ÔílFÓž]ˆˆÊ]²$!›· tx~ÁÕáSòã\Nt|ñ(Zë°$¾ÚTÖׂfÒ9tö§Ù»ã¦çkÄÝÁY³“4ŒEëñ ð`sѲ1à!3wWWº–ýAÕ;n¯¥t< E–!¦3HEÕÙ¥W6ÔÁi`!¹àaišœÓƒU5ïÐvÁ9¤Sl“TÁ›’RËá5xhz´Ý&ñçuÃCj¥Rغg} °ÄlÃïkÑHs.0ž*ó&R_­fà!:1‰Ä\Þª@ÉÚz><‹ê¦­¶èw¬Cý"Ý­í÷©¢“ºÁŽù™Y´?yLnÆ¢ø½ çðʧcwºvùΛj®MYBZ’,{ ·  (Æ»>Ù…úa'ÀúgÂ8}ý Ì„7íüØZ±àêà[2nˆ¨(g^› Hm¯IvPuoB³`(+¢ü ƒxå•Wðâ‹/–´½½½èïïGOOŽ9²îq.\@ww7úûËÇŒññÇGWWFFF,/«{«Õð,ì{Žv(Jl6ˆ³ƒƒ^xX'?7Çê(_c½ôœg)a3úq1Ljœãƒ¸àø°Fè‚UµèýôSèÙ{½·®¢ç“~Œ$æaM¦’87‰ów³ïµˆÚZkkÑèuß[¤nØa­ßã‚€xDÀpdÁýÁ“…nG÷‡2„ òÓ!ýù6€–×_”d„çÓÇx„c¼zˆAëqZÇuJÃõ´Ðµû¯Ïà_ ë¼^oQÜ.^¼¨+S½o<=’•¨$ô+ú}í!ˆ?ÔõüŸs@$"*'™ ;8v¿0î2‹‚bëìÈÀON›ÄY‡¤äô ‹›¦£,©q(â|Y5We}-šö´/N¯‡ôa¢ !Y’Ÿ™E<< y½z j|ðúßzɈ9Îëu;›±÷Ä—,YßÅ8¨nÚJ®qƒŠM…-YOjÄÝÁÌu‰e,K¾ä'ÀƒÅ.µÀÃnî@v!2 Í5Áè‚oQñ·rˆŒMjJ«ÕݱRys“¿ÍÒ¦—©ÈÚ­úÜU¤æÌÑš‚Ù"þóÓÖÓìc©ßAÐ w‡É¥Þ›”ñÁë%uyHÎEPY_§ 6°RN¯éDáÝ$¼Uø·4 :¡nÌž¾5‚­í÷k®”É@ày´?yN¯o÷|iƒ3 AÀ‹ÿúþ8ø)bšUA¢,ƒ—DË_Å)¹Š;, öøàP 2»ºÁšZ?}¡d§¯‚‹¡ $D›A .NîjDZ­-ËâG% ÅXÓ&Åjû‚" øUÍC Mv{ R§@púôéÒ>|÷÷£³³Á`ÇG0D Àðð0úûûqöìY —]|ß{ï½¢”óÝÝ_@§‡…%lØ¡¢lx.T)âfQ¾”aµ10v7Ë‚—DKß{¬“Ÿ—ÓPv€b^ü‹‘FïüµJŒ,æÏ2Îþ\¡€Ã‰î:Ðý@Ç¢ëÃÙ;·2°“V‚5 DNb½Ø›;¬<^”e„âI„Þ+\NÔz\¸+Ü,€ÔÔß(ì@•â%ϵ„²D9ëäŽñÏóùÓ¨-Oo_ÍO5i¾mŒ§¼ü½+º¯ù^x¡(®‚gΜÑ>%8ª@W>¼ŸÉb@öµÅTí…8õ6 i[@ÒÓÓC€¢²SîU4j|\yÁÄÉ úíÔI¼ JfŠÝøDN/@ÃSc€œ)›frz=hzøÁ,äð齨¬¯#}—hCÏÏÌ"9± tHÌEðÁ™aú¶9›’q.'ÚŸfó¬…×¶óüð‡% 65m—š¡U[¿³Y5ð06xMðÜsÁh=°'þüë8ó'f ôðŸõ.ž8Uä]°.Èx‹öÖ”Râ22¨©üŽþ8ø)ivÈ)àp"&d²Nëäçáx×{ge–ƒ®óT´/`·[½J;,ýŒà”³ç/PYç‡5¬ªEïgžBdogïÞBïÍ«¸09fËan2™Äùäj¢-àG°¶mö|iØvXë÷H:Ȧ..–Y„j½.me©Ô¦µ+ì°Ö¹Pë´Y±`èh¥íŸL#’È ãçuùhéwzúj¡¾S(-sïï½oÝÀälJ×õÝÑÑcÇŽå9drrRsº ;Å€$ô*ĸÁTíƒþMÉ¢Ñ(z{{ÑÕÕEÚ›¨,´ù`ââ@ütÚ¤®)Û»:˜ 9$n—Uó,ö¢nW3é¯DRbF@ljI7^)çqãÒûºwã_K[ÛïCÇŽW›K–$̌ܵ$oâî`Š,¹ð ð`sQ²þÝ‹3:ÝZª3‹/þŸQßoëw·èŸ¼RúKåekq¢È©¢¡^ûçr’NmôF%•²E=ÜÕħ¦LÉ«²¡•¥'´ãÓ³¦ä£È2¢S¨Þ±z!ý>õÂÖ=÷ƒs¯íŠ2ü^?„”v·ÖWŠ6êc}uš‡èÄ$sKnÕÊN.œÛ…TL•äÖöûqã—ï«:vú–~z›ÍC–$Ð ƒº]Í8ñç_ÇþÏ¿Dl*lø|_ûø# mâ¥û÷‘Õ}R² ’Šu¿¡ØhÕ_Îñ!øàã8ÛÞŒcMÍ¥’8wçFG0™*r¹µÂgvìÆ±-«ï‹¨„‰®¹ŽV ØÁÖ zp!zGSš¡¡!ƒAÃeà¡4 èëëC0ÄÈÈ H™èdýôì| ÖJ×ÀÁÔ/Ï´2sËQ»›>LŒsña ( ~‡E/J”{[ý3 —Ç-Ê,7½yQ&]k›vXyþ9× YׇµîNtµ>ˆ®¶1áìÝÛèìÇHbvÕJÂËqhóû¬­A°. @ø86zØvXyƒ7~1¬ûz~饗Š2nœ;wNW:¦öÐyº IèuôÿšCšx *õööâ+_ùŠ9¯l ;È øé´IÌÌ,”Ì܆ì(Š8%qrì*”ÌLÙ´IÝÎf4í¹9mdåvÝW»H¯†?ÀÀ[oCàÓ¦äǹœØwâiÝ›ªqu0Ws£ã–8†”“»ƒ …,ÙõË5 c#œˆ”œƒœŽk¿Ájk6ä´ È2øù%Àƒ†ÔMAý;Ü‹Agº,ð°ÒP}ñ¸´/4áœ.r•m9¼^Óòjz´½äç#Kæ§ÍyL,¸DPôê/ÏÇ>ºª:Ÿúf)>= ÅêQHñHEbp¬q“6öÑUM® ë¹ÍL #2¦Ýrœq@sÖ@GÍ‚õÕjvy¾ÇbÁV§XT®RÏÏE¾ IDATœ¾c8.¥‡Š“ÆÌº™3Å\p¢ì`‡ŸÑÈBåHt‚ÈUmɱÁêZôì?ˆžýÑ?FïÍ«8{ÇþðÃâ³w4Š›Ñ(Î/±8ïÈ9@øýh«ªD£×“í{E}<ÎäÄ3†f£šÂŲ¨õºÐXá†ÏÁé[èn5ì•Ç…–þ­Ä°/J‹Î q^D<%¬“Æ ì`†£ÔŽMæ=æÞ1úËH¤D]WBkkkÑv€×íîàå+²0„Ý1U{5@v1yOOéD¶TWWNŸ>mÎ5bØ@d ßH§N:Ÿ¦hÙÆÕÁœv+GÈ¡õñO-í¨ÛÕL:%ѦQb.‚ØTRÆZØJàyô¿õ¶îÝ÷×’'àǾO[vÍV6ÔàÉd‰щIKòž¾5¢¤!îËdÙâ<”h9Ið È"„Ø„ö¼Ú¿îcµJEb‹¿GÆÔC•õÆ&4Àƒ$Ü{ù.¦­§›9›tf›JÒáâÖ¹xz-Õïn)éù[áîÀ:«þ6~E½»ÃÖ=÷ƒs¯ '\ïÓéîPµÝÒ8²¾:ÍÀC2Edb- %mÿä\¾ÚÒîP¦•’ÞÚ~¿zàáÖZÓgà”N$ f„e@Fzx»ç;üÙ;ƯAÀ‹ÿúžè|¾b*ÒꊅwMN†MQ–—cŠx¿Ÿ€Å)/ßs‰|‡cMÍ8Ö”}0 úg§q3Eÿì4†bQ$D¡h}¼ÁíA[EÁª:tT×®íä°xcC[ :(Åë_Údw‡ ¯§§>Ò”|*“ÑúŠÇãðù|$%ì§â‚%Ì›PMƒíyÚvÐ\®°ƒ¥˜WJ1ï)%ì µÛvГÎõ ¬ïú,À5Kà‡¡ò‚rÏ` |ï}˜—㬫A[ÀŸýYå‡ã²PˆšÅæ(pŒÖãW>‹"FcqŒÆâ`iµ^j½.\°K7Ùѳèݬc(‹Ž±ì°èܰ9ˆ²R¸=ÕÂZb¦ÇÝÃìùŠÆâ;™ÞßÀÀYÝY½ôÒKE»öu´tÙdq »ù¢+ÅUA´-¤ìíí%À‘í‰DÐÕÕ…7ß|Ó”üJ;(¶ÎŽ ôä´I¬­—%5QbW“ÚLNCN܆’†œ¸mûÐ×ílFÓžˆˆ6“dIB*6_4ÐáÆ¥÷qãÒ{ø´iù¶?y»ŸØo‰«ƒÓëEUÓVÍ›¿ÖÌÈ]È&lê¼–ô®+•»¥Hvm¦~«2&ÀÃê@)÷“ÈÌÝÕ•î¾# •+ "Ä%˜ׄº¶Ce‹:&NI¼<ÄÃÚ_ÆW5ïÐt|©wOß’ kþ°¥èØIq8Àp$ÁØbY§×c¸ÿ•Yî|,¾9VÀ BŠÇðûªóÚ¶ç5?OÌF0þÑ5Íu£9XŸµÄl6í;þ\@à‹GKÚægfK<ä®+µBu;ÕÓÕÓ·ŒÙØ%#‘5‰ë£Ý€Êú:¼ûßÞ0|î AÀ·û/ãæ®^hy ˜µ>Þ(¸,m¡û•ï#s®´THP\Òºð 6dÝNâÁìø'šàf,ºø{\J%u;B´Vøách«È.Žé¨ªC[e>¶Àƒ¬LÊÐÀ¼Å·ç¶}GlØèžB¡iy‰¢HZbèÁò3Nto݇î­û`]E,¹îņÊvZ#°CQbc9ì`Ww‡"ä¥ë'§nœZéî`ØÁªcL€Œ¼zHdI¥Idç…{€ƒZHAËü¢Æ)ÃèqšÇ“ŽC°ºÁêõ_FÅ3Y"ßù-K¯PšêOeh É]œx6,‡À†ÆpECéï?A¯ö9xØxÊA]]]¦}ÙK¤N¥ut(ìPŠ›*AX‹1 m$ØAS|m ;í“¥†`r^f@ ” ë´Þñv€`rþ F ðƒˆ¼.tÁêZô<~=b8>³#·pvä.„ÆËvf™L¦0™Le!ˆÁìg‹DUÖ ¢ÑëFc…'CZÃiÖ\C>ÈÂîv.ŽEÀå@Àí@­×•Ýä /T`sØ!_zµc´J'Q–IfÁ†H2ëâP0-òý]1bPû™ÐCk\¸š¡Ñ^þžþ燆†œ8q¢h×ô™3gt¥+w²H„Ýú“Uøq(RJWj<ÙEýýý¦núÁ6LõþòtÈôA‚R²S'ÏxE(ü$”¢.t4·Ý”trôJr(©;E~ÕílFë½h=°u»šIß#Ú´3â3³HÎEŠ:LßÁàÏÞѽ}-q.':¾x-uXRg_m *ëkɦØ)H"ž±,ÿÁŸ–—»ƒÍeÙ:|<”´XˆñiÈ‚öÚm{ç6¶‹d:¾|'d-® •õºËÕ;YG =u€Àí‹§ŽX©º¹p¯¿ÀWÊpz=¶©«¯¡Þ0ðPjw‡XhÚ4wyaÁ"EÓ Vì?ü¾zà¡eÇšã‘â1öÑUíc(Í‚óo)Nÿ­Ü¢xø4Ưakûý%í ñðlɧσt"¡þúÙÕŒ‘¯¨{º=¢x2<¿®]û“‡Q·³gþäÏN$ Ça`&Œ¯þª²gv 5€¬¥…Ü@ƒ¡é5××X¢R–cï}/ïP§Å%gêèeŠ0í¥'‡ HtéãUò2ì`Ú£ÄðkNWÜäטâqÔÖÖ’›`(=?~.\ ±XÞztoÝW"Ð!7þ*¥)¶˜7U޽cª˶9ì@è“fÂvÍkÝ¿i€ Öw“Ô…ùÓœJödd]òÀ-¾ t?Ôî‡;ɤÑ71†³#·qvä¢û.bP£eÄÂg^ŽC[ Á†š¢>Ï=b­ñA«»ƒÊEí¼ "$ŠY÷‡iÀçäp;pg!–¡—÷Ér„Ôät‚ˆóâi‘dñ´ˆ8/¬”v TŒ‹FîÇ´Â ß*ÆS^þÞ$Rú!öo~ó›ðù|E{ö¼xñ¢öp¹¶‚b+l:"‘…‚$ìÖJN AŽß‚ÂCNÜ ,¨¼pᆇ‡ÑÒÒBúQÉÔÛÛ‹îîns`ƶé·,†à@÷vÚ¤šI1%=U$W“!qJâ6äÈ(¢=Ý!—º8´Øk«5JDD¥˜›šFr®8ûˆ'æ"øñÛ¼nj¾Í=‚à®»îÇØ¸áE`kƒ%y-<ŸJfFîZ–ÿàÏô»ˆ”ÊÝ!;±Êvm²~«2&ÀC‘],eæô]ØíŸï46¹¥3‹‹sâçÕ/Z­lÔoó%fô¹3ÈEÞM–&Àƒº8å¡EÍšG½}Áéó.Û¨»‰Ñ›Ò¸Iî©È=‡—•"c“ªóÚºŽ»Ãõ¾w!¤ÒšëÆù·€¢‹3ݱ¾:P4 EÖÖ§†?(9ðN$ f°ŽÒUZÁ0-ÀCt|xLÝæÃ³¨nÚš·.'þüëx»ç;¦æS©ºßÿëCxÊÛ¤µàÅ€cèÕ»¾o¶…õ Ȱ –¾/sK€CÂÈg`çáõÖ‰RÊ=—9»ˆ(€_8lÄ6ÑDÊäxM l4 ™–Ïo>›E;+ ¯¯ÝÝÝxõÕWI@,ÐÉú=èªþ%Õv}±;Úûë,ý°O•(´VOBå;X3;Ôʔ# `f^”I× ¥£ïmFØæO# —S ðƒ@åõ'Ž7ïÂñæ]õYôÏ„ˆ åëþ°T AÀÀô –¼górÚª*¬¯A£Ïƒ¶*?Úª+ï9A¬oƒ°ÃZÇÄ3₀хWw.Ž]€8øœÙk–a6È-ǘ;ˆ²¼7ä\DYQ7Q&][VÞ{¨uw0vX8¦û•÷ps4¦ÿ¾øäI´µµí:½xñ" íÏçtÅý6qÈBAzk$'nBIgá†Ô~Âô2úûû ð@T2õôôàÅ_4'3ÆÇro+‡L$0%=mÒÍ § %=EL”]»É‰ÛPæ¯eÝl(ââ@D´Z¹ÝôS±âÀIÏ£ÿ­·U¯R+ÿ–¿pÔ’k›fø·4èÞ|•H½fFF-sx7.½§+-W¹¥¤î´NÆr–+bÇJ1Rj–ôË)¥ýêÚšá­66ðòóñÕŸÅâªÒ6-øÖãð°4M|zVsúŠíŽÄáA½œ>/ÒñÕi4M[Rž¤xp›pÃÒ|¨dqŽŽ…Ì™øS<Ä%»ó­tg¸Ñ÷®ê¼<Õ~lÛ³6ð Å%bÙÍE`{QãÊúê Ä´}0>x‰¹HÉo‚ã3³li(ÝM‰Ã¡éøÀ–FÕÇN„xqu»šqâ/¾Ž}ó/1úñ'¦Ää¯~=€·Îà¹{àM¹óK‚ƒaÀÑ̺§Z®rxo™b€Ôßá3 àZ Ÿ9 ½ÆÉÐÊò…XS‹`M-º÷,¸?Œ£ob }㘠c£(!˜šÁÀÔòÍF:êkÐVíG°¾mÕ•h¬ðd¯ ÆÖ;fű¼("4/"´ä–>àþÿÙ{÷à¶®üÎó{\<ˆß õ %R´L["ü¶ÛŠÄ´Ý¶2Óé0kÍLÅ™ÙænÒ³[eÍFé©Zkw“]u­g➪([r%S»ÙPݩݪ4U%w§;’«ÕM¹¥Ž%µPn˲ž LQ ’xã÷µ€/‘yïŹ€ç[¥" Ý{î9çž{¿Ïù:‹ðƒÛŸ“…ÏÍnœžZP¡vàœ "žYpnÈóà ¢r(@M½—Ì‹¬CšžýJaûò1ï~ïZE°Cww7††† }&Ïž=«þ$›6ïN‹ô*4XV;¡"æ!ósÒw s“Å¿9cÄp8ŒÁÁAÚ®¨ ×ÐÐN:Efjìl»ëÃÔÉvêµVtÚu©UÝ]dòœ‚”¼9õ…åÜ\Þ:l{òqt¿ô¶í}õ­-´‘QQ-(ŸÉ"95ƒ|ƘMýŠÁæWpëÒeð\žXº¬Û…¾Wb÷Ëú8rÕ·µÀ×Ô¸îÆÎTd”œÖ·=†ÿþÍmÏÙ´“Þ ÒÒ-Ÿ5$>®ÍÝa÷À‹•véG¹â*©+qw´"¿à.h°Qwh° ¢ÀƒŠIG]]IàÁjuèih€e!òÚ\FZzÌ#Ãóé r‰$‘´²ñGÓYéðÀç8D®Œ)N«,ìp9Œìœú`;‡¯6ÖXË0¶>¨xŠÐƒ^mÅ÷r>n*ð Ö2ÐÞÖíR4ñL<œª(o’("—LÁS¿eÃ2þ³¢ÓÃõó©—ŸMNà^*‰ÿùÉgÑ–] ÅØX› ¬m…ÔfsvÐ"‘2‹uÈè'IaëIA9|v'¨È:<„'¨Èjppp)allŒVˆ}³u/{1ØÔk‘;Ûí× ì@ø:vØà¸„@8-µ€¦Åh蓞?Fé1ÝôN¿w Eç‡Åã…¢ãÝ#§ËÌ#÷>àta°k'»v /äq&r£“EbÎç @bùÿ'ܬnÖŽ@ £„Ð|Œ¬øœE׆tžÇ‹Hs<Òy‚,+«Ÿžƒj ýºÚ~Ó¾ÜßýÞ5œûhBó3àõzñÎ;ïúÜE£QMk›w'`sYÿe­öRs¸É¢k?9÷7 ˆæm^ÌS¿þö Nj„½4sw\³«ˆ³a»á1‰kÖ(2oÕ[Ö+a <¬P4 ƒÖ˘¬ÌŽ…O¨°]o7u¥yÒªò…¼òÁ¯>ØZÑõµ Zwô_zp\ê^r3v}ÀTÈåó"=]b°b¬ éih@zzZÓ¹-=]¦å{î>™{D^@!ó(ðdw,-‘ËêÆ¯rÖggGµM. vw{]l¬¯î‹ƒ[—.›OG;žÃ`c/·…r¦vˆp‰Ê.kdç]µ°ƒAý\‡Ñù^ÀªõR£°ƒ™n ZÊI,_²Æ²¬ÓæjFÐ\¦ Ògåâ¿Ådxf­cóè¥N†z÷`¨wÀ‘Tª?Ô0‘áyŒMÍblj§®ßo´y=KðC¨­ =Íõź²+hÓ*a‡réĹÀ-|6¿ )»v¸ö%Âa·-‹Ÿ‘‚0@šã!ÈÒœA’ÏJ§£ب=kPŰÃzçÛ°;œüÁõŠ`8vìŒþnddDÛL„OAœ¿ ÆÙ †aÀxwÕÈ˪šªþE°¡0¹09sgÁÅaÞrY …B´Q¦p8ŒbÎ¥6_Ø]P!ì@¸£¡Ã­S‹M aã|~ëß7)1~Írn-;;±mïãè{å`EqTTµªÅ?Í®ÿôCdã âÏ|ÿ×_Óe£WÖíF #¨z#WªÊÚçìø—º^#üã4ÇØ–pw°Iü¦k4ª¤ dS°ÛŸ|YÃ$·ëùÊ_.­v€ôÌœâó·?ÕWÑõµ8<¬2âQÕçûÚÔAj‰Í.§ÏWæs/ù¥¸T™åŸ¯­µê€‡dt†ØD5;/ÑÞ—w¿¾yá#õÑ oãÚ@û™ÛMî6—öºs×úvfÕíX'*È'rO禬ۭʊ,ÐT <ÄNUT¿\RÝË™¾W ¾µ?úÿùc¥ê{#ø³kã]qé|H)˜ÆQgµËZ_º^§6`‡6Ö‹)^y?uKèš}SŒ<*°1Ö”‡‰}QN§@e]?~ƒƒƒ¢n%ÔïmÅPë> 6õ¢Ëå·`+svÏ«ëÇv»ŒÌfªv ZF™`WgaØ¡Ò6¹ÙaÌÈæk3ÁŠ>Óø–ª €k¥ûŠîÒú÷±kË =¶C-áX £`ôÁŒÍÎÖäÜb*“ù;_âÜâß^§= ~„Ú› 6!ÔÞ´ì¡ì°^:/‚ãÅ" ±A::çÆ×_ÕGsÂBú+ÛŽ’¶fØxš ÓvXmÎ~4Ó?TÔ¶ßxã ìß¿ßðgêâÅ‹ÚFxn2WÜ8H\0eÜ`ì0¾îâOwggc¼È¡ªæª/B s–rlP£ÎÎN øô†*P¡€õÙ?ŒjÊŸ³a»im˜Õ<EŠÙlà!ŸÉ@(ð¦Ù¯±uÀCËNåõ5s/R‘‹†Œm{ûpø?þ)>8ñW˜¹7N¤ŽNß½ƒp,†ÿý©—ОYê¡ï©ôÔ&n_A§:àAo‘ˆµðïÀ…Ä}UçD£QbeˆÇãx¨…B¡%·‡'NûÒ¸Ze}Èae/al§]g×sIa"×!'lRØ© Mš ;€pZzBVÍ—U`ÆØÑÐŽÊ¥ÍbÙýABÑùAd6|T»¶lA×–-ܵ€Œx>ðìlÑâÁ$±îFg”2aÙbq.\†zšê‹. 2Öº@0*ïSÁ1«Žç Ê €ríMMà-Âî¦çQ«`‡ï~ïZeóçþ~9rÄðç'cjŠ\ …ÌM»ªÌµÕ¹Cx:»6O`[ø¹fGpúâÎtYôH™;ÅÝœs À˜ƒ”¹[õÕ}âÄ Úæ¨ ÑÑ£GññÄÒslý]Ø[šÛ¹Ð!ƒVŠiE§ÏœÉ@27 Y³{¸1÷MJÝ€”úrnÒÕÖýÂ3è~é9lÛû8…¨¨Ö‘Pà‘žCv>n8èpýü‡Äbfź]Øýò è{åñ<Ûìvøšákj„Ín§Ç`%§gTÅŽ©Ïq¸~þ‚¶¶Áºá²€»0Ý!FôJ›kpÐj™b$²­tЀÄsÒ1Õivì}¬änꪦʒT:Xé °‘*Ùå^Ь®”X”' . …u»é“¥R¾Öp‰$$Q,-ÍȪi ˧J·•ÚzÂðü&D!š´fãÉ5ðÐJw‡[£ÊÝX«$€ÅÌmõ^›Ã ¶¾Ý´vacݰ{sqUçr ¡¯¿fzŸ‘žÓÅâMÑÄ„U èPžÏÌ|å™\2¥šÜnÙÕ‰Ãö§øàÏÿw.L¤žî$øÖ/ÏãíþgðÎ. `ÒÎùÔÝa×—ë×Âõu—‹c—[ÿ€~A–àЈjqx¸}û6¹q'¦“ß*ÒñãÇ144„ãÇãÔ©S›¦Ü~» þlêÅ`c¯¦ç¦5ª†ª´?ªš!ÆŠ°ƒÒÝô)ì@°žMžx1ÏaŒ¸¾¬þ ;èë¼ &}†P»)ù€]~€™"±AQ.::0°µx®øYx6¶?„c1ŒÅjÓb,:‹±è,N…‹w7Ö#ÔÞ„ýA„:V8@¨¹_jŽQ2(9Fì°Qà¿•ahIæÃöåösò×+vvèîîÆ;ï¼cʳ‡{Ű †XýÞæí ƒñlìî €*}n’5²±j?¹0WUn jõ7ó7¤íJWÅãq âÂ… ÄÒtt¾ {£’M´¨‹Ã¦êÄ7E±i#4ívæ æ­{Ϥ<Äø5È©/ )Óëkrè~ñY¸¼u´QQ­#¡À#9=ƒì|ÜÐëê:@çÓût‹¯ò57¡¾µ™‚&)ŸÉ"95£ë5Âÿx.¯é\WËnËÔ#‹›®}Pà¡Jd“ Ë|ò¡¦4I¸;Ê€ée“ðú¶ÊÈÚJÁ P­Vn¿:à±Ûh#V)ÖãAÛƒÏåÀz<ÈÍÏ[6¯uÈΩƒ1ZzŒßÅ?—H!—HKK® N´;ŠÃJf.® TØ}ðÅ’Ÿ«&i?&º;,åÁß®xŠ–YzÐǪÚJ2eð°Ò%DÑñn7ê~dãà $lñrÉÔˆ¥jÂë­ÃoÿÉ¿ÇGÿïi|ôÿ&RWžÇÿú«ðÆ®ÙùX˜'õ’›³¾d‹Ö¯õÕãiÄXfZÝó*ñ†)-4a‡¼êû| Ek¤ºã¦°ÃÇÖ ì`ö#UÉq$ÝH;EL‹ÑùsPØAY>tL_U}hH€À*BfŠ¿/Vs™ûjjF¨¹yéïx!pl£ ›úÓ|@‹N5\y‡¼ê«ÆÆÈ­S9ŽÇqpSW´ªÐ™3gpæÌŒŽŽ’û"Ù"êtù1àß±ð¯].—† ì0š°Ê=¦°CÅÇnÚãd®»ÓR (LËÒ°ƒÆvbF>HÂe,ë½3p¹0°uÁb!ýH2µä1:1‰ &knÎ6öpcgq €×é@¨½¡Ž"üÐÓ\¿ @¬®sÀðŽÚ6Ä(ì#´¸;lT¥Iءұ”0ìï¼ózzzL{ª ´ óÅ}€ˆ’·ÏÝÆî. EP§bï~Ý+~nh8ȹE27¹é«¾¿¿GÅÐÐm‡TºkxxGE"‘ ’ãÙ ¶óͰƒ 1§•bZÑiã³Ô­çó1KÞ;9ƒ”¸)õ…©uÔ²³}¯@÷KÏ¢¾µ…6**ZÜ!?ŸÉzÝÌ|×Ϩ9˜|=Õüxîð7*Ž÷)%—׋ú¶ R™,I1;þ%$Q¿@~žãpu䇚Ïw·Zgƒ<›u7TÐÕJ†k±b¦‘Þ5Ÿ‹ÙyH¼úÆÛõ|ˆH¾J9<¤g”~×+›Œ Þð{áp¹4œCƒ·*•Èó:¥[9®…쬶ZÉè d‚.•.ÓÖàsœ*P¡ëù~xk>W M,ŠmØÆfáõ·£0¯Ž<ÍÆ˜¼þ:ú35ï倇ù1ÿ ÚÙìvt<Þ g¹/Òlv;lv»ª l =¨˜ ?œªx´ ¢DÝ/>‹ÃÿñOñÁ‰¿"fÝw'™À_ùo=±¿åër:ZëÕÔ{P ;è{Úƒ èT,1]ÈêV³\Ïœ¤m¬8Üètù1žW÷ÅàíÛ·‰®¤Ói [ Vü¿(!‰QÔtÕoAWý vw/? ÏÄŽÍÎÌÖ‘)¸4Å¥ñ¢Óq›ÏƒPGöï "ÔÑŸ‡%ëþ°á1ë´Fa[`P=°SÁµ+³Ô@$öâÿ§s<ŽþùeÜ™¨Ü)øí·ßF(2µý›}}â¯4¸Éâ´±àÌ‚3„ÍÓØoÙ¼iq ÙþTŸaù˧3HÏÌ-o©2Û06nŽ~TqÄç8<øô†¦ü±õí–il`»jàaq‚e6ðÀ%SD6»ý‘EÊ"ìéÚèÍ;Øz’l½¹ÝªHó–ʆì|œHÝTª–]8ügŠþü/qçòÇd‘<ÿþ—‚“xû‰g±%®ÃD›:;lÞú¢°Ã’‚N¯ês¦xcvÏàTº ­TÈ×j*ðÇÑÜÜL'½Q$Á™3g0< `‡ÛI¼ûýkÄ`‡C‡™ÞÞ{zzÐßß_Sëš gV‹NÀQêÆR`ÄJLj¥¿«XýX®%¥ï,ÿÁ¯€¨+ƒfõ÷÷c``€BT†+chhˆhßno|ŽÎ7:‚/ÝiŒ9­S‹M e›Ÿ(ÎEdÉR÷ÍlÐÁå­C÷‹Ïâ©oü–.;¸SQÕª$QD.™ª9Ðu»°ûå°ûåçÁÞ4Ïît"ÐÞVÑF§Td•ŽÍ‰çZOñ‡S¸~þCMç26œM;-UgŒÄ[õvŽê™8J¬‘­˜)¦Œ?·žQVÇÞÇJV傼rxȧÉŽÅLÑV_ ‹>Q²lÞ ,ÀŒrxDs÷É.H¹diw;ëŸãpë‚rà!°µ -=]k>¿9úøœzÈ…­o‡µÎîÐ6Ö »'1§nR6so™ù8¼ Óò¾¸(Z™‡ìüÚ@X¡P@*6‹-ÍMä^føêTå»1ÇN«›J?.o~ûOþ=þéýÀ…ÿûûÄêïRô!¾•8·CÏà)l „¢©³ƒõê‹Â¦ÔWÕàðÀëãð`g…jhŸ/…¼mxö–º…S8L,€%ƒÊ\Õ"äppÁµa ¾9 À·úøFvp§ÕÏÈ9eÈÖ©Öª*‹\]é2†”ÖUaN‹¤@ÊAS}É䧪Œ ϵÞîИ–Y°C©ÿ[ Ø÷\À‚¥ÓæPK3B- ÄB:á™"ÉÂ3³ýrá™yc¿L&­±‡³{XÜ,¥mË‚ûî öï .ÃvPÖç2„ÛºÞ°Ã ¸%|kò_>F&'Tܦ^ýuKÀ‹:rä¾õ­oÑEßêÞ¿¡ìe„6wy‚ñlìd×^rns¥ÿ †©³³Kÿºººh¥P®ááa=z‰D‚XšŽÎ߃½ñyrë^cN+ÆÔbÓhíö Aæ–pu0÷¾I©æ~YH™rýmO>޾W¢ï•´PQ©yvEéÙ9¤csDÑÐkë :Àî¯<¾Wè:Ô·6›‹EµVùLñ‡Qݯsu䇚Ïuµîc³V¨=#6e{¡ÀÃZY2ÒÆVÂÆŒO>„¬aWî@yàAPñ¿];ð x¢ù¦²® Ù¬niËRå ENaX ì£FÉè Q‚Wäp©ÒÀëqãÁµª@…Ý_,ùŒª&VÊj4%°þvÕÀ\?ÿ!ž;ü Sóέl{ÉãfÇ'àm<âQÑ"ƒeÕÕ±Û ÖíÏmÜöfîÉ# àaQOýNq—Šý‡ÿŒ|†L7•ËâÛÿø ¼±«G:Ÿ’Nù¨³Ãæ­/ ;¬‘‡‡»\ÜÖœµùþN|U“cÅÓé4A€ÃA—¨†.~ãñ%ÈáÂ… U]–N—!o+üEÈÁ¿c“ÝMò°C\àÕAÈ^;«1kùV|¬¬-Í “ §·xœÅaFÁdÂʰcbZZ`†àsÏ8^q€¿Þ´þ¬ ;”û]¶4ÆœVŠiE§¯jš…œŸ^pu°Æ}3t¨omFß+Ñ÷êÔ·¶ÐBE¥BBGz¶¸~­OïCß+ˆ t°v{žÿR÷ë\?ÿ!7ȵ{`ëÛéÍR®ˆž‰Óh’j~à5¸;°¶îÝCäú…2à@¹`èRòµhß¼šÀµÁ»T«:º;ˆ…Êì}róóªÏÙê3¤Þòé Ò3³DÓ\ïù¶;øìì¨â´êý%¬Èå°&w»'`)w‡¥~·¾…Ù{xu}Öäõ/ÀsqbYUûN¦ ‰âÈàm ^û2D‘ˆN£a+™ žÃéT}N =¨f ážA LXêööá¿ýëÿ?zçÿÀį?'–îé»wŽÅðvÿ³ØoD ÑBÔÙÁzõEaÓë«õbŠW$‘ÕÉNÐΫóW½ûÔÔÔ¢Ñ(‚Á ‘<Äãq477ÓɯÅðð0Ù/‰ ÖAÿ ÔïX‚jß½a£þ—|gÎL«>§ÇÝ _ç͘TµF Dv¨0-îçfƒH? ¡ãÍ‚0¬ ;À€ôµ²ÍןiÊýî,Ñ>4@]õ[ÐU¿ƒ»»–®Ïž^ ÂS³Oš³[f%ºt/ŠK÷¢øîù1t7×ãÐãÛ±¿» A]1`~£6RÐvب]V ìÀ((—–çX‚Ò9ï~ï.]#ã‚mEØaQ‡B(Âðð0Î;G…TT«ÔÙÙ‰P(´8„B!4ˆÊ ‡ÃÄøø8±4OØÎ7‹®4:-õ7§¨‹­kªµ·J‚œŸ†,¤-“%3A‡îžAß«Ñýâ³´mPQ©”Pà‘œžAvÞø½¼õZvvâÙÿMH°Ùí¨om¯¹‘6 JEÌŽ©;¸8…ëç?Ô|¾»µ×’õg³V½µ=§ÀÃ*E£ÑQR;$Ŭrx%B:¦:®çɸ;ˆ¼I(í.Á%•OÔ]>¯æŒNÚòÖ…[´F¸;H¢ˆØ=òd.ž,û“ŸÞ@vN¹…l¹þè¦Fw—Ý–ÛèAUÇåùøv¿ü¼©yÏ­pypoñÁ³Å‡\ ðeþÁClin„Ãåªøš.oúЮNÅÀCv>QñBI,ˆ).oÿÙŸâgùÿàÚO~J,Ý;Éþí/Îã­'öâpó mÁéŸ!ïekv°ì=Ù<°PtyP<À§Ùì­#;°3¶GëB^sZ‡ýÞVŒ© p¾xñ">Læeˆ‹ã8pA099‰‘‘üõ_ÿ5&''«ªýÞÖ%°!ämCÈÛFoî#ý¯>ÝhB} A§øÒ˜“… ®La‡Šµüq²Nõ/ëpï­ ;>ÇC  @¬Œ5;˜ X¡~HÂZ ˆÅMäïGÀåÄÀŽ ìèX:6’L!<5[!îO"+Â}ÛÚÞ„ž–úÒðƒY°ÃzÇn6Ø¡Ô1 .·'’ø“ÿò1¦ærDÚ‡•a‡¥µ|0ˆcÇŽáÈ‘#¸xñ"Âá0.^¼H]¨6úûûÑÕÕE᪪ÐñãÇñï|‡hš6ÿ“`;ßìS–œµ' 9Ðz¦*+1 ‰‹.¸:˜/³@‡-ÍMxâkÔÍŠJ£ò™,’S3È›°v5tè{åZvu’ïÙíð57Â×Ô¸´Á+•õ4?1 žÓ?&ôêÈ5Ÿëðµ@yˆÙâ†ØŒ…Íå£7ÏDQà¡JÄÈ’LZÜ”ÜM]‹H¸+°òé <þ-šÎ/h̃,U¶˜p¸Ý´A,Ë[6o©)õ»žÖ[uÏ×üýIÈ„ H.™^÷ùùü§U=ÿ½/®ùvæÞ8‘|æ’)]8ö¾þ :ÚñË¿ý;ðû¿÷>û›âí'ŸE{. Ìí¡fÜ ÔŒjÉA¢ŠÕãiT Lóú•jÀß©º\áp˜ð‹ÅÐÓÓC˜šy² N/ étzé3aP‡Ã8{ölÕìbê·»–À†'ü;è^w|Ó¯ÓgÔïô»èðÀIZ ;h;VVŸ&£° fÀšî•L( î¡%`‡*s (L«’Ït‡*É›Òó4”AQpµl\/Arq]-1€Ì²¼ B”ùÙU¿]þ-ìíZJ§èþ° @ŒM‘ukÕKwbI¼÷ág€¶-ìïâÐÛÑÓV¿P_²MT쀂V|¿?üã[8õ“[ÄÚC5À+åóùpèÐ!:tF‡qûömܾ}ccct©AUòûýK® ¡P]]] CUŠD"$Þ'Ûƒ¯ÃÑ~ˆèÒ~óI¦E§õLµÑ½“%È…9È|Â9ÊMBœ¿ 9gìFH}áÉ×~}¯ Í‚ŠJƒrÉÒ±9 :¨ªGñ‡SÈà†ûÉ™Ÿ ñP»³©žY7NÛî)ÆÓ1vl®b ´ÝåcsÀæÞƦ_x>#Yv#°ž‰Sà¡´ÆtZ-SŒ,BfŠqa^ýÎíu~¶’q¯Ö B?PÖI¶‘K¤4åIEMÁÜ«ƒµÓ3s´µWô´.*çT¢T"¯~ðØþTŸ¾“ÞD ¹D’xº\ª¼{KjjVµ»ëY(þÙÙQMycýí–nÃ6Ö ¶¾|ò¡ªó²ñ"ŸŒ¡ëé~ÓòÎs„‡³èTãÞâƒÃé,9¤b³ØÒÒ÷–ÊiV˪¼*v Ê²øË%RºíBѲ«ÿìüwøå÷ Ø¹B‰ÆfcøÖ/Ïc¨wÏÆn4°Þze±l}1›îžYõ.eÓ²–‚å`‡¸À!àÐc øwà/&¯ª:çÒ¥Käæ Aûn ¯]-@ ‹ÿ8ŽC<¾þxvöìYŒŒŒàÎ;–.[¿·þN„~v¹üô†ëµöŒ¬È#/‹`<6ì:àÔhâ¾êkôx*)ì íØ„T_WØ¡ 'žZò¯@±"ì ¶[ÕÙÁ çR®F|¶²~Œ„ÊÁ³XùÑv&`Ù BV6„ÚšjkÂоLjs…¢Äø$FÇ'qaü¡å{¥©T§Ã÷p:|mõEøáð3;ô{ ²WÈoØxš Ó­wÌÂ}ˆÎæðîߎaì¹ïEÞzë-b¼Y ƒKðâ¢ÑèFÁqÂá0‰¨¨¬¦ÎÎÎ%džşԵªšuâÄ ?~œlŸkwñíwaoT°É3§cj±i¬úF"æ å§I0?g&€¬Ç]Ï?—ÿëEݨ¨4*3Gr:¦*^†”fîŽãÖ//còúM]Ò¯ øúúkèè{Œhºt¨¾6žŽé¿éË—×®ãî•Ot½†˜[ñ}}:Vº}.öº†âO— ÁH¼%ïo4ë™>J+ 6‘ƒèðBâ9Hù´êó{¾H,/!«m±PŸãJ>¯'­«ó- ´µWølN·´Å ‡Üœú>ÚåóêV¡Àcîþòéæ ë>ww?R7AØMÐÝa&°ºØú jàÆ?¾f*ð\2_sãÒß ÛÚ1s·tþüÄ$Úï­øš._*R] ù'ó’œç8H¢¨Û‚‰u»qð[ÿ×ψëç?$·xàùÝjê}ª‘kµ;Ȭ_ Ö—–€ÞO³3ø=’ ¹2AÊ‚¬½ðZwÒ¿xñ"öïßO¤\±X Û¶mÛôóßt:x<¾ô“Shç™N§122‚‘‘dLØñE‰úw` ~uo 2¾)Þg…æÒª>¢ÁáFëmUÉ'Õ;M-:“Åü„þãÏq¸úƒ÷­ñ|/Äy?GE—v÷–âOWñg•K÷‰ªPëÙ¤¬§Ž}{Èu eŸÕ€ÞÆâÎ"™¹¸j—‡|:KÂ&‘Èó–Î_AC YKO—nù™¿ÿ@“ûÉFÊÅË;FÄD1_ùd¤ëùþ¥ç¥n]øHSÞœM;«¢-Ûë`sùTk3÷Æ8…@{›iyÏÌǶ47av|¢¤ûJ.•—JWìòÀjØÝ›u»ÀsæÅFÉ=ɼ úî”Õ÷Ê´ììÄÕ‘"'77,ëöPSNúãK’ ™@ìëì°Ma뵯 «¾›æÉŸ—ÒbͬGSš‡ý;pAå®îx¨\Ç!‹!#CP åF£QŒŒŒàìÙ³–(à ×ø¦¼³›á³˜J¯áçœ$`»«þ‘ÏÏ̪߽§ßÛJ¶ãfL¨V#¡Zƒí2]°SA›4vá´HÀfÕ;0&ÀLÏŒ‘“ðjÊýîXø·òX!e.Á&„‚8úâ^ Dd£‘‡øù½I$- @Ü™Iâ½ÑÏðÞègèn©Çágvbÿî |Ç£Îjž}5€€Z'FÅ3Ã(|nH€ºçXGW¯×‹cÇŽ[Z]+]7 0‡ÃˆÇ〥Ÿ¤ƒw©jW+¡†®®®%ÇêÖ@UëÆÑ£G‰Ãc6ÿ“`;ßìæ¯Õ+™Ö5•Öû&å!sÓ%s×f2Ÿ‚4Rê îٰµ¡oBÿ?ûm"TT$‰"Ò³sHÇæJÆßè- :P)žã0;þ¥!׺òwï›òL©·%b.þ±AÔ5€õµ” ìBƪŠë} <”Ö(€ƒVË#r€Ã«i‡ðÀÖ¶’Æš:ŸuvyOÇ”¿4¯[ÈO.‘R «Bc.tzᵳȈÊç)Ó|‘‡×ÎV|}–±=rßWŠ“+Û­s°©Wð@J‹ÁþGí/UÓé4¢Ñ(b±˜b‡ÕŠF£ƹsç,S. 81¾)ïì²_vXTNó¨·»–_Š$Ô¿Üîñ4Zi·æ T °éûDa‡êO«ÜÿYÀ¨Ø¤Ï€à5eÂù-SÿÕ;”«gváßbš–iã[S šŠ„£Eâç÷&ñÃú|A]©îÌ$ñݳcøîÙ1¼ÜÄ¡'·aoÛ²Áêz3vPÓ¦I]»Ò±¼Üß+\†r §~r‹è½ôz½8qâzzzèÔ¼Ôó @Y(bˆXý3‰`||œVà&ßïf PÕfW<ÇÐÐÞŸü­ƒ°·Ô¼äÝ<¢N´ž©*¹graraÞÜ,Jyˆ‰k׃ ‹–î.|å÷ÿv>÷m*TT$x$§gÀ%S¦eO^ÿ·.]¡ •a’D±2ëêѾÞ¸U•õ´‚(ÌÞcsÀ^€Ãׇ¯Œ†ûÓ¨"1²‰çTï»ÖKŠ IDAT¾HnÐ% ˆ…òé \>¯Šs´ÁK’DR•‰Ïêçæ!(Ø ~=iqw¨o×xàs’Ñi]Òæ’åûœøƒ¨*P¡¥§³$|õÙ?ŒjÊØ^Uí™­oGaö$^]Pãø'×Ð÷ÊCëËiµ“?ØŠDtZ7—-0B ½Mñ‚,>9…–]×K>cœãëvã+ÿú_âÖ¥+ûñDÓ^t{8¼³C{¤NSD ;;H² A– ÊD%® ”E,1oEes†a`[¨;Àa˜%8‚óh°<…4©ÇÝ€±Œº±÷n>޽u•ÏX[ù8œTð %@=“Éwyƒ¨Eq‡‰‰‰Š ÀZ C§ËÁ¦ÞÈa7¨ôßÔuv3¼²¹JBX <ÜW»&‡A¸æOÔBÕ;LÓê°£ð>V# @2-µúZ’iÕìÀ@çk‚à5)ì@äwvã_QÞ•.ÁÁ&„Ú›pô¥"1z¯èþ0òë»ølÚ¤À›utév—nGáu9pèÉí8´wz‚õ¥á‡õê$ìÀ(lÓJ4¹J¨è‹KýÍ,´%á[³x÷û×05—#zﺻ»qâÄ ø|>PiÓF@ÄjXv‰‡ÃÄw>§"¯E aÑ¥aõO**ªe9sCCCÄû6ÆÙv×€qo¥1æZßaÐbÓz¦ÏÇÆ2ËÕaU¥Ô ˆ³— ºž áÅ7ß@pw7m.TT´:dçã¦\?òÉ®ÿôCdãú¬-)è@UrÈEÌ܇XЬÊÌÇqu䇵33‘é„t ÀçpøšáðµÀîrZ5ËÔáÁ$E¬˜)„ôŒ¦s·îÛC,¢P>ˆJ ضÄ”™‹«ôØÅ^ñCãrÑ'Ä@i…[ŒPAC;lééÒeb»§ŸåS6ž,ûÑÏo«Jë‰2î3·ÕSÃŒÍÖß^umÚ±=¨Õ"ô`–¸UÀƒÍn_×å!53[ðüyÁ ¬Gy€e&G *ÄBB×äH¡U»_~÷ß–­{͞ǩ›7p1úoï{»ó-Uêö ,Ï2A‹ƒÀaõɺ¯\*<]–!bÑ‘¢LM-@¶¶EØÖ©E ;,ªÇÓ¨xøuf¦bàelë:y¤ÅÊæN!o:]~ŒçÕ½è" NA*ª ”Ïd‘œšA>cÎ{ :P™©Ùñ U±_•èW#?_áØVÖ"üàðùàòz­˜EÝi. <”éç­˜)›ÈOªßÁ½cïcª‚@7|pÖÒ3Ê_¤;Wä);G}°UQÀ(ŸÓÞVrî¢|m­ô 1P"oaàAÃDT Ø£x¤xÕ‚Ìg²Ê@N\2èçw§ÕÒÓYøÐìîа½*­šœ Û5·.]6xÈ-Xé­\D¬çòŠÍVì ÀzÔ-;•_/;Ÿ øœdàpë¾Ñܹ8ð°¨;ÉþíÅóøfïîè…/I¨ß2ä=ïÆÑA¢,—$ð’hár#Y*BåjÂn+‚vƶä ag,<˜tOzÜ ªÏ¹›WÜÂ]öõçË‚,A%8Ö"6Ò`S/þbòªªsÎ;G,ø%Nƒã8¸ÝÕíÀq"‘b±A¨¸N†‡‡qúôiÓÊó;»1ØÔ‹ÁÆ^êâ`¸ÔÃdÌ Êwô]éüsfî¦êv»೫ÙM¤V`ÂשØAÍàLa‡ ®c@ZªëS&ø<ÉÆ<ƒøëèî`Õô­;TãbƒìPê§C.X@ðØ€¸ìëÂà]€pt£w'ñƒOïâ—ãS°ŠîL'ñÞùëxïüu¼¾wíÛ†PgÓ‚óE™z)W÷›v`°T?é“#ŸãÜå ]îÏ[o½…ÇÓi¹E´Š(ç,C‘H‘Hä‘Ï(ñ¨:;;ÑÕUì'AK …¨¨Èéĉ8~ü¸.Ž5Ž­ƒ°·¤•¬uK‹Në™6§ EW1gZe!qúg¹IݳÀº]èÝÿ"žùݯ£qÇVÚl¨¨4(—L!›3tà9‘¯áÖ¥Ët 2Ms“†µÿëç?Ä̽qZéæŠT+&®² )¯~GЭ{÷Í h ”ŠÐÃÆ;Þ ÞñŸJ‡ XV¿Å¢$Vx¦Åi¤•°ÃC.‘BvN¿±‚K–ïs"WÔ¹•rwÈÌÅ5»;8¶We›fl°õíà“ê‚Õy.È'cÄ jcJ]ÎÊí³Ôº&¨ûâ£Äê%ŸÉ>R/FÈß¡ÿnÚ§nÞÀÙ/ïãíþgð”½ÈU°´ìÀK"ò¢¨ÎÉÁŒrõ~\ÁuD©¸3é‘úe˜¢ „ ¦D0 .ÆOÍëÏ{<ê‡O33]ÓcgŦÅBEéCm{U@ÑåáСCDê7.TÝ*>G4E4ZùX“N§122‚‘‘dLxJ!KÌ5uvÓ…,$Yùyu¶åy×™YõÀCȧfnRK°ƒLî:¤a³R†ÂêÎ11-Õå”5Ö‹çȕۗ<¦ŠahLߊ°ƒ©î&ô‚J¹L8° @0( BíMµ7áèþ½ˆç ½7‰Ó¿¾‡÷?‹ •·Æ{÷sŸNàܧhó{Š®ýÛlðËiì÷L·çWÁ3j+þÎñùy#?¿‡LN ~/¼^/ÞyçMèª2ßJïÛ"±úw`ÙAbõñz*W¢E×…Õå_ùÙJ¡ÔÿSQQé+=]OØo‚ñÐ`\êä@ë™J¿{& )Èù˜~®eQÊCL\ƒ4ÿ+Ýkktxêw~ Û·Ò`c** ÊÌÇ‘œŽé¶yízâ9·.]Á­K—uÛé^/Ð괷Ѿ§471‰ì|ÜkÍÜÇõónšºeYÖªY ë} <”P4 ƒ–Ë—]Ìj:oë>rÀƒÈ“{i¾:0553 _ËÆdž^À…ž¸<n}¸4Ô›ž’*ÜiW“ÃÖ:rõSà1wÿnõ#ò ™ÒýŽZw‡ÀÖ¶’î×7™»Ã¢œ ÛUpëÒS‡Rý [Û‘š™…°j¡æ©ßRñõŠö[ʃƒíÊíø\žh½-¯A_ÆMå²øöG¿ÀËÁv¼Ý÷,¶¤¼€¨2ÊÎdØA”%ä¡2ÐÁ¨rXvX¯~e¹X¯ËÎË¿1L|p0ÅH”RŽÄà“¿SÐâð•xLó´²êSÜ6ì ]Ò"_QpzÈÛ†N—ãyu ###Ä€‡‰‰‰ªwÎ\R‰Îž=‹“'O:ô{[1ÔºC­{+‡`Öw¬°-´a8J¬ûì¶â¿’“ïswYø…>H‹ÿxµ#m°C^‘ÕÍuŽâz5œRýìÀþúmÚ;n ;(‡ˆ–Q&˜ÇÅc,;TÚ&͆@8-SaRùZ'oµ#¨úLCÿDa‡Ê7v(õ“Å£D€ÀëÄâ±àþÀáÉY üÎߚį£s0[S‰N]¼…So-»>ì\åú ä¬[çôߚҬพх©rš[Fõ€bù;ï¼CÄÅÊÚZ F¬ç¡t]“ÿn›:,PQU·ôtu°·€#x°{6iíRÀÖ3•î÷M– ç§! Ó²(¥n@œ½HúN³nv¿üB¿}Í]Ûi°1•JI¢ˆì|©Ù9S@‡Ì|ãŸ\«jС¾µEõƨTÖTf>nìÀs~ù·G+}“ˆ5®Ž½©Úñz#  È\J¹E`ë£P‰,ŠHFgÖ|¾¦“ªx0 –$‘6F yk»y48<Ô[‰]þþÈ¢~mk½çY­»Ãîƒ/®ÜÌŹ¢Í6›­o¯ê¶msù`÷ æÔMî§0sw-»:MÉ7—L•ü<ØÛÉÏoBZh6»[Zš*¾«« øÙñ‘tx ÞÐ…—ÑmàRô!~/vCíÁá¦=@ZáÒDØAÀ‰<I•q÷D–eˆ² µ£Ó¢kÄâ]%lŒ ÌŠŸVS¿·c™iUç|šÁ+~¯ªî²9ÀÚ”¿XNxé=ØÔ«ÚåáÎ;¸}û6zzz*_w¢Ñ(¬¢¯97nÜ :„Ãaœ<Ål ë|Coâû`I–‘øÊ]Œ,‹eߟ‰ºè±4XúíÑçßn+‚v0°36ÓAˆO£zà!3ƒWüÊæèv0pÛYÕŽœTyàËÑŽçT@ÑåáØ±cDêwbbÂÒÀƒ ˆD"˜˜˜ ’^4ÅÉ“'qéÒ%ÃÊpпC­{1ÔºOAƒ´áÖQüé°“‡ˆ,šm@}]1Ÿñ 6›’b9•}@й¼k›ª¯¹¿~ûšÏ8ÒPØAÛ±Õ;®F‡ûɨ¼‡Vk‡Œi•=ÞŠŽ ÷±Úa¾â²[aÙQ¢Ú,°ƒÞý‹º?äQ„Öi’][pôÀ“8zàIĹÎü:‚3ŸFðþ¯ÇMm*S‰¾û£1œüÀý1t°ÁFà J`‡EG‡‘ úƒÝÝÝ8vìˆŠŠŠŠjsKOW›ÿI°;ÞÜd®ÔÉÖ3•¡÷Œ¤«ƒ–,Jyˆó¿‚”¸¦kí-‚O|í7ÑÚÝ Öí¦MŠŠJ…„äô ¸dªfA‡–è|f¨TµK#a‡±¿ÿÀи)«ˆaKæ+ênëA‡òŠ[1Sj‡Ž}{ˆ^_âÝË8ÿ”„òéŒ)¶O›E’(B,Àz¬ñ‚¨Éêš¾X¨ÌA"=­.ÀÑå­#’o>Ç!Öµn¸dº,¢ÖÝá‰C%Ë ÕÝÁÙ´³&ž7‡¯6Ö ‰WöL^¿‰Ì|Þ†€)ùÎg²k€ 'ø :˜,Šu»‘Ï(™T×àî);–¤[†´°Ó¤—Nw’ |ûò/ðú¶x«·[â>3§ñk_,H8Q °‘ëXo‘$.ŒMÅ©¸K†±ÁÁØTÕ(ämÅéØ Uç|–Áâ:7ĬͮÊÕá‘…ŒPyŸÔåökr°8wîŽ9Ÿ¯ò¾!N##X®-NLL ‰@Ȭ†‡‡122‚LFÿ}¿Ý…Á¦^ßñèrù×hE÷Ö^üÝ¢/NÊÊã²y   ú¤ÍÝA‚Œi^ݪ‰õ€el O]CBCÿ±¿~ÛšÏ|6çúƒ…Ôkåã='3²±÷ÞwÒN$ÓR ¯P ™V-ÀZ"âe'˜-ŸÕ:ì ¦ÿÑâî $½Õrp/Ô¹ @\q’,/OG àqbè¹^ =ß[„>]€>5~È䜻6s×&ÐßÙ„¡ƒ»ÚÙØ+4·_…˜åߣs9ŒŒÞÃÙ+º‚ðÆoàÈ‘# Z+‡ƒ~•JEEE¥TápCCC#Ÿ¸Ý ÇÖß…½ñùMP“p õLeÚ}sòÓ@%KUE)sâÌÏI߸¨Î§÷aï¡W±}_±8*ªÍ¢EÐ!;oNhéÌÝq\?ÿ!fîé÷^¦eg'ú^9 Ëf¬t¨]ñg(ìùd ·~yEóùÎxö_ÿþºÇdffŽÅVü[ú;3CfÅÿ%; ˆu’!/‹é[ºò²$ð †Î lm[×)AÓÜ^0&Xdþþˆ¾–Æ¥]Â%QDüAT{Þyó]ølÎ:k/æÇï#;7_ìœ,wí4|x¡¦:•–Ý]•׉(bn ÒS\ª´[‹/ vçKåÕ2î7G?Ò”¯ZqwX*O`;ò3·TŸwûÒôý5SòœK¦ho3ìz._*àÁ,„7Á}¨eW§®‹Øtnâ>.Fbè±=8ܲH.L+ {?¼vN$É2…ˆ]§z›eÈàe%ärî`lpÚºÃ=îFÕçLñ$%­l$ÈV8[0KΕç;-à³;+JãhÇsøonýXõy###"ó¢"A(²L{K§Ó¸}û6âq2ËÅp8Œ“'OâÎ;ºç½ÓåÇÑŽç0ÔºG‰ùÔ"ààtÿÕ‚\lÚ`˜8H²òsYƆ‡{©ËžþTõ5½v¶¤Ãúƒ…+«O“QXâî 2Ùô…éj.G $Ó2ÂdZ•|¦;ì ñ3UçÉ:¦OaU°Ã†å-“ŽVØ©ðÇÂ?F$,Àå»Ë€Ç‰¡ç{1ôB/â¹ðÃ5óÞOŒÏâ¿7‹6¿C½8ôÔ¶%g„Òõ!+»OFÂ+e€ðíYœ½üç®Lè^ÝÝÝ8r䈥Ö]V  ŸŠŠŠj3èøñãøÎw¾£KÚ6_7;Þãl¬á¤­g*Sï›,A.ÌAæ¦dQR§™Ó7PtthßÓcÚ÷êTTÕª|&‹äÔŒªø’¢ ••Åsfî÷~0þp cÿæóÙ:¾ö¿üOÇó>®l³ù©Ï‹›dΣÍb~üþÂÏqðÙѲ[xˆq <”WÀïX-S,Ë"ŸW¶ãb×óä_P“ è lÝ8X6Fjfq'ñ\"¹ (‰¬Á%’5ר¹Db vŠ4jbâšw›g]-‹Dž·lLšÀ&£3ºV‹¼PöáëT¸¬”sw¸uAðP+îK}º¿…Ù{Uîùd }¯0ÅVR, xÃ!vVÝuÔÀ3÷È9<˜±¨­kð›Þ†3÷>ûgëïã­¾}x [¼QŒ¥a‡¬ÀSØÈuªl÷Ååe ¼X,ck³ë?^´±^Lñêú€pf ‡vÁ`lºÔCZä+›zqôÞOUïø>22‚Ç ‰ÇãˆF£ƒæ¯Ð#D"dÖéétÃÃÃ8}ú´îùî÷¶.€ûVMDœÜÎÚª^ÚaHª|V[غ¥¾1œ™Â…Ä}Õ×$ ;ˆ²„‚$AÂ2æ`ìpÙì•W«ƒéf„ ;¨;ÇÄ´TקɰƒÚvLÒyAˆAã5ôLŸÂkûR°ƒ–t÷“4ìPêvžçQ´×[~x¡C/.À×"¾|n=4e&5•Èá»ïáäÙÏpøÅ8üÒNø¼¬ñ°ƒÖg›Î^™ÀÈ…î<0æ;ˆo~ó›Ä`u****ªÍ+Ý]‚‡`o9X£µG!ZÏT–¸gRUçê@0‹âüUH‰kºº:´ììÄSß8„mûúàkZÞx–ŠŠjcå’)¤cs¦‘OÆpý§"OèÚGè:¸¼^4lë  C kv*ˆëU{½_~ÿïÀsyÍi¼ôß}‹èæåm `D[@bêóKnÅßÍq‡¨Ѩ†‰Ý䙯KÒ†Çyeê¬GY®,ŠÈÎYËlƒK¨Ÿ@È¢dé¶Bš$#’§¼þ»¥óœökÒê'²ÛBOT6‰N¤ž™Õ²^êòL„?WœN9w‡Èå0øœúI‡Ý¨)w`lŽ"ô0ÿ¥Ê¶›Çƒë_ ëé~súÁd ¾fcvÑqzÔÝs3 Eå3Y"–Ÿ¬Ç­h±ì ¨ßuƒ±³Eò0Ùdßþèx9ØŽ·zûÑž ¢^Aó¥Ó¥Î¤®SݰÃÚâË(È" ¢¸àú`‡0`òµáÜü]Uç,z*-x+J#àpc°±§TîúžÉdpñâE:tˆHY"‘š››áp˜³|å87nÜ æêpñâE¼ûî»Èèübô Žoÿ øw¬X€ÇYt>°ÛPó’¤*Êle°P„½”ÊcsÀçX†¢N<¸ªéš¥û2uå!#%À‰¥¾PäÁ0 ¼vuvV[µ1˜Öì ¸‡”ÞÇÍ;¨*§n šÊ¸É`­.+vÐð»a‡J]@è|÷ŠÏò(:?”™nÃÚJ$A$!’–®%A èä°Ù`HåŠÐƒåEvpÙìÈK‰ ¬.Û²eùñû¿Ðt½õÜÜ6»¢a&Îç×…Y«ˆŒM›ÓƒÞ·°âceminxœL8=PØAõ9ê‘©°ýT’Cð`t~žÊ¯ØáÀ‚0B¥é“ªs=?³ìàpäØ IDATžºâ|A–?—%`åx*‹¥]£´^WOØA·óW998åeç)ëüÚÖ„ÿâ%œø—/áÌXgÂã8õ7 ®Ï…'p.¼>|µÁf·òg§RØA^þ=|{g¯>ÀÅO£†¹9,ê›ßü&>l蚪VD***ªGE]®Å7£dZÏTÕqï6tuÐ1‹rnâÌÏ ú|ßκ]Øýò Ø3°-»:á©ßB›•I¢ˆôìÒ±9Cƒ·eèÀº]èzº=/?¯ èÀºÝt)è°Idìùd ãŸh‡Ù:þñYº^:w ¡söýW¿‹ù/¿ÄíŸâáµkHMM/ãpX6ä?lÄE(ð°Î3bÅL)uxèØ·‡øµE¡ºÉDèÀÊÁûåÜl6›©ùÒ»Î*M_,¨"lÝÝ¥éZ¹D ¹DÒzçR¥ÛCìî}Uֳö‹ Èmc›³a{M8ÎÀvÕÀÃâdÎ à(BF½ a=nUN9þö6$Nmx\%ÔoÉ }Îx7¥e]”¼´ÁØpµì†ÃÛ nês]ܦrYü§±qºþ6ÞêÛ‡§lí@ή1µòÑ‚$A&ñæ‘›NyI„ ËðØÙŠj èô¢õbŠWÈ~1ñ¥®ÀPty8*Ì8Úñœjà ëò7nÜ@(Ò}/nܸX,VqZét'OžÄ¹sçtËoYÐ(B›vÈ€L¾ 2JÖÙ¡ÁáF´°~_d³1hb=ËóIîp¸y²¨LóˉxI݋ЌÈÃcw€Ù¨ç6ÊÝaÓÁ„§ŒN•Æ6iì`sÔ”QÓ5§E2ß,ØÎׄ¶kÂÀú±š³ƒÈ\¨ó+mÀ7:W­#IX†,¿b˜_ST#ì€U°ÃêÿwaÙý¡€¢óC™.`0Ô…ÁPNü«"üpâ§¿ÆØ—³†Î¾Á‡7^Ú‰¡ßÜ ß‡²6¬v—ÞžLâìÕ \üõ¦æs0Z/¿ü2Ž9‚`0*****ªJ‰D044„ .O¼&\¨“­g*Ëß3Y(º:ˆ9ã³h€«CçÓûðäk¿‰¶Ý»PßÚB›• ÉépÉ”) Cf>Žëç?Ääõ/tv¿üv¿ü¼.ˆÚNÔ·6ëQPYSfÀ3wÇñ«‘U”ÆÁ?> oKsÕÔ³»¾;ž;ž¹D_^¹ŠÉ±k`¬û½܈‹Pࡌ¢ÑhÄŠ/€•6X=¤*wx0}í$J`ì6Ëå‹Ï–þ’…­ó˜ZW"ÏÓFƒ"EÀ¼è€PÛ r{]l¬[uÐùø'×úúk¦¸ä3YÀ§Û¬ÊãÍP^Ç]»I–Uʧasù–ÚžwçWŸ½‡Âì=]òx'™À·?úú›šñöÞgÑ΀<‘èÀâK™ÐHa‡M+Q– ð8œÕÄ~ÿvœŽÝPuÎÅä†Úö黢òD€‡Á¦^tºüÏ«sišššÂÈÈ>L¤<ét·o߯ž={t«3A‡‘.«Q8ƻヒ©©)]òÚéòãÄÎW1ØÔ[þ /à°o®;Wâ™*È(YØêí.dí’bé—â6ƒíÎzØVôxZÝ^oØŸÝYvr0¶ ‡™‚¤þƒ,ËàD»Cç¡Í(Ø¡ŠŽct¨¥õ­©V†,˜7Æ@dZ–†*yÆ”~&k,»‰õ£´1zÑ$ð@*^tzpy”ÝÇêy¶LyK ýrøÿÙ{óà8®ûÞ÷ÛÛìÌ;’ ¸X¤%‘–"Y MÑ!mÑ–bË6o®—Jy±®“zÊ5ýªr£$̳Þ}¶CºnÝбTõêY®qüêU"çÉql“¶¥P²èE’B I‰ 0XgŸé½ßXfÝ=½ÍÌùV¡ÌtŸsúœÓ§Owÿ>ç{ã¥ä&Q ¦ÐVÁå~¯÷žmªÁË?[/Ý%øAE ~á‡åÄB> Þ·ƒ÷ïÀØ|'~þNž{é¢sÍßÿÕœþk8|ßÞÛH˜3Vgå¶Ón6_"YÄËo'púÕë™Ì¸ÒÓwïÞÁÁAìÙ³DDDDDDµêĉxòÉ'‘N§-O›n½ ܦÏL°Îj…¤ž‰ê©Ý4) M\(¹ú9\D»]Z{º°çácó]»íìëãH$"ZGK C!™r%ÿ%С–Õê×S(ÖŠþ»vÐÈR¹;¤¦¦ñËü§šÒ¸ý“¢kç­uU×ê²Å¸ƒ­­Øñ¡ƒØñ¡ƒxû»ÿU½Xä1'2!ÀCu¥´z­P Ã@©2htlÛ\veõZUïå»Å9¨ExøBÞ³m+sQŒ{Q’À{¾ïH…‚#ù¤®' 94I¨ä"‘¸8\Ñù¡œ*º;üƤ»CÛ–¦¸àøÚ¶€O\4¼ßås¯`×}Ž—·˜É"ÖÓåH^F¯i±ž.Ýî ³£ã–ºdÈ¢äèC$#Ǻ$MY ”ùÛ¶€‹ö€O\€R´ç¦~h~Ÿ={õmÂç·ìBO1(µG$Ê.:85†h5C{†‡>‰„˜G·/lßËë+—Ëáüùó-¸ï9yò$N:eK»¶2~œ8ˆÁÎu€™Öào¢š¤ %àÁó²vXR·/ ¿Ì §ˆ(. ‚4‹N_~úæ}ÞÙôU‹ÜÖK„ñ­à+¨æîs”j°cÝÁšñ4)Ç`¹Ë‚fýi ']×a«÷!°ƒ¡sÑrÈÀ ØÁdv¥o¥+…ÙϨ*ããkåÇŒbD – :Ê‚¡Z9õ8;p¾›Ÿåô•}¹‹EŒEøA *=ƒìÀàÞEׇŸ¾“/;ãú0*âø¿ •À‡Û±gÛ†òÇ”à†eåžÊàôo'ðòÛÓ˜N]vuuapp‡"b"""""Ë´ÿ~[žsÕ—«qr õLT¯m¦IyhÂÌZ`܉¼mvuØ~ÿ=¸íÃDÛæˆ´o Ý‘ˆh3Yäæ\.<ÿ’áÅ,È)Ð!Ò¶4ÃNÕdr v8ûíï¢2ï2ß´ ÷}ñ±º¬s±Lü¬"zvá¿´Sà¡EQÕß„vlï·%_ÕB‡‡ðçWÅ‘-¢›²Ó3æêO’<×—––+MT\°ó…º?­ÐÂÕIÇŽGÈÊBAs£W ¹;ôß³ÇRw.¾±yÆvš×Ú1yÍÐ~…T“ÞAï®÷8^f!ŸësfÝ·P\ÿjæ©©iK¤"ïèêFŽõæõ¨:ðÅF:ˆCœ¿b¸?êU^–pêÝKxvt‡¶âp×{É­®·õÇYU#°C £™T¯ž+j*MCц÷0>ìwb(olžøræÅl=¦œ"–VZ·@f]N:…C‡Y (\ºt ²,[âô066†±±±šÓyùå—qìØ1ämxXú`ë&œÜþúý­Õ&¨@4„üÍsâjZ tà¥:*´}îeç 7V‘^©“¯³–4ØuǺcb}^Öš vÐ}‡ô¶£› Fê±Ö´t§f±{…à„—a§a‡zšÞWrUpåoMßöBx ܰœ¹<¸vQ5~g²XúÇÄ‚~Øaõ6n±xryø¡¿½'>{/žüÄxîõ1œøé[ºº`{wº2/?3‡îìÃãïD¤…+•oäã%¼|açGðòÛ äy÷‰" ‘]zòÉ'­‡êÂÕ¤ž‰êºÝ4š M˜&»Øl³«CkOö<üal¾k7b=]$蘈hå“)dfæ\ vtèØ²»ì³4îeµ"ímˆv¶“1§Iåì¯>û¯HOM›ÞŸ qßóäåfÅÏÏ{µhçʈÕutxà8‚PÞ¦… ú»¥Ûò<­vGm¨_`™7ç ñ¼çŽ¥²»CÀÝ:ïw›‡LbÖÑ w%w‡‰ó ¥³½Œ»\~ñׯ«˜fák"à¸ØFSæc¯¹<ð™,ÂqgÆs_À¢Îs"lÀjÞê±YUà9V£çŸ¿c;¸hø™w¡íq›YN_»ŠÏïØ‰lؤ8èü¨ÉÝ8;•‘ *1´©}÷F7>—™°H(§”,X–¾Y—xê©§ðÕ¯~ÕÒcF.—ömÛÀ²ÆomyžÇ¥K—JÕ>Æ=õÔSøþ÷¿oyûmö·âÄ–ƒx´mGõ  Û>Gõ” )– ‡º‘³°C¥!ŒOãÉ«¿0•Üîpç2W›òÇÂÒööCfu?'°ÃÍt-+ãÒ6u;P:ÚÑ Ø¡ÞÒ"°ƒ…Ÿ¹;P&ÏK§ÝêvXú[S\ðùPäæf¡†ÕîÕÜÖÝúÜL§£U>Ÿ¸ /οô¸JT¿.æ%)@¡næ­•ÒŠ}Ü»ƒس—¦pòïâÔË—mŸ>y}/_H`ðÀv¾ †œÇ˦1tÅ~ðB÷¼l÷n bÏž=äfÞFE"R DDDM©T*…'NXš¦·]ä@Ꙩ®ÛmY5ÙEWqÊÌ ÐD냹€»<ˆ]ö!ÖÛíè|DDõ&UQPH¦‘_p t{}—ϽRS°özrtÅcˆvv€õq¤c5©Ü„&/¼[S÷}ñ1Ä7oªÛº¯3‡ÇD€‡SǶ~[ҵʡ©/…¢çÊ$äÊín™2/@STGò©­îr†¶7rÃ)‹²³ÎyŠ$—uÕH]O u]ÿä»ÿžÝeÝ[Æ~sRÑx}³‘Pts]¦h.&3\>yá]H<.à,¬$8èÆÂ(fôY~sxHX<®€N8Z/†ÏùBhÛ¢¯Oú#m¼Rf ÂÌehª=+N øÆÐoq*xqø°~€¶™•øg¢Ê燦BÕ4Ð&‚Æ÷F7âé©ßÞïåÌ„­.)…GZ,KϬËùsçpþüy˃q‰R©úûûu;HȲŒ‰‰ LLL@®ÑÅ.—ËáèÑ£Ö¯và+÷âHï݈±UÆzŠâaÀßDe¥äê )uVpoÀðùË?DZ6w?tÓÝ¡rÐu€Ò7‡çh’ X”£¬¾_mØÁê逥åÜHËh:fÚ…ÀæêÎnÀkÇ#Û/ÿL€Œ#%ø:Ò©”ž×`‡²ÛhÕûEþ€ÏèëózÊÌ¢äúJD™®¹ÿÖìßÙƒ'?y'Nþâ2Nœy é‚}ï,ò¼Œ§tÿ×O.AQ½uƒÿÐCáСCtpHfàz"""¢FÐsÏ=‡t:mIZ”/ö–OxÐÕ@¤ž‰êºÝVQS¡I ÐÄ´+ÅQ’¯BM¾fKÚ½»vàý¿ÿ ´mÞˆHûÒ=‰ˆ*HUäæ›[p<8{Ic¯áÂÏ_B!eßX´ùÎ;°ëÀ>['õ‡Ãˆõv9ƒDä-¹;\xþ%Œ¿^›SÒíŸ|ﺫáÚ¤8¿àÕ¢u*#ò¤®ºÎ{±PW9˜¥Ó&à¡‘h‰€ÏæÉKæ°¿w.L…òÁÊœ‹V>²À×E¿t^/©c»þs3}=ÍÁÉB¥þŸ¸8l(]Ù_~Bÿй¡Ô§3 »Ñĵö˜ZMì·o`û÷8~Ãèhá‡ÌêÚÖÈÇn1œþ ÕXO—3ý2Ú6Ò1y âüÛò1>P Œ9=4ì`8;è‘¢© MÓvûÂØˆc„OÚïåÌ5[‡9ÉZè¶—‡cÇŽá™gž±|…Ì%§†±±1ôõõ¡½½2צ\.‡¹¹9K@ ä0qäÈäóÖÚbïwâäöG–­¢_éF¦É\4 ȼP‡…÷ìð?¯¿‚—Ò×L÷ÍR¿¬tÐ -hÖ0ð@QËýš»ÁV¥I˜d¸;P:Û±Q³i Ð7(X™V#À–:/Ôrì–¿™ajÙ¤$††5;`<«îka‡5ßië÷) ë8 ‹k¿3 ;¬þ{i ¯ ?”Y§¿½O~òNùÈmxîµ1<ù/¯c|ξgû^Âá0:„Ç뽉ˆˆˆˆˆjÑØØ˜%é0ûÀvòˆ«CÞȨÚ¬Z•"Ta°iᶪŒ³%W~Òò´C±VÜ}øcØ|×nÄzº\]¸”ˆÈË’E ™™Yð™,jI,¡* -λOº;Œ½>„ Ï¿TSØ‹;>ù‰:oû9*ˆÕ•ª· ª649h`‡‡@4lx¨Å@,<÷Ç(f²ŽœÁ±3kÕu3™OZ{©,zõª E³ð·mퟸ` ÌÑ+#àKÓúƒ% ì@¤«jÍ7âžH—aàá\f 1n_ؾ›™¯î`Pf]¦§§ñì³ÏbppО9 ÏcxxÃÃÃ+ ‡\.g ä°¤Ó§Oãøñã––¿•ñãÉMÀ‘Þ»×9›ÐÕ¡(éB)à°îäØá·¹¾rõ¦“}¢ï~è ,× <0 ò Õ@»†ÔRfNÀ–]jm„¬vb °ƒ5Ó(«+ÓrÂÁê´¼:Í­ о `ƒÀkÇZa‡åÿËMX„Nµ@ ^€–~û[uáæÜÌ Øaù÷ìâ@DÉõa•b!÷íÀàƒ;pò¥wqò¥ËxñâTÃMƒ»ºº088ˆ½{÷ZŽéS,#•@DDDdFLÜ–?ÙærAˆ‹©g¢ºn·õŠè²«ƒš¿eö@µ>žjûý÷àöD×öxLDTAK C!é^x§Ý ðcûïÇöî±5.‡ñùíl·¦ 2з³£ã(.ƳúBAôîÜáøæ&ìðÚ³?¬)ø¦M¸ë>×°}#?åÙ矎 àÁ# aD>_啎c·Ø³ºêÑàpckn2r33æËÀ{ǽ@*V^í— ¹·Ê†T(:ÓDoB<™Ä¬Ãý€‡Z&øoâüCéìØoÙÏ/Ÿýµ¹q.¾±©/>\kĤ±•w ©4RSÓŽ­ø¿$!W:íχf0>çn8ÓuSi'aïX ø!€ñjh.€ÐÆ;¡’à§/B•ì»¶é8½ÀˆСø¾?wÉð~/g®ápû­¶•kN.Z < vÝ“3oâÅôUÃûž:u {÷îŶmö¾ìäy¼MsïcÇŽáÌ™3–¦ù`ë&œÜþúý­ëL’Ø’«C7ÇI%+%ÐA”ëô¼;ÌIE|æ §˜»úTû­èö…tå E¥ãHJ<4ÐKÓ3>g/m–Àu;Pv0¶‹i®O­öú7Ó÷¬>ÿtøÛèîà„{,ø»Øa½cx@€`ðËoÛȰAR!«!ˆé$@’¬–Ýf(0 šX–¾ñ¿ÏGëƒ!(KU,ÊgKàÃÙ Sxòû¯7øðÐCáСCسg¹Ñ&""""ª;Ñ­·ÛôY]ä@Ꙩ®ÛMoUš0Mq!îC ÌŸƒš}Çò¤[{ºp÷áaÓžÛíì ]–ˆ¨Œ„|™éYK£4#‰çqùÜ+ûíP݃4à ҾŒ7Ròú’×W>Û EdgçÑÚmpV1“Erb².a‡p{;ý øBõ JùºsxpŒ<#ÀC%‰T=Ù·ÔO€°Tt>ðß +ú;̯kXÅÕ‚rÉ O‘$(’äùþËTŽjQ~!UÑuÃ.•swÑ»CǶÍèØÖ_~BòŠqnŒæ`#Í=™æb @ÉåáîÃsøfÒ¹>ë øQÔ mgW°!]¯ÃzëÀfØAÑ4ŠYS¡h*8šMÑð3 Šrv0tœšÉzqbÍZ·„F€(;Ôþw°ƒž}¡Å| |…†Cañ™fÀÇ‚eìPë6U÷×;ˆ’ž—ÁóËîýé °üÞgU:ª¢AUKyâJ(‚e)°, ÎGÃç£Á°Tõcð/þ(„ò—ý»zpö½ß.ÔøÐÕՅÇãСCÄÍÁCbYò•ˆˆ¨9ÕßßonܼåQ0ºPb9z&ªÛ63QDM\€&&Ý)®8eæhâ¼åiï:°ïûØGïëëãH÷%"Z¥b&‹ÜÜ‚ë Ãås¿1´ ¥…b­ØupnÙõ[Aˆ´·!ÚÙî˜kQu‰…"fFÇ Vˆ-u@È'SHNL:~ì©©i ýÛOkJƒ ñà—¿Ô°¨}ÍO%¼Zä1§2"OêÖ×8€Í^+˲W­ÊÞ>°É¾‹¶Å€BêºwN¾@4\Ÿv$¯¥€~†sÿ椚Ãߥ*2ïL@“R£»EÛ<ê´»ƒ¦ªàË€/s£W 9`ôßS~¥±ëo^‚T4Þ¦l´§é/<4Œ^‰òÂ;®”ש.@1“Õµm¬§“ÞÕwMšœFÇ€·.õFÜ,Ü–/¾ñ†+‰8ÅÖ¼Vƒˆö!’ #À°ÈËê‹8;9¬½­ »<ŒðI óIl Äm)SN‘À«2´u·û[7áó·ãÔÌ›†÷ÁÉ“'188X7íšH$pôèQŒŒŒX–æîp'Nn{Âë¸3Q°!RrwhÅ’«ƒVÏ/P]€*žÿ"ŽŒþÜ”#Ë’﹫<ÄPá2ÃRÆHXŠÆ.AU © ¤Eˆ‘FÉÕ!@³`–Òôì`$ËÜ4‹ÓC}À”Ž6´vÐ4 I„ (+¾“AœXaŽ+µN×< ;XU®*ek4çCŸ™Ÿì°vÜ1º/Ìç«*f¦¦QQr{Xr9¢?KÃϱùXD>ÐKÏkvd€þtªî¯vP™¬ITÖ¦ËùMdÁpydYƒ¬(à…RºøýLu(½ÑbQš+òP†—\><ëmð!cïÞ½8|ø°í.xDæDà""¢fÕ£>Š#GŽ 6¶j±¦8µøH=Õu»™-¢›®Ôì%(óçÕÚü[{ºðþß›ïÚp‚øæM Ó'œŠ¡µJ‰DbÌ©¼ð°¾ÆàAà¦×Ø <4²-ÆXç¦gé2g$ `Z[]?îJÀƒ›ä¦TpÆŽGSk»ÉÏÍúö¼·úÄa!ot'9 IDATUvrž_H!‹‚¢iËëAÈ•¯ï±ßè_µ8´¡ýïßS!ó¦Êå‹oÀµö$^Àä…wлë=Ž–UÈçüá0ëÁ ‰·곂¬f9ÎÖvM•AYøLÑ,üm[ÀE{ Î_”±7˜` |xš}‡¶âS·ì€?å‡ ©dð ª}žMÕvÍ=0 <%—‡Ç{î²ïa, Ûâ€ù'7}Ï-¼‹´lü†ûÔ©SØ»wo]õ ãÈ‘#È[¸bÌ—zïÆ‰-uL ­¥Öè’•è Êu~ ÞqvÈ)"þfâWøÎ´yW’ÝáNŠèÎ3ÆúMçå§øiÆðqœ™®OcÛØ¡‘`YU‘ù‡Uà‚¢ î÷ƒ]~OÕØj9Nʺ1¥éaÊäøa%@Áæ2˜ø¬Þa‡þV¡áZ*aiµ-Y|þø@Š‚ /Ù<"~¢!"~_ùútvÐs=¡QTÎ¥g©«Ó]úß*¥·:àˆÒY—‹¿5 à…EËQðq4!,» |¤ËÒJ‡"PÖÌþ]=8û•‡ñÜkã8ròWŸÍyfÆøÐCaï޽ػw/¹ '""""ò¤b±ñÍo~ÓØ]wñº¥"CƒfHÔ,íVc5) M\4Þª”ùsP³Ö/>¸ëÀ>Üõ‰‡ïë%«¬-?í…dÙù…†Z{º°ý{Ÿúz‰í!Ò¯ÙÑqd窻µvw‚õûm+C½Ã÷ý—/ kç­ Õ/ä2qlüü‚W‹;îdfxX_côZ¡X–…¸ê‚ÞÒÝAZËÁIY ÙžÊ ¸V&Ñ!à¡VÙq£YÎÝADˆ¹Âì¡ø‹©ÌÚÉÄõDYׇJzï¡ýû×䛯oø¹h¥AØõ,.Úaæ24ÕXÐßu€‡b:‹h§ý× . ß±E?«˜šš¶´Î$žG0ÚâXýÇzº0{ÅØüQå³`BÖ¯$Osºw‚‹vC˜¿bÚ1|ã%K8õî%<;:‚C7á£]ýèZi)bÂðlÄÝÁt?®±î¶â؈c„7f§|:9b+ð0'Ðí [šf GzïÆÿqõeSû=zÏ<óŒ§WÌ<}ú4žzê)Ë`‡VÆçvÆþV°zÐÄÂÒi-yDÖ)§ˆøç¹Køúµ_šN#Ìpx¢ï>C—˜eÓ\žÀ°v°¸þ)»ÛÞù—÷š¦­;¬(¡†¤ Ü„,Œ¦c1 `eZ;PM.ÔO½ªFða&[¼ ;Üx ”Ü|~À(m¸¸_N‘Ep Ž–P |0;T;½°…u¡ QTJñåÓ]ý¿? ˆ–œ ÂåŽO–4Ȳ‚BQÍPhC,¶LYB‹ÇÅר£¿³Þ½'~ôžüçב.¸(ñÀÜ€ˆk@}ˆ´Q³ËÀ4C¢fi7+ЍÉÐødVe/ÎA™yš8oiº­=]¸÷3ŸÂÀ=w6}ð±,ÈÎ-€ÏdA³ BñZÚÛÈ)Þ¤R¹ùäæ,YhÒŒœ:¶lÆ®ûÐ1`ÿÜ4à ÚÙˆÍDÆ¥vð…‚ˆßÒc[&&QH¦?v«`‡Û?ù(ö} ñÆBu-ੈž}—>ædf$ªÔc ¢ûb´jµ÷@K”«~V Žo±«WÕ²Y^êr•ÚŸÏ•2iŠê˜,x«?—swÐT驸#öÜÜ*’ ¹ =qþ‚î4¸ ·ÜQž¼þÆ%sçc¤\uVÔG‡á•ò'/¼‰çÁœƒ—$ž‡ª(¶¯:A3 h†qíÆÖ«rT«x- Å Å¡’àg/Cì]E1/Køþ•|ÿÊôöáÓ›v KnŠ´½êÙgÉv0ÝwA¶`N}(>€§§~k¬+N'G˯¤næ¤"dMKY{^<¹éxnþ] åg ï;==cÇŽá«_ýª'ûÃéÓ§qüøqËÒ{°už»õSˆ±:ÆíX¸<4ºŠbÉÕAk”—ªÞpwX‚þtô§5%ýxÏ]kA©u†È€ðr]Áu¶mýUóÆñZàõa‡UiiБDl¨vOFÕV®u¿sÂÁÊ´< ;˜ì›–§ÙrÙx]ªgw‡óT5 A*_皈|)øßV=k•“©‚>Ý­p ]¹-kí&aEQ‘Îåë¡R½øÂ%W2Y¨­ Ê|¦ª …¢ ÎG#` •y\<Æ"Ê:>yø6 ~pžüç×ñͽåÈT@õ-–%¯P‰ˆˆš[{öìAkk+Òé´þ»A1Yc®Mx¯5lfDÍÔnÑUWjö”ùs€j-4½ëÀ>ÜóŸ?hg{Óº:¨Š‚ìì<²só +a–|2 >“s$œÈ;’E ™™Yð™,,T0ÚBd<*=°´oÞh[êvøÀ^ÜñÉO4\ߨ[\ô®Ãƒ£ˆ<­óXƒèn¸UZ#qÒRÕn„ÔÊ7@‘ãcnzñ͛̕EQ!dsð·¸÷‚C*T¦ßY—€!—­›þÄø8KÓ+çî›]€*˶2ÓkÝøLs£×t§ÑÏžŠå{å¼á2Ñ\l„8Õ,í6 ŸFw,lÜí¡Z[–ÐHär"4U3^?\ Y@Ì[w]_u}‘D’¤"—“ ±EPËœ4”T à)`ÕkX؇ƒ÷bðƒÛqäïßž‚ÕêííÅŸüÉŸ`Ïž=r¨sR DDDM¯={öàÅ_4v..€òyÿN\4C¢fi7;Ѝ©Ð„h²KïJUÊü9¨Ùw,M6kÅþ—Ï5µ«ƒX("˜Y7È7;7@4Bœš@K ƒA×KrtØ|çè¿s·c ãóaC_oÓ;ÈxUza‡ÖîNlˆ/UɉI3ÎÇjZ ;Ü÷ÅÇs\äËÇU)¢èÕ"Ÿw23òSLKæ_vqa<Ñw¯©KL„ñÙzŒ¶\L ì`Az:뻎`—V3éÆPx0;:N—a£ýØÊ`~/Áº;Ôþ·M°ƒ‘t–×I¹mµEðA`n¾’Q5 “É:¢!ÄÃ}n Õ¾« KhºÒP‚ ¬îWJ‹õ4U‚4ÍhBUigêæ´2_Q(Ê…Y„ÂLéÉÒv4€°VŠÔšÓmOÎþ÷‡qòßßÅŸ>ó+äøÚŸå,irr'OžÄ‰'ÈMv‹DDDDvrhÐ ‰š¥Ýl,¢&ç¡ 3®¹:hrJâ'ÐÄyKÓÝ~ÿ=¸û÷E{ÿƦ\i=;7Üì<ŠÙœî}øLŽ ,!_@fzÖõE /<ÿ’í Ã®ûŽÇ;¦hW"mˆ«ƒG¥vð…‚hÛÔgyþª¢`vtÏ;~ìVÁñM›vE*ÿœ’ŸŸ÷j‘%ÖhyªAôŠãV®0ëë®êbPÛI¤?8<ÐR+™)³\ã /är®³T¬ìðÀøÝª¹NX~ñ–Ïô¿ÜìÊ‹‘¦ªÈNÏ•h–ÃYÏ¥ ùBYè#qqDw½·¿á å'ä“o\27®Å6‚h­Ì€ ³£ãŽ—SÈ9s#jdhíÑçòšJX{Ý%Òq+µ_´¡Í÷À×¶í w;Ãñw‡ðG¯ý ÿOö-LG’׈‹ ì`…dÕº9ÂÞ¨¹ëÚÉ™7l;¾9ɾùÖ“›öb³¿ÕôþGEÎå92`-ì°;܉óïû#ý°C[KcØNØÁª,WÏåÅ<Þ*Ìâñ‘Ÿb„OšN:Ìpøjÿ>SàKÑ`)Ú¶c´åbÚH°ƒîãð8ì ·m†€R r-n ÊêóNÀv»1¬[ÇÒ²ò3¯ÃfÝ)LçG`»af9(] vXþ[•b¶ô£Ê+¾›Í,ðÕ÷¯Ô.VÀËT,ÊåÓ1)0ào–^¬×;TÿLÓ€|^ÆÜ¬€"¯¬Ý‡Ñ€ VvÚ6øÁ¸öíÏà[õ;…êÑÈÈ>ýéOcxxDõ«ÕNëDDDDͨXÌÊàògãîÃs vàtm@´³ƒÀ•^Øõùлs‡åùË¢Ô°ÃÁ£ÑÐýD®PGÄá¡$<¬£D"Q7V»ܘ„X ? ×]¢Æ‡ìôLm÷jŠ Á¥É¼"ŠUWwcÒ£HRE:Íž{åÚnÎ(DÈå!WN"ÒS37Êgìbw‡¹Ñ«7\%ôhGw˜3~1òG@sdŬ²7%ÑÃûH¼€É ï8ZN‰ç¡*öÃD¬Ó½­O'a­ï”b=Æ_ÊËE÷xNŠfáoÛ‚ðÀýŽ‚YÆÿ{å]<ö›pbú¼é›"&œxÖL`פjÖ5ð¡øÂ gx¿—3×Sì™×Ëšjôc8¹ãÓûOOOãèÑ£®¶¿•°Ãç;oÇÙÛ>‡~=HдGªAÏeQæ2@ºPŠHk(yv¸TœÇ±‰_Õ;Àã=wa[ nê211æÙwYs²]4o¤«+Xº`JG8;€ )ÆëÑ0pà"ìP­ïÙ ;Ô§u¦M`‡õƒì ¦ãç9F?ì°ü»åàƒv.˜Í )jm°CÙm4m[ú-JJm°Ã’h¦=°¾õë·j{éësšdÓæEȲºv;@‹øµ5§z,äÃË_ÿ=üíÞ –±nÐÊçó8räNŸ>Mn¶ëT‘H„TQÍóê& ¾wü°›´žª£"B)B-ŒC“Ý ~V’¯B™> ¨Ö½ŸéزÿéØWp磅?jšÞ- fGÇqõü[˜¿:Ùd€¦™X."ï*ŸLaêaÌ_óèðÚ³?D!•¶<}7@šaëéF×öC ˆ9+½°Í0èÞ±ÕòøM‰ç13<Ú°ƒ/ÔØ×T±P(û9?¿àÕ"‡"}ZZ]†õùÀº´*¿éA|x¼éÛOÈf]É·š»pÁ  uá,ü!{„xËÍ®¼S™´~"ª©jY°!qQÿjcÁXÛú+ô/“o´7Ôß,2 ƒ\wxJî!vËÈ Z(®¥ñ|Ò[†NþpØ–:ñ’Üà…© üÕü _ú)~®#×’×çú@`‡†—bq#Šo5¼O^‘ðìÜ%ÛŽ1!Ù7ïÚߺ _ê½ÛôþCCC8vì˜+mo%ìð•{qrû#ˆ±:Æç ˆ…ó„Ò4 •æ³€¤4àzv8“­)ùOµ¿‡â¦/31&PMXó¶š¹4×ÝN³8=‹ÓZ±]c ¹Üb VÂVNñ¬Ìíòêvrð(Œ`¥û‚k®U>kTØÁL:‹¿;[‚ +¥§'oU Y€ÏJ |È--hdvXóŽñoÕ>7`¥ÏÌ@ 7¾£_¸ôCSÆÓYï|)“Ž,«X˜Q(ÈåÓ äøPæñ‘ß» ³§þŸÝ·V)ŸÏãøñãxöÙgÉ wŠ8<™qqhÀ ‰š¥Ýœ.¢¦Bç ']su€*@™> 5ùš¥É¾ïãÁá¿ùë¦ruÈ'S˜¾<‚«Co#;7_Ób‰¾P-ímdبs©Š‚ÜܦÞFrbÒÕÕÉtJq%Ûiß@:Ü:’|6±Pt<ïäõ)ݰCïÎð…¬Ý\rVqbÛrç ôÓ*@ŠWœ6 OëôéEz­P4]âU"¥ÕI&-eR±[ºº>­{ûÔÕk5ç)äÜqxª\¸ÝºÑ’ …¦ës²(¡˜Îܼ(I2ò +®^¨eÜdAÄܨþ>½moå@Æëo˜ Ðd#d Z§~Ĥ±qgÒ%à!m±=.ÐE¹‘,$ÓŽÞxÝÔøà‹o„˜¼)y šêÌœf†/âï.áömÜÛÑõ `€ÙÊ\= ;y]‡ÛnÅ÷MÀ §“£ìºÃ–2ÍIEÈš –²‡}?1pgÓãÊ›sD;sæ ¶mۆÇ;ÒF¹\GŽÁÈÈHÍiµ2~œ8ˆÁNm×BþÆìüÈÐÑáÆÕËÒA»¨È5ª(𠆢á£ihL•su v89ýFͰÃÑ><Þs×ÚÃ4 㳦j¸ zv°a;Êâz¡l:­ÖkCaQU ï³Z>š±¥lºÓ1Ug§ee¾[°ƒi§…ZŽÓ¢óœÀµ¹9”KgÙo?Kcc,ŒI=Î ÕÀeÑñ¡¡ X¿]ŒÀz ÅßârHµVØa¹X@³€”¿éj±^:ËÏG½N˶ËeeH¢Šhœjåö €°Hø• ÅÂ>|ïËÄc¾~ýgH¬y‘øôÓOcxxO<ñ¹Q®#‡""""¢Z?Ôy†DÍÒnnQ)BfJ0¸[‡.ÎA™yš8oYš¡X+>øÇƒØzïï4è * Ò‰dgç-Yx”f´vw¢µ»“ uÞ/ró ÈÍ-¸`½\³£ã¸ðüK˜½býÂÌ\Àí¼Û¸ÇñE*i†A´³ƒ€:ûcòúÒ‰›ï®[ÚÛÐ1°Ù‘üóÉ’×§tµ©°C>™BrbÒ•º{}¯=ûÃšÓ ··7ì’P ñ°»ƒã"ÀC=7ËBÅ/U&ÀC]ÝÚ**øtÖVGó­ZØá( «L:<ÔJ»Q M©}uÂ*¸!;3mÕê‘ gý]«»ô¿Oå›…á1ãq“Í$.Úcxx©©iÄzº_œ±d}œ+6kDöÊMð¡ Ëxaj/LM`K$ŠßÛ¸÷µô",‰ò0ì@ܼ®n_»Ã†ƒÿ§¥‰ !‹ÓÉQœšy³¦²l ÄñDß½5_bb¬¿öªuâ‚JÙP&K·ÓÜì¨oSÇá>ìôê(ãecÚ–²éJÇTiÆ÷i(Ø¡–²éý¬Ê18j4ì°îqéü»ìïRø9[6´ 'Jd AQQd¨Ú:ÀÁê¼T²@Na€óWßÏbØaÍwµÂ«¿§i ÐÈ /c­†–?§U$$ÄÛ¡‡ÕÛùø4 @Y™ÀþÛz0öíOcðï^Ä~cMPÄ™3gËåðÄO@ú:qw """"2ûØ¡N3#j¦vó@5qš˜t· ÅIÈÓ?TëVK¾e×{ðá/ÿ1Z»»š¦ÇO¿;‚¢ñ>áx+"ímdÂ:—,JÈÌÌ‚Ïd è`£¸@m›7‚õq¤Ó­#±PDâÝ‘5@VvnhÄ'7a‡ÔÔ4rsó®Ô½U° âÁ/©)` ²Ãƒ" ^-ò‹NgHžØéÓYxØá!½ùp\SUP´µ«µŠEýA¥h0à”àEÚ7rx€Üô "]µQÅB6ç8ðPmRɹpqpv(#µÝÁ3Y©ý"²ÜÍÏä •9ÏX¿µApŠ$—Í'qQ_÷έ+Ɯպþ¦ñU¬¹h¹Ê¬7Þ/B!ªd,ÈòÂ;ŽÏCUÛW«à‚3Ùu·ëØ¢ŸÈž½2îÁ­«ÍºiûûjðAÎLîûµèJ.sÓõ¡½»eLÈymÚL`{jÕúz=Ü~«)·ƒ“ÓoØ<$¤œ­ÀÞpNl9ˆ?¼ü#Ói?~Û¶mömÛl+ç±cÇ,v‡;ñÜÎÃè÷ëœ×7"ì ¨%З~”¨õ ¬©((xP_^‘ @CtLp©8„X£ŽOüª¦òtqaœ8°ÒÁÄP ÙÚœcêvÐŒ§éì ·^L‡°ƒÁm(ûöa—?[3;P_¥çsVÁpv0Úk‚l„¼~=ÀõzÙ®õï*°ÃòÏ"~‘·âó‚(##ˆÈ’¾:§Ž¡M-9>ð9À|A€¢tí¿¢Ox vXþ7ë+¹=Ô;¬ÚG–¡‡ (š*¿]€¬ŵnÏýåAœø×·ñåïüÚ’nyîÜ99r'Nœ ЃÇEÚ‡ˆˆˆˆÈÈ-ugHÔ,íæ•"ª4aš"º[ŒôPæÏY–ðãw>ù{¸û÷?Þ®K EÓ°Í0ÇcÅ[ äÐZ É”ëeÉ'SúÑO1yá]ËÓvt€hW¢¤Óé£&/¾[1N27;ïð ŠU¿÷…‚èÞ>Öï·4ß…‰I×ÎI+a‡ýÕ_"¾y“ íRÀ¥Óg0}ñf.^BçÎ[±aÓ&Üzè!„;ÚÝOùò1©EâðpCx¨çÆ[\a&ÒqÓžHD×Vé€@‹þ‡Àù…¼1q5XnÅêâB6E’ÀpÎQ—R€Å ™T(8{óª¸ï„RLgo8MhªŠÜ\ù‹’åðR¾Pf·P1ÿrêØ¾¥âw©ë HEã0!“q½õdÔåáú…w°ëÀ>GË)ä F[lÍÃÉ1Ó-¹uƒì%-þ¶-2Sç¯8 >d/$&ðB¢äúð»Ýq }"B,R ?h&°ƒ]¢)ëëvot#º¸0¦%c.8ÓRçóÓú (% àUÚ¾ÛÁÁ®;p6}µ¦•ç—쀎;†sçj¡±;܉³·}1Vç¸Ýˆ°CŽ/ýhä…lÕ¹¸Zrl4ƒ«Q¯Ê` aÆg)ìf8|µ_ͰĘÄØáfºV_†¨zƒ¬GjO+À°à†]'YT¹y¥YtŽ8àÆ°^[)e²ŸÔ;ìé{v¨ö]=¸;8;TÊ7ägò³ˆJ2fs<„å/WËìÇ14¢+æ‰ ©°> À¬“¿Ça‡›7ü+ÝnÔ«µ°Ã’di™ÓE•O‡Ài@žVM׎|ì½Ø{7¿ù†®Ôþrqdd„@u y6GDDDH¥RÍwÐr j”vóX½àêÊì P³ïX–^kO~÷ÿ›ï¼£éΣ+ø["D[ŽÇ,_ÅœÈ ùrs󺫴[ùd ž 㯿ayÚ^¸@6ö’8Zv_Ø™E¡ÃñVä“éòeÑ»s‡¥°œª(˜·$®ÕŒ^}ö_-9í„’ãWñ³¯}Ò2ef|ùÅ/lËW_ß-G«ˆ¢WO·³NgؼË{¼aôhéÅérÈ@‘亩Ô‚sHÖ«—j+ÖWRnzÆ’²ñé´gÚ„sá¦Âi‡U–=Õ÷ó )hªºfÖg} \!•YóYââ°îý-tߺµâ÷×ß0îî°ä\@´¾Ø°q‚4=5íø$VÈÛ1é=?b½úƒ„SS ÒÉ<,.Úƒð–ûèÞ Úï|0À•\ß~ŸýõOðµk¿Â¯èq U žØÁ[c-eOýn¿ÕÔ~'§ß°íX'Dû>ž8ˆÝaóŽhù|GŽÁðð°¥å:vìΜ9Ss:Ÿï¼ç÷üQó¢ ̤l±I`ã # ˜ HÉ|UØAÖT,HEL‰9L‰9ÌŠ—­zœ‘¼žKX;,£ÛqKjG÷y`Ëe­ a]â‡ô¶£©@|á¾÷y IDATkÂg P ( a–3Ö&FŽ“ª† L‚vÀ^„jþ¬ÞÝÊÏ7 ìPó|óÏÅÆxÑUË·¡) ½­¡òÇ¢i€$¹$P̪T!MÿXW¡®èrFŽÝè>¬DK޶õãŹ–¬"›–×ï3 ­=w÷ ´áìׯÇßo;èÈȾð…/X~¯Cdð@DDDTÒùóç_‚}êï@µe?™!‘¥m¦‘"*— ­xÍ}ØA Oü“¥°Cßm;ñŸþ毛v°.´Àú|híîD×öôßµ=;w ~K@B¾€ÙÑqÌŽŽ¹;H< Ï¿„ŸëÛ–Ã\À]öá£ÿíO±ëÀ>×`ƒH{º¶ØA§ôÀÐÚåÌâ¼­Ý]ÇP«a‰ç1=|…ÀUûGa ì°¢ Eüìk_¯ØÛw+çÉÏÏ““{Qx¨cq‹+\/ÖWd™TL­W/f€Y,)[a!é‰:b}¸ ³72/@‘$gï§ËÀŽÞG+ Šéx H2Še  k‡gYËÂs£úÚ6VuC™3ÞïBq銃2±÷ì踳7·¹¼íyp¿Îíôßð™q'!raîíAxó=õ½LЗ¨ßÌ%ð7o½†O¿ú#|;ù Ì-&ç_vðœXÊ«áCñ„Mñ ågp>?mK™¢ýãuŒ ็ÑÊš_ùÝjèÁJØáäöG TFÁŠ $sÀ|¶ôwSH?ì h*2²€Y1´ÌƒWe¨ëì›SD\2H+%÷^•‘ÓD$ÁESq¡0‡9©`ìðç}÷®…j¸ÌDÌ*ØÁÜ„ Q`JG8;Thá|ú÷Yü<î÷¯uw¨V.#ð‚e°ÀXjÃzƒ(`‡zqw LݬØ$‹Ç3£çÍjwÝN+ÿ¦i ÝÑÚ"~p ½b›H€ÃÆx~ŽY7("PH……q£~´êã‰Îr²]ÞÆh}ùž¢_ð‡K¯WUûmõ}yA (ëçÁ¡>Ы§ô> ¶Dж©›v¿›ö܆¶M}Çc–ô¹§å ƒÏ»^ž±×‡ðão| ž om¼Çæ;ïpt`|>t ô#ÖÓE:ŸNé…â·ô€õû)S %‚®í+v ¶D,‡–ÎO7œ$žÇ/ÿñŸ<;ÀèK¿¨;Ü8žBo~ÿÿs¾«,0LnŠѺJ$g»»»½÷°€¢i_9y–w;w¤Cÿd^,òž©Ëå.z•µÈáASTðé4­­îÖAÌùÀQ7h8E”\­çbú&Ýì¤Ë _ÆI#7»PöóJêÛ³ ]Ę6XoƵ ™Å„bss†ö™Gï®÷8:‘UÅÖ‡FÒæ~]7·"Ï{®½i†1lÚ<çB¡Pªƒ˜¼)3åx ²ŒN\Á'® 3Äïõ àÞx7º¨ §£ØÁ{ý h›"Œ‡â[ñý9ãnH'§ßÀ‰Y^&YS‘óèö…m­×þ@+žÛy|ó{¦ÓX‚Nœ8mÛ¶™N‡ÀµÞ¸h@^(9:4•ôÃ9YDA5v¿QP$ÌJ•ï‹æä.çà§Yü{ú*þvò•šèÏûîÅ¡ø€e—–¢aÜèã ;X•¦‘ e·`Jg;:;¬“V-=:ÍJâºiQ ÷ûÁ®¾o¶ª\V V¦Uï°H¿^aWÝt:LX¿Å°ÃòßmáÚ"HŠ IQò³¦ÒªB)€õþPî™e`Œ^ÜÆç£!ˆŠñ´j‚´’ËdPxýV_ýeÓ2ü]ÌúõBhÑÀªW.G>þ^ìÙº~õçHçk{cÕ½‘õbYòú”ˆˆˆ^|ñÅÆ9­á3$j–v«‡®¥ÉÐøhŠûÏ’µâ$ä韪5±T\ÀÿÛw‘Ó@kw'ÂñVÈ¢_(HÀ†U>™B!™öä“ÞÁùû) ©´åio¾óì:°áxÌÕc F[ïë%ç”É‚  v¶D¿¥ÇѲ…ã1„ã1ˆ…"h†¶¶È'SHNLºRïÏãì·¿‹ôTí‹8†ÛÛñà—¿dì×~ûº®í®^u¼.«;<,“|QÄá¡ÎÅùV®¢¨Jî:Ý[j_ÍÕjW;"k‹ÅH%5½R)ã ·Ñ‘­Þ:ÇW¿'.õ)âä`iQå좫ƒûÏ’Õì%ÈS?° vèܺƒÿ÷ ;¬ë÷#Ð!Ù ¨|2…©w†‘œ˜ô욚Ƌßþ.~ùÿl9ì°ùÎ;ð‘?{wþ˜«°Í0ˆ÷õ¢móFrNTvna]ØÁ ¢k‡{óU_(h9ì°01éìšš¶ vˆoÚ„~ýÿ´v€äø¸gûp5àÁ«‰Dâ¬ãã$îtË“ËøC+ d:·T䛢W»e¬;h_½fYÞ2/8ævÀ×Ðú#apÁ £õ­Hd‹-ÅtÕuç]ÃÊNª¢ ˜ÎÀß•TÍIÁ¨„|¡,è17ª¿wï,Møè å201AòÒÈxDÒSÓv/p"?rsG´¢?pøÛ¶ ¤÷n|¾óöš’2túôi?~¼æCùûíc°óý;}õ;(*0Ÿr¥¿›Júa‡¼"‚W/>0-å¡jåK“’€Q>*~™½ŽoMý¶æ#z(>€Ç{î²ü2cƫ֊Åʦ֓—°ƒ•õEØÁ’¾V!-–¡Ñê÷£-D«Ï0Ë!Ìrhá|h õùÀ¬vŠjVØÁËÓÜZ€ «Ò'°ƒÎ¿µú‚`á6†€ŠeåS¤’ëC~s€¦VÏséïUÿûý hšÒ>X;¬|èS‚|¡ê  æ·(W«—wõÿ>áµÓÃþ®Î¿wsÍü%âî@DDDTÒùóçë³àކk C½wѲ2«Ðø4~fíýˆ R’¯B™ýwËÒÛu`>{âkÇ[É)DÔðZ:x!ÈVây¼úì¿âçßú6f-^ðÓ+ PZ³sÛ€ëåhTùBAôîÜÑ0±Fª¢`vt…dÊ•üKÒ?X;<úð…B¶–Y, ¼¹¸_º;¤ÝÈ”ÎS/Š)˜å& hÑï”à5‚õs†÷±2`¿èËC9°¡µïÇë[Èæêr `jxÙQ\twÊ;.ØvQÌ­½(æfª:L¬Vû@‰ ¬|̘px î&.Ú\ÀT ÷쨳„ª³à¢}S˜XO—¡ 9Q}‹¢YpÑ„·ÜPßûÀFÚ]-Ïoæø»w†ðÙ_ÿ_»ö«üК¯ìü@äšüŒ3Á †‚å—i(?ƒ—3×l)Ó„˜µï€W½ˆ9¹ý||Ãöš’4tþüy÷`‡X¸~O Mr<0“D¹¡ÎwQU*þ,»¢@ï›DES‘W$ÃåXŠ+ó\ ¬S5 ׄ,®‰¥gD¿Ì^ÇÉ™7k>î‡âx¢ïÞUNkê4ÆXÆ©gK`;4,ì@™O‹¡(øaŽC˜ã䨵 ƒ‘6Y·¼šµçåÒù§ÛAÁFw'Ü#ìм°åÆ6Zåú“x ¿ði@n¶ï:°Ã’Z¢þêõ¦·=°Nù±NšŒÑí5×Ê£¨šþóté@ €U·Ž±°ÏýõA|þàöš‡O=xG‘H„TÌTd›óu%0œõ'ââ`kÑå<ÔÂ849ï‰ò(³/@M¾fIZ\Àý_ü<>|äÉiDÔðòèž ?þÆ·0þºµ‹ÃulÙŒƒú˜'@ˆvu kûXG:¢I±þÊ‹Ð5ì ñ]u»ááa=z´æC1 ;øØú†Š"0›²Åº?¿5h(*22²€9±€1”ÌWü™óXxäd‚ªN3;äiEX3Ü‹ªŠ>‰¤\ª{«`‡¢}¶ÁšE@ï5–²qp1“Uc>v•ߨ¼;譯°8-C0€f²^,*—™²Õ3ŒPógõ!;ØA—3D…þ¼:½%ׇÂBé·,Þü® à÷3Y}ýÈ ì°Ã’hð·”Ü–œ'j€@Š¢U‹Öù?   ëyòÛ‡¿ÿò¾š/ëKÐC"á­w'Í&<•d xðmp¦p®t¨/ÈÁþò«Ð„h|®PÈ“?€š}Ç’äÂñ>õÕ£ØóÈCät"jh3YÏ“ÞÁ¿ñ-\xþ%K‚ª—Ô±e3üÂàÁÇþÀÐâ™v‰ñùÐ1Ðhgéˆ5ª¥½ ¾ÐÚøÇFƒ„|³£ãxwâ›.Ÿ{¯=ûúƒ`úâEýeÛ¼ÉÑz­æð›òìsBW ˆ/«Çh]Ékƒ0ÜêYÕH¿JâÓKËŸC´·ÇÖãä‚At½w„\\0èÊ]‘$ÏZéš$uŸôÎaãûvp6¨ZÈ•‡+Gt§Ñ½s+€ÊîPX0îRDLÞð„b2SÆ.`.8ùüáëõÅ ¼·€.pÈn$Ñ\þŽíðµmœ›…˜¼Up×eè­ô<ÞJÏã;x[ÂQün÷FÜmÇWGŸÙvžQ4|Ã0ƒwàLrÔø ¸”Çéä(™&ªiLHãÖ`›u Vy9c8{Ûç°ÿ­ïa(?SS6ÇG.—ÃáÇ×Þøçr8zô(ò5Ž£†aŽ6ÔipŒ¬éBC8:ª^•tC 7EAÖÈš¨hP3>«¸À!YS1/¯½ÊÈ"® ((ÍÝ­‚¶â¶ÁÐÎõV­½ƒ‹ùXæÆ Yœêv t´¡—aÊ´¬ÊÛ©c¬GØrv ÌœÓ&Ó6û*ÃбíºéèøÎTšñ} •œd¡p>€ [±<-QdY…¬¨ë—WwÓ1†TJ“ñ4Èy@•MÃ5£Kò£´”¿Ø]ÍÁ?´ €?üŸ/Õ4Éçó8zô(Nœ8Aï]R  •@DDD³Àƒïþ 'PC}J#EtRJª0SšŸ{A‹°ƒ&Î[’܆¾^üçÿñß=ñ¾›ˆÈ. ù2Ó³žŠIHMMcèß~ŠÙ+Ö.êеâîÃCÇÀfÏk0Ú‚x_oÃâ{A½;w`~|ùd)Ì7¡ms_ÃÔq>™BrbÒµü_}ö_-s[é»ëNÜ÷Åǃ`úâ%ÝÛÆ7;7V(’äú"ÞfoÝÈ”o 3]^¡:Ò¾¹¹…õ')Þ¢b}ÝÀ+C†öáÓiKËÀ§3w´ƒáì·¨ò»ø²DȺh* ÖœlÀoxŸbê&#ʸ;È‚¨ë]Rû@‰Z¬äî0;eœÉtÛíÀ Q4 .Ú.ÚUÈAL^3 Ù¡+ù ¾3ò6 3ÄûÛzp[kîmíÅ3O ÙìGÑ0ÎÛ‚.¹<˜žšz {£}ˆ0>KË”óè÷·ê_¥½št¼°‰±<·ó0öüÇwV®roBO?ý4†‡‡ñÄOÜø,—ËáÈ‘#˜®ÑÍ0ì@Q@`ÉõnÉD¢o ¹ÀC–Uýía¤öaš|-€"rqezÊðTí稥·k«bB?´ý]<úߎtÞüÂT###8rä\©s"""¢’††† ïCG¶Yù°ÇÑ'KDõ*98<*4iš˜öN‘Ä9(‰ÓÐä¬%éíØ{/>úçÿ•œ^D +/‚ÏãÂÏ_Âå_¾biº¡X+v܇þ;w{æXi†AkOÂñéŒ6ÔmÇÀft`sÃÛÂÄ$ É”kçç/¿ûÏ–Åž |`/îûâcŽGr\ù»vÞêX¹ª¹;@~jÊ«ÝÒ•I"¨<Þ@ëvøôÚÀqU–¡HîQÔ¬__à˜ä12)Ðbü!vrüªååÈÏÎ5üÉd5(âìý»9+FU¹¹º¬ìœ¦ªËs£úûm¤}ÑÒ¹ÁTpx0/1þrU1{áæ¦‚Õvyr[ÓçÈJk7THšpXirèˆöGèމȶ}ðwl÷ „5ÃñÃë£ø› ¯â³¯þ_»úKü\Æt8´È£’o‘|ã ì°$CAôË牊„gç.ÙR¦1Á‚ù™7ýþVœ½ýsheü5g{æÌ|á _@.Wº7zê©§022RSš_Ù¸×x;µ·LÝbE`:]÷°C^1'J0 o%MÁœX€¼Ê–ý¸`亭J7þÎÈ"Þ).¬€þ~æÍºXŠF;·ŽÃìÙŽ²¸^ìà´(ƒmh§(§ö±ê°P°2­F€L;-ÔrœçV×O¹ïìà’kƒžm,€Vÿ¯©€˜ @1È|é³Åï— –£Í÷7#°¥³¿²~Àß,­Îg¾`ª¶óxé@xí÷ûïèÁÙo|­áÚ@õ%èÈyàˆˆˆ8{ö¬ñ[¢`¯54'o#ÍŒÈòvÓH–*@-^óì OþÀ2ØáÀÿúGv jXÉ¢„ùñk˜óì0öú~üoY ;p?v؇þ·?õìÀèØL`"ý—^EÁÿÏÞ»ÇqÝw¾ß~Ì{Ìà $!ô’´%Â’­X6EÂW¶„rtc*f¶’8‰ Jû&ªkêæîÆÙU²t•ªBîîÝ¢êÒ[wת\Ù›D&ЯºqBÊ¢t#Ù Ùc> €Á æÝ=ýîûÇ žƒîžžîà|«PÀ NŸ>}^ýú}Î7qí¦k°Cv:‹ßxiÀ· ó%Ciã;w:Z6±PùF%¯vOâðàeÍÌÌŒvvvz²lª,¯rKŸ;eÙ€ñùrI€/ä Õ… n3ªEྒྷ.nõW·lxìØ¯UXaxP< ­pž½cPˆm¿»:>½ŽÃƒWâðP˜p JÑ•ž1åvPuÿ«ñ ²Q˹p¼ÉÔ9‰hk‰¢Yøã;àïXt}PŠIè°ßåÿ47ƒš+ÏÙ»£M¸7Ö‚O4wá@¤”D<±·4Ýæ dX°”»AéÕ¸<œ›»Šøtú#öÞïTëò`áÅM_¤ïý"úßûvÕN7nÜÀ¯ÿú¯ã³Ÿý,FFFªÊëÉö{qbçÃæ6ŠE¶ŽÆ¤¤”]$¥®Ç´¨©((¢ µa'Ödäâ¾ÐâB[tóÐt·å宼&ã¯SWñfáNÕõâì±€¥º‚j™N¯xP«·åº7êÏnw;ó¢lÊËM7;óò4ìPMÙŒ~§Û˜—Sãk÷»ö· °Cµó—‘9Â*±nšÀ+ÓèJ~0,À_  =4‘ωEÕxÙ+µ³ÑöF…c ÀßÈ%@L•'¤­Ï +?Óq7 úö´àâüúÿí«vz8yòä2G;¢ÚŠeY°,yuJDDDdx CÛkvëïè½/‘GEœ\?<) ]ÊxªL÷Ôä?Zõ1¾Pÿ2ö~òA2܈64UEv:áZÀôzÊN'0öýWm ¤^ÐGcï§ôÜ"šÑÖGcvˆê_² ysbÙ"ËN*yso|ëeÛ5à·~ûsåXnÿô§†Ó:éî «*ÄB±b!öjwc§ä©9MÞó¼3„Û›—Çñ–øí9A7#uó¶¡´sL¢ó@oÍˤt¼ˆ¶6£˜2>I¹|MÊË%ShÜÖµ)ÑF“tMOšæú¾,ƒÈ­š©É"ÖV4Ø~Ÿ%àSeœ™þ žßuÄþ;R1‡}¡ :Ö÷i'ôÀq†‡‡«ÊãÉö{1´÷q“'›ò×Ç`Ðu Ç—êXª®!¯HuÕ–Âh'Öäq_(° ”)à"£¸)d¡âî}¯ÉøÏwÞ¤Tý*dNÁÐê W®Öê;¬±æ3ÓÔFöåuØÒmÎÏ`}[>›@€JÇî `%/Óué2ì`¶ÛÌ_ˆÁâ>ìÊß g Ë«í×ìP››ƒ5~ë680h+iì€ÌÖ6?ÈE€ñƒbhŠÀó 8N†®Û0ï™)#U!½/h, p•Çåüß‚ dUƒ(«emYš€FÀÇÀÇÒæÆu`Éåißž\üOŸCÿ¿©zA4Å3Ïü°kÇ3ù“w §m?àð +ÇѪ’wßéÏÌÌŒ»±_<˜Ëð ð°–ÝJ½¬R]œs†@RcÀC°1b xÊ–7ñ]öZÙlf—‡Z¸b•bãI€  ˆÆƒò¸t9hVsxPeÒÀƒ/B0°~?¢mÍKŽyí@>+ó l g”jú_(³½Ù`~‘ã¶äM[ JnTëYÍÂר_c4Y€œŸ†’Ÿ†&{çÚŠSd\NNár²|ƒÛ £/Þ†ƒñ6ôÅÚÑÅD‘†Z*G^hº}þÉ»¦ëXz&R]„í³†cK•£bXж¼ {-UËÃåü$F¹ú"ö®RµËƒEÙ =T£Ï7ï5;øY ±Næ}^ò¥2ôPÇâUœ*Ùô>Ñ8ì°xO¡kàQ¶|mbXpª¼ávyEÄ„˜CZ—Á·Å<þëÌ?cN)U}4NÂÐê ­_­Uk‹Á†ƒÀæ¶q1¯õ¶qÓÁÒ1Öv < ;XuY!°ƒ…¿uwöë$ì`GJ·>?Á`]lô*•WK•€0ëG¨ÙOAƒûÑ¥«Ô‡7JÃøº± hèjÅz¡|ÀíY¥ H Ä£4F|Æë5@°äÒÐ.èáüùóèííÅÀÀypScàˆˆˆ¨¬K—.™Þ†Žî©ê6ß^À¡>E Ï®œƒ.¦ž¢Éé§YFì¶4taVäv÷ë«©«9Àç1 Gšf™/ˆ*´/ˆ@Ë=´Ü•Ï@ÎÏ@)&¡kŠ§Ê™xŒLO`dº<ΗMíèb£€@â}jÅWª>¾\òÐ^ éÎ4EWˆÑ£< /XÕ3]àõümCÁÒ+uòö›ø«}Gm/ÓÕÒœ9¦ X¾6ºósœ¸ñ&Jù»çp6€þæèoîFëôµ´!uaSù×딿l-×ßyvÐ7ì›Ó8 ; ºö`a€‚’DAR}Uºr_7 ;;K·ah@áè (€òS Tëc1S¡j::[BÆæ€¥Ðüìpzà8Ï=÷NŸ>í‰Àüb±ˆëׯ(ƒ›Á}‚DDDD‡H¯Aâp¨_È¡>šIƒ.ÌxÒÕ°vxôø—=(í5iªŠÜÌ,r3³‹ïçZ[ж{©)?›D1•ö\ EòæÞ>÷][žÜuÿ}8ðÈaÇã^ ßS¶¶ ÖÕA:%‘)q™,2“S®íü1Œ}ÿUÈ‚hK~íû÷áȳ_ñÄ"ß\2…Ì­[†ÓïxàÇÊ&˜…¹9¯v[פðP' U¹óç×¾ðT”ššÚ%E”¡Hä’`ÛêñÕÈ xQH$ qü‘ˆíå‘ù„\Á¦¦ºˆW“ó ·XW‡aË5YiOdXÍ.š,”á‡ü44±èérsŠŒËÉ)\NÞ½iÞmBoCcmèÄÐŽRè²  jª(ãÇ`Ç}85ù¦ù¶Teœœ|§wÖÞû MŤX@Opkпü‰±A\üÈ1xíû¸¾VÓ} ï?†¾ˆÉ~ˆzô\!)@–T­îdž¤©(ª[W£líÀ’¦!@—çHE×0)0) i*XŠ @›ß- ¥ð8qëuÜìy´p0ÒŽçwvv`C«(§Ülê(es½P&ëÛ®<=;¸X6§'¨çå$ìõ<ƒ¬"âè?ãRzrUòœ"âÂìu\˜-Ú6±ô·, ÚZ€°Z† ÌÔsUÇéPýýŽÀÕÁ0˜ÆN×#° Ô›.¶ t„( (HZ²îƒ$ëEµ:ØÁ œÁøº>ŠGcƒD®dþ\ºâ»E ‡ºj-…ƒ.ÎzÒÕ°vð‡Cèÿý' ì`PB¡ˆÙã«Û,¤æßÞ 6 •ä²DŽGvjÆTì„’£ßï¼k[žm÷ìÂG{¶¡-»vx:F“È{ò‚3ËØ÷_ŵ7Þ²-¿ÝÂC_zÚ3u|õ•Ãi#­­ˆ;´0¹*Ëx~ãt’äÕîKêDžtxÐäõ¹¥bõÀƒ•ÀëØödïl¼’|1•._(狞Ì”}©Ò7ÇÑyï‡kR.9·)€ÁEw‡ò ÀžUaÕyp¢¡£©_\3=–"͵i"ä‹Ç›EÛ–C Œœ.¼&&`Ðâ\¸H‹<Ðîn]™9¿d§u·"EÖ¤ÓE“ñ\“‡¾ üñðÇwÔü° Åns™_üî`¬ ½ 1ì‰ÆÊD(J£™J‚°SñÝx%sÃ’£Á7‹s©«8ÖºÏÖ2‹9´úB«©z cƒÞ ƒ×¾³³ïÕdßÜûËèo2ùЂ¢€f®ø©j@ž¹îÇCIUÀ©4Û;e{Vtº¦cFæ0)á š¢à§€º;W¾ž¿““o‚Síi£Çâ»ñÕîO¬}˜5V§?Zƒ}: ;èæó¤ ƒ°C-êÛN€ÂŽ v¨:¯j¾³âìÀ—çÏãïÿãš°ÃZÊ)".$®ãB¢ @tÂèkhG_cµnG4Ì”áfí9 êó¥Ë0WedX°45ÿ}ù÷šå­µk„ÕïìP=ì@9œÆ(ì°a>êÖ®múœ®ÃO ðC@$bA(UÓ¡(*t •»TЬA×uP4ÖG ë:Y³;P€F(„ßßH9¨*SZh˜Ë hŒúàcicùX=œþƒOà©ÿôšå3ùÈÈz{{qìØ1ǯ×_yåœ:ujÍÿ% œ:u ¯¼ò žy智s| îDDDDÀ訅p&*´ÝÙûV"ŠõÙltqºÂy¶ˆv¿öçFœ HSU$oŽƒËäHex¸ò³)SÞ[ù{êÊ¿àísßµm¥x_0€ƒ?Šžûz¶=B ˆwoÍ÷çDÆ¥H2æ&n»,É‚€7^úŽáÅcèßúMìxÌSõ|ãÇ?6œvÇ÷;V.>m,†“3æ ˆÃC=hff&ÛÙÙéɲñ³i„Û›W}/Šˆ¶5ƒ¢ig;VÀo8íBpv¸9扠êhk³iàAÈåjæò Ê2¸T ‘ÖÖº;ª,C,¸ð©kõ·Ê­"J¦ÆR¹®•U„ý‚ŠIãÀCl»±¹Ž›Ë‚ÈѾùs… %DŽ#Uós èyy[ûñ¹~PK(&]YÜÖX6‰±lrÙwcmè Fн BD)?(™TâQžÙö1<}퇖¶š}‡w ÓoïµèÕÒ>íZrAå|½ í}ýM;ñԵؚï“í÷b°ý>óÆ#eèÁK* 彾ߨ•TEU¬Q7³v5·¥< †3Ó?ÅùÔUÛöí&ìÀR4Z—^‡Øán¾v;@í³¦÷[ ØÁdš ÁÝü7[Vú.eW¹\†ÌöãªÜ L Y:.¦oãìÔÏ-Ï, ‘Lj8Ž‘Ô8NÝ|{Â1Šoá–íè5A Üu(*KæsiíòF}>YQŸ¯üã÷!¸òE&ÖžwìP¹í*õ‡z‚Öú¿*ª–ñeƒ¦œYƒ†y—ˆùï$I[–Ö(¿û >]†&–æhB8,¡$ŒÍ—|žË‹èl6q=´=,1œ|t/T=|ýë_G__Ÿ£PÁõë××…–=OÃÓO?Ç{ ƒƒƒðê{µ•"ÀQýk||ãã〾¾>âÜbAÙ¬ù÷~tÔŽs‰D¯OÈ¡®[Ï㮀°C¿öçJ`’øfoŽCâ×wÉ 5D‰»ƒ‹9éÉ)Ï­úÍe²øÉ¹ïÙ<½÷“âÀgÃô¦ë:Í0ˆwoC¨±tL"S*å ÈLNASUw®ù§x㥗Ágí‰!ó…CøØo}»?ì©z¾ùÚ!ó%Ãé,¿­o¨pffæ¢[û&Àƒy]pÄsúòúAB¾ˆP¬ÑÑòD[›‘ºyÛØ.lŒB(Yý~ò·5›Þ¦0; >/ª \§365Yºš\ÉmwëBŽ^àÏeÑºÛø*ÂV ‰[µ²ìãÔßJàa=ð‚K›ðɆÈf[nž,­g]"?eA°ý&T,_™Ï«7ÀD[d¬ÎÈ)PŠÉùŸT]ÏX6‰1$±Ôø¯#Fg0‚¾X:‚t"èkjT€RçA€©7ÇZ÷Y ŠæT''ßÀéÝŸµµLEUƸCO°ÉÕCƒí÷¡'ÃÑ÷Ï!§V¿"ÌÁH;†ö>n~àxèz\”_vw¨c‰šŠ‚"ÖÀÑ¡6Ê+"RJ I™CÑ3ëô ˜‘8<7q 7„Œmû²ý^ vÜëÚñ·²vX3_»aªÞ`;ǯ‹€B¥ý˜qd°NpÓ%‚²ØV`(Þ½fž½në°ºÁgqƒÏâì+e÷‡Æö2Ѿ«@Pݰ¼EYFQ–‘ZpÃ¥–¦ óû –;A¬{Ü5„*ýo«Ã0vÃ| üÏì>(½ºr-æc¡nk ;,ý[“IðE6`(oÖ_~Öî÷3‹ßE6ªûUû§ÐkÅ\ATÉܾÆç¥î†ÏO!Üò®8øØ^ŒÞ˜Ã cîzî¹çðâ‹/:¨?44d*ýÈÈ^ýu;v ÇŽóááaLL,°;räNŸ>¾¾>RQ&êÓômTЪ»‰D¯OÈ¡þ›Pƒ.§¡KÞ´³ vhíÙ‰_;ùgD¤í7Ä—0õþ/6 Àwo#•傼ìêpíò[¸ò£K¶¹:´Ý³ ±®϶G A¼{X¿tN"SÊÏ&‘O$Ý»Þg cßÕ>–pŸý÷ÿñ];=W×W_yÕpÚøÎŽC)—ƒf &”›žöj7võ"’æåÉåÌ…l Û×>ÑóÙ¼ãÀC°Ñøƒáb2ØöN”²y„cŽ»QTSöʼn(…®i ›šì¿çT5pÉ·uÕå ²y—ïÙ5[dQ@lçÓÛq鬩þ-•øBæµSÖkÆ­V‚?nK¢µEû‚Ðdãöfv]°š•Xä]…ÌÜg§uµºGò¦ù(†\þ¹%Šfák삯±|>_ÔRÆÔXöš„À¯rƒˆ°>ôFcèÆe}8ØÔŽÎ@h†Ð–¸B€Áö{ñJæ8+ƯÔ7‹s©«8ÖºÏÞ‡b­lQÆïjÝô7íÄèGGß?‡1nÖr>ML?òEó24‹x££¨åI©ëþ®êòŠY¯õê%ö¸;äw¤òK‚äè »|=''ß´4ž×Ów?„ø=ë¦ê4ظ¿M;À®2.¤ñ8ì`´,öo’¼ÌÖq½ÃkëÖòZÉŽfQ+%D#ÉqŒ$Ç_Ÿšw~8Ôº ÑDUÃí©h:R%¡ Aäç±€­¡‚¬ÃàïÒúߊ°CÕs°‰qirX+×`XÙF7·O]¤Â<øX¿õþabþôùhÄ›[IÏÝ…,ÀU‹#(CKtú>lQÂÙW¯Y›Ï œóÜŸÀöÞy6ñþUdnÝ2œ~÷áCŽ•Í¨»ƒ×æü%usç$âÍZƒ}Þs7Üúö+š¢@äxG/âÍ@ Úº¦¹âF±R+W¶7¢âììü±àD@³ö-!—G0ÖäÉ“Dårç*:8rC,ÉžªÖï7ä8¡Éæ4¨²²n¾B¾hØå"¶½Dõ!Š &ƒ¤“7'è9Îö¾ZÝÈ‚°éû  –Šž9DÛÀFÛÊs¾X„<ïþ ‰ÅMq|œ"—!Aˆ+‹ÿÛ3AôFîÂÐ×ÔhT9ffÁB£yk€wQÆgº>†S“oZÚ~hö]jÜN¿½ù?ã“øX´ ,ån;ôš0´÷q|tô/,çqñÞ/"ÆZ€ðb€¢Üí ºp"P(Õÿü J¶‚®–PíÛΤÄãŽT€¸˜A¯Ñ'Šš„““oâr~Ò¶£ˆ0><¿ëú"íë¦CsT”ñØae¾væY°e ·2ì@UÙ¶ÕæUϰ…òµåàÄ6¯Ë™)\ÎLáÔuàSÍÛp¨yø¡Â1)šŽ” %¸žÏ#È0h óÐ ÖÖÝ¡ža;Oý«~ëæ̤µšÆ.ØÁŽy¥ê~gàÿ+¿Ó5@ʪð‡Êgì8-À ŸÛbaÐ…¹tPK†¶Yë³¥­Ÿÿׂþð½1‡±iksØåË‹0A-uýzuÎ;^ì@DTÊf³8}ú4†††V¹9TÒSO=z0 +nZæ-0ͯî‘ÇD‡MÙªRÊó®€}°C÷Göã}îì`PÉ›Â4ÃÔÕ›EÅTÙéÏ•ËnW‡½Ÿ|>sØÕ…47qu ²*·¡%.“ÅßúrÓ ÛòÜýð!<ðÛ_ôlë»ó·æŽçðÃŽ”KâyH(ÆX° "š»Ð‘8ÞÐ8³2~–œ.¼(&ص”õ|9EŽ·5?Åc“›â²YR ›Dt Š@ Š@Ë=Ð5J1 •ÏB)&¡kʦ;ÞÅrß] †€ƒMe¤7C”ñ£#Ag0Œ(ãGo$è0oÞTvŠ˜ßP¥¥¾áˆøn¼’¹aÉÅ€Se<7q /îýœ}ÒAWqµ4‡„ÛÜ¿{V¬?8=Úò!ôE,€AàwùZH”_vw¨c)º†¼"BÑ8ë°ƒªk˜–9¤$~MÐaqî^r œœ| Ù>0³ÃÁó=GÐŒ¯}<Æwú"öM,FšÏLSÙ——a Æû«[°e°7‹ƒå²P°3//Á+d P´A—ÓS¸œ^?´oC´‘šécT“E“àÀÒZCA´ƒeø¡Ê:ªkØaÃc±ð·]°ecÚJåB…mÌÌ]F(·Yk~³ºo]ÄÀ€@tõŽl†eZšBˆ7Q,! yÐTÙý¡XRP,ÉÐ4}Cø¡1b0°a½r†”æ»Ä¢~\ü/¿Œžßükä8k/¾‡††Ð×ׇÞÞÞšÍÕ‹÷­b±y8EDäa-€§OŸF.gí½ãSO=…ññqœ8q‚T¨Íó¡V¼-÷è¦{ß÷yPrØ´ÒDèâ,tUò|Qí‚~ìWëòö¬±®Nâê@dInCKÉ›xã[/Û'À½¿z÷ýêž­óÄûW1ûþUÃéw?|È1pC0G+<»P*qx¨3{±PB6_y”È%¾3/ Ù€ßð*öÅdæî›¢@ÈM9DšJæVìŽmï0 <³ÅãxA´ÿB_Dp©"­­u1XTY†Ì»¿­¦Ø(ª "¢í–·÷…‚†ú¤b’ì\pKY{œ™ÚÖ|ätáIÑæÛ%ùóšªBÛh|³. ±mÎÞ›=çT#>cþå +Öy]ÍÂר_c€ý‹î*Ÿ© ÈÉå’Ë~¯¥=óîÐ×t÷¼¼K@o(¾˜:}÷…ÈB,˜²$eé˳Ž>ÁÛ•^Æèónóúj÷'ñ{×`iüBC‰w1Øq_õ ±¤Ì)¹„q1‡ž@S]ö©&&€¿Ý÷ ’*»;¸%E-ƒRýƒOι:VaQS0#sHÊ%¨@°èzRT% %ßÃùÔU[bO0ŽÓ»?ƒ(ãƒÛ°KÑe÷ÊÆ‰¥Råšij#û²v°5?€À.åUq?…¨çåØ޶÷â¬=ÁµVµ~hïÁ¡Î. ¢>Ýä1éP43ŽKÓh ¯€¬Ö[=Âvÿíìi7L£›Û¦ÒwnÀÔó›a@¢Âÿ5(I€/øªx~dÀ©¦(466? ”G¢!€f2%ä+@?ÂAÖ|9–ŠÆ]èa¾K”¡‡Ïá£_¶v­Íq8yò$^|ñŚ͉çγ÷þ`ø000€ÎÎN¸%<yS €Âðð°eÐa©¾öµ¯attCCCdܯ#+ ßþKøýqP¡í¤ëJrØô-,¥¡K™º(+¼­†Ö4u¶“ŠpHn¯¿ž®]~ c?xÕ–¼|Áï3;¸. óY½ÁU§ÑÍmSé»zL¯(Å2üào–^{qw LÎWL œF¾ûò°³9†¦)ˆ«òhi ¢¥)`ÏFÓeJoî1"E” Y‰«œ%ä`Y~ˆêGnY¥‰gL#ùš•S.‰uÖžæ_6S>ò²¡žEÑ,ØhØh Lj%1–OnÚc}nâþç‡>+pB…—J×… ¢ŒQÆïÊqõE:°+Є ÑøêoÞ÷;ØeÅ™ÂÏ!޳$•]ôú»'j*òŠàà{Js°CF0#‘7ùâ CÇ‹Ó?Åù¹«¶Åw?„ønxv€î`ƒsMXu:;lZØr1¯õ¶¡,œL©Žj¶¡,æa§»C@„»ôÀðG?þ·^FNñÎ}§Ê™ÇÈì8:®…1ÐÞƒ®]茈f¬/¬8nEÓ0Éq˜ä8Ý Qt†C`ºò¶T…9Ïñ¿ :LliØv0z¼š ˆÀ½ %˜×ºÓ0óƒ\Xü[,ˆ–ÆxI(iøi„ƒ,hÚæ‹0€`É£ëÁ½¸86³#×,eyöìY:tÈV`à•W^Áùóç™cGFF022‚ƒb``Žì7‚eÉëR""7•Íf1<<Œ‹/ÚææPIccc‹ÐÃÑ£GI¬ÐÑ£Gñ /˜ÞN/MA¾ýWðõ|cïç<;ÄØ€ “n¬ 6u5û©UùM‡Ça£uàUØ6çEÙU^ÊUq?ºua­ïì„ ¶|Áо†v\|ð_ã`ƒ7WRKˆ<ÎÞ¾‚ßxëïðÜÛÿ„WÞ»ä˜%î_ºéºT׳9¼>=ƒŸÍ¥‘Zú\ȳ°ƒÁ¿·<ì ߦÒwµ„°Ñ6º…mLïÒ¶” eA×k;,ˆ ìòÅ!h†B4äCKSѰÏ8ì`濎áÔ©SxüñÇqæÌÌÌÔv—XÌÛ«ŠmVŽŽâôéÓèïïG<ÇSO=…³gÏÖvXP.—ÃO<ãÇ“ÆX¡jêDËý ÊaR‰®K_òCЏuš]^š‚.¦¶ìð‰ßøª”?¶ýB$Þ4OÄ ¾½‹ÀISU$oNx và2Y\úÆKûÁ«¶Nïýäƒøìÿþ´ça‡hk :öî&°‘%¥'§™œr v¸vù-üýÿý [a‡{õ(Ž<û•º€n¾öcdnÝ2œ¾}ÿ>tìßçÌœjÚÝaγõ<33ãj`7Y²Äš²^,”- a{å`E’sO0³JüÊ`lMQÀ¥³ˆ4»÷ 9ÚGö޹•³‹‰Y4íè^üÌÏ¥ÑÐUGbbþpãMn‰Og¼qc`³»CùYAù„µp›½3ƒ¶ÞøBAÈ`#iTY©u˜‚k¯½žËDûÞ\yŦÉx°Kmþ4.¹d™¦©*dA¨úư”/XÚÎ ØzQïåL:\ÌÝÂÙÙ÷*¦{²ý^œØù°µ„üË8s@ºJ·9ÎS:t ‚¦8¸×aU×0-s˜‘8¨_øqªŒf~‚ÿ¯pÇö#8iÇó»ŽT (wÚ´Ó­ºWjBzŸ¥<)ƒùR°ï8j;¨ºEÓ èMƒŠòç¥é4ƒ Ã"À0÷]‡°ƒÀ®þé&„±Ñ1zv °Р…»÷-} íýäï`´0‹áÙ븘¾KéIÏÿ.§§p9=…37Ç0Ðуc;zË®aÕrý¤JR%A†Ag4ŒÎpA–ñ ì o>Ø5HceÞ³ ; Êö0;PU|g¤½UÐU ÐP´Á¹Éd €/@+ïÏêu e1mÀ’uBbQ?†þø0úŸý!rœy8÷ÆÂàà å9mttgΜÁ7\[9ŽÃùóçqþüùEׇC‡!µ÷ýˆˆœQ6›]tp¸xñ"&&&nv€pÀ™áE _ÚŽån¤## P}±Vv5w¤"ÒŠ`t€ï¦¯á/SWÀi²íGðdû½츯ò`t vÒ,:ý‘ªz…¡&4ÓÜFöãiØ¡6ét]§Êà׃þ©¥ãB…¨©j } öíUØÁåm¼ aÔ3ì°x"—äå ûÚÑ×Ðô–?_Lß^üñÁ©2ÎO]Ãù©k8ØÔ†ö]عhPÆZ ªŠñ\ã¹ZÃAt7D[xND`‡ÊýÑʸ·cûõæ†jÜ#l¬l£[kc»Ê£+€‚MÅn07™¿Öùìk¸ YÔê¼½^Ú €%yúz[pú™Oà©S¯Yš‹Îž=‹C‡¡··×Ôv£££8wî._¾ì¹{±±1ŒáÌ™38tèÐâ"ÀQí4::º\ºtɳåC__†††pôèQÒpNœ8ááaËŽÊÔ0èÐ6P¡í¤2k&8Ð5èâ,t…««b«s— ì@D„òûþäÍ ×Vƒ_«¶KBeàapœÃÁÐŽ®ö»ùE¢u³b»ÈqUŸ{‹ÀƒQÙE8u#“2¯P ¹ô#*Ïþ%ðËRm€ n<›K''ßÄéÝQôãëÜ XË·¨Ê®BÇ·}Ç·}s·p1Wž¿û›v¡¿©Ê‡/øk<_J çYÝ4ýÌØa}å32‡Œ"@ÒTHú¼KÅ @Ó  F¥Ý²xaúm| æl/c„ñáù]GÐ騠OºW݆ªzÅÆãÍÌØôp:Syé5)[^– jææAUˆË¡ÊdÖR”…þee¼P6AN¸1Ø™—a‡uŠ@Úpë¿dìoÞþæ2Й•ÅeÄX!é‰sÑX.‰±\C·®` £Ûv¡³ÝøL´É •]J² º¢èŒ„ÀÒ4*¥©æÕnOa5ì£Çµ4 uk×6ЫkcÛÊ£b¶ %0kçgCN åUq5¹6°Ãº7ê|–<6|l/†_ŸÀ…ËÖžU9s§OŸÞ0ÝÌÌ ^ýuœ;w‰D^ÇqÁÈÈ"‘000`îXP4Ë’çeDDv)›Í.:8xÉÅÁˆr¹žxâ |å+_14nvõôôàøñãøÚ×¾f-U€<þMø?ôG"ƒÃ6ÈhIU+tq¶®\@+\…–{·ê|ì@Tïâ2Y䦞’7'ðÆ·^¶ÍÕáãÇ~±®O·Í0ˆwoC¨±tH"KÊÏ&‘O¸÷,úÚå·0öƒWmͳûûñЗžö\,j%%Þ¿Š«#æêÁ)w‰ç!ñ¼é턹9¯V÷¨Û Oñ¬iÜ‹…²yÃiKÙ<‘0|¡`MËm‹vI(&Óˆ¶5¯*g°! Æç|Wµ”[Êd¡J˜Áìüܺ:kâVQ˜šAKïnïôÃ\ª,{¦<šR›àzUUªrר0¬ªÓ|¡XãÚ24mc‡ѸÃYà!lHR…ˆ¶¦DޝjûR¾à™~O<Éš]è¹Y'Z­e.‹ç—»„. Ä ¢Þç UÆÉÛoâôîÏ ÊømÍ{zØj±=o£êoÚY=ä°T¾®à¢ë@ŽJÒ¦êcîÁ«Ý’”Â#¯JPt ™5œ(ŠB#@ˆf+Ž›ÿ™º‚ïe®Õ¤ä#íx~בãF_û]KÑU¸;8 ;èæó¤ ƒ°ƒÑz±r¬Ê•a‡ eT>ZAˆa«¯?»ÝìÌ‹²)¯­;¬ÕíÎ!€½Yi Ë"hKv¿b»˜/€£½8ÚÑ PÀx)‹é[N\ÇŹIän*!ò8{ë ÎÞº‚Ç:váØö½èíh"&ïo—÷ ªŠëÙ®gs茆Ñ ê÷mnØÁê\lÕÉÁ ±òkÁ”Éü݆ìÈÓ¶ò åÏL r;S&úÁÊÿû9Wv–°õ:`ƒô>êòiv談Ñ÷{‹‰„ùûñ±±1œ;wÇŽ[~W,btt£££xýõ×ërX÷šãpþüyœ?‹®}}}†ó îDDÕknÆØØXÝÏ /¼€ááaœ8qƒƒƒ[ºmOœ8‹/ZvçÐ¥ äño·çÈ@©Jr ZYßõéê”a5ùUçC`¢z—É"39割Ȃ€+ÿ®½ñ–-ùÕ‹«C¨±ñîmÄÕÈÚùLU171 ‘ã\·vº±,èßúMìx¬îÚã§/}ÛTz'Ý Ÿ»©’gc ÆÝ.¬iÔ‹…Òdª,ƒñù ¥Ï'RhÞ¹­&Aø Šmï4<¤Vº¦OgÑÐÑêüàømmF1•6µ]11‹¦ÝË'!E?—F¤ÍþãPe\*…Hk«'ú¡—Ü@Sj­J2|AóÀPòúøbÿ¢hº¶qP˜"IÐ5mͱ*7 ÏÞ™1\>³ [,DweeÅþäÍ ´íÞåÊE¿,–Æð™lÝ´‹ÈñDª#žå Ü[øŒù•¦i_ "cs˨|š"@‹P…2¡瞺Р!ƒ““oâù]G–ÿÆ—Qeèa}‘v× [U+à|© =l"yvPu I¹„©qÞÉAÕ5Ì)%èkÔ·®ëÈ)(6„ ½º½”Ç7càµÚ€ÜØõ޵îÆƒ‘r·m» Ý[¶ì`ªËê6–Ë‚Q%U­*¿’¢ ´jec›@€JÇî `%/Óué2ì`¶WõC°Ã‚X h[ò`?Ï%Pè»Äê 5bpû‡1¸ýÃPÒ“N\ÇD)75’˜ÀHb›Ú0б =;FÅ`0öÚÁó3ŽG,@g$„ÎHØ»°Cµ§þU¿ui6ø_­Ü¬Â”Åz³â²Ñ6TÛÔvXø,Ï/¦²=X*탦 ¦­ÍñÕ¤Xò¸7õc談ñéghiØ a``ׯ__„jŒL7?=ÿ>tÅÙEo‰Ä"ü‰D–ÁÑèú‹ü´zä½ Q=i|||p¸xñ"r¹Ü¦;Ɖ‰ <õÔS‰'Ðßß¿eÛ{xx===–ÛY+Þ€23¶ó12xì|®BЏu¥– ‰³@¾—!°QYÙ銩9Ï”åísßEnºz¾ž\ÛÛ,-‚즄Bþpˆ,HMLºžNà—^Ÿµï>ÌáȳǃìÔ»ó·Èܺej›OþþÓŽ”O§¡XtÍá¦g¼Zåãn€433“íììôdÙÄLávc'eMQPL¦k ¬*©˜Lû×>i£–Ý(TÙúÍ^l{‡ià¡0»xÊ5l¡€@ƒý«ksÉ9¢ `ƒw/°<æî°p¡Q éŠ5‡‡¥ÌlÀ¿a@óâØ.òkº/T»bþª“B öAŠj©~‚ÖëAt (¦ê¦¼b‘·<(’ŒR~k¹ƒhjåÀÑäMóvà4K€¢êÄ„ãXùeÁ B‹ÐªP€®ÐdT˜Çt9?‰3Ó?Å3]Ì7ž}y+º†Qn ·"VïsjsྤYÎþ|=¢‚"¹;ˆš‚™CR.-º8¨º†¢*AÒµ5a‡¥Ê©‚ô]ƒ÷ø$þ2u?ãkc?»'ÇWw<„Þ`|åLê¹ve)Ý–œ¡¶ì`8]í`ë»;ÌOÑ5¨º†¢*·ãVL ;àÆ`éõÁ5Êkñ» €ŠFhZòœo€+ î§ôc4ŸÄpâ:†×1–wÏn|,—ÄX.‰¡‰+8¶}/vìD´…˜ ê¦BvV‘D\ÏæÑÝAg$ŒàRÓ Ø¡RÛ›ù»ìP È`5Pß(ì€*êÇHýL,å£[ß·]°C%-@l º¹j½ôÔ<ô å` ˜jÆ©$`Éít_¾ò…ã…ó?7=×p‡'žxŠâL@Ýü ˜–_Z~ Zþ}hsÿä8ø°pÜ###|êSŸB__:„•ïöˆÃ‘1-À /^ô¬‹å/ßkëRƶ!3]J؈@zrÊ3‹<^»üÆ~ðª-yÕ‹«ƒ/DË®`ý>Ï—UEäIpé,”%õ‘xÚv÷ðÁ%¹íÎbç¸]P|çN|æ¹??®»öH¼ïýͰ©mö=öhM,_uí£ªà’Öbù<ìîxÀ(€Öu À¯JæK¦Ò …"üÑpÕ«P¯'3Dd%° ˜L#¾s›¥2hU<Ìuwbrì}SÛ³ëþOgÀ`üö•ç§§Ñ|O»'vϹ;ÔîEÎÂ%@ÓxKO|¡ qà[ <èšÉðPLfl¯KÕÖ» Éëæ¯uME“SÐV”Èq–úŸ­îÆßr“KBÍέKo¦ÌŠ6NHd»ÖsƒÊŽº¦”aY€&—ˆ+„Ë:ŸºŠÞ`±Ýö_ÍCûB-èôGê·’d›ÂT Èó€ ošþ#ëd]…¨©Ð £¤*àU ~ŠA˜qê1^•æA‡å×Â%MA^7¯Iu‚¦BÕ5|cvÿ›¨Y©Ÿl¿ƒ÷­UŠõÓUYswØ$°ìÌ5‡ìªkU×ÀP êv€ÍyÕv€Íy9 ;XèsvÂk}·€Ph ÀÌ;@¬_i}mèklɽa¼”Çpâ:†&îüy|ýæ†n]Á±m½8ÖÓ‹h3 øôÕuC“Цa‰DÑü}šS°Cãþ2ì°ä3ݸßUðaA—/_ÆåË—ñõ¯‹ðÃ#+HÕÊ® ~‹s¦®œ…òßu* :rŠˆ¢*Aƒ¾jyU×Wîž×ý*ƒ_ÄB¼qed3ryuõõ­ªk¦`‡ýuê þ.{¼V›{™õ]¯ÂÖÜœìëzmó5³ó†éêv°Ü'½;Ø˜ÝÆ@ÁÊ(ŒPëq½Qà2«q hžŸÏ9à D¯›}O¨Çï¹Çﹿ ?̸?pŠŒ³·ÞÇÙ[ï㱎]ܽÀ¯­>V#ýeþïTI@Jdt7FÑ Ý=WÛ ;(é¿k ;Ôdû:…Ö›s«&ªiïµòZï³”Me8ÁÌ@"‘XtøÓ?ýSû,>ÿùÏcppGÝ}dppãããøÚ×¾f-U€<þMøöüÁmr 2ÙRÚVççË?;hÕ­TüÑ_ °QÝJSU$oNløîß %oNào½ ÙBÌÆJÕ‹«ã÷£¹{[Í«¬VK%ñ%Rshhm!Ì)’Œ¹‰Û®a;Çí‚|áúÒÓØñÀuÛ.oþ?ÿ\ÊðЗžvÄÉB, Š–·/Í¥½Zí£^(‰z«®?ïµBñ³iàÃ&o24 …D ±îNÛƒð Ú¯3,UöÎ̺—Î"Ø­I× ?¢­Í¦m‹‰Y4ï¾gÝú.&fÑÐÕi{y¹äѰAgW2×UÍsî ˆBMY×4„šš…9à_L:}H¿ ú‘м:Lõw+jëí1íð ŠE<¸¨ìt m-6Dß·*IP$Ù”M`q. MU몎Å"´Wÿð£ÒMYÑ(éüDžÒÂy`ñ|вüÚI1/ˆJ IDAT‹ÐUš"@“Ëçô(B_ò‘5¼ó&Nû£ëaW¯‰CQ•ð‘p‚õ[e9 ­ LD4-€¼X†&êX¢¦bJ*@Ö×?^]HšŠ©ˆmÐ6Fªº†´,àŽT„¨¯¿ŠYQ•LÁÿRJc(ùæ”RÍêq}WÀ«°`ÕÝÁ€lqw°t½aºZÀÎ¥£`1aIž´Ùý:;ؽMPGÍ ›ó²ê¾`ÕyÁÔwºÁr˜ü.¢–(”² 0€ºvå-»ïÇ8ŸÃÐäÏ1tû &JyÇçá‘ÄFwÁ‡ÎuÀ‡õŽ}¿UÅõL×39tFÃ茆 ú mkv°ûïz„ìp“pv Ö†‰*ögt??ë€\üM˯ݺ™SÙpy5]}õsV]Ó!+:d±ŒÎýÜS÷tãþ2ì@6LG7î‡^ºu—îx¢ücccà /¼‹D__úûûÑÓÓC mJÕ#à°8Ÿ4}LÛaÐÑÞµ0!°;ð7C©Y9.\¸€ . ©© GÅÑ£G7=8uâÄ ŒŽŽZvýЊ7 &_Ó¶•‚– à@dQš]œ…®Ju} êì?T ;xä0Ž<ý;¤OÕç0ðìpåG¯áÊ^«:ŸzruÇcˆuu€fÏ–Ñ è°TfbÁˆ¬«”/ 39åZ¼’]ãv©â;wâȳ_A¤­µnÛåÊ÷~€ÉwþÙÔ6íû÷9x誊üÔtUy¨’gÇ÷¸ A€‡:oÀUKWØW$ ùD M]í¶—)¶½Ó0ðPL¦×tM«Y+—¿Ã4ðP¨<,Ô7?7‡p‹ý´e~zÍ÷ô8ZG|&í9w‡ò  ¶eRd ÁX“éí²“3hëíãcAÑ4tƒFü —‘Ûx0Ów£­Í–ê!Òlþá­® U»ÄXb—ª¢@‘$Óà9¬ßX¿Q$¹jwÓct:Qõ ºRåœ¸Ñ —5¿>l †¨®´é,{Õ²úújŒÊ@>oë¬Ë4¹´<]Z>¯;®) ªEN•qüƒÿ§ïùlÍ ‡¢*ã'Å|$ÜŠ¬¯ÊU5 ™b‘ÊNº^vƒ(IåŸM :nKyhú–¤©PÖ€!äy×;Ú[ÔÌÈ’r ª¾ñõ²lp,Ì)%|sö=üB¨ÝõEeW‡ òìP3wªõàF:Syé5jóÕùÂÂõ£…㥰k-.á ØÁE× G'ta Ý^ÁØÁƱHUèC¬´iæïÓ2¾²„²ünĉ=„z£ù$Nð†§o §Ø·Š–-öÌ;>ª‡*gЬ/a¼O ŠŠk©4®¥Š®-Z¾uXn0»ÙŸ­À0N9Ÿ¡DÞ<ØÁzÿ(vX–&ƒY׿û Ê™[×%Ä„)h™+Ð’£Ð÷½3N§Ó+»¸Ÿ:u @ÑbpppåŸAxr“R©FGGqîÜ9ŒcttÔr`zµEø»@†ŠƒÅàx24îà7 ¿÷4´ô[É÷ùóçqþüù•9£Þˆh4º=¤ÓiKiÈ“ÿvÏ¿(>rðdG)Ð…yèj¡æ‹¢ÆÿŃ<9.)_@.™B.™‚”ß8n YÙøºìÍqÍ›,ˆMTmWøeÍ\~Ÿy®dìA)¢Ü}â!´ö÷º¾_0>š{wƒfWæOE$§cÈ&¬Ï‘4˺æ¨uiªZ„Q2Ùš·kÆEÀ»¾ôEô½¯¦Û&‹áŸ¿õ¿™>ïð箈£E!†˜-?ÎDÊònm‚Q7dÂ,*‹ëèèpeÞä\LÐÚ !˃ñûàkÙ–Ÿ­6/MªgæhêéA’©Ï–þóp²ŒB2 ãöA]…T$Mƒ mÍs.¾.ísvÑ®«2î¥Ê 8lñ£J’¥£Ôtìæ$̱†2Å1jG@özm7î·ÌŒñ•¢èÚ›óŽ’,ˆð…C¿®h°Ï2Ùª=DØU¿\Ð2:>f>XÓŠˆ'Oõ,‚¤·$èPkÕó§æ“ÈO¼Mvvç—99‡“7~ŠÓ·|Ì1è¦Ä,RŠ€ýþfG¯ãLchuãÞ`虬„Mº¤©Ð°½ûƒ¥{›”GLæ‘·èÄBÔ–ÐÃóÉkøiz]^ŽÛðT÷ÑÁZx¶s ì`ÞÝaÁ¦ÚS··Ý‰ÒõÍ’4¼ª˜¾®¢á§hcy+ .è6ö};"LB „…>_)Ø0‘VYî 5 ;™ŸVŸÓ(ÿ §móŠ:Þ±Ç;ö`<ŸÁÙ¹k8}ý L2›§×€›8>&û䪟c¹›®-ºBðÑdíÀNº>ÿ7sŒ“°C©ö't ç”q=£ç›ü]Tä“BÑ¡è²l`áÐö¬ÁÙ>S05Okª>£ —UÀù(Ã4(ÆÄÅ9âÒP €¡OìÅÈßÅùK±ª¬ÉèPÍH›¾{ 9ÑAÑAhüôܘë\ÖkÙby7wèíí]A ¢¯¯Ï{ùâÉQŽŽ®@ µêܰaJˆÜ242r›a'‡’¢ü`nù#héß@~ïiGÝ6Ój"‰àøñãªiøapp###xøá‡-¯f Ä~z×ñî­ààÉææ’ÓÐ¥E@×j¾,jügÐr7ÊJÃÜ}â!×»:@¨¥ÑÎv׿OÊ0såݲúIQèØ·§jÎ%õ.Y˜˜‚*I5=nW«±§÷þéWÐØÛSÓm³8>ŽWþîïW\|ŒªûÎ;°ÿw~ý#ËàcsöôCÞµ1•)7dÂÊS@Äm™*x€ì|lƒhŽ…/‚`€>²<„ ¿íµ5EAn1…PK“Á ¥ü –è®v¤¦ÍMJ©Éé’Àä LÀÞ],2³³hº¥ÏѾ–›ƒb#Qh§*‘/U’--V¬ß3{× Y¦(DÞØY©)ã_|YóŒß‡@SùEã;¹èšM@2ÞŽóU'UZ¤«’E’·¥ê—©éZ–˜ËYÄ\nû±=kþKmÒö:½'O5 ]S -Ü€”œ¬Ø5¯ ÉŠ@¼*c47>_ݬ7'¹öþUX(4„ñà-U×0+çòõò^þûH üº—Ä£¹9ü÷…·± 8·+ZbðT÷½øPÃn##ÜÕmß狘pwØa°ƒáã*;,bX¼"o}ôºM¥ƒ4m×NWUtcØî:f`ÂBž*`l{½öa»Ó'tó}pù˜€ ($¦žÚ´k÷p²ÿœì¿çS™ü-ÎL^®Ø|½>t,9>ìbÖ¾å7 ;¬þYPTLer˜ÊðqÌŠóƒ¡ §±³`½ü|Ù.Xø|+ØÁj=Ùñ¹…ß3)‚°´$r^qYÈ&EyÑfaÒ]aýÚUÑM |–‚eu *A…Ï¿>Ð2B.ý[ûvúÏïÁír¶¢kE²áȦ÷ƒ`œ»F¨õßt}È\.&jâ9jbbxöÙg×üýرcèëëC__î¿ÿþ•Ÿ=y2£ññqŒ¯¸6ŒãüùóuQ6‚m92´dä³óX七u{X¯t:3gÎàÌ™3èííÅðð0†††j²íŽ?Ž'žxßùÎw,¯&.¬´}íȃ<9ÑfõãêZúMhÙwÊJ£õ–^ûÊÖ}ÓKùÂÊÆÁÆHÅjEŠ(¢å‘O¦KZs*dyHùØ€óÎBn€rÉ^ýÞž-/ð–ñq8òéÑwÇ×÷’¢Ðܻ۱Í(íRìÝëeõ šeѱoOEúòNT.™Bzv®*ã×®q»^ý÷}w~ù‹`šm]U‘žÅè÷ŸAvnÞÜ<ðãÞ?ýJEò™šš‚¦•® ‹îÃb±snȇ<”§QÇ\7Çh+o× »¡‡hw;bWŒÑG©é:¶_¦2à‚-wœ_óÂD)xhéï1 <¤§¦Ðyø6cm¶°€0Ýе/ÀLDä [œ±äÒiéŒk§¦*Ž_c™èôELÕ…\WhÎ|›gfç¡)Š«ê;Ø5<€&f=à¡ê/ª·[µ˜Ëf·¶Ù[˜˜ªú®e—‘ÏmVç0mÛ‡³öu$ãAzË>OžÜ.MäQ˜yÓqW‡ÍT)èAÑ5\+$‘óØïo†Ï››\'ޤ¶„tèK€ ~¢tðt^•“sˆËyÛò¤ä4º®cRÊà¿'ÞÆ»‚³/…>ß²Cm‡ ŽƒÁ¤M¸;Ô ì;ÓCUa‡•q@3P4$Mƒ¢kP4 ²®AÓo¦Á$8’‚Ÿ¢Ö~–ÊŸ0€[Ó*·m åw‡Ã„KÓ_/ZZ´"!@’ÄͰû[ºqK7Nßv?N½Ž‘ɢÈWæ}ÙObøIlŸïÀоµbë·ýFÀƒu}‹—d\[LãZ2Ë #¼?Д3àƒ•yÓ®ó ŸUv°ÛùÁ ì€2®g%m¿òêMØtÐ%€¸¹>“%É„_€†?@ƒ ¬-ÀrY¹øå4X¿.6Ñ…‚ ¡PÂYâdÀª¸Áf<ú‰½8óâUg'’-!; :l¼îM×]ŒÁ~ ºR{N°Ë;»À©S§Vþ¾„XvƒôHw¸Î;‡T*µâÖJ¥êlX3Äpq0ªe·þ”÷ž†.%«Vxì±Ç022‚‘‘‘š„¡NŸ>sçÎáÒ¥K–ÎW&Ÿ»ï/ÊÍA{äàÉÁ¦SrÐÅùºpu-û6Ô…Ÿ—•Fë-½8ñ·ßp}Ðr9²<’S3(¬Ú069=‹îÛö{ÐÃòýA‘K¦‘M,@ÊÛ2Yǃī,½¬™Ëïàâ3Ï™Ž%X¯Hg;>ø¥GlŒº¾¿pÁ š{»]ïx dù²6$ 6FÐÚßç9;8¤ÔìøÄBU®=þú%\zᥲÇíjÑ>îúÒ±çþ£µ}?¤ggqùùaöÍߘ>ÿØ“'+{dffmÛ˜[â]ûîË5v’^tIyr%ð çìYð-Cv¨èÆpÝØMd:†Ž¥'‹¹šzº@¤ãuÝÕa~ÁœLA•$Cƒ®iÈÎÍ#Ün/ô‹/€ lŸ¼…t™™˜«g¥ì¥dQ€/1 ¤¦chèA’ iÚÀP]ùUY³Íó_ë@â×ÌÝÛT‘jõî$Õ¾gBUìÅ\~ËäÔì\I‡ƒZP9e [;[¡º Úƒ‹-!—•аðÍ5&Ô›¥ƒ€œ5¾®Øâ¡ BTÃ4¡yb¬*îé?¿g/L ³mMûA„úA6¨ú:•àZAµ¶­G¡ñcÐscÐøë€&¡–µ„X­ÞÞÞø!®üÿý÷{«u e˜aùÿe·†ÑÑQ¤Óéº-7áï*pq0ú8HÀø”ØO &Î[rî±s>ÄéÓ§kÒíáìÙ³´Ô‡u) %öÐ»Ž»¬TäàÉéöÓ ‹óЕ\ýIJx°ƒ%§g‘œžÝðwMU‘M,¢qW玚ª"_°rXŸ¾“Ê%SHNÍTµ/½ð®¾úZÙéüèQühmI7´·¢¡­¾cŽh–EsowMÀ'µ:÷,LLU%>IŒ¾ð&^ÓÖtÃøÐÿôghºå–šn›B: >6‡©ÑQ¼÷ÚEÓçúÜq´Ø_‘|l|ž/¸×áaÜ5ó¢7u•¥”3eð¡;€3À@jÊX §¦(ÈÌ%éls¼NC­Mð…C²¼©óÒSÓhê7vq zÈÎÄÐtKÊ0DD×àJrÅ®ÓØÛƒÔ{“æ&©"ð4ÇBrcCLÚ¢œ%òí6©ù$Ð\Û ¬z"JU¶zP-rÚ©rZy1§È[Ïa) Àhô:»'O.•& EW‘wE~* =,»=Ä$ûýÍŽ_Ï“153~ðš´fGø•6+aÅÙÊà_çÚ¡êâr1‰‡¨;ó¥BN•ñ\ò*ž]¼Š¼æÜs@bðxç]øDcù‰¹vˆÒZ#»[Õ ì@ØÝ>îŒWçn væ«n v¦Uk°ƒÕ±h¸Lä¿A)þS–\r›ï¨v¼sŽwîÁx!ƒá·3ï]v|.Ï)2ÎŒ_Á‹± õÀ'öîT[`‡õÇò’Œk i€|4¨EKЇ¨Ÿ½üÙŒ»ƒÛa‡ ŸoÓŸ­Þ þfêÝZ:vÂ6Ü;Yƒ¦é›÷eUÀ†Ý[@×udS"dIEC£¹ÝSõÕëc‚*îF­æË^[è:Àgˆ‚†h³µÛ5ÀC4Äâä#·âÔÈå/Mé0ˆ@7ˆ`?ÈÀ.€tçβd¨õƒjÿX]Á«511‰‰‰-wõ?v¬¸Û2±ŒðT]- ëÁ†T*eyüšå[rp82´§ò.&Ù莃n= %þrUÁ‡t:Ç{ ãã㮩æîëëÃÈÈ~øaK竉 +}Å•Ä-ò‡ºQ½¹:Ë”…2ólYk2.Àg¾þµº…–j³uô¸Ê&°01å(”àopnã­jò àÕïþñåm¾ˆFp÷‰‡ÐÚßëú>CRš{w×ÔœaÖa„fY4vw"ÜÒìMŽÄÄTÅ6T^­Ôì.>óœ¥ÍN·S×‘ÃøàŸ}\¸v7ÔUÙ¹yÒi̼ù&.?ÿ#Ói´ØÃŸ{Øñ¼*‚€Ì̬½ý2Ë»µiFÝ’x(Oç|Óm™RöÚ¦ëZù[¡Ö&Ð,khgz!ËCÈðð5„J+åò†-WÑîvÄ®˜›TR“S†‡åº¶zPeÙ¹94t•Oƒ é4²só5³0©„TI‚/1¿x™¾ Ð i‹àï²ÛÌÄæ¬÷9+.(j!OÕ—R!8h³±£H2h–qÍË'TÈdM?hkªºíCU|ÌüË Š yÝ“'7®W2³ç¯B×Wå«’ÐðªŒ_ñ1tsaôqÐéuŽ*Š!Ht±!ÌHü¦ÐÃf ’,Â4»vÈ«2brq9ïh~ÿ9=ާ—1ïðu>ß²Cm‡LŽ ÷;;FÝ<Øaóc=ØÁ–þm%-¦´v:ì°Y¿°('oFÏÓÌ×&}Ñ€¶¥g¶Õ®:±æø¾@Fîx§Ãéëoàôõ7–í³&ßLsBß~û׿‚§܉Áþ&À¯Ù;¬ÿ\PÄx±¥÷Z!–AÔÏ!êgõ± WoÀbv€cK¦cà3£×Ø v°zMî€`!øÌêñPU6Ê4?gÑÜÛ ’¢jë–¢éhC:¶}Ì?B¨µÙV5c“®þü5\úÑKöÎs‡Û>û¼ïÁ@×À8Þò¹@ž…"ˆˆ¿ó®%Ø øqìÉ'Ï«*ËHN¼g{ºïÚ÷ZãnɈ<ÔIC®Y0Ë TYÅ0®ÊW´»‰1c»à§¦cèh0t,ŸXͱekQKbW®›:'=5máY×~èAHg@± ‚--–ÓÈ/&Á×ìPIúRDø"æ¿Ê-Þ|èaý>8ž%dì!ÿäÂö/wƒMQ0~rÁ܃”šOz;ÏW{Á(Uo41—ÍF«þ@ᤠ™,¢í&ëeûÁÊî $ö:»'O.’®)ç¯BÎÌ:’þ¡ß}ïœûyY@e¥¡˜³HÈyôqQt°A¯£TQ’A/Á‚\€¨+5~’†¬©h¤} @€%)0¹P‰Ky$”<2ª³kŒßäãx:qoåãŽ^çH° wÝ…ŸÙ5«ÅÝÌ+¬n.l`ŒÛü »›aSåð`sç¸0-ÂBŸ¯ìàT?&ìȃ•kÚT”J¿Î Yr}˜g‘ØÐ•¢ ‡áý÷`xÿ=yï2†ßþ&òG«~NÈãÉ7.àÈò»bï_&úK¨¥Ùtì…›ÔÜÓ MQ7u{ 6Féhw ÎñTý±+ .>óf.¿kkºáö6 ~á è:r¨¦a‡üâ"rñ4MCvn¿}þÓi0?ø«6à¬û‹®ªHMMAÓìwìÊÍÆÜÚDžÃC=(‹wtt¸2ob2‹@[“«òÝÕax8` x( 4ötm~Ã(¶åßÒ}r ‘ÝÝæ&F ‡\|I!Ðd.PGW5dfg!º×2gƒ4E­ØµTEAdW—éóâ×n¾ðcüµ»àX­Ö>Ìüæsõ'òðPe)b5‡<‚Ѫ¿ ptŽ$È‚`j7„Bfkb7eÁÒŽd| Ÿ×Ù=yrË:EäQˆ]†&Ú¿¶Ú}û!|ä‰?EÛÞ~ÜñÈgñÿ[ˆ|ÎrzÕ€MÅÛ…Äd}\QÚ›¿ª%† 7€'iJ€¨m\k‹š‚¸R@LÊAuØŽ}^ÎáÿM\ÆÏÒΞ´3A<Þu'>Ô°»nÛ˜&Hôù"ö$f œ`v0öTë@9\;­vؼ®ë¿çØ ˜IË`0²ãîV¢ì„VÿÖ€®¥M  £6Ý~~¨ç †zâììuœ¾öÎ'¦ë/¥øƒW^Äç»0ô¾í*Ñç “ã°DP>/Êà%àob„| |4…¨ŸCˆ£—œ l€`ð£ç» v(Ë Â:ì CG!§B4(жéq K‚ãHøƒˆõêì`hœk2 +±õ×`¹¬Œ†F®ôºiÉq!_Ádä„¢cª£öѱ²­÷îlJE`¹M` Àª=g†>¹ÃÿåuLÌ™{ÆÔ•,4~¬è”PG"˜ÑÁ"Ì¡‰ÐòÓÐ S;ÂýÁô=âÒ¥•ŸKÁ›i50±Z«%*¡ÕÂj¥R©5eôT¾ÈБC C{ªïâ`q)hHKà:>uñ"´äkÐøë-Ú“O>‰¾¾>?~¼fúG4ÅÙ³gqûí·[:_M\Xé_îï$;+‹žÊiß:uuX· ?‡–»QVžü³º†âc%ahîíÞ±Ãĩ؞`c¾páÖfÛ4UÅÂÄÄ\®*u& F_x ¯¿YV:‘ÎvÜ}⡚HŠBcwü µ¿Éck/­ÍÈÆ ˆ"| a„[š@s<9<ߨ*âc…Ê»¹ÅÇ&pñ™çOÙ»&èyÿÝ8ð©O"Ú½«fa]U‘^—š›Ã¯¿ûß ˆækîúÒÑØÛãxžSSÓPûœ ‰77Õ¸[2âåë€#n˔ʸx0>1˜ èT$ Ù¹Âí-Î ŽEKÿnÃÐÆêIÎ,ðP|þÕ™!ØÒ 6hÏ·üÜ<ä| ¨Ò;W é4²sóÐU­¦¥*W6€[„ÚÚÀÏ›sÀHMÇVÆͲUÝi¿äC“p(º«Ã<ðPHõDæêq"ÉkæPše+ž1—ÇÜÕ±ªòÄWqÛ§Xù[ÛÞ~üþÿùm[ ‡ß÷,Nßò€…îËxQ ˆUæÑÁÑÇEà#½GW l&kdýæ—Ën ëy&"Hˆ¸yíŒ""&çT*s/:qÏ.^E^“»Fbp¢y?N´ì·úXà®’üúoÞí†̈0Øl,G ÀD}²Ú°lLËNØÀî2ÚbÔŒà´û‚Õ i;`‡õk•‹ÿÒ4¡eãÇ;÷àxçœKLaøÊ/~8u /Æ&ðøÞÃøÄn ¤aKØÁè!Lö饟—] É,@4I¬¸@„8>†FÔÏ:çÚ`ä³ÍæçZ‚¶êÓ&ÓÉçäxº¾ýq²¤A–5är AÁ0]:oNÂË?«€Ü: B,¨€ÁÇœDº€,/­¹† ªD KIh2k Ê\ÿeR2ZÚ¹ÍËH¢U-]cø±;,¹ùW_ÃÙ¿ü_ʺFN•qòÆ?Uz€˜”CLÊíxðAÕ5äTÂV°Œ ¤Xø)Úñ¼ÄåbQ¯ÌüÙüß󣘗óŽ^çãýj;¼ÁÙÂÄŠyó?»ðÝT”æ ”ÓFØÁÌul(V¥ëÁÆÓsì`¨D¾ª]ÆZƒ‹ó‡“°ÊȻռF ª ,0€¸ñ¤û[ºqî¾8—˜\¦›ûsŠŒo_ù5ž™¼†Ç÷Æà@#Àm¾[Éú²züÁ늦#%HH kƒÉ;üh ùÐômžŽS®Ä&㯜t«;q§(QžLF†PPM]Oׯ@54¶07Ý€Ê@šRü·Ås„®ëP ½=º.@•-û» p˜œçÑÖ胟£lY#iªIÔ6wy``ËCaºÁµÖáSÔÆùœ ô¡ éýÅ#¤Å"øàž<§ÑÕ€Ch ünÊU¹ Á6­¸>è…é"üyËQø!Nchh£££5Õ†‡‡qöìYK+º”„ûI±®ÝÝQj /{ªjsK‹ƒ£ªS¾Ôø¿”•ÆÁÅíŸýd]÷ƒ|²tð£}ßž=^B­Í(dÍ;¢W pX­jLÀÌåwpñ™ç —Åø8ùôƒè»ãHMô@cÑÎvÛ]:<í,å’)$§f*~]Yðêw€ø {ë{zpð¡O#ÜÞV³°ƒ®ªÈÎÍ£¾éxç]üöù,Áý÷}‡?÷°ãùÎÌ̮ɳúõ÷þô\œMñÁ6ë¼›2ãåkÀg]·xž_nu_eE»Û ©é˜)àøÄ"hŽÍ9¸ÛÒ߃k.š:G•e¤'§,¹<¬Ü| Pf{Èn]Õ‹/ /ÂL øÒR6`§W ª48  "|шéóRÓ±›“1Ç.wÔVD Œk»u Ï|_Ôh"ïí@ï‚1Ã^E8$MUQÈdlŒ–<ŽOlÿŪ‡Ê_žõõ¯a÷퇷=nïÑ{ñÉ¿z?þ_ÿcY×[†ï¸ Ÿh¬ü ;|È©rjiYƒŽ¬*BÐDîfp™MJ)¦¤,n*¬ÎË9œžýÞÊǽΑ`†Úc0XŽusíÀ°?Ðl­å8øpOãÉ_]Àçç0ô¾ýí2ÙGm†¶;&–- –-¾¯ŒØˆÇ äc¶>¿×£°ƒÑtkvX÷¹ØaÍ»&ECrAFc3‚$ÊŸ6ùfHcÇ—pyPU¥˜ádFë£ K[8!4’Cl!ÆѰ=ßO(òÀòËê&>ù…ÛðäßýÂü{«Ô%Pí«§§ðÍ›ˆär펠Ûªéý+Ô4þ´ì5è…ihü5牢§ºÚ¿Ë]€ƒÁÇÑj^ðï½k°ëxœJ¿-wZú-ÛsvéÒ% cxx¸¦úÖÈÈn¿ývKçªs/jºÛä¶ŽR}ÙSÕUç®ËeTfÊsrÚó;ñàÉ?óîÃ…®ûv| w¸¥||a[èfYpA?|á0¸` *;ãWv°c‡øHg;î>ñ¢í5Ñ7¢µ4Á“§r´85c@³[vJ›©ÿ¾¡ÿè}`D»wÕ$ì f³ÈÎͯل{æÍ7qùùYJ¯íÀ~Üû§_q<ßN¿}þdçæ‡ÝÚlãnÊŒ<ÔYƒ®,¶rî \oéï1„ÖŸ*,9@¬Rˆ£ác邦HDìÖi—rg¨wØÁ`äóJY°ÃŠs‡¬!•”ÑØÌ–{FÆçÆßÆ {D —‡RÊ 24]ë§ÏÊ+0åÆ|³)#™QU´7ù@’å-)z›ó«†ÄÐ'÷bøÿy霹;-sTë}ÉÕì““!1Þ÷Q~‘C #‡n¦^˜¾ @¦¡¦áÉS-ŠðwôïZÿ®šÊn» Á6j= ªõhqNå¯ç‹Üõ"4¥ e_ãÔ©S8~ü8k¦¿ â›ßü&N:eí}Ôä?‚ÙóoªÝIj {rUóËièb¢îË©Ì< hÖŽÖ[zñà“ÿzGô _CxË ~6àG[Ÿ·ký’:ìC:6¿&(Ù×ð—ü~½ªÖîð@´¸øÌs˜¹ünYéôÞqƒŸ~ŒÏçúþ@Rš{w{vz*Kšª">6Y*~íK/¼„«¯¾fkš4ÇáÈ#'ÐØÛ$‚†®ÎÚ[+©*Ò³³×ÝË{zpìÉ'Ï{!vv˜}ó7Åv¦]÷0î¦ÌxÀC5èʢǥìÑ]†5 Fl¸i) RS14öÐíÞí¿ãÀ\»`xHON÷~À–Ålv6†`k (–õFß&ZMVô5Ú`úœÕ`@)À B¹t°fë@ŸiàA-$ÆÝ^ç­fÛVr»J91X} JNÍ ¹wó¾¾85ƒBf{›+Vw”?ê5¨'OÕZgf!Î_µvhèhÃÃßú÷hÛk>8Û.è¾=ý?0š›ÃSÝ÷V­~WƒLQÚWw}(%‹uk¬«È«2céü„\@BÉ#&å6¾D È•ày'ô‹ì NÏ^D^sn]¤œhÞ¡öÃÎ5`•aU× ª*Ôum 4Ñ>,H0$…šµ~¨$ì ›O“XŸ‚AU!iÅ:tlp¾4>¥Â4¿á—‡NE-8;X8Înw;Ó"lJk§Ã›õ7GÀ‹×0”¾Ãùw2:iuÝ,_»UÚd`~ð¡m•ãƒCàCN‘ñí˿Ƌ³xêðèèaÖ~c`ì`uþ2Bð’^RÈ­=& „|ÌA (r¯À¥@ü ƒÈK›ü„ü4"A$A”† äC‡Ž¯?>—% ’¤eI[a ¾ !¯–îSÛ¸<°e¸Ÿú‚4 ¼²õq$¨ IÁìBÍþ² Š"¶Ïqsz‰†Xœü­8õ_Þ0ÿÞ*sd´vi­ÌÙmmÿ ÔºÀp¿=? ]Zôœ <¹O”ol ü»@ú»Ü78¼ôªæÉÐ ´g€Ð ÓP/BM^, ~ÂèèhMuÅááaœ={—.]2_â¯C]¼ªé®z¼%yª7é taºZ¨û¢ªñŸA—,ŸÏ8ñ·ßØ1AÌ‘Ž6ä’)Hëb·Ø€ßsvØ¢¾"m®ËW5a‡\2…W¿÷¤gçÊJçÈï>ˆ½¿óþšèŒÏ‡æÞÝ YÆž,«ZŽ,vÙõjÝ··~æÓ }>„ZZlm©¹6)¤ÓàcsbÓÆ^¾€± ¯XJ³±§ûú_‚ Ï{ffÖ‘´WÃÀ0®û\õ0êe*‹ëèèpeÞòó‹´¹ËÞÉׂ/‚°Ùj%ÆÞCKéë(’„ì\ávû'ù–þ\»pÑÜß,#=9U¶ËPtÈÎÍ#ÐÔ6ôáúú‘ªc©ë€/Ò!1u^üÚ8ZúŠ7.¿rApmÝJòÖ¶·—_fùŸ^rÿÑKç0‘O;ê €Ó‰< ’²æó‚¬`!+bwkCm}5XžBN-:&Ûc›Ï 9ìzwÛ2a‡åß}UÀÃvu¯)€®ë\®h†,ÙÔ*·e_†,ªPä-æ<’Àª IV‘ÉɈ†­­;–,?À*3ГÜf xHŽÖð`=¢” ÃÚ¾‘¡ 4°6wËðC¾è±ü»'ONŠðw`›nº6°M ئzƵrÁMÚhè]»@w|òä?BK¿em­v醇‡1<<\S}tdd·ß~»¥s•™³ "·”ßëËžÜ;­ÉièÒbqYçÒ²oC˾cù|.À‰¿ùÆŽÚ±¤(t؇…‰©•ïý a4÷v{°ChqjfëD%›À«ßû>dA´œF Á¿üD;Ûk¢¾ý a4vwyãÃSYª¤4sù\|æ¹²ÆìfÚ÷ÀÇÐóþ» ]ðG"5Õª,#33 )ŸßðÙú`3ªØá½×.®)?A ­M9î¦ÌxÀƒ=šÐë¶L¹Õå¡¥7¦.]1t¬Uà„,š³?øÊ×B¨¥ |œËCjjÚàtMC.±EàolA’Þ(\¾YJÕqx 4öô`ö7o™ì±àæXWžkŠ]Ó¶íoËe1Ýnù¤˜]AµÞÒ»é¼RÏsIÊfŠÚò\!˜»:’¢@P”)H+>fÁá y~“ù¸ -ÿªI€ˆäB Èârzy£ýQÒ«û+!vrƾ_.ÄÃßúvßnÏNôvB—ró8yã§xªû^ øª{O)"RJ>’B7×€&¸À]{’4‚ Î :AUá§¶~ìWt ¹€˜Ì#¥{ G$ë‚Ì­+§Êx.yO'.;Z¯‡ƒmøó®»ªÞW_kl;ìâÂkwÑuIY@#Õ3.rŠŒ¼*oÒuSyÔu9EBÃmsÝ€ˆ-êÖHzÕ†`cZntc°T_ºC0‚…ú®ì@hs[ËTEØaõÏËàÃÜ6àCÛ œ¹Ž“£ç1‘Ï ¹q¯ÄgðøþÃ<)~{@X#F€òwý'Ì£hz„(lr.¡]!HÑ`ñ½r4È! cm#lø€X²°vXþYƒŽÉD½mA04i:íÕ¿‹¢V>ì°N¢ A×õí¿´#¬ýβ$!ùœõ³Zèµ¹?È”ÌÇR`h ²R+ ²‹R Ù,o¤P:Ò9Éð@@8Bïo«\ýÄ^œyñª¹g% ]ŒƒàZ]´²µ/š”`Ïír 99´æï P ᩼þµlXõsMhB›Šòƒé{ Êä?B]¼h)‰S§Nahh}}}5ÓwñÍo~§N2²*@‰½z×gw`?öäþ¹Mƒ.ÎCWr;£¸Rjü_ÊJãØWþ­ý½;®«µ#Ë]ª&ì0þú%üê™çËJ£õ–^|ðˬlÂèv…ZškÌðäÛÕ’—ú2®¾úš­éú"yäó··ƒ$IDvw;ÜoëÚAU‘_L‚OlÜY\úÁ‘|ï=Ki3?Ž=ù„ãõ‘™™E!v$í™7ßÄ»ÿôÓµår¯»b±˜çðP‡‡ )çNà!ÚÝaxHM• jJ0ª–þݦÓ^»î;oÅÚaˆ|ª$#ÐÜdkºµ,Y¨0 «-ͦϛ¿6޽÷ßSœk  ¥‚Prç…Ö^į™ ÌVr x¨²Iª™Þz¦ª€ ½Ôìœ%|'+M䡊Yh²5Ÿ„®ÐdÁök,k–X°Pþ(HÆ’ âBÞ²¶{3Š}»Ⱥ ÿ&ý)E@LÎ!! XØÌOÑÈ«åƒÆ¿ÉÇñÙ‹˜—óŽÕgÀSÝ÷âöÝÎŒîswÐtâî 4‹n.¼yIt YEB#³ÍZаƒIIØ¢ÏZ‹>¶q3ñ`³çX¨G»ÜèÆ`©¾ôê8/Ôì`€p ì°úçvèØ|8޵ǻö`dü2N^ziÙÞ]Ãà:ŸÆ“¿º€ÏÏ `èÖ÷!´‹07Þ삈 C¬í¼X\㤠Rñ³Uî>†‚¥ °ð±4| µF¬¤åì—ð‚¼í9š®c!+¢£ÑoÙ‰E+]³I]˲–%ŒÍC&Ehhš¡Pâ=‹&ºe§†£à{Vhmôc&^|Ö£h0ƒ\FÞº(_1È]³¶FñèÒîË¢ÈKSœ|äVÓÀh©K Ú?Ví'kû“$èªîN.9A¬!ôÂ4tµ-["V`ižv¦¶qd(þßå~dž c—]°,Ñ»º´¿néü¡¡!œ;w®¦Ê|òäIŒŒŒ`bÂü†Nj⨦»Aø»v@_öT3Óœ’ƒ.ÎïW‡â"Q„{±¬$îùƒÏãàGzÇSmtyUE|l¢jqGŸy¯¿YV?z´¦Æ\cw‚Q¯óÕ˜¤|¹d B&‹B–ͲhìîDØBÜ\­ŽÛÔì.>óÒ6o¾ÚyøÞ÷ÀÇ@û| }" k(–«N#O@•7¾7ËÎÍáòó/ ;7o)m&àÇõïlmq´ NןÿÑÆgEÚµñ —\÷\íMÁ¶è€cnËT~~¸Õ}•eƱAÈòà㋵ºë…^ÇŒ¿f~<§§¦ÑÔ‹­yQ$ ™ÙüÑ|5f]d·A¬êõ}ó;E¥¦c7'd®€>_xØuh¿yà!ŸçÝK*ºà_o¨©šW1.–%wtÅ} Þ¥k Ô|ªÈÿ/¤\‘/µÚÊhÅ…@‡Z½Î]/óªÈ£»l+ìð‘ÿù«¸ó÷Ž;’ßÜBm{oÁGžø*.üÃ-Ûa*§ÊøÆ{/ãѶCj;ìŠ6Qt SbSb-ŒL-Œßõ}IÔTh6~£ªê7ÓâUi rÈCÐÔ²Ò¥,Im,^¢ÏüçùQü,=áX]H_j½¿×z¤­ ‚·+$YS¡ëú¦íÕïoܶ<²¦BÑ5‡Qô²ÆtR¶pÑ-׿¦ëСƒX²ç cÇí4Ø¡çXwN v§eUFa§ÇµeØÁ¥iù›UØaõÏËàà ­ý IDAT ðäÍê^ú|¨ï ŽïÚƒÓWßÀ¼ú2²dwã⇓×ðJ|OÝv'ß×øµÒm\+°Ã†Ï·_V+(*EE*/­ùl„²ð1ôÊφòC”îI5pŽŽLABs„C‘Ö®M`sׂ2в78¾‹» m =,Ÿ§*š!mâ _'`ÑÑD,QÜÅ—õß«å²[@$ h¢å@8ÖgbMFbËÃàÞfhÂ¥kæç5þz•€g#JÝ,NøwÀM bM¬†!èüµ¥6ºæ½€©eQ>þ]å/¶?å¯]¨¡²ÃØMµELßcÞýß¡KIÓçž?###ª™òF£QŒŒŒàÃþ°µ÷3Ï‚Ùó¯½nåÉsÝÎruXƒ±¡+Ö~ô(îùWŸ÷ú§šP5aYpî?·¬ÀiÆÇáî¡ëàûj¢¾IŠBsïî’ñGžÜ5F²ñ¤cóP¤µï"IB|lþp4ÇUtìÄÇ&ŠŽVPã¯_Â¥^²´iéV¢9ûüº¿k§}šzz@PTMô)ŸG.ž€”ß|“»ø;ïâ·Ï¿E´Vg==8öäu ;åÞvw[†<àÁ¥Ü˜)Ù¥@Ñ!!16ièØÄØ{®| !„ZšL»<¤&§l–UH¥!ç ;ÚíAuàË]3ò7šß¹;¿˜Fn1…`SÔàÁî4%‘­{ûÌ/ŒEº¦ì˜àl;¥[ر~3àA‘$ïaÒÅš¹üŽésêÝM@áãP )(ù¤­AæŽß«ÖA”? :Ô :Ð’ y½¥‰<ò“¯C×[ÒkèhÃÃßú÷hÛÛïH~s I,¾W\‡·ííÇGžø*~ö(z€3ó¿ÁhnÝs !Ê=ëÑ„\@B.ÀGRèæÐÁê.ãN³÷…œ¨)˜’²ˆIe=À6ÒÙŽ»O<„hg{m,‡X-½Ý`jh×ú,MU‘ŽÍ#›/9FI®ðÀ'‘šU|®}ᥲXÖ+ÜÞ†ƒŸù4ÂíÅ1ìDÐÐÕYëYF.žØ{ùÆ.¼bù==øØ×ÿlÀ¹˜6]U‘ššÞØ(WÛÁ€«FÝ–!/²´Nä¼›‡ÀÃ$ú>0èº2t؃kÌé©iH¹Ø`Б<­v{àÂa$¹£bµTIBtw7R“S¦Î‹_Gp©3~Ÿ-‡Ë µ4k†²ŠE”¶)¢»:Àø9Èsí¡ðq0 ðd²Mdós} Ú)¿¶Ÿéšçð°ü2À•/Xn˜ß›ö×—õ£&òPòÉС^´ @ˆHÆÊß:Ôâ¹?ÔÊÚƒCˆ]± v¸ï|êë.äÌZq5ì°ò‚ »ËVèáRn¿ÿîYüuÏ1 Ý5§ šŠk…$Æ…4Z?ú¸|.ƒ-e¾45IED\ÎAÔ5(Ʊüú(„¦rzÈ©2NÏ^Ä/ùÇòs[ OvÞ…^”#P‹…`ð j3w‡V6€Ö-ÝMÖ¯lÖÿlƒtói‚Êß?¯È`I ,IÙ;é?êÅrŸ¬¨KC@ ÓùÒm,¿w³i•àï4ìPNÞJC¸Ô(v°S´ôH€BÓ, ®ÍH_°#w?P~ûKœÛìºâöpè Þ-~«PIG£°aða‡5é­;FUòFGˆ¥ŠÄÊÏÌ–åÜàØ@lݧ3ù-\ÌÖa©:0SGV~7›þ’8…–v…¼ QРÈt ) CÀ¢Át€´¶€àX ½ ˆ-æÀç%0…†f¹Œ EZ·–"„L~K@8báeðpü¾^œü?X¤sæ6Òù1Àà¡:¥Óõ÷É ±ˆ¸YÏË.P ÐòÓË/¡V€ˆ"1O%úаPü¹èÎPlƒ›pCÍ»3¸v×w$:Úªå>¨‰ ¦Ï˜˜ÀéÓ§1<<\Se>}ú4Ξ=‹´…ÝRÕ¹—@En[ƒ^×òT¹©Hƒ./B—Ò;¯è…hÉ_Y>Ÿ ð™¯ÍÛlÏSMHÌå±01YØ!>6W¿÷ý²v‰ï:¸wŸx¨fàÆçCkï† :=¹SÙÄ&¦ 6à‡/ìü¦šª"5;‡|²²1#©Ù9\|æ¹²œX6SÏûïÆ¾n:n†ÛÛhrÿs¦®ªÈ/&‘_\„¶E¼™"øíó/ þîUËשì°øÞ{ŽÅ–‚€a·6õ¸Û2äöhÔ­ËÏ/"Ðæ¾I0º«Ãð±|bB†‡¯Á];ñ´ô÷àÚ…‹æo€“ShÛſtX!•†ÄçhníãvÌ@T%©êyð75™®¯@=4ÇÚ <˜ê›Ó1CcSÈòqÛÏ+öbòõ·Ìµ_>åë÷oI®ë2%»Y¾ˆMX:¯Ô|J.…C“…º›š,@“g!gfAtÑùÁƒ\+93 !fß.ºü£/âwþø‹Žåw3ØaåEAw>óþ-~vúšž-ÿZªŒ'oü¶ÂPÛa×µ¢kˆI9Ĥ¢4‡>.‚(íŽùŸ$`é‹WQSQ$$”<2êÍ51SàޤA$DM…º°á´«CÀwãö`;|4ãànØa3(½\ƒáò¨ºn½lÀ:tddÑQØa彃"¡‰õÃöÈ¢Dÿ±P/–ûdIpÁ&¨ÀîñBØUÎ*Ã0™–Ùñ·kK;ÇÓ¯#ØÁw‡õ?3:Ð'f˜"¡+庿µçîïÆÈøœ=´ÍN¨sBO^|.ìÇÐïÕµytƒkƒ‘c¶››m„¶K“dð‚ŒDfíç!C!ä_!¸"Á1¤!Øaùó .&Ü–„,i¶Ã,KZ˜Ï,Þ~  @p‹t”ÂRÀ$aéº$E «5„dF@<™I7² * 9Ë,0ËPˆF‚€’5˜o ±…ÍX\Ó7»u4ÄâøÑ^œù±¹/”µÜ(MÈr¿SpG4)ÁDvÀÛ‰ÍëzÙ%ÈÈ¡Òm¿äE·iÕf_›8GÔ 0±XXùÛ:P„\2[9kìÐirpTtÇÇ¡eÞ‚.%MŸ{êÔ) ¡¯¯¯fÊF1<<Œ'Ÿ|Ò|Ï’Pâ@w<èu-O•“Z€&Î6mvTSÓ¿’…2÷ã²ÒøÌ_} mÞw[žÜ¯\2…äTuÖµã¯_¯žy¾¬4~ô(~ôhÍÔ·¿!ŒÆî.v¨Iù&&QÈò†Ïiëw~mªH2&&! BÅÇë¥^* NÚð<Àq¸õ3ŸFëûöŸEI¡Žvø#î—‘_\D.žØt€ìÜ.ýà‡ÒÖÁÑJÀŠ 9ñÞ¶e)GF`‚ @®ýÂyÜuÏÒÞ]¾b±Xª££# Àu3Ž[]| !„ZšÀ'Œ9$$ÆÞC÷àÁš.òâo¿ë8𪢠;76€¿1 ’®ïá®) T¥ú/Âm­0&8íæ½ñûPHe\]×"ŸG¨e{à!ÚÕnxPø8€ðdNVv×v ›ƒ\e˜r4}ùó ¼PKÍ–WáãPø"ä ïÀɫǶœY ?0 u²ÔƒÄøUHI{”¸PëØ}»s`€T( 9½ý [ÖïÇGN~Õ6èÎÌÿ¯d¦ð×=ÇÐÁ]Ù–)EĨ2I¡‹V5ŸŠ®rJX釫œò[Ì—T…v3¥~ŠDA•ÖÍ;íê |¶i/þUË­ðS4(Çîjv bÅå"HìÙÒébóòÈ«û_•aÈÊÒæ®ËéÚXÿЦAÒ°Ufy7;Î ØÁä1Va;Ý,9Eè6äI·Ù½¢àDYN u;ÀÆ´Íô'·Â«hÀ€,Ò@|ãû¾¡¾8ÞÝá·~‰ï\µŸž3coã•ø,ž:|öîpm0r ±Íø«ì°Ý5yA/ÊH¬û.9äg°ÀÒ$>C!ࣶL/“—ÐÜÀ¡IsàŸŸ*¦úÿö×°Àoâ¦ç_µP²îµ >p,…¹…ŠBÈljf‚te›5Ñ)d À*Îéä#·š@ãÇ@ZvypOD)A‡Òµ»ã9ó|RFrdpÀ¾äÖN·µc. äP§t(?èÝùú²túðð0FFFjªÈ'OžÄÈÈ.]ºdú\5ñ2¨¦»ŒwrðTö½,¹c˯Æ~ hÖúcòet:èu$O®ŸX4¼i¢ÝºøÌs˜xýMë>wŸx]ßW3õhŒ¢©»Ëëx5 äô,’&¿+níïð;š¯j¹±”;^7ScO>ôé¸$I4öö€v¹SK!F.ž€*o¿ÁÝØË0vᕲ®Õ߇pïŸ~Åñò𱹪€«Ý‹Åι-Oð`ŸFs[¦„4·r_öàÚc/=cW®»x€îÁxû§?7uŽ”ËŸ›G¨½­"y”òyHù<üѸpIÖåTDw3û¢æG\~1Üb Á¦(h–u}]kŠE”@s[ç5Ôjþ ]S ‰¯rª±¾Ž]œ±hèÇ'¿þ5´ííwn-X(`þêt#V£~?>ñ—Oà—ßýnüò×¶\ÿºÄŸ\ÿ†ÚãDó~÷¶«¦âí® Itsat³aÐDåÖÍ:t$åBÉïb󪌌*m 9,‹@“•Û%‡à§ØèÁiW‡DzñÕ¶A42>0$ ‚ðæ'’ V€“^_M¼,[:Ö:ì i*„-û¸îHTl©1ãØA·u2•7;a;@«}´Öa‡JÃp8}3p‹¥ë9;l÷™Ó?7+Å1H­]·D§o?Š¡[àä/ã||ÚÖióz6“¿|C‰ý8q×-@HÛ>¿µ;”g›¥g ­P^ß⪦cz1¿¦_8M!à§à(p,…[üÊg!»äò ÃÜá÷S(äU(Šf ìP¶»ƒí°Ãò‹ˆu.眀Á-»"%êÒ—£>–IP ’¨A•9¿q7;–#A38ŸköUŽqƒ{›ÑÛÂDÌÜ»C î&%˜†zz3TI®Ô½cBÖµWl¯ž·{ŽíÕt7ÔÅ‹¦Ï=sæ †††pÿý÷×T™OŸ>øÃæOT¨s/Þýû^·òäœ4º8]•vl¨ ?‡.-X>ÿàGâöÏ~ÒëKž\¯Å©ä“©Š_W\|æ9Ì\~×r‘ÎvÜ}â!D;Ûk¦¾C-Í5•ß*E›0åêá–f„[šÍ[5¥\2…W¿÷¤gçlM·ÿ¾¡ÿè}+¿Ó>‘ÎNWÃFA‡B:ËϽ€ä{ï•u½ýw~ù‹Î¶o<>‘p,}£°ÐîÝÄ|™ò€ûäJà!51ö#î$:£»:Lݸ„ _ƒ»¡[ú{üÜôy c7*<¬ÜTRi™,| aø"õgå¬Jîxñ@±,‚--È™¼)ƯŽ#øAP ‚$¡ÛDšQ2ÑŸ2·o¾ƒ¼˜Ëƒ ,9 È™Yp­{½;Š“sogGM»X}q`¼~Üõ š³Ô^tÈý6±šÈƒøÓ³ä`¦ÞdRrRr$Û¸t¨é-ë–®)ÈO¾n4ÛJ÷݃O}ý/À…œsÐT‹S†`‡ÕúÀ—A°¹oý?µç…*ãïgW2“øëžcQî<]øƔ˜ED7†ÏáñU„„-¿—MÊ2šˆ¤,@Ô·%KR7 hþ.ö+üSjÜ‘kÜÂEð•öAÜê(‚Îþ­ ë2-g©•  •ñ/Ë’H‚pìYÜ>]ÂþŠT ŒÕònvœM°C¹}²"€‚ÝiÙpüvy‚ÍiíØpÚ9¢Ì>PõÉ·ÊÑU2Ð`Š„µ5mʧ¯Žbø­_ -Û÷þ,§Èøûw~ƒÑÅžºó„z ómfdlÛqŒQØÁ( a&Hß&ˆ€Fº AUõÿŸ½7 Žã:Ó5ß\kª kIeR¤HˆÔB­„ÔZ(Û´(‰²å­E‡[v÷ŒçšrGŒÕa{†žð)¢°§í‰;&.iÙwÚ}GÖfKb_s%‹e€¤¸‚ @l…B¡ö%+×ùQ[fVfU_Ū̓ç|gÉ“'¿ç¼“}2›‘Í‹ˆeòÓÎw;iT»p¼þ*^' š&ç¾ÞMŸµ Æ#“Êà’…I¿Y{ธ]Ú Iš'÷‚±ª ’¸8ƒÎÇzÕC§ŒËZ‹ý§´K;“ŒÓ²;ÿOëg®~ˆLÿ ð±v(ÆŸù4¸Ðydz?:oX ¾m³ûÚHØaÃà©—ÿ7Sa‡Âü¹B.§/ŸwãYCÇÅîLÏ]zÇ/Xn©Ȩ̀ð×Ô0.äÆçÙi¾xK‰2Š÷ÿÏ_;¬Ü¼Û^ø¦ ;Øf¸Å†F0z¹×r°ƒÈ eé³Ýo½‡óš¡°C`Å Üÿ½ÿiìàòù, ;ä Dz® 9<² ì rº_;ˆîƒ( v`Ü.<ò£1v9Ñk×L…>{ó-M°`i…K>lÚ[Ág]VÌI’ïAKÇ­–tZ]Ûr vŸWulèüK–#¸®‘ÞÍç%‡PÓ¶ª,y–D™È8¸xN¿¬ÇSÑO‘eˆ¼uÀOm-"èÑtNx ðÀ¸œrœå}žOgg¨®(² >“-ôïÕ+T÷ïk)iÈgÙ`mËÕƒ =ˆÕðÍ ÂÉ¢Š^|Sƒ\ªrƒÀ‡Î]Ô>¹³˜ºƒ"‹Óc#rq»ãšäc!9!9’q‚ñ/ãk²UŒš7ecÈ Ÿ1 ÎyâG/bÃç5=ß±ÁaäÓÅ«¶n¿¥ üòUd¢Æ¼ð™ªöðRó½²ÖŸ‡†ø B|~Ú ãEãM³w@^–æ3ÈÊ"’RI‰‡¤¯ôÅ–Dù`º¿€_Œœ2%í{«šñÃæ{PM;J9ºÎþ5aݶê$i¬Ñ ;€¤ÊEUaà$qötKà{Q–gú¡œ°C1mÒª°t¤¥Õ—¥Pc02-KÃ:ëÖ†Ê÷¹Z*üa€85­ùù^¿ÿ‹8Äîï£?“2lü̈^ °[é&†þ Èúã-Ûó¨n¨·•mÖmã¼€ñþ\éãpâ#£8ò«_<}Ç®hݼ©¢|nÃÖ7Y’0z銮xš@sÍM¦å-ŸÉb¼@3„QŒ ‡_} cWû M·íûgñ»|>T/k²\›Ð¢è§êX±÷|÷…i@ˆeK‡F!ËæÍû>{ó-Œœ>£ù<†a¬:LôY1Sv”qÖeÕŒ…Î\²,ð\×®: :‰‚K¦gX—ÛêÚV€fYÍ÷#§Ï– x˜°Å>XIݼ šÏÉFÈDãðÔø UxТ¢¢U &ÏèêàoêêR.’i‚mæ˜'àŸSc1Z&ŸU†¸>Ëä=>2Šl<¡}2\mþ#ec’!ˆé1[Å¡” ‡üØeäÇ.ƒ©nXÒáµ£wa#9.tÞ´^žøÑ°æÁ{J°‘Dz,bHZ–exü_þ >øå«_î5,Ý™0þáÊÛØU»»6VD{ˆ‹yÄÅ<¼?í„—dá$)x)v^BTd¤%¢¢ -óàdi‰G(Ÿbp@E`ÉÒí’–x¼<ôŽ' O»‘ñॖ{Ðá™C9ÌVv˜4š Ñám€ ˦©¨ƒpQ´ªcUùMg`¢¤Èf¨™˜;Ì‘/Kð€)¢‹v€Ái¥µÔa‡ÙÚ…)`ƒÎkX"ÿK vPf~¿LêE`€¸éêlhA×ã_Ã޳ſ]ꆑö‡kWЋàgwßà-ìì>Ô© ëBE{˜'?¢¢ šâÀ‹ò4Õ–&ád(Ð$ —ƒ1I ¨ì :`‡‰2øÜ,¢©ü‚i:˜…Õ8AB(–b7ÖMüU,ü^üþªBN›æêë‡ —‡ÒV§%ƒ&*9w=8¾=Ê È‰\À”e˜ÝOÜ‚ÿý¯ÚÖÒ½ Q‘f}up¨H?ÛŶý\Šçúe;!\ù¿4Ÿ7¡òÐÙÙYQåݳgöíÛ‡þ~íeâðÁÚÀƒmE _". EÊÙ¾ 'NCá†uŸû—¶cõÖ;lGÚfY8c½ý% œž°¾O»ñÉÁ7uŸÏ8¸÷_6u}£¤(Z–ÁU]e7> ŸÍ!téŠæx.’¢Pß¶ÒT˜%‰">*©?Æzûñáo~o¨ªƒÓçægŸAUãô«ÁŠ$!—H ©DŽÃgo¾…±K—‹¾~˖͸ç»/€u»M+_j4Œ\ÂÜ59½°A ˾€î³ä³³=„c¡P( |VË›”ç:߃àºvËùÍ[_g•œJZ0t¾­wwX®Áu«5ïdÏg2H†uÇÞF¦€¬×GU’¬˜þ'æ­¥†Àzb«:Lö§¤qýkþ-ÖaÛ o;Ò6ËZ.™Blp¸,°Ãåã'Ðýö{ºÏ÷55âÎ]_‚¿©rÞMÃ3N§Ýø,l©È8ÆzµC§¬Û…àš6ÐsÔØeI*l ‹WT_ÍêoYƒõ;¾ú¦¾P½¬ .Ÿ5B‹IB6C6Õ¤zpíÄIô=V´ªlùÆ×°vû㦕Qä8$FFLßH[/ìXZÝ¡PÈ’™vÄ“±Ö`›å*™¦Ñ÷q·%¨k[®¿bIà¡¥ãVÍÀŒ÷^µð0a’("O€K¦à¸>´õ‡ «)<€§¡^3ðîéÚέ H$MC­¿#úT•!ÇÍȳ¿9¨x²qØfžÕ.ožw XL–M$+ZÍbèÜEí÷|oyäbÅô„äÄtÄÒ>Ö´q9hY6ù· _îŸË!>8‚Øà02ã1ćF,Sn9Ÿ:‚¼ Ú[¶vHÆ^hšóá^‘ør>mHz ímØùòOà+Ñ‚d¤·ŠI ·›wí@Ã-møøÕ× äŒƒš®p1¼xõî«nÁ÷‚w Èz–F[3!HÀM±%‹Íÿs¬¯ }dxº›< ø^Óhw`›:[ë®ì7AÂK±H‰*Ÿ‘ÀIÒp/Ê ;Ìa´¹› 4 äOžÊ;Ìç[5é•v€ié*£þ/Å9E©%Ì#ˆ$0ì¤ä°·ÿ‰O‡ Ÿ¬[ƒ½m÷¢cm5@˳_ÓhØPQç„þ²a‡™ßù¤Â¿>ÈL„îlhF×ö¯bïÙñÓ³'Œ[/üäoÅóɵؽõÀ/ÏǹÊWFØMq³Â3FjOçQOU£a‡Yî >'ÂIn:ôpý‚ Ðàs‚¦ͰÃlùgxD¥<2‰ÂšQµ›E Êj·snȃ ·—Z`Wþ#¨ê Ýl³æØ)Cɇ¡ˆÛ“¾<¤ÐŸuŸîð¸±ãÇÿlûÑ6ËZ&Glp¸,×>yð ôzZ÷ùõ«VâÞo>[Qà€ ;T†é…ÍM4›·q­È ï€À•nSMãÐõÖ{EõÕÙì–GÁŠ»f‚¹V$A@f,¢Yñ 5:ŠKïBìÚµ¢óÀ¸]Øöâ4®[kZ9³Ñ(2cM0‡ævËqè~íEù„eY«ýV͘ <k–R±˜eU‚ëÚUÃ\*ôXÞzkÉ;«½ð77">4ªé¼hïU4mÜÖc­ .E–Á%S“àëñ‚v:,ÙéYÖ,±U «jhÀØ…KšÎëé»Ño,xƒ‚ÏÕ¶M­í˜®òÀ%g¾4«k[à¸Æ:!ec ÜvÛ‚ѬöÝå˱{AY|#ŠÈÆ×ïê/ýˬ±CB|dÙ¸vI5Ú[ºѲÀAˆ@HŒ@‘­Ëxjð·4!в kÚàoië2çåÇÄ®ý-×Oû>|¹±Áa„/÷"|¹×Ðq}÷LqRõrùÁøšlEYÆÕÜðÃÚtC{žûÅ+pxK3×KŒŒBÈ™+Ãݲq=/-ñ_þÚp°çxrÇ“ƒx¾á6ìª] /ÅÚRË3 Iƒ,‘äå˃áÝx¯±ã6Å`wÃFìª]`qËVw˜þ<Ízf@B.ŠEI1#€޲y(š5Þo Ù~&ãý¯2-Q‘A¤†ô „ˆ"Ú¤`£tÁ%€0ô–ÑPX`¿8 ¹Ø:ƒo]|gÖÜü1rŒ\ÆM}»ï\ 0òÂõRØA7aØAQ²Q’¡ üMS$Xš,-ì0õó*¾<\c›–ön¸;›Û°ûãC†ª=¸r]Ñ~vÿ]ð®$çi×ó”Cë1À¢¬íñúïÑLM·¾kª­ç‰ŸI~Rœ€_¨LŠX†‚×IdÐ €&Œ¤P4 I”‘ÌòHæ k±†Bµ›A Új7Šºqÿt{éérìV…&ü,¦ºÊüù鈌ù; S¦<Öî|`%þíµÏ´Íò2½@í]ó2Š“ß@bdÔ°4>6=û ªgÆYvà³YdÆ"à³YM片ÞcàÚ‰“†ä£aÝZl{ñû`ÝæÌ!IBbdùTÚTЇS¿ù-R£áâÆO’´êpÑgÕŒÙÀƒ±ÖeÅLMHŸô=‰º¶ Ö òÖ×À[Wƒt$ªêøÁîsXûÈý–ósp]»®€ññ+èÁª–OgOg@³,ÕU–ƒ3ÌêÓÝ®u(w¹<ÆzúPßÞZ2YT‚¥#QÐÜ,“ÚÁjêß“ƒLÄL2OmB–[ô圀 ã˜ú‰¬UÂûOuk>‡ iÓY,¨9$F 嬥Æâ©)(7LüóÔ– ™ÈËç*Ì[¦ÂC§Ï•5oR.)G>|¹>ø—/yÕ‡üøUðãW KoÃà‰ÿ ¤s¶dh´4ý­6€íÿò}|zðM\:|Üðô„Ïààøìª]»¨ÁÂÀH*–¤À”ùó>‰Çž«‡p…‹šî&O^j¾wauv˜þ Êz°Ö];kyX’B-ë'Iàe¼"CQš á (8H ”¦Àþüf@pb!,¸|°ˆ‰<¨Mo©Á%8Ǫ†Þ´J;ˆä$ìp8~mNØaª}ëü;hu>‡Î͵ê}iHþ-˜–šï4À¼(#NjȋҬÇ$MÂÉÒ`RUš³Ž=Za‡ óÊÀ­ÐÏ©éïÔãðß=½g?Æ¿]ÔþL:—uÇ"xîOïcßÖûѾÁ0 äSÍ8`"ì’¤ vIV H2Š4 ½©Cª\ ª\Œú±Sì0aN‰lR™¦R–$Œ%%Œ% ëL*^|U,ꧪ;ódÎä{‡ä|!Xž,Á+3Ê],Ì4rÊmÐ <ä#€œ/@•ð|Uu[Å¡"ýlÛö³Åj|LðP©*­­­xþùçqàÀÍçJ‘£ ë°Ul›gh“¡Q(|ÂöÅM㽜¹ 9qFw*[¿ú Zn»Õv§m–´èà0²±Ò¿Ç8‡õjQÔ›¾ðÖÜwWEùÛ†*ǸTZ5T@R|ÁSU€òÀIÃç.âäÁ7 pyÃÒ¬¿e Öïø"èYúA9aE’À¥ÓÈŒE ÚG¯8‰Þ£Ç æñÕmOïÄÆ§Ÿ2­¼ùT ÉáSUã` ËÆA¶jÆlàÁXë³b¦‚Ayƒ]çÐzw‡åò\·=ÇÔDG® À‚ehGÏÑ“šÕÆ.\´4ð09Xó<ÄÈ8²Ñ^UU éò!BÖšÀŲð64 Övs:}õí­`]N…;xëjTÃ8ñ¡üÍAm$YFbdîrÖµ-ׇðå^d¢±²äI‘Eð±ð±%«ú rç!ç 8Ùòå'ñð÷¿[º2HÆûJî»Í»v eÓzûå¯ W0ÉH¢H‚bZИcŽfõp1ì¹ú>2’`XšªUv¸ùùsØáF¸(.ŠÖ_6ÂâÇUzÞfMÓʰƒÉy³ì@˜œ–ѰŒ9&ÚÛÿêf·ûü;è ~X–ÓW'Få_Ïw09}°ƒND6/Î (ŠNÀ‰šDµ‹E ¦oì0õsëuµ‡~fšÚƒŸu`ßæ±³e5vÿõ}ôgR† sQÀ ü?LnÁöÍË€*E=ì@,pŒÁ°Ãäg °ÃÄgYV ;ëÏu-aM>Órí9ÊMRÜÕ²) Š2;œKåÏðpg)„²Yk\¨ó;ád)õ}ÚŒû±ÚcÅ4ÀúÍŸÈQ@ÌÀôàbâÆ%:oo‚ÏÃ"‘ÑöîBÎô¶YþÙª´ê6äP‘~¶‹mû¹‚Œ`k ÐÃè{šÏ­T•‡}ûöáõ×_G"¡1(]â Ž¾zÙ“vñm–‰L2,¨P^öŒœ‡4öݩկZ‰­_{Æv«mÖëö’„Øà0rÉTɯmìpÇ®hݼ©¢|nè¸úRc®*/êÛV‚v8]=÷ŸGqî?šæ->‚wÝ9ëoîš@Y`IFÁź‚ÿcý×ðÙ›oKzêê°íÅï#°r…93©:@jtݯýÁßP.?š²êÑeÕŒÙÀƒ …ƒAKæað<Á®óhé¸Õr*um+ÐsLÝn"Ï#t¾Áuí–óspÝj vŸ×|“‰ö^EMÛªŠhçŠ,ƒK¦À%S`œN°^OÙTdQÔ,µTJó6êÎ\@ÇÓÛ í£ZÒR>[]Ì׿ûNh Þ–óiÈ·äw7Ú|M¦“¬V°©êVHîo²Æ=|øÜE]ð í­3vŒ¸‚šC|²` U ‡æM·¢eãúŠo«-×O–#68Œþ“]þì’¡pYò³UøØøñ«†‚~õ5S”K;øÀ’$ò²~‰Vš á¤ÓóùçX/~úÄPØAµªƒ¹O7³½`‡9Ͱà@E{š„Ê:)3ì (E¶Ÿ¢üRæ€#¢Dý…PŒÉ×RfóÿÔcr@}\GâêÈ~.‰®±:–yL€nþ¿I0B)ÒŸå·/A%ð¢\p× \ÿ,ˆ2¢é<¼..–*ì0ñÙ#ëóè!AÞhjÐÙÐŒ®í_Åîჽ† 3¯œ>…®È^ÚÖÔËs—c®2.;ÀØ¢Iõí¼H`ÁzÖš&aR €¤ П“!3×— ÖE‚¤¤sz†ô %áu1Öº¬užO£œ°@‰(³Ÿƒ €b ªf‰i@SçíMøãýÚf˹! €GƒÉW°!‡²>ŸÙŶý¼®RähᾤÁŽ9‚¾¾>´¶¶VTyý~?öìÙƒŸþô§šÏ•"Ç@Õ=P&¥Û,;´òQ(|l){`þ~3ö@Ö÷>ÁáqcG •­m³M­É’„±Þ~\éßiÇGFñá«¿G6®/ø•q:°í…¿‡¿©±¢|nÕg¬Û’¢æTy Y «[á¬òššã.i8'¾ás—Œ›³;Øôì®9ƒø]>ªKÛ¯ùlÙhTwàjt—Þ;„صk†åiíãá¶gžëv›RæR©:LøçÔ«¿5Dñ‚©n‚«¡H]²êÑgÕŒQ{÷îµGtí_ÿõ_¿Àoµ|ɲ žç!KdQBÍÊfKåv°ˆ…À¥2ªÏi¸Åz€€;àÓ <@.GÃÚÏUÞCƒ(BÈæ d2%À É’]?‹AâËú‡fD._Ñ6ÉÊåÑzwX· \2 Å€²Èó_îS݆ý-Æ}³»Î«–F›¼A9½ Uöe“²1ɦsªëëP»|îñŸbhÓ^L÷‹("56>­ýt¦ê\OÀÖ-åß5á‘5ïþ@4œAc¤c…äøñ«È‡/BÊFË®èà© `ÕÝ[pÇsOaÓ“O eÓzT76,º>íª®Â¦§>»¿þ,Ö]³b9X— @aÇü'~ü<àòÑpö÷Ñsì¯eÉ—œOƒ pLuho]Ñí­Üc'?~|lÀÐt^žûù+hXSú/£ýƒ–ñ節[а¦ ÿæ5„/÷šr Ňá3xÜ߆]ukÑî Tl›$8HœÆ±—@i`‡—?»qãêrµ3€—Zî±@Ù°ƒþcÍUv A@6* Ç4è@1Î×Å´Kv(_Zsþfì°Àÿ3Å-íöq‰‚B„W0)¿:Ç'‹Á ¤r8AZàxŸ¤²<Êš&gw#Æ™ùÂd`Cèc€äô@ïÝmëÐlÆÎ£o£;1ärWR <÷ÎûØwß}h_ïØ9êÂØAmŸºé·¯£ñìÜ£þMÇ»4‚P_OzÚ´šqÉDØáæÿS ¡¹¬‘‡H’CÏPu>Z<3áÓç3ó§B`MÞÓŠ  ÿ“×`¦”oçƒ+ñâ¿k[[PòXÛH£Ö(lÈ¡¬Ïbv±m_Û6£Îèú ¾§9•`ß¾}ðûýUz¿ß½{÷â[ßú–æs¥Ø' ³U–z3PòaÃ68ZŒ7ELA?®ûj·i;Vo½Ãnl¶YÊŽÃXo¿æÍ7° ØAàô¿úšÑùÂ7+´,³a‡ µªºZ…d(\˜s:4A;%é3éÈxIË;ÖÛó{Ýýt6k{à~´=øÀœ¿ÓN|MæÇºñÙ,¸x¹"‘ãpíÄIôûÀм™­êK$–DÕ†OŸÆ¹7ß6$­ ØH)gÕ¡¢ÛÊ㘠<o‡l³Z¦(júî„=ÇN`öTëÚV€fOBTIš…Î÷ õîË5€–Ž[50rú,Ö<úpÅwã p²ÑX· Žª*P¬ñhf,Rþð-oA´÷ª¦súNtaMçÖ¨*>þfõŠ \ÒàÁßÔ €' ÌïuNèlC儱,±/OâµÕŽéAóPȧÁŸW @œ`J‘[=Õèzâ«Ø{æcüôÌ CÊ›ì9~ߋ݆í,˜›òCÙF 2½9Ò¡I >¢™<Qž7-†&ð°EB`=Js^×ä{îy%¡h¡X^ƒ–zêüÐiò|FÅqŠˆ€6ùv‚þÅß,'"žæ!I…¶CQü^n'=½Œ×›VkЋ•A/úCÚÖ•Ü—µÃ'‹Çú‹Tã°!‡­ÊØÅ¶ýl›žz£\ jî€ýDsªûöíÃÞ½{+λwïÆÞ½{Ñß߯ù\iô=ÐËŸ³›Ô’ìF2>EHÙãÆBý$ü@Ö÷Þ´~ÕJlýÚ.»½Ùf)Ë%Sˆ —vèû´Ýo½§ûز[oÁ»¾T±°ƒ'à·`›'à/iÊ’„ñþAä3¥ÝÔøòñè~û=ÃÒ£¬ßñEÔî–9!Iþ–eÎ,@’K$ JE¨dL€×Nœ4D±`²mÕÕážï¾€ÆukM)¿$H€ÏfKÖŽÌ‚€”9«}VÃlàa‰T8{SÀy¤wñ¡¦@hÓ£ƒÕ´ ||(dÉàoÂYåÕ¼S~:F.ƒ+XA‘eäÓäÓÐ,;©ú@ÅËgÇÇ+&ˆÙßÒ¬xˆBÈq†J…©m“jUV´Z]Û Ç5¶!r>]1Á´¥69¯}á.°,¸`¿­èEDræØ:U}~ýªò+<\>®=p„dœ Üêî²ÀALAHŽ@Χ-QoJ-×/¹~̸\¨YÙ2©ì0Ÿ9¼lùÊNlùÊN„/÷âÔï^Çåc•EíH‘ÅIødœ \Ë*?˜ :À†'ÁÃ{¾[ØA–$$B£–mߥ—º3atgÂØ>í6l÷¯FõTÖx@R 9I„2ÏK+Š à$„y¸CÃ˃;x(ß Þ¡H)µÚ‚…Õ–ìpýXÖàÁ†4ž£1-]y+#ìƒÓ*'ì°@ã¢öÅñVgµù] ý:ë­Œ°¤òÂì°ƒê“ó;EA<Ã#àuLÏað5úO¸Ê¹é™Ý{ÛÝèl(¨=$„â׿2¢€WNŠP6‹ÝÛÖÕŠöüÎ×ÔögböcšD£Ï…l^D*/À‡©ŒT¹T9$¡®®Õ–*Û™sBãÿužŸÎ ¸p-zˆDÏÖ¦*8Y e5)4@š¸Ã!©í74žC23³¦³ª=,‚µ××HSb:ooÂ?]Ööì™eEà uîämC%Z‘±‹mûÙ6êŒj|lIyê©§´ßºcŸ€ Ü Ò»ÚnfKÉä<”|ŠÄ/âB3ÖˉÓP¸aÝç?¶ˆ)c IDATçáð¸í6g›e,‹#68\–k÷}ÚO¾©ûü•›7âÎ]_ªH¿Û°ƒmZMà8Dú!•8ÆïäÁ7ÐÿéiÃÒ«jlÀÆgwÁåóÍ{\õ²&P cxyŒPsÌà¶§wbíöÇMSuÈŒEŽ”vsìÞ£Ç S¿`kWÁQ»jÚw„,XµëvYy\±‡%Tá4MCoô=‰;¾ºÃRy ®k׸Áz<`Ü.ÍéÈ¢ˆÌX¤¢vl÷66è:oèôCLœÕÕ—LÃYm,d@;XxëjŽD5'¤Çà°‡ÙûƒŽùU^(òâ| ¢È2¸ÔÌÀïÙ¾³ªÅGF‘Ñ~o[(È|RÉ!³ äàonª­[°jëUÁþ‹Ñªƒ¨j¨©ƒîoXÓ†'~ü<œÎàòÑqêw,›êƒ,p…Ê”ÛÚ[ÊÉ”oW)ƒ AHŽ˜v‡ÿËw°å+;ËVÆÄÈ(”2ìZ£jÑ%—Ãǯ¾fªºÃl6*dp |Âg𸿠Ûmèð4V̸@$¼4 Q‘!+2DE™òŠ A¤©yèÊŒâÇ׎ #³À²ÚÀK-÷ Ý©ê6-pO)ñõ x¶tú ;°«-K)ýfTºSüï¤(¤Eb^ðǶS®ãfkUØ¡çÖ¯Q€‚Ñié5µ°ÃMßùiís¯>.  ¹¸z# :Ïì´Ô|w“ïó’NTœ£óó,i ¢ŒL^„gb‡öRÃÖòÀ0 ŒNféllFßÎç±óÈ;82¤Ùè¹€P.‹—:;€FYC~UÂÄu¸@Ûp;i¸4(௫=°4yCÃhe½åÐ|m c^±°ƒŠß'U¢9ø½,Z<¨ó9K«î0Õ„ÀRaÖk4 €¤í¹i<‘Ÿv˜°d†C“¨õ9fkj5ÈYržL:Õ>ëÙ€Ce?3ØÅ¶ý¼´ ÁÖèRyH$Ø¿?vïÞ]qÞÚ¹s'¶mÛ†#GŽh>W}¤÷Ÿì&·Tz–€’Øã†šÔĤØ'ºÏßúÕgPß¶Ònt¶YÆâ#£HGÆËríba‡[ÿîAÜúwV¤ßmØÁ6­V0Ià8þÕ«ºân沦·ás>zEo]UU†]WäS©¢ÕsA‡†ukqÇ7¾ŽÀʦÔ)ŸÍ"9•ŸÍUÜôËÂ×ҌĠ¶ÀCg ÀͲ†þæ jH(‰<úÅrÍÀƒ”7޶LOà:íp@Èq‹ÒÙDrÖñ!ŸVï§r/Ö]>þ±®óÿò™,é1H¹8Äô˜.8Æ c\N¬º»9Z–-Ù¾ëòùàoiͯâãðz°áóbÃç-»êPPSÓˆéÂK† õÊí/ !ec3ÓÛ½ÃëÁS/ÿËoßX¶v$ò<ÒcÖ|™Ãçrøû~‰øÐHYóñn¼ïÆ{ÑÈx°«n-¶ûÛà¥ØŠ'h‚¥ÎíŸc½xeè#ÃÒ{Ü߆ï5mÑîwv˜´µîÚâÔJÊXÄq¸)=;ó™š7ƒ^4›;}ŽÉ †ÑŠ¥OŽWà¯èJ«Ã«}Ã…>.¡¾|a¢ü0çw¥Sv¤r‚Šst~ž'Í '€¡ ° iì5ÕÂSÿ6 @ƒœwL š&àg8üèSØsêþíB·1ó·¡kèy#}Þo+iØaº¢Ci¿¦–2è-‡ækkèïFŒ§„¶ôâiñ4§ƒBk°ê†bA)çÀ'Ögô@jb©¼ªcU,Hrza;;´¿»²b !A{Ê¥}îoð£„m6ä`ûÙ¶RÔU÷ n•‡J`ïÞ½x衇4Ÿ'g®@N_±U}÷’ ªbf±Ì´”¥ð_Y_¼AˆuØúµgìvg›e,:8Œl,^–k ;ܱkZ7oªH¿W7ÖÛ°ƒm–ï«ñ‘Q|øêï‘' KóÖ_À² ÇÐN<õu†\3ŸJ!—H Ÿ*~3S3AÆíÂßø:Ú|Àœù‹ 5:jˆ´Z)`ÊÚsÙ>+gÎ̱n–›¥0 ƒüMƒWÏÑ“…à|‡u‚Z:Ö¡çØÉó·Xw·VÚÁ¢¥cúNhá7rúì¢Ty˜Í¦Â@³,rú®µ"ÏWä0£­.oÑ < Ÿ¹!Çv!åâ°m¦é|M§@o‹Íæ ðN«ƒl§£¬ù8Ãç.j>txA2NÈù4Äl R.6lnkÞx+Z6®Çª­[–t¿õÔàm¨3MÑâfÕ‡ãÿÏo‘ …Ë;VMQ‚¤A:¼ Üø¤ ÜýiçSòiHÙXÉî ímØùòOàk*ï<4Ú?h͹E`‡©6*dð‹‘SøÅÈ©ŠT}(•½<øÞ§ó?7mÁ®ÚµÚO´a‡ÂsAb­»uŒK[yô–Pé;CƒçÞßC3ÈId-/z 3Û˜bµ†ûšý…0(_ó¥£Ùg:Ô´¦U”rN` ˜¸ ü/2Ø!ÇKest~V‘f2Ç£–v€˜¬\jØa¢³ °‰.±@š¼QݰoËèlhÆî!!¿~u%•Àž÷ã¥;nGûfïÜþ*ì0ﱋvÐsŒÚüª0./áB}¡Zê=Öº@Sd çйÐÉ^Ï”º<ˉU¨·Ê²Ž—àvNÏsÇšZí3!1Èùœa #A8 ¼êŸr/M³!Û϶•ºÎ×2ÞÕÓW4×ÝÝÇ£³³³â¼ÙÙÙi«<Ø6Ç$'™ ²hj]–8 …Ó·»¶ÃãÆc/þ£Ýîl³F÷—$Œ÷"Ÿ)O€èɃo ÿÓÓºÎeœlúâc ;¸~T7ÔÛÐ6U&òÆû p¥Ý”søÜEœ<øΘ ~§Ï‡MÏ>ƒªFuï”}MME]Od£Qpñdb$Íà¶§wbíöÇÁºÝÆÏ$ Ùh ÙhÔ_hõÛ©ßü©Ñâãl’†£aͬ°²`Ù~ …ú¬<ÎØÀƒ9Ö‹3o4<ú>îBûƒwY&ŸÁuíèû¸{Á ïÖ»:,Ý‚ëÚuKAåa(ËåmlÐuÞÐé ®kW¥x²àdLð ™â³>õ)Ó»hè…œæs<~$aq\*=gÙD^Ý$ÑßT^u¦¡su>|H÷…b±Å]OM«¶nÁª»·ÀS»tû/AQðÔPÕPgˆ¢ƒ›ªú0ð·Ó8ûö!œýÓ!KøC‘EH¹ø¬péð‚ éë~£A:nÈ=Êù©ÐÆ‘+›jÉ–/?‰‡¿ÿݲû1ŸÎhR¯)¥ýíà[–‚n¶JV}0ËÒ=Wá 3fü§ülÅ6}P‰ ;(ÀÞ†yÚæâ†&¬Šq Q¦ñž˜VŽ †ŒRcP•Ò" ½Í—vÐÚŽË;Þt\“PŒKÛ°ð¢´À9:?«LS–,/Á3¬\.Øa걟ã0 L¥°syºjžÃÎ#ï ;V<€%•ÀžcDZO¹í[¼³äoÀ¨ ØA«ÚaθÅå%ô &Ñ7’FKƒ- îÙÁSæ›&C«YåAÕ³£px Ly?¼­#ˆ#]ÚÖ•|„«Ùk-Ž€ a«8”l5Ç.¶ígÛ,PoTݚؿE€­ò`Û,=NHXRyÊÒ㽜‡ûD÷é[¿úŒä\!6OBRX·kÑ•O–$Œõö—<€zŠ…¶½ð÷ð7UæF\î€5-ËìNV¤ñÙB|ÏbìŸÓÖ!2YŒ÷@–¤’^·Xõ•›-°b6=û h§SÕñ.ŸOõ±Óf’.F6…h¨‘K$0pâ$†»O›:´lÙŒ;¾ñuÃÔ,fËf,I(= `4ìà^¾¤cîXMRæ¬Ú•X}¬±s¬ À“VËEQ³~?Ø}ÁuíðÖ×X£Q:Xt<ý8ºþû»sÀ¯}ä>MÜå0gµÁu«:¯}j)©<5)ÌdÀOÙÕýæ›N.ƒ4K°s:lΎ׬Ç3+¨âmlítBÔø6tæZ:n5$oZ`ƒôXÌ´:«k[ŽHIE.n7™¤Cᡪ¡nÑú#—LÍú}|dTÃú¯¬e8w訮óä|ÊRu±êî-hÞTPtXÊF±,|ÁF¸üÕ ç˜•–߾Ëo߈‡÷|gß~Ÿüþõ²«>ÌÝ–o×,¤TâðzðÔË?ÁòÛ7Z"?ãý–¬Ã³ïÂÕOUDªúp_u ¶ûÛpõò%7Võp1ì¹ú>2’1‹F«¼ÔrÚVš·UVЈ—b±ÁS'Ië/Ï"€ÀARðÐ,2"o\™Uw#xR)Ýuu©'Ì“G+ÂV0tåM).Ø^M]™¾Ú¡R7À¡×î-;@~*ðPbØaâo6/€+ÀE FβÓ¨[=Õ8üèSØóÉ1è½Pt³Ìˆö|p/¥6ãþûëVE{¨$ØÁ`ªaÆ,Q’Ñ7’Â`8ƒÖ&/Z<æÝko,Í‚H§)ÀÃä&x¦õ׎5µš9;ÊÀA9A0>ãæùvŒ¹í˜²Ûn€vCÑp«ðmÁ ðÚÞß8pûöíƒßï¯8OwvvâùçŸÇ4Ÿk«<,¶n'Cɇ¡ˆ{ÜÐÚÆþÈú6œlÙ°·?ù„Ýþ,l©È8²±82±Ä´ïY· Á5m ŽEQNãé„T¦ÍS—2ìàðxlØ¡˜grIBjl‰Px2öÑUåEÓº[ey“á1$GÇ*ªÎf+<úˆ¶¾R¥-~5ŸJ!ŸJ#—H–ï\"Þ£Ç0rúŒ)~¬X-ßü:×­5%}>›Ef,>›-Ï=utçÞ|«d°’e‡.«76ð`Žð¿[-SE (Ê̇ªžc'ÐñôvËäÕ[_ƒ;¾º}'º¹2‘çá­«³ÚƒÖ»:,g,d­wuè–²ÊÃTHaâF2là3ðk.hÌ•7½€Åð™‹ëéC:…·®´£¸ˆ½u5HG¢ 'ò<¸dÚ¨ÈßÔ ¿€_Œ¨lò4àg+¶éó]9”,¨îà§Øà©M̵Sñ"4äÏC3œ$_fÍÇ™;(¶Õ2ÂF€ –]1®o[ v˜5ø·|°Ãùìx‘ùWŒËk©a‡y~§Je— vE)€†26/ ÁXàXV6çs,½‘°Ÿu`ÿ½ £¦/~r¬èö™üäÔÇø¡¸ÛZV¸î\¾,ì å;-°ƒV8a ÁSM”dô &18–AkS‚µ®Ìý€L@ä ŸÁð<îoÃö@:<‹®m§%/}„ãÉAÃÒ|Ü߆—Zî±`iuì _& ²¬u×j/‹Þr•vÐ,YÍ8@“$2¢0}óÃÊ;ó8† MOw›,Pà)@$™h`äÂ_=i©Î—ÇÏ—']ùZä°ƒSr…*^í÷­‹Ù(Òy^Ýù7ð»’Oäç›@žMõ¢Ä°ÃÄç¼px0C塘qްži`dúK½=k7¡#P‡‡ßFB(Ö}¥ûSÀö–neAŸÍëo#aBãqÅ„Áy,¶¿k9ž0¯s¼„ ýq †3h_^ ¿—5ÿÚB  x€öwŠU¥ò xož;]ÿoÇí›I)B©FgÞ¿ G @ÒF%gÛRvŒ 9ØV¡uFù6@þ# 10fß¾} <´¶¶Ú*K¹'Š)(ù Èö¸¡Õä<äñuŸþØž„Ãã¶¡ÅŒK¥¾Ò7'è0Õr©tÅ—7‹#12Z‘°ƒ¯©/|³¢6™œöÜét¢¾m%HAܶMz% ±¡$xï^®6m†•KEà8þÕ«HŒóÞÜéóaӳϠªQß{aeŽ:U$ \:l4 ‘3VÙsìâ%\;q±k×ÌÜ.l|ú)¬Ýþ¸93'IB6C:)k6vp/ß BÅÚ! ËŽ¶ÂÃR´P(Ô |–«pšžx€ž£'Q×¶¢è]äm›n-·ê*QåaB‰!{lÈMý+vc0À&€…øÐ̉[ucœ¾*xkp×ø®«¢Ü[ðÖÕP§8 ¡®m…áù÷Ö×€fYUâÓ&ýù´*r)˜"‹º‚@]¾ê9•*×ò¼*ZpÜeRxˆ‡FºØS1>oXÓ†UwÔ–º1.ªêëàQ©žb5ó55â¾o÷}ûëøÛiœ}ûÎþél¶|ùIÜ÷íoXN©#>8<ç‚I9íƒ_¾ !§N¸YÖ‘©nSÝTbº ¿bíÝx/Þ÷¢‘ñ`wÃFÜ_ݲ(T>Hàå¡1PEã‡Í÷`{ M¦¼Vì°Ö]‹ ëÑ^½åRn4ìPD€­›bà¢hä$œ$NßyÝ(ß\?Ž$b>à¡`‡$[¼>~ ]™sâo#v6­¼‚º´ŠÍ—`Âä´Ê­ì@Ýè~Zßnã]ñ0¶Ê>Ð$YúüÏû‰êóÀ†††ãg9O”L œ™KÝAµÄõ¿ËEÀ#½Ì´&ÑÙØŒÃ=ÝBw¬øW¯tŠžDß{|àSôÁÅÎQ Lù=AÎlOžX8_®YêÕ«ÌÄÍO)ü¾6bÃÓ,Ðuiu~'Ú[ªád)sç~R%€ñ„¯ÙHÇ4àAä9 ¢ @ºi² dÈ`$HjîBæyiº¿&í JnÈäYñücA¹@0~£’[Âf¶¯m«è:#] ª7@Š}¢é´þþ~>|Y3¶ÊÃRìŽ2>RàrñŒRì(¢>ÿ­¾{ Vo½Ãn‡2µAÔ‹É2±8bƒÃe»þR†HŠBÍòeóÂb>|6 èéÏÃΪ¥ÓÃgs÷ö¿î—ùŒu»M?-”Å‘_ý‚AAUc¶|ã렋賩Ñ0ŠëvC‘$ðÙ,ò©´)ªçO£÷èàLRD`Ü.¬Ýþ8Ön¬I›êf£QdÆ"åòB­Ã§OãÜ›o3vj€€”-»gKغl³\…ÓsW¹Èóèû¸ íÞeמæoÂßÜ8k€úBfE•‡©PŸÉ ”bö.üå¶ähÉѸ:£ zë êƒô´WÕ}¢¥Q3$fc`mà¡°¨Ái_ ª_µrÑÁM$çýKY_ááÿ³¼Ÿ=5ÜòÐ}hÙ¸¾bƒûö‡§¶ÆrÁðÅØ„êÃÃ{¾‹³o¿³ïB¸§wÉÕíòÛoÃ}ßþ:–ß¾Ñry˧3ÈDc–Ë×Å¿|€ðeýmåfØaªM€R6†üøÕ²(> ¼2ô~b°Ý¿»j×.˜nM3CÕÁC1x©ùÜ_½\"Kv  ¦x€ýû÷W,ð`«<,µn)BáF H¼=nèÍanr⌮s7{Ñî3V2-ªSÍUÁAçÑÁadË{´Ôa‡ú¶•óæ? cüÚüïš<¼uµð”iÃÉRŸÍaøü%Uÿ$EU¼_dIB|d´,ýÔhØ¡iãmX¿ã‹E§# bý×L+·Èq¸vâ$®8iˆÁlV Ð!—H 3±Ä†ÙFÂLuœÁuÚÆ[1cÕ.Þ …,læ™%jÉ©Áîó¨[½þæ ]ƒZëÝèúïïjŸÌ—Qå!=ŸÉ ŸÎ 77ØPCEZ|(„øPH[ýG¢ó¼)Š/þæ fàAÊÅ€Àr»2ˆ:>«j¥/òéù'\ZÝ$ÑW&Ø!]Ú(—1.'Z6®Ç-݇@˲%ßTs ÙÅ«„åðz°å+;±å+;‘ũ߿ŽËG?Br‘ïSlÀ}ßþ:6|þQKæO–$Œ÷X._™ñξ£_d>ØaÚó‹;·;YàÀ_…)}Y%¿€?Œ_ÀjgÛmØîo«Õ‡?ÇzñóÐ'†ª:42ülå6´;‹€àL ~«U/Åbƒ§Îywú(%ì hOÓHØASÛQ@“äÂ*:``çZ3±:ì eTžºð‡9“éÏ'ðÐéÿ†ÿOx;ÛW¤¬òú:òeD:ºü¥èó±–´Œ æ/&­* ˆÜÈj‡·GâÚæ i‘©=ÔÕ&ÈüeæÀ¥úlìpów›yà< $oègx½ó Øýá!¸r¡èÛÀ»ƒ×€·—¾8zPSï¢1dˆ”Á Eï/t¢®ëê]ãX¡'‘@ÆÄpÝc㪾›j›®¯ït4Ö€¯íuÕðVÓÂ'j§¨Å „&L”dô¤‰sXÛêƒ×Ř;¹³€˜+¨=úÔvYA<ª@àns.Ž —¤"h†€§šž¡øÀñÜNºËL¹å·6Ui ¦€¢íA‚„³ H£’["fC¶Ÿm«Ø:[ ‹[Ò³r抦d8€}ûöÁï¯Ì;[åa‰ôP1%Ù7Š0iü¸îs·~õ8cÚ5J:ðÙ,’Ã#–€òÃ@ÊœU›]_%ŒE6ð`žYRÞƒU¤×sô$îøê» ´bT?ùmÛ0-oSÁ†‰Ï|&cWšmˆ…P×¶Âðt½õ5Úêói»B&}¡]áÁé]|êùL’ ÎÝ~GÔ·ž€¯,e8óçÿa)ŸN@Í›nEËÆõvgàðz¯+:,=e _S#þþwñð÷¿‹ðå^œúÝë¸|ì£A£J2«ƒr°S IDAT–…Ä[oG«ó„œþ‡q5°Ã´Æ gp kÀÇ &G ¥_ ¸ÂÅð‹‘SøÅÈ)ÜWÝ‚û«–£ÃÓh9凮Ì(~>r W8c•AV;Ø·êý°ÇWu€ ëA»+0= XmyôšÕaÕÇòJDCқ¤ lG%‚ Q®Œ‹v_~KUîv_z }5ÿšëëÌ—ê2Zv ŠlÇjÓ4z\¢‹éÉÅÑ!ù&\ˆÇ±¡¦fžü+Æ•»œ°ƒÕŒxQB‚ç‘$ÈŠ†$àfø]ìeÑñÙ,Øaêß[y —ÂdÁÝ×Ûï#è 6ã[Çÿ³hw½;x x xiÇFÀwSN€ )ˆ$ '•@(›EO"P.‹P6‡ž¤¹@ƒÖŸöwÂ=.´|hT££©íõÕðh €™J³õ—E;L=.ðÉùZ›ªÌW{€)i€v¤sî ý›LdÄÆ8("¥®xäli(HFTPô G‰Òìã^ÇšüñX¿¶æ# \ÍÆÌ‡ÔV%ãA{L*/³Û϶Ut½iÌâÿÏÞ»·qøžÿ~Ýx€_¢DY’e‰¶%ÙNdYgçáD7Vv3“IFÙ;™º“Úu¶vküa¦îLÕ|Ùý¤[uï—ɽ»vµ€©KWÐ3¼›Ô¢‰2êòºƒüì¼±Ú&>b¡€ÒBÅt†86Uw¡ÿÝË""ƒ}¦:=qQ%š*ƒ¢ÉeËüáéh½ÅÚb±´éïùùêw© v5ÞÕHÌZcª°eïîÐÁér¨Òg¼Ñ)%E‡ñ…¿þßð7Nÿ“^¶µóCï£`Ï?kyШ¸(ä“)Ë¥kêòǘ»1j8<ߥ vX-ŠfÁ-=@óIˆéI(¥æÌiÏ-NáÜbe7›m|‡ü=öÄ0ì‰5­nÎ.Nâµùk)˜ß??ÄK=O‘A±–{B>€Þ$ÐÒ˜ ^/pµ:äãnœNšAy£‡êap±,(Š2ß}Ç6vؕӘ¾€¬R¥qV)ãåéðb`/àPõ×e"hdšCƒÂÖ[po²;‘@Ÿn‡‡„XJ!À§"%”p-“ÆÎP¨º:§L,¯Fœ,Ý8w È‹"’ŵ}WÒ4dË"r¢„ß ŽeŒ¥¥‘°Ã²¶I@€n¬ÝUÿø¶]:9?û²Rm@ï/§&€7€—ž|té!H”Џ¹˜]ù—(–pk1ÛòsŒÙB ³…ÎM%ðÊﯨ@Ãñ†ãa ÷„ïv1µâƱYÿiØaµÆfrÈäÊØ³-–¡ë\ZÅñE€b†h¶òy½£U­;èØÉØÉ®FhK¯…$yýxâ>ý9«rŽaÖ“¢9PβƼásy’mRΤ¡X=‰Lèäé׋dNœ8a[à .­Û]UhBšR"ãF­9sPÒ …ålh„†?…¡ÃO­8?Ü8ý;Ü<ý;Ìݵtº9¯CO?…ÇÿÇ£ˆ Ú¢¬ÅR é;Ó–Lׇ¯½i8¼3¼µ¦knf½`½P%RfRvš*7¥\n iÜÒx•%û<Ñøa;2Ù‡Be.µ —p³”Æ¿eÇ1+ÕÇ…å/»DZðÎÚ"±‰»ƒ¬©P4NšeR¢YŠÆNw‡Ëx~L/KÍXœT•ñR&¶‰{Aó ƒ¼\YÔW[´5_y‡ñrnìå»ó–S‹úvü9¹p/–œ¢Î4èwzã‚Éq™åaô»z´¨•/Œ@U7Ki@º»£U¢T(`»?–¦7.ÛšÀ*ê®I°Ãòg–¡!+jÝaQVîƒV«j¦sEl ùì;,îT¸Ä«Ö[íÄ©Ï G~ùϦ@þ<‰'£1Œçó™·(ÜLÍJøå­IüòV‚Úòc8Ás;z°½ßDUÀ«³ÛvXV&/âÝ’ÞѯËÑ X2 ¯z>E±À=pe&U†&Ê€àa3fã^è¡,)kËgi84äxQNŸ¿ê Š‹ Ig®{Y“l“r& ÅŽIdB ¤Îè 322‚K—.axxØ–5L\ZPjªšô<·ÕÆ{uþ »Çzò^€?ÚIÚ¤TÊéßøfâQ[î¤/ ’£ãM]ßî°ƒ;D°+F:Ÿ ´%n»¼Ê¢„ùñɦIµôÏ{å‹EñøŸ|,Ï[«Œç/`âüÈårÝÎÓÐA‘$’)”²ÖÛ|ÆJ°#¬ÜíÇì06ࡾ°Ïj‰bªXÄ'‹"®½u{¾ôR‹&ʨËC~nÙÉ)z{î¿é¹nÈÏÍ‘‚&2]ùÔBÅýeä*X§‘m½n‰vðF:tr)ÓöÀƒRÔoçˆG[ vð@بìŠ^ý| áy¸qžÏÓZzö>LµUâ¼^x:Bð„C¤0t*:4ˆèÐ >ý￉r¾€‰.ãæéßaâÃË–pX†¶/Av’ª(˜»1 Í‚»Ÿ\ÿí9ŒÙ‹³ÞˆakÇÍD;xpCà:‡ -Î@ÊÎ4ÍõaåF°0‡‘ÂÜ sx°ÝÂv>´@u‚ø‡ÙñÓùëð2ÐÃ8ð÷}ÏÔîZaØ¡¨HÈ*e(ÚZ'ËÃA_håeœØéî¨z1v ª<¥a‡õ¾¢àf(ÈR ñ­-¿ƒ[ëî`Ø”»_\Êë»Çz;;HŒÎ47v jl×µÆeج,(7A1SÿÉ~th[öîFth!+ÚAÃÀðÇ “Äy=+îVˆ¹£˜ü°ò·œ/Ô= Ñ¡Aô>ºC‡Ÿ²“ýj6ìžš†Tª\sƒ=]kúˆX*á“ßž5/íàÁÇw×ÿ^Ç߇¿Ë®«5+0+pnqê¾ßöy¢+Ÿ·óðÞ³Ë|B, !å¡h®SPôrnÂß÷=ƒ¸ÓSã [¯š;dä2òÊý‹ÍDUÁ¬X@Øá‚‹Öÿø$âpa§; ¶*`¢Q°ƒŽÓ×úåâfY”U¥²¹Æ´¹YœQw§ºÁÆÊn€`¤ ï9Á©ÌŽD:«<¿…aªÎqYvV]W†½úµ›¥4 S÷Å—$\ZHa8¾ëôPkZÑÀò©vÇ¢(ÊÕ‡…ŽøW}W¼w÷õ{çPt¥nŠ’\Ì‚Œ89<è·õŽá<&8ò݃¼~œúü¿Ã‘_þ´fèÈÀÜ4_Â/oNá—7§;㡾8ŽíÝŠø€èR«Ûqónþ|SVT\¯ÀÑMV©T0~Ÿâ`hHÊÆ®²¬¡\¼çž’°ôUЫ­œ„© 7šV8‚ XO÷P8r&²u½5(‰”³ß MÐçkwà¸<´HO.ÏA“rdÜ0QJò·†Ã~îÅ¿ ÒBò„‚˜g¦6]`ìt»ˆGá mëX_Hgžj®Ã9xtöë ãë #—š‡X,=ðX—ϋ؎Ö Â.Ÿ×v,‹sI,Î&›rnIpéÍ_™; >}ƒ‡Ÿ¶LÙ&?¹Ž‰óž˜¨ëyÜáì{ákuÍ»ÕAÀz°Ъe×>¾m—1ŠõÕ%_µÜÄÅQ½eóµ_ŸÃ“Ç_ 5i¢Œº<ˆ…>>ù)@"Ë)5:‰Ôè$ôÀÞˆ~àA•Jm_ÖF¼­µk½,Št¬ÈÏ/T_°«ñPß•·N×%Þà–.D‡ѳïaÛ.ô®·§¾Î<ámÚEkˆo¨@s7naîú(„|“VTè…!üñ(]±%Àa¢CƒtÅZ¢Ý‹¥R£ãPÄÆ;óÌÝÅ{¯þø>÷†­O<ŽG}N— ×{n†Ð+>¶ ݸÛO+º>l¤Õ‹“õ.T®—>Ä÷º¯Ò•`Yv(/0Ц­ ;¬Ö‚\B·Ó JÇ ø€ŽÖ ;hú㤪ÌCƒ`‡ÊÏ bÉÀ«ë»!Ü, /ë¬!:êÐŒ6þ vDzã8Rê\XtIiæí;¬W—õp3ðÊ@©2· ²úy?ƅŪ«© H¸YLc;øûâÎKÞÅp$¯Ãa?d½ßªüÌPì= ŠM†@ª¦UŸ¤¨Ö†ôÛã"pÓÌÝ…g‚N§>ÇÏþ¯OÞ&7QMÒl¾„Ÿ\¹Ÿ\¹m~{x+žÛß t«€æ^ßë14xn+@bY5þ|ãÀ” œ.fýK’ìÛÞ‘›Õ?CƒjÂ}ê¦ãÅEÚ°'È”3‘­ë­IId:Ÿ†<ù#]a²Ù,Nž<‰£GÚ¶E—;wgšpš"’qÃD©ÙËÐdcÉ£_yN÷‚g¢úŠf„û{¿ï{O(ˆÐ–¸-ÝV‹ÀVig½ºß•Ó ƒî];MÌAX¼Üáý¾¥¶°};½o¬U̎醜n—­ÀUQ0?>…r¡Ð”óK‚€S?xÙ™YSâÛýü—нwoÓËUÌ]¿ŽÑÓg!Ôõõááç¿„þ§ž¬Û9ì:æÁ4ç…+¾ÛØhë:<ØÂÝ ÀC½u À´Z¢(ŠÃ0Pª¸ ¹<ÆÞ»„'†Imš¤à–8â»¶-íŽOÔ x‚ovÇÂl>A–Úºl—áÖy‘mwá‡{eÄáÁª‹)#eà Zª „*lóóÕƒ!~g–»ï÷Á #ÔÓogxÅÁÁÛ†;ÑZq^/|Ñ\?)Œ¦Öƒ½îEï£Ë¾¹îqË ÄjÝ Óº*ç HŽŽ5ÅÙáö»ïã½üñú¿½÷>ÒSÓxúþ¶awg¨Œ»yžU]¬(ãÀ÷âûñ\ȀȢ°Ã¢RFN¡éx÷©i@I‘áf¼yKÑØî épÆh#ØÁŒz^%†¢á\ÈHeHªZe>´•ÿúÜýÎr¼Øta¶ñ6~$Ð×nè*éS‹@ùÓwJGš å±É°ƒÞv\Ów € üڹð7¦ x€›¥ ¶·îûç!²¦áR*…íâ·yé¯ç"”õ÷øìs9°/o~øÉæ.b™;3øàµ7 ¹;P4 gx«%ÊÙN®ÍÐ6>„¿ï{FÇBýÍ*¾^©¬íæ‚, ¨”M›¿ài{<Îm;T}œVu|(„œÛ×C +Ï“N¦gþ (È¥æu÷a;Á‹sI,Î&›v~3a>À¾¯¿ÐTØ¡”ÍbôôSÜo&–ãÐùÐ ~®@N·».°ƒ@€À5ˆ8<‰D"dz,·Õ²Ãá@Y=ví­³þÚs¤RkP>¹€ÌDåßÔ,){ı,âK°Â2İ`ˆyý+ƒå'¼š†¬ +p0 "‚œ †Á.‚ãQÃFU¬÷*©²aØäì@çeœöFÁV½H‹Àë§ŠÏI3pÒ ü@T•»Ž«`MÃI3&—µ`F]†=1YEßnA§28ÒÙiN^ì°vq¹ž6VKü´¨•ƒûðwãçtµKù9@d˜Ö©|™²ˆ¡ ¼ÕìgcØaå~Œg!HJ80v*PuÃ8© ÝCë;—Ù°ª<†ªâoDvIèaUØ—= €@VÓ/oLá—7¦ðéþ8Ž oÅðS¡ øÐ°Ã²>McxG¼.GÃÊU–j[¤Ç9«{Þ!ËÚ†pÅðP^?3nN†j\sHó]ú#l!9r&²u½Ù ‰t耡ÝÝOž<‰ãÇÛ¶õ—»tsš˜‚&åÈx_âSPsŸ Û³g¶=¹Ÿ"QC¤* ’£ã„æ.ò$°ìŠÃ ’F©S­ìì * æÇ§P.š–3a_,ŠÇÿä›`›õ¤Ç'0zú Òu= {ï#è;x`M^Ý!s犂âBÅ…¨6Y£dìÀz#àã»M_ã`eà!‘Hœ²Ë¸L€‡úë€g,Wñ:ŽgîÌ"5:È`©Ñ*u/à ‹b[—Dz+Ã2Ìà]r|å/ïjÙ|KŠ‚T¡€T¡Ç!À¹àã6y³«3¶æï½ºšœEa €H Hó+„Õ•Djt¼Ï Þ¯÷`UЮ˗•’~à!ØbV‚ÕÞLæª++Ï5ì¦^\ùÍÛ†Âîùâg!,檢êiº}ø»ø¢p-æjBÔš’E‹3³(,¤›–=°hšþJ4ç…ÃoíkѲëƒ3¼r> qþ6TIh›¶¸ÏÅ÷ºöc;oâC°F»;Ty¾Z`à7¹ÆÆìt‡kÏ‹ÑrlsØá^-ëa‡ú”µ`pÞ€ŠËÃë úv÷?•Ç‘bà–kK—Ù€‚™qY v¨ €X'~NJ•qjØ«êž H” ˆWmå% “I ø}èñxîº=Pf—£Îïê;,;É=N¤ "4hU¤ážÏh—‹¼(o§ÃQù‹eš ;˜á ±úo§ 8Eà#°ê}ÙˇžÅ‘ø|çìoÈMƒÅtnF˜;tí}?ÿ冦_Ì]¿ŽÑÓg!d³u=WçŽ!ô<ˆPÿÆ›”›z(’„B2…RóSÙv,íð0f«q™\šê®KVLÃ0 (J×N­B.©KW0ðÄ0©UrY\‚HN¶ àЭ@ â“L° IDAT1¯<â?HH`]·%ׇ…b>ŽC§Û ‡ “ÞN·nï}ÎW“³Ï.àJrÖÒD5ÒÚxPËyhªþEz­dI(äòЪ ‡33‰êûÌ`ÃnÖnœ{ÏPØmŸ:€R¶ºí;0N'ñ\A¿-•óæš:ƒhƲe}1îw\tr> );¥”i‰¶èa8Þ‰cáð2&_7, ;Ô*'ÍÀqÏ®ç,Ec§;Œˆ£»ÑRu8ÖÌãLrbX{œV§ÄÝî Ò©Õ¯,ªCjç`p;±X’ªžZ%óº1“+BTÔ5Ǹy M¡7胡7ާ°ªMÖ‡–?»(¸5Õh èv‚â* (.ÐÇVÚÝs7뫾7eÖSºSù«”¡•“k¿[úkýòúÎŽ%pì‘A{z+¼Ûk¸7o2ì°¬Ä| ‘H‡ëà19W€ª®?&Ñ4…XIZÿ÷.oSÓO±~P»>»$€)g"[×[‹5-&´_7ðT\Nœ8aë¼— v/) ­œ"ã}¥ÎswðG#xô«_ HTwYvû`7Þ9o(ìЧ¶ ìШ5­¬pfoŒnø»'@çà€å×-È¢„ùñɦ»®Øvç/`âüÈårÝÎÃr:ÚÁÃOÃÜ«]1ìêŒá¹í• sQq%9‹«É®$g1‘M̤ۧR[¶¹¨¿Ž"ý½Æ&Xud‹ÕÕ½ž›¢Î­¹¹¿ô毌¹;ð¢CÕ?pg9®åû‚+€/çõ€ˆÈò×,EA)³ˆ\2©dëW£`p†·Ú¾W»>¨’Pg –íùÀçóÁAîÕéFPmaÕ+ÕæÁ<Å¢dÀ墀ví¢7–¢1ì€F4sË’ªò\­;˜Ù~j*³aÍœ¾Æ¨€\Y =ì‰!ÀpÈ*úæŸ'ç¯ãxì!€QôŸßì »Ì º;艫šï¨*ë²Ññ³êZð%؇ÿt碮R9—½ƒ|^·SÚEÁµt|ˆ»Ýˆ{Üà—Ÿo4v¨ƒ»ÃêÏœ“Aˆ¡)ˆPµœW‡û [üäEEI¢©ð¸X„½<.'èewP³`TO-¿mú»vÿw.ËÀù{ ‡¡(*þòw§ë7ß[(×P WV Ÿ.¹CPà]ljK-C+§ •“ФŕÏPŦ¤· Êxåýëø×O&ñ½§Æ¡C1 `Òµ­aó͵º6žÅ“>'X†¶üüž¦)ôF=˜Ï–‘/Ik~óº8Ð4uµÃƒ¦ojZ·¶ÎúAñQ›ÝQÈ”3‘­ë­…›åìÅwëÞíýäÉ“¶ˆËƒ•ú˜ MLA“r¤SÖYjö²a§¸g¾ûmpÕ84ÕrŸ˜Î ;3k Øáâko ÛÿØ^ìûòçl_4ࣷ›lh‚<¡ :û1?>u_ÛÄ£÷õX>¥ÅÒSÓMï›fÂ}`Ç~¶!énèÀèÞûúÐåÚ eˆÅ"œnwÕùY†I²mß´ì XzƒçSvªw<ÔY‰DâR<·dÚœN§n஽uÃ_{®-êO.‹KpC©[“´¨²£–â^bb^?x– V#À‡e¹NìïîÅþîÊ‚x;í"¥¤¿N‚[º ‹±`ÿ×TåB±ªcóó Õ—Qw¬îi/¤3†m2·?ý”.×§‹oÉöO1 <!ø¢°N'ˆ,¯Rv…ù´åì§.Œ÷^ýqC`‡eP •D;x8C½p†z¡©2ä|J19Ÿ4äÂÔ(ÕÕÑa¥Âë60õ|.šEIg]QuxÀ®rwð2N {£k¾«)?FóÖ Ø¡./£ëPTo#`‡Æk"<Á)+ÀPÙÝÿõ…º’t*;ã…Ý€_1!MšÉŽ '¬; Æø P®?Þ…˜Ç‡¸×‡þ@ˆ¸6ØPËàCÄãA‡Ë†jÜÛ=·Ã‰ÃýÛp¸€Oa<³€‹Ó“¸8=Ùt÷Uj?àA-ç ÓÞŽPË”X¬nâ—ÑqsˆÖCrtÉÛã†ÂnÕ¹Ë Ã²`­1Î;\®ЈȲc³¢ ”YD9Ÿ·,ä°¬÷^ý1n¿÷¾±›Ežƒ,è·ål5w‡> à¼pr^ Ô»ríVʹÊ_!¥Ô¸gÍà•íÏ#æô´fa×8%î`yРßdÇ9†¢àað1NP«Nwz°Ó6pV;¬œÝ`ÍúížÖµrð‘@¿þ{P¥ŒK‹sŽøª;w£3ã2Fhì@éŒ?(ów¯ ôéÎeï ŸWàíÐÑþª5ò¢„¼$a,—Ï2ˆð<â7¼N‡ñòYï·v²4:|ð¡T–Q–Õ‡[þŠ\<Ç‚¡)}ç¯ì`Q ì°úïS"ð»%èaé»ÿzè°3Âÿqþ\ “xòÔ?ñõ8@s-=?¥þŠƒ…˜Øg¡æG¡F?Dÿåqv,—¾´ñ}¼‰×ãÆéÚxûwEÀ2tK´ I¼ßáaxHÿÜVSËÆÛ¥ea9r&²u½‘¦µô0ÃÚÿ0ÔÅu;yòdKdŸ¸<4©ûIÙuœÅH§¬[yË9¨Ýv?{ƒý¤‰ê¦…©iK쟙™Å…×~f(l+ÁÞ%'¢ö–U ¤e]xíg†×ά¹¦ÕvhèÀrº÷íEïÁp¤±êÔôå˸òÆÏkާ‘°0ŠeA”1»µ²º1²¤í¥ÉàÂ’7u‡ryŒ½w O Û²B䲈ÔèÄè0 YmÛ¸ÜÐÚJ X(ó6Ï‘£?Øþ`^ؽÉbïOOâí±[Mƒ„ÄUpÑ!Pt{´s)ŸÔ&Ü×Ó2ù×Tµj‡=4xç¶ú?Ø3ú@ň»çµÿÂVÎëE +Öy!jMÉ¢ˆRf¥lt°ºÄR gÿáUÌÝ5ÞáâñÐgžÆG?ÿµ®p´ƒÍyÛº­Ðœ÷¾2P%šT‚¼?(ÅÊ<Ê(ظ‘Žøû;4ÚÝÁ¤óYÖ QU hdÜ]œê¢k–Õ²°CuÌo­;4ÂÝAO\¬ ˆ•¸öy¢)Ìéªî“ó×1ÜýÀËÖ€¨:Çe'Ø¡šü:Ô5àËÑȾë7º»ýÙ…;x.Ö ðj]ÆbAV0•/`*_¨À.=^/x–ÑÑGµûÓÓ”ÏÚ øàt8¡iDY…¤¨”J%¥RŽ–^9–e(pNÆøùŒsÕü¦rX}ŒY°ÃòßO‰ÀY' Þ=øß3 –¢ðý÷Îo€KÐÛóµ–‡ÖÌO½ƒ€wð.ü°xja´®ç™žÇŸ½r/=;ŒCŸ‰®Z¯Ç• *˜š+b «½ïmL›RZv )g"ÛÖiVŠé8 xÈf³xùå—qüøq[ç¸<4ºªÐÄ4)G:eevàe·v@VE7F—¬š0Þ†¸˜€ZÖ¿hkêÒUô ïËÙÃÚYXÌß…îÌÚ¶1õBèv æõ!æiÞ"øš'Yš±ÆÝiŠ‚“aZ~Q5 3¹E,”Šèöš ´tº½xnû.<·}WÓàiqr> gx+œK;)·²–DêQ°;Þ2ù¯v¨<ȨžPô×·í\ùÍi3YCa‡žù”î0—}oˆè@dÙ믢 œ/ ”ÉBÈ ØMOMãì?¼ŠÂ‚±ë³ÃÅã3ÿëŸãö»ú!Xo'i<ëÍ[<ààÁ¸—ÜkÂ[ï;FSe¨B .L@.Îë>ϱÈÎÆdȦ°ÃÝè(pU³;ÝaÄ A$vwvÐê³ ‘Àõ‹‹SV€ âò xX¸Ž¿-|pÉúË’Àÿn½c< «Œo|û¼QŒäõµƒ¿çû^4˜.­ê< Ê]ø!È9÷¸÷¸7kAØaõwU80›´“®Á÷º;ÔêäPË1›‚ÚƒÛÏF÷‹À»V±‰xñá}¸´Â+7®Ÿm”SPoéþR{ÎK—áµ %} ÚâUhr®.ç*ˆ2þæ_.â…;[qü¹!xûX}í¼É›É!vwšÿüYÕ¶isÖ€à@Ê™ÈÖõFšVu×xÿ€á iNžüÉ›†Ã/ážn|xG<Œ‹Øä~ØA³`Ü!hªlvØÆ‡°oÀœÈæ°ƒØÁì¶C`SÚ÷Fqñ °j½êÑŽøOÓúÒæ0VÈa âЙ÷6Öku ž:ÄàŽÇÁ÷óú\FòsHdÄ£tíå£#lF‘EŒ-æ0à÷UÀÁ›~6ë¼Á¨âØÆSÅoë³iXmó°¼yðTøÝZèáå§Ÿ€š µ0 $Oƒé<ܾ“Oš~?uñ*ÔÅ«ÐJwêrªŸ\¾KÓóøû£û”oÒ\Ó˜nN.bÏ6sçØb¹ñ°ƒXVáñ5á§©°H9ÙºÞHÓ2$Æ¿ŠÎ]à_ýud2ƒö~¦G\Ð)•T!h*)ŽK™û­¡pþhOþñ ¤‰L—ª(HŽŽCš¿[µ$¸ðÚÏ ÁÝ»w´ ì@3 :ûA·è†±¹Ô<ÒS3WmNWHgQZÌ£{׎¶‡TEÁâ\ ùÔ¼¥Ò5öÁˆeaY0qþ&Î_€\®ÈêëÃàá§êï#ŽZÇ€ÙY¼ÿê?Õ\WÍ€€¶®»@€¢Mô6€g,7éQ0î(þ.H‹3ºÃ'®ÞÂÀÁað~ëX;ç“ KN“¶ƒbúƒKuvoäÊŽ²ª@V+&Vƒ ÷B ¶œÔi*Y½'ßëçËɰp2  žuX‚X(‘ËèöùávXÃa¥ÓíÅ·öÀ·öÀÅéIœ¿…÷§'ë_Ç’€âÔ‡pø»ÀE‡@Ñ­uY3âîàà9ø;#-SÕ:T€­O>®;ŒÝ€:YfžÐ"€Ã² ói¼÷?ÆÜQÃq¬†Š«U!ÄFJÊÎ w,Üw‡º,@³èØÁô6A`a ÆÅh€RùÏ‘@ ‡¬Î]”¼‚¿êzpªU¦·É°ƒÞ¶\Ów†–¿s+k~?Â÷oýFoááµ¹Oð½-{î‹oóúÐL)AQp-ÁXî^ðÁF°Ãfy®ìP×=¿­û»V{¼€O•w¸5MÌè!3ŠëíßÕÆ3ÏJ¡Òþ ý;¡•î@™?_ðáVjöÃÓøûù~&8ìQB©¬€LNDÐgÞs¤vqw œ œMiÓmÚ•I9Ù¿ÞHÓªYLä°nà^~ùe¼øâ‹¶Ï-.š¸Ð„ë–}:¥&e¡•S¤HšQ ¥ih´¡°Oþñ1R€D¦Ëj°Ã©¼Š¬ŽuË tÅì`Éå2’£ã(åò¶ÇlbƒýmÛ'%AÀÂä´%úäj}0‚‹¯½Qs<õ€&Î_Àèé3uºö>‚ÁÃOˆ〠àÊoÖ\_\çö¦À@+–]‡’M$cvkxhàX+K‘3¼r> M•uÇqí­³þÚsMÍG>¹€ÄÕ›HNBØ`¢cEõ–à¯ýð¬9]rfX^Ü¿ 2XÁuÁª¢r·ýÓ ·Ã—à ·Ãzoà$EÁx&m·‡ÕÚßÝ‹ýݽHó8=v §ÇoÕÝõAZœœO‚‹5m‚R—‰[^ÿÃÃŽÞ--“ÿjaºd»buKsrtÓW®»ñÙµÃÍ$ï÷Ù¢>§áþ^:5oLÉ KJ%ù”U»€Ø]S—?Æ{¯þRÉøÃ¬{aÑB;ø–ƒ›2ÏËè‡F=Œ‡ü=õKT¹:u†L+÷:ÁõhV‡jm—͆V‹S€âÝqøH ¯/ÜЕûÏ_Ã_Îrém€ƒ¡òªƒKI½Ç%3`‡eye _i|û¼QŒäçt%çó£ø^úQÀ£T_æFÒºIÙ ²‚k ða{Ј‹·.ì°Yž¬ ; Êc(aì°üÀ§— ‡ÕN‡ŸÅXno'¦ w=eö-P\×:N™µ\ë(×°=ÿ®nàCA”ñýŸþ™zǾØø­5ÜH7§±—y›‹È’Úüê¯sYS\ ”Ã×Ôöܦݘ”3‘½êŒ4+óÇ_W7(gš¨o3­Vjuy`{¿AÆû¾R¡•ç ÉÒÁš$#ôìÙ…ÝÏ&Hdª¬;À;¯þØ0ìpä»ß‚ƒç[¢^]±–ÉËšgé ’£ãP•Í׸ÕkѺ”O- 3“°\º’£ã–„’Ÿ\Ç'¿~ B6kzžYŽCçC;èP½ÿÿ„Üì\Mq8üq8CÍsÚ`”¢U‹÷’ÛYÒØò§VK¥J 4´ƒ‡#Ô qþ¶î82wf‘¹“@pK¼¡ióHN`êÒUÛ@Ë€Ãò_£ª€ *DE†ªi(ß8™p³¦©È‹eäÅ2XšÇ鄟ãASÖzófE·‡euº½xa÷>¼°{NWÀ‡«ÉÙºOSe‰«²3àã»A;ìS§”ô;<bÆmâ.k•™X¬~±­ž¹Ð–úA1^û™Á §!w–s‚圖nÇà ´¥ž0Ùõ¨ãG©©(Tþ–”óù–Ìga>~òî\¾RS<Á-]øÌ‹§ë®»XfJ¿ËÅò¤ñÕ:-ç¡Jú_òõÂË8Iš ã°CµÅŒãê±*D31}«³ì@UQ¶V†V‡q­ކwèÞÏ'p%“ÆîÛ¼z3³ ÔÆè|3a„ Ó­™—Ö°䨕ß^ܲßù亊ª¤Êøû›ïâ¯ýû¿¬/í5—ÅÚßYÆG©yÛC~xŽæÃ›åÃN°ƒÎÔ&ådì°üÿeèáÜZèáä~G~~# Æw›•§þlßrø[x†£oî°|Hž1}7ßÿræcÜLfñÒ7÷:ç MP¾$!1_BÿF]”ôE0±Ïµ¡ËÃ&R-C+'¡)eÒ±šU;5¹;¼@ ÈTÉ¢„ùñIËÀ^û’·Çu‡k5Ø!؇'l¹ö–KÍ#9Z]ý²×–ý1=5rÁz@bffïüã¯93a‡ôøFOŸAzbÂôü²‡¾ƒÐwðXž¼'7[¿ñfͰë‹ïnj>(E°jŸ²c» ÀCãdY"†V(¬\x+äÅC lÆÞ»Ô—‡eÈ!qõò©ËWzÌãCprÐçà jDEY5T¾“IojôdQUJÈ•ËðqœåÀ‡e·‡ˆÇƒN·×’ex¸÷oÃxfÿróΌߪ۹”RÅñó¶w{0ºà±ž‹ù­jòó Åê€/Þë7RŸÖW~sÅŒ1¼wøCN î µépog®XKZhYd¬TH%aŹA¥–…îÕ'¿=‹~ñVM®Ä¡?ÿÖØÒSú_¤0n6Õ*1=i(ÜñèÞú%ªÜjƒ4SËRÑT(Z%Nš¢ÀRôúç1  °ƒ¾ø;èC«­j凣;ðü\wkþ§¹ñwÝÀr¨¨*/&Çe&ŒÐØ28~Püß­¬ùíhd‡nà~“‡pQÆKCáíU›;¬Ÿ2å2.&’èñy0ð¥éæÁúÜ ØÕ¦ú`èH×Féä<,¿¿ëÄtr8õ¥£þé0žÏ¼©¡ÌülÏ׺•^’×¾¢”rmÛ÷ ¨‹W¡$Ϫy.y¿¼6…üÿ-ã¥öÂ;hý×T7§ r`ºæ¸šîðP·ûhW¼Nýˆ@-zB¢v©7Ò´*&´_7ð'NœÀ‰'lŸÿš\ÒÁÆ>GÆ š\€Vž4•tª&ªw‡žGv“$2M’ TµÓ~£tᵟaüƒËºÃ9x®¥`w(X·uÍ”ØB Þœ¹Ù*¤3ÈÎÌZ¦?Þ;V¼ýƒBjƒ%Í‚dAÀ'¿~ 3—oz^ èPž>Ssݱ¾\]75Ë›Ñ[TÄáhc%‰Sñ¸5/²´\€²´ã×9„Ò´þÁ¢ž.rY\qr°:äà]è„ðP$Z5à°lU’¢@T¨ä‚å¤j*²B QDØíѰ4B©BEQDO †²æ¶hýÁüÅþOáp »Ñ IDAT…Ý{ñ“+—ë>,»=Èù$øønP´ý.wrQ¿»ƒËïïó¶D“Z•[ÞetXUºês-.¤3¸ò›Ó†Âò>/z}Dÿʦ Açõ"ØÓußj"¢Z´ìÚ ‹"ÊùdQ„"ŠmWs7FñÁko sg¦æ¸¶>ñ8žøÖ×ׇ €­à®ÔLiª 9ŸÔn›+T?G;Ô–yS4%E† U`÷µÁ)p4 ë³ ?˜ (˜;4é8ÃíÛʰƒÎø*P®À¦A–ÇW;†t»<¼½8k©‡°§Ç¯/†ÊLÓ¦ÕaÊhZWßøH@Ö±Ô8üil^™ýHw79—½ƒo|ø&^Zx‡¶G¯¢¿¯Ö ;Ü£©|©’€í¡"îFÌ=Ú v  þ}ìPmš7 ]*@IÀåµÐÃÉÏ~G~ñSd Þhå”ä0±ÏÚ}FY—Xiÿ.ÐÞA(óç¡fFL‹÷Üí^|¹ˆÇŸ´<ô +*¦æŠèªý¹›Ö‚ )šåÚP´åÛs›vcRΤ‘$¶‘(g(¾[÷®ð'Ožl à¨Áå!uºE}RÓÐÄÒ™š]k5¸;<óÝo“$2MVƒÆ>1 ;<óÝo· ìààytôt·\{Ó ;xB¶qxPé©i”s–+NýàUËÀç/`ôôÈesªø@ƒ‡!ºcê¨ä'×1zælMq°¾hÓa ²½…E€¢jÀ>«%ŠVïv,ÖÛ Æ„RÊèŽçæé ØÿGÏ›–®ÄÕ›HN 5:iéJÝŽb ÂŽpA~ó…«Á†²,°Á¦’U³ùE„\nø9kM`Š’„›óIô‚p;œ–-ÃN·w|øÑG—ð»ÉÛõ©«| …Ñwàê~Äv»NK‹ú²F¶öµL?«ÖÝæÇ«¿N»b`ÓÓ{ñµ7 ‡zæS`9ýýÕŠîà Á‹‘ñþŸ/@.‹+`CÅÅ¡ÔöåR˜Oミ¼;—¯˜ßòul}òñ 7âð@³äÁNmó–$4U¿›Û±èCuÔë•SëÁ| é°CAQTdh,$× APe¢ ã„Çá0©µ:Á šÉñUYÞfÆi†ãB-aŒö·¼<À‘@¿nàáÜâÆóˆ—ˆp÷<çhØa½ò7v0i,\/L§¸<Àñø#†€((þfô,>=¿/íXr{0»|6CÖY/( >J- âæ±³#¸vÇuSÁÁ¨"žZ~Ûô÷Á˟׆ÜúÒQ<úÓÿnx> .^åÚÚ¿ËF3È®(¥90Oƒöï‚2û´rÊ”ho¥ñâËïâÄ·Ÿ„w»µ_WMÍÐu›âòÐJ¢X?(>j¯öܦݸíËš4Ò¬ÚHLçÓ'¤+Ìøø8Nž<‰£GÚ>ÿ†]Jú˜Ðö74Zyš\ È2êî°ûÙÃèì'HdЬ;y'¿ ;»b-Q/žoÉ~.—˘ŸÒ&Ü×Ó}±´˜CzjÚ’®˺ðÚÏÕ±Qéz|úPͰCz|×ýkäfçLÍß2è`ŒA´¹r³³øø7kŠƒvzÀÇvZ"?´bÙu.ÙD"1fÇ6B€‡Æê¬<ÜCqá­(N}¨;ž|j‰«7ßµÝpZòÉL\AêÖ$d‹îØóøÐ á¡pýÁíÁY†¨È²ªB%ÒZLéR’¢ ìöX*]ª¦a<“FÌëC‡Ëmé2ìt{ñ½ƒ‡ð•‡Æ~ï î䲦ŸCSe§>„3¼\x«-Ú–* PËyÝáüÑΖé_b±úIŸ‡‡`W mî‹éçÎ#y{ÜPØà–.tn0vSe1wÎëEGX§DDRÅA‚X,AEH%¡m8–Jøèçoáú©s¦ÄçéáП ¡ìübÈá÷‘ «¥®ÓúAoãÀ¡@`6CÝF°CÜéÁ07?:ò¦ACZ kjÕ ß ªERáwp5Ö£ `=å]€¢m¢ê~hÐ)±Ԗ–‚ ïÀ÷o¿¥ûôg³Sˆd£ØW‘&ÃzÛ²™Î uê 8À©b¥îŽûsº1+ 7ÉsÙ;øÆoâ¥ù'ph0ø%óÓ¾Ùñë„M¼[žÃÎp_?ð ‘ŸÂõv}Ø‚‹‚ˆ¢(ƒa(¸ 'èå͆–ÿÆU`^îܽ¿Gðÿþ ¾súß ·seö-P\gåç+Í]QJq°}ߨ¸=,œ7%Î[©E¼øÃwqâOÁ»›±î}ì’ËC,à‚((XmLJÓ€“gàr3hQ4(g'(‡Ï¶í¹}º1Y‰N iZí&Æ¿2~¤;\«püøqC.êÂE5tJµ UH6c!ªCMÖàîðä¿@ ÈY vÈÌÌbäÍ_ û©?ùZv ½Ý ™Ö»÷,¤³ºÚ[hKWË»;¨Š‚ÌÌ,ŠéŒ¥Ó9òæ¯0}åzMqtí}ƒ‡Ÿ6^|òë·0sù÷¦æ€•,¸òÆ›59sÐN7Ü}ƒ¢­±,žQŠV-îKvm'xh|CùS«%ŠR%PšªLˆw—¡ÝÅÇÞÑ <‹y$®ÞDâê-¹¼%+nG8Ї"QôBëº8,;7,»6ˆ yÐ.Ê‹ePeI°`6Ÿƒ Kˆyý`(ÊÒåØáÿúÜW0‘Mãÿýý¸<;mú9ÄùÛPŠi¸¶ìµÌÄf#)¥´¡p‘þÞ֘Ċ"©ºq4¥ÃÝÁÛïó‚6x(¤3¸ò›· ‡ßý¹#Æn¬ü>0ë´c<†@‹<("2WË b±©$@U”óyR0Õ\·J%\ÿí9|òÛ³†àƒõ´eïn<ñ­¯Ãér=ðع£úï+hr{iTFaÇC^x§¹ÀCÁ‡ ;Ýasó£#oF`‡•ÝÍU…‹a –«ÖØz®¹=jm·†œ  õˆË©¬¸< pìóD1Rз£Ñ¿fná¹B?Æ ‹Øî n^‚(q™ù•a‡Íœ)B0{÷¥ßè~ ÿq¬6èe·‡}sQ¼´ýâ},Àj&÷=íþ¼mÒ^dUÅGÉ%·‡p,M›hµÇaä3jèF1;PÀ|^À|qÕün‡¾ø7;f½4ï‘ÉÌ-ÝãkŽï؉±|÷ÁãÏd¦¶ÿÚJ/Í­·¢” ”"ÔìG¦Äw+µˆÿ¿ßáÄŸ> ï6kÞW¨²†O®gÁ Ò`Öqy( * 9ÁXG‹»@PЮ¸Á~B ‡v7ˆZ¤ÞHÓ²‡˜Ð~Ý;Ä¿òÊ+8qâ‚Á í‹àÈ‘#Ø·oFFFô]ï · ‰  œm3nhr®âÞ¥©¤ïXDµ¸;´ÒæxDÍ“a‡·ðCH‚þ°û=ßRnáþ^8øÖt[×ÓÞœnB[ºZº– E,LM[~³À±FpãÚ6ÄèÚû~þˆÃ'?¹Žk\$¯XŽCßÁ5ADúõñoÖäÎAÑ Ü}û-µf¶®{Ú)»¶²"¥±²,C+öî.õÎðVCÀƒËWåò —E¤F'¸z™;³–+ïB „‡"Q<^kǬjÚŠ{CY–‰sre<ë€Ûá°\Ú²‚A–Ñì°<ôTÀ‡¿:ô,®&gñ“«#¸š4w|PJFß»÷1Мײå 瓺ÃDj‡¼5^æëYØ›™IT?¶wÇ–nNÌs ùù¯ =\€­Od“ÅÖK¤—q6v`vXV^ת…LwwÐLޝÊòÖs›S»ƒUa=eà–W€8Ý«Ûåa¤0‡DAJâ¼Þîw[v0Ðæ¬;¬n?á20Ç­œîÙ²;vÖ”y$?‡?ûý¯p,ñŽïzèŒåi£²¡t¶1 H•¼;=‡‘%·‡Íâ©'ìPÓ Öz5sô³Aüéby-ì°êUÕX,‚e=psìƒÓit¬€Ç$à´(Þýño;€±Ü"^¹ñ‰±«¼œƒ’x L÷—Èüi3©e¨¹ë¦Fy+µˆ_y'þ§'á°Ö«+UÑP\T iEt†ø K§D„"NkCpê’þ÷?ëÅz@q1€¢[§=·L7&+ÑI#!ÍŠè®èÀC‹¦Ož<‰ãÇ·D¼øâ‹øÎw¾£;œ’¾6ö¹¶è”Zyš”#ÆJ5L܈š,«Á’ àÂk?3 ; <¶¯eê&ÔÓ ÎãnÙ¶W­kÍ0èÞµ£eËAU,Î¥OÍ[>­™™Y\|íÚÚu_ŸaØA|üÆ›H^¿aZž–A‡¾ƒÀ¶(\dUMœ¿PS]R4wÏcÖ‚ÁÊENˆ¬D"q*[sâ Ö´ƒ‡3Ô 1­áÍfÀC>¹€©‘+HÝš„l1 1æñ­1ïݨ«ÝY†¬’Å{D÷k¾X TP–eÜNÏ£ÇÏÚcØßÕÃ_w~§'ñêÈ¤Šæš*£0~||~ëQßš*CΧt‡ ÷õ¶LòÕ×÷üXõש`—¹×àé+Ÿ¶æã}^l}òqCa.ÞThè.¢Cƒ-i™I´Áõä·Y!XO´¾ ói|ô‹·pû½÷M7¸¥ O|ëëõtW&3e`á qw¨IF`Çm®¶»B€jâü³ÑSÙ&MYŠÆO,edš9°CAk‚*¿j(« 8šÑqn;l¯•a=is,µ­¥èކwèàìâ$Ž…·âZ>ý¡¨ åeCØÁèÎû͆V&"®Ü;Y{½QŒäï´fS$¼’øÿºp/m}ÃÛý¯6vX¹žj·‡¸Çíþ»ã|#aÃÅz5sA½°Äñ«š†ù‚ðÀ>1›-akÔW}ÿ¡ ~wXÞâ€UF‘'ž:„K ó™ÿÿÙ{Óà8®ÏóŸGeÖT 7@ðhQÔIQ”-Ót›²ÕcºÃã¶Fè™öîÅÕŸ´1î9ÆÝî3ôD8bz=±K¶ìضE;$Kš¦Ô’MI”¬ƒ’JâM A pÖ]ygî‡ÂI\•YYUY…÷ÿ‚BU¾—ïÌ|ùòýÞÊRûÖÓƒ bý ƒ{‹9#TVãW=5èöÏïߘJàøÿã<èALë0ŒlÝ™L¯ <€a‰˜Ššº•çnX U)îÊšfOû¢ÜM ÜMמ˷“•褡¦E´ºèêÝ ¸ Ùœ£ø‰'*xèëëÃñãÇÇÍs¢Ž Ð) †¡I¤³8LÄݨ”r"ìpöç/ >f~“Ì®‡î«(ØÁ_†/¬èöç 0}kdíñÍ,ìP©ë„DÑ‘QÇôÁõúçÛ?ÿ§¼â¨j¨Gïw¬Áz…puèxäJ¤èð-\ý×7óŠÃÓÜÚ]å¨|à¡0"«RНŽUÑúòÆ…·@‰ÁÐUSqÅîŒ#v'‚`Kva©*Ɉ\ºŽ‘þK“)Gå{ÎÅ¡;\ Û 8$$qpЉ}#QÒ Q!ƒ°×™»œ+š†áØ Ú‚5e=À½Í›°³®ÿrí~{é‚­q‹‘KÐ¥øº.GåÙÊ‚G65TD_2tª”Û s1™2GÔ¶e¡–˘ÛMªv>dýa?*ý„CM5m•Ù-•”JC•d¨²œý,ËŽ·Ë,gM\Ä•?œÃ m{÷Ÿ=ŽÝö¸épéé¨é0Œ7D*3{¿®˜Ÿð8R³eöæiSB E>_}ÃW ·%Hǰ­3ššì0ß~t})ð`g]—ü¸;؆2N›wyhçèõÕc mn¡û™è Ž5v#ÅIJ'Ðî«Î£Wì`õÚUpØa•ïj€ø§íß@ïùÿ×Ök긜ƳW~‡#-x¦£›]€ËȽÌl‚ޤ3ˆI¶‡Cz¸ÜÚ…,¤­ °U€cÖ›QèÆ*íyQ8Eסh:\ì*à‰°ÃÜçð΂£IãñÒW¿Ž½¿ýâŸe´ÉwAyZ@ñ…Z4TÞ«Iõé ÷©þþôüø?ìB¥/'M1 -$EC"­ Ú·ºÛ¯ªèD ¼{ù8­ûåèšUÑÁÒyºN0\ŶéòéÊd%:i(¤iå.ºz7´©wM…@?öîÝ[eðä“OâÔ©S溃…!Œ‚ò4Wf§Ôèb k œw)¶èîÀû¼ÄÝ(o9 v€þWß°;´ÝÓƒÞ£‡+¦n<ÕU³d-±<@c=â‘•ç°ç`Î멸¼ëš†èÈ(„Dù¸.ýù –œWæä°ïûi.(„«CSÏt|ž@€Ü J U1ðâé¼âp7lwäZ…•Öc;DñH$2T¶÷ ÒmŠ?&ƒ5³ì;Šfá m‚<}Ót|Cö£ýþ½ˆ\ºŽÈ¥ŽÊkw¸~ÞÉÁͲ5 ¢ª`2"€Q^JÉnOþ/Ž 5H6 ÇfÐ௚|ÊA^‡oïìÅÁö­øÇóïãÒä¸mqËÑÛÐîÆŽÙ™ZËÄL‡ñׄà®òWD?’Ò™œÎÝÝ¡vÑâ|ʆ>Úÿê–àšvt›Úi}±\78Oiûo°¥Uõµä¢_ ² EçdA `C‘$ î \ÄçÿëM¤g¢è§æ]+:bþ¥ qxÈ£/¦¬í<|$Ü1;ȳ!vèô„dÝöåÅ‚M…aS|êâç×\Ü '6Çg³ ;ØƦ´Í-Øö*óÀôÕ÷˜vy¸!Fq=GgÀ¡LnÜ c `©¼ ûÛ‰0‚Ýí˜Êåÿ5ÒÎé€W2Ù¶Ðã«ÇáN¼2}Ýö®ö^üú/LàXdŽul…¿É( ì0'QÕÐ?1…ö`ÚU9„5ò«»aäpìºñäðÛJǬÖ€t÷ÎôkœSÑu¸@v€ûàƒ…1i{UÎ}_úí¯­ÏÑŒ¾¶íß4_øñQÉîÀP ûþ½Áþþxîé º´ùU¤å°&ð’ ¯<¸\4¹øï dIËÛ=oM ‡ =!ÑF¨3Ò¬6„˜ÚGLpòäIœ8q¢"Êàøñã¦Тƒõ|«â:¥!GaÈ3¤s8TVݾôͯW¤»ƒœ $’`y®âw¶/µœ;\|ë j~C̺-mØì›S7.·Ûò»¾rTxs+h†AôÎR'zÎëA}G{EÂéh ñ±ñ²pu˜Óǧg FšËóèýηMÃv»:„6oF÷áÇQÕТÒ)ß:e«à 8ó:é`‡‡þrn3dUJñuÀÓNKeh t½t’ž m‚½mÉå¡ÿ·¯;&‹!–¦!ª ’’ˆÉ4ˆìUBQãñ:6}ºa`,™€²‚ ÎëÇÆ™ë—ð›‹È(Š-ñª©)dn ï¦{±XÓŠÃCC÷ÖŠéCrFÈùØiÀC`vׯ•O[š`É>¼qèzô!ËçöKK•×lÞä‡ " }K d²Pƒ"ˆR)R(%ÐĵAÜüàŒ\øŠ`ÿ®ËãÆî?{Û;W<±»&sÃûI[¡«–îýZág8@·aUí‚9ZùÚ™æ7AS‘T%[Ü@_i¡ðjq:vÈ¥ýXJ£Å6IÙ÷œÓlSY¼;=§gÃÏ~ÕW¿Ç4ðd]ž©Ûð.'g°7XWœë eÃñ9;#ÚyÁ°1®uÚÎJÇ4HÀÍ…yŠÿÞùxA€Hk N}Ž3Ó7Ñ×¼GºZ€ j_?² ¡XSÛkƒðs®ÜÚP±>¯;µÁÌo+þžM›‹¡W?f6ФCVuŒë||¶Ü) `]4\. .ŽÏ1öÀsŸC:°C.-,0ß®Å{ða<ûÇ÷¬fÔ$´ÉwÁ4<žG+¬¼¥zâRQÎóú¥ìý}G¾Ñ¸J˜ß^Ì$d´5ê`˜ÕM[¹îWiĪb‹Ø³2ÿÜG1lÈUÒÆ†9)Q¥×iVNWÊÝlzÇøJöîÝ‹ÞÞ^ ˜ï|4«r:¥¡Ã#04t §^¢ópwøÒ·¾^1å &SHNN#-YüËy=hݽƒ4”ȉ°ÃЧ¸øÖ;¦ÃšðÐSß©˜º¡á¶M fCµÉPKõI¨’ Þç­˜Í?—<£Ë ¢#£Òé²J÷Ч–×ÊÌißSi 2PEƒïžÃ­>¶%î@Û¾ú8ê¶u“›@‰uë£órë`â<î’ׯœã`U–3AÏ•Íæ7ôQDŸþåð;¾z,ÏY¬³,x¿¯duC`‡ò66˜Žâ懟àæŸÄÍaN[î߇/;j‹ ÌĵAóÏî*RÙE¼÷Ï»;äûh³`?áÓ²7?&ó¦:Rªlì.Šv6ì`©=:v°šwKð„…´Q9ÆÃió.AÖoÕtáås“Çgb7ðLæ€×S$Œˆ)´zü&ÒkÑÝÁL\N€`cüTu¾R8Ÿš`äì¢ÜvwßoØ…_ŒQ°kÆO†>Ä™©zôµìÂÞ®àÑW/›Ýîþ>¥(èŸF{  ­ßòc(ÃÜyœ ; Çc(“ÕËR+c€(j¥…ŠÂK®ÄŠªCQ(ZÏ1ðùY0 µ~Z‘C™nÑ€Ü^X„~|w/ú§§pêêksF‰K | ýöŽÊUº=q¹h§ûÉèl óAg-nÐt‰´‚Põê8p\iÜW0L ƒ<ggQââ@T1õFšÖ†S÷ÔÛ¿2&ãäÉ“èë뫈2èëëóÏ>k®ëÈQÂhî}%蔺]Œæ ‰ŠXM)kÏ+;¿r¼Ï[Þy×4¤£1DGÆ ®âB.gˆÉTE.z.uÙ; vˆãüéWL‡s¹yúÁSp¹ÝQ74ண ,çÚm“f˜ŠvvILL"55SV®ùôÏ%÷­'¾a vHŽãâ+¯"9>‘wúYžÇæûöcó}ûM»KÙ¯äø8ßy×rxŠåáiéqîu̹îqx 2£H$ÒߨØèÐŽ&@s-_¨dÕ塚ƒ:‚a0 ( &ÓIÒðˆŠ÷Phè5 \PÖå =ÂíAWÄy§‡RAjjÊt·ßW1“;R:CÏÍugjè¶¥2¢éü^*_|ódbqKaƒ-M¨ÛÚnùÜ¥„ ìààûަADˆÉ¤T² ÀÐÜYj¥§£¹ðn~ð‰%·3ªïêÀî?{õ]¶Ä7rÁüâGÚåv„KR9ÊŠ»ƒqá@ 5ÿ÷£v`)Û½5`)Ú¾üXÈ[ZU`¬¹@Ù0'³îØÆ(n=[­÷‚ÂŽYsµÓ`‡5€Ÿ2<À“ánÓÀCZSp.:‚¡ZÀP:ZÎ÷âg^Êdþ,•—á !×ïJ;¬•ïq‰ËÃi¤ ÀÜRxöÊŽ´à™-{ѸÉ5 >v˜û¬ê:®Ç☲nn³¼,+v°Ãb%ØaÑwn×rgM3J+Kw§AVôrÙãîJƒa¢”$ÜnUÕ,¨»+•²Ð?ö¨À´ ÈPó¿Ÿxðú§§10=e©-kãÿ ÚÛÐ| î¹{M ýœÇý>þ9üø»5ßMJkÔ*×(†¥Àºh¨ qžvœä@T)õFšÑâûNõn¨ø•ép•<<ù䓦Тƒõ|«¬;¥¡ÄaHS¤#8ý²­&¡'­åîîL zg¬ìýVija‡t4†·þO¦Ã¹Ü<ýÁ¿«ØȺU¸È‚슓”Î`fdÚ*p—“¥ˆ"Þá×yÅÑñÈ4÷ä¾@}ôÂ\}ãM¨’”wúC›7cç7†è]|åUËuKÑ,¼-½Ž^Ÿà`w€Dô6€G–(FË@Yå"á 4AŽÞvdav‡ëÑ®C[ º¡CTUÌiÒʈJ¦Œ"ƒcÊ"(gèȺ=ì¨mÀÿ8ÿ>nÅóß5ÛÐÕ’BV=†Û7ULßQÄÜ W3î‹ËˆÉc„ÉÁa\{ÿ#k.žÃÎǬߣYîêÒìžN`gI×4H©ô<à Ä‚Ú)*&侚vÿÙãØòÀ>[ã½3pÑü;OvU²:î°;©™…[ô<‚bÃv] š¡/9 €¡hÐÔê™êô„àg¸ÂäÇDYІšÛ¹L,êçÖ Óñåv\¹Á&†Éa½2©»l0 IDAT¤lüžÓ³gO÷dM7Ž3o"®™›D>ÄT3à—³‹ÆS1ì„‹—Ç|a»`3ß9vXüÝ .Q·¿ž,Î.íïÅïà½þ;øÚí-èÛ²›]« vXü9&I8?6‰ö`Z^sç)Wع¦ëÂ@ÓK#Ö4ɤ²´ÕSs÷n¬;,»GJ¤) > ¯5W–+}~LÎpÀìzò Çãä¡/ãÐ+/!n奲.C} lë¿)ڸDZϤ±Óaxž‡”ÇË鴤⇧ÏãÄÿöPëœrŸIÈèhYc.ȵú8Íí¡‘*ð /ô—³2ÿÜJ1žÊkÔr ª„:#ÍŠh-10¡{¡EÏ› ööÛochhíííe_íííèííÅÀ€¹qŒžºQ¾ÒÐaH0T²f¢,ÆØ&ûçœv~å ªëëÊ2Ïéh ÓÃ#«::Ü-Îë!îv¶¹YØÁÌ»ùBKE¼ÿ‹¡ˆæŸúþ_ ØÔP1õSÝPWÑîµÏÅÆÆ‘‰ÆÊ6ŸþåA ©g:>’Ó±ª(âÊ¿¾‰± Ÿånw €m_}uÛºICtßy7/×¾¾ËñëhݱñH$2TÎ퇥Q?Þ­Bvú/Ⱥñd¸§&̽(x/1‚HJ@£?»³ö”$`JP»ê3ä‡(‡ÁsjÁ…þ?é8T4àaN¯OßĹØŽÝÙ†c›;áo/Í.‚ª®ãúL S‚u{`Wq,©زøwØaî/ïb © «Â kZÎ. †¤Ò*$YG0äZp{0 ;Ìý½_þ¸ðúco¸':€¿:û{kóEÂè±~ÐÁÞ ;~5”„¥‚=ŠÎÎNüä'?±|î‘iœü_×Ð÷o;Wqóͺ(h+ì¥é:2¢ ¯{å×l¼{uàÁãeJ¨e×(OK4äŠ?!ÑF©7Ò´ˆLˆí7 <ÀóÏ?“'OVDôõõ™vy0ÄQò (®¦¼:¥.A#€®’Æ_wœ<ÜøÞ·Ë.¿ª$arpB2•û5Œaмƒ,Tµíá@ØÞáEÄMlx8§{=ºŽ¶Š©o(X¶ ÑÊJMÍ 11YÖN6×Þû£¯Z_ÕPm_}<§c…x^<×bø9m¾o?:9–¸¥DB<ñ.&:<¼N[h€,|÷œå󺪛àªnrþ3˜æXàál¹·=<”FŽ´¡ ´&Bg.ô†®B‰A‰•v¸=Ø]׈­5µpÑ dME¦ mžˆ*_rTËz€§z÷cG]#þñü{È(J^q•z°²Ã³Ûïƒ?\c[¨5w.¬4U…¦ä6ù:5|ªœ[³œ µm œÇÚÃÌÅ·Þ±4ÉÁ–&lúÒž¼êÅ*¾µž¯&D`‡RÝGR2 !ž€”J‘qÒÓQL\ÄÈ…/0qmŠ µO t˜ÓÕ?¼g)O¬—\+,õu @yçC§'h ‚‚†­ç4(ºIËm| :U‡‹fàf²SnšÅvoؾ¼X͵Z”†µ8gó²®üÓGUP§Ê7Ïvº;#®\ãñ/ÐWßcx€33ƒè«Ý–Ý•ÀåTðü2ÈhU@ÁRyUì`µ­Røn—‡ÿØz/~:r¾¨]5­)85ö9NO\Á±ÛÛplkü-9´g[Ý ²m+&J8?:‰öPZ«}æã¢òH+l<ÆNG‡``Áå!QW…Lƒ=³ŸUÇÔ”„@€ÇÓkçs-¢Fv©À‹®ÝÛqvôN]µ¶ H›þ”o (Wõ†¿é›–Â;v ÙÍ(òN}pºÑù`qwpc9 Rfåßie àYc®‡ï¦!‰zÉês(²æŒ sR¢J¯3Ò¬ˆ,ŠöoÅ…`ÈæœÒ_zé%Äb1ƒå¿Ëô“O>ix=}̪Àƒó:¥¡Ä-ªD¥“UØ¡Ýâ‘ DZô;;Ð C‹MŠ;vxõ LÞ¶ÔÚ臭M \n7jZ›I#­Ié b£Çõ7+׌<6ey=ß9–t¾…OCÍÃ%Ⱥ:ìzâ(Bm›IC´¨èð-¨¢ˆäøøüÿÀ,ä—,]4qGy<ƒ9xè/÷öI€Òp–u6qCWD(±ÛPâc–v·K<Ë¢«¦;ëásqPgwõ”5²#‘ƒ'& ºa€¦ÊkõÐX27ë‚›-ß[ý͛Pwð0þÇù÷q+Í+®bCVvy·o²wPÀq%«;9ÉùØ©¡Üˆ†mxˆIGc¸øÖ;–Ãw?úP^ç÷TWFáý~Ô´mQñ$Äbqˆ©44t:F² `âÚ &®âÎ…‹HÏD‹ž†b€sy½òó»)Ð.·ã-#9^TDè’ùFÇê¶ÍF`á¤e;¨†QS×ttXMŠ®A‡/ãÂvoÍ ‹±-æÅj¾¨ÂGƒZ%oFa`§»;Py¶I§ÂùÖ0:Àó Ô¡Àf´ñ Kæ&¥ÏDÑ—Ú %€Êî9Åî@8·öWjØa¥vQ¨ÀJÝæ|Œ é¿Ëåá‡mᎠ­å·i€¥ç,MÁ©ÑÏqfê&úšwãÈ–V A^9Ý€ßs®ÏÄ1•Ñ®†Ÿ+ÐÖñ«¹;˜qb°ë˜µÂ"·ø½‹¤ CQtsõ”c?0 “QUí‚ÇìßjÒ ˆ0ÀôÂ':€þéi L[X¦ËÐÆßÛúçr kdFL‡Ùºuë<ìpäÈô÷÷ãõ×_·œ†¿?ÓÿÙöÐ\¼†4C÷0„å ³2¢`ù2¾* »vÃuqÅ–D†M”§ÌÞ'¢J¨3Ò¬ˆlS{êè˦ÂÄãq¼ôÒKèëë+ûü···£··æºaêÚïüNiè0¤ jš4ör’.A_°´œÜ¬¸:Ì©®£ œ×CÚŠMšE&sTš†>Àµ÷?2®ížìüÊÁʹOs\E9UlèK»¦!66fEŠ(âý~W½ß9O`ýÍ=G/\ÀÅW^Ë;ÍÄÕ!w%ÇÇ!ÆâHŽÏ»5DoÝrlz)š»qgy\Ó=&?[îm—%P$éollŒ8.qR ÂÔ-K;Û©®š:tÖÔ¢Ñ_ UϾH˜ûKDT’5­,ÁáØ Ú‚5e =´kð·f¡‡OFós§)ô $¬Áe][+¦Ï(bî”v|,’ó±Á¦†%ÿ»,<Øœ?ýŠå|m¹üuá¼ÊÆ,îpbÔ’ •¢Lv±Dt(!O´TsÑ‘QL\DìÎXÉÒRßÕîÇFkÏ®¢œïó×Þ´æîà'ÖºVdt€ÁMÖÞ­ v0l?Ÿ¤©ó|öÓtõn/‚¬ÛžüXÍÛâ°—ÁІù8çc¹•Ó_ˆº&°ƒ=mÜJ\fV‹Ë£©…EÜ}õ{ð£Ûæ ·q%s3wp T;ÿÝ”$`JPË{àxØ*ì`áņ5|*àÓ€tvgÄZ—ÿGË>üÝ­JvO—ÓøÉЇ89ú9úZwáȶV ¨¬œ›a‡ÅŸcRÖí¡µÚö*³ÃÝÁɰÃJå˜Ãu†AXÿ~íbiëm–’IŠª£ºÚµ~^WËÃC2p†f§]‚“‡¾Œ/ýÆÚ [C¸=q tuyì&fës¬‡‡#GŽ,ùÿ¹çžËÐÃÉN¾y }Ñ ñÝ5祡ëi) —÷·‡¯jý¹UÞÍ •(ÞÆN,³ÐY¬¸;PŒÃšÀ¨Rê4-¢‰ Ýkx€çŸ¾"€8tèiàAOßp~ÇÔèb0tÒÐËm|¼èæ7À*'w‡äÔ4¦‡GL¹:Yg‡p[+|¡ i(6)55ã¸ر±qKïáM Ø{ôpÅÔ Í0¨mk%N& ÄÄ$RS3¦¯yNÕǧ‡LÌú:†î¯>ž“˯¼Š± Ÿå•Vâê°º²N ˆ—ذꣲ®!3ühÞÖ_—¿Î±›2ÒΈÃQ^çQÇu8955]’sWónìihF{ ³®èr *ß Ã(ÛtÇfЮSf‹åuqø›á7ðÛKòŠ«Ѓ•En¿þpMEôC×!åè𚞘Ê}pX›§ ÆÐ§–,4À_Î{GvÞçã*îp­®£L¨êaR–!ÄHÏD¡)h Ü02V‡»µåþ}è~ìa„Šh[;rá \=ûž¥°®ê&Ò,HI˜‡i¶z‚hä|ó»´ç¬2 ‚¦@Óó9ëe\¨e=P Ý„ÃCaXŠ  ä ;¸(†5Ÿ~+yq:ìo»´v  —ØP¥,zLp&6ˆÉf jáeüõTAŽ»š#e¡â²àØ2ìMÿ&¸¼ðlûµ?Œ_O^Æ ¡´/½Çå4~2øNŽ|ç:öcï– ¤˜ëÇa‡ÅŸG)DRt†«ÑXå5wžRÀ0qLNñëÆ'ˆTI¥S m•rzùœÀ†µ¾g”W\9†Y!%à­…´ì ×â¿=ø0žý£µ1°6ù.h@ófüjw,…;pàÀ²ïžyæ\¿~7nܰç©®âHo+{‹»[ŸÛÏ€qQ2: =ÛβYÑ _ 7·9†¥Àºh¨Já'RÀóÌ<ø34–4çÀyH9UJ½‘¦ET 10¡{¡EÏ› 6<<Œ³gÏâСCe_O>ù$~úÓŸšëžr†Å…œyù£0äÒ¾ËTzÜÚâÎrØÕ^×4L!5¿P–f4ïè&Î6*!fbCÁbHE¼ýó2Î àО²´É¡SnÛTQùÙˆIÄÆÆ¡ÉrÅäièÓŒ^¼j9|SÏl¾oÿšÇ¨¢ˆ“÷âûºî.ìzâ(quÀR¸!9>ŽäøÄ Û|S—R¥äé› ]n°ÕMpU7v9§þi]tjñ G"‘²·Ÿ!ÀCétØ"ïêÎ3,ÚC5Øn@5Ï“VAT1’5^—«<³ÐC[°¦¬¡øöÎ^ÔùüøÇóïçO!¡CW-¹ê„ó\Èï¨þbbGñȵÜ_†×¶mË-ì~ìò˜à*¢ˆWß°œ¯‡å]6îêâÉþºZð~¹ˆÛùPI Ç(:2ŠØÈØ<äPJ÷†»å« ¡û±‡±å}à<ž¢—ˇ/¼hía÷;vçGµºd~GÕ#5³˜TlØ!ØUfÓNt[=Ù¿2š‚j–·'?TþåîeX¤5Ù|œ‹Žc)AŽ_9ý”Íí¢`*‡zt*ì`º ,ÂóƒaP²PB;À·jºðòÌ5SÉz/1‚HJ@cÕÂBNQS1”I¢Ó°¡¼ àR’+ŒPèøí„ò88¨@<;è¦Yü§¶ñï/ÿ‹#î“ãrÏ^>‹Þ;uèkÙ…½A  ­ŸO`‡ù1¼®ãòT #‰4:Ã=œ¹sÚ; Çcr…%l„’©ìïú cQú* Ÿßí‚ßÃæVoë|'J@Öéa­0«A…>]p}áúu|O/^º‰·ÇF- èdh‘7Á4cãŒa3懭[·¢±±qù܃ß'Nà»ßý.Òik;žýýk8Ñö,î*aOÃÅÓÐUšfÀвî~ ÖE›ŽÏãcŒxðxÙ%Ïý×-,Œt ð@ ¢J©7Ò´ˆJ :´ß4ð'Nœ¨àÁj ñŽó€C…!NÀÐÈ;‡²_'¯ÀPÍC¨­»w uÏNGçML¦0qcª……¿œ×ƒúŽv;Ø(Eu\šÎþü(¢dîyÌÍã¡§þ¢¢à€Pk3xŸ—4Ô2î_±ÑqHétEå+åµN¦ª¡Û¾úøšÇ¨¢ˆO~ñK$Ç',Ÿ‡åyt~Í==¶ &ÇǾ…äø8R³€Ã†O)"äé›§o‚ñÁ…6õ—Þ‹Ö <ôWB½à4 å¹ÀÔaKU[Ba´TÀ‘]¤‰ˆ'IU1–Œ£µºüm"¶mE×ÿúÇ? £(–ã1tBä"¼›îEÛwû´âî][m-§b»,–œÉ}BvzèvÎdžÛòƒBú_}ÃôDËœ6íÝ ]8¿:a٢ Ç!ÐÔ@.€vôk9”T² 62688 nX¬-÷ïÖö¡¾«£$玌â÷?ý¿¡Öº¹Ð&Òà,H¬9‰¶š[Q nÕÂ9í†Zø*xé,x,jjÀƒQ´2ð²,$]…j1¯^Æëµä$vX3^'ÃT¿¿[Õ20½ð2°¯¾Ç4ð§'¯à™š=¯ÍŸ$“B-ç^ƱéZaÕ}Á*Œ`æ;'ÀVˆ¶ ðYõü¿^» ¿ª¹Œ×gn:æ~9œ\6ïÂÞîàÕWΓ°ÃâïS²‚þÈj½nt†p»˜ÜÏŸ/ì@ÙxL`PÇ0hªñ –’‘–Õùã\, ?ïBÐÏ­Ý)s߉¢PwAwßcÖŠs§ LÐ@báÇ“}{Oÿ q sázú&há(OˆÃZqx8räȪ¿ùý~üøÇ?ƳÏ>kí12þþì=TšÅ‡4KfgÛK°x¼ ÒIºV¸•Ï•+戥ÌÏw•Ôá@D•Pg¤Y9@´+(.C677õòË/chhíííe_>ú(Þ~ûmsã>atõnç\NÔ4 i0tÒ¨ËXzòŠ¥p|ïÛŽÎ×ô­Ä#Ö[úBâBo³QÄäà°ãÒÕÿꈛ·ÿØ7¬ wÙÞP¾P4Ôr¼†kbcãÈDc™¿ó§_±¼N†åyì\Çm!9>ŽO^ø%TI²œÆª†zô|ç<À†iwB<Žèð0RãHFÆóvƨ4iB ‚írƒ o«º©$é t”®8µ˜ÎVB]à¡„c8Ç6 –-ðÀ1,ºjjÑ®ƒŸ#nD•-IUË>IIÂd&…:oùïܼ£®?&'LjõaM3J+Ë~ãõ@‚ªÂEÓÙò¡r+Ûuëä®ïDAÃRðùØå÷˜\Êé~ø=Ì2[íUU8ñÐüÕÙß[jŸZäM°mßèÊŸë¶<ìÝ»wÝߟ~úiœ:uÊRšþþL?þyû—ÆÒ®"VµüƲUñ™Â]{}U,–­#8û'óó‘EÝÙšD•Ro¤i9PLÃa¨·e:ÜóÏ?“'O–}þ:dx0R7'¬¯5tò %Ir¹ßÁä)¢ù÷«ëkíî0òù%S›Ü-Vxs+õ¤qØ(]Ó0s{º¦9*]×ÞûÈÒ;øÞoFóÎmS?žê*Ô´6“†Z†ý*5=ƒÔÔŒãú–}tò¦uPªûðã¨jX}àdì°ù¾ýè^ÇA¢Ü%ÄãHEÆ—88äSfªŸ*"ÄÈ%ÈÓ7K>8ØÝ Dù(‰ 566hsZÚ8ŽC&“±-¾¦ªjl Ö`k¨–T<Q™i*†›q¡Š/ÿ·mÁüðàaü×?žÅTƺ¥œ.¥ M\ƒ»q‡--M0O}7to­˜6¦Ê24%7@(ríFÎñúkBpWY‡uú_³nÑ·ãð!°<—wÙxàý~xÕä¢gaRcrR)R ÔĵÁ%Î éé¨c]VR°¥ [؇֞]ð…Ko>rá |øÂ‹–aàÂ[Hô CW¡¦¦L‡;R3ë’ËZªÀºa@ÔUhÆÊ¸ hº :$<ÃæȺfìmn³÷2ÃÞòÌáX†¢âÝH(2$}õ1›fáu¹¯–þ‚À †ÍñÙ¬‚Áv‡±)mvBÈ,LöÕïÁnŸ3UüiMÁ¹è©iØ…´‰š†¡tþÀÆ…¨|ÛsÒ¿Züu0Éjö:³ÇW‡¾Æ=øÙOy}}z¯OákCíèÛº.À¥¯‘w{`‡ÅŸG)DR´|hU™‡ìtmÈå·œÂ9Å›TÖt‘ð¸ØÜÚ&•[y¯O:¥‚¦)x¼Ìòº^/Ï^_R€ó®…ká¶íxiè&^2ïpb¨IhÑ0áû*{ +™¿ú|>tvv®{\__úûû100`úã g>Á‘¯·®Ò•OJPQ›Ç¦œ¼›Û£Cì_,Á»ixýË_EÌÏaÐ…v3!Q¥ÔiZDS½*ó2`r!ΩS§pâÄ ƒå½õ¡C‡ð£ýÈTÝÂÂtÛ¥ Ð¥ @WI#®éñÏ,…{à{Ç›§xdÂìÀy=¨ïhçõ†a³¦‡GrÞt°X𯀅wðm÷ô ëáÊyîv¹Ýء씎Ƙ˜‚V€ ¤¢ØØ¸¥>:§¦ž=hîéYõ÷|a–çÑ}øñ5ÏQŽ"pCÆ[sàCô6Üu]`¼ÅY£ÁhiÇ–I$9[ uK€‡ÒªX›vùnôWãÁÖ6âæ@´!¥ê•c#:šŒ£ƒ ÃU;¿·kðwÅyû ÜŠ[ß\IŒr¹Áç¹ØÓÊÏÐØU9Àƒ™Å¶ÓC·s>v%(„ó¸s {í½,Ùh@mG»-¼ÏÆU¼aZ ‚ì?‹2¡1…O@ˆÇIaبÅ@ƒ"ˆeãØ°šœ9ÌéÓÓ¯àêÙ÷òŠƒæýÄÝ¡È÷þÁVÀÈak`‡Œ&/qtXO’¦BÕuxXתI2ìR#çC5³üÙS3Œ5R`cy®yìÒŤsNªá‚¨©PA<Í€§04½Î Ë v0ЧepÁäʤ\ë<Ÿ´QVK­WPZ <4ô˜àäÄ©oBK_îŒdR¨åÝ®4T,Øa¥ò/…«ÂJßQûa!a6¸áËŽ]XßoØ…sñ ¤&¬]Œ)0 »»ÙëÓC8»ƒc#Ý8ÖÞÿV­(°Ãâùž¡h‘”€öUÞÂÃÈñ+°CéÒt¢¨­ ;äÜó)•Ràâ(°,m¾,[u`Ȧ¾<ùØ—ÑþË·ðÒZŸùtõvP®JÝDÀ€¡˜ö]ÏÝa±ž{î9üõ_ÿ5Òió/O¾GömZÊ{.¶*ÀBU ¨Š}ù`]4ªƒ+“ ÃfÆ 0žB4¯¢·g¢ò»‘$Ù,Æ&´ÚÔ»¦ƒž8qÏ?ÿ|YgÿСCæi"  …¹ær™‘£0äÒv+Eº=yÅt0ÞçÅίtl¶2Qóü…Zšj!ï ¡ØØ8¤´³\¦£1¼ÿ‹_›hjÀþc߬˜º¡á¶M +`íÍF‘H"66^ѠÜ>>ý;Ëa«ê±m ×…Ñ pñ•×,ÇïÐûo¯éQJŽgÁ†Ôø’³ =ôJ!3ò'p¡MàÂ[@Ñ…]‡å`‡‡J©S<”Vý¾å´D1 †a Y°_â¡0vÖ6ÂÇq¤†‰6¬T½rìËtÃÀíD ¡pEäÇëâð·Îz§o‚v¹óZð©Än›“¯sÁªƒ®uÕFRŽ»}¤¦g ¦rŸ˜©mÛd­NDßzÛRX–ç°óð![Ê…÷{‹V¼ßÞï#îõ&3â ±82ñŒ µ¨,–¢#£HÏD›&® VDÞZzv¢µgê»:9Y—ŒOO¿b‹3†§q'iÈ¥eÌ¿øÙê ¢‘óÚ:+ÿ;ÌçÕÐ!¨ÊªÐƒ¢k0 {V†0V~eg¤¤š(ô0‹§?J;,Y¶. IDAT;P4ü,gáÜ%„,©ˆ°ƒÝi´´c`+ sr递û·ó<]¿§&Ìí,8®¤Ñ›ÂÞPõ²ó_NDqo¸~w’uڲΠ,œÃ Î+ÅïWjHd¯‘íîžÛüþúÊ¿ ­)º’Ð<(†‡¡$ v?Mk N~ÓãWÑ7²Ǻ;€fyåve#ì°ø{QUqy2†¡X Ûë‚z¹¥aì„̆Ïv¸+¯’ìØaÑï†Äb µ.PwÈ9 ¯ðóîYAŽÇó÷îdzï[ƒµñ·À¶þy…ŒX—߬8<äâî0§ÆÆFôõõág?û™éóŒ'œùä6ŽÔ—Öå!_Q4…PØ…è´b ôàö0¨ ° èÙF¯ÍV-œí7ÿLh›»ˆ*¥ÞHÓ"*s1µXŽ?^ö.½½½¦Ý¥ta´¿È›‘é i†FÁU’4‹î_úæ×+¦ ˆ«Ca•ŽÆššv\ºÞÿÅ‹PDs×3—›Ç¡¬Ö0XH²¾ò1%‚曚®#Wr怑¹Ïûàã…1Êñ=½xiè&Þ5îÀkQxñghÖÄ&MÇhÆáŽ;†sçΙ^|̺<Ü» h.o—‡9è!ÔI«Öâ €ê ¼û®ÝBÍÐXÊ|¼ù´mÃmš¨ü®C$‰DDöÞs¸0¡{¡EÏ› Ç+Â塽½Ýô˜ÃPŠ;¯m(qKÐ)QÜN,¸;À—¾ålàf×ß­žf뉫C¥ˆ¢åË…Ôǧg:].7Gðïrz¿_. µ6WT~*¹ÅFÇ7 èdA©kï}h9|ÇÁGVu^ÈvhêÙƒ]Ou|F‡o-roGr|‚t&JWÄy·¾®ËþñsÝ€ìÆü!<”P‘Hälcc£#ÓÆq¤u,s8†ÁöÚl …áçxR¡DDw©’€˜J§ásqðº*ýezøÿò[dÅR†®Bˆ\„wÓ=¦m¯äèmKçlìÞZ1mJJgr>vz(÷òZ ¡rè‹éh ×ÞÿÈR^‚-MhÚÙmK¹ð~_NéµCÃÀ¨&ìÅ×nYFrb B<±!¬)ó~x_äÖ…"ˆãÖ°’\÷¼ƒƒ]–ÜgWÿð®üá\ÎpÙzb€a)n*ùé \'ÐYí^vŽ!…ZÞ Çv0Ì·1§À+µŸbÆ¿I†³.síîŽÕmùøR_Ì*ŒÌHÖíÁ» †4QPða\Não¯¾‡Þ±:ômÞ…½;€·ˆÀ2ÄD ýcj}ntÖàféü\V;&×cm‚@Š¢çƒX½Þ[ù}¶MK’IÖÀóŒ9ØZu`Ц~<ùØW°å—/XóDÞ»åé2æ~ß²âØbå½Ç3Ï<ƒüàæ¯ ç>‹à@s=Ê]MÁ`Á¹ii’˜Û˜™f(x} <^fÁÕa•êî¿n~·WŠ«)Tó*z{&*ÏëI"‘ý¢CûM@e¸<ìÝ»/¿ü²¹ËÀð§L‡ë=zÁ¦†Š©Ÿê†:øBAÒP,UV˜˜D&Ûpy?úË›‚†6oÆæûö¯ø[¾°ÃÎ'¾æžÇ•Ê_rô641 OKéµ~k>c©Ž¥ð@d›ô:®a°«7Û‹uõØ";B­yƒÔT¸Ùʺ̎&Ø ƒ¡¨ŠÈ×Åá‡ãÇï¼azÐ¥ÄÈExšÍ ´­ìð\Û¶ ,ÇUL{Êõ¡)5=1•6UNËîk98/œ?ýŠå¼t?úmåRÌIK·ŸìÒžÀ!ÄHÏD¡äEÂJš¸68ïаøs¥ËWB°µipµ6;üQÐhÞOKé ùÜ÷,Üû8:=!@[cìU€a™@ÐTûò®ëK€ݰoÕH›;[ž  EBÀå¶§,í†rªØÁ±°ƒÉ0vžÃ ì0ƒS䮿Ïo>€¿ºfþÅÃéÉËx.¼ð-‘z9޵õ`Aç–®R8/ Àñv€ñ`ZR 9†Ä8žÛ|?þúʤ5Åú¥K—`dnƒr¾¾ààÃ@rÏ~q_oÇ3Û{áo3—^0w‡•ŽÊˆ˜Ñô¡½ÆŸÝÃ.ØÁ2ä°r2² /Ç®™oUÕÍ•rH.¿¯fé}6WP[Og¡Æ\Ï1÷÷  üŽf/cíUUøÏ÷îÇÎlþN­&¡Ç@7Õ¿þ}´²eÖ&‡-?ÈmÚ»þº°-eBÑ4x¿¯huàòl\ÛL]Óæ!)•"7­YmT°aNÁ–&„Z›ËÂÁán¥§£¸ùá'¶ƒÙk OãN[w8ØcC ÀÃ@ëìEk••—bP]…ûƲw»96­"©ã¼ðÒ®ŽœÝZ× :XŠÎ¯, ;P6ãˆã6"ì`ØÓG)›âY v TËK€‡¾ú|q“ Ñ_ ¢/¶>fÙo¢®áz2ŽíÕ¡õÓU*Ø2QžfûIAòYàsv¦€ Õ€N¡Ý€¨+xnóýøÛ›çò¾^JPâ \Õ€+Cš ûÀºemsjçþxǻѷ{Ð"¯]6Á‹¿‰§IffÁ‡ªâÃ+¦Ë@FV1™!©ÚüoŽE¸Š_€Å¥+¤ÃA°Fz„8k}~Ÿ|´»w?N^¹Œá¤ùWµéAWoh§ìZjϘËFMßÐ`}ξ¾>œ;wé´¹ÑF¦ÐXWYó KÁÃfìÒÛýÓahOK¡šWÑÛ3©7Ò´ˆˆÝgê1 <åïò`%Ýux ®㎨&¡§‡L‡kݽume‘ǺŽ6¸«ýHMf¡o(ˆªº0h†!  ÀJMÍ@H$•&Eñþ ¿6ߎ¶´¡÷èኩ—Û]QN•¤:ÌõÓ‹o½m9ü®'Ž‚u/Ÿ‡IŽã“~i)N–ç±ï©¿DUCñû#:<ŒÔø’‘qDoÝÚíbhh¾g¸*/ÂÝÙqÈÝpƒiÝþA|ÉÿÓW‡‘›ÂôÕaŒ~z ÓW‡!§2Í«®ˆÈÜþÞM÷Ø=0Îux诤6JV¬”^g8Òëšã8@Ó°©:ˆž†fø9b/GDdF’ªVd¾f„ nOE¹W´kð7>†¿ó†å8äé›`½¡œAJ|Ìü ›s¡±{káEvŽPeFŽPÐôpî «”Í®=™vñ­w,>pqØòÀ½¶•K1a+Â!•¬9ÈAˆ' ÄãúEz&ŠØÈ¢#£ˆŒm(°Xpo˜‚­Mà<ž²¬Ë«x7?ü¤ ñS4kÛCþF–¡«Ð%ópÕÞªú5*§@i ëöÛ^ë†ÚF—0†¢ÑÆW瘣‰š ?ËY/Ë’ÀFàÃæøì¾­^ëž—²9­TîmÌrÚŠá8±ø{·ˆ cäãÍûñ£Ûæ¶ŸŒ|†çjöžå׈A-ïF-ï1׎M_µÖÉ{1¾3†*FÚ-8Gl€!ï¬ËC ­øZͼ>s3 ¶Ðåì"%( P|=@»`ˆãÒš‚S£_àÌÔM<·õ>ìÝÚò<v˜î5 ESˆ¤tÖV£Öï^?>»`‡e¿H "‰Ì²cEÅÈŒŠÆ€Õ^×’ßTM_z<•gû´v˜û,É:dYÇÑæÎGhÕ«[øáäc_Æc¿{Ù€G†¾¯”#Ï’G7çî  áv»áž}î÷ûÁ²,TUEjÑæ±XvAmm-¾÷½ïáç?ÿ¹éóžþä&žéÚG—»ÚåÂdzýæç$).0ž‚4¯¢·g"Ro¤i-]½‚aÒ½ Ü]ÚÛÛs9’£0”quØC²¤5w‡?ZVù¬ª £ª6L*¼ˆRD±±ˆãÒõþ /"3÷.Ö à¡§¾S1uÃpê:Úôã0ÐaAý¯¾E´æÆ[×ݵ¢Ãì Jæã-6ì0çÜFtø–¥4—³Â]›QÕT‡pwšöíï_€J’žî6„»ÛÐþè>ì›ýn~ûä†Þ)ÌZCWm(C¥+N­î³•Ôv ðà€û‡S¶9ƾÚFpdðEDdI²¦Ú¾°Ë)O%Ь©¨<í¨kÀÿ~ïCøÇóï[ ‘‹ðµ­ýR[WDh‚ù‡§pÛæ‚æŸf袖·˜ÌmÑçÔðm¨&ìËjWqÁX è½xŲ»C×Á‡ÀòöÁ"¼ÏK.ž6k£C² ,ƒ&® n¸r¨¸aq½Þ¸ˆ+8‡Ø±‚‡ÀöIËXŠZc…±d‡—ª®ÚæÀ°jÛ²!œ µÞøey>§½@®Øa2·žÆuâ´ ;”Ð)Â,ì`¥,s+ â½Ð*ðp.y©Ä=ð¯r[½œˆâZ,e©&g盀;ãÏp ìA¨@œE»;€Ë™i<Óú%\¢¸!X| ¨e²€ã!ÜÉ‚RÖú›âëè0¤©ÂÍ_È<{é,kÁ3Ý=hÜæ\úòüÛ ;,þ,*>Dôp謫†Ÿw­.·3ç^ (š¾"ì°8\$‘‡¯‚‹¥ç›¿mš…(“ß­Ïê°ÃÜÿ©¤ŠšZ.÷ó-þýA8Ãg»&jnÁ·Ú·àå!ó°ë]½=ëhR4ŽŠÎëõâÀ`ר,…eÙ%»+/þüÿ𖀇s×#x&¶ ð“ƒK¯Ã û¯™ßšò´yq8Y‰^~"€Q9‹i8 õö¯L‡+g—GºCš„¡I¤n”»¥ࡺ¾;¿rÑê—MÃÔðˆãÒuñ­wL¿{w¹y<ôÔ_Àå® ×>šaPÛÖJ`‰€K598ŒáO/X ËòƒÝ¡°µ²*4ìãÏF°šv;a‡¹ï:ÒÀ…êY—‡8@ϵÝã×~´fm‡"Cš]Õ ºæ>èÂåàW Pôüÿ…Ð{±;èÿd}ã»plWÐ$›ëoa‡ÅÿÇçoO¡5èC{Ø–¢­9:Ì}Îv€é””Sš§ÓžÕÏ•K{²v€±Îï³×U‡ hðxs°xlÓ€Ë sá'>€³£w—e“¤2ô™À4<^Ð1›“£ã8nMØaÝù–`O?ý4N:e*ÜxBÀõÛ t¶`{i›\øhÅáöuü„¨E "¢JÚmü K.Ï?ÿùÅ/!ZX›QØapˆÞº…èð0’ã¦nÃ]›înCó=;Ê nXO\•ÝG¢ûèA$Ç&ñÙÿwW_{rÊÐ _èÖ'ßÙJjãxpÈ}€ãžV’ªBj†ˆ(O Š\‘ÀŒ§’¨âÝ`*ÌÁâ©ÞýŽGqirÜRx9z¬¯Œ7´òÀÚðàöûàWŽ£†¦ªP¥õ'_TYÆôpîduC×ÖU[mBaèÓÓvšsÚòÀ>[Ë…÷<>„*¢]m4Èaα!:2ŠØ± éÚli‚/š‡|5ÙÏ•¦ôt7?ü7?ø¤h 믅»q'(š<.Ú%Õ‚ÃÃ^ýìî® 6ôÊ®Ñ`?ð°’KÑP ÍR|-|•%w‡er:ìPåºû¿é|v0yŒN kžÇ$¤`Î(PÝ.¹™JÀäøüæGLpzú2ŽE·Áß°r[ž’ELI"jywþee7ì@Ù¿Ý0 ÿzßmO_øç]:=!<Óz~2ü¡õñzò*è*€öm†án€‘†!eŸ» y  Øì.âšP°Å?iMÁφûqfrÏuíGgàõõËÜØañç‘x‘dÛƒ¨õ¹ ;X× —d„«x¸zù¹ri;…€¨Üë$Ráñ2ÖÒ¸[n0€šýª½ª Ç{zñ£ó›oï‰Ë kî³ÙåÁÙƒÝzþùçMpæ‹ÛxfÛN †¬t^©®ÏþÉü¼$åi.|ˆʯ1‘$U”¬º<üô§?ÅñãÇá˜PÒèÒ «¤,6Ú4uÅR¸w¢5æ8¢1‰¤£Ò¤ˆ"ÞÿůM‡ëzè>´ßÓ[1ujm†§ºŠ4Òk=ÐA‘dÈé ôE›€iŠM6·VÒåáAÑ  ÖåëæAÓ´£ËæÚ{!>fm=T]wê¶-ßôÊ¿¾i *°vˆgá†É«W7 àào¬wmhÚ·Í÷ìØù®jªÃCóîýÁ·ñÙ?ŸÁgÿ|Æðazh»Ïô¦ŒæX‡‡áH$RQÄYÁâ …AS!h*< i&DDV•Q„+4oºa`FH£ÎkÏ®i1QÀD:µp Rhkì0ìçx€×å‚ÇåBØcßbñ¿yðþÏ7_ÅT&m)¼8~ Þ¶û–-US“Ðó;r·ì.ü ”*âCW®»’O ݆jâ²¶}“é¼]|óKyhÚÑmûÂjÞï-þ½>ž(ëâª,ÏCR*U±÷’‰kƒË‡¤ú®¸<îŠ+=ÅÈ…/póƒOŠ^ß\x øð2ˆ³Q†®B—Ì_£öVÕ¯pS+Ó‰‡îÅ.š†¢›xšEk=×–ÕW¹¸(Ú|YÚ ;¬ßjÌÅ—óqå;˜X^ްC)Ó5÷=¯¬¨ÙƒÚù¾UÓ…—g®™»o͹<Ôl˺F,®ÃÙó_NDqoM=Ü S|ÁTýX‡T]‡¤jÐtÚ¬ƒ ÇÒ ) Ã`EÕ¼ÀÚÕ²àt UBãˆ9˜&âHÍô''ðúÌMëóÉ« 7(WTu7 ­m|0Ô¬óÅ‚â]„¡d¾‘‰áÿЧ§v¡ïKÛ€FyõòXvÈ¡m¬ªø|4Š —Ãö† Ü.fmÈaqçëÅ+àCâNp0Ô,A  |­0¤Y@“P ½<~oEdznǼYÐÃ*ìPà6±´Œ«fÑ ¢5äϤoÆ b“ëÃP€b.ý²ËKƒãi …ÂaÊd,vÈ}—Ii<Œ½2Ft d±ì!AÀù'ð?ÿ¹õ¾nÛåÁ…ͺù@y«ÂV:wîœå¼¦Ó¸;±ˆÎ=¥»¯ J÷LͲV- ýê[£Ö›>°ß¨´"€Ñn•]—‡×^{ }}}8}ú4©ÄÍNYJ<ë`gè¤2v©ô„]w‡S¤òˆ¶Ttlº¦¹ªL·~z³÷­Ýsp§¿øbÅ´‹à÷£Ö… ¶‰Ñ¦‡†·ü=È.Ø5ÏŒ¡ùpxŸ·,ë_SXœž…$ntœ˜[p vØJšª"T‘NŠˆÏÌ‚#µðwþlÿë?² &åÔõ©On€fï bøoYÎË.ì}€ÙÁAÌÜD&G¥j=ÜîÚ‹`!·¬¯ OýÛqðùgÑ÷—ù¡…Íߤ$2S·àm>jj{Z˸¹zú+­½I$»;ÔçÖ‚-Èê/i!"¢”V䊜ry[,|":ŸN¡ÚãEÈãÌ9«-T‹©ÕÆ* IDATÄß_}ÇVz9ú\UÓrਮd &ç,çiÛ㊧o6ó)“HbÎðÞ aØÍ§;£ï_·Uþ}O‡ÇaJVàÁp;3-‹ŽOÀªríŠ ª,CJˆHÇ‘I&a¸ì^Au¿6,C»n¨?б7´6÷zQ c×Îf††16pã×oA\ˆîX9øð>ð5{,­R@d^š ÐžÀ’»ƒ¶…W"ØŠ‚æ`‰‡a·,¾‡á 2 ÃÜó»;lŸŸåÀXqµrv L´…ã°ÃmgE”ù6,º(Xï_VëÄM°¹eÀêtõ^è‹w]ZfWÉçj³Ž†êøYmÙí¡û:ñ‚¾yå œßl›íêyÕßmÝ € ÊËa^¤³®#Tþcɹ<04UØyÜìïÔö}ZÕuddª®ƒçhxx4EmY~IÒàË-~`ØðhÑñ•së…§ŸÆÇ_{Íúýí–..tqpXýýý¯ô …pöìY¼üòËÖö=6’퀗GH¯:U^´;Sýhyv@¢Êh7ÒµˆˆJ.».¸páz{{+¸rLä: e†'ŠFz†jýÙp÷sÄÝh‹)¾¬`qfÖUeR2¼óÝÿÇF?umÑ.4à ܶÇÕ‹ ZÙVN¥ËÂåAŒÆ°83M¶רë;çº$.ÄvxxýG¶Óvöùµ}%“Á­¾n@`Ç_üïá­®Þº.A³w1;8„JÒj¸¡éøaD´úÈÎa=õo_DóñnôýåßCN¦lç#ÍõÕä¥`4÷SSSÄá¨hêðûn+TT–HË9 ¤,9ˆï&醄”±}|y_žï°þøÄSø_ò:æRÖ'(º”\&>UÀƒ'àG¤mOEõ9e.x}üƇ¦ó ·íÝ~²Ão nzçW¶Ê¿§çXw¼^øÀÎÞÄ(é4f††éhÛ´¾Š)U–¡¤38Èéô²sCrØéÀöb(ÔÒ¸fhð×f?­í 3Cø…™¡áuqX-šó@¨;6@V‡(…ì8v;ì`z¿†Ãùåé?×Ë p0¯"¸1¨ºŽ¨$­ƒ–œt  Õ‚!÷BÍr}@PâÂrœ®Þ‹§‚­x'1f©ª².ƒ8WÛðú¦ûV ·ãQœˆÔ›+#U ØÁú>TCG"£X/ÿm“’³ç®*o±üE†`¼O‡ŒÖ[AŒI‰åëÔ—ZÃ7FíÝWe»ž¶)ôdÁ&ü8ôÌ4 q4 =(‹€²J¨ Ãæà´î¥bèýõ%|)Úƒ35µÚæõcvØnmxx8d¨’U’GpgPËjàáÂ?ݰÞî| (oËÎw@¢ò;iTp‰ˆ*]LÍIè W¡‹÷,§=þ<Ξ=‹öööЬÚ“ÿ¹·¡&²÷)†N:Qö^ yÇVºîO>K*hSEÇ& kš«ÊôÎwþ JÆZ\YswºŸ«œ~^×Ñ–ç*çšÇ¸w!W]Óœ_€[v{ÝÚ]:žyz pçÇ?AbzÆR>9Ø!ØÐ°á·J„Âö®rmhCóc‡É…¬„j?uŸùÛ?Çÿä¯ ‚V/t¼åØÖ\ t©Û–îQ\<À‚,¡–H •‰Mƒ¨ÊÐu^Žƒ—å*þxãrf9¨ XІŸç!0î9Å%$ U‚geõ¹ Ó\J´ <ð ža ;ps^ ¨ÄÇñøãÃ×.Û£åùû Xºb}‚n/ ìÀp¥+J&=$çIšLó¸`¬?>1ÃÄ­A듦"¹;pÅÊ÷J:©ÛCÖE¬m ))BN¥³Óé²|±^ѱ‰eÀ!çàPI µ4¡¦µyƒkÑFårÿÜ8,ßlsðá}ચHc•HZÊzð3\xЋ5o̼ÍÓ tÀ¢[ŸŸQOg‡ }“¢àe9Èš y‹}®ww ) AV€° dð3<ü ¿í±å?“uç$ì§Ê·z;—Ã6ú¥éýØ(‡ò*ì±õ°Ãº4‹’Œˆ×j+˜|;ØayEsWæÍÿ®å üw·Ç,·ä—‡MöÔÜMÄѬ޾Œ”Í>\dØìÃÛSFÑȨòš<•vÈ©VA{«SÃ"Ô¥à3µû0%‰xyêlkèhOÀG §Ça¤ÇC…!Í ÊÓhb„pr>§)øÆ½+xk¡_>~ýÆÚú± ;Ø"b©¬ÛáÆ"AOþü)c›ß²¶uyؤL5{MfY ªj²ìÀÔ&}zé»Éh²º<Ñ ѤÝ0P6BIJÖ­ŠÚÌÂìyeŸ ¯Ì3Ο„ð¾M§t”®¸µ ú*±] ðà¢{k·,*gðP&R SâÊË QQPï÷»*ðßñþ)¥!­{ñ)Aƒ¨(ðsj½î°~Ò ‹¸ ¸]Ц!£ªð°öúZkUÃÑùÂ&*^¼\qŸÃu 8Óyoܽm9­¡«gïÙ«—GJ3 ¦ÙÒœ#TY†¦ä¿yœ2__ž€¡¦†¼7æ«u÷í÷l•¿Xîœ×=A MÃâÔ4³sðUWÁª¶}ã…È©4”t¦bà†Jvo Ž ÖµºÌ »¶/Ðaç¤IÖ‹zK×µ¢æ£I< –¦‘ÑÔUÖ[‹ZrSàhÆÞ"úȺBp4Y× úò~Š^vwàhš…—aÍO9ÀŽå—Û® `ÊD¿t+ì`ùøíÃ’¦AÒ´U€ÿæn  dt ^šµV®Õ¿Õfq%0ôl¸ -|ãrÂR5ˆš‚‹“7ñ¥ÚGÖº<¬ÓX*‰ˆàAh«çMfa»*v5ŠªçIgFȨXY…g‹’¿©ï¨ÍÏyl»„öxwçVƒsMbJñæÂ}ûm‘zÍ€öï¼-Г÷`HÓYð!3 Ð(ßž,¡9ëRûvt½ï¤ðå…“èìñfûs1`‡m¶Qu7&¢h¬ö¢³¾ ì–«À™ïá€S‹)Kå`˜%à¡Ä°C<%o ;¬þOÉøXðìF—E5Àó”ýòžP€‡Âò*ù¹<ÄÀÔ=]Ì©[QE Ëi022Rð*Ï¡P§NÂ¥KÖ$»;G§ü»ôFdÕ)ñüÅk¶²`jO€D£—£ä@DDTØ ¤ûAW=}ñ¦åœ.]º„ . ··×ÕG‹Ywc³Éû]C‡!ÏÁP¤m¼GÝú;±îOž"•G´±?igf]U¦ÙáQÜúéeËéžzñw]˜U †¿&äúrÖïoÇćùƒdy>ïB“¥–’É 1—k·ÚÄK<ž¼?üµ5à};/6rmñÉi[iþÆ'Á®³éx7øºå|:žyÍGÓÓ˜¼þ&®C•¤²ûn(/›êð™¿ý üðO¾fzP¢ÁU5æ6^¿Utóá÷Wb›àÁ%šššêollŒ¨v[ÙªB¨L”Þ$XRÕŠÖë%* j]ÄTºËÃBZDsÐÞ),äñ¢+\‡ù”É‘-0 x†EÈã-ìÓçºáêÄCÌ¥¬OVtÕzl¨©ž` ¢úH&a.èszÐ<ðÏc3÷Š‘kÖ'Lîî°^†¦A\ˆ.¯NÏy½`y~Π¼Ï ]Ó §VV-’’"tMƒ’®œ•Œr+öçÚ+Á½!çÐP þÚ„Z›Ø`BËc“eãäÁxCàkö€ ¡vJº à¡Ó»È·˜ž²ËC7 h†ciÓõóY ”cs\zÉ!È®ÞlÀ@›§!Ά¢À,;G”2JÆ(^žv°–_É`‹iÞ‡¤iHÈò'Ã|itÃ~ê_ëòð•½Oãïþ‹ål¾7/ÌDãzÛã¾½ʼnp=XšÞdÃ^½›n{°dV?{qF@ver/ÇdW¦w ìûÜÚCaê-ÉÌJ|¹í $5oÇÇ ¹ÙzÈÞ„€®ê‚¡µÁHÜ¡Ä]‚‘zŠ«¸Ú,á î¥bèè×â=8óTÐÍŸóÜfj1XZÆ£-5x¸é·ú»ÉwU¾%—]ÏÛ×s«Ö³, IÖ·Î;ß¾‘/Íæ}nu?ÛnÉ´ŠÚ ³áwEÖÁ ´ýòRUþ•Õûí»<|&| M,*äÂ@`Jˆ4o9`ëÕW_u$àñôéÓ–‡©Å4¦q+4àu¡»rÎݲïî@y›Ay[È ]YˆDDDÎJ¶ù· Ûàüùó8{ölÁÐc1eÇ…‚ò®z~nè0”8 %:éRD›´”õ…¿ûŸ<ÛŸÖ]½8j_%¶'Ü¥~®C¶d‰´L¹Ü8ï‡*4 èúÖ¿¹Iº¡#šN!ì«Ì¥Å’ ˜(ò.v“ñq<þøÄÇðµË?*Éþ쯸>²:0~+M Þƒ*›íò¹`¬w¯¹6%cýºÖt¸«(îÀùÜ¿Š…’NCI§‘ŽÇ+þº’rÿrÐG¹*Ô’…üáÔè@¨µÉ•Ûú€¸°ºˆóѲ](š¨Þ·é*D¥•&Y_á­'X¬‡ä6‚Ás×SŠM1¥ŸëS ížj°”ɽ#î6W˜§,ߎo·a£ðcµY®¤¢ µÙÂÔöNêf7ÆfÝrZçòðņœð LÈÖ%/N}€/×ÏB[”'£i¸½Å£¡ðöõï4ì@Ùp‡Yõÿå…/W®u $2 ª||Qòßò»<°CN=<ú¥®ÙôËmO wèg¸—.`µ5CƒëêÙz@1¨ÐQÒ<ôä½,ô ,Êb68WMdÿïÔÜPSð{WpW<€/}ì Aݺ)um漴ɶEÃÕÑ9tÖW¡µÆ¿0;äþ†ƒLÅSÛ÷u ð ÙûiާT¾¾GaPXëî°Íþ$es÷ u+'+N]p“”l9m»<è2´è˜ð㦧hním.Z Úrj…çžžËiúÎJWÑ+˜e\¸°´Ò·l»;Dž%7s®ˆˆˆŠ;()¾LÃo@›þ±å=Åãqœ={ýýî]LÔðÀ×fkRMÀ]%ÝŠhë§&²Õý™ƒm2½SH/ºËIæÊ+?@*fíqsw|ìñŠhÎã);pƒÔ´¸ß^ŒÆ°83M–‹ºŸ`¤þÚÔŒ%#AßäÙº¦(ж‰[á¼(:ûÞŒ¦ip Ç‚áܵ0ÂȵËã5§®O}rÍÿ¼wÑÖVÈ÷GÂ04 o}ó¿–…›CÓc‡ÑüØ ÜÀ}äBT! wµá3ûçøÞ‹n+½²8¹iüí^‡‡Ñ©©©X%¶%\v ª¡#¡*²i!"×)È ˆo2)X5‚û:“²?/ÀÃVÞéW7 $$ AA¨Øþv¸®g:á»·‹{qæ9×Ù*MU¡)ùÏ>4g ¶&/ÏyÖöÇÑ÷¯Û*±ÜàóV¥9׆à@\̵}lXý¹\E e7Š&·y®™Ùux0œœr(x{ÔÈû7l+;±­cÛEÚ§Qúc…Ýzvv(´,Ž—-¿·å%ªö`‡ìý˜Ùc4¶?–€ $W®#¿ÿ_á3þ“åcy36Œs GÐè§·-Ï\&ƒ±T­¾Àæõ_Ìÿ­¾Û¦ÈšÃØ.gÊ/©: ÃȺ<!ÿ ß™„@¡ ‡Ö£4Æ®ëËI >áô C^ìåÙþÅ1%„ÁaèâéqÀP³i”oŒô$`8„ô½©!Üýq _{òI:7©+«°•g›-~¿;³ˆXZÆ¡¦ê•@k °( ÊË!žb‘VÕ-ËPãàX€8ÎÂ~L÷·<}΂“„¶™[µê¼hvÈég\ŒÅÛ@x(Ã@`ÊÛXFGGqñâEœ;w® }Û¦â)@Æî“¾Ò¿l»;ð5`jO’›9׉@DDD¥”läèÑ«0dëÏËÐÛÛ‹ .¸²f¬:GÅø`¤Ç`hd¡H"£O¼o+]÷s§HåmÐÂØ„«Ê3rm·-¥ñ…ªqò…ÏVD{Ð ƒºŽ6Ð C:§S·±š†äüÄh¼è Ãš¶¤ið>/x_eÇjØuwh:ò(‚ +ÏgÓñ8†/ÿÂÚü‰¦!ÎÍC,¢›F!ZíÞƒˆ*[á®6œþ÷ˆ¾¿úo¶Ò¯wy  ´æZ‡‡¾JmG 㾎ö7,¡Èx(1n³4(ª¼,Y×À.?CÓöV€-‘¢éÁ¥k+K 9SÑÀ|®û.ÞCJQж–GKkƒF—àÜ!‹ùmÁ2‰$æ,fêiõ±‰ÑfïZ¿™;ÜOU°(õR,×¢múb:ñ[e 8äà†œcCMKÖÁhsÍ CN§›DtlJ:SÖ`Ãj-»9Ôì-Hc»LZÊúù¥÷#Àð€æä<±|ah‚æŽÇî±™ |ßM°ƒ•ú¶u,öI;n N:EضH£Á€¨(Žäev².É•kÊóµx,ЈkÉ)ËÍóõ‡¿Ä…ÚSYˆb$ñ›ÿ8 þ.0'/UÓÍ÷ÅÊjP2ªo®N\;äÔ^çÇTËÔ±•g…9èá_ß~Óra«é‰AÐ@^èhÿ^žâ=Ò|Öñ!õ”P PŒŒsN]‹³è}ëøZâ£hüSrØ!÷y.™AÿC‡šªðp[ç¿M¾-aæ“¢¢´æ7š¦PËßQ–£ ª†³°CžôžEFQóîOÉë~×ì”w³òÔ€› °tжëò`¨ èñÛ «•罇¿˜{ËrºóçÏãìÙ³…B¶÷ÝÞÞŽêêjÄ-¸NN/¦³ÁÿEV(è²ç KïXcIç_¶çî@רÁ="ÑJÆ ¶ù³PF^¶•ü¥—^BOOOÁà£ÓzõÕWí%¤;Y¸§½c9Mݾ6Ôu@K¢µZœ™-ix>Å&§1ðú,§{êÅßç)Gr;8+UVœ_@*ƒ®i¤BŠ »î Ï£ãÔZס[?xݲCƒ¡ë®ª@cÍKpCÓñÃ6Õ‘N² Õõü³¹ô>F.¿o9íz—ÈJ$×z;.(š½~ÒB.OoœXï‚cpetS!k*b™4j½•g}ULÀ-òq<þèÄÇð_~Y¼¹AãÒº;0|ñ¶L2ÀËÔÐ=KyFÚ÷äŸè¬ ¬®6±ÜÝ]Å;ow‡’hfhc7134ŒØødY•À &®«K0CÎ¥!÷77TäM\ ®ª l€<ˆq³tÕzÿëô.oÇÞO—7ì`ø,’ïxìÛn‚`§^\;P6޽Ì`P€¸©=¶ù¼]ƒªë`Úzÿ[ÿý:—‡?o} Ÿ»ý}Ë]i@œAÿd=¶‡‡U]ÇíÅ(N„ë¬÷¹Â@ÖÙpótÎòªÃË¡¸°Ãv¿mó™¥hÚÀ cÖœ·¿Öñ4z‡~Q+ìù€ž¥Š ùO3Œªª†4=y/ =H Å‚ò6Á‡‚“î¥bø×ïýäSè|ÒS|ØaÓm $%ýçÑY_ÆjoþüÖ•¦(ÔUyòóH+4]‡O`!p̦Ûó< uõËçbÂKˉeé5ßé†a¯-6ûîQ øuá.zâÃò¸ (o3Œ´5ÐcttçÏŸ/x…瞞˫1'3 ØE(ÊÒå‚.|ï†-w0°uÏ’›¹ˆˆˆÜ5(éªGAû; ‹öéííEOO-·¦béâÅ‹ÖçAâ¤LdaTª ²õ•¬»Ÿ#s0¢µReɹW•éÊ+?€’±ö|åØo~ ¡¦†Šh“šÖæŠ7vüÖ5“Ab. :WvÝZ?òx««—ÿ?{gÑÊîøù€í§ŽÀhƒNÿ‡?Âÿu¶r2e9íj—ZÝ|˜ý•Ú~xp‘¦¦¦b޹­lQ™¬XPâË@R³/ Yš‚—#ÎnTBÊÀÃrðUXû(šEÓÊ @±£Í{p¸®ÎN;žw¤m<ÁÊZ¥[SU¨RþÕ'¦Í öƒåó»#¬ÞfäÚ€å²{‚Ô´ïa6qx(ŽÄùhr¸ž…Ê%èÀ [+çȰÕßÝ 6¨¨E“Û¸r®ØB– ü#QÖº;TìP•oýv%„œh'»ý`9áL¹ pc6¬beX†-5´jƒ› Å¼Ö¹<üvø >âoÀ¯Eë÷?ßœ|ß®û’·­›¤"ãn"ŽÎ`õæårì¬ZIÞ‰ñgŸ²ªÿ|¾ºnLÂË÷®>" )Ì©20ͯº–ÕàÂO8=éqè† :hB§„0.=5 #=*Œô$(® àj`d¦©6QSÐÛ ”ÓYè7ì÷ÏÕß›&VgTÍÀíÉ2ªŠöHмãu9r¨ IDATĪï9–·Øì3ÇÓ@Z³?-ÂÀ³ D¨ù](º6_¿¥ 8tiÀU.-Í8 c`nÞbŸž€!Í"e97£ƒ‡¡Y€ì ÏgÏžÅéÓ§mïÛŽCÄÝ™EôtÖE|üðºèÞl©Æ’2.¼rÓVLäY€!‹s”Vr ""r÷ d÷|òÐß6V/Çã8}ú4úúú\=ŒŒŒà5Ð*%„I·#2?BÅû¶Òíÿè RyDkÏ¡“S®ZõþÖO/#>iíÙ`ݾ6øØãÑ¡¦Fx«‚¤c ôbɹH¢H*£šµéîÀaßÓO-ÿ_ÍdpçÇ?)›ãnzìð2äî"ÎI…JÓ§ç!'ÓÈÄÞï…7B¨­©l‹úðÔŸ}}õß,§U“³0ô hŒ–rë!Ƨ¦¦ð@T2õÁ…ÀCZS‘ÖTxÒeÜ®ˆÏe鯇¡iÐå²|«iUY³‚›¢k°ã’%° ü?W¼`äù”6_ap€¨Èí‚ctâ)ôþËÿëx¾ ]û+®®d1ÿ$/69mÊ"§FõDÑ4¨%—1³üö|äHq'b‚@.\):6ñë·06p³l\êt¸aIhØâA ‡²––ŠZNÓésê<`#èÚmýŸ¢á¼ùÇÎq9 ;˜mÇÝ\;ÚNº1l»‹ã…rpÜåVò×t«W"·;€Ê:%,Ê2ª×Ì1 {u¼Îåán}¿wÇz`ȽLoŒ?À™`Àlžý~LL"Ä ˆØ])­È°Ãæßè¾Í6 h†f=Àâ”»C°Cîó¡pïÊ3P%ˆ­ôG¡‡Ì4t5 :t L̇ht ††‘¼Ca(‹€šåk[! ~ö¡)øâõãIÄ™ç7‡V×[>øÀ"ì°z›‘¹$’’ŠCMÕY—ƒ­Ú޲?¦E)kG“ëšIØ@Ö±ÆäqȪ¯ÀXÛŸ•ñCèÔ€Wžáõ=Š?øÙÏ-÷=6¦á¹²œßÒU‡ ÍýÐeËiÏž=[P°cOO­E¤)€+^`èÖÎJ%–´rJïýÖ»ˆ‹ÖÛˆ¸;”Rr ""*ŸAIñ5`>uâ¶ÒÇãqœ;w}}}¶F'uþüy{s ß>Ò‰ÌÏ÷ãXN³ÿ‰ã¨ª'«N­šÞ‹)¤®)Ïìð(nýÔÚJñœGÀS/þNE´‡¯&„@¤–tL›£1,ÎÌA“eR%”Õ1›SÓÑ£`W=ŸðÞdâq×g 1‚æã‡Ñ~êÚO' ïd1è½±eÈaýo²˜€²†ºžwþù˜¼ö¡µ;3]…Ÿ_³ÇÍ}•Ü?IÔŒûäZº&¡*x(Ñ  ìÂvZ”2ˆKÎ9‘HªIMcQÊ âõű@7ţD4‚ ©ÊS$UÝ}®ÎÀ™ÎCxãîmÇòôüˆ´í©¸º22LYpwðü¦¬/W»'LÜ´wC×}°¸1âðP¢c¸ÿîû¿~ âBÔÕe µ4­uohÝ]6Øh0'Šf—‡_ Ê\†n}NÔé­Œ"Î ËhÊá¼`)e;PÖ}%ÁfÜvv°˜ÆÉ}¬ú^5ôµuEÙ¯IÓ k:x†¶Ÿ œÄÀrÓ½>„oU]Ã[‹-çoN]ÅÓ‘Ï Ð°YÖ¶çíx'¸zxV߃S…ôEÙñê4ì`&à4óúGÁ@6Ð÷P8„úBöûh‘ U„í]uë7w âªAÕ<]|#5šu{H‚j0äG®W߸{À:èÁªÓ‚ØƶÛÌ%3è áÑ=5ðpŒ¹¾FYûNd$mûþ±m>†¹í–Ä0”©í`™+è|‘§ÎŽiÀmf¹)Î:„óW®b4a-E‡ÁèOty.€@‡ŽA_¸b9ÛVxvJÁ% Ù(Ù¾:2•ÄËoÙÊ‚¸;õˆ¨¬%yzütÑÞ³ÜåyÀNA}}}xùå—mL~xPÞfÒ%‰Ì\y†j=H}ÿGO’Ê#Z£ØÄ”kÊ¢d2¸òŠuèí©/ü.8»‹š¸H‚ßÚVr°*UVŠÅœ[p•SÉnÑìð(fïZŸóqö=õÑåÿ§ãqÉ2ËëW%× @VUË_VÛ ‚Cã…*,ŸwÑc<ÞWH¢Ù€\–¢ñŸÚN㙾c¹;ˆš‚W&‡p®¦ àõ­ëŸÊÂ7¢ó8©ßº¬… ¶ò1Ñw(ó_5n4݇÷áìûñzñy0×´47/ô- =6ºúP\µù¹–/ ¾úâ@—`H -€òµÂH9rÊ[=[@ëÛÃ*ì`’IÊ ®ÞŸEO[·±½ €@ðÐYà¡PØÁ$Â倇|çR HËÖ‡Îqmr<[øÿ>^É÷Ü¡ƒøê•«'‰2ôä}ÐU‡ÊrŽË„ŽA ØryÈA¯¾ú*NŸ>m)­àÈþ‡óè9RÜU8=¼ €‡ôÊ©âܾd³a‰»ƒó"Qe J®ý¤ÛÿÐì=“@OO^}õՒñX ½½½¶ÒÒ~âî@daªŸ¸c+Ýþ'OÊ#Z–Aɸçýç•W~€TÌÚêîžzuå„Ìð<Âm­¤SZ’É 1·€T4F*ce'6é† .ÿøò/ J’+Ž)|`/ºží§Ž#ØD\‘Š!YLcþÎȲ{ÃnPóc‡ÑôØaË.º”%ÇÝ|h}•Ünxp™¦¦¦FG¸nö•3¤ˆÜûA/߲˚Šù”ˆ:e9:ü_òq<ÎtÆ÷?¼îH~&ùMÓEœ§ò_û,¸;@ãsõİìòu|rÚrÙëö·µÞŽ#'o³s%'‡û¿zß•C¨¥i àà×TÞ n:ØØ$Äù(Ä…(¢cPÒ™å¿DιœŒ·†¸8TúÜT±>.ö{—‚¸ôB¢Ï+vðÐ,B¬‰U‹;À©<—ò- œ`8œŸÉzp2Ox@à*Ïêõšd$ • ÝÁ6rÈu¢ŽóRt ª¡/¹¤W•ÄùåóÒ‰@#>êÀ›1ë@ãË3àLm÷ÒëlÝ~“ª‚1ö@0Ù)“ã„Byåé;E‚²Ï: g÷á0ìû|(»™¨ÍK/ÀÖC]Ÿ@ï Ѓ¡A]èåm1*àªÁÔ‡ž¼CšÎ‚©±,ô ÍZá/îÖ@ž<íffš…Öm¯êúÌ£³±ÕÞÍ÷iv•ux ÕF¿´;YÓp…ßíüÿ ^Y¡÷èQëÀ=>P¶ÀhÞ¶Ë…>þñãOÿôOqþüyÓ ƒíÀÈ …ŠŽêÖ¨ÙþÙ70‰KöV‚e›ÏwGD ""¢ ”Œ\Û9(Ãg;‹ÑÑQÛðc!êííÅÀ€½À?ºúé¦DæG´8b9Íþ'ŽCðûHåe§÷š†Å™9×”gâÖLÜ´”¦º©ÇžÿTE´G¤­4ÃŽiBb4†T4IIe¸ -F¯Y_b8m?¾üÿÄÌ &¯°£ÇB ‡ö›éy, CWUÓi¼áêŠ8ö#Ÿ?cxZйöýûÔÔT_%÷W]ãNõÃ…ÀCBU¶~aND´Ó'3š‚ª—ïÓú”"c:™@?PNša€©ÇŠ|ʹ<Ì¥ »yk8°,ÏïÌø)¢‹A&™¿^Æo˜Ÿwî^~ùe{×HO3(>Bº,‘¹‘-ÏÙšïÿ(™‡­(9¿M–]Q%“Á•W~`) çpò…ÏVD[„šÁyÈû¼í¤k’ó £q×ô["àîÛïÙJ×Ð}¾p-Òñ8¾wß¿¶#å4FÐ~ê8ŽüÞ9”Hóƒ£HNÏ[Nç ‡*âøÛOG 1‚ä”5à…êÖCºTé}–îT€ßrcÁµ¼@ZˆÈuªžzõì±r`häýù7*†»ÃNÀN´³]Q&óuvð @ÐДõýä‚´) ²ÐCF2‘f«ß,ŒÊFÛx†gòÊ}ŸÑTÀÞ§«d`‘Ôlàð_^ÂË3ÖWwgÐ?EOWÀT½ßˆ.àɺ°4m®ÛÙ2À2$Õâøp#ìà »CN!/Ö*?ÆE e#ôÐÈû—œ€ŒÌ4t5 ºª`Ì¿€¦„0hö1‹7a¨" iëåi‚‘)üäw¯ Óÿèü¸°±=¬¸ Ø„Vÿÿöd ±´„CÍ¡­ÓX€Àëe ¦T†™²Ù€ÖýÎ1d-ÿf6©†¥ÌÕµÕóÄ1 øÉJßî=vÔ2ðd]˜ÈÓ.ž‘m??`>uüµ‚öÇñÒK/ᥗ^±cÇpöìYôôô ½½}£ÃÈÈúûûÉMÆz©–ÆÇÅ7‡0:´÷|ªù,©Ëœ“ïÒ"‘óFùŠmøŒä=èba‹ä|õ«_E__.^¼XðÀ‹/¾ˆï~÷»¶ÓÓÁƒ¤û™éé [éö?y‚TQöQÓœ[pMyÞùÎ?AÉXsÄì~îBM eߜǃ@¤–tÊ-¤d2HÌ- ‘ÊpaÛŒ\³çjUÕÜŒëßû>f‡J^n>àCû©ãèzþY4?v˜4d‰$‹iÌß,Zµ¬ÞÛÖS9ñNŸïûûæï‰X.ë¬ø‡¨$úÆêskÁ¢r†D®”ŸãÁÒ4$U…¬kÐ5]7åÁÓ48†—ãàe¹’‡¬©˜N&öùÁ{¼²Ñ³mûñ/CâAÜÞ*ô¡¦•wÓ,‹©¼ÛL Þ3?iá9DÚö˜Ú–¢iPK+Ûqx(¶»0%>¿¸Y3Cøÿîû¸ÿ«÷w¼,•8DÇ&›„¸ÅÌÐ0Vν`}5 …©"€®”rœUÖ‹sÍäw»p;ì§Ê·z;ÃáüL–·Ðza :pŒ½}lvÜÕþlš­^¸Q6ê²Z.;Dzu¹XŠMQ›ÞÇÚÝ¿n¬sÞ³\æUÁÖ‘ 0å[£,þMÓq¼Æ´bÝåîëc¿Ä?Ö~&›gžzW ý s8QWŸ¿Ü%€¬]œÀLÚaIÕàØÂŽ¡°CîsgM5b’Œ¤¬d¡ ÀÂJù ï,ô ŠÐ¢×@‚ÂæO½Œªúôä=Ò4 U4 ”·Ff0 [©÷F.p§Ðù´g¥~Ì”ƒÛPÀT<ûÂêPKÈ\>y~§h ~?›uyØ6Má°¼DYÍ[>·á;^ ‡(u:0€dö‡žHÇ"a ÌY[ ÍH®¬Ía(o èÐQè±ëŽì}``('|;ü\gé}t,)£÷¿¾kïž6°tõ£ä®î¹ä@DDe ŵŸƒ|ïï`d& ÊçÒ¥KèééAoo/z{{ 9³Jì•+Wð…/|ƒƒƒ¶ó ø0ˆ,IOX‡ »Ÿ{‚ßG*(;¯Ÿœ†®i®(ËÐÛïaö¾µ÷ëÍÝ]8ð±Ç+¢-BͤCn"1CrnJ†¼gv«Æoݱ *Ù…DoÿË%/oøÀ^ùü´Ÿ:>H®‡¥Ôâø ¢Ãc¶ÒúÂ!„Úš*ª>ºžÆðÀq®Žõê«ôþK€jjjª¿±±1 Úme[P$ì'MDäR )ÿÓÚjèÁÇ•g@tFUw•ÃüÇNâk—d+mížÖЬ“|Î ª,czÈ<ð`ÉÝaÉ=AŒÆŠÅ-—½¦µøt†ÛÝÓ09ÆýwßÇàÏ߆¸ݱrTàVDŽYo4ç]‚`|5¤RˆUOÐá•ÊÒÝÁ@„ó:w\%… óZ½ËA‡ Û®Ê×çª|ö÷³C•P@ÓÍågDpÊ!"o¹Vòòs«­¿©ÂÛFÓ 0 Uì à5@΂+G|u8Wßÿ¥åQ>­ˆ¸øð&ÎUu‚–·|_ýÞœ¿ïÀ¥Cƒ¾x ”o/h›ùt4ºª z¦Fb0Té Pþ6©ñ‚ QSÐûëKøGÿ¿Bà1Ý|ߤ¶8ÏØ„ršŠ§¡êµTƒeh }góï|>IƒªE…@>‹€Ä!™Q¶,_`ð²k¾£jÅáÁìyÁʸîÒk+0ß¹C‡ðgo½m­ëª éqPÞ–{tyFz†4ëÊ™åÝÙÅ¢æ¿<žvBÒRóQÀ…ïß@\”íµaçÉX‘Æ)"9oT´ozú@+,è1ã«_ý*.\¸PøL&188ˆo~ó›ø‡ø‡Â1ü1Ò¥‰¬ÍíåyË鈻QNª¬¸fµ|1íŸ^²”†ó8ùÂg+¢-¿Ÿ€Hëúfr>ëæà ‡hkÝúÉe›í,—´œ]¿ù qsØ!骆Ù[ÃÈĶÒûÂ!„»Ú*®^‚Mu4Fœš3wÝ#ÀÃŽŠîU?€Sn+TT–HË•b’aè˜ðj¼¾µ«H–4Cßumv¸®m¡ŒÆ¬n‹h÷§©*Tiû#+îÐúˆù–ÏN0ã“Ó–Ëî à© µ~8¯gמßÄù(nü?ÁØõ›;æ4jiBë±GÐr´»lq~ű!6>IàEsÐB´#–!""K×Á”M˰:ç³xíZe¥‘÷o½‰#°ƒa=OÇ!†Þ®È°EU~À[€SG>¨€¢€€ˆ'óçG98VŠ;€—e‘VU¨ºî¸¡èx–¶>F6û­!<ÌŽOÍâ\ü»‡qÆòhy朩í@c;eêØÆÄ$B¼€ˆÇ“H(èaâ;†¦@Ó›¸qØu{0Q6YÓáÝ̲g’"~ö° EB¸1»ý¢E<:0¹öœðå¶'À耑z]‰®~ Ì?ò¦= 0Ø@v•zC…!Ž‚âÀžÉ:?دk z¯^Æÿ32ò÷‰"Á9Í%3èÕÐÓV võy2¹ŸuŸ«ª8,,ÈÛ_c(“}?ÏþêB0 ñÔÆýUûy„‚ü†4/ãüÜhu5à×Ìòáž;tÐ2ðúâm0%œ Æd[?eäbÁÁŽÅP2£Jñòg™œt/½¦™JâÂ÷oÚÊ‚©= :@–·*ÖØØ­E$""çÝ#Нßñ'‡ÿÖ‘y@|øêW¿Šßú­ßÂÙ³gqúôi´··o™&“É`nn—/_Æ÷¿ÿ}¼ñÆE±à²ÐþvPÞfÒʼn̟ D{÷µ­GºIågÜ‘_}凖Wˆ?ùÂgÁy*ãÝwUCéXZT2‡äÀu•¨4šµµh©Ä|8òù38òù3ÄÍa‡”šc~pºjo¡Ÿª–zÔt´Vlý´Ÿ:Žÿ÷›æú3ïÚ G§¦¦b•Þ— ðà^õÁ…À,Èjy´Q ””%dTaŸ–œ²Ý®ßéþþ÷w~f9Ýüè¨ò °|å¸bdÉ¼ÛŒßøÐt~¡¦x‚掙¥ñ2;Z«ü[³eˆÈ§Ö¾üþrÛè Ô㣿ræj¢,B›tõ# 8óƹë: #q†*ÂçA u €‚ ‡{b ß¼ò¾9ÔmãhRdØ!÷]2£ ÿÁBz`hÛ°° €…˜T×”É|,J@•ƒ(©ÙEí©¬³Ã2¼±.×Ï~žÈWöËî?$øýCñòí;–úˆ.ƒÁs%™gM ¾óß@¾û-WBÅ<ü€w‡VsKcÙÝáüÿqÍž»ãÓð)rF """2( ›x›…rzíµ×ðÚk¯ÚÚÚÐÞÞŽÓ§OTUE*•B2™Ä{g©©)LOO;wP4¦îã¤q‰¬Íëw,§ÙÿÄq²Š<@S®qwzû=ÌÞ·önýÀS£¹û`E´çñìêq©d2HÌ- ³˜ ne¨¡w~åÊr#8ñÅßF×óÏ’FÚAE‡Ç°8>c+-Ͳ¨ëº²ˆl>Þm x` ãÚx¯¾ÝПIô¬{Õà+®< Ê<•Pª®a:¹ˆ àA­—<øp³>ÒÔÇ#¥X{Ñ¨Ê Æn|ˆöÇŽUL]HÉíƒBb“ÓÈ$ÍŽ4°¶â+ðËû±ªP VüÏ•¯Ò%§Ó~û îüüH۴ų«rwq €ƒ3¢W ÍfÁÎ šóÊ!r ˜2ɪ$Ðaíñ„X¡ðc3ø¾°ƒÍz)i¾… „‚æ`‡BÜVËç¶›SR%€Ì‹ehø9¢¢X¨¯m®ƒ›9ÚrªO÷³NhšÅ™š¼íÀ›1ëNSâ Þz0§E&ÿÉO5t܈Σ§6–^àœÇiÁ¦“æã%ç`À±+Àƒ“ùo±²™ƒ¢©àìr;k«ËHHæúsµ ´e€‡ ¯$:Þ‡Ãá룿‚¨9°»¡A]åmè0ù`ý ªB_ÏBÒ,(¡¶`èáÍÙô¼S‡3Ÿn<Û´¹™óŒ‡ˆoÁCƒÉ­¼_,Øp\Æøåá|vß>ËÀtº8 ÚßgUÚ`LНu7ôPq[²F¦“xùÇC¶²`"Ï‚âkwiȈˆˆ JGçE‚rÅèè(.]ºT’ãaê>Ð$‚ÈÊœ^‚!Ï[N¶ÿ£'IݧÝáî Fc¸õSkçZ_¨ÝŸ¬œ æ@d÷Ý#隆T41ƒ’!÷óå*1ÃÄ­AW•©é±Ã8òù3h?uœ4ÐJÍȘ½u²˜¶w^l£¦£4[ù º6?vØÔv¼»2îÛ ýš.ÕÔÔT_cc£+˶ H FÇDD¥WBÊ@Ñ4Ôù›®¹BŸ;|ß¹~Õrºñ¢õÑÃáò Ê24e{´©Á{æ'+<‡Æ.‹ÀÃR=Z]…‚uá¢×µ>°¤B¤)*$Q„8·€‰›wpÿÝ«¦Ü>œRý´íFëÑGà—×Jýâ|3Cû~“îóW IDATÅxC ´$P‘{·i›+2éùæx• ;@„óvl;;À©ò­ÞÎp8?“å¥ hGŠÂÕMVn«€Ï:—W)`äÏËÏqÐ ™œ­0e¿Í8š¶Ð†&óKÀ|6ø£]¨Æ—šŽã­ÄC[ê_ÿ%þ±ö34™ë3IEÁÝÅ8…jŠ;lÓîØ^ñ®«Å}4FœšÛv;ŽãÜ|}»¡­ðàn]pÊm…ŠÊi"¢RFU0+&Ñ’Êp©Î8ŒŸÞÄDbÑRºtyp²È·’¿*˘2<4X„(šEÓ¶Ü<Áf††Ÿ$'Ð-ÄxCÙ¿¾,Ä®û?Q%hÿR¿Þ^• ;›8<ìì`¥ü”å[½]ÀÔºz ( ¶ªô°u“ (À0ìÕc¾}8 ;˜Lä8¨ºÕÐm·MaÅ aÛ4†µ újˆò€NÁC³èôÖà\ýQ|kò}Ë£HÔ|s¤_®~ 𩦎s*B€çкþd`M7 -õ-ž][—~EBRìå¿K`PKÑèi ãêÄªÕ y8˜†=€´R¯¾|»ûÓè½ó3ܳ ®¯iU„}t ”§Á\"š]Õ }ñ i†š*z5_¿sêžAàˆ±i]mÚ¦”É>NÁ2T°Æéauÿ¶"P…šZѪ¦ç/·Õß-žË}~,GÛ?/X-{‡Ü\ z?wø^¸n©Øº8 Ï96ŸÚ™i]ö|Hy[ tÿ{Èw¿#=±ãź7»hÒAÍž<|‰Yw è»>‰×Þµ• Û|`ÊëùIÙŽ2/"Ñî”…ªÜ¡:xL @'²~Þ0R÷-§ÜÿÄq~©B"׸; ½ýžåE»Ÿ{umӜǖç*º¿¥—‡Ìbº¦‘X!R2ܿڿãåèúÍgpü‹¿`S]EÕ¯,¦‘‰% «›_$Þï¾g,±ÑIÄØ‹?ÙM®ëéjË <¸Øáatjjjd7´Ü­~¸x€„ª Èr¤…ˆˆv@UÁ¢”A•@V®v«þÇ<‰¯]þ‘åtã;ò+¼9o=hdóá§CO ‚’æ@W(sÃ骃б=pUA÷Ä.^¿/5uuÚÖcÐNP¾,A# èÙW –¡mº. …H²n±ÏšØŸÉ¼h†‚?ÈØ?/Ø5z´µÀზè2 i”`æy„K焺´úüÁÿ êԛЦÞtã´³|µêqÛùï\³•Å×€©=Y¡wUr """ƒr§Ey›!úß ßû;™‰²)7<¦î㤉l7t ïþž$UIäw1íŸ^²”¦º©ÝÏU–kç­ÌØ9T®”Lã·îàæú ÊòŽ”øÐõ›ÏàÈï©8ÐæG‘œžßv›øƒITµÔ£¦£ÕeÖU ³·†‘‰[wo¡Yá®6øÂÕ»v\…»Ú0ryëŽ(Šøéß-íD€w«ÀŸº±` r†DD;¨„$àÁÅ:\×€Ãu øp֚À*+˜¼‡F‹ŽnR&‘„¡o¿|Þô yw‡PS@“nz\÷ÏÏáÉú†.…´ýR_4 c ìºa !)¨òðËß<b)ióü„x†q?ì°Ý9’>Ä22¦ÖÃñ]ià¡,¬}Æøåö'ЬÇ7F~åÜA^€6ÿèêG@qæ^Ú¬”EPB(À6ôð½É!<ý~ zÎvØRp vÈ))­ƒ¶J³ì°ü‘¦P]ÃCLªEÕìû¿Sªá²PR©`‡å È~Ù‰ -ÄhÂÚKE=qŒð´só™RKßèÒÃ6~Lõ£PÇ_…ž¼‡JÓòx)YXªæ¾ë“¸t}Ê^¹÷ü^…µˆˆˆÈ t£¸Ž?„6ùÏТW\_V;rÞÐÅ@·dÚzä0©V"׸;\}å‡P2’¥4'_ølåÝãU»*[³Ã£¹6€Ñk×w¬ Æ>ÿ,Ž|þ ø`e:¥æãya‡åóùø ‚Íõ`=;»ò¿,¦1{óž­J}áÂ]m»ÒÕaµ‚MÛ/Hãbw g¾+D€w˵1*KhóI íT]ƒ¬iÙÀÊÏñ»¾Ît¶ <Àèµ²äTzÛßçF"“4Òò¨õ‡~ ›Þˆ¶ Ú(MQ!.D‘Y\ Ú˜½7‚¡Kïtà¼ì{â8ö=y5­ÍeUg3Cøÿîû»r …(š%@QÑd#¼ b=öŽÍìêüT!yíòí6l»T·^ðò…í‡r0“ã…rpÜÙICS@UU-•†²übÊÈ2ÎÚæpKS¨ñ»Éï2À¨Ðé©Á‰@>êÀ›±aˇ;­ˆ¸øð¾~àµmʱÒ.ª¡£a'êê·/« §EßlÒôµê^øçmŠ–ÃÛÛw ‡É^°,]¿­«Í“eeñXp™´]71—Zqy¨±E‘(–£Û;À¸^áòð•#‡-»<Åes×3·Þ­êRõªóŽ‚;ößAO_‡ûŒbûWzŽe ‚󋯼mþi­ìUš]w:pt sß9vÌØ ±ÔDDdPdiÅ­k¿ : Ðd: ­ò>ŸŽœø^h뿲ÞÑ‘ `"H»Ùž7ì8™ôž@Ï@?©æ.—[Ün¿û>Öï-ZÚçÔ."<q t :xÊ'SXüè>¼ŠB*½oåˆÇ™¯^ÆÔs|˹‚¥ô…D =£û×Gâ lÎ/CW­½³&®• ŒÔ¿G#Àƒ;D€+‹¥†††®8ç¶²mÊi "¢}–nµnÖ…‘Cèóù±Q°ö²BÌå;ÖåASUHùú7þVÜ„€}‡,—ƒáJ·7…¤õ‡=oOëa¾m £#Ú´ è[O`î­_#µ¼Ú²ã?²:tÔˆD÷Þû÷~û!ò›É5§1Þ0(†í ‚æЬZ‚¢ÉãÑsgÁÉñÐ`‡Ú÷¯;ŽÀ6ÒQ&ËßèÀhѹ:ô¼P-O–̬ÜC9pîò£·‚L­B 5·;è R/¯zåê žÒ‹Ó°W@F!m¿¨æùÒÿ¢ì·aŠ¢Ô¿a ¨ØÆê@XR4]Žœªàf*‰“áHsc½Z_4Y¿ÀA3 ˆªfmþ3™?ESðì]U‰jr9- pÄéfb ääJ „U€/wÀØÝñ˜/‚¿?ó%|ûî;¸š]sîjT\†&­ƒî9iÊí…®åa¨yò&(ߌ’­cÇ¥^Ÿ»‹ŽBzízkì°=ˆ f÷@5ó4j÷[ àyÑ>…‚†|N­]žƒ;À´VŒR𝶡áAœúÂEÒ€.”/ ˜ÉBÊ塈"©(E±<{ ‹^³ &9­É‹çqúk—1òq(ª%f’óKÈ,[wN\¬ËÅÀC:‹ÍtK;%÷ë \<¨†Ž¬ª Èr¤…ˆˆˆMѤüÉ©sø??øµåýâ·;xh´Ò¿˜Íacñ¾éümÖ'”VîK­Zÿ¸4çþÛ/CבO$+VP%·ßú5V?™kÉ1·ÝN<õüÑÎrXºö1îýöC,_›íì¹›@±_¤äØà ¨ˆ¨MâýÕfãê‰ ì`xëçfv•ÿn‚LÖ·õ2VÉ3(¹Øe£×ÚÎÐíÚ•ÇîÀ@QB^EUA^’wWL<Ïù"hU…Ÿçà5Ö:;l+*Ð)ŒñA,IY¼8tÿiù7¶ºÅK÷‹WÂÏ 0¤7î/[åˆ ó ù}öúy•¼y–MQ•‹ P%G‡jù÷xy (— ÊÄx´P×>ž­Žª3ßµëoªF¿ªñ7ËÐ8=Ø‹–סze¿þÄR~CÇ­ÿË–‡Y}‚ÒiRiäIúî‹û¿¿Ž{ï}Urþœ¿7‚ÓüEŒž;Þëí˜zêd7‡r°aÛ­ñEHç'"ÚG=M3éÇõ’ËÖ¾rä0^»yËZ_Ìßz:x€.ð›»,2^0½‚é}tgêÅò&ŒüÚu¤«0 ÖV5ÌIJçÇÀn½ÞZˆçðÚ?ݶ•3øtÇ?3"‘AY¯*¶Ô’Ã6àРÁì+ïØñ={ zòƒ¶ƒÓÿqu ƒÅ¹œl¸;ô ô¡ÿÈi†.Wfmcß˰2{ +³Ö•8õ…‹»`EÅLÖUç'åKpƒ”+@EèšFÏVj5ŽÅ¯byöVÅ‚”û¡ÀPÎ|í2N<{¼—ï*!@Ïè€)焾© °BûWþ—óEäâ ¨¢ŒÔÂJE|R¡DH™<"“#;ÐC`0ŠÈ‘±}wuÈÇØœ_†®ªUê>>à…0 Þïžø!»;x "Òœ’²„ _´Ñ~Üdòî]y„cˆÝU¹ìº<,ßø¤£^Hù4¥¶­±*ˈÏÝ5ßàñ£¶f ¶É'SÖZzÚsMc8w‚r±ˆÜz¢jÈ­'0÷Ö¯‘Z^uüxÇàôÇtÔ˜N.­`îWïâÞ{vDyiOŒ'¸õo€€ DD!£ëÎ'ÀZxIä$ì°ŸíAœoµt­€Ê·S@Àë\;Ù)›©rZØ®Ó@ÂÀ‚”Æ7çÿ ÿ¸YLb<øæÈ£ø«‡üJƒòöËEQîÕòbì^§€¿ä ‘ÉÕ9N ` €W¼Pd¶\2øÖèãøÆÝŸÚ J#qO|:Šé“AS°Ã¶nlnâBÿ†±°R{í¼YšFÄç±T~c‘—”’ÛC“°CÄÇ›;lÿà9œìÅLl£zš#E Å÷„ŠS}"<ŠWN=ƒoß}w )8&“nœ‚ž™Ý *“ÆhÖW[»šYÇÌïÓ˜õ;;4ê+ ö‰¥Š`dž{š‚Ê¿g˜ø`„Xˆ¢YÔ Éº¹sØÚæhð³ :˜½vP®¦i­xxrtÄ2ð —ÑÑoÔtÕö-åãÝúôLíwK±_@³<ÜYËà 8ÿ~Mð´©µ »¾úOö\n˜ÞGAñ½ä¹‡<š‘÷%}ê: K0 Ðe É0 åÁk°ËEO€ž(Ù[0Ä•–bƒ #@O!Dæ gs¶ÑwÇΜ"MÒårƒ»ƒ"ŠøÝë?²´Ohx§¾pñ@·&ËÈ'SðGÂm?¶”/@)ŠEJQ„"Šd°tÉ|°2;‡Ûï¾·ïL^<©ç.bòÒyÒ8[ŠCpd î¢ ¬‡ßØtµB¥ïÇ*`‡Š¹M’‘‹'9<Šè‰I¡ý_¶Hcc®öû=1…˜Î"³¼†žÑDŽŒ¹¢?àÁ="ÀƒË‹ÅRCCC‹\‡zoÊi "¢}RHpï*èM“*Ó…‘Cø>Ç¡ X öÙX¼1›k‹ë€*Ë`›¼9käî°±pªl¾†¦ŽÚ*×`‚BÒúC¡·]ÀƒËP ]G>‘|àAúÞo?lIPÿá?8ÓüEø£x¿v{7~öK¬Ýžwm Ü@DtÀՑó 0œu`v L–ßiw·Ãõê¶ÇДýãP6`‡F€ªYK_®2Øa&Ç“×ÿ/¤«.§5 }ÿ\É|Š~æ+÷QµëªƒçÌåUMa€t0 såjzÛÖqFóÀÝ `P˜ô„!êžžÄkk×mõ¸—îÿ¯„žA`D3ÝOT]ÇͦûúÀR´‰ò;ôP¾š=M¡ÇË#¢ªC× ÈÛ+°Q€ª0ŒúÇö° z¼(Šªs¼*óS'Àe ºŠôj|ßËÃ|˜zöó8óµË÷“ª"VØ? ¡aÙ<<¤lºR€¦9ƒg§\séEóÐè¶Ã† 7±Xl¦«Æ%™š:BW|Ým…R YUAåH µQ!Á ÖÅPÏÝTcûëƒãqùØCøÿ>¹fyß…®áä¥Ç[^ÆZ´¯Yiª )_¨›fùÆ'¦ó~ÛîÍ€írxp“äbÙØ:´2«¸Üz×~ü ˆÙœ£ÇšxtgŸ{¦ã@‡mðÃm E³%°Áë ¸ˆ¨Ã•ÛYÝÁ•ï÷]æ`0¼é´ å8œÐ)鄨uËЀ—·œVÀ@ep¿UØ ÌÙ¡ìP®·ÒŸâ¯æ~ƒ—ýKÎ{û!Õdý†½qϱ@¸H•C-†¶5(1/†x?bJ/ œÅ;™%Ü“–O#®äñÝ{3øVè3€ßüj¥9UÁÍT §{{<ÏÛj¤¡@ÁË•ºý`wÒ†¢¢AR5(ÚîsMSàÏ‚gëû ;4}[ù÷P d‹~s£ôàÑsy`Î ä+ëåÅCŸÁáQ|ûÎ;¶ÜDjÊ„Ûퟀ.'`¨yb¬=ˆÖ¡‡»ù~~m —ÇKÁòVç×F€Õt{¾¿¹œË}!¡ö¾V·íùžçið<]rI1[F³uÔNØF ànéCØãÁ¹¾(®nX Ú3ŠË<”u[C+¶þ–•±¾L,Sè܇u«Ž)àÕºtÞúJ°îpw ”Ý4˜¾…cƒ BgA‡Î–à‡â y†”°´‚>%Œ”àda„@dÞhýÑŠöœIˆÃCwË îëó‹XüÈZÌ©/\´ý»}§I×4¬Ï/"Ð׋@´4cÍÙOEèš]ÓvþVŠ"TEÙ÷¶'Ú?)¢ˆåÙ[XüðÖï-º¢LÑãã8óÕ˘zî"i +ðzÈ :<Ãù¼ŽöƒáX¨’ìàAÎ-¥Ï,¯í;ð@QXÖµaöou]ß'ÿ#t.`S ð@DÔFñ ‹°‹Ý@`Èœ°WtÜðXüª|¡i÷…V«˜ÎÔý>µGnÓ| ÓÄ#çìߨxJu•O¥HÇk Üz¢-®çN`ú¿y¡áŽªŸ{¿ý7~öKä7“®(E³`|a0ÞX_´'@:1‘KEÙ¸ºS¬3×pØ!Ì ¦ÓÚ>–­:5Z'ççt®S·[ɶÊC¡EíÔDú,·sŠõé¿6„¶õ·+¿Ã7GÅä¤I7+õÂ2° ;lïÖCz{` @P’< 1˜ô„0£®á[cŸÃ¼ó3[]ñ©yž…ÏÃZ?esÎkÉa8P k@åéO8,W^c§ƒøû³_ÂK÷Þû©eg¯dÅehb¬´2¬'ú`Uôœ‚‘ü=`¨€š`#XíÕûãò­CÀ´T»®œ†µoY;ß\NcÚà àåÌågöÍ^ÚýÙL¹Î¨ÀÝÝ÷7_9|Øð°„ÏÁÕª7 jUH¨å÷•uKѱ*[‡â‡¿±±?îp ""êÂAi¨€®À0T@WC)ýK€k·|lTð€»u †TûžŠòDÚuÕÈó IDATC*Ìm•.ZÆN?ßGš®‹åw‡ß½þ#Ké}áN}¡»¢uMC&¾ŽÜÆ&8A€§Ê;q)W¨HO\ˆöjrX™½…•Ù9W”‰ó 8üÔ£8óÕˈNMF: ê94„ÌòÄTvgͱ õÁ$÷N‰wwÌÞ•nkâÞ» ]‡˜Í×M›3Þ,Ï¡oòíò0[ý¯L[ï»]âð )*Ò«1¨ÒîJ¹õfß¼‚܆s–ÑÃMáÈãbøÔ‰¥´v{7~öKW8:0Þ0Ø@?ˆˆ:LŽŽ×;€@3MÔa½¾$ì`´>O3îvÎÝ H@’íÁâîsËÖ~tøabß9ðºsç±½Í 8„e ¨æØåd0ÿÞ4#E`!€0+`ˆ/­Õþõ3xmíº­®óÒÒoðJð2†&iKç+öx0äóYŸ3Z;4µmï8Úw‡ÀÛ}È)*–Ò¹Úé‡d Wf@ÛM`8|çØx=>‡WWn8ëö`hÐ3³ ø^У³ éQŒÊ?#7C̓ò ÀÐÖ,".ðóÛ÷qyz ú¹·v¨—vO:U×1soè[î>bÓÑÁzCgÀ@É¢‚ •>>9:Š¿þÝ–úƒ^\—ÉJ,™® Û²ƒÒ£ö²ëÔ…4ÕR?[ˆçð¿µ<Уmtw Ñ”Zqëöp+ Q—C‡±õ/Q E{@yGH=¹jÞ0ò –÷9úØÒ„],7¸;ÌþóÛ,„×H>ÿå®m3]Ó åóòyÒ‰Ló•Ù9¬ß[p äœWÀ¹?§ÿí3àIü'ðŽôÃ×®”ܲ9߃ ÍѬ{Þ8²¾"V©‘„ÐþÇqàÁ]"Q© X,¶044´ÀuˆÝ¦LV¥ "jËÅs v )wG½q ŽaHƒUÑŸœ:kx€å6ª$ƒ[ûJù ½ö }1›Cü¶ùóŽNŒÛv´à¼BSõ ô´' ¼ÚF»$‹H¯Ä+Úìþï¯ãÞ{Zz¨¨§¾#“˜ºô9=AôŽu ì\ZÁï߸ɾ‚4'€ ôï€DDD]®ƒ ;ì½þÒlûêËí°C+êÜ)€ÂïµwœVÃvò¡ @¦wŠ•REÓîÛšÉÇü#/Ú8~¶di‡ú¾À0€\Þ™<Íô%Vú$`݃IOJ/ œÅ;™%Ü­»få5/Ý{/‡.ÙRYo¦’ag·Ý:o;´ä8Ç¢=Pu±²Uò8>¯Óà–ÈUŽ—ç§0ÀK ïánÁYÇ?CÞ„–LöO‚òŽîÞ¿{G¡K J†¼ ÊÓC²¾bãÏ×pùÖpR®<÷}…vûœª• ‡é#½`ÚÚÜ`§l­„Z}ÿÕcÉR&OŽÚÌÓeÒ(O_§ÝÒ•vÓŠ oûhBW—èH•M…¶Ýú.º³£t@_&""ê²A¹/È%x¡ÌAÏÝ«~Ë@\ˆˆºwÞÐe²õ{̱3§Hsv±öÛÝ!ŸLaöŸß¶´ÏñÇ?‹þ#dx"¢Fckev ]Ez5îšr1<‡Þc‡pößý1Žþ›ÇHC`y¶`†cÁpÕoe=ìðàœÓâcžËjóSNM˜¯¼†ó…ñÊ©gðêÊ ¼¶ò±ÃS¹=7c {N‚bK.&Tp ÆæïC  ØÒßVúyf3sILŸôP  ÷9QÁÕ NŽ…›wt°»åÐ\ëDÖJ{D>ÜË/Žà­åkïhrw@3~P4Ðm\TÄ‰Û K}ßÞ)>C¶ÑuâÂÛe,æ³`«žèÐi—v”èËDDDgPnà †º1h€V‚L-׆ÊKYzúŒâJÀfŠ ‚òŽ€òM‚ö&MMDÔ%s½¸byßGÇ»Xnpwøàõ[Jï ‡pê‹IãUQj5ŽÅ¯bíÞ¢« ˆÇ™¯^Æä¥ ÄÍ¡KÄ ¿ØpAÐv‹ø0õìçq⹋ˆN¸®ždWÖ¡—ýfÆzxDŽ‚/ªº_r~ ™åÝÁ„P¡‰a¡Ö.ÒI³ ÏG!‘†\Ð,>à+ýë"G 7»; OÞu"ÀCçȵ4)K˜ðI 9,Ç#êów ìAy𭧇†0Š`1mí‡Y1—ÇÆâ}ôMjI¹TY±µ_1“­û}l¼G›|áWî& Ù<§ƒ&Cב\Z*í®²:;‡Ûoÿºb›]þƒó8ô™3u/ôá ‡\]/k·çñÑë?FjyµmǤ9¡ää>D "¢.E³0tóAPWse+Ìw¤»ƒ=Ø o:íǪ’®¨©uŠ®í¤¡AÁËpð±,¨vêRÐá´uÁbµ vE³W.y75¥ÚYÙsÙHÞT¿©’E¡ ™Œ°PÕà`‹°Prbè—À®y0é ãf1Îb&ÇU›Žß^x¯žA`T³ˆš†™DÓÑh z° #t#ìP«_µáï“ýaƒ…d¶~zœ+÷y`µ2ü˜/ŒW.¹=¼ŸsþÊW\†&­ƒí…V\)­n¯m¹<übm/.Cà˜™¾a¥Õž3tÀ*P•Ær4X*ßCíÉïÎjÏ /$˜;ž aIÔͨеÇÍPðYx}Œýë,`ë•Ä“££øk|`­s)™²Ž¦Z©_í”–ölýÏ• +ï [Kfèòž*jÁë?xæ$¥Ô&ò"¢¤µ®"ËÜ®\³÷Þ¢9w9íó 4T@W`: o½ÃÞqjØ…œ9Tzâ]èù…¦ó1’@O_:KÀ"¢|17l8<ýÜ£¤É»Tûíî ˆ"®þäMKûŒœšÂÈ©¤ñˆº~ì®ß[ÄÊì-¬Ìι²Œ“Ïc깋˜¼tž4ÂÃO CLç !4'*`ÓYˆ×²ðEÈk¹ãƒ/ª cì—s‚xpŸðÐ!ŠÅb CCC‹\‡ãmÊi ""‡ôèõv–؇eÁPi¼:Xg‡F-@É)¡eÀƒÀwE®ŠoÅÝAø›>¿ò•&‰\›ªÁ·ßú5îÏÜhþitS—G ?ZÙ^=ƒý®­¹XÄŸþsWÞmÛ1¹žap=C`|2 u‘hOZ1e}Ç.ƒXжPƒ®3б<ða+yMFASá…ë8ìЪtû;Pàá̇²Q¶f‚ýU“.u”™¼õ•اýƒÖ\ö vØNÏÐ%§‡l®vNÁÛÛ"f1?bJ)U·FÇ7îþyÍ:œWòxéîûøNÏç€ÙR¹rŠ‚›©NG{Í—ßîy;±­Ý°ƒÙr´£,eÁï“‘ ŽÁ͵TãùC20¨³ ïÎñ†Ã‹‡>ƒ'"£xiþ}çÝtzf×Ê;RryP2ö]–ñ|êÖíµ¡ØA7PÈj0ÊÚXÓTH" øpºæñn.¥qÁËAð0õçBØ!“R kÃrºf »•&Ü˪ÔSÄÖJ‰Ÿ±~g¥4X)P—JÿoPŠÝ XP4мå[5çn µÒÿݲCP¶ö^ãÎZÓù^ Ü!ue‹’_¹nx ¼# ø^çïé;ృˆˆÈåƒr d(]2Äí ŸmW†¦oåò÷ ­ÿjªpêþ0ùŒì-0ýOòŽ®BDt.æº C¶îœ>v†8Ž©ç.âijÁ}¤ÑˆYw†Ôbí÷M…D …D Á(BãÃ-Ü$9[x`››‡X,v¥û9:KW|Ým…R YUAåH 9q“êó#ÀwÞÊ/aÁK¯ÑÍ%Ëâ3C£x{ñ.Ò¢µ—÷©Õ8R«q„‡/—¡ë0tmþi1›oXÞܦy°cpêhs74åîª ]Óºº¯í…TIÆì›W°1¿Ðt=o»:ìò ¹¶NÖnÏã½ïÿùÍdËEsøèa°~P4¹Ý&""2§œ&#€N»´;UÜšrª\v¨,­”,"êñn9=-‚ ‡ósX”…6ôðÎ÷ ³çg&ȵÜÕ«f·ó?¨‡YÁü¹ì7ì°ó0â$Pd“ù6;lk¬ÌpLˆàƒ\ C¼/ œÅß­~h«ÞßÍ,áõ¹yMbúH/X†îØ!—QëÂåR$q ‘(–£­Ó~XÛíƒçú¢¸ºa-(Ê6@yúL&Þv(›™¨’h¾ôLJµñ½½.Ùra0=eÙuxè$•M3óÖꘈ™„ à@DDäà Ür_Ü2˜º\eo•`‡VÕ–š…ºú#С3`"ˆÛ™7ˆtî=}èè'Ý¡ µßîëó‹¸ýë÷-ísê —à/wFDtÀÇè¶‹Ãúü¢%8¨ õaòÒyœxî"¢S¤áˆ‘™EqsñrñDW{†˺6Þç­ní¿$«³t.`M, ‘""jfB¦ôûà¦#ËäÉ [3òó<.NÅoY_e?6w·%ÀÈE¿9 \SUˆÛ«ÃÖ)«nnu¦ì&SκºUƒ>zýÇÈm$šÊ7<:ŒSO? ¡'øÀwM#42d ši—Úéê@܈ˆˆvæEÆú£öb ÓÞÁ:Kƒú¦5ê¦Ó M1•—U…Ÿe[dlìO}·ÊUB°¥PFóý Þ>{·é: jõQ«Lª÷,‚ÚøÝ;l+IÀ0ÔµCÁü¬ XñbÌÄ’”ÅóÑ“˜ÉÇñnfÉVµÿÝꇘ àØIë/×c…XšÂ±P¨I¡…Aívw lî×Ê2íÑPÐÀÀDªÞ`ž¡L‰@–ny•Úi²ÃáÅñGÊÜ~n4v] %ÊÓC²¶’c\*àÎbÇÎðηCÙ6EÒkÂÛRd/S3¿\QÁ• Nއí÷±6²¬£W-]Ÿ Hm*ˆð•NÆð”|¼ûnoº¯Ï:ð ¤ÍÕ¦%c °åÑzÂÐe[P‚éiÒ;jý>{=¨Îqx0vûÓ[×c–w§GÛ|ŸÚ·ÒDDDöîmô¼`ê¼° 7è0´ÎtYn5ìPq¬ôuÅ0C—A±AÒ¯ˆºaò8Øg'ZÆÎœ"Ý¢Kµßî3?}ÓRúÐð ŽÿágIÃhmà nvq>àÃä¥ó˜¼t“—Γ†#ªÿÌ¡iP »ÏfJQ¬X V“hråb …hŽjüŒR>DŽŒf™[—ÙÕõʱȻò¸Ò­}ž%×vÔ IĘ׆¦ÁR4i)""‹òq<¢>?hŠêÈò{XÆ4ÙÖ'¢x“e!©ÖV{Œß¾‹ÉGÎB/—*˦‡B2]÷{1›Cü¶yàaðøQ°MÞ(2üîõÅL¶kû×^Ø!·žÀGoüØ¡]óf±Ž«Ã¶‚ýÑ — ·(¹´‚w¾÷ý–º:P4 .4 .|4'IŽˆˆ@{‚@ÎÆ[¦pÅ1w. f=&Ò ó”tÍÒq‹š\¬´‹©t†Ãù™¬oÊávÜ D¥gî8v`ªÉóQÔù´i0µv€Å¼jn£€žÎ6HgãœkmëQ€I#„˜œ‡jèøÖèçðÕü‘×ì­¸ýí…·ñJài–Ë¿”Ë#ÀqÚûìcúœŒæê£Þ6WÁ†-8Á±ñDUÖÉP‡™•Tè¿ôhÀ£àVu·‡¿?÷^]þ¯­|ܺ+¥’¶µßÏã xqí!`Pkn>ªÓß4¥>ìªdÐ(¿XªˆpЃ¡ˆ×zk#ìE‹°Ã¶tÝ@jSA¤7_ÿþ²{:˜îïÃk7oYkKÍcõ2§7€ ( x®×[ë¦@ñÖȉ  ;[Ž\QmÍ –UŸw0Â(„@DDDåãMß½êËe {1¡vÂ;Õ.' .ýìÈ—Añ}¤ÄÉ¥{δH€"sÚow‡Ûï¾o9˜ûÑç¿LŽèÀi}~ë÷wþu»&/žßø 4`KΖžËtMƒR,èåà‚&+Peûïàü½H-¬˜¶•‹'PH¤éGÏèÀ·?­øÌqœ›‹{¥[Ç:H±Xlahhh€ë<ŠÒªŒµ|~ç3OÓ`<ÍìBÓpo ·jèÈÉä-Â¥h„½^W—™è`ˆ¦h„=žÎ’ ^Ò˜&åãx,‹?ÀÛ‹w-ï¿ðÑ5œ¼ô¸ó7Ì…¢)‹JSî·­º;œlºü¼·4†TY†¦¨]Ù·Z;ú¢8û¥§«º:lKè Öý~¿tãg¿ÄŸý²eùS4 .r|äPiuL"""¢&5“cÚ× îqvØVð`¢üº•2$Ø2ÑŽåixÖÜqÚ;TÛ®jõ¡'êÌn{Ú(ºÐŒÉü {Aõ<p *5ÒÖë«Ñ1Çó`oqLˆàf1Ãã;ã—ð—÷ìݫŕ<¾=÷^ö_zeËm{3•(`Èç3Wþz}½›a‡V•eì°ý9àá0=ÅXâ^w—ZyŸ|â´ÊFxaôá’Ûý÷q·rþr©+°C2¾³¹ŒÏìfÝ,¸@;e2šÎïÎJ/‹€—³;Ø™k­~ ‰ºík±"ë(4x}Œù2 P,mœî³Øh(Û·aÖúi5‚/AŠæKNV8Ò[»Ú7Å÷ZÞçêRpøU‘ªé­9Á²Ës*oý}89u™vJ.;0¶î!4qwÈiE2(ò´Ä»ûÔN2Ô•¼ Ê;Bú-ÑAQ]8ßÊ0dë@êØ™‡HwéBí§»ƒ"Š˜ýç·,ísüñÏ"<nxxhoÓå§·VincTí…Vgçpûí_7;š>ã àÖÃ#Øu×b±ˆw¾÷}¬ÝžoIþt ""2õ í Ãê œÓ€6ÈtÙÇÄDieR5y,Çàc'¬kàiÆ™s9ˆ°&8ö v€š«¤–òJi63†@Óœ„¶Õö:`™…ìNQãE -úSrH©¦ýƒøúÀ¼¶vÝVvWókxuî&^˜>šåñu3™B€ã¨¶Bö¯,5`‡ím‡ ã}˜YN '«ëšÔ€ÏVw{8æ 㕇ŸÆëñ9¼ºü±mב¦®›{Ÿù¥b›"†@µv0,MÕë‚”I8AÕuܼŸÆ…©>gîœþl¦~L”+—V 4(š29ÏØŠ-}rÔzPc…CH»cÉt¹ôù¡) Ø A1 h®äQ­ì†Úò%„(ïˆåÕucɆ&]pFÕt°ŒÃn×e·%3ó›ÖëFpi-ˆˆ,ÌÃ5†r†²4dPš—¶ñëkÜþ´­ uõG`úŸ„õô7€å(ÈRýt,G5ž·¶þ͉ î¬fpl¤Çfß3ù6Ò;;€a…¼5WÆC:ßýAl"Äb6k¾è  )%§WÜê€Q„  ÀxÐ%ÂÐ[^Šïµ<¤‹‚³ÀC® "lÝJo©¼d£n"ùñˆ¨ód¨[®O€aè•.8–È lVzö qÅeÑï‚òDAñ}dŒ¹}#UP>ؘCF&pS7j?ÝR«q,~tÍÒ>>ÿep‚@ŽÈõÊ'SH¯ÆË\;ªür8XÒdš¤T@ š¬XŠ•rJ…DÅDªú¿›iËùeWÖñ»ÿã8ýgÏ`ôчëß•¡ñ!ôŒtl›nìÜìî€Rüx׊Dkuž\Ûa º ÀSó{ŠrO@d#؈¨U:H®Û wË ^l ¸8qÔð°|ãŒ~Èq—1—¯ <8íî üšjx`=¥zPeZ­À7J-­"2ÖyvÒ™øúN€³°C /г_zBO°aZ4²S÷nнß~ˆ÷þËZ3wG& ‘i1>ëFîLˆjAZéxš¢i RíòR[ÿ9ÂV9j IDATS6£­çÚ\›X€ºô³çÞÌùÕK_륱ljýU»©ál}ÕÛæ€¢¸˜ÚbØa{Ûp\cJKR)ð÷;ã—ð»?µ½²þKK¿ÁËÞ/àØ åúQ £=ôõUwz¨Õ× ìà|YªÁuÒ°“aƒ…Í\cØaûß|¶,rÀRe›yüxù¡§ðNr/Ϳ߷óšI¯áùÕÃ@X6Äo:ØßÃQàxЬWMǰ48®?–öì³´‘G_H@8À[ì{&?[ÝÏ6Š1‡_gî-öõæéÞÊyc²Ç"ð€’Ëåqy ¤®Ð!th Ø` Ø XPŒ·äA{œ¹Ü #@ú†µqu?éT/vyàžºU‘6ï)(¾wËOâ"‰²Ê £ dt@Û]Źy÷2([rÉJ~à¢ÂÈPW~väËz r¡È¼Q³fŠ6€‡Sxè6í·»ÃÕŸ¼i)}ÿá ÒO‰\;– É4Öï-"µCj%ŽB*ÝqçA ‡ð¸6H¹<4I&+mwjØvfp h0«ÿð ô=ooOãGUEr~ Ùå5DŽ‚/긶^ý¨2Öã87÷J7KµÕaŠÅb CCC‹&ÜV¶‚®A3ŒŠ•ÞyšÃÐð0‚.!ŸÌ¼‹V¬&ê|Ñ  Çs°èxŽaàãxÒÀ%°,8†AXðb*:€¹Äš¥ýUYi‰ËƒR¡©êŽ[Bå1å†î©Õ8r›æƒ5€€’Õ&€Šò…‡‡°ˆk®l¥ ‚÷: ‰™,ÄL)h¢YØaø¡)¿ô¸)ˆÁã÷ÃvÏCÊ{ßÿî½÷¡ãù2Þ0„¡S 9²² ‘û?N€®˜wœÙÏÀÊÆr0¨¿aZ‹ßàaäµy–åå¡™æÏ£¥°ƒÑ¾[*†.FÙŒRîº`(€®’Ay¤çïÁP³.+T zàÆÿ½c@ ™7Z\Sâªå}ú&ÆIÅu™öÓÝaᣫ–W¼¿ðü—H£í«QDj¥äÚPH¦Z#µƒ"J{N“ÏcòÒyL^º>è#Ü)Ï šµ A)ŠP bÛÁ†mˆ!³‡R‘˜[„R”YŠïk½Üÿõ ¦ž»h:½*ÉXŸ½ !Dtj¬ÐïÍäl‰ÛŸVlÜë~”ŽÅbWºy¼à¡3õCáÊ’1úhŠ‚‡q_÷²âìàãÉ &"gäãxD¼>°¢éóùIÛT÷`³XÀŒŽ[€Ö¹<’iû£lÏ%ƒ ±¹»æo@xc§ræÒ³hˆÙ|Wõ!¹XD&¾H.­4;¿ø9úÌSi)šFÏP¿kêàï}k·çÍ—æƒÙZ¡ˆˆˆhg¾dÀð3¹5L{]v&ÒJßÅ@1´êy–¯ð ~–oî<¨Ô‹Ùúnìgý܂̜×^à¡ìÐཬ ª°ƒ™¾á4Œàõ’(еs³ ;P8예c÷zq£Pº—}¢çþ$zo$nÚªþ»b/ͽïBŠå²ªº››¸0пûÜN`‡–þ­$Iƒ¢éÐ @QwF(`YKcið<]rå©qNC=>7ã)ä$µ>$QþY0€G‹À ÌWÎy†Ã‹ãŸÁáQ|÷ÓÜ-¤ÚzåÍk î$Ó8¾A?21÷RµçJÞKƒ÷ÒMä·g,i:n.¥qz2bzŸ–ÃÛÍí7<˜XÐàñštmål-ìöØxϬµwµ8Gï ­öeNWa@mC”œ!@{J®Ñ[pÄNó6ÜÔÖÒ€BÁÉ >QVÑ•"q‘Dû­2§0ö|.‡ †&“AÙå2r·šÚÿ«ÏŸÃø¡0 ñ³_ÜÄýeVRÝvzûSÒHDdÞp{É Ëûp‚¡A²šw7i?ÝQ´ìîpê á„IõEëó‹%¸a5¾ãÞÐé`öø€“—Îc䑇äÐ)”²¥ nÁ Ô¢UnÏ¢t‰¹Å ¸¡Hï;ÔPOb&¾© lÎ/CWÍ¿ÓY¬þþ&zŒÂ?u}ŸXÙãîÀ²,(ʵKÑ\éö1L€‡ÎÔ¸x ^Ö–.: ÄsÙê+¾í4?Ï‘žFÔÜK3ˆúü؃9ÕÒ…°à% mS½^6‹L„{1Š`1´´«\Äl¾H¨ÂåAK7ûö‹ß6‘DOÙª#¦Ü,À0ùÈYçnfxÅt¥%uxxð@÷ŸÔJ †®#·žÀGoüªd}ÅÖÃã‘?ùýæ‰jÎ+ÀØg—äÒ þåo¿×ı4w{ðí ŠˆˆÈQ¬u«Ë˜”/ܹâ7H;;TÍžB„W5*_Xrƒ ǃ¥èæÎ…28ßjéªä«Ó€È…”*âÕµëXÓ˜ÉÇñdhÓA|eø(ÀiM×_Åþ0´ù>AÙ<ŽÝíšVŠˆnÔOMä•Óä=ŒCçaÛÙÁ0Ÿ—SÎ Wú_Uìµ­UØa[ãyœÌGðAjm§ ¾3~ ߸ûSÛAåo$nâØ\—Ïœa¹~rŠR‚ú{+ç §ëßj?ì/ªÈKjí¾RÇ™A”5ˆ²Ž£ÑãçÀ°ÔiX†Æéá–ÒyÜYϘƒÊÝΊÀ:ÜáµrçFÆåþI|wqï&—ÛÒBwò)`c²<´ v ÌæW¿¯ß\Jã± _T¶T,jÈçTèÚžöΕ.;¾ 5WÎZÛ|öY¤ª=;[¼f‹¢¯iœÖ`ëUEØcã½Úɾ­ùáØÐ·ÚOAyG,žÝYÍ`ú„sNŽ©œûVW^µv+ÃG@yG@û= Šï5ý(@Ô%2t@¯ ž3ªl« *  «¤Ûõ|NTýŠ”¿gk¿Ãüoÿë³$錈POå;ž¯ýé9|íOÏáoþó[ø›ÿüVseÌÞ%Œ€ž FDæ ·Ö¦¼ayŸðð©¸.R.± ]ÛÀ%ŸLaöŸß¶´Ï©/^'¤áˆê?ó®Æw~‹ß”bÉ¥¡ôýÁph0«èñqL=w#<äúàín–œ-@ÊåÛæÜPrhˆ!s?Þñ`C5‡K‹¸Ð,ƒðÄ0zFþt™å5ó×ÈxÉ)ËÍãfá­+>àÁÝ"ÀC*‹¥†††®8ç¶²eUª¡×"ÙÏ‹šÉŒ^¯† "{ ðD¼>ÐÔÁfhŠB¯×O¼I…/ Š‚‰p/B‚iÑÚÙª¬`cá>†¦Ž:Z.)_€"Šàb6×0˜^•e,ßøÄtþááAAg‚Ê9¯°Sæf•ÛH´¥Ý5µ¹úò‰$”¢U’1ûæ[°C /ŠSO?i v h=ƒû>nZ;ðÑÃðD“I‰ˆˆÈQ16ª™ÜÚVPâ~ßKºv jm¦`yX²®í¤ãhT½ÌÝ;ÈLÉÕÀ_}ú¯xyåwH«»?¼•þpnq¯žxÓ½æÛ±Q,ǘ?÷V98ÔÛ.«ŽåSòö‡‰Ó°à•ÿZ ;l«'l&íåggÙÊG8*aìF bé€!ÞïŒ_Â_Þû¥ífùO÷‹cÂáØÃ¼­6Ì)ò§åwÁïMümÀ@*«@QõæÜ`¨’óC"-Áçeà÷–ÙJ—¥ ûöñ¸±’„¨j êbÏç ,7<@¢r.òøñ©?ÄLf /Í¿¸ÔÚÕ÷—‹¹’ã„Ø¡ÞüÝbØ j:nÞOãôáR`y&­@,Ö†ó ùœ YÖîåt{pvJ.4CíBVïg¶/C¢VxØÎ³_â¥tv`´ÏQÄqé­/;Å÷Yfî'0‹®s+*x[ã=}$Úòz4ä$ 9 =ý1°ò „0ýŸy”<ÐušªBµàº\Ú§B M&uéžF%Uàȧ«RÊ.ìðÍù_âoW~W3›«ù5žÛ¼nÊÐÖ~väˤшȼáÆZ¶Oõ!ÀC·h?ÝÖç±2;giŸsÏ=M­C¤ˆ"R+•÷©Õ8q÷wò|2…B²òýì+z|“—.`òÒyââàÆ1Q!g ;C+ݶsŸv ܰWSÏ~#TíbÑ© „Ƈ‘˜[„˜Î6Ì/ýé*|}að~¯«Î³š»åÞxÈ+d& ÀC'ë €¿pcÁ6É•Àƒ—åPTjÿ E`"[“(Í âõÁÇq]sÎÄÝÁY…ù<Î ŽàíÅ»–]Ä\K7>q|%MQ‘\Z­ ìÕâGWMç+üè›8äÜä9È…êu&–€È­',¹´[™xÉî“7¯´v`=<|áоžûÒµñÞ÷àìÀGÁÓœLBDDD-E³ h†n>¨.¾ïÀÃ>Â:¶Ñ"8Áp8¿Òé’»€Wã×êÂÛJ«^¸õSÌ„þ ¨° ;¦T  ˜ë{VêDU–iìÐ`Ìù}€,5yLÃú~a'‡{ðÁÒî½í gqGLâÝÌ’­™,¯)øöÝÅËþ'ÕM¶Geýì@ý{œ:v0Úw|yQ…¢ê0` “S j( 80 e vØ™.u ••á÷±ðûØÒ° Ó£Ä2ÜYË@5 óã‘2JÁêP–8à._‘(Àpxaìa\îŸÄKó¿ÃÕÌzK®Êwo?_»‡§ÎaúRˆè £Îù™ì¿6a‡m}<ŸÂÔ`˜zn){Œ­TEG&© ÔËW?®ÙrÔÇS¤ÍUdó Ä-!gôsxÙšyJ¢ÀÔ/[´² .Žà­åt @ ï¨å}®.%¶ &çS9 CÑÖüPûä™ý[õΓPïÿôÍÀú3P|¯Ëû›jÉYÄÐL¾Õ½ƒ"ðQ›žÃ‰ìKÍZÞåË\=¨gm-‡ÞˆÑÞW÷ ãGÿï×ñåûšmèÁW §¯%íFDæ 7Õ¶ p*4U’M)>èªÖru-Exhaœ##!7o‰TऺPÄÏÄJ/6¯€µ— ¹dªî„3~-…L45/cîÚæ>³œß.Ù@ÓI ±™Y\øá)‹¢YˆáC`½d" ¨ÿœQðBËÅ-åI/`ØÕŒ—¿,%d{>ÏÓŒdæqfòMÓ%fðjä&þx ßº}›c™êuoÙ¡Üoªâ@ ,e²€0o“Ì­SæúžÕ::@1öýg‡ìlUy°|Nö­Þ~}qfÖ­+žÝõÎÈobBŠÙjž )†¿ºzgÙ'î*uBéÀð´¢`dqé,}× © L ITÍXõv6¯ÂçáÊÛm&à~åÿLN…ªéð{9Pô:U•¯a¿·€ë‘8â’\¾½Êú`·|â÷Âg=‰ ±»xqzóù¬ãwç‰Lßûä<þàn?¾û¹£ð>®Wéop†ìà²Ó‘4öõúÍŸòy²¬ƒh×[•gìš"ˆ®˜_–V‰EH² IV‘Ësè –~î—Œ)vP`¹ dŽ®Ú_ÂJ×ÜÍlÛn( P¿ç¦ëÅú`X d™ŒbxOÐ1;â)‡ƒÝ¬*<ÀÉ#a¼{%Ò´vÔ3ÇþØîß/àjRÏFÈ÷ H rC½mƒð°w <Ék|b ¡ö=%kó‹5“´ØÇ <{A±>ÒxdÜhè’u (¿w\XlšºÃÔ¥Q$æÌßo8QÀá¯|=ðõtÂ×Óžã‡àë±KST|îëC.G6š€”HCWמ»òÄ6oKÕ_Ne1wé³÷ËÖÞðú)áaÛ"‰ÄÃáð»N¶šm9MENSábZ¯{µ»Ü`i E†ªài~Q„‹åH§"0š¢áøô=ôÏ1 Qwp E! º°œËÚVyPe¥.*fp÷Êgæ'<‡ðÁýŽž_×ô²Ñg½¯¦£èÜ?PWŸ©yë/o5EEf9†ôbãçk9-dÁãïjž¬\lfoÿýÃ/\½GAs"|sŸ}– 9 ¸»ü.ÓäÉê1ýÝ d[õ-Ev(¬K㪄Óc¯!¡ZÛL`$3?ÎïÕ|}˾7“ì@UhcUu̦%%g»Ï%ox¼zß³LèX©#Ï9w=Y¹n=î„ÓåÛ';1p‹¥ÒÊKb/ÃãÙ¾Çpfò×Èhö¤©G3 xáÚÇxÖ= ø4Kd‡ÕõA‘ôÐKÙ ¾¾7É €LV)C`0 ƒ*o·É øõÈË:b)A?_ =l²Eä ï a&–ÁÔrj•|Q•ì°þ·c9 ÎW`Ó’îDp†ý]xen /ß½V—;õ Ó¸ðæ,¾;ù N}qЯ–ok»cƒ™¶5Û÷PPkXLHè¸à÷p–Ú5TìàÖäà ;îõxZÞBvØð| «€ci|[×цÄ–d;øéÁÄù|>¼kù¡‡‚m‹ØN –Y/ŒG0üHNq1$YC:§Àërèý‚ Uõ“'èi*á¡Ð–y¨s¯NƒéüYäÔsÍMàøøeGWRÖT ‹itu–Ôió‹øÁø¾þ¯_F2ecƒ@]†½¦ûi?2n´ dëjëûHí½UVÅ›rnE’píÍó–òþòIp"y?Z E2Ãâd᳨À@”¶7zŽŽ‚C«M×5 R< 9•œÎB•ë÷,é^%8ô+lHÛ»ò:ØÞçFÇ`]Mh–§;OwaCU]Õ g ïâZìSï~¼áŠ¢ZYáa:‰Œ‘ƒ¶;Ρ °Ï¡ßÝš»Oø…{sW~‚Úáå´‰®ÂÎŽ÷(:Ü¢îP´»ÜXÎv‚l5•‡JRi,Mß1>Ô¿ÇQûX‡œ-¨æ µ[.3µmÉ>’œ_€š—qùoX&L°£_{ÊÙh®ºC‘ì äjßAó÷@èE“é/Aã@±Öב|¦4×jï5)§Ò*&Ò¬ƒŠz9Ëa"H¥`T™YU xîöŒf¬? HÜ´æl+wœgKüV«'UçãE”’»µÑ–”T£@šõ26ç§*eûüó”"óÚ_;fAQ…^§ò`šŒT;ÙXpà>W®­í@ âÙ]á¯oŸ·}¿¿…ðeN?8X =T»ÆKØ–V7)=˜©×=Nv0 ’²Ö—Y–(mÕž¡JÛm"ˆ¾\U5Kʶñ[ƒäW>û‚tøÄ‚ÚCNޔƄ*B@NdÛpscp³—ápºï~œêÀ ·>ÂhrÑñ»RFSðý±ñúü4¾{ø(üž påÛÚ¬Oa"Å25eÍŸ·æ’>2?†Q€ªêˆ-)hïä#;ÓÒ YÖHËUÓ&2rIÂP =Ä—„ºÖõ¹-÷U¬dü÷ØN͆Zÿé©«ÈLZÊ3r' ¤(@tnþOÉÎhlX üñ£ýøÛÿòIK4©žº„ô@@Ðôu*éÈ[ÖïqW_F—³e päþ0~þO߉Rš IDAT“ho=3:7[¸Ïqƒ`[Ž%ž0qÜ=€Ä\óˆÉã?´€ï´aðñ‡ïù6+*1Äçæ¡HÒ‚ÁÎ@‘àPTq h-È©,¤DAÅÁ‰rÈF˜½èØ4–Ʀ¡æò;ÒŸ^~v}þ~Pžã ÍÍ2-It(âÓ|}Ãÿ-LvˆºÃ*HÄ×öÆ9ÓŠ†Åä|ˬBd9]nð sOûA`YDéuÇ0hE$$i[©`颥<‹IDærw:·!R$šC_—ƒŠ¸ €¾Èð¾ÜÛŽÑÉåÖhVBz ¨Ã`OÐJØ;P}¥åå,TU/zËàÈýa<ÿ7€¿úÛ7lÙ¡Å>ëú:i2n´B+©Ößz j1ùL†8p‡"ŸÉ"× wÊ›¡HÆ/~`)Ï矹·î)EbÃâäô†ï;¼×½Jnì'‡„&+ȧ²â)Èé tM¯Û¹"£7»ÈÈ ä–w†* ëÐÖ× wGÞpþÝÝhÛFøÁƒÛH\n-HÍ-nQû „‡mr]l_D"‘sápk²Ã—åUå:;.q€M\¥Á]öwá•È^ž¹V—»øË·¯áõù)<{ë! Ù´ëæû‰ÕúZ-sÓo‘å,:Þy!“R7ìòA×å£iT&ËAh®Ï㙣ýŽÙ‘Î)d­@ærëÞËŸùÆüÏgÏ·L³ê©-€ =N|$XùžE")!ÔæßýÛGpñ½)ü·_ݰ޳¤Yè© }÷g“qƒ ™­•›µœ§so?qÜ=€äübÓÎ=òÚ¯ HyK}²sßÎí—‹·¦Ÿ›G6G|nžv8zŽBhpzFèàøz:‰SZJNB.š¨»Šƒ’“Ãüè DFǶµÏBƒ{À¹EøûºÜ» ‚ß‹]ßÎ%‚8p.‘t,‡1öÚo¶#„‡íÅ»ýñ3ßh5£TCGJUàc9ÒBÛo`¤ »í{y8c>A€n˜K% 7y–AÈå!ŽqàÝíÆR&c[å(¨. üBÝíŒM@•ÍïÖ·ëˆólz3ìoo{é嘥rc3³öÕW.ZÍ˦¤d ™åÆÏÿÖò9ýþ“5ÕÃhEÓM¹Þ>ûCÄïÎÕTE³pï>Z𒆀€ i hëKî‰\¼AÖ5RÙÁb:ʤýM#E8˜NbW›âôøkH¨ö78Ýõ@öÉ0+do«qv ›ûiQý ²C>•Ò°žðð § £™KÕùÙò8þ¬·…ó›ìˆQø£)se8Iv(¦s‰k„ÓçtæZgò¸~Y”µ¹è³}á¦Ä³}¼pç=œ¾Œ÷óöì§VH Q w… ¤‡$;ȪIVWuFé44 ð, Žc r4(ŠrÔ½”O**Ö©\ÛT!=$Ó ü>®bZ–¦0Ô@¸Í›‹I¤%Å<Ù¡ˆ |) Üä€)nCð²N÷ÝSxaâ#Œ&ˆ˜Ïgñ½‘óxüN/ž}ä8¼k-Av ¹?jºŽé…4öµUïOÖý4Õ( (Ô6¶ÇŽÞJ¼¨PfVRÑî*¦“reT‚0_H<à»71Ôº€òì…‘°¦@úúÕ;xfyhw.àpf!ƒ}~g ÛÔÝþø±~œù‰ŒÜ2M«'.ƒ{A{ö’EÁ=¬Ld²rU¼øwßÀçû{$SÖ×úzìcBx ãA³[N¶¾IZQÝ`ßbñ¦©wdbqL_²¶þy虯í(ß/NN#1;…Éi$ææI‡Ü¡ð†;àëíDﱂrƒ¯§¡ƒ„PÖªÐ5 òŠŠƒ”HÕUÅ!M`~ôî¼wÉ™í5Õü»Ãp‡Ú쇧;„@ σå9ð>7éP Â×ÞÝð?Ã0`[wCìéH$2EZmåZ".Øö8‡$<À‚”…ÏÛFZˆ`Û€¦hø~AMQÄ!4CG2/!¯©XȤ7^ãé4u’‡6N¢ÝåÁr6[“ÊÃüøÂ÷×ýÚÝ+Ÿ™N+z=u±§’ºCžP»eÂCz1Zwƒar‘—‰ÆpíWç æ­½Ôî9t=‡Ú¶¯™êüø'„ì@@@°£À¸Ð,’"ra¦žcX#Ɇµ2›Jv0.¯Š¿uÐ ݯFÇð³è¸íý›='`E€Ñj뛃ìm“'jL_ÊoE"€öY§F°$K€ 5ªÎç–1íD_H¬Á_eú²¦4gÓ¯Fí}–ã ªRýZ©I…¢tÙa·‘Yįs€±–ðìÞ¯àÛ7‰yÅÞK匦àÌÍ·p–ú2æmÛ¯zôÐÚ¨ôP®Œ‘T]G*§@Qõªùt’¢#MGÃãâÀÐTí¶P€ºjƒCdÊâ')¯AhS&ÍÚõððxÈÓ™xSK)¨ša®ïRëþ? }*pU–7ÇÂg?‰ ±»øßo~ˆœ¦:~g¿Å7µˆÓ×㙯 JS•( KCSõÕc±Tɬ¿›³4ÏÐt EÕ>¶­ƒÀ™W4RT²ªg™²érÙ2„‡uðûq¯ÁÐePt}7”¡\»‹„‡‰Å$"³9„ÛÛ/Í9Gx ¤ymŒ:óûñ·ÿù“–j_mñmЮÿ É¦A÷ĈF\@°É„ôUO׿ñâß}ÿÓwþÉz¯SSDåŒÍnEyÉrw0°áUVÀòd“Ð]Ó\XjÚù?~å–Ò÷; Ϧ>¹Ÿ›Çâ­i,NNañÖ´%e ‚íõÄ_O¼+ß ¶Çx(ÅÓ«$‡zb;’Bƒ{Ø» ÞÎv„îëGp<AÐ Ö-€fÒ‰šˆÉw>D:²‘ØÚâꯒV[!^€Èn}0—SDs¢ôà ŠB§Ç‹ùtª&•‡éK£|õ©ºÙ›€”6dÔìÁæ]Û¡ æ-Ʀ£u·Ë AJ¦0õÑ'–ƒÿ½!zêÉšìk–ºÃw.`òƒßÕT-xáÞ}ÌÖ®êõ͹ìÜÞ:½Ýd4É/N—+îUqUÂéñ×lŸá‚ûñÜž/þáUë¾]GÇÚë•ò8AvU­œÇdYKù­óëaO·e…‡›R Sé$:D†±á/£¼¿T­@8°êW»¼ýRùÜ"TJ¤3êh×ZÙCÁ >î_‚:µ¦Šæex<ßg&Œ¦ØªjFSpfü-œÅ:Òƒùü éáHGA(Ü_ïïëŽå ©¬l«<Ã$Y‡$ç `Y ,MÔx êØX¦©Àø u*û›òÄ ’i< ªHæ*AvXÿÙô ÜæÂÔR3±Œy²C¢<$qøD6 É'Úw៎=¿ºq±.jUÁnâÂÿ7‹gGø ¸tó>wˆì°ê7Lr#g:’ÆûƒÖƇ”Š`9 4MÁãb‘É©¦ÊÌåW”( ÝXëk+—šZ÷SО½ÐhЭmñÊû“øî}‡‡bÏTMG$šC8är¦@€u§žûïáÕ÷¦1:¹Ü:í«ËЗÁ?Oú:ÁNÀˆ vëóý+×"œ}ŸñG0„/<Úß¾?m}¨%*dÜ hn«*Ö87o§)„ð°“Ž.C“›£¼Vü7/áDÃO?µ­üK;¡Á=à}žU"CÏñC¼n¢Ø° ¡É ¤x ÙhJNªë¹”œ„ÈÈfÞEtüvËúÄÝDûþ]ìÝ…®CûѾ7Úw“ÎÒÂÐ5 Ÿ¼ôó­÷M®¥çkçHË­D‚msD"‘‘p8œÐrR 19ÕÐÁR$€œ u!²‚.7xž\E^UI'‘×4ð ¿P~·3YÕˆÃF»Ëå\Цá©ý÷á'WGl= ˆŒ”êùñ ó ž«›fà µ[¿ÍÌÖ­W¿v³ËÁÿ¬ÀãXÒ¤ÍRwX¿…O~úZMe²A+‚â¬ï{3ð»jM-Lv°b¿ãê†Ãå™ð·Ì¬þ|zü5$T{/oº9þŸÁ¯®Lt€¶h÷úº¯ß=ÛIÁj;Sû©¢Ö¬ °”ÏA5V‚iÐ ?zmÂÜ”bP% דËnï´XO£r`±¦UÎ_AÁr›”+‹çš.H”*¿²Uý:íÜÌe€ùµàÑbÏï9‰ïM¾i{$Éh ^˜úg=¿ï€jÏ^F¢ ö¸Ð,äd ©œs/ÛUÕ€ £ þÜ" ·ÈˆÕúÑÊ'ËQëTJ§1CX¨…ìª@¼HeTø}\U²ÃꚊ¡q Û¾SK)D¹­ç¨vm4à÷²ÀM˜â6\:^–ÃÙûŸÄ­lϽ;9çw@M,âÛçßÂ3púKû”Ê~5Ó÷-’€f)p E^#]de‹q Ñœ/)”Glß‹K­y ? ¾ ¡kîüö2–®OmùMlí{æ9Òzk Ñ`;¯øV+¶,çÑ%¸H ´ÞàG3¹=Y2 ®GLÊa1“^ý¿ÝUyrÉõ]ôúü˜ŽÇp_¨ ýmAL'b–˘xÿ#t ìë°ìV:ºŒøœy™¸]Gš+9¸y'3RiHÉD¿¯nvU[ JÉ>ýå[¦” Öã§Ÿ+ÔÖæÍPws9üæ‡?ª© Bv hU0‚×úýV“ Áw޾7mq²eÒþ@vÐ(Âõ¹Ämü,:n»UŸ8‰nÞS8¯[±f÷æºsŒ‰^Éošf½¬M¿-ÉRÉ|Ä eßGä  Óˆ«f²iô¹½&m3ª‰åõ&;TƒÇ ¤Ò ';Óõy½X IˆË2[›ã{ºñ—»Ã÷ï¾g»jR g®¾ƒ³ìIxûtkönúýúr €°ÇÝpuM7–ûå¡rÃ2’Šl^…ßÃAà™²¶¬ÿ¤(X#;T²‡ª­OKy 7 †µV€È1ê pcj)…xV¶Ö¿)ƒ °[>€ØÆ5Ö>w?>… ±»xaü#Ûª%åQ¼<ý.üÓ,ž}à!øWAí¡^d‡2mÈ»h¨ŠÃXûî.eÐîãÁ°tÕü,G¯µCs€"Â#²XNš#ʪUÕÁ²dcKг'åÙX$Bz£OÀUP¾ˆ.¸8"ÁY¸9>A@*ŸÇýûñãË[.C•L]ÅG••Ÿ¹rÝRúðàþ¦ûÓÛDzÙi$63‡žÃ¾ú^wŠ †+=›¹| K·¦,•·{ø‚}½5ÙÔ,u‡ ?üqM‹eBv heØ›FÒ +ÀN=\!d‡Òéš@v€üÚúô˜}u£ou=€‡¼áÂ?ž¼5»ËÕ½©d‡*ç.Ex°@v kÐ ‰ì&¤ØªBÄT&‰Á‘aªØf˜ ªW”ƪC€t–N åT†3ÏÁéÎãgíÙ)€¶Ï D\p¯=“a}0TóŠZ ‹só‚¼jÙ–?ÿ·àÿþOï#™²Äe¨©¹Ì³—4(7ÙòŠu5>O‰÷kš¢gîä3Yä’©¦äµ_YJø+O´”ºC|nÓ¿ÅÝk7ˆŠCà wÀ×»¦H¼™ Àû6þïë逯§“8îG£Ipç½Ë˜yoÑñÛ-qÝtìGÏñ5’ÁÎA‘ì]Ž#2ºuÓÞá„Æ«¤7‚D…í œkUÃó9 ù¤…Z>AD@t¦ˆ¼ü†ºªb6€¢­x°4]‘âb9ô·‰óêˆ_2ò"úí8êÂXtÁrw¯\GGÿ[*¥ Ê2æÇ'L§ïÜÑçmº/Ûz»-â3³è9|°¾‹FU)IxPó2®ü˯­-À:B<ù…šmrù} Ww¸òË7±0~Ëv~Šfá &d‚–ãnöœÉhísQ;-]hôjüs·ƒé¼½<zºpºû(¼ ¸”B@œY»ËýÆsµû¡ZzÊFÿ)æQT“e•&Ä•’žÇp—ªaàzrÃÁÎ ¶Öü¨éC›ðsÈE¸E ›µV¾ƒ "Ã` Í‡)*L»WÛ NwEDÎà¸ý¹ä*éZ§ô`¦ž%T/"™‚Ÿ†BÒå8LvÐ yE³Wì_’u¨zAЦJØVð ÇÑ€Ó¤‚²6mÎgG@.¯”Q¹ü Ÿ7áâ¹âÃzÅ3~hÀ—²ÀM˜ä6t/Ëá»{‡qªk/NŽ`4¹èøúåÛ+jGÂS+jf®C;„•Mÿónª¢CÓÖ*‰fÑÓ.‚)5έäÝ ‘qþž €h(²—`‚ð°Rn6¯Àë.¿î䏸 `ÁþsHCÙ †PBÝç›´ozâ²µ{ÀB#£Ëþ¢sóuUÓ15—Æ>¿3 (Òëª^$/lþ,׫tÁ€—Ç«ÏϽ| ûÒ'Ö[$¿D}­>ÜË`}€j-@urj{Ú+¦ÉÛ <´ùEü»o?jOå!ñ)!Ç ô„‰ãv tMÃòÌlÓÎ?uiÔIÀhÃÀ±›î7Br°‡žuŠ ÜÁçYý3Y:ûÉÎó¶Ç7)ž†OAJ4†Ô•&0óÞ(n½ýaSÕ¼áô®zŽ"¤Ÿ %'!61UV0ùVé~'B+WáiÅMË|â‚íH$2‡§´½,§©Èi*\ éj̓Èr¹=`i";¿š¡ãN*]ßøð®Ýå.K ˆ.ô·ÁÖ E¡ÓãÅ|:…§ößg‹ðï}„ãú´#6Í\ùÌRúðÁý-áKo{»å<±<¸R²x×V¤«o¼ )eM:ûðSO:b“+ÐXu‡ØÌ,®üòMÛù)š…{÷1Ђ— ; 7s±ÆžªCZÊ©²Œ:‘ŒÆÖµˆ|am<%%pvö#[Íåa8<ÛW :¾¸ên‡¤@Õèõí\ñg£æ>ºAÝØ~; 1š±6ç¾)Å0œ  qEÆR>‡ÑUûõGÐT€á«Ô«ÎdÊ\딃׺Y² ø|XÊIH÷g)7 ®­EŸí{ i]ÆÅäŒí!pBŠáÌÈŠÒCŸn‹ìPD$›EZQ0ÜÚ¸»·ÃdP@^n,Ù¡ø]Õ ÄÒr ÒÙ€çhÐ  ë¨/Ù2—×0YÕÀs´-²ÃzVì(ƒ °[>åefC òOg<‰×¦ðâäˆeVÕ¾žIà;ï­¨=|é pX1×'j ;ÿwùYäR´åM×1·\Båa%½ÛËÂëgës/¦ ¤EÖáu³H¤eSåfr*P&.žf¨µëa=Új –ÓUlwºŠ®õ¥au?Ò¾û,ॠc8{ìÀã\g2‡\ðºRÆVþR”2î Wþ8 *¿ùSPP›ÙTÎs§!àð½ß·Ö:j -8ún圆Šjç3U va[åAš…‘›åê%JÆ ‚FAŽZÎÒ¹wk¸N£vÉ&¨ÒÑeh²Ü´ó_{ó¼¥ôŸæëM³5‹ãæÅïy’Chpø’Âfõ„žã ½›€OÔ–wFǦ Šï_nJy¯½ÇaàäqBp¸‡ ä$DǦ¡k:”œ„[oXº´°ÂC$9GZr#HúÎÁ«þ¢ [ÈçÐïö‘"hüG3¹=Y2Ô•] Ëù-d‡6Q„PÂg M£ÇëG—‡7 í.7Rùƒ°'ú÷ãüô„õ6^ŽaêÒ¨#;9Ì™? §Û1e‰ZaÇ)•†”LAô×ïþ¥©[äLãïþÖR9{9og¨f{D¿¯¤âD½ çr¸ðÃ×T†«÷Bv Ø`\h¹¸éôÎ8:Ô_5­a­ÌIv¨Rnž]MòÜíß ¡ÚÛ½æt×Q„yO!ðÖO›·½Ù¡ÔºÉI²e£Ÿó¨&Õ¨òe-É›^0ówôöt[&<ŒdæñŒ: PÖÍtAKÑæúr¥cªð•Ò5€ì çí•U#Ù¡ˆ¡`/,}9à¶{U)žÝõÎÈobB²OÛ@zØ­›CJ‡§#‹Q µà幺 ]Dk8Ù¡ø¹•ô°U9Á%°ÈHjùº4ˆìPìÏŠ¢ƒçéê>®fÃÊÿ«ŠY3Ñ –Ò’¹þLpÀÃy F¿mc†S]8Ѿ /Nà…)Çç%/O¯S{øÃ2j’ŠC‰ÛÏ@ÉSÈçtº»‹itWÝ2^ IDAT³šŽhx|l¡­êDvÑM#›x–MS[ž•+7+©p‹[ï—«}ËaPBÛ†V¿yì_u€âC0,¦Þ‰âÂ{ 8ñ•.G«}}*áƒíÉoµbý#ªâ#Ú(‡[ù+ÖáÌ3÷ã¥ÿ6†Ñ‰e²ˆ#ØŽqÁV°ÖŸïON-ãÑÏﮋ95©<¤o€!„2n4®w(ÖIîàÖMÅtM'ÎÜÆP$ ÉùŦü⇖ˆ{ûѹ¯¿á>šúÝeL]Ebn~Çö…"‰Að®),øz:àí-M•‚–ÏrrѲÑxCïMwÞ»ŒÉ·?Drf¾)×íÀɇ0pòøe‚{£Ïɶ«ºÃÏHK–XæìœC‹brž š¢Ñ&Šð "qF¨›&²<Ôô[ÈåFÏž¨µ4½¾6ÜŠEñð®~|pwyÕúî~Ó—.££7¼!û»EÆ& ¥3¦ÓwîoŠ>/D¯Ç’ý°81…ÝŸ{ ~×_~ëdúÚ¯Þš—-Õmï£Çñ“¿±Ä+ÿò&2ËöƒÒÄð!0î $Ê¢…ÉfíoÙ¡þ¦°º+ýHf//|jë,zºðLÇ ÃËm ¬·Kv Ѩfýjµk!;.5•%é$­Ä<š6VçˆÖç#™y@^kIÓ0“McÀã¯Þ—«“ÀírPEÖÉE¸][ 5©=–Ëòrü>L¥RÀžìÒƒ—áqvïWpf²Î¤d‡"Ò²‚‘…(Žtµ”X*¥·Av h†Åò`óüelQÕ"éEQ[Ò¸] r²ºQå²ÚáÙaÃóÈë?nIÑ0µ˜B$‘«0În:Ö®¿ŸÆ8àÖÆà½,‡g|§:ðÂÍ1ŸÏ:z×ZU{˜;„Ó4ìS+Û[­½LŽKœ@ƒhhªM5ÍHÜÝŠxË5FM”åhÐ ]3àY¤³Š©údr¥ ¢›)áQG…n–È[û…¶ðŽå|/¾y'îüÎU;Sps&‰¡þ@}üêÄcï¢ĺ[üÈÍ(!;ì¬5/Á=Zì…Õp®+×"ªo •HJhó[íª<è© ƒbÉ;w2n4¤·ØPxð·ÎýtM#ÎÜÆˆÏ6/€_‘$\{ËAîð—Ÿh˜}³×n`êÒ(f¯íˆ¶îYQZ(*.•ˆÁv†®i+$‡DÇî¼wc¯G®*¼×“ÇW”"$¤{ÙhñéÙµ{ê6Uw@!œ`HäêÎAËvðe9OZ‡ að "¢ 4Eg˜€—Í^šÓ…vׯ !:4àÛëÃ\*‰§öá7®Ø*çÆ»¿ÅƒO?Öædíî•ÏL§½„îo)?¶õ„![SȈÍÌÕ™ð°‘Ø çr¸yáCKezêIGlaX¼ËÕ°öX¿…±sí_þpþ2@lМ˒ÂŒ¤0첫–´Èuá@}K¥3AvÈ­²ž¹õ¦í<Û÷…•›¡†€è6g·²CaAP›¬7Kvݨ^V…ó/Ée7¬“ƒ3š‚ˆ”Ek­Sé¢"ÃÀ6Ù(¨ZÔ¢Ž`·-K)G° Àq€¢˜·¿¬]†½|0à÷aI’†tI@dmþZ$=|sìÕšTrÊ’,V›ÐÐ12¿„¡Pa¯»z^ d‡f);l.GUu$3 Ú|ü–´(øÜ¥z?-w¾šTDÖõgjm)[/d‡õßEŽÁЮº|ˆ$²˜‰f j&Ç€ƒ °G.ñ@rcàüp['þñøWñÒ«xùÎ5ÇïŒ/O}† ?šÅó>†ð)Ú9²C•q‰á(0…x^#PyÆ\ÿup>ãñ±HÅx6*”™—·+Ñ ^ ĆòÓ/¥as>Ú7}ùcªµÝxç“9¼ô‹qœþµ'Í!à¹Z·}hHZìðä_üÒF<éçTˆ ¬³N¸r­¾Á­5©<$.ƒ =NÚ•Œõî5ªuu‡Î½¥wŽV$‰8t›"½´Œ|&Ó´ó_üŠd>Öª÷ðÁº«;dbqܼø!¦.Z²­UÜ_O'BûWÕˆ2ÁN„O!M@J¤z^%'aò­qëíÒ;é×ÞpGäpü0N'O€ÔÜ"RsKŽ•SwZ^ááiÑ­ ¬;‘H$‡ßp²ÕlS Ërí¼@Š $tPJìnÀÐôÖKË@d9„ܰ4yÁhËÂ/Hæó¸\à˜Â‹uBth-DÒrv÷ârä.¦Öw-M/Ç0uiý¼å¼ñ¹y¤-ìÄßÝbdèØy‹„‡¥[Su·KÎåV‰7Þ¾PRõ¡zD°Ï kW ™ÝzâƒÿÄv^ZðB “Ý4¶(Îæ¬4`y+ÂBvp\Ý¡‰d€VÈp.qï&nÛòÌ·º@˜÷Êä4„õvªTW–©^^3È ˶É/§NÀÀʦæaÞƒn΃yÅÚ‹Ì‘ÌÎLþº>¤‡R6šèû×—ãˆçe …;ŠìPì‹yEG&§ÂãÞúü@¸5Ù|•3dÊbÞõýÍŠ¢ƒM²Ãúï"Ï` Ó‡¾v–ÒyL-¤ )Zu"ËÏs ð©lzLvz÷ý8ÕÝÆ?ÆhbÑÑyÊD:oŸ{ §§㙯íú´Çk¿OÍ¥04¨Ï¸V!­ËÍ “Ú¤ØP¥LEÓ!+xnížéñ‘gg•§vºõ¹j-ónß}0b[Î÷òÅqœ8ÆÏ9»köõéé¹eI*ÖÈg~‰DF¶îs¾ƒôs‚z$Äöï ¬¯@ÌÒ­o“SËØ;P™ŸÉȶ€‚ʃ-ÂCê!^CÓˆ"!:´ z|mÔ(žÚ?„¸ôž­2î^¹Ž@Oý»-勌Y# ôi½`ô@½²'¦Ð¹ ~cb^ïrAÎå0öÎó8ÇÞGc¨»ü“Á¾òË7‘YŽÙÊKÑ,\½GÉ€@@@@PM";8Dv€üšºÃs·cË%Ýœ§»WÞµF§lÔÉÉ<”}ÍvY†©ñ9xOöÆãQ$–Ã@¯o«ÊCæv±%‹Œ¤š*.+­XކËÍTŸžQÀT*yïNQu  ón€ …ž¸l9°^øÙ(Îö>o7ç¨M7g’ðºYx]\kµK€œ}å*¾÷ƒ÷í_b®^4|]K@`´«zfÊRžO¯ÍW%<¨šnÛ¦6¿ˆo>ó þñ•Q‹÷Tzêhß}¤aɸAP×éë¬å<ž` ìoŠ$EâØm„ØÌ,tMkÚù­ª;ô;Z±ÚzN‹cúÒeŒ_ü ¥Õ¼áô?´Jnè=F6Í#¸· §²H/,7\Í¡ˆ±×Î7„èÀ{Ý8øÕ/’Aé¹›¦!:vJN*ÑGS¶ò|K«–ž#-[$¢ugáUÓ’ ™HõÝ«P yU-üi*TÝÚC']rºŠœ¢"£Èèr{4EÃ'ˆ.âäÁ1 öC`(Š8£…ÁPz}~(š†'ú÷ãüô„­rn¼{Þ?y¢Ïk*½”J[RFèܶ'…,ÏÃÛ´¤TK <ÀØ;-=,Ú=üD‡H ¢ßªAê8™h W~ù¦}[Ç@sä¡0Ai4‘ì`cée¸Òå:NvhRºÕ Qwxv÷c+“B`ttð®êöP&û…™ú9Jv0¬çQTký¥Ùa3„/.‡=ݸ˜œ±ä²‘Ì< Ó[캞ŒáÑŽpåúU;V|±Úh²C¹<.ÈfmÔ©ve‡õÇü>,IÒŠ@¦€ÄÚÄqÒuÞÝZͤ‚x>çq¤«^ž«œÞÄuC7j _AÝa} ~"-£#(€µ¥^~/dVH¥Ê®Fjp€ìÀÐTuE;ã¼ 5ƒ€‡GÀÃCR5ÌD3ˆÄ³P5£‚-p\–à6žäT×N´ï 7?ÂÅ謣Í}qißüù"ž¿ó(†ÿ¸ péµ/&ÓoQyhÐ}›åhxÛ8¤²ªiÂCFRðñ (À0ÿze*ٜϭ1Mm` -€n; ݆ÊÃÄB/þô3<û¿uôÍ™ªé[Æ>k(=è$ ž–qúûçñ³‹µíÒJû†ÈRŒ 1ëY»à;‹„‡+W#øúU˜ÌØPÅYÿ¿´Nx '.Â7ê>_²~}WÚxM¯ EÐx¤—–‘Ïdšv~;ꇿü„cç_¼5]Pt¸t¹%Û§çØ!ô;´Jnà}nÒi î½Û”¦AЧ‘ž[l¸šCwÞ»Œ±×Î#·i7}§qð«_ÄÀɇ0pò8ix‚Ò÷Íœ„ØÄLÉk!M`²ŒºBKoÞ~Ž´niÂÃB$ ‡Ã -§——R¨†¾¶%ÁÎXÁ@NQl*!¯^ŽyyA—4 Ðw}þ!;l¸9Ý^ÞÕÑùY$¤œå2TYÁÕ_ŸÃñ?}ÚܽeܪºCë¾äì>¸é÷­½ð^¼5…zî¡dss9ܰ¨î°ûs8fƒài܃ þߟØÎËwƒõv’€€€`[‚¼u>ƒÃ/yëLvPbmX/²ƒápy&}Þ. {º e *Xš^#<Ø©{©úòlcì¬æÙt<®ä+ç¡ @/ü0ì±® –ÑÜÌÄq CÜÐ%MÃL6>·×~€¿¬ÑþZæ˜Y"Eñ˜ ¬šDv(b(ÀÇ ‹û»WÚxéáù='ñ½Iûd[`…ôðIQéA3ß÷Ê|—4 #óQh÷#ìu[Ê»ù;ËÐÈ+z}ÕL’ŠG39^7W²¿—ƒ ÐH¦ª+.˜! lIS^•€ãhó׋Yu‹J"Çà@]^,¥ò˜‰f–”­d‡"Ú5à+Yà:LlÜXÀËrxþÐp!:‹o`>ŸulQ|ï£ßà[‹‡púéA`ŸZÙG°ÐVeŽ-%óP5,CÛßjHçò0èÙåB4)A7ñSV4Ð …@;–#ϺMÝý |òX‹ÊÃWf0üz§Nírœôp}:Í%=äèÀKoŒãÌÞG¢Æ`]JìÅúH''hu,AÐb/¬†ON/WM£©µ0ïé à öã·ï[#žr†¼Šï ãA½zZ~Érw…ÝõUYnè»8‚æí²‚äÂbSm°ªî0ø…‡Qw˜º4Šñ‹"17ßRmR$8ô?DÔîy蚆ÌÂ22 ËM#ÓEǦ1òò/êJt îÁß<…“RAEd£ $g"e¯‡Ñ—^1‹+<¼JZ¸4áaçá€o´¢a R½.i¡¼4t,fÒŽ’ÖCàXt{ýY2t9¶@ôn7´»ÜÈ*2¾~ð~üøòǶÊH/ÇpýÝßbè䪦½{å3Óåzºá µ·¬ï*í®Rv\ËËX¬£Êƒ¦ª˜|ïã’Òjå0øÄÀ ÎL¼)š†àm̽yaüÆoÙÊK ^ð¡½d ض hëó­‘Ô<†½fî]&çÞõP0p,rm¬ø¼X¦Sê}+s6¶lÝÁ¹ „ÊFÝØ¹œ²ãOÃzžâñÍꔵ²â²\ÙfFt@!HÞÃp–U^ÝÂw»®;XOeR»Ý`AÛó«jsw¦z€¡ ¤9oòœ†s}lÓ1/Ç¡ÏëÆLzeǽ¤‡aO7þr×cøþÝ÷j¡VIÚ“ðîUkñT]Çõ¥8âyC¡€ ‚Qò¸ÀÑÈä-\KV¿[ ;?³’Q`À²tÉ4Ï #H#›×Í©k=Ð Ù¡l£b~ž§«ŸƒBÝÈë?Y†F8èB8èBZRVTr寙!Ѐx IoZO„z1ÜÖ‰—n_ÃOgÇ¿¼<õ.¼4‹ç¿ø(ÂO1ÎÞ6ùGÕtÌ,d1Ðãmü|€ËÅ ³GD2.#ŸÓË( Ý B<(32+µòP˜òRWË Ч0C[xÇVöïÿr`€S¸ËqÓ®OÇOçq ÏoŽàã$*02Å™ÿë}¼;q¤X&øy²Ø#pv-E@Pg2®^Ëy3XXL£«³üÜ$‘”j¶í/¿wßø³YΧ§n€ Ý+„2n4jÚr–Jï 5E!>Ý&ˆÍÌB×´¦ßªº' 8ü•'j<߇˜úÝ(²ñDK´AhpOäpü0ÙÑ å¡«ÒóQ䢅ë‡y´íé+:D­É R³‹©¦²ÑF_þ9¢ã·ëR>ïucàäq<ðÍSì'‹ *’3óH/”'ªGǦ+öWžçAµîæÐ£‘H$NZ¹4H”ëÎëhQÂò’'„‡ޏ”« Ù¦ ªû‚!œï ÚDÑE± Ñãkƒ¬ixx×|x×Þ‚b~|žn„î/›&26aI¯{pKûÍj‡èõ@J[“!]ª#á€%uÑçEÏ჎»QdøàÇöÕ\áö‚… ¶?Öo]îwp’˜P2ë¼X/Õ Ã¼¨;üAp¼§Ë^¶…EO…ÀfDÊ¡>S²lXcZ+K5t¤5¹rA”µ@ÞaO7.&g,U}$3d‡·T]ÇÍTCþ u߃ËU `™Ú®;'È«3q+áÁ,Ù²ÙÊ”?à÷aI’ ­¨A"œ², ¬ž îgH—ÏáùÜ „ï¯b£IÅ…H:‹´¢àHg;D–±Dv–¥AÓ€®£%ÈÅï±”Œ ŸßHzXŸ„¦àq±p»H²EÕ¡é4]‡®,K¢((¥^ÞÙ$; †¡Z‚ì°ù»×Åa¨/€½>Dâ9ÌD3dmk^—¯Bì?·~ü±ìÞÕ†;w­™“qƒ€ ^½PMYJï´U¾f›œJ` É…Eä3™¦Ú`YÝáñGÀ‰¢åódbqܼø!¦.Z:_½0ðÄqô?„“Çáëé$‘`[ M :6 ]]·AN¢p¼çsCŽŠD‡ìróIJNÂØk¿ÁäÛÖ¥|o¸}çO‰šùµ¦!61ƒ|ºò3ð‘—QñwAZ¹šDÝ¡HäØÎùV5lAÊ~Ò@;9Eu¼L7ÇÁ'ØãB dÇ °µ ‚í †¢ÐëkÓƒ˜ŽÇ0ŸIÙ*çÆùßBôyËî:bEÝAôz*’'Z¡Ý¸{庥<‹·¦P/Ìʼn)dcæ¨{}ÈÙ± Aº“ïÿ™å˜­¼|h/hÁK.|»pL±¡Nd‡ºÔ¹^ª†µòVÔF2ó¶ÕNw]¹!E–E€¬ÕÛL *ÃXK_K?³šGÕlŸ'.›xaçÖ€u÷ðí¶Lx˜bˆd%„CÔ–:FrYô¹½ð²œù:¬.WÕáÁQbM²€c±eÚÈlÚQ(Ÿ¥i YŒ®¥Û—nyêFzøöµ×qVý<È—¶‘²p½P@ZVðñÜ"†:èp‹[Çœ*e»iI­ÞDv(¦Œ¥d´· ’A™|(¸.‘)ù{NÒÊ(Õí«f—‹m.Ù¡Z»PX†F_‡}Ä32"ñ,"±ÜÖôû”ÂßoE º‘0ÜÖ‰ÿtì÷ jwS{Ȩ þzô}ü«è|÷OûUû÷Œ þQ5‘h}]ž:ÎgʧóºY¬lȆ¥À°LÉl.›Añ|óƒHš†zÊL˜®/A±¿ùÂ÷1 èÀ©S»ÆY“%YÃÈx/¡DÞé D‡³ÿ|gÿù*Ù¹é6ëÓù{d­EÐØuA-ã–²Lx˜œ^Pù‡T#áþüÛâ¯þö k™tzêhß}dÜ pº7Ú Ìz‚Š¿[Qy'hIBr~±é6XUw|üakëûX×Þ:éK—›Z×âNî'Bï±C$È™`Û!vkÉ» ¥§iªŠÔì‚ûúl—¯kÒsKw¯oî¼wWò+¨9çŸi –# EP}‘Ã0ðò¢ˆn¯ E§8š¢°ÛÓºÒL& ²,Cøú}Gð—ìî\ýõ;xð«OÁjßp<>7´…õ]G¶ÇB$<¸ß2áAÍËX¬“ÊÃܵ1ó“6wTÝ¢é†(<ȹ®üòM{ã'Bí%<Á¶‡¡['p«•jb°·rc¨ñ\ֽ⼲C«@Z{ÜröîG¶<ô­® ê´0:@}¢×ZÝÍ¢²´}¿Ùé;¶HÖ•"Òšb.mzáÀ°§ÛV{]HÞÁ3òÀo%ÜL%0ì°ô¿®®²¸û}›²qT+ÇíÒé²dÝ0 iÚºµ ”õëЙ" èóy0³^ŭޤ‡Œ¦àÌØÛ8‹/áÀaq­½7Ûk‚ì°ºÞ0t\Y\FŸÏƒ¡¶Òcd™ï.A&¯nØ…¾¢-u&;¬'=$Ò¥‡Õð« 6“D4$SÊÚ±’FÖ4h†Ç®õ‰2å¸Ý ø•]ùk&;À‚ÿL(Œ-¿¼<^zýˆÄr˜Yʬ)™ñ ˜e€Q¡¼Úà gÕ~:s7Çó_~Þ/Ž’Š(¨µ”çÊÕHÕ4‰¤„6¿X“mÿæ¿{Ð:ထ¶5áŒ- ÍzPg¹åV§ÂšFüÚÊKMÃÒôLÓí¨§ºC|nã?h*Ña=ÉaàäqÒñ¶-¢cÓHÏW&ÇÉéœíñ(³°ŒÌÂrSÕ²ÑF_þ9¢ã·/ûàW¿ˆãßùS¢æB`™…e$fæMõß±ùMÅ4 ÀmÝM·‘Häiñò „‡‰shAÂ,HYø¼m¤…v(Öoòh MÁË ¹Üº\p±c>]¸Ý…P’ô@Óº‚.¸E ¯T´µ[⻳³–ýMq;è9ºQcP£ÍìLð!¹»¶vê-â§Mâæ|Ï>sᮺ¸'ž–OËy±@|òñ…±­²”‚âig_½‚³ÿµD‡¢OCƒâ;È"ïž T&Ø^ ]½–yˆ“Ó1d22<žòÏ/òyµfÛÚü"¾ù̃øÇWF-åÓ3S`œ&’qƒ€ºd}Þι*+QxheÄff¡ÉrSm¨—ºÃâ­i\{ë<'§›V·ƒ_ýâŽ#9誆äÝäiH‰x ¼×¶==`Eß´£Ç‹[3UÉÀ{­?;â)$gæ¡ÊJSë8öÚùªÁâVÁ{Ýxà›§pðé/¢õ1WÓŸšƒ”H™J?úòÏ«÷I¢î°­A;çüE+¶Ïa?!<ìXx8©¼ýÅ`—ÇƒÛ ¿ €£I@~=ÐîrÃ'ÜÛ»o}¶8w§'°”-ö·qjð:ÝÞmYŸ€èÂ×î»ÓñeL'ì•«²‚«¿>‡Ÿ~ ,ÏC•eÌ›'<„ú÷€å·Ïâ=4°Û²ÊÃÒ­)¨y¬à\=­ª;ìþœ³„Þ%ÖÝ×r.‡ï\°7Iõv€©º»9ÁNG©ˆ¼F*;Ø,ÓDY,E;l›Ñ:uUÖÖ2/-ØÛ5ë™ÐPôB¯í àDˆ4k¾îN‘ì¨1Ø dpPÙ¡x<.çÍåq««„  ò`•ð0!ÅII©’ç¼™J C×ú>eÂoUt£@0°â3d »AÉ U =ä þÍkîdÒÐ ½lYIYF^Ó°Ûç­Aí¡´ý,Mc(ÀÈbtcYu&=üõÔyü¥ö(N=Ò]RÙ£d«#Ò²Œ‘ù(þöÞ=¸­ëÎóüÞ'.Þ AŠ õ %Q/[–Évû¡ÈvÚ±ÇéìØNw:l¼Õvg«6³ëôVͤ*IïlUÒãlou´³éJzÚ3cwÒ3©ØNìØÞñ#±eÙ²e[JDY¢^”HZ ð€À.îsÿ ‰Ç½$žO•J pιçž{ι¯ß÷|{[ˆø<¦ …¤hPU£nÄó÷šªQAÀÇY;ÌŸ/XáV.Œ$1“Q–‰DYE6¡¡3äÏ.Œc—‹F À*Ù4ØaiÞ·Àõ!1ëú kùßi8á´…BæÜöÛð䥓Ug^˜Šª‚Çßûþmâ?Ø•^X™ß+Œù‘ 1/xX…뚦ÐÕæA:« Q¡¨úü8 x9p,q µ‹a( `ñ9„q‘´ ̺Ï@y¶ªbú>™Âcÿðýôv _îöp0Ü…_Üò96…КÝÿó1üôäqLfÄ’iþñä{ðp–³vÌ(vßlA, è:Ò²Œœ¦ÀÑ |<Ø/kÑ fŽÃŒ(BÑ5Óm4#Ë ‚ÓÛ47Î{~Lf%HÚ’úÔXôðÌøÇˆ¾'âÛ·| h—‹ïI±CašÉŒ„#ãØÝÑŠÀ—-‡eiø<R¢bÝåÁVÄ1Ò,KƒçéòyKˆ(f$eÞíÁ  iE‡¦ÐuMe(d) ‘7è9G”•;Xu~¨Bì°ô÷oÖõa}Ñø¬ëÃpN» ‚¢)øXß¿ávw¾O¦ûD%FÆEs‚‡Õpª2Kfa£ ޏT³  må/ç  ý;aH£Ð-º#–Ìâ‡/õáéc—ðè¡í8¸w|mP‹w¥ y±ƒ‘ïËc3óBQRkÞvë9 Šo#7vk¬LhN(¡ °x8Û°·lQ”áõVøó?݇ÿëGo[­ò 5Š]íÅÎȼAXÛ´o鮘F×4ÒPu„®i˜¾VuqÂÝAŒ'Ð÷Êëí¿´âõß>ëäÐs׿ï7ª†tl š¢bæZ¬Ä­¯yF„+”?7«9ç®`ý­»ÉÀkàã>}õº¹g,‹ðöÊç%+!14fkáH'©…«ÃÇ"BBUØu=é{æ7P³•ϧ<Ïç]¡ë“áh4:DzAyˆà¡y9Š: ß+´ ƒó"¦ˆ–vùèÌ0žH€¯Ì‚]©$ÚÜØ¥)ôm€¦V ”Ý00^(/±ßºaàÚLÝÁ8Ónùþãñz‘M$L׋£is+¿Û;ÌÑôcR’ [©¯W®xya?{…ÙüYGDW¤8;û*Žäþ½{]¯Wžw+¸ÏõiUÅé±Ilͺ=,mËÙÏ~U—¡jeÊ_a±ÃÜÿ9Y(Î|¹àb™âsC™|3’ $±Xô@¡za,æ±*v€só¯À3è‰øÐñ!ºSBô‰éÅÁ}ßܺûBíxòâIˆjõÁ7¢ªà‡ý§þO{ðÈ¿ê6hU]SD§²èé,ïlRŠlFC:©=Óå$"C!âÀ»j”0½°ó§''m<àiÁò ¯~hWMD…ô O¡oxyÙ{»ÃeÇDZRp%:³êMD ]`Ö}KV n>H°2@»» ‹C–ò|Ü«,xpˆÿù/oÿþß_´6²å)jªFó6™7kìLY£ëCЈà¡.HON#OÔM}¬º;ôØ‹þßÃåãXJTïó ç®dåv`>> pÿbb‡bß• „'Ô7•ŽÍ²èس­l:%+!~edÕÅo3#1|ô“g‘®ÞõÕiÃÍ?„í"„Pr*ƒÄð¨íñqò§Ïšv*a,[¿áòÑh”8<˜¹¯'MÐԭ׊KrtÖ~—€N€ˆV. E­‰}Í(2Þº‚óÆK¶Å@Þ…¤  4ŒØaþ"¢ðïî9ŒH Å.;ÝѰîstlÛj+ßXÿŪ¶kÅÝ¡mKOUâŠbP4 †«íEü¥·ŽÛ{`ÞL&nÐtè9뫵D\ÞÕ«ð 8;Åœ|w€X…tjþKZ“ñ_'ÎY¿6á¼ØçíÈÆl¥høX®¸€ÝzSNŠœ*«H@óœm­å~i@Ò5ëûâYì¤p0`ýzW3 ü×± ónE»Š¡ch©8¹RPõœ(Û”Ø!ÿŸ¤ªÐ ÃT>Ý0Š;ATh{_ `¾^K¯mÍ:BXøŽ¥iìl •>þ½i€_Ü?æD^¦ú碦àñ ¯âÕ÷b€È˜”¹ñ3’LãäõILf¥¢é)šB‹ŸÇÒΉ`¢œrù©…ÙBVtkya àæ+·[ÑÃL¶`ܬ„‹CÑúæû±SõYò{d“€}ÿ«7¦añ5ÆÁp~qëç°7è\ÐÂ?\:ƒ'ÿù 0ÂTUŽªéˆNe-ÏKÙŒ†TB){ÆÔ5‰)9I«Ý5UAÑC)ë׃ éð`,ù·ôg]®zΊ(>¼¢›ížBßPé«-v X?˜ŽÃù¶!b‡&¡Â $Ö"|›å,gÏEËþžœqn•ðîßao´‹ƒdÞ ¹†·@êì0•®^Ö2b<ÄX´nê3ôû>d惎۷ôàÿðÑÿ»c+"vðEÚpû·¾Š¯¼pwÿÍ7Ö¼ØÈ x¯.¿B¨ô=“+°øÙKÛönÐ,CaƒB³ ‚›:KüVYì™JbêÒðª‹ßüÇ~ð”#b‡=„G~þ·Dì@¨šÔØ&/Û—^>†™ó‹ÜÖ¹»Ã‹¤G˜œ—I45u«úÏeÉÑY,‡N­nè5t_OŽoÊ}“TÓÙ FSI\OáýkCø»ãoá?žzñ¬}A•‹e‰ò7¦C†Ÿwá›ôi©SÝ~?BX¿{'þèÏþ"Û·6|?jëÙ–·ÐtíWµ]+‚‡Ú¸;Ôv^§â¿|ÕúE)qw MŠ–µ¾bT„7)xpÚÝaÄ@>€ßÖ~˜ ø·%b¨òE¾À  :~=uѲ+PàîàZ|â\å÷ݲ#‚Q}rªÝ*m_Ómë„b󥟰¸í ï´U̯¦/I¾l»ˆi¤U "Y± vÈ—¬ªš/@VU»Ý™pqq>¯©ò],ƒ–JNcÅúº DÈ冥õ*,»W‹¢÷ -xjëç±Uhq¤KÿðÚ üøØ ÁoCb‡ù{bMÅÙØ4ÎÆ¦¡ú²4E¡%ÀCàéÅyíŠì!ì8E;€Â>hš²œ|& ½T[×RìPl¾­‘˜ÁÊï¾»tìüº€ƒë;Ñã @`òÏ |,‡#{áëÝÎYÏ¿66Œïþ§“H¿_]9#¢¥s•¦H%Óç´™¸M­Q0ßì)%‘ËaØŽà 6ÆÅ®•˜HC«Ÿz¯’è¡.¡yÐ-7ƒÝð%Ð^²EãC• „²SžÐe9ÏÇý•hœry|î>ë¢=UÍ"MdÞ æGƒjýºs æÆ©¦‘^EIBr,VWuêÿí1ó÷‡…Ñþ‹+"toÛ„»¿÷WøÊ‹GpÓ—ƒ÷{H* ek~W;B=]ð´·@ùÁyòs;œ_x÷ºÑ~ÃVx;È}g£êîDpS'è‚Ø!è¯(v˜‰!1< ][=ÇK%+áô3/áܳoT]Vçþ]xøg?ÀÇ"s¡ê~9q~©±IÛeL]Æ¥WÞ±”ÇUéýÔêr”ô s°¤ ššº)UAVSáfHlFhŠF‹Û ï"±jÇ€jØ@ýbd¢"##Ë‹p|x}o_ANU«ÞÎ]Ý Aü.–Ew¨µ¡26‡ZñÝ»îÇ÷ß~’Zb<›JÁåó gÿ^°|siXžG¸{b—¯XÊ'¥Ò˜¸2dKŒ æd¤'Í[âÖBðÀÕø"þâ[ïÚÊGÜB³¢fâ–óìóuT~ÏÜ b Xiy?œ;kƒjÊTò÷¶²SxqzÀV“nْߣ/i/«*v l´›Sõ²SŒÅàV%§J>H<Â{Ñí b8gmÕ£œ®áôÔ$öµúËÖáB2Ž›ÃëÌÕ³ØõߘF ¾pá½8²ù^<1ø[\‘âUO…ÏO^@ôí4¾½çfø¶h mX…Ø¡ðódV‰kãè ù±!ä]–&àåAÓ 29­qÄ…}mö;Ž¥±±Õ‹kqºn˜Þ†霊€›3_+¿—ËSgb‡yº4°ßÈ¢çW^ôŒ•2ÊÌ@ÒT<Úsz}!$b‡U¦Ýëkè@ýŒ"c"“Æpbç'bNÄ1)Š‹ÄÉiüÓ©÷ñú•‹ŽˆöttáÖõÝšCì0ÇæP+þæ®û °Õ¿(OŒÅðÁ/~…D­€Q vÛ[µ×®Ëƒw‡¶-=5qc èÚ^þ ~pÊFXâî@ š]‘ çÒ–óõzB&N+“l¹W^ìÍZÛ®Y±ƒ­:δµF!*‹˜T²x5~ÅruîoÙÃ/;€|¥»¬7ÊpðX:+P(=ˆ4ëeQXpN°Ó^þå«þÛṩ À W¶/§#™´¹ã`@á‹Ybw17³ b©œ+D)1Ï¡ÝëE·ß°[@XXø·ÁçCo(ˆˆÇ³¢b‡¹ïv¶†Š‹æX/-‹Wcõ1<Žl¾{½ë™nŽÏŒà‰“ocठ'ÅsŸU]ÇÀt'¯O !ÉËWõ÷pð{¹Åů¢Øc)[Ûtq "A·¹m|VæVPsZìPîY;¤³ F§3¸4šÄ¥ëI O¤15“ƒnÎ×wîÀ§sˆÜÖÁ¾P;Bœ ÛºðÔ{±Õ猻À•tO¼xÑ—í?·™MÏášný\'I5z-]"ó° xC!ÓiUY& ¶ÂèšVwb¸|üú©KÏ¡xð'ßÁ~ò"v0‰ôaý­»Ñ¹:ölGÇžíØxû"vXCÇ¿’ØaêÒ'«.v˜‰áØþ©j±Cçþ]xøç? bBÕh²‚©KÃU‹ ï™—¶¶P™Ëhþ Ì IDATåU¿1€ÃÑhtˆôsÁCóS·êŸñ\–&‚gXtø{¼‹ƒ+Ç0hu7Öê’ªb:›ÁÈL¢¤ÀaŽ„”Åo.žÅÏΜDL¬þ&ÁŲøÒûð';v‚‚Ð4b‡9ºC­øú¾[)K•ô½ò:N|Ômã ·"ÔÙa9_âúÒÖWiLŒŒšNÛ²¡6EæVš¨ƒ'NAÉZ¹Ãµl$“7@hJ´¬õÕÁ;xo>ð½Ôûçz;˜`^ð`Ãý-–Î!±ƒ‘°âx7u ¢f}Uìƒ ³§ê²ím/«Ž vVJìP.O±—‘&ÊR Ãþ¸áôE靿¶úùñ™D¹%uX^¯¡t R9!C!Šb~œPy‡‡@¡»W5‚JbA€‹e‰‚Ç.F`§|›ßùx=A_ñôsŸ×K@[1ÑÃgqh‹#óÞ)Ž'.¾‰w%™.]—rûW!}:§àôØ$.L$ êú¢àw·‹AK;/6Xa±Ã,,C¢)Û,^?Ÿ‹C$ä1_wXHSiÿMÿnMìg1:AZRæ¿Ë)¦R®MŠyÑCUõ)ó÷^x4¸ „8öÛ±/ÔŽÝÁ0Žì½ ÷wt;ÓÿÓI<öÛ·0ð¬½@ÀDZ†$›N¡UÑ-—¯k5ú›Zhl[‚®\[ Ô&.Ò¨¯@£9hÿN°]_Ň›öÞ„bý`Úï!B‡†¥Vƒ’@X{Øqy8Û-û»(:Ä øÜ}ÖEFfÌBµgÛì¨å<œÛü;7MQH#¯ õ*v˜¸:Œd,.¸ýóŸÆŸ¿ð#Ü÷wß"B›ð^7„ BÐGƒ ¿êüøÙ[ñNríý38öƒ§ fí/DÍû<¸ý[_Å~òø;ÛÉÁ%T…8>‰óW‘Kgª.kðÍí»d½Ow‡¦šŸ£õZ±iâðГE#(¸Ñé@`YÒ u@›Ç[÷uÔ  )‹ÑTÓ“ŒO!–N!•+?/¾‚úýû8u¤ÛÃëð¯o=„áüÊAA@—?ØTb‡9uoÅ7n¾Ã±ò®Ÿ½€S¿zé©é†o›Žm[íÝ(ÚpyHYI´lèj¸¶9sÎÞ 3<„&EMOXγÏ?»¢x±x=§ÅµÀDC¬Ë‚8¡ÎÅ æh\ÈNA5tœN[¿á¼8ØÐÅëäc9óu³*R æoìª+§\»b‡*ÊJ+²½q3WÖìnFeƒRÂöpx:ú1f˺¨†Ž Éxéº~—“‹§£J‹Öy¼p1lmÅPì¿émÖFì0GOÀÏ•HÀÆå s|{Ãíx8¼N j ¾7ôžþÝ à¬õw âˆh*ƒׯ1’Á³ Ö  ^7³¸œ•;Ì~v»YÛ,àpsû…ÊåÌÍ¡k®=;XWÌdÌdä’yrŠ†ë“™Úˆæ¾óëÀ_‰@[þÂ#Ű;ƽë6áȾ»ð¿lÝãLÿWjhÖÈö­`yëm2vþ¤ó.#jNFzÒœàAðûàk¯Íª‚¼»6s„8Çõ3ýÖç­@'(šæBóaè*Ô´õ·}¾uÕoܬØÁi§‰òš±!Np:Cb‡Y†Ä¤g]Þ¹f¹Ùg…¼Vݱ)mTN_(^wtÛeú!åLû;^VŽ™ÿëÝÔµª†Äk‰«³.åúr#™tåú+Êò1\A,@SºCAp4S¹mf]!JÎeò¢[nµ`åËßÝÖ –¦Ë÷‹lZþœê›ðo×ßîXµžÿOüö8ÒYscʆ„ªë˜JâÄ'ãHdß«yÝ,Â!8޶?n*9(©3Í‚‹®Jì0÷ØçBÀÃWlÏÀÅ1õ#vXòûTJª8V²²Šœ¢ÕFìPÈW2À…ÕF†ÁN þï=ŸÆ3·Þ/[ýsË¢‡‚:N&rPK½›M'çì½4ãø¼"™v‰\}“ÖÝ!)f[W8.ÒÐëU[:¸ܦ¯‚öï@£B±~0á;Ánú*˜ŽÃ¶V3'¬Ú$H°2PãyÞÆœ88¯(jHÎ8T{çí6œ¶tº8D0P jº¶ÅËÄáa%¨g±Cb,†‰ÁÕ ß"B¡vd¦’˜8?¸êb‡Óϼd;|Ž=„‡þ·dž T“®@^TôÑOžµ•—a0 S·mF‰Ãƒ•{zÒk‚ºÄå¡1aií^?Ú½¾…€B]POî….'Ç1ŸÂ¤(Z, '¦ñ³¾ðÒųHJΤ–º:Ð…Í-á5#ܹ¹k#¾wè>x= n“Ã×¶]Öï¶gÕ9xâ”é´© óA¯ÄÝ@ %9f+ßÁІâ±-«%€³åùXÞByFýîïl™“R#¹üËÐl1Y´\Ìá–-ùú1ú²º†8—¹ý¨Fì`º ‡ÚÖ‚@A­$)^VZUì×+³„~Z¬Þ^þÕÉÁ…âÈ0”NAÕõŠé Ë¥ÇH™ f7ÇšJ·ø>Á¢ó‚K0—®\ÿtÊÝaIû,ƒÞP ò *ÀŽô2Ç•Ã-[ð£Í÷ÂË8sÕ'Žã±“o`àkô\…Ê/C%Ç—@¨æ¬¬¦,¥÷„¬-¤WøÍF=‹àòñV|›Dè@ Ô–ÌT‰áÕ¿;ýÌK9qÆv~ÞçÁƒ?ù<þ9¨„ªpÚÕa޾g^Bv:i+o»;¼HzÅ[zÒk‚£õZ± âðÐpø]:ýGƒ¥ Mèÿ\€É*QÊÅA7¬½¬IHYüòÜiüìÌI 'ãŽÔ­˜«ƒßåBo¸}þïµBw¨ßuXô ¥Eœ{ã(ú^yé©é†k“ 6W‡ æÌ½ÈHŒ˜|­•»C-±"þ˜Ÿ·8´ËG&pД( ëBÀ½¾uð1üò8Ç‚ú kåÁ©í.¤[À_²¼Z‰ ÇÊT ’ ×ì¸;tp^ô -­WÞ~­ÅfúŽ•²(¾7ÊõÙÒ Õr°XAYÊÂ*/Nž›º€ô*çûS¢rp²¬X;@ØíMQeÓ¹ù¿6Æ Ç,cîØ® Øa׃6Py_\:°]˜ÅuÜçíÀ‘-ŸEçÌB1EÄãç_ÃsoÓ\™}±0vJÍ'¤N Çpa<±HXÃs4Z‚<ÂA5'ãÚ>6•65a~eÇZ9<°.¾&åŠSq$®[_Éœ wМh™8tÅú‹•ƒ¡ ù… ^4‘Ø@^ÐQ1ƒb‡bmàо\HOCUÊ|79bý˜gÏ…œnOë(2ûÃRðy8´…´yø½¼KƒãþyÜ ü>Áš©\Š‚n)ãa”þ­‚Ë„}.l^çÇöÎ ¶wÑÛ@¤Å½\àC91_X˜w*|çXÓy$Y³WÊÎøà׿v}Q‚ç»÷| _ï©þågIÑC™c"É&ů¯Ä” ÃfÜ0Ë9üŠ$¾°GG­¿ô¶³â4ÊÂë-žZo,ÁÃüqaý`ZnÉ‹ :ç]h~UêB{7ƒ ß vÓW‰È¡¡ "¡®æuÁúó÷³ýÑò×7:<À÷Yw2Ô”åê ÂÂøY TY!]‹ÛŒ:;ÀÕ¿"ÛéÜ¿ þä;øÂO¾ƒðönÒ9„‘›¨ ±ÃûÿsÌŒØ_¸i÷ŸÝ/üä;àýrP U‡§¯Œ`úꈣ®0uiçž}Ãv~Fðƒ¥êúYÌ ¤Yƒ%M°fxÀë±bã¹,º=ÄZ¹^¡)AA@`••ñóµ?Fša #ËÈ(2RrŠæŒ }BÊâØðœ‰9{CÜø“í7¢;Ô:ÿ‡ãÐå‚c˜5ßg<ïÝu~zò=œ½æhÙ±ËW»|Û¶¢gÿþú_Å¿gÿÄ._±œïÚé±ñS7UH3æ–ÖÊá¢k£u9sÎV>.Ø @hFrSƒ¶ò mXÿâ˜8¡>Äb]æêêx;ŒH)L*ùóZ“qE²îLv¸eK¾LV/½=+ý”UÒ)ÇÂNYÔÊ”•Vkc£°¬œ³îs<7uLn‡o}åý˜I"Ä»òAùE]/t@–ž/ÓŠçbl µ ™Ë!-Ëóé‚.WygÊÂXãù|”»aÔØXšÆÎ¶œŽM–L³èóNðâBßð1<žÚöžy¯Å¯:ÒGŽÏŒà±“q|?~½\y— +cĬ#Õ00OcdFDO‹Z¼‹Ò°, vÖpÂë)± p¹dsdECNÖýNÓ€[`áq3  …VÄ(³T™yÒŽ;ÃìgÃ0 åthšE5@Sù`x—@ƒeéu1Ìm»Èw4EA/wŽžýŽchkûhæo3ù¿’~+ý‹ŸŸ<}ËgÑã àÿ<÷AUý~Nôp‡Ðû.SsÍÈ„ˆ¶°,­”µÿ|Šf¾@JSósÑÛ6°ëðÐ(1Ô†‘?—Q»íÝ x7ƒi yFv†4 ={ÐeÇ·G ] Ü] …õ Ü] 4 DØ@ Ô3væÓ³ýåïs9ªª/\7VÉî#ظ>ˆkדÖfŸì((ÿr «(Ö¡Î뛑$¸$6ÆITYÁÔðµº;|rú,.¾ý^M·á‹´áîÿãd…vaH "3\Õ:T+và}Üñ­¯bûƒ‡È%T…”H!1<ê¸ÐÈ»¨|ôÓgíß÷Ñ,|mÝ€2Q¯Í7F‡H/²<¬Ž‚ñp¼>Œ®;^ö¡î­8Ô½uQuøü nÒa–Œó¿¾ýn<ß߇_wÞF³‘„‚߇PgcÖn ç\6ßv lºô¤9‡_[¸áúÑõ3ýÖ/B}m hr)J š-‡–MXÎwgp"¼w!.ÆÒŠþå~l±ƒÙÕ5,o×!¡CAZIW1”MÚÂ}’àx/áWhÝzÝ(ÒÛí+•G·$¦êUnúòaÜôåÃàýÒ!„ Äám›ð…Ÿ|—Ì„ª¨¥«Ãç~ù†í~\ \ ÌÌ…znJ"x°s/Oš`m0k2\¯õ—²ä Õ ~—€õ ;4.Öº~M3 d™4Ff˜žÄÀô$Ff˜Îfj"vHHYüæâYüøÃwp&6êè6‚‚_ºq¾¶÷–y±CPÐÛÚ†vˆLps×Füí½bS°¥v7c1ô½ò:>øÅ¯0röïnkËÐæÛ&¤ÛÄeýú``#Žlù,¶ ÎÝGõ‰ãxì¯áÝß&4S~ŸìŠ–ü&iyáÉ¡qDg2æ¶aK¬P_bÃ00“RL䜊éi““9d²*4ͰµmÀ"Òâ)¹=š¡Ðö˜ÛG3[Í_ø]—|-»lù¤»×mÀÑ{F«n!Ž9ÑÃÀ³ÙŠÇ!:•…êÐË4—àp8àÄB#½~Ýúe–+\úÔl iâª&<Ö M6( „5 %X§=-M#:û¾ãûm85è2 yŠ`ÁêÙ]ž´œÇÎûDE"N ƈ]¾Z—b1žÀÛÿô3ô½òzÍÄÛ?ÿi<üóàÀã‘ ea…h±CÏ¡Dì@¨Š¼«Ã(¦¯ŽÔTì0øæ‡9qÆv~ÚåƒkÝ6Кª~ŸC&£ÑèQÒ«¬CÖ/øßê±bÓJ]n/9B%P 9U…¢k nÚx†›åàbªÊ,Í ìñB`ɴЈx˼XV4 ŠžwnÈ;8(Tº±r/dbé>¸>Œ3±QÇËv±,þh}7uoÿÎÃqèðH¶A»Ç‡ïƒx¾¿¿:¦fÛ‘Ò"®œ8‰+'N¢cÛV´õlD[÷ƺhƒZ¸†/’Ψr_m¤³ÙÎ’®`(;³ü<¯ÉˆÉ¢å¢÷y;Z7¿NЬºDØi÷F;P‹wß©Õú y-q‡G7cß¿©¶¹Œc_¸ ,UÄÉAÊUØ=îU9EÌݨ @.W|œ×ØX†Æî¶VœŸ47^(^ Ø%—<‹Az…Ùr/žyÇgFé/¢¦à{CïàÎé øöM7ÃwƒZ¼N¨°¯&Ä‹æ:UÃ…XCñ4zZ}ˆ=åóPæÊ]6þVZìP¢²’†ùÇ#fú\ÁܦëÒ)é´ –¥Áó4·Iç‡Ùÿ.Þ‡xZF:«@7 p,€—G‹ŸMQ+#v03guà›"ð´H.ü¸/ÔŽ£÷<Œ»ßzÞ§‡wñ‹Àgỿt]TMÇd"‡H8¿Ø]Å’NŸÃ‚‡l¾¢C©ú¦¬R|›íÓqC¡çøA 4DØ@ 4#óç] |Ü_>¸ÌiÁC0 àÆ:p®ßZP›‘ŇÉA¶}­"CÏŽÂFaäfƒàÕ4@ó̓b\íÝLÚyà ö]rb./ 6µKb,†ôd} »jíêй<þºöï"@XÉy§ ÄÛ?ÿiÜý7ß “`9•Abx´ænUѾ‹8÷ìöïóhBçžèÌõznÒ£¤WÙƒDš­-êVð0.e9@…dU9MEVQ êŨçT ©œŒ Ë…€Ë¾EjPpϯˆOhLDE†8ûRYÑ4(šÍ0jâÒ`…áÄ4Ž _Áp2^“ò÷ttáP÷ÖEŽ­n"Üq‚‡oØ‹Ú#øéÉã˜Ìˆ5ÝVìòÄ._àó"ܳ‘m[á ·®Ú¾‡:; ø¼ÒÖö[Íɽ" :ø IDATÛ9ýÌKU•ÁìøäMw„‹OÕs³¾@z–=HDè"D"IÁº»©3tLË9´ò®µys†|`zNS!krªµ—>’¦ÚÒ‹,‡·<ÃÒàLŠb]Õ§/6ŠcÃW”²5)¿;Ø‚û¶îD‡Ïš¢àw¹Ðîñ#}ÙQvµwÌ»=¼:p¡æÛ“Ò"®Ÿ½€ëg/¬ºø¡{ÿ^\<öžå|×NŸÅÆOÝ!°x¥B5g~%{[Ãôq*qÚº ‰õµ“F š CW‘·çîpx3"¼×|<Í Š²ª­„+˜jÐT.†GW¾kãÝÅëê”Érm¶Íˆ”BB-°œ× :·\ÞÁÀ†Ù“¢n®nEƒStcÐË–ä1 ©\Òqµ2ëF˜‚¯‹‡·˜ˆ³^Å Ð+2_ô‰ãxõú5vL™ã?+ Šf3¹\ˆ¸=ËÓd³€ßk²¿”™3œèGžx -צ!«tw(ü¼!àE"—ÃdV*¾O¥ÊØ&£[Ü×íØƒ}¾|wømÇœBb²ˆo]þŽïÀ£{w·U³7ÏQÖÓHÚ¬ãÃt=aÚüØ¥ª&SBÃDš å8,v]Gub‡"eëºTRA6«!`óAõ”ý:Z;˜ÃÀB™Åò<š^€+ ×s¢‡»ß|¾*ÑÕTOüú=ÁðÝV¼³¦³ ÒY>7ðúY¤æÇEçì¼t}a\µápÊx@1k$èI'‚BÝÞÙ‘& Ö”ЕXàãs1üñݾ’¿‹Á€àX¸þñ?`íT›y[f¡½Ä!èSïÁP­Qj ÚÔ{Ðâ'Á´ßÚÛC´Q‘WÎ1 V«ÿ73Š$aúÚ(Iª»ºÕÚÕáÀcá¦/ï'"a¥i±ÃÝßû+"–"TÕÿCcP²ÒŠl뽿ÿÔ¬ýó)Õu+0+v å$Øìx=7/<Ø„&M°æ¨ÛÁ2žË®™ƒ Ã@VUÈeK§p}f“™ R9ٲ؀å<4E£ÅíA‡ÏOÄç.ö¥,^¿r÷Þ›xéâÙšˆ‚‚_Ûs3¾¶÷l†Ðáó£7ÜŽ.ˆj„‡ãñµ½·àoÿøóØ\¹Uùçħ~ý Žÿó/páí÷09| ª,¯Èö#Û·BðymåÕˆŠìËEC’´ÒýÍÇ ]3 åt¸—:Ø>?¡6b»ûøÙ\>ýÙ…W N‰žÿä zÂáG;Ïâó‚ªé˜Lä /ú½~‚›ANÒ“t¨ŠÃ8žÇÑp{çà̾¿0hCð@q ø6¬% CyÂGXÁGš€@ …vwÁªo|B„(Êðz‹¿‡ÈIÎ ÞÞc]ð Á›ÈA^vq.C›x º8T»³Ž<mâ-0÷“ö&”D‘$pqC/×>Ó×FëÎÕA‘$|ôÜo0Ú_› ÊÝv?n~üaâè@¨9²˜…®jy‡#-'C•D€¼Ï Ša@³ xŸBзfÚ¦^ħŸy #'ÎØÊKÄ»H‰ãÐ5}Eûz´¯ºó*ó©Ç@¹ÞQš.ýI=7õQÒÛìCkŒh4šˆD"}öÖcý¦e©áYUÁ´”^㹟¥)\¼&V¹÷p<ÂoÉ$Á,+áæä…wlÜŒÏõîÂ:¯ÃƯ#<¯í½‡·íÂÏúNâÔèµU­ÏR„¯µÞp+Bð…[à ·Ú.{Ç]w¢ï•×­×éúâ#£hÙ`Í›uÕ~Å5'ÃåóV]ŽœÍ"q}ÌúÍqx M„<5]±÷Òåë»Í»;”Äy±ƒnùR-JsË:/A „XWyW‚"å9–Î KĪ¡c Sd1m!့¼™}¾Y‡*V7µ %‡7·‚¿“bkeIª U×ÏIȨ ôÙv/ÇÁÏóp¹—7¨¼cˆ‹eÍ÷é¹í/} jY¸a Þ#)Ÿ¼vOµß „”‚ý(- ˜”²J¥ÐSè$¦ëy—Áe²½X9·ÈdªÛfÅsŸÛ<z[ƒˆ'Í÷­ÂÏ;Ò@Ì\[´p0°Oõ¶â»ÃoãŠy£1YÄ·.ÿ÷OnÁ7wìƒo¿\¡ áˆØaišh2/|yø¼ø!èAQ±Àj‹ŠüîpY Ь›¯“¥~›ÿ_ÊjyÁƒ‚ «‚¤ZŠæÒ:=Ì’=<„}¯ý7 ‹)Ûýü‡gO¡÷7÷ ÷ËË GÆÅE‚`X  ÏJ¾‹çb(•‹6(!²ö.¤uBm!"`ÚŠç,eû¸?†ÛnÙXô·äŒóÁ¹wÞÖmýT›y³¶äÌ ¦ E_³|¼m]êˆC@ü$˜–›IÃ7Ìõ©¼¢›“³DðPŠôä4cѺ«×ÄÕa¼÷ó_ÖÄÕ!¼mîþ›o ¼½›tBM’iH‰rÉ4¤dÊDúÅi<áÚoØÒôí”™J±aÍ23Cz|zE·yéåc¶ûúü-Ýjí]ô]»; G£ÑÓ¤ÇÙ‡Ö&OøQ=Vl4+¢ÛãoèÆ­¥Ø¥)¸nŽ+L²<=ƒ·‡¸:ª"–N¡/v}±QäTµ¦ÛX÷lÞ†{z¶¥i´º=DìPÇ´{|øëÛïÆù‰ž?߇󱺨Wz:Žôt±ËWæ¿ uvÀnàóÁ7+†0C¨³¡Î$ƬïÛå·ßÇ­ñpþ¡ÀLªnŽ›šsæ¡mbĺØvù@Ñäò“@ 4Z&9nOôçe8<²nGþÛî΋@ÕuGEÞ[c±ƒQ³2G¤4$½ÈõoÃCT¶îÕ+´äÅ–Wç6ªÛG3e•»ÆRr¸.¦æ…sˆª Q•ÑîöÂWD¯j:\¬u®ö—K¿Jˆ+RÏ ^Å#7uœ3b„¡ô –AÄS°Ò[&³Xð°b+Ô‚d³y—‡:;Ìý¿!àEZQ3ÅÛ°R9 EÎù ¤"¼Om{?;…ç'uË{-~ï~t ßÝÃû×]²9±ÃÒ6·!v(ü.‘•‘ÈÊšJ!tcC«,C—οÊb‡¹Ï‡ééÜÿÏÞ›ÇqÞwÞß¾»çÀ 08 H€‡x‰—(Y'-1Å¢ãk+V6ñ&޵Uö:›’ËRÞ¼o©Þ8ŽòÖî–²©ò*o)ålEõ.e¹j½•ДYŽeË´Emñx 3sOßÝïƒ3@wO0žo•DLO?O?ý\Ý=ýû<ß² µÈ<évP*vÜŸ—Óù´ôÀ 8~èS8üÓDV÷þ,úÔ©_â{Í¿…мEr ²E3!ò+øÛQƒ)îóxÿ OYÐbçú»™¶MùÜ©Hyš7(©Ëuü…¾DUàTÕ€ ø÷;øCôºOdi°<(6Lše×cøÄ²µ[é3 ƒ›Añ1Ò«¤¸QÄá{ÆjªGÐüj—¡éH C-ª\º¢ ï''qõÔû¾ç͇¸û+¿ƒ½¿„t"_¥e”Æ2އ¥TÏ 4žE Y³uVÏ"38¼âå¸õÎ9;-«LMÇÄõ!è²²ì}ýÊ?ÿ¢¦<¨®{Ao¸oÁv®0ØÈUþéuµ‰Dœ­O5ìÀÉ:dӀĬή©š†¯°OÓ8Âc°í8mX%âê@äIEÆ•ñQ¼wû&²Š\÷ãµH<Ò³ û;»`Y6rª‚ÎpV‰vµuàmŸÀÅT¯_»¸âŽûô¤ Äl‰¡ Äp‘Έ¡Äp¨"Ñsp?2\ cã黂ÎÝÛ¡ä ó‡ìϘ½ê>ƒ%îDDDkD¶e@I^ôœþÉîƒ1üÒ19Ë ;Lçg;ÜÏâb¾ÂÕê¡–<+ÀŠe`@®²’5ËáAv¿ÚÉ6© d Qž÷Ö–·Û®ÓX°1\v˜Ý)¹†¢fà|ÊC¿¦üê¶¿ÎuÔÑÑs8t«ñ-ÎËu-—Eˆãâ¸ò~–(\/Ë;E—]ä’ûü—v˜ÒÎÖ(ÃDFU«÷«Å‚Äy ¸+\ù¹ÏµOvÞCMðÁ“(šþ­p^4uüõÍ÷ð£ñv<³ó>Äï ØŽ@?`‡™ïl(†‰ñÆ eð!DHàœç½Œ°PvhmÏPsî<éBps¼•†j…#–Úw>ô`šÛðÖ¿ù𠇢¡ã©Ÿ¾»îœ #±­»iå&ò>fú\Ÿ?ïá…4ÅZÀqx òçÉŒTQÍó†—`ôþÅ P|àÁû{pê]w;¶< *¼ƒ´ú ÀS2ÇÞÛõY2ôÖ øÝ–;¨°ÑU›@n4Ël,0:3’Äéc'ñѿ΃»pø›ÿáÎ6Òˆ|Qi< yL°ê°©V(­Yà¡‘`‡³ßyÍSZ;yíû¹¡,ÓZ5}}ú9.¼ÌÞ?¨|ŸÖØÇIÏ«MxX‡J$ÆãñA 釖ÖTHÒúìš³eA{ˆ¼`i±@"K†7‘ËöIÈálbÉâò¬Fß"p¸wv·Å”W¥) á&DE‰4Ê*Ó®¶ìjë@ªTÀ«}çð‹Áë ]^¥P„R(V!‚±f°]3”m#* à&ë’ciP…;š›ðÑX%À+Øaöö )7çBvà{;>‡ç†ÞÅÛ¹!_§É³ÅQ|áÌkøÒÐ^<~çV„vþÀp²ÏÂñ—ÈÉHädD<âÑâ©¡`‡©¿)šBS„ƒ Q,Î\¢Ü§©i“‘]@÷ÌMÔXFY9à¡ _®ÇÇ0˜wÿ{%Æ×ïMµ¥4q&&r+9ù;oPR—ë/ô-øZ,jˆ4‰¾žÅÞÝq÷Àƒ2 ¬sàa%a‡rŒ”ÁýŒhm«Ñ\ VJº¢`âÖ0t¥ñ«o¿¾7î» àð7¿ŠÞGî&€È“ãÈ ŽÀPë{­ãC5Y;Œ_$°ѲÉ2Män%QšÈ®ª¾>-©̽_«#kÈÈ®fÑÝD<€È1 ;Ìükƒçið<M·ËêsmjpvàyÚ›óƒÓñH¹›^á§û=ª–÷½0>hnÃóÆxï'žûõ«7¯ãÀëm8ô­@ ,èõèî0=m˜ð@´dG!U@DDT×yƒbàØ0lÃÝ;² } ìÙ]\4ê°BêCôàü﹫ u|}÷„†¦då΃!ÀQéŠN×å¹[¦‰Üè cã Ù.§ÀpŸÿ«C÷>|7ó«àÃ2ˆjC† y<³, 4mh_“î;䆒8ý÷¯xJK`"××9YAf`dEܦjéëÓb%0w}`+/¢Ì¹ú@z`í"¥ëWo¡öfV)\eŠ &äÊÁ1,MA`XHç à0“/qu r®d!Ë㣸<6º¬lµã¾ ›ÐmYðÀ²è GH?^C p<>¿{?>¿{?N^ÇÉÁ븘J®»zèï šâí <¨…ĦÚàƒÌí×iˆÃÑj—mP=§ÿRçl“šËª½ûv¬^'w†¢ÁPLÛvT> eð{¾D†A+'9>®óýê;ÌÕ€œ«þ¥9¸™ÐÜ_Û·I“÷ÁŽwm¶å¼ü”‡sv°Ý´-p ͘lƒ*i¬ í#°ì²Ââüþ¸ °P¡ýÏsÄÑÑs8Ð߆mÛÇý· ë¸”É`OKKy[©@SÎçåt{%@–7}Ú'Y¶[¹,šn¢¤™3f“ÇΪ$ŽÄÎŒs¡±³5оTšn€a(p, Ž£õÝ©¿ ¸'\ é¹Á½·îÄ¡¦øÆàI\WÒ¾žwR+â/nüû“íxæŽû¿€h;û>³÷7,c ŒÐÚ$"‘нèñûY°ÃìïxžFk»Y6Q,sÁ'ùÍû›—×i<½Z?×zÚ÷QEàÆÌEï‰Í» &èá¹ógðâ¿þ⟛™ïã2¶u¯@àüõò¹e4Çú=\<8Pëx°mD-ùìCDDDTçyƒb®‡ó%«¹¬tû{†=Ðë¾V´ñr°?ͯ¿Ñ °XÅÐFK„"š+M^ŸÀC1Av$ Ë4®l™‘$N½ü}”2þ®xM\ˆüTúÆr·G—åX¬À#¶£bdí½ãoØ¡4žÅ©o½ Cv¿"ýOÿ!ˆ\÷·ÜPVí¥”Jzîë³ÅÜõePá Õ¯¹Ù+ÜÄÝÁk©‚õ©D"q<g4$‚9¡©h¤UY·AŽMQÈkå š¥hlÙÁ¡GD”%Ò©‰Õà0˜M#«ÈËzle±#ÖŽ‡{¶Ví«aA@g8†¸“¬Y=ܳ÷lEªTÀÉë«Îõ¡æ›÷„³”Üò@Hj±Û²@ÑÞ®Ké!÷ÿŒ%ˆˆhÕK¾}ÎÓêíÐÁñxûŽò×°ƒÇýà-?‘aQ2ô…á`‡Ë-,Äù€çá7ì°@3ù&Ô"2Æ"«šX3ù°èäŒsÁ¹Aµ‹žOy¿‚¡;;Oj鼪¦1­¹ ,ÏË‹£p ŠZ¤&Óh¦~:`i,CW¯wªöþ:?/‘aß×Á1·‰Í®‹eªðÍ¡gG`Ê×鋦Žçn¾‡[~ hUŸÿ˜"ãR6ÑfÀ¶Ë á–vpª@(´±¿îM‡ ¨Ð «êyäU +ÍÙà8lEpq, Ó´aš&TÝ‚$2`ªz½Tú{{ ˜àëœù$ÎñâöOâhò<^Jž÷ýúu¶8Š/|ø>?´Ol¿¡i•ÛÑS¾sØaþ>cycy"Ï •º>¸io/ßWfÿ+I ¤Y6¡È&tÝr]N !IÌú‚¦ô8*Ù™OlÞ…3)üíeoŽECÇ7N½‡7=ÜU¾/Ë(ØÖÝ´¼7†ý4`–Ï÷èåKÈjîƒéèÀÆõ}sm› "ªtONDDD´Üó%vÅw·Ëz¶‘&7Dpë¶» \[µÎÜl#ß0°Ãt™äaPádØÍ‘Z,!Ø]Wç›N@W”†,_ß›'Ñ÷æIßó%®D~j9a‡¦ íˆlê=kq˜µ¢FtYÁÿЧðíŸú8öþþ2(ˆÉ2MdF dó+r|¿`zϿղ­ê÷¬œ­e¹)ðàƒð°¾uÀ—±`£ª¼jX[ßUÅx†E,Ï0¤'-P²Ç`vbrX ED ÷mØ„}umè…Ñ"‘üõ¢¶@hÚõa03ׯ]™á›(é:©J¾€þwÏ¢Ýà¡¶X]Ž¥Šž]¼ØÛÑ󈈈V·´ô-OAÌSz¦ç~„¾ü>¾Ò;ùE m‡ûyP…ühŠB€åæBóöc) bØ¥(t‹a×Ç­i?Ïç<·1uw{&qÁtÿ²MjhAé”ÿ IDATóÅ2 Ò¬G@À[Ç«¹p4 Їb%‹ì9n3Ç ‰Â’€B­ýµj^5é‡÷«_NÁPbûvß÷Ô5¯+i¼põž íDÓñ9%J%ˆ ‹ÞpPU@€ÙÏ`Ë;8Ý&ˆå2Îy|û ;@É4–„ ¨›óhxlii‰òaÛ6J²–¥! eȱZ4 d—@i.ôDÇ^jêÆs·ÞõÝí^»Œ¥oà‰½x|ßV`§¼°>œàSþí£èæŒëCXD<:éú°Ô“›v1 Šf¢ ëIËèòpffž}þü9o·$Ò¦õ}ƒm Zï"QcÌ^€€ó}ÕAÈæêÌûнøÞ1wШ¥ ƒYOÀƒ¥ÁL¼á ìÐðÇ_¾¿ÿ»û±©;Š›C<ù§?À©wÝ÷ÜÒ@€‡ÆYd÷A¯ÑÎŽšëå]Ûj”Z,!—LA-6æBxº¢àÔ˯ Õ?èk¾ÄÕ¨ZØ!ÔCdS'Xqm:D5 ìï|ë»È ¹_Xkû§>ŽÃßü*DŽï7Òׇ`h+“åì°õè ÷-º0q®‘›âl"‘ÈY»ð°¾õxPd ‰4P5W¢ùÊ(2³i f&py|ª±r/ ·ÇÚq߆M艶,~£Á²è G…!ˆÖ¶z¢-øã{ð >¾…†oáb*±®œ*©ÿ½2ð€÷f¶‰áĦ0Âm1ˆMa„ÚbšÂž(e²žÓ^u”Bq"éôDDD«V–Z€šºê9ýçÛwà@¸}23·©—v˜MQrâQ¶^~IM !ð±ÍÐÆû=åóêø%º±v‡–.ÿ¬mùD†A<(ù<¢jþJÁS €\vá~u‚@Ù’¶$ì0µÝ²šZ˜G[ ü;Ìô eЦIdÁ°ÔÒe™oû À νOÞ&5×Õí¡hêø»á_áØØeD<âÑâÍRõüÝÂNúÛ"Û–B0Ä"šìgÚŒãÃPs·c°Î Ü͉n¯ÙNöý¢¼,NCQ^Àñ‡?…¯ÿ/duoïÏ;ƒß<ŒÐo™ü2ÏÏvw8wÖSTp ¹ÑÛ(ò[÷odRDDD«bÞ ÄNØÊˆ«47ÒU‡bI›,ï‡öîö𬺎íÍñS°Š5åÑð_ž=‚/üîþªûü—gs <ÀÒ`yPl˜ q¢9JݨÜgž‡ @j CjZ]ý¦Ñ¦tõí÷qöŸìkž|(€»¿ò;ØûûGHç&ª‹h–…åÓâ§4Ë"‹@ŠEˆ­ý vøè•Åлîß;ÆîØD`"g÷ÅšŽ‰ëC+ê&åìоÌÞ?XzNÓ²`åÑFn–·HÏôGxXÇJ$™x<þŸkÄòMh*f‰¸:¬_)†ÁìDrÈL Q̯¨ƒÃlíëèÂþŽ®%ݦà8t…#à´C´ˆz¢-øâdŸJ• ¸˜J¢/•Ä™á›(é:© yÊÜ™!&]!XG¨5†p[ ¡¶ÖÉ®RUOƒï–<ÓõƒÑÆ"""Z÷b‰>Xº÷„žé½¿än£²»CƒÂîU.oœB¤Y˸<°Ã¢îUŽá6ypÒ僭ЖÓp~î>àšã“ 5ËгlÛ†iͯ7E€[xͯ;Ì–jš¸UÈ¢'< œ 8§ª—«‘ÆÅ|¸AŠïFqð}OyMO]þ^ ? 4;\yrÛ¥LÙ7<ðÜâi—v˜~8 ºV¦ê;@NÖfÌ.(sÎùµ%ä5©¢í­â¬lmyMa'¾ÿ¥%ëySwwîîÀG}.WeÖóˆÊÔ4”4 ¥t Ï#ØA(ÖÒÐΫtЧÀpß_óݱ Ÿø›§îl#˜¨n u´ wÛ[@/Ͳ#!‘Äh|pýÄŸ5ìpësèÿ©û÷±;6á3ßþqŽ!ZRÅÑ äGR°LkÅÊàìÞàv>{¹‘›e0‘H|Hz§?"ÀÑ[hPà!¥ÊØ&«AW‡õ¥Œ"#«È̦‘(ä½a¾:‚aÜÛ݃±vˆ¬³ËÇ0è†ÒÈD®Ô¡­'„‡{¶xƒ™ \+S @T‘¡jsAˆIE7tNCÍÝåà ¯.nE ÄÕ…ˆˆhõIKß‚Qðnõù¥Î=Øhv ;Øö©A~:Î z­êîàú¸> ö­œï€’[:í¬¤s™V‘ž<¾b™µ÷‹ÓÄÄ,ÛFZ€(ŠËP ) íR¶2à˜Õ¨¦¹hTÓDNSÑÄ ÞË<¹”ã‘™rpêp ™À,÷úýÁvœ-:yd[lË5 üÐB|l3´ñ~OM]Iã…‹çñäþ; ±xÌÛv)“( NS@4 д»ºðs[µcP J€RB=aY7aÙ6xšq ;0StÄ"yoi)Ïu©ÙÎg`dˆ`+n±€úˆk3n³ôDÇ^iÞ‚çn½ãª_ºÑÛ¹!¼ýë!<6°OôÞ9>Pæç:³ÿ6,‰ŒŒDF†È3èŽÑÚ$θ/ì@Õ–¦ ëHLȰfÕ¥nZÐe EG¼EBHâ–S« v˜Úw»¤(àƒ™ëÅáö øË=÷â¯.xƒÄÞ¾‰#?éÁ/…‘ÉkˆÇêü|ûó`ãÙ3§½]ÞÅ8@“EÊ×O M~/\-Iª€ˆˆhÕÏ´Ø g\¥¹Ð—¨þŒ«Ögá²ïïÁ©wÝÕ¶:¾¦ÛÈÃLý¬¦<œÂSúÔc;]¶‘¯Ï$DkþÆÔ4ä’)Æ&jmi(ðÁ2MȹYjjêªçôûÃíx¢koùC¥—g^kQa‡ªî ;Œére…ù2f^$^“ÝßsvT Fµ—,{FW=µ‰£:£XÎ\'Ú¤ ¢‚ˆŒ¦L×Yˆãâxp‹”tÕQ»eUexXÂuÂÑ9:g6ÀÌ=ßûŸ7”<˜@óôg!¶F!µÀýÁ©^¿„mW¢8²·³ì ââ/¥'hˆDšZ:írÁS’D@-Í MŸaP€<d$q hT`ÏæÁs4U^pÂÁqæ@³¶›¶bQG À‚ž]ïKÇ€8åö ·…sìó[ÅÒ7ðÂ𙺸=Àé~¼‘î/ƒ;v#~ Hvõñ·L°Ãü}ÝĵD×’9´†E´FDÄ£’³ã;9v`ݰÀ³÷±lÃã%4‡´EEçç±`‡)=¤i¸>s]}vï}xkô6~>zÛS¿}îܼøÓÃÈü®Z_àaÊݪÁÝÞIn¶§§_‹ÔÁêmvº¢ú–g(ÞŠÇþæiĶ÷±µÊ Ž@Ï@+.¾€(+ð`Åòïl|H‚‹BŒ¬Î…úh–AǾ;PÏB› j£á9߯°¡4žÅø•AX“1Rb$ )ñÕ}"?’B~d¬!Î77”Äé¿Åu:>Àg¾ýçdž!Z²¯G'VÔÕaªŸû;@jq;P¦Vmä&"Àƒ"Ñgë\‰Db Ÿ°¿Ëw[.®K—‡µàê Î%oãòØ(Å<âÁòMvO´"Ë¢#^ó@ÇÔQdyŽ{ÃjPD”°#Ö†ýÐr·ò ˆ–KSè™±¾˜Jb0;Lƒ™ A,"CÕ0zõÆ4ÀÀI"ÚïØ2ýŸSb)w""¢Õ&Û2 {_ "Èpx¦÷þḚ́0Âv±@î%÷ñ(ªþûUtw¨G€¥§s® )…e,“½øçEŽ­˜D†]d•zÛ{ͯɋc´IAõå¼\ÆTÐc°C”€’Ó@{îyÛå̶‰Íx;7äîÞJÎÌ@êÚ‡Òàû°-opû #g°-ð›Ø¶SpÝG§¡ŽB¡ÅÓ.7ì@Mþ/ …ºÀ†iÁ˜óÄ$©Ù?´WXñ¿E\Þoii¨YNÔL¯*•ŒèÁ ì0ûs¯Zv{ø((sÝŽ4oÁ¡¦nMžÇ«cõ³i~#Ý7ÞíÇc—'Á‡= Ðbºïu€æÿ=VP0VPpm$‹ÖˆˆîX!‘[x<Ê^¼ê ;@¦ U…f+WQR tÅàXº6Øa¥U©,ŸV% 8óåñ ½'Ž"«»VKÊ%}ÿ2þìc{€z¾ÿõÅÝ¡CV䛹 çÎUÖ`¤ ˆˆˆÖî¼Aó ølmÜU²ó}IÜÿ± ¶Jõöìîpß ò0м6{˜™þÀu›ÍVSXÀË/þž+Ø6uGÈð&SÑŠi6øéì@°9ºlÇ•sy”ÒY¨Å⪪³³?ü1®žzß×<{¾›¬¸¾‚Ê Ž {sÄѾ†ªÁPË×e%›Gîö(ÄHm»·€fWgLV A ¶z¯E†¢!Õw}Î6%›Ÿv®`b4 )õ|ž™a”&² q¾º¬x?üͯ®[ØÁ2LhEZ¡Ë0Á‡«ºß×CZ¾„ìPº¬¬xY|ƒX Ì]_v ;Ÿ½ÒÈÍ”M$o‘ÞêŸð@”)¢†Òš²î#,ˆh‘V÷C‘bxùìi$‹3&SAþ•‚ý#¢„¨ B`9Ä'ë;Bár0ÊD£)YÈC1ô9ç44(†1çÜW“j@`Y´H:­¨vµu`WÛÜý/¦’H• ÌL`0[!J:y©^éaûö¹>Ü>×¶4OÃöï/IÐd™TÑš—’胥{y¦÷~ÄùÉ€ðù \­ØÁ»ƒ§ãÚu8—êPAÁÔ‘©ÔÖ•ŽcÎ_SܕۤæyÁ¶îÜ ¦‘­ò3Îb°åv»yUªÝ‹X–/Î,C;ì'‚Ž'7và%œwÕÆf) Ä6ÏÙFs"„ö; $.z¾ESÇ7úƒ!´Ñt=.e2Ä©I¸ÀiںÓ͋Sšqñ v͘K›…nNPÀ4E!&ñðÜÒÇ™w^ÓNóî“§¡‡ š¢*–qÑcI6pOæsƒoB '»îÆ¡¦n¼0ü+\WêyÏ6ßYv|ˆšî!Ž:Á³·–DZF"-CÄ›%Ä£ˆr÷ºê¥ñ,ÔlJ&_ѽ¥cßöUëÐâ§,ÓDîV²aú¹¯°Ã½_ÞàîÚÜØÀqwðYx šXÙ7©†Ù4 1k¿«Ò¶`¨zË*Ò|Øa)e'A¸2¾¸ÅPG0<§Žs‰˜r’¨¦d1Ũ¼Ê¦bèHò³>¯^ˆa)uÃØïBO¤ÅäPnQQB€ãÉŒJÔÚÕÖ]è˜ãQÒ5 fÒÌN U,`0›FªXÀX©H*lRʼn4úß;ƒþ÷ÎßÚïØ‚H§û¤hŽ@PDDD«GZúŒ‚w‹ÛÇb›q(Ú]þ°f¾1\àîàé<–v˜Ôœ÷tœ‚é~¥ÆÃôdy<¸1L­¨pMõPð3¯Éïš^ÿT‘n™àh¦j^K›B,ç­Ïs –çÜÿhn©•ÝB¸¦N…”ç¹%©ñÔÅŸáEñ·€6ÕõX˜vz`Y@tàQOØ¡ÒwÁÏù ;€>Û¾yr{ˆç r,ŠºÕ0AÑÏ0ó\Ù‰a©ãT9¯-±&`b–ÓÃTÏ¥fœLذìr_f¯·TP}—ÄtàJÈÎBxqû'qlìŽ&Ï£hÖ/pøt?ÞH÷ã±Íx¢w|h6v˜¿¯¢™-``´€Ä¢;Dk“–¥ý9^­ÎÇeÛ+¡«50=¸ÉÃÍõ˜Zæýš,à·4àõ™ß’wlÀ_î½uÞÛÊ›Ï}xÿ¸ÑOÖaøâîÐ*° °TØêÚ JTýN©ð°©;Ц°€\ÞE0‘¥Á6ò ØðšêyæèÏ<§ ‡Ê°ƒ€„ˆLEË­ÔAô½yÒQÐþÐ…‹ºp¿>ñ#Áîúì'q×ç> !ènÁM]QPLg!çò05mUÖ[f$‰S/¿"âU±;6­ëÕÖI–aø‡I*r¨RÐ{Õ65MdF¦"Ag_z ¹¡¤ët{~ï±uViEùÛ£(g—ÛJ&¿î‡üH ÅÑ X¦Õå¹õÎ9œýÎkµgäv L…ëLx B"‘ø0¢¾†ßž5ªÊè „×tÐŽP<ìús9›®+° ïlš bÚkÇŽÖvôDš=»1p ƒ)€ˆ(¡(R©D«Nޝè”!Šº†ÁÌR¥"Æ&Ý!Ö»+ÄèÕ½zÃu:ŠI‡#""Z²ÔÔÔUÏé·JQ<¹ñàdf˜ <¬ìP-|{XÑÝÁñE¢z¾•÷óI`ØÆt¹¾Ç™¥2ðÏ€BÁ¨p²\n 5æ%0 o£tËš<ØžðC,W¹Î¦÷«àî ™3ÀDDRwÃÚ–K-€þà.Æw£4ø¾g÷˜ëJÏ]üÏîBî_è]JO:=´·‚°róN¥ºçx€åŸƒõMË^xŽÀR"ˆUú“KØaêï--MàY·sEh†Ù0`Ø,0 6‘.CÔ$ç!pˆ¨ùî•$ÚÀ¾"‫"`ÎÝùñÖ8Ò¼/ ÿ o¤oÔµ ߘèÇeðáHûf86«‹·y=`‡¥æ>jfž,(:.ÝηÖ&ñ ­Mbõ¼—vp©Ä„Œ-]³ÜBü¨+/×?¿÷ÛnWàú̼ÿìÞûp|èΦÝCb×óY|ëŸ.àÿ9tûøÒo–»Ãñ~Ïîtdßä(> [Ëp°u€H=4Ø=>Ñzœ7(©ËušþÁêï)³9‘&ÿßsg§Þu·Z¹­Ž¯)àÁLP“sÅý«#v SQÃË èPIj±„wÿ׫øõ‰×qÿ>»>·8>åä K«r˜Òpßeœ>vº¢ú–çžß{ ÷|åóàÃ2öˆˆ–Q´ÃEƒ-ÓÄø•›Ðe¥aÊþáK¯!qÖ} ööO}þé×|Û“ãÈÝuµÐ,³nÇBi<‹ÂH †Ö81K+ ; ïîo‘™Ü_àhJÇ|½!FäâšÖ샙 2šPÁ0z¢Í艶`G¬Ýs>Ã Ì ˆˆÒšp#!"ª¦)âž®…VÔSe¿/•(»DdÓÈ«*†r$Pˆˆˆh5˶ ÈÃç<§2žé½¿ÔnÃ=ìPù¼X¹¼žÝê;8qw˜ÜgL“aX–³ãP¬™®ÉîÁëmb3µ9X"°º`hÎûOÁ*86T×t›P¼Â 2LH¤BÝM¥åæÇn“š] R*ÍBêÚ‡Ò­_Á¶¼­@öFæâçƒxbï@Èt7n¨2ôQ5ìÜØ]†jqrð2¿Ì¯ûÙß@.[%ÿçm3V`…£®¦ ²Š†k+TÅ‚ Ñ ) 6€‚Zv™èKÎë¶]/ÿwEsWû1<žÙx?o݆Ïàlq´®ç:>ìjÇãñí8´3ì•«·‰×#§`Uýº5–W0–WÀ24Z#º[ƒ‰Ü²Á!‰…¬ÎêfÖ>–m£¤3.kv˜Ò§UàE (Î$Àáo~½ÜMÆ_)‹¢4îýÝ<Ͳu´Š\!ñ¡€£6#!Dz:—ž7e™‘†‚úú>†ÞuÿÎqÊIf-«˜Gfp†ê®ãƒš6´¯»ñÒˆ \ùáI\ùç_ÔžQ °p…ÁFn¾$ Hæ³H´*Ñ”ÞBƒyC‡l˜µ×]×ìYE&£©%ôDÊ€C-.ˆˆæk †¨ä QÒ5œ¸Å4(äP+*0V*0 M IDAT’Ê#"""j`)‰>Ï+¯À“b[ ¹üav²SØ¡þîWùmâw‡U;ÀRpvœ ßL«œñ¦»óœµ]±Ì¹çç' PÍ&Ø'p‚¥iÇU4½/ËÔ|â8Œ©J…ý{ÂÜö9ìÀÛ¹!WMlRb›+ÿ „ ´ß%qÑóÐ~iô<â—ƒ8²/p¶ëq“(•€[Cع©{®Óƒ‹ºõv Pvx˜ïòPìàûßp¶2_B€ç°1Ä­lqAUÅšãô [rІ&‰_º¾gÿ»Cz࣠P˜;¾¶IÍx~ë£øen/ ŸAR«ïsÈÙÂ(Î^EÇÍ ž8¿‡¶u"´Ï$ÛY¢Ö·°Ãìt†e!‘–‘HËyñæâ-Džq×·Üœ €HˆG¦¨A_ È©¯ª[IX[°Ã”þ üÏ™Õ4·á/÷Þ‹¿:ï>À§ ëxö§àùGï:}ŸnÐ@®|bÏž9Á‚·@ ºùž…ÕÅ6Á¶tÀZßn’°Mò0²<Mª€ˆˆˆÌNng¤NØyw×ûó%+Åb}VHß³»Ã}kÊÃ@óÚh#sôg5ÕÝúÊýdx“©¨a•Iâô±ÈŽ$}Ï{èÂE|çOþ |ùˆ6­¹ºÓþðÇüÕ9ßòœ <Žmï!c±ÁÛÞãœêjex DˆEÙÔ VäIE®±B1’sÝšø 1†‹BŒ„œ}YÁø•AX+°øM5Ýzç>zå_=Í9Ÿùö7Öl»—ƳH_¿åt(÷™èºš‹-Ó„’)4$è”ÝK¼= T#ì4¼ÃÃq2ãû/¹JH$Çãñx@CZ)Œªòštyh–¤5;@G(ŒÁlš ªe–Ÿ€,‹° Ì‹r "ró Åñ8rÇ®ªß§JŒ‹HMBƒÙtÙ%"3’¾v(†ü`DDDÔØÒÒ·`Æ<§,¶Gb[Ê9;ÌŸ(ýžx—g?–¢±-­!¿•ƒË¨à˜°mc;?F…í]E”êêÆàýÜ«ç%ºXÀ€£iÔâì0µ-Ê òóös±`”ÈÝ‘Xj–®€æÄÊç×Ô ³”žñÜþzè]„˜‡qhO ÀÚ®ë(!—P¸Ñ½=`%Éuݺ“KÁÓ7Ó ïÁå¡A`Ù0 @ÍrûçK0í¹ýY×,ð"3½©¨eàÁ)ì0=°làîâ€Ë"`ÎÍàPS75uãhò<Ž]BѬïóFR+â¯ßà CŽœÛ‚Ç7Ýø~èÒ—®C¿a7× Pt£y Œæ’8Ä[$Ä›%° íóõ  i ]­$&d¨ºé®Ÿ9—+ ;Ô¢& xX~1ã\òìÞûp|èΦÝߣýíųxêïGïS\íeûe9|Ï_8ëíò/vï¬ð Š‹–W\¶×q0º½ÎúV.©"""2o¸½nó­ÜÍ\èKXèP7àáθû–ÕÆ×DûXŰoÏÓÿûÏ~Á@íï+Î÷%ÉtA¦"ß•IâçÿðèŠZ·c'2ø×ÿ÷à·ÿ¯¯Å5Swº¢à­xÙWPdû§>ŽŸþ"øp€ŒÉÍ2è<¸ ZQ†e8ƒèY'C)¶½R, ­P'ò"a×íSÏ"38ÜPç•Jâ£W~ì:Ý”›ÌZsŠÉqŒ]ñ¶¨#¶n`SÓQ@i<ÓPÏôõVVðÁ·_ÁøÕ›µgæì@™j#7)ê ÅJ43išæ; Ã|¢˶‡Ç#į=«îžh Þ¿}“ ¨z×s¤¡0z¢-è†khŠB/÷É Çƒ[c Q£¨-B[ „]¨Ìû,p 6¶1šV+iÎú§÷Ë¿{Ð]:p‘ÆfÎóèýâ®×¿ç)»gO¿£çöÞËt’&›è©w‰¬æ-h‘iþØ"I`Àž[¿7äÄáÁï %U@DDDæZni¤.×in T_®XÔ ú\¹©;Ц°€\ÞEp¥Á6ò ØUü¾Ý¶`޽í9ùŸÿŸ‡ ò¾´G.çÞ%–»È#SQU üê,>8öÚ²KWT¼õ/ã·¾ö•5Qw~ƒ"|(€ŸþClÿôÃd\®ñA‰TÂ*V A ñv-J¢0:ÑPç“JâÔ·^†!»›øPŸùöŸ¯é þ‰·Ýß;±,Z¶l@°#¶¦ÇÁ”›Cqtº¬4l9KãY|ð÷¯ 7ä\èì|ör#7íÙD"‘!3½ÿ"ÀÑ´4Mû@’¤†Òš öf‚iÖ€š¥µIe£'ÒL\|TG0ŒŽPñPÁ2äàËÃÇ!ÀóÄň¨´«­ BÜÓµ0@îb*9Ç"U,àæ*škõÜôÜhNÝ.Ò Š&sÑÊʶ ȉ>Ø–·@¸ Ãá™ÞûbøòK9GîkviÝb¸†ãÚu8Ûq^ µ´øq–8þ5ÅÝ58ÄpÞÏq–K÷¯_ÌßnšÀRàs }6&P2t¨få Fe„Ü{‘m,ECd(¦é¬ÜSû„u@ž©‡Á¼‘¹áú¾g1à¢xã”çù§hêxêÆOð<󛨶Cðà]Ðu|88ˆ=Ýmnv_×Kµ“Øaºb\¸<,ñ=ËPÐM—Ç÷;€j ¶‹‹­±&\ŸÈ•&eY€¦ZàÚa¿ƒ6Ù%½ p1d˜ys'»îÆã­;q4yo¤o`9ôvvog‡ÐÑÄãg¶ãHo/BL f:ëoõ€(‡ýœÆr ÆrÊ$ü Þ@4Ä»ï“>Ó…xLB@d1š‘aYö¢é9ŽFHâœõK¿îêqo±”>­ß§ï©4·âë;÷ão/¹wVxéú%vÂ7Ø!oÅcóôºYIœˆhuþ„`"}}j¡ÔPåÒe§¿ýŠkØ|ú×ô¼Svbq÷„Jˆíè]³P“©éPó%(™<”l¾áËëæ©(Ÿ` ìðÐÀ:JfìúˆD™M«T*—¤Æ½PŒ*2º¤àš¨ëǃ¥é5Û—~÷λðòÙÓHód`¹À²ˆO ¡&DÑ7¸¡ÜïÊ€CãàˆUÑjÓ®¶ŽŠÎSnƒ™ \K"U,`¬TlØó°tjê*ÔÔUpMà›7‚B¤‰ˆˆVDÚxM+¬?¹ñ ¶M9×ÌŽÓZã°ôJMe ÝÓy¬,ì X †V“³CÑÔW lª—rŸ×‹´U¼äåàÜhŠBO8Šq¥„œ¦B·¬éíÍ¢ˆfAMÍ;C¦åyµöÇA±L8rw˜~0›$&“jêv ˆg6Þ':öâ¹[ïàlqtY®·I­ˆ¿»ýküÝí_㱋›q¤}sÙõa» lO}×wØa‘4†e!‘–‘HËy­Ñ²ëCHâ<Á³·59øö+È %]§Ûþ©cïïYíꈡ_rŸÈ¦N°âê¿oÒezI…!+«p˜­^ùWôÿô}2óv>{¹‘«îl"‘ 3w}D€¢Ùº¬ëú» <¶U^Etµ<šYjGöw̬V‘QäiøA1 $ y¨†ŽŒª «Èkæ¼§œ–C<†È²è†!²:Bõ±Š¥) A¾ 68Þwx‚¨²Jº††o!U,¯ =?蜈h¥àø²#D[ÇœþÚ—JN;A\L%¦¼¦œ)g ÷ƒ‹néE“yŒˆˆ¨~²-òðyÏé·JQ<¹ñ`ùƒ…àÁÍjú,jñòN»Z¸†\yØÆt?,úÐY£CÁÐb9ïyUûÎ4pþäåúümÏ.‹m‹ò¼·´Èͤ}¨©oç†\¦áx®©–®@ï÷Ü¥æ@»çu>oŸ±B\¹†==› –lßœªb¼$O»uÌ«œ·¬4 <ÚCAÐ4U¹LóÃrÖ±p‚ÀÑs™™:Âxdd º]ɱÀMÑèi ãV¦€´\zb œM;;ÇJûPKŒµˆëÀM¸!úÜBx>Ô±"àCR+â¥Ä¼”¸€ý—Úq¤u3uw!´Ëº gççtlû;Ìÿ¾ 븦è¸6’Ck“Xv~h‘ÜÍ9“ŸišB[TD,"@™tzàXG;Jïëul%a‡)}RŽÍ¸ä<±eŽ^¿„ŸÞvÕ³¿zo½õYà°Ã¹œ-ÿvûÖÈmüíA44&ö«$‚m)€©a½É¶ÍeãNWYÍ* """óÆ ˆ»`ጫ4ú*¯ø¯ªF]ʸçθûÞ¡®2àav°õ,¬â OYÜwÏFìÙ=SW~o¿3è: ÅÇÖ× "SÑ’Ò§^þ>teeW&Nõ"3’\5.º¢àô±î»âK~|(€×ÁêêDD«Y¦¦câúPÃŒøÒk¿zÓuºÞ‡ïÆáo~uÝ´có–nh…´âÂØÀÕ :˜šSÕ'¦¦C-”Vÿ}Ьàƒo¿â©oW¼o½÷|ƒ€²ÃC‹¸;ÔQ$’Œhæ¹Ó¶@àDQüz£–qTY;.ëQQQBT”Ðm©ø½bHÊ«˜Í† ¦Àˆ) fÓË^öžHóôßS €i˜@]\ª‰cˆ,K‡å¸A Va¨™×®©R'®ãG×.¢¤Ï}aÞâO8\µŸ­”{º6Îr.¦’èK%€°tjêj|hÞ¾y#ˆˆˆê"%ÑÛòöÂ9Èpx¦÷~„¾üòÎ-ìÐèîKÀ­œ„(+4ìà^†m!S xXŽ(;œ%Ë@œÿΖíO^®Ï¿>°`#Är`i†e¹p=Ñç‡Â]–®À(¤À†Ú–ÜWˆm†­+Ðs#ž»VEèÁ è0o?EÓðÁµëض¡ Ý­Õ2…rŠæ( =§–÷‹‡C ËU-½$“`¹Øaª_I<‹¢bÔv˜ú»=,a$/Ú=ØsöÙØBHTÈËh‘X`Z6†rW6'ÎSÿnÒÊÿõIÀ0· ¬$øg £8[Å_ßP†vÆm3ž#µÄuk`‡ùÛÆr Ær ®ÝΡ5"  Zºªð™¦(DÖñþ¾_;v€. Ø`·gôì¾{ñ?ù'×Yý<9Œ·N&pø°Ã ³ãåvËh*þíOþÅó)Б}-¸¿TrÍ°ÍÆY `ù~P„AD¢‰ˆˆÈ¼Ñ¢¤.×iFSEŒ¦ ho Í}V«“Ãæî¨ûÞbäK[5n¶ž,VÞ[0S Àãë2@4‰5—ëíw–¥O‘©hmëô±(e² Q–«o¿‡=þÙ†¯3]QðÖ?¼Œìˆ?ÏK¡x+û›§ÛÞC:$Qƒª8:üH –i5dù>|é5 ½ë~¡ŒØ›Öì4Ë cßvänBÍ–÷gE~U€–iÂ(©04¦¦A/©°MsM€ •”Jâô·_<áÏ} ÞæÞ¯¬ä[Y9 ÊT¹ ðPG‘2¢9*•JÅãñø\#–oT]Àƒbè$@½‚D–ßhÛÐÌòKmÍ4`Ù6LË*ªLÖá”4ÓDZ®|#Á04x†GÓàŒ‡F TçŽC€ãäxp C:LÝÆ§’®A1t”tºi""Šè G0˜™Àë×.áƒ×«¦+ñŸOþÿõÑO£-"JÔКïñÁð-\L%ЗJâæ ÀeS²-Úx?ôô-°¡6ð±Í 9‘4‘/2 )ï¶·On<8ãp0ƒ¹RÁÔòï·-mœ`HîCu_V €QG§AÊÝ9 ­‚ä2/{é`_Ër^ïË;ð0;†æÖY”0¦Êîû&gz¹Ý4oÁ ‰P4uW§¬MÞË8zŽï‚©æa©Ï]iI§ÇCËÆµÛ·1V(`Ϧ`é¹ý?-+eØ¡b{Ø ë˜*CM¢ŽÏ-Ý^JŪùÍÏ» é0aƒ¡(„x! A‘…¬K»< zþnþæY£Ad %M‡nÙ ) ôHËH¤eˆ<ƒÖˆˆî¶ DY|ªõ³×↹¾Oêó ð‚4ÝOwlÀçº7ãCîqŽ^¾„Ãçö-¼þ˜Jeøè‰“o"«y P¤ø˜æy¼/`. èùõu“n¯ç(9!HDDDæFÅÇ`kîn ¤@ÙåAü/üàý=8õ®;·[[Á÷¶žL¶Y‚-ßò”Çg{×G‡`À‡‡ý©•LEDÓºúöû¾9lîiF0Èc4UÀhªè)á¾Ë _g™‘$N;áìÐypûoOƒH‡$"j@™šŽÌÀpC”ßzçœ'Øð™oc]Î?4Ë ÚÓÙ°}îÿgï݃ãºî;ÏÏ}ö@ƒx5AR|Ó"ÅWdÉ–,ZfìTLÛqâu”ÝdfSÖÔ8ÉTJS‰R;µÚMœ?fk*›©OÕN93ó˜lÕÌÄRcylÉ9¶LZYR’â›!€dãÑ@7úuû>÷Fo ïín œoª·ï9÷ÜsÏ9÷õûœoÕ©ÁuÌ\¥ímV¨a¥v=ð—¯5 °@hòb+WãP*•º FòæID\ -¥ÓÂÀƒí¹¨’¼¡+8W.Ó #K[˨Ûtœ‡³V!kÎò¹ CÍ„ŠB_¼¶À$IBWš†.+„TmÞÌý­¢ªÓô‡ŸphŽ ÛÆ°-Ê3Ÿ ݪzkè·&Çkž¿hYüÕÀ{üÑÓ§E% m(Íu€/æ¹:>Ê{÷‡yÿþðº”Çsm¬éXÓÐÚ· ðAHH¨!㊑º8ý3‰œéÚWù§¦Øð9oþ6ì°ò›Ê=ávŠ`»Ípwð; AÆ*ûNS9Þëqϰô>.¹«åSK`·m×P/­îì°ØQ¥;žñé@ÀÃÙÛ×øç÷ ö×–_é¶w+Çæ›ð7CÁÛ½Òuª¾áDmÃsŠà:[çBݵØZ‚BBBbÜhuI‘~ßÀÃå+)žzr×¢åF“€‡cG’¾׸ÒâÀƒgeÀ®—uwèí‰ñ[¿qbÞ²PH]|-îSd¾ç¶[y³8<ˆ¡¨þ{Ý© WÞüIÝù|ö3ûù­ß81²úî÷¯òŸ¾=@±èïˆe”¿3DϾÖt:È<å'ÿñ/±ŒÆÌä|èKŸÞr3« m˜GŽ3ãê0ÑÒå ^þXÀVë(«d<¬bǬ|õbðá_ÿm ˆgÙû©þO û‡M)¯ž½ÑÊÕ)Üš,<-×ñþM«nÌØø.®ç2šÏÑ‹/š)±õË>ë¼0ûÿl`Ì\04SžçQ¶mÊs‚yBº¢ Êk TÁ†¨¦Õõ–„06ºŠ–‰é8«Â UeŒïÞb`ôþ¼öR«Þ¿?LÑ2‰jº¨|¡ ©žhœžÝqžÝ½¨¸?Tà‡Ví?M¹ÉàƒPd¤®à¹v ´}zŒ—ö|ræ"˜Yàa¥õYm€ZØ!,«ìŒ´(lëÀ°„ÃԀ²5ü8z+¦ÉÛVíyI^íÛ÷¼ÕËÕÒ°ÃÒ›Oè¡`åMX0={-¦sŸoàÀLN®mÓ²ÚXèÁû…Aóð ;Te[—éîîæÑ¾>dIª’¿1¤rOfÑ…™|‘ ð°DÀ¹ëyKÂsÛVÞ´È[í!öˆNÞ°kè3¿K+Œ«+À!MáH_‚¡©Ê€Émº;Âtw„ì°šž±à² 3ñ:{bí|mߣœ½sÍwVßà›·> –€òü¤C\˜œàÅwÎ.²Üq¼!³&KZ¯œfKɵ@Ö6éΉè@!!!1nl4Iá~È^ò•æò‡©¥¯g &íž}ô±¤ÿDv‹»H9ҰC=îÿøkŸX´l¡ÛC{È[Ò»@Þ ï ÅPÔp½÷ÊkuîG£:üÏNsôÈâþÿ•Åî´ IDAT«_@^Z¡+Â1àÒÒàCRñÒ®§x¡ÿq^™¸Î+×)8ë÷¼è|öç³÷`ö_NTܺwp ¿½?ì²üµófíËüŸš,‘š,Ö’]Q’Û"„u¥µa‡õÔ',87ÿÏ"ððò­«üósO8°ÄßÑÁ†ŒYæô÷ÿkð& ¶¡t>јý–C èà˜ly°™€!($$$Æ,)Ôå;ÍàÐÒï•lÇmJñÿþ©¥JÇÀ33ÿ êîpôHß’NîúoKÍÝa E’Þí;Íø!øœ¿4w?`|p(p9÷îîäÿüg¿8ÏÕa¡žzr¿ôÙƒü÷Ýlúþ4[w?à½W^kH^z<Ê/ÿù‹ô?~!¡Zd• <ÛÅuœyAÒf®èo|Q´èì„¶Û ÐaѺÁaXàŒàvÖâ8Ö(äm“° (¬´Üõ@–jÏË÷~úÌk!îvx؆õùZ‚³æ·ajöþí¹îGùwÞ÷]Vf˜PÏÁš×—Cñ‡NõB߸ûÿ»ógŽn‡ˆ³J=®¼Ì.¹3r¬,³¯k!U¥VØ¡r/­øƒ $ ª ~/Zöâqg…í§Ke¶G£ÊvíÛ¯å{°ÃÜÏí1zÚÂ\NMaXÎbØaU何×9^„ãT†ÕE]+®è<ßwŒç“Çx}ò/^fÔ\Ûû€…º]Êp»”áìƒËÄ®hœzo''Ûz9ÙÝMrOöXÐíÔ~lýo|Ôýi ÓáîƒwäH´éá‡ZÓ7ý|µùÖ¢_°áïU(T Ôå!kš|çÒ]žÏí…¶9û»z%o Nÿ;dÍà€ÒóÙ ¨Ð¨j×:ñœQ¶Š<ÏBb£|ˆ`e!!!1nlIj’Ú†çÓáò•Ô¢™× …æŒA<³E׳2óÿèîð[¿±ôœŽpÙ<„7ð°Õ‡¢5pà° ƒïý0pú½»;ù¿ÿ¯Ï×îüÏ_=æxÈÆó'B»]_ý;6NÑd l°#‘ ¡Èòªäš,Ó­^÷ Ë5}ÉßËóI¼U·ïz(Þ¬ËÃjÛ¯å{a‡ªâa'éæÚX–‰‚±üñð ;H>ש:>\ÁÖâqfÛ>ÎlÛÇ…ü¯L\ç|vdÝÏ÷Çâô o¤á.ô]Œq*±ƒ“í½œLvß.Wˆ§ööß$ØaáI&orkdšîŽ;ûbÄ#ZsÏ[ë QøÑ^C‚º<|óêÏÿì|~&èð] Tœ|žëM&ƒ»ÉÇ?{¯¤€»À–»QgßÁÊBBBbÜØ¬’"Ûñrþ€‡KŽ.ÛmZ;ÒLJWü’^é~k¹x.ž5 Þl=¹…;²:z¤oQýWÕóÑH†á{Yÿí(€[ˆŠ6ŸnžË6 f4ªó¿ÿLÍ.%_ýÕ£üoÿÇc:Wûö,£Œeháõ¾÷Cnþì݆äÕuð¾ü­?yH.´uU ¶KVÑh© ûjYªŸ9fŸMh‘0Š®¡EC•ï!mC;C¸Žƒ‘É“0¾¡fàŸågÿú¯°KþÇñC_útCa‡©;#Lß›·LVUâ}Ûèxd;²ªlÚþkæ ˜ù¢po¨S©ë\8ûZ ö¼¬"ÛP~áëHm;šZvµ4Šlf[¹z,ZXó%€¡¥·Tên2™N´bùð ÔRéŒÃCHU «ÚC·‡’eQ², ò0T–$ÂjeØê•aEC™™Au«8?TÝ€‡0ƒëz3Ë4,”aÛ e'› 9ÌÕ™ÂShk)ªé<»{?ÏîÞÏx1Ïë7¯òÖÐí¦õé¹ò\#usj˜pÏA”Zs…„„¶Œ\ËÀš 6#\LÑøûgD® <ÌQËõozg¸¸¢Õ˜_‹ÀË(o›þVçJvf>(ö·³ Ë™nHžÿ}¬.wÝÅå’Õ^=ÿÎsÓV ._%¯¸¦VT Ç®½ÌÕeŠ•cWtNµí⌿À‹*¤éÇå =œ»Dêç^:ñqè.×¶ï˨]W)‹Ü³,Rù<»ítÇb‹ÓÎ|—%‰þ޶Õë~©ÀrY®@ÖüþÒJ–C-°Cõ»$I´G5²«¦õk.ãÂövå™g ŠÌÑþNF2nO×<_Èàù«{€GË•¿ÁÜÕ  /ÚôÉx/'㽤̯Œ_çõ©;¾¡ fiÔ,ðêØ ^»·*ãɶÞÊ_€xÌ„˜ë¯^j­ûç*ÛqIM•HM•ˆG4vöÆèN„P¹±× vèw¡Ãƒì¬ËïíÚËß úÊf`r‚ &9ùù8ÜVàï+ãû7?àì­k‹'©m(O4e×%µ Ï).AkmÊ+÷ RN($$$Æ­"IïüÍ zùJŠ…¯×róÞG;’ô<˜é–<3 î(Ùµp ƒòZÎÝAQeBuçß t-'©mb(Úâ*Le¸òæ[Óÿñ?;ÍÞ=µM²WmçGKò³wüµÙÌýQzö­¯ ÂÏ_ù.C\lH^‡¾ôiNÿéEe• Ì\±$/4ܵa-÷Ã*‹\"Bñè F‹„[ê±J…ÑIŒlnËza‡FCÅt–ìÐ}ÌBiÑo®m3}oŒüè$}Ç¢Ç6v|Ðd™ˆ¦ÒÛC–¤•·µ ë`›ó–‡U•’eû‚I"¤)D—¢á¬?ì0÷' Tu¾SÆÎDŒDTçòý) ÛñYž…ë€æþ¿¯ ûË0©ÀßG ¸ø9HRñÂŽÇyaÇã¼>u‡×'ȵÔ5Áíb†ÛÅ ¯ŽÎ ÞêåÔ¶~Nîìí6° êƒqžŸó¾dqí£ ê=¹âúÐ;Çõa…|Û£³1M×™=öª&ŽÈDb ’$Õ1¦¯“ŽÛðÓÙýÿÃGOú^¾~oþäi¸ª€/߼Ƌ«hJò È¡æì·$ƒ+Ǧ—c¶páD„ 7¶¢‚@——€ÊMŽ>–„WüµN»u®+<+»ÈåÉ- ƒç?°m%w‡x´þ÷¨çß¾  µ€+µŽÖ]õÀÿøkO.Û®—RÕâØÿÀÃz«‘°ÃÇ¿þU>þ;_o É1-ŒLnõê¡CÅäìL窮¡FÂhÑ¡x 5BVÖo¶«dPJg12¹ °^ì°çÙ7 v° “ñ+·W]ϵm²Cè9²oÃÕuµ½”sE¬’!µ«)®€Ôÿ ”cÿpÍöCÏÞhåj°ÃIB+éÇ­Z0Ûs…˃ÐÚ0• Ü +Š/À¡Í"Âø³±Ÿë,´ E£ù©BŽ¡Ì䚸8,TTÓ8sà0¿~ä„è8BB3šëúðê•‹¼ÿ£¦/v~;?Þµ½s’,.y…„¶ªœâÖôƒ@iO´õò\ßÇf2bËÁ" TI®q»^ö¥q°ƒ‹áÚõ•mŽbŠÖ ̃ ¶ç’·-âê\„:aÓ„X„–‚y±óD§•d$ºxjè'ª š V¥/$õÏ´ïäüôˆï£m¦ 'ýƒÑ„nSüáåóRþ“8ªû«×9u£É2ÛãQJ¶åÚà)LJȲÌîD;ÛÛÛjo{+ÁPŠó—'ÂÙr×óVN;ó]“eBÚŒSGDÃõ< Ó­)í²e\Ø^¤Úö2ù‡teIX"ÒxbO7·Æ¦IåJ5–gá:uÂÒœÿ·9ð¹<”$ø0£ê’²ŸÙ¶3Ûöͺ>L¶Žëüþ0€¸ 'Úz*î=¢ß†åÚÇ<¿î5þn;.©É©Éׇž®sò)äl ¹¥Ç ÛrÉÛ.…¼M¼C#Uê?o¯¥NÚð®3‡ätßNtv305á+›ï|t‡o^=ÀS÷øGçÞ¬«XJ×33³?7O’«¸<Ìùx³Ês@RZ¥0â†JHHHŒ[\’Þ²®?(oðîdͳ±×«cGúü·ÔòDkT°S{ñ;O·p'Pv˹;Àlx= <„×ÁIC Ek"Ó¨-³0• ÄôH¿úEÏ‘ªm½£ccMÖHØáô7~—C¿ò¬h¤[@F&‡™/nè€úFÊ6-lÓÂÈæÈQ9×/„ ”†¢kMÙþfqÕ¨ªØ¡ëà# u˜±Ëµ_‹ºöÆxvä:f®Ò7¢óÇFQ1åÊ·ØpWùè?@ÞñÉ5Ûµ4Šlf[¹ºð°VmATÐrJ¥R’Éä°»Ë'€¡f© 8„U°ª.=åÒ\g‰Í¦¡Ì$CÙ)R3Nëá"Ðñìîý|áàá5wÛÚ(ê‰Æù'O|Š¢õ?¸y•·†n3Q,4u›fz{ú¡žƒ¨ñq„„¶ ÊéÁÀi_ÚóÔÌÅ+¼(ܼ°C\ÕfÝ-VÕ·“·Íúʶ@ ê¹¼>@a>ðÐØÀó;¬´õ:;(Ê|àa©¶\CyãºFXQ1{•2,‘wWR³Ïžëz4ð`M?@kO¢D;}§m8ôpóM^2žæÔ‰NЂ.U%"Í –®ƒaY\Ÿ˜äÞtž‰6’mñ•õj°ƒ ƒ¢ÂÃýöЙþ¶(÷sEÜåÆâ9ß{âáyËÛc:ŠbS0ìUÓ†¤÷ÐC2EÓFQ$Bê|øA•dÝž »-̵T{)ÈcÙm7v˜«¨OÎP(WC0¬ƒ±xŹ®ç²Ã¼>9Èùì½–½nÈ3çì}fˆ¿íåTO?v¶ÃA ©ž ;H«§ŸëúÜag_Œ°^ Ÿž²0JΪÛô<Èe,J‡x‡Š®Ë 97®‰>a-py8Á?zÛ°0”ÏñãÔ=zˆ¯¼ùýºŠ#Çö"woþ~K2’Ú†gf6ÿ¼g¯#ð ¢…„„ĸ!´Äù>Ò[¸ë+Í¥+£‹€‡rÙ&j|hÆ3Oïñßrë_±®…gM/^\§ä;»•Ü€ºëþ£‘ Ã÷üXq ÃÑúHÒ»ðÌtÍëgÔæjÔÝ!ÕùƒßÆwºŽöà C!“¡gCƒ,ÃàgõmÆëw£ÐãQ~ùÏ_¤ÿñâAoR¹Žƒ‘É? Z]KA²"£EÂhÑ0ÒÌ÷ª„Þ]¹Ï– <ÛÅu¬’S¶pLkÖqb“(}cˆŸÿÅ·Ã_þÖŸ¬Z—¾Æ·XYUqkˆ‡’UEôa!ô.׿÷VÃ]P#(Ÿø§Hm;ÖtB“[¹º³©TJk$<­¦ïЊ3JÐ.Pý «ÚCÀAW” 8lFeŒY£ÄPvŠŒQb4Ÿc´°þÀ‡{úÎ^/$$T›¢šÎ¯9Á¯9Á[C·ùÁÍ«|”jÞ³ePº 5ÞM¨ç²AHh‹È)Nᔂ…}­ÿèl°ÿ²“Š4vhŠü½ù<°Z@¶Tc¾k‚¬èúà‘_jfñ5½{?_8ð(»ÛÄÛÂr\—L¹DÖ0°]—6=Do,Ž"Ë¢rjTº:>Ê«W¸:>Ú´mÙù œb½k/zç.QùBB[@AÝúôÏ÷›ì—[«Éoîî௼ÉPŒ„ªa» „jqw;Ø®¼lAÖ_QqcÈÛ …ªy™ø±µn&ì•@wÓ¬vØ‹W€)@?i·ajöå÷ó½Çyqð¿û¿†,efœ¶j=„ÎŽ]âÖ;S¼ô±'‰ïqV8Æ«À½Fl " ÛæÚx5=E²=ÎÎŽ¶Ê}x ®ó¤éP^<˧¦È$ã•HyË¢l;­ ¸VUaM]þú\Weº;BäJ†éÖÞÎWrw¨Åí ìÊ•ˆÅæ×‡ëy¤‹eŠ–ÃŽDY–þ¦J2Gwt’š.rk|ÛY® ÞòûP+ìàwŒìràg€¢ ¸§‚½0S¸¬ó\ïÇx®÷c¤Ì¯Œ]ç\ö£fsÝßêUÁ±8Ÿ¹ÇùÌ=„¾÷¢œúñNvôpêàöŠûÃn{M`‡…JO–ž.Òº¶o £(rÍyZf|PuU•PT M—ÑC-zo»Ç;•Ùêzˆ¯ìÚÇÙ;×|eñŸoâzu\ÏÉ:Jò ¬i`>[ÃåÁ[  CD ‰qCÈÇi?ÜËû¾Ò M®i>–ô<”'Ö xð¬,¸Ö˧}Ͱ_UoOlEw¨/àüÛwý_»E¶7¡òDŸÜHºuþÝ@éz{b‹ ©ZTo;vv¬YÝ4vè:ø¿üç/Ò¶]8½o‰i¡õR+ÂU…;â„;â豓wîÍs{PC:m;zißÑÛ2uYLgE^cMßã½ÿ ÅñæÅØy¹{8ü‡5ß7UU‘»ºZ¹úð°–íATÐJJ¥R’ÉäÐ’(t­Àƒí¹¨Òê/é,Ç™?#à* )¢ µºdI&¬ªó„ZS£ùß½~¹%œ–Rw4ÆæÙÝû‰jº8`[X¦có 7M¦làÌ žÌ›e2F‰C]=zð©Ã=}üIÏ/7|ð\›òøMìü8áÞCÈ¡¸¨|!¡MªzÜ^ÚûÔÌ Am/×ÊÁ ðzþÞ„ª²Ìh¢†í¶ìPc¾»^ËÖ5x£ìPÈXFãa×­³Ì „æ-«v+ ‰PˆŒY^½Ÿ,̫Ĭn出\Æn¢Æ{ä`ÏäPœØ¾OQþ·œ¯»éŸáë¦ø¹OsàÑhÞÒu/ùhc¶BaÐtl×e$3ÍHfšxXggGÝñèòÏjn«:YÁ nq]#®ktÕâ1×8D–hé„C.ÃÆ²Ýùë4vpñx0]"‘QTiÉí”,›;éýQ¢¡ùûŸl’ˆ†¸56=ëö°ÒøÜ(ç–Õ:ªú…üVàj¸ò¹D±’zŒv>Î ;çViŠ×Óƒ~5‹¼šºÉ«©›Änͺ?œÚ»ø#œ4ƒ×©ß1¥Ti³eÓáÞXž{cyzavôÆéJÍÇÛ¶\ì9qg’ѸJ4¦ É-dýð¬ýxuÁ€Òõ ’Þ½öû®DA)‚c²iåÙÍÊXÜ$ ‰qC(Ø­z¤ßwš±ñcãù@³³ѱǒüà‡×ýµð`ACä`/}½ïîÊò7Ÿ;¹âïpÖßî)³@%À~,Ó%ÈXeZ¨Îã¾P°l‡¶¯&Àº Åúa‡ê²d4:x|Ô{‚Éú]<ׯLê9|¸Õ‡N€F­xóM^(|œ3õC›v¨~÷<0JøARq2È—M®¥a=Ìt;·U(,}Ž9éä…~(8ç§îq~êvŽÆ»yì]|eÿ~’‡ô üÐ$ØÁu<kñýéxÆ`ås§×xxæ)ÿózå‰uèfžµÌ%®…WöeoOŒÏÞ¿â:á:‡Ë¦|;hÈ`1­c?—C¾«>ó`”Äö¾%»yþ],Ãÿ,Gô­êX²œêuxX 5vØþøa>ÿÿ¼(`‡  hQLg(¥³Ø¦%*Dh]táìkŒ¼s1PÚ ìðÇk:ɪB¸#.ú°Ão_äÿþ!¶QÞÔû …Z¹xvXc àA¨½L‹¶ç.éòàâ1Q*̼ìm®\J®MɲÉ Ë2ñPˆ˜˜~m1Y™çà ŠYÕ7¤~xûZKÁïßÅý»xv÷~qp„jæ*g–Ù.ª­.­ø`¦±óãD’G„ÛƒÐf·ëqwØ3ãî°i`÷CaEeg¸­¾íÞçæÂ«l?¤ÆÝS¦ÌŸØÇÿ³PšSè±n”hgð.4=©+ØùúƒU ŽÅŸ ¿Ã¹ìN^úØ“Ä÷6èͱ¡dW€‡Ð,ø0Q(2Q,ÎÂñ‰H˜°ªÎ‡¬$@V*aš;Ìý®ª2íª”-Ûñ°°,gþúµ8:,hSª&ÕU¬ò*ðÆœÿG§Kìíi[2ßî¶0OÅz¹5–%5]Z¾=¯ì°\(WþJ\ AJcéLÂrc¼>9Èí€ç÷µÖåü—óü—ÔubïhüêëûùG? ì¶W?×ÊcätÁdzÐ$¤)ìèÑ“û>Ç{ä2VÙ¥½SkJ>àÀ³¯6¾²kÿöÚ@Ó7+·} ¥óÉõÝw9оÉ]\4©ˆãƦ=Rv¬Ü2§ Ïôw?$É!$½kv²àÿ…뇺}ƒC“ÀÚ¼c:ú˜ÿàh¿ûÓãhe*çù%ÔÝ᳟9°ê:íõŸ{gÈÿýºÞ²X1­¿ô.(Þõ•Ä*K/7 nžÿŠñ[¿q"PºPH%›ms ·Þýs#a‡C_ú4§ÿô÷D»ÝÀ229Šé,F6'*ChýÆ¥’Á{ßú6é›;uÌÀ]‡v‹>,´¦~û"7¾÷¥Éì¦ßWUUQZ{²k<¬u›U ´šR©Ô…d29´äú~©°x(YÖšÀKÉt]&K%,×!ŠˆÔèg JnÐE›H×Ócë^†*äðDÿ.¢XÂ?èPUH8Ë4LsÁ‡¿xï<ÅÆÏîê–ó‡?@ïÚ‹Þ¹KTºÐ&5 ”îó]{I†b•ŒKÞJlnØàÑØ¶¶ë5a_6޳CݲåÅÀC }¬(c–Ù‰×ÝvÊ®CÞš™}§loRÔæµo?Ë4­¾ã¸Dšd$ÊH1/Ø¡ª%\‚ÆèU¢»?$¯kIV‰ôÇH]ÅšnLÀÊùé¾~aŠ1ýi îúj—Ë~wl(Ú ªLJyPà üP(ë$"aD4\qP5p,Û ú}æ3¤+„æ-Hà8ŽçaÙ.¦íbÙî’é烑ˆJ$¤.P^¥Ìùßr]ЦM´:3é‚üUYâÑí ’Q®¥2–ÓZ°Ã\E=x|&(¤Xüp ÒÉs½#ËÞã\f„ ù1 NëÏVp,þÓƒkü§×xæƒ~NmÛÁ™_ØO˜s뾦ðÜÚÆ½²åpçÞ4C©<ÉmQ¶w…QÙ×öRøi èáSÖ<àátߎ¦’Þ…ÒóÙ–hW›ÞåÁ³@ªuæ6($$ä{UÐÊrMÜ ž™Æ+Oà™ip×ò“Ô6PãH¡nuäpe™O]þ0µfeîh³kG‡oÏÎÚ·@=Ïγü̳nw€_ýâáU׉Eë{çwþí»þÛQ-îb8Ú´Zw‡®mógÿhÄ?ðíìhZØAÀu c“b&x¡–ÐôÈ(ξÆôH°qi+¢¯¿¶èPU$ÒÚ±·©TJk,<ÕªïТ.cå¶çV^~Wo˜\gÝËe:Žh5 Pp«aUE–$Q)›PëáîÕ4÷$ä ´HAAE–鵉Jl°÷ôño¿ðU޺ͫW>x®MyüfÅíaÇñº‚…„„ÖW®eø}¾ÿØL&K޳_7)ìЭGH¨«–mLØ!o[dÜÊ}çÉXg¹ä+Ë …QNötùØ—ÕAÞ¶êÊËÅ#U*ÎÏHOMDHFã‹ïÁü¸;Ô½lf%©2Åx-ik8Ö;ãqFй`í·c¾ËCRñµÞcœ»hÌ2Óƒ„zÖ=”„“‡‘CqÊã724Z~çúë|mêÏ?]f}°ÃÜeUÇY®@ªVùœ³N¾l’/—™yné$pH(×5šâoû`‡ùŸÞ¼eŠ*¡ ¡ë2±ês ðÁv\\¯²®¦J(ŠŒ®É(ªô0‡ýªV(Ù3Àà ý Óyj/wÓ9F¦ ØŽ×Z°ÃÂßb|¼Tù^gá‡ÒÒ‰+:gºör¦koe\Íq.;Â…ÜØ†p8?uŸóS÷ùï^à̹=<·ÿÉ“:nð ÂÆ3Çq¹7ž'5Y¤§3L²+BH«Ú7JŠ*kk{¦v¦+;û•]ûèÐt²Vs"%½ µÿ×Z§1mr—ϵäP°ëQ!!!!1nlÀ‡9ÈÁÍ^ªëÝbìع‡.ïÊgphŠBÁœ7ãz3uô±¤࡜^àÁµ–uè€ØÁ)ùÎö³ŸÙ_Sý†Cõ]»þà‡×ý_?†ûÅp´dÆ’ËÖÚÝ ·gþ„(Ãb‰¦Õ“€¶¶Ó"wœâ Ðjm¥o ñó¿ø6v©(}×ÁGøå?‘¶í=›²~lÃ$?š¦œÍPšÌRLg°Š¡ö8±¾m(º&ÒZ]o” FÞ¾È7ßÝR CU¡P¨•‹÷7¢…®½D4—P­z™ÆŒý‘ØÃÿ]wýŸˆÀü` «ÚŒƒƒ*¡†ë‘ŽNŽôô=„„O yä§}ƒUcObMÜÜ5KÏîÞÏý»øÁÍ«¼~ë*E«±AN)CáÎÏ'£Æ{D… m@…VvwhØÁ—‚Ý ˆ&V)c‹Ã+ägã6æ¸4âxÙèA·³8àßpm Ç!¬*òºW,PrìÅil›¼er/?Í®¶ŽUòk2ì•`xÛZ=m@DX‘éG˜0JÁÚoŸfg•y®ëQ^I_ 4Û¼95ŒëF‰vÖݼôÎ]ÈZ#uÏm T~vìÞå¥}Ÿ$yPžïö°°î$ÇB¢25½e‚=°++¸EUg¿ç‚ë’ÏgÉ#3iUY&®k$":q]kQì°\`¿®É躼tÞ ÒÄCã£ö1¯ I,YæÅåÛÓÝF²#Ê­±i&òFã`‡ÊYSÿ”–?bn~(Épeeøàd[/'ÛzÈ;&òcœËÜãBnŒQ³ñîpRÁ±x5u“WS79q©‡3½{8óÉð´ÿ—¬AŸç*ÇuI¥‹¤ÒEzavôÆj 9›PXFÕÖÙuõ€3Ïåá+»öqöε†oæ!ì ·Ö˵Míòà9 ¹¾ÚÊãÆF‘›½„3õÞš»8¬•‡&ÎÖ®*ͽv:öXÒw`¾gN@lO“»£‹g­xí›çîÔqw«õ*†£ #IóÿdŒÒäcó–­‡»C(¤ÎkçÙiÃ7M4ÇÝ¡‘°Ãéoü.‡~åYÑX7Ì\‘܃qÊù¢¨ ¡–Ñï½ÅÿöÓÀé»>—¿õ'èmÑMW7…Ñ4f¡D~t×¶1 %ÊÙÿlE5_?r‚g÷ìçÕ+ùéÐí†æï¹6¥û—Ð;w5d¶e!!¡µ•ØÝá(à³0J°EÞJÖŒ¬¼{"í„—r·i*ì`©ñû¾ô6¼U·‘Ôý_WÞ*M=thìPUÆ.“T£¾óš¶Ì¥a€ÇÂ’c3m–i×Cë;ÈÒêikÍ&ï±XxÒ~Ãh.X•k¿¸¢óBò þìÞÛš1z•èîO4ÄeJ÷Ý¡tÿ"®e4¤[ Æøú•×y~â8ÏÜ Öâ:‘Œ Uuët¬Uó´]—ŒQ&S "¨:„C„5…°ªˆÌ~¯©¼uÀ+¶Á%–kªLO<< =°zy¢!u‰u¼eËÖŽîì$S2¹ö ƒa9K—Ýì°ÒºõÀ ׺ðÄœþy) Ã*åe‡è¸¢s*±“S‰¤ÌrcÿZ€˜g`zœ—‡?äÌOöðÜñÄŸu!^¬§h’$áy^àóñxÆ`tt²;±MB” ÝÄõ{èbÜ_sõDãü“'>Ågvïç/~ÎGÙ©†æoN c§ˆôGÖ¢…„6€ìüx àÞyî+i½Üš ;¨²ÌÎpÛ Ûmìà­M]J±üÜ®à:¡x0ó|7©Ç|3ï˜àI …ò¶ D}ç•_è˜07Í¥¼eÒ¾¤eìÁ ©`šK§ ˜wB‘…ȘeÿíK’ ÏÖû™Î}¼ž¹Í@aÌwÛp-#u…HÿñÆÜ{‡âDw‚Ò½‹8¥LCò,8ÿîÁûœ›楃Ÿ$ùhý´8" sD¦\†ê!Íæf¯ÛCaUyèÖTâº:;˪Øa©m/@¬FtÆB(ŠDjº´|~3Ÿ]%´ÜXv˜»,Õyê@/#“î¦sØŽ·tùê©ûFÂKå}Ü€j÷øH«Àc*XËŽI=Æ™®½œéÚ Tˆs™‘‡DG–fj´\äìÈ^yp“ç.äÌáÝ$I©8_¬"="S.9uŸ3ý€–éâØÞb‡‰µVÔƒb¥ §ûv46oYGíÿ5$½»e¯%µÏßDWô^õÄ(nn„„„ê»ji¹¹ë8éŸmZW‡¹º|%œX“m}ÌÿÌðžñ ÉÛ»°J{ÈõÙϨi½zA“sïÜõÛ+:úF<£orãóÓÜ»r=»CoOŒ§žÜ¸ì½=ó߇_ºâ0HlolЪ€¶¦Šé,ùãØ¦¸§j-¥®sáìkuÍ’¿™a‡b:KîÁ8F&O9WXt˜«òtÀC£4=2ÊðÛI]¸Ni2+*dF¡P¨•‹÷7©T*#ŽÒÚKDç ùÑË´(ð0f̺<¨’L" ¼Zsç‘%Y&¬¨hŠBDVM 5p«zkÛ ­£>±c7©|Ž‹£÷kNÓ±;±ÝéI²;ÑITÓEe ­‰âzˆþ¶vâzHTÆ:ëpOÿò—~…×o]åÕ+­Æ=DsËyŠCïNF÷ˆÊjqÙù‰@é*î¬ìîÐò°Cð¢ˆ$‚ãMÛßEòüïÇŠAÑ^cêEò—OLÑ|ÌÞ2æ8<Ô\ÖÕgìΘfíõ>'/Ç[¡»^e¶}Y¡ìÚµ—«®eÞòëÈòÒéê)ö´µq!ð%‡æBÌÂì}î ÛŸàwn}?ðfM?@kßÞ˜aFV‰îzœòøMÌ©á†õÞÂ_¿ô:Ïç¹c{ Ëô ¬ ;xa‡•ÖÍ—-òe‹‰¢±h¸®¡*2‰ˆŽªÈ(BW«ríAþKýVƒ‹D{D'¤)Üϱ\wÉudY¢·:+©OØaîçήÉD„‘©w'òƒ|ã>a‡…zÄ‚Ý3coQ†[:¤˜–—c©Ïõâ¹¾C•1¹˜™çÑ*DÁ±8;r…³#Wøü…=<òQ’¿¤®>hÛª¸^¨|0J±¶u~½°Ç+•2쉷³;ÖÆP!W¾v¨”S5 vqƒ^Á/Óf=!!!!_ã†Ð†‘›»Ž3þã-³¿—g‚Žëu¨Eíavíè`øž¿)ÏL#é]Mè®.ž9¹Jƒ°ð ÿ3Ñ÷öÄøÜéý5­[ððÑH†ŽKá~ÑÙ7âø4ý¡ï4 ‡+ÿý­@ÛþÍçN.w(¤.jçA¢ vxøù+ß°Ã’„ZUVÉàÆ÷~ÊàÞ­+ŸC_ú4§ÿô÷6çùÏq˜¼GfèžëúJ'TÿØ9:pá·/2=2**dt]Gií8Sáî°NÀƒPÍJ¥R’Éä°»Ë7VžÚtU–(X&eÇF¦(¨’Œ*ÏôhŠ‚,Õ SÍCh‰EVæsëYHh%u„ÃüÑÓ§ùîõ˼5t›‰âìŒ/ÝÑ=±8»;:g>· ¸A¨) ) ùUÖéŠDéŠÆèЂ:sà0ÏîÞÏ_¼÷3޿߸ >ϵ)Ý¿„Þ¹‹PÏAQÑBB-,;ïvÛm½Ë¸;l4Ø!XÐG\Õ*û¿ìv›áîЂ°C­ ƒæÎÎwúšÍÙ`ZÉg¹(o›³Ž‡AÀ‰å–;àÁrÜÚòjô²ù7âµS?®T\ŠŠáØ>ó›Q_ c玙6ñµÞcœ»¨O–Çn¢„ÚCsª õDÅ)ÝÄsí†ä9Ïíaÿ'I’Aw×vhò–dª³0ÎÙVÅ B©8DÌù Ï _ 0¨@¨~†T…ÝÝqÆr%¦ kÞo]%ÙA›ÛÀÕOU‘ÙÓÓF2åîDŽT¶TG¿bùr4ó<9÷÷˜ ' ¨Æ†Œ+0¨Á˜R‘VFDˆ&Z€xcü.ç~tç®ä¹ãû‰Æ[ >ÌEÚUJÓöòÐC€ã³ø`–]bmë\IΧ“;8{ûZ}ynØ¡zhÕ6¼ <ÔxÝê–AÏ^„„„‚ßï µà‘4Óg‡-¦Á»“<óôž5ÙÖÑÇ’þ‡òDS€Ïʬ 1ºåxþ¯»kuw€ú`“óoº–l @"ÔT¹ùëxV}³)ßý`€bÆ~ž¥ÔµmñLãøŸl¸‘?å»Ü¿r£î|ìÐú ƒP++}cˆ g_«{¶ü£ÿËçùÔýöæ;÷9…±É‡~`¡àJ \'}ã#áäPƒÂáp«Që$<é¬-éò0V.ÍyÌ(¢jÂy¡‰Ò]Qà XQM£¿­m†Êüõ#'øõ#'/æ)š&»ÛD% ­™v¶'È” œ%næº"Q¶·µ£+âÒ©µÇ?zú4ïÝæ¯~>žªWæÔ0Ž‘#²ã8’,ÚP«ÉÎ ä­9ÎwwذÀhç ¿¶ì°"Q{^öJ/Ø%õ'Ï_7©Ç}ò£œLtÔ°þŽkÞ¶H, d. ((’´r›6,‚Œ›;x+¯§iÁÚ` yq-;¼üÛLHÏÖýó½Ç97=ÂmcÊwŸô\›Rê Ñ]7ôzCkߎj£”º‚[Î7,ßÂ_ÿðužK=ÊóÇ…¤±|}5vhÄúÒÊ붃á8d sÑ:‰ˆþ~HDC„U…°®,ß*P—ŒD²#Jo»‡a9 U` Y–¤÷ê—°®ðh‚==mÜf"g40Y´ÿ^í®A@‹§òW]6€È+«œçUðá\æ·‹ëçL]u|xåÁMž»tçO‚Oš‹ö]’V€ê<žUðaGOœí]a¥òüÏqZ µß˜9ÕŸî«xô.Ôþ_ÛXö’²\ü·ϵ‘ð $´…% ‡Í(gìïÀ5·Ü~_º2Ê>ÿèšlëØcI~ðÃëþz›™nÂÁ6*«ÈÍ]”ý¯~ñpMë)ªL(üÞúûoø¿®”£{DgßhrMÜ©÷%•ÕÙûÌõpwèíY=Æ©ÄÎq[vHh!jh™moØ ¿¬³B}ÃIÝÿ½gÞ5ÁR@s‚¹1,£ŒU^xX>¯¢¬P7€mÕ–W³a‡‡Ë%ð<ù×w2e¤˜¯Ìò¤ü ò*”gð¿´ói~çÖ÷õM·œÇH]!Ò¼±÷`¡8Ñ]c¤®3—SÁ±8;v‰sçGxa×ãœ|¬ bΦ€V['c˜`Ì,›ª€$ª"i•¿°J"šA¬âÀ KÑj@ÎJ°€TC[]"˜ë`¡+ÝÕI¦drw"}cˆé‘Ñu/S,¢sâ`?ûwto¨ºË”¸pg¢•‹(ÜÖQ"zOÈ—R©Ô…d29ìnÅò•ðÐH…Uª¢+ª„¦ªÒßÖAX§ ¡Ö“®¨ìNlkÍ“œ/E5ß>ñ$Oô?Â_¼w¾an®ePþ€PïA´öí¢¢…„ZDv~ÜwšS;f:¶DÍ×AÕpØ¡þ`GcÛ–Ønƒ@‡Eë6vhòriD:}×û­Ò§ìí°ì»yÿ°Rxð•—TVÜO×ÇAÓµ`媩øè{ª –å#`Ú«¹èèàÂÄÄ y­²¬Ï€fŸEwòµÞcœ»pL› œ$Ôµ·±U.«DúcN c¦9ã,§ÛÆ/Þ|“Ïïã…'‰?j-®§M;,·ŽízdJf†ÈV~Wå ‘ˆê$¢!Q}õvåvVKÏ"Øaî:‰¨ÎÉÝ]dŠ3àCÑlìàÏoµý®5ϪDUã \Õ ­@iy×Ò¸¬qªs§:+ÁK·ŠÎMÝ[÷‡Ñr‘~Â3Ãý¼ôôljÑ™¿ûDÚŒ±!ë÷?|÷ýV.^6•J½,FÞõ“ˆ6 ¢ïТ.cå%Ç&"fâ¤*àPupj´Bj%˜\ðŒÐépOÿò—~¥¡nžkc¤®âZFÃ…„„üË)N Ú=Sí¿ c,¶ì ÅË foØaÕ|½ÚóZqË¥¸àTâ²î{n•¦À–SÏs”1ý?H«š$cÍ DX¸}Û¦=÷WÞZ–ù…Te × j‡V^Oè!¡ÐÒõXËþhtš05Û&žï=ιénSŽ©™DÖÂM,õÎ]¨ÑNJ©+¸å|Có~#s‡›ð% IDATs?Ì ©'8s´zÌŽï‚ð‘ςχDÉ„ÉJ}Wàîx˜xX[1}³a‡¹Ë1“±àƒŸúÚ'kuth”ûDU½s †4R`beâ@4Áh‚çw<Æï_y“«…µŸ­øüä}~óõq^|’S¿Ò½Èí!Wd0 ·)Û/7†3ñس=ŽªÈ¬›v¸pgâ;ÑÙÍÀ”¿ÀäÎ'6.ìPmêj;žSšuCZ5q[ž ®² „„6—ä°å殃knÙý—äµ{ÖÑf׎†ïù öóJ÷ãLà•¿Õ¶çñJþ‡½»;9z¤v¨c©`ðZõý7®û?ÖzxV$´ŽãSúguOW~ôÓuqw¨ä±ø™]‡‡Äöd]å¸ûÁWÞ|«îã!`‡Ö“cZäîSl@¹P3ÔHÐàЗ>ͧ^ümô¶è¦éÙ»÷k†•ÔHˆx¤3”³yl£Œµ ˆ_‹Eˆõt"­ç³À5Tøš­@Ã)Ò7?jÙòVA‡ÿélÜgï_ÐÊÅîë,Ñ,D/Ó¢ÀT ‡ÝÑ6q”V‘,É„UužƒƒP3%`¡šo¼<òÁlŽëa8VMy(3cÜÂ1Ohkªêöp¸'É¿ïnƧÛï¼×ôö¼\Å6—®øzöŸÅüî¼÷Êku×£€ZO¹ãÆ&qwK×CúÆÐ¢eÅt–Rº~ÇO-¦}çâw í»úÐ"aÑ—‘U2yû"wÞ|·a ÀÓ/þ¯ûÍ3›¢Ž\Ç!ÿ`‚üØd ô’"ÞÖ¾åÚÕB°¡Õ\VÒf ;ËV+Që,™%ä[©TêB2™ZÒ¿é~© €‡%4p«º¢ˆJZ3 Øaó˰mÜ6ÅkqÀ@Ñ\b™e­y{T$‰¨®?#¢š˜ug+è‰þ]ùÂWù×oÿ˜«ã£ ÉÓš~€SÎÝõ¸€„„ÖIv~ÜwšS;*_¼;Ô(²'Ò^qwhÆeY«À+–±Îr©Ì™D»O1jjÞü¨YOª½\>ÚNƬ^Í HHVØõÿ³÷îÁq\÷½ç·Ÿ3ÓóÀ  >@Q$MI±ø(G¶Ä’%g7QÖÎnRQUœµ+ëÝXÙº¹©ÄIêÖuåFIU¬{+¹Ž+Þ¬ß]ßŠí¬£HŠ©X¶$‹¤,S2A=ø  0x 0˜gO?÷Æ`fÐÝÓƒéœo @O÷ésNŸsútÏïs¾þ’ˆL pJƒB„÷ ÊñËuOm^_¦·Ù,ëúgXÊd[1™¯0ïALÏ嬕§T;òÀGþ•, y#øRì^#BQu õÙD°C¹4†eãæ`”î0@~uçïoø\èÜÂ>ÿR_<¡'½­¶,ÎCƒa(äÒ*tÍÙ€S–7êAQ5¼km^lkŒÛ‹•ûó`Àú²šxŒ«7X㇮dvyh` ²& ÀQó‰Dë†óüTMÇwuúÑÕ@Wg};ÚëÞøð¡ƒÝh ™ |ïäR«No^`¬ŒžJ;ÔôØ£¶´¾Ý¬âÐvwm=_NMfÚDö¶åôÇý÷õ›Þ¿w‡&“øàCëßÐ~âÝLRçI§ÉÚjÏŸ®ÍÝ¡R?ÿÖ¸å´Â½Ý¶òœž!°C JL¦‘šœ"É-W¶RP¡dXó· Wnîݹòw¨?n9˜9ºo`yÛö$R“3¸õêÛˆ\s4=ëÀ£ùôJ}¶B;OMÆ·=¬TI‰ëã+pCqLp³cÃfj%С¨w®M¹9{Kñxœ ‰Ê"²«ïÃ¥.iEF^Uàc¶wófif àÀÒ4iµD MQè… ìà2ɪ Y[»Ê£Tf[AQ jk† ¸¡y¿¸*(FÄzÐÂòðs<Ž_†!H›mE ¯<ø~pó ¾5rÑ‘4µB¹‰wዬK0"Qeéš­±|Ü™è.#£x;Û&°KÓèóÍ 5•¥Á°CŴȧ«1ˆqÖ€¸”™ÁÑHÛæù²x]’rV`‡•9Í ×燦ëU MÁC3«iIàñTO‡Ú8ßR4 ’¢®Ì=iŠMSUAo¸ ²Œù6VêÖå0ìPÜ6 a^¡” Ò0åB¡Ñ0¿Z_g"»ñfzçR“¶Ç¹|üú•l þ݈ñ¡dæOÿlòÞLOàÉéxr÷ûäòõÚ*°C û(ê2ü°”Ë/ú¢~<œs°ƒ•º]þ]UÃd"‹øR¢¬nr¬…û‹a‡õêTøG? ¹ëyq¦Ão¿ýCüoóGðä“Àà*ÀD³üaù%ªê̳Sjhöq`½ºÛƒøÇîʼnÃM:ŒNÎ#“7ž³ùF'Wçcñ…4âóiÌÖÉ­Ø!ØÁ"À‘]=—0[Èo;—žaÁ3 ˆ\§Þ`8â(âˆrëTM‡¨®}1¬i:Deí6U×W‚ü‰*« ((( òÆC ‡eöúä=¤ · Î c¸£uá5Ìç²5§W„êµ3Q…¾'¦-ãg8  ϼM`èóÀR¥Ï [;Ø>Oa«ù¢×¦s4Б쬥doŠ‹8*F¯âh}%¥‚eØ¡ô3𢠬@%à„Xóذé}=¶¦`r;Aã´0¸uY“ ånCÏOA—÷—á ÊÛ Ú¿‹€eêO]¼Ø°Ó o©=[iãïÙp&±ãî ‹"^ÿ»€,Ö8J`÷(==‡ôô|SäµÄœš4™Sq# ¹ÁðͦüÂj=ÄG®oø¼D]"ŠŽnr6ó"×Ç‘¸þâ—®ÕíºòýÉ0xúžæ¿­©*²³ MÓ‡’1$[j(§ÃC½øåO܇Ýã,6³F<‘^0ÄÒ˜IßáÞ™G6o‚÷ù|…Bn¾xpð@dKñxüR,àJO§r™–xfÕ½Á˲ ÉJäD.”Àqz¶ï—’†ÂFwuݶœ´qr·Þ}€hëUPÌdÒ˜AÇ¡ÍëCØë#ÓB·ã?}òqüíÅóxgj¢æôtM1œzï#DHm”¼õÕk»–;m³”Ò™Œntwpv0+[øºCçÐÌ×Ú´†|ÖÇû›ùÅåU¨uëõ¼I oR* Ì{,S¶Œ¥Û% Ðu€¦*Ÿ“dd t}c™2²„¹|nCÝIšŠ©lyEAØãMSð{xøx®rþ(àR;ìPÔ`(ˆx>±–`äþ&“xùì5|ã›oaâÎÆ@¼÷àÏþôQº+Ö:Ž#€QS©Óß\«œNk$U…¼nU?2·„ŽÇï?¸yß©}U]S›ü9¼±ap¡RÁDDõ¾w笯–¾xp½»ƒss‹>O ÄÎAØÁŒ»å`]:µÝÖ1Ëåc4@5êrÈÛn¹h—²3ÆñÃ0/‰k‡Za‡•I±x7‚º®#•/¬º‰­;VRÕ²°Cé~‹yEA·ßt¾€¼$#àõ€çÊ|ÁÈsÀºùº©~b€8ãRb¾¶~Ý+“«°ì7‚/ÅîÅ3w.ØîÇZ!1~ÞØ0ê)šóBè?V7·˜‘³øã±7pd® OÅîÆÑ}a V‡/,kqe0\Ôâ"av(ý[”UŒ%2˜\Ì¢/êG_Ä–¥kóÌö“2P…®ëài»»BØ "‘.`r!‹Œ([¯w»Û(Xë«p`AÒµAp¡p¡¨¹E·ŒF³I|þ¥á«ñãúõÕñ›f)!ûÐç¡á ˜+{&/ã­÷çpt_;¾:¿£ô5·²ãYóN`ºÔb«àÑ€áU*?—i2éjë'?DDé¤ ˆ\§lVjðPª}a|ñ·>Ž/þÖÇñþq¼ùÖ8R)¡§îhèPÔÝcÖq ðZ;‘*?fŸimº;<ñØAKûwwÚÿnÒŽ»ÐBWÙ]v$ÐRïÙÉ•4ôä;ÐRïéüÄÖºQ¸í®&NmŒÔó±Åwˆ¶ ޵ßp5‡‡óßú–¦k HÞ÷ép÷çÎ4¼=,ÞšDêÎlÉó2‹öÝ;àïn}WMUUÑ]4\ |_8lAà;ÑZAˆJ@° EXqŠ0«Ôä äœ9_hÈõ Ä:ðП~½Ç†[¢g¦ç‘™]h‰¶gôùø·†•v¸õ©ïÇÉÃuws(:4ŒNN õª‰¢(·vp‰ð@TkGþš[3w'ŸÅ`¸¹;(Í èñ@àø’à$"¢æÀq¸ú¯’“7®Â‘]·MÓtˆë£ ¸A!ŠÈÚã®c>›ÅB.GÀ‡Ó™¡a ´µã¯.ü؇1~ô@DTïqÙÆªÈGƒ]M;8ù\A£ÏW\õÃE°CUà@·¸%é6ν‰Xf_c¼~†³´jÿŒ”EF–pª}•|–” 5Ô½^9°¸°xXt^Ì塨ZÅ4—¤̸/ˆª‚éL=UC2—‡àáà÷ð Šs- ÀúUÖ(›×p“ãÂ:¼>Ìòö¯O"°¸úLt&²7ÅE|/qÕv–SÓPwè0ܸ¶ˆñ¡dê <’ÅÓ£¯âH||Ø6œpw¨v€‰ó[q0ÕÍ»Ñ(šŽ±¹ &JÀ‡â*úN::T8F’5d³ dI[“žcp׎TZC|1øbЦÙ:GÍûÁÂu¦jƒk#D ‘º€3…¾üÓ7ð,Ü=ø#, YrÁÜj¼EÁã§Áy¬½ŸTT ¯ÌãÀ@±h(ÀOV¡ŠÁ@ÈðÐÈ^ë%Š‹@WgÐÁÊj ÀÑŠ@Dг¾éf+”ÏÎe*®nÞº+ÖpÀa½NÀ7þþ§Öz~Þb@“®îfwWsÐEë«Ïwuúqÿ}ý¦÷o yáñØyù¬ woOÝœt)uæ,t%]{bšd¤% ‚éüĶt{Pçz~'Üü~¾l_J‰eg6SçîÓûþì»/`îöxMùß÷éðП|¡ám!;“X;€¦(˜¿>. €÷ûZ¶ˆÉ4’ãSÐT­ayHMpÃV®êOäŒJƒÐ›% ¸ûsgpÏoÿrK\ƒfvuXïêAÀ†:± 8œ8<ˆ€ÏÙàÿ‘S˜YHc&‘ÆèyÄiܺ“pMÙ½^¯Û/\"<ÙV<‹Åb#ޏ1s…|ÓÞ?ï—%]”¨yÕæ-ÿ"@.㘠* Ôu–·Eª­ÝæD2‘*ºA„½>R)- áÎnüÙ'Ç_ --Öœˆˆê<Ë¢åUÇý ‡!!8ý.¿.°ƒƒîÞXŠ®c™] ;PNÂ%i± ­Þy#ÉÎZª¾KÙœjï2Ü"l{E†¢kÕy«°`8<è:@Q¦aP€²nÞ_­Ž%ME"ŸG§`¬R—+Èm‚L1»ô¹\[1@˜„$DÂxkF„¢Û€„ŠÛ:$ Ç…Õöò¥ž{—38—š´}Ù·z h¾ÞÃP2s(ÌÝ€&‹u9OYð!Vpv0Û6(“ç¤L|ffŸr}ÐŒPâÉ<»‚ˆ…}æ·ùy>¯"–Ë~¦(Òi 4MaGØ¡ÞæS"æS"âɼ¹{a½DÀz™~TäÔ4¤ÄmÇúZV•W¡‡ÿÑøq›¢o€ç¡!4(’^Öñáhp ,O£Öÿêx¢¤b°§¹œ@›W:@ÑätóGS­`8WÕoÜ "ªÇókx¸=¾ˆÙ¹ º*¬ÒŸÍJXJ‰ wyp³N´> H 5¯+)@7ÿNCKÛ[Uߪ»CO0ÌËg­/P@ûë³ê®–¹uî5çÓÍAŸ~L÷£¶úhÓN)Sïí¼rÂÝ¡ÒØhÇݡ͂»Ã‡¯¾ñw/×”w·À€±šxÅÏæ“- —…¬ª¤2ˆZVš®c:ÂB>‡Þ`ÕZ@B|úüÃÈEüd|´æôô@DT?érÞò1CBdù`3ârØÐçuØÝ¡a‡ê­mí¿œ `õ9óh Û:ð™Å©B höÛN…íI¹€ÏÂ1º¹z)qyHæÄUØÁAåÖ¹²)ª†…l¿ÏX½ž¦(]è°;˜­ `) "¼¿°`ùØ5Ú™Fý€¶úÁì8Ž/K?Ĩh°ÜJèØ@'!)qÒâDÝγ|8P|¨Tßf`'öÙJءڶ’ÿEEÅÕé$&²Š….®é0ì IZØamÔ4é´ ± "äçÑÑæ5à‡ta€¨^'pÆÝ¡° ¤3ëæVž#«Ìa¸PØ@'¤Å H‰ÛŽô«èA<¡_ó®@Àp΀²4EG)óÀpÎÒcÓiˆ’‚uZ †Æ ÔúP÷¼>sÇÚÝ^I7qÀWùyÅø¡«9hö¹¿šE€"¢ºDDõÅG-¿ðò|þ7ï«øùÔtŠUÔò¢G›åÕÞµühÿ ‰%@ÉY¸ç ç­?G ‡Oï1½¿ÇÃ"Ún?Pêå³W‘J¬·qaÐñkX/ØaåšH (w¾ ¶ç3 øhëw M‚ºx±¡YøÅÓ{jvwªÖºv™sw{w¾úFMùŽîÝéØÁxþÝ^ñ [íê@¢Fkß§À=¿ýËöt¶Dy伈äØ4ä¼èÚ<–:·$®¸¡‚ü>GööâðP/ŽìíÅž¾ÛieòŒN&pùÆ”+¬ˆa°î޵"°ƒ‹D¢òˆœèÐ_skæÆsi µ»¾y†ETðƒgÒ¢ˆZFE!•@´­ÚûíÅ:ü~t dÅÊf—Àñøâ½'Ð)øñOW.לˆˆê#%Ÿ´|ÌÑ`—³™p;ì æñ/»;¸v¨”žXKJwðZnžÖQ7žÇ{–Nw)3È´sm§Dó±2ðPKäó€Ïƒœ$¯…š©Êm™)]ÜDY4]‡¤ià‹‹Ý$J¡Ždiózª €0®{‡×‹¯ó¢h?} À@V ‡Ãã«;Oãó£/!«Úw±Ûjè¢Yx:÷‚ õ@œ½ÕÆ8lVeÁ‡žBåúnUØÁlž—ÿÏd\O /êÇ`gÀè3fÒ4ùy&» ì°îXYÒ”%pP˜C,âC,⃢i˜O­ƒšv€°ŠâkõÃÕ§¯Ew}-þ¡#}-«ÊøòÈëxÖ÷ †þ—òAã4KÕ}‰' Ø£.РPÃñrh*àA7Ó˜@±mÐ¥4½”œq}(òþœˆ¨®ãQ=ž#<Ö‡W_ů=y¤bpðÂBޏŒ[N¯­ws‡‡äô F^|¥¦|G÷îÄ_ÿ ˜ ­ruHMÎ ~鈮V ;»€¥É×å‹€MæTtpس#ZàÉ °a2ÑT® fåñ¸~‘“gIkvð@T“âñøX,pÄù›ó@ÈÝuà=ˆ ~Ò˜ˆˆˆˆZ@óÙ,Ò…q{hýÊÁ#èôð‹çkN‹@DDÎK+X‘3$8\×°(`ÐBSÀ”îLZÕ`ËNUÒb4@5¾`òY_Ý{T\DFR°{]ª”%),£›ÜWU芊lAª¾_I[8YE¶é¬Q’Ë"ôð]<Ô;P›÷Ñ0Þš™¢éöÓçt [¦WA”ïdz»>…/ßþ·¦‚€ö ôƒœšFaöt­~°ûð¡çnÝvŠÎ °Ž%ÁÌ>ÎÂ¥š\Èb>-âÀŽðªÛC¥4M‚ª¦C‘uÓ°Céß²¬!1W€?ÀÂã¥Ár´?´û ¨šáü°$"™• hZÀ=÷! IDATõþÕhØa Es^ýÇVÜjík+N¨ =l…ê =´¼l̯ñ£ŠÍ_z5×Ä.DDM4n9,ÊÛ ¤¯[:&—“ðÂËWªß[ÀÑý¤‚+ÈðŸ2q?ÎZsÒdhY{ÎeŸyÌüs.ÃÒh5ÕÙËg¯Zoß»;èRjâü–µ]IC™~Á€ZõN(NAË54¿xzOEg+ªæ`rþ-ëÀNç&²(âõ¿ûÈ¢}²<ëÀ_ÿ ø ¢­•”Î!9>E’O;—XBâú8fF®aþú8”|T8QÃÄÜý¹3Ø÷ø-:hªŠÅÑI29Wä§èà¿tä÷ñس£clˆâðÞ^|öÞÁŽÜ˜ZqnL´ÜPN>ŸÏ½}QÓ–fgg/‘î‘H<"'ô\êò èf ytyÜ90F?<±âÞ.R4mÍ÷¢²ñáÒª+BTðƒ¥iR¹DD.RAQ0ž\@w ˆ°×G*¤ÉõàÀ ´EðÕ7^AN®í¥ ˆˆ~ÁPÈX>&Æû‰÷¨KУó°Cfœ)ËVÃvòL9yÌ&à» <{¼ŒŠÖV8¼”Á©ö.žp°,¢¦BTUxKk…–·åRièE¨sØ<´,ATÍ=çеÆÝamnu,fsèð°«Õv°‘ÞO,Ô–~P˜[]tÈÁWwžÆÓ·XSwoôPœÓ°NH‰Û'êz®‘ì,ž¾ù*ºÇýxªû0Îìëöäk‡6ƒì›¥SGØae,UÃí¡}Ùí¡Ü*f](@Q´Êc·™:Í*Èfš¡À{hø,X–^q~…ða>%B”Ô-…4E‡,k(Yª¬a)Ð,ÎCƒá¨†€|¤¬A>þ¡­ùÏšk ´0ôÀèhȪ»jŸ#R\º6 èZsW…šk2"¢æ3ˆˆœíßekµú^¾‚‡ª$œÍJ¸=¶€]ƒí¤’Ëèäñë#ˆ”0VÁ¯´Ú¿&®X¤eoºõ÷êVÄwö…+®|oFïÇÄ%ëí;¸ß¹‹¦I q"(BLôDëuMÚ·ŒMä„»ÃÒûĹ cÖßéx=ðG*?É¢ˆ×þî[5Á|@À£ù4 Ôä 2³ ާ¿t ñ‘ëH¹pµy'ˆu Ø»0ß{lí»V>( ºoÀñóf¦æžž_³mêÝ++§§æ‰Ï“†½N=dž±ÿÓ`ßã¶\Ù¤t ·& ©{#çEÄ/]7 ‡‘klZ§îö öôE±gG‡ñ»¯ÝíöÞYÍ,¤1rc #7¦0:9[wÛª.Y–ëâÅd)Šú6iñ.k3¤ ˆÐsp)ðÀµÀC›×G`‡V™lª*4]‡¢©+@C\0 µ.çõ²ˆˆ\*M×1N!'Kè„ÀP©”&Ö@¸_yðüÕ…×0ŸËÖ”ˆˆœ‘®)Ðdë«Ô `+ßš† ôºœ·Ïp(kjßzZv`;PœJË„K„WJÞï t[Þ\šÄ©®@Ðj¯ËuÛç yô *ÇX‡t]G."m¦`‡¢ºý~Ìçr†ÓÃ& òž iëËy’’‚H¹¼Ö?TwéðzÑðc2›Ý<=ªJ›ŠÈ€DK«Á#GýÝø÷;Žã™;jêö‚(š…§s/¸p?Äø‡PóɺžoFÎâ™É xnÆ'¯À™þ†£O–»NÂ0‘†©tê;”þ=¹Å|FÄ¡þ^μËúߊR’o°Cé6MÓ!æUˆ¢ ¯A0Ä‚¢:Ú¼è{1´#QVWˆdV‚¢jut3*yã$AUu¨ª¹ åixƒLCBê‹Î*…Ù+ýÝ®²ŠŒ?¿tÏî<…À Œ'òø8ôu9ä|Ñ\«<{;=7¤ èAZhîjÑTz`HÀÑ–ŒDDŽMdxÐþAhÙ1K‡år¾ýüî¸ÏÔt ¡·êªçÛU;ûÂèßÑf9ˆ_ËOö–e””Å{·}w‡‡Úcz_‡EoO¨¦úúöwF,C±AP|Ô±k¦.]6 “FL³RïöŽ,-$ué2t¥±+"o…»Ã9;m_zñ,MÛjçžøúÕ%8œ¨²ä¼ˆäØ4ä¼3ñ‘k+Ïù…¥¦­—"ÈìéXqè¹Çx‡Zº­a:¶ñ}î=vMO¯ÂÓïß9'®£Ém 0"ºw'ö=þ OßÓRn¥ÊÎ.`©APQ.±„™‘kËÃu`Cw{Göö¢;ºúw-*‚ —oC6/më:v³»Põ ÒÜ%<Õ>ÉÇ“±XìŸü’ó7•Ïâ@0 –rO`¸—åȪßM¤¢3ƒ¤*Ðtò2à`€!jiŠFTð“‹CDär-‰"DEA( ŽaH…4±ÂíøOŸ|ÿñõWðÑÒbMi舨vi¢õ/ªŽ»Œ?j¡œÞ¯>°C€åf=[W'ëÒ–ƒƒ“à„‰c¨µÏG]øÞüUKÉ]Êβãõ•” èCÀ1ØDU…®ë€$Þt;¦A¡KðcI*`AÌWÜg„½žMó%k:rªa³•)g· †‚HJ2¥ŽO”vÕ]dÈ­Î ÏDv@ÓB@s^ýÇ æ!Î\±¥YÑŒœÅßL¿ƒçf/ãɱ8Ó=ˆØ!hS7^§`‡Zö©Öÿê;ÿeoÍc(B_Ôov–£6ôw :RYÙ‚IQAS|A‡gLCÌ«PT‘vE­ùÜË3èëô¯ÄgDÉŒdüˆZú# Ø!ŸR ª›ßYC>¥C×ùE³ðƆÁá•g »Í.á+/¿gƒ'€£2¥›“)° XÔÙ÷¤uïÀhºn•¯ñ£Šhféjˆˆ¶nì "rêy!t·eà~ôú(~hŒUÜçÆè<¼žü~žTô:<>ˆÿþ]küº8”t% ¨ÖVöµëîpè`wÕk¾^;ûkw {ùìUËÇP^çÞïëJZò†¶uî5°;ž¬ìðÑlwH)Ñð:œqwPšxó‚õñµs×`åqõÜÛ÷rMù=ñô¯»v`½<°„–R.±„Ôd¼æá‹C³­èÞslž€áÀìé@ ·{ZÎa$ØÓ¹èß{¬ü;à"‘¸>)["¦ß½Ò”e|ðôÜ3ÜÒhªŠÔÄ r[ nÿèí–uo1£Ý;¢ˆEƒØ³£Ã1°¡¨‘S¸¼ìàpùæyHY'ÇÕ‹…ÇãñKä*¹lGª€È!}.`VÌ£×çžàp¨î>­‡ в 5(®Ìo§?@܈ˆšDEÁ­ÅúÛÂ8ò¥K3KàxüñéGô@Dä©…Œåc†„Ú¿ütvXû¯¦ëЗVhж“_r@Ÿ'P‡šwÐÝ¡‚@ãa iÑ: õw[®Í)‹x>‡X˜¶Ÿç Ûç ¢£°ÈÅ•ÕEq-ð`Úiã=ð±ùÄuÏYžGÔç½Þ«\;¡€¬ªÃCë`hÊZŸ³rÍד¥hˆ„qi~Ц[8g™ôûsÀ-? ¯^ûV€€"ðï:95Âì èZ}Ÿ©³ªŒçgßÃó³ïáщÝ8Ó± G÷†ÁÂæãÒV”ƒûP›·×JyºO!S1 ehÓ°(€eé5ý]Ñ4Äó«cÃò},+*ÈŠ :Ã>¼ìæe¤ `qAF$ʵîs àƒtèìG€LÞ 2yɬQR-ßÄŒZv(“wUÑQ(¨hä×0\¨Œ'ˆÜÄ»5õ±‘¥9üõ‹âKí€jÃÊss2…€À"àã°ý´µÊ†®Í ZÀÅ™ ÐU€"‹JmgȨùDùzA±A[+­ó¹ŸáÙ¿x¢ò­AÑpct‡ÆŒ9+ÑŠlÙ1 zbÝF °zíjpw° ÞòÖ¼zþûÄ-;aåß娵Ò/6þ¡.]¹·%Ú¿:÷cÛÇ|<2¬¶ì”»ƒÇâ-ä­øùyæncä¥WjÊïñ§ûÐÕíƒñTþ¾–ó6×w¹NI7 äÀ/ ѽ;ìíDtß@KB µªET"¦Þ½² B¤³HÜøÒòo7\ãÞcÈî@Ï=ÃËÐjR% £“޹³˜Ñą˘¹¶­œŠn {ú¢Ë¿;[†œÔèä<Î_#€ƒ™>Ïó`ܽpìsä*¹Ox rJßð»5s³÷Ç“@õJ,ŠN ¢"7MþiŠFw ž¬ODÔTÒtãÉEôCÄá§ÉU„þêÂk¸2WÛ*bü Ot]‚’‰ˆZ[ºbý¥_Œ¯ñyÀaØA§ES h:T]+²B`( M¥èÁàÎËR:xŸÃeqv(—ž-hÂ\ž¼4câ<ºõó³šáÐ ÀðØã`T´Ľ™šÀ“ÑÝ€GµQþêiÏòèðøÖ–±‡I[ŽT5@V޵ ð4 4#H€‡a@ÓT…<”o':Ë +IyùL@7%ÿ8Cmm¸šLš¼UÒß- =\ÊÎàlòVMÃU£¡ÀÈf' /NÔ|€³É[8›¼…=“<Ùqgöô{ò€WßXÿ›õ+K® 0 2è&ö1‘N-ã:€x2Œ¨àÐ@^Ž)þ2çeXÀã¡Q(€ÃLR\;¬ÏÏÜR #¬:=lR·Š¢!½¤ ) zg8€Å1‚õ«ÓËY:>Î’_þXQ5dò ’Y‚Èäe‚¨P?ª¬C‘µÍëtÝñ­Óõê÷åz¿ò ôC~êrMŽ*ß›º£ÿÒ‰SOµþÆÁ+ª†÷Gqïp‡âØž 6ÍL¶q§¦hP|z!ÑÜÏjÑ6z&U@Ô¢#÷@{Íòq·ÇñÂËWð™Ç*?ßd³Þÿ0N ‡u:yÜúê꺒†®¤×Üku9i˜TSl‰»Ã®ÁöšëéÛ߱ѠyР3£¼’†–©-Øp×@~?[c‹ÈåìêkÉw@÷7ý\KK½]²7çݽ# ÈÜ©}Î\mܲ¢®®Êß!»0fý׃pÏÆÅ[²‹IœÿoÿXS^÷}úÜý¹3®o#î(ÒSsДµï«h–…/nš¶.çE,ŽNB‘¬·n‡±tìX |'`ƒs*Bƒ§ïÙð™”Îaþ†QM¿c,ÞWtŒ`¸Fdr5¿®óÂ…m ¯Èy‰ëã5»³˜Q.±„Û?zFšÊÁÅŠJ¡†€ÏƒÃ{{ðñØÓ×Q·sÎ,¤qnä6.ß4\²@“­¦áÎÕyÇpÇêß…>ʦݜõçÈÕsŸð@äˆâñx2‹ý3\êò0[È#¯*ð1oò>Ž# ¦Þ/1t’ªBÑT(š†‚¢,;8¨M].ãü ýBˆˆ¨6M§S(( ºä‹ùf¿òà#øÛ‹çñ“ñÑÚ^,L¼ ¡ÿˆˆ,J­¿ü"öOè ì é:$M…¬o>7Õ(ºE PÁP8šG3Uêà°ÔÚ/ø šŠœ*C[¸å(>†GÑ&Êâ ƒ¥[ÜßB:ä->: ;Pxux€£n{ÀCaoeà²ßÖ’’T<è5‚FÛ]‘$< [ÁWÀ‚®„Rv°,Ä|~]SÖÂL™t[õó HJâ¹Ü&Ç™Hw¸Xq €?è;-=P4 OtøH?¤Å H‰Û[rÞQqÏL^À_O_Ä“WàLl±ý,Ð%o¼”¶³þ3SûØg`£Ïš,O¦ ãâèŽFWWÕ¯$,ç?ÔÆaqQF*+ARÔêù¡€DJDoTX…š6É«XPáWY0 p‚ñ³þŒ y@Jº²øch„<ÂÁÕÕÍ€ JˆdF‚\ÐPÈY‡}2¤‹*¼lcß3Òž„_@nâ]h6ܯŠúók?Ã7_z±ÿ©qÁ¢¤âêØí‰8’Þk3w\6{uQ°2í¸ »úËÄMrˆ¶…è@ÔZ¢ƒû¡-¾cËåáÛßÁý÷õW])@µ³/Œþm–Ý ôü¨àþåû®d@ÇVŽWsÐó¶òü‹§‡LïÛ߆ß_ûJìo¾5f½=;;€–¾fûØ]üá¿ûÄš¾ñÂËWðíïŒØ´Å‹`:?ѼwO)µÇŒÏ}êcøÉ¥[¸U#ðpè`·#@tWûÎ9äî ‹"Îÿ·ï@íÀöÆCò…¦h'¬—GçÁÝH\ƒR0úŠ·-ˆÈž>Ðls,<™]ÀÒ¤µ…ÙR“3˜¸pñKׯÁÂi¸Á=âƒÂ aÆ]!q}¹¦å•K,!9^€øÈ5Ü~õmW¸x8!¿Çž[ 5”*“/àò)œ»<†Ë7¦0³Þ¶m¸2tt ~øymÆüg ÀUŸ«ÿá[?rsñFâñøˆÜ7#U@ä žƒKÀ€„ÆÂ3¤Û9%bÐ ©Ð «*$U…Ö̶ìe$p<‚oÿH'""rF ùT]Co°TF“ë‹÷ÖÞµ@º¦ÐÃÀ/€æ¼¤R‰ˆLJÍ'-s4ØØ™&:;HªŠ‚n•sUסª*dMƒ—a v弫Á0}ÞÕ瞌*cNÊA®0G±<¢¼o-ø°•°ƒ:¥l åGbûiqkëôT¨ß›¿j©ˆ#ÙYdDCu\¢ùBCÁ68;¨Úºz’$@õVWâ6P¾‰Ë—<œ2éöò¶üÿHYFF–íÃÅm»²ÀmÝ µ†Ð ݸçÉ"øÀ…z %n¯ÀõVV•ñüì{x~ö=œëÙöÝ8µ· ׇÍ@M‚ùMïS­=˜u’ àì°òNEÕqñÖ<ì#ñUIC/¹–"í–ò’©sȪ†ù%]ó®CÙŒ†PWÔ°”¨Ü¨O›°­IUëd‚¬:ÂDÓÓyd ÒY YQEN”ÍÕc±þ\òYýÇj‚²ŠŒ?ÿù;xvÿ àHã\Pç—DLÎfÑ×å—ÞÚåÞ@eŠ B× Fc3JSMh²°Q+Š@D­-:zêÌYËÇår¾ùÜÏð‡ÿ®z66ê±GàÿSk#Qv Àƒ./Z>§–¼d+¯]~<üÐSûz<,zc¡šëç£É$>øÐº“3åßåÜÈoÓÝáOïÁïþÎÉ Û?óØ0î>Ø?ü¯Ø‚´ÌuБ{›ÖåAûñÆçD“úÔÇ÷#àõÔ>Ïüµ_=âHyÚB^x<•Ÿß¼0f9ÍpÏF•K/¾‚¥iû®æÑ½;ñè_<ÝTmÅÛÀŽ_8Ô„C*’cÓ—ÌØÊy“.câÂe¤&g\Q†èÞè96ŒÞ{"ºo'‚=ä†Ý¤*:5Y—hɊ伈ø¥ë¸þ⮜¬h÷Ž(bÑà Üà÷ypdooCò2:9‘S8y —oNm›vZŠ® ;ck¶×¢÷³ÅÍÅŽŒT%rLñxüû±Xl €+#8§òYW3™4¼, ÇÁËr`iòÂm3n Ø .C¢"·t™y†…Ÿç!pºkKÁ8—šÄ¹Ô$ºïøq&²Ûp}8Èrýaj³}°µ°Ãºó_½“( ö•Ù_ß°?EQ…9$³H’¶i²’‚œ¨@ð±›×Í¡ Va‡ªc>ð! 4œLÖ©®ëH%e^‚—Eg›«š†lAE*+#'Ê((Ú*Qî¢ q¸áÐÃÈÒžû×xjh7àoÌ16AGØ /ß+jšžW¸P×]›štá]Í¢ÉBD­"9mÑþAhÞè¢õgŸ^œÀ[?›Àý÷õWݯ= ïïª$¼]tòø€eàA§À†‡ ‡Ò5#š”€.Ù[ÿsO5½ïðþ.G –—ÏÚsWpÊáAWÒ¶\Ov Dðùß¼¯òçƒíøÚŸ_ø?þ?{ïšÔåA]¼h»ýE‚><òqôÙ³#Š«!‡vãÐÁ˜#eêêªî~ÞŽÃîµÊcïŽ`üÝ˶óÈ<ò—O“ÕÛ·@R:‡…[ÐÔÍŸãâ#×0yá2â#מïžcÃè=6Œž{†M¹µº’cSÈÕ Bó"n¿ú6nýèm(ùBSÔÇz°¡»=¸en Uï±—oo ‡áÎn¶:ýt Sî µêg³®G¾OF+wŠ<åÕ£³ÿ¦3–Vd¤A¶±+?iº†œ,!'+ ð ËÂËràfÛ·—º5hºŽ‚¢,oS·EùYš—]m r "j}ãÉ=´€œ€´Bù;ô@DDT]ºœ·|L̳ ÌÈÙ®ûºAðé›_³ƒŠº:™DF”1Ô2ž?È‚Uä²ê¦ù™K‹è÷ú×:­Ù8 yèTEÃÑæÆOPXħ*ušË¨ÐËtU†¡h„níþ¢EÓ‘Ê®ƒ¼x¡:ñcúÖU„Æß¶‡Àó}ˆ3ÿ<€ØÿܸçSEÕpu,‰£û¢6²6;mÎI5Eƒâ£F@X3Bš—®»DDÔšc‘ƒbº>å£ÿ×Ö±ÿ×óoãîƒÝðû«ÏÁ²Y ?¿<…»Æ6Ý·Õuòø û¬-{ cÝ‘XMþÜV>w DL»;ô÷‡»®ßþŽu7 §`Ðsc¶ŽûüS÷mZû÷vâ÷Ÿ>¿øÚëÖ›@n Œ&tóô]œ‚–|ÇöñŸýÔÇVþnոÃÒèê¬ <œ»`½ýp^:w¯Éé\üî¿Ô”Ï'¾þGd…þ-PzzééùªûäK˜¼0‚‰ —º¢{ÑÁað¡{ à@D´Nõ‚št8<Ô‹=}QìéëÀžQW€ EÍ,¤—]nãüå±–jwÇa ܾ5 ´µ£ÃïG§hH~òŠ‚÷³n®²×ãñxk5‚ˆœ–k¸“Ïâ@0ìªú¨XD!“«¸O£Ý±ô.»7 ž¾—¸}UèˉëAÎ‹Ž§}ûGoãÚ‹o¸tp3ÜPÔÌBçFn㕟^í;‰–hk;Û"·£Sðã`glKÜ,ÏÃÝ ;ÀsdÔr¯Hd‘£ŠÇãßÅbã\ùT3WÈ»xX/ES WX‚ñRˆ¦è‚¦(x—]*Ü[„Œü«‚ƒÀH IDATÖÛv ]vo(…XˆˆˆˆŠ*uz ÐCsË èANMƒöÀGúI…U®Xãýæw® ìP¯ ºnÌÅŽC€åá]dÎkŠå²hºŽ„”[uÄ0[^§`‡ªyÜ"ØÁ*l@éÆÏ2Õr*Ô‡glïÍ¥;8íX§,Œ`ëŒ"CÑ5°]9“Û(ªLÍI yJu–Χ[;vÙÕAZoáîTú&ë>&\]X¬=ý-€²·ÎCè?ÚÓ˜•{Ê©ÑàÃŒœÅ÷Wñ½ÄUì¹Á“—àTOC:“×^''`‡Ji5v(þŽ'÷PúÛªëá|2¢ ÞKƒa9¤SòªsB™c ²Z9_ºÐŒ½q`@ÍUÝ_Uthªn®N«i–Y¹o꺻YiO¾Þ»‘›´·²îÈÒÞüáNíoìûÓ›“)t„=`{Ãk3w¬O5Ø õyC+ˆ@ñhJèAWEP¬DD.m¡¤ ˆˆªÝ~ÚCO_‡®¤-û£×Gññûvâþû̽?½=¶€……öuÀãÙžïß{ô€eàAËO‚¶<èr ZÆ^ ­Ùq‡Åý]ŽÕË·¿3boÎ-ìrîna£üÊ/2µ_±½ÿÙŸ>Š_úì?X‡°ô^ÓÊô €fÏ9ÕËsøÌƒkë4_AS4Ï{N¹;@o¬:¬õÒÙ«–ÓìÜ5¸ò÷ϾûrIû«ŒïûôØ÷øƒä¦VGIénM@S7ºòÉy“.ãÖ«o7ÄÍ!ºw'Oß‹ÁÓ÷è…ˆh³{j`‡øÈ5|ðÿÖPG—¢ü>Göö.C8²·×µ×ctr¯üôFnL5=ä0ÜÙeÀ¡ø»ôúÔ¸Û³ø}2r¹W$ªŽ¨^þwݘ±¼ª`¶G—‹V_35ùÑ5ˆŠ¶Aˆ¢xfÕ!ÀS,KSx†­á¼ÝŠHª ­-Ïë,ža—Ý À¥iR)DHŠyŒ/-b<¹€%1x6½¦?uûƒð²,Âíè1ÐF‚ß·ÕC®®è¡EôÅ{O`<¹€–m§Q˜»Æ#DH…•‘*Zÿ"°l99;€¨*Ua: %°°—aWƒÒMi5]U×!©ªx]Üf6À†Z›fJ‘6Öe·.,ÂÕ «*I+ÌyªçiÓóoR>N$#ø5Àð8ê³ìòðƒÅQœÉ !É~»,ÍWÉ1󢈘O¨Ùá€ghÔ2+£çL¸›0ÇB‘•ú¥orEþ˜àC²P@<›«=}NödÑú@º¦¬8=¸ zÖ‚rjòâ„íUêíjT\Ä3“ðÌ$ðèÝ8îéÝ1` Dª84ìP­O–ì_Ê”n8=P•Ó‹µûpkÚpîd8 OC*h˸á»ùÒÏugvXë4Ñ€'*쯘¤Le² ß‹1BžÎ½(Ìݰuü_ß¼„S?<|RlXUÃälƒ=§¶IÀ2#¬(¹æÊ·¾½Ù!rmÃ$U@DdjòÏƒŽž€jsEöÿü_Ïa÷_<Ž®Nss…¥”ˆŸ_žBoOÈ1w€fÒ©ûm¢ªyèr gÎC[ú¹íü™ gXÃû»À²Î}çú²`qZhçVÈÕ%ëAv9€‰ ó°êÉãƒö\>”4tq ”·×Õí[û±­z,곟ú|nͶŸ\ºe vØ5qÌÝÁïçá÷óUÇ5;%E‡‡çÞÆÔ‡öÝ¢{wâ¡?ù¹ŸÕQéé9¤§7ºÀäK¸þâˆ\ÛòÕܼƒ§ï!.DDTØ!—XÂÈó/ q㣆•«p8²·×•î ¥*Bç/af!Ý”m©Yá†õZó˜Êºúüs<O‚ȵ"uDõÐsp)ð )‡ÍT %lw7…† ¤4ÇÁËrð²«ð Œ'ðÓ;áú&–\3˺ñ’ é}Ñ.‰õb´‹TävxØÕuÜ^L 'BØë#ÒÄúãÓà?¾þJMÐC~ê=¿šó’ %"Z';Gƒ]›Ç8;Te8(Ò¼*#¥JkV…Ψ8ŠAç1>lLWÒTDyïJ=4ƒ¼ªX(Ëjš9U`œ®˜º°ƒ]@Á©´ŠŸù”àNµõ[F²³ˆgDÄB´½vY._Ëš/ä×À0UÓ® p•€It/@ÙÈ{­0˲IÕÀ—[| `‡bú"FÐL<—³ŸþÊCæö…|ðDwôCZœ€’š†&o}àõÙä-œMÞ‚‚ÙðœéÄÐ@Ø-¾\g6Û§^°e¦]åŠ'ó…µÐúchŠB,âÃÔ¢Ñæ5M¯ZFGWþ\•η&mK}¨ °~@NW¬[EÖ­Õ‡‰k,kî\„ôC+dl9¦ÌrxîâU9ð ÖÓ7Ó1«¥µìŒ¥jPv•G;¾‰üˆÁá+Nµ¨ŠÓCŒ÷ãùÙ÷jJ¾=xcÃ`®ï)š…'º žeÇ)q»!àCV•ñ½ÄU|/qÝ7ý8õ³~<Ù»±àP¾$ˆ¿ èE™hÏ‚*ä?¾˜ËÒ*Y”9& pˆÈ,f ›>/[ås Ðu`t`9Ú^9iÞØ@m:9;@RáVyºöBÍ/Úê7ߺ'²3-ÃØt¬¯ÂÏdY?´Ç™±‘¥Ñ©Þæ_>{Írº» ‹"ÎëkÊß_ÿ#ø^'IénM@+q¨œ¸p×_|ù…¥-ÉC)ä0xúrQˆˆìÞ§†R“3¸ôü¿ 59³¥åؽ#ŠG>¾'ìBw{Ðõõ>³Æ¹‘Ûxå§×pëN¢iÚËpg÷ Øp°³Ûõñ0µèíWKñxüûds·HôQ½ô,€¯¹1cŠ®aVÌ£×ç'W‰È´¼,·ââÀ3 ©¢ªzc|oŒ:7£óø—kïãíÉq<²gKÑ»Då5NAÖTt mÙ¾÷áæsÙ5ÛçsYüàæU\œšÀïÈõý@àxüÞ‰‡ð‡?|9Ù^†VÈ 0{ÞØ0éDD%ýªbÀƒ£îjEØA‡Ž”ZýK|]ב’%´qž²)TR'çC^Sà‡aÁby,Ê" šj¢,zmõ²á î”Åtì¤UOØ¡(Ndã9!Àð8ê³ìòðƒÅ[x*sˆƒŠÛæE1ŸP¹lÔæ×€¡©ÊÇIàá Átú&ÚÉfiqFkeÛœn¡œvòQ>ý¡¶2’ŒLñþOÙè[ź¯=<Õu1.€gî\¨iL5Ë÷à ƒ õ¸züçB=àB=Ps‹($nCÍ7ÆQxFήÀ{nDpfd7Nuõ"6ÈûÅò×Ù,ì`y¬5ù¹ Ø¡øÙä|/‡X»¯bž£!r’‚4äŠù ø8øx¦zh ÀpðxóýsCÙ(€õj¾ì>4C9 ;À»©YËíˆñnÍ—ƒÍÂÛ=ŒÜäÏ-›Udüàý <ù@C]â‰<{‚›»<\_ýüÒâ¼õºbZ÷KDËuÁ…¡S /‘Ê "2ûLADDdÿ¾ÃA·Ý mñ[ÇÿèõQ:³d\ >tuÐÝ€ÇÓº¡'Xät5Š©جÉPß¶§Ï<6¼)p²w¨£.PÊËg¯Z>†—!ëÆŠeÌ-ÄçÖæõ‹¿u?þ÷ÿóŸ­L“ åÆŒ²»é®,% Îý¸¦4>û©Áçá6lå­«¶Ò{ô“û+_´]ËV¿ÎvzîÇ¥_A.i®üé_GtŸ+×=mz¥§çž6žgå¼ˆÉ —qëÕ·· t|ð ž¾û\ "¢å4ì°Õ®EÈá‘û÷#à󸾾3ùÎ_Ã?ýørS@Ça ÜŽáŽnìŒa¸³{Ûô›K X,äÝœE;4ð@TÏàknÍÜlD› Ž4/ËÂÇñð²,hŠ"•BdJ/\{—ëD¤ÎdÓøÖå‹xp`ØC*»Å5ŸÍBVUôÛ¶Ey+ê%—Åß^<ÿôÉÇ]_¦N!€¯<ø¾úÆ+¶¡95 Úé'‚ˆ€®ZïKCÂ&«;;(šµJ Œ¨*ÐõÍeòšŒ6¬‰Xý¸ÇCÓu(š¶â@Öï âN!³ÑéaØ¡¨Ü€ª•w+Ü ¬ÂàSV€8ÕÖox˜‘³¸´4‡£í!«=cÓ àùBÞl ç’jlj" XHómЦ#U( °Ü†Š‚Àsð%_Úó¬£ç´ ]¬ÛÆÒ4ŽvFqi.Œ"[KÖ×=¯CYàæZèáLd7Ô =€¿®‡€"„4Y„”¸ %3]S’—Qq3ýþfúì¹Á™·wãTw/bV¤ûb[J^nù^çù½äÚ×9ι±ý®­ä9±u²IVî“bÉŽ,'²H'2µQeE€Aì˜}zz} vÌ`º{z  ¾çðp0ÓU]]]U]ÝýûÔ÷ƒô^±@L•p»¨†â >Ÿ9èbnÀü¡›ð'ÿÝFH_\<ò(Ô«/Ør†™Ôí{6¡}õü{€ãgz1ž´Þ¦ÅþÓõŽc4¬H–Óôüê¤írmüÄÍØ~ßA9+]Ó0~¡¹TJVÂ¥ÿx_~j¶ò÷âá k°ý¾ƒhÛ¿›¸v9ا‚2£qÿÁ³Uquhª LAKÁÉŽÌ;9;Ùíú²îж`ë ‡•ªwÝíîOQÌý"ÀQE400ЉD:ìtcù†rYd5^†t¢iñ /ÇAäxââ@dK•„fêÕž èÂ;¯ƒÀ’ql9+.IÐ ÍP>øtÊ,è0S—ãã8Þß‹ÝQ÷C­5uxx÷øî›Gmç‘þ¬X Úã'‚hÅKµ±¢·¡{t” Åh†ù€ÕÐÁRæVlóÐ D&¿"™¬kSÀMQhÈéRšŒQY* ;p4 ÏBsájÀ”“°ƒQ¸„€ xSp5¾i£¿4~ ×~³pa* ?&ËeÁ¦ú€ªæfÂeäŸRd g2ó¶K« ’²Œ&Ÿ/¦sä\SïûÍ–Ÿ²Ù×MäÏÒ4:' ‡™Ð£UØar{Ž@sEs„Èú(ñ«Pb½ÐiÑÊ3 ~8;~hãÖ .Ðf(Xw2(¶­UØÁ$\v¶/¿—…ßËÜŽ¦(´5û1ÊKÈH@ øl¼ófšÈI "rPNÂ]8ñäÏ*>&ܾgîØ³ ;7D—D_èÁ‘·»päí.¤³²k˹enXi¥ôþØkËæe¹ø¥¾Þ£ä,¹_$J’¨’z À?ºµpC¹,ZEò2k¥Käxx9ËM†ÙQµ`‡I ¦“xªó]Ü{Mj/9ËXÉ\²6†Öšºe=d?ÿð ^:ƶóAOllI°;Ú‚‡wïÅãÇÙÎ#Ûbëõ h2}'"²ªŽ@‘ =‡a4WÀÕgÚ,tSËO¿¼T ”ÁC3ðÐ^(†Ž„*/˜g ç±Y†ý:ž•Æ(ï|ÈËÏreæe˜KãÑ€\>àÒÏð8P»‡Ç/ZÚÓáñ‹øƒôµðû,×D»ÉI¨÷Î4ÐbÈ™,Àq@1—“0BVU “’T#™ }>€å€\ÎRþå¹=˜ËŸ¥f8=(Š}Øaòs•œô\ ž† KfŒ§h|m øÚ¨©a(‰«PS#‹Z¦yðCí:ÜT¿ ‘5`Cv6ü`vXh[;°ƒW‰Çб®~‘+˜Æëe xXðc­Ü3Ú°ÖSºüfƤ™.s¶ñ4r9ÝÞX7©Üôó£>)¹$ú‰§qÃØdE/ öàà;«ë÷bßPëW/°Êijúd¶^Gx(R1 (O 5 (IRDËTp "rÅ%‡ƒ®Ý}ü=Ûy<ö½×á÷ñ¸îÚÕŽ”)—SÑ5þ« €ÏÇ#–4qèÀ&ËÁîв0”(nz.¦g{¡§/Ú.ÇmûÛ±mk¤èïÖ×WÄÙaR/¶ÔNWÈá€âÃ0¤«–ÒÄöƒ(ùÝ=ÖÛ=s ´q.œ€žÃ#w†mìº;46øl;Ì̯Ñ_‘6lè:É^Ð,ïqËW&+3ÇðÙ‹U6~âf´íß¶ý»HåU@NÂçþõUœû·×*VVŸ—ǧnÙ±dÜRÙ޼•‡.^ueëEvG[°¥!‚­ M9žtŠ™×Üôv 6¶aýêÐ.auá¶G9Ôžçýf vU×qöJëêÀ2tAÐ"bWlÀùaƒç™ÒcŽ™¿in6ð0cÞ˘(snŸ”²Ü.˜Epnã‚ÍG/YvBéŒcàl‘ë÷ùYÞåÁŸo{…$ÍÆ­CO”'L&Ú Õh†s—ÛyîId[r "r£˜ÚÝ0²ý–ƒ¿gêß|_úÏ7ã·îºccGË—NËH§å‚D¸nißxC›½Q3Û Š»&ÿY€;a» ¢Èãs^Wô÷jÀ‰¤õ bÊ·¶BÓå«Òoö`m[-4Õús£m×Dв*„Þ+qKéôd×¢NÀðÐ×Áëá þf×Ýá¾{:;N‡-Ùþã Çl8<äÒöÇĽ~á­äBå †?¸€~üïü‘zl¿ÿ 6}bVˆˆ*(§`%+¡óÉŸa ó\EÊÙTÀ‡vcïŽ6ø½××kç‡ý8òv~ñv—+Ë·+Ú‚­ MØmAƒè'a†2ŠŒ´"##ËS  žvu™oˆ´œ gniˆDÓÀÀ@,‰üÀo¸±|YMERU`9r²V€–ä0©ÑLã¹,t}öã°¬¢ /CkM9ùVLÊâÈ…Å=ãROu¾‹v^·b¡‡œª¢7›êÙ‰ïV‡j–ô hzbcˆBx˜ß¢Òý*ïöÔ$`è.(Y-ÈŠä@D´ÄFB¹ü¿Ê jþÎÿûÞz÷2þÇ×ÂвVÃ_H3†¥š*¬_®BA¿c~~ÄZИ.]¼†’€+/èO¾| |¾ù×p†¥±¡½¾âðÈ‹‡mÌÑ|ÅÀ‡L·¥$o¼Ùí[›l·íC6ãñxÛÚUTº CM.Š3šS°Ã]û¶¡}u}Áß––»Cuƒ>7~âfl¼s¹@9$)žÄ;ó#œ?|¬¢ Ãäy‹^»…T:Q…å$ìðæwþ ‰¾AÇË8 :ܱg“ëësp,9åæ08æ.·Ñ™.»£-¤ñÏfHæ$¤äÒ² }Îâ|IEFO*îÚòG}l73¹Dž* ª°ž€KèÉ$±-H—«–+ä0©áL ãÙâ_rš†¬*ÃË’—•Ô«=[dÛ­Át/tÂo_Ó±"ÏÁP&5úÉiúâ±e=膾D õ>Ÿë)ñ3Ãx¥çÞë¿Í‚e1¬¥±(ø9)Uq$¯ëDTæí–xiüfZ€l®\&¿‘³ó‡R@ÂÄgަÌïOÓI Ù"[ Z6Ù†F5!=žËsq(ÜÇm•­Àw›Ã5` }Éty°ÃäçI§‡Ðßö¼‚A¥¼ùØr€Ù®jjxÊ9Ë Jk _Äáñ‹øf/pãéÕ¸)Ø‚›"Íð·RÀ6©dÿ4;¥Û•‰6Ø7š†_ä©õLã °Dé¤ZØíÊ»:ø,†*Ý'íôašôÂ>¾‹lFƒ®•+0 º%ë/khañ칚ä†?´”æB:Ž+"XÜ{ÏTV$kø9Pè‡3€‡!ëÀå%/´¬W Š« 5hò¢”4ˆLÞW-­ËŒ7 &¼Úè±²òI$sxüÞÆãÿð6ZV…°iS#V7!åT¬m«ƒßỤ̈̄±ÁoÙ±ASu ¥04”B((`m[]Åù­èÐÍ|³¸ñÜjÜôv nji†«´(ÖÆÃ À“ÿŸïOÀïeá÷róa ÁZƒ…,ëPÕüþ9 Ž£¦Ç³}Òj¦¹éÀ˜¿k9ÄFd{ó%¿‘À°ˆ)Ö_F.f_âBÍ–x}¨÷ · ú¢ö¾¡4Ö¯ž³zÍÀô5ÞŽÃ-àÁ¶h_è9j²ºà˜pr!""Ѳ¼Ä„¶ÃG 'Ï9’_ï•8z¯Ø_U´±Á‡µ­uXÛV‡^ׂµmæVw'$œ8ÙÆF?Ö¶ÖU%¨ßŒØlËéÀ®D‘ÇÿÆyàGc£Úë«VާŸ=a}îÎ@ñá 6v†!ZJöÿýè¸ÿ#¶w{ÿ½øÓ¯¶vÅU“0¤~PU˜?ë±ÐÆß.;Ÿh}Ýyý‚Û±¹’ô]‡¶8 3E#¥Wé|ãÍn˰’]ÝðègÞ¸òÙsRý¿:ƒ£_©Êy¶íÛ…m÷DôÚ-¤²‰V¼ätR,‰ôà(ä´½…cuUE.ž+˜»îǺû‘)Ó‘¡°ÃŽõQ|ù[ÑTç^GÓ }#øÉÑ÷qìä%¤³²+Ê4 9쎶,j웓²ɤ¡hÖpÝÑ\)Evíqm 7¢N qÃKIx ª†ƒKèϦ ð°ES4|<?ï?7 …@>ÜÃ’a¾R’T]£CeåÑÞä‡_`1“0/ßN9§ª8rá,îÚ´mE‹¨?„Þd z‘ÕµS²ŒTÿò³ôÑ =±qÔyEÔûüUs{8Þß‹3ÃUƒ`KCÞ½wÙPôûZÛqzx¯Ù§rÂkA{ȪD+G†f}5¯ŽI‡Ça‡ùã1L'¥æ½H(ò´ÓÃä úƒÇ/ìî0GE‡Ùµgýx­82Ø‚,eØHc#/³ûð+@|Z9X·Î2ðO žÄcõ·M¸<”;LjD’ñŠ– E×ÁÑ4hŠ‚næüOB1é4 e©¬5ªùt–a súÓi¬ ˪Ÿ’ýÆEü"ŽÁ©‘±Ùð“UØaRmY ×ƦÇ?Ãã±µÿ›#ЃžK!Óû«e=ù s.Ø .Ø ]‘ Äz¡¦†¡+’kÊ8 ~ø`5n ­ÆÁ­-ÀÎ,à3 ·›*À ê:ÎöÆÑÑ>#p«@žMÁ#0ð˜í“åþ]ìØæˆçiø,ÒIÕúø çŸ¹ð4ƒÎä°åvç†voÕáäD|÷œ] 4,îË©‘˜4xHç¯1Ýé:m¬Fy *S´ï™Ò€Váq”@±>Rï+ûîTÑ Óp+ŒÜ¨å@ðJhh8¡á4Þ>Þ‹gžëDcƒ·í_o:àyh(…ѱ 6´×#\·ø«¥:°©ªÀÃç¼n$°4ÖµÕYvÏ(G—ûb8öVõ¹¢ØVù9ºµÜÎüÇ9|ê®kl©Œ¼`° <€žìSIàA—¡¼ =]þ‚sµ/¹ûFx=ÅÝ\.ôàâëcŒÓî¡ `j,±ëRbUmûvaû}ɅȦFÏõàØwÿ Wu¦"ùoüÄÍØõùO!ÐÜ@*›hEkÒÉ!yejΙçU”Éø*7Â>//öVìݱֵçìÈÛ]8òVNžï_ô²ˆ‡]Ñ5SQq%s9 ¦“–A‡I‹¹úø¶O»;Úë]ߌžyÖàA‡¶W¼l”7 $Þ·”&‘Ìáä©ܰgÍ‚m®˜Ö¬®ÁÇïØ„Ÿ±@¯§Î ßÐΟ?CMBx†+;/çðÐ×/;îqwhn6·`Û‹‡ÏV¼=ú#õ¸å«“ ÉÉ Ž}÷)œû·×Ï›÷‹Ø~ßAl¼óæ :ÈÉ̼ïr)sÏÜ=þÙÐ<ãáÀði¸KL“C9NE¯¡Äp¨ävn„öîhÃýì­ð{Ý÷Œ+•Íá_~ù>Ž¼Ý…Á±ä¢—gW´ûZÛ ä`BЦ¡?GFQÊÊç\|ܵÇ(°,¶…à•Ï<÷ÃnrÖ—ÈsRDUÒ¾æÖÂ]ɦÑ*ÈYrë@E3Sn³•V°8š—ã-2±àzÙv»E=1{êÿqÇܽgþä9R#à¿m¾ò›[ñ¿^ïÆ7~òí²½ÚsaŹ<˜†3iˆŽ^ž®0Ц¡'6Ž  Aôƒ+ÃýfpèŽãÌð@Õ\fêæÖvܽuDzqt($‘ãñÈî½øÆ«GlÝ(ê¹äñ^ðµä†œheÈΊÛÞäʯ6aÐËâ¢@§&ÇkÃrìü‡—4=sÃ%;Àá¼æ¤ñ³FrÙÊì`¹@ÐiúZ|Oýfüuÿ{–wñÜH¾Ò° ÕÒû6ÙŽcJ’¦B˜9W˜“6§kèM§ ¸9ËIÈh*šïœöV¤.Y`&$a¢ü~žGVUMYÝ2“÷‰4œ¦å««À[mëeÀSÇÈqØÝÔ€S#ãˆÉ¹òÛÞ)ÿÛè|èá{WßÃáØÅòÆáe=Lµ'±ŒX ` ÔÔ0”ÄU¨©×”/­)øñH~<Ò…ϯÆ=‘MèøhØ›ßF¬À ÂÅ·ëN£ÆÏ£>$Oc¦;íì@›ƒµXžB:©¢¤a°=<‚¼ŽŽõ.Ù6n¹Ý© âYD@-zùGbV7NÌñÎN_Ëžïµ>ÆQÞ(ˆ* ŠÅ….hšäŒëëËçK´BD """4ïZè2Ï<׉—_9/þþض5R2ÍÐP é´Œm[#ÓNi‹ C6ãñx»¢ûس»Ÿ{𺩿£ÍA´¬®Y”ã~úG'¬Oiø0(¶òñ´ØæÝÚêÔ?yáÔ‚À¤ÓrÑÀüûïÝix-~LínGë@O_„6rЕ²óx¿w÷^Dž7ºÅÝÁãaM9¿œú` *Òo? >@U´ª÷þö'xÿ™— §2Žæ; :l¿ïà²=/JV‚¡êP²tMƒ¡éP2ùûGMV ÊŠ#ûI¢ø³=Î+€fh0<7Bð`yŽô—IЧ0|ú"tÕùjBkšQÓÚ\r;·ÁnvuKâ‡/DZ“—Î.®cì® ‡ÝÑ–Š,º5–Í`0U> r.>Y×\{œÛëáÍ;»ì¼Òˆ‡VmσsÖ¯/åŒ+ÎöÆñQ??»?.”—Ó0D¡ã1Ì‹¢Ÿ…G`Nªy·‡¢Y ^‘ñ×Ú˜jýŤØÀiQ4 Æ[-kmåÔl‘á Р/jùcIyx¥ˆÉ9ü´ï’õºðPq1"(FÌ;=è2 ]ô`åe'#€b}ÄcEˆ@DDD…&b¼ãß|ýî»g'î¿wgÉíÓi§N`˦Fx<‹s_õðçöTxXÛZ‹/þþùK9Kc˦F„‚‹ó û7»mŠÓMÕkæbôÔ9KiÎÁ©Ó ‚6RN- <:°Á€‰¤µû#upxÐF^‡žüÀ‘¼Ì€{ÜÖ´˜s/{Ú¦K‰Ýòß¾€ðÆVrݱ þ_Áѯ?ŽÔ€³ÏŠ–è É ´œ24(™ MCÎa8¤)Ùâ€>Ëç ø€Î+€â ±ÒU­"°ëáÞÔ!TzF'`%+áÄ“?svX·*Œ¯á šêÜë8 :üÂæuÖ±ëk¨ûÛÚ±¯µ@VÆlÃ@_|ܱx¡îTÂÕÇ{]Sâž'géˆDUEÝ‘H¤ÀN7–/«©Hª ,™˜.ú 4áæô )ŠTÈb(­5uHÉ9ä&&ö,CÃÏ{ÀPÄ £’ŠIÙ©:·¢÷&«Ã¶µÌ~ø¶k#^þêm¸íÏ^¶=¼s¥w´o^qçfzH%æÁ@ èúЍÝ00’Nc,“A(¢Æã…¢ç zâcN§Ð_T¸a¦B‚;›¢¸~UëäVª\s9Mþà’„äöµ¶ãx/Þë·¾B¬¡«NCl¹– ÎDËlËUÀêt Ãd篘ÏÓ lÁVÁË€àœã„uØ¡¸S\`‡©j€š‡ü ƒµíøñˆuëù'ßÇWê ¸<ØjïùºëK§óÀC ┢@)â\2UT~^u5›A³WÌC õÃ2i èŸ_%ê?,z!§4ÈZáÀÈYÖ8M-}Î\;Ìü¼¾6„¯gGcPuÝì0ù¹vbžÔ=€&ÐC݇fÁ›Á›] ?t¦‡ðè¹ÿ˜ù€ÕÁö»ð¶ª¦£{0…õ«‚¥ÓÛÍ™ÛÞJy-Œç K!XË!b!Ë:T%ßgi†ÇQ`ù‰±¦wºÍŸH ÙjK®x¦$ÖZNĆÑ1V³øÀCj‚;ÇEy¾ÏÞ¸FûÖ‚¨ji\`„É›YÀP`L®d¬Ï|ÙO4ŠbòÅú[Ö"‘™‹vzІ =ÝíÚb>ó\'††SSþ )–ñë“ýؾ5âhð´Y­Y]ƒ½mű·zÏ{mk-þükàóñ¨«±¡½~QÝ,ìŠÓþ*¡–‡Écûó¯ÒyA÷€ûïí° ¾jz¦´ØVÞ @…6ü0äqGêÐ ì`×Ý€£î K£±ÁœËù‹‡ÏV´ nüÄÍØxç>r½1©äÕa¼ùB÷«ï9šïR”¬46¸j°+uÂebæ±°<Þ/‚øà ˆ€¨‚Rƒ£ŽÂ4Ë"m0åê8;¼ùB¢o°ìòß¾g¾üÙ[]uŽ.ôàû?>†“çû­ õ¢ûZÛ±¯­ ¢Ÿt‹’T}‰ÍG†¤"£'wíñÖz¼Xª€ç?óÜc¤,ਚz À?ºµp=™$¶ëÈYZ$ ,‡€G€È‘›«òóøy²ÂY55˜¶pÙÞäGGëü•†ÂæhaÇŽÖZ|ížmøÒmyƒý+xòÐÃêP SI¤äÙ«3ˤŠIYÄ¥lþšÏ? î‰ÍúÛmj ÕbGdv6•·ª&Ç0¨óú–´#Ð#»÷â‹?ÿ‰-EËÆ ¦†ÁúÈM´¬ehÖfv‹ÿhiEüŠQYûõÐÌ4äJMçJØa¡ãµå8a}ÿþbp{%§Ÿл§~³-àáðøEÜ3¼ ë[û«°Ï ¸—4)Uæý×D>¹+.s, EËG˜ê†!)‹Õ~ériÎ~Ÿ¥²Ó Ðì÷ãj*Uzà¹é:¦Y~–ÉÅöáBØaòs½WÀîHNŽ!5i×nv˜T’ÿûœ–›á‡IðáÀð:üÁŽðß‘[¸­8;Lªo$H~‘³Øÿ`~û Á³Ï/ÀÀ#Ù61¨Õµþ†ñ¸ãe›r¤4èc€Mꢖ]Õt¤² üï S'è±³ÖƒØh_™`/î` PPS® Ä}yÝÙ‘* ""²'šÓt==þ¾k‹ùò+Àô ©:Þ?=€ìˆ.ŠÓÃý÷v8µŸºu‡kÎÍb;:ˆ‡]Ñ5ØßÚŽ- M¤³Ø”¤ªè‰A7œ{fãfض‡§bkž -`i‰DÕÔóp1ð0$e 9IÕMÑSnÅW\%"rŸSÖ‡B°¬®Á2Å£2¾øñMøé»WðÊk+GæT]£CØn\‘爡hD!¤äF3iä4 ^ŽCƒoù‘Ü]£C8r¡k vp»<,;åæP#x˼ŽP¨ÅeA苇w߈ï¾yÔVúÜð‡`ÄÚpHDdJ…!¶;¸3 Gd範Í3ôÇk¿9 ;PU€0°Èyú1òÿ´üÞ‡µëpxÜz ù÷úßÃcá[ò…¶<·S@_&…Í¡ZË}ÂÃ2SÀ(†Ž¬ªÂ;óA± dEÒYÀgm@hòùp%‘˜Õ¥iŠ3Ãu¢Ñç­(@.gþ¸¨ ~gv˜”À1ØiÀùñ8úRióý®ÐçI§‡Ч7øÊêá}xr¨¼ ¡• =LUµKá‡Ããñúë½xèÃí¸ç“-@«2¿}Pú†IÐà|ëÅӔʃáÞ¾ °ƒ©m3Ó_tKÖ-¹)Î늶KÛ(ÇùTPܱˆ@*£Â?œÿOŒ£sÜzŸ£Dâî@DT=ȈˆÈ91á½ „(´á_ºìÊ2Z…Ît aÛÖHÕ]î¿w'þä¿¿„D2çH~“°Ã†õõhY]³¨®“zñp—­ã£›ª^Vºv´á£–Ó=ýlgѶV x°ëôaHWaÈ£ ø°µtò(´á_ÂG«·h}Ÿ¾ý#¦a‡ñDÆ6ðpÿ½;ËXÑHÐt;®”x¿ˆ;¾ýè’sX õÿê ÞüÎSýð²£õïVÐA×4¨™ÜØ A›ãr@4-UV†Æ@34„P|À¡Æš!Ž…Nˆ*³yŸÁUð†kLƒ€s°Ã¥—ßAß['ËÊÃçåñåÏÞŠ½;Üñ<+•Íá©ã_Ž.ˆ¼¥¡ ûZÛ±;Ú‘ãI')C•€àýñW÷¾h+ô|æ¹%­`i‰DDUM±H$ò$€]9 5tôgÓˆz}ädUzà¡„"ǃ^Æ«­-ãñÌð°³­¦à÷«M¬nó¿·íø3ËûìY¹Àä–»Ê;WzpäB×’(ëÆp#6Õ7–íæ0©  É\V®»£-8¸~3^:o}Å)]‘ ÷Â&A;DËW†*9“‘+`£ì2Š47g;ÍXËË)ØaÁc©ìP" |žÃC¥a‡©Éˆ ħç"5í°àÔ< Ø¶Òô—¯ŒY’¡9Áí•¶ë4‘vG“^žn/uuÚ«âð@DTé;8RDDD•›ËøÚ@yî6ôKÒUW–Ñ ôNË8uz`‘ ‡[+üÏÕÚÖZ|÷[ŸÄGvD]¿\ýàïß²ÑÀø²Ü l·kq-4ú˜eçåW.àþ{wtyÈåT¨ª¾`»²ëô¡ÇO‚i¸ÕäÆ2´øIG]€<ìðÈÝ7ÂëáL§9bsêÛö·uÒ°Uöæ éþþô³'*Öîîøö£4÷ð…$'38þ·?Æ©>ìh¾?q3ö>ú€+@‡I¸!—J¯X×çêRGfÂýîËÃBM€8?”{diXˆá«Áû¬/üáì0z®<û‹²òðyyü?xÚW×»â|y» ßÿñHg« ‹‡}­í8¸a˲XÓ Ò £"°Ãh.‹”"»ö¸£¾êò‹³>FZÁÒˆª­çáRà†rYܵ{^8~ÅÒ>{âãäd-cõÄÆ\;4ù؉bGÓ*¬3ÓN‘ãÐä:–ŸÛt÷Ö8Þß‹‘ŒõH"yô¸`³k‚¨ˆˆœ–®XÚŰa`Ê2P>ìA–Ÿ•§‡f@Í T¯ì°Øn &V¿ggÒ¢J°€×syxbð$«¿ÉöÛòŒtª¡c$'!"Î~©æg9 #»à1zXYUƒmX h&>Xè/|îÕq|cà&¬ÿMðë e6a‡Iu&ÑÛȃY ¯Ú¸Vêšýy@±m/O߃œHY>¶!—è|:æš²ÄΪ@ˆÉ9vK{ÉmÓi—zư¡½ºlnOÙÀú¶:<ýÄýh_vUý_î‹áƒÓƒÖçŠþM‹S`šíß=a}¥ä]22BÁâï î¿w'¾õ£è½b-¨SO]»»p¯žé†>z †št´º®YÁ§oÿˆ%Øa)º;ØmÇftãŸEôZò,g!u¿òŽþÙãt6hÛ· 7|鳋 šÈÉÌ”sƒœÊ¸¡Bš?04Äp ¼á8/ykUþ¦0â— « /*3 9xÃ5Ã!Ûûs vP²ÞýÁ³eåá&Øap,‰o?õKœ<ß_ÕýNº9ìkm'ÁaUv€S.wwØŸwwòqÌDKLx ªªžD"=ZÝX¾¡\YM…—!]ÃÑÉ'ïAÀ#€'vmD+X‘šù7®5¢ùUnþèã›,q)‹ÁTM~²bÀrTç`¿+Ë5 9l 7¢Fð:–/Ç0hòð,ï ‘ãñÀÎëðÝ7ÚJ/ œ†Ør-é DDüŒ‰Õ¥ É¢³ƒ5Ç2g`P€‡¡gåé ‚Ù Ð·Z®  •ÈK`XHšbÝ©¢Üºô)@bº=–ãòðRÿe 4ç! «m¹@Yû2©yÀÇÐ F¥âAÙŽENÓ Odï¡YëAÉ©EW³+#ð4ºš D+¼OžÏ»<+G%a‡…~³òyâ–¦±­±}É4ÎŭÓßó:°-œª ô $òÁEz(¬iø!$®BÍŒCÏ¥ª²ÿA9?:ó2þ y->Ð0zpv€XjÂåÁÏ[sf M©PfûŸnï»Ð¶ýÓϵº³ ËçÁm`2ã­–51¤U]à°÷! UÍáuÙ[锉3‘3"Ñâ‹¢`›ï‚!õC¿=ÓíªòýÕ÷ß@S£Û¶FJn;4”‚ÇÃbÍꚪ•oÍêìýh«­þà7ïÜŠo~ãÂu¢ëÚÆãgä ›­Ìth»-àáåW.às^WÐ]#žàþßîÀ·¾ûŠåýêÉ.0µ» ÏÔ$´áʸ°ÜܱwíÛf9[ÜÂu¢iw‡gží¬H[Ûø‰›±ý¾ƒä"RDr2ƒ£ö8º_uΕ¤ùÚ-ØõùOU2Ñde lP2rÂDÆKMGjh ©¡1p^¾Æ:5~Ð$†ÊÜõ‘eP·nFΞ¯ˆáøšÂeAùó¤!Ñ;èìOþ j6g;½›`‡Ÿüò$~ð“cUÛqs¨¼†3)äTç©e]Cw2îêcßn€Ÿ~æ¹v“–°ôD¢º‰CÏø¢[ ןM£Ý"g©Ü 'EÃÇóz°´sö«1)‹¸”÷½Àr$¨›¨jrÊ9AàÌßÀîßÚˆÖzzFÒË:FúÆ2U¡±p±´1܈¶šZÇ!‡üõ„B(®¨ÙÝÑ슶à½~ë+ iÙ´Ì8±–t""Û²´C™JåìÀPt>À|"O–¢ÁÓe>§l»£€„z³˜—@Ót8sŒVöïÑòІáŒËÃMõÍð7Z<ŸE‚ŽSªIÓ 0³WV{ò/ÂG%©`ZŠ2 ò,R9MÃ[ÈRºT›6 ™‚€¡-õ–a .´‚ï™(gú¹­¾T&ì0óóê€5³£1¤dÅì0¹ Üñ¼Ä㇧a<È»©©a(‰«‡Òš‚oö¼ <ùQüd°F)ÙîŠöijá~Þ=Ddž°¹<€b çgòo9§¦)°]Þ5¶Ô¶g¦ƒ‡ì9<{sGt†EJU“sxì¬ àæ5ˆˆh%Þ/Uc(¢„(! F—¡Ký0²ý0äW8?üù·â/¿u§©€éÞÞ|"_U€àÿ|t?~ãÓ?´œîÝ?~t¿k›ÉÓÏZŸ+R|¿xNíß=uÎrÚ^B(€@´ Ï‘Š)!_SŒàAzpª$ƒf™)7š-Ñ5 £ç.CÉ:ãÎ{éåw0ÐiÿZèءڮkBµøø†-Ä͡’T#étEòîNÆ!r_v‰®kŒN."HÜ–¨ð@´z n¤ ÊMÑx<z‹«ÝVLÊâÜèºF†0N–¤ [CµØTßX‘ [""»ÚÙZøÁ•À[ƒ~ãºUø«Ÿ[»)H%É r\!Á‹Má´ÖÔ¡5Te+´Mþ ŠZquüÈî½øâÏ‚ŒbýA¤4ü!|­×“†J´¬¤+e>`4 XÞ1PEØaB¾9Žt>–3Ÿ_Ñ VÃZÙ(ÇkvpÌÂÈ_¯Ôœ3íÆêþ} *ßåaPIã¹þñPÍÆ|ð:LœÏËÝ©6‡jçmöð²,ú3éÙ–¶ý„chxXõ¼`Ý5cæ9N&óNs\KŠ•—¢(EÏÂÇÅsM†n®Ÿ;åîP!ØaR~žCGSÝñ$ú’6Fó:°}zÐ*=0b ¸`3¹x™y†Â àk[À×¶T ~øfï[ÀÏ& ‡V¥d»3ÕVç>ËIËd Ï”nûùа ;d3*²iª2»Ÿ " _€£+Ö¯±¥¶™¾‡?:f=P†õÖF_®>d‰SûX× ÄÙz¿#°‘;"""¢%4Ѻœ&äQ@—Ë*z&#ã/ÿæ üùט›ò\ß…ÇSPŽohÃ5[›ðÁéASÛ|ï;¿C6»¶¹<ýl'Ië+*»a®H‡vØî:´ežËCõÁz¯8»B2ïñÉïÿ)ø€¢ÙrÚÕ÷‹Ø~ßAl¿ï`Eë{pÈ%3SièšNNæ‘®éȌő‹Ããá ×”íP°Ü%„üBÎ/’¨É Æ.ô9;dFãèú×Wm§w ìpìä%|ûŸ~‰tV®ø¾nnmÇÇ×oFkMièUÐH¦rïÎ%Æ]}ìîñÏ<÷Ã'HKXš"ÀQÕ500ЉD:ìtcù²šŠ19‡:ÞCN–•Á„fðxàç=Ž€ƒýx§¯ƒikÁÚ=ñqôÄÇqäBv4E±gU+YÝžhÙè7w¯¶ < àaÙÊÃVo¥‡àEk¨vp¨­8P&ršüÁŠKA"Çãî­;ñTçqËiõ\ Jâ* 4$ZV2ë®6ëʼn B Î v¤e¾@°ˆ)2&ßôÓ àasùYuQ°åààd^}?N´CöÐvŽÅ«ig\žzÃëi¡‹”Á`3"IPƒ:XŠž·È²XbHÊ"!˳ÒzYk|~d ª®[oדÛ… ‡âyù½ìkmÇÇ7lÈñ¤1VQÉ\®2ù*2®fR®=îZÛóÀä,]àh±ô€tkáú¥4Ì"4ƒ ÀïP}uáÈ….Ä¥lÙyìÇÉÁ~\¿j öµ®_ѳDËCû·6ZNc"Z:ÚT߈s£CÉ»É@kM-šüÁª“âM¾r €ƒë·à•î ¸·NÁË£—H!ÑŠ—Ÿ±ðpÌLxiF©t½¡Ÿˆ9ÓùÆÇq·Y(ýìïÝ ;8ïá· :[ÌC.ð—ßÄcuûŸºp{3Q.ÕÐ1’“™<#MQˆxE„Ê„ -GÓàh –¡1žÉMQ¿†$€èx¾èvÑSøåv¡}z„ÙÀÃD?7 ’ªC×ómƒ¢ž¡ÁNÂeÀ9Mƒ¢ëÐaÀÃ0³a$«í®ÄçzQÀG½854ŽØ¬‡Ó†y°b{ª¢ÐÍ `ÄZrA²!šà ¯…'¼ZfJbJ⪣ûøfÏÛˆ<ý1t|Þá¹÷D;‰Kyà2±=m~|^v˜©äx¯ÈX“ î˜&Þ{žH!®Z{)DÑ,hÎ]AaVD|U„î êÔ´»Ã÷δåî@ Ñ|À#Qåî[ˆˆˆˆ–áPD‹m Å6hÿ´µòþ¤žy®½®kÛJ¯\›N˸Ô=fj['´íš^øÑƒø¿¿~ÇÞê™÷{0àÁé<ò»{\ߌ.÷Å CÉóìßÐî¶cjwC½ú3Ë銹<ŒeJÛ®‰`ïG[-ס¦`ÄÞ«X]ܾgîØSˆò®MØaÛÖ&Gû wxñðYGëò–ÿö„7¶’kÍ 9íêàÔã–¯=ŒèµÎ2𬠗Ì@Š% à°¤Ê ’WG¼:±.„@´ Ï‘Š©2£q$úíW½ožÄ臗m§äS{˾T6‡ïÿø~aÓɌքjññ [°¯µ}Ùµ)MבUó0çB^ŽKÓŽÅWÚn÷Jå;ÎÅÇ\}~¶‡§@Ä'ȸtE¢‰KÏÃÍÀC6Íšù+\MƒðB×)œìw¼¬ï\¹Œ®ÑaüöÖâö@´äµ³µ=1KizbcÄúm9¶…¦(N\AO¼ŒÃW{ìõ§ÚÝdP!"²r¿BDDDD†¢Ùs‰†[óÓÍ2 ‡¿ü›7ðØ·>ijÛþ« ƒÂuÕY}z8õÁ^<2äÖ²º‡lr½«Ã¤ÿ»·m¥£›\s ”µíòðÖ»½øØ-³ã sÎüî[°H%Tðâ¡;¯G´!TÞ³‰œ‚×~moÁ3Ž,¦Ç–ÆÚVóïßN}0€Þ+qçú÷§`ãûÈ5gæû«38úõÇ‘);/Þ/bû}±ëóŸ*;/]Ó '3SyÈA•r²V¨2cqdÆâðøEšˆ;‹ÃJ^Fòꈣy*Y <{ÄvúÛ÷l§n]b v€”œCJžn+ M£Æ#`u° ]Ù°¹2ÀCw*ŽTaвçb¾êòñQO‘pi‹¼m$ZL==™$fàÈ…³…¦nTµ(ô dM›ó·:ëoeâ÷•LĤ,âR¶èÊñ!Á‹ˆ/@à‘EP[ƒõqi0•Ħp#©¼e(eqGûf\¿ªçF‡ ©ùq«5T;1^!°îœî‰çêò¹IwoÝ÷ú/#£X{(oè*äñ^xÂkI%ï)‹“o°ˆ jâ'@ëe™ù½áÎ¼Šœf! ò¬{Çé£Zþ¥ŸáñPÓüu¿uÛô´¦à{='ð•ീW]ø8LÛ@63x0;L~GBèAZV–MÖq±üUHÄÁƒE0 mî˜æ~çñÀȤËÊÐuÃT:IÑ ê9½$Eƒ¢éPf€,Cƒgh<–¡0”É„fÍÇ3Y¬ *;L¶w¿‡ÃîhÎ%ЗLY(;S@§óЃ®HÈôþ ¾ÖëÉ%Ç!Q4 !²|x-¤Óв±²òëLá¥×ûqpS=àÓ‹·3«c$ÄR9þ…Aš_`|˜ýYULUÆô¤*:2)¢Ÿµ56Nm7<=·<°ÞWµ#]JZNãg9€]„Å^&_ÿðÄÅ3¸²|E܈È}ŠœR¹Ðà /žÁÇniGcCé /MÕq¦k;¸iFO?Û‰DÒz !Úî¾{?! Š ÀP­Í݇†Óø£æA£cD›ƒ%Óÿñ—nÁþ/?]”c® xñéÛ?‚öÕõŽäç&w‡h$h)Í‹‡Ï:²ïð†5¸å«“Áa²œëÁÑ?{£^.;/Þ/â–¯>Œ¶ý»l¥—bIââ@dIª¬ ÖÓDß|uÃ5`xŽTŒÉÉ Æ.öB×tÇóÎŒÆqéåwl¥õyy|ù³·Âïõ,J½T vØmÁ;w/{ÐÈ 3c­JÓuŒf3ðr<}•­/«1'fu.înw‡ýyw€K^$ÂŒhÑ400ð|$éÐêÆò%UIUA€]ÙÄJ‚Ð+¹2¹“Ê©*žì|¿µyx†!q†ºF‡Ð52„®Ñ!äTs+ýyX­¡:ìŒDI@}•ÔÑj}UȘ”%·ÌU#xqýªÖ%QVŽaÐä àñgR ¢×oÁOÎX_ÉT!ÀQqQKv`¨|À¥å@Ñ”õý,äj᤻å4†³yùM`H…‚ñ«ŽL”«&ŒN»,ÝS¿/]ÄÉúÃÀÃãq°o-:6lƒ“Û üÿì½y|\õ}ïý9Ûì3šÑ:–%K¶¼`cl2Ál  K e¹ Ür{IIr{›@_ÍÍó$m IúPò|{“'Ф7fé%‰Më˜%ؤްÁØX2Þ$K–dm£e¤ÙçÌYŸ?F»5Òœ33š3£ßûõòË£™ó[ÏïüÎöýü>ñê.XÆï“f;F¨ùó·›9X8¡„QJí¨0Wþ4EÁnfaU ª0uU›4ê C#$ªi‹Æ‘#ÑĬÛI²IQE°4@BÍPsæ'Ê b’4»ËC–眕¥.”Û-¸0Híô–J@±)4å@ôˆ€÷‡Å»–œ{²ÍY`«½Âh7‡þûü¸ï$n>ôy8>—Ð'vHqL"Âüb‡ÙœŠ2;Ó K°Ú˜éçM-çã HÉñ ºâ!íûËb¬#ôŒ—•ö<­ x"ùl¶-ÄËçõ]öY¼ÄÝ°È @¦¢\ÃTÜUðC´†Åb^ßÕŒo|ý¦´¶F\lƪ†r2®æA»P¶zcÞ÷y®‡-ÁÃÃmÄsÏ\p—‡lº:ŒS¨îïì» K¸3“Æ;~ô$LN™´¾uG_x BVu_ÿ…;Ñø•4õ­"Ëàð0ø`˜ì‚nYA¸áþaØJK`¯*gµŽ™‡pÿÂýÃ9Ë¿ù彺Ó>zwcÖÄ~ZÉ…ØaY‰_Ú¸k+ªÈÀÓˆ¬(9ÍŸ—¤œä+(2º"AC÷íúdLãËì~%@FZaC“. ä™=F®\WlñÞhЋK]%9;ÀÞÖ³ Þ6A–±¿½…}HÂïo¿€=€]g›pz /m±´ú±ëlþ×ñ#h^§#`Ö¸"}›/’µ²ët8<‰à`ˆó …r»+KˉØAw¯Z §ýA¿ªHCý¤ „™d(vH(2FDƒB ~‰GH CAnÄÓ±3XŠž{NX±²œWÖÄêœiÜœ9·õBõbT€›¾ÒË_T_¯{<>Ûý!"ƒtVÆ›/K–J?/†¦à±šQf·ÀffAÏçÌ’ES°p J¬&”Ã-w* IDAT;,°rc&¢ŒŽ‘ (é‹'$V[›æûnÊqNˆñ"ä¹cÄ%)|µ~VgýÞm1¡qi9¼›ö27E’N$S¸Ë³wºWd4oŠ¡~:VÅ'ÌÉS [íu ÍúW~ŠÊ"vŸk†Ø+Ç„N±ÃøçH\L½-cNï˜ÖÊŒ`~UbQYÿüøñä¹cÏ`›ö{!³m¬u‡ä„öçÖ¸8rñw&@"’ˆgÏК†* “ ƒPĨ3þ™Š¦êÎIÇ28ÔŽ3ç|io?8A_ˆŒ³9¸ÜÀÑ»´_¯ÛêA±Æt´§ktÕmÜåa*##1HRzAtßzêÖkcu¹ _½+îݶ>«b£¸;˜Í¬fw‡×w5g¥ì{~ú8—T,ú¹AÇpð™—pðûÿœ±ØÁá-Ççúl}êѴIJ ":8‚¡óð5·"ÐÕGÄ„¬ bè|ü­]ˆùƒ¤CfAŒó:ß‘S±ƒ¿µK·sÌÖ õ¸ÿ¶ y럧ÿy_ÖÄ6ŽÃ[ñÿüáçØÁš…Å´š†ÛbÍi=y)Wî#†Þ?›+«aMÆùí¡à!„|³À7ŒZ¹A>¸×¡)N³.³4•þ[fII®r9ù·<íoyÆïŠªâü°/oØ£| Í}ØXµ8Wwã% ûÛ/àt A>Ž7[ÎàpW;î]}5êÜ¥EÛ^»]츢‰ì©dëur¥Ò%Ò¥ÄbA•Æ¢Hgè~@`Âë6âÕfíA>‚¿œk éDaœ Å#"˜<ù@H… ^–“”°&Xéµt‹®¬¯ËdÒ>u^M¡!ükï 1Xh_­ß€M®Šôò¢²)vÈV^ê¼iZ:æBì0±…i.›U¸ÉUƒB=šÇä€Å;›ðm×u€UÊh¼õD#¨±ÛÁRó Ò Rf †ƒÃÌAVÔä}£:]DS8–KÓsç—HB0›³DÌQ/^Žh*ñ¬µMiô¥ªá¸§•ÃP)ûy\1ky>³4«ÊÝI·‡¡@ò~<<àšhÒéA™üáÛ5[û—tÏŸ‰¡‹`Ì06Y~–cvÀV{bÝŸ@IèÜï¾€/p5ð'¡ìˆÆŸÉjêÄŒ)íüY.Íu{ÔÙïc 6;Š¡´Ï'ÏuwØ"ó+ ìðp†MºkøñÅÓhÏd•0U‚Ô³LÅm kȤA("ˆ¸@ ©(ŸP¬LÅmöéJÿú®füðïÒ_E¾£s,K£²ÂAÆÝ,<÷ü!}ûÑàׇz]j»Âåad4–Öøyø¡øÎÓïfÅe ‡;n\ƒ[6­ÈIþzÝ–×y²êî°¬Ö­ÉÝ!âñÛý™/Üxëßü9ÊV×-úyÁßÚ…ƒÏ¼¤;y*éº:ˆqq‰p bœ'“3aAHDbHDbˆôÁZV[™Œ‰[Ô}¢È2"ýÈ æ>»õ­ÃºÒÙ­&|í›òÖG?}ãœnËNÌØõÕµøjãVØ8Ó¢o M£ÒîÀ`4¢)•åà4™á4›sºõÄ9J‘s’ï§£Æ^hfÌÝ¡ë‘ݯÁC@„¼âóù:½^o3€F¬Ÿ¤*芆±ÔfK¿! K3ðXm ) 1Q˜S°8dv"ʱjàdÆãkd$†DB‚Ù<ÿ³Ë'þëgðã—Ž"³ÖŽKËpß¶õ¨®È­{܉óÝ Ç5§«¬°_ኑ «Vj¿ßøéÏŽeTfý¶ë±õ©Gýœpð™—Ðúö‘ŒóIåê „càƒaââ@04±‘ b#A°&öÊRXÜ΢w}Â1{ô¸Ôëî°ae5î¿mC^úi`$Œ½öûŒóY[Q…§¶Üºh]fÃm±Âm±¶~²šý»ÖШ¡÷É UÕãwZÁÁì›TJŒX¹˜"CTp „"#!É&`i .³vrâÖEw(`ˆz´ú±¡ªºè]"a¼rú㜹:¤âô@ÒþŒˆf'ÂK(w.ÌØ ðqC_XŠš¢Pj³¡ÂF,µsŶº¼q®Y—˃-š`+Ââƒ1g8¯P™?À¦º›M :FRð Ž d¨¡€­-@~Ú¶³ˆ( V–;¶vShqý†ùxìÔ»¸©´õ6×ü}˜#7†´¾O%*˜#ƒãÀ'¤,•¯±^3¿ó$€ËÄŸ–_…÷ƒÝhŽj·FeÏv|„®í@‰¨a¼Mw¼,ÁÁk³¥—.ÛbjóOç»b014ø±•¢IUQAÓÔéEEYX±Ã,ŸËm¸­&tŽFЊÌ?ÞÇEŸL®2å`LرüñxÛ;£ºæ9@ÂßsÙâX|`¡¡h¶Úë½tª¢íÀ¡DÚT8nÖ0èù›æ€©n/iˆÒ›z@•SÖ9Á+ÚçÆ÷'ïÙ÷ ^Ô~ cu—£IMŽWÆ‹ÒÀ> mÑ ž=2'Ũ‚Rß^0e7v®!Á`¨b@¦¢‚ƒ6.Û y`Ÿæ¤gÎ  £sËëKÓ¿§’|z·kÖÑÃ8Ï=Pß®sƵ S²!é"¢šÇ×l.ÕK\ó¦-qYðèÃ×á¥ù(ãú{œV|á³×¢¡faÇû?¼ +ÝÜ”µ:”¸,(qY4¥ÙóæYøGô/Y¶jnýÛ'õ\îÂþ¿~A÷jëã˜6Üñ£''\dAH ø`B!! "‚=ö ÀRâ„Åí„­¬¤¨Ú(„c÷!YØEw3rwø\cÞúëG¯þÑxf‹šÜµò*<ºq39À ŒlÇ ŠŒÖàˆ¡Û¼¹²=²û•N2Š"x äŸÏðz½{xNܰ´Ž ü,QWaÎkK$‚BŽ)±XPåp¡(Ò9F¯ËCÂß<#TvÞÒ_¹:ûd¾Ã@“A–²ª‚¡µÔqêÓë{)ÄCO"‚¶XAIÀ™ðpλMVUüyó{ؿ幃OsèÆV^”ö¼ÜœÉxÊ×X¯Ù¾3)€I„ÉÇÿ¢º_¹øŽ®ýÖÄζ øòúÕ€YN¿=3Ž“ÎpøJÁC>ÄÈ,Ž¥!JJfm˜CìàŠkAR`13WnŸg±Ãøg–¢±²Ì…r‡†àgÞ'ÎLkQ€Õq uò^ÂÁ˜ðƒºíøfÇ{ˆÊúVzü`­n"ÆÌÕ©fa®\Þw^sÚ÷G{qW¤p(ó/”ÆcxüwÖ6ÿxÕÃlî3ò&“†“ôÅÉ•ïŽvk®RÑq{ãbh‹ñÍS‡•ÄÜ•§‡~5Ö¦â6€&…„|C¢‹ ™† ÚVŲ*߯9íÞwÎã_¿IÛs›1ÑõªÓZ­¿˜ygßt÷µï3Çj€.wü´ ´ë(:\þã`û4ÁÃàP$-Á|ëÉíøÙ/ŽCQôM§wÜx×Ö.XW²»Ãs;é.Ïä°áÖ¿}â '‚ÅDß'ç±ÿ¯_€aÀsý¶ë±í»*è색AÄ‚ì1Î#Ô=pÅ÷®Ú*pV ‹> #Ôã›?XÜ΂l‹"ËàDú‡òvŒêuwøìgÖ`ãªüĨ=ÝÓm}åñDãVl«k á ¡ë×PâAi2Nn'Ù[Å<ŒÂNXðÅ+ãDEt´«E¿´m9^xìZ¸mWöwS×(þì§¡¹K»sDûè°&Áƒ¢ª  $ ·Å?ˆ]g›ò^ýí-¨²;Qç.%ƒ? ÔWØ ÷&Z1 c F\á¶XQç.…ÛLÄ…ŠãPåpÁÂ’ËÊ…B¯Ëƒ@yÐyˆI èºyžçú¡(0YU!* L4“^ÆcÙ~FTÐ âR<ˆA!ŽŽx~í¿2Ø{–º§ý½NB¶ór쾔б̿w%04y-´ÒêÁc•×àåÁOuí·—?ÅšK¥Ø²Ö“f=®Ü¿¼"! $&û*_b*³ü9š†E™óˆÀÌ2ˆJ“â’„$O ¦ˆ8šN‘ºŸÕ,‰&’ŸÝn¬­Dg Œž`R* @…ü¿eÊ8µxðƒeÛñdÇïtÏ1ñ¾Oa_±µ¸V¿7œk Q›…{Sdw5×7Å´éŠ(:éð0×Õ‹’Пv¶ò4 ;+ ¢+Ò¾d0¦ÃB‹¦›h'”ø¿‚­º ”µšì ÂB"‹ ™ŠŠ ¦ì&H½»5§;p¨?¶Y³[ƒ,)8ß2ˆõë¼`YzÑöû‹: ÍéK¯ËÃCíxø¡¨¬H:èF£¢Q!­ñVâ²à‘ÿ´ ¯ýò”®:ùó7 ºbaW7‚»Cm­[—©ý’þ’ïøÑ“([½xüô—ïâØ ¯e”g³ ñÏ@õõë0ÒÞSp} ÆyøšZê@¨Û—Öªóe«–ÁUëEÙêeðn$ EV "6ÍÐ%~ã<¢#àƒa(²’·zdâîð¥<º;üô£¥'bÂTÎŒº~7T.€ €=doä"Áø|¾ƒ^¯· €!ïÂUEPQÂpWü–d²u0ÊkWÖ§;À¦:üííXñßßD0¦í…hT0¡ÔšÞŠ‚,D`ï@$Œ½-g SŸ½­gñ•ë¶ è<Hð >ÁvF0 #ÀÇ1 à §t©s{pÏšõDøP@p ƒ*»N3Y3l«kÀ¿?­9膹bé@A EÏêî0•q—5EÐfTq)DG<ˆ¨,âÓÈ0……˜!Û‘DdOìCˆÀÁr`i:à­·-Zë5çàR§„'ïu¾\µï‡zÐÎêÚwßí:ŒŸYïÄŠzËÜí™ã»Îp›ÊÌé÷M®¿Ó‘ÎÂ1ˆ RÎÄ`7qá…‰mPÕdL÷T\fnž²æúœ]±ÃÔÏõ'¼N. ˆ'¦o3uÛr0ÀÀä8Ýd¯ÂÿXºÿÐ{L×8U ¼ï¬ÕÈI&W×ìîZ$†.jJÓXmÇcºb`ís˹ÚÃÑrª¨²¾y<ÕvïLޟ}%Uš³€6;È@œ( ¼k”…;L ú÷‚.¹Œ§‘¸=r‰*&d**v(ShÇj(‘VÍi÷¾s?´Qû¥TTÀ…–A¬¿Ú»(ûüƒc8úa—®}EY LìJ›’N":Æ×뻚§¹ˆôùBXÕžÁŽçîÁ¿ï=‹hLûb†¿9|_{à¦ë"#¸;˜Í,ª½.]ûHÖ¸{ýã÷£úºµ‹rÂ1}áU´¾}$£|lån\ûg Ïò¥HDbÓ~1ΣçØit;PÏ€æôþ‹—á¿xŽƒµšáݸ+þà¸jªÈI}1Uü–'ÌNLN›aœ@„p,éNÆq%w‡ªÒüK^}çFºÓ±aÚ9$‡?7lý,,‹ÍÉů÷<²û•Ùcʼn:%‰^0jåÂ)f–!{N#AûŠ{Û×V¦;Œã¶™ðïu nÿþÍùw‡i  ^’ðÊéS—çƒ Çá®6ÜÑp92dS½[Wÿç _$„`‚‡/Âåà(ñ8‚ m«ˆvFñ³“Çðè†Fx.²“ MQ(µÙPa#;ùäîUkñnÛyÄDmuÄ`?<:aèñhȹ£BŠ€^!‚‘Ç@"†Kcâ†3‘áâé =Aý”šßògPn²ÂÇGó×_3¿³K@œ¤ÉûËo×nÁW.¾£«HEUñ-¿Ã;%÷aŽ:¨)ëI—‡tÄ:ò×üŽüY†NHKж2Ó;€XP¨°™1Ÿ¼Ï•U,&Ì Å¬-(|ÄãX8›–”!À ¸0/I³´¯Š< ''ÞåY¦èö.é«RdRd,Y?'°Ž Í‚‡v~ˆR©Ç&bгng‡9Í–fº;d*v€‹“Ï÷ ¶éêÿb""‰Èú]`7çxß߇gÏŸ\x±ÃÔaüj´tÙVÐöåd!d ]L ÈT´˜ =ºÒjÓ%x€`ˆÇåž–Õ¸]ÿà ‡ôí§’k ìV Ê1Pö€®ñ5ÝåÁ?Ãò:%mgg¿7þû_ýFs¹—zý8ÓîÃú†…äÁÝay}©.Ç•×w5é*oÙÖM¸þ+÷/ÊùVÇðæ×~ {…õqª6¬BÃ|žåK ¦íþÖ.t;žOg-O)ž@χÉ<ËV-ÃÆÇî…­¬„Å ƒ&ƒâi††Éag3Ãì°Ãä´-ØñˆD!Æ"Ѽ:9¤: ÑÝaÿG-ºÓ±a&Fww¸¦´rüãN²·Š "x ‰=0´àA‚¨*àf,Óè4‘¿´´¯QWaOk»íë*±}m%Ô”w0€UÅc[¿ëì)C‰Æ9Þ{7,­ƒÛBVñÏ„ùÄ?9»¹•D DÂè Ž ÈóðEÂŒ†³–B’°·å,þüú-d'”‹UŠ"‘glœ w­\«ÙåAU$ˆ¡~p®%¤ -P  EAV“Q]|—ãatñ!tÅCˆÉ".óáEÑš›Kì@å'/·Éœ<äÚÝAK0¿G†&¯“WZ<øoK®ÇOúOêÚUqEÂSgáùÆ[›4Kæ#tFÂØ4Sð¶óBÅ”þü]ü>ýü5ˆÆqpÉ h?Ÿ€@–U°ÌXùf•6+hšš£¼Tyç^ì0õ³ÛjÂË*Ñ£'ÝeCøØðSÄ95[ÐÆêv$á}ça_áE“Ç”ÙfÜa@ID4¥k‹°”61CŠßÝŽ)÷¯¬]·ØX E0¼œH+_†¦Ò+ÿ2 HÉ ö ¶!(i_¼ƒ±WÀYSp7#‹múÀô&Ÿ³îì<—;Ï¢ª†<°ª½tÙM X'™LÚ¯ LE‹ŠuêryŠâÌ9Ö¯ÓÞÝ@‰Ë‚—eÑôµ^w‡¤SBˆ[•”Ä T¾jÂUÐXö÷?ú=vÔçî°¶¢ ¬ÛH~‘à¶X1ÏüXì !(²aÛYmwb©Ý w‡¢„D­ŒÆNXðUe\ç"¶mFç¾Æ”Ø8cÚ^Àv‡X[^Ø7j¼$áPW{Vòª*±àæ5ØXïÆ5µn0 ŒJxó“^:7ˆ >eõé>l«k .°}]vÄDÁx¹EÃÀq×D^“ß^ºsYßêVRd‚¿Œ•:#߆™é‚Î 3)xàlMÁjg‹ÌxÞ#óiåiµ¾ ¨G IDAT3é—}9y ¤~3Ô¦¹®ŒÅ9fÜÅ(0îà¢æàš> `ÓF»¾2[Y "€agçyìóue¥94g…¥ê*#—!ÅüYì'Êè ¨áОFÐÎ5dJ!d>¿™ŠŠÿñ‰¥”e T¾_Sº?îÆ72(7‘Ðç aY»èûøõ]ÍúÜ0%ÆÛ«Rªà‡ïƒ* k3šÊRÿçüõ7¶¢QÁŸ¶+ÈM[ê±õÆ:]ý~äÔ%l^[ +7Áùí=øԫý>Àf3eMðP½Ä³Y{øU0Äãõ]MšÓ™Khøì‹n~ýô—ïâØ ¯e”Gùšz,¿}óä³ —cL¡˜çqö×ï¡çÃÓy«C¨gGŸ›¿úÊVב“ŽÓ}ºÓ;,,¾uïZܼ¦bÚ÷fŽ™;Œ³z‰oýmxè…ðoÇ»µ_PôÁ€¯_œ²±Îæ®ô9%EÁ‘.@D˜Ÿ‹UŠ"Q¬­¨B¹ÍŽáX”teÚb<ÛñÚ3NšEȉ@!Ûyy­6´E‚ó·EK½2;@0É@"y_òlÏQðJf¢æ¨,â»íGðsópÔÈÚêFm¡ Ê-°4=G;UýýA¥³Mfù›YCC”•+·Ë¢ØšZ0ABÖsjFŸŒý=Mø0Æp”‡T#Až dÿ‹%×£):€v~„EvN˜ñ»ª%6AÌVpŠ j污§xš‚»Ì‚€ŸOŠäø¼yRà)ãÒ/ûýÉ€Ÿ}gÈ>Ÿz>‘D|åØàÎKuøòêµðÖX€ª‡ˆS@”T )0„÷‡ûð¾¿|a¯î§ò}ú÷‚²TáâD2ôA;Ö@öm¤}|9#Áƒ,)Š z‰«hûöÅùݽA}ûÅ“Û`CUðJJ¼ošÈAë8È¢(cï;çñðC##1$RÚÎë¯öâ‹nÄ/w7k.ûR¯gÚ}XßàÍj›Œàî°¼¾4ƒñü¡®tkþhÛ¢™O…p Ÿy ‡OêÎÃä´aÕ]7ÁV>]˜B3 Ì%vCµ7æ¢ù彺ƒªsIó+oÂVVBœ‹–³¿~OWº|º;ÀÑÓšÓ”Ûì¸kåZ²Ó ެªc†¤È!‘%‡¹0º»ÃæÊjXY6ài2BŠ"x ‘0°àa0G\–`eÈácdîÛ¼xQ{ºîÐ(<åÙæÃ:VøgÕ'¾÷à5ðº¯\ÅÃmçR¦ÛõäMØø­ßâL·¶‹ IBó@6V-îÃ’¢hÚ>ð›{qðÜ ZúÂä@'dÇ¡Êá2¬ƒ !5w¯Z‹W›OŽ ²H[<€o^8€¨,’ÎÐ%vP³”—ª¯ü4ób)åf †>ã¼RÖ‰Ò˜/Lò€÷CÝhŽfeWˆQ|³å÷ØaºŽJ%m±Ãøukg8Œ•%%™µ]·Ø!;ß¹m& GP¯7dOì&Žž;]ÊÏjî]æjG ±ÃÔß,ƒ«*ÝT¾P=›¢à3€0Ùæ,ÛŽÇÛß&sh1ž¨ùg11®@àÐÆK#dF €5ѰÚ9Xí™Ýs°²J+B#Q$Ĺ_âp&îR“æ„ÜÅáäxÞ3؆.>DÆÀ,ìè¾.4ØK°Òá†×2ÝÈÇÇàKÄÐ*ÊöO>ÐÎ5 kÈ (ž½Kº€@ ©ˆ5h[=”H«¦4gÎù2.·¯?T´‚‡`ˆÏÈÝb360Iaƒ*'ÁUIJÔ0µMàrO«Ò7þçïÄ;û.èr%Ø{øS4Ô”Ájæ²Öž|»;T/qirɘv¼Ÿõáì¹ÍéVÿÑ-09m‹bÂ1¼ùµdüאַÀª»n3˸³zŒ5WÉÕ!¿¸ [Ÿz®š*r¢',*:G|DŸà2ŸîÐÞ£]x÷ªì‹⢈þH>†¦á4™ae98Íf8Lf2ÈÒ ‡içeEFg8hè>ÚœŒCÜAÜŠÑF0>Ÿ¯Óëõ°Ý¨uì‹GÑà(!;ËÀ¸m&ÜÛ¸T³ËCw0P‚‡LÜV/qâÿýÓká°°)ûr.þéË×ãöïÐ\nËðà¢<¤Ë¡sƒØy¨¯î AÈ:àÊî„ÓLn" •mu Dð@ ÌB[LßêâEÄw/¾Ouç"›b‡”¨Yt–HW¹ÅªQð#±ÃÔ~Mž“#²€g{Žeu×µó£øî¹°ƒ½(Óöâ¬'A¹Õ÷Ìk†™‚-ý‘¶è";ùS—C0&ÌžwÄfŽ™;]ÊÏ;`îmYŠFÇŽ ¾·8 qHª¯ÉŽ/WnÀOúO‚PDóý<Ç«"«ˆ…d¨c®˜ÞJ;@ÓmžH/‰ ÂâQžrKÒñAoÕh %n’Õ„XD†$©DeìXLf³…Ñ–ñï&ƒFv\&cxÞóJ4ˆöhpѶ_åû ó}PFO€.¹&)| É½uîMÒLE„ì_NÛ—ƒCQttŽd´b|"!!²Ìm4ž{þ®@{àJw‡q7†‰{šø”÷¬cB†Éí„¢éÃÁ¡(þã`;þàÖ€$†åu X–N+}‰Ë‚¯>~#ž{áæ²GÃq¼÷Q îݶ>;÷#yvw0›YÔÖ¸u§ñ_>ҕι¤Ÿþò]áü­]HDfwÒ3;l([]7öoœK* j¬ú[»pð™—2;x7®FíÖ)·–» ÑV1Σùå7ákn5ü~‘â 4½ü&¶<õ§à¬‹1Σå­ÃºÒæÛÝF´/¦z}umVë ÈZG† - ++ |ÄÑInã¶XQb±Àm¶‚¡i2ðÆˆ‰‰D^3é !(²aûÊc¶beIi’‹­Š"x •0²àÁC&'«öÓ¾ªé­ë*µ B² “ú…x¾/ fãxo—®tN ‹§¼&¥ØÀœ¿Àöu•ؾ¶‡ÎkÛG­þAð’DV“ŸgÜo÷Í}K ¤MQ(µÙPasÎ(plœ ·Ô5àHN?B1¢K°@Ï^úB´`Û]QQp8 žç¾p=b…lç嵨Ð@RÓ‰,Y±C””d⃟æDxÓijNàÛ›®ìRúýI4VT€Øœm±Ã¬nÙÍßÌ2°›9DÙ;€¬fvÞm®ül@±…´ÿw—²po±§Ê0,Æ1,Åð§WgÕ¡„°°8“¦9m¦Øah,)³¬=©>˜‘^Œó™‰PD° —‡ž÷üžè`JÅÁÑní&ƒª†ì? Ù´s ([=hûrÒ1ÆÞk¤ ™ŠSVæ×{®WE®Û¨Ú‚‚>=7‘àH:!›àárO/ýo}â Mºÿ9Æøåî¦ Áƒ,)èó…°LCàþ·žÜŽ×Ý„î^íâç#M—põ /j2_€0ßîËëKÓŠÌvŒþrw³®´'þoioÛyxRÈïð–£úúµ¨ßÞˆúí×zŒú[»ðæ×~!…˜#Ö=ð‡°WzRþnq9À˜8C´õãwÚÕa&¡ž´¾uW?ôY2¡­oÑ}ŒæÛÝ¡ù¢ö…s—•x²C2LˆRàãðqôÐA¬¯ð.ZÑ/I ü˜“ƒ±ç;3:lè¾Û¾t<ýÈîWdæ*^H”)Áø|¾^¯wCª Ⲅ¾xÕV;ÙY:˜KLMîk¬ÁS¯œÒœ®;4Zp.Í:ÝþúÞµðºçVÞÏ'x€¿{p½>—?qy˜õB>&à{»ÏàŸ~ÛJ:ƒJ,T9\`(ŠtF‘ÐX]KB¦P@Sxz ¢º°Ûí°Ûí¨¬¬œøÜÛÛ‹÷ß?§ý4û÷Ù;`ÁÄãx-vôÄ#ó4\ÕáR‘NÎè·Pò%›Oˆâá 9ÛûF/MH-zHÑV^–Ð â*·§ ÅãØ-,dU/Êé¹4ÄKÃÄÒÅ+v˜«/VÇN3ÊG­(笀7®º×5ý!¹p^Ø’¬´z5>ÿÜ;ö;S&Äà-³á,ͦL/‰ # xÊu®(GÒÛNËÜýöä;ûÎ@Ð…nÂ-ihûr"~0$²˜@ ,òi(ŸAÿÀ´óu•Ï÷/š¡ræ¬/ãÕçGFb¨^â*ª~ù/OìÊhü&™éòÐ×Bµ×¥)xÿÇÏ߇û¾ðŠ®ò÷>ƒ'¹5£6èuw0ÑîL(-µ¡¬Ô¦;ýK:Ý2!âFëÛGÐúö8¼å¸æá»°æ¶Áä´j|f*v`­fl}êQÐ,‹è`ê1b-÷ä½­­oFëÛG ré8pÞ«Q¶ºŽLª„¢&Ô3€ŽÇu¥}ôîÆ¼»;èa]EUÖóÔ¸/+ ‰8ÊIL¦(ˈŠ"BQA€¢óù•?‡?7t_®õTœd÷+;ÉÌUÜÁÁÈìð £V®—'‚‡L࢜þŠ&M]£šË¨¯°ccÍ]Ú„{í#þ‚<´øu¹Nl¬sãæ5s[W–ØÓ[Õ`ûºJÔ•ÛÑ5¬m5ä®À<Ì2Öÿì§i·B:Ø8UqV)B«kÁÐô¼+#‹î¼Ä&gVÝcÁ?ín2\R RqêÔ©ÜUF«ØAO^Èf^éQcs¤!xÐPvºõ™ÙoAÓDâ§s>¶&D×^ ؤ´Ûä‹Åà6›áµYõõEžÅãy»¬&xIžߦ‘'*™g1‹(Ì-ü¸3ì*ää ¾·ì<Ùñ;rB* ,c/üëÅ´æ3ER!‹“× fƒ%eV€±Í›^LÈ2Lf c(1@MãºWëù¡%¹Âgg<ˆ—ûÎ’Á@È E˜.~°Vƒ²-e­Å:Iÿ,Dä@ fÌ RÈ h_Õ´?e*Rù¾Ì꿈‚þ3Íg|ç‰W€ÿ‹?ÿÍŸ’ñŸM2uy¸iK=î¾c ~»_»ËBßpû?jÁŸY£»þzÝnßހʊÌV­fX«2‹%xý×ù}ñ ãØ ¯áäÏþ ×å~\óÅ» 1.[ß:Œ£/¼¦[ì઩BãW‚­¬±¡Ôñ%&» &‡5oí㟯Óëõ6ØhÔ:vÅ"¸Êé&;K&†Ñœ¦¹3€íë*5¥¹¯±F³àºC¬-¯*ˆ¾Ô#ØXçÆ¦ºùoä–ôOz IÂ@$Œ*y±Û9%bBÖ¡) ¥6*lÒ‹€ûÖ¬Çÿ:~„t0…¶Ø(69+‘2ðiJàñ‹ àîPQQ“É·Û=!h9dŠ(ŠhmmÍMÅ)¿ˆØaœ›#…à!Ëb‡Ùœ 0)ì^w‡©ü¤ÿ$Œ w­®ž.z˜£’ªâÌÈ+*À2´¾þɦ#D:ßÍì{jüž‡ÇÑÅD¨jšcgª®JŠf퇔éŠTì@¨Km`pr^Û±ü³¸íÌ¿’“R°ÉQ˜Ô´æ3U$ar -­pÀåttú絟¾û'(ŒgªúÏ[©8•|IÜ *…¡Ž¹?$oÞÍ ­Õ€© ´¥àœD¡­GIôÁÈú >àýÄpféIÐ?PÔ¼û»‹øÒÃ×f”G4& Äe)ø¾øÒã¿‚$g㹨ð804ªÝ½ôõ]Íøáß%ƒõ¸<,«qã«ßˆç^8¤«Þ¿zïž|äVÍéö>£«¼l¸;”–ÚPVjË(ÿåCÃ!ÿÅËxãÑïàÖ¿ùs¬þü¶/?S±CͰé±{¦?`hxVÔ ÐÙYœ|ÿnõ”À\bÏK?w8޳»ÞËIÞënÛŒ­_¼cN‘ôycùRÜõ—_ÄÕ·oƾz]·ãCë[‡‰àP”„zÐú¶¾÷îU¥NC¸;辆³ï¶Äá‚?ƒ¬Ì~Mge98Íf8Lf¸-Å!v7’‹C*:ÃAŠlØúyÌVÜè­ù™¹Dð@0:;ü¨•ë‹G‰àAïÉÆjCw( )MSרfÁC}…ëÜhîÒVVûˆNÁƒ¢ª )*ïý8 ëº0zà3µim§Eð ·¯»‚#'xàsp1zÿ?)X±ƒÛí†Ãá@?dYÁ”X,¨r¸À`®", 7ÖÖãÅG!*ä8$ƉÈsœ[§ûQœŽd\Þ¸SC®D sñÉ'Ÿ@sp-‘2ø;‹ÎTþ]"·É„€ ÌŸW6Åytw˜Ê?ô°eRôFyYF“ßÆÊŠÜ÷O¦ß¥;Œ6³ Ê4 ¼ _¹] !ÅÄÀiå@QÔüN…$v˜Yï¹Ú3[Ú-a`oéDö·–,Ãc•×àåÁOɉÉàØ7»j€R)­sƒ$(I¡ÓŒšJÀX4 dI3ßꘙQßyk.ˆ»!ß( (Ñ ډ׾´”¹´¹`¬ LåD1óšž,ä )’t* „"¤¥•¸äÀåžö¸H:bÕå.4Ô”cÅÒr4Ô”þþ¿/h{6xæÜ@Æ.ßzr;ÞÞwgÏi_»o8„½‡ÏàÞmëÓNs¤éú†Cºú-Sw†¥±¢¾4£<>8Ö©«¯Šƒßÿgô}r·þí Vf¦b‡«ú,–ß~쿱V3Ê×.‡I:EÒ 6+®‹qgýz>ÌþB7Z…3©]߀G_ø+üú»ÿ†:µ_ÛÇG‚ð·våÕ„@ÈÅ1Ûôò›ºÓ?ú¹FC¸;è¥+0‚ÆêÚ¬æÉÐ4Ö–W¢?Bb,¦ÈÆqp˜Ì°qLLá‡0˪Šp‚Ÿ:(jñ<»:é0tý–9]‡ÙýJ'™½Dð@0:{=”±r’ª /EµÕNö”FJ­ÚW>hê è*ë±íË5»<Œò1ŒÄc)ë)È2,lþ§P=î.3n^S‘Ö¶Z°}]¥fÁCËð nXZX7ÀQm/Òªf_-D’“¸ßÛ}Fs¿å»Ý›Í†ÊÊʉàM·;)úzÿý÷‰ØÁ Ø8U—!æ(³iI >îí"A ŒÑK±*ÑÔÀcÏvו?ÇqX¾|9–.]ŠÊÊʼµ3 ³³sá ,2±Ã8W¹Jñá°oî¼rÌŸGw‡©üCÏ1DäëñàºúéNs´1"Џ0ÀU·¶þ¡R÷¬¨ˆ&DŠEQAÓ,, ÇÌþÂ]ï>™™„¢à²šà´¨ˆ 2² IV¡ÎJp, 3ËÀlbÀÐÔ•åÍ)|˜CÐa$±ÃÔïÒÎoÊg»\NN>+yzÙ-ØãoEP.<»æÅÄÍ®±YkiÍÍ"Ÿ ͶY94,u¬ µ€²¬bÎwZU¤hs­Ö“( œL¾€<8Ú—ûÏ@0Jj¼r¼çʡΕ\ (ÖŠM>s¢Ìc×¢Œ))Ž(Jr÷¢Xk ìa¦.È ¨‰¡Ù'%j~—cU BƒúëÎ 9¿B±1ˆgœG0ļÃÃsÏ‚$w‡™«ùÊ…Un¹vÞû¨EsÞ™º<ÀOþñ>Üz·¾€õ#M—põ /j濆í bÿ‡-ºÊɆ»Ã²7ÌæÌÞ½½ø/~¼¯h¾¢‡£Ï¿Š3¿Ú§;ýÆ/Ý“–»€É‘¿ÕÂcþ N¼¸ ¡žì‹Ö\Ý€Ûþë}¨X¾4ã¼Ìv+þÓ¾ŽWŸüG]NŽg,xˆùƒˆûÿÉöÅ?y^ë€×vï㪩7%ðœ³Yઙ\øÔU[Κ³Æ0mÙ°ªø­Æã1`bXÔ¹K‹j¬ˆ²Œ°˜9#ý±"9ÙBR”¿$3×âD Ïç x½Þ=3j{y"xЃ3i¿¨ìÒg£w_cfÁ´úu 3’®€ö•ˆo¾*=±ƒÝ¢ýñÇ5ø§ß¶jkCp´èÇ{*áH”—Ð9Å?ü朡ê;¾:µÇãÝn‡Ûíž3ˆspp½½½dbË÷~cTÙpšÍ¤317/[NÂætx£)<ˆæðæ¼)ŠÂí·ß>!þË'Ÿ|òÉ–mB6óÒœf:†×jƒ/]8±ÄÙ±ñ*àýPw^ÇÒOúO¢Å·¯»öJÑCªûöX &Eºú,Ù?Ñ„„hBœ¶¢¨ˆ b‚Ž£á4s`ZØÑÿ3‚óc¢„¸$%… 3ÃÀfaa›eáAƒ\}Örœe*vÿÿª8pÎ Ä“ûªÞ\‚oVoÆ÷ºß'''óåªk’ûo…8ï˜Qd²¤Âfå°®¾ŒÉ0Œ®r9ŽžœŠ}çùxc2àåéKA@(T1ˆÁ´Âÿ)Û²) Pœ ëJþ©*® ¤-Õšë”Ñ*ÿ …¡Šáô¯§5’Nº @ Ѩ°èûàƒcøåîæE×nÓŠêŠTW” aiYZB¸eÓ 9uI—ËÃàPdB Ð×BU…CS`ÿú«½xâÏ>ƒ—þ·¾¯´s IDAT`þo}Œ§Ù+õ{ùxBįÞ;¥¹}ãdêîPâ² z‰+£<.÷ðÛý-1[ß>“Æ­O=š³2>óÒ„¸B+¬ÕŒÍ_}Èðꡞ}þUÍAúsa¶Y°å‹wâº{¶eµ®f»÷ý_ÿ¯>õ¼æ´¾æVˆq~B80þÖ® QÃÄç‘`N÷Álõ ki le%ÓÄãcˆ"ŠŸ³»Þ›wŒÌÅרZð}pnh€ „9à% aG8‘@B’о½­!cÇôUXm§þãܧ§ÉÈ\<Á¡Ø F…Â’'Ë‘=¥=B‚æ®1n›6±D}…÷6.ÅÞÚ²ÛG‡±9…M/‰†X=ÝÕnÙ}Kš‚ §=Paû:}+D¨r8å±ðÅÿy‰<­NSQQ“É·Û »Ý»Ý®kuêS§N?hŠB©Í† ›ƒtÕµ°qb¢¶‡þW¯ð¢ºba µLlêóŒiŽsCÓ`jîßiš †"åÕßžÐvýžÅ k¦»Ã%}îN§Ób‡ÞÞ^ -LarcÈ8¯¿ÕÛâeþ|²%vP“_¼;z QYûKÛ¥•lX] YÑÕïGK×`F»yßè%àLŠÒh§/¨¢‡´E#©Å3ÓŠ’‚)§Õ+ÇÌ_Æb‡ˆ(Â]æiå,uÚAO]¥~!EY?ngû__ÌŽØa¾rn û'¯/¾Y½;ú>Öåòp[ã*mB—&K ©µ|BD¡r§g¼&;P%¥5gŠ eRìÀšVŸðšf(P©\!Æ¿–ÂI‡­ç ùèbŽä³=CqhTŸè¬qmíœ9…Êh(†ö^?FC±œ•Á24*=N,[âÓf,ñ¾$Éàì%_AïG5vyúßi,MÖP&!ÿ‚ü¢ïƒÿû{ûоSÅ Õå%XZáÒ}oa5s¹<|ãë7Hº<\êÁÚ5ÚÞ~ë©íxgßt÷j\æ;ß:ޝ>pÓ¬îð«÷N¡o8¤«o2uw`X«Vfî¢öξ–‚Ÿg~µÕׯCýö볞w¦b‡­O=:m•~#Ò}ì4š_y3«yÖ\Ý€»þò‹pUæf…ôŠåK±å wàØ¯ökNëkj½Âm#æÂßÚ…PÏÀÄÿ…@|dR„1ø>c¼–­J..P¶ºnBa-sÃVVBáÒ}ì4:×þOn½&m¡âBÑPS¦9Íåà(b¢›Ž…Œ‹^’äã ˆ²¼hÚ-(2Zƒ#†®ãP<ö ¡‹ "x ŸÏ×äõz›l4j»ba¬w•’¥ņQ^ÛKÓCçq_cæ²þ¸±F³àA”et‡¨u¹ Ù>®Y-j31ØTçIk[‡Eß)bûÚJ:¯-Ȫ+8²(태¸äÏÝ1æñ€eYx<pwÅÿÙ ££@€Lhy¢ÄbA•Æ¢Hg&¸¾zŽtµkNg${Í\câ˜9ãX͘4ƒ+-&nN!FÊü‰8CœîÀ¥^mçΈ,ÀÁŒ÷fˆv´b@ÐÄWUeŒ* '<,~± ÂÂ2¨±ÙÑ Ͻm6Äš¼&{wä’®=ó•oEi‰b< E‘ð«÷NáÄùÌœ"&Dë¯J…´Ú:§ÓCÊ~Lö¬¨óŠ¦Ž àó•ðiŠB¢_4>k9qIÆ¥`Õ;l\¶\Ô…ut˜ÓAÕ&fÐ*r˜J¥8e œ<çºY‹n—‡Jkk ynê â§oսʤ°3þ¢úº±›úؼǠ"«pÛÌXViC3§ßùÔdfæž3”0—H&“[¡7’õH |³å€®,V,-Ã>{mq?»èƉóÝŸ[¦^77Ô”a}Ã\½Â›2 É(Ä"Ž4]ÒµZ.@ B&D£ìöÅhöú®fœ=W<« {œVx\6,­(ÇeCu¹+'‘›×Öê<8ÔŽ‡Ú8! ‰!âQⲤG‰Ë‚?îûÂ+úî­‡CxñðåÏß0Mô1îì‰9Sw‡e5nMŽ©… OÈtð™—ðÈž09mYÍS¯ØÁZZ‚Í_{Èðb‡Ö·ënãl˜llÍ«Ãl\wÏ6|òæa$bÚÆë@s ¼›VÃ×Ô kü­]9unÈ7þ‹—§ý?WM8«™ˆ! ŒŽÇqv×{ºÓÛ­&<ú¹FõËa5Ãn5!׿v¢¯Ûêý¸' ü„È!œHàxoZ†10¶@q•Ý9ï‚Éf–ƒ7˜¸*‡fþkŽ:÷ÂĨ]ìÀÑô@w_ß2ƒ-.ˆàP(ìð £VnCr*`)°¦…R«U³à¡©3 Kðpßæ¥(y…C0¦íÅdûÈð¬‚#(6t¸;,¯Lõ =@ÒåA«àÁ ̸ ðñ¬åõ?ßiªf–‡ÛíFeeå4—»Ý¾ }!Š"qwÈ×…;àÚé"ªz¬¬«¨Ò,xhïñ/ª>D‚8ÿ¹<7¾u|ºâbA@¤-öÿ³÷îÑm\ç¹÷ƒË`p#nH¼)Š´HÉ’iK¶%;‘ì8ŽåFNô‹c§MVíž´Nܦ''m¿8·ž&§µÝžôÄî—œÊqWc×qkÉ—Xr“ZR$ëÂH¤ ‰¢$P¤HB ’ˆû`€ï$H‚$fpûY‹‹ 8{ÏÌž™={ö¼¿÷q££Ì´vð± Œ\äµb±[·n-z{Øl6øýþ¬©È€B`‡ä÷õ* \¡|Q†?ìr®-[ŽMœÓׂS°‡¸ÛÂÞv‹íü1~2ð6Ѓ¯'‚olÜõºXFí9 =t+ï{ŠcH0]f¹ôßM"0ªiˆ’àg†°C qŒ-;$ÇâÀð´&•z9½Dý™~.AØa9ë2;|Àá¹—yOTnæ<?o$ð°`øŠåV¨%2ÀÔË;ÜÄ¢q˜T Tê3'* ›VH—¾ãQ€ÉÓ|ÁI90™C½p£ ƒ!~™B÷ÝݶêÇ_Öj#¬ÕFÒž•»ƒJ%ƒefÍöC_'Ÿ»ÿß?ÌI}ÙÀšêJÜñÌçA)ä‚n³î—ßÂð© 9«ÏX·Ÿxús05Tdûi•[?u7g—‡Ñ®áð3GnÞÀ¬“!JG¹¸n¿þù{ VЂܿö œ¼0À©Ì±A;”Ñ4ÊèÄqtOâ@÷i¸óßËf»web¬ Û®•+ £W¾g.Oô‡ý‚>>L,ö"éÁÖžð@T*z èA£¼h<†±P…Š)ªT—Á>Å-Àò`×0¾õ÷—É:¥ û¶UãgÇ®s*7äu#²IæƲRÏFy@[êõ/+—ñxvoªÀwÞÈÿ¾KÀC:WîÁ)؆øe5 ( MMMhhh(ÜN}}}`’ݰТ¥RÔé ÄÕhIm³Ôà%ŽeBöaWÉeo¬Õ[ ¸:4Ω̵À:4H?ÏáŒØàgùÝÇZ[[‹Þ ÃàÊ•+…YY±…\Ö%ʬ®Z=º²™|ÌdRݦø¹;tnž›pNä z8áÆW/øñB쨫،öÓ S)N@ ¡(Ë v€x<w ½ŠÎv€ðEÄ–ZÏ‚eÇ!£Q˜ÕJˆ±X±ÜgÁÈtYpƒ–«·r¾ËC=­Åã·àå±8‹—SÞÀ¼,ÅV0ÌàÀÛgJv¸O߈ûõ‰?º;,8Æ”X s¹eŠ™þK"$Ùeç—-5÷Oö}íR:’6‚|»ÿ$¿qwK ,¦µóBZASØÖR3>šò0éMŸÌÄ Q êšÍÅþïílF[£ÝWFÐ78–1ôADDDDDDDD”¹^úéix§Ã‚ڦƪòÙ1aòÀ:óŤ”kÙÞÎf^óA ]Âá(††Ýh¨ç–AøÏŸÙ…w_Æð¿w ¡ƒ÷O÷áý´…R)ÃC´dUÇkîÞ™ÔTëJòš¼òÎqÜòÙûQÞT—U=«v`‚!ô¼üœ=¹›³ßòÉ»pÏ—.ø¾´Þ»3ðÅÈ ,q…!’¿‰ò£À„]/¾>{\øjOg3vlnì~n^ÏxèÅ {²`N¥ »N.‚„,O(˜Q|Û '}26š¦¡Ó ~ìr€œ™kOx * 9N·Ùl~ÀãBÝF»ßK€ŽÒ˹¿ôìtÈ@§äžÕüáíUœ°O¹Ðbœÿ€ÃW„­™jž“s»6Up.ÃÇ­¢Ôõ‡¯ò*GQî½÷Þ¢,ý~?.^¼¢=Œ„ƒð†ÃPP6+€x<«,ªD«[JJ†Z­7<Ü2…_ìwàHð2ðš»pÏ|š öu†ýxc”ß½¸¼¼›6m*z[œ;w®0à¡(‡ ¯¨@erP—ZJ¡^]†…ãTQ¦ëÉÀÝ!87%ó/÷Ñëk+±¡vî9E$–ltöïG÷lá”{Éç¡Ð¾ÔsßóÝ…õÍôòû4ó]Òéa½V©D¼lûÄãñô@yÃÆac%³ ®;$ˤ­s ˆÀÇD1èñÁR¦ê¨SаWÈ!SØa¥ò;}À{sAá_µlç <Àñî~<$ lú‡ŽÙJ>Ëû}úF|£æöÄ›CóÝ\oå9*Trˆâ)ðD‘Õú¥”"±(}Áx÷¥¹º?üD0"@H@I9nz¡ÑÈQnÈ|̯ÕÈñÏíþGVô¶xúvB¥âï”^S£ËªüB=p_3þâY:k §uS%î¼½m­fÔVkÑÖj†V#_t-Ù.:ñîá>¼{ørÖëüèÕ÷²ryX °Ã‡ÏýKÖAÓIÉ4îú÷°¾³8ó`š LõŒ8È͸€Z†P´P–k¡©1/‚"ˆøéú¯Ï ïícˆ³ë+ exjÿAïëÎö¼øïܾ¼Ñ{Ïܱ›œ,H8^p½(u) ¡oâA§Ó9@Îε'<•’^€€‡ Åd$ ƒŒ&G*CJP ŽðÀÑKcØ·­šóúöm«FQ…A7â²×5ºxˆÅ‹O¨‡£ÜÜÌÚÌ$R ÿèö:zݜʬ%:ø½ž›°úx•½óÎ;AÑž?žtbÒDÀ‰`b‘…C7†T(ÐвÕ;ä«/HÕ—‡U%ÚUèñssQ±Ùo x˜ò²vP)¶æÁe1`{(írr™ët  †:šrÜE€8»ç-I߇F<Àró.Ùœ¿Ràzb»_¸ñ[âwïêh\UÔg—©éà<— ǸÁðÜœYÿÈi0"""""¢U ‹QyÁçU&mVå­i‚å3•‚¦`™À¡c6N寯}Yµ™J)+¹ãüÏæ]öÿ¹ÿV4×U”$¤kñuy8ôn/z e^ÿU» ZM5¤RqÆõì¼£øûxéÿž.Ztn«ÁíÛkx—§i),fMN·I«‘ãÏŸÙ¿ü6·ó\SFãû6bçõxà¾æEpC:ÕVëP[­Ã÷m„Ç{~þz~øÜ¼Á‡+ïÇŽ¯=Y÷çàl`s{ÚÿÔš‚ Õøô_}šŠâÆM¬ïl#Àƒ€œô 8陃!R®)CpÓЇpåícNzrRß³_¾j…°ã+ eh¬*礪Ê0äåÿOàßÕ€ï¼ÁmBщ`Ô7JuÙ¼ïCÑ(äÒâu£ƒ<èQ³.³É•<»ýj¯ÓsFýÓ%<ðiwuJ{úÂLVîEoƒ±±1ŒŒŒ¬@š '²Æ–Ñ4Ä)®îPîPÃ^@!¥P®TøhVÛ,5ø÷Þ ÜÆ1./‚a†¼"´ø¸ØóÇ$¿™AÏô8¯õ755A¥*¾«Û¹sçx•‰DˆÇsàØP@!×°ƒˆC])ßmÔêÑ51–{ØÁ3××vû¹O”*h ›ç±4 ¹‚¦r=øY_³ÿ ÿ=r;îo¶jvÅýô1 N9GÑa2B-£Ò¶8“cµÌw ã;PqfëYð9œþ Ñ(Ìjå2Ë v@J›ˆ8\×|2Yfc8;×~Õ²_¼ú·qñtSÞ€ ‚Ìœî+éûéã•·à‰Ê[HâÀ¾EçƒT"Fu… õf5 Æ €±$ëí hÉâ>4ââQî÷ LõJbng äÁ³ý'xU!—QØCÜrª$À /Ly³Ÿ‰ÓQB‚îǧ|˜„ ¾åZ%îݶåÚùó'†s†ÊµRÌB‰K€ºtâÃüò¿g­»:ÑÑTE:‰é5J^@‡ÞíçòÀFc¸jw¡¥™ÛûÉï?{ޏŽÞ¾±‚ï…I…§ÿhgVulXoÌË5ôätÂvщWѳìrIÈáûšñÀ}³Z§V#Ç“ЉÏ}¦_yæ ïëlàhš¼›S™l`‡êÛ7£ãñO úZóâì_ÏYàtÓÎ<øg bߪÛÖ¯!j)Œ1–!4Õ•‹ E¹Êríšj#&Âð‡Ðÿ«39»^àÏ>¯÷–ÅÐÞÎf^.ÿÜ}ÿó㟂D$ZÓ×™+à[Sû[îƒN§óMrX›"qD¥¦ü³P7Îôcc™R‘˜© U©æ<<;‚ç¿°•×úø@Âåa!ðËE°Xµ©:ó‡–lÜ £^‡ŸãVÆé›^µç¹Õ¬žýü‹SÃð‡£üê±Z±?gΜQá‹%ú%µt6¦`”Á°×=~(W(!“ûÑZUÎ%E!Àp{‘fž@›ÕLHв5œÞº§ÇСIL:þè?—"•J…ææâ7ŽŒŒ`|œ°¡P(²Û€5;€ZJa½F‹kÓž Ú%CØÂsA¾¿ñpϸwKSmæÇ=‡Ðü`èº}øFûV@Ǭ؎Ñx]cãØhЬT.Z†Ç3k³¥¾ãèì ‘É0! ƒ‰ÅV>wÒÔá0PG¨SA!¢øÊË'?‹2hëlˆÖ Ð¥š=TšðE¼Ãùü³õ;qWGcÑûb›ÝYšÏ¦r=¾bÙŠõLV.IxdPϹ)H¥ Сڤ„t ‡Ø9Ã…¾Û’JEó¯ã|Ã?Ñ¡D%O\ú%hçTæbÿM< ^“–sÐöµÀ:4&±a4Â/࿵µUüàÆóçùJ¥2;àaÁÉ歹j¸#a¸Â¡eVÄvfn¼ÝãçžénsS §ås =žê‡ó¬ßk¹ê6£6¿<é†;Áz­f6ˆ¢l<³6ËäxgÚ”)1äõ!¶T½+¬c"šV#ì ÀÀeÙäçšp#p “ʱϰ'¹¹ßÙì7‹<؇]9 Ò®¤T0ËæÏkðé2‘U®Ç#¦fܯOi»°ƒ\&Ù¨œ:$_”‰çìú›}Æ¥ÄHʼn:ãQ€ñåvøH\OL…?ÛG§†xU£/SºŠ‚aŽqÏ,ÐÌ@DDD”•zÐÿZ‚e”j •Bµ‚†Œ’dtŒ–¾³±üÁD±/ž…!lv'Þ/²#Y(ÂàÇoœ$ÐC‘•Ï€í\ëç¯÷àä©A^e÷íj#@rºçÀj#o—‡_µã¡Zæ}}`ZœH ÕÈqðµÇ±ïw_†?PèáOžÚ‰†zïò*• 5Õº¼ogj؈Ãn %’C¶µš rÝþè¹}¸1ìæ|Í9~Û›ñ²WÞ>†¾ûO¼¶o­Á”\†}ÿã÷Q»yƒàö“¢e`­N-‚!R%©‚†¶ºr‘#45• Âc0Á¼C£˜¸2ïð(5öJËìßl,VRm}kCæ“;jyv·‡]›*8—ôL•D;òM’ÁP?:ŸÿøÇ?^üE†ÍfQa¥¡ixÃaø™Êd4§²îPîP2‰ëÊ4(W¨¿¿ãÞ¸t½ãN¸~•*´˜ÌØ¿i3LJ59!òüà}Ý1IŽH𲘴@/· Åîé1ÜojÀ/FùÝ‹M&оï6›7x¸eËôõeð°Fa‡¤6jõèšC(Ýs‹ˆcû±âÙ/º}ü2¤b±å]Ä4…¯ýÞn¼öþytõe}>öøÇð¥žÃøžï.¬_/¨øŠmáôà‡Ñf4@ €HP©ît‚´D‚­Žé@Âé#lŽ.t‡(ìîçò¼ïøÂ"žËvú€sÏ¥—7qŠ•¥vÞÜ@Ûð a=:ÕÜ©©YòøøØ®…¦p-8'ãǵà$®…¦àg¹[åzt¨+p¿¾ë ’”Å€|€:£N£Vs¹bÉs'‰->bQ x¾üÑ’9Ø!âÁ"pŒk?»ìƒ‘ø7õì˜àÛ×Oò®êÑ=[ÖäøË1îÁ¤7‡Ë3 5áz$""ZÛ’Ë(XLÞå %ôþY -F-4ÿytkµ‘ÄU,µB­Z•B–—@ìdIwˆJC"øhtrßú§÷Ñz â¢>÷ϾXƒm-5¤—_—‡·Þ½´x€«vÚ6™!•fž8¯có:üðûà¿}íͼïïŸ<µÛ]Vú V#§ýËVµÕ:Ô°X¨=·[wü§2>§+£åì¹tëŒxèχ±Þ"È}¥Õ <¬QEƒáYbÖacKŠE¹ÊòÄx/KD–àª$Èzý1Dbª‰+ h+{E>ÕXUާöï(Éãú…¶ñ£õÙðXûvÈ¥R„£QŒú¦Ç["’¢P&“C)“A"ü@y^º»ÎŠcƒvôޝ~—¹\ðÐôA§Ó9@zêµ+<•¢^€€‡ ÅX8ˆ šdÓÎøA^£ƒ}ŠÛKÒ—^Ï xøÚÏÎÁà,ÐëhÇo7ðXûvÜ]G,T3U‹‰»åè„ÇX,±XDH°²¹´\ ¸ñ£ÁóœƒE“jkk+ú~3 ƒ+WøUUUü$Ú­ IDAT‡B ¹¬+‡°HEb´éÊÑ51–f¹Üþí›Ëæy-Äú½eÃâùqÏ&É`Ý\@£Œ_¾üþ»ÿvܿє±éÛ=E!–E×è8ê5e¨×”e| }7ÓöTò%4GWZ"AVŸÁ(›yBRZÐ žþ ¿ËÖ­ûC (b@0qÌ64á‹x‡ûs‰ÝYTGªP˜û½¤]UD‡ª¡X®hΈ¾4÷%µD†U%:T‹ÇrÝþÄ •kÁ)øØÅ/œÍ²„kD‡ºrésæ–Œ{£V£–N¸9dt½¥Q, Hø½¡ä’Dyfšß=ˆ‹^Ô1À ãá ÿÁ»šm-5«>85éØ`™À”7Ǹ''Î@DDDù ú'"Ž´j94*9´j9$bqQ¶á½ò_„„˜H ‡"ž%æî04Â/ óCwçwÎnÖÁÌåE0Ì f02îYñ¾jÐ(a1jŠ~Þ[«°5œÇôcã~üêû"xÀï`hØÍÙAáÑý›166ÿõ÷ÇÈ“ÓC.`‡†z'‹RVmµ­›*qñ·ÀÍÈt²²¥ÇŽvÈ\U›ðÉ?} jƒpïLHx°—ç’¨!¿J…"P`è Xj¬*Çß>ýÐ,t\jÊÆåaÔ?C}6ünkÇü~‚eáaYxB‰6Z*EMCEÉ ¤V×=õ™;vã»GàF‰$óå#©T ™LðÇíé×¶ð@Trr:f³ù(€]BÝÆÁÀ4¸ ªÔeœ‡žA7ÜtJ~7ÚÇw5à~É-`ŒaYا\°ê/a"])r)O(ȹŒÕœyfôl Î¤â <¸ÃAÁ¡h”W¹ìåUŽ¢(AYúý~ÞA–DÙI"ìÖÀ¤ŠÁ c:F8…L’–¤ÄˆgHy‰X é2/Ó’àÃD € J86Ï&²v˜ÿ/uÄ¥ñQ|¡}Ûª{@·LJ5ŒJ\nÙàG'§ÑXUŽ`˜NDŸ€˜Ñˆ‡'®óZ_}}=***оßçÎã nݺ•ÿŠ…(ä².Qæu«) ë5Z\ózR–‹so¯H ðä>1Z]©_¼w1ncÔ\Bðƒ¡Sèö5â+MíPWÅæ·ûí3à†+B½¶Œûq™i{±HZ*á ;$%‹P£Qc"ÂD(¼ri·§HîiA°C6x,S®$otR9vikqÔÃí…˜Ãå)*ð°\pËRz¡aÏ,À KQ-+Cµ¬ ¡Xn6 7‚;B(¶üý‰fÎÀôŽÍ±x ±x|6ÐXèâ1H%ÙïSG‡ºF8•õM£¹¼BÐí8êŸæ´|¥VŽ_œ¨‡Ÿ+Ç–-[@ (ýôéÓüR)¢hçTæ#ûMìél‚}x‚@D‚ŸÌd|$ðÐívc``€WÙúúzèty°E/ ËºDÜ÷·Z©†;†+BF°Cºï˜¹@cgÄǹé××-ôޱÜÇ[îÙ½FÉ{r{¡OõãZϾáíÄú r@[±-|Q¶‰Ih%2˜JHÄ¢ŒaPÑoØ!õs¹R%…ÃçG,ƒsD#§ÒŸ[Å„D9„é:¹,3³}[ü³Àpyà <؇]@g3JIª™ç\i°F€ºÄK~{æK4ÌÓ40®DtB ßh ¾ƒ(‡{f¬ïcDãË;¹¨%2HE"HEb¨%2ÈÅR¨u¨[üÎÌs¬HÆ­ß%ï‡b0á%ÖÏLR% É0³Y, Zbx9¡.ÉðÕ+¿ÆÑ)þ×£{¶”|€Þ”7ûÈìÃ.âÜ@TÒ—)fƒ @DD$$IÄb4 è5JAþö_þK°mŠ0xñxrÿÎ5 =:x’¦KãõK?=ÍÛÝa_–@r0ÌÀ><‹ý7avåí]ìwâb¿@Â5nogs΂w3Ѷ–9u™óþ-åòWí.h5ÕJ¹¹Ùì{°:ßý›_ÁÆ1Ð>êôxúvrvœX(•J† Öµ7ÞôxC9«kâÊ Þzêû¼Ê®5Ø¡ã;Ñù»{Jv •r„ý¹íõe XLZXLZX«Êg‡BHASóž-ç=g.˜kL…#ì30[Š ÎkO¿³û<µ'¯²ñ8cX,ñ“*±xæG*ì¬ÒP†ßÙ} þãƒx•?6h‡V®@{¥%£å ƒÃÌ´T %›ýÏR*º»ÎšUœÊxÀ—å„•—ÆÔå‡+0÷ž1‚MãïôO#¼BÜ–H$‚B!ø{Ó ¤G""ÑØD%)§ÓyÀl6¿@°þnv¿m9XH&‘@/Wb*àTîÍ®aÞÀC½I…‡¶UqÈŸ 0ꛞu!ˆ°lQ…Ý!áSå»7Uà;op¼¶}Ó«îü6ih¼|”_FiN‡†††¢ïÃÈÈÆÇÇy•­®®æ I´´$"1ôrôr˜ _$o(ˆpŠóL,Ÿ÷Ð’îf:Ff~¬2°‘K)ˆÅ‰§kJ,™u•H,—[¿Ë‘yðQÒíáK=Ø¿©€Ëh›¥†3ðp᪱õë ¸rc<íC1Q±e­6$H¯©© *•ªèû{îÜ9^å(ŠÊÎÝa)‰rôý’ÿ+8‘i ¾Û¨Ó£{r¾TP PŸûǵ÷l/j+}‹ñL÷v6àQòÊô—ö984…/÷¾‡ÿæ¹´ÕêhFí3Å„qÓDF “B¾t;¦À”D -Yf¹ŸWø¿’’¢NWÇ´a6¶äñ‹€DIÂÈ´¾\/“²}ê ‰l⟻µµœÏ³b¿TäóöÏ <üP5p¯oq{µ†g?Kèè’ÿ¿(œ0Da0•r·Dsýœ|¦•1@ZÂÀÎÀò}gñJïØ 9 ¦WgX$ñƒhů?æ4¨¥w”â8à°áï‡~Ë»ªDFV3JMSÞlýN؇]°OpÎNK”©2X«–¦J}‹ÇãˆÅã³—;‹Ï~EV5¸25Äñî~ïî‡\F¡ÍjFk㺒¼‰ˆˆV‡´j9ôeJhÕrÁm[ÏUF'³{¿òÙGÚ±óŽzÔVϽz=qjï¾Ì9x:9\^:f›u\kšô8—©0©ùŸ¯¹àÛÄã áÅŸœâUvOh fp±ß ›ýæ,„PHuõ¡«w{:›±·€ ÿÞÛ7òšûùõÑki6ÃU» -ÍÜ’ØI¥bܾ½óÝOàÈ^Å«¿èÆØ¸Ÿóv)•2<ô@ >÷™ö¬ÛF"£¥¹‚3¼±d»ÈýHçî„">îý\)À OÎ`‡íŸ¾[?u7Tº²Òx~Õ—Á;ž]¶ò$ÈÞXe„µº¼dÀÇT8bŒHé·“@ÄÔt“Þ¦¼Lz¼Üˆ| (døúçïÁŽÍÜcvâq€a€årU¥¾‚—HŠ* øðØÛpòÂï燷úl1ôª¤DRb‘r©J™ r ¹TZ’™”j˜”+ó[L•™ß«˜œ¾•¡†åt~b].§Ð›ï陈ð@TÊzÀ·„ºqc¡ ¢e1HEbr¤2ÕPŽ.·‡àC]#p"Ð)ùÝ>±«3ð½®ÑYà!eЏÀ»ì'ö6ãûÏÞ‡ÚêÅî;ï¨ÇŸmN|8€<'O fµ]½C°˜´¸«£qÍK!Nº•ü“p¨”2Á·ÉK?= ï4÷b¹Œâ|uõ rH§÷O÷á¢ýfÁ\Oøº<Ø.Âvɉ¶M‹ÓÉÉÆÆ}œÁ•J†[fêûØn+~õ§ÏÞÀ鮕“b5ÔéqïîõøØ.+TªìÏq‰TŒ[6™KÆ%—:ñáow•TE¦8üõçyÁæö&ÁÃL0„®_Ïv ä2lÿôÇp˞Β@ÂóÝBcU9Ú¬ëÐÖh.¨£M!µÐ-b¡ìÃ.ÃQ8\8Æ=†â QbÚ¼Þ‚¯?v¯ù(“€¸ˆ&~¤T|È§Ô Oíßgÿ¿Ã¼ëÈzHU,ŸuH•’¢ —R $È¥RÐRj6…h有Ç1Á f׳œ.{&…¾Ë/;N79òDx *e€€‡h<†±P…Š© T£ÑqÊîÔÁ³#¼]öm«FQ…A·ÌC^7|‘0Ô2zÕ€ªåÙßêy£þÕçðàòò›©ªªBEEEÑ·ßf³Áï÷ó;êë!•’¡F!E‰%ÐÒ hé„Õ\8E Ê eÀÄb³Ñy„†¦s²Þy¤8OH"Ð*À08>hŸu2h1U¢N«‡I¥ÆÝuÖœƒ¥$%%C­VnXz®:°·³ šBM¥7'ÉF$(Y«Ëó¾ŽRól,;äÌõ¡î"Žu§Ö—/K)lÔêasOp«'876ºäžË ]ü™fŸ]¬Íj†A³?~ãdΠ‡Þa|¶{ߘ¸wn*ì²íOIʼn€é0ƒÞI744…F´T¼vÐ*e%'·s;¤~6)åÐÐ| ƒ%C)“B-£ Aذƒˆçï¼,³„óD]xx€u%Žznp:¿.oÑ€µ/}„gO܉ú!»ó6¯´>©-ŽeÖçÅc‰Ÿ4Ë*U€ž×Œݾ1ì>÷jVÕ=ñàvAS;Æ=°L ëÒU韯«æ í à„…°BûKIîgr“¿+ eh®››SŠ0,"Ñè, 3`Ù|ÁHbx2“™ì3öaBaF°çÕÅ~çlÀ"ˆˆˆr-‰X ƒF½FYýÊèä4N^àUö³´ãGÏí[q¹wÔãÐõxñ§§ñ—ß>œÕö:fƒÅ¨Y6Hp5* B*•¬$²Õóuw¸kKcF׿”7€ãÝý8{iHÙ¶./^|ãDá ‡Mµxÿtçr?½ßÿVz‡­þIôJÎç[zøè’Ûmu‘°]r¢` ~dÞò õ4Öë³r=Yüœ›€rN”¢¾É£//ß0ߥ32À[O}>§‹s]šêJ´—ìðásÿïpv.G”\†Ý¿ÿ0êÚ7”ìã×¹'ýÎ~‚<—aÎ"éP¨VÈ ×(!£$°O$’ÏMLÃ>â‚/Á…k2ˆT žÚ¿“·S$²¼«ÃJŠ2‰2²<ßžvlnÀžÎf^cƒ¤Þê³!ep[U]η/$@ˆdN¹„‚D,Zõ0ò`b,BÑ(Øx Hl<ž•›ÃB ø<ð1¡7ÅÒCx *a9γÙ|À>¡n£Ýï%ÀC†RËh¨d2ø#Ün ov óàéšðÌϸ‘]u`GM",[”ö åpà’Oíj©ÀÑÞ1NeÝ“¨Ó¹?…2„dÉ0 ®\¹Â«¬N§ÃºuëHçVdÑR)h©€BpÛ¶’¨×•ã£Ñ›9©»w|½ã‰ È7.õà±öí¸»ÎºfσM¦JÞÀhÕrT™´})GD´’4‹Q“·,“É„ªªª¢ïg6àaÎÇk vÈ @Ø(—£^­Á€Ï»ò6&ÅÎ-äc¹OVWÎwm‹Çc`ÙÜ<XLZ|ó‹Ç‹oœÈÙµågüÕÀ1윬Æ7š·C]Ç.ÛÖR‰*…L”Åt„A÷¸ &¥Uj%” ™JZ’WØan%-• \‘ú}|uÁÈp]¼–YÆyâ– pj.Ha·¦–;ð0îZŠãìe­âÝ=a{œÚÜàwÞæãïLû.$@O*ðÀåRò’ ¥„ǽ†ƒ~ªÜÑ0vŸû921ûYH™) ü@DD” IÄbhÕrhTrhÕò’Úö=×y•Ûq{]F°CªžüƒNÜy{úÝ—yeæOêÀÛgñÍ/~|MõÕS^îÙÏÓeÕÏDZðÏ៿ÞÃëÒ—)V @´»p¼»_0nËŽí]^xû žÚ¿3ï뺫£ÇÎÙf¸Í#-çòÀFc¸jw¡¥™{·$ôÐÛ7†p8:{Îó=ï9õùkvøÊ3qñ÷ þò¦ù­o=õ=L\½Á¹Mu%îxæó Âî«r ;Tmj€¦ÂPRçIØD8âüìNžÃæK­¡²¼lÙd Iù‚a؇'àŸùíœ"F'?Dù•J!çwoÆïÜsKÚù“LÄ0ÙÁs÷W€åßéá©ý;páª#«ó눽Nß4jn+Èq 0KÛg$n&‰H ùL‚VJœøNh E£ˆÅc`cq„fáÃÑļ‹)PL¢mÊ%ôK´Çét~@z*"€D¥¯0ðd£˜Œ„aÑäHe —]ÜuÀˆ@ÇÓšõñ] øö/lð¸eö°OM`›¥¶hmÅ5ð¾R[œ ƒ:“ èåVÆ¡N€çç¨o}®±‚¬«µµ*Uña©sçÎái}ÖÐÐ"".j¯´àÌð`ÎÁ¢Ã८“ðG¸³ÖŠ2zíÝ“[Lf¼wí2§2®ÎÏ$bÔ© 3˜äñbŽˆ(_²Vóýû2uÐr (šM¸<ˆø­C­¥ ‹8öéôw:`B’ØÁbÔà¡»Û ­FÈ! 1¨•2X«ÐBe °`­.çý‚™(s-C$ üÁ %,afä÷ÀáòÂ1îÁȸGˆˆˆ¸>ÿ¨2hÔò’¾çðɬ)£9ÃIµµšqèßÏ zE˜‚y AÁ0Ãy §TòÄ6”‚o“>÷¯r{o߸äÿìÃ.9Ý'(03õL «wÛò õ+h [š«pÊ6ȹì¡wz—&'ðxC¼@•J†ŽÍ\îƒÇ*H{Ó´-Íkvxõ=¼ÊÖïÚ6ûùƒï¼´ªa‡î—ßÊì`^_ CUEÉ+c׉ã@¶ÏÚ£–H«Vг0ÄŽÍ iïs¾`®:fáCd¯JCöv6g:@,–pgÈ•¢ ‘â<šv©4žýò}ø³8vÿ £ÜðLáó›·A'/^"ÎÙ„›+Ä8‰E¢YHÀï´4}µŠÊl¼™qaH§À‚ÄÏFXÎcÓL7>¡_ª/ÞŠ()<•´œNç›f³yd|4àF`šʪ/ç <ÀËG¯ãéOð³Ó)eØ·­?;Æ=N¯kí•DX²‘ =£\pr·4ëŠ3iPoâ´ï çÅ}(Å™‘AôŒ: ¶]*• ÍÍÍEßw·Û^eÍf3t:éÔˆ8ë±öíx¥çl^ÜT~q©{×·¬ÉvÝdªä\&9A–šµ¦R‡…/!'+‘ ÔÚhÆñîþÜ_êëqË<Ì)°±TÀµL®¸Ô•Ø!©õeZø¢ |+Íf‹„s÷ÜÇæ¾OVÐݳz2+;ãE÷Æ?çöÐtÔ ÜR98èhõº2è’ð‡ˆÃ1%°Cæex/“áö•G›‰àÓz¹–óù4Ud³ÍºŽ×=艾·1ðÊB÷Çþ¥ÛgɾˆÃß¹ªcáŠÂÄx16CÐ*Õ5E.^ÚÝ!°Ãßê€É9Ø¡ÇÇ?a\Fá‰o+úX`ÊÀÙÞ!t]ºQrƒJ‘€’0CªÃR™ ‰„%‰XœR¼ì†à Baj… “vžãŽcÜûÈìÃ.8Æ=E=wÂÛZjÑf5“ƒKDı/PЉWÉjåüþ€’Î@Ê3bÙ8B‘ÅÏDÁ0–#¿‹9¯¤VÈ  )¨445oûKY'/ p.óä—nGm5ÿ¹\@ý#8rºoÅlý«Aöaîøõz~×­T,x‡‡Ÿ¿Þƒ¡î»¾L‘ ˜òpð˜­$–Ò‘S—ó<@«uÎõ #ÂpË|ºkcã>T˜Ôiÿ}`›ùó¥R1ÚZÍpÜôâÆ°l4–·ý7”Ø`5B*c­Éã á/ž=Ìv©•¨ßukâ¾óÜ+¸òÎqÎu”ì0|êBVu$a‡Š Êkוæ½ëôGœËÑ%³2——Á¨SA’ãHõåæUz®:9CØG\Y±¯víØ\›r6eòÐÔL óÜeZ«xjÿNüí¿üWVõ¸CAüèÌql¯ªÅ}Ö‚>ö±x|t0NÿLã‚Õ_ ç&F…¾‰o’^‹höù4Ñ*Ð žêÆ…ƒ²Q($är[q’A¡„J&ƒ?Âm$˜ ðßz¤7ðÐb¬D„æxè›Ã{Ÿ `€L´{S¾ó·2ƒîI ÎZÔíN‚§Gç(ੵµUü¬sçÎã7°J‰»oÉ¥R<Ö¾Gì—qaÔ‘óëúŠk -<‚ÿK]JJ†Z­7´5šÝ*Vÿˆ‹s™†z¯u•—€»ÃÏ_ïæ7O‘ÆÝáÈ龜&k(Ú}{:›Ý™÷ñ %£¹®]»Éã¸õàé?JïÊâ÷G–"2‘eå%ú&19™Û$©õ†¬¶¯”uâÃ|óÛ‡qñÿ€Æ¦OÞ¸òö1Ø^;̹¼TA£ãñO vúðBÎ`Cu ÕK2z²Ùa?ýÆ®;@«XßÙ†õm U…Ϙ>d³s.cÐ(±–%£$¨_g(ÊóÑrΆ˜Srر¹>§o±Xâ'×JÖ+Î3§··³öaþヲ®ëìÈ \qá±öíEu{ ÊL‘‹iÐ7óM§Óé&G‹hv\Iš€hè <À`À‡e$Ûy&ªÑè9»<ô º10îçå&$\¾pwgèaYôºFQ®Tå­=F}Ó8b¿ŒAŽAª‹)²a¨ä¹»5´×s?ïþâZîÄÑA{ÁA0™L‚€FFF0>>Ϋluu5är9ˆˆøJ.•â¡æ64+p¨Ï–ÓkñÒ¸sM@Âå!ÀƒD,Fý:ìó/¤‰ˆŠ)kuyN³·µµµ•4xX`ƒÀ龓ŠÅhÓÐ=éB4ã¶?|ŽËäÿ…D›Õ ƒf^{ÿ<.oÎêõ³ ~0t ïMVà0[Å_ºýÓƒû—'ݺÅ0«¨.SC.•𠄨´ @<ýÿ–-³ÂÿrDptž(Ÿ?ÆÛ¥­ÅQÏ NMä÷-0J¯Q¢µÑÌëtÔs_}å$^X·h ów)áÔŸq¨s…sR"A_N!àgðEÓsW3ËŠD€R-…ªLʽ¯ÏTIØ!ÆîóÙÃ{:›‹=å àxw?Î^Ê)d– •kU¨4”¡£É‹I‹JCqi š§…‹ÍBj… zr6cqªD1²0‡" ºz‡ÐÕ;T²ðCd˜šbÒ@(Ì`dÜ3Ó¾Þ¼ö!ï¯4§#£`1iRž×Œ³çˆÅ˜øžÂ’V-‡F%‡ZA˜w.X^²s¤ïÜV…"7×a[«¯üäQì{ôg¼ë8ðö|í÷v¯ê¾Ïqj¨ã<ôÂ6µ]tâä©AîÏ` ÜìÃ.¼öþù’sB[NûodL²y½…ððë£v|î3íKB7†ÜY4-EKs<ÞÆÆ}óeUŸD*†e³†¸:d©[>w?Žþ|÷Ÿ8—•*hìxæ1hª…ý~ÎÙÓ‡žŸ½•UIØA·Îc­”<³€ê°?ˆ÷þáUØÏØæß?ÎØðá«zìû_„©¡ª`má›Äøw¨29æ_‹R+d¨·rîê e CøR~ = G&Jº¶o°`óK^ç²ò™‹bYþÛë©ý;1îöã7<’ü,ºÿ„Cø§ß~ˆÛ«ëpw‘“Ý-¯i"1Á'º|–)¢ycKÒD¥.§Óé6›Í/x\¨Ûèú±^­T$&lµ+8ð÷¿ìÃó_àÜõÄ®Þ.[ÌU0(r?xlÐŽcƒöœÔ%ã9@*É]$N)ƒVIÁÈüE[8…;,8í;ê›Æ¡>F‹\´µµýdçÏŸçUV.—w¢œ©¹¼|ÛÝ9u{0©Ôk¶=[Lf¼wí2§2–ȧ )ÔTê0ps’œ¨DEW›u]Î¥T*šššŠ¾Oׯ_ç 655åØX¥°ÃRÛDZ.5E¡£Üˆ.×Ïuf¸eñ"á@ÁÎI‹I‹'÷ïÄ¡c6tõå´îÿ>÷Ñ[x|ôÈt|ç%ÎeKvð¢ûåÜÁºuFN® ÿõÓƒ‹`‡ÙmŸÂ¿ýåÿÁcÏÿ)4†‚´Ç¹·Žq.“ßk±eÐ(QSYš p—ƒ!’0÷ÎØ>â‚/‘*ËË`6”ÁZ]kµqv>¢Êg¼x>ëöøpÕp᪃׳ÅRаQ´Ã6v{¬Íh.¯@,‡Žål<_$ŒèW@©D ™$óKÅPb ˆ–Öo'F…¾‰GNç9RDó®sÒD«D/@ÀÀC4ÃX(‹BEŽÔ RËhèåJL…¸Mê<;’ð°kSvµTàh/·É†eqÞ9‚­>gmàñúÅîÜÞ1›iGžs»Žú§ <ôŒ:pÄ~¹(®IÕ××£¢¢¢è×`__ü~?¯²6lÈÉ6Häb¨×ËÀøXD}q°>’E~­*éöpwoõÙ²v»Y«î@Âá«F'§1:9vBH«–Ã\^çÄ49Q‰ŠªÖÆÜe=»í¶â¿Xg/^äUV¥Råž\ŰƒˆÃ6¬P¿š¢°Q§Çe÷T^ÆáqÄ ùN g"MáÑ=[`­6âàQ[Î3¿<öÞ›êÇWnÞŠ;×›Cdqûe¸í‹0¸<áD€Q!‡Y­„Q)_rùå? vHnËRÿ¿£C6ËÌû.ÎoÒ8M|Ñ¡ªÄÁÉ«œÎ›b}Z«ØÖRÃúbß;À7?9=ð鋲é»rÐ?‰Ä"(”(”’ÂÎ=¸ÅÀs:€¡Û7†ÝçO4» ‹Qƒ‡î.\‚®Þ!9u¹¨ç±\F¡©Ö„Æ*¶n¬A›ÕLࢼ) @u‰ùñ¤„V-G›ÕŒ‡înÔ7[¿6ûÍ‚NLMq¼»Ç»û‹?؇]°LÀ1îcܳª²U/¥T8b©ã­/S@¯Q QÎý.S ‚÷u(CeyéëW™’Ah\ÔX¯å,0>÷™vœøp€wñ‹ýNtõÍËà¿ZdãI+•24Ôsj­¨vÂ7„w_æ\.éî0å àÀÛgrê@)$rü³­¥†ðpêì¾ä@¥JŸj:.©¢ii¡aÑh þ@ol46€ åRд*¥ Z|ͺ9¼{¸?½›—‹Ê’ã³ÍÞ_|þ›ˆø¸C9­ŸÙ»,ìÀF°‘ÄûyJAC$)ü±c‚!œýñëˆùÏ+¤Â eå™?G Ùì¸ô_g—]&áä«GpÿŸ|6ïíöqñ×g9—+%×¼\ª”a‡Lžã“@ÄrÓóÞ/L¨—„$øÊZ]>ïfó̶¨²Yx¾ØŠÅK§îž«ŽÈázAÆ“Á^³‡Q©Â'7´¢­ÒŒép8+øÇ0ìq#ÌfFƒP1h‰´D ¥Œ‚B*p3àƒ¯NòYê9RD E€¢U!§ÓÙm6›{´ uí~/2°ÊÑåàöÀ<èòã`×0öm«æ½Þ§hâ˜$\vÕ­‡V.Ïzßû&Æp¨ÏVÔÀû\k×&î ɨoÍå… þ?ÔgËYöxÞ“ …­[·ýXùý~\¹r…WYN£17”l(ßµ$j1¤jäb,Àb`}1°Á8ˆÖ–trkßw(ˆcƒv^×ì­–˜”k×áAIÉP«ÕãGh¤çªcÉ,·•†2D¶`™§ˆˆÒIAShm4gíò`2™J<ܲeK·Œ' ËºDëÊTiŒã,³ôwf…ÑX צ=9?“ÄbŃA·µÔÀbÔàµ÷Ïç<À`”ñ㯎¡}¼_©ÙŠõ䀒ͨÍÓ}vCpCŠÅ0*å0*åÐÉeŠÅ¥;ˆ ; ÓíÊpû–ûò['åþ<-„1ÈÞÎfØìNÞÐW¯ý':þú÷ÐñÒÌxôKËž\ûÉ\(›:/ÐÀkj pÚðÕ«¿ÊvЗ)ðäþyž †™D@õùþœƒc™îç†ZÚס¥¡mV3ÉìMTÔq|2Ë<È*è–ÁbÒ⮎Fà .ÎÀöቂ^3ù†‚aöá ô¸`v­Ú Í\‹©éà’A£ršB•I 9MÁZU‚ zÒõf1iè@4«¶M‰€Ä±1_N“¿ÿì}øè’/ñËzð¨ Öªrè5ÊUÕÞÇÏswr¿};?ð£Ò$ìùïw÷ñÊ6¿mS-ìÃ.xûlQÆÑ«Q|]½Û‹Ï}&}HF.]J*Ï:?¬uy¼!Ø.:qcØÛE'~sj€wß»’îüúã8üõçxÁí_øjîØœö_A×B^ßìwb‰eëL4mÏ®¿Žàdvó¸IØA*“ÂPÅí=ǯÎh9ûéäx8÷Ö1„!ÎåÖâ<á`¨ÃZW¥¡l^ò¼åàˆÕ¨B¼®‰Å¾SxöaWr¸æÀÉ Ek'WÀ—{ÎÀ¨Taÿ¦v4—W‚³¼à‡q¿/cØ6†À‡&fòKèŠ5;”„»Ã Óé<@FîD‹ž H­"½àŸ…ºqA6ŠÉH™T^I5ºÜ3$¾yv$+àaß¶jÔUtq 2cXgF±ÇÚœÕ~÷Œ:ðVŸ-/mjPï¼ë¨çþ7èžê¬yß6!ÀÐÔÔŠ*~V2›Í†á7Yœ+w‡T±¾XFâaE¢A¢ƒ®#Žõ°ˆ :&Ê•trjnÃ^ëFôMŒ¡Ï5†++OÈ×jõxrÛŽ5ß~›L•9ÀbÒ f “MDÅS›u]ÖÀCgggÑ÷#ðÐd2¡ªª*'*P™\ÕÅ×ñ!Ø!ÃõU«ÕðE8ƒ%—SK¸g‘™ðø‹~®ZLZ<¹'³ñÎl¿ì½Ç?†/_~÷6≺V˜Å€,¶t›¯ÄÅàôàô'Ž…Q%‡Ž¦aTÊ!§$iÊ– ì€ ëËh\–‰gW†\‰©ÉUi:qé5Jì½½‡Žñ{–÷°aìîþW¼ùíýØý-Õô Z©¯âÓ¿ñèSó±l6÷‡7Ô@Wb~ãÙë'ðíYC¹ŒÂÞ–wØáxw?Žœê+h€–\FÁZ]ŽÖF3v¶7ÀZe„Œ"6îDÂTÒ¢ÒP6ƒÇB•I‹ÎÖ:°±lv'.ößÌ 2ã£\Áöa.ö; àc%Û2 D¼¿ ´˜43ÁþÚYwˆ¤[ÄZD,†Å¤aìïZÕè$÷U77”³àa­FŽW~ò(vß÷¯ öP„Á·Ïàk¿·{Õ›ÝÉ˵' ¥pQE…4-ìП¿ÞÍkL« )¼øï'Wýõ,—å÷™$ÌÌO¶×\kâåòpèÝ^<ô@KÁ\Ö’n »14ä|ti^oO]JÌ} ¹14â)ØöÜñµÏ£ÿ×g0qõç² ÷Þ–vˆø‚ðN â_ PÄXža'¤ Òš_Ÿ×þ¥jû§ï…n]"ØßXk†˜ƒK…wlÃ3ãø@œû çÞ:Æ«l.ݸKA š"°QBqa­Ã ãä…Y'‡lžò!WÀ—ºNBIQ¸»ÎŠû7´@.¡`"˜Ž„Ád2£ÙÏ MƒÐÈä ¥k3tzš‰àfÀ'ôÍ<@:¢t"ÀѪ‘Óé<`6›_ ê6ÞLà!©e4j4: yÝœÊýìØu<ÿøè”üí§¾õH~ÿÅÓœËwã®:+ä<CGì—qfäFÞÚ´¶¼x/5ÚëôœË r Èå#¡À*• mmmEߎ±±1 ð*k6›¡Vçò’ ÆÁYDf± ª%Ë3±PQ Ö‚äR)Ú+-h¯´ bÐ3‰a¯“Á†åÊÑ¡X°ƒ€lu8·ÝÕÑ›ý&o pÆ==ÿŠþæ'ñÄ_[}l…¾ŠOÿÆ·¯ÍP…€þA8$pGÃxØöï8êθ’Ë(<µGβ¶/9ÿp̆ãÝý9-F 6ÔšÐÙZ‡í›j¡U“ì§D¥'‰X ƒF ƒF‰šÊ„#‚¹¼ Û7Õ fJ~(¦CQB¡3{oNÈ/t‡°µPÐRXLÚ¼Cp…P2Û,qòYýâÀÔ¶i>h<4䆶5wеÕ:üè¹}øÂ—ÿWy‡Ë‹CÇlxèî¶Uq|ŽwÛy•ããðP[-ì ËÃnœ<5È«?ç ˜—š,¦üfµÅâ Ö§…Ũá cfâòK˜jµ) 5œ85ˆCîÙ¿ 2d¢¦OÞ…ÈtWÞ9ιlõí›Ñú™=ó¾c# ¼C£iA‡Eç˜Ë MMþc }x×}&«:¶ú^ÔoÙЭ3‚’s‹:ùêAw¾îÛZjVÅúÿgïÝ£Û¸Îsï—ÁýJ¼J¤(QiQ¢-Y7§’c×¶RÙ®ã|¶å¶Éi”ö«c7É:+éªíôK{ÚsÛ9ic÷´‰÷4vb§’œØ–”8ºX²hÉ–H‘’H $!’’¸î¿?@ð ’˜Á…pžµ¸H33{ïÙØ³çý½•{Vv®ÍŠIê¼aÁ¹®tÞ°Ð^£Î·ü‘>¸yܼŽF­;ªêÐf0‚Ëá , ?Ä&²ó\!Ž•ÛðóÝx`µ€XàU±éuÏ0õà족(Ä<öÒ[JF%uàÀ3÷ÑwZ8°³/¾ÝMÙå!<éò°ƒ†+A>ï7V/ßBgµV ¥„€ÛOíÁžÍç…N&ÏÉ11v€ÖÖVFGw7½c>ŸŸw‡´n@Â@Ø1ã&‡2.5À&"@Øb`UÄ’¶UÖ¬xËÁtT¥¢XksxasxgY‘Ε€à¡Ú †ix|ÅÖmÒá"8ù;‰!>™ûR\.ÂYvgþ/ øàr9Nþfµ°ÄB"Áp”ò¾AÌ­ë4a‡¹£1 {I {I ¨D øA%€Ïá¦UNF§ ;ÌÜ®`‡,|µ™†Ç€öF\ _ÜÓŠü穌Zÿäúo0øõ»ñÂ7š ¡ôàŠcRVÆÿ\o7Wƒðï ÂÁI—O_æ`æðW¾`ÓðXÎaƒF»škpWSÖTjWT«•3ß èJäGb0êTض±n_QðC…N…x|ݦÛ;бʽæºC¤[EBbÊB,$`Ð$Që*4Œ=/ÁƒQ§‚LÌ&Þb•žÜž 죾¬fc¿ÿÞµøó?mÇk?é µÿ™Ëý0h•hk4tÝš†ÇhÝ'··Ìœ¿ ÁÝá½c½ì·„–ѧ­Ñˆ£gz(ï·ðd¦*du÷XñÑy3Î~<ˆ³ÒrÀÉ·êÿp; w4âäßý+å}:´xpêÿ‰X^Ë(ÎôŽXæõžaz~™l°æ®æ)ØA$A^Jm}Ácwàêï/¤½}Ý–Ü>ÇðØøø­ã´Ç’•$­Zʨµ_ 1®ð¸\vMhÄáæÿ3LÃc ‡›œë,ø:¼6jõQÞ l2T¢Í`D›Áˆ`4š~ð øÂ™eAòxóWfÂÌp<†A¯›é‡yÈjµ²# «Tb£®Y›^ƒ0û}X+g­Í–¼aSkpÁ2”–]ÕL½ò^_FÀ@ßå¡cÄŒ-åU”\Ž›®3&ð>—Ú¹® G/ŽP»VÜŽœL‚´Z-ÊËË—ý80::JkßŠŠ ð™bó"î8àž*æÉ¸à‰8À0™@„Œ³DI.B'•ƒàñØÊHg̑Ƞ‘H1æ§õuÞ°`ïÁ„2±åZ%FFÝ+¦>ݾž@J!]ÅãqB³÷'p¹ >>‚Ï.ZÎЙËý´`hjjA,]Ò ‚ȱ»Ãba‡¥}Æ6Mj5.;Æà‹DÁì‰Èj‘¤Rõ›FÆÑÆà!9>|xÎ\îÇñó½9 6$c²]ÁŽ~<=ÔŒÏ×TÁôû#'½~å Gà G0<™$UÄçC%@& P‰ÔÊga‡ÔÛ$ÿ¯ –ĬâvPžZ!Ác{Zqè7™e|ñÖG¸ü¢ ¯?úP}1Lm,æ€ÚöÙØ6×°Ã  'qݽ0ø^<›µ6{úÍ9‡àⵡœ”›„v´Ö¢®\ÁÞ±Z<” ÷‡X<Î(øUqi) Ôrñˆ<‚¨+/¯óè¡/•C£’²™fYQVÿ %j øüìõo=·DÏUz™Cœê†A£ÈË|-WzëÄ%Zûµo®¤´=Ïe¼»¼wì:{±-5Ç_†þž‰ËÃïNšð¹]©º=A„BQƃ8¹Rw?ÿe'Þ;vqÎ K©é±{ÑðÀ¼{ð{”÷UTèp×sOLýﳎ#0îBœb¼‡P‘[7æH ˆË‡ÞE4@>©nm@Ëýw¸<.ÔÔ)>{÷4¥íïxpGNë僾I{}‚É@p.îIç&‰óÍéKáH ‘èì~‹Å§’§ÍU8C8²ü 2qêào/5H!³½€Ï_‘ëSœ<䬳;½è¼a™rr ᢬K$‚3fΘMLÂë´ºYÎd$ µXL xòxòùd‚•›$ Ïí@8Îø ©×Ù»V ‰X•¬Vë ^¯?`SÑ ±Z¦˜ÎÉjAÕ©5¸>Fm1Ô Š/ 7,5&ì0H¯+€ —}v<}ý7èôÙ³ÒN"};›òö0>›scµ\Œ{6׳+VÉ5.% ÉT6äêU%ØÑZ ·/ˆÎ–eX­,9½8½‰¬3ç¿'Ù§v 1óÞžj«ix @´Ø²Îˆ»6Ô°ß +TR±€RÀSw !ã†i eY;.¥B„þþ><ôLJhe3†#xýןàÙ/í*È5¯ã½SãI$‚È’a•‚ñAånOçΛ٠v©{ÊÚåqDذÚ@x€7ß¾¼h½5욺•íöñÞ±^üÓNä™[Ÿ}Õ;ÛðößFØGí^–/¢åÀƒ Ä"„}x†¬ˆE¨Ïà ‘â’ÜÂ?=¿8ϰöþ*½›÷nêEY øµq8DÐóaúîÚjŒMu9«“ÏÞ=á­}··Öa¥©ÛdE,^|1¾Eæ”n_ú¸Bð àOߣÈ$Óæ"3u?TèÐ6— d»Kœï@w¿Ýý XÆWÜu6~x @ã$øÐ¨ÑaC™Z©ƒ. |×Hø‰{> Á vsH¥+Î1¦b§Õj=ɶ«çl°*B½ щ8F$ª$r¶¥–P£¦Œ2ð/¿ß›ðäÞåÁìràÝÞnföÑØDÖËܶRvç IDATµ® ß}‡Ú>f·3«Ç`óyqÜ”½ 2r16wÖþõõõP©–?ëNoo/H’¤w66ܸ L}œ‡„@lò}VŒÁãA#‘B%³•ASë´:ÊÀC×ôA1ƒV‘20¿Øäõó ;,¤¹u- !ò! Áç­ ¸öDýlò­­­Ë~ü™€‡R©MMMù=àbƒ84Ï™Æk³¡‡ `"ñæj±šr3ôô[Eô¡VHrîöÌÑ®Åç UÕÄA>¼µ 2‹6Ö°gKîlªbƒYY±ZDJ™J™F]âÞ4éüpñÚºM·ÑÓoe+‰Õ²k!0˜nÿìljŽ^üÑ® 8øð6¶‚W êÊ5躙y’'‡Ãû¨eZYÖŽ­i½ßznþæÅc´öwzxõ³xöK» ªM,£nÚôC÷S{æ"• ÂÝáìǃìź„ø<î”kP¾Uc(\"„×O N²’‹º<Œ;ü¨©ŠgÕ=†©ú§—NáÕ;O ðb‚J×Tb×ßþ9Jë«ðÎ߆ÏJ=rësOBQ¡ƒgȆ€“:ðÁåñ .UA¦/Íé¹}Ü…áó]´÷—¨äØõgÓ¡H‘òRêk 7;ºò§cKwÝߤ÷]­–‹™˜'—b‚C!ÕÕÌúò¥é&]&fºJ$á—ËX–ÇËx°ŒáJ¿=ƒèî·°hŽ®Úpm4?(!4jõX§ÕaM‰j±®`¡h”­¨%4èsÃa¼CÈËlK±ZôÞ‰­VÅ&«ÕzR¯×›T1õoù},ð†d!tR9l¤—Ò~G/Ž`p”Dµ–¾Ýa&.§Í7±·ní‚Û£QüâêeÆÖ;Ìþ$.€Ò;nGCiv² ííÎÊwgcì¬Á5¸ã¿@ x "ÿŠ©Úš$iYªT*F‰Ç0Jú›˜€ˆÏ‡J$ÁM3$ÄÈÙWpxLÄ&/N7À‚ :dOm#^£¸Íá…ÍágÑšJ<.Õ«JÐwk´(³›LŸ µ %!Æ\$„>äaQÃNg.÷ÓÚ·¼¼eeeË~™€‡y6(­.ìn™œÉr©–Ç¡_>ŸÇÅZ•—ù~D'9ºYí/^Âö–ZÆ^ŸI·‡#§»s\h‹øg˧xÝÖ…Gn­Å#•k 3Æiþ<¹‚3j»¦ïaeB|*‘|2> 1³¿P…˜ ;d4~¶ÚÑ˨›V†Ì¹zÑüÿ}x¹ésØõ5%P¡6&ìð‘øˆ‡Çnàë¦ßÂôd­]l_@(2•{Ö} — á ˜`æÿ\.‚ÉlB‚.7ý“+QHh¹Þ¨åbì½s-Ö×ê§\½,cnˆ…>‚™xåZ¯³b•εW¢ èSás›×À:îEO¿µ¨á‡æÕ†©¿7®™þ[*dÍÙÆæðÂ6>=×ñB0 Os¦‘1ÆÞ3³þëäÈÄBµ áSK"é„ P¥*ACi*•j¨D"ÄÙ<§)ÕÍ|w7€ÃlK±Ztìa«€U‘ê?eêÁbQØC” ÙÀÍ¥TWRJx€C§ðü#™”Óuyødä¶”W-˜ûËžK+’,ÝÙX†S×ì”öéËðpÚl¢ÕfJ)!ðÒSwàÀΚ©>ÖivÑ*«©© ±üôù¥K—hïË$w‡a ‘XâF*‰À@ÈãA%C&‚Ç¡¶@–€¦ï€8üI">‰XåQ,è}I*•jÜ¢è¢Óy½í im+ x¨6¨g<› ÁÁ"Ž"ŽbÌEB,$ —Š —WPÜ›'è1ÁÝ$IôõõÑÚW«Õ¢¼¼<›-@!›e¥[~&°Õ,êi”/#´”•â²Å‰èD2žu"5LAjãò™K&F@Âíáé¶ ÛdÅÑÓWàôrw=Å"8d¿‚·Ç¯ãn“OW¬‡¾ŠÔ‘ôû‡bÿJs{ߤËÅ1c[Á‡ˆàAÄç&~<ˆø|ˆ §ZPA:×¥m&¨]Çé\?)Žc0T¸Ás%øêÃÛðê;g³=t’vìîø9 lÀ ÛîDõŸPÅ‹vøH œANºnáóG8åÊj›ÚÑráÄñxÐìtä"ßÏÁãNýL‚s3͵5)9~¬¯Õc{Kí¼À䤳×܇½‰ìv|ˆÄÝ$ !àqW†ã+V‹‰ÇåNÁ«+4ØÞR ‡Ç_ðCójdêÊ5Еʡ+‘C–E˜!Û2 ÁƒœE˜FÆàó‡á „ha¬Öï_ÄÞ;ÒJXÁªxTWQJÙá¡ЙxˆEã¸ÖkGK³!«Ùôƒ}Øuïk´‚“s)¦CP¯¾s–ö½ï=;ë(†ª¬Â)¹ëð°´äѲ~~CU.^¢åòÐ}ÕŠ¦uú”ï[n{Šxp{‚øÎ ÇðæÛ{«îhÄÖgŸ@i}"‡h߯O£ï7g(—Sÿ‡Ûa¼«!Óÿò :$uùлˆè»plÞÿ9¨VMÏ»¥jù,ø!] u›àM-8—î¿y Ã=&Zû4ŠçîÀŠ9J‚ ¹F|ÞÔzYˆÈÕz‡“€b‹„‘®ô[Ð=éâ`wz ®Þ…|>ª”%hД¡J©†J$Fï¸ÇM½pËv\þHd–h$RäJ”IeX%S@)¯ø˜“ñP·ý>¦æëV«ÕV¬ <°*VFÂ↱õf¿—ÒPZƒ –!DbÔ2t¾ò~oÆÀ]— `ÿPÃüÏÿdÄ 3Å Ó¹’ ù C…LìÛ\Nx·gü¹®`#æŒÊxpS9^ÿZ;T’Äâ±ËƳ?ûŒ^ûI¥¨¯¯_öö°Ûí¡µoEED"#ú•/š‚f*‹ÁæóÁB!”"Ä|z‹ÿQ`":;ЋC1Å›À~YÅ‚¹Õ:­.§ÀÈÄBèKå°Ž{‹®þB‘ÂûNȹHÈ%B¨äâ‚w}0 ÑŽY¿~=¤R鲟Cww7"zðL{{{þ”…¨såËJw.“vD'âø|I-þÙò)¥fqz¸xm¨ *5ÕéQWQŠ3—ûq¢#·YÉXÇœý8æìÇ6s)k@KE P\¼oåv˜÷÷œmƒÑ(‚I>Å6|'áÁåB&âOþ&Àç&^Ï™ÛCªzÊä3Ò¨‹Á`ñ@ö¡8d¿‚ÃïöáëŸnÆ×ïj…ê‰`| Ò/As¼£Zf::#~›ƒn¼0øÙº³Þ¥J)Ú±B"»ËàÑh Ñh HÂ&Ý!•-õå¸Ü·øýw[£{Û VH(G,O<ÜMó/0=v¹²Þ±bµ\]î@"Ö—«EŬŠRV«Õ¥×ë8ÀÔct†CðF#ó ¶Á–PZƒëcÔn@Ýþ˜ÊÆOWt]ºllÔP¥šÎ’ã pÊlÊèx¶¯Õ"ÎöRÚ¯¥J½ìí¸su§†P4ŠÞñÌ\N›MMªÿû¾uøÞ›g½öÊ{}pûé(nÙ²eYÛÁn·ƒ$ItuuÑ›8ðù¨©©aÌøNÛzB!xB¡)×e`³yN\$î^Yk¼Œ$äóQ"–° CÎ&ôøàæuJûœë°›Ò>º9‘QÕ_¼€=0ãñ8ܾܾ¤b!ä!¸ÜDôâÜlÄL×[4Ý‚@CÃòD"8N cppVõõõŒ6– vH·ŒlÃii”/‰BÆ—¢Eš€îV)päT7Ö×ê âú ìmoÀæF#ÞÓŒ¯¯¿ÕOq€šµñ Ùm Je¾-º G÷-¼ú裬Ààõ‡`N¡•($”Aèt”\^ðþÉPA/qÿ@¥¯Úwy°z°¦NƒbPwýñ!x¼¡‚;öú?ÜŽúvÌ ìõã݃ߣ\ž¢B‡õ¼gÖka2õó B$„D£†¨D‘÷sï3càÃOhïoh¬™ÈK•àÒ6BdWŸ>ð°þžÍJ³ÿÌÔcwàè?ü„öþëkõŒu™cÅ*%!–rR•Ši¯…q8€ÓïŧW-èèÀ•~ üÁÂø5)Ú F4jõX§ÕAB̆E|f—ñ‰‰¯ñ±£ª[Ê«>ÌT0ÙíL™¤¸J©†O@/“'Ü „¢¢‚!Âñúܦæ«Õ:ÈŽP¬– <°*f½@Âå¡IQ¶ÔjÔ”QàÅ·»32uyxrðpÜԛѤî©5øéÁvÜóÝ ²[ªÔ¨ÒH)×e§ÕBxpè²Ñðõ/Ö†?ÿƒÕ³^%ñÝwèeÔjµ(+ËÏB´ËåI’p:°Ûíðûý ÉÌÖÖÔԀϠI½D@`Ÿ7å !$øK> Ë¥Î\î‡ÓKϵµµuÖƒÉ\) 6¸\.8N$ —ËEÛÑ!)‚ ÐÔÄ€ÌI†}vZ°CA‡tÊ_긄1È&h‘&æ†u"5LAjî;ÁpGOw36Ãe*©|xºMV=}…öµLE¶‰ú?²¸û¦è뱺B‚Ë ;,å®ÀYú3gäümø<d"">"‚7éÁ…*™YqQa"m–8Ö *Ø'ÛÐ^±\À+– Ø÷Ù<]±_¸§ø ¨Ó´¡Ë§ Ä –V>^·]ÁËÃÑIÚsV÷V¯Â¶fæÀúmFlX½ ã®éûòR• ©d Œh,ñ$À´š9_÷QȦžt˜«™Ðļù̈"“ëk±ÎáH áhá¹³-¦Äƒw6yN¶U¢@@ððÖ‰ËøÝ…¾¼|fój¶6Wcã(”†’u”"žœñž/‚ixääo«Ã Û¸6G⇫â¿>J)ïã÷‡qþÂîܼ°c`. ‡o=»Wz¬xÿ8}ç¿þ‘qüýO‹§ØÌˆñóâµ!9Õ`˜þzO™VЇîoLo.ÀçbM&km’=ofôñI¥RH$”••A ¤R)õTuúðê¯Î;ç0hS„A£D‰B Mø)\HðѼÚ0å¨BE¿;iZxwøQS/¨~›J…;”®©Dý;Ðð‡; §†hŽ}ë%„}Ô’VñÅB´}õQbÑì>¤”BY¡‡÷ö(â±8#V+!R+ -O¢³H ˆË‡Þ¥½¿D%Ç–ý÷ÌzËãBVJÏe‡Š»ÜñàŽ¬×Iˆ àÈ?ü!Öþ"QPëѬXQÑL'UÁÁç-ºã „Ðu³]ƒèºa)È{P A Q«Ÿ„tÐJ‡pE|>V—j1ìvÂ?ç™g¡€©”„ úÆç¯Wë¤rˆø|èdrˆøÄ!âÐÉ ÃóŠc´“uw`•Þ\”­VÅ*«Õ:¨×ëOØÉÔc´H¬•«ÀçWf4GÀH,uö:_ö2F… Cµ ɉSWí´œfꥭØÿýhMˆ’îf—#åÄ(]%a‡|)‰A”ûÔ}›ËñÃ÷©=8ì·Ã ÐÊø~:Go=Ô8v€?ýqí2Û۳߆ɀʤsƒËå‚ËåÊI¿‰D¨¨¨`Ôx#æ P*–À ¤u=‹aÈíF©X‚RI3d³°Ã’RŠDP‰Ä,è÷… *•jÜrS ¬í¼a¡ü ‘Çå¢zU ún"³•_Jf$N ˜K?¬ øàrf»Dp¹& q¹œ¬fU„"8~žÞƒq­V›u—¢™`C4Ìذšššòl,*Î"_|¹vw(dØa®JCÀˆ2^zxLÛˆ¿:G¹9.^B]…mÆ‚kšêôhªÓãxG/Î\êÏ(8$]‘±Ž9ûqÌÙº5Ñ®ÅݺUUÇyléþÉ0Ø!m ÂN¹½ˆ˜† D2!™˜ŸXŸÈìÀÐ?}™´¡X•+è!©#ã7pdüª®+ñ…·Öàéš&´lQ-?ävè—…@D88<~‡ÇúpxüÜÑ܈voZƒóŒ þ¼à¤P8ŠPxúÁòAjòg¹ˆt5å 1GT  VÙsx)àä¹ Ë…HÈŸõÿJ…)~õû.¼ñþE9ì·R±[›k°­¹Ík +ÎÁ!—’‰…S ÄÖ Û\wÓÈ|þpѺClm®f;Å ¼jËK)»è}xòæ¢ÀèáG?؇]÷¾†¡7í2‚á^ýÕ9´5±·½!'™ð—’ÓãÏš{á3_Û©téõqŸ‹ ëôimË$]a€ÃCÒ­A*•B*•Îrm «K—.åü¸ƒáúGÆSö3ƒF‘€ ´J4Jˆ…ü¬@@V¯B×M e—‡O™ðø£S:•Ä¢q8œþ´]L˜¨Bdz ›a¸£«65B¾J»èöç~ðnvú|ã¹'!)M ÞˆJËââ°:½‹€ƒþwζ/ÝB4{îN×Ý>{÷tÚÛÖmi‚¢,ûküðMŒÒŸ ï½³ñYµ”24*é‚÷í7,“Ã@^œ¬s¡F­.8ht¨RQgxªT%p°ù¼³Ü€ùàC§Íw0P°}ÂF&@ó"±UJu¢ÿˆÄS±m©^[¶¹ŒsŒéÕl¶Z­'ÙˆU:bVÅ®—Á`à̤u2eÁV°Íç…•ôÂæóÂ\t˜+µHLޱj‘2ÁÂz5:ÊÀpyøðoïÉè÷µU`gcN]£,7õ¢¡´,£Àû|Ã@/´UPà“3öÖ­¥vÑ(ziB&÷n\…x|ã¼×O]µÓê°~ýzH¥™×Ï„rP™ò:lldä8T*‘B%ÃPÆ~Dâ1èe °ÊóBH­D‚Çc+c™´N«£ <œëÄþÝÍ”?K@ðPWQо[£EQwÙ Þ/Í Ð[ÊÉc®cDòoÁ Lj¥Ü#Žž¦Ÿ5/g„|ƒ ©$•JQ__¿¼ žMØjY´ašç•ë×”À2Lp ã ðlù¼b¹2F½O9Õ ƒFA;Ãßrjo{¶·ÔâèénZéÊtN»>˜Œ¸[Y»z 2âóÛ,—°C®úúnjČÆ3Þãó™€J*€ˆàC&æC–|ˆ™MØaŽ\QêYåJ–!°‰®ÄBÏ~iÞ:q)g}ÝrãËE¼b¹ˆª‹ øaWi%¾°®¨í‘+Ø¡SÜ&pòà qÒmÆá±8<Þ—SÈaêÞP)ÅçïZ ¹¤p‰£Ñ¼Ñ¼dpjÎ$ ŠYPœÕÒZhNNB‘Íèob!ÞäÜ=Ù|~Vœ:–S¦á1üÏÿø}Îø<.v·­Á¶æê”ø¬ò£¹îsÕy#ìÕ5ù;ù¿id,§L.Ô¼šu Y©Ú¸Æ@y,ë¸8û¨oÉd’ ãr— eY ´W*DxãßËJàðÅkCè6Y±½µÛ[jó|éôøq¼£7ksúïo\0+þL*ìnO0/Ÿ£Õ&»“.ësgS9K<–®,cXÆ<è韔Ԗ—¢\«Äö–ZÊ@P&.Gß»†/Øœò=»ÝW°ÀƒÛÄ“_~‹1°CéšJäRÈWi _¥…|•2ƒš5U º8¤Òà©OÑýÖ1êß9O=E…® ÚÎÚÙ k'}·–û¶AµjþÜŠ®»ÃèÀ<£é?—[Ïæ¬×É?|¦Oºiï_[^Ší-µìä‹UщÇå¢D!†F%›·Öasxq¶s]7-è¼a)¸ûD¨Tª±N«›rrÈ–T"1äBl>ÜÁùs¾$ø°£ª6 º¬#‹B…¬©óZâü’nfÁ&I8bîëÙPŸÛp<Æô*|‰X¥+6Z‡UQËjµÖëõfUŒýÒóû x°ù¼09ÇpËãJp˜+g0a˜$!€àñ —ÊaTª “Êg:™Rd˜ÚäñÔ5;.›h©RgtÎÏ?Ò„{þîCê‹ Á~Ñs™ö¤mc•*ï°C.µs]ª4R˜ÇHJûuÚ,ØQµzjâ—ŽzÇí´ìÑ j1þó¯îJùÞŸÐtw  ioŸH’„Ýn‡ßïI’ËÚv&£l79¿ åpižP€‡…ò$t`ŽÚ •øàæuJûtÝ´ÀÑÊD)0êT²¹ ¾î¸\¸\.â¬ceE£1D£“sÇ4Ü#¦#¸<Ø^Ú”«««ÓzÀ9óûw&äO°a!mÙ²ey `a‡‰4?›âçQ)¡ÏTFW"XACˆñUýøþõùf0Áß9‡ƒo-HèA,LØ ïmoÈjàH:šéú ‘âîkF|^[Õz9PJÀ¹†8ËÉé6ˆÆW œ!fl£’ &] ¨$ˆ¼Ôåp(\o¾élxtÔrqÁõ÷Çö´Â Uâèéîœ~ÎLøW€ÊJìRV¢E^†uªë¤€8ÔN~¿ÝHoìrrnaâ½!ääà²Ï†Ë¤—}6œtÝB'iÏkÝnX½ ÛŠ0 8Áí Àí  D!Y– ƬV¦f‹Á g>x<î˜Sîo¼wo¼1'e—($xhÇz|éÞMlG*%Aˆäï'ç¼osxa÷‚ „`N”'¡›Ã ›Ãˈó¨-/Å ÿí^¶AW¨¶6×à¿N^¡¼ß+ÿrß{~é~ Eq媵Õ%Y XnZ¯Ç~°O}å— Gp¢£':zÑÖhD[£1'àix ¯ eõ~µ¦JÇÙ¸äv… ;À¹ó欕%•JQ^^>åÎ òþÜ*‰äÅÝ!%]!Î\îÇžöìmo ´CU­¾þ»“&<þÈÆ”}Õí "Êw]; IDATŠB(,¼ð¤¿|îHF®4éJ¦×@nÐN °jS"P&Ai}öÂ]ÆûÌ8ùÝ×(ïWqg3Œw5D»EA\>ô.íý 5X³uþ-UËi»;|úî™´·UhÕXÝÞ”Õ:éùð®þþíýEO?°¬X“º9”2x\îäHhÒÁa]7,Œ¹ç£" A`“¡rrÐA+ÉtÈãp`+¡‰1Júà_àyêFu¸‚|2bFïøhA»>ÐÕÌÉ¥â…|>ôRùäßô²ÄßJ‘*¡ “)–Œ£ûtœñîÚn‡Ù‰UºbV+A/x‰©ˆÃ aK_‘6Ÿ6ˬ/à¬ÞxÆbò¸¦œÔ" êJJaT¨ ±QgÀ¹¡AÊå¾ò^_ÆÐÀÎue´]úhº (%DÆîLÔ¾Íå”]BÑ(N›oRryè£Wï?úÓMPIæ/ƽò~/eP#©ÖÖVÄììB.— áp˜‘•©´fÍš‚è_tÀO(½ ¬r(t`žµô2ðtݰÐÎL™ÌÂ\ ЃXH€ „ØŽ”cÍÌNK€÷Ï]£UAóÜf:5$!‡ÑQæºhµÚœd¤Ë\4`ªeåvÈ€ Y~ò5mh x€¿©ÜŠÿc½OŒz† B‡@­à±=­hk4âxGoÞí mïŒ]Ç;cס»)ÅÝÊIøa•0„Q|éþQà°ÃbÛ¸ü“Ää×8ŸÇJ*„J"€J"€LLP;7N? vŨ¯æ#ƒk.´½¥uå¥xýןÀéÍÏCSî[8å¾5ýÂG BÅ¢Eª~ìRUÎÞI0]â*œœ¹_â[yêµË>\ÑÐì²ó,¹DˆÝ›VìØ—Ž¸\.4*iA;W°*^Åâñ) Âí κÿS+ÄŒƒl/žÿײ>ÏЕȱ·½{ïl€®DÎvŒ"’®D>Õ¦Éõ'Slg›ºl/lãÓÏP’€ÄÔ¶YtHö½?Ú½¡ `#V¹ÑÆ5èJ䔃±º¯Úpô½kxèþ¥cÑ8nÜøÃ5uðùÜŒŽùþ{×âþß¿>’µzH j¹u¬¯]…ºŠRZ÷ P¦áqôŒ¡Ût;ëóv‰D€g¾¶mIˆA*`M¦`a‡l‰ÇãaÇŽŒXëííeìs¼T:ÑÑ ” ¹Dˆ†ª2ôš©=gõûÃ8aŸÛU—z6êCe…ª úÞ{Ç®ãýã½Y/wÕ0ÜшÒú*ÈWi² 3,¥°×“ß} aŸŸÒ~Š Z¼€cÿûÍŒÊxlOkÁ®Å±b5W2±• J™hêží\×:oXòþ\"[Úd0&.ëi„Uª¸‚ŒùÉ“'«Dbì­[‹½ukav9Ði³ÐNf[ì E£³ ˆ¥b“.J‘*Q"Y”\,/ÂxW’×­V«‹mqVéŠX­½Ž„õ cŸ|šH£_8„‹–¡)!_rý¸hñã¢eh ~ £ŸÀó4¡Z›Yÿä`;êþêݼÿýõö”÷ù‹Œä쳟¹¯2ðŸŒÜ–òª©‰Ùb F£´@“Zìk«˜_þ0^|›^N‘H’$ñᇠ‡&C ©ºº"‘¨ Žy&øà ‡à ZঊËå€U®nl‰´¨rVË·ðñ©…Z¦¦³]ƒ´ ôBÂpxü]wR±€ò¬‹–1­}U*úúúàt:ášDGííËìø•ò«’&ì@¥¬å€Òžd¡|"Hb€?ªø"¼R·Ò÷ZÍT ÐÔUhp°BÓðØ²€Àðƒºf~Åæ·'Õ~_@°Cªr¢ñ ŒyƒóÎB*€LD,}ž¡éèÌr?7h•xöK»ðÖ‰Kèé·.Ë1$ëüÈø À‹· ¯7¬^…¶F#„DñÞo|hÕ²¢>GVÅ)‡Ç‡Ç¥L£N5•)q9u¼£?~çlÖÍ yµ{爛9™Uñi±ŒòOR,k&<‘”L,ÈIÖzVÅ£­ÍÕ´\~þËNlX§CMuzAQ‡=ÃYq{xüÑèî±âµŸtdµ.œÞÀ,7µ\ µB2u •($³Üâ¡(,cî©ëÏéñçLþûç÷.Yç%%’¬À%Ë)·'˜•r¶lÙÂØ$Iôôô\;œèèÅæF#%·¸¶F#eàÞ|ûò‚ÀƒÝ^XÀƒÛÄ_>—(K “ zç&TïlCõÎåu;÷Ò¿Am€/â®çž(˜¶ï3cø|ý1gÿ= DóARHòõtt³£!úcb6‡¡nSưÃö–Z4ÕéÙ «‚W‰BJ ˨¿»Ð‡®›œë,Ès©Tª±N«C›¡’v¢Ã\H5l¿øUª’)8£wÜŽÞ1;ÌnçŠt~Ȇ¦àˆ„Z­†@Àxxúe¶õXQš›²UÀªØeµZ]z½þuÏ0õ±(áJÌËÀsm̆N›eÑIH>”„èêÅ·»3vy¨ÖJñÔŽüìô@ÎÏ÷©5ع®¬(¯Éj­”¶[Æ»½ÝxrãÒ7øf·ƒÖ±½ðȆûÛORƒ¹š‡ÃÑh,Øãçq¸P ÅP ňÄcðGÂÇbÎ ÄÕi@4¬¨‰àñ`+ !le0Xë´:ÊÀù®»3ú\£.ñP£¡©ˆíÛùV¢ïÑÓèè(£Ý–REE¤Òe„£™ ;,øY„R¾6A¯ÞR½VînLg~Z׌—G. “¤çV–„ÛÓZð¡˜>sà‡A)Zd:Ü­¬@‹FY)¨ .ÞÎé:@¤ó~N¶™ ^NŠsš@ xPIÐ(DPI³ƒt8¼Óÿ_&©[*Ï P*T‰…ž~` ºMV¼uâ‚áX¥§•àêJ™% é³*h¹}A„#㨫(]6èÁáÇÊpœ 5¯6àÉûÛ°qmdV9™³bEUûw7Óüþ0þþý/ÿãƒi»$ÝìvŒF” úÉ’¾÷½p{‚xóíΜÕÓ€Ó`L¶Þ¿:¸mIØ¡¦º†UŠ‚ï—Ý=™ÃÝ"‘•••Œ8ŸK—.l[œ¹Ü‡v4Qºç2h”ÐØGIt_µ¢iÝü5©P( ’ ŒcÉkÿÞ7³¤G2½m_Ùúv0âœú~}}¿9Cy¿­Ï= B\‰ñ" .¢ŸÀrÝîÍÐÖ”§|O¢¦?.ßìH?±bÅú:(ʲ“}t`Gÿá'•aÐ((+FÀÄŒeU4\¯YåC<.R1^ó(Žwô¢ë†…²3¤‘HѨÕOBFÆÇaÌÿ’ ¥eh(MÄÈÙ|Þ)ÂFzÙNL·ïóx…;±Z­ƒlk±¢"x`µRô2 <€‰t£DÀœ÷p,†“ƒ7‹fò-—‡—´âÈÅaÚÁïéH)!ðÒÖ¢¾ Ÿ¹¿žð`v;ñɈ[Ê·ö4»¨«T)!“Ëf'-GŠb‘X,¿H²ó\”BnÈùͶT ­DÆVDh“Áˆ7:/RÚ‡ „aËøá»Q§B8—±°PÄår —Šà%ƒlGʃ.^‚׿r5jjjvD ‚*3[e¥ ;p2(_0Ûå^ox­ŸÑ Gpè7Ÿà¡MØÞR[ð×ÀLðáÌåþeˆ$à‡cÎ~söƒÀFiîVÑ¢(ÃêR% Úpêv_ à¤Ñ¨lƒt¶™ vS¸¾ƒá¬‘¬îÀ4rô*14É`¨›Ó h2S2]MuzÔUüŽžîžÊBË*µÍ« hk4õyòù<”©e ¶ÑY…¡lãÞe”l/žÿײäÊ‚¬X±bªt%rìio wÙGI|çÅcøÞó÷R Dv{‚p÷XQV&Ce… B!½ç?úÁ>È)ôÀýÕÁm f¿©T€5uš‚ χ¶mÛÆˆã°Ûí)ØzuSÞ§­Ñˆ£g¨'tûÝISJà,VÖÔ1ìs{‚xõßÎgTƦ/ïǦ¯ìgÌ9÷™qî¥ÿ ¼ßúG÷@Q¡+˜¾>ð»Op¸ií«ÒkuV+èÇ™˜:Ò‡³åî0:0‚_üÍ¿Pr–˜+‘€ÀWÞV“šb±ÄO*qðø—ÇVtËêÄÍá1ôšíŒ_)—ŒX§Õ¡Q£›rB(4%Á$ W0wpé1I'“C'“cGU‚Ñ(ÌnÌ.Ì.' @Pв&ÓK_¬»+ÊbV+BV«uP¯×°©Çè ‡ˆE!æ-ÿeéøqÒ|d8\Tý .*‰ÏÜ×€ï¾Ó³ã|澨$Ë»˜éò‡änò³¯­U)Ìc$å}›zQ¥,N&_p›ú$÷ÀÎÔÁ„Ϻ„•¬ ~Y1DB>¹">;½,i%2h$RŒù©ÅÇ;zq0 Ù« %0 #*ÌlÆr‰ò P$Š®›–•}ÓΨqu™`‡tËädq?N?³Æ\UNÅž·Hux©öðlÿo3j­£§»aÃc{Z‹"h¶®Bƒº œ?Žwô2"0¼“´'Ü8n:BŠ©.ñ£ÒB¯Ê(`/ì°Ôg¥º©É¿Ót«ó1æ BDðP­•C߯šÚô¤û­þPL <¶§mÆeu4a²ªÊ°µ¹B¢xï7¸\.T2QQ=¬X%åðò<˜†Çð™ОX±ZÙ^œí˜Z¨+×`ï ЕÈÙÊI¡§îo£íf3`vÒ‚Àn÷Án÷e>üèûд^¿yñXQ¶D"À3_Û†;7§wy|.*+TEáêÕu€º:h4̸÷ûä“OŠæÞ3ÝõxƒV™X§˜ˆæü…!|y'‡q‡kê˜_Oïë¥íî Ið࿃Òú*ÆœOØëÇÉニ°šëvÅͨ¹gKÁôoÿ¸›–ƒER›÷ß³p»ŠàÓ\¸ÙÑM :XÝž¹›Bˆ dv8øðV6äâq ºÄð9 8Q€€u|ȳ,£n˜FÆqmÀ†C…éú^©T£Í`Ä:­Z]Qµ„@B •Èà ‡àøYˆš9ñù³Üf6Ÿf·“íü)Äáp 3>¬Ùjµžd[‹U±i¬V’^ƒ0‘4)–—Êtü8Þß›ÖĢД-—‡çi¡S´‚õÓÑ3÷ׯˆ òùGšð§¯vÐkË® xªyó‚Ѓ•Õ»¯­bÞkG.Ór¢`Åj¥©D,YBbÅ\µŒøàæuJûœëÄÁ,dtáq¹¨«(ŵ;bñxÁÕXHPz@ÄŠžÎu "‰±ÁåvH·Œ¾'Ò?®%Ë¢Y>ÝÏÔÛÓÖð_/ߌ“n3ŽŒßȨÕzú­xé?Oâ±=­E ®VHðØžVìmoÀ…kC8s©ÁðòŶ‰c®~sõ#ÓÄj±:á¡VêP±º6€q°ÃÌ¿ƒ‘®[\ìça-¯*¾ˆð`Ð(QŒJ:š\¼6„ãç¯Ãé ¬øoŸ†ª2´5!—‹ú<åRJðy\vÊÁª(‹ÇEò,ÓyÂoþðhÆåèJä8øðVlm®a‘«|έ^ü콋ó‚÷Ïu â÷/âOìÆÞö¶¢RŒYt]€Ì `|()‘À°J¥BDiÿ¯þY;*+”øËçŽÐ4f¢Ê´R|û»QS=ÿù/Ï…a•½|~ñÍI’>t(‘HÐÖÖÆˆóèëëI’+r\ik4â÷ŸÞ¤´ßÆù C)ÝLbÑ8Æ~”–0ò~õßé¹;Èô<òœYçwî¥70~ƒÚÚ‹¢B‡õ¼§ úkç!úóÿu»7CµjáµÓLÜnvtS:¡4³ÕlÀ°ogÓ²¸ô1QéÀ35 f¡‡\ËéñÃ42ÓðºMVF<# *DŠF­~rÐAB2R‚ÇC‰X‚±Áhî`Þp(폀ü@zaõyaóyYŒ»Ã ìHÆŠŽXàÕŠ‘Õj=©×ëͪ˜zŒ–‰µrøœåYÔ*fØ!©l¸<ÀKZ±ÿûeýøžÚQ³ìî ç>ðôÀμøv7-p$âho7]ß•Hœò}*ªÒHS‚0Ï®pwV¬–—ÃA®„\(d+£@Õf¨¤ <Ø^˜†Ç²8›„LÃã =”ª¤¶¹ØŽ”«¹ñ¨½f<\QJ È&ŒØ!ÝÏ, NNû]¿^ÿvuýß„ƒ@rzxõWç°§½¡¨‚‚Ô ö¶7`{K-zú­Œ Ÿ ܤ<«Eê!R£EY™’(¢€& (c©û %!>·|°Ã”üƒ1\†ÞXæ›rý–k‹;ói[£mÆ >° +VÅ'7ýüýÇï3.ãÉûÚðG»7@&f×níì¥ u$¥Òk°þžÍ‹n“ „`긒ö¶™º;$a‡ÑÁÌ\³÷´7 ­ÑÈN¬`‚ì0c7D£Ÿ5ÈȪºMVôŒÁ4<˘§àŽ_BhÔê±N«Ã&ƒZ‰lE·§ˆÏ‡H&‡rZðCRUªT©fÃÅI ÀæóÂJz)Ç‘²$Æ;»­Vëëì¨ÆŠŽXàÕJÓ ~Êä4“^ÔÉòOJ‡c1œ4ß,jØÈžËþ¶ ìl,Ëzöÿ/l.gD=…ò”Iù§ÛqÏß}Hk_éÅÿùìãyNf—ƒrY©ú]ƒ«•".‡ƒ*U D|v:YÈjÔê !ø#ÔVëŽwôâ`–2…‹…Œ:o; ®þ„J™n›}9úí…>¶£<¸;äv•cÈ1ì,¿’nȉĆ*¾‡×=‚–Kÿw4óì–':zÑcºÇö´Uv.±˜ 7 áÌå~ôô[wœd,‚NÒ> `ÑRèÒ)B/–bµB ˆ&M$ᡊ-ÞŸÒ…8YÜ&ئGÙ©ލåb¨¬ÍÎ\2äƒ<*<ÔJ‹tàr¹K„PÉÅ,èÀjňÇåB@ðòòYçº`sÐÏæ×¼Ú€ƒo-w,V¬˜¬Î˜†Çp®kpIÈa®~üÎYxH!™Xˆ'ïkë¿:G»Œ³Ï|ë×øÎ7w¡ižþ=Æ›càñ¹(-‘ L+KËõ¡i½§>øsüÓK§ðO/*ÈvHxüÑxèþÆY¯Ó@ U·†]xóíNZûjµZ”—3ãyiww7"‘ÂwøÝÞRKk?!ÁG¡”rRšî«6ØG})Á)‡Ãh4ÎØkà½cô\r6}e?J뙕wÓ{{ç^úÊû­t/º‚éß‘@=¿8A{ÿÍûïYúZ  <ÜìèNÛiA(e l¤Áhf—®Pî`q=ç‹Åàpo/ó2ÛëYÑ¡Æj¥éðä ÉØè³ß‡*©<ï.ç†@†Ã+¢üé;ðáßÞ“q9?9ØŽº¿z7kÇ¥”Ø×V±¢.ÈëÊ2GBÑ(~Öu{ëÖb£ÎÑqÌÔà(‰ÿqäjÑÕ·X,F Àå²Ê\B>F… ÇVFh“¡gÌ&jó†®A|x[ö¾e"u*  [B‰B¯?„x:T0Y¿½pþ`„­F¨Ha‡´ˆ<–/ˆF?pkÆ­)q²ùÿÁ®®ÿ›èÁ2æÁK??UtnIÕUhPW¡ÓãÇ…kC¸xõ£3ãÛ"$lrž‹GH ½@ŠÕ"5V‹Ôñ ´(t€|ò [ÙäøXNïš™&ÒØf‰r¨Œ ½6<Úì#Ï­v^ijk4b}­W¬¸Ô;RtHr‰VÐP¥…(Þåj¡ ËJEp¹°bµ’¤”å/;¬i˜^„T,À“÷µaÿîf¶ÁX±ÊP¾@hÖµHÎøß42ë¸7ã€%2Fç 6®1°>Gûw7£ë¦çºi—á÷‡ñã‹lÄãnÌèxbÑ8ìvìv„B>JK$0¬R@(\|Þ÷­gwâþ½ øö‹Çpî¼¹`꿽͈/?½y*Ð[* L+Ci‰dÉs.&}ç…côë°½ç@’$úúò“ÅX¡Âãn„ÛÄ•«V ¹04âÎJÙ{Ú2J°aõ*Z÷ ç/ ̓~’²ú`XÅLçÆ³S;ez 6|ñóŒ;—ãß| aŸŸÒ>5÷lñ®Âšüîô®—u»7Cµjñu&H@ûØnv¤Ÿè£®}CFõðÁßÌv¨-/Åc{ZÙÉÔÌyL†yBcq€ÏiËéñO¦áqÃ…÷œP#‘¢Í`œrr¶a)ŠàñP"– Dœ˜¿ø#a£ÑŒ UÌ!ª¦Ý¨l>/‚ÑÈ,ÌngÁÕŸT*-„Ã|íé¬èŠX­(Y­V—^¯ÀóL=ÆèDö`qþ¾€†<. y\+¦œºfÇ©«öyAîTU­•âonÂwßÉŽ•i¦Ç“m¹üa¨$¹Ÿ|ÿä`;îøïÀí§w³ŠFñno7º¬#ØQU—• çþïŸA8ZØ«•F#4Z-*++Ѹv-4 4 <ý4ûeÀ*ãÌ*U x6@§XÔf0Rl/LÃcY 8,QH@ÂpxüU\.úR9,£n¶3-¡‰‰él6‹ !cn7‡F ú\5¥¥ÐhµX;ù¬Õh°víZü×áÃ8|øpaÌrÀiWaCÊWF€UàötÖ°©.«Ðp{¸xõÛÓZ”Áãj…{'¡Žn“¯Ýb¤ëÃB208ëžõº”G$ž`Öop€åäý¤,6ÝßTQ@8£ŸéÀxb~ŸÌ&ìÀYâ:ðs_š½LÚ`‹PwÕ[_»jE}B8=~B¨dbìÞ´[›«1hq ×l/Xׇ¤›Ã†Õ« QJ‹¶ý„>ä!¤b!ëæÀjÅŠÇåBW"gô1Ö–—â›Oìf]X±Ê‚~üÎYü×É+yù,2b+|}ã‰ÝøÆ+G3KÞ|»ÝW­xækÛRfj§ªP( Ëm,·=iMëõ8ú‹xïØu|ç…cY Ï…šÖéðø£Ѿ¹J…R©`E89¤ÒÙñþqzYòëëë¤ÕÑÑ‘·Ï2V(ñ­gwÎ{½»Ç ·'ˆ³ç͸5ä­a%( /4J)J•RŒ»©Ý¿xòæŠÚ¾²Ÿqçqîo`üÆ-Jû(*tXÿèž‚oüãnôø ­}%*9Öß³yéûAûø†»o¦½m&îüðM\ýý…ŒêÒ Qàé¶°“¨šÈB¸H<†#3'&¦ oñçfŪ@(Óð8úGÆÐmºÍèäE Ž%F­~rÐA+‘±OÖëX !˜ "±‚Ñ(‚±üá A‚HJ'K¬[¥‚!€ù@„+˜r†`!Àc~ÒÒCV«uíÙ¬èŠX­D½`"=y.Xne­,¹ˆÖ5v­›oÅuêj"ûÝlþÙÔ³?û ŸýÌ3-j€8‹L¹AÌ…!’JB¦ˆIÍý¦V‹Ô‰fôAÐÄ€þìÁpmæùÀÙO«ê*JWÄ÷§×‚— "š?G|4T•¡¡ª ^,£n Þv0úœä!ª %0h”¨1”e» |ˆÄÂÄëäÀŠ UK! ò÷°•ê÷ÄíÚUCV¬V²¾ñÊQtÝ´äíó¤b![é H&â›OìÆ7~xd†ëlÝWmxæ[¿Æãn\0x™Öý ÆéÀÀ c ~H‚suÿ½kqÿ½kñó_vâ翼Ì(LJM­åøú_Ü­wVA©±À·_¤çî@šššqv»ÙÖ IDAT££ô²¬_¿===ÙYÓX¯l»«zÖë·†]è¹jÃÉ3ý É0.wÞFØCÿÈ8jËKQ2™b®³ŸÏBÔÐ5T•á\µ5Û³öQ_JXŠ$à ÉpÊë}9ukØ—úº[õÎ6F‡å³kè~‹ÚuÈ ÑöÕG n¼éûõiDi[öß“ÖvtFFàM?ÖØTGës>~óXV`‡¯>¼­ Ö‡“±Í¹|dSc÷ 2cŠè'ˆ* YFÝèî·Â4<–1œ»\jÔêШѡÍ`œg•7<rÉ©Nlb¡hÁh±‰8üáðäkѬ|æ< "…RAÁh6Ÿ7ñ~(8õ~Îî‘ ÃÝáe¶³ÊD,ðÀjÅÉjµêõúC0õ±(,2/ЃÉ92œyPá½Wá¿}®_Ø\‘Öö—ÍNtºpòjÂm!Àuš]xåý^;},Fm2TRvy8×5˜“ jÃÿÏÞ¹G·qÝwþ‹Çà ‚ø%R¤hQ²h+¶bÉRjYVm9õ#‰OãG›f·nºµ›6›œM7–îišfýȶÇvÚ$rœÝ¸©íÄŽc[r*‹r$Y¶d‹õ %ðM$¯03pÿI‘)bp@ÞÏ9:¤@Ü;w.î æÞûûþ¾6x†‚s÷ɻՈ$ËaSd@M2€ç®ý>(•€r2þÉëË2yÙ æBb…Èvæ$PÈó1¯¤b2È|ìjÑÃ#]oM½KÁ‰sƒèôøp÷¶f´6V.Ù¡«×R¸eÃ*ܲa¼þðäygÖ¨k1SÌ/ŒÈ†:])îëX‹][V«™kålï ý™1íciì <¬[åZ²âœ)¢qc‘8R)>«÷› Z¬_½ëWgœ/z½cð†èEÿNÕP*¸í¸ƒÙ ]R÷”©ŸJ¥J½äÇ& “^Spw‡›××Âi3cd,zÍ÷õ¼ðããx{ÿyQÁɹRbÖb÷íkñç_Ù<NÈðÂãÌÙQe›››AQòx¦þè#qãF#$<ÌGU… “B‰Y‹4ÜXU>µpfcJ¤ë\­Û&Xð§ÏŒàs·Î½?=ê¡Ö(¯ ÑÁAá ñV\ßY>{µl4ŽßxFp¹ ßC™¥¨î7ñ`Cvˆ*[³±ŽÚ•ùOÙï¹ÕÝØ ­Q/øg~Œcÿ~ §vê4¾xÛFù¯iLº!ðó,—©T™r LL\-̘KL‘N_Þ+[JŒGâð Ñé¹ÏPI–+ºs°Œ“.49œ0Pä…J¡˜v‚0-„.‹!€žT1©øÉ Q ‡°êô°ê2÷ò…„0Sަ]# ?t9¡‘PçµZ Föc³Íçó"#– $b°\y2<Àp²0‚‡Ápn™ì·4Øñµõh©±b­;{ëÉ Õ¥ØP]Ї·Õúü4Þ81„—Úz –]ÿÉW;qwkj¹õóÝ­ØÖXž³sEu–íÓÞXþôÑÍØñÔÁEqW˜›¼ÔÖ‹>?½¨ý0PI„ ÙMäF2y_´º+ FÆ¢8ÚÑ‹›××J» ¡T¢Òi…g(8½ÐP,¸íôûÆ‘.²vç >ËÇ©îRª€Ž‹—µÍU••³¾‡—µ°a.d#v˜È¾] Ö%²þœŽ™eýñÌïcs;=H)zH²þý½OññÙܽµn‡eÉãtz¥%ܲa>s] †GC8í¹„>ï¢q†\ç3ð$ÇñƒÇðê¯KñìÖm0ÝÈÍ=nY^ǵÓ.?ï†<¢ÚÔ\·bÉö·P¡Ã|Ôºm³Ü¼þ0á8¢ñ$‚!0 –ã%o¿Ù …Ù …ÛaA™Å»Õ¸¤W2%Îú©T*¡‘Á~*P@­VA­R‚šüI ,'ôZ 5‹äæò·_ÞŽoüðÍyÿî´™±÷«·£Nb×Ba9óúû=ÞÝz]ÁUÅH]…ûåíøÁÏß—¤¾Þþq|ûÉØÜZ‰?{ä†93·ç äà½÷Rd^ñCó:þùé»Ü·÷ŸÇÛû»päX‡ÃyëËÊ•l¹©»ooÀîÛÉZÑ\„#I|ÿéC¢ÊZ­VÔ××Ëâ<:;;AÓâö ×­[W0ÑFp,³n4îMf%vÈu¾W³Â&ØUðøÇøÜ­sg­ñÇP[#/ÁÃÀð{ˆûúFYá§^‹ [GØq#\-Å'néMQå(v6ë÷‹"ÀÅãY¿wõfáî6ƒìÿ?¯äÔ‡: …Gï½YökÁ|j~¡Ãô{&Å* ’Y$¤"Û—ˆ»C‚áà âLÏ%x†E™pÈ@QØä®B“ÉF‡ƒ „âeJ ‘ùl¯ïÂñ<¸ôåΔcÄUï™ç¦Ä¥Óóþm&SŽÀâˆê¹Ÿ›¦„I>uÙ1â G‰"‰)ÛGF$!Wˆà°,ñù|§\.W€mrmã8Ë`Œe`Óäos˜åy FÄ—ë4*<²­_øL,FJØa.jFøT£þXÞîb7a^!íéWÞEßá“‚Ê”T8Qç-Ew¿ v÷#xa@TÙ57µ€Òåw.ÀÐ É>ÙGes úý½Ãxó~’S‹EìJ ÛoâùÌ ¼Z‚hH…€Ëus,ù(•™—'f¼EYÄy2ljKÊÖÜÜ\°vŽŽÆÆÝaŠZ· G;„í×Çã,zûÆæur•—àA &·Cív÷ã俾.¨ŒZ¯Å†‡ï¥/¾{^÷[‡E•3XÍX·ã†¼·o°3û½6G%åÙW3toüÃOÁÄ“9µñ‹·m”ýšP:-<¹)“Væ. bYæZUPZ`*q¼¢ÈÄã‘8:{|¡ÃPI–+ºûH•¥tÒÁÁ…Vw%y˜#äZ Z ›ÞŽçá†' Ì†Îq1tdzdD¤€ËŸÏ·ÏåríP-×6Ž2 $øôyò\óÑQá_Æ“b‡•6=øô*Ë P«ò·»­©ÛšÊñÄ}Íx©­ϽÓ%yöÿ¯ÿìSlk*džêÒœêyâ¾f¼qbH’ y9@Hpp%ªKñ›onÃ~¯ 4“ÿ6ÜÝZx©M¼¨Æ^V†ÏÿÑ¡qíZØívK Eå¬\'­îJÁ‚8ðaîÙ¾>/m²•@'XŒEâEÕ—z-…r›£cQ2°p)(.ë‹ÉdÂüÁL“ïb‰‘Jì0_ù‰ê”Zˆ0çk ¿/ßõ/ôZe0s@ÿl¶Gœë±ÁäÄ#Ýo¡•ô£&ðÒo?ª•eøÒmQZ"/;ÜÃ!9)l`¸”äAãv‹v‹ëW¯Ã¥Ðçƒ7A¯7–ã—åíà÷áAìêª6Æ…‰hЕÙ<¿˜ÇþP¸g•ÆâßxJ§''Š%Mè@'SB:qù5­F ¥†^KA¯¥ V)IGŠb.T³Â ¥’M›ê*Èü€@(«V–åÍåaÕÊ2ܳ}ýtÐ>ASý÷ükG@O Á¤b¦ðás·Ö¡¹)¿Âù™â£Q÷ŠØJ P«¯~^²”è°å¦À2 òÆ_~ý Ñeåâî …Ð××'ªlMMMÁD£þ‘$8&à@²`ýc6hQf1 N qúìȼ‚‡p$ †IÍ.Äqè©ÁÆ„í£¬»'J*œEw®¹¸;ÜxÏŽ‚´±¿½+ë÷ uwxã~Šˆ<§ö}ñ¶E‘ä&ÅåVV£Aö{s!A”r©q± ‡3Ӈƣ‰¢»w( ›ÜU“"'†âÜÉ…P2ÄŒ~–Oáç_÷V+•0i´°êtШ–×÷?¥R¡Újƒ7F8)Ý3›æÑ—û金ILN ä ™9–;ÏxFÎ ôÐ4—ÈÇ.k[£+mz€IKa0ÇÄPa˯ð¡ÆaÄ÷5ã±ÝõxîínÉ…;ž:ˆO¾· 5cNõüäÑÍØô­ý¢ÊžêÏYt!5Iއn6(oYëÀ¿|¥_ûñ‰¼Šö´®œþÌÛΊ »ûî»qÏý¹›–,£ σæXps¤ÑP)”Щՠ”³íýÅG«»vƒ¸° ‹_:7ÁT:­`¹ÔtFÎbÁlȸ^ÑCöÄDd$ª®®ÆSO>I:¯ÈÆÝa>ÅBõ,u±Ã¥`ŒçÍ@úò78µñ+x¼çwxÎû±ä]ÏpOÿ¿6ܲqÕ¢1\ ,—ɈÎr)0laÅÙZJ†êr4T—cû¦ÕÓî}Þ`ÑZt‹áHd𩳸n¯øÛÇF`2[Ù÷†Ž‰:¶Û^RÔ«éô±B±¤`çÂò…a3÷»(yFR«UÐk)uèµq€ È‹I‡J§*%çË‘{¶¯—Ô=À¨×àæõµ¸çÖëˆpIBvnn@ÝÊ2üíß”\ô\>479±cÛj|îÖº¼ŸM³¸p1•Z™q}p•Àh$ | ÉÛûÏãè‡ý¢Ê®[·F£QçñÉ'Ÿˆ*GQTÁÜR©4zú2.¹þÞ›º%‚g|س»qÞ¿{/EæD²ã俾.Xàj©GåMë‹ò|ź;8jÜpÔ®,H;.fýÞºÍ×eýÞC?þ5†ÎxrjÛoÛXIEÒä I§,„Z ¤rXŠ.æ­sÏP`ZäP¬kà›Ü•ƒÝ‰j+ù®‘šžñ BIáÏ#¡d—bJT[JaÕé—]¿9M%'“%Oö_ˆINÿ¾:µNcƽԢÓà ƒMË>¹Ò>ŸÏ"W A ˆà°ÜÙ`/Ùzµy4Öš­PËDÚ[ç¼lùm3iÀóè÷Ó‹£ÂfÈ»ðÁjÐL ž|µ?|§[’zÃqO¾Ú‰Ÿ>º9§z6T—â;÷6ã©×:—IJX‚¸k“,w=þç/;0ÎO†’gº>óðgE9slß¾ˆKJ¥‚ŸŽ ²Õ3Ptj Zµ:5š œ+˜ÙsÄgWý>_VGºßB?–v^ÀrxïxÎx.åÝî<ŧÁ°)°\ †“彦ûxýax‘éŸKš¤2»±;õó‚ðgž÷vÀ“—iè–uEÙ])>P4hœ!BBîã)Å#šâ§z-£^£^KÜ‹ŠJ©„«Ì »ÕH:ƒ@XÆìÜÜ€×ßïÈÉåaÕÊ2´¬q£e;¯ëL˺ ;^øÖýxâGïæÍ•£óì:ÏŽà•WOaÇ¶ÕØ³»1ï">•Æèh ££1XJt(/7¡ÜA2÷‚oï—Ž¢(44Èùexx~¿_TÙúúú‚‰6.xàSi$ —E16œ}¢Ål.ïÄ'o}Sÿݶ¹A´Øabâj‚B‘ù‡©Ÿ"Å2š‚¥ P¤®6ªÎj®¬‚$.»ŸøÃð §…ÅH•¥M'ZÝUht8AÈ1–%v˜~vO§1 -iÁCh qŽCx 4Ë¢?<Ž8ÇN¿.v»*ù««ž%W A*HaYãóùB.—k€Çdý%HGQg’—&C¥TÀfº¼8¹‡gºÝÑ€?}þ8ÚÎæ\ç'†lιžÇv×㥶^ô„e¸ÈÖÝAŒ E{_H”{D,™‚Õ°8™p¬ ¶¬µã_ÿËøÇ7ÏâHW@Òú¿soót_¶÷ ;”Ùlxäá‡É”°¤áx/L ç¸Y4¥B£F•ùGòfךFÁ‚xýýÓy݈V)•¨tZá ‚/²`Áå*zP(‰<TU••0 äÂ]”x®ó!v˜YN¢ús>¦„îWÒ¼:À§›õò­–*œÚø§Ø;ðû¼¸=x<ó‹6ܶ¹A2·‡Ã!Ép`8 —B*ÅÝ%ávX2"É Ã)ˆ`˜†×F4Î,‰KßI³»S?iБٰ8Eà¥ÑÓ¢Ž[jÖEæ¹™0\ áXr:0@ÈS¢°@ˆ†V£†NCÁlÔBK‘9¡pØJ pÚÌÐPÄñ‘@ ?xl^~û~uháç>§Í §ÍŒ–5nÔU”aý7Lz-éÄB=ÛÛÌxá[÷ãù׎dõy‰eÔOã•WÛñÊ«íØ±­{v7$°9I"Ib`0„ªJ+>ä‘ï?Ó†ÁaqI6nÜŠ¢dqŸ~ú©¨rF£±`¢¡ÆÆâHó€ï"-ª6·þ“cÔí¶ò©4‚cq”Ù·¤D'¸L°»5Û6-J{Ùh‡ž|Qp¹ ßß©n k_P<àj©GíŽe#~ëîP³±Ö…qŒêÌÞÝ¡b]vb‡Èèöÿð9µ«µ±RÔzn: ð\vÿ  TJè/•àA ( À±ÂDJ ’ùòL‚ᦈ}m½ ™TÎõ>´µOÜwÙ^öÐYá‚™M­­äîI dAzbQ†A”ÉúQ* ³FƒF•BA:IF8 &4:œ8çT®ã¢í¼hYãÎß„]K¡Æ] ÏP°èúÕlÐBC©àõG–Mvg¥àœj0"|“Îîp‹v1‹Ø"'¤þœŽ)¡ØA1OýîPÂÀ_~“U­Ë«ÛÑn —ËñH0X.†M-ÉËdÊbæyC4¼!Á0]”"ˆ FçÂãtæßö—iàbr7Ð&ú¸;?³¶hú(g¥“EçJE(~6sO ÇÐjÔ0´ÄùWLz œefœL ®¸7hñè½[ðàîÖy×oê*ÊȽ#tø‘`8˜ôXLz”uY‰Ñ½w ZÖ¸ñO?t‚Ík¶yp°Íƒæ&'vl[ÏÝš7†IáÂÅ>ä‰p$‰þíCQejkåáäÒÙÙ š' (”hcÔÃà`&y[p .¹xkÌe#‚aaýÕyÖ‡æ&×5ÏO‚‡ëš„gv÷/Z{>ó2b>a Wï¼ ÝoÎÚ%Á×Þ _{7ÊÖT¡þέ‹*|ÈÅÝ¡iÇâ¿KèìƒÀÓ|}§º³~¶îïþð0qñI5Z+ñÅÛ6 +4p\ÆÙ!ë"Ïgþ©TÅçp0/ŠŒè!•ÊND¡’±Ø¡ÓãCÏpFàP¬Nśܕhr8Ñhw¢ÚjaqPI¿¨‘y þá~úCcèg\«i–Å@x\Vm,”ËXŽì#W AJˆà°ìñù|}.—ë%²MÕžšHc8A£Ú`–´^›^øÂÁGž1ÜÞ²¦²kß>¦„¾P5#\V]^ûèîÖ ôüŸrüÉóÇñæ‰aÑõ„ii‚¶5•ã¯î¨ÇßYxBk1P³‚ïåDLA­«l¸è‹áÞÍ•ØÒàÀ¿èÎÉíá¯î¨Ç3]?ëµP\øBþ¦ë¯'7PAÏ#Ìó'3 sf­&f­ŽˆdÂÖê:Á‚øÕ¡Ž¼ €ÌÆy¥ÓŠÁ‘PÑõ«–RÃí(Y6¢…B˜ËË z®ªª"lÁ?ع^\$±ƒÄ‹)v˜¾1¦€ ! Ë ÄfÏѦÜžõ~Œ'~/ùp˜r{سµ·lXuõ<–OƒaS`¹ÔtòåJæ;Àr•8Äë#Ž#O"¢Ó`9ùf5z¤|=@MÌ?NgŽ×w̯@Œgñ½¡c yqŸ¿Û^"{w‡ŸF”N"gŠÒ¡„°ô˜?B4ôZ f£FJ%™orÇbÒÁn5’`epMLzmÞ׈—™•@,Á"–`1ìg-N»y}-^~Òüü}íèË{{;ÏŽ óì^yõvl[=»a4æ7ï”ðat4†Ú[Þ·\øöÞýˆDʼnù››å±/JÓ4º»»E•u8X¹reÞÛ8êáÂÅ̾h"ÌcÜ»¸ ÜŽÁ‚‡‘QÍMóÿ}l,†IA«]Üð%‹‡ï'祭}m'ÑýÛ•)©pb¼wX”h xaÇžù9jw܈u÷ß¶(看»ƒÑ*>ΆMfGˆÒõ eýþŠæÕ ¾ç“ßÆÐøkÖ^‚=[…Ýs'&2޹Àó@šÔšÌ~QÑ£ÔT¦oÒ|Fø0S "¥»…”xýax†ƒèô\BÏp°(»¾ÊRŠVw%š.4:œ È=E¡Lo@0U^¥T¢®Ô.Ësëá…Ge'n¸­V[ îm>Ÿï¹bRBB†}±àâ1É¥:½à2I–Çás£¸®ÒšÕûŽG—7‚>?wáƒÕ Á¯þæ<÷N¾þ3qÖ£ÛšÊ%kÏ3]Íág‡{|_!]„KÊ#ãêj— ez xú¡ëq¤Ë׎ >TÛøé£›çüŒÛû„Î’ KA¦Ü.E#Ъհêô0k´ ä?9[²l­®ÃkgÛˆ Û´8Úч‘±(œ6s^Ûg+1€N°‹Ä‹®oµ”Õ®Rxá%›Õ|&*J“kjÉ w±CÖ›®Dkà IDAT?—þU¨¿! Œi€~žíö°·ê|¾¬÷ümáɇƛ‡;Ñ鹄/íÜLdæ€ †[6Ž6¹0—bÊ "g3„h°\jÑÅß\y\#à`§ï˜˜ 1žÅ㽿ƒ')~c@è†lAŸcã è :ÁÁL-S‚3¥R £^‹I-E¶ÂÐP*ØJ (5²ÊN „ÂßÏE,Á"6„I¯A¥³ôš÷p“^‹½_Ý…£½q{€Q?W^mÇ+¯¶cǶ:oÖu/äðñWö‹n{©Y?¿w ôZN4ˆfTŽ4, §d„ *5 ×™ñx$Ïpž¡:=>$ÙâKBd7ÑèpMŠœ0PD¨*Wª­6”Íà'³ßE™ÙëäüDñÏYZ• •zŠ‚Y£…J)¿gñ8Çâï˜Õn¹b0Ša˜ì#W AjÈîÀçór¹\m¶Éµ >o‚†[/]`¼I£…Q£Í ›±ìo÷Ꭰn¬¯²f]¦Â‡ÇîhÀ¶¦rìxê ÂñìBZª­’·å§nFÈçÞ麪-ÛËñôñ¡º4ÿc<,Îâç'â' V-þ TG©°Ú•Yìn]eÃcw4 ÏO£íì(~}bí}!ôè«>Ó–êR|þ†•¸»µb9><E“Ja$Å¢Äùa‘ÙZ]‡×Ïu.÷ükG°÷«»òÞ¾J§,—B¬›¯R£T*à¶[à F–E¶s5•±ùN R‹æ=F@1!î\s©_RwŠê·±™Ýf :{¹gƒÑ‰C×ý1ötàñÞß!œ’6@»g8ˆüü}ìúÌÚ«ø ˜rƒ˜@˜˦¦S Ó¯I‰QEá/]­ØU:éⱆ{ü)Ð à° ˆ©àciüÝ@[Nb‡µ5NXLz0\J6Út‚dA'X"ê!étƉ$J'§]Ì’¡Ÿ0?*¥“%F,&é@1 ²Å,ºüp•™a·^{qÊíáWïŸÆËïœ(Ø9lóà`››[+±çÑÜäÊëñ¼—"G’XSg'n"ùÇgÚD•£(J6£èëëU¶¦¦V«5omK¥Ò8ß5Šp$³¿›æß…8øÔâ/²Ú-Â÷G{ûÆþ<ü±E<Àº&'Μæ€ÝõÖaÜüõ ÖÆCO½6&,!TÃ[ìî—äøÁ 8öôÏqÓ׿\0ÑCïÁD•[sS (]îsßX0´ àEè÷e]gźºßóî_ç¡ÓPxäÎ…‰0¹Ÿ#1 P?…b¶ƒ‚–Ëör§Ç‡žá0"¸Üsïtṇ7Á¤v™)|Xí2ÁnÎÏφêR|ò½]¸ç€öþì²÷?q_~Øž¸¯OÜ׌SýãÓ™£j‡Q´«ƒ _(!~Ïp°äù Tã0¢f[-ÞVKî`Âc¦óƒY«…E«‡YKr ÅkE Žvô¡ý‚-kÜùÿpÛà ¥h@©TÀí°`t<†(\òãI­¾lñK(>N…üh¶–A­˜™ieBz1ÂbˆD r¨_´û…€ú¢ÁÃEÀÏ~Ó#ÎõÓn/ž–t¬°7?8ƒ›××býêäâÉvKvóÈ™Bˆ7?8#ø8·[Waµ¾»¬«`RMÎí©Ì¿+ÇŸ@@ 6¼“ãx¼÷=мøïh ¥ÂM×U#K K@­VÁ¨ÓÀ¨×ިͅtzt’E‚áˆÈ°d˜r}‹ÄQbÐÂbÒC©$"sBæÞk1êPbÒÁ¤'óo@XJðé4†ýa„c Ô¸m×̦jÒkñàîVìüLžíŽvô¬ÇO âø‰A479ñÀý-y>Ð4‹Óg}XUcË»³ÄRãÿÑŽ£Š œ®¯¯‡Ñ(×ûÎÎNQå(ŠÊ«»C8’Ĺ®Qð“Ö¹i<Có²è7³Qx€;g³º&&­vqC˜>û™Á‚‡îß~€Ö¯Þ 9ÿÉòúÚN¢ïðIAe\-õ¨Ýq#ÎüÇ{’µ#24R0ÑC<†¯½[øµªÓ þæõ’´!I' †a.›;IIšO#t)€/uåµ×ÞG»x¼Cg<¢ÛüÅÛ6 NL3‘ÎÏ>Î>•qFÈ¥às¼í)—¨‘“×Fgž¡z†ƒEyU–ÒINòpE ‡û=EÑÎ"IÐû,Q„|@Â$>Ÿï×.—«@µ\Û˜àScØ$TE6ÚËE zGiüÏ_và™‡Ä-è03ƒaXŒjƼÔ×8Œ8øØñÔÁEm­•ÜàJ áäB´|RÑvnTÐû×®]KnšB™?( XuzXtzèÔä16¯“dJƒ[ªëðˆIýËoŸ@Ëc{òÞF•R‰J§ž¡ ø" >,/5R)1‰/ù1¥Te²éð<¹¾ŠîYeqj<€µ%¥0©)äEì ©ð@Â׊ù˜æ°1 ëK³7­jöÕ߉GœëñxÏ{h§G%3G;z ÓØ¾i5¹€‘…œ"â[7Í~ÁÈÛbW5€Np.3Î^ žÇ¿\:™sû·oZ3ËÕ!•â§ÅJ¥z-½V ¥–T‘âÓH06…$›ùI ,UR)c‘8B±$¬&ÌFÔ*%é˜e„J©„É A‰1#pÐP*Ò)°Ä‰%XœëE]EÙ‚ÏÑN›{¿º í¼xùíè¸è-X;;ÏŽàÛOÈ»ðO¥qábI&…ª + YŽ$ñý§‰*k4eãîÐÛÛ ¿ß/ªl}}=(Jz!~*•ÆàPÞK—³s'Â<¼çc’9;$%H”O§8ï¥jkl‹:6¶ÜTr\P6ÇéWÞŦ¯Þ“×¶±Ñ8=õ¢ 2j½-ß•—öD†FÐþÒoÐúç÷çõ¼»ß:,ªœÕeGß'ça]a8jWæÔŽÐ¥”*%ŒVóÕó¦EÙ²P{ýø×¢ÛzÛæ4× ÿîÌçþ Ï <¨T¹·GµD¦¸ã‘8<ÃAtz.Á3\ÐÝKŽØ F´º+§ qØ"È“@œ–}U*´òO°Œ(B> ‘bÂlöø©œè¡Ã°iÊ%«Ï¤ÑÂi4c„Ž .ÛÞÂ?¾yßÜÓ(þŽæÐN‡ò&|°48ø¸ç¿Ÿ3¨Ýb ðÄ}ÍxìŽ2úç!É‘ÈD ÒKÄ1–ˆC«Væ7À¬ÕA¥ ÙHóÁ½MëE :.zq´£7¯Ï¿ûŽ^K¡®¢ Ýþ¢íçÒÔjFÇ¢K~L)”€Z9i‰{‡çVKNmü žõ~Œ½ œb$3]ý£„hìÙºnVÐ:¡8h1^±æ`OÍ-vªc ©„¥ñ½á£’h®[½µîùÒé4è:qyÌj5ñ¥RB­VMmS3~2‚.ÅOÖ3–KãÓH¥ø¢t"$™g¥Ó‹Ä1‰ÃlÔÁVb ‡%ŒeÒ½¡ÐŽ9@|:î?V:,°[δ߲ƖÇö,ºðáϾ!oЃƒ!0L kêìd€,À‹?>ŽÁá°¨²7n”Å9p‡3gΈ*›/ÑFŠKãT‡ “ݧy`Ü›Dp@Zw`f‘ö|{úƳz_8²ønÈ»o—ôîô+ïâº/íÊ«Ëá§^–¡´jJ4ŸŠ"ÉÈ šÎôƒV»¸ëH_º¯¯¼Ú.¨ ˸/ìü§¿ÎK›úÚN¢ï°0GMWK=\-—ßíõÕðµwKÞ¶3ÿñÊê«QRᔼî¡c’Š4B¾N½À©wŽ iû ¨¿y½`áC’N"IÏ-ιR`1”Nƒ’ò¹E| À±Wö‹:?†Â#wŠŸL`k*„,3¨T˜Ð»¡Åçîàõ‡ÑÙãƒg(€žá`Q>'4:œh´;Ñê®DµÕ¡8DZ '½ƒ²mŸJ¥‚^¯/†®ÜGF!_ga>Ÿ/är¹žð„œÛ9J*xpšÌ¨+-ƒg\܃óþöKˆ%9|sOLºÜn+#¡$FBɼ6T—bCué’«ªKçt¬È4<’ÂO¤J$O W†ëÔj(¡€R©„N­‚F¥†J±¼PÒ'“'“Ó®Vž 4‰ëò02ůÞ?w·¤¶’ ˆ.Ú¾Ök)¸%ðú#ËBôd2ùLÅΟ–b`PCñB,ƒµ–R˜ÔÔâ8#ä»þB8/,ôZ>ê·±™Œñà jtüºñ>ü:ØÇ{~‡~&,ɨ †iüßwObÏÖfØ-Fr-JV`Ç}£øËÚõ0…T@hr^Þ¯¢J€Wà=‚wÇ{°?Ô#i;wÝ´–d¥“ ,¬&,&=”Jò´VL÷|‹Q£^ ‹IG:„@ –¹8öŒEâH0ê*Ê Rf·ÎܲƖ5nŒŒEñ³·Oà½ã];׃m|øñ ¸¿{v7J^ÿà`Fƒe6Xsðí½û‰Š <¾þúëeq4M£»[\Àw>E)nÞó4bÁüdN§'àÅ8 ŽÅá^Q²¨mØ}{ƒ`Áô>9íô %l4#¦ôÝ`³ åá»f½V»ãƼàÌ/য?(y½=ÿùQÞ>ç³ïŒ¾OÏãÆ{vÀQ›ûµ-ÄÝÁ것K2sŠ->ùÍa0qqn'_¼m£èç‚BlKMˆP.P€c³=()#÷M§ñH|Zàà "ÉŸó¬Ý`œtpp¡Éá„*¾D³•ܱº1ï‚‹N«V­š‚ËdT[fÇ2^)êÎGCˆGåÞ/ù|¾>2šù‚ì W#{ÁÃ(“@‚OA¯’înuWa 'ÒîHWý³OðÌC×ç,z2‡@”A…Í€ ›jÙX]LbÉ”$Ÿ+@†Â!0"﵉9ìó(•z5=EÁ¢]Þþ3]¬:=lz(IŸ ¹¸<¼üΠܼ¾u…±‚w;,àÓ‹Ä‹¶¿µ”•N+|ÁÉRM³6b)§Æ¨1™Qq¥ñb8#ä»~Ū_ >°Jà¬`fO~¾¬·Zª°wà÷xÎû±$£‡åx¼y¸“ˆ»Å((ó"ìõà÷¿ÄgÍ•pi.n§è\LŽƒæ¥Ý„ÓP*ìÙÚ ³d'äB:ÆX$ŽP, «I‡Òø'WôZ ¶ŒzMNA¯@(NTJ%,&Â1qÁ‘ †Ã¹ÞQÔ¸K¹9mf|ãËÛñè½7ãWïŸÆã]ËðM<ÎâÇ/}Œã౿؂r‡IÒú/x0Ý‹ža^n9Ö‡wˆ·Ô××Ãj•‡sFgg'8NÜ|vóæÍykçó&v`¸üã±¢YoG’2<¬EåJ ‡…'9öÌÏ¡5PçVÉÚsè©ÁÆ„í{lxø.PúÙè²újèm$ÆÂ’÷YðÂu ò¦õ’ÕékïÊK[g}§„¢8ô“7pÃ=;P³qmNuѡ쿵+‘æçV|ò›Ã¢Ž¿n• Íu.Y_ßbŒk¥ø°ÐöºZ-_7ñÃÁ3Ä™žKð 0Mݳ€¢°É]…&‡'„¥F£Ã‰ÿÚz3^w·¬½ueð ‹Zô T*PQnÅèx Q:I!aqY ˆ>Ä28E…шSÉÂeó) õgQÿ¬Hfþ-"³ç–·ZªpjãŸâñžßá¥ÑÓ9¥hœÁ›‡Ï`ÏÖuDôP`Üv N_¼$Ûömß´µn²AD È~>›âá F ×R(·™¡V)I§,¶JKô‚2pay|?BtÎëa¾`±8ƒ· *¥ðïú›°ss8žGˆI`,GzbV­>¯íaÓ<:ÇÅÐu{Éè!ä²ÃL ÌÏçës¹\o¸[ÎíìǰÖ,­h«» c‰Æ“â³'ÐL ßó<¾¾vûÉÚÆp<ÚûB°)¬vš‰ã€@r-ÄŠ8h”@  Îqˆs!P*ì£h»ÀåÊC-­8é@\„Ý÷ËïœÀú5n´¬q¤­*¥5+lèðƒ/¢ÌUsQ^j‚R¡@8– ƒ ær{@}t&‰Õf ¬­øü|øcêÇ"Ô?gÝsK`]jà/ÿѪÖa_ý·‡ o!œÊm£<¦±ÿØyìÙÚL®¥Rë¶ÁlÐ"gdÕ. ¥ÂöMkˆØ@(2 ‡þKc°•`1é¡T*H§`Žã(5¢Ôl n@˜—J§Ýþœë‰%XœëE»T´À®®ÂŽo|y;½÷füêýÓ8p¼ #cѼûñƒøö“ûñØ_lAm4ó‹ÁÁ,ÄåáHßÞû®¨²F£õõõ²8ÞÞ^„B!Qeå$ÚȆKÁŒ"•Ê_`–Vws“S𸓃à(— #zð~r7ÿõƒÐ˜…£zêE°÷6<|(ýÜ}çkïÊÿµvð#ICÇÚå3?òÿÞÁί}”Nø÷'@`ä¨] †žýÙvzñ È.5ëqˆU¹Ÿ|¦öŠ%–7!Áp83-p`Ç\NÇ”Ð}!ßõ+®Q·bŽ÷)ØÙÌ¿.SFü0ƒÏ—Õã”ñ+øü¹WÑNæ6 Dp¤£[Ö×’kª€4T—ãĹAÙ´GC©°gk3ì#ùp„"e,G$Π¼Ô½–"’'\efØ­FQY¶ °¼Ðk)T:­ å\ŸNÃ3„«Ì §Í,º“^‹w·âÁÝ­8p¼ >ìBÇEo^ο·ÿãÉxì/¶à37äž‘8I‚¦YÉ\#Š•ï?݆HTœxþÆo”Å9p'ÚÝAN¢l ,FÇcHçqM¼ÉhZnæÜß‚_üÇ)ý°_tÝ¿ýÞ“çpëÿîë³.××v}‡O :VíŽQV_=ïß/üö÷yï³ÈÐ"C#(©pæTÏ౎EùÌã¡(ÎüvVpY_vßokæ{5ÍϾNÏüXT›w~f­dsr• àóO»¦µž¡À´ÈÁˆå9lrWfv§lìýñúCãè¡?œù9SLPìL‰4¦~Îܯ²”¨ѠÑîœv‡¸–Ó¡Ï’<(Ë`,Ï9ŽR,§‹ÃÝáY2Z…€„yðù|‡\.W€mrmcj"~:Š:“EÒzM-v®jÀž®œ¿¬=#1|õGá›w7жŸ‘P(ƒ‡6òŸb¹г°–ÖÂî'†ð'Ï'7(³±ë¦µ0´äƒ!ŠœTЇ×†Å¤‡­Ä@Ü$ÄbÒÁm·G@ ÂVb`1‰·Ë IDATKRŸ/E8–D¥Óšs0åÎÍ Ø¹¹í¼8p¼ ï—>Óx<Îâ~ð>þêÑ-øÜ­u9×7ê¡Ö¸|ç­C!¼øq{h‡ååå²8®®.pœ¸ÄnrmdC4Î$iN”žÑ¸Ü!,c8ädÓ¯ÿüôݸõöE  æ à­Gÿj¶nÂM_ÿ2Ì+®3ÁFã8ôÔ ‚Ž¡·YPç-óþ½û­Ãú ÒgƒÇ:°îþÛD—v÷#1^´Ïü±¬¹¹Fkö‚?.™ýø˜ª—M^Þ+fè<Ç…¯·–šõhm¬”ìÜó)xPéÔÖëÃ3œ:#U–R49œhuWÉ6pþœgý¾%)nüüŸî“™èÔjpéô¬DU–RÜÛÔ‚Vw%ÒÂOL Ê$cD™ÅuÎî!Ʊrï²~ŸÏ·ŒB! Qqµy2<@<†j£YR—°é Ø^½zr_p¤™¾óËÓØÒ`Ç7÷4Á¤“îÖÃóðøb &°v¥yÉäKÉőܹbÉÔ’éßPœÅ“¯vâ‡ït‹*ðûÉ€"ˆÆm¶ Ì$à8pY.«QÃôÛJ JµRJIr…ãyô‡Æa (¸ÍP*Ò§×âÏ[·àïU¶g8ˆç_;Šo|y{ÁÚ+ef»Åƨ×À­.×!¢Ââ 0~1•Nãb$Œ¡x k-VX5Úk×%1B¾ë—»Øa ›ù÷© g7î«¿LNüuÏïrR·c´Yš*ZJí›Ö`ÿ‡çµ Õå¸y} ùÜ „%F8–d‰Ûƒ¨”JT:­°˜t¤3 ŠJ§$=$Ý~¬tX`·æžŒ«e-kÜxhw+~öö‰¼~øüÈYô‹£¶fù þòëoˆ.»yófYœMÓ8s挨²rm,ÄèxL”A b2š <ÈÅáª*¬ø_{wá¿ýÍ9×Õw8ãÚPÿ‡·àº/íš×áÄ¿¾6–T÷†‡ï¥Ÿ{ÑýÖatÿöQm64ˆÇ…}¾S]9 ËÝa&޶ ry]Ê>ó¶£våôï).5¥ÆÅã`â¯áŸY+í‰+ò'z(–í×ñH|Zàà `<š@±a7ÑèpMŠ*e™ôoJàp.0rU`?an’©«Å€áq›ùht8±kõZ¼{Q\°äÔf!E¶’Œ ÔR=h)5ÜŽøÇc`Ø„‘ð`Î2Hò)œ ÀªÑ¢Ælž-|˜¯®¬_›×V)ëÏöµ|×åk¹ˆfþ~}è3ƒ³7Lwß«J‹Ç{‡pJ\v–ãñþ‰‹ØuÓZrˆZ· ×­^Ó/üØJ…í›ÖWa CÜrgÊ¥N¥T’Î BNH-z€aáX•ÎRIˆœ63¾ñåíy>H!z`˜&­vù…T9Ö‡£ö‹*»nÝ:FYœÇ§Ÿ~*º¬\D QH±Év.x=B„pˆ¦YòÚ|àþ9Ö‡W^m—¤¾îß~€îß~€²5U¸îK»P³­sfÃûÉ9tþû~aý»ãÆ9ÅÁî~t¿uÁ ¢ÚYî0bǶՂÏ;1†¯½ ®–ÁÇäIøNu-úgÞ÷éyA‚:”}âIîò¸æYnZð †ÂºU.ÉÏ]¥ÒüU)zr‚Ò@pB¥BÒéñ¡g8#p#êZl 5-ph´;Qm•ßú«?ÃIï ÎúGpÒ;HÖ%æ¤wpº_«,¥¨¶Ú2ãÁá„Ã`"4ßwσæXÄ9qŽÇó²kc‘¸;„ì##ŠP(ˆà@X˜½~*çz“ñ¼ ®Ô¥Åûý%ùr 'ñõŸ}Š{o¬ÄÃÛj%u{€`”A(΢Âf@ÃHFï |¡Ü»rL,6¹º:„¥Ozb—¢™…,"z˜Ÿ{›ZpÂ;(ÚNô½ã]pÙÌxpwkÁÚ¼äDv ¼0= ƒ±€+·DBƒSc \zjL%ЩT…#ä»þ¥,v˜ú½&ØXà´H_~Ã#ÎõØ`râÖÓÿW´è¡ïÒz½c$¾€lY_ –ãÑÕ?Z°cÖ¬°a{ëjâê@ ,¦ÜV:,P«.î{ýa$NÐ3|éäsür€ˆ 5•N+øtá˜tб‹s}#p•™á´™%©3ŸÂ)DÉe*xëî@Qdq£££UVN¢kžcÅ€8‡§ˆ=û//—ã~únœ>ëÙ³ÒeY^À¡ïþøîP¶¦ 5ÛZqöõÿ6°YPç-ÓÿÃv÷cèX»h¡ÃwínÂç¶Õ‰zøNu‹<øNu#•dýóæ’,†ÏõbecmVï§Ç³_ˆ\}„BAv „…&U>ß>—˵2vyHð)x4Üúü,¼8Mfì\Õ€=]’}á¿öÑ ~ßåÇ×n_ƒÏ68¤}Pá'Ð杻2Xí2ÁjXš–]Õv£ —‚‘pn ^ Ç#ÉñEéž!…«@X>Ñõ1Püyëüýá¢ëxù0ê5¸gûú‚µÛVb@:=a¨ä†R©€ÛnA L|C‹°ÌQäþ>_"_"—a†ðáZå¤|-ßõËá˜R‹¦~·¤€Ö0pª`/bn0:qèº?ÎIôðþÉ p;6‘`ø²}Ój˜ Zœ8—ßlZn{ Z+ávXH§EÃ¥ ]^G™Øtåß g.0Åm/îŠàŽ•ˬ×êV–]~¿Ã’·`IÎÇQBÄ@œJ§±ø(ø´´Ã¾`áXnG Lz­$uN î¹õ:<ÿÚQt\ôJRïŸ?g¹ÍMâ²`‡#IXJtËjÜ|ÿ™6 ‹[Cݸq#(ªðÏ\4Mƒ¦iŒŽŽNÿî÷ûEÕ%'ÑÆµX ±C¯wLpƒA#Êáaä—ìçÍ_>Œ=_xIRÑÃÁ ¢ eõÕèýÏAxp‰1iö?  >·­F£;¶Õá`›GPù¡;°î ·Ò »vÿ¦M’ö¯p™ñÀý V+ñÛýçE}fþÞá¬ñPöB Jwù{3ͧ1Øé~-·6Væo°+àXñN @Q…³C‚áp¦ÇÏP’,‡bÃn0¢Ñáš98a äuÂ;ˆÞAœóûˆÈA&â4qzÚb¦#È&wå’s€˜6°<.Í#βàÒé¢7\É¥x A&Q M}–\i„BBv“ „ìØà 97ÐCGò&x›Þ€«p¨ÿ"h‰äÜ#á$¾óËÓØÒ`Ç7÷4Iîö@'Shï ÁiÕaµÓ µJ±¤eÃ(8€?–LåÔÏ!šƒËZ\‚‡'_íÄS¯u‚@ M £hT*Ù.-6'v­^‹w/ž]Ç ¯…g8ˆo|y{ÁÚm·‘`8ŒEâEÿ(• ”—f¡ˆèP‰&,;-|˜r|P«²;f–õçæö ²~…tý#+±Ãú4pSøÈ$.^¹ŠXŽÇ‰sƒØ²¾–\g¤µ±n{ Þ?yQòe·½ Õåh¨.'M Èï¤8ŽƒåR±Âä3¥˜Œ­‹rs´óÊÌŒï]ã>¥ÓRÐk©iQ–Ûnùÿì½yx[å÷ýµµo–dK²#orœØqb’œB!KKB ´t†é3¥Ð¦Ë´ÏSڇμïlM;Ïl-o Ì<L™™„t†2ZL $iÉF !†ÄŽG‰mÙ±­hß¶#É’رutŽt$ߟëÊ%EÖ¹ÏÑ}Ÿå^~ßß2‰°hÎl‹ ²ÕU28}ìœEb4Ʀݨ®’³*ÜkmÐáÉo݇ƒ'Íxvÿ P‘ü×$ÿöGGðwßÛÉ(èz¹áDñÜ ï3ÚV£Ñ ¥…Û±=›Â†…(–h#²ûï…dâr›Ö2Ë<å¡àA]%åTôÀ„é÷9)÷¾{: PÌ­õljÊYðÓï ¢eÇæ%ÿìKo#âÍ<ú?~o#¾ô?nF÷Í €¯þQ£6ó]^zVm¦t4†É3¹;iU2îŒT" LK®P)˜ûWlWo Œ¡qúÏ]*™yŽlJ)(89”ašÆ‡Ö)|h¾þŒ˜f­¾ÝÆFÞÇF„é¹±AZÐá+1Œaš.«¶úÐm/…ÃÜKÜ…†„¥ñ€Çð6-!×.ÀœèáÞÕëppÌ o”½@Áf¾ðLÝÚ‚{ØW¢Û}Q¸‚1¬1VA§*ŸÅÃf½Ém›Q{›µŒ÷é ÇQ§)L6¾p;~ð&Iߊ@ 0cÊïÃJm D©ŒyxxÃ&œsÚqÉïe\FÚ¢¾¢‡ÆZ ”…è Z%dž 9) ÜÁ²Ø!û³lLJ¹Êô‚2×b„Bˆ* XþRÄ7Ü6‡÷=~àäõ¢‡§Z>…/_ü5£Sììèe¬_e„JN‚= ‰Q¯ÆÜ} Ì“ôLå%|‹h1Öà¦U+ S+HåEÆå§'2‹úiC).òsAv= ÛæýŽT,‚Q_•EÈ$"uUœ "âtb{}ª”RNi<0ü¡(êjTÐiØ ììiÇ–õ&<ù³ÃèœÈ«¬p8ާÿåþö{»2¼K…Yæ¹ä/v@ ÈllØÕÕÅÚqBØ0…mä]7‘xQæ‚ct«;çí:×Õ•Õ5ÂGÑÛô ÜwOGæÿ·nj„A¯€Ã™Û³dü·,Yð0õÞ &}˜×q+äbüíq[Mµüª6û»ïíÂý½˜SyÎ +^ù«Yðïz“1óÞgs1:æx4gÎÖ;¹©0{aTEÅaCEñÀœ³dªdEúZtèj±V_‡}-¯ÕáCëÞº8BD%Ž+LáøäŽOŽáyUl\Q®º4ª5œì3šH 9;¿],‘¸Ê®TÝòår8„ËáP)ênr <KÀf³ùêêêžÂ2wy±@€{ÛÖ¢oÊ‚1¯›µr©Xÿ÷àE¼kvâ;Û°ªŽ]…r29‹á)?jT¬ªSBZ ˆ&}îmŠæ71ë£JC{fÒ‹?xþpy)x BaIÍÎÂô£YC2-Äw¶lßÿæWyeL8tÒŒ±ižüÖ}ËjZn¢‡t€°ËG!•J‘“À.Š®s…ð…ÃÐH$0©TЈ%ì•_îb¶Ú2áÃ<¢‡Gk×c"æÇ÷/½ËèTë™Âö[V‘k®¤Ý,V¬.?&¬ž%‰Œº*ÔhhYQÍ}V90/V§.»k8«Ó„ÛOšÙ §3Žó‰"´*´UrÔëÕÐVÉóCØ=ÁÌ8…@ 6)Ä|[2•ÂŒÓO £¾Šµ}*eìþã»Yq{°LzñÒ«xì‘M9m‹.ÁÃа ?u€Ñ¶&“ Cî.>ŸEÁëõÂçó!DذlŠ6¸ F'àð'øíìèeÄéÜ™:<ð™´èá/v`|Íð™oýÉí׉Ãz65á7sËÄñø1õÞ o[ÃïM½7ˆßÈë˜åYb¨®–ãÄ{8{ÎŽ@ Šwß›€H$M³4뜰æ¼T)Çð;§`h1B®­‚B£‚{*wá̺•…UV¨äç9;6íš:ŒL•Ôµ¦“+ÐmlÌd×ç{f}è·NáØä>´–V]–ÎTÀ‡©€o˜‡! Ñ^c@»Î€öâæ\HJÈÝa‚´¡ÐÁ°tö€ç‚‡B¸<¤ÙÒØ­LŽ~–;²“>|å§àÁÍxdk ”RvoSî` ¾p&½ Õò’>!5ŠÜíTÇl!|¢]ÏxŸ1:‰(äµ`äõþi|ùÙ“Dì@ X!LÓÆbPIHÆéùÐË•øÎmÛñŽÌ«œñ7þÞb÷ß «9ör=ˆEX"z ž<ÅÙøb1œ‰Ç ‰Ð P¢N–î³Ï.ñ8rßç’ËÏ©>8,±úŸO °1Ãb¿åÚ÷·ú÷¯=ìnºGü—pÔŸ{f2ó¤ÝÄ塈´«Ñb¬Æíëç²Y¦3ÂHÂOEçÆ…ñd&k[0›[LÌqAQ¥^ÕÖF]Õ¼Ÿ„9\~ A*·ŸºJä@(Þ`Þ`$#ŠÈfe}MFüКõþFxa(dbTWÉIå®Âî Âæþ8‹3‰alzþDD6Ov÷3>×Ö¨PW­ºîóõYcñÖ†š‚%$ –ó5=6íº*€àbn“Ùsh ™­ :@] µY×y(R¸>C$FclÚ µR £NÍš{ÑΞv´Ö×`÷OÀžGfû7ÞÁ­›ѹvé¢þ@tÙœŸþýŒ¶‰Dèìì\ðï4MÃëõ^åÚ@Ó4|>~9´×××3mŠTjNo¨(s¿1:ó¤#çí zE&½ÜPWIñÏ?¾ëêð— ¯>òûŸß0ï=ò¾{:r<Àð+Q·± "™tÞ¿³%vøÃ‡oÁû§¦ðÒ+°Lzápò3+v4ƹçpîð•û§D :–»˜/ýÌ_îôLáÔ¹KóŽËùˆ\$B‡¾îŠÈ¡z¹²$ŽÛáØÄŽMŽ7‡eF,‘À ÝŠA»¡[›[±¹¾™T Ç”»ÃS¤µÅ€„%b³Ù&êêêöx„ÏÇ9-Œà:tµ¨U¨pdrT<ÎjÙû?˜ÂÛ—ñ]«q÷v-ù’ÉYŒÙBpcXc¬*Y·‡ÍÚÜÏcþ³>ŠF†Ÿu¶÷¨øÜIrÃ"¬b§‚Dðp£þ€¾_íÞ‚çûûò*‡ŠÄñÝgzñÙm7áá{º \ÑX«B&Æ”ÝWm! aÔWÁæ"‘H’““P@Ø;d/DÓ8ïób"D\†…ÂʹRÁ &¨À’Ë2©ª`R©npl³K;^Æî,–ígl‰*´0'z8¡â§÷Ú³ú^l<óoð'rª!.Å%F'àöQpùÆ£pû(±•WÉ%PÉ%¨Ñ( ’K¡SËQ£Q@""S™„òÇå§®\‹s¯VW€TJ‰1>ãÎ\Êú|e}ÍUŽ×©LÙ}ˆÆhÔÖ¨ ¨¬$Y®s YÁ΃YÁÍÙ΃£Vî`tÏßšÿãõ«æªÓ"ˆõ«PfV„ë›vÁî blÚ"Ù=Á¼õçc±{EmõœðA­”B*-Y„ÇþPþPÕUrõU¬<×Ztxöÿù<žxº7¯ÀƧÿåžúÇÏ\—½ü†c¥XIyEÞ¸5ä ßÝ\~ ±xqGÎŽ^f47г©©ìïù_û£|âÖf<üØË˜šñ—ôoÙ±µ_øÝ óþÍ W¢sm-†Îå–q:‰a`ïèþÚï^÷·áWÁòÎyseeÂá8þùù÷J²Î™ˆVÖ×¾Ö´ ½Ç†Jb¾¤C_‹]-ºhÖ”–lÒçÁ[£çq|r¬,ΑHæjgM¦BG‡ãj`8E•·$–Hàà˜6+îkïD­RE~q!à-…ÃîTjËzŽžÙ¥€7Æëdž0ƒ£Vܾ¾…\_,ãòS°:°:ýpû)n\ @œNÂê \^£ž ž2êª`2V\‹„’ÂŒÀŒ`xÜ– DÕªdhmÐÁ¨WcM³›Ö6A­”’ÊâiAÃØ´ T$ޱBá¹W*_võ‘¦îœÈ|¦‰±aµ­õ:¬_m,H"¡P„"1 ^´blÚ‹Ö’2׋ð¤bQF¸ÔZ_ê"™JÁæÂé¥X>¤EO<Ý˸ ~þê>¹­uÉ"†r<<ÿo'g¦Ÿ-`ãöövÞ[*5 ‡7T´ýî¿È(ABçÚÚ²¾n®E]%ÅßîÞ…¯>Öƒo~çuÆŽ)Åà3÷tà±G6-ú½OnkÅÏ_=‡3÷,æ¶ ° \ VÆ$n ÙÐÙZ·¬~wÿÈ^?:„hœæÕqÉE"Üblº"r¨…^^º÷½R:èõz( (  †Ì{>!‰–äáp82âˆl‡,š¦y®Ä ¼2|w¯î@÷ŠFrcf‘Ü%!4:j³ÙŽÖ" ²H äΗ‡yéÐÕ¢V¡BßÔ¼Ñ0ë希wÏ;ñùžF<ØÓ¥”½[˜;Ãû£.t6ª¡‘—N@ë†f &—œIÅòÏôœLÎÂŽó¦ž|á8þðÙ“ð‡i›Ð©$Bñ8R©ü±(*IQ®’ÌÿÜ  –G·›MÑ:iÆØ´ O~뾂ˆdƦÝH¦R%ß••¨«QÁá­(Zö/B™3ŸØášÛ¤-ΈÎx\60Áã¨Õz•B-c[}=¶Õç„z+õÃø3>‰0;÷¾)Ø$@PùÊS+ïÂö³ÿ™s˜'DðÀ1: «gNàô—uPµÛ?çTažt"ãüÐÞl€N­ '¡¨XþÌuHÜKÅŒ d ™Bï•Ïd¤b!Œz5>}[vÞÚN*ŠÒŽ i‡Û•×å*h`‰£opbNqÅ bý*#¶¬7Í !t¤’%ÅØ´ }ƒ81há•{ÛDã4†Çm™ŒÉiDgë ´Ö×@[%Ï{l vežxº—q»¼ôÊÀ¢ÙÌ3cO«[ùÕf—¦}˜šš[¼ý6S^å<÷ÂûËþZ‰DÅíÿÂ8xÒ<ÿ$(Ž#£Ýd<ŽQÈÅx镯ûV«¥PWå'ú¶áâ˜Ñ+ëãÆ*|ù‹Ýøügoâ¬Îš4èýïGpâ½ üãOŽòZø —‹ñ­?¹·nZz°êŽ­«ðóW@(â<ƒË½¿þR±­uèîh,ë>v$FãåC§yåêС¯E·±ºZ4kªK¾Ž'}ììçÐA¯×C«ÕB£Ñd^ˉ´(¢¾¾þªÏÓ"ŸÏŠÆr”À IDAT¢àõzyë ñöŸÃvµ®!7g(!w‡=¤µŤ¢\T÷B!©««;`+ŸQ+–`“¶xöQv+íÜe·QH„øÆ®Õ¸{à ÖË®¯–cU]i(¯wüàÉ-kê¿Ô…ÍÚ²©£o¿øžy«ð!öî)Í>Ü#>šÓ÷5 ºººòÚçáÇsú~³F‹‡×oPLü±ì¡«³&ÕW©QYÁÌ HPY‰™…rYˆžëïcMôwõ´ã»_Ü^cÄhL\ö0ÊZÅÛó9ËGñú­N?zç´Í<€Ï>ð@IµÅ/~ùKüò—¿Ìi›íÛ·/)ÌRxçwrž=¼ó³óÌ$×ó‡sÎ iaË÷â¦4«TØVoÄ+[ðÀÊ–Ü–‘Ø!M¬8¡½êðy¯»/æ\ÇÛoY…öfb‡œ+Áp «æIÜ~ŠT±H€c L+ªÑb¬&Bàœl‡‰ËR!ΨRHñ×´Í+ª¡”‹ó ]n\ëÒ0pÑŠP$VÖÌ|¢¶Z…õ«¸}½ [ˆÈ•ÀSú-818¾A ;]Á¨«B÷Ú&t®¬cEüÌÍ—ª•RÔV«  •ŠÄðõxvOÑö?ýçÏ-9ýšvjªåEo‹~þ=üè'GA…ɹÉ&=ô«å½üòˤRyLcƒGÞþJÞ‚Š¥pâ½ ¼ôÊïD;¶¶â±G6A¡È-Á!EÅñØ7_C˜Üƒx…V%C÷Ú&lêhdí9ͼ0öüꃢ'hRk¯88Ô¡ÛX>ÙìÃt/ô³ºžË”´+‚Á`€F£amM¬œÈ@8^‰ îZÙŽž†fÒHyrÔ6U ‚‡I›Íf"­E(&Äá@`Æn‡ù|€ÅryH³¡ÖˆÆ* gnT,öŽ`ïQ þìþ޼ø³™ñ„á DZÆXŪ‹l]kÈYð`óE<ûš>žLb=ç`Eì ” Š&@ Iį;LB"dö\H¦RpP!¸#a´Uë!+rÆ(®áÂéáK÷t£¶ZÅù±Ë$"´5é16íF$VÁÚj¥ •••p0\ü%®âJÀýUÎ A?Æ‚þeS“Á öž7cïù¹Ì{÷¯lÁ+Mx´cÍò;T¦€æ0!Ë|üTË]Œ«‡–9ܘ8„yÒó¤##~¸iÕ âü@`õ´:ý°X=°ºüe%š%ð›ÅŸþÓØý•»!“ˆ “ˆ ”‰Q¥”rîŽÇwÒ. Ô•×´KƒÝdK`»'ˆC'Í8tÒ …LŒ-ë[ˆøÀ ž4£oÐ2çNB¸«+€ÞcCè=6Äšø!™JÁãºJm•,çg˜R&Áî?Þ…¯ÿ㫌Ž!—7\tÁÃï<ø8yjŠœBžLMûpËíÿ„OüOÎE·ßfÂí·™ð·»wá¥WðÒ+g0|®xYÔwlmÅ~wÃ’Å^×¢PˆñØ#›ð̳'ȉÄ#¼ÁH¦½neº;šÐÙZWÚ}§ÏîïC4^øu:\}Ý‘C-ôreÙ3oŽ`ÿ¹„éâ­ƒêõz 444”{h4h4š«!|>_FáóùàóùŠrl¿±\À†ºzH…$ ™)%äî°›´¡Ø; À›Ív¤®®î(xîò0FùQ-.^HµLŽ{ÛÖrêö`÷GñOcC³lmaMø@E83éŪZê4RÞ¶±F‘{À¬Ýe¥~¢tR†YwØâû¯å]Æÿút&|9 GBùâF9+;™Ja*àC[¾ìë‘mÑÃÀE+vö´äØ••hkÒcÊîƒ'.‹öPÉ%‹°:H¥RäB'äÌ g¼ÎŒ‹C¹º70áõq ^·àñc'ð@k íhǶãÜ—‹Ø!Íê00-sš¤jÜ_³:gÑÃÄe‚áTr 9Áæ!F'0aõ`pô29ä@¶øA%—à¦UF´«ÉyFÈ«ÓËe¬Î¹ E%5;‹úïãøÓ‡w £‰Ñp^qvS_>T)¤Œ³fó»'[ZÀàfÄ¥¡ô "ñL`Vmµ ;{Ú±óÖö‚$; €¹¹¦´Ð89äÐÊ?¬¬¯Á¦µMèîÈ/ÛqZø ”‰¡­’£:!EkƒºûÞêÏy¿ï[rà¯ÃBSƒ 7á‡ÛFÀ?77-‘ ¡®’¢Z+‡PX‰?øòKDì@ °ˆÏÁ7¿ó:ö½ðPAö§®’âkÔƒ¯ýQ.Mûðæ3Þí³àíC8ßwK³;¶­Â'·¶æìè0ŸÜÖŠwŽŽb¨ˆÂ  Û0ÂDàqfÒ‹GÆ,E£NS¼Ó£çy‰Ôr~ñ¿ïÀÖµìøÁ;äŽF 2Ð$œ5¾Ö½kõµx¾¿/ï²ìîÂgâl¬Õ@!cÊî+‹öˆ„0ê«`s‘HìÄ¥ñØ{ï,+÷†|ðÇãØ;bÆÞ3šU*ì¾µ´š ‘dÇÊYì~ßÌ§ãÆøŒ¯¦µ¸cãʼ\B‘8B‘8¬Îª«dÐi”Kî=|O7N Z ß~{dNô°¤1ÙåZLìAúQLMùà\“„'0'²+$pà·É G °Ìß\(Ê~›4ñC"‘¾|çÎ;`™ðÀá å-$hiÖ¢ÅTεu¸i]-c7‡ñØ#›ðçß?ˆp˜ôÏùŠ7ÁˇNC*Ꭾ•¸cãÊ’>Db4^>tšs±C“Z‹µúZt›Ð¡¯]çD1\Ò"‡†††«œ ÜÕ·Á`€Áðqbä´øk„ÅçF4‘ . (!w‡§Hkø¹Ë ±Ùl{êêêvhæóq^ ‹.x¥X‚­íóºpÊ::ÉÍâðË80p™UáƒÝE(š@g£ºèŽײÁ”»µ›ÍÇNærW0VT÷‹§Þ23¯·f ^ûßwÀ¤W›@¸AEżŸW.ð9áÆÜÙÜ ½\‰¿w¸¨Ö¨L©®’C&aâ²§,‚Û$"! X]~Äâ r‚…ˆ˜1 âˇãñcb<ÚÑŽÇ»Öäž'SíRóýbhŽc²ŒËÃ6u6(  r+OXÝDðp󤃸9p\¿iׇîŽF´7H¥ Ç`±z0auÃê ,»ß¯×Ï9Ò‰Åbh4Ï=e/˜* (ü™Wñù|ˆÇãóþ?[@áóù@ÓååTurøØÚ¹àß#1Sv¬ÎJ¨•RÔV«xåúŠÄÐ78‹V ^´’dú'Ð78Új¾§»`n„ò&íæp褙TDã4ŽŸÇñ3ãXY_ƒ=íhmÐ1./™JÁé£àôQKv}øúƒ·ã»Ïô漯wŽŽæ$x0蕬dHO$R˜šöÁzùÆ}Íd"…oýéä$[„¦ÆFèôz455áÝãÇárÏõiff†4H‰JÍâµÞ!|î¾Î¢ƒPX‰?x¨ Cçl ¨Ç3‡er.øÑî àp†,£ÅT ¥Bƒ^ɉ¸a¡}>öÈ&<óì r"•À3úÐI3ŽŸ/ áÞ_}ÀÙ<Ì-ÆFtÑ¡¯…^®\6ç@˜ŽãÇï)˜«9ð‹k333„ÏÇ^¿Ôì,í3Ø\ßL*=G†¼ÎR8L?æƒE‡„üØ à?ø|€ŽX‘d2?.÷V­UZô[/aÌËÝdÓËx÷¼Ÿïiă=PJóûýT4þq:ÕÐÈżi_\ µ\xé µv?;‚w0†DrBAá€'œzû™MnhÖà¿ÞÁ«v$üB!#¿>+HÀN@ˆ|ZtvèkñwŸº?î;‚K~/³>DCMÑŽ_&¡­I)»þP´äÛ£²²F6w‘M.zCü±8ž>sOŸ9‹GÖ¶c÷­Ý0U©–^@¶»ÃBƒù>+–Ø!ý¾1 Xd™7n—/þ:§º³ºˆÑ HDËsê*F'pvô2G­$›|†c8üá(úG¦ÐÞlÀM«V,Ûóo¹’vr0O:ÊZä3¤;Ó® |0äJ¶0#û÷݇cNŒ—G¤…4M³ºèË5—]K¨&S)xaxaTWÉ‹.|°{‚xñÍ~ô ZˆsaÁsäÉŸƾ7û‰ðÀ˜ƒ'Íxíð £ÌÿfŒÏ¸ñÜk}ЪdØyëtw4æU^¶ëƒZ)…N£˜7P³­IÖƦ]9•ïpRxÿÔnÝ´´ã¼8æÂÆõƼ~EÅ1bv [<ÉÐ9f¬$EšlaCSSô:ššš®úÎùóç‹*x8}ú4i¨Ây!A¡ +ѹ¶î*уB!FçÚ:@çZ~ÖÝ'·µÂ2éÁoŽ”å¹!‰®g¦)µ1#P‡ƒ'ͬ÷Ù:ôµ¸³¹ÝÆFÈEË/>dÒçÁÿ9v° éêëëQ__–âÔÇgÒí4×'¥2⇙™™¼„ جDð#ñTÆN˜§l6›´U;!JÅåaŒ  ³ªš7Ç#°¥±­ZNY§à†9ÙK`ï1 ^=9ÅŠð!™œÅÀ„­uJ4TËySŸ›µ8:’[¶Ô3“^llÖæ½ïb¹<¼Þ?Íh»f‚ˆ%5M&` --ÐF&¡V¡*‹ßŠÇ1ð™{>h¤2ܼ¢uʪ‚ì_-‘Á‰ –åF$fAì °BUµD w$ :™D ™÷»"UÙ•ã‘@X)(ùvÕË•øûOÝ‹ýçðÚÈ`NÛ*dblY_Ü 2Ae%L+ªá „auL¥Jº=*++`Ô«áð†¤¢ ܳ÷œ{Ï™?>,æø-v¸áwxÈê00!Ëþ5m9 Àê  ÅX½¬Î“`8†þ‘)˜'ä¢)r ŽZ±~•‘–I›ŽZažt”ÀH¯×g iAÃRËt,T74MÃëõf^Ó‚¾¹Dˆ„¹=0ü¡(êjTÐi +tI H–uB.ç >r!‰áàûfüâÈYâSD¼Á^>tß?ÏŠð![¸'  ”I2½8„'Æ]›Ûr<À;GF—,x ¨8.޹°º•™ƒÅ¥i¦¦–+ôô¿,ÏìéK6ð‘¡¡¡Œ³ Ba%6®7Â2áYÔù…O<öÈ&PTï+‰ãM‹ÒãålWÃôØ™)iA}zü˜þŒoâˆk…|é[[~ÖÆˆr‘·›ðàÚõËÊÉáZŽMŽáùþ>N÷¡P(`2™ÐÒÒRÒI4–+ …---‘ŠËåÂáÇ‘b¸n§Ø{ù¢\p;0áó"–à~þM"Á¤Ñ¢­ÆTVÐ68ëq"žâý|´ÀSäŠ!ð¦ßLª€@È›Ýà¹Ëƒ5B¡UQÅ—‡4µJîm[‹—v+è$7ñláÃ7v­ÆÝVäUÞ˜-„P45Æ*^Ôã“&gÁƒÍeE¦ã Ç‹"x8rŽYðÏkO|‚ˆ¼$š Ño‚-€;†3"•r ]žFR…/®ï†TÈ}Æ‘µX ¡x P%‘B)–,:ø~gü.>Vá+„b«ÔÐÊäˆ&hDhÑD‚Ñ1‰*+! ª@(¨„D ‚ ²•ÜG¼†é8&}Þ«ÎÏj™÷µ¯C׊†%—óàÚ X«¯Ãsý'à /mñç»_Ü΋ó°÷øÞ=3«3ˆd*µRŠz½š×ö¿K!£¯rzˆÓ 8}Bá+ç¿B‚•õ:¬[YGnF ¤…ß»µß|4’+Ï—‰*°Àw ñ~–Ù¶ú8à˜ë{k„RÜ_³¯»/æ6–uù—࡜„×fÀs:%ù;ât2#|èîhÂúU+@(/bt}ƒ%{Ý¥¯5­V …B‘y/Z†Žr\ÖqZ ‘Î~—Ãá¸J‘~-471¼?%S)Ì8ýð‡"0«!¨¬äüXž4ãÙý'ˆ£Ù‡'¾¸VI¥®"‰á‡Ïâµ#ƒä>Ã#Ø>¤ûêúúdj­ :¬¬¯É9;ôÉþ)PT ÅÒÖ޹ùÑ–æj…K{~úQX&<™ìíK¡÷Í8œå<¿fÍèt:èt:t¬Y¹\^†ù ( .\ }‰‘vQà -¦jôJX&<ðØKT$VB)C ¬\ð^ðG%–ä>“Í·þäv¬nÕáù?É«ºÔëõÐjµ×I²ËŸoü˜3z½^8Ž¢ éÓ‡þs—X{>çÃëdžX)çsëñéÕËÒÍ!›çúûp|rŒÓ뫽½}ÞsPºŒ1;dÆí¡ j•Ì“tØ­xÃÓÞ‰ µ…™kˆ§’òºJáÔ î^ABþüsJ65¯;&Ÿ/ó®hkÔçµ}(ÇØ´µNÅÜÏî?_9KNœ%p­xðÚsm¡çov†Ú…ðù|ˆÇ¯¶M?‹¯}FóÍÍ$ÝÄwŸéÅúUF|÷áí¨­V‘“f™S*B½^Õ5}-3y¶g_Ï×ö©ù–M¸Zøpß7¡³•› ã;6¶2š¯{ÿÔ>¹­uÉßw8Bðû£hjÔÀ Ÿ?{t"‘‚ÇžûnŽËÇK¯ ”Å5ÚÔØ¹B5W ÍW\äryYÝ‹†††xùÜ$” …ëê‹%àö„áñ„—tI‹$R!$!r1„ÂJ¨«–˜¼°áãûÂÝB2±xàkï›#E¿_)ŠŒÀÁ`0,Ú'.é1}v€8EQñCúµXÏçSç.áþ;;aÔ>ì©d*ïµ¶}-¾Ú½eY;:sÉëžëïÇÖ)NÊ7™Lèìì$sReˆÏçÃÄÄDÞåDópd0»E;dó†yR¡í5ܯ”ˆ»ì!WOÁ'6›ÍWWW÷€ïñù8ùêòF,`“±:ú¦&X³ºš»?Š¿þï³ØÐ¬Á7v¶aU³AM ܃ÎF5”ÒâÕ+“ þQ;;õ›LÎÂŒA§’ì÷N8)øÃ¹w’¿÷ùNrÃ",Š3‚7T^Éœ/Š ‹ !¨`/ÛáŸÁ·ƒTz΃äþëì‡øÃ®[ ¶O‘@°¨ØáÕs˺].¸øþ‘9‰ä"1\»wšZ±ÿÜàu™FÖ¯2âá{º‹ž±ñìØe|÷™^¤R³äú‹%Ð{l÷Ý¹ŽˆJ𦠕®ƒB¡¸j!|)AÖÙ[?¶ü±8¾|ð0öœ3ã©í[°Ñ Ã‚b®ß_÷Ùl~åU'€ÊY 5÷á6uîsn?…€DT~ÓW1:³£—18jåMÀu:ã]!ª³ Á·ÌwÁp ‡?…yÒ-Z S“…½R$ŽápÿEX]üT]+n ® ¥Ç|,2÷²ô½ ±PmUþÁ‚‘±i7Zj8=üèg‡qè¤yÙŸé`çôu-P(”‰Ið×µN&|(ŽZñµxŸÛ¶ßÓMn>Ë”ƒ'ÍØ÷f?ìž ¯®õB80-åzN_¿iW¢´¯˜xƒìýõXY_ÃI`egk´*¼ÁHNÛúè#VÊQ_I&Éè^0Æy£ƒcfÎ%äî°×f³‘N.WÁÀOxÄå!o”b v¶¶Ã âÄ´Tœ»É¼I¾òÓðÈ-x°§‘‘h!F'qfÒ‹ÍÚ¢‰6˜4Œ~;[Zð0ÉÀ¾wC³†‘ayáE2bH¥fIшÐ4ÜW>–…‰D‹ÄU rÞ-ÀKg?E[u¦Xƒøbh$²‚ìO'_øþ¦ãØ¿ˆCÁrá‚ÛŸÆïwvå´^®Ä7>q;þꉻ06íB(/ºÈ!›¼p€ˆ²˜ÅÛ}çñåÏl&•Qb˜ÍfÌÎò÷\N ÒÐ×¾²I:à#;ð£Ø[G§­èÚ÷*¾wÛ-ؽåJ°V!Å× fÙ)OGg\LR56(  r|º}TQ2‹q 2˧3G ÞT/”ù.õ®X™ï¬®^ýínZµÝe)È)Ûq„Ó·ß?Ï;W‡k³P’Åãò%}ÏÍfjj }}}ŒËl¨eï™L¥0e÷¡µ¡‚Jö><ýócËBìvDJ?SÓ†rpdIÿ†kÏßt&ùô3™k7“yûö‘8ö½Õƒ|÷‹ÛÑÚ #7›eÂÀE+žÝ¢èΛ׊ù–Qz¡ë7ÝŸ.¦°˜ËÀÊÎÖ8~f<§mΞcî@ŸL¤à¹’… ,¼stŒWç’®¦k:: Óéбf ”¥KS†††H%8''·–ö×Ô AV޳çl·ŠŠã…½§ ~Ÿ‰D¨¯¯GCCÃuâòrA$¡¥¥%#‚ðù|°X,˜™™)X?ûø™q ]ÆCwu¤o=6íÊY¤˜F.á/ï܉fMõ²¿?p%v B‡åÁÌÌ +I9@ÃPð0éóÀð¢>üÑ&}Nï-%äî°›\!Þõ‰IùC\اV©Âç֬ǘׅSÖ)ÐIîô{YðöÀeüÙýŒÜ’ÉY|8îA»± uiÁëJ#£Y§À¤+·Aî¨-ÄØÝ"»/ŠUµ*ù½ ÷w7aQ–"°Š%“ˆ%“Äb‘ J±2¡JñâŸû Þ0“Êf¬ÓØÑ²šóýTVT@%YøÞþ£‡y@\hŽX.æ,xIÝ\ß„o ¯þvþP”4ìµ÷B:Kv/šjµ¤2JåGQæÇó';Èz¡À ®Y(èìÚ€-‡ÃQð ï¿÷!~9:=ŸÞކš¹ .|¸æ¹–Oy+#Á0çò«àÁê ”àÁêôãÄàÜþâˆkôz= xˆ•ëõ›^Ø.f滳£—ažt`û-«Ñb$ «|Ç<éÀáGyõœ/ø°ü¸xñb^Û³=†ŠÄhØÝAÖž½oõà×'ΕE[]+h¸öu9’vlʾ—¥Å…~6ϸñõ|_ûÜ|nûzrs)cìž žÝ}ƒEëf÷«Kõú¿öÚõù|˜žžÆÌÌLÁEÅÇÏŒãÔ¹)ØýQ|çÅÓxps#ÙÚÂÈ­Ál @QDLšÜö +‚`Îå¡P¿›‰àA£Xž‹Ž„ÜH2Z§“Ĥ” IDAT)x#xAee”"1d"Ôó8ôš‡0h·’Šf‰™@aÞô %ó º&}\p;HcdŒÇrÞ¦BXyÿîÓ¡H {~ýiÔ8;z™J¶&+ÝG¯‚¤3]ò9Ð|[Åúpº±íå^ìÞÒÇ»oʺa¢ïg¯ÿ<Ÿò´‰¹×+ÅnTäØ ÇJþŒÑ ô NÀ<é(ø9]___òÁX‹ýÆbf¾‹ÓIxÿ} oºPýÕ××£¾¾>Ó§$\-~˜™™ÉÌqI!܆ÆlŒ¶ûj÷íDìöÅ"‘]]]äÚ[F˜ÍfÖæÑ¥B27¾¤1qw ò‚Üi– .Ü!°¡Öˆ]-ú­—0æå΢xÿSx×ìÄßüÞzFb³5€P4Áš`©llÖ¢·?·í˜-l`gÿ…<0­a1”b "ydÄH¥fˆÅˆÅà SÐJdÐÈdTT±C‰"P-[xqmß`?©$5Q!¬àÝqýâðYÄé$i H$S¤J„tfÕBÈ8”:Å úðÇâøöá>™²bÏ=Û¡‘~ì’PP±C eË’@x.`r£²6çºR¥í´cžtàÄ ¥`Ï’ù®8™ï&.{ðêo°ý–UeãHR.¸üN Z ~¦ÅFÄÁ°leÐ3êª89¾…N“_ÿ_=÷ï ärùuî äZ-Lš¢¨Lp—⇾Á |ý^Åî?ÞÅ;'I3.ZñìþŸqôÜmiiA}}ý²=e‹Š -~ŸqãÇÿu÷oíDwGc^eu¶®ÈÙåaèœ ¬-œ1äßö2KÀ¢P(°k×® Ý8óý÷÷zˆÜ„n4æÏ#Hð¶Ûn#}†PEÄäÍçñÍ.@Û¤$ …&“ ---Dp¼iAEQ°X,¸págs_i·‡»zÚ±³§µrÅ",V7¢ñÜûŽæVtɉàÇïaMìP__žžžeë`¸\Ÿ§.°'dËG„T«¬âUÝpu<ÄÝ@È"x Øex.xJÏå!3è°¥±ëkè·NaŠ£ìÞv_ùéxäÎ<²5wåòŒ'ŒD*…5ÆÂuȶ­5àûsÛfÔdmÿî` ‰ä,„‚ r ”,Z© ¡x,/ÑCšTjîHÞX6+Fœ6RÁ,Ó^ÃýB€Qµð}üØäFœvÒy"òÓÝÁî bß[DÐr#Beq}¹À¥»CÚ½!-p(÷‰ðì t°Ö… 8Í"ÿúè¶ý¼{îÙŽ†š¹<Ìó>Ÿ2TY‚Eî‚«+P’çK0Ãáþ‹;~½^–––²ur`B¶ø!õŽË ­`8†ÞãÃèîhÌ;8‹À1:ïç\p´ÜTìÃV=®DV¡H,/ÁÃØ´ Ãã…Ÿ ‰DÐh4—†kÝÅE¡P ­­ mmmðù|0›Í˜™™á$8Ëî â‰gzñõog50‹P็H ûÞìÇ/Žœ-Ø9J-Ó4 ªärÓxùÐéLFi™„YßjÝÊ:‚‡âÎ÷¾ôÊNfuÛÕÕµh?4W±Ãš5kÈEpò LÏ©˜QL7Ûå„?Å_ì>€Ÿ¿:Àé~4 ÚÚÚHFy†}—ÎÎNtvvÂb±`xx˜³gô¡“fŒM»ðè½›?›@­”B«’#™JáÀ{çsÞ^.áKºIãx®¿•µjâê°|bu<®–Êo+ ±¹¾ Ì\*z½l®oâÌ­¢„Üö+„ÀWˆà@`›Í6QWW·À#|>ÎRtyÈF)–`›iì¡ ìVØ© 'ûÙ{Ì‚3“^üÍï­‡Rš[]Ù}sYH %zØ`Ê={îÀ$»‚›?‚†j9¹JšÆ* b‰ñ(ü±(R©ü2š]v"và€ŠŠ lnhæt*‰r‘xÞ¿…é8öŸ ÁF=¯‘ðÒÝáGû“ÆYAe%©„€ÍÌ© …z½Z­ƒ¡,Üò­t°–Ãá€Åbá,³Ü€Ãm/õbÏïlÇ«MW†ÙF6ÞÏ.þ¦eW'ûÇÏÔ­ê&õ_*ëó£P®éࣶ¶6µÙb¥ .pêúÐ?2«Ó]·­DD¦^‹IÿÈ‚‰4Ó. Ëþ™Hȶ2èqåîÉ<ÝžÝßÇÙµ'‰2†ô¿´°P:h4ôôôd²ÇsœEEâxòg‡awñð=$(ªÔ¸hk?Oó}¥E$øxññG¡ÆÁ0ñ 2·¼LŸ§l÷å×ékѬт¢ã “IÐÉ$Â9ô•îlž‹œdɵ„ Íj-îl^ÅIÙ%äîpÔf³!W ¯U7}vƒç‚ t]²©Uª°SÙ{(ˆSÖ)x£aÖ÷10éÞéÃO¾t3VÕ)sÚ¶¢\Œf“®Ü…ÎLz±±YËÊ1Ø|QÞ ÎLz±u-™,!, ‰P½P ½\‰P<†P<†@,÷ 'ÂéËdÒ• 6Ô9-¿²¢+T OÔ½uq®0E"Od "ˆ4Þ×ÀE+G­¤A*!CI¾CÓt^ÙÎÒ™æÒ’­zaÒ.œRûcq|öµøÉ'·àñM7}ü‡B‰ò¡%Œä7N°:ýœe°f“ÀáþQL\öp;þ#™ï£P(ÐÕÕ•É|ÇUvZ«+€W;€]·­NM ‹A0ÃÙÑˬ–™vq¨¯¯' Á„¼È'8..ŸJ¹„ñ¶}ƒ–¼ÇT&ÓÇR(DÌPÆdgç*8kß[ý°y‚øî·“ /ö½ÙϹóf:xŸ¸9ä?N÷«¹{ƒü䥣¸ïÎNܱqeÎÛ¯¬¯ÁøŒ;§mÆ'¼E<¼°÷Âá8£mo¾ùfrR˜|‚îIÒ‚üûÒnyîßNâ/¿€³ò‰Ð[Ò}k‡ÃÓ§O³–)M4Nã'/ÅCwu-èp*“ˆ ”‰¡I VJ3ŸGb4ƦÝH¦æöÞ@îñ=w¯îXöm<â´cß@þ}e“É„›o¾™¬ý,S†††X/³µZ¹H|()9;‹X‚F<™JfÄt*:ùqÒ(©Pˆ‡7l€݊¦'9K~<µ 674s‡RBî»ÉBà3$J…@`âòPxj•*ÜÛ¶c^ìVPñ8«åS±¾ýâGøÆ®Õ¸{Êœ¶-¤èaƒI“³àaÌbMð@EˆÒIHEܯndàfá£hLPŠ%PŠ%Ð+RáCd 7t2‰w/‘ ä…X‚{ÛÖqº£J AÅüžÎpoކÈw¢¬„r?3°<ù3âî°<0©žc6›lß¾,81yFeR›ÍfN>¾ýÛ>œq¸°çw¶Vìïûй]À¶2ux°:ý8üá(gÙä² Ì&ÙÙi- 'Y¥ƒáz áöõ-ho&mVhN XX»·§ƒ!É0 ØÌHË¥àA!c>^ËÇÝA¯×£§§‡.S²ƒ³>øàVŸÍ‡Nš_p ”2 ©lžb÷ñ½};çõ\ŸíëÖ­#âaë³³³íí휃 ÷جN?º«+§íZt9ŸO– OÁëqèœ ïe¶ž`2™ˆ0°0 º‰Dèìì$È6Ýl ×ãDñÿþõÛøï×9)ŸÌkƒÁ€]»vq6ïõò¡Ó°:ý¸ïÎN(ebÈ$"(d(åây“©¦ì¾ŒØ˜6æ‚V&ƒT°¼çfœá~ü^þë˜ëÖ­#Ï£eþ**¡–È –È@§’ÅãˆÐq„X˜5aC`/tvqZ~µL•dá…ïýçs²^$\O…°šR^Ûk‡a÷I#-8$•Àc(ŠÂðð0£mÓÙ« ÌI/^···chh.\`µü½gçÊ{êS·C#ó_ìÂY€fnãù=§d ý#Sœ•o2™ÐÒÒB®MŽÈÎ*ÍöpœNf„0 e½#°ÍÌÛi…b¸‚ÍŒ´F7sJ™˜q@ø¾7û©êêê°uëVr’`0pï½÷Âb±°æˆ̉Ʀ]xò[÷Ñé´àG?; *ç¤|"t(Ü8˜+áCÿȬN?¾öàíI–ìØZ_ƒC9î§‚‡—^d\ïÄÝ¡ðätßÙÙI„Ô É×ͶØ$)Pá8(*ŽD2…€?šù[(G2‘šçÙ%†P08.‘ !‘!•̽ª«Ø]c¶áѯ½‚ ÖÔ5 ºººÈøºH¤ç½.\¸€¡¡!VŸÏÇÏŒC*-ITìòQˆÄòÛw•XŠé€•PI$P‰¥7\Ó-GžïïË{zóæÍ¤O¼ÌŸ§L×oD³†yìaZàjÑÄŒÃÃôÇñd±DÑh"ÔìlQê¸;ìA—‡âÒªÕ¡±J‹—#.;«Â‡öŽÀî‹â‘­¹ !z`âÔpf‚Ý,®`ŒsÁC³>÷ o$Û=D•h¥2h¥2@(C$A##–Lâ¢Çbe_ƒõõõKÊÐDÓ4¼^þ‹{(ŠÂÄÄ£m×êP§äî>* Q«T-ø÷§Ç'‰sG>¤Å Þ[(þ·úI#år͈…ˆÅ¤"xH>‹]]]¤Ùê3ˆDèêêB[[Nž<ÉjÆœ½g/àŒÝ#p4óe`æ“Ø*g3˜$¹g£vû)´ù'ÖÑ îÍ;°z!HÀuaI/§…Jl.÷L!Žaû-«HE€£æuïîééA}}=©Hë°‘– ‡™DÃgn(ÃkG˜l lÙ²…œ$„ëžÍ 0›Í¬dŒÏ¸ñÄÓ½DôÀ3žÝ¿8r–“²‰Ð¡ðã`.…VW?ù¯#xôÞÍKz2yVZ œ@ëÝ÷&04lc´m[[ ž/0ùݧÝãÌÈÇͶPTþ@4óo>AÃRÊȼ¿‡e҃εu™Ï$! 1ÔUÒÌ+žÿ·÷ñWs©»A é¹Qò æi÷Ê>úˆñ:í|,UȚkxjvþhþh•ÐHePKe Ë;rÿ¹Œ8íy•AijÙ̺ã 4«µEù=׺JD „é8ÂtT<^4C6ÄÝ@`"x ¸c7ˆËCÑ ØPkD‡®ýÖKó²g¼÷˜‰„••œ ¶®Í=†Š%`óEQ§a' DŒNÂŒA§ân±ÈÄ@ð0é¢à ǹC‹¡K K9à‹Eðº™Å9½^íÛ·ç´M)å¼óÎ;Œ¶“…øôªÎŽ«²¢U7–ì`Öá qÛm·AÈó‰¶Ó§Osn­Z#PYÉËß¿ïÍ~ÆY»ºº fŸL&¯û7›ÃdM<g-P¤²¢*…A*J<Âáp`ff†Ñ¶ëÖ­ƒB¡ •È2 …;vìÀÌÌ NŸ>ÍÚdò€ÃmÿÙ‹#_¼ïz§‡4|;TP¦€ØÜýß$Õ”E»ºü¼wÁpŒõ²‰Ð¡¸påÐbžt HE±ë¶5ˆÈ”,WX~xƒFÛj4ìØ±ƒ8팴Z•lÉÙ­—<¿!Ãd¬† ’Ù˜íÙý}ŒÇT===äÚ#ÌK:xº¡¡'OždeÞb|Æg÷÷á»_ÜN*¸È„"1<ñt/Ægܬ—M„ü¸vÛÛÛY¬ô#xv¾þà–E 2‰Z•,çþ¡eƒ÷k§­ ?e€ñ9ÞÙÙIN¶“OÐýæÍ›I2$7ÛBâö„áñ†áö„s8 ³a| ˄gèÊ¿Åç[šµP(Äh1U£ue ÖwÖáÖMMPWI!‘,>÷ðøŸ¾Ÿýœ}çŒuëÖ¡½½ôñyø|îééAKK «k‚‹‰Š#1š×ðhâú\©ÙYx"ax"aH„ÂŒøAPQQVm7éó൑Á¼Ê b—ÏS½BY”ß”œE0E˜Ž#‹ñBàp-ÄÝ@`²ºF pqyàb[[ЪÕáÄ´Tœ;ä½Ç,¨ÕHq÷†9m7ã C)²&0¸–­qä´Í™I/îÖ¬`ïÜ÷E9<0ýGÏ9pw¹98å ób v²—£µÅbaœÝzs}¤Bî&F*5DÁ‚?69Æ8kÆM7Ý£ÑÈë¶™™™)ˆØA¢ðò÷Û=AÆ™M&SÑ3sÍÎÎ"‘H •J!‘Hdþ óy6ƒ¬"1­W–‰è?0mc‘H„öövRR__ƒÁÀjõ€Ãm?ëÅ‘‡¯ˆ€E„E;”!«‡?¼ÈÊB]6$(‹?dg!d+¸˜ËHÛ{l÷Ý¹Žˆ8¢dŠÑvDì@àš¡¡!V3Òj«ä¬•% P[­BueŽM»p褙ÙxÜhDcc#9I‹Þ§wíÚ…¡¡!V4Òç+=±ižx¦—±PêFý¸¶¶6dÉ£~uOOOFøÀ–óa4NãÙý}¸k'º;nü 1êÕ9 ¨pœÓzQ(ÄXݪÃÿý×÷05ãgTqè,<ù ¦ç„Ì8yò$o-KÀz9»3”“ÈÁ2áÁoŽahØ–—³LzÛ¡sW¯_õt7âæ®zÜÿ;kqÓººyÅ¿|c˜u±ƒ^¯ÇÍ7ß\°Qf VûÖÀEÉTŠ•}Ø© |Ñ4RÙü×c"{({(•DµD•¤<\ÝžëïËk{"v ì®_K³ºp‰–Ó"‡P<†`,Æë:'îûT’* 8å©R8È1*Pö Q«TáskÖc}-{§?ìÁ»æÜ'gÍÖl>n‚™¸< L²àêÆ¥¹U§n0å>Ir䜧#€ÉQLúر»6™Le7HÓ4ãI3µDŠ­Í«8;¶j™ü†^a:Ž}§•]*6Õ\Np€Ü$‚´Ž¿A}?Úw˜ÑvéluŦ¢¢"‘‰ …J¥:ƒƒ³³³œ[ Z% Õ*ò à‹…qwuu‘`¢Þ¾};kõ=ø¢ñEÄE;”™èapô2¼žu±Ãºuë°k×.²ðÄ3ÒÁ•ëÖ­coÌì§Ð{l1:A*˜eÌ“X]Ìæ¸Hvy—PŪc ´6èò.C­”´¢¦Ú¼ÄÀœ»Ӿѭ·ÞJNÂ’éììÄöíÛYq¦;tÒŒg÷Ÿ •Zž4s"v0™Lصk:;;És‡ýê;v°:÷ÓxùÐéE¯‹¹@ÌÇÙa;'õ V¢ÅTëp{Ãxî…÷•£×ë¹=çzï<þ<9y³ÈgT˜ãp8XK±‰?Åа ýMÃz9°$±ƒeƒöžÂs?ÿ³_á7Gò;܈“ýSxö§ïãîþŸøÔ³øþßÿæº}}ûÏ~ÅÚþÒsž;vì b‡ë[ïÚµ‹µ6K‹B‘¥ ¯¬¯Éyƒvë’¾ŒÅ0ðÁìrÀ ÎëQ*¼=:‚K~æ÷ŠtRyž2u‡_ Í-ç¿!šHÀôcÔíÄå`€÷b ¤Üž"W ¡T ‚Cl6ÛGù~œÖ…Hry,´o¨5âwV¯…VÊN´|}£¶PÎÛ™­„¢ì×ù6‚‡wϳ?IÄ• #ÍÆæÜ;˯ŸšÀÙý>ÀñÉqVʉDeéî`6›AQ£mïkç. \.¡Vyã@í·.Ž \Æ6ÕCCCŒÛf)Hë„P˜Ä¼ýý}ƒ ŽZmÛÖÖÆJE¡8uê«åEã_*¹†j*+ɳXÐ4ÍxáU£Ñ ïc0ð™Ï|†Q€ÂÿÏÞ»‡·uÞwž_8 ð^@Ri’¢LZ&k™¶L&Ž.•ËîŲ§Í6Ït6q’Ù}&ÝîLÒìLwžfg*÷ÙIó¤I“§v§éÔ¶²][r­¤¶E%ÖÍRL™E Š7ˆ€Ä ÷xÇyÏ9ðû<- Â9|Ï{Îy¯¿ïﻃÎy|ó\R°ÖáÁ:_;0qþ“1\š”ôœF£¿ó;¿CAYYŽÔÀ$z­Fò’b”•c`”mÞ_ZZJ„¬È‘‘¶¾:õàMUa!JŠ5(/)B]U)Zª°ïÁz4ïªDy‰xZ±s*êÿ–±ôÓO? £Ñ(ú\o_¸‰^Fw‚Óç‡ðÝŸœ—Tì ×ëqäÈôôô(j&ikk“ìýM°“èEÔ'µÃƒJ]ˆÆFöw5 ~Wà;ß»ˆE[VOOÓq:ŽBFÄ vttPÛ$‚k×®eUyBó°ÞÅÔöÃyaß~ñìªÈÁ5Hk™'­nüð¥«xâàñ/¿òSœ>cÆ^¾ Ÿ_š@Q£Ñˆ§Ÿ~ZÉ¿ˆ$’}Huÿ„ˆXúèKÖqx©;7--/c!Ĥ{îy,„‚ˆ//+æþÌøÿ†>a>>\ê‰ì@Îä‡Õ:=tœ|±žpîyLºçá ‡±¤wXAîV‡ÃñOô–J¼Ó B~N8Ÿí…,¢³¬2/nHe±ÇZÛ1è´ãöœ¸,-H ÿíÌ-üå—CI‘°&õ†Õý»+QÄ©$»6çƒ@$†1‡{êJ$+‡ÃF³Q¾Å;' ë\S³YËEä/g,Ã’+ƒÝÄX=7*Ðd§*,(@CùöªÙ §G†˜Îo4³Þ¦ZŽL£k&%…(Ù£Éê:`ÍDª×ëÑÞÞ®˜÷pttTra‹}v»w­þ½T§…†SÁ>»ˆ%‰,Š‰Ô±X,àÅY”e.3p‡`ttT’ÅæW‡VÚóWž;rÿÃd±Ãfâƒtˆr@ôác8ß?†©{ ’ÞÊ®¥,À’ŒŸ¢‡ã‡: åh‰6¥ñ{a!´œ ÅZEZÅÚûó¦Þ> <¾Óy÷ìÙC•KȆÍf“%#ígiBMÅÚµzô(úúú055%ê\ßýÉy´šª$qN!¶ç/~rç$˜tttP[¢0ôz=Ž=ŠÑÑQ˜Íf浌dÞ8·2§Þ¿·qÿU” >ßä”4s?­Vú]e¨1–@­¾Ÿ$äʯ¦ðú[ƒLçTZ–\5èžã8E­!gr¬i³‹-aÒº—+õDˆ¿¼0Ž×ߺ‘vÃvœûàÎ}p……Ò,ØuuuQ0uŽÐÕÕ“É„?üPtßœ=|÷ÇQR¬E<¾yprkCõŽNM›ñæð |iß(R [O‹Äbpú}pú}(ÕjQ¢ÑÂPTœÕ÷å?Ÿ9¸Û`00‹$‰ÜBŒ;|*õ%’Ÿ3¾¼ _$Œ¹`|<®ÈzW»Ã zK%Aé7 BfÇËCÖ¡Q©ðD}#~½y8•8ÁÁ¸ÓW/ Ïò/Ã<íE,.úÔ Ó`_“pÑàÄv™>Ž9Ÿ|öaÍF=šª…/è¾Ó?C!9­cp|’œK¯×çä ˜ Îgetwh2TBU°ý¢îÉÁ~æó+aIªMÅÍ(P ¬³êìt=ùn?œ lïoGG‡bÄI<ÏÃl6§åwi95k Ðj(p3ˆ–™L¦¬gå:mmm8räˆ$mÊ«C£8qñÓ¾k+±ÃfB9þ?Gˆð1œ¹4,©ØÁd2áÙgŸ%±ƒBéêêÂ$ygÉéa{ÔjJõE¨6èÑPk@K}%êå¨(Ó­ êv/qy€ÍqO¥RQp‘µóáíØ]_…’b횟LˆÄÌ©HtKHAOO$îšÿþ¯RËBK°#µØ!!F%±ƒ²çÂG•Ìiës0;6|^o,Oûµ•—á¡öìlÅÑ!YìúâY¦órGÏ|tßÕÕEnVŒ¤sM{'ì÷Ñ?0“²ØÁ|Ëo¿xõÒ•¬;$³´$.>AjW";H8©IÑ7';=Ìy6w:v×±Fb IDAT1Ûð¡wü¶¨òù"Üó-Â2ç‚Ýç…/’}s·G†à ³%÷HÄ BŒ;|ªì­®•ô| ¡ ÆægqÏ·¨X±ƒÂÜ^¡7…P$x ˆôpB …,æÝi,3àØîvT‰³q=um7Dp cNŸ¤×Äâ~pCbÁÌ,e½w,×É"L!ˆípøqÙ:!Ùù¤Ø Í6ÄX=w›€A+OfÚ’Ò3ŒÌ:ñ‰}šéüJÈ´år¹DgAÜŽòN-TEÙùêEpú»{‡’‚Tå¶l†ZUˆúêr苵ÔQ¤ 1‹•h–ÔÔÔHèñâ¥OðÊPR Q& 9âî;Ì{¥ÙœN¸:H,Od“É$Ù;K¢‡ûh5j”—£¦²M»*ÑTWšŠ”—oë‚ÑÛgA8Ê6ÖùÜç>G4!ë8\ŽŒ´ì©Ï‰9•Éd¢‡„„––ÑkjP'þûYªL™ÚŠû¾¤b‡ŽŽÉ‚ñˆÌb0pôèQ477Kr¾7Î À>ë]ó‹ Ð|‹Í¾¼¬uèì¨CUåæûŽ?}sÃŒçÏEwèlGLнÁ` D"ÇÒé\ÓÞŒXl #&§íìhDñãW?Æ·_ìenG”@ss³¤‚5"»Ðëõxúé§%é›'lóøÎkÀŠnúïÅZŽYô0ä´ãŒE¼(jiyÞp3‹ž¬?ù(~6:Ì||OOâÜáS¥ÉP)Éy<áÆæàôû˜M²rw ù ´›‘Ç…ººº‹gs9í¡št¥(Uç×À·²X‡c­í¸05§ˆ,íÿíüôß=)ø8§'Œ’"5*u’\ÏóûðWï :æŠEze©7À#ÌÇQÄ©d¹oÏ?aÂk—„ ­LÍÐl$»_B<á7‡oHv>£Ñ˜“¶Yƒpµj55µÊR¦ò¢"TïÜæ¾6ø1Óù•’iKÎìHºfœA•Õ×ÿÒ©«l±ÈºJʤæñx0::šöß[XX€ºªRxýjÌy äCŒ°¬££#ëÅYùD"Ðãƒ>m/üÍ÷¯âÑÚ*<º«êþ‡r;:l÷çýÅé ^«"î‡ÔbƒÁ€Ð;—ƒïl__s;œ !z8~¨cÛÀþ\C«Q£HáX»òSX(\%5>3‡þ6‘rSSvíÚE3! @@¶qxkC•âçT=ö=$„¤$J¯]»Æ|Ž¡1;NŸÂïy„*T"ü¡þýÎ`Â6/Éùôz=º»»É¥0Çà8===¨©©õ@8Êã¥SWñ‡¿÷äg‡úê2ØçäKú¦RâÁÖê-E ¼‹a|ûÄûÌÏ?eRO?b‚î)ÉHvŽ¥S/C#"‘Ô’LN-à¿|÷|Ö::HEww7 yò„žžTTTˆÎ ÿ‘Ù N­ÂñC›ï«|t7†'lãw§N¿_Ú÷ÄŽIîR!!~ð†Ã€R­:NƒRœ*½{žïÝA$Æ–¥­­ «ý)«;¼Œ:q{áX Nÿ"‚2 3¿gÃçñœþÍc­ž­¿Û«kðH­iCûãã£äî@2B‚‚H'œÏöBÞö¹ñDEþ-kT*kmÇÕéIŒ»ÙßÞ0^½8‰ Ÿä;ü0è4()ß,³8+.6UHZ¯3ó!ì©+‘åž±^ç;ý3øÆo¶S‹Dˆæ’uÞHX²óõôôä\MNN2lnjE‘ DbÛ&O$ o8$ûu[½n\´ŽãË<Ú’ûku×çäî@2B‚‚HJqypG#XˆFP©Ñæå}z²qeS„UôðVß4~¯§‘I¸`žöbÿîJ¨Uâ7üï­ÁÅ— c®Üž“\ðàð†ÐlÔKrMë1è48¾ß„3ýÂ2i¾zq’„øÉ‹g×lw%;_[[[Îeûåyž9·\[„nS“,åj(3@U°}›ä£8Éèî`0²>ÓÏó²fs(}(ûÇ/ºÊtÇqŠÊDj³Ùd¶AË©ÑPc€×ÂÂbKK7ŠJõE‚Ï{÷î]ä;b„e]]](’¥$²[@mõúñüëgqá«ÏÞÿ0b«b‡Páê'ž˜ðàŽêr]Úê_J±Câ^RF­ÜGªŒ´ëÊ<>WDr’é™fÞW‚H™P..—K´j;²!;_æT„òBôð?9—ÿãÿD•)©Å]]]”Ù>OHvQã|èö…ðò©+øã?øuX>êÏS<¤*v¸;ãÁwþò"Óõäª;t¶#&H’ˆK³®iWVVŠ^*vøÁ__ÁÇsúžà·û·¡ÓéèÍÓñµ‚Ä7Î  ¾ºlS‚c=íxùôUæsGb1¼9|mU58ÞÞ)‰ÛÃzøxÞx|ÕýS©P¤VCÇiVÿ”ŠS·†˜3ÝÓ¾!E*„j§n nøÜêu#Èot ø1Ì7¤H,†×†>Æ¿í>„"µ>>ŠQNî„b)¤* ˆ´rB …xóú&=ÙØ‚Š"¶ { é¾i¶Çm»4–ºÏ=!< æC‹ôƒÝx|¯|ÊÙç÷7>fÐêÁTŽÛ‰òŽñøÇ[7$;Çq9™iÁb±0/|o—§>ªõú”¹Nݵ¤„{È×ªŠ ²úú¯MbhÌÎtl"›Rk5,5å%Åhª«@e™……k§¢¥:áB™`0˜×ý‘a™Á` ëqÐÓÓƒææfQç¸h½‡>YùKºÅIÎÑûïü€3kë\J±C"P‡ÄùCKK‹$YW-V†Æî)²´5ÊKŠQWU†–ú*4ÔPmÐC_¬‘\ìŠðèýè6Ó±z½ž‚& Y1›Í²ž߃õ4§"ûä Û@¤1A‚9—à*°Šõz=ªªªDýn;lÎÃ?Lbê›qôèQÑs¹—N]E(²qÿµµ¡»ëD—stÞ…^»„KÖq„cò:ŽóñøŠûƒß«Ç‘Y'&Üó°û¼˜ úä£àgFf¸lekWt:­E¢ûS¡Ì8=2´áçû4Ff~rIì ‹aȹ’¨÷ú¼S)Å>Ao ¡THð@iÄáp\p1ÛË™pyÈg޵¶C¯aSa¿Õ7 ˜m5ï‹`Î'¾î?,<ӋӆÖ¼.gæå<°;àïY¨A"˜9cFDÂE’\Ì,˜Úªj˜m·CÇq0êvÎȵbÊ8¥›j16ܹ€?aÎDª4X³Ù,«°…yZX€Š2šê*PSY ­†LY#,£,sÊA ÑË>Á gR°QºÅ’ö‰=1ás¡DøÞüÅ $b£Ñ(I€¡¶õ)„)±“““²fÐÛmªÊø5æËœŠP~Ÿ,&Hþä{ýp.ø¨"Û)ăO?ý4eµÏS8Ž“dNÕ?2þ‘iÙÊY_W–Ò÷®üj ïõ²íKå¢;´tßÞN.ó¬ˆYÓîèè€J¥bþݱØîŒÏ‘ØÆðÄ6c3±¢‡p”Ç+?ß¼}ýâS](Òˆß/Äb¸dO›ðaýïö†Ã˜ `õ¸1¶0‡‘Y'¬žÌ,z0ôÃ!ÈG_^Þô§F™ÿg?ûYzP ÀèèhVîç2–9¹;Dš ÁA¤ŸJ(d¾»–f]Ó6¢Ýl§g<¢)}7]b‡RõÕe«?íM5kþ®áT²—ÆðD2Rˆ&lóèÝd¼X¬åð•ßyB²²& zÇoÃe¬Þ‚<_$‚¹@÷|‹°zÜsadÖ‰±…¹UAÄ?ß¹…‘Y¶ìðF£ÕÕÕôàyžÙyT­¦$ub w‚H$x ˆ4C.Ê¡²X‡GjÙ,âß!xˆÇ—1æŸ=ŠÅåáýÁ{²ÔåÌBP¶ûôüþÁÇxƒ<‰Áx"!ôŽKë’‹ …b¬ž6í†A[,y™ªõzp)döIX)² ›j—˛͖·ï°sÁ‡Ó†˜Ž5в½~ýº¢„-……(ÖÒF ÄÒÑ&•2éééäqÏÄÿöóá ºq›±C‚ùûï÷€ð~6b‡·>D0"¾ÝìêêBOO=°„$ÀQ>޳¿ºËØuB_¬]84ÕU ¦¢¥:-ÔªÌ-+¿sÉÌ|,½£„œˆqàJ•VSæü¡HÞÌ©ˆÜáàÁƒÌë5Ccv\¢5äTyéÔIÄ$ &’‘Jô`“~﫼,5'—ÿ®÷ØÖœ»ººè]H3™ºÏgĬi‹yðß[Lé»?}sP±ƒ†S¡½©O>Ò‚/üÆ>|ýwŸÄ¿zæq?Ô¹úsäñ=kþþµg{ðÕg»qü`öïmD}u™¤e¢1<±R¬yë³À>»1kkC5¾ø”´û‘X ×lwñ£k—qrðc :íiu}ر߉ÇäyÌøçÑ[LçP«Õ8zô(=œÄʸ×lfîOcYôn(å‚rw ˆ4A‚‚È '”PÈ|wy€}µõÐk4‚ Db¢ÄNOž`TTÙŸBøĸÓXúA¬7À‹¾ž­xî ¶…–ß2ƒ „ð3‹ 'y¹ša›ÕêY«V£ÇÔ$yy8• •Å©ml¿Ü…íwpœ"lªåÎ4ší¼ön?!¶¾è±ÇSÌuz<LMMQ£Ãˆ/)AœElÝ׈Ípúæð~ÔgÆŒ/)K¡œb‡Äÿ{îgºáÏ>ÁƒcÞ‡9ø,úÝÝÝ9éÜE°“؃/ÁÙ_ÝN[™‡jƒ µ´ÔW¢®ª4ã‡5ë3sžp0ÛÖÖFý !b¸„ðȃõ»Æ—N]Í‹9‘{ãèˆzî‰éí³àí 7EŸ§££ƒÄ‰Ä–óa1¢)ÜüÖ£×í¼wè] ã;ß»À<Ÿ àùô“É û|FÌšvss³è½¶éiOJßûèãi¼þÖ ¤×ÞÞTƒ§?ó¾ölŽ<¾ìÙ…êòÔç­ZNzc9öïmÄñCøê³Ý8òøIÄ4†'¶ë£ÄŽÙ^ùù5„6I@“x–åÀêuãg3¾{õüãð¬?\³Yáet¡8xð =”€•µ)Vá&9⊜(gÍùÝ-Béà 2¹<(‹Ï5°-(žé–pÛ&ÎåÅá>´ÌÊóÜ{ÂòL¨u|ùð{d èH™‹Ö1X=nIÏ™‹¶GGG™­žµ¶£H-}¶ªjª‚‚¿÷þØæ‚æ{™í™¶&''áñxòö¼cgÎ2ØÜÜ,:‹\:¹~ý:ÓqdSªXÅKJgÛßÈês~Ô7Œ÷æp{ÖƒØòÒý*|XÏVb‡öûA 7.Áe®2È»PüËGEŸ£»»›QˆÍ竺»»EÃ>·ˆþ‘iÙÊX¬åPY¦C½±|UàP^R -—cƒ3ŒîÇQ!+}}}iù=uU¥¹¾ñ™¹¼™S¹Ùwtt0ë\ðáä»ýT‰Û0xÇŽïþä¼$cj꫉íÆrR8=H‰^¿³àá;ß»ˆEÛ>+9t¦ŸLÝç3¬kÚRÌóÁ(¼‹;ïa»fýøÁ__‘äz5œ û÷6®ŠZê+%«K-§F{S ŽêÄç»ÙsÐžØ “É$jÍËí m9Ç<øèn<Õ#ï^Æè¼kUüðý.àÍ[70¶0—™þ'Â%+›sŒÑh¤þ‡X…umŠã8ìÚµ‹*‘b ±¨ŠJîDN@‚‚È'”PÈáÅ…¼¿Qµ%¥¨Õ ßL´zDùGø8f‚ÌÇtß/ÜýàŠL‚§'Œ0—åÜÏ3º<üù)3^½H¶äÄö8ü‹¸lôœ¹˜a›çy˜ÍlÁG5úRì«•Þ·° †¢â¿ä£8u‹-+2mñ<Ï ­R©râùd Nà8NQYŒ&''1;ËÖ744Pƒ¯{Ì*^R‚8‹H­ß“=Ëéá•Q8üAܸ7ØÒ’ð“lpyXÞÞ¿/¹`qx(’­>í³^ø‚ì"ŽãðôÓO“ØØ–––Ñ¢‡þ‘iØg¥qáÔjÔ(/)F½±­ Õ¨7–£¢L‡bmö÷ý#Ó°Ï-2ÛÙÙIý !.—‹y.}±µ•™<°f¹WÚœŠÈ]:;;™ƒöN_’%;|.0>3‡û¾èó€˜HµOéééÉŠ1J½sˆÅÝþæ°åª;t¶“É û|Æf³1¥¥pñs{R˨þƒ¿¾‚`P|0ã¯íÙ…õÌãØ¿·QöD¬îˆ4†'R¥¥¥E”ãíåŸÙ\dp¬§_|*=â?4 Ëœ ¯›¯ãÿýðxÝ<€k6+¬žôÄJõŽ[˜%w4Bªþ4 Q%2 U«ÑR§±È ºcD.@‚‚ÈJqyÅc°‡y¿öÕ²ÙÅ‹LÍ‹/3ÿüþ†2ÏÁ–ǺojVžgé¹ý hªf[ÐúÚË}sú©Q"6%ãñæð IÏ™«¶Íf3³ÕóÓ­òÔG¥N—Ò÷NÝD±ìJÈ´e±X˜ïMii©âŸÍÞ> †ÆìLÇ*)0Žçy 3«×ë)°@!÷˜U¼¤q‘:&“‰9C-œžÄ Ç<üQM»àOî#vrwØLì€m¾áûKO½w—·º\'[]žÿdLÔ˜.Û2ŒÙKKK‹èqãûÝF„>WOêªÊÐR_…†ª zE’ Ex¼s‘M`­×ëEmÀÄN\»v--¿§ÕT‘ë»:4É<§jkk#±‘5°BQ¼}þ&Uà:ü¡þüïz‰ þ$±!t}ãèÑ£/G‰ngw‡ÿý[ï0ŸŸÜÒO¦ƒîóÖõN½^/‰ÐÄ“‚àá—Æa¾åõ{ªÊõøÂoìÃçiI‹£¢ÅêÂ=J@¤®®.Fæãß8·u°o#¾þ»O¢H“¾ç1¾´„±…YôŽ[pr¨ÿ÷¥^œü½ã·eAX= w1Ký!UÚÞÞ·ÛM•˜ZµMåh*¯À±Övüþ¯=ŽùHX E'w"gPSDF9à|¶r<°ˆúâü(×–”B¯Ñ ¶xÿþà=ü^O#û„*¾Œ™… šlõøa¶ 0ZfñÌ>éU¨NO{jK¡VH~îoüV¾õÛ Þ&ÂIƒÈmzÇ-ðJ‡ÅY„0:;;Ee“þÑGÃøñ¿8„ØònÜ›ÇCFªõIN )‰RøE ÷Ç<ÄPo,—¥‡Æî1»;Ø`÷¶µÁívcjjŠéø(Çùþ1<óÙ‡¶ýžZ­B±–C±–ƒ¾HƒÂ‚œ¨¿Ë7&޲õƒb6b;FGG™ÇáBÙ÷`}F®‘ÕÝAª 4‚ ƒÁ€ŽŽ¦u’Ó†ð/ŽüJеT‘ŸòGß9ÅH™ÜG“Ø`y—»»»Ó&8dáʯ¦põ#+Ó±¹è­Ä l˜Íæ¬_Ó¢øñ«‹:G{S ž|¤9-Bˆð1ôL3?Ó”0€ÊÁƒqöìY¦÷Ùí ¡·Ï‚c=›·¥­ ÕøÖÆëç0a›ÏÈõY½nX½kƒÁµj5êô¥¨-)E‘š[ùS¥FyQ1 EÅ)Ÿ›ÕÝÜ…©úÓŽŽp—çR¹I¼—›±U8lómrb!ðÔܺ©5¡T©·72;uksA¶²wttd}¦­ÑÑѼ¶á¼cÇÕ¡)¦c•g6³e<æ8Ž6åÂÈÈó=Îñ±3ÌíÖ[æ 8|Á5b…±ïªÛÃ*,b‡áûäÿ´Àf©¼Þá¡XË¡XË¡²L‡Ê2j*KQo,_ýi©¯BkCõ–? µæ_µZÏ|æ3ôÀ’`0DLØæag,¯ˆ¤ÐvÇb]9§œœ¹Ä>ÖÉÅ9‘=\¿~é8•JÅ µ;Íîƒwì8ÇØMÂjB)chNŸÊëz¼cg"&Ú ¦$¤D¯×gUÐß·OœÅ"ã^]•wÄ¡„5m1îõÕe8òøž´×«}Ö‹©{ LÇRÂB*Ä$ù²ÔÙZ‡?ýêSøâS]y%|0™LäÄB¬Âº6µ~Ý”Åá¡©¼"¯ëþ‚cZ)E=Ao ‘kà ²¥¸<Ä–—` ä·ËCc™ðA›Ó†Ãý»§fÌǾpXxV˜q§cy©rº<~¸ÿî7)CÁØûÑ;.}¦6! …Ñh”é'§µ®xžÇè([ðb“¡ûjå³zÖí°ä£xŒm¡Z ™¶xžgÞ00 9‘IŒÕÝAiq“““˜esdêììT¼°%_‡ÙÆ‘¹ ^"Ro»º»»™Ž Dcxe ©?/HŒ‰‚›_\óÙ–b‡d’ÿmF»úñ+Náa5•¥h¬­X#dH*Êt¨(Ó¡T§]Ak9l{Î;wç˜7~ÛÛÛiã—1-pæÒM„"ÂìÊy^Zg†ååLJe™Dý#Ó°Ï-R?HäÔ8¼ººKKÂʤÛÝáä»ýÌãVJ ¦¦†)ûì„mƒwìy[o¬m°²æDí!---0™LiýåeÌüþÖ ÓùrÕ:W¡ñNæÆÒmmmi›ç}ôñ4“»ƒ†SáÈþQ¬åPª/Be™uUe¨7–£¡Ö°m¢Žä„õÆrÔU•¡²L‡R}е;_÷Æ$P”0€½^Ïü<ÙçÑ?",ˆxÿÞFt¶îÊ›ú%w"ÍfÕŸ&ïu°œ§¶¤,oë~Ô»?UBQÉÝÈIHð@Ùà Eô†A^»[|„ÃfKÃøÜþ¦ãÎ*Ôåá/¿ü“«‘ß„c<ÎX†%?¯^¯O)Ã6Ïóp:˜››cúq:°Ûí°Û혅Çãa’M…ëׯƒçy¦c=Ð*ë½ÜÉ9âµÁ~Ë®„E_³ÙÌ|ora¡¬·Ï‚ Û<óýUJ`Ïó`n—ÄXÙO®ˆ—ˆÔ©©©að8;:³ÖåáÓ?g¼~8üŸŠ”·;låöàW­Ì…N \‚ËÕþ€qUа“!UÎ\º)ëxŽ XÆ^¬YAݾ.ߘHùû±˜|Â9œBï\Ìo/‘½ãðáa¶µF£ÑˆP($øØÖ4:<ôöY04ÆÐÎ 4‚ ëOoŸ%/ëKLÛÀqŽ=Jí!b²I E¥.ÜTðð§/že~?(ÐXYÐx'3ci1AÔ,|pqŒé¸Ïw·áÑ6ê娩(AE™úb е´œ:¥sh95еôÅT”éPSQ‚zc9ZªÑ´«5•¥(/)†Z­Z=ÆbuaÞË–<‘’#r´“¬ë]½ÝNù»öY/þò.ZS2”‡XíOÅì'÷§Gð9Ê‹ŠQ¤Vçmý2ïTJQOÐÛBä"$x ˆ,\”Cc¹ðÉÙ¸DN ¬®ÍF=Žïõ¾Œ‚9]àƒÿ|”D„ .YÇá’¡}ëèèHi¡paaA2—žç ±°°§Ó)¹ðÁãñ`jjŠéØGjëÑd¨”õ^r*Õ–ÿfõ,à²uœ­-U@¦­@ À켑 6¨þP„ÙÝÁ`0(J`±X˜…-¬™à å@Y~ò1?þÅæ}ãm—ÿ§›¥BÄŸ”®~å'›È`ÿÞFIë§·Ï·/Ätlªã9‚`áÀÌÏ×å ¸wžW/-Iëì°éDâ¤V—oL Í_/‘Ýãð@€-¨¾¾*•ŠiŽž.‡(œÁ=ÝAh!V—‡s}øC‘¼ª+1mCAA‰Ùá8.mcÀªÊIÑÞ={W?²2Oˆ;4‘yh¼“¹±t:×´]³~˜o f¬¯.ÑÇ”µljU!JuZTôhª«@Ó®J”•ã*£»ƒÒöEåÐÓÓÃtœÛJÉå¡d/ºÊì ªÄ±%ä!¤èO×™YÎS«/ÍÛºW»Ã ¹;¹ "»øŠ i úŠÇòö&Õ1 ÞÆœÒQÏùØ7RžgpyDbøÐ2+[]Êéò`ÐiHô@¤>)œwáší®äç5)eF£’‰ÖDZ°°€¥%éÜyúûÙ68µj55·Ê~?·Ë(prˆ­ìJ±©îëëc¾¾\ {ûüMBl‹ Jºþ@ Àœ Ëh4*^ØBlO.ˆ—68ŽcÞ üåœ}ç׊ ¸íòÞwzX÷o[þ9£YýÚ+®!Áå©(-F½±\²º Ex\`<¦:ž#Vôz=ó8$åwÎ2½ Äxù¯c€TKEîÅ Î1fϦ~{Î*0/))AyùJ߯äðЇ‡·Ïß„sm“ÄF„aç±*1mÞ={˜3ü„Ð÷™EÄ$•º4l|ž¿}‚ÍÝÜ•w23–N÷šöGO3wüPúÅ0jU!,3ˆð1z¦‰¬Â`0 ¹¹™éØí\BoœÀç˜e(µÿ!1!EºÞ)Üb¾[W’Ÿ‚‡èR¿rÙ•RÜoÒÛBä*$x ˆ,ÂápLx5ÛË[^Âx`1oïSE±Nð1ƒV$¿;_f=<÷„ å:á“ ³ vyHˆ¾|ˆ”ˆ­ Çxœ±˜e9w6eú‰Å¤‰>šžžÆüü<ӱݦ`Ðg¬.YÇ12Ëf1¨›j—Ë…ÙÙYæëSz&1ç‚'ßc´(-0ŽÕ¦`ϪC(Ú¤Êo:;;™Ûóÿgâ#ÀZtÿƒ$Ãm—þ¿½È!Áð+¼â‚7&|þÒÙºKÒz9sÉŒp4Æ\§!7b‚³úG¦·uyˆÇÓwñ8V”"yçûüŒúABîq8«ËZ}}=¶„µ•¥()Ö¦eNuúÂÓ±›mš„Rú`–ñóéóCySGbÚ†¢¢"E$!r¹×½h0@«]›lç;yÓ6/ÓùÈMPYÐx'scét¯i›‡‚©(-N›+[2”0€Èf{ì1¦~n+—÷b/Ÿº’’D.a0(!q¿2›™ûÓõs³@ €¹¹9Áçi*¯È˺¿¹0‹èR\ E½èp8.ÐÛBä*$x ˆìã„ i2êòÇ1îžÃÇöiôŽ[Öü|lŸÆ¸{þ¨<¶Ò• EÂEc¿$¿ŸUð`ÐiðƒËÃËž°l÷RN—‡ÄuÿýöàôÿqMÕÒó² ü‰ìäÍ[7‰Ißž577§¼PXPP ë5@½ëAªð<Ïh]®-Âá¦=i¹§ZõÆÅ³ Å©[ƒLçSŠMõµkט¯/2‰ýÅÉóÌÇ*)0ÎårÁf³1› Âb{:::èèèè`:Îâ!+)ÜèôàÆ½ùÍEk>.Yýë÷í3•eÿÞFéæ®³^æM0!ã9‚KOOs°Óv.ñ4ïÁˆ5–Ÿ™Ãð„ƒéXê‰l‡WVV¢¸xEüÏâîP[™žÌy¯½ÛÏì˜GÍ„’aÉ<;a›gv1k•NSiyÖÖ#½«Dr:55Å<ï]ï¼700€åeá±ZåEÅyW÷Ñ¥8ÌnÅĈ ·…ÈeHð@Y†R\dÄå!cÐiÇéÛC¸:=…ÛsN8¾5?·çœ¸:=…·oßDï¸ã2 :*‹…àÞ$¿Û`·æûÊa¶ÅS%»<$xn&~ø,þÇ×{px¯ø %<ä×lVX=nÉÏËqœ Í~ŽãdËä¤ÑhP]]ÂBñþ›7o2gÀ±=é[ìUm" yïÎæ‚¦ó)a!Él6#`»¾\È$6xÇŽ¡1;óõ+)0ŽUtÄqe)Ïq8ŽË ñ!1™âOÍ[€™¤ ÒIâ†ØÒ2nÏz[ZZ+vH@ ¿ pÁ{ƒ—à2T”£Þ(ÝÓé 7©Ý$‘íV.ËK鿱¹1Î0º;P?Hdë8\¥R­º;l‚‡}Ö§eNÅšv³Ms‚PÚø™…+ƒ“9_7bÚJº@dŠöövIÖ:Kô÷ÏQSS‚[7fnÿö‰³XdLRFbAeAmZfÆÒ™X›1ßbàï®K{½ŽÏÌaÂÆæÈN ˆtöË,ÏšÛÂøÌJ}á(/iÙšÊ+ð¥Göã÷_íêÁÿuèþ×Ç>‹c­íx¤¶µúÒŒ×%ä!ÖôQföuÓõcOV1baA y(x w‚ÈÔT‘•œðB¶Ò  IWŠRuz‚$~Î[ÇÀ HO˜A :íØW[Ö i¬$õá6òã?´EÿîG˜£ˆS >öðÃ5hªÖÃ:',(ö­¾i¼pX¾L33 A4Tê VÈþ½p¸/n'Åà”n¹pñ– G\Ôòäÿ"zÇ-²œ»­­Mð¦Jee%Ün7¢Ñ(óï-((Çq(,,„F£Aqq1T*•$×äñx09ɶ‘Ûd¨@{Uæcfƒ~¼?6Ât¬lªyžÇèè(óõåB&±ïþ„ÍÝAiq“““ðxНX.æaÌxh¨Ð¯ýN¨˜]iS/xïâ¢÷.SÙ÷ïm”°` >ÑëõÛøu¹\«¼.—kͦ®Ø~d³ç!ù³ááá m‹^¯_ý³¢¢‚‚rÒ@ww7“`©dÇzÚQQ¦[ý,‚‡•ùP PðŠðxç"[–2ê ¹Çá¬ô4͆€‡‡Ö†jY¯±·ÏÂì˜Ç’ð ²ÁÄmÎj+åÍüº¼ Äø¥åMþ=öé¼P pœð>X޶!ÓIÁV‰ñ´ÛíF0dvGÝŠæ[z½:nUQSS³&“1BâÕû§ÓàÑGê¡×k6ý÷?}ñ,ÓyI,¨<(‘Œ¸±4ëš¶^¯¼¦ípwg‡×î{¹fý‚ÏñØC i¯ÛË7&àö±9²§c_$Ñÿz<¸ÝnIÖ¸¶ê{wê“Fãj°tEEÅêz‘ZZZ0<<,¸_ž°Í3»˜lE·éjÚ#(h;!‚XŸÜ/‹Áé_D8ƒÓ¿²OiõÜO¾#V‘- ycëÄÏz1ëõN¬_«Þì½N¬½¬çÚ˜›çùëc»þt}<€˜qòîŠ*ä×çäî@Y "{9ÜÑ¢T28¤ŠÓï%vXSÞp½4–ðdc 4Œ™Ïõàcüá˜duâ FQg(b:–Eð§ú¦e<éuy ˆÞq ¼‘°,çîêêu|Â!›˜œœdZ(€GjëQWR–±²Ì:qÙ:Ît¬2my<LMM1_ŸÒ³ÛøCœ|-Û ÒãÌf3󢡨v‰È~¤¼Ç‰Åê­²^Š 1 «›iëž)èCzX]ÞrYð ª·ÍÎ>6¿ƒNƒ’¢O7G ô‰wwØ¿·Q²Œx?ÿp¾`„éØŽŽŽ´3¸\®UCb8ÛÆgCðŸÁ`€Á`@MM ‰ d€U°¬<?”yAëò²pƒ‡Ë7&ŽÒXG ›eÒe§?olÜ(âcqu”3˜ÚŠ0gpgMø $’ÇÄÉý7Ë{¸Õxw}–Jz÷2CKK ¿ïƒwì8Ö#ŸP6øèæB‡5}ï§ß]ŠœP©3Û6¤3é‚ÇãY ¨t»ÝÌYOåì›eJR2«ciSËk6éÏþöÃ[Š~úæ ®~de:/‰•EºÛ´\Ãb±0¥»»»¯±°Óßs¬uœœîúÙÎ&h85ƒi©×P„GïGlörì‹$ÆÌټƕè‹×¯q%÷Ç a"‘ý²Xjõ¥8ÖÚŽ&C¥dç,R«WÏ·*†hZë^üÃk—á 'e"!ËåÚ –"Üïõfïvr=® +Ñ)Âb±0·«ëûS1‰= <³çá¼j»||£Þ¥÷õ6D>@‚‚ÈR”äò0ð¢R#OÀd4Çyë˜äç^ôàôí!ì«­ÇÞêZÁÇ—0<­ÒMĈ'šzßo™~a«Æ~ܰºñhS…,÷:.D~b™waÈi—åÜF£&“)§ê+àÎ;LÇ®X‚fØÝa„-³³R2m]¿~ùúr!0åä»ý„¢LÇ*)0.`tt”éX“ÉD¶Í9«x)‹×Éçß*Åh4®ƒ%‚©)€ Ö éñcó‹ØS½N°X°öϱùE[Hl^'D—‰Í¢Äk‰‡U°ôñ­i<ÕÓ.™pˆ•åeaßw/q®-X#WÆ:ÉAÕ묓…‡dÉP-„Äfq‚킵•.hôx<Ìãðòòr”””¬ùÌïž-ö‘=õ²^ãÛço2;æ B˶w-ùÝJ1È,-ô¼Ç­¾[‰vN©ÁJÁd2 N*quhR6Á„š­.ˆFÕ &ŸJ6· ëÇÔre”•›ÙÙÙ5í©¥‡5›ôj_^¶yÒ1ïbßùÞæqT®‹s %Žw²…@ ÀœšežÇ¼¹¡Ÿapxhm¨F|i ¡PDþ~é\Ÿ%£ ëÉÉ¥²¾?N8?ÔÔÔÀd2‘BB˜ÆRp¨©‡Ö ÒÁ%ë8“Ø?!OâýM‡•üïÔ^m%@Nv7Îfç1{ÄF£qCzýúuæ÷ð³-‚ÜQrëóN¥•܈¼‘Ý|Àóʳ¹îhöPõÅÒOøvðqy¬¡øxýöi8ý>&·‡Z})œa‹þþp %Eâ›Þ€H·ˆ¯n,x€³ƒÙ`  ÎP„"NEo?! žHg,fÙΟkVÔ<ÏcllŒy‘£ÛôŠÔ™ ¶ºdÇÈ,Û$T 6Õ6›9#[lPÅà\ðáí 7™ŽUš»E__ó±”ñ8·IE¼´>ÃV Èú¬<럓³ÉS¦-a´´´0õo Nà?þÆ£÷?(Øø§'Å\0Œj}páþ”õ›¿`*ënSêÒL}Ú{|Œm)åxÎãñ`ff6›-g7íŒÍf[ͤ¥×ëa4ÑÐÐsbàtQSSƒææfÁA—á(þ‘iÉÄCéâKìs4%ŒuAÕ‰?“3ÀgSêTÙ,ƒýV™ô’I8=%k¯w|Ê6XæP_¿Q¨°3ÀV´6TÉ:§:}aˆéØÍ6ͳåÙL5$ê\‰AÑ<ϯ¶›g$‚1’³ÃÓ8Y ‚ûÞÁ;ò$U‰ñÂÅÉÄc_°,me[Û`³Ùàr¹RêoreL@$‚~éýfCŽlÒów}˜¶ysvìJd÷xGI ¤mž799É<·ªßµ6á‡kV˜Hj·ie¼®*,DÛFL;=²:=¸ƒ¸|c‚éX1 }±Ò“x¤:§°Ùl 5. á8mmmÌB(Ê‹Šq¼­CRW‡T Çbè³±¹AI'!Ròx<9ÿ a+wãDR®d÷—L÷§¬ë===kû8—Kðœ7A•N£-æÕ3r/èW’»Ã7é­&ò<Dãp8œiÜ».›¼^¯_Ý £¦íaÍhùá¤þ-·©Øauì>ïCµ­ð¯ÌNܽ k„-ðCªÌ¸“öyŒL²Í#¥ÏÙl¶U‘ƒR³ÍŠ%±9œxgM&L&ȱEíííLcñËã«‚‡‚B`9žþ²¦þÝñ™9 O8=ÖIW'žýdC¾¶[õçÉmåVsN·*€HüdÂñIŒÀ¼¶¶šu©Õãñ8“ÃCme©l×øšǼõ›æéì_ÖÿdÚõ$Óýmòû”p…HÎO¤K}BQ Þ±c߃ҹ±,-Rt1P…*åµ “““°Ùlyù~'ÆÉƒÁ€––Ê6¦91°âä°ÞåáîŒ/ÿø#¦²ä¢;t®“‰ñN® F¤&tžÇóGï„ÔÖc_mj›Å:L/ S?ûÃÒZûÃ1fÁ|ã·Úð­×„h_½4‰ÿóø^Ùî…ÓF³QO.„ä\´ŽÁêqËrnŽãrÎÝÁívÃjµ"3¼=³åïÝAqV 6Õf³™y*2‰]šÄÐ[E¥¹[°f¡ã8ííí r½^†††ÕM'·Û­È,Ñ,$,|GGGÁqÜê&lÝî ]ÈDcøpÒgö6®|°‰Øü‘ü7Ô(0öâÅ»2•q·© ­ Õ’\ï©ólÙhÅŒçHä°sýØl6\»v ÍÍÍô¾¦ˆÁ`€Ñhܶ»}!ŒÏÌ¡µ¡zý+›6 üâ3Œî™ë$\.×êÿçKß›î:Þª^ìÊ)†`ÝV©T0>·ÛíX^^|>©úÇõ Þ±ã\Ÿ%ëæT G†DP•ÛíÏó”2E®³³³«D‰ìÔ 9•¨Cs ¡%C ø¨t×EÅÊhHä°sý ```€Äi˜ÀOßÄ×ÿõÚ€÷ï|ï"}¦räÚþA®c4199IÁÈèèhÚæy‹…¹ßxò3MkÇ¢³ åªåò’"”èjàœ÷aÖ# *—‚ IDAT]Ÿfw`Â6/[?,rÈeW%1$¯q™L¦Õþ˜Hýgq4ʱÖvt›š„•M¥—bN¥·.9j$[9Ýó-bÈɶ‡ÉêPŸK“ƒƒ|cñä¤\éJ6Àº6µYò¿ÑÑQægãPS+ B&–9À½ ÷‚~¥÷½¥D>A‚‚Èr”äòpÛçAMQ1Ô…’œo|a>í×0ä´Ã âÉÆ–XÆ~h7JRÖpT\Vˆ·àÅ·Ìð…ä|x{tìA”Éׅܶ/Jæ„A`õ,à²uB¶óó<Ó§OKr®Ä936*2bš`6èÇé¶`ÇDÝ'‚,²‘p8 ‹…mó9W2‰½tê*Óqz½^Q"€ÑÑQQÂÊ0”ÛÄb1œ={6ïëçyLMMajjj5@©¥¥…²Ù&ÑÒÒ³Ù,8ÿýÛ3+‚‡í—oèá±§H‹¯Üù9s¥rwè¶Â9ïc:¶­­MP»™ØDšœœ$‘ƒ’ß×––Å ÓMgg'ΟîjÕ?2Ö†j¨T@<Í«ÿI­œö¹Å¬ë$‚¬=Ϫs ²ƒÄóúà›Ä<º¢¢bU!F!F`^__Õº5C¿ß……¦óIDÌÉwû™×,Äd›\ìŒBý©Lk$I ïLMMà¿Á;v|I¢ß¿_qxŠå啱@ª[én&''155E"ýaBü`4ÑÒÒB™¦e˜ÿÍ?Z#xèýç;xý­AærÐŽ2ûO"½ç%’¢° ×ið¾qhÍg,‚‡º-ÙT……¨7–£ÚP·/ˆYw@´ãÙK7Ù®u‡}‘D"¹ƒÐs„ø!±ÆÕÒÒBãûe¹žµ"5‡Ûs¦²r¨ 7uwЪ9¨ äMrr(=cjJ“9¶K6PQQ!É|{rr’Y ÐÙÙ¹¦?åyf3[Ò™ò¢bÁ¢\àW.»RŠúªÃá œÈ+Hð@Ê@.±å%X>´–ˆ/f4‡;ÌÈuL/zÐ;nÁ±ÖömE •E:Áç–Ôá!"nÒbÐiðÜþ¼vIX†’@$†S}Óxáp‹l÷Ààá FE9XD‚pŒÇ‹Y1åML•̳vw89ØÏ|¬Çãa *S ¹Iìä»ýp.°³ÉÄIÙL"Ó‘ÛD"ª„MúÐD0µ^¯G[[ZZZò>è#Ø&t#iÐ>‡/ˆº²Oç=ë÷f9À£†_Å÷íã¢÷.Sù¤twxïÊÓqz½>¥Í¤D¦;1Ùˆûu™pj1hoo§ÀËM¨©©Á`ü¼õLãø¡Nk9XNc™U)®6‡"<Þ¹È6O“r¬“¾NdÀóx<p©Ð6e³ 4½^N·ºáœCì4g` š*))AeåFñ¿Óéd:_‘Vž1LoŸ…Ù1oý¦ùf$»4$þ$—†ì/“øaë¾W(¬ïÓfÄd*Æc© än’I‡)˜X<‰þo``&“ íííl)ÑœxÚæÅùw'ÑT] ß\ö_~A•I2Â2Ïc3%x÷í¯Âã É~]N…ÚÊRÔV–Âëc1†×,~èí³Àíc+oGGdž~šD‡Òޱ“׸âbëñ6ËZ×N!¹ÞFGG;Z©‘,DlkkC{{;­qmBKK s0÷fljÅ—÷íϸØ!ÈGqrðcæ1õV yHä ìö áa2™PSS“É´ã¼Ûb±0ßëîîî5÷x<̉=šÊ+Ð^•nêŸÌ;•RTrw ò<„Bp8'êêê¾ «½¢bËKó/â¡Rq• á`Ư…ÇÑ;aÙRô°•"]Äãˈŗ¡V±[î5õ8¾ß„3ýÂ5Òåòàð„Qg(¢€`fÐiÃ輋*"MhÕjjjÍhþÉr“nÄ&p—îÿéå÷ÀÇhÃjÛ¾½¹™)#%Aä*ÉAÍÍÍy+|0 L™³Þ™Æ]7æ/ðQ °T<Šÿ:ý+æríßÛ(‰»C(£„Mðh4·ÌjL›Àé%Y¨D›Â÷Il Ýäê¿uÝBPK˃:ÅÛå^ â\Ÿ…éw$6Sí\.תƒc 6sƒ0ÐëõÌAS•••())YóY<‡ÝΖ-½  ò¥£’_ûÛçoŠrÌ{ã7èÊÑ13e¥]Û<ŒÏ̉<,/Ë×_/Å·waÓ6¬¨YÇãÅban_ ö~nxxhhhÈûqµ^¯‡Ñh<¼:4 à^ï¥,JÂëõR%( !ó¼¬ÁÒe¥ZüÉ·ÊËŠà]¼Ÿ0­¥©RðùÆmsLå()Ö¢¤X‹ÚÊR+k[Q>Žp”G(Â#_ZBœ¹dfN NˆÉu-}ãëááa çõšôVH)xxfÏCøÒ¾'²âºÞ»3‚¹ [bõ¢¯D›ÍFÉ:r›Í›Í†Õ±éfŽ‹@`U$!Ezýúuæ2ßó/âäàǨ-)…¡¨µúRÔ–”å´ãÃ'søù¨ŠêðMz³ˆ|„¡,Nøûl/äÝ Mº«Ø›˜(Cö?:ö üáN]›–ÄýØYô ”A«´‹þƒNœRý›¿Ù.Xð¤Ïå+žH½ãªˆ4r¸©EêÌmž}0y‘t¦¯UmmmŠßØœ°ÏcxÂA7s8ŽÛ2 AÈ{áËFÒøÜ"‹AÔ•}:*p»­¤pÿ¯3¿ÂxØÍTž" ‡ã‡¤i³þáì'Ì‚¸Í.— f³™‚£3DbSxtt”„Iï¯ÐL\ö¹E¸ƒ¨(ÓA­äÞ§)T…)š}¾sÉÌü{¶Ëú™8$~(ƒÂf"ˆTQ©T¨¯¯ßôœ¬†¿øùGWƒŸ¤Â¹àÃé Ct³‰”Þ…ááá¼u^ª¨¨œ?n›ý{——仦ø6‚1mƒÑhÜ2@•ÆÔ™'àÚµk”e:iL-ôy „¢øÑ?^F”°(Š`0H• 0„º;LNN2Ï÷þä[¿Žò²Í÷žõzá{îþ 4“íb-‡b-‡r¬-ÛøÌs’ææfüüç?§€é BÉx6ÂqL&“è$3ÿfÿ“OÄ·: úñþØó˜:ô>99‰ÉÉICçÁ8=¬Î;M&L&“h1ÐúþTì³Çaõºaõ®ÝÒªÕ¨Ó—¢¼¨†¢b4•W HÍ¡¶¤TÑ÷%º‡Ù=§”â~ßápÐâ7‘—à „Ãáx¥®®î²Ü寋è,«d>žEðÐù@9~ÿ³à{/táïŽâÏO™%¹–íDµúR8¾ŒÕ³?-x8üp ï­ÁÅaYðÓáòá㘚  ÙHB8ÿh¾AÁïi¤F_ŠnSf»'róØœílP•Äç(“ÚN´µµÑb9A¤@ò&Óc=–7¬‹åN8W\ ¸8àÞÊüãç®,Î0—çØgÚQ¬_÷ÓNnO±š››a0Üw'Ì¥ ¬‡zhÍßoß¾­¸k áÃÚ>žÅzüã‘iëiGA VrM °rþTŸ™c±vttlëØl68§ºº*•jÍgÑhN§“é|6ñµã=’—óµwûEé†)‘ÏÎK,®‰ã3Ù¯8HHÛ6ôôll«Hè@ãêlŸ uN»24E‘ÂXZZ¢JP›ÍóvjÓXƒAMåøú¿¾ßw••¯ux`Õá!U^:u•é8•J…™™rXÊ"Hø°±‚‡l;À©[CòlN,{÷î…ÙlÆèè¨àq ‘$܆††³õKëûS1ýåNDb±„wc2¬„B«æPWRŠ"µµú!D“¡2«ïÃÍ…YD—!töø>½9D¾B‚‚P' —{(€ú"=*5Z¦ãÝ!áÙ7~ÿ³¬ÿÿÙ:ñÂá|í¥>Áü›!¥ÓØÃ=u%’Ôs8*Í¢Ý7~«©žÒáò0³DC¥jU½ýDÊôŽß†+ƒb¤|äéÖöŒ—ÁÐØ„îîŽyeæÚ½^öövª‚ÀÔÔl6ÚÚÚòÂ…5sÖû#ÓøBW àQÃ+s¡÷ÝxÕu“¹,»MU8øènI®ë­™ë#áîíAYÅÅÅhjj‚N§Ã<Øû© !ù3¡Ü½{w5óåȧ‚ˆÄgÙ$HÐêììD[[[^öóF£Qð3Úë.Žõ¬Œ U+ ÁR‹ U€Z•ú÷Ï0º;p‡öövx<¸\.ÌÌÌP %‘h4ÔÕÕmøÜn·3Ÿó?ÿ/Ç$/çà;Îõ‘ &!®Η@édAlªLHàðP¨’÷º–6qyÓ6¬Oº ¡CuUªFTWW£ººz͸ÀšÏwbnnss÷ƒ\ãéÿŸ½woê:ó½¶,Ù’lmù_d4ž“È%á¼CN›ØIf漦$™¾v§yÏH›ô4äL ¦™Ü3§“žÂ´MÌôB $jgpHÍ€ˆeƒ±%_"[–±.–eÙç[ŽC¼×Þ[Ú[z¾ŸŸ€ã½´öÚ{i?{­ç÷üÂ?w9p]¾,ÉùÜÕÕ…åË—cÑ¢Eˆ7òóó9'ÿК$AˆEø= mmmÌ Áÿëå®úwJòõûÛ‹ŠÒÑi›¿›©×??€Tu²àãsò\'Î]d{¯`uš››­q×ð˜>Ÿv»àœ~‡æózaïî–Ô„…Ë—/ëâ8uêÓ±R;´:ûqÂÖÁtljj*Ž?.ëkޱoO/c˜×¶YëÖaf¯WÛl6øýþ˜›¬b‡¹ž§|ž—|˜CàÆÅ*¦¨¡Kžr4š-‚(Z˜~õ廊#Öï‘à/÷ËåV!w"®!ÁAÈŒi—‡'¬”z_;¼ÃÈPeGìó®u:(ÎÒâÈÖãµÃmøñš1ìãÌÍ%zHWk8;î ÝØ°òäêRD»†ýîQgi‘¢TÐì'nÊèx¿;ÿ) D„ùKc!N¥P tÉZ™SVV3碢gÀ Q*•qY•OŽÈ!ISìÅëðb³wºÊVøßÑ®&ïõzqôèÑ·‡X±VΪÿhgæââ螸ô!¯>ÜÿÕ3É×|9ðÑ9¦ã4 ¼^/Þÿý¨^}f&ŒË–Á`0`Ù´ÐAJ„“¸fÏŸÏ7#€°Z­Q™»áùšŸŸÕ«W_UY8–a¿½—¯šs ‰€J5•ôÈRð1S …búóÄâÝcÍÌç?»‚2qcñ¡P"©k«VÏ|H°:f4IMMÅÂ…×?ÓºÇH«Vaó}kïg]CseØx$œœu£y¥×ë‘uƒ ð7s^²ÏQ¡¸ºråì¹'ÕŠ•áDéÎÎN˜L&&79žžÎ9¿£ÇÅ_ðLˆôÚ ““@Bÿء¡¡sò¸XsÖh4ÎÄÔb †™žÙÓ‰žk¦ÝåÂ1µÝnGët<­÷áp\••…²²²¸ˆ«³³³¡ÕjáõzAÌãþ-*’e¿¿þ_þ d×8¢©©‰ù³®uw¦Ä ФD„Æ'¾xÆÝž‹æóÜ*<ŸAð°çƒÓ蔓½¡°†éçq¸€‡¤úwƒçñ̳¸µ5*ïºñ^܃EððùÈø‚cÐ(U’8‡ã¶´:ûcâz„ç±^¯Ç2£‘“ûY4™<ÝD€žïRÙúYzíqCCCÌ}_Ìvޏn]exˆWÛIIIÈÌÌ”ËPõ `•6O\úÞ{Ö­ù™(_u« çR×І+^¶ï¯@ •Ä,µZeË–Ád2ÍlÉ Fƒ5&ÓÌü½9ÜxæLD«fõööb`` n*á)•Js¾wOŸ·_?ï¦*<+ÀÄÄT%éÉ0WíÚÄÄ©ÄÈð¹ˆfsâÓK#w†=“Ãó+œ2[t8ûw¤ÈìçµsV²¶·y¿£ÌQõØétbllŒ©½Í×"U-lá?€_ø„&Ú¬y4;]6Çü‹µÿeó;œœa› ÛívIÌ)§Ó‰ÚÚÚ˜ ³9„HJLJBÁ¹ŸÉB09$(¦¾XÝÂkMÑt^20Xc2I.¡r¾1uøf?O[§cêH'\:N¼ÿþûX¾|ùŒ˜%–ÉÏÏG{{;ˆ/§²¢B–ý.--EAAzzzè"Ɯ׳Â|³Ò„Œ-BÁI(”W¿Ôffh00ðE…õEÅÜÝÌOžëÂãÝ-عöŽàÆ"‘"\Ä#ü<–’ØËóx®5®ÆÆFX[[#ºÆ5»¸GYYYܸy³÷8ïìÇÚ¼è»ù‚cØþ¬,Ç~v!ž"ƒAÒëNB½‹_{޳Ý].ì6›,‹|¬^½úª÷ööJÊ}O¤¥¥É¥«6‡ÃñºbD¼C‚‚/Û!—h}~/òÔÜ.SU*ôs,°rìüÖÝ~ó¤¤W1aUQ:““Álý>|x© +sò9ë6©Ð3:.˜ó«ËCíÙÏñèW‰êò0ì ÂíƒN£¢ÙOÌÉ©^Ú/Ð@DEBY¹V2ýaqÝ!äƒÐ‰?1õR—ÄïµNAs'b ¨Âç§«\Î7H‘pòÙl!„ÕjÅ™éM¦H-6Ïv{¸v±8`<||¥MÞ~øxˆRTJ<öõ;…yÏ q¼©ƒùøKY{Öû:3¦5k°ÆdŠÉ£Ù›Ã›ªª`·Ûq¦±1b‰ZáJx===qQ•¶  €³à¡ÏuCW|H_0GòA”[C" ê*ñÐ>lˆÏ «a'†°°Ð`0@«ÑȦJÞ|㌙¿Ïã÷ÉpUûð³ËéŒhB‰q‹êšB(¡Pýýl•oÍÏă÷– ÞÏ7öŸdÊ }f&ôYY3s+,Š„Aôï‘$gÌ®o·Ûa·Ù¢2‡bU0ÌStôðÿ„@™ Œ‰TÏ#41õì?pô3fFBB¯ Û,¨Õj¬Y³fF8,ǤÊùÚÛÛ100³ÙSIÔ¬•³øˆ౯êdaâ†CÇ›“®«WXäPn6ÇäðÍ‹«*+*`·Ûq¢¾>"Îá*Ó±¾!œŸŸ¥RɹšrGïe¬]½ÄÀw7Çö}?-j0N'` 1%hãÙ>û¿×@„Ý"ÂÕí£áüt#››{ÝÏûúú˜EuBV„ Ó?83b#µZ¢¢¢ƒ^¯GÖô<‹×¹6W…øp¼lµZ#Z•6,Ž¥êð,â !€)¦¤$@,[¾U£=OÄæ}Xäp­;`<Ìïr³åfsÄÅn·µµµ1íö Óé ÕjáõzAÌMEE…lÝfÏ£×_¿yóMœîDM‘zÞëõú9Ý"Âïß¶Y"ˆh8Cdff^ç’æ÷û188ÈÔÞ]¥Å‚UƒÍÏÞü?²›c³Q–ͱCÌ?^0ã!vÌ&\>VÃ\¢…<@’˜“Ÿ—T«F‡1™L0›Íq'r¸Ù:ÂlñÉúúˆÌéXw{ÈÊÊ5,üL“ã;Ü‹%¦Ö–{ôQ<öè£3"ZBòÔZ¨â}íL%þÏŸUEé¸ôßÀúGpÖ¹ÊÈýà†½Â¾H=ºn^;ÜÆ4&ϿۊÙr—¨ã×3èCA†IŠú ÇmðŽˆÖþm·Ý†7ÆÄ&Ûðð0^|é%Þí|£dÿ*dT©ªþĉ¨,˜È¹n.Úl¶ˆV§›%‚l{úiü÷¿ý[¦ê¿áªräHLº¨ ««K´ö7mÚDñ£Ÿ‰„iä‰g§a)¼k‡™í¶xæÌÑ÷xµZ-JJ®ô5551·÷¥ËQ¤Ë€Í=ˆ~ïÜ£~ô{F`ŠÙk˜ §B ÇÇG4ób <„üÙÆ''Ðὂ 2æõûªkª¤Í‡O»Üx`m·(jÆé!’¢¡ܾ1è4*ÁÚ{åÖï<Âù¸þáQìoèÆCeâUŽ …&q±Æ<ª,Om—pªWw}f&jjjbfcbtt»ÿùŸyW·,.Áʜة$"'kn«ÕJ‚ŽÈusñ¹Ÿý V«•[,àvóJêdI¾'ØÐh4ø‡çŸÇŽ;0|åÊM×PXã²e0 d;|³q2PS]MUU¨­«Cmm­( Ê---ðz½1±¹”ŸŸÏk|¾äé௾¶Z°öš;¸Ô+g¡Éd‹…’X¸ÆØz=*+*f„%¾ñz½1œuêÔ)nïcAÁ0¹pºµ}®+ÒŽÌzÞ]³aIÈ£Ñx+„ËåšÙt¶Ûí°Ûl¼¿s¡R]½788ÇÃÔÞƒ÷”ò®;uQJhžË¥!|}ˆè?ƒÃ‰Ò.— µuu8qâ„(qs81kõêÕXºt©lÇ,==³HØ# sBb" TBŽMðöOKjŒF#, %VJø=8ì¢600S‰–±\)› b ¯×‹ööv¦c¹º¨åg.DÇ…ËÐä(ðg_)„F£‚c±Å“çºpöB“{›ÇˆŠ»ƒZ­Fyy9¹900[ˆv|ຯ3_ºººàv»Éyi6÷ P´8*ý}ëìDu¼Âó–DÒzß»²ÕTWÏ8@X­V466 þyË—/¿*.à\ 'LiNŠtSy|EºŒ™¿‡qú1<ê‡mx£ãAô{FàðŽ óbzZ­ö:÷V ³fA| BæL»<¼`«ÔûÚç÷b±vÁ¼\2R"W3¢‡{(®=·7(¨àaÝíÙX·,ÇZ8û›cذò¤¦ˆ÷ˆéw¢ C#êgÒgt<ˆCmâT 5bÛ¶m1S¥7 á׿ù Î;ǫҜ<Ü™_D7AˆŒÑhä¼0>6ÆžT199‰ÉÉIø¢×ëñúë¯ãÿ9‚O>ùCCSUR´Z-¾ò•¯à¶%K()ŒFƒÊŠ l°XDKøo.­_¿^Ö‰Z­–©ròô ð+V:þYÔÇÎl6£²¢‚6 ¼ $Ö¦p89ËívcõêÕ13nJ¥:n7·õ‹ŽÞË<øA¼sôœäÆÜ’â/æÒ_ãÐáóù®ÚxæúÝsí:A(B?Sÿr2ÒPywÿàˆ(Üõ™™ÐgeÍ84 AƒŒçƦªª«ªÒŠáúÐÔÔ„¡¡!”••Éö™Ë•ŽS’ãHR¡01Á¿­£ŸF·j4ÅÔ⾋-(¿ ›Íf9UB½),ÎiAD–††æc¹º¨)±¸ =—áö`ýWãý?rwyxñí£xã金ªNætÜ£Ÿ¡p$bc«ÏÌ„yZè@Ž¥ü ¯qY­V8xPáC,;/eggsv^:ïìJ_Û:`RÕ{ra‘³ÝàLcã”øáÌÞ±úÂ… ¯ûàZ'LrR,‹o¾ž£KQC—¢¾NL FCãè÷ŒÌ#ÜQ ú%}}äôì{—Üâj(S” bƒí°Pêm¾2ˆ¯¤g‹Òö±óÌÇê4*üêñ2¬ßqþ èãÐáð+…kÏ5@q–°‹ÌÏ>¼‚ÉåÁÇÏë.à÷/u /öê’AÈߟÿTåx¬‰àƒÃ‡ñ§?ý‰WK3³qÉ ºñ"'ÁC”øÏë×ã?¯_O!0b ›KrOôÈÏÏg®’÷e„ÅêdáD!u m‰ÎB¹Z­Æ† Pn6SR–„7…Ï46bßÞ½‚'hµ··Ãëõ¢¬¬,¦*Òr<ô¸€²’ˆõñ·6!8})}f&ŒÓrK"Âq‚Ñh„ÑhDm]çD””””«þít:™ÅÇ›ï[Ë9j>||¶“÷¼1­YƒÁ€,½~Æ­ˆíç°XÉY]]]ðz½(//—Ýs8;;---œŽñ èðF• üŸ·vŸ?€½µÑww ¡Cdæ³X·ÛÚÚZ¬_¿>&ªKggg“à $ÌÀÀg§¥™8{ãZ&µ°è¡«o« ð>¸ úG°ý—µxqëýœŽyç£È Ðgf¢¢²åf3Ýd"`4±íé§E‹­ÃÅ=¼^/V¬ˆ=ÛìlîùCöá!ø‚cÐ(Uë§/8†=vwÐgfÂ2½.MïåòeÉ„5&6UUÁårMÅë2µ•ž~uŽTss3sa«²ü"¤$±§‡E%™×Ïá°",ˆ¦Y€¨‹"ÒÒÒ —Ûç šAq5$x ˆÀáp¸sss_ð¬Ôû:4ÀàXª›oæå¤r_„pûøm&¬*JŸqz[ôp±_Ø ÞÑqŒCHQ g¹µîöl<òÕExë8÷MÒÚ³ŸcÃÊ\Q ÃÞ \#èÓ’éK 9ÕkƒÍ-|õµZ­[·ÆÔ‚Á‰úzüáàÕF¶6 ÷—,§ b˜P(Dƒ@ij…{÷íC}}½`mÇB¢Gvv¶(‚‡•}ýNAÅþ@'š.Eeœ, *+*hC)„7~ÄHÐêííÅ‘#GdïΦ  €sæ¥ÞËë߱ƴ\rDelHà@pÁn·3­„ƒËåbúìÒ%y°ˆ$Bb­àžÿï[ß"·†8EÌä,§ÓSÏá›ÑÑë¼Í„„iÑC€½÷ÿô"X5úZHèYf öîÝ+h€`0ˆÚÚZÜy粯.Í’\IDä`­V­U«x¹¨M‰ôH_ ÁKnÁg?gŠÇ_xû(¾ÿÍ{çõûoìÿXÑäµïÊ$tˆØZZZàõza2™b"¾fu#¶¹‡°,+'bý<|¡¾`0"ŸEn±‹^¯Ÿy7Ú½{7S<Æëõ2ïñ,LQã«E‹oþLLM¼ê߉ Q=·P Q‘…ú‹ßO… ³j6O„€ÿú½è!·—‡}÷ˆCÞ/Þ]žƯžsﯢ¨ …⪵=‰ó¦Ãáè¢YCWC‚‚ˆ^Å”²Oò.-WQ®¿EðvÏÚܼÛXU”ŽO–39Dº¯×â   CØ$œg^wO÷0 @ž·ÿ²å.QÇñ¢ÃC‚‡8ÄṂºŽ6QÚ®©©‰©·‹/bÏž=¼ÚX˜œ‚GV®EJ’’n>‚ˆ,ßC¼6jIð@Ä25ÕÕ(7›±wï^Ø»»i7 Î$nÉQôŸŸ/x›éij<öõ;‘¾@Ø÷’CÇ›1:ŒèøPRVô(ŸÞÌÚ¡Åívã½÷Þ‹‰Š´:J¥òª ®ùÐÑãÂâqïéŽÞ¯o‰èx˜L¦ƒÁ` IDÌ»ÍÆù•ê‹ê‘}}}Ìqô㉷fvò\çcÒÒÒðãíÛIàGÌ$g í¼vI““èáÚÊ™óÁã'Y1Q¨T@pŒ»ÓÃg‹ÊRL›q5ðE"²œE¬15Aâvjd‹³ïÄE-cÜ÷|ÿõCLÇØÐ†»K‹qWéÍ¿'Ï^ècŠßçKصtƒÅB±~ck1Š{tuuÁívÇŒ¨X§Óqž÷玈 œ>Þi?¦¦ø9¾bu§Ó‰wß}—Óq³]F›šš˜cÙ‡þ¬©·^ír­ÀAH” ¯oÿ–\%nÁ‚ë~òL‚ž©u·qÏ$šDÈÿåoÄýžŒŽÏ=.MÃ.8£è.ÁaÛi¦Äõà b„i—‡'üZê}õ‡ÆÑç÷"O­½éïåhÓÐïåVuèSÛoWu·gãWß)÷~Ñ ê8\tx°$7U¸{À=*¸à¡8K‹­K°c3çcû‡Gñæ±N<ºN¼ï@0„žAŸàçMH—Ññ µ‰“K PÄU„—<(X¢´ÜDRë£" HH‚`b¯ð S"‰½ïœ½jô\1ueE=§c8®¦D(++‹«˜š q ƒhnnf:Vhµ•·åá®ÒbfAÂ[œþRÁÃÏ_/ÚX’k©t˜-Bj]zv|]VV—ë\¶á¡ˆõoÏÙÓ¢¶OB‡ø¤¨¨ˆó1aaþÀÀs{Wi1þëC«%1cÁƦB¡É™ÂWþÔ B¡ xR¯—›ÄäØ$Bc“˜›Àd_!B“ÈAÚõïÓêôù¼pöûår{¼Jî17$x ˆÂápü&77w;€"©÷Õ:âFvŠI 7Vˆ¦ªTèçX¼ál—›·à]·ŸÚ†ðúávÑÆàbÿˆ ‚ïè8Fƒ!¤(‚ösë}Kñæ±NØ\Ü+i¼y¼w—d zž×Òåô"w¡IŠúˆŽÛ:0àÞ~]Ÿ™‰ÊŠŠ˜'ŸÏ‡—_~™·ØaséZä¦.  ‚ˆ)*+*°Æd®]»q{sµPÉb‰àÝãÍ µZM›6¡Ül¦I"!ôz=¶nÙ‚3صk— É–±"z`<ô¸“?®EL7–°‹Ã“‰6 A°ÛíLÏŠ0}}}LŸ«U«Duwèèå^1Ôl6Ó¼"n;—›Íص{7¬V+ïöä(zàR1óÜÅ>Qû“˜$«Ð80œ[ø˜0%ŽP$MõçÆ¶ˆ—>3••SK8®¶Z­Øµk—`¦»ºº@¶¢<„ôhnnf®V½ù¾µ‚÷çñ‡îf<\ú’ØüíçÑõù à}¦bÒd¶Q¨ØúÚøZÎë\,}·¹#Ò·Vg?Îô‰S‡„ñ ËÚ”V;UX¸©©‰é3§Ö¥î–̨” ¨få¸-DÊu¿3 Ábt,/ÆU¦Žš~Ü'µír¹5†¼J3„ æ&‘†€ bŽírèäøäl_’´¬Uq·›üÔ&œŠû•GLX·,[´18ks ÞfÏeáÕ¨: ¯<Ê®î}þÐyQï¥Ph=ƒ>šùq@Ûåœêµ‹ÒvMMMLU8ùÉO~‚Ï^mX—Ø âˆP(Dƒ@ă;wî„Åb¤½`0ˆúúzæ Ùh‘Íÿ}ç¶Â,ü·¿(EìÐÜáøÒÍa!°X,xù¥—(1K¬1™ðòK/Á$#[Xôàv»e;&,ó·ÏyE´þíÆ¢V«a6›Q]]7þ韰uËl°Xh˜ ÁƒÓéd`mÞ¸©êdÑΫÿ2÷"Tø2ôz=¶=ý4ªªª®þ°NÊ’RMS$M RÔ@ròRÔS?ORN9<ìùàtÄúd±X°sçNŠ©%NØùP¨wa`JôÐÐÐ Ëñ☠áp»ÝhogKFüZY VÞ–'xŸr2Ò°y#»âì…¹ÅGN_ÀžÃÂ>§Õj5ª««±íé§IìG±5¿ë\.Ÿ¾ øNf¿8ý±àm ±íé§QS]Mk]qŒËÅÝ¥S§Ó¡½½y¾?xO)r2Òd5N*¥ SS“‘†Åz¬¼-‹ 2‘¥Ó^%–¸ÍN8ݲÉ+{Õáp¸i†ÄÜÃAÄÓ.X'õ¾Ú|䩵P+æþ*ÊÕ¦áÇ6´ïùä^m”æä‘Ø â…‚DƒDü²ÆdÂOvî„¡°w[rªV†µ¢åækç%v¾X|žë^§ENFÚuêþÔ†ËÃâUÜ©¨¨ÀÎ;)1K†l°Xð̶m‚ÌÙ±±1=zT¶ð²²²8ÓËÓåA‘˜ˆ…©)ÈÏZˆ¥†,¬Xœ u²gxº;˜Íf¼ôâ‹ØöôÓ$v "KBIjj*³;ÚSó|nò±1 ضmÌ|Wwuu1'jD •JÅù˜ŽWÔûíñ"âî@1µ¼ »=å¢&W§–˜š áéíí…Óéd:6Õª¿ÿÍ{¡Us‹ ´jÕuýÚóÁiAÅjµ[¶lÁÖ-[Hì Cf»=ÜX„¶áAÑúã Žá_››kÏh4â';w’ؘemª­­¹€ÕãÝ“ã¨NV¢0G‡e‹²‘Ÿµð:ׇ3mŸ# Éåt¶ÓÌ ˆ›C‚‚ˆAÇGŽÉ¡¯}~/Çæ®ö©R( eØPÚåaUQ:^~dµ(ç±_x—±ÿ‹³´øÑC+˜þÐyQï¥~÷(Få¤8ÕkƒÍ=$JÛ5551³x¢¾ÌÏR³4'÷—¬ ›Ž â‰TW'ˆxA¯×cÛ¶m‚$z„«ÕÊ–¤%…zQ+yü¼óÑ9q®uf&vîØS¢×x$œl)Äœ•³èE°Ôçæôû*¥ 4(ÌÑÍŠoÉ€^§…:yÊͦîOmÌç NIÁ¶§ŸFMu5ôTežˆ 6÷Ên …‚©ú”.ÉÃÊÛòhà‰˜@£Ñ ¦ºZĬ––tvvÆT¬ìñE½ßŽ~†þÁÑÚ7RLCóyë–-‚%ZÊAÈ$DLM„ð45±%çd¤¡òÞ;Dïßâý¼ „¹«tÑUÿ~áí£ØsX8A¢ÉdÂË/½„5 ׈è±ÁbÁÎ;)î!gÑCzz:çcœ^ñŠn¾ÞpÁ0ù'UUUØöôÓ´öEÌÀ²¾”šš:ã¬Æ•Ò%y×=—b Eb"ô:-–ç 73 ŠÄD {8ÓîË)œu8¿¡ÙA7‡»<&—Žvxo¼Ùž«å^á#°uc Ö-~Ñól—ð/š`®‘€(×jë}KQ¤×²]ç~~^{AÔ{‰\b‡ç ŽÙ:Di»¢¢"fª¨¯ÇîÝ»yµ‘­Mƒeq Ýt‡Œcrr’‚ˆ{‰BU«moo—Åy³$wŒx¢öéý'á!QÌl6SÚœ³$ÚÉr3˜e#øfU§‰‰HU«›™†â[2°bq.–ç 0G‡ŒšõÔ5° Òu:¼üòËT]žˆ8.— ~¿ŸÓ1 …}}}ÌŸùýÍ÷ÒÀ1Ç‹ÕÕÕP«Õ¼Úijj’mZ)Ò?8"šx, ¶mÛF1u Îç;vðžÏ€ô…LBÄÔAKss3¼^¶=ÞÍ÷­Eª:9"ý¼«t¾óàüªckÕª«*i¿ðöQ|ØÐ&H?Ôj5ª««ÉÕ!Ʋ¸G0D}}=søhÁæð NÑÄO½ø¬ÿsAæëÎ;ÈÕ¸ÁŸ9oëR9iX¶(§ååîðÍ ‚ørHð@1ŠÃáèð¦ú:4À@`îMÆœTwÿ£W”~¾ó” 5Ja_”lâ¼€õ úÄyÉÔ¨ðëǢߪ[´sÈå!9ÔÖ‚Àø¸àí c¦™ÝnÇÞ½{yµ‘­MÃ#+×"%I)ë±àêB‰U1ÅÄÄB!z~D¡ªÕ655a``@òçË"xèÇ/Žè¡£Ç%Øp˜ðFpMu5mÇ •‚$[†+à±&YD–à>ç׋–ç`Åâ\,.Ð#'# SS HLœ×÷Ké‚ü|üô§?¥9ID–Me¥R ‡­räæk‘“‘FOÄ$åf3žÙ¶×s8 ¢¡¡Av YRå­N‹"V«Õزe 6UUÑó;F1 xù¥—©.}êÔ)ôööÊâ¼Ybj‚ „# 2 )]’KYd y=xo)*ï¹¹£„V­Â‹[îŸb)v0â™mÛP.@ÁBzYÜÃëõâÈ‘#²Š±YDˆ¶á!(uŠëþ¨ ”ÐÏïOêt«Rfþ(‰x£ñcAæëË/½DBabNZ­VÎÇl{2ñº.ÕÞ=ˆ¦ ýréî1‡ÃñÍ ‚ørHð@±ÍvÃrèhÛÈÜœrl.¯(Iõ|“ýçâ¢C‹½aonŸ8ÖÙënÏÆ#_e·:ûáo?ƒgt\´{‰\b‡º+¼Â[¯«ÕjÔÔÔÄÄÙívüô¹ç8W¦œMrRî/Y.{±AAI¸Z-_êëëe‘<••Åù˜ŽžË¢ôåý'm6‚ã!’-ùUÀÓjµP*¹Åñ£cAd,Ð\'nP)L}8{­âýßüÍßP²$5X6•Y¿´j*cˆi ïç°ÛíFcccLŒ‡×ˆÚgŸ½Ð'¸xxvL½F€ŠÃ„´Ñh4ضm› · ²poÑjµtá "Š4662ÇÚ³"ÉãݶܻJ‹¯úyNF6o\‹=?Þ„ÅzŠÌf3¹,Å B÷p»ÝhhhÍy+•JÎë\¾±1$•$\%XЭJAê´Åóû£.P^%–øÉo?ä]ì'ì4Lk_Ä`)ÆÁBNFZÜ®K½s¼UNÝ%w‚˜'$x ˆfÚåáU9ôÕG‡çzmFª*Z•Šs{oÇ.÷µزq©`íy㢉¬½#¢]¯W]Íìvá ŒãùCçEë¹<Ä6÷ NõÚEi»²²2&}>víÚÅ[ì°¹t-rSÐMGA×Pn6ó=„“§¥Kõ¬sŒIÎ7£®¡ ç. ×®Éd¢à8BˆdK`j3øÈ‘#²9o–Š´ƒW„s…ìèá¾9g6›i^QÅnç¾ÞÀêˆöøCwÏTv%zßœ®®.ÉU„g‰“Åχ=œ¼MJ®Œ?4 jª«y‹ä$&f)@FGGÑÕÕÅtì×ÊJfDÑ`åmyØ^óç¨ûÇïÌüÙóãMØ|ßZQœȹ4þª¸Goo/šššdsÞ,ë\BÆßï=Ç{}Úl6£F€bJDl)ÁÃìçRŸÏ=÷ìÝüæÑý%+Hì@A7AуÛíFss³¤Ï“i#©WØÅb?€_®âXEE¶nÙBÁq†Á`ÀOvî„¡°÷¼•K¼ììlÎÇ)XbÙT6Quh"ʸœÎˆ|Né’Lbb¾XÜG¹¯ëR¾Ñ Üܶӌ ˆùC‚‚ˆý…7db}4>9ëÈõö¶¹©iœÛ²¹¼8v~@”~òMö¿–³6ñ,}/ö`<4)JÛ¬-À#_]Ä|üÏë.ˆænA.òfÀ;‚á€ðbµZ3 {÷íã-vøFÉr”dfÇÌ}£T(hòKü5>Nƒ@_‚¢‡–– Hö¥P¹öŸßm¬ê|uu5*+*èæSôzýTbž¢‡®®.´··Kþ|Y’³<þ€`ŸÏ"~ZC‚"Êð}Ÿž/›ï[KƒMă555ÌǃAÙˆ¥„ÇÔÝA­VSLM˜=TUUñjÃétJ¾ ":x½lì¼§TÒÕª÷|pZ±ƒV«ÅÎ;PZZJ7KœÇ×;(îÑÔÔ·Û-ùóe÷_FððÂÛGyObb¾DÊÝAêâ@±øã©‹p ûäÒÝ;Ž.š1Hð@q€Ãáø ›úÚç÷bdüê N… tL‰¬¿9&žJ}ÝíÙØ²q© m]tŒˆÖÏPhRT·ƒW]ÍËíâ»o5Â3*N¢¥Xî„|©©©^¯—ýyìÚ½õõõ¼Ú¸3߀•9ù1u}óÒÈ©‚ X˜œdFêRÕX˜šBƒHÄåf3ïÊ–§N’TµÚ«æ3ƒÃƒP•³àRße|pò<ïvÔj5vîØAUïh4ADMMM’+lÉYB –¼þ1N¿Ï÷š_"QA¾VV‚•·åÑ€qÉ“‰—`¸··WòÏ_©qàèg‚ÅçjµÏlÛF151Ë<Å/R/ÀâšFDtÈÉH“´°¸®¡ {ó!äçãÅ^@!½Cf+ ¢¾¾^²ëÓ|žÉâà=œÆ¥^öõ²;Äà¡òž;°¸@cëö¡¶¡C.Ýð*Í‚à "~xB.µŽ ]÷3Ãî 8oï„Û7&Z?Ÿ}x¯dÿ0ýQdzwÐ'š¨€¯Û…70Žþîœ(}ëô‰ænAÈ“ÉUDkëêx‹Jsò`YlŒ©ë»0%¥Šnt‚ˆ0ɪ$ß’eÅ9ÈÏZu²’…ˆijª«y‰¼^¯¤«Zfeeq>æì…>ÞŸëñðý×a’gèNÌ2 t³„=Ô××3WŒ,Á,® 7š¿œ¯ UÏ%¢ŒÍný3´jUÜVÑ#ˆ0|çN¢Aœ'ýƒ#ØT˜5vŠ©‰QYQÁ»€”“,Éá 䃔ãì“ç:ñ"Ï ñÀT•ø¿ÿû¿‡F£¡ NÌ Ä:—×땼›šJÅ}¿—¯ÃCÿàÞùˆ=ž.**ÂSO>I7)1oZE.Æ¡U«âÖuôã­ð‚réîv‡Ãá¦AÜ ÁAÄ ‡ã €crèëÐXÿU?+\˜ÎÔÖ›"º<ðMöŸÍ§¶!QÇÔÚwE´¶ùº]œµ¹ñóÚ ‚÷+š„k$@“Ÿ€>33&**œ¨¯Ç¾}ûxµ‘­MÃý%+bêú&'%!'•Ü‚•‰‰ Þm¨” èuZ,5daYq stX˜šE"½n±Ç¦ª*^›Jííí’­j™žÎýKˆ*²ÛY‹¿¸ÝPXˆ—_z‰³ˆëo«Õjæ6Âð¤ ×-®® 7‚Å)Âh4ÒID•HTÑ{ðžR¤ª“i°‰¸§¦ºš9v–ºXøf8G"úyo}p¾QþÏvŠ©‰ùÌi>¢‡`0(Ù$K<„<(]’‡»JI²o=.¼ تÄ7B£Ñ`ëÖ­¼Ö¹z{{ÑÙÙ)Ùsdq"æ[Øãý3¯•efdàéü€nN‚v‘‹q<þÐÝq¹.ÕjsáÄ9»\ºks8äî@ P AÄÛåÒѶ‘«EŒ… tP*œÛyåßÚDí'ßdÿ0®+c¢öÓ;:Ž.§x)_yÄ„•E:æã÷ŸêF}›Sð~õ úhÖ¨©©‘}»ÝŽÝ»wój#[›†GVÆ–’?9) Eº (èF'FÆÇ…uR)ÈX Añ-X±8K YÈÏZˆ…©)P)4à„ì"yúÔ©S’¬jÉ’àÁ·zÖ oŹ‹ü\" ……ضmU½#n:oŸá9oÝn7ššš$}Ž\¡… äú~-&9iq[E æ‚OBV{{{Ô]–Xœ”øÆÈ\Ÿç6ðß¡˜š˜/5ÕÕ¼¬½½½hoo—ä¹‘è ¤TÝ<þ¶ïªå]\€ÄÄ|Ðëõ¼×¹ššš$ífÉÂg/ôáä¹.¦cSRRðÌ3ÏP MpÆåtŠÖvé’A3 Ø ÁAćã#oÊ¡¯þÐ8:<ÃWýlqºžs;Ý—}øÕÑK¢öõÙ‡W HÏo!´û²É"'Úœ^xFÇEkÿ'˱P£d>þùw[qÑá´OÞÑqQÏ™>‹EöDív;~úÜs¼ÚHNJÂ_®X…”$eÌ\Û´äd;„ P'+¡×iQ|K–ç`Åâ\,.ÈDnf2hªV‘!;ÂÉÓÌ1ª×‹¶¶6ÉKõ,> Óu m¼“³(1‹˜÷½b0ðÞ nooGoo¯$ÏÅ¡… â«Õ*jûRMÂ"ˆh¡×ëQSSÃtl0”­ËC¤xý·ÇùÇISÙºe /çÃææfI&YÒ iSyÏX\ —dßžzíoT;œâ·éu.V¤ìºÄúLfƒoìÿ˜¹Ÿßþö·¡×ëé†$8cïî­íx-Âqâ¬V›K.Ý=æp8ÒL 6(³„ âíré¨ÍçÁøäÄÌ¿§g2µóìï?µŸ: ¯<ºšWçìnóÓDÓæîaŒ‡&Ei»8K‹W11ï ŒãùCç(ËCüb(,Ħª*YŸƒÏçÃk¯½¿ßÏÜFrR6—®….Y×U©P ` t$v ¢HLDª:9i(ÌÑaq+çbåmy48„¼â ƒU<⌖–É%x°$L³n$uô¸ðâÛGyÇz”˜Ep·¬ —a$™œ¥Tr6Ÿ#‡"q¹ÄÝø,]’‡»JÑ@Ä5¬1™`2±­wuuIºúl4yûðit÷»)¦&"ŽF£áåÞ"Õ$K„tѪU’Mà|áí£¸Ô{™W$v ˜â8ƒÕ<î§ÓS®K—µº†6æùk±X°Æd¢‘àŒ˜…8¾VV—û­¾Ñ ÜܶÓL vHð@q†Ãáèðc9ôu|rÖ‘/ì3Ôäh¹‹ú†üø_µDíëk pÿÚ|æãÏÚÜÐiTÈÏwc! ÁÚwE´ö]·|•}c·£ßƒç´O®‘Mü8…o2SÔ_Ì|><÷Üsp]æ·Pú—·¯BnêÙ_Ïä¤$Ü’¶K2ôHKN¦œ " Ò #,æÄ-’KðP*•œ“¦YO½~ˆW_³ôzJÌ"˜Xc2ñ+QrAÈ›Ý.jûßß|/ 2AÜ€šêjæähry˜;ûgxµAb‚z½OlÝÊ|¼Ó锜{‹ˆ˜ ˆÈ°yãZ¤ª¥·/$„{)‰>”›Í0›ÍÌÇKÕu‰EðàõsÏ ÙóÁi¶8(3•tLˆµ6¥U«âÖuô§.Â5,›B¸o:Žh&;$x ˆøäUÃrèhŸß‹Á±/^Ng°¹<üô` Fƒ!QûúëÇ˰Pþ úÛOì(ÎÒ"Y©µŸ—GèrŠ÷âúÊ£«±²HÇ|üÇm.ìoÎÂ-š„Ã=J³>Ψªª‚Á`õ9ìÚ½›·á7J–£H—!Û1Ð(•ÈIMÃ’ =nMÏ„.EM77A!)ø$nI1ÁC§ãÇwôp«–½ý—µðúǘû¨T*±cÇJÌ"˜Ù`±ðÚ –b<–䬎^Ý DÜaQðPyÏÈÉH£A&ˆ Ñh°iÓ&¦cÉåázžù§ÃÄ»“³Z­ÆÖ­[)¦&xa4QÁ#Ù¯¡¡AR…8X’+ ‚ŸœŒ48sû˜û—€-û·´¡Dð†Ïf0 ½ x,ßÝDÜaµZÅy~ªUØ|ßZ`‚øÊÍfFæg/1E]CºûÝÌÇ«Õj<³môz= &Á›ÊŠ æy %5·Ið@Òä©oJÏEÍãà…·ò*èAbBH¶nÝÊ\”§··’:–¢<œcê?µ1Ï]ÖØ‡ qŠqÜšŸ)Iq`$ØSw¾@P.Ýý±Ãáè¢Y@ü ÁAÄ)‡ãU69ôuh,€>ÿ‰‹ÓÙâùïh쵯[7–`ݲl¦c[z†ñÚá6è4*d¦‰¯¼µö]gt\”¶‹³´øõãe¼Úøáo?Ì™áòHã¡Išøq€Z­ÆÖ-[d}D}}=¯6Jsò°®h‰¤ÏS£T"C­Á-i °(=%Ó‡,M*4J tCD§A žFXÅ–^¯’9–ù:8K½—™72ÇD1z½555ÌÇŸ:uJöcÀÅÉôì…>¦¢µ a4¥ýÖàG±;^xãøáï„såpSõʨ0::Š„&®ËÝBòD}=<È«"]:î/Y!™s ;7Ì7,ËÊA‘.9©iÐ¥¨‘’”D“… ‚5•Ì MMM¥Q†¥Jü|ªÙyülßUË«oßúÖ·Hì@ _‡©UÀ㚌É'9ƒ äˆÏçƒë²ð÷ý­ù™°”•ÐÄ<1 0›ÍLǶ··G¥Ï\+ÌŠ)xxáí£ð²¿;TWW“Ø%®æ“dÙÐÐ ‰óÈÎΦ‹Iãû›¥çîpò\'|ô{,TXˆmÛ¶‘{)!8kL&^Ey¢kÏ…ØN¦u lî6l —4‚b¸;ÜUZ,Iq`$x»îœœº»Ýáp¸iHð@qŒÃáøÀ19ôu|r6ïÔFJ¡À2}S;µg?Çì¢9Àª¢tlÙ¸”éØa_ß}«IŠóˆ>®¡Ð$š»‡Es?xöáÌŽÐÑïÁÏk/Ò×U¯Œ˜œŒŒ»†ÅbÁ“I¶ceµZ±{÷n^mdkÓ𷯊Ú9( h”JèµZ,ÐaI†~Æ¹Ä !ÁøŠÜB0ø$xƒA´µµIâ^­V˾p!ý8[Ÿ™Étlss³dbm–5j.óøCÁƒZ­ÆFA A„± ¼6¥U«âv]ê§.ÂÞ?,›Kïp8ÈÝ ‚2΂x@“:Úὂ<µjE–ésÐêêG0ÄÝà§ÎØ·÷Üž$‘\ž}x~ùïLî ‡N÷âÝÓ=x`mj•öŠûbéǧ¶!¬*Je<ÞyÊ Ójas±Ùï?Õ•Å:˜K²xõÃí Òl0n·‡#"Ÿe(,”µ…¤ÝnÇ«¯½Æ«ä¤$<²r-R’”é³R¡@JR’“’ Uªœ¤„"‚nAð'R‚4‚ˆÖ˜L0L–Äííí())u3g>ˆQ=ëì…>^•ïÌfsÜo&Ùív´Z­°Ûí3B¾±³¡¨ƒEŒFc\oeEÅŒx„óûôt¼¥K—ÊòÜ=þRÕÉôNÄ6ª¢G’A¯×Ãl6£¾¾žÓqÁ`===X´hQÜYG {Ÿf>¾  >òÝ|Ó¸\.¸\.8§ÿ޹}>ßM ÇÍE4 9fÌ¢¦ºß{òIøý~ÎÇ677cýúõ4ˆA˜JàÜ|ßZÉõëÅ·ÎËéôF<±u+=7f^‡iµóek3†éçoøïÚég1‰H0£ÔÔÔ๟ýŒó±á¢<+V¬Ä¹(•JNŒsûæõ{ubwwˆçûÌçóÍ’°Í#n¿÷eM;bPÜŒ™w!yðžRɉ#r?Žqà˜UN]~ŒžP!$x ˆ8Çáp|š››û&€GåÐßæ+ƒøJzöŒËùþ>Îmôâ—ÿ~¹º¬(\(J?u~úÿ–â{o±iIþú\úÇlóàô¥A„Bâ&$Š)zÐiTxç)3Öï8‚a›èàùw[±äÛiÈÕ¥0÷# Á3:ŽÔzôEŠÎÎΈ}–œ+¢ø|>üô¹ç˜6‚Â$'%as©xb7DlbŽñ%1Iu5ž|ê)ÎÇIeC‰Epq3ç?€yT¾3bSUUÜÝG>ŸoÆ•áÌ™3¼âŰwwÃÞÝ}ÕÏLÓ‚5&S\Z³×TWãï~øCæä¬E‹E]°¤R©8ÓÑs™’µ‰¸ÁNUôBRTVTp<SBáx<ð©&””„ÿùÌ3q{¯¹\®ñ°Ýnç%¾Ñ±³ÅˌƸMæÒh4Ø´i“‹±ÓéDgggÔçwVVœN'}ID”Ù¼q­äÄù'Ïuâä¹.æã«ªªâ¶à„Ïç›y[­V¸œN¸.³¹VÝì9nœ~‡ŸÇñ¸¾qq8Ö–BQ`ÊÉTŒgr¹;D4~¾Ñx˜E„çê²éïÈÙ?‹5„\›ÊÉH“¤80¼s¼¾€l Þs8QtGÂAYŸAS.J½£Cc Ž¡JÆ2}:†\ðŽq¯¤°ÿT7î6ꑚ’„â,­(}ݺ±ûzðq÷±a_ýFV*‚%N‡ÞÞÞˆ~fªš»ÈÂ.B•}‚˜×½g³ Ú^¼VÑ#!Ÿ½,‰Xn·n·:.nÆjϧq©÷2óñßþö·ã®mX<Üxæ sB%§gÌ5‚bµZeË–Ád2aÉWã_>=¯YãZZZâRÐDÄÕÜšŸ‰ï-•TŸ<þ/ñ¡ÉdŠ;÷R»ÝŽõõ°¶¶^WtC,®uïÔgfÂ8ëyOlªªb*¢"5—–¹z3±TÿàS\]^^óñ\xÎF"~öûýsÆŠ×î¨ÌÞ— ÆÂ¢¹:»Ø\›zê›÷Æeœ`ëFí©9uù ŠîBX(ë“ 8wnn•C[® ¢\ T VæäádwS;?üígX’“†Ô”$èÓÄ©qè”Ãðÿ‚70ÎýØÓ½x÷tX[×HÃ^ñªÞÑqüé¢ «ŠÒ<ºn>:?€·Ž³Uýïè÷àçµðß6ÜÆþ’;:N>Œ£««+"Ÿe4e½HøÚë¯ó^èûFÉrÞb‡°ÀA«TA£TÉr,mîAÎÇP’®¼èèuq¿·ÉÆø¦LNNÒ Ä8v»­Ó=v›y¡zöôµ ÑáD£Ñו,g³ÁbAmm-Ó†’*Zj4N‚‡~o÷¸pà£Ï˜¯©©‰‹gµËåBm]Nœ8!¸“ówGw7öíÛ‡}ûöÍlÊÇCÂ5Ó.,ÉYRª€IpŸ£ó±¼'±¾Û„"'# •÷ÞAƒƒ±sø;Ê6ëïsÅÀs …àñx gb˜E‹aÕÊ•3‚aâjX]:;;±zõ긣þÁ¼óÑ9æã7n܈²;±²Z­8Q_/ŠCË;tXP¼S‰®&“ åfs\\‹MUUøá~Äù8¯×+‰wb‚ ¢‹]ÔÞØ^ÿÓ±úÌLÔTWÇM<©„éùàº|õõõ¨¯¯‡Z­Æš5kâFü ÑhPYY‰}ûöq>V*k\b8™Öý©©/±*Xòù|8Q_ºÚZIÌÙ¹bêk÷¥®E ¡Ñj¯*Ô¥Õh$¹–ír¹{O¹«´8n]{ß®;'§î¾ép8>¥èŽ „…A„yÀcФÞQhža,N]ˆÅézt ^F¿—{ÕаkÀë½koÍ@ŠR!x_uöü÷?Ã/Õ3ÿ×o4àÒ?fØ· "ó’ MâSÛV.„N#lâó¯/ÃYÛÎÚØ*ÿï?Õ•Å:˜K²˜Žw{ƒ4Ó#@ww7FGGEÿµZ­[¶ÈvœvíÞÍÛÒ²¸+sò9§T(¦J†F©BZrrLÜw¾ ÷ùM‚‡è2 AÅáÙëñqßP „’›3>NBÀXÃårÍT²lmmH’ÇìD`jÏ´f ÊÍæ¸ƒ6l`rykEËŽ×u‰Ïoì?ÉãX,1¿ùØÝÝßþîwøì³Ï$ÝÏðüÖgf¢¢²2æ“´jª«ñw?üaÜTÀóúôð$â¡Eè®›V”Ýû´ÏóbqŸÏ»Ý>#d;ù¼^Q*Ά]Ô¬V+>üÅ;ja! EE0 $ÆÔº ‹Ø0žoìÿ˜9ÁråÊ•ø¯õW1?·ÿå_ÿ'Ož”ôG8¦Þ»w/6lØ€r³9¦×% “ƒ 0•dI‚‡ù?kÂÏ笿Ï&\9ìB3׳)ÞŒ7{>·^óÿ|>¬­­¼?SŠ œg/ôáÆ6æããÁ½4œ0)'ü~ÿŒøAŸ™ Ëôó8–¯Í‹…)‘=–L?>ǽ0§ÑhŒ¹¸Íç󡶮ީh“ÔïÜè™e4gžßz½Yz=ôÓ"MÀµ))Š#Á™¶Ïaµ¹äÒÝa»Aˆ ‚0ãò°À¯åÐ_›Ïƒ"m’±6¯ÿvá<Û"…Í×?hÇÿx`V¥#I‘ x_X[€u˲q¬u€{ä â¯ßhÀ'ËQ”¥…ÍéÈø†B“8ÛåFIÞäêRmûÈÖãÖ¿}Ã>6ñÁóï¶bÕ6Š@0D“]dFGGÑÓÓ‘Ϫ©©‘íBÔÞ}û˜6|fSš“‡;óç¯QKNJ‚.E4U2” ݬDÔML ÿòœn/dÆT¢ADƒhØƒß ×å˨««C]] ……3›GñFeEêOœà¼¡äõzÑÛÛ‹üüü¨õ=;;N§“Ó1žk’°êÚpîbÓç QYQ³÷F{{;víÞYõÛuù2vïރĴðA¯×3 –¢]µòÝ]¥”PFÄ>Bn*—.É“ô¼Y\Éùl·Ûc6ÒjµâÀÁƒ¼ N»ww_³Ç[¥Ù¹0›Íœ¯O0ŒzÌ Î^èÃÉs]LǪÕj|çoþ&fÇÆçóáýû7>|²é·ßïÇÁƒqðàA˜ÍfTVTĬ𡲢‚ÉmÃívc``ÙÙÙ²yž‹ç~ö³9ßu5Z-û}Ï Ò;xÍ犊âú™s£X!ìŽêr:àYŽI IDAT%Yá:ž¦»ÃÇÌÇVTTÄ´Hæ½÷ßÇáÇqŠ$®Ë—±oß>8p6lÀ‹%f…555s>“¾ ¹:™Þ¬°Gÿà.õrÿŽ7ÇØ¨ÕjÅ®]»âæy~× ìš+6 ‹"ЦEbÅçB㸫´9iq#øFƒrswxÕáp¸A„àà ˆÇorss°Nê}Ÿœ€uÄ 2¡Ö 4'çúÙjöŸêÆâÜT¤¦$Á˜·@”þþêñ2˜žþ#S’ÿ¡Ó½xóX']·®‘¼£‘«TÔÖwnß–ä¤ &ÑiT8ðd9Öï<Âö¢Çó‡Îcç_–2ïö î\A|Á… "RMKÎUOÔ×£®®ŽWK3³qÉ—WÖ‹¦¨¡HH ”ˆ:ÝýnŒM кûÝXVœC%ÆÆÆhdJØÉAªVÃaìÝÝ3 Òæòò˜Þ<š‹ŠÊJìÞ½›{ ÞÖ&ëä-?Àk3X΢֛áóùðÖž=øä“Oäýý3KøPµiSL&Êl°X˜KÑ®€§ÓéèI7‰„âñ‡î’ô¹’ ü öîÛÇ{ýElfWšU«Õ(ŸŽ™ãÉ‘²ÜlÆÞ½{9'E÷ôôD,ff^ï€Æ•ß>Ê|ì1\MúÀÁƒ1Q6<÷+**bò]™¸¹¹ëׯ¹{7ÚE*¢»puóx\§™ÖÖÕáĉ²ÿ.‰%6o\+¹ÎwŽžcJ¦ªŠÇjAõõøÃï÷ð°¬Ï#,D¬­­Eee%6X,1w­ŒF#“£š\´ "Á›öøøl'S?biíSïÈÑˆÍæšbˆ!„*Äpò\6?»›ï[ KYIÜ\¯?žº×°O.ݵ9Ží4ËBHð@ĵlpTíó{‘—¢E†*Ëô9èrÁ˘¸÷‡Z±$' ©)I(È~q¯8K‹g^ï½ÕÄtüwßjĺ۳aÌ[€3—#:ÎýîQxFÇaÌ[Àäª0ënÏÆZû›™Žÿ¸Í…ý Ýx¨¬ó±£c€†&º¸ÝnA“n„œ«þžildJ|œM¶6 ÷—,¿áÿW*X˜’]²šœÉ0 ¡»èº àcÁúGâ²A°pb:!B*jç‹ëòe‰o¿cÅ* »6g"&:JôúþûÁR˜^øÃ^áA†ÝîYQÆ“&¸FÆá› i[OMÍ Ã1 ×È8 V¯Dœ*øÌñ¿€ö\hê>˜ýN£F®!¹™ÉHOJ@nf “« -Ý.´v»xêäî@C‚‚ îÂáptedd¼àeêÛ1z 1Q+°1}5z†Ýó2]Ë3>‰ú¿‘¾*Û RE¯kNª/«ÿNSùc´c׿L”®KÆåƒ˜šš y{{ü¸|c™IZä¤g%¨×ªñÂ_ÜÿñK¶M£Æn7,mý0ç/ÿ^‘àA|&''qýúuÉ?‡×MB¯×‹£G-vØ_´ñ+ªèh¬Š‹C’F‡è¨¨ˆìw1ñ+ ÒG#.#1ñ+`¿F‡_r¡¹Ã!¸ ¹<„ž‰‰ j™sº¦†) "O¤•tx4ÖŒ–~¿½½½\M÷ ŽÜs£ü^”——+& '2Ð.†­§ß{é%<þøãŠ8Üguyp»ÝŠÍ8Mˆ‹Ëå‚Ë5{`çñza³Ùü»å’cddÓÓÓ³ëÈØX$%%-(¨Õj—%TÊÊÊ‚n‘ Ú””Ù¾Ëm éI س³ˆ:­ÌimmUäüÙ50€#?ú‘âÅÂæíÛ;£Ùív+ªF}ã8u®‰©¬ÉdRDPV€'O*>“ôBø|>Eù`\:;;‘——G/¸¡TÑC ³5‰äËÁ½ÛdW§ z(È ÞbaJ¡l==øçï}•••Š˜_±º<ô÷÷Ããñ@§Ó…¼Îz½^´k±¸;2ôóLUu5‰"ŒÓç>AÝÅ6ìdó‚ûX×n2áx|hº~M×ïK³â‡ä»Äbò‹÷¹rwxÛáp|L=‘ ¤…A,Æž-÷ŠNÎL£uÄ•I€‡rrñÛkŸÂ?ÅØÞÑ7ŠÿöÖEü?‡¶ÁdL½¾‡ÉǯÿdÇù'SùÝ?©‡õG#'U‡ÇhXÚ|jjÝý8Üc(0$@¯U3]ÇáÃý™+ñÔWŒxû£N¦küø×-Øôl"âãè•.zzz09)­ãˆF£Á¡gŸå²}ÄÈœPž›Œø•ЪTX§>N‘}-6%*}4Ô)1ˆŽ‹¢Á§ Èå ºúöA³Ò ))ƒå|¶›ÍÌÁ¼F}ã̇ÁF1Á{6› UUUtòäIØl6ì{üqîY]:;;Ið@ܦµµÝ6l6\.º»»C’©ÚëõbhhhY+¥Hk¡÷ûb" ­V‹ì~Œ ¢E„¬zÏ?±“:2œ8qBÑ߯¦¦.—Kï×…(1™ <Oذ¤âÍ÷.0eŸÕh4Š ° 'GúœZIcžuMÜÞÞN‚‡c±X••¥¨ìô§kj(ØSÆlÊ[² ÑË%˜„»víRŒëe$íO/†ÏçñcÇpàÀl7›¹ÿ>f³™)ë|{{;÷"ã»pÑïgW¬ÖˆÑŠÇ7·N]À…¦.Ü»õBãûí^ï̾Á‘Y7ˆ9­]ôää’‘›9+†`qM=õQ \Ã^^š~äî@!¢C ‚X‡ÃáÎÈÈ8 à—<Ô÷¦ÏƒÕq:$©c¯ŽÅ¶5FœëbÏ8ßÑ7Šÿzôøí?î@ޗľü÷ƒ¥0½ð!†½~á³$¯{>=¸FÆ1ìñ‡­ÝÇýShìrc•N…œT áÃèØ$®÷žÚaÄÇÝChìv Ÿ°Oâíóø»Šu4pÃÀØØººº$ÿœçâò`GŒÌ »Ö?€¯då"I£…*::²&ªŸ»8„„²¼å%ÁCñûýÔ2~wDÚFt ƒ¥Ræ“’’³Ù,ø¾†+ƒËç5^»‰v¶l‰ûöíSDÐ^kk+ÞÒÕKz½:z½jµzѬg@ÀÀOHêg±X`ëîÆ‹/¾Èõýeuyèêê‚ÉdZ2³>¡L\.®X­°Z­!·°—ëóq!ÄY¤$'#%ua×Ï@p‚õJp™à¶åÈ.‹¸›z‹%"‚•ò~]lΜ’œ,øët:¹rE»ÁXVTT(Â5Ïf³á‡GŽ„lNMc^ÞkbÇrç´´´4\½z5¢ûÝéÓ§Qb2)â™ÒÚÚ‘N1<ñÔŸo‘]Xz¤$'+B,äõzqôØ1ZËÎ!ƒ÷}kÖ=®ÎÎNn}ƒ# þ¾£W¸óãzŽ^¯UUU4x#œ¦ë7qðÇ¿ÂþG6cÿ£›gÇÈÀHÈ>×ßÏùN£F®!×Ý)†X —Û‹Ú‹<5ù‡ÃM= ¤‡A,ŠÃáøŒŒŒ§ìࡾm#Cx09°f¥Eé«ÑÔw“ùz}£øæ¿œÇ™ï}†$q³©ç¤êðò· ñ÷ï40•oìvã¯ß¼ˆª¿Ý‚?^wajj&¬m?ìñ£Ñ³|áØ ­7oÝQï|ì~Tþâ<ãÂÞ»Ôƒ½¥k¡£b®]»&ùg”——s™EAŒ€Õ­YFüåý›"gbú¹ÀA¥†Z¿Q1äâILø§0xË‹¤•Zj "b‰ôÌYJ9Î}êÄû—íLåßù¨›rôØgÎÁÕžaY܃¹Â‡ŒU¤$Ä"&ú‹€åÉ©8†}èê÷Ü%ÒÈÐÇáéFük[ýßÿ?{ÒD#6„¸Ýn¸\.I?#kÍ.íØë-– V·gçâ™Í[݇TúèÛ"8pË3F‚‡111A 3N×ÔÈJìpŸá ‹ÙùRR]] ›ÍÆåûÿ^0e¬å%`Úãc{¦ìÛ·û{+¥ØA§Ó!''F£QT§´´4¤¥¥!//~¿½½½èìì”ä°ÉÖÓƒ9‚zñEnE%&Nh4‚ Ið9x½^œ8y2¢E‹Jeÿ#›É…Ž“w±Ð9ïØzzPU]CÏ>«¸9³PÁƒR‚e"=ÀRj±ƒN§Cjj*233‘––&Ø…kn æ|ü~?œN'œN'ìv»$—J=dee¡  @° Ñn·ƒ=‹»wíâÚåÁår‘YÆÄD¯À뇓]½XŇJHè!µØA¥RÝNâ‘––Æ´Ïu/‘b@ŒHî!ÅûX ¢‡íf³`Á0»ÇÁƒ4^»É4¦y˵µµ¢ŒY£ÑTò¥úÌ\S@øßï‡ÛMÉòÅâ†}ü+<°öK²¬Û û.4uÌþne‚-WûqÏQ/#ˆ®%¨ ‚¸‡ã㌌Œ·<ÅC};FoÁ Ñ!&j`óê, ú|ó2_óÚg#øÊ˿Dž|}Iç¡üò`)îû?~ƒa/›2õïßi@Nª2õ°zes†=~ {üh ‹‹¹-zöÜû{î-]K[?»…/^»Ýø¸{›²ià†ˆ––I¯¯ÑhPYYÉ]»üç™3xçw‚ºFÖªD<¹q³¢úKt\ÔmqCà â®÷Ç覦§½b5Q\±ZQSS#«:Ü» ×­¾s®ÝëBßà:zÐxí¦dBˆºº:dee)Îé%ƒV kèb™Íx¦  €ëLY€tb½^âââd2 ^Fx<tvv¢½½~¿xÙ“|>~xä~ðê«\ÌhµZlß¾]p¦’Ç/ñ6› UUU!É€I„–ô¤ìÞù5DªØÈjµâÄɓР—˜„'³ Ĉ) ud;(!ÀRJ±CLL ¾úÕ¯J: p Ãív£³³Stñƒ­§GË/¼Àí½6›Í‚~¿Šqrám½»{×.në…IJ%1AƒcÏï¹hE.#>ä=¡‡”b‡ÌÌLäääH,C>Çív£­­ v»]Ô=.ÞEZ­f'â@; Êel žcggs;žƒuwÐétذaCHæ^ËqZ¾Cþ7àðâv»Eß‘À'×?㢞êX®DÞçG õ.‚$x b9<`€Ur¯èäÌ4ZGÜ(\™4;ŠŽFyn>~{í*ïÁyR‰’““ñµ¯}-,ßI§Ó¡°°ùùùhkkUøàóùpôèQn³ÒV”— <(yü³¸\.I³Qáeÿ£›e„E,<…ÏÎG¥R݆žd1÷ÿ«Õ껦N'Ξ=Öï_WW‡‚‚&¡€\aÉït:¹†õãÔ¹&¶95ç–R;;|ùË_¹ø4 ^...Fgg§¨Nj­­­¨ª®FåÜ®‰Oœ8!ø~Ûív<„K}=ׂrw?Ú85ÒãáŸÝWÈHY‰G,@Ùæu²¬/«»ƒzTUWK"vX¿~=ŠŠŠÂòôz=JKKo çÚÛÛE"ž8qÙYYÜ:™ng<³±<¾û„ xv8²Ô×3—ÍÉÉÉd’°%°6_L@ÌFðî±bÅ DGGGäyn||<¢£¹JœIîbHð@Ä’8wFFÆ^桾7}¬ŽÓ!I={(©ŽŽÆCÙkQw£ þ©)æë¶ØoaÇáÿÄùÃ_Uô°ãþ4¼´·ß¯™©ü°×²ïŸÁ‡ÿô¢££055ÃuËÐÇáéFükÝ5ÁeÉå!4LNN¢³³SÒÏ0™L¨(/çª]ÚÛÛñæ›obf†} jU*<³y+wb‡¨˜€{à ¨?9+·<$xšééijQU]-jpÇ\{ðÌÌL¨T*477‹`1—xM,¶±µÈˆƒ{·¢îm8}I Q]]ÍõáÑ]ïy­&“Ið¡»ÝnW\À´Ùlæú¾^±Z%;Àƒ>öï§R©$>Øzzpäȼúê«ÜÝ󔔦L%Ž_â Ž=Jb…R´v5ÊKó©!8y'³——‡ÂÂÂrà a´¶¶Šv d0`0¶)¯‰ÅžEس³§Î6áø—™‚væóÃ#GðSp±ñ¡ÎL*%<Ûl6É\Yòòòî ¨ë|oŽðÁjµ¢««+øöëéÁ‰“'¹tø0›Í‚ƒf”8~‰YN×ÔH’“ûÝLÀ ,Ù+u:]ØÄhkÒ±ÿÑÍ8¸w.4uâÔÙO‚€õù|¨ª®Æ¡gŸUÄ=]_P€e™Ä´´4ÁéŽ;£^ág¼Šý‚I T7-•Jµè»]®bˆ+V`ãÆw<·¤>lÊ[rW£ÃîBGï€hÉXHHH@TT/ÝkÀašEDž“ÔAài^*:2éG·÷ΉX@ô  Òþê›;_9ƒ»‡D­ó©çÍX¥e_ 6v»±ûõz$Æ«ÑÙþ®bs;ÜëÞà!8ÆÆÆÐÛÛ+égTVVrÔèõzqäÈ‘ 3tý÷ÍÛd-vˆŽ‹‚&S…•…qH}Hý¦8h2U²;x|ã4@Ä(ÝOI™ BüIˆË‰'‚*¯Óé°aÃ|ã߀Ùl–ͦôžE8þÊ>ì~è ¯åóùpôØ1ÅÜóÆ Þƒ¸æb6›¹=4òz½¨ªª’$0+ .#*• ¥¥¥Ø¹s§(‚Œºº:æ¸p_F#¸œÔîxxÏnÍÛ³ ¶¶–B¡|½4ŸÍ8‹,£ 6È¢þ[‹ŒxýÐcxíÙÇpŸ!9¨kY­VæLÖrƒ%hçw`ÝŶˆtw8qò¤¤ÂÁ¼¼­ãÈI$ÒõŽßa9Kˆ 9<ȃ`Þ[¶láÆbxãºÕ8þÊ><ô}ܰ0_§¦¦%&“"Äj&“Iðýw»Ýðx<²·gw‡z‹V‰²¶éõznÆtÀí!11 Ì×ñù|¨ª®Æ‹/¼ øñ°Dç)€†Xâ^_O Pò³S¯‰¥†à– LƒÁ [áÁ½Û›™‚×ß=ËTÞ50€z‹…9[«œÈÊÎ,2 Öyu©ù4^»É´FÊZ³†ÛK—Ë%Y6é………²³‚g½^K—.1_£®®%&wýa»ÙŒb ¿ß»ÝƒÁ iÝXDĹ™|¬qYÄÎÝÝÝ\>kZ3\Ë™Qß8Žÿîò]Áù÷’‘‘œ€\C ŠÖ­Fnf2Íg‰âÃÓ55’%ôÈÉÉáÆ-E¯×cÇC;qþÜY ²%¹ôù|8qò$*஘JJ÷Þ—úG—Ñrºÿβ>V©TÜìIˉ€b1œN'ü~?†††àñxnï ûýþEËDEE!;;¥¥¥Ëzv•••¡³³ ÷¼®nØðüÑ÷ñÝ'v.8¿MOJ@zRÂ]®¨½.tØÐ70‚Æk7Ñaw ÅÇÇ#:š«ÄµÏÑH ˆðA‚‚ áp8º222^ð2õ½éó`uœIê;7wä,zxj‡ç>uâØu%M67öþ¤?}²ÆÔxnû[†>¿Ä.zØq÷M¯UÓ@fDJwFƒC‡qÕµuu¢d#›œžÆ>ªÃ?¥Ùú¤°}ŸØ”hÄf¨dëà@D£äð )$x?6›)Û0ëìÀÛ&t¼&¯z ¯¿{šØçß'Nžä.8z!JL&¦rv»=è, á†×<©³x´ ÏË˃N§ÃÅ‹™UZ[[qÅjeá¿FpÄÞÞ^<(—Ë%Y`^V¬ˆÂÿ÷?égNÏ™–¯ ¥8ÓœZ(rsw˜Oyi>0‹jNŸV†àAä,e’©¨»ØÆÖO**¸½·UÕÕ’º$ét:®ÖLµ}0¢‡'NàÕW_媤¤¤ kÍÁs*§Ó)¹à!úGà˜ ¬lºvó¶À #9A‰]â5±HOJüI®fr<ŒúÆM$rÃ>€öÙý¶>ÿ.5r )ÈÍLFzRr3SH±¬âCžÝl6›`ÑÙrQ©T0q´Ç¨s°¢‹Å‚Šòrîõ”˜LLÄP&õ6;ð^:Oá‘~']^ÄI¼h×…æ­n·°Ûí·…¶ÉÉÉÈÎÎ,Ú6ÈÌÌDss3ÚÛÛE©û ûž?ö>^ö±e‹zgç³»ÎÜ¥£wà1DÓõ…Ý©¢££yKôõk‡ÃqŽz:A„<Á˜U,®â¡²m#Cx09ãŒE¿ªÃÑGö@« ('*& ±)ÑÐæ¨ïæ@ðǨoœa$`rr’A&ïôz=—ÑÀìÁúáʇñÚ»gñ{ÆÀžÖÖVÅd­5™L‚ÝœN'ׂžƒOœ<)Y ‡Á`àöpÉ`0 ¬¬ gΜa>€!âÙæ|òòò¸pYš¢õdee!%9®a¢ŸP&õ`™ãûLãUðÀ’€’³„ž@›‹µ7¡R©P\\ ƒÁ€K—.‰â|èñM=Ì'þóùÅBn}ƒ#èè@ãµ›èÁÄ W1\à w‚;ùI„`‡;##ã9¿ä¡¾#“~t{G­½;ƒŠœEg^*CÙ÷Ï%z¸åó㿽u ÿðØz<¼ñK\ö7V—‡¾á1\wŒbmÆbx<0199‰ÞÞ^É®o6›¹Êêj³ÙP]]-úu½~?^=_‡ïí(—\ôMf ´™*DÅPô!OÆÆ'Ið 333Ô2Àzå ó;“w¾ûÄÎ{fsYŠ'N Ädâþ°EðÀ›eø|x= ¾bµ ¾WBàUÄ `¡][[ËTÞ50€Úº:ìÞµ‹«ñ+Tðàv»áñxd—­ŠõÐ*’a9@V©T0²8,é—bkÑâSû~fG¯kAW·ŽÞ…mïT°Î'æÿ|² …¹Òî›ÍÌþ‰Å…·ÿ³355+~ˆQÍþâÀKÖþG7£ñÚM¦>nQ€àE$Ë"€ 'uŒ,w¯×+©c¤¦¦r+$6ðûýhhhˆ˜u2KViÇ#Ë9õå–¥…ß.4uáBSÞ|ïØóPvï| ä{ŸéÉ Àuáó_^“(ÖÄ! õæë7íEkgƒÁ‰A„\\JÄ&ña g.®X­’‰u:䉤^'<¸u~ÿû:Œ w ommEkk+wÏKSI êêê•aI¸.<¾qz‰ÜrxPÖ½¬¨¨€ÕjEWW—(s†`E pƒØZdÄ~WÚ>Ãÿó<5õ‡£‹zA„Šü$‚ ‡ÃñOØÁC};Fo!-VMôÝ=¹ŠôZ5N}g;L/|ˆaopv}ÿò~ »ÝøÇÇÖsÙßX]j?ÃÚŒuw´)!œk×®I–<%9ûœ›¶°Ùløá‘#Ò]x¯ž¯Ã‘¯}C²ÏˆËˆAüZ5 ˆe¡V nx‘˜ü«oÜO7@©Døp¹\‚³&À† x³u]”Ã[ç¾Ïdïóù¸ Ž^ˆ“ ,òI»Ý¾ ²Üáù0ø¤„YJ×z½[¶laÎJ[[[‹Šòrn´JL&h4Á®N§óv_)`uÙ „¯ …b6›éy‹–.–‰Q¨ b)׊¦k7‘žœ€\CrH„?33ÀÄ0-P{<ÀïŸ-§¦-­ˆã»ûwâ™ýÏE@÷¢µµ.—‹Ṳ̂sçBÖMbd² %§Ï}"¸ŒF£áVÌR[W'™cZ€ÒÒR®û|^^†††˜”x\'³f•¶Ûí\;³dÇ?¸Œº‹mxþ‰!Í®Ÿ¡À u¥¯§þJA@hy/Áe@¯u‹¸ýû9ý8^£–¸~nûÍw­;wå:Ó5wíÞÍm?—r‹÷„À¬hcëÖm8{ö,Ûü®¦†;—‡‚‚Á‚‡þþ~øýþˆÉƒ5ßË©r1ÈÕˆà•J…ÒÒRdffââÅ‹AÏm¢‡ã¯ì“D¤ëóãݺ&žš¸Ûáp¦žFá‡AÃsx¨èäÌ4ÚFÜØ¤_xƒ%71êèü¡§þ¥R­Ý±E9©ºÛNÁŠj?ÃuÇ^ý/EÈÐÇqÕÑ2ôqؘ­ìvaiëÇßU|!xHI LáB™šš‚Ãáìú‡âfãÀëõâèÑ£’ÎÙ†‡ðÖå xfóVq¹úhįU#&~W}pÂ?…ž¾!¤''P¶ÿ0 ×ëgGð0៤@(—ËÅT.??_1m¯‰Å+û0SÀ_pôBhµZΪæt:%<¸ÝnѯÉk&Úz‹…I ´¬¹™J¥¨qm4áv»ÑÞÞ.¸,Zëׯìü!µàAèØÕi(zš…îînaí¬Ó‘Ø! ,åZÊÀB€Mì0—©I`$zˆÄ~¼ç¡"ÿà²ðùr]W F"%5Uð<ÌétrñÌíèu¡opDp¹’’.×@.—Kp&¡äåå)BHl2™àv»™ÖdµµµÜ%(X¿^°sšÓéä^ð opß=ö>žb'ÊK•³6”YYY‚Ë q™`"ÔÌC\hêúâ>n~¾Ô}tžXaÁ÷«ÝÅ´Ï(FÃmB)÷¸RSS3~ÒÒÒ°aÃ\½zUpY]Xûs(ö¨á¢f1Üh²Þ!!' ***`±X‚>ëñø&ðüÑ÷ñú¡ÇDÓøðÒu¸†¹rj|ŽzAȃÔA°âp8>ð6/õuŽû08±¸mÝš•z”ß—UttPŸ=¸½âlªlÊNÄ™—ʰJ¼J¾£o•¿¸„÷.öp×ßö–®\¦ox ×£]\ b¢)£½P¤ îßµkÓÆs8ðz½8räˆd›ó©ïîÀ[—/ˆr­¨˜(įUC¿)Ž+±ÃÔô4úGÐÒÕ‡Q߆nùh@F£>r"Êþ~ZlÃsrr‚ʘÄøÑ$q¹ô¤|÷‰Ìó“Z™§äˆ‰á@I¨±`xÎD[sú´d×...I&´PR\\ ½^ÏT¶¶¶–Æoˆ™›“nÌ:&%Í¿ƒ;Ü^#O“4•8vï|€I f½r…ûïÎ(ÆËz·îb[àÔåî´Äb•J…ÂÂBEŒù@VVÖ9J½@ñsj§Ó©¸gý›ïýIE,ŽNAÙ¹s3“ñ=úGÐtýæ=ŽpyÑŸÓç>Y²¼'ç ¼Ši÷¸LœŠ@£°°YHÉÛ»˜uÞ=44$y½„&œÄ<„"æB:***““ôµnØðüÑ÷Åß}Ã8ýQ+OMzÞápÔPÏ"y@‚‚ ‚å9üTöê­Á{þ{’F+kуxÆ'ñ¯u×ðíw¬p¸Ç¸éhæüTèb…5vÏ.¸ysµ ““ÒdX/((àꮪº¶žÐ …ê»;ðáõ– ®¿‰›5ÐdòL7xË‹–N'_ô ŽÑ€Œ@&üSÔ"3==MÀ!™™™A•—k¦Ë­EFl-Êa*[[[´¥u¸YÏp˜äñxg¶ 7¼K™ùN¯×Kšé?¬ë6Fq oZ,ðü~¿$*€2¿äˆÍfcïDä23L‰M‚‡È#^‹= .ç`zfÉ –ùc(¯Ä€EðPPP€”þÄŠ^¯Wp¡*JH¬×ë±aö¾Å™ˆx=£°Iª9u¸æÕßêþØ‚Xl.P´v55„L¨(/ç²ÞRîqåää(rÝ»e˦r‹…»=k¹ ›››—QŠH,TÈ-9 !>¥¥¥(..ú:7ìxíݳ¢Õëݺ&ÞšòiêM!Hð@DP87€Ã¼Ô×75‰ŽÑ{ë3Ä=4v»E=üû3¥¢8=êǛۃ¹ Up™‚‡U°2A£Ñ òÀnê[U] «Õ–Ï>Þxuw°µs¦ ‰›5ˆŽãÇÙdÔ7Žv[?zúܘš”=5=Á[^@Æ„D¢«Hf’Ú4ì°x(- ü\žb'SÖZ³WÎ'++ )ÉÂax læõ0Xjw¥¢Óé""@K«Õ"kÍ®ÇïÆu4Šw8ÙLM3"^oæók‘«ËïsålN\a…r¡©“)µ™SÇ4©ùt:òòò×Oòóó™öl==\‰´Z-SVi%Š}%vÙ$øæàÞ­LsB\²Ö¬áƵ~>Rí·¨T*Ź;HKKƒÁ``*ËÛ<œE€Øßß/il6FGG—ËÍŒ\7SÁxgg'¹ÂGyyyزeKÐ猿¿Ø†Sgƒ*Ô7ÚÐÚíâ© _q8]Ô“B>à ˆ q8oh䥾ž[ð-q:(¦èá¯ß¼(ZÝŸÚaÄ™—ÊD=Üþ·cn ä̶|á‹Ô»ÜH×Ç!&:Š«L¨¬¬ä&#Ùéšš ³­XÜtëß._@KŸ 2 ±ˆ_ËÏø„ ]Ÿ ¢£w¾ñÅ7VnyÈå!Ò§h1™š"Ç 9À| W‡1ˆ×ÄâàÞmLeyË^¹ë× .#up‡˜‡)ÉÉ\_±Z%Ë|g0––¦èç\~~>Ós‹·-9_rx/‰‰‰Ô=ÿ–àšdØqÄkb±µH¸3”õÊ®¿7KÀïÃ?4u .£Ñh˜Ü¥ä€¥¾^Òë³f_–;*•ŠY$-µÈDô95 bIr3Sðú³á>e.'å\Ö»µµU2{¥¹,͇õ],õüGl²²² ÑO)åû˜ÅÝr#ø9É"÷ûýÌmMð…ÑhDYYYÐÏì·N]J¨ëóóæîÐ à êA!/Hð@„X<ÇSe›o .ù7b‰Þ¿lUô°);QTÑô áïßiÀ·ß±ÊZø`ÎîðàŸÄÄ$ËæšÍÜÐÕ[,¨©© ê©©©øú׿ôâñ§ÿë,ºÝK?·¢b¢¸Yƒ¸Œ.Úxjz}ƒ#héêÃðèÒb†áѱ»œeC÷[\¦©=¹EÉ‚(/ÍGzR‚àr®´¶¶rýÝY² IAkhH¼õ¯‡Á 3±)ÙÝ!€J¥bvyà) Kp–Ûí–¤.Gp™"rxþüu¹˜ÆAˆÉ Mé#’òÒ|¦¹²Ëåâö;g)ØáA(%%%Lp#¥ˆ˜ÝwU²Øh42í\áLì´^Fsêp’žœ‚¸¹™)xë…¿ÄkÏ>†ýlÆÖ¢¦½4‚^ŇRí³(ÕeiþwÌÉÉ\ÎÖÓÃÝ<<;;[6ïãÎÎNŒŒŒ.—˜ h‡–}JhooGgg'å£×ëE=®ú£¾q¦²ïÖ5Á;Ε«Èa‡Ãá¦ÞCò‚Aˆ‚Ãá8àm^ê;41Ž›¾¥Ä=¼óQ'Þ>/ÞBA Ñ0ëH!wáÃÆl½ð…±s”© HINƾÇ碮­­­¨®®zѸ}ûvQ^¿?ø¨^ÿâ–÷Q1QÐoŠCL<Ó;—Ûƒ–N'Â6­–#Œ ”èwœADÈá3ûÝÌTŽ7‹ðù°wx<¦àæpÀãa°Ëå‚Õj•äÚ6lP¼€)k€OÙ¨å4~Y®¯Qƒþ| ‚×­f j¼"ÑœF®È=úBS'<¾ ÁåL¼º;H¼V+--U|Ÿfû|>®Æ~ÌÖÄb: EØFDîœ`ÿ£›q¸òaeê~þ ^{ö1<³g+v?ôŠÖ’°] L&—âC¯×+™N©.Kó1Låx›‡ËÅqÉï÷£¡¡©lan†(uðx½\?«X¸té‰"1âV<¾ þ…p÷õ–nê›l<5×y‡ÃñÔkB~à 1yÀ0/•mqcriÑÄ=üÍ[qþSñ~›²qãçßd,Å\áÇŸÉê¾mÊN\æã.ÝÊC‡q±!h³ÙðÆÑ£A]C¥RݱXÔëõAgóõúýxõü¢žÄ£¾q´tõÁÞ?Ì”½ÿ–‡Á "—pØ Õå·ì•óÑjµÈZ³Fp9)-ÃÅê/&“ ))üeÕ’ê@R¥R!??²‚XX²àñ”šuüJŒÉâü®¬w^N[Arck‘ð÷*ïnhB¯ä¾îùCS—à2)ÉÉ\Šˆ½^¯d"bÈËË‹!qff&S0’5‚,¥8 ½îJ]¶å híj­]¯—æãù'vâµgÃóOìÄ×Kó¡[BhüõÒ|l$÷5"6®[=;‹ppï6¼~è1Ôýüeß]Br„`Çl6sYï+V+|>Ÿè×UºËÒ\ÒÒÒ × Å°Ô×sõ=åâ¸ÔÖÖÆ<§â‘Qê`³Ù¸í¯Á¥/]º‹ÅÂÅYbˆš®ßÄñß]TæÝº&Þšê9ê-!Ob¨ ‚ ‡ÃáÎÈÈ8 àg<Ôwrf×Go¡ aéEj@ôPw£ þ ‚wÿ¤7~þMèµâdRÔkÕ8óR¾ývÞùH|Õuc·Ýn¼}¾oü*6~ ú¸°Þ·ÜŒxÁeºú) "ÜìÚµ‹ +z¯×‹9Ôà|±C€@K—.1_Û6<„Ÿþ¯søç¯”ßþ/b‡Qß8úF0ÊÉn.äðYÛ_ˆysŸÉIj 4èHŒ Ž¡!áÎ]Ea8lßÿèf¼þîYAeÙ+yµ•€‚õëaëéTÆét2g“¢¿,·™h%:,..Ú.š7òóóqõêUÁå®X­¨(/Wôø5 ¢Õ%»­˜.EkW£éúÍå¯i¶AðLÔ ÓÔ„8l\·§Ï}"¨LKK 5œŒhºvSpSI —ßUʬÆ*• ………ÑgT* ººº„í;p6ö³²²ï• ‰:§f%çKI8\ùð¢ÿ^^šQßV¼þîY\X@ô´û¡ppï6z@¢“ž”€ô¤„Å4Ÿ¿š®ÝĨo½õã†}€n4 ·ûŽR à"Áei.yyy‚Ïvm==ðz½Ü8ƒ°œá‹í¶äñx˜ö@§Qø:ù®ßçf>_-1™pB£aŽs°ÛíøÍo~ƒââbÉÎy=œ9s†YärüƒËØZ”³¬¤:§>j­o˜§&:êp8>¦žBò„AˆŠÃáx###ãiy¨¯Í;‚´X ’Ô±Kþ­¢‡a¯{^·àÌKeâMFµjüò`)rRuøþ{Í’´SßðÞþ¨oÔ‰Üôx<¼ñKØ–ŸñCÆ*à2Ý$x+ؽk—ìëéõzq$H±”••-šíÃh4µa-ý}xëò<³y+b‡ ÿúG0xK< ÐáÑ1¬Š£ÁE™™™¡FàíÒYÙZ”F @±“•wÁCAêêê•‘*›¥˜ðxO\.—$ÁØz½>"‰Zv»]P¹ÖÖV~ ãW,QQ0σH;ð%ˆp LMŠMâó÷6ƒ» ßseás ŸÏ—ËÅ¥ëëZÈétÊ2ãpG¯ }ƒ#‚Ëmç4£´”î"………%$ÎÏÏ,x¸¦ñ2öYæÔRºŠM¼&‡+FG¯ë¶èA§QcÛF#eÜ'ÂB@±˜³H@ÑÑë‚Ç7Çàúfßav—à};Þ)áT|H#xˆ—¥¹dff2%³»bµr3— ¸˜²$õkîÝÐÐÀ\öÏ<ºàïušXÁ×òz½ÜöU­V‹ŠŠ ÔÔÔ0_Ãï÷ãÒ¥K¸zõ*6lØ@£×ëa6›qöìYæk¼öîY¼õÂ_Þ{mâö¢öbOM3 à0õ‚/$x B žp–—ʶ áÁäŒeý­¢‡ó-N¼ò«f¼ü-q³½ü­B4 *ࢮG :ÀmË–-KZ›Âãñ>¨šK}wRµ:xò˲;LMOÃåöÀ10"úµoyHðIøÆýÐ΍!D€¹¯‰Eyi¾à̵W®\áf¾±¬–á~¿_’@:C”† IDAT1Ä&“‰K±N‹DYÅÅÅ;®Y¯¦R¸-E’ËÒüï͚Ѓ§yxVv¶àsr·Û-ŠàÁét nße›×aÃ}¢µƒÍf㺿V”—£¶¶6臄@ZZ¶lÙÂ$ê€öÿÝeìtó¢óo¿¹︟§fyÎáp¸©w„|¡íq‚ DÇápœËÈÈxÀS<ÔwdÒŽÑa䯝ZÖß'i´Ø™½u7Ú˜?óûï5ã¡ûÓ°ã~q³Mí¸? 7~þMüõ›ñþe»äm×Ñ7ŠŽ¾Qà£ÙÿÞ˜=+|È͈ÇÚôI >î¢AÆ•••\d’ªª®:ó˜{ÇÒÒRøý~æÍ8ÕÒ„û®&£¼4_Vm:ôy05=-ÉgŒúÆip…µZ-¸ÌЈOô>EˆÃ4µ%Á,‚ŸÏ‡ÖÖV0äk­¡¡!I²×ŠäÁëa°™h ƒ,³ ‡ –,x>Ÿ6›‹¿””¤$' ºu»ÝK ¥— ‹š"t¨TÀ„HIpcT³ "rÙ¸N¸à÷€¥ð‡¦NÁe Ö¯çò»Úl¶ ¬cË–-ÙRSS x ²LIIF£Ôoü~¿èIXœØÒ“É¡ˆ\xðÔ]d;×/¡=®ÛDšËÒ\X¶în®¾#Ë>œÐùÉb°º;hãÔøÿÅ|ç”ðý/¯Hß)\hµZTVVâØ±c¢\/ |hhh@^^ŒFcĹ¼(£Ñ·Ûööv¦ò§Î5¡üËù :—]iû ­Ý.žšã¼Ãáøê!oHð@„T<`€U+òÑÍw8¹Ìç^ÿv¯ûÆ;%&ÌfsÐñsñûý¸zõ*®^½ ƒÁ£ÑƒÁ@/…P\\Œ¡¡!&gboo¾÷®|øŽß{ÇüøÅûWxkŠç¨7„ü!ÁA’àp8܇üŒ‡úNÎL£ùÖ þ,qù‹ÕÜÄŒNL ©Í~³ÛåÁ+¿jÆÏž”f³äÐ#ùø‹Í™øö;Ö¸=,F@¤@b…È"%9û\öõ¬·XPWWÔ5 JKK—S©T(++CmmmP‡±‡«>ÄëÏ>Ö ¥P æ/žç ²G^ ^‚è"ÿä5‚ÌÌÌP#ܰµ(G°ËƒYËBIAAàˆ`D“‹¾_EËZ³† ‡¯ù¸\.Ñ3Ñnذ2^aöPXèa OÁ™YYY°Z­‚Ê ‰r(ø§?ýIø:$&zÁŒ[AH‡:˜¦§äÑÑ€JMíH°e(íæ,èy.ë P†ù¬Ø4]c;?à5£´Tó8–ýW%ͧß΂õ ¯ëÅLà÷ûÑÙ)܉¥! O8éè.¾¢=tB)4EàÁår N̰‘žÐC§ÓA§Ó žkÚl6n’ô°<ïY‚£ç¿›››™ÊÞgHÆžEKþKr%G‰X£òÀغ»%™ÚívØívèt:äää ???bÝ_”ÄöíÛñ›ßü†ÉüBS¯Ý¼#Iç©Zà÷óÔGÇÇÔBþ¬ & B*Çy©ïÐÄ8œãÂ\6¦¯Fnb2ógû ý¶{ä¤êpú;Ûqê;fd§Dn ÍŽûÓh@†˜C‡É>cÅ«ÕÕÕA]C¯×uئR©`6›ƒÚðø&p¸ª–);S°ŒúÆÑÑëBOŸ;¤b‡Àg‘ÁÔÔ45‚°lPâÃø¡VG^tÛÖ"£à2¼ X2hÉUð`*)áò´ˆÜ‡T*òóóéÁÆ-Ž,Ž&N§3èϵÛí\.i%eh%ˆP¨ãf… ‚ÊP«gQQÔŽÄl†R¡¢5ŸÏ—ËEF"-£´󸼼¼ˆ«T*&w0žÖÉ,¢y1NmmmLÎi¡Þ'‚Ž^“@‰WS±Ý– Cĺ,Í%55Up™ŽÞÅZ­p»õ`Îžš››™ËÜ»mYÇ’ô£[!.y/¾ø"²Ö¬‘ìúW¯^Å©S§pñâEIÎ+ˆÐ®9Ìf3sù7ßûÃϾnj/uðôõ‡¦^@|@‚‚ ¤†+˧æáALÎ ¬ÜºÆˆÄ8ö€¿»Aòïõ›3aýq^Ú[ˆUÚÈSWoÊÑÓH !»ví’}֛͆ªªª ®¡ÓéPVVtƽ^ôuúGðüÑ÷C&:z0ê›ϪstŒ[„à§@}1 ‡yàõz™Þ‘ÆÆu«¡Ózð,z`™;y<ÑÅLbŒðz,v`Vqq1e¶úœÄÄDÁexÊFÍœÅò>˜OCÛ^¹;DxˆŠš.ÄÆÞ[ø…ÙW«8-MÝÄ€º‹m€wëšxûêO;Rì'à Iq8ç¼ÍK}'g¦q}ô–àråäB%4MÛçœoqâíóÒOúõZ5^þV!¬?zO~Å1}p•VE!$kÍìÞµKÖut¹\øá‘#ðù|Ì×ÙáŽñ©×½axÃ>€7ß» iÛMø§Â.t˜ ¹<„€9Îä$5Á,¼g^b jb=PZŒ`³0i4nƒ³Ä<ÈÒëõ04çÌ…fä f®j‘¶¹¹™ùßÜ~?uJ‚#+¢g…í¬ø!ð£ÑÎþÄigÿ„Äbä„¿w”’¡”GF}ã¸a\®„S‡1Dó),,$!1ØDÄ<Y²¬#ÅÊ"Lvé¯mYG:bAX\iš‚ò‰åÑÁð..X¿žÛï+¦øpÆ í²IïâP¿/^¼È\Ïåº;@z²ðÄ­--Šé·Z­¯¾újP™û…ö‡K—.áÔ©ShhhÕ‘‹ ………ÌÏýã¿»ŒSµÀÖ7ÌÓW>ïp8jèÎ?à ˆPðf- ¸ÀæÁà„° Úxu,¶­ahyåWÍ!û~9©:üò`):Ž}3"„±9z­šFaÐh48tè¬ëèõzqôèÑ Åeee¢gÜ6زeKP×øýŶ;ìÅbÂ?…ž>7Zºúd!tà‘Q]éðS ¾ÃÁEk#/ó‹ËƒÓé÷?Ü»u=LJÁb: Pæ»»ÑjµŠÓ¡<öûýÌðt5v˜ÖR‡$™°"ú‹‚X.,R¡Ë¹Ô°¯¦$'SFéÀÜE§cÎ0ª4Xö¢y ²LIö|ÃñÐét¢««‹ñyœBîiÄ=Ÿ_ßïcžÝ–ÄÚãR©TÈÏϧÄ»˜7Gb–ý;–ýd§Ó‰þþ~¦:îd³ ÷o®AøúÉÖÓ£¸5Tå8pà4MH>/°ùÛßþgΜ!׎P©TÌ1+®a/~{¡·¯ü4Ýu‚à <!9Ÿ[?æ©ÎWo .³f¥)él.OH\æ2_ø°J«¼,I«´*¼ü-²»»wï–ýaÜ‘#G`ëé êÅÅÅ¢‹FäääuÓç>¹m,s…ƒ·ä·±3ê%‡‡H`jzšAÈáàÜLás ›ˆëá€Eð v@ëASžƒÅr0 L÷J‡% O‹¡ty°Z­ÌÁ]/>ý5êŒAœ£ÓÄ .Ã[ U0sK¹ :z#+£´Ø›FQcŸ!xš;ÁCjªà2Á&hnfKF¶bEWVPÇ$èAßàˆàrë9Ýãòz½¢íq“ËÒˆåÒEHGZZ ƒàr+W®„’«sýWGÝq‚à <ÇÎóR_ßÔ$:F…›RlL_xu,Óg†Òåa.áß/í-DvŠr²ŽüìIrR)‹J((((@Ey¹¬ëXU]´ØaË–-0¥uF)--eZ@ÎåõwÏ¢£—ý@KîB‡ÛÏjÊü/Óûâýšþ)jØ ™&áÁ×18<ùž7Ùa<ˆq-^ƒÅ $w‡…a9 ïç(@‹Eð044$¸ŒÛífÎ@ûåÂll¹?‹:#ADΓ½2D‘–QZD‡‡ÔÔTÏEðÀÛØgIŒËCgg'³èÿé?ÿ3rw N`9+ËZ³†)Ó½ËmI¯×K~Ê#,û[<9˜²¼‹…ÒÜÜ̼}pï6Ä3ÀYܤy/EJJ ^|á…º=Ì·µ··£¶¶µµµhooű‹¡¢7FµZÍÕòÀt§ ‚?Hð@D(yާÊvxnÁ7%,˜V­™9l³©0¸<ÌE¯UãåoâÆÏ¿‰Sß1ãɯð½‰ñäWŒxjmÄ„ ³Ù,ëúUUWÃb±uœœœmî•––í"ñü±÷oäò"t05=-Ip=ñ,Ù˜íýâ×c‚Ü ‚+S$!øä>»iŽæÃr ¬#Ã\Ä<„âPLÎlذ)‰æ ÃSFZ±Ëž•1»œN£Æ?Ô_ÀÃÜr÷®]a>³{ž‹§NÂÅ‹ƒ>§ Ä#??Yûú«V­BTTO_íׇ£†î0AðI 5A!æ0€§¬â¡²CãèöŽ [+Lúà#:†0.0#õù'ºú=ÈI•Çf¾^«ÆS;fÜÞ üúOvœûÔ‰__îŰW~YÕw¬OÃËß*ÄŽûÉêz14 |>Ÿ¨×,))‘í÷­·XP]]Ü8Ðëa2…>c“J¥‚ÙlÆ™3g˜7nØpøµxýÐc þû„ }ƒ#܉æB‘ [‚ƒ¯d$ ÏÓÒÚÊuà}Jjªà,œG”` `…<·»øý~œ:uŠn„ !tŽßÐÐÀT·ô¤„ … b>S“ÀÄÓóÉI`zˆ£ö")ˆ×ª—iå|žÌ#vᥳ³³#¾Ý¢¢¢088ˆ3gÎP'š‡ÛM HçÃçñx‚Ê.Í’%š "j½°€3÷ÔÔ Æ&î^ûÆý˜šš^à÷“˜iO»ë¦p'žÝ–ÄpË<{å:Î^¹NY$xKУeØgv:Kº_¸Ýntuu1Õéë¥ùA½7®[ F-ø¬ÑjµÊ:é£h÷üsáCEy9ê-ÔÕÖÂ50òzøý~tuu¡«« :ƒF£z½ž$aB¥R!//ïžÎhjµ±±\%ÚðÝ]‚à<R‡;##ãi§y©sÇè-4:ÄD Èb0”²ñQw‡àÏûõå^zD~™A犀R|Ü=„óŸ:qîS'»ÜèvyÂV·Ç6ðÜ#ù$tXëׯ‡Õjõšá,›Í†'N×ïõz”••…-#­^¯‡ÙlÆÙ³g™¯Ñtý&^{÷,¾;'ó”„Hð@K3)P€Ir!739â¾sVV–๚ÛíÅN=Øà™HÏ~Gˆ‡ô¡D¨¸\Șknnf%Ü»•:!33K‹LOÏ:=¨ÔÔn!ú<Ù‚ M]Ô Àš4D úF—!Q 033ÃÝ<‡PõÿææfægÅw÷“»¡\|ãþ;D ‰æ ¦¦gd}F3tË» Ðb©µ}V„ z·Šà†;ÖøF'EH䱵Ȉß_l\g—Ë…””ÈpØÓjµ¨(/GEy9®X­°X,¢Ç•,—€@µ½½ý¶ø\àÂC~~>ÚÛÛœ?GEEaåÊ•¼}¥7GÝY‚à<rGMFFÆy;x¨ïäÌ4š‡±I¿ü…ÌÄÔ$¶²qÑÞ-ØåáܧNY æ³);›²o×µ«ßƒî~Î}êÄÇÝCöøq¾E|»¹ëÓ°J§Â¦ìD;;[’a¿ßTÚ­EFêL„ˆýQØßON+VÑt Aa§Ûfã2˜>kÍAhrÊ€ßxí¦ðïËñœšïœZÉAzBǼÓédÎ.½û¡ÎàDI¡b¾`a~&óQïø¼¿ÏEA®°$ý"·%BŠw±ÒYÊáÁn·£¿¿ŸéÚ{*åý»qÝjÁ‚¨­«Ã¾Ç¸~[b2¡Äd‚ËåBm]êë냊»†¹â½^£ÑƒÁ@â‡q/—‡øøxDGGóôuºÇaº«Á7tÔ@D¸x@'7‹´qœã>¤Åj–]fET“ËÃû—í\ÞМTrRu Î:+|p{'ðq—[ð5°A$ `6›a±XÄYì–”Èî;z½^TUU-v(++“E¢Ñh„Ûíft€×ß=‹ô¤l\·I+µp0d™“+£¾qÜ2B  [‚ƒ^a±©æ=S*C ŠÓ¼È8Økh4®ƒhä” ˜ˆ,–:f³É±öQrw ÄfšaZ韢VÌ ‚'4^çÉZŽƒXú…ï½e“àÞBENBçÉÍÍÍLõÒiÔØ/Bvi‚XŠ{‰&üS˜ðOÎù[å ‚¥Ã> ü9Äù»XŠD DðïbžX_P€¯ç÷ûÑÐÐÀT6=)»w> J=¶å@§Qß%[ŠúúzìÞµ Z­6"ûoJJ ö=þ8ö=þ8ê-X,–°>gÜn7ÐÐÐ@⇒ŸŸ—à!&æÿgïî£â8ï<Ñyé†îшcÈ6‚² RD^Zv¬»“Y'ÆÉœ›isG›{’=’3ΜrFŽŸceÏÆ‰­9gGÌÄ™»f­¹'‘ìèf"”‘lÓÉ Ø9‚ˆѨD7мôk5pÿ@`½ Ôõô[U÷÷sq]ÕUO=Õ]UÏóûý2µx^äÑ$Ò><QBH’d/..þ6€oie›{f¦ðXáýÈL o„vqi »àÒÈ4Ý’Ÿ4Çûæ`…§w•òH°æ¦&x<ᇠ7S[f8¯×‹ãÇ+YK}}½j‚VÔÖÖ" g €c'‰ïúÊ7m„sÚ“4ÃýNæŽ%³Ù¬([Ú5×,Me–––Ø”2\‚™¢Ô"QƒªWwÐxö»©©)ž<•{£hø¹Ýnáëÿ'ë·áR  EÍâ rU¹ ²²´4¶#Q4ˆTB£ø xHæLüDáˆeÓðð°pvé/}frøÙK1f¿6…Q'"Ц*<°Ú‘rç®ëïï_÷ïëùÂÞ‡W"ýÎ1dá“W(®òàóùp®­ Ï46¦üqÞcµbÕ —Ë…ÓgÎà·¿ýmª>w?””” ´´Tus<’N§Cyyù-Ï©óòò´¶?‘$émM"íc^%"J˜¥¢F´²½¡¥E|8þ$NHFvf&* ”W&qzØA(¦ž=|Ï<ó Ò#L±¸³®NUûõzkkÄÁ»wïFII‰*[]]]D7é_ÿý]„/ cãCÒôgfÿ-N§‚cÌ –H,2Ói˜ÒrÕ®ÉIMï¯HFg‚<¦§§#Z^mA°J¬E‰q¯ß®®.¡õš zVw UYZXL‡(±´–YVë$`­_SóºšuONÅBY–ïÈJ®û6æâóO<̃CŠäç+Ojçpΰá¢lJ à¡Á‡”âD‚~îö|Ëãñ```@h;¶– bS¤É9 ŽMâƒ?ŒcpÌ…ëSs˜÷‰]sî«ß&´Ü¹sç4_Y:š, š›šðý—^Âþýûa)(Hø6¹Ýnôööâܹs8{ö,º»»#NòD·Ú¶í£ó'''™™šÊ±>àYE¢äÀ€"J´ƒZÚØQ廬Ên ÊÍÊj]²óâ›b¯ñé§ñícÇ`Œ¾®««SU™º“--°Ùl­£²²ª=f:{÷îhúcßû_a1ç$M_fÀCòciîÈ„B !íRð 1Ñ)Ò‡ÿÌDK$v¬ðàp8„š>ÿ釙–ÔwMZ| ¢È=Pª|B âK–•߇'CFéùù<ø¼¿ŒH¬ÎƒH²Ký?>ÁŽDŠ©!‰ãNåÕ¨“!‘(Ñûïéé,˜ýàéÇjîøÝ¼/QÄ#moðù|8}æ ;Ç}¥aß>¼ôÒK8òo N%‰2Wmü]f³&“ 0Ű*[Œ<+I;Q’`À%Ô’Qoji›{g§Z âå}¦\hR­²²2?~\è´aß>Õìǹ¶¶ˆƒÊËËQ[[«úc ‡ßüÎŽ¿ëß°qƒ1)ú1bK¯×+^fpŒ)ˆ(qFGGSnŸÃÉhy7²,GüÀŸÙï(V÷*ZÍó@–etww -{߯\|éOv±QÔ¥Ea$aq¸DQÁ 6õ›šõ)^FM‰eˆ’I$Ù¥~pyh‘H£üAecGjÈRNÉG‹×xƒAÑë×JØ111»Ý.ôþ»ª7cSá½6Š@ˆ>3kkkKÉq‡pUUUáð¡Cxé{ßSMÕ‡›¯o~ ‚¥åä¡6lÐÚf¿#IÒktû÷ïWtÓª–,'í6Z[[#Z‡ÙlF}}½fŽ™ÙlŽ88ãôÛ—qiÀ‘4ýxq‘©BcÙßbf X\B2 ?ú ÉË¿ÇM‡•UÄ,,,°HÓD&h½¼t¼¯±" –X¡õl´ÌÞ§NÉåø^î–å.’ ´_ýÂ'Ùy(&ÒÒ€ŒŒïÛxÛFD)"Ü,³7«N‚kÒ}ûö!--@%Reâî½îi»»»…³Kÿõ—XÝâG¤ÝHR&Ka¡æ÷;//_eÔ’õ^‰-[¶D¼Žžž¡å²õ:|nêá]ƒÑ3(Á~m .·Aù£q²O>\“A/´þ×#œןË-U¬V«j¶m%øáìÙ³8wSUff¦P¢Ä{–GŽ(¹0àfìy IDATˆîFé¨cZÚæAÏ,æB¼ø¥äÒ°o^øÎwî9R¶y3:¤ŠmEKKKDë0›ÍØ»w¯æŽWEE¶oßÑ:þá瘜IŽ,‹Kœ9£&ãÎ{Ç1.„€`…€ÅE`i食ÅÅå߃Ìñ¹Á@¢”I…†éééˆß_ëÙh?óÇ̉Y*c0°Sc k‘f ýäÃì@3:=ɧå/M‰(߯Ñè”ÇL¹kê…I0Y4YìkhÐÜ6G;ðgbb‡XÒŸg>½÷mdwŠßç¡Òj}Éxá©ÿïy UD‹Ï·DÝœkkcÇSUUš›šðꈦ¦&”mÞ¬šms»ÝèîîÆÏ~ö3Øl6áëÅTâ÷û1>>®µÍþ¶$I—xôˆ’ ˆH$IzÀ;ZÚæž™É°^×?9ÁLšQVV†^xûöí»£\¥Á`@cc#Ž9¢Š¸ÑÑQ¼xüxDëÐét°Z­Ðétš<^555(--^Þã âïßì€/ ýé¬ð;&“Iñ2þà:Q K€Â-<°°°üú´4ÞºQxœ.—¦·_dp5 ¿_¤Â’¡:Bee%öìÙÓGEš››57éOdBæšÏzz„³‹±ºÅZZ Ï^þ¯ˆô ¶!%æû5ÞD*<$Ku«oü×ÿŠôt>ÃIø}åæÍhØ·/åÛ¡»»[h9“A/ýÉ.v$Š»éY/!J“Š—I†àÃ}ûö¥DÅL­xöðaMö«H’zȲ,üý›ŸkÀ¾úm1Ù§ ¼€©Y/ìצ°mK‘pPÅéÓ§1::Êέð³uÕŠ^x/}ï{hllTU%2‡Ã›Í†³gÏ¢§§G¸ên²ûðà i*Kဗy䈒O&›€ˆÔtÏ [+;’18?ƒrî^òƒëãèt(¿á)/4±7PBo:ìßû÷£¯¯oõwjz@æõzñÊ+¯Àçó ¯C§ÓaïÞ½B“¹ÕäcûÜn7æçç…–wºçñ£Ÿý_û³Ç5Ý Ìb3"çˆãn–9( OY`ÉÍEFz:µÒs#ÜÈ"•z Tùƒo—ÆLqͶ•lþâË_´··c‰•£Æ`0 ¹¹9e²ßÝnbbv»]hÙ'ë·áR ;Å\z:• ˕ٔÈàüW"JJ+<ÜžxFË ð•¯|-'O"(3[y"TUU©¦Bs"  WCüÒgv!Ç¥Éý^X\¼%ÁQP^€Z~> ±”n<åçç+~æ25ëEþ#/A’%Pà…ï|¯œ8îîn>ãJKAš››5›(&’ ãþþ~áD_|²6.ûgÈÒÁúèVœïèW¼¬ÏçÃÉ“'U“ R‹}ë™ÆF<ÓØˆÑÑQœkkÃoûÛˆæ|DíÎãAoo/z{{Q^^ŽŠŠ ñ ayÌMƒãn%Iróè%<‘jH’t©¸¸øÛ¾¥•môÌB¿èoË:-yæ0åóblVìúé‘r3;©‚ZÄ´ÛlpMNF´«Õ ³Yûçš^¯Ç'>ñ \¼xQ8ª~Ü9ƒSç»ãö )øÐ6vD¦î’‰*Rì°B—©Cù}…¼vE<æ?ƒ4:Á ÞD«4D£Ts2TxXñ_þ2öX­xãÔ)\»vmõ÷YYYظq£f«‚iE]]öX­)=XØÓÓ#øY©guŠ«´´å 9¸|ŽôtVx J$ƒ\UmË–-Iµ?õ»w£~÷nüý?üÞÿ}x½ÌXkƒÕÕÕ«×Ô©d­,¼‘d—¾oc.>ÿÄà ٗۃÀÝ’&(/ (‡nZf)ì ÎA™Ï ãIä¸k–ìQ2~·¤LëÝ['ÑóˆÃ‡a``ÿüË_bpp²,#++ ÙÙÙÈËËc‰‹Å‚ªªª”û.^ù>^™0.bkIA\?ÿö<ºíÝCð•gŒ^½Š×[[ÑÜÔÄN²²2475¡¹© ¿íêBWW—j‚ìv;ìv; QQQŠŠŠ”=N¡Ph5Iª†¼)IÒÛ<ˈ’ˆHm^p€fžðàv­–苆<£nÉgO ZGWWWDËïÞ½;©"òóòòP[[‹îîná ‡÷¯\Å¥ìªÞ¬É6XdÀC̈<\sÍÞñ»¥%å`o9Æ‹‹È1d#';ó~? %­xˆJ$³ÊÊJ<ô(‚ââæŒ³ÃÃÃÂW>ÿé‡5›–´M§_d¸W·ô4@Ï.JU?¸ ¿ûp<ì×Gš8„HÄ_|ùË«•ÔˆÂ!’]}­€‡H²Ký?>¡èõ¾€|GEÚ›«*Üü»›–— ±šmËÏW>Ö+2IŸî~n*IV{5ª¬¬Dee%;Åå\ðx<ÂÁ†ð✔ϥÃÓ×àÔy±m¶Ùl(++Cþ}ì0Q°³®;ëêVƒl6[ÄóA¢ÁétÂét¢··Û·oOÉÀ‡¾¾>áù' 2ƒå9‡D”¤ð@Dª"I’»¸¸ø €‹ZÙf½^£Ñµ,EOï*eG º‡‘‘áe“ñf4##‹>ø`Dö§Îwc“e62» ÝÊl6+.?¥/EZh`aqy U~®‰JÚˆhÑx™B”¬ªªª]¯LÂ’eY8Þ}sñ¥?ÙÅÆ§ÄÝf.Wne`añÏÌL S·\‚ˆˆî~ ADˆJµ·H²KW•aã#Ç\wüMI%"@,‘+ÙˆD <ˆVÞóèVäoˆ…•]Õ›ñÞïG1ä omm…ÑhLÉŠ±´üàõzW+?$:øÁãñ ³³3å\.\.—Ö6û˜$InžIDÉ+M@Djs£´Ô›ZÚæœœdddDe]¯`' º‡HJ®•ñ) Üÿý(-,hêµ³ïqà†î 48s[•‡¥(Í»Ï1dó€(°È,q¤qŹЗÑ`y݈‰_{<ÅÁlk~Gì¨Dêïï¾Oùê>ɤ„KKôzÀ`²²>úÉ6,W`°i™h–x"JŒH²KþÓcjÖ‹y_ðŽ>3'¥Ìf³âeÆ×¨œLbD'0‘˜ññq¡å²õ:̤ ¤.éý0Ј’Áôôtì¯ÿ™Qš(*&&&4—]š’›ÈsõžA‰ —e›7³ˆVÎоÁ`Pè½ö}| Yº„íë¦Â¼ˆ.|>^<~œA1¦¶à‡›¢5΢6v»~¿_k›ý,Ï¢äÇ€"R¥%¦Žii›õz}Ä“}¾õ§5<øDaˆô¶··7)£î  ºº999ÂërLâ­w{ØÑhUQ‘ò`¼Ç¢[âr• ( ˆ $ëÃRZŸ¥°0æï­¾%Ò¯‰è#===™£ÿúKO°‰ˆˆ’@!ˆ¢B´ºC¢³KSò xr¸Øp `¨rM”´çCœÜäç°çÑ­ ßß}õÛ°É"žÜ”AñïŸ+Á?yí5:tV«uužDSÉêDaÚ…›ÖÎÎΤ zÈÊÊBFF233Q]]Ì2á·_BïÐ5v6äçç+^æšk–¥ÞU@4«Å3ᓚˆf½$¢è²ÛíBË=óé)[‡ˆˆˆˆèvÃÃÃÂÍž~¼&¡Ù¥)y‰$êä¸L¤8.A¤ _|²VUÛ’­¿`ÐCâ쬫CsS~ôê« ~p:8w»…Û¨E(B__ŸÖ6{D’¤c<ˆRˆHíjicÓÒÒ——§x¹G¶˜YÝH£Ñˆo9• ‡dжÓ,4999¨®®Žh]oœïƸs†Ž ÓéVû–½CÑ-¿½´´ÄƒAD‡CóÞ‰R™É Ç—þd‚ˆˆ’žE ò«ä¥Y–…«;l²lÀ®êÍlDŠ ‘€‡é9Çe"$Ò~LTCtÛuxAAL׿}k1(UO•³M…yxúñÈæëø|>}þy´Ûlì@ r{ðC]]]\ß``?ÿùÏ5lÊn·Ãï÷km³²÷¥<‘ªI’dðm-msVV²²²Â~ý#[̸ðü^˜zp"ÊÊÊpàÀˆ×“l%³³³‘ž¾|‰g±XP^^.¼. „7Îw3 /38öQùí´(lÃâÒ|AöG5YX\ļ/€y_€Ÿ)ÎWûÂí?S³^\Ÿš[óçêu7Ç\kþ\±_Çgã†IIC´&Åzà‹ˆÖö¥ÏìBŽ!‹ ADDÉÿ,ÂbI‰ý,++ãÁ&Š@¿pPÿçc22Š“É$”H¨ýÒßÅD eTâi~ÿîªÞ• È––œkkc'J°uu8|è^ýáÑÔÔ„²Íñ p•e6› .\€ÇãÑT›¹ÝnŒiíPÿD’¤·Ùã‰RG&›€ˆÔN’¤cÅÅÅÑÊ6çååÁétÞ35ƒˆ"³Çj]}pÉMç… °wï^˜Íæ¤hƒÁ°z]QQùùy¸\.¡u]sÍâ­w{TUV”£¨¨v»]Ñ2=C×ðEÜè;i¢P aaaC%mu}j=ƒkWñ0d鑞†ct™0dé`ÈÒñ hȸs9†[¯Q}Ù8q ’Évzz:ìÌÑÊ0ë/"ºÓÆ F<òЦ[Koùþ͈,¿MFz:²³ÂdÌï""¢È1«4‘¸……ôöö -«¶ìÒ”œ Ox|ÿÊUì«ß†ü ü~Cl"{RÅŸo_|²ãÎŒ»f#ZOkk+FGGq`ÿ~^ë«à^kÕŠ=V+\.εµ¡½½>Ÿ/¦ïët:qîÜ9ÔÔÔ ²²Rõí …Ð××§µÃ;àYör¢Ô€"ÒŠg\ÔÊÆ¦¥¥!//oݬñÏ¡ßúSfŽ!ŠT´‚:::°wï^ètÚŸk4áóù°xcòQuu5º»»1??/´¾÷¯\Å&Ëì©}€.…‰”ßöBxÿÊUìªÞŒôt Òù÷KKKðø<aZLàÄ•jó¾à-¿Ï1èaÈÒ!;K‡Côº (•ššõÝqü(~,1Ìdëv»5—Yˆˆ>òþ»GïúùœˆÏmVx""""¢DŠäþöiVw 8(--UœHX®òÀ $Ê\sÍâÿý—K¸zÝÍÆ R©l½{ݪêmüÊ>…ýô×=Øl6ŒŽŒ ¹¹™UdTÂb±àÀþý8°?Úm6Øl¶˜Nò—eÝÝݘ˜˜@}}½ªç¡Øívøý~­Òƒ’$ñKŸ(Å0àˆ4A’¤·‹‹‹_pX+Ûœ••…¬¬,·NÌü¿«À·þ´å…&X¢(Ùcµbttm”‡t»Ý«•´ô––½^¿zSš™™¹ô ‰e¶y«½”Z°©0.E™L&˜ÍæuƒùÖòÞïG£R—–˜EXãæ}Á[&cf¤§Ã•Éê1VTT¤8Ûá¸k5³ñ’P?H£¶–0-EäúÔœòûCdUIõ™™ r%""¢˜¹} .\O2{>ÅIII t:dYY°xû¥!ìªÞÌ1™{˜žõ¢gH¿þnN7|EKYYYL&‚?ýxêǃ Y:|ñÉZ¼úÓßÀŒ,ÑÇèÕ«xñøq8p`5‰#©ÃJÕ‡ÑÑQœkkƒÍf‹Ù{9üüç?‡ÕjJ0kn·cccZ;„ïH’t†=™(õ0àˆ´ä€F[´²Áyyyp:x¬ªO¬Oï*e QŒØ¿^¯7¢›Ñ• ‡††Í·‡Édº% ?''ÕÕÕ¸|ù²ð:_;û¾¶ÿqNLNaEEEІ“˜žóœcŒ¸ÂÌùü<IdaqñŽ "Š-‡ÃÁF Ò¨ÿðd-""MÎir»s" ºÈÈHø>6[¯CFFšø6¤§ó^šˆˆH%´]š’KEE/wê|7¾¶ÿÓlÀÛ¬9¼ÿûш3°ÑÚŒÆØF3IZ,m*ÌÃW¿ðɨ=ø|>´´´ «« ÍMM1k[SVV†æ¦&<ÓØˆsmmhoo‡Ïç‹úûȲŒ‹/bûöí¨©QO§P(Ó*12à {/QjbÀi†$IîâââgœÖÊ6§¥¥áKÿÇáDZƒ(š›š â ‡ŽŽÔ××kº-222}KЃÅbAyy¹Pùd˜žóâµ³øê>ÅΖ¢DfÚþ­_|²iiÀÒ’øûC!øƒœ®EVà ¢Û8ʼn(|###1[÷žG·2-¥¬hÇÎÌk?pÚ¥CFúúAS³^v""¢{ÐBviJ.¢ÏÕÇ]³8u¾_dð;ƒˆ’ÄcöñI”o*Pý¶F3躺ºðWÏ=Çj*e±Xp`ÿþÕÀ‡sçÎÅ$ð¡··n·õõõÐé=j·Ûo™O¢Ç$I²³×¥¦t6iÉ’Toji›Ï]vá——]·¹æšæAP  ±ˆèÃÃÃl¢ŠÅ dé3ñdý660QªÆäÕêhwû †ØP* …0ï `ÞÀÔ‰ˆb“eƒ&2KÇBIažâe4˜ñW•Ìf3 …–}ÿÊU¼åjJ¶Ûô¬í—†ðƒÖ·ñâk¿Â[ïö0Ø( œy§G;× 7‚²õÑ™˜¾RíáÙ¯} o¿ý6;ƒ F<ÓØˆï¿ôלo)‡Ã .Àív't_Ýn7ÆÆÆ´vˆÞ‘$éeöT¢ÔÅ D¤EØäieƒŸ}ý :¿õ l0ðc—(Ž9‚ãÇcôªøC`»ÝŽüü|TVVj¶t:ôz=‚·eÄß±cÞ{ï=áhý¶Ž~l-)À¥v¶TYY‰îînÅ˽ùN>µ™™€È<|·×/«; óxòZY233±cÇŽˆÖûÚÿ× _@fgKA%%%BËõI躆ô @i…Nðl|"¢°º‘vqR+Q’XŠâª––ØžDD U•ßÇ$>”0EEEÂU€åJ?h}Ó›(ŽÁ1Þz·/þø<+9¥¿Ç—W‚òs£›íff/?Ž_üó?³c¨”ÑhÄýûñÒ÷¾‡ººº¨®[–e\¼x1!c7v»]89f}[’¤Kì•D©D¤I’$½à-mó¹Ë.üò²‹TÍëõâô™38zô(þüàAüùÁƒxî¹çðzk+\.mõ_£ÑˆæææˆË vvvbbbB³Ç4++ wü>''UUUÂëõBxíl'Ošd2™P^^.´ìç»á ÈHKôz`®¹* @f& ÏÒx×BD1»ÝÎF Š¡Xß/}0À -DDZÇ"¢Ä*-Ìc#PBÕÔDVadÜ5» ÕÀ_@Æà˜ mýxõ§¿Æ_Ÿx ?úÙoÐ~iÓs>v¢1îœÑÜ6o*ÌÃ×ö›,¢|Ÿ¸„S§Ná¹çžC»ÍÆÎ¡R‹‡‘o|–‚‚¨®»³³3®An·cccZ;H’tŒ=‘ˆ8uˆˆ´ì MÝ =ûúÌúBçÿç?ãG?û ÎwôcÈ1ÉNA”¢¦fµàdÈÒá+_øvUoŽúº]““hiiaàƒÊUUUᥗ^BcccÄsPnÖÙÙ‰žžž˜o(B__Ÿ›þYö>"€L6i•$Iöâââc~ •mžõ‡p¸õ ~ü;xI5Vª:´µµ­û:ŸÏ‡'Nà…ï|eeešÙ¿• ‡£Ï?/¼Y–qáÂ444Ü5x@Ͳ³³1??ÅÅ;GÖzè!ÌÏÏc~~^hÝmýØZRÀRàq ð++å·N§âe‡“8u¾_|²võwiiË? ÇŽ®%¦%ÒôõY4%¢$2E—Çd#iýmåÒØDD‰ ‡Ø”puuup8åÈp~÷á8~÷á8t™(+ÎGùýù(»/› ó`Î5Ä|_|y5Kûàà…Á1üã®Yl"º«q× j(Öä¶²tøâ“µØT˜‡·Þþõ•À‡3§O£ñ™g°³®F£‘FežilÄ«'[Z¢@ÐÛÛ Çƒúúú˜m·Ýn‡ßï×Zs¿"IÒÛìuD0àˆ4N’¤—‹‹‹xD+Û|î² ¿¼ìÂïàä`J¼ÑÑQœsöï×ÖC«" zp»Ý«•´Æh4Âçó­Yå!''ÕÕÕ¸|ù²Ðº§ç¼8u¾ŸÚÍ+…¬”ß^y𢃈â'??_ñ2N¬Õœ»< °qˆâÀ〇éY/™ˆÖå È1s5Ñ];™qžÔ¡¨¨Û·oGoooÌÞcÖ³œ½yr†÷’D¤>ÉòœëR ¾ùŸþ^;Û‰¡•n¢~¯ïóáÌyV«{¬VTUU±©Hþ}¨®ªRœhônìv;ŠŠŠPQQµm …BQ«Dg%Ir³—ÑŠt6i$I—|[KÛ<ëápë…Ï=VƒÌŒŒ˜¾—ÍfÃñï~Ï=÷εµÅüy$…¯¬¬ /¼ð¬7’pFª³³ÃÃÃQÛ>»Ý¿ß¯µfý‰$Io³wÑÍð@DIA’¤c>ÐÒ6Ÿ»ì©N‰â®ÝfƒÍf‹x='OžÔäMôýû#¾Ñ´Ûíš z¸[æç=ôrrr„×ÿV{/Æ5”œÑéS555­ãškßo};¡3Éja!üì®f³Yñú§8@œÔn/[¯% xøHOOOl¿KN62Ñ ±Ä‚Àb~½±Èˆ¢XñdÌÌû01=± 7Ç\»î†49‹©Y/æ<~øòš•‰H[8Q‰(~ø<‹Ô¤¾¾žAD¤J#‚ÕÝ•Pãs®HìªÞŒŒ¦Þxª IDATŒ´¸¼—kr­­­øêþÏ8ÙÒ¢ÕÌýI©¹© MMM0 ¯«³³3* ­Ün7ÆÆÆ´Ö”3že"¢Û1àˆ’ÉA­mð·~ö\òóÈQ\9}:j7Ò§#¨‘èͺººˆÖa·Û£UÈÎÎ^÷5;vì@ff¦ð{¼q¾[³dILee% #Z‡?Â[í½xñÇ¿ÂûW®²Q£DIÀ%?‘ –K¬ (^OäT<ì1˜rMNò`Eñ¼½—q×lB÷q­É×A9ăO¡…EÌyp¹=«ç׸s.·s?AžkDÉ,“D´lÐÁûXR=¨' }drr“SS1ŸD?状·ÞíIȽûíU˜ð/ñöX­øæ‘#Q zèèè€Ûí^> iõ;î $Inö&"ºˆ(iH’t À+ZÚæY϶^áÁ£¸êÄ´¶¶6Í>lnjBÙæÍ­#Ú¥ãá^7ÖÙÙÙ¨®®^ÿ5×,ÎwôódK1õõõÐét¯gz΋Sç»qôþo½Û£©Š!Dj'ðpÖö{6\¢®Ù®* þZë38ÖÕˆ(²óVèZ)ŽÙh× n¸}òõ‹; [ p˜˜žÇˆ4‘kS˜˜šÃ̼Á I.£r1ç‰C"i×4+< Õ××cûöíl"R…¿;y’ßÉ ;gžÐm¥êÃs_ÿ:^9qí6;s•••áû/½ñ|Y–a³Ù Ëb 'ív;ü~Í%á}S’¤3ìED´>Z%¢ds Àˆ–6ø_?tãä;ÌfMñ‹òè'OžÔdÙu£Ñˆ#GŽD|“ÙÝÝQT}¼ét:èõúu_c±XPZZ*üí—†Ð;t'\ 1™L¨¯¯ÚúüÚ/ áÿû¼øã_áÔùnÌ{lh•Yä,GM)**R¼LPáÔùî„n·HæE£Ñ˜rÇ÷ö€–‰‰‰˜Ww ¢Ä+sš#FÁ 7g–wÎ0³ŸÐ:VJ îÝ»WÙw²²²‘‘……»g‘ÌÌÌDuu5º»» )ŸŒá„ðÚÙN|õ Ÿâ ’B***˜UœH¥L&Ìf³PU¢Sç»ñæ;=¨y Û·ÞšŠc¶¾€ŒÁ±Iô]CÏ 0õ&ÞF@000 ŒŒŒ‘GJés.µñd¼ùN&¶Õ59‰¶¶6´µµ¡lófX÷ìÁκ:X,žqÐÜÔ`y^¨”””„•ଯ¯OhŽG‚½#IÒö"Zˆ(YÐ`‹V6ø_?tãä;WÑüøf=Ò¤“'O¢ú¥—`4jï¡DYYš››qâÄ áu¸Ýn\¸p šØg“É„ÙÙõ³YåääàÁŽþrL¢½{{jà "ç–ÛíÎê.ª¾¾ô@¤ÒÏ~ÑARP¾¥ÚÃö­ÅØT˜‡J °©0†,±€¿qç ¦f}r¸08æŠJ¦E-^‹ÜLd‚ÇÊg½,˸té’Ðû–ß¿ºL¦^9í6ºººàr:ášœd£jLÝc øæPŽi&yÎÛ%úÈœ7À â'ÑMV®§»ººØ¤:ã®Y<„‰h§¨¨ Æðð0œNgÒî«N§CQQÑêÙl^ý[GGd™••r¹\h·Ù´šÝ›¢$‘ ášÒxÀÃùŽ~á¤M%%%˜˜˜HÈgÜèÕ«hmmEkk+ªªª`µZ±³®Nóc+j ›Í†Ï~ö³ë&át¹\Z¬ä1à { Ý ˆ()I’ä...>࢖¶û[§?Ä'ÌÇö’Dҟχ“--8|è&·g]šššÐÒÒ"¼·ÛŽŽŽÕ ßj–ùùy,..®ûºûï¿?¢›â·Ú{ñ@©…HQ°¥¬Lñ2Á`0!ÛZ__“É„ÞÞ^¸‰ÇÒÅÅ%6´ÆúÄÄÄDÔÖ×;$¡wHÂù•ï½› 7J ó}—ˆqç |å åÓs±l)ø¼T“H&\¸pA¸äø#•%pNÏ+^ndtUUUIs®ŒŽŽFTù‹(Ö“x ÔrË÷q¼ÖÄÉ»”Âr3ó~x|Á{Þ[­Gƒ“QîÊëõâ•'8Á’Tmj65'ñ?PR°úGÉ=2+Ð$VEE***àv»Ñß߇áù€ÂÂBäççÃl6¯þw-ñ®&ír¹’"Óùë­­hkkãÉCšpûs.-wΠýÒðç Õj…,ËèïïÇÀÀ@Â>ÛûúúÐ×ׇuuu¨««cðC Eô Ë2:::`µZ×ü{(Òê½Ø1I’ìì!Dt/ x ¢¤%IÒÛÅÅů8¬¥í>Üz¿úëñ’&uuu¡ÝfÞ»Ü`©Ý«£££=´Ûí0™L¨©©Qýþ x<ž{¾®ººï½÷ü~¿Ðû¼q¾µÿÓEÑáC‡püøqŒ^½*´|GG>ûÙÏB§û¨ÚúØØ˜ƒégd ¢p1àˆ’š$Iîâââƒ.ji»_ú¥¼£ÛKrxIs|>N¶´àÈ7¾¡Ù}hnjÂèȈð &tvv®[ŠW ÒÒÒ ×ëêܓ“ƒòòráòÁí—†°}k±fË’’8³ÙŒ††ôôô¤Ì„k"µéêêJ™}µÜlÕ2‘ .—KxxÛ–"ä³fS\¶W­íɵQ¼¸fDJOsÞæ<~ø2ƒˆb&Y‚8É’´Â”á È0d騤y:EEEkLLLø((B‰›VD#!²,ǽº\éëCUU•fû™ӧyB挻´„è ÈxëÝËŸÙ55ë'\ jóx<ÀððpL«w‡c%øÁ`0`çΫ•HœÑhÄáÇñ7G %a’e]]]«ÕŸü~¿ð<Ž;&I’=‚ˆÂÅ€"Jz’$½]\\ü®¥íþO¿úëaƒÕ¤=}}}8}æ žilÔì>9r$¢¨z¸pá•è7“ÉVÀ°œ­ßív O¨¶_ÂôœX•àÚÚÚ[2ò¯Çd2¡¶¶µµµÆÀÀ@ÂÇW}>l7ªÉ0ø!r‹Ï>Œãßý®Ðòv»(**B__B¡ÖšàI’^fO "%ÒÙD”"žÅr),Í›òã{¿æ‘#Í:s挦3þ®DÕ áuȲ ›Í–ð¬ ëÉÈÈ@vvvد¯®®Ff¦X Öôœç;úyr¤°¢¢"444`÷îÝq â@{l--}ôÿ=¾ &¦ç1îœYý™žõ" ‡ØP –ˆlh‰TVV¦ù}Q¼ŒH°\Ýáö ½.CÑ:X(þæ<6Q ,..azÖ‹iSs v ¢¸J–ÊiDZ1èàÄ`"5òx<èííZ¶°°¹¹¹)ù]ÌëRƒŒŒ ¡±¿k“Ú žžë6›Í¨¨¨Z¶¢¢ hhh@eeeØA±´üpâÄ |å«_ÅÉ–&PUU…Æ’ˆvwwÃn·k1Ùà €ƒìD¤ˆ(%H’äÖâÅRË;cøÍ‡Ì‚k¡…%6BŒüÝßýð<5°X,øæ‘#=¸Ýn´··«z?•ì_vv6ª««…ß«ýÒz‡®ñäHqxê©§°{÷n²A4jqi ¾€ŒiÒä,æ<~øòêÏÔ¬c×ÝwÎ0ð!AzzzàI’ìûáJ† J˧¥¥ ½OFz:«Ýzçõ@Öx-_ï­`%Š’àã“36Q4¯ko t˜šõbqq‘BD)0§æ55Q¢ˆLpžžå9G¤F‘$o©¯¯¿¥ª¿‹‰âkÓ¦MÐëõŠ—³k'ñóâŸQµµµ¿¿ÙlFmm->ÿùÏc÷îݪ©ÞÃà‡È<ÓØˆªª*¡eççça·Ûµ¸ÛÇ$I²óè‘R x ¢”!IÒojm»¿ÜrW§¼p{ƒÜ~ØžÕŸ¾ñY\™¾ëOßøì-¯—Ü~¸½A¸½Avˆ›CÈ^ÏÖ­[…³\ÃÏNŸÖôþ—••áÀ­Ãét¢££Cµû¨Óé=€²X,°XÄË}¿q¾¾€Ì“K!‘I¼jÏæPQQ½{÷®f$‰GÕŠž…Å%Œ;gî™ùÖ1îœÅœ—Ù¨ãI–e ¤Ü~oÑx…‘I÷KKbÁ»ÖG×Î&¥×)¯äÄ,rDâD‚‰Ç3l8¢(` ÅÛJ8%¿,kê+ À%¶cÇÅËL1àHu&&&àp8„–ݾ};L&SDc ZN†ɘQ4 lܸ999ÊÏý©yM"޹0$X!ª¼¼<¢€¬µTTTÀjµâ©§žBmm­jÆXü æð¡CBI8óòò´¸»ïH’ô2:‰ÈdQŠ9À@3W}³þþòçö)Ÿ¼5ƒ{ å™tÈLOGNvæêO¶.#¥:EpagÆ:L&Þ±CøFôüùóصs§pTºì±Z---Âë°Ûí(**.UkƒÁ`øÁPÕÕÕxï½÷à÷û¿—?©óÝ8øÔnž` ” LâUrLi%#Imm-Ün7ÆÆÆ011§ÓÉ/H–c?‰fAÁ÷çââ"&¦æ™‘C–Ž(ºººâÒÔFë¹Æ,èu™ÈÒe"==²äœ¡…EÈ¡ð’þ-..!(‡Vÿ=3ïû݃Bï»R}!V*ËÖÕÕall ‡‡Cýíæà‡²Í›aݳ;ëêüàÀþý¸rå |>ߺçë† ´¸{ïH’ô2?q‰( x ¢Tu @#€-ZºÉܰa¦§§…–ÿ`ÄF>zØkÊÊÄ£åf<º%ŸÚV¸nðøCðøCpL-ÿ» 7 –Ü,˜M:d d¬R ¿¼€E•fÁV½^O|üãx·½]hùË—/ãÂÅ‹ØûÄšn‡æ¦&?~£W¯ -/Ë2:::V'”ªIvv6æç籸~ÐC=·Û­øÁø!œ:߃Oíæ F”Bfæ} xˆ¡ááaÅ“fWÜUUU¬E÷¼Ö2™L« 8òóó¡Óé …Ð.8þo±Xî¨ê‘¡|îÈœÇ˸Q B ºãu7'Ñ È ŠÆ´Eýò_ûÖÜ–pìÞ½[ý ¢¢ðxxÏ׬Wý!QÛ[¶lQ|m Æk9ŠŸá °ÚÚÚ[‚ŸWš•`ÀQ˜×žrÒäB¡6©Öœg¹z/ƒ’‹%Ï„q׬¢eFGG“â¾&\f³™×Ô)®§§Gxrñ틊Š?wÎð %˜ÇãÁÀÀ€Ð²%%%kŽ7Š$¸ù»8•<ñÄš³¥è8{ö¬Ðrz½~Í„0ÙÙÙ˜™Qö;îš k¬7Þ‰ Æ3°_›ŠêgT"­Tœ¨¬¬TeðCWWºººðºÁ€;wbÕŠªªÔ«»Ó°olííkÅgeeÁ`0hq·ŽI’dç'.EŠD”²$Iz»¸¸ø‡µ´Ý¹¹¹ƒ…BQ]ïõ?~Úy?í¼ª8øÁãaPšÇ 4¿Zùá^#ÔbÞâÉ Ðκ:\¿~]hbc À«?ú¾õüóšn‹Å‚o9‚zp:èèèXÍž­F£>ŸOQFŒœœ”——Ãn»G}ã|7¾yÐrKV¥"YVKÊ6oV4©Tt2Q¬ù2«<Ä@¿ðy¿V6´»)**ZóA½,ËaU#»9‹T4?¿´^î8Öª›,ÂÜdÙÀÉYDëe===BËšÍfTTTÜñ;¥Æ3À=*¶¥º€¸s6.‰"5çñC—‘Žü F6F’È5e ¯©.WJ]S«eb%F$“œ ïx.!D<=Ëd1D‰ÖÑÑ!¼ìÝžgŠ|¬ÐrÓ²²2ÅÉ<8†D===Âýà¾ûî[3˼Èdlµ&ö¸øÛ…–ÓétŠÆ\AÍÁ>Ÿ6› 6› –‚X÷ìÁ«Uóc@á2h|æ´´´Üòû´´4äååiq—Þ‘$ée~âQ40¥&¥ºcF´¶Ñ±¾ˆ] ~ø¿OvâÏNü?yg’ÛÖ²“sôÏÂÖïDßø¬ê ÜÞ Ï…ôz={ì1á准†pêŸþIóíPVV†D´»ÝŽááaUíWZZÚ¥GÃQQQœ±L„þ@o½ÛÓ+ ÆÔ©‰ÇÇïߨ·i„JJJ"ÞN· ±ÞO8ƒŽáNÜn‹Æ'Ä8àá‰]…ýÚ\“òàe§`VN"-êéé,‹e”[k°3??_ñzXáh}‹‹K v Í™šõò^)‰ˆùk=«tÙfeÁ˜¢×SÄkêµ’‰LpVèOÚ 2rbb‚ —•¶oß~×ó^$©Àêw±†«˜Šô}¦6Y–…Çrrr°qãÆ5ÿ&2Îìr«/øæw^~þVYYQðU¼­?444 ¡¡AUÛę3gðÜ×¿Ž“--Š»´jÕ KAÁßoiiiZÛ•ù‰KDÑ€"Ji’$¹µxq•™™‰ÜÜܸ¼×õ?~òî0öÿíopôŸ~[xž–pÝíÇo‡¦pid:쀉x -,Áà B6æçcÇŽÂËÿâ¿® ¶ÍÆÆÆˆÖÑÙÙ©ºŠ¢0ª««…ßóý+W18Ɖ’±ÀÁcR#/€ÅÅ%6DE2Q ®®Nuû#2Ø©õL¨® ìxð~E®Rqr– §èwXfw+e¯Óé Ó)«XÆ€¢õ»fì@š41=Ï{¥$±É²Añ2ZŸ¼#’¨ƒ×#üm IDATÔ)úY71!<>°Þ¼ÂÂBå× Î$“*ÙŸ“Agg§Ðr&“ Û¶m»ëßE’ $Ã÷±H2û¤¶®®.á1…ûî»ï®©ð09£®€‡€ÂûWÄžw›L&ÔÔÔh¶_˜ÍfÔÖÖâ©§žR]ðƒÍfÃñï~Ç¿ûÝ”|h|æ™îµŒF¡`"8&I’DDQ€"Jy’$½ à­mw".hÝïÂóÿtyµêC¸Õf<2úÇgño˜„ÝéAhAƒv¬î™‡wìˆ(KÊ«?ú¼^í—Œ~¦±V«5¢u\¸pAUec322­<³sNNzè!á÷=uþ|NÎ_È`H–t¢x˜óúÙQéDH¾Ïcr&0áÅRP£Ñ¨éã«ôz]vU+Ë´šŠ“³Df‚AÞO¤¢ŽŽáe×+e/òYÌÉYDw¹šõ"d‚ Ò¦ÅÅE¸f¢þؾ}ûºI”&¸V«˜Š<Ûr»ÝLš•¢Ün·ð˜ÂÆ‘““³îkD‚Ôtô›ßÙ”„–ݽ{wÒôµ?ôõõ¥DàÃJ•‡x&IJ7%Iz™Ÿ¸DM x "Zv Àˆo0Q²l¥êßø þÛ[W®Þ0âôàß>táCi~Á›Ä¨ÝÈ{ø'R?þ¸ðƒCI’ð¿ßx#)Ú¡¹©IqÉö›É² ›Í¦ª‡Š"¢ ´´Txâìôœç;úyb­ƒÙ©(™¸çð-ÝÝÝBËÝk¢@¢ˆj•mÙ¢ùã82›Û‘]ÕeÈÒe*Z&'g‰Ì0 ^Ꙙ˜ª@,OÈXo@P$e Á÷ÔDjZXÄÔ¬— Aš6çñ3!DÈ5fA¯ËPvM=9©é1"Ï­xMz†‡‡…¯©kjjÖ™€7îJ­ âü\eÏýS!{2ÅŸ,ËÂO………¨¨¨ëu¢´ZÅÔh4ÂRP x¹±±1vÊÔÕÕ%´\FFƺÕVUyp«#àaÜ9ƒþ±kÔÂÂÂ5«»&5?¬>¼râDL+h'Rã3Ï //O‹›>à ?m‰(Úð@D@’$·/¶ÒÒÒzqë „pîƒkØÿ·Ê–à˜ò¢ã“èŸMXà+Ñùh·Û100 š}Ý—ììì°2ìÜÍ›ïŠeöÉß l{c1±4Öª«ª/ÃrĤfS³^,..1{© Y–…«;„3Q QDÙD>Õd$F©Oì|PxÙT›œUÈl´tðxÄ^·oß~Ï^‘ÉY.7ˆnçñ1¹%_@æ} ù,få×ÔW4œM}‹`1Ÿ]¥Žþþ~ákêÝ»wßó5"œ,I_n·v»]hÙòòò°ïE+‘Ú®lR%ðŒÔápð»8ňŽ)èõz‡õÚœœÅëWC…‡þ‘ á#÷ªîš¬Ö ~HD¢-ŸÏ‡ÖÖV¼râDRÌ=€Á¡!ü[G‡7ýMI’ÎðÓ–ˆbDD7‘$éYhm»sss‘‘‘¡Šmù`Ä¿úÇný§ß…]ñˆàƒkŽ¥£éÿ¸ðk Àÿøá“¢ÊÊÊÐÜÜÑ:º»»U3y.++Kø³¥´´Tèa\sÍâ|G?O¬(™žžf#¤ ¥¥%Mlg(´€™yÆ3BŸ¹©®¿¿_x0*œ‰‰’Šb1Z~ÿFl*¯—j“³Dú,Ëp8üÒI²,£§G,(·°°0¬``‘Ñ Ì ~¢›y|A,..²!(yîç9 W(àA$ˆ0VR-ˆØb±%Pã Ÿ ßÓp²ŸÂ°Îm‘ ÎCŽI¢8­t®Óé®î z ßÇ¢I³ø]œ:zzz„ƒ7mÚökE® ]á! ‡ðëß -Nu×T°üðùÏV«åååq~èêêÂÑ£G5}_,püä'?Ñâ¦Ï8ÈO["Š<ÝIs_iiieªˆ…_÷»°ÿoƒÿöÖ¡À»ÓƒÐBì&kº½ÌøMz½ŸøøÇ…—ïééÁÏÏžMжØYW‡ÆÆÆˆÖa³Ù„6E[$…«Jç®hëèÇôöo'’Gdâ0i_(¤ îy?•çÆ”>ƽ½½Bˆ;Q D&[ 4ßFGF¢¾ÎO=RÑò"“³´œOô{–©¡§§G8À,Ü "×Ýã®Y¢›0ˆ’/ #À~­i"Aı¸7ˆ'‘çÃÃÃì,¼¦^W}}}د+SKE€Z­ðã üÿì½{T[çïýå"[\ÀŒ_@Ä—pìi*ÜÖï tÞ•¦x:Wü¶“5/ÌZgÍLÜž9ïyCפ“Ö³šÓ9§'ugæÌÅ^3MfÙï‰gb;¶³NÁ­Ý5)”‹CòE lK‰‹$ÄòþEÖ~öÖeK¿ÏZ^iAÏæÑ³÷sÙÏóû~<¾y¸¦½kþMÍúÖ|†Ø±¶‡ƒ- Øl6æ¶.//õÞ,Uð0¬Ð@Y­V ]q±èrÑ”}žRćéééÈÊ Þ\'))Itæ¥ya3ÞÈ™Vv Œ`žÑ˜³ªª*"Y ¢™¢¢"=ztø!\8'&ðý×^S´èáâÅ‹˜››SbÕ_äyž‚‚$x ‚XÏó·|WiõNNNÆsúýøúÁÃø}‡p¬d7Êsó±]“Ñzµ~4ŽÆ¿ùo¼gÁì\ðtÃ~uω!Gh‚¾)ÃüC/Á¹àòåËQ“Ù@*'`0˜Ë ‚£Ñ)dSSS‘˜È¶dLOO—´yñ¿Úz©cm€ØÃA(1Õ,--aÆ;G !Ö´Ó€¸@pÃ<®+)Qô½ôz½°ŽŒÈzÍÕÅÈàÔ’®Áœ5¬ðà,ÁƒÍf£y6Æ‘r\ZZ*J`ÆœE°ñôGÄ"S³ô®¤dXDÄΉ ¦ÌJ^S;ލ1!BƒÝnÇÐÐSY±AÎ,Á†,™GŸ´Ff¼þ±‚ÓíÁ˜cjåß0ïÂýQçʿчî5¿ç'¦19í]óoý5î:a›Ä˜c ®i/½<¢Dá{E± ë~&‹szNNޤº*9HVÏ >t»Ý1sFKlŽñ¡˜ìÄ ÀéŽÌšÐ9åÁ{ãLeóòòPTTDج?9r$,íåóù+zèêêBÿ'Ÿ(ñVŸáyþ =ñA„<AlÏó¯øHiõþØå@JJ *róq¬d7~wß!4×|ÿùÙãøúÁÃ8V²%Y9©Ûï[ð?þ?ý(øÅÅÅO—…w'De‰x³s X aöˆxæÀÌ®)‹‹‹øÎ_þeÌ´ÅÉÆF&•n·›9µ¯Ü°¤ PVV†ôôt¦²lè éwSâ2ËaË墊ˆj¼>ʼ$»ÝΔ (nX¾K@M41 sV„UìÙ!ù:Ú, RTI¢Êø|>EW2tÃ26 §Ûƒ…Å%Y®¯d]㫸mß¾}¢ß—¥:­›ÛÖµŒ†l4Ç6Rć۶mc:+f9‡˜˜ŠÌ>×±g«f\ÿÆ#*• eee0 xþùçQUUÅd,J=¸\.\½vM‰·wÀ«ô”jHð@±9/˜RZ¥Á`~ií&ijr2J²·áXÉn|ýégðŸŸ=ŽßÙw·Bœ¶ºyü øë«hþ§NÜ>øÖ/,bpl·‡]¢²Dl†œâ b-)))8v오—Îï~ï{1ÑÇáÔ©S’ÄCCCQ‘F–ã8æ,[*ûï¼'h—Î=;µ¢¯¯ÄÃN'º 9óDlÁê†Æ(NXò+.x0É,xøüÁ2¨Uò¬ñYiåp„½^Ï´vëïï§)F±Ûíp8LeYf,ÁY~a‘nAD ³´´D¢‡#68sýš%sšIÁkj­V mn®èrCCC”å!F±X,Ìkêýû÷‹Zfq“žœþLØ5¬ÎÈ3 O>–yAnñ‚\sÇÔ¬Ãã“°»f±´ô™Øî8ÙCç8Žéý ‚ 0Ÿuåå塬¬Œ¹,+Ö‘Ñ¢ÇhA§Ó1²9X,z`cVAKRRSv€ÍP/‡ísN3•-//iÀ~,£ÑhP^^ŽúúzÔ××£¼¼\²Xm#|>Ξ=«˜1ý'o¼¹9EÆ3½È󼛞l‚ B ‚ 6çùÛP uV˜Ç/Æ·vDOMNFEn>^¨Øÿ'â‡ûgñŸÞìÅßµÞ%`˜òè~0 ÓØ4$dhpÎøé!ÛrrpàÀæò<À'ÊLÑ÷Z­ßni‘tÞÞÞˆo´'$$0HOOÇÎ;™ÊÎùpõ}r•Y ˆ`X\¤àÇXÅl6ÃífÛ3¬ªª Ɇ±\ ŠŸkss™ÆÅhBNǸÜ, *Jòe»^a^–øï£àà,¨©©]Æãñ ^ŒÒÙÙÉTN£Ñ ¢¢Bt9–5w¤œï‚ ˆðá™#Áƒ’)Ô2¬©ì* Õ kj€œ¥cA˜â 8±°Ž;§1æ˜Z#jX‘!šÄ b™ñÌa˜wm*+(}¯(Öèééa29"뜮dSºúz¦rýýýÌ÷Šˆ^¤ˆµZ-’’’˜Ê²f¼áãð øåÇlBŸh7˜RÙÙÙ¨ªªÂoýÖoáÈ‘#’kaÁù ¢¾nܸññq%ÞÂ3<Ïÿ‚žd‚  ‚ ¶€çùxOiõž‚yj2èϯ?”çæ‡¥žowŽà~üŒƒâ^°ºçð«{NŒNŠWaÏ ‹ä€8 ÉÍàoÿöoc&@[§Ó¡©©IÒ5ŒFcÄ7YÒŽ®¦¬¬ ©©©Le»Fp”Òf(a8¬a Ž&”K8SʘnA`ÉÎÎfvC n·›éÐ…5&Zðz½°ŽŒÈv½Ï,•µ~…ÚLÑeœUk00•3›ÍäHcH¹§ûöíc˜±¼Cù…ºYñ)™ "š¡ ÊFËáÁ:2¢HGõõuuL円†È´#Æd^S9r„©Ë>öä”'¦KKKsL…=ˆTiÐøºvb*[ZZ*é¬1''GRÝ•lêQS]ÍlN¦±‡ñaJJ ˜ÿvJJŠh±D¸=îÜdžAüaXEn>~wß!üé‘Z+Ù¬Ô´ÖÓã_Àw.ÞÁ+?•íaqñSÜçgÑõ`noðß“²;„/|á Ì/ù¯ÿöïÿ®Ôt}Qk0ÀÀ<,oFݼy3¢ß!))‰Y°ÉÉɨ¬¬d.ÿeyXÅJ:°!d‡SÓFn¸éëëcÀUUUEýwcc•LwOl×*ݱ)#ÃV°\Ïçó)ú@X¯×C››Ë4×vttÐ@#H˜ååå1 ̘2<¸IhCÒÔÉÔDL²´´wÎܱD§F§]NÉ®ÒZ­z½ž©lgg'9KÇf³™yMŸÏfÌÅ"xpÆIÖ4ûä æãD0]É0ÑØXß­U*•äìR‘•œq‰ã8ÔÖÖ2•5›Í°ÙlôðÆRć………ÒßÓ£8ËÌ׮63")ûDðkº£GÊ*|8{ölT~WŸÏ‡7ÞxC©·êEžçÉy‘ ˆ°A‚‚ ˆ'Àóü€o*­ÞóK‹¸ab.Ÿš†c%»ñgGjñ•Šý(ÉÊ i}9èdÊöà™[ÀGCnÜãg±°øé“率ܨÃEºFƒƒ0—ïììÄŸý,f6™››š +.f.ïv»#DDz)µf\ÉΆV«e*;îœF{ïý-?“š">¸E©‡Ç,‡Æ´AMÈ*™ʉ”@¢¢"æ@p`·Û™Æ(mn.“,šSðù§CsÀÂ’åAN!G$¨«¯g*çp8˜û)]H˜IIeÏâ\97O"€&MM@Ä,s$xP4,Bb%‹ˆ Ž1˃ÇãAÂß'ékê£GJ[ˆ=ÌÇQVðxÉÄqœè2.—‹:®ÌX,¦Œ®P^^.Y° 5ÃC¼f\€ŽŽÊ c¾„3…ôôtdeI7×a1Ó› Svñ[]w™ËJdâÖuáC^^ž¤k9'&Ðn4FÝw¼xñ¢R@¿ËóümzJ ‚'$x ‚žçà¥Õ{ÂïC·“—|§·âëO?ƒ¯<ŒòÜÐ˲=ü·«¢²=€mÒ‹_Ýsn™ÁaNX„gŽ‚A‰^¯—ôâyíÚ5Ü»w‹‹±qàÐÒÒ"I4044‹Å±ú«T*&×ÙÕTVV"™1Hº­spKGC¹]­£½o‰…ñÃÂBx溄jëp"Eô«Ù Œ.eÑ„\Nq‡+‹™\cƒ)8KÁxÀræÖ5[oo/eUR8RƒKKK% ÌÄ®•'§½tÓ"Ð’‘¡I¥† bs~š›§FP0L"âînEçšêj¦Ìi@ä÷@ éØív 1•-//gÊÒ°–`÷p¹IGšxÉ8ÃbÁ˜OlŒ èïïg*«Ñh$™ °Z­’¯¡ôŒK¬Ä@&SÊ|¢lz{{™ïa±¿ÕH=W–±IŒ9§™Ê–––2™–ÒÐh48~ü8ªªª$ âŒQ&xh7ÑÿÉ'J¼%ñ<ÿ*=™A„<AÏ‹¦”V鞉‡÷ÎÊr­’ìmøÝ}‡ð§Gjqp{aÈêü¿?ÇÉ¿ù÷xqõ^\üý#S¸=ìÂÜŽ<[‰!ˆÐ±ï©§˜Ëúý~¼}é’,›’ÑÇqøvK‹¤kôööFÔU…ʼnc5ÉÉÉÌ)>çü ¸ú~u*°¥äöx<äÈ'|úé§Ô1†Ýng>tÝ·oŸä@Pb6›™¿[­Á èûjµZ᜘| N{v„¬ž,ÁYJwÀã8õŒY€åCšs• «ÀL¥RÉ‘@ŠB‚ĶL‰‰tìAÄþù,-Ñ»žR)-Ü&ºŒÏçS|–‡†'˜Ëvvv’èAÁ°ȵ¦f ‚ —›t¤ •YB4Â’q›Þååcpp‡©¬æ-SSS²œ-*=ëЉ†fS·Û›7o’èA¡°fU–Å2r Ôêè›wüÂ>ø˜m©R©(»C„)//ÇñãǙϽL&SÔœ]ŒáÆJ¼ Sèi$"ÐÎ?ADð<ïVꢭÍ6„ù%ùò³SÓðBÅþ fæðÇg;ñvLjøÕµG@׃IŒN®uºðуXÝüܽ{íF#ÆÇÇc¢=t:ššš˜ËGÚU%55UrðÈÎ;™/ºFpÔ÷ýJ¯×3mRÓA1±t˜Ýtvv2•S©T¨¨¨ˆÚïåñxس; ÐjµŠ¾¯r9Ä®,†Z•²zæe!E•$º\·Â„ëëꘄAÀÍ›7ilU Rfr8ÑŽ1­óSºy؃HJD~N:5“l•õ’ˆnÔªdäf‰_'(}M]k00¯~¦ìiÊÃb±0¯©÷ïß/ɱ7À„ æ„òÑ2d§1G¤dNÌËËCQQ‘ä:|øá‡²|—žžx½ÊͬÈqNH ’èA¹ôöö2•KJJBAAlõ‹º¶¹soœ9³“\kBÙÙÙ¨¯¯g¾ÑòžõÖÅ‹˜›S¤èöUžç‡èI$"à B<ÏÿÀ¥Õ{~i¿‘ýº«…y\h“ÿ®í.^¹ø1fçD•[\ü÷ùYÜvavn³s ð ‹ô+£Ñ³Ù3¢‡Zƒ ®Ôn·›ÙyVäpõØ»w/sÙw6Éò¦¿©¡äì!•••¢ËX,Ú˜&6daaAt™üœ j¸0àóù$¹¡Eóæ»Ñhd“N4(ß<ÆØÞ.ù…ÚLT”䇼®…Ú¬ˆ|¿HÂqNž<É\žDÊ„U`¦Ñhd˜ýò—¿d*§ÍÒÐÍ#ˆÕ}2-ùÛh­JÄóÂ5‚‚)cÈòÐÓÝ­øï-eM ·nÝ"!úûû™×Ôååå’ë`³Ùà÷S¶obÙ4H,$x‡¾¾>æ=?9œÓív;¦¦¦dû> ϸT_W'I€H¢åa±X˜÷$·oߎ¤¤$Yê111usòŒ×®¶¸™ììlYÖ*„<¨T*?~œ©l4dÒ»zíšRc_ÞãyþGô)Hð@!žW +­ÒóSès9d½æÃÙ´Ý7álχpxgCV÷_:ñ­7{pÿ7¦<ºLÂ46MO®‚ñûý¸þšš’u“2’475IÚ`´ÙlÌ9Rá8Nò5ÒÓÓ±sçN¦²ãÎi´÷Þìç…yâƒ1•ìÌòñ/FGGiP!Ãår‰.“@íXÅyyy(++‹ÚïÕÑÑÁ|è ÙœN'¬#Òɇ+‹ÃR_–à,ëÈHÔ¤†f¥Ö`` X=ïÞ¼y“9}<^Ìf3ó˜»oß>ɳ?üKKKt#Qc ±9œ¹™’3D4A”M)ÚÚ91¡h³`9àXŠñ °,J%у2d^S9rD–:tuu1•KII¦c”èt¢ËØl6ZgKÄn·chhˆ©lyy9svðÕ°:ÛoFÂ3.@ss³¤òÑ{(c¯€µ¤¤¤ !;Îfð<usò­î{Ìe«ªªè‹2²³³±oß>Ñå¬Ã‘ ÷êïï‡ÑhTb“Oh ' ˆHB»ýAâ_ÌÜJ]Ä}hÄß'ù:=ÿ~ôkœíù6+ü ¡w»ÿpßz³ÆA6цgŽДŽÝnG»Ñˆñññ˜=œ:u iiiÌå{{{#²¹˜œœ,‹»GYYRSS™Ê¶uÆýA £Ó«Ë¡XåX6žwäfRc‡O?ý”©Üþýû£ö;Y,æƒO 6²;È‘2¹¢$ŸIìÇKp–\ß3Ò475IZ¯ ‚£Ñˆ¾¾>Т|îd½GrÌAÀ£ŠKM¡§ÌÎÎR#<MZ óHô@Ä$xP6Ú, RTâ÷ÓÚ•³†“’ÖÔÀ²è¡££ƒ‘£ÇÃlГ——‡ü|éÙ ûúú077'º\RbBÜdMËЈß7)ÔÝžÕÀ€ ƒ¤÷CT*•,û™f³Yös3£Ñ¨hó,Ðéth¸§=±Gt388ȼ^*.–Ï\gjjŠ)³x(çä{£NŒ9Øb ÒÓÓi¥°dÝuNLDî½ÞçÃ[/*µ¹_|/G1h§Ÿ ‚žçoø®ëþÞøæ—™Ê~ôp ÓÙŽkƒ}žr…½îÿ¾sñÞxÜœâ£Ñ«ÕЇ2\DZ­ßE•¶ì V%#—á Iê÷Œ–õÚ‰'$_§¿¿7oÞdv;%BK__óšZŽ€Œ¾¾>f[….n`b±X˜ÖoñˆZ•ŒÂ¼L$''Qc1‰”MYa®è2=ÝÝŠÿÞÇIÞ€¡¡!r—ŽÑ5õÑ£G%ÿ}A˜¹!;8§<sLmúÏ/„o]—ÁÅ×~K–dÊ(#í=Åá`3®Û¿¿ä̉RÌ žD,˜zœhh”É4ÐÆF£½½½ü…x<fÓ³ôôt¤§§ËV—±±1Æy*5díÓ~ûsÙÙÙYF¼õÖ[0°X,Ô¢•JÅ”™$RÙ©ßxóM¥Æ¹¼Áóüz₈4$x ‚`„çùW|¤´zOø}øÐ.îsµÐajÎñïðÆûüJ´ IDATàê=„qÊõë×áõzaµZcb#A¯×KrUñx<èèè{½ÕjyF´Z-sŠâöÛsâHU‹KsªTwªÕŒYÌfs\lÄy6òx˜6õ8µ r”fBWÚ u¹ÜÐBÛí–”R>---&²;8NXÜÜSö …Šñ"ëȬV«âïY}]“Èp=‡­­­ÌA8DèæBÖ{"‡ÀLÊßOLLÀ¡Š"º‰q† ’æÓxD­JFq~6ez b‚yÄNJ¦t‡øÌiΉ‰˜²”ººú½²µµ•Ì<¢ »ÝΜɱ¼¼tÁAOOó~ç³Ëdk‹1ǺFpõý>üóµüÃ¥ðï?ÿWÛû7ý÷/×:ñ—>À¿ýü#üôCºFÂ.„ˆUX‚»ìv;5^ßS4 ÊËË%×AŠð*˜1&8õÒK²š™Ífܼy“úJ”!e¯@Îì<Ïc~~ž©ìSeÛCÒ6~aþyyæU›Í†ÎÎN\ºt‰ÄQBNNŽø÷¬nܸ(±‰‡|“ž4‚ ¢Úá'‚F€)¥UÚ<5‰¡Ù'W{Ø=UB‡Õ´~4ŽæêÄìmøÆSÓÓøÙÏŽ¥¥%ŒŽŽbqqQñßéDCsà:°¼±îÀ¹¤¤$$''Ër­ÊÊJæ²ï¼¿öp³P›Wý¡¦ºÚ\ñ DD(#–ͪX² 5¬n?¹Ù$QÀœ&´®r ÈÍfÃÍ›7%mü777ƒã8Å?Rƒ•285ìÙöz—nc*×n4ÆDß=ÙØ(‹`)@δÑÃ|ÀTN.™”5YUùN¨UÉtãŒÁÁA:Hg 11…y™$z ÏO{¡J¦¬pRTâ3ÎÄJ¥Ô=ÐÕô÷÷£µµ•-£AÐÕÕÑ5µÛíf\äd¤¡ 7CÚúlØŽŸ~hÂ?\úWÛû— ÎiÌ âÎ.&¦<Ÿ\L<BüÛÏ?Â/?¶ƒÃ¡Y3®?°ÞBR3…ƒ±±1ÜøÙÏ”ÚÄ/ò‚aceýÜÂ.ö߯¿~ÜuB‡ÕÜ8‹o½ÙÞ=GcœqçÎ šÍðûý‰ïÔÜÔĸ ··7ìAsr¦¦¦¢´´”©ìÛúH{¼ ÏPW_ÏT.B)(a³Ji¸Ýnx<¦²;´™P3i¡E£Ñ ¢¢"êêe±X`4%|UWW3RGÆövIåŸ=X‘ç NÜ,ñbšv‰ß7Zà8-2Ÿe{èèè CáÌf³F£o¿ý6&''™®#‡ÀÌn·Ãáp0•MS«ðÌSÅtCã ÇÃ,X wf hD­JFvz*=H„¢!·ïèÇ;·õÚ®¬PüþŸÑhTüþU€æ¦&Ù2-F#óþÁ†ÍfCoo/Z[[qéÒ%ÌÌÌ0]gÿþý²ìù±Š‚ð~þ)¦²3^?ºFðÏ×:q«û†Æ'CÒÖSܹ7Ž«íýøçk¸Õ}–±É°Þo'ã;K4 Õj™ÆÊò‚ Àb±à½÷ÞÃÀÀÓ5òòò$gNƒ§X1õÐéthnn–ízf³×®]£Œ¦Àn·£¯¯7oÞÄ­[·˜®‘””„¼¼<Ùê466ÆlVx¬jwÈÚjjÖ–õ‘ÑhÄ¥K—ÐÛÛKëÓ0ímíóùðÆ›o*µy¿Ëóü/è)#"Z ÁA„Dxžÿ€w”Vïù¥Eܰ =öóå¬ïÃ<¡Œ¼ûgÑüO¸ÇÏÒÃg¼û››Ãìì,sR4ÁqN:%)ˆNj0©XÔjùfŠ‹‹‘šÊxòÎ{ŸülË/ÂPz€Zƒù¹‰„P†ˆîÞ½Ë6^©U(ÊËBR½NFûöí‹:qP__:;;%]#-- ÍMM1q¬V+¬##Ìå µ™Ì™äà Cf ŸÏ3Ârºàµk×Ð××G‡áñx`±XÐÑÑëׯ£µµ½½½°ÙlXX` •K`Æ:>&$ÏöÑÍC¤ñ¼pl?5 €¬ô4jBfgÅïE>)>±´ô)Ý€0Á²g27¿õ}®Ð±”ÅÒšº¥¥E6Ѱ\výúutttP`Yû‚ÙlÆÍ›7ñÖ[oÁh4Âl6KÚWÔh4(//—åþ³Šˆkô;E‹BýºFðo?ÿ]#˜£m^XÄà°­¿2៯uà—[0ãÀY¨ÍõyçÄ„¢Ÿ_VàÎÎNzO߀€È!ÔÛÙÙ žç™¯wôèQÉu’b& cŒ˜zËÙOšdÜo d4½~ý:, u”±ZàðÖ[oáÖ­[èïï—ôü")Is+ŸÏÇ|f_›=;µ!œCÃ7_ ‚³Ù¼²i6›i> 1,ï:.lõ»xñ"\.—›ö#žç_¥'Œ ˆh‚"T‚ äáEŠË';á÷áCûgiõÚî›ð¯wÁ¿ ,§0ßz³‡Dq†ßïÇÛ—.XÞà‰—3N‡“'OJz™‡‹M€„„ÙDÉÉÉÌY\3^ÜèÀ&xð(üÙá8õŒ‡6póæMEˆXêX©×Ó`¹ £££L: +ÈA9ãFyyy(++‹šú‚€›7o2»P¯æ›§NÉ–U(Ò´¶µI*ÿìÓ‘½Ç¥Œb cŒgÖkr‹A@?®]»Fî_2µgÀqöúõë¸~ý::;;144$[ÛÊ!03›ÍÌõ9´·¹YÝì8CJÏöm8T^D 11¹{FžýŸŸ‚e‹küÒ§[ R 󲘲¶ÆL»†Bô,‹‰Iø «E×.]Z ËP|äÈY®ÓÛÛËT.ƒSãp¥¸gÐ26‰ó?í»Ða#æ…Eܹ7Žó?íÆÕ÷û08LÙ6£¦ºšé>Üç-Ñþž½^ä`³Ù$_WŽÌ‰$›­‹sbÝŒe¢‘ZƒAVÑC ßtvv®(È[«³”Ê%pXMZZ¶m“Ï\gllŒ©\rR"~óÙÊЮm|þˆÝÃÞÞ^\ºt F£‘úEÛ™å$´èÿä%6ë€zº‚ˆ6’© ‚ ¤Ãó¼»   À-¥Õ½ÏåDaZ::¬ÅduØð%õ‘èáõoTcOA:=”q‚ÕjEç¯#Ï<ƒññq”––ÊæB)j ˜L&æÀ@›Í³Ù,‹;V0¤¦¦Âï—g“hÇŽàyžiSâýÛ÷a8´ ©)ÉLÏQMuµ¢Ÿ›úº:´¶¶Âçó‰.J>~ü8²³³£ö;ÎÏÏÓ ' Ì.K øÜÁ2¨UIÔQFucv»]¶¬C ÐLjxÉëõâ׿þ5sùŠ’|h³4ýjU2JwlÃи8§.“É“É3÷2 zøþk¯1ͽ[ÍÉf³f³¥¥¥(++C~~> pA´›Ýn_ùj!§3AÐ××ÇT67Kƒ£ûKèÆÇ!¬A<ÉI‰ø“ß1PÖ‹KÔ„dl6[D"dO÷9TnÑ%ùè—ù-d©ô=¬ÑÃk¯½&) ÞF ahhyyy¨¨¨@Q‰þ¢qM-Ç»N__³¸åÙƒÁ¯éýÂ>øx(jEcÎiŒ9§ñË-8¸§%ùLªX…ã8ÔÔÔ0¹Øl6tttÈ’…@©sáèè(l6›ìº*• û÷ïè8ÀB[[[ÌÌÅÀò™$œ;wNÖ넽½½(//GYY™,â–XÇív¯™C_XX(Ûµ&''™M¶•A­ møâ˜s:*ÆT›Í†ÎÎN¡¨¨;w¬ÝJì7bûŠ677kÂŒµ¹2X×6Ÿ’±€/R‚x¾vüi¤©iüàt³Cù…L¬*¿>`Âéö0¢çfkÖwdpê• Å M*,F!¬nâIIò$]§ Ñ}ŸƒsY@ìY†Rô‡‡fEPLÁ–Ÿ±zM*qÏfÈ<Œ³P¨ÍDY g¼~üôC&¦¢?kȼ°ˆ®t Œ ¢$‡+‹e[G8NhµZÅ>ï'˜M¦†††,›Äú{¹ +928lEUU•äö”:¤¨’™L=¬V+t:]ÌÜ÷ZƒyZ-~t挬Æ{Ôßßþþ~¡¬¬Œ„ˆëæâÕ"‡pžƒeee!=]ÉÅÅE<|ø©,KÆ%ÑsXÎáëÅùùù(**¢µ*ƒƒƒ¢ËhóòB^/ŸÏ‡·.^ÄÜÜœ›õžçDOAÑ ‚ äåU_ð´¢j€ììl¸\®\^£N†AŸ‡§K²q¨$Ù©k~¨$ûvfã•‹ãá”´¿Ç¿€¿¾:$zˆü~?®¿û.þï?ú#ÌÎÎbffŠþNÇ¡¹¹™Ù5XtttàøñãaÙ€OII‘íe===ày^tÙöÛPºC|êU«Õ}¡Ö`€Ñh„Édb¾Foo/FGGqôèѨÛT³ÛÅ;¨)ù.TÌÍÍatt”©¬:%µ‡vQ#JD§Ó¡GÆÔç*•**²;X,ôööÊv(£+.ÆÉÆÆ˜º÷—/]b.{¬z7Ê s1ãõcÆÙ ò²ÂmÈàÔ˜ñŠoFœhhˆ©±Y§Ó¡¥¥g~ücIóï–ï7z{{ÑÛÛ·Î_gÍp$µKKK%;Ñz<æ€ŒŠ’|æeÑdgH â uà€RXZúÎ)<¾Íç¯À<ëœòb^XX08§<˜CV·`&AYe…ÛPQB€"…Ò®"y\$ý!| é÷¹B÷äþ™Á©)sÚ#¢‡ó.0#³þ [fgg£¼¼<îÖÔ«EÃ.—+ì‡Õ”——˲ïØÓÓüñìÓÁewpNypõý¾®BÅà°ƒÃö•õàê÷ˆÜlh§k¥ ´Z- öÏY‚ÛíÆÑ£G£:K2 wíÑÑѰ ’3'JWÃ/,2™z´¶µ¡¹©)¦ž½^o·´àÌ™3pNL„äo¼U*ÕÊþV<‰A€Ë劘Øð±÷K³;8æ,íb2.±2áŽnÑb oôöö";;{Eü@™ŸŒÝn_&ŠóBÍÕk×0>>®Äfð"=]AD+$x ‚žçÝ/èUZÝSRRžžÎœjp=Û³Ra¨ÈCýÓ;°§àÉî{ Òqöà•‹ã£aé,$zˆ¿—Ù?ûžûÿãããà8IIÊv ×ét’ûÝn7úúúPUUòºr'«;ÁÞ½{át:™诼wGt™ááá˜é ÍMMø‹W^‘äÂãp8ÐÚÚŠòòrTTTDÅá/kp# g``€¹ì ÇöSÊ4¾ËIyyyDû©ÝnGgg§¬©ãuÅÅhii I6ŒHqûömLLN2•ÍÉHCÝÑ Ën±Û29Ìxæàž“-Ã’XXi/_¹sÂÇ¡åå—qþ´µµ…ôoŃó×úÃ_–”ä¡DŽìlïìª$ \S¤ñÐúmŸ_€Ý5‹……å`ůN·S8ÝÌxýŠpm&ObpØŽúÏé×d† Â3O± ’QYº]–z,--áÓOé~„ )âÄUªõÁeŸ¥Ìik×ÔÍMMÐét¸páBHÿ–ÛíFgg':;;‘²²²°eÊ ÷»ºÛí^7ÈùÎ kj·ÛÍÜè{Ú¬'¿C)Yì°~ýpµ½¹Yܳ%ùq»~8ÑЀîînæ½s·ÛÖÖV”––bÿþýŠ}_í&nGù¥¥¥" 2õø N‡Ó§O‡ÔØ#°¦d6]-~ÈÏÏ)1âê¹ØívGÔÀc=ÙÙÙHII‘åZóóóp:LeÅd\’‚Ø>é1Úív¯¼äåå!??å±¶± +Cü>ÕÕÕ…îîn¥6mÏónz‚ˆVh7œ Bfxž¿]PPð-¯+­îóóóÌ |¨z>_¡…¡B|¸ôÔd¼þjü]ë]¼Ý)=…õßµÞÅžíA .ˆÐªÌ!½8V”—C§ÓÁétbûöíŠo»Zƒ&“‰ùeÝl6¯Â…tA™œŒääd&Âf×Û¹s'ÓfõŒ×ô45f}Áo^ù|>x½Þ˜¬Õjµ8qâ„äÃá@ša³ÙŒ¢¢"TTTDìà7àl-–X;üÃf‚/)›ê‡+‹‘›ÉN‰Œ‚F#K  ‹‹Ev'ªX;Àÿzë-æ²/;°vžLJDN&‡œL3^?&§½+œá‚Uð«Âp²±z½gÏž•$< –ÕÎ_fÍá—R‚.Ün7<ÏŠÀÁëõFU V(âàwpO!285M¤q«S]`ý¶{g| €—ðÀ6+ïÂÄ”cŽ©gkcÎiÜ꺇/NO%ŒH Ý'¯ðùSR<„Œ¾¾>æûü›ÏVýYÊœö8õuu¨ÔëCê.½~=ØsZ½¦ÎÉÉQŒÂãñ¬Éˆæñx¢* r³:Km_ÖÌ™)ª$<{°ô‰Ÿó ¸Õu/&Ö &¦<¸Õ}]#ÈÍÿÎèp:¡ôU‡V«E}}=®\¹"é:@íÒÒÒ¨v¨ ®=OÄåÈaà!eXm&p`O!>øØ"ú:±hê|fìqùÊÉ}%Øç4Ч€µÁÝ999Š@¬ïkÑ&nذÈ$v€‡bq‘m¾ 6ã’ä÷gÇ”bû¤Ãá€Ãá@ÿš>’““s"!1kɾ¾>æý²´´´ž!áêµkJmÞ3<Ïÿ‚v‚ˆfHð@xžÿQAAA€/(­î™™™˜˜˜uh¦Q'ã·ãkG‹‘ž*}jù“ú½Ø]¾’¥ùeÇ¿€o½Ùƒ×¿QM¢‡rÿÁƒ°ý­ëׯãþèàr¹••…ÔÔTÅ·ßÉÆFX‡‡aautt ¾¾>äoÇazzZ¶ë•••çy¦Ì^¿xÑ–Õj™ùúº:X­Vf¡ÌjVo6k4…-ªÝn‡ÍfƒÅba 4ÛE_Il&x`ÍîÁ©q`O!yÐjµÐææÊ¼Ž,>kÖV, †††B”«b³ÙÌœºxWQ.öï.زfpjøü\Ó^øüáqåËàÔ¨(ÉÇà°]tÙX=€šêj”œ>³çÎ…Ô o£¾éñxVyT*ÕJúsFF1°À¡o ŽJ ÂÚª­¥¬«;;;%ÌÅ”½0éëëc*—š¢ZÉoŒ9¦pß6ÓÐCŒ;§åä(–¡ñI8§r—>{îs0k¬­©Ïæúµu´.‹ÅårI<Øl6I"â`²Üè0Ë–ý©P›‰Â¼,ä<2Ù]” ðù0æœZY¿¸¦½+ÙœBÉŒ×Ï´>buïŽ6N44 §»›ù¼eÍZlC}$Sn·óóókÞ¹ççç£~|º' uXm&PQ’‡®«hS, }¥¦º:l"Ä냻³³³‘½2æäH=·óóókæähËL,^¯W–ëÌÎÎb’1«q°—äÀÉ0§×ÕÕ¡§»;¬Ï¿˜>@£Ñ¬ô“H÷‘P¯…çÆRçšššÖõ­‹™â¢€xžÿ&í íà "t4¥¤J'%%!+++¨¹…«ùòÓ;PŠWÞºŸÝ5žD‘åþƒL/ LNuSÓÓ¸þî»øí¯} v»=&ž9ŽCss3¾ÿÚkLnÁ‚  ££Çi=Õj5eqæ PZZÊ,¸´$þÙ0™b*#€T¡Ì†ã©Ç³Ù¼’FuõFój§`Ãë§ÅrAÆs†‡eÞ`;ü”jU5¢ŒT×Ô ­­MÒ5òòòÂâ 'FGGWÜäCE¬Šàì¹sÌe¿z,¸ ijÒò²°°¸„Éi/f<¡ßP¯Ðå1 bý@X«Õ¢åå—ÑÚֆ˗/‡%ÛÃFývýá°|ÆqÜšƒ¯õnybçñõsyàwrÍçцÝngt3›ÍÌb±Ã•ÅAf±…”LJµU»V‚êb×´}xÜuâþèææ…¸zN†Æ&Ið&X¯UÉI¢\ÿC¹÷¡Dö…¤ø° ÍRTI¨=´KüšZBe­Á³{ÇáÔK/Ád2áìÙ³ 4ÛlMB¤¤¤¬5³¬©’ýÿÀú:V×ÔR X2ÁË"âÕ®îëILL„&-}÷Ç1j—Öî‡+‹±o×ìÞ™‹4õæÎËÜuâ¾m÷Gx`‹ž K¹‚c£)ç-›õçÕõÀò¾ÝVãD0¬÷¶Û?ÛƒQªà)ÆõfjU2Ê sÉÔc"ÄÖ¶¶°d{جæÂ€"ÐÇVϹ냼ƒÝ»Y½§µ¾ßúœRE [±zl‘ÂÇ™×ÎÁd\’¿° z­––†“Ëg­V+L&ÛÛe=s•s<õx<°Ùlõ‘@¿ÌA‘ïŠYúžœgÆ«©¯« Ù÷¸zí³ùU„™ð"íü¡褌 "Dð<ï.((xÀe¥Õ]­Vƒã¸-70ÿðXYH„«9T’ƒ×¿Qo½Ù#‹èáìAAv*=œab~~wîÜa*«/ÉÃÀ©ìÝ»w1h6£¢¼^¯7&'u:Nž<‰sŒ“‡}}}Ø¿Èꘀ””Y vìØȶñ¶V«5¦úÇqhiiÁúó?Y e4§åMKKCMuuÜŽ¿ëÏ`±XØú¡6º<šÔd¦Ö`,xp¹\0+rrºöÈé– ÕÕÕhnjŠI±CGGÇši1|î@) óÄi§““‘Ÿ“Žm™Ü3>Ìxý²ŠWS˜—…Bm&“e¬Ë'µÎ_¸ KÖ%9Pºl´´! ‚ 0Pj3QQ’Og‚°æ Z 9iLA·J¢ï>6'úîÃ5㣆9RHOï- ‰h2ÑûKûçRÏ[‚Ɖ­‘²ß(eØÌLàpe1™zlÇq+ÙÎ_¸ÖŒ¦Áô1ÚãbCŽs×ÉÉIÌÎÎ2öE]ØŒ=&ÜâÇ‹’’’5s†N§C}]œN'º{z¢Vü°¾lÖ?‚¡€bµ@€¬ÂˆÍ ð?÷z½!É&¾ÕûE¨ 3ûûû£æ€Wyž¿M£#AJ€A!„çù+gœRZÝ3220??……µBƒÝÛÓñÿ¾ðTز%ì)HÇëߨÆ®~‚ûg™¯ãñ/à•‹ãõoT‡T¤A|†ipùõÙƒeHLLDÿž©ü»ï¾‹‚íÛ1993Á“µzzz˜ûûû±sçΦSæ8NVÁÃz£P200s}ã8|»¥EV·*¥êt¤ŠxÑKN^™ÃïÞ½ûØ|,Ïlá|G°£Óé ÍÍ•äR¹°°°&ëBÀe2??ÿ‰r9GºÝî•ÿ†“ºº:œllŒÙ{ýÖÅ‹l}8)Q’prR"´ÙlËä05ëƒ{v.$‡ÕŸÚ.> 7ÖiWÏÅÍMM¨5pùÊ•¨9&¤­OYèééav¿€þGYúîóq—ň,RHOr— bæ>ÜSÈ$x0™L0ÅXÆÒÍÖÔ'VÄĬ{£DôÀš)@„•̳bÙU”‹/Õìyâç\Ó^&1åáÊb¼pl¿ìk°4µ Ǫv£ªb'Fº046‰ïcbÊþûæñÄÔsXk0ÀétF˜*^߯ŞW²P³°•™@§FEI>“èáì¹s1/@–÷³[^~íF#®\¾‘ìK„¼ÌÎÎ"=-ÞcqqccclkgM*®Ë´JXÌz6[_kµZ'Ž IDATÔ×Õ­ˆÚFÛÛ×XCÙÙÙOÌT¤Ò‰††\×år1ŸEïð<ÿ# ‚P ñIz^ðEO+­âÙÙÙ˜˜˜À§оv¤R¿7ìõˆ¾õf$ÑÃý‡³+™ˆÐ2ëñ0gwHNJ„¾t;JvlÃëÞƒkFüˆßïÇõwßÅÉÆF‚ :Up´ÒÜÔ„W^y…y㤣£ÇY{$''#%%E6‘«#}BBRTIðÏàíóù`µZCæê)t:¾ÝÒ‚3gÎÄÕt¨6¬Â‹#Ôzw¼””,,,`vv<Ï&Ó—ä‹v—'‚§áÄ Yä.“JqµJKKCsssLgcq:˜`{¿X³G–@‰ÄÄädrÈJOÃÔ¬Ó^?eûŽ…yYÈàÔ˜ñúE—GÚ•ñT¯GËË/Ãd2áüùóQïFl‹àÁívchhˆéïÕ英s{NÈ2µщÇãaºÝU”‹ý» b¢"‡¾ûãÌfñ@¡6“!Ä J4ô bÿ>D½’‚,ÏžÅøÃ¸¸GZ­§^z &“‰ÄÄ1°ÖaÙ³—""þê±à2ON‹;‹HMQá«_Ør‘r§Fñöœ•¬0Î)î܇elóÂbXî[,¾Çžhh€ÓéT²³¢q¹\¢}}}ÌãÀ³Oo½.cÍò/ĵʱGkkkÜ™mÅ>ŸYðàp8°¸È6ÿ4ÖU#111l{]N† ÁœkµZœhhÀ‰†˜L¦eñC Ï'á6Ê uuu!«òƲšB†‘)/ÒˆH„’H¤& ‚-<Ï»•ºHLJJBVÖr ãy¡2"b‡é©ÉxýÕØ½]Zf‰ûgñƒ«ô`†˜îîn沿÷\€e÷¢ß{îóu¬V+ÚFLNNÆL»r‡S§ØƸÝnôõõ…´ŽF–ëŒ3ožäf`ÏNñã1zPªÓépúôièŠãø®®Nñ)¤å¨¿F£ARRîÞ½ËT>UŒçk÷!1‘^CE­Á€ÌŒŒ¸üîºâbüÕéÓ1-v–úYHQ%£þ7äÝxJ r¿-ÉÉI²]›5°#pOèõzœ>}/½ôRÜ„Gó8TWWmn.ÓšZ ¬.Ä©)*ÔÿF r3è†Å½½½Ìeƒ æ‹fúîóxëF/¾ÿ/?Ã[7zIì°…ÚL(‡©.Âe…Û¨ãà>ËÑY×ÔΉ ´¶µÅÝšºåå—ÑòòË´¦Ž0ÕÕÕhlldÚs d³g®,ºŸŠéÏ©)*üǯ=¶ŒlœYéim–_ªÙƒ“_®ÁáÊbdpê°ÔÁjµÆÜsÜÜÔ„ºº:êÐIKKcšÅ`·Û™ÇŠ’|h³4Oìc›e€xgÏž»{~¢¡ÿã‡?DCCÓý'äë{ÕÕÕ¨fØgg5¯›ŸŸÇÇ™Êî*ÊÅžbíÊ^—sʃ1ÇÔ–ÿ¤ÂbÔ“'òŒN¯×£¹© ÿ?ÿ'šššâ滃\$$$`nnŽY8tìÐnlß–¥¥O1朕1…ž?hlÄ?þã?ÆÕwnxäzëX­VæñöwþÐ&†ËàÔ+Y&§½’3>T”ä£k`„éðèüù󨩮ÇqqÕjª«QS]Mî´a^#è++Q]]J½~å™óz½¢ûªÇã Ú…Òf³1gÞ©­Ú…œÌåzæoË€}r†nd`·Ûa³ÙØž™C»üîšöâ×#èúÄ × 9„Cn–õŸ£@ßPJa"zèííe¾Ï_:,A”,—/_F­Áwkê€ðÁjµ¢µ­ÚÀ®¸úÊJÔTW¯›8NÑîÿn·[Ôþ´ñ "¡ijvåâíÉÙ_|þ™°¯½¶erðÌͯ¼Ç«UÉ8\Y¼âLÿñ½qLLyBö÷NgÌeH€“Ðét²fb—÷lCm-j 8N¼ö_ÿ«èw±ë2RTIxö`iPŸeÍòàœ˜Àå+Wâîl‚㸇ûv£W._Ž«Lã‘\éõzÔTW¯ŒÉ&“Iô\ÉêF?66Æ\÷ßdzØ50‚Ö_™‚:wJQ%áàžBf!˼È:×q·’Åét¢»§ÆövÊöaÒÒÒpêÔ©¼/õ÷÷+ù=ä Ïó¿ '„ ¥A‚‚ ˆ0Áóü ¾à«J«û¯‡½ž˜CInjÄë"—èá÷-Ø]CE=œ2ÓŘÝA­JÆ×óðc?áØ~ÜubÜ9ÍtÝ+ï¼½^\÷Öh%“58Îh4â+_ùŠè´áÁÂq¦§§™ËŒŒ0o´}ÅðÔÊÁ”Xb;û Çqhnj‚^¯Çùóçc.Õp(7¬” «;pNá]–]á µY$zÏ~îsxûí·át:cþ»êõú•äxàü… Lå28õñg(‘Søð¥š=¸ÚÞ/ºœÏçÃå+Wp²±1.Ç€@–ÓéDk[ÚÛÛcn~Žƒ^¯G¥^¿iö$–¬J.— EEÁõSö¹8 µæâ@õùÌxæèæÆ8LåRSTxîh…⾯kÚ‹¶ŽAt D÷án–jURTÉÐfkÖü,€Ô€Ç¯M{´²þçÀ²ØÕ}–).Âöìx¢‹0=÷™Uhv`ÏY]ÕYƒ,}>Ξ;‡S/½—÷P§Ó¡¹©i%ØÒØÞNÁ–!XSo%TgyÏ“áAªˆ8M-n¼îhþáÒ[~æ¹£ؽ3üfž §ûñàÍÀú`Ì1…®‘•µ„œX­Ö˜ÍÖYk0 D§Ã™3gh y¸P]SƒZƒAòŸ×ë ú³‹…y8¸§jUpaQRˆ­­­¨5Ÿ}ZJª5`2™ÐÚÖÆ,T#' p¨|ôßÍ>#–ÙÙY¦2SSlYj-{¼u£WÔÀ¼°¸b¶ó¥š=¢þ&K†¹2ˆiµZÔ×Õ¡¾®ŽÄ$-- ßni ÉØìr¹ðÖÅ‹Jmšxžÿ&=!A(<A„— PœåÞoµâ¿½\JRÄë"—èáï  à”fSNî?xÀì&^ÿ›>üþsUøûK¿Äœ_|°­Ýnǥ˗ÑÜÔSmÝÜÔ„¿x妠8AÐÑуÁ’º¥¦¦bnnŽ)êÜÜs†]A6j«vXÎ’ªNõÌø|>t÷ô„åÐÆjµ®ÙÐ×éta Ô¯5P©×ãü… 1³éØ°Š—@ê`èëë<ÀW¿°oÍxL¢‡Ðrú{ßßþÙŸaqq1&¿_ZZNž<‰ÚÍ9ÑHwO³(QŒó¤\È!|(ÌËB¡6“) ¢­­í1‡ÐxC«Õâdc#N44,~”õAl)pXO¥^±yÓ‚u¡”2×ý†þ±w£üœt ÑC c6›%<3¢ƒù"‰Ï/àFÇ s6ÇP›¥A§†6[³27æfk‚Ž’k>^=·‘§b!E•Äì>J„Vqb(îs§ÆáÊb&XOOOØö´¢yMp™¬©)ØRüû{å*C°kê<†21k)"â:AèîZüÞsUxç½>ÌÍ?žý¥öÐ.¦ëÊ…&M½¡àaõâ…¼,8§<¸soœ)p{3bý=U§ÓáôéÓ¸|å ÚÚÚhP´Kq1ªkjÖ¸Éo4§¥¥‰:¯ výýýLuÌ­b*@lyùå¸~^ÁùN§“„ˆÛp+Æý07Wt[ÏÏÏ#%%%èϳfw˜4Üu2 ÛQ¨Í%þgÉHŠÀøÄ=Έàç¯S§N…Lˆö“7Þ`6pŒ0SXŽ[#‚P$$x ‚#<Ï» ÜRZݳþþ6üy]t“Ê!zðøðƒ«Ÿà™Ý¹ôpÊÀüü<º³;ädp+êQ˜—…º#LÎÁÀrFƒêêê˜:ôÓjµhnnÆüc¦ò6› 6›-hwZ±h4&ÁƒÅbÁÂ[@õÿµ.CÈî"-úð¢®Ñ¢Ãá`Ü;ÒÒÒPSS–gU«ÕâÔK/Ád2áìÙ³ŠÞp&±ÃコÙÌTvWQ.öíÚñØÏIô:8ŽÃ«ù—øÞéÓ!f¾WZZêëëQ_WW™W¼^/.œ?ÏT¶P›¶ì´tM{ážÃÒÒ’¨ò‡+‹™×jgÏžÅéÓ§ã>K¥=±–a8l´&bé硞‹7 È ÑCl¯ßXƒ«×g‰vÆSøÉõN¸f"—ѦP›‰Ül ´Yš•ÿÄú½ VáÕº° eé÷™Õ¸%T÷ùÀžøøÞæñ"ä³gÏ¢ò‡?¤Ì—jí­y½Þ•`KZSoüÞ^¹jMͺ·Æ"^vŒ•""~áØæ¶9\YŒ}» Ð50‚û£NøüvïÔâ™ÊbädF¶%'%÷®“¥Á—jö¬©,cLcËj†‡‡ãâ½üdc#jª«qùÊ•¸ F­~dL!FüTRR"º½ìv;òó·^dž=X&ºL§Æ=;pçÞ¸è²ìõuuq?Ǭ"šL&´èîî¦Ì¦[ÌÅ%:$C]IIH“““Ì÷/`Ò Õôàã{ã¢Î)ñcG¨ÏW‹¼^ïŠyõy©««Ã‰††½]½v ãããJmžoò<›ž‚ ” íºA„žçQPPpÀ)¥Õ½khÿûÎ~ó@tÒS“qúw¢ùŸ:áñ³_Þ8‹ù…%z0eÀ48Ȩù{Ïzâgj«v£ï6¶ÀìX<ô«©®†Á`€Ñhd*ßÑÑúúzh4òw¨T*¨ÕjøýÁ»g¸Ýnð<Ïô÷jíBNÆÚ{»»(W´à¡»»[Öl N§—¯\ êù|>FFhssÑxòdÈ…z½?üá—è&m0p²±‘ó×ÑÓÓÃ<u wy=„N‡ï¼ò ¾ÿÚkŠßÔŽW¡C€Ö¶6fًωŠï“É!+= S³>Q‡¼,”îØ†¡ñIñóåÄ._¹‚“4 <‚œ¿ÖŽ+%%%Ð? ªÔëe_X](A€J¥ É\ü$·X=Ä&}}}Þ§«ó=»FðÖÞ°þÍ N¼,äfiP˜—Iâ≂Àì&žÁ©qpÏjDºĮ̈UÉ8\©Ã[D— 8KŸzé%ºÁà8î±5µÉdŠÛÌõt`M-§Û­®¸X´¨Äív#;;{Ë~*ED¼w´÷µ µ‡v)JXºÙ˜õ¥š=xö`)îÜgUƧÓ2§ähë/-/¿ “ɇÀø %§N§ÝNO2x<æì…ÚL”nc*ÈòÀÒW._¾,J(/ó^¯GsSÓgsqww\f~ÐC÷h«äÑœ,Ûµu:ÑkœÙÙY¤§§?ñs‹‹‹ÌÙ µ™+séýQi÷|B¤€aÂ-^ðPFsµÕæ7ÍMM°Z­0™âz½*Çxs²±1¤Â•þþ~æØŒ(àžçBO AJ†A€çùo|ÀÓJ«û›òxªPƒ’ÜÔ¨¨OAvêJ¦VÑÃÈ„—J‰Ìz<¸sçSÙ}» °{gp/>ßÿÉ Ì1ÜëX=ô;ÙØÓÀÓÆ  èèèÀñãÇCR·ôôt‚t äÝ»w™þNª:Ïm¶ÏÑNÓ>ŸÝ2eyhmkÃåË—™‚—øñŒêêj475…LN·^þüÓeL‚hkk[q$Öõ¯uÎ_½X×€ÑL@ÜÄ’ûðw#X\(].צ.”n·[Ò\Ì»‰b ·ÛÍÌ'æ}:Ò„Kì8j3Q˜—… NM!ŠA †_ªÙC H÷Y2÷ìÀ{c˜ñúE—íéé‘m_+–×ÔV.M1™ýAÿHÐXS‡zM›—'º=Ï–‚‡Pz(¿À¶·,¨*^ äîagL&Ô q3v„N§­mmhooW¼Y‰®¸Ú¼¼ñ“\ãK€ÿ“¬âDøÒá½Ìeå ¶¼ü2M¾ÈÂt²±qq`` æÜíâ†pÍÅ,ûgssÁí+9,.²‰å^X5'Ï͇7Ãõ˜sšiÜØ3óh4°^5™L¶Zc¶È‰áÑ9w¨ïŸËåÂ[/*µ™†¼HO AJ‡A‘£ÀmYJ«ø÷® áo÷‚KIŠŠúì)HÇëߨÆŸí¤§*B|øá‡Ìe¿ú…àÒÔ*üþsUøÉõ_3ý­žžž˜K'ËqN:…W¾ó¦ò‡f³ååå²×-)) Çavvö‰ŸêsQwd9êzr28ìÐfb\ä†Văa¯×‹ó.ÈâîÐÓÓƒW^y§N y°ð¸ÓNOOOÔˆ´¹¹¨®©A}]¹#m€ °Ûí¸}›- iª:yÍÆóVDü+hAäs®Õ*Î=®ººÕÕÕquؽgÏc.û$G÷H>dhR19í}bpu§ÆáÊbt ° ÅbF®P¬ý‡ÃuÇ€Éë£Ã¯ááaÅ~‚°´ZíŠÃl$æxJ·Û½©àÕ-5E%j, ÑCì ŵO)Á|cŽ©ŠÊuyÐä ¤ ™šTøü=XR]„Ÿ$ '”ŸKwl Ë}þRÍÑ&«×Ô%§OÓÞɈ•5õêÀeN‡¼G"‡H¬©Å®i\.ŠŠŠ6]o‡ÒÐCɸ¦}pNy0¿ÊˆdÆëZ¼P¨Í\ÉúÐk¶aä¡[Ôß7Å™à!€V«ÅÉÆÆe*“i9H;ÊSºâbpÍÊ»wžVÒ@PWt»Ý¾åïl6S]ìÙ!Yø|pÏ ÛE;ÊúÉå+Wp¢¡&Ü'ô«ÕbÄ€»½Õj…uxX1‚Äõæ¡îk›‘ǰö›ŸŸê3>dªÓz“†]E¹x`c7oÉ‘±ÑÉÐwµ¹¹QõlÎl}Äét~ÖG­[ã•´´4TVV¢úÑš>ç >Ÿ?yã …BQHÏónA(<ADžç‡ ¾ à_”Vwïü"þ{ë¾ó•Ò¨©Óž‚tü—*ñ×Wèá 3>ÜrSr+êŽV 'CÜ è¾];$Ò]¾|•ÒóÆ : Ì®Ò}}}(**‚F£‘½nÇAøý›¸,,,àÞ½{L×ÏÉàP[µ{Óß?SY,ú€¸»»'™6G¼^/^{í5Y7bøþk¯áÛ--a}n¿4ª§Ó–M´@í€ÓÔoÛíÆ­[·˜Ë;´{CáÐf$&& 0/cŽi=„èù¸Çµ0¶·G•‹»®¸†ÚZJϾŠÖ¶6æ±ñ¹£Q•Ýa#’“‘Ÿ“Žm™ì“3[’xt ÌâéóùðÚk¯áôéÓôP‰Xk­Ö–¿`ÿ‰ ­À/ÇqÐétà8%:]Ä„ [­¥Å²™ ¥ÍfƒÃá`ªGmÕ.Ñc‰””gF óG€Ÿ\—× "5E…ý» °o×ìÞ™»á:vaq ÂÂ"æüüÂ"|~֭ĉ”‹0¡œûüù§ËÂRǼ,T”äcpXüž«ÏçÙ3ghM‚5µÓáˆÈ»y h2ðßJ½~e-Têõ»3½Õ™‚ñ Ïî0昂Ï/àþ£€Ðû£ÎG?Ÿ»3öFtww£¹©)®Ç ýª¬^¯wM¶ÓáKvà};ðNËqÜŠ¨!’fbq»Ý²Ï×)ª$®,–å;}þ`)³ñÊ•+²fЈtdù4™Lp<š‡M&¼OD„ÚÜ\hóòVú—N§ƒ†ã¢êþ²ôÁ`ö G$´÷z“†gžÒI<”n ú³3ñ{ÓºGãj´¢ÕjZ­V8œÎD¤Ö«á˜{Ù½l#®^»†ññq¥6áwyž¿ ‚ ˆ€A„çùŸ|À*­îãü{·¿]“5uúòÓ;pŸŸÅÛ#ôp…‘õ+¦r©êdíb*û±ý¸?:׌WtYŸÏ‡³gÏÆÜ¡ß‰†ôtw3mô ‚€ŽŽ?~<$uËÌÌ„ËåÂÂÂÆé¶GFF6ýÝ“ø½çmùûý{vˆÞöù|èîéarª:{î\H6[}>¾ÿÚkør½l4¯n“Àao`³yõÏ‚aõfTà0fýωð“Áá9wyµ*¹sLQ#†­V‹ 8ÑЫպœy…q¬—Âj· =ŽÓéÄåË—ÙÖC)*Ô2®‡"ArR" ó²àó ˜˜òÀ?¿°áØ Å‘Ö:2‚ó.àdc#=\Æí&nr«çêU"@†˜µÁê5Éê¹|ýï¢<—˵áÏY2r2Ò˜3½èA¹‚ é™QÊüqÔ ×ŒtÁÕj‘ÃþÝAÍYÉI‰kÄß<\3Þ ç/‚ˆ´‹0ý÷ùpeqXïó³Ka›À¼°È´¦>{î\Ü%Gûšz}Pr@ ¼Ñï”Ð^bñz7ÞÛ—*"cèIÆS˜œöaÌ9µ¼^šöʲf 5>ŸV«5¦ ¥¤À= x^?V¬"öÌ×_{uÖ„h:mTß´´4Qf ‚ @¨Tkû­ÅbÙR ±ÏsWWº»»•:”¾Çóü«4£+à "ò|À!O+­âow;ðÔ ž*ÔDMþ¤~/ø)~9è¤'K&7ýÉdÚÔÝôI|õØæÃ‡4µ />ÿ ^ÿÿÞc*o‰Ét²§NÂ_¼ò “{¯Ãá€ÙlFyy¹ìõJHH@NNΆ¢‡¹¹9æô今rפB݈œ ;´™wN‹ºö•Ë—E Î_¸Àì> Ñæz½rðKäâùêö1—MS« ÍÖÀéöPC†˜ÀÆõ‰††•ê€kÜÎí«3¬”là¬E¬åÌ™3ÌíÿÕ/ìWL0Æú¾¿3?3^?œnÏcŽÙ…yY(ݱ Cã“L×okk{LhGÈ;ú:ÁÖ`õõõ1¿½p쀤ï@¢e288ÈüÌÔý†^1óÇ}›4gÁ}» p¸R”ÈáIhÒR¦VÁîš…Ç秇XC4¸¡§³³“ù>س#¬u]ïEë¯Ø2ÉFèõzZSÓš:lm"6Ðy³uP$DÄ¡ÆçptbEÜðÀ¦lçåv£‘L ž·Êù=žF‰’’ÑÁÖ.— ùùŸ™ëI†¡j&§½²}§Ã•Å›dÎdzæÌ´´´(ÊBq}Œæâ5ó±Xæçç7<°fwØÊäçÅçàFÇ Úo?úz%ùøÿÙ{÷°&ï<ïÿMÈ$$!$@ '- ˆ‚´X+f Ng»kgíØéUçùÕ}Úgmg¦×®ó{ÚiŸ>WíîÎ<­îoŸvvGÛÕ;ãÈØ9g·VÐ)ˆ',Á J"p á ! ¿?0”܇$wòy]—òýÞßûþïï÷óþ|Ö- ª luDQ[ œéÌÕ?‚÷ ‘}š€(À|£F„ 3™^1 Šºººpô£ÄÚ\¢Ë † ˆ˜‡Aa†a˜A“É´À1–ÿGþyû¨äñS¦¿Û¼/½oEÛõj`<0—àall ç/\`•磖óa°9E‡Ê²YY<¯ïÝ_´—ig_l6Û¼ÛG“Õ ‹Å"x“×k‚orÒ X–ÃÍhC—¨„Ï?AF–!D¥RaUIÉ^{ŠÓ=LÎuÈ8Ý3N`“Yl^Ù##µµ¬½"å¤Do§Q) NÃ5â½ã0ycébø}+´pàÀÜ¡[G A‡{w»ÝSëf¿ßK—.± ø0äNÕ'b||^ŸŸ*T¸ÝnNm&Ú«õ%Ê‹sQZÉ»°C"‰Cª>ÿ8ÆÇ'¨1¸y~¨ho^„ a¹ték¡Y¸êy‘9™“¸¦¦†ÖÔDÈ`cèÜÓÓs›¡s8EÄ|8´w:Ñv͉® ñD:Ö¦&ÚŸ&f%+++èq`ppðŽqÀïg÷^»­¢z­ Cok}¾"™RÔ%"°<ŠëÁ² IDATx½^ètº;>ïííÅØØ«rÜÍÉR!Ãæu…(_™ƒÎÞ!t9]wéñÐ%*YEYs²ˆðKÑX¦¿Ìü££¢=ÿÜÁ0Ì Õ$AÑíÂAD Ü5™L/x[le÷ŒMà‡ÇxõÏFL™¤S¢·oœ˜@\¸põ¦ãcë y)CEY.´uí½?@uu5Þxã¨2îÜTY «Õô¦20i¨ÕÐЀ¯|å+‚”-..ÉÉÉp»Ýp»ÝdÊ*Ëò ×̯ÞJ—f±ÚŒ>R[‹]ÿ÷÷=TWW‡¬Ž- V•”ך(&ØË\˜¯pè^ujŒùÇá£y7œW«Ðb³ÙP[[Ë:=_ë¡p#‘ÄA¯UA£N€sÐ=å1›«GZ¯×‹7wïÆÿ~ã˜:l"ÂCVv6'ÁƒÕj û»˜ Zt9]4‹.F‚Ý­žÍ{bbׯ_g¹0?§‡z­ z­êžó·×çÓ7|G„á»1ìñ½vV*•´MÜÁÑ>Bww·X‹¿‡a˜ZªE‚ ¢ =‚ ˆÈ€a˜wüJŒeoévãýÓ̼¾;2:޳÷üa¹©¤›ñW›–PÃ⸸¸;ëÑ톭•]d…雎|ðí? vNg_ªkj¢®Îv>ûìœáGïEoo/ë åù¢V«‘’’‚öövVéR¬#êl(2V36›m^‡tGjkƒ ×ΠÁ)J µ€æOŸ;xÉG"‰ƒÉ 3*ADc_}gÏÖé+Êò`NÑEÕ3‘ÆK`2h`NÑA!Ÿ\›-2's2óz½Ø³gÏa´ ‚oØêôôô˜4Òºzõ*«ë–dò:H$q0¿ìƒDdÒÓÓ1m&æš ¿w”³Q‹ïûáE¯Ð¨Jã©AœH-¤õ¼®87¬e‰¹¬©«««iMMWCçH5¶´18tü ÞüÉq¼ýÓOp¼¡5êÅêêë©ÑwÍâÝz```êÿ ¬¯=}P*dÐ%*y½·Ò‚LV^æÔÖÖR¿!"r.ž-ŠÃ0˜˜`'¶ÝÌóœ¬TÈmÒC£N˜wç`ðѲ³³©w¬ušššÄZüs üHµHD4B§_A‘ÅgˆîêwÍ}XjVC7éI²ÁÈè8._ÆÈè8˜ÁQ\w/bP+¤XlJ„)I “.+&Á¤S”tï—Ú¯­X€6f‡?sPËâÀlÆ«üãYå• â±õüntè5*<¶n9gçÏjµ¢ÉjªpF£O=õjXŠ9š››‘žž>å±V¾øâ ¸\ì<ì<¶nùœ¡P碴 -Á÷Šòàt:a±XB^Çv‡uõõ(_»–©(£º¦vGèæ­ã ­ò`[÷HÒx ŒIjò”KÄ»wïf-vÓk”¨,Ë‹Úg£TÈ‘š׈ýC^¬)Zˆ®^†=>ÖsÞîÝ»±k×®¨ŠÊEDlã,«ÕÊîÝH.ãýxòýmRô@‘"¶žä…j3¡`[E1öÿæ³»ÞÛs[ ú=+Z•ýCdËpñ"¼pArÔ X©žïdYŽ kŠbØãƒk͹5 $níè¡55±p1tŽ$ñ|hlqàWŸ4ct̳õ]WW‡§¶o§†Op~·0) ïííeuÝŠ²<èµ·ÏoÉZÜ£cŸàåÞ2)6=˜_üç9ÖyÎìè\‡ 6‚‡™{Ücccp:¬çd>HâªOD²V…þ!†Ýw·9és/x ¨ÙÄtºººðÑG‰µø.Rˆ ˆ¨…ÜoAD à bRô J~x¬/8ï¼ÿbùûO^Á©V'Îu ²;€Û7Žsƒ8v®ûO^ÁwÞ?ƒíÿ|þ'ñʇç±ÿ“+8Û10gúgÖ/BnZ"5.q8SÞLƒeÝÊ\è5üª•dbYûÕÕÕÕ¬7o"•òµkQÂRÄá÷ûYáÌ7ÿææfViÌ3êLr3Œ¬ÚÞ½¢<© _$ÈÚ#Gh@Š2êêëYMr¡±ÅÁZ46JµRA•ID5\…I|ŒÄ€.Q‰l“ƤDlzÛ‘ÝáÀž½{©ñ‚‘ÂÒmgg'kƒŒòâÁŒ»)ÒCäråÊ•)ƒžHj3BS˜kÂ3öôš;½¬æ¤ðíëÃroºD%E(‹q¸x~hÅ"z€Q^Ï rÙ”·hJŒÔ$˜StAy—å“5E aбwN=P¤B(Ø ÖE‘&"¾–†V:~&¦ÅÀ¤,y«'f¢R©‚Ž>ˆìòÙgŸ±Êg‰0æ£N5EÜÖ€€Ýn§ÆBDÔ|<]ôà`¹ï — îäG/Aª>‹Ì¤&k Q'̺÷ÕÕ¼Ã=6‚-"z×8ûß_Ì·°ƒa˜«T“AD+´›Oa0 sÀëb,{\\’’’'øµÜ¾qœjuN‰ ¾òÆá•ÏãpƒÌà—âŠÄ)þnóR¨dèÁzó@zû³kbyø ×¨°v–MG¾ØVQŒ–õìõzQÍ2B$³óÙgƒÞ\ÐÙÙ‰ÎÎNAÊÕÜÜ–ðä•«Ùm´8p`ÖÏ=ê9ê¬Ð/ÅÖÌG –²9ûúèP)ŠðxcÙ¥R)4MX®}ªÕ‰±|íÿ|ßÜ{ÿrì \fF°Ø”ˆëÉ뜿põ¦cåê¶ž{xòû—f²2è>pàV•”@¥úR˜`9vŒUžYô¾¶`ýÔï‹5 ñNÉ«ØùÙß—ÍfƒÓ餸( ¾®."Êq¼¡f£…¹&Nù¨•r¨• ¸½>ª\"*°Ûí¨®®f>A.‹*cU¶<ñ•èA·“ýš¼¾¾N§/üíßÞ6/WŒ))°;‚_''ÝÀyž †€è¡Ëé‚olœ*:Lp~ç¤b*:P8 >{\ºt‰µhíáò V*0>>A2Âinnæà¸%ÞŽ[4*4*¼>?†=> »G½¯¼ìT8]n\¸ÜÍ:›Í†Ý»wc×®]´¦&x%???è3„ÑQv}&T"âétöºÂöl :5²xÈeÒÛ<Ö>§Ëƒ1ÿäºÇçGß £cãè ^uÌbA~~>5~âËwkg,ÇùujŒùÇy[ï¢.=ÙŒ1?»²D/¾ðõ!‚W òó¬{¿ÑÑQ0 3éAȵ³Ð´uö†Md"úhkoÇñ?üA¬ÅwØÁ0Ì Õ$AÑEx ‚ˆPn-F«ÄZ~F3e(n⇖Î!Hââ¨q± ^*E“ÕÊÚe(=-m^WˆF-ëô{öì‰:g;Ÿ}J¥’UÚ«W¯¢§§‡—rôôô°O^Y–Ç*:ÃLÖ®ÌA‚"ø±ÉëõÞæQÚn·ÃÙü¦Ynb6väBðŠßïGss3«´f£W-F¶I“A ©4žhÖâÁ"—Ëá÷û9 ÍØ8nQ*dHÕ'b‘Ù€d­JÐöñPÑ",\ÌíÙ:´¦&x'TNYB-"f£ËrL¨(ËÃ3öž{| þ×»ÿêQ<¿u ¾ñðJ|íÁ|”dNý,2'Ü¢ ê§hñ‚©ô-Âæu…ø‹‡WB£R]f«ÕJQˆÛÈÑ»uiAæ¼…áB¬÷:56®ZÂ)¯×‹Ýo½Å)š8AÌ„˜ÕçóÁét†tí,],ĉ$:"°ÿ~1ß‹ Ü¥š$" ë‚ ˆ†a˜^cÙãââ””„¸ܸy“ †\.Öáû‚Ùtä‹'+ŠYµ€³¯ŒªúS©Tزe ëôŸ}ök±ËÌ|ؠר°–§P¨J… •°àX­V4ÝŠ”Ávú¯ï{fο=‘õÔÒà7"­MM4H‰6X<“³?Xþ"~°üE<–QLù<£c~üìøÎùHã%HJL  &DÓéä,v·æu­ Ïo]ÃYô0Ðb{G3 …à¡´ 3lâ'2謅ßeyÐkÉóv¨úHFj’ƒxÞ]½®©ŸÖŽ4¶8pþr÷mŸûü5"Òhnnf½Ç1Ý‹°Z)G¶IT›!‚‡1~RR¬atÜ"‘ÄA¯U!Û¤Gj²F0o·KàSÓššˆ(RB$x‡ˆÒy\Ëç¤P¾2Û*ŠñÒ7×ãŸþv3^Ú¾;}•ey(Ì5!7Ã¥Bi¼ºD%2R“‘–Z˜ý¶¼ìTVéŽÔÖRã'Bún —=_K$qHÑ'òê g‘9W-æœOMM ‰ˆ°öÁÑÑQÖ‘VBéôp>°‰Æ*'(Dä²oÿ~ÖQÇ"€ý Ãì£Z$"V .‚ ˆ‡a˜×L&ÓëÅVöøøxèt: Rä4±óÙŸþÄ*Z)Ç_ÿE9â%qèpcâÆ”×œ¢Cåy8Zw‘Uúúúz”””`UIIÔÔá¦ÊJÖÞ–Ün7Z[[QXÈþ éÒ¥Kp»Ý¬ÒV®Îãõp¸¼8ug¯``8øCóêêjüèG¬D+ôK±R¿tο'JÕx"óëØåAåëìëƒÝn§ 9ÓÄR´òñW† i«oûlplï´þ;Þ±ý\þaÖejïìCÝÙv”séµ* y|Ÿ Š&D‡ÇãÁž={8‰ÌFím†qÄ—kµÇÖâGq•ÝáÀÿ|å|×.š Îmœ• —…}<ˆºœ.øÆÈ;Tœ9s†u›)çIøM·~Õ¨Ð?äÛ;†7nÀçGWﺜ.ô ºát¹1æn}k6jaHRèSÜ¢cåA™àÎàà k¯ÿs9ôÐßòäßÓ?L8‚êùêÕ«¼Ö3[4*4*|þq¸FF1ìæÏˆF!“bóºe8ðû¦ Ç$ZSB Éá—¯ÌAÝÙàC%ÈeÈÍ0 7óQËiœQȤHÕ'"Y«Â°{ƒ#£¸ÁÓ¹ËòÅ ÐØâ:ÍfƒÍf#ÙvÞåƒî‹Å9¬„á ™æ-ºz‡xë7yÙ©èr¡µƒ[¤ôššØl6ì|öYjD/óq(¢ï„Ãéá½hï >šzÍ_1Í¡?Dww·X‹À‹T‹AÄá BTp‰±à …‰‰‰Tƒ"¦¯¯]]]¬Ò>¾¡f£iÉ,J…É  Y¹Ë‹s‘“n`¾ºº:êBºï|öY(•JVi/^¼ÈZ¼ä÷ûÑÜÜÌ*­P¡P+W³ó8âõzñÿøpö¿aöµ÷Ö­=‘õ«r5qˆ@„»ÝÎʘúí’Wï;@’\‹×–¿ˆ³_ÿÖ§–q*›åÓV qSõ´ ćÇãÁîÝ»aw8Xç‘ —a[E±`]ÅNiA&¶UsÎÇëõâÍÝ»ÉÁ¡ üÊ‹s"b<ˆøôjIÌÍ•+WX ¿[_HsH˜ÆKªO„LßýцŸ|ôŽ}jÃ…ËÝèr±2,îráÂån|Üt~ß„Ÿÿç94¶80ìñÑ!l£ëÝË‹°F¥ÌÛ69õÌ…€ò"³É·D2|å»y]!ä2nùÑššà£Á XÞáëµ*TÌsœ0µ¨(ËÃs¯ÁÏ=‚>€ò•9¼†Jã%SÑd’µ*^Ö÷ ™”u”‡êêjjüÄBŠ_ô%'ax@ôÀç;ñÆU‹Y÷éÔ××cÏÞ½Qw6I„a.AÄ¥Hpì1“¶kÁG-3 !j‘Icc#k§t€À†aÈû,A1lAˆ€[‹Ô*±–_­VC.—SEŠ”öövVéÒ’5xúë¥S¿ÇK$“‡…iHÖ†fã`Ç£ AÁ. •×ëÅž½{£ª.F#¶lÙÂ:=Ûc«Õ ¿ßÏ*­PÍ¥™¬1l¼j¤êy ¥jlZ|@«x7cbž»=è4Ùêt¼˜ÿßîú…ê œxøžÉÙʺl£c~XZ9ߣR!ƒZIl ñ`·ÛñÊ+¯p;L®Cî›×I±PZÉ‹÷r¯×‹šš8x*Ám½,q–^£l]ˉ$Žwbv®]»Æ*Ù¨DøMÌŸ®^~üËÓèèî$ÿ>—-ø}N¿B<ø|>ôöö²J;/ÂF 9U„¬g>æà€rj²†a›Q§æMô@kj‚—5uJŠ`yG‚ˆ¸²,›×"A~g9–嘰­¢ÿë¿?‚—¶o@eYžàž¯§+|Ø®A}}8R[K€˜4¶®\ÏyPȤÈ6éy]»m\µšs>V«uÒ ‹s‚EŒ¤9y&m,¢;degSƒ‰Qºººpô£Ä| Í0ÌYªI‚ b :Õ"‚ ÜðºXËŸ””„øøxªHâó±ó6øüÖ5³~.—Å#3- ¹Á7B” žäà9Øf³á˜ÅUõ¹©²’µwÞÞ^\¹œ!Æàà + |(ÔÇBèydCÚƒóþîÚ”Ò ó·;äõG¤°9¸¨ÊØ4ïïî[ý#N¢‡Æºz¹™JÕ'’Q%!š>ùæîݬ"ùLg[EqÄ…óŽT6¯+äͨ×b±`÷[oÑœH°F(ã¬Íë–Gܽ áÕ’¸“ññqÖc#>¼>?Þ=|£cþ\ïÂån=„¶ÑVæëEX"‰C E·-á'jT ˜StÈHKâ!ĨScãª%´¦&"¡<»G’ˆ¸|eÞxî<÷øš©ŸúÛÍØñè(-È ‹(_ÂJÁÚS}mm-i“ó’@ÆÖ|FD?äSô°yÝ2^Dv‡ƒ"/œ(0ÊJ¤ÍÉÓas–•/ð³""¯×‹ýï¿ÑÑQQ–ÿæÍ›?efÕ$A±hAˆ†a^ð‰Ë‡¤¤$ÄÅÅEE]¸\.jw¡h±kŠÝõ;‰JîËJAzŠñÙ,ËYÀÉsðÁƒ£n“þ©íÛY§=sæLPÑØF…HPHQ¹ZØÍ2sŠ.drƒ<ܵ4x‚-6 >"„à¡2¨ïs=üêd3÷OIRÉø‡A|s÷nx½^Nù”d’Wî ÙVQÌÛ3³ÙløÎw¿ Í‹ „8äÌI7 0ב÷K¢‡ÈDhá7qo.¶3!;¸p¹ŸŸ¾€LLL°JŒa…L²¨¦¿„Sœ¨I‘ªOÄ"³ÉZ¤RvNƒ™“±qÕbZSaG¥RE]?‹Ü ãÔO¤0SøÀfLáò~^]]M‚)YYY‚äË÷yŽD‡ŒTîÂÃés:_¢‡@ä¥êšêSDÄôÁPÎÉ^Ÿmלh»æ„¥¡uêçÝçîøyûà ¼¼÷(.¶3A_§€1ɇ~ˆQ–ýæÍ›—âââþÕ"A± Å×%‚U®ЉnÒ‘J±À`DâÍ8˜5s~opÔ ×¨£ãã¸îŽÈ{9áÊÊʨ5ÎÁ\Ñfؤ†VÇõŒxÇ)OEY._s¢Û9Ä*}uu5víÚ%ØaM¨ÉÊÊBUUjY„˜öûý°Z­ójÿèííeUÆu+s¡×ÿ¼×®ÌÁŸ>w``XØ ãòÔû1âŸÿ5Vê—âTocP×°ÙlXURBP °!muÐiö­þ®Ž\Ã'= A§mïìÃÀzކ;j¥j¥n¯*‘ˆ8êêëQSSÃ9ŸÒ‚Llã]*– <·Æç¼¼^/v¿õ*++9 =‰ØCˆõþcî©? zèêÂ7¨„™¹,"½$Æmלa¹îà°‹3Œöø0쥊ˆØxÖkUpŽÁ76NPDõ\˜kÂÀýCx}ãèrN:»éèîLjçÎwX¹L c’Æ$54*ÅmOÖª‚~‡)ëµ*¸½cpxá RðÊþqÓe^×Ô[ªª¢f_”žlŒ,#YD©LSk‹ùŽ)(­=A_×îpàÀÁƒØùì³T 1ŒZ€9CHaxª> Y<œƒnÎyDGO^DŸ‹{~õõõ°wt`ç΂±уJ¥‚R©äìØ'sr` ÞÖÙ‡®^¼>?Ú;ûBö¬¨_ÅÇÇÅÏ?eÙo޼鋋‹ÛÆ0Ì Õ$A± ‚ DÃ0ƒ&“© ÀÇb,ÿD¼+M™¸O—<ï4×G†1èó¢c°×G†Ñá ¿Úº¡¡OmßN=³PQ–ô¦£\Ü #\#£p\Äφ6J… OVãퟲ bw8p¤¶6ª æ¶TU¡¾®ξà7®^½ŠE‹!5õî¡­Ïœ9êliÉìܲ710äLhÛ*Vâ½_žìëSË‚;,--4‰ŽŽŽ ¾¯“iX_«v]5þê!¸üÁ‹ - ­¼q§êáðc||‚*Ÿˆ<‹ÅÂ9³Q‹ÍnØéð)z‹Å[K  ó†oã¬Ò‚L˜S"ßW‰"‡òâÎ"S‚;Þ0EZðúüP*dP*d0êÔpx182Jý2Œ° ™:t0Tw"¡½³/ï=tº«Ýýóþn‚\sŠÀä^T`}`6ê THïHL: c|âú‡$kUðúüpºÑ?$L†Ü #§¶q/6¤=tšµ)÷ã_.½\;p:‚*¬‡•ú¥¬¯•$×b߃?–“tÚÆ6¯+„R!ãt¿IL TÇãAuM ¬V+÷u†Q‹ç¶>Ĺ“¢¥B†º³íü¬¦ oª¬$Á2qWø\G… ƒOHô~ô%EwˆÌ):¡p!+ IDAT\lgÂZ†€Wf]¢ýC¸F¼T1!†‹a‰$ŽÆTâ6FÇn÷T;×£×(¡×ªž¢C‚B†Üt’µ*d›ôöŒbpdt^ò²SaHRãèÉf^D55E{ B½¦ŒÇb‹i¼ºD%t‰“{ï>ÿ8nܸ‰±[ÿJ$qˤPȤHâPQ–‡ã,÷Ìkjjb4’1)Á åÅ9!ÙsS*dÈ6éÑ30Â9Jq@ôðqãå D’w£¶¶Ö¦&!ó"??ŸwÁ›9yàVô†æ¶î° „^³‘ÍÀÀ}ø¡hËóæÍŸ^¿~}Õ$A1ÿ^K€ Bœ0 óšÉdÚ`½Ëÿk{¾™[¹$žUúì¤dd'%Ù¹Gk_:ûÑÚ×ßxhBÆöÙgØñÌ3tÀ3Ç7!Q©à”G¼D‚Ü #œƒn0}üF{¨(ËÃåkNÖ¡0«««Q𣉢Îív;êêëakiÝáà=ÿÁÁA\ºt ÷Ýwßóûý¸té«|‹›±¦hÑmŸ)2d¦%Á˜¤FW¯Kˆey¸ÐÖnçïyoH]tSB ÒŒ¸>ê *]‹ÍFž³ˆ»R•Q‰õ©eø¤§!è´Û”dr.ƒB&…1Ižþaª"¬óäž={XE:š ‰øgóºB˜St8tü oyÖÖÖ¢¾®;wî$ƒ bNø<è •AŸè!ÜcßrzÂý™¬û¸­)î4‘Hâ`LRC£V oж豢5S 6 {10ìÚ»<>ío9餧è VÊ‘ž¢ƒLz÷}u£NÍë y=“a†–´¦&îFVf&/{Òb‹ …lÒTd®÷–ò•9hüÜŽavÂËwöìÁ÷wí"Ãì…/cëP Ã{\#Rôy9­ã2)¾ö`>>nºÌ:bÊLH„HÌ{.æyì fNò ¹Aãçvt pîÊ3Ñ‹×ëžýû1::*Êòß¼yóR\\Üÿ š$‚$ô‚ DM— >vc–Ϋü¼`K¥X‘fÆæ¼B¼¼æ+øÆ²•¸oZ4_,ðÓŸýŒZá-Ò’5xú를ågLR㾬Þ„ž¬,F‚‚æÓëõbÏÞ½]uõõøîw¿‹W^}‹E±C€ææføýw[X­ÖY?Ÿw‹¢TÈ›aDfZâ%ü/c¿ýç°nwƒ­Gþ•úeA§"<-}¼³Š]€¨º3m¼•A£R 5YC•A„…c ^yõU;D8¥™ØVQŒ9ÏÖÙׇÝo½…={÷ÂétÒC&f%+“»¸OÌžúº m‡’œt sMô "½V…ò•9¡½¦Fy×60Ù7uHÖ’AS(àK´¦I‘mÒC!'ßcwÚ;ûPw¶¿ÿ£ ÿv´'Ï´C.“ÞuΈ :5­©‰¢RóÓæÄ("¯ÏKC+^yïwxyïQ¼¼÷(^yïwx÷ð)=ÙŒº³íh»æä]©TȰ­¢˜}¹½^¼¹{7ìv;uŠXx2Ä—0\—¨DfZ/cÐÆU‹±|ñ^Ëg±XðÊ+¯ ®¾ž1+Ù< î5'{}~4¶8ðîáSxsßpôdsD‹ ¥¥…D1ÂÑ>Bww·X‹?·a˜AªI‚ Šð@!j†4™LU>cù»=#øcO'LMç5ß»Öëna¼HŸ;wŽâ-¾÷­¼ç)—Å㾬\ïÓÇOê5*Ík¸s«Õ «ÕŠªª*lª¬$oxÄmðaœ%vOýä•<ô ³;h®®®»Ç»ÝŽêêjØްÖss3¾ò•¯àÊ•+èííe•ÇãЍTß·“5HËà¸>ȋʋsÑåb-ˆ™ÎJý2@’\‹Á±àŒ4¥jä&f£m¤#¨t-6 DDvv6oÞæƒaCÚj¬ÐàÜ@pQAšÛºy<“ž² :55BøŽêØ!èµ*<·õ!=ÙÌËü<¯×‹ÚÚZÔ×Õ¡jË:Ø"8¯ï£ÉS@ôpí:ELй ä¹8âç >Ö¿æÌ):ÞÖÒ¨\®ˆÛZ´ˆÐÑ;0Bb‚7.¶uOyÜU+åP+埸Áa/†=¾Û„‹W-†Ù¨åÕ»4­©‰Ù(ÈÏG-‡ôù SñðKX§óO`ÌïqÖëçmOY†Þ¨¶U£Ÿ‡¥$¹–Sú•ú¥A l6E9'®Š i«9çóbÞÿƒoú½ Òµ‰ÍÆpÜn·Sc æÄãñà˜Å‚ÚÚZ^ó]–c¶Šb;„€En†‡ŽŸá=g_ßÔÁÖÚòròNÃpŽ–dF§~…LŠÔd zú)“T®Î£9…`MÀè ‰x"¢5…LŠŒÔ$ y082JQtÎÌæù]/1Id­ ®ïmm-à]úØmöøhMMD$/l[²3‡¹˜¸qcNƒf÷,™fYŒOpòâÞ&À>Üè˜ï>ïûá Ö¡¹FT”åáxC+§ë^¯MVkL†âU+åœ ÕÙbLRC­”ãjw?§sŠ•ey°°Ü¤·X,ÈÏÏyÝ{<ìÙ³'"ÄΞ=Ë:íó[×p¾¾R!CÁ¢T´]ëcíYg&;}ï>…nçë<ª38•a¥~);~Tš––⇳Ÿó"x¨Ê¨Ä·Y¤k»æ¼Í£°1µwtP"f¥®¾µGŽÀÙǯa@^v*ÊWæ`Ôç'ãÔ0RZ ³Q‹}¿þ Ãü¯…œ}}¨­­Å±cǰé–G/:ޱ1¤®ŽUº¹ ›£(ºÃt4ªÉHl$zà³QË{ä,"6!ÑkŒŠÖôZ4êôy¨îÁHâ ×ª KTÞ&|0êÔxâ«+ðqãe\íî§55Á;íW®°N[Q–Çû¾â%’9#³‰T`Ä{»ÐhtF” ¯Ï‰‰ÉßGx%M]óV´‡`×¢•eyèêuq6v­¯¯‡­¥/¼ð’F1—.]bV ÂpJ1íˆU!“NE^:uþ §³Î»õµúúz”””`Se%òóó©aÆ|DZIPÈ`ihå,xŠœtÃmóò%G/F9ž{½Þ©þ“•™‰ÊM›°ª¤„„z"âЇ¢»»[Ì·°ƒa˜³T“AwB‚‚ ˆ(‚a˜³&“é%o‹±üÝž49¬2šBz]]‚’sõõõ1)xxú‘RN›û\P*d¸/+Žëƒp°?®(ËÃåkNÖ^Ë«««‘ýÆ!=´Û³w/ïFœá¢h±kŠñ’×do¢¥B†ç·>ÄIô<$ɵ >µ)÷ÆëõÂf³ÑæµH`3vœ¼È˵“äZ¬O-Ã'= A¥këìäàY¯Qe¼ììëC]}=Ê×®¥†D˜Œps¤¶–s˜î¹híè™ÓëZ‚\sŠvjþË™:(R$kUÐké@„/Ì):¼´}ŽžlFc‹Ckx½^ÔÖÖ¢¶¶k×®EùÚµ4·ÆÕ558s†ò✨C‘è¢U C„‡T}â^¥ç‹Ûã‰ùç.Ñš4^‚T}"’µ*ôyàöŽQÄ"hrÒ ÷üÎlÂ…LН=˜ó—»ÑØbÄÐ’ÖÔ±I]}=:Ä*­Z)çÅ9N$3ó<ånç+V-ÆoN}.H9šÛºY‰o·Uã½Ã§f. ξ>¼òꫨ¬¬Ä–ª*2"2<~ý›ß°J«×(E% ŸMX yÙ©0§èðû?ÚÐçr RF«Õ «Õ:e¸MûéÄ|ß‘2Rõxûà AœÎÜ‹¢Åf¤40%kfÐLE~ÊÍ0Üuî<}þ ^«>Æ[9ìjjjP`íÚµ())‰I›1ÑØØˆ¦¦&1߆aj©& ‚ f‡AQÃ0ï˜L¦ cù­}×aHPba¢N\å¶ZáñxbjS6-YƒÇ7…µ ñ .HFÿ]½C·yB †'+‹ñžÀ¨/xã¯×‹={ö`×®]!©ÿc‹`†œáàå§7òÞ&"MôÀ•ú¥87ÜÁV DC‚"xÑØ‰ëŸòvýªŒMÁ ®9BŠO?†8pÙYYä .ƱÙl8pð ìv{ØÊ0:æ¿M<9—·C³Q‹… ¹ÆI„Fž+ňR!öŠb,ËY€CÇÏ`tÌ/ص½ŒÃÔÁ0cˆ§Ó §Ó9õû¯ó\¸p]{L¡R€¹1ÒàKôàt:cÞËó²ÿï˜:˜ Ú ÓÙíö˜7Ø·á€ðá†î&Ü£cp{ÇàöÎíÕ["‘@*gU×±üîôð÷apØ‹þ¡ÙE>ÓEʹé\éêÇØø¤ñ’;¾;ìña`ȃf€³Y®Ü¿tþu:]øàCÿE‹Àœ¢ÅÇ—3´¤5uìPW_ššÖéÃé`)Y±Ä µR·wL¹'Q)Œ‘¿R!Ãs[›?ù/ïá‹Ö¦&TmÙBFØQÄ‘ÚZø|ì"”l«(ÝýÎQ)˜wío|u[‚9ö¾4Ü>pàÊË˱©²’"0E16› ûöïg>I“€ŸZ„7W+åX±ÄŒÜt#Š–˜aš&n`Ú¢E(ZlÆùË]‚®gKV­BùÚµt6atuuáßÿ\Ì·pŽa˜©& ‚ îò.K€ "*Ùà€b,ü'Ýh²ä0(”!¹^R?×i²ZcjCö{ßÚ1eIÖª TÈà¸>ÈÊÈ]¯QáÉŠbìûõŸX]ßîpàÀÁƒØù쳂ާÇãÁ‘#G¢¦ mÙ°œÓ¦Õ\ÄK$SÑ?æ:Ć€èá?ø/ÖaÄ“dZÖ×_™¼à!šD1bÇn·ÃsËcjË­z X²­§w'Î|Ž•ú¥œË·!muÐiºzùÿ:~Ã,ú—×ëÅ+¯¾:åY§ ?Ÿ &b›Í†»gΜA{{;ëÔpð€83²”Ù¨…^«Bn†f£–Œ`ƒ 0ׄ܌‡qèø™9…&|áìëÃÁƒqðàA”””`íÚµäÑ+BÆè˜6çNŸc½½‚GGËLÓ£¹‰‰ˆ.3E†$uÐÞ]c]ð —á1Šî@€D‡4C"=ˆ ‰$/ÂI4*Å­±VŸãã7¦¢wH¥ñËâ¡I‘— ;3Tþ£yÜë}6Òœ0è5JlZ\™æ³&÷úü8ÞЊº³íA—I.‹ÇWï¿ã7ÐÕë‚×çGWïPPÄ¥™(-ÈäÔÖ|þqè•HOÑáÔ¹+‚ZÎ\S“—ÜèÀn·£ÅfC}]ìöíG£R„ÝÁR$òüÖ‡ðÃÿø˜÷|³$ß1ÎMܸ1u¾2êÇĘ˜øò3ï­Ï&÷Ë×àÝçy=8ûúPSS˱cxê©§È‘Hq:h±Ù¦¢ °!Y«B—smÓöÍF}~töºîšfæ;xÀÉp{$ÖP¬å¸J 2±Ðœ,¸ÑëõÂb±Àb± ??j‹öÕÅÍfC“Õ kS§½0‰$LŸ0>ÕJ9Ö-Š%f¬Xbäœøå§7â¹·~.ˆ`00oú‰"¯×‹÷~üc1ß‚ @Õ$AÄÝ!ÁADÂ0Ì ÉdÚIуNlå»1Oºx4+rI¼à×KâIXa!ÁÚ¢…X±ÄQeR*dÈÍ0àzß0zƒß\–³¥™¬õêëë@PÑÃ1‹^¯7*ÚZ)ÇÓ_/ô™iIÀ›èáþ‚L|Üt™Uú„xöžÉÖ¦”bÿ•_•Æf³Å\Ô™p0ÓÀrº§h¡E''®Ê‹àa¥~)t2 \þùo^Žù10äá͘óèÉfÎÏ:²23¡R«|ihc4‘r˸’NŃÇãA‹Í›Í»Ýµ‚®.纜C·ì„9éFäfÂêu8ÒQ*dØñèhncpôä…„Yà+•J¬ZµŠ µB0çöÞšgbÂH.uôàRGÏmŸé5J赪)ã‹ÜtCH -„dºèA!£íå`)/ΉjQ ^dññô‚$’½+dR(d“{(¡ ðNxǵÏ!&dÃô÷²ÀOŠÑˆ¬¬¬ì]UÏJ… ›×œ¢Ã¡ãg‚J;æŸÀ¹Kxiû†Û>Ÿ>¸00ìEW¯ ½®;„ r*Wçñ"ÖQȤP$I§žSùÊüêdó"m!¼ËÓšZœïéV«¶–ÞÄų­œî,"ɽ{ø¯†›³ïÄK$S6îicâÆ äf‘–„WÞû-<£üDº±;ØýÖ[ÈÊÌœŠCD6v»}ʸš‹à)@ÿGO6•&˜ù*y5ðžF˜St¼î»ÍQi||b^i:uH¢=Lßo±Ùl¨Hˆ(Ò>Ø;g+4š7nòZδd Ö-DeY^Hý¤%kðÚίáå½G…‡"ñCÄðÞŒÑÑQ1ßBÃ0W©& ‚ îHAD) Ü5™L/ø‰ËßçóÂÒyf抦ÌV«5âð:ìv^òy~ëCyñ Ì):¨• 8®bbžS†íof£æÝ”‚oãŠP=„ó½4Ô„­\ˆ$OòA9醘ZWÕÕ×ßyÐårÁårM ø…^ãß £ÑN•JN7 ÈÙÛËùÚ¡X?¢,+zèráÐñ3· 2&ºÜ*ï´è =L¡”æžßúêζÃòi+/ÞÛiM-~kt¡Î@ôZ6”,¦=•eyXS´–O[ÑÖÙ‡ë}Ã8¹‹u~E‹ÍSB 6„ËsàG/<†ïí=Ê«Ãîp ¦¦µGŽ`my9Ê×®éq‘öN0®njjƒ®¹"¯N_NwZÀ5‚ãôˆJÆݣóŽRˆöpúÜ• £*²eæ\œŸŸO‘"ŒÀ¾¹ÍfCGGGD÷Á@$‡Ç7,Ë;׊%f|ï[‰’4ç9Mü0}MK‘É…çЇ¢»»[Ì·ð:Ã0'¨& ‚ îMÜÍ›7é)AD1&“i€gÄZþB½¦¦ßó{²øxÈ$€J>·×3ÿÄüðø¿ÜPêìÇ穱̓§)Ü3?LܸÇõA¸FnWòwõºÐÜΠíšS0OeùùùwÊÌÃpönƵv»¯¼ú*ë2édTeVb¡úö0÷g.âìÀçèpw†¬nÒ’5øàõ§BÚ×9‹, ­AO~üÕŸaCÚê[Ïús Ž±Û˜þŸçˆS½ÁS•••xjûv´æ`.AƒÇíõÀç‘$×rÎçµ ïàõ ¢,rY<äs…¼¾q4\ìÚ$Ææ€çѬ¬,¨Uª)‘ÁOlºå-¿¥¥%j¢ I‚|2²Uaî,Ë1 b(ð, mÓÖLÓÅ¢Ó™yœ›n€°r yð³ãgBâ™v.JJJ¦µh¼˜éÆ|z‡#¯’¹Æ)„øuýE|bm£ÆNKÖà鯗bMÑÂ8¾‹„9j¾ý(??³ØßÁñáÏ.æ[ø„a˜ T“AóƒA1€Éd: `…X˿ޔ‰eÉ)I$ˆ—H J'A‚T ™$²xv^ׇ}>ô{ÝèÂÿ×PG e¨•r|ðúS±A1_œƒn0}Ãh¸Øº3m!ó„)èd¼¶ü¥{z‚¿ê¾†Z‡û®üçZ-SNº?|asÈÛWÑÁÃàØ®ûœèöô°ºö/¿Å¿\z?¨4Y™™xã7"§/:wxŒrS/ ÷â'«ˆ9OpΧöš[NþePi6®ZŒ¿ÙV~ÛgJ… ñ·Ä‡óéO¡ô¬#$FƒÆ””)1E‰˜ ˆø ·«,Ë1q?´]sNF”èu¡È#ˆH€ÏÃá`ïíÐñ3ö†}¬(¹å/–½zk­VkÌ æÛoˆHõ@ÞÜÆ`ÿo>£Êšg}Fj´D"ºxyïQzó |÷E¿„ IDATe6¯+mùÙÖÇóëóÍ¡ãgX "UôXS[ZÃ*&¦5µ°„Zà0F-¾ÿ>äf ¾µ_œ›aÕD¤1]1âõ¡íZr3 XS´+–˜¹æˆ×‡ïí9’q"+3ùSÞç þp:è¸eXmki‰éÈ«÷ÂlÔNF‚È0²Ú矸a÷(GFqcÑê}þq\¸ÜVb`.žÞÿh.æ·ÿM ::D×ÿŠ›ñô×K›g¸ÎQ¯ýë1NÑ‘øD©T¢àV?ÊÎÊ¢s$tuuá={Ä| V2 3HµI1?Hð@˜L¦…ÎЉ±ü R)þjùýHWkÉßãÃΣ‡¨¡Ìƒï}kcÈ ¹rî‹.üÓÿ…ž‘˜«/LƒÂJýÒ Ò]u_þö_àÛ¿Áå¤liɼ¶sSÈ ·¸ˆ¸._E\\nvá4/_ÅÎÏþ>ètïþßÿ¶ g»ÝŽºúú{zž x lÏ×CP@ÄÐ{KHø‰&Aýx,£µëª9çsvàsÿîëA¥)ZlÆ_ØÌº/E‹Øa¾ÌŒa4‘b4N‰$b‰ºúz9Ì|Ä¡ˆx5ÆÜ…9&Á½Ù7¶8`ùÔváC€éÑn¬åt:ÑdµNz„¤¾Ïþ½X.Ca® ›×†%jÊ\´]sâ½_ž¦ š/}s½h"wâ†÷F!—âÿývED§4þ 3w~ÿÛ‡¥žß=|ŠÕ:»´ “×¹>^"R!½ãs¥B†øøÛȤñËâïüÞ4ǧÏ_Á»‡OãzÿpDÔq,­©ù^ŸwØíS{v‘ì1:-Yƒ´dÍ”¢h‰&ÃägDä1âõáÝçCˆ¼f³Çf³M ì䔀#f£vÒÑGŠ.¨ÈÃ\#^øÆÆçõÝÆZ;z"âž³23‘uë|‡¢1Åfÿ 8wˆD¡ÃL"m-;Û\–••…ì¬,êKóÀëõb÷[oattTÌ·QÌ0ÌYªM‚ ˆùC‚‚ ˆÁd2U8"ÖòëJ|¯øA(¥RAòêðÔHîANºïýý7DUæwŸÂ‘b¶Îøðþ¾¯ýxíÂÛèpwò^>µRŽç·>r [ÑÁçÎ3X¬ÉFóà%Öå~òÔ_ãú¨3¨4Ï>û,Ê×® éóµÙl8R[Ëú°499K—.EÑòåt¹àñx¦¢3ò'¾dà‰óH’k¹¿\Ô÷Ó’5øàõ§h\æ‘À¬ÑhœÚÌ.˜å31b·ÛqÌbAcc£Ø7ŸEEÀ8º|eÌ):4·1¸ØÞæ6£cþÈ[ïk”ÈÍ0bYÎæš¹†×çGÝÙvÔi¸gð8Ô»‘F Š‹åØ1òÉ3f£Ïm}(bŒtÉàv~”db[E1="$àaþä¤~Ë(-` &hü½7Û*ŠQZ–k{}~¼ù“?°Zs&ÈeX_’‹ò•‹`Щññ’Ûæþx‰ 3„ r©ôÁ‚PXZñÁo#ÎX,ÚÖÔ|0SÜÐÑÑÒè B¡VÊ‘›nÄŠ%fäfP´ÄL!"ˆ_~|>¬sTÀ`4à9›ŒF' WÛívQzózs2 „¹†»¾ËûüãpŒÂí»gÔ‡a§Î]ÁÕîþˆ›‹³²³‘••…‚[}‘‰ÑÙÿÂuÆËü¶–†Öˆ>˜î,ŽD³¼oy½xïÇ?Fww·˜oãÛ Ãì£Ú$‚<AÄ&“é5?kùsuzüÕòûÉ›÷æŸþv³(¼3“ž„~øãôù«1]g7·ówÿB ž{| ßXÒgÓ?äãzpÑ!Ù V‹ñ©ó “îÃ5ƒÁ±!Ve~ëówq¬û“ Ò,_¾ño„ă¼ÇãAuM yn1|›`ᯠº[þù¹y÷zÿ0~𯿫'ùh!+3*õ¤ÑËt㬬,¨o…;r„ÍfÃèè(>9y_|ñ†‡‡©âÂL\\Ä´ÿ£×(Q˜»å+s ×ò(>4~n˜ˆsõ÷¬ìlÆ)/y‘~°e³ÙPW_úúzêxðž Áí½ §‡q"6yûà t9‡èApcÓStÈI7ÞÓ(-œüöt >nü‚*lÌF-^Ú¾!¬eèêuáíŸ~Â:½ŒÈ"Uø ö55‘P[l¶©(¨±æ4$'Ý€KÌX±ÄŒ5E‹h Œ€÷„תEÄø0ÝhÔh4";++ª#®Úívô:“†Õv;œ½½$nˆ ?&E³ noܸ‰aÏ(GF1>>qÏõÆùËÝ'|˜ÎtDVVRŒÆ¨íƒ6› î[¼â†hŒœ²eÃr<ýõRÑ‹ - ­øåÇçEun•?m=íýi¶unÀA^?âââÄ|[û†ÙA³"ADðà "Æ0™L'¬kù×™³P•ÿg¦]ø5ì®j s°¦h!^Ûù5Ñ”÷µêßǼØàWð`_û/ðbÓëpùù=¨¨(ËÃËßÚÒç¬è­à¡0é>4^ÂÂÄ Hãâqy¸ƒUyßý þáów9ÝsÀ8z6cèéÆÒ3qO‹°0ǃK—.¡³³~¿DhY¡/ÀÙG~Ç9Ÿ ؆Oz‚J3_ÁÿmÄ/Oœ‡Û;FF¦ %f‚%`@1õ{o¯èoä2˜S¾Œ’¬Ue\?0ä¹-bPWïPDFN;9锯Ì,êCc‹–Om-|˜­ßær£Ñˆ£1¬B'ǃºúzXŽ‹ÊCÜHåû;DÄò&w6¯+DùÊzDÈx÷ð)óˆÙ¨En†1"‘­*ÒK¥X¿*÷d >_*2ÄKâ —}aA­”êÏ^Áÿ9x‚Sþ¯íÜñÆÛb>ÜmM­R©}ëßH5 wöè¿{Ün2džƒ5E ±¦hV,1#-YC$ Œx}øà·í5°WÞ³Å Š š¦ŠqoŽ˜ÜŸÌÍ0L r3Œw¬ÿ†=> »ï5wØãCc‹­=¢¹w£ÁcJÊÔþt Úq$Gh ì‰æãÀï±ÒÿÔJ9^Ûù5Ñ8Hœ/mלøå‰ 8}þŠhϱâ¾À<˜×Dt¤•À|6ý:ðÙ\‘ÉT*4Q¯íÎØÀ0Ì ‚ ˆ !ÁADŒa2™’œ-Ö{øæ’BÜŸÆï‹ôÿ>iAKïuj ³ “Æãß_yR4‡ï>±è¡æÈºEUF%ïùŽ áÖÇëÞá5ß5E ñ½om ©G¯Ï¶k}˜¸Gx`¨;ÛŽ£'›ƒÊÿË_ÄŽœ'puä&fÀ”‚OgX••íÅ7Oý 5lâÎ<ò[¬Ô/å”Áý"ÿœû¢ ?üEeð@D'  ¦¢lÙäòwæÁ¡´]›ƒt9‡àõùÑÕëÂÀ‡¼-s@¯Q¢ru>J 2É¿¹AÝÙ¶¨0KàX0Ëq0‡Èê.— Ÿ64àüùó§Fbž{|MHƳù@‚‡¹Ñ¨xõÙMô ˆï‹àA8ÌF-–å.@aŽiV¼B00䥡-dÔ̆҂LT–å%|Ëâ!—NŠ” âã%¾2ÄK$ó¿üÓ|´é¤%kðÁëO‰âYŸû¢ ü¶ç/wEÍšzºñótñlD3×ÕÐ1í³éæ2ô"‚#'Ý€Ç7aMÑBÑ{£#bÜ# bÏ|Çž>ði@:Ó±Èô1!0fÀ)¶Æ¬Ü #roý Ì?êðLJó—»ÐÚу1ÿ„¨ŸC@”t¯ý-¶‚Å™ýŽæã»Ž3ÜP3âõáôù«¢‹úÀf;s;}nSs˜Û:fYçÎ\ûr‰B&—Ë¡×ëÅ\.+†¹J³A;Hð@ƒ˜L¦•NЉõ¾[ü ÒÕüà¿Ýp×h£p6ÂáyŸ-ç¾è"c›i¬O-ɇ –ÿÙÏñbÓëAIßœt~øÂæn˜ù'pµ»^ßݽ#¶]sâ½_ž*ï,U•‚Q¡GaRûÏcÄïaUÖgþm#Ô¸‰Ûx&g+ö­þ§<^»ðNÐ"¦¹×û‡ñîáS¼FÚ)Ð-F‹ë2U6Bȳ%¢­³]½®I1Ä0°DJ½ ›ÛòœLâ%qHÒ¨ KLŒúü!^ÚVQ1ãÛ+ïýŽúËD’0…77›7€éZq‰ˆ‹›üqTß‘ B».ÉÍ0bYÎA¢Q‘Ð_V,1ãáîÃÊûÒ§> €@¤‰ ׿*z¸—ÃHãzÿ0~ùñyXZ)Ú#qT”åá¡[шÐ+Q`^¶gƒ‹‘'»Ì@Ì'êƒÏ?ŽÖŽ^\¸Ü…a"Á‰§)ÅÓ_/©{n»æ„¥¡•Ö³„T*Err2ââD½µ…a˜ZªM‚ öà "F1™L;üD¬åOJñJé:(¥Rv 4ò$ äF)dIñøÏs—ðÃÿø˜ÆÌç‡÷ÿöÞ=,ªëÞÿÃÜ`¸#《 ML$…`ŒÐF4&ÇxK/'š˜¶æÄsz¢iÉ©Oûm‚éå›TóæäÓØÓš`~M/ m½`{ Q ´ÄÈhP䪎ã wf†anðûc˜‘˳÷ì¹læózföš½×Z{í½×ú¼?ïâæ»CѾÒi‘AŒK¼åò0’½W~‡οÂYyþ=€¦½í=† ßg+xx,ùëÐ[ú#ŽÂâØ…P÷ßB£Žhá­«ïâÈÔ±§1BA(¬¶AÆÛuoT!FÅú{¹¼1Hð“ÄØHˆ¡èÖ§LHEܹçLO± æ*â! Oéú iïEýõvÔ_»MH0¦hS s2ƒº*U-8§jE¥ª…Ä~"$$qqq y=ñ´K«ÕSkAxxM ÁADð"—Ëxš¯û¯ˆáßÝï¶èA q‚âD1‚qïo~ù}^Yêú‚üûæá'Ï<Ì‹}%w×D‹d¨xøC,Ž]èÕï¹Ð]‡-U?Bm÷eNÊKKŽÇ®gWù|ñYo4áF[K«_M{/ÞøÃ§ŒÊ[>#Å‹^„ $ÈM¸Ö!+ÎÞ®aµgÛÿŸ©^§Ž=™)+C¦›¬x//ÚâE;XïÁCñÖ•ÈSÎ…Þh±ò‹^ÉЖ»ßøp”˜£Õ ÆÁæÃøX}г1‡˜(¢°ìÞô€:Dg ·PDHÅ„†N@=òÞFÕ ±ÿ¦à  ñE–ûî¾~œ¹ÐŒKM·ÈcšãVµgë–OyžºC“ºÝ:£ÝÙEÝî¾~ûQ¬LŠŸ<³"`êÍ}r0ðäª%£²ˆóŽ!Àb˜¬„‰An~Äh²àï_]ÇŸÏ~E•áGâ¢Â±x~2X4)3bÜÞ®³×€?Ÿ­CÕ%rvô+r2±mCžÏõFö©d%Hâ«àa$ׇJU+ÍÁE„TŒÂœL¬/Pò&ñÓt ­K‡÷Ž×Pðh‘6<ÿž’±H€ÔÝ öçpcVR,c#!„bpÐuB%‡ëCýµÛèì5PåS^÷<¿fz¸Vr‰üCLL $^ T?ÕjµùÔ’AžC‚‚ ˆ G.—_Å×ý¿†ß™Ë÷a!Å NBŠáä+îMê½YJ§Ã$ÆFâýW6ñf‹œD¥ª•ή†½A¹Å÷b_ýï8)Ï_“i¶ÁAtô í¿øÊTT3RðùI¹€+}MÐÛï›ÞjÀ?}ú=êÔ< B*Fz²½ï¦§Ä#R*±ÿo¸?Ëãe.QõF6¿ü>ãkQ´H†ÖÇϱ>Ï?V—aÝgÏ2Úf]þ"èf¯Mìº3vµÔ¸Ð]7üózÌ}h5¨qÍp“:a‘½`'™t™àpf°ÿ-„Xdÿ;L,‚@â– Ñøo4áÂU Î}ÙŠs›½š5žø2Ó½¦½5—oøa¡Hˆr |ÕNšŽ>4©; iïeÔŸž~ôk¸']PõXsù><ý%u¨aæÏNÄÖµð÷XˆèÁ?\jÒâ«æ[ä€8\©~®¨mÐ3‡ŸÝ×ç+±yu¶Ï¾sÿ‘sŒ3›ýõ3Ó*}“ºeÕõ$~ 9À3ŽD*eÕõ4L3RgÆaÙâ4(3NÑžCèB÷<ÞcfBfËc‘‰”Ñ~N×o‚ªQƒVMtý&ª8b±EOæcù}ó¨2&ľA&“!<<œÏ‡p Àb­VÛC­Iá9$x ‚räry*€ ¢ùz kÓ2ñbB„!Ç„:E‚0æ«ëMêì?RôYt—-NÃϾWÈ«}^÷¿£É„IÈŠ]€ƒ¹¯{Ýé6ÆŽ/v¡×âù…?3ˆ˜-6´uéÐÕ×ïüSÁC”(Ÿ<ô[çëÜ„{&@o5 ¦“m÷Žó¯ ¶»Ž:µŸQγ/Ò8kÒSâ!•L(d`ÂîCå¬}5-³ƒG‹døø¡N—or¡»k?ÛÊI=úÛ6ÕqŽtõõcߟ¡ùf'£íË¿ñóïű ܺëÐcîc¼?›ãÝ–Ãtñò"Ž ”ÈpûoG°o䈠_¦cT[—m:4Ý쀾ßÐÀµ¸Î—‡@<<ž²s_÷HìPÑV…çw¡¶û2£íVädB'»°MÁGwnæd¢0'3¨¬´‡†Ó€ý÷N×àƒ¿ÖpþiÉñXªœëÕ…+½ÑUƒµÃ?L¯§S±ûù5~m'M{/šnv¢¦îºË@u¿8ÜÒ“ã¡Hä‡Öß!€Ðtô° ½îN“ûU|å.—š´ÎýžîèúM°ZíÊ€ÙòX,žŸì÷6²‹ôØ« †`Ù90°Ù‰ÉåÁk4©;Psù¹9LA˜Xˆ±2\oëÚ:˜™…ûÌÂ=óf"VÆëŒ˜#„¦Zç<¦7ðe³;ÁŸÊy ìÙ¾&húl“ºÃéxÂõóÁÁ¸*!:" 1¸/34&ñ‡Ãå”I›ë‡ç] FšÔövwÌŸù31•?`ˆ;ãcåpÆl?>Êy æfŽ988Z®Bɉši;Çš½`Ä"j4ŒQa˜«ˆ‡<^†¹Š¸ Ÿq[4]д÷¢õVupbiÉñÈÊP O9×gba¾Ñ֥ùÚ¨5¼¡Pˆ¸¸8„„ðzré­V{Z“ ‚;Hð@AärùoðuÿÃ%"üä©e˜“M„”U×cÏ!æšôÈ98^ð{Ì—¥{¯¢­ ÅßÀ§·«=Þ¿5݃ù³ÑÖ9yFíp@ôdÜhëñxaד,ðLè1÷aíg[9©C‹üpïǸԤe´Í÷½ätÖ˜'›ƒ”ð™€S.õ\e¼ºëðÂùWèÄ÷°?q)h‹#“‹7z½u~ŠàÁÓñ©¢­ [ª~ÄZlµûù5.'é ðáŠc¼övÖzži5O™ê\D F,À:œ,ý§¿)Å¥fn>&ʺç+¸`ð·àa$F“_5kѤ¶gꧬ۾Åáà HŒFzr|P ¤þ# Ú݇‚ÑîB"¡`”3‘·Ü‡¬ûµg*ÌØlƒÎ×¶Á¡Qî!¡ƒ°Ø,#ÆF+lƒƒÔÈPsùþQw= ‚€ceR(£í? ш‹’º”I%"BCì¿¡N-ûÿC}zßѤîD[—nøúÜ9mÅÍIq2¬Ë_„¥Ys=v T¼íàë9(½Ñ„²*ûñ8‚»Ó’ã±¾@‰ÂœÌ ó‚ésªV¨†I¾%uf2f'â¾»Rs÷l¿%IrŒáMê´ué|>ßækÂõÜAmƒ†Æ‚Â!*ÌÍœð~£IÝ݇ʃBÀ–§LEѦŒf§x¯IÝéWÑÖTHDB(fD#U‹TE$"×¢ùM4½äþ@¸dEN&¶mÈ#ׇ)p\Ãj4=.!!!HLL仨aŸV«ÝA­IÁñ5‚A„¹\~ÀÓ|ÝÿÙIÑøéæeQc%ÇkPr‚Y¶ãôÈ9Ø»ä%D #FeÝËÞ+¿CñÅ7Ðkñl"}¢ Z¦´uéðÜ«y¼(Ÿ»?t©)^oŸ-U?»ÍG<.')N†ý;7úuâŒM_û¹òGx0ñ~@jdʨ:¯êø6ãý”LüLˆ†cqìBÌ‹L\è©C£®+÷™½-h˜èÜ98ÁÃHCd¸}섆"lØ…A,Ž2øÓÀ….!4„¹~Ï68£É>Úlw„6Ûˆÿ9ÿvj.ß@YÕ¿ èâ†ô”(¢FÝŸEJÅ£úv„ã5O^F ø|²bØ -Ør+U-8§jÅéêzÎçJv=IÁ[v¾:Üâ(è™{ÂÃDXhÚ{¡ë7ÑIA`EN&^ÜT@ÁðZîèZæšøøx…B^7³V«]L-IÁ=$x ‚ œÈåò²øz wÍIÀO7/£Æ 2ØLžŽÌº/ `qìBD ]Oâ]è®ÃÚ϶²Îp»Pz´\…·Vz\N´H†ƒ¼Žµ)…^o£â‹{±ëâ^Ë™hÒ8ûÚÓs7bKÚF@Œ8ÊÙï@Ý ºkŒ÷ãÿ¨öà\; "„áØ{ßK˜'K÷ÞÙöàð¨í®óø{¾žOçÇVÛ AÉñš€ Œy:mæ¾Îh OYƒ¹¯3iŒÜ÷µŸmõX˜Ø3оó“oz%êX—ýqD0‹"¥¬ o‚*|Éà =ðÔ[ÁÃ×ÌÁ?ÞÌÔ| ¶Aƒ²êzFAk¹÷ÌÁ†¯ó离¯M7íJ Å„{ŒÌ(î7x#Ã=A¸‹#ØÛ‘½~¤ _ƒdê‘¥áÜ”c¶Ø`¶Z[¬6û¾šî¸Lè§á½”¿„QCæœH‹™ Ñ‹N¡Ž·ÜE>R&ÅÉP˜“‰u‹‚>0_o4áXùE­PqÖfëòaÛ†¥t± PÇ9 ĽñžžbwD›Ž0·Uo¹¿8êîÅM>q t¸œ82Ås1w”–H©Iñ2ÈãdHŠ—!)Nùðo¾_·9˜œï¸+thëÒáåwNí˜ìŽKÔH_“º# ë*.*w§É‘½`Ö¨¤céf Z4]hÕtáVGZouÑɤìÿñFJ\äá}-_Üa|ATT¤R)Ÿ¡@ªV«í¡ÞMÁ=$x ‚ F!—ËÃ.zàmZÌeÊÙxvÍjÌ ÂSÁ0µè¡Ç܇ü¿} µÝ—Yï§rž{¶¯á䘋ö•r6áñtÚì½ïeÖÁÆîr°ù0ž©*ò¸Šj4xñÍRFÛ¬œ¹;nDŠÂ‘§t¾g²¢ªãKXmŒÊ<|ã8þëê{¼¼î\C IDAT9G7Ìz?˜?¹Ð…î:¼uõ]4é¯yô]\¹©LE[—»KÊvâ±åñ³Œ\ü!x˜‘Œ½K^öHtÅÕ¸(cL[—ÚNû¬#KßHaD[—Ž× ·IqötGPERœŒB¦`ИF$K+=«ÂÿüÉ}Ñã×—dâ;+²1#Ö¸ "žÄ'¶uéðÞñš)…R1ÞÞù’âd0š,0š,0ÍοùB“ºÃ™!DwÆã¸¨p»ÀaØÅÄ „/™HÌ0݃½QðÀ„éàq©I‹ÒÏ.úLè&Bö‚ÙPfÌÄ=é31G Ah( #æT œSµL@š»A‡ÁJYu=Þ;^ƒÛ>;%ÅÉP²ëIªPž¯M꧸˜Dö{jy¼ éÉ ÓJÜàÎóUYu½×ÄëòaóêlÎçŽôFʪêQV]ï—þëDde(œŽ¹é)ñ¼Õuo"çUÏˆŠ±mÃR·î9ʪë±ÿȹ ¯owDcÏ{‡¸)|R‰Ê ¾‘بÑxF“Å9‡e°@}»}z# fôèt›ÉÆæÕÙT^ºž5Ý쀶S÷¶áááÉx¯z¯V«½@½˜ Â;à ‚‡\._ àŸáÙZ‚eY³©1ƒ6‚‡›ÿ6ÎZ=ê‘¢p,Ž]aˆëÔÇ\ˆ¸šôÑMØüòûœMgÅ.ÀÁÜ×G‰@¼ßEMêl{í0ú]ˆ½÷½ä|Ÿ”;êý+}MÐÛ•Ù¨kÅÖ¿ïäÍ9:V`4¯ÖíÇ©[Ÿ²þ._$ðÁ’›©Ëƒ¯//Ú™ßõHhµ¥êGx·ùˆWöô#Å×çXu³ú~ó¨k†¯&á!HŠ“á÷Ï(‡HäÜÀޱ‚ƒÑ„ûãv÷ä9 Sñý5KB|<Œ¼öî?RéR`æÎ"²ÞhÂ€É £É³ÅÊ« àŽE㮾~hÚ{ÑÝ×?턱2)b£Â‘œبp(¢>n ¸F*Atbwà»à)#ökíë„ÙbƒÙb½óÚjƒÙbóêþœ¹ÐŒÒÏ.yý¸“âdÈS¦¢0'“¨ ïÁíÙÃ[P©jõù÷“ÐÁÍ{èAÀ<üµ¦üµfÊ{çÉ(ûÏç¨ByN“ºc8`lú‹…„„ >:)3¢±( Œ¥RÕâ?pIZr<^ÜTÀI=³q:ô%W‘¢>Íó8’‰¨4ÐÏ¡M×ìÙR1RgÆáº¶º~“Ge1öì>TîÓþ+Ž GBÆlÄÏŸÙÌ$Ì·¯ÇgÌD6þ!¤ãêu˜õt\½Ý­t^½Í—W¼ÚLDcûk  Öå/`h߸݃%!¼ |{o«7š¡jÐ8¯o|O”å@"‘ &&†ï‡ñŒV«=H=• ‹s $x ‚ \!—Ë‹¼ÌçcøÉæeX0‡&õƒ²êzì9TÎh›‘Y÷G2•è¡Õ Æâã ×Â~â€+kÏ&uŠÞ,åLô-’áà¯{”mÝ.t×!ÿ¯ßò¨û¢ÒÛ;Ÿðy+ü÷·oSþœgÇ/å$¢·PÓy‘q™}ú]¬Ì¢‘Š·®Džr®sBl$ŽlòSÁ&«å–þ'äa‰nþä­OñZÝ~ÖmT´©À+ Mêì>T΋…ñ9Éh}üœÛŸß{åwxáü+^߯ÇSV`ï’—¹O¸âéψ÷ZŽzu_Kv=9í³Žt¨¹|žþÒ£ò¼•á0˜L£ÿ×¢éÀ¯Þ;å2pkF¬ Û¿Y€{Ò\ˆDvÑq6©;.†rž…¹™¬ú›ÃýÁbµAßo‚Ñd…mp7uÑÝ×®¾~tëŒößïÔ5ÊÀ™4)Þîô"þ­7ŽÀy_;2;à›@cbz0Ö™AŠ0‰pø=ºFM9F™à‹û©±Œt–`BG/¼ñ±×ö•DÜ·}¥ªÕ'â:0»6#W>ÿÆRø!ãØ¯¿K•:Mq$p$ÐŽH$À‡@è™ Q¸Á,<ñðâ plàªÍ˪êq´BÅÙ|:“ì÷íS ;ÈNEZ²]‘>ü›oÉ.FŠŽ9ptäs¶£¾G&ÑM(ÚWêÑÜ1ßç.ÉKböÒ,$gßí8xJ˧_@sþ Z?ý:mçmU²ëIŸAG ¼åZCønÌŒ”J~ç<y3Å0bnÉ××<ÖýíȾ1²_ŠëQx˜i û9&Æ\Eô\h¹ ‹uÏMð®V«ÝB=‘ »à ‚˜¹\þ1€Çùºÿá~òÔ2ÌIŠ¦ÆœæÔ6hð⛥Œ¶‰†ãÏËçºïKqWTú„Ûz°Ÿ'Ãþ9 ¶áZô¿Ï݃-i½Úf\‰VädâÅM>íoϽú‘Gÿ÷ÄÌG‚$nÔû5]*è-Ì"‹Ø8!pQ_EûJO–Ž|¸‹'¢.Ï1|´äúçV·?ëmÁÃò9(^ôÂ8‡¦hÚñÜß‚OÔ§½^þ_ü…§×.³ãè°åào5õ¨þª†3îIS ÷îTÌULÞ’0 4”êÔŽ ß&+Ìë°(‚_Bˆ‘MhÚ{ïœç#î]4í½øªYëQù12)¥ÏD˜DdwdIsäqX² Å'Çh¶Ø`¶Þɼ>0¦½Œ& l¶;¯I0ÁoìÎ vÑ‚X$„X$°?× ‹ÄÂ;ÿ#<Ãd9X2 $RªOÀ€=48¾^¡@H(bí̸$rð z£ eUõ(«®ç4à„Ìϵ±b£ ¥g/¢ô¬ ýî=÷Ó3!1ñ¹=2pl²±ï;èÖÙÚlƒÜ®Á'Åɰ.–fÍ%‘cö±Š‹œ³ ô䃃,Ò’ã‘•¡p øÞWkGœ÷#Çí·Õ‘L5w®œç:¸Ù‘(`äëˆá×cß›¨o{*>`Ò—›Ô(>pÊ«ø³rafÖ|d¬Z ÙLïÞ˶|ú.~pŠSço8•;ljAø´^áMÂ$"ÄɤÐÙc€ÙêŸùŸ¤8™ÓÇ‘ôÃó×[øÊ À‘làï}®ã™ÉÕ½-ÂÃĘ=cüx=Q‚&è7Yðó’ÿEg_?Ÿ«´V«Õ.¦žEá}Hð@ALˆ\.P ‹¯Ç0;)?ݼ áa"jÐi›¬û?Wþ&Þïò½”p9æÉR'Üö`óa·>ë ”œ´9-’¡ç ÷C~Ûø¶þ}'çû1'"{—¼ì‘sŒuÈ u¿Zc;Š/îe,ôaK˜X„Ÿ<ó0¤ï2Ô1Ao4aÛ«‡Y/rPö&ïc66«çå„„axÊWB>Îw÷õãWÿêÑØøhÞ=¸á,\¢ÍJŠA\?RºLw?m¶Ø`¶Œþ 'm*àlñ:ØE¾ ¸e“}ëé¹u#ŽÂâØ…£Þ·Yqö6³2µíøÎ¹g¾ÿ.س|¼qßKãŽÙ]vœµÝuŒ·ãBÜx'˪/x~è€ÛŸ¯h«BÁß¾ÍÙ÷/Ÿ‘ƒ-iOx4†8„êþ[°ÚpøÆqü×Õ÷¾î]eisØT'ÅÛ3ìÒ4›s°/èo]5­Å ÂÐ=Û¶§³7b‰=˜’à½Ñ›mf‹30>Ð]!J?»„3šYm›–õËïE¬,ÜœëBðŽYI1Aט±˜-6X&È*èJL1¾Ü!—åú‚±¢—ï Æ[Æ„‰EFoǧ‚Ýâ´+$ 4¯;¡ƒ•…hä§¿)Å¥föAJIq2æd¢07“‚´„‘xÛ:u¡¥%Ç;3aÚóoî—ì¿Ý¥ê«ìûcù„n?üNVå‘à„`â©Æôu‹hLðÁsݱò‹8Z¡òxn}ªlîÞ ç $€ðž8†(ç)PüìJ·Ço9!ÇgÌFæ£Ë06 T @Ìœ™‹ñÏ}­æüe”¿r:m‡Çeq™œi**U-N„7Ý7hüšžã—CQÛ A“ºsJ S±Ÿ ÔÄhR1ö<¿&`Ú€éœÒïO~Ïë®ó½{Ü«Õj/Ð(IáHð@AL‰\._ àŸa™r6ž]³$à÷Óh² Ï0€sµ-¸ÜÚ†m=ñ/š7òø(Z †mö¬?,ýÏIE Ùñ‹)Œ˜ðýÅ'Am÷eÖ“oï|‚³@.,‹ÇM4ðHôÀ¥€d2*U-(>pŠÑ6#Å5a rî÷™K=õè0u3*÷Ûç~€¶ŸÖ›Å“ÉÜT¦B;ÐŽïWÿk¿ÏûØž»\ žNÛ€-sŸ@~R.ë2Æ àlû?ð3ÕëÓâzH‹lÆ3À¾€¿ëÙUHçClV{Æm¶{–mÝ×Û10,€Ð÷›†ÿï_AÄþ#çXÝ+®ÈÉDaN¦3ë»@ÀE,¼T"ÂüÙ‰Ô‚ç˜ìÙÒÙ H‚¯Þ³‰YÐõHöý±ÿûs±µrž…¹™ä@%CCvg‡AçÁhBéÙ‹¨þª-·:1#V†{ÒøÎŠlÈãeA9ŽžÁ6¡ÀØ9ƒÍ«³iL÷m]:ì?rÎã6œHô@b‡‰û¼2CAë\à‰ØŠ©û€7æËæÏÁüÕ:…€]ì?6DRÿ^ŒMº~Tî=„ú¿œõ¸¬ý?Þèóù`‡ó˜ªQãñØÌc”Ã}-XÿÔ6h q;Á&ÌäRÔê "¤b”ìz2 Ú‚‰àá¯çñGŽÑüÀ3Z­ö –A¾ƒA„[Èåòb/óùžý§%X–5; ÷­«¯­·ºð·4àÌ—Í03Ïâìv÷Mêl{í0ãíFfÞw…0T€Ü„{! º|¿Õ Æâã°Ö÷†Õ'—ÖéÑ"*þuv~wá“èM_K KÀKßr¾~pFö¸>ÕaêÂ¥ž«ŒÊ}ëê»8rãã±¢xë*ÖÇ¿ïƒÏð—suœžgSÁ6³¿rž{¶¯aõlÁå3rPñð‡Œ¶ñDð»[æ>µ³ ‘‘âѾw˜ºÐ¨»†ÛÑF]+vœ…•è%P „¬;z£ ›_~ŸñBûTÙ ïÁVôˆÃ€ªÃ@Â!ˆp8Ølw¼)Š`*x‹°å±ûW÷¤Ë! ¥F&㉻P‰4ø®;žŠàR³?ýûA19nÁŒÅÌÎQŤáT¿3ÖýÇïX³GHÅØ¶a) €JU ö©ô(3úع;0«»¥Ê¹ÈS¦’ûƒ›°u(fº–µØA$•àž'ƬÜÑÎ"vIý_ÎàÜïìï÷¨os½È„fM'Þ>r®j褙‡È¡0'“Æ!7¢VoãK'™É´'Ñ˜Š ·ðߥU|ïïjµÚ-t†Aø!UAáZ­¶X.—/ð8_á?}„˜p,˜8“F“7ÚzðÉg—X TªZQ©jEÉñš ´»NOI@RœŒñBÄáDZqö#º8Xm¸Ð]‡ì8ד©)8øÀëX÷Ù³¬ö»ùf'JŽ×`óêlÎêbÛ†¥ÈÊP`÷¡rQz-:l©ú.6?’tsùLžr.” ì?RÉ*ˆ°Ïù¿s {¶¯Þhâdž>Xh¾Ùi_39QãtX:œì‹pŠÅ|¿¿Åi÷#ó±ljQì™.C|Æ”þë¯X‹šovâh¹ÊçÉm]:¼w¼†õx,DHÅÈSÎÅúüE$r ÆÁ‡”JUk@ÜÉs£½¿?õß»E-‰‚ ü ‚ &lP ‹¯°÷UøÉSË0')Úïûb4YðÙ—ÍøÃ©/ éèã¬Ü¶.JNÔ ¬º>èì¯ s2Qr¢†Ñ6k?NÞúgMh¯·ôãJ_îŠJwùþÚ”BlÏü.öÕÿŽÕ~—œ¨qÚ6sEžr.Jv)PüÎ)V꣞ػ/£øâ^/ÚáÕö[»¿Ï݃gªŠ<*§èÍR¯gNWÎS0®× ÝuÎàöKß8Á`=híŒêŒ)£Mê·ë§­K‡Ý%åõ£«}Íž_€ÒžÀkuûow´\…m–2Úfw ·‹Ñ"ÖÎ*Ä☻±8v!Ç.tÙþ®h5¨ÑªW*nÛ³T´}ŽKj»/;?—»;2¿ç‘°„)¹ ÷r"vд£Q×êRìóÖÕwѤ¿æQùé‘sð‹¬"ÈÃñýê{\Wøs’\o4áh…ŠÑ6$v BC±€ØþzÐfôÿ¹ÐPruà;‚ÐÐ;çœÔýó{ìu$9wÏÁWÍÚ)ËQ$Dá¹ K $î|§‰1]®9a€Å4þ:ãòó!€HâÞâötbhÈ^G\±ý›ˆŠñ§³ã]Iè@£±Z¨ˆÀ‚é|¡rž/n. q=‰”Jðâ¦,U¦²+¨5(«®G“ºƒ‘Ëq‡¶.NW×ãtu½3™ÄSÏL…?Å"©÷ÿËF$ÌŸ=þy"@ÅæÏÆšÿþ‰G¢‡’5Xš5×'ã¾Þh±ò‹Œ×Lƒ´äx¬/P"O™JóÞÁ=1„+úMü÷'UÎ$?<¥@>µ4A„¢Z ‚ ÜfØå¡@4_avR4~ºyÂÃü—îÖl±áH¹ 8uÞ#Ww¦…“¶.6¿ü>ãí’ÂðÁÒ·¦üÜ]Ñé‡%º|¯Ç܇ü¿}kT2£}ˆ“aÿÎ^™P;Z®ÂÛG+=*#Z$CëãçÜÒö„ƒÍ‡==DHÅx{ç^ë÷ûœÃ±Š‹Œ¶ù·ùO9…5 ’XÜ3~QAo5 ¦“Y¹l¸Ýµ6-«®Çþ#ç8 þ/Zð,U|õöa ¾}âd(Ùõ¤OÏO§mÀ–¹O ?)7àÇO6çÝ÷½„w}—õwZ‡¬¸ÒÛäÒÕ°‹„^8ÿŠGǵrærì\¸Íùú­«ï2vEñ&»Ÿ_éØÍ]JŽ×0Z슊Q²ëI¯QCCö}›Ínm<ä*H_…¨l Cƒö¿ïô!áçí+4Ë•ÀÉl±Áìf eAh(+¡AÌž}µvwCƒÑ„&µë º¤x™ó™H>âo¦Ø¬Ã×oÛhLJدÝ݉(1 ܹvq‰ÁhÂÅf Ú»uÈLM@Ö|u|‚{þÝd±ENuL0ÃÝ9¥©›Éˆ,¼„{÷]/¿s’•háî4¹[‚s‚$~`öŒ?ê³ ÅMêl{í0'û)WfàÞ§›PП1Yà_|;®^÷Hô°"'/n*ðê>VªZ°ÿH%Úºt~«§ð01Ò£“nÍUÄ#"L‚ÛÝ:Üî½o—š5>Ý¿±‘xqó×ý27OðÂ;à÷Q9O=Û׾ج®]0ûMìùã¨Û{ùÞ%îÕjµèÌ ‚ðäð@A0B«Õ^Ëå[ãë1\oëÅ/KÎà—[¿î·}øàô—8䣬ªF ž{õ£ XDIŠ“!O™Ê8kvÛ@6ž23ú•Þ&D Ã)Œ÷^Œ8 s_Gþ_¿…^ óIĶ.ö*GñÖUœ×Ëú%²2(>pŠõg¯E‡½õ¿óºËg;x"z0Íxù“^ËFÎ&P¨QwG” ·ºž F L Á€Íý¬L&ÞÏXð jÔL9ì>TΩÍñé[g=<Ĉ£P¼èÆý¢­Kç¶£…Þhâ$ãÑÓiP¼èNÜ|E«AÍx6#wÎ.õ\´¯¿Z÷ßÓX±ƒã $äñþ#Ö2´º/ÞºÊã±Ôbl–ÑA’c´ÿX-öÀI‘˜ݧ3CCö¶´M¨2,†QŸà‚âgWbÏ¡r—÷ÌyÊTm*w΋EˆEª<‚ð1Mê4Ýì´ÿVw¢­KçqÐJ„TŒ¬ Öå+ÝîƒWÐ06«wÄöv’ ÷n{Ÿ4dáúùÁ‹bºå$ذ¾@‰²êúIãÓ’ã±ëÙUäêÀ#’âdس} ö©dˆÌG—Mø~Ì/Ä€ÝéaÕ¯·£ô_ÿ/«íOW×{͵ ·n¦Ï–éÉ ÈÊP8Åöé)vQƒÉ8ù<ðD8ÄÍšÜlïAÅù¯$”ÇG‘Øp6±þØÇ@A B,ã^}X®šb‡gHì@á_h™‚ ‚`ŒV«ýX.—¿à ¾Ãõ¶^¼Súž]³Äçß}¾^í3±ƒƒÑŒ·VBÕ¨qÀ4(ÌÉd5épøÆqlœýÈ”Á°ºë›p/„!ão£Ç.Dñ¢Xg$¯Tµ¢RÕ╉ùô”ìß¹EûJY[hï½ò?><vÑC«A]÷².£ùf'Šö•zEôàNðüXuwúå€Í„› a‚ñû• ‰…ºßýÌ_‹câÝæ}m"ôFŠß9Åù¤¸v Ý£í$±X›Rˆ"cQQ¥ªÕ­6;V~Ñ#7‹äp9=°—Ž~½hG£®ÖAÛ„Ÿ9Ø|m¬¿Ã•Ø!ÐHŠ“ù-ÀÉù½.‘G‹?CC€y€y0’Í ‘Ä.~ ¦CCëûÛ ƒVÀjµ/˜ˆHøà‘R Š·®B“ºcÔ=Až2•Õ=AÜQÛ ªAcÿí¥ ƒÑ<üìÙŠuù‹°mÃRªx–X-¾ù›Õ~í#bÄý¡Í»å‡Ò3Á’=Û×L8¯F×]~?C½¸©‘R1#×_.\sÌÍÁ?s–=‹Ù³¦N®òàcþñÃí£^^¿¡ÆuµÚ)Œ¸~Cë7ÔP}U‡>Žwm3VüP˜“‰ÂœÌ z¶-ÌÉÄÑrÕ¤ë>+r2±yu¶Ûeê&½YêqI%¸÷©Ç Ïš?ágd3Í«:WÜ·y;žDåÞ÷YmÿÞñÎ]¸t랈´äxde(•¡@zJ¤sÛâ0»SfÄÊ0#Ö^îN×xEì@ŒÆáÊìHFäê¾84Ô?sôMêè‡ûtzJ¼ßâÖå+Zð!£073 öI$íòð§Ï/ãóºë|?]ÞÕjµiÔ ‚ð/$x ‚ X¡Õj÷ÊåòÅžæë1œQ]Çly4V}mžÏ¾So4a×S~;æJU+¶½zÅ[WNÛ ß<å\$ÅÉg½4Xûq°ù0~0ò.m´áRÏÕ 3›ï¸ë»¨¸ý9>QŸfµÿ»•£d—Â+“6‘R ÞÞùëÌý½>V—amJ¡OÚ²xÑ´nàÝæ#¬Ëh¾Ùéç 6Á¿c]ôVƒKÁƒ\šÈXðÀ†ÚËãð†ØºÍžeíHÄÖÎ*dÜ'j4ØìÆçʹþ3%B*Æžç×\²KØìý«ò«ëøÓçWø~ÊÔjµÚ-t7LáHð@Ax‹dñõÞ/»ˆ‰˲fûäû~õû¿Âhòo6ж.ŠÞ,EñÖUÓÖ®sóêlV“GnœÀª™Ë1O–:éçzÌ}h5¨‘áz±á`îëHýd)ã,ô€}R¾øSس}×êçÅM0 Ö0åcõ)Ÿ u À#ÑC¥ª»•sž1'-9ž±[Æ…î:§@Aoíwñ$R06“ÛåfÅ.Dmw£}Q¹<ì>ä=»có ºÖ)Ï/WĈ£œ¯MYɸ?¸sL•ªÆB)ŽûpAÝL•¾&hS;}lf/vÈŠ]8¡ØA*°ÏÊ+r2±¾@é·ïOO‰wë¼XŸ¯ôh¢Ülâ&ÐbB¥”ÑŸïØ¬£³:y‚S #¶/žAðÚ ʪëý"rËþ#•$x`y]ó%ƒ$x Ÿrx ¸xî&qñt¥0ÇžAyªõ‡»h‘)Q2þð»ßàÁ|ï …Ƚ#†v‡p!T_ÕáâW—¡úª—ê.t;µuéœâ‡´äxædNká‘+'G¶b«=‡ÊY»„;ûqÊ ,}áIˆ¤Ï—‹¤aˆJ™Áëz/øÙ³xÝaÖ÷3Þ–+—½Ñ„ýGÎqz\Êy æf"O™êÑܰ@„„ÙçvÝ#Þ÷Çrüïõ^o;ÎÍû›¡!»[¡•á3íìÛÙ,€ˆÃ9Y½ÑUƒçT­SΑœ®®G“º{¶û>À¿0'Y -W¡IÝm™§LEanfÀ‰œã¼¸~£V¨ø~Ú\OwÁA- A¬Ñjµ=r¹|-€ ¢ùz‡ÊT˜-Æœ$ïBmƒ5—oÄ1Œf¼øf)Š68'è§…9™(9^Ã*xù­†÷°÷¾—¦ü\«^QÔ¨@l1â(|üÐüíÛ¬ö_Õ¨ÁÑr•W'ÜŠ6`Û«‡×ÑÇ7ʯµÌ}ºëPÛÍ~åtu="¥bNmãÓSOü7ê[‚‡sáús ’XF.&f3<œSµŒÊÜ~´\ÅÊùƒÙñ_c%xHÄ:ÿ^›Rˆh‘Œ± h"G‹;õÑÊ®DÎqØ÷Xúèæ`Ü;\è®Ã©[Ÿ²úޤ°üBù£Ñ¼¡Ĉ¢yX"þOíÆå>ºt!â¢ÂGýOo4M:©­7š\ŽiÉñX_ ôûµwóêl”U×Oºx'úö.6«k l6 °˜íYÁ~Â¥Øa$f3 ‰ÿ18hÏT64 92Þ…Úƒ›{FCÂ=\][U vq^R¼Œq6É@=Ʋªz«¸ÈZhë ÚºthëÒQÖi¦çÿ¿oêœ FâMA‚HBbk‚ &g*ÑC˜Xˆ3su䬔dœ+û ¢£¢êxï!F/ œý¼Ê)‚8óyUÀ:A4ßìÄÛG+ñöÑJçÜœ§ÜЧb«’ã5¬eıC¨@€øù³ÊsE¯DŽ¥/<‰òŸ`¼íéêzN2Õ—UÕs"¢ŠQ8œ¤‡ËgÃÐP@",Ãò“=Fq!vˆ„‹ÐõOœP¬hSAÐ>ÿzêÈ '¢1ÂA{@;Ûù‘ÊaÓ1§ùf'Ž•_ôÈ™š-Iq2N×™§;½ýØýá¿'õ^kµZmµ(AD`@KÂA„GhµÚV¹\žàK¾C¿É‚_½woüûJ„‡‰¼ö=%ÇkîØòÓQôÀÖå¡¶»‡oÇÆY«§üì¥Þzä&Ü aÈø[ªü¤\lÏü.öÕÿŽ]9QãÕŒC‘R ¶mÈCñSÌžê-ºIÝ-¼EÅ7>Dþß¾å‘èáXÅE¤§$pÖßÓ“ãqšé˜9"à»Çº õ>Í—Wo[VUïqâ3OŸ1“âdؼ:Ûë" ‘Èþc³ÏóÚF‹þpºÆc±Cž2E› )• ¶Aƒ’ã5£\Žó”©X—¯œthZ?Ãr v‰Õ ÄÞ®îΜ«mAYu½Ç.2G+T~<îÓ?`ÁU¡ŸßbØ¡Õj/P‹A$x ‚ ñ®nDg“)©QmÔësŸK—Ha ØLn•;O–Ša8 VfÖÉ•ªVædbwI¹O‚¯.ôÔ±Ú.R8Ú cmÊJÆ‚‡¦›¾çÈjË”ÌzܾiÚ}~~pÁdœ‰ÈOšÚîE;Ðî¶Ø¡Q×ÊØ©ÄÁÓs7"7á^$„Ù…®„h«Ë—ëËÀ¿Ú šÔhëÒ¡IÝ9i¿LKŽGzJ–*SY/ʦ§$ dד(«ª‡ªQ}¿yøÚ๠òààAÀbµb1ža1Ož½ÍS ’0ªkÂû g%´ºÿy³É8)OïlÑm]:h;uP5h íÒ¡­S‡¦›œÞc«¸½ÑŒ7ð¢NôFŽ•_ô‰°Øóg›x:Á ‚àB·.bB!û,µA'Y —³EûJ—õü‡0h³¡¹õÒRçð²>¢£¢ðتB<¶ªøávvˆ3ŸW¤Âž]¼Õ™Ù¾0'Ó#w¾?Ïíf‘@l$îŠd3!‘…O«úËÞº¥ÿúY=ãzÓé}2B_¯ „wœZGºeþ鬊u™R1^ÜT0jž<+C,/®¯ò«…{±°ÕB'v_këÒáh¹]È¥Ó¥Áh&§Êçªp½­—O«Õ¤Ö$‚,Hð@Ap‚V«=8ìôð4_áz[/~Sú^øf.çe—U{–•"Bî ¾×o£m ƒÓý›®¢¶.k?^­Ûßæ¼6åg{Ì}P÷ßBJøL—ïÌ}÷žXÍjÿU-WyuÂ3O™ŠcmÓjPû¥=cÄQøø¡Èÿë·Ðka?1¶ÿÈ9¤ {›,0cƒ¹õVƒKÁ`wyhÕ»_׋câ\;³`ª’ã5Ð÷›VåhwWÇÊ”&õÄc´ÌûS„0«f.÷ûx wplè­\émrûó‡oœ`õ=Éár¼ýµ_Né¬ñ±úã²½üçÈh¤jÔ°²mn¾Ù‰ÓÕõˆŠ±>_É*£Q¤T‚õJί3î3eÐF÷Ü|ÃfõMFêÁAûw hè%¼ ÛLx604H¤Ó£šÔhºÙiÿ­îä\Ø0§«ëQ˜“ð›Ô:)iÉñ^ÍäIá-B ÄÂØZ,¦{I‚ ¸ƒÍ\ëÚGhnÝ‚ÕjÅüyéÓ¢.œ.È?Ÿ,ÃÙÏ«qæó*\ª»ûh0šq¬â"ŽU\DZr<Ö(½ží>Ðxù“=»¸+vGF@6sú‰J÷-€âÞ»»<Øßtx´V•Ï,àÛ“¹d® Böçgûþ7ÒÕ˜˜AÛ°#ƒ0›0ó]•ªì>T΋y‚[Þ)ýW®uðý0>Ñjµ;¨5 ‚ š¾#‚ 8C«Õn‘Ëå©–óõÎ_½…wJ¿À³k–pZn¥ª…ÕvIa øÁü§ñ`âý£ëz gÛÿÃ×s&~˜Ž¢‡ÂœL-W±²ÆlÒ_Ã[WßÅæO­áiÔ]CŒ8j\¦wÀ˜ýÆ}/á…ó¯°:†’5Xš5×kY*ØL¤úKðà¨ÏЇ?ôHô`0šQôf)ö<¿ÆcÑCZr<ãþÕ¨ku ˜zÌ}ŠeäaÌ&ÞÏXðÐÖ¥ÃÛG+}Ö~z‹ñ6 ¢E2F}`²ì1Mjæã¨+±`w~Q÷kyéòÀ%Ö!+.õ\uÿb §n}Êê»=°wJ±T´U1.[9Û`J½Ñ„JU+ëkÓDcZɉœSµ`׳«"«‘·܇†ì?Ó9Cú´ ¬¾ý. R#¼‰ÅìY&¼Á!{|Ë­7šÐ¤î„ªAcw#ò¡¸a²çë@<8Å›Eým–Ò Î‚PoD}ALŽX˜<,ƒÄAø™‰ £\n·ÛÝR§‹èa$N×o¨qæó*üåÔiœ©¬BŸNç÷ýk¾Ù‰=‡Ê!#O9ëóM{ׇýGÎy4O)‹vKì* .=yÚÖ#[—‡²êzló å)S!»õü¹"'Û6äœ8@ÏòÙù¹õy~sÈà‹÷ÊŸˆÆáã­y‘©˜Ü”“oÄÕu¾F-€-ÔšAI(UAÁ1k\ãóœQ]Ç™ZîÄj4¬èÓ#çà·9¯;ö@è³VュoáÇ ·!BÈýëžCå¬O‚7ŽÜ8“nà^ê¹ ë먾w}Ëgä°ÚƒÑŒÝ%å^«6í#Šòk›.Ž]ˆƒ¼îQ£»•Co4yTŽ<žù„Z£þÎÙcé›ðsaÉ„îÕ‹¯a:ö4é™_"'ø6Ç[Û ™°?°é‡ѪWCo5€Op-dR÷k1`sÿü:|ý8«ïY>#ùIS;3}¬.c%’Zš5—“úpLðo~ù}ì9TΙØa$Í7;ñÜ«Äu|hŸe·ÕoA¹ÉîKõg.`’à“ &8Cí¢6§HHì@÷L4ÿ8#Ån··ãjcÓ´®«Ù³Rðä77âÿûŸßàÆåZ¼ÿÛ·±í{Ï`VŠÿƒâ F3NW×cÛk‡ñÜ«yìâÈý•©øHDR ¾ö܆)Å3g&B‚iÛŸ÷-@|ÆlÆÛ1uþK¤T‚7Lú™¤8v?¿/¨S—å¤8öÿx#‰ÜdpÐûs´cï¯ñêüQžr.5lr¦ö:Þ/»È÷Ãè°E«ÕöP‹A&4GApŠV«í‘ËåkTˆæëq¼ó§/&Â’Ì™—¥b1Á ;nsé0–U3—ãÁÄl¼Z·ŸqvwWp•ù>PÈÊP`EN&N³œ~ë께9Ç™‘"l&´êÕ~îà¯cññGXܪ58Z®â|ò®IÝÁj¢ÞõcY›RˆßçîÁ3UE¬Ëh¾Ù‰¢}¥x{ç¬ËHON`·Ê•‡%"=r+QþmþS8©ùÔëß7QÝ,޽ŸÞ®ö[œj|¾Òׄű ! áÇ#×5ÃMfckì‚IÇCuÿ-Fådéîà®ø‰»CZr¼ÇY‚Úºtxïx ëkS F3ŠœÂþýºxE1ç`ÏìåïЍî îá2žÅb® $šÔ¨Tµ¢¶AU£†ÜCØ:<º‹rž‘áb¤'ÛŸÙ•SsÚ.Ú:íÏ£é)ñX_ ¤ŒˆêãX­PJ]E"!a€Åä¾Sh ’йED`–šêòÿÓÙéÁ÷‡Wwý ¿ªÃû<‚?Ÿ*à õM¿î—Ãõaÿ‘sXŸ¯Danæ´¸ÖMNçw¶ä½ð$¢S’¦ü\X´ a1ÓÿÙCùí•(ÿùFÛ´uéФîðhM6O9»Ÿ_ƒ’ã5£žé#¤b¬ÏWb]Á¢€:8ˆ”J œ§pk>B9OâgWôñ¾˜£u$¾qÜ[{s^$-9Û6äQÃ×Úzq¨L5e­V«½@-J¸à ‚à­V{A.—opŒÏÇñNéøÉSË0'É3Ý›Œ>+g.Ÿ2À~$‘ÂüBY„“·>Å[Wß…ÁÚÏz™ï÷l_3m&Œ¶mÈC¥ª…U6 ƒµ;ο‚ßæ¼yX⤟U÷k#ŽB‚$nÜ{©)Ø»äeÖú%'j°4k.gé%ÇkPr‚@†‰ë€7Ù’¶=æ>¼pþÖe4ßìÄîCåSfÀ™e†8Ál›Æ1sßÄ‚‡°D´êÕngÊß8{5^«ÛïõºÿñÂmX5s9þëê{Œ¶K c>i&pÖU Ÿe°Õ[úq©çj@„¼ÁdN/ÚvXmn—uòÖ§¬®[O§m@jDŠ[Ÿ­¸ý9ãò—z˜%¨äx ŽV¨|ž »­K‡ýG*YkÁƒƒÁñDôg+Çe€à¡RÕ‚sªVÖÏIþ„iöG_£ïç®>Ó’ã‘•¡@V†é) ¬žÉÁÁ;„„Ø|åÚ2“á'„†©= Ëjø¾04Ä.%W‚  ƒab§X‡è!-5BaðÜ,º{!^ݵ0 Ä£%'ìë*+r2±>¯‡í9Tî‘3ݽO=ê–Ø!T @LêÌ è·©-p€ñvµ ûRV†YÛ× ­Kí°ÐOÏ‚Û6ä¡èÍÒIç'VädÒœ7 |5_:h»#xðÖm–"O9—Ü¡¦Cà{G ‚à+! ¤€D2ú'LjEØ o"gq/ØÜ:¹ƒîíöv\¬«ƒÁДuj?ü —ªÎàì©?cÛ÷žÁ¬”d¿îÓéêzl{í0Šö•²Jzæo*U-Œ«G’Vp?fåº÷\3g&BƒD½+‘…#óÑoÇ¥ÓbRœÌ)šçé) Øóüš Ÿ{Ÿ[ŸGb–øLð08znƒ+”óxn}Jv=‰â­«Hì`ôXðÆGUÓAìð®V«=H-Jøà ‚ðZ­v/€wyýf²à—%ž‰˜ "„áŒÜÆ2O–Šß漆ôÈ9»ªQƒÝ‡Ê§MÌSÎe¼ MúkØñÅ+S9[m¸Ò;q€óÁÜ×-’±n“£åìí KŽ×`Ûk‡Ñ|³“ulÞBo5à‡w}+g.÷¨œ’5(«®g¼]¤TÂ8ðÈ`í‡v ÝùºÇ2¹à!%\¡›Ñ-‘ÂlœµÚkõ! ÇÎ…ÛgÛ™‹D"Š1„!ÜF!h=ÈX5– Ýuî}§±=àEîËH&rØè1÷1rwд㋾´|FŽûîmUÌûºTÌ*«VÉñ¼øf©GÙѸâhÅE¿}wH(?Ë&¸ep(8¾“‚¾<È2'½g6šðÜ«aÏ¡r‚Z¢Mø¿”Ås§#Ká±_/n*@aN&-âóÐ7BOÈî&Aƒç†»PhäGÄtdhÈ.¦µZì?ƒ6ß¹®as¯z±î2ÚÚÛ'ýŒÁ`À—*®66aÀd Úú)~xÿ·o㟟؀(™ÿžT¼øf)¯„z£É£µ¿¨”¸ç ÷’P…EË\ÂmÅ} oÃ÷çt®HOI@É®'Q´©›ÉÆæG²îžø ïüñ]y¸gGHÅÎ9’£¿~{¶¯Áú%%PÞø¨ ×Ûzù~µZ­v µ&A? P‚ ÂÛìPËç¸ÞÖ‹7>ªbµ­ÞÈ|âÙ±ƒƒHaö.yÉcÑÃéêzVAàJѦ²J¸+zè0u£ÃÔåò½q>ð:ë}(9QÃ8Vo4¡h_©G®Ö¦¬ ¨6í0uv.Üæqß䫌èl&Ù´Æ; XÖAÛ¤}J"DJ¸û–Ïg?â5—‡ ·!R8y«‚ùøÉl|›(¨R#S[§ësGÉ"ÛW­±—zê'tñ7S¹Œ¸bqìݮ˲ôy­GÝ\Üõ=·?ËFÐÁ4—ã,W¨ü¸È꥙† R"Â÷ ñ¤Ì©®Qžˆž¤8Š·®DaNfÀïkžr.”󦾗ˆŠ±ù‘lg–B>1þÞD$öþw)=A1›0ÚL&Àb±ÿ˜Lwþo³R=ùóÞ•)ïüþ=·n··£æü—A/|€ÇVbÿ»qãr­Süà/ø$|Øs¨œµÓŸH*ÁÒžt볡¢g%]¿L}h «íøèâ- s2±yu66¯Î¦@wñ—îúüEŒ¯›+r2Q¼u%Žýú»Î9JؼSú®\ëàûa\O­IÁhªœ ‚ð*Z­¶G.—çhÍ×ã¸r­ï”~g×0›¨jR3*‘‡%r²ÏÑÃÎÿWuͬËÙs¨ÜiÈÔ6hФî@[—nÒzÏÊPx”-Å!zØ»ä%gà·Ë>Óׄ܄(—êצâñ”øD}šñ÷Œfì?rÅ[W¹Ù;Pôf)ë ì‘̉HÆ–´Ûö.y ;¾xMúk¬¶7Í(z³%»žd4‰–•¡`l9|¡»‹c:_÷˜û&íO)árhí°™Ü:÷·¤mÄ]}Óú]š˜ïœ¼õ)Ú˜ObïË~ ;/¼†ÚîˬË)>p{ž_Pz£ •ªVTªZ|n÷ê=ì\¸mÂ`gë  Wz›pOŒë,s_Gê'KÑkaê8î©,A¹;ÀÞ%/ô9)ŒÀ/²ŠðýêÃ`ígU†ÁhFѾRìÙ¾Æíà£ô”xæãâÀh‹òsߤ.Â!æÉæàRÏU·Êß8k5.t×á\;7ÙæÓ#ç`çÂmÎ×'o}ʪ¦‚‡H/9UŒ%)N†©˜ñ¹røÆ‰Qõ2å¸eéGUÇ—X»pR‹¯aã€ÀÉýÁ@;+Ò–´'ßWÌû¼›×»²êúI'íýMmƒÆ/ e¡¡öÀ<®-³4ƒAÁêÙÁ_8 1CzJ<"¤¤§ÄOû@ÿH©{¶¯AYu=jÿöÞ=<ª2Ï÷ýÖ½’JtÎÓ” IDATUîI ’p«˜‹%( ¡[Q´Ÿ–Vl{E£Óífœ1¶8Íì½[ѱ{ÚŒØgúààžî Ø{{i{·Üt'ÈEÒÍ¥)D’@+TÈ¥RUIÝÏ+©TÕZµêšßçyò@Rë}×z/ë­uù}ßó½è»6‚"]&Êçç¡l~  ™ðŒ>åS/¨A^ÜnÀ>ÆÍ-ÌíaËÈ•ôIÊŠù%]ºjìÇ>܃ú£ÇðlÍ“HRͨ_´Kî¸}Æ R5<þ£µxüGk1l2á½>®ÿõ! ßDÖÉüpã9œh½€‡«Ë°îʘèó¨ [v×ó._¸òd-˜Üul’ªœŒ;ón_Ì]ð箌DŒEñüª% Q>?{ë[oHTxWYî.ŸGÎqÌÑ–‹xïP["4¥†a˜fQ‚ ˆø‚‚ ˆˆÀ0L³V«­ðûxnÇ{‡Ú RÈñò¹qsÌJ±Ÿ}÷=Üóç-z°ŒÚ±yw=§ ðpÑ70‚w?mÂáÆsQ=Žs7j¿z Ûn{yZÑC¿mý¶d)¦>ØM“kP·t+~øÅ³¼ö¿yw=v½:} LËy6“P<¤»—SFõˆÍoÉí×*³±í¶—QûÕk¼E—¯aûžx鉕AmÏç¡ÜÁƒ#pVü,E²éè· µúõ!9^xQI“±Q¿~"@¿}¤ -<äïÎæþb'BªLÍI4ÔqyzWŠ»Êæq^W^9‚šÂµœ„jN· ̓« ¸…B0óïfªsªBÞ/_¡E$œf‚ lßs7$ÄCݰ “±Ù$…B"¡àŒxCn8Bí“ „F"œa¨3Ñ(+ÎC‘.¹jé²f„¨!V-YˆUKÒ‰”èßy"6Ô.èAVì@bO‚ b2¿{,ÏxYEõa¤¸»|ÞÞ{‚wù+L^ýõfAŽåýb¤i4¿/»ëú3½Rýb¤¥²Ÿ•–è‘:i»D U£Áßýä<³îq4ž:ÿ|w7¾<Õ„«Æþˆìß2jÇ®ýM8ÔxžXõìý×·ñv„ÕèrpË£÷pØ>wF¯y·-â\¦£§!4Ñ~žž›¡ÆúGî¦H ºû†±ãO§¡)/0 ³F” "þ GæADÄ`¦N«Õx%žÛ±ãO§1W›ŠüÜø1«Š¤høÞû¼6|ÓŽƒØòü¢Òó¨ Û÷œˆºÐa2§?ýËFü\¿÷ÏZás›ö‘n¤É5>·×èVá!ݽøcwke˨»>mòù ¨£§›Þ9 X;ËÓ£®jkLÎí4ÙÔ—0ÅêlÔ¯Ç/ZùóáÆs(š‰‡W–Ü–óÉÍ‚§Û³Ó0ëÿ¢Ô"4]kØËp)R¶ÝþrH¢•4yЍç£KûyÕ5Ý9 éz¹ÚÈé¼™vžÏÏãµ¾¼Ööô©óÑnîFûHסM®2 Ú¤T¤éQ‘Îþx`ÌË´‚©™À1ã)^ëQJÕã6Ú°iÇA´¶÷†}_ùªÙ(PéÐeéA·å2çòÚÌèeI’H±“Í@*Þ ?"¾‰;òû$ˆx˜Wñ>W g³NEº,ÍÎŒ)'@‚ˆÚZ1.zpØ—‹=b SГ ‚¸—34QÛÃÖAbºÈ`¶Úx¹É†ƒ¿nL‚uì¤ÿg©ê”,^¸b±¥%%ÈÌHÇ\ÝläÏÑa®N‡¹st1ÙçÃ&6¡‹Åb…ÓéİɧˋÅHÄxöé'ñìÓOâË¿4᳆#8y*2›}#xé7Ÿà‡Õ¥X÷@eT„á}#صŸ¿ô­O~?èm•©j(ÔÉ3z È»m1ç2äð@„ñ^Õ¬4b õu¢ÓÝ7Œ_½{4š²“a˜m4¢Añ =Ö ‚ " Ã0›ÆEOÅs;~õîQü×'¿ÑÃ͙߅`ÈaBJ‡†{ÞGõgñ=´¶÷bóîú 3ß EËù^lzç@L¼ ðņí|t¹lè2÷LÔ\Wµ•·åã†6ÜU6ï†Ì@}#Øð›Oë«òôÅhøÞûH“Çfv'¥DáÓõ`Yöø¹~ýÄØðáí½'&¹Q8;“óé›ý¶Á€‚©HŠ[Ò yЧ;pWôðkÃv76q{=^/{ñ†c2;-¼‚Ôs•YX–}çr)²È½¹«¬€W¹³¦vœ5µOûyßX?úÆúÑ2hÀÎ ¬ˆäþY+°vîã}jÅ-i ºY„“†¾/9—™nM¸ÙuÅ|jæ=öþðçÚr¢õ6ï®Û÷ÑCº{Q³ézTç²÷†ì&Tþ/ÁC´m¡å a2Ëì‹"¾K„¼pÝ'AHÄ 5ŸÅâÈ®iEºÌEzeÅy(ŸŸ‡²ùyQÏJJ±¾^ȬàÁaçT" ‘±.YAq3‡0uà!ütôô ú|>ÒŒ˜ÍøËé¯`ZA@žV‹¼YZÖ!"-úE ¡Q«qOõ H¥Âܘ[,V8]Îw²¿›­V¸œÎ \Xzg%–ÞY ‹ÅŠÏŽ`ߟ÷GÄõáã†6œhí¦ŸÞqÑøæ]õ¼ËÞ²ö¤rplH“K €ÌùsqíüENeZÎ÷Ò=7!8b +z û~H´ŸÐXÇØöÁIXmŽxoJ Ã054¢Añ =Ö ‚ ¢A-€ åq{Sgs`Ç'§ñßÖ}ÉÊéßFóɬÁƒ7|EºuK·â‡_<Ë»..™ï…àPã9lÙ]ósâ߿݉eÙ•>Ö{¬ ²>„Óäl»ý<}r¯ýnÙ]]¯>>ñû+;̱ƒ]ò¬)‚€ 4pðÊÞuoøÍ'Øõêã³.i3Õœí#ݨH×Oü>d7ªÀåR¤*T¤ë9‰^/Û€WŽ ®óCôùy“«ÌBMá£><ÇŒMS ‚áþYÕünV"(HIRàÞ% Ãî"cqZ±çÒ~ì¹´÷ÍZšÂµsÙpKÚ‚€‚—XbòܽyMcF5xÍ¥5sV…½m¾¬ÝÃé2Tž¾µ ÿkt«|®·µ_½Š–Á³œë-+ŽþË1o¦c¾¢Ø A bO$bÀé}Ò\!„T ØíÂÕI^Y†Cç8Ý#xnWü÷‘$‰>¸\€Ûå[ü {#‘P*Á·Àøü‰I(L‰zž ‘™Ùãa뢀Äðq¨ñ¶ï9·b‡`éeô2 š¾nžòYNvr³³QXÜöß‚¨Tlb›Tc6l6[ÔÛ¡R%ã¡Wã¡W£íŒ‡¾Àç _„uŸ}#XÿÆGØðÄJ¬Z²0"íl9ßË[Ÿ9. ¿|"#õ¬lHä¤à€¬Ü–Qu–ûÓp D ç²‰ŒuÌ_î:Šþak¼7¥@5(AD|CÑ ‚ ˆˆÃ0ÌV«­Ð 5^Ûq±o¿ÜuÔ¯èOfe³Ã"ø±z°F· oÞö2^øê5Þõ½½÷´™jÜU6/¬}/b€ b>flò$íæ.Tfø‰Ô®E]ç‡8rµ‘ó~ûF°ëÓ&¬{ Û÷Ìöö©ÂG°í¶Wb^ì°Öir +¸‰úõ0;-œÝ &ÆuÔŽM;bËó?ð»]Ñì,œhíâTw»¹kªà!H¸ŠVrÿ¬héÂ1cÚÍ]0œ§H“QœR€eÙ•Óº‘àåîk箎‹óøáêÒ° &sðʼrÌYŸ?†ÊŒ2d)2"Þî.K`ue)Ò!KÎK>îåé‹Q ÒE¤O5žÃª% aµáãú6ìmhüeõŠœ%ØTú„“ƒÏ/bgç^õóu-‘P$±™$]މX¬€H¤l†c ØŠ_Ä’ÈY¦{çîáB"ÄÎÐ]ÄâÈ5çf¨±é§÷ûuÌS%Éo8DÛ!ˆ fíÜô1ù™Ó‰Pp9ç4ßK"û]#•Ò#ˆD!ÈGA×E÷Má¡å|oܼË'Wý¸jìG›áÆäªääqñÃu!Di‰>fŽ»´DÒ=þïš'±ïÏûñYѰº>lÙ]–ó½qSe^Þòè=ÁßïJ$På¤Ób0ŽzV6ç2=×ÂþÞ•˜™÷¥á~F+¦ÈÄf÷¡V\ìŽ÷f XÃ0Ì(AD|C—ADT˜$zh@œ‹vjų?¸]°:;ÌÝ‚çÍÔµ‹žAóÐÞA”°yw=¶ü£:lÖ»ñ$vðÒ?oo|ï~Ú„Öó½èAáìLé²pwYTD„ ×*"T\.Àa÷¬äñ°×_N •±×áAÄ7ž­‹¸NßÀ6½s€:«m†³S„9ÙY.e%‹‘““Üì쨧J•ŒÇôÿÑ#ø¬á |VdÊ1 …7ñN8E{ë[y?ß\øà2¤êrƒï»œ ˆ)Åûy·-â\† óh‚Ø{»=¼õ‰ÉŽONãhëÅDhJ Ã0Í4¢Añ½&‚ ¢Ã0ÍZ­¶Àïã¹Þ›¼éD|‚gÛGºx$ûcÌeƒRr=¦®j+š ¼ƒ)-£vlzç ¶o\‹”$a#wúF°}Ïñø›ÓcF¿ŸwYz MʆT4õ¬@¥Ã¦Òx9oXFíø×ºÏB>þ‡t÷¢®jk\¸:ÜLš\ƒY²ÏÀÿ© ¯—oÀO‹“ŸÝæ®ýM(›Ÿ‡òùy>?ç“·ÝÜ5åoCv§ ÿJ‰•eè²ô ËÜÖ>æ“‘ÖÎYÖqçJGO¿_¡Öº*ñÒo>‰øîëGíW¯á9óS¨)\‹Eš¢˜=ß¹,è’gÁì´‚5 :ŸªsªxoEz þØs˜×w‘Ð<¿ðl*­õ;w·}ó;lj{“—ÎË]e1™Û›m–ÓgR)7gÞsk|_î5L®ìcÜç´lÙhйr3ÔÉJA„‡uuà‚Ó¸Ñÿ"‚Ht^Ùq@pgЙ‚×âä©ÓøÃ‡ìß¼n¥%z䣬D•*9âÇvOõrÜS½mg Ø÷çý8yê´àû§èÁ?+Òeò8'}9< ò:¶••™¥a¿Æk_s.S”’/¸xk2é%Üû=À¹R>?eÅyQ™Ã§o¶£®ó#|cê€Óã û>úNr.Ì<[¤)Â-i "»ñe¤R¢ðù}Ì:åÏÁ|…Bâ]g·Ýþò´ý×eéAõgá…¯^ IìëVWÒE&ˆD€,íe z±FD±P$\’VJ$l1=…%‚ xbç!vðâöŒ‹õ(­;ADXØõi:/_£Ž¯Ä>܃×7ÿ~Tó<ýwÿˆù¶â½ö íŒ!¢ÇSZ¢Ç/þéEüîÿ{ ß«^.xý‡ÏasÜÎ?®oãýÞèÖ'¿Ïi{>Áý‰H9<aD¦`r‰XLî‰ÊÑ–‹xïP["4eçx<A‘ Pþ;‚ "ê0 ³M«ÕVx*žÛñÞ¡6¨r|§|î /š…­]œêj4`Yö‚ŸÙi’9>M®Á¾åï ú³ÇxWv^¾†Í»ëË@Ór¾—³#F¬P‘®<ßGÐ*³§ x­«ÚŠ[÷?±c.O_Œ}Ëß ˜±=ÈR¤Ÿ¿_ð$~ûí»¼ê·ŒÚ±ew=6ýôþ)Ÿñq9i™º.Œ¹lSÜX‚%EªBEºCv˜1£ßìú\H“k KÖâ’õ ç²÷筈˹ôÒº•X÷Ê{QÛÿ†í€µsW£"]ïÓ&ºk]Içd²pzœ0;¬PJPJØöÍï8ØÁ[6U¦YDÊ:[WµÕïw„®^î]²Ð¯‹ AD‰„Í"æ “†K"å|N¡"ràv³óÚíf&#³?) ‚ ˆÐp:B¿Žr{» P(©? ".¯?c´.‚Í ¿·¡5,u¯ûñ£(Y´pÊßÅb 454j ZÏœ™¶ü±7&9i=c€i$~ƒ©}9AÌ+ÈG™~1 ç ´d1r³ÃtŸ›Ÿýýßâñ=‚÷>؃Ͼ¬îÃç ÍPcÝÂ$1éáíî -›¬sƒÞ^"—!93•¨µYaú9A„ ±˜¿s©ÏúÆŸ‰Gwß0vüét"4åÃ054¢A‰ ‚ ˆ˜€a˜šqÑCy<·cÇŸNc®6ù¹×îñÉüÞ<(|†š!» PMý{EºuK·â‡_<Ë»îÃçP4;¯, ù8[øÆ*i2–e÷0ºËÒƒ ¹ïÀ׊t=^)­Å«mÛÂ~Ìåé‹Ñð½÷ãÚÕᆠ[‘YŠt¿. kç<€ö‘n¼r„×>N´vao}«Ï¹^8;“S6/‹‡r˜ •ð9“&× M®A±:ý¶A ÙM²›0æ²×b ÒdlYŠ (% ÙMh<ËùX„nEì|N’C•$ªÓ̆íH‘&O¬ á=tYz"rnN^g®~ɹ>n“Y3gvvî‰ø8>¤»uU[ýº:Ô|ù"Ž\md¹j¬ä.º°$b¹°¹„·M‹Ùº "ˆÅ€˜æA ˆÛ ÀˆÄä m<Vð Ô¸:€”2°Dü]wJ8¬‹ŒP2èOGNv~ö÷‹Ò’ßddd 3#ý† þï»gúŠ~öü´ ›L. Ý—zp±ç2 íŒÃÃ& ™Lø«álÌ÷ÿ…®n\èºî¨­JNFiÉb”•èQZ¢GaA~Xö.áîýM(›Ÿ‡òù¡»¿û)?±ƒ,I[½—Srwð×7Üök ¯èÁšœDÈät¿˜ˆt÷ ãWïM„¦´XC#J‘xà ‚ˆ%ª4 ÎE¿z÷(þë“ß™=ðɲÜaî†ÙiAŠT%Øq™–i?[£[…7o{/|õïúßÞ{ÚL5î*›ÒqŽSÁÃFýú ÇkÈnB¿m`Šã†—Ú…Ï ®óCt[.‡íxSeê„;xI“kü à¹O¢}¤ æn^ûص¿ w—ÏCn†ú†¿óqyðuž÷ @« ý%T$…V™=Q—7Ëþ˜Û6Eü I"UMdà¿>"¬\e– 툻>mŠªØÁ˯ ÛQ¬.>ÑÁC0n6þà3ŸªsªBÚç¦Ò".xx¥´›Jk§ý\HW/žXÉk-"ˆH Hìváœ$R;A„Px<€Ó ¸7fû‰Ø@rSЇ0ÙW½à âñ¸Íâ‚ ‘û˜áîàu(P©ØçÃ…ùXzçõ¤J‰³gi‘““ ¥B¸ç=© –-eŸµ-[êÛc_²NG¿d“u;q2f«'OÆÉS׳R—ꣴD²’ÅSD$¡â>Ü[½ï}°môÉ–ÝõØõêã!ÕÑ70Âû=[áwïàäÖ@îÂÓÑÓOî½Dد+Jß÷Á\KÐýaâbs`Û'aµ9â½)Ãj†¢Q%‚Hê¡fä-ç{ñqC[L‹Åiů Û±í¶—Ñd‡¢´D_¿ªÇg _`Çïß…Åjå]WßÀv}Ú„uTò®cûžã¼Ê%e¤¢ð»ÜÞs‘»ƒòn_ŒÞ¯¿áTƉ˜ˆÄG$b¯ ¤RöþÀíÜ.ßÏkE"Ö!J"¡ç±‰ŒuÌ_î:Šþak"4§ša˜fU‚ ˆÄ„kA1Ã0ÍZ­v €ú¸¾)´±7…ÿmÝw¬”¡l~çŒ*̓AhêÏ… ®j+š hä÷@Ú2jǦwbûƵ¼2L·ò<”§ë±Q¿>`6ùbuŠÕSÄ Íƒ0cF0£F´›»`vZÑ>Ò5m0¼JšÌÖ•’µsà•ÅÞì°bÌeó™M`ƒ{ÒÝ‹?ö|~>¿ð™ƒ‡c•`]6´ÊllÔ¯Ç/Z·òÚOk{/öÖ·âá•e×ë¼Éñ/ÄIÑ€£X—shËîØúúi4àÀ•#¸ÖаˆšÏD´=|Ä3+r–²ïM¥µØ×s÷w\°ÇZ·t+ T:ŸŸ‡ÃÕ~X]ŠUKÒ…$H$€8‰}™ætŸ¥T$b_ÂI¤d—NABb·ù;LÆåœ"rˆ$nwxêu¹ŠS"ˆ8¼’bGàõz:Ä"; Í‰Ö ¼ÊÍ+ÈÇ›~•*yÊg … Š‹ª‰DE^§ˆ —ˆIb¯¢Ípvâÿ± „ð%€Xzg%JKô(,íyö=Õ˱ôŽJüÛo·ßà2Á•½ ­¼-ç{q¢µ‹WÙEß_Y’2øu‰Ü"îù¸Çs»x‘˜žÁÎ$Þüð$.ö 'BSž&±ADbC6‚ ˆ˜ƒa˜­Vû4€ßÇs;.ö ã—»Žâ—?ý.ÊyŽOX/è1 ÙMÐ%Ïò»MÃ÷ÞGÅþÕ¼2plš o}‚·7>Ê}츀Þ]‰×Ë6„Ô/7gØþ°íöWÐÐwRÐÀØT™›Jki¡°,û<2g5ö\ÚÏ«ü®ýM¸»|r:L¦ß6S‚‡†¾/9—)N)û9ÂÇi £§Ú,ø{ë[9»ñÜL®2 Ú¤ð+œâ¿»˲+ÇëìÆ"M‘ ß \ E0m·†ï½êÏ ‹èá•ÒÚi××p¹:À½Kbý#wÓ¢NÄÞ—iRûÍãfwÜ®·KØÀ‘˜µ['‚ BX¼Ù<ƒÁé áa$ —àÁ㦾%ˆxE¦`y¸j¼.y„°ð *W%'û;H$RÎË G¡(-Ñ£´DïߣjÛZÏ&!ZÏ`‰ÚqzÞñ)-YŒ²=ªî¬ä5*U2~ñO/âÞu»x“eÔŽCçx%7Ùõi¯}fΟ‹9UeœÊ$g¦ÑB@ =ƒyìøä4¾éîO„¦¼Ê0L(ADbC‚‚ "&a¦N«ÕVx>žÛq±o;>9‡–-à\Öâ´NdóŠ`²´§É5Ø·üTöï@ûÎË×°yw=^zb%§r}׸ï¯fÞÚ˜Ÿí#]h2°N£Ft˜»}n·"g *ÒKP[…êœ*¨tØTú^øê5ÁŽeÍœUH“kh‘ç¹O¡yÐ0í˜ø=GGíxeǼúìýÈÍPC•$ç>7Ì]>Ïñ~ÛÀ´âã! ø¬ÓYÍ›Gmص¿‰÷±ÊÅrl*­ÅÒ¬Ûnø{ó¸CÃÁ+Gx×mqZqàʬó˜Q#”EÜÎæ!îŽB~¦É5‚‹¢åêžXIÎDÜ#g¦¬ÑAQ\®à·õ€HËC|C‚‚ˆï{'¹pØ‚wz‹X±/ K O§êþþo§ˆr²³QXP©tføïx…&!.^êáÜ·øóÁCø¶½–;#Û IDAT]ݸjŒ|à¥ÅjÅÉS§qòÔiì¨Û…œì,”–è±ôŽJ”•è}ºrLÇC®Fi‰_ùX¬V^sŒë3¿–ó½hmç77>ønë‘DUN:-a Ô„LA\Ø}¨G[/&BSv2 ³‰F” "ñ!ÁA³0 S«ÕjÓ<ÏíðÞ$ÎÎDçåkœÊ3ž4ÐÓévaÈn ð^‘®GÝÒ­øáÏòÞ×áÆs(š‰‡W–…µµI±™õˆ3⣋Ÿâ˜ñúÆ‚{8äj#Ž\mÄ[ç~xªðÔÌ{åé‹ Ì]£»—›x½|~Òøs^™ø;/_úWÞCn†zZטÖiÿîô8!ÅÆåz0b©›‰%‡Š`ø¸¾mZ1D RejüÇ¿B®2ÛçzZ‘®Ç³Åÿ?ûê_x»ç|tñS¬ó Ë܃i2²QÛPè2÷p.#´¸#M®Aóêý¨=ýÚÄzˇ|Õll»ý¬Ñ­òÝÖ0º:¨’äx鉕¸«l-âAAo¸€ð¹‘CDAÏ׈ŀ"‰ 9€gáƒHÈd¬3!<=܃ñs²³°ôÎʉßÑÕ/sçè0wŽ÷T¯À·@ŸÑˆÎ ÝèìêFÛ:»ºy B᪱Ÿ7|ϾÌ+ÈÇÒ;*±ôÎJNöSX_¿ú ^¢‡V¢šPܲÌåTF•“±DB‹Aòn[Ĺ ŸÄp‘Äãa¿ƒ\.ß÷b ’R ëÜJDìr´å"þ¥#šÒ –F” bf@9‚ ˆX§@€ò¸¾al½ˆâ¼ ^‚‡ç«÷r˜‚Êð¿F· oÞörHîoï=m¦:¬™ÇŒM¸ÖŠ˜ëæA>ºô)Ž›B®kgçìì܃ò´Å‚_š,±ÝúmœËh•ÙxnÁSxð÷~ûFp¨ñœÀm„V/¼¸ nTÒ业7æQö6´ò*[”’m·¿ìWà±(µZe6¾›{og¾±~3žÂ²ì;ߘ:P™¡òë€ޱ-Om=âü_[–qßvû˨)\‹ÚÓ¯r:®9KPSø(j §w §«CYq6={R’ ‚ ‚ ¾ð/;@䋨‚˜‰”ýñxصٻ¦‹Å¬°IDkHXá“4e̓«¯ŸDÊÙ1`& •J _¸½W@nvö¸Hä@g+€è¼Ð…VÃY\èêŽèñ]èêÆ…®nüáÃ=P%'£´d1–ÞyJKO+\±X,Xÿ“§±å7¿å´/®YþCqw¸åÑ{8—IÎL¥ ;ÃðxX¡ËàÃÀÉn'29¹ D,r´å"vüét"4¥@5Ã0C4ªA3侑º€ ‚ˆe†ÒjµÕšäÇs[Ú{MHJJÂèèhÐeúÆúÑ>Ò…bu`ÇÑo:[ví¢gÐ?óCٺα,»2êÙä™1#þýÛ‚n¦eè¬`u]e0æ²…$«ôÛy•»Ö 3ž ËøùÃß8 ÙM1#xàŠPk–Ùi J Å…ŽËS3°…âî°Q¿Þïú£MÊžÇ4¹ ß{Ÿ·èᘱiBðàt»ð©CP `E4ÕeáîîªÀ"éz4Üó>º,=ØwéöõDó á¡Byúb¨t¨ÎYŠ5sVùý ·«ÃºÕ•awN"‚ ‚ fîEÜê¶H®Œ¸”Œ™  Ñx&mÊ¢Y|=_ „×€ÄÉ›¥…D*ÁùöŽ)}XXT/X,Vtvu¡õÌY´1 Íp6bÇh±ZqòÔiœ<Å‹NvÈÍÎÆÔ½;á J¥ccca;¾îsªJ‘ªËåT&93 ¹Œ&ê ÂíìcÜoÜnÀ6Èåä8D±Dwßp¢ˆ†¬!±AÄÌ‚.+ ‚ ˆ˜g\ô°@€¸N¢Ñhàr¹`·X{àÊ<' àÁì°ÂéqB* î2 ®j+𠼂s6ÛѦwbûƵ³Qçfª9×ß7Öÿö]lÔ¯Ú¸~téSÔu~‹ÓósðËþ¯0;I‹Y2´Êld)2Fü0d75ò.¿Q¿?>þ13ެ[EÑŒ^ÿ—ßÏùdþ7[o\ûFx»;<5o­_q‡T,A±úF­žWôPðÇ»9gÿ?f<`ý s¾ÇzºäYq1ž]f`z¡R Ò¡vÑ3¨]ô ï:ÂíêðÒº•ÈÍPƒ ‚ ‚ „ÀCâ…˜F$b]„™P°ADèÜü|1JKô$và@nv6®^5bØdšv•*¥%z”–è1٢팭g h;skdžõOv‰DðÜt¡N±C(î üç2ªœtš 3¾b‡ÉØí€ÌHI'CQ§»o¿z÷h¢4g Ã0Í4ªA3 2#‚ â‚ñ›•5‰Ð–´´4H¥Á¿]d\……kü†ï½|ÕlÞûëÁ†·> ¸]ùü<^õ¼r¿6lü¼3¢ö«×ðÛoß ±|tñS¬ð¥}¤'û¿ÆÉþ¯ñ©̘c.[\žWf§>R)RUT…37ãt»`vZè ̼ûi/w‡\eÖÎ]íw›buOqYš\ƒm·¿ÂyŸ§í#]7ü­ËÒ§Ç}ÝØù?ðÿnþW<[³UwÜUrdúÞaUi(îÉ™ÜrÌÉST%)irΠ{Y»-4±ƒ‡pÑ}AD•þ!+~õîQXmŽDhÎÓ Ã4ШAÌŠ‡W–ÑE AAÄ!.'06ÊþØí€ÓqýÇnlcìgeÚ'¢ƒ˜g່Þ*E ¡Rrw ‚„"÷w‹y³´ÔyP*Bw¦ŽTIò ¶ë‰¨»Wß}¯ê°Ó½/AD ë˜o~x2QÄo1 SG£J13¡Ç›AD\Á0LV«­ð|<·C$!-- Ae{9påžS¶ÿ~Û€"Ne*ÒõØ·ü¬üüǼ÷{¸ñ´j¬{`ú€òUKâí½'xÕß2.BX;笻Úo0r(¸roDÁQB(^9­25…k}~ît»0d7ù 6—Š%ýš"M†TÌ^N¦É4ìßdÉ>3Ú‡ƒ~Ûz¬ŒàAñÏþÇMiCaÊÜm„.y-þ~X‘³„W°¹yÔP„5¹Ê,Ü?k…ßm Rê¯Ñ݇·ÎýŽÓ¾ÛGº§ü­ÇÊ@—< J‰"¦Çª¡ïKÎebUðÐeéAÍ—/†Eè J’cÝêJ:ADœâv±‚†`9<Vár2d^‚˜˜;“BÄb@* >ƒ¿˜Ç|‹È! ’ˆD¬èÁ%€©Ÿˆç˜A¾¾‡¹Gì¶´þ õ® ^gaAþ„:»ºÑvÆ€Ö3œÂÎ á¾Y+P‘®Ç²ìJÁÄulÝqÿ4àÂGh7wá¹OùÍB?ÝÜügß÷ ””¤¢IB ž¢ˆ!» cn†ì&ôÛqtðÅWƒØ83 ÙMpzœ‘LGªLaÇHÐÛ å|bvZÂÒž–ó½8Ôx—¯ñ*_Sø¨ÿõ], ê¼Z£[ÅYð0‹ 3fŒy7>Õ¹U1׎mßü›ÚÞätNKYq^Z·¹jA8lL®xØm€\.|Fw"ñp9YQÍ͸ÝlQ°óH$b3þ;92ÉäÔÿ‘F&ÜNv‰ŒÄ*A!»Ýèè¹…œ{´îÁÿS­Ô…Á_W;]è»~7o¯¢êÎJ¬yp5öýyLî*+¸MßÀo÷b~îi41gؽm¸îeHð@‘å?>9(b‡†ajhD ‚ f6ôú„ ‚ˆWjT(ë/b©iiiôŸ©¦o¬í#](Øå«àj ×¢áê—ØÙ¹‡÷¾·ï9Ž¢Ù™(ÒMÍR“’¤ÀÃÕeص?ô û¯ÁÁ+Gð€¢”|T¤ëQ¬.@qJ>¯¾üµa;^9’0'Ñqcš X;çÜŸ·‚“ð!f‡5¨í¼‚ˆépzœA×%z#7Æ.O`ÑÆÝ„,EFTçJEº>,™ìŒ¨¥:w)çcÛ¾ç8o±CQJ~@w‡`]9øó·tùü{õJÌ ZÏrÚ>U[Aÿäê@A„?ŽÐƒBìv@=~î!]¾ÅSæ‘(8Ç©Œu%qM/—“C@4‰Øqr„°¾ˆEXF*£6:z®Áåv#/‹û{•K=—ñÞáñ­¥Î €ÓéB›Á—ÐéåÁ::ô]5²ÿ¸zÕˆ6Ã٘냻ËçÜ&’îÈÝaÞw„‡j‹ÅÔÇ v|r_}{%šÒ šF” ‚ W'AD\Â0ÌV«­Ѐ8=Èårh4˜Lþ3_¸rÏ *xä- ¨«ÚŠ.sï O˨~ó v½ú8R’¦»¯{ Ç[/ðHöE‡¹æîþVž®‡V™bu>ŠS P‘®Ÿ¶üG—>M(±ÃÄXLrĸ;»˲ï@Åx¿ðÅì´ }¤Ûï6Åê|¤HUsÙ0æ²ÅTŸÜ(+Î øœÐ.̨0‚§Ç¿ÃCš\ƒT™:,Aé“ydÎê€ëeš\UW§Û…!»)ìÇоpºä*³PSødJõyÈcÊ1§è8÷]—¥'"çduNUÄû\‚ ®ãñà½^"ÿ¡§ŒÄ ÄíÞÝa¢n{R&t ש ¦¹“ØóAÄ~÷F[{h·³".8ìú)WÒGDìr©ov‡ÿD,•ú¹¼ß[üÝÏþ ǾlÄö77't?Z,V\@ñãè»j¼AÜÈN |¸wÉB¤k’a²Œù<ì­oåU?w¼Dè:– áéîƶN&Jsv2 ³F• ‚¸zIA$ ÃlÓjµžŠ÷¶¨Õj887¾1dšpÿ¬ÂõÛ¨ÅêÐ5…kÑpõËÜ ¶ï9Ž¢Ù™(ÒeMùì¥'V¢hv&ÞÞ{"®ÆQ%MÆFýú)®þ‚‘«s«BvÍH$,N+^øê5¼yÛË 'zà5ä0EõX R¸ò·t ²o§'päDuîÒ° ^/ÛP–¥Hª»ƒ³ÓÊmlUs8ï£yЀ5ºUËœ‰éó‘\‚ &]£xÆ35¼èr±?b'›­Y,¦>$f.wxëw»YAAã®:ED´aÆáà.v˜Xß<€m”=еA±Æ°y Ãæ±€Û}§¢G¿îĘŸå×>܃Ö3¼ýæf”–Ä÷³ïc_²–õGÃf³áØÉF8.´1Є ‚ÂÙ™ Áüͽ–ó½è¼|sý™óç"kÁ\ÎådIJÈ’”4@„ Ð} AOwß0~õîQXmŽDhΆ©¡Q%‚ |A‚‚ "¡`¦fÜéá¡xn‡H$BFF¦ˆ\9"¨à¡ß6ˆbuAÈõÔUmE—¹‡w°¨eÔŽ ¿ù»^})>,[^Ym¦›w×Ã2jù1,JÉǶÛ_ö$¥Èð[¶:· ¯”ÖâÕ6J\àå¿·nÁÿXòÆ„KF"Ÿœô¶N· CvSÔêù8ôõ‡ì fGàþêœ*¼¦¶ÿ\¿>à)KYG…`Ìeã´=!QCß—@i-§2Cvî¢H‰œÈÕ â:`cƒ¹âv³e)p‘ „ƒÄdÄböÇí~[‚çúä 1–ÆÀ6ÈåäEDìàr»ÑÛ?Ô¶I VU-Ä'_ü•÷þþj8‹e÷}ÿ×£àŸö<æÎÑÅ\Ÿ´1`ØdB÷¥\칌áaÓ„áØÉFš4!’—¥AÍ÷ï jÛC<Eæ.-åUŽÜBÃ6bËã‡ù”îSBX¬cŽD;´¨¡Q%‚ ¦ƒ!A‰H €åñ܈éD-ƒ0cFÁ¿Ç\6A‚’`ßòwPýùch<Ë«¼eÔŽ o}‚-ÏÿÀ§è᮲yx{c6ïªGk{oÌŽÝ}³Và¹OúìÓ`Ök>ƒmßügXpã‹ÓŠÿv'^/ÛsÇ–¯šnËeÎå®Ù‡8m?ä0EÕA`EÎ΂¦ö‘nA‚Ö­QÕ¹UaióSóÖ%0+Pé ”(xíƒkŸª¤É×t.ð³„ÓMƒÏzÉ—!» 5'_Ä{ ^7¹:„úFÀ\eÔ†Ž63¡yÒÿ¹›©†v\T”›©Fn†)IrŸ®a„B;LÔ=A„™œ]gý-Õ¢ñí"œ8ÌEb·7+| ‚ˆ6ƒ¦QØ® ·ÿNE!š ÑÛšCï>܃?|¸'¢Â¯Ž~É>ï»x©/õ˜b†ä¤$(r  GeÿJ¹ Ý{+’²€ÛšGm8ÌC𔑊9UüžQ*Ó(‰K(\û¶›s™"]fL{°Bk¾u! Ö1~¹+¡ÄÕ Ã ÑÈAÓA‚‚ "á`fH«ÕV#AD©©©€ÇsýuöÞ#¨)\+Ø~z¬ iŠB®'M®A]ÕVTöï@ýÎË×°iÇAlyþ>?ÏÍPcËó?À‰Ö ؾçúbKp߬ب_ï·‚íË5sVagç:©Ç9nlBó !bY߃eSé xú$w!Æó%NÛ÷Ûx§ EEz ç@w¡ÆËéü’ñ!ݽ‚®ß7kEPël–"ºäY¼öÑeéá\&“D$`_Ï!¬Ñ­ zûH‰$¸Í—/’«A„™–ó½èAßµ´œï…yÔ†ÎËׄÝIûôåf°ˆ"]æø¿Y(ŸŸG3Ý÷­#4±ƒ» P(‘ˆúu&àñN'àr²ÿŸr_ @$f $Rš b1+*sØ|¯Ùb SPþu_è8—°¹ØùMßADã™s™Çî½oþÏ#‚ìÿîÁÿ>p¿~õxüGÁ¿ºx©{®?kl=cÀ°iäúgã"†îž\ê¹<ãÆ5'; ¹ÙÙ(-Ñ#7'u ß|‹O}U±ÃúGîB^vp. ×·ñÚOÑwïàUN–¤„D.£E!ÂøJ ¤RV”ŽûºÖ#aðŠ.ö 'Bs†ÔØ ‚xJ]@A$"㢇°¢‡¸ö\•J¥N^ÑÃ+ ‚ úmŠ©«"]}ËßÁÊÏÌ»ŽÖö^lÞ]—žX9í6w•ÍÃ]eóp¨ñöÖ· <ǃ@b€ Ž–5ºûHðp®‰9ÁCJÇËå¡w´í#]ؽ˜V8=NHEѹ„çÓïÍCAöŒ»ÅÝ}‚ ~®_”³CŠ,‹Rù¯ÍƒÜû'%€Ãø¸wÔu~ÈIðÀç˜ÂEíé×ðÖ¹ß ^/¹:3¾VÔÐÑÓ–ó½1qmÖ70‚¾‘)Î`…³3'Äw•ÄÌËähâ Xº>ÅF$ž[ð¤ßm”R¤ª ëäL,‰áö¸ö¼n<“ÇõßKþ?ýËFÎå>º´? Hf2Cv“`ç)WøÚGºÙw0®kt«P+S‡”±_%MÆëe‚j«T,AEº>$JCßIÎeŠS ÛêÜ¥œì9Œ.KOT]G¸2d7¡æä‹‚:x!Wb&bµáDkZÎ÷¢uÜÉ!^è¼| —¯ápã9¨’äXÿÈÝXµdáŒOG\Ï]z‘è¸]€›¹<ãóÍíIŒ€Öp—K(x0ÿHà@Deýãc'—pKØ ;‚ ˆHc²Œñ.[¹x:zúÑtö’`Çó»Ý AñÁ¼‚|¤$'O¸5äfg¡° *•ÿD1mg ø·ß¾U¡ØÙA£š*x8Ñz×s—9Ue%)ùg=ç •þóãöØE"6QÏ‹ÄbºÆ#¡ØñÉéD;@-‰‚ ˆ`!ÁA‘ÐŒ‹jü>î¿´¥R¤¦¦bhˆuò;påˆ`‚`F‚R×®EÃÕ/Cr(VôEº,¬×eaý#wOdŽTžR¢ÀëåŠtÉZÎusuHd±ôõÇÜ15P»èüÃéW‚ ÌŸÌ1ã)‰+x°8­œ\,¦#˜~M“kP·t+~øÅ³¼öq߬xnÁ“A‰’„;@ÃÕ/9—)Vç >¶kt«ðjÛ6Îå6µ½‰ºª­·ë²ôDý<²›Pýùch<+h½äê@Ì4:zúq¢µ Ç[/Ä„ƒƒXFíØ²»)IrÜU6oÆŽ­; ™š=`ƒ%ôô1!ñxXg¾¸œ€SÿN" ±Á냻ßáÄéä´þFm¡Eö>vï­ ¨èa¦²p~1Ô))Ð/Z¥B‚ü ç®´1à½ö Íp6êíJW'¡æûwN+vË$ÈÐLnðqw€Âïò{‡(KRB"§ ¡b7[9m_VœSÇ/•Mu• åþVÎÁ`Õåb÷íö±o¯ð[,aïÉ b¦±ã“Ó8Úz1Qšó4Ã0u4ªADÐרÔAD¢Ã0LV«@ô P( Ñh`2™pÜØfÌ­2[ºûmƒpzœ!ðN¦®j+ºÌ=œ3‡O†‹èÁKn†zÂù`3wô\Cëù^t\îsmDÐ ½ý4à8HÅh“¸UJÇIð+r–„Ôç±Nó Wð}0ð &r˜ÿ¬ÿ{¼ÒöoœÊZœV¸r÷ÏZÁi_Ñ‚ÏÜj2„,x²×î5ºUøxùÔ|ùbÐN÷ÍZšÂµA¯¥)²dAÄCv¯àûPûÒézÎâ*ØÙ¹µ ÿ&àùØeæ.x¨Î]*hkN¾(¸Ø\ˆ™BßÀöÖ·FÍE+RlßsbÆ Ü.Vœ\n€â‡#ôyãt°‚˜xPÊ»]øz%_DÄ ÅåbEÀFDäןГ ý`ù-µ9p¦“¡ À-úÅHÓh°ì®*¤jÔ(+Ñc®N‡¹s®;ÊžúêkØlü”Ö±$t€¼, þö‘»‘¤˜þBNnÚ”¿y6¹’9.’3Sy+ßrDâ!“ž±ÐDþ"rep×v› ÁŸÀv²Û˜Dʺ‹Òu#1SH0±ÃN;A\!ÁA1#=Tx>ÞÛ’””0™L8Ð{5…k…ë§Q#tɳ=Þ}Ëß 9«6ÑÃdR’(ŸŸ‡òù7fGi9ß‹Žž~mîäý"G™‰{´Ën§Kž%¨˜Ä›J_ÀÊÏÌïâP,†F¡ÄÀ(·Ì3r‰I×3ã« ¤L“®Ål·aÄfƒÙÎþÄÁd÷ŸŽ\XƒÍgÿf§…S¹cÆSA Ì«àÂ$.Tç.å.x4`íœBÞ÷˜Ë¥$p  5ºUèzè8ê:?¾žƒè0_DõÊÄçåézh•Ù¨H×cYv%§1ÏR¤cQj‘ ý¿¯çç2¹Ê,ÁDnSûí>¼uîwœËÕœ|Í«÷Çô÷fíé×ðǞÂÕG®ÄLÀnhãU^~sû¦À\b tÉÚˆÍêÜ*Žáû¨tX‘³„³ƒììÜƒêœ¥Ó ð†&D‹š“/ RP®^g¡Öö^0×F‚Êœ_VÌŠóÊçç᮲yt1G„…Cç°·¾uƸ97B¢"š$Š ˆX€ØÐMIe€„²šÃH¤l°Aᛦ³—ðþá¯ãº ¥úÅ€œœlÎCÁÜ9(+aŸM.[Z‘c¸zÕÔvŸ5|}Þ ]Ý1Õ‡éê$Ô|ÿNäe§ܶ /)ð;zúy½¿š»´”÷qËÕÉtÓÞ÷*$¬»¡Ëɺ1L¾§‰X'0‰˜Ý6¨gžÐÄ“q8® -"‘H4±€jU‚ ‚/t©GAÌDÖhPï Ñh4ø¸ï`‚`ÆŒ‚ 6P÷÷U[ðôÉ !ÕÓyùÖ¿ñÖ­®Äº*Cª+” ƹÊ,<· °YH–"YŠ ÞÇØeéáU®@¥Cí¢¿Á«mÛx•ÿ¬ó»åVÈÃôdðpÇ·°»\¼æQ¬19¾:· ùªÙè¶\æTÇG? ú<6;-Qm/Ÿ øcÆS!]( ir ¥á›áàE6J‰‹4EaY÷õ⽎sÜ|ÙTúV~þc^eŸ>¹̓l»ýe¿ç—5,Tê:?â|.ú"W‡¾o¹€Öö^œhíâµÿÖö^´¶÷âã†6¨’丫lž| RP— bæbµaÃ[ŸÐaœ"]&uA¼‘H9B=Èd”‘2‘ñ¹\l`ÐÍA="$‘è…ˆmD"@,&Ñ$A‰GJ’΂¾î 7´;D¡‡V™ ©H‚ö‘n¿â‰4¹ڤ찺hÔu~Èk^sL¡ªs«x»<À[ç~‡æÁ3Ø·ü…"B6µ½Ry¾®^‘áÆs‚‘[Fí8<.Öûau)Ö=P9í Q‚¸®Æ¸Ø¡pvæ”ùíñ°?íì|÷.Y8cÏ'±$|‹ô~91‰¬+Á&‰D ÈE€ƒcІHÄ:Dˆ)È=aq8—Ãÿ¼˜D8Ù9!““ðˆ]d2Àf ãè"‚ ˆh\ËIB»Ðí5cûža;>Uòuq‚—›E …ùHQ±úU*Õ”íƒï )fÏÒbî]TÆââ%߉ úŒFüñïÇg _Àb½€r¥\†‡VÜ‚ÊÅs‚Ú>%Iî7© Áƒ¶l>’3Sy¿X"‚áÚù‹œËäfÎ,±‰Ë)üó(X…BIsˆºû†±íƒ“‰Ò;A‚@‚‚ bF2.zð:=¤Æ{{ö^;„[4 ¡W Ó?£Fè’g…åXkýÿì½{X÷ïÿÖI  #ca0²Á6à„ÔØ‰mü´Æq²'¡³Én/!ÉénýÛvoÒmvŸmB¼=Ýö4ÙnO³§ÍçÒ=iqcgÛøÖ®!qlÜ8 †Û1`°e#,îHBwýþ„Áæ¢ÍH#ñy= èû™ï\f>¯ïû1´ ŸÅÞ®}÷Õ78†ïþô]ä/ÉÀW¶ccqÞ¼…j–~¼´ïZ;¸Ê•è‹Â*¼_“ZQQzcû›7 ×—?ÏyÆöÁq'š-=ØœkæmÿŸ½Þ‹‹6Nmµr ¯i&³‘¬Ð²nÓ2Ô>9û~uþNNÉÇmagÎ=a½vØ3QrH$Td±,g#t̆Ýù¬LU:2Uéèw¢ß=4)>¤)uH’©¦ÐE$ „C·ÃÂI(¸;;¼c?YÙ©HR éú)丵kw£fÕc1û»Ø2ÔQºÛTû¸'Z»ñÛc­Q+§± GN]@í7ïæ$þÄ‘æ 1‘Š—g#Y£„yI&´j%̦LLºÂ|ïãÀíºõç!¢op ׇn|]êí‡#ŒÙ9ó—d`×ì± d=!Ÿ…ññHÀø‚ÓâKe̸KeÌ ÙqwÌð8³w"sËd€T ø& ׃Á¹ÇR¡`D "1  9´sOH4JGB|„ÒH8„w†õÞD*¥1&"ú$«U±»8µw{ñöÑOáòx#^/”݆g¾÷dÔ·_&“#5U‡Œt=2ôéQOu12:Šë¶éÏNþé4þÐØ„æÄ;Ëuþ’ <¼môºðîÏ*2äeÏ~ÿD륰îiÜÌÒ ÜÓ”É$;Ä’…–èëó Óo À|ž¢”æ^©u` æ陋7î¡ö Ž…ÝOñræùCVF Œé)0›2•ž2y_—àŸž¾üðõàt{asB²C íY‚ "RèQ A±`±Z­-IHéá'=¿Ä3ù‹¼¤%÷eqZ¦/Òt]ÀsoÀÜt¹ùFKßàúÆðaë%^ ûž*Ú5ïkV¥š#žá¼ñ:á!yºðP‘UŽ¿+x /^x•Ó:„ä>¤‡žáA4[z8·Wˆ”åɹ8Ã2‘`Øs#±"OkB‰¾g†Î±êãе¦¸JõEHU¤Ü’l1l„ŽÙ°ûH–k#Þ†ø ¸¦„›n©°é5`ROv²u^AuþƒÓÎhÁE ñõeøú=eó’ÉÖK8ÑÚ“ãÉ1îÁwú.¾õ•¬“(BèãV«f¤†’Ì{3³)3â·RéÌEÖkò³'þ½ñ3™ Pª˜sµÓ2€Ö‹×Ðyµ–ÉzYé)¨\_€/o]» ÓR¤2^û^`ƒÌLîßì3¹OÃ^æ¼\Î5ÇÓÃx¹‚)ÆŽ‰$q ýC"ƒB1Ql˜^ð.•Lˆ/T€‘ð×.²ÃTü>À’q¢P×Üb§÷ôä’ ˆ¡Ó&áªm„SÛwßÿ ×ú#¿¶ûo¾…/Ulæÿ3ºV ùÛX¥R!IÅ|NMÕMû>Ötu3Ïúl6üáØûøCc®ÛúE{Ü$)¨,/À¦Òü°ÛȤRä-N‡lÃïC÷mjŒ%+9o ¥;Ñ"ˆìsÒ|ø¼Ì{Õ…ÄÔ{ g&îƒr‘¦fbrÁŽ[—¿$%+²Q²"Å+²)‘šLv€’‚ ¾ Û†AÄ‚fBz¨ðN¼oËxÀ…=—þO/ûNÄÒƒËïF¿{PЂ`¾¥‡­×"Jo˜G–íœw–z£ÚÑLö!ö[³nSš¶ú–ŸÕ®­Á~Ëaγó!=xü>¼ßÓɹ½V®ÁÎ¥;¢r.q)¨o¼Þ<-ù zÙƒØ=´‡UlR†½£1½ÞTåT²>w[†Ú#\~7/ÂC¬öŒbÿ•#¬Û™“sþ¦ð1>‘^3Bô8®rJ;ზ᳜Ú=ùµ­¨\_0ûyjéÇ™‹×p¢µ[п5lùßžÀésWðжu3ÿ]ÊHYp3„ÑG«V¢dE66/ƒyI†`3|)UóJ%7j&«U“݈YÆKÊ]xÐ,[`ƒ×Ë~†Â áó1Íñ"‰Èd‘§<,”n©€X€þÏ‚'RÙ!„ßx'$‚É÷f|¾Ó“ ˆ¡TÈšœÄ:åá³N+NŸ»Ùgjßÿ‡¿ÇÚÕE,ß—3© É RS™ ˜Ruº¸Ý—¯Xð‡cM¢OsÁ6Õ!D¶Aµjö7wÌD+—X¯ONyd¢$¥ÑýC":‘6•€aŒc§¥'Z»y›ð ]WÐuuï4¶6çacñ²9Ÿ³³ãtyMvxÔjµÖÓž%‚ ø‚„‚ bÁcµZ÷ÆG¼÷‚ýã¼I§UðÐ…’„"+)sÞ¢{£Ú€UºÈÓºÖ 3ûþͤ)u¨/[ÿø0çõ¹8`ÀÓ{WAÉaÖfK<ÜÁüöÊG¢Vè¾<%7â>ªówb÷'{X· 7ÁîuÂôA.‰ÍÛù*ÓvÖç퇶Ó'4Ø}Θ%3ðAÝ…WY'cÀÝÙ[Âz]¤©2|^3bM·Ý©ݑæ è´ôO›…È:‘$&Áa&NŸ»ÑÃõü%3ξŠ©¾™â›ŠÇ“ÕJа^€ä/ÉÀÅ˰±8/jû_"”ILq¸ß;½¸N@¦`f˧YÓÙ!“³/Ôg_Í1YeB 2i‘ÿƒ€ÛÍHñ’zŽ€4ã±FJ’RAkBÿÄ,é׉ô† ³ðyù±Ô祿o„8‘J…*òÔŸ©ýÑß‚ bIVz +áaÜíÅ»ï·E´ÌE†L|ÿž@~^x÷¥µZ-² ¤êtÐjcFþ¶³íxåõ·ðëwÀátŠ~}“” Ü¿e Ê sX·5¤i‘> q¢µ›ÓÌì9Ör¿/ T@¦$Ö/®}Ìþùž1cá'Á€°ý ™k:-ý8rêN´vO¦ØŠ &»o¼w_¿§ŒÄ8]^ü¯7Jvx–d‚ ‚oHx ‚ V«µÞh4¦x!î? OH?6ÿ Jî…ÁÞQ¸ün$É„žŒ'éáÅOÎY¬Í—ìµmÜÅ©)7ÿüï Ë^å¼NƒãN¼ýÙ§øR~§„_XÝ;6:™Á…}î^¼%jû™SÂCßI`mÍä÷iJî7mÃËQVý·;ÁîuòVàÎú8[TΩ]ËP;î2ÜÁy¹v¯#®¯Ïõ]¿áÔ.Üã?YÎßΊ¬r<³¶&f ±Bè” 13ë P³488ŸÅ˧KSgØÏº)u‚fß>fS§ã=I*×àÎ’e1K‘LÌr­PLÖH^1-ŸÈå· $‘¢P.œñãkwðx%âCz H^Oø³3ÊḏAEÛ‰A08³€6 ?S¬ºvËÈ Ÿõ ^ïÂIF!â ™ +"?î% ìýAâD­RÀ˜‘ë@xE¤´tahlœóò–ååâǵߟW\P©TX²x1ÒÓõHR©b¬/_±àw‡àç/¿†+–«q³Þe…9¸oóš9f#Y­D¶!uÞ×qIwЙ!Õ”Åy»T)ZºÄ˜…”ÐŒ‚$νBû¸Gš/àÆ6QJ3Ñ78†çÞ<†ßkÅw¿¶•&Kš‡ìp¹o$Q6i¯Õj­¥=KAð A1Õj­3¥‰ûÅþqe‡n‡…“r¿iÛœ¿¯»ýi4^?É)9"„ÇïÇ{Û±z‘·-6…•öðI¯…óò´r ž*ÚÕ}=SJÆ| {GoùY•i;kááÌP{Ø)ÃÞј iJ¶,Z¦ë§Xµ;n;‘ðà ¸¯Ôw5 ÇÁþÁÝöÅ[–p’ü>”ª][ƒnÇ•¸Iá™~¯f}|üss1}¸ÅõSE ³éFòD(U‚Ò$øá+[‹qäÔ…°f Ôª•¨\_€¯l-ÝÃW’øA"áwvf™ŒùZx=üÏèñ*I|Ìp-‘0Ià÷Ýšr!‘NÌò¦ÐD‡Ä!`®áªƒÌ±-ó'¾ôâ÷ñ+MëWAç!N æ<÷û"èCIïí‚Yé)ðxý;i`ÜíÅŸvq^N8²CªN‡¥9&¤êt 1¶!Éá­_ïÃgíçâjÝõ)j<´mç{R2©yÙóORfwãDk7ëþ—–G´}Êd üDT?O N¤<œ¹x GN]ÀÑSâvº®àÉŸ¾‹ï~m+6/£ƒTv¨¦=KA A1«ÕZm4z\W±çÒ¿G$=ô»á æB.þ-CíÚäiM¨ùøYŒxÅ5;ÅöÅ[æœuŸOÙj>~–S»*Óöy_³ó/QúÞŽˆÇøìu+.ØPnÊÊ ì¯³{ܰÚG9/ç©¢]0&¢¾Ïµr ¾ð£³g’HªL•x”òÛN‡5£ÿ°gˆá¤KU¦í„‡pXì^ñÇ™Ïzã˜ÚÂ&Ý$MÁÿƒÏxJá™ q‰Sňi’Ä ©Yé7’"B)SÓ#(9bv²ÒS°ë;ñÒ¾g•Š—g£²¼€¢Ç2“ôàóEÖT²pfgø#¯ÙðxURü6K¥€”få^0L² —ú¿º˜tD-Ü7ñ„ëuGFOu‘¢˜˜èš­ôJv c› 1‘“• €»kÖ×|ÐÒ—‡[¼Í|²Ã"ƒKsL ‘æÐv¶¿;|¿;tD0ÉA&“Á/Л°$¥›ÖåG|oÄlÊ€, ³‹ìÆ’•‘ýרèÄ'‘pæâ5¼ñÞé„I£vŒ{PûËÃxòk[é>óM$ ìp@ íY‚ B(èö!AAÜÄ„ôP  $Þ·%RéÁðÃâ´"OkŠÊúVçïD©¾Uï“ÓLèBP¢/š3a€oÙ¡±¯™u*@ˆ*S弯ÉÓš°ó/±õG¼®¿ï÷tâ“^ Ö,2bE†á–ćϮ÷rîÿoV~#¢4€HXž’‡3Cíõ‘¦Ôá~Ó6Öûó¸í£°ŠÜí>GLϪœJìþd«6ŸcÝs¦¥„³Ýá&ˆ…§[ÿÓ5Íœœvá¾\*C’L˜‡Rñ(=Td•ÓšBßàØdŒ÷\€BbD²F óf&¾PbÄB–"*× dE6^ï4N´^‚c܃âåÙ0›2D™æ@B @ø¸Õì@*efû_(³{½Âõ 22E¨x” ÄB08‘ìA sþ(T’ )<†„H‘H˜óÚ/cÂI€‘É™¿u”\B„ØuÎ);àœî Õhf•Ru:äçåÍ™úüîÐ?y ¿;|W,Â<ëQ«ÕЪÕèLvXoÄý›×@¯‹läd¥A­ ïÃÝ™‹ì œu¦EÐd¤r^?©L…:‰N|"jH%ü§e&‰&:ÜÌKû>„yI¥7OáÍ#­‰&;TX­ÖaÚ³A„Pð@A3S $=ÀâìšðåˆÓ IDATkÒVâ·›þ/þéÌOp¸·)¦cw§¡lNÙ!/ÙÄëØ {FQÝü§¶ä?€4ex³»Wd•ãµòçðh󓼬·ÝãF³¥ŸôZ›–ŽÜT=rÓ˜hæÞ1néó¥jˆ‘n‡å–ã¡bÑÖÂǶӰºló&[øþ˜ÿçiMÈÕ.a]Ȩ· ߎ@xpùÝq%<\·âßο̩íÎ¥áŸB¤;L¥¾üy¤)Rñâ…W£2n‘ =yZ'áˆH\¦Š“3åÝ”‘¿$Éj̦ÿj§|Ÿ¨d¥§à»_Û `+(¦èP&e‚a>|–+˜¯…BÀÏ ‰ßKÂ!>|Þð¯ sß> ¤ X½/dÝŽÐ×‚à™ ©™ãÕïgþfƒÌ—TÊÈ R)#;è@„±»q¥oîZ½Ï:­œÓ~ôì­²ƒJ¥ÂÊåf¤êtq9fmgÛñÁÉf?y ¿?,ìý¸E†L¬ÄŸ>þýƒƒ‚,CŸ¢ÆCÛÖñR”kÌHA: aâDë%ÖËXZ^Ñ:*“5tâÑE"ð'iü Gßà~òƱ„B„’^zjgBßo—_¼û1>h½œ(›Ó’‚ ˆ(@ÂAAÌ€Õj6H éáùË/ãéeßaÝÖð‡U|ÍÛ›‰·¥¯ÁþÍ¿Àë—~‹gZÿ V—-êc¶}ñ–Ye¹T†å)y¼Iuóœ“-ª—=Èîõù;1ìe=Cÿ\xü~\°á†J™ YÚ Ž;Y÷“•”9§h JÓŠX''2UéqqíµûØýñ8|Ü΃p’>B„+øòór§ýl©É„¥9¦¸§ËW,“‚Ã'›Kq˜Ê+6cõª{ÿ8ŸdIJ*Ë °©4Ÿ—þÒuV陡ôM¶KVF´ž ¥;ÑŸ(¤Ì-‘ÄÇ}*û¸o¼wï4¶-˜}ß78†w޵áë÷”-ès Ád‡U$;AÑ€„‚ ‚˜… 題ôïÛÓîèÀK–·°ËôUÖm»í–¨ !’d*üÕò¿ÀWr¶£¶­õ] œŠ†Ù¢•kðí•ÌZhœ$SaMÚJÞ‹Ìë»8ænY´Yå¬ÛÕ¬z -Ãg±·kïãèñûqe”Û}êüæ:’§5¡D_ˆ3CçXµk¸ü^xƒ×DùÜœJ•©’µðÐiï‰H¢²{q±ï­.~gù#~sù½¨œó ¾ v/s µûðoTbÉ%²ÉkZ’L…$™jŽõÚ‰Š¬rTŸ|M×O 6~ËSr#î£T_„gÖÖàÙ¶:Q%úBT™¶£bQù´kw¿{ÖqúÝCø’ñ.Œ ô/ŸýƒºO,n"¦$Dd¥§ +=e2 ‚d"QÊ%ÛÏH´Šš$<"Âïãùø0ÍðNA„X¸Ò7 U¸–~Ö}/2dâ«þÀä÷©:V,7#I%þY®ŸlFëÙv?y ­gÛ£"8À²¼\Tݻū‹pôX~ú¿lYe…9¸oó¨Uü˜zé: r²ÒXµù0”JÊi4‘=¤„"ÚHe|÷/rN´^ÂKûNL¦/$~ÛØŠ/o]»`SPv¨°Z­-te#‚ ¢ A1V«µeJÒCÜKMÃÖÒƒËïŽjÊÃT2UéøYÙ|ge5ö|ö"XŽ &>l_¼ß^ùYe†L•«RÍKø} Õ2ÔŽG›ŸäܾvínÎmëËŸGž6G4ÁÉr õ6á¸í#,O΋¨¯R}Ѭ¿3ª Q;ž«—=ˆÝCìRú\ýèëÆò”¹Ç`Ø;ÓýU‘UŽTE ëÙþ[†ÚY¥L»Ü¢¿Ö{Fq~¤?»ø:§öZ¹†ÕøÈ¥²¯[V— ý®A {GácY•™¬Ð Y®EšR‡4…nš‘§5¡ñKo£¾«µm/pN¦™‹» wðÒOíÚt;®"v±á~Ó6T™¶£ÊT9MNqùݰ8{auÙfÜG¥ú"ì^õ8¾ßú<½)9}ƒcè›W†`þÍ„1#…Õ Aˆ‹`ùŠBÎxHlñ úü€,‘žR¼AAq‹}Ü»kÞ×»½gÝÿ_UcòÿbNuh;ÛŽÖ³íh;{mgÛq¼ùTT—¿È‰ w”áK[· ?/mgÛñ½göອ_åå/ÉÀý›× ÛÀß#@.²´^¼ÆºÍÒòâˆ×W¡QÑ€ˆ*2óÑ)(`ÿbþ[óÜ›Çp‚ƒà”(8Æ=8ÑÚÊõ nÛLv˜d’‚ ˆ¨AÂAAÌÃéáÓDØ®ÒC,R¦R ËÇ[_Dã*êο‚ÿºúGtÚ{"îW+×à.èÎß9ëöÉ¥2¬Ò™‘©Jç}»†=£¨øÃCœÛßoÚÆ)Ýa*µkk§5E$]ð…ÝçÄ™¡vÀ‡¶Óõµ÷Rø¯-™#ŒIŒÑaïæm›ªr*±û“=¬Û5\9ˆ§ŠvÍ=^^gÌ÷YUN%ëbòã¶8 bØæ¹pùÝøl䮼7y,³åÛ+aõú4…î–ëÊùÑN¸üÜå»× »× ë¸ “ü©Òè6LÊÕù;QeªDÝ…WQwþÖâËœÛ4ObêËŸGš"•uI¤”è Q½ìATç=¾ ýî!Xœ½aÏwî€99——¿yDl¸E†˜Bñòlde¤À˜žB©G£(!ð@$ú±‰b"•ÒqB‹ëj¹¶J¤”vCb`h4<‰ášm„uß‹ ™Øð…2¨T*@«ýŒú#££Óä†ËW,Q—Bh5”¡ î(Æ/”'þå?æ>d™ú5îÛ¼kÌF^ûå*;tZú9Íôn,YÑúÊ” H)Vˆ2àóò߯D"^ááDë%üäÍcpŒ{b¾.ɦ,(ÂHX³ôÁ7ÎÿDdGš/,8áá·ïŸK4ÙáQ«ÕÚHW3‚ "šð@Aa0!=< àµDØ.ÒC,S¦’«]‚nO~ÍýŸ¢ñz3Z†ÚÑ2ÔvòCVR&Jõ«Qª/Â]†²YÀ¤1"/ÙÄ{ª0!;üñ!ÎE©ŠÔÝþ /ë*È­>ù¯EËñB¨0ýL}t;,3þ8sï¼>‘6éQ«ÕZOW2‚ "êï!i‚ "<¬Vk½Ñh°ô딇©˜4‹Q•c@YF1,Î^ø~Ø}tŒ13`·Ü4³úò”\$˵“ÿ·Qm@žÖ$hQuÕûßÄ™¡sœÛ׬zü–‚ÚˆÖÇT‰–{F¼^ •Ù„fl·³>'õ6Í[üî ¸cºÝU¦J<Ê¡]ËP;J'’5Ø"VáaØ3ŠaÏ(~ÔþRØÖÍTç?ȺMHx`®Ý‚o§ÝëÄù‘NtŒuäY “ƹDŽêü¨Î߉n‡û¯Aãõ“¬{œ‹ù¨È*G÷ýò&f„¸ß´ ‹6Ì(9ø‚>XÇm°8­%n“ xûΟák'j¤”¶éº:€®«L´ù"”a^’³)fS’Õ*0‚ˆ2”º@Ä\He€ß'Lß2J ˆ¸"¼Þð¯ ~?ó%ñJ¥ÆD4±»Â~m¸bÄTî¸mŠ Vò¶¾mgÛ12: “2ÃäÏGFÑc±àŠåªèÆy.É!´þÿ·þ \ê&é4IÞ¶zÿ ‘Ș{@,Y\ù1¥Ðð )Ù™À§ìÚœ¹x %+²ÌI$€Bxx|Ä%“‹/Ý¡ÓÒŸ¼y ]W_–L¥@²Éˆ4³ šEéPô7®¿Æ (uÉÜiS@¿2wòg®\;yWþûOœR Ê1ÿÁ™ËøÅ}œH›ô"ÉAD¬ á ‚ X0!=¤x!¶§iøO0(Ó±sÑŽ°^/–”‡É729ò´&˜4Ɖ™Ç{' e¹ÌÊ¥2dªÒ ºù‰ˆŠmKô…¨][ÃûzåiMhÙq5ïÁ‹^¥“ž¯ý¿5?˺P9áÁîu1<'Ó”:”è YK2Çmq.l·û1Mµ˜õo„ˆú®†ÉÄöçuët“WÇXëä€Hðüè¶[`qö˜d€Qm@²\‹<­ 5«ŽÂÃjÁÏǛŌ–¡ö°’Jô…ÈÓšP±hJõE¨È*ŸñuýîAô»‡`·ñ¶ÞÉr-ò’sX_›róÑïtÌù›Ã>ïkqJ„˜zfiÕJ˜—d¢dE6̦ d¥§ÀlʤÁ"¡âC‚ æ¼· ð ‘PÂAÄàqq›µ8Ü.@©¤´‚ˆ.·WÐþו¬å¥ŸŸ¿ü*þõù1:?b,2dN kWÏ|?¸ÏfÃ/^{Í Sš¤T ²œIu‚Heû¸›ÓlçF>„šHCRhÂ@& F©„éKLœh½„Ÿ¼y Žqpc¨R ÍœƒÔå9H3çÌøšHe‡Y¯­©Èÿ³ÍXúÅ/àÔ^Ž(ñ!QI@Ùa¯Õj­¡=KAÄ ºMFA,±Z­uF£±À#‰°= ×Á HÇýú°^/¦”‡É74âCžÖ4YdÚï «ðW.•!M¡CfR:2UzÈ%¿=úË‹ÿì~7¢>~ñ…tënš) n~‚Òx¢*§{»ö±jsf¨}^ÉÈîsÆ|Ûª—=ˆÝC{Xµiá(€/èå>>t­ {/5D0Ž;Y·™šî0ìÉvû~XœVüîêÃð møŽ]?‰«N+§þf“ø&$f„äŒÐq9Ó8–ê‹æ•lBR uÜQšÃ¬ÇWo'™fÇòB䦥s^î9[ß-?³9í°9ìÓ_×?ýu=Ãpz½ „Ç1îAkǵ[Œ/Ÿ.@,¤â‚ a€[áî\$b¿TƈQ|§ÁPÑ3AÄ‘ÈSñxÉNÆÝ⿇ñó—_Å?Öþ@ô뙬ÕâŽÛ×áŽÛÖá‹›7!{±qÚï]n7®Û˜IB'öÿþ üþ Naîs ™êK ©ÈLÓFÔGëEö²ƒB­BêÄŒë‘@ âÁ1î^Û-W0r·''@&J¦±ðÆ{§ñÆÁÓÂ]kMYÈ(ÊGÆjóœ¯Jv˜¶ÕIXúÅ/àóß°›«5ÁTv¨¦«5AÓ÷Ž4AÁ«ÕZm4‘^ºú+Kz[ÊÃÍdªÒ‘©J`†Ýç€Ëïž± VEîSáR ÞiïÝçàt{F­¸öoëÐyüsësœÛ?³ƒSâE¨ßÂQ.臻ÏûÜ‘ñv¯öᎧEå1Û\öAh …:­.~öù^|hãöp$Ù ·>8-û‡©S‡×ƒžáÁ¿‚Óë¹åuD„×¥$ˆü%0›2ažø—$‚à†$Š ”&AˆíØò\Ä/•$æX)€›Çz%‰ÓÓ‚ˆ ‚AÀãæOóxU’¸ ø"wûí¿çŠwmàÞ~dt{~ôœhÇï®òõ¸kc96mX»6Ì}o¯í,sòï㯽.˜è tªäd¥!‘âÌÅØ¤;€L©  €¤,fŸ¾ÚiÀÆâe r¼dr@%¼v⸌è &9Ü>îÆsoÉÖnAúÏ(ÊÇâ ÅaI ÑB$ó `% (;4‘ì@AˆºENAYÈÒƒSf"Y®E²\;!@ÄžuïEËÐÙˆú¸ÓP†9÷`Ø3µ}P¿U¦JÔ]xuç_!ñaæ+v.Õ!W»=Ž«¬ú=ÔÛ8§ð0"R’,vÑÓ¥ú"¤*RX-Cí¸ËpGBìû‡O|ŽiZ¹fÞ}<i Fxèw†õz»Ïã¶Ó8nû-Cíœ×Y(î7m›7IA Ø}XÇm°ºla¥ qáPoçT‡™ñ˜A7‹eÙ9ó¶™*?ôŒ Â11ÍW(MÂáñàòÈýbA×Õt]ÀÔ¹¶¦J%+²a6eÒ@ÄǼE÷vŸõ] 8ÔÛ$:Éa*ÕùŠvÝ|AúÝC°8{a÷ 3†cÝh¸rÇmñ²ŸÂ‘ ÄÌTIb¦¤‰©„䈩é!1Âæ°£ßé ?T³0“Q¼<fÓˆ¬ô(‚¸ ©œ)>|92kB<ÈäŸALRibñ+”ŒéµB©$ù‰ â ¿u>BTdg²Ÿ¬ãw‡ŽàGÏ~©ºðÚŽŒŽâ­_7àç/¿†+–«1ÝÞ5E…(^]„µ« ±iC9Ö®.âÔÏÑcMxöG?™Lx‚$¥m[‡5f£`ËP«È[œ¥‚Ÿköq7§âh>¤2z“)\d’NË ˜û Ê)§W €É'Ä|¤ÓÒ'ú.ã^ûe+:@’^uÙaèsö©àZµ2áŽßž¾‘D”*¬Vë0]‚ 1@ÂAAD€Õj6±À¤‡xIyõ] øqûKõ¡•k¦Eû~Xœ½0iG}{ªów¢:'Z†ÚQßÕ€ý–ìS —þÙwªr*Y ÐpåàœbÀ°w4æ³âW,ÚÀIxˆwjÛêXo÷Tî4”qN¹íóù†+«AÔ¢äj— ÊT)ºõöŒÂ겡ß=(HšƒÝçÀ¡Þ&4\~}.~g¶ÛœkÆBaª1›èáôzÐ3Ì$B´Û¬nHSÓ$ µãZ;nÌ.¨U+™ô‡%™(^ÁÈÉj ± ‘Ë„$fÆB‚  ¿©ŠP¼«T^ àãð6C&ÙÄ'‚ˆ~a ‚Aæ‹’^BÀ¿Ù <ÞðîûdRY÷?:6†{vþÞkøÏ9¥‡¶³íøù˯áw‡Ž`t,úIËw•¯ÇÒÖ®f$6é ³qüd3~ø\><õ'A×½¬0÷m^#Xª¤ë4È6è ãÑFmåî 3-‚BùûqMüÁ%cê½8âñ  !;¨ z˜*ÊbÊb÷÷L— µAõ1p Œ°n“hIÃ=}#øáë$Ô&d‚ Bdð@A²P¥—ßn‡yZsPßÕ€G›ŸŒ¸ŸºÛžÆò”¼i?ëvX¦Ôqž>RJõE¨»ýiÔÝþ4Z†Ú±ßrû-‡lòC8 yZî7mÃËQV}ŸjŸ3UÅðÅ|û+²Ø?üê´÷Àîsp:†‡=±—<ê»ðl[çö!‘‰+ó vŸÿÜú<ÎĉXR_þ¼hÖÅôÁ:nƒÅi KfâBËP;õ6ápo“ ý²¦I Q('“"fKŒ‰=#ƒpx<è‚ÓëÁ9[ß‚;Ǹ'Z»q¢µ8Èü,ÉJ "RSt · ݹ%Dˆ\ÁÈ>AŽï…RȯP0ò’Ï øÃ¨£”)¹„Š› "¾ð„ë; ’ „D)_xP«Ч¨146ÎjŸµŸÃšõ›ðÕ?߉?»{ÛäÏ[϶£í침Iº”¯.ÂÒ–昰iÃz,51ÿç“ã'›ñ¯Ï¿ˆãͧÝ}Šm['xáìC*2Óøs†ƒð¹"—ŸÏœJŠ’c&Ƭì&¸é£{lqÆ‘SðÒ¾y“d*­+Äâ ÅÚ*¡Y¤É8ŒYöýóìàt'̤J#ªHv ‚ Ä=6#‚ ˜"=tHM„m Gz`ŒKè-ÅLð%;|¯h×-²À¤<œíD©¾hÆ}0ì½eýR}‘ Eâ¥ú"”ê‹P»¶fr¹×›Ñ2t-Cí ""MÞ¸Vç?ÈZx€†ËïáÛ+™ñwvÌÜŸ§5!W»„õ¾îëA©¾(îöwËP{Äç÷ÔÔ–H޹aïè Ç„5ïA§½'.Æóï ã$ÍðM¿{ÖqúÝC‚ô/dšÃÍQìGJ8 Peªä$êmBuþÎ ä}AŸ(Æ "«{»ö±jÓ2ÔÎIx°û1KxöŒ¢êýoFÔÇ9;p—ᎈÏ5»Ï_ÀËØÄ“ìP¢/DÝíOÇlù.¿g/úÝC‚¥9tŒu£áÊAÁÒnæîå«f-Ø'"#4®eÙ9·ü.”Ñn³Âæt ßiŸüÙB`Z ÄÅË™ô‡âÙ0›2+ ˆX!•1³Ôûx+¦TÒÌ}é„ôàfŸr"“Š||K$Ìuƒj– ‚`}ý ! AѪU° ‡?™AYa'áAhî*_ß7ügÔ—;2:ŠŸ¿üÞúuCTD‡ü%xxÛ:èuA—“¬V"/;2 ˆíãnt]`Ý.såR^–¯ÐÐ}!É\™‹î÷?aÕ†„‡øoÙaqy1§T‡±”†>gÿ,*Q’LHv ‚ ˆèBÂAAðˆÕjmI4éa¯õäªMÈKZ2ãï»í“ a{/ö[Žð";«|Ø3ŠÎà­îxñ«ó¾¾Çq{»öao×>lY´µkwó">´ µ£e¨Ý ºWÐm·Ì°=«‘¦ÔááÜÿ§Švͺ}-CíöŒF´>×›gÿ]ßÉiËñŽEõ©Î϶ձjãð9q¨· ;sî¹åwv¯8„‘ŠEØ Ã휖å úc¶ÕÍOD”ZbNÎ5­#\B²‡uÜvËï~ÔþR\É_|;6³]6XÇm_kæâPoõ6áÌP{Ô¶kiª_/¹Dô™+✭6§6‡}2âòÈPÂIkÇ5´v\2ßç/É@ÉŠùÖ0 IDAT D" PA?ò×§RÉÈ!f¤R@•ø|€ß;Úƒd"ÕAFO$‚Hðkc<öMšœ™T ÿÍL³mHEv¦×úGŵ©Ñ æøÉf¼õë}øÕoöEeyú5îÛ¼kÌFÁ—eÌH¼ ·ÓÂ^vÈX±”·åK):HP²o[ź͉ÖK¶Òà‰>e¥N ó}PôœûÐô©”1Û™ÏY·)N€t‡” Šd‚ BÌÐã‚ ‚à™D“œþqì¹ôïxzÙwf—¬Ò™içƒ)ž¯>ùoýÕw5à©¢]s¾¦c¬ÿÜúú\ý¬ûoº~ [ÿø0Éu·=ÃjÆüaÏ(ö[Ž`¿å0ûšÃ’š®Ÿšö}ª"…IƒX´U9•“i!\fü¿™9%޵53ÇÝvˤ´Ž<2m<¼á?ܪÎßÉZx€†ËïÍ(<ˆ.ûÍ:~=®Îñý–#8`9ʹ½V®ÁJ"¢Bçj¿{zÁôqÛGøÐv:.Æ’Ëu'Rì>,N+ú݃·$c𹌆Ëq¨·‘Óu9–¦êñý-•ôÇX„²Pˆ[EˆžáAØœô .¢ë꺮àÆ6$@‰Drc¦û0k£æD©¤‚p"¾Ž…Ë€Ÿ9¦ž ‘2©T¨KÄB@Èk„®£!8½Öð'ÆÙ´ÎŒ·~*ªm¸wû6Á—qùŠ¿;|?ùµ¨¤9„ض¾›Jó¡V)]ŽR!CNVZTîQ´^¼ÆºMª‰ŸTW…:‰NzÉX‘˺ “ z ‹—ÑŠ>e‡dSÌ÷m‰HVP$k Òëb:&\ÌK2âú8HPÙáQ«ÕÚHg9A!fèñAAÀ„ôP àDØžù¤ë¸ Æ$CT‹VÅHËP;*þð¯i‡{›P¿Æ$ÃŒ¿ïëFÍ'{àðE6ÃÿÞ®}hjÇþÍ¿œ”æ¢îü«¨m{!âmñŽá€å(XŽb÷'{«]‚êüQ¿3¬õà“<­ yZÓ¤(Ñxý$Î »=›×æiMx$ÿÖi}®~êmÂÝ‹·Üò»aÏhÌÏÁR}R)¬Ž‹>W?ì>’åZVËöŒÚènß°g5?Q?(~rÖó™ i ¬.\~÷äÏì>~öù^Ñ_+sµKPwû3¨2E§0ßô¡ß=‹³WÐ4«Ë†ú®·}ñ5™ ·gçà[e¡Q(AĹiéÈMKGYvδŸ/‚"QH˜™î½^ÀçåÞ%;ñ|Èä¾AÐõ‘¼ü<ûõR)Ó7AÂ’™¦…mÈvÊCYa>j¿Œ®« ?6#££øÝ¡#øÕ¯÷áxó©¨.;IÞ¶zFðe¥&'!'+ ²(Ùºg8™+ùIxPºƒà¨R4ÈX±/³j÷ak7 "…OÙaѺU0U”EöQ!‡Ö[qÀ7î‚ÝÒǺ]I'<$°ìPOg9A!vHx ‚ °Z­ûFã£^K„í™Ozè°w£,½xÁîïaÏ(ª›ŸàUv1[Ê_²Cˆ3CçPúÞ´ÜspNÙ ºù Ö…úáÒ㸊gÛêðl[¶,Z?)?Ä‚4…°ò@õ²9c}×ofÄB©¾è–$ùèëá%ÕChê.¼Š÷Y˾W´‹·íL’©p~´sÚÏŽÛNG=Q€ %úBÔ<µszØ3 «Ëë¸MÐå´ µãPo÷6Åd\5 (*ÁÝË éÍW1›qÎÖ›Ó>)Bô ÂéMœ+s ô —ˆB3Ý{½@ÀÃhš!ŸR‚ "1Ëù W‚ˆ2©9Yièî »ÍÃÛÖáß~Õ—GŸÍþòkøêŸósï-”äðûCG£.9€>E‡¶­ƒÙ”•}oÌHAfZtgØé¼Êþ^nOƒTFÑAÑ û¶BÖÂÉÖK°o¤‰@DŸ²Cnåd¬6GÜÖ˜IŒã¹¤;hÕʨ\Û…€d‚ ‚ˆ-ô( ‚ ÄjµÖF`Hv¯V——™Ë㑊?>Äj†6Ì”ò`÷9ð£ö—xŸE|Ä;†ª÷¿‰Æ/¾=cZÀ~ËÁd‡›iº~ M×O¡¶íTç?ˆš‚Ç*E¤"«%úBÖÇMŸ«Çmá.Ã"Ý® ì…{w|ç_áÜöœ¼‰*É º–ié#È-‹Ö£"kªL•QÙÇ.¿{Rr¸y|ø¦e¨õ—pf¨=fã»)׌ŠŠaÐ$Ó›®B¡! …Èro<³9íèw8Ðn³NJýNGBlïÍDñòü·ňÄ'”Ô%ðþ€ 0u’X© J‰”™±™ ‚ ˆÄA*ã7åA&£(‚ˆ&©ÉIH×i08Þ}w½Nƒ‡¶­ÃÞßÿIëÿYû9ìÚý]üèÙï#UÇî^úÈè(>8ÑŒß>ŠN6ãŠåjL¶!I©Àý[Ö ¬0'*ËS«ÈÉJƒZ]»¬op Žq»uMO…BÄËòš$:á£À²-·¡ííìÚ8Æ=xçX¾~O  H8sñ/²ƒL¥ÀÊ+¡6è#¿ve¤A®Žýy<ôùeÖmâub›•ž%Ù ‚ˆ'Hx ‚ YHÒCÇX72UzÈ% ë-Fuó‚É!~Ôþên{zòûú®tÚ{YÖ™¡s¨n~û7ÿò–ßÕ¶½õñ ¥>Ô5«O(ñ¡¦àq<Úü$ëv WÞ"<ø‚>QlÓ\é ³a÷²w¢½½õ] œ\î4”áÛ+ám]ì^ç-cÖ1ÖÕt‡}á-)(¥úÕHSêPª/BžÖU‰¥ß=ë¸ ýî!Á—E¢!6 šd4É(4dMûù9[zFÑ=ÌH—G†â~[[;®¡µãÞ8xZµ‹—áÎâ<¯È¦YïQ"¥E‚ ‚X(”@Ђ‘õ#•0}]r²Ò léaÙˆ‡¶­ÃÛG?Åúÿê7ûðÁÉfüÿóQüÙöJ,͹õ~íÈè(Úζ£õl;ÚΞ‹©à"I©À¦uùØTš5ùÀ¦E¶!5&ÛÛia/7“§t"zdßVe²;»g ¿mlÅ—·®¥û]" ÓÒÚ_Џ¥N ó}¼È2•I©¢û+ë6%+²ãî8HPÙa¯Õj­¥³œ ‚ˆ'Hx ‚ ˆ(0!=äx&¶g6éÁð£ÛnÁò”¼³o뻢’xpf¨-Cí(ÕÁê²aß•ƒ‚.ï€å(êÎ¿ŠšUMþlØ3*¸Ø1#Þ±„ªów¢¶íô8®²>nNy°ûœÈT¥Ç|›¸-Ãì ǹH‘°ßr˜S;sr.ž*Ú%øúêmâܶD_ˆŠEP‘UŽ4…yÉ&Nû1Ú¸ünXœ½°ºlðü‚//Ö¢C¦F‹+ ±9× U¼aPhÈJh Â1îÁÑSpôÔÀÆâjÜ»‰Râ”NK?žü黬ÓXnFmÐcåƒÛ SE~__"•"9Û š1ºÈ>á!Þ„‡–ªé,'‚ â ‚ "JX­ÖÚ éá‘DØžÙ¤‹Ó “f1’d‰³´ÛaAÍÇÏFmy?û|/^^ÿcü¨ý¥¨,¯¶íTç Zb8›ùTM|¨Î϶Õq:nNyYå y¾7ö5³n£•kðTÑ.$˵‚¯—ó3U‘‚ú Ï£ÊT7ûÁô¡ß=ë¸ ÃžÑ¨,3–¢ƒF¡ÀíÙK±cù*䦥ƒ "e6 ¢ÝfEÏ#Aô;q¹m'Z»q¢µZµ•ë ð•­ÅÈJO¡NAAĉP%~àó²K{˹‚d‚ˆ59YiP*d°„'”æ ;S‡·~Šký£¢Ú–+–«1Op˜mÌ*×@¯ÓDm™©ÉIÈÉJƒL*é¶Ÿ¹xuƒDFq„Ñ‚‹ð0)•åt+FØÇÝ¢“@‘ ©B¥~CŸ÷°n“•žWÇ4ÉA!.Hx ‚ ˆ(bµZ«F#àÒÃùÑN”ê‹~VŸ|#Þè͆ÔiïÁk]¿ŽZÑíˆw µmu¨»ýiQŽH|¨ïú j×îFuþθ<Žj CÝùWXK}®~êmÂÝ‹·Òâ‘>R)¬¶çŒHdšÙhjçt®WçïŒZâM§ýåÚµ»ãFv°û°8­èwF%Í!´ßc%:²°9׌͹fzóDDåx›*A8½´ÛúÐ3<ˆsýÌ¿Noü<ÐqŒ{ðNcÞilÃÆâ<|¹¢8.cÒ ‚ ‚ ˆÄ@&g¾ à‚AF~¸ù£­DH¥ÌkItà‡`‰”Æ”àNVz tÚ$\³ÀFÑk¶!»ÿ²§Ï]Á‘æóhùIJ&ñ@ ©| ÑA©!'+-¦©Sé¼ÚϺMæÊ¥tRÆ!™+—"cÅR °œ ß1îÁOÞ8†çþî>Ä(cwãÉÅ';È5IPéÅó,‹ð`6eÄÍq ²Ã’‚ ˆx†„‚ ‚ˆ2 AzöŒ¢ß=ˆLUâÎF]wþU4]?ÅýM˜T ‡Lû_u¿Õí|ñ«¨][ƒ4¥N´3÷÷8®âÑæ'QÛöêËŸ»„4¥5«ç”òPßõÜe(‹J‚JõEbƒK’@VR&væÜ•õãšîP³ê1ñÿÍtÙ¢šæcÝøÙÅ×£.:h lÎ5ãî…0h’é 34 %ʲsP–sãoíð zF†&EˆË#Cq±-¡Ô‡âåÙøî×·ÒŒxAA,¿ÿÖÂ|™ŒŠò¹"˜L[ ÞY?à0ÿÎtÛU*¹! ÐñK°A­RÀlÊ„}Ü¡ÑqŒØ]ðÏso¿¬0e…98}î >ëìÅÙ.kØËÓ§¨a6ebuþb¬1qäÔ=u!îÇ1I©À¦uùØTšµ*zA™T ƒ^‹Ì4mÌSBØÇݬ ©u¦E¼®ƒ\Eˆ¢IñÃÛqì_~ɺ]kÇ5¼ñÞi|ýž2Ä(òä‹ï¢ëê@d;x–$R)´Yâ’ì–>öçÂòø˜˜Æéò&¢ìp@5áADÙョa6e`cñ2È(ð“7‰Nv€$½R…¸ž¹sIxˆ‡$^§Ë‹ÿõFBÊV«u˜Îr‚ "ž!á ‚ bÄ„ôP  $¶çféÁåwÃâ´"OkJ¸}WÛöëÂôJ™ ÷¬,BºZ‹\° ¾¾E©+`P¥sêο2).ÄÞK Q[¦F¡ÀíÙK±cù*ä¦%n‘¸Ì”qÎÖ‡ž‘A´ÛúpÎf…Ó+¾Bï4¶áDk7j¿¹fS&íH‚ âü>Àë¹µhV*”ITØM$6Á àqÏ<3þlçKÀ(T$±!`ŽÙ`å±FÚñû¥Š®í7Ô*ÅŒâÂ\˜M™œ?‹VÿÙðÂ6ÅÕå/ÉÀEK'…h’¬VÎ(ˆ…N û¢÷Ôœ,:ñ✲o~™SÊÀá?÷·)t?K`Þxïtĉ:BÈ2•I©¢+×À|ãnÖíÄ~ ‡d‡Ë}#‰th“ì@A$ R‚ ‚ˆ)2‚ôÐíº è¶[àò»j‡5ö5co×>Îí7çš‘¡ff±¿mqtd§×ü¿ô6Ž}ñÿ!U‘ºýˆw õ]L!°˜g…¿™½]û°ìÀ]¨m«Ã°g”uû«m{sÛrSî´âÖd¥ ë–¶/Þ‚¬$æjEV9j×îæÔÏ~Ëa@žÖ„}!§>´r ^¸íi<²l'î4”A+×DeŸ=ÛV‡¼wr¢E(å 'lOJ)Äü[â²Ååzû‚>tŒuOŠѤe¨ømüŸÏ_‡Ã'üÃéBCþºl#^Üñ¿ÌºMÉ ñ $;AD| §! ‚ ˆØbµZ‡Fc€F%‰°M!éáéeßA˜b\c’!î·«±¯M×Oqj››ªÇêE‹oùùšEF\°Áî& ãîÅ[Þщ‚úǰßr˜õv°½ÑGÁãx´ùIÖëâð9q¨· OíšüÙqÛG8n;Ã½ÂÆqxÇðl[êο‚šU£¦à1Q¦Ô<†ºó¯`ÄË~†§šŸE•©’ÒDB‡½;j) \„‡¼dSLÇÇâìE·ÃÕ4‡ÐXýìó½øÐv:*Ë»=;;–¢Ð@‘óaÐ$ی͹f€ÍiÇ9[Úm}8g³¢ßéˆú:…"ê¿ûµ­ b„.¹) ‘Ð1I1ÛurÎ× ‘`ø¼‘É!¼n@¥¦ñ$„‡/Ùaòð2 ¥ ü‚#‚Sd-6Ÿ•F±‰~ %&¼}ôÓ˜_v¦eEK±&ßuÁ!D²Z‰¬Œ”¸¦Â%á!ÕÄßýE…:‰.1bÙ–Û‘·ù6t¿ÿ §ö!é¡ö›w‹ºx<žè´ôãÉŸ¾Q2•æû¶ð.&Hr¨ô)¢7ûö fS†(·%Ae‡Õ$;A‰%<A„Hô¤‡Ž±nø‚ñ? bÝ…W8µSÊdØœgžåwr”›„‰"ÎJÊœ,¸¶ûnrM%TçïDª‚Û ¶Ã½Mh¸òÞä÷wîÀSE»ð_[^Á߬üÆd…P„ć¼w¢æã=èvXDuŒ¥)u¨»ýÎÛVõþ7é‚*\Ä»×)êmÊÓÆFx°û8=ØŠŽ±ž¨Ëõ] øŸ§¾ÙaS®u;¾Œ¿ßPA²AÌ‚A“,Šˆ£§.$dÒCpbrpãNÀíf¾&¿¼æµADxų4ƒ=‘ˆï|<ݲ£"øýü:7ã£c7ñ¯u^Àí\Næóи“ùlúœú¬ärN¤ÖÌsÛjÜ»h²Âìþ‹-ÈÎŒþÄ7«ó¸oóüSõ—°û/+°©4?&²ƒZ¥ˆ«D‡›a+<èL‹x]¾DF†W,Ùúý¿‚2™ûyã÷à»?}G&&ò ¸cwã'oƒ#‚Ä™J•VB©Kæ}ý´Æ H¤â,ëºÈ>á¡X„’NOßvÿûáD”*¬Vk åAD¢AÂAAˆ„D–:œ—Ñm·Äõ¶t;,8`9Ê©íæ\3”²ÙƒµrÓÒ‘›ªç}ï2Ü1ùÿaÏèäÿ«L•ÈÕ.aÝ_ý¥ßLþŸ«4ÿçó×qè¦D‡d¹;sîÁÿ»ógø^Ñ.˜“sÝŸ#Þ1¼xáU,;pªÞÿ&ö[ŽˆæX«Î߉}!§¶M×O¡îü«tAå™4û‡‡-Ãí4p3\GO´E]ijÇÃ~{/5ÀávÙ!Ñá[eaÐ$ÓN'Ü,@üð‹÷âë%e¸=;…BÐe=u¯8•ãè÷1"ƒkœ)ÒñûfŽ®q¦ð‡Ä‚ :áLè,¥'*D‚Á· à÷Ó˜³~áú¥÷ÉGÀ?!9Œ^/#.盃¡ÏUî¹?'y¼±µd² ©Øý—ض¾IJá>3ëSÔ(+ÌÁ#÷~{þzªÿì 1“@©!'+ +—âRt˜ÙäÙB‰ ‰…*Eƒ­ß|òªçÞ<†—ö}Hµ¿8Œ®«œÛ‡dµÿg¼Šd ä"=÷Ç,}¬Ûd¥‹/§§o?|ý8c(1 ÉADB#§! ‚ ñ`µZ‡Fc5€F©‰°M!éái|™ªt¤)uq¹\‹ÈÉ:䦥Ïûºòœ<ôÚGááñ©ÝTáÁpOû]MÁãØýÉVý:‡n‡yZj×Öà¿BïøuNëöãö—Ð2ÔŽ§ŠvÝò»»oÁÝ‹· e¨?û|/:í=‚îÛ–£8`9ŠTE ªr*QeÚŽŠEå±=Þn{[ÿø0§¶»?ÙƒŠ¬rN©ÄÌpËŽ±n¸ÐõÇïÆg#¢.:Ø}üìó×qø&ÁŠo4 6çšq÷ŠB’‚GrÓÒ‘›–Ž»—3à9[N_»ŒÓ×® ßéà}yoÿáS|ÖÕ‹ ±±8/î 'Àëæ>Ûm À)€\AÇA ©lþBZˆDƒïÔ¡ „=hÒó9N&"Mî¡\¡ºõØðøÄqÁ«\_€M¥ù8ÛeÅg½8Ûe¨¿ìL̦LdRa^’3±áfdR)² :¤‹d}"ÁÎa&ùÌ•KéÄN0–m¹kÚŽ¶·GÔÏ;mè´  ö¯¶Ç­+^Ú÷!Z;®EÔ‡iK™ ²ƒD*…Ö˜!Ú±s ³nc6‰k{Hv ‚ ˆø„„‚ ‚V«µÅh4V ¥< ¾ä޸܆ýn7·ä™Ãz]²R…͹fü¡ësÞÖyj‘öÍ…ÆU9•¬…ØåjV=¨]»ý§ä¼~‡{›`÷9ðTÑ.$˵3®ÿËëŒC½M¨ïú ú\ý‚îãïövíÃÞ®}€TEJÌŽ·Š¬r<’ÿÀ亰nÿ‡‡Ð}ÿ‡1Œ¦&Š$iJR)ñ†5îð9auÙ`LúÿÙ»÷è¨ê{oüï¹ì¹'™I2™$NHL T …xP!>jÀêOQðwúüN­Ô¶§rŽ=bësôé¢JÑçTXikK+]-Ï:z(õi¹µ]”h4@Bb.Lî™ûõ÷Ç΄„ÜfïÙ{fÏäóZ+KLf÷ÞßÙ{ÏÌžïûû1Ïé×´Aï0>º0¶_é:7›öŠ^ÑaÝüEØPZ£¢70„ˆ¬ÄlA‰Ù‚ÇÊî€ÍiÇÉÖœlk4üðÙ•n|v¥z­ T.FUÅBXÒS$ß7vbBZŸ M¨è²F™ƒ”Ì̃µå2@Aߨ$#ÆŒö¡ “Qßq)ð@"x£ ƒOº¦ðzØÏHãßx}ÒIxiÕ ÊKòP^’—LJ–Ž>tö¡Ó6—ǷLJÎ^ö~­FŠל:¶\®9 é©:˜R´(²fJò9ÍÎHA¦QE’$O©Â [õÌ? ó|#ú._‹ªúæNl~e?¶}k­dÏc©9vöÞ¯¾UùU+ñ…"Q¶O›‘™„¯y#íÜ+<Ý"c“„BHâ¢Ûó„Bˆ%kèáù˯ÁȤ¢*ë΄ÚöƒÇÐæøœór 2Ì0¨"ŸQ%ߘŽf\î³E½ÍE†üI¿ô €/Ð[±&k9N\?Ë©Ý=Wÿ{,ððÿλ¿»z§m5¼·ó´­[ÎmÇó0#a IDAT¥›1?¥`ÊÇ„+>Äjs—ÁíbعôEl?Æk;†|#¨üËߣúîwãz¨øŒó2ÙZnÁÛÙ¼–˜J9Ÿ/Í#­’ <Œ¿ˆöZæ¶áâPKl_?Ý6¼Ò° u ¢®çÎü"l(]L‰³Î€ ¥eØPZ†F[N´µàÃ6á®7—û×`ßáŸwrˆL.T4ÎŒˆ„‚€ŒBâqý ^·0að›y½€Z~£Ú“ŠQH*ô¦U3¸­(·e'üóiЪg1AÅ$×‹ŠƒG…‡T«…Nð$õà/¾þéߢ=ôôàÙŸ~€ÍV¡jùBêØ´tôbÇÛÇ£j#£´P´°ƒR§Ú”*é>ljã¼Ìâ¹’Øv ;B!‰ 0B!5ú¡´rôCjRp\x²á8?ôiBmwuÏǼ–[šcå¼L…5éÚèr«“G¨¹ƒGÃm*|”s»uhut`g½ßzÛSÐ+£ÛÞ{¶œßŽýí‡f|ܺœ5xgÕÏðø­£^g"0ªR±íögx/_7Ј-ç˜0ûË5 ”Å6»]iYÁy™æ‘¶˜lÛc)çejE\n‰yØaû!|óìs¢†îÌ/ÂÎûÆ“å+'…m=¨éldžº±ŸšÎv4Úz@O‰Ù‚'ËWbç}ãÎ|á¿hÜw¸½ø8SUrûž¹T ÿ̳œBH²R(µ– >(öG¥fG3Ö9…!q fØ!Ìë¹QG¥¤‹hïÇäräYŒ(²f&]ØZ>çQáA§¦#I©StX÷ï[ 2Dÿ—ÃåÅŽ·c×ÓÔ±Ó°»Ð0FB‘æ5:$Ò›`zÛ›Øü~ w¡»†TŒà1S?™å³½QKF òä·Óîä~ܤQ…‡¤–’“9VéÁk¾ÂùûÕPw¹;ž~P²KãåÙŸ|À«ÊJ˜ÖlBÁÚ¢mŸÆ” …ZÚ%Wý.7ÜýÜz­ –ôø’8ìðu ;B™K¨Â!„"qÉXéÁpaý¹oãÓ‘&Éok«£mŽÏ9/· ÃÌ{Z=î)Œ®ä¬Ý7ù¦è wxÂÿU©xÈz/ç¶÷\ýï±è­Xb*Å?Mþ®hÀ7Ï>‡=WöÏø¸lÏ—nÆî¿{e<¢'’=+^GÃÿFàÞ+°éãïÅôœá*žC>‡æ‘Ö9÷šë°Ã‘®¢Vu˜—fÂÖÕUغºj,ìÐhëÁ?ªÆ–Ãïc_]MăªÏu¶ãW5gðôá÷PÓÙNopI‰Ù‚¬©¥ÚÙúVl~e?Z:z㾟X æ;˜‡B!„©s 4U'I\¡àÑøÅ€]Ÿ^«¢ŽPšAƒ’ rÍiIvØæ ¹Yfñ<<ø‹ï Ré®|Þ‡Ç^üIÜÃ’Š×Þ>Ž+Ÿ÷ñ^^¡fPü轢䌚 éÏ}8Òν¢uÑ-™qÝæ$;졳›BÈ\BB!$$cèÁpâáó›%z¨å1ˆÖ R#ߘÕzsRR£š\¥`&÷¹ß1éwë­k9·]7Ð86¨]£P#[kÆÆ¼/cmÎAúÜáwbïÕýøÊé§fíÿù)عô¼±ôò“òü/Ð[±gEtbzhµs<”ÜoâU©1¸O~çœ =Ä2ì`÷;°µ~^m؇ß)xû:†Á·ËWâG÷<€3;ƒšÓ瞺OðòÉc8E`¡×éÀUc_Ý'ô‡‘èž,_‰ÇÊÊo»§Ïþôƒ¸aËÁ<„BI^4À›$Ü1+Ò7Û2™¸a "ò磵C`èi u¼ ZЬ(ÈI‡Š™/J\\§Z³è@™#¡‡ŒóiÏáòâÙŸ~€cg/Íù¾=vöþE?°a‡*Q«/èÌ&Èà͈½ƒGàÁš·í¥°!„’\èÖ !„’ ’1ô0â·ãž¿>†w»þ ÙmäxÈIf ö‚ 3ïÐC¶fr… 0w`âìAë­U¼Ú?Ø~lìßz+àùÒÍ‚… ÇÝ‹gÎoÇ–óÛg}–˜Jñëå¯â¹ÒͰh2“îü_o­Âã…¢j#V¡‡êës^f¾¡ !ž^Uìmsâ5ªÕѳ°Cí@¾rú;8m«¥ýGJã'÷=2áúÛ6Øÿýç?àHóEÁÖs¤ù"~Ys†Þà"¢uóK°4Ç*x»á/Œãz…įî0¶.ÁK„B‰ 'bS(™í2 õm" Ƹ2]À(är =D!t(²f US‡Ìt}ÒÒq6—ˆzØñöqì;T3gû´¥£;Þ>UÖ5åКMâçª{ˆÍÕÇ}˜B‘5>ßÛRØBI>të‘BI £¡‡-ɶ_O7¼$ÙÐCíÀgœ—ÉOî¦×‚ 3[r9/—®2Nùû›«<U©xÈz/çö÷\ýï±kjXuÙ„=@Ý@CÄÁ‡u9kðΪ7“2ø°sé‹(3•DÕÆÞ+°åÜvÉ3óS¸WçPÊb?ãW¥eçeæB…‡n·We>ö\ÙgÎo¥ªÃ²Ü<ì¼ïal(-ƒŽ¹1SSÛ`?^>y ½N‡àëü°­…*="²ï­¼ r™ðC¢âzˆu!¤ãˆBIV ?ZSàˆM&'W–ËØ I\± „__(dõÔùÍõ COÿdVêüÅ÷±ðþ/ Öæ¾Ã5x-ÊAÿ‰Èîò`Ûî£Qµ‘S±_(mer9t"†)ïÓönÎËXÒSb¾v „B’Ýz$„BÌè‡Ø¯'Û~I5ô0èæ¼L†NØ/:¬©FÁÚ²O1Hx½u-çvêÑê¸1À¹À`…FÁ~A!Fè]'¿àC‘!?fÇ‹Q•*jÛWïFÝÁŸ\úÍ„çNhÕ=Ü+%âS(… (È04¹|B‹WEºP0hÕ0hUô$D€*:°ºû¸2‹çÑ4©St¸ëÿˆòo>,X›:{ Ïþ$ö÷±âiÛ[G£ e”"gÅbQ·QcJ…œIœä%Ÿ e rcºv „B’!„D¡‡Ø™m`ýT *éÞ°Ÿ*À±ÞZÅ«­ƒíÇÆþ­”)q›±xìÿŸ/ÝŒ y÷‰²\ƒ¿^þ*ÞXúV™ËEíÛ|ý-Xb*uz+®Þu;;/þF´óeÈÇýæñü”‚„¸Fñy~ëx\C…;àáuäê”í|óìsh!<òHÉbüèžPb¶Lú[,ÂaëèÍ !"z¸d1Ô"¾s¸¼ØöÖѤî¿Ux „B’š`Çe€œñ}ßbÿIÖðªJ(™(Y* U&!<Ï·ÑÿZ2R¨3fžªCIeÎá«ü›ã®| *ƒNöê›;çLèaß¡Ô7wò^^k6ÁZ)î÷Š µ šŒ´„êWw?·Àƒ>ÆÁ@ ;B!ÉnáB! ŠB±Áuðö­ÆtI÷ïT£*YïåÜÖž«ÿ=áÿ J=æ§Ü¨¦ðTñãx®t³hû>|åôS8ÒubÆÇ.1•âåÅÏâ?Wý ߺM¦àÛ³sÙ‹1y+-ømÅŽ¨Ú¨øL”mã3ø½ŒgHÄÀèb~þU©È×ßÂy¹æ‘Ö¤|útèü"O£·çÊ~ü þu8¦¨N³;ï{JË c&ßp·9í1 ;@£­mƒýôæ†Ýfo&±úæNì;TCL!„„¤T SåAE“œÇE0x½€Û ¸]€Ç3ñÇíbÿæõÆo&|±0  Vó;~år@­¥°‰âÜ †´j˜zêñ¯ŒÙ)¸­(y#T ¥á¤&P'$…÷߉ñ}d,¦ÚÇ•Ïû’>ôPw¹ûó¿W§P3(zp jqßàê²L Õ¯MÜ'¥*º%3fÛרÖKaB!$ÉÑmB!$QèA‚o®à[2»ß1éwë­k9·S7Ð8)@aÕå [kûÿu9k°ûï^%`ÖãîÅ« »" >dkÌØT¸ï¬z»ÿîlÈ»Om{¼pïJ|l*܈§>!¹c«úúGœ—YbäxPÊâSâ—O•‡ÚÁä«òÐ<Ò »Ï)Zûv¿[ÎoÇÞ«ûmWÇ0x¬¬[WWÁ¬3Lû¸Ÿ©ŽYØ!ìd[ ½±™#ì^ÏØÏu‡]#ÃÓþ„çòù¨ã¢”Ÿ&îˆû× §$)ûNFw !„¤&“EVP*©ºC¬…B€Ç͆þ³ÍOùX°ñxØe’©êƒ\h´lðA©䳄ä£Ç»Z#LЇHãˆËzÇ}NÊ5§A«f’£CCl•¿P½nü£ÿÁu#Í AAN:J ,°¤§@A‰¢IZ:z9/Ãh5‚o‡Ïå¦'#ÁdÏÿø> V/¤½d=Ø]lÛ}$ª6 ¬„*Õ êvªR PŠp~‹ÉÏãÚQd͈ɶ}Xw ÿ¶Â„BH²SRB!‰­»»{Ovv6ü6™öë醗Ÿó@Bm·\„oÊRÔÜK=Ï4ÀzÐ; ƒrâ¬Së­U¼’3;ŽaSáÆ ¿[”Z»ß16z~J~½üU¼Ò° §mâÍ~>¼Ù´ó¾Œóî›´ŸãÍO)ÀS)xªøqt»møl¨ Wí×ðéPê#Zg¾þì\öbLÃa;—½€Aßö^9 ™ã¿ºçcÎËŒ¯ )"~åÏ—˜¾€ßwü‰Ó2Í#mH&½ž~t8»Ek¿y¤[ëw ÇÝ+h»ËróðXYùŒAøeÍ\ˆy¿¶ÅaD`.¿NŸ`#^ÏØï¸ê²ßø·Q£E¡)ƒ:XÂ^Ûw;ž~Pôõ(ä€?†ûEÁ!„ä'W°ƒÀ½^ïM”CÕb*¼î™C3-ëq*MrU7+n |…Øý ÏÀ°û*—Ó{Ûd%“Å>Èsó¡TdÍ@ãÕëŒ?ð¤nܹ M߇ã‹È€Rq£TŒf£©z Urˆ€ÃÅý…6Õj™³ý5ÒÕ Ïˆ™Åóèà NÑaÝ¿oAͯßGͯߺ½pèaÇÓ U'M?m{ë(¯s-̺¦)"Ÿw2¹<áª;ÀH{çebql}Xw oýßsÉxÚSØB¹ !„$@¡é¸ÜgÃêü"AÛ4¨„½4è†U—3áwFU*²ÞËy ÷ÁŽ£“; ~í@ÃXèÁ ÔãåÅÏâH× ¼Ù´¿x3Ã;üN콺ûÛE|ØÊÙ3”9«±ÄT ƒRÚ z‡Ñêè@«£cb1©Xb*E¥¥"®ÇÛžŠ×€sèOEÙ´::Ðæøœór|*&Ä3ðP™Ur\¦y¤5i®Ëþ‡Å«D Æ5BÇ0øvù*”çæÍúØšÎv|§J ¶Ä2Œx<ñzà øá?@È×o· ƒnŒ-u¼DÕ7w¢îr'Ê䊺žXÏ`ª I9 !„9A¡Ô2Àë‰l ½ €’aHìDv m#ÙBcǦ P(Ø27Èl%‚˜cS|.SÈå(²f ¥£/!BÁ Àc^€?$G¦I‹¬t]òT¶ ’áqâ»GQÿÎQxí7î/¼ÿKX¹å«P§èæ|•óaä.]„#ÿú“ }ÄG²…öªA}s'ïå3J ‘µt‘èÛ©ÍHƒ,ß„ùyTY,ò}R ;B!s !„$‘Ì¡‡OGšðRñwã²þ2SIijý‡¹|>h™øÞèïv]Ÿöoƒ¾á)_™µ‚sàá÷ wFUêÄ7™2%–˜JQÓwîÀ`ërÖ`‰©o6íµÚ09ø0U0ãfþ`µ Xb*å5?¸†Ò˜¬Ï¾"ŸêE†üYÃ(S‰kàGÈ¥Å.n…ÃýKž›C<‘º8ÔPœÁÜo6íÅö¶Yb¶àÛå+g­ê6§¿ª9Mo(ÈŒ¼?F¼ØÇ˜®? Ò“ÀSMg{LÖ³ïP ÊbQåA›=S ä!„BHò’+µðûÙ÷SÍö-“±ïE”Jš-?ÖB¡èÃcmmK­¥ç‘$>EŒòiFXhÕLB„‚ö:ÏiŸår¤ê5HÕiª×°ýN×"0ψüÓ¿¡ïòµI»ôÇS¸zâ<Ý÷2Rr2ç|_å.-Á£û^Æ‘Ý9eq‘,¡‡ºËØw˜ÿ÷ŽZ³ ÖÊrÑ·S©Ó@mJMÈ>¶·s¯ümЊW .‰Ã{)ì@!„LóÙ”º€BI£~¿žlûµ»ý]üKÃö¸¬ÛÈp¿é4äv ¾éZnš{ܽÓþÍ `Ð;9ôÀwüÁŽcSþ^)Sâ6c1”7’ËÖ˜ñòâgñÒâïÁ¢ÿÆt8øð•ÓOáH׉Y=ôzúæÙSñ:/ÜÑcw.{z«àÛP}ý#ÎËð •Ä3ð°A(®jDÛžù†ÎËð <ôzúÑë|ûí~¶Öï4ì clš±¥fOÅëxcé Óþ=IÁo+vDTé‚>ø Êø–­æ³ÝÍöÖ„¾ûC~\n¼]»ß-ç¶ Zñe^š [WWaÝüȯg¿¬9Õ`è2S þvß!\½[=JK¯À‘oÀÆÞž¸Æ®y*5Š3Ì(Î0C!§Û8\8}^h¨Ã¯jÎp^V«TBdzb×±³—Dß7¹ûp¹Y*‚A `å…ü>vÀ!d²Pˆ=?Â?¡õ !„ÇûÙèûŽÑ 9Ä÷º.Æ öÓUò $ÑĪà²\>{<…\Žâyf˜ziuRhö°ƒ^£BNFŠçY°pž9iÐÏ0ã{ÀÏ~N#‰Çç”ÞÀöúwŽÎú˜Î¿]DoÓ5zG©StX÷ï[°ðþ/EÝV8ôˆv¼}ÿVþÚ•ÐÆ ˆ 1¥Æ$T!w?·Àƒ%=E”í °!„2wÑ7å„BH¢ÐƒpøÌ„ßÒß‹®‘aA·#EÍ}Fûn×ôƒõ§«^°éÖG9¯ç`û±ÿ®”)±ÄT:eèÁ ÔcSáFüzù«X›³&&Ïi‹½ ϜߎWvÁîwÌøØ‹C-¼f—-‹žÀÕ‡Ná¥/`MÖr¬ÉZ>6ã|ëC§E ;´::Ðæøœór|JY|G_.1~ó2bVxˆ…‹C-ð <¢´y¤_9ý´ØÛksÝüEøÑ= ߘñ2'ÛZðaÿ0G™©Õw¿;éxž*XFKëà@ÜB¡Õᶬlg˜aP©é áÀæ´c_Ý'xúð{x¯±žW¥Y9øvù*^Ëþéì%ôôˆ¾Ÿ*5J £Žÿ Æ@€1Ôí J~³œUñ9ŸW…‡‘VѶ‡Oõ.Õ½ÃSž·Ñ8eû¯4ì‚Ãï¤=Ãà»+îB‰ÙÂi¹“m-¼f~KcRppõî)ɺFÎûm=“¶›KÀ$Q¸|>ؽ±ŸéN«d¡Ó#C«£j<ÏÃÍ8×Ùu[å¹y(ÏÍòÜ<^í®»ŠGîZ,êþÊdl(AŒCU¡q“p”°?rÀ¨fŸu•dãó0ÓiÕ×ï”LìfB&DlÁÑ @Áàtã^ã†*Ä#f;è%$•ðº H7>¡pƒVâyfô:`p 0í‹•ÈBì{A½F½V Š^«äÞC(Ä^GôlV-Ÿ÷R' ó|#e-uÄMÞ'àøK»£jçO£•Kÿ×Wï’þ9ÕÑ‹_¾Çÿþ¾Öl‚µ²<&ÛªË2A–À÷{]}ƒœ—±dxxëƒsø°>)+¼PØB‰!„$F¡‡èè­(3•p¸:äqããŽ6¬Îf|ŠJØ ;ûx¶Â,Èþì8:cà`C·¢y¤Îî)ëàƒÃïÄê_Ç*s9ž/Ý<íün— ‹ã^]@ŠøT/à]ÝA£ù¡zܽ°û¼C3±ûœ¢öÿÅáA·÷H× ¼Ú°K°öJÌ|wE%t ·RÐцààêݼ*MEìÐ@Û`?œã¦ò³9í°9ìÓØ;1Ì`sØÑëtDü<”çæau~ççBŠ¡Ø @P)0j´ÈÐê¡¥‘§¼ØœvüªæÌ¤@_:†Ay.;³Ûceå¼ïW_=ð°JTªÑp€Pm*Ù6ã!b÷%š}Á;›½RÉIv¡¿}~;H\¥¦ß$±…«7Dü>Ïýìk„‚>Þ“ºÖ‹Y¡$Ü>½D'“*8¡•Šÿ€~…\Kz 2ú˜´jfìG¥PQˆwÏ‘±;¥SÃïrC¢“Ìöd,Èø±ç/ÒÁ4…÷ß •A‡ã/편bÆTþtöÊä¢jùBéžO.OTÕ(jEƦò¼R§*ÕÐÇ–ßåæ¼L¶€(ì@!„€„BHÒKæÐÃg#MxoÙ.¤)SD]צ[Å3Û9/w¹ zàSáa¶Yß{ÝýÈÖ˜'ý¾2kçÀÃûíG±sé‹B=©rÄxóS ``ôhi…?8õh¶XNÛjð•ÓßÁË‹Ÿv ø wµ X”Z$Ê õDV}ý#ÎËð ïã½|þÚ•1 !è̦„?®FÚ¹O4£× 3 …!„F·r !„9 YCŸÙ/ã‘s›E=l*܈gÎo絬P¡‡ ÷™vìþ™odözàù'U,ØT¸?¹ôNëöÙñfÓ^|É|cà±Q•Êþ0ìÇËÖ˜aPêpq¸eÆÙéÇŽtžÀþöCpø¢<Ï¿Ϝߎ.þ6æ}yê>õ9Q;Ѐ%¦R =ŒÃ5 DxƬSKL_àx;B«øŒó27Ÿ“Sqý>åòøÏòîõ? kpt08…H²â¼ Øk ú& wüó ;Ü܆Zξ"e¡$Y!±"“±Ÿo‚Àçã?Ð_¡`+‰ñY"=U‡ôTö>§ÝåÃå…ËãC „Ëã«¡Ë¡U³oÔ´j …ŒR£€J©„Љ,‰!vp*D"€Üe‹pé§"zlçùF <Ì ³xüÅ÷£=lÛ};þåÁ˜ÝߊTÝåN¼_}÷òY_\cQ^L¶U•j€B=7K qÜPØB!ãÑm\B!dŽèîîÞàëɶ_áÐÃD´uU©x¼pïå/÷Ùp2ÊÁx|f®h˜õ1½žÉ3€/1•"_ çõÕÞ´¾Aï0Zí¨hÀ)Û'¸8Ü‚^OÿØß J=ÊÓê˞µíl› 7âU?Ãã·n„^ÄAï?oú^™az0€š¾ ¢ÎªŸHj#8ÎnfÑdòŒ(åÒ‰Å'¸0[Õ>Žt@»W”í¿8,Ü b!ÃóÒLغºjÖ°C£­Gšñãªñ­ÞÁU 20:IÁÁÕ»g}\Þ:§¯ ¶µà—5gvûµ Ã;˜`P©‘¥7 ßhBI¦Ks¬(Î0#'%•Â#ìJËÆª;„™u”˜-œÛ:vöRÌûE¡dƒ- RV˜fpŽ\(•ìcÕqD3i6ÁÛ>!É&às„$âñmØ!Ìë¦AšDú‚ÁäX!±&W°ŸuÔ¶’\ÎVl˜‰LÆ~ŽÒhc 7hÕ°¤§  'EÖLÜV”²¹([‹ÛвQdÍD‘5¹æ4XÒSžªƒA«Ž8ìP¨I*Ь’ÙψSrý“»4òбŸòP³‡Tþß§9\^¼ööqØ]Éì—ÝåÁ¶ÝGx/¯5›`­,ɶÊärè²LIq<Ù;zbº>§Û‡ÿ³ïC ;B!dâç|êB!dî ÐÛn&ªå/÷Ùð~c=¼Q|#ŸmHåþœÏ2(¿×Ý?åï×[×r^×)Û'ÓþÍ  ÛeçƒMcá»ß˜ŸR€%¦Rh³5(õØT¸Xó|ØÖ‚š®XQdÊ€VÉLù7•B1lÈ1¤¢8Ì۲²Ç ÖT#2´zhfN<×`}.® ôá|W>½ÞÕ{žÙh¨%ì°,7ëæO}äS­ëÊç}qûB8ïÃŽ·KfŸv¼}¿ 5ƒ¢×Äl[µi%É›,¿“û‡¦²¹¼Ö;\lëMÆÓ’„BH4Ÿm© !„¹%ÙCŸŽ4‰Ò~ÞŠoßUý.'Þo¼€>—ƒ×òj…‚ûóíš%ðà€?4y@â¦Âœ×ÕãîEóHë¬ ‡jú.àãÞ¿-Sžq{DÕÂÖå¬Á;«Þ-øÐboÖsÛg =´Ú;?ñ }dk̼×I0&ø~§`•Aì~¶œÛ‡ŸûL\•Y³Äê¸2ìp÷­ °:¿‡/³Uþ÷Ÿÿ€8°ßÿËñÆGÕx¯±ç:ÛÑëtˆö¼¿xûTZ*"zìzk½éð«šÓpú¼ ¹í ¹%f Š3Ìc?አ·e匕øTcJt.ŸmƒýøÔÖ¶Á º]o €>§83¶ öã½ÆzÁÛ—f“å+§ýûlUe¦S¹S2Ï—\ÎÎp*µ;Þ]"ôRDÈ”‚Aág¤§™½øx°Ÿ½¦zÜìÏǾ’­z×ó`H¸Š„ˆA–$ë Drç–ŒýŒþ‘%ñ‰ “‹ß—$±¼Òûœ’“‰”ìȾëñ¹<誻„¾¦kz˜EîÒÜõƒoEÕÆ™úV¼w¼>îûr¦þ*ÎÔ·ò^Þº¦ªTCL¶UÎ(¡6¥&Íqä‹Ñ¤.á°Ãµž¡d<)ì@!„Dû‹º€B™{’:ôp^¼ÐÖ…O _KTmؽjjÀå>ÓuzîÏuƒ«{=“~·ÄTŠ4&…óúŽpØìxÐáìFí@>îýÜ2ÕÜÊ»Š|ˆ$ôÐí²¡v aÊàÈ\P;À½t4Ÿ°@˜T°&k9çe" ÍÞç øæÙçÐboãµülö[p¢¿y-dØþrõò„`õ¡˜>ßnÀ6Á·%¦R^ÇH²qú| ]å *õØÏ\©Ø0›>—M}64öö ÏåD †£d÷Õ×Þæ¼4~°¦ :F5íctŒ Ëx„Z:ú耙A,âÒ ö$™„D¸ìéüH(Ánp9ðzØj9?^ Ùÿ÷z·‹ @$Àþ`@¼pŽÏGÇ‘.™<9ÖA‰±'—+¨îó¸Wšo~r—-Šø±}M×às¹)ô…÷߉5ÿû‰¨Úøå{gÐÒ¿÷í.^‹¢ÒDFi!2¾P³íÕgg$Õ1dïèáôxK:÷ï˜)ì@!„Y?{RB!sS²††ývÑBFU*®Þu;Þ@'ÛZðqG+§åRTÜzÏVá:œ]Sþ~}÷ÙÉOÙ>áÝ/þ`½ž)‘þ¹økÐ+u‚=ïáÐÃL½Ãs6ô0èæ¼Ìü”|Þç ”TZVp^¦y¤÷úì~ÞlÚ‹gÎoG›ß iLÊŒ•ØRWÔ}#tØ!ÞÊL%عôEÎËm»ýzÃàd[ uB’ès9ðéõ.´ Àî>¥U2ÈÒ ?[Z£­¶AÛ\7~tÏ3†ÂøTy¨“P…)òÇx|ÍÞM’E0D}0W…B£<Ü®iÁàhõWb‡[üqû–ÆÉ©’ÉÄ=]ìö !ñ'vQJós…ßüä.-‰ø±]uì÷€>—#]½ô„΢ä¡J”ó¾ŒwVý ߺQ°6[ìmx¥a׬ý8Cµ œ—1(õ¼Öe0È"Ôõ€s rï¯n· o6íÅWNÚGµÍ[}cækÆH+üQŽ€JưCõÝïò ÜTZ*ðôÂ'0× =@ÄÞø ƒw–/à *5Š3ÌPˆ0}ä Ã3™:=¶®®ÂcewD¼L‰ÙÂý=Äçô…ûtB¡ØÚ¦ìI² 1©sSÀ?Xˆ¢ÂA0Ć%µšAPäÛx R&æ`b¨LÈxÿ(¯ÊƒLF×1 wˆw_ÍçôHn¹ú._û·ãz?<#N:`fQþõÈ«¸÷ò=ý#ØE•¾Þ;^úfþ“Š=X …Z³íÕ[’«ºƒßåµý¶ž!üÛï’6ìð ;B!¡À!„2Ç%{èá°MøA·› 7âñ Âô¿}ï7^@ŸË1ëc3tÜ{Ûý‘ÝàjF÷õÖ*¤1ÜKŽîr@¶ J=6nÄ®úÊx JŸÊÑ®Øß~hæþžƒ¡‡!ß§Çòy¯K£PKjßù" Ùýé:­õ;ð?Gƒt_ؤ1)Ø2ÃàûAï0ï +a/ú3 ;Üdç²Pf*Á\gsÚéMWt»":@†V'ZØmÝQ·1/Í„o—¯ÄOî{„s€Á¬3@Ç0œ–á;óÜ\ðAÑ &D*ĸÌÊè›IóûØ !!Ûó°á³„yÝ ·ÿRzm"$RJ&1Û&„H‡J-NpV¥¢¾“OäÆR“’“‰TkVÄï®»1ñUy˜Æ˜‚/~íN}|³3õ­8vöR̶¹§û×ð^>§b1´fSìúØ” 9£Lªãf¤{ðÊ’Ù÷Ëá°ƒÓãKÆSîëÝÝÝ[èÊC!„‡¾Æ „BHR‡¾^ÿ¯x·ë‚·½§âuÁBv¯‡šÐ6Ø?ããT<êNG:¸zºAÎëóª8¯ó”íÉÙ3v.}ÿ\ü5è¨ðó¦ßÍÚ§sµÒC¤ Œžÿ²J½¤ö¥@oå røèvÛ¦¾»mØß~[ÎoÇÿsâxµaNÛjÛÞ-‹¾1ãÀý‹ÃÑÍœþs/â/=§“æX}¼pCÔa‡°ê»ßÅš¬åsúÜïu8@‡ÝëASŸ Wú" :€5Õˆ|cºhÛäôyÑëäÝ™_„­««ð£{Àêü"ÞíðÙǺËtPM!§A¶! <$ W?XMAßHR(ÄĨÈÞD?ŠM"U2 aìœBɶM™×Fàp‚Rɾ7%‘)²JkÖw¯]z<#Nd.ˆ|Ò¤Þ¦kãöÇAUf{ÝW1`´¬zæ MOãÝή§ÑÓ?“m~mßqÞŠhÍ&ä¬X»ë¬\MFh²Ógÿîl„öБ@!„‹¾Æ „B€ä =ÀÓ /I>ôà ðç+M8ßÕ1ããÒµÜíG:#¼;à™rözëZÎûâð;%z€y_Ư—¿Uu°­õ;`÷Ï<àÒîsââPKÒ_7ª{>漌!Šà‰ÑI®øTy¨hûwóH+ö·Â7Ï>‡ÿyú;øyÓïP7îïB)3•`ÛíÓO$Óêè€;À¿„ù?}²Ÿ^JŠã:IÁK_ÀžŠ× ;€Q•Šê{ÞÅ‹·Ód>DÚÁ ÚûÑÔgƒÝÙ5A!—£Ð”,½AÔmkäWfYnvÞ÷0ž,_ɹ¢ÃTJ2-t $^Áƒ f%IB.ð W¨&Áëdˆ $DÌÒC€/AŠ(°FFØÀ›l´MBÈÜ¡P W‘A¡>@‘ìôZîŒ}NϜꣀׇÌây?¾k\…p\ï§mºŒ40Z þîÉ `´üªj;\^¼¶ï¸èÛzìì%Ô7ó›HD¡fPôàš˜ö­6# 29 Ëću×(ì@!„Îè!„BÆPè;!C𷮜lkwš |ª<Ì68ìùwMx¨â<ƒ=œpVz¡dkÌøõòW±6'ºœ=î^ì¿vxÖÇõz"®°1—Ì7ðZN£PC)“^àJË ÎËÔô×ãͦ½øÊé§ð­¿>Ÿ7ý-ö6Ѷ1IÁÁÕ»g¼F´Ú;x·ÿVó¢q¸9)ŽÏ‡¬÷¢öˇ±eÑ¢´¿íö-¸úÐ)A_7ÄÆç5`*ùFˆ´]wØñ©­}®ÈgÂÓ*”dfÁ¨ÑJrŸJÌ|wE%Ì:áÂz#Cê©ÂÔBTá¨9èUI³{KN0x\± iùýìú¤ŽQBØkµJ#Ìù Ãh[tr2ç(”€:Êó_)`p‚Ìl¨£G´¶¥X !àõ!»¬8âÇ»ú‡àìû÷Ð^83ÐÙû½iV n{ôÞíÔ7wâ½ãõ¢m§ÝåÁ®ü+JçT”A•jˆY¿Ê%Ô¦Ô¤~3y5´FuÅ[¾þ¬·®Å–EOLû˜Þ²è‰±jµ hut v aÖe ôÖIÛÌ%¬1~]Õ=¡ÕÑ6Çç¢ík‰9›.bÑ1<È©¢¨ ÓEO"„šÎv Žx9eÅz›Ú&\}Cx˜….# ^;[þ¶GïÁPG†;®snÇáòâµ}DZãéݾ3õWq¦¾•ßuMÍ `튘ö§R§cÐÑ5‹#mÆ»¬»GaB!$ﻨ !„2•ÑÐà €=Ò’ißÄ =¬·V¡úžw±éãï¡n 1êö¼55 ÂZ€±©Ðëé‡?”¥ìÆÛÄJKòõ·p {Êö ïÀCí@ŽtÀ)Û'pø§ˆ¹Ê\Žy_æ5è<ÚÐÃÑ®ØT¸qÖê þ`‡[$?0^ê JiÞ,6ªRybKcRP}Ï»3w½ž~t8»yµß<ÒŠ-ç·'Üq´&k9*-+°ÞZ%¹sr‰©KL¥Xo­¬ÍAï0jØŸqUb­<7.bÓçr cxŽ££²ôäR¡ˆÃ(Õ|#÷YÒzœlkÁêü¢¸ö·Ý塃n ¡9¶^BÄ¢P²Á} Èá—ËF-|ð <`Új*Àï­P¡¢s®O¡ø„Æ>»•”2öGÌJ=t<’D"“j {Ýõ€ ú÷92rš•éë­‚^¥ÄÕ?$jû¯ •ôJ¾eÏ‹ø±Ýõ—ás¹Áh5£ŸIpö A—‘FÐ44FÐÆþ›Ñjðů=€3oü|<îaÕ7wâØÙK¨Z¾Pm³»8‡ë¯%ëîQØB‰ <B!dZÝÝݳ³³+T# Cíî.ìYüïHS Wju‰©Õw³¡‡ßwü)êö¼N¶µÀðã Y9P+¸ËP;ÐñÀ^0€g÷¤™Ã×[×â'—~Ãi½§m5èvÛf Œg÷;ðJÃ.œ¶ÕD¼ŽÓ¶”™JñÔ‚¯a~J§mŒ6ôi•‡Aï0:œ]°êrè“”+d,1•J*ðPf*ÁžŠ×g<ïÝ.·ðjßîw`kýŽÃHR°&k9–˜¾€½KL¥œ* $¢XWmˆ”Ža(ð !Þ€­ƒ°{¹}y)…ª:F…L½N§å4Ô¡<7:F%Èv”˜¹ÏØÒÑG‰Úø™Verš•™Lžé;à|¾™„Ëd€R (EÓäó±a†Hi‡Ý†¡çü~qóÏ&è ’v)l?‰Ù>! ÷  +©&¾_»îÓûB‘Kz zú¹U ?˜_ð÷ iü£U'Ò¬hÓÓ"|ô5]CvYñØÿSàa¶÷ hÒRàëï…÷߉O÷ÿ™W{»œÆÊÅ0h£¿w¸ïP .~IpƒÕ‚¬¥‹bÚ—JJ‘ÎO©pGYi†Â„Bì½u!„BfÒÝÝ]›¬¡‡Îã‘s›ñÞ²]‚†ŒªT\½Û.ìÄ/ì¤Í;ÚÐçrbAºø•:œ]“› 7r<À‘N¶ B$Â3ÆóD]7Ѐoýõyvû̓ e2vf(NÆS(ÙŸ`ñ{üa#;(V¬Ë¹ŸCØáæåd'€‘HB! à‹ó6€=v$ð’?-%#^àA&£ÀI|ãgf'„"M|Cíבɡâ^»êéTWnܳÎ)+Æ•ãŸD´\W]Ó„Àƒ×îlõ ©Ðo ðÜÞ¦6t×_æÜ–ÃåžC5ؼaUTÛTw¹ïW_à÷yXÍ `튘÷£Þ’‘ôÇŠ»oó2e rátûðö±z ;B!D04ÚŠB!³JæÐÃgöË¢„`Ûí[°ÄTŠM}C¾‘¨Û»ÜgCÇYùð“*3,1•"_ çÙÂtUGxˆ&ì0Þ« »P;ÐQÕ…ñž*þj>ã5ˆû”ílÌûrDýÚ<Ò†E©EtQáȨJ•Ü6U÷|ŒmÞÌ@ó5Y˱sÙ‹UsiiƒÝÇï\{¥aZìmqÙÇ2SÉhµ†/̉` ݪ ‘š—f†Ò2ºˆÅ™ÝëAûÐ \G¡TjMP)¤së¨<7sàÎu¶ãHs#ÖÍ/‰zÚû9/cÐQè‡p ^7 Mÿw¿‘]¥¡™›ÉDryl­ƒl8‡/ŸOÜ0F"¸9 ·kO€„Ÿ™l4ô B8„2º„B‰>÷\ýƒÄ <ŒHMfñ¼ˆÝuM“~g¿Þ4«…ºihŒà¦[ý_|üüyë/àsy8·÷~õT-_ˆ"k&ïmÚuà4ïes*Ê J5Ä´U©Èv7§Û‡ÿ³ïC\ëJÖ]¤°!„ô΋B!IöÐÃÝg¿Š=‹_Ãm)Å‚¶½ÞZ…ê{ÞÅú“ßdj¿Ë“>iµwLªZ°eá7ðÌùíœÚéq÷â”í|É|Ç´*ìv´ëæ§äGB3(õx¾ôŸ8ïì¿v(âuu»l(Ð[¡Q¨é¢ÂA)¦¤t(3•`ÛíÏ`½µ*¢Çw8»Ðí²ñZ×þöC¼*¡ðÙ§p°a‰©tôߥI}Œ'ZÕ†HèO–¯¤ X‚AtÙ‡qÝaç´œJ¡€5Õ£F+¹}*ÏÍC¦N^Ð}u5Ð1*¬Î.xèä1¢·è–L: § —³ƒ¤c¾Þ Ìv/8ŒPké˜"ñãó Ó†Z3‡_·%2Î,¤>9¼RɆ½‚&D ªî@!„Ø(º%gê[9-ãìoÀ°Ïé‘l_e—ƒÑª#|ïsyÐÛtmB% Wßf W( I›XåÑjðů=€¿þê¯~ìñk IDAT6w8ƒO?ÈkÙ}‡jpåó>^ˬd-]ó>Ôf¤Ñ4™L–Ìa‡![(ì@!„Ä!„±d=t¸»ñÈùÍxoé.ÁCKL¥¨½ï0ÖŸüV `u<“ª<¬Ï«â8ÒubÆÀÃ+ » ;„ý¼éw˜o(à4Hz‰©«Ìå8m«á´®w/šGZ1?¥ ¢Ç_nIúÁÛB“B@DjA‡‡¬÷bËÂo ÒRñ2½ž~4ð«ÎP;ЀŸ7ýNÐ}È×ß‚½•–s&ØîËD®Ú Ã`ëê*äÓé'ƒnÚ†à0š\!—#Kg@–Þ…„§Ø^_„÷ëy-û«š3cmøS(âxH€­Žyƒ!vÝé%q s.ƒl[sµZ‰„'Ö•™Œ­lãq SC.£ê„B‰K÷JãCí=¢mßå–te,˜‡îúË=¶»®iBà!ÀÙ7 JŸ–Æ81ð°A“ìÅ "î÷ñê›;qìì%T-_Èi¹žþ¼WÍï~ŸBÍ `튘÷Uw˜æùP(`4“9ìPÙÝÝ]KÏ4!„ôî‹B!œ$sèaØo-ô`T¥¢úžw±éãïaï• Ñ7Wy(Ð[ñõ^ü¾ãOœÚ9m«™žÛse?Zìm¢lÿ+ ¿À¯—¿ ƒRñ2O?Î9𵃠½Ã°ûœ¶k®‹g_µ::°íÂ’8oËL%Øtë£XŸW…½•Ó²v¿‡[ø]÷Ý6l­ßÝ5IÅÓ‹ž@Þ:r¨˜3Çoí@vCuÏG _µ!óÒLøîÊJ˜uØóüh€ÝËmv>£F kjT 0Zú¾%8ÒÜÈ«ÒÀ†œ>/ÖÍ/áµ¼CˆiÌ vp>ϧ1*JFú}ç_|> <øø…m+ÎQ¡Å#ü•衯‡ Êð~-R°a‡¹´!„BHìYÒ¹"©pÀû½h €` ¹DÊ]1Z |ãB9KŠ#xßUׄÛ½gÂï×û)ð0ÑLñÝÞú ^ÇÞ¾C5X¹¸mäYí:p¿{n9eP¥Æþ^4Uw˜L©T"==²äü€EaB!DäÔ„BájôÃ|åè‡û¤2ì·ãž¿>†w»þ Jû{*^Ço+v$D_„«<Œ·Þº–W[G:OLúÝïÀþöC¢m»û¯æ´L¶ÆŒ23ÌOµ3épvÓ…„£*5.ëÝva'–º/®a‡‡¬÷â¥/àêC§P{ßal pa÷;P;Ðß´±[ëvDU…¥ÌT‚«¶۷`SáÆ9v`ƒ2;QðûUøâá/ã‡v&}ØAÇ0x¬¬?ºç ;ĉËçCcïuNa­’Aq†…¦Œ„;°Çš JË¢jc_] ~9|àªm°Ÿó2|foœ d²ØЗ˥?¨5â7x—ïr„D+’f[‰„Î]þ×tµ† -p~ À0€JMaB!„ÄV‘5ƒó2}—¯‰ºM>§G2ý#»éÍ]vYä““¹ú‡0Ô1±†Ïåž  7½§V( I›|ߊÑj&…G"ÕÓ?‚÷_ˆøñgê¯âL}+¯uiÍ&d-]ó~£ê“QØB!1yÏA]@!„>F+=,p@Y²íßÓ /þ>çÁÛÞT¸z+ÖŸü†|#’«þêã²ÏYïÅzëZ¬·VEöð‡ü¸8ÜÂ;ìðfÓÞ¨ª°äëoAõÝïÆ-´kƒÞal9ÿÄ©ä#„L«ó‹pß‚èURíÛɶÔt¶£m°½N ÄlA~š «ó‹oL—ÔövÙ‡ˆpŠh•Bœ”Tdh³Úкù%8ÑÚ‚kC¼Ûø°­¶n|wE%§ç²Ç:ùÌÞ8W0Œ°³ÃÏzì'À[¯P0ºee :®Hâ³b¶•H¤TáA‘`SdÉdìµ=`+ÝÌÖ—á°RIAB!„Ä—YïÇsö ‰V©ÀïrC¢“d1Z ²/ˆ¸ÊCûGö¨eÂï×`ÌÏ¡ƒoc ÜC“¿óË«XŒk]à¸y¯ºßuû¬Ç»ÝåÁ®gxo{ÁÚ•qé³¹VÝaä¦ Ñ¤þÐj‘’’BaB!„ˆŽ*<B!„·îîîV°•ê’qÿžnx ;®ì¥íJKªïye¦I÷;àA«£cÂï6>ʹ‡ß‰#]7ª ˆ]ÝaüzOÙj8-ó%s9¯u5p~sõ 25<¶#·]؉/þrLÃiL /Ü€÷W¿…Ðÿ׊ƒ«wcSáFAµ °ûø‹NÙ>ÁöÃQí×ÁÕ»çLØaÏ•ý(øýª9v˜—fºù‹ðÌŠJüä¾G°¡´,©Â'ÛZð­ÞÁ¯jÎà\gûXØm=8Ò|ßÿËñãªyU‹?‚Ñ’ ¹9†T”dZ6ìödùJè&ª6z|ÿ/Äɶ–ˆ—±9ìœ×“M¦%“±ƒNcAÉÐàV@Z« !¼.'è7Fr[íA£eÃtJ†­!—³ÿf˜‰§×B!„ÄÓâù¹œ—žeÀq4|.éTxPª'ßëÉ,Îxù®º¦I¿sŽ Ð7 qúj½|«<8\Þˆª<¼üzúùM —õÅEКM1ﯹXÝÁ?Ã5B«Õ"55•„B‰ <B!$*ÝÝ݃HâÐÃŽ«¿Æ¿4l¥í%¦RTßý.Öd-—tt8»àݘzwË¢'xµ³çÊýû”­Fôê7Öõ §Ç”z¬âz¨åX¢ÃÙEHž&6rk°äð}øá…1Û·pÈaðÑ ØSñ:Ö[«k;Ú°C·Û†WvEµ WïÆSéœ8N7}ü=|ýãg%_µ‡yi&,ËÍÃ#%‹±uuv?ø÷øÑ=à±²;Pž›—TûêôyñòÉcøUÍ8}¾Y®³/8&zèÁîõÀîý‹g¥|æ[&Ó¬ÞÅïþÛ¾Ã53†zúGð^u=¯¶U©zä¬X—þškÕf¢Óéšš´^QØB‘ %u!„B¢ÕÝÝ=˜]  @Y²íßuýðÓÒoÛ¨JEõ=ïbÓÇß“ì¬àþ`Înè­€½k²–ãÄõ³œÚéq÷²ƒÊM¥œCÑ8ͱÂÀ†Q¸.W;ØÀ¹_»Ý6dkÌ {n¬œ—áÚOFFü›¥;ŽaÓGß‹É`õJK¿u#Ö[«D«|mضÖíˆ*”ôÛŠ¨´T̉×@)_¿¹Ð1 òéÈO3!ߘ³Î€³eμ—iìÇ/kÎpÌ~mh/8†Ýó€ ÛÑ1<ˆ>×Äó0C«ƒ5Õ8e`!KoÀ Û5é÷Z%ƒ¼4# *uÒ=o«ó‹Ð`ëÁ‡*4Lçö8}ÞÑÊSW,i°q\PxK}X˜…L¨4€Ç„Ählû Óòø,›ÈB!ö‡3LJ\.\å¹úJe¿ôm!„BHLd󘡷éÞ/Îöø\Ò<è2ÒjÍÂpÇõˆÚhÿèÒx_Óq½:¨>-uŠî¡©¿YøÀ—Ðþq=¯J ¿;TƒÿõÕ»¦üÛ®§ápñ›HÆZY…:ö‡çbu‡é¤¦¦B«Õ&ëîÕ ; Ò3M!„H } D!„A${¥‡ÿêú#î>ûU ùާâu¼±ôÉîÀ›™[}ƒß~^Ý»ßÁ+„`Ñd¢ÈÏk½\«/,1rŸ•¾y¤•ó2½îþ„>/Â!1iâÐÝva'>ù¢†,šLÚE×~Ûú}Ø H­B+%‚D› È•„ ÉIf&s¿Ï$¿?&‰Á\˜sæÌ5ïç㑇2™Ïçœó9—Ìåó:ï.Ü÷ᶸ…¶_·¿mØ­D“’Û?}Bÿ½¥Q®Zĺƒµÿ¯ï NËÿAÝ?`÷õ;P$g?qñ™X=¿ZS•Xɪ;ä+äfÕÆì·"4¢‹ÇÄBQ\[Îü3~|~wÜú__x^_÷?`îkÁOWþkÜ"|„Z­íxéÒ~Îíõ9ËÐØðÌ‚8>ûÜFì<ÿ‹”^G¥D‚eE¸£ú:üýª5øáºxeÓƒøÏ/ݺq6Õ鱬 Jõ‚½Îx‚‰ îÁ˜ú9ÐÎ_ÞsÄí‚7Y«×£Ý†~û(¬o(‹wö¿7y ôE%øBq)ôE%(T-Œ}ûO7nÀbm/}MVí˜-ôÐ28ÀþºÈaÃB%29w: ™"=ï/'¦Mº #!‡@‡ç®26„B‘àƒÏ Høž»|dmX¸‚ä–¤R:– !„B¥ª”}5È ×Å·u zü)162ÍìßËèô5Q÷ᵃ™%ôà±ÒÁ7‘T‰bîò˜•·®†"—Ûdÿ½‡gÞm×Ë'¸­§L‚² ×'ç=Uw€@ @NN…!„’4x „B¯†±1 ³ÀK™¸}“¡‡qéKåf4}éw¼†Fü|.cö[a 8¦þ½½–[•‡·‡ØçaªÔåX™SµX…Í‹ïb\zM¬ÛTk*X·évö'dÝ µX—~mî=õí¸Ý™ÿáÊM¸tÏ{húÒïpo鯄Œ•+äŽ9ìà ¹ñÓöÿæÜ^+Ñ é¶ß-˜ãsçù_ĵ2[Tµ½É°×ÊWýô¸Ñoã§jÍç8>Œz=ð‡C…Ç0êõÀˆÜ‰ÎâñМF)‘âGë7òz˜ÎäqÁìq³î‹ì‘ЃTÊ}ò¯€D ;¤kÅ¡ˆ]€A,Îì;ãOTmÎr˜¯m ø½Tñ!žç­H{?"É©R2×yŸ,™~ !„BI5j… E¹ì¿²tõÇmR©úÁlU”yZd•FÝÇlU<ÆÂa:ç ¼Fõ‚÷‰S¿Ÿ­òpðDÛ¬U¢Qøùef%ç/ ½ºƒ@ @nn.¤™›–§°!„’(ð@!„¸`f 29ôðQüB+sêÐz×ès–ñÒß±¡S8:ÄOÑ‹Žž©ÿßR¹™S0ƒKczÈ¡Z]Áº=ÛÊ °2»Žu›nWûsÅG‡¹¨YVÙˆ†-àÀ†? 0çµ_­Dƒ'WlÇ¥{ÞCcÃ3q¯æðÙã;Ö°4öîǰÏ̹ý¡u/"[šµ`ŽÏCÇ’²\ªÚÀ>ÓúyêˈÜQÏéŸyg=w Rùa²™~nDB|{.Û­x¾¥yêßr¨îP”«á4"1 WD‚ÑÞñ]$Š<_®Ä’ô‰4ºíK"ÏÍTãã@À{Xal"4AÕâC,Ž­Êƒ@°0ª”\k “A(ÈŒk&!„BHºáRåÁÜy9në“*@¦™ýFHU·®ŽºÙ*<€7ŽU2Ò<{þϰtúä-]Ì©ïcg:.¯ûŽ´pêCš¥BñõI›…^ÝA,#77âÌ}ãNaB!$MPàB!q“É¡GÈ…û>Ú†fëGqé¿BUЦÛ~ÇKèÁ?ÀÏÚ÷ðzð…ýèsÙÒ,l¿î[ ï•9Ÿ†tŠÖí»]ìï|Äe9\&œ»‚øÂþs]p£Ÿ¨%üWx¸÷Ô·a°^àµÏ‡+7¡ïžÓعb{Bƒ`ö¢ÕÚŽÐXlw¦jµ¶ãÀÀÎíŸ\±ŠÌqÜ4|&!Õ¨jC|Ä#ì&·‹—~D³»…‚™Ùˆ“ÿÐŽœ…R"Å×mÄÍåU¼ô÷nŽvGþf´›†Y·¯§ê±ŸâHŹÊ"“r§ÿH¥€L(”‘ß‹2ì{W‰$²íbq$ø!D~„ÂÈcrEä9™,ˆ„øPè!@*çz rþ.ô?mBaô/Þö =!„BHRÔW³ÿÌ ž‡€Ë2c#QÈf}\§¯‰þ½¤×3m3wŒÒÁ7‘T2kuéj¿|3§¾6µE‡[àö8õQ~ûš¤L«Z°Ç…±dzØá%†aVRØBIbB!„ÄÃ0[t:<œiÛ6zx¶îGøZñݼ÷Ÿ-ÍBëG°åÌ?ã¥Þ1÷÷³ö=€;Š×ÇÔOŸË¼r‘ [*7ãÇçwÇuœ«ÔåÐÉ? LÿÿxⲜV[;§e™ý£(U§åy ÏYÆ*@ÐÃ"|"Êx]×-gþ'GÎòÖßúÂÐxã3 9L]_}&\´÷ÄÜ+äÆOÛÿ;¦qعb{ܶ34‚+è+äFh< [À Àš+,$ɦ~Ôb%²¥YP‹ùûR¢iä ¯Û¨”HPž‹rmʳsQ TS!ŽöZx;ðI!–ÀðC#•Á¸ú×ÈäÇLònç›QaB-•¥Ô>²j ê Šð´ \í3´`Y~.˜Öm×ÖWÐ Ç RÁA$Z˜ÛžÉæ á0ÿý€T°0§x #¡‡€?R™#Úã[*KüDÿT%‘þfá%R{B!„d©*ÍgÝÆ;j‡Çb‡2O—uò;=i”Iéë QÈ¡«_ ¦­+ª~†Z;QÖpuE€p ˜2Û™ŠäÙ¸ç …ä×,FÞÒŰt± ߸½ì;܂כÎsZ/mU4¥Éù¼\¬”C¬/ÈãÁÝ7„˯‡ sSò/Mܼ‘B!i‚„B‰»‰ÐC+€_dâö=ÚþØC.|§ìëq鿱áà-ôÐíìÃwkbËŸ\tô`eN*T¥x¸r/ë6—jMERökµ¦œu6Õ ¦³ixÈ–důo)}oÿð)ÞŽÓrÕ"46<“ÔŠ=`¼&~®1½û1ì3sj«•hphÝ‹¼m—/ì‡+ä†+ä-àˆ„8T¯˜- !É/ËA©²rQò&g+%,+С\›ƒºòU*(Õôb!AžoiÆ»ý=qéÛäáçNxyJ%\?TR)tê,8'B© rqä.k¹Šø~)ÈÛgÀW ú™— ±™ …*5¤I¾Õþºò*”ksð|KsÌ—ÝgNÁ ²nG‰á:Bq¬Ä B9ÝÙžoBa¤*K(Ù‡s‚HE±˜öÁUã7ìŠGÐç³ÄâÌ«ŠC!„’Nô?3°tõC™W—u y})(äŠD›å…qñÊš¨L[‚^$Ÿ™¬îµSàaríü Rå¡y÷+¬û>Ü̽òvÙ†ë“7&Ùšy,X[;qåЩLÞD ;B!iˆ>Ò&„BHB0 ³[§ÓÙü6·oGç/ð±³ÏÕíˆKÿ Ï`Cáøæ™ÇcîëÀÀ¸Bü nç>lŒž!”*‹±sÅcq<\™”/‘ÐxóM ˆŒ«DŒEÆzl,ò;¡0ò{ª*07‰÷cãñ[†P¸p+ÇB!„¤’úê´u²jcî¼<£j_üNT…¹)16Rµ>ûÌÏctúHBÐÝçÈïŸGå­«¯zÌc±!«´B*û7ƒL£œ3l2‰k•€Û] Šê!ÍJÎ ƒ„1$ê…ޱœùCGÏdò&RØBIS4£ŠB! Ã0L£N§24ôðû¡·?©y Z1ÿwüØR¹x =¼=tb =ô¹È—寽ÊCµºâªw;û9ôQÎiÙúœ:¬í¬Ú¸BnNa ³ß ¼€.øšˆÞjmçåœI…ª®­ÖvNæòÓö=œÛ>Z»•õx¸Bn˜ýVØØŽäý=òšÀxM¨P—¢BUšeª¤R ;$Ñ©þ¼Ð–6ë[ž µL†—k*x KP¨VÇ=D`ñº9…>«ßf…F*Kz¥¥DŠGV­AR•Ðc@OÕád|ø³¬pñ6l yDÑ©Ä)ô RÊãBI†®è&‚Óû/’®ôKÙCgÜÖ'èõ¥ÌØÈ³5³$ 9tú œ9U?—Ï´Í<€×bO™pGª™+l2×*¬×%K…Â/\—Ôãp¡1: [kW&oâc Ãì¦3BIOx „BHB-„ÐÃ'ÎN¼~OÜB¡±¾óÁÿ‡qÄ6û!ÖÐCh,Œ‹Ž¬Ì©‹k•‡•9u1÷Á%€iÇþÎ-ÝÎ~Nël 8(ð0 lî=õí˜ûY_xCÌ• beô q ûÌgÿÀaÎUIô9˰ûúkW´ ‡¦fÿ(¯a >ô¹ŒpݸN[ŪÂJ¶„ý±pÁ4Œ ¦a,+(¢<Áúm£x¡¥9íÖ;O¡JJ…—Ÿ¿™ÆÎ€yŠÔøèiS*uÂŽ…5õtòÂAÀŒ'hYãÂašŒORÏTèÁÿiu Þú•EþK!„¤ C× zŒf :Ñc´`xÔ‰áQ'§¾Šr5‘Ÿ< t¹Ô/-AUiUßË0ãã™óz¦~i À²ønÐë‡Ý8 m)ÿŸ1†A„Aˆ¤ÉO†Ë4s7SÌ"ðà0ŽÌ:^®‘Q <Ìa®°Ét\«<°UÜP‘,9åéB!¤Zõ‚Ùïa_CGßÏô°Ã7†i¤³œBI_x „BHÂM„ú Í´íûÄÕ…û>܆FýÓ(“óÞÿßU‰ßhþ>ÆÆc›ý0zønÍCœB¶€FÏPܪ§e™ý£ªÄ5 ÛÙ‡jMÅüoT±Ï<ÛræŸÑï¾SÖnjb¼„ÆC¸hïÙoåµ_WÈÆÞýœÛ76<3ï:›ýV˜}£¼¯w<˜ýV´ZÛ±2§.êÐ×J.ðÂôB LþýÔ1ÂãüM5§Ô¶­+üMD衪4Ÿ&BØ^3BüNîŽÆHŠ™ /A(¤°!„äsyýhë„aâ§÷Š…×þ§ÂÝLL$/š?è'~Šr5´3HJÐ/-J!…Û`ÕŽ1tÆ%ð~§ʼäm'’J QÈg­:¡Ó×@‘«…wÔU_½ïœÃçºûê÷Ÿ üNϼÁŠ…*Ú1Y|㊸Ô¥EÈû\ò¾3“¨•L–-äÈg±Ãidà³Ø¡.-BNMyJîó°/€KoÂÇŒfò¡MaB!$PàB!IÁ0L“N§Û  z¸íì8ø…=X®©á½ÿ¯•ßÚ¬J|åÔßaÀ=S_oD·³»¯ßÁ)ôÐç6"[š…Ý_x‡ŽÁtò¶³MŠoµ¶³îG-áö¡5—vŒ—[à!4†-àHj.6݈“#gYµq…<×{qlw5?d<†?ÇÔÇovaK忤­-àÀEG|a?ï}ÿ²s/ÜQì‡Ù<¹bû¬ULÌþQ0^SZ„f“A.Ú{°<»6ªç¯Ì©ƒV¢a}½£*‰å ðóæ&x‚Á„,Ïäveĸ©¥2Ø|^^úÒHSï.šëÊ«`r»pðB[Ü–Q_]B' !$èr}•D,aK"ÄÕçì IDATâÈù±o/FúR°‡BH’ :qÚp Ím}hëLÚ:?Ûãg;•‹ò _Z‚7ÔRX= eZ€S¿´Ím}¬Ú :Qûå›ã²>Wj@™§…Ýè›õwÅúôž8U?Œ¡A¯…üªÇÝ#£x˜Å|a“éÊêqñÍ÷¢ž°U|c}RÇAÃyòúÐùûã:sõçb… uÿ5 ôµ)³¿6'.¿zœÂ„BI BB!„$ Ã0­6°gâö9B.Ü÷Ñ64[?ŠKÿ+sêÐvçQès–ÅÜW«Û?| ®›uÛÐX=PK”Ø~Ý·xÝÆj ?w;©VW$¬× Àd•p©‘ À–÷ÿ9¦å';ìÐç6¢ÕÚ—°C«µ}ªº [úœeعbû§çÿx}n#Θÿ‚mivøôü³â¢£¡ñèf’m©¼ŸÓröÎÑ ž { -¸lOÜ1iö¸3bÜòJˆ„B^úQH$)¹›êô¸¾¤,nýW•æÑ HKá00ž„¢0x é@ ¤R@®ˆ„„טä'bI¤B„LNaB!‰çòúqìlö <øä+xþ`sÒ³é½bÁëMç±ígûñà“¯`ßá–HeB’`MýÖmÆx,ñùZÍïLÏ·äÙsWc©¼uuÔý½~0†ÎûìN„A:g!2RÅb?°¡­*ƒ¦4y7 +åJ¸Ý?8äõáß¿<#ìùmÏïÇÐûm)±Ÿ}Œ=Ï¿žÉa;€ÏSØBÉx „BHRM =ôgâöM†~7ôf\úÏ–f¡é¶ß%=ôà zÐç2b{íV”«ñ¶}+³gÞE¾ÕÖž°ý§S°?¦c<Ø‚º(L ¸£ÛþÑcª4’̰ƒ+äFËhú\Ƹ-£ñÒ~îmžpuСÏeŒK0#)“¼&´X΃ñ™¦‚¶€FÏÐTåc[úÜFüí’{9-ã²ÝŠSý=t’ÇYËàÞqœUÚ¬9v"¡5¹Š¸ÍŽ”ŠD(Vg¡<;7¥·ó‘Uk ŒS C­ÑIHKá0!×") SDÂ2ÙÕ?r PFBI$ø@!„$RÑŒ§_>Ÿ|»^>‘R!‡¹ :±ïH |òì|ñ( ]ƒ´#IBé—r«9Û~^Þ›‚)˜¬40ežyKG}zgö›ÐÄ+8’îdêèe7®€$Ÿƒ•m¸>¹ÛŸÅ½ùå?—qxÞçt¾v ¡kTЈ7wß.5¾…°/©‡±À†‰y„BÉô‘7!„B’nâÆ• ™º¶ÿ»z_ŒKß“¡‡{Jÿ*æ¾b == Bã!ì\ñoÛV­©˜ñ˜+È~ÝVæÔqZ¾NÎ>ð`°rd¸‚žŒ™8ë1ÍUÓð¼Ô{€sû_|aGÒÂ}n#Z,çá zâ¶Œ£C'9£ÖnÅòìš«‚¡±Ì›è ûqÑÞƒ÷FZÐ4|­Övt;ûÑç2ÂpÀì·¢Ïe„;èÁíÅë9-ã@»$~L^h9SßÞõX¢_±`ÇP!‘`ya1–êP“W0õS™“‡buÖŒŸÒ¬lÔä`y¡Ë ‹Q¬Iý°ˆR"ŃúÕtÂ’"Æ(ð@+A¤rÃô€Æ…BHrºñø³o`ÛÏöãøÙ¸½é9y²¹­O<÷ö >„)ÊÕ rûJ‘—ÏÄïñ>[êT©Û-Ór[\þóy£»©YГœ #MáÊ¡SýòÀJ ;B!™ILC@!„TÁ0ŒM§ÓmÐ@Ÿ‰Ûøû¡·à9ñlÝhÅÞûolxbºÃ=ðièa÷õ; G_º54ÆE{v_ÿ$>䮘֡z– A·³u?zŽÕ&é¬CÝÎ>NÕ!Àp TYœ6Çt…ª”u›Vkû¼U7Ônå‚áäÈYNmõ9˦ΟD ‡Ðç2Âèa²¼ý‡1ì3sjûO×ýÜCñÿ;à3¡ÛÙ‡ng?\!7ºç8÷Ôb%ªÕPK"ÿ­Ö”³ºVñE'/ÀíÅëñöÐIÖm´°ª¤ J‰”^ð¨epLÜÛßöà×ñدʽÉãBRM;"”gçÆtÌBÒ›nD!„š¡kû·dTÈa6“Á‡¿º¡Û6­šÅÄjBØX«_‚ç6³n×ûÎ9,¿ÿK¼¯Ïž:¡He^ö¬•&« œ9U_Œ¡A¯…üªÇ={Ô ™F‰€ëÚ7FSæi‘·t1,]—c^fáç—A$Kîç岎…hª;$“ñÐIØZ»2ú% "•¨t !„’¡(ð@!„”2ñ!ÄJN×àáLÜÆ£¦S¸ïÃm8xýžŒ =˜ýV¬Ì©Ã“+¶ãÇçws^~µ¦bÆcݪ;p L­‡º§M-¬Út;ûqSÁjNË3û­‡ ¤ÇKu.‡k‘‹¸}y¸ýÃsj§•hphÝ‹ 7³ÝÎ~øÂþ„,Ïr£±w?§¶ŸÏùê³—Åe½º}xÏÔ‚V[;º}p‡¢¿{ØgÏË"y>ª5X™S‡› VÇ|îGkKåf¼g:ÇjÝH•‡#]°©.#ó}Iá ðBËiÎí—Ô/ÇwvýŸ©ׯ[‹O±ëÏìvSà!ÍŽ™~ÿ••˜Qº3`ª‡€pøÓê "úD”L À!„BHô†GØ{¸ÇÏv,¨í>~¶Ím—ðà«pß-õt ÞåjP¹(½W,¬Ú :ãxŸÍ™2!E®vÖÀTÞº:êÀCÐëÇÀûçQyëÕßÝøìN„Aˆ¤Tt:©Zõs߸"æÀƒ4K…Â/\—ÔmɤJ¸}h4ô~[ÔÕ@‘—°í û¸üêq¸û†2ù¥°!„²Ð×{„BII ÃlÑét@††>quá¶³ ±þi,×ÔðÞcÃ3¨P•Å8¸‡.:z°½v+{_C¿û §eÏV ÕÚκŸ˜šrÖmZmí1-Óp _–»`Ϲ}ࡱw?çcm÷õOÆ%¸1_Ø‹ŽØŽ„ŽéþËGXOÈŸô/uðwm÷™ÐjmÇ{¦shµ¶s^§Ù ûÌö™qÚÔ‚_uîE‘<7¬ÆÅëg PñE'/Àæ²»ðÒ%ö’£ÝpçÒeTå'Ú ðƒœÚª´Yøéñ7 ÊÖÒ@.ý¶Q<ßÒÌù˜™÷zd¡ÀC²A?06>ówá0 Mv'€PDc@!„~ôÍu¢ÇhËëG12qyxÔ‰a–¡èúê’©ÿ¯*̓Z!ƒJ!EUi>tyå&~ò¾Ã-8ØÔ·7° ÷¯ÛÀó›ÑÜև߹ª=Þm¼¡–u•兀¡:=ÿß1ùì®” <È4Jˆ¤„3?ÃÑ–!«´ãHt×êwÎÍ<‘*šâ|:?3îÑ*k¨Çǯý A/÷<7Ô'¿ºC÷*Ò½ožŠú¹b… +k²Ma_—ß„ÍäÃõ¶PØBÉ|x „BHÊÊôЃÑÇྶ¡±þi¬Éùïýï\±ªR|óÌã1õÃ%ôà ûáó£±áÜò篳^¦H œ5¨ÐíìcÝ×lÁ 6¸L”îr\Ši™f¿uAÄfž5ö¾ÆiYë oÀ–ÊÍqߦÐxFƒ>—1áãé ¹±à0§¶›ÊîŒ944=àÐáJ WÃ>3 Á#Sá‡Í‹ïŠKå‡Í‹ïÄþìž`ª<ðä‚iG»/rnÿŸvXP´pðB[Üúoë¤AN¢±1 àÆç{Îxä9R9…R…óï³x-SDB!„pÐc4ÃÐ5ˆž+ôͬïÊÎæ=Å\ï/*åA—§AÕ¢|Ô/-~iIܶõé—Oð¾éª­{>ù v~ûޏ9Y˜Öê—°<Àå÷Ûâxð;Ý)5>êÂ\ØÃ³þ®êÖÕøËÞ·¢êÇ;j‡¹ó2òk_õ¸Çb£ÀÃ,¤j®è޾&êj3–“¥BÞ窒¿½Zn•{M†øFíQ?¿ìÖ/B¬Ç}{|Œ—ßBØ—ÑaÅ—†ÙBg+!„²0PàB!)m"ôÐà·™¸}Ž ÷}´ ÏÖý_+¾›÷þ''r'#ôÐç2bCQÖÞ€S#gY-O+Íšñ˜+äæ4YšK…†étò¨ÄJV™=aïTÐCŸS¼+sêpSÁª¨ÆÏìPµ`Ï{6ÕD iø N²<Æ@+Ñ ñÆgâó™ÐíìCh,œ”ñäZÝA%V²ƒ´ZÛ§¶7чùL?T©Ë±yñ]QŸÑ³Tå!yúm£øùû'8·ÿö®ÿ@¥~/ëâh‡¤° ¦a<ßrfOü¿´ï1šQUJ_”'Úøx¤²C4çÇy®T4vÉ&Eªo$’HBãN!„踼~4·õÁÐ5ˆæ¶K)Qå ÷нW,hnëŽD«¯ŽÖÔWðò~dßáì;ÒBÀgßû{xâ¹7ðø·`ã µ4 „E¹ÔW—°¾‰ÓÖÅe¿7ò‚z}$`Rv4yZ8‡Ì›å#Ûê½ï|0#ðã2ŽéN¢”Ex¨¼u5çÀCqC}ò·U­„€ã]1þü«ç/¾í‹ñÿ[Õ7„˯§°!„B2 !„’ò†iœ¨ôðÛLÝÆGÛ‚ï¯ü6ï}'+ôà ûa 8°D]Ê:ð°,«zÆcï™Ø¹V¥.iB³+äÆ{¦È„R¸ááÔ‡ÁÚ€·‡Nâgn/^;Š×Ï[y"4†Ù?š±U\¡¹? çTÝá·êÛ¯û*T¥qÛN[ÀnW\AORÇšku‡› V£Ûyu`aúö¸BntO Öö´9þz\ýøYûüR¬ÄM«±¥r3/Ub©ò°×ЂGV­¡?ø,™<.|88€íx‚AN},_·÷|ï‘YÇ%ÑoŪ’2Ú9)Æ `¯¡ïö÷$l™†®A <$A(©Þ­±ñHÊœ%Ÿ(Á1}2N!„k8v¶Ím—"¡‚4ÐÖ=ˆ¶îAì;Ò‚¢\ ÖÔWà¾[êQ”«aÕÏð¨Oï;‘´êuùŠ"(uPIÔ(×ÎS˜~{ÜALfïpB×s×Ë'`èÄÜÂé=㤣yFˆ¦~ZõˆªÒ<¨2:!€ µœÎ»3m¨ýòͼ¯Çb‡¶45B‘òl <Û¬¿/k¨Gï‰sQõ5WH„3ÉÔJ¸GF£z®¶´b… ¡(ƒ'SûV,Nêj§v>‹Ö®ËQ?¿¸¡>îÕ¬­¸rèT¦ž1 ³›ÎRB!da¡¯u!„’¦…vÈÈOw]ú5.û†ð\ÝÞûNVèñ™Ð4|†õr¾\rëŒÇZ9L¨®ÖT°n3rxÏt§MüßÁìí¡“x{è$Ö¬Âwkžs¢µÙoM‹ÀÃ|Á¹tÏs×¶[À—z°^­Dƒíµ[ã2&®ÝÎ~Øޤï®Õ¦«™ÊòLm£>§wL„‘¸R‹UønÍÃøYûÖmßíïÁ¦ºz(Õi7Žý¶Qx‚A¸ƒôÛ"_~y‚ôÛ­3ž{ÁÄmâA¾R…Õ§cãpy–þÙRi³ðد9÷ï³é ÎL©Ò”ªÓµuâ¾[êi$Ðø8 ±o b UyH6™×˜Hd´Ï !„2»áQ'žhñ³)QÉ!–íx½é<^o:úêll¨ª¡k;_<š°mÏW¡._ m5ʳªQ—¯í= ½}ŽôÛ»Ñn6 ßßàûñ³0gèaxÔ C× zŒfô-è¹bŽnlÌ|¨rQtyT-ÊGýÒ Bd 5õP)¤¬Ï¿ÞwΡòÖÕ¼WcðZìЖ¥ÌøhŠóç ÿÐÕU×.7 DR*8õÞYÉî˜Ê]²#í½¬ÚŒ…BûÉ’{7 ‰ZÉ©]ï›ì‚•w¯‹ëv =Ë™3ýÐü&Ã0t†B! !„’6&B­š¡¡‡ß½…ßëÿ Z±†×¾“zèr^B¿û ëeܘÿ…µZ?aÝ›Éø­Övì8—ÃlN›ZÐjmÇê¶á¦‚Õ3~oö¨Jùc6[š•Ôå7öîçÔn÷õOò¾î¾°}n#¯)eöÏ+}‡èG Öv¬íhì} woÀ%ë9U}¸£x={_ðÏ̺íö¶”­òÐo…ÉãF¿mývk$Ð0tH³Ç—ÉêßøÑ¿¢¨|1¯}š<©žÌïT^hiNʲ›ÛúàòúiòI…‚±µ¥*É%"1ÅYbI¤¢!„BÈtãNì=Ü25=“LU~8Ü‚ïZ5gðaßáì;ÿÏFWéÖ¢._Uºµ(Pêxí»\[rm5Pv;Àt¡ÝbÀ_†ÏâÜÐ{pì¼oÏgCÍm—pº­m]ƒuò¶œÞ+ô^±D*ŽL"*åA¿´kê—@?­*IOj… kê—°¾½~0†N”5ð{ã…±p>›òlMJŒH*\«Ï>ó¼Ræi¡«_ ¦­+ª¾C'‚^ߌˆkd4¥B©0æ"©á@tº,Ù°ŠuàlÝI­ò Q+! Y· y}0¢?_ ô5Ç±ŠˆñÐIØZ»2ý°¤°!„²€QàB!i…a˜VN·zxßúîûpõO£L^Ìk߉=œ6}Ⱥï*uùÌýî3qš@|­ÀÃd5®”cåyð£¶gð¯uÛfÜY>4†Ù?šUø¤³»‹Nã¥×X/C+ÑàÞÒ¼­s*àé / 4‰Þ°ÏŒ—.íÇK—öãö‰Šl«˜l©¼?­«<˜<.ôÛ¬¸`bÐo·r®Êê–¯[‹{¾÷ȼϩ¬_κ_³ÇE'RŠHfØaRs[_TwP%ü '§-áDŒ…€ñ8.C*+!„ÂMÑŒáQ'zŒ0£N ["“N]^?z¯X®znQ®E¹‘I²Eyèr5(ÊÓ jQªJóSç³€ :̶­»^>ƒ'Ú°mÓÚ© ò.¯{4Çu –åÕc}ÙíX]|”’Ä}ö¡”¨±J·]õ¿P®­Æ±KÀþŽ—p¼ï ^—sül>éa`w{Zd2ñzÓy¨R¬©_‚7ÔRø!Ý·a§sñâ›ïñxŸÝ•2PæÎx€Ê[¿uà!èõ£÷s¨ýòÍW=žjU-R⽺Buà¡hyÄ B^?«e¸ŒÃI í60 ÓJg%!„²pÑ×;„BI;¡‡•Ðgâ6~âêÂmgÀÁ/ìÁrM ¯}'2ôÐjmgÝïl“‹¹ôS¥.Ÿóí®û/ÁþÃp‡·ëÖËØ~Ý·x©îªA‡Idžޥ?1x{è$Þ:‰"y>«ª±Ty8Õ׃Mu‰ý³æ ÐnFËà.˜˜¸TRHEßÙõ×|Ž*›}®ÒÐÉ“Zx ;(%Tåæãüð«v†®A <$ÐØxrÚþ‘Jñ¸” He€*;B!¬_Ó¶u FþÛ=Ȫíð¨óÓ»ÛwÏü}}u ôKKP¿´$)“³]^?^?q>! RMï žxî |uà ÜwK=žüŸ£3+|ÙTû¶®xµ¹ŸÃ9ætÒ¶9G Ùl\r6.¹Fgv·<…c—þÀ[Õ‡A³=©ûÕí àøÙ?Û¢\ ¾ºa66ÔRåÁ4SUšúêÖ×\ï¨gÚx=x,6d•B˜"eòd%¤j®™Ÿaæ×,†"W ïhtçâå÷ÏÏ<Œ…ÃðXìPæié`œ QÊç ™ÌféÆqáM¬–aë@y·Qü™JѺüç¢~®º´95üo¥±Àxè$…!„² PàB!i‰a˜¾i•22ôà¹pßGÛð“šÇðµâ»yí›ÏÐÃOÛ÷àßëgï§Õú ë>g [?©t˜î§í{ðêÚº«Â#f¿¡ñÄzÙ>›CÇ8µÛ^»5¦å¦zÐÞ3£ê<™^õ¡J]Ž;JÖ㦂Õó†~P÷x죧X/«ep !O0€–Á´ àÃÁ·O¿ñÃA¥~ETÏ-\\†‘ËÑÑe»•Nš$óx¡…¿I4J‰?\·ÌìÇÏv`Û¦54¹„Db@<„x|# ÒøB!×âòúÑÜÖ‡æ¶K0t ÆõNõmÝ!Š#Høé›Û.aÏæO ÔëMçñÇw?A(<Æk¿©[ëÅæÚ‡Pª©X“P]w’X(F–,ûªÇJ5ØuËoàXcÃî–§ðÛóÏeÔ¾uâùƒÍxþ`3þê†Z§;œÏZáÁ{¥ˆý‡ÑØ»?å‚“Ü!~Ù¹wÆ8= *T¥™uîzGæüZý‡ç‡Œo³^öÕ›8WwH‡ Ã¤ÆKûéDô¸úñ«Î½øUç^ÉóqSÁjTk*°2§îªÄÊœ:èsê``Yæ²Ý O0¥DÊûº/ôä%õËñýkÔÏ/¬XÌ*ð09Öñ؇$:{ -ðƒ¼ô5v(ÏÎ…R*Å>û;¿6·õQ•BX’H#á>*=…‘Ê+!„2ïû]£›Î£¹íR\CsIÔé]^?öhÆñ³´Ó'ðvøæŠïcûª3î +iÛ˜#Ï›ówY²lìXûsl­ÿ>?±gOeÜ>ž<·(ø>6ÞP‹}‡[X‡²âUåÁ52šR‡ùª<”5Ôãã×þ„ ×U_³…D.7‚^$ïúŸiÄ2 «ç+ó´È*-„Ã8ª½{ )¶ŽICï·E?† Šoä÷¼´¶vâÊ¡S™~ø©ì`£3‘B!!„’æ&>䨠Óé<œ©Û¹£óøØÙ‰çêvðÚ/_¡‡·‡NÀU“õ[­ìC Uêò«*Äb2ððžé~Ùù§ðE¢½=tß­yèª1`¼¦”<¬/¼'GÎFýüùöE´Õ,l«eNº·ôvÖmÒ)èDÂF=Î~úgÃ>3 ™y “¨&ÎÝNýöÛ¬XVÀß;ý¶Q龈/ó6 <]©´Yøéñ7XµQk³’¾Iô<ÁÞíï᥯|¥ ÿtã”gG¾Ð/Pª±X›ÃºŠGsÛ% <$ˆPŒqoKR‹H È‘ÐÃø8ûöbIä‡BH|„Ñ¿½ãcW_«…¢ÈßV‘ˆÆ(;Ûƒ'ÚÐ{Å’:ï·'îL¿ïH îÛP¯Þ²‚—àCÑŒ/¾½à«:ÄË %ë°ë–ßLUtø,Ø—´uË•ç_ó9¥š ¼ú•wpìÒðø‰­pì·&ƒ_ݰÞµŠª¦¸¯nXç6³n*á@~§22eÆg¾*•·®FÇ[ïEÕwÔÆÐ ¾æªÇÝ#Vd—Ó@$eÿƺX_Ã:ðà4'eûÄJö×Âׇ¡3ÑŠoä·ªóHÓGiú(Ó= ;B!dæk7B!„d†a¶èt: ƒC¿z ¾!4Öÿ´bþî¯ЗÀÃlÕ¸r…<øaÛ3¬ï°žlï™ZpGñ§K|a?Ÿéª»Ç iä ë6Z‰÷–nŒúù¶€FÏÌ~kZÍþˇôR IDATÓzß–ksPž‹"µÙ2ŠÔW_ï|¡†]Øü> »è·Y1ìNÉ =®Øƒ&& Ëûdù–Áí¾€ ¦aº` vøÏão@•Í®}¥~ÎüñHRö!aïOa‡;ª¯Ã¦:ýŒJuE}pyý4‘$D"îš™š„"@&B! ¢É= Kˆ%TÕBâal,r] Í}]žü{,L\“ÅtMNEÇÎvpº{x"¹½ì;Ò‚ƒMmxðÎU¸ï–ú˜¶wÏÓI©^‘éiʱcÍϱqÉ=ó>Ïò'ms¢yGqšTK!ü‚Áȵ˜M›P0ò#G®åtN>C× v½|"­ª¸½<°Ím}ضi ªJóYµßw¸ûŽ´$u–åEgç~G7<ÁôxOÿÍßÇöU;%ËNÙuÌ’²_·RMßÿ?±:öfäµÀí `ç‹o£¾ºOª~,]—aüšÅS…ÃðZìPæÒÁ@,“ Àâ#Omi¹ZxGÙUËq #ïs‰ <ˆ•ÜΑËï|õss–.†f4Ó7 ;B!dî×o4„BÉ$ Ã4NTzÈØÐÃ'®.ÜvöüÂ,×ÔðÖ/Ÿ¡‡E!†}fÖmù <¤«îYîo 8` 8-ÍÊøí—‹¢»vÓðû¬û¾·ôöù¯>ú\FøÂþ´¿£C'áyRzeb1jó Qž‹Ú¼BÈű¿-•‹ÅЕ@?€è°Œ Ã<‚¶áÁ´Û‡Ñ„>fCA‡Ù-_·?zm§àN!‰~»•>Iáû0áµú  ¢[¤B…¾¸½ìzù ]ƒxâ[æ}n¼Ã‹4åØXqJÖ£¡dýŒ*á±Î1§cüŒ@‡¥ÀÌÉ’ífúÝh7ÐãrØX–WW¿òëªJ‰ £n+™(¶;ÜïXûsÔåëñĉoeì5b²ÚÃW7¬À¶Mk颙Bb©òÐñÖ{(k¨‡2¿¯È<4ÅùiQåA™§…®~)˜¶®¨ú™­ÊC8„ßéL£\ðÇ¢TÍ~ Šõ5¬¶žHeç+ÙVǦºƒH&ª¤ãcc…Ü®Ñ}C¸üêq„}ÿ™ó7†i¤+?!„Bæ}ýFC@!„LÄ0L“N§Û  z€GÛ‚ï¯ü6o}òz`k¾êÕê œ6¥Æ³úœeØPx#65`eN*T¥3žc 8°ý£óx°`|&èä)wV¨KYZ­íœ+z´ZÛ9í·é2|a?úÜF0^Sæ\÷|&8Œ _ŠTÈÅb”gçB.£H¥A‘:‹— \ÉÅb|qQ9¾¸¨ý¶Q†SºêƒR"‰zÒµÉãÂ>C > ?úÓ,©_Ž{¾÷HÌA‡©ãº|1ë6>I/ž`ýv+”R) ”êk>ÿú’2Ö z *‰!‘B ãsÜŠz²M´$„Bæ66ÆoØaÒ8"#¤R Æ[ÑŒ§_>Þ+–ŒÛ¶ãg;0lqbçwnŸ5X¯°ÃdÈasíè˟ÿŽÍ"¡¹òü¸Mò¯Ë×£._;+7Z˜Ó87ôN ‹Û¸oª}»nù §¶*‰:)ÇJx,s›kF»ÙÑ•àõ¦ó0t b×£_¡À~ áZåþ²÷M¬}ìoy]ŸtªòPyë£<³WypŒRà€PÌ~²~^ ûÏUÃþ ¼&+9 Ù.±‚}(ÎÔý닼º*„ýxF¬PéòX/ËÚÚ‰+‡Neúáe°…a˜CtÅ'„BÈ5_¿ÑB!$S1 Ó:zhÄl·ÁÊ».ý—}CøIÍcЊù¹sz2BóM~¿©`^º´?ic¬ÏY†-Kîǽeg 8L÷îÈ9ÜÿÞ6 sü2³Z=÷ýkú\FäËr ¤ÖËø UYB—×ç6r>¾lúÜFØŽŒ»äVÝace-caŒz=×|®T$BUN>¤3DåbIB*6ð¡<;åÙ¹ØXu>¸ÒÃð`ÊU}¸¾$º/Žv_Àv<Á`Z£Ë×q»KàǧfÞ©rIýr¨²#yÆúukQ©_ëÖN=Ƨ%õËq©ícVm<Á”)Hb-Öæà²ÝʪÙãÆ -ÍSÿÎWªP út"ŽR"E¹6ò%k]ù*§å4·]¢ÀC‰D€H„Ñɚ“Á )è@!„\Ëøx$”O …âåà‰6ì;ÒÂi‚lºhëÄãϾ1crv<Â7”¬ÃÖbã’{XµÓ©%¬ªÁ*ÝZ¬Ò­ÅÃËÿ‡{àHïx‚nÞúlÕN|±äfìny Ðnn…#`›úýÙÁ«'dj¤Ú«B!uy+á :°8«J±úš¾¸yƒkŽ‹£çñþ•±Π­Až¢åÚª©±ù¬~G7ÜAL&Ï0úݼîϹô^±àÁ'_Á®ïU¥ùt1M±Ty°t]cè„N_ÃÛú¤S•‡üšÅÈ[º–®ËQ×g«<øìN„Á”ÚÞdpHrd•ÂaaÕÎ90œÀƒHÆþ³[kg?|£ö¨ŸŸ÷¹Èu>èò`x:z–3gú¡e°a˜VºÚB!$ô1"!„B2Ú´ÐC28ôðû¡·ð‰³¯ß“¶¡‡› VÏù»jMŠäùœC\h%l©¼Û¯ÛzÍ™H¿áÏ_ùûÕšŠ9ç ûaô0Q­O&ãRá¡J]Ž3æ¿Àögì¸jâÔîX/»I-db1t* db tj ´r²eò¨+$‹\,ƺò*¬+¯B‡eûÑÏrâr<(%lª«Ÿ÷9& /´4§LÉCýÄ+õ+¦•Ó 鮨¼Œuà¡ßfŲ‚"z!–`*5ë Âg™=n˜=WI>YIåà…6€JÊþ Ñæ¶>ÚAI Q¸Bá"œ»R¯Ë D*3 4æ|qyýØõò‰óú³÷Š?ûžÿÁý€=Nóv¸¡d¶¯z %ë9µÏ’e#Kš}U0 þŸ/¨±¹öaÜU¹ ‡{à@Ç^^úýEËNVÏwìW… >ˆ€ò¬*”k«P—¿uyz(u¼ŸcÿüÆ×pþRøB>Öm¥"~ºþ«£»qÌl“‡A»Å€vs+:F?ư;>ULÝÞî <ñÀ-XS¿„.¬) –*ç_ûòjsš°>çùFUj¿|3šw¿u_³UyHµíM¡H„±p˜U›ü¥å¬^ÓhB¶‡Kàaèý¶¨Ÿ«(È™ nŒa,‚Prí)za_W„ãb¦RD*;PØB!Q£À!„B2Ã0¶i•îÉÔíüÄÕ…ÛÎ>€Æú§±\ÃÏ{zÐÏSÝaÒæÅwáW{ã>ŽåªEعⱩmƯ»_Åߟû7ŒÅ¼ü› VÍûûÉ*j±jÁžÓ\ª3hÄêŒ;t;û(4è´\ý¥E‘Jƒ"µ:µE*Mʆ jó Q›W›Ï;UõÁ %e]Ô¯FR=çïOõ÷`Ÿá\Rª:..C¥~y¤ŠÂú›PX^†¢òÅ æšS©_3<ªM¿}”IPWP4Nˆ'w€Û]r›Û.ÑdB!„¤¼ñq œ ·E㈄+¤T=F3ž~ùz¯X’²üJÖ]õïv³΀=îËí½bÁÓ/Ÿ€~i ^o:ÏKŸ‹4娱æç¬+:̦*§çM"4–ØÏ&ƒ«uk±ç/ÿ…~GOʳýŽô;zpjà _Q„ÕÅk±¾ìv”k«y[ŽÉÃð¦È’eãé ¿Á÷þô Öma?N_y›kæ¼ü¥ë•:¬/»`töსwÑ|å®8ùëö°óÅ·ñø·PµÂ VȰmÓZìz™}…ï¨o¾‡å÷‰·õñXlPäj!Ó(SfŒ´e…0]¸4ãq^ª<Øœ+-„pßQA¬Ï*™N_ƒÞçXµqs³±Œ}Õ“!úPe^]ÕUÿ ];ð°9qùÕãð1£™~8©ì`!„B›×p4„BY&>4¹W§Ó5x8S·ÓècpßGÛðlÝÜY°ž—>·TnFÓÈûx©÷@ÜÖûZ“ü`sÙ]ØùpÜ&us :ô¹Øþáñãq^ÖáöâõQ.:z°2§bÁÂ|9ßjý„u›jMyæ]×|&t;ûÐíìÇ[ƒï¤Ôº »v;Ñ6í»‰rmʳs§þ›J²å l¬º«®C‡efpFˆ#žnž¨81—ç[šñnâ&&..CÃWîBýú›°bÝÚŒ©ÔÀy<8„;¸Nˆ'±YW^…}†–”]?C× !„’ò±‡€q UyˆUьǟ{ƒÓÝ¿ÙÐHµh(Yºü•h(YRM9Jç©V ¿mê®ôgOâÌàIÞƒÇÏvðVÙá›+¾í«v K–ÍK2‘åYUè±u$åØ(×VcÇÚŸãÿ¶=‹æ+ï¤ôqlöãHïAé=ˆ|EÖ/¾ëËn9¬`töóV=âîêÿ…÷›ðÿÚÿ‡uÛ“—ߎ)ððY¥š ”j*ðàç¶¡ÏÞWÚ_˜µ’F,&'ØSè!ù6ÞP‹ƒ'Ú8…ÚzOœƒN_sÕ$þX9‡ÌiRç†(…ʼlx,3çOÇZåa,†{Ä Mq>ˆ,q9æ7¤Y긮›HÎ.qk2t äþ¦ZyŸ«dÕ¿±àRã[û2þså? RÙ„Ba„BYP†Ù¢Óéú<™©Ûè¹ðͶÁS5á;e_ç¥ÏƆg n¡‡•ÙuQ=ïuÿ€Ç>zŠ×es :Øìîø v_ü¿°¼¬‡J¬Œz\Aºý¸.«*mSWÈÐåeBEŒVk;Z­íèvõ%¥¢C¬úíÖ©Š2±åÚ\Ôæ¢\›ƒl¹"eÖs²êƒ/B‡e摸…” Ô¯ž3ìà ð““ÇpybÜâiIýr|é¡¿AÃWîJËê n[d²ŒÛî@{ó \ì‚×åBoÛÇ€úuk+&BlU° æaÄSJ¤¸¹¼*¡!6 ]ƒ´“!„’ÒYÝaºpKhü¹:v¶ƒÓ]¿£µ,¯—Ü‹÷ ._Ϻ}–, %ëÑP²[ë©þ°¿ã%ìïx)!U ¢¡‘jñ?wDCÉzÞûžœlŸ¬ÐƒR¢Æ÷®ÿß0:ûpÙÑ›ǵÙ;Œ{q c/Ö•mÄú²;8àûÀ¸Ð©JyY·ÿX÷ß89ð6ëª fï0Z˜ÓX¥[ËëX¹ƒ.(uØ}Û^ôÙ{°»åǼ(ô:¶mZ‹'ž{ƒSÛ¿ì}þ÷VHr^Ö%àrÃc±C™—:7JÑçÏxȯYŒ¬ÒB8ŒÑ}Æ;[•Ŷà2’u…¬*lL·ÉÿÀƒŒeࡵ3êçj«Êfô/Ï==ÏÚÚ æè™…vx‰a˜-t5'„BWx „BÈ‚Ã0ÌΉÐÃo3y;wtþ;;ñ\Ý^ú‹Wè¡HžêkÜ nÒÊœ:ühù÷ñ“Ÿ‹y¹Z‰Û¯û¶×nE¶4+ª6ñ:LÚR¹:yAôDZ×µX‰ReqÒµ•9u¬Ût;ûqSÁjNË;9r–Õó«ÔéWÝñ™"ágZ­íèqõ#“øC!tZ> ©4¨Í ŠÔš”XG¹X }Q ôE%ð…Bè·¢Ã<‚~»vŸ7¦¾• ®/YŒMuõ(PÎþÅM¿m?¿ fOüÂAér‡BpÛìpÛðº\pÛ€^Ãyœ>øÚ›g¿>||êtäþý¿ }'¾òe|é¡¿¹æ2Ù$ÀävÑ ®$yH¿ ^†'Ñ·&Ž—»0B!„$ÒØX²^çSà«x…4R-6.¹[W<Êy’ù|êòõØ‘ÿsl_µç‰}ŸìÙ›¼àø²¼z¼ú•wx«ê0›d†Z˜Ó8yùí´ ;|Ö©c85p ËòôØ\û0§cÒèìG–4J ?h·¯Ú'N|‹u»sCïñx˜ÔgïA®<¯Ü} ç˜Óxêôc¸`iã¥o =¤ýÒ¬©¯@s[ë¶ÞQ;:Þ|Ëïÿoëã0Cž­†P$J‰ñI%sVy¨ºu5þ²÷­¨ûúl•‡p ˜rt¡--bxp #»ª,®ë% Y=ßdˆþïwvÕÌ€P2ûô¼‘¦0ÒôÑB8~Ì0ÌN:#!„ <B!dAb¦Q§ÓÙ4ÈØO(?ô>qvâàõ{ Ç>‰8¡‡•9ŸcõüûÜ?B(âÇçws^æ=¥…Ý×?‰Š(ïêÕç6¢±w\‚p{ñzl.»‹u»ng?ÄB1« D>°iÎçœó9—æòy}Þ;‘–*Áœ’qtN å·ÏEíÉ68Ýìg‚oÞyºÒIçU.ˆêïw0{»ª|mÒôOz~<;BÁàyÌ.ÁñO¿…»;²ka *ööÎQxœ–Ëš4Í;²ûÖÛŠÇ"9»J'=' ¸½?_=¡`Èõ=>´ñXjNކÓç>†aÖÑœB!ÑJéí¥O !„2zétºéva‡ _¦Ãº’Õ˜ªœu[Ÿ ó¿þ9j{Žñ²m/”<ñLÿYÒ LU‡g‘úظ÷þã V„ÒŒÉX{Ås˜¯Ñów™ª°îÔ&Þ«Zœë†Ür<3eyTm\¦*Jhèa—© ¾¾“Õ2KÇ-½ã]ôx$Ç&å/…¬ÖUš1k¯X™4×ãH¯ÞÀ§¾ðÃÌ1z¨e©I»Y Å%+5°«°ƒB•ÑÀþdâóxÑa8 ›¹ Á ¾¤€þН7þ•·þ¹ë·OãÖ‡ð÷Ï\_ùC•ˆ=;¯“³µt!'ÈCÞªÞ—tÛõàÂ9X¸ „!„B’’ד¸ÐƒT „t "ÅwØ!^A0Ú[`¼Ä q—ß7½Œjfo\ú1ža‡sC´;`œgxm»ÓÅ`[óìný2îá‘x»½x n;«ð€BœÆ[èasÃzNUž˜¹*fUÚO£½+v.Ãþ¶=Ñ·›*Áš‡+Q”ŸE7âÚøy56n«æ´lj¦ óÿmÄ©2Þ¶G3Q©Rž4ýco7ÃÞÞyÑãLí xk ‹ý‹¹Ý}Þcj}Þ¨ =xí.tdÿ]†ßíÁ¶'^e½Üݳ}ÉeP²êœØôZ¿9ÑsUE(ª,?ï1YF:R³3~xàñáÔºOáaºGúicp/Ã0Ó›B!|PB!d4c¦À|µ#y? ¿_Žm»£nK-IÇ®ÿJ3&ó²m‘†@—úàþÛò+Ðrë^<7íQ¨ÄƒÏ_ž3 ïÍ^ƒš›¶ 9 ¾ÅiÄÚãï¢ð“¹XðõIv€ãÖ&0žNº “T£½›[?dzukpçÞÅ/ö>„—êßÀ–Ömv‚ÕãÆ3§ñ‡ÇŸ¿ûœ1Àâq'Ýv Søyk‹°ƒB•Ž»ž} Á]¿}zX„|/Œ 'Ñp =¦ŽÃ›V¯å-ìN« ^ñoxèªr4×¾è÷ãK¦²oÓ8æé‹ð«²9I·]\f`$„B!ä\|‡®/¬Äçw|‡GËVÆ4ìàò;p¸ó»K†@.NÃ3WaÉÔ‰y?&*ìBùÊBÌÈ™…"u1<ÌößébðÆ¡—ððŽ»±­ù£v€- ðô®PoŽü£}§ßú®Zyš,*^Š1J=ëå¶û÷žçìg¾²V~ƒGÊ¢ŸÆéöaÅë[á`1Ó9áßâ›Ë0~Œ†Ó²în+­ÿ”×í±Ú.ª¨HŠœ „§(u¥“ ™yu‹®“§ÑZUwÞc)ÈàÄ©2¤f²ÿLÚÃ*l?5 ?W]tqeyè‡óÑÃtáÄÚGKØa>…!„Â'u!„BF;†ajt:Ý|„+=”ŽÔý´¸¯î)¬šô(¸3ª¶úBÑVz˜›]ù WYÒÌ‹¶ãùiâùib—© 5=õ°ømý¿Ÿž1ósfC-I´íšžúþj|U®ÊeéE¼„ú·6! _ž;,ÎGG`ä~éZÓS®ààWpp\t£åÉiÇö¦loj€^•Ýkr %þm-Û‹°Ã]Ï>5¬*:´7B©cÐçmZ½ßoÿ&&Ûpªîž9w=ûîúíÓý§qèCƒ¥eytñ&Ð<}ôª ¼Y½§­=I±Mµ'Û°˜ !„BáˆÏ°Ã¥k¼‹Ùyå1ßîÁª: 䦳³ö¿yèå˜lO"Ãç DÈ–ë-×! Àæ³ÀéwÀæµl>Ëm¸ü|Þ¼[6ŒÊkÂì6á…}ãöâ%XT¼4¢eúÂEêbVÕ!RQx+Þ;ü:«eØ4¢Ñ·Ÿ}•-[‰Ùyåxà‹…°û¬ÜÛuû°âµ­xó™;è¦{P5™ IDATœ@OÞ³Ë_ÚÌiY¦î$Z«êP0›Ÿ ”AŸ6cÔúäønB B‘“9`8¡ø–aßÚ"nëø§ßž×OAŸ^»+©*Z ªü¸»ÙÝw\ÝçUEH»Ñ‹mWO¸øó`¡Lè©9æ‹*=#~B”Z·1 ÓBg?!„BøDB!„ ÃXΆÖX:’÷uå‰WqÄ~¯O‰nF'µ$Ïû3¦~¬~;§6ØTw¸0ìp¡ùÚÙCVo8W‹ÓˆuÍ›±®y Î3q?Çm᪠:Y6om6Ú p\˜ ÔC”¿—úÓ3¦°ßÖTÙ ÑÞ‚o;«Qc©GmO=ÝPãÀ`íÁÚƒÿP¢ÍC©6zuæðÝžÃSçÍÅcïüZýØaÓæ3mèh9=`5‡sÅ2ìp®¿üîeÔ|³Ïýí¯P¨U˜V~ ð;vm:]NºX“€^‰ÿºî'ØR_‹/ñ*"„’X½½@(„zÏ\„@J õ!ddÙWwŠ·°Ãõ…•X³àݘVtƒñ›, pú¬—-/¸x=ôí{² DÈe!C– PÌ6 ô÷£Bœ¡@„wë^ÃÚêUQ ^)¶4l@½¹+f®Š(ÄИ”qyT×Á²’‡YÌn:] ²åº˜÷Ë…¡‡ÙyåøüŽïðÀ q¬«Žs»Ígº°úýxòžtsN¢ü,ülþ4üm×aNËÙ´š‰zÈ5üL”âê²@¦JƒL­LŠþQæfÁÕeAÐwþç@Y“ÆB3q,ºNžŽ¨w·õ¢pˆ½Ý ©rì¨;çÄr)çeUZ0u'Y-ã³%Çg«–‘—¥**€P*¹øo¼T‚Ž]ߣc×÷£áT©E¸²ƒ„B!<£À!„BÈYg?|¹W§Ó#<ôð¿íŸá¨ý>ºò ¨DÜ?€.Täc×uÿƒ«·ÿ ž û2Ö×°¨ð%åg&—§Ï~ë›·$ü8¬kÞÌk•`Üpœ¸,½i"E\öc¨ #M£½5–úþJTÁ!±êLm¨3µa’&•ÅSRñA.–p^Öå÷á•ìâe¶B•ŽÇÞù#fWÞ¥7MœŒê¶Vl;y,i*>Ba/ü~ üy ‡Ã„ÈC_[1C!¥A5ÍXÍCØA)QaåÜW"ž ?l«: ¤¼àÔ›k°§u;/Û4YS’”a‡H¢þùUm»±bç2œ±èâ8DZ®Z<´ã.¬œó ôª C>?  ¾«ùJ=ò•…ýÞæµ ¾«õæØ|Ö³çtËEÏ“ eð=ì>sèªEyÀÅ¡‡|e!>¬üwn½6ªÐÃWûP:1³ŠéäKÅ7—a_] LÝì'¤ò»½8ðÖfÌÿÍ/yÛ‹¡Ùr„qRô27CÛE³­òpdÓèJ'Aœ*øNx,ö¤ wÄíu¢û+ÍD=€oY-c7š«š!"á¶D~ŸTæk/z¬7Bëÿ~ ÛñQñ7{=Ã0÷ÒÝ™B!±BB!„ 0 s¯N§Ûད¼ŸG'ñãý÷`]ÉjLUNâÜÎôŒ)«Èà Û)VËeJÕ¬äUáa(Ÿ Ï^‹×’çKÍ/ÛwãÞñ‹x­ò¿ 5=õ˜ ,ä½íÑÈp†+8ôÔ£¦ç(L3uJ:ÑÕMGaqéUq]¯ Ê©ƒ_ؽf*ÌþéMxì?B¡V ›cÖcê@{cóU ~o¾Þø×¸o£ÛáÄs•?ÇO=ÊzÙc&º0“Œ\,Á<}æé‹àòûPßi‚ÁÒ ƒµ.¿Ž!„$¹Þ^Àï"xé ¬Þ¸k©¤›u‚”NÌÃÏæOÃßvæ´|ó΃Ț4ºÒI¼lßíÕh‚j€™îA•¯Ew³ñ¢ÇÙVyhþæ f—@® ]ôùáê²öÿL'N•!5SwwäÂ^?‚^„RI¶»³æDÄÏMÍ΀$=í‡k¡ÇóŽj„üÑpˆïcfé„B‰5ú‘B!äΆjìÂ=Ø}“ÓÆa‡ÉaOxâ€ÑçÀ·íÕm­ø¢ñxTëV¨Òñìæ÷1mÞÜas>zN´6œ„ÇéŒøùžûOxœÃóš<ÖiÂäl-ÈðÕwüÊò àò—á…ÝÛy *BaÇïãvè:[B"¥¾$dÐ÷)€ %ºë ‘úþRÞØ²Ígº¢jcVÞ<¼}ÃG1 ;xƒ4õ4Àæ³Ä¤}½jæT`OëvNË—éæbœzÚ/ñyR‘º8iÃ6¯|¹0î ñ³‰wáç“U[}UEÎ HؼloùUm»±ýÔ'1 r zØÝú%vŸþǺjr\Û­xxÇÝЧ¡|ì (ÓÍË9è<[eRæåÀKè¡®± ?¯Æâ›Ëè¦ ËoŸ‹Ú“mœÿ^Úð)æ‘C.–àÁ²9øÍןQgBHƒáÿøjGH« ”H øâø2V H\e‰d·¯îç»ûÜ^¼k¼Óíìt10ØšÅvã¥SœÙ ˆ]Õ5AŠËgóƒ~ L¥>%d0BâzãTåA,¡>ˆÃíÅê÷wFÕF¬ÃÁPM–t{Ìqé¹8 cÓÇ£ÉÒÀj¹PoN¿rqÚE¿ËWê“2ì`óZ°jßãØÒ°!æëº½x * oEŸ[ºÏ}᣽k«WñºïozmŽÓ¨5„ÁÖ”´×}5³ÕÌ^d¥j±è²¥¸J7wÀó–®pàãÜÐÚïáέ×rج޸k©IŒ¢ü,<¸pÞüh·ûޱG6í8¯‚A4BÁ ºNœFÎÔ"œ8–*åVykT¿à*4ï<q[ ŸýY“îîÿÙÙÑ ENFÂ÷1^R¢ØO.Õ0‚ÞÄM&ãé²ÂaE ¡T EŽæÕðvŒŠÊ°µnc¦…B‰' <B!„D€a €é:n€¥#y_:NâÇûïÁº’Õ˜“qÅ Ï]×¼‰Ó:®É޼¼5×°Ã.Svwìy¥I¤¸"75áA‡ÓÆacÝÎN¬Ç;³^Šùö&Sè¡ÑÞ‚ ÊÂþŸ# 2LϸŸ¿bµ®OÛ¾ÆæÖχuÈA%K…^•]šzU&´iÃgÆ´¾Ä<}<º:Ð`îÀ‰®á;s9ËÑ9_4ñN+™Š¿Ú …zø”K76œDË* ›V¯Å÷Û¿‰j½3ÇŒE©vLøï¸ÓŽFÈ ÜDÂ à˜¹ÁP¨ÿ1‡Ï ·ßüt5{eyœZ ÍvI!l„Büºîí ·)Pß2‰ðzc¿±˜®ÇKÙøy5œnîƒübv°y-h²4Àôĵ__¾Ïï}”õrÕÌ^Ü4þöóK—¨‘ÎçRÉ¢Þ\‹;ïã<Ó~$Æ(õX6ía,*^óJlå+ ±fÁ»x´l%Ví}_µlå¥Ý­'?6׿Ùm›‡^Ʊ7¿7¿=&Á‡ CS²Jñöá[Ì©½ºÆ6lß߀ŠYÅtO… J°¯®uÜf—o­: U¾ã¯½Š—íé =h&Mx @UƒÎcO¬Rü“kÐZU¿;²>]'O£ù›ƒý} ao7C•À*ÃEj&û¿7v£ ¹ ÚÞÎÚÈ–éyZtlû‚NÏh8”»;Xè¬&„BH¼QàB!„†aîÕét-žÉûi 8°ðûåxmÊoñóÜgôiqY:€rËY…¸ÌæëNmŠiéÒÒ15GžÅþå…EøŸ#‡X·×ä0à‹öݸ1·<æÇ7¡‡ùÚ«YL§u*òY/ó5³wØ]‡zUF¸rÃÙ€—JÉH&¡T›‡Rmx–ó¾ðCCW¼@Ôí«âXåBÊò˜¸ü>l©¯å¼>…*ÏnÞ8lÂÁ@íM§v¨(*ÆÌ1úþŸµiJ”jóÐÐÕíM °zÜqéƒcfSÂC»Ýv^Ø¡O‡ÓÜ´ti„kÑœ?Ec4Ô„ÂæõD06mÒŸ?B'"1ðÇnBQxäb¦n;þ¶ë0çå'kJ°rÎ+1Û>£½F»!¦}àò;Ðbm‚ÁÖ§ßzsø½´ÁÖÈ©½êßÂÁöðgC…ª"¤IÒqÛ„»Po®Å”¬ä)è»ýÔ'X±sçö‡2+o–M{$áÕ"‘¯,ÄÛ7~„ª¶ÝX±sÎÄøœKF.¿[6`[ó,™úk”ÜÀû:. =ÌÎ+Çoçü/ì{‚S{olÙ‹9%…HK•Ò͸¨ÿ1kko!‘ýú2Uq5 pwöÄd;‚^?†*œcilúzKKƒB¡—ùcf-Å„BI$ <B!„pÀ0Ì:NW`ÕHÞ×ÿmÿ Gí'ðÑ•o@%úaöºfö´²,\“Í®2—Áÿ5=õ°úí¼õAßLû5‘UD¸"7ŸSàÁä1cóém¸wü¢¸Ûã¶&”i¦A”ýÛµ8Óqšž1¥ÿg™0²A·óµ³‡ýu¥We„+7¨3GTõ†hõ…<@å6á­BÙ_9"”’Ègí2XºñwCçu=öÎ1¾tà//¼„¿üî勞sdÏ^Ù³yá%<»ù}L›77!ÇÑãp¢µádBÂzU*‹§ú™H„Š¢Ë WgbkÃ^*‹ vÜÉÈãç8Ýøœ’Bê* oyˆ;—aKÃÞÛÎA‡s¥KÕX9÷ÌÎ+iŒdg°5á…}ã¦ñ ±¨x)äCÖeáÂÐÚï⚊8õõÆmÕ¨˜] m¦$1J'æañMeظ­šÓò~·ÞÚŒ¹ÝÍ[ÕW—B‰ÊÜÄM!”ˆ¬òÓî¸ÞÚq[LÝI´VÕ¡`v ÀçpÂÕeå-$2R¥çkÑu’Ý$4>›’ô´¸ngÏñSƒV—HIIJ¥‚T:*&’±x”a˜utB!$ÑhT !„BG ÃÔètºùÖ(ÉûzÔqWí½ ]ñ¦*'a]ófœgX·Ã6ìD>þ\Ÿ-ê}î 9èÕÙ½lަÊÃæÖÏqc^9t²ì˜WOÐ £‹á¥b¹Á®ØTó(͘ŒÚžcÃæê?ŸÎþK†¸îE"”jóPªÍƒÅãÆ‰³•LNû }|Çå3â¶b¡UPec]5çuÝõìS˜]y3àí'~ƒ­xkÐç;­6ü¿ë+ñú]ý!‰xñ8œh®=Œ Ëá|„T²TVç@±&÷_q56­ôÜŠ׊|RJ¤pø.ž9M( UDÓ[sa÷y9-W1«˜:o˜ Óö 4ã¼@ žýW  þ"„½¡áÑ&!#™X(ø|áŠ\¤ŠÃ•RR¨O³}çeß¾ñ£˜ Ì7Ú[`´xi«šÙ‹ƒíßbO *³òmÛìoۃתWA)Q¡bÜ­˜WŽŠÂ[y •ؼܹõZëªãuÛÇ(õX³àÝat¸è=Õ¸[ñyÖwxà‹…¼÷Ùp²­ù#lß‹3WA¯šÀ[»ç†Ò¥j¬Yð.~õåíœÚÚðy5ž¼gÝÔhñÍeQ…èlÆÙ´3–ü„·m²·wž $.žŸÅŽÐŸ]êJ'A3q,«ÁøG6í€f¢¾lFdê4(ÙyIr ]'Ù-ãµ9y<ýƒ8»9D"T*D£c++€ù ÃÔÐÙK!„d@_½B!„Dáì‡<óÔŽô}µ¸îÀbüOû§œª;À¢±7³z~šX×}Ô«20O_„Å¥e¸®¨5Ù¬Ã}®È͇„ÃÛ΀ 8±>~ç°»3q×ç‡u˄҈ªyX|6¬=þ.š­I}½ôŸK%exv^—^…yú" ;p –¥bæ=î¿òjÜÅÕ˜9f,ôª áî“49¸ãòéX\zU\+e¨d‘Ïnv¬Ó„c&Në™:o.îúíÓ€Ã{öv8×ï-Žïß sWÂÂR‘ÿ4e:ës@-KÅýW^’8V‰·EÚ€Á†üt„4"›5—ߣÍÂz¹Ë sP”ŸE8 õö~?àq>/ðŸ =\ð_ þ× x=€ûìóƒêCB¢‹Ñ4ØšöB@*Dbv× %\%Bš ˆÅtý ÅÔmÇW”­ä}p{0@½¹6ê°ƒËïÀ¶æ-xè«»ðû+‡EØáBvŸ[6àÉ¿Dé{Yxà‹…ØÜ°6¯…s›±;(%*üvÎïñíÝM#.ìÐÿ^VYˆ+¿ÁdMɨ¾_˜Ý&<³ûWØÖ¼…×v;]&4YŽŸ :µ [®ãÔÎWû`궃$ÖóÜU¥ÖªÃhøìï¼n“ÅÐWW⪴„B(rþŒ~ê×±j«¯FŸP0KK;xƒàvñY¼oGhòe>›ö–ƒBR©™™™£%ìP `:…!„’L¨Â!„BH”†±˜®ÓéÖX:Ò÷÷‘úàr»X/Wš1…uÕQJ|_®^ž“‹\e:/m¥I¤¸<'‡Ú¬—ÝÛYšžz^ª& ÅôÂpF6Laû*çÔ’Áûýcãv¬kÞ„OŒ_%åuAâC›¦DEÚe ßAJ 2S#¿f¶5r«F¢P¥ã±wþÐÿó'¯¿ÁjùŽÓ­Ø±á¯¸nÉ/bÞ'=¦N²^ްTO…6MÕòzu&þ¯áï}s¬Ó„ÉÙÚ„¯B“4Ùèr» …§´VËR‘*¦ê\T·q Üý²r6uÞ0 †C '³F0þO$RäI')à~Ö&!„ý¥“-ˆÅ?üë ¡Þ‹Ÿ'†«9P¾–íUÜ“5%x´l%¯ÛbóZp¢ç(!îéM—ßÏ›·`[ó¸üÎu¬¾jÙŠ¯Z¶b•äqTŒ»‹Š—² Ô›kñÀ— q†§Êp}a%VÎ}ùÊ­¤KÕø°ò¬Ø¹ _µlÕ÷ Gþ„k#–Ný5äâèf@ït1ØÝú%vŸþf·)êm»oÕ_± l"ΟF$HZªÏß–¿´™s Ÿ} U¾ºÒI¼m—ÅLž¨JŠœ 8;º/ªò Ê×¢ø–kÐðÙ·‘ÿ½4vàІOû+ax¬vxí.H•r:À)ð`ãÿ5DÐã»äïÜ]VØßår9”Jåh9T»ÜvöûoB!„¤AB!„ž0 s¯N§kðÜHßW¹\‘H‹Å‚ÞÞÈFŸÜ˜Ë~V±4·…çk¹ *ü¾Ýˆ[”ü… ¦æèp´£>–³ÀN¬Ç;³^ŠËñ „‚Q·Q¨`xpœóewþ³…µ8X{ü]|lü癤º´ %ôêp¸¡X“C7Ž+on IDATÀQ&S.‡0ÂQ«.¾ã8@úþ5ÿ ­~lÿÏUÿ·uU[?‹yàÁØp=¦VËxN¼ýÄoÐÞ|*êõÿ´x*/×a©6:…êÂYS² ä(Òèâ’ËïÃÆÚƒ¬—+ÈQ£tbuà0 >?m…B€×=„Ô·„°!„¯!¾Û$„DùúRþðk;Çê+ç¾Êëv0N#Z¬MQµ±»õKl8òÇt¸P_å‡- 0F©Ç£e+QQx+Ò¥êK.So®Å[¯…ÝÇÏ çJ‰ k¼‹Šq·Žªë%]ªÆÛ7~„;·^‹ým{b¶žYyó0E3S²J‘¯,D¾R?`¨$\Á£½ßw`OëvôxºâÒ{Z·Ã`mÂʹ¯p =ô…Žuñ[Ä: á«ý øjJ&äañÍeôÞ8Šò³°âžXóþNÎmÚð)æ|Ë¥¬—quöÄd[‚^Dœ{^›ó¼õ …B¨ÕjˆD£fhÝc ì¥3•B!ÉŠæ2"„BáÃ0ëÌ`éû* ‘™™9äÌ&7æÎû¶Ý;îNË}ßnäu;.ÏÉEšDÊiÙuÍ›áÄþ '™PÊK;Ó3ØètœÈ¨°®y3nÛs?26—ྪIvPÉR1sÌX,.)Ós®Å?]>¥Ú< ;Œæ7Ð))ÈSF^v»ÍnÅ3Ü‚ ¬ù¤î ÉæÚ# ;Ì3–×°Cµ,‹K¯Â$ªÞBÎñ‡ßr®ÖB3X/½½€Ï»ö}^þg«'dd¿çRxl/4+=!$9q­î°lÚ#¼¬ßåw ¾«6ª°Ã¶æ-xf÷£2ìp.»ÏŠ÷¿Ž}0+v.ƒÑÞ€ÿ°ÃoçüoßøÑ¨;áJV~ƒÉš’(_#¤à¦ñ ñß7}‚;.»EÅPˆ£¯”8;¯–­Äçw|ÚûÌX½à¿1+o^LúÂ`kÂC;îB§‹¹äs¡¤—ðËm·bKƸWa©klÃâç>ÀÆÏ«éÆgOÞ³ãÇh8/ïî¶bï«Àïæï s(D׉Ӽ¶É†27{Àdz&ÅøW±noß9ýã±Úá±Ðd*â³JH´.ïù?»=¸1~•ðóY«P¢¢¨÷_q5šù#T]½:“nj§TAÁÁÞ^,Ý8aî„Õãf½ž/¾ãK§nêÇrÚæ4U:ïý`3w¡¹ö0x:ÙmiØ€}0|}ùú^ÂY©ZüþÚu˜7'ºÂhoAÇ oÐ3jû¹/ô IÍŽâuz/ê͵ýÕ:]&êØÆiäu;/Ň•ßàïw7⑲•PJT¼ö…ËïÄfflë‚ëÙ`mć^ÂÃ;îÆžÖí ?f·UãÁ7qªlC¸[óH%©ÎËÛŒ8²i¯Û”ÈЃ\£‚P2ð„©w\‡ô|v“¡øÝÞóB!CûE${îÎ{á9絆_ÃØ&Èårddd %%e4t±Àü³“ùB!„$5 <B!„ÄÈ9¡‡Ý£a/õà ¹åH)â¾=…Š|,;§eù®ò«L‡.Û ã½Õ¨é©Y?‰BNA…Lϸœõ2‡zŽ&üÜí 9üëÌáþ+¯ÆÌ1zhÓ¨¬:9çsJ Æeh ”ê ;¸ü~4tupZß]¿}zÀÇgÿ”}xlZù5¼õC0@{Ó)ê# ²Z¶½©o?ñXLQoG‰6•ÅSãrì+‹§â§Q¬ëʼ±t c fžÜþ ÎØ¹JZ¸ „:réí‚د'BAêoB"~Ï"øg’’n‹B’©ÛŽæ3]¬—[T¼4êuí-hè>Š@ˆÛ‹ ƒµ«ö>Žjf/ÈAl=ù!y¨fZ¦›‹—濼´Ø|t{Ì0Ú hè>ŠC¦ý8dÚ&ËqôDQ©c¸J—ª±á–/ Há>üÁ`k‡^êÿ9  ÅÚ„zs-‚!~ß(ä+ ñhÙJÔ-ëÂêÿ1J=omlMXµ÷q¸ütº¬Úû8žÙý«¤:œ«ùL|qöÕ¢›Dœ¤¥J±æáʨÚh­:Œ†ÏþÎï{䆫80cÉO Ne7Y”ÍØCë?í߯ž–v:ñ. ™ÈîóÒ 76¡‘€ë‡ó-äÀgs ä @äô:ÁÛS  ðì÷Ù„B!I„B!1Ä0Œ…a˜ùÖ†ý•H$ÈÎÎ>¯Äë½ã%l{žŸö·ãƒ*å…Eœ—}±þO1ë£,)Õ ¢©Èo…Ô²Tºi‘‹(¥RLÐdGTÙÁ ±«Þ@øKpƒ…ýìSWÝTqÉj·>¼œU[ U:®[ü ^úÁãp¢¹öÌgØÏ:úÝö¯ñúƒÂãtE½ñ ;ô)ÕæaqIë{„\,ÆíSh°ûpäòû°¥¾«vo‡/È}Tº6S‰ŠYÅԡÈ?Ž/Ôß„D*%Hh2)8ÛF õ'!$ùp©î…·r^g08[ÀÀ¹ ƒµ«ö=ƒ­‰bÜ4~!ž˜¹ª¿Á@¼A:]&4tE5³k㨪ü0%«¯ýø}¤DñªaOëvìnýò¼Çl> u쿨j_/Å·w7aõ‚ÿæ­âƒÁÖ„§v݇wÜc]É[„Úéöáù?‰íûè"“¢ü,¬¸gATm4|ö-Z«êxÝ®¾ÐƒÇߪ2µ’´'ìRåk1õŽëX·ÉÔÄ¡ áЃÏᄽÝL'^’ò;ŸWûlNø{ìèÜ~R‰t´ìþ'Wv°Ð™@!„á‚æ3"„B‰†aîÕét»¼7Ò÷5%%6› “dã “e'l[úª<¬oÞÂzÙ*c ~6™¿ªi)fäæã‡ê&ëš7ó „˜Àqö°šžzX|áPH‹Óˆ§Uæï“úÜTÉRQ¬É¦p‰ˆR*EfªrqdeÞ-7L;B½½ý¬=¬×{å ?¾äï¦Í›‹Êý¶þá­ˆÚzì?B¡Žî‹ò` ó™6tZ9-ÿéŸþŒ½û?^ŽÉÌ1cQQtYBν:ÿ|ÅÕ8Üц/ù|¹XŒgçU [žFÓ0³ÇЄ-õµ0»œQ·µøæ2êÐa&ÇB0®(Aƒ¯ ‰Œ@Hd€Ïô²}Šð²šþ‰œ£·è …O:7H¢q <\_X‰t©šÓú¼ANt…3ŠÁÛ»[¿Ä†#„ËïŒKéÓ‹ §aJV) P5rÑÅï·çxn¸‚.¤ÈUäcûÀþ¶=ÃöypÆS(/¸Õ2PíÎ3hwžA¦, :ÅÎçÌpò“ ÿ„fk^=øïœÛØpä(L/‚^5á¼þ¬ïªE‘º²¬˜lû¢â¥¨(¼k«Wá½Ã¯GÝ^—»cØ·5ïï„©ËNï£ã¤bV1jO¶á«(‚&G6í@z¾vÐ ¬ß“ƒèn6B­Ïƒ\£Š[(s³Ðurà¿g³K`>q­U‡YµÙZuª|-Æ_{ìí¤É!Uʇý¹ôù²^wgR³3xo7àöBœ&‡åHº¿­CÈ?jf§xa˜GénH!„ᆄB!qÂ0Ì:Ng°€j¤ïozz:²åš„oÇóÓãxèv»p²«5ü6¦æèp²«Ÿ—õ²ëOmÆyåƒH'ÏÎÌWÓSßÿx¥þ¢çÖöÔšk¯¯šƒ^ B#‹¡”Ê ”H! #^Îä°£Ûíºè1¶d 9ô—O†Çá„ì3‹=ðûÿ€AC U:{ç˜]ysTýa3w¡­éü^ö÷,É ÏýNÕáåØü´x*Jµy =?d"*‹§â¦‰“ñÅÉcØch‚k€éà¤/ÂíSJ(ì0ŒtºØÓÒ„/ xL¹?FCÕ†™Pˆý êhõ†€!õ=!‘ij¸K0Âq(B S¸ˆ„Á@ø¿Ð7ü”@ DÂð¿„ÄS“‘ýÌÏã¸Uwpù¨ïªE ФçîÖ/ñæ¡—cøÞ\)šé˜’U }ú„þC$Î}n¾R|eáy¿7Ú[`´PÕ¶õæTµí†ÝgMâÏ)X>ãi”éæFÕN·ÇŒn™²,èUE e#úšzøÊßÂps>O]~'Þ8ô2VÎ}弊P ÝGQœyyÌBéR5VÎ}‹Š—bÅÎûp¬«£ÅÆmÕhò€xyòž0uÙQ×È­ÊßížW?Àu¿ûˆSù½§X ámŠWèAª”C’¦€Ï1pèaÆ’ŸÀj4Áfd":²yÄr) f— §ÙˆìÉã ”ˆ‡õyôEÿ¹YÖ¤±è:yšÕ2ol‚>‡ –C'еïðhºüïcfÝ !„2¥ôööR/B!„Ä‘N§›àcúѰ¿zÙ¬÷Bö3êOϘµ$=êm¸·ê N¡‡4‰?Ÿ:ƒ×þ0Xº±£ù§eK3¦àw%O Ñn@££¿ 5–z8üN49 tq#]*ÃÔœ\Ì)™ˆrÞäb‚”ÈD"È%(ÄHEbYŽ„ ööÂhíp€tCW6­aÕÞ”9³°øßÿ 2…ãK§B8ȹ{xÏ^ìØðTmýNk¸ÚJÎØÌ®¼wÿöé¨*;8-V˜ §ûÛe«¹ö06­~ ?3 &CØ¡\,>/2³¢>HâT·µ¢º­ߵ歚ÅhpÆðÄv0,(ð@!±ÕÛ ø<WtJ ‰,~ $–jO¶áÉ×·²ZfŒRoïf÷~Ó`mD»óLTÛj°6bÕ¾Çáò;yÛÿ¬T-]¶Wéæò:x¼H]Œl¹ŽÓ²Um»±ýÔ'ØÞò ÎØ3Áˆ\œ†•s~“°C©P†âÌËc>h?‘l^ îÜzmTUž˜¹jÀN¼B°¹a=žÜùËQuo|þþ0§dý‘ˆƒ&£+^ß §›û º’‰˜ù࢘l_Ôwšbrè³ø¦2,¾¹Œ:~JDàA ¤2ê{{½½áJ¡Ðù áj4‹}üÕžlƒN£„6“^SÄò¼çvèC¡Û÷7`Íû;Y-s}a%Þ¾ñ£ÈÞó…h±5¢ÓeŠòuµí¸‹·°C_С¼àÞûT*”a†v/mÕ›k±¹a=67¬[åAŠk®}ÊBB˜¯¯PU"Ä^c6¯×|PÄùøÉÅ ü×ýeÀ`ƒBœ†)šÒ¸TʨjÛ¾X˜Ð $³òæaŠf:Ò¥jLÑ”ÂhoAUÛÔtì‡Ík7ÈßÀaªœÿצlÃw¿à*L½ãº˜lŸ27ÊÜØŸ AŸ¦#ƒ>§µª‡6|ƺísCª|-9™Ãò\á#ðÀÔžÀ·ØUdÏ]‚Ü«KxÙWs¬ß5 䌖K|7€Û†±ÐÝŽB!Ã!„BL§Ó= àÕѲ¿åê™X’» áÐ3ðOPê‘/çg¶‹Ï†ÂOæ&´ÊƒÃç…ÝëE»Ã†æn3¬^]©d©(Õæ¡D›GÕF¸¾ HÏþ_ A,Æu¶ý6»VÏÐ×îÖ†#¨3µ±jûÂÀCŸ mrôc!‘IyÙŸÇ §Õ ›¹ ¶®î¨Úê1u`dž¿àûíßðÖÇR‘E—%mˆI’‚⬺8“X§Ëcý!‡Ö¸­·tÒ¬~è§t†©€?zˆ'ªð@b- ŸÛ.$"!  F1Ñd4cûþÔžlC󙮟S2!¥ó0§¤öñÄï<ŒaJI ‡Ó(DbeãçÕØ¸­šÕ2”­Ä£e+‡þ;  ¾«N¿#êí|f×0Øš¢nG.V`QñRÜ4þö˜õi4Õ³ýÔ'ØÜ°_µlùy¡”¨ðäÌßab攸œ‡Ùr- Ó'Äeà~"TµíÆ/¶þ˜óò“5¥X9÷•—.QcJV|Š8×›kqçÖkãz¸¾°ãnEEá­H—ª‡Ü¾w¿†- xY÷ø1¬y¤i©RØãÀ»ÐŒ%· `vIL¶/^•†ªòÍßÄ‘Í;X·-N•¢ü7¿„\£Škå ¾„‚A0µ'¢nÇ|â4ö­ý€Õ2|¬ß5ÀÑpz4]Ú놹—îp„B)(ð@!„’t:ÝmÖP†ýÕËÆ`yþÝ(”ôy…iù(äq†±xTyðèr¹ÂáŸÝ.gø_·‹Nt”hóPœ•ƒb 6n¤"„猒‰ÄÎùùl€)‚þC²öö¢Ýn…=Â*kÂ`íaµŽçþö×A«9¤k2‘ž¥B¥b~ð8œp;ð8œpX¬ð8£Ÿ‘³½©ß~´•× Cßy²¤äª¤¯ «L§°U’1XºqÌlÂî–&œfyíñå§Ñ Õa,¢(tà H¬ôö>ïÐA‡‹ÎIQø¼¤ÝüØWw olÙS7»Ð½6S‰ŠYÅøÙ‚i4À/ŠkÀã¦û5V¿¿_ío`µÌ_+¿Æì¼òAŸãò;ÐÐ}”—×7ù#¶5u;eº¹X>ã©gÊçí=%Õ.ÅhoÁæ† x·îµ˜<¿iüB,™ú븜‹ñ¬VïÖ½†ö=Áyù%Sÿå’Al¹EêËâ²õæZܲùʘ®C«ÈÃ¥O`QñÒ!C—ºFÖV¯â%øð³ùÓ°üö¹ôÇ"‰ÿ&]¨ü7Ë Êׯdûâzƒè8Ò„P08èómø­U‡Y·ŸžŸƒ¹Ý iššIc!N>e½vºN¢n'‡/ó×Õð÷ØGÓ%}Ã0ëèÎF!„‘„„B!IB§ÓMð1ýhØ_¹0ËÇܫҧ]ò9YÒ LUó¶N>ª<ô Ýa ÿkÿËœý™ðK«Pbf¾Åšœ¤?ÉÏésnhA,BrN¥©H|^Àa8 ööÂ`醗ű\O½ÿ2´‘…y„B!di Hd2ˆ/?„A¸A^ }zL¨ß[…ï¾üíͧxïçáv¥TŠüt5Ý,\Åá4ªÛZav9º-ŠT þöò2:(Ã߃c#ºïÉ€úžðüº%ø½×Oý)€„f³ŠÃíÅš÷wb_]KÔ[Î/¡à> ð×^ i*]$6V¼¶uìª~¶è»Ag”wù¨ïªE ý…Po®Å ûòs–Lý5Ê nˆyæ+õÈWÆíømnXµÕ«pÆnˆÙ:Æ©&áÙ9«céÿÛ3ÂC|±s…¹XËß¾dõBUtox°x<¤¤ô‡ðþº%>£]H¡®šŒf¬~'šÏtñÖ¦"U‚'ïY€9%㨃#‹‹ ¤ü=‰_ÜÄúžqêÁK¿?ít1h²4ð²m.¿Oïzf·‰sr±+ç¼½jBÌûR$aFά„ Öuð!Þý8ES—€E¼Ù¼ܼùJÎÇi²¦+ç¾rÉß©‹/ˆàÛª½ã½Ã¯óÒÖ¼‚ ”Üx^*]¢4XŦÏør!ö·íá܆6S‰7žYDÐ8q¸½XñÚÖ¨^Ïö èÕ@~qª šIc!ˆÑçÕ‘Vyð»=Øûê°;X¯C3qlÅr_ø”ÈÀCÎŒË?¿Œýù|ü4¬ß7Œ¦K¸á°C ÝÍ!„2QàB!$ étºu–Ž–ýM…sR¯€x€`ƒZ’µ$sÛŸ 5=GûôQeþÁÞhI†Bñ!‰  !‰ Š!¤@~6Ø@.Öf·Âêñ°^ŽKàá'ËÿsV&l_›k£½ézSÌ皤ÉAeñÔaWÁE¯Î k':]޳•Zñ][kÒn'Í892áYÁãξ…B€ÏÃ_H[($4¦Œ‡Û‹å/n†©Û“öç”bÅ= h°ßP×BðzùoW( ß» á[ÅCo²z¾R¢Bݲ®K¼væ/ìŽüÛš?âþž)½+ç¾·óñ®î0Xäb–Ïxeº¹1ß‘\é¡Þ\‹[6_Éyù%Sÿ7¿ý’¿gèáªõc¢ $Í+¨À¢â¥—ÜÞ)šR¤Kù©n¹bç2liØÀyùÅ7•añÍe ñaê¶4pû IDATãÁ7ÁéæþYW23\³mŒuP Ò*Ñ„ fOÃŒ%?6¡W—C[ÔíøÝl{âUVˤåk1éŽë#Oà  §ê<ÆÎÑté~à^†a,t#„BÈHEB!„$¥ÓéðêhÙßÞÞ^tww#ÐÁ%$B! Õgå@¯Ê C ÈÅbÈDâp¸A$vÊ­Óå€Ùéä´ì3lob7ÐD­ÍÁ’ÿ r‹ÆÇdÚ›šáv8áq8ÑÖt ‡mM§Ðcê€ÅÔ‘>ž9f,*Š.–çbÇ`éÆ1³ »[špÚÚ“ôÛ«ÍTbã¿ßMn„ð¸X\JÕH,xÝ@ˆçsW" Hd¸ÌÒΖ"U‚5W¢(?‹:üü~ ƒ"P •Qÿþ± <ÌÊ›‡+¿¹ø5´µíÎ3ü½&·6â™Ý¿âþ~)Îa‡DVwÈæ†õøÍîåð‡øOÓ>8ã)”Üó}É¡‡wë^à ûžà´¬\¬À‹åojˆWèa§a–mû)§e_šÿgŒMüó'©P†ÚY¼mo4¡Eªo>s´™Ê †ìçNÈŸ@ Äbª\ÆE“ÑŒå/mŽªâ[®Añ-?ŠÙ6Æ:(`:Òˆ oè–~·;žýün/ç>¡‡HC ‘Øú/ÿÅêùlþ;zªŽÂßcM—ìk À¨FË>Ûív¸\.:ø# %2‘é˜9f,ôêLêžûW!‘@.ÿGá†èxœêá>`Í`鯯ºjNËæŽ‡)sg#C—ƒ mNÿãyEã!KSôÎÕcê@ÓqöÿMçüâ ƒ‘ŠD¨,žŠbMΰ=G(ðÀ¯p‡Ó¨nk…ÙåVÛN3MŽ,±šü¼{ ìÿgïÞã*ï}ç²æšÉ 3CfH d”B¡[E(ÚV°G O­ØSAÄÝîîSj±çl[ñÚ§j/¢ ¶V.ÅhUÊEAÑPHÀÄ ™$„5L&Ìdn™kÎC"ÈmÖš5÷ßçyxÐ0ﺾkÖšÉû}ñ³„%UÕI(œ“¸Wß­Ç«ïÕ§m}+ïªÅœ)V:ðªÀƒ€BEÇ—OˆÀC«ëK8üvA·kõžhv6ðû¬”æ°Õ¾®/èÂO¶ß‰Ý[_v:C†_—×Þíu³°¯{7¯¶“ÍÓñ𵫳¢OÞð· 8vº™s»ËUª$txã¾÷oÅíuüöuŠÜU{ÞÏC¡øóð¥îá UØãeÛ¾¬ymGR˘´è&ŒœZ²m”Èè+ËÁ(…O¦r©hàî²cïÓ¯ó = £l=äBàÁßÖ ÷þÄÂ5±ÚX–}…Þ±!„Rèc!„BHcYvç™ÐÃ+j aŸ5 d2Ün7(œ›»´ %tr,:=´ %Ìj LE:0S1 4rR ÛãNª}2ž“mÇq²íxÞ[“ZƒùÖñ9ýž ‰ —2t¡$Á¡ÉaG}w'öwwÀçì¾Ì™JNó‰XH¥@ªŠŽÉdv ÂKUˆ‡€ÄTåá’ì½lÞÙ˜Öu®ymG|ÖÝÓé¤ };ARõþ‘Œh,‚ö¾c‚‡šzx‡TŒK'ýGZÃÒ2›>WÅrÖÝôtyÚqï{7ãHï‚-ûÅ¿…?ìMhÀz2|a/Z]_¢RweÞ]kj×âÆ WÃâþýO=»õìL6_ü>Üå±Áö¡RgMi•ŒM·|„ÉëJ9Wùüäž„úÃoôúZS»·×ÍB³“û³ÓûZ°tÁ4)åC? ‡/v¼‡‡B€\LŸÅ8ß1ÅŠÖ®ü}ç!ÞË8¼a;ŠËMЖ›R²ÑPÎ#0T<ô 2há9éH¨Êƒ¶Ü„iÝÉ+ôp`ý?†ŽÑà¾dcè!‘ãIîý-ð¶tÒ%ê0“eÙƒônE!„BA#b!„B²˲Ϫô0£öY.—C¯×Ãív#‰P'HÁ@ïs$e`>k°²©H…D …”¡`C ‰E"här¨4r$T=%\ýxï±h‡Áæ>Mô,זœÊ+Sz †T²¯ª/Äbè?kªßd×ë•*ÕEtýñàð{ÏTrèÄþîμاiÕ0éé¾—oY|PLTàÇPÍ*JR``ˆÅR·ü(.kóŽFø¡´¯÷ï;Á]p¦c"â³7aæ-IIˆE£) =hËMèmëJøµ×üh!ö>ó:çõì}úuÌøù½Pµ¡‡H0;Q_ÎÝ ŸöÒ¥Ù€xØÁEïR„B)$ô+6B!„pæK«™f³ùË âAU*…^¯‡ÇãA  Npr©fµæœ ‚E; („ã‰$^ÉA¦€F.§’=~Ÿ Ë©6—Qàá ­B‰ùUßHªòÅÅhärÉäP320؉ 6C46€þè¥i§fd`ÄÎë(t6W/š{ìØÕÞŠŽ<¼¾7³šNrž’É€°ˆÛš‹!¢ °I…Xj—‹Ò1¾œm< apà…ROD3B“,Q,×]6ìà{Ñînýêÿ#^´»]vÙÚ1èì;Žž¿Š ¬‹0ΘþµåšŠœ8ws®¸—¶båŽ%ø ½Neîî܆qƉ˜1rnJ·½ÕÕ5S”öÊé8'7TÌçu>zv¼Û¶ ­‹/ùºH,‚vw+º<6 Wš0\eü8.©~sàšœ —¬R1ˆõ,ðãŒ5X>yÛû0¯ç®ÁÀ×€z,IÐüÜ-]0 ­]=h;áäÕ>ÐëÆg/nÂô‡îLáç–Ô„: dEj„¼‰}_l¬…I‹nÂõÿà´žp ˆÏþ°qèes¥‡l´ŸFï… jâ´u–SØB!…ˆ~ÍF!„’CX–]n6›x€6ß÷W$¡¸¸ ௯:#Ãp•~¨JC*“Ì’K¥ÐÈåÐÈC³Õ“ôð‡CG…Ùg5”`›T*Hµˆ\vmÙ(\o#h_‹DЫTÐÉ•IÎÞ& (P$”x‡Ôww  J­B F,æµ£Ë ¨[J'<1 á0ÿô%ÒørhP I•XªtŒ/eoãñŒTw8…¾ö¾+RñôM’-bhr6 ©ç ;ÚÝÇàð³pøí°õƒ?œ™çoKqåe~§‚^aÌêê_W,×áßÙ,hµ‡üRzhéýÕï†Dœ_ßS­©]‹ë^³ðºv6µ¬ÇŒ‘sª~‰EpÒw'}' K¡’¡X®úïÁþÁG¹¦SJ¯Ç¾îÝœÚ5õL(ðÐÛ߃`´_ÐkmIõ2llY‡fg#§vö^ö6Ç´ê+x=§Äç¬H)Çšeóq÷/_çýìë<Úöcüm³S÷ÙåLèAg…N¸  4#ŒpMü=bäÔøä\C}]§pxÃvLZô]„ýYzH4ô‘.}‡Zá9ÔVh—ãC,Ë>CïJ„B)T4r†B!$ǰ,ûÊ™ÐÃN@è”J%†ËåB4ZSŒÊ¥RèäJ¨e2Ë(Óh1iD9]y|¾ÕŒ *F•L ŠÌO0(زR)¦”Y°ÛÖZÇÒ¢†9•W Zaf0è Wªé:É"þpM;ê»;±¿»þp8«·W«PÂjŽSú#a¼ÚXÏk9·ÖRu‡B –rI|–ûh4þ÷ÅÖˆÅñ±"Qü¿ÅbჱX|¹„ ¢‘UZ»œœÛ(µL¼y.œí]èlø·Gmù`_ jÆ–Òý ñê:QË<ˆ÷BâÏálÖmӼѷfd¼š)â=#~6(×TàÝÛþ…Õ{VàåCÏ%½¼üj¦(¡ûù:é;b¹ÃƼº®&›§ã*C š œÛ6;PÏîô¸!.ˆÎET ÇkÛ~ï Ø¿Î§–ÎàU™boãqòÌ„Êr#–.˜Ž5¯íཌëßÁ´‡î„¶Ü”ÒmuÙâÁd¡BÚ‘%p4çÔfÒ¢ïçÐÃáÛa¨m¹ á@?ÍÇ¡¯,£Ì\E£h(;&7 Ÿö g{=bႪ¨Ü€xØá ½ B!¤ÐÑ(*B!„Ų¬ ÀD³Ùü €Å…°Ï"‘:~¿'ç¶ß¢¹”¹èLÀA®tæs’9b‘h(¤ÂH$`Î̸¤0ˆE`Ä_ýŒd·`DØ_–(¤R|ÜD¬oü\ðeg¹T «¡SÊ,‚¿¯©¥-]CYÀæêEs»Ú[ÑáÎþYÔª %°K`5”\0H¸ÛÖ »ßóÄÝ7N¦AÉŽÏHt2ªõDç6lþ3¬3®úÿ½ë6`û³/¡«±)éí|Vè¡©DØÀƒ„¾b!ùBŨ±Ðš™¯%sµºÃ×­šþ¦–ÎÀÊKà ¹“ZÖ ~ƒUÓžJi¥ÝÝŠb™q~ 3X:é?ð³]÷Áöqn»îÐïS4¹ …"ŒJzvNËñ‡½­q¶H,‡ŸÅp•YÐýX2açÀƒ/BÃÑnŒ«(EŒk•ºŸ'mÎ+Z»zð÷‡xµ‚8°þLèΔà2ôÀ(Ptð;]œÚ¿m6Ü]vôuâÔîìc …‡*=d*ô f>ðàý²îµÚ%÷âa!„B!„Br˲÷˜Íæ^.”}V©TÉdèííÅÀÀ@VmÛÙ¡…Tz¦zUlÈù>Ç0ˆÅK¥ˆÄC瓪3ä—hŠÞOLE,ª¾&/C—Pž F"I­F.§Î™Añ*¨ïîDß—ÕÛ:¼ì“—b÷z°ÛÖÊê¤@Å¢çþ¿HL€³AªgÓ9¾$_€[u0¥VsNئ-¾ Ó߆íϾ„·W?•tŇ6íAe™•寂¾.¤R@ˆGo‘(¾,BRÁlÈýI0æ^Ð`i¡IÅRÁ^gÒœ+nÆšqßÖ[qÂcã½؇Õ{Wàw³ÿ’²óŒö£ËÓž‘ª©4\eÆBëb¬?ü<ç¶=;ÞkÛ„y£dt*´c8ÚÝ­gL¬€´Ãoüº›sÅÍ(ÓX8÷ûƣݘPYЇ1Ø"UGÊÒÓaïõ`oc;¯ö}]§pxÃö¡ ©$dèA3ÂÈ9ðÀ(˜þÐØù×"ÐëætŒZÞùão›}æóxÎ#(.7 Vµ‚‹LVx‰DPDQˆa‡gY–]Nï8„B!_¡¯i !„Bò˲¯˜Íæv[h aŸ¥R)†—Ë…P(”–ušÔñƒV¡„N¡ 4€E§§Ž˜‰ ©r©jF¹”„FòŒ`$u¿¸ =¼Ùtîþ@ΣÁå>%! ^1e¸ºz¥Š:eøÃ!49ì¨ïîÄþîøÃá¬Þ^­B «a8jLeœª‹Ôµæ½Î•wÕRG!a` >X8b—ÈJ$gþÐ7Í#‘Ñhj–-¦K‚Yó‹þÛìe÷bÒÍsñû[ÿWRÕ|V>W‡Wu'Š”…•2ñ V,‰L³€LNá.’ö^Ö¿[ŸÓû bÔ¸1C¼ó¥ºÃÙÆkðîÂý¸½nšI|¦óaõžX5ý©”…NúN`˜Âˆb¹.¯ÎÁ¼Ñ °«c+l}ÜÃñ[ÖaÆÈ¹ ²h+QÏîIÙòûB.£ýK„a~NÅÍxùÐsœÚ4íÆÝ7Æ ‰Vy`º÷iå]µXùlÚN8yµïüôŒU£0rjuÊ·U¨ÐƒDÆðªòÀ(¸öþØûôë‚ ·kÛñ9Ì5U0V=¸l݈†ÂÐŒHo¸9S©T NI¬ ./7€å,˾Bï4„B!_{>¤C@!„’X–Ýi6›'"z¨)„}‰D6l|>¼^/§¶Z…:ùW¿œ1i Æë1€(% Ô2ÊŠuÐ)”ÔÁò”Ša ’É Äÿ¦pCa‹R;Í›©Hƒ~ó:|vÂÆ{fùtÓ*”°h‡Á\¤E«ç4 œûñA¯RA¯TÓµ˜f¿÷L%‡NìïîÌúí5©5¨1—¢ÊPÂë½ÛÖ »ßìÙÕcJQ3¶”: Ék@$œøÌèÑhü(02 >d‚DšºÀƒ”†¥•¡¢+?|[ñ(>Y¿‘÷r|V>[‡v[ÁK‘)€P?¿Ðƒñö44Ú`Ðáƒ}¹?Kq&wgCu‡.O;ž©_M-ë‡~6¥ôz,ŸüKL-Ák™ÅrÞ˜¯ô°¯{7ïm³õµbÝáßc餟¦lÿm}­˜0üêœïÇÑØ¹½‹Æÿí]Áy9þ°ï¶mÂBëâŒí Ÿë¢Éy0á pº¿fu¹ Û½Ðº˜sà¡ñX|»LžØ½^"Í¿Ï)ÑX §ûp{#ˆžI~HÄb(åRH$b(å ÔJ”òxõb!)åxä®Z¬|®ŽsÕ³A‡7lGq¹ ÚrSÊ—P¡‡âòô»<ˆqü¦-7aüm³q`ý?8µ;°þÌü¯%`”_ý.ËsÒh(Œâòˆ%éI¨g"ð R© ÑhP`lnaYö =5B!„œ~ýD!„’GX–m7›Í3<`q¡ì·Z­Fù0=&éŒ^`À²©¨8%³“Ü3pP32¨rŽt¼O(¤R\o©Dµ©önì;aC0Ñ¥)43i S(aRkÒöÞIA‡Ì°¹zÑÜcÇ®öVt¸OgýöVJ`5–$]YÄæêM*pôÈÝùUÝ!Dbš¹šœÕ'b@(=p5 $ÑxðúUúH$€X”ÜLö\®”ΣЎìþ~WTºâ‹fÑãkŸ€¤Bm'œxâµx¤€+‰D€\ „Ãñ WÂϧbªì@„ç ñê»õøûÎCy³Oó2TÝA¯0 >Ã8ýœkR H¥€XB< U,Ÿ5Ù1óÑ(0ПúRŸad@0(ÜòDÈ¿a©0ºÌ€¶NNmþùÜKø÷U]öuB„>Ø×‚Ê2n­­.ì냉ß㢑øŸ …ƒD¢ø=P*‰ÿMˆö6ǯíà=v6RH•è>\eÊè¾÷] ;œí‘ñÁ»ÉÌö¿¦v-$zXøyXŠÇpšÁŸ [_+ô #$âÜrÐå±÷³…ÖŨg÷Àæö¹Ùö¥¼²F¦£ýð‡½‚‡:¦–ÎÀíuœÚ´žp¢²Ü¯ê$˜øgšÁ €DœŸ÷ôN» ½}~Îí¼¼X§2F­Z£®2&¹ƒ4­ú Ü=o2^}¯žß{j×)Þ°ý¼ý©â²„AΜS1+uÉ0øNõr®òão›ž£6ôuJ¸MÛŽÏ1òº çUÂúáh>eDR!ŽDDýi9?R©:IÁ=?˲ìrzj&„B¹Ìó"B!„üIJì3gB[h aŸC±(ÞélÅ7 &\m4S'(@ŒDÃ@#S@%“åý ê@8Œè@ü·xžF¸É¤È%RÉ(üq1*†; ‡³YtzXtú¡ÿ·{=è„Ï ?¸úp÷Îkw¶ÁÃÐ+”Y{Ujhä :¤˜?B“ÃŽúîNìïî€?ÎêíÕ*”°†£ÆT&x§?Á†/ò®¨bÒkp÷“s¶/ Ägí¿ÀäCb±øìü"Q|à´„{”aÂCýi‡â3¤“ôK)ÃmûK>7Òìö 14œÛŸý3¾ýབྷ¬ò0HˆÐË›÷¢²Üˆš±¥}®D¢ø5"e¾z߈Q¥#’ZÞ@k^Û½íy·o3F}'#ë•K¦0ftß7¶¬Kh&úÕ{V`œabRaƒ5µkÑtq~¶'?ÿ~7û/)©:‰EpÒ×…rMEÎõá¾  ¶¾VøÂÞóþm¸ÊŒy£ð ›ìî܆…ÖÅ9_ùâR~íA—É'ð`wzλ×K$ùýY¶ÛáævøºP8 ‡Ë‡Ë¥œQ§†¶H‰XÌkywß8­'zxßïÚv|sMŒU£R~ cÑ(œG:`¨Å;ôL•¸öG ±ëÿ½„p ñÄúá Û1ý¡;/¸?½m]P—èÏ D}ÜRM©TB£Ñ@TXçnËY–}…žœ !„B.„B!yŒeÙf³y"⡇šBÙï9í8ðaNYd45c^ 8¨ÔŒ LþF+# # #‹ÁJnú^¥”A•a8ï_bå3½R wF·ap ÷× ¹NÅ0Ð+ÕTm%Å~/öww¢ÉaÇþîάß^“Zƒs)ª %) élkývŸ‡wû•wÕ¢H™›}7ÂÁIJ†#$Òø,™¤0„CÂ…Îî{Ñ(…gÒúlÌįáh„ÿ2Dˆ‡žècTbªÇ”rØp{®òzxôOïãÅŸÝ–ó•Š„$"êç$…ŽvãÑ?½ŸWUÎö}ë=™ùÌ®0d|ß?íÞ•Ðë¼ 4BÒàÕwëñÈsuyv˜lžž’j‰0•g|ÿûB®„_{ÂcÃ}[oMj}ÅrÞ˜ÿ!®2Tó^F³³óÕp$A—§=+ûj0ÚÖ×…#½_àsvZz¿ÀI߉ˆ@Åaá•‹ù}í܇ŸMûþ:lÚŽk"ûEÐtÁágÑåi‡Í} M= hêiÀ‘Þ/Ðåi‡ÃÏ"‹`œ¡`æjâ%‹¡ÓîJù:zûühn·£µ«‡ósc‘RŽG8—ÿgÇ@Ö½“ÆÏÁQ¸ÚOò®\0Xå÷ý¬¦ æê±œÚXÿÎeŽa?ÍÇáwº…í!aàîú*H&•Ja0 1ì° ÀD ;B!„pC¿º „B),˺Üc6›xºPö;‹âÎV|Ó`ÂÕF3u„#—J¡J¡2g¡ç×4Ó¡hþpÞPpèït’ˆ(ÿ~1åÚa8æt 60@ƒ'\Ž"™<¥ƒØ ]}w'š,ê»;Ñã÷eõ¶Ê¥RX %°K`5”¤uÝ önì¶µònoÒk°ò®Úœì#ƒ•’Q1 ~Îw‘Hjûa4B}(ݤLü˜‡‚@,vù׋H@*‡·ûDõ˜R4㬠¸=øÛŠGñÀæ?'ÜæÍÆšYßGWc¯mm;áÄ ›ö⑽¯’+žxm>Ø×’±õ—i,(×XP,Ó3cú8CÍ« üç®ûÑî>Êi“GLÏȾ©™"È%ŠŒŸãrMöaw¯ß×½ÏÔ¯ÆòÉ«x¯³X®ÃšÚ—q{Ý,xBü²njYkÌÓŸ™NúNÀ\Tžç'íÇéþ8üvøÂÉM„3cä\lür¯*O~¾ –âJ8ü—o;\eÂp•ù̵:jFÍë<ñ YŒ3LäulZ]-°õµB%-‚\*‡\¢@4/ÿ~Âñ"»ôÞþžø²Ð‚á*çmàúü•Ëz\>Dy°ˆ7‚·Ë #I¯¾X•P»Êr#î¿u^ܼ—×zÙÆ£`ŽÀ\S•–ý ú‡*=ˆyL¦”L•˜´ø»ØþžG8ÁpI ×ÎO1rêÅp±h.[7ú]è*FðÚ¯¯:ðöÇ÷W¥RA£)È tϲ,»œžš !„B¸ ÐB!„‚b6›gØ@[Hû=BU„9e‰%Ô ²P¾‡€ø,Y¬ÏƒS^úBAˆE"(¥ ÄiU&“H0BS ƒRMïú#Ø\½zàxëJhdò¼¨¸’müáê»;‡‚þpvWiÑ*”°†£ÆTSQf~yÙ`ïÆÛ-‡“ZÆÎÏÙê¡P| y²DäJ¯€þ@j×!‘2*ð“Ñsăñ¿EDâø‰$þ‡$q¿9ÚGž«ãÕöMÂÄ›Ÿ ×ÙÞ…ÕWÏEÀíá½½÷ß: ·ÖVÓ‰#D`Þ@þqkÚà–i,g¨Á8ãDL-råš NËèò´ãß^ç>¨ú¹Ù¯ ÎN§ m%ÌêÌWxø´{î¨û6çvÿX¸ãŒÉÍbßÔÓ€›6^Í»½¥¸ÏücJŽËp• •º+3rN¢±zÏ„¸TàHô˜?¶wEFöËR\ ‹¶Ú1°¹lÿyáÀo°»s§uübÚSI÷K¡<¶÷a4õp›ì|Ûïî/ˆ{ÌáV6­‡¯ã|xôOïcoc;¯u1J9fÿú0Êô¨Tt–¼ÚzNöÀsÒÁ{Ým~ŽÃ·'üz¥^‹~ý@B¯K$ÐYF@¡Kî{¹ž#y…›låÔá6|ù—­…XÕÁ `9˲¯Ð“3!„B?4·!„BHaYv§Ùlžˆxè¡`jEŸô{ñ×ÖfÌ)«ÀUu„ ‘K¥I$ñ€ƒ„#‘@!Íÿ%Þ`»;ÚàûÚTÛ±¥š4|¹OAnR),:=º=nS9õu\ÓrH‡ß‹ýC!{Öo¯I­A¹U†’ŒW÷°{=ØÖúeR˸{Þäœ ; pdYˆW`º&óQ,šúuD£tœ3I$ŠW| ©S3¶&½ö^î!„——¬À·~•®8¡×*ÊñÀæ?ãÉoÿÞÛûâæ½04˜V}o÷z°¾ñó¤ÂJÕcJ±fÙüœíGá0°ˆHÐm$?…Cñ@KªÉåZ#ùloãq<ú§­¼ÚNœ?lþ3§6ÛŸ} o>ü+ÞÛ«VʰæÁù¨,7ÒÉ#$I© ;HDR”[°°j1þ÷Õ?tÙ]žv¼ÞôG¼xà·œÚ]?r–NúiÚ³^aD•þYsÞû‚.Ü^7 ÍÎFNí~0áA¬šþTÒë_¹c 6µ¬çÝ>U³úP—7?~¬ï|aoJ×csîέØ{bÜÁÓY÷þcTšp͈é˜1r.,Ú1¼*üuþ?Ó²­þ°Mδ»¡©§?‹ž@ò;ärUÆDuÚ]èíógÕ6É Fšt— ›´võ`éo6ò^ǵ?ZР~!é,¥P¸FO¶ÊCÏ‘ì}æõ„_o; Óº“Ó:Ä 4#ŒP—è9µ‹E£`Žr|ÙO¿€më¾B|\| À=,˺@!„B’BB!„g6›ïðr¡í·A®Ä eÐ02ê1 ±±r©’3a†B5\L·Ç=Ç/ù¹TŠòb`ë”I$Ð)”(QA&¡s"„èÀÜý¸úWñAÅ0PÉdP32¨è=SpþpõCUXøÃá¬Þ^¹T ëY!‡l#DØA­”áÕ_ݙӳDû¡'€T(ãÁ’_RÑW.øl"葄仕ÏÖ¡ñX7¯¶lú&Þ<—S›——¬À'ëù"]fÀšeóó~VdBRí‰×vàƒ}-‚.ó*C5–T/ÃBkjæ(‰Æ"8pjÞh~‰ó ùû'ýfŒœ›öã\©³f¬²ÄÅtyÚqã†«á ¹9µûëübj錤×ã†or\ ²Wâñ™ü˜HÅRL*™‰Xš’~{Òׇߎ`´?eçÕágñ^Û&|~r òÓE+Æ9”‘ª~pö±¬g÷`WÇVØúZS²ŽB<ne3^áábŠ”2˜ š‹>O¾ún=^}¯ž×²¥³ýeúªÆˆ%ªFq^§¡€Ï^ܶñh¯Ÿñó%¼*RÈŠÔÐŽ,Ixƒ?œGmɽ÷‡Ðºe7N·t ýŠeÙG鉙B!Dx „B!0›Íì -¤ý–‰%˜1b$*Š´Ô €3á…øHF…”X, 3Äÿúw’¸#ÎSh`/?ðéŠaˆ“8¾ƒ!ƒR %ÃÐO¡p4 O(8_(”W•+8È¥R 8¤ÍÕ‹æ;vµ·¢Ã}:ë·W«PÂjŽSLEš¬>®o6L:”ôÂOæüŒ×ý@è·&š¡??¥+ð ezù—‚ †¿”.O;º<6¬Þ³ÍÎNmŸñ‡´Ìàÿuט§§d}²>íÞÅyVý2ßÙ*Èy临Àº(%¡šråš Á–7t`}'‰¥nŠ][±«c+çk"—¥¢b‹?ìÅçì¼×º)e!‡³ýü³1ó›còö‚aépdývê‹U0é51çiL(xtí5Ûì´î‹DÆ`øUW@̱ª­Ëv~'ÿ üýN7¶ÿâù„_?rêLZô]ÞëSt(./¹ì~&[½¢¯ý$޼ñODƒ¡B{TÅ¡ÇïËúm6©5¨1—¢ÊPB™õÛÛ`ïÆÛ-‡“^ÎÊ»j1gŠ5çû\À/ü2)ðŸ(ð@ˆ°’™é}âü9x`óŸ¹=c¸úðŸ•×!àöðÞæ»çMÆÝ7N¦“GÖÚÕƒÖN4í†Ýéá=ˆñBÔJ*ËŒ¨,7 fl)*Ë0éùní½žø¶v9Ï _\n{G—}¼ aT– >ó÷à¿mÛׂ5¯íd¿Ë4¬©]›ò ðUu‡H,‚Ÿ|ð?9Ï`ÿ×ùÿL{Ÿ+–é0ÎX“µ×Ä3õ«ñlýjNm–M^…å“W%½îmÇß¶.àÝþ¹Ù¯ ^9C¨*é:øÃ^¼Û¶ »:¶æT5¡,ÿæ^ Ȳ+cìêÜ 8½ßyÜ0ÅŠE7Næ}¿Èfno?ÚOöæÄ¶JÄb ¦>ï<Ø{=¸ÿñ ðø xç[É ²"5ŒU£¸½g…°>–Ôz¹Ty`”rÌ{rERëK$P—è¡qñI?z[»ÐÏó³F×Î8±ë@!>.7 v8HŸ!„B„EB!„r³Ùü €e…¶ß¥j î¨2õ¹_ÈûÃÙ5ó #–ЀäcsõâЩ“„×|E7 ÒKŒ$•ˆÅÐÈä(’É¡bÉätp³œ?B(E8E8zæOšÃƒ•[«¶($ $bUnH‡ß‹ýÝhrر¿»3'¶¹ÊP«±VCÉPuŸ\ TØá{3'`é‚éyÑÿR1ˆ]®¾–«$ÔWFR(¼ îþåë¼t}ÿÉ_bö²{9µélø]=/©í~ô‡s1­ú :ä¢ýzoc;ŽvcoãqÞý›/“^ƒiÕ¨[zÉ~zöv6톽ד’íQ+eiÒáXg"Ñäo¢B |OÔ`uœ+XŠ+ñøÌ?¦½Vh+aV—gõur{Ý,ìëÞÍ©ÍGw¤Âê=+ðò¡çxµ½ÊPƒUÓŸüx$[å¡ËÓžÒ ƒÃÏbcË:Ô³{Ò>8?›Q±eðXîîÜ–ñý¹aŠKL˹jA—2XA)—(å J‡Ÿs6ïhÄ‹›÷òZžaì(Lèδï‡fÄðK.¤çHB^þï)=G:°÷™×~ýµ?ZsMUÒû*‘1ÐŒ•áüJèöÃÇ …¹}Ïàò¢mËnôÙØB|k]`9˲.B!„ÁQàB!„œÇl6ßàÚBÚo…TŠï]q%®1•R' ‚q|8ÚãÀ obQ(•H`Ñú™D™D L%Ã@Å0TÁ!¨êD8¿ÄbèÄ©$‹!¿È ô³«·ˆE✬žOç³ÉaG³ƒE}wgNTqK¥°žrÈEÛZ¿Äg':’^Î S¬xä®Ú¼é¡xlRE×y> täðdr€2»¤P$3 K©Õ`å‡obdÍ78µÛ»n^¹÷aÞÛ¬VʰæÁù¨,7Ò $¾ ìm<޽íY³]j¥ Óª¯Àœ)VÔŒw³·ñ8¶íkɪíLÄU†j¬©}9í• êÙ=ˆÄ"hêiÀc{¹Í =Ù<_»:íÇj’i äEVŸÏ¾  ßz½ž;ñÏ óñÇïldÝ·×ÍB³³‘Wû‡¯]ÉfaƒßR±ã 5P1EœÚ9ü,º<6£ý)9OÙ48?ÓŒJ~wÃ_òîXª•2Ü=o2n­­Î‹óÔiw¡·ÏŸ“Û>\§†É äÌÌ +Ÿ­ã]jÒ¢›0rjúÏ©a¬rMâ_†øn¸lÉU¾Úùÿ^B_ש„^k®‹kï_(Øþ~=ø‹FÁ6á´ŒÓ_Úкå#Dƒ! ‡X–}†>IB!„¤!„BÈ™Íæ‰^PShû>ÞP‚;ÆŽ‡’ Dc1v°„Bè øÎ©ô •ˆÁˆ%¨ÐéQ¢ÖPåBrŒÍÕ‹æ;ê»;Ñì°çÄ6kJX ÃQc*ƒ©H“³Ç¾?A]ËaqžJzY£Ë xñg·å×½'=E"‰X'ù'"áÔ¯‡*„B“Ì€®òêqXùá›PéŠ9µ{yÉ |²~#ïm6é5xág ójVdÂ]kW6ï<”‘J\«€Ç̹ã|CÅ|¬©]‹b¹.­ëuøY´ºZ€WàauZ§u›ÕL& ¿:'Îë¶ãoáG[pjó×ùÿÄÔÒI¯»©§7mäwœ’ø~©sW©³&zpøY8üvô…R3!¶?ìÅ»m›ð^Û¦‚®èp¶ie³ð“«ÿ‹÷±ÜÔ²>«÷¯zL)½onÎ?×´võÀÈÝã2F‚‘&Š”rØ{=¸ÿñ ¼ž/”z-fþ×0Êô†ß$2ïºbé}¶ábIÌ*Ðùi#¬ÿGB¯e”rÌ{rEJö[3b8Ä1zÛºng{Ø}_â[ªÀL–eÒÝ…B!$ÅÏè>ú(B!„rž•+W²kÖ¬yÀU®,¤}?ðဃÅÅ4º$I,A+W /D‘L½R5ôG§P¢B§G•¡j™Œª8’åüá>éjÇ{Ǿī Ÿãí#_ ÑÞõÕ,Úa˜RnÁ¼±ã0ÃR‰J½1§ÃU®þþzè_èpŸNzY£Ë X³l>dL~½ÿŠD@DÀ  CƒÕóYª+<ˆ02:Τ°ÔT•bÛ¾„#Ü/°>»nö&Ý<—S»I7ÏÅ-[ÑgwðÚf_ „æã§0gª•N`j8Ú5¯íÀÚ·÷¡í„“WßM·`8‚P8šsÇzu~wÃ_ —¦¿bA›«áX| é®Î­hv6pj?cÔ\ThǤu›ÊáÐ)ô9qn+‡]‰¦žƒh;*ID—§]Ép•°¯{÷Ϲ‘øçÙqƉ‚p,g¿±äR¤âs?sõ]`}]8î> GÀž²ªïµm“Ÿ¯Bã©z„ca8wð4*uWõ®Ç2ÛÙ{=xçã&\sÕHè‹s·\áé>?B‘hÎn46€Ó}ˆDñp­L*A}s'çåDAH)ŒU–´nÿ@4†HJ}âAäh$‚°/À{*ƒ¶ –ÀyE¢Ð–›Pd6¾ßýn§û»°½øòµ­8ÝÒ´ ÀT–eÛéÎB!„’zTáB!„\–Ùl^àéBÜ÷¹£*1wT%u’´h,gÀWür‰•š*:’åšvÔww Éad€}:È¥RX´zX%°J È£ŠE-ÎS¨k9Œ £ùÃù:“u(¯ô,¹2¢ ù©ß¤òb‰Qà Í;ñâæ½¼ÛßóÒ“˜¶˜["¿«]=NÛ Þë½aŠÜUK'°@4íÆ«ïÖó®HB¸y¢ö¥´WHÔt¡é¬€ÃÆ–uœghÿÅ´§0ΘÞB°ã 5i¯„‘Œ.O;nÜp5—0oô­X4þÇ—|Í} ë?Ï9$•-VÞU‹9Sr3ЙëΦ-R`¤I‡Ÿþî^ÏŒRŽÙ¿~ íU@?º ]bURÃ~8š'µ¾ëßA秇zíèÚk0þ¶Ù;¯ŽƒGa{¢Áüè§=˲ìrº“B!„¤!„BHBÌfóL[h mß+µÃ°äªIPJiö}BÉw6W/š{ì¨ïîD³Ãž3Û­U(aÑ 9ä£Ý¶Vì¶ 3X%ßËÁ`òË¡ÁêùO¨pÌÅÈå€XBÇ™¦•ÏÖ%5üûßÃÈšopjÓÙðÖÌú>nïõRè!ÿµvõà…M{)èFË&¯ÂòÉ«2wÎ]_ÂáÿêóÍ ~ƒÝÛ8-ã¹Ù¯sš ^BÒimã³xlïà ¿¾LcÁÇw ó9çÓî]¸£îÛ¼Ú&2ð=øÃ^¬;ü{Îý»YŠ+ñðµ«/x}ó Ge£»çMÆÝ7NÎÉûu¾@)g ?þí&^í­7} Ö›þ-íÛ-–HP2¾bIblí‡!â_Q†m8‚ÏþØ1R굸áפý˜DûChݲ»P«:¸Üò캃B!„¤!„BHÂÌf³ÀN5…¶ï ©wŒ y:ˆ”B Õ`À¡ÉaG³ƒ…?Ιm7©5CS‘&oÏ‘«?€ _„Ýçdy…vìb±$Ÿ¨ºCÞ‚ÔTy‹¹‚Ž1)\ö^î||<ª)µüwë'PéŠ9µÛ»n^¹÷ᤶ}t™aè^YYn8ç¾YYn€ú"ÿF²›7Ä«ïÖãï;ÑÁH£ÖEXS»6cëÆ"øœÝsÎÏVïYÁy¶ö¿ÎÿgZ·[¯0¢Jÿœ<ç\+-YåaõžxùÐs¼Úf"Ô"¤zv^8ðøÃ¾Œ¬_!Q¤.ÅýU˜Xr-TÒ"Th+/Y9Ãö¢Ý¼49ÂágáðÛÓ^MAŨ±tÒO1Ù<=þ]‰û^8ðÛ¼ª‘‹Î| <€Œ‘`ûgGðöG_ðj?û± 2¤N.•AeDB¯uwÙá;Õ›Ôúêøï¬=&}í'Ѷå#ÝÞB|¬k@<ìpžp !„BÒ„B!„3³Ùü €e…¸ï×—ŽÂÜQc¨Ú!„ä(‡ß‹fÇ`‡Ü 8@•¡ºa¨2”@§PæýùjqžB]Ëa#ÂL?_Ha võóÈ.• Uw(á0IÁÛ¡\=RȶíkÁš×vðn_^=+?|“sèáo+Å?ŸKßk“^“^ƒ"• •eF˜ ñÿ§@Döh8Ú5¯í€½×C#®2TãÝÛþ•áÏ@,Z]-çü,ÚJ˜Õå9yÞ¹VZ˜Rz=Þ˜ÿ¡ ëî ºð­×+á ¹9·½~ä,ôÓœ;Þþ°/ø-ê¿ìIýõ]ƒqÆŒ3LÄ8£ðóÙÜÇÐäl@SOCÚömÑøøLñë?Ÿ—ïÉß›9KLÏ™íÍÇÀ„ÂQüß—?€¿Ÿû¾œ:“}7#Ûmk\£ºüçë@?ÍÇ“Z×g/nÛx4¡×Ž_8£g]“–cеóNì:P¨uë,gYÖEO¸„B!™AB!„‹Ùl¾À+´…¶ï¥j î¨2µ†:!„d¹\®àr©VC ¬ÆX´z( $p׉ ®å0Ž8O ¶ÌB ; ŠF€ñ43á¢"ÈÙd2@BaBþé}ìmlçÝ~âü9x`óŸ9·[3ëû8²ûÓŒï¿ZAT–PYnDeYüo’>/lÚCU2ñ,/Q`ûí‡Q®©Èèvrì‡/|î,Ì?ùà¢'`Ox–âJ<>óiÝîêáW_rfülw{Ý,ìëÞðë…¬ò°±eÙq/¯¶¹Vå!UTŒ“ÍÓq͈oaœ¡&íý³žÝƒÏO~ŒzvOƪXI#Óbœ±Å2Æ'>íÞ‰h,‚zvoÊÖ»ò®ZÌ™b͉c”¯8päþòþ~^mç=ùeú¿°È˜ÆIèµöÃÇ ñÿ²íÃÏqxãö„^k®‹kï_˜Úï \^´mÙ>[¨u±,û =ÝB!„d!„Bof³y"⡇šBÜÿ[F[q}©…:!„d‘f‡6woÎÀ¤ÖÀj,ÕPSQá…ë„®êÕcJñè}s v†éh¯ô ‘Æ«“Â20¯àëb ;r.o ˆ¥oLjfýë-ÄÖ>Å©ßՇǮž §íDV—ê1¥¨[Šê±ñ¿IjúÞÊgëÐvÂI#~rõaÅ5¿Êè6£ý8`ßwÞϹTâ3Ù¯šþTÚ¶[*–b²yzNŸ®UXaM­pr3Ë IDAT•ynÜðM4;9·Ë¥*ëÿïµmNùz&›§cƨ¹YÕ'wunŮޭœ+µdÚ ó1犛1µtÆeÃ`k~ o4¯Å‘Ó_ÀÿµÐV²žxp~N<{äsàþû•íèíósng½é[°ÞôoÙf͈áÐŒ¸|p×Ýe‡ïT/ïõønlÿEâ•Væ?ÿŸ)ÛçÓ_Úкå#DƒùÛ/Áà–eÒ“-!„BHæQàB!„$Ål6ë<`q!î¥v–\5 J)è"„tó‡Chr؇ª84;ì9»/UCU†A§PäùtõðvËaØÜ§]î S¬xä®Ú‚¿^€p8^ñábÄb€a±„Þ_ ¹ŸDÂß¼‘CaB.¨µ«K³1©eð =t6|5³¾€Û“õǨzL)¦UW fl)U€¨Ï­|®¾@ˆFË´hX’ù I—§]Ûy?ÏöÀƒ^aD•þ9߸VyøèÎc‚Uá¸8[¶WypøY<ùÙ*ØúZS¶£Ò„+`ÆÈ¹Y]iÄágQwì lo;k·±LcÁòÉ«0§âfËu¼ûóÚÆgñA{ Û¤VÊðâÏnƒIŸÝ“Lä{ࡵ«/næ^̓QÊ1û×d¤ÊƒX"AÉøJˆ%—þâ¤ßåAo[WRëúàÿ<@¯;¡×^û£0×T º¯ÑþÚß߇ž†£…ú8÷€{X–uÑ“-!„BHv À!„B„Ùl¾ñàƒ¶Ðö]!•⎱ã1ÁPBBRÈæê…Í}z(äÐ!ðÀøtÒ*”°†Ã¢ÓÃJ÷ì¶µbß › Uàîy“q÷“éâ9ËÀ‹Åÿ ‹ãD":>$.dÎî'—#‘Æ3Ô¹¸Í;y ê:ŸÐÃÁ·¶âù?Ì©ceÒk0­ºs¦X)üÀö}-xaÓž´…¦”^rMÊ5˜Z:0ÎPsÑ­ƒƒðû‚.49ÐÔs{Nüþ°/åÛZ¦± \c¹ÈvÙpâá>Þ¼y'®ñ­Œ÷…ö}FûÏûy¶Ê5Áþg×ÐÁ²É«°|ò*ÁÖÏ5p1(›«<Ô³{ðÂߤìýâ*C fŒš‹#çf}ÿ’Š¥°Wb¸ÊŒ/XñᲪâÃU†,™ð ^¹XÐkê™ú_ñê×_7ºÌ€v[VŸãö“½p{ûóú™å…M{xU¢¿p6FϺ&#Û¬.ÑC[nºÌgê(؆#I­çð†íhÛñybý¹öŒ¿m¶`ûèg{qäíº½(P¿bYöQúTA!„’](ð@!„Bc6›'ØÀRˆû}é(Ì5†ª=Bˆ~/l®ÓyQ½aE; Vc ª %[Åáël®^Ôùîþ€ ËU+eXº`:æL±ÒA&$ ±X<übg¾J‰ã$âxU :’˜'^Ûöµ$µ >¡‡·W?·W?“Çl0üpkmuÖÏÄœ ¶íkÁš×v¤tSJ¯ÇÔÒ™˜Z:c(àŒ•;–`SËzÁ·ó*CõÐvŽ3Öp@ÿi÷.ØÜ­øÙ®û8¯wÞè[ñüœ73Þüa/û/øo\é©ÀL®á:(ÓXðñÂU-èò´ãß^ëm6VyØØ².%ïñ÷‹,´.Æ8cMNô+©XŠq†šóªOtyڱ꣟`GÇ{Û¶¯ËI¦)K„‰mã³x¦~5yURá‹2ì ‚·]y~3óO‚mß·^¯äuÉ×®üðM¼¼dENVz8›/«ïÕcóÎFL«¾‹nœ “¾p+-îm<.hØA#ÓbIõ2,™ð`ÊfÚ2ì`ÕÇKóÞB™F¸¯ŸšzxÍŒ¾d²¬èÑXä‚a‡\P,ÓæÝ5ºdƒxlïà ½vÛñ·€ZáÖͷʃ?ìîέ˜7zAÆŽ›Í} «÷®€?ìt¹ ¬‹pãèspÅ%ìÄ+¥¬šþÔP%OÈ-ø6Í}+Z_òXúÂ^øÃÞ”ïb¹üÎæ¤î)Ǻ±m_ UÊ Êr#JÅèîéãÔŽm< ¿Ó •!3÷ËUy&Qma±jT‡ž#œÑþZ·ìÆé–ŽBí~n÷°,»…®DB!„ìG#¯!„BHʰ,{Ðl6Oð €› mÿ[ݧñXýnÜ1v<&Ð XBHž²¹z‡ ¿=~/šö¼Ý_¹T «¡í0ªâpé:(å VÿhjÆ–Ò'„’ºÏw]=h8ÚÆcÝCA‡lóÉú0VŒÄ¿¯zè’¯ûÁÚ§0²æxóá_åüyñBø`_ >Ø×‚ïÍœ€»oœ\pZ»zð„€a‡ÖEX5í©”`m㳂„42-ÖԮŜ+„ÿÊéÓî¼¶'ÛÂGoOÎöé\„žˆ…ÖÅ ¾³·×ÍB³³Qí1*MX:é§g¬Ièõ? ‹vLÊŽÏ`U¾÷–Wß­ÏÊÀC!=ÓüÛ¤Jü태۱ G0zVf&¿èwy+/X"¹à¿KåLÒë0×T¡å'ôÚž#ÜB }í'Ѷå#Ý^¨·°,ÛNŸv !„Br!„BHJ±,ëp‹Ùl^àéBÛÿþH/7Ä5%¥¸eô•Tí’“ Mþp6÷i8|^ôø}±ÿU†Tè†Á¢ÕÃT¤ùÿìÝ{tSç/ü¯uµd]Œe°"d,ÀÅÛ™Ò„˜„&…¤s™ÍäLréÌ:ißrëLÏ»Þ!Ž;gÓ)!$gM“·í¤!ä¼o§BÈ{’@:ÅÐb q‹mŠ© _²}‘-Y²î’ß?„7mÝ/ßÏZ¬£g_žýì-mùùîD Òt€e‹çâÿ|ìž‚›ØHD”ˆ©)`*„#Wþ\*Š$@QûhZOÿ(ž°¢­«7+×ò^ËËhzd ÕU7|Ý=[žÀmë×bËöœ¯ö0íÖS8xŠïªÇ·¿±¬ ƨÛëÇß>„Io áe-6Ôc늗±Ü´*¥Û|ðÓwcžø}³íýé½{Q¥­NÉv<,ºM¶„`"àÈÙq­”çݹªS–âëÕëðQïþØÎ“Þäâ­ò0êB»pËŒ+ÒÚ_‡ûàõ“ÿ’´å©å%ØX»9£Õ*‘HØáò1øþ¦?&¥ºN¥ÖŒ–¯¾**œ4æ³§4ð[›¶£{´#®PÇИ+k«{o?Î9ž³ÇnÒÀ®Úqð„OnhBSýü¼«¯íiÃ…{ÂËÙ²l+ž^¶5åÛÛïêÅó‡Ox9_¯^‡m«ßHiŠn{§è6kªSxðݘ ºáû0áw"<Âd0?ŸÈœÊãšIkæ¯=ððé»Àêä®?‘*é <$;ìËU€ä„.—h%pÙÐë쉹ºøÃ>x‚î”VpÑ)KñËu¿ÅWÿ—®€Stûl­ò RÊàNB°1,«›‡NXEµ™èŽy’JÞŸíŽë’Űp„®s1½Ö~ε¡þúÛ+Œ¡gßx†Æ õÖØ àQAöñ["""¢ÜÃÀ¥ ­F£±ÑÐêBÛÿq¿?9ÕŽµó,X;ÏÂAD)1`˜Ö="Ìü¿Í9O0úKÒB 3\KE‰æÒéCŠYG_(«}Gl=pú¼)__­yþë£wÃT®gçÝÄÔ á 0c›È ¡ •ryáôWOÿ(^ÛÓ–³A‡ig‹)ð0M]ªCÓæMhÚ¼éªó8&Ð×yzæïöÞ~Ømý€¾ŽÓð8&`·õÁnȪ>s¡ùgÐT_'7¬@EYþUéjëúTô¤À/Ò*ôøé½{S^ÕaÚßø`\“P/·¡ö‘™ »©ÒïêÅ€Ë&º]2û1lp`ÜgÇDÀP$T×á’N†Î´µ›ÑrôÙ˜ÎWÀ‰ãƒ‡“:¦ª´Õ¸Ã´'ˆjwÆÞ‰–ÀÀ[úW|paoÒ–·¡öl¬ÝœÓçC2ÃÓ¶­~&Í\üÏ?ü·¸—ñÚÉá~ýÿÕfÄ#¤¼ÊƒNYŠŸÞ»ï¿;®ÏçѰДUã@£VLàá+‹çÆõÙ¦ïxjÿê/3²Íá@>‡ Å¥×þ¬©Ð” àN¬Bnù"ṡѳ1wùµÂñÓèo=‰°¿0ÆÓ5tx@„^~S@DDD”›ø|""""J+Aî2Í^(Ä>8p±§ìÃxxÑRT–h9(ˆÀ•„Ë«-Ló°9ǯjgsŒÁ ²E`À!9†Ü.œ°Áj†?”ú XÅJ9þæë·á?¯ý v>Q " à†â1…hè!äJ@"ÉïþÚõ~;v}ОûâqL$mYêRjWÝùùn0÷ÖÞÛQ[ú:ºa¿ôß¾ÎÓð:]ë‹¶®^tžÄ“VdåS›ãåöúñã·%´ŒÅ†züôÞ½¨ÒV§e›w´·àŒ½+¡e¤#ìýq„î0­L¸2A8˜oÂä@ÞVo¸¥´8¯÷o¹iUÌU’x€§—½×Dð.ìÁ#K¿—Ò¾yíäp¤ï`R–U®ªÀó··¤|r}*¥*ì0íÙ¯¼ˆßôþ8G5õრ{p_͆˜ÛŒùìi9&ËM«°¡ö‘¸ªXÁ½ó,Xi2s@å©`8Œ`$Œ>§£ÞI\…7ÄÀDô‰ŽýNøB +¤šY?n˜… އøB!t  SÄÐdú&/6.ªÄÓ¯dU"¢…C@ IîŒL WÒ<} ýñÛ‡~R~61TWel½†êª+ˆ!ú:O£¯³ÖÖc8{äxZ·kÒÀ¶·¡­ëS<ÿ­ÕШ”9Œß9t “ <íy±¡¿\÷Û„'èǪ{´¯´·$vž®þ·´=©ýøàaÑmê q¯Ïö¡ßÕ‹qŸ½`*9\Z^’×û·fþz‡V[“ºþå¦UXl¨>:Üw ¥‡d†ðüí-Pçpµ™DKimÊÂÓÞßô|ã×_Ž;ô°Ûº«æ®¹¯ýaûBAøC!t8gÁgnœ>oZ'… ”É`Ö—Á¨ÑÎ(qCƒ°Žã¬}8­ë-Ó©ñôë°|)ÃDD±JfØaÚ¢ËT ÿBùvpUà Ó¦ƒë×âþ­Ï¬‡áìáã8¹ïú»ºÓ²m]½xòìFóß­…¥ªèqŽã‡íGððÂ¥¸Õ0‡ƒ‚(Gx‚8|^\·ãô°›c 6ç8;&Í*J´¨Ðha.-ƒñÒÿSrXíðŽÃj†?”Þ'Í+å¸ïÎ/áïÿúNH% "¢E"@0…ï ¥È—Kók{Žæ]ØÁ`®DÓæMY¿µ«îDíª;qÿÖgàqL ãÝÑ?û¦t½Cc.<ù£Ýø/6áÁÕõ9yŒ÷ꊻm&»­;š¸ýOM/¥5ìÄx¨ÒVÇüÚp$„~W/>›ÈÈš­?™´×Ù“–mSJ‹óú}Z§,Uaáøàá¸Â47²±v3ZŽ>+úÉ÷íŸMzà!Yaµ¼,ý^JéfÖYÒ^bÏ¿ÇWÿ—Eô˜€.ìÁ7j6ļÍã>»¨ëe"¿õ)¼ÑõŠèýjëú4« Q)àöÆúUJ9–ÔEWy=kËXànOJú¹1†þøgô¿~§ìAžæ7DDDDù…""""Ê ‚ ì0­ö(¸Ç8ûB!üâL–æàá…KYí(‹9|^üiø3üa°Vûœ>/;%M¾X½¡B£C1¯—I}/²Ú‡asŒe$ä0­©~>þþ¯—ÃT®çA!")èVcH¥€PEE¹ÝWçñN멼:þ*½ßÝûóœÛnu©M›7¡ió¦™ðCÛÎ_ãì‘ã)[çë{ÛÐ3`Ç÷¿µ:§úÊíõã`œ!J­9íaØÇÓµ§m¨}×oIhýÇ‹nÓﲉnSWÞÓëF<l=EB k\ò¼Â¬™ÿ@F@4ôð‹S¯ŠjÓ.ňGˆ+0s-»­;“vØÚ´fý‚œUZsÒúW ²?½w/Þ·è¶b«ø“}?tÁ㋱@_ÆAA”EF=“øàÜëïeÈ! ”2Œ%ÑÊ -*J´(-V±c’Ìáóâ¬}½Žqœµgt[jÍsð½_Å—ªY툈(á™Jýz¦¦€PËs»¿v½ßžWÇß`®Äw÷þs–äô~\~°÷öã7¯þm; ¯Ó•ôu}tŠžþQlÛ²•2'ú§­«“qNxüéÚ½i;ì¶îÄ@á¸Ã´ÛV¿qÝïíÄDÀデ1áw ÛÞ U“ˆW¥öæÏíGB°ŽÆDÀÁ7¬ëÝæyu‡iËM«ðJŒ¯ííHÉ6<^ÿ”èIà@4ôp_͆„׸ïöXßJx9fÏÝÞ’‘@²ÍVW¤­òÁõÆåc·Æ7._< ªτߑ¶cïXï<7ˆ†…¦¬%*EA½,©?>‚^?œýCÐWUdd›#á0‚^äªÔ¼—•/šw×ÉdÐëõöƒa#vèå§*"""¢üÄÀeA0Ox¹ûÀ á'§Ú±Ò4kç-`µ¢ ñ¸ñ¿:ÿ€®áÁŒ=ñ>ß1Ü^ÓlŽq Mº2¾=µæ9xô¯nÇ—Wñà% LߺÂÁÜÞs% 湘ÛX‡ÚUw¢qýÚ¼Ó†ê*<´½÷o}ïÀ{-Ûa· $uìxþ•ýøþ·VÃRUžõ}²÷PW\íþ©é¥˜+$S¼Õ´ =~ºvïÌß'üÑ`ÃñÁÃè¶wd$Ôp#U7 —_²·­_‹Æõk9ð2èž-O ió&¼×²ÿñêIYf¶‡:Ï Šn³ØPŸ’‰Ò7Óïê+° )’àwý¿É¹ñè_9Á1 Á:v:íO1ì[€3öΘ_/&/™¤p~m]¥­ñZsJ¶A§,Åšùë±Çú–¨vŸ|vTtàaÄ# ¥íÙ„ÇQ¾…n)©Ä¬âì{¿yzÙÖ¸ªæÀÃ„ß ¤ñ+‚ºòÑA¼ž~{V=°A£RB*‘ œ«©ã8ÔTpaÀ.îuö"ÊÍËØ6‡üÑÀC8D8ÜÊÁþ¡1 †B¯ê`C´ªC?9ˆˆˆˆ(g\ú³Ñh4î°¥PûÕˆ’Ë àõö6üa°¯ ö[)“Áxé2]¡¡X&CÅ¥ŸM(³†Ü.“.Øcr»04éʉí6ß2 ›înDS}uV=‘ˆ(_e$ðP@ýÛ¸n ¾»÷çhYjnÃlýã‡x¯åe¼×òrÂËËÖÐÃИøÏ™¨î{ß-Øñ˜®°ƒ'èF¯³çÒ:;®úÙ´€Qï/Y¦®¼_¯^‡z÷ßðu[›¶§t;–›V¡RkÆ€Ë&ªÝá¾x$ÆÉúÛ>Þ ÛDOBÛ™oa™D†EeK²z7ÖnÆ/N‰ ¯uÆ^5&Ýp–›îxˆç}7ÕôšbŒMx æZY9[/:ð0Ñ?”ÑÀCÀíR«†Çž¼ja_gùLØ„B;¼ àÑKUቈˆˆ¨1ð@DDDD9G„§Fc+¢Õô…؃“.üë©Opï< VšÌDqòøáჸ˜åOÉ¿‘˃ Ó¦ ®1 2d3‡Ï‹¡Ë ¶—êb9î¼u>6~­>랈œ ¦¦€pˆD¢OMLEŠ$€DÈä@aÿ^›ˆ(s²©ªƒÇ1¾ÎÓèëè†×9¾ŽÓð8&>?.Õanã4®_ƒ¹ K ê8Ý¿õ4®_ƒŸ<øØm -k:ô°ëſ͊ðfgÕêSúdøëéwõŠž<ë”Òb@ãÏI;ŒxŒx†Ðmï€ÍكɠgìÙÏîÑNÔ•§®èj‰\SPãfÛê7ð7û¿†3ökWš|ìÖ§°fþú”oÇšêõ)›ØþÖŸþ5áñšoa¨Òšg®Ù*žÀèw#³ÕƘ^?áw@§,MËþèâ…0dϾÀC‰JQP‡Y:µè6A¯/£Û ‡£ŸÙíÉ™“?þgzöýa oÇœžáMÞ™6ˆˆˆˆ(' ‚°Ïh4VØ`U!ö/¾ Öhµ‡…KQV¬ÊÛ}uø¼pú¼ð_ú…Z.‡´HPÉåÐ*¢%½‰ÄÈ–°ƒY?  ”ÉaÔ|L¨ÐhQ,½ò¶½B£C±Œ·òùr]štEƒ Ž1“.øC¡œÜ—¥–[pï_b5‡„C@0põÓÒ§p)üB!@&cðˆ(þãÕ7 .Õ£é‘Mi ?L‡¼Ž ôuvÏ„Î9SûŽýñ^Ë˨ª¯ÃC/¿€ÚUw̱šÛ°ÿô‡xóñgѱÿ`BËšôðü+û±m˺ŒÆ™ôúE·Ynº+#ÛzðÓ«îP¥5£ßÕ‹O╺G;g Ýöx‚“ÓRIaÝëꔥxÓñF×+8Øû.N V¡ÇrÓ*<^¿%mZâ™Øn›è¹éÄöÃ}ðÁ…½ m[>†ÊŠËa,©Êúí¬+oˆ«ú‡m¢'æÀƒ'äN[à¡®¼1/Æ^SŒ¾*Úc*׉n3zö"jÿ*sÛôøá±;ZNØ@Ͼ#·^,ô[±ND«:t𮔈ˆˆˆ8K‚ˆˆˆˆrÖ¥ÒµwÆf/j?ô8DZ­ãXÞV{èŸp`xòÊ'$ºWOö(-VÁ¬ŸÅàÅ$Ýa³~ôŪ™qZ,“£B£å(ùn˜6g–®®ÇІù¨(ãXND0 3Ä""a@QÌÐQ¼Œ†øÞ·Þkyïµ¼ŒE+—£ö®;±hÕr”›çÆ€°>3°¶Fk¨!¦{©®n¼t÷C¸ó‘xìísŒÕ¥:|wïÏgŽY". ØÑüÓضe]fïûûí¢ÛTi3óýÀñÁÃw]Ñ*t質ňG@»pÝ£hŽò"]€¯ß‚Çë·dlýñNloŽâ¾š ×ü7›ó<^?ù/ m×2㊼ ;È$2XJksf{ã©þÑëD«=˜ ±òµÚC º*ìp=ŸÒ¢"˜KËxRÐM½ÞÞ–²°ƒR&ƒY_†êÒY0ëËl(0ùn˜6g–+æc͵°T•ó`'A8{ØaZd*’P°˜]F"‰VƒI÷:sQE™%*&½ñM :{äø5C ‹V.¿n›¾ÎÓð:]ÛçcoíF_G7žÿí¯ .ÕÌyqÿÖg`0WáߟmN¨ÿ»ÎâÇoÂ÷¿µ:§ö?SO³.ÄÀÇŽã®y÷Åüz›ó<÷À'ŸŨ7wÕÝ.üuå |ÓÍCñLlÿä³k`4°€¾û!Ÿª=BalJª»:Û“¶<³~ê•h¨0±s €Í1›s‚Û…¡Iœ>o^îgýšê«±¢a>*ÊX¡$UÂ!`j*þö¡ `àˆ.)’g`9ª©~~Ò¹àì‘ãøÅãÏâ±7¶Ô~ÏmX‚çû«„CÛÞ>K¥!gB ™x¿Ë–ñýÖ*ôWì{¡QÔD×~W/öXßJÚö´ Gqøâ´ Gsþ\ „S7 SZÄ_Ygúz¡UèE–ºíXf\1ó÷ÝÖ8cïŒ{;Ôò‘J$Ðk §ÊC<‚^äªÜW¬êp…gAØÁn """¢ëÞ_³ ˆˆˆˆ(ß‚ð¦Ñhl°@C¡öC>T{(-Vá3÷D̯WÉä<è†þïö¶¤,ǬŸ…•f +Šä±!· ¤ 6dž.òU‰J¦úùhX :hTJ€4‡3Ûžˆò‹DRëL–5wÔb×ûísÜX9öÖnÔ®ºM›7Ô~'+ôðã·áõl]ÛñÁÃi[W¥ÖŒ:Cêʱܴ :EiÒBb_œàí ºñ‰p»ÿ¼3g«9\ˈGà Ïckæ¯=ö?ùì÷3‡îÑ΄ÂBjy ¶6mÇlµ1ïúÖ¬³@*ÉÍiuå 81xDôµ"Öã8tç\å‹l0K«.˜ÀC™N-ºÂƒ³o勿eý¾±ªÃ ¢U:xvÑ0ð@DDDDyI„^F£±À …Ú¹^íA%—C£PÂðßôµR‰Õy<ù<‰À \~Âý*“H ’É!Íå™j"™‘Ä&›è‹UXc©E­a/´yv­rOÀæ‡Í1aÒ(”×û<]Å¡a¡)gžRœo¦"‰/#$¬ò@DŠŠ cÉôä†&4ÿì@AŽ—7ŸxzˆÃ…;v½ßŽoc/<ðõêuXnZ…5ó×£J[²õÜaZ)z‚/ :¼a>¸°žàdÞõ¿Bªà ÌckªÅºG;gÆþKŸüSBëdé÷`Ö/È»~Õ)Js:ÄQ¥­Æ ˆ < żÏ~'ÀÂ’¢é5ÅJ$G"y¿¯³tê¼Û'Vu¸ÂNO_ªÞNDDDDtC <Q^¡ùRµ‡7˜ µr¹Úƒe–ý8ü¾+~‰£J¡Ê ”J¡Q*QªTåÝäý@8»Ç»w=Ê[*‘@«PB_\œ—}x¹=g:j{å<¬4/@±,ÿn¥­öa ¹?ŸôUk˜ƒ MþþÆÙ ÁæƒÍ1›c<¯«7L«©4\ªà­ä@™™bQò°ÂƒxMõóñõ;jñÑ kAŽ™¶së0·aInŽ=|læÿËÍsa¨®ºáë“zØÛÚ…5ËkQQÆš_ô[ÛÿNú2+µf<~ëSØX»:eiÖîûk'„váh^f¾?(⯕óÙrÓ*ÑmF½C°9ÏcçŸ~’ÐØßPûVÍ]›w}*“È`™U›ÓûO¸Ìr§äµ‰˜ðçß\ê2 #Žü}ÏÉW¬ê0à àQAöqTQÌ÷Ùì""""Êw‚ ´ÆFDCë µrµÚƒT"¹´¬`Ò*Þ`žPNŸŸ7åë G"pø¼pø¼è—8Q¥Óà*É»~ñ¸ã®î ”ɰÆò%4Täß$ñΡA±õÀù…±vÄÖƒŠ-ÖÕ.Í‹àC!–ÔñµU°TP¿ÐJÉDDyNR”¾0•¤(?úìÉ MèéÅ…{Á¯Ó…m_û&žÿí¯r.ô`ïíÇþ–í8öÖî«þmÑÊå¸ÿ…gP»êÎë¶O4ô0é àÇ»aÛ–u¼ð\¦ßÕ;ó´÷d¨Ôšñô²­ØX»9íû²Üt—è Gúæý1îs}ÊžÇtÊÒ¸ª›ü¿g~Ž3öøÏý•s×däÉÉý/´€ƒJ)Gõ-e¸­¶·ÕV±‚CŽHçÄd¢d‹„H¸|!úĉ”ý“±ëŠˆ„Ò·®| Q)±mË:<ÿÊ~†r$ôÐ×yú†A…³GŽã¥»ÂÝO=އ¶7_w9s–à¡íÍxó‰çâÚŽ®óƒhëúMõóyñAôÉÙÿáƒO%~Ò*ôx¼~ ž^¶5cûÏ_¢|°¦z½èÀCçð'q¯Ï¬³`óÒïåe_*¥ÅqUGÈ6uå)_ÇdЕ|²ý»…\ J··à+d=Vu¸Â‹‚ 4³ˆˆˆˆ( <QAáM£ÑØ `€†Bí‡\­öÏì^OÖl‹7„F‘_Oƒï޳ºÃýyRá`Z,A‡ËùC!üª»÷w¢X&Ëú}³9Çgþ›ÏLå:˜o)Cã¢J,µ±pîl^DsP‘@8±epb9¥ÓÔ áà•A‡«Æ6©É€¢"ö[:I¤xM£RâõlÂk{ŽâÖS7nr)ôp³°ÃåþãÕ7 .Õãþ­Ï\÷5M›7Áã˜À¯ž{1®íùñÛ‡°ëEVÒ€í-8cïJx9w˜VbÛê72>I8|ÓE-/Y·àšÿÏSùG<f«|ÓÍSËMw¥ul>yÛ?@-×äe_ZJk vy‚âèâ ¥>ðÐ=Ú‘—}]aÐÂÝoe'Vu¸B'¢U:ØDDDD/ˆˆˆˆ¨à\*•Ûh4›¼PÈ}1]ía•ÉŒµó,T¥ÓcØí†7ÌèvH%¨dò¼ê[O0€‹qL€¿ÍX…†ŠüxR¾Ø Ãåœ>/¬öá¬ë ‡Ï ›sÖÑaØœcð‡Byym˜¥UÁ4[Ól=êÜ‚º#L庼«ÂRˆ¤R N¬=QºD"@À =ÜÌ€P‡…2ZõÒw])Â)ÉP”§× '7¬@Sý|ìz¿]ç jìäJèáߟy1¦°Ã´÷Z^FÓ#›`¨®ºîkîÙòú:OãØ[»EoϤ7€]ï·ãÉ + úÚsðÓwñ‹S¯&¼œjz ×oÉŠ}ª3dÿó1P"×À¬·Dÿ{)Ô0[]s ᵓ?‘¾ƒ¢Ö;âbà!Õ•7@«ÐÃp¦þ}÷¶„Y¿ /û±¬¸<++Ä£J+¾:p¯³GÔëý!_Ê÷c"Ž1]aÈþ hTJVyÈR#ç`ûð«:D½ ùRv""""¢¸1ð@DDDDK„æKÕÞ`.Ô~ð…B8p±'ZíaÑRT–h982À *AUðƒ°{'a÷zŽDÒ¶ …su¥y7‘Úæˆïiÿ›oû ä)>sMäð¾Çt¸œu4;6ǬöaØãštåÝu X!‡i¶–ªr˜Êõ¨œ­Cµ© º’bè5Å 9ä© ( Æ6üZdüVÒ$>ñ“触¢íÅ =¤õÚ"NR½Ž|հЄ†-ëÐynï´v¡­«7ë¶±¦Ò£A Ke9* ZT”ia©2àC§°ëƒö¸—›í¡‡¾ÎÓ8{ä¸èv¿yõçxh{ó _óÐöfôut£¿«[ôòßi=…W×£¢¬0ï£'ü´´=›Ð2´ =~¹î·¨+ÏžNYй“ÁÌßsLWh¨+o@µ~Ì:KÒ .嬙¿{¬o¥t÷Õ<ˆeÆü ‹™õùóP™tTÜ™ N¦|Ç[E·É•÷öY:5YÄïpã¾#˜° ì À‰hU‡}ì """"Jþj”ˆˆˆˆ š ­F£±À› ¹/']xéä1¬gaµ‡ SÉ娒—¢JW »wNŸ®€?éá…T µ\B‰Òâb(¤ùy‹8âq‹nó—f f«53ý4èš@0‘G±§YçÐ º„„ƒÓüª<â ….¢A‡|ªâp­pÃ,:z PÊQ¦SCWR …œñÏgrEô©ùbI¥€„CƒÒ`:´oÅ€)DÛ+U@Qû3d²hu©•y(**ŒÀUÃBš04æÂÑÎOÑu~ç1™¦Édõ L3ÛQ¢RÀRUã¥pÃõ|ûË Œ¹ðÑ kÜë=<´½M›7eÕ1éx÷`\í¬­7I¨Kuxì/á‡_¾/®uüx×!lÛ²® ¯9;Ú[0à²ÅݾJ[ÿ½±=«ž„Ž„`;ņz´ G3² ËŒ+PWÞ€:CCÖ=ß6q>«Â)”|ËM«Rx0ë,xdé÷ò¶ÿª´f(¥ÅH"xB£?Ž÷*£!7e:5†Æ\ÃL&?þÖ“¬êuÀ¬ê@DDDDÉÄÀ¼K_º>j4÷!ZíA_ÈýÁjÙåòÊàŽgfìH‹$PÉåÓ‡#“âqZ7»bæÿÕræÏ2Àå÷Á #‰d] ¡kh'.Âéóæìñrø¼8k†ut8iL»Q¸aæ¼”H ×£¼´*eᜟ…N*N“å‘EƒDé Äv˜6…h°GÉy_iQT(€ßŸšå+…^©(ÓâÁÕõxpu= §Cc.ôôÛ!Œ¹0d>ý½g`ô¦aˆšJ4*åÌßFC —¦¯ïk5zúGqaÀ÷2¼NÞ|â9ȺÐCÛŽ¹°ëýv|ûË bœ<Œz÷Çw=‘*ñÑCB¥Öœ5ûÓãø3F/>°å\È¡X!‡i¶–ªò™pƒJßT]JòR ôšbžD_ ¡`|m§}z½$ï0Òmj*»—G±‘J‰J\¨ˆ¤r@&㹕«,UåØöÔº¤„0Ù IDAT¼N^ºû!Ü¿õÜ¿õ™ŒìÏ܆%X´r¹è Â=O}Gôºîá¼t÷C¢ÛMzØõ~;žÜ°"¯ÇV¿«7î'¿oY¶ËM«²b?ÒvÈÆ C"lçy-ËMw%uyOÞö¯h’J:EiÒ'ëwvâoö ®€óŠŸŸ<‚í-øåºß¢®¼ƒõ&&üœ±w‰ng©4äܾ*äRè5Åpº}yu Çâ<”/š—šñÔû.ìûüN7O®¨W4_ªšNDDDD”r <Ý„ Í—U{0r_øB!üâL–æàá…KYírÂlµåêŒz&Zθ׋q¯]Cƒ¢•Œ&Ô¦ ”éJJ™ wTš±Òß >/ÎÚ‡Ñ) bh27ž.7K«‚¥ª<îê ×R¦S£¼´$î Q¾ ÄMÀ¾žHð{¹2:¹›’ЧáÔ-—•Ò¯¨P(€)y4`_;€R„èñ‘J£ÿeÐ!÷%3ôïµ¼ kë1<öÆvª«Ò¾?½±-_^ ¯3¶Ï—ß|é…¸¶³vÕ¸ó‘8öÖnÑmßi=…W×£¢L›Ô}ר¢Ûtv¦d2ì]¯ÆÕn±¡O/Ûšç†'èF·½¡ÈçD’vPËK°±v3î«ÙÕ׉ņœ±wŠè»É”m˄ߑ֧ÖÓõÕ•7@«Ð_5Ù>jÉûj"ÕzKÒÏ…¿?ðàuûßpâoö ¿ÿÛž¼9g&üN@›üå^^#V%*,Uå9Ùº’ü /Þ³þ Gl=¢‚CnlÎ1ô:ÆqÖ>œñ>ª¯0a¥Ù‚R‘ç²Õ> ëèðLå‹l•Ê€H%ÌžU‚òÒH%žtD7 &7ìp¹@PJž†D7VT1—W0¦CÍ?;€¡±Ä«oy.üê¹Ññî´W{˜Û°ÿ½çþýÙæk×­ÁÝ[ž@íª;Z¡º ÷lùÞk?Iÿ£V<òeI¯ò ~ún\O|ÿzõ:,7­Êøö_/ìÐÒölR–¯S”b˲­)©¬‘*³Õ8c×fÄ3ÄÀCXnº+¡ÀƒZ^‚çnoÉû~ªÒV'ÿZcU‚ƒ½ï¦%ðpÆÞ•òuxBÉŸL>áwà£Þý¢Û5ÕÏÏÙñ(•H Q)àNBÈ5[øAqŸáÎKÊzþzöÁ¸õ"ߢœšAØÁ® """¢LaàˆˆˆˆH$AöÆjDCë ½?zœãøçößaí< Öγp€PVš­ÖàÁÅõ) =Ÿº„¬±| š+'8]p°9Çà…²¢oâ :8|^|<`Ëx5Š)VÈa©2ÀRUŽ¥5Ƥ¦1è@$N$ „‚©]GÀ0‡™‰4·–KD7g©*Çk?؈ç_Ù ö¤,sºÚÃ=[¾ƒû·>“¶}Q—êðØÛ}§áqDŸü;·a Ô¥º¤­çî§žÀo^ùyÖTyÈñWwØžñm¿VØÁtcÛÇ[á N&¼|ƒjþ実å\U‡l .LÐ)KyÁÎËM«ðJí7ÖnÎû`Ìlu”Òâ¤/7ÑÊ™¶Ø >ôuùµ9Yb Ž|Q.WxZ™7‡ñ OFÖ+?þÖ“ûó'8’ ÃˆVuèeWQ&1ð@DDDD‡KÕ0دö.öà”}ÔÔb¾Œƒ„²Î} £}°ã)]Í9ŽŸýñ•Ð)‹asŒÁ–âuÆClÐÁ ÁjÆÇý6 Mº²ò›ÊuXb¹KkŒ0ÍNíe™A¢øÒ0_`j*ZABÊo>RTíËd.ˆ2K£RbÛ–uI =x.¼×ò2Nî;€‡^~!áÊ bÍmX’²e«Kuxh{3Þ|â9ÑmóµÊCÞŠQïPÂË_[ó×xtéÿ‘“Ç´$Ž€F·½#§ªXP|ê ñc³Î‚ûj6äuÿÈ$2Të$}¹Ý£|IòF—øÈNM¥!©ïÝn¯=ývôôbÒ˜ùû´ ƒÆ2-JTŠKUI Ш” ­S.ËŸ”ùX}UEÜëó;ܸ°ï&lO Ï=êDDDD”5÷âì""""¢ø ‚ð¦ÑhlE´ÚêBïÁI~rª+Mó°vÞ¨d¼å ì¡–+ð_–5ោ'Lùú:„¬ë¥L†ZÃQA‡!· 'l°Ú‡³¦*Å´b…K-FXªÊ±¤Æ•Rž–õ Zˆâ%wý„‚ <$J*’yÙ—²ºQVШ”xý›ðã·á£Ö¤-·¿«/Ýý×­ÁCÛ›a¨®Ê‹þjÚ¼ ïµl‡Ý&þ³}¾Uy8øi|OÊ~¼~Kf?DBè¶wb2è¾âç»­;qÆžØÄâb™ Í+vÀ¬_³Çլ˞m÷‡}¼Hg²Jiq\ÇåÉÛþ!ïûÇXR ©$ù7ÝöŽ˜_[ghÌ«>ð'¯ÊK÷h'ÎØÅWy]sGmBës¡óÜ Úº>EO¿Cc7y`Èù«TSiÀš;j±¢a~\á …<n¼Æ]â«ÊÊÕñFú[ObàðI^ü?׉hU‡ve þÚˆˆˆˆ(A—JùÞe4ŸÐ V{À‘Á‹8eÁ5µ¸Õ0‡ƒ„²†¹´ ÿ×Ê5i =d ¥L†;*͸½ÒŒâ‚HÙ\ÍÁT®ƒ¥ªËÏMy‡/*Ó©QQ¦Í«_¥S8œ¾uE¦€H`.)~RY’ü&š(«|ÿ[«a©4àõ½mI]nÇþƒèØ÷o}w?õÔ¥ºœï«û·>Ë*öŠ÷¢ Íì""""ʺûqvQr‚°Ãh4î°@C¡÷Ǹߋ_œéÀRÃ<0¿e1>Mž(Õ¦C¯··á¢s<¯÷µ¢D‹Û«Ìh¨0Åôúl­æPSiÀRË-XZcÄ,:íëgÐ(9Òx¢“õ ö{¼$’èŸH$ñeI¥ Ÿe£W×ÃRUŽæŸ}ˆIo ©Ë~¯åeü敟ãž-ßÉùàCÓæMøÍ+ÿ†þ®nÑm÷ê“V$þ¹Þ >4q|ðpR'Ã<,ºÍã·>•ÑcgsžÇ˜oôŠŸy‚n¼vòG‰ÝSê,غb;ÔrMÎ_ªõÑm¾ IÚgÅ©Ì܃Žûìè›øÁHr‰5¥‹ Qä~X+n½ GúˆjcÒÌÍû~IUu‡èµ¶5æ×Ö•§þëçx®ý™Öïê+ÔVSiRì<7ˆwZ»ÐÖÕ›²}iëêE[W/þú®[ñío,ƒF¥D!ééÝFU[•°/€þÖ“NœæÅþ²a Vu """¢,ÆÀQ]ªöÐh4›¼ÀþdÆyçîgÁJ“™BYÁ\Z†ÿ~ÏžîNì=Ó•wûW_aBC… æÒ²˜^ß94˜uÕ–Ô±Ôr –Ô¡RÊ3² •mÁýB™(¦¦2°Îû=Q %à÷‰¾"rOˆ²VÃB^ÿÁ&¼ðÓ“òTâËy®¼ ><ôò xéî‡D·;xš” Š™®ÑïêÅ€ˆ§ŽO[3}ƶyÄ#à³É«~¾Ûº£Þ¡øï%ÓvIdÉD?e?Vñì‡mâ|J¶%UAŠ÷ÙñÇ¡cWþÌoÇí·ü%T25 Ýæ¥ßxHUl‘ÊêÑëmì×Ú*muVöQ¼AŒd]çÞèz5®v®®ù½}×ûíKßwhï´žB[W/šÿnmAU{ðùÅWçÕϽyµéñ?Ûгïwû ¯hÁÁ® """¢¬½'g%Ÿ Í—ª=¼ V{€/¾ V|<4ˆ‡-Ee‰–ƒ„²Â†º|Õ\ƒ]í貪ªXúb‡úŠJËn~»ïðyññ€ CƒY±ßÅ 9–ZŒXRs ,U†Œ…@!—ÂT®‡^SÌ“„(I2>`à!qEE€¢øâ =!Ú¾¨ˆ}I”Í*Ê´xý›ðÚž£x§õTÒ—ŸÁ‡ÚUwbÑÊå8{丨v“Þ·Æ<‰2[Åó„ïJ­9c“q=A7lWOºîíÄöƽÜt„JäÌVW@§(½j=ž c¾QŒx†’€0ë,×ì¯ë÷ïdÊö? ¥ìÉù×ò™»ÿªŸ…"A|æîCMimÁ¿?,3НPÓë<Ÿ×}’Êêb—ÝaZ™w}›ŒëZ¿«»­;Å_{U 4Õßø=«óÜ ¶½}(­A‡Ë ¹ðü«û±í©u7 =øü¡¼ƒ£¢ÛÈU×ÿ.Ïïpã¾#˜° ¼øœ Ѫ­ì """"Êv <¥È¥Ò¿¬öp™ÁI^:y +Mó°vÞ¨d¼%¡Ì OMa2À_-¬ÃÝóÁj†utgíÃ9±ýJ™ µ†9¸£ÒŒ MlA"›c '.fÅ>^rXj1f|{¤ Œ-ÊKKxr%Y$D+Kp²}b$’K¡¿¸JEEÑ  û(W<¹ašêç£ùgbÒ›ü'Þ^|h\¿ë¶> CuUÎôÏý/<W•‡wZOe$ð0áOÞz»G;E·YS™êáH=+B‘«'|¾vòGq/·\U‘Ò°ƒNQŠ*­:eéu_£–k –kpKI>›ìõ$ø‰gŸF<f«“7tß°’~] yxñ¿Ñ¸T–B«ÐÃpÆÜ&‘ *Ù.ÕÕÄ„ËÒ(‹'ð–I;Ú[D×iMõó¯[©§¯íiC×ùÁŒïߤ7pÓЃ7ŽªÙhpDüq4,œwÝëo= áøiVu¸«:QnÝ—³ ˆˆˆˆˆR‹Õ®vdð"NÙGð@M-n5Ìa‡PF„§¦`sŒÍT7(–ÉÐPaBC… `µÃæƒÕ>§Ï›UÛ^_aBmùÔŠ8:‡qÄÖ“û²¤Æˆe‹çeEÈaÚìÒT´rf.QJD¦2³Þ©P$eÿ'J"”Å@(„ƒ7®öP@*d2†MˆrQÃBv½ø·Øöö!´uõ¦d^§ ÇÞÚcoíFãº5hÚ¼ ë×f}ßÄ[åahÌ…ÎsƒhXhŠ{ÝFƒø*‰ÝöŽëþÛ„ßnûõC U_¨Îp£e]ÏrÓªŒ§Þ‰ó˜ º¯úùnëθ'b«å%xþö–”„”ÒbXJkEMò—Jd¨ÒVC§(ÅÙñÓ× wˆºRWàŒ]\›ÏPJɬ\AÉQWÞ€ƒGÄ]F;QWž_¦ººƒ™ª Ó˜14fd½Çcõ­¸Ú>x×­WýÌíõc×ûí)©~•ˆéÐîÿöš! ·×Ÿç[<ÕÔýÕŸyz?ƒíÃð ñ‚þ9Vu """¢œÄÀQ°ÚÃÕÆý^üâL–æàùµ(+V±S(m¾v¸–ZC4P°Æò% MºðÛ çpa|™˜³«”É`Ö—Í„Šc¬Žâ …ðñ€ 'l7Ü×tXRcÄRË-XRc„J)Ïš± Q)`š­Ïªm"ÊGS‘̬7$ <$EQ —GÿDÂѪ—¿'!Œ`å>J‰æ¿»m]ŸâÇoJIµ‡iû¢cÿAÌ•hÚüM4=²)««>Ä[åáà kB‡Š2ñ‡p$„デq|ð0ú]½èwõŠž´ •Z3â¨$‰Àøo#ž«C ž \Ø÷rŸ¼íaÖ/HúöÞRR‰*muܨuÊRÔÐ5ò‡„¶#žàˆW@*žé‘-‡q'Æ~~.ß%úÚa›8Ÿw‡TWwÄUS¨3¤§»G;ræµ}&®võ LWUK8xŠ×öMég DLzhþél۲{ýA‚á¼8çÆ&ÄWà¹<ðö¢UNœæ…üJ¬ê@DDDD¹{oÎ. """"JŸKÕZ­ö`f²ã¼s ÷γ`¥‰]B©ç …`sŒ!2uóèÂÛ…6XíÃi è‹U0ëg‰®äŸGl=èÌh_›ÊuXV7KkŒ˜¥SgÕ8PÈ¥0•ë¡×ó¤ "I"e°¨4ÕÏÇ®M)­ö0ÍnÀ{-/ã½–—QU_‡{¶<Æõk¡.ÕeUŸÔ®ºs%ì¶QíÚº>°:­ÛÚ.´ááýw'¼œx‹ õ¢*$C8BÃzÍ{ÿÂx‚“q-÷¾š±Ì¸"éÛk)­MJ…µ\Kiíu÷=qÄ5µX /c§PJŒy=r»nø›c Vû0¬ö8}Þ´nßtÀÁ¬/C…FüS\mŽ1œ¸ˆ³öáŒõñ,­ K-·à/k².ä0ÍhТ¼´R‰„'Ñ LW{è<7ˆmoÂИ+åëìïêÆ›O<<ñ×­AãúµÑ A–T~¸ë³ÑíaÒ@[×§hªŸ÷zk* ¸0`ωqSWÞ˜öuö8¬E®©xì±¾ßý™Î‚G–~/©Û)“ÈPgh€Z®IÚ2g«÷Ù1毽*{×:†©äNðBUÚjÑmz=yÕé¨îý®^Ç%=éíŒëšÈW»ÝÖq_çëüÿìÝ{tTç}7úïÜï£ËŒ¿ú›˜^=Ö‡—ßì)Ùª³yùž[cÁXÅ<î|Ãâ¯Ï&»—^|Aoå…®éuä¬ê@DDDDåýþœC@DDDDTxS.ïr»Ý¯ÕnòEBøÑùl©_އ–¯‚IË·,´xél¡ø$F¢$Ów–u÷NŒÁ Á31o`¬à•ÜVîoj]Åa¦þÑñÞ ÞÀxÑÆ¹su#Ö¬¼k[Ü%{.XMz4ºj ×qYr""""1:î©Çÿú üó±óxõx_Á&ž;Ò…sGº ímhݶñf¢X6=õEüËÞŠ®òÐ{Ù·¨ÀƒÕd(›óEÊéÅŸ™s²ÿ›WKÞî3ëÿJÖ~*v˜ÖRÝŠàÇ’Et›á¨_‘c)e5ùÅHe’¼À/`cýVÑm¼Á+5…ªî .ðP˜ël(u{§ÉUÐcs¨ÿ þòØŸKnÿÕ/t"‹¤š•üc¡›UÆ‚Q¤3™ŠxÌ£˜Lä}V©T°UÙq´΋ö­XÕˆˆˆˆ* g«=Ìî¤ïÞû؇?^ñ{ø´«žB’„âq„“ÅãÈd³˜˜Œ!0ƒ?‚!0+j@àæu BµÁ$©m¯ß‡“Þ‚W¢˜Vï´£³m9:W7ÂdЕ칠Q«Q_gGm‰Vœ Z Tj™b<þ9öDDr±š øê:±}c+~|øTÁ'ö]Ä`ßEüÛ ÷nÙˆÖmŸÅ½[7¢±c ÌÕö‚õ¥±c¤ÀÃb¸6 LæK™ -U:“ÂÀDÿ¬¿‹&Ã8qý-IÛ}¼õI4U­’µ¯÷Ö¬Q$ì{Í£…Û² ƒ!¯è¶Rî§R žž„Ac,ê9ÌÊ·Zfkˆs+šŒTÔý/Du‡RtÆwBt©Õ¤8зß?ýmÉíÛWå>sþêwÿGYUu¸Ý«Çú°}CkAªpʈêz½v» 7¹ «:QÅaàˆˆˆˆ¨ÈXíav“©~vùÞûx_¹g-j& ÍË a00ÑXžñQd‘«Þ0™JÁ)í/ýº~‡¯v|:ïÛ3è`Ôë°¶Åû×­D}]UÉŸuÕ¸6hÔœõLTLj.Â~U|èÉÎUkÞÿõóè½ìÃËoô ïН(ýøðä|xòÌÍÿ;š–¡±c ×­AcGLÕvY*AŒzq½÷\hþãïÞ²O1®.ªîZ[Ùœ# ¶Â}¬1òÌYÕે%M¼vš\øÂÊÇeígsU ì†jEÇânK„Ȥ*fEÔXÄüŠÝxªøV~¸ó1=$2Lsq¤mÎŽ²¿ïufWAª;ˆ±ÚÑ^°ë«Xv½Gz1æ FYtV4ÙWÝ_±!‰`|{O?‹Ãý/-êþ5ºªñ—/Q|—Ùš°±~+Úhs®›å±r‡úâÒhŸä×—<~$’銹æøF®,¢Ñh`³Ù`0@·`U""""ªX <•V{˜Ý@`ÓóZÞ‚-õM0iù6f©¹4œ›HI&à G#ø8B&›ÅåÑᲿÞÀ8¼chª®ó6“©úüCèºV” C½ÓŽû×·`ÍJwIWs˜¦×iÐ誆ÕÄ/>‰JZ ÀóÆTªÜ""RFÇ=õèØù0N÷}„>]ô•…G½CõáÜ‘®;~wï–7ÿݺmþDÿñw§¶w]t‡…ô^ö¡ãñU ýc!t½×_6çFƒ­¹ û‰&ø™û¸&­ºÃS÷}CÖJ µF'ÜX!~QUì«pi´WT›á¨ ÈjîÁÄ„âágcý6tûNŠ;?b€ò<êz ï1.ÔãCʵ¤ûÆ;è¾ñŽèvN“ w[ð¹¦ÿ0 è˜uìÏøNà¹c;Dpnw·ÓŽ=uQ±±ÛP¿O´>…õ[<‡6ÖoÅŽö8ã;¯ýú1„Ñû;Ös›;VTÌ5g`pdÞß[­V˜Íf¨ø†ÿv¬ê@DDDD3…ˆˆˆˆˆJ«=Ìí­kxÏïã+[qŸã.H‹&ðNŒ†£a GÂSÿŽ`$šû·wb ÑäÒZQñ¤w_%ð0™Já½!/º‡¼ˆ§RíS¹Us˜ævØà*£p‰–µ:>Èf ·O ?ù$"*ˆMí+°©}ººûñò=E>ÌffU©ŠåtßGøÁ+lj%Ê¢¿f¥`ûòæü݉ëoIªB°ÚÑN÷fÙú¨UkÑRÝZ°1¹ÛÒ i’rÙ…K"‹ GýŠâéI^XKŒ”ª-s­î_NêÌ®¢W)¦3¾ãÛ×HÌ‘˜ç‡sógÓ•¶7?‚õ[±¯g/~zþ…Å_—5jÜ Ê~lú*<Ñúv´SRPfcýVüüá·ñå#Ÿzü¸²æ·ÏUK¯×Ãf³AË‘nǪDDDD´$ðQ á8€f·Û½Àw9"9ãñ~zéZªjð•{Ö¢Öhâ ”€é à Œ!’ÈMÄ™`Ž„1p°æá ŒÃÁeÍMÔ/fСÆfÂýë[йº±,ª9L³šô¨¯«*«>-%:(à\MÎ "*¬íZ±}CkIÊÍ«Çúð__=]V}Öª óŒO ˜˜{‚§ÔêO´>%k?[ª[¡QîE‰F­Eمᨸ°‡”à‚R+øG“üì ÔH™¼í'TÉ÷»’ .²ŠÂb …¼8Üÿ÷¿TÈBžô|*‘µŸ6}v´ïÄŽû¾¹èêmÎìh߉ý={Eµ F+æ¼ó ßöP©T°Ùl0™ø]À,öØÃªDDDD´ðk?""""¢&žÕ:O€A IDAT8"9q<î]l­oÂCË[8 2¸½¢ÂÌà\ùd²à ókªªA8‘ÀhLüuy±½å÷ŠtX³2WÍ¡¥ÁYVc®Q«ávØà¬¶ð,qÙ,Í™ 0]@­æ8U*P% SåA«ÍODDTxÓÁ‡ÞË>¼üFú®ø8(üà•c8ÚÝ_vý^Q}oAö3ßDÜᨀK£½¢·¹ÚÑ6§|½ØõÕ¨1þ½U­ÑY˜ÀƒB+øG’a^JL›£cÉG»¾zIWwÆ'0TäÀÃ-Ÿ% [’ãôxë“Ø½é‡‹:ÌôDë“¢¾á`Åœ{·Uw0›Í°Z­Pñ þí¼ÈUu8Ρ """¢¥‚""""¢'Â9ëXíáV“©Þº6€÷ü>|åÞ5XUU»$Ça8ÆH$rÇφ#·~±<3°Üp Å«2𰥩®zøÃ!üä·ïŠÞÆÅaž‰1ã“ë·Q¯Ã§Ûqÿº•¨±›ËnÜ­&=]5Ðë4< KT6 ¤S¹?™<ç(h4SøÉUÅÑëx\Ù}¨T€–…^ˆˆŠ®ãžztì|ƒ#xõøù²œ¼¯Ä˜ä£\Ãðû®Ïà}°0ou‡ᔤí~¡åqYûÙ`k*Ê1¨1:¡Uk‘Êä ¯3‰<(¹‚4†Ygå…´DH™Ì-%tTJŠõø-R¯£Kņú-ؽé¿È’ûäÜkÝf2Q9ŸñNWxÐëõ°ÙlвtãlXÕˆˆˆˆ–$¾; """"*¬ö0»ñx ?:߃µŽ»ðèŠVÔK§´õ¥á¹WTŒ$ðNŒÍú»áh#Ñ;WÂcH¡4Í :LsYmhwÕ£Ï/n5ÛT&S°°CÍ„û×· su#L†ò›̪¥/›’É\ÐA¬t:÷G•t:*‰Z“«¾ d½ÕˆˆJIKƒù§à™Ç7¡ëL?þùøyøÇBKnÜК×íÊ9ì@ÖU®ç2¸Àªã'®½%z›N“ îÍòƒ¾º c1çû=£CT•‡:³Kô>”\Á?RäÀƒUgçÅû6ê· ÛwRT›r ®XtÖ¢>~KÁÁ ?âI? ›¾ »:wcGûN†B†Fa·Ûa2™8wbU""""ZÒøu1Qaµ‡¹]ý—ƆQo4c¹)÷eê\ÁÅš/È@K‡ËbÃgšn :Ì´¥©Età¡V.sàþu-XÛâ.Û±gU‡Ò—NÉ8]äv²Y ‘ÔI@oä$öJ¡Óçδ¡½P«9ÆDD¥ù΀ÇhÇc´£÷²]Ýý8Ý÷"±DÅßw‹I'¿Ð¹àíººû;¬väÖO˜oUê‹#¹•Ù½Á+ˆ&#¢¶¿±~«¢ýÆ'OÏŽ ðÅW¨”êÓî¶4ˆ <ˆ¨ä þQÃ3iÕ:¤2ÉYN·²ëÅ<EVÀWšÛ²lÉïr¯Ð¡„›ÆóP< 3òˆnS﬌ֿ÷]ƒÎh…žþ̆UˆˆˆˆhÉcàˆˆˆˆ¨ MU{x¹j[9"9él×cxÂAƒA$  ÉÊ Õ¢Õq6,k‚Ëj›÷¶ÕF“¤*Jé\݈ÎÕhip–íø³ªCyH&”ÌÅh2Y Ë…8™½2èõ@òUzP!¤`5"¢òÐqO=:î©G8¶ §û<8Ý÷N÷y*ò¾ZLz<ÿ͇áªÿýCïež嘬û6ë,ètoF›sÚùOjŸ*Fq¨ÿ ÷¿TRã¹Pu‡ᔤínm|H¾c^«Ã›uV4ÆyÃ!·k²·H ‹(!"2h#•UgÇD|ôŽŸ›´f^¨oÓæ\‡£ž#¢Ú Ç”[‘ZƒÆ()TiÆb#<ég<—~ã÷ÿwüiÛ× rmïúèuÑmŒ†òi]ÿ8ˆ·Ïz1<…Ša‡;^"ØÅªDDDDD <•-A<¶¹Ýî]ö¨â¨äh4ÔÔÔ # !NsPH²éC«ó.´:îÕvò¦¢:W7bû†VÔØË{«:”%ÂÓ²“ =TPk_ D­tžDDåùÏ€íZ±}C+±xÅ…ÚWÕã™Ç7-:ÇâØó“_˶ßZ£ÖþMtº7/j;ÃQ¡¤Æ3š #˜˜aã×ÞÿžÉ½fU¶~–ÊêðµFnD†ò¾½”1¸8Ò«È þÑTa*<ØôU³ŒZ#/з‘Rµ¤Ô®!ù¸ÛÊêƒ!ÒÙOzä‚`ßþÌ^ԙݸ8Ú‹Z£-խШ•›fs¨ÿ è6ËêÊókx2·ëÁlæð=Aöpˆˆˆˆˆrx """"*s‚ ìs»Ý¯Õî`0`0‰D‰DÍf9(”—¦ª4U×¢Õqׂ•æã²Úà²Øà„ Ú£^‡ûׯÄýëVÂT櫼¹/nYÕ¡ô¥SÊ…¦M‡ &€‹þUP›r•ÒIqÁ• ÐéXÕˆ¨RÜ~è»ìé>ú.ûà •Íý°˜ôØÔ¾m»/ïêj{þé-Db‹¯PXkt⛟úOhuÜ'Ë}ŽúE·is(·šûÈ༿&Ã’*tÞ½YÖ~ÖK£ª^Ñ)*ðÐæìÀ¥Ñ^QûP*˜Ê¤OO Q6x`ÓÛæ»bº8Ò‹.Ïë8ã;Ž[36ÖoÅöæGŠZ9¤ÁÖ,º7PCò¥UkQg*^u‡e¶& -PŦÎøNðE €?\ùž\û[~669‚àÇhstÈ”›9ö—FûD·[¹¬üª¹þæC§/ "žäBE³èð´ ç8DDDDD3Þ·sˆˆˆˆˆÊߌjOØV{¸…ÅbÑhD(B<ç€Ð-\\VÜSᄦêZY·ßá®G×@AîK¥L]Õq_*]6 $Ú€D0pÑÕŠ1\ÐjLH§sÏ~P«rU!4šÜßDDT™¬&6µ¯À¦öÿX½—}è½ì+ÉÄtÈas{óÍ>çëÕc}軲¸ªpfϬÿ΢+:ÈA© ÙéLjÁÆE‘“õ§mm|H¶~ÖŠ®ü-öXhÕZ¤2ù­Ôn‘0y׸¢ØyM†¡ÑhP]]D"`0ˆtš«G-5MU50hu¹`ƒÕ†jƒiQÕòu¯ã.Å56¶oü=¬Y鮘p€Ûaƒ«ÖÆ·L$âVæ_¬L&WQ‚+ûW•*wLg×L@P©YÕƒˆh)sÕÚnVrˆÁ Ž¢w*QÈ„ŤGÇ=õh_UŽ{êó®äp»p,Ž—ßìYT_V;:ðÜgö*²Òt)Ž Þæýÿ.z»rOÖ/æDù¹ú“o¥Ž&û*ÑÛWrÿH2¬x¥“Ö «ÎŽp2xógÅ\áÿö°ÃL¡D_>ò9¼ñÅßHª¶°XRÂLÞà•òúÂÚPý %Šn)œ&žûÌ^4UÍ=T"ôp¨ÿ º}'E·«wÚËâ³À@$Žcg½¸24ζ³;\U‡‚ˆˆˆˆhvüZ˜ˆˆˆˆ¨Â‚0àQ·Ûý(rÕš8*ŸÐëõp:ˆD"ˆD"Èf³”2gÐjá¶ä&ÆWM¨6š`Ôjášú™ÜĪ6š`ÒéS`ùûé CçêÆÊyŒê4htUÃj2ðä.Ó+òZ2ÉÀÃR Vs ˆˆèN®Ú\8vSû |uÆÏo†FC¦þÇâ¸:4*i?+—9`5ÐÒû»ýžzYƒ¹?>|‘˜ô÷ [·ã™õßQlœ‡£BÉó|&íKYõ¼ónyJU¸ªÖèÌ;ðPgv‰Þ¾’+øã ø5Îuøÿ]¤2I5&,·¯(ʱ yæ ;L %Ø{êYüÓç_-JW;Úqi´/ïÛG“‘²y^©5:¯(RCž¼ªLÌ6~.Ë2Ô™]¨3çBCmŽuw^ËcÂÍço`‘d—F{Kâ¾7Ù[°{óó0¤2) Lô£ÍѱèÊ ÁøöžzVRÛû×·”üyuú ~ó¡€x’ Í‚Uˆˆˆˆˆòį…‰ˆˆˆˆ*” ¯¹ÝîãöØÉ¹•ÅbÙlF(B,〔 ¦ª¸Y•˜ 4r_@»¬vµåñ¶Ö®7Èx¨Ä ÔÚͨ¯³CÃÎe%U¤ï«³YVy ¢ò—Nç*™d³@6“û™zª¢‰ZÃБX÷Ô/x›Á„ç ä³ 9ô^öáh·ôJpJ‡`$æuû õ[éG4^pb}4Ý_hstÈÖOƒÆXr¦kŒNhÕZ¤2©o;=IY %'*GSႌ‘UoÇÖÆ‡Š~¬º>z=¯ÛõA0>Q”p¤*+ ®–_ Ü–eEïC›c]Þ+ü_éE›³Cö>H©îà4¹ðþ¿ù>SÏyž$Ò ô¿3¾’B‹±¥q;žZû ÑÕ"É0CžEŸãÏÛ!©r‡Q¯Ãš•î’}\]ÇÛg½Fâ|‘:»×‘«ê0Á¡ """"Z¿&""""ª`S–ïr»Ý¯x¬öp •J»Ý£ÑˆH$‚D"ÁAQËb»PpYm0juŸü[3ýóò 1ˆ!×}ªÔ ƒF­F£«UV®¦Xn¦CÅ’N3ð@DåyíL¥€t˜­ÖXf*ø€d.ø ÓñZG$§–gIôãå7z$·-DØ¡”äSi⢄‰÷MöI“üçb7T•äøÙõÕ›É{L¼ÁQÛ&â' ç#•I)¶íR1Ù¹Ëó:žh}ªà}3!Z¤ ª<4Æ’¨Î"¦Á„2ó£¥ä^4U­B›£_]ûLîš>Ò‹¾¿Ç®½‰Ìt*X!‹}N½BÙ-ùZu o?ŽzŽHjûé¶F˜ :Ñíbñ$Gá¹óºSï¬BKƒCÒv§"qüºû*®ùÂsv^»AxCADDDD”?~MBDDDD´‚p@³ÛíÞ໑[éõzèõzÄb1„B!d³YÊ< Z-ÜÛÍÿÏ /µZ¸fü®©º– Õq¼qÉí+5èV“Íõµ¬êP¦2éâî?æ1 ¢ò»n&â³f“͉ NzC.A•)›}å[Z½¨ gTšz/ûÐwÅ'©m“½eI…`lrtÁÛxWDoWîÕÑíúê’¿£#ïÀƒ” »žÀ€"+͹ÕÓ—JàAŒ}û‹xTá!xE±óC. ¶&öcJ¾•Fn½–®“½mÎü÷?ÌõåïºÿüË•_(RõA®¡ÔëàÅ‘^|ÿô·%íÓ¨×áþu+ó¾ýx0Š÷/]Ç7àY8ˆ°f¥÷¯[)*(O¦ñ›þ8ýÁ/ÔsÛ`«:‰ÇÀÑ"ž©jûlåˆÜÊd2Áh4""Wô}mªª¹åÿ3C 7ÿ?c)a9ÞÇϨ×aûÆVQ_`–“euUpV[x‚”±t¦ø}Ȥµ†Ç‚ˆÊàš™Ê…$]ë2@|0z¨ð$‚‘I„cq$’ 'ørÁ-ô:-LŒ†Üß –§®î~IíÌ: ¾ý™½éc4YÉ0âéÉowqDB…‡ªU²öÕR¢ókN ¿s®ÍÙK"«e(9¡½TÎÃBhsä?†—Fû0ò ÁÖ\²}œ)ñc¨UkQk,Ê?ëóû¨Ö¦¯Êû¶b\éEHD¥‘Åœ³™+Pó ÿßÙðŸqÆw{O} —FûdÙߦeŸ“-@LL ŸW¥#>¯½õ˜ä}Þ¿~%jìæo708‚®î~\µý® øàª€5+ÝøÒƒë à^øhÇÎzOreŠ9ô"WÕá8‡‚ˆˆˆˆHâ{xÑÒ"Â9ÛÜn÷.{TqT>¡R©`±X`4 ‘:+MfË«j`ÑëäVú§SЩ5p[m·Üîö Pe4¡ÚhâÁ-"×mÇ)56¾õ·U䊾&ƒ®j®V\²%xHgx ¢Ò—ÉH;ܼæfÄ$ g衬£ð…ò 9Üú|—A8–b·žHzz­V³F½z†¯±Jœ,„£O®ýêÌî‚ôÓݦÍ!ÿ*ßÃQAÖÛÝÚ_y'é—j%Z ‹Îš×Äs)ç—’Ú#ÉÈ’¹6l_ñlúª¼'œw}ô:v´ï,h¥TxòØ,¤:“ uiL™h°5cCýtûNÎ{;¥Ž{—G|u‡&{KÁž—6ÖoÅ_ü-ôíǾž½’Â3OŽÊÚ¿Ám"#ÏÛ!¹jE>Õ|ü~ò‚è Ãí>¸*à¿>…¯?¾yÖט×?âí³^ ODù"onßa‡ˆˆˆˆhqx """"Z¢AØ7£ÚÃ#‘[i4ÔÔÔ ‘H ¢ÑžßVfþŽê ·k«›û‹ÀÕu®yÛF“ \L “Íò U¸»¹"'«ÕU[àrظq…È”@à—C"*‰¸L×Ý,J:Îg/¿s ™Æuÿx.´ óvÉôÛµšô0tÐë´0´°š <%¢ëŒ´°ÃjG¶6>TÒ÷MÊdè…ó˜ÐM†1ó‹Ú®Yg‘u’n©Vw¸ùþÒèÈ/ð`?&GzVeúLL,©ëíOá§ç_È붇ú<ðÐ`kÝf8ê/é1w[Jª?ÿôЫøò‘ÏÍYÅàÁ懱«s·2ÏO½&º\Õ]´"B';ÚwbûŠGðµ_?¶¨j—F{qâú[²=·Š©ò°÷Ô³8ê9"y_l];çg†±xG»ûñι«²¾‘ࡇ@$ŽÓñg„/îævÀÓ‚ x8DDDDD2¼wä-]S¶?êv»E.øÐÄQ¹•^¯‡^¯ÇÚúåxhù*˜´Å{51ÃP…Ê’F­F£«UV#ƒdU U&ˆˆæ“NÉÎJ%­–UÊI,žÄÀà(ÒL †c‰;BÓÕL,&=ôZ-ô:–I*´.‰ÕžZû¿-½ëg&•×$})Õ(šì«dí«ASÚïsjN æ±’xsU‹èm+½‚4.Ùêrx¸4Ú‡Á ¶æ‚õOʾ¢ VY,»¾ºä»vC5~þðÛ8pþê?x³À2[žh}J±°C0>!)<Ðæ”§²Yk}.¾ñÅßbç¿ý)Ž\þ¹äý¾táñi÷fÙ®1Ã1aÁÀáþƒy?Îg³r™«gýo8€_= ߈üŸûF‚øy×Y|ã‹ÿþí7¡§ÿâ"+–-!{AØÇ¡ """"’DDDDDA^s»ÝÇì°“#r§“¾kxïc>¿¼[ê Ÿ ñ…LNò@PY²šôhtÕpB-I©¤üÛL§-«<”…b„æ2] "þä}…F­†É …É ƒq* Q‰UÆJEïeüc!Ñí¶4nGSÕª‚ö5š*þ$å±ÉüVމŸp/ebÿ|Ì:ËÍŸñÀ¾ý¸8Ú‹¡ê·`{ó#x¢õ)Eª`ä×?+´j-R™Ô‚·3ë,ˆ&#yo[lu ±‚‰‰%xhsv`™­éæ$÷… †¼ <€M_…P•W¦yƒ%;ÞufWIöËn¨Æ®ÎÝŠ…fÓåy]R»N÷梎Õþ?xÈG®H =D“¼qõ0žh}J–þ Gýh°5Ϥ¹8Ò‹½§ž•¼}£^‡/?¸~Öß]ð‹£g1™H*6Þ <ÿów§ø¢nn¯#WÕa‚CADDDD$/5‡€ˆˆˆˆˆ@„ AvX —#r§ÉT ¯]íÇ÷ßWcÛ/ÃTÎÜZœ ;Ñ’”Í™¬üÛMsŽQYHg2ðÜ+‰°Ã|} Çžˆàº^Fïe>¼6Œëþ øÇBÇâ%}ÊÉé¾$µ“k"¦žÀÑmlò.Lä7WPJ…:³[Ö¾jÕ¹5æõÄWŽüŽzŽÜœ´Þí;‰ïŸþ6¾|äsÆ‹7ÿ±ÆèÈëvRª_\Qîc¤H WPÂöæGJºmÎŽŠgƒÆ(ûu œu}$>ð gØÁn¨’Üvÿÿü ¾´z‡äöo^=,k%’¹ž“‚ñ |ùÈçD†n÷ÈÖµ¨±›ïøyÏ¥ë8ø¯ï)vÐëõ¨©©ÝngØan^‚ð(ÃDDDDDÊ`àˆˆˆˆˆn!Â9AÖørå—é6ãñ~t¾ÿxþ}ŒMÆÝÕÁ;!> Sî+ëêu´48વñ "¢%+“Vh»YŽm9™ˆ ‘L—eßcñ$Æ‚Q£! Žâ€€ Gà !žD,žäAétŸGt›ÕŽŽ²™”+÷JóÁx~IxâW—2©>f­g|'ð—Çþ|ÎÛ\íÃ×Þz¬hÇǮϯº„”êRªläKLµ‰J°£ý›yß¶ÍQá%1R•ju‡bÆ'pÔsDt»R ¿üíÖ®”v}®ò —á¨Ö1^lØaÍJ7:W7ÞñóžK×ñ‹£gWF»ÝŽššèõz>Xæö=ëA8Ρ """"RDDDDD4+AöXàGcvqüMÏ;xíêïKÉ¿º•?bØ¡BLJX†¹¾®ªlïo•Õˆ{—×Áj2ðà“âÔüt‹ˆJ˜’Á„Lšã[ÊÒ™ †Ç#wŸÂ±„Ñ<7ÆnVƒÁuÿF&"Çâ<øs½œ ‹ˆµuùCKô1”B<ßûa)”˜ì¼÷Ô·¼M·ï$ÎøŠó1K­Ñ™ç؈ØH©²‘¯¥Vá¡ÁÖŒ õ[¼Ýã­OÂn¨.xÿ6Öo«ˆqfu‡Oty^—ÔNÖ úÅŸË?ÚþÿåõØ™œUâéIŒOŽÜò³çŽíÀ¥Ñ>ÉÛ¬±™ð¥×ßñsßp¯Ÿ¸ û9¡R©`µZáp8`2™ø ™Û ëAØÃªDDDDDÊãWÂDDDDD4'A<‚ lðÇÈ•e¦Yœô]Ã÷{Nâ¤O¾!š˜Œa,åàV8´dî벺*4ß] g¡/ %q˜U<DDTzáI¤3™%q_ñÆ‚Q 008ŠÞË>|xmøf‚• rz/ûD·1ë,ØÚXœÀƒ”ª r3É]Êd{¹';´Æ¼'³ê?X”1Õ¨µ°è¬ ÞNJõ ¥WðÆ—Ö<Òç8›~îEV;Ú±{ÓËæþxƒWJª?µF' #Ÿ˜¦H a5Ù[J24²Ðcg.Ñdï §dëÇØŒÀÃÞSÏJª 1ͨ×áé?úÌ•`cñ$~|ø4&ò¾Î2™Lp8°X,P©øË\/õ|K„m‚ œãp†–C@DDDDD á5·Û}À;9"wšL¥ðÚÕ~œº†¯Ü»«ªjµ­¡ µ‚x'ÆD·iYæ(«ûh2èÐ誾ã Xªl*5€"ÏåÔ,lM6 d3@&dgü&µÐjÎG "*®L:w„˜YäK­žzÞ\"bñäA«I«Ù‹I“AW6Ùé*þ±GŽÅquhtÞ6í«êoþ»ãžÜ¿ß½à½o9WÏKʪúë·Ê¶ÿ`"ÿ î#1¿¨m;MòWw3yúpÿKØÕ¹ ¶æ‚W»¾jÁcÛ\Õ"z»JVx€h*\”jÅÒ`kÆ¿ÿÉöž~‡û_ºåw·>‰Ý›~X´ñhstäz¢$%*¼”³®ÄWxè¼[Þç'¹Îç[3vuîÆ÷O[tÛ7Ë2ŸÌ½N8Ô?=ÿ¢¶õÈÖµ³VýÅѳ²†ôz=, ôz=ó{ÀÓ¬è@DDDDTx <Q^¦>Äßåv»_ð"€ŽÊÆã1üè|ZªjðèÊßÃ2‹MTût6‹Á ¿/©$“©¼qÑíjíæ²¹µv3êëì¬ê°iÔ@ºÈ}Pk*sl3i •þdâì‚·Ï©dnB­ÞÀàQ!¥Ó@:•û{Z,–ºåÿÓÿV«§þh–Þ8…c „c‰›ÿŸ€°š %ÔÏ8N÷ypºï#ô^ö!2£Ïùê»â›õßbµ9×-ÙÇU4QlÛ¥°*ù¡©ÐC¡Õ¸š÷6ffEÔ1:«Ô&Ì‚ÝPç8€Ý›~ˆ‹£½°ë«Ñæì(‰~‰<—ÎñÓªµ¨1:ùâeJ×G¯#”ˆn÷iyZµ¼SVv´ïÄó/`($®¯78€á¨ ËsD*“Âo…w±÷Ô³‹ÚÎýëV¢suã?¿0 àƒ«òÍ4 , L& œ"ÈŽs(ˆˆˆˆˆŠƒ3ˆˆˆˆˆHAÎ ‚°À·+ßL³ŒãïϾ‹Ÿ}x±T*ïv7B$Ói`éýXt£^‡š2•÷mC‰œ¡(Ç*Ÿ ëÍU-¢·ë *>‰§'‘Î,îÅí‡cÑ7܃ËãñÞwÊ$yñ-Çz¡¹­ <ˆS‚ñ õÝîÓwËÆ3hå¯òôDëSXfkÝN®àÖKþWÆ/In_ï´ãK®ŸõwŠÉÄâ®_ŸmÞŠæú•ð§†ø@˜ß +AØÃ¡ """"*>ˆˆˆˆˆH2A&AxÀÈ•u¦YL¦RxëÚž?û.Þ÷ûf½M:›…?â`UïļqÑíZœ%}¿êª-hipÀdÐñ ´Eª² RUV…‡tjj⬌ÛL$ò_qœˆ”¡Ö(SåA­áØËt%…Âiù¾NÊfçN,UðäÍðƒo8€DR™'´—ßèÁ×ÿö—8ÚÝ_rcÐ$a¹lïcŠ\5!ßê%ózXKán_ñˆ¨ ¶RVV—ƒÝ°p5Ž:³[ôv‡£‚¢ýÎ73—ë¡«7ÿÊ$Jy±-Ô5%X•Xìúj4F)]ikØlm|HÖ~(uLvÜ÷MÑmä¨ðpq¤o^}Ur{£^‡§ÿè3³¾–ŒÅ“xçìUÉÛ®¯nÄêåkp5ö;DSa>æy) àAØ&‚‡ÃADDDDTx """"¢EḠ;ÇјÛx<†Ÿ]¾€çϾ‹+±[~7‹ ÉY©ç¤w@R»µ+Ý%y4j5šï®E}]4j~¤@9jMq‚º ÊÛd³S+|+ ç$Z¢¢?*PF«å¸C&$&ó §õ➨’ =Ì*É`x"‚K?GŽÉSo`p_ÿÛ_âå7{J¦¢Ãíš«Vmß’ª&8Öɶÿxª¼fí'•8ÄL° y‹z¨5.°o²‹?ÿ<EûLLð¢X"V;ÚEÝ>*ᚢ„:³‹o†®Äœ&šd~~ʧêŒbªîÌ´Ø*?>ûw‹jÿÌã›P3G5U©Õ´Z-–ÝÕˆ´!±øOþùíÐ,Âk """"¢ÒÂÙ DDDDD$›©òÎ++÷LsðEBøÑùüãù÷16C:›ÅX4Ê©0ý£KªîPï´ÏùÅf1™ :Ü»¼UV®†HwÒé•YÁ|.jµ2ˆ‹%—·²ÃLYÉ$ÏQ¢bÒêä½FªÕ¹?TXb Q‹?êÉ”{B¨áXƒ£¸äñc,(ýýSWw?ž{á®–ôý•2Ἐìù&¬–[…‡™žh} 6}UÞ·ß׳·à}4ë¬7«RÌ¥YB…¥Wñ—{Òüø$'ý–Âã½`¯ÇÔÚ¼Â>KÅ`ȃ£ž#¢Û}úîͲ÷Å 5*vžn¨ßRÐkÙ¡þƒ‰ù%·ÿÒƒëQ_7ûsˆ”êv»‡)U‚'þüNX/Â.A˜°#""""*AüZ„ˆˆˆˆˆd%‚G„mþ€—#2·À8þ¦ç¼ô»^L¦S 2™JáHÿIm;Û–—Üý©«¶àÞåuÐë4<¸4+•*z(È¾è •3v™tn­’Ò)®NTìk¤V¦ª4•v ,Ù¬øpšÅ$í@1¤¶°D2ëþ |xmXtŇ¼r Ï¿r¬d«:Üò¼ˆ+‘_='º]Ä$ÿ…ˆ ŽçöÁé>+OÑEL,—gÿ‘²¿[3z%8pþ…‚ö1ŸúÛœ¢·+¥â†Rƒ!“³Tx`Õ‡?®SÅ»®4FØ Õ<ÓÓ‘^\íÝî -Ëދ΢è}ÝX¿Ut›K£½’Æ´G8%©Îj žþ£ÏÌ{›žK×1ŠÍž p:°Ùl :,¬À‚ <-‡ƒˆˆˆˆ¨<0ð@DDDDDŠÁ#Â6†\™hšC"“Æ á:~võnDÃ2s¤ÿúü>Éí·oh-‰ûQk7ãÞåuÐë4<¨$šNè ¹É¹rQ«ƒ©²ÂÍp_Y “áùITlzýÔ5RÄErúȰCq¤$Æ©Å%ÍxÍ'É@ áÃk÷T{˜;\-«ûã0ÝUÔý{¢Û4Øšä{ÜeÊ¿ÌÉ®ÎÝ¢n o?Cž‚õϬ³B«žÿ‰¥YÂJî^ çŽR'ÍÏn'ƒ¼xJ$åñî \)ZëÌ.´¤Tw€N÷fE®EJ[íh/Ù1€?ùü§\ü¤ëÌïæy¡GMM ª««¡Ñðs¼|K„u‚ çp•ˆˆˆˆˆ¨`Ax@3€ýù…“ üêú~u}€Á‡2±Ø°CçêFÔØÍE¿®j4ºJcåÃL†“ýÊ•F#Ïä\•*79Ø`79¸\úüÎrµp¢’¹FM¹àƒF ¨g¹¾©Õ€V›»þUê5°d³Ò«;L„·rx:Åñ—"OâÃkà FË6ìw™ï.»>7ØšyÞ6b«<ìëÙ[Ð>.Tå¡Î$~’¸Ò“Ú#«„wV€Ie’He’ûê÷ÞàD“‘²Ãh’¡{9ìêÜÃý/å}ûÃý/á‰Ö§°±¾0“Ô›™ó÷M*<ŒÄüˆ&Ê­ÚOÅ%µ›­Â„AÔñô䜷i®j=™÷âè9Åóõœ¯KÁ97Wåš_>a™RQktò€M yDW:€N÷fE*1Ø å¿öÍWKj·©½k[ܸ00wåßpG»ûsÏÛf3¬V+ƒù;ˆ\U‡ Qåà´"""""*º©/ö¸Ýî¼`+Gen‰L¿õãÂø>åtamM¥€&&cø—þ ðƽ-£^‡§ÿè3E»/¥vH$¤µÍ"×V†¨|© üÐcà”608‚WŸÇ龉%dݶ,„—ßìAWw?žy|+>PÉ‘;ìäÌŠsa@¸99p1œ&žºïè”iEë&{‹¨ª sÝv8*àâh/znœÂÅÑsˆ&#’úMFpi´—Fsÿ?Œ—nÞo%'ª+¡Í±îfÿó5dïG*3{Þn¨ÆŽö¢ª<ìëÙ‹í+Aƒ­Yññ³ª0;ðÐTµJô6•ß™âéIQ“¯ç 5ŒOŽñÂY ù¹äTktB£æÓõ¿$©ÝÖÆÏ+sýÑW—ý9}òz—¤¶Ï<¾áèÜï—|Ãüøði˜L&X,h4|E˜§^ä‚Ç9DDDDD•‡ïð‰ˆˆˆˆ¨d‚à°Íív? `€&ŽÊÜ™4Þý؇óã#ø”Ã…{«j9( šL¥Ð5ð;ôù}²móK®GÝ\”û£Q«‹vHK;Üò8Hz°T¾ûÍfìmó(UêÂW y¨T€Zd²…ÙÏRJWw?ºÎô£ïŠOñ}ùÇBØó“·ðà†VüåŸ>ÀÁ§’Îdd;xãÁ(~qô좷ÓéÞŒgÖÿ•¬«Y/f[ÃQ=Â)œ¸ö–¨Ð„#1¿äÉ›4·]»q¨ÿ †BÞ¼nJðܱøùÃo+Þ7»¾ÃQÿœ¿¯3¹EoÓPö<$è±jÿ|Õ5R™$OP‰–Ùšò>§(~ýšõü5»x f8ÔPt%ƒpªFÒ¿’Ô®ÞY…®3ý¨²‘JgPk7£Æn†o8€±` \½¾?ìö*ò°G„} """"¢ÊÅÀ•A^s»ÝÇìð]ŽÈüÂÉN×qa|Ÿ½«w›­M¦RxoÈ‹î!/â©”lÛ}pC+Ö¶¸‹rŸŠvÈfd\¾í%ã€ÚT¹“¹3i •2©\e‹Ù¨¦BZ'µ—(Ä<+VB!%œîû?>|þ±PÁ÷}´»ƒ#x~çðš <TT£Ã%àÅ_½‡ÉÄâžTo}O´>Uר¡cxwèz„S%?ömŽu<°{Óño=ž÷í»}'q o?v´ïT´_vÃü+¬K™èì  š ËšIle“T&=çïÂÉ ON‰D M«ÖŠ ÆT:1¡«™¶.H‘þXtÖ²¯¾ñ/W~!©o$€—ßì™õwz½‹555ÂU®£ƒƒ0ƒ”ê°µQ™Àƒ]_UÖãyÌû’™„lÛÓh4¨®®FMM ôz=OØüœ°^„] ;- <QIÁ#£àåˆ,ìF4|3øJ&8 "L¦RèõûðrïûøÉoßEŸß'û>:W7âK®/Ú}t;l¨²‹¶ÿTPbäL&·íJ‘Nñ˜´±J%Ä$CåB¥ÊUæP+\±8~|øžù»Cè»â+‰>]Å^9ƃCŠPkò| ™Hr°ŠÈ7ÀÑîþEmãëëÿJ±IPgvó@•¯‚“ñçóüDÝ>”à¹c;ï—Ý0ÿÄã¦*ñ‡‹£çëo<=‰tF¾ Æ'Gù rÞè«%<ö®¬µ¬îpÓ`ȃnßIÑí:Ý›{þ*÷ \—Gžµy4 ìv;œN' V­Ë÷RàÏAØ&Â9ÑÒÁµÝˆˆˆˆˆ¨,‚p@³ÛíÞ`€*ŽÊünDÃøùÕK¸·ª¿ïpÁ¦ã aséýý#£ôcY+9Ü®Øa“AW­­¨c­àð"•R~âx!dÒ‹¯X‘ÉæBz#'º—­.wÌÒ <>4šÊx\Pñõ^öáùW޵¢Ã\Žv÷cs{36µ¯à"Y©Õ幫I“AæÖ „£qÄâ)¤•HŸÑ/Žž]T{¥Ã@åÆ'G0ò  M͹¿Eg…F¥…ÝP‹Î »¾µv–ÛY¬˜0­ÍÙ!º¿b+È¥ÁÖŒ»±¿goÞmº}'±¯g/vuîV¬_v}5†£þ9ß\µ '¯w‰Ú¦Ò¡’`b5œÐ^TmÎu8ê9"ò±)Ìû+µ–çÇ ú^Ônër垥f$_/âò/þ?º¶¨ö*• V«f³™'¨8ß°ˆˆˆˆˆ–&ˆˆˆˆˆ¨¬‚°Çívï°ÀS‘…}Ç1Ü[U‹ÏÞU}¾KÕV°ÉT ÞÀXABÓî_·oY[Ôû]_g/êþ3ie«d³¹}”ó)žÍ.>ìps¼³@2è¹H`YÐëä =¨U³n$‡>…>~¾Äûxí÷ÔÃjâEd¼Žæ80êuŠTyPå¹Z*«5vÓ¼éàk,žÄx0Š@d‰dº¬QWw?|#AÉí v€:Se^ºð#ÄRQTâQx%ÿ`<ÿÀÃDœ–š:“‹ƒ0ã±r¨ÿ èvN“ îÍÊ\sæ»)EJÅ™:óÜç7pÉŒ´Šº*• ‹f³*®N!Æëv ‚àáP-] <QÙ™ZÅéi·Ûý"€=¶rTöa` žPkkœ¸¯¶nÉüáÐ̓70^Ð}éÁõè\ÝXÔû_k7}h:S˜}”ó©LrfBÒéÜ sNeA¯Òê\Pe±çF èt¬ðA‹|î á»ÿôk\-‹¾þó±óøê:yàH6*Uî94½@&ÀbÒ+xX(p¡×iવ¡Ö.nu`“AS]êëª0ŒÂ?*ËàÃx0ŠwÎ^•Ü~Kãö‚„ ¹ª¥â/¿üÝ‹xÏ÷žYÿWhªZ•w»H2ŒH O`uflÍÐ*<Ö¼RÔ±Ú½ù¿à+Gþ@T›¯ýú1üûŸ ,N 1 1"žžœõ÷bŽç´‘˜ÑdfU‘1 &²mk2àà“^©ôª:bty^GHÂãEÑꆪ²>‡N\KÂkJ¤>exzªò3-qj•+AŽ ‚° ÀŸ!÷- ‘Iã·£~ülà~3" ‘IWì}˜Œ¡×ïÑþ øÁé·ñ“ß¾‹®þ‚†jl&|ë+[‹v>YÑ·˜”¬îPÈ}(Ù÷´ÅF’ PÑhƒ ÐJ +¨T€Á Op-ÆÀà¾þ·¿,‹°Ã´®î~8’6y×56³ìû/¬¨×iÐèªÆêf—è°Ãíjíf¬nv¡ÑU ½®¼’¯Ÿ¼ 9h²ÚÑgÖ§`}5ë¬0ë,ÿxñð×'þo^=,©ýpÔ³þnŒÅFEO1¢Éˆì÷=Íÿ…üÆú­ø³û¾)jû¡D_{ë1ÅŽÝBWK¨òpq´W±þF’á9bÅRQ>ÙI9gôUΉsŠ÷Ë 1*´)GúöKj§d °6Ïê,réöùœ=ÿóõÅq×6“É„ºº:X,†òð-Ašv """"¢i <QÙáEë|£‘ŸÛƒ•ÀÝ 8üÃ{ïàÿ~ïüKÿôù}ˆ§RïÏš•n|ë?nC}]ñW®³šô%1-›©Œ}(%•ThL²@&*#*U®:ƒÑŒ¹k´¹•¾Õ³ÌP!÷{½!×FÍŠ´H]Ýýxæï!+¯Ä”,„Ó}ñ’¬Ôš…+-õ:ØÌFY÷;[àAΠÃíjífÜ»¼n‡ uéu408‚®J{cÖYðÜgö¼Ïn˲%ó¸yéÂð÷ïíF4–Ô>˜˜P¼ÃQyßGDÞ×Ý›ˆÕŽvQmº}'q¨ÿ "ã±Ðd)UJ<e+iã âis®+É~ÕY­cÚß \íÝnKãvŪdhÕÚ‚R¤\'šìsWµ‰&ÃðòÚŽÉd‚Óé„ÝngÐAœƒšAØÇ¡ """"¢[ÞSrˆˆˆˆˆ¨‚0`Ûí~À>pT6|ø08ŽO9\¸·ª¶lúîƒ70!‚70V”PÃlŒz¾ôàz¬mq—ÌXÕØÍ<ÙË@ZÁ‚+é ÖsŒË‘Z=÷DÛLfáI¸Dbuu÷ãùWŽ)º¦Æt¬múµkд¼Í ÏõAôžÿ¯¿ù¼×%mûTŸ›ÚWð@’¬t: Ÿÿ6 wU£ÿÚÇÈÈ2ÔhK³Mÿ_­F}]öÃûU«áªµ¡ÆfÆuÿ8Â%zZLE—oúû_ýûÄõ·p=¸´Y=Â)ì=õ,¾ý™½ŠMœVgvá’È‚DÃQ¿âýZÈóüÿáЧDµ9зO´>%{_ìúêùŸ»«V‰ÞæÅ‘^ U¹ñŸ-ú1¤ÒÃsâRR[?¯XŸj H‘RiÆ2Ïk„÷…S ¶7™L°X,Ðh¸Ø—Kv ‚pŽCADDDDD³aàˆˆˆˆˆ*Š ºÝîmÈ:8* '8!\ÇoFý%|ð‡C"!øÃAx'Æá„Jr;W7âá-ka2èJª_UVcIôC­Q¾Ò@¹®lŸÍæþ(…*Ã$7%ÃíkÚðÔW¾ˆG¾ðù›‡Ûm€/?ü¿ö`ÿûïøö_¼«ï²’y}¡Õóåk5j5VÖ;pÕ7º¨ÐƒZ«Ú3½Íº œÕ–‚V]Ðë4hipb,…o8ˆt‰½ÁÕ¡QImÿpåchsö-âKþo^}µ ûÜP¿eÎçs²P IDATß †¼ y Òop}âkؽ釒&ÌçKÊçᘠûÇÑdXT˜¦ÍÙÿsÓßãû§¿w)«µçC£Ö¢³ÎY©¢ÍÑ!áø+\á!Á t+ƒÆXð@[© yp¸ÿ%Ñíœ&—¢Ï“ U“‘Ûß Ñmšæ©hã§r^¯‡ÝngÐAÂË䂯q(ˆˆˆˆˆh> <QEá8€un·ûiä‚U•…•Bðaf¸ÁÁ/ùq[¹ÌG¶¬E}]éf&ƒ® 俣ª}(!«ð?>ûw8y½KÑ}l¨ß‚õÛ°±~+Ú°ªójwq¤GÏáŒïÎøN(‚ˆ&#Ø{úYEC œ‡£‚ìýHeÄWÜѾ]ž×Ñí;YôÇ–]_5gà¡Îì†YgA4uì½+Š÷T&…ñÉÔ,rµMÏ‹ %:Çù%ÛóV«”²CÂðÄï)û<¹P5¹]_, yžkÖÅ‘;+Fèÿöî=¬É;Ïÿ›œ Ä!"-°[­­´j»»žv§vªÎãìo«öð›n§Û“ÛvvŸy~–:;ûÛ™v¬Õ™mǶŽhgÇÖC«3ÕŠ×TÁâ¡¥UP±Œ¢ ˆ7K@BB‚Ï«„ûÎ}‡$¼_×ÅÕ ù~sçs8äû¾?:Ìf3t:¶ÕÉ `µ …, DDDDD×AØ`·Û?ð€—Y‘¹6øp÷wÒ•(ÿ€¶N/œ^Ô9/]÷ÿ±d̨Ìššƒìt[Ônc¢1zÞpU©"ðDD$ŠÜa«% Ï>ñ8žyâq$[-’çyö‰Ç±æ·ï¢®¾AÔ¸š†äKãŽ%Ùéô€¿è¾IW&µJ…[Òlèðúàpv Ý3° @€”áX n1EÍkV«TÈ9n¯õMmð‚ƒº=átwxòöŸDìÎßž€¯}¾§Z*™ÿo³À¬[æcVÖün”kËG®-ÿj¤ÊQÃû±þøÙÃJ‡2-âçT"ðà J ­›½÷ü>í~g¿•”©ØqkÑ'ãbÇ…›ÖYì1]ëªQ´»‡Ë×Öoàa„Ñ~¥£G¿3ë, ñÒ%‹µÎE·IJ·—xµ¾ò ÑcLZ3¦Ø Û¦áÔªÈ.O©’ð}x„1µÏ¯Õ¹¾9†tKB]ت‡ˆˆˆˆˆŒ"""""Š{WÞ<)´Ûíêö0ŸUwÀ½j1Ò”ˆ;RR1Ò$~P“»]˜6Ü(‚=ÔjUÔl‹J$$—/+3BBè9ˆˆhàVž“5ìðÌÊ—–‡t¸Öü¹³±fÝ»ÜQø`?ëþÍF=ÌF=‚ÝÝput¢Ó@§/ÐÇã´H¶ê QGï2‰F=Æg¥¢©µÍ—:Œpû(k¢³Û÷UJ?Ù^€\[~D¶ÕpceÙòëEÊadb~pÛ#X˜³éIY²owOâѼgq¸±ïT¾Ž?×þIƺ(zaJ=¦ÙÓ${ ¥,úd¬›³?Üù×ý>VÉ.%ýÝy=×–/:ðPå8†³ÛæÖΖ~§¦Ô>#Œv5Ñ2‹%J\‡Â¡W"j‹v[«‹ êí{¥’5á íµ’|}]Szº;0è–„‚ÇX """""‹"""""2A¨ð ÝnŸ‰Pð!ŸU˜‹7þäq÷|èìêB“Û…Î`šÜíWC m¾N8;½qSƒN‹‰ÙvÌššƒa–ØYa6F×°j ÐPnn""¸š^•)ì7!¿ûõ*äOœ ë6fŽN=¦òt#;/-Wd;'É  ’ígm(lÁ.ñ?·¨µ¡Ÿ]bñ{´Z¥º|¸äò¢Õå×'-ÑjÔk¡ÓªaÔkaÐ}óÿ½‘ÚÝazÆ,I â¥xóè/e ;hTÌ÷X˜³zµ!ba‡kÝ–’‡]‹¾DC{-^Ø÷(Ž4–†=gOèaíßüwXw¯sžAG U-¡›D[õÃàô‰ë$Øìd=6|]¾°Æ÷,6^Y¶ü[wgä»Ï(vèaÑYû «RjUè¿ñFT«T°%›aK6ÃÂíõ¡Óè5ü V«`Ôk¡V©`Ðk Óh ÓŠû¨øHµèm4iÍX:ñ©ˆÔãÍ£¿@¹P&Ë\)Æï °`5l¦T€YÁÅ⑞”…Í|Šâs;°òàrI‹i¯å t`eÙrüçÌuz|ó ªZ*Pë<ƒ:gl¡’fO“¼‡0:<ôX˜³ s–âpcÉ5õÏDzRVDöµEŸŒ‹ú8Ÿ‘iÉ]ÿª– L¶(¶Í­-ý4*-î°ß ·ß…@wI: 4*ÞÐ ^hTEC5±d}å’ÆÍË^ èv „×^G*×6é›k‹¯Ÿ4lÃçŽRXÒ!ÔÕ¡¥ """""Y~ÿg ˆˆˆˆˆh(»ò¦K¡Ýnß ÀRVeàt:t:ü~ÌÒld§Û0y|ÒFÄOöE…+ÝÕêлºä™O£ ÍëT @÷e…æfàˆ®±}_%VÖ†5GÞ„\|ºc ’­E·µî|ƒøm—ÆL•êö ¾æ—în„’ Cãû°N«Æp­rAáâ#Õ’:ÒL¶DdìÖê"”ÖË𳻟ý,ÿøuŸ7iÍQ±ŸgÝ2w¥ÍÀ ûÅÞÚá]ß]5Øxâ7XÒG ¥\(Ã?C•£o“"¯§ªårmù²Í'Gà¡Ç]i3e[tÉ7ýz®-_|àÁqLÑÀƒ/Ø9à.‰: (þ ÆbúhÔÐ^+éÚl3¦*zŽ¡‘®…”p^nJ>ƒá+°L„Z–‚ˆˆˆˆˆäÄÀ€+oÂ,»&ø0ƒU¸X >LcGvº ÇØc¾“Co¢­»Ãµ´ºÐÀ`˜¡µ&4W%/ܹ²×EÛý-B$‹>ëælÇÖê"¼¸ï±°æÚ}v;rm“®.´möØ}vJê÷ÀèPüµœhþJöcd  ï£öw•fm":î^¿Þ_'…ÞT9*ßn—¿wøÂ"½˜>Z­._)í{åmKß?jUdÿ(Q|n‡è1£­·à£óï1è ]BA‡ý,)ow]ãÊ›23ívûƒVÈdU.Úƒ=]z‚q¿?´ê(?^€  øCá1 :ÄÓB~µè ˆ¯Å@æMHàõ‰(VÕ48Ps¡M-í¨8݈¦Öv4µ¶Kš«ÃëÇ×uáÝ%{ÉâEX¿vUD^{m}*OV‰—λüÅ·×'©+ÍôŒYa²+ºmuÎ3xóè/žgúìtæ(\Ƚ0g)rS&añÎûÑîwJžçÍ£¿ÀS·ÿoü©æœj©ˆèk8ÛöÙçìêîŠùs΢³öxÈMßClG)š=M°›ÓyÁŒ°ñ)y8ÕR9àÇ+qŽkT cà._›¤Eþ&­Sîî0”ñz×¼jµf³>ƒ‡aiœžaKADDDDDJbàˆˆˆˆˆ¨‚ |à#»Ý^à9VVeà¢%øÐp3ʆìô”¨îx „D£>ê·Q­TêÐBÿ`Wÿ‹ý®ŒÑhãoB Ö†j!'íÐ:ì‰bšÛëCMC *O7¢ât#*Ï4FÕöͼgZİæ­wDIžßÿˆhऄ`FÆE·ËpãÍ£¿ »Á?ÝþfdÌîóëzµ!âwƨ\[>6?ð)^Ø÷ˆ¨EÏ××±¯~þÓAÙþ@·ç]g1Ú2F¶9]þ6XôÉ1}Î 3Øp±ãB¯_a²ÃfL…Ã+.@Yå¨@®-_±mî¸á vB¯6ð¢AÑp¬30è 믑>›‘1[Ñî(ƒHqùÚp¤±ÿàBOÐÁh4ò’ƉÐÍ‚V ‚ÐÆr‘Òx """""º A ívûj…žeEĉtðąd§Û=*i#¬C.àp#‹96{$$„º5hu@0tîîë£R…‚ju|ï3­èîº/Ë7»;E·ŠÓ¨<݈²Ês8{¡%ª·uÿg‘’‹ùóæàÙ'CþÄ Š=W›Ó…¢Íˆ—7.Qœ9XyNô›1UÑÅÕ°µº(¬;×›´f,™øÔMÃ`ÑGwö¾'ô°xçý’CƒéKá ¬‡` K2"m„õºpÃPïÞÐ@bWB•ЃïÛ.J£ uw ¢èàöúP|¸•g%-ÜV•'«ðØÓ˱ò—«°â¥å²jëðÆoß‘4vZ^8¢8RyºQÒ¸ÉöŶÉpãÍ£¿kŽ™ò³…À¤IŒ‰}eÑ'cÝìíX¼ó~´û1sŒý§IÞã£ËçžEoí;ð"¾Sé– Å·¹µÓ`wÔªèzûÛíwá˦Cèê\ýœAmDþw¦ Qgá…> à ¶¨Û߃a}å’ÆÍȘ£èv™µ‰0E8´×Ð^‹½µ;¯ûœN§ƒÙl†N§ãI#]€e‚ ìg)ˆˆˆˆˆh°ð/DDDDDD"]ysg¦ÝnÀj™¬Š87 >¤Ù,f11܆Q¬YÌKHô ØÀåv{P©BA•š5$l×vrˆ§Coêêd >,ÿéË’º;LËËB¢Qσ(Ž”I¸~fZ²½£ôÖê"8¼ÒÉÿÓí/!×6ðã}rÌì¯\[>ÖÍÙŽîüë˜ÙæsοÈ:_WwW\œ{]2Bë[{9ǬcaÒšáÙÍ¢ÊQ!êØ—Âåoð(»ãÅÿ|q]Ø:ƒ^T·žÄö»y¡Ã0CʯÁáÆœj©=n|J¾âç£Ý<*âõ¸¶»ƒòüª P„ , 6ˆˆˆˆˆˆ$á#ÙíöçêøÀ[ê‹Ô|H6ëpÇ­vÜ~ëH%L#’Ͱ%›Yˆ8¡Ö„>‚A ;êøp¹èÉ?$$„>Tj@­ˆhp¬<‡²ÊZì=R=ä^{Oð¡è`ý¯_GVFºè9vìÚƒ»‹%=ÿ÷gæñ$Š3R:<Ì=[¹ëœó vŸÝ.yü’‰?ÆŒŒoŸY›sûì®´xvò ¼Q¾2f¶Yî…ø._[LUzÓßöç¦LB¹P&®Î-Ç_`ÝÚ鈪ÀÃ¥Ît½½~­Íׂ®î4*Þ¬@©ãt(X]þФqJ~¯Jƒáƒp.n­.bÐAN„nò³Z„6–ƒˆˆˆˆˆ¢ß'""""" “ «dx@;+"^[‡>zëþt 'Î5³ ¨U*d¤&#ms7q¹Õ€Vêú`0Æ+cèsZ-ÃDƒ©©µ›v•ãá—·÷ ɰõJÆØ¿º+¹JÜÏN}úyIÏ™:< ùãÒx0ÅÙµµ©Uü¯W“íŠmSщÿ’ŒK½V=d^¿Q¯…Z•µZ£^ ­F £^ £^˃ƒˆ(ÂÜ^>ÜwÛ÷W¢ÃëgADpºÚñ×þï®]…¥‹}ëë'Nbå/WIžÿ…¼E&ŠC5 -¢ÇäÚ&)²-%õ{àð6I»0g)2­c%µè“cjŸ5´×bñÎûÑîwÊ>·Í˜Š)# 0Ù~rm7ï. e!þB™l‹€=]î¸8û;þrS&¡\(5g•ã˜â‡K-HOÊâE4ŽéÕ†!ßÅcýñ5’ÆÍȘ­xíìæQŠÎÏ ƒlê,cЈˆˆˆˆbDDDDDD ºò¦ÑL»Ýþ €Õ2Yñz‚ûŽÖáŽ[í¸#gdÌ:€N«îÊk0è´P«ô„رˆ(Z4µ¶cû¾J©fÐ!L=½%e‡°~íõá†Gþy9œ.iÝ2¦åe!\‹K‡*Ï4Š“›’¯È¶lýºHÒ¸ñ)ù˜;f¤±æ[ÌëòµáGŸ<$kØÁ¤5c²½óÆ,™l/@i½¸®Aåå 0ø /ÄÁ“ðå_„¨ >¨U*õ¡_¯{:1€V£¾h`ˆˆ(ö4µ¶cã®rì=RÍbÈhãæ-p5ô°ü§…¨ìy¯ /W‚ jÕÕ¯z‚ ªo‚ DD_tøFR¢íîÙçí =,]¼kÖ½+yž‡çNFêð$´Dq¨æB‹è1¹6eº;ìªÙ&iÜ‚œ%a-,5iÍ1³¿V–-Ç‘ÆRYæú›¬ïá‡ãkAü{Þ9Æè@¹P†Évy‚t7†ÅÁ¢l‹®ï;µgZǤ5Ã÷³Â‚|Ý4úâò·ÅDàä?.‡‚Õå+%›‘1GÑíÒ« ²w :ÈʉP×áÕ‚ ´±DDDDD«x """""а+o.ÚíöÕ`ð!,¾@'k8YëÀ¤±©˜9i4’CwÑÔi5W;,ô¸¶ëBD£ž…$""¸½>lÚUŽ÷Ûט7!Y£Ó±swÿw½^ñâóXñÒrìØµ;vï¹RËÆÍ[°yÛGÒ_ËØ4«E¾Õ›þ@@Ò8³Q‡¾/Q«¹à=&7e’ìÛ±û¬´î s–†Õ¡Ô Ñ¿ ×åkà û {žñ)y(ú»Ý²„zL)>¸P.”Éöüž.wÜœ7»›~®MüyWå¨P|›}ÁNxñ³¢•Ø…÷ãSÂ_p?ܘ2¤k¾þø’ÆÍË^ èviT £«ÍEO=~öåsøþž»ð~Í; ;„¯À-‚ ,c؈ˆˆˆˆâ;< ²+o<-³Ûí… ,eU¤;Py*Ïã¶Lš>ã3m, õªøH5ÞÜV†¯?î^[fF:V¼´óçÎF²Õ();„Òƒ‡û»â¥åWÇôH¶Z°â¥åxæ‰Ç±æ·ïàß¾§kp"=¹ ©Ã“xÅ1·Güu9Ëš-ûv”Ôï=ÆfLÅÜ1á/,µè“£~?½°ïQ´û¥ßIߤ5cÉħ¹Ûÿ{¶Uo5Æè@¹P&K—‡®î.ø‚Ы 1>ZôV´vöBÊ•°€ÝámBó 2­cÝn—¿-ìàEŸa†¡û7ž*GŽ4–Jú¾$W÷š¾ØÍ£$uÞøÊq×½ÏoáÁ-Ó.utØÏRQ¼a‡"""""¢(!B­ Ë܇ÐT†¯ëøMðóMp â< BDDWUœnÄÃ/ÿ¯½·/î™éxwí*Ô|uK/º.¸ðÊ/W hü³Wº;ô¦'øPóÕa<0wVÄ_ß´¼,ÌššÃƒ˜(ÎUži=Fî…ÍåB<ÑãfŒž=$öQñ¹Ø[»Sòx“ÖŒÓV)v€LëXØŒ©¢Ç}qñ3Ù¶!^: ܬÃÓ]R«Z”ïòàò9AñE¯6ÄEˆH*©ÝÞ¦ì}U4* FšÓQ3Ð IDATEùÊqOXˆXȰƒ qM­í(|û¼¸f'šZÛãêµÝt¸‘˜î‘lµ`ûÆw±­èX-‘é¶:< /üã}<‰è[ÆK¸Ë|¤,|7i͘'Gw]twwpùÚ°òàrÉã{ÂJßáÊHñw4/­/–-¨àòµÅÅùeÒ&Bs“;·K©s•CùÀC_])(v 7¤ Ù×ÞÐ^+ºkMÏõvJuwøøüxøÓ¿Å,ÄQÇ!Ôá«ð}ˆˆˆˆˆh(`àˆˆˆˆˆ(JÝ|¨cEÂãpz°î_â‰Wÿ„í¥§àé °(DDCȦ]åxøåßã`emÔm[¢Ù ½N'i¬Õ’„/>/÷íé5èÐc Ýn6GoæÏ›š¯#oB®âuzáïC¢Qσ™(ÎÕ4DÇ"år¡Lô˜ÉöÙ;MD£õÇ×àB»´_Q#v ¹{Äö}o:$t‰V7 áäÚ&Eäü’"^B'tå8Ô'Ù×¾UBØæŽY è÷%½Ú0 îŸÿŸ}ùW[ß kw‡%[-øjÿ,–ãṓ‘?.4ÑàöŠ¿æÚäíðP娀GÂbu9º;€YkŽÚýãòµa}å’Ç?yûO"v€LëXØŒ©¢Çí®Ù&O­üñ³ØÞ¢·ö}þIì°‰ÐC<íƒhÓÐ^ùãP74á\w¥¿*=)³ÏîíÞ9õ+üÍŸnÃϾ|‚§'Nø® :l`9ˆˆˆˆˆh(ѰDDDDDD±áÊYìvû2…2Yéz‚–~{óFã¡éãaK6±0DDqÂíõáµ÷öEeG«% O,{ç±y»ørff¤cýÚU˜Qp÷€¿R¡î7Z¿6ô<7o‘µ^cF¥àáy“yPQÄTµ=ÆfL•m!_‹G£ÁúãkÐîwJ» g &Û "º½SF`÷Ùí¢ÆÔ¹jÐì0Âdûù]¾¶¸¸+ýÍš›´‰Ÿ’S-âÎ3Ç1Å—Ï $ñ𦄠]^²¬ÙaƒÑ|mTRqíI×Ýé³d¹ŽõE¯6ô:ÿEO=Þ9õ+”\üî@;Oy8¬°Z&¹ˆˆˆˆˆhHb‡"""""¢#†+žGè / ÓÊóxþ×{ðóMpªÎÁ‚Ÿƒ•çðð˿ʰÒŋ°ã¿‹ðÉŸ÷K ;<ó£Çðå¾=;´9]رë“~'µ»ÃÖ¯]…_ýû˲ÕËlÔá•ÍáAM4„Ô4ˆÿyܬM”uª¢ÇLY÷û&œ»ŒOÉÇœ¥ßæ¹»nì>+O—O—;.ö½I›ÍM›K9þ¿¸Èà ¶!u}7…q­nL²ßW—¯”4N®®C}IOºþ>,qžÄϾ|ßßs>>¿…ay8¼ K„B†ˆˆˆˆˆh(c‡"""""¢%Âj»Ý¾ÀsW>¬¬Jx¾®sà?6ÀèT+æÜ9÷æfQˆˆbH4wu˜>í.¬ÿõëp:¸þ"8]âeŒJÆ߬pСGÑæú}.«% óçΖíµ>ûÄã¨8Q%K§‡' u8oÍL4”txý¢ÇdZÆÊº bïT32fÇý¾‘ÚÝÁ¤5ãÉÛ_”ma²#Ó’:W¨q%õ{°0giX ´P‡»9=.ö¿E—ŒÖÎÞI¹)ù¢çsx›dë¤qó}0x]6Œv‘”óøŠ7–à‚„nãSòeë:Ô×þè9w¿rÂÛ§~…£ŽCÈà|“ëþø%ž_»Ÿ|~žÎ‹BD墵«CfF:þüÑøtÇ””Â÷ÍvøÞœ¿ÅÑ’½¢Ã°æ·ïöû˜gŸxÉV‹¬¯{ýÚUX²xQXsŒ´Y0kjn"Š()ÝLZ³¬ KõjCTÖfku‘¤qsÇ,P|QûÍÌ->Œâ tà !üÑÞa@Œa†¾ï°Ÿi ›1UôœåB|wyHÒYûùº%f‡†öÈýÌ­QiÂŪÕå¯H7/[ùîŸÿXȰƒ¼ŠÀŽDDDDDDßÂÀQ`ðA§¿/>Žç×îÁöÒSp´yX¢!êòe ;»€@ ôÑ }Ðàr{}(|û¾½GÒ]Á•bµ$áWÿþ2j¾:„wcùO ñØÓËEÍ‘h6ãíÕ¯âÃMë%JÊ¡®¾¡ßÇ-ùá©Áúµ«ðÀÜY’Ç_t¸PÓààANDUç:#zLnÊ$Y·!ÅçvHºË¸Í˜Š…9KuÛ¥vߨúuQØÏÝÕÝOÀçF]rmâ»<|q1ßàýy$QgAmìõkÉúhTÚ˜=$\²$Ãn¶‰g íµ8ÒX*éº;Ù^ È6yƒ”6}‚%ûgãg_>ÁÓ’M€[AXÆ Ñ·1ð@DDDDDGn>¼ÁŠÈÃã àÃÒ¯ñü¯÷`ÝÎ/| "º»¿èô†>|¾Ð¿»¡Ÿ/ôáõ~_( A‘UÓàÀ“ÿ¹5êº:<0wj¾:ŒgŸxðèÓ˱fÝ»¢æÈ›’?mÃ#ÿÏbÉÛQ´yK¿Y²x²2Ò«Åúµ¯#cTšäñon;Ȉ"ªÙ#ˆ#e¡w¬)®Ý!iÜÂÛ” ;˜µ‰HOʼú1Ü`ëõq&m"¦gˆà9¼M’:~Ü(^º<èÕ†›†q¦Œ¼Gôœ§Z*„ vý3,·ôúùÑ}|>ž™4Òº4XtÉCòûÑêò•QsÝmõ5ã¿kÞÂÊ£Ï`{íFäumС–å """""ê†% """""Š?WîöœÝn_  ÀRVE*Ïã@åyÜ–iÃCÓÇc|¦E!Š3—/‡ ÝÝ †>€V¨ÕC³v^_Ák ×ÓqÁ ÓB­NõZ¨Uá߇eÓ®rlÚ]®ÈëP«U»EËÌHÇúµ«0£àn@›Ó…ûç/BåÉ*QóüõŒ{ðþ»¿•ÔÕ¡G›Ó…<,]¼HÑc"ÙjÁ¦·Öà¾áòåË¢ÇWžiDñ‘jÌššÃ‹ED­³Füõß26®kâòµa[õFÑãlÆTÉÝnF£ÒàÖazí8à v¢ÎYƒÖÎë;ÍȘƒÒúbÑϵµº+l«Â¬Ÿvsz\ ½ÍžÎ^¿–›"-øó…P¦Èqrã1Ü_‡ ¥Œ¶ŒA³§ m¾–«ŸiÎÀ“=æ¯ ‘;î†^àÁåkCñ9ñA3“ÖŒ)2vw8ãªÂçÍ¥øÜQ ’]€B†ˆˆˆˆˆˆ†"""""¢8våM³ev»½ >Èêë:þcÓج&<4}<îÍÍ¢Å`ø€ËÇ÷„%Ô@§‹ÓuwÃë  Ãë‡?„?Я¯ëº CÔ*M:$õ°˜ Ðižq{}(\·•gy}z½>ŸOô¸g~ôV¼´üjHAjØ¡ð'/àÿ¼ðlدcÇî=ý>&3#ýj8CI÷Ü5O=¾ ¿~ûw’Æ¿¹­ Óò²hÔó"EDŠ«s=&Þ;²Sáð6‰z¾S-¨sžA¦Uz¨%^:<¡;í7{z¯¡I›ˆÉö” e¢æ¬rS>ðàoÔEówØïÆEw=¼]5&ŒL̈ùc¡ªå˜è1#L©¢Ç˜µ‰7í,¯¶V¡Ýï=nî˜}^Å…JpÆuŠ?ÈA"""""" x """""|PŽÃéÁº?~‰÷Š+1{j6æÜ9&ƒ–…!ŠAÁ.Àï—q.ÄGè¡'ÜàöúàõàïOw7œîN8ݸÐì„N«†ÕlÀ0‹ F}ß×ЊÓ(|û“«#ä”1* õE‡nìê'Nâþù‹àtµx³É„ ÿµßÿ»¹²¼ž¢?|ÐïcžyⱈG«ÿc%ÊŽ|£•'Díðúñá¾ãxxÞd^¨ˆzáöúPÓÐÒç×í)IHžÄB 'Ð!êñ6cªìÛ0Ø‹³ot¸±Dô“Ö¬È"ö[‡MÐbÞž»ç_zXxÛR¼uô—¢Ÿs×ÙmxòöŸHÞæ®î.xnY!¶þŽË\[¾èÀƒØÇK:§|N`/ƒñr—”®uHÖjýñ5’Æ…sÝõ=ø¼¹%Ân´úü@~ :…"""""¢!„Áåx||Xú5>,ý÷æÆCÓÇÖlbaˆbDwP¾°CX =øA¸::áöúàöøEunç9›Û:ÐÜÖVɉf1B­R]}̦]娴»\öç5Òƒ^šÚ:ÑcoìêH ;Œ½% ï¬ùî¹ëNY^Sm}Jî÷qKÿ ¢ÇÖÞíïã–Iw¢ÝÝ!zìöý•øþ}ße—"5 ¬¬EÅéFÔ\p 86fT ì)IÈeCvz òÆ¥ñœºAS|w) xcMñ9ñ”;XtÉ¢‚ #Lv¸ümW;L±`£Ö,:ÔRZ_Œ…9KÃÚ׭ޏ<èÕèÕø‚½~}²½Oü—¸ß¥(Ê0Ù^ ØvÇS—háòE¦¦ÑþŠ”Ã%¸Ð.þw“é³$]§Z}Í(vãHs):ƒÜò+A(è°Ÿ¥ """""’Ž"""""¢!ˆÁe¨<•çq[¦ sî‹;rF²(DQìòeÀïSfî`Tjut×ÀéîD‡×gG§,Âáq¡Ù‰ ÍN ·˜hÔãç¿Û‹Ê3²?×’Å‹°c×'¸pQ5ÎjIÂöï^×ÕŠ6oÁòŸ¾,:ìðç`ÔHù¾WìØõI¿y`î¬ë‚‘lµ`ÃoÞÀ‚¥‹Ë.4Ô¹½>¬¬Å¦]åhjm—4ÇÙ -8{¡+k¯~.uxòÆ¥!\¦åe ùDG@| +Ëš×5©rT Ýï=N‰ÀÃÈÄQâ÷e,.u¶ «» &m"fdÌÆî³ÛEϳµº(¬.ÑÐa@.½ÍžÞ#LvdZ²Qçª5ç?S4ðÚmCrñ¼RNµTŠz¼Ôn8ÝÐÛgë+ß4nFÆQ?ãªB‰°Ç/}ÉZ :Ɉ"""""¢!ŒÁe}]çÀ×uج&<4}<îÈ “AËÂE™®pYÁù~@mŒ®×ìî†ÓÝêä¡.Rœ¨¹ˆ K.yï4:}Ú]¸wÚ]øùk«%ݾñÝo…Š6oÁcO/5ל¿¾›ß}‰f³¬¯¯è[ú}ÌüyseŸÎŸ7Ì…»‹Ee—ªŠT‡t¸™¦Övì=R½Gª„º@äÝ‚iyYÈN· ¹Z{ºÜ¢ÇÄÃ]ûoæpã~ÑclÆTdZÇʺ•à âIµJ»y®Ü-}ávypùÛìî‚ZûoÍZtÉW»fô&×–/:ðP娈ÈùÍÀÃà‘rîXtÉqqΈÑÐ^‹½µ;EË´d#×–? Ç~Þ\Ša7.xêx`*ƒA""""""0ð@DDDDDD >(Ìáô`Ý¿„©X‹{óGcÎcaK6±0DQàòe «Kùçvê(øKœÓ݉Kí8ÝQ¿oÊOÕãý½GeÓjIŠ—–£âD•¤°Ã¯þýe<ûÄ·»H ;,û‡ÿ…ÕÿñŠìa‡ÚúTž¬ê·K/´}»ê篠¤ì¨N»¯Á_Ý:s¦ŽÅøL C4ˆ‚]‘yž®ÀàÜ^.¹¼pº;£¶“ÃÞß{å§êesú´»°úÿÿ–ýøÙ~7²Z’ðéŽ-ÈŸ8á[_“vøÿ^|?yö)ôòw*رë“~3XÝzde¤ãÙ'ÇÊW_=–]†žššZÛQÓСµM-íp{}8{¡EÒ|©Ã“:< ?. ©)IÈ•u úkxaÍNtxýƒ¶ M­íøpÿq|¸ÿø ? Ô£ü_0z‚‰U-â6çÚ&ɾ&­ô€ Z¥Á0CÊÕ® s–J <”ÖcFÆœßEýF­Ž¸<èÕèÕ†>ÓLëXØŒ©px›DÍ[R¿K” <øÛxÁÄZfY³E 'è«ÖW¾!éú8#cv¯_»à©CÉÅÝøÜQÊW9 :EDDDDDDô- >(ï«¿\ÄW¹ˆÑ©V̹s,îÍÍ¢ ‚Hº/‡:=$$Dæùü .µ{ÐêòÀÆÌþðúxk[.Ùæìéê0³ànÜ÷½‡Dw˜>í.lßø.’­–o}íѧ—cãæ-žËl6áå—ã™'ƒF£ÌŸfwìÚÓïcæÏ=èûú™'Ç¿}GR—‡ƒ•µ˜55‡°8ÔÔÚŽŠÓ¨ip ât£äPCÏÑÔ:î®íœ`6ê=ʆüqiÈ—†üqiƒV‡h;ôV·H…ÌFè1U-Ç$/BF¾._Tl‡Ë׆ íu¢Çå¦È¿/Â]ø<Ü`»xa²c²½åB™èy¶Va…m•Äz:ãæµè­höôÌɵå‹•|q± K&>¥Ø6wuwÁì„^mà7\® b™´‰޳¡xØZ]„v¿øëDoa‡Ï›Kñ¹£g\§xÀ*‡A"""""¢bàˆˆˆˆˆˆúÄàƒòÎ79±î_â½âJÌžšéy™°%›X˜8vù2p¹^¹Ñ~÷•µè @‚ H R*5k Ý—#ø\Aå»<8ݸÔîÓÝsû¢±Ù‰ ú—Ú½²Í™7!¿ûõ*ÔžoÀýó‰^\¿âÅç±â¥Þ»7H ;¼õ«_`Ñü¿W,ìÐæt¡ôàá›>ÆjIÂüyƒxH¶Z°ê篈v•3ð'Ü^*O7¢¬²•§¯C‡×Ê3¡Äî+×±i@DcØáF½…º/ïjçŒp±ƒDôÒÝ!Ó’-ias4ªð¾w3Ø QiÐÕJ»Î³@RàáTKª’6¾`'<·"õ‰4‹.ùj€¤7SFÞ#:ðàð6¡Îy™Jvyðµa„ÉΓ{® f‘ÇýPìî°µºHÒ¸¹c¼AÏ•n%hõ9x *§ÀjAޱDDDDDD‘ÃÀõ‹Áåy||Xú5>,ý÷æÆ½ù™ŸÉÅ^ñ$؃¡¾ôÍÿ&Pi&€ ùuG¸ñA÷e@‰K°»Ž¶Ž˜ëæp­5Þß{þ€ls>ó£Ç°â¥å(Úüþåÿ¼"j¬Õ’„õk_ï3 6ìh6áM…ðc÷º;Ì›5û}éâEXùËU¨«o5®§ À`ÞŸ¤s{}8XY‹ƒ•çp°²6ª·õÆÄ´¼,äMCAþ-²-6Ñvèí|ì ?Œ•‚YSs«O$T9*x’^£¡]ü9šiÍVd[ä XtÉhí -εåc|J>NIX¸ýæÑ_`íßþ·¤mhöŠ.è”þî¼?Ù^“Ö O CÔ¼%õ{°DÁúxnžØƒ$Ó2Vä1fr×Û#¥¢ÇM¶Àþ»æ-|î(妬"„::Ô²DDDDDD‘ÇÀ ƒ‘q ò<TžÇèT+æÜ9w䌄ɠeabTwðûCĸŒ+!‰.@­´ºP’ÏåA8 ã©ìÑÔÚŽV—'¦÷Cù©z¼¿÷¨ló]VL¾é ‘?qB¯_;ç¸1·àÍ_ý÷Ü5EѰ””ê÷13 ý¿â¥å’º<©fà!ÆTœnDñ‘jì=R³¯!Ô¨Å[Û^íl?. Óòn‘eþ×ÞÛSa‡½Ð‚·¶Ä[Ûb̨äKì©9¢;6$u¢Ÿ»ÊQ Rã)wú íu¢ÇDóÝó‡R®`aÎRüì øïAovŸÝvõ®êb´v¶ÄEàA¯6@¯6À컫Ød{è._\,Ã’‰O)¶Ý.¿“ßePåPþÆöC­ÃÃúÊ5¢ÇFøõ¼züßxP*‹A"""""¢(ÀÀ‰ÖKðáAVVF^盜X÷Ç/a*ÖâÞüјsçXØ’M,L ø®®ðç n/ Õ‡Â$îîØÜn·×‡¦–v¸cxQl÷÷Eù©zÙæË›‹í›ÞE²Å‚‡–<†»Å-²{`î,¬_û:’­–^¿.6ìpköü×kÿ‘°ìØõI¿™?wvTóçÎÆrKœ®vQãVžƒÛ; ‰F=/fQ~½*>\÷GSk{\½¶k;˜:LË»åJø!KÒq /Ä8{¡g/´|«>ùãÒúíþ 6 \þ¶èØŸøíÈM™$ûvhTò|½±+A®-“í(ÊDϵµº32f‹î<á vÂì„^mˆùãÔ¢·¢ÙÓwàaÊÈ{DÞ&Ô9Ï( é`‡‡A»F‰ ‡iTš~»ˆÄ›âÚz\BBÌf3 Ôj5š:y@*‡A"""""¢(ÂÀIvMð!ÀsW>|™ÇÀžÏk°çóüÕ­#1gêXŒÏäB°hç÷‡º3Èå2¿Ðé5ÿ¢3$9Ýp´¹ã"èàõ°³ô„¬a‡g~ôVý¼mN•'«D_ñâóXñRßwyvÈ›U?/Ä]Sþ*"a‡Š'û LŸvWŸaŽÁ’lµàÙ'ÇÊW_5®ÃëÇÁÊZÌššÃ‹CjjmÇö}•(>RÓ Ä{¯é^1-/ ³¦æˆêüðæ¶ƒC¦>©Ã“7. Ù£RnCvzJØá¥:מx ¨j9ÛaÒ$Ê2Oo] –Lü±¤Àƒ'Т¿Á“·ÿDüïÑèò`Ö&¢M}~}²½&­ž@‡¨yKê÷`‰‚õqùÚ†Übz¹Iéþ"ÆPëîP|n.ôSSNƒÁ£ÑÈPy :E!¾=NDDDDDDa¡ @¡Ýn_ õÕ_.â«¿\„ÍjÂì©Ù˜ž— “AËÂD¹Ã7έK`§9¨bc;[]4µ¶ÃÆEݽ¾ÞÚV†F‡K–ù¬–$¬_û:æÏ›Š'ñÐ’ÇQWß jüªŸ¿‚¥‹õù˜å?-vŸ3¯­\‚©S`ÐG¦Áþ²Cý>fþ¼ÙQyL,ùáD€Ðñxˆ.M­íظ«üêÂö¡ê`e-VÖ^ílðÐÌïÞ´kAñ‘ê¸ë€Ñßq²÷H5öÞðù¼±i€D“Ã’Œ¸ÔîðœbT“rÄÜÅ}0 7¤àbÇ…«ÿa²czÆ,Ñ ´¾32æˆ~Í­-qxÈ¢ôÉöѵýâb–L|J±íöt¹xÓ‘›1Uܱ¥ZNÚZ]Ôëç`0`2™" &ˆˆˆˆˆˆ¢3&""""""Ù0ø9§¿/>ŽK¾Æ9#1{êXd¦²ÔÑ Ø¥\Ø¡GÀ¨Œ@BëŽH×O¥÷x§»gÜùÃyrñ»_¯BþÄ ¨8q÷Ï_Ôo—ƒkY-IøtÇäOœÐçcŠ6oÁšuïxÎ|ÿ<õØ2äOÌE¢Ù±ÚîØµ§ßÇÌ,¸;*‹¬Œt,Y¼HT¨-*w{}aßžÂÇ Cï®íl0fT º/¯×Nñáðë65m:îJ›ù­Ï7´×¢¡½GK£¾^•gÃ_娔ÅöJ=o°» j߯“›EŸ|]àæ,E¹P&)8³ñÄoðŸ3׉ã vÂpäMŒéZš´‰Ð¨4èêîû—Ÿ)#ïxpx›Pç<£X(¤#àæ‰—¯Mô˜&»¸ótuxpùÚ°·vçuŸS«Õ0™L0Hà/ýJsX `õ•¿iQ”â_J‰ˆˆˆˆˆHv7–!|Èdeäçñp ò<TžÇm™6LÏËĽù£Y˜Arù2ðGàyø}€ÞÀš‡#AáçàZ·×‡¦–v¸½þ¸ªwc³ïï=*[ØafÁÝØZô’­ìص>ý¼¨°CÞ„\|ºc ’ Ku IDAT­–>S´y {zù€çüá‚ñÏ?‚ ãs"v€Òƒ‡oúu«%é¦ÁŽÁ6îlÑ z`—‡Áãöúðá¾ãؾ¿qvÍ’ÛÙ -xí½}Ø´«³¦æàû÷}‰F=šZÛ%/ô7iÍx¹`5æÜòýÝ­¼¡½‡KPå¨ÀáÆý8ÕR_?w…¿ˆ9ËšS-Qñz:¼ ½†¾Ýme„ÉŽ¹c`[õFÑóÕ¹j°µº s–Š×ìâ¦ËCk§£Ï¯O¶À¤5‹“”ÔïÁ…êãò9y"„¡JÂ5Ò,"Ü£Qib> $ƵÝôz=L&t:4å1è@DDDDDcx """"""Å\yÓp5€Õv»}€B0ø ˜¯ëøºÎ÷Š+qoþh̹s,lÉ&&‚P!º»î  R³îR%$„>.Gh§õ·¯ü ê›.Å]Ð…ÞÜvþ€,óýø±¥XþÔ?!ÙjJ€æÎÂúµ¯ËvøÇ,À“,AæèŒˆ‡JÊõû˜ùóæDõ12Þldf¤£®¾AÔ¸ƒ•çx$ÅGªñæ¶2DjjmǦÝ娾¿ÍÌCjJ’¤y2-Ùø—;Wâ{cÿ׀Ǥ'eaaNpå”é @ŸÛÃ%h÷Çö"à/.~†Éö‚°æJ‹lc‰^#o'ŸáÛ·éϳ%ç÷Àám=ß¶ê˜b/`hö6ÅGàAo½ià…Ävyøâb–L|J‘mö;ÙA%Â2­Ù¿k†ÖuxKõ$&&Â`0@­æ/÷À QŒR±DDDDDD ‚ l! À#êXåx|ìù¼Ïÿz^ÿà0¾¬¾È¢DÀåË@°+²Ï°îáŠT`$!Põñ—¸`w7ê›Úpª¶‰a‡~˜M&¬ü·±à{Ûða’ÂK/ÂöïÞ4ìPqâ$–ÿôeQs>ýÿ>ŠÌѰgDÄk<ÀÃŒ‚»£þX™?w¶è1+ky!‹°š^xc'^{oÃaèðú±iw9~½å3ÑcMZ3ž¼ý%Œ0ÙÃÚ†Pb)ÖÍÙŽÊG[ðÛÙÛ° g ’tÖ˜¬éIÇ1XqJ¯–·­™Eoíå¼JÄÂÛ–JžóÍ£¿õø®î.\ê'( ,ºþ»LyèyÞ&” eÊ]ƒnžXn,Qö˜Ò[‡D¿r¿\ŠKšÿÙlfØAyu!Y„B†ˆˆˆˆˆˆbo]ADDDDDD%Âìvûƒž0ƒUQÎW¹ˆ¯þr6« ÷æÆô¼Lv}PH¤Ã@¨ËÃåË¡Åô$F™}§îã¯p޶-ívwÇe}å ;dgeâ¥gŸÂØ[²`Ðëñî{À¿üŸWDÍñîÚUXºxÑMSqâ$NWû€æ\²x^zæÇÐh4ƒv€ýqxXúÃEX³î]ÑãVžÃ´¼[xA‹€M»Ê±iwyÔlOþÄ$[uWÿ=óÞ´ë¾ÞÖæÃ±ã-ßüÛéGʼn–¨ªi§OüõqÜ~Ö-ó1ë–ùÀ}@ñ¹(®Ýâs;b¦óC‡¿}Pž×Ó¥ÌÂi—¿ }2H~à 6Ô:k¾ý½2c6JÎïÁ©– ÑsÖ¹j°µº sšhö4a˜ÁÓµ4i¡QiÐÕÝ÷Ô“í0iÍð:DÍ-Gמ_Ñ!7eÒ€;M¬j¸ðqÝûØ\óOŒÈ¨Pxåo‘DDDDDDÃx """""¢A!ÂG>²Ûí3‚ÁE9œ|Xú5>,ý÷æÆ½ù™Ÿicad Òóv-ë/•JúP:o ¹á¯pn¯Í.x}ñÛ¦Cî°ÃªŸ"Ñl¬zs¶íüxÀã­–$¬úù+ý†Úœ.<´äñ‡ò&äâ_Ÿûgtwwcì-YƒVëŠ'oúõÌŒtde¤Gý1“?q23ÒQW/nXÅéF”>ÆN7âµ÷ö¡©5ò Ê­&}73ïMCÖè$dNĤïÚ® :H±ÿ³F;Þ‚c•-ØÿY#êêcçnß32¾é†â ¸aÒ&Êþ±~è zåykg[”MÊЫ 0k{½ËÿÒ‰?Æ¿–w¥ÍÀœ¥¸+Mú¯ã]2ZûéV1Ù^€ÒúbQó*ÙáAlø‚¾q¸q¿¢óÇcå/ΓxÿÌÛ(¹ø ÜvD‘Q`ƒDDDDDDñƒ""""""T‚ ì0óJða€¥¬Š²TžÇÊó°YM˜=5Óó2a2pÅ|¸ëýqÚ ¢´:ÀשÜüí7]8‚ÝÝhjiGs[|/²’3ì0ëþøÉ3O]ý÷«kßÄ'Þ7àñVK>ݱù'ôûØûç/ðbû¼ ¹øã6@hjFþÄÜA«uʼn“ý4sûÄš?w¶è.§y!SP¤»:X-:̼' 3÷¤aÒwSyž™÷¤aæ=ßt„¨=ߎýŸ]ÄGªÅþÏátù£rdZ²1Âd¿úo—¿M‘ÀÃu×áÂ[«‹°·vgTÖ§ÊQ\[¾äñ¹)“° £âµ»»@ʱ謽2­c± g ¶UK;^û|~1sÝ€ÏËf€ô¤,Å_¯Ë׆Å;ïÇ©–Ê«Ÿk÷;±­z#¶Uoij“Wà¹É+¤ÕRoí7ð0eä=¢ž@Ê…2EE½í{RÎ@¯ËñÔÝ¡=àBéÅO°ùÌÛ8í¬âA9%utØÏRňˆˆˆˆˆ(*\y3r¿Ýn/D¨ãƒ s8=ø}ñqü¾ø8»>„©;8ˆÏÍÀCØTªP(¡Kf ª@{%OäöúPßÔ ×õôú²…–,^t]W†_¬ù Š?-ðx1a‡GŸ^ŽÊ“[Œ”7!ÅÛÿ€ºó ÈŸ˜ fðþÌzìDÿÛ<£àî˜9~–þp‘èÀÃÙ -p{}H4êyA“QMƒ¯¾·g/´(þ\V‹ËþáV̼7 þ]Ö ¼Þ¬ÑIXöIXö·†Î­ã-øèãZ|ô§ZTœh‰šý’i;îßÝ`7G®ƒKOøÁåk»Úõ!šÂu®3a¤höŠÌÛ£w ¯sžp‡ƒÁ4ÂdÇÅŽ ½~mÞ˜(9¿o“øß±¼M(:ñ.Ê:ßÿKAbDpF!YME”®l{6\m·,Ýïo·6µ:Û®õ=¥­[íwÏ×%;ûÛr]ÃÚ²Sm v_êjå tN†™ar'†:6tf`†›úþ1bZ s]s¯çãÑ#ësݼ¯k.ø¼®·Kë‰T*½v(-ÿ¿ýÇœZ¯*&;·½ñÕWH¼ê*DEFú¼î•5ÇF\&:<ÈÝߪ“ͼ‰¹AWw/þòêGØôêG ;hS¢ðÔŸ¿sÆU(Øšã—a‡o‹U9:PlÍ©aþùZ.Vþb*´)Q~±-Ö3ë2àŒ¡ðæ…/£úî¼qóÿà®™`Zü,¯ï‹Ñ¬wíú”±Ù»6üĪ̸ké–%?Æ»û¤=ZßÔÁ˜‹ôMíøË«áôÏÔR›…¼ßgbÕíS¾VKoú¦+EeM ^«Ç®=}7yVoªðݾ0?)ó“²–^5—âPs)ê:*%w ÊhÑ{ìiðÃmÓFzZ¾7$GKÿ±Ë—á©âÂÕÃvèXž¾Ÿµ”£½[z›ÝŠ­G7b}Öæ¯GKŸ ½=P‡{ä8-žÈÿrÍY]b*œí>,¯U¥A‘(¹ž¥ EX,3,1l]zÍ@4¿ÿKÑÔi”cn˜¥…:V9æë4Ž×Æh3nœ£KCh(08 _ |=xé2ã‚€ óÿ BßÔî^û˜©UÅñFTotiî;¬øùÏðò3›^þ¡?äØaÈúß­ÅËoEUm®™–î7µ746¸LÆÌÀ <èR’¡Š‰v:Œ2DßÔŽÔd5o`½²·¯ì«pûz½t0™û÷ç ly¾ö’Ï—–· ð ïÏG°æ¾™È{t®Wj9ÔùÁdîî=äý¹Âí]Fzz¾Á¬‡ÕÞ屉Óî0]qÑdêõßùú¡æoîý[>‰²Fi_êÚ+] <È;/­ž <ôû6ð0=^z¾\jXÄ©: ôxìLJÇÛåaº:‹§Ü†}§wÊ{Ÿ$ʱ½¾ËÓW»Ü¹ž ö#8ÈýºÍ| ?ø[§—¹f 6-|YÒ6”¡QÆG†d§H<À¾Ó;°bÆýn¯·;Â:9œ½7Æ(ü¯ÃC§Ý‚=Æ·ð¦þ%[/ï+Ppþw€DDDDDD4ƱDDDDDD¨„%Bˆs²"ÞÓn¶áŸe_`íߊðÔÛ‡p¤¾eL×cœÌZx^P°#ø(¿ù/Láø|p°ãèîµãø—_©°ƒ¾©o}pÔ¥uø"ì°{ož~áïN¯{ýÃáØõÐNJF¸Bá7õ¯ª96â2º”䀼¶ätyh=˧íJÑÕÝ‹¼÷»=ì Š à CÍí;äÜôÞwÂ3[úðØG°ju‰Wk« óر=é|8m¶VèM_``„‰Çþj~Rö…ÿ~šöÿI_×îÚ„{©O¶wœ—S©ÅÀ×¾=‡1ŠXLŒÖJ¯‡Ù½õèíïõÜ1:Ñ™`yúJhcReocGý6”6 »Lÿ`?Úº…GŽ19Z‡eé+$í¯œ'ù;SË¥FV-GªŸ\ìò ±^½Òë•¡qÛõã-¥-ûñø‘5¸ñýiȯÉcØÁ§ÀB!DÃDDDDDD4„"""""" xBˆJ!Ä*“á>˜YïùüD òß9„_ÿå}¼Z\v“mÌÕ`\ÐØÜ6}£»×}SÇÌ1Ÿ³ØPðþg.­Ãa“Ù‚»ÿ}­SËκf:6ÿg MˆŒŒ„:.ίÎAeíÈ9Á0{†ôÎú¦ÞŒœÔÕÝ‹u[ÞÅÁjƒ[×{Ë*?^敎 «V— ªÖ¹s^øÆ ä=qÄ«5Î{âˆÛ»; qfâo›­uUz2?Iz#7£E›]~í#e<í}¤Š\V{—ÏÏœ.uèòà)qá#wR†Faõœ‡]ÚζÚgG ‚´tñØqŽÔaâÛ^®~Zò6b*§–[œºLòºmv+*D¹Ûëbé寤½¶+eÜS#ºïz¢»‰-¶F¼tü¯XZ4º{Þá ÷¾B“ÏJX""""""ºÿ$NDDDDDD£†Âp>ø ð|ð*[¯E‡õXû·"üáÅq ª¶ž±ñ¤ûqã|×å!(˜×ž¯Å°Cw¯ïFOŸü׸/ÂpÛŠ{`¶ŒÜ @¯üýýèè8{É~ú‹ª³®™°×˜œ FÕÉfÞœ ojÇ| §Ï¸/ ¢M‰ÂGïÿ»^Ï…nR´Ç¡äãfìÞk4&ÿ¹˜Ì}^©±¡¡ùÏÕxlý¥ Î=éÜjïBuÛ—&ÿûZr´NV‡W&ÜkcÒ¤Ÿs³Þ#Çï•éêÙ»FýApPˆS¡­* +fÜ'ÿg%»>4ìë±w Çcá™ùIÙ˜—tƒÓËo¯/”ü4gŸÐ­&Ë£÷>)¬| ZÕÈ÷T¥¡OÙÓð6>t7n-𗾨ÌnÞg†ã÷w“…«„–„ˆˆˆˆˆˆ.‡""""""u„&!Dž"À]Œ¬Šw5´šñÂ{G°ö™"¼ðî7¶úc[Û%‡±v€wËjÑÜn‘=þra‡¢K<vØò_/¡ìà!§–}ù™§ ¹*õ'õH¿:ÕïÎÉl1¸¡›”°×X†Œ]ݽ¼) êd3Ö=ý.¬Ýî›øÿàof òãåÈùA’׎c×ûÉcÌ–>¼^ï•ý[z{1ÌÏ…+Ú»[±ïô§–íèA]GÎõî{1YÚ+eoO§’~Ï?V»Õçõ—ÛeÃS÷=a|x¼SË-ž² ™2'ëçCå »L›­ÕcÇ)¥ËCgŸÛë %­_…'žÒ¯  )¹’÷¿B”»ýºêèAï@ß 8éÓæ2¬×Ù°Œ»œ0ÃSÕÄÞÿ?²e-E<¹Þg°€îüïï ,  '„% """""¢ÑLQ @£ÑäÈͪx­×ŽÕ 8PݵJ‰EóR‘95 êXå¨;Öà ßËá7âã,|f¬†TžFÅñF—ÖñøÿùÝwŸ~Îéñr†Æ&lØèܘî½7åþ+ªjë01i¢"#ýî<ŒÔÝfË ø‹XU T1ÑNuãâÎŽ#Ñ7µ£êd3ZÏvBßäØnõ)ç:LÌJû&qµãß©ÉñˆŒP *" ©Éjìsñ§õØôêGn[Ÿ*& »^ÏõjÐaHe¼s]Yíùk$ï‰#¨ªõüv¶×";e”¡Q#.Û?Øú³Ç S¥BxA¨ÜÉ·àû’Æ|ÖRŽ3î—4¦Í&.L6W‡KžÝfHPjÜzì_û¾ÃƒœÀà˜œ¾xÊ2·ìƒ§'¤'(50Zôèw¢£Æê9ã‘’{ÑÞ-/˜`´è±õè“X=ç‘Ë~ÝÒg‚¥×„…û'€/O_‰üŠ 8ÓéÜó^®ywÏzPÒ6ƇÇ;Ú¸vÂPÖXìÓëêBÍ{Mní’Ã4'kž¸Þ¿­ÓnÁã[ØÓð6Nšëxr|øc €üó¿«#""""""rDDDDDD4&!Jäh4šÙÖXɪxW»Ù†×ŠkðZq þeêd¦'áúŒI£æø‚‚ÿysî{h(¯+_Dc«iÌ…jõï–Õº´Ž‡¸ï’§÷{#ìwÿïµNMžŸuÍtlþS¾8y  KñÏÉÁ•N28ð0´ÿÎväÒz¶‰qÑnß—®î^¬6à`õ—¨:ÙìR‡„‹ƒÃ…$†‚‰ñÑÐÄE#ò¢0ÄPPÂY;?ªÆó;º­·,Ñ¡`kbUauM:=ºþ’›ñØG¼r,6»[nÄo¯Ûàüñ›õ°Ú» ‹ICpPàüyH΄ûöîÖ+ÚlF‹ó)ÔµW¡Í&dO^¿˜Ñ¢wû¤i«½Ë/ÎÁº›%‡NJŠ&ð8?Q_…u×mÀ†ƒÁ&³GYc1tª´+ÖGXÏxløÝ3ÀãëÔ²g:Ø^_(©3DLX¬SuÌÔdA‘(ùµ·W¿Ãý‡>œQ×î™N6!A!P‡{l¿K[ö£¬y?ö4¼Ã“è[»á:”°DDDDDD$DDDDDD4¦!*¬Òh4yVÁ~P±2Þõù‰|~¢¯Wcnú,š—mbàŸ†ÐP ·×;Û7ÎÑU‚|£±Õ„î^û˜:ææ63Þúà¨Kë¸ïž•XôÜ ŸúÒ€g_*pz¼Ü°Ãî½ENMœWÅDcç+GSs Z¿jÃÜÙ³üö|˜Í–—ÑMJèkN7)EràAt¸7ðÐz¶ÛöVàƒOë½~ü§®¼Lb\4㢥 CêDGb¨[ÄâOëݺÿOýùûX³zf@^S±*…ÇÖ]YÓ¥·{õx*D9J‹²Èé1m¶VØìVL»Æ£LÝ)9Z‡‰ÑZ§ŸJq}OY›½ uUø¬åcÔµW¹%Üp9uí•ÈÔd¹}½½=>?Wrºl-zͧ U¥Äu®vj¢>hUiX1ã~<t£ìím«}Ú˜4LW÷ øg{Ú=vÞ‡º ]Lª¤IåV{jÚŽ`êøk<öyw›Ÿ”õÛ$)küuíU¨å^ÙGO=ý¼·ß÷9]6`ïéX=ç÷ÔÁÃÁñáj„… й7òÙ)‹`4Ÿ’è´[PÖ²ožz'Íuþ‘@>L,¹CK@DDDDDDc™Â$„ÈBèÜ ”Uñv³ ¯×`íߊðÔÛ‡p ª! #$çám„8Âä}ƒƒhl{s6 Þ?Œsݲǧê´xäû/|Üeµbã–g;ܰ`¾¬°lظfKçˆËݼ8w,¿UµuW(ç×Õ…×Y/çd¤®Cz¨+A9òûieH”KûUÚ²Yƒߟ†Ç¬eØÁÇ?¢¸K+„Èc؈ˆˆˆˆˆÜ‰""""""¢ó„Bˆs²"¾óù‰¼ðÞüú/ïã…wÀØj˜}7UžÊ<‡¡ìîà3í&+ÇÔ1ZÓgäOäT*±ùOy—|nãÓÏAo0:5~Ö5Ó±sÛßem»ªöž~a䱪˜h¼üÌS¨ª­C?R§èâßÍqÇBà!VãõmnÝQ޼‹`íî9<ø›(ÙóSĪüç›ÏÒ›tÈÎ’Ö­aå/¦B7)Úíû²æÑƒ.…ÌÒ!ãê$äÎKGbœ¼ý³Ù­øëáõ²&ÌzèM_``°ß¯¯C¹¼íCã^·¯Ó_ÎÍ’Ôe²Æí=½#`îw"¥ï\=çahcRå?·è±õèÆË~MjÉYÉÑ:,“zøÀð.š: N/æ|ç˜ìI‹üâºò‡`Ñh”1ràAN§¡æcxªúøÑûßÃ#‡îÁž†wXlßÚ `¡B'„(`9ˆˆˆˆˆˆÈx """"""ú!D¥b€É`fU|ÃÖkÇêüÇ‹bí3EØYví&›ßïwP ˆp§‡à ŒaŸj;gSÇ«ojwù)÷›ÿ”‡¨ÈÈ ïxoÊ?ýÌ©±³®™Žw¿#{âûÚ?ä9¹A|õº¬VĪb Ž‹óëóbhù‰ì³etGð7rÎ{Ww¯ìíýåÕðÏ’Þè.òçrÿĿܷ‚­9PÅ8÷M1cF¼GŽcÕê¾qBöøÈˆ0¬ûå ß¹$SöºŒ=6”?$+ôÐfkE]G•Çž&ï1ŠXÌKºÁï_3FË)·¯Ój÷÷×'ßeh¤äqûNïpËÓømý]?FehÁá’Ǭžó°¬Ú ©åØVûìw.µžñXàåî™JZ>¿bƒÓËÆ…«^6;e‘¬Ú•6¹µ–^þÈ?’CÍ%î¿·KÇ´Øñæ©qç‡7bŇ¹xKÿºì<1¾c†ãA!“…K…%, yDDDDDDDW „0!òèÜÀȪøN»Ù†–}µ+Â^üª`ë±ûíþŽ„…îxHü8a †|ÍÜÕ3¦º;t÷ÚñÖG]ZÇ}÷¬DÚdÝ…O}iÀsw®Ž6%Ù¥°Ciù'(;xhÄånX0‹~˜ƒÖ¯ÚéW§ùý¹164ޏŒ/º#¸[†ŒÐ†¾IÞ“ö_Ù[ár¸g4QÅ„áèeXuûT¿ÝGݤhT~¼lÄN·,Ñy¤C…«aÈûÕ¡¸ðqî¼tÌJK’op!ô`µw¡¦í,½þû”ó\Ý-þ6ëݾNoLôwFTX 25YÒ÷ßnuËÓøû½ÔébBÔDÉc´ª4üöÚÇ]Úî¾Ó;QÚXôcn±6yä8§«3$…ˆŠ¿Üíôý!8(‘¡QN¯;;Ez—‡öîVTˆr·Õ£w Ç¯C_jº:cدGŽvé´[°§ám<|ènÜZ4ù5y8i®ca}ü­Žƒè„«„–„ˆˆˆˆˆˆ¼""""""¢!LBˆ!„ÀB¥¬Šo5´šñÂ{GðëMïã©·áH}‹_îç¸q@h wtg<@H¨£[Dp0Ï»¯Y]xr| :Pyç:»eÏýa6–ýô¦K>·q˳NUÅDcç¶—\š´ïlw‡¿müêO:žÈxU ¿?7ÎtxÈÎú>_´Nª:ÙŒWöU°çëGÉžŸböÌx¿ßWݤh”ìù)þùZ.Vþb*²³& cF<²³&`å/¦â£÷‚]¯çº5ì`2÷!ç¦÷\;ܹ8W7Üð»;"2Bþþ…ŒféúûQ×Q…6›ðËó=?)ÇÿïÏèðÐ?Øï7±—LY&kÜŽúm._W6/uºHˆÐÈ7]3îsiÛÛjŸýÎkד]ÖdþÑée;ûÌx¹æi§— S9½ìb™×•»»<œëi盀ax"£¸|‡‡Ò–ýxüÈÜZt?²e-E<¾W à.!„N‘'„0±$DDDDDDäM!,‘ó„%r4@€¥T¬Œï|~¢ŸŸhRйé°h^´‰þuJ‚‚ݾ€AàëAàÛÍÆäX>(˜!ÓÝk3ÇzÎbséi÷©:-yàþK>Wøæ;Мk”óáîwd=ÝÿâmUù鯮ùß8g2BBB.éFáÏœéð@ÎÛôêG,ÂyCawwCð´¥7é°ô&Ï¿~‡ÂUµ.­gVZî\’yÙ¯%ÆEãÎÅ™x~çAù÷‹>„õ 6C«’ÞµFoª‡¥Ï„ÔØïùÕyNŽÖB鵉ïrôö÷Àh>%«î#­WîóãÓªÒ0->Ç;ª$Ý^_ˆÕs‘½moux A\¸geL~_S„eÇÑn²ùÕ>Žçèôv¾ëC„òÒÿ•ŽÏ‡†1ì@¾UìBØ!1!›ÿ”wÉçN}iÀ¶7ßqjüߟÙìRØ6lÜ<â2ª˜hü(ûô÷;&PNLš€Àx>ÌXêð MIöøµÞz¶“/z·,ÑdØÁ[*k:0ûÛ];$ÆE#ïÞEÃ.sÛÂY¸q^ºkï‹ìVl8ø*D¹¬ñm¶VÔµWyìÉòRÕµWaÉö¹~vò™ÌšÇÒç?Ò^ž¾RÖ¸²ÆbÔµWÉ¿¦û»¼vŒšÈ‰²Ç®žó´1©²Ç·w·bÓáKCMžûQÓÙàèò°½¾Ð©e¯ôäþ+¾o™´HÖþï;½cT¾ÎFƒ‘‚(ŠàpœímÛ§^ÄÞˆæâ-ýK ;ø3€ÇLB¬b؈ˆˆˆˆˆüDDDDDDD.B˜„ùB€[”²*¾×n¶áŸe_`íߊð‡?Äþçü.ü@ä¯ÎYl¨8.¯ƒ@¤R‰ ÿçwˆŠŒ¼äó·<ëÔø?ÿVþüg.íá›ïÀèD à_ÿÆsü;$$É4sŽ c¨Ãƒ§¯ì­à‹ÀÊ_LÅ®×sv¸‚]{ ȹé=]›p†¼_-BT„bÄeW/[€)ã]ÚžÍnÅ_¯—=!ØÒgB]‡ïCÛë ñówèôSè}Í•IýÃK1]áÔ“Û/g[í³²·ÛïÅë0FëRGõY›e׎wT]R«Þ´Ù„GŽuyúJD‡9ß/¿bƒÓËÆ…«^6;e”¡‘’÷¿´±6»{Â0ýƒý°ô2ôp9M·­«{À†ÃmeØR—‡[‹æ#¿&'Íu,²¨p—"V‘'„0°$DDDDDDä/x """"""r!Ä.!D€É áx*ùXC«¯×`íߊðÔÛ‡p ª¶; Ct®tw¸ÿßV!m²î’Ïíxoô†‘'©jS’±ù?ó\ÞÿÂ7Þq™$M"r.ê€HÝÀd¶ ûõY×Lç…ì}S»[»;¨bÂ5aØÿT1þ(xêÏßGÁÖ^W°æÑƒ¸õŽb˜-}.¯+ïW?Fj²s€£"xìÞ#2Âõkf[ísØzôIYc­ö.ýêS·M(–j{}!~÷Ñ=èì œ·ÕÇ;ªÜ^/«ê%Ë¿'¯ËƒÑ¢wºCÀåxs2ú„(ù]”¡QXwÝYø‡ì;½ó’-žìòp÷¬^öL§‡šËøÇ(T’öcñ”e’÷Ýf·ºµ« »<\žœëO§º´ÓI͹ üýÄ_ñûŠÃ맟ǦjÖX(„˜-„(`9ˆˆˆˆˆˆÈ…°DDDDDDDîuþ)x«4M,€UÖв2¾÷ù‰|~¢xø—©pC†sÓ'°0ä”àà±ñìZ½¼'ßöÓ%XôÜK>×eµ¢ðwœÿò3›«ŠqißKË?AÙÁC#.÷ËÿõÍ„º@ëîÕdž ®«u dÕ§š^¶êd³KÛʘ¥?Ñ!çóƒ$ÉãK>vlßÐÐCC'L¦^TÖt*k:Ü2Á~¤ýÏâû²ö},04tbÕê”–·¸e}ë~¹WK«ub\46=p3Ö=ý.¬Ý®]eÅ0šõXŸµÊÐ(IcûûQ×Q…éñ’Ǻ"¿b¶Hxš¼+¢ÃT˜®Î@r´ÉÑ:Àôø Ä(bñ‹wÿUòúê:ª©ÉrÛþõô``°ÁAþñg½ì”E(m(ÂñéÝ,öÞì”EHPJÿÞ;ðµ÷º<$DhÐÔi”ÝYB«JÊ÷ãù£eïÃÖ£Obý‚ÍЪÒ.tyS·‘Ü=óI¯µâ/wc~RöˆËÅ„ÅJ¾®vÔo“¼ÿÛ¿(DvÊ"·Ôâ\OÇ…{¹F…šs¨9[êsè`ÇE?c €ˆˆˆˆˆˆ(0ð@DDDDDDä!B@Î×h4Ká>d³2þa(ü T„bnúÌMObø†¡…¹«gTc­^ §Oz”T+þ³ï|¾ðÍw`µ<¹é{ïAöEäÚò_/¸LbBÂ%ÁŒ@ëîàŒ±xò4|¹Ý²³& ï÷s] 83ÞÐÐéC˜û¾ CTwÀdî…¡¡ÆFéO}Ϙ5÷ÍĪۧòÆ~»ö°ju‰ÛB'ë~¹¹óÒeMMV»-ô`´èñHɽXwÝhUi’Æ…¦Ž¿1ŠXŸƒ—«·x4ì IÅtuf$ü nNûù°œç%Ý€O›Ë$­ÿ³–Ýx]¼Q{g-O_‰Ç>$yœÍnÅÖ£±>k³¬ŒW{åø‚ƒB0><m¶VÙëÈNY£ùöÞ)küŵR†FAXÏx$ð£ˆÅ²ôN‡ ê:*ZNEp8zœ{ÿœ Ô S“uIg g´w·¢®½ ÓÕnyùS¸È_4uœ^6$$8dþ|l*bñüO)!‡–‚ˆˆˆˆˆˆ [CDDDDDDäBˆ]vi4Á‡UT¬ŒïÙzí8PÝ€Õ P*Bq}Æ$\Ÿ¡…6‘§‡.ÑÑ9ªñô™vYã~ð~DEF^zßûª ;ßÛ;âXmJ2Ö?üËûnhl»ûŠG\nå/. fh®J¨sdhlq™Ù3®³¯ÓÔ‰ÎO„Õ7uH^ÿÊ_LEÁÖ¯nR4t“¢KoÒ]q¹¡n•50™“âK8>7{f„ß^û¸äÉÁC¡‡ÔØtLº²½¾ü­Û׫ŽHÄ’ÔeÈÔd]²ÿq#L ÏÕÝ"9ð u¶3,}&¿ rP(v|ÏðbGrJ!€|!D%KADDDDDDˆ"""""""/B¬Ñh4y–È eeüƒ­×Ž¢ÃzÖC­Rbnú†è‚E(ÂBƒÑgµÇx¦Í,yL6Y÷Ͼù¶Sã7ÿgž[:l{cäí}»»CâU W(êùbô!o†¤êqI׈Gçò„Ià{ÂîµóÒ‘¼÷»ÜéÁf·âñƒá7sFvÊ"Éãõ¦zðÈ„àâ/wãwÝãÖuÞ’‹ì”_q2tÿàð“rç'åȪ±Ñ|Jr'áXzÍ€Ÿå•VÏyü÷ò¾o×>‡éñ’jäl§wQ‡#&,–>“ËuÚPþŒ½¬ñeŘ®žì”Ehê4bºÉÑ:ܨ»Þù~4ù§×®–™®Î€:"íÝÒ‚&¢m6á–ûÒ¹žœ …B¥Ry!ä@þ÷£]G Îw %"""""" XA,‘÷ !LBˆ!„ÀB8ž¶G~¤ÝlCÑa=þãűö™"¼Z\ c«™…ãb£Fõñ5·Y$YùóÿõÏUÕCñ‡¥#޽aÁ|ܲd‘[ö½ðÍwF\fÙÍK.ù89i¨<ÚI)£æXLf‹_íOÁë'x#e ȹé=ÜzG±ß††d\„M܌Ȉ0·¬ïù£±ïôYcõ¦zØì]n=¾ºö*¬ûèn·­obô$<ý£×°zÎ#Ã>ù}¤IôÓÕ˜-=Ÿ\ÚXäÖúØú»|úZ úî3Ô”,K_!{[n”tYíÞ¯Ar´ëÙtehVÏyÊÐHÙëØVû,ŒæS°ô™`éõ̼å»g=èÄëJ‹\ó)Ý †,ÿÞJYû¿½Þ=?R»pÍ‚ƒƒ¡T*µZèèh†üS)€[…:!D>ÃDDDDDD40ð@DDDDDDäcBˆ!Ä*“<ÇSøÈ\~øÃ‹bÿáSh7ÙX˜1h|L‚ƒFï¯Ôzúì’–OÕi¡¹*á;Ÿþ¯85þå¿=å–ýÞ½·ÆÆ¦a—‰T*/éATddÀ£ÊÚc#.£KI5×dõ±:­;1^ú£Òóþ\“¹øLæ>ä=q“g½Òò·­72" y¿Zäö°Ã…ûn²Ú­¡‡mµÏaëÑ'e­ë¨r[èÁÒkºîBgŸëáRuD"~•ñ[lZø§žÒ>ðuÿˆËH™à=ä³–r·žûþÁ~·‡L¤P†\>ô¹dÊ2¨#e­ÓhÑ£°öYÉ׊7Å(beMÚÿ6­* «ç<"{¼Ín½iêôÌ‹ó“²ñ—…¿â×£ÃTxaÑNÄHè0"¹~×j²d…C*D¹[^#ýƒý^¿ÎüY‹­ebÿ%!‡Æÿ˜l0Y‘#„ØÅ’ÑhÂÀ‘ŸB„yç»>ÜÇSùÈÏ4´šñZq Öþ­ˆá‡1(8(“¤G+½Áˆïí¹ðqù§Ÿaùª{qBzı+~þ3·MÌß½oä§hgÍ¿ö’€CâUWdÍÍ~Öñ ¥NŒ—<ÆØØ…œ›Þƒ¡¡“ `¯Ÿ€næëxì‰#n]odD6=p3ÌšìÙk7Yçý¦È¸†/§¬±XVè¡°ßm¡‡üŠ 8ÞQíòzV̸ÏÜø:~¨]âô˜+Mä¿Øü¤lÉûÒÞÝ £ù”[Ͻ?>y^…•3ïwéú“Ò Ã.”‰nYO¦& ‹§Ü&{¼Ñ¢ÇöúBXúL#v&‘kyúJ¼qóÿàFÝÍ>7/éÜ5óìýÙ‘a»¥\I\D¼äk*;Ez0›Ýн2»Ö|Û¹žö1ý}²ÅÖˆ7O½ˆ;?¼·ÍG³Ýȃÿª‚ã÷G:!Ä!„%!"""""¢Ñˆ¿™ """"""òCBˆF ÀR*VÆ¿ …^+®Á¤D®Ï˜„Ì©IPÇ*YœQL sW7ººGßSÞÃÃB%wyxîï…Øñî^dÍ¿;ßÛëÔ˜è¨HlþÏ<·ì³ÉlÁ¶7ßq¹e?½é¿CBB.Û™b´ÐNJá Õ W'ÉWUÛɳÞÀÊ_LÅÒŸè°ô&‹ ^?¼?WÀØèþ ÓS&Æã±{ŒÄ8ï„â㢱éÁ›‘÷BªO5»¼¾²ÆbüúþÁ~èMõ˜Ÿà yr:Ô\ŠÔ<íÚ÷æˆD¬»n´ª4Ô;wò-ˆSIî@QÚX„nÜ'K¯šHÿëâ“©ÉB¦& B^W‹mµÏB“êÔù³Ú}xР©Óè–ÁŠ÷Ã`ÖãxG•¬ñûNïÄtõl$(‘û=ïü¤lY!Ÿ+®†Á¬—4fñ”eØwz§ŒúìÀòô•.ïóÙžÝOüU‹­¥Íû±§ámœ4×ü^!€|!D%KADDDDDDc;<ù±ó]VÐÁñÔ¾*VÅ?±óÃØ’’8ÁA£ïWkI 1²Æµ¶µ9v€»îø9bU1nÙggº;¤ê´H›¬»ðq|Üø€=G&':<¸«s†¯UÕ“n}£àõÐÍ|·ÞQŒªZÏ„Q~sÛüî— ¡ðÙqþî— ñ›Û¸e]eÅØ^/ýmæÙžvk“äq/×<3FÙû{CJ.Ögm†r„ÉøÃqvl®Nzࡽ»Fó)·ëþÁ~ŸM©ƒ‡24Jr‡‹-zl=ºqÄå¬ö. ö{ýø”É“ö‡«Õú›e·Ù­Øtx=š: s?Ž —<&;åDz¶µý ÷ü¨ìË€‘'1ävX(„Ð !ò…&–„ˆˆˆˆˆˆÆš–€ˆˆˆˆˆˆ(°œŠß*F³À*khYÿ4~x­¸“U¸>c2§&A«dqœ**)‰±hl=óM®–‚>­÷øvÊ?ý ¯Ïr˺þùþ¾—ÉšwÝ…‡+ˆŠŒä MßFîütì,©†µ»Ïmë4[úPZÞ‚Òò–oî1a˜=3³gÆ#6VœL@¬JÙ3ãeo'ï‰#È®fË7ûþØùmåý~.Ö¬ž9&¯“¹¯×#ÿ¹ts’¼_-òZPl$·-œM|4þòêG._Ï;ê·!A©AvÊ"i¯Y³1a±Nš: ØR±Aö~Þ’ëÒ{É÷‹É· :L…Î>³¤q{Oïpë~ZúL.<<)S“…RrQÖX,k|…(ÇöúB,O_9ìrV{bÞž&Gkew3ù6­* +f܇mµÏɼ£ ¯{ÿñýM#†QüÁøp5Z¬g$™®ÎÀ´ø ïÖä°½»¥E’ïaßÖfk…&rttÍj±5¢´y?ö4¼ÍpCà0(P „0°DDDDDD4Ö1ð@DDDDDD Î?Õ/@¾F£É#ü°’•ñ_ß?LÓªq}†ÚD‹ âbÁ•Ñz£Ä”‰ñ8}¦Ã³¯…¦3.¯Ãd¶à]kø Í©:-4W%\ø8>>nT_“³®™>jŽÅØÐ(ýø¯N’´|b\4nË™…WöUxôX¾‚x좯iS¢ ›í@Ìr bUaW C”|Ü‚’Í—*¾½­µ¿ÿ•Õ(Øš3fîÇ•5È®»ö. x‚Y:¬óqW‡Ëï×dlz y/¡õ¬kEž?ºº˜ThUi’ÆéMõ˜™0שeó];L‹ÏpKˆ &LÚ¤ùÜÉ·`Gý6Ic*D¹[ÏóÙîŸMÂŽ ‹ñ©÷+gܺö*´w·ÊÚÆŽúmЩҩ¹r0ÒÒgòIà!A©°žÕM]6OY†ºö*Ù×ÈÛ_ü7¥.G¦æ~ŽQÄ"$(ý»sdOZ$9𥠮¬ö.ôô¸­³‡·1ä°vÃrØÅR}ƒ""""""¢Q@Q „]GC« ­fÖC­Rbnú†T\Œa¡Á04ŸÃÀà`ÀOî¼t<¿ó G·Ñ?0€žÞ^„+¤O6™-066Âd¶à“Š##.¿è_s.ùøâðC 2™-Ã~=V3j^[Þèð·.œ‰âOë]ž .—±±ëB'‚Ý{ n[oá'°ô':,½I7jï¿&sví1 ÿ¹TÕvx|{‘aX½, ¹óÒý¶&©Éjl}t9Ömy×åðÚ¦ÃëñdÎ ’º Xí]hê4 9zøë®©Ó 980D‘ˆu×mðI}suÒ6»Õ·mKÌ IDAT-O›bé3a`°ßoŸê¯ ºë6àÑÒ_Ë^ÇÖ£Obý‚ÍW ÜXzÍ@´oŽO“Š:ð¯dõœ‡ñHɽ²"6»üx Þ½íP€tyˆG›MÚqf§,Âö/ %×çxGêÚ«0]áÒ>Ÿëi¨.'ÌÇðyÛA†»9 ˆ% """"""=„&!D¾B`!€BVÅÿµ›m(:¬Ç¼ø!Ö>S„W‹«qÜØÎ¨¦NJ@„"4à%5YëgOñè64W%Œ8qÿbýýý_µáHe5ªj][Uslı3®¹ðïDEFôù©>6v&¯dtxÈØáaèõ›÷«EˆŒu5\óèÁQymìÚcÀÒÛ‹1^[€»î+ñJØaVZžôg~v¸øšÞôàÍX0KçÚû“îVl=ºQò¸¦N#l#<ß•îë®Û )„1œ…´ iîä[&=œZÑâÞ.#uYð”ÈP羇jUiX1ã>ÙÛ±Ù­Øztã¯#_¿ãš‰•Üd8C¹êÚ+ñìÑ'âÞ®–5nù÷ä51Ü^ïúÂR¾pÂ| OUÿK‹æaŇ¹È¯ÉcØ!pìp«B'„Èc؈ˆˆˆˆˆèÊx """"""¥„%BˆUÆX ÇSÉÏ …þÿWà×y/¼{Gê[X˜Œ©“ ‰øc¹ù†HR{®S@Ö¼kq¦yøëÚd¶ ©¹UµÇPþég¨?y ]Vë%ËT0ù?1!i“u>ŽÏ 5€”<$iyW ©Éj¬^–5êjhlìBeMGÀÇP'‡U«K;©·ÞQìÖn#]Wy¿Z„MތĸÀ¹¿;‚ÌÐÓÓ‹žÞÞ×SUëDw‡™Ó/ùX7꯿XŲ8gÎïw®­‰j—¶9ôäþM¯~4ª® “¹7 ÷»²¦%7£ä@‹× ßvkÎLܹ$QŠ€=ÿ¿ûåBÀŸÖË^ÇÖ£O♽.©«‚¥Ï„6›@‚Ró¯v£³Ï,y?´1©Xæ@UY['yL¢º«äÎKGêÄx¬{ú]X8°缦Ãr8ÐŒ’›alìòÙ¾ÌJKÂêe š¬µu5ô`³[QXû,VÏyDÒ8£E¸põwº¼\½EÖ~\nÒ»«B‚¤ÿ™lº:£µ8Ó)­yZiƒûýƒý°ôz¿"8\ÒòÊÐ(¬»n6|6»UÖ6÷Þ ­* Ù)‹.ùü¹ž$Gë|öºÒŤá\OúûݶÎÕsÆ£¥÷ʪÕñ ¶×ºíó”ñáñ²:”,™² ûNï\›¡î*— _9«ÍÖê“ÀC§Ý‚²–ý(mÞÏÛ2䘌 à:X"""""""ùx """"""ƒ¾Õõa6Á‡¥T¬N`øüD >?ј”¨Âõ“95‰á?£D\Œ2 ƒ7ß0S&ªñnY ÎuvûÍ~9xH¬»ðï¨ÈH„„ðס¢ªFN‡‡x·l;5YW»›^ý« _Kݤh¿Û§Êš:/*k:`¶ø>`’;—d^èö1š¸z(k,Æòô•’& ÷ö£ÅÚtɤô¦NŽwTKÞ~¦&Ëå§´_ŽRbÇ‚!ËÓWbKÅIcÚ»[Q!Ê‘©Ér˾Ÿëi÷zàAN½´ª4¬˜q?ž?ºQövŸ?ºº˜ThUi>gµwa`°ÿ;o  Ar´³ÞmëLPj°zÎ#øëáõ²Æo(_‹\Ý->ë|áÔ{ãp5ô¨—uíej²PÖX,yìöúBÉ­‹Yí]èè‘ø‘£ÅÖˆÒæý(m)ÂÑöOø†0pØÅnDDDDDDDîÿðqBˆJ«4M,¡‡52X™ÀÑÐjÆkÅ5x­¸j•sÓ'àú -´‰Ì¯ø‹‹ƒç,6tØ“ãg¤jš•§qàèiôôÙe¯Ë™ ‚3ô§G^OÚdÝ…«TßÝÁÐØ4f^3%åÒ'ù¹óiüQ äýêǨ:ÙŒWöV úTs@Ö1cF¼Ó‡Êš˜Ì½04t!V†XU V¥Àì™ÒÂ$††NO¢.ùØÎ+9Ð “¹Uµ~W§Èˆ0Ü–3 w.ÉÕ¯+WC[nÄú¬ÍÒÞgZÏ`Bdò…IéÅ_î–µíåé+Ü^&Ê/O_!9ð8º<¸+ðp¶§ã’€·(‚ÃÑ;Ð#iLvÊ"ͧ°ïôNÙÛÝpð!<‘ýÂ%¡›³=í.=¹ßUšÈd´ÙZaµ»¯#M¦& ™š,TˆrÉc;û,Øpð!lZø²ßÞ‡‚ƒB®–ÕåayúJY9­o;×Óî±.'ÌÇPÖ¼¥-ûqÒ\ XF8*QpþDDDDDDDäF <€ ] œïú°êüœ5@ÚÍ6Ö£è°JE(æ¦OÀÜô$LÓª¡ e|l(øÐÝkG»É sWbß#¡È—ŽëgOAÅñFTÔ5 ¹Ý"y=V› UµÇ1ã—öGo0ûõŒk¦_òqTddÀ_?ƆÆ1ñ:1™-¨>&}Â_jr¼Û÷%ãê$dkó…N–>“O IEÌcºâëtÎÃø÷ÿ¾6»UòØõÛ°<}%æ'eûí}h|x¼¬ÀC‚RƒRr}Òå¡ÍÖêÖÀCiË~|Þö J[öCØÆN u2Ø ÿüƒ$ˆˆˆˆˆˆˆÈC‚X"""""""ú6!D¥b"À]v³*ÇÖkÇêä¿s¿Þô>žzûT5 Ýdcq|,BŠ”ÄXL›|RcPû~ýì)X{{þϪ!I-½sBáï¸vúªmÄeR§è.ù8vtx+Jetw˜21Þ£Öã¢qç’L¼òØxå±;ð›Û`Á,_×ñÁßÌ@Î’®øõ¼'Ž`ò¬7PøÆ Y…ªÚ”–·dØ!2" w.vœÏ;—dŽ™°àè^ò»_.D¤Ìï;ûNï~϶žÁÀ`?àÓæ2Éã³'-òH-\éð8ž6/ë×Xä¶chë^¿†bò³Ðë®ÛuD¢ìñF‹…µÏ^øø\ï;ÆÄ(b1!r¢[ש rir~~Åc~}Š W{ýuWÖXŒ6›ü׋ÕÞ%+è3¤ÓnÁž†·ñð¡»ñ£÷¿‡G݃·ô/1츪ÎÿžD'„XŰ‘ç±Ã KQG×¾éú eeÏç'Zðù‰À¤DÕ…îÚD6ñð•à   ]úì°X{pÖbCw¯= ö|ŒפNÜé¡êXŠ>,Á¢æÈÚnëW_¸ÌÅBBB®Pð‚ »÷IŸ œš¬öÚþ%ÆEã¶…³pÛÂYŽëùd3ªO6C¦U'›aíö} cF<òŸXpÙ¯™Ì}Xz{JË[ÆÜµ5\Y0K7¦B—{½Ü¹8Ïï<(y¬œC]š:²ö÷ZM–Gê  u­óOîä[01Z‹3kßéX2eÙ….®ðÅ„ÿHö[…u×mÀ†ƒÉê^8&®'(5Xž¾ýƒý>érñmÉÑ:œíépiBü·ej²©ÉB…(—<öÓæ2l¯/”ðüûßÄ…«®Ëƒèj’Ômæ„ù>o;ˆ= o㤹ðÌptÄÌBX"""""""ïbàˆˆˆˆˆˆˆœrþúyò4MÁ‡•¬L`jh5£¡ÕŒ–}µJ‰iZ5æ¦'anúÇGÂBƒ¡Ž„:62 Â©ãñŒqŸ~d…œéð0{Æô ÿŽŠTò r:éÁŒO?‡S_°òç?»¤#È÷ '‹ŒŒ3×NÆŒkzÿ«jÁØ(}‚ /ß–ĸèïL®wtè…¾©âl'Z;:Ñz¶Óíaˆ’=?…nRôe¿¶juɘ ;DF„aÁ¬ÉX±$‰qÑüÆr«—-Àê'·KWÖXŒ•3î—4ñ½w •_}*y[ÓÕž{{© q=H±<}…äÀlÿ¢Ð-ÀÑåÁ›Ep8B‚BÐ?Ø/{Ù)‹ÐfØQ¿Mö:¶}ël†"8\ÒS÷=%F‹ ‘Ñb=ã¾k44 ËÓWb[ís’Çžé4"¿bÖd®÷ËûO‚R£E/ë:JPj0->ÇeL\éòÐ;Ѓs=íþMW©[#>oÿ¥ÍûñyûAtÙ;A£BÝ „&–ƒˆˆˆˆˆˆÈ÷x """""""ÙÎÿñ¿@F£ÑÁ|X @Ëê®ÏO´àó-x­¸“Uº?LÓªYˆP„""A…¤•߆f¤jPq¼QÖØïíEÑÿ”`Ñ¿æ`Ñs6Yç–}ºxâHÈØù5h¬*& ÷¿ðw$ ø»¡PÆåº ZM¯~äò6þñ\fÏŒ¿ì×ò·Ö ð£þ50+- ¹óÓ‘;/ß@Fš¬ÆóÒñÁ§õ’Çî=½CòÓã–Ó’·3=Þ¿ó´ÉÑ:ܨ»Þ•4®½»¢™š,—÷álO;zz ÷Úq+C¢`ésmðòô•h³ YOê2¶݈õY›a³wy¬ˆÔëálOzzܶÎÅS–á³–rY“û_®Þ‚»g>àóW2><m¶VÙ×Ïã’<ÎÕ.m¶V´õ¶¢¬y?J[ö㤹ŽßLF3€]ò…•,‘aàˆˆˆˆˆˆˆÜBa€#ð°F£Ñ,…#ø°’• l ­f4´šQtX¥"ßÓª‘™ž„¹é  e¼ìâðƒ¹«kÎZl>߯Üyé²`µÙ°ó½½ØùÞ^D*•H›¬CÆLG`aöŒéßYþx½´IÛ±11¼xÄî}E’Ç,˜¥ ècvWØáÎŪۧ^ök•5XûûOFíu3ebuwï¥ûÛ¥•Zw—V×"Ö»‚ºUÁ¶-(6( §r&&„Éfˆ™$L2 I~$!d®9d&y=46T}¼!ªÁ‡avͽ:_ßÝ3àu8yò¤~ñüKzø®rQĉ:_½^~íõ ÇÅsØá§7†¼Ÿ …ž\¹[Þ,‹ÛëbâØ‘štÙHM¼ld\wòˆu“.©‰cGªäpUPã¼MÕÚZ±)b¡G Âgaþ­Z¶í>5´ø‚WäÙ¶ B¿:j‡¤Ää>™ÿLnçXÝ1å!ýò㟚ÞÇ=\¬Íß(‰™Ny®|í;^¬“í'ò?»%MóÆ,кÁ÷¾Šå.™)Y*O,5]'³]ö/Ö{GßщŽznØG‡Ámƒ:Cë)ñ‹Àˆº®Åë Ãp©'øp=•<º»?lú°Tv›E—»³4­+üå²S KKµ)m”M¾ÆfUy}jim‹Êq¿8yŒJ+½Ú{Ä3à5Øü§­ºõ›ßPîèQ\qàé_ý—©q3'æÆÝ¹v‡N„Hš4~¸ž\>ãœÏïÚ}\÷þÓûqSGªU“.©¼‹³8t©®mçxƒJuJzÕèï¹nºk”3<]yWÞ(óÃgO:ð uvyøcæ»1ìóÿ ìng^\½^·O¼[O- ¾†ViÉ”‡C>~}KêuQ[Ôžau†µƒÁ¬Ñ×jŸw—Þ­Ølîýr¢J÷où¶žûêocâz°[ÒäÎÈ3ݽ / óoÕÖO7ÉÛTÔ¸Xîò”˜,Ãq±*ÊM¦ËCBB‚RRRd±X”’’¢u•/òØàU,éII뻺N€8Gà ˜®Å/IzÉ0Œ\un“4‰ê þ@«>:xL<&IÊrÚ55ÿ"sgkjþE(‚º;>t/š†¿Ÿ3E¿\·MUÞú?ÿm;>Ôå—åq!ħLr2Óã®ÃCcS@>·),a‡Âßÿ\NkŸÏ×ùZtے˜­Ãı#•3<]Ffº&^6Ry£†+-ÕÆA˜Í;h{I™ªkÏßî+ ÐÝcÆÄÜ 3&^ªœÌô óLÞ¦j½ydæßzÞíGð?„q1}4Ü>á.S‡w+6kaþ­áéòÐä‰^àÁæÒ±GúÏ%SV¿º_‹×ûò‡²ßuv3˜xwL\ÙvCõ-uªñW‡mŸ /¿ÕT'ŒµVÅdàA’.rŒ’çÄQÓ]nÿýhë÷û|ÎjµÊf³Éjµ*9™ÿ[|+ïþo ]Ý%À ÂÙ1¡kQ“’ž4 c²:ƒ7HrSÁÅëókÓ‡¥Úôa©$érwÖ©„;ÇI" '3]Ž•«x·‡T›Ew,˜¡‡M,Ôwnù.€·êµ×å«>3#κ;46ôÀSƒ^P~¦ …$éžmWñžã¦ñõ‚ §Ňªú<—#G;÷ïHµ*ïâ³Ö÷1LìêD@׆sÛ¼ã€ÞØRrª®f•®RÉá*­~«H9™éš;=_s¯ÎWNfúÇ.ºnšV¼²%èc®;ð²þƘ)·sì9·9ßsçRæ+«×0ÃæÒ‚üÅZwàå Ç†«ËC¿Z£ÒseKJ‰øùKÉŠÈ~¸j™.ü^Ð] º=Y´LW,ÐY±‘ŸÎÍ+뉰uØ5úZ­ý몠ës´¡\k¬º`8i $%&kXÊpÓÁ·s¬¾4z®Þ­Ø¬äädY­ÖSA z>Ië%=éñxvQ¯„ŽŽªb–a7¨3øpƒ$VÃrv›åTøaœ;KY.;E £¶öv•UÕª1ÄO—ï¦@kL„^}îúÆ óÅë·uÛûúÛ¾qÞmþ¸þ7š5ó qu^yŸÿ‚Ê+*ƒ·ú±›ûµˆ;V<þʽ³ã@HûèOØá¥WêÛ?(4}Œ9Óóõà-³¹aFIi¥W¿²%ä Ã…̘˜«¯L¼`èdÑÒ5¦B9îŒ<=2ó Ù-içÜæ6þmPû7|’™ùDDê‘auEdA|eC™¾¸f¬©±OeMXºo±ê[ê¾ßrßa-Û~Ÿü­'L7|¢Þ¼é£˜yŸû[µïx±égý=P±ÉT—‡é#¿¤×æÿ)&ï…¶f}\½#ø¿7ÛüÚ][¤=ŸíÔÇÞ”””Ä/–¡aƒ:;9¬§ ‰”Ä2dzÞãñÜ&)WÒ·Õ¹¸ƒ”?Ъ÷J>Õ³¿Û©{ÿï&ýËsÒ+›K´óÀ1ù›[)Pˆ’•7*K™‘’¤Ú,º÷[úâä1zί®eT,[õÚë¦ÂÇŽŒ«°Ãê7‹¢vصûxHa‡1×’3¸0£x],ùÉÚˆ‡$i{I™|z㻌̞ojÿåõ¥Ziböù÷y8î^ÓQé¹Z¿ØÔØpÕ¯Æ_­@[sTÎ73uxDöëvŽÕâñ?4=~ÿñ=Y´,f® »%M°íoÖèk••šô¸UïjŸ·8&ß;¶¤eÛûwN‡ë÷é·å/ëñÝÿ¤*úG½zä—*ùì/„¿­]ÿ=`˜Çã¹°C @Ü1 Ã%é¶®¯ITdè¸Ü¥qî¬S `^EujëýQ9Vi¥WßÝ3`Ýô¾rGŠû×l0vx0ÛÝáÑï^«/‹s,>T¥ŸÞÒ>úv¨óµhò5kU^ÑhêŽT«VÜ5_y£¸·FZcS@+^Ù¢í%e6‡Eó¦éë³'(-ÕvÖÜ-]£&;}iô\Ý:þ‡}vzøQá÷T^_ÔþÂÕõàL‘êð …Öåá_g<–yE«ËƒÙOåﯕÿDïVl6=þ÷ wFìu6£ÜwXÇN Ïß&»<,È_¬³_ˆÉ{c[ûI}ü?;Îê„qÔ_®Ãõû´û³"®ßÏ/‘¡¥XÒK’Ö{<ž2ÊÀÐE‡w<OÇãyÒãñL–t©¤{%•S™Áï¯å^ýöÝ¿ê üˆ IDATßW¿§Eÿö[ýü7èí«¼ÚGq‚4:Ç¥´TkTŽ•7*K÷~«@?gІ¥§Fý\ïû—¥¼à1è©_ý—©°CNfzÜ„›zô¹·CÚGÂ’t÷6™;HÒ’3 ;Déšxà©v¤ÕoiÉòµ*­ôžöxZªMKÌ4½ßw+6kÙ¶ûTã÷œõ\_!ˆ Ùw¼8î^ãPº<¬=°*,sˆV—[RŠlI)Ûÿ­ãRg„¶|;¦® ·s¬2¬®°ìËl—‡ÍŸÄn³¼¤ÄdŽ‹U¨ÑVÏ[zþàÏô£¢Ôã»ÿI¿-_MØaè(—ô”¤)gr×ÿî/£, m@\óx/x ©óÕkÙOŸ05vÑuÓâæT¥ŸÞò~§3µxü”m7ôIÝAýó»K‚o·8ôü¼a?_[RЦäLhMØr»Öx9ø¿_RsôÌœWÃ2‡)9Ó#ÚA’ü­*©ÙÙß›ôËjjlºÕ©7oÚ©Qé¹1ó~ó·6jßñbl?ò~¾óÖõÁ¿ßsçëÙ¯¾1`çßÐZ¯w½­j¶k§÷}yü•Âä“´^Òzdzžr€ó!ð=Ã0nPgððÃFâÜ*ªëT[Ÿ ÛhUUO’Tå­WSàôDs Uïí:bjß‹¿y“^x扸}úxX·ê¿týuׯýyœË¢yÓâ¢ÃCcS@‹–®1ÝÝÁ™aUáïÿN“'œÿ“Ówí>®‚¯ýN¾zó]$V>¼Py£²¸FØö’Oôès›bzŽc.®wÏ?zX¹n›~[¸;,ûž{éõúî¤{uÓú‚ ÇÞÕ2M3f†ý|¯9+¢õ¬l(Ó׌55vAþb-Ì¿5ä9dX]¦:këãê ´5Gô?ûðÓÝ/ä/ÖŠÙ/ÄÔû­>P§}Ç‹CÞÏÊ¢w+6=®øÛ^eØ\Q9׆Öz}äÝ®jÞ×GÞí:äÛÇ/…¡mƒ:C/Q Ð_ÀÒ~¸MÒõTch#Ñ£¥µMû˪ãf¾+×mÓ‘£æºR<ÿ̺õ›7ÅåëôÏ?þýôéÿ<ï6߸a¾^}î1{u¾zM}­Ê+‚ÿ4çxêîê'ùüÞ‚ †ê|-ÊðjHa‡n™­¹ÓóÝ=­­½ýTXªw褥µM-­}¢zRR¢Rm–Sך59YVKRXæj&šz‡›Z²|­ªk¶ÿ”ä5Ÿ naü4c¦î¿jYØÏõoŒ™JJLŽh=Ívy°[Z>ëYeÛçpÅðI_Ø^î;¬c'ŽFôþÖF=\ø=y›Ìý½òëùŒxÈ%X5~JëBëú²Ï[¬o¿/øßS³ŸK¨¦/Ї êéæPG9@°<€!É0 —zº>~À@ÄR—‡ )­ôê—ol7=þë£Y3¿W¯Ïª×^×wî¼ð‚F›ÍªíooÔ¤ñWÆäyÜ~ç}zùµ×M—îÕµ Z´téñ/þgnûÖçλM¯E_ûŠ÷7}œ9Óóõà-³ãú¾ÕØPKk›ZO¶©ÑP[{ÇY]aBaµ$ÉéHѰ û©0„áì” ½C¥•^=ðôÆk<ý•5aYüß[4‚¡tyWÐÃaIÓ„ì©=Ï@[³>®ÞñëÀìâ~Iš>òKzmþŸbïošº¿ªÆZèôÎw¾t$œ]/8 /ï$$$¼*B <€!ðú2ÔñÖåá¥ÿþP{xLuf¤ëO^ÙPÀ™Š÷ìÕ—¯¿I¾úþ}Êú—f\­?mx=æÎ£¿¡¾äd¦kõc7ÇÅëJw‡»ï¯'—ϸàv_û¶n;fz޽´Ç‹¦@«N4µ¨)Ðzê+šÒR­ÊžtÍB À ”Þ×Èæ´â•-:Ÿ/ž«%Së>ó\ùaQôÅl—IºÿªešfÌŒ‹sÝ]³S'Z#^Ï—÷üBoyÃÜý9‚] BjèaíUA_céV§Jn7š;æ¯ÐAß^8KGGGikkëÛMMMnnnþCGG‡—ª€p ðÐ áœKïÄ%†Sîç ;ÇÒJ¯øS¼ûë³z¿þý¥?˜/¡‡`Ãݼ‡÷Êå̈ûóèöÀ-³5wz~Ì_—MÝøÐ‹¦ÆN?\»þ¼à‚Ûݶ¤P«~}Ðô©V­¸k¾òFeÅ|-O4µ¨Ñˆ©ûR¶Ë¡‘Ùý¿ÿ‡ÚÝ!ÝêÔÜK¯×Õ#giTz®®9KRç'Ý×·ÔiŸw—>¨Úªª¶ª¡ÅÖsíÝ$Báîò0*Ý­Q鹟w} N׬É3õúØ-=ó•We·¤…4‡äÄdM1]I‰É;Ï¿G¥u"^Ok£îü÷äo=ôØ‹ÓÝúóÍ¥1w¿kk?©}Ç‹MFjüÝõ‡àC¿_¸SWdMºàvÇüúÈû¾>ªÙ®Þ÷åñWò? pÚŸy’^’´¾ººº÷MuWGG@Xx8Â8»Í¢ËÝY—Û‚ ˆÚz¿*ªãg]ÒæL’¾Ôzxãåç5kæbòüV½öºîû—¥¦BÏ?ó„nýæM1qu¾zM}­Ê+Ì-œ8v¤VÜ=?.®É7¶”è—ol7q-ZµëÏ ”{Iúy· 5ì I~÷Z͘xiÌÕ.V}IµY”7j¸’/¸í×zA'LžÏ·'Ü¥{¦=¢ ›«÷ÄO6híUz§lcØÎuѼiZtÝ´S÷Ü =|Þø‚¼êß¶¿ ««_‹½ÃáÉ¢ezªh™©±óÆÜ¨Åãò.r\,·slÄα­ý¤þâÙ•zy¶ég>bjl¬vy5ôð£Â塚>¸0ÇÝÓÑ=Óήcg÷†í:äÛKÀçr*äàñxʺLHH˜,©û—6úðúãrwÖ©.ãÜYqy{J=jko›ùþû‹ï賆¦öñ³[ª»¿ÿ1u^Ë~ú„–=þsÓã¿4ãjýiÃë~u¾z}ùú›T²wŸé}¬|xaÌw#èvÇò×uäèñ Çýü?¾ {–L8ï6á;ô^¼>ÐZZÛÔØPý‰f5ú[âê¾#Ii©Ö ^—ÛK>Ñ£Ïm2µÿPeW6”éÉ¢eZwàå°œëãwÍפËFJøÐÃO žÓ%c¶¿îŽÑpÍš<m(75ö_g<–pÆ”œé²%¥DìKëþªut~On»Oû=.V»òKzmþŸNuo8èÛ«¼ÛÕØÚ  }†zKHHÈ•”ÛõãÍ” „€ ~@]’ã<-aO±Äüœ«kä9? ݪj|úù¯·†¼Ÿùóæê…g~.—3c@ϧ¬¢R·ÿï{õîöBÞ××ÿf@»W„#ìK ôûóÞY´tMÐãfͼH…¿ÿ»ónŽ°ÃŒ‰¹zô»_Ð56Tߨ¬Æ¦5ZãÿïáéÊɨڪ¶Ünz‘}·œÌt­üÑB¥¥Ú$IŇªôèso›î\Ò\#õäß®Ûþ&fO•Ý’•¹oþdƒ¾¿i©±Y©9úIÁ³!Ï5Ò]-êuÚg"„`Fߣ»þp³©±±ÚåA’ü­Úw¼8èÐÃ>o±~¼ý¾ n— «Õ*‹Å"‹Å"«ÕÊì8Ÿ †θ¾Ò$M“¤ŽŽŽBÊÂ%‘ÇãñÔy<ž—<Ï ’†Iú¶¤ Tgú´Ú§M–êÉ×?Ð÷Wü·î}f“žÝ¸SoxXåÕ¾˜œs–Ë!«%)nj<2Û©9ÓóCÞÏÆ·6+ïóWkÛ›ì\–ýô M-˜–°ƒ$Ý~ç}ªóÕȹ„#ì0æâáqv¤ÒJ¯©qþÓÔó>Ž°Ã˜‹‡ë[fG½&mííª­÷«¢ºN{J=*­<®šºƒ"ì Ižã jim;çóÛK> zŸã†OÒ¼1 Â2¿«GÎÒ› wjAþâöS]Û ÕoúyÒe#µú±›5qìȨ׼úD•jüž°í¯¾¥.jsŸ{éõš“;ßÔXoSµV~üÓ°œïgÍÞˆc†Í%G”$ÙvCóÆÜhjì“EËbö¾b·¤éŠá“”œ˜Ô¸+²&ÉnqœõxRR’RSS•‘‘¡áÇkĈr¹\r8„p.Å’î•t©Çã™ìñxžìOØA’:::%ÑÕ„€~@0¼>¿Þ+ùTk6ïÖÿyîOúþãÿ­ÿoõ{zãÝýÚ_î•¿yà'%&*÷¢L%%ÆÏ:œ;=_WŽ1BÞ¯¾A nýG}ùú›´uÛûQ›ÿª×^WÞç¿ eÿ\¾úðu×(¯¨ÔíwÞõ×£xÏ^M}mHaIzp臢´òxÐcfͼH×ô½h¼Î×¢¾µ9ä°ƒ#Õªo™}êÓù#­¥µMÞº*­ôjO©GÕuª­÷«­½}pÞ×ëÏq=xMu@èþÔù@[xÖ‹fØ\Z1û=>ûùöóÛÂÝ*>Tuêç´T›VÜ=_wÜ8CŽÔè.šþù_ Û¾j›ŽGuîÌ|BéV§©±Ežm*òl ý^Uw@mAv†á¸8jõ\˜kŸ‹ü/ähC¹ÖX³÷»%M Ç]1|²¬V«ÒÒÒär¹4bÄeee)##C©©©JNNpºþwlÐ!‡¾~5RNn T Ì ÃpIº¡ë«@’“ª ?.ÉqÊãÔ8w¶Æ¹³”å²È<š­ª¨®‹›Obo ´ê—붩ʾŽ_šqµ–>tŸfÍüBØç[ç«×†·6iÙOŸPyEeDk³ø›7é…gžˆÊë°áÍMºýÎ{CnÜqã Ý8{b\½w}îmm/) jÌ‹ÿY Û¾õ¹³ßµû¸n[R¨â=¡/Æ^ùðBåÊŠè¹76Tߨ,߉æóv<Œ¬–$ËÍ9ëñÍ;hÅ+[‚ÚWVjŽž™óª$éŠá“”as…u®k¬Òƒ[¾czü˜‹‡ë—?ºé¬Ç«kôò›EzgǨÕý_g<¡+²&…e_cÌTRbô‚¿Pò”~¼ý~Sc퇖ÏzVÙöÐB~9.–Û96bçXäÙ¦“ Uœy]¯;ðrÐã.NwëÏ7—Æôý¥ÆïQiݹßWµõ—ëpý>®ß¯£þrþFP¶IZ/i½Çã [»›„„„4IÓ::: )1Q`Fwøá~@ì6‹.1œçÎÒ8w¶Ü9NÙS,Q;~umƒj>;ŸÎÞhÕ¿¿ø5·„7¤á=J·~ó&]ݵš4þJÓû©óÕkë¶÷µá­Mzùµ×£Z›‰W^¡7V?¯ÜÑ£"²ÿ:_½n¿ó^m|ksÈûš11W~÷«q÷^}à©*9\Ô˜OJþA¹—¤ŸöØ“+wëÑÿØ)_}Kèsºe¶æNÏû¹¶µ·Ëר¬M-ò56Úî ý5>Ï8«+Îê7‹´ú­¢ ö3oÌZ<þ‡’¤\gž Gøß¯¡†ÎwMªÒê7‹‚~˜º/gäiyÁ³aÙWž+?äA°®{ýóÚ¼ÄÔØqÃ'é‘™¡‡Ø&fO•Ý’‘ó«l(SeCtßû[uç¾%뉠ÇþzþuõÈY1}ézè 6”éPý~5·ùùCÁŠHÈáL “;::vQn.¢ŒðBÕÝ¢3Ñ‚ˆ¤¶övyëNÄEð¡ªÆ§•붇=ôÐÍ=z”fÍü‚&¿B“Ç_)—3£ÏDYE¥Ê?­PYE¥ŠwïUá¶÷U²w߀֯™‘®GºOwÿúßU¯½®ûþeiÈ]¤ÎO_q÷|¥¥Úâî}i&ððñ{ 4yÂpIR៫ôèìÔÖmÇÂ3Ÿ0‡š­]‡&56µ=òF ?ëš5s=ôîZÉOà%ô“™®•?ZxÞ÷hi¥WoîŽxLJ;¦<¤Y£¯ y?™)Yú\æ•Q½föy‹õµµSM_¿X óo iKš&dOÈùµµŸÔÇÿ³#æ»<Ìɯg¿úFLÞWúöêo¯ÖíÕ‡ÿó®>i8ÈÍføÔpX­ƒ&$$’¼'y @8x@†aL–t›:Ãn*³.wgiœ;Kî—Ü9Ne¹ìa?F¼"zˆwîÑ£ôÈC÷éúy×ÊåÌ0½ŸU¯½®e?}Bå•a™—#ÕªwÍWÞ¨¬¸¬«™îîÑiºíæ|­ÿï2ï9¶¹Ì™ž¯o™òû½»‹CcS@-­m¼yÎ!\‡ççm8õ‰û‘\Œ.I˶ݧw?mjì¢yÓ´èºiÜ®±) í%ezcK‰Ž=ösÈJÍÑ3s^ ˾¦äL—-)%ª×Í“EËôTÑ2Óã—ÏúUÈ¡˜Hu‘¤Òº¿ªÆ_•Z†Òåá½›kTzî€ÞCZëõ‘w»ÕíÕNïû:äÛ£ÆÖ&•K*T”C½%$$¸$¹:::Êx9@8xˆ]á‡îΓ¨Ba·Yt¹;Kî^] ì)–°ì;‚„úgþ¼¹ºþº¯jÖÌ/(wôù½ÖùêµuÛûÚðÖ&mxóí°ttèmåà ã6ì I«ß,Òê·Š|fÃmííjô·èDS@M-j ðÞé¯I—<ë±EKרº6¸÷ȯçÿñ´ŸÿƘ©¤ÄäˆÍûº×?¯ýÇK‚çHµjõc7Õ‰¥º¶AŇª´½äªÒ‰0u G§I•îEïf_©3ðñ“‚gO…dÌHNLÖ„ì© {ÚšõqõލÕÒl—‡oO¸KÌ|"ª¯ûGÞ÷õQÍvôíÕAß^yü•BT®ÎN/y<ž]=™„„„dI×Hú £££™—„ŠÀ@ 2 #W=á‡YTáå´ë’§Æåvv‚çmqywð¡¶Þ“ŸþÞhÕ/×mS•·ž¿Ÿ¾4ãjI’Ë™!—Ó©²O+$Iå•aëäЗn™­¹Óóãºv›wЊW¶ è‚ ;´´¶©)ÐJÀ!D©6‹>wIöYϽó—AíÇ‘§åÏžöX~æ•–¹Ð>o±¾¶vꀼgK+½*>T¥Ò£ÇUZé5ÝÂnq虯¼Ò¢I²%¥hJÎô¨_?û¼ÅúæÆ/«¡Ågîž=z®–Ly8¤9d¦dés™WFäüâ¡ËCºÕ©’ÛGl^yß×Áº=:Ôn8äÛÇáR,é%uvr(‹µÉ%$$Hòvttìᥡ"ðã Ãp©'üP ÉIU.—ä8åÎqêÃR¢¶Þ¯êÚ†˜ >4ZµñÝ=*Ú_1¨^7Gª5lŸ>ÐCØA’›ºñ¡ìøç ;´µ·w…:ƒ MÖ˜ )Å£‹³Êr9Îz<ØÀøá“Îú”ùl{Žò\—GtþO-ÓSEË‚—“™®ÕݶyT×6hÑÒ5¦Æ†«ËC¤&çòBÉSúñöûM¿cÊCš5úÚ˜<÷hwyxyÏ/ôÖ‘7‚÷«k×iî¥×‡|| ˆ‚ êìäP‹!‡Þº’´§££ÃËKBAà ΆÑ;üà¦"·PB±|xo×m|7þ?`Ö‘jÕƒ·ÌVcSË€w‡Ávèöèsok{IYÔÛvhimSËÉ“jœTKëÉ®pÃIµµ·sc‹«%IŸ»$[I‰‰g=ŽÀCrb²¦3#zõ:]³&ÏT‡p¿W¿Y¤Õo=.\]2¬.]‘5i@®¥onü²vT½kj¬ÝâÐòYÏ*Ûn˜>¾-)E³§*)19ìçÍ.5~îúCðAœ9¹óõìWƒ Jn@”øÔpè9ÔÅËÄ&KrIòtttü•—„‚À@3 c²zº?L¢"ˆ”3Cî§ì)–snߨPõñ5ÆP‚ªŸþß;«Ê[—¯Áı#õà¢ÙÊÉL—$mÞq nCŽT«ýîW5鲑ƒê}R|¨J>½1ªÇ\|Ý4]u¥[MVnTQ–7j¸ÒRm}>ŽÀƒ®k¬Òƒ[¾cêž´âîùa›GcS@‹–®1Õ½&\]¦äL—-)%ê×R(ÁIrgäiyÁ³!Íá"ÇÅr;džýÜÚÚOêãÿÙ¡“í'£RËŸ}øˆŠ<Û‚¿Û« ›«Ïç7 ÊÊÕrðx<…ñz ã%eIú £££™—„‚ÀÀ aF®z:?\OEiYN{gÂpjœ;»ÏDcS@ŸÕ7©¶Þ3óÞ¼ã€Þûøˆš[âcxNfº–,˜¡/íó\â-ô“™®G¿{­òFe Ê÷E´º<¤X-º~ÖxM7š›ÑãRf†ýœÏý¡‚^¸ÿëù<ë±Ì”,}.óʈŸÏ5kòt´¡<èq+^Ö÷ò@wyȶç(ÏuùÀünúdƒ¾¿iéñóÆÜ¨ÅãÒ®>霋þCQÙP¦J×—EžmúÙ‡=îñÙÏëÚ1_×!ß^}T³]Çü„M[Õr( '”+)…î < B†a¸Ô|èîþà¤*ˆ»Í¢K §Æ¹³äÎq)Ëe—;Ç©–Ö6yëU[ߤ¶ööŸçgõ~mÞq@Eû+b¶–ŽT«n,˜¨¯ÏžpÎO‘—â+ô0qìH=ú½kÏ{>ñ.”O©ï¯a驺í]¥‘ÙÜÚÂ…Â’ôÀSUr¸*¨ýöx¢ÓuÀl—‡9Óóõà-³Ã:—EKרº¶!èqñÞåA’–m»O/î~Úôøñ„®È2ßðËaIÓ„ì©a?¯¶ö“*©Ù©@[t>äýÎw¾%oSõy·IJJRrr²,‹’““•j³«]mÜà->u Õr¨l'˜&é$Ý@8x جžðÃ$*‚h»Ü%wŽSÃ3R5<#Uév‹ZZ~aa,útè­øP•}îíˆ.²Õ7Îг'škº¥µM-'Oª¥µM­'Û:n=©¶ö•VzµrÝöˆt¹rŒ¡¿Ÿ3E©6 7–(KJLÔÈìŒ †$s‡å³~%·sìY_丸ÏÇÃmâ ÃÕÐâ ú~µú±›Ãb2â W—‡QénJÏk¬>P§onü²ö/15>5ˆÔù×ø=*­;•:®=°Jë¼|êg«Õz*àœœ,«ÕÊ ¡\=] )@ÿxb ÃÈUO÷‡Ñý$3#UÆ0‡\i6æP†Ã¦.û€Ìå³z¿ÞÛuDÙW‘Eêýá6†é¦¯LÖŒ‰¹¦76´â•-Ú^RS¯óı#µdÁ åÊŠÛkµ)ЪúÍj ´ª¥µMM _#U5¾°†R¬ýýœ)Ÿgpói©VÎ&«%©_Û?þʽ³#¸ÅÝ÷_µLÓŒ™g=žœ˜¬)#¦+)19¢çødÑ2=U´,èqÜ2[s§ç‡u.Ùå!Zõ>—}Þb}m­ù. ÓŒ™ºÿªe!ÿ„ì©ér±Ï[¬ú–È}}m Fµÿ‹þP±ñTÈ@ÔÓÅ¡Œr˜Cà`ˆ3 £;øpƒ$7Á@¡ ‡U#†94Âe׈aÙú¹È8ŠöWhOé1í=â‰ø±†¥§jê¸ÑúzÁ]6:;,ûÜ^ò‰V®Ûnj±p89R­Z²`fØBGK[{»¼u'T[ï7ݤªÆ§ÿ÷ÎǪòÖ‡4—/N£9Óóéê0’e OW–ËÔ¸Õ¯íîÑIDAToiõ[EA9ßbýht¨ÔiÒ‹Á“r2Óµú±›Ã:—¡ÜåA’^(yJ?Þ~¿éñ‹Çÿ@óÆ,0=>3%KŸË¼2ìçåomTIÍÎ÷ÓÔæ×Ñe:ê/Wm FGýå:\¿ŸbA¹º’ =O%œÒÕý¡;q=A¬°Y’4ÂåPö0»F¸ìr:l="#âÇÝSêÑ‘£^•VzC^´.uFf;•7*Kò ]u¥;"‹Ø›úí–Ýz£°D'šZ¢úZ9R­º±`¢¾>{‚©N±Àר¬Šê:µµ·‡e›wÐ{ ªÛCŠÕ¢ñy†æNÏ×° ;7(KJLTö0‡²\%%&=¾øP•|zcPcÜyZ^ðlŸÏE«ëÀ[n׺/=îñ»ækÒe#Ã;—§6ªäpUÐãC—IúÞÛ7겦ÆÚ--Ÿõ¬²íæ;Âäg^©a)áïÌSî;¬c'Žö{ûÃõûTðª6P£Ã ûºº8x¹I!–lUOÀaå?8'º? Öe8lrÚm="½óßB”VzÕ8©*¯OÍV­ñsÛÌ »†eØ•j³hdV†Ff;•j³È™–¢,—#ja€Í;è-%:rôxD““™®E×MÓŒ‰¹qt:»2ÔÔû~›­Ú{Ä£¿ìûôœ¯Ew f|ÞEºrŒAG‡jС[umƒ-]ô¸ççm8gw‚l{Žò\—Gôü÷y‹õµµSƒ7gz¾¼evXçb&4" ž.õ:]³&O ->SãÏ é[RŠ&fO {裭ý¤>þŸ:Ù~ò´Ç 6 ŽÐÅ Ê< _èþ€xí Ä…$%&*3#UY®4Y-I2‡ÒJ¯¶—”i[É'a ?äd¦kÆÄ\Íž¯¼QYqÝø›Uv¬6*Çú¬Þ¯Úzÿ©ŸCýâY¸‚½-ZºFÕµ A¹cÊCš5úÚs>?%gºlI)­Åu¯^û—=nõc7+'3=¬s1ÛåañøhÞ˜!;º<|PµUÿ°ñoMµÛE$Byß×aß>í;^L°ñ„.ˆÀL¡ûâ‘Í’¤.‡²‡ÙåtØ4Âe׈aÙ"Bp¦¥hXº]δ”˜ªCcS@%‡ªTZy\¥G½òo¸`"'3]9™éÊ5\y£²4鲑a_Ü<Ðö—U«¥µ7Êbµ$)'3]™ö°ïûÑçÞÖö’² ÆL3fêþ«–óù «KWdMŠhMÖX¥·|'èq‹æMӢ릅u.f»ëWr;Çšo&dÓÐZ¯C¾½:X·G­õÚé}_Çüòø+¹é ^ÐÅ †x@ÈÎèþP ÉIUoFgg(Ãaí B s(£+ «%Ii©6e8R”f·†í“⣭±) êã C®ë@ñ¡*ÞCDf†]Ã2R•–j‹Ø16ï8 ¯l zÜóó6ÈnI;çóù™WjXJäÞ›õ:]³&O -¾ Æ9R­ZýØÍa¯©™NÒ…»eôG,tyÌwÝ$wFž–ªÙ®cþ Uù+uÈ·G­ Üd6¨3ä@€CàagFzº?L¢"ˆg›œv›FH—Íš¬.{çc›ÒR­JµY”b³(-Õ&k„:E :< n©6Ë© C4ÂHÕµ Z´tMÐãä/ÖÂü[Ïù|4á/ÛvŸ^ÜýtÐãî¸q†nœ=1¬s1 u¡·l{Žò\—èµ[ÙP¦ë^Ÿt¥¿×Ôù4µù•œ”¬ú“Ÿé؉ íô¾¯ÆVŸùöqSAÜÿÚWOÀa=åˆ]Q†a¸tz÷7UÁ`q¹;Kv›Enél§CY.»Ü9NÙS,'ü´FMV 1ˆX-Ir:R4,îT[ôß—w,]GŽjLVjŽž™óêy·¹Èq±Üα›weC™¾¸&øýçd¦kõc7‡}>f»<üëŒ'ÎÙ Sr¦Ë–”2 ×òÚ«ôà–|Ö¯Î{ͮߧ¦6¿Žž(×Q™šÚü:\¿Ÿ›Ÿ¤õê 9”Q€ø@àQeF®N@8© #ÂñÇר¬²cµ"Î¥Ú,JKµXÈ¡·7¶”è—olzÜSÒ¬Ñמw›+†OR†Í±¹sã—µ£êÝàçC]¦3uÿUËB>~,ty¤ï½}£Þ)ÛhîwÒð ºyÂ÷Tðª6PÓùÕR£ÊåjnósãÀ`µU]!dz‹rÄ'P†a¨'ü0‹Š`(¸Ü%IçÎ’=Å"wŽKÙN»²\vŠ3À*ªëT[Ïâßx’”˜¨4»Ui©6e8Rdµ$ÅÌܪk´héš Çõ§Ëƒ-)ESr¦Glî›?Ù ïoZô8GªU«»Yi©¶°ÎÇl—‡§¿²FÙv#äãÇB—‡ú@®Y“§†_ßï…¤¤Ó¾’““• «ÕÊCE±:;8¬÷x<…”`p ð€˜a†K=á‡I“¨ †š¬®àCw7wŽ«ë;Ý!¢¡­½]¥•ÇÕh¥1ª;àj³È‘j ûÂúp{ô¹·µ½¤,èqýéò0*Ý­Qé¹›û5kòt´¡<èq_/˜ % f†u.«ß,Òê·Š‚7oÌZ<þ‡!?Vº<¼òוú·÷ÅbQBB‚’““•˜˜¨äädnŠÊÕpPg‡:J0øx@Ì2 #W§ ÜTCÝ%§‚ßǹ³%‰@Dµ´¶éà§5jko4çäLKQZªM)¶ÎEÑ'šZÔÖÖ®¦@«›Zbzîi©á†›E©]_ñd{É'zô¹MA³[zæ+¯ÊnI;ïv³§^p³ÖX¥·|ÇÔØ•/TÞ¨¬°Í¥±) EK×èD׫ÝâÐóó6†eÑèòð‘÷ýÎï5Û%I;»~þ¸ë;0ÄùÔnPgÀ¡Œ’ ~7z nèúî¤*ÀéÎ DtwˆÈîêþñ56«ìXmÜŸGf†]9™é²Z’λ]Kk›ZNž<-Ñr²M-­mQ™gªÍ¢¤Ä„ÎïI‰r¤ZeMN¾à¼ãÅ¢¥kT]Ûô¸ù‹µ0ÿÖónã°¤iBöÔˆÍÝl—‡1׊»ç‡µÇÊuÛôÛÂÝAëO·ŒþµËCCk½ùöª¡Õ§Cu{%h.ôëX]áuvQ€¡‡Àâ–a“uzÀduì6‹ÜFç[¦»KÄ8wꥪƧšºq9w«%I£s\a[lÞØ$µµu¨¹¥õ´çº}I³Ÿ}|Gªµçù0.†e›wЊW¶˜»|Ö¯ävŽ=ï6¹Î<ŽQ™{(]æLÏ׃·ÌÛ\ªk´h隠ǹ3ò´¼àÙ°Ìá|]º»3¬Û£ÆÖzóW¨Ê_©cþ yü•ÜTþÙª®. xÀ Bî.}…"º;G míí*­<~ÎÅü±Ê™–¢Ñ9.%%&r1LjƦ€-]£M-AíÏbýäÄdMÈžzÎ…ø¡2ÛåA’¸e¶æNÏÛ\}îmm/) z\‚#çÒÔæ×ÑÇôµÖÊ–œr*ÌÐØêÓ!ß>.rÀ¼­êéàPH9p&´@‘c·YtÉ©0Dggˆl§CY.ûiÅ»¦@«~Z7ó½8Û©,—ƒ 4­~³H«ß*25vAþb-Ì¿õ¼ÛdX]º"kRDæJ—Izü®ùštÙȰ̥øP•|zcÐãæ¹Q‹Çÿð¬Ç×w†j^Õ:ß뇺«QmÀËÅ „…À† @ôe9í§BÝÝ!ì)¹s\’¤ì^ÏǪêÚyŽ7Äô“5:Ç%gZ ] [´tªkÍ]KýéPë̓ᑹsã—µ£ê]Sc©V­¸k¾òFeE¥ŽIIIJJJ’$%''+11Q–D‹®Èé „d¢Š€BBàC ¶\ÒˆNï1Î-I]A‰y›–VzÕØÔ“uKJLTÞ¨áJµY¸ˆbÜæ´â•-¦Æº3òôÈÌ'd·¤s›äÄdM1]I‰ÉaŸû>o±¾¶vjHûxà–Ùš;=ßÔØòjŸüÍ­’¤ßýy¿>ØS.I²X:¯ûÄÄD%''s‘Ë'i—8 Œ<]Î@L–ä¦*@l²Û,ºÄè ?ôzºJ„ª¥µM?­Q[{{LÕ!ÕfÑèa‡8òÀSUr¸ÊÔØ/ž«%S>ï6™)Yú\æ•™û²m÷éÅÝO‡´Eó¦iÁ—'©¼Úwê±ýå5§þ]îñÉè 6|Úëßb’O]áuvQ„à ÃÈÕé @qì|!‰Îî®S?÷”ð56«ìXmÌœOªÍ¢¼QÕ”˜È‹GJ+½Zò“µ¦Çß1å!Í}íy·ÉϼRÃR²Â>÷ú@®[;UG:»+$$$œê° IIIIJJJ:õóùž—ÊÕnØ%ˆ@?†áÒéˆIT.é @´´œTëÉv‘~Úó#†9d³ô,æÎpØätØ"6Ÿ´T«rGfvˆS+×mÓo w›¿|Ö¯ävŽ=çóɉɚ2bº’“Ï»Ÿcþ óWžú¹¡Õ§Cu{OÛf§÷ýÓ~þøŒŸ jÅ:=àPFIm€†Q žÄ,*à\²]v¥XN_€~fpB:;<ѳm†$)3îѽºQ þ46´dùZU×65.99Y‰‰‰²%§è{“î—Ë–©¦6¿Žž(?kÛ²‡”šì8í1 .`«N8ÔQ 4@†1Y=ˆÉ’ÜT@$]îÎ:çsãÎó\çóÙý:Æ…ö3””Wûäon½àvûËkÎû|Ñþ*•õöùœÕj¥Ð"ͧÎpC¡¤]§’ x"È0Œ\u º¾ÓÀqIŽSöKLÎí¯å^^ CI±NïÞPFI<QfFz’œTaâSW°A=ê( â`€uu(Pg‚.F±z»<Ï.J€Á‚Àƒºº@tw€˜,ÉMU†<Ÿº‚ ’ =O!%À`Fàˆ†a¸Ô~èþî¤2ƒÚVÞ½¡Œ’`(!ðÄ)Ã0&«3øÐý5‹ªÄ­bnØEI0Ôx‘®DzB“¨ @Ìé7ìRg¸¡’g#ð r†aèôN„ ¢§\=á†Buê( pa€!ˆ@DnˆÀI„ ‚T,©L„€ˆ!ðàœz… r»¾Ï¢*`*VOç†]§’‘Gà@P Ãèî‘+© ëßN*‰­ê 7”n!3 #W§ &KrSÃÊ;::޶µµí?yòdY 8ÜÜÜü‡ŽŽ/¥bcFzºAL–4‹ª€°UR™z:7ìòx:DHRA×÷YTÜÖ®ï»$ÕõúN ú@à†Ã0º¹êéÑŽ æf(ëúªóx<»(Àà4½B}uŠÈ•ä¦J†Ÿ:C ’TØõ½L„ â<L1 £ ëŸ¹]_Áñ£w¡»+C]ïÇ<Oe€CàQ½‚½;Fäª'$1Y’“JƒÞ!†Ó ]?ËãñR&ˆ1£W8Bêéqæ¿gQ)`H)VWXA½‚ "ă@Ü:# 1Y]$¤Ó;HH„$€XÑ;¼PÖõ%Þ@ÀcF®NCôú·KÁ‰n¹’ÜT 8M¹z‚ ÒéÁéô. ug%˜Aà€~ê#,qf@B:½ÓD÷6“¨bDï Ý Ïøy×Û”y<ž2Jˆ6D™a}%¤Ó»Mt;×¶’4‹jZ[Ïñø™a„n…}mëñxê(% ^x`2 £à›œÙ‰"ÔíÎë-Ê%•™[Ží<O!W*çöÿèó£GFÊß¿IEND®B`‚kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/images/kubectl-logo-medium.png000066400000000000000000003425371476411216400312770ustar00rootroot00000000000000‰PNG  IHDR,%^rß½gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿÿÿ ½§“ pHYs.#.#x¥?vtIMEã 4m7å€IDATxÚìuœÇ™÷¿UÝ=<˼Z­˜Á¶,™!fN.vl‡œä‚w¼a¸ä.L— ƒƒtÀÇqlÇÌ–I–Å,­¤]i™‡êý£zfgwgId9§çã±f{ª«««ªýð#8A'¨®®Î~@HWJ•å@UÞ§¨J" D€0,Àð?"ï®ÿq€4’ÀÐt]@;Ж÷éð÷ ¿Z[[_ê);A/‰Ãï⽜(˜L4àTÓ€F`0¨CƒS)B@ BÇ‚<ÀFÛ Ðƒ®À>`ÿÙôOÁäL)•ý* ĉÇáx§+ôOLyà@sJÀ|`¡ÿïL (AsGï¥ôÓòsÅгïŸôe4>ø—‚¡?'µ-m Í‘í¶›-À. Í‘©‘ –Vó€3ÑÜ& Éï÷x§tbUþ‰È(Ä€`p2°˜ Ôø¿É‚(…Bé}0iHÓ´V3Ä „0ƒ!Ì@#Ä0–…0 ¤4‡í.åºxž‹çظ¶k§q2iœt'“ÂI§p3i;ƒçdð\å¹9pÑ"4°&)4ˆí6/ëÐ Ö ¸Íaþ;pZÌL«?é€u|Ò‰UySAsO'§§sÐ\•9êD¥PþBH¤ia…£qÂÅeDJʉ–V)«$RRA¸¸”P¼„`4ŽŽ`ØViZHÃDBHáËÈkjnL¡<åyx®ƒç8¸N'ÂN%È$H ô‘ìí&ÙÓÉ`w;ƒ]í$z:Höv‘êïÅNâÚ<Ï|0ÈZÿÕ„®ÕJ©çJKKwlذá|Ó4ÏgHÜí¾ 8XÇ'X•—ù %Ñ:¦eÀ9h±fZ5\Ï4 œÀ0M¬p”HI9ñÊZŠk§ëOõ4b5„KÊF‹°‚!¤i!¤‘yœWVôÿœœ(˜O"ûŸÿgVLô9©ìõ|`sÒi2É’}Ý v¶Ñ×ÖLï½ôØKk ƒÝí¤úpí Jy>ˆÉB ¦€>¥ÔŽx<¾þ’K.i»îºëR«V­rb±Ø‹À=@æ`ŸtbUŽs¡$Ÿœ\„æ¤f¢­sC”() ¬p”hY%%u3(oœKyã\Jê‰UTŒcBHCBƒZžXx\QNÇ%r ¦<×É`°»ÞƒûèÚ»“ΦmtïßMû bŽ­»ÃÌrÛ0ŒÖP(´1“ÉÜcÛö#RÊ­hñ„5ò8£€uR:¸ RÓ!æ)ÏC)…4$ÁhEÕõT̘OÕÜ%TÌœOqM¡¢RÌ@!¤L^¾òùeM#EB×¶Iö3Ðq€Î¦´íØ@ûÎÍt·ì!ÙÛ…kÛèSäHÑU¡ðk¢ù'Àë8¡€uQžÒ¼8¸ 8­@õ| À E(ª¬£rö"jžLõÜÅ×N'+Fš¦–Þþ‰ÀiÒä˜å¹d’ úÛб{ 6¿@ë¶õô´ì!5Їò¼QÜCàõðwà>`'¾ñx½4t°^bÊ㦢heù+ÑÜÔ\´2 §¬RŠS>}u‹WP·øT*fÌ#RRaY¾nûÿ @MD9xžGz žæ=Øüû7H\·ñ<¦î>06R Û;|Y’f•) ›li`Ïž=ìñ)v=ý­ÛÖ“è!ëL›G)àyàV4çÕÌ qñ¨Ð À:Š”TÀÀ›€3ÈsEPž¢¥UL[¶Š9g^BÝâDJ*@ˆ£ÂI) 4(™_À’‚¶„ËÎÞ î ÜÊ‘à©!WˆLb€ö][صúv?û=-{pm{$×åÛ€ßùŸ€w¸Ž¬£@y@Uü ðf´ºätS† ¼q³Ï¸˜Y§_HYÃlÌ@à˜è¤–U™7Q€ãÁ³­):Sî‰ á“2çÓæAH‰RÙûÂlìl~ô`ÿH]—B{Ößü§x¸ŽØŸGò€ª¸øW´®Ê€!±/‰Q·èdæŸwÓO9›hY%0¤»:4·$À¼ $lų­)úmïÿĆÈZ sœQž×¾;“fǚ稚>ƒ²šÚQ/!$B Ò‰Z·®cÛ£wÑ´æ1;Û ½hö+aÜåÉЯCɽÛò¢âaÐÿ…ýyÔ)¨ÊÑÖ¾w ƒjs@…ò•ÒxÊÙ,¸à•Ô-ZA0{É,|)¨‹™˜:SÝ)÷˜^?Ëd­ ‰½ù¿ΦR’è룳yƒ½½)†#Ä” ôt³õ™Õì|q ¯þ¦aþÂ\ÌâH’R’N¥hkÚMÓÚ§ÙöØÝôíÝ‚ð2…®|Ô¯Ÿ¢ÅÆÀut°ƒò€*Žvò|7° ß= T‘ÒJfv /|Õs—bƒú!x‰­|G …Rƒ´6í¡¯£Ši ÔÌœ=fC@}Ì"l  :ôe‘UŠmÏ?Ëê¿ÝNû¾½8vHÓÄ0Mš»²ÓiŠ+«xÃ~Ž’ªêÂ/!Ø»iOÞþ'ìÜcgt;;…áôbÚ}H/]h»ÿ~ŽÎíuª82¿‹ÿ›äƒU8xÚê‚á@5ûÌ‹Y|Ñ«©œ½iZ(ÏÅs-73½o+!½l^ý8ÝÝxžG_G%UÕDâE£ÀAÅAƒ…eŠ’çÛRLÕ°)¤dï¦ ÜóÓ[è¼L'θÃ2‘†9f_íûör÷O~@gsópÅ»´ð8f1†Ý‹åhàRJey&ð´KË÷ßWWWwÁ àš ¬)RžÃçR4P] Z™®<"ÅeÌ>ãb_úªæ,B¨”w|ÕKK‚îÖôuv"ý€æ@$‚aZc:ld\EÒñÀôg¼CbL°wóF¬F&´ÐÁÞÁh+d°·å …?êKìÜA×ûRJ Ç”U‘êíÄ´{‰]’=m>pI,¾®¯÷VWW§O€Öøt°&I#êoÞ…Žñ@y.HŒ™+Ïg镯£vÁr¤xY•NÛwôz/©¬"^Z†NQV[Oã¢ÅB¡‚"²lçÛÒX X‡xåŠú‚áédB÷-Ò0ˆ—•3ïÔU,<ý,"EE<ß=lþ™1uWJ)Jkjˆ3ÐÓíT ¥$Z\Âì“NfñYçR\YÅæÕOðìÝçì7¾‘Þ¦lºÿ/ô·µdC…L4‡~*:qà׫««×qB¿5&ÐaM‚|° —A‡ÐHЖ=ò¨_ºŠ“®y# ËÏÀ †_v •¥ !‚”«ÈEǬÔà®ã‰! cBÃÃsÁ¡“ëØìXó<ÍÛ·¢”"/¢¸²Šê3)«­Ã0tÝŒÁ¾VßñN¾èRJ«k ŽMy{6¬cÏÆõ¸ŽC8£¸¢’ªÆ™TÔOô€"“NñäíbÞÊÓ¨›5—öÝ›Y÷÷ß²ýñ{Höu#å0mð=´b¾Nˆ‰#é`Cy\Õ|àCÀèùM\1c>˯ysϺ”P¼Ï=²ÞèÇŠ·$K+‚Ä-Á€­ØÐ™¦7st\² ö£a!͹, ó£Ò$¥ô½”Ÿ^fÈÝ$¯<×)G8ü:Yo÷¬~Jˆ}ù–P‘u‡xŽCóÆçX{û/hzá ÜL:ßsÞž¾<8'@kˆNV‘AáF4W5/{Py.‘ÒJ_|-K.¿¢êi/ûØ>¥`F‘Å’ŠJiOïM]vöؼܒo&úúHô÷ŽÅˆ—%„4È$úÙñä}¬½ý´ïÞ¢µø_ß@ˆ}(ï·Å±+Ûô²¡<°Z| ø Zo¥Å?ÓbÖipÞ;ÿƒ…¾ŠP¬xLåìËŠ|¦¤"dbI-6õ9$\õ²y« !H ²þ±‡Ù·eûö//'¿ÔCMJaX\p»aZô4ïÁN jÎO[œOÎu؋żÁÁÁ—zä/)¬<òÁ* Ü„Ö%œ ˜YÑ¢tÚ,θé}¬zí¿SR7#WXáŸrÝi—®”Çž>›Þôñ Ä…¸&!©~ömÙŒmgp‡²šZâeåZ'!&¬À3浕çÎM)B±b–ŸNÍüe$º;èkmF)¡/P\‰vJ^‹Åúc±ÿWëåòò<ª”ÇUÍ> ¼–¬O•ça…ÂÌ;÷ N¹öm”5ÌÊ™Áÿ)¯º×‘Û¾û‘ÒWÙ™4v*E(©´Æó$ýŸ,¬ t6…Ï¡cÿr Ûòƹ¬¼áÌ>óÌ@ð˜ÆûýSôwvÒu …ÚY³ D"‡ öBÒÉ$›W?A_G; 1cɲ‚í²ŽšCõ‡ÓžëÙýâZ ËdÑgSÙ0€Á¾^š6®§­i™tš@0H8' cgÒܽ‹íkže\ý®÷0ž‚/ë2 göoݾžg~÷ö<÷¨6ˆœR¾øÚé´^ZK¢Xqàh㔇N+ý ŽBõ¡ÿ³~Xy\U Úô}èÒì¹¼Tóν‚S¯eõ³ð”wdÀ*[!+û5ò·\ƒ·“l蜱ÎÏûcü¯GœD?„ܳ—¦/ŒSVSW ˜xô‰£ö¹Ðó`_?]I%iß¿Ÿº¹ ý°r\¡z =ÏÅ0 „a ›(¥}í8NDZéïꤪq-;¶óÀ¯ÿ—æm[ql{èºh.K‘MœEeåÃwOT„ êb&®§htèKÄö×XktÕ¢ŠYKxÅ{¾ÄÆüuû%‰î¶,·U |í»õI`suuõK­/FGQhwŸ™hƒUæp:-Dÿ'+¬_®&¨ì¹Ä«XqÝ;XpþÕ˜ÁvvóŽG¹R}zãeAeä÷±@)¿æßPµc9ñuóÈણíø9Uò(«k$-"/%“v«7iF(¯ŸIW;åÓf‘I{ˆLö¹¾â½¦/’ \TÄŒÅˉù®!åõ ôvt`”ÕÖ“I&yô¶ß²gÃz ÓÓƒ] P(]8–±_C°°,@IP¯a< yö@’Œ›-&;Ô§RC.ÊS¹ý!dˆE—ßDù¬¥¬¹íûÜü\VL5W£E°OwVWW»/!huëÑ)”4‡Õw4.ô°ò ‘^|X äô 'ŸË)¯¹™ŠY‹°EÆIO®ãC`]”R~Ivox™xÿw)%†˜P`HÁüò å“Þ”ËÖŽéã(_0%–ašÌ\vŠæžÌ\z±ìŒкg7víDAWëAÌ@„KNVö¾¸j:‹Î*CÁXŒ¾®j“ŸI4Ï+ZRJyÝ4Rƒ´5íÆu\ú;;†AýÜ…ÌXzòÈ|ñÇ)%]M[yîwßfÿ‹Oä$Ÿž>< ÿœa=ÿô{:¬££â/F×e ~Ù™œúÚ÷Q>cÁ1µN°„ºZ³œX—eJelWáü•È–£Ï×é‹ós©brºFUÀHp„Š”âÀîítìk¢´¦ŽÚYóÆå¦s×—’T_7ëîø›ïÿN:™¯ïÜŽÌø ðR굎 ýS;ŽæÕÀO€3¡|/㽆ÓÞøaŠj¦sw‘}( \W4ML+0)°]0ÁöÔ?{XOë:[öa˜V0tˆ½L}2†á‡àk7œÛ™˜„ ^ZNeà Š+«'æó®cÃÔ,ZI¤¬’Î]›É$²@ZŠ~)§µ±XÌùgr2ý§¬<ÿªÑ>+sAD¨¨”¯¹™å¯|+Hü% ­‘†¡l¥D†a"-ô0 sÊNwùΙÇcüÜdɱm¶=û$vl¥(¯k8üN&)…ãØ¸Ž3VÙûqi²/¥Bç•Ï\HùŒtïÛN¢»={Ý0ºœ\x&‹¥þY¼ãÿ)+/Ì¿_% èR\;ƒ3ÞòqæœsÂ0_ru!%RH郗˜ÚfÏ'¥N&ç¸ _¦ ¥ç‚á(•ÓgŠÆ^ê!KJ)\;ã‹BÈáÖÆ îUæÖ}’çR ýOIÝtê—¬d°³…žæ=ÙF&: RÚC~àŸ´^ž;zòÁ*ж~¿ ò<ªæ-çô›>B圥ÿ”ëYÀB ¾lK“¯®öÓÄŒ$×¶sêKMJ)ÜLOéøÂÉŠòJ)Ú÷í¡u·Î _T^Iåô™•W"„ôÁO aJ ÿ_ih Ò±zª„4Htwðøÿ~ƒõwÿÉ÷ŽÏ­ÿ]À{ðòVÆÿSqX>X• s ½ÍeRL?õ|Îzë§(›>ÿŸ¬`(ƒ¦4§.Nyž‹ç¸(¥&ÍY)Ïãé;ÿ@Iuí1á’58H_E%Fý¦ç]‹õ“+![V?Æý¿¸…íϯfïÆuìZû»^|žþ®âåå”U—-‰ E,¬€Ä´$†!5`å@Kƒ{ eúÉg ÉÁ­ëqLvÌNA;t¶½œ9­ Àª®®&‹[ø::…±‰R)™{Þ+9ý¦«¨=<°òAÀuœcò`Ú-Á¡ò<\;ƒkÛZwƒ®@sTïOJzZðøŸ~N3séÉGå~…ÐzE¸™ɾRýÝØé”öѲÕä#kŽß;v:Éãü5Íû|‘PÇ?¦8¸s{7­#“@HEoG;}„žª¦OgA¦eQ¿dá¢l~;•ÌÎÏttºš€æ—+h½ìGó,µÀ7×µš‹.}-'½ú± Á*û°kïå¡H}á‹%™TŠ-O?Fj ŸUW¾*§kÑÕïfÙô\Ÿúß|&`(fŒ\ˆFÎSº@ìàñJ®ckçÖ¬n²V®C l…DoÏÜõº;Ù½îyöoÛÄôEËŽ ×ìçe÷\—þŽn[Çþ Ïжs#}m8™ †e-)§zÖBO>‡úE§)­¦RW‡ö„sU‚†‚©² ×ÓÖÊc·ýÕ»Ó²¸îCSDÎõ¬Ò49éš×**áá|‰þöƒY®ïd´gü»€ÇŽƒÄ)Ó˰òÀjðàU Í †X~Í[YråM:Ë‚š¬zÚ[iZÿ-ûq3$‡±3)îÚÉþ-9íª«(©Œú@‡Ç2e`.ˆÙ· çËS¸®Âu=\ÛÃq¼<ê#OÙ·»N5:qQÓ,IS—ÂVæêhŒOJ’ýýìߺ ÞÏþ­›B’๻o§rZcÞÃ(÷¯+9'û{hÙô<ÛŸ¸›¦uOÓÓ3ˆªÒÙP±¬(8I:°÷Éõ¬yàn*«+XxÞÕ,8ïJjuÊåI—iXuåµ8™ vn%“J çÏ¥œLÏuY|ÖYÌ\6I`öçaá+®"‰qß·?CoKSÖ3~:gü;‡^n õ²ÕÊŽ«ï£˜QJG9ùºcÑ%7´æ[Y Sb˜’]k_à_ÿœ®ÍÃ7…ŸË †Þƒ—½ímœùª9&õ•×ñȤÒIïÇ*Ï£·£ö}{@)¦Í_L¸¨ø°õB¦%±RjŽÕ¶=Ûç(„Àuv­}–¼‡Ö¦Ý¸v!µß¯ô³%œzù«8íªk§¬€Ò@)Þƒûعú^6?ò76íÅŽ4@í*¨\ ‘J0ŒzTÜ €æ§ÍOP—,¾à•,¾ðZJëgêüî“àèÓÉ$íûvÓÞ´›d'ÒÄËÊP v½¸–¦1-‹ë?úQæ¯:mÜ=W¨0®’­O>Î#ßÿ =Í;óókíDsZ÷ÃËGÿ²¬`õ=àЩ?‚‘"VÜø^æ_ðj„0B!¤À0dœ Sÿ-¤ölìíáןý,ÍÛ¶k²åä³ìúåo;g\óÊc^Õu<û2Ø™#s]¥û6¯§yÛ&_Õ¸x93–ž|ÈÜŠaHB“@ØDJ1ìZé¤CrÀ „ÀN¥xö®?³î¡{ɤS9½¡ašÌ?ílŸD¢¯‡ä@?']x9P„‰Q ‰“IÓ¶s#›ºm« §ÏAÕœ Ó΄¢é`ñ={ÇéÊÈd4?‰hz’¨Ç¢W\Ã’‹^CÙ´Y—¯r0 I¼$€aÊðv<È­ŸÿBJnúÌg‰».ÅIEÐÀUŠÖ¤KÒ¹ ôÚì_ÿXE‹Yùú÷³àÿ‚0´EÅ·ªh‹J… •HÐßÕ•§¼U9MJIy}=Õ3fÐÛÞAóöm~eœcO†)‰èïIã:‡?Ïqè:ÐŒÑÖ$+$R\rH}I)„MBÃÍé!E,?§UzbN)Ö=ô^¸ïïxž—+åyÌ=õ λáÍX¡(m¥ôO»?_?e§4½ð ëÿñ;v½ð4 Y —².Ç—é@M⥳p)̹ 5ílº›Ÿâ‰;ÿÎÆÿÊ¢ó¯fÉÅ×SÖ0;—V¹Ð}*¥ðD6¿™Bù/Â’êjœ~:v*5.X) l—XÄ-©óuY’ =­Zpªæ-çœw|†ÇòY:wo΂â,ààmÀ#/ñðee%Ì«JtÕÜkAƒU(VÌ™où0˯¸Žh7—ý!Ÿ"±áØðàçÉZKQ“š°« iÐá`Âe°7M:5”ÂHHƒŽ]yüGÿEçž-ùâá:t“àø_6€•çÁþàß©”"‰rÞ;>Êò«oœÐä; Eêû.`s3_BçS¥ ¿;uÄtY‡2oB"q‹Pdâì#)•pì;1¢‚þ®NîüþÓÖ´ i˜Ô̚˯¼ióÿðf*1HÓÚÇY{ç/ÙóÂØé”¶ò5@ "P¾ÊA¸ÂŸƒ£\-ÏÀžû(²’,<÷r–^r3ækÅ¿/ÒJC/ aZ‡îãgI?a ë‘´IŒÎ–+¤AûŽu<öÃÿ¢{ߎ|cÅj4hm…ã´^€åƒU]1䣀¥” …9ç_?À)¯¾éÐ8«—1 ô¦I'/ÝðáP4 :X$2¤a$wmgÏú(*¯dÆÒ“‰–”Žù¢Èúi¥úhZó/Þýš¶lÅ.Y Å3Á"míó\TºzwA÷vð\¨YÓσâFÆÑ®L´< »ï#n°à¬KXzé TÎZ„4L”çbZ’hQð@K)]5ȱ=ÒIg\µ·<Ïã?ü/z4åƒÖ}hÇë}püÖqXy)ß‹¹ £Ò²8óïfÕïÐÞÕG¬†GÿОá¬>äq„GÙÛÝ´$E¥¡)‰ùcìïNOŠ;Ì–’Ïf¡(ÄY 9”v±ë™XwÏïØ¿kNÕˆ×C呃/ƒaÿã¨wÀÀ~¨Zj¿+iEàê‡ÏÃî{‰‰æþ –^z#Õó–a˜„PB ‘³fçU5dò<•óÙó\×Ѿ{“Q( $lØûјqTŸ ¸/*ѲÚ ÕMAçVØs?VïêgÍaÉÅ×1kÕ…D˪uD=¦L/P5q]?þü‡ëx¬ZôD] :ù^ý’\ùñ¯QRß8ù…SЇÿ;ùÃP®;,¢>*¾ÞGy§_s W¼ýÃGHI:‘ §µ•äà . éÔ½ý¬{ôQv¯{‘×þÇ'©˜6í¨YÓIgrŽ—G™"ñá±”îJ—¸Êø"Íá¸_i \‡ö=[Ùxÿmlyüôf¢¨Ù¯D̼Tƒ äˆo•ëÚ¥ó5‡µþ§¨îmˆ“ßECmGq\ù$µž«sjÇíÐò‹aÆ…P»‚%ä‚@$ ž½»aÏÃÈö5TÕ”³è¯dÞÙWRRרse¡ªNÙʧ“IóÜï¾Í¦ܚߤíBô ¼ô uÜyºç¹/|†Xy”5Ìâ‚wŠ’ú“^,!ÉÁA¶>ó žãŒó|  EcšËJ ŽâŒ”Rl{úiVÿío´6íÁN§Bb,¬€Öo$Iö÷S7g‘¢¢£b­T ?´%ó’ƒ@rÀFyjXŽç*ÛÅNû:•ÃØ–ÒÀu2Øö"þñ;¶­~ˆ~ª`ÞÛ´è.G†;x”ÖMÕž6¤·ª^¡uTÁâlz †ËÓ ÿ…®¸.¡b¢|±¶(îújÛ_a×=0ý|¨?C»DIàRž­Ò¹P2oà î‚Ößÿ†5ÿ óϼ„Eü •³aZm :Œkçªú(…rò«ßI¢»Ý«ïͪꯡõÈ[_joøãJ‡•g|º\‘¡”"T\Æ…ïù/f¬8kêo!hÚ°Ö¦¦œW¡½¼+8ýê«9ÿ†™{ê©ܽ‹xY ViÇD)%ÛŸžÛ¿ý-Z÷ìÁö#ç]Û&“N“J$H â:AIU5‹Î8…ıõC;ôQ¸Ž‡ëxÚ¢“ÕG¸ /?Å äž;¥4Ø—ä@†tÒ>.À*KŽí )„“©„C&íâNEù;|±´÷½m³ã³<ñ«ÿáñß|Ÿ½­™…oEœ|3¢ú­“*Å•UÙLDlxâ š·ïà¼ëo@)eà?ý!kî»cDbº¬ A¶ W&™ vö<^ù¾O27?#K^ªÛ¼|ZÙT3‡Ã©¼6>ñ0íûv瀪¸ªšÚÙ󨨟ŽaZ ûöðÂwqæ«n$RT|T­„†‰4t‘<«òeAø¸ãÐòÂgö¾økïü{6m$Sv2,¸Qu²ç—U¤æ„òÿ™XÅ$AjŠýæ€K@ºÕü8ìº5Ð Õ˵e±d6Hó(¸„€Dì}Ùüe!–\ø/,8ïŠkty´É€ìtŠý[7‘N R^ß@Å´FÖýõ§¼ðç[òûØÜ<Ç´^rÀÊ«øŸhvS*Ï£¸n&¼ï«”5/!¤ 0°‚F.²=+6y®ÂSÊ—²>+j( ñ¡LÈrd~¼—_’I01Ûé”QNuÔ‚z. S"…¶¢ÙäÚÉð8ЊóÛ¼»üTXp¢r™ŸQî¤uL¹Ù¶À‰)´J¿äz×á7êàjØùWTßn¨X 3/†²yGÉ{¸ H÷Àþ'M÷S‡%¼’E\;©L¨Zu²MO<Œë8UT²ì¼KPžÃ?ù,;Ÿ¸;ß§ïàõ@뱬—Té>BoõE ¢”"súM¡nɪѓœËÀé’I9¾é|È2åÚYŶ[82©VÜa¬ªÀFj5ÊgÍ•wäBf¤!ˆÄDâ!JŒ+Èr^‚@Ð@Hs¤‚¥§J~Š—Lr]O?ÀÃ?ùT}Ô@ @(5bjÔ$¹¨‘mó~˲hÅsõçBñLDç&Øu'tïÐ.árÍqQ?.?Ñ „òyP&I'ÀÞ§ïbçc"3ØC¼²žpQIΰàIƒL2‰PÙ0ƒ¢ÊjÌ@²Æù´m[K¢«-»gúSûð±v*}I9,°æ–g‡´üUoåäkÿíe]WÏs]íÈé¹CÕ˜¥‡U¯ýXÁðK5´Ã&Ïsq2éÑJO?ƒéá„Ë‚樜IS¡Lj’>l¬€Ä èÜ÷S*‚‘娃ì|ú>þñçyæÎÛèsP§¼±àˆÕûsâÏÑa‚”PÚa¢¶…ûelà9¬¶ ¤ˆMGÔ(]=»4ÇÕ±YÂå~ÎøC¤±3S‚ÒWí*’iAÓê;Ùùø_H÷wë„‚E¥£²œèªÕ£ý‹ª@)n~.Û>ˆ6Ž=t«Ä/ `剂o> ˜Êó(ªžÎYoý$ñê¼üI|)y¤jm)hÚTDëµg#ÊA_ìú;tl+thñŠÊÓ w3r¬=¡t6Õòù¸lÉÞ§ïfç£&ÑÓF¬¬†pqÙÄ附 lú怕VËÐ9Ù«ðËrzãûh8éQK4¦Z‘~¬z$ÄÐc”Í! ò;Ù0¦xœ%ÇQ£RÒŒ»„PØÀ°†ƒª”Â׎¡ÿȪŽgîüæ­£ÊÕ‡Î,MÒ?jb}L~“ÒGMÔvdû±Ûú6[ÌJW¤Q{&¢l1ôï÷k½žÈdK€›†¢Á¯h:˜…¤5ôÉWÝJRN}ÏÝÇŽGþÈ@G3ÑÒJ¢%«LeÉ )©ŸÅ O“êïɾxçû5Ç‚Ë:æ€åWhŽ¡ÐÎýæŸsîÕ,»ú-X¦Áâª0KªÃ4–h(P4èL:¸jdÉðã”üØ0tér]¿Ï8":9¥”.ŸeL½¯TBû3l‚Ó£oŽ­† ƒô`?ÛŸüýøs<{×_4P­øbþk V›õ€y#£;ŸU¸íÐ×#Õ6ï÷ ¹¨ámEÀn«ÓÛˆH ¢æ DùèoÖÀÕ¾NV¤bbà’–nwàY-^MgŽº?¥­l.Ô­"-b4¿ð0Ûú}÷+«"ZVU˜ãRŠHIHœ–õOá¹ Lt:š‡¶£ ZǰFˆ‚:ôÆó(›>—3ÞüqBÅå%Ëj„ ‰%CP2èJºô¥]úÚ[élÙ¯Ó…#ÇrøS§I—.Ÿ?T+>ˆ˜­Tþ1%}ÔQõò¿*Õ¾µç¨¾&D¤̼ªÃõ k* ës\á*Dõišãh“.åûaɼï“CöO¥ü¼\P·ŠŒ,fÿs÷²ûÉÛ±AÊæbøI2…tvöñÐ#ëÙÛÖÀ¤=˜ʹh§ÒµG“Ë:f€å‹‚A´¿Õ… Å›¹ç^Ã’+oÒb`{Šþ´KÆUt§\Zvt¥éJº¤ƒ´îÞë8H!©lhÔa0y$”„ L!°7ï#Lž«°Ó:ø8›uÒu´/šÑ£ÉA;ã ¥P! RVPbZÚ3~Ü2h[0ØÓ§êÇŸãÙ»o§+°DsTó®ÕIó €™þPý£†Ú ñÍÛþ'b›¿ÃÊú$¡®gèÚ¹Qy’ŸßêAêp[Gµõ9®ˆ\e>píºkWÇÖ²Z»‰„JòÞÜ¢póa÷¬´ˆY2jVd÷C¿¦ÿà.ªç.#/fÿ¾6þñçÙ»· ψ ŒVÿN„^s Z÷]/kÀÊã®^…öf(Ï£dÚ,ÎxÓÇ—”[ÐAÛ£uÐáà€C{Âa0£+" G¨›»€’ÊšQ‹1§<È)ujcI—”£þi9­,)ž£plO—ãêb¯̀Р˜ @ù$Jôwö°ùá»yøGY Z¬jî«5P)ò€j*¢žÿûÑôÕ½•Іoñ¥O˜o|ó[\qÅ<ÿÈíìÛÛŒ¨=mL0™´¨7Ñ&ÿÈûP È®òÅÐߢEŶAJmU4CzC ¦kσ:4Èžv¥¦þ{,°Ê @é~ËáŧӺæ´oz 'RÏ#«÷ÐÑÞ›Ëåï…Ê12=˜‰ÖìsX…ζrßÑr(=&€åsWµhQp&€a8õú÷P¿ìŒQlbäÇß-Bâ¥å”ÕÖ-.V–!XR&„LIÂVt :‡§FÊmü—ö„DÇʼŒ§ÙIòIJa Ì€æœFT/øq¯)B tõ²ñ»xàŸåÙ»n§ËZ¬•és_ ‘jFYýŽK×Úþ'Θéò•¯þ7ñxœŠŠ <ÏãοýQþ°ʇ%êMµí¨ö#Ïó+_TL´ÃîÀÁgt¢¿P¹V¸g\2 âõ~>zßâ·í/:Ýsåòáz®ÜŽÝl€t¤Âåô¬¿“ÝÏ?J*4Š S¸Á2¬þ]H7é¯8óÐEYw Ñð¨'ðËã®Þ¬|Ñ ˜yú¥SŽñ˶/tžçAÂö( 8þ÷Ã!åy8¶.ãnX#båËê›rÑǹiB©7sƒÂ÷!—ëMJáWV9ܱiéÁž^¶=þÏÿõ—ìÛ± §ú,ĹïA”-Ê@ œ‚®þ⌜ɗÈõðÒй‰³_y.ÅÅŹà .""Ó$Ò½È`ɤÅÂÑãyÁIè¤ sµ¨X²Q<Õ¿µÿAÔö;u2Á†saÚYZo†´ëB60`Î5º°FN·˜!UxLƒm°ÿ1h~‚e¸™ b×=‹_ë‡éµsC•¤ªVm¾/;‡%À‡ÐÁÑÝa:ª€•V'o„RŠHYK¯z3V8:é„|¹9‡Yp•bCk’΄CÊñ8ÐxÜ•ò<Íý‰ø?©K‘ë@e@çÛʤtõ“B²äݳq„\! ’ýýl{üžûË/Ø»}§ªsÞƒ(Ï•;*ŒæR³Ž>jJm' vsæÌv8aáú¨X°õ8¢^áÆóQçê¹E³ g@ã¨æ‡QM¡ö>¤«á<­ W vÞ¥EöÚ•ú_/?¨|ä¼ù–ÓÁvh~ö?®¯U}¢j ¶àm»ÕµQ¹h˜®2]º”@ïv¬¾¾òŸó×ß;ÒJ`U,¿ 'Z#Œ}E¦ï}茅Ì:û*g/™RöÐx@ ô¥\’ã3°=¶wêªÂ‡‹3Â00°|¿¯CëL„LBkTqLiL+€0ìKÎ}~ lBHœŒÍÎgá©ßÝÂîM[pªÏDœs³æ¨¤éoô¡œTG$^宅Œl+À³1°)**Ö,Jã¸jHlÊv8)Qo‚¶Û:o¬kÑgöXö¹‰Ô çÞˆšv!ªåPpÿã0ó2˜v¶V¼oû³«xÅ¿ìËo°MUóàyZ­:E§›V â3±é¨ƒÏ#*æ‡B™a’Ug`& ´hh¢ófÝìäÒQ,·jáû¾«ÜšÚ‹€Wë{S8ájºÃ‹ñ<­;œˆP4XU!0èH8<Û2HÚ)¼!&ÊM6ʆÒ*¦$³ÍqÇx‹Áþ̰㎭0,…aý™OIº[šyò7ßåÅûï$U²qö×勆D?Ïš×)»a}TA€Ǫ'´úA¢†Çëuuu’ö¤v¢œtžw¤ÔͧR#çÙ úPbÖuP{Þ¾{ñ¶ýU+ßç] Kßb†ÞȪVŸ£z”òêd”øëïse€²E¨æ Õ ¡Ò¼{ó°c¤Kêx.;Ì…À;VWW«#Åe5À ¬» ·ººå½ˆ ÒU«Ø¶w…ûÛ˜1£zâär J‚E~ü\YÄ f¤ìÉ‹{¹P™c˜Ê Dã srÊ%+d b—åyŠTÂÅ HL?ù°Hé>]Gá¹’ý›×pÿ÷>žý}pÒÇÓÎÑ>9ÊE(ÿ-~ÔtLG§­y®ç"P˜#R777cÁ0ÃC`5±p̱:o*线ÖÕ‘z(€P%rîëÕ§ãí¸ õü·aÖå0sZ^3_ôhÑb_ËS€@T¯BTæ••K¥ä"bÓñ<Ùµ³l&ŽU„gøÎÛB’ª<«o'F¦Û¿onž=R¢áQ¬n ¯Ð7îaÍ&S²7•âÙg·QSSJ0h#ú3.IÛ# ¤<Ž7).JcÛ$û{ „#BÇ&„4Ä”À @æ,€Ã'CyIy8€l˜i )îÇí¡œðøÙGu’C„AóÆg¸ë>H‡×€8ÿÓoÐ 52±îéõQGKÔú]Œ×V Á5*‡ySÓ”‚™âåX飯?·0@s~^ߢh6Æòÿ‡·ÿ¼·éXÃE¯×-úöjeúg@Zˆš3'ù¢_ž1%Tyc³ŠÁRD×VÌX Ò$i@É pƒ¤+N!Òò`v`µè’÷ïDe=l:šJ÷*à߀€2B$+W¢ŒRyìÙÓÊúõ{8õÔ¹ãv"€ž”Ë3Í Šƒ’Τ¯ÉðÝ­hÞ¶‰¾ŽvŠ+«™ÚY‡%âM– SbS3Ûyjü”Íž§ð2 ;“çÙ.‡[³4”e•QI …”tîÛÎ?¾ýq:ÄäÓ5ö¼ü èc,êl c¹&ß6û軎“×T±oß>UjQg¬Ìû.t1îi¬û*8qQŸ?4&¤…œ~¹æŠ6ýµæ{: ¡m-˜QDý+t(å†ÍfyÍq›Ùþ”?çZß§¢ 8ƒÛøÇĈÄé²¥z6c¶dߦ¯~ ˜-QodûIŠzSèWOI’Éd®e&“¡­­B3Ðþ%GPÔ›èüC©qjÔŸC¢¢([Œ\ö~¼u_Gu=Š˜ý*-ú™‘<Ž*OÆU)=>5´¡E¼¯k-)EDªñdpØ<+Nªb±dköY/AsYOI“Ž€÷NAªGû]ú&b¤*Vøuáü…‚$>ø"­­=£õ39iȬ?iÿG¥†A8VD(£fÆf¼3p‰Ò¦@ŽãáØî¤˱=RƒöQ—’­ÝŶçŸGœt³.êx³æ6¨¾‘‡§p[¦Úv䵆·ÙÏ$ÚæÄ˜‘m GèêêÊÍC&“¡`P§,ΡQ÷QèyÇ ;rFÍEÖ15 \{Ĺã^›çë–ˆø ä¢wqÒFXÿ›¿ö¹ŒÙ¹÷tÈÍ0®ËC„«w°}H•OJ‘)žmȵ/.€a Í¡íáÃ:{à®–g'4S²'ZÇÈ83!íí=Ü{ïó´·÷æ@KAzp€®ÍdR‰C‹0÷Ô3Xzþ%Ì=õ â¥åGòVÇ%å)ýìÔPzä±Ú¥“½iÜÃ(çN.厧‰`°§5wü§þbDŲ<}•*üÐŒ®q@êPÛæ=€bHlËè>Ǽ€§f1ûöîšw¥tÅäìöŸ(&© . DÁ¹žHMàòçËE”,BN¿ Õü( ¶äõ‘­õ˜]1ìÞ‡ÀJçB¨zvS8½³vsHU¬ðu[€N'õNtE÷â£ÁaÕ£ÓÇPx8éò“Ë[AKK'wßý,--]H)éïê`ËêGÙôÄCì~ñù‚Ù;'CV0H$^„ò˜–ÝÍ5õ÷¤éëJÑß“f°/Cb CrÀ&1a 7M_wо4®sù>¸·îÙAGó>?GQfR²sõýhéDÎy%9QhB€ ÐÆÉ·ŠH hM¢­´ RÃöíÛ}‚@ @qQ\לˆƒœ,H¸a5és'R‡ p²îˆPêÀS ×YA–£>Ÿ>Hås`B"¢ÓP}ûÁIÞ“¾Í‰7æƒÚ.ëˆVÞ ®–fç-S²'\·1F“‚º¸ë®gX¿f#ÛŸ}оÎÉÖÕÒ±ïhNKÒ‰6=t;^õŸÎ#èê„\Ìm™¸m¤&u-& ¨Ú"47ѳ…Úº:¤ïø …xÅ`xÕú,þ»ux¹? pˆÙÇõFž?%Qo.ìNA ެ¿Õ½’¹¾r⟗T…Ö%û‰ÔAº•êÓL­Œ ©ò“QCùê£À[ÃJbw¤9¬jà&ݯÏ]•-Ÿ”˹εÓÏÃ÷<ž»Qž"1mÁ’cbÙ;’0•QSçô:Šä¹.-Û7ÓßÝ(\×-ÈaI!iÛ±‘–;ù•“áb˜pÕ–\ÛÑ¢ÞÈó§ß1ÚÒ³ ¡­~YnÞÍànù g/­àÿ½ÿÃæäæ›ßÍ;ßx5lú!j°9ï\ƒáÆãó/y¤ôQ“>·À˜&â²çy¢âd„Auoö­|^áùöò¯™`."XHíË5–¢VyØñ™8y†64‡u:—uD¬„y¿8);I™âù8áªq¹«|2*@K[‰Œä´‹WRQ?­ QŠk?É ²Ã:SHí”i˜:‰ÌK¿¢×Yá9ž.k?¥Q XRfzq€æþ kZ’¸G‰ËR)*¦·½ô(Ÿ6òºé£¦[¡ØõÌý¤¬Zdɼá¾V£L <L­­šªëÁDmý߇µÍ:¡I­@Îôá%ZQýûa`$Ú ÝC`p ïÿÆO˜>}ú°ÞŠŠŠøä§þ‹§ŸyžçŸþφHħ#ã ˆH•VNgk%æÔÈa½ mPUà«:Âç2Æ3çA Q¶ Õ½*WúQ4y@•=w䋆Äv# RTï>DíŠÂcGø\ÖI˜ýMå€v 3ð‡è—u$ÝJ€7’gL—- ñ˜$¹V)#ÄÁ <²z/)fÁ‚,Ë&Þ™nšT: Mºïôàv:E0% ët,Ó20ü Ó¯çüóÏ/8µµµ|á Ÿçþû£½{÷дïIìJ J *OF–ÌAŠó8Ó‘cë~ ´hÔ}NæÜ‚×.0B Ê—ãµ> ©.W=Ÿ9{þyyúÅìqa Â5úåàf†Yÿ g3Ó×*ÁŽÍÀ‰MÃêÛ•å|/A»;=Á!ÐaVwu!~ú”ÒJ·pÍ”À*7žu¡««ûî[CSS§¶€ÊÊâh¥…Öä$Z!½mìx~5éD‚Xi)'½â\â%Œù†Õ ˜–$ZDõ¦±Ç-ÇS4÷Ùă-}™£žõÔ (¯oð§}¤(¡ÅÁö=[è8؆Xy*ù¡±ÁdÜŽÿ˜BÛ_&h;â7¡9)Õׄwðh}š¨jcîôJV^y §v#K—.£aútJKK 'W°öÒK/åÒKuš£T*EGGÛ·oãéÕ«yì±GyaÝiÛbá–.CÖžŽ,™ ftœy,4Ž9#çhŒ>õ\å!b3Œ܇WoW5Ì+ÿ^°ú6£Ò}?;„P.Vºéj˾á ב.[†5°7‹eè2÷OUWW{Su$=RVÈD@!Í]Ió+KB\×cÓ¦&ZZ:Y¹r>‹O'°‰˜BèKOÛAzº1 ƒd_'Ýí”ÖTNÙ™TJA$jÑg{Ï@SO†¶A›¤}tÁ*KãºNûÖ>IÚ¨@ÆuÒ°Q:¤òÄçÜ[Vé&s¦ñ¼ö‡,êšqp’xëQM÷ÜÌâÙU\úö‹¹ø’KYºteee9…ú¡’‚p8LCC \pÁ…¼?õAvîÜÁC=Èßïü;O¯ù6ÝN9¢þ|dÍiZ—“ÿ0ºÃõ]ÄdÔÎÝwßŧþßk©ïý+Îó_BunÈ›×Üâ2 ²÷=RŒÌ¶î\÷ÂW8¹t7?úöxàþû¹ãowrûí·³¤ÎÅmyŒ\Š˜|pÃíAäÖ–<ò Rƒ*Ñêÿ–·…|¯†ùfé’ˆÔê ¥™ò÷¬g„±ƒ•8VIn/x2Åóòoy pLÍÅá9¬¼‹¼x-~æÞTåJì¢YcŠ[C[#쬶çPF%Mp3JHÚÚûhi餬,NqñÄ^ÿRB0h6(©„=a°rnïL)J)LË¢²aƸ1S’~løÇïØ ˜yC1E½qÅ7!PÉûþÊgþó£¬Xq*±XœÅK–pÉ%—Ò´é)¶¬¾Êû¾:U{Îv®ÙgÕ¶ïÅo±¼¢“¯}õ |ä£cæÌ™‡­Ÿ:ZTRRÂùç¿‚W¼â|únfÛ“¿ÅN'‘Å3t)®a>f…n=oƒ(wß}˜;ÎMמÏnùç>±X Ã0(//çñÇeÃŽNdå)ãrpCœc^_ ¯s-ÂË â3¥§* „úwhܪg¢l¶V¼÷Ì ‰2‚{·!”d&p;àL–Ë:dÀòÅÁ"à³À PxV‰ÚóQ桲ìbÈé/3€×»oï#8×â¶m‚Ï ZžÆ=¸¯w/EÆbos7±XˆŠŠñ]”ÒÙ=åS¿Œ$ÇvI :ã¶ ‚†âQÛUd&·hq euÓ°ùÑïCUrLK‰E,¬ Š ãÝLšçþòSºå,DÍ*m!b$Hg-ö͉y8ûfÅ¢éœqÆ™¹_JJJ8÷¼óØúâcl}öÈÊ“´rw<%qVW•ÀÝþ»Å›¯¿€ï~ïûœ}öÙ£’îGžçÑÛÛË®]»xö™g¸çž»¹çªª¦²²rÌ󺻻ùÌg>Ã_oÿ ûöîÅs=¢±Ø¤­ŒBjkk¹üÊ+™ÙPÆÇOÇîçñFD0/+gÁ¹Æ×/ àlû }òéO~Oýç§G¹¹¹™o~ë[´ŠùÚ—Ž!0̨‚þo¹Cyǥ܋ê߃,YHîe6ÊA†W^Bš¨¾íŒ"Jg3>“¢ðÌf¢#Õ‘}óVû' X‡d%Ìã®VÚsL)ìøLÜPSã®ôD 7ƒ°p¸í›‘][)÷ú™ ± ¾Œ™±8á0–´§’<×¾ŸÇ·­§5XCÏôó¸/&“qX²dƘ\ˆRº rÌ:t.Ëu<ý™q-s†,­ÖΣB@W"À3̓ ŽÈã•} ²}|@ìt;&/)V™90èMé&„ ÙßMOÛDí…£cõ†Ḭ̈†ëZF´ ¡jÎâÇ?ù)—\z‹/ÎýTWWÇ·¾ý]úÞü&}á›+>¤e‡Y¡† Õ»oÃO˜]ÜÍ~û+\à „B¡I­E&“açÎ<õä<ñÄã¬[¿‘}-íô z¤‰B²‡–¸å–Ž >»wïæG?ù9Ýn5½‡â`й3ë8ç쳸üŠ+9í´Ó‰ÇãŽ%óæ·¼…+Vð‰O|œ»ù*ÞÜ›U« (ãóô@©.œM?a^iÿó½rÅ•WŽâ({{{ùÜg?Æ=ËV1ärP` ‡G¦$"6ÕúØ`Æ-¶ªÜÒB+Q}{µón.^X´È”,$зƒ<‡k€Õ“Í•uhn B …žç½ ‰’ÒÅ PÈÅ2Çìá$0ú› k™öm„Ò]¬(‰séÜ鬪¬¡!#jZX†Ä~ >!p(¶öõðó-›ùÃÖßÑÛ±…Gì~‚%‹Ç­LÊ!iJÂkJ ¥”"“vIØã++ TǬœW}iØ ,lè‚°þ5]Ç!“Òé‚á2Ïx „ ¯³/ÿ…³lÙòI­Egg'÷Ýûnûãm<õÌ ´ö ¼È (YœÖˆWaKpöÞϦÍI$D£…Uƒƒd”…±èíˆ`1}Í<Ûµ™gþ(?üß?°ò¤y¼áõ¯çêk^IUUÕ„c[ºl¿øå¯øò—¾È÷~ôc’ƒ0¦_î;Wæs–R8¿ÏÊ™ð½ïÿš•+WŽêoçÎ|ê“ÿÁmw>Ž˜÷d(ÏÑsÒþ^…^"ªS'«T"''ÂtµOTîD¸Õ·2ƒ,B(áÙ(!ó³5äúµc¸ÁrŒd[–˺]¯ôàdÖ~BÀú£ÊEdK D"ƒ·}ù‹ÓïùÎ÷.ó\”‡,©%Ö0—ŒÀ¶]]2ÝõPj,½‹@¦:1Û×â´oBõîãœòRÞ¸t§–U5-¤~¡ …§ür[ 2ž‹,+-çkgžÍûùšçxáÙý<'{þ…yó ¾a”‚ä@Çö|ñp¨H©Žö¾Ç»ðÓ 0Ûi{RÁÔŽ/†ýXBÇU¤óÄ7×¶Ù¹öYzZu,ViM³–ŸŠa y wl¡¯³)$]šémo'ãyJ)=7~‘ÔBcBÐݲ‡Œ@„ʇ¸œIÔx(E.¬ù:oxýëøú׿Á™g•k²xñ~òÓŸqó¿ÿ;=ýļ×"+–j½Žùàîø#EmwóÞ÷½•|ðC”NÂjÛ××ÇŸÿô'~øÃ²fÓ>2ñňš7!çÍÅ ûq€¾RXH°â $Èd2c–ëºúÖ…3Ž,™,™—“êoâáOðøû?Í-?üï|ç;¸öÚë&kYYŸÿ™=gÿõéÏÓºµ sîëÀŒ—Ï7‰³åg¬œ ?ûߟ³dÉ’a}$“Iþzûí|ñK_b}“¹ðÝÈâ¹¾ñiäRM¤FpÎ"XbTò"6}´žªñ@è;T)T¢ ŒHD: 2¡\36¬ÏŠ“)šC8Ù–=¸8 øÓd¸¬1+¨L4ëV‰Ž¸¶oïú —+Ï›©›ÂˈL¯EyÚ?ɶ]2i‡dÒ&™Ðá,ž§|ð9ez¦åJ3=¼cá®™6“¸¡rê C ?dÆñÞÛÛEÄ0V~œ§×ÿ˜×¾î |þóŸáÆ_‹åƒîÒ¥KùÍ­·òÕ¯|™?üùg´o·P2âÇç4„[ùòwþ‡ëo¸aB]•ã8<öè£|õ«_áÁ'ד)? ã¤7cFëóB¿ò 'äùzÆ{ÉX¦é{R¸@6i LDñdÑlTâbžk~˜ßÿŸüö·¿åÃþ^xaî^ Q àïx'Ó§Oç=ïy;7ÿ Ÿê\N/Ø 4Æùî÷þwX)¥xá…øÚ•¿ÞõÉ’30—_¥õaÃBh .zìg°N“8˜W³p,Q3CÏÛ#ÂŒƒ ¡úöAÉ ¤“ÐΣÊEº)\3>| B)žK¨s ÂMƒN¡~ Zù>a©‚;%¬bÀttœ ”@ÐÑuÐÚ¿q㙞çI!@„bêùãñ‹%XZì** 㺩¤M_ŠÁþ4¶£0mdZÖ2Íà?N9Ue5Z„ S L¨¤H!H{.ƒŽCÈ2‰4¨R …Nã2=ãgŸÃ™;wðå5ðÿÞDï«ßÄìç+¯Æ Rú/`ÇÎNô3ÐÙFWËnÚvo¡mç&úöïÂêë¦LA™À‚vÛfÔ6°àü«YòŠW«¨·®bsŸMû ©ü’dJ)‚‘(Ó,¦û€ÎPV;P4F~UëâÊjŸs!v*I¼¬˜HQ±¶¤åµÉ¤1÷®ëdèlÚ±†!qdª^æã½½• ¡JŒ“ÿ{wü‰ïGX¿núðGr¢SCC_ÿÆ7yÛÛßÁ‹k_ ··‡–æî½ï^>ñ‰¯ríu×M´?iiiá[ßü?ùùoè2æc,û8fÑÌìd2Tz$+pS˜!9®¯žbJÀs†òAeÉщp ÖœPgòàÆ;yþõoæM¯ øà‡hll³o!—_~?øÅÛßþv¢æ.}ÃEÌ™;Ó49íô3X¾|H îééáÇ?ú!ßþîØ?P†1÷ݘ% ‘WÀvÔMB—5Æq!±¨ž­ºà¬[ü¹W²Ç²z¬ž=¨éçâË0ì~”4q­øèë*7\®ÁêßÕ{ÌvL´F k#Àj:MÞ=·|oÆ_¿ø•o9™L%Ê#P¿„ø¹oEc¿q²ŽŸ©¤MOw’î lß|þäÓYYZ­cõ¤À0¦ÔÁÆa`Ò-=XS l×¥'¦*&h9®M 0„dGo/?Û´‰¿îÝK{(J¤v±ÊZáÊsIö‘èj'Õ݆èï¥Äuh †YRZÆŠŠj•—S‹µ,$‚dÆeOO?wïÞÍ­û¶ÑUßÀ™¯sO¿™-Û-†gxÐà2~XR–󰜷ásó2E, Çqp2ŠtÊSLôvñÛÜ@{ÑÈ™Wç‰ÔdÅ‹ìíèéx ±åçœ{Êt>ù©OqÞyçäœ<Ï£££ƒÒÒÒq9Ïóxè¡ùÔ§>Åê ­ˆY×#kNË•!›Ð@ $ÎοpníNî¾çž1M_xá.¼ì•ô5¾Y4k„È5‚Û<¯ã¼=椹Å|æÓŸæò+®—Kô< 6PZZJCCCÁ6Û·oç£ý»w5^ýUµçúy×GÕ$@jÌ͈{Õ³wów‘Ó^‰U1äŠR@üËÍq˜ ‰êـ׿cåÍ(B(•—Þ| I¨mµ.o¯ÛxÀ;€Ÿ㊅cͲ@gVþ jý½÷Ÿìd2Ù‹ê#Ì‘å’FL“ßK8ÄL´’<ø4oš=ŸSK«QY°’>XLLËDJ‰”Ò礴>Ëå¹”X&Ãð­_Cz2Åœ’b¾pæܼlkÚÚX×ÑÁþ=ë°m !(¶‚ÔF¢ÌhœÎÌâb1ÊÃ!BE&!$QKOO8lPV\Á)Ó*yCûB¾õâ üòk¢íÚ·rÊoÀN'Iõ÷`§5Ø[¡0á¢R"ÅeX¡Z6ú )rÖ 5Æœ O'2i)‡tkc’ôµî£¿»1Í÷±Ë'èPu ÃV kÏBÅyxë­¬»ñ&®¿öj>ðÁ1wîðªHRÊ •׃ƒƒÜòƒïó•¯}“vc1Æ)oCDjôÃ;2ÓĘº7™~Š‹‹ÆÆ¡:#DU O¥ÃMd婈øL^Øs7½åÜü®åýø åå…ÓpK)Y¶lÙ˜cضmoyË›yrC/æ¢÷cÍ"›{jÒkq(ë˜ÍÑnD ÝÙTP…€j€˜ÿ`%t¥Pƒmˆ`1jBt…›gÆ´¾K³Y—¿b‚´3cV»fØm $[÷îØqšRCe¤«züR]ÃǪØöÓTš³+둾åϔӔ˜Á€+m‰‰ŒC4haÍm… `{.°ÏËY'³ïýiñ8ÓŠŠ¸föP Où>+Ù R14&×óH{6 Ç¥3™Â’Œç?%%¥¾xö™œº¥†Oýñ§lyè¯8©nb<Ã]&F$Fqm#ËNgîéS5kÒ4GéÓ¦BÊS¸“ÖBбg+)7ˆŒÔxÈáAj¬s•‡ˆÖcœô>ºÛžç–ßüš¶ön½õV‚Á “¥––>ùÉÿà7·Ý=ý:Ìih‘v¢2d£Æ…L/ååµãÖP*Ï’: *Ý·”bÍ}=}ísùÒ7Áš5køòW¾:.0E?øþ÷xr}æ²ÿ‡Èw™Š>jä“=׌!ÂÕZñ^¼° žjøuF¼²z,Dõíóã ‡Hø™m•ȃ¥ðBå8‘Z½Û²ÜÆéÀ,`Ëxs5`Û `yÏýùöúDOïBá'63Ë1âù¦Ö1È/’àÙ)zöoâÌ’2†©Ý²¢`ÀÂÈ+)5H*åâz i 9RºJÑ™J±,JƒAŸ±ôP®K2m£X†$`9\Êa”×sI8=™4]©4©É$©4Ý©4=é4ý¶MÒ±I».ާÁ.ËyU¢,”rÇž­Ì-.áMs0+^LÔ°°mvöõò\Û~žûý÷Yóן³à¼«Yuí;(­ŸqX 5RžGëÎ ¨p-ü÷ΑRÔj—ûÉC£ö,œÌÛ¶ßCÿ¤+“Éðáˆ[ÿôÆòÿ‡Q¶”a &ã–‘{à<ÈôRSsòdgmè3ê~ ¢$FÕ¨È4î~ê·´¾å_¹í¶?0kÖ¬I¯U&“aÛömPºDƒ•7B$5Ì#¼ŽÒBÄñ:žÓÖKaŒÐYàÌs¿ûÒ ‚¨Þ&=~ÿÊ!<€ðl2ášaUv”´°‹fiŸ,MuÀÙÀ–ñ¬…c–ëò¢9µ8¸ñÁ‡—k€aMX‚tÇ^šÖã¹6^O+Ó¦á(ϦiæþGû\GBsQŽë‘ÈhÑ®Ä4 ð‹d Àõ4G–u§ˆ,¦á‹_=™4;{{Y×ÙɆÎN¶÷öÒ<8@O:ƒízHaÃ$bXDM‹ˆa2L,!uÿ Ú“iö8 86³#¥ ¤m~³c•á0óŠKYQVÅeõ3xÛœ¥t$Sün÷~vÏïøÓ†§¹øß>KãÉgUвÓIÚwo…¢Y:£¤*ðÁ·÷(¿;ÏEKè>8@WW'“·iš,Z´ˆà÷“IÄ(Y ¥5ÖÛ={x8¸„N,çöQWW?¹IóõŽ£½¼Ç¿ˆØtŒÆ+Ù´õ¶mÛ6%Àêééaÿþ>š»›h þ¤ |`cpð”=ˆ°Šîç5gîK+úA“ˆP j`؃¹—¤P.ÒM"”ƒtÓxF”|´cxf4+ àbàçÀ˜a$cVèEû]:P°³¿Ó:¸cç©9q0\„U={˜_ë°ô=Jû5%Û›h¾û;„# ;•†dQ+€ƒ7ÄaY†ÖYù@%sz+T á8 “±qlO„kèÒ`Jé'è°™üë÷¥mÙ—àÉÖƒ<ÚÒ¶Þ’ŽCi ÈÜ¢R®¨ŸÉüâR¦EcT…ÃDL‹€!1¥ÄBˆú7êyhÒñH;´'Rìë`KOëz:øÑt9ÏSqve=×LçäÒjþ{Ó3ÜõµpŇ¿NãIG´„$z:ènmAÔ]4Îæ>DEmn?Œw¾‡Œ7r`«ä§?ù _øâ—&f#¥äƒü@€/~ùëô Àœõ/`e‹~ŽãH Ê{@•›ÀTIê§M÷šÚ¯m¬{ @‚×µwç­\yÑÙœzê©SZ¯ÜsÛöt`.˜;B;Â"û¨{Ë®Õ/†t§§Süc8På͇V@O•hG‹´Ø'ØÁ „çŒvoP /PŠ®Föõgc%Ðìk¾ÆÚE hAû]ÅuÀ·îîTvwçÄA/ZCo:‚{°¥´•aHLs(õ°i ú¶?M¼Ôâ¬÷ÝÌ@[;Ï|ëû¸ÊÃQB€ihÓ³¶ò‰œ7»” ×A¢A)b™„|ŽÉðÛ)Àö<Ò®Kg"MÆÆQí©$«Ûrÿþ}ìèí%bZÌ/)á sæ³²ªŠ%ååÔG£M3ç”:œå/ðÒSZV6‘„0(ލ/r’WÁÕÞ <zivôôñx[ ´5q{ÓNª#¢%ìlmâÁ[>Ë«?ýŠkŽ8h )è=¸—Á4">-ïAŸÊæ.üöSx{‹@n Š7bÛö¤ãC¡øÀilläãÿ»Öïǘ“Vºãঠ݇@eú‰õ“å°`‡Å"¨ÔΟ-h€7¿ö*>ûÙÏOš›X·nŸþÌgH—¬ÂŒÔ3Ú"8òºcÌù”tY#Ú*¥¹ª@)*ÕŠˆÍ`, òg¶`æë±öúq…ºµ({ºv|VÿÎì¡4hM°®¡¬kC؆–-‹ƒ ^¼ûÞÙN:­ … ¨'ÙžF ¹Bä8,)…/Ë Õ²ŸÆÙu„KJôo‘ƒŽS‘kk Ò] £<¨ðsø ÁM"ðP :½é4}¶MÂq°=W)Z“<Ô²Ÿöï'áØ,/¯ä½Ë–³¸¬ŒÙEÅ4ÄcÄ-Ë÷…ÒUB íÅPæÇìÇõÀUC`–«0"”Å‚¬ŠV±ª¶š›SËØÖÝÃß[vsGë’žÇàöu<÷—ŸrþÛ>rÝÅÂ$èØ³[Ä0ò=܇ÝÛ!Šz“}{ ‰×µ‘¸ÛÄ¿ýû'‡§–ßË0 ®¿þfΜŇ?ü!}þ«ˆy7!Ë–å®% f^õ•ê¢$¤j‚Ô%RJ½gs©wFÞc!ÑP¢’pvý†X;ŸøïÏpÓ›Þ4å<]fÖ?L2¦ÀüNj-àŒv ØG6ÓH=Ö˜±‹Ù9—Â*CõîêcdGPF(ëDj¢}²þ0–«àkï:¡ƒOÿ¨RI`—À°â 7>ôð›•R&€’Aœhƒö?*0qž§ð<—LÆÅMÛHC÷iX2$éØ jÈmÁçª<_¡^‰`eKøùk€®tš‰Az3å¡ÐÊð´ëòhK ܹƒ¤ãrÑ´iœ_?i±(eÁuÑ(ù¥ ¥?_¶•R³Êr¤O‰Ç%°Û¦Ø j+…'òü°´kIUKË+yǬe<ܺŸ_îÛÈcwÿžÞÎVV\ýF¦-Z‰aÆuD,y®KÛ®Mºvœ¶éFÓá€ÔX:¥ñ®Û¹æ²ó¸è¢‹ù^V®\É­·þ–Ï}î3üü7? ]}9fÃ%:ćññ^²šÊ2ÊÊÊÆ½Æ0À¥x/tŸ ¯ûE¼Ýà§ÍäË_þ«V­:¤û[´x1ùȇyï‡?‹Wq "Ö8ú~&»‡ pê^¯‹£Ù}S@üËóAMHD¨Õ¿ìX±Q×e1Tn¨7XŽ9¸?ëDz:Pté¸|º\ªª²2 )s™Ü`)N¨b«…?H¡@¹$„4–‰íê@`å»5 ç² 4"hÇΕ«û80˜È‰“Òç’¶õôðûÛYßÙÉõÓ¸fæLj#Q†¤6¥>Å’’Ûö¡'ëå™?X‰ay>Ô&e;¸®"Œ@úûZ"¹7’ÈÛó:}g:M™)°„¤,äÕ3æryÍLžhkæ‡ë^àÞ5ï¢úŒ Yõª¥zÎbB‡Óq&;9HÇÞP´ ]À¨€ãáá€ÔD"·} •F 7¿ûëSri(Duuu|ãßbù²å|î _¦eãn̹7j'Ç1a=Hdú²iÙ4HcR Ä4$ÊË )Þóï);B‚3ˆ³ÿ~B=ò¯o¾ŽO~êSÔÔÔÖýÝpÃüñ¶Û¸ãýXóß<Æ´i:W[Í2Zç¹(»a„G{ýç:T¹Ÿ"P}kQ‰DI|Øù†Ý‡•é¥p‚å~&RPF'Z¯KÓt|aÁª:“J åÇÛÍF{¾ 'R‡Ê¾ÁÇ>œ ¤{—–ú. úF¥Ðºæª¤oÔÌŒiHâAüœ];ûzÙ70€£¼œr¾e0Á7mâ?Ÿyš¾L†Ÿr*ïX¼˜ÚH„¨e2¿¤”ñ8–o%Œ[fÖa3+îX–þ˜fA°Rž"•¶H1Hƒ'ÀIÜàºJ'ôóÁ ¥«°Â˜BjÎËÓý„,ƒ‹êfðóS®â3ÎcÚSOñ×O½…Gù? t¶r^w!ƒÝíô¶·!ŠfŒØ ÙOveŽPÐÜùþ¿NÕt7¯¾æ2V­:íîc$…B!Þù®ãw¿ý gͱq_ü^çÚ¼¡Œ‹ç@²•¹sæL˜B;‹‹Q™þ¼>ò>ÙÙêÛ‰½éûÌ´Öò½o}™ÿùú7¬@ç{ç»ÞE8±o  ]ƒX ¿§É®Å$Ö1›}t˜Ì <]TŠ@º£X˜—‘/? ©°Š Õß<ì…gH·!Ý$ÒKa¥Ú^ÚÿQ`GtÁMEÀiP8é„€•wÒ 4«ÂÀ‰Nþ`"!ðZ_$$û©Z¸…ÎTà¦RD- CJYÎJ·f'>kÛ?0ÀÖž\¥EÈAÇá/»vññÕOñ\{oš¿€O¯\ÅÉ•ZéY ±¸´Œòoø¬êPGó›ðAJ;}Ò>^¶çis ã@ÆFdlâBRl™9‘%È8.=É´þ¤ÒØîÐÆJ`"J Æ=ȸ.*Ã¥µ³¸mÅ«ùZÙ2úÿøKnû¯·°åñ»ð\7Ï~r$„ §e‰¤ƒˆÕ ÓËäÔ(:R†x]©0[yó[ÞR,”R<÷ܳ|ó_箿ÿ}Ò„œsÎ9üþ·ñÎ7\D`çqvßn‚Q¹“DÚÌ›?Â~KJJ¨«­B ¶xÐ%Øý8{ïDîø¯¾`6þóŸxË[þuÒœcWW»wï&•JÙæ’‹/ጠp<Æèœû#×¢ÐËbüuÌ©Qk™SšGµH—jÁeªÜÏ#¸,@X%л7ï>D.šûáåéÊn¸Ï&BžÁÒßdŸáw(<3Œ©19#Ϩ¾ý¨=÷1)›9”b°£úú©F š’iä8¬lš—ŒëÑ›LÓŸL£”"é8L$™&–lëéásÏ=Çvìä↾tú\ÑØHÄ4ñ”¢6e~I)aÓ)EëŠ!–¥ÊðË’ç¯#ÚÔI¥!ã€ãj%»‚ a(AÚqéKep|¼íz¤l?×8®GÊqu9°,çåéM$eF Äo™±œ¿|-7ô ûÚ‡¸÷ÿE_kó¹-AûîÍ8²,E(o rûï¼½Ç:×µñö?Èů8“+ ›÷Ÿ}öYn¸ñõ¼ÿ“ßáº×½w½ë455MúëëëùÆ7¾Åw¾ñE¦«ÕØë¿««·d·³0ðÒÝD­sçΛ°¿X,Æi«VAÏFp“ä„|ÏÆëx{ã·™XË7ÿû¿øù/~9,Xy<²‡[oý —_~9çž÷ ÞýîwÓÖÖV°mQq1oxÃë1ûÖkÀ€±×aÜuZÃñAªÀ!a"" ¨L'(›a`E}QH\RB÷·h]X¶ú àù •Äq=”*çâ¸ÃÕKË€‚Ö’ Ÿ_P|¨E)ÜH ©ÊSýüC…H€“ÀÝôGêg³ü†×`ƒx¶ÍÆ;ï¢ì@+—74R‰Pj‡QCƒ–íz ¤2¾ÒÞ#(¡+“¦=•"`<ÕÚÊ×Ö®¥4âË—s~ý4Â>PÔE¢Ì).F AÚÆõ–!}±ÏÃÄ’Žƒ%eN‘Ÿ[ ÏÃp\Ãsƒõ²€”²]ÒŽç'ñלT@¤ ’¶Ë`Æ!( $rXB ŒìÕý7gI ÄE3Yj•òØšyjÍUÖQZ7cRéz•ç²öï¿¢­7„QwvÞ}ŽdŒóGnX!Pû¶ÜÁ§?ùQ-Z<ê”d2ÉG>òažØœÆ<ùøñ¼øä]¬}ú~V­ZEeåÄ ò@;˜žrÊ)œsÎÙÜý<»žû ™Þý(7…—hÁÛÿ ê ÞûÞ÷N¨Ã(..áÎ?ÿš¾þA¤ÃëÛ‰³ç/”$Ÿâ-¯½Œoë[\vù哿ªlÛæ;ßù6úȧؘÍ@d%kž¼ áôqá…\˪ªjî¾óO´ö Œ¢9 ˇ?I]–`„¨WpÝÆ8 $8¨Î5ˆè,„ 2zLF‡é¡¶#ªk,ÿ˜Óµ{×#Ø_ÄiߪSžg±J0£éN¬¦,ˆ…{Ý#9ðÉÖbà=º#E¦x>™âñÙmw÷CÄœ¬ú×›ˆÕT“îícÃwÒñè¼qÖ\æÆK˜ ‘–¥¹+`0mãzž^T×Å’‚®LšAÇæ…޾þ⋜USË{–-£6J·T…ÂÌõÁª=™D(eYBA-ö1”Wk0c6Íá›ÇóÀvF¸8 qEC€3ôÝq<”Òú¸a5¾(0…$ LahPóŽçáz “<=ZÞºK!˜/çò²ÙtØÇÜFÚs¨™³3,ðfóIÒƒ}<ó§3^†,]…ÊmEmáóî¾8¹>Å'>ñ‰‚&þûï¿/}í{¸3߀ŒNC„+‘e‹Ùµñ)V?ø–/_δ =󩮮ޫ®ºš…ófàôlEuÇæù™Q]O{Ô›F^Ž«°Fž/QÞÚû›tZ$Î×^ÌÊæ|æÖ[hÛ½™WüëÇ)©m,œùAú;Ð×Ù‰˜3sx‡ãÔ$ßÞ㟋Ö%u<Ï¥×_]0kmÛüúW¿bÀœ…•Íœ©"\¹øßY³õÜtÓM|ûÛßæ²Ë.c²ÇyÝë^ϵ×^GWWžçQZZ:%Ÿ()%ozÓ›¸à‚ hii¦¼¼‚3fL¹¬Xkk+Ÿúäðó[ïÀ«ûÌ ?å±ç`–,¢eó}Ü}÷]cH_qå•Üò³ß’lFÆg2œËÊi_Ç^1£ºÉ5ñ4HYqTºnvÝÉõ§@†f Õ·Q:wãmDT ®%gŸ‰Ì¢Ÿ¶Ši§ŸÆ ÿûK:¶ügÞåxV™éÍÞå 4C5l³OV‡u ¾\¤Œn¸jŒAëDc^Ó£WDIöõóð׾ɖŸý’•I>¾ü.›>…¥¥4LJHÃÐa5BBG*EÆõtYt³)Ï´µÑ™JqãÜy„Lsg%€ÆXœˆ¯³B …–þ»7•ÆáQ> ¬@ëª •7$fÅù|%º)$ai⺊ÞTÛñ†)ÝÙ7¥GŽóJ+gx«¾x97/YÆ™55,()¥*&bXÂ×½yJ‘q\b–EÐ0°m^c’”ãòäÁƒ,­(gz,–ã¬@s,†”‡BCC2M_¡®ïßRR+¾³s›?Ï ÊÍ›°êù\VXš”˜š£ò<••:m³ë ÕZðAa•aý… “2+¬}·€ǦßÍäé!F§•ÖóÛ%ÿÂíIþþ¥w³å±»üÆy[Xy´n_¨ÌcçGÍ ÷Q–( œË8ç‚×¹‰y3jX²diÁMôЃr°ÏÀ(ž—ã®r"òÀŒ`Íy¬Óy÷{>Àw¿ûÒéô$¶çKO<ò0¯}íÜýÄ^Ìyï@Ígx„~­EsظeÍû÷ì§´´”sÎ> z¶è*RL¤Tá6èÐ:ç‘0‘z”Ý£37Œ<·P£¨D ¶QZâ2ÿª+‰Ž!R+×¥dæ ¦­< ·uN $ÿçi@ãÈs&XÕè<5€Â U ŒPÞ@…oz ÷ù[(ÚþWN/ŽóŽYóùÏ“Vpó’e\X?eeåÌˆÇ [¦ÖYYBŠ\Èë)”§ˆ˜†òp\¬ÎUŠ–D‚½ýýœT^‘ë²dIIc<Ž!´‡|ίʧl!Œœ«D¡ùw³ù¬¥XÏ •}© =É4IÛö[İ( †rÄa{ǃ´ë2èÚ9½Vv ¦¾>KoÏ"#@@¸žGÆsµ{…òhÇùþÂËy§¬ááo~Œ5wþ å¥òpÒ)ZwnÔ khSNè[5rR& Ryçz6toâ´U§ô,·m›‡z›«= ó½Ês}y -̯¤·ì*>þ©/ðéOÿ}}}“Ø¢/ 9ŽÃ¯ýkÞð†7±v_kî[u ñÈl P2RK{wŠ­Û¶ŽÙçYgŸCÀkGeºG¬Éè%š F¬Õ˜z¨¡×—BåJÊêŒkúûEXÅH+JÝ©§©¬ÏÓEGAÍIË1í.eäûc£H‡Ñd"Rg£ P€¸¡ª¼ÄÿìÜ­wk‘sÊ+¹pîÉ,,+¥2¦(`å8&)Ò00&Ò0|§P‘ób·³Žçâ9Ó¤,¢'£ÃpÒ®KýˆÊ' ˆTÃô§l\åQ‹ú‰ÿtË”D´uÇSª0pyÀj„þ*+â™Bà ‰å;ƒ¢4Ø ßUaHIOî»PZÙîe¯—÷oHšº]Þ±ŒçÒo§ý„ƒ‚"3H@˜™A>=ç0‹&wîˆ{F%¢¤º»ÓH”k®ˆ×ÔŠYdWÇ: ÐÌÔRкôl\ᘀ•§p_è@@aâ†ó¢Ñ½ î¶¿QÝ±Ž·Í[Ì% Ó©‰† ›&f¶€„TÒ41L#§`Ïê­t(ŠÂÎ8à8$2Ï£,ÂEÑšH’ðó]ö«‚ˆi’q\2޶,:è$^9K,CЕLaJI‘!õdß>#Àj˜epè{Ô 6ÀP2Ç t§ÓD¤EDZ£Ú Œbf`XÙž.¬jæ,Bþ´zÚq5í¹D¥E^jTBò”™a>ò»[È$9ïM¢»y½ˆº¸)ý¦šHa:.H÷FÎ;ET¢•’Ëâ1ÄÁ];wr ccnÃîªPŸú»¬<Ï*âWþ%û›ßÀ7¾ñÍI×-<ÚÔÔÔÄ'>þ1n»ýA¼º«1+V‘Ím?¶ENi@ T²qãF?»Éhz]]³g6°oÇ^Œ²“ÆYŸ×™*H©Â}«ÌÊîAD¦MâEVà˜´À*a µUK0l/ )sYJ”RXÑ(á’8ÉDÏŠb8ƒþ|²ý8çÒ&OF$\”™g„p¥d“vy_$Úö"ÿ6 7ΙKcQŒa P %Ò21‚AŒ` V"¬´³¡‹›Éàf2Ï#`D|/ølFЀï<™ÉSœ+´·¸%d.m°ÇS£ö‰RPäç¸5ÿtU#9«|ŒœO•¤R3DØ÷½Êº,Xºž?ööT‚ƒ©A’Ž3¬]Pš¥vÃ(¶BÃDÈ좽¾v)ßo¼€ý5÷þðslxø.¡ ­ª}:ݰ‹OQÔ+¨ËRÏõš©­*³ÀÂÖm[H8AD „ÑYFöé÷¸Í†œ°ÂÀÜìX<+Žˆû,coÿS\TYÅ«gÍ&Ô•ŽŽË€«H ´, +•ÂñÅ1)ÂóÀÖá.±õßB’ 4[&ý¶æ¶ŠL)iM$òÒ°+¾óç7¥î)´¯Uþ„BŽÒ^×Ñ–Á‘æf @ä™Ôà4¬”Òa>9#AÞxl_G•r]2®3l Ä 1=T„5¬†Ú(^U9Ÿïϸˆ®»þÈú{nEÄÀ€Ý‡JÚðSÑGÜÈ“9w°…Ɔi”øéƒFÒÎ;Pf)ÈàP?î;âXöoÏE„«±¼ÍUÜ|ó»Y¿~=S%ÏóH&“$‰aŸL&3¥~š››¹ùßÿG×v`Í{;26‹á‰GêšFÎ+ÑiìÙw}ûöŽyùóçC¦ÜF‡‚k5¬ÁˆÏè? j> ®AÙ½ œ±/1ÖAÿ¾e œtŠ´Ÿœ/jÖ½k7~ÿGÜTz(•²! ¡œäHÅ{ :½UŽ&¬2t]B@áŠñz÷£z›ðM¶qíÌ9”:xÙ2 †CDÂAL_OeÈ<ñO§Õ`åB`’ˆeb!%Ñ EÈ2ȸZïSP±¶£c¨(„”ƒÄ,‹pÀÄ”’HP§C¶]—®T ·PŒú;ç*èMgH9.ã˜T"çž0w–ÕYõfR´§éɤ†PÚ¬Ì Sn… 樱e£®†W¾‡++ærËœK˜n†Pö ¸¶>;Ý ÉŽ©ƒTŽ‹brç*’­46N/è àyû÷ï‡`ÙPºã‘ÜÔx—òÀŒaͺž--ßúÖ7§äòà8?þñ¹üò˸âò˸âò˹âò˹ì²Kyó›ßÄ®]»&Ý×-·ü€‡ŸÝ5ëõ:ÃfŽ*ÄMš[ª gÀeëÖ±ï3fÎÄI”ÖåL¤ fMRÃÖT Âõà‚— ¯Éë¦àõG~U`ÅplI¢£C ËŽ6lunÛ‰ ­<•…„ŠŠÀNâJòc”‹”’‹€IDATÐõ s4‘Ò½¨Èvêx oÓïÁµQes©² æ•”è4/¦ ðuTÙŒ¡–„£­ìv„ëæÒgV A,dñ´dø^ïÙ AÃàìÚZnݾ¦þ~fiºn4`¶,„ï”fIi0T˜£ VúæPJW¤ÉéƤlÎHñÏõÂç¶”R ØúÒ)ºì•Áq#€‘ÓU ,a`Y†–®ÇÛ˜`5¤¼¾¼lß›s9ïÚy/Í-cÔ½B;ð¦:f¬x ¯ ~-ÌþÚ©¾S¯ç@¦‡iÓ ‹ƒ™L†öö”’ÓóL ¿Êõ?â5çñàC÷±ÿþq ˜fÉó<~öÓŸòÑý'½ÖRåûùxðÜS8öGøá~î»ï~(]5"µM¡±º7ÿžÌâlÞ´iÌkÕÖÔ À =¨ËÉûÆ-té1eļ&c kýµ²ûFtj@å“!qÚÚ¨\¼ÏqÈ &èÝßL°¸åzyÎç‚@<†pSxf%-„v«0ÑéfrŠ÷‰8¬FtAUvO3å5fž<šŸ%fºØ¨Èüj7#r²gÓÊ×˹1—:&ûo6íq–Šºò²§gÕÖR ñ§];G8æñ"þýÿæÞ;ÌŽ³Lóþ½Os«ÕÊÁ²d'ÙÈgcc&ç8df`ØI»Ë,ÌìÀÌî0äÌ€ÆØcç„-Y¶dåœ:Ç“O¥÷û£êœ>%³×÷^W÷IuêTxë®'ÜÏýd‹%Ë™}îfYVÁ# BBÂ3h µÔ†yñH"•ª.–”•Œ¤BJ ×L_b¦43¶~Îm›oÞM«©ÉwCÝRþ¥g#;pŸ 6ÂAk\‹³¹z3­žÙV˜`ª° ¸Â+ÒÜÅçÿçÿ8+çË÷TðÊÁ6‰ÙÛ^{Œk-ÕÚý: :t9h¤Òi"!éäfŸ·?ÔÕ›y^ç^¤‡0Ò ÇüBèYÜú9¶¡ú¢ö€©£žüÐ0žã౫üðf<†7£ZCDÒÂUÃ3hS>`UÆœ€U“!ì!°Â¤ôp3ý,½lK/ßx”]Wz¨AÃÓ™2ÇJ+X­y ¥ V „•â†AÊ4«náëz—ðìÐ{ÆÆ|á¾@Ô¯è8dí)¢›¦*~Áó4K]2Z*RvÝÙ®žP˜7Ø>ë=æ2!§‚ì ‚¸fÒŠÐM×ÍêO;·sXORBÉu(¹ÎìØW X¹Rbyn ‰VrkÃJ¾Ðu)ÉÁ'ñFž÷ßv >hÉZW÷幉³@ªæ»Ò³P±I¥æ¶PlÛ¦X*ù*¡ įD £{ú¶ù7ì îÀ“ô.j£µµ…†”’ŸüäÇ|ú/ÿŠÉèV´¶+‚Ü©?ÏFD»¡ãVþý›?âßøú¼~ Ðm·Ý†9ù4ö™‘Ö„¿¢Â¡›q#˜µ5çϨçô™¾y%g¢Ñ(ш9åÎ\Ýâê³K)Aø]pÊÃÌÙAg¡}¤r ³žâø$vÁßײðÕ0ÎôNNZÈôUïOŸfÕuSa)pv—°ê?J×E5:ׯ¡œËƒ¢0\.2X*ÒžˆÁ ëJ’SU ª‚Z²Àª`57jiBБ±,lÏウ&–$“üúÄqVÕÕ£ü-Ûóü 7XwÌÔçt¡"š>eéÔþU&Ý ÷ošE5íùl3¬"]QÐ¥2í÷k?ŸÛz¸Ò#ïX=M¨4ÿëÈ:%ÏAGÁò\"ªNL5ªGñí-ç1æù뾇(ëQ”Ôr°&}3;<à ZÈÕ;7Q®*\¢ó¨"¸®‹c»ˆ ÍbZI’¬9󵿀»t‘åaœÑ]ÈágèmUø›¿ùò¼–+ãСC|îsͨ¾½íUø÷å9.Z顦×R.ðùøG.¼ð"6mÚ4ïzßóÞ÷âº.ÿöï_åØÁ'ñÌn”ärÔø"„Qç§óE-WfX©Fœ±ñ“‹Å9õîMÓ$dÈbya+f®q6ÞÓ\ë™ù¡"¢]x¹ÇÁ³ÍùÚ}™‹ç7ã*–¡§°².¥ñ BɤŸ)ô<UÞÇ@úÒé‰*žžÀï#Àï@J°pÐ]Çïbø/ZŸ&ÝÕæÇ«ŒãžàéÁ?^%¨6?­<¯%† !({-ùÝ”§8(ó˦H eštÅâ!«*×ww³kt”#““!(¹q]'©Oñœ&Ëer‹KNýJXÕk( µ.a “5XÍkmMCçò\ßóס¡ÐLêô0IÍœVºPˆ(:ºð“ž”ÓV§"øpÛ&>ܰåÔ}È\PRñÿÎÕÕ›ëî=×w¥‹"˜·»²çyx’qs[S%%%²<†3ü,ÖoÃþaEd'Ÿýèë¸ûWwqÝu×s¶‰D0 Ãgm+ ×Å ”è"ʶ‡w–Fáp˜}üãÜÿÛûøÆ¿þo¼¦‹^ãi”c_Ã:ð5ì3÷áe¢‚Áþ05ÿªjˆb±ŒeÍ톪ªêOÏ™û<Ìšk/ÇŠ’³^εŒí¯Œt²Õ=åa¼É]L+ʮٿ™¿)´(ž4ÉOÛ’JÖp&7 $ϘFX­§¦¦p! +Št|ÀJ77ŠG Åch†§êÜyü(¯íí¥ÓHýfC«&UŵR‚síÑ(¶çq:Ÿc]}­‘(>ͲdŠ‚ãày»2LU›ƒÑ>4®ë—U ›RÚ^³Š9âXSÏâZMý¾à\Àª2tEE—êœË‡*|/ ¤¨S²Ó5Ç2¤h|®ëRÎXY~rò^Ôů#…,ú®P¨Ál«LÞYTûrn+láϧ†n\UõØi¿S)YÃÍÂÛZ:AGƒÉůÜÈ7~Ë_±¶¶¶sÒhmmå‚ ë8øÀ~h¨µ˜äì§ÜÜ zºZX²dÉY×-„`ñâÅ,^¼˜·¿ã ðÂÎ<úèÃ<ñäÓì?ô,“Eö­/-Úé×vΤ§ P ªá)Î5þVÔüoúë7›@ ûÊ F ®'¼ÂqDl‰ŒG.|+¨IŸ@*¥ïrÆŠœ‘é•A šž‘¤f>'ðËÀ€• ’!<×#ZŸFÕ4b i‰²fOvˆoîÛÃßmÙ‚®ëÓÀЬ*î_Øô)ŠãÌQr°À~ Á¢xOJú ¶µ·ó‹£G¸eq/aUÃò\ÂꔂCHUç+Û‘d ¾’iÌ0 ©÷1°²\wAVíóÁª:^X½LÀS…‚‡dØ.’ÒÌ)‚)’”â =WpúÀ]<~ú·¨Ý7ùÃÒ(8%„Yz´&LT<æsgíCílç"2 ƒpØ„ÉâÔ—zƒ´2x™#¸£Ï£ÒÝáâ«7rÕUïå’K/¥««{^Ëm¡¡ª*[·^ÆíwÿÒÉ#´ ãð\à,]dö(ç_qÞY;ìÌš¦ÑÑÑAGG×ßp™L†ðä“óè#²óÅé?ZÄR›±%¨ñn”PÒÎI†çt]Çqýùx–ã;ç9šçåoÖ|ĵ"ÜŠ,Bl©ÿžQ5ƒ[ ë,C((f…‘¸år•<*TÏq‚°‚Ê‘mpÊèã{‘fÌ—,÷÷9Dá´`Õზ=3!ˆ¤“4ô´s|W?z]7ß9|ˆ5õu¼eÕ*Põ©Àz¬‚Xfè† 3ÌÂ…†*ºã ò¶Ãæ¦f~qôÏЉ’±lÂa­¦&púyž(•p\IƒƲ]\ÏÝs¯Þ¨X€ØóšãXg«šù¼s漚ý“sÝ餤+”âŸ{^É›þŠÃlj¨;%ÔÖA¼ò¸ï©f09Q¢í~J[ S-¡™õ›3­×cÞ,[,£«³ƒgŽƒ»iMâfOâMìEɦ%-¸dÛFn¼ñ½lÝzçÜtu¡±yË…ÔE&‹ˆx ¿5;XNaõ³eËÛÏÙ‚›k!H&“lÞ¼™Í›7ó¡}„S§N±}ûs<úÈ#<ûÜvŽžxšLAA–ó¬Üz ñx|Îu•ËeŠ¥’¯ú¹ «7×ùXàóy?žcECD{ð†žâX:(:jjSU–!%¨£”Ù‹•Ë!T)%ªaàZ6žë¢šv¡ˆ”µ<ŽjOP'S8GÀjªÂBžç¡ÏÉ0èX¿šcÏíG«_D1ÖÂ_oޤirãÒ%(š^½ûN+ÿ™ÿYEÆu)•,lÇ!tÊ™c¿q¥D¾2CÁqØÔÔÌ#}g¸²½“‘b‘F3„P¤Z5ñ1!ñe‹Õ ¬F((О¡jÓ­§ h9Î ÷fYVóZCÌ V³–ÿ#¯f¨BШGªr5ӜǦx_뽚oì䩾û9ãÚxv©ÓÝÑids9Ɔ3dÊ@¬ ¥éB”†M`¤˜:3·ÍO(:ŽÔ®‰SÔ]×¹õµ¯åGþœÂ®“JΆ0ç_´ŠW½êÓ\qÅ,Y²ô²¤K—.eÅ’nžÜó;´®J¥{Msiçp†Ÿ&i–Y¿áÜÔIÏu†Aoo/½½½¼þõ·111ÁÑ#GØ»w“™ ×\sí¼û<99I&WD$g÷öû£­¨êÇg?Žåq,aÔû!„³5ž™1„žÄ±…Ñ1Œh¡(h!ײ(g²„ëÒÇÆÉöõûn³¢‚g!§ -µÏ^X° êˆAà¡à9.B@a^#i˜LXu¦éûÆš†åx8ŽGX׫…ÇRú2ÆZȧ1hÓè øÏ„ßÙy˜ÍÌ ÂV |v¶uÌ1”ù" Áļ2ÕÃå‰.Ž•'¸gì?ÚÃaQà²+®àƒø <óô3¼ïïgbl7îø¼þGQÝ‚¨[¬kSQ ã©I<8ïö½þõ·‹Å8°?ËW¬àüó×ÓÞÞþ²Eò^ÎH$|ú/þþéÙþâ×(›ËК.F‰v!ÝîØNäð“t5)¼çfÍš5ÿeÛ"„ NsÁÆ\°qãY—?sú4™œ…ÒXqpþ õ2]J „š}áAk$`öŸ{§²¡†"F~h;ÈEÑÌ_Z76Žk;X¹'N"T!¤PE˜jœ«™ÀäZ°š¨(+*Rf›Ä.• Å£´¯[Îá'÷¢Å 7ö2:ªðw;¶s,›áÖ%KhŽÅhŠÅH†B(B™ó 2(».ŽxªŠâÕÆQ†ªø}ÿ¤¨l …¹¸¥•å©4÷Ÿ>Áú†NårÄ5 Їãz,S tÛ«.] ŽP9)seU-°²˜Vâe“:çšOà:^Þ\©y.Ñ„Bo(Íû[6ðÆÆÕüxx_ýñìÙ¿Ï}æ³ôô.žŠ©H9ygïWPÝŠÒöÊš;kÛ-„ß°p’Htþ˜†iš¼æ5·ü;ñÇ›nº‰Ë.»Œ¸Ÿï}ï»<þÔ÷ÉyM7Gw³à{oû;X¶lÙåþ©ÇØø¸ß´µÿ!ôÎ|Ò­<‹¥Å\ÿ UóBhQD¸¯<ˆ;{¢9‡Ðz™3gP4ƒdW'Ц"=Ìé3Lžò³Ø™¾~ÔPŒ J %èö$À÷ötÀšÕ„¢†4zpHPÕ$Ѹdéå‚R6G8§÷첇I¡…S”<æ(“¥"íá0«Lβ1TÐ<± ]Uˆ™!C÷ã•€[Õšñ¹]¦¦b¨*ª¤ “²çrÇ‘#,M¦¨7C~Ѱa"\?Uò*ÍÙ]½…^N{S¨Hk™Ù]Ìü]²B¯Lqp é¾ôb„œ|òiN<þ”K”FFÈô¢ÅQC ÕÉ¢xÁ55ü„¹«¦-Òë€MR‹R4Û0œ!–_q1F$Œ•Ë£êžë2¸g/j8ÐM´PO³§ÿ8Ç'ÇYœH¢ ÁH¡€í¹Ätcªórí~…ÒÁ‹*h9®K¦hS´¬€+£« º¢ÒŽðìð O°¹©%ˆs©Ä4ÕSÑýrйxU3[wÍìŒ#„XA¬öœÁjA÷oö:)™pJ˜Šêמu2½¼ù蓤4”^M]''TE¡Uqm]/n¡À÷žyL©8›}-dö(J¬ióÀ³‘¥œãw±q‰ÁW¿úÕ9;Áüÿi†Á’%K¹ñÕ7qÅWÒÐÐðÿ; ª Ó4Y½z5ëÏ?Ÿ‡îýFOíÅÍŸÂÍÆ+ô!í H!Ú9ÉÙóó| „O3Ûî@¨¦Ødçzü$nö0Ï'ÖÒÌ¡ûî§88È¥o}=¿õ6V\~1å|ÁТõUGs²Awh~æ,x3°|Y™rbrt?Ë.ßL8•@zålŽxS£ÇŽ“AÖƒ¨F·œåÄÄ0»GFiÅh ‡™,•È”ÊDtÐÙ‚¬ihÙŲ_X ÿ†4 ¯qÕñ‹cGèËçYW×HÁöâ Í@“™CÅÿ««9²Ò« ¨é~WžXë|™`…ôƒæQŘî¦.8±œ8ÒcÔ)b( ª®ƒ¡ªTk7 k>WÍ” —$:iP ~7t˜‚7‡¤ˆWFæOã•FpžÄ;ó Úðƒ¬hüÃ?üOÖ¯ßðlìË®ë’ËeÉf²¨šöe«™ês=¤R²k×.¾ÿýïñãÿˆ{ýkv>ÿ<ýý”ËeTUÅ0Œ9»\ÿ±£³«‹eË–á•ΰ¤YÒUW"­œAͽ„=ô{JƒÏáNÀ- ùü:Í ú ÎÈâý¡y)AhxÏ#ÔBOâf÷"Tœ,²<ˆÐÓœm¡"‹§ˆÖGèþL]媽ŸE×£:F$LcO7Ç·o§˜+¡ER¨N¥*ÓLø`r¾3®Q¡4HбŠ'\&Î îl#”ˆ¡š&°êšËyæû¿ÀÎ ¢'ÛüßPM/Âmkã‹;vò¾å+¸²³“Ér‰}ÃC,M×Q‹!*&Ã4Y.¶ë"„tµšIÀ%M­|lí:¾°s /ñ®e«À›Ä²]Ú#qt@JÛ¦+j)b–è¸cåõzdŠ /„ªÂ̶ZçÈ“šZ~aÀ›)##%”=CQç¨/0ÿLE›«`Tª !ˆi†ln‰˜§` …w4­ÃPT>väAFíÙ]]dî8íI‹+¯ÞÆÒ¥ÛX{Þ:6nÜD[[Û¹lÝËRJ …£££œ9s†£GpðÀ~8ȱã'Èç ,[ÚË_~æ³lÙ²åOþûµãñÇçíoÇOŽB¸Ëgλ94òÄ–¦4‹{±bå V¯ZͲeËéê¾p8üG[q×^{-W_}5žçáº.¥R‰ññ1N<ž}{ÙùÂNvï~‰ÃG¶3|drZÓ¥#ùòâ^rîeª-ìËÃíõo^ö$BèxÙ}(f+¨!ŠzŠ3Ïm§{Ã:¶}à¤ÚZª¼,¤$ÞÔÀâÍØqÏc鎙ÙÈ0ca!ÀŠUvÀ¯C«ÃV 8LÏ…ëQup"Nvh„Æ%‹X²u#{ø=j(ŽJ h&—óo»•C üÛ}Pr]®[´ˆ¢ãppl”HÒÑr¦‹XsÌU%5=¦éµK ®ÜÜÕ‹ízüß=»|1Ï»–®Æq!k9t†ã$4ÅõHŸ¶PkÍußù&oYDTM¢|ª¸†Þôm;XÍE_˜õÙÜï¹ÒcÌ)ùt…s˜ð®ô(».®ôЕD(º¿¶t™°J(@XõUW+cÒ)£ISøÍ_ßÔ°†I·Ä§>B¼¡)%cccUBnCC#ñ—ŸeõêÕgݦs®ë211AGŽáÀþýìß¿Ÿ#GŽrêÌ£yòeWI€Ñˆ÷€fσÛùsîºë®—Mø|9ãŽ;~Îñãgп%ˆïzÒÍ“-3™dß3ýüúÑß x·1]šêâtwu°|ùrV¯^Íò+Y¾|(ÊË£°€‚¦i˜¦I2™dÑ¢¶^vRJòù<ÇçþßÞÇ÷¾÷=^:²­ûM~C (7:KÌK(J¤wt;*j*°¦¥‡š¾Èçg-8ÒCZ£,Þ|Ûþì]Äê§Àª²”t¬]É ¿~i—ýLáÔ09 `éø¥9ÁÓ‘Z’]ôí>€]*£áT‚ü¨O@\|ñF†c´ï(á¶ÕÍÀ.Œ°æ¦ÐC!¾u×=èŠÂÕ]]]—ìQT¡R»UE™Ö†«vA{8Æ-K¨3B|óÀþvç3ÜÐÑÃU-ÝdË6õFˆÃפÒ]_­S*JMý ß6>®ä-Ûöíö€O¦k m¸fžÒs †VÈÊœËÏ÷¹ÿFCƒ_ìðÂŽçù—ÿó¯<õÔStwwWcói“Ï7 …gΜfÿþý¼øÂ ìÚ½‹C‡Žpf`”Éœ‹%£`6@¸%¼¥­EO ªá ^ã;¶^´Ýû¾Ç¡CÙ²åÂsþý—; ù(Q_ß¼Â2Vt„’BÕÓ¨±ÅTãzn‘²5ɉÒG_àáíO#œ_cjmÍI>ü¡?ã#ùÈ9»²RJŠÅ"Š¢`šæœÇYA,cÍš5¬Y³†›_s ú³rß3¿Âè}Çl@9¢f)A¿ÇƒN:€WF ~Á÷‚®¥@Ú8#ѳ¾›+þìÝÄꞇçº>‰48Òó¨ël'šŒR*çÀTk·Fç,€eP#é „ŠR¿Œ¡£¿`âÌ ‹»ÐC!Œh„Òd†P<ʲm±ã§÷`ŸF1b¸¶ƒkÙ(ªÊò«_‰kÛ|ë×÷Ñs~C#Ãù"ûå8+ëê0C†oÍÌ8^þ~+S¤¿™q!éüvG\ݲˆŽpœ{Nãî“Gyxà4W·t³µ±ƒV3FTÕIé&Mz„¨j“O­–@ˆ@ Ï•EÇ&¤hA-¤ð‰®ž3•½ €Æ“’‚kVtT!°ƒ)]LíKÞµ({.õZxÆþÍm 8g°Ê:ÕògMEªÊ´¾qÕôygH€+Óx–CÁ)c(Y×¢^ c ñ—±cï/ùÍoËw¾õ-’É$K÷òõ¯={öÐ××G2™ä‰Çç·÷ßÇâÅ‹yãßDggׂÄáÇùèG?Ê‹»÷12QÆ"¡6Dt5Jº¥¥C‹Y bšŒ²x5Œå@.—ã¿r¬[·Ô;|¶wu[ª±æ¤©5†ˆÄQ"huø<"·„kg96¶“úß_âꫯbÕª³[¨ccc|ãë_ãÞßüÓ0X¼x17mbË– Yºtéœ*‹/æcÿ8¾þ]8ÖD 6XKœ>¦C œöPy!ÌFPT¿Zºx™—ЯœRq˜gÍÒÉáŒBYmE±ä»ßý6œÏ§>õçl\€¹wï^ú¬%èK^9N¢Fþ¥ZŠ2ÇÉž¢æ!Ý"º:-«ý_2.»ü2ëbŒ‡QÍJÇó¹Ú5Èê †Q´(Zz-¹¡çm>0::ÊóÏ?Ïû¹óŽ;xâÙƒ8‰ þñÙþßúûhHÀ†óWsíµ×²mÛ,Y²dx  âT úç«9%}È -ŽÐâHk 5¾‚Jš¹‡?‡¤3‰3ò ­‹£lû່75L‹Y¡F8LvxÔS0 4ளSú‘r`…`aÀšºWšqdz9GŸÜÎêë®Àû–ªk¸¶ƒfš´®^ÆDß {a4ÕûAkÀÊå %¬¹ù?zŒßœ8Η,Ç‘’áb‘¸¦û-æÃ! ¨î®hi)L›Ä38TEˡ츤Í«âõ$“.3Á5-‹xbä œâÁÁ“œ—hદE¬‰7·‡S„…@:6º¢’ÒC>á½r‡¯=‘Š„=•ISÔéáj†ÎPjd+X‚R%ŽôÈ;6 Í'ÓV£2‹¸ÖŽ’gOYRšJQñž_¾Ty¿²ZOJ?¾åmYä­"±ÀåÔUcúmVøw»mÉEÜëäßü›7m ô‹$;w¾ÀÎcˆô:Ô¥oÄ5#í ‡Çvsð÷ÒÞÞ>/`õööòêoàÿ~ÿ!„ ,)IµôËì‹Éß+¯8L}*LKK+ÿ•£·w «W,á‘—¢&W2 8ç=O3·[â¨OEÜÞïÿû|ös‹­ÔAt Úâ÷bD‚¤†t‘NžÑüiîÛþ÷?öÏ4&ÿ7ç­^Φ͛X±bÑhŒ—^ÚÍ·¿ý]ÜØyhFªº/¤¦ Õ£ìI:J¤{Ž/ ÀCZ#xù£¸¹#Ôw¤ØöÁw‘no³‚H*IvdŒr¡@$•ôßV?ïZP+Iä›Û –Vû™ó]#µå<ü¡ƒGé\¿Í0ÐC!\+‹¢©˜±]¬%Ó7„P­bYxHÏ#ÖÔH౟ÿ'ÛÚ;i G˜´Ê”\¥d£**zÈ'ÉM–-J®CS8>W¶^UX 4¢PÐ…`Q$IZ S¯†é2\Õ¸ˆ“C<0p‚<ü½á74÷ruCçÇ›ü>ƒ®çc¥¦MS “Ã^•%¥oiÕ¸sž”ŒÙ%ÒZØwçæ Î;q-Çè·òdœ2ˆ©:­FŒN#A³E¯öœ=«*µ•þ™ò-«²cÕ Œ(jPU0f1×v°m›„Ð)ºa¡Í̾½6jˆiïn=ŸûöÝ÷¿ó8ÀÞ={Q„‚hX¨Û€jö3©F ­m¶t¹ûî{ùЇ>LOOÏìÕ Á;ÞùNîüåÝ Œ½ˆÖ°ä\Aa9Çn×^h/{”åÓÔÔÄää$–eQWW7/½`tt”‡zˆÑÑ.¾äRÖ¬YsN±·X,Æ¥—^Ê#¿ÿ!8PÃÌ}¥/nwl—Ýzɼ­Ð,Ëâ±ÇÅŠ¬Ãè¾%ȼ jµã…CM­DM®@:9Fr§x`×Axæ.ïG(ÂÃ%„’Þ„ÖzQM5žœ{»æÝà™ „A–Çñ/¼Z–iá•ðr‡å~¤“'ÑÔÀ¶¼“¦ÞžYvÿJÌh„H*RsÎâõ("Ð&›bg‚°ì|<¬Fà]@ $N¬ ;Ñ H„ÅØGØ(ѽq¯^[,cå  (UÙ§la-_¶Õ0ðlÍ0P…p*ÅÁí;h´=–§ÒØžGDÓ‰¨®'15Õ/’T EESTÿ¹ó±ÓÕ ±CEðNHAXÑ©3B¤µ0tÚô[êZY•¬çd9ÃöâùÉ!ZͽátÐTA‚çM±Š¼m“wlŒ Нöi¹¾ò'ø£!T< EÇÔªÒ-‚¾r–;GöóÅSÏð/gžãöá½<2y‚ç²ý<“=ÃÇùÙÈ>~2²‡‡&O0á”hÔÃ$µðô2P9u-$h¾&VRQg„ýcp®TÒ²)–+YBM¨„TÍwIƒ©Pö\\?3 8HŠ®MB58Xá»OÜÏØÑS¼µiG‹¢Ý(fha„Ä¥D1SŒ}œŽæ_|ɜ󿥥•¡¡~žxø×(©µˆjJ|>kjŽ—nwð~Þôºk9|øŸüÔ§øö7¿ ±aÃlNØÁƒyßûÞË?ýË·ùÕ}Oò›{îdÑ¢nV¬˜Õ }Î!„àΟÿÇX4GSˆ³€PpÆwРä¿ø¿X´¨gÎÅ^|ñE¾øOÿB)yJ´ƒy‹Îƒ›˜P ”p#jrjÝz”ôDzjãE¨ñ%¡27ðÏslçü(°Ñ½Þä°3(±^¦8ŠE¼ÂQ܉çðrZ„A8ÛÞÿvz6m˜«7¡(¨š6ý¦!¥\Ž=ƒjÄÐe‰*bÂ=ÀγV$v¬;¾Ø_@Õ‘vûÌ‹,½l F$ŒkÙ32¡”Xù"™þ![Ò³õâ E½QERÍÐ9vœ‰“§¸°¹µZ–’…ð<‰&š®ù©Üà"BˆLä9ëU´ä2…FZ ‘ÐL\RJˆ Sm¬HÕ±;7ÌOïA8/ÞhJI?#€VÉu°=³æBÏI—²ç©`¾ô9N»LÙs|‚«ª3lçùjßó|úèÃ<:qŠ¥á4oiZÇÛ.àƒ­xwË:ÞÞ|on\ÍõuKè ¥² ü|d?ÿ1´‡1§È’pš„š6Ç4¡ tEU‰h:QÍœn5yší¡yþRtü…2µœð-µ;‹*„‡,éﯪBšÊ¯‡ñþ¶u\há—#ÇpRkFÒ/VÕ"ÕíZ×Ê3zô n¾ùæ9ãKB–,éåw÷ý‚Áá jbéÖÌ µ6#¡à厑vwÑÒXϾ÷5Þ¼)ŽIž§öôñú׿~Znxx˜÷¿ÿ}Ü÷ØÔî· 5melt”§~÷Ö¯?ŸE‹q¶‘J¥xàþ{9ÙŸA‹/eAkjúÞ"˃xýwññ½‹·½}~ ›/ùKÜÿÄa_ÒYÌL>Íïzú­ò„bT‹‡V]èí9>—²<‚—;®…]Œt2xÙý¸ãÏ!K§QÌfÔÔ&PB¨Î .yÛkYyÅÖêvHÏ£œ/ ¨jµëóøé>´9ͺB`å ìôI„Æ \»M÷;拜MË)âìÕ¡Ô/cb0Ïð‘ã~I¡§ hBQ(狱X wÃŒ%¨_ÜñRþBUr¶MÉõcCeÛñÛÍÈæ]œå̦&=ÈÛŽß Ð£ª+šçsðãfåŸí,H wì÷8ƒ÷áå D{К®A­»éY}ž¯yk¯¾b^3YJY?›+„ ˜Éòè·~Hfp¸Š•RuÝwëg»‘>-lþ£]û“µ.‰‡×c«)öø’"ª®UX(>¸•³y"uéiZžçonÆJÄyatŸi›Dà\Ï›.¡“"Td S½h;”·ªÌP{íŒ[eJŽ3UjãåxàA£¦ÓŒ!Oç5uËø‹e›øíøQ>¾ÿwLØAIÏUiLU››Vð-*Tµ:Ó šÉ‡öò–ýwÓm&¹{õëøó΋è6S(’©¸Mµ˜ÕÏ©B ‘x@›ã}-çsÇŠ[¸(ÞÎûßÇ¿ö=‹Uó©Qmõ¤$g•˜,äqÊÖœwÙ²ç2ᔫ™E[º ØyLEÅPÔ*ECVvF@J3¹(ÙÊýcǹwô("¶xªeWUs¼f¦„›qëùÎw¿ÇÈȈ>]Çqpî Àmox×½òBœ3÷mÈ* ?ýB­\˜"x-Ëc¨ùý”ŠEš#6ïܺ‚þÉîâ—_>-†õâ‹/ò­ï|Ñð D¨e*¸¯í×±}ï_üâΩô 7ÞHsJâfN¿Îèïi ð7û· ÖZþà‡?àØ€‹šZSs˜û‚œRs-ºqvª.ë ËCH;‡tr¾•UBZ£¨ÉóÑš¯EM®Gi?~5þ$ç]}Ür£Í׬:œˆ“jmöß‚Âø$å|=š>O%(ªâ[aóX‰óÝçËÝß2 ÚÂèñSØ¥2V¡„P¦„ú<Ç¥œËknòéŽ7åà ßDÔÃ!Ò‹{x|÷^®hë$¦êŒ–J4„¨Bõ-,OÖ”ÒøuÝx;e×óÛÆ;¾q\áM é_h:~Æ®ba™ŠŠãøvªI‹ãxq’ár ¡Vþ¢w3_8ü,ÿëØ3|~Ée~vÏõÀr|Kª†eï?ŠªVž„o ¼Èß{Œ·5¯å¯».%$TÊ®…¤ðsN‰CÅ1F(ç(K ¤õ0ífœ&=BB5©ÓB, ×ó¥žW²6ÒÈ?õ$Ïá/Ú/ôûöTv­`—)X~_W 4uö½(¤¨J¥\D¢ A½ö+„ðùbÒ©äWi*Éæd+_9µO‹ùÕ´`®¸ë8E<{¡ÇyîùGyïÞO<grrÛ¶Ñud2IGg'çŸw7¿æž|ú³ŒŒ<ƒÖtÙ,kj–«(Þän4{”ɬÂgnÜ@S]„ï<¾—¢’âš«¯š:žÇw¾ý-N˜‹×Í¿£‹Ör5?þé/¹æšk¹ùæ›Yh¬^½†+·måÇwï@K®\¸àX¼ì!”ñGùÔßýÛ¶m›wÑÝ»wóÿñS”†Kz¼z3û£²zs~t¶Ìs gÄ-û–UÐLC掠5nC ·ƒ¨±bKƒ¸£³êòu\ôæ×£›Æ,°©µ¢„Œœ8‰ŠE™ÖBEøçMV¤“çóÖt=Ù™„M"\Gaâ}/`ÿ³êê˪?ZÎp,›dGx«PD„+ß q’xc#ûó98s’[-!oÛŒ‹´GcþF»®_¸;#3è)*Š®‘RÀºòIžŠ¢VÝØªûË{Pöl2¶E½BѲV™’çРFX©CG¡¿”cÙÄ{º×òµ£/ra²×4®ðWàxàÙ>hU¾ ,0gº£v×Èþþø¼¡qÑyEcÜ*V5²NŸïåöá}S èé8ÑTUU)‹dÇ&}E–é)^U×ÃEñv–HV#Æ{[6Qu>süõïk^_ÍVJ$N@µÐ„Rí˜]; ®tò9u?(_¯‡k8þûŠ€¨®ûEæB°"ZG\3ɦÏóEÝ€’å1Üò(^þ4N£”‰Ê,:´%ã¨Ûw ‡h7 tMÁ’.ãV™»¾Y¤ŽQ*æðÆ@F»P¢=T¥ŒgeÍ8Y¼±í”,‹‹WµqÛ…‹)Xw¿ói. ·òö¦µ\–ì¢ÓLðæÆ5 Xy¾púi΋4qa¢¿·Ÿ ª˜Òà ªPp¤HBû­ÀŠžƒ®(„Q«·oWÊÊ!PCðƒòšZÑ]’4›êŒfŠÁËFæŽaØc´ª’Þh˜5é:ÎK÷°*UOW,Nib¨~ÜL ‰a((ºЇ­¸ ;žäöƒ‡ùÍÁSLž¸½ã&”hw@&­¹¨¤‹,Oâ ? ¥S¼rCÿò¶‹hHF884ÎÑÁ<·lÙ2-Ftç?çôˆÄè]QÛ™9Œ¦Kyzû÷¸ãç?ãýøà‚ÛÖË.ãâÍkypÇs˜Ñ.f©" Ö8Ö©;¹òâåüÏÏÿÂÄÖÛï¸­åæ yáý[Q5ËÌ\TºH{iONQ)¤7¶¡„Qc˧Íu¯4€;ú8Ë.ZÎ+Þ÷vÂÉ8ò,e„¢0zì$ÃGsñ[o›Óõ³ EÛÁ«3¹ÃÕÞõs —ÚÅ¥;kéÑC&®í èšO í£ÇO“èh'œJúµfÊô;ŒW¹ÐA(‘ ÚÑÁ7`´\âú®E •‹¤C!âš´@Æ-…èL-v7”Š~•Wñï}pQ„껋>#MQˆ«”m?«é¹$5“‚kSpÞÔ¼ŠÏçŸæcä;«®£ÃLL¹}n ¥†keK—:õ{tTÞÒ´†6#V%ŸÞ3v˜Ožx˜%[7ñoy+~¬aÄb1b±‘H„íÛ·³ç¥—¤è:<8yœ£å >én涆U4è>غgs}|þô“üdÙ;%‰_ÊS)çñ¤dÄ.RTRZEâš1Åe£p·|Ó´ªee¿5ÕǨ¦“Ò äès¯È*®llg[ÓrV'h G‰êºžð¹<2–EÆ)Sð"!êU;¸(Ûô87uĹvQ7OlìçßvìæÃß%§ô ËQÌ:^y™?Žfdy£ÍÛÞ²žw]¶‚¦D‰Ç@¦€åúñÊèëëãÎ_ü‘Xçk8ÍÇõB"Ì&ìè:¾ñÍoqã«oZP}"‘Hðö·¿ƒÇžþ$^i%TKH'‹uêl\™äÿüߣ££cÞuõ÷÷óÿ똽˜‰s™ù/©êå]@ZcàÖªtøn­,žAmx(•öñ¯poüV^v—½ë-ÜÔÙ·Cz{x„xS#=]³>BPœœÄu<¿ñîôÚ]æ,§²€˜ÙqDz"¾ºžÜè8‘dUÓð‡üègXqýõUkª2´PÈO]fsÓµuÍ*Æq~ôÜ~?ØÏ5‹¸´¥M Í´†¢ˆê:I´å²½ŠeP˜ÊVI¥áÕÈËÔfý?C¨Ôëa ¡q ?J>ÚuÿpôÞ¿ï>þ}ùÕt‡RL±o™V…Ç'NpߨQ>Ò¶‘#JB5ÁÃÇøÄ‰‡¹ø5×ñö·¾•p8L4¥¥¥…ºº:B!Ÿ‡ôƒü€OúÓ³š9-MðÅ3ÏÒB¼®a%QÕäÓíqÛ_rïø^߸jÖ©±)•©¸aˆª“"šŽRkÉË–.!MóU^•”f°ÒÍò±Þõ¼ºu -ßÂÍ;eÇEjÎ'Áù‡IñoV†¢` ¨P°m,×¥^7‘®_pES¾º™'‡û¸sßQ¶Ÿü#9E¦„ÎÚU)^±z%—/k£=ó‹I¦l1Q,û`[c]ýêWw±÷ðz÷s$f_¼zýf^Ø÷¾ÿýïñÙÏþÕ‚ݵ×]džµ_á™#/`¶UËç*Ùg~Åênøê׾ΪU«æ]‡ã8|ùKÿÌÓ;Ob,~7~R¾vBÍ3^n<ꬋ9U•©±ªüÉ!­1¼‰(±åSÀ,=Üì~D~ç_)¾ñµ¾x`¥¨ 'v¾Ä‰»ØøÚ %âsº“ƒÃxRñÃ;5¡a  ó–ü‹×ˆº ¬äûH¶.eôèIêuT­¨}‡ŠFã²|*%65]_SÝ.H´430TŸä{Ù!~râË"1.lje]º‰ÖP„¸fú¥/Ø®GÉu(ÚÇ¡è8Xž‡¨(„:-D³£E’ÔBA ¾JÌTÀZ ºP1uQ=Â@9O·šâ/{¶ðÏÇŸãÍ{îæ_–^ÉÆx ¸9cnå2_ëÛI—‘`]´‰ˆ¢R4úÊ>wüQV_y ïxÛÛˆD"´µµÑÑÑ1­ÃSO=Å¿øEZZZRV³k•qªœáË}ϲ6ÒÈyÑf.ˆµpUª‡ íæÕuK|JÂŒ¡×CýÇéÁM¿Å4K E×jýib …°ªòÅ[I›&‹c©`y‰‡‡ƒ‹P¿2°®PÀ’.9i“ŽD ÍQ+’DÈÀ–.“% Wx¨š a„=•Wµuså¢N&e™ŒWFè‚TÄ 6P´)ë ¤"éÏ(-Û4Ý?CCC|÷»ßîE5ê9[þÈ·²ê }!ßøÆ·¹þú8ï¼óæ½ðêëë¹éæ›xæï¿‚t¶"´8xVßoèNó•¯|oÁZJ€;3¯û‡(ͯš;Ì#ó'´¢¦(‘N`UyåÙßñ¼±í%‚šXíÇåì,Îøó„´6¿ífλîUhº>'è” `Fý¦[BQÈ ðûŸÜIûš´,ëE›CÀÓs]FŽŸDh¡Oªë>'Àª6š2H_{.Š“Cî"¤%ãŒ?óK.ÛŒ”’R.ÏéöÒ¸|ÑÆ†yÊä/-o"V(Oã'N¢×|öÏ0£Q<ú4'^ÜdzûàÙK4d" e´€÷å ÔT_–FSšF~2C~b’¤aPg†p¤‡Š/Õ²!Ñ̵ ‹¹,ÕIZW]<Ûó³KÔka´ÀÒ*çñ¤d±šæs=ñÕÓ;¹mÏ]üe×…¼¹i5QÕ¬N2 <›éçéÌÞØ° !!¦è€à;ý/0Üæãoy+‘H„žžžYŒ]×åw¿û_øÂ¸üòËÙ±cïyÏ{fñƒžÏ òãá½, ×RunkXÉ»ßËžÂÄ[ç¤1L‹ÍõÞ4°ò—Qâ „¦¦V3†¤[ÀðªË+Š ©™%¬;xÔUA½fbhŠ_ƒ«HŠ®ƒ¦ M ]‰D"#<ÆŠ%ÂR%©š¤ “´nb•d€T¨þ¬,YŒJLʸè475ðë_ßÃΗN w½kj?ÏjiHôôމ/}éŸùú׿1o“S€n¸‘¯|åkôMìFK‡Ý?-¡cüë¿~•Ë/¿|ÁŸÚ¾};ÿí¿ý5Yã|ŒôšÙ`õ'©9>ðl_§ÝÉΔ/{Y@k¼/wwb'M‹\üÖнþ<ß ›cÎIO2Ù?D¬> ACåR&Ëãßý1Ц±ôâÍDÓéÚF©þ¯ ÁDÿ Ž †fõkô€", XUqoaçÑ' GÖ$¥3»h\ÒF)“GQÒmäÇ&9r‚b&ÏòukÑLß‚°òT]C­±(*VÅ]tm‹±£ÇXùªKhèéÂó<º×¯¡eÙbvÝu?®íм¢—g¾'ª¢²þ†W«O“no#t£Ö ƒ?ÍÃ_ûU烋ϧՈ1lå9ZœäÅÉ!îÞw˜žPŠv®çæÆ¥DU@XÑ«å/®”Õø¬‡¤M‰ó—]òŸ£ù»ãóë‘Ã|¢c':0±rÇ'O°2\#=L¡ÒWÎò³ñ\÷ž7ÒÚÚJkkëœíÖÇÇljD"\sÍ5†ÁW\Á+_ùJ¾ùÍoΘz’{Æñ¶¦5¬Š6r~´™-Ì™Ó\P±ü¦Ï»©I+æy¯Æ²ªü…M5 ¾Æ~u²çààѵ  ª‚•¬ZZŠ*)jõuÞ¶,¨‡H™)ÃÀ¡jøVVÑs°p)—\ ¡ © K¿ _›ús…Ç™‰<’}§&ESh𯨨?üá±ÃË1²\溰µZóÜùŸ¿à¦›~½`—ŸÕ«WóÞ÷¾‹ÿñ_Æ~’Þ®ÿüÏÿΫ_ýêåĉ|ò“ŸàðP³çJªúZ/¤ÎÉŠšãkN~ʪšs¤5Š7ù"J|%gèLm„ó®¿ 7]?¥º0_6P@¢©3ñ[Ndxâ{?a¢o·Þ@²µ#žþ}!°Š%úö ;2Žš^„¡ªÃá«PY™ZCÛç—E”}Ökç†Ë>|Œæ½èa϶é{é ñæf­-´°‡•ÏN§¦Ž›”URhEü-?4Œ@²üÊK@xe;hm]ÎIµ5SÎæqm?†ŠE©ko#œLji®îtï–ì¾÷AÎôñããûùLÏ6‡ÛÙnãæºeœ´'ùÍÈQþüÐCÜ1°ŸOuofS¼˜ªãJɸUàT)S=ç"¸Š£ÒäÍõkØkᧃûxóþ»¹4ÑÁÇÚ7a ²¯0J«óÜAMãÃÇÉ$ .Ür!áp˜ööö9ÓÛ¶msôèQ²Ù,õõõäóyúúúæ<)GJìÈ °*Ò@Z ±6ÚÄŽ|?RzÓ©ŒçVµÓl–«¨H¼ªö–¡ûæ %~ÛŠ[¨R•ѯ€˜®+´êÂ!T‰'¤¯’jK„ÍÔÅMUÅÂߎt ¸…~Lç0‹7,eÝ o¤mÕr” ¬f¡!„ œð3†c§úxú?~ÎDÿ nº–úÎvßòš1<Ç!34Lÿþƒ8Ž lFÁ§ä§¬)e4éú*¬Ì ©özR­yr+¯¾Ïq)eóŒŸì£åüõ~‡W!žÄŒE}}ñ`¸–…çL1ž¥”ŒŸs$ >̇?üa®¹æüqvîÜI,›%NWòöGq¥DUTV„ë¹gìEÏ&RQ–œ ¬˜ñÞ 5ëQ™ñˆDUº¦ø”‡°úêÁ]œ,fye{'ñ(1C'jjD ]Q÷Rñ9u •m—¼íÔ”ßKdHjÄ5=°ª$VÙ¥ìzBEÓc…2ÇGr Â®ÃcœqyëÛ°m›Ÿÿügäe+f¨õ¬«¹.ò€æðü÷øéí·ó¡xÞoÇãqÞñŽwp.ctt”Oÿù§øÏûžEë~ ¬?Ëöý @ªrLÝÒ·¶RPR‚´‘N¯4Œ—;Ž(Ÿ Ù”¦{ý2–_v1-Ë—¢™†OE:[#U!Àó( ôí=Àö;îFJɯ¹žúîN-ÍsV¾äÇ'ÈÐà0J8Pô™Ì„2‡…³„“•Y̳‹x…z®½+_DAãânìr‰ÜÈV±L¢µÍ4}ÿU=Rívô$Åñ <×WtŠŠ[¶(MfèÙr3šiàyNÉòcb™,v©Œt]F÷¡'{°³CŒžõ ~ôóP»Þ„éX€f±Ðæ@M=u‘ödÀ« ²Û—«ñ]ÃQ¼B?²<ˆ&r$ë"´lé¡ëüWÒ¾f‰ÆF„ªœÉ³€¿ðÉàV¾@vx„#ÏlçÀ£OQßÝÁÊ+¶’nk%ÑÜ8Íp©|ÏÊçÉŽ3rì$ý#è~vUL?FEÎbayÀÄ´)%vvˆxcœÖ5Ë9õüKÄ›%ãOeÈ¢h:¡dÕ˜G˜^ú;U±°” ¨kDB´¬\â#¸')´‡±gÐÃ!ÆOõcY‚H$…tmÆÏ PœÌI§(NfˆÖ¥üº$±ú4 ‹º(ŒO²ùÖWóÂoàóGžæ#]°<\‡å¹(žBD1¸%¹‚-ñ6î;ÆCcǹkô†ðËQ,é±,šæ­Íkˆ }šËí ä‰ês[úÒçx)BólÚKól- ÞúÖ·rï½÷òØcQ.—yôÑG9zôè´e ÓÄs=†íãN‰#NTÕ±¥‡-Ýù-«?¬PÀ“–ôcI¢BQ¨¬ch½eÙ2¶u´ñ«SǸãÄþéňé:+Ò)V5¤éM%èHFiЇ¨…HE "!dXGª©ùÕÂf¤.AGx”=—lÉf¸Pb¼XÆEüöéÓœ×xÏ'nE×už|ò Æ aŒ–Þù© _u•C¯ßÈîßæ7÷ÞË»ÞýîsøîÜãôéÓ|òçÎ{žBízã vüŸÎŠš5Ü’ßø¡Ú&ËCÚ¼B^á4Š;F8¦P¿¨™–жr9 ‹ºˆÖùñàŠ5uN@应óòã 9ÆGŸ"34Ì’‹7Ó½á<’ÍDëÒ3Šœýá9™ÁÊùǶ¿€Tc¨¦_§:#†•ç,1,€Ñ©câ!Nn˜Î /ÀŒ„™8=@ã’EEàX6…ñI´pÈgµ«*sg­¦¸ªš†«Koö¬BR&‡•ËÓ÷ÒA¢õ)C‹5¢¢†“ûŽ3rì$Ýui\Û¡0‘! ùÒë†AâN<úf$Ì+ÞýVvüê7|þégxKÓJ¶Õu ä[í$yGÃ:^S·œ>;˨]BAЬEéÐ’„¤6M#ÝŸÕJ7ŠÁ.jB!¢hŠE¤”8ŽÃB£­­¯ýë|âŸàþûïç _øÂ´ÏãªAw^áÕu½¼iåj‡R€œæ¾Ncf<ù`…誂&ô*çi*¨^û|ê±=åƒukyÛªåìã±>žèç—‡‘µm"ºJK,BO:βÆ$«ZÒô6&hL…GTO ‰ãy‹9Ç!çø 6žÏÖ@ÕÏæ§Ÿà–×ûmæÇÆÆxòɧZ ÏšDh¶¯Ó%4ªýæ¼èƒJ ÏB:E<;ƒWÅ+öC!Ë£=Æ;ÞùÎ?¨ÓÍ®]»øÄÇ?ÆÃÏEë~3J´s:×iÞq6Zˆ§åS•­î•ð gp³‡Q½QRM1ÚÖ÷Ò±öU4-í%ÑØ€á¤<×å¬C<Ç¡”ÍS˜˜`üL?ÇžÛIÿþCÔwupáo¥¡§›X}cíyðuÝË…ƒ‡Ž0tôFÊ {èÍt 3ÌXƒƒƒ•Êò*H q c˜!AÛÚØ¥2ʼn ‰–&<Çŵlʹ<ši¢êúœh >#œJ’Fzž/Ò ‘îlň„ñ\—ìЮe3rô$¹‘1ÒáVòãB­K|¦»B˜IN¿´—öµ+Q5RÖW;$“¨ºN¼±Á²ÉIÍ\ùw°½¹‘¯ß}?'ËYnk^IƒÆEâ!Á$aÒZ¤zD¤ç[OóUehB!®Œ;¥€tï÷˜)ž=u ˲Èçóóž4ÇqbÏ޽ᚪS ºÍ$«" \kaS¬•Õ‘FŸŽQ-1‘ô[SJ ÕÔÿÔI›z>N¡XÄu,þÑh’t*Ess3ítttÒÞÞAKK étzAÖ|ãôéÓ|éKÿ›o÷Çdµ5=¯DhQæ,’žk¼\|w/‹´'|µŒü1܉]$ëËo¾ˆå—]B]gû4W¨ñ,J™,ù‰I&ú8õâKôï?D(cÕ•—ÑyÞj’-M>¡âaÍãe•³9&†pm›ƒ?ÍäкÂÙ‡çz¨ZˆP¤FÅ7ž¬%ÆGµH\š—/FÕ5œ²U ²e†‘ž‘WÈ RʪJ¨•˃"0¢Qð<_«)X®tÑú4å|‘ìÐh5ì#38BCO'ýûNb6wלG‰NP39µk ‹üš$UÓÅ|ÿ׈†1"!Š™ ®ãà”Êh¦IÓâEè‘»ËÿãijlK¶ñš†eô„üâç²ëÌ]!Qµ¬Ü)ÃET¡Vt–†ëø}¶²tQ„BÞµ¸4ÙIêÌÓ<ñä“tuv222BKK …BcÇŽñÐCñ­ï|‡ü¼µå<Þ¸ù*z ¨•^ˆH(”!S˜5w)9ZšàéìÞÙ|Š€œcVuŸ;UÙèZPª"ð<´†™\ªY`Uka1?XÕ™ZóÜÐÀP ©]õ!ºzÒ\½e1EÛæàÐ(¿Ý{Š»_<ÉC{w°ª3ÅMwÓÞáÐ@†'^êgÇá êZzøè'ßÇÅ]„X •‰DX´hÑ,Q)%®ëây¾N¸EQPUõrõæRJî»ï>þæ¯ÿ†{P[nÄH­ñw\Î5¡fÎ-^fÎ/‰Àª÷Yëåܱí„ôQV^{!k¯}umÁÓ{ù díR™b&Ca|’±Ó}œ|a7C‡­K±êÊËi_³‚dK3¡Xta  ÖiåóŒ÷õãÚ6Çw¼Èé=ÙüÎwP·¨ÛÀŒÐ÷ü ä¯ýæ`åà,X#ø~c ‹’îlCz7ˆÉHéÄ {ŽƒëØxŽ_”XÅ"F)ô<ך,Ÿ€ækce†ýïÛ§_ÜG¼©ìð(ÂH¢3„ÿ=ÑÌà¡cäÇ'ˆ¦SØ%_ÿY( F8ŒRÊäžÄ±mô‰ ù’‘f\Uçžñü>óW¦;Ùšè¢]¡]g¦‡ f«I)ñ¤¯ƒ¾" Œ(ŽÑ¢Ç˜pÊ,ÔñÖúU|åž{¸hË4Mcbb‚‰‰ ~úóŸqû÷Ä«DµáÍ,oêDh•É-ýëBÙÿ›uî}×è[ƒ/Uu.ItL1Žb±/Ƚò•H!V hM£8pšVfº*Añ()*žaŽè¾¤’!E²"n°bQ’w¾rOä;äó?}MX˜,]¾šw¾ïlÙ²™d29¥™T{ K9gƒR!Ä97.ýCG¡Pà‹_üÛ÷0z߃0ÒTú'Î9^QtŽe¥D:Y¤5nÉoþ0¹ƒ®Õl~ýGh[½Âwû‚§cY(ª†¢ž@Ô«P¤0™¡89ÉØ©>Ž?ÿ"#ÇN’libíµWÒ²l É–&Bñ¸Ÿ<«U….Q96“h¦‰PÊå?ÓcÙœÙs€}?ÁŠë®eñÖK«ßB ëûOžªñì?\µÐ™Ãv¥‘’p"F(èT湺iâ”˸÷,FJ ”D·OÊ®í—(úlwB•s_’Æ4ÈŒ1~ªŽóWqè±íhñÞY)Ñ"iŠgN2zâ±ú:ì²…cÙázÈ$QÊ妈ªBñåWu ËõÐc Dà &sÃÜ>ÙÇýc'ÙkâÒDKCuÄ#ȯ,<­<)i3b¬ˆÔóðÄ .Š·3é–q<—w´®ã¾}GùÆ·¿Å_|êÏÉçóÜþÓŸrÏÆ_6mäýË.%Ô;+ǵdC¾4Õÿ°fºÚžKÞ³ùâ™gøíÄ1>ݱ…V3Š®¨ÕLä\àÄ8M[Æô„$k[„t-Jž´fà™Çbn°Ráb‹L\êÔÑPt…’åµH ¡8\¾¦•­ç·ð›§øû?O8ÚÈûÞ÷^z{{qÇï¦2çu&¦Õgþ¿¦i’J&!¬`5Úùƒ¬¨9–•ø¥5ö8ÒɃ[ÄÛ)³þu¯âü®&œˆû€üe‡G±ËeÍ(ênn…šËS˜˜¤”Í110ȱçv2|ôéöVÖ¿úêº:ˆ7Ö«KûÕ+µ•”3Y?k³9Š™,©¶Š“&úq,‹3/íçÅ{ ç²­,{Õ«˜8uŠH:úÁü‰ÉZ°ò€*“z!ÀšÄ·²º%`„üVÙ5@EU±‹”°”±N©„[¶°²9ÂÉ$B„qT=hnP(LŰ\×/p‚ɾ¢õITÓ`äèI?pØepCtNSh&èFOœ¢ëüµHÏÃ.•0"!4C'‹’©þž¢FæÆ·d„¢a$ÛÐcäóc<à±3;X¤‡9?ÖÄÚH]F’„jNu™AVå•„ðÛÊëŠÂU©¾tæY¿¶/Ö˜S¢Éˆñ=Wò¶ç~ÅÿùÊ¿ÑÙÑÁoú þ±í2ÞÜ» %V‘ìÀW5Í—É›)€²¥‹&Téq¤4οô=Ç=ã‡ù`ëz6Äš‰*z«ÿ23‚ð(⦎ZÑÀšË-¬ZNbJñf« *Ï5ªõ‰NDQ¢¶ ªª)(xÂD**%YDÅá¶«sÞò:>òŸâK_ú2ŸùÌ_ÒÖÖV­ŽPeš¥¥ªêwúSŒr¹ÌøÄ¨É©óù»zó½”jÜ-'‡;ò©t‘­ï|‹·\PÕU›ºHáT‚˜¦/l]II9—'76Ž•/P˜Ìp|Ç œ~i?É–&Ößt-éöVÌH˜xcáD"($˜¾½žçáXVUÈχÐÛZ(e²d‡q,‹ã;^dïCÓûŠËYsóMx¶Åóÿq;ko¹™ÆåËpŠ%ÊÕóŒ_"Ø_ù…+_YP€o9å ˜ñ(B(h¦Aal¢Úf:âY¥L3§œÍJ§0Ò¤t]¬ [&¥ôU!Š“YßÕ,[Œ?C¼¹«TÅðuqæB š12㸶ªëX…"Ñt ш_ÞãºTŠ0¥ôÝY¡Ôð¢¤\z¢=Ö€kå9œå@v€ÿ?A³f°ØL²$œ¦ËLÒ¤GIª&aECW|É[zœmby¤ŽŸïei8MŸ•#­†Øká›K®ã}Oü†'ËðÏ]ÛxË¢ Ñ€ùîz/C±\-Èv¤GÁµ±<¿ µ+-Ž•}™™§²gø@ËùlKuûdN­¦MÖV«ÈöeVà=%WJþ}÷Kœ±²lnnbU}š¶D„¨©£*å85`¥ÉiÅÊh>xiºâ󬄞ƒ¡ ’1…Ы頏²HßD†EÝq¾óW—ñöÏ?ÊW¾òï|æ3I*•b×®]ìÛ·­[·VµÒ Ãø/,Ïó8räccclذaZ“‹Êâø‰Ó¨áeÁ½ð´¢f-âÔ,K¤“Áz„æ.“+>øaš—õÎb¢;e‹R.?g)Líp,‹ìð(ÅÉ ®í0xèŸxU×Y{õ4öt¡ê¡xœDS#ZÈœwÿU%Þ¨¶7Ñìð(…ñ Êù}’/îaå ׳üªW¡ý‡`—ŠDÀó(çr”3™)÷²ø1,`aÀ²S•_wË6…± " ©ÀrŠ1Ñ7ĤüES™<}†D[+…±qôhÄg· U(àK•YPU(Ut »TµmJ®K~lœ–•K°KAc1ÿ*z˜r~ÇòË.•‚Ì£…føŠ¨ž;¥m—Ê8eŸÃuÚo«fÕL Sxv‘¾R†“¥ LGs"@LQ‰+:U'”ã}VŽc¥Iî=ÈWs¢4IO(ÅE‰Nþ®s+û £¼³m}V –VŽoþzHÊžKÁµ« Õoåx4s’ïÁ’.Ù±…MñV< ©™DÕ {ò`åágçLUA­©œ”³-¬ iÔƒ:ÏõsûáÃèš`sK×öt±¥­‰¶d„ˆ®úR35VU-XM{^)hV%ºæ¡k.„¢H-M¸¬!d‘žžÿú‰‹xÃß>ÂÏ~ösÖ­;|ÿû¼øÒ~î¹ç.»l+›7oæüó×Ï &È(•Jœùo?ågwü'e¥­íFŽg†8vûÜùË{X²¸“-›7³ö¼óXºd)mííÔÕÕF1 mFÓN×u±,‹b±Èääƒ?v”={÷°sçNöì=ÄàXGmFM]¡ÿ_á¿ø%Û¶]Îkn¾…‹/¹EQhjjâïþîoyÇ;ßËØØóhõ›g Ð¹T RÍ.­ñJ¸ÃÓÚæ•y鎶)ÅÇÁsô fU‡¨À·ë8'38–M¢±ES)f²LôàÚc'O³çGÑC&o½‘Tk30cQÍMáA)“erp8¶Çæµ´Š“Y2ƒCXÅýûòÒýilbëG>D]Oo%JÉá‡!74ĺ×ÞŠ cãXùúnÖ\ûJòcãØÅ"_ÿí£œ*gx]ãJJžC¿£^ Ó¤G0µêò\_gªè9<™=͇_¢ì¹¼¥i ÛRÝ$T×·ßü;Ÿ¢’VMÿU`a•<‡¬ëwÊMªæTL*)1ÓÕ›—c%«Š¡ŠhöÚ僂åXRåÚó;yÅŠV~{à4ÿ¸}'õà³¼í‚el]Ö‚ê (‚¢ Ý—‰D4ÌJζhkŠ65Je5¤`ª « qU¢ë’d]˜‚æašÑ„Æm¯ìå±ý‡ž5ÕŠ]ÑQc=h¡Ö`r•ðœ<ãv†1;ƒ×ŸCž*ú.• t „ŠŽP[ú”P£i1„¦Úy¹B3©”]™ -W@ã%ôü9_ø__`Æ H&“!xÿû?À<Àc»Ãì| ,ϘkH¯ÆªªÍ4 ÜÌ~ÂZ—½ç}4õöLw%D‚m˜½NÅc¾E„_“Á.•9øÄ3ôí=Àª+¶Ò¶jyuN'›‰Ö×ù뫱¢ôYsƒ÷3Žm“nkÁu]2ƒC&2Lö²û¾ß‘gÝk_ËâË.E5 \Û&;0Èû`äÐ!οíu´¬YMnx„Âè(Ãqäw‚Új™#µ»s6ÀêÃÏ&0yz€äâNÌxUÓHµ73vò MË{BI%H4×3qâ$‰¶VìB§\F‡1¢Q_Å?ÐWát ÇrÈŒ“joöƒçù"®í­K2:0Žž˜» ¥t,4] ,”é´‡üøª®¡ PsãŒêC4á–³8Ù!œü(fX¡¥§ƒ¦¥H¶6£‡BØå2¹‘QÆŽŸ¢ßa £Z¬=VXX~½“[ÊQ>Jª)•û8=m¢œË‘fù¶K‰Ö×q÷÷ðÂñA^]¿Œ-ñ6 ®Í GE¡àZ<‡¢á"ùÏуü|tÆÛxSãjÚŒ8YeÐK ¬h, §‰©Ú”{”¥ƒ‡LN¥»–Ò V\Ç…8VþgžÿsÇó¬jNñ†åKæ´ºéQ²|y˜H\ãæ‹º¹hi_~b7_z‡'&yÓ…Kˆ…4¤*É»å¼KR1¶ˆÙ~Ãôc€ Z"!¢3,@”ˆÇâÄc:Øðl.Y×Lsì%K#¨±®àb Å©ÞEÔB ¡šskYÍ}ç þUÝù¿+%¨&zó6{úÜwßo¸í¶7ÐØØÈ'>ñ v¼ãXù(±Å ¨FÌ^Y zÖ, ò/²é­¯fцusƬªð™ Ê–{™¡r£cXÅ"{x„ÉþA6Ü|]5¦( ɶ– q5k3¦ àžë8$špʃ”syú÷b÷}¿C'¹ð}ï¡eõ*\ÛfòL}/¼Èñ§Ÿ&”ð?K¶·3Ù×O9“apÿ~Þÿ;)‘jòÄïõÁÙk ?ðÞ‹Ç3 ì;L÷&_"µ±·ÛGÝ ÕAÓh^ÑËÁG~OqlŒH]v¾Hat »TƵmœr™r6ç»…©é”-NîÜC)_Às]òcHÏ%ÙÞÌÈɽHÏ™-”àÙEÌtÄg,ל#Ç¶É ŠÇüà  õ~×ê‘£'8øè“}ò9Æûöâz BÑ‘žƒª8ônZË%ï~ ÍË{qm‡Ìà°ŸµÔ4õUa?‡‡wÓ5ºŸ‹ãí\šè¤ËLRün>#N‘ïáéìiÞÚ¸†këz1„ß"«Hˆ«:KÂiRzpB+¼*¦ªbI7øÓ¬«²tÙ=>ÂÊdš¨¡ÍZSÀå¹’£#”4‹7(Kæ$ˆæË6%éú^#‰…tš;BüÏ[7rÁ³ |öáß3T(òÞW¬@ÕÅ2YËÆ”¦B2aP—6ØNæÌ¨O…hoŠ‹pò †ñ¤ÆØX–HHcq[ˆþÓ`!ZÜ·Šf)q.Cšóí—A=.J¸…’¶˜ÿøÇÜtÓÍU½³«®ºš«®¸˜_þîIÌHs^Ï\£“E–ÇüÇiÛã³Ùݱí,ݸ‚5W_±0'ÐõÈ¡‡BU‹ |·mrpÂø$V±È®{¤˜ÍqÁ­7o¨âÄ‚hCj Ó>ÓZ+eó¸¶íƒ™”˜Ñf$B1“arp»XâØs;Ù÷ð“4,_ÎÒ+·‘ìhgôèQöìåÌΑ®Ëâ˶ҵy3Òu?y ·\¦÷n=ô(õD¢á@¼ð3„Çj·ãl€U¯¿3Éà¾Ã¤»ÚH¶4JÄhM.­.ìÚ6F4Œ¦kœ|îyôp'—GØ6ºôåwÚF\ÕÐU%È\ˆT=V¶Löé]Ø¥"ý£ã”ry?Ø'm¿ ÒЧO*éâYy"©öYÅÖå\žüØ8‹¡¨*¥\Ž/¼D²¥‰¶µ+i]¹ŒTG«ÏÀîFzÈ$’JN%ªz^HIËÊe´¬Xʯ»‰ÁƒG:x„Âøf,Jëêt¬[ƒ ã”-²Ã#”2Y\ÛáÈSÏqâ¹ç)±öóžË©ü0ÿ‘=ÃÝÇX¤Çh3b¸HÇ0•Oµ_È–¸¯¿ä\±œk’®P’%¡‘ k{{]RT<¡aKSU¦·rEÞ÷ƒ|á¼K¸ªµ{º¥5‹]Óí‘(/Žp2—¡-AS”.–ïæ JÒ¥\vˆ%uP<´8¼î•=t4Fù³ÿ|œOÝþ •LYby*<š¤%­±qY=Û6µ³¨=†åzôÏ•YÚ™¤>%Á³ÈMØä&üdDgSŽzäJÈ'j"æÀ›³ÔI= Zú|žzænvïÞͦM›¿Dè}ï{?>üJù(ñÞù­,éù$Pk’ùXñnöñx‰M¯¿#^PòÅul¤'§²ƒÂ¿Lô PÌæ°‹%^¼÷A¬| n¾ŽH:Y]_¬¡žh*?Ã5•`—ËŒŸé'ÙÜx¹¾ZCndŒÜèŽeqð‰g8üÔvº/ºŽ` ì½ç^†öíCJè¹ø"º¶lòk|ÇÆ°‹E’3;_àð#!¢­˜éN”âiª`íÓ¦ÉïžKÍÂÞÊUUqs.§wî!öª­¨ºŠS¶ÈL0~ºŸÉ¾A¬‰,¦ã‘2T<•ú†V’¡QÝÀ€J ˆº¢Võ£"ºAX×É[eŽOŒ±{°ýÇÎàÙeœÂ†®É<Ç·ìó>jîRJŠ“J¹<‰ÿŸzqÅL–u7]CûÚU‘Pµ7¢‹­OûÖ˜®OcíúsÊ?¡Ñú:z/©§÷’Í~ídP iŠLŒSÊæüôp&ËÞûæÌ®½'³M«ü¾„Pº ™hÃ*gØS̰»œEæF¸µnoh\E£aªv>ÚÍ+#uèV—¨uõj€+¢é³­'!i…¨ÓB<6rš«ÚºÎÎb7à†~»ÿ8Ï ±ÌK’ é4FÃDt4ˆêªaEE7•ªà*`x\tq#ߎ\Î;ÿãavåjcÐaF(¸Ò¥ìäË ±çñ#Ü÷üó¼þ]ÜpY¦©P,»íË ×aê¾B€i¨D4TÔð5É…b6RÛ6}~woÏçgÉìI5ÚÉÈ€Áoîýu°.¿ür^µí"~ñ»§1£Ýs[Yž´F‘N޹‡ßçPæö°ö¶+iZ¼è¬úTºi¢·48%hÊù®móÒo¢œË³áæëˆ$§z FRI E!ÞTÒ'yƒÀˆ„PUúÎö@‹ìR‘É¡ªnÝ¡'ŸåðSÛé½bõ=‹8õìs îßÓ¶nõKz‰54PÎæ‚:ôíÞÍáGC‰µc¤;ÒA¸5±¿£ÔP F væÈçó•®µIàuà&Í$™‘4SeüÌ ÇŸyáÝІ'èTB¬®kdMc ½u 4ÇâÄ M¨xRb¹EǦ`Û,‹‚m¡®ªX®‹'=šb V44sA['ç5µ¡+ g†OQ,çÑ ß5àäÇм,ç­"œˆûr?ÉØÉÓ,¹p¥\Žý-+–жzáTÂßq]'ÙÖBª­#™-‰€ O‹°± EJ™,Åñ ò#cd‡GÉP÷Á*?6Ω^b÷Ý¿ Ý›Îgèð)´d{ä—Áj#‚ñëÍš¬"oÛD«›V Zg(ÁÊH=ºP±¤CεPké BýG]S9‘Ëqßè1néXL¤Ö-œ«Pƒ¤â'G±{b„º‰¢)LØ%P!ÒQ n*˜!-¤Ti R“d, Å€ŽE1Ö¦êyxÏQ&d5Ö‹PC5ì׈FÚPã‹Éæ&Ù±k–å±vIº&([ž-‰‡u,ËïA©k î8ÍŽaôÆ A©ÉöÎ . –ùˆ7cÔù]´¯Œf×ä÷¹ ¸¯‚GpnÖQüÀ{€*<<Çãø;H&‹bIÚÛ‘ …Ñ•@OJ\Ïc¡úp¿ôÄ#g•‰è~Wâ¢ãp&3AkàJr££”ryÒí­(óÔN:e‹ñ3ýX…"Á±í/0xø(¼æbõuÕ¢h="ÕÚâ+×¶ÜRý5J¹å|žìðhÕªª`ôÄö=ô8-kVSš˜äÌó/йñšW¯B3M<ÇÁ)•(çrÆÆÉŒRÊdpËe&NÂ.¹„ÒuÕßP¼r­–vÃTÀÎ °ð_šS¤+fQ²æh S (3ÅîÎaÀõ¦˜èÄs9ô¤Š©újŸkšZù³M[ùúö'9>|„P}7²œ¡®ë|U­Ö$ây~ÑfßÍËz>v’ÁCGX~ÅVÂÉxUý0œHøT ˜)×Å.–(e³”³yìR §la|WndŒÜÈùÑ1Š“2à“ëÒmœ÷êkH¶6SÊæØó›9òÔôTW,³öܳK¨å<›ÒËЄÀARvíÀ帞GƒšROÕ4"Bó©óÕœ@óêë¸ ÞÌœ8À¶Övš£‘Y %+✠ KÞµn9÷ôeGfÛãÑ3}\¿¤‹kWt’ñ,š’aZë"„5t(ãâºцÐù’ƒÃm7öðø¡~¾¹oJ¨ÙÓ«ÌJ·ˆêexÇ;ÿŒþþA~ùãopñyÍ4ՇР#¤ú›§J¶ÃéábP`\{XÿÄ¥0/c]ŠÙDQ´óÃü€«®ºº|¯««cÍšÕì8¼ßƒªLþ~TàåOj0YrÑæ*˜¸ŽcÙ¤ZšÑæ+Ïu™ìÄÊçªÊà¡#ýýÖ^{%©¶–ª[©:©Ö_Z< )¡T½± ÿÊÊÈŒQ˜œ¬r$ýÊ•{÷(¡tV>•˳âÚk0cQÆOœ ;0Dnx˜âø8N¹ˆjêDê“DSD"iÂÍ&O0qb/j¼ #Ýâ–¨‰_Mûfîß¹V鶘ºÎºt#)ÃôeX)ÇóPg úგ¼ïz’²ëUŒê¦–]‡‰b‘¦hŒá¼ïãw$R¼íüMüŸgŸd|ð¡°ÀýŒF°.×OÝ– EM {öyÒí¤;ÚÐ 5èìX>#^cY>±.ç˽f‡F™ì`âÌÙ¡aÊ9ß5¢"éñ¦šW,%’J`D|ë®”Í1~ºƒ?ÁÈñÓ”KFzz¼iîÉ)Àµò¤…`i¸®jYÉ@P‚´¡ÙˆV—GH¿û”BkãQ..¶ë é¼­{~éw<0pŠ×-îõ¥eK|ÙF×"šŠ’®Ö0½q#ï{↤N¿£ó]‡y¦owmZÁúEõŒ¸%ºc4Ö…È”,\$ª.*”=;ëÒÞᣯYÃo=À©ÜQ´ÄJü sàvK_&úu¯{=¿úÏ_°÷è8Í mÂÑ4…XÊÞÇ9x¦ŒZ×Î Ä:+°ÌõôO2 ½áBî{ðî½÷×ÜrË­þ)‚H$n9h? ž}në”6^þ(=[×on¬^cšan_¸)F~lœb&‹Pò£ãì}ð1z6­§ei¢liÆŒEƒ›µGvd„h*…2ñ<»P TEÈ €DS Ìh!àÔ®=dFƈ·´2qê4 K–pì‰'(Œ¡*ÑÆ©ÅmôôžOº§dg átÕ4ŠŸl+gòþí“<ÿí»°'ULmÚñ9 œ˜¹ç*ô|pª…#=ò®CRž«”G1«Ãr ¶‡©j„4L¹„¡j~&*X:o[¸RÒ‰k‘,­oâªÅKùÉîç©[²ª¦¶¿~»\føØ ¢u)\Ëfrp˜¯ºEU§eþʹ<#G#A)›gôØ öfütŸ_æ‘li¦ãü5DRIMómÊùÅL†±ã'9=1Ia|’âd–r¡Œë)(F =ÖE¤>…Pgd5g ×ÊÓ¢…«]z„€ªW•LG’$ sa·¯òh¨ )àøÅZ +oÛ”=M¼ª£ƒóŽ5òÃãû9¿±ž•ui?¦H,ÇõÆq1芪äÚuí|.wÿ}çvFEb‹yi¼ŸÏ?¼“]ºŠm«Û80:INØ´7E ä RñÝZ§ìá5¤xíù]|é¹ë È™¡„p´z¾õÍoÒÞÞAº¾‘¾!?Ϊ© J`MÚeoݵŸ»sÞÎ3µsnþ—ºPb‹ÈO®àoþúohjj梋.âĉ<ûìs …üZÀsnê*Ö†–cñ–>1Ú=‡í¹QÿعŽÃÞ‡'ÞÔ@ϦõS†…ăÚÀÊPT…xCàËÃÆ'(åòxŽ‹ªi~vQõfʹÇ·¿€ 3~âžc“Ÿ¤uà ÚÖ_OÝ’N" )´Yu+¥ô(ŒNRŸ$ÖÚè÷/LÅY{Û5G'Ùý“©$5X±—Ú¾ÁX°jJtvá›h)ÈØ­¡gÚ<ŠŽaMÇõ<2eÇa¤£!E ê+®¢®ªÓæÛ%]‹yòä1„¡O“»‘øw—ñÓ}tž·šìèši«÷}äŠuåŸ3Aab’S;wsúÅ=xŽC]w'Ë·]Jª½éIÆO÷1|äGŸ~ŽÂØ$VÑÂu%¾À¸ÐL=„b´ùA{Ý âÞ“L‚甩ÓBŠŠ øTn^DQIhçVâaÐä P,â }ž¨Ýа\¿Î1ÓøÈÒóyÏÎûùÕéc¤£íFÔ·ì”@ÿ?(–®Xi†)xÛ¥½HUòå^äD©Œé`´4ÈWžÜK]*Äú%uœšÌáè½íqªJ¡lÑ4´@ÑDá–Ë{øÎs-¢„[ü‹X(h [xø¹gØùš×“ËM²üíA=h°!OíìçÎÇÐ_jhøR+^{~ƒxzÓVöžú¯ýظq='Oœ`÷¡¿ÝûË •¸…34¶×ÓØÓçãw=ÈŽãZ6BU8õü²Ã£l¹íf¿®¶Bß1 buéêµã¹¾$L9Ÿ§”ñ%Å¥ëQ‘4BASU*­éKÙ<Ƕ¿ÀDÿ Zؤim/Ë®ÛJûæ5Dê’UÁ)Û”39TCÇÃwS³g†°rEâm>XI×C*ESéºt‡îzÔ_MÅ£Ÿ‡Ùaðsµ°Žá×ôœ>`Uš-ü!C ô´*Cž'§WUQª5vÕ夤>áÒÎ~uü8m+–N©™ÚÃGOø¥­Íôí=€QU‹ð¥/¾à^ßKûÙ÷À#ô^º…Ö•Ë'ãäÇ&9úôsœ|~¹±,¨aÔPÕlG†145( ©Q’Õç<9¥”èBeö!”DT CQ‚0|-¡–{…oUÅÂþ£"AS(9.·ï÷\ßÑÍÖ–6B±(º©¡ àÙ¼²§ÎôòýÃXY—â2³º°‰©ª¨†ÀÅCו©¢dRI7mí¡!âk;öðÌøq<ãÿ#î¿ã亯ûnü}ëÌ>³½,vÑvQ  @¬")J¤(YÕ,É–#ÇN⼜þ$~žüò8‰ã’'‰›dÉV£*EФÄÞP‰ÞíØÞw§×Û~Ü™Á.°)‰’ÏëµØÅî[¾åÜS>çsÂ,æ2|ïÄ ]-Ûp{$¦âD´7úQýN+zE›M›Â¬«Ðx'3‰è®¢¤¢5‚ÜüiùŒìK¤29,Û.®°t‹o¿8À’ÕŠËß¹LY½Jêú X~’RKv‡×.D±ô8¢t,D[ǶM'€nÛÈ•»˜Mðü›cŠ©ªA¼Ç+­;;Eý†M¸}Þwo^ ”ØP²ñ‚(’\XbèØ)ºîÜ7^‡°LÓ±Â0 ÛÈVP( ¢x-KnšdSib“ÓÌô1u[„µïeÍý»¨íéDõi×hÒ °ËýDI¢Ê&‰t4¡z5lÛ&15‡à úpù=År¾ œÍ§`eÀÞ»ÂZÎ[ eêÄ y".wy¨s†ZÄXý¼¢É !MC)™ÅßÝÌBÛÞÐÄk#,ŒMа®S×ÉäòÌ 9xM+óm•€nNºvøðqz_?@íúNÖÝwþª ,ËbâÜ%.¼ð*ñùJ w]¢â¾†¡Y®”J¿øEßâ¢LJ/`Ú–“\&.É)7JYy$Q@®el¢íLœ[eY\Ë2I:/ÍŽÐ]qJ²KÙ©ª ¸y+ÇÞžá+½—‰xÝlQ*j*’X4³–3(®¹¸ÿöZjkÜ×w.+v’Z–e‘\X,¯ßrvËÆ²L‡¨ —#OŸ™eqt‚øìá5töavn"ÐPƒ¨HÅÏ-kÔZ̤[–IltšìRœô|”ôÜ£‡NÓ²w‘5øk+x ŸH#”¬:G&ÞÕõ½*, xø8Aó¬e.+/r` ‚ôn<@+E%|.>Õµ¢·žøT×5®òå7bÛÔxýl­©çHßU­Íˆ)™x±–©bÇV°í¢iê4Ë ¨ø&Î_¦÷õƒ´îÜFëÎmx+ÂX¦ÉÕc§8ûÌ‹XROÃfެ®ìW‘7óÙ8YË ©× 帒[¯a§ùÄŸ^=_Qø§m[œØU)Èîø™X¶EÞ6ßÛ–sŒ*ƒæ‚|–Î:ÿ©g'¿wòM¾ÕׇK]Ïz9DPSË,¡Ö2Z™‚e‘ÈP\"=ëCTÖ»Ø~¹‚ïŸâí©)ž99Je¥›új K´]H PQáÂí“0±YŠåñÔÈ´ÔøÀ\³‚QzjÇåÐý’ÂÄBŽTF'_P1LIp+öü ‰ Há툾Îk:动ôëš‚²mìåÿÇ.»r+~W–ë”"Fò*[þÍ<Ì?|ç$o¥æ^¯çà²W979÷*Õ¨nÖÕ¼gëʉ­&Dù¡QÇ'¹í£r2}8/êB&K6‘$‹“M$ɧ3èÙzÑÂ2 …k?ë\!?‘Ž&Ú>´›šMkñ7T£¸Õ"¨Ø&Ì´0 ùdšÄä‹ý£,ö’˜œ£rê"wЇ¿¡—ßC!•uú!CB‹}#Ø®éó,#í[.練–űNâô Û8n¡­9qQð½>íRgE”ð©*>—û+ʲfš×[ê˽/I¹»m-§¾ÅìÀ0Mݘ&P]Uæ´Ö~f¯¢gsH?©ù%z_?à”ælèÂð!ÓWú9÷“—±]5¸#ÅÚ¯_‘’Z.¢¢7ufž är6Ôrf¸¾ ðæÂ(ÿ{ì4ÿ½ëN§˜Y¯Ñ¿XŽuCÇ%J\J,`Û¦(e 5˜9ÀâƒëéOláÏûNãs)|R^ÃZ9HÄå³Ùtr€IDATl¹¶hãÒ$*%:, tµú©®WY×äõu"Ít>¼—`s-ÞÊ0® Ys!Êr1XQ ¤ç£Œ¿}a嘿C±­×õòó´Ä©+¼ nÐ-ËÉ&½É:YÝ äÖ¨òúpݤ“‰¦(T{}xUW9¿(©"ÃGQ°l›æ`˜ûÛ;yöR²K%1¿ÀšÛw”)1|Eß=9·€ 0zò ¢$Q»~-ŠGCRUòÉ—^zZ¤™rG_µØ Š2lò–±¢ˆÄUà YSçža{°†GëÚ‹tĵx–ífmƒ6O£‹3LäR4i2`a ‚"b©2z>ƒK“øâöNærY¾uå ¶hó± í¬Tø]X‚‰¡hªˆÏ/“1 ÁBq)Ô=TT¹éXçcßöþáå~þö¹>Î-ñ›¯E©1$G k^‰pDEq äÕ³e¶m `!(!òJ3O½Åž]­|à¾vþáé~$oë*¥7¿àul[ã©óß”´òzÉÄâäŠ0†ù¡²‰í;·qé•·˜¼Ü‹- Tmhcã½éhÆ[Av«Ø–;èC”%gÏ v‰ú¼DÙ”‹&q‡|è™é……T†Åþ1®¾q =“§iwÛ¾ðá¶T¯æ¸³%ëÖqÊ–î5É%Òô>û&Ñ14W™Ç+‰£°nˆ_Áϧ°bÀ1àvÈiS',ºÞÓôøµ¾~×ò†+EEj}~|ªËÉ`Y]§wa–W†úØÙÐÌ=mkËŠìÞö.&“ >NES¾ÊHy°Ý~¾H˜ù¡ÂMõÌR·~-²ª:Ô¯¢ÈÔÅ^–&æp×vݯAYáܼe騸J ³d]QÌ®®PZÇc3œNÎò'ë÷t©å"ä2ø Ã6É :›Ã•¸:Îáùi>ð b×uÜ.…DF§‘ª‹²s–móÔÅ^âz'7¯¡7¢à¸†9Ë"ìU)˜&iÓ@Pk«ŠHÄ£²ûî ¶lÛÉý/5òÇÏŸá¿ÌŸåŸ|#Õnü^•|Τ¡ÞƒlIL.¦Wva.‰e- 9¼•+sIþãßž¦»=HÀ£2³”åâh†˜^…\±Aö½ÇùzæÔ¶Õ‹1“‘±({÷´ñÈCëùþO^@×cj7méµ23ó.bai´@øŽöOë4$MÌÍ{$\=y–l<É™ç^D«±ñÉi¾c þú*‡…¡è2ç¢ â“3¸EÈÐ*ÊdÅ]éz6G!“%Mbä Œ8ÅÔ©+´ß³“®Ý…¿®ª'¶¯uªºÉ}[†Af!ÆÌù~úŸûz}0Ä*€Ñ’¼'…µÌ-<|PtÛ"ZÈVn$þ·l»œE”E¿Ë_u!‹â-={M–‘‘D>O2Ÿãjl‘c£L§’ìjla{}ÓŠ¦(<Ù½ E92;Éð‰ÓÔv´ã ‘U…šµí ?ÅâÈ8¦^À_ãpqK²ŒQЙ8 A !ªž÷¸ Þ'±môô"Í./ŠæÀ'˜. "š$_³œŠL¢?ž ÒåfS°bE¶ÐÂ*Æÿ,ÌbüÊÂf¡ã'ãWÙ^SEGØ‹Û%cbáR*>SZk<|îö55•¯õ_b,™â ·uÒY$âwñ¸pk’ AYÁ…ˆ%@¾` È"yÓ¤`š„ë]<ùù6¶wUò/¾r”ÿúÍóü‘º•Ê€«n¯D6ari" ŠŸ•MF¹¦°‡2F®¹—ÙÔU¦/Ï‚m€Rƒäk@qU•1\·à÷{Â?YCc``žLV§gSmMn®ÌÍ »*V¹¤àdõ‚äq ïz ¬.Ÿ×i‡w#„±,ùdÊiHj8%c£§Ï37tS ?z?mûoC‹Š(ö•Ýž]UÖ¼+‡VIDIÂ2L²±$VA§ÿ§ILÎqÇ¿úõÛ78·ÞάʚY 5³Èå§_'»”#X‘:†S ¸ªü¼&Oá PÛ¢…<¦Ç^0Ï:9Ã@*<^*4/®"ŸvÙ{Y9­eI ̤¦Gé[˜#šË°±ª–nØLS0ŒÀJ¢>Û¶ñ»Ü|zóÖMVóúp?}WÇ‘>|•¸}°lFOC,¦XK•ì%~Ù[û®öþŠ€™‹!¥x n3IÁœ²ç¬iq¹ñ•XŠŠi$çdb†u¾n¹hÈ›sÙ 5~ªà åm– 9làÕ™qîš®#ôP]Wí5E4du‡JF©©pó-uÔ5¾rþ2ÿñÕ“|bkw¯«%Ž› ÑE¥Ç…WQ𨠆m‘Í™dLƒ@…‚G+*XÙ¢k¿¯÷ñ¥?;Äÿæ9:líŠ É"ýQÎOÅÔæU6x1CW²tE7R`R`Ýu+åf™·_ñü‰*ÈaúH&óÔTûؼ©†Ë?…àúæØ6RX‹ï ZÓèb%båÝ ]×ø†G°l‡—Ý(°,Ó):¶­b?Ë0ŠŒù…¢‰sùõÌŒ²ñã²ñ£÷á««ËvðT+nëÚîʰ‹l4N>‘Fñhøj"e €¥›Ä'fÉFØ–ÅÀKGHÍ.r×ÿõÛD:š<•Í­÷¶i“YŒ’š]¢ÊÒ÷Â[ÌÄð6lAHõ—Ž4€·n5 ï9­WdnH»€N֪ʥ]skQñ( 5>?/JÿËeGÉ臱¡P ‘ϱI3žˆqya–“S㜞ž`)›¡#RÉ£Ùß¶–ˆÇ[~Ùˆ«À&$A¤%aG}3¡ &˜‹1’Ó³X\2M>›#6=ÃÒä´ÓÑ6gêR?²¿Aþõ5à´Mì\wy"|²fƒX/η( 4i~*]ZYY¥­ç“‹<=3ÀíáZ6…*¨tk’€¥©˜¢Ž‰`H¦Á\>Ã`4ÁÑd”¬ìf:¾Hg0@…WÃãqá Ø:ˆ)]'š- ¹%Ü~ÛkkÈ&áéËÜ›YÄ%KȪHÒ(4tlÑFÕD¼~ Õ#’³Mt,T×µ2£Âíµ5<ýúãñÜÕŒdŠüÕ7/ó³¾²-«Q®’ËIK^­[ä¯kÆŠ7'bë)ÄüÞÛI}]€±ñ¯qïË;娯üav¬Ëò¯ÿÅœ<ÑOʬBTƒ·¾oÛÄJõѰ¾‰P]-é¥ £ã(.•lE•Ø,ZCl4+«ƒ‚ ÄÌ…j“/Ü×Ážá~|q˜?{ñ­µ>öo¬cKG5•n…pÈå0†JN=h¹pZpžsÍôÄv~ÿ{‡8zzž0.¾yp!¸Ô ™ÆæÖ ËWÓ¯~žnü•è®dv¡ÀØDŒžî:ÖuV£)9t3ƒ †(ùpfb€JÏ,ÿéß}„Ɔ ¢xð½éÞrõŒÓ«óÔ3?%11†©`ÇÇÇ •]¯ñs—¸øê›4ìêæ¶/ _]¥cíÜ/"’ª8Уb¼Jd—J¸­a…ë#H"F:KrvÁÁH “'/1yò{ÿðsT®k½f½ ùd†\4¿¾ªÈç์‚N!™!—H9ã)úŸ›ñÓc°î75Èóo.Ç_Àé#qSyÏ kYë NƒÕ:XÈç¨w{— Œ@F/P(úÌ6N–À%Éìmn§`˜¶…$Jx…€K#äÖð»\¸¥ ß´ƒ·“k±(Q€z·¯¿*)1‹„™/BEë²€IÞ6p à`¬ü.Õ ¼rø\ ­!ë4?§rI\U¼6Û‹yò4¿kh©© ¢"DSE€\²ºªJ˜‚Es…—ÑMÔʳ!裣³›¾¡$¯öNðÝ7®òÔ¡!Úêýlj ³®5H{£ŸúZUnä´ˆKqØ„¢{øàÃõÜöv þг&£…J¸±Íü ÊÉ^/ÿˆJª8o%7TPü¤s Có˜fMM!"A‰éB©È aëq¬Ä%x¼m[xûÐ0‹q±"pÍeºé#9Ô¯–i2pô$Jr†?ü½=|ûûg™îdížX¶Íð±Sô<ʆÞËæOÅã¾Ñý+ŸÒiAo6ÒÌ‚S¾cÛ6‚è(1Ù¥"»Õr¬*ŸJ“]J8e>¢ÀÌÙ~†_;Æm¿óQj·¬#5³ˆ'D”%lÛFRd¯Frf#›wÚê™fYyZ–ÅÒà8ý/`qdqÍCÞäh/¢ž¢ü–ƒ×¸ œ¡$?¯…¼á$ð¨$ŒKùbq2´b›ù¼i–ãE’(â’d.7®eÌ£‚  [&KÙ dWƸLÛâ­‘AæÒ)j¼>v6¶Ð]]G•×ç ø-–žä ùtš°[û 'æQÖD*Y©dÛZNNóÊ`/ãSK¸*Ú‘½Þw±MòñIôײ+Pï(«âµ± ).j\žp›¼m H"Š ’6uL,Ò–ŽfÉdóYšâ¸b²è ÒÝnB¾4lhãÂÀYl_®Ú ¼1ÛÇì‘ã|¡g·µ¤òI¼²[¶]6¢$àVd$‚š¤Q`*•¡»Êφ-뙟]Ã¥á(gÆyóð ?~{”`Ha[g÷ÜVÇšú^—L]•‡º:²*કøÈîV>ÿob¸*‘«÷^#Ý“4XÁ¶i;ÁõtµLÌfâ Vn±ˆ¡e`pÓ´¨ªðÒXçer0ŠäkÛÀJ SÈsïÝkQ‰“§Æ)˜~ÙC‰Zù¦q¸¢µ4>ÅÒøŸ}|=;w4ñãç.•_ÆCGOÐwøÛ¾øž¸×ÁH­ðlÓ$ŸH“&(¤³NàݶWiÅcQ(¶êZ/›8vñÃgÙú›¦íÞÛÁ²p|ØØDG¦È'6Ë0ŠÏW¤k²,ôLŽää'.±p¾ry¼Š aú-ìé·P‹k¶(38Æ7sáSXyàeàQ€œi’± 4'Í\"“QDEr8­ÔëÔ­ |ˮм|bÓV$AÄ«º˶É:VmkÙŽýaÙ+¿J×ð©*U¹áz%K̯º¸§m-ÝÕu<ß‘ã½ôfÔPÜT%þ¼"`éYÔBšý5QE³¨ÌÁA¶7ºý(e\•ƒ½²l M–Ê.fòill’f Á…mèè–AF×ÑdUð‹2‹Û›ª¹o±žŸÎ¢ÕoBkØÄåùaþŸwÎððl¬k¦!ìq¸ÙeYp»$4L¥×Ecµ—ÊJ7ÓÉ ó™µ µë«Ù_¨&0»šå¾zžïå•ÓÓ|xw#ìi"šÊ“1 Úšü¨.îuanKÁíNú¿X(ȧïÞòÆ –Q ¼ÿ|ï]~e(ˆ˜©!ÂÂE|´ƒªJéL¾“Hă x½*k+8vyÑ)ã)İңlÝ[KçÚJ2Yc''ÀU ‚‚­'±ó ®È*5†v~»eüB”ŽÖ »v¶Ï,Åó´V„9yŽþÃÇØþ¥°á#÷YŸ¤ÞˆI+¤2¤gÉ,ÅÉF“dcd—œàºžÍ9¥k‚€¤*¨~îWÀ‡ìRzÙEfÎöaé»þÙ“4ݱŹ_AÀ4 ÒSK$§çË…ÒùDšl,Af1Nf!Ff~‰ÜB )“¡­2Àã؆ۥMfH¤²,%s\£_c¢8Æu-½V“ŸKa-s ßÄ!‡¯·ÅBžF͇X ¤úU·V伺Vá^z¯˜¶ƒ˜5, Ó¶0‹ Æ«ëÙŠ’5tò¦S%¾”ÍÜ@XâãºUQ…,ŠL“ ç¦ø/€*¯ÏôÜFƒ?Ä®\ o[¸ÂM¼/JK£¡R”hÕbG«eÉnJ©qÀ¬ .Y¤E 0Š¡ÛI³€( øÁ‰ÍrøT…dý>VÏ­ïböLš“3WÐj×ã©]O69Ç÷†Ç981Ã}í ÜÝYGc• ‘œnMå™Ëg¨,¸i­óS_áu:8«—lÒ…rÖú¼ÔÕ¼¤Ü~¾ùæɬÎëd:šA·-Öµ ETüªÂÒõJHd¯ÃUž<Ãák—ÞÏxâ/n±ÙH¸\ ùpwïkGÒ«ƒ•e‘ÍÝõ|ûÇ'° q¬Ü4%Áþ}›©¯ põê"WbHž-ƒèâ›ø”%RÉTÞƒ —‚ðXy¬ø9d¡€¥Ãþ;Û©©ö1|u‘tA$µ¸DÿÁ£ô|æ6<~ctÒóQüuUe ɲ,â£ÓLŸéeöÒ ‰±ôlÅãF Ð"|5N÷gl=—'Kv¬°¢"s}´ìÝšûwã«­,ãÂ2 1R3 $¦æYèa¡ŒÌìdsh5• ‹zU†°;ìF7,^=ÞO*Ç4l$[@ElEY>I/áC·”_ÄÂÇ-< ÇÉ3‹õ††eaZVHQB•+ìzŽê¼aÜE1,‹Åyù•TjÞŸ¹^½–Åb&nšÔøüÅ@¾PŽóÅó9¦“ ZC\’ÌýkºP%‰ï\Í\>‹[•H[:>¬¼IHsc Ë" Ùxe™¶ C°ø[6óggÎrfú2Zí:Ô`Š7ÌlbŽoôNóÂà8Û*¸££– Í!"!$˜MeÉL´Öú(X&!·‡æ¦*l2,Îlj:š$!ˆ"j¨CVyþxëÛBÜ»§ž…XŽÉyËŠÖä*ÅÀ²ÇÉV–ð6¶™wpK¿”¼?`QÉ×ÌôL/ÿÍTF<´·EðxÔbó^'6»¥§ž·@<3†•ºÊöÕìÛÛF(¤ñïŸf)éB©«ÀÌNRåOò/ï.¾ûÃsœíG‰”WXñ T¨Q•˜’Éí;šðyU®ôÍ“Í >NÛý·³éãy€lDYÂWã´’DÔôý/bìài,ⲫ… ¹—ŠÎ¼U¯ÛQTË÷™M±PGÏå0ó:¢$¡ú=(š»©p¾R3‹,Ž3üæqfO^¦B‚­­Õ¬ßÛE}UÓ²¹:µÄÅáiƦ£è9‹°ªQï ²!TKM£Ÿˆ¦!‰?ºÂp"ZÊúOðÌÌÎÜrZ~Q…U^¤¼e±Ï(Âò†AÞ0V| ¤xIBSܲâ°;,³¨âùEAY“X®€DA wa–ãýTT.÷ÍðXW·{Xf}™¶EºPXQTÈç0m‹°æÁ²,fÓ)®ÌÏrnv¿êâ‹Ûvá*¦dïjí ‘Ïñt¦êCr¿WŠ›/~ËÈQ­j¨¢„%Øè–IÞ2pIÅOUœmñšeU‚:X–ŦpuBâÅiükˆ9\ªèFÝË$kðËNÁt¥×%Ûˆ2ü+i+ÿߙ󛾌»v=’Û+Ü„¨!‘‰òÊÔoŒ\¢Þ+ÑÝfçÚj¶tV ¨6cÑ­u>\nQ•D7þ@Ó#R0­bñ¯ì­ “ªäµc“ìÝVƒ¢ŠÌ/åXщlG Ý€Óœ ‚â4ý¥ÜÁ_XTò"†·óêds¯²¦­‚Ɔ÷ÜÝAÇšJA`íšJ6t…9|ô,õu*¿ûÛûéZ[E:]àµ7ÁÕàðÏ 2† uA>ôÈzÎýù°z@0ãÐò—¹}[+gú'xèá.ª*½¨ªÌÉÓdc1oïfÛ+÷ t†O@r)äi†^=Jÿ Q}ë¿×᧪ !à¸p–a gó‚ÃA%*ýK)Î,¹$·ZÞo–a’œžÇÈåñT…ÉÅ’Ì^âüS?ûå·îéfwO>‹±yÞ85À™ÞI̬ŚP%4m¤=\A…ÇÁaŠ˜¶E2Ÿg ¶ÈL&U6PB.wïõÍc ¹,Ï¼ß k™[øOVÀ|>K“ÇwUJqYâ’%4Y!šË’Ñuš‚¡Ê —P.ó@ ˆ×E-Ûæjt‰k#ü×OnæK_9Εù9ö¶´­X®’ ÞP«(3©$ß¿x†d>i[]nzjêÙ\Ó€GQW"X³Ž¡¥N/à©Û¸*nèçS'¨z‘ĢŸe¿¬àWgEf…¢Ò%Û‚J‹}‘F^šãκzün)S° \¹,¯†âõb[ÀÄ¥åÒD4ĺ6ó¿Ž_ààÌ´º ˆªARP5¨þ*¬B–©lŒÑá%^î»Dw½—/p=­~âù­5>, ÃÂã—Hˆ& ™<’;XÞ<²djqœtÚ ìVɦL~|p˜¤àGQ–ug¾~fd¯Ó õçRX¿Žà¼èiÂä.Þ8ÞËïL!˜½8|•÷‡ûéÞXK àæÃl Íðïþp?÷Ý»E9~rœ³—|[]•,E]¼}hˆO|l _ûæY¦â± Q‚âïë"‘Σz$öÜÞBÀïf~!ÍÅK3šk¹ý÷> ® ‹˜>Ý˹o>QÐÙôñ©ß±Q–(¤³D‡'0 :–a]º’Å.¡¸]¸‚>ÜAŸ+µ /N‰YÐÑ39¯Ff!N||†K?|…º|†ñ[ÒVal6ÆWž=ÊÙ+“4xB<Ô¸ž5µTh^DÁ6‹8!›¹T’h6ÃÅÅ9²¦^¤©óøÞüê;³Ï~⋼‰\¶À©wN²¾AæñýÝ4T‡xñÈößÝÎí;šhn ñÜO/óöé9îü·Ÿ§~û†kœì‚€™/pù‡¯rúëÏRÓ³–îO~oU˜ôÜ‹cdbÉNYY• ‘¼‘Ë“O¤ÉÇS˜ºQ&Û+•ë’yÈEdc Æœ#sî ÿêSûéhªdr>ÎÿÆÄg²|ª{ìÚÈÚH%Z1kãÎnYÁ°,æÓ)RºÎÉÅY²¦øe¶+\ùG5µ³ôÚOßuV~Q—°´zž> x Ûf&—¡Ú¥Ý€FÏÓdpi‘Û;ÃÖ·ÕÒÑPÉ›§]°wO+‘°ÃÊùâ+}Ôß¶}5f¾€¨(äâ)Nþí?zž®Gï¢në:ôlž…þQ†^}‡øÕII¤å®´ÞµAoÌFŒ|Ôì"™…²[ušD`œ‚gË´È'RŒ>ÇvtÐÑT…eÛ¼v¼ŸlTçöÜMÈ­¡›&¦xí"Š(RãócZYC']ÈcÛ6ÓÙ±B®L'ãUÔ7>¿aë•“sSü˜[[Wð *¬ená1îå½ C'g™x%yÕ ¬JÙ‚ÁÞuU´V{˜JÆY_U]^—æfèlððÿ~b3s‰™'ëæ³Onãïÿþ$>ÍÅñK£Ü}O;Í!ššB<<Âå¾y´©¯ÿËê÷í`çïþF6Ï‘?ÿ©™¶|öƒøª±L“™³}ô>û&uìÿÍG‰NÍs楣LÁ¶ÉÅ’Ø–…ìRQý´wЇêõ »«HÏäг¥ÎÌÎ[¡×ŽÖ <°k]™Ïj.š¢Þ Êã%£;TSî¢:±¡è"³©$ºi`Zvù…8:dŸ¼)ž5ô¯~â¥?ñyþý{Ð=¿Œ…åÌ3À^È™óù,^Õƒ#š‡¡™)rºÅC[ëøêËcduƒ€[Æ´,³iîî Ô*|.*ü2óéµ>Y•¶Å•É$‡Òz¶R_W‹(J\¼x‘©ÉÉ[‚tÓ,g—0SÐé[œ£#RIЭ­ø¼…M[¸‚ ÍËÛÖн¦Îi °ì ÕÕu®¨(ðÆ©ΞãLjŽ5]kÙ¸a‚ 0<ñÐúG¦ùó‹çˆéy¾ÐÝEÄåÂ'*ÈŠ@Ö0ÑA¶¯Q—šªîªgËÉ*üîݱ8Õ7Aö`ŽÛZˆg ÈŠÀ'>ÐÆ¶•ˆ²†(ÀÖÑ6ÿóÛ‰ŠŸ{d'!ŸÛ†l^g)‘az!Áøl”LÞASëº ¶…ª(½&¶BRØ&è‹|Á¤`˜åRÀë&ä×P¤b%Áér-–¾Š%]b1/çQD„Ò1¢Xü=ˆ¢Tþù¢€,IxÜ ¯Í¥Mdxæí‹Èª_h®³kS ²$Ï/ I‹ñ oœ “Õ¯1’$rèè/¿ÒχîÜ„WS‰%³(²D{CõUAb)¾öÜ1ž}þŸùäV.\šáÇoŸ§«³’î]KC]¯G¥­%ÌÿßÒ³©–á«‹ü‡?~‹ñ­ëyë$™ÅÝŸzwÐmYŒ9GÿóoÓ}Ïmì|â^´€—t,Eôýà%"> KAò†I¬`’3-,IÂv©È>îpÀáXxfC>™fq` ¦fùÝÇwS `Z6²$²±½–o_8ɹ™)ÖVT¡ÉJ±åž³:99Æåy'€nZN’ÉÀP¯…€l8œ(ä©¢Èú¿ù“÷¤p~a…µÌÊzøg@‹ Ìä2Ô»½7ûHœµ8Ü7Ï·7ñõ7¯2žˆ±É]Ëpt‘Ålš§IˆøT+=ÌÇŠHZÛ¦waŽ©dœd>Gûš5456"IóóóŒ ‡Á´‹¡#á†ÒIˆçr¤ \²ŒT$|y°——°ùìæÛh EÊx/Û¶ »5j½b©,>ÍE:WXu<ì¢kU*t_a Ð\âU³—`(B{[à´ßîïï§¡¡¥ÅE‡ff¹…UTZ5¼`pplA© {i«íBs+|õô..-ò››ºØÓRC•ÇM•߬ Ë” ¿Kª“ÙV[ÅÁÅù²BÐTYùüܹ½K²CÃ$—ÍávKð¾r…ïœäÑ{7 x˜šs~pб™` x¨©ðSôòkø4·ŠæRp»dY*Z´6¹‚A"cf1Éðä}£óÄRYB~õ­5´ÕGP™h"ÃR"C2“Ç0LÑAå{5~¯ ŸÇ…Ç¥àRd$I,vÚq™æRð¸UdIthL‡C–D¼n[A–$$IàÍ“ƒCnþò/>Ì•¾9þô/"I"Ýõå5!‰"½#³È²ˆGSðzÔ2QA>oð­ïžF°D¦â Ž/Hç°,›–º0Ÿ{d'•!MUüø¹‹|à¾Nþì¿>Â;ÇÇ\´µF¨©ñ#ÐÚ¦ºÊG,žet,F>opöÏ#©2?v?î€l›éÓWè{îm¶=rM×\Œãö{zpÝàkÙ±¡Y–Š”ë6†iáVeDA`1žfv1ÁÌb’…ÑQR™ÓD@ÀãR¸£.Ìþîe]Kz^@A7tšjBÈšÄ×N¿Ã½ítVT•ãÆš¬°»©•íõMèEú¡¼ipra†¾øRéun?°!ÃÏ!¿¬…&ëEàŸ@ÒÐY*ä¨u{VdÝtËB‘$*5Oç‰Û›øÀ–ZœÂ´LÞ$kÐT'àV%66xa"ŠiÛ\œ›æØôŸ€!{Y»v-–e‘ÉdèííÅëõÉÎÎaÙ6£€K’YÞÆUÔ"tA7M ب’Ä'»·aX•ž»”(’D{¸‚¡™ÂÃéÝ·\â©,ébu»,9®£«¸ „eLŽª¢ÒÙÙ‰¢(d2úûû±,‹ŽŽN,-±j»yщE7ûýt†B˜WM¢‰4Áºû·uP_àÕcýüÁ#ÜßÒÈCM¬« ò©¸\ªÛ)ëQdEeT¨òkäf ,ËBDdms—"|ùkùÌm…œË±±£[]¦pÇ€×ü„žr\Ê4-ŒbH@–D¼šŠßãF‘Eæ£)Ç–¸2:MÿÄ,³óIdS$àvóâàeŽMx¨òúküA|ª«ÜkTEr–À\.³<©rxn]Šó«PXð}à“@дm¦sªŠÁwÓ²XZ`$æ4x´l›C½N/ñåÖòʹC¼<ÔÇ'îhäÁEDQ(ú¼÷nªåÛ&XœçÜÌ$÷o®áÌÕ(ÑœÉåË—Éd2äóyr¹›6m"“Í–C†em Éʪ7-~—»˜©T‘ŠoýÕboU¹|•t6O}U€ñÙØŠ~qºa’ÉÊ“‘Lç‹ozItʉÞ>=DÁ´gpp|>O:¦»»¯×{-î{] «di ÜÛÜÈm}5¼øÎž¸»‡ª°õ­54V…8Ý?Ák—Çyytœ av4UÑU¤:¤áóʸ4 ·[DóJ„\n¦Ói\.¹¨,À¥È<ºo#¯ŸàŸþŸwXó[ÃhŠÌÄlš³ÃKäD›ÝÛÛh® 3·”`Ow+õUAB~t¶PVÜËãz7C3”Ý,Q èÓ¨ zèY[O®`päüUºZª¹wÇZºZª ú5Ô¢•€í¬#Ý´ÈåuR™<±TŽh"ã¼¾•òÅ]LLÆyêåS¬i¨Ä­*ŒÎ,aŠ&ÿöŸÜ͆õ5¸Tg ‰¢ÀØDŒ¥h–ö5466 i œ:uŠt®À^?KÐëvƬ"È˯öñø£ii#ŠŽ»ù£g.óýÉAê÷ï¢ö¾vÖT…YãÊ3o°á£÷lªEDôlžþª ±ã±ý,ŒÍ`tš6uÐwø,ç^8ȃ[Ûˆ¼7Tˆ†Åb4»"ïq-©-ÇJg~8Åä|œÉù8Só ƒJ·—µU<¸~MÁ0Š$Ò¿8Ïéé FbK E9f"‰"ª$á–4EFR]ÄÍÂò`Éqªe~.ù¥ÖuÁ÷ ¢— yâz —›ÑdœØ ¿uo;õa·.ÍñÚÅYþâg}|ãwwñ™;[ø«Wøè®fÎÅ0-›‚é kïÞPÃÃÛjxúèeÜ.™˜ÊLZ`íÚÂá0µµµ†Á‹/¾ˆªªd²Ùâà Ô[S7{T•*UvÂóÖM²O¶mÓ £Z2g&yü®n “™¥DyÁ¼n²9‚Qb¨Ó´0‚-puj‘ñù$k;:¨¬¨ ¦¦ŸÏÇó/¼à´3/I‰žey»ùe߃!™?¹ý~ÿÈÛüýóǹ{G›;êñy\ÜÑÝÆú–.Ìråê gÎ÷#™6!·JÄã"¨©ø5…Çv·ÀÍ›ãtn®G’Ä2­­K•¹sk;u•~†'—8Ø·„iZx4•ή:«‚¸]×€‹²$:$’× !·Ç겋›]Äïq•Çmj>Îcwu³GnU)—`-ûˆc-Ë.E"äÓh¬¹QAÚØX–( œë›â©—OQÐM~óƒ;©­´¦-˦³¹Šw®ãëß<ÉØDŒG\Çï|áv6®¯áÌù)LÃâC»×ó¡‡×Ó½±YË÷£ë&¯¾Þ×dÓ¦¸Ýnòù<£££¸\n*+TþÕ¿¸“‹'æPL‰5½<ÂÅË345†0 ›×ß jgûþå§™èÕIÞùŸß¦~ûzB­õåŒúÌ™^Ò“sÜñäCxB>ο|„ºÎfƦ9ðõŸ°sM ;64—ŸÑ*:¢¦VD4tƒ¥D†‰¹ÃSKLÍÅÉet‚ŠF[8‡Z6ÑŠPY‚–æ°`¬«¬fSu½@<—%šË2ŸN1—N1™Œ“,äIæ3Ë{‘NO—tÈÏ#NãÃïªn[LåÒDT7¦e¡*"ÛZÃܳ©–/ì_ëçgxêÐK©¿ÿNj‚nºêüH¢SÏ–/X˜–Ï-óß?µ…Í-!DQàéwÆØ³ï.žxü1DQD–exùå—WßËD·LlÛ©-¬®3žˆá’d¼ªŠOUQ%yÕó]›«ëyóÔ ÷l_KuÄGÁ0YŒ;ñ5U‘©ŠøYˆ¦Èë+þ°OÓØÔÂ?ûýßGÓ4$I"óÊ«¯.Û46YÓp «(+çËbc»Ÿïúà/O_äk.Ñ;2ËîM­¨ŠŒiš4U©¯Íˆ¥rÄRYR™7~Ï5¼Ý÷mtâPÅr®[I9»k¯¼ÚÊclZjÃ|ú¡T‡}T…}7tT'Ãw×¶5„?;r…‡Fر­‡è⮽íÔר«ubL¦yíy$IäÈ;#<ûB/]]ݸÝõÑÈȹ\Žöövr™iª}~66KLÎÄû=Ø&ô.p×¾>¯“©óh …§¦Ï(èœþÚ3ä)FžÁ2LÚïß…žË3zè ÞP—W#µgîê$m;6pè;/²®ÚÏ=·ub›† ’ì4U*%(tÃ"–Ê2½˜àêÔ"c3Q≚ Ðs}máU^𬠕’‚°Âh6,‹œatË„5•/—æg833A¦ S0 IDZ‰ø)N+úŸ[~é²øt:]’NwM·,*\n"néxŠC³(’€ß-³¹%ÄGw5Spã×dvuVâV%~tl¯*³¹5L¥ß…( øÜ2{ÖUãVDþá­aZ;»Y×ÕUlÞ ‹‹¼óÎ;TUU‘Ëç)$SôÔÔ¯ÀaÙ¶M¢'gèÈ’XÆ^Y¶MÞ4I $ótËÄ%ËÈ×u,ˆæáÍþAò¶AwG>J^7È%K"n—‚n˜å88®ÂÔB [ñrÇž=ˆÅûÊår:|˜P(„ÏçcäêUÖ{|´ü¸ Y‘Š­¼p¾—•–/(rW[=·ûëyýÊçæfi¬ Šbͬ*¯›š°æÚ0šKa|.ÊéñƲiv÷´ðºHe ¤2y29}Å}ßÑ´l“; زm ºA<•e>ž&™É‘Éé$Ò929G ZV ŠE‹(HUÈGȯ9x1A¸vÎb&¯”ñ[>àü½Ôú½2[kn•º ?^ÍUæGt®ãнX–ÃòMf©­p÷öjCNŸà¥7úñz*+¼„Cr1ƒédEÒ™ÿ鿼Êü¢Èºu]†A4åÂ… ´··ã˜œ¢5TA&S ‘Γ×MúÇçÙ¾­ëk4dYDUež}ê(Ñù8Ký£Œ8¿¡šÌØ$‰ÉY"]­$§æ}û$d3Äæc¨š›ñ‹ƒN¶vt’'îéÁ«¹(ѸKH’@® 31ãÜ #äÜ̇Ɔ98:Ì‘ñ«œ™ž qŽéd«ÿõÈJ1ìâ„ ¦“õU%™öpUÕÌëyREd;²àß#?¯uNwèoáP(‹yËd2›f½?Ä®ÆÎÌLñÇÏ^áÁE>r[#[ZÃ4Wyñª–í0T\Ì'ód &Ù‚‰Kq²/Ïñ;_=ÎÐlŠíö{#»/oªâ&ˆhžU9´Jÿ7,“¥LšL¡@…׋&+¸—•÷4C<±®‡ï¼í¸Ÿyh UAA ‘Î n—LmE€…xšt&ÿü»Ý¨ÀB!ÇO'G˜Î¦1l§©„WUX °£¶šµ‘ ³Y Ñk²o{%«ÝÍG_~‘¡©E6´Ö®pÁQʉdŽ‚nàÒbi*ÜúF瘚SöQñãq«ï­qgq< ºA,™e>–b1–&žÎ‘ͦ…Å5E#Ž5¥*NV.Ðèj©æ¶ ͬmrš‚ØÅsæ :ÉLžd&O4‘a!–f!–"šÌ’Ê:ÁuI xÝ4VY×ZC[}n—|ƒÅt½Ø¶i_ST™lá©EzG柒ÌäI$às³¶©ŠMuìØÐijo]àÛß=CC}5m<C7é\bj*Î…K³¼ñö>_ˆ£G’ÍfÉår¨ªJkk+ äò:gû&ɶ‡Åx†¼n09ç§/õRYá%p³kg3òßÏ_~å(}  ËÌñ™OnãÔÙI®<ûz*†Ë§1Õ;Btrž|&Kb>JCÀC¡àðŠ)ªP¦`:Û7ÁáóWI§ „Uöp÷tvÒŠPáñ2ŸIñæðgg'Iښ׋ÏïG U¢H"–i±”Ï3‘I‘^œ…Á+„T•uMl©mpjmÓv0sMp5'ªç—÷| '„ô Éû¢°–Ųž~‡bKûÙ\†FÍ‹_Q¹½¡™ñxŒ·ÏÏpl`‘‡¶ÔñÑ]MloöªH’H[7/Î’+˜Ä3B^ôø›×h­òâsKN@þ½l*œÌdÞ4þ…9&±ò†[~¤GU¸½¡·,“3uÞ¼:À\*‰WU‘GiÚ@Ïϧ6í`<¥`½nTEâ“d³…2Y¡iÙ¤²yj+ü4׆ßõ^-ËÂ4 ¾}µQQ@UPÝn$IÂ0 òü¶ÍC-üÁm=¬¯ ;®¢l²uCˆG.¶ñÜô(]ÍÕË…@,™åòÕFg¢X‚Œß }M¢$¡t&b)ú&Æq˰®¥šÎ–j\Š|KÅeY6}c³ôΑÊ(ªŸßO°ºŠ¯·Û…¢(eKÒ²,t]'—Ï“Ig˜Ç9õòY. Nó…ÝNmE€Š€‡óƒS<õòif“ätÃA”QÕåBU$IÁ²,òs ^œÆ6ϱ¾¥’Þ³™î5uïZm ¹‚ÁÑ WùÙ‘+ŒLÇ^Ÿ—Ëi-—ÏåH¥xýämuaîÙ±–==mœî›àíƒÃlÛ\O.o08¼Èý—WŽQ(èÔÕÖÑÐÐ@¤¢‚ºº:¹té²,FÉä Þ<;Z¾ÛIvó“Ÿñ“Ÿ‘N§ij ð“ï–ÇÝÈ]ûÚ¹Ò;ÇüBšÚÁ F6§sê»gp©*UÕµD"Ú×x‘‚^ ‘H°´´Äמ;NwG-{·´Sôrahš—÷²¯±Z¨õùñ( ¢àÔíŸã»Oc¸]´uuR]]ÇãAZ¥“{i>“É$'Oâ«4ÂL$bô.ÎÐà°½® ›ÞØ"ºi–ö\øÞÌÍäý´°À ¦} Ø ¹¢•Õ%+ˆ‚@k8B­ÏÏpt‘ïžäìh”õèzîÞPMUHcScï#žÕ‰¦u"Nœ žÖéª`LX?|áøägg§¨¬­FUT–£ægff¨qiìlhA•$ ¦É[£CØšÕpâ ¦i27?O[ Ä~â!>¸q=‚,`˜6ϸě§G…®)¦l.Kti‰ m5´ÔFnyË‚ 099‰eÛlز™H$‚¦iÈÅNC¶m“/˜››ãé?÷ ÿûÁ;¸§£ÑA±»l6×Uð½ó†é(`p|ž“W&U뻩©©AÓ´²")=W:f||œ³CÃLÍÇÙÓӆ߻’~ºÄ´!‰"W§8qe’–Öv65Ôã÷ûW(¨[‰(ŠŒ111A8àdérh2Ãמ;F4'Ѷvn··Û…ªªÈ²Œx]]¨HÕ‰Åb òÇßxßþð.îÙÞqÓ±E™…ÿÂqŽ_™¢¦¶ží;× Ëc]:w¡P`qq‘Á¡!¾öÜ1î»m-­u†˜™M’Jxþ§—˰eË6.\¸ÀG>ò¶mÛæàÚ$‰\.Ç¥K—ˆÅbŒ±iS7ÍÍÍ7¼ A žHpäðtÝÄ4mÉF¦–øü£;ŸQãöñÐÚõ\î2å“Í•ùY¾yþ$•MlX¿—˵â|«­W·ÛM<'•JÑèõÓŽ ) 6Ñ ÝnDA`"dr+aÂ[ðóÛKò¾)¬eVÖ€/Á’6h^üE$¬[–ÙP]K} ÈÑñþöµAªn¶©=-!,Ûfl!MmÐM<£SpÑÝâ`ï<†iz|z6†*žÏqqvŠªÚvïÚ…,ËeDîÜÜKóóÜÞØ‚OQñ»ÜÀªç¼§(TWWSQQÁå+WøÚsǨ‰øÙ´¦ö÷PÆgcüùSo1-pÛÎ]TUUÞôþUU¥¾¾žêêj†††yåD/5!Œ2âþõ·‡io_Ceeeï éÍ¢5aÛ6¦iÒßß×륵µU½±;“mÛLNL`‚àfr*η¿w†§~p–LJGøe¾`ÓÓ³…¦¦¦b¬Ò^U©¸\.ÚZ[©®®æì™³|ã§'èhªb&›äù¾KôÔÔSåõáUULËâ™+çñVFèÞ´©¼7ÞM …½½½ºŽn™¼31BF×IäräM¹t’ÑX”´$ Ëœps_ñ²~aù%9SV•1à8`ÈY&ÙÔuuÃ6·‡-µ œ‰se2ÎøB†–J/kj|œ^°,fãNlèÑíõŒ-¤97Cß›ÆD>Ç[W0Dεk0Š<]†aÐ×ßO³×OG¤ ·¢0‘ˆqn~†®uëp¹œîÐù|žTUERED—²Íå ~üæ9\¾ N•¾(ŠD£Q&&&ðzv‡€×MÐçÆZ¥“¯ #IkÖ¬)¿‰–ƒMár¦ÛífÛ–-ÄUÿðúqf3àÊbG%àsMf9Ý7Eg×zº»Ñ4mŘÜìÜ‘H„Ûwî$U8Û?IЧQ P[¤:â'ðped–XÆdíÚµåM¹üœ×]?£££,..¢¹Zê"h.…Éù8?;|™¶¶5øýþr2åúû½þ|¥{E‘õëÖ¡zCüøÍså$ȵs@2“çož>ÂLÂäŽ={¨©©.[„7;¿mÛH’DgçZÖv®cxjÉi«H<ý“‹¤2mm­eìÞª–S<Îüü<]]]¨ªºBÉ”ŽŸ››cjjŠÖÖVl[àõ·ù‡o¢µª‚'ö÷pÿÎ.Ì‚ESS ---e7íÝÆÅëñ°}û62¦Êìb‚Ûº›yc´Ÿ¯œ:Âß:Ê·ÏäïÏc4“dýºõ(ÅÒ°’5[ú~ý5A`bb‚X,æ¬-úæ™NÆÉ›Šè älËἺv[oñ E¯—÷Õ%\fe}ø Ð-3¹,õn/!ÕÅ|&Ípt‘Z¯Ÿ*¯L‘á¹4=-:-Um©ã«¯ñ±]ÍÈ¢H4]`gGÿýÉ-üëïœÆ4ß­T±”Ö²8:1Âx"FGGÁ`d2‰$Ix<Æ'&H..òÀºTI"o¼Ÿ'‹ašfù=ÏŠ·žmÛ¸\.6nÜÈá#GøÁÅ!>ØÚÆëããlØXCmE€·N  WÒÞÞF*•"“Éd0MY–ñù|<Ï F  »»›³§OMd ×ùœ˜¡-0MñÎÅQÖ¬qÆÒ0 àP( ë:¦i–˜$I(Š‚¢(ȲŒ,Ë$“IqkM¢:ìC7Lž?p‘¬¥ÐÚÚ‚iš+Ω뺃ÄETUEÓ4\.× eW²,Ó±¦ƒ gO2:¥³¹ Ãp Š,ðæ©zǗصçEannŽT*E>ïÚiš†ßïÇçó¡,'È+J{{;³³sHbŠÁáEžûi/k;6¡( ùüÍÃ1¦i–­@]_Ù F„òz©¬¬¤ªªŠ±Ñ9Þ91N]8À£û6"‰¯ŸèG]´¶¶‹ÅH$d³YlÛFQü~?@`…]úîv»Ù¸q§NôlTQ¦%A•$ÆâQ§,nÓ&ü~?±XŒt:M6›-[‰¥1÷x<¸ÝndY&›Í200@8&‹qGs«ê®á(˜&¯N`ëÙÒÊÏ_ÁékúKÉûÃ*É8ðuàÏ(f dz)ü²B߆œár4AE:€$HÌÆ³¦E*gðèöþöµAÞXà¡-õL.eXßä‰]M|ãí! ÓzW³U@àÊÂ,g¦'˜ŸŸçèÑ£+,‚x<ΖŠüݹ™)zæð!Ž9R¾F4eýúõH’L<³ˆ$39ž;p‘LÎ1ûúúÇjK$ìÞ½›ÉÉ©râ[¢Ø6ƒƒƒ —cA†a ë:š¦ÑÖÖFcccYqÙ¶ME$B°²’ç/qn ÆŒ˜ãáÎFúÇæ˜X¤±©…ãÇ“H$p»Ýø}>dE¡Ï3:2‚nTVV²fÍÂáðŠ{©©©ÁŠp¦o‚–šÊr¦ñÈùÒyG‰Ÿ>}šxŸ/+öåq&˲ʿWUŸÏG.—Ã4MDQ¤µ6LEÐËå«3¼~rÙåãìÙ³d2™Jª£)½ Âá0---TWW¯ˆß„Ã!lQ¡odŽ®æjô‚y›‹&yþà%‚ác££ÌÌ8¹>Ÿ¯Œ—š™™q ÐU•ææfš››ËAxpb{{;W.Ÿæïþþ ‹iL«Ÿ¡¡ALÓ$—ËÝÃ+ÝÛââ"XuîMÓ$ŸÏ³k×.Y·¶Š‚aÒ\¦«¥š+Wg8yeAtqúôiòù<¯×‹(Šär9±m›ºº:ÚÚÚnp©+**ðÃŒOÌó™-·ÑSS$ˆ<}ù3¹ étš·ß~›B¡€«¨ Åë–ªH Ã@UU"‘Hùådš&F¾Àb&CïÂ,š¬8fD‘élš©tòúÌà/m]Á¯@a]ge}Š"Áßl.K­Û©Å{bww®¯æŸÿÃ)æ2Y’~ Ó&•Óé¬õóØmM<zŠÛ×V °˜Êô(ï)Þ. séo âòx¸ß>|>ŸÓIGH&¼}à•Š‹ÛšËƒ:ŸI©ªbÇöíe d4#SSSÃÂÂby!¦²yâzzzk¬Ø”`br’|>O0d|bâ¦÷X²”6oÙºõëimm¥¡¾ž@ €,ËŠAö3gÏrúôi¦¦¦Ø²e §ì5Ô×sèäI\Êܽ·KæÀ™a2Ùá©EN÷N ›6SSSÔÕÖÒ¹v-µµµ„Ãa¼^/ªª"Šb9‹”ÉdˆÇã,,,pud„óçÏ;÷oøð›Ÿ¼}Ù壹©‘`0HEEelšËåB’e§KK:ÍäÔçÏŸçôéÓÔÖÖ²iÓ¦²«¥( —±Ù¨3˜†ÍÏ_æêÔnw†ææfyøaººº‡ÃåÏær9fgg9wî'NždllŒÍ›7SYYY~ITVV`Z WúçØ±};kÖ¬)[.—‹æææ²;kY=ÝÝ|uB7Y§ý L¥È$—øè]râÊGŽRÐ Ž_c1ž¦¦ÚÇŽÛvÐÓÓCmM n·Q) D£Qz{{9úÎ;:tˆuëÖÑÚÚZV˜’$QSSƒœÊ°¹¦€ËÍpl‘c“£XÅȾ½{YÛÙIuUUØ\Jl¤ÒiæçãÊ•+ SYUE6gss=12LÅã膉^pjMEÁ¾¦ÀÀ_ñKÆ®~e k™Ì ü  è¶Åx&…K‘™Oó'Ÿn ™Õù'w‚±…4YÝ$šÖ1,›ß¹o /ž™â™ãüæ]íL.eñ¨2ï%z•3tÞ$šÏñú<òHyò …ßùîSøD‰t¬'èÖÊ`D˲ilhàã¿ñ‰(–SÓ׋“1“Ø÷~¶nÝR¶Ž9ÂSßý®³È­Rqm‰oiåÝ»\.>þ¿Qv¡–Ç5A ¡¡žžvÝ~;O}÷»œ8q‚Ûo¿½lú‡#P>´#îÛÄó/12½DOOþð‡YÓÞŽ,ËåMTR’>Ÿ††vìØÁá#Gxî¹çÈçólÚ´©¬´ÂáWt‹l¾@k}˜w^¡±¥‡>ð ÍÍ̓ÁrBâVÖnÉšíïïçÊåË$“)¼}-[×5KfšZâ‰'>ƞݻVÄLÊç´¯u(FèììäŽ={8áßÿþ÷9wî[·n-¼]n7±d˲q©W¦æyõx/¡P˜G?ø{öì)œWŒ·¦iD"Ö­[Ǿ}ûxæÙg9~ü8·ÝvUUUضªª„Ãa¢Ñ(===Üwß}+ÆvùùJ–j]Ý͘H’„Ëåp¡õ÷õsïÖ6:ª«ð.úúøÖÏNpäâ8wß}7?ôµµµå±±—)Ê@ @KK »wïæ7ÞàÕ×^£P(ÐÕÕU¾V8bÄ(søý.^ÉŽíÛWŒIé{™o=¢©¡mÛ¶ÑÓÝÍÿøÓ?ea~vvðÛîr\[Ë$o˜d³:?=ÒÏ¡ËËWûóü’™Áåò«º/¿±ãÔ"à´™ÓÃQz'ã<¹·•Ïìke:–#–.ÉLG³tÔúù7^Ï«f8Ò¿@*§Mà]T–(,e3 GÙºe û÷ï/gkt]ç'Ï=Ç©ÃG¹¿­“ZŸc:‹E: ·ìò—ܙՂä×Kiñ”޵—ÿζð¸ê*xÜꪟ_¾A—»U¥{ذa¿õÅ/"}}}åókn7SÄKeyãä{öìåw¾ô%:‹Añ’[VúLiš¦‰Ûíæ¾{îá3Ÿþ4ÓÓÓ —Õ47’ìd C>¥D–{gÛn»ŠŠ $Ir°c¦¹b¼®ÿ*ŵfçæHg²T5»»E–Ê]g|>oÙÝ]þAÚ^QtZZÿ&I;¶oçsŸýl9ÁQz.E–É '½¯<ûö9d·ßù/ñÀàõzËJ¦4öËÇܶmøÂç?ÏæÍ›ËnjéX¿ß¿êü¯–­[±.nòN–Øï‚G‹VgcM;zZ9|aœûxˆÏ|úÓÔÕÕ­x™-ÿ*]Çï÷ó¡}ˆ>ñÃÃÃLMM•ÇEÓ4,ID¯5¸dOs%å7?÷9î¾ë®Æ(¿°K/°ÒºŽÅb¤3*ƒn>²¿ÀÇ«𻩉øpi Ãs‰åÃ0ü%¿îêzùUZXàÀðÿp;à3m‹¼(ÏØüÅOûø«ßÚÁ>ºžC} ìç“{Z[È *"¿±»…s£1¾þÖWž…ShÛ6©T «øv{üñÇñx<åÉxó­·xëÕ×x ­“îšz\²\lò*á’e4Y!!\*½Q és,¡³¯‡°_‹ãبŠLmÄOȯÁô¸A •N3::Êôô4º®S_WÇÚÎN<šVV\---<úÁòÝï}ææfÂá0²,ãr»_àØÅQªÚøä'>E‹oii‰Ó§O344„aÔ×׳mÛ6šššÊ vÇŽÌÏÏóÓŸýŒšš’$!+ ñd¶\«'‰Ò J|µôúò¬RéçééiÀæ‘;6ÐV_±v`¯ž››crr’l.WviÊŠÍ4M6lØÀí·ßΩS§hhhp\RIB7zèƒg‡974Ïg?÷y6mÜXVÞ¦iÒÛÛËå+WˆÅbx4ŽŽ6nÜX^/‡'>ò‘2³Fww7¢(–­ÛÙ¹9x›iR[[K(ºAiÍÏÏFñ–5y(ÊÄä$¢ ðè¾ 4U‡›‰òƒ×ϱ÷NDz*½Æ'&¸pá³33Ű@OOÕÕÕåkßyçŒOLpöìY*++q¹J@^‰™h’w.Œ°uÛmôôô¬°û8îÉT wÑr ‡Ã„Ãa‚Á @€ñ‰ ,Óäžki¨’Lç޳"ר+LJ™ZH,ßCOQDµ¿Öü Ö²XÖ+ÀOpâYä-‹ˆ?Àß™¤½ÆË¿ùðFþõ‡Öó¾wŽÎ:?;Ú# L'©|q;‡zçùö¡:êü7-oD"Áìì,ªªòá}ˆ–––ò"=vü8?ýÉs<ÜÞÅ}k:QEIUIPƒÒ¸Xn»úE ¯›ŒL/áv•(EVçƒ/½õ(’îÞìžÇÆÆøÎw¾ÃØø8ù\ަ𠙼I]S;Ÿzò“ÔÔÔ”•ÁÖ­[yëí·#;>—‹ƒg‡ðxƒüî?}¼ ÞÞ^žúîw™››£ªª I’äíxäá‡Ù¿ù ºoß>Nœ<Éèè(ÝÝÝER?‰‚n,£Y‰#³,‹sçÎ9ôΙLùï¢(!Ë2ªª–¿NŸ9ë0!ìºæ¦8u|`ZæŠ1Y\\ä¯ÿæo˜››+Æ—²ø}>v﹃Ç{¬Œe;vpôèQ‰„ƒ‡,Ýf1–æ™·.°kÏ^6oÞ\“L&ó?ù (góù<¢««‹O}êSTVT`YÜ}×]<ûì³tttàõzE.Iœ|çgN¼ƒm[¦IM]_úÒ—ÊJKE®^½ÊW¾úU‰DQÉø=®r1X–ÉîžVîÛÙU¦UzîÀEܪ²²*ÍÿáÇùñ3Ï ëzù:'Ožäõ7ÞàSO>ɦM›ÊPŒ{öïçì™3ÌÎΖ¡²¢0<±ÀäBŠ»ÞRŽ7Š¢È•+WøÛ¯|QÑ4 Ó4Ë™ZÛ¶‘e¯×K*™B’$Ž^)³Ë†ý UAü>‡/Œ±Ì À Yï—²‚_½…þ?`?PoccÉM¡Jþì¹~DAà÷êâÌÕ(ÿóg}üþ:ÙÑab)‹, |bO ÿí'—˜N®ŠÁ*A&&&( Üwï½ìÚµ«<—/_æ©§ž"€À¾–5x].䈈«ZBöIˆ*Ø"§@Ìß¼\@À4-2yÝ4oií9–ˆÅ­jËMÓäg?ûKÑ(›7oæòÅó|ù‰;ð{]üÕñýü€/ýöo—³U^¯—-[¶ðZ1F¡ªŽËOåÙ{×m+ôìÜßüÖ·˜ššb÷îÝ466N oxx˜gž}–HEÛ·m+»Û¶mãõ×_§«««üVWdiUH†(ŠLMMñÍo} Y–Ñ4ééiÖ6„h©‹PÈ’)ݤ ›Tj6¾s;!/†áÀ$V{ùˆ¢ÈÈÈóóóìÙ³‡T*ÍÀ•óWVÝÝݼøÒK,,,àóù°Í%³¡­ŠÝÝ­t4V²ÏðgO`l|œp8\vgGFGI&“ìÚµ‹¥¥%&®ösÏövúÇæ©­ðSö x¨y™]J2µgn)Å;—ÆyücOÊYÕË—/ó½ï˲ػwoù:¹\ŽóçÏó½ïŸQ[Kee%–eQ[[ËšŽ¦¦§ijjrÖ¯ 0Ma‹2••+¬ÁsçÎ!Iûöí[ឆQΦÓi²Á,CYŠFyíd?ëš+ØÐ^ËØL”×ÏŒa"–jØMœ@{ÿû­L~¥ k™•uåú²¦ÓIx]Eÿã'}„}*ô±M–ÅŸ½ÐË]•ܵ¾šú°FuÐEУ02Ÿ¾¡OI à ›ÍÒÑÑÁ?øÁr©Åèè(ßúö·R‚L†ŸôŸç·ÛM°Éå ‡u“ùXš‚nbÖMxàp…—~A7¹U ¶eY«º:ås B¹„¢©¹™H$‚ 8Œ™ë[kù­ÇvóÿþÛ ÒÓÝ]¶^º:;yùå—I¥RD" ÃÀãñpÛŽ+baÇc||œÖÖVêëëË×UU•®®.b±‡¢§»» Dìêìä•W^!NãõzÑõ<ဧLù|=_X4E×uvíÚ…¦i¼ùæ[ܹu Oܳ¹=¡\xkÛv9ne7ÓÍÆ/—Ï#I>Ÿ|¾€$ ìÛº†©…§NŸæöÛw–]Q·ÛM0$“É\WÝ!ÿ[·~cÙUùùy:„ tuu­È¸VUUÑÝÝíô˜š*+ÿp8LCC ´´´`²äXâmõx5.E¢P(¬\;EË$Ïç‘‹TË.Uaz!Nïè,¹‚S,^Ê,Ì,ÄÑ•lܰ¡<ç†apààA’É$›7o.ÃPAÀãñÐÓÓÃÛo¿Í… ¸çž{ÊÓεkéïï/g† ¹ u•UL,åo€`ø|>²Ù,óóóTWW—Ë¡dY.ñr©¯¯' ráü9I¤»³™ÓCKXת9ß.é€÷S~åVQiÙ8À±‡í0—ϲ1¡Ã¬æü¤—}]UüÙg¶²§³’¿{c˜?}¡·ì6dò&Í•N-ÜxâFõh?ö€ùæ·¾…eYÎ[.åÈÉ4ô^æ#u›‰§s,ÅÓä Î"Ìä HÚÍ]BQ˛ϴ¬›þ7Me/—HR–¤e¼S]Gc%•^úûèéîœÍ_UU…Ïç# …Èd2444PWWW^ຮ308ˆ(ŠÔÕÕ‘N§W-Ç™.bJ¸ÊÊJ4M#•J‘N§‘lƒ¶†Šò}­@ ŸqeVÏ.*)G b‘*æ: M.òÊ‹«Sã_çz:v¬(¬k©æâ¡«d³YÇÚ)º?Mc)-*&‡{Ì´>~ß$I*ƒ gggYZZ¢²²¿ßO2™\q]—Ë…®ëeÔ98å?õõõœ:u ]×™™™ec{-ÿòÉ»%ÑIá›ÎZÄ­ék±=ÇÝkª±sC3ºáP°tÝ0ËÔ6ÿçñÖ¬)ω $S)†‡‡ñù¼TVV’L&oˆ•išÆÕ‘‘1©ºººr­h__?[+Ù¿}-G/M²®7ìÝ»—™ÙYΞ9ƒ[Óhjj¢¶¶Çs-ð~Ý5›ššH§Ó<õê9BÁ ºa•¬«ð§À*›õ——_‡KX’‰âƒ| ð˜¶ÍH:Áº@ˆ«cKìgSK˜'÷¶òèöf’ŒÍgÐM‹Æˆ‡ ~xttÅ mÛ¦ºªŠ-[¶”ßœ¶m“H$øÎSOFÙµk.—‹ºÚZ6lÜÄÓo]@Ö·]£b1-›|Á@òÜ<à.K†iiq!Þäpgá•«V滄eÈRçH—"SW`aaaÅ"ôx<„B!R©…Bt:ͦÑ4íÚ³”¡,sùòi‚  ëzÙ}([Ó4ü>Ñh”……ömi££±Ò¡ÎYMV#£*þªPpÓ*|ˆ“~ï–ð½A*‚^òù¹\®œ†EQÊUAXˆ§ñùÔ/ˬ•DUUR©GŽYõ†aÜPÇYKNNM‘Œ/òÁGï.òÊ3¡¥DÄ-žG”DtÃbtf‰L¾àPðX¥d…3ýÙ\éÅ$÷ÜÖ\®IÐ …b ÌâÔ©S«–år¹2Á¥¢î‘‘ÑöîßE$è¡Ò¯râÄ º:;Ës ùÜg?˾}û8~ü8/\ ¿¿ŸP(D]]555e‹t¹477377G4•[îý|xÞë ~M k™køààÓ¥†gg‘‹æJØ‘šÏ%³­-¶öˆsróFR9ÛÆï÷ó[_ü"º®—±A£cc\¾|™7–³f---Ä ~üÖ>åRœŒUÑ:0-û–·ÛMÁ°ˆ'óxÝnçz3ê€ÐéF½š5Vڳ˻n˜Ùm;Ó‰l9vÇïó±°¸H"‘ —ËQQQ±»¤( ŸzòIÉäM÷ x=üþký%IÂåv308H…ßÅv¯Ç²ltÃzÏ$y%KK’K´kœX¿” mÛZQš% X3ݰp»µ"O¾]žŽŽþàþC×ozI’hjjZaåin7¹\ŽÞ+WØÐREȯ1<¹X¶¶ã'œp åv¹>ÿÙšK)S$•ðe¢±T–lÞ¸¡ò óå/™|.Ç­¤²²rò¿d÷¦f"sÑÛÖ7ñüáwhmiaïÞ½åà»$Ituv²¶£ƒh4J?çÏŸg`pÞÞ^Z[[ioo_QJ9•6Ë”Uð€þ«PVðëµ°À©Øþ`ÐàËvñ@O]9ÝmÃ-c@ËŶm—–$©Œ)é\»–»î¼“£ï¼C(*ƒÿDQ¤«³“h4Ê ‡.ó™‡wôi¼gMI1JŠ‹+#3lïjÁ²­›¾UKn@*›ghbÅøŒJ8£̲m&æb¸U‡&‘ÎaÛÊ Ÿ)YÓÓÓX–U®­[~Lccã{¢{¹¾ÐضmrÙ,ë65RÐ Æçâ<Ã|JÇé×7<ålæÒ®k2[r#cÉ ºñîdŒ–e±KKåVÅʼnË2iËKidyåÒv»ÝlX¿þ=I £%I’꫹ع±‡T¶P^§‚€ƒûÂÁÝÝl-x½^¼¾ ‡Î—;Ñ ƒD*ÇølŒËWgIfò˜åÏ)IkÚÛßns=´¤DÍcšmõ$IIJl6µ×±Mñ£þ€É©)öß}7ÕÕÕå¸ @$aÏž=ìܹ“……Nž:Å[o½ÅÌÌ Û¶m# ’Íf-²LœR¼¾wä_B~m k™•uøsM¬‚È;WSLÇóe+«d*¿›”Òàù—‰ÇãáŸþîï q¹\|ä#!•NsêÔ)vïÞM0,jׯ[Ç©S§xíx?¾k²äôʳ®£€‹µl%ŰfÍŽ]ºH4‘&—×Wwõʦ¹Ãyž+ô[l;Y)Áñ|¶e³O#Ã70+Å2•ÅE'D`¬R +–ý–‹,ËlÚ´‘3gÎðwÏGSe‡CÞ°0LÐM§]]ée´üþLÓdnnî†âéw“™™Òi§f!~­*F’DîÜÖÁèL”—_~™S§NÑÝÝÍömÛhmmÅëõ®WWWóÈ󹧇ï~ï{劋ééi’ÉäòõùŽ;ø+qËcù+;ó*²Li} ¸x\àÜX’?ýÙU>¾³¿[¦>¬ò:Ä÷+Àv×ïA ›ÍÇ™ššâåW^á£OI’™]LR(eëvdj‰É…$Û¶mGàâÅ‹?~œÊÊJ6lph‰ZZZÊ@ZÛ¶illäó¿ù›üõ_ÿ5§OŸ¾žTqøcÞ6†w“_·KX’ð_€m@‹ ÀOÌðƒÃW‘1h­öðàæ:ÝÖ@g½Mu¨‘ó†…wµ‡e:::8tèkÖ°}ûv,Ë" ñéO}Š¿úë¿æÜ¹sìØ±£L}R]]M4ãS´ÖGPd‰¬¡—•M©~¬T4 Îjkm¥²¢‚C‡•ßB«‰€c©,ÄRä :‚àZõ8]×éë룦¦†uëÖé=,.\8_ŽK”î)›Í²¸¸ˆ€ÍæµõÌ.&˜ŸŸÇ0Œ.`"™,ׯ½B¶Žiš4661?3F&WpJŠ~ùr1M‹¹¥$Ù¼¾ìzïñ”Åî4 ±ôªŸY­$ÆëVÐóyâ±5Ë` %:œ­[·®HRÜjL2™ §OŸfÍš5ÌÏϳK³¶Øt´Ô¥G(6g-­‘’\ï’—j9kkkW3==Íââ"6làÊ•+Ì-ºò1EöÒ–––rhãÝĶm.]º„(Šx<^fŒÎDQ‰‚nòʱ^B‘*Z[PiKK ñxœ™™Ž?Ψ«­eß¾}ìÚµËao°,ªªª¸÷¾ûøá¸œ>¹€“L;¿Zë þÖ2+ë4N<ë/ÕT—‡õ¾0ñd†?nïã‹ûÛy|gŠ,ÓÍ›.ôæ¦&lËâÇϽ°ÒeÓŠÖ‰išeVˆå×,añz{{©ªª¢±±‘þFÇÆÊ/ ˲¨¬¨ ¹©‰X,FgggÔz³û™™âñxyÍ.Í'Heóø7½#³Ì'òÜqÇmå„…(ŠD""‘ÄãqÆÆÆøî÷¾G6›åÁkóêððõÊøÂÎ_¹²‚_Qñó»É²û¥²c‘Ø6),C!dQ¢:Éþñ%þë³—¹<ž¸yg”"&§¶¶–±±1ž}öY²ÙlÙëêêâñÇcdd¤\ª( š¦ÑÒÒÂÅáY&æb yÌâ›Ò¶m|^/Œ“Ífod¸ÉÂ1 ƒt*…Ç­:•WÙm¥M177GKK eHƹsçhoo§½½}ÅâìëíÅ6uzÖ6 K"Õa?©D”«W¯®ÀËì¹ãB!§ƒËÌÌÌ op6q)®sáÂÇzlkcaqEr@¬ï•>?ŸÏSÈçP••ï?QŠmÑÝNwì¼N*[(»™Â»(.Ó²Ê=ËJk)¥a¸U…Ê ‡K—.Q(ÊcRSSÃ]wÝÅåË—¹rå ™LfÕ¿œ‚gdd„Îbú?“N 04±À›§yãÄgú'™_rpQˉüJ(|QYXX(ßãòµ“J¥èëë#™L²nÝ:¢Ñ(ÙL†‘‘ËÏäv»y衇H&“?~œh4ºê Ȳœ8ØÔÔ/^ÄívãóùÈd2¸ E’Èt._Å0-fgg‰Åb7x Š¢PUUŶmÛ¨¯¯çâ¥KåõsìØ1Î?¿üø~OéWî –äË%,Iøÿ[€ 0œŒ1Pȱ{}{7u2¾˜á[FHç Ó¾iüÁ¶mRé4šKáÜÙ3êèà¾{ï-ÿmÏž=ŒsìØ1@¹yB8fÒ太+ªFÅR=•$IÜ{Ï= ô÷sðàAjëêP…DÂ)ðô/#Kƒk‹rqq‘B.ͺÞnÉÝŒ„ÃŒ¡ª*“““,--1>>Nmm-¿ñ±•]—R­ä‰“'ÙÖÕÈÆ5µ¤2y‚~ ¿‹C‡³qãÆrYEuU¿ý[¿Å3Ï<ÃÉ“'q»Ý„B¡2]r>Ÿ/DmÛ&›ÍÒÖÖ†®ëŒ\fK{->M}÷ÞdŇÆïéj©*IJl¦— M.22µÈb—@CU£Fh«àó¸Y¢wdŽX2C:“^K«««s(|fvv¶léær9R©™L†|>O$ÁívsæÌšª$3Nœ8Á£ü`¹Dfýúõüֿȳ?ù ‡Æï÷—“K¥s&“É2³D:¦¡¡|Ñ5ÞÔ^ ¤²ò†MOw7³³³ £iÁ`°Ì?”ùÌfffØ{‡ÃÒ:00À›o½Uâ.Û»á×c]Á?¢ÂZæö„( ؀ǣ±w}=Í64щ¯¼>D"[`ÍjúJH¥R\æÎ­kxùå—è\»¶\f!I|äFFF¸té·Ývš¦FÉçsܶ±…þñ%Æ'&H¥R<÷ÂóÔÖÔòÈÃóå/™2::J*•Âçóq×wÒ|fGE²¹,#£#ìè¬g}k ±TvÕçW…‡~˲˜ššr|ÜÿŠz1çñŽ=J*6ÏÞ;n#â÷à÷¸Èät¶v5ñê‰K?q‚½wܱ"Hú¥/}‰¡¡!.]ºÄÄä$é"nFóxhlh ±©‰ÖÖVÞ|ã ‡†èëë£Ê¯²gsélá]³ƒ‚ °216ÊC»Öö_ë}ðì0gû'¨ ù¨ yi¬áq+N‘¯éÒÇfâ7=ïüü<³ÓSìÛÒZl=¶ú=”,„¡¡!$tºš«q»dBÞ^xá§4þNCô¨ª*<ð[¶láÒ¥K ²´´D:FQjjjزy3mmmX–Å7¾ùM¦¦¦˜ã‘=t5WSñò¹YJdDBÁ`!–dfzfùfF–e>òøãÔÖÔpñÒ%2™ ¢( YÛÑAÇÚµLLLðöÛo344L!›dÿ®.&çb¼õÖ[lܸ‘öâ}lÚ´‰ÖÖÖ2ÓÄl1 (‰"~¿ŸŽŽZ𛩮©áÛßþ6étšD"ImÄK6¯38¾€Ç­ y}|òŸ@UU†‡‡`br’X,V¶EÁëõ²ÿ~î¿ï>–¢Q~öâ‹$‰åîà×”öò¯KþQ-¬eJëY`;ðorºÍ7ŽLóì‰1rJ¿ ¿&3—pµ«Å j"»»[‘%‘Ñé%žþñ3üΗ~»¼`C¡}â þúoþ†ññqZZZ˜™™¡6¤qÿÎ.2¹ ¼þúëN€öj–ÑóÃTWWs÷]wñÉO|‚B¡€eYeœÏò7¾iš °¼N¹†eã÷º‹nÍbÛ6üÞïý©TªŒd/YAËãe/]âå—_bow Õ±D$àq±m]#K‰4O?ý4>Ÿ-==NWkËBQ6lØÀ† ÊAgp`¥z1Q¹:<Ì‘£GQ‰ÝÓCU؇æÊ3MÝ¢Vâ$o¬ôÒÓQ·,N5?½w õ•l`b6ÆÌ¢Ã• xÈëÆªäξ·¯ÖÚ -µá›Æl–ÓX/.Ìsߎ54V‡Èä ìÜÐÂ+Çúùážæ7>öÑTCÕÕÕÔÔÔp÷Ýw¯ d–e¹LËFÑ4‹/ÑÑbS{-n—ÂÀøŽ»ÌÿïiÛ»V½Kî–»§÷@ „z9J¨¿£pG;zhZÒHqº÷^dK¶¬Þ{Ù]mߙ߳+KŽGü¼^+ÒîÌìwf>ó”ÏóyLÊŒv#XX™OIžÇ¸¡D‘`$Ξ¦vé&9™ïDaÖ¹ÌÔD„Ë×Ô‘ã¶3³{)»ÞÙWVX.•LÐ34Éö#md<ey]÷íäùKººº¦?')Š¢LkFͤ°ØívÜn7¢žæ’•ó2#î*‹|¼ù²zn{ýj.ZQÃú%\{ÁbÌbŠ'6n$‹ÍÒ¢Ÿ¹öYÍþìï}>’$“ï±±nI¹^ù>',¯&å'?ýûöï'NOWå²Ç'Ë2f³y–4uöaZTTD*™¤ºØG2¥òÂ66î8NcûñfŠóe{2m6\.n·»Ý>]ܾc‡šy½ôaŒšøgà…ô÷oâï·LoXh.rA@DÊ=>®¬¨aQn!½I&Æñåä099ÉÈèhF¬¬uEÜzõJœv öÌL½± »4âv#åŽáO¿»WÔJ ¢«¿›Õ J¸íõ«±˜äL¢Ygl(ÅÛÊ߀†ÆÖὬ\¹›ÍF:& NËnLLN200H{{ÇŽãHãV9–p{Í[Ù4°‹qi˜%U…4¶Ò58Éd" ŽDHgžìÀ¬¼œªªŒÓÔÔÄSO?ã?Fd*„Ófa`4ÄäTÔ„Ì(÷ª¦3¿2“ /lßá#Ä¢1Ež5Œ4{‘F"z2ÝG¥éØ1úûû©,ô"K’1‹Ð$cRŒ¯¡u§Ûhatt”ÎÎNcõüRÖ/©xÑ9ÕOùÙë´±¤¦eóŠ(Éó`6InéÇçÏGEÆÆÆ™˜œ -Ó ²nq«”bµ9¬p,Á‰ž1<^=½½´µµ34<ÌñãÇ &X³¨§ÝLïP€¦ÎadQ`åüRVÔ•ÐÒÞɦ­;4„ïÌ™59õá‘MžŽŽÒÔtŒ‡² ÌÇ /¦ÀçÄï±S˜ë¦,߃ÛaeoS‡Nô³rA)ËæñÜöƒ´vtãpاlÙuŸ¹X,ÆÐÐG娱&r=Ƭƒ‘É0ª¦c·š(-ð زc7ííÓÕFEQfw³ƒe;;;9ÚØH__/š® Ç)É÷°¤¦\¯ƒmœhm'žH ËÒ,ïl¦Z‰¦i455ñÄÆ3•(bÀçAxþoCÁ¬ý½]^/›eBC€›€»€,ˆ\_]ËEuüñÈv´vb6)¢¡ jŠ$ð±[.dYm‘!a¢jüìÁìÚßO8C—u¼.'áX”J¹Œ?ž÷#FüWãOhñ±[.`é¼"¦¢ víâ7òãú¯IGùTÑ[U€Ûí!180ˆ$Oöt2 «(’ÆD êmõ}à¿y.ò wÜz Kk éèçK?}†oÍÿ2eö">¶ÿ+íƒÔ–æ±óHWø.!­©lžÜÆûn\GIž‡Ç·7ÑØ6L2¡¡¦à‘ ïâù¡|ëøÏXŸ³’OÌ¿p:§}Õåºóa‡Õ˜¾;‰ÓØ1LKÏ,«baU5¥¹”äy°˜åé ¼ùg¼Â ¦AË ü¸ ŒÒuI®‹Ü¸†ª"/ÝC“üðÞ­ ŽN1¿ÊÏ;¯]CIžçdó´®óýû¶nqòƒU_â®¶ûت>ÇWÞsáX’ÜÿKSëùíúïNGù÷}_fGê>Ûeäz|úGò.ïíÜXv_nømŽƒ,®.äÞgrkÉM¼¹ìu|æð7¸à áh’í[FøüÂSå,%šŽ±Î¿œo4ý”¦` ¿Y÷?˜8ÊG÷™îöM4“/ã²[x݆…,ª,Àe·ð»û°šnº´ AÐ¥9z*uÐt¥¬ü²Ž¦ hˆˆÏFÓÉ=ªfTäF&Ã4¶ R_WLω¢H™µbZIÏxFÙx.+ˆ7M0‰£È¥ù^XƒÉßÔ1DCëc0ù>'o¼h)³ QÔ_Ü¢“‚TJ s` Ä4óÊr3ù½SÚ¯2 Ó::ÑxŠT*ÓnA…9×#™„`8Í&b³È³Â¯S-›Ô3ú]ƒc!6íoåüe•”x§sv3É¿B†ì:29Å}ÏâpK?ªhæÆnÀçõ0<@gÝjfy] íƒ8}…|ä½·³~Í*.:ÿ>º•¥åyØ­fJó=¼ýšU¬[R΂Š|’é4«–rÛµk¨+ÏÃbRfyºº&b3›Ùy´ƒ6Æe3³ ÒVZàåÂú*.X^Í…Ë«(Êu×€¦ñȶ¼p¸sæ’ vÂ?¬àX‘H$ Z!àp!+£™‘HEšΪâæ•æb5ÏÝÜ®gÖ tŠr\¼²š•óK§Ï1dDœ~e=Ògö¶óèö¤Õiï8‚‘žù?ç[ÎþÙL÷9m?ëpI­@öïÇfH$`$>Æ8ïV¾ÙøDGoºˆ@*D«šÏ/ú0;F÷ñ§žÇ©sVO[ÙJ8šdL Sj/¤Þ»¯7þ˜r{1—åŸG bmN.&Q!©¥Xæ]ÈBW ·ÝB×T®»ÆPUkãM—,˜Nhÿ" †¸ tM@”ÈRÃ… œ¼t]'ßçä×­™ÍôŒ«1sÛ&YÂ$7ÒÙ(:Âl0ÐuH%ßI/ñèÔu#œœ{ÿ‰ AÈ€õKŸ( 4´ôŒ üì{ßfÕrCé6žH05fdt”Ž®öì?@_ÿª¦’ãó±tɞݵ›• Jéšd‘u—®Æžë5¾ÌÌ'‰®#›§`$n¬mf†£ªfÖP:}1Ã8N0ÁÑ4­{úï.°ýHmmÎhŸ `P¾ü^`¯PÀ‚Y õ$ïã5.D*IDATã€KÓt¦b 1NS°•[! ܵüÏò/ðÁÝŸJŠü¹ç ¹k™J‡9jg™w!á´!¤÷ÓÖ{Ø?qÔøÖúë©sV1ÏYIŽœÃÑ¡füÞ¦Rat4DQÆ$™ Ý#tÒzš[.'ZFpȾ¾è3¼¡ô*޼ÐÌö‘}œŸk …ÐЈ" …–\<ךּ‰¯ù!Ï7ñºËj¹vÃ"ìVÓéû$Oc§(­ªD㩌"„€Õ¬!œ-A™›^<;¯Æ8¦ÓW*jÚX鯼’R)2ᙎ% &š~¬ÒiãgñeŒä3<(Œ~È$ªjH䨬6ÒiƒÀ™íMu9øs|„¦¦8ÚtŒm;wm\6‰dšMÚ(^» OY²Õ|·wæ¹0À=ëMžiÍ$I€éÍéß' [¹÷¹£DãÉ™ÁßbˆüÃÔCÿ¦sôÏ>€—²Sô³ò€ÿÌ‚  XLˆ4rAz ã‰q^_|)Ÿ‹|”¯íû.Wl¨FY(±iÿ^&ÆÓ|}Ù¼£ê&Þ¾óãlÝGëT'é~\f3cÑÛF÷òåÿ˜iÇ­r”SãŒÆ'Hª)̲D‘5«l¡/:@B]Äy¹+¹¹ø¸jxC镤µ4‘t³dF$¸jØ2¼ ›dÆ*[q›\lÚÅû÷sAŒw^¹ÃBÿhв,NW93Ž{FC™igºUˆÄSì<ÒÉÁ}ŒN†QU-3†Jáí¯[Å¢ª‚`:eÜèŠræ›3kÙ™$Îñ™y§³µìý)IY¼³ûœ$ŸÌ_do^A³ù¥½¢—˦" r=N\~òÓ¬_½jšŠa±Xp:¸]NBSSLLN2:>n4ÑÛ̼á¢%¦¢Œª°fù|Ì.BFïmöŽ  5žÀfñL‡¿¢p2”ÖÔ“¡ëLà£K¤H$Sˆ¢ˆÝbB’Ĺ ¢À±®Q~÷téºìÿ„_I`¯pÀ‚iÐJcÌ6Ì> H’(Ñoes`×+—SbKð¾yoc*æÎí¿Âå‰FTn)¿–[ʯ#®&Œð¡yoçÀD#‡¦öSåõJ$¨qTzòØd5Î Žš98Î:ÿrÌ’™Kó]Õ4Lçò‚óYêªäWk¿††Y²ò›öûˆª1¹ç! ·WßÂ{ö|†ÿnú ¥¶BAâÙ¡mtÄ:YH÷>yˆTX&-&(/sá°™1ɳ‚ÝbÂi7ã¶[ñ8­xVÜv v« ³IF–%ƃ4÷RSšËÕëçã²[PU©hœ|ßÜCgáäëTˆgŠé”AœäŒgujþ(+!Ÿzueö¡i™åo í²×t_vS$‘P$N çªË®ämo¾Ÿ×‹É¤ 08<ÂþC‡Ù½o? G›H¥R,¨È犵u¬YTÆÏØŽg^ö ä¼UÈ3’I>½k*™IÐs}wI"CÎ%#Å­óÌîãÜûì!B&3¾…Õ¸Ë IÇl~vÕG:yã%ËP§ûSºüê‰Cô„fJ:á•“·ši¯ Àša!àó€x‹€@DrÏÐ#8d;ª¼›dåÂüõ\˜¿€”–â«GÀÏ[ÏK¯æ»Ç‰$ˆ¼oÞÛ¥Ã4L§ÎUÍEyk0‹&îézK¯æs‡¿I[¸‹2‡!ø¶&§žï7ÿŠ¡ø(iÍp#jœ¯ý::7•]Ãd2Àý!½ÑÜŠ“EžZ>1ÿvQ&לÃ{kÞÊ‘@3O laëÈn¦ôInZTÏ%•ó¦4U]'­i$Õ4±TŠ©d‚‰h”‘ÈC#!övôðLòš¤ávY©(ò± "ùåù”x1½„–Õ鼚™Æ¿§â6àNÝ— œôþÞªÞéLÓ¼ÒËX¢(Ÿã$éà=ý}ýý\|Áù\yé% ޲kï~º{ºXRéç‚ú*Ê ¼äùø\6v4t%Ü%yˆ²d„ƒs™®£XÍXý^:úÇæÆ2¬UUçÁM üqÓÜË꘿j!îªd«™äT”mg锉¢@÷P_>~ˆ®ÁÀL°:|W×ê¥ìUX3òYca¡¸A@ ¬FøyßÉ1{¸­ô&D=ã*û7ñó¶ß‘ç°óÄÀ&ÜŠ»låh ™†Àq¬’…æ`Š(ó‘ÚÛXæY@,çÊ ¸³å·<Øûå­Ce ­y˜D…‰d ‰³kìo+¿K.'›èò›õße™g>² Uc œ +ÜÇ_úžåù‘m(¦ë++XQ¸”|ÇI]-A3 ÃVYÆm±R9™/]#©ªL%Œ„§èLÒÞ7ÆÃÇÓ²´®ˆ[¯YEI®{–fº¦>4ökT_› (Tõd²øïû¥iFÈ)Ësƒž$ÙòÒûÈVO }Ï´_›ÙÌÊù¥¤ÓCã!òì…8x€#G¢¦â”廸éâ…T—øÉqÛÉqÙ0›dz'ÝÌ¢ôÒŠµ’I¡êâ•lÿÙŸXµ°›5‹Êç'ÀS»Žsï¦#ø×/#wq ¶‚DEF×4¢£ȱUÅ9è舢@Ïp_>~öþ‰™`ÕŽV{á• Vð*,˜ZƒÀGp€@Hâ[¿À©ØySÁ5™I&)èÙˆÛ&³¦¸œg;šYà˜ÇÅùk¹·ëQ6øWq`ò(‹=uŒÅ'¸}÷g¤BÜXz¹?w,x?ðŸ¬Í©çUo&¡%Pu“(# ½Á);ˆ¨FËDZK£ˆ2ö¼&'BíŒÄÇÙ3~˜»; Ä(« »b¥Üã£Øé!+@“ɱŸ”iT}š}­H96;~›Ey¤up2A×ä8j:Ì{6ò®k×rþ²Jã˜ÒFUNþ¾ÜÑßc/×>5Íð Dáô^šøR¯ =@Ó@Ö@8‹*£>ƒ£e’LTçd$ªU†Æ§H§U¼.^§ ·ÃBŽÛ6+ÿ¤é:#“aÌn'’"Ïízr²z¡ë:5—¯c¬¥‡ï߿ۮ‰rÅÚºY 'ŠûŽõð»§âY±ÿ¢j§ Åa›Þfdx—I"ÏçD@ k(À/?HÛl°ê«-ðÊ+x•Ì­^àCÀ/€+ED&’¾Ðò=ÒºÊÍ…×¢¢H…0IN“…"§›¤šdµo÷t>ÌK¯¢ÚYÆC½Oa—í¬õ׳Ä]Ç5E:o(¹’ÞÈ1ã$žu £“oÉE%ÐuÜŠ‹jg9mS]H‚¡øÒÒtGúèó—¾gøsßc,*Èa¡µœÝÝHI67Rä0f»¤4•¤ªb7™ñZ¬³¾s('¡é:v“ ¿õä(Žì<·ÙB¥7‡|‡“ý½üæÑ=Tz)Ë÷N—òô™ù¤¿’âp&ËOÏ"gÞÖΓ$@yiÞVöû%'ó]ÓÛÊòÖô¿ž¡ëFÖç²1Š¢È•E9H‚„ÕdÂë6c·*ÓÕºés¤ëDcI‡aV˜§¥UŽ?²™’µ‹ñ”Ë„…ë?| Gr½Üuï“$’i®»p±±–‚@ÿH_ýeByyËê‹×uÔtˆÈqZqÚÌt LòË'Ò1093ÁÞƒq= ¯|°‚W Óýll>lÕ‚ Ucì Â-;Xî^L$㡞çŒ$¬ 2Mò¦Ò«9je,1Éjne‰g‘t„­#{p›œ¼¾øR<&’ ±Ö¿œõþˆ‚Ä=OÐéã%WQj/Â,™%…–P;F÷Sí,çéÁ-ÄÕ7–^Í@l˜ßu>ÀÝ ËÆÀÔã#|¼ö}¼¡ôJî~Ž}=ììébkWÏu¶Ð<:D}A1։Ф¦"‹"n‹§Ùl]h*E(Ç"+$Òiî:°‹¾P€7/ª§sloŽ•ºòüL_âì0ÊèëË´óH?h¥2íAgµ-Òæºp `¦Óæ%!ý:Ó>tmvµræûᯣBÂI†¾,œ*§Ý‚ÇiÅç²á´ÚqXÍX,ÒÜL¶7t0i²P°¸Ù¤œî}’t³€!õbNTÖÔôßkNr…²y¦Óyo©ôÉ2ï6ÊíÅ|âÀW‰¥ãL&ƒ,ó.Bçþ§ù™HE™¯-½ƒ%žùì;ÄuÅ—ã5¹‘¦“¢ ¼+Ñ„I2ÑéçÓÿ‹½ã‡¹­êM\_rK= ˆ¤£<Òû4ö?Ç7ë?ÇÍe¯Ç"™QDY2Óêà{?Gk¸ YÖq[ͬÍ- Ø5Ÿþ|Üf‹‘$ZÇGyðXç—W±¡¬Š‘ȲhLAñZ¬F=8Íf.¯šÇ_š …Ïd·ÃÊÉÌØl“d ýâ>¼é9ÀFÓ ¯h.j„$1ýøÓ4Ã’¤¹sD³¦ºŸ²Y6( /ôï©’4Â4E$«ò éÚI²lfYψ¢©.6õ£&ShfZZEÌ rn]9íÏîf²³Ÿü%5³©ºŽb1³è ³¥±?_ýa~¿á‡|pïð¿m÷rÇü÷paÞ~Õ~?—æŸGëT'i-M•£ŒG $CDÓ1œŠ“ì•i“­$Õ$Élˆûü:=‘~îÛðcVû—KÇèwÓ6ÕÍ=]óº¢K¸¢à|Z§º&CXe+öbšƒm˜<Â-K–²¼°«¬`ÊÈg¹T‚ý¡ ¿mØGg8HžÃɆÒJÌ’ŒUQ0‰2c±‘d‚B‡ “$qYe-’ òÇ£$êâNGö‘å¹Ã·å|2–e¼ëš‘cz©Š¢$g¥næþ{–1/ÌáE ÂÙ%Âÿ›–”æ¤RƒÑÚdpÒ™>HY§5¹&§bhîåXçáX·ÃÂ’ê"VÔ•â°™O&ÈÑϪ­ª¦Ôº³™ØDÅf!O"[ŒíøªJ0;íôî9Jî‚*„SÂJðTQzÞR6>ø<æUËj&yEA¶[;ÚŠ½(«Ï®i· O"Ï&Ñ5fî‘íðê+x ÌY= ïä´žæ·}2ž ðÕÚOrÏyßç]»?Íw›É %WoÉÅ&[HhIFãã´‡»ùsÏä[sY`rf.N›d%©§±HfžÜʉ£Ü·áN–ûÑéc :LZO³ixç4õ³‡¾É‰©vÂé(yf?rW±ØSK…­œö‰Q.*¯6뢧‚LF£$Ô4°¯¯gýBÎóy‰în@‡Œ÷eÜ(^‹·ÙbFr7©ªFÃr¦iötàíO›éœŽ R'ë¥ÏÇ™ÂÉlsî?Ú²òÁc0#“SÈ’DaŽ ·ÃB,‘fÿñöëbt2‚®ƒÇiaiMn»•‡653Qd«ÀarÑ×`÷þ&**[¸lU-“SÌ&™y%yTúNÛþBæ¼T•øÉ³( ëÀU”K2Åâv ¶\/E«лû(u×^ˆ3?ç”m^Va}mÏíÁU]ŠÉeGWu‡•ÄdˆÎ'¶1ÿß^‡æv0¶ó(£[£%’3OÄ> °:½g^öš,xOëŒÜÖGŒñ~–±äߘÿî^÷]ÞµûSÜÙr7fÉDŽÉC â'~GqRi/å×b‘»–:g%¢ 6ÙBZ3&‹<3¸•zïBrÍ>O#¦ÆÑt±ÄÏ m'šŽñ`ßFü6ó|¹T[= LsOϽ˜{ÄÕQQ¤+0Áö®vƆQ6¬>ŠÅhÛp®\ÌÛ¿òy¿°ý/ì&¥©Fu2cb&lÉþÜ ðHóQj|~†ÂS4´ö³¼¶øeY_QÌÿ†êÚËméôÉÐôty.Q˜œŠñð–vîA›ÑQñøn¸p1‡[û8x$ÀŠÜ Y`É%ª†™çþ㇋RãZÊ­ ߊߚG¾½ˆk)5Å×vÞÁ·îÞL‰£ MW‰ GX__ÌÛ®\‰×e›´t]Çë´qA}%ím¤xÅd«5•FRd$“BÅ+èÚr€¾=,¸þ¢S6¢,áÈÏÁ[QL25ª‰¢€l³2¸³‹ÏÅídèé=Lì?ŽžVg‚Õó öfxõ‚¼Š“îsY$É&ãÀ6Œðp ™†éîØ»9ß¿Š7]Åý=“köñÞš·âV\Zs©p”ÒjG%®-¾§bÇ©8E™¦@3ÛF÷rSÙëx¸ïÜŠ“BkÇCmì;ÀcÏó@ïF:#=˜ey\t<++ KYœ_ˆÛ*S“ë¡Ôåå‘–&"UÅ\÷™çÍŸý¯ÿÀí\ü–›(šWMçÑ&Z"‰ÒßtœõÅåXäÓ?cDA`(2Eo(Èx4‚¢È\P_5­D9çp–º[pöJ3mŽ9—e{µŒØ\Ë!ãÁß¿w3mÇeÞ³ðÿñÞ¥ŸfuþÅôsïÎgIÝ|ëÂ_ñþåwp^É%,ô/êX91ÑÄXtYR8>ÖÀ–Þgè ¶Rä,£Ò3I”Ø?¸“w.ù7Ö½rû|¶4¦y°u¥˜MsŸA(Èq±ï@+ã¡(9Õ¥ˆ²ˆÙi,.¡ú÷5Qº~&›åÔ- G™cìD7ÞÚr$“‚b·Ð»i/¶±–>&·Ì$ i“Õ?tÀ«¬à5XYË€V؃&®¢ 0šœ`ËÄ^ -¹œ´³Ò»„›Ë¯eUÎ2¾ßü+ö÷a·ê„“IVù–áR¤´49–:½<=ð7”\Á–‘=ì?–‘Ý<:ð Má£Ä¥IŠ<V—±ª¨ŒÅù…:ÜÄÓ)Z&FñÛì9]”¸=,ðçóXK©’®ûÀ{¸â¶·áÍËEUUNìÝÏï¿úM&öFíàà¾ýhšÆº’ \ š®N%1‰Ò,2¡E–©/,feQ)] ¬.…‹WÔ¼¤¨^Ö²ðr’J5-C¸Ô^Þ6™lÒ^–çöö4]ç7ïa¸ÃÅ/ÿ=”\ÆT*Dw¨‰ø-ãÇ(°—Pí­Ã"Û°ÉþÒz/¿8üª=u||Õ—xÏÒscí­,Í[;¡<Þv?O´ÿ™}CÛ‰¥#´MÇ$™Y[t!µ¾…<Òð$š˜`QUÁÌù}³Ìa3ãqXØôÜA$§›ßc€ŽÅŒ¤ÈX8>ó#I Žâ§0TC_õ`¯¡ðT›¡òð[Œö=`¾$ˆŒ$ÇøzûOX‘³‹dAR¤ôվ岹«•{»EeTM¥2ØÆ±À Æ>¼ÿKtFº±™!×gf•»–«‹¢ eIƒ™»¿ÐábAn>‰t “$c’d<+z™°™¹ä†×sbï~ÎÓõĦÂ4lÞÆƒßÿ1ñÖ.®Ÿ¿ÕÊ¡Á>†#aC,ãim1 @ Þ½ªëÄR)NŒ0põ’ù˜éŒIá,°€öý­t‚S/«‡ž­ø $’FÛ”Å$ÏÙn’ÝÎéú g6>Ï®‚ 02âà±!>S'e®JŽŒîç¾ã¿fsϸÍ>–å¯Fd~z𛤴öb:ƒ-Ü<ÿݼeÁí”:+3àjoKrWð©Íï¦u¢‰/nø>ÅŽR¶ô>Í'îæÈÈ~âjŒ´¦ñȦfá·½n ›éEÞš¦³~I]<ðÄ6¬>—1®K°x\äT—R{õš|žœšRŠW-B×f #š]v O¢©*‘ýXt=–šYq a :ý!}-UÖ^³€Ó ¥cðM†€ïeU45“À#ãÏ2œÃ)ØqHvGZ‹D™ŒÅèM¥=Ò8C–Åm“­“\SRC¡ÃYÎêCQÂ,˘$ 93“N$Á¨@ ‚ÀD4J_0@ZÓ°:8¼þáÏè<ÚÄñ»ñD“Ü0 ':°¡¬Š<»‡É<#1I’1%YÓ†iŸ£qx¶É1B!TåsA}ÕK*SNÛtKÈß¾Öªjp«$ùd˜&ˆ†^•ÁÑèp×#»H&S¼é²zV/,sŸYÝ.“ùôäÑÓ™˜ õ”§ÉÅÞÁmlîÙÈóÝsSÝm¬)<Ÿ<{!Õžùìê»BZSq˜\ †ûH¨qÊ]5È¢Œ¦C±£Œ/oø>~ævõoâSk¾Îòüu,ò/çS›ÞÍB=?¼ü"ÉßÞóÿxÈÖÀ¿]¹zNjˆ$мé²e O±óç°¸FåRÓ±úÜÔ\¹Ž`ß»~|ÿÇ{ð×–£k•BS5$“ AHGb„·1ÕÜ=“¹Fµüÿ÷é×XÁk4$œi3¦CÀs€ƒ/éè $GÙ5yMÃÛå89V;³™JoË K¨/,¤¾°ˆ%ù…,É/¤¾ ˜ºœÓÄÌÄmQf@cŽâÎ#ìlì`ïàvžï~œýC;¹ªò^_ýfÉD‰³¯%§É¦k\V~-ªžæwSê¬Àir#".³gzŸ9¶<4]ã¾ã¿"×V€¸Ìnì£Â]Í5Õob]Ñy K:Ä`8„$ˆ9Ýl(­¢Äí!¥ª<ÑrŒ K¸xEõ,ï*2Ådž;Ôšëwšjp¯áÌ Y2éiUSÐ)Éó@¬•Õ7½gnÿ{ÿƒø½jJü3¦Ùè§ÕíÊ®Eÿxˆ¡± š®“çu!K"SIb{C›wõsMåM¬-¼p¿oú9+ò×`͸ÍÞi%×'Ú`4:Èœ÷âé÷ÿ%Ÿp|E4á³ø1I&‰ †#ƒ¤Ô$ª–æ'¿A½¿5ŸÑè ó¼ èŸêfïÀVöï`"cp|’|¿mVÑ!ŽÑ7Dœv3·\±œÜ·•®m‡˜wå:‚}Ãx+$œ…~ê^·ý¿|˜o£îõ`rXQ“)"ý£ØdDâè'HÁÑ–½æ_‹öŠ›Kø¶ÃZßü70oæb,ðú¹º¼†"»s–äKöIž}Ìf7…“ RšŠÃdF¥3.¬¦ëÓ9œ,=!ë1¤5h*ÅX4Lwp’ÖñQ:'Ç &âØ¥n•ž NBSW`‚çÚ[È+rðÉ·]D‘¶¼L*eÉtö,rUÍH‹™ÏýW‹$ ¤T;ïßJSÊÌ-ßû ¿õ’‡±aY%9;¥>Ê ¼Ø­¦Ì¸­“&Æ\À?=wˆMƒHª‹´–&¦cQÌHª¨AK‹|å¼qݼ[°H2¿mü?=øM¾pÞÿà6{ɱæQãŸiyé›êæcÏýXþ–ä®äݯg~ÎÞ\÷NlŠQi<ÆC'îáØxÅÎ2ÊœUˆ‚H¸‡Ö‰cTzæáPœ´L£Â]ÃT"HÜ<À‡ß¼åµÅhºŽ Œ‡cÚtPd‘ÝG»ØØÐÍê¾G¡ÅbÆU’O¨o˜æÇ·ìfÅ;¯CO«ôo?ÂО&ÔxræÒ†0&2ƒÊóš+øñ°fZÆÛR1ʽ­ÉÉ+Q'FG¹²¬šz²øbBà\©§ÉHÖœifM–ÌI% '$Ô4 5M4™$1 385ÅHdŠ©dIðÛìÔúó(uy¦“û‰tšÞ`€†¡~:Bã¬YZÆm¯_M¾×ù¢„¶¢€~fš$˜ÉAí(®ìçf­—®s¸e€=G» ÑÓ:ÀýŸüO®ýÂÇ8üXÛZ;ˆv‘x®‘»Ìë7,ä‚åÕ˜y:789åû÷n!1RÀgëÂBÿRî<ø ö låà ?ÏÊ‚õ|kÏçq˜\¼®úMDRaÆc1†#ýFK”h¬™]q "9L]£ÔYÁå×óDû\^~ÿ¶èýüôà7¹¸ìjò„"Žæ·GŒMqðþúOQç[‚E¶ "y¶Bú§zø¯]ŸæÈèþ}Õ¸©î6ÒZŠoïþ"¿yì *Þw §Ð3Þxö:0& -¬*dwc7ý‡O0¿ÈO*'9A2+øªK<|‚ñã]Œl!ÐÖwje£ øð ¯Á|Õ\ö/X0+DlÞŽQúý à‘h„?µ6Ñœä²ÒJr,¶Y`°Ê5¤®JV³jÖ@ËSö+#á)žjkæØè±Tjzh§˜IØÛÅÆÂÜòìr¬vlŠ Q€¤ªN&é è è NRãT”øøèë/`ÝârÌÊìêÛÌëûoñþo,[iœ™,®¡Iþç[±/\@á…KØpÈ&…¼ê Þñóo‘ŒÅH„# ·uqà¡'¹ë¾Ghîá]ׯÅf1rw;t09håçWþšEþÅlïÛÂæîÔx°x;/ô>É‘Ñýܶø#ô„:é ¶1• 2îGÕU4]CD,²õEǾ¾øböí ” ryùµÜ}ôNŽŽ¤Â䮆ïRã]È¿-|^KÎô¶òíE”º*©öÌC‘~ÀÇž»]×Ðu{ï_þIÞõÔs´öްvQš®c1ÉÓ¬ìõá°š¨-õs¨µ5±Q‘‰‡Â’ˆÕãDA¤ãá­hÉÔÌx cPDCöšþW°IÀ‚“'8?? ø"Ƥé¯ A ©iìì¥{*È•eÕ,òåN÷ú¥T•Éx¿Í> ´RšJ4™BCÇ,IØfHÄÀT"ίía<ai~¹6YA‰VÅD®ÍŽ,%õX*Åx,ÂñÑa†Â!Æ¢"jÙ$’ïw²nm9«”R[–‹Íbš9Ÿµ,ÉRÿÊY‡§ØÙð³N­÷"HÇ„FƈBDFÇiïá­?øO j«1Y-¸òóð•R0¯‚§¿ó3ìÏâ¯_ƒ ét N0ß[Ow>i ¶õ=K0 ­¥°+NòlE´L4±wp‡GöÒ>ÙLBKG±ÈV’jY˜}É«ºÊßR¾xÞw±)v\&7kŠ.dKÏSlÒU*Üó¸mɇq(NT]EGÇeòPì(CdÒšÆÒÜUTyêho$œ á³úɵæã’}L„"Æ\@ÍÐÒ2+ñdzÆÂ@žÏIj —t"‰Ù¤Ž'‰Ó·åB4…¦Îb­Ob4ùÿ˜˜y-ÿ+Ø¿,`e-ãm¥0BÄFàKÀ›0!ô†Cü±å(«òЏ¤¸¿Õ†I’ȵ9N-'£é:)Íx˧üMF"az‚“\\QÃ|>::£‘0G‡‡ieea)v“‰Þàã „C$ôV›‰‚'ë—3¿Âè]ó{ØÌÆüAM›»7Ëš‹7ªiÆMt&m©é)7gÈ}I"¨âÉŸg®Ii¾‡ÏÜz1‡›ûHD†py,xÊŠyjç1~óÞOóÖï…òåKÐuMwþt¸ákŸáÑO|‰U KY6¯UÓPD’ Óêà™Î¿pÛâñ±U_"Ç’ËD|”ÃÃ{Ø;¸ey«¹²òŠå EúyèÄ=ÄÓÑ'ñ3!¼"™¨p×^³(²¶ðþÐô ª=u¼máûp(N´LéRdŠ¥¨ºJãØAæya’ÌäXs %ÄÒQÐ!œš"ªNá²’@€Y‘q;¬Ä'¦NY;1#ê' &SŒnepçQâSF#ôÉ?˜¹>ŸÔ% ÊÚ¿<`Á¬±x°ø P& qUeû@íÁI.+©d©?³$½(Od‘d,våE ÷l¾ÐébQ^Ïu´p` €p*‰C1Qêò2qh¨UÒ)Ês±~~9å…>Ê Œ)9.»eÚË›NÚŸ;5—g¥k'%ÏfFav7/åd 3šçªî-®.dIu::†z¢êB~þÀ~ñæ÷qÁÞÁš›¯ã¢¼I’qäsðá§xlk# + 0›d©ÃÑAžìxAùÐòÏ‘cÉ%’ óӃߢ#x‚÷׊õE—à±øHª :-‚@82&úèi@ÕÒÄRQ&Cñ5 Hš5ÞäXý\Uùòl¨™::‹ŸÕÏ¡á=üpÿWùáå¿Çeò ê*’ £¡! ûw’üæñÝtM0¿¼€ K«f……‘xѤgd3Ö^tU›©Ú~| Cxï_Ê«šiç+c3BÄ0ðS`ÆÓìu(‘)îokâØä(—WoË(<ê ‘͘xcH§‰§ÓÓ öÉxŒ”ª¢é:á.³…þ|†ÃSt…&ðû휿°Šy¥¹ø\663¹^;n»QÌ*0¼x$|öÚ?ÛÕÌq[g"‹Ê@;›m¿Ô{fO‘6þ-ò»ùôm—ñÌ®ã<ùƒŸ²ó×÷Rµa ¥õ‹°{ÝÈ3M#†nzJggÿfÞúèeŒD¹¬üZrmù¨ºÊÞÁm<Úv×Õ¼… K¯¢ÔYA®­€ñØC‘~ÑD >ŽNRMI5ÉHt»É9]14tó5òl…ø­Ó 6½‚L¾­IŽ ÉFB3#˜˜ä‰¶?s|¬»¾O8áî¡@ZHç2}Ö:^ZcT³«1Š¡G’t<²•äTìT¯ê(ð_´…Ä¿*Peí`b3Øñwd^Ÿ*³jŸFéNâSLÄbqT5MZÓ^ôRumº¤- bF[ÞÌåU˜%™g;NÐ4>HU©ŸËΣ¼Ð‡E1³§…<¯³"½ä<¿ìdÌÈø³ g•´³èñ;ÛÜÕ_×­¡,à´šyÓeõ\¸¢†Çz9|xG6m"‘R±Y®^?ŸÃ­ýì8ÔÏEeWRç[Ì£m÷’kËGe‚ñI¶ö>ƒE¶±¶èB\&7ùöBD<^KvÅÁXlЉ¥£èºŽ$HØ»áí!°{` N“›Eþz슓\[>ÃÑ«ã4»qšÜÆwÈ$ñ%Ad$:Â@¸—”–äé·1Å¡8¹²üí|hùgE‘#ícb*B$žtdI¢­g’¾‰fQ"ŽÏôª¦€ßÿÃk¤qùå°s€5‡Íð¶BÀE> ÜXEA L0‘ˆ“N¥(´ØX–W„]Q%âè0‹\j’²­:J†|:b‘e.]WËÚEeH’ˆ¦éȲH¾Ï‰×i¨‰ž©[&;&ûó\€5WÒ\~™Îü4¿Ë|vª¡(Á(¢(`6ɘM2EÆe·pÕy ¸rý|É4‰Tšx"…ªé|ç÷›Xí¿œ·/þ::›ºŸÀ©¸€ñø(m“Ç)wUá6yéuMGXž¿E4‘cÍÅgñ3@Ó5b©šžÆ$™É·Ò;¢ rhx±t”EþzL’‰\[±QôŒ—%"â·æ! F*`‘¿ÞÈUJ '&Ž’PcüûÊ/Rã]€¦«|k÷çI¨q..»šP2ÀýÇC€.â‰þ÷áØmV|ÝÃ!Rimf»†¡]õ Œ–²ä9 :içë%lFnë(ð^àqàÓÀrIL&‚΄ž¦Ôá£ØåAœÁÇšœL dõ¦B²@e‘I4ÀÊi3Sèwa5Ÿ}IO™Öè› 0Ò)C®X–Μ«šË²j©”!³+žÊKû+ú“I›ˆ3K IDâÓRÄ’$b1IiA€‡67°ïX‰¤Jh*Å›W_€,Ê„“SÄÒQ<–tÂÉÁd€Jw ²¨Ðlå÷M?ãÇWÜ‹ÇìÅcöQè(¥-ÐLZK‘P¤´ñt›â@4]ã­ ÞC\eè*¹¶º‚m¨ºš¡CØp›¼ E×)wUSá®A×u¶ô>C½˜bg9³9‹øøê/óÙÍïe$:Ä¡á=<ÜùK–/,à@s?ñÄÔã‘ÉL»Õô ÿ‹¡²0˜½ÏÙI;Xg°ÞV £¡t;F¿Öí@!ü¨Ã£C4OŒQát³Ð“C®Å6›ŸÑ'ã´MŒ±£·“ÅuäzhºŽ×i¥(×"‹¤Ó'‡…žê1ee`²ºT’æ {bN©•L5Pû8X¢ 0ŒðÜÞ4´à¶[¸ùŠåTTÄTLœVYa¦éÄbibñš Š, ËiUCÕt4]%™V™Eyt[#}]×T½‹¤šàÑðÙ3¸•-½O“R“ŒD‡°Ê6jUSA×Ñt• Å—²½ï9n¹‡w.ùwì&åîjön#œšÂ,YH¤ãLÄÇD“hBGÇmöâ¼¢*ø,~¢©0i-…I2ã±xQ¤“4•lñ`(ÒÇÎþM\Pr%fÉŒU¶¡éPáªÁ$›˜ê橎‡qº$‘4Hæ¨Fÿßw1è5¯É>À—ÃÎÖYÚ àê¾ < |¸žŒÖVRS9§7¢ÊåažË‹ éã1F3È#Ñ0º¬³ji)ÖW!ŠŠl »TdqzDvrÍ©€5³Åfz´üKä­då¥õÕgZÖÛÉV!;'øÉŸ·£)n®¼ìZvìÞÏÏæŽ»d†´ÍÙ'äE Ì&›UÆï³a·(Dã)Fâɲ$q¤µŸ¶Ž(©ÿKòV°g`‰tœæñ#Ôç­%¥%q˜ÜÝø,² Ågx^©ª®â2{¸}éÇùíÑ;¹¶úfr¬yTyjIªqÆ¢Ãø,~©©é¾E8Io˜é-:Mn’j‚´žÆ*Øð˜}'ß7c½¶ô>M4¦>o5‚ â09A0h š¦2`LDA8F–g,#üû>†÷y­³Û9Àú+-&fó ·c´õ| ¸PŒÙˆi'Çh N’H$ˆÄ¢øÜ ó],*( ²(‡·}º§Ìb’1ÉÒt[– ¯l®„¸(QÏ6É}êTœ9ß“ñþ†'¦8Þ9LKÏ(£“StL0•€Ÿ|ç ¼é†kùÀ'>C[ãî3Ræ4v™š2of„—Qõ4+2›™dJEÕ4îyr˜•¹°À¿Œ¶Éfþpì¬+¾˜O®þOû—£¡Ñ>ÙÌ×vÞÁ·÷ü?\&7js“Ò’ÄÒ16”\Fµ§—Ù‹ T¸j°)ºCíÔúJ¨tÏCÉxW±t“d™1! ,²UWQ5‹lî8fƒ¡D¿´ÞËÒ¼ÕømùX$+“ 86~˜0!·#YD`ÖIÐ1ÚÂ~‘XÉ^[çì¥í`ý 6ÃÛŠcxZ[7a´÷Ô Ùž„¦¢+2.³›š²Ö,(¢²Ð‹I1n IQd‡Õ}@&ÉÌÒÜUïó/ÚRórÙ9Àzí”Äüw0ªŠoÆ Ÿ.$Aˆ&ÒìoàHû0eùnÖ,(fEm!>ûô|»ë´ô3뮟äeé ƃYQUuDúG‚<´ù)ÁBII×\v }C,š_Ëù²,sððÞöæA×1) ïyÇÛxü©§8Ö1Äú%•¨ú î~Rq"KòìÞ<³ûq5Nn®îŽ'ÐR‹ª   Msaé›9ÑÙÈ/D±ïÇf“‰Ä£DSaT͘¯(‹J¦ ÉhQµ4á> %(’‚Y¶à2y¸aÞ[ÙÖ÷,ÛzŸ¥Â]3 F#‘A\f7ÕÞ: ì%4Œìeqî ¢©È´¤œk:ŒE‡q˜œ¸L.L’ñ@É‚ÐX|”Ǻï%.GÙ>þ, 5žÑLfzjÀC3ø‘ùGÛ9ÀúØ àêÅ~q/F5ñÀJÌ‚(V5Úú&h˜ä™}í,©Êcõü"jJrpZ›(ëueé ÙŸ_ °I‘ÀÁp5Ã,EÑÉ0n>‚Éæ!Çáà]ÿv ݽýT”•pÇG>€Õbapx˜ÐÔóª*Ð4C2¥¶ºŠºyu4u ²~I%š®3Ž£é:fEÆl’èŸâ®‡w3ã¶[ky㵕kž`Å2?;ö ñÕo¤«gŠw-¹‰­ü4Šh¦'ÔΡÝk`Tz€¾©.Šœå¨ºJ4AG#­ ¢ b“íLÄ oF@@Cc±WVÜÀíf~ÎbŠe˜$?9øß,É]Éå×±<[zŸ"”`‘­¤´$fÉò¢õSõ4#ÑA<æÜf&Q!ªÆè5³uð¶ >MG¨³ÙDJKÎô¦4 #ôû#pŒsÕËjçëh3€k£"ô'àr ® w6Ù>Œ²ù`;û(Ëw±|^!K«ó)Éub6ÉȲá1¡ÏVC˜ËD &#a&§"@6LƒX"ÅÆÇ0Û½x=nÞsëÛÈÏó³yûN~úo`µX€á‘1Ì&^gºôoµX¨©ª¤ùðVS1~ÿÔÚúÑ5=Sé³021Eu•ËsA€ÇŸîAÕtÞrË<7Ž£¦aAE/ô>I}þ6_Jµw>µ¾…¨Z𱨷þ»â Ÿ`82@g~¦$AbiÞ*~uäLÄFɳ¢ë:““kkn¦=p‚»¾O­o1« Ö³$w%¶ÝÇ…¥W²ªpOu>Ìññ|Ö\âéÉú¢ê_JM2¥ÐQJ05É#]»Ø2øGÆ÷1™tc¸îIKb‹ïÃðª:ýP½üöšBñJ°^ãOÝG0*‹q p †¡i:ãÁǺFÙß<@Kï8áx ‹"ã°)XÌ¢ÑýûÓÉP”X"5¢ˆ¢@K÷(ûO QUQÎùëÖðÁ÷¼“oýð'ÜxÝëX»jš¦!Š"ÇŽÓÙÓõW]1ãó"[w«§ÝŸ6à}·×ò–›+YVï!7_bÆ<¾òùUÔÍ÷NË;¬[ϼ*wþ¢‘¯~óW¬ZÈ»®]CSoÏÛ†ÛâÅoÍÃarq`h8ö &WW½‰”–$šŠ°,o5VÙàB!€I2³±ý¢éËòW“cÍCet ÏVÈÞÁmœoäâ²k(wWs`x‹üõx,>NŒ¥#ØÂŠ‚õØîÌ ‰lÞIC£/ÒÉ#d8ÙÏ Ãy²÷A:B'HhqDAœ©Õž¾| cÈÉäðð0‘HäŸ}Ù½&í`ýZ¸"‘HÚáptcè=ŽñD¶cLô1 ™é8‰”Êàx˜#íÃì?1@kßS±$²,b5)(Ê‹«[`xS&Y&‘J“VÕL/¡@<‘¢©c€þ¡1>ñÁ÷2²iëv>úÞwc·Ù%‰ÃGãÊK/žÕ†ó§G#fò*· ÐÜ>ÁÈXŒXLet<Á»ß>Ÿ<¿…h8E àØ±Iþø@_þïlÝ2Æ-—­àú ávX¨¯-¡c´—gš¶SîªÆ®8ùÆîÏbUì|dÅ繺òH¢Ä³]²ºð|lŠŸÕ ˆH‚L,åñ¶ûñ[ó¨Ï_ƒ,È(’BJK’o/âñ¶?a•mœ_rç_Bž½´–&¡%x¾û1ª=ó)´“cÍEÓU£ýìyûÚïâw-w2”ì#¢…'ƒÆºb–žZ€»1䉌‘Lžª¼¬’e€Kw8ÀnŒPb;¼€Ë ž€K¤è›âhû0ûp¬{Œ±`4’I˜Ù˜{˜!=(²„ÛnÁjQE‘´ªá°™)Éus¼sP8ÆÑãÍØ¬Vnyãõ³<©ÃG™ ¹ôÂó§iÁPˆüü.æÚ9¿¾ŠúÚb\fx¢»M"Mó_ÝÇÝ÷¶òë{NðûûÚinŒ²¼º‚Û¯_ËÊù%H¢1ŠKêkKØßv‚‘ Cô±öûxË‚Û)qU’cõS`/æ‘Ö?â6{)sUâ2{1KÒz‹le*bcǃ,Ï[K©«Òð²tCOÓUžèø3”\N¡£AÈŽ8>~„ÞP~G> {ùSǯùmËx¼û~š&Lf&Ñu` xC“ê«•àÞááátöAtÎþñv.‡õO¶yŽ`~~þ³Àó@ p1ppP ‚(eœ©`$A }˜£ÃXÍ y;UEjKs¨(ðë±a5+˜‰ņ×a%šHÑ7¤¼ÐÇÕëêØ¶g;ã¡(Ë–,e*!ÇçEÓ4ÅLõì$áK”$^ر›ÞîNÞ~É%èºN¡ßEŽËFCk?«Vø¹ñúJ~ô³c¬©­¢¢È‡Õ¬P㤠lj-S@Сñw?±® Ê‹ïz#ìë9LQ…†&£rmÔå,Æevã2{È·ñЉ{D‘2W.³«l#¦FI¨qÊ\•ll§ÉŪ‚µH¢„U±NOÑîäÀØÆRÃÜ×ö+þØö¿lzБĢ(Ô’Óâ}3Bë Æ@Òßišö A¾ l†‡‡‡ÕsÞÔ?×þåæ¾Ú,#o`ª€ ÀeÀ 4óûi3XîFÝKlŸËJQŽ“’\‡EPd³"ÓÒ3ÂÓ»›±:¼äø|  aS„ã)ªkPQVÊ®}ûð˜Ò|ü­‘ëuL‡‰›´ñó‡·aµJûü¼óÚÕ˜d‰h~„„çÞãwÑ3ÕÎÇ×|™¡XmÁfZƒMô„;ˆ¤¦ÐÈjZ s ·EF#Àfà ibbb*•JÉ@êŸ} œ³“v°^E6¼d XŽÑt½¨ÃÈ}½¨Ë6KDP$ ‹YÆf’qÙM†P :ãÁ)̲IJyE¬YTF`*ÊÖCí§bÔ–çqñŠwçãÔ[¯ç- ßM­on³›`"È›ÞÉ`¤—w,ù0~{átˆ#cûé wÐéf8Ú"¼(MW3ù'ñÔ>¾¬E€. )—­À UÏÌ ÷2Jcçì•bçëUl'†÷µX‡d•€‡S ²->'ÙG¢`HÜXÍ2« ‡Õ„×iÁë´â¶›ñ8­8¬ 6³ñ2›dB‘ãÁ(²d µšr½Žé¡ K’Q0ÉH¢AÇØ´¿•_þ¹;Vý7×TÝH4á#Ͼ wé0·^½šG¶6Òp$Êõµ·Š ¨Œ'F81y”ÖÀ1œf7i-EJKf†Cè/LÆ5d[ú0¦"íöb䣯0$^Îå¤^%v°^C–0pÀb`°¨Æà|Y9Íy7œ#ÃCÊ‚™ €&‰"²,b’EL²„"K躎,‰È’ˆÃf$ŠH’€$ˆ3ûžQ5 MӉĒníÇ«Pì*GÓUúÂÝc¸ìVRi•x2m4Hëz&œË&ųÂy/aIŒy}=¹¿ƒùÀVŒÐ/ çêÕjçë5l3BH+XUb4c/j²Ìï]ÀY (ÏôÎfÅKgž8™ÏÄW}z*M–ãt6[g˜ŠÚýR-Ç1”Ú1zù¦8§Šðš²s€õ/f3@ÌŒ2æc$ï+2¯2Œü˜?ów{æ½2s„—ÿ Ó1)‘W aÓЋ1›¯ËÌü-ʹv˜×¼¬sÌ2 °a„•> Ô²¯ÜÌï½™÷80<8+FÅRμ$f__jæ•ʼâ`Á¤F(7†!É22ãßñÌßÃ!ß¹î_Ôþ?`nz®»X†%tEXtdate:create2019-03-21T16:33:41-07:00+œ§³%tEXtdate:modify2019-03-21T16:21:05-07:00ï¿:IEND®B`‚kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/images/kubectl-logo-small.png000066400000000000000000001731421476411216400311210ustar00rootroot00000000000000‰PNG  IHDRÈÃÇŸ^]gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿÿÿ ½§“ pHYs.#.#x¥?vtIMEã 4m7å€IDATxÚìuœ$Çy÷¿U ó̻Ç|§CI'81Y`É(™9†Ø1$2Æ!;¶ã$ÆÄl¿&™I¶e[œ˜á@nj˼ÃÝ]õþÑ=³3»³w'0%©Ïgvfg«ëWýž§ÿמqkii™é'ˆ @ ÐQöjš€z lB€ H@P€€,Æ€a èŽ—½ú‚ßÒÁ¾ÓZ___é³Öºâ7!ÄŸºKÿìÚÿõÈÓh3ÂÂB°X¼ÏÆGè¨ÖØh-AãËâàô?6«=Žâö•û”¶B ADÁÍa`°Ø ~s‹G* 7KñÁupþ$•ÍüS_ÀŸs›a|I° X ¬mhÔZKfÖ!‘¦‰i‡°Â1ìhœP›XC ¡XÃ!ƒÁÖhtðèç`’Âx¾¤ h¶(ÏÃ-äɧÆH ö1rüC‡v3xh£Ç‘ÀÍç|ûC¤ãápäˆëºw¹®û{à  ·üTÿ›Áò¿ U¤E3¾„¸¸ô\­´¥µÆ´CÄšiœ»˜¶%«iY¸‚ÚŽ¹Djê1íBHÐi5ÍøýS4!‰­n>Gzd‘cèÛ³•Þ][:¼—ôÈžS@‰2ìî~ <†oô—Úÿ6°ü¯È`„ñuñk«€¥Z©Fc‡£ÔvÌ¡}Ù::O;“¦ùˈ74cÚa_0¨?0œjóA ÐÜ|–‰ú÷nãèÖGèݵ‰±Þ£8¹œo¿H™ž~o›ìÀ÷¨ÿ{€ò¿ S€Ñ„¯>ÝlÐZ5h¥±Baj;çÒµê,f¯Ý@Ó¼eDjê†V¾p¢VrP‰?ÿÎÒ—0ÊuÉŒîÝÍÁÇï㑇`¸ûˆ/Yümzñ¥Ê€{€Ñâ1þ§åÏý>'m 0æ/ÂÆ*­ºŸldÿƒw0|d/žã ¤TÀvà{Àûþ®÷`˧Zÿyå$@¦c ðZàeZë9Z)‰:W®gÉEϧó´õ„“u€F+õŒÎWÞÖÚtÆMާ\öŽž› BÑcU[r<…ÃI‰r] ¹†i‚Œôvs|ïVœw!¦e•¶ÈŽqàÑ»yêÖŸ0°o;Z9Å3îÑÂø©ÐÞ×€CÅsüOÊÿ(€LÆ|`¼Vk=­ˆÔ40÷Ì YzÉ h]¼ +yÎ m)À–‚‚ÒÏ™ô(d³ ?ŠaZ4Ïš]´ÅV6‚Œ‹w’s !è;tÇný GŠFÁÐñct,\Äuïü»ÀEí7­5¶lbËÝò ¦è ¨f“I)K^/­'%ðÛ¤GÙµñ—lýÍë9DôÅfà“øjWþr€ò)Rc%ðaà:­<;œ¨cé¥/`Õ5¯¢¦m–ÐûF¹55ËêCì)rÔ¼ÃEàª-~î9°ŸÝOMs +/¸¸ä*þœÐI ðSöðyaCGö²éßbÏ}¿Ãɦ‹‘ùïãeüaAò\åºüŤ a|©ñ!­õ|!íËOçŒÞBת³†ùœ¸kŸ«fˆ óé€U·PÀu „£±‰qlp€CÛž¢¦±‘®¥ËK¸Öšôè(ÒD“5¥Tdþi÷©‘º÷íaÞêµà*¶€40­ )ñ ü’ ?å­µ ,À§ÇB<#Uâ/ eà˜ ü=ð*­¼H¤¦ž•W¿‚•×¼’X]ã³V§¦%&U¾•þ) =5ÊñLA ¦þ+8¾wv$BSל\žc¥5ìyìa†{{XyþEÄëê+ö+—BÒc£lÞx;V(Ìê‹/#—N±åî;é;x „c±’K8ÙØÄ ßu£/)k¦DLAÞÓf0°„4ï;Æ“?ÿ&;ïü9…L!å8ðà?ñ3#Ÿsh­×oÀÈW„»žÉqþì½Xeà¸ø8Z¯×@ËâÕ¬Åßеúl„8gúΚ½\Où| þ׿•‹ãŠä?ÀlWoŠH˜ºÝ)7QñVúR O™d3…êÇ­Æõ’6†¡PÐd3…’=1päé±Qšgͦ¦¹%¸?‰Š`G"8…<·ûìzä¡ÉûÑÊï3­hèèÄ(Kä*¶9I“yI‹¡¬Ç“ý9O£ƒ~ñ÷Õ(å`ÆšX÷ò÷Ð0›~þFŽìM )oVÿ¸ƒ³À~ºrî™äÏ STª7ÖZµ™v˜…\ǪëÞH¼±\Æe¦‘y2D+…ç:M=ŽÃ´¦Í–›‚5­Q )ØÜ›!UxlŠjR MC{J®Ú~SéX¸ŒöK†‰çújf!›åÀÖ-¤FGȦ³,Š×™º6 Ï8iä2y†{û|š‰”¦I][sV¬f¤·å) 7 ôFºÖäB‚|Ø ›óÈfÜ@Šä3iùzËÜA_Jæžuõ³±ég_æà#· å¹— !æãÛ’?iiiy.U®ÝÀgðÕ¸‘gz?K« Mø÷f­¼p¼±Õ/z+ 6\a…Nʬ=Ys ”çNû^¦šö}Ø”¬kbJx¢;ÃÄsgÓNÁ³¥•¢ÿÈAÒc#4vÎ&ÙÐTe#èÞ·‹ã{wbZ6ÉÆfZç. Qß@zl”c»·3Í™éÀàÛWaKRp5Žšt Ú¶™ÃÛ6ÓØ9‹Åë7`˜eŽ)q²ivÝñ¶þú›äÆGŠ*×§ƒ×<{ü5ÒËÀ±_G½Fk%Z®æŒW¼‡–Åk(æ|?Ûæ:”[ !$†eM Åf~—9Ïa´ü™4­N>²‘Ò8á¶*£žÉVÅòÁvª”ux"N3Ò… 5<Èøà ]„¢q¦=«à˜Ç¶<Àã?ø,ÃGö"¤tñ½\"` ÿ9¸‚gˆç®•ãlàëh}‘RÌ;ç*ÎyÃßS?{ñ³–å­R"¥aš>8¦pʛғöÇŸªˆ‚‰‘!v=|/V(L¼¶îÙ´h‹¯™¶ñ<í©É¾«²Y(£¦±Ó²9ÑDVÓ>‡Ö%ëH v3Ö{D X…«ð KôÇãqÒéôŸ¤‹íÏ eà¸øªÖj…аâš×rúËÞI¤¶á9wß !Òði†Q@;AS®ƒòÔŒRæÑ¤”Øá(Ɇ&LkRüC‚V£ñ¥<¿¿D•‰D<Ç!=:Œ[(`Ú!¿ŸŠÜ1)†Ä´üWMs3sÖ‹WÈ2p`J©yBˆsðéôGþÔ ù³H ¼ø/­Ô¬p²ŽÓox'§]ýjŸ*r A?ÿýáUX§¦“5?*핎{ªÍ0Mâu ˜öä,í9ûž|„dCÓ4'ÃÓº¿@¢–ª®ˆ¢òH[ì:9!ȥƹÿ'ßåá_ÿ”=>ÀÀÑC„c1Úš‰×F°C‚HÌ"±°Ã&–-‰Ö$˜½ö, ˦w×V<§Ð*„¸? ¸÷O ’?9@p˜À_ÿ©•jŠ7¶qÖë>À‚ó®Aƒ©ƒÞœ²XhÀOîq ŒôõŽÅ‘2¨ %…Ÿü#&ÿeÿOVyz×\ÚÿY6åyx…Zye%€žN›Tõ¤aÒ½o÷üèÛ4v΢®¥íiÑø}PøIR£=‡èÞþGŸz˜¡#{psìp 3=áÄ ¥¤÷à~øù÷ÉNŒ“›˜ ÿðAmÛÄhïqúàÀæMt,\ˆ—݆Æ0-:–¯%ZÛ@ÏŽÍ8ÙL½â"à(°óO’?©‘€ÃþøˆV*YÓ>‡³_ÿ!:Vœ5ÍÞBàäsÛ½ƒáîc áX Ûµ;lqõ[ߎaÊn°„„Iÿè¤Ë2x÷4ž§ñ\…ç*Ô3  Zá¹Þ gr¥<´Òºò ½,R’çðö-<ñû_1pä ³–­äÊ7¿‹HÙ‰qîÿÙ÷8¸õI ÙÌäs+¸ðe/çâW¾r¦»`÷=¿cã>Fj°!e?>Cûû€úcî2€”ãÀG´òâõ³sÎ?LËâÕ U03 Sb˜í¹ÜyÓ·Ùr׸A‡yBžë²þšk¸öíïxÖ PZƒòù¬K.ã>ÍãùjÆñ}»ÈŒ2ç´5$šNé†) …M K¢•&Ÿsqò'fÞ¶™Ç~w3ý‡ày.u-m„¢1Vœw Ëν°úyÑÉeèݽ™÷ÿžý;÷2*ZÑ §AÍ,°bPTY½Œ‡¾Çh=¬X¿ž¥^KmÛœRu—ÉCû`î9Fv|;äSꟺï^nÝÊ«ÿù_h7o¸l)0ä•ïÙ¾ñvîûê¿2Ñ!åð·ÀMü‘Aò' –©Uo'GÃÜeœÿ–¢uÉJ LS"  EOÓhß(ûž|ŒB6[RG¢‰­sç2:0ðœe áÖhÂFHAf¢ð´öí;¼Ÿã»·c…"¸ÎÉ÷•R`GLÂQØT³ì°Az¼@>;=V#¤äÐÖ'¹ã;_!=æÇÁÚ,á²×½p®•—h^¸‚Ëßó1f¯ZC(,±lÔ%[¢øÐ¬P¨¤o·Î›Ë’õë¹à†—qî _Ȭ¥K)äòt-^üœzrLSâ¼SW·„À0L¬PˆÎ%+¨mn;áæ–mKÚ„£RN!œ4%NΛŠP®ËC7ÿ¾ƒûBMÖrÞK^EۂŦ… W\“4 ò©qö<ø{îþîxôÑ „OÇu9Ô/3Œ¯{ÎtŸÁ÷¡4.#žËáÝ»8pßÏÉ&ÑÐL$Y_ÊRDk¤!0-_ÂG º/®šÓbIAgÔ lHÆÅ@Ê%ŸqH¶tQßµ€¾=›ÉOŒF…€ƒÀŽ?–MòGU±pàUÀç´òêšæ/åÊ÷ýmKVž2ÙÐs]„”†‰ ‚Éw+ÚÏùug& dÓΩïp’" ÅŠ˜Dö4`”7¥4ãùu¤Ø´RÜõ½o°ó¡{¨oëä¬k_ÊÜUë¦\†/y³ã#|ì.6ßú#Žm7ÚÍ+¡f6Ô-„hsÉnz:÷ÒýpüAê¼C,?}-Ë/ºŽº®§µ! S–®g¦þˆ™KÂhÖcl4‡ëù&RÒýÔÃÜ÷µ!å«[Çñ'×ßÁ>˜øGHYœãùÀ×´R-õ³æsÕûÿöåëžVÆŸ ò§ÿX-—vH? 5ëTše$jCyâG๊ñáÜt &é‘aŽ¢¡£«ÂÎ)#32ÀþGïdë½9žªÅ­_ ¡€ÚžEï†Ñ}k†Ž³!9Û·;žP û!jý,[³Šå_GÃìÅØa‹pÔÀ´¦{¿Š¥|Ljë( 9åM1þ¥dð©û¹ïka¨¿)å~ü‚À$€”ã\à;Z©yÉÖ®|ï¿1{ݹÕÁèËå5›ÀO?í?|˜¦Y³ªæ'ü!Z¹Qž‹çz!0Ló¸fýMØDb'¿þI/ßÝ,K´!$H t³÷¡ÛxêÁè-´à;Ѹ̷%€’'O{î#ÑÝúêSÇ9P;ßßö™%3Ý’Ìíañi‹Yqѵ´,< ;F°L|Ö¯REƯž™”´Õm1z¿/|üÃŒ#„ÜŒ¯‰l‡?Hþ˜Y |_kµ6ZSÏåû¯,:ÿŠêÑq!pr9öoÞLÏþ}h¥ ÇãHCr|Ïò¹/ù»K¾ôò¸D)?[ò™ †i>«ˆ·Všñ‘I‘ï¹.Êõ¬¬Âú=ÕÛDâ3Äuù¬C>ëž”•\ÔûG{³û¾ß²ý±Ç`.jöóõ ¡ï H÷ÂÜ+AS~0°³ƒpìô±{À°|‰R¿ û™%7=OKmgÁÂN–_x íËÖa…cÁ3zΔyu!7†¹õçß统ý8…\!Äø’äø_,@p4ßÔZ_e…#\ô¶²êù/Ÿ‘§<;oº‰Gnù5N>_ú^k¿¼ÿò ¸áý(Ų ÷öâ:¡HÃ4îíe×#sÁõ7PÛÜüŒ½[Ù”C&U©^•«2Ï´™–$^Æ0*‰„>0\ y盧ªZy ÝËŽ»oaçæí ÛËг/GÔÌñ«ràØý¾á¤ Ýs®˜táV–…î‡ÐGî\h[MËÁŒ@9Õ~ ŽW˜€¾-„F63»#ÁŠó/göšóˆÔÔŸR­ãÒÑD-?fóðO¿Æ¦Ÿ­\@|?T0ñ‡ÉÔÍ€#üp¥4 ÎxéXyÕKOЂ|6ËþMO’ÏdüÙ_bÉ$‹‘-=#!%GvîàŽï|‡¾C‡ÐJ•ȆÙTŠú¶6 ÓÆót%Ë“”‚?åc½¨ç3¹ŒSõŸmsEj4G(l"¤(éà®srWµÊsØ»…mwýŠ]Û0[ KÞ‡H´#üDJ .ø;¦»!Tܤš¼Yÿƒ¯rYq˜}¢ý\è{ߎ>þ ´®…æÕ`'N(ÅãÅ kù¶uìÚÍÞBÛïnfÙ9ç³`ý¥$›;§ÅRªMCªà!¬¸ê5¤‡ûÙ}çOAð `?ðñ––÷¹ÉÌÍ[æ±zð>­•µ`ÃÕœûº¿õ+ù`˜–E8ŰlÚÌgéÙgsþõ×sÖµ×1wåjré,­sà<îøö·Øýè#(×Åu¿|À% GãÌ_w.Ê“äu%Ÿu)d=ò9—BÖõßs…¼‡“÷ßóY—\Ú9iîÙ6¥4NÁ?§ë(”w QoÏ¥w÷&þñW¸÷·wpÀYB~ákç ìøäà­øÊY1ÕOQ™&¥ˆ„VþKšˆäDÛÙˆp#ô= Çî7 ‘0#OãN5Hâm¨¦•ŒëF<µ™ƒýšÌÀ!¢5uDkêÒœ6.”ç–òë‹Ó’aÙ4Ï?‘cûë9,…ë€À¶çÚýûS±€\ |O+¯¹uÉ:.xÇ'¨mí –´J®¿]šë¸(O£uq†õãnÁA9OüþWl½ë6Âñ —°ðŒ³ñ‡m÷mäüë_C´¦ö9«pâ¯KSÌ™à¤jÐsѤ4p½»Ÿdë¿dïþÒ çA׈pÝ$J­\u*~¥§üæ¿MÙnò @`x'úðíèÔQhZmgú`A?½þ-²€3CÐ÷$Éü^/Çò‹®¡eÁi¦*9n¦—…È¥&ð\—üø ÷|჌݇ð=[×O>—RäsŸh­ÖÅÛ¹èo>Ió¢Õhåù”Šˆ‰iú^¥|”R:°3üÏÊ+Ölšù\žë’­ E£„cq4@4Qƒñ,½]B€iX!ß]) À÷é;\ÚÅóž{·³Œ<=;Ÿ`Ë7³ïÀ™æ ¡ór;z|?4,‚ö³|çé¥d§¤  ±ñ-,\ÐÆÊK¯£mÉÚ)@ñ›òt/Ã,ßp cGwqï—>Lnb!ÄíøŒðç $ϹŠ€# ü;Z_e†"œùÊ¿eÖº Jz¦T‹|Þ¥¨8NÁ+éà^ ,'?Ÿ”’H,N8–À*¦É;†£±’‡Gi…r½RÚSµ#¤!ˆ%BD–á7‚¿H)0-Ó6p Þs%¤ÒÀsŽm{„ûðEî¿óŽÛgã,~5¢iBZÓÁQvߥï*T'E‰qBPÌô›ž|7"š×!jÁÈA8|'dú \vž–¦}OYÍlœºôöM°÷Þ›Ù÷Ñd’xC †aU8F Ó$VSGMS3uóMïÎÇAë¹ÁEßÇÕs¡j=§)‹w¼x/`.½ìzV\õêêÉ5Ïi›ytºNÏq|®RÄ)»~ÃQ‹HÌ:! ¤!|ÕÏ©.E„+$1 qB•LžSàØSsß÷¿Èåxè\ÜůB4.÷uô’‹ôÄÛEy­ÞS‘0pb)20á:DÓDÝR? ‡ï€T7„ëü˜Ê©äç”Æ…ò픚Ù8uËéë €ò8‘xÜŠe¡5ÄjjH66! Ù††Ù‹ï;ÊÈ‘½B±Ø ìú³H<ÝðÿÖÊkh]²–õ¯yáx1½ŠÔ­)Ï+åW é¯]~ª€ E,LëäÛz®Â)L7ê…„PÔÀILË'_zSVž*ãèÖ‡¸ÿû_ä»§;¼!ƲÀxõøƒ‚âD6KÅÿeŸ‡€°“ˆÆUȺ刉n(Ç| ØÉ™âå¡o³ï3ìÉóÔÎ €2ÎÞû~ÉÐîGEÂÄZ1,»ÂëeØ!ê:лóq²cC!!ÄRà¶x<>òlAòœ$IàsZëõ‘šÎ~ý‡˜»h)§5‡˜S¢&d0šó&‹8ÿ‘rº…40 Ó=Íè·;džøR5ä2î4¾”>8Lkrg!®ã«E`Ùò÷}ÿK@]×BÐ GiúÓ.=CY VSG}k;õ­þòÊ@MØàœ® Q“ž”ó'_ü²š°)²N=O¡”B 1Lß/Ú~Þ{ù|¯T>“eßÃ÷r×7ÿ›ïÝDOô‚‹|{hŠWj(NÑc5¹ÉÀ‰êÛÄÍOñÓï~‘óÖ-â·?ÿ.éäZß+VUuzšç`ÊïEÕËŒ!–#ê–åN?vB5>?ÌÍAïo÷ÿï~ÈÿÍ—#zJ’³ÉGçrøÁ›éÝý$OtÆ+Š6ÂØûZµâWV¼+ëg’gE5i8ý8~(ï*àÅXtÑ‹h]Z¾^|ØÑšZb5µ“” †ðg\CžºŠ¥µ_†æiÛºTŒ¹LmvØðº46ƒH¤8µEÐ¥aPÈd9øÄC<þ›_°ïH†|Û¥°f=Šù ð&K§ŠgåY:Á¾Áorh+/zÅ¥4·´‘HÖ°¤#J_fY3»Š©rÜfÚ5LÙF㻇ç¿Ñ~>º÷aô®_¢#IŸY·<”ëƒÀˆ€›»†É hñx ã0¼ 7—aß#÷!g³6€VjQ¨]Jhh‹@ˆ7·÷ò)kÏ :Ö Z77j¥b s—²ô²ë˜‘„fJ¦²ø³0’syèH GÍ\)|úød7ÍÉ—€‡"&vØDëò.Ù”SÊ·,[Réäzzv‹Ïq8ðØ<ú«Ÿ²ïPš\ë%°ö,„õí å³téÏ­+vÊvZc«1fÏžô…$l[v&U¢“ÅG¦]Ó”k˜é:Ë÷Ó@¨1çDÛyèÇÑûnG‡î‡Ù•ð)-sŸV´Ò{‡ðÉý› ç1„ #Ú/i¡þšWø\3i’m^•:Œ,Œ6‚¸ØÔÒÒ’zºÄgcp›Dë×gja"çœO¨¦¹*84³$«[#¸ 6÷f*€ 5 ç¼b7œRRúÁÁS0¼­A4nOsÙ†£~Œ#5ž/Iq×QX¡g·‘ ;Æ?ø[6&ÛvyŒ(¡Üg®º<+À(L<ÂAŠ@¡P`<“#Ä4ªÊ‰‚†SÏSqÝ'Ú®ò„ŒùŽ‹¡é tÿ#¨]¿D'Û!Ùé³K×$ ?}› ÷ „ !ZÎx—?£)¬:ôÈDÛZÐ/ÜL®qÑîà¯]y-~e”§Õž1@ZZZ`pÛ<à¯ÐÊpkr`¢ƒºY¼¸szœ†ˆ)iŠY¸J2%y¯rI²òÏÊsý@ÐÉÚ)€#1‰ª5Ë6Røê–†BÎæ[v1j>ó±‹|,åR‚Þ=[¹õ«Ÿæˆ»±úƒˆP¿T²Wd?÷ªSÕ}5“^.¤ 3È_™cp¬€hŽNÚ 3wÊÿ×1ÓµN½¦ÀЯø>°)Ì¢ãbŒ†U¨#·¢ºƒH“Éúù,}›FÑr6$ºðùahïBíÀN6ℚQF˜|ýJì±Ý˜©£a„|pGKKKÿÓ‘"ÏF‚HàM h3B®ù òžÁ#즵µžd2ZAÛFó[z3xÒ…êëõ9ù<Ýûv1>ØÏÜ•kIÔ7>«J%Bø9YjëÔgê4®ãù…ç»CHJ¥µÐ ´F+|` Éh÷!~û¥ÿàXää¢Ëƒãiâ™F̘àUìËÁÁAF²a„€”wÊ5T;WÅÇ™Á#N÷tÆÏ3Ò#ÏÕràUh- 5Kpb³BÓÓ3̃nÖ“(&^øñOiŽ82Z¨ê¥B01<Àñ=;p …gœÎ:å±ú’úøÈçÜêTsx®SPrŠB6xÏ+Ü‚Æs}Þ+äyèÇ_á˜X‹œs¹R¥&g碦â}¦ïÊ~£Êweÿ ­ŠºF0óq…W™d³Y†‡‡Éx!ß#Tε*Ù#Tù®ÌN™ö]Ùõ¼ªî{¢ï‚km>9ûEˆž'}Ï×Ük!Öåo£Ü _ƒó)¿Ÿ…•@‰9"¸V EéT¨Yˆ“˜ Zøqº®)k]>÷Áw¿t‡²ä×úícmÛóàƒ;p¿úH>b´¿×_ø±:­5Ɇf–žs!Ë6\D¬¦îÙ×¹ÒMp‚ ½ò± ÏÈð0ýý}ìܵ‹‡Ýă›~ž}&éÈ2dà d¤¸¤tÀO½Î©×Wv¯Óû€€¿/í:D¨îAÔ$¦IQòÀvˆ™€T$g!´G(Ûƒô²¸V’|ýr¬‰ýå.Ç_¨éë'{ô§ @z´×£µ($æáFZOŽb\×cÛ¶CtwqÆ‹Y¶l–šÁxŽËh_¹Ô(¹ô†1ÿ„¶‹‰Y8ùIÕIGñð±4®:µhû‰ï£lQr|õj´û }9ÄòÙ“Òã¤ö„ÿÙËR‘ú ¢áT€œHŠÀ=„Ñ{/K}\uÉi\}åÛY¹j5 §tOñx‚xðê3øäG?Ì%—^FMMí)_÷©6!õ lذ«/=—Xn{û5ã9mÌߨ6û#P™>BG~Ìk/Hð_ÿþ÷¼ý­odùün¾ùfœšUþd4¥oEšý¤§L‰|miSé/‰òy€ãûu³¡Z(3†2BþÍöø¤“¶ÂnI§Ó3ªA§@½:xB„rÍgãÆgqbéT¬@áe‡QÛ1ÜEdd7Ñ¡0¼Ñ‘!õd1CQZ[ë¦ÛÚçPÉSŒ‚»®"—v¦MD!SЙ´± A¶JÞx$QC}k»¿*kÔ”†$5}®–Ó *#…LŠÇ~{3ã5vÂ÷:Q%¡¨ôæÖNšÆÜfþêu/¥­­ƒõëÏ¢«Áäî_›Lx>ÂNNQ]5 QÙ!äþrõü~¾øÏõ×ß@"‘¬Ú…BžîãÇÙºe3wmÜÈÖ§žbáÂ…XS$ñæÍ›ùêW¿J÷ñc†$OT­y\[WÇE]ÄÙ«çq|˯8°o7Äç ŒÓì!ð†wÑ2ü >öîkùû¿ÿ 혦ÅÖ­[øéíÛñêV—TÍÉ$°²þ*õ¡öɈ#OöûFMñ–‚¯’e»á("Þ6mŒj#„tSXé#€hÆÏ˜IŠTµA~ª'½=aB\#„hiiyè„g×SHÌCëéô™Ä=€5zˆ¥jˆ³«çÕ1+'f™Œ:yîéÛÎ϶nâîþÝr׳þìåe`PJ“K;Ä’¡™Ý¹Á$æ:ŠôD¡ê:‹Â,l‘)(<šf,ïÓ\Š%e Ë »×dFGî9NSWuM] |€ÖPÈM.d#„ 3:ÀxÚE¶×M­Šùƹ ׳7Ýɧ?ó9>ú‘‰D¸þ†—‘J¥ø»ÿ6ã³_‡Œ·QZÀ7øMƒ¿æ=¯¹˜·ýõÛ¨­²ô³RŠ]»vpÇíw²ñþÇÙ~`ˆ¾”EšZ’Î~–.YÌg®¯Øç®»î⟾°+¹ƒ&ûÛ¬^XÏuW]ÄUW]EggWeW Á¹Îã¦o-áSŸú ÿýão‘i{!2Þ^v¯ÿI–ˆûùìçÞÇåW\YÚÿ¾ûîáŸ?ùòu—` ¨tKOí§²þ&"Ò†Îô!"U=Y“Û „Ý€?†h]=¹MQÛP¨YDhðI¤›nq Aìjmš)‡bf4Yzè‰MÔÊ«7:Ö ;ÖøùPŠN—X%ÊÁØŠ8p+k2{ywg oŸ7+Ú»XQ×@gÆù\ÀÈÈ0_ýÊ—¹ñ#_a¿<£~Y™½6¥ÏªyÀ„'…Û…ˆÏž¢^÷/ë­Ñ™CÈ¦ÅØùAÌÂÚ£ƒêöÊŒ`ez0r!¢ÀÏÒétž*­B‚”£ßc•” z÷ì»R+5W˜a ×ÒÐ^纸®_<.Ip…Ý}ènhJòÚyëiG0JlMNyØ¢¸z¬ijâkjùæ®]|ñÛÿŒ;ò:Ö\xáx RJ´†|Î#=‘!›e¬ÿ8Ç÷0~xÞP?xÔ5Ó¼r=sלK$Q7-YkÏPŽŒ£H<_zëè¥ÇF€“î[;gá¹.­óf! ·„Ÿ×TÑÃÇâØMÈi庚;T#¬……¯ç¿~ÿ ö|ŸøÈ‡8må*Þüæ·°háB¾uÓØl½ýcÌkKð_Ÿù,‹/™ö‡ùú׿Η¾w;‡õiÈ–×!C5Èò ¥r©Ì«˜l–eù¹)ÚÃ0Âu‹ v!ÛS=|à‹÷ó“_nä½ï|=Ï¿öZl;T±ßëßðFÚÛÛyÛ#5f^{Œú96¼ñ9sýY(¥¸kã|òÓ_âž]àµ]i.+]T Õú˃hx9?‰J†* |­+Ž!¬8yÈ E¡ >£º¨nH‹BÍ"ì±=€^¬kiiÙXÍ›UMÅjæ–@è¡Ñãè¶íg+Ï5¬†YX ³@k Cb’pØ$Y¡wN‘Þ½•×4×ò†ˉ˜Bú˘Y¦iBcI«<üT'ˆÛ6ïZ¹Šsûûøúßâч~ ms1“õ <œ‰Qî':>L›[àÜpˆå55tÅâÂâÈH7·þèË<ðûŸ0ÿ%ÅÜÕç"¤,å¥d<ÁžaÇÿ?èôÚæVV^x9¡h¬äB®mi§¶¹ Ó2‰!q¢bż¦<@GËf´J‘éÀFÜÀoŽÞÇÞ×ßÈÞþ2^òÒë¹èâK¸à‚ Éd3 ô÷cZ6]]]Lm=øÿô±Os×ÕñjÌh“ åVœ[k¡óU%O(d#t1ij²ŒX D_Ä££ûxý{¿Â«îº—÷¿ïï˜={NÅþW<ïJ~P[‹”‚Å‹—`‡Â„ÃaÒ©ŸûüçùÌ7og(væÜå“É AQ¥¿”Fص`„Ð…qD(ðžNíóâ»´"‚No^P.ª,­­qâ³ñBµùራ»ªÍ SbÁ»¶°Ô½¿üUGjhx¥»m)"«¸‰âÇP$DBíc¡ÛÃ˯&l˜†ÀYX¶aH´€éf°ÐÁ,DÀú¶6Ö5·phlŒýc}Œ;‚DP ÑÖ§un µá0B Fòyb¦…‚å­\>o6îæ?¿úq<ç¤i“FiM¸¦ž†Ù‹éX¼ŠDCKÉ~Š©/UW®ã‘ó¥MµÕ ™Ý=ˆø9ô¨–p4ÕƒÑy{Ææð×ÿö~}ëÝ|þÓŸ ³³³£˜Úòù7}÷&þå³ßç¨}æüu¾Z:¥ó*—tH&§ô%OÒ4Ê|fí²±v¾üû{x|Ë[ùØ?¾‡K.½Ì_Ü(hëÏ:»â˜J)>ó¹Ïó‘¯ÞšõJÌP-%jHyŸ”ÞN¡¿dÂM„Pc¥ŠVNeñï aգǡ[V£e9}Æ—"ÊNâÆgcä†@ˆK€F`àd1»x& ¡·ÞzÛJ7Ÿov«mIéSŒò\2‡¶pz,J¶}„,¬pC L))¸~7CúvDÚuIIÔ°@yd .ZiæÕÖ± ¡¡dØx®Ë@.ËÁTŠcý½ô¤3 d³¤‡œëá*! Ëf¶çò½ï|–öx”Ë:ºh Åȸ;7þœ»ÂqZ/ºŽUWÜ@(šàdkSø‚aºZ"„ 5ÜËèXÑØ0epM}À§0”‡‘쉿‰_?þeÞ°éI:;;g¼®ïÝtó_!·à͘±À˜WSã'“çÔ^˜åQ[[{¢eZPÚE6fçe<>²›W¿ý_ùú§ \óüçÏx}Ùl†[ï|§år,»f ý¦Z?œB ‰ˆv¢‡·!eù+U#Ú7ÔS»}&°´‘^+?ŒnBÉI!1ÐðVÐj°¶¥¥å÷SÕ¬©q` „™¶zvï=C+OZµm˜µí¾oz k`l×dŽlBõ ¹«Ã÷üX–maHQzÅB&¦Ö¸ù<鼃e„ÂæË9.Ž«(x -4Ç3iéëãѾ>Ž¥Ò~i +Dƒ¦>¦ÕŠ‹XØÒ@ŽRd,—w‡×1Ë2žuËMÐrus×(ÅÏnþwïßÎyoþ¢ÉgF§B2rü Y•D˜eµ¥N¨GW %V¬Ò L¼è,žxrW^u5Æ åQ—¯XÁ¢YulÝ ‘FJ“ÖTU#ø'C]ܨ+)¯*†o…,‘±û Žw?aÿ>tˆ#ýŒº¢»ZŸ¤N¥¿"ÚŽîØOÇÅ šq^r±[ ˜È¡s£ˆXK@ü,KãÕ7Ög×bä‡" .n›ròiq€n`®‰e>zûM©ááeBÜø,FÆZM ¤À4 lÛDg‡Éy€•WÇáß¡µF ]fsøÒÃCyÈ`¦‹Y&VPX:ë8d—áLž\–Çúy¨¯—á|ŽæH”Ó›šyÕ¢Å,HÖR 2äôÒ¡åÏÒ(W“ιô¤2læñ¡>eÆI “­÷ÜÂýÑ—¾õŸGÀÓ‰fàÐ^Üp»¯W?Írfª¸FGڹÈe³Ä|ö´¶~ýY|ïŸæ}ünÝÙèº a øÇ™$> Ta‚Öæø4o”?NÊ Ý©L€r×íø~’cyç›/䯘9¥Âq |îóÿͱ|¦cZ<çL" ´ 'í <þA@?)n/m ©^ˆµà™1”A £´²â¸±ŒÜ q>PŒ–Ÿ¹š‘>ämDÍ7}ÿ47ŸoÑÒf\¶QèMùHi0~œæX˜–åËéß´•l÷0ZhLS–$GÎs‰ ©5R€!%½¹ #ù(MÞóHÚ6×Κϕ­sÙ<4ÀM¿º™ÜHûU׳pý%X¡èÉ—£‚Bjœ¡þ!D}+Õ×è8uP”þo`—.1xá ^È©´¦æfþíç´åßå#Ÿ¾‰Ã©³0›Ö0Uåù!æÏ™U•gfWx±* ]•FöÝÁ‹Ö'ù×ù/–,]vÒk2 ƒ·½í­üöÎ7ðÄøÌä,&ÎS@ñ4«„0Ñ6tºí*cò·™êî­G§‚WVøÕ˜… 7k×ãYqÜhÊŠ!‰$ˆ³ñùY¥6xôáÏ2­­­q„8 4nÄ?HÕvüñÆ„é/(iJ)…ï@·- áÇ=º3v’W=™ ßܹ‹îÝËÚ¦&ÞqÚJÎikå´úÚ£1,) ¦ÿÀ –¼L´”þÕx \/çPÈ9>ëY ´™¼[šÄl l ´§1…䌆V>¶ðl>«©ýæ—¹û3 {ï–`)å™9B&{Ok„]S¡»—ROO9)hò¥½Ñ‘xë^F¢ÌÛäyû÷íepp êõضÍëßðF~øsqûNÔ¡_¢”ßgÚd¹C,\0·:Èš±Tª èÔØDÓèÏøÈÛÎå_ûÂŒà( x^å¤ÒÚÚÆ›^u-æð#“ùãT釓|'J¯2^V´œ1„òÊ@WÎÍ*‚E!Ì$2èü¸O!RV~ÓÀtF@k”]‹nÄB³)B£*3/É`1BâÆ:|ŽKÅH‘¨ñnÎ.æn8 ¯àà Ò‰µÌ >;0wÈržÇÑT Gyl<~Œ/nÛFÒ¶y×Ê•\ÒÙI[,ÊâšZ’–]øµ=-0­I©¡ÁqTÞ‚ƒp<âÒ nZh9‡TÎ!•wÈ;(ëjŸå«„ߟØÂàœ¦Y|yÑ…¼¯;Ë‘ÿøþâë²éI‹BH† O Âü«g™e‡À=Àúù&_riÅù¾ûÝ›¸èÚ7píKßÀïëŒÀ]¿þl¾÷í/óîu=vîð.´rPN–˜fÞ¼ùU÷[²d)Máq¼Ü ñ&Ž"ü„+á'ßøWÞÿþ÷ÏH„¼í÷¿çú—¿–·ÿÍ{è rÝ‹íÚë®cis/¨+åƒ÷T@AÙ¶%šB„šA; rÕS:ÁÖ–¿²/¤‰&nf7=‚v²h#„í(^öJ|–o©Uu“”­/øJm„d¶ål”¤\üj7‡qøw¬½útš—-eÿ½÷SÿÔN®éèb^m-¡PiH2—¼ã'Üg‡þ\–_<ÄæÁA^µx çµ·2 êBaÕÖâ)£4¶íƒBi×ÅÁªPð< ×Cj@‹@ }É‘-¸(’°´0\…ÐCK_ª(‰oטR²<ÙÂEvû»—û÷%>fÚeÏI1+Zÿ/8Ÿwïà¿ÿýýt¾ê¯Y¸þ’Šà:yú‚ØÙ3¡UA1x•§+ÒÏe—VJŸÿâìmÆš3 €^3Æßüã7æ-oy+v•4˲xá‹^ÌÅ—\ÂÞ=»q]ÅK–PSSSõ!›¦É‡>ø>.8ÿœB5k×ÑÖÖ>ã Èår|éK_âcŸÿ9£É˱cxÑV~öÛßó¶·§£Dp\}ÕóøÂ÷þq7ÈfÒ[¥ªƒbJ“¶ŸaXEX ÕQúWû†zªÕ³™Èðý,{ÞÙtœq:ÂŒ<ÄæŸÞFÊ= eÅ‘…шµÀÆRÿÌÐ`hÜpÊ dðb•êEw?DNGþßã²HŒ«–­`Y}=5‘¦íG¸³ŽKÖu‰J‰~¡µû{z¸¬««G)Ú£1b¦‰‚H$‚«!›/`‡$x°ÐÜ)àÐþà7¥Ï–00Œç Ä ‰%RûÛH-QÁŒæ(‰\KTÚ¼ªs%‹‡ñO_ùOôeÍÕ¯ ؾbh` Ñ<…ST$§`„ ‰?Ä™ËÛ˜=gÒN˜çæßÝ‹®ÛPò2ñvÆŒ—òÁOÿ‚ÁÁ!ÞûÞ÷Ù¦µššZN?c=§Ò¢Ñ—]vÅI·â£û8_ùÉfœ¦k1Bu \ŒP-‡»M6mÚTßÓvÚ¼8÷ö÷a%º¨H«:¦gEÅä,áVôèˆÍ´ïäga× û#a ±à’sh>í4Ìp­5K–°ìª ýäa<3Ì€kð5+f®jÒÌG¼H‹ovÒ¨·Ó´ë\¼)”࣋–ñŽÓVrzs õ±v8T¢!xž&aZhÏC‚ãé ãN%µu~UB fšÔ‡ÃþªSH´ÖH=)tK÷«T ¦I©$éœCÁUþÿZ3,_RHSHb†Ü€lb&Žr)xgÔuð•öshÿÅÏxໟ!ŸM# “ўär²˜³qÊv3n''öqþ9ë*r5vïÞɶCŒXà) "î2TO¾ãzþíÛOrã{ßOÿ)àÙ¶ýûöñWoýþû'ûp[_€´k'¯ AN¶ðä¦ÍûÄã Î9c¤— \]z«°I¦IÛò~¤biA{é²óÏ$E4ˆ0†gÖ™+I´·ºl8)/&Y'pTÉ9³?‰2hF˜¸á&@ óX»~ʵÙm|fÙ<þýŒ³yý’¥œÑÒLs,F(FÚ2XAViçy y¬ Xxhb‚a³¬ÒE6†#8Ž"Up}ÎL`Dm“|QZ›WÖ¹ªÌö’&¾‘u]La J ò½[áÑ4h­ K)$ð´b¸c´Ãñ<º¢5|vöù¬¿÷aîÿú'È¥Æ8¸‹‚¨™\üåizf¦~§=‡ý¬[³¦â<¹i3£ª !‹ËŒÁè!̪ëE|í÷C¼é-ï`Ïî]PpÜsÏݼâuÍÍ[Èö+ý$©¢‹µx/¡f¶íÜçVÒžÏ<} ¶Ûx³¨ì©Ò¶ÂU ŠI`i°ëýªòU¶›ò¯0ÐFNPQ{'à Y‘5­õ¸ZQ]ø|Äê ôÅ ÃÊŒ BuþŹ—ÛüÓšÓÙÐÖNC$L$Æ…!Ã2|p ÊÃÍåÑß¿nJÉH>O.è¼âm¸Ê¯íWp=œ)“…jUqÃô(GПÓeh Lm”€òm€ðí r æ³>1PÓ aÃÂ(M­棳7påæ=Üõù³ëá{Ð2Ù>ÊÙ¯Ï (øQî„böÜ9ÏaÛŽÝ(»uú1Š©¥ÒÂ踊[¶×ñŠ×¿“Gyø9†ÖšŸüäǼúÍæñÁ˜-çRr—H­¡öìedt¤â‹-¢.œEyy¦ŒüJ‰JÉ[,­VÜgöº™²}ªá$ø`Ô’êòþT'—c¼§§8ЈÖ×à í/$Z ”\~3I¥h„²â( ^z€9ûxó’e4Ç¢¸B÷yìðs< !ž‹(8ÇÅ@cˆš&QÛdÂq¨ Ù<ñB¾äµ›&RJlËôú@jH!|*JùýkòÀñÔ¤šH¿ˆtåۢ췬ë’q²H¼¯Zùë±ë 3v”¤ ¨þƒ‰ì:‡íî¦oËcˆP€Üð nÆò™°Ê/žçFélŽSWV’Çq :Ú¡úÊú¾åK}}ÞlÙÀÃ+xÏû?Joñ¡ÏÐ:Ä÷¿7}÷;ÜôÝïðË_ÞL&“™qû‡~ˆ÷|øó·.Á¬]:é”(ŸÍ‹j¢çØ@ÇU£¥¥•–Z í¤O僼šúaÕ€;>³š¶Y™w˪!=4çÐh†÷ ûñMÁ¾‚p2ŽÚˆÚĨûý[¥_l`Â_ÀRïü!Z„Y³˜S“DZ&1ËÄËHéçŒ ÏEx*n ¦‰õÕ-SJ„€:;DK4Ê£}ý\3gN n™$­¶ñOS%(‚w%@K<åḠK%p”Ô§` ¿S -Ⱥ}Ù†´G˜$’z;‚Ö韓 œvn !iòÞ®³QBð‰¾‡(t\ôûê†-{à§j´U± ct´ÖW¸`sÙ,é€_u‚TÞÀ#d7®âñ{¸÷Þ{¸þ†—Uìöïç¯þúo¹w{Á§lUèæþf'ï}ï{«ÒG~qó¯8ž_€]×…¿ÌU¶ÿ&„Éx!ÆÞ={Y·îŒÒñx‚–Æ8[¤0µÓ÷ò±ª¡>•Æ#$DZЇKêrµk ÂŒ’›ð(¤Rxù£‡Ž`†C¥ãÚñ(ð¬8Òçe-" #T“ I  ÞD?‹Oï .º‰ m–‰eH¬ RnJ‰)Bé8D5· ÃP aJÉe]<ÒßGÖ/©´?¦…=ysÏÃ+ªUÚ¯ha•Ö$8ŠÆzé}ò³ > I3Dƒñ½]Lº}ËÁá)ßÓV HEH˜¼¯ã,Þe Œî»ÑNéõ© §`ŒÃd¤½XtR45ÔV/sù©LÁwN-aŠšLhåây.§zîC‡ò¶w¾—»öÖ#»^Œì¸Ù~NÛKøä—Ç~øCTE®†ßZ[š¹nT~ŒòHEõ{WÖ²ÿÀÁŠcX¶Mc]í¦'÷­§`“T¢ÅïÇHÚ å¿M“> ¤ë†ÉŒ’ŸH1ÑÓ‹ j²iƒÐ(«d›ÏÅ_w³ªŠÕ4¡5†å±øâsˆÔÕp,ÂÕº‚¾nH¿(¢DðÁ|.¿¥†P˜;ÄÜd’ùÉ$÷ï@ È{>Í!dXƤZUÔ*J¯âà.³=¦ëåD‰& ¡(1Óª™W>«"²žÃ`!K¡T|R^ 7Ϙ“ÃÓaiò÷]çð:‘þ‡À™@gûʽ'Eðx9‰ %®ãú“E±˜\9•ESR+T!…3¸óØyñùÍ\zéeÓ¦çyüÛ'ÿ“Û¶šX-çøçU®¿â­ •¼„üÄ—Ù·wï´}_ûÚ×òžW­¦-÷kt÷op†ž êøêÊ’OÁ=i#NOo%%FJI"õó2ª‚‚Êãœ%P*DÔ=? ÊV¤ª<®oK´¨!Ý?ˆV íyA]R–…Ï®)ŽÛ6|AQUÅjJibMuÔÍj#±õð{ÇF8#CHYZ3\àK‹ŒÒØZ–F8ŠÍ’’yÉ$ΨÇí|k×..Éæ˜W>E?&!„‚¥BÒ,ÑBŠF¶_êÔ« ˆjà(õo‚™À•!a–IAX˜(©q˜ÌiN!>6k}îäWC[0ÖøI‘‘ffð`¦¨C¥eÿ ÚÚ¢Þí#¡ ˜ªºTîTRx©#˜™}tÄÇ8÷ÌÙ¼äoå’K/%™¬aj“R‹EÁ®¥´øLñb´Ï's³†1}®lhhàß?ù Þü¦=<ôðÃÜsßÃ<¶e#‡ú!#ÚÑ.Œp#Ò°ñ'G‹Lvzýc;È{¯|TU¶©ö½öÙ¼VíŒ!Œ(*½a&a¿ÔOÅÔaÕ’8âƒ#8F B „ö³ ƒÉ§_PôUH;ÖJ©K­«¡¦¥‘ã»zùò®¬hn"‹V¨RR¢‘0Ò+Ïl›~Ë ËbaM-ŽÒ4E"<1Ðϲºz”Ö¥}bbIÝñc!ù‚G&ï’°l,aÑÓQahQyâi€(¿°I›G"&#öHL|ª²mLxÂÒÄ’&;ƧfK÷Á»yÜŒc$çû³™•„ ‚†v0(UåɧØ'SljeYئ(-£ œ ^êfz‰1ÎZ3‹Ë.¾Š 60oþ|Ls檓BÎ?ï¾øã¯¢ôé•ç/;À²ùtÌÁh‹—,eñ’¥¼æ5¯¥¯·‡­[·rïýòÀ#[ÙypŒá\-^¨é#ž…w=¯¬ÈÜ3Eå€/•Ê"Âm¾Ñ^\µZ?˜I²£9œl6pʈ@’”«Ù!D!å?7åÆf`{@˽J+ÌH3¢mé<ö>r€[Fr,Þ²™÷Ÿy&¡Ð$¡P¦!Á0ü*#ž‡ëºH!*Jøh i‡˜—¨áÜÖ6n=r˜+:g1+– "’E¤5Lä¢ÒÂq®«P’€9&ýÅzo:8t8¦¢üûjßU€GPM0…,s"hDùLǼçø#lÛEV¹„DžD,NÖ•ädÔ.GÖ.3>(Åm‘JWz’jjjY¾ •í÷܉a Z£ã¬[ÖÊÕW< /¼¹sçžSÛêÕkhK¤84Ño®A •‡ÝÉé«WGNz)%mí´µwpÅó®dbbœ}{÷òè£ñÈã›9vÜáª+/¯ØG)ÅØx dý ªÓ)¢ØWS}¬ 5¾´‹ µPõ¡Çf˜|J’ñצ“ÉbF"dGGÁÍcdzŠ»‡H‹Õ$HkpH¢v\’m̈́ÎÑÊg'ã=ÈÛV­¢-™$lšA-8†–’áLíº4ÇÃ%Tç]E¶à7,.nïä7‡±yh¹‰$¦ Z“w’+„PÔ„„…%Šœ*áDuÒIÃ\¨ x81`Ê·¡Ÿ#rJw)ņd¿‰ÔóàøQ¾7°m‘<¯yóËY³rÿü/ÿÂ#OÜŽªYŒ1ç…ˆÚe•j–Ð #>r¥T‰`‡Büó?¼µ«~MWW'ëÖ®aÎÜù„B!žIëèèä]oy)_ú¿`O-*qÚMÉlæâ55¼ä%/~FÇM$’¬Y»Ž5k×ñf­ÉçóÓ‘Ùl†ÃG{@¶•wò)H‰¾/þ¨"Ü (_r˜Q~ÂD©cG¡•ŠDÉŽŒRÈdÚ»‰%¶¸$H›7 ü%ªV»2†•Œ³ð¼µ¸ù<©þ~FŽ !j»x¤§›ýÇi ÊÁḠ¥4h‰pu tP5pLñd™H,!KREi]\ÖfÆæiEV¹~ºªmH!‘RâIMBX\ïb<=Á£©?–àea|7³cý¬nä•W­àoÿö]UëìžJËf3 ô÷±{÷.xà~6nÜHjb‚Y³fUÔ­šÚ"‘HUª|±õõõñ÷ÿa>ñŸ_à»ßÿ9·Þz+›7=ÁñãÇÈfÒ!°,{Z!왚a¬]»–³ÖÌåò³gsñºV–wºÔËC0¶ÜÐN²éa„Uð¼f°U*þõµ¼†öîÛÏ¥—^ÂÐà O<ñ‘H¤jű±1nºé&¾ô•oðé/|‹ÿúúÍ|ûæ'øíÃC<~(ÊÁôlFŒå8ÑÓ‰%ÈäB&&ÆYÖé±aÆg -›7qË];1›6 ­(F¨#Öˆ-À -bÄëâÀ@„ǶpëÝOñ“Ÿþ•ä‚ Î¯šè8÷Ü}÷Ýw/££Ã„C!bñ8¦i’H$™3g.—_v)MIøÝï~Ž-.«O8þž\:A˜èñBç¡&?Ó°ÒXAFëm\ô¦ëXpîz<Ç¥Íù¹KRâd3ìl+–i!½, ºU+=jû*‘HÎæØžM¤†ˆÔÕ`Z‰–&æŸuÛïx/A¹.5ímt¼èZ¾÷ýŸÐhGÈ;~âQkMö*gk)À-ê—>‘0,L.mšÍÂh=¿9~[{s[ÏÖ×µ±¾¶E±::CIBÒBJ?oÃzrž0 058ªdx<)Nžˆ´±¥¬á*J6„15Ï=éÆ-k®V¤Ü<žÖhËÀ“þw5fmúîL!OÈómI¿&X“ç]5+øÏŸÞÌ ×_Ϲ6ðþ÷€·½ýlÛ;À‘AÅ×^q.7Þø·D£“³ðèèŸøÿæ@n ²f=2žDÖØ“§Œ§ÔäÚtÁ?ÃvÆgÒ\÷  ÒŒPšÍuà€±âVâ]€ 5v€‡Ý„ã86M?·ß~゚_ܾƒ ¯žˆ‘bv“`ÃKxÞå³vÝ:š›[0¤$!qqµª˜¤*Rq§j_»íe&K•˜¦P¨\?–³ó^w Î>­•ÿRŠìøÑš$5m-„B²œrÌj±ü±k #5Œek9¶y;K¯¸;ÅÉåi]¶ÁGéD&N6KÓ‚ù ¯?ÛÛÌ óÒ›J3-’B"MƒŒãbÉ@ßGUÞž§Z°,Ñ@ëœ8ç7tñÈP÷瞣¬L4smóΫíÄ—¾Q\ÌQ÷ãF¦Xs= Qi‘õ\lib‰f°t‚ò<†œ,iÏ%ë9˜BRk†©7ØҤœ_“œ0mdðHÊ0†´_JÕu\,¥Éò„̰§]ÎHv°èÈ>û¹Ï’I¥Ù³g/;†Z‘í—bt´°)ÝËž¯ÝÂ…œÇù\X:ogg/xþå|æ§=˜Ñ–É%¦)+TWtÔa=ÌâE‹È XSº¼Ñ}ü]³H$ª{~æÍŸÏi ›¹cO29¯ŠM &ß„„\§-[8Í®¹ùæ›ùë~U{&fý f”œrÙ™cû­‡ùö¯¿DWƒÇüY„l“ÇŸ:J>¾>Ð ÊžÁ‰ u!V-:ÓŒkÐT¾•:€¡z9ë5/`ñç–˜Û†mã9n ‘d’p‡‰-L„ĤE«gq´ÕÉfZÂ1ve†¹¹w/)Çee¼ [Kßs&$žÖ¤œ‚ïù2 r®ë‡‚B1Jk<åÏèYÏåžÑÃ|­w+?ÜÅÆÑÃ<4qœ{ÆðÛ‘ýÜ9zˆA'C‹#a†&Ç—i™$¬05vSH<¥Èçó8…‚_VH>E_@V9G-ã9Ôÿv×/HmÛOBÚ Ô,ÃLÌA˜Qd¨†\ÞÅLmçÊ+¯( j)%³:ÛùÝ/¿ÇˆÛ†4£“Teä¨ü8]áÝÌé¨ã+_ü,ûäÌõg•¹% $Gh |&¯Ô‚Œë 4lË ðÑð55À9ÉN^Þ´Œš–ò‚†E\S¿€õ‰6’FˆG'ºùþÀvòÊeI¤Søª’² ›QÃòÅ¿§0K lé³– áÓúóÊeÔõ«Ï»(²Ê¡Þްm¬‡ÕÑFöòôÇ!­¤Ð~yÿã»ïáò ÖÐÚÚVz&MM͸Ù!î¼ë>D|áô &!$zbì&wl /\¤¸có®ºî¥„B6££#¼íÇož°É×\ȶûé?ô0—]zqUWo4áç?ÿ9Ys®¿^z5ŠˆÖ0¸‘¼ãj^üâ—TØ÷Ý{/ÿþå[ðjΚ¬F3Ås"iØv F¨. æœÀ×^åº0ŠšØí“FQTj'h›ºÀò sö+^Œi[8¹…t;fðàÆúúI6û•k„|l3ý£XÒ+dª¹_÷­$aGȨzöÄ ÙH)‘†—/`†Ã˜¶U±{|Îl6àx#¹žÒxŽh-þ*Q¾Á]p=rŽG®à9@šñ|Á§s)p=MBÚtšqVEZx[ÇZ¤¡¹qÏ]ôäSài O“0C>ÁˆÅåÔ|"e ñû‘ƒüãÁû¸´v._Xp/o>E‘&Òˆš:#̹‰N>Ôyè8‹ûÇŽñ‡ïeÔÍØÂÀ’Ïe"›ÁË@ù±SH²ÊÅ ˆ€£^Žˆák¯ÏA EÔ49¿®‹_îe1 »]$ð ´“ô:søú7¿Mwo/‡åðÑ£ôòò—¿œ VX¸c{˜t_dCb¤rO‘íæ çt2mº,#óWÎúÑ~Äm j:)m¬æ üø¶C|ó›ß¬Z›xÙòœ»vîÄaªÆ„¤0²…Ë××ñæ7¿¥B•Ëçó|íÿ}—1¹aØ*뤑]ÆÜ-%€Maã–³§¨½h…. £óƒèl/*ßÚŨ]‡Qw&º0Ì‚UqÎyÕ‹°Â>µÝ0MBqŸlÛ³{/ãý“ iLõžMµAª(~nbðàqÂ5Û‘†ŽÏM¤ˆÖ×û Õ(¼{$ZšÙæäÙ7>JÄ0I9IÏö½L¯,µÃàj„’8®žð¯¶D*IÎó)'6¤EK,ΡÌ/i\ÂúwòÉCóÉù*ö«eL7(î€à·CûørÏ&þqÖÎŒ·!´`wªŸ[G°M1aiÏ%”W,2“œ›ìäôXŸž{)Ÿ<ö ÿrô>þuö…Ä ¾ÀK²¸Ê#d†1/§Uà” 4u–¯‚´‹ƒ‡mDL“U‰&޹ù¦ÓÒF{9œt7:?€‘í!–=Â?9ÎÀ“O`*åç(„l"Í-Ø2‡î¿ mC–Õ¥õ®ÊÃ~Š3OoàœÅÍÜøã-Üðžç#¥d` Ÿo|çx‰Óß_ Ö«=‡Ï}ùÇ\zé%,[¶¼b „Ãa^öÒë¸õÁ/£ÕÂÊj“BàNbaÍþõ#ÿM]]egãÆ;øí}0ë_à—-ާÙ3J‰©? PŽ7…Îvƒr0›..EÓ½ñíÌ^äqþ_I$™œ¤µª¦ç8ŒïaÑygQ^¼[¹EÆöÌ)e^T”h1BdÇÙwߣÌ]¿Ó¶H ŽÒ²útžÂÍçÑžÂ0L&Lƒ›íãívˆšPˆöX á(´áâŒã =ˆ[!LÞ¤Š(°>ÅD( C¶Âd bÚ`~¤Çó¸®q_:¾‰ïõnã m«Àõ|Ê@m ntWzÿ:þ8ojY͆di·À·ûžâW‘!æ^q+V,§¾®Ó4éîîæW?ÿ?Út'¯¬_Ê›ZVó÷]çò¾Cù}›yGÛ­‰J¤_Jµ =laSnPƒX ð×*‘liPc„üÌKCÒ‰ÓkaLkŒiö†X¬ˆ…XÙœdIM;m±…XR`™’HÄ /\ŽR<èM0R;ÌÝ¿Äm¼#ÔÐ'òx¹Œ¡\º"ÁG_v:£ÙýŸFåp¸ô²ËX½à[<ÖÓëô‡†xÙ~¼‡ø}ˆÕ«+‹M òŸŸý ö¬bq‡g ŠrÁá¥Ñ…aŸ™ r¨ñmÈø"„•åáï`Ö"‹ßò*â %p”°-%C‡’K¥©mk-Dy…l¾<4¡Um}t°¦€@¸¬…ç8¶In"Ev,MÝœY>›W =?ÇEkE$™d¸#Îgîẉ1<­Y[ßLØò¥OT>µ ˆsàj€9åháKa€±yA[(AA)®o^·º·Qk„yQÓbÿvJ±­/vobU¬…ójºð´æ¿{žàÅa^ûºY8>ÔÖÖrüøq~ñÓŸ±}džÒC|"3ʸWà»6pcÇY¼çà\Z3‡¥±&?G(hG)_:VYj´ÆÕ CH ¶ð°ÐÄL‹z•¡-ó8¯l›ÍùMkh G© ‡0l Rãiþ|G{$ì!aв®­…–-àg‡÷óýÝ?gÏPœ‚¶¨±Ò,k*pÝE ¼øÌ‹i¬pïîcH+ä/…–NsÓoÆ /Å¢râÓ ³ö4~üë[yÅËã¬)+E544òš—_˹b>5ó¹/òÍ•«ˆ«ýÛÕ×\Ãg¿ôCäGÒĽƒþÀ+xÍk_;-(xûm¿çsßø-Ô_EY¤ï@¡gþ vÓèÂÈdö ëFçú0êÏAåGùí¬¾h>g^ád¢Ïuqsù’í±÷GHpÚ‡!H ‘ÏúRÏWÏ €;Õ‹^ 4y‘”´‘Os÷‘hHP?»ƒp2Æ®; uÕjæÍA¹®ßQBà¤3d†G>pÈD\þá¿!²l>»œ ¿>°—Ûr#|û©'y Îâáú›:’lí¬á‡wqëÎͶ¤ú¹e`· $ë˜ Ó®S"ö24Äê¸yh÷£+”¤ÅŠQP_ì~’°09;ÑABZ|b| Ï{Ûk8ûÌõÌ›7¯ä¹Ù¾};]]]|à “ÉðÀ¾„p3<¿n!]¡$?ÜÁ¥µs*)ïEJ€(û_è8þgÃþÚŒ†`v )1%ÊõmÂpYF¢•E ?ˆÌB†©KåÜ—_ÈškŸ‡ OÚÜ|åyØ‘‡ŸÜÂŽ;ïeÙ%çÓ4ov àZ)vßû G¶%d—"éû›¦J0Ç¢  Çh^Ù„“ÍÑ46ÇQȹ4ÌŸ‹Ö'“%”ð9EÊóü|e­ïîaõµo¨¥JÓØÕNnxŒB&GÿÁnºÎ\Kû²Å4ÌîÄ0-’-Mlüü×ð<ÁÛW „f_v„_ íæ×ûxcÛ*ÎJ¶“vŽçSh­ZÐn$x[ëZî;Ì{läüä,®¨Ç®Ì0XÂàɉ^¼Em¬YµšŽÎÎ fêàà /9`Zÿ-©>6§{¹°f6air(?FLž BW|¶ [ÈXrÊ%­ Ôš6†8§°Ä Ë_CÅLÐRBbÛL((ÈkpØÀCA*B¦ÄôÝÐ’¡LŽþT–MGéXy¿¹å×Üýè¬ä5UlÉ) ‹|tŸù¯oqÑ…ÑÖ>™(¥äÝï~­­ÍtvtrÕÕWOÇ‘#‡yçß~ˆms°æRYHái‚¢¸YÉÖwA~¾VÞðÈ÷‘¬·Y¸~+.µím%í¥ü8v4Šç8ì¾÷Avßó ‹Î;‹¶% J;Àx_?G·ì@Øq…â×À©€P´›Ã6ó4Ìé$32N¬±Ž½÷~ôAæXµœ_ÓE“å—ëy2Ý‹§4—ÔÌåÊšœk§·¦§òcf[Hvä†èZ¼Œæ¦¦i =ÍÍͼá oàSŸúÍÍÍœyæ™<úè£!ÐÑ›Ò}\V7Ÿ&+Êñükâm“T†jàTJá/I§…BA\ZHS€=Ù4Þò0ÍÑ0ÚÛXT_CK´‹ï}ÿûÜxã[644ðö·¿£ê?zôoçܵ#‚Õ´ºúyN潪mmñ’ûW¹it~á eh\bÎÚ0gÝJjÛÛRL·7„@yýì¸ó>†eéEèéáQvy_Üú¯jXD½åÜhË:¹{ì?Üx8ýöÔ/üˆyƒŒÒ‰1âæýzaAìbB9ēɪïeË–qÙe—ñ©O}Š«¯¾³8;JIK¢–q¯€Òš˜´H+gRµ.IQjLG 5”P¸B’Ò_`Ș”(‰Xµ–_=À·öìB MW2Îâú6Õ0·1A[]”d­…Qzy¦"¯=2¸ äÌæÈ¸.¿|è8gž÷"”ryxÓ!ŒÄ5“3pÅh¬ÌÛ×Zá…ñïÿ’W½ê•Áʙڞݻyçß~€Û·ZXMgWÿ”AQö½—EFÑnUAçz±Ä0uõ‚æ¥Í´/YEËÂyÔ´µb‡Ãh4ZéÉÕÇŠ‡q]2#£{jy’hm ë^x5sgù%~ŠÛÆûú9¼é) ^˜i!ò% RÅHwá`o„W sõz†§kÍ Ù,ùT†š¹I¤4*nÒ0ƒä(­ %â4/š‡a™Œõô3z¼‡ñžRCãÈPÒŽ2Ü}Ôð¦m“Ÿ Q×ÑÆð‘ã¬}áUëÚÅg~û¯ŠÏå´x3õ"Ê‹j—ð¼‡1/Ђ:&„_‘±8Q)­1ª›£=P#mÆÇKªTy“Rò²W¼œzßüòW4›QÎOÎb}¢+jçqV² d•;Yé±8J¤ ÁÿÅ¥èÚ·5Šï†Ÿ=¼¬©Že­ë)dÙ16“Cìá®în Ê£.bY[-«æ40»5Ž•d´KF¹8R!,Èy.?½ï nb —_v9›6=Afb™½—JFý´TéQý’9y„Ê`¨ ÂF–¶„fþÜ6 …êõµÊÛÝwoä}ú8OkÃj\ÇdIÒ“¢š›W¡Ýqt~/}Ó=BK«Á¬•óè:mõ³:‰$HÃô»JUÖñ &-¯à¡g×>=¹·P`ÞúuÌZ½‚hmMÕDkMjpˆþ}9´e72Üå/¿§K±!¨(ìʧ¨i®¥¦­™î§v# I!“Ås] ËšLó‚„Â0JAC€ÚÎV²£d†FèÞ¶'›#›³6áGaÝÝ;ölj$32Šo¨Ç-(¤³¬ºòR¶ûþ¯xaÝ\®¬ŸG­ÆV&uÚ_™Ös5Ý &—D&ÃbÌó¸+"lܹ—¡¡!fÏžeY¤ÓiòI~úÃÑûÐ.ΆxßÚW°$Ò@›VŸ¬è)L°?7Êó Qe kzºa^‚W0dµïýÏuñçÖ¶rî‚V´¡Wz³vŽŒðxÏ_hžÐ,Ÿ]Çé‹i¬QÐ.»òû'û µ¬âõ¯~-ápˆ•+Wñ¥Ïÿ+¶eÑÝÓCo߃ƒ#d²~-bÛ¶¨I4ÒÜÜHgg;³ººèêꢭ½ƒHdæÃÃC|ík_çó_û%}z-VÃBJŒÑ§íÎTÆKÂÈïfî¢Ë/¹„ŽK 'Á#ðÙjŠX,…ääò¤‡éÙµ—£[·ãæòt®\Æœu«I45 ÍéT™ôÐ0#Ý=ì¹ÿìXÍ(ÎD?PšDúúúªmèõ'Iæù³¦‰ç8dGǰ#¾xRž[F~"E(Çs”ç¡<¿0—1Ñ7@j`˜Ñã=89ǯÊTp7b tï>Èœu«°cQLË&œˆ#¤ —Nã9.-óæðD,ÊÿK òPjkëfqZ´‰ˆ°JjVEQ†àSİè²Γ׫â-„÷îâáÇ¥¥¥Ã0øÁ~Äíÿï\£šùàœóh×ùž-ÀD&ˆ©ø.À{Ç’×.³ÃIržCÄ ÜÉÕTª@’h鿤¤Rjÿ$ˆÿÒeïþga ÑÉh‹#¼ÀÔ ¥'xòÐqnÝv”¯Þ¾‹¦º0ƒYPÑvιìuœ}ÎÙ„l¥áp˜ùó×±hÑ¢Ê1[,f'ª%¸e2iÞýž÷òƒ[#¯À ÕN·9NÚ "áýxc[inž`íµ1ï̵X?B+Uý(·9œl–‰!ºwìæØ¶hOѱb ³×¬$ÙÒ„ašÐÒ(O¡<Ïr 3ÒÝË®» Ñ5›3ßz%žãлe+~ó;´ç) ¦H¾¾>ZZZzeX–¬ikžŒ4º“áúB:ƒ›ËcE}ê¸7›C{^©0Wzh˜P"ÆÀþÃ>Ë6£0âÉ’P”ôËhw±ºZ<×!aX…L¥†ma…Bïd—›cïèQææÜX«£Í4[qlaLÒuð×~ð´fu¼•î ßÉüêÞ;ÌÒ³¾ïþ<ýô>½÷²½j¥•„„„¨Ð«1`ì×±`ã’ÄNü;±ãÛ±q˜^ H ÔI«º½Î–™Ùé½™ÓëÓÞ?ž3³³EXlóÞ×5×Îîžö<çþÞ¿þýÒá ñ‹žþâËß :VŹsgúÚ“üYÃAv7´;3*e²%(騕–iÝ2y%3Ë?.å½U}D7®ŠZê Sº›¬†Ž‰n™xùj«!Ù°‘Öµ¯‡ (6È6e3O¾(òÀ¥ Yöô4²§/Æèj’¿úîyRÔò;¿ö ª««0MÓI.TîñæÜ¯b yË4MF&°ÂP6ƒã5bs¬QÆ.'° ‹ˆ¹Ól?ØÄÞw}@u Û²)¤Ò˜†‰7´Aäv¦I9_ »š`aè23¨ïï¡qk¡úZ$U­€Â¤\,¢y<”rΨp!•&17ÏС—±dío¸r.G ®וöÿCq# 2”DQp¯ï^ܪÓ-à£òSùÝ¥ hª`¥±mÕ%PBÁ%ºC÷[7ó¿¾Ä#ß~˜_øØ/‘ÏçH&465!ËòuE¿×³â++øüþ«²~Ùl†d¦„(¹®V×úç± @¶žÁÖSX…%4ý,Þ3[ïy#’"W2ž:²¦áòÉWƒ(esäÖ¬ÍÌ1yê,¥\†þª»ÚÕ×á ø7Ü®õ%U Åå"_euz†ÁC¯ Bìz÷»™9u l_M ùøêúŸ–_ ‹@Ú¶lw~5Qn@u»È,®PÝ݆'ägm~Ñq¹|^Ü¡ål½XØ DY¦”+PH¦Ñó|U°×WÙE>•Á²,ÊùªÇ(Ëe'm”˘º‰à’*«ŠW€Œ©s¢”åD)ƒšZ%`™„ ˆ2ABDæJΤéuG ˆ­Þ†´»cõ´Åj¡P‚\t“’eP° 2F™áÂ*®]æB~…÷Uõ±ÓWCXvW®6[ @pjŠ$\© ‹6‚d;‰£Í±†ÓàûÓ³\¾˜ÀïV¸³¥ž»Úë©yTÛ‰fC%c…j#H:ÕÆ­ŠØ.7†ìÂmfø/ÝÆoýÝi¾þõ¯³°¸ÄéËܼooÙO,! ÝP¼óÚU(äYZ\dàü/¾t„ç^<ÎÞ½üúøUºº{Ð4 YVz¬ô ’êÿÑAùµÿeéØå¶™Ã*­á2Ïò†_xÝ·Ýâ<ܺÒ%+ÊFšV”$lÛ&·– ½´Âü¥aæ/SÕÞB×­ðÇ¢ø«bˆ²„¡ëóÎ,"kå|ôÒ2 ƒ—¹|ø±Þ~zï½£T">2Jï[ÞL)“¥°ºêÔ×aW Ò©üÇŠmÛ5ÅDšB"…+ècub–B27fþÒ™Å%$UqÌV"…]‰=lËDv¹(erä֒ض7B`òº*ˆz±„mY3Êù<‚èܽP"³²ŠaŽÔAåd°-Û4DÕOÛ¶HZk¦áü¿ma%¬R‚ª®:þojÉ8làýÕ[pK $s`Z–‰…ÍL)Ã|9Ã’<›š¤Ëáã {©R<¸D‰jÕ½QÓ°°*×D»’E]Â7Çâ¦XC´©xøÓÛob©Pà•ù¾;9Á£c<ÐßÌuý*nIBEdY@R,ÙFTʶ…$Ú`å0ÊL\¥2goÿ¯2YÚ‚ä»™ï^âû‡¿Íg¾ü4íÍQÚški¨¯% ãõz‘$Ã0Éf³ÄãkÌÎ/21µÈÄl‚…¤DQ¬CtßÄÀãE0ΈKœuEIDATªRÛã”þ-Ã`îâ0‘¦zdUáÒ³/“Oçí ±z7Ý·Ñ{ç:oÝG÷íûiÛ×"–È-NQJ­a–ò˜ù5ÜJŽƒyÝïy|2'àgÕ*ñÝS¯p¹°FÖ(R¶MRf‰¤Qâhfž¯®\d¯ŽVm¡Nóa 6² ÒîÑèò#U6~ÑÖÑm M‘%Ç¥JEç´—„«Ó¹•€ü³/±dä鯉:@’mLÁ"­—°›ímîêjàÒ|’LJ§h¬öbI³©ËÙÉb‰¬¡“³ 2º-Úx="š[Åíucê%Ò™ñcÔ'6\'I 0³äÉ'Ÿ¦»»—¶ö>ò{ùý¿}\o¼Þ¥ÒSN÷í¦‘ KOð,°ÿÝ¿€âv]×b–+òx‚€^,’ZXbmvžáJkz}_7š*dM­¸Q¹C.‘@óy‘5Ë0H/­°xyŒK‡^&ÚÝ‹¯*ÆàSOSL§©Ûº…¶[nvHµçæ{þG—q…‹ó`cÃKKKÜÐŪ¤z‡‚$KîÌršÅ¡1Ü!?É©y¬xŸn³Ë&,»ð{54ÉÑ—p…D¼ªŠWÑð¨ Ya1›æøü 'KkrOÛ2)á\™FËÆWĶ™<3@û-ûh¿y/’ªà …ðÅ"(.·#ÚX–‰ j«ñWE1t³¬SÎX%1;ÏôéR ‹d“EÔX«SºöD˜6ŠŒdWØ’/ð·SâE äUQdAVøoP\ÿ϶§‹ÇŸzž_ú¥_Âçóñ÷¿Ÿ/}ãûŒŽ^ºsâ9Ý·ë.Õæ•aÛÛvmmvê•:ÚúR=nGe,_ 1¿@fe•Ë/¡¶«ú¾nç{®Š9ûE0ŠEÊ…žPeì"³¼Bv5Ar~K‡^!ÐÔB!™$½°@´­•¦½{P}^JùÌB)Kϳ)sHÛêÃtx‚ü§ÃG¹¯½™ûº›ÙÙ¥>âÁå‘@³±UHJˆ–À‡ïïb2™ãOO_FŒìu\&Å‹m,²PÌðÍg§ùÅÛY^-Rp#ËÅ’‰n{n¨Á|ò*¨ùgêz²§–“ƒ'9yâ8wÜùFš[ZyÓv1üÈ’+FÞ‡Y¸æ¹V9E8’§ç 7ƒm“O¦°, ߦÃ@/IÌ-PHg=|œHc=uý=•DO,º§ˆ²‚Ë_^Ë;ÃRÎñ@†^8Œèr“_]#ÒÚB¨© S/“œ™%¿ºJ)Ÿ£œË±<<†hCDD³X¹ Â,N Ü ðù|eà-6´…e™}±Z:‚B“Q‚ plš±*¢"‰"åJáP•%¼ªF$ÊàÜ8‰l’úÎz"õ¸ü>ôb‰é3pü¬LLѲo¡úZ—†¿:æd¼ ÅJÿÌ8³—˜9}Ž™Ó癸ÈÒå1s ”2Y$EÁ‹ y=,3}vÑW ]iÓ¶m Oz‘…:WÀ`a–]4¹Ž£$ JÒ• ÕzŠwS†Ê-bn/.ͳfØ[]åhüH6eLl—DG ÀwƦxv>ÎÉ…8%³Œß« iŸDÖ.cac 6†b±¯7ÊË'‡™.‡e¯“å)Oð§ÿý·[$$'h¨õR_íÁíSxe`‡Où¼=?%P\³ÍE™B¡Œžྷ¾Y–™œç©CçÕ0v)VùFÏÄÊŽ²í ­tØ·¡ž¥y=WYË´HÌ/RÊ嘑fuzž¹‹Ì Í‘IKHþ:DùjjÓ,Q_HóŽpš$! ".I¦×ů¨Î@“h_ M—ì˜âJÚ6k”‘ˆâæsÙ]£ÚçÆmÒåšK¤&â¢ÁíåÈÂ*S%•sqòå< 1†d©HŠ€n[d ¦6/BNçÉsËànD@@/¥»²è†…Z^¤¿#B]ÔC¾`ðß>?ÌHq¿sšßÈGz€XßÜWªî6¢aôÒ 4;N$æëßxˆÑ ¢ìzÕÛ2Ьn~ß›ðEiõÓÊ%äÖ’Ä'§Y™˜¢ç¶›ÁX—«’´©TÇ-“R.Ofi…ôr½P@œzG!™âü÷QÌfÑ‚nª¶6Ó|ëV:ßr3Ý÷ÝJûûˆõ´âŽˆõ´PÝßA¬¯…™# ëûöËTtxu à„fÞ0¤œaRÔëî¯xM¥sCÕVÈ–K¸d¯ª:IÅ ‰mÛì¬m¤gq–r©’Úµ,–Ç&ðE"ä“)¢­ˆ’„(‰å2ãGN°26E¨¾–†mýèÅ ƒ£,ÎPÈ”0 [PœŒ‹¬"ÈADWŠW¾®²ºþÅÛ–‰OÏ„Sµ÷ˆ2~Y½¦JÎpD¼ Ú”V‹œŒÏ³·ª-àLnn¬¡w.ÂgG.ñûÁ=D<šKº“(ðàî&lÕæ_¹À™ŒÆCçh®ñro°#iÑÑÀ'ÈøUÛw¬§ùÉSL9$Ùèïç‹O-#¤Ïñ‰û]È’€$ |ÿð ÏU#W·]?þª ¸¡8Ç•ßmË™ó6‹ÎáRét-º¶ðÉ¿z”¿ù¿ßb5§"{ûÄ0–€]Nkðij¸áŒ88,#¹µ$¥lŽ™siß· ÕãÂ6-dUÙàÑÕóJù¼3od9½(I³9‡FYÃSfëÝ÷Ò|ë.|ÕëN9›Gñº0J:é¹%¼ÕŒbË0 6Õk¯#qvD©œÙüÙnJ >¬¶U•(—€ØöQÁ«ž7Ù5Ÿªá®ÌV¸d‡þsý+ð( û«êx||ŠHC=™•URK+4mßBvu ±rÂè¥2—~pˆr®@ß=wà ¹|èF^ÀÀ‡ìŽ ø4g ÷ÊAw͹ÑqšõJ¶‰…€„&9ºl›Îï‚ÍJ9O@SѼPD° ÖÊþal€ÿ½¨?èP±ä2||Û6þÝáçyxb”Ÿëï"àQ6À’"ð–= TWi|îÈß½œá‰óÓܶ£3g¡¦EÚ›ü(n‘lÎ Óè«›ŸEtW!ˆ*b`+†e³š:ïôŸ™6/œY¡`¶¡šùŠÒÒbM·ù`{‹Ê®(¥nú±ÍZîošùd‚·sã;¶ü»X6 Dÿ«É¬· ØåUj;QÜîWH>™Æ(•˜¸D°¶šPCeEÉ'Sdâk˜å2¦n83æ¥ÅLŽìÚÙµ¦dj«cëGßJõÖN´€×i{*Ó)rËkW~VÔlï¢v[7²[u¨~ s=T˜†Ö3X¯ Êš.ÛP•1Êуp½Õظ%A·¢Ð\h•Ó¶P%y#¨ Ø^ÛÀ‹ggH..9Cõ®Ê`}¹Bþ0~äå|¾7½o4ÌÀ÷~ÀØÉ1”P ª²YGÂ~Ýîƒ()$0Ée<ª“•×å­Dϧø£ñ£üQß-Ô«!Çš˜&ºm6Êd­2QLP5°:jüüÆÖíüÙùÓ„¼*÷u6p)”p¦r†Á–Îؾ“ÛÏÕðµ#£|æüëÎdŸ€º,ÒÒê#WÒ±ËPRa*Š›26"C3’™%Ãâ–­UL¯ óÊè)×íNæëªösûGüýËQYNÓMò‡ÿy?ò7gXŠ ¹¢¬“bˆ’rƒç]·)Ì$¡ú®˵Ë(—É'“dVVɬÄé¿ë62ËqV§gÉÄW)—ŠN ªH"¢"!i*ZÈG ¯ž–®„Úp‡ü Å2™…8ñ¡ –ÎRXK"©*îpoU˜š­DÚ‘TISY»8Nffy=3:@¥‹÷µ$¦ ²e¢‰’Ó€wÍÒ-Û†Z_Ÿ¦^ÅÌq{»žašÌfÓT{ý„\nÞÔØÎCg/b¹T·ô"ˆ"ÞpÌrœÌÊ*ññ):nÙ'dáâ§FPÃíN°m¿n‡úª%"%Š–±‘¡ªpO8´‚Í—ç/ÒìñQëõmÄX¦à¤—¯-ÒRÃl—Œa˜¼¥§©|†O¾HY4yKoš"R*[¨Š€æ) Mu>ÔÚÎm7ÕðgÇùìw.s×­uÜ[3Mx}2å¼…aÙW{CVI snÎÇ·žÇ4mú:ܺ’áÈpÛ,ÜP‚àŸÝÌ`lü&ª¤²±˜Ÿ½§“ßýË pEo¦W+Y,âòûnÈÞhÛ6™•UôR™Ù ƒˆ’ÄЋGЭ2áî&š÷ïÃ[IHÕ00A°p«yÑB7 4E"ª¹HË>þêâ%ªC>nïïBq)È(”Š%ܪľî(UUÏ -ð·O_¤·-À­[kèk Q_ë!R)âÐöÈ´ìðò¿¸›ßÿâIîÝßÄמž`ɬEr©›†“*!Âz,°ž¾¾AKɵ6¹NJ”¡‘Ql`koˆgÎ&ÁS 6Xé!Þûèíìæ“¿îækÞ¿RГJ¹<‡¿òMDA ë¶›Á†B&ÃÈËÇ( enú­Ÿ#ÚÝrÝXm)[i„u9ñ¦¬©(n I‘)ç è¹¹•ɩΠ±ó#él²,Jé‚( xÜ•>/R:G~5Éâ¹a.>qžrølRV‰²e"‹y[ÇÄD!àRƒ€[fo,Æwk\ÂËï9Îoëöv5SöRÖ-¼^EôàÉ4¶z™­â•áe¾üÔ8ŠW ¥ÑËöž0»ú¢´Öûð{T¼~™m7…¹íp-Ÿø³#œ˜w#†nq×+Ô rE‡ï§ ŠÊß,S/bÛ£S9’‰}ÝQdk釥µÞi>úÁ7óÔ3£øÕ¥«ˆƒÀÔÙó4Z ¦Éô¹U­Í\|ö\M~샸BW˜4™Q*c– Jé–i!ˆ²¦"k*z¾D)“£˜Î’™]fú•³ly×]›j0Ë:–ab”Ê…ÙÅULÃÀ,ä×RÌ`âùs”Cûp)¤RŠÀ³7º™¯å8~˜×m«i¥TÄUa1/²(¢HE%âö HRE#ùQ™R‰L©T¹fH[«ki „ðª%Ãp‚U™1·±)A—E ¸\ÝnzªjxCk–ç&Fyna”r° Yó¿¾ "èùoÒlóUaâ¸SUš‡°êÚh%)aàUdrœ¥“·ÊHzYs!ÉPö6TqÇÂí2Y³Œ•åÖ»ÂTWû9~zAíÇÖÃòJ $€]"—ˆ“MðÎOÜÌs/M1eX\zîEüÝuìüèƒÈê•øDôB‘ôü ™ùò«)ô\¾¢°,8Ù+ŸÙ¥b&©éÒÓ‹ô¿ãÔïÝ‚Q*“]Z%=»„i8V¤”ÉQJ¦),Ʊ旨,ª[CäJC¬¬);€¾ œºÖ½úgR‰C&ÃÀûRz‰_Eñi.üªæÐÙPéɲ,ŠÖY+—¢l´¦xT•îh5–m³VÈ3ŸI#Vj&ëÿúW&‹" q»7ÒÃq{ywÿZƒÓ|mx˜Œ¿ Ùxí ±A,çØá¯CJ˜È‚Ól(lÌr8NÃð"»ê½ìiÐ×¢¨ÔFÜÔÆBx".Ò+ nÙåÇò†yôØ7m«Âí–°VmŠÖz›‡soÙçÔÌBÅÍZ?6~rPl^¢äfÍhçþñ4©t‰ž®jBA7Ѩ—][ü\ºtŠÞÓÌ·wò•:Íj>‚­ÙÑ+³{{ûõqPwa¦ió-¡D"ÜýÆ.²yዳvô³ó£osZÖ±Q}VG¦ûÁQJé,Õ[;é¸û&<±0²Ku:*d A±-GÃÃ2œ‰R«BRÊä˜>|ŽÉï>ÇV¯Ì¾îzLÛfxr™Ù¹QËCc¸ž_¨Ç‘<·ºÄÄÔ¢ ”<²òLV/ßðf½Öˆ÷`ܰíî•R¨ê´,Ì O¼K–)ñLšöH OE#Ý´w©h„*2m‚ 0™XãötÃfbfíµuWëÊwnó™ORÐu|ªÆöš:4YÆ´-v×52›NòèÂj´íµuØšmã—çËE‚ŠŠ[‘6Y%—"±?Rá¥Yö×Váuˈº‰KsaˆU&æsqG_-¯ÄŸ>ÏñÔ(£[àõÊ$ :H*’ê&•r3¿”§»%È÷^™!nøbÜzp.ˆJ$/›ˆ¹~* ¸vI¾NŽŽ œüãD½çø/ŸØÁ{ßµƒw>Ø‹×-ò‰ÿp+‚O?7‹­mAT¼\ž2ùÀ»«hŽœgfé%vÔç© QÂÛ¶ÔòÜ¡Q¬p˜½ÿîݸ‚>ÀFÏyòeÖÆfhØ·•h¯C(­ç‹¤f—6Fg%ÍÔsýHšâ€«Rÿ0K:‰Éy¦¾ýC~ng3;zøÎóç™]£ÓWŃ [©òú°qÊ%Ã`:•``u òð«ÚäŽX͹GΞ¼ñ½øçnV.—Ãçóen`Ù2‰©.gF66ý…ø<±š2çÔûBh²L¦Tä广‹+èºHµ×̤’lïtÓqsèÂ2eÓdd-Ž_Õð©W¬$ˆDÝ^ú«kÙßÐLS0¼QɯŸóó$tË(a– ˜åõ“Ç]LsW¨–3™e^NÍs(5Ë׿éF¨óù@´Ñ“½@ÇÃ3s³äl›êª©òhx°lƒT©„)[Ä‚.šj½ôÇBœf®XÄ6 l³Œ(ÊØŠ›¹d«á–íµT×D(K¤–süÕ·YÔ%lSÇÌ­±¥Neq©ÀgŸ"]2 CÉíè¤޶ ¢äüu\ ”ÇÞ”_Ïv­K!X?‚me:1ÂRëIçd–ç†Ù½½†Ý;¸ëŽNÂa‡žåÿ~uSi#G:>O[£Hw‡ÂÒ"÷ßÚËÀÄÜßKWgŒÏ|í,±·ÜMÓþ­¢@f>ÎÉO‹r6OûÝ7QÊæ¹üøK,_Ã8¦¦“î5Ë:¥Lžb&ëï†Q(¡J“Fp˜ƒ^ß°•‡ž9Gj¼È;ºw°¥¦Ž Ë…(¶3z‘-—˜L'8³ºŒiÛ„4í+¿¾ûÖo?tþÔOdA,à>Z4M_B/9CC8îUÙ41,“wÝÔÆg8I²X èr1²gW·›wîoä¿>FW¤ M’Ec£qfS)·‹Óù³³³Ôx}DÜž3Pšƒ!¢/’ ¢[&Š(n´Ú‡ÝvF£˜©9¢¡ p¨°,rU§ñúÖX2,žŽOr\Ék©'•N31<Ão;7šƒšŠ¯¬ÔK‹G—§‰¸øåí}¸, Q¶‘\ŸŠ[•¸uW5ÿÓÜÅ/|ïjc!DQddj™RÞ@³mêüíø=^L ü>›okŽt°uô›gÎÆ)é&.Ÿ›z#ÃZâŠvÔ0ˆ.Ö3W’¢hoPïBEóMÚ›ôþ647ýî<ÇáñS$²nÊäEÜH‚àÜÔB"Y$µ–Ãuì(Cå2uû¶ræ‹ßÅßPMíönæO?=H×Þ~âS >zO4ˆY(!*2Šß‹öã úœÉA·æ M‹Õ±Y”‰iÞô®›1L‹…å47×´âUULËBE¦’ †W—)W’Bó¥<Ó@€å’i~udmùÕº-_@*Áú à˜ w- Ô¹¼ëîH‚€KÔ˜XÎqû–(‡O'©õXȦxc];ZÂx=8Òk¶ÍR6ÃøBœí»ö³¯¡ž³çÎU(õ¯>ýÖzÓ¶‘ãsS57›Û6,IW´š´·Èý·oÁ0­+TJéü XlÇê<ùòE¾?8ÇM·ßŠßçãĉ„ƒ«:w5I¢-èçT|5êâ­;»ùú‘ËÌËð‘-=ôÕŸ&8jÚ* ÙÜYžÕ´ïn$äsó\‘_¹µ›–f?[·ñ]¸¤'Ž.ñ…çÆ¸ÿŽ-(’ÈÄü*©l‘€WÃ¥*x\ š"#I‚ÓÚ#@¾¨³œÈ2½˜ •-R ÐRF‘%2ù¥²£{¤*^—ŠÇ¥âÖYBdYÄ­©(²X!Œpi2!Ÿ¯KåñW.Qv¹H¥Jxm½}ͦœG/aYNMC¾ÿÌe&F´T»YNd x‚ÜsS7‡Nrâä ÿ·ðô‡¹ûÎNÚZ‹ÿ÷sñÂ<üù˜95HÍ–ª·v2{üq‘½÷߯üðÕL¿ÀÁí­ììnØÈnz*ÀÖ G¹TÖ±-U‘ñz4r‡ž9Ëìâ™D ·¢òØðTI"ärSãóãS4Â.7–$aåR`S¾¡Iòdê~½eé'ˆm÷ÍòDUÉ5lWŽª“iF³¼iw„ ,rgK5¦i#‹î­ã÷¿:Œ¢Z|nDÑK&&•JáóûÑ+Ì뛳Ö"ÉÔxýø5íê´°mv»Qt‰T®H},ÈB<½ñ¼\Á™p² Љt¡™ÁH¹l–t:M{{;kKË• }Ó´ 75Vsï\O¾0ÈÁ½m¼õ`? «|hŽGž¦Îã¢)ì%pÑT㥷&ÄT6ËO °§¯‰³Ãóüöÿ9‹&;#– ÑÖZM,ä!›/±»·‰ªQXMåX½'¼ž¯’$‘Æê ½­5 O¯ÐZaÿ–B>—“n¯ÔéLÓ¢¤J:ù¢ŽašÄ‚^Aàüèg/ÏqçžN«CŽeaWO#²ÂÑÕ0¿ô¾âõzùáÈÉS§*€PDÑI÷ÚÎt¢&ÉX–Åj!G^/ãל9±âU+’Do¸†Ãg'ùåwÞL©ìÜÀÏeÙ¤³… îÞ¥Õ Ûwîáïxªªòµ¯} ò°±Éšå 0„ ÂUø½ƒ;h¿èçó‡.éÐZaWo#é|‘D¦Àå\‰‡Wñœ·é¬ Rp&K$Ò ¥2±‡ÀÖʺ3i©*Ò†5(Ë,ÄMÜ.Ÿ[C‘7'?`¨²ŒÛ%ã÷¸ð¹$Æ[nîÃíR®&«´Á‘|s‹ —J,Ȇf‹eÛ}.öô5 9ª`ëV¤§¥šT¦“‡¿~W@âÖ[[¹io½=ÕxÜ•:¹=2B˜ÚÚZ&&ÆEúÚ*î9ÐËÒt–¥Õ —†–)uTU²lÜÑ Ù¹e¦_>ƒmÛTõ·‘'ÉÎ.áº}7Kc³29´ùîÜ߃`‹•ä€i›$3WÓÌ-§H'‹¨¦LXqÓòr¹ñk.<ŠŠ[Q°l›jÖ`„¢¡óäÈ%‡7A€¼“y5¯© bß~±lYó…<‘*^>?ÏZf€¼¡·ím Ðh¬òrbl•d®L®dà÷¸9ÐåŸ^šw @à*ñùk—¸©iÞ´mòºN^×I 4·gcædO}ƒ–øÞKyË-}0rEb!/²$’Ìä+Sƒ6¯ŸÏw•Üš œM® Î87ÁT>Ç–ö'–ûÝ(ŠS —zåV¯×²ùÉlžT¦H®èÔ‰EFªÉçV¨ŽøÙÓÛHCU¹ÒÍš+”ÉKL-If dó%tÃÄ¥*ÔD}4×DðzÔ wssæ·¹&Œ( ˜–År"Ãôb‚µŠ;jo=ÐO*Sàñï =]U˜¦ÍâR†áËË|ý¡ó¸<Õœ??Àòò2ý[¶031‘#S˜–Åj:‡+)rúìuµ~þ][ùË/<Íù©$R±DsDaú™£d’Yôd‚ãßú!àÌšß¿» ¯[Ų ¨—972Ïì|»dS|ô†jP¢"É5æ 9Æ‹y¬Tܹ<ËBµlª57}Ñj:ÂQR¥eÓ¤=¥)楥YÖ2)A¸|ïµlø×M”@ßà7–ŠyÝ^vV716·Êï}õwnñËww°µ9DsÌó–HäÊÔ†Ý<|tšÕl w@ºá|ÀúwY0t–².¯­°.gbÛ6š(qKS+¦mñìĈ“Õ’!Ÿf©àNáÉ#ƒèºcrù­õ‘J Ww[…R‰¯¬®ñPvQÓœx½T¤WSùÄî~nn­eKOˆÎwð¹¥qj#~§N³’fh:NÑ’¨mjEU5†ÎÜDÉZ¢§9Jcuèºk,éçFXË(š¯/€;ìAUÕÝÐY,•8{zŽ“—fø•wÞBsmˆ§Ž qôâ%L[D$YAELÓÀ( »áƒ½ìíkºá=^ˆ§yòð «‚†RI­— y0ÇÙÝYöÖzž;4Æ­·´²–Èó§ù3sÚ6DP_9|t®È‘a‡¶S7%²£%>ñŸŸÁçµø§/~€Ïüñ]?9ƒÛ­²¼’åüÙ!$ÉKGgn—˶Èd2\d9‘ã¶í=?…¾drój}~|šÆéùY¾75‚»¦ŠêÖn<äJËi™”Ë:“SS\¸t–÷õïbhu‰¢a°§®‘”¡3ŸÏ!‚…c=æÿ9ëñºRYð%à}Eˬ-äèõ‡èU“)yîôË©‹üÏl§».ÀCG¦YN•h«6‘%—"!Н^ÕÈ•Ë<31BuG±hÛ†ÙÙY´d¯¢1—I2\ÎÑÐÒBÎ4àŽ®&>ô¦=˜’Å÷^ºÄBV!V£˜/paâဗ*Wœ%+++˜–ÅÖ›öãñx6¬š¡ëÌ,-ñ«/Ÿä¯åÜÙ×DSÌ‹>kbÛ6—§WYÈÒÒÖAMMÍU–†aÇ&_Ôén®BœL’¦*\œ!k¹Ù±»MÓ+sü›+†a077GÈï&ô04¹Ì÷ONÓѳÇ,IÏ]_¦i_]å ODzllkÁªC‹‚Àðô2Ÿ{ü¦¢µg;žÊ08:çÙlŽ3c£(æ"‚b³¼œå‰ïQ(ñù5î½÷^úúúX[[ãÄÉ“ŒÑÖÖNkkëÆƒeÛœ9}†µDœË£q^xiœ¡á<…‘ñ ]Û©¯¯»êžÙ¶M>_`llŒ'¡—ušÕ0>ÍigšH¬òÝÉË´lí'VØQ®Ü/Q‘D‰t*‰mÙè–‰OÕ0-‹Éd‚ÙRž²e ŒâxA¯i½n• êp¬KÅ{ú_8I¤¹‘h$B¹\¦\.cÆ(EQdaaÄZMQKÄÉëeܲÂb!Ët.àÌ|˜z-Ö~< NóYàEˬŸÍç\6â³xd À‰åâ6›º¢]eKcm-!~ïmý|vàÆóÉ‚ç—8¿¼@Cs3cccˆ¢H:¦IÖ¨9>7Å\!‡³³¸¸Hss3¢(’/ê<úŠLOOS.—Y]]¥¹¹¹òúW[.Ã00 ƒÅÅE+›[¢ªªŠêj‡¨®®¦šÅþæ…ó\ZÎл³ž“—fPÝ^¦¦¦ÐuQ¬pŠ"±XŒššdY& ÐØÚÎåéyzšê°m#ç§°$ñx|ãùÖ¦nVQQ—ËÅøø^‹¶†(GÎO20¾LS³‡ .\÷ÓÓÓH’D­ÇOG(æ0ìˆË+ XØCÀWyý8?@*±ÈyàëüÎr©€dÚliñq°7Ê?r‰åTˆlÁàûùøNqy!CMÈEmÈ$Y×}D˜O§¹´¶ÌwÝEcCgÏÃZK²»o;¶m3—˰s×.ª«ªX][cmmP(äHe ¤K{vïBUU(‹xòº bY6ရšˆåø•·6 ƒ‹/2::ŠeYôôôÒÛëœü>ðŸþÌgX[[# bÚ"ßzn€®mûyë[îEÆÇÇ9zì©d’ªª*Ü|3õuu¼ùž{H&LMMÑÝݬj躂HMM š¦a𿯠hNŠUE¤J¯Úääwîî`{g=Ëk™Škã\·aœ`qaYQèí饹¹‰@ À}o}+Ÿûüç) ˆ’ˆiZ»0IÚð¡ý€ŸµD‚cÇŽ1??KÓØ±c½½½ôöörï›ßÌO>I$ÁåvS(yù•WP•H4B_o/R%s422ÂìììFÏ› äòyL½À;ïØCÐçæùS£DºxÇ;ß…KÓXXXàØ±c¬Äãø¼^vïÙCgG{öì!NsøÈvìØËåf6»BÁ”¸ï"’Ïç9|ø0Ó33ȲLÀï'ŽD¾LGCŸGel.N$àáøÐÓËiDA(,¾ëñ¤²lœŒÖ;-Û¾IÕ4¾üü±€Æ/ÜÑÆ?>7ަHè†Åƒ{ø«Ç‡¨ò«Â™aˆÇ㘦ɇ>ô!XZZâ«_ý*ûÝ~îèêB‰ˆ¨! K³‘祫Zm‡'™)°¸–ÙÝæeZNÍc³ÿ,Š"Çç‰'Ÿ¤Ëð¦=-¼òÃAÒé7ràÀ‚Á »víâôéÓ„Ãa’Ù¶ào¸I’æ?÷9b±ÕÕÕLNMqyd„ÿüÏÓÐÐÀm·ÝÆ—¾üeÊåòFfGØÄm󾃤žâÂ… äs9ª‚ŸËËÅÈòŽ;oBÜt­ë=T³³³<öØc¨ªF)³Âå §xðÝ?GkK ÕÕÕÔÔÔJ¥D‰T6Ïó禹óMï$ð“J¥øüç?ÏÒÒÝÝÝ$S)zøaxàöîÙÃÖ­[yåða‰²$!Ù:K#Çi® sèDœ`ࣴ´4“Ífùö# ë:ÉÕeê£l¢÷ìm¥.äÂØ’ù"ÉLö®#¯s²96Å’¾‘Ò¼²© àëmÛŒŽQWWG:¡pû.Gÿä‹ÏaÛ¶íø|^º:;9räÈF@¼oß~ü~?º®óÌ3Ï`Û6===ȲLuu5ƒƒƒ?q‚·××SSSC0$N£—K¼®÷r¹ÌØè(---ÌÌLsï¾nÞÖFÙ00M'øõ{4Êe›Íòà‚ Édp»ÝÔÕ7 $ÚëB œ£µ¥I’ˆE£Ì/, ( £3ËÄ;iokàܹs që­·RUU€ËåâÈáÃlÛº—ËE[k+““h.u1?·ïìàÀÖ–>L¶¢kÎähGGÓ”¹ÿ`'kéüFãä3>»J¸©—ººZlÛæèÑ£,¯¬ð†7¼¯÷Š«xôèQ:;;ñûý4ÔדJ¥ÈårT…½È²R!±Q*…aA¨©©AUÕ obýС„¬¨dòeDQXþȼ^ëñdÓzøŽ Lš:õ® _|nŠoÿÎ-ìíˆðƒs LÅóÜÞWEgµ—ñ‘+½TÑH„žžÞt÷Ý”Ëe}ôQr¹û÷ïgdt”¯¼rŠöSÖM ÓiV®iX”D òÐMË9©¯ˆmÝpJÂ2M$Éáú2-›ÖúnQg-‘Àçó‰DÐ4l6‹$ItvtP*•X][CE.^¼¸ñzù|žd2‰eY¨ªJ4erjŠ€jÒÙTÅñÁ¹ Rƒu±UU‘$ U‘q)*ª¢°~y¶m£¨ _3µcW®q)½¥6ÌåÁÕ¬’ËåÂ4MEab>ÁŽ-›imm UU™enÎ!a0 I’(—˸\.ªkj8áéT‚÷ß¾…ÛZÐ ³Ò·uõ´ô‰…¼ô·×RÖMtÃAô£… 4wt: Ã`qi EQœM\Y¥Ri#›¨iÕÕÕ¼|ø0^Ùà-woå»G¦H$’TUÅØ²e †apüøqðz½±ÒzV¬³³“3gN“Í®"Š‚ |‡WáÇZ?1@*V¤ü9p°dš-3z–;›½È’À–¦ [šœ ‘ ‰¼takx=6°Ø½{7]]]„Ãafggfë֭ȲLgG'çΞá‰W.q×¾îF<ÁÙ%ŸAÓ4yRѺŽârýd)– Š%kãŸõÇd %r…2¢`S,PU—ËÅêê*‚ °, MÓøÐ‡>D!Ÿ¿.ÀŽD"¤Ûåbfj’_~û~dI¢¬›×Í3ÙŽÀ;%Ý ¨ëX¶}ÅÂVh”tóêúȵϗe³r¢K’„,˧j¡lŽD6{ðàA:;;7>+oƒ×ëݨËx=èi Qö3:Ç´,2ù"×^€$I¶À…±z*ŸÓ´l2¹"s+v„ÃVóþûîãÖƒ¯¾A  ËòÆ=žŸ›ãÁÛúp»Tª}ðÄóöw¼ƒp(ÄþýûÙ¾};³³³ 2|ù2SSS´µµ …0 'Ö«œ?çÿ ?Žõø©dÓþøs]°•­í1ün¥BãRñŸ­ë«Ÿcccœ9{–ûﻆ†Þ|Ï=¼ðâ‹lÙ²·ÛMWOgNQ]¤¯­æÊ¦‡ Óëóù(Ù*Ç'Ñ%ÿu`-íhåÙÒ†XüF]Ä´l.Œ-Í—Hd ×¹psssÎ0_åhE‘ªXŒR©tÝÆµm›••DQ$‘L"ت,38±ÈZ:p[fc³¸ša|>¾á"®»ºi²¼–¡¤×K2ùs˩늅W>—p±†×륶¶öúB€m³ººŠ(Š$S)²Ù,­u­•öygØÊ´ìë%Q¤®¾‘CgF¸<³Š @®h0·œ$™+ñà&ö•@ pCIj{Ó{§R)LÃÀçÖ°m¸e{+>ž¿û»¿ãà-·ÐßßOUUtttpG6ËéÓ§9ôüó477“J¥Èf2‚þ”×™Ö½výTR±"6ðà.îÿêáTѦ-ª±µ9Dk¬BÉæY&陞yæo¾çn»í62™ çضm~ŸÆ–6^87NuÄ·Ñh Vn¾$‰ôôõ3<4„¬×}>§1ЭÉÞ«ÿ½\.“Ëya`žÃÈuÜ'‰`kkk˜¥,Á`ˆB±H¤?|õ«_u€ð*ô;¶m³°°€ )ŒÍÆ©úÉ˯:JnY6†i9 †6ä %Ò¹"%Ý •)8Ã`7¸°RÙ W,coJ ›Šp‚`“Ë;Z¢(òÜ¡C;v —ËuC«$kkkȲB:W¨LiŠ÷~³õ­««Ããñ’ΤP]åå¢hÏç7R³=ô³ssÈò«o»¥¥%Ib~%EGCÔiÌ,YÃANž:Åá#G¨­©¡¯¿Ÿ¾Þ^Âá0·ß~;~¿Ÿo}ûÛèúF—Æ7ø1óÍë§fA* I(ìXΔ›þôñ ê—ÛäM;büâ]d‹–}õÌGKs3ǧµ¥…¾¾>î¹ç‰###ôööRS]Íêê*¯œ›Ä¶ ù .MÛ}>Ý==ÌÎÎ^Gƒ /餳E\¾«·ÙÔÔ”S„êí#‘L°´¸HÀïX™T*E*™`gw=ñT‘••8 >u¬ªŠúúú«ú‚Öë‹‹‹ Ün7«©Õaß ïÛú°ÒæÏl˜&kéÎÂÂÉD‚R¹L0 ›ÍR(ˆº- ùþpŒ†úzÇd2æææX^^&‘HP.—‰D"‹E ‡}¦*èq# wÞùFR©$ ‹‹,-.²¼¼L¼Rh¼í¶Û¸tñ"/¾ô8ÇÈߟ¬uµŸdý4ƒt`ÃÕÊlEqçé™s+HX,¥ŠD+T’¢("Š"333ܾ½‰›¶6ó/rælûö£ƒýû÷qòä)jkëj6÷nååSÇ8x1csÇÏßÍÇ>ö±ÓÅëõ"UÚÀé AÊ yPæ¥÷5M“;vÐÖÚJ.ŸÇïó :šˆÅR‰ï?ý4Q­DgS3 ÐÕ\E®Xæ»ßùïz÷ûijj¤³³“öövòù¼S¨’eÜn7.—‹/ù+;ò~ëj"~WÓ…¾õë.•ËÄ—æyï·¢)2†iö»ißÛ…(ÀÙËNÖïѸ´Pºê¹ËËË4żt6µ°O9¯Êu#LOOÓß¡6@E^yáü>?{öì&‹ñÀý÷olHpj!^¯—S§NñéOš7îicK{-§‡g™YJô¹éjŽ14x‰Ûo» —ËEss3¿ô‹¿¸g¸*Ügý7Cfuž·Þ¹Ùå?xò{¸Ýnúz{©¯¯ç]ï|'…Bb±¸ñ<ÇÃ~ð¾ñÍor×ÞÚê"\›§©­Ÿ[o=ÈþýûÈçóäóŽë©È2~¿Ÿ‰ÉIž}î¹õö“CÀ_úOÃzÀOÙ‚¬¯ŠISÜ›/›ž².ÓëoÀ%¨ÌeS465‘Ïç¾Lryš¿÷6ê«Bèz‰¼|ŠººFâ««œ?:ÀÂÔ «ó|èžlë¬gvq u9È›ªnå\i˜­[·ltÔ–ÊeÖV×X\\âØÑcüFÕ‡‘t¸{Ó´XHéÄ¢1J¥2–mãr» ø •Édç±ÇãÜéct68‚1¢ É;Å>Á(òÂáã¤2y4͵‘ öx<¬©é††‡‘ô ýmµxÝnMáòto¨Y‘Y[KpôØ1ºê¼ìíoÙÐIŒ<¸4'å™+–qi Ñ —ñÅ Ím‹E¦¦g8{†ûöÒ\&™)0—( GˆÇWanzœš¨Ÿ‹ã‹$2¶·WqþÂ&f US1§°C*fdlœøü$ï½{µ1‡9¥¿­–x*GGC £˜chbh,†ªªǪªb˜ÎÌ™3gˆz :ìÇçÑ1xåèIVéÇ®¿·¦9´¢©TŠñ‰ ÆG‡ ùݦ3r½–."+*ªªâv»ñùüø}>Ün7ñxœGy„x<Ž(Š“À¿.ÿ´À¯ðÇZ555àÄ8ŸÀ ˜Ô}uõܽµ•Ç/"* ‚(bè:¶4r×¾.r…2ŸúúQ¦Fód=Yj¢~j’M|¼óøÒÔø¶,óž»wrnt–ãOyÝ;ùÉ¿¥¦£Šlºˆ™7)Kyªƒ.V f–â|¸öýl õð©ÄßSKJÁωüYÚš« IѰ…QÄ%”i¯ô¹1M‹¡©R™ ÕAgVÞ´èlŠ16·Êb¢„¤ùpy} HdÓIÖâ˨’MmćϭR6Löö5 y9qi†±ùd%ço¹¹‡ê°Ó²Ñu'ëdÙ¦Ó"aZ\[à™ã#TÅ"}.LäPÈðºûݬ¥ód :^«2h’Íåyðö­È’ÈøÜ*Þ¶Ý4998ÍÀxœ¼!£¸}ˆ’B¹l€UÆÖófMð{\DƒnßÕNÀëââØŠ" x8|~Šñ¥È^T—Q’)•Š,.¯ âñ52ºêq#–J´×9 Šó+)T¼0Ñh—Ë…a 9RÉ„SoÂâãï¹[¶·¢'.MsìÒ,«9 ÕíÃãñ"ˆNQseu•xÁQCû5UU¿Z.—ùÿ@6Äü5ðÿˆ¢ ¼ã¶^¸¥›ÁÉ%ž;1Ê–ŽîÜÓ…$‰ä‹%þö‹Çy‡ú^žH<Å{üLÌ$©šÚÎïtÿ*Ÿ¼øØý“ܾ»¯}e”ÿØø›üñå¿FlZer¸ÈëÞÃóúØ·¿ŠsGòìvï¦ÑÝ€,<·x„µ¾ƒ/Œ?Ì@öm]z[«éo«åÌð,Ù|™7ßÔG,ìÅãR*’lñDžbI§:êÅ2a%™#àÕEHe‹¬¥sÌ,%™œ_cO_#5‘^·ŠªÈØ–Cè NKM6ï#«B^¼ò…é¥ó+iêb:ªq&ˆm,ËÆ4mV“EN MÒÓ£±&„¦È½«©“ kø½.ºšbÎøqå½&fSØXôµUQ6œiNG`Õ)J®¥ò$3yÎ Ï‘+–¹uGÕ¿C !ä e.Œ/pjp†|Ù¦º¡QȦˆfžW¢.ê'ès#‰©\‘‘yJºÅ˜¤Ñóž{ð×Çá4êñ3¼ó`?{Žd&OuØÏÐäeÃàž›zÙÙ]OÀçÆ4mâÉõUÁ±@’$òè¡f–4TÙÙÕ@®¨óøáFç“H’hàÔ;þ;?E×ê_ ›@R |ɶ¹Ç¥Éüü=Û¹8HUW‹£S,sä|†_ê¯AEjc~ü§g¨Žøh­‹:dØØèºÈä Ç'¹ç¦¢!/élÛ¶qk nMáñ#—ùÖóƒë--_~×ÀPò㬑dóÚÄí{Z¸Õ0ÌÚ±ùkÉwoå½òäŔك")\šŸáôбíÿ¥Å׈_ñ“£üÓÀ3Ìç—¹%p3ïo};ížf¦r³Üßp2Ã,ËsL-®q4~š=á­œNœ'¦ENñΦ·P0t.M."äÜxcwìkÇãr$*1î 7”(8$l²Ì6ïK{“Øn…ùÔÙtTGüÜ´µ…=½M¼Ž+äq)lí¨£§¹šXØ{ºœ 82 ‚¦é0n¿XŸÔuçÿ7×,…×pô­' l Dášç ðʹI‚5íìÚ¾ Ã0H¥Ó¬­®‘L¥Udž9:@]ØËwOŽQóà4îîÛxI‘Yš¤©\àÀ¶VÔ ƒfK­CxW€¿~Є.¶wÕãs;©o·&ãÖTdYâ…³“|󹋔Ê&‚ÀóÀ¿–ÿ%ÀÿY¬±.¿.—ӹRKÙÐ9#]â-õwð{íŸà÷ýw¬€A^M’ÎK)UPYÎ%ø³Kÿ@PõÑêmæÍµo Qhåôü©†41-B° Etf´Ãb„ø$üÍŽßå±¹g9¼r¬‘C· Ú|èö~NfNR¿Ûæ¾[o"ì÷Tæ1Ö-@央´´Ø6$2yæWRä eÜšBgS ¦l¬´Y‰›x¥××™¦êæ5Æ–)8ºªõêÅ@Á!œ[ÿýFK”6Æé1+ z­K@Q¯ÿ÷²n’Êð×ú¨¯­¡P,¡(2å²N¾PàÄ鳌ONQõsrp†L,JïŽnIºêäØÌæhèNw² ÙW](8LÙؤ׉(Àñ¡9¾ñìE %AοÌüKnÚq WÕG¦*?oEÑ›±s´¹¹£úr3‡ã'É{V±s·çI”S(¢ÌùÌËæ ©%öGw2–Æ´M‚J€Ûªöâ’4›{†_3÷×ßÅýuo¢7ÐÉ7¦ã–ª=tùÛøÜØ7H–ÓÜQs€ß:÷”"q(« N.²°–br~¹•$ñTnƒg A@‘D’™ß}ñ«©eÝ$žÌQu24ëË0œ/ró)nšqº(:§½Y邹jO¸òk½gmÓÆÑõM–äG€hý5+½zˆ?a XÀ‰UFgV8?º€?à'•ÎP(–XK$8yæ—GGiªòóž»¶sin ÏÍ»¨ÝÚ‰¨\}îμr†‰Æš0ŽþÊzGˆTùœãs«šnÙކͥɾðÔYâ©<¢ ,âXŽÖ÷Õ¿äú×t±675~ ðŠÿs¢0ãû“‰ÿC­»ŠÝÑìˆng(9ÈøSVKI>9ðWtúÛÀÐ$•íá>=µ4yêyiå8 ´Óèätâ"†e°XXà{sÏò‘¶wñìÒaæ KüZ÷Gèö·°ZNR2u.¤ÉЋܷ¥½ ͨ¢TÑJtT®J¦A®\&QȳÏ2>µÊÉc3nèn¯âž›{ ùÜWÎמð¯wSnÐúkª¯CiÍ4¯·‚pã~"Ó¼Úò\»lTEæ¦--¼xöe­fhjé TÈáŠ|ô¾=ô·×Röqd`;@ó{¯L{®g/DPw+Çžy‰]½M¸4Åiѯ\ë÷^ºÄÃ樺c?±-툲Lþ† À¥É8Ÿ{â ‹kYDAX~‹ +â¿48à_ ëUSScÿp‰‚øC¹qÏïÿ9Ýÿûl láñ¹gQÜyBŠ@§«¤ž¦Í×ÌRq…§çŸG$nŽíæ®Úƒüåàg¸»ö6 Û@œJ¹$H¨¢‚Gv‘Ò3¼±æhx#GVNñw—¿ÀÙìI¥475¶r ±ÅÙ0›†ª$QDEFÔ"n}UµØØ Õ|žÇÏ]ääÐù÷ÝNCUˆRÉ î¯õã_¯›³`WqµhËòÕnž¸É}6áë`zµÏhš`"=Í5|š“ÒN刅ªi­‹ {+ "ñd%èG”*Ú–E9[@ó;óØ1>ògV]àèÀ$ L»m¡¶zDE¡”Éá*¨‹¸8±Â?>~šùÕ ¢ $€ßÁ!}³ÿ5ÀÿJ1ȵ«“X8 Û%QΗ–•3é‹ìô÷QÐ Zy ¿¦R¯´Rç®fK°›Þ@'Óùy:|Í|´ýÝô»Øê&¢†øÖô“4{ë9ÝEPõóÜÒaŠf™Ñ̽åxþo9>IÄíåMÑ·0•J0”˜åÔü,Gf'ˆz¼D=^l S*QÐuÉa‚Ì:Š(‘7t–æ° 6y[g{w½sPJÎfÓu'¨~µlØ 7£QyÎæocÓf¾66±,ç}6¿Çºër£ oYP.] ˆW{즷ÇÜ.M•‰<´ÕVÓX&v#W‚QgVóëjÆ`”ÊœûúSD:QÜ.dE¦vg/£ókŒ½À®îz–W3|ê©Ó(;û 4×áŠE‘B"ƒ6:IMÀË—ž>Ç‚c9ÀÄñ<¬-pÀ¿L7ïkZ•‹,ã Yý7I³çÒCü‡‹Ÿ¤ÖWÍÏ7~˜©µ +ÅU¶»xeå$#™ ºým,–y|îYtK§7ØOöҳĴ0š¤1’™b¡°Ì_¤?ØE­«Š¸üe<µüZû/óÿv’OôþûB{˜‹›\\Š3JR®¤‹tË$[.q{D‘d±@QבD‘x>‹nYÜÜÔÊZ2Uq6m“«fÈ-ëJj}]—έlÜõícË%çgsVçF†¯šåªX·×bÑÁ°’—& (Ñ ‡šˆpPÃã¾æLÀ­)˜…¦îˆjÊ.r®ÀÊà‚èÔidMaûî岿å‡Ç†ùö ÈÖÕâo¨Âò;™/AÀ6 –W³|éûàXw«¾À¿28àßÈ‚¬¯Š%1@J„[–Ëk®Séó¼¯é„öðÒÊ ¶‡ú88Ï©µó(¢ÂöP/“¹Yn­Ú‡Wvccóäüstú[Ñ$•ÿzîϹ¯áüvß/³5ÔÍW&!¤ùO}ÿŽ_+½Ášzœï­~›Û;¸³½{;{i8í$O òÈðy¶U×!nYÁtò¾T{ý$‹ždKO»z®.Þ‰7>7NÓ)þmöÿ7ê%ö•ר¼Ö3aëÁ¾(:®ÔkµPàd‹$©ÒìXEÝ0Ñ+U’$²´–æäÐ4ÇÈJDüy½Éóú7gíøÄ2±þvÜ¡²K#·’`eh‚†=ýÅVYS]Ï|ãfu¨Ú·-èölQ@”$2ƒ“¬ NQ(êëùoRaCü×üÄ ×®JL¢ÿd%AüÓÉülõï ý ¿ßñïùÍÞ_âscß$¨¨uW‘7 LdgÎŒóÃÅ—ø`ëÛUtd…¿2ñ(o¬=È»›ßÊ\~‘óÉažœžÿþôïQ´ 4j͸E"}±j¦k\\^ oš,“Bw µ5!ò¥"õþ@eN\ØøQ%‰b…@íÚ%ŠW»L›ý~¸¾¸¸y­×®ú7áµꯖ,GƒðÂøºšª9ra’¡‘¥’Ç+ÈŒÄì^j„qc‘çƒ'ˆ„UEdwo==Í5W½¶eC[Cß+C¤V6Ö ú<Ôíêaúð9Ò³K„Zë°+“’±Îf”æzðº­AEaî¥ÓÔß¼äø0Ë/œqtTî'€ï®ï“‹õojAÖWÅ’ØÀ9`T„›rF>ôrâ$½v„wòÐÔãÜSw;E«„në,– ª~öGwà–=<·xŸìe 9È–`7Céq^^>ÁãóÏ0”½ÌbiI-Ñõa©iʳx‰3É8K}-Ô¿ã­Ô½ùR^ O4B1“¥*w¬ TD-Ö±Ù)R¥Cçàζ |} \¥½v­[‚åî¼ÚFÿç–¡WbéêØ$›/ñ•'Î23PKmñvŽ_˜ã{'LÞ̇ZŸ>ï­L-¤øîÉñQ‹GñÑìâÝ]ãðås,/4˜·ð¹!,%C{Côª‚GSÉgòœž#ÜÙˆ;äÇô‘œ^$5»DÝöîMd“™[&5'ØÖ€žÍ“›…dŽøáóØeáNãûðoø\UL΂°«lëµÇ“ç°Ùùî_ ÑSË£ ¡jA)Jƒ»äæäê9æ‹+\H óÊêQN¦2oØì«o¦¿ªY”(è:;kÙY×ÀT!Kº³‘ÿÑ'Ùu×ÌŽqø‹_Ç8uËCCt„£´…¢”L£b=DdQ¤'V(ˆ$ì<·ïvXÏ7¯uÑP˼ނlüÿX•¡½Š«&l´ƒü³«¢Õ&]Ç^¼ˆ{áv~c÷R4òŒ® 3¼0I[°›:_—“™H ó«»þïïûÝ‘-¼0ý4}Ÿæ§dˆº«hõõóíSOSWã¡.ÜtAÐTbðì( ™"†j\AÞXˆËO½B¨¹o•ÓÑk&™…8˃Û›ÈN-=?Ai~Õ1G‚ððËÀ1ø·ü ®«¸¿$ tXØ—ó(ŠLƒVÃBn™#k'ðk*çVG9ŸäÉÅr.uáÂ%j‚}µ!¶ÔTÓ­¢)¢Úë§Æ +£'VCØå!YÈóJ&NÏ·‘O¥yù‘Ç8ô©d—è⦆z"1ºcUxuC84U*2_âðÌ$Gæ&yëúèi­¾áæ5 'íú£*àë˶œÇ¯[ÇÕHf <öòE …õUÁ«ž£ë•2Ã&l®Ç?àtÓä駸³êç¸?ÅW/}šžèVÞÛ÷QüÍš~Š'Çæm]äö¦{h vÒì`WÍŽÍ¿ÀÝ­ð±í¿Î‹3ßçÈü!îiz/Ç'ð†MÈõ¥©2íµa޼pŽœ$á ð×Ű “‘§SÝߎæó`–u2 +,]CQd²çDZó%p¾‰Ó[uþíÁ?c«@²<øDAÜ–µòò kÇ9Ÿ>OsÀOÌ㥷*F}X!hØU[OOU5õþa·E’p+ a·‡ Ë…WUñªŽh¢X ›É03p‘¹—ŽÂà·W5°§¡™*¯—†@—¬°’Ër~iC£¼45Æl:ÅR6ƒ'¢ðÁ{÷ )2¶ålòÍýOÂ5ÁúzvK¼Á·*‘d6U›EÎÌóØTž±œ…»¡­!ZÙüÂF]cýõ×¥ Jº±)lsq|CÇ'™XâÐô“ÜÓö6în¹Ÿ=µ·°½j/¡ž›~’¨»š Æ#ûð(^$Qâë—>KÌ]ƒ"iäõ,¿¾÷÷‘M/Î~Ÿý[š6Þ+›w(Vƒ^yi¹*‚âÒˆv6‘šYbîäEj¶vb[éÙe–Ž]¢4½Œ™+€ dÿü`~6À?AúÖ¦¶”%œß0ð{ˆT²C.vgC !Õ……@K‚ˆ$ŠH•@Ú°,Vò9GþyÓnÛ6²$Ñ S`˜&²(¡UˆËrå2c‰U&“«L&×H‹xUF®HŒ¥l–‘ü2?ÿ^‚^W¥òîlrõšnÚ«Z>*é^éÙ§u ]ÌÛ´7F‰]\ öî7óø÷ž$›»@cuˆúêá w£™RÆçã<{|œÔš@™,^ÍC¡¤“Mhü矢ÞßÄþ´‡ºÑ$ ·ìl23xohº—NÌ3Sß#â®âôò1¾;òOì­=ÈÛº>Èmwóöî,&8»|”ÿrËŸsGÝÛùæ©ïÒÕáÁïq‘ÉÉ LË"ässñÒ$Zk#²¦ xÝd—VYeéðR£³Î…;íê¿ Îhbˆ¾èvúc;™ËLÑé§9ÐN@ bÛ¢ ÑhE“4 Ëà냟á]=¦?ºÃ6»bxC«çið·phú)Z‚4ÚùÁôCÔµÑMY)é¦C2'ŒÍ­RŒE±2Æ{™äåǺ;Ì¿ü-‚Ÿ]pÀÏh r£µ).Éã4:žà÷û†«Ê\6ÃÚFn©mÄ++X•¹n€¢¡“.(y½ŒGQyyzœs‹sDÝ^,ÁfÆHÐÒæŽÆN:cÿ_{çW}žñßÿÜö¾«Õý.˶,,ËHØÆ _(8„¦’PhZ‡@˜B&6é”™öSÊ43ôKJ:Z¹ ¦Ž1ƒm0¶±Á¶|·d˺Yë²Z­V»ÚëÙ=§ή0²MšI;ƒŒŸ/»_ÎìÙÿ™gÞóÞž‡š²\Í"ZnÓ-“Í{~r_ò%'hæI }z`0|•Óž»°dšàqÙø£›ÍþVó¢ ¶îÛë¦j]UË›pÔÖ°¯ý5¥^ÞéÛÂþ¡¬,_Cké*^é|`"Àß­~šeÅ­>º¸‡hjšŒ¡c`0™œ Ð^Œ@` ¨òÔÒXØL½¯Ã4…B¥»š®P‡FöÒXÔL45Í™‰cL%C |Èèn\™L÷w®©Í©Ë˜Ì$t&Ú» ω§’H¯a xtåŸéçó† ðÉæˆrøð°$Ä߯2úÂ÷‡û9C5L44I&•É5?1»T„„CU¹·¡‰££C¸ü«–ÕP_YHÇI™ßÏm·¢Í%€¦i½™\¾s‘‡¬÷+¯ì^NÈõJ®¿˜¦5aœÖ3èY§]cÓÝ+ qúø‡ŒíÝM‘$(//àÈñ-¥_áüTå®*©iö î ­òvª<šæC2EŽR‚ñáTˆ¾éóìÚI8šà‘Ƨh*jåG'žàì…qÜNÓ ÆÃ)’ÉBIœÁRÿH\ú,?ï˜WÉã’hÁRÒÛ+àÀ‰¸Ë0M*nZË*qË ²°ø|•K–$ºNghŒ «¨+÷ã´kTy°Û¬E…+õå®4$˜“ªz¸z”°†öRé ZN™ô\¯äJzΙŒÉàX„x2…¢HhªŒÃ¦²áæBÓ1öëçHÇeæî\p“‰q¼6?“‰‘t˜Jw-ïômamÕ—¸±t%•îñQ’Ù©l›ì@ ŸÍOÆÐ±Év\ª‡™tá–(rXN¿ËŠ[¹¡¨™ýÛXà[ÌwZÿM¶ñôÁ`Wü¬ó_Èh!¶íDµÙ‰¬i"$1…5Gõ,V9ûüæ æ%Aà²hrk4a›âû²mcɸôÎpunMEÙ¦I4•"Ÿá\(@i¥›²B§ªR”H§¬×Ks )·_®i¹fõ¥YÆÕï1/ô Ã1v|ÜÅÈd‚¦:?wßÚhåFÚÕ¯O¦ bñ ª*c×dâIX"MÿÈ$;öp£}#îh?]Ñ3¼pê_éu²ºò6fô(k/faA#Û{_£±¨™…tNž$™‰3 £H 6¹œ…Kr3b.ÕCLŸÁ®ØñÙ f#nè|8¼›¦¢Vª“ZO=e®JöíäÄôAœ. ¡Ø0L˜º€÷±œÇö`U"ç1ò˜·Éã’h’¶ûMžHe³]áI†bŠ5;±DE_ƒæÖr–.(æ*ø½NY²¢ÁœW# H¹äZº|ûUS¯¤+m ¦¢qz†ƒ ‡9Ô1À’e«yô±¯ðŸÏÿV.Ʀ)ŸYý²Û$Tà°Ë¨ŠåŸLgxû`'õòZVU¶ÑqúwÔÝËÝõ÷Óîâõ®ŸóÑÅ= G˜NO±¡v#E޲F–EÄô(Ó©0z6MÍOÖÌbYIEvÅA:›Â£ù°É–—‡$$:‚Ç &ƹoñƒØ ‰êFƒŒ§/’ôDI’Là–6îë̃$üwaÞ.‹&XBuo !6Å3™Ú=ŠŠýÜ~S=µEØ5k#Îi×fææ ªö;VÂÅ'?VõÊÙu¸‹=G{¹8¡¶¦¯»]×ùŸ½ÄòºBì6å*;â“qtÕDUm³}¯[Æ™É ¦YS¼ŠýC»¨/hà›ËþŠjO 5Þ…l=ÿ2ªd£Á¿”¾ðyÖW™Õ·&•žZŠ‹±Ø0 |‹±+Nbz”¸£ÔY‘ÿ;øl~“¬™eGß–µàÑ|tLã­ÁÍô&;ÑM=o,tkgãWÀðÜg3_qM$9DéþxUXDy¨î3ê ¹¾”µËkh¬+¶„ËL¸Ü-*÷å É«( $Ób‰)=ËÅ@˜7öu2ŒS^VÆ÷¾ú-ÍMd2n[s+ß~ò¯©-õ" ‘Û1ÉýD.GŸŒ22Á4MÊŠ<úœéd<¥¾²€šr?É(v…‹3ƒÜ³ðkȒ峸wè]<š‡›¿K >‹§~B >Jµ§YRñÛ‹XTÐH÷ÔYZJn&Ma“íHBÎI™$2qJ]¸TËÏD‘dúÃ=œ˜8DKÅ*~zîº#Dõ²ˆ^àeà—¹3ÿÔ³˜ï¸¦’Ç¢œÁª»¿l‚?ÆÓõÏ qôü( + ¸ei5-‹Ë()p!ËbVˆAOçÄ lW¨2Ò³L'f˜žI’Éœ°ûh?å•5Ü·ª‘¦%‹yü‘‡ùåo6sËÊ›¨]PǦ‡b÷ö_±â†«/`š–íÙL‚·t2™ ¸TA’÷f Œéd'«¸§îÏ9Ósœ÷Œs\Ÿ"á²:Ϊ¤ÍÚ\Î Qî®Â©ºh«¼ƒí=¯±ë›4µ’ʆ™LXQÞÆË?%’ž&™Ià³ùQ$«0‘1t¢éiJœå8º‘æÂt¿îyt7CýèF !$Sò9,§Wî¹g­àš$H—ÅÄJäŸ^‚„ê™ì²³‚J×à$Ňœ,¯/eEc‹« ñ8l¹5ث̙KYF§¦,ãO F&"ì?=ÌâÅ7ðÕûîf"8É·7}ÃšÊ OSQ^™ ­Ë—±åuƒî¡ O÷‘ÎdqÚ4†'¦X½ÖÃÿr gB³¥ßïþͼÙb–—¬äÏ–>ÆtjŠçŽ?û~K0>N$5…nX{ºå®jΣ:ÕÉßâÇí?dßðNZJVñrçó|mÉ&‚þéó4ø—â³Ycè’Ð ˜C‘Þ»¸FÞ¦}âÁä8H5´$äcX¤xkêúSg}­a^tÒÿPÌY™ÄJä·B (‰%Òö¾‘0G»F9Õ;N CQÀíR±k2²dY¥å‘ÉLFb9m[‰#g‡˜ˆ|ÿÉ'…ìo»…†E ‰F£´?ɺ[V£©*áH”Ýïí"09ÅH|”¶õ(.ûï¯æ;-ÅçÑPÁ¡ö/½ÔÏýkVP\ÿݾ“*×Büv?oöü†JO-5žz©MÅ­Ú‹±«¶uÿšB{1ËŠ[ñå’ðmݯp{í=¸T7åî*BÉ ÎMž¢¥l5ÅÎRLLB© G&ö³óâVÚCûy{h3gçHdfBÀÓÀ?pIþy›Ÿú¿Ä‚ yÌ!Ê ÖäVà!DHá5 ÓŠ&ä®ÁIÚÏYd Πg 4Ež•ºQ—]³r ÃÀ¦ÊôãpzŸò¯ßMÓ˜‰Å9ÕÑIÛÍ+Q4“g:9rh/×ÜÀt8þF DñØØúÖ [Þ`óæAFûom¡yQ9åE¢f€:»a&åѿDzâVvômᆢ唺*pªn™o÷mfeYåî*Üš—¡h?gƒ'ydù“øsó÷·ã¶yM ±åÂ/xñܳlx…HfŠ=‚™”„tRñ_XÓ¶Ïa9Ï6ú®ebäñ…"Hsˆ¢ƒÀ.,²´ !âBOÖ0¼¡hBê ÑÞ5ÂѮ·Ed²&»J¡Ï‰ßã@Ó ÜvŽ?Áàh€Û×­§¼¬”Ññq>†õkÉd³<ûÜóT¸R¬ZZK™ßKÇÀ êœ<cqa¥ÎbZÕ±¢±†B¯“÷ô°õÝ!ÂÑ8C‘~ÎŽõPë­§µôfüMœ&gIá2<šM±L8:ö·ÕÞ…OóãTݼÓÿ[*¼5Œ'/rhb/‘c îáý‘·8j'”šÀ0³i½Bˆ×±"Å3X“¶#X M_bäñÿn0_Kèó  ø2p+Poš¦ÝÈ )Ûm*E•%êÊ|yHYÐÑ7F &sSëM\¸ÐÏÀÀ6ÞûÇ„¦¦ê9Î#÷®Àã´Ñ~v˜_ì:H¡ÇÅÆ[n¤²ÄÇL"E2!›5°Û6¿ÝÍã ~ÌP´-ãÏPWí¤ï¼Ì“ÍÿÄúê;é wñ£Ÿâ/šgCí½:'Oðó3ÿÎ×—>Œ×îåøÄÇìÚN– )#A"Ÿý£ê>v†q4^ôù|³bE×j~ñ¿Áu‚ÌÁ¢€¥V´k±H³(5LSÉ º©²œ“éWð:m¤R) #KKC%Õ¥>z‡ƒz¬k©Çïu¢ëYz†ƒœ» Ä了Ô7; ©*2n‡†Ûiã¹×ްÑóܵðOxêÀ7YwWšcgGÑFÚxpé£xl^^<ýº§;YS}átˆóágzII 3‹nè—:ÿ¦b èÀ"ŬJ߀ªª SVVö…&F× ò¸YÀr̪š•@+°(7MÜ`Šü–Ÿ,²,P§]ÅiSqh N»ŠÝ¦ ë–ó“Ó®RèuXK_’˜ý4M“c]ÃDFý¬«ù'‡ Içp:lŒg(°¢ib™âúŒÕËÀÌ5.%„% F±&hO` yv`½6%/ýc× q9®ä÷ÀUcŠ:` Ð4µ@)à3Á‰iª&ˆ¼ñäÿ…OË;= L Ó@˜bvÌ4M‹‚´@Ä0ÖšòVo¢ «Ã=ˆU½ÓçÞüuR|6®äÀUVÉ‹Eœ   +êTaÙÑ•~À¸; å®ËyRY–"Xƒ~i¬êQ +"„°Ä ưÆ:†‹X‘bËÔ2{¥»NˆßÿzÎ#™×ðø%tEXtdate:create2019-03-21T16:24:16-07:00…‰Sà%tEXtdate:modify2019-03-21T16:21:05-07:00ï¿:IEND®B`‚kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/000077500000000000000000000000001476411216400242225ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/apps/000077500000000000000000000000001476411216400251655ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/apps/apps_suite_test.go000066400000000000000000000013621476411216400307310ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/apps/kind_visitor.go000066400000000000000000000046521476411216400302270ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/apps/kind_visitor_test.go000066400000000000000000000120341476411216400312570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/000077500000000000000000000000001476411216400247655ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go000066400000000000000000000030471476411216400264050ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/annotate/000077500000000000000000000000001476411216400265765ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go000066400000000000000000000404101476411216400307350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/annotate/annotate_test.go000066400000000000000000000523231476411216400320020ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/000077500000000000000000000000001476411216400274715ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources.go000066400000000000000000000214741476411216400325340ustar00rootroot00000000000000/* 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.NewString("", "wide", "name") if !supportedOutputTypes.Has(o.Output) { return fmt.Errorf("--output %v is not available", o.Output) } supportedSortTypes := sets.NewString("", "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.NewString(resource.Verbs...).HasAll(o.Verbs...) { continue } // filter to resources that belong to the specified categories if len(o.Categories) > 0 && !sets.NewString(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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiresources_test.go000066400000000000000000000221731476411216400335700ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiversions.go000066400000000000000000000060031476411216400323610ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apiresources/apiversions_test.go000066400000000000000000000051631476411216400334260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/000077500000000000000000000000001476411216400261125ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go000066400000000000000000001061441476411216400275740ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_edit_last_applied.go000066400000000000000000000072021476411216400333150ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_set_last_applied.go000066400000000000000000000166121476411216400331700ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_test.go000066400000000000000000003757421476411216400306470ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply_view_last_applied.go000066400000000000000000000130261476411216400333430ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset.go000066400000000000000000000560631476411216400303140ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/applyset_pruner.go000066400000000000000000000130371476411216400317010ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/patcher.go000066400000000000000000000366231476411216400301010ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/prune.go000066400000000000000000000105571476411216400276020ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/000077500000000000000000000000001476411216400277235ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/000077500000000000000000000000001476411216400310545ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simple/000077500000000000000000000000001476411216400323455ustar00rootroot00000000000000manifest1-expected-apply.txt000066400000000000000000000000541476411216400376370ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simplenamespace/foo created namespace/bar created manifest1-expected-getobjects.yaml000066400000000000000000000004741476411216400407740ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000001601476411216400350360ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.txt000066400000000000000000000000551476411216400376410ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simplenamespace/foo unchanged namespace/bar pruned manifest2-expected-getobjects.yaml000066400000000000000000000002331476411216400407660ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000000651476411216400350430ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simpleapiVersion: v1 kind: Namespace metadata: name: foo scenarios/000077500000000000000000000000001476411216400342545ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simpleerror-on-apply/000077500000000000000000000000001476411216400371425ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simple/scenariosmanifest1-expected-apply.txt000066400000000000000000000000541476411216400445130ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/apply/testdata/prune/simple/scenarios/error-on-applynamespace/foo created namespace/bar created manifest2-expected-apply.txt000066400000000000000000000001741476411216400445170ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/attach/000077500000000000000000000000001476411216400262315ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/attach/attach.go000066400000000000000000000271451476411216400300350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/attach/attach_test.go000066400000000000000000000462641476411216400310770ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/000077500000000000000000000000001476411216400257265ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/OWNERS000066400000000000000000000003601476411216400266650ustar00rootroot00000000000000# 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/auth.go000066400000000000000000000023321476411216400272160ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani.go000066400000000000000000000331521476411216400271730ustar00rootroot00000000000000/* 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.NewString("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "use", "bind", "impersonate", "*", "approve", "sign", "escalate", "attest") nonResourceURLVerbs = sets.NewString("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.NewString("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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/cani_test.go000066400000000000000000000233201476411216400302260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/reconcile.go000066400000000000000000000255741476411216400302350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/whoami.go000066400000000000000000000175651476411216400275570ustar00rootroot00000000000000/* 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.StringKeySet(ui.Extra).List() { 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/auth/whoami_test.go000066400000000000000000000222021476411216400305760ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/autoscale/000077500000000000000000000000001476411216400267455ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/autoscale/autoscale.go000066400000000000000000000224411476411216400312570ustar00rootroot00000000000000/* 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" "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/genericiooptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" autoscalingv1client "k8s.io/client-go/kubernetes/typed/autoscaling/v1" "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. 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 HPAClient autoscalingv1client.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.HPAClient = 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 } // Run performs the execution 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) } hpa := o.createHorizontalPodAutoscaler(info.Name, mapping) if err := o.Recorder.Record(hpa); err != nil { klog.V(4).Infof("error recording current command: %v", err) } if o.dryRunStrategy == cmdutil.DryRunClient { count++ 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} } actualHPA, err := o.HPAClient.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), hpa, createOptions) if err != nil { return err } count++ printer, err := o.ToPrinter("autoscaled") if err != nil { return err } return printer.PrintObj(actualHPA, o.Out) }) if err != nil { return err } if count == 0 { return fmt.Errorf("no objects passed to autoscale") } return nil } func (o *AutoscaleOptions) createHorizontalPodAutoscaler(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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/autoscale/autoscale_test.go000066400000000000000000000170721476411216400323220ustar00rootroot00000000000000/* 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" "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 expectedHPA *autoscalingv1.HorizontalPodAutoscaler } func TestCreateHorizontalPodAutoscaler(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", }, }, expectedHPA: &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", }, }, expectedHPA: &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", }, }, expectedHPA: &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", }, }, expectedHPA: &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), TargetCPUUtilizationPercentage: nil, }, }, }, { 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", }, }, expectedHPA: &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", }, }, expectedHPA: &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.createHorizontalPodAutoscaler(tc.refName, tc.mapping) assert.Equal(t, tc.expectedHPA, hpa) }) } } kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/certificates/000077500000000000000000000000001476411216400274325ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/certificates/certificates.go000066400000000000000000000242021476411216400324260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/certificates/certificates_test.go000066400000000000000000000250131476411216400334660ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo/000077500000000000000000000000001476411216400273225ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo/clusterinfo.go000066400000000000000000000114241476411216400322100ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo/clusterinfo_dump.go000066400000000000000000000231511476411216400332350ustar00rootroot00000000000000/* 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.go000066400000000000000000000040221476411216400342110ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go000066400000000000000000000453031476411216400260640ustar00rootroot00000000000000/* 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" 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") 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) 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), debug.NewCmdDebug(f, o.IOStreams), 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) 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/cmd_test.go000066400000000000000000000374501476411216400271270ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/completion/000077500000000000000000000000001476411216400271365ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion.go000066400000000000000000000157741476411216400316540ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/completion/completion_test.go000066400000000000000000000046141476411216400327020ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/000077500000000000000000000000001476411216400262325ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/config.go000066400000000000000000000077671476411216400300470ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/config_test.go000066400000000000000000000761401476411216400310750ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/current_context.go000066400000000000000000000041321476411216400320070ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/current_context_test.go000066400000000000000000000043641476411216400330550ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster.go000066400000000000000000000045511476411216400315710ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_cluster_test.go000066400000000000000000000052661476411216400326340ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context.go000066400000000000000000000050731476411216400315740ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_context_test.go000066400000000000000000000053151476411216400326320ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user.go000066400000000000000000000064771476411216400310770ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user_test.go000066400000000000000000000116301476411216400321210ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters.go000066400000000000000000000033521476411216400312670ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/get_clusters_test.go000066400000000000000000000037731476411216400323350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts.go000066400000000000000000000123771476411216400313010ustar00rootroot00000000000000/* 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.NewString("", "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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/get_contexts_test.go000066400000000000000000000124501476411216400323300ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users.go000066400000000000000000000045161476411216400305670ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users_test.go000066400000000000000000000035101476411216400316170ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/navigation_step_parser.go000066400000000000000000000113331476411216400333300ustar00rootroot00000000000000/* 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.StringKeySet(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.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.String) int { for i := range parts { if valueOptions.Has(parts[i]) { return i } } return -1 } navigation_step_parser_test.go000066400000000000000000000053661476411216400343210ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/rename_context.go000066400000000000000000000077101476411216400316010ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/rename_context_test.go000066400000000000000000000116501476411216400326360ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set.go000066400000000000000000000166471476411216400273720ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set_cluster.go000066400000000000000000000155161476411216400311250ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set_cluster_test.go000066400000000000000000000155261476411216400321650ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set_context.go000066400000000000000000000117771476411216400311350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set_context_test.go000066400000000000000000000124571476411216400321700ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set_credentials.go000066400000000000000000000376361476411216400317500ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set_credentials_test.go000066400000000000000000000313771476411216400330030ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/set_test.go000066400000000000000000000051061476411216400304150ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/unset.go000066400000000000000000000055171476411216400277270ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/unset_test.go000066400000000000000000000110471476411216400307610ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/use_context.go000066400000000000000000000054361476411216400311310ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/use_context_test.go000066400000000000000000000066161476411216400321710ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/view.go000066400000000000000000000124451476411216400275410ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/config/view_test.go000066400000000000000000000201141476411216400305700ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/cp/000077500000000000000000000000001476411216400253675ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/cp/cp.go000066400000000000000000000410611476411216400263220ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/cp/cp_test.go000066400000000000000000000603021476411216400273600ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/cp/filespec.go000066400000000000000000000074341476411216400275200ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/000077500000000000000000000000001476411216400262305ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create.go000066400000000000000000000337761476411216400300420ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_clusterrole.go000066400000000000000000000170031476411216400324460ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_clusterrole_test.go000066400000000000000000000326701476411216400335140ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_clusterrolebinding.go000066400000000000000000000167521476411216400340130ustar00rootroot00000000000000/* 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.go000066400000000000000000000044361476411216400347670ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap.go000066400000000000000000000325111476411216400320470ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap_test.go000066400000000000000000000343031476411216400331070ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_cronjob.go000066400000000000000000000145031476411216400315410ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_cronjob_test.go000066400000000000000000000061111476411216400325740ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_deployment.go000066400000000000000000000206141476411216400322650ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_deployment_test.go000066400000000000000000000143651476411216400333320ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress.go000066400000000000000000000341211476411216400315550ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress_test.go000066400000000000000000000336741476411216400326300ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_job.go000066400000000000000000000177511476411216400306670ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_job_test.go000066400000000000000000000114551476411216400317210ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_namespace.go000066400000000000000000000121641476411216400320420ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_namespace_test.go000066400000000000000000000026131476411216400330770ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_pdb.go000066400000000000000000000202531476411216400306510ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_pdb_test.go000066400000000000000000000144411476411216400317120ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_priorityclass.go000066400000000000000000000152141476411216400330140ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_priorityclass_test.go000066400000000000000000000046611476411216400340570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota.go000066400000000000000000000174571476411216400312510ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota_test.go000066400000000000000000000065571476411216400323070ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_role.go000066400000000000000000000311031476411216400310410ustar00rootroot00000000000000/* 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, 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.StringKeySet(groupResourceMapping).List() { 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_role_test.go000066400000000000000000000423021476411216400321030ustar00rootroot00000000000000/* 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 }{ "existing verb": { verb: "use", resource: schema.GroupResource{Group: "my.custom.io", Resource: "one"}, }, "new verb": { verb: "new", resource: schema.GroupResource{Group: "my.custom.io", Resource: "two"}, }, } 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) } for _, res := range resources { if reflect.DeepEqual(tc.resource, res) { return } } t.Errorf("missing expected resource:%#v", tc.resource) }) } } kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_rolebinding.go000066400000000000000000000176201476411216400324040ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_rolebinding_test.go000066400000000000000000000043001476411216400334320ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret.go000066400000000000000000000337341476411216400314010ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_docker.go000066400000000000000000000262361476411216400327270ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_docker_test.go000066400000000000000000000176401476411216400337650ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_test.go000066400000000000000000000424611476411216400324350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_tls.go000066400000000000000000000166721476411216400322650ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_tls_test.go000066400000000000000000000157151476411216400333210ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service.go000066400000000000000000000321631476411216400315470ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service_test.go000066400000000000000000000227441476411216400326120ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_serviceaccount.go000066400000000000000000000133061476411216400331220ustar00rootroot00000000000000/* 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.go000066400000000000000000000030331476411216400340760ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_test.go000066400000000000000000000143541476411216400310700ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_token.go000066400000000000000000000216721476411216400312320ustar00rootroot00000000000000/* 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 `) ) func boundObjectKindToAPIVersions() map[string]string { kinds := map[string]string{ "Pod": "v1", "Secret": "v1", "Node": "v1", } return kinds } 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.StringKeySet(boundObjectKindToAPIVersions()).List(), ", ")+". "+ "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 := boundObjectKindToAPIVersions()[o.BoundObjectKind]; !ok { return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersions()).List(), ", ")) } 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: boundObjectKindToAPIVersions()[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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/create/create_token_test.go000066400000000000000000000256601476411216400322720ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/debug/000077500000000000000000000000001476411216400260535ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug.go000066400000000000000000001033031476411216400274700ustar00rootroot00000000000000/* 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. `)) 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) 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 } 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 } // 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/debug/debug_test.go000066400000000000000000002324661476411216400305440ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles.go000066400000000000000000000311471476411216400302330ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/debug/profiles_test.go000066400000000000000000001013061476411216400312650ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/delete/000077500000000000000000000000001476411216400262275ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go000066400000000000000000000414771476411216400300350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_flags.go000066400000000000000000000207641476411216400312050ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_test.go000066400000000000000000001105531476411216400310640ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/describe/000077500000000000000000000000001476411216400265455ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/describe/describe.go000066400000000000000000000210431476411216400306540ustar00rootroot00000000000000/* 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.NewString() 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/describe/describe_test.go000066400000000000000000000264161476411216400317240ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/diff/000077500000000000000000000000001476411216400256755ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff.go000066400000000000000000000520741476411216400271440ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff_test.go000066400000000000000000000340501476411216400301750ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/diff/prune.go000066400000000000000000000067771476411216400273760ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/drain/000077500000000000000000000000001476411216400260625ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain.go000066400000000000000000000363311476411216400275140ustar00rootroot00000000000000/* 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.NewString() 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/drain/drain_test.go000066400000000000000000000733351476411216400305600ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/000077500000000000000000000000001476411216400257125ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/edit.go000066400000000000000000000105751476411216400271760ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/edit_test.go000066400000000000000000000222101476411216400302220ustar00rootroot00000000000000/* 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.NewString() 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.List() { 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/000077500000000000000000000000001476411216400275235ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/README000066400000000000000000000026611476411216400304100ustar00rootroot00000000000000This 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/record.go000066400000000000000000000113141476411216400313300ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/record_editor.sh000077500000000000000000000016521476411216400327120ustar00rootroot00000000000000#!/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/record_testcase.sh000077500000000000000000000034071476411216400332370ustar00rootroot00000000000000#!/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/test_editor.sh000077500000000000000000000020421476411216400324050ustar00rootroot00000000000000#!/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/000077500000000000000000000000001476411216400375645ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400413230ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail0.response000066400000000000000000000014651476411216400415110ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000001476411216400413240ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list-fail1.response000066400000000000000000000023051476411216400415040ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000013601476411216400411050ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000013001476411216400414450ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000015631476411216400411130ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000016411476411216400414560ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000006101476411216400413360ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000015211476411216400415060ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000007741476411216400413520ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000023631476411216400415140ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000022671476411216400414360ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400366535ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400404120ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list0.response000066400000000000000000000014651476411216400406000ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000001476411216400404130ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-list1.response000066400000000000000000000023051476411216400405730ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000014011476411216400401700ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000013001476411216400405340ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000006101476411216400404240ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000015211476411216400405740ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000007411476411216400404320ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000023351476411216400406010ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000021341476411216400405160ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400403555ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400421140ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied-syntax-error0.response000066400000000000000000000023061476411216400422750ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000006451476411216400417020ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000006471476411216400422520ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000010561476411216400417000ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000010621476411216400422430ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000007461476411216400421400ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000023361476411216400423030ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000013541476411216400422230ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400357025ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400374410ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-apply-edit-last-applied0.response000066400000000000000000000026171476411216400376270ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000010351476411216400372210ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000010351476411216400375670ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000011271476411216400374560ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000026171476411216400376310ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000012421476411216400375440ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400346205ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.edited000066400000000000000000000012531476411216400361400ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000012431476411216400365050ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000011111476411216400363640ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000011461476411216400365420ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000012551476411216400361440ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000012431476411216400365070ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000011131476411216400363700ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000013661476411216400365500ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000021251476411216400362770ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000014061476411216400364640ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-create-list/000077500000000000000000000000001476411216400335505ustar00rootroot000000000000000.edited000066400000000000000000000007401476411216400350110ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000007071476411216400353620ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000005771476411216400352540ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000011721476411216400354120ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000007401476411216400350130ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000007071476411216400353640ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000005771476411216400352560ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000011721476411216400354140ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000011501476411216400351450ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000013731476411216400353400ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400346035ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400363420ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit0.response000066400000000000000000000011741476411216400365250ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000012771476411216400361320ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000012751476411216400364760ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000571476411216400363600ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-error-reedit{ "spec": { "clusterIP": "10.0.0.146.1" } }2.response000066400000000000000000000013171476411216400365260ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000016061476411216400361300ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000016101476411216400364710ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000003501476411216400363560ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000011741476411216400365310ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000020011476411216400364370ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400342775ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400360360ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-from-empty0.response000066400000000000000000000003021476411216400362110ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000005341476411216400361440ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400346355ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400363740ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-output-patch0.response000066400000000000000000000020661476411216400365600ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000020171476411216400361550ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000017661476411216400365350ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000014031476411216400364060ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000025341476411216400365620ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000032121476411216400364760ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400360625ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400376210ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status0.response000066400000000000000000000051741476411216400400100ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000034211476411216400374020ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000034211476411216400377500ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000471476411216400376360ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-edit-subresource-status{ "status": { "replicas": 4 } }2.response000066400000000000000000000051741476411216400400120ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000014041476411216400377240ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400341525ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400357110ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-immutable-name0.response000066400000000000000000000007731476411216400361000ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000007661476411216400355030ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000007551476411216400360470ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000006671476411216400360260ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors/000077500000000000000000000000001476411216400336215ustar00rootroot000000000000000.request000066400000000000000000000000001476411216400353010ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors0.response000066400000000000000000000007721476411216400354670ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000001476411216400353020ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors1.response000066400000000000000000000013571476411216400354700ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000531476411216400353720ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "data": { "foo": "changed-value2" } }10.response000066400000000000000000000005401476411216400355410ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000020271476411216400350640ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000020271476411216400354320ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000002521476411216400353150ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000012741476411216400354700ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000531476411216400353150ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "data": { "foo": "changed-value2" } }4.response000066400000000000000000000005401476411216400354640ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000023561476411216400350740ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000023241476411216400354350ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000004471476411216400353260ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000007511476411216400354720ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000531476411216400353200ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-errors{ "data": { "foo": "changed-value2" } }7.response000066400000000000000000000005401476411216400354670ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000022531476411216400350730ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000022531476411216400354410ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000004471476411216400353310ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000011231476411216400354670ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000042731476411216400354130ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record/000077500000000000000000000000001476411216400335635ustar00rootroot000000000000000.request000066400000000000000000000000001476411216400352430ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record0.response000066400000000000000000000007441476411216400354300ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000001476411216400352440ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record1.response000066400000000000000000000012351476411216400354250ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000024531476411216400350310ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000023731476411216400354000ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000541476411216400352570ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list-record{ "data": { "new-data3": "newivalue" } }3.response000066400000000000000000000007751476411216400354370ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000004451476411216400352640ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000012631476411216400354310ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000021431476411216400353470ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list/000077500000000000000000000000001476411216400323075ustar00rootroot000000000000000.request000066400000000000000000000000001476411216400337670ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list0.response000066400000000000000000000006261476411216400341530ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000001476411216400337700ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list1.response000066400000000000000000000011171476411216400341500ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000022151476411216400335510ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000021351476411216400341200ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000541476411216400340030ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-list{ "data": { "new-data3": "newivalue" } }3.response000066400000000000000000000006621476411216400341560ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000004451476411216400340100ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000011501476411216400341500ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000021431476411216400340730ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400343645ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400361230ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-missing-service0.response000066400000000000000000000003411476411216400363010ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000005401476411216400362260ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-op/000077500000000000000000000000001476411216400323645ustar00rootroot000000000000000.request000066400000000000000000000000001476411216400340440ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-no-op0.response000066400000000000000000000004461476411216400342300ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000007051476411216400336270ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000007051476411216400341750ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000006311476411216400341500ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400355055ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400372440ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation0.response000066400000000000000000000020661476411216400374300ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000020171476411216400370250ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000017661476411216400374050ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000001031476411216400372520ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-not-update-annotation{ "metadata": { "labels": { "new-label": "new-value" } } }2.response000066400000000000000000000025001476411216400374230ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000016141476411216400373520ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400336645ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400354230ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error0.response000066400000000000000000000011541476411216400356040ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000012711476411216400352050ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000012671476411216400355600ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000551476411216400354370ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-repeat-error{ "spec": { "clusterIP": "10.0.0.1.1" } }2.response000066400000000000000000000013231476411216400356040ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000016041476411216400352070ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000016041476411216400355550ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000015061476411216400355310ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400343555ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400361140ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list0.response000066400000000000000000000011601476411216400362720ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000001476411216400361150ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list1.response000066400000000000000000000005701476411216400362770ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000001476411216400361160ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list2.response000066400000000000000000000006331476411216400363000ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000030101476411216400356710ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000027141476411216400362510ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000001031476411216400361240ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "metadata": { "labels": { "new-label": "new-value" } } }4.response000066400000000000000000000012151476411216400362770ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000411476411216400361260ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "other-field": "other-value" }5.response000066400000000000000000000006271476411216400363060ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000341476411216400361310ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-schemaless-list{ "field3": [ 1, 2 ] }6.response000066400000000000000000000006401476411216400363020ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000031471476411216400362250ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400341745ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400357330ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-single-service0.response000066400000000000000000000011371476411216400361150ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000012751476411216400355210ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000012441476411216400360630ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000004511476411216400357470ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000011741476411216400361200ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000015071476411216400360420ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400337325ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400354710ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error0.response000066400000000000000000000011541476411216400356520ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000012651476411216400352560ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000012671476411216400356260ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000014741476411216400352610ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000015021476411216400356170ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000000751476411216400355100ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-syntax-error{ "metadata": { "labels": { "new-label": "foo" } } }3.response000066400000000000000000000012061476411216400356530ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000013731476411216400356010ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400372445ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400410030ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-field-known-group-kind0.response000066400000000000000000000007641476411216400411720ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000011661476411216400405700ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000011361476411216400411330ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000002151476411216400410150ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000010201476411216400411560ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000013321476411216400411060ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400376465ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400414050ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind0.response000066400000000000000000000006711476411216400415710ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000011241476411216400411640ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000010561476411216400415360ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000001471476411216400414230ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-unknown-version-known-group-kind{ "extraField": { "addedData": "foo" }, "metadata": { "labels": { "label2": "value2" } } }2.response000066400000000000000000000007451476411216400415750ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000013011476411216400415040ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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/000077500000000000000000000000001476411216400347075ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata0.request000066400000000000000000000000001476411216400364460ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/edit/testdata/testcase-update-annotation0.response000066400000000000000000000020661476411216400366320ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.edited000066400000000000000000000020171476411216400362270ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.original000066400000000000000000000017661476411216400366070ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.request000066400000000000000000000013631476411216400364650ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.response000066400000000000000000000025341476411216400366340ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.yaml000066400000000000000000000016051476411216400365540ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/events/000077500000000000000000000000001476411216400262715ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer.go000066400000000000000000000065261476411216400315150ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer_test.go000066400000000000000000000200631476411216400325440ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/events/events.go000066400000000000000000000271531476411216400301340ustar00rootroot00000000000000/* 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.NewString(o.FilterTypes...).List() } 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/events/events_test.go000066400000000000000000000164251476411216400311730ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/exec/000077500000000000000000000000001476411216400257115ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec.go000066400000000000000000000301521476411216400271650ustar00rootroot00000000000000/* 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(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 (*DefaultRemoteExecutor) Execute(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 } 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/exec/exec_test.go000066400000000000000000000312361476411216400302300ustar00rootroot00000000000000/* 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" "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 { 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/explain/000077500000000000000000000000001476411216400264255ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go000066400000000000000000000157661476411216400304330ustar00rootroot00000000000000/* 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`)) plaintextTemplateName = "plaintext" plaintextOpenAPIV2TemplateName = "plaintext-openapiv2" ) type ExplainOptions struct { genericiooptions.IOStreams CmdParent string APIVersion string Recursive bool args []string Mapper meta.RESTMapper openAPIGetter openapi.OpenAPIResourcesGetter // Name of the template to use with the openapiv3 template renderer. OutputFormat string // Client capable of fetching openapi documents from the user's cluster OpenAPIV3Client openapiclient.Client } func NewExplainOptions(parent string, streams genericiooptions.IOStreams) *ExplainOptions { return &ExplainOptions{ IOStreams: streams, CmdParent: parent, OutputFormat: plaintextTemplateName, } } // NewCmdExplain returns a cobra command for swagger docs func NewCmdExplain(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { o := NewExplainOptions(parent, 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) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } cmd.Flags().BoolVar(&o.Recursive, "recursive", o.Recursive, "When true, print the name of all the fields recursively. Otherwise, print the available fields with their description.") cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Use given api-version (group/version) of the resource.") // Only enable --output as a valid flag if the feature is enabled cmd.Flags().StringVarP(&o.OutputFormat, "output", "o", plaintextTemplateName, "Format in which to render the schema. Valid values are: (plaintext, plaintext-openapiv2).") return cmd } func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { var err error o.Mapper, err = f.ToRESTMapper() if err != nil { return err } // Only openapi v3 needs the discovery client. o.OpenAPIV3Client, err = f.OpenAPIV3Client() if err != nil { return err } // Lazy-load the OpenAPI V2 Resources, so they're not loaded when using OpenAPI V3. o.openAPIGetter = f o.args = args return nil } 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/explain/explain_test.go000066400000000000000000000237201476411216400314570ustar00rootroot00000000000000/* 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() opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard()) cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard()) err := opts.Complete(tf, cmd, []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") } err = opts.Complete(tf, cmd, []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() opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard()) cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard()) err := opts.Complete(tf, cmd, []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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/expose/000077500000000000000000000000001476411216400262705ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose.go000066400000000000000000000524421476411216400301310ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/expose/expose_test.go000066400000000000000000001344061476411216400311710ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/000077500000000000000000000000001476411216400255445ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn.go000066400000000000000000000207151476411216400306300ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn_flags.go000066400000000000000000000066511476411216400320070ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn_flags_test.go000066400000000000000000000101021476411216400330300ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/customcolumn_test.go000066400000000000000000000323551476411216400316720ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/get.go000066400000000000000000000630501476411216400266560ustar00rootroot00000000000000/* 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.NewString() 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/get_flags.go000066400000000000000000000134241476411216400300320ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/get_test.go000066400000000000000000003137341476411216400277240ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/humanreadable_flags.go000066400000000000000000000102551476411216400320420ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/humanreadable_flags_test.go000066400000000000000000000170451476411216400331050ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/skip_printer.go000066400000000000000000000025401476411216400306050ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/sorter.go000066400000000000000000000261211476411216400274130ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/sorter_test.go000066400000000000000000000475601476411216400304640ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/get/table_printer.go000066400000000000000000000055011476411216400307260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/help/000077500000000000000000000000001476411216400257155ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/help/help.go000066400000000000000000000045661476411216400272070ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/kustomize/000077500000000000000000000000001476411216400270175ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/kustomize/kustomize.go000066400000000000000000000023441476411216400314030ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/label/000077500000000000000000000000001476411216400260445ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/label/label.go000066400000000000000000000353471476411216400274660ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/label/label_test.go000066400000000000000000000575151476411216400305260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/logs/000077500000000000000000000000001476411216400257315ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs.go000066400000000000000000000405421476411216400272310ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs_test.go000066400000000000000000000722261476411216400302740ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/options/000077500000000000000000000000001476411216400264605ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/options/options.go000066400000000000000000000030211476411216400304760ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/patch/000077500000000000000000000000001476411216400260645ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/patch/patch.go000066400000000000000000000273721476411216400275250ustar00rootroot00000000000000/* 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.StringKeySet(patchTypes).List())) 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.StringKeySet(patchTypes).List(), 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/patch/patch_test.go000066400000000000000000000205751476411216400305620ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/000077500000000000000000000000001476411216400262635ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin.go000066400000000000000000000174451476411216400301230ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go000066400000000000000000000222331476411216400323430ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go000066400000000000000000000156771476411216400311670ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/000077500000000000000000000000001476411216400300745ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-create-foo000066400000000000000000000000521476411216400334670ustar00rootroot00000000000000#!/bin/bash echo "I am plugin create foo"kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-foo000077500000000000000000000000441476411216400322320ustar00rootroot00000000000000#!/bin/bash echo "I am plugin foo" kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-version000077500000000000000000000001561476411216400331400ustar00rootroot00000000000000#!/bin/bash # This plugin is a no-op and is used to test plugins # that overshadow existing kubectl commands kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/portforward/000077500000000000000000000000001476411216400273365ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/portforward/portforward.go000066400000000000000000000335441476411216400322470ustar00rootroot00000000000000/* 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.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.NewInt() tcpPorts := sets.NewInt() 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.NewInt() tcpPorts := sets.NewInt() 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/portforward/portforward_test.go000066400000000000000000000557021476411216400333060ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/profiling.go000066400000000000000000000044171476411216400273130ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/proxy/000077500000000000000000000000001476411216400261465ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/proxy/proxy.go000066400000000000000000000205051476411216400276600ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/replace/000077500000000000000000000000001476411216400264005ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace.go000066400000000000000000000264361476411216400303550ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/replace/replace_test.go000066400000000000000000000256571476411216400314200ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/000077500000000000000000000000001476411216400264655ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout.go000066400000000000000000000043241476411216400305170ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_history.go000066400000000000000000000143741476411216400323060ustar00rootroot00000000000000/* 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 al 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 { printer.PrintObj(historyInfo[o.Revision], o.Out) } 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_history_test.go000066400000000000000000000316151476411216400333420ustar00rootroot00000000000000/* 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 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_pause.go000066400000000000000000000144351476411216400317200ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_pause_test.go000066400000000000000000000054031476411216400327520ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_restart.go000066400000000000000000000147531476411216400322720ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_restart_test.go000066400000000000000000000211431476411216400333200ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_resume.go000066400000000000000000000145001476411216400320740ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status.go000066400000000000000000000173561476411216400321330ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_status_test.go000066400000000000000000000244551476411216400331700ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/rollout/rollout_undo.go000066400000000000000000000124061476411216400315440ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/run/000077500000000000000000000000001476411216400255715ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/run/run.go000066400000000000000000000555541476411216400267420ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/run/run_test.go000066400000000000000000000500471476411216400277710ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/scale/000077500000000000000000000000001476411216400260545ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/scale/scale.go000066400000000000000000000224641476411216400275020ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/000077500000000000000000000000001476411216400255605ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/env/000077500000000000000000000000001476411216400263505ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/env/doc.go000066400000000000000000000013071476411216400274450ustar00rootroot00000000000000/* 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 // import "k8s.io/kubectl/pkg/cmd/set/env" kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse.go000066400000000000000000000105571476411216400306710ustar00rootroot00000000000000/* 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.NewString() 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_parse_test.go000066400000000000000000000062341476411216400317250ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/env/env_resolve.go000066400000000000000000000256641476411216400312430ustar00rootroot00000000000000/* 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.NewString() for key := range m { keys.Insert(key) } for _, key := range keys.List() { 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/helper.go000066400000000000000000000113241476411216400273670ustar00rootroot00000000000000/* 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" "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.NewString(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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/helper_test.go000066400000000000000000000054741476411216400304370ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set.go000066400000000000000000000032071476411216400267040ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env.go000066400000000000000000000415501476411216400275570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_env_test.go000066400000000000000000000663031476411216400306210ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_image.go000066400000000000000000000254551476411216400300570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_image_test.go000066400000000000000000000536721476411216400311200ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_resources.go000066400000000000000000000254551476411216400310070ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_resources_test.go000066400000000000000000000432341476411216400320410ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_selector.go000066400000000000000000000201721476411216400306040ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_selector_test.go000066400000000000000000000214201476411216400316400ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_serviceaccount.go000066400000000000000000000165471476411216400320140ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_serviceaccount_test.go000066400000000000000000000315741476411216400330500ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_subject.go000066400000000000000000000240121476411216400304200ustar00rootroot00000000000000/* 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.NewString(o.Users...).List() { subject := rbacv1.Subject{ Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: user, } subjects = append(subjects, subject) } for _, group := range sets.NewString(o.Groups...).List() { subject := rbacv1.Subject{ Kind: rbacv1.GroupKind, APIGroup: rbacv1.GroupName, Name: group, } subjects = append(subjects, subject) } for _, sa := range sets.NewString(o.ServiceAccounts...).List() { 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_subject_test.go000066400000000000000000000221041476411216400314570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/set/set_test.go000066400000000000000000000027161476411216400277470ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/skiplookerr_go118.go000066400000000000000000000012621476411216400306000ustar00rootroot00000000000000//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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/skiplookerr_go119.go000066400000000000000000000013611476411216400306010ustar00rootroot00000000000000//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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/taint/000077500000000000000000000000001476411216400261045ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/taint/taint.go000066400000000000000000000275101476411216400275570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/taint/taint_test.go000066400000000000000000000270251476411216400306170ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/taint/utils.go000066400000000000000000000160761476411216400276050ustar00rootroot00000000000000/* 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.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.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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/taint/utils_test.go000066400000000000000000000311001476411216400306250ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/testing/000077500000000000000000000000001476411216400264425ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go000066400000000000000000000716571476411216400277170ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/testing/interfaces.go000066400000000000000000000016301476411216400311140ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/testing/util.go000066400000000000000000000137201476411216400277510ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/testing/zz_generated.deepcopy.go000066400000000000000000000116501476411216400332640ustar00rootroot00000000000000//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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/top/000077500000000000000000000000001476411216400255675ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/top/top.go000066400000000000000000000040461476411216400267240ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node.go000066400000000000000000000151661476411216400277360ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node_test.go000066400000000000000000000363161476411216400307750ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod.go000066400000000000000000000222241476411216400275640ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod_test.go000066400000000000000000000355201476411216400306260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/top/top_test.go000066400000000000000000000103021476411216400277530ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/000077500000000000000000000000001476411216400257425ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/000077500000000000000000000000001476411216400272305ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/crlf/000077500000000000000000000000001476411216400301565ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/crlf/crlf.go000066400000000000000000000024401476411216400314330ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editoptions.go000066400000000000000000000636211476411216400321300ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editoptions_test.go000066400000000000000000000274271476411216400331730ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editor.go000066400000000000000000000064141476411216400310520ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editor_test.go000066400000000000000000000040751476411216400321120ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/launcher_others.go000066400000000000000000000034571476411216400327550ustar00rootroot00000000000000//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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/launcher_windows.go000066400000000000000000000050771476411216400331430ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file.go000066400000000000000000000052011476411216400300560ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file_test.go000066400000000000000000000053261476411216400311250ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/factory.go000066400000000000000000000060761476411216400277510ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go000066400000000000000000000144351476411216400326260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go000066400000000000000000000777541476411216400277570ustar00rootroot00000000000000/* 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.NewString() 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" ) // 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 '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). 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", "", fmt.Sprintf("%s This flag is beta and may change in the future.", 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers_test.go000066400000000000000000000404201476411216400307720ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/kubectl_match_version.go000066400000000000000000000100121476411216400326350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/override_options.go000066400000000000000000000054401476411216400316660ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/podcmd/000077500000000000000000000000001476411216400272105ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/podcmd/podcmd.go000066400000000000000000000074651476411216400310210ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/printing.go000066400000000000000000000016511476411216400301260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/sanity/000077500000000000000000000000001476411216400272515ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/util/sanity/cmd_sanity.go000066400000000000000000000115001476411216400317270ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/version/000077500000000000000000000000001476411216400264525ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/version/skew_warning.go000066400000000000000000000042201476411216400314750ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/version/skew_warning_test.go000066400000000000000000000066531476411216400325500ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/version/version.go000066400000000000000000000130761476411216400304750ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/version/version_test.go000066400000000000000000000031461476411216400315310ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/wait/000077500000000000000000000000001476411216400257315ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/wait/condition.go000066400000000000000000000156441476411216400302600ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/wait/create.go000066400000000000000000000017661476411216400275350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/wait/delete.go000066400000000000000000000112451476411216400275250ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/wait/json.go000066400000000000000000000103171476411216400272330ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait.go000066400000000000000000000315731476411216400272350ustar00rootroot00000000000000/* 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) { visitErr := o.ResourceFinder.Do().Visit(func(info *resource.Info, err error) error { return nil }) if apierrors.IsNotFound(visitErr) { return false, nil } if visitErr != nil { return false, visitErr } return true, 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-9bda076/staging/src/k8s.io/kubectl/pkg/cmd/wait/wait_test.go000066400000000000000000001676771476411216400303130ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/describe/000077500000000000000000000000001476411216400260025ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/describe/describe.go000066400000000000000000006047021476411216400301220ustar00rootroot00000000000000/* 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.NewString(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: 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.ContainsString(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.ContainsString(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.ContainsString(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.ContainsString(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 { w.Write(LEVEL_2, "SecretName:\t%v\n"+ " SecretOptionalName:\t%v\n", source.Secret.Name, source.Secret.Optional) } else if source.DownwardAPI != nil { w.Write(LEVEL_2, "DownwardAPI:\ttrue\n") } else if source.ConfigMap != nil { w.Write(LEVEL_2, "ConfigMapName:\t%v\n"+ " ConfigMapOptional:\t%v\n", source.ConfigMap.Name, source.ConfigMap.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.NewString()) } func printCSIPersistentVolumeAttributesMultilineIndent(w PrefixWriter, initialIndent, title, innerIndent string, attributes map[string]string, skip sets.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.NewString()) printLabelsMultilineWithIndent(w, " ", "Annotations", "\t", pvc.Annotations, sets.NewString()) 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, events, running, waiting, succeeded, failed) } func describeDaemonSet(daemon *appsv1.DaemonSet, 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) selector, err := metav1.LabelSelectorAsSelector(daemon.Spec.Selector) if err != nil { // this shouldn't happen if LabelSelector passed validation return err } 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 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) 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 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) 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.NewString()) } } 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.NewString() 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.NewString() 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.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.StringKeySet(types).List() { 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.NewString()) } // printLabelsMultiline prints multiple labels with a user-defined alignment. func printLabelsMultilineWithIndent(w PrefixWriter, initialIndent, title, innerIndent string, labels map[string]string, skip sets.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.NewString() 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 roles.List() } // 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.NewString() 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(result.List(), ",") 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.NewString() 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(result.List(), ",") 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-9bda076/staging/src/k8s.io/kubectl/pkg/describe/describe_test.go000066400000000000000000006176021476411216400311640ustar00rootroot00000000000000/* 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 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", }, } 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", }, } 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) } } }) } } kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/describe/interface.go000066400000000000000000000050151476411216400302720ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/drain/000077500000000000000000000000001476411216400253175ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/drain/cordon.go000066400000000000000000000066651476411216400271470ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/drain/default.go000066400000000000000000000046171476411216400273020ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/drain/default_test.go000066400000000000000000000035611476411216400303360ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/drain/drain.go000066400000000000000000000375011476411216400267510ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/drain/drain_test.go000066400000000000000000000376561476411216400300230ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/drain/filter_test.go000066400000000000000000000036151476411216400301770ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/drain/filters.go000066400000000000000000000173151476411216400273250ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/000077500000000000000000000000001476411216400256625ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/explain/OWNERS000066400000000000000000000001431476411216400266200ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - apelisse reviewers: - apelisse kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/explain/explain.go000066400000000000000000000133241476411216400276540ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/explain_test.go000066400000000000000000000120141476411216400307060ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/field_lookup.go000066400000000000000000000050131476411216400306640ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/field_lookup_test.go000066400000000000000000000055411476411216400317310ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/fields_printer.go000066400000000000000000000045051476411216400312260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/fields_printer_builder.go000066400000000000000000000020121476411216400327230ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/fields_printer_test.go000066400000000000000000000036371476411216400322720ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/formatter.go000066400000000000000000000077041476411216400302240ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/formatter_test.go000066400000000000000000000076021476411216400312600ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/model_printer.go000066400000000000000000000105361476411216400310610ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/model_printer_test.go000066400000000000000000000107331476411216400321170ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/recursive_fields_printer.go000066400000000000000000000044671476411216400333240ustar00rootroot00000000000000/* 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.go000066400000000000000000000047431476411216400343010ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/test-recursive-swagger.json000066400000000000000000000031241476411216400331760ustar00rootroot00000000000000{ "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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/test-swagger.json000066400000000000000000000055341476411216400312000ustar00rootroot00000000000000{ "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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/typename.go000066400000000000000000000031351476411216400300350ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/typename_test.go000066400000000000000000000040001476411216400310640ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/000077500000000000000000000000001476411216400262115ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/explain.go000066400000000000000000000050621476411216400302030ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/explain_test.go000066400000000000000000000075761476411216400312560ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/funcs.go000066400000000000000000000133221476411216400276570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/funcs_test.go000066400000000000000000000247331476411216400307260ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/generator.go000066400000000000000000000052331476411216400305310ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/generator_test.go000066400000000000000000000057741476411216400316020ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/template.go000066400000000000000000000021741476411216400303570ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/template_test.go000066400000000000000000000024221476411216400314120ustar00rootroot00000000000000/* 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/templates/000077500000000000000000000000001476411216400302075ustar00rootroot00000000000000apiextensions.k8s.io_v1.json000066400000000000000000004770221476411216400354500ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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.json000066400000000000000000016537041476411216400336440ustar00rootroot00000000000000kubernetes-kubernetes-9bda076/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 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-9bda076/staging/src/k8s.io/kubectl/pkg/explain/v2/templates/plaintext.tmpl000066400000000000000000000350161476411216400331220ustar00rootroot00000000000000{{- /* 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: