pax_global_header00006660000000000000000000000064151613724620014521gustar00rootroot0000000000000052 comment=7605ca50c42eab0937bd3d4de825e0bcd120ba1f kubectx-0.11.0/000077500000000000000000000000001516137246200132455ustar00rootroot00000000000000kubectx-0.11.0/.github/000077500000000000000000000000001516137246200146055ustar00rootroot00000000000000kubectx-0.11.0/.github/dependabot.yml000066400000000000000000000005411516137246200174350ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly commit-message: prefix: chore include: scope - package-ecosystem: gomod directory: / schedule: interval: weekly commit-message: prefix: chore include: scope groups: kubernetes: patterns: - "k8s.io/*" kubectx-0.11.0/.github/workflows/000077500000000000000000000000001516137246200166425ustar00rootroot00000000000000kubectx-0.11.0/.github/workflows/bash-frozen.yml000066400000000000000000000020511516137246200216010ustar00rootroot00000000000000name: Bash scripts frozen on: pull_request: paths: - 'kubectx' - 'kubens' jobs: comment: runs-on: ubuntu-latest permissions: pull-requests: write steps: - name: Comment on PR if author is not ahmetb if: github.event.pull_request.user.login != 'ahmetb' uses: actions/github-script@v8 with: script: | const body = [ '> [!WARNING]', '> **This PR will not be merged.**', '>', '> The bash implementation of `kubectx` and `kubens` is **frozen** and is provided only for convenience.', '> We are not accepting any improvements to the bash scripts.', '>', '> Please propose your improvements to the **Go implementation** instead.', ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: body }); kubectx-0.11.0/.github/workflows/ci.yml000066400000000000000000000040361516137246200177630ustar00rootroot00000000000000# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Go implementation (CI) on: push: pull_request: jobs: ci: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master - name: Setup Go uses: actions/setup-go@v6 with: go-version: '1.25' - id: go-cache-paths run: | echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - name: Go Build Cache uses: actions/cache@v5 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - name: Go Mod Cache uses: actions/cache@v5 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - name: Ensure gofmt run: test -z "$(gofmt -s -d .)" - name: Ensure go.mod is already tidied run: go mod tidy && git diff --exit-code - name: Run unit tests run: go test ./... - name: Build with Goreleaser uses: goreleaser/goreleaser-action@v7 with: version: latest args: release --snapshot --skip publish,snapcraft --clean - name: Setup BATS framework run: sudo npm install -g bats - name: kubectx (Go) integration tests run: COMMAND=./dist/kubectx_linux_amd64_v1/kubectx bats test/kubectx.bats - name: kubens (Go) integration tests run: COMMAND=./dist/kubens_linux_amd64_v1/kubens bats test/kubens.bats kubectx-0.11.0/.github/workflows/dependabot.yml000066400000000000000000000011171516137246200214720ustar00rootroot00000000000000name: Dependabot on: pull_request: permissions: contents: write pull-requests: write jobs: auto-merge: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2 - name: Enable auto-merge for Dependabot PRs if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }} run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} kubectx-0.11.0/.github/workflows/release.yml000066400000000000000000000036251516137246200210130ustar00rootroot00000000000000# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Release on: push: tags: - 'v*.*.*' jobs: goreleaser: permissions: contents: write runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v6 with: go-version: '1.25' - name: Install Snapcraft uses: samuelmeuli/action-snapcraft@v3 - name: Setup Snapcraft run: | # https://github.com/goreleaser/goreleaser/issues/1715 mkdir -p $HOME/.cache/snapcraft/download mkdir -p $HOME/.cache/snapcraft/stage-packages - name: GoReleaser uses: goreleaser/goreleaser-action@v7 with: version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Update new version for plugin 'ctx' in krew-index uses: rajatjindal/krew-release-bot@v0.0.51 with: krew_template_file: .krew/ctx.yaml - name: Update new version for plugin 'ns' in krew-index uses: rajatjindal/krew-release-bot@v0.0.51 with: krew_template_file: .krew/ns.yaml - name: Publish Snaps to the Snap Store (stable channel) run: for snap in $(ls dist/*.snap); do snapcraft upload --release=stable $snap; done env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} kubectx-0.11.0/.goreleaser.yml000066400000000000000000000054411516137246200162020ustar00rootroot00000000000000# yaml-language-server: $schema=https://goreleaser.com/static/schema.json # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at https://goreleaser.com version: 2 before: hooks: - go mod download builds: - id: kubectx main: ./cmd/kubectx binary: kubectx env: - CGO_ENABLED=0 goos: - linux - darwin - windows goarch: - amd64 - arm - arm64 - ppc64le - s390x goarm: [6, 7] - id: kubens main: ./cmd/kubens binary: kubens env: - CGO_ENABLED=0 goos: - linux - darwin - windows goarch: - amd64 - arm - arm64 - ppc64le - s390x goarm: [6, 7] archives: - id: kubectx-archive name_template: |- kubectx_{{ .Tag }}_{{ .Os }}_ {{- with .Arch -}} {{- if (eq . "386") -}}i386 {{- else if (eq . "amd64") -}}x86_64 {{- else -}}{{- . -}} {{- end -}} {{ end }} {{- with .Arm -}} {{- if (eq . "6") -}}hf {{- else -}}v{{- . -}} {{- end -}} {{- end -}} ids: - kubectx format_overrides: - goos: windows formats: [zip] files: ["LICENSE"] - id: kubens-archive name_template: |- kubens_{{ .Tag }}_{{ .Os }}_ {{- with .Arch -}} {{- if (eq . "386") -}}i386 {{- else if (eq . "amd64") -}}x86_64 {{- else -}}{{- . -}} {{- end -}} {{ end }} {{- with .Arm -}} {{- if (eq . "6") -}}hf {{- else -}}v{{- . -}} {{- end -}} {{- end -}} ids: - kubens format_overrides: - goos: windows formats: [zip] files: ["LICENSE"] checksum: name_template: "checksums.txt" algorithm: sha256 release: extra_files: - glob: ./kubens - glob: ./kubectx snapcrafts: - id: kubectx name: kubectx summary: 'kubectx + kubens: Power tools for kubectl' description: | kubectx is a tool to switch between contexts (clusters) on kubectl faster. kubens is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily. grade: stable confinement: classic base: core24 apps: kubectx: command: kubectx completer: completion/kubectx.bash kubens: command: kubens completer: completion/kubens.bash kubectx-0.11.0/.krew/000077500000000000000000000000001516137246200142735ustar00rootroot00000000000000kubectx-0.11.0/.krew/ctx.yaml000066400000000000000000000016701516137246200157610ustar00rootroot00000000000000apiVersion: krew.googlecontainertools.github.com/v1alpha2 kind: Plugin metadata: name: ctx spec: homepage: https://github.com/ahmetb/kubectx shortDescription: Switch between contexts in your kubeconfig version: {{ .TagName }} description: | Also known as "kubectx", a utility to switch between context entries in your kubeconfig file efficiently. caveats: | If fzf is installed on your machine, you can interactively choose between the entries using the arrow keys, or by fuzzy searching as you type. See https://github.com/ahmetb/kubectx for customization and details. platforms: - selector: matchExpressions: - key: os operator: In values: - darwin - linux {{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }} bin: kubectx files: - from: kubectx-*/kubectx to: . - from: kubectx-*/LICENSE to: . kubectx-0.11.0/.krew/ns.yaml000066400000000000000000000015251516137246200156020ustar00rootroot00000000000000apiVersion: krew.googlecontainertools.github.com/v1alpha2 kind: Plugin metadata: name: ns spec: homepage: https://github.com/ahmetb/kubectx shortDescription: Switch between Kubernetes namespaces version: {{ .TagName }} description: | Also known as "kubens", a utility to set your current namespace and switch between them. caveats: | If fzf is installed on your machine, you can interactively choose between the entries using the arrow keys, or by fuzzy searching as you type. platforms: - selector: matchExpressions: - key: os operator: In values: - darwin - linux {{addURIAndSha "https://github.com/ahmetb/kubectx/archive/{{ .TagName }}.tar.gz" .TagName }} bin: kubens files: - from: kubectx-*/kubens to: . - from: kubectx-*/LICENSE to: . kubectx-0.11.0/CONTRIBUTING.md000066400000000000000000000017101516137246200154750ustar00rootroot00000000000000# How to contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution, this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.kubectx-0.11.0/LICENSE000066400000000000000000000261351516137246200142610ustar00rootroot00000000000000 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. kubectx-0.11.0/README.md000066400000000000000000000162041516137246200145270ustar00rootroot00000000000000# `kubectx` + `kubens`: Power tools for kubectl ![Latest GitHub release](https://img.shields.io/github/release/ahmetb/kubectx.svg) ![GitHub stars](https://img.shields.io/github/stars/ahmetb/kubectx.svg?label=github%20stars) ![Homebrew downloads](https://img.shields.io/homebrew/installs/dy/kubectx?label=macOS%20installs) [![Go implementation (CI)](https://github.com/ahmetb/kubectx/workflows/Go%20implementation%20(CI)/badge.svg)](https://github.com/ahmetb/kubectx/actions?query=workflow%3A"Go+implementation+(CI)") This repository provides both `kubectx` and `kubens` tools. [Install →](#installation) ## What are `kubectx` and `kubens`? **kubectx** is a tool to switch between contexts (clusters) on kubectl faster, and launch readonly shells for each context.
**kubens** is a tool to switch between Kubernetes namespaces (and configure them for kubectl) easily. Here's a **`kubectx`** demo: ![kubectx demo GIF](img/kubectx-demo.gif) ...and here's a **`kubens`** demo: ![kubens demo GIF](img/kubens-demo.gif) ### Usage #### kubectx Switch to another cluster that's in kubeconfig: ```sh $ kubectx minikube Switched to context "minikube". ``` Switch back to previous cluster: ```sh $ kubectx - Switched to context "oregon". ``` Start an isolated shell that only has a single context: ```sh $ kubectx -s minikube ``` Start a read-only shell where write operations are blocked: ```sh $ kubectx -r minikube ``` Rename context: ```sh $ kubectx dublin=gke_ahmetb_europe-west1-b_dublin Context "gke_ahmetb_europe-west1-b_dublin" renamed to "dublin". ``` #### kubens Change the active namespace on kubectl: ```sh $ kubens kube-system Context "test" set. Active namespace is "kube-system". ``` Go back to the previous namespace: ```sh $ kubens - Context "test" set. Active namespace is "default". ``` Change the active namespace even if it doesn't exist: ```sh $ kubens namespace-404 -f Context "test" set. Active namespace is "namespace-404". ``` If you have [`fzf`](https://github.com/junegunn/fzf) installed, you can also **interactively** select a context or cluster, or fuzzy-search by typing a few characters. To learn more, read [interactive mode →](#interactive-mode) Both `kubectx` and `kubens` support Tab completion on bash/zsh/fish shells to help with long context names. You don't have to remember full context names anymore. ----- ## Installation | Package manager | Command | |---|---| | [Homebrew](https://brew.sh/) (macOS & Linux) | `brew install kubectx` | | [MacPorts](https://www.macports.org) (macOS) | `sudo port install kubectx` | | apt (Debian/Ubuntu) | `sudo apt install kubectx` | | pacman (Arch Linux) | `sudo pacman -S kubectx` | | [Chocolatey](https://chocolatey.org/) (Windows) | `choco install kubens kubectx` | | [Scoop](https://scoop.sh/) (Windows) | `scoop bucket add main && scoop install main/kubens main/kubectx` | | [winget](https://learn.microsoft.com/en-us/windows/package-manager/) (Windows) | `winget install --id ahmetb.kubectx && winget install --id ahmetb.kubens` | | [Krew](https://github.com/kubernetes-sigs/krew/) (kubectl plugin) | `kubectl krew install ctx && kubectl krew install ns` | Alternatively, download binaries from the [**Releases page →**](https://github.com/ahmetb/kubectx/releases) and add them to somewhere in your `PATH`.
Shell completion scripts #### zsh (with [antibody](https://getantibody.github.io)) Add this line to your [Plugins File](https://getantibody.github.io/usage/) (e.g. `~/.zsh_plugins.txt`): ``` ahmetb/kubectx path:completion kind:fpath ``` Depending on your setup, you might or might not need to call `compinit` or `autoload -U compinit && compinit` in your `~/.zshrc` after you load the Plugins file. If you use [oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh), load the completions before you load `oh-my-zsh` because `oh-my-zsh` will call `compinit`. #### zsh (plain) The completion scripts have to be in a path that belongs to `$fpath`. Either link or copy them to an existing folder. Example with [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh): ```bash mkdir -p ~/.oh-my-zsh/custom/completions chmod -R 755 ~/.oh-my-zsh/custom/completions ln -s /opt/kubectx/completion/_kubectx.zsh ~/.oh-my-zsh/custom/completions/_kubectx.zsh ln -s /opt/kubectx/completion/_kubens.zsh ~/.oh-my-zsh/custom/completions/_kubens.zsh echo "fpath=($ZSH/custom/completions $fpath)" >> ~/.zshrc ``` If completion doesn't work, add `autoload -U compinit && compinit` to your `.zshrc` (similar to [`zsh-completions`](https://github.com/zsh-users/zsh-completions/blob/master/README.md#oh-my-zsh)). If you are not using [`oh-my-zsh`](https://github.com/ohmyzsh/ohmyzsh), you could link to `/usr/share/zsh/functions/Completion` (might require sudo), depending on the `$fpath` of your zsh installation. In case of errors, calling `compaudit` might help. #### bash ```bash git clone https://github.com/ahmetb/kubectx.git ~/.kubectx COMPDIR=$(pkg-config --variable=completionsdir bash-completion) ln -sf ~/.kubectx/completion/kubens.bash $COMPDIR/kubens ln -sf ~/.kubectx/completion/kubectx.bash $COMPDIR/kubectx cat << EOF >> ~/.bashrc #kubectx and kubens export PATH=~/.kubectx:\$PATH EOF ``` #### fish ```fish mkdir -p ~/.config/fish/completions ln -s /opt/kubectx/completion/kubectx.fish ~/.config/fish/completions/ ln -s /opt/kubectx/completion/kubens.fish ~/.config/fish/completions/ ```
> [!NOTE] > Tip: Show context/namespace in your shell prompt with [oh-my-posh](https://ohmyposh.dev/) or > simply with [kube-ps1](https://github.com/jonmosco/kube-ps1). ----- ### Interactive mode If you want `kubectx` and `kubens` commands to present you an interactive menu with fuzzy searching, you just need to [install `fzf`](https://github.com/junegunn/fzf) in your `$PATH`. ![kubectx interactive search with fzf](img/kubectx-interactive.gif) Caveats: - If you have `fzf` installed, but want to opt out of using this feature, set the environment variable `KUBECTX_IGNORE_FZF=1`. - If you want to keep `fzf` interactive mode but need the default behavior of the command, you can do it by piping the output to another command (e.g. `kubectx | cat `). ----- ### Customizing colors If you like to customize the colors indicating the current namespace or context, set the environment variables `KUBECTX_CURRENT_FGCOLOR` and `KUBECTX_CURRENT_BGCOLOR` (refer color codes [here](https://linux.101hacks.com/ps1-examples/prompt-color-using-tput/)): ```sh export KUBECTX_CURRENT_FGCOLOR=$(tput setaf 6) # blue text export KUBECTX_CURRENT_BGCOLOR=$(tput setab 7) # white background ``` Colors in the output can be disabled by setting the [`NO_COLOR`](https://no-color.org/) environment variable. ----- If you liked `kubectx`, you may like my [`kubectl-aliases`](https://github.com/ahmetb/kubectl-aliases) project, too. I recommend pairing kubectx and kubens with [fzf](#interactive-mode) and [kube-ps1](https://github.com/jonmosco/kube-ps1). #### Stargazers over time [![Stargazers over time](https://starchart.cc/ahmetb/kubectx.svg)](https://starchart.cc/ahmetb/kubectx) ![Google Analytics](https://ga-beacon.appspot.com/UA-2609286-17/kubectx/README?pixel) kubectx-0.11.0/cmd/000077500000000000000000000000001516137246200140105ustar00rootroot00000000000000kubectx-0.11.0/cmd/kubectx/000077500000000000000000000000001516137246200154555ustar00rootroot00000000000000kubectx-0.11.0/cmd/kubectx/current.go000066400000000000000000000024761516137246200174770ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" ) // CurrentOp prints the current context type CurrentOp struct{} func (_op CurrentOp) Run(stdout, _ io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } v, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if v == "" { return errors.New("current-context is not set") } if _, err := fmt.Fprintln(stdout, v); err != nil { return fmt.Errorf("write error: %w", err) } return nil } kubectx-0.11.0/cmd/kubectx/delete.go000066400000000000000000000053371516137246200172560ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // DeleteOp indicates intention to delete contexts. type DeleteOp struct { Contexts []string // NAME or '.' to indicate current-context. } // deleteContexts deletes context entries one by one. func (op DeleteOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } for _, ctx := range op.Contexts { // TODO inefficiency here. we open/write/close the same file many times. deletedName, wasActiveContext, err := deleteContext(ctx) if err != nil { return fmt.Errorf("error deleting context \"%s\": %w", deletedName, err) } if wasActiveContext { printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.", selfName()) } _ = printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(deletedName)) } return nil } // deleteContext deletes a context entry by NAME or current-context // indicated by ".". func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return deleteName, false, fmt.Errorf("kubeconfig error: %w", err) } cur, err := kc.GetCurrentContext() if err != nil { return deleteName, false, fmt.Errorf("failed to get current context: %w", err) } // resolve "." to a real name if name == "." { if cur == "" { return deleteName, false, errors.New("can't use '.' as the no active context is set") } wasActiveContext = true name = cur } exists, err := kc.ContextExists(name) if err != nil { return name, false, fmt.Errorf("failed to check context: %w", err) } if !exists { return name, false, errors.New("context does not exist") } if err := kc.DeleteContextEntry(name); err != nil { return name, false, fmt.Errorf("failed to modify yaml doc: %w", err) } if err := kc.Save(); err != nil { return name, wasActiveContext, fmt.Errorf("failed to save modified kubeconfig file: %w", err) } return name, wasActiveContext, nil } kubectx-0.11.0/cmd/kubectx/env.go000066400000000000000000000011321516137246200165710ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 kubectx-0.11.0/cmd/kubectx/flags.go000066400000000000000000000055721516137246200171110ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "os" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" ) // UnsupportedOp indicates an unsupported flag. type UnsupportedOp struct{ Err error } func (op UnsupportedOp) Run(_, _ io.Writer) error { return op.Err } // parseArgs looks at flags (excl. executable name, i.e. argv[0]) // and decides which operation should be taken. func parseArgs(argv []string) Op { if len(argv) == 0 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveSwitchOp{SelfCmd: os.Args[0]} } return ListOp{} } if argv[0] == "--readonly" || argv[0] == "-r" { if len(argv) == 1 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveReadonlyShellOp{SelfCmd: os.Args[0]} } return UnsupportedOp{Err: fmt.Errorf("'%s' requires a context name argument (or fzf for interactive mode)", argv[0])} } if len(argv) == 2 { return ReadonlyShellOp{Target: argv[1]} } return UnsupportedOp{Err: fmt.Errorf("'%s' accepts at most one context name argument", argv[0])} } if argv[0] == "--shell" || argv[0] == "-s" { if len(argv) == 1 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveShellOp{SelfCmd: os.Args[0]} } return UnsupportedOp{Err: fmt.Errorf("'%s' requires a context name argument (or fzf for interactive mode)", argv[0])} } if len(argv) == 2 { return ShellOp{Target: argv[1]} } return UnsupportedOp{Err: fmt.Errorf("'%s' accepts at most one context name argument", argv[0])} } if argv[0] == "-d" { if len(argv) == 1 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveDeleteOp{SelfCmd: os.Args[0]} } else { return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")} } } return DeleteOp{Contexts: argv[1:]} } if len(argv) == 1 { v := argv[0] if v == "--help" || v == "-h" { return HelpOp{} } if v == "--version" || v == "-V" { return VersionOp{} } if v == "--current" || v == "-c" { return CurrentOp{} } if v == "--unset" || v == "-u" { return UnsetOp{} } if new, old, ok := parseRenameSyntax(v); ok { return RenameOp{New: new, Old: old} } if strings.HasPrefix(v, "-") && v != "-" { return UnsupportedOp{Err: fmt.Errorf("unsupported option '%s'", v)} } return SwitchOp{Target: argv[0]} } return UnsupportedOp{Err: fmt.Errorf("too many arguments")} } kubectx-0.11.0/cmd/kubectx/flags_test.go000066400000000000000000000074741516137246200201530ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "testing" "github.com/google/go-cmp/cmp" ) func Test_parseArgs_new(t *testing.T) { tests := []struct { name string args []string want Op }{ {name: "nil Args", args: nil, want: ListOp{}}, {name: "empty Args", args: []string{}, want: ListOp{}}, {name: "help shorthand", args: []string{"-h"}, want: HelpOp{}}, {name: "help long form", args: []string{"--help"}, want: HelpOp{}}, {name: "current shorthand", args: []string{"-c"}, want: CurrentOp{}}, {name: "current long form", args: []string{"--current"}, want: CurrentOp{}}, {name: "unset shorthand", args: []string{"-u"}, want: UnsetOp{}}, {name: "unset long form", args: []string{"--unset"}, want: UnsetOp{}}, {name: "switch by name", args: []string{"foo"}, want: SwitchOp{Target: "foo"}}, {name: "switch by swap", args: []string{"-"}, want: SwitchOp{Target: "-"}}, {name: "delete - without contexts", args: []string{"-d"}, want: UnsupportedOp{fmt.Errorf("'-d' needs arguments")}}, {name: "delete - current context", args: []string{"-d", "."}, want: DeleteOp{[]string{"."}}}, {name: "delete - multiple contexts", args: []string{"-d", ".", "a", "b"}, want: DeleteOp{[]string{".", "a", "b"}}}, {name: "rename context", args: []string{"a=b"}, want: RenameOp{"a", "b"}}, {name: "rename context with old=current", args: []string{"a=."}, want: RenameOp{"a", "."}}, {name: "shell shorthand", args: []string{"-s", "prod"}, want: ShellOp{Target: "prod"}}, {name: "shell long form", args: []string{"--shell", "prod"}, want: ShellOp{Target: "prod"}}, {name: "shell without context name", args: []string{"-s"}, want: UnsupportedOp{Err: fmt.Errorf("'-s' requires a context name argument (or fzf for interactive mode)")}}, {name: "shell with too many args", args: []string{"--shell", "a", "b"}, want: UnsupportedOp{Err: fmt.Errorf("'--shell' accepts at most one context name argument")}}, {name: "readonly shorthand", args: []string{"-r", "prod"}, want: ReadonlyShellOp{Target: "prod"}}, {name: "readonly long form", args: []string{"--readonly", "prod"}, want: ReadonlyShellOp{Target: "prod"}}, {name: "readonly without context name", args: []string{"-r"}, want: UnsupportedOp{Err: fmt.Errorf("'-r' requires a context name argument (or fzf for interactive mode)")}}, {name: "readonly with too many args", args: []string{"--readonly", "a", "b"}, want: UnsupportedOp{Err: fmt.Errorf("'--readonly' accepts at most one context name argument")}}, {name: "unrecognized flag", args: []string{"-x"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported option '-x'")}}, {name: "too many args", args: []string{"a", "b", "c"}, want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseArgs(tt.args) var opts cmp.Options if _, ok := tt.want.(UnsupportedOp); ok { opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool { return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error()) })) } if diff := cmp.Diff(got, tt.want, opts...); diff != "" { t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff) } }) } } kubectx-0.11.0/cmd/kubectx/fzf.go000066400000000000000000000075151516137246200166010ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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" "errors" "fmt" "io" "os" "os/exec" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type InteractiveSwitchOp struct { SelfCmd string } type InteractiveDeleteOp struct { SelfCmd string } func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } // parse kubeconfig just to see if it can be loaded kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxNames, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } if len(ctxNames) == 0 { return errors.New("no contexts found in the kubeconfig file") } cmd := exec.Command("fzf", "--ansi", "--no-preview") var out bytes.Buffer cmd.Stdin = os.Stdin cmd.Stderr = stderr cmd.Stdout = &out cmd.Env = append(os.Environ(), fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), fmt.Sprintf("%s=1", env.EnvForceColor)) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { return err } } choice := strings.TrimSpace(out.String()) if choice == "" { return errors.New("you did not choose any of the options") } name, err := switchContext(choice) if err != nil { return fmt.Errorf("failed to switch context: %w", err) } _ = printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(name)) return nil } func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } // parse kubeconfig just to see if it can be loaded kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxNames, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } if len(ctxNames) == 0 { return errors.New("no contexts found in config") } cmd := exec.Command("fzf", "--ansi", "--no-preview") var out bytes.Buffer cmd.Stdin = os.Stdin cmd.Stderr = stderr cmd.Stdout = &out cmd.Env = append(os.Environ(), fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), fmt.Sprintf("%s=1", env.EnvForceColor)) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { return err } } choice := strings.TrimSpace(out.String()) if choice == "" { return errors.New("you did not choose any of the options") } name, wasActiveContext, err := deleteContext(choice) if err != nil { return fmt.Errorf("failed to delete context: %w", err) } if wasActiveContext { printer.Warning(stderr, "You deleted the current context. Use \"%s\" to select a new context.", selfName()) } _ = printer.Success(stderr, `Deleted context %s.`, printer.SuccessColor.Sprint(name)) return nil } kubectx-0.11.0/cmd/kubectx/help.go000066400000000000000000000047011516137246200167360ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "os" "path/filepath" "strings" ) // HelpOp describes printing help. type HelpOp struct{} func (_ HelpOp) Run(stdout, _ io.Writer) error { return printUsage(stdout) } func printUsage(out io.Writer) error { help := `Manage and switch between kubectl contexts. USAGE: %PROG% : list the contexts %PROG% : switch to context %PROG% - : switch to the previous context %PROG% -s, --shell : start a shell scoped to context %PROG% -s, --shell : interactively select a context to start a shell %PROG% -r, --readonly : start a read-only shell for context %PROG% -r, --readonly : interactively select a context for read-only shell %PROG% -c, --current : show the current context name %PROG% -u, --unset : unset the current context %PROG% = : rename context to %PROG% =. : rename current-context to %PROG% -d [] : delete context ('.' for current-context) %SPAC% (this command won't delete the user/cluster entry %SPAC% referenced by the context entry) %PROG% -h, --help : show this message %PROG% -V, --version : show version` help = strings.ReplaceAll(help, "%PROG%", selfName()) help = strings.ReplaceAll(help, "%SPAC%", strings.Repeat(" ", len(selfName()))) _, err := fmt.Fprintf(out, "%s\n", help) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } // selfName guesses how the user invoked the program. func selfName() string { me := filepath.Base(os.Args[0]) pluginPrefix := "kubectl-" if strings.HasPrefix(me, pluginPrefix) { return "kubectl " + strings.TrimPrefix(me, pluginPrefix) } return "kubectx" } kubectx-0.11.0/cmd/kubectx/help_test.go000066400000000000000000000017631516137246200200020ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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" "strings" "testing" ) func TestPrintHelp(t *testing.T) { var buf bytes.Buffer if err := (&HelpOp{}).Run(&buf, &buf); err != nil { t.Fatal(err) } out := buf.String() if !strings.Contains(out, "USAGE:") { t.Errorf("help string doesn't contain USAGE: ; output=\"%s\"", out) } if !strings.HasSuffix(out, "\n") { t.Errorf("does not end with New line; output=\"%s\"", out) } } kubectx-0.11.0/cmd/kubectx/isolated_shell_guard.go000066400000000000000000000010761516137246200221650ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" ) func checkIsolatedMode() error { if os.Getenv(env.EnvIsolatedShell) != "1" { return nil } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("you are in a locked single-context shell, use 'exit' to leave") } cur, _ := kc.GetCurrentContext() return fmt.Errorf("you are in a locked single-context shell (\"%s\"), use 'exit' to leave", cur) } kubectx-0.11.0/cmd/kubectx/list.go000066400000000000000000000031631516137246200167620ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "facette.io/natsort" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // ListOp describes listing contexts. type ListOp struct{} func (_ ListOp) Run(stdout, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxs, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } natsort.Sort(ctxs) cur, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } for _, c := range ctxs { s := c if c == cur { s = printer.ActiveItemColor.Sprint(c) } fmt.Fprintf(stdout, "%s\n", s) } return nil } kubectx-0.11.0/cmd/kubectx/main.go000066400000000000000000000024361516137246200167350ustar00rootroot00000000000000// kubectx(1) is a utility to manage and switch between kubectl contexts. // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "os" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/printer" "github.com/fatih/color" ) type Op interface { Run(stdout, stderr io.Writer) error } func main() { cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ()) op := parseArgs(os.Args[1:]) if err := op.Run(color.Output, color.Error); err != nil { printer.Error(color.Error, "%s", err) if _, ok := os.LookupEnv(env.EnvDebug); ok { // print stack trace in verbose mode fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err) } defer os.Exit(1) } } kubectx-0.11.0/cmd/kubectx/readonly_shell.go000066400000000000000000000051661516137246200210200ustar00rootroot00000000000000package main import ( "context" "fmt" "io" "os" "time" "github.com/fatih/color" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/printer" "github.com/ahmetb/kubectx/internal/proxy" ) // InteractiveReadonlyShellOp launches fzf to pick a context, then starts a readonly shell. type InteractiveReadonlyShellOp struct { SelfCmd string } // ReadonlyShellOp starts a read-only sub-shell for a context. type ReadonlyShellOp struct { Target string } func (op InteractiveReadonlyShellOp) Run(_, stderr io.Writer) error { choice, err := fzfPickContext(op.SelfCmd, stderr) if err != nil || choice == "" { return err } return ReadonlyShellOp{Target: choice}.Run(nil, stderr) } func (op ReadonlyShellOp) Run(_, stderr io.Writer) error { badgeColor := color.New(color.BgYellow, color.FgBlack, color.Bold) printer.EnableOrDisableColor(badgeColor) s := &shellSession{ target: op.Target, extraEnv: []string{env.EnvReadonlyShell + "=1"}, printEntry: func(w io.Writer, ctxName string) { fmt.Fprintf(w, "%s kubectl context is %s in READ-ONLY mode — type 'exit' to leave.\n", badgeColor.Sprint("[READONLY SHELL]"), printer.WarningColor.Sprint(ctxName)) }, printExit: func(w io.Writer, prevCtx string) { fmt.Fprintf(w, "%s kubectl context is now %s.\n", badgeColor.Sprint("[READONLY SHELL EXITED]"), printer.WarningColor.Sprint(prevCtx)) }, transformKubeconfig: func(data []byte) ([]byte, func(), error) { // Write original kubeconfig to temp file for the proxy to load TLS/auth. origFile, err := os.CreateTemp("", "kubectx-readonly-orig-*.yaml") if err != nil { return nil, nil, fmt.Errorf("failed to create temp kubeconfig file: %w", err) } origPath := origFile.Name() if _, err := origFile.Write(data); err != nil { origFile.Close() os.Remove(origPath) return nil, nil, fmt.Errorf("failed to write temp kubeconfig: %w", err) } origFile.Close() // Start the readonly proxy. p, err := proxy.Start(proxy.Config{ KubeconfigPath: origPath, ContextName: op.Target, }) if err != nil { os.Remove(origPath) return nil, nil, fmt.Errorf("failed to start readonly proxy: %w", err) } // Rewrite kubeconfig to point to the proxy. rewritten, err := proxy.RewriteKubeconfig(data, p.Addr()) if err != nil { p.Shutdown(context.Background()) os.Remove(origPath) return nil, nil, fmt.Errorf("failed to rewrite kubeconfig: %w", err) } time.Sleep(10 * time.Millisecond) cleanup := func() { p.Shutdown(context.Background()) os.Remove(origPath) } return rewritten, cleanup, nil }, } return s.run(stderr) } kubectx-0.11.0/cmd/kubectx/rename.go000066400000000000000000000055501516137246200172600ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "strings" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // RenameOp indicates intention to rename contexts. type RenameOp struct { New string // NAME of New context Old string // NAME of Old context (or '.' for current-context) } // parseRenameSyntax parses A=B form into [A,B] and returns // whether it is parsed correctly. func parseRenameSyntax(v string) (string, string, bool) { new, old, ok := strings.Cut(v, "=") if !ok || new == "" || old == "" { return "", "", false } return new, old, true } // rename changes the old (NAME or '.' for current-context) // to the "new" value. If the old refers to the current-context, // current-context preference is also updated. func (op RenameOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } cur, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if op.Old == "." { op.Old = cur } oldExists, err := kc.ContextExists(op.Old) if err != nil { return fmt.Errorf("failed to check context: %w", err) } if !oldExists { return fmt.Errorf("context \"%s\" not found, can't rename it", op.Old) } newExists, err := kc.ContextExists(op.New) if err != nil { return fmt.Errorf("failed to check context: %w", err) } if newExists { printer.Warning(stderr, "context \"%s\" exists, overwriting it.", op.New) if err := kc.DeleteContextEntry(op.New); err != nil { return fmt.Errorf("failed to delete new context to overwrite it: %w", err) } } if err := kc.ModifyContextName(op.Old, op.New); err != nil { return fmt.Errorf("failed to change context name: %w", err) } if op.Old == cur { if err := kc.ModifyCurrentContext(op.New); err != nil { return fmt.Errorf("failed to set current-context to new name: %w", err) } } if err := kc.Save(); err != nil { return fmt.Errorf("failed to save modified kubeconfig: %w", err) } _ = printer.Success(stderr, "Context %s renamed to %s.", printer.SuccessColor.Sprint(op.Old), printer.SuccessColor.Sprint(op.New)) return nil } kubectx-0.11.0/cmd/kubectx/rename_test.go000066400000000000000000000031121516137246200203070ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "testing" "github.com/google/go-cmp/cmp" ) func Test_parseRenameSyntax(t *testing.T) { type out struct { New string Old string OK bool } tests := []struct { name string in string want out }{ { name: "no equals sign", in: "foo", want: out{OK: false}, }, { name: "no left side", in: "=a", want: out{OK: false}, }, { name: "no right side", in: "a=", want: out{OK: false}, }, { name: "correct format", in: "a=b", want: out{ New: "a", Old: "b", OK: true, }, }, { name: "correct format with current context", in: "NEW_NAME=.", want: out{ New: "NEW_NAME", Old: ".", OK: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { new, old, ok := parseRenameSyntax(tt.in) got := out{ New: new, Old: old, OK: ok, } diff := cmp.Diff(tt.want, got) if diff != "" { t.Errorf("parseRenameSyntax() diff=%s", diff) } }) } } kubectx-0.11.0/cmd/kubectx/shell.go000066400000000000000000000050571516137246200171220ustar00rootroot00000000000000package main import ( "fmt" "io" "os" "os/exec" "runtime" "github.com/fatih/color" "github.com/ahmetb/kubectx/internal/printer" ) // InteractiveShellOp launches fzf to pick a context, then starts an isolated shell. type InteractiveShellOp struct { SelfCmd string } // ShellOp indicates intention to start a scoped sub-shell for a context. type ShellOp struct { Target string } func (op InteractiveShellOp) Run(_, stderr io.Writer) error { choice, err := fzfPickContext(op.SelfCmd, stderr) if err != nil || choice == "" { return err } return ShellOp{Target: choice}.Run(nil, stderr) } func (op ShellOp) Run(_, stderr io.Writer) error { badgeColor := color.New(color.BgRed, color.FgWhite, color.Bold) printer.EnableOrDisableColor(badgeColor) s := &shellSession{ target: op.Target, printEntry: func(w io.Writer, ctxName string) { fmt.Fprintf(w, "%s kubectl context is %s in this shell — type 'exit' to leave.\n", badgeColor.Sprint("[ISOLATED SHELL]"), printer.WarningColor.Sprint(ctxName)) }, printExit: func(w io.Writer, prevCtx string) { fmt.Fprintf(w, "%s kubectl context is now %s.\n", badgeColor.Sprint("[ISOLATED SHELL EXITED]"), printer.WarningColor.Sprint(prevCtx)) }, } return s.run(stderr) } func resolveKubectl() (string, error) { if v := os.Getenv("KUBECTL"); v != "" { return v, nil } path, err := exec.LookPath("kubectl") if err != nil { return "", fmt.Errorf("kubectl is required for --shell but was not found in PATH") } return path, nil } func extractMinimalKubeconfig(kubectlPath, contextName string) ([]byte, error) { cmd := exec.Command(kubectlPath, "config", "view", "--minify", "--flatten", "--context", contextName) cmd.Env = os.Environ() data, err := cmd.Output() if err != nil { return nil, fmt.Errorf("kubectl config view failed: %w", err) } return data, nil } func detectShell() string { if runtime.GOOS == "windows" { // cmd.exe always sets the PROMPT env var, so if it is present // we can reliably assume we are running inside cmd.exe. if os.Getenv("PROMPT") != "" { return "cmd.exe" } // Otherwise assume PowerShell. PSModulePath is always set on // Windows regardless of the shell, so it cannot be used as a // discriminator; however the absence of PROMPT is a strong // enough signal that we are in a PowerShell session. if pwsh, err := exec.LookPath("pwsh"); err == nil { return pwsh } if powershell, err := exec.LookPath("powershell"); err == nil { return powershell } return "cmd.exe" } if v := os.Getenv("SHELL"); v != "" { return v } return "/bin/sh" } kubectx-0.11.0/cmd/kubectx/shell_session.go000066400000000000000000000102321516137246200206540ustar00rootroot00000000000000package main import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // shellSession holds the configuration for spawning an isolated sub-shell. type shellSession struct { target string extraEnv []string // additional env vars beyond KUBECONFIG + KUBECTX_ISOLATED_SHELL printEntry func(stderr io.Writer, ctxName string) printExit func(stderr io.Writer, prevCtx string) // transformKubeconfig optionally transforms the minified kubeconfig bytes // before writing them to the shell's temp file. The returned cleanup func // is called after the shell exits (e.g. to shut down a proxy). // If nil, the kubeconfig is used as-is. transformKubeconfig func(data []byte) (newData []byte, cleanup func(), err error) } func (s *shellSession) run(stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kubectlPath, err := resolveKubectl() if err != nil { return err } // Verify context exists and get current context for exit message. kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } exists, err := kc.ContextExists(s.target) if err != nil { return fmt.Errorf("failed to check context: %w", err) } if !exists { return fmt.Errorf("no context exists with the name: %q", s.target) } previousCtx, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } // Extract minimal kubeconfig for the target context. data, err := extractMinimalKubeconfig(kubectlPath, s.target) if err != nil { return fmt.Errorf("failed to extract kubeconfig for context: %w", err) } // Optionally transform the kubeconfig (e.g. rewrite for readonly proxy). var cleanup func() if s.transformKubeconfig != nil { data, cleanup, err = s.transformKubeconfig(data) if err != nil { return err } if cleanup != nil { defer cleanup() } } // Write kubeconfig to temp file. tmpFile, err := os.CreateTemp("", "kubectx-shell-*.yaml") if err != nil { return fmt.Errorf("failed to create temp kubeconfig file: %w", err) } tmpPath := tmpFile.Name() defer os.Remove(tmpPath) if _, err := tmpFile.Write(data); err != nil { tmpFile.Close() return fmt.Errorf("failed to write temp kubeconfig: %w", err) } tmpFile.Close() // Print entry message. s.printEntry(stderr, s.target) // Detect and start shell. shellBin := detectShell() cmd := exec.Command(shellBin) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), append([]string{ "KUBECONFIG=" + tmpPath, env.EnvIsolatedShell + "=1", }, s.extraEnv...)..., ) _ = cmd.Run() // Print exit message. s.printExit(stderr, previousCtx) return nil } // fzfPickContext launches fzf for interactive context selection. func fzfPickContext(selfCmd string, stderr io.Writer) (string, error) { if err := checkIsolatedMode(); err != nil { return "", err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return "", nil } return "", fmt.Errorf("kubeconfig error: %w", err) } ctxNames, err := kc.ContextNames() if err != nil { return "", fmt.Errorf("failed to get context names: %w", err) } if len(ctxNames) == 0 { return "", errors.New("no contexts found in the kubeconfig file") } cmd := exec.Command("fzf", "--ansi", "--no-preview") var out bytes.Buffer cmd.Stdin = os.Stdin cmd.Stderr = stderr cmd.Stdout = &out cmd.Env = append(os.Environ(), fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", selfCmd), fmt.Sprintf("%s=1", env.EnvForceColor)) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { return "", err } } choice := strings.TrimSpace(out.String()) if choice == "" { return "", errors.New("you did not choose any of the options") } return choice, nil } kubectx-0.11.0/cmd/kubectx/shell_test.go000066400000000000000000000051321516137246200201530ustar00rootroot00000000000000package main import ( "bytes" "runtime" "testing" "github.com/ahmetb/kubectx/internal/env" ) func Test_detectShell_unix(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping unix shell detection test on windows") } tests := []struct { name string shellEnv string want string }{ { name: "SHELL env set", shellEnv: "/bin/zsh", want: "/bin/zsh", }, { name: "SHELL env empty, falls back to /bin/sh", shellEnv: "", want: "/bin/sh", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("SHELL", tt.shellEnv) got := detectShell() if got != tt.want { t.Errorf("detectShell() = %q, want %q", got, tt.want) } }) } } func Test_ShellOp_blockedWhenNested(t *testing.T) { // Simulate being inside an isolated shell t.Setenv(env.EnvIsolatedShell, "1") op := ShellOp{Target: "some-context"} var stdout, stderr bytes.Buffer err := op.Run(&stdout, &stderr) if err == nil { t.Fatal("expected error when running ShellOp inside isolated shell, got nil") } want := "locked single-context shell to" if !bytes.Contains([]byte(err.Error()), []byte(want)) { // The error may not contain the context name if kubeconfig is not available, // but it should still be blocked want2 := "locked single-context shell" if !bytes.Contains([]byte(err.Error()), []byte(want2)) { t.Errorf("error message %q does not contain %q", err.Error(), want2) } } } func Test_resolveKubectl_envVar(t *testing.T) { t.Setenv("KUBECTL", "/custom/path/kubectl") got, err := resolveKubectl() if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "/custom/path/kubectl" { t.Errorf("resolveKubectl() = %q, want %q", got, "/custom/path/kubectl") } } func Test_resolveKubectl_inPath(t *testing.T) { t.Setenv("KUBECTL", "") // kubectl should be findable in PATH on most dev machines got, err := resolveKubectl() if err != nil { t.Skip("kubectl not in PATH, skipping") } if got == "" { t.Error("resolveKubectl() returned empty string") } } func Test_checkIsolatedMode_notSet(t *testing.T) { t.Setenv(env.EnvIsolatedShell, "") err := checkIsolatedMode() if err != nil { t.Errorf("expected nil error when not in isolated mode, got: %v", err) } } func Test_checkIsolatedMode_set(t *testing.T) { t.Setenv(env.EnvIsolatedShell, "1") err := checkIsolatedMode() if err == nil { t.Fatal("expected error when in isolated mode, got nil") } want := "locked single-context shell" if !bytes.Contains([]byte(err.Error()), []byte(want)) { t.Errorf("error message %q does not contain %q", err.Error(), want) } } kubectx-0.11.0/cmd/kubectx/state.go000066400000000000000000000027641516137246200171350ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "errors" "fmt" "os" "path/filepath" "github.com/ahmetb/kubectx/internal/cmdutil" ) func kubectxPrevCtxFile() (string, error) { dir := cmdutil.CacheDir() if dir == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") } return filepath.Join(dir, "kubectx"), nil } // readLastContext returns the saved previous context // if the state file exists, otherwise returns "". func readLastContext(path string) (string, error) { b, err := os.ReadFile(path) if os.IsNotExist(err) { return "", nil } return string(b), err } // writeLastContext saves the specified value to the state file. // It creates missing parent directories. func writeLastContext(path, value string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create parent directories: %w", err) } return os.WriteFile(path, []byte(value), 0644) } kubectx-0.11.0/cmd/kubectx/state_test.go000066400000000000000000000051321516137246200201640ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "os" "path/filepath" "testing" ) func Test_readLastContext_nonExistingFile(t *testing.T) { s, err := readLastContext(filepath.FromSlash("/non/existing/file")) if err != nil { t.Fatal(err) } if s != "" { t.Fatalf("expected empty string; got=\"%s\"", s) } } func Test_readLastContext(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "testfile") if err := os.WriteFile(path, []byte("foo"), 0644); err != nil { t.Fatal(err) } s, err := readLastContext(path) if err != nil { t.Fatal(err) } if expected := "foo"; s != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, s) } } func Test_writeLastContext_err(t *testing.T) { path := filepath.Join(os.DevNull, "foo", "bar") err := writeLastContext(path, "foo") if err == nil { t.Fatal("got empty error") } } func Test_writeLastContext(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "foo", "bar") if err := writeLastContext(path, "ctx1"); err != nil { t.Fatal(err) } v, err := readLastContext(path) if err != nil { t.Fatal(err) } if expected := "ctx1"; v != expected { t.Fatalf("read wrong value=\"%s\"; expected=\"%s\"", v, expected) } } func Test_kubectxFilePath(t *testing.T) { t.Setenv("HOME", filepath.FromSlash("/foo/bar")) t.Setenv("XDG_CACHE_HOME", "") expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx") v, err := kubectxPrevCtxFile() if err != nil { t.Fatal(err) } if v != expected { t.Fatalf("expected=\"%s\" got=\"%s\"", expected, v) } } func Test_kubectxFilePath_xdgCacheHome(t *testing.T) { t.Setenv("XDG_CACHE_HOME", filepath.FromSlash("/tmp/xdg-cache")) expected := filepath.Join(filepath.FromSlash("/tmp/xdg-cache"), "kubectx") v, err := kubectxPrevCtxFile() if err != nil { t.Fatal(err) } if v != expected { t.Fatalf("expected=\"%s\" got=\"%s\"", expected, v) } } func Test_kubectxFilePath_error(t *testing.T) { t.Setenv("HOME", "") t.Setenv("USERPROFILE", "") _, err := kubectxPrevCtxFile() if err == nil { t.Fatal(err) } } kubectx-0.11.0/cmd/kubectx/switch.go000066400000000000000000000055701516137246200173140ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // SwitchOp indicates intention to switch contexts. type SwitchOp struct { Target string // '-' for back and forth, or NAME } func (op SwitchOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } var newCtx string var err error if op.Target == "-" { newCtx, err = swapContext() } else { newCtx, err = switchContext(op.Target) } if err != nil { return fmt.Errorf("failed to switch context: %w", err) } if err = printer.Success(stderr, "Switched to context \"%s\".", printer.SuccessColor.Sprint(newCtx)); err != nil { return fmt.Errorf("print error: %w", err) } return nil } // switchContext switches to specified context name. func switchContext(name string) (string, error) { prevCtxFile, err := kubectxPrevCtxFile() if err != nil { return "", fmt.Errorf("failed to determine state file: %w", err) } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return "", fmt.Errorf("kubeconfig error: %w", err) } prev, err := kc.GetCurrentContext() if err != nil { return "", fmt.Errorf("failed to get current context: %w", err) } exists, err := kc.ContextExists(name) if err != nil { return "", fmt.Errorf("failed to check context: %w", err) } if !exists { return "", fmt.Errorf("no context exists with the name: \"%s\"", name) } if err := kc.ModifyCurrentContext(name); err != nil { return "", err } if err := kc.Save(); err != nil { return "", fmt.Errorf("failed to save kubeconfig: %w", err) } if prev != name { if err := writeLastContext(prevCtxFile, prev); err != nil { return "", fmt.Errorf("failed to save previous context name: %w", err) } } return name, nil } // swapContext switches to previously switch context. func swapContext() (string, error) { prevCtxFile, err := kubectxPrevCtxFile() if err != nil { return "", fmt.Errorf("failed to determine state file: %w", err) } prev, err := readLastContext(prevCtxFile) if err != nil { return "", fmt.Errorf("failed to read previous context file: %w", err) } if prev == "" { return "", errors.New("no previous context found") } return switchContext(prev) } kubectx-0.11.0/cmd/kubectx/unset.go000066400000000000000000000027301516137246200171440ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // UnsetOp indicates intention to remove current-context preference. type UnsetOp struct{} func (_ UnsetOp) Run(_, stderr io.Writer) error { if err := checkIsolatedMode(); err != nil { return err } kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } if err := kc.UnsetCurrentContext(); err != nil { return fmt.Errorf("error while modifying current-context: %w", err) } if err := kc.Save(); err != nil { return fmt.Errorf("failed to save kubeconfig file after modification: %w", err) } err := printer.Success(stderr, "Active context unset for kubectl.") if err != nil { return fmt.Errorf("write error: %w", err) } return nil } kubectx-0.11.0/cmd/kubectx/version.go000066400000000000000000000005421516137246200174720ustar00rootroot00000000000000package main import ( "fmt" "io" ) var ( version = "v0.0.0+unknown" // populated by goreleaser ) // VersionOp describes printing version string. type VersionOp struct{} func (_ VersionOp) Run(stdout, _ io.Writer) error { _, err := fmt.Fprintf(stdout, "%s\n", version) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } kubectx-0.11.0/cmd/kubens/000077500000000000000000000000001516137246200152775ustar00rootroot00000000000000kubectx-0.11.0/cmd/kubens/current.go000066400000000000000000000025361516137246200173160ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" ) type CurrentOp struct{} func (c CurrentOp) Run(stdout, _ io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } ctx, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if ctx == "" { return errors.New("current-context is not set") } ns, err := kc.NamespaceOfContext(ctx) if err != nil { return fmt.Errorf("failed to read namespace of \"%s\": %w", ctx, err) } _, err = fmt.Fprintln(stdout, ns) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } kubectx-0.11.0/cmd/kubens/flags.go000066400000000000000000000040521516137246200167230ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "os" "slices" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" ) // UnsupportedOp indicates an unsupported flag. type UnsupportedOp struct{ Err error } func (op UnsupportedOp) Run(_, _ io.Writer) error { return op.Err } // parseArgs looks at flags (excl. executable name, i.e. argv[0]) // and decides which operation should be taken. func parseArgs(argv []string) Op { n := len(argv) if n == 0 { if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveSwitchOp{SelfCmd: os.Args[0]} } return ListOp{} } if n == 1 { v := argv[0] switch v { case "--help", "-h": return HelpOp{} case "--version", "-V": return VersionOp{} case "--current", "-c": return CurrentOp{} case "--unset", "-u": return UnsetOp{} default: return getSwitchOp(v, false) } } else if n == 2 { // {namespace} -f|--force name := argv[0] force := slices.Contains([]string{"-f", "--force"}, argv[1]) if !force { if !slices.Contains([]string{"-f", "--force"}, argv[0]) { return UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", argv)} } // -f|--force {namespace} force = true name = argv[1] } return getSwitchOp(name, force) } return UnsupportedOp{Err: fmt.Errorf("too many arguments")} } func getSwitchOp(v string, force bool) Op { if strings.HasPrefix(v, "-") && v != "-" { return UnsupportedOp{Err: fmt.Errorf("unsupported option %q", v)} } return SwitchOp{Target: v, Force: force} } kubectx-0.11.0/cmd/kubens/flags_test.go000066400000000000000000000060421516137246200177630ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "testing" "github.com/google/go-cmp/cmp" ) func Test_parseArgs_new(t *testing.T) { tests := []struct { name string args []string want Op }{ {name: "nil Args", args: nil, want: ListOp{}}, {name: "empty Args", args: []string{}, want: ListOp{}}, {name: "help shorthand", args: []string{"-h"}, want: HelpOp{}}, {name: "help long form", args: []string{"--help"}, want: HelpOp{}}, {name: "current shorthand", args: []string{"-c"}, want: CurrentOp{}}, {name: "current long form", args: []string{"--current"}, want: CurrentOp{}}, {name: "unset shorthand", args: []string{"-u"}, want: UnsetOp{}}, {name: "unset long form", args: []string{"--unset"}, want: UnsetOp{}}, {name: "switch by name", args: []string{"foo"}, want: SwitchOp{Target: "foo"}}, {name: "switch by name force short flag", args: []string{"foo", "-f"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name force long flag", args: []string{"foo", "--force"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name force short flag before name", args: []string{"-f", "foo"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name force long flag before name", args: []string{"--force", "foo"}, want: SwitchOp{Target: "foo", Force: true}}, {name: "switch by name unknown arguments", args: []string{"foo", "-x"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"foo", "-x"})}}, {name: "switch by name unknown arguments", args: []string{"-x", "foo"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported arguments %q", []string{"-x", "foo"})}}, {name: "switch by swap", args: []string{"-"}, want: SwitchOp{Target: "-"}}, {name: "unrecognized flag", args: []string{"-x"}, want: UnsupportedOp{Err: fmt.Errorf("unsupported option %q", "-x")}}, {name: "too many args", args: []string{"a", "b", "c"}, want: UnsupportedOp{Err: fmt.Errorf("too many arguments")}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseArgs(tt.args) var opts cmp.Options if _, ok := tt.want.(UnsupportedOp); ok { opts = append(opts, cmp.Comparer(func(x, y UnsupportedOp) bool { return (x.Err == nil && y.Err == nil) || (x.Err.Error() == y.Err.Error()) })) } if diff := cmp.Diff(got, tt.want, opts...); diff != "" { t.Errorf("parseArgs(%#v) diff: %s", tt.args, diff) } }) } } kubectx-0.11.0/cmd/kubens/fzf.go000066400000000000000000000044441516137246200164210ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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" "errors" "fmt" "io" "os" "os/exec" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type InteractiveSwitchOp struct { SelfCmd string } // TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go. func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { // parse kubeconfig just to see if it can be loaded kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } return fmt.Errorf("kubeconfig error: %w", err) } ctxNames, err := kc.ContextNames() if err != nil { return fmt.Errorf("failed to get context names: %w", err) } if len(ctxNames) == 0 { return errors.New("no contexts found in the kubeconfig file") } cmd := exec.Command("fzf", "--ansi", "--no-preview") var out bytes.Buffer cmd.Stdin = os.Stdin cmd.Stderr = stderr cmd.Stdout = &out cmd.Env = append(os.Environ(), fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), fmt.Sprintf("%s=1", env.EnvForceColor)) if err := cmd.Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { return err } } choice := strings.TrimSpace(out.String()) if choice == "" { return errors.New("you did not choose any of the options") } name, err := switchNamespace(kc, choice, false) if err != nil { return fmt.Errorf("failed to switch namespace: %w", err) } _ = printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(name)) return nil } kubectx-0.11.0/cmd/kubens/help.go000066400000000000000000000037511516137246200165640ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "os" "path/filepath" "strings" ) // HelpOp describes printing help. type HelpOp struct{} func (_ HelpOp) Run(stdout, _ io.Writer) error { return printUsage(stdout) } func printUsage(out io.Writer) error { help := `Switch between Kubernetes namespaces. USAGE: %PROG% : list the namespaces in the current context %PROG% : change the active namespace of current context %PROG% --force/-f : force change the active namespace of current context (even if it doesn't exist) %PROG% - : switch to the previous namespace in this context %PROG% -c, --current : show the current namespace %PROG% -h,--help : show this message %PROG% -u,--unset : unset the namespace choice (set to 'default') %PROG% -V,--version : show version` // TODO this replace logic is duplicated between this and kubectx help = strings.ReplaceAll(help, "%PROG%", selfName()) _, err := fmt.Fprintf(out, "%s\n", help) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } // selfName guesses how the user invoked the program. func selfName() string { // TODO this method is duplicated between this and kubectx me := filepath.Base(os.Args[0]) pluginPrefix := "kubectl-" if strings.HasPrefix(me, pluginPrefix) { return "kubectl " + strings.TrimPrefix(me, pluginPrefix) } return "kubens" } kubectx-0.11.0/cmd/kubens/list.go000066400000000000000000000073601516137246200166070ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "context" "errors" "fmt" "io" "os" "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type ListOp struct{} func (op ListOp) Run(stdout, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } ctx, err := kc.GetCurrentContext() if err != nil { return fmt.Errorf("failed to get current context: %w", err) } if ctx == "" { return errors.New("current-context is not set") } curNs, err := kc.NamespaceOfContext(ctx) if err != nil { return fmt.Errorf("cannot read current namespace: %w", err) } ns, err := queryNamespaces(kc) if err != nil { return fmt.Errorf("could not list namespaces (is the cluster accessible?): %w", err) } for _, c := range ns { s := c if c == curNs { s = printer.ActiveItemColor.Sprint(c) } fmt.Fprintf(stdout, "%s\n", s) } return nil } func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) { if os.Getenv("_MOCK_NAMESPACES") != "" { return []string{"ns1", "ns2"}, nil } clientset, err := newKubernetesClientSet(kc) if err != nil { return nil, fmt.Errorf("failed to initialize k8s REST client: %w", err) } var out []string var next string for { list, err := clientset.CoreV1().Namespaces().List( context.Background(), metav1.ListOptions{ Limit: 500, Continue: next, }) if err != nil { return nil, fmt.Errorf("failed to list namespaces from k8s API: %w", err) } next = list.Continue out = slices.Grow(out, len(list.Items)) for _, it := range list.Items { out = append(out, it.Name) } if next == "" { break } } return out, nil } func newKubernetesClientSet(kc *kubeconfig.Kubeconfig) (*kubernetes.Clientset, error) { var cfg *rest.Config var err error if paths := kc.ConfigPaths(); len(paths) > 0 { // Load from file paths so that client-go resolves relative paths // (e.g. in exec credential plugins) relative to the kubeconfig directory. // // TODO: This re-reads and re-parses the kubeconfig files from disk via // client-go, duplicating work already done by our kyaml-based loader. // A better approach would be to extract the current context/cluster/user // entries from the already-parsed multi-file kubeconfig and normalize // relative paths in memory based on which file each entry was read from. cfg, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig( &clientcmd.ClientConfigLoadingRules{Precedence: paths}, &clientcmd.ConfigOverrides{}, ).ClientConfig() } else { // Fallback for in-memory configs (e.g. tests). var b []byte b, err = kc.Bytes() if err != nil { return nil, fmt.Errorf("failed to convert in-memory kubeconfig to yaml: %w", err) } cfg, err = clientcmd.RESTConfigFromKubeConfig(b) } if err != nil { return nil, fmt.Errorf("failed to initialize config: %w", err) } return kubernetes.NewForConfig(cfg) } kubectx-0.11.0/cmd/kubens/main.go000066400000000000000000000024261516137246200165560ustar00rootroot00000000000000// kubens(1) is a utility to switch between Kubernetes namespaces. // Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "fmt" "io" "os" "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/printer" "github.com/fatih/color" ) type Op interface { Run(stdout, stderr io.Writer) error } func main() { cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ()) op := parseArgs(os.Args[1:]) if err := op.Run(color.Output, color.Error); err != nil { printer.Error(color.Error, "%s", err) if _, ok := os.LookupEnv(env.EnvDebug); ok { // print stack trace in verbose mode fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err) } defer os.Exit(1) } } kubectx-0.11.0/cmd/kubens/statefile.go000066400000000000000000000035121516137246200176070ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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" "os" "path/filepath" "runtime" "strings" "github.com/ahmetb/kubectx/internal/cmdutil" ) var defaultDir = filepath.Join(cmdutil.CacheDir(), "kubens") type NSFile struct { dir string ctx string } func NewNSFile(ctx string) NSFile { return NSFile{dir: defaultDir, ctx: ctx} } func (f NSFile) path() string { fn := f.ctx if isWindows() { // bug 230: eks clusters contain ':' in ctx name, not a valid file name for win32 fn = strings.ReplaceAll(fn, ":", "__") } return filepath.Join(f.dir, fn) } // Load reads the previous namespace setting, or returns empty if not exists. func (f NSFile) Load() (string, error) { b, err := os.ReadFile(f.path()) if err != nil { if os.IsNotExist(err) { return "", nil } return "", err } return string(bytes.TrimSpace(b)), nil } // Save stores the previous namespace information in the file. func (f NSFile) Save(value string) error { d := filepath.Dir(f.path()) if err := os.MkdirAll(d, 0755); err != nil { return err } return os.WriteFile(f.path(), []byte(value), 0644) } // isWindows determines if the process is running on windows OS. func isWindows() bool { if os.Getenv("_FORCE_GOOS") == "windows" { // for testing return true } return runtime.GOOS == "windows" } kubectx-0.11.0/cmd/kubens/statefile_test.go000066400000000000000000000032221516137246200206440ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "runtime" "strings" "testing" ) func TestNSFile(t *testing.T) { td := t.TempDir() f := NewNSFile("foo") f.dir = td v, err := f.Load() if err != nil { t.Fatal(err) } if v != "" { t.Fatalf("Load() expected empty; got=%v", err) } err = f.Save("bar") if err != nil { t.Fatalf("Save() err=%v", err) } v, err = f.Load() if err != nil { t.Fatal(err) } if expected := "bar"; v != expected { t.Fatalf("Load()=\"%s\"; expected=\"%s\"", v, expected) } } func TestNSFile_path_windows(t *testing.T) { t.Setenv("_FORCE_GOOS", "windows") fp := NewNSFile("a:b:c").path() if expected := "a__b__c"; !strings.HasSuffix(fp, expected) { t.Fatalf("file did not have expected ending %q: %s", expected, fp) } } func Test_isWindows(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("won't test this case on windows") } got := isWindows() if got { t.Fatalf("isWindows() returned true for %s", runtime.GOOS) } t.Setenv("_FORCE_GOOS", "windows") if !isWindows() { t.Fatalf("isWindows() failed to detect windows with env override.") } } kubectx-0.11.0/cmd/kubens/switch.go000066400000000000000000000064711516137246200171370ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "context" "errors" "fmt" "io" "os" errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type SwitchOp struct { Target string // '-' for back and forth, or NAME Force bool // force switch even if the namespace doesn't exist } func (s SwitchOp) Run(_, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } toNS, err := switchNamespace(kc, s.Target, s.Force) if err != nil { return err } err = printer.Success(stderr, "Active namespace is \"%s\"", printer.SuccessColor.Sprint(toNS)) return err } func switchNamespace(kc *kubeconfig.Kubeconfig, ns string, force bool) (string, error) { ctx, err := kc.GetCurrentContext() if err != nil { return "", fmt.Errorf("failed to get current context: %w", err) } if ctx == "" { return "", errors.New("current-context is not set") } curNS, err := kc.NamespaceOfContext(ctx) if err != nil { return "", fmt.Errorf("failed to get current namespace: %w", err) } f := NewNSFile(ctx) prev, err := f.Load() if err != nil { return "", fmt.Errorf("failed to load previous namespace from file: %w", err) } if ns == "-" { if prev == "" { return "", fmt.Errorf("No previous namespace found for current context (%s)", ctx) } ns = prev } if !force { ok, err := namespaceExists(kc, ns) if err != nil { return "", fmt.Errorf("failed to query if namespace exists (is cluster accessible?): %w", err) } if !ok { return "", fmt.Errorf("no namespace exists with name \"%s\"", ns) } } if err := kc.SetNamespace(ctx, ns); err != nil { return "", fmt.Errorf("failed to change to namespace \"%s\": %w", ns, err) } if err := kc.Save(); err != nil { return "", fmt.Errorf("failed to save kubeconfig file: %w", err) } if curNS != ns { if err := f.Save(curNS); err != nil { return "", fmt.Errorf("failed to save the previous namespace to file: %w", err) } } return ns, nil } func namespaceExists(kc *kubeconfig.Kubeconfig, ns string) (bool, error) { // for tests if os.Getenv("_MOCK_NAMESPACES") != "" { return ns == "ns1" || ns == "ns2", nil } clientset, err := newKubernetesClientSet(kc) if err != nil { return false, fmt.Errorf("failed to initialize k8s REST client: %w", err) } namespace, err := clientset.CoreV1().Namespaces().Get(context.Background(), ns, metav1.GetOptions{}) if errors2.IsNotFound(err) { return false, nil } if err != nil { return false, fmt.Errorf("failed to query namespace %q from k8s API: %w", ns, err) } return namespace != nil, nil } kubectx-0.11.0/cmd/kubens/unset.go000066400000000000000000000033221516137246200167640ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 ( "errors" "fmt" "io" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) // UnsetOp indicates intention to remove current namespace preference. type UnsetOp struct{} func (_ UnsetOp) Run(_, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return fmt.Errorf("kubeconfig error: %w", err) } ns, err := clearNamespace(kc) if err != nil { return err } err = printer.Success(stderr, "Active namespace is \"%s\".", printer.SuccessColor.Sprint(ns)) return err } func clearNamespace(kc *kubeconfig.Kubeconfig) (string, error) { ctx, err := kc.GetCurrentContext() if err != nil { return "", fmt.Errorf("failed to get current context: %w", err) } ns := "default" if ctx == "" { return "", errors.New("current-context is not set") } if err := kc.SetNamespace(ctx, ns); err != nil { return "", fmt.Errorf("failed to clear namespace: %w", err) } if err := kc.Save(); err != nil { return "", fmt.Errorf("failed to save kubeconfig file: %w", err) } return ns, nil } kubectx-0.11.0/cmd/kubens/version.go000066400000000000000000000005421516137246200173140ustar00rootroot00000000000000package main import ( "fmt" "io" ) var ( version = "v0.0.0+unknown" // populated by goreleaser ) // VersionOp describes printing version string. type VersionOp struct{} func (_ VersionOp) Run(stdout, _ io.Writer) error { _, err := fmt.Fprintf(stdout, "%s\n", version) if err != nil { return fmt.Errorf("write error: %w", err) } return nil } kubectx-0.11.0/completion/000077500000000000000000000000001516137246200154165ustar00rootroot00000000000000kubectx-0.11.0/completion/_kubectx.zsh000066400000000000000000000007651516137246200177600ustar00rootroot00000000000000#compdef kubectx kctx=kubectx local KUBECTX="${HOME}/.kube/kubectx" PREV="" local context_array=("${(@f)$(kubectl config get-contexts --output='name')}") local all_contexts=(\'${^context_array}\') if [ -f "$KUBECTX" ]; then # show '-' only if there's a saved previous context local PREV=$(cat "${KUBECTX}") _arguments \ "-d:*: :(${all_contexts})" \ "(- *): :(- ${all_contexts})" else _arguments \ "-d:*: :(${all_contexts})" \ "(- *): :(${all_contexts})" fi kubectx-0.11.0/completion/_kubens.zsh000066400000000000000000000002061516137246200175700ustar00rootroot00000000000000#compdef kubens kns=kubens _arguments "1: :(- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}'))" kubectx-0.11.0/completion/kubectx.bash000066400000000000000000000003251516137246200177220ustar00rootroot00000000000000_kube_contexts() { local curr_arg; curr_arg=${COMP_WORDS[COMP_CWORD]} COMPREPLY=( $(compgen -W "- $(kubectl config get-contexts --output='name')" -- $curr_arg ) ); } complete -F _kube_contexts kubectx kctx kubectx-0.11.0/completion/kubectx.fish000066400000000000000000000005761516137246200177460ustar00rootroot00000000000000# kubectx function __fish_kubectx_arg_number -a number set -l cmd (commandline -opc) test (count $cmd) -eq $number end complete -f -c kubectx complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "(kubectl config get-contexts --output='name')" complete -f -x -c kubectx -n '__fish_kubectx_arg_number 1' -a "-" -d "switch to the previous namespace in this context" kubectx-0.11.0/completion/kubens.bash000066400000000000000000000003761516137246200175520ustar00rootroot00000000000000_kube_namespaces() { local curr_arg; curr_arg=${COMP_WORDS[COMP_CWORD]} COMPREPLY=( $(compgen -W "- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}')" -- $curr_arg ) ); } complete -F _kube_namespaces kubens kns kubectx-0.11.0/completion/kubens.fish000066400000000000000000000011271516137246200175610ustar00rootroot00000000000000# kubens function __fish_kubens_arg_number -a number set -l cmd (commandline -opc) test (count $cmd) -eq $number end complete -f -c kubens complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "(kubectl get ns -o=custom-columns=NAME:.metadata.name --no-headers)" complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -a "-" -d "switch to the previous namespace in this context" complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s c -l current -d "show the current namespace" complete -f -x -c kubens -n '__fish_kubens_arg_number 1' -s h -l help -d "show the help message" kubectx-0.11.0/go.mod000066400000000000000000000042521516137246200143560ustar00rootroot00000000000000module github.com/ahmetb/kubectx go 1.25.0 require ( facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb github.com/fatih/color v1.19.0 github.com/google/go-cmp v0.7.0 github.com/mattn/go-isatty v0.0.20 k8s.io/apimachinery v0.35.3 k8s.io/client-go v0.35.3 sigs.k8s.io/kustomize/kyaml v0.21.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.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/mattn/go-colorable v0.1.14 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.35.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) kubectx-0.11.0/go.sum000066400000000000000000000302271516137246200144040ustar00rootroot00000000000000facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:1pSweJFeR3Pqx7uoelppkzeegfUBXL6I2FFAbfXw570= facette.io/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:npRYmtaITVom7rcSo+pRURltHSG2r4TQM1cdqJ2dUB0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/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/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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= kubectx-0.11.0/img/000077500000000000000000000000001516137246200140215ustar00rootroot00000000000000kubectx-0.11.0/img/kubectx-demo.gif000066400000000000000000005054641516137246200171150ustar00rootroot00000000000000GIF89aW"#$CCD;<N^l.>O_o_ߌ0>_ <:|1"+ZQ;zƁ!K: [~ &ٴk۾;ݼ{ <ċ?<̛;=ԫ[=ܻ{>˛?>ۻ?ۿ?`H`` .`>aNHa^ana~b"Hb&b*b.c2Hc6ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fbIfffjfn grIgvމgzg~ hJhh.h> iNJi^ini~ jJjjj kJkފkk lKll.l> mNKm^mnm~ nKn枋nn oKoދoo pLpp /p? qOLq_qoq r"Lr&r*r. s2Ls6ߌs:s> tBMtFtJ/tN? uROMuV_uZou^ vbMvfvjvn wrMwvߍwzw~ xNxx/x?yONy_yoyz袏Nz馟zꪯz뮿{N{ߎ{{|O||/|?}OO}_}o}~O~柏~~Oߏ p,*p lJp/ jp?p$, Op,l _p4 op< qD,$*qLl(JqT,jq\0qd,ψ4qll8qtsnLp4jwK5s|gPa[}P"t})W(Xn7z*(6`P oL(e!. ! ,\  Ii8ʧYׁȑ4MG@&XcLluz"P(($BȔ:ӌNׄ2bv|^D{8sdcVj7C~n.H (]2~xc,o%&R\)F! ,j  Ik8ʩX'ؑɡj; З-~C@\?ZpX<&aK@꓅:TBL gJPB   ~PBkwy4H e[(>l2.*w.T.u8!,y  Ii8ʧYׁȑ4IFijXLX` s#`(X G)t0E(Ѝ_ᙲ8Za5!\X.\ i=HSvy>F= xsxf|mofcepnXZ\;LNPR],,E~q,5S@!,  Ik8ʩX'ؑɡj; З-~`,3d.N 0@DhyEJy ?XE&I\<#И$ ,~(g*`v JV j[hH HX$LNO%  ):2.t2bT.<!,  Ii8ʧYׁȑY: Jq85vyWa8GQ`/71\0 i%XQٙHE@a*'%n'p`bdVsY[S5M`a1D I,?p3e.&yJj=!,  Ik8ʩX'ؑɡj!U _8[j!9"(#p4(dFE~'y8vQ7fwxS25k^  YN SL[.Zg*&&"N!,  Ii8ʧXׁȑYj+L_|APY@qd$UG XxCF0ʠ!|Sn},Bt ( VY[pF @BoRCRWI4NAh.6*z'7! , @0I8ͻ`(dihlE,tmx|`",ȤrY 2Шt*sRجvfzb7L.z;N}pgqkTOHAŽͺ .*&B  "\xA!ÇB@"ŋ/N̨2!ǎ ?<(r&Lo%K{._Ƌ)͚nsg> с !, ,s dihlp,tmx|%P 8 譁D0Jy.`Y!pȦ ̎ ֑ЮЕl k$ G |"NHfoy$ '  tC H$# fhp $ \"A "H%x,{(k٣[˟;N$ܨ¢&o xLDt'D%޾V[c@L7 #!9FREL)H0V.\P$s>CQ_FtT!OK,$eCOu?h&zesRŀ%ԥ8:BG'j4za3Q/E鈢bpŰDR<`^k)Ȕ T@7B5ldziȊUF"!Pv,>"Dۄ㴛2d8"e9a dc A>G6<0R2C8g<@ud<3GPhg;!@b$9ЉfNd"$xXT^bC P&HeB:F4)^`Z L*d8aʐG+ʬ>MU*vQφ4R5`S.?Qk"NQTbFnm+©lXBXQ̞6m (o#j/ {.¹K[`{ŠhO4GdVl 20'4n0 qĎ ]0/C[׬t ǫniCuJYϼ5v!{24j>ͬ6BMȜ4E:l:OiĐ㚩./3vwr+-0jl]8wt2^.nQׁw|?,߼~ϳ!=~hܟt"|~#k0zl /ek1S=o(uU< {C2@i T8.@!~X3πwBhy%} QD0qڛDTx9\ǂy_cem4AhF8 $\(||LHnOt  F }HwL*WVt,YJD.sK[r\05L*Ǥ+se23t&/KjӚ&1iLn"ӛt%3H|&8if<9m2!, ^  dihptmߥxH,Cr @ʘPs⦼`2)X)Op`%()hv{jR/ Ez}~@sm}>snLp4jwK5s|gPa[}P"t})W(Xn7z*(6`P oL(e!. ! ,\ Ii8ʧYׁȑ4MG@&XcLluz"P(($BȔ:ӌNׄ2bv|^D{8sdcVj7C~n.H (]2~xc,o%&R\)F! ,j Ik8ʩX'ؑɡj; З-~C@\?ZpX<&aK@꓅:TBL gJPB   ~PBkwy4H e[(>l2.*w.T.u8!,y Ii8ʧYׁȑ4IFijXLX` s#`(X G)t0E(Ѝ_ᙲ8Za5!\X.\ i=HSvy>F= xsxf|mofcepnXZ\;LNPR],,E~q,5S@!, Ik8ʩX'ؑɡj; З-~`,3d.N 0@DhyEJy ?XE&I\<#И$ ,~(g*`v JV j[hH HX$LNO%  ):2.t2bT.<!, Ii8ʧYׁȑY: Jq85vyWa8GQ`/71\0 i%XQٙHE@a*'%n'p`bdVsY[S5M`a1D I,?p3e.&yJj=!, Ik8ʩX'ؑɡj!U _8[j!9"(#p4(dFE~'y8vQ7fwxS25k^  YN SL[.Zg*&&"N!, Ii8ʧXׁȑYj+L_|APY@qd$UG XxCF0ʠ!|Sn},Bt ( VY[pF @BoRCRWI4NAh.6*z'7!, N0FV5׀ d*i-q05-`JH"^N-`t8-VN6-h@{Ih! , Ik8 ֍a8v%xrZN<uKyʀ@ /880KX$0|L{EIlEqBC90QL}^$yjl}bwfT1VXZ\ _yNPR-BF dn3/+|3f+=! , Ii8ʧYׁȑYj+Lc|9@, Ā!^DpYEpIe(|J@Pa2BȕRy{A~^wPuwRjlLb^`dfp4SUWY[]iLNQA DF6.*"*7! , Ik8ʩX'ؑɡZ', V50sVa8@ce ʀ֔P*\C,Wi7^vIogz,fhu{~j}(q*v*"&!, Ii8ʧYׁȑY_G4V0 ]cG`1.D:*eqJ (!8GB[l{}n5^H)NOvoUq{}|yx,zw(*%V*8!,  Ik8ʩX'ؑɡj; З-~㔁8< ɀ7<> 4BpM$B JRp"M<| Ua },*`uTk&`b_TVI sN~Bu `62..KT."<!, Ii8ʧYׁȑYj+Lc|9‚q@$Ā9` p1fSd,B0T` 0~JL&RL*jW"_fWTf s0KMN D m.^6"p&?!, )@0I8ͻ]!dihlp,tm&|pH,Ȥk:ШtJ^جvZ-ްxL.?z|zڮT|u`#wi[gM(K¾Ǜ;֕ڒ  J: <3xPÇƃHb1,j܈vc; C@ɓH\ɲʖ0c.x)J6sĩE>:*@FC7a0dwSJr۝ć ULDJFv|JOt8P%T!Fjs?yfy0Ld4ŋ`(g\eDVdžb\V_lb| АteETa{H=XOlQJ[8]:AY2cX(Adh $UІW:2d%4@h%X(+/. YB!p%g; LfXY+"<CeGPhNJ(tA:'1kI_MWihS2 ĥJEI0LAϷ|<eBAú mˈL NQ ~S0XCwzYJ_>G&8* Ԯmߝ=,L 00>/@̇SSygSֵ;l4=»Ad<+2%W쮴ԈOCkG# |g72yN\k5P,i 1f\at ^T*>Q Y q wH;>|۞4< H2J.` \Y~ܒ U KMFt"2i@+ІȔTŒx!sHJqYd$a1AR U#Xd5# 20f Xc-.A!Tݱ)G%8 t6>Uz,CP>F\v@%xrH8P؈8(Nl(n#&CLzAZFF҇h |p gfuK`!LO̴l28T_RE(Orc h@iN"TqHhA+d~vp{TJg0RG+ L#>jCehaG hGВUn gh21X,81'%KyH5tOe1blU2ыN؉T/ma00`ZAfAtYr *ݚb'a= ?.+:HsYElgKͭnw pKMr:ʍtKjͮvrUl}Bwey/ +W~ L..#LaN3n FCLᎸ(Sb뎷06cLcϸ8vqwsۮ@^Ld# H‘d,PLeLXnel`A!, ^  dihptmߥxH,Cr @ʘPs⦼`2)X)Op`%()hv{jR/ Ez}~@sm}>snLp4jwK5s|gPa[}P"t})W(Xn7z*(6`P oL(e!. ! ,\ Ii8ʧYׁȑ4MG@&XcLluz"P(($BȔ:ӌNׄ2bv|^D{8sdcVj7C~n.H (]2~xc,o%&R\)F! ,j Ik8ʩX'ؑɡj; З-~C@\?ZpX<&aK@꓅:TBL gJPB   ~PBkwy4H e[(>l2.*w.T.u8!,y Ii8ʧYׁȑ4IFijXLX` s#`(X G)t0E(Ѝ_ᙲ8Za5!\X.\ i=HSvy>F= xsxf|mofcepnXZ\;LNPR],,E~q,5S@!, Ik8ʩX'ؑɡj; З-~`,3d.N 0@DhyEJy ?XE&I\<#И$ ,~(g*`v JV j[hH HX$LNO%  ):2.t2bT.<!, Ii8ʧYׁȑY: Jq85vyWa8GQ`/71\0 i%XQٙHE@a*'%n'p`bdVsY[S5M`a1D I,?p3e.&yJj=!, Ik8ʩX'ؑɡj!U _8[j!9"(#p4(dFE~'y8vQ7fwxS25k^  YN SL[.Zg*&&"N! , Ii8ʧXׁȑ4fn(88UW ,e`֊^z JJSJFV%35WmNWHQ\e -q* :i,tu,&1"p&X!, N0FV5׀ d*i-q05-`JH"^N-`t8-VN6-h@{Ih!, Ik8ʩX'ؑɡj; З-~qx m"@\E7i@:] $ !py n,nprt|mkik=J`bZdTVZ P^MDpLCEG"s(*Rq$ZPbsy N Zў.*`4!, Ii8ʧYׁȑYj+Lc|9q84 ݤp4/O   `@"E !x$Tpi8YGi{&jJtq&kmTb eeS7MR\ W M `96T*r02[.?!, Ik8ʩX'ؑɡj!% _Uw`((p,_3֓ !, E@0I8ͻ`(dihlp,tmxEpH,Ȥrl:ШtjPجvzK+xL.tOn-x<=X{jWDvxj^WL͹Ѯզٝݖz;6 Я,OÇk0ŋ%&ȱG }Icx%Sx2ʗ0I6sꄁsϟ&zJТHJ8SN*OVĪJ^Î+,FfӲvބ/@~,0 †@Ǝ#wH!, s`&dihlp,t-(L@AȤrl:ШtJZجvzx4tyN~Llm? a _ mC©  >C:ӉmB9Ȧh?:?  9t@ *!4i '-x3jȱu 0D,(`!>i-4fWL-tmǣH*]ʴ)@T9 0&\Fd8&T㴭۷p貫-uXEҶ Ё]̸ǐ9@_{զ(R 3fQ  (װc˞->[v1}I${uͼГ޾MGe4eFO [9OϿ@u5o0W"F(6{$]UW Ձ ڲ P 'T8(T0_HTR(-t|CN ގ<2F)T:&C@sO>ș=45QTUlfTi, > G 9p6`pp 案&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&,9ٳ^0]fvf L{]+k80 m^ʫ'g``FhCO Er $F8׵'?QM49َztq7!P%KF%[@`Ks4>Vʼ@0)@KSsKR$qW%Q@ sI f#Iz#uް(m@MZ I>J?F1Zj wK*bCru *qWg.k!‰z  u}T!]n`)Έϕ=X)UH"HMR6aa> DM&h/^ uPA<ŀLCgUIC1n%xC,C*zILtM5M1im[- }UXk&03)M.lTk+x{Jk(FOH`Ԡ+($iJ%㓧Cod[O,u1EZp\LEտgL&)L9pM+\2j&#mbH,M&_Z9fHRftBn؁^ݘz2{Ӡd RDPmJl8]mB*:{q3/ץM4l.%)8}2ZY^d b:֋}t_,n{~۰|%8e,MS&ʡo;j6Ī}QChfni2* MrPƸ,v-/49]4S f/45(O/?-5 EP7Q'9o$a^%W܃^!$IHmA.Kx5bI &EI ıti,_bV-FfR5+@'ѭ71\r{Ël]Ͼ?YRj8gbUBcNI1uCOMG.5`v Nr>Ob>UƆ80 U(R 2saN{E6/d? x@wj<ߠd?N20u}q}284X6x8:<؃>@B8DXFxHJL؄NPR8TXVxXZ\؅^`b8dXfxhjl؆npr8tXvxxz|؇'&q70S~fON懈]~"Ns1! `31:s(^0pV7rn∆Si/؊T8bÈb?@ʸH Hach8f2X֨Ah3sgd B8$d>~hK8(s ƋbE#'t?FWiI']uwd#m1[ o2ҰQ7&nDEEn%G#L4 DpGQk_|Ҁ7zkCuŷzL3WPQi/^x9sv]e2GٴMwe.gMnDNq"b#Wr #QVN*aNCzB1Myi/HfQ= a`Yj]d*@5Z-fc6iEF?WQ [0\5՗yҘi"P@(BȈ-cd\v"鑇-UP,YjUQ}׀Q_Xձ\>Piv3Y܈- Y-C?VYR}]AS-٩6kU&EňIC_`UV9*2٘.¹BvɌsy_H]>\"=ҡfT]%|y\K6Aɢ\ڥ8/1.`@)ũ-C.1r'刖ÞU?GJiIVk-[R[fv9%饎D4q?ףl04a'|-!qi5lh%!zkFjuZ3eP?$=:t֩$Yz:BL-iMXu T=SĞiz:@)2]ft?=*8VpZZp}XopDIqY2tv'c kYa^G id\F 0F~tGS$w01Ab-mI p#{W2tEWt<ї5KCdDڰF` [*3闲#УD"|IɈg|azh_HII҇ ׀}$)}C ]AGK ʫcO(V `X$)Dsr\5T۠\a ߀u,ޱh@2$Q @ŧ @.'| xAq2;/;țR0IzrҋPZ/;ڻ>x<2Ľ[{蛾껾۾;[{ۿ9\ 0z2 tsHJWu%`A!0 !=TWvџĻv?LVb#h)]S\=9=% 6@vȩ|ou]љDD/crFjak"C։i٪Dлki4j7"%m]mE65vǸc,M/?8t݄<`7Uۆ-LiٴP%$Q@W8ԭMd?qUm5 >M1˭Վ\ѭ-`ޚZnc-I0 76Ѹ9$ysi =:)ߚ8HGƌll07)4#BPO2,w䨓ǝ oUȖ6 L fBiC!6$A] 3N}RxSܓf`yyEPGR:}|p=̛6N Z5XaIM5A.cy>Գ3Vl >&h sI /wn4PQ԰nSݨLh7-]K|Ԁ.V\Fd| ΐw iL ;^3[~oA5V N}2k+~ceA z /S[J!] <~0nƀ쯘7P 4qe[#|೓^(,FJ59cH n+Wv{/r,BU/ ?q C" }i`1~c=STj>`2Kk{y鬖a=Ȋyz4/ yL=bB1WiS-U]T_nݬ'u%ɀ g_38ݼ=lQg4ܺ,zd@֏/"=e dD}ɺΌ$it~C`O?Jt'[QD<8/ *g<+ψ_1oRZS \DE:O1/X}D%"ml 8y±<ӵ}㹾G@Q"P J 2$IG18%±J, pjM,6R>|! M X`5 HA0^p !T=p&uiR. 07 ĶFKOSW[_cgkosπ44%87 B %u 5<+v21jSut  ؑ`@ 4s@-N}4,D1& Uߎ$9D:&LXp@Aw6L,E*)Hfz@C#̙z 2`^(v,ٲfϢM,~k(~=@^{k] ETQLǠ؎="yP~+*qPmEٛy08 Rz"+9q@( T;'Jw9ΟC.-:p%rd`SQO: x`A]Y@k/"p2^7uL1o#Pq䵛7DMC`_ݭEIhhMR`=geb/pךy45 @lPW߳`^M&,DɣR~JoP@qb_@b: RG@bɲF'3E8#8?RPNfSAt Տezz a; B5X$) 4a6=/cZp̥z4h tX !Yb8 #cDmFɢ<1,188LAĸ&K P=Tc"5bA @hW(,sx3M(@$9-WEtakC8>#Gɠ!?GG1 fnY#.iВ 6i3ȟGs=^ap(@Z/ǧ9g.AƧKR\I@`2Q$9´AaȺA 2 X1#`УAW k,(Ws/Tr/D|Cܑs8] B;"'I=JBie+MhJqFCG`%D(FW$>I_؀4`c*G`O%!Yt̴S]:QԺiL;D" Y5Yºsƒ,O4D~,q[ 2Z/׉:a1,0lq Nza И`5ҀB Gg0ѱ|nhM2DFxkB! ` xtE yŤdhtH@1e 7ubNfNJ&7 N6axv>H.\D<#4Alhe ; H*U`Ib<`!l A 7Ch3K23 84tb 'FqML1N<W V4Na*Exܒ>`|Sgr`o@2`r>BށJZxGwx>(YBÒ$+p?܀tiy/a&"ļ5);CWh8-nAWr;cTɠ^)K"@+vCM!10^tWkwr.F !@ܖpV6ūdEܲe].x-2ҒfFAn=KJ3&Dgn^*M i"/44`m rh`y$bGv؆uL.;q:Zރ&nA;m {k,L&cޑNt08h_dž cnxݣa'uzu؆{aD&{$@#}AFG>nO F ۖHA5e4?a܇CIzfpy:LB'2n`;sJn1}uY{jY^ʀh&!09NAZ/^.>Zs0z RޭuZuF4JT^. !̂zB޵BpT \G1XA])lmi1`і?B߼a@m=DH@ˡ ZZ= m űʭ`i:AaVѠ -WNE4#iѣNT~_h=v(: ֡{W(6d"Tv݇޹&wt(K(n.'Q>D60Q4c\^WvłA]vjs]dGx^)]qZ~dȷL*FfP9AhpB{a"1P }z@V @,+P" 5]kȋi)!-dxŀ>%5` ,IzbU R5 ׬DoA]bN 1J FPX}i 06:!j@a$>G<੘<]9ꚶ _Ǐ&(k΢UlXĶuߔQ4V]eQ$Q$u-g^+-<֚}." dcGFB,@\_'$OӈQxO7jp0Cj,Jڹ 9UA'Oe hg i;099T3n"-,R$t,ަmb[li6]},Ef@ڡN^ uvaC;[qXU$ WzU_X\ ke.WWJ1[4hdjYgiƂ oJvC G\h0' `XC0\\@!s0#ZbZNP 06RrXd_""if@Ѡr),mft^Cm5>1eSF%zU O6CA*9U,W%oTRԸKlK-A `4ގ`!+ CkA ,'`Sz,.g~e@}_(*zLD1z EArG~' Sr1w$ NSuUBx)d9×`d1!m1#y7e0tխ#1)q'GDO[uGktHt49H{tJИxJ˴FtMNO]A4t4O55 *@Y&SC5ZRCPT3iTkVs5|֍mXc ǕA,wu[[*s.u]]u^^u__v` `vaa#vb+b3vc;cCvdKdSve[ecvfkfsvg{gvh6ٓ_ LCo\u-kCĶHmtF<%1L4Ӌ}@W$7WH{~縧 K1Ʃ>pxzŇJ8˒kK]D(8l7ov3O.hoc.M%ݏ`A[# P AS C8 VPZs YyB_C W`96h_(l_t ! TUwmAWI!o0[fj3Ol8n)3CRƅ'R.E?3*0S+U,2!a}^af$Que_fi#4XZ=NQ2 hڐ"-@¨K&~!CIaG(Q#[jV bP%HL޳դ*|F[0OXF0^5W`H0 vq TiͻC~"T!a7 fMdZv %g]2(5 T!fQJf.X `F_tLvI MǴ2izjֲm9*c_r5Hw1) reg{P(@ X@X@46b8M\X"j!hn C ͢즦Re~pվ6q  d0&2A&L}+(L+3pj,jD3@M%T p(  b9@(Z!-`D꠩dش{h:Eе)zPg3ϐeG{Ҵ>r+sW`ra k`p(1qU]6TsZ|kq2xi )] gP56(ɐ W Rv0b S Ba6T1 "zaP;s0O~QK)r1i@.aVQLa2L%3Nڀa+3bU!>uT$lх(լЧ[KrNDUzU$لKy_t&.<2NU#v%`V~`$+P((Öe6'; ś"K`0࠻:|`ȂE+@.ۋX+,CtXϰO- UϘ,-ѹ0R(.1/133;RVyqN@>vKHd(PcS/BlěP 2Wܞ>5@s: 4r] QՓ45ڜ ҝAr1A/"W'3q*;YZf#jl3ǐRDϪ z`X s8@WtĴЧ:҃BE uA%((]k:;z W)_ ,6h!6cuFԕ=[T 1,HrںҷKy2VP,ema#żʞ j߿ 瑸@.@;Ec."uKE+g [`B2lš(NAR>ɻx "Mc4'lK۩"M;UR].SK@@熍,d `lCKN!ƎlѐlC vS\k?y1:{;OR\9S]ЀW`u0 ^ K " iPE["笶Djkj4!5Da aVck. +Z`ť%`jZDۑ C`JYC O@a]' h#jjb@B**m-nI۟Ϭ$T LYrbwni7y9zWLO[7P‰Z<1Ĕ9`,"k0$@@$ǧФH(JAL 7U;%Ȩq1Sd281lZ9Q`t@ဣ1=Khě {Yւ @  Э" }H2  ( 6 mL"_uM1vx` fnoQq7@j )4yQxBV )QCWn\2q(* (ʉN-5hӧ2-QYZ|HH+Co>-/'W>OL! [f@ɖy #$0`WS0a e3Y`{^uWFay@ Tf#~=f L6 Bo#$SQ$!Bm\ĢRaQX(}Q23dVMT g3\D']H'IAgY9I':*p m$1z_0o9؁[j*9@&WȞtJ3WӨ uS*>]2b`xN4a0vwX8h0zJ;7~@gaRa CG  "10XGH@Ipi€|K9[2HC/L | qvKPr>^(ؑLw`.|! c(Ұ6!s>!( * b `$p()R8^ ^dc6/(Zf<#Ө5n|#(9ұv#=~# )A<$"E2|$$#)IR$&3K~$(C)Q<%*SU|%,c)YҲ%.s]򲗾%0)a<&2ZB eȜ',ּ&9MUN 3mRO].%puHjZZ Ob2+_H0ȥ#X'P06p e;'$zvZJhQ|""dୌJ? \&>v;/f}PȔ&T'$AW RbeY4D©OvZUE5,M >p&š[tZ,'(u:ͧ sh[j(? 7G^k+{RE`GyI  ڈRS\Ȅ!P fy z*PlMk)w6<;+>.[o?GM?ÑN<^@: `9/50ڀ(5`:Z^X?R<6BO(ì>No 8]Qb4*Uk\YR>d!vri-I–?Rp*Qh?PE[ȵ$LfI9ZtlJtOT&3K5t-Yfr`3\ʱYL"yx_^T0mp" tzCk*9s4jpРQN]\ q`h5p@m7-"5{ җhNFLEk4"4(q*%'@K7쇁 Z|;fF:Ɋ'[=UlK\Ex>I~ pP+Td\[!0>G7P *aMv#`nx"Haxu1_v\ f:APa}E"eeeWHbXcA_k151 gLNIK:l|]0Kɓ͢)&[o:j].^\xx~[[ j~xG]j!yc n*#^So3t%2 ̽ |N tzz_:8 f*Wd Lk $$ c|d#tdcV!U((AhiMIP ܥŜU, DY}ed1:_#HZQy(Fbm|::_'#}}姃!X|g tc@B8 h=UL`ZƛZɇjnQyXwx dQ[VChZ蘆)ۉujQ)HPAn"_͝DYGm1y(ŖaT0f-~b^n,!;6_~N,2Gn흶iAzTpnZJrrb]I9פ<Z4f`-J~ >Ңnt&e隀z[+>'V2,~Yޒx©PAe$H~uBn#HϝA -AhN͡%ueuE&oGnk7hH$01DX9WRm|A( "DKUGԊs/kAvHаLʕ(,f֩-/]6osmSb+-4BT ;01dBΌ=DDe(xpYc#:$_U^=1-òVhљύ2ZS%F`LEmDΖaL%neM"pV)LN1o'na0Pd]D<kZN84,3hG9.;[pi 'kuTvx C1c\6meyO6elDs<*īRx'׃x-)n㸸8+p 9t6/9( @h~>9OxG;BHky؄иsH}1}դ-P 9?PAy}9`Qy:7?:GO:W_:go:w:::::Ǻ:׺:纮wb.(:LSf@,; sz%?c:IfSDq pնҴ #C>S{C,L5BBA0DDs%8POIT HJNL9c{PԔfL{:0mIQ5RI^Iܵ LU1*׈LS?puʜ{PUTVn~"h/͕ͷo:Ugr4PjPx| K|`}8dd^e)#Q~0ksDDӨd}buYFXe1cX>| j<׆a,`⛒tUىy/av55QwUj3Wƣǟo{qxȗy*ǃBK9I-AS oYK%w|2}Rsy0k-eX-(:WԘrߘ6<¯U哂72$<d(d1 Ȫ(FHj=D ) @#0 /ؖ*B,H%|~*?$  *n|K)> $IfI xf35,/]~$$ Q7 /1FHo0)# UG }ޮU)xnp)U)4 P a. fqz*IHG:q-Ç#Bt\y$0$ b#Xʠ)CL AtdKkz8p0hˑsZ"S(ȚylPA<;b˄$x cKE ^ F.!/GֲlWa/1 r#F&XDu *mY 00>VӰ?TzU'}ă3eٶhZج YeۊP  mгkΩP]3LS+kk xl6^RoG* e ` $htd*Ą!! PG "c91XE`re@u55t%<@"h%$x4Pdž yPtUH A)4r(UUŒ71"KU*Qf}) e,}w[T2S( P[_AtBfj PS[S#PiePǵXXW(Jޏ:gql>+i@WEb@6#u%-,$0fY5bbnk3IbFCU0 #YD/vCiM"=f&S1:GXJВuSGbL!9\ՑÑR F[#8jw g3}%yFuo=wφv1oJ4hpp)a d)lO(VMęJm٦&Bs1{/4K?7~<+ W0?)/&*T491It5"bnѽoX{V0}xߢ}(֒8LDn'P Ef 1>XjCz14>\24ty[VPmqula_*ų^S," C擶a`Ȩ, H-i̔2%HBjͦn/`D<*HAdiD̠ꉿ  1 dE G^J QI^řX>Jȱ2M\D>8h,h$F# ȦPO9Jv ؽO#)/\c%B&&dRK%w(MnK&v4YDJ~2Y0{P DtAPp\YL@2f܈ue\h@i!;U=SG뗫&{$ظ+/}Yy:9n O.HgʈUa;әi]gb@JA{KD iAII^D>QIHh@) BiLj:CqeD(^6 v{qI(0X8X#4`Шά37I=f<#ګ;&$Nw}J-pJG iv gB/:X&NzP" u]Q`n$KGq4Ū ԓ_q#a*kk܉GED =4J+䁥r[fB}4@m\xEPB{~  S3|R`[ e"~Z[xWMvugGY\2!X S #w1)%#ip$"=2xB2E%$2{r#S .!pL4su&,؄qZ&T &.=]l w|b"\҄S)l;%kr%!7&>sbyFe6! 4o`}V:+'qQ9QAv<{R6 `92a3SSP"fq .'X ̡H3W l-(5Tm1aUY@IgS5E$ͣW3QD_8[x`5R) QqBUUD}-A6O-R p>υt憵 D;:WCf/aH5Quԁ ;([O ^7Q`bTd s@Pi 4E-89-CWvpI`By6eO0 З,`Uv0 C|${i0j0P@) ^Rs ˰ԏpnR鑁p05w+secЕaj-ёyX I]9֜ <8gV,YysyC'|2YQ晞깞'E<9BIdHi֟HQ7 }Ě٠jڙp 6ہ*,U%f;2z8lq3:zsTRCdѣDj.ziŏZL*DmɜM:TZVzXZ\ڥ^`b:dZfzhjlڦnpr:tZvzxz|ڧ~:ZzڨXNg  K"ʝ8787M7D>r;AGթRxf} 1 .( "$И@ʆ-YM@ʌ*>R% g zzxl6 r \ɰ3-@uCj7CVfa8fk1-!-Vs_!#ŀG!&fpIap5+ ZFsyُ 홍CJgk:Ok{s%pE!f!^С#wH|7}/h$paNa7>ReMLƧ0V0ê 1>0ir{:e)hu_#}hyu;eԳ1!綯0z.5$vy.U al D/Ƹif\$y+ZZ;h(bTdՍha \JDr< p`z7SzOgHw n:MyY(g( >1L0Ϊ.x_dh(IOq0Ǜ<,U\ cdesTn n6h( X'v=Ƞ 1t $ _ֻx ux]Pɰ@#|Y0;ԛ5glJi}$ V'ߠZw`IW7NJɌ c q. nPbxgP| y{Qtsr\ܜ QG=1r!B&3!'H*bot0G12#}/;~. BzHIKr)N]r f&0%K ¢kFlSM4CuBǁ '7'GaY&@EwVcv+ lN$>il^  ,lIM{Eƺ"V̦/R?t-C.r.{b.)`.Ӑmn/'@ Fa F2|R#N:fsH̋(u)A=Ւ07k铢=9VK=L 4p#ȁ4 #cSa#ɹұ6565x06ל5 ,X t˓h|:էM2md=q.KC5:eGw:g@,d:";qpj;3g缺lx\[OZ l;0&wL uiQ2GU+fk:YcLs?k QUDS{Ykۃ>d0OjZZ8X)Z[J=O;]i\:#(eM|phY`%U1|F=_% ?%ܡ9G Ɓ~ ``,+敭v99}b UP,OUrWrYktv>feOt:J_!`lXϟHO8toi0`33f 09nmָ6j w2,6(4B!FE`o\D ټaATMw}:Zqw(ވ nF0ҥxn?老^XDuy˂qƦ0"r\hcF"PxfOPeCɦӪVC~CLC3tbRA6Y:N1y5ͫQVVE.j+ xk( SQUi|*"Bnp$3{]-YVWZ@!<+1asn!#}":@s-fVS >2sF\Rԙxԗzzxà ^MQYxg$x$!V_XPÚ)Q_gAKu=8݂ē/Iq1Xa86X0MdA`0@mfGB[\Ht Ww" D}rdft,9f tBdDoٕbXX C07[je2v_\!Ɖ K  jJ9 `` ?@Pk0ơ+**tdhf @Y8e kbֶ580%=+9ۦꂊ" O]5`&4a8-J"*(8`#=p}bq2ja{i+AS\-SC ]B8;Н@]EVF Fp 4 bGeXkpmP~w` p-YqFc{KqmP` Ju"gq u3`qL&12G^AiYypWxRXxP}27S1[ d lV27s8<ŏLG` j:UcP"C"w&FW~zJw4-P_ K]z>1 %$_;r*jP`_ bpnm=RNC圀n QEo˂6UH (@24|w<1br@f FQ\b[()qB_vC\P ^=$H?wd-1n#޻ǰ Y= 18#"KB)[J} F_pQ`^i[Rdsyҗi"Ec"3,D465sҜ&/Q/(3W'9ˉ~0 3ssbP>'?|(A Y z^&BԖ#$>͈b4VQ4"5 uмԩ#]Hv?p2)Ⱦ`:)O{ӟ5B*Qjԣ"5J]*SԧB5R*Ujիb5Z*Wկ5b+Yjֳ5j]+[ַ5r+]j׻5z՘Y.{u:J@& D J3U} "p|NpaQ@H:VZD~\{(V#Ѵ^QJ(vlv-p]U"ladI1 ";5kM;@  N3V+8V/}EGVROB5nfG yP[F!S6r(&p0 C \:s%n ܥjtcs{X0+Moc"ijPӆ@q3-g)SXM _5@րWQ`6qp"0Ɛ8` ($t+֨C.rY]h@E{qxqTvaH[6؄b/wd|EMBi漐 |NР.h*F'YgZ->@r*XG#EXqΗq_գ[Ý2i& (_b{1¬*RAI<69m@5.rN"%{MtQm@  ؄,"WSNN iJzgXYD*f1MDPO-Db(9XB:;`~-QRò&sxF=9OBtP,l`{' vMT2$o̧ĺ2p(*/ gkGwrM]" `sY~eN*߂ϲ0?(WexS80y,J>5z_x ' :`M#T ¾d2ќ T]W>-\piّЊ_,PDYmQ\D nBph恞$aɘ^lɎsڄҪx Ο&AI XJðɵ_^ ,$f-H\E A5Axd'd T%~,Q Ё5]Q뵄eQU!}M (,<L`m ^`6ev,PFFr<(p"WUС?(V5"H7eGx)aANA, PI!(FBZ"BLgˬB̍$$Gɱ DMCKY&=MEy!KzB͚YE^L$܇ $DQlʡ%0\'tcV]#ĢV$9й F&FZC41F-! Xd$O 4` ƒ3˹o/6J5JK 7" KTOQH&F\i ΰxL!XX !i%WLb~ ]bb\NiLYnZ TE2"8=p($V]/"7Y;&0f?hAfTW=$|OSP>P~c.c!e5&OTjRa xM2A hV""Yʻ% .(L\IQ0\etuX;%奃L"1_27h]'Q-`$}"QA$sSxPL ii4~r^2Tu*~_ g%]h=t\6DS8[`\NBŸUF(s'^^LBT:D$}%$TRHP^"ZTm#In)T}O+fLv "](F&&08anu8ܥ9D/~ e^ffl[w9iAaMmG6fCf>Vi0i `Q/eEa'eW|~2DH*Y*ꮎ*A+ңbAAgM%gBk. \, NE*+ WL)v@Qjt$sjUC Fڌ؈!M:" (hp'4zߜˇ*ecb*d]q~a]@ [K>,+ZHM~~NىW"}!4,-bf2lU$-vU-+]P܉_A(K5XFJT_NŊdqԴev I5? ܛ>c,*젭ŘUrs,jaͬB M2.5_NWfbc5(D7L\g<ō@]_H@Qrx.4$*z"ńS,$έhN.Ϩ"NO *4V{~ŴDT#10υ=I}(!}:Ojό@,È  E/=()Tݙ0&a_q~a /1R'l Me'| m`u Pg;Uiҭ s7&RFr8;s#\i9;fs;*3JJq<21s>T>?m8sR3^ tB+PqTnBUA&D[Eԟ]3F{GtHHtIItJJtKKtLLtMMtNNtOOuP P¨B`2Q;2; Qwi^xo. .P-ur}V 8iFkMVV5oL!Z3B5ta&`KLmWwRxY3Zˋ p/#\u/5MCsٌ ؍H 0_mA5B a2PkzŇ_,8xS~^GK!lBUB8E.4Ӊ Xkڑ(YseUaَmٔyٟ`"AdSnwȪT}nfP#-rd7Bet+Sf?¤Y 5^pjq*AE fOn,8G(:U3T>!сN 27B1AQ7\E7)u1J,?6Qu9TdxaѾ<dPd:)kA: T6I'l|(B;mV{ʽ4fّ\y+GuQ*ŹUX@Vm韺9"(hD xj%Kƒt3Be"2!4g vERN_|!.X'Hh׃=~mxJУ'FٸB*2?i!1xR$T&V" A@0[#:x$=8j$4HBP#j# @2d(j:L׵6 A1z`Hr` vGJZw ŧ:h!>p0C)9%e"eI 0X42rdsRR‚РP(T@st  +K" R(& pnz>x+3{` , i`)HvzDx6,膢?nO]˙|>hJ+%&hעA`¸H=phB5J]i7Klf$ Ae20B!d}rɫ'wѪgnmVD{h(C\9E,oTS)pM14> q_NVFRW! vF= q̉ /.-BHY{/ F %&hΒ u-Db"HJ0:^0e EmqT9̽Hڿ\㼢HJ溗ʌxL\3uV, e!tRu*sF‘YمNr6MF?\!e*RJp h6Op a])Wr~jLc[w,ޗ 26 \;50IZG\C>ܳ:ḃ A`dQiR/w Lqcqx.@vW ߱h߱oB#o P'(‘E@%U(c &W$@('EMJ!HSfWeIh+p(J7@=՚Nw=Nb技"s~)"߀q@=vEP #Zl.pOoe'nT)KxVY`E c A".&x,[ c@1hN<62Ev:Dċ$HE T+B`aeH9\r <^.ό4yln 8ytF>HŤݑ gI}gufہ40q+x%x{$Sy䳞`W Fx'J7BBU>uͤagwR:L{^E l R|Onh6aN]+~ M o4H[r  |s@uiJ3Fth? P TR>8q6%Uz DBr"{e97|BxqwaS=7'#m5%XU=f9Q`2^MLg6gHPWV_8Y-$QEp$? tB|O0Ora'[X%s]5^ 8Yg "2l ss}dzw+{CP$t$ .pz% *XzF'69`[xaF8|AO;4fgbFXTPH$DfkC)>M4Dh8vE,DDO2PC~t,*``}@3u $XH` O`{0/ (A{N-ֲO)&!Υ}ANG@ 8yx'Y8SUtZE )V:"gpPG@`.”J@+J{P$LP"6y6P dM#H`ZbO<`DQǣ~M7MexQS~V"=!dS"#+_lR='yIY49qՈE@ 6\y'"); th!2J*%kn sIp)gB-TQd )*Q4Iss#1eҷ&$MA(8 Z52"/YAO }8d Ih`djca.N6}I ?$@b`#Q%uWYR`<H/R`4ab NhY}iF ѡZ: -8$j@pǂEM %# kя$ɉ%q,m1PCP@{[y]֖`?dQ 2P: 9j4"5'I I!q*C$c'4+QAS!$5Cr3ZT1 gб2 !& 0@6}$JB$"qգ]$_AМYD!\PQjĝ(P!mї5?qbQ; 򫡺4 "" iO9 :^T^YQ. Ff"r."zCE#L#!Z;jz&E460Rĕ2GcJo&c7+`y 9^ڌͦK0QZJɺ@䁡13+*ԫՊ?'٪|8)R" rvdR=+,6 SJ)$32g#ȬւE›EP`x2 c ՋbbS,#Bڹjⷔ3Pm~Zv;TVCx`_txb5 Z^:zPKg4^533?dC\ AI$4.6 55^- ^#` 8 .6xRKЈW䈋@B5p(43#a7ԻƂm!9'LC˹L;3%Zuv:J`;s*;)Bdb`"xQp 49P sB[fBDqG'Hc>DspÃ;?c&a@ tSSJBcLG%Y/P0A?f2 2g*4Q1F4CE{9C4KdIDu0*1y|E @ocºKnw7!GAAG*uW HSA4wN0S|pʋvʗJaw)z "||sԑ^FdNVHȒVT L+F˿TJkH ,zF`z L,( Vq\jᄉ]8q8orĞw& qx %٬[EZm| qzQB[(AaR"R/R`d&LvUo[V7}UgdUo0*x.̕}oo$ǎyf[UYG~^n-Yql%X\ ƋER[x5mQl=Zh~m-ن["$C8m1zukK2PAaWծN\]r]] b}*[}혙^_ةMpQl8B):-b[cS&mKٽo[A}b֝ `ŦyceYmխmbdL!b]2t-:u .Nnhxk~#N%f!&+E-1.%3n7U9=㬳?.CNEIYqKOQNUn30W[Y_snLp4jwK5s|gPa[}P"t})W(Xn7z*(6`P oL(e!. ! ,\ Ii8ʧYׁȑ4MG@&XcLluz"P(($BȔ:ӌNׄ2bv|^D{8sdcVj7C~n.H (]2~xc,o%&R\)F! ,j Ik8ʩX'ؑɡj; З-~C@\?ZpX<&aK@꓅:TBL gJPB   ~PBkwy4H e[(>l2.*w.T.u8!,y Ii8ʧYׁȑ4IFijXLX` s#`(X G)t0E(Ѝ_ᙲ8Za5!\X.\ i=HSvy>F= xsxf|mofcepnXZ\;LNPR],,E~q,5S@!, Ik8ʩX'ؑɡj; З-~`,3d.N 0@DhyEJy ?XE&I\<#И$ ,~(g*`v JV j[hH HX$LNO%  ):2.t2bT.<!, Ii8ʧYׁȑY: Jq85vyWa8GQ`/71\0 i%XQٙHE@a*'%n'p`bdVsY[S5M`a1D I,?p3e.&yJj=! , Ik8ʩX'ؑɡj!U _8[j!9"(#p4(dFE~'y8vQ7fwxS25k^  YN SL[.Zg*&&"N!, Ii8ʧXׁȑYj+L_|APY@qd$UG XxCF0ʠ!|Sn},Bt ( VY[pF @BoRCRWI4NAh.6*z'7!, N0FV5׀ d*i-q05-`JH"^N-`t8-VN6-h@{Ih!, S0FjuG萙 6._\+QMS$TUMWԓn7Ox4^ ]9MYdZ! , s di'hI`Y|pH,kȤrD tv*$TCszxL.zntAe\^1TgwMsl,n\ " l R "wxiKR ^ (2i7[R` o`jw%|kڬ] Y{ j]'i䙖y4Usc0m2E=pf̵BȱǏ Ck knQY3h$E(šj`F#@ JQZTDq'"5՟"\ b! <KI  Ib\ m+,8%7,VT Ss(/KWTPܹTub% #V!WЪ˜hs N$/Z hVΞ85t)X+'X%Q>K}pI(%k&BQT$gU# 8ЀW Y%v+ 8K9hw'H:(չ"@ۅV=@%hiH&$R( B|"hO S40^^p0%l@"9c,< _J\ӂIS%qM,BQ İ[c'(ǠUHBC"sP95ԣq&Xc`;曘rY̠ڳH rfJ+9ͭI}<K|>`jF~v]lZC@LvC_`?\Mk4t`JiX̢s,Bȴ٬bjmm;]-Z;yfDռ>UF FH")'Ʀő)gd2"VE_4tX6HueA٬DJ]|5IjZg@iԃ Q(Zrs `)v }bXM+5n$UT 0J|, Ob&ƀٍo^A{G,dh>/(MJ7޼3߉UU&7`=AuKtE{`%}>@ 5RSJOSPS1r*&(axݚ** (H(v{82ݡ#IeMqVh@(U@Ogd4 k Oh R2EvI08XmQxpIŰ`a@PT 8HG]jT` MxK^MXb6EN|Lx%2> `rq8G: 0tV'/0xkn ԝ~_a( aw)ӭ@ S( o楛 'L-m3%^ @L"HN&;PL*[Xβ.{`L2hN6pL:xγ>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^MbNf;ЎMj[ζnwPnA]:Z,v weyvn]- H:%w0 <rPn\[ )[6>wĻ oCM`Z8SĸWrk`0Րp{ #/Aiqr[@б(=4_EՔ 0BSFM ʝoA!d;1msÈBS$5;H+&`WO|_n!#vGE5si3ʫZt[Ɏ!"`K&x)C$(?| ea981>mSWEԩFsy>\wT0gǣ*RF5P+Z_Dp|x|gIG=:9'uu{O10? g6߇D%{g5cL$yC2H c+3(02h X0ẃJ P@iA^+'|P5J?7D#L5}]w?9ysqKO' Nca/G>>0@3l؆k06 vxPEw $B|aO:6PZ!pL qs(X-ksv؇J\YP4JH!G`>pS5=H`Ԅ#I$:x`{.)n1؆׊d!M ! OC$D}7Ix"(h#q."QI4HuxUܵؐɆEWx8=s+td5CsXiGQ/5:$`?,&p#QIG(y8px|t }tN%0`Ja FFP=*K\{C($)'Q/)ɉ99d{XxH OT /zQtDP^vah6j0("'.h5Dj0cbY9@0G!:PH:bx }u"{ }WjF@f* ` W2U_=򔍲~bQC$'+bb!INJ0ubS ?nx80yyuvR!M'%?Z$`w']0{u3[BЙ X‡07"ґрnR TacUfuVA J Jδ`;9_X+v4iwsg7qwwy>~r ZFzKr /(҈NPjX MVzXjANǤϘ`bj2ZG_:jlj$`\否mZvzxz|ڧ~:Zzڨ:ZzwʱTCKX?Vc.WqRz.I;uy HA恓XL*C:5) 5f.ڹ Ϊ{ݓU'Ys!%P5vc ;b!ja@!ZKu1R~6F棪X"i>B4zU#e*BɪF H:=2VAp0U zgs'z%[;@!a%"R-(':% EEP*PJ Q$ X*RJ$*-P{Op'PKVK;4 pb1K'֬0-0L$+-!+ViVy|)K{Nk3O R`Tg ){gk6rkǞ F x„3 0(r۴[O(O|)-㶠`<ۺ&Ѱ$u`6'&`༯{S*&B4Hz79@ ԷP@R2=#1 F; :kھk;QCJ!b6/`1K"`?3KG5G]YnԌ'q 3ȋK X EoE q( I6T <;{/V˒Lq $ѡ>zplU1L2$+8m@P85G~0Ϡ( AĪES\ t63˂ha%L;&j&Jj|bMu0ÐO5PUIǪ+I: 6BbZ Jt5KS3ų COLAlɜھ#䲌ˑ  P!q,P,TM:3¹˱)F@!GGAIχqS=s %W( ɯudƺF# uؼ N"غ/[nQd"Q3"-|9le\h" p =6OC#"ecBx~D0AV1*Ȋb?)< Ռ'Wk!M$G N=S}!75;`Y2u;ZR˺U2t!X4}. {Kn bwʑ恺΂/d )-g!=~! y0k3|1u"8 ҁ"< ˵1@'ЍԽ( mM/rVj0kNiȡ]" ܴ֓8"k /Y<% L!­v׋HD{HE;oBSRMe S†&`E<WH=7]6<]£DD6c7ޗ0i jL`d7q"X0޳>@|^#oW47<`E3>8\ZQ)RXuƤޫB!ƹ ȃ忴1n~KPId.KZKhvdR[Ϳt d$λlZ*to b=M~yJ ;nD7FG8u,=GZ)3X܏(pq-cUS93)R"Pb-U]Rs, -) y p\H@ \[ Z^~yZP!՝J$ /#鮨]^%O=Ie1?}⑩\ Խ*`ݹPLj)@ R2;޾;pK;/E=_,NÀ$57O z}{m ԭBtnUu 5{3-R#PvR8} ;4.Cd" 1pUm~?_ȟʿ ?_؟ڿ?_?_ (LPDs3]FK?0("@8I3*R+6%-8,f!iy^ F `dš ܌W"VRւɜcbcBKBZ̀AK@Sk@CgCAA BJABR0peJg *Av3vCI&B<;} S}Ji+"p3 &<ƀc2)ŦA<0 [px,`@@y \* sf+ l"bK*D@V|( \x5g+<0jHbO|k7  %2uǿ7Hc#.b8-.*!3 HlEY L8`7&# >@@t/A,mjVfx:H7#t+k  ph X=$j~<> 7Q!BwJɠ)XCY?LBMTWyq( ᡀKMVBvXȃRM?pBt W *zI@ׅ,1 8QRVUx ІY8"&i Pk[/ <0@S D*8&A CbKDӊ(IX! ;R/2#`c0pCTC %FI -<n.r @'EY.I;t,Ymx(+Η%ׅ2K慊dPWY5*4})VXutwY6tv X%MQ0*GրԼ@CMV6x[D6'QVJ :*u=[=ki8#s,ݩPԀLn+t2PQ/3kγ8B>h;!Yl\,x =*E E̎:-GNw-  p1X0\4y1L c A0AZ{ft$kʠDKiGHԢ[Jc)†˔"&MBFL_ԬjFa2 m]k @cO4G ^*84,M3u86Qs6n'v!'f*?zpvtm [q-n Rby.0gA5r+ z9 nҡ- V:Qh9u0Ȱ@:*0WQk+ۡ@7lg:Riu #|QNO6g s&Z?ttx77K9#Mc+Vɖ>^rH6q dgX$.3|dŹ$4Cko6UɣUTm#||FgrVU;+qz qh㱥-tC D=+u7\5ӟK%s:G, _v!/o"M9>Wz']كآw8cNW{7/cu[ X֙ح]g[\ߥZYI hA98ޕK|EǸ PRZqZeyue U5'\.|5߂'1_hAq`?(J .-Ɗy] .٩Հp yj ]Q$lAsJg֟4_ _8RalA  UП*}HR|I[tEtwJEIeIy E˭p%a:MUDž ՠ,~Jo0"aSu&QNVu) 6A5`EQp:$;(M$5@ԖCQ\:Z_@cbaK\ zД9qsdIY $4d@]M.8DS@"${U}PIcɑL{$.ޕ9xaMKx]]ҧ$aT6M`Ü`IG5 9p/pu̔ +2i(BlʇJVԉQ'  e B8(E`VF¬ffPJ(dG:B96B]$CVJZ'TC3T0* xL "#)bdqKd+:$a}Z5ˊɔLN, L^V#-jc1 .-Ӷj6F-VNAҖA^~m4v-OԒ-ڦ-ި-z۪-(#d >c?lmk&.Ip2Nn@.ëP~.:œ(ҝ.ꦮ.붮.Ʈ.֮...//&./6>/FN/V^/fn/v~/////Ư/֯///00'/07?0GO0W_0gSŴh@0 nTCr|,&jSDDW4IIsEgxwVF,4s\HZfH4NNrM*m!R/5Sk0+ Ȯ:3@2Oh2A7W5XX5YY5ZZ5[[5\ǵ\5]׵]5^^5__5``6aa6b'b/6c7c?6dGdO6eWe_6fk d6h6(ku!Eg6kotaue๶m6kAȎ4np0Nqh`B 7s77 |z[er+Mdh797xϨI) eB-al J6<\x׷}7paB J IdQ,Tf}'0=cltAΌp/8wxm"(Г?"&E)dx8xvFQp2Ɋr.ɸ9j| x4\v r˄W9WAyW63t#X98nݣAp `Sb*9yĊJN+wI0?:6ήll x:n~w8L+㹬w:ǺM\6pb`4E_y;/]?kڒA\iKRPǎ;WXo;;'zO;oMh1Lu;ۮ)ƻ;绾;;<<'/<7AeU/\aN5cn͠Ǘ+.- ‚U@Q)ee*)3EZsЉ4CǰÇ#JHŋ3jȱǏ CI̗q)qKA`h+1+-˚ |q@+ jR^B%bS!7U L $#Ѐ5sWv-$5AgV-R &ʯ>P)fD}`ʫm%`mDsX>[3ȧZ hUwļJ)R|*yiF~(c\/SXسkν)\!d (Npҍ $PXq J}}\v'n@cjF%|t2<"Օ4zU@Fo@N~*@̇ /= h; bW 6b'E栅"7C 2\ gP{d`svG#zi̶B;<I |MDL~qH^CrH/LʏNuީꪬB+uR@;guf{c[-`"y)A3@h1Zȋ7~/dc;@ɫ]j1+|@f %4̼) x F Ƚŀ0eQE=Y%%I DmH't*tC9}&KԦ(3h0PSB>LP鱟s@k{JM|W}yGx'%KI p0V(G6Kg3k\FnYUfGfZN$e\#a@i;G/+}BcaVQV!FE(LVPF M A5Xٷ߷%EH4w,[ٌO|`#RJ.l b7'xA]QNg((w ]-7Ire x!{>pJ  DĆ"\mVJb]+BH2h!",>҅>#~E0[fdhFHAܰW |44q)$h-_TI?@2 L #,mR$Ty@bh wR,1U(c+hƾe튃d*F=M_IrZc!d<!%KE lLD9[W^A`AsP2Z(q.KԠ"xd9& 4m 4 gXȀA:&Ƥfjy: .14'.IG=§7Iֲhj/hA#Rf@ zY|($K$$&d+&.(T E +A4_?M~I\<= 2P3A)r$38/32f~ 7mVsiMr1k-W&r[K_"mXsVbxVl%( {9^n+jQ}cz%yYd֟,p f-lS\FaԀEIKWg=WQ(_ O7 FnsW8vyK c*pu%dFȏsǰAwQ k7V`w"DD>gէ{ӠE$ 0s%K=s $!p./oئ6<ˮ&c-tёyZ`AY yQvgF|ԀZ(#$=ۆNᜐ`6ۦܬQijsz,'zu!׃ IADB 0%!Top-(O 7U!"x:x t yE%N mQ8uVKcaҮ&/-leCSž&7H\M"ع7hTا%0HBv$,&,˾OSK+,0n# sU%%{ P)aR7v.@,i[6r3}&P|6CiN 2 4WrpZ x~{QTp'S`BXP(Nt)*Ua+(}8gbGZg |7e eV{>@B9U~Fu%6OV~CPR8TXq&zZOLgVb8dXfhVeUхI؅G9g8tXvxxx6׆~>҇]x8/2 k-!nЈwⴈ؉8Xx؊8Xx؋8XxȘʸ،8JxؘH='$88 H;#{KHx蘎h`+,a ~Xxghw>$=t.vkcyً4iaTc昐Yȧ'?6-Bܧ"i*W)wJ0aeKH>@y8)G>P1q"15!ne AYVyBHC ɚy%1b]p58~G șʩp?БbG5V{sM˙ڹTHeeӐݤ2Mse0f.{ٞf~5q1Dcݶ: lR$1yqq5w>d70izꗷ Dt!:(Q%i# x,ڢkIoh`4Z@6ڣ>~R:^4X{,/!Qp&'8$0GpdP07PNMXeC9p{ΫnOZkwp(Dz`"x3 Jaa@ <rZHm½!=0)!uxȆ0.uB7Jj]3Cge!1 `;yœn%>tlÙ1ad^ %*)} `(|-qx(ׁT\5&$̲Q!&kp@Pѯ">k+-pB) A'ݜ& ,, .A6ҊWԘ]G}=2c|7ٵdUJ"a]]Soz8D|S +ڹPm5[~V|U*PU :;%\,|!T;ՠDFeُ퇛mpM"2D <BRED>0X"iC~+3)4aXNbRwpڝf3 Fxv1&2nC߳$V]s0sY &sq^۪VmPLQ80Ӂ6}BCDaаW κ$âg !Erگzn`0^Aő mrDL7aLkEp]h, q|s 0y|h4xM+e2}BRy'm ќ(B{s /5; 袬XI,٣Pwc,}>N#XS b1>14w$zԷۡ>Ug܍YgƻPRJPhFN[b D鬑\`UiSaD #gQh,MDI<.dJb8^P/mP ^{rA vd sЖkNv5_C ~+̈́z.o4[;:/.g{ 0daE+ڀESM桪^8 T `0lor @PtPֱ)rN+{(O~OkBHr+ ?ݱour0_ҠR|.j+|C7q!Q;qYp/1AB"0O)POʺ^Ah<"JcjPBJ>0) )G5~n= "&*.26 ly8p@4Lm* pjrJ $dnvqf"N]$t J& ,OUN6B9WZBc@szd/ҷoQ* "44_ ۜ/bxǎ? )r$ɒ&Or!|0@S,Rq5)`g^!4(f K8`E%D.R,C PƊ -%K@Z5Nz_Î-{6ڶo㾃Oy.|8Ə#On17ΟC.}:EE;޿/{ϣO~=:ד'/>wo?{aAv! 2X_BRHҀ=o !|X'+آ/3X7☣;أ?CYG"K2٤OBSRYWb[r٥_cYgk٦j koYwYT'oyzX9sڨBjri@`񧧧7!-6i'SlD*#,W1c@iL)P[ūMp@ܰrzpݶo]S8#)3ݓ4܃^xrgf3'= :PS0XL3f_YWGMgyq޺!)  _|]+3ʹucPG?`sBK}z[Uv_H?/yH"01B0 +h b0 ;0"! Kh0*\! [B H>^h4Fh9< !H%(0#@`'BqIHd $ Q"*qxźh3hr,`[(41.nHlAz@FMA\$RG@! Bꂆ/(OǑ0n2XQRp%-G ;fxhEX\&3sQA;{ /9ϼbX@3ML}`Ag1%(eo3zz&K@NKqx?J5C(A>Ҥ| 4CO=P3r81wCїtJɌr$b)?~C4BC*6{ H *E)23Qơb5G(>p 9&֒LFVd#h@1%-G}+a 6 b2,e!b6xf;ς6-iKkӢ6]-k[6-mO%zeV"-o{7.qkgI0S,ҝ.ukb70 1A C>7]/{w }'!jo|80 "Ek80+l oxj"X=oifaJO@+N:120{93Ν1&=*/Bg;(V2l39@7EPnS1V7S3_K54"$3RTXVΨ9Ғ4+9q>rpi\s|E h\iJ!m[,Vj[:׺uq_#0 TܞPuJg# ekٙӼ7msg^w+R ;OzbvŲKUg;0 ۍSeC<8kLw5{AUu7|q 5w m<:y{"g|5(Po ({UvǺoS;B`69׻GXmaXR᪤ađa{a[ab ·H!!"" b"2b#:"F,-0#Z%[$e`'tib%A()(6++ubb,-tq"F.c0—. c1c""wbk1Bc4bG2[#H5b#$v!8G6z7r6V%fD9#w! `9;.c=#Q#u?xc@dA\>dB*$!B:CYCBDRdTdFEFzr$|HaHI!, s dihlDAx| +$P:ШtJZجvzxL.LJK Ip4"$0*LF#/rPmd+ y(j?9c*X<l5MƦ9[֖$ #j"ADHH%(Hʁh|0Nl@`T> 8 a ٕa . @ <7"hԝӧPJJūx.'~ Rj(1ԋvbşuccxHwDB܌PP :!m0pߒmۺ^ͺM6֬dGæ`$O ,I"XaP?4paj'!PBH@ : <azM(hܦmZuC  :B@ƭ~1{| u!aXÍB͕)P]"g#p}j[ hH&,587D AP`sD&4Lho$)R;  e ֡yw0 `#%#%!0璔Vj饘OE*jXFZ(g6@'ؐ / /4Ylj0&yjgF)?`+jq bZ)!bjp)Fn$ĮB~ x!8 É0@ږ`@Zi$lN.VZDP`Y&t!ýkj#Tvu #]hE(%T's?0$D(-d' VQ>Rx-πkS @ w*qP;3 NT\4he\WnyTb5"-V0܏|'/v(HDhF.ڴoÒ V& cD:: 9@yZ y;" }i(:ո@8i٬<=mXc\/9cp$`3Ò}t'-#R0";LmBC3<˟x!u[ΚH"I8Fƈfy8̂-t3P YU@9+0g 4@_EKV9q&-ܼHqf3LD"#ΦSl$`%Ey@ r`!x8ANcIvED(AJBj@@*x2LsTIp%?iO Rd!*%,IjZF01lz 8pL:N"qz,Wh=D @JЂMBІ:D'JъZͨF7юz H * 91Җ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:,DIZ6̱)&۫E M-#u#86$X$( WA"¸-ĭgM0-0d%P yHB@IHW3J.+5{֒µNr`@w#Cu'hDJԓ^NKz^Vy5nDM 4%Y΂ 9K\Fx{nv]䯀`!,2"5*pĉRuF!=@&-' VKf"o]h g{:`&4)RE4${KP~W6 0u31f8(I󖝳9< KGeYpln1(jʱK %Fњf) 2:_g%A!0Y>0`ΰQ(:Wt̝-7C%V{dx#IhЙ-;NWX(GӘZ4P`}U~P2A}R ݦ2e@ʥ`mӜ6č]7YzK8* 4efxhxh}#%ie@py_d%sj\-/h8B"I#N""Ud2uެ|F{#ΫZٯ\\\UZ`XO3zM;=w\(%>:7A "8'EhMl[b ¢އ G O(c+/ӡxpadc/O gGn^P!l'8f'0#WrzHcE|H6&nA K5z/6wye9AJu"p'nw&Hot4J,Ff٥$+x7":hfc41Ho#k!36$۴"3re{}XDc_ Ax&x4&pҦ^ \B'wm;&8Vt炅.0yL(,0Շ8"Z\0SGnFxp5}vGgR;{lfcm&Ri(2 }[Vd"B(H:eWJ@fi2QpTWԇ[=QXWDH{4;4h3/ς mxjcgM}+z Sv'ЂͰLwrqhx`NH~W5i0f;WjW;%yg,Fs%%=)s`xYc=Bזw(g0!X9uTc6Dۦ0Hcy1XCw "8r?h8"Cwbq(F8[3SV8.{Hy }›aop_w(Hc >8hv/ \PHPZ|w-ڶnbHb!@g"_9wzQ蚌y痄38N)"ҝv f IBy^urZveW&1 R:9|o Ήp$F(fh d&s\rphyWu&H5ɚl9a)lt8 D"Ą\u(7e)""@-aH|=SHwz&sKgLgaB=`S9g 7 3҇gfo~f;G w2ja_cihhfd}PF.1T 0PR]jY_Jp7bsXkl: [ٌ#mezyx瑃go#~& 69|ڬw:;Rd`M*&!R FI#bȪ:_:puz+X' JH 7NJv%KҔn]#b`68ДK%9vpyKKčtVY6{8:<۳>@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjl۶RRDޖA/D''t;5K I4W4ȴmy^k%Z5i8мZpû "2,[)zX_` Fs߻ “y  j s^;YhA!aB)z3+bHaыFC0*S@9ؾZPR!`b ªP3`>{   5t $Y1xeä(p* KBfaT(D=:Hli8rt2sL̹R2[e\(|uIP!:o.s{w9e-|&O`ÈPpZȯR ۄoSQ@6;*ڣsDG&&\MyvE߹/[Zn?L2FkJA)CVͪqQJg،IF4!7^a?&^rZ\#ӆ}aZ^^3uCT6h,`qQ_(-HhSIZY&X*~s*~A߃GXCۣ fP-*DK[Fڄz\ m`*Oh" @ܻ?;hpʼðd3 @<ΒKGFY ou<,+DEKY/%"SYI[GoQpCE2Fv,/ [*|4Znt8?A{2` @6n/!F U-:>dF*s0c#h|cdGv8@CLgiG LxJWԧԽa~2Y:^pM3XC[1MH=;c 7D^AJ1KCICkLfXa+VPRIఝv$ަH &dJD,D6D"z9ޑ=kPNqoqoFANX&"L귩GzJq gv܃mxKgua᯸k@Q;-LnFBdg5)Z'f3ܭۆ!l394@sP{#~?#(t/ι~l׆iGm F4s5lF/}}Z2q} xDBJsmטlA}p-,sg؅0MjY[rfK|m0ƣ/e>6l,,??X(oҫrc"@Cy5~!Эڱ& "97Q*Uס7\[L>[,z2 3\y#_E_|Z"xA) h(Љr ,$Hnҁ8CSE{pW{0$hQ&]j;ҷs-BB -6Ǽz"s.uW~q+z&+zCȟgQ>t+Qv}#r`ʲu8C.o_; &8mma,vA0@!%@;5=K+hem 0Xh40#@PGdq W"p$dĜ9>BIBfjnrv≀(d̰0(5l̽((ք Ҫ8d#'+/3O)zV[_cgتe0QqLL)}\ד  񋆩#p!+Gkx[' CoRHE(^%шW#~* @NJqhR*mڢ0N6OjA|"qHѰ!7Xё"X,-*>.YZSEYTBa osDkeҦO K1֮= (U$pB@^+kobe6NL1bm7DΪA2!𖕇eݶ["&"S$-ωJ&D@a(h 0@QuQa;街y* pJZfWsJ@Yt!3k'݋1V0e| 552~CސB1r7)dY&fP`&_ @>,iÑ}Xn%-k3 #)9 u'˱rhf iJwxN-g )9T GTI (ZO0@  mCT*df )qPcpV5''jR$r,r(nKZJ2P+%yp?(-睆˙T٫! P10M +Ip'-wC_ztΆK~ +T+tqbRKWnL 4kO[y8=ὰ!L볌Fsk 6oF\B-rA%W;(oc -܋k B .&+GY1(ˈRƴ "BO;R)U"{T]EF\$i!Ke@ !={5LsȮ/A"!ZkVːE qW3h]%_X/I O Ӌ/oa$]t?bd:YAF80H Dxȁ% >@3 D;1'<`|#ʂ:@U$ O!rC$*׋d3 'QB 8#X PD:>]JT Y9bDW H K<@`9\q@JV;*@bY`$)9q-|e?($xBRYC!0^n er?FqJ˂4j9ifJYnӖ*|Ʋ+< P.tce8C Jbӂ)رh/6QDQԔpf<Ԁ4Һnu<i;Du{\%MO w~)\/'Vp7 _hK[N4>n;wbyHM5f>7wû9`q{;7<8 n#< _8C<8+nc<8;<"9Kn<*_9[<29kn<:9{=B:ыn#=J_:ӛC=R:իnc=Z:׻=b;n=j_;=r;Þ0Ln1;>)7T:yC>w (CP(5{<;σKF8d>d^1=[R>={qK5[u*Ɲ9I:\)(|IC>ҟ>oԉ'uB3tQZQ'+ X?_?_Sh8.@P,4TCg3` #iM4L5S `JRֽ߄MO{T` `jTI|Ȭ ` ] j!Ì NAaå`d jDGRaZF*! Stl !&!p1~KS aN"`E "b"*b^JSW XM1!.b'z'z^)`x  b,,(J 8!GtPhBA -1"c2*22c3:3Bc4J4Rc5Z5bc6j6rc7z7c88c99c::c;;c<>c??q  @@"dBFb dǷ!B (dEZd_lXEdHbB^ J$E#iM`B$AR2%G5zMIn1TkM+OSXNy\)E1%N+Y]heY[z$'eb*fOu $LeiW,p`0bfhBr= L JM^lѥ@+DA2&rH;q"ϵllA_ D&lf$vrg͑h@@Akr fw{8(@v XznM -g %7Mÿ8JdJn- <7pο)^GAPhhFLMݢf֤\W|rü%P L"ih:d@R)فVBj~+ԔD!阢r隲iiiiijikڠM%:*i VtJ<^ZXT,HBElK)^m%1ƝiFd: 3DE!ET(]X{홐|HjFւ9FPvre},Ts4eԼBjK_j<`S|ٴ`V@ףqbJWNXgx$UATAeE:X151XLw@1H( Ab)}Rm20Q'R _0L3̀.^Žnj&v Nη|UmD$Gpā2%dN12+dYR±R'9U$>p4C9 0CTw=XZk=\*ו-hla|Ee}u h@c ʥ6EOCYY$YqșuʳV\VZv%@ r..8U>4#ZHw2Yu<d,b%)L- -$u v \z \ n ' aDMEc"D//C~U`V~G2QB@ִDTZ +iSiM(t$h-c0ETU& D1M`)DZkL^ȋ(FELyx ! nzB*Oqa4CY֞ R T3 BQUF" kLQaH >PBQ ShR3' $Rl\A}0"-{|LX/7Y5yBn,0A <$9A []ߤeVEU.mYE t{Yhk{, qROPtC2 )0,~BAdH+PӔDH Ă2_(Rm-x HEf]TL,AkL{`e!;^H (5+&>s MfU5U7GC ,\iҀ D4jA5oG HA|ՖaiM`9Q_/W΀1GJ.YBCṔ C|N4@//m8 A7dlpd[3 5f*K*o8!4=/ rbs0+uX\ ;H~YmHkǖ(i@YIv@VO{A3gY-D4rMd LpvVPW YQ3mުv 4wsJt5`),SI{m6. qDo tZzE,ܴhBDw< Ŕ4 jMyjD/ZtGP@hV{x3YRK$TPs-dJ':(Qk4dy9R$UX 򠐈`5 [q _C]E]C׷Gڊt {GϷe#7P")ɘG"eH {͊1rryʨ]$߮튕{d2{ٲĪ٥ĪHmؒΟz[-%s'gFf$vpcSsU &|4<%;O:|UɟSsXOg@8di2X*Me$A!|t ȣ| _y{*ڱ mz#7:0C^4ton v`[ pV#gcDdKzHSz<PÜ#@[w:"G'V(}=]CLj򊡜п:S$~/$| npp5^*vY]78#hp hAL픴{POh7(GW xVtr哎hYO?2Q>≊C40Bͧ@BL*ۉJH05= N CK0e`0$pP(R`@b5Р33Cc2`%`t"@@`CZE's`BT`H{d 0@I|< ]@0(L%zL3D{2kʪ;tPEx8H)A'X8Wb' (xqr<(Vc E$ H@$åD6ZRlv)p`@9\20 c;+bP\"x 6رd˚=6$bϢ$purhJ HӉ bP؁%!%!xX=1Yћ\kW]v&"['zkH;٫HM#):ڂAfGud#C@ڑr"}4zIf$>[RmR: 8m? `V 2V>aNHaw5,M!.^T%  m1S P9} WQD0N 0d-_p512Ex8`"!d8@2"]R=DɃp'B=Bd2D< &21}@;DxDfeF#  ՞4 1_^=@6R2jOg?-BdiXHll.le)l\qԔ<8m~ nKnka;jk-J(Koދoo"ȗ+pi( /p? q;+dRq8O r"Lr&\,Idn,2Ls6ߌs:s> tBMtFtJ/tN? uROMuV_uZou^ vbMvfvjvn wrMwvߍwz{ x} *)٦c^yOY֐  @\Qz袏@I,l1;ƐN{zi]" ļ|ʣuEj.H1}D 0IQ(O~l;у08*;T~)@@ C aߠ"\8A. )B`M(_Ri_IxLG $ ITd$*m,\x6! PF,^0qW֞E3#|T0(Xtfc1 DvHkD*fMN8}8Wx<^'?0i((#C @QPr~aK5t GTd*s &4&{6,VL q`3P;|7#T,9'$ 13 ٜ\F"X9? PHpgT&M) F8 E* rG? R2(Bv7ب1:Ҕhę.gRkt4i2#2т:m ԠvRZR6TLmS ըJuTUլju\W ְud-YϊִulLꈆBt+j- Fd 6jQ\~Jcm,ޅX±,59. %g?(w︋%QNv;3%O`@+ Ӫ$msu?rn>,8)x8U,s ^nip&B #Юaͱw:&hKAdi]wv+y9~^/Nx&(hƱ} xw@7q/  x4n^  "@ wD.rz[WIJYFLv!=#[d.s,AʃHM—?,>Dxe3@v] 5MVkq6ƤZ7Pၡi"c?PG>ʧ1&T7V+hAiN]0Uʍ^-iz״JK7ό.d+{n hK{Ԯlk{U( u.ύnnQrs>"JuKdR%uR$)?1NS Ѭ^pi@)*ʏ_R`\ֲMF\{,/Mۼ-/%W.(ܝw;`.g^^\ZBRٚk/ܼo"r`X4d$1z0sG @IM0lE9E`&|g"7zrk]\̽zH );6zӏܡ9\ʛ֎菏wD](BЀDABeExBjŋ XPsBٿȧk [gbG ,8CXd&gosgab;`*n|X3~EI*?DwfHȡVS01[Xb[g S}v Ad:Ȱ"$Au"eRoA0"I h ҰYzf 0TZy |А@A*Q|sS!x5*B{&H PyJᅏs{tr Csp(@|;phTd0<&{LT%65c6%5 Q0wR"PHb9q&2#tX `O&,S+v5ChRJPv-E v1{E(0Ap*x<ۈH?ZzY`@v,z({. 3W[;9 |J=@. 6tQU=J@ GH YLQɀ=pbLI]#`$Xݨ9 ^E2Bd=\bGr y5P` 0C` E 0T (4wY;YVI WĘ#uI$={Phw++Kz`9dJ`eqC)B͙ !*Ux80$E\i ɍ=; b8ڑo!WJjF4*,[a#I$JPJV EQO,b@WfQ7V1c|kʪxڦU7.0JZb慓'aR!¶ը*uiw *@#ʥAeHHf1zdEZ !S" 7 :O*m7@EqZ0Ѕ^~KK>p Y#UءU8b3f |uC )Hev }Jue>0pJ5ҰGS@ bJnpW>YT3ҀT{JRWi<KxsJwK+Pe\¶ ַdE; [bа0Ѷt Mɳs]Pف8ahyppESck*AHjDCEt$1HOQ#>G gCGq1Qt efz&R'$΀x@}!$*'r 6R%P7`GV־As"µaJ Hpa*WWCUٽqBp(Zv۱eѹHSSYALE ;\K=nRiMO|HĹFnxWYuRLKJ奎䄒[ŌcCP{є|J! z p ,6*MW?Z˛B5y0( 5)eovʳYx5zώT}\' WP:$-M:`a(mP' >]} }uf eN)|-,NML"G";Qr.fXK`WN2*rD5T(7 &▏n 9e+>)ϑKP~⿰Ok=̀& .^Kn~욫>Ѿ.n׎ٮ.NVszMmr.Npo/70ҺusT</O`-?n]!/#O%o) '/3O5o7/,)B,( F}E@Q( <@PI%HUoWY_`XAxE@ Je,.Zowy8B_?n-E T>FE{}nnc?XhѤAOqsŰ*P@N0'SxEMp1/ʿ/?o= ͟(XطoY L$'ۺC؄;?0(#Y2'4*R+6r/8,6N 3:T+%"%r> ͉̊ ccB`Bf'h(i)j*k@[EE^A$_ f.%b҉3LBs'9z:{;|<;څyk¤rA-()ণ Q k7r#Ȑ*X H@  Ʊ2M&H2m)ԨR *ojPb0@3o+ˆ ȓ6 xHjR/)CW_@F=am HAl(`r o r{ 5زgv- t([\If8 q`OWI= XAD76"Ǔ/o<׳o=ӯoK[ B8,q@ @?ٵF@c q0$"ff}9Q")"-"18#5x#9#=#A 9$Ey$I*$M:$QJ9%UZy%Yj%]z%a9&ey&i&m&q9'uy'y'}' :(mlO &)D$<CQY<6E;T<0#WkYBW2 X a|Giy W*€./p3W6!S(JU"ķLhm[@ח-v;}f 9c+{l!볠ܪsQr̤4QIIҬ`䥯FY\#Ub:) eW 5v Y@Q(=(pJЈԀxNt#4 Կ+G#5HL hB+IA16(5 hѪ 7N 0-!v6q.Э9V(#| !, ^  dihptmߥxH,Cr @ʘPs⦼`2)X)Op`%()hv{jR/ Ez}~@sm}>snLp4jwK5s|gPa[}P"t})W(Xn7z*(6`P oL(e!. ! ,\ Ii8ʧYׁȑ4MG@&XcLluz"P(($BȔ:ӌNׄ2bv|^D{8sdcVj7C~n.H (]2~xc,o%&R\)F! ,j Ik8ʩX'ؑɡj; З-~C@\?ZpX<&aK@꓅:TBL gJPB   ~PBkwy4H e[(>l2.*w.T.u8!,y Ii8ʧYׁȑ4IFijXLX` s#`(X G)t0E(Ѝ_ᙲ8Za5!\X.\ i=HSvy>F= xsxf|mofcepnXZ\;LNPR],,E~q,5S@!, Ik8ʩX'ؑɡj; З-~`,3d.N 0@DhyEJy ?XE&I\<#И$ ,~(g*`v JV j[hH HX$LNO%  ):2.t2bT.<! , Ii8ʧYׁȑY: Jq85vyWa8GQ`/71\0 i%XQٙHE@a*'%n'p`bdVsY[S5M`a1D I,?p3e.&yJj=!, Ik8ʩX'ؑɡj!U _8[j!9"(#p4(dFE~'y8vQ7fwxS25k^  YN SL[.Zg*&&"N! , Ii8ʧXׁȑ4fn(88UW ,e`֊^z JJSJFV%35WmNWHQ\e -q* :i,tu,&1"p&X!, N0FV5׀ d*i-q05-`JH"^N-`t8-VN6-h@{Ih!, Ik8ʩX'ؑɡj; З-~qx m"@\E7i@:] $ !py n,nprt|mkik=J`bZdTVZ P^MDpLCEG"s(*Rq$ZPbsy N Zў.*`4!, Ii8ʧYׁȑYj+Lc|9q84 ݤp4/O   `@"E !x$Tpi8YGi{&jJtq&kmTb eeS7MR\ W M `96T*r02[.?!, Ik8ʩX'ؑɡj!% _Uw`((p,_3֓ !, s dihlY,tm8|pH,Ȥrl:ШtJZ*!x I JyDD@{B 2l8Px lrT j!am0B'+(X#/դRj#e]&΂ \jJ #o1@GzP{g/GV" e CΔWnI >udxXP& R,Nԇjc5ѦwKqLoC2Ry9" lnϜF zu 0]< ь-Ue8 2fkBbŌxp+Cݛ7ylt\= R9<K<A*pÿ kI ;g2=#fSQqD' G*ZQC@ⶣP'3kKue@/ cH4yڀ*^2HT%t Jtҳ0`e AUL ZC! t.2x|-"Z@-WtZ2qiPW(`ਾ ^."%/Dg\g9"t,'\Q\ `) Uade2!7_*QJɤI *Q`F( w0KY:{3J`PAP/Ym2Lf&yƞ[ vp+# ESx <I2j1ClgSDJժڲ\*GgtZ٪U.'(zB +G@+HZ459*b4khY;ZiXZͬD&D% dmC^f6~Be@I+Hl)(`sV<%- 욓-Hpk<*մTla,\%֘+iz3ы# AcPʻ fK^)Um`3(2*>m&E&a3Yׄ hl2 0Er.-pq;m$ –M}--:A QMbm 94 mqyg<ޝU\t$?MoJn4y.`wG6@&+xבsM|x8lpbo[AiFyaa.|8JkXs^rX,rR& TIWfa<%Y VXXbҊ 'ŀK: cf d*rfrHop[ wrP[qpPC?0 H>HEpN hg25Ttr+Fw!2/3~ T07vW,$!!N!g1f 6Vl _I(!8vq= n.{efR7U8.j8CKxOgQ69G[D$QYg8 [x8!U@ PMTmcf1 :q  v=p,\%`='C ' 3i$d@95 \<80 aC3 p5aa6Y+Iȟ[Y'i(Iā<GP:"ʟOk>c;@q1j@R/Kd[ S6i} [ʥHMh *^abR|}U*R|=IQ=p;"W3dyʟNճSYGN3}{W J *ѕC(5l:*8 d7g#C"i+ !l1bS:BzjPԤq2" , &i fƒ %ӣ\R.6!fZk;/D~CFfvc;CΙhaA*i{j _؇ \!6Mz5j5! Sfڦ:y3KO  #q  =5Khe3g1P0@DxQ#KMwCbG#I`:V:0^Zȣ1h%pdH4)+2\Qj 8u.qisBv{VE@ NjRFEA5 l[dJL5Q#DV SE?!Ҙ 6a!6[oQ;-KLH^Mdee++s7ٯ$>c4_Y 6HP1 (>a%;l% ugV(8OC1Om\ DU`ZGY )\l`yal+N $ʀ/R"j,"ܫ +TEYjI:! EMl߫DKiz׻(.蜷"adM=Z0j.VGB&Mv3vX|1%.z8uESu S`4Rr1 BV]<%Gq{ Y4da@ש@-=:-u3Gv:&йQ)(6SM4Y(ۙ!"຿!7k? s7e(l^\`ٝLKM*:Γq,p U\-  y]nSM&ӭ+ÙK~zۻ\Xʺ q YY|xEۦ,,xjlM:9;2üZ<PYWݲ1pU̴r8߭L]CwCh;YZfr|,1[+(F"*пjZ)l}Wn9ͬ,n^nPaG=eglqv/\Z(X>ԿyCY">YX4w(+ÀΪ;~d&/)1lPr1 g&X/n ;m>04s6%$ `R0[q8vY,ҫMk $l$@-6W&ӧNo4 .!Td:#AN^9.uF~^A5>nV1@ۼ)/rq=Ob~S5IFjJT'\ G?z^j6,1ɚ e䣆8L\޾)ZIZ3aQ 9[狏W9YިNӊ!AB /{ajetp`MHJmm|q)(1_HLe-mP$08],4PB\m2s_#Zӈ31:q F3A*\Y9(4 pF"k`(!u,C!hbVjV!K`K Bl'"+S!1إ@FG4<\, Yń!|%6qH$0XDJ2Y1nr50<>>85Rֈ.&"`"'l2[F+[_cgk3/Lít L 8Dz׼LkLk h@*"Lp23¹hqTG40O<ٹe1&@ԭa+ (p\T4N/T=)QT BoZbM ۻ y gHGS4əAq~xLq֯KSg1OI̺蓥*5./?f.ǧS^X^'@I RXbxTX{!X'" ,38 T;jE<C/[dOBW^hYpM#_$FH e+1oNX$X9{ -~V"pZ` RZbrکZڪZ⚫ګ["2۬BR[brۭ߂[碛ۮ[⛯ۯ\# 3ܰCS\[$# u"X7`yv\3\7k WoLC] '4yqSS]&]FL_1_c]6 3 \ }VpWpQm9+ O$4ql w M ޹矃^u,=6}@ DT("S X ޻_0~il-LpSy$CFğ^CBL haq=磟>|)lgv.tu3߿ˎ{?gb,I<B05e0Am (0"T89 Dڒo! khC\V-04dfx#"1˚ mMܸ<ɃJ"hn)(@\.:"hǣiH Q$CD 1\B+8 Ȍ|yO2%)KiS2\%+[W2%-ki[2%/{_3&1ic"3\&3gB3Ҝ&5ikb3ѢT'9W' (d^N򵭜#BD'P;=O`Rٸpa?P<.t!^@ +S(Ge,x!8WwJ&Hp (Mkz5G )\UBu4_ !Lmԧ>l x@\\@" ]!v+ gZ4q5[ꄉT.*TM"p0k׿k>e)&TTE>QQBl&b6bF DRNdt Ǖb\74תk4YPOOp 6WyZ*nX ?T$׃l h&Յ.`Tn3țZ!OHdW+p7}`c%UmKԱok=SoqBDm S`ްJ 8!aK,]VKE]YxV8ݐ z"+12SMT,zUBfZ+cTTlbrj^0&rs&7iz3?:Ђ4 mC#:ъ^4GC:| J7{ LO!쫤{J ZiI!%'$G &<%-eL/c)eXvJm? HrŜ:gԶ-G飤VQ>64Ծ;5OFIOF8简b*Ha BP@ n@'A(04& &ガկ*'BԔ末6]";K abT&P+b=Cd[ `^mUm@b1dr 1CXDoBB;v+[T0k“Pxx@>Q˳6d/U

FAR8!gA&B{'L"<~ܜ6$] T`".iC!`XRj hjLM[)!k,y-N jO !҄ j9}Nڅ$)/f@|FvP`^ `'*b,E, Z#NeF쬸kB>sj2#0d hk_])R.lipFҞhVGJbn ծڦjB#hR+\uvh l!Pm{AbZ5Ώ-6cў鏥iN=g<-8k.2++ځNXC&֡-]FR"{B5&֢. Ѣ`".X,v ֪ta:w]p^Ac9@5ah"l yBf5q튫^'VZ.@ik)@ix5(o2`0/jpV.2W8ЂV[sZO7pqWEL^AeAm$!5kL,_|Pq]"`j`W@ ֒'>m"X1U,ovQe DP'ڢqA u hQrFc9& 1&ݿm_@c~Zc2;EJ""tG\`%ILX5o' @z;@yZhݲ3JAQaD]5[+CLTJ0J/ [!>Q<}{~PI7(l`0|6D~xe 2~~ #G+v@'W+A"TcB㮃gOKBxAWKB\-^Zv3[^'%tc{󿬀 `Y '`8pY2N 0tN )SШtJZجvzxL.zn|N^~ ~G5'845 F 2 ('8P>-{f&<L&'&B  &P-- <3H:~wA"c@7xIHŋ3jܘŬw Ka &!;˗0cʜIJB$hBD@42ͣH*]ʴ2yl@wRc$˧`ÊKh&u`PBj(reȢ˷߿Q`+ 5h˘3kY >0VUt`ECc˞Mm;( crnЁd<8 УK>[-Bn=Q Aξ🆔Q`KɐϿ4$ \ܗ_~& 6xǁ<@a;fvH~ Dov!,JU2'2@8<@)DiH&L6PF)TViXf\v`)dndH50 2p)tix 矀ZIчf8q]6TI`w"]/28P $iZ 4pJ/wtG X@p*7I0B0vVt&v>dG|j#fy2hg":!'naٰ˦k'[.D뤻ff 7gd3:,‰ %|P"2#9 'J p xHRh0B^C 0XmЯ BLZ/M,)&LnJKrJZ`C iRMOD/YG:H(֖JWᤍEDQTJ֙.w|[ (э]<pD170ѮQ涃Ma(Ś6- ֱt+R<1Ʀx\5P-DZ#0L )Nq[ѪT=gN{ہʬG,!F'K[ABAk̶ԏ _:39R"ִ 8yw^ T8*Tcq%'ۅB(-` OpU@e^X{D*\: LPMx-N5 ,ՂLb1lvrԓW̱FۡixZ;NKφMMOJ<ŒM55s0rN+H9*. PuB6;fx09)|A~XHpSG?1]gdŸJ@O>2)kD*Cn4<_\%M]wj׾V}ƣ,n[B~j61Dx~!Tv4'0=w>WyCjg0<՚ Xjoyι%lEP; TP7J$03(tc8bJXB^E$0•p&r€D7.ufb}/u6eMvu_QSgkMaymE% |$;w`@jRR)€+a+|~b* <<:6*l3,Qn]jFc+wIfy]"65t4>x+_b)#Bg^G`!EWWeG%~P0ÇXx(-j8X-sC؉HKU񉨘8^؊GA&Xh1؋8XxȘʸ،8Xxؘ̀d:W#H蘎eqHx"܈'3Ddf4612 QfȏY!ok^L:QF d#*<)P0ҐxG/<Ԁ&$ԑ0אhb(AygJ,sI#DYe6)|[ |*FWFYV ?I9p07 Ztfy #t8e&pJhYvV95T8j9$A4uyFsiJK闿ҘQJ QYyS]iLj#buY)23R.ndoWwyU0?0bYIDƉSKI9)~)ѓ:yѝa{1+93/wع(a6^!@;ٟbF :zfP  ڠYA4Zzڡ ":$Z&z(*,ڢ/ 'u4J)%'3 Y.Q@qL`ailң|DA6xT'7|LZewb"Vڥ<椖!;`FN% }l:#;L@W>JPmڧaLHbcv&7ৎ-)`t\v6Do9 کcR7 #CKmᩲ%ZY[0eE7:š%_psD4ePIVRȺ76 * 7nT*$'m]8vEt:O]:z$u7]0:7#r) Q:Nrs*S@aSXG0QF$ 1:84lb R\y [FB"^0037m3A`h8&{CP/PكM|3)g8ebLP;#T{bXa\`b;d[f{hjl۶nprk$kz|۷~l(D*۸k0odhr۹;rph;.(I;[}+c{!{1k[{ kl@;%2{DS @5?I2=8D ͕蛾껾컾fl;sPgC_;BJ۾  V{+"t~RjC,r4M=nps2bH(.rUQZ cj~lTo؊nkߴ"+걅s0*M Pڕkմ)I[w z;_€:p&'*͜!6pSk>Mbay*NC"Y(&) wGG4e C0"YVD.';: A,K~-`0?AML#APΝCP'{.: CVWQMpTIHJ).| ;^ @+"GՅshua~d2#. '_ a#W!/up)^? `HX{4k8kښ O> 4TP.`d\nKǕC_]+^G66‰C[.>etY;uCOo \sYv&pm/e_lA&|╟/䴫mP<|#)\(YK0 qo`nFp%Q_v7,YY%3~-L@>Ap)`I±<ӵ}㹾!h<"%|B)u Da^)0O:@(()$4,^0`ݩ՝$ı 1Ub"  (Lf"'+/37;?CGKOSW[_cgkosw{;5D"|]-͕^ѣIb6\9TLYq"$-MxB TpX/\ 'PW?[ $-)s&͚6o̩s'Ϟ>8<1Fu"mbcoт7 bw"-Sxbd*Q֭/^11̞6NzM[laPA ,%')Ɨ?#Nx1ƎC,l)j6ESꋻų"d+uE3jS*C\F )@Gl6_YͬZ$cϮ};޿_,=l[v):8M}Xj,㚍-ՀCol,_28<1nn5m0&0M{*7WPwBX7☣;أ?38`~mWA!` '@`@@1Q59JMGKd2s(PM}8 Lrkj*upo|!aNS e`^9Y$P aZsI#B_Bڪ)4J(w[j〯  9J 4$I^ǖ 'q9+9R3 @'  ohKI"Ziqmxt2$!Kn\[hɘsܱ2'90f| @βJbA@ɗv[p  0psVC*C#)]U UJ$-.o.`5fa&%$s\hqlrЊՊNG>ȃ^#3޸CS^cs޹矃裓^駣ꫳ޺^㞻޻_#3߼CS_cs߽߃_磟߾3#М*NF߿0  h@g~ G pfI&ȣ0 ;Ѐ=~Pi,X# !! kh08 *)\ 8Xh6B'B1R4h+RU(tHD6G0\bqh31j\cp+GB$N-t %  ?@ 3}#@ٶf%pW31ΌDKD:,/ƀCbf!8e1%-kiKdL>Bǁey ,h%! O$`X$.f@%*i+D;@50월R%1d  yg2'?, .w*R&?hȢ MDeh5Cam,Z 4<268$n# FjY%@y3B*Qz4e6dpx(i8IPAFd-)TDN"~A:U3< B b4qC1*_׿P*ax75@,@P BT̆چW^A+hv +(œj5^" D7YeiEl`{W-la㸌 H ʭ@,\&<&"c;̥U14+]Hg5˹E%xXTpZ;e«8Q%q Dk Ur ?uF"M9gybAKY *#i/K e+l8 |[+2cME@Z+\ V`gb^*x"z R+ƃY-ze(iPu79>r.˪dʸ\]~$$Я-|^gP^~S1dI,&}RZ;O%oD*:nh#%04 WP,VR5OVmҡ^6l,U \2CT T t}!=2#jtٸe vd7L+2k3s<&NUL#} W)6Jv9`Q}ٰ|/¥[E@S=ѴکK9{Ԃڸf:8/A[.gK=bFKnv!_䍸.ʓ#X}n=QmMѺFӕJё~ !f0dL.##$]utačN -(Pq2L_B:7>竑ı|c?>=-|_?oVs@{q??o??`_""  $`:R`ZI`I]r`[:8` >@  `  Wb```a!2a_]#8aZaA!9Uma IՔ aa!aMJRa!ޡj: aa8Pa!:,82 %VbU\b2a ڂ'>(Za$)acb+"4"4a Mb+" h. 0R &"1:cb] YHa%c 3jFc6c+vc8c9`^0:2c;c< O'<9f>dѣ?2N@$ $A*A2$!, s dich뚅έF|pH,Ȥrl:&YجvzxL.4Fnj8ê  x"l_ z ,& {' /*w' #  #% $[z(*b * ~ᾎ* W";T #jc ȏuJ}SqL8SKy٘0*)1>kLKc h'JsQS*]:Bw3b )L3ˊ@!ɪmSk #5Bkؕ<(@]=`Q@16U1Iv `ڈ4[`eE t6j{2  $q4 E%/X8 PjhV כ\>DDO}O ClcG1V~$0daB ̘`~%F3Tm[NxR" 5MQ&"s+  X#?5 O װ $cC@@-I=(be,0 kމ0_ *%xP |XvV @Ց@ U2aTZ5&XBg/V\Tf+CPi z #jUFJB 0jF@Z48u4a׮Q FH%d$UM6' z<.k@9xW3(bR8٭Ǫb뀶pHzSG25Uε`&Y 2Is`^ € I3=S(A~|t.;F 'աҰ/\/hF0c0!j|(AbΘ@_֗+ jC̔*ƽp Z, ?mp/&TN,9$'pvj4@iI@Sz,lp/F%7 3*iYRϦ'Oe`j5p^Z`Y uBstWԏKkqMC "<P!O3]~'8BqxŁGWg%Zr6$4( &9{,ds;@IDl} !Sn(Cx%ddHPMwLc*,RYL<%s2SX@0KV|'"UNfV0J{E&ⳤ" u<*xz5-YCZì2rU"+r ,!XxTM8*IzFr_ J`TN٦(CIۡV&l*#ZUéN] A8%E#j6s F1oeuܑt& VukR UbGzpӳ9 R0~Ґep$6G D[B>a/1ȵ@(R RƱCnۼ(B[0*ʅt`Qx/{c Nm[Vdp/,tn]7:gHpeV 9ɤ 39 X`@gXT?8(Lm֡+ ! %A.g4Cp=`k3:$/,e:ISԜ«>q;WeXj•Wh46@hn ʻ,UW TK`pPƍd] Dy0$Dp80-m$RtDWAwobP"p LM{zkm"91[uيo^ɾle9lSU;.fά]2:<1»PHu@0F-8Wѷ\Cv_S lP,Uz(}!V~ޤa4vi^rS*`yH-d<:pv0FxkdX#ȃn(萝%OHC.~H$^{!T?`$8gJt,Ռo6LWfϽˤrMTgJTD-U%w892Ci\H? xT֏5?J oTϴTumxDuL5u!V19aD8\w,a2 :3:#cX JE"h[11B23uvz'XrS`C~`Pxp@'=xPs|o!Ba\u@8}m㡁AG s@}) f210Ems&wBJD?}) V6{E`l\v!7uZ(zp(06z# e)!z An$3HPB6E[3<u5 8A:^j"(gXi[QJՋGMg +0P|^3S1P07b ^ Hee L2"!  [m'&dy 0;%ݤrcx<0r#&dڅ*|&v p֊, BZ8gt:f2+ MHPqnE[(G͸8%@&qȑmh ڒjhh(ip6- .wK `dpx`!M)Kv–kzKIw)GS|9Fi u kqPjްo0M™9Yp` cfٙja>Yyٛ9yw'Źٜ9Yyؙڹٝ9Yy虞깞ٞ9Yyٟ:Zz ڠ:Zzڡ ":$Z&zJGa,ؚ0(Z6zDЃb2o"00 p ,s %%ca  ` j@Y >WsTT%R7 wՋH| =P7)EL3J^/4\τ]2{e^0ഄYPOs`WÊBRvlSSE=)`o7u~j+sAeXQTyd[xHUSj8cb,U__au/fuEǷ]uUQ?t/PVEJVPWÌ|Z[z0[ *Z6jwO\#c&AƑWҵ[E]`)p;>V6^GycR^:_"x50լҋ< ,燦f2v}aIDodE)ladl'PeOR T\fВeUdFmf>9z[10K̋ѴjŅFX fíef 1ObHFɱɴhkbdd 8oæ&Dkly;LD[lNH 5NX\=IO-=@dMg]15FVĴT׺{LG;j0WIycN4}-r˼q+7sl![ Ӣk29t U Ӕ^jp, 7i&YBH{l;ؾ!#>鲒/ѿ>3 SϬ%,(:9+(F$_rvٛC&0q"#:<`̘z=_F44)BODPӞg 8R\^`b?d_fhjlnpr?t_vxz|~?ަc`ࢇ*V*b@?A?"ADz]a Lh2~9ݭ= "&*.26:>BF@ɉԽ-pp.] P* TbȊhva&xFqX 0  0c)m!ڎ!ZoswcI捀9W_5*04<4@` x@@ ep/y' PiY  0X<: $8D( R=YK,a#x]aEEQ\a N:ôhyS2s,F*,◒+wwvͫw/߾~25_ϯ?Օ b pFg @4Y'tw&@aQOB'`^]AL TX2E]z""C3U6{8X  Bw pC>i_()X ;M6 Լ0>Pl@[}sZ^{٧ygЄFW5d֏ !mfȄ&h"7nɕ4( F |#h,E)pj,QՄTi:ڭ̖녈LO ȊBBX r碛"+80ڎ`N;XچwT55*"+ %T zE+T>tŪ`5Ko-BM}jble$wmL(`̮. BSwîWcӅ@uf րkaQ" !rc arwiwjf34q/ #uP(p+#%ƓWmeOtdV Un#fB8{P{侵i C՛X##9Wh"0&aj6l0b@q|  >% lo.h qkg1yu` F*?+ ݁0x Kh4P@Ӭ"Ң > K5}="vP/po*udEFo{39so;+7)#ĝ&҅[Yh"d]!?$!  (F0hHv<^b牏ja5J6lߑ7 S24 $(zPKp5GlP ikb3/p UFeQ,=r=I!g1!rT\*HWF( I[ M4\8#X Y" } Y~%`@NRHI͓K*])KP_-@<9'`+1Q%X R69A0=h @&O\|:Iu(s.(L*CHz((}Q,Q4.EJpV,aV0ޥO5F=w5YIVOY/o#$2DaDbXex2!1 t"Z(@d+vHt$` mnSF=s0?~'gF๗P}JɯR G ·O<)|1IbߠIf62"@h8 OIIrKcZ \Bxx3*V[Mр7~ .PZ Mؒ>R`>Ò2 *-.u JؤWJ`'C9|Ѣ(gi% XH Ald"_@3evitDYTFwl!! TW){&Q_0ⱔ ,{2q--aC KCUn< 9-D椝%zNR5kMk!5G[;6\ v^\gC;F!DAfc.6oiem;^7}q%Mw;7<8 n#< _8C<8+nc<8;<"9Kn<*_9[>l}7kn<:9{s'#wE #u +[>ָK uj݊[g0ir, [P pI{x>"{>R@h]FFaޔk:LK ok^?ozX? P᯼K|=VrWyJD Gx_MCU9`Zi N؟8uI,_5< ;) IЅ͐>bߥBި|YB]\BqK1k@@޻Bf[b$ FCoD*JʼnX8ieHjrN -=im̥gk>*˜-6jҠ4-@Q<@ 0ʌ0(djhkhd'+Fh_8u_K(o b٤'X_olPןz\,DP]1aA,VF%qe_e42=@ q.8&$%\YȈHXҒ*R퐖- 2D9@,("B+PĕiٚJچkʁBߒlfl^aD`ܑg4zص CHd vu`j/ \0ESn)h+I \#t֮ _ĤS5b^E@SEPњ&`]:N=<S .1^eU|T&o`oA5b)bەMP(L7a>/jԣ lA˴Ny/OR-?5ި>M4 a_ؾo|,B2@BGo %HS&)M8xpY|p-g)^^Ō`U`.ȯ P!"a]H_ʉ q/ nqqX Hf5j&E(]屖5/̍q{'r4=S1&bZqXN \,U/>2́ʦ0]/1O' (+2&' Ĕ EoRA+#ACRk6LLO2zPd<jMpl ā&xg2>/YT ^u^rWɃ+Lo.3lIy.$=8UDWJ`Q~䯈EGGZ)[n,ElNO>OkTNCc졦WS@c6#[шВ8#$-DF1`yEXnIeY` ,D0zASdgLȌ2zs&3e*z(x,i\D2z &'$-ccoqZ@vdjDˀUCs>d9"j>0V:0 ,*B!qwiRl&*hW7tcPv{{]Sx8xx8se-^L`9u㜏 y#y+3y;CyKSy[cyksy{y#@>{2\"A{fD"\^zm /= _I_w[zYSE?߉$Ԥ+݋{9\zO Qpy78z^9zmi;,mBA另^ަjo&nxvoTwi0v$-gMjgM~r ys{ӦZ/}o(:_]kXl0d +<c̮HFOdk N~@FgFr'VdkeUw 6˼EU N_fُWT߼5Ute 2Tc|TR.C.f%8UEעx-,("xZjޏڡ:'<@!`Q* LJHn=MHiT2s#7R1AV\[a Ƅ:{0H c ;)R5kw{7&V֡Aa*JS$( >3d>P8M ߕH&0*iHo$d扦ʶ L Ģ@̦99J$bH\ …ո|'C Lh g@R6B8p8b؈`2 :BZ3%;#[ ,2t%f"@aaeE#"V0r  ç~Xi Xr>/)mʉ @* bYLP5,ƍ;z2ȑ#( w c,BeiGs-HV hR))`à$>%i eYVi kmP@ Ev%|̃ TCl>fHU~ @ %>@@S*,$`tQe"P) # hVB 0/ڕPp? Íx‹&.qoq r > ?!r*r˱n3b6ߌs:s Dz/5LtFtY @l *MuVsqZou^ vbMvfvjvn wrMwvߍwzw~ xNxx/x?yONy_yoyz袏Nz馟zꪯz뮿{N{ߎ{{|O||/|?}˰_}@ŹxJf :>.̂ J 'i;@`a)ΧOʉ rPf5 6$Da_Ss 1# ]@0( U4PmF= !EP\-8׾ rU1bwpSB#Ka-"XBHIcX( } ` U1GG\wl`A~0# SO;h绖 :dRhj0()KIEq@j!:e)2<,%$oea$[+ B @/Oy(['b)i)Ma8€_ >d[~\LSeXLT &a2%)Oy,˗X"w~N`YҖVYaLDBj0RfABL} (6i6& 'x0} Vpi+u >]p({@(HD6x1ȩb<]O- V5tNiFVI^%:L(j<7\LO\eURYzp=e_觢=.IKVz7^0'QѠ( `FĚ@ mZ=|6ac 5x(@Ӕ\&$D-`ĺ v%+}l`a)]%݄u(YC`LB[vX9-CK+\5tsդs yy_X*P*:@r]'j>qt-i_3%P# ؊&[*P(7dS{ߜ⛠!{p[Yo. \˂Emp xЍ F l=7Zd5GUIvkUn3\ݍtRB7c[5uCePW4E`-jP< 4cam.J1e?heO+ Kֺ@B %*³/J4j%]o3Ό4x/sYɦg Iʫ84u4oI E'yKbnK\[ |Ԟ%$ĭAh;ʌ DZ9N,^ڴ\m# f: Ka}`TJyo43r 9}bKk!oZ5!/' PU\G X`&sݕ TE+횅8dfAfk5odS+GnkMtf- |bLD@}G NI= DOyojګYzl%VrEq r$,y 0w4/Cܻ7oC& (k7$'e{PR7Pg#vW)'vW TX5_h^p&F(k1k h7FIR+r(Q54 SxSgU>HXrd Ad Av?͖X1)qJ}*W5>x|;^4eYˁMNq)6EP^Hi NcV0^@$4Ƒj &P9igB]x]v `[H}z*V1upuH֏}eHA$"b1(lI x'IlApph#~jqT&myQgG/ԊuV8A_ >Q蓕@G4׊O)EMGZbF4'~[8 TXS?/i/ . --`094<M3!f)?薋i~A w#A x2W:56L@7ىŝ 7i牞驞7 )Iy0҃)m}3gl~;6,@ ʠ  *Jjʡ !*#J%j')+ʢ-/ 1*3J5j79;Z#ycCHj13#!T%@" OA" Q& TI @0i*թ5C#w2 !m`HK2S!l:ΐ0C%=.) F,|;SQbzxz5a0p}Fr"v_"H>y)&ЕTF!zABfb 0`Gjǫefw0 K9ohVcG^Y5PE }.YBLR }`$Y`@C PCYP "N\aꚩ9 "T +/8NS}0038E/!J|,+vR&@ cz:CYÂ/"Kz'+<Ø73 4"s1A9A-}MdUGMx8txSmF>}[6[MSkd0;MT!+ӈ9ͨ4r/Ar/ǁj4HZz/,b~خaqPQF?HcC[F52Kи5й5nN[&/KnJ!j 숉+pHZ{6l`bTlR$)0iLc,Ȣoy+rAR$2 N4|3 +w+#`=г{7'ǗRbbV@pZv`[Zsи|'}pQZzīrR;`PQLԻ5'M!UH5#n;.)P<G{|= ʐ֓#psKBU=U9` 뚈HbP2med CYe˱tr]Ѹ 3a"r ;t9qD/m;l;;>7i 'ba^wE/aL;e~z_֣ fLsDHØ$ $v8q2pw"``@V Y#i `zaxş|DG@?0| c>dp2Da w7Meܜ*lLpȗaL $VuPʑ7S:Mד;ŒWC7|t|3pT.aB۶ v# WF9N>LZ2NBZh0p\z+).4ez L+}-[v&ӧS~! EzF.xA&(: cG$ɐ(R (Ket"DM4,ĀQ.vbvvfNkR2KY^V>K%Aͯ8l`&L A(|z!] e[-elK,݂/Ǫ2cqRAt  iIhuv[\v;3}uI ݅:[m㗪I+~{FuGeHxn-w@1&Zh"-/EngE~+ǰș+,E91Z>T^8Ne Pla*{[!@\yoU|0P^T[M%nXK˴\%Բ~Tc1ٹ&agn-c,Q=H@w VyQoYpr,D `JYvE  24kJ;YrpKN LF"0ȩ(HOD$lP3!/gMnJUn*w$+ڕa\Bxo?Aސ(~aFo:,mT*WfkevH=ȸ1wn+3D1n8#DkB{ p +ۺ/]7C;?02ԊJa, TR%5+6 C!Q8P`X{]P_AJ B×AKA&%#c_k(@S*cq1rrҡ p!_r5!@CBagÊIkJ!ӰTȂub9h5"l1c aRh"F,2r#Ȑ$,i ̡l%̘2gҬi&ΜB,'-j(ҤJ2mjЀNT+ r+ذbǒ-kVI=-ܸrҭk+I6/.l0Ċ3n1Ȓ'Sl2̚7s3ТG.m4ԪWn5زgӮm6ܺwM;=AqY3;`V4x K); O cqA{."iB@m*?6013 ّf8 J8!Zx!0Z r)-q~yDrJ WiT',lȟp-Oxx0K,@H|B_ԠbG$z_8xycnHeP v_z A[&&n$@,c8\VUl-bu 꿷BվɲH̶ˤraa(2[0Lbs3B䨫`z}&`2mmBlZsDYĢPglB3`Ľ@z:*s%d&'Ģ(( {B ?C ^"[ b3h @!Z-aD\!3CP@c%DEHA=D5Fmb52%` @$, Ƙa5D7>.$CD?"5KDCAV$Ffd12DFT|GJdE$J<1xJLBLIMޤa$NdH|O%QQ%R&R.%S6S>%TFTN%UVU^%VfVn%WvW~%XX%YY%ZZ<EKRZFeXe 4@<$\]$=-BTǂtТ(L@0ȢL4 ɴ 4-"4fx_~Dpōtd:ǔXId EpfhlȌ\de}Y_!Y&L&ܣ:kt!%^#hZg5=Ȕ _]1j ̞nE 2 H /$dxu.}jF[ZtNXMPK{¿P_8)߆p{D̋i`&,z@^YCu ʵKDA芣 k0*K* |H˅b ܚ7XC~%Z>F }@1ӠAC/ ( ^X!̜ (Aa]EHt)@LMj6АLYc؞le6@\v(hC|;HX ,( &Ĉ<@P KGd % ( I pM 6ѥU՞{ij@.dR•t),<&W &.UF[0#kT> Zp&OF8'2t!i* X$dxrJ!)T %϶M$(ʪ51BP$h+,9Ut䡓.,'glx @zCtK6iꇈ^ɸNa`_ņP”P xF$ лHͮ~5]\E &PlT>hˠU OEݭ` !­pѻϔ$Eq]E'U.̨i47j%I&1tB(m7HW^RHq ҢܐjhV.TBhΕ&&+bh#DyA<%תgޝ@ȵJ^PҷH:ua@@7%He-hpD x=iB1~j9E  TX{^/f ALirrz N/4LA6ٜ/6}QJfHVhK/AJAd(վΰѰ-t b8"w6V . ޔYbA Qmqqi1'\ۂI<8^_YE[>P0C^58*w?6H0pe Ȕd-/Xɂfѭ4wrn$rﲌ́+[c9뇅YD 3]##d3v蓱2Ie[s1tu4~H7C?F+lĐW8Z @%`8J~ @P4o+T璕m]` חj>dfN#sőWM c= n2;RWu#h|BF4b wfqN3>E۬?eJ6 CQ{}S-g59FtCntxu䫄c3YOC`Pw"@+l 5] ΠB CQ>k5^YWWe6bZ;ddRR;iD^׭+*Oݏ$)JAb*\PNDλx/nKM7 H;KU^i1n Ȝi2˕%݁91,: g3TE"409yec@&@0& n߆rʴ0/V F@ɧ0\%hުtHNQ}p8 ~y6bW}ݱblc 0cW VB 򥻁kϠO}ni4l>73z6Cw0 G;¨H!2P"̙ AQ]KI -R)L~ i(;}߶' rq BIzx..BӓB}[ wn$>|pۯ ۈ^J68tP|@ɴ=, _,=du6lu̙'"%:cB*K>kyHM,ʴNBp) dihlK*A,˜ 3pXLc$X `G!RujC@#Xna-$Q^{f3li1 $j"np$`IUIyDŽ0ͅؽݽ ;\1 H0W*\ȰÇ#JHE j @ ױɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhb7%0R[PQY%ye-q4h!N"AA$Pز(ѥHCfԡFA dQuA`*1 N8d1HSfR4,E~dQ<麪{gAC'j\f! vfO ĀcjGk%]?\j!pR-%sd8C= E0} r  u#[j(`eg4Ekq &`*qc`} P[ $nrHqgpxk*8 i @h "GF (mŝP-d1W"9aI"B鍐ĝL7,d!e0*l9ar1$H|JB?<& ra:dl%+ ޚTWxA  \HCCB8 jv\%ar nei5(bpG9'R4@?Q@^*$)q'1( P7c &eM lLI|޸2ճ})#ł8g¥1,Ѐr7W3r"q( y0ϊ< D Pu|y5.Bz)AXL&PL@/V= cf5j3B*X浠u05%w2`P@\[P< H  雁͍@ xZJ,NvV s,p2Pnוx H/C/^Pڰ0 b4_cRpH*A5*oFt#Hc ( y.REm;GyHZ6<}G[ ) l/%/:pt$ DX\be2<2pHuQ."CnAÃZq `IJ(x*$ 'Z̭ @9h5j#P: !K]~NNk\ -g^Ў&" 5 =*FcjWnF#8,jI +ep* gI Sh\{Z@$ 3:4>W1txU48浏uxiw0+OA,MEE$p4V 񾨠Q#Z>P.9[|D`*J7ݯPowv0^łEd\NZ@T2Jts4» MD]Ŝkϼ$Dz>M_P p>bepsqpUj8LG5ǀwqVԣ G:W0m7ox~R.VDq!=VrNMFGkoj" qͦ`SǦQ 17wc0#cIE.a'<) A(i7A 1O >=|"ԓ==hyQ;F Q$;B2>ls>h0g9,B<oB-P;35ůDݦ!J䛣 I@I=t,EĒFI#KWNȑ˴WF(Fe캖A<rܠfg ?F R 4- Ng"DS7m<3Ǝ0vzmqlooEԫqTq@nUwPl a,W #g7S^TX@£?`:r6i }tYF*QV@@muLȖ;ƠQ?bglVa͊girbUZ1~~Aބ[xWY7Ƃ57vTjG$@p>Xx0Јf`1,-V?A=ExKrvp08E±"M:Om*pFV2F"б D:#.lhT _`="9R`GE@e )qH^H?VI& p@fw!8h)*A"sX"܊.H Q3]< O,,.Sm߾OES} EQ)ç"uB0 +h b0 ;0"! Kh0*\! [02! kh0:!{1B"h#"1J\"'BQg8`E/T[ 1Jm2kLDetb $ Owx}xciz=.dts'Hgc@,,xQָ%E 0@=2 "#Ơx}{Q,pF4i21B"ێ0U)il=`}҄4JsMtT]3 6 = '"9Y/Dix_|-qɹ^V䗳hFtXѐ#]X}H9 NۇKT%l;Z& !ɵ' eO2X O`We(H$4+ViKfJ:9rvݡ(I3YS,Iof:o'k>7Cn-}nȌrMk@M/?ue~5M7-辆-$ *JfVcZ4"{a܌=Iհ8Դ U{5i߬⭀lx!>p +䙾HEQ5ڦXFm)ҵ hE9ISmGR+J yvGkI2%Թ۝% E\0J)6eSޚ^?x!ܔaTiawyP vKC_N)iR hbLhMv6p>M]~ PPƱ1)N@1I@WII '8(`C@ |"e&,, ,BV( D(|4@< bCew Rxൔ[9 k* #CJQ#-P01X2aš:cmXrG5Li,&4FD dkr3VTelb!GZ d7 `L)h BJtA'UP)>JNx"(pUX0]EbKne`̊#fIM&m6b :Hk0b.8"NdZ!Q3:ԺI!6jedfkl$LXDY|Ҭ6+^&WMH&ـdٹ؍SnΖȦ FNGӘiefibX,,/r"OhPi2SI*߳0k P=jfEd&eBH'c`awA,|L'0tɾ h^lxew>z p8 DHFr(jV&$~bJg:&Š-h$oFPjŹ󍔢L|dZm:)8 عg.tL}mROUt5A/Āa`FYrb2 {>K_,P$_OŔD,f5NOev~0}]jf)$fB }{Dz*H©^yFE~YYqHf[PCuȝ9J^P*(M"E Wƭ:\q Vш`HEmH[iD5Sgm$hiopq ~L[4|KƃkRUjlB4TSgjfة(*}nigk kg~2i !ohȚ)5 q1ފJJʫȬ* (xI ‘TɩAY`›M- p~ޝ"NVCHcW\ Wuh4CۦO4KB*}.x͎ݭZĚG8 *.Z} HB =Bw-|jR"$ޤ ilҬ)c" OVWFKĄobhO_C) ϠinpRB yO9,E\d#)G_jVXPFptp l@sbPĊ| %D`WgP af ~ " ~.@ش@op JAM[lXaQS^b1Oqñ{|NOp#_Dq2<(qg ӄ[" 뎉Br%Tۑ%* E(r1o=N A# p-r!2, ^  dihptmߥxH,Cr @ʘPs⦼`2)X)Op`%()hv{jR/ Ez}~@sm}>snLp4jwK5s|gPa[}P"t})W(Xn7z*(6`P oL(e!. !d, d`&dihlp,tmx4pH,rl:ШtJZجvz0HǴTnxN~&B fhqFlCxC>kgB g*jnpɁ AfB%>  pE  e o X#J4C {3jȱGЇ-̀2 \k# > >W@ J0  gҖR"Z&$G* \IDѯ`ÊKV ">H} "U("X߿ \Z$E[3#B39k̹K$JP9}uA T ۸s=5k9X׭짩 xdУKNjاEm}]i-]; VԽ`yw%^rg&`$i@]i>p5W v HDi!4~]3^U3tӆ:t!h8nV>T@>@pcDBRlQE9ViXvxGa W7  p)g_ixIVz矀gj衝 袌6>*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸檫ePs/i8k/e׀Vk lA׆+n&kƀ櫯AeC@$ IDRkľ 7<*ҠEN>kCf }RS,AD<ÑblJMt f#l%3;C X9D^-2-vܬҀ ar I$A T qLd20$JB$5cmS m94 mLpZIV6x'DBD-MeTjw^̂̂dX"(2 z.ujT7HИrGGy38WHC&B"S'<0*EP t]`Mo?XP3!<]L6w:, hoAh D3(ఠv; W(ѱ0̍ cH(6̡3@ H"HL&:PH*ZXlἶ8X>H2hL6pH:x̣>E[ #E?L"F:򑐌#pL`y(9vR (GIRL*W @$ 8ƅ̥.w^ &bR` z,UK86S\ȰPh*f13MM&qoVTPHG@B4l,!0.%%C˅԰~ @ hL,qn =:CCM)HDd94?r yIWҖqLgzWP&JG&2gB#I` *Q\STͪV:5E!_$nT=ۉɯԂL>8Nij;(PD%Y#Jվ ` u"H9@c‹:5H@$ @-OpA&b! L`gKawyU@ "`Zvq&IjmŊL" IIm]kixK^]t_K\ U&Cv,L= BE YlpαMҒ=z݀8``;@q!G8$%O;{*F*p`sY8αw\l<(b=q^yFEȮ#9%t.{`nkʂ>zW+θy^=k9\h( 3ؼL~ib֪Ap)^Vx\+I :ތ4BLMJA_ZT6gM=N!5ݠdyԹ!]2{u&& i6i G_&tKLݣ86<6Ԟ3xaEDLVR7{0UOSϸɘEm GN(OW0gNϹw@ЇNHO;P eQϺ ^Mc;kd(duT2lbЖh q|{hZk}NQ\O˚A\RYo\z{~Թ8@#Mwa oVϸx`<_/wͷjg@G}|$/c}ySam{#,u 6n| :+nՓ'q8_U=u:ɳ9!BePEgٰ$mb.A $CpoE~>mAEWKbygQH]{S$?h ee!;NwhNq ,Xb B?h,BЄM">cࠆʂ_Ç7gG2.Cp;*|B 'Hhe0`peFFfͧ0%|f&3?6:#!8N1>1!m{ aі{8!JdE9  `4t$ZHc59E ESw=iXbJ%!%0@px.HReqq<3? 1 o# b%}pIgS$u5QرoTQ8D5'G$+pAPoǧkvDZ&؉ $ŏHۑW]yzQ-}@*iG e b-}p9jiurpcQ3@jb,: ᒐ>oЃ68 l)$P;'ɃoKXeF*NB,>va2KHApgӗ~!#gT_XXi QWz)G@^BrQO/a&v tk1%bFЏwgh0\8[ɔىEIgh\=GgŹp0Ȍqp-vAQIZyZQYeF)*ZqJl;*=;AC[bu$,!$Ǜ*bvȠeҙuiJ2a3Bqzv K'# QT:"f{Hjy)q5i ¢Vb4IR+botDPw"5A⑌(SagF1*([| Eep>mJ X~x8zݱ@̚ Tc" )V~Y;niBp,Z GF<SԺGBٮ\dD2VAAp@DxԆ]% "E%zYiXX($ȱm;a؋abMbJm̰OS? ]c+؄"Q?Vnn`KzXWs$l;Mа 짮ЂDMrK8/rۀs&ӠP5vof`g,fm$J7h悰 ۺ;[{ۻ;[{țʻ yԪҚaP`qѼK y&E{vLPF T-Bj`IJ FWiKnGH+JBjJ$K3ݛF }T YJBZa;TFF۔4'AMbeqC%$hp{SNZzLOT`-$ǀo;^#0)Ht ?*L$]7[M$\BLVLGPٟM$MᶈpRՕgA ,ֶƦ$m㙂r922MblwQ>Q+s \Q8nY{ɘ|r05S3E_E?j)pYg,B{RT@$Q]qǾp$nx;Ǧb21p> D7F3p)&8q)hMH7aZ|w_7Afyh=Z k,3`fG>Wa EG D\SCVFX6Y %òfZ]{}ry0RUQ(C qHmU,V 4]#7f^E\ m#fyQ?x]ߔ7F[0_LFBWQ ]5^GOڲ{豚6jMqa|FFe^th<6$gpdBʭkrQ0ܵ`~p i޾r KGR{Wf k~,kG`Y /W̝ +cw?: ÇO "?$_&(*,. r0Epv8~mZ?UAqR`"dVT5/]S_J 7p2Na01ZLo{7Na&-i>K[Ӷ^v|QJS%W$^ZQCB=?l(b;U^ [PXvT1%X= \eǧth%?L~Iqb{E'Ԡsks^1\ģgz&.(g~:a".({i"Q"IDZ0'RQq8o(1;0C`4!H$r:C1xX8詪1pKEMT`@G^T@.{0 5  $RNRVZ^b! ؽ YD(ed dR #'+/'(8!% Y> 15 t9<=ʋJ}0]oc<ސM H yeYBASC0PŋAV Ȗ.+!A` H@*$bD便^D2VX0EB*u*ժVbJ $3] tqŁv@`:Ev+5 Z3=f{;.:/S_=S QI]cXJ\XP½zN 9i+Ehyέ/J# @{^^? i-YE+wLދiz@P]jWH6l U8&E kH +1v*eLSrⷧp9rP Qf\bNc 6ygj׽z#(WPX\X;Jc Z! 0ύzdaZ= CvHLMBHihvQ#Mķ! t?)z" x1 3@EBnCC_@ȣۘR8φ\fY9fYW#hAͪP{Z$@Q@ sgN toZyRb@OڈˑxpkXY"(E9KUKW"gf֗oG@=_!f), ,RgE"NC,2=ŸV,/)r2ĈJ*mz  RJDQV03j5,SHHܡ \Hʕ -3$~*  P,R8DYND O2Qm0&a)BuIh;A=A%\Q.F*) lt 'nԔ T7k].s:XsFh 3*4h_N4U(mRnK2THys=Ϗ-/&v%Unq6c2WuI2H^ZHF1Z+`@vV  J,-%84G3U=dIt;~b4"R8IwiqC8B {DhY\Id=rml;9z3?:Ђ3 mC#:ъ^4GC:Ғ4+mKc:Ӛ4;OH5KmS:ժ^5[W:ֲ5km[:׺5{_;6mc#;^6gC;Ҟ6mkc;6o;7ms;^7w;򞷍€w_V.?XĘ|e,W#gi0r193rQ@d9~J,ob)fE~o+:=E Λ;O{xn+:Ck\#Ap 0Nt$* c0QOf̀U 7Nsf׊vaw`[WDLMHmCFt4?Uh?~sOto۰qlZ!Mߞ|] lG8KB@8*SyTu :?OX% AVB)(XAT!ZoB f\d * H m" ]dEQiQdX+Q8{PaG@؎ 8 2XխkiR|mmb-i*4UJ #K"(@'(\-X "b)8lK/N8J AĨ.8X1#BOQJ K Ea7r/ۅ 0HcZ@ف#&IP@Y"llbd.t]S՗ )<Jy Ƭ($-d^Fij Q-%KCmKu-& df:֐66^fJS틿 蚏n1a\ +S,+ȴ&'A* LhcjZ;`RMUIPhgU4h*L@ |eA SB>S@`JFap)322fUI%!ᅭF  _'i3$h_r,D(@AncZ69ftʙ:r%lT $#ֶK`W5zA٭Uxh XS9#t2=3 $!}N) ?/;bE 2 A/)@'@?E}sb]坯$0n>aqm' \L6M_8 *tt NEtO޺@$ ݛ¥b MRĜ%,ꝡ:L2u+8֒3_2DFg&PG.61(溋2X3vcO8XGve[eGv;@6f{gvW3oiK>qijvkkB<fhmvn_Snwp pwqq#wr+r3ws;sCwtKtSwu[ucwvkvsww{wwxxwyywzzZ{w|ӑY#~w/F#H/  @r9~'x[Ba1mxx0h'4/SE&ZEhx:G**A[mc"_C8'#y+9S I0hvw$͑ O{8/9Frg4@wy8 2dꂸL6O(D9X﷠B[ۑg9H c:|oʴ)#gx ҇@/:~Y/zzW7(3 p4PI97sWz+;tQK /{k;ry\~@Ѓ'mdal{{p7!B} Khv#=Tƫ{g븐bvTߝ0}ȐGÿ;;"Gl.pS@|ȋXw.$ȣ|ʇvɫ|c|.|w}C|<.}#}+3};C}KS}[c}ks}{׃5 Ӝbdvo\ػ}=Lj!E}%PH֕QõP](j',O`3">O0 ƀ.=DサS:uo =&(VJSb/$A003us>R 4Is4(Gۃ>~_B'7C " ) L\̶Hx>xHs8ǁ{ƾq`__0=$^V_\m'?dH扦ʶ L->Bq0% JD@}  B!:xA: du 50ז Tdz +;K K@ҙW˫b `$pp`eՕ5`<UЇDv@Ǡ-dE N2TG_MٴP;) p,') [Wc YcPy@4kڼ3ΝJ[5cNJi }?u`Fs*[ $@jsN6x $J*s\YyTTjќ%^I&X8kеgc:/   @mu AEkIA~F>!moN(uE7-r] 8RI{ȹ!;8Hv(x 1haESHCy=./#OP;sG ҟJ&&YAl`07TA?8hjB^u,+|U["@=P j{*uOu,jq\ d."Nh_hHXBb=誃([ 71P0t&s.BL3 kb4dT倦X䅒!N (b,)O !, fEe1ŜaVHAh)%b#@J(@B @9UB!vLI;h y0 0DJ(:D&5D?&< @@d 5q(c RPhbN1^Ä0‘qc 4nNKkiqr\ Y-%R@ZzĖ"<&\z'|.`ہI )OoZ6`@0P=PB@ ׸2u.劂 c @P*`ˁ;~Pd͘eJ~"6620Jk+p'qo#$J1Afs\f)q8ܿmsiZ&X["̸j@&z25iE]kHgUM&Yr`'CЀcg\pR- K=ۑBpMzȐrL#VN}1U$qAq VS9whhJ]4L<~c@xD.|+yS2 (Kydd*ky\[t03&_.ό4Blf"g 8ytMD0wewbH/0'`am TL6q(B +Um ʖ )11pRݺ{:*qM ^j'0;8RD#:B}/pr^OsN^ëXK;Ro<|a|+4V {wC/-oǨy$&bE};rϠZgTUQJ匄a,5SsТ9bUKU v.Y1Qe C2lإ ;jѽc.4sA 2"0YhX$G:8{,"a/Y x7M*Y8oMf@\zoC^ 58s/^w1G3vZm2[Ў˿Y%[4y׃?`րaIARccEŢ[@&g'h(i)ġ"dYcl^%-n.ojɪFE3t4u5 X6w8y9oz;|<= r>},hPڀm( Ĉ'R8JDp#Ȑ"GVi@2LI2i&Μ:w'РB-j(ҤJ2m)ԨRRjˬZr+ذbǒ-K*ڴjײm-ܸrҭk.޼zm/.l@#n1djGl2fOg3,2k4ԪWn5زgӮm6ܺw7уg/n8ʗ3o9ҧSG-qڷsqoǓ/o<׳oO{?? } x * :`t!Zx!jaz!!8"%("-"1(2x#9"A 9$E9$M:$b(JUZy%Y8e}]z%a\9&e%'Z&m) |Duy'yt' :WhzB(*蛆v(J:iy)&e&z)nȩzM"** +zksR+kJ*{,*R;kubectx-0.11.0/img/kubectx-interactive.gif000066400000000000000000002424641516137246200205040ustar00rootroot00000000000000GIF89aXkkksss;;<"#$000[[\KLL789|||ccd678CCDSTT+,-/0134,GCNΪ>Yl N"TG!:y6.2ׯrrs_xDPW~Ψp\uf! NETSCAPE2.0!(,X0I8ͻ`(dihlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~/I%ƾΙɭրѫgٹL5 oLȰCQ=Hb3j#Ǐ C(ɓ_H\ɲ.cʜ&͛8sϟ"fJ <*]z3)ӧPO:JjƩVjEuׯKvزhn;elʝ+.ݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.!5,D! , m @pH,ȤrlШtZXt+z`&w.cynz<yqzQg&$#kPffa^U  XMq'!%! BnN͡k% b˹ͳm K"s'CU`rK>LԆ3Z bgPYgƂ] i$IE&OIr˖$T)! ,j   dh,h+ðүH o%gÓ/p%RLD)xB 2Y5,+&DL4)xsvq'X}kCfo.*j`e L7N^+u\3$ }r'[J)h" obrF>cmI:O!!, ,  di@p,+e=(0A԰%2" JU:bTnכEfQ%Z*&L ᣃWY$*"z ^#g #*%)d$_%TeV v ~a$" CR91x0FFĞ̑G' K E#Gvy:׮k'nb l 9O†܆@tm!,   dh,h+ðүn1'P FB ,%t4Qđth,֪gGukx }[wmrYz@CeFt[H!! ,   $h쨾h°ү]n⠙O$@a<&&ɼ"b`)0tT)/mu U;4wv4x?~urkB'$ ( fYz$b'\^>.>:6^!!,   $h쨾i´Ұ]n1Q1, 0KQ*iH0q!Es^ ǩP'(?vl';Y$H~V\}@$df e<#`Xv_b~ $Q>bIF:D!!, @H0I8݀`ȍdihlp,tmx|pSȤR[:)ZI*vNiްxLВ9n:~om~d]Vj`X)$LDſȥ3B90 pU*4pC JqE/jq9G? *Ѣ"Uti!, ;@pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx׏ 3kЄF.· 8"KfQI`E HLȈ@>DDF.`mgT!l$,-in?280 !,Pxȯ`@yl$1!ѳYܙ<ߝ3F@zlq;F6uv/0 *8Em0GZ}y:(VoF\vw] #8 ȇp8:i^lnZ*ٜEbCLV`<(fí*'vAJp2l*;m<-&7mZ-Nn5{R,X;ͺV̎ 0@l!, @pH$ Ȥrl:ШtJZجvzxL.znpeq>~ott|sF\ud vnÝk h qBe _ R GNk [0s \f y!P0  ށ cr%LBg P2f8IfrLE ͒NVT`.\jUi8uI5--7雞R`ǫpzEFMγ^bW2pT&*A- !y|L#(F p82 `+` }`Ä\.+2k0Vۯ2"H d'h ,²)X6 /YA^D`7~Xs^r0Ju;&E:݅[]AQI9څ!EU}nKmm&v"I !BW#~$Anzts.vϐ6"AUYqGÍVa%ydhepY]T\&v9Iy@0y*'I?bUė 4(^I_b'axN-SRގe Zu*hvp\fAN (`W$*[ v[E!(u"_m! iW92?>W_}fered. ]X١Zf &(x e˺HV#m1˯\LaNq1KfBʞ"J12t>sŽ"섃#pX&fI:phĹ)EUC%9~N7.;{8E7uq3YaxϐGۀuNZܙ1>Ccx'65#qyuy+'¡FEF<:˹\@ꁝ*wHwI8!mWDDVKn~ACArE<ѕc"W.3s'1<_2(f >NzY NӲ5;f5-ejܬw:(|*uQHCP4v(=a}kwD10Qt䷱0w'" MOl H2B 8E VƂQA) U}_ )R6|⺲qaktql",h uE0nÅ/i#n6ӡn@(o_\㐒w7 Ytb.IG=)l@SVD2mHno}QIibFNBj^t\ulJQwn2d&(Kdc,QG:iS[?i^FadXtSQd\ne:&̈́޲oa Rj%Olb-%IOxҺQEϏJaVH:N OM4K쳖 u&7 P|)f(MhR .,ͪA"ʄ<$qd R\Cx0:9)] 9䘝rPUcgYYB% q4!& jdif2-=Ԥ(T"ShJ *y Zb r{RۭRNo4u3HpS@8B*BsUu HOءؘГ4.9֮ LjwFEn{[FI}ahl6&3Y\z3:"p 1>s65l=W~4ʴ 93alyxkA4Ƚj=t Aα692Z=}y/8pP8y- xA 4uKdw/m0:C? GqQA\>=A l o\X7X.t\1t*` T ꐰD]R..jTH$1B0l5*м&}fߨ̱~Dc+2Ldi/ kjP:Gc?ef (\& NG =ccGnB3@Fp#ƫ19MH+?}rwq4!gD~o"w 0<y@ksza"&;"GQ+:۵Z"\$Uf&B]RA"$']z`B;8qK4-4zf䁾_B>"L0%KbKM8 XaǀVT#o,)2`BVWHXH-oVVux&&-%L8o-[eX)ZY wB$U3Y%hM;6(M%{"2x7 eHA_UR.w`j27l:n .>~qm "^&k$~*⨖,0Ni.NO5hܖ`ΌfА#o6^](y68upQpLNH&Yʵ!}L_Efa6XMt>[H^ZPZ+ j^^x np0Pg^Pw懠Y >TN rvrUQ؋xgj,чiW5mG,ԷxS]xԜuFbWôyB>'KUM׺^O^9&Ióϐ]z=';}f %=NM7FMAdD1'^~ͧK~S=Bem7W.>@, !B2ċedb $(\#,!? r {ζc,}o[D8:8|5K.\[jFڛڄJŪ}4Nno҇.n2]F% =pcj$%VU'.n_g"/{-X/ 10fUˏK ȚhN* 9Zo?E)x ŕgÓ&(\/XH,{T_Uql}Ѝ; X290 8:K7?5Y쉢)ذ&`؂1 1)g #FT&DѴ^Y~aǘ\. US4X k$PV"%""&0꜌6 8,4<.$:LF C6(,dJ,8D&vp9!;+p~oy1jqc\m /;ݸ> *5@8h0Ա٥Te3/;9oVzQR $%BԅSoYÁ.qГE,"FivH%7IgE35ia ˞k:Xe3x֛Xl">È PT}{-svN|C;zQ+v[wo`~Ij@wđH3 ?W܋; B YUғ=ALo hx}wM޽00)R*1BKP-hVg0*?0A:f@ӣqX?`T9B밉il $ n\#b;$<9#ԫLD|>>!EHU)@3Z4܃h@JaNKGεtBkTX8{3@47\Bˑ3{E;?u\&92:DET>;,`uÁLMGlcbҖPWMrZ Tî0|^Kd|6Ƨ΢\<هc~e DJ~̃Xo[-4D lN%C jʣ)Y&4#}ث k^8ӑo @oה-@r~igjppCq]׫d+E0#E\t' e :{bɯ;oÈp V pU'fٿL2;4xnG_rYЮmUe uKDNPJ,va>\4PԲrl 6%cُ3qsd] 0FNJp6 (x0?]|V$;@NZRCIR֖(JHPC4iiNjD%'ڱQA*u;9+7>2BǮ`S"jnT(z`6Icc5A(c@ZdD]V$Gj> mi$&ghv˪sEBak򅠐uMk v:$aԟZ: %nVZO-I@M&W%l%Eןu@dm}jygR}kpm%D>uj`u,ڳœPu2Sx|;L S~H (drI$!gFm01c T@.LaA@(P%{)kT_U4w !_oWxbDۭ\e+3: (cQ=dZ_! hE͋oG%;ҕ1iMo:ELd?8jK7t&SKSJQխvaMia PC-cҦM@9 V6c Q^hWdJYW({2%z|V2ŝ_׼a!`lEȺZ)pMxFhw!qO~qo5q%7Qi;-wasA3qs ;ρtkC7ёt٣Z{kGF4\q"#tbJ{Kt1udF=--Q[tG((&YMfwxyc|H -VǕxD_Eb{(4Ox~/ݖ_qGd^|%WIh/0IdW\>!^%o[bK4,Byzd̾>',Ͱr  ר 7C\YkDB/ۃ8, Sb'2 $z.MAV+P-OC.MS2jm ͉`M S'g(B;HEDBXP3 +5-)!&p3 tS3Ր* Rގ˔/Nth(F0DW"c:I0.RȎ$sUB_ ZД4T~4sl@)ġYg,Pc6 } @jWJ(.3 L4 7Ԏc dJxpN,!;R#* 'Q:PfZ2nJ,UI&sqq;Sp5QG=_#+P% VJl!swӈ\5RcgF 1lc&}DH$K*uItbHDh$e%CuK+ h\F[)b~1TQ"c  $]/&QXqUd9e(nтgV&WGKK %`RqTg qޒwglur'9}ԃ[Β 8YfdLL6*iI-CUI~[ 096DQ 1CzҪ2)3w̪"j,ng YD{ rI=Ϯ.Jzk*wb־Q+{P 6.k"+L [h+G)4[JM1쌪_u򪃏x  ѷX(k yB񑨈[Syٴt~K!>!%-1~9㡍y|=_PNnOmoY7Vn[ƙCA fu},5n'R2ۀ4Ӳ^l^7|@c xoMJ9=ݤ< kJ~Ҽ黾۾3pvW"/n۶~.[p4k;7-w< 3u$͕r /bIH\O|V*3T ^\RL#Ps_ y?HN2 A4a09|P%rpw{̾{):! 0x- yPcBa`B2JlqMX!,IA}PJmq mZM,.`d.ЂViTYBF%`8]bC6Xfl˅QW- A Aa1 X$AmRaH,tnj,`ЭCA9AS ƁЃUyLBLQxƋ֯PU+ |8$ծb/_$]Lv"Hn;DMU5G^vQȠ>~Uy&8,Zr9䂮|07x#Rqȇvttʙ~%V߆_3ֈ ρ . !3lziwoeaVaz(XV6=QE!̢@)ֱ\{rbj]Z!PFV!|A~~p:GLbjՕC[n1\xWk 'ZӶud$xQ6F; )[їUTjfѶ#jx2Un;⾚,[M x,(<7-klh3*ڂ!ApȖe! P)j(*BnL1gӘ! ѯ=cj!AX pFSԞX=-^YEgvtE^=]!A V .p_fBY H=l<0oϏ6qp6:e6-1\uN a ȻJ{"ђ?Ыg>{+-G?WsSOmV+";?2[{32b/:z ccY-m1ZTll% KrBUAWCB=DXKYDh/y }@ӎPAn@ Y69vw?ͭS:Ca&ҤhZ]gPY--|bցƇLi{hJ^ ^YDŗl\2@v '#L]t?fdžx7w,A{Eh@bgTBW047?hnAIuU ƬSV{3&I(Nj1b&\1j141)c< Dq0]vW$d F %( (^[SPO+O2%EFt[ajL2BCإG0#$3"ryF;̀ sSjZMBm.iR`l iU7AILlJJ2WUO!{jɆ<933{I#7I I34{*1[(.6NjO%:94nզlңmq&EQ;TU"*k$AGӶ&0& :փ K1vKrn@ԦKb'qXL]P°bp$ׇUJajY6n,&.Xs\MB++c ނPy/z0bš EMq?6D>l>Hl wEԭ3Ȝ 16W0fc<چ'?_?HĶ۟ƚ#Q_"`%_B5JI_QK`(` i =@C` AN HC Ѡ _i:tA"aRa億J!ZbajraVaaaa< a"aaaa b!!"bb"2b#:#B-E%U"2m>U$f E'N1dbe"GE,r]-ɝMfS:J;d"" T bAc\ܭz8"<GĭHD `mpmӅ`y=J*dGe% 䘠GOFhdCFE8]uE_Ł\ᔞ-R40P\Pʐ)lS=Lhdy6j9聥z 8Z'^AmUQHYGvRZuܖM\(v`uK,Œ,[3Tn%EV1Ĥ1|fjN9\hbU`Zqf|VLO1UOKě̤%0Qd jL}YIEHE^7G8H$JajQmj't*B,i^Vm֕~p҄`4HaPn(j@cc٪MF9no|Kh ndXƅ'pm豴 }F5QhB[ԙauExY YҦl̇ ~ x$]JH^%9cɧɵvEZlG.~~XpÄY?ޟTғK9 L%%GVIwNcļ3ΛJBTqD3bTS}= ,XMjK_EKz Ɍ٭ʪ@*| m|!e:݁LlbǦ) (h gHp&OKCY%Pl@vT1kڠ%$K&菊XG'cׁ^i `բufQHgN)lKe -[rC'`rjj8vVp\!NI|` TPMzd&0ZX{ 5bV:l=hlgU> DxWBvТ xOcή f#$ئmK$O&AZ ̕YS5-ĵ=,PP'WئnȒ: *iH%Q&l)n0^9[ҭ7%I-e U,ƕ !M*grﺹ_>l *e]V.z*cqc@w]]Y/d9ҿ,=6 5'n>窻"+N/P} CCT ZXȬg"pobLa,bЩp|l lTI5]*$M0:,ElFI긾-bM ZqvUNQ%f#BՕS>o\@I-'&?"1JުC.Ɠc~qknt8$kzlL,O .o$$20smr o52Y(`qKP-˔[]t WdYjFwy xxa*>wf)2*P'Dcan<ëGN!sG;Tk63/.㒘YԜbŊm 6[ K\Y}h^-74=I :/qL%M+褖>95W mYYuۓ4fh SZ@}xf*s0_t"ȢX&hQKiYZy$ 'q ,<xʮIP2pqs g 1f$5ìqţb]) ʿe6h[$v]v҂z] ɴg}'^6m.Szv,,i6uįBb{4ɴt?E6[]-p\PLc:QvLdZRz3Q9AQvz]1gT %'$ cBHqrwYhEtN}%44~V8|w~ tJxGS]QxL`ԕ C(vGweߔ]C54Vt׸,֤xgL[SL>+;J|Ӗ'p[3@c'fnj.k ydoW{ⶸQ]H /b*ERhlskX 8mƪfVz5lUK:X(4*dXSxbZh8@dSó$-rx+K$BLH,*" ܢ0r]-;3@'/7sHW_P'_lR PSB.p٣p88$Ø_ j!O]B%x֫a9ZiJB@l?# Ǡ,FNIEWQ6P?+[d,*E%uEtx$+Ʊj`'~SY{J):A݃%EON&EВZ 5(?4Ķ"c$ k6aX7P}j, '+>lޝ\Ryx7_8@im̩F"8}tWVbz9Ms6( Eؐ ŴLɞ]:鋥7kv[vu­hgyN VPוkT>e;.f)] ؖL4MI_U0΋[j~S[5"OfojU:BfߋwFTb+E cZ*HaRt=r> Z6:y好p&C0a8Zz*?q,iMvUL oNDz+Sfgڍ*~ Z4v.7qڜ MJOdcKy,@ѡÅ!p7> BҸ 4UZSD 4֪ ۸F4q9,l f+s%dst d IBE&2lq1_lЛ̀Mrߋ5DӅX"-Z2ԃؤxFGunzҥQl {I53jjuLSol0_iJ笤*NVp/ )/LN/qgMXuVy]cRH* AM*2on;iH ZsFZ(I((UF9( 9JFUF;O+ЛQQs{Ly(:.uJI?I҃b$9HiF8p!i4恮y>N>D E :ʧz5G)S6kP-1*f=7Kz43]Hn"% +_9Ow3uepȴ& HOMA@ee#CAEk6[8đV$jtKc:9;Be:b=RW'@:X#!&3ȆY17`s=wk;׻MJ`k-w0gXqƤ- O¿l_:Vg0Rǒ`y9="e |t;0! L9@ dvCf,:"Aa_&SDzE:cmPlۨ#a"5fKc ok]iDʫ]" WcãA Y`ǸQ( ڽ f[r˃hT).fXJKfz-hθğ2jCפ6oLbPΓn*r\(Cs9RUl(xZYn"l|M[ rN>apgk=X߀"$NTr&-NȺgUF)xo{qAw{J΃U$@nQAP|Jf- ?ra_-Θyp6|ӀWpyti٫N=`6})i/eYuWK+9گ+hmDO}7Yu%};jf* xG卷(*p|5ywD.yz3}e?{+ݑ0 =S߭):%|7ϧsVS!i"-`}GtCJg !$BD$T%t"t'()RDoedDMA xAPDKEkRI7 L0}BON<`4ԞlN|1BRέ,S5MƂFzI;^o==\LOM0SD%6 FlL FP5H\MRM ?,5ǼMK4I[KZpKT5yW|<=T[]WjLeV3'ֺ%j5 UYDRW]ݴitVv\قĢ]A REytZO/UH. Z%GUYgc?ӽ=T=Q}3 Ŧ%$H2VH,&ܰD]cV\ŏ5UEքdڻUKi"]Z}ŕLQ=Fқ}l[,Nku|ҦtMmS5Үdܾ4K MO Md}E`XUDe@mD]|5ԩ_R28F I^5=Xǯ[OeJ`%]͕ZcE_ u4RtɁL >HVUb%5G Tѕ#LMZDFG5☼qdTcXlՋWch9 ;cj=>#<A@?6C!NdCU1!G0AdEIdFVWM侸dJPFN6eS^dQVUCRNSMfYeU[&Z]6d\e<`4b6cFdVeffvghijklmn夛 1igr8sg;o{&Aqq}-c68}0|V6À~gvqfFt?Xg+`zp6yG Hh&6hV.`RhPpi,2隶i〔hH:&޿gf龘ijj6Fi6>&. 8gFCjjgFބhygNk[k]g뉦kVVVn+l=j+.zfm/q$Nj؞mn>l0dl l;KfVVqFp^vnms~i^@fnsx捧nFnfm~k&JAlkWlgsl nWm q 'hW q!,D!, 6@@pHHNrl:ШtJZجvzxL.zn|~GuV(--yz,-]IOųM§x.m.ޏȻYӰgCH}!02b궰v,AWa>Fh4'(y ,D3p\bW8sI_B&k%'jf >/t jV3Ut.TMu¥zpJdT)ؑO#Z,^'h i{oI4rsE zL` F \'RZFppt T A<_PpņT&tͼ 7 X٫[3pJBeֵ,[@%⯂JɎ;!~>`ݶ:ܢ>=u9M%u!t $R{սw_}a\⤟c\&AFiuKffP]@V1>S!BjHhgEbQ! |$NAGXmhb/b[Jo- ٣KgID\EWr($:PD¡.P0n5瀠jr6/ *v~aH:f 1낢J+|[ Epj~&¡ߊT&kc/:5f{ͪKspLK Iֺ4Eƫ`D`gS g6U`<)0r~ $ n9F QJ19NP"3\5|y ȍ[Q@k⑫s + m/Kc#,{[p^sq ZLDJ]6 ︪2fX2P,-xdG3q ZrklFqOG2A 1LlTS'XuP4VȠCkz*,WJ%Tf+.rioӗd2[](\;ՠp`X;ԶQ XW,fM20ĕx$7D_dgE{\o;4`?uM\ߕ"E SZ091C"YHETDj0b`$PCz 48!h0!o3 A=@zHTʆ=*p8$!EbHa].9&-IN:Wi탩|+3vxl{, I TȒqON0,e:#hI +$L%5["TL·3e }Uy(Hk>3I JFn66 J$*AtF2+BI%\8e2SPk.‘0d^-W#KKd.Ԥ Ħ>m ע0upI6[Se~iIӁ)MҲ C,ӵ!a$I%$ZeTs2KKx)akK;>'6X(o,#@-'xka*fgWЉ1Tp@bqj9遟!܏ӿ \[_ȁ`Vt}T&cO^%U8<Fl-x+Q5KN/R@0_ұ>mm[0i%}{싒,2V o``X2:6mlfx-g~!:l'|o1ep6Ρt~L6/!V>eJlJ]^XZ~}m9c}s-Ä91U{Ka!}2B^!udB撰L#Nk"z]IW@T)o: b(,F\iC+'vFA [Bb@Mq&Ü_㵦N2z˄<oMr#:> /Y}z,uycincgkcYlV;=oG6GqOP>LGme6IGE'{n c][xP3&kb+,"hL8F_[[tu6*m^k -Z_r/k@"h8}†RcM0)w8uS(\xƖ/҃(.`De4' 1qTzG+Q"&m]/8%\]rĵ!42$>!x9'8$R,uu.QC%B́j2@?A~t",F%n54 Xb ql I^%+Ci9E;"wIБrv5 hl:d_!FliLa(@/hwA 8T08hla#hf#bWGt*` Vxl) f9 ! I(YydP!򀘸$9VЖ؛ʹε]9٘ pd@9q~y@䙁 #yiU\c:R9"i9dxNEH ʜ g &zڡ J0$Z&z(*,ڢ.02:4Z6z8:<ڣ>@B:DZFzHPLڤNPR:TZVzXZJK^`b:dZfzh#ڥjڦnpr:tZoʦvz|ڧ~ xZaB9 ZZB:ZJ G#ըs3@ک^9P_ ? /zf9>*[jY<.ʫ:Ԛ*̺*z ڢ4SZz0z+J$9)`9S Pk`z0o1; K'j& G3$ӭ%;#ʕ Z:q'"GEz8s)"*\b&j+Zk*G;H k/S Y&:?k" ^˲&곇AD+&n;:;1*';k*KK[#K哶\%1J#۸&J'J{~+`bRƷ2+Z-OR [Nʳ'PH $j,:{A l[֫"`[c۽* (*ZK+ ۷s: +«E{{ЬQ۽ k ,5%#l뿪A=%LLQKl"z këV_ Ȳ<˄+ʄʛ,-ʕ,Ü8ܢ{˚ ۚ+Ɋ|R-@:]]ҧἣɼAa4ӽ̹,Z #(;Ü f}{JѪ;K(&lI/:qM =Ԍwk}8k /k՗;$\L`K `ʏݿh٠-;RAd;ɉ B$0ڎ,՚z$+lS xB촟 o;(|oաНE=zCma;8MQ8 1 Ӕ k/뷞=^NP࿺ >Mz>N^s*~n >$u4%",N02^/>6~8.5<>A! , 6@@GH,Ȥrl:;ЧtJZجvzxL.tn|Nt~\~xl(I^QDč~-.Ρ.dݜ-hr|I,Էȅ[ v@g̸m ,d DGx!BMkPrr SfB-oR10ό/E4G*Ӎb!-D%a,G ӲNx&@)$ERuiώOdﮡ{fE"P^9lռEtvaGgR/bj#3PB H4زi[(TCLH;\Xȉt /֜ /,8:EX)bIVx%R@BșSoͧ xD&0PĀMSX$@v7v]Ş{Iz֜tO,QR_ ̅v gl0: {ҵa2,'{DLl>YYIћW'#fH0P`L&%e!i x0^dZJ@U\Eh5a5=&Igf‹R)7iFuͦxZѭPW,50ɪ}gj3Hl{'4:̢d-aХD4B0)iM2zdJ@$J㗨{Ev6 y)mS<ڤj:Ce@8oG:.MT~rhH #GxzT#}/~ 2 n[ǶL.~}TeW|#JL|N|6 J F;2}FӅ=(N)RSs$еw2\kvw1DڞTZ+$<#7JN4\K6j kbmgvcի j-or-BWϮָ[L<}w+4z}K8{o&8}v!)K8hk26!|CAk@61kK\<vB G>(RK!ݒ0w "l@p "'O %HǼsHC`ŀnCܵRg)~PPA (@BS![ ~:P/NN!k-x+(E``EE ^vk9m ŁH UkT!a*2^o҂l8;@ A:dg\’_D(RBQ}DIAhVpÎ>e\nHHy"]XbpWYsIv)JQ0]̈́> 9zl8t4V֬SPM`y<4i?w*N! NM"ιbFF$Jk ~9&HiI_fH̅4IiendiHA&OQkPE-:cH-l2Z| |%//zr8 LJZ$.żY$I$ 83٢ŷlu\S@NNJ\UIyԖ(riƃ{~( =h& =?Oդ.3QסvjkPSM$B[6۟N^#-W½Onb[JAVUfPTȊD#`3b| JZ2X^nv޵[Rfw=&;5+QUOv Q\\{vcSEȕ_nh-őץ=j"=I[HBiukE%PpiH|S]\8^Q]B*wI&JpDE~LXRfmJ @Ѡ%d/Ow/wB"ҴK<|٧D|lK$t͜|xLjD(Dǒ}.˸ A˶hդK}2JH==2PZ-sg] wLqd5o &V{4&`~:~4ܬGw xvs*S&r(4"\asu,3h>:ȝJehSӜ$L?-gQ ^v_Qj[L#+g­;(-ӵ!\zO'v _칩]]m޾. foc=whHuN+a:C.Ky+5!PXnHҡWHbQGm};1"xz]x٧pg>co56 $WBD #w?|<+Riv$ `f˲VQ=x-8i ٵ>Ztͽo盒XeA#&A7|jGc.inxդcSq@pDigWҖS#>/2C3 }7JLUΧNOnN~o~Q{r=.ڮϾ~.>"]aa.sT^;O ơ<,Z"\ wcn&%mq}Юlq^8`$" 0.ϔ>L@^r=ܵ=POJ 5n22*on;> 0R/@_߸2r@w?s'2~-o.ok\c]t҂o҄@wm҉̨֜UЌL\n+-ΧE+‡XjbZ-N? .wOػ?њ 2t^>/- nadO!=ۀMr_zO ;(o[/lW9b-G!篩?>cl2PZ.?߳U(kKzϋAҕ5,7  ьʁzC\|/K\Ȩ:Ÿ?Ɍ$Uܩit^\|BmӰϭd"kX- 芞{ٸR I:m<#4+r"{U 9bRN\ñIMLη\̪@qKۼ3k% >y;>{>;0=- ħ޽@4M󹏰Ǜࡨ7o:_F F:hҕf] o/Ѧzkux0s`%$N*=R:@3;'rm^.Jzo; d+`E>cL>;]C13?(s\@ғN+4:ۋ ʏCIt$7z7?4r7z9"|CI`?;D7zv7?4O v^\^7z7ڜ{|CId3s7z 7z7mD7 =7z7z7z7z7z7z! ,)};@pH,Ȥ h:aI=>جPTఘ)-znrs{NuJprj4w\^cCiQuh BM vz Z{ĶiR֜RN O~vl*ݞiT*LIQ'Ŋ; %0D[,ɱc,jIiZ>#aB+ 50ru`X( m2k!>A 4\Av[D.h; nV7V /ۑAm f uCysqMQbwA[c= I+16 .skŋ3M;Vw)p,&4ҧw\PQ#NLKzŤ vW^C?jm_T ]pF|Av|^QW"\ hzAt YS[ɘex#4xh #O\$Sq#.MHvAEqPyE wtm`FY"Kb]e؅M@P֡[RׄR$p8ʒ]nP%lW@}0"շޘa gjWr QlO *A4Ύ{.͆Jޛcķ=ftzz~1靡;iB.Aklz0d,kC@$nC>ġM8O$;;͂;ڣ`P7M>0Glt"!TCSHb`{cA-%6 ۩ۡN|a PЃ;xݩ!.́Ġ2( GK%a3[I#=.:RbDŽ3)&SlPZ0M#Y7F,3 C=*:KRpOiVJY'3Uehnh-' *C4\\hB`xY f&boc! e"[YIu6 Zgޚ>eo,_P+#reLnw}sBcͷ.6[.|sSe/fDnH| >, p[F^9 WgebDdž -l>,1oѧc+y*mdTU/r Vj\3:@6$+3A0FOdo͘d7"8)7Z0ΰ*.-q,75sϫ+h7iwG5_­&NCj{pxt5X徛Ov AfcA.h)` wfΰGUFQM1Kһ2Ի&}dעh ?-NM "7tTܙFpf`;K:Xր=\%Cs~dWԙ @2!)%=` YV!ǔJʢeK[@Tә3EʵDz gɑ)wϤ.&v j3ʰKoF!<`(){×K#wƴڀIhQ@ DOF |"&y$B9ħ3G/<A>d4'qJNI $.KRL'OVg~.w̷EwrGeH N%N}w)H\k1MeLX@'r45U 0V'(l!q"Vda&' d1-vաeb~]53Ѓ%w[ehg%$*=˲s rmX[ca11OЗTh,qvjLxFtN((GW؋X^H| Fʸ،@G r2؍11X r9r؎HԘD@7L؏HY ~f"9Yyّ "9$Y&y(*,ْ.0294Y6y8:EΫƠXVQ|)ɩr 7BO6{P<V)@!@BD0ej ÑYMdt#%uY<&ߪE"\&y0uKva(eK Qؠ!Bm4(Ğz۹V[Ƣjǚ=;a<9dZsnF#jd/&jЈCڢK}Mذ ,2rIL, #qRJ{ٳMm+XvXާC%\Vyeږ؅gv BI ~5܀asAJe` 鲧s]F ur4^u=xVĢډ- \zvHˊe0ɤFe(9X 쇡QKPj?&:@nY T:p؉{Y_98难 q~W.~ꨞIjԥ9^~뺾>^~Ȟʾ>rDHھ>ԞEZ.ԅi\>mn^~zX,z)†ۥZZNr&}݌шkCM/Uׯ'M/H]yC9Μ)wHk!y%Bx iHZ{ހ:&E/,| i{H~}NKm "b/Z> OrL< U>RZ$.ߍ-ӑ*?:TEW$C0±{Yjj'TSQ /g9 5n6A$ȑH{r(SSM|6rsPfm40HZ̸nM|J5C"o ?5s3wP74'r.pSv 4j,jX6!b-D"vJ !`C 5fp!l )ŕ~a\6iz rq^y)hhh ȔД6$ &0B,$2T&8hW@E `%/H=t_pn˒$(gdt,p/N@8'~0ח[&F $!hHe6yQW,>ZӗlLMlQY70ߢ! S /|rXQڼ0 Khtz98/0d^UWfUӏkW_$ȋ * T W`\,J0Ȼ7(R]gL${RJeM> *ga!\ɲ`D3!v@"` .qq,\5RbY% jUyeaG>do(DW(ܮN/tC?MkT4BuT?'tlH 곮= 02-.lB?W;cҋj8A@È6 l"c,"K`i s ,>.)*-RCMKcݺ0 MĒ0ˣ{3Ȭ̩zd.F Ϥ0M++<-&E\6XJd>}t8qP!I-TyLUUAHIc 3>3pPXClM*fm(ع<<ʚ]ՂضQ_Y Xe166>Xɰ(a0> Im"Bu\A -w t@:w6kWї(ju9d\`l(F9bYirgf-Srn >Bc.^z4%au?!B4v6;ƴ Yinq!\=-i<|cO6 dAn 6R(ӥs7^l{4 NĜ(}L$Hހ߳]'v!Hxa%p(ܐbSKa`zi>>t>e39pwQS>.7j"i8#@C*(hAoTbx <C;'zaݍ6I 6 ́&DeR2(3P4ZX$BS#eDшn* T']áQɬ"NQ1J* `{B\F;; ˘pP I +-T%߱ E fC . uC!Wz( 1:n4GAR%5IQR-uKaSΔ5MqS=OTE5QT.Mu^qUuB*Sy*WT^Ui0@,ճ $Xqbw Vl{NWBN&}p!1Ϫ-[+Jϯș^8ʆlU ItP #e*/njX[֥ʵߞ9`OKYëun93pˉ=?ZCocct@Fw5F8q?քgwٰgpN^!)l6#{F)Ӄ}%ϊ_ɣ2i~fxm'M((EfT * ,Nneoj XVKjÚ/r}IN7(-d#dRC,/\Pg1 nŘ( RFpD, ѥ0 e L`P0 l"+A\FCn((Ύ!"CB p p0Va()$ȖOƨo󆱿2@^-v>0! bҒ. F-Kl.vkj5Fcx F)@ÎB-QI)53KU6 :34Es4 34Q35ULsV5a365gG6m6q^Z37y7}x6 H~39s9993:s:::3;s;;;3pa3BAK5$r髬 K`*D0'J TBQr~kCɀ3>B@uDRGkE-r\PlF-/MGWB}4JB40s-K+s+K,&.s)n"؋K+1FZ'3 t<&(Rh.iP I*h8L!3G2\w6=׈OVuchwU T5XSdRm/YO$So/d{'Uq9)C)-SC+*,ؽTOY/3IU81ɫUmkQylD//0r0ÎڤGob4D%3(}c0i?1;ȲCJ=酳A ID˴U{=a;Եo*83JZWDH};{%{CMSչ۸4[. ˨C5 5, Л6`ٻ;;Zj[<图; Ԍ[<־=+< ]c~ 4 @IU~֛ٛڧW=9%μ@]=ֳ ߳卞][Y\̞;{^V`}>E?(SĮ  |[>i}}~1_"}Y_ ~9!޿9>LՒ ;e10Vԡ1b-LKjI4n_ +ZPkp= "&".16:>BFJNRVZ^bfjnrvz~B6)1Ռ$]L1\xPU%IAauU]90`AaqQm16B:8g)D0R7],= 3& A`tS7`<1Lw 1܁"-xbiX S䜵*gxhdL쟕33̊-@ >}Qs'Ϟ>j(ѢF"MtӐQ)^; A)aUX눔,15d1#,ʒ ֍хrPqFY5Vf^&s &qh$߁9@X'TMlGbP&V8%f*gdN^K˚psI#T+"O|9@KC.}:KHv` W k$̎-%QE7渰mǰn6YQc__T\AC4D@m.lpdB'Gg]vIך]&p!iu(FA7ꁘm, CQ`zTӜSRY%!a[r)N2wlUƄŁV2XeTVgOV\;@x&gIb`-qށd gىX Vg CEwF=;* >8E)Z`ىDos<Ƥ^ VuY" ʗM c0`lf gGŜIzD0޴u)ũy5QmHjmx$r(P(I)4&n /T^zzi&0[bZ*ilwP ['+Oj NR @8j  tҾ!f׽a3QFM\pɵ|Mii ؄ gRX-Ł4BZ:nFc+b,g;jKgU<̢]-k[>Ѿ֧1gVڎ6-o{,6xq[ %I+m7:t7unb7r/!Z%jQSԵ.{7a+$ _%6*5z*70 \"xJ^0k)^+l c80;8"1Kl8*^1[821klcS;z@0'%/P%qBA8ʜ~> >{B BOCԞS"Er(#$yɡ3,e>@e ` }lqCWQ'pV2sܓ,%߫L :ӏN^UXuѢ6 FIm)K:׺vk]!: `D`Yu 8@kI'ٱsάCy]?h*"DJWiŪ00Als'@# !<<[W(+, )@( Q?\qGSsfL=% f0tNP2bND4?^oy F?4u2s XqwĽ~s3Bi9"UY`d{=:r[Pew^w;pOLppEN))BO䡄w̽|8|z'0?sQI\r p_DG獭qzAJY !Ĺ] ]!YF=Y ý_HuˍYa%_qN Rȑ_W\yక `u)a!^^a `_=S#H_>ޱݽ Nb!6 $Aa!l\š&$aµa@>ٚ2!!߾^_*B1)(d~`)ba="4"a$ }^ b 6ءY&>!=)E_=đO0$䁈& L޺/%&[.!A An^Б3~H^<^!ZJjZ&p$7mc2m] |9"!!P.<>\ Ora=eOr"?^5d /JȱCN#C c)Z/B+#TJbq3^z!"FA6VWK$aBP4bdd 8z[=OBPQ.#U.CHE JƛffUfV%`6[N#3*>1bY'RjclNG! $HcIe(N>sJI%#M֠bz#ʌ*Tdnr&mcf_\ b< *ed*mb~y g"nkgql-:effodnʜFvPtq>ac*''tRXRr$M^L4Y(org*bbf>h__t6!cJ%u"O@PCJ_?n'K:gCf Be#0b[[^"b2v'.6 BPI#J)OhPdh-f A %&dg&]$>$!0Ym:)'.sq&?eڦ)XeY)|.ߗFb&]b臦iv#Ye',>iF)<_{֩֕"٩o^b 1"#[n1\>%~a!]&!a3n.[e,h06\bp`l98:q\*bduՉٚh]Y~gfkAPRnkUN,"۹s(Dn+\eGykAk]V.Jhb,*!tβ"^B(_ҧ&6@N> ށz%JiƢ(Nm łffy&B0-IBùmũ$jܒvR»uF֬hTyiZ՚ڛҵײmM'1nnL ~nZV""o/ "2oUǥ9Roj9FfY%oZooooooop FAc<-s4jr|&Sp YYrW]He-rY`@PmjFrp LAnx) yZfj K0#1puMp5"0qуѭp1rpsql^BnV#՚4q1nhqѡZ?-[ϲ񢱊,(!`(*o&!v"(xZ\+j,.^ L@# !i`ı71#,[N(왂z[u-fzV顕n_}eNdW!Ujb3Vy]mNN3u12&-B[뉞6]boA{qDKG>$0sfD @mq(JRjE"x*!\u 1E)0q.&>k jA73 ͩ` 2 qpnNh0J%tLq]yRcN@".ҨiO5 5:U!Q׳j@CAӡTO%й RmgG;NY15f4`עAsL㪈v!]8bN&Z6j扺,_*`Fn{,/pd;@fp,!69Zp/gQOvq2Y$7 hD68ΪkwpI7r3_]ym-T6M;*@!O(عp*)cw$[S;ۣ}1hh{v xudFGi e޷:j_jNhXA%WZibWrj6o_ZP}"Fh6;'\F8 e)&$ C lw jo+wS:{w Ұѡ&vhb8Lq7Չ*y~K j;?dtXN;vzw27r[vf i݊b[Gzy%v%!hyh4b*-zC{Ǹ2xn$!Ԫ-2{e%KϤp_'a©r'iMz!Wiʂ꧐|Ż7e߯x++bn|b_م}!3:4H2rщFGwtwSjZQb"J*"7Vzn314ir609 C38>x0!uqS5={Vzƥb8He~ſ*m@ua$&B>3*\} @9^=[s)k"eus쁬sGx;[c>"оN$$A=%#AW˩u%#CS? ?22Saަ4k]ٲսm o~b@N0^Fwm2L$$18*MtXBgFKJ 3a9R&S"Mq2Hb2J"Rzt (#+  %-5=EMU]eej&^,FNV^fnv~>PjJ'/7??ݐ*CghhP@ 4B" ;a*FHE=~RH%MDRJ-]SL5męSN=}TPEETRM>UTUQ`1CkMplU @&mݾW\uśW^}X`… FXbƍ?Y2ܬ8,[fA)Vb._s3hMF RGf׼ s[O*\pM,Mf5[@͊qȉ_ǞrJgFsDcL«cq ҶF#DR.KHp,6',D2K-|$6), pK5dӖP0V;%y96Ȁw$Ai:6&{zκlU>:ÊƛⶌNBm&m_/qS9q9X?7 ޼syAG] z֣8g'7o/9 Ԯ}"܇E`r]iyWz/W{{vG[ gۇ_ !!, 5@@pH$~Ȥrl:ШtJZجvzxL.zö|N~xpqHLK H*\ȰÇUhI#3fSƾ]Hɓ(Szܧǖ0cFXъ̛8sɏ%< JѣHRIzΧ6LJJ&Vj}r+>r^ÊƱhӪ]+qGl%K7iBPҬ˷߿ LÈ+^̸-I啓3k|ϠCɹӨS^ͺװc˞M۸sͻ Nȓ+_μ[_b9 XCC(! GW@Dӫ;``!8 `@!A]I"y 4B}X`$i7C"0 AH PТ!HP`za Ƞ$9PVC c(,rb 7;6*`")$E^N[*ҥp( Y 6b@~`4GHAD4٧,&Ѐ镘݌6Hb"(4~@xoL ()Di)$Aڞ:pDHp`@舮ڙj聉J+f +~8@TF,L`. kHA DTCG(!f(#)af oAp $sH2z2uXY*`JK@/j1}vH8H] @V\:IJ+fxߌ<xvbKm 9[,0;:1L!Ihk .svE4߉1kw,hc`]z}!HhPyːKC˯/+ڔ&k,zaۛv׆+#j~$G<ѫ=whAz`!r?B¶p/88 ;ަ)4pg#_BGlq1,@H}[ (9t8')U(Rj,]Y ;V0@j^Cħ G9k?0?C3W xhViB`-_>̤?HSc4?[lc݆аF0nx#7V!&t@L#ʓA\L"h;)$عM^NZ+he Y af[%71@)'9z0!Q r0KiB #ڕZ}e 1I"А0$Ą@2/>3N'TI]yss<LR15Oc._o(Nw5CvA R%ivYS]} xb߼-Kr$mv(GJ/myBͱEr W'VL,s}]_WQPV|\VO˹ŧ%שEvf `I (B X:-`CS{5ۥ9)7| \e>5؜$~ۘuM9 %0Uz^nt"KQ0D QeAi0)0d2¼ } cWnB<,;Rdk̦U),Bz&yATj^$'K哯dRr {_q@ םaY}|,sJ6p϶GD6oU5p%JՉ!.I+QZj%׺&ΈkziLO&BznE(vq\^bp# 5݉Xc!px' ȞU̲jHWW= Ÿ5Lke9?m$%9lcPڃ^t+NblOÅp!=Gr<@7ZnSxNĥQ oc8ڄ`6;d!(Pms)=Ep&kFn чHZBcu=ALx pAA`eOГMoи: d)b~3[7S8T9t3{`^S"̥s4{{2'(H.2tTTM1_u}KUd4xC/p64 BS9Rup>elc`fIx +.b8i4nI+C9Cu6~`T9 Ƃq 6 Wq|FZ_v1rXTz%+1V'U/aE؈8Xx؉8X[9{a؊8Xx؋8XxȘʸjDȉR6%t $v Ը&iic0\b(ml&CN vgXxlh9uDf1PYn)8'م'qӂO*RS(5,GP3G"3"e~$2YvxtG>iHR0BG0D(Y[EvX*c3*9 4YKI\C$=JiPIY%r\s52%C'SO+3{&4# :[bo\ifD1FevXYke:c[KsYw3esh_r&%e{3c7#6y-FFuHlIShhyi%X5;@'e|dYRw%MXBZ+Bz'ᘥVS[: H`:OhF}V*k"cF%fNjz:xyURIgX|j)v5?ZiEX`t4_j\'nv}I#zF_*Y@at]t@4^s'6Ⱥ9%_5cc9F1͔pȬEY߇[f1S5)K ) ġCvsIy1Y`T`c\cA[Lh&NscbJ/a&,y#}K:R+W< ]Ƅk;{QvtJR$e exXGoڨyWkc55J2kF1ir*4P~:6U2mX)ڇ]vFy 7qʸY:"Z 3{ʱ?Sg pUnFU[v{Q @LfAڠJkcֵRr|iLJzoOp`ot38ss(zpF~E~Iq)Uy$Z:I˺QFtn ۬tu{@6u dD^$ <zGcGdB+z{m0\c,/1ٳi|G. <1p˻sLaJ NpT{&s{<{7L2xr |T |)Jʙ  &}&lzsҲ;c+KBOdts$YXe$9唉;ڒۇoJWUHx42&H!+ti{˕<8 K BD(.~X!+46RXbPlLB(cIh<҅W%[>]~rj]i\lhRܧ\;<\|Ȝʼ< LM@W<\||"uMq$o\mh|+@1Cb>ȕGj{(?o2)G6/y1B3^e3t$I}e|bXS5wdǐ‣mFb1(lyp+@~[:GIPmIVƈ́ӛ ̐:C 1bjpM+[`J(Y`hWsX5׳.7 'dK ?D9ӹm9IP,g4>Hʟ1ぬEe&I A`j2}d;Wl^ pnO.xڥ B`6Ѵ }kc_U gH_BDέiDs▤W&]xWv fwĮVk煿ovN\r"XS7q{HոDWyjR*oZ VgM"ZfFm[;vc4|Xc-;Wm uMr"\\exI.XzY YXgܺvݸZ\"듾~~ڦ.@ۻ7^mRҜ"{d~~_޶\yװM`FNBJ"Vžw.ڔ+¤f妻%VYʄgv[ GN U6Oa] [v-:dł8 wͮVCFg6ڮjuG1 z3EK<:љ \jlGk>⡫Yk,Թ.z=v}>zFiIEDob`oN軄2n/o;}joC9ӫE (sNޱHEg?W=}?K'z(i v,ZV 4a_bu R=$gvUϳ0'~ffrwܧyss|[Z?[7/-r@Y;za{b*2lýه<4}XNO[|+DŏX.Ʉ2BD,1|F`(7jj$ bF5E}⁕XIt8pǰ`Ȓx  Ǔ/jlJAЏssiSpHLcŤbǏi@$[#4N.RDPX_Dn~WVUYM8,C!aϼH "Fj qX&|20CypexTG1︀C2RYـh|w44D5fS"x/֬yĆ(YbXi 袝%-b%?*J`[)| $?S&ּ&RB?!е+ 2 HD9Q>!%fĒ+3_!.l V_I*io%X my)׬v '!"Wh n =IȊS*O]) }v g;گ>!lȍv!iIOҕ1iMoӝAjQc.00PjޔtvakYϚֵqk]׽] @6=v+lfC%hdpMY0mni7+( o]b4ntؾ}ߩ@nqZ˾A=n,3=,ņI퀛;oQkC<.a؀kQ@k^ M ǯqh@8Or.?9cp-81N44 1xMrab7WpC7pN}V [\M `.0 @z  c[YGyucJg+%U#ݞ߹ n<s[׾   pz} `Y_?$9#r=0o%2tZV0z*6،$hk*^,zoMn$8A.6V@/Mh2`NV-V{Fn4/ܯ+N/# 0eP|H.rTґ'Pc"/#/.(3Gӭ. S:1Wn8S&,s6Ä3#4%4#)o>1=57es6#[R7]^38?o!:y/f9C230ݓSA{=!31)pύTOe߮l@o MG.Dߤ43N2iF dŋ@Yw䉽CkE T]XxB؎{Ã#@TcqC8!.EX-Y>ÌJ8;&tF~9IdԞPŻEef {W4IPEa klh\Pt+" zCBeR^eq䤹?gJT͕V`UflIRA%bxe#cl9"9l6k] D% dz :Kk ɩx%_b tPtLwO[ cUG#F z6[O"ڣ]ڝ>, ow SGa@0ĸym9<˓W[AL w05ݳdHpⴺځN˥N\PAX=eK T@fr沜G@+RغMokl^A 1b.xq0+J޸XbECAy}Uc՛Ll¸ "G!h<"%|B)jb-1l>5~9n= "&*.26:>BFJNRVZ^bfjnr, t }* #'+/37;?CGKOSW[_cg^Ш|˓gϷX;&p4Æ 8(Q&3 >s (H`/ ˖^8Рē6Yxs'q!kܽ{wr˦Ntj*bj,8$R~6|v풨iGŵuj׫,#S~ ,x!8h0\CBx(`@¾AXq"EJx+ 6aA)@@zԂ­>] `=!~ ^:d̽sz`5 GViyBq_OcmM-)`=dUwvڣYvlTpnCry5iSFظ-&:i$!ȱwS"=hWOC{ qTiBPS^@<ԖfsYD7Qo T!F$(ik6ǀ Yl M){ >0zJ^]Zduvw;.Ro Gt𦔖&Eg}Uezʗ&:cc&r1N}pPc<hhc鎘xZUnn*GyoRR6 } 90U (b 0ۦI0:mb+l `;H@ !ZԉN|WߕndF y.C2N0>J6A_9tQƜ:*ߩAFY@Cn8" &sΌ]oG7F j:7 -EAEpt[_m7?'ثo"g˦fʲs=S:?#^{|.=>Hs\ʒMo7CYz;swդWƲ^?&c{Դoedh]a)+`trF8^:8%" 'lw`e‡Lu$2H;ض>d|D 7.@9e0!zN4"f0  /%+k~+9:<0hĤC(jwo#gX $G 58 PZ# ^jF";|Oj Z辮dkS#)!F,csHG/gCD9AeG"Yb&Ʉ`{ }~-jU{taNxMXb"5-l] tN6k&P 'R^FC$tHx*  :IlȘ☗ˉQNB`6YҼx€ڪ 6O]JKr LCPT9xb.|pӴBFAН8[HXP|Ks`{sY!j~9@q Xf3W 4 cAߜ&J7.+?kb7.w7/yk7]0f%9 7/80 l#8 ^0C80+l.\0z0A5܀8Ƅ(P:q1B09JCFp%C9` "+c 0u/?A,p28l3=GیTn9YX(Oe3*ӸЊqumMkX:KѨa_%^߲n'nKCk={Ρ!{<tN2M-c^ˆ(1 -/xd> ~qV=g,ނfh;Bm?>o㟄_>C?Voc?G>??VO?_??O??O` "`*:!", @@p!Ȥrl:ШtJZ9׬vz߰xL.zƶ|N~]jw)j+5c-p)i)p-fNY**M4vʼ^bf IֱTFmV*6H ]^l%HmsA.a?$rz"(գj.BDQvʏ\zhF#fSK#(!,l͋0mٗϑC/HIF-Hb$RݰQfrA*$*l]B 6\B#v Y ˘gS%-(‚^ɠ[t [4_3/0Ry4t'lh-aĤq75`=6j j' #n!_DբXVlPs9)enVlMpuuu AAqpbR&{#QK[uJ I V{G\D9¨櫯rə&Wh,ӎC%4%¼ކ! vKN{׆n'sn+.[:uC*&|45Z::3Sl%ZtR1]=;ԺJ((Hr ŏFE̓ob,%R!G^Mmc\j=3ਣ_u3f`@VOu~5 \S|'3I+o\ye/lbo|{η;3Wĺ]ш̒6:J(xsX>+iC56[uȹ]2H/XqSp&NY›R[ ^E)otб7N+xQLTD&LK UB,ƕR>t@Z}DXH![3'CT氫0+,ii"Z OU"¢t&.5 )(5:i BXs{ٚr+ P MDk z)/~3A@UCTu^mU`dӪ?x+@'׏x\m@~~;`z3~>0ͷtاr^1xq#aHrxPsr2'|F]8`3{/H]r*P'uby'W{,wskg·8#7=D}"9r÷3H!$Hst&ׁ_rrPX|M(rƄ^0օ~WsF(s&@#p_~UQr@XPph.'S"/G 'xhU'hw@ .H zHyr57h,xOx%iЇvx%)njsE1V}"$z7snF0ghሀq׆'{(sRP0|UXm@g"ׅ'@uHrHsI(r#Ȏh+Wy'ghrhy؇THr;.$gi1ɏ0G_hXCY( rq(Ǝp{1RH ȇ˜p9V)Ua$y(h&w㘕r8{hx'W'{4{+%Gp86`4IGؘr0XwkYr]+هLr?s)'ɘNh"rʉ|}sYy׀1 kIMzYr晜iꩌɚ>h&-癇(83P.'רrqViI9s037Ki(7mrgPɀ&B-whɆ*oIyzy/8ͧ%ʄI6ti+)H/GwɃHzrٙ88r0#"͑ǙJ)/:E iW*sݙr';J~jɤB)xx-r4)wzK8gwyx﷡D LDYE }imh1礚ɊP-UZSrXŇ٨`ڍP%Wzb#zr-MIr*r?ګj6*:Zj: trf:a-*uhlxZ{bI5X'_)ʁ ڨ@9Jũr*tڦ dzHH\:z٠غ9l`z  80KQ䊠J:O}Y|Xʬ6]ڰAٛ,)jٜFsʩ3 gZ&fkٲ+GiK0g[Zty*:r* 劶Hyru"ǴcęYJq ^ʸr%5|~rh͑sV{rZ%;u)nrcrB;5Z9+Yw jZ]Up(~Rɰ9)٪K=V!0)w8H[ (gr~srѫXik˿q|7ЙΙ l܉h `ƛy)D+Iڠ`siL넵h,9yX3o1;M8) 1s0Ѕ2ra1ˍ9Q9rQk|<̥詵 |sikx^r)&0Չr~2gH016%Y{lȦˊLƘK= P4:@MÞ+HJiJ.L=T]JӸyj\^};}wZ$_w ]lwְjr=9t}xZ! ", l@@pHHNrl:ШtJZجvzxL.zn|N~/C.j(})j+5c-o)i)Gp -fPZ**O4BY`fΰKӭVEnX*6JM ^ +-o4˦/9إpp ,4X N,b@ N1*nبAȍxE T6PN.p@ e4FJ9(f)=]jqJpD ,POBLdlX|sqٺ;#.Y {'lۡrsҚڗO]lMau ·E|k=W@܏9 `@hxCD\m]ye_xZ`x[- LndC q[g҉eSEANf6M3O,]e#x6Aw -V&8w(3& gZK.YT#H+ĩ N4A (g(qƉR:8yHp׉&B%7ЈM wiحi f´f=m&%0_g*gA@ #wpEd@j5岥)jKfUQ+!UHnE14L!a0Qg58e:݃OVxzet*2 4iW[}5 Q12^o[lM6-H.2PB@z% @m/d0Xf d6AAfP,-X+ ];Oq T]@3K_K-+=h#2Փa,@{t,lFqRhڠ999v+'-B-& Ay-Den54wZ 0>Ļb=#@?OP=etkI^[@(Lӥa/L(^9GL [Mxp*YN_l>) EzutmC gx@ vK0A3B,@(>Mcҋ!Inqچcm&B$$+*:L%,Du6\.vkS$`X< [SEY-".Lsx)NG( #xxapQs,1EL> CM\m1 29B>HdLtDV*.yNQ |@77's#<&E|PAx \ҵHp 3\)`lDN2dd! xE#\s,+NŞco$e&,'j2-I~׽vQC->x K*6T5c7p l\eЊYt!tꉹGA#&]$U6H: L4V2j"őGֽcKsfޚp+ l O 9C2\`aBr4'i7%z d$(hr(Qʄ `pVv#%-R1&gfWԢ0~{.?alDU #EQt&hhGEQAMLEۢ^׃G'^;.ԭđҪIR0kB6b1Õ:UП:2YM0liFc2 |ks1)Օp^ >iMe/-,vS}fs]W #*3Fg, G9*Vbg jg!PaDc P"Mӵ0?Hv1)-sg3z'frNv1.=/Yn%={#=RiU@ЇNHOҗ;PԧN[XϺַNu(p`NhOڭpN۽O{OOufր~@ϼ'Ǐ3|f@G]X'uY!@th`{zf~T7yt|0Y~d~H7zu| H~˗wM7FsAӂ? uxr'#B7a^t860gŃ4ЄgtzN|zW~V+A'+Ts~'gt?7 tW7(ut AW"dHЇG0}7XX `0Jw`0x(+X'Og@")sxAg)*t~W3(bx}RG( yu֓A-iN9*針7 ؓ@ǁX bÒۈ j iL׎Ҙoh@ǖYKIn!}ԑS 9)tV٘o),AGl tzDׄ@}4P}H{{3`;tKsّrpǏ=Mc4Y^|1VgIt g?GCFgʩttI7|(Hw@Ih`qiɊC?wO#م^tDY?J7'x'83؝9ق/0Wׂ\t Iuy5 PȜI?tI~8ʒkx *yJy8$bʣJy}CDŽDGɦ9yيB&E+tn٣; j x 4ʝXJt8Zym&G9ޗ:]Bgx~YfڃH}DXxK>FjOy:t Z:QYLzywچ;ڠZ*ut9קz $*Iu7pM0g.3h:*\Jꊮ8ʯF7ى zڃWj{׭ڐ槭J%kt0 Hɜ4*Mw(ڣQs1!˙M@YJĨ ;{5h?kt!zᙘ9]] +"Ka˨:ʜ3y 8 J{ˠ8ztRSBǵ۳)˲}uY[צj+rG'ɷZ=0LtxHJyCxPR=եg"웻KZ\W7ԩO?d]fC\HgnGԔ' י'v vrׂ=؄{؅؊v! (, ;@pH,Ȥrlsi(جvzxL.zneB4\…f#"NrR`u}VB"$#BP_| ntŇDPCDC H HPCͿ$ʛB%p0mNլH`7OH9&р<D4Gɓ(xXɲ%P<0Au1T8N K`B<IdJ|^j74Ӯ NHdCm*Y5o˷o'[pD!@aqS`HI;(C&+&TٯӨSߘÈcc&PE2٦ن= /gv]`)УKd뗹 AvKc[b5d NM޿_4'ԀY Tz啅^D9D߆vhUV RacD80RhD68c0Zu @UH+Gi)g\7Yp(Tz`ܴ@nI85)fᥒ(A&aASt)ȕ6ēxa&HO SThg*uYDg祘fu$Ďg2F%{cQ$ޙ AqracQ\Z Pi Dn HQל3nLܰ0pDV*)v+k覫+k,B4\0C 70 qgq/ q!l2#;0r4|63U8 D#tH'J7LL?-ԅMXau\w-^-6`mbEl7vp]vtS=wx7}w|w߀w IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ05]!d,, ,@@@pH,Ȥrl:ШtJZجvzxL.zn|N~I d bUlh fa WIVUiWB SkYMXLc HirݽPx!R%|$a!;aL)- mk;@ȁWZ=f$oQ0 aBY͇Hɇ˙#T^^O 1 0X bR``g~CޑhPMG|W}c]8[a1uiȀ7T(ݍvxMSuN8AsgjQu h.xi'.12+]D6kN$g?$1 z۱z_Kg|8^U1C m捲.)?8D| )Y,|j`9T̤f`={ J+cx)}z arx`ۮHxmš*)K@2 (.y<`oFYIgXˠ@.n3J<5T9XF*O#h|t0η38e(|H-&+H ]"1\5da1)fs;׫2b< 'Y@-,Re֣WKFuV{5VۈD<.p O6f|DDd[K>H &3L-L6`A}MΑʚ¶')9L/,2F Q=vU5PBrKA@)ΰ"rת+t#%?zZʖ=)dFS H?(}^? :-s`v’Gm @a@g }ȥ='hfoPlGQUFC'ý(y ` ėLd0\;̈f12"RտT')=}Vʓ鲖 RAPaΆA,)ѣHuMʴӧ BJ*UVjݚ+ׯ`z*ٳɢ]˶m:nʝ \x޿+Ñ#^̸1 Ŏ#K2˘3{M8M4ѦS^"ְcˮQ۸s੻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JMъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHM*)!,D!,D!+, ^  dihptmߥxH,ÖȤ!eͨ9Z}TJ0AźDΓu Xp:^|:fMg{Eug@uoK=upMrNcQ5ufiR}\RY,xX}*7))݁+n8#pxK( C&B(Š! ,\  pI)8ʧYׁȑYdn3r~|@  5`<3 FcZ;dd4qA  CS ni9E77^vRz|6 t~ sSr~*3,Nu&"*O!,j  pI+8ʩX'ؑɡj;<З-~ @)\ ?ZpX<&aK@ꓵ$d@P*byM-K ;!  PBZmpz ,H  q2.*&T*vx.~8!,y  pI)8ʧYׁȑYt("ڰc^FDc0<$ Xs` hjkje͈Њ2+&60d#nŢ0 a2, g/<+ʎ @0|X##j/.@*4њ%5sK"O.2GJ/N\B7X B7ltO8;Tදݡ/Cm<)2@L8V9s .%jb2MrT$3xHJ D"*Qe(OX5wja ]KpfF`0ieK[;7SeVjC VˤAm}QXy#pl >QP'xlrt NadF9S7D-ʒ+涸SRED= b X;z-w^ 0IbBid"tg`:hFtAH6!mZF4F p"A';8|" oZ9 X-}ch2!Vh Z'TU$" P*U+y`;SF'RntY(^Ĩ`0Qwvku`n(4ƿ@U4 ׀9PbڋyGIȐkC 5CPZdعhX5?i䣄̐PB߽'(R Xxm"oT߶Dg]Eu;7F/o2@PHe^TU^˳`?ҊUڰSSc)0+C_O*(UV.DԮwenԱ8M1cL6C*Z%,S`"WxU{8Q2/C&*chyK {F.z$Q|]l@M>Qb ^aW1Q^-WJ[E& H3i~.WP4iSB^V W)[-} H$3f+gOXJ戰x:mѨniAGeXTLb<+͂B*3!xH!MoJ}HST<76ot"SvzrJj͕e$*m[@|NlcYhMrDz3}8u݆sw]"y ;}Ӏ~> p %~8cg3s B.ѻ8A򕏼.79c6٭|?GyK>t!!, ^  dihptmߥxH,Cr9@ʘPs⦼`2!X)O0`()hzUpq|5yE{~e@sm~>snLp4jL5sdgPa[~Pl)W(~={)n7)}X- A(.IPB\p"0!! ,\ pI)8ʧYׁȑYdn3r~|@  5`<3 FcZ;dd4qA  CS ni9E77^vRz|6 t~ sSr~*3,Nu&"*O!,j pI+8ʩX'ؑɡj;<З-~ @)\ ?ZpX<&aK@ꓵ$d@P*byM-K ;!  PBZmpz ,H  q2.*&T*vx.~8!,y pI)8ʧYׁȑYt("ڰc^FDc0<$ Xs` 6>9?SȊ  TzO:nH( ԸW!,pI8ͻ`(0$ ָ4x \>l1\/ЬZجv%S@p\ 4};fsxOQ8.T %'1&r-q u?A#z7[&()=x~{PR:ْ׾sX7:S:h ct $xLP!@0  lTFP{@ P` ,hx ' P:0qxre˗U`p7 5A e<8a T; ,@*ԖU Pb4RĘ 6& ٠@R;JN4E {Mr Bp&tDb 88 ӌ&@1KR\'y \P. Y) 4 P 0L0E`)"]@j@Tӂ\n1 m(Dg&#S<(mr)Qpf Wz&Lst  %kQ'd1&TAoe>UUu@G(m@Z`Љ D! 7  {ޅ=<]T2 :EmvD ^`\iOm0FͼKǏU_;zs*P>nD7qAaa@> 3ZP@\/ bE*-4\ jOU*+^(@0xpݍDD \ؖMcXA0YAPu"F+lg1!me \?3zRЛ&0Q W1dѬ c?F1W|ɱv.W3YJzX8~5Ӊ(&7ٚP񓩳K\)__M&$5P iEn&Ws~=hf2XNAIb~MB>0'EbE;ѐG#-@RҘδ.&iWB GMjpԷ!,@pI8ͻ`(^ihlp,tm8;bpH,H!ol:ШtJnnē?Ͽ|V&XgS4lQbc(߅>tQ a@7 mEE1m ƕG8XLid8v I[(R#9 $TTl2 (-p~:v% bA"BF iE$Wq()i5嗀40PmurS*@ vȁt)R@hX(詨FgҪ*/j뭙Ԋ뮼kůkA$#VkUd+.ܖŸ覫n+Ӿk/c" @듊+o 4pg 3k@ELi8wQ Eo "_DQGtKkw/]ͅǀ6p@:}.L•@\@P0Ifp/ta6>0< w: $$0i*LG5Ѝr6qPl}K`5m!0 i4@`-L>bXt2~@:D(=ZL)h A 8]C1ˆ!#`QFrOT )#2bN4^ր(r-R( q#!BJ'&hxZG#>ű0UT7G|4:#&,$O+oߙE+v di@"9 XO dQ$($ RE0;6݁@T3GFlICy|ؙӄ,=!Ylq&5i&: yD ƼD4 Q8)b0'FXwr(u_$p:nӦ(AG<pp-1;TG|WSv"y1Ptë%9pi]VD9@yLf1+r/"@Y 6dLg{0FRw`c;Hv2CAEJr\B\A:X$-:%I L: SN$Ѧ i]zX,DG2h 4"О.* Ug6 r}&ȁ#<cX6s ׺e7 FCUn\~Ѥ9Ejg8\dE+}6JMļ@ЋN/FB.> #2C#n YRSD2궭,YB)ntwo(TOӣ.D}bN\v I&x ?&'ad/~z9mMsK{T׾vK$BhU*(aS/$.mœ"e!YYv4TdRSŊ@lr2E#? ea{ L ࠢ CstwRG)!s:ޒ uns"%xHs9:Z{7΄vnQ\3UʴH4S>6B7VQ~( Rq.Pp:jxdeQmZČr"  GٹR>6C@<0;Bk[ o*RcM[@&ANLY!G.vlMZs7r.{=_'S =wGGlo;q++bN<0й1"nlj r 8$BeRg-Wl3@ 5I}R2fo6uZ^uil{le>:*R->*DWbp" bJlObuqNLn->SJ7V6K(@BObDo4U"_Q7`$+ Ω6#uiLρ9GRwځ#oeo O&$02Q$bg42lX<P /ʖnq3'Db1&RX*&v>vTD,XzГRcX/gw9疇礗n騇*aǞK.yiP/| qR i 8 ^Zz3<ċp5 X} @@|5tЗBa|v4@ ; F8[> )UwLWXo,2@P3 ׂ‚D `H jHs"Dra /T(P6mRdataxQÎD >U_kc5t6d8hQ@G aˤb.Ҕ  Y@^w^'8LIFrRZB u{>G-b*GlxHĔ{qq.v}8 ) <;#q6 AWJU|ɺPB#M113 `Ic!gݮz5l#e䤇j )мMR-yFY!DaY!6H)m.tΊkdܓ:됣6(9&)փJBH#L+z ڠcҴTܱ qaRZ @GИkC2(jdрuU +Rǔ^)MM `0Y*,n,8² iނ  HJ&uET-&)^&'SbH u1duUh1,$AeQǒ2 W0 D`ѫ5usbJOH:i—ڃ*Q%"P>$afn'* ݫ Q߉,,eDFe~_޷ =mUI$>.z5Qg-u%6'ۺ*?~S' TO)W00mdxAKt^&P930c scwCHi'|&`KU1g+@,+Za &{k2%+D~zI'E%vFq>q#OVI"!M B!#pu(q)]U=M;@Ѡ] E"Th.X _ 'X _R&RIsh)3Tc7v3F?FQs `M ` taQS(HUG[WuB hW@b=$I upIIōF!-d`œxc"C4G=`n@IK2gDʥ FKĄg5ՑTT5RpykZ;iQ34]zBZ=3*Cn:`<ؘ7y:"e1^ 9xC'ٙ Ø9}b LٚCyDPy=ٛ@ <!, ; dihPp,tmx|pH,p((GȨtJZجv{ w-z.G`B{N|-v" $ ' #- b#N$ pa  u- $##`# Ӡ `ȡ G "#2$((@)`i&T|9gDc0oi睶a \矀*蠄j衈&3m+0餔V:icf馜v駠*ꨡXjꩨꪬ*무j+c뮼믷+Ē&첾Vk%DzZ''a+4$&@4uݪyk(6ŮHN3ؾ$,: s;o>ah,@okoЪf$sk62 1  .(XQC.&<ZK1 t}SuEy\@QH09+ztb@'ӵ($@ Ԙ`[p8|h# c 9"p$.>L@:=u`H^'$%@haj6 ͐! 54 ]Q2b ӡ($`"`8Au=U6Bk$|/)5F!0!E `@:IPՔBGp'+K]0,/Y8H6,7&?ŋnjoh',E!ʘbh?S,oF\ܤ _zFIAȼht5$2 9C0 4VZs@+6KTCe(G01U4kjFe/cؘvHnrbk݀*0> E;h;*]a`!({@_iU p~x\7+vє9%vN\AQjUR׶=ƫLLMrhB^o (X鹱%OF&L6PF$ )Xf\v`)&!+, q@80I8ͻ`(dihlp,tmx|pHȤrl:ШtJZجvzH#xL.z6|N۳~MyVuW{fSĖʋ~ex]nJ Xa \0Ç#cPŋRǏ86G2H S\dC0c`ɛjɳ:q tQWE*=ӧ BJbҥXA]5֮`%} Kvزh Iʶ퐵nq6>uw߿/Lˆo8Kyĕ/ky縝?!, `pI8ͻ`(dihl n,tmx|pH,Ȥrl:ШtQجvu%@4!&뮼CH C8s )paja:}K_cj mqsjj5:pl(:IV   ' ʧN 47p`;J! n=ӷ8,KX J /2m,=0d65+%IȈI7#d<I@1B.DIF #Gt "`Sf7A}u,dR`9X.`R (Eh/_47юzO@JҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTÄFjЕc9U hI8 8!ڜs)] ԂWeD@> 4hM{3nCW )9L;9VlYUMCg8x,^Ap43uif۶Y*ֳ-YDI=-*: 坒)IKE#D,FIMI쌢]x")HS*-dfw(4ގDx` k%ATZvP5Hr7VU8 4,Ԅ+?b.̵j(op gѬưd !@3򒜾Pd"[*pi7;k Quq+ R(r[_@yd TxTJdg y<D*@AZ(ǩ9&#G99Uk738)/P{\D)܌K%4k9 ȕ|V zZģD,2MY:q&Z /-C'k j .g߼0e0uGcYѦ z6D40."vKll}# G` #~/˱m%[m `a7= =L}!܄.3ִ=id0G}!^f$pЮopmj܃` IT2Wƒڞ>-jՍI) )N374l(8vy5_ʂz sQ` d$,ϝw"f g5p`z1t7oW)ɕ!qlYQh/ _Lft̋&AIу"ʌ1 ~Aꝫ1_[}g5etGJ;ߒC4n'{pUDK_[Nd]s}}À'gqAl2 i b}?ƀ q"b d0H+駂 K #V$'vAuKvsH ق**U7|3+N7?O(`%PE*Ёq{#aOFC%xA v \5?Ur#;vc% !;h^>̃;׃*N9wgE7(ЄI?fph5 <"b=Z4<dX3x A>?w3g(?6mR?3 I:*>fc|6?wTH3f32oP< b{{d1oSfXӘ+pw.`h'Fʈ+1A`@ '4PpW5?xǃ#Wtp<4BryϘ>OHW#hHdQtH}PC%G%Fwt'rH~ՓI!t!"S .Cj2sĐ? @}2v3q8sE4zi'bfg5߈ EE4}rl9 lM1 N!, wD`}&`Ndbs 3nG? 0$np{t-5D Ly)4ahb 3_>Xvs ~c bv ejnT Ue{#Wh~eͤ f1 8g.dA" c#gga"8i JL $ qaI wwjCy_И^v.dy9%\py ^d!E`Ar yА0gd%l@)\`{ kY]Az^ajrA GN8'%QeF :Z,tp*74%v嘧1|%}zyj Y,姊:.Hڨ:Zz Uz:CHJTڪ TSZjSSګRzRZQǚ*MڬЪ%z Zךzڭ Zj皮ڮzZ*گk[ { ۰+[[ {[!, ^  dihptmߥxH,Cr9@ʘPs⦼`2!X)O0`()hzUpq|5yE{~e@sm~>snLp4jL5sdgPa[~Pl)W(~={)n7)}X- A(.IPB\p"0!! ,\ pI)8ʧYׁȑYdn3r~|@  5`<3 FcZ;dd4qA  CS ni9E77^vRz|6 t~ sSr~*3,Nu&"*O!,j pI+8ʩX'ؑɡj;<З-~ @)\ ?ZpX<&aK@ꓵ$d@P*byM-K ;!  PBZmpz ,H  q2.*&T*vx.~8!,y pI)8ʧYׁȑYt("ڰc^FDc0<$ Xs` JuԪXG^֮`!~ KزhM˶ɶp Kܺxg۾s LˆANxƐ-$!,  $dihlp@]C|pH,ll:ШtJZجvzxL.h.@pgyJiO, s~B4DUH C{4>KIݓ:  :65>  = <.  s1 xIɓ(S~7t$@8 jl'r\A@9t M8IT 肇;kUG\2!+Ӫ]˶۷##@Ӣ郮Q@]ŗ`Ey:]( 5.S^ͺk0ZL>3sVyY4$l~j=#< (B3dKZUG3$Ef( ȦsOشiKO hJ ӄ.J$?. H $ze#CTB,$1u.PI'  @1D>L/.ɡx;To8DZ>X#41碌o,ʊ]#.9eX-} 8ZF:T<9 8ͮv Q vg'ʬ갓eA Aj̹v֞jSGB'[*0ӕJv}@_ 7DNiᷞB9 "j(N1)+aU-{3DU $Dۜ+Q淰/>[!?kԱ_:iºk[猗Cx#^Mj )Չ$L:eØfoDy9`wmD}@PFNDpҪKʀHg99uؓb| 㜙>ʱ+]$<'iMb뎻I i_O1o#ϣN9#wc mMk}&զt1q|LwerPEg @@HIWT.D`6ɜ޸.7/k 1θ7.;Ui ~`%GUzDb>0ybFWJ< H1Oȋ@^:3:}0o^)j,?=֣ ^%0Gk2ڂ\cd"vb~3k$.wVso4F}x@0%HJE:: m~HP>y7 `??@֧AZIFG x7U8$'?tsEu FM"␞AĞB:9JS0!I.]e42,TiQB9Ӂ R1uom`IuOHOfFD37 ;#.>宣Xj[VtAsm٩(gBS;`m,+#"'KY(kvRb򲼹s %hZI$Hh1d"eXņJ80ebC~a6frܙvPu$?9pVSNgv+_,d+|oD,x·o*JHb=p eI/'d6 E^zqHѸ0)K#kh2S%mC1;kE}CB`hfaV8JJB >Pi&t!85PphD67t 07T!Z9EǼnT[.4[Yr}7816&5/x&4`'6OSS.q+dfG"4x-55;L5,|a$F>'U. ҘbbW %vAreF .a JW?כD+s6󽂩$;S LgEѧ٘%XT&K9]\f9lN9QS]U2w/0/]I..X1}z0:{[aaIʣ|xPd7[uO њjZLEpW%>W52Ey T#CRY ] _{beÅLd&=/IQ'c|noaZ _W,ˤQf<@p{܃Ǯw){Е>E4#M1ӳ|YLņZJ!q= uMdh":Yt˙"-+ƨBshxQ:!OVwpu|瘒XB-uu# mvl"a.ٻR~ eXe}"R[:xGt=q4a1/ս7R=PS5N"ѝFzZ6c pX+3zer*+' ~'{=Z#Ԃ٨ḨBјfs.ۨt-`m$RXmQ' ɱǓAN^:UoB%:i1( BD=Q*%a xMlKY% kժY!}*363m Mb8ppXmP0W;+tQ}T.}!<ms,8P~C?Klu( \ȂY{iϡ0Lb5=y|w[Ȥqtł'"Vػu`|QYYd+Bx4,+OW3ȕCjvhӸeOwGɢ9 '>FSQ0WKh>p,di'`N$ΈGt;PEM2zFapi@nF.zh$K|ښ Ɨ㨼a>ž5Nod'^AdX[J?c84AkV@ҝ}v9"mdH/;VfoN Xof"'O\%'O/U7Z:{䢢r)o.ͳ!#|1tM %=O%?Y3O!ݫբ3Y\0qZon!N{ގ2G4Fv=x<"WWL 渷C_ml垀Łil,Kj)E#kY }FbjOg_HΝ y{nq=QK#Ƹ* zG(aWrXvIo i2r:q) Mĸȇ-A0ّgb-kv2eu `)zrZ s< JIbfjn D4>>mDi`%9Uƚ @*H񚸑fJYb_ݮ 9*$4T8%՝\g]gs'vP\r0 0@``x"DyKfF}!%6T<Fnyq0-;s kRs|nj0҉OB1ײf˚JyB0A])FJ7ް 5v"ta &l!-1;ao(PvڈADW$n&|CyI6 H"5D| ^`(b:&>zd:Jk R1'DX16<&؈FC8@x$PM@ 3_i(KVU%p@<2\JʹЫo ' lmMC|],pH 3"ZЦoDOIpQv x= L( SV0V?Ѐ$ {d`~ L$$ d{KS45X ZKCn!(W!Pu&q~9 11yR,qm@cYZg -Т8$*5rR A=\ߩA|MIx%a9Rr[K%+[WKA%-_2%/m^s rHX0ic A&3A$53Ҝ$ptpl@Ui3pMrSBEHwӘ'Q@O]F2.4Z0= 4 5׊$.@ňb4hC?q4")IKjғ4*])K[җ42)Mkjӛ4:)O{ӟ5B*Qjԣ"5J]*SԧB5R*Ujիb5Z*Wկ54b`Pl TM_)ַu㈫\m6l8w`Zms:P+5'ZO-  F$^ӰAZqrK˂NMLUmc项N)>j%u$a@q[ya)wm!7g!,s 6Yi s pcDV&OB%ޜV%vUv mdN) 2jW6xos ɊjjLI)87 PWs* r`HnAe.ǩhDm 2SXER +!g!"⎴&+.xd:0] rJLخ2C$L %;#΂iEkK_8AX]?q\lRZWy! Ź5⦯Aw@Ј_,ǃ.;jh`EAFsCmKQ7j.W9S%=8?>J2Ҡ³(H+!J lt9k>dmhN+XV+\dɱ@9V` dtPM) 'Ӆ LQUiY% ^a$zàj biwsɧEEV&m+g']f9?WN K aG'L ~mi\wȻ-ԍc;d0[7AP+p@zg4<>{Og;1 28eOG>vy ԋߠVWhEºuAk(=yLELV T[ߴ D~lNUߏ ^& Sv%ZލHA/G|߄0E`},C *AfZ݅_m]Ж\ ՜j3YL (uu) FD\D0!tx@Ó]ia,,[BFܽ ]4hMp%/UP=T#ބT)PXP R_=r ! ᲁa.b`?ap/A Aq 1N T@0I "_1Hc֢Q`#Z݄*s B:vTXI ՖBNHihD>4AE]:y.*/ 9 Y\$#C‡2 3" \{LFn F"$@58㏭JЭd&)ħ$& n"˄` Ƣ(E6 $1`e!C!ʴ;DD-ݥuXNI aE<¼ LL^"#-@%"^N)X @ xi%*&wݥμ<%`]ώmWLeP$؁)BYB@$a lf. ^V=#<㱥 bCxpp^ ֯H4[ecr<=Bx.Sxzz&Q>gHH·ܚ}f= !I6Ij. DF$nΆ_ɇ!V329N +dK>J$K' 6Qwp]dZC PƖuNw][TuDygp~ $%Lh:%|Z⁈QBN_u6cIAP&@ h.mЅ Zuq1ѡ(]aC%8@]7$ ?qF])h &8 UPʞi%@N"i= {gicņŠԫPBDʤ\إBT@C.D搅ɬI|)*(E`,M}IL @MxHQ+zA˕ !$̇uH:ŀ?zkN;=#SCMִH'4Xe}0 N̴\`q.i,huPlFYC(v'^l;ZN)dҨ=f#ŁR%P`jŎ@"yQuZjJ|m-OfZ¹O%2L)S㖩BNq7jnK㮓:T>SѮn 4AEYTwj&jTheno o"o*2o:BoJRoZbojrozoݍYҎڀmo (Q[eگ6!h_EtߢojQ]T'nA_=Qpe BOꔄBfOyV$z0V@LjiPy1ߐgM}!o0nTKA mN+rpI XtE1rq_8terLM `` 8\NS}A~"j%e߉b`Z؞j \SL>I=ئt)n @X"ݹYJձԆ݊X$o10np&Os؋/#ofB ܰ=$޲|H9nqOIRE (l)0ڝ妌N괬̮UA6{AS€']sP9-HM^.jd)׎q:̣!m HZmĚE~N N [[d,/6BܫI'2@_] :p[MW+] 61n0Ѱ򞠛MM`%,E4oRuU[UcuVkVsuW{Wg55E .u۶s:\@ƙ+Xd 54EP`tRʡt,lpIk5Iµͮ:*K&)M +[%u^sv0PGIA-0b|.v)Yl|f1(f#)9̇@ Qu,, hn9%W!`N_:`i_4T.\wJCf_0R#qIi%m1g!=uA@F"i=]ʦBaبةʰv1QsJq$mO6*[BE$r& sj倀^,35YoDze8"%TZ>H b wBnDSi|M,▸8Mx2T!TΕ~^bt[iӼvGebAc1aF60CGτg$p%ڗ&,sTc(jDI긇c*`g+:")'y󉊟m/>d{[<K@Mm]%?'LT# #Y2{^O5X{{su ,8g{{ Rbg2lm/h| Aj(~נ@ONvccsdF'㵿 ĕ5oVԮ kk.l`} ֱh1S <[#OksBRy_W d͗7{HˋƟ[O{AO׷{n"hf4u1.,A#~+weJP7J_j6l>h:G fP6&,9EK6}9UoxBQ3 .~ögGޗH`3h <П,~;C>v{Q.w@;rW=@ۚ/_KLՅ"F3^? se[K+hx6vHW 8$m“@Tǧ\Ǒх5R(V(8HXhx)9IYP@@':JZjJ&V`:$j©B֒29Pc{IL]m}4ԒԂg3K&r <2sP@%Bud " P@S<7J3,jn?l(!?SQʕĝ|fJ"[# /) mNKm^͵j~ n*ʳ➋nꮫn bLKoVq@jšH{oLp0 Rp+^u oq ryxg&r*r. 3" * tBsJ/tN? uROMuV_uZou^ vbMvfvjvn wrMwvߍwzw~ xNxx/x?yONy_yoyz袏Nz馟zꪯz뮿{:6˝6XImC{﾿—3P8/ `0); 0)!eC%/2پB}xI~}(9T0?NȦ@^r촦SimɎ +u;Rt>=W:fQĨ9s Qs.Pr=^0@ Wv pT(c*`UFr$1&zPؠYUQ4Ŵ1?a \y D|F!⧁a 9)P§9JrHG eߘ0u:%M <rs\?Q0H!-f>Zxhs2- Lke8%CK В,/Oc*pq0' BF#3O?L "g_Qlps[ȅn cG@s"F. '|C@"4 !UaxU XTh2t M􁂨ͼ0`O!< C BI"4=Ǡ 1RaL1;r,7Y@OW'4}$H$soi CT`b) P٪}xGx,&EEE.@)IOaRt+VeC2R1F@pM*bP)B a3[Yxw4`RP IfUC}ALD2Ply`q g? FhZ=3_J'&  by~ jnP<'pBbCrΦƒ¤2l˪ d"@|  OF*<p]&&_z\yA,)ן"hfc٣w,j|Nځ `h{Rp3JhC/%5:BVI=ٵWedK^_}NA !}QyWr20{[+I$\%@8ge[5![Wj7 T{\WC h>{u7F'07G)jiwp'hpp;& "clC$_5# +JU-i)FO5J>tw]4 CPXrzH X`8gH7sS}ZXBUm({T0\Gv7kJbqe%]iFbcwV KN xrNE`0#gx.7/r#H0Dwcz;xCPf w@a^FzFrSA[Cs2 o ׁRomri@g Nt' EntqJ`%FYjLԄV<HW}c[,t=\|HwJȄv2;v⊭օR 6 ~arcfj36cHFFop sHJWvߥ" v;vPfIO\DcP'!eHD#fJQ Vc&PD["s04xe c2O0HBP=4Yug+ lӓ1]Q&bi,1K\ f)R+aTH6p0U"N PD& 0N4Bk\h+4ÙIrbI9 /aO9XGnʕ"R{Y6\*_Y"8.t}` 2'U7C9#+%%y(Dr1 3p$Ob4%-mLѕ]r$!&rNՀ&n9q2MP0nBƏI(y P[HXLI iC`p=`e'@*Q!+XCCN?ah贌41*c448gP`o C{q+Bt/^/#~y j/УJ iz)caJJz6#ByʩP=[=JjB>ZFʪꪯ *Jjʫ꫿ *JjNJɪʬ *Jj׊٪ʭ Y * =z(T,F> S>m$ ?P3kKR4#?jvSԱ&A$ECʰVQA,jꔪîx*s$D@Q!+?v!H֢bYNyD9 7{@b{2#KQS&hP#LkY%hq4ʘ{GW"rK/' &[ž~;ITsb?ˌwI-C( uDID`/^ q\$C$0/} M  Y!`qHa5FKW%{T@>9 YrYt!b.&es aEdsixEcE.gyjmԼ乄YG˖|((NwP^ciQ Ƅ[ÈUpsFy7gMz#Ip*FMaz VHLЯ@ CPj@䞩kI{ZeQ5K砯/[7&D#0%,~XY靹|yj5` .M4@&p*!mZw1d'Ճ1!Vs%A:se`P_cn5f.dtN.fH:]x)\#"}5 GB[Ll_Nڳ7]>N !ۈKɆv*h"m`+{5U#܍E _Ju+*L/Wtm+*J4?Xy>2'{vyCzRkh|'GIڀC ;UrT<݉HNV5 U|mwjns3L/U0mW$+_>-J̀]?]:cP6q2/wK+_8z.ytlC O ?+K!%VLM4=oh/|EM$Њ ,DHGWv֕a|pj\ ?],sp'Ǵehj15ڝb 04# p9@.?0h[t0\2'y ,"2md0 aHTcE[^EC(`!XSAIOPPɡfДYO#@+ӜK@fjdH"s3ttm0e57xUuHɩ:gXMK 2اp?R(& „,a7 실_V*! m0"#ۏ48qfV,G &t6 0DI^:j<9n :=B5t $2l"`)Ӳv(@qV0Ċ**"Y |.dI=.3=DžE~4xfn-iC^ L&@ ` ?r% Pa x3+{*_K~ ֖o ˾Wnjl@1 ؔή&tNAZz,K1 pi)T[|9+", tZS\=8 -os*/IO|.[ ;5+\!t/'p]@E@F i2Ei t]V\xQ"{X,  /Au{ E٩Fmsacu]/=㴂m(}Bd~;h$]K ꡻]37 p4 ؞*pMf< -==0UB _J6AMfp1Lό/K,c,bӞVe9E$(h *]'rb-H!TW0U I:rjgMK+SUJrF$XtadXQF#l('pt؃I*j8ST93AI_*?m& 8mjyxՂlLĶ=E~-Q-U kD 4H7e8J tr = (s=@d  ֫Vo/[ T6PsL9W D׼7xT p ׵2ABSA[mQL",xp4E _NVqO6 RrƆ@a'H`]"51H鴌=|iA_A_[{ ޝ|P*p4He[yM,٨0n7pCm VwBB҂8_F@HW[|{ Ҿ TgtD- 1&#$y6uyc-mx^?X@9 ֙ax8E$TR@Tn3S=gk'9gR_ڂ2 Śte1l)Kݰ[e L3[n@+VH1};A qb]TvA`M#&4#gj`(\ E_ͅ"@LeUx)uY Eэ>̇$D_bLD(YȚ.d`܀}Qp ! HGA:N$ ]]A`LyWu4B0@ H.haesd5LAP_(dF$XhM6 HJ5T5`%!e|zٕ LpUeDv= [`< IOaxՉU6}ǥ W,*F/ xyY A`vZsDY % C_ H#)$`plUI%(|L8KMu"8xXePD"`\<E)F,#[ \IA $$')T\ؿ$^Bil 'E5G4i#' Ut`$#qJ Y@teHhcLV$?6 '=3# AzL@EVqn H1]d#t۬jHCŧlee>&eXV\"FaO@]^>`CVR&Y![EFI<~$ ~' ,5Yq)%3`Zi_(a\4 Z!'L&(À$J@U_ `q*`d\W}t:&&WP Z9e:D?:Sf\@mnV"f[p@f]"+~`|eD۱̅f*"YuFVHc^ddƅ\r.#|AY^MB@ &Xy:|X]enYlf`aMX:g~~NV$Z7U&[k`$I@팢Ј[ J<sDCBΨI_Xpɡߔ#ܐhOHH 8_UiKAdaaEjDJHsu%0ȁhVnN~~ kH"%gJ."gx)5֢H[IǤ$ xݢr=1X1 7zĨ"bLPVPxIOڷ`ZLIEP–H"Oа-QG!üxjkch:)VgbZgg z8Gʀ 2% )X U-BVH`K ^ŜJDэABԟQx͠!댁EA<%@iiP-;NFeBԤ>Xԁ2vEˈXA",hΐecM 5YխT-…ڬj$OϮcYd,5O ҾN;ɎYJ].=np(R&﹪'>Z'vPVOb/nevo%͐wCp}\#/&@8QL//@(ۤYʯ3C0GO0W_0go0w00 0 0 0 ǰ 0 װ 000--01'/17?1GO1W_1goqlڙZG3qe%b?eH=ἓ=޲K>1n$c>ȣ'{#>1ԏA1&׽R字}T HJYPŽ=?JKW,VQQ-3kcTW@L\ZreU9lTD3LyLr3o- ZpG##? ]U`M^֯%;H<ݜ H2ccI$dO eAf!s3 k^/Jk)j%G#RʪQo"p2~sUۡB,Z4%ec)h 傃2Q;iFsi=6ʩWMSQ.xmJ׺$عY =WFNW jgc^ӉSbYL,, 5nMwrA!6 Zoڡew%pz>y&Q ̠^E69\t y{exH8vds ]5pppe~営07%>g[1Rl<bn*CƨrQд6- Ice`\Ūh13fX*^u"\f+L?<$2[P|D:_x(}8\`mQyh8p&H -Fm1[8]NS6&9t]< U`#;hBhFg FY˄6Bz.kdt| Ħ#y'"RezgIrP|**fc  -[wpilXY޶r$ (B~ P2(4RA,,ZS^q_ҵg:J'ȟP z3`u=4J.E`*R6[ !U, {J R2v&), n\:& ,d]E9_Qlx%D"Zi~149)5Q.˥3.Doh<-~D׺ʚKM&UPê0V$ L,9Hj7@'KK/V"tbcJZc d‰ h%:XoK{4a`X2dtpGlXVųhӪ]˶mb,D8ʭb:dh>,9wN3 ,!@&TjaD!sTYF1BuM^}&hyPQ?%1kY#52v2 6~o=P1LF!i K(=9FcI`Tsnӫ_Ͼ}  滯(EQ_tb`%,aͥXemuieȍk-CeZot|B@ *1, !@ 0b!n%%LRgz4Y *vu?y{3V%@lxoVdkH$Е"3׵ڨ8|;@a>*p1Rʽ&Fæʏap )*,ǣۉ\E`JDFя @ JW!2 \ 'DPW=]ja '9(m#5 [*Nm> m}G @]{30,vC8o'27|A a 7c$y7mWJ$^-ĿC r8vU/vŧaj" &вVFUI]Yo'uEN¢ճ52p1,S Z ]@=\& ~s'H A̠7HHp:@HhrP` ,V B3̡w(Aבu< HDnTiM&a5rH!گsa.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZR=$ײ.5r e (3Q,%*C)vXQs znH4=%Pwt.må?w3p<]8Gf"ٽ-AI&ItYA$=u~KP!i& K)Y־\0>aDZg6L ZYl̏<1s@<4ks/J(gCaWiZ (u.6Hi?7OlI*Z6~WLҁhq\(j* İ u&*PRRy7-' DҐ" @D6ͦtJ%']Vm:Mb*8xW+amY![]"Rl^],6n|WbԫsjX-26ʬk ?F#B%7 [ћk 0!lN:YG5nJ^"o'5"30B3XIKɍsX@9{9o5KI G)P1sl=`'K Ӂž!\I\Qg@@pNȕt+&UˏʠXޛ;>2PngWv wz cJ1`V23 אG`q!@^SG2u>0n{SfepG?HOaE3.ao[v `K}7!R: |L.7&&Ε#(0MXss6l.钁#i9(2԰˂&i~zef3Lo갩8K%;yْpck Z&;n k,B)͑(-8ףt9>Q?|뱥 Pk Fܝ[a[GPdPC4&Y;+d:`&R|8I@ 1d q|Žhdd:PEܤsUbˠ\sR*xrUoL)!eT hGw =jPHA@6Ɍ_սxo"~ 59z> W)gIk$Lux *>5of`nOg$ ^\E?QԤ,| &J>:j)hZKrX{b#CfvUGgU@! coobjbJp `ge`)}QW''2P& XCs!prQZ@/Yj/Fs6"(A8Ud0tT|af׃a6oeAzC[`Q ?'tU+L%D ,%Ei@  }' YW+ {u LJ wb6P8 XAM3?~x fh̡}LZIQ #}ukx$WG,jAxDlkՈ,H6s'Cc7$ rhoh],PYu%D27#)CsЇW3-'>8~qsw7o=c:5xo%WNT[,$HI +ԉLS`Y% qY !TnsAUxlr Y,.ˆnq2pUk$`c32,Oh4^F+ݒcPUi)@ P TVȉyU/"%w5cOu+J YkeRDak8U9(hQy3J/x0icjē4 }A^CH}tِP MpF -2>:]w"pRt<ǀ$?)ixY"n+Vtkg~v`P8u/akP&goAnI!;&zh }b("j ppd6b>!)d5  ys$oٙJp6(ȅt!y4ZA!jS1da L#^i᧍ ^Z^QhHob:p [6xqTHyHA愭v]=Jr.z$ǧm$|?%a2ڶtHbZ28Y\)'N8~^3!Q E6Іr+f(f);Bxsa(go(%ځ*I W~)ar%1q*ښ$z+@ZQA;NF'!q2 g7 ATH1Cѭr"u8b^衩;OyRC)-9qɊt`۸ex2`V%טﵝfwa>jUZD॰H >J2t'!l:jh"|}[+RDx;Zu*n 8oQ,86@Xy(t!E$FS2M*bYxW/ؒatu&R:6\hSCYwuvx$tLZz,TbT`x eoWn @ ;e9 *'x`yZq"eY7TG%q,}&ˆd[::> oj?`~"cphRZͲ30-P1-/W-5-k.ӆ-p0MÜd/k5. ]3[#7m a$"I .ҐKx%VQU5fp!'T”v @4%h{'4SG2HȅUUܔ%IRT R;h4X>  5UWqlQqR6Q7Zb=6XoY79'6%vNXT|uVgV֬!*hL3ȃ9bչN,Mh#U1vș43r;xh 9K~ULFrZ9<1⸉]M }G@,}=D0DtȂ"=$DGI%,.AW x4]6}8:<>@B=D]F}HJLNPR=T]V}XZ% F*C4t0>Ҩ!d;˥УG Y=[k/\EZ ũK06/t@5$bc9j?jbUi D!m4wQOI䳝mB$E5SPF Uf_^ pᕄ0(}2= l~=͗<}J3*"Ս$/n 2T[ĔI۱3{]E= -" R1ߖ\p= )D p=k9[8 YB[ƀK$\B0vFua̕(EZ_E}Km ]-_0e<Gsv^ t`Ob9`g`}8@aj7|A@e%8BXd#Mv'/im6ҀS V&-iC 6f#be1j]ڦ& x7t M/Ol’@_ Xy%0X '{S#/)X[%8Ѐ.}#E#0>۴#LEg\#v=#uC@.H: b.O/%7O?su)G;SEv.ҮH3@ȭ!Xqgq>P4>Ml|퐲F^\(nj1J~!ssP{V_bu@#Mr?}l У -б⁏ Ѷ8ۦ1s#]|bC iBe3&tQty^Gfp 9>Y'w|>ZF8Ԑ 'Lm{-;wQ0'uG*+\0eY±<+LJߵxH$R `8 W.U!<.;Jv@@|9(0"&Tl8( 0=|e}-T6=EM Q4Al ȹ8=NtQFj#pFRl̪UQ,0ufGы p?t<;a KF 3m t錇'TMgsDY$_R+t Dnefȩ _ &t0fb edOBa3&QErg9HM\˶K:*[hd3Z}\,dP`ieck%!BjGE*6Y4ӷpL4TX1+A6!" ƭ)A޸Cπ W FJB(&318B-]x6-w1X@8 T8Ni &qhX,C٣mCx~i@ )5 LC )PA6 Id=E@[P!X%O6j&ArU$7B246@ͧ8vDpug38!f `'tdy᧑QLٝkYi56Ø h8Im9@$xa1҈ #؀VE- :UmYZM F6HGvS"K~ EPTZvu+i}&%5u0a\IxX@ǹpl|5A!B@C9M+nث%˼FBFrg}!.KiG`fDGo/Ӡ5 ;1*O|΍1 P@1c{bZO&X/cU=/ZǼ=NCv]أ _RΨ sZӥZ1g O[ ./ S0qL(/࢐@/3+" e LҶQ 8D_ jo)y,aQNi.. n-brJ? Ջ`+_k g̺>̤RNu(pB:I:B? uյ6e:$1 4[ȅ?KV:ۃ0 lw& $qΑlH5Y#g7iu+T&Z2#У^́h5" \>Hs3̤~IM Y+! ֬'r[l8.2J *U'2`i`7:Q܈Bql@$2p͋.(=c@# :eT#[*۫L&OM ِ2 K5* RBrTde^Reo Jiov3"k_'0&Q1V@4 aMnЄpa7GP2k4GMtOfh@$RAm`lQ0 }#%\ DOXb `x%P#fC37J2s4C 2!D >)~P ЭPd 2l`p35 ĨKǐQI0.m< @'+ 0 7u}ȯb7x3 d{vBQq9)׾T% YjYzI> fƳK-73"ћYQG4KBESe;"\(6kIEq.ٰ}Xb*T2Xѧ* hNd==HIPJ@bEԗ$8H<( 0ڃ"ӯ6_KB 8i0qU7pygH _]9c m L<`p!;'ABM}[(^.&ܤ![ M.Xt+Q碗yuI2cܴa$_2k;Ż&O:uNT,@\gRWVi^}m+C {Yӛ) =\ڠIoU>Ǒ.@!߆7 G>c??d@ @!#A`آ  St]XA!y$Pn@O1E5M08\)D:8޹X$!Fƒba$dL T@TZIL"(]H` j"+JD(QI>5*[R el,̽SPBA؅$i ޥKEP eHd0EgVDn&9&qE`Z#Je-Rc_V^f&,eԥ@@ol+4@s^@D@4Zr70J"$IUfkEobn:hun f~r!lk aMZ1}K TtRz(!mhcT )Z#4`ׄJ{-y4UmJŢ} $t,u ͏xf&ž$OCJXT_MBM"I] O$L)@̊LLzTA$&y2 ^`{Y UbBG4i$%J B~ŃjZ*2f NA΋bқ{:Br^O5 nHW&רh\ mJ0p <T Ŵ3>dB]LƐi'q Iv̼,%؀FU=9(jȵ^_] qJ2x\G*,5[ JTΛJ F 4 ˩Ft5+&hkHWت& bL?#v'6جG6Uzu W&L<&ߠ(~~%G,TBvMlr&X4)X{c, 1d^Ț_ <{u+'78 P]x(Pi<l0Ԗ}t C6Q^tG79P`la$]g=SQQU( ']ӭ:E|nIrw,#RJ!姇%57:qSчA~:Q[x'C- j6h0pk"άT ޭe]p=g;oYY0_]>reY82 7K;1.7UKr58u 1m"̸c{U{n~FCy1Z334('sTGL< IJ84Ka;quh3jY:] iXvȃFªWCa֗%G7r*Me͛|2^0ZfVQ,eUBJp851Oؼ˄Xn0BH)KU}L +`nM$THn:֏ {L]-I=OIǀUK$gYA ć{\QO~qHl$&n9&!|$WJ0xGu}s5Ȝ6ZZZ"1vWAڙEvy.w|W0  q EK< 0@D q 08mV@H%A0ڌ[ӡ- ^[TwS`sE 6 "䐳 s@0EzYC`t{ҩ)9$b{W"H`8`'=V x0} `V0NNgc3 `Y:m~CntPނcφ};0B>xǮb)" QNx<T(X[Djcڼ3'Br# +@qjN10fٰP .  (1~͋X X`3, RA'TFL?H*Qu%SëYLYʇ* "}\.W-ӣ*`Mĥ=b k 9\VvjSQ wrV5 }HD3ePx# K'B&-̟ 7 ILAZbN@qifx0&0 5$ a L soqޞh<=IaŲOorS]f/e6~,&(a99AVcCa3E\LM:ƈ`Xi`0`6,RKȐ'!, \UЍ/EFuKUA%ƵD"E⚬?r^u*eS5Г%@}T|RK(cYY:X#*Ma)`6ãRXdy$Ѭ+L?K B-u@^O%1x¨ rHТ(i[ZsU)fb0B5XT,iAnݡzc$C$hjjǿ~-ڂ F7` 6pJڙ͏V2$D:>FN"gOE5@+I1#98nA˨K̨dXԈͯ*d)ALh-BPIGǨ.0޸c نRΑ<֓W -<ú`@}P-"!6’xWgKVYאl6AԬ&th(18 j(B4l8meZCnƍsr{rd2y~06a=aܱ2*X&V< \?љAf#Y8t_"Qwb@1R*|t7aE"ܝA GYÀ.n 9aAp`MGmEv2,PW2Y!?%ͫW}B]b B!vTck$D P.kĜDz Fqb""9ݥ/z2=9g,6~o}>E>-Ɗ&Uybِ ; L7lOqg݁)Q3 uՠ%חW']Q(:z e|Bmƞ8 !͑ Ian96WVUױo|_`#N3x Ek{Dg+P}Um%u%'yV/`b'Xvewo9z3zz 4w  (xPCMh4GJ`)6g#bZaM@0Gb:d 0{QcGY&Tdd& v0@sAQ &t <2ӆV?8PvcBI#bc#ҳBwgLH(xXRp5kHe(HQWB")oPqj b !1Eq(q7 JCsgb[i`hUVQ T%BGyTjR<@oT wvq Q73(gea%2u^3u"5H%e'_ǏƲtC+86b+fzW"8H5ɵ/.HES' " I=3T[X-4Mߔ:=k3ΐF;u1mc N8n=W.Nn!.#N%n')+-/1.3N5n79;}ALS^ª4LCK%5a8 jm.E^OPE(4xd4G.jhlQ>]%mx 71`Fbu 9lз.Kub1< +$h;1A`Dq @%){Sz0LNY ?qAOOPɥyUx53KsDžs(RvK u ȻT D5`5}RfI_ék3S}SԚ'SqF- EY^8e5+d]&V#hj U+QWEW%Ӂb+jol߃@][u`(~4ҡx~+J6wu\p$=Oow eSgѕd5+-2RVܡKKé9߆]bOE,zjnZ)۫9 0jjfuP](麔f+"|(F#+y"ٻ|Wl 0 F ("c ;k#¤3)pg&CdO;phrTd 0zY7,"c  CK WԟH ۠B@HRKi AP,m-nnMg׍_WnWPޑ@OPȉ IvV7/kM=>Z/L@, 9VCZ+ |xLQF( @^F*aQK?v7X8S #~,nt ˅J&\R|QόQz UjӏD4)VJkgt`R[hUoe5pN58hgEj(er8 l a@,zrie6Jg6]<Bo)v3ԈFW DTX 8f8$hJ<59}XK`mpT*C{% ?u*(VBԅ<a9D#DqTY,HVpp0A"xY4Xō1f eȑ$M D7E4 9vHa7f,P+="rx@^9i|5qi"Q ͹'57}94թg`c %&zڗPLJd|.<ߦ7R51̈́a_M :Ei%Y;R1.A,5S!+B6 )@B+ͨ!BƪKOQC Â:%xY5i 6_pm 9'y h ^Hdz8- ˺i:ۗuj,z qiLؠ."roj/ZkD?f"#,sk/W,) +:'2bv+ QTX̀xE&c3,M- 7Vr5?DA8ݍYG+^|u>ogCxtڕOm `Vw!#6#>$ dM%A$^&n"'v'Y&~(ލ'&*"++B(,r,v!A-/"0Nb,#1zE#363># #4N#5r_5n#7v7~#88#99#::#;;#<ƣ<#=֣=#>>#??#@@$AA$B&B.$C6C>$DFDN$EIBS ҂( ZGF $JU$JUnJ*.`DU.#TSG΂""O-CHMHR.ɄLS:e( >,!b:d-ڣd')-\ʼn(Ƭxa9@RKЗLUMΥgB CQ$d6A)%f'DUBEP` TSV2u&.)!J~]kI^2ݞ(U9ir9^P,'A2ɛW1eVb]fY. mlfzh'hRږuYLu2XgES 5Xhu$@[4@ 81Ӥ(@zCĀX%4mz zfh'ĩ[t>cˡAK%&7B^țVbv΋yO惌Pޘm'zhݔ] Ѷԕ(9%kRdoV-&pJg"u{!~h%"i9! YL]a V`kF ݠ_!ңO x; ݠ[1'q[ʇ8j ]e)ey9=5vB)ψމvFVp*ЅlBPbTiD.5@SˁЬ%bH]ń+ *|9 ,j`!h9B&Q(mR)|$ ]ei],q W&1IexVVBh"|H-cZ}f:jJ ,)B-QaF6Pt֠&cfd*+oR"Ƃ+3eYC6Z돵JԑH^6lɚp @ߡ=*́#!+@@vBT(w-N-lY&xNgY,ŮQ\ @EtUqų=Y<+jă"hvى ,|!z$TN m @UmdAq̎RYmYEa58GFKda JrWcxQV]YY-n; 8M"-X`үPTnG\HN PNceC^n`U&ےaFȀO옻C ۄ RI%IZI*b/.rʽH*mH˔Se9Xquܦ|8«Tl%wF“j/YI9yb*->tq<Ԋ VͮTct 4W,I\@#ZZ4, F"/YJrƒFRWpȀiTo C\qS#<(+٬xLm 1&K܋T-)ur-m^Z QZJڌ qrmqeήX˲LB .Ӯ94pu@?4Lh O2q-ՍA#;,E&FaHqQ)Lŀ`qZym22OBݾkf6!:°vUfHr*OCɱ%A0@D"0k  bՀ[l@VJ6u5WA( p|̦\QA$j3 87]ӉOUhE -lb-4qC%$LgPԌG$;YDEIu 3`)v3`i$&lbk?G1b Qr7a\ d YArΕu=ªj*N*0tB& )Yh6c0I3aw\4f//tΆo חi_p3b,dtpibBp/j:Y4OVC)>lCJFK6G. XddUl>2,,p^j+1 Aa;K\ @/︛F,0FE`1s0I{㑯dY-t#ĸun:cu=`9gl/0gpɁrvb|tɈ=Bg_{vnff4y& 3ul=yt/^3dՆ֞{IyܗEٴoy[pސDY93iEi\txl0 FogԶӌQXY_"P\Y)9Wb:6BԔ1RMSeJp(vI }CZD/$Iݺ:}vh;i{4zn6 Ya*{f3/Tа e I\$'e::(taFNVn}ߋ#ħa 3\0JV 0`jfX&3H@ddY MDIk&-TԒVeCKfF+[$;V;`B[l7@U=[$|i~U*(} -FQz;,'f8ź %CS[0/4 Ƶ۠N=u|'P@ȻSx#$s  E?N=t u2\}m@[\&$Kш tG* j䭷=KF!xx'?Ma PZ" $$ -fAxP.6%2v˒>(d4TٮyXV@ND uH! K߹N\;S=B@=j-"vݸ˪? 3]-1 pЁpEVЍo_+z9(OXi:\?j84F2tXw$Dr(T :I!!/@p|'Awܡ Pǃݡ7qH H/ :TyƲA<d"ⴾ ԯ,P`\I 6ѫ] NR(4j_ .IɊcp7t?J(&!C!#%!|YNdGYlRpY/أ1<Pq D:V;_R/Ac4#; u`5OXjt+TL"k_isnQBخ<:vx$EF iq]XTPr1ЈO9\!ݘD関qY \BvbSyPH[J&A%ILpWF2LULA\ ]x8|cD7q++8$ђ-@Li)ϑ VZKF? fZLFt$+(964$ !CU6_*O:WdC{1+q% ]k=Kog `uԉcBq_J1Éӗ4 +V7XuGt5̲4붋ۙa@;.4^] nZ.yTmǯYJeUlv^pa TT(r<RD>xfBEiu7W/?)R $ Va[쒖 MM\qSLrӵRiѺ.Țr%*oD'-cMH^l; U=2KJ0o:ÅrA-2W d0]0ˆc\T\yn:t]>pVlO_E M+cx{%k2?Ԑ==֔d#ae r,d93Qj q6u D19R=l 6@ u*$3 g#+m\ts UaH'4-ʃ{t Q]LW Z¾E]_zkʸ}_?);$a0]ȿ ftYg}~m>Iv@ (OİqX^4Oz}b/&%N zÍ`8I@%Zy;g +@ȎK- _OXG 9⨣rB |vc;i`uxk DpY _{SNΖܴ5)kI_S󵄫ke~׈.}Bs+ grO|KnƖgN>%Bwy'z:Ƅ.!4'{bZQV{uS{ 860wAl1uEPd7d}J0IYB;6h\$hsz͔'sW h.HZM;wBWu5jgʱ9v'i&~xrkd r烝pK*Z)F`b=m7YwA2Ƈ6tD?bBPd$P~5$Y8L0J4CԍP\fpSh8"24D-0;eEp_9/m ,){G cہ43 :qx[orbwiǵwUԌTh(H?wEEK~bsi0N ^H:6v1Y^.JC0 ]R$Ug%jyRʣ>Q9b1pX7 e(Yc)t(xӕ9p5$#w pUe c!65Z_Ɩ"Jg~?VՀ ?s8y{"fyWW>].^7qgVfHvǜk6TR.!~Bٛg8f_ AJG a ʶoI 7E*1MPI EJsVF(MDs iI vǟQq VOSX 1#b+w_@JRU Ek@apYpH[;^X[@l<d2I.(7c1$bn!2uGiPQdy a(;)@HBw2]*,%Mp*|[~ro"c 0!@'z*"AW0^2)#+k&}l@(9?zG; Sx'hQN;SH* OzU:3!ZqwyDa 瘫z%ZDs2rQцJǃ"U(@ n;# f.!_N*%5 Z [&{(*,۲.02;>R@8:6;@&۳< F{HˮDI۴NZyT[V[#R{Z\"Y۵`b!_;f{h el۶n;kr;t[q[xzw~+}[{۸IK;{۹h[{H[+;ڻ;ċ[ț|sۼ#;4{ث"֛۽ k!;{ !<, ^  dihptmߥxH,Cr9@ʘPs⦼`2!X)O0`()hzUpq|5yE{~e@sm~>snLp4jL5sdgPa[~Pl)W(~={)n7)}X- A(.IPB\p"0!! ,\  pI)8ʧYׁȑYdn3r~|@  5`<3 FcZ;dd4qA  CS ni9E77^vRz|6 t~ sSr~*3,Nu&"*O!,j  pI+8ʩX'ؑɡj;<З-~ @)\ ?ZpX<&aK@ꓵ$d@P*byM-K ;!  PBZmpz ,H  q2.*&T*vx.~8!,y  pI)8ʧYׁȑYt("ڰc^FDc0<$ Xs` 6>9?SȊ  TzO:nH( ԸW!",  WpI)8ʧYׁȑYj+Lc|_O Q9ihYHDԕnU]&Eey銀ڞ|-!5,  dihlY,tm8|pH,Ȥrl:ШtJZجa*x "y0l~~fsm%tsegi%g j^' <o#^$rg " %$ k #íg$$ g $ u w]_* "իbd%F- e%G #JHVV]mb8V좈L\f3G$ek/7(ੑA(is&Gܳq/GZ Nr$ Ǩ]e(8YJV`a$\HvIÈ+X-N`Lj@`WpA*8\ar2#ˁE%TYbe .cьyqrffxjd/@>F@w9#0` :J#oyA^@66@H Bc )fXN(6}ձ؅fvBcnA-,n35%`T2dV}ئ.BelFqc;9@!_*q%UuBje i;F " ](Nq 6b4iB矀*Z@`'Qz_CtV9((*Y.6|Rg oAƊZ0+.ni%I'PzfoĈro5^gfva}&,4eX i+\d֏QBk rKn éۋ;B$ *^j. $ %pB`?n,FVyFVR6"X0,4kn Wq$ knbK^z `<]j4/P0Xlts'T'oBn$GRba33$oeeǝbpNs`]'O"BvxX-9&\+.Ftx\KN-qv@m2 dՕ7 6ᢖU@sS!k*G lёFHP`zYW~s= nጧc4p}’_K% 8R4S˕W/I` 0 /!] E1/JP0ǃQ3ꪶ > 'xbfV XG$³#& " )0L!p#u `+T! G=,&7Nb< gJ1nL A*ŵhŪI+GVcB%2Z9&RRh4Fg2VV䲑 >jNB/ȉ(~1%(zG x@-2(W}!7XzwTW'C{=rp+_[@Pd f`dʥ .T41 *f(`HD=@١ AUgp2P;&`Q"\#01X FWjfX*9p|5ẂHR3vQ% bA'ˀ&@2(]p>f]r-0bX0CZD,*0 ll({Ihe {EM|Wn%|Φ e݈:QA@>) ` p#w A u$sI+E 5|;0acp LL`,󓻅kyƨLP 1^pW  }} AJS ^ E4%(?Y^ j@W0=pXGHV٘yT@uɦH@0 IWp 4!EgBV&5SVE$@qj8.=JZX6y1SC@$Guˁ\6;1QT{s0 ^`Q@SёMjrZp "1 PJVeQ)Hy %)VKd ~$)S4mPAg" $$i~ 5$r%ѣ="&;"5bb)~pcF5@L R"f#I@aE.=gjR3;҉000e) VvHH"!HTqh@՟y&:ՍFh_#c4vAAn0JzdE 8J=/%Rbx #"]aEUHsR,B*PRZ[w>7=߆;L*0*Rg,tcp+PY z6Z3AVfyKO""$#VhAQG}`IS>[EJba`cCU["5 .sK"efF%LOZAX MQ+W*LY9.#R&5;$`Rr H=Y*XXP6bB[#P&j%$P0ڒ@bK#R ?ڒ/bœ<@v"5P0룇Vu 3q9n+·;Y?b{s *!f.隱c8YfEGJSqzù%ȵ+ 5 'Y)r,p 8?.B@#QV t:{){zhy˷k-󱥒$)6J;Zg^= ˒'7"_'b%>B@mSky1ˆPh`jS4EY[o,q}:e;Y~Q |Cut[l8{2(\ BL7I:^A_"4r۫A Ѯ -S햸"`ۧ@̎꺼}<6+M'C2\,#F͌v-DmRj=@/#5%Gsp|s*jSΟ𚫟 @@r=Y N]]T-0go5=z^||lajp 1=b"GBP4ߛ`M$nm\m=d&I}T#٥ZR(S]^2I6 c\->^u`Ah11 M7Qx5k۔:ڭ=lԓخ=tR;<fl/wM0i 1M9~$ZU 5$BĴmX׻%E.Ă7MV P3>0 ]WN[C/f ÏCL1%X֖ߡYm"h'jb^~X-@/}m TO]"}}[^@]A!zE4z.Ϝ>ߤ/`⾽J ቝM:.Z֦u'@$ )uᡤj[?P  {|Jã ]@"G BPr HS-طgNKA MZ#)k ~.! l~nBfez$\tH*`Akz4D/#FzZK;]z#ȬC ;QQV @~1cAOư@p hxԯG`rj/RmEH0 qm\ypb)0$-8AIp=4$8"\  䤪4=(}",(AH008 L1,<< i2 : %(CӷO,YKhu:V,c* @  ֫k? .2. *^#r%˖ Ps)s&͚7Εs'Ϟ> z)F" aҦNB*ԪVHu+׮^*vl1Mv-[Hli-ݏ Vݾ~=0)'lƎt%JȖYs0˞?-z+zu$d$0Li~έ{7ޑ&xP7Ə#O|9ΟC.}:֯cϮ};޿/~<ϣO~=Ï/>ϯ?X" tZިM8([erء#X'+آ/3X787j@`Eq!gSP)$AE ?>ym I2b \9٦osM# T<D=S1XBeCM* <9]~)[^@ڪsS|BP9Ճ+E"58B iVqXr; ꩶ}=`@)v{⺚ۮ-aHBvBVScOY98$IH*` %r#\')Q*B0T V B!zt$h̹\ ! N 0`el3, +1 7-8W!}C'tzb7H*c 9#\@(4Bj1"]7|ظ 1ʦɀ"pnB[2 z)@8J8Tq, ,0rEX`Pl#S(~_û:1h / ;xaـc|`b3`d:łF)jTч x@ 9h6FA2 f#",Z 8{-8`Y~Z"آy 9Rbf ϵ Bg-dP=rG;R1x}HZ QÇT&oƨJp/ZA,h[:0<8`3x:$*t4$Pw5|?"F@y\&>hfR(9qrԤ0HrvLV %W;e!siЃ"pa.Iqm|&7X@e"{띅*7ã<RHbI?ㅈ VtʅQX'0V&e4]1{p ?m&ӀB `Ŀ&4ZjjujYlnj2>LbIE"3` ~a :-.<\7R񀈼Oa.mAX! QDӢ6q#CgR @m dkJߨWMk.C, f/uA(9d1 H@4Lټ~Rt""m\/}뻢y>zcyҀ#.*evcL閃y%HRmO\BK\0ިQpQ0c8:~n8:ݭz{.g]`6uVRV̓lf^8I=l_N |d }qTׁe1Zֲ9oKR6>r6~f׊1 }0ށGjɷ# U(0K/:,3}P@E rHP(̄=pڣ:o R3*X@x5(,pcذNcċ?#F3rpa2 jYK"lo> $7 ^TY9%Sqw"9f @ǥ034@ (R(`"SEdBXJlǬJ d)p8d7&ˡw2HżG00R3G$bW$ * L`X1|T]w'9^|@PAso="b`IaamP VKKz`AXBt R '淿TDǏ dF%0yŁ A??oo(+=? ?`HflƩ*2`:1 _>b`jr`z`` ` ` ` ` ``a a"a*2a:BaʉXYѕjra\ 0_WZe D@aKt^Qn٘b ֈdqȹ́˵ J$Rb%R PIÚX(b)J!p=&H,l),"L"VR&>b0 0`q5)0;pJ^ABc4J41"26J8#3@PQc88.5VA.JO5)/6"9VA2!J9 OePJd)yXQ9ds= SBeT=-e8Fٓ\HeXX"]Y)VFB<Ye]eD0όޠ^\eaa"fb*b2fc:cBfdJdRfeZebffjfrfgzgfhhfiifjjfkkfllfmm@DnfofWfpʠDM1Kvp*g VRrrrJg J%CnjWLv`RdaH*"&[ZEDN$aUK wMFFH@4Aں=-]l:4rw>d8^I*1BUS*3:F(Qu&Nn96lh))ŘȀ$BPLHі2)*AUK9x6r2꧞؊'؉5@!hªꬒ_E* YYŒ+:PZu*0]E "S1dj" d?LKy*X6`[s-O"ۍjHh n*JW"_o':k+Zd zǂlȊȒlɚɢlʪʲl˺llllm ж z"O-$RɘV1FBEے+0: ļ\n\`>1禂؂N % O .kCbJC&&ZVrE"@S9I|ȀC=M T)\@m0Y^|œD\o [5ɀlp*"Zᓇ!F$ @^Lhz2&eV -4GlN4OVaS{9 P R|K(z@9H95+U3=sW-=Jif7N7YBOLCpxm ,Ce|#;C!l騲ΛEQT|)[FӅKHA=H>R<=C$RYW(?&|&Fw3|f{J|xo=9:\OqQ!893X/|aM0A@C\ f b1tI0,@ 9 $@H;C: D3 !P j*P [(8HXhx)9IYiyGe%bwvRրB0b3u0drp'+к 0pXP* 0K ,{rs-<2-kb}>К ΍mp#= S#\~ԹlLj)xчιeSt)ȑ$K<2ʕ,[T QJ7 䀎%Fp+ADs  ,C$ *Uڈj 8`H<#հ֤`k&Xс\$Z s8 rhqU`:Uo7rA k"V_?<̛;lSlTܻ{>˛?ۻ?ώ>`-}` .`bNHa^(!P~b"Hb&N!*b.VX;qb6ވc:R/dB9FJ.dN> eRNIeV^eZne^~ fbIfffjfn grIgvމgzg~ hJhh.h> i2Pل$9i~ jTxF4D4TªS-LO1 !O2VO+8ݭ:Adnm+\@)vY;F^ͼ-q 5(&1j,:$B2~xKq_qXŮ©!Oq"H,+ X@UWZTqaq/W>ml*@, @J/]j>PFP|XL&-w4qf  m,#]_❊a/BlyPw A $cv 4]xڢNzzHxNNpR1l 4,_OZ@EVUg@8.qt>6Uk`yֆ7+o'<) lAT 0 e~+ H u@F S =ȏ!<4m HϾ%4H! Lq d;ЂJ!^{WhpP?OT㍹1brsJ@.xOիX 1@]*N' +[+*%&Y%!^q2o~V̪i"pЈ5gUձWήʰpꈉ07Fltw< Mw[0^wTnll$ N<)ml=]WΥl=};dq qX2Nñ1@[ï(&4>4RXo1.mBqwOr$02!Vނ4ԣˀx32cX(^_#ǠѼqaY”YO[g,d T+>`',KMhv>pjR6g)|וF6MB_+yf1K` #2_3b\? ] "1TbG101&nXG&;܉f}c\@ VOqزǽE{7oX쑯hRǫlZ&N|ip78!0b~XiȎ( *>f;`U<M2[M}·5qPECp$II9  nq+Cs` TWkG)n!+QmQqQ QS"mՔ1Q[)T腪aÅehia ӆ1gqH(jHHw1#{ȇ|RxwXh1}hȈ舏(Hhȉ艟(HhȊZ)(Hα~h-cq;Z THh |1CX+2(F(88D4F4<|ȊXiĶ@$莥b#AfBG7πxHȏ؉߷BzWW 0QWTVq(q}}`gA 18I ^!ɒ-Aɘ9e8oIPZɑy;iGinwM AP~H)Se#8PBT_Qi=`9&kɖwӓWYedӖwR)fC4yI(Gh"uk4lx ;Xwi'8Wuy'fEfwR}H`{|efCڮ/2QGa W l } qLd!~0@gϰqaaJhF#<$oza!s !l&>F;N`kO6 {B <[L `@`xrx'5^qY06/!xo5JZg5X1R a4 î)jkTeߥBAZ y55H{DE{s>)\a*EIk|1N\J#۳ZWL^ Wcz4Ds9[z8qWE`( GH ћ͘2+*?Le0xmNcTs 糕-6*_#k9[ ܻGd}`|u|l`?PWʤwC3c8йeag400{ck\F ruk Ͳ~ IX-`¯ Κ2::[+hd7V_lLRˇ3,Ѝl@A7ԾSd '۬'Ï&FL] F-Ήp0}7= &Lռ|9+HS&0ñe?T ©AR_ -MhΪO bq7Z,.=j7KtziӋ9dٖԤQLo mQ~P!`S|#0Gi@cU9$,0҃lڢJ1&FH,SK.dbo/_/h)$x_!vafPӍt6uy Um"ϘhOh ~ư0 -ăi0`sH4ONYK߂N(hPaz{͵*ݯ;1j!<c*2 $ $Đb<@<ԃ@38`" !*1-|[ae;>?` a!b"c#d$ea@ R&J(' ˃BKXXM-OJiM덨CؚBqss²hZ\J88CMfjJެ ,,<[K(kkU" o%9fƏ]ǂ3s?4cPE j؁1ecNṵ48c&KRRj*֬Zru҂ |0@ٔ 4- D  J(WE t@X>TQa5> ñlx`*HIQv5Kq3 NÁZqJI-fM/Ыj8Y`'a7 1hL`3D@I@qxO,R;LgQKjsJ8!Zx!2@#@ݐ B\ fQxWB,`JE6^$Im-FS?ԒTx8v4dN0o ְ?? wKqIB7ef{yS|#!$]B4f0^Ԡ vՉ868Ijhm!f]HY2ivQix)j)zj b9D`T+ӀՒ ڥ g ] ԏ[(#M :p0+o[:NX%uµˊ-+R@,n鎺Lyut?zK$&zX&) @)샟:0K<1ŒpH^іh0@k~k ėnvTPÄ́A9[=fv)bi7'֛S;a*@Ke _1-ͤJXNim,L,R 5|OB@`X3R9at127p5Rѭ+8;;ad,,ҞKI&7/64BIy꣤'!Px~<3R=R`M ' HwIoz9397A+i plZ*H_)(-)sZU(,"U#P:yF<ޕwcE=)HfNH*PiӜ:[KuBK4[;f~RCExb" wJҞ aD; (9+rJ1 Y[g*l*K.JY'i11Ia|f"y5F $"i5n- X8!T"-O Ƿ'/!\ܢ>팼[p$Aepj4SjY:2|&4q^tcbq"=՜6e"3#(<@ @-0x4D[!iiQx.xB۲i ,P /t( 9`:(':uX۫S 7!mb:,G.~1c<$EK;y٦ UB"JX:bbm8P,)0Hl챆F֪q\^$l.:vWUnD>ȜYLX@e֐ Dhsb^>y˸ψ0Kd̛;IJ|іd%M'Qąs`{SCA{L]T1c-YAN`ޢ&B:SVApM f?Lh r1+l p$C [mI)mvl@X dz ̀ $bhrzmXy Ku֢҄M,TqK $W<o{LP7:SySǭȍ{UpZ>l)t)I&3N:[ SVN~szȡ5f?;ӮΊn;4bӽv1HQ{Cgx#ZI .S>*||U,H)@]/Sֿ>s>/??ӯ?/ӿ??͜ &. ʘf2iX5 V^`U@`  p \ ` ̝AS ֠ `=E(j`Ք ޚ49^@ !^!f LlP>Dj! F|E]ղ*!!J:d(@Dء&".қB8AZ "#n"'vb99E%%8DAx${x+",( UR`D^ {ts|`,0cK͢[A/M(q[Ȱu4N#5T VL gY[5#9\cl`N-Ѐ!h9#= Pk\YVT9أ@$Af>>EDYҘL|2IAFDN#E ,TZ\EHd I$JUXJK$"U(L֤M$6 OJVD$PQ%R&R.%S6S>%TFTN%UVU^%VfVn%WvW~%XX%Y*:KYZn:uP%\ƥݵe\%^[_%e]`aBID8Ͷ l &ff&Ut8BdNABh&j&#mFqd ]98j&oNF+D%0 VM@o.'sr gKXIݞ0ͪfsfv^H4^iBt!Ɯv'zzoYH8A`gz'~R D_lo/Nr'&$1I|Ҁ"Ny^(&! dL @^ NB| RYԳÉ(AB %xc1nn5nH)B <˥$MNi3TRGFZyR*hĘ)i)f]f)oʩ&))**&.*6>*FNL< L&ޥ ^vjMF߹ݧӥ^Smj' \PA  FAlP0db*Nt.,h2dV,sY~YPY4L4@, _R'\3uXfB C &+*B[w|mLɁǀ)hQ@(Eupvo p(fOߥH>|暘6DB̶I$ (lxUW 掙Gލ0Y]䈢B> 'Q*}O-3-NUu~"M P`6K@/̓i+afAr b5D8QZصQ:2:d q!!!{)O) La1Ƣ@%lAb뀔]*B9г]TJ. MQHT!A ׈E(t/K IVd{ArԮ]8K݁CU_sq1&0+ZȌ$Qw:9S³>Hش pBCTWQGJYqngtTPBK2D_}h5 QtG=\`]}ߥ@ˍ=#~Is!?װt2@A|F%%HI+#{bj^O*c3 1nA>wU1#UDlrB<-#|F-TH]5[CF`Y-rYPG׽дT6~Q(Y$zh7~3v C( &2, `Cw4mщ8ys2 4D7rros3m7Ѓv$L@xw'i+@/x 󭨷SvHAUSE7u$vMTk3G˵5ltOV8]8%I3AS4-y H >s g;C5`6I(oArNݳa+JoVI?r6a6R9Isc9iyqS7\=<964U9s,B|KǸXI:D(quw!9& Iau3;P;AXJ O [)+>k!kUI8ozwƳp!/gN(NCi+&x-ƃw5"B5&(ڸTßcyD<o~C?A"fzɮw]+F wuH]yg??Ձ4Cihlp,t=gx|pH,Ȥrl:ШtJZXmzxL.蕈nTvN~Rq.kE[,W4 H*\ȰÇ#JHEKhxPp`5@>EӐD]ɳ' 09DƔH%s@. jd8/ F \` ذ(Pk E Eѽ T}[@tg@Kɜ+ $T_SMtLSpҲ[f] |RXDiMɵDZ|6zY?AF`GY_n?Ἰ75AEwm]Wma zRob ,@D ƑQ}%s* `X Ba!Hc0u(ؘ@@j)`befheeMKX 3&H=(Cj7b_(p# zDf Fp YMڕ`'%dCd hƤ'喤S2|i <`   h&XQ M8j & u% ^j-)1@%ޛ(@Ԗ1R6% %+p·_ l"=9K3uu)َI ؕ~Y"L6\-f B^W.[~99jޟGQch1q+eMLHMVF-U[9B5e90lp& >n|+')aiH0&*KMDQTLl a,%{dH>Bhf_(x)p@@%miZ3vnĉ0 a[4(N10{4s 15JSgiXF"FGzl{/DKM&'!f uLOʓ-i K@Y*IGV(<8eU,rliʙ/}Tg+ce^%!Ae\LV+qmNPʎ,/(Pl9n;ha-O6ReNv%b* 1ȳmgba*\JaZSɔ-KkKI Ha2>%y/+Cک»lAl/iOV5E=z,9]aPys"ӆ;a{Z }o$z-I=r! - 'G#Z@n=6jnAH % w;o@D vȡlI.N0O97H! )ڲ=>S0sXcJ\۰P.<>sO)k\qk&hX ,Z n4E,wmd>ޜBSQwCpCr,}J{@ҧ17[,H0;;qh=^ǻQ6&[A]2 Dg-`"r.0|r Si1rU%@Pa@-A vpqd1dXBo|7cU"=R+2v++Q[V7DDOwXZ ^`ЅbXfxhx dl؆nTp8tXv$uz|؇~8Xx؈8Xx؉ezW64e GH5Xle#*8H@#h"u J(  !cBJZa+.,9HƂjqd(+"*7N PCBӑWh(%8?~gM 8 ݱ'h\ ЍCO՗ 3{h@w{b5Y ]xatN6vYptgPb[J͓AE(RA+@N!BG@kuBB$FRAX$E[9ᓿxfw%zQF+uF8#v289b6 yIzї'~4!#)sчoDN)C\8;eyx}|h15 S(rh,.88Dfcs3P/{du()sI6*Y'6c5aWaRr#DDp"u11!Wך(;0@g-vC#HQUcj%ܡM$2j t#GIi6!f^~ ">$^&~(*,.04Az8:<>@B>.1`DJLNPR0@Ǐ`[kS^`b>d^dp 4ޜ hpr>t^v~ikn Zf\>^~{]1{1#[B.I҃:XM3~ꨞꪾNN860n;alPs:J}f=ڬ~Ȟʾ ұ3;m>bSό;~;|>M>5]ų;n>P#J\9H9>ˁ2H {~> ` @5pخVh@ 02?I. <Oi1aNY^P5Y-GO!4_VX7?}>;lk_1BHd= Gи_0NWZΐWI.'?_K.{lM!uݞP"!u Dt\#%ѡ ՍW4P9lho1$R./xzuO+ uzj`j`jR\`j`j``j`j`j`_jaNؼoj`jFƫvu`j`j`j:oj:o!,  1(p,tmf|pH,Ȥr, -nJZجvzxL.z |N݀p8T" '/v6V: LG\% ['5 |~ <S{- px_ k(#8x3 0BRU:Y2`3Ç#JXE>8h#E9-$?h 2ʗ6 rt7! 1r  ЀE){ H)1+l KEÊK ( p$VҶݲ筀PK nI8/j%zGk/QdB}nlxW `lgs;ͳ5`8ཱུ)* :Xvb"~>N`@WGL( wUؼq3_ KlXTfMV 1Ơ @M2v4h#\B 6b#މPz4@ xZUi (/X;m'C9C!0E :ud)dЈ ĠҀ(< W ?{|XőPi TG 4rˆ"g!*'! z y&:3i!&,h ,t"4ƐS$(̥#0?Z&ߢ X,_H6 FMkTLGQH#(y[Cl H⠦VLG2NVr{pTY @& Ё"5m xIBdlc[? *|1iAޔ4۳h^ xQ['t-;ͪ-Uq[1(~A[8 1"@gPʆwPl>3`BpP)h&w\7ތusAؽYg,kL׾]~UcQaa!*{vg>1 n f$v%?" i oBP-˦zH H &:hgG4׿}0 ~ _d3@LV'9z6xA;_H6&yu` _ w7U.1@'^# {g\"X#V(H'8.D90(k3x9zwԂ8Bh5cC ja~G؄j 'L}LVxX(z( d@`b8dXfxD@ h ۂpr8tXvxxz|؇~"Xx؈8Xx؉8Xx؊8Xx؋8XxȘʸ،8Xxؘڸ؍% O.7:Eҕ(8ef&6[5@ `D4|n `g4 a:'7KO0* !I&يFt]s$a~Xc#LзJ7gm5~#ёHlGuKMyRyU ,T3`'{q˷MeV q298up]2Kj| ualy9Iz/BFq/ t7 tbiJhoYu t}AhYxe7B4aw"AKAFDHZ^D|| q E`CL5IbIspOؗA6ٞ9?e F5Dr P'wmz2!,ATH/'vvu(H(?HS'`u;)]'0$*?5Qr[\Y"?8}h&Y8F!=JG0(I$o*VuK2Yv r1'qv11f2LL+8M4ndi,Z? I6T1f,ܙw7#T*K',H|D' GE..Ql:φ.V!.122:2p)ۖ2. =ZP!:FiWR3lוVĵ=kPvĈ|RAIg8sp6g9( :=8k:Ct mCJ7"Mgq$QGEuZu'օ2h)_ơZ]mD"=abtCW$K(7oKPJ",=م\?z[$&O{w {CH-{!|ZМ7Y|`(jt} _p `m̱6!,\(< =M] <1'i΃%AW2=4]_ji<>ӑ @;HJPX{.5:ݗK=T]~J`'Ii^`b=d]f}hjlnpr=t]v}xz|~׀؂=؄]؆}؈؊M3؎ؐm^ٔ]x? `٭٠WLZڨڸH i[ڲ=ۭX Y|Hr* ;"XKa pChbl"m(r( (:Н .7J| ;!1*xI=. 7N POK /]_pp PzCD*teQP!>4NZĢH "?,bJgM3.^53sU;e%~ DQFA2^N2H+y_|>ypZ_E}ୡ *$9E-Nu@vncK35&6B3~Mѩ^x'h9s+>Zꖭ&]cn΁GJ*ln\N=䧗]c}w'Cm[>퉽v69mGb~^ӎ-[Ύ[^/`>^~?_ ?_]i4@,f$Is%W#0e :) @/Npޙ𻍆 cn4o2'( '- $=P(o`69E@&-b׮??. 1WaFP ~4`fzKWhgВ@/_WFu$/ eW g3q1 pPwv-CAڭoS\kd$Ot"r1Y Dpp{khO/$b/ @^$=і$0!p OʸP֪@M8DC5#g?]#$lc yȉ`Q,!h<"%|B)jb-h@}Hb88$8.91(0]((\jablXʚ<p( '+Gy*o `w675`eXJM0C 1}uPShp/N,p,} FhCuK.SI ĝŵPpmgV jSX|jTbg-AuЪi *+ @ & ޘ'6LyKpHj!Pܺmo`j^0XjӢ6r) AtC`!SZ4&"+A!p4QL+Yp9ΖpTaBU(fpeB2JU $f8aR0 l`$ q@PG{X r)s фx 89ۛjI:82Q6І4"EY5!aɡm1B @ȃ p!XVg1Ql+eX s|09PgNBBo>0l;?ȃ3?:Ђ49ъ^4BC:Ғ4+miJњ4;mG_:Ԣ5Km [W: >5km[:۰C_;¦¬smc#;٥raC;Ҟ6mkc;6o;7ms;^7w;7m{;7<8QĨԕj||N8q4$yGf_fg3k=#?ˀ5_79{ZAi fw7yu9PNJ'9c)]Ocݒ4&EWb'=r;Sbw5/_z.G5C]jeuH>;koO+G:ٝЋ~Iz~zMG3="{ Ppxe8M?W[GX`".;J^ӄl#@тt4"cέ댂_?Y_@M D lQhMȟ He}3\ՊH04`ꠟ ۡ͟H\40Q:艞B?5!k,]f,H]$;Q- H8%lO6D4aP W ]N9a ,̀0$a@ @8-q`-"M8 [h-!PY\E' ,%B'™ȅ( )`4J4Oהޡ °) =8!}] #ƄE]b6Q6 `) R EPG4r]|uc2rq`A.@afJejIk10D"64Iԅip]NFTCUx /8 y#fN U0h`D&Ausp*]BV+yJJM;ߏ|%,@l~ L^"y.?UNA9 ~UU0ϥo$W.e8`n cxgGe%(Rz. FFpieGT6h%-_l3&A+ r\O#_84OѤyBѹc䄌J"Ua 'f"Y͏Via;"VoNn2M#)&̈́E%svEЄvehI x$)l'`vIفWo$FhXS@~8`pgB.EڪaT$^Ҏ\Yl䜀:"kA)Z$@y q>ŒZ%F %BI uhDt9$ AhQT$(zk4( j3PGW1i^nEgyhC $Vb6k+D,l6fU7Pe@R M^_,,1BEnITQTU #p|/Pa8fVjɆ,,$=1=q2RVZJ]UiVV'X[a- UCeZl7λ2aI`bGȒ\:^-&Z 2Ly啕XE_Yb[ =]V2p;p"p)p[]cTc0ir%Hp BaPz p ppq q#q+3q;CqKSqoZ6N]pLj1 pʑ\uɥ\, dJXŜoʱvnXr;#w N]s ?Kd-ہ%xa qM]DN@2Br0 3/1irqraA@lUVȞ8 ^_ I[uPB]YE6cU m4O)h3 qZ|0t@U `2JouD7d@mv \d m/rŧFME tL4,02 j4_2c2 5lt&D ‚jG4E )X`-MM9+nkOes]W6!ĐKc(EZ /M3bFԀ"K)/?SĖ>jm@:rYBB[t+hv@G|D$LA4sG_XW\_Z bܶ]嵚t+,ckynņ Bd(Ey@c\Df*bWɼbjZDlT"ll2 bD{j9suR- ^Bcn6c*P F)HÒd'&?wt( B hDWMJS')H=Ip $_bMwhgYtYQI**2Ț\xBp7Vu^q}6J[V^v^!hsw ̜“Fa4"S<&?-d67eShl<|{|_}x[K4cplN :bIq#F >A3dy: ,Uek'(nwy Uxsώ >0wU#ק"A] 7OCg` Q 7Ah8RpcND(*ЕyK,ӭMl}JX!,FўAH·bto7 N jDZtgqNRsew9S:92K>mzyvu^;ݼ{ <ċo|;=ԫ[ܻ{>ZX߳?>ۻ/E> p~`~` (f G`NHa^Xڃnȡq7հja&b*b.c2Hc6ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fbIfffjfn8oIgvީpIQ60|҇`%fx!5I\&$CQuW j xD'UDɀ)[rtr!M:$Y>T[#NQ!> xNK-')M0|~yfci d O7nM` x#U'1,8co+Eq5Nі6[mq-@0B *'j+4Yu@+O݀ [ mt; EAcD[;5H+4[79 ;@)Lw} !2ySqu_MpvSo/k!@HLJymR YhOȺλu>_MAzS .Y9ܣ`=Ps-亀]QUxpbi$ gt[N sQ5: KNx&ƅ ЬO@7ұei"*ޫjQW 4g,\ |0Aʑ<DDIG<%Q M¥yUXL2V$`]T`mOjd` c6T0cA4"**7AQ .(&Ņrqe)[N'6VmN %UQb@4% '@I?̂FPƾpC*лDL1ws!s!Ȱa+ !44p6o~4E) *hQ`vVN@L~ #ARwI\ M(pM7\ PNd7@$4-hvwz2km"WHPBEfAЀE `SyIM,$S`D??F@,8Qkl "J#Q ai&9հMNƊL@j*٠Wsx1Ī ;^f 0  eYzk? ՟F =$6+.I c3YPT0eQ.ajSͲr PcA$̖<цMggVmP3 e&ƨb+V6h%53X#}jz9.ر~eBn8}܁tOQ&{N7Y%i 7;!y9vt [5ȹ_;75M%m*_sEipK}M "I5^x ![D֠(h$F I"2f`6}H!DH -VWt]aji9M5+Ṙ 4*(шۖ#| qh`KlJSmlnVk ޏ@oXv\€{v vH ttk^ 0ZIXu`a Y0,dWq"Nٰ]Y0@uvv|`;/Y{8( <7xX|h$%Lq,<f.KT 0P3x="Q6*W> h2P'  Q1cE`S 0*'+Q57:"_OHi MX8*ѓqS/FfM{siy8 t}0Hh ȈȈ舑(X|Hȉ艟(HhȊ芯(HhȋA#Ն ġ.U7s fDD\hxSn)'A-G 6+(٘)01(Ќ@ s†{aC8r1XָS\A91TĐgwX,9%Fx6aY1," ؈űzāaAqo`2#N7{QISYh#H8E9' Ci?IZ=9"fbeG)Xgvs5A^X;\) ]ig4"(!aLPnteVB,VVJC Q*ɑqE0#`!$:Zm*Kb*TIkZI*=>$Z)Y7= i=^jQlWu)`HŗVi XUGL[Pa ytQ8 wi9Rd@1breHZ41(BC C RР` %DR~p'OQ,Q`wѨYcB9CGP3WpٛGN5dAҠx # Y\f071a5М9(8SM-Zmᔕ A:8gfUOQ.$ad"Et ;יt:@cqPryѵb&?ZE˶aeUC+ۧOip`/X75{SާNDj~a~eO"K aecvhУ1tv2 _j;0O˓\e?C (˗ z>w a]>SQK@Ye)5Ր*Ȁ%+s0lb@6˛ۓ6EhT~hd>w҆<5 i2q6b*'ѳ"0`OHs/7$Kij$h gEj"0+R?HE1 CE&FmY l L,I pɷY}wh 6<()lp+p,rb3`5>7\Y)h8Sɔă{˖hlo;Qdf{ VvҡqՐf&h w5l_:!~;]AUɵx +KN}՘1Ԋۖ{ j-0n\ 2z}c x[P\2Ks5k gU.0tv9m@9p%vO}kʰil "l\|+˩dN`R ݬWҹeZ4q3z ^q_o G=92`MYWF)o2Х}&s;h΅:QpIڣoH3] A37;hmxb8 BDޱdSR:uvڪ:A, />@~Q w:we];ȭsbdl 7H8H7xLV,`(a؍K0?e+Z0.t|){DA̫zPwS01' S}i.pHIB@ʚK<1[4b$+͋E4P"9a(@^bby XZ׬@|ՍpI0u[ (kW"(0MV+"M=?wWwӚ8 Q<@ג7J YUvqu0JO[őK>9WnGYx (ʳ\z-#$^bSX φ#mNi$q;)Cq =WTp|7xyk^}^p\iP3z,xq~tO"(9Q#Ә,'l"@y p`U/C1"I*(;޳tl4c{àdf8Ξm#2me/0*u`WFR(0YGA$) H'LPv h$},Lȩ{p8=6FPdu A0~# 9dbz1`X2ݝpZLRDŬ>j+^~lDc ]S(V5OF"kurJ#HbH)N8,>qB \KIb2[px[NIQP Ӛ='>#7H90f+ޒ#V&rg8&$hE:!7qOΖ;ZΖ5^+eKqJ%~0wǴ(L$& [&՝/(P,T8a$A9k/8 q9XB(ϵn>V€IbPD\JmUoDFD/yy={ E*"R^ZMXJQKl⬷2}./)ǟMوh (5p H%B}\ $F6 "CXBT!;"ZGCYU#ilEpx_H4 c9=^Ƣ޲ZC-܏?Q 6pJpFpGZ^fvjT%顀.xT O`${iUo'KPP:a@`*ޟ@Z#Uy?;H#sA&#qkF9~,NFp7p'[T/ki//7ZK"Hr:%֥n%G(&EDcuU\KSt.RYL A@vچTF/e ) ֠ % S8A V 8( .!6!$. 1_E!fRm~ an`ێ@] !!A:!z 55HS."#N6"$ V%^"&f&n"'v'~"(("))"**"++",Ƣ,"-֢-".."//"00#11#2&2.#36384N#5V{ɀ5n#7v7 @6~#99-`c:;#4z_Z}I$_<ЖU1@ >.$C6d.ٕ1y+a 8$GvGvv=^whOy|K$Ln"?ՐV ټĤO$P2a WhLWOS>%T$hiC XY+JT~%X[i9]@Oy0_S%\ƥ\T< W@ʼn,V+\&aČJU% < a--[&eVeֆ}Qe&i& eQ~x $YfMMi&m& &Wʸ8ڠ KS1(0Knm'r&',YbM`aB*'vfv^9Oe{ ?,Bv'{& &d{Ƨ| |'~eg~~(6d^g&.(6>(FN(V^(fn(v~((o]ya.E"bh (@hJbb^rgN fyHAOV E*ra!@n0-JzK^Ȱ!!tE4*f?`|4d. &nTգF@Bg܌b*(Nԙ8>ULyhN6BzujJ-BŢ#0p,E흏441k \DyMp( 0 ?- TwQ@:1/M|M2ݱlLªIV 6'[v J""N,e͇lV^0B7jdODopTBЌdoCcRudPllFSH5Pn&Lzψ>^ce駾~`>>Ǿ>~07kg# +?&?W_?go?w????/!y1TABy0/D 3O?$)@0Cྰt&ψk8n;a $ s$l ;k, EC  {9UF$T:`Q35~?B70;E64 ')4@Vٵӫ˟Oh*8GjB%P]%] L\dZxß uء W@'iH,T Wth p[IUbq(p y8̐*V xHCZ\IB|B(П PR@5e 8Rvޞ|^Vߟ*蠄vvsJ8f@G8޵?QׄS)$.AA Fw4\PU^b.BY.8 %`;P8#j0igҹ= ُMJRQZeò/`*o5k0!T4GAMܻ)&e^l'ʡ8B+= +5B7C )rM7;b b4J.c("+}rѐ_sXip+?YX6z2s 8Iʍ+J־`@tJC =,R Nt+ח<%C2FviF.qЀ)UsOO׃|oȘh B|Je~ օ1fINo#LR[,L_ڙԝÅبLw}S28tv bAlik]( ȷUBLQ_stmw $%A+TY `:dҥMWA`M:yAk4mRJ_f!%fUg*k_H>R+=MF}!sǃ4+|,y9+D֑*ZpDfLN؋QJf/D'gV p9dK+- 蚅FĆ2u,lPөyuhX zZ.$$SBVt4@^=[S xEefSjUxLچkejo)*Xβ,Tco) I.-ۃy6;َP,)䒱,l"rU?)v׃o )V&  \,iw}+y='׉[ [ ٲgMk|qbP0 4tjH̲eK͵[fH ! -NlBGN#JHɉ UC>U>A u.} E;80߅\z?o.G v0.&TV&wY3T&4&m%Jb%JK] /))YpҰw}k.#4SF*-7!#=w)_g 8Xx (8؀x؁n"8$8 x(*TP.0X ,84X61:<؃>@B8DXFxHJL؄NPR8TXVxXZ\؅^`b8dXfxhjl؆nYo/> GUapsx~0Bp8H(S#D0z#`2g %@OWQ{رb2Cmb<#*`}p& }3~osg, i0# ~}R}pXh()0Tb,06hUxm^m$ou/Sr&/EF+5w=W/6]*d/sVF7!(aA8*elࠍqrr4Azr UcHuon񑄁? pKT5;R\5` ywPtP, gz} piJYyA2Ő 2I+}+2!p0<\;T~A.jQM(6j!yeOH EW|ٗr.3@2Egrw9{px~d@;#Շ{Cfѡky$-ŏ)@1s:Pq5cX '2OY_60 2Q16q>Gz~~Yhgg99DRi!x`)WNw`2b7=~2C4:ITwj݂vR< 9d y4Aß*TrbV-ȩMi8xd1,Y<~sϷ ZrmÇtx4\ǃ*(#>Gah&G0}gygCoMqEV12ţbPpE`,BJYZD >`{3xp:M gP LoGso^pS$1/ug@V =#LIT`+3$\Y1m6ZVp$sy.܁4:P@z +RCd"JDۄʁ:nQw:Diz#GG Y1*f@_g\6PߚX0I:&KCJ)kʴjj1ÄQ)EqvfMI&4Ǚ*iA{jHCt0۲ͰGZP 9 \6PdD9=RDp*ݵ(4nuol ZYd(/Aeِ9;P*RNk>&HegY+k[{dG=ĹpoIs9ʰzSks&PBl):Xe/]0H̋Wd wQʵ V>sNvb6ԉJN_xa I}[ː:0TPc 3UxaEbhUXsmc@{^ ڻc5(2v+s$b#@o vu3ހØtVU6!I?fEfR.n/F[aDpSREg¤עdo:vSldgp/s[o C!!~K7ml0=y`nL\vZ;U7C7P ׄ34}%t\ ,bPnlJK)wU6zAlAqX)9*ʢ2i̙94^iW BD!jpxiQk mgU78q 8 1yxn0um#crQaq#:q_La&p'CO  -LNFp)uGft19~ߙxu.mдg D/O m.ټiz'፳t/T8MͳBV6j1*Q0q'{*`^Q{7m SԫZ%&KF,pwx7P?˙^~ >^舑{ ">$9_]*,.0M0(1<>@D>VvYALNP΁@TNUTQ\^`^@TnC> g m6b= _:o#s a|~8dN 0;UyQRb)K9DX难lTU+IN'TB;;1 1n GsJX2N S^>觾'B;.Sf%JM`a9ھ~NP ``ZP%C4C{1_NP /` YW+C~9 Hbp.kl=fJ;&(.oF^ /Ֆ3TvxOG*HJT3fA/ PS|/Y-i$Kn/NZtnt=Ν;2&|?_|RND%sES]vu!oiqO';R¼Y|&o]g?K/iw;=< ]1+"뵿ns߸ 虳B> _Goz0t5{;#R?ib" 8y±<ӵ}㹾!h<"%|B)jb-1l>5~9n= "&*.26:>BFJNRVZ^bfjnrvz~ #'+/37;?CGKOSW[_$84,0tc$x, 54XM@9pC Ȣ- o Q(g.do$ɒKYHy"L1敜&8 ɦNAI4  Ґ,ii V;#b)CA%]+׍m& Q+R`4wYvkKUƎwA@ *HԻXWNNtFEh[0Yc&v9& p`ժ;,b4o$rǰ (8ph{[3L7S*`[f4S9@JllЦf0l9 a!j(ɶ@Ev0U` X@L#@[3 @j`P  b}X6]$*)a˕ WEX%Z^hF_ouŷk{l6$T[ >^ $6Ahli @R zb >G8nࠔ(Ո& l9(ph[%wC1T`rR[)Gj (=@-aO0QfPUبDJfnDese6i"d4a8 j*˂E' 6B]^r- 7*hWW!n<`5q?+rv0oQkS`pQ]$0, ɺ; [sX* l9:]@90|ʅZ N2oX Y8ǪS094?-1S6 P7ZO/9NuכsnR* u 'jkg(J*UD]Ng!tvr7#@L`&{nSOl ^jz =nCmB.mhz6+4 Cf" 7.U/ tƀVl;J`. [ [K$*? 9.0BD6*QfWAP94aC%r3XǪҖB&A˞@o--3A̰hV"؋j3*7F0S!Dbx5|"!(uHp= aj pG6OgELǎe,2*>YKy=*lT yDn&6! VU,nӤ1Q'Fm2ѺUj+YD)$Ĕ85ܧ(pgI)FH" @J`OR y0<^NJV%Qҭ|S܈9L9m)HJ)@#B%Ґ:E&|j ϣ"5M=<)S%(( O`CP#o:Lm~Pc!@Qmi<ESK:n5×'S4`jf+&5,e+kb6,g;ς6-iKkӢ6]-k[6-mkk6-o{7.qk"7ʵnA[txpڍxHTs tͶT1F^(s4(BVApd$o0v̓ҬKl޹޷}ҨhJ)Hykac%̇4_$ f_<_;Yj[,XyC4v`Cp'L.r-44O>5:*qn ,(gQ58sJy $Is :yYHhe>ewif--gj8d|t&+dgԘ0{ AkQ-Db0)A61[W:ֲ5km[:׺5{_;jlE oF~DΚx35h].\k!2[E#]vR(X%QRZgzY moP9U_iHjMa>I[ZucTD-Ѫc(WZD+"8UP^!L>M|Ճ*b R jqdv{HuSCDr("sdB+o(ౌv6*.x6*tEiPbăWT!Wx0HE-(`{N&7$Сĕ=`7Ӆ*Zi2`=bW9j%u $K Cd:ڻ-.P<(hq\{G< +">+_kʩ.앾QDZu2XAV$W{4JSDFF0:`^ᔤ ! `M_a'ڂ?D\y !E`c@<\ ^DK"J"aՓT/bFNb^H(z*X\m(N"&0ZZ07(HWġa N %P]l3P@*. JcЙbl uC~EȞH_"#a y I-ڢ#1$(KJ0+A"ޱM.XJn!Nu"5bdS$^ƤD }LR Jɐ3O֋zarAvYYM|YY.9|QSXd/B jj-tݓX1 dԦ{f8؜4' ޓo,mNgDEd'NRTW^TF=GUMBU{RΘLѷ) &lyԁ %uqunJC9J|JH}FH|n $&@p HX1% mX Bz`uD-@gz5dD͒9bU) J-}u5z%A}]0 ݂( UT9Y2WSLy\YdwlGT5ÏZ2 {U)#[44!pJkD͉f L[)΀y*馎Vr꧂j1xjj¨jj,ꪺ꫞B.W!8, ^  dihptmߥxH,Cr9@ʘPs⦼`2!X)O0`()hzUpq|5yE{~e@sm~>snLp4jL5sdgPa[~Pl)W(~={)n7)}X- A(.IPB\p"0!! ,\  pI)8ʧYׁȑYdn3r~|@  5`<3 FcZ;dd4qA  CS ni9E77^vRz|6 t~ sSr~*3,Nu&"*O!,j  pI+8ʩX'ؑɡj;<З-~ @)\ ?ZpX<&aK@ꓵ$d@P*byM-K ;!  PBZmpz ,H  q2.*&T*vx.~8!,y  pI)8ʧYׁȑYt("ڰc^FDc0<$ Xs` (   u w' ""_#  "'   #X_|"T x ,ݕ˔S`Kѓ06 E-bYF88&(q 0@&lfւ JeJIA xF5 4D&3 `/vٺb@ 0[LjIeA 4;8g'ɪXaoLId R1GlCԧ 1a u H".CH+mJp?E߬S0\gPJ0<g`NR0-7%}L5$&/ 9O|rس4 @dPE0F]b!K##\#, n$#"vhq*M#|"k D ڙf,1j) ^ n)wD!, `&dihlp,t ;@0C ] rl:ШtJZجvzxL.ϱ{bB4|  hJv;y?Duw={}: ^nyȁI\>G =£Hys>E%>A}:掀 (@ \㯢ŋ3jȱcq,r'[C}}سwKN;8bAk_Ͼ|xmXxh߀h @t9@^v O pC$ބ0op@8㎗#V&@Vo,Y/>+{TG@!0Qi, $Ck!љm f E8P@ }ya@ X2x,ɀ!δVP*ro5t=İJ6uC/Kup" 'xYjBI( [])'f!@! IFHg=76]QI5JLAP83<$xAz}fqN8ۺ 7=wo D1Aje_$׃y tpK 8H.hp0|S x<M BNv\ Hp͡ XuW&>#`'  86pH:x̣> IBL"F:򑐌$'IJZOQ6IQa򓠜&=N*XdJUje"@b*2"l9B?Q׀^o&yi˖?&tm+!Wc"`uԢzkߌ,&! ! X@BHq0eda^IF: 6MZP|-+28U=g ୃbNZZ.nҞ#`ҞsTdSSg0VlH*g +Vq Ul@bi  ƪ;! ڽ$`]u"m (ԉ8`ޒp8@iH^%@8)" XW৙ P,@"t7 VIU+[VedOq?1mm!6ARq;q\L|bpA  ْ'=k t@AuQ; 44 apTN8TF\xae8R낕&^ د (W>=kCXSrKWCe$X"v CP$]FF_+z`BZ@oЯEc܉^֒&BxmsU t͊I+X\q h܃9hD8m:7M t7IsG.O `v ""8 xskɋ TѽDӖb>#.(@,+c6hDJsh ք`]EX Q" ' )),.;4Ty=;ݡ*BHs!J4jpEgV,cKRegY 1`j;θD="٥yɚ!עs r':JPx?fzAo5qsyrpV.Q9Uh(a.CD RzޯF]-ܩ<vQpL*.W!\ge# h$Ao xug9xU^Ȋ8 E\4hז|Q)Ony>%set4~j +F3WQY5ӛ'#hn`/m::#f rTɇ!j?JB8 ks}w=fenn D W)ʷ2_ Q6`rv<: '5(R]Fd?q FgwQWG1a~(g[G!4H$@4{^-rv &0Cp0vWŁKW]ޑ~"j2o 3&N?$p[V*kj9B`uMjgeBPKWW@PzP n!Ҧ\Q[a1nr85M-2Z/(0b8q^Er7 v g$gJ^*ol RaCl _e$Es~#qQa"/s^6ʆn@ v>f5tOClKfn(8biK$:W8'y@d$!xXrH{&g[FUQxΖm+HeHH(g}"5qW X6˘["i~67ɂG(QL"byZGV~9D10~ꀎ Frrc)}2e;MRDf `|Hc8"uؔ?Yn4~ ֑gR7b?򍸕(9,$]&AX26bu&jqj:e\ۘJw3Z?Q6UNncS[p##\LB6流a{InV!ZĄ.@sс(;` QaV|C.1%BY8,)+0 ~1_ЀJ)RPCrZBW"p 0 #YbT#s6$EOTPEP%YRy`Knvlof3o- X#s }I2?0mi(×}"6`-&bj+d*R"@?`rm !`RV))xQ#uV5R w) 3/G?|4H` R h4z5PKXf)E !"5NC/4|ZG`_y"F ]#*RY>~3p$M3pOdCNɣ6kҰ@dWMde .zm Nq*$@D6`LʄNN9yZ-fm`.cC u*$Z{ZEWK_rMkG%6O`ںڭfj 蚮꺮ڮZipZzJ " [{{,R$P[ ;[b۱ ";$[&{(*,۲.02;9eE" 4bz~Jl-EwYA 0^Z[p~#8K +HM“{hjJOY/p^zdzX`7[n*ڦBOtrS#qW:5Q&do0a+){+P[0GU೾Z0TLz. ٺH"V2 _2nYHBPpi r!M[Jɧ;[zE5w2/)S@芺j_B1dn bdUZY]ɡ:h)$S5i\|/;5sP78Z:ܴ2)1XBAmŔ-Q= r0'*e(!BHR81`sM+.|<w" 'ǿPŤM@ v 2RlW#D"0u %DNf:0s $:t|[+BKQg!ި)22= +։wI"EÎR\ɖlcþW6+[-]EgJ@cj75$hInJy\8c*Y˫-g}"|e$s u`f~TY&zͩ1t|hT( 6`c6հzgWOcsX@Fy<$ AĭפXq6N $fp8 xD4 F! :$(R\ּx'rreާ;2=|j4$LV=@=†8` 1rs,˹*˝fFK㖼Pj(23!s`;sa$|NH*E!\@>VN)BPߍ9N8*e] V @Ax8߂#' ZZ#BnRQ .(AG)&+=bJMq2k|ʢ"bacE hhb#iW I+^(YMlN3z ~I8@dc KCL*&X"d mzL%{7s]@>t?^Y͠Qh90`f6BZ y =s^(fh WMv5YYp?GuU8y±<ӵ}㹾!h<"%|B8)1l>5~un=Xem"&*.26:>BFJNNZ^bfjZ>XaimQʄΆ}*J #'+/37;?CGKOSW[_cgkosw{ (p "Lp!ÆB(Q&b̨q#LJM 8b!   0Aǚ6oI(T1J FCqWG+993ڶokr|}n|q#O|=ҵI:%`};6"{:ᩋoT]>b=Qbf@~eDpՅgRE RB xRt Nf R#X{U'ĆQX@b7YQԧa,~(G"IU [׋@5&[>HI`H#DP] Y@}T@iɥsPq't?g) Bc% vM"jT2ڨ<=P^2۬BR[brۭ߂[碛ۮZzjHҋDbp)B`0L(lRĴ=BZy$a p#JLKtOD̀%oq  G%O{^!%/ћdgObz$# t`zF1 9OQvBp$@Zw,&  &hM&b@qO@C'G Bz"D( 삾dP.tnRP&E(uP6dZ㓣G FSRPoɘ n1|)< s6`M8?hل?4 .'- t܊ U@08ڇO؃r6X%+cԇx,ٛh|OB&,mF i ?0lgMa>~T40O~u4ℰS-W=N$$Ka."'|) Mw?+ ]2%DbXdS 5"-؈PGl,-IKN",oo0[E8YN!`H!N0Ҭ' @9 ;@ R(TPdFPm)(~ 9'5Lq4lzR D@3 C[!l6̤tkS3Fy @+Qے+ qv۱R֦߾lҵl(jڝ("i)v’ N~h"xPK%}}pKҔֲd!T6fLfG&/f T(*aSCy e;`#~%[Tx}Q  $W%5LRAq 349L6!pF;@%3o(}׻E!tn%$‰G6*/9'6b1>h KR reiAa./9j#\A >;E0s3V}a"S5>RJ%y—ntO)&rԇlzs\j/? KޠMn᢯>aʃ@N970(N|m]2VnUnoBx Mۉ* Tąyow ^`=IPHT1A\%T.ZllUU]\]YOйDPI>?f@d_dA"dBF(dC:$GCJDRdEZEbdFjFrdGzGdHHdIIdJJdKrqd(NA۔LZdLja%&}į(DheiH,$# %VD%@\Mj QIRBVXBpuAM>$ bXX%-֑sm"SXSle,e&$fԄ%A% &Gb,t`5m T`ed%\f]ffl&&fT6&&f!Mg, YJ&Z<'DYfo4էU PԶ}M` aFQʄ"hբh%UMXM '%bS^T Ьn]w1B^} 5vȔmʥu"2brE4@Q&r)'j)#LQQ(fHuog@c䧁\S0JfG윔 ADـQ8|hL1QQ+ʥiF-H +8 چ>⣢N N!zI&Lt/j&L VM   V !-TӹՕb)l]Yav]d`VTAX~,@z+DjJxEJGq"A׺B۽>IÈ^rL]\o@iA H|ĊĎiYGLy4̣tv WIV*1 E!uHWJPB @ d)9KW~ IJAz]m/M!lA,TT)dJq(RGqƢAiR*#)E MJ'aHcHԱ=W`!|Ejl)A.B=udV9! o(yjYmjm!&؂.sY,ۮPdAUAz]lVXegReQ]pL'U[)!bܳ9d(VRZrjwr*nt&paԂ~̮ͥjLZa 6*l,[ ŝ4I^`DVF!k14޶)mubjeRWjݜeFX 8^S| /*jt#͛}]D!"ȺՀ'/LMŰY 0psrQ(X{>׷ޙıx䌞ixg F~1.F:߽,sVoj1pJVZQ2Ja  a ǀ .7 b@ q*r}mn""@#7##XfĪ%]"j''[V(/73Υ2ډ2.ò邉wfq-G-R2h12Qa'XYP0;z}=荱JAe/ |a\oN4'o -G,':_Xj̲o5AW&6%0bSP]s2B[BORH3nGZQQk뉵t)fZIu #J ܚʈ8PYtj.tg]QBfd30^!t"au>ju| P)3]tRIlsPkȁHb4r͵Ax@[q c9Ps3VujSJ7f^ Q'r6DI6IvY.4m!u3JRUXHRhrsiC1Tv k]>ݨ`dЎlEc ^JFLL CQ6]" H]Q(HxA+ WTEl!k+uh,t,&sρ8֚Ht86ϰY@9xRB"͜;{ bO*yTb&_2ٴk۾M_H0ΘACW{} ̛;8\@IVӨCg˛?5Gۻ-rۿ?x=_H`P`>9`^ana~b"Hb&b*b.c2Hc6ވc:c>dBI${dNdeeO^eUy\ebeJ~i%j&Tf@%IgvV0$p)ax F "u,@/Ii`%fVoҢ[^j\}@Wo Z+1`Iuej}qP_inVF FF_mKC mAnl o,͢[Zoqׂ_LqƶfTo ,RY-Kre4pU_1GCmI tc3$΂² *rROdE Ҥu.,TM6qtm-#bw6kY$qAF (c}*px,ȼql£FRx |]8z)Iz κ'{N.wˎ{{@|N||/|?}OO}_}o}~ϑvu/ǝ>Ϻߏ?3~%o:IyG/cd08J TX`E0daT/ Yl 3 H@7,P/ҕ'a|qp۟޶F(} LX&FI8DHѰ' 0hNf, ǂ0dF. ᠩQ1dI߰D-||6>)ēb 9!*H&P 2L5 #^@> Ja  GLzu- a+@ NP @\c0N_"Ds#ސ̞o[_4y!,rLBCƂ7ܚjL^JHwAx 9Y\!d9B< qBŔ D:6Jb!ezV^Xc78/q\p7􈭰Hn;#b^9.s*N8Tǣ lT?hJtD680[v ,2<"B-1pdb{8n7ZFÙ7_CJ^;k[kmJ *$R R"h46k^.- ȟ8m;aU5 KJ]>" tHa;{uU$:HUx 0@.r<1X)# 3BU9nc>E\!-؋ n{ w '^0o,akLU_ĞfAl6(Ccє3{!t C#($ʲE%ԅ_=GSV&X{e$Uԭ&_H.\ P+3 [hQ/cl͐qPgg=-I8l'8d~V!dKuQq_Tթz$Õ`Ģlme.Ku~ΊY3\>tnkbmmKXw,vkq>gPx7݂x {+ LҐZ9@um|.abG#s`+Mա,Wk? 9rd:u0t^J3{ydD!G`f&l`##1g -KE S&ɸf1- ;2%x|޽2LjTX$Hͷ#)V"k<UK'VNU;+;wσ?އӔڎ4sg@\5+遗Ҫ1ŐrB bb=e0M?j[ qAv0 v4._|$zPcte'M2-`}ׇWg# (,%F_Req tRyCsB<'>x[c\\2tj_iG\ګ 1ig=qZo(KeCcӜeQf݂2d:chҮ7v"aMcD]wqF\D EdJtVpXt9бH+)&2:;n 2sR-k:VHc'K?%)CԲTH%|\rJB sۏmg/Cg1%T{z{9^^-`$F<^B't̊b,jM-F 4aG -8)93Z;3PQ)baa򤣳:?Tc"Qm~qtW6I4M[HšQ)PLs\IPG+,q-'EF\hP3Gu TQ ^KTkh ?,h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*Wl%̘2gҬi&Μ:w!&5Ǝ 2m4EO %%t*֬Zr+ذB ,ڴd8L 70KBuw\+6zk6n8ʘ1` m`E$a Q\o- q2T^fk֧hO[P ÇPkG8H0H;;ҩoU [읾䱒]lt( &= AgTw2 &;93d˖DB6fP#8RQgR]g |)oX#81 Thdt9ȢEm4e9 &9<uZ? '7uGhH\l̖FeuU6)uICS b܀.g ǁ) S,Q-ڹSQj5N$hU@U& "CZ׃AA:+ eX (<ڑ0EZT?=@[=cdGVల0wAQr$fю) JOс"7(lU cRq%pXIH #eư#B3s!\>Eox b+?(nPҷK0_eNGdaDC! 2,\VlssuYlByn@IelLí~ZKTA#Y٠7Ps.;,8X_B< OU( TŦ#s(jlPy9bS(&ĶNCRs < yHDdu1/|897 !(S@Qj"[UF):3b25%Z(  Vg@lkI42#Ȫ0JƓ- ‡v*j:\Փm :t,aiE^#vN'vۆž5% ZX OW@ PG%DPMPh&A=U`3q&0mlOI pQkȦ$ZK8O"z֣e:hBtHI} IX:bEt8.uۖOFpb<.͌Dx}6vv9; 9+F~4='y@F\ joO }65PmkxDjJ\7ǦUNef5+慍 eӬ E8(fn;F-n֧FՌ@|n4M#-qS, ޔ 2qyb7nxxgaqxI6oMDKt6BT[(^ %f ,j4:Jp#Qi~jIe@j &"e8!R3{ }hh~h8X!7ԣΈ&hzL3pC Pv꣯7@꓀ 2sHz]л,) d0iy$BzSq;E!萰B?L磯MyO< c=hD_tH5 M_ okGkP6RO2cwJƈ4"kص2C86VxW=>3Ox1$LI!we;q~m#8-LijᅱB ȋ - Do- %5N >ΈLn vN\zx f `Ga4   !!&.!6>!FN!V^!f^anmt!!!!!!!!  L 6#>"$F$N"%V%D&N@~V%("))"*b&""#"-֢-"..b@+""F u!HpɵAL sG!Ar6n#7v7~#8&aJ1L恞YLH[mHA8#??#@#d A$&.̝:s%#\,oۙE՗+ )Wņ$II$J$"B$9*diUIC: 4ō'^J%QQ%RvK.e+^eE`H@X ScDپ(lФ%%[[%\0%]ST^JX^jS <TZi=La՚T\N&eVe^f)#]$'^Te^fSd'Xa$#Mهcd`t<_$f&nn&yfcgFP_JUbܡɎXfO2kkdA)ovw~'x S^qTd`EFŘ5ܤx'~~\"S6DTU2eg$u~>(FK! *FN*V^*fn*v~*****ƪ*֪*檮**++&.k %!4UA1+f+i- V[XJkxAʕ`a`.z ukT+JW~,a%n,0 k(zƫ،4,!ʂ—rl`J%MiVlvB3-RgЪl\+>-j=q@ idr&!z.Oi y49mV"َ]QN4.&^G}#HG`hٖˊ@]0 <14 BdB%Tz 灗pE >Iio].o2,h0~qW0F֬%=qPI!e^9˲@ /VL{\$[xU+Ģ2( ]ƚ/ C-!m] ίĉlW3Ʌ+h{ٗ!c +lt%0j#30-Zj  T]e0EGY~E\+g %  V/HH2L|MV|s COA9Fq._.l~tZs&:+F5췚H4y0+DX\-#XP<u STt3#LPK@{3r"Bk8CI-2ݲ1"{4H5l5LJQ kT_d(TPut$\s܄CMaP:ǮZcb%@7#hߪ~]Jv I_璔,DžZ(#t(+n^*wcΡZY8YPPs˭ 0sO+h`S6:.#Tzz| =NHGlPZzP:N'w2/xc-ĸb[dTkSXMw(fiq`@>S`hz&Qjz1L pH,bjTT/SRu4]@Yz蘧@zy`Ur;'=wZ j1},W&y02tk{leP#Sh'-) N) sj2Y:HE7|(P([c (p5bE쯷Gz2ـ<*7cN H*\ȰÇ t뛶})Z271XHBဘdjQdi_ Ea0)sc=0Ir(48@KHoMk5 J2CߠBnh/ݻx˷߿G316d["JF(UK\QLIZNxSsc&N b1rd<@W( 2[FV6۰`M5;cسkνsO0,ˋ uq,<<Oe "ȭ `+JZZQ:i 6oG Q !\ =9h|߀q kf ,`%dH%yʀ8m/?)s1ml%N~ IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZT%iAn[.Ib^L8%(XDda&%0 /LaX !?D+:_`qt!7p5UQ(f~W| O$P PH#[yi"zErjqnCˆJ w[q{L D f&L5蘒fG+!UCPh6t9(L4Q+]t &?vK2YiCMjW+=7q!9rMxkZ ~ڭ&@}Ql lSjy2tO8QBdAM,Tksz 4r<}V2,RQ`/L|HgNs,aޭKR%+YtD!Gd< -k6/%{~RoC Ƃ ʳ AzЖ,8/' lZ9xM+^H8 @n+69PչXI\%F`ݠU.C^1` F^QEHzo 8s1;w¶\PvzK!$W΍g׫#;+Dl ~h l@[V&7#hHC<}x*rǥ|~f\D49G} C7R08ZSA ;2%?te\TA2N yׂ\pr?@;gƁC19Ou, 6CAͅFxHg NPX RMVxXxyPYb(T=s`8jI4 AɇiȆtXvxxz|؇~w"l!xHi؈`2ሒ8FeQʼnVHx!xK؊X8JsI؋XhHxʸzFX`֘EظR48H1X蘎r؎"8wY 1 Y А9 YcؑyVđY&y(9V"C)02 ,Y3:i>+!!,D!, 0 hl,tgx|`EȤr @BQ2@ixzgK>5K߫`t~$d%kp+:%\3 2tvW.  X cIfh4_mo.Wr5]asG %t '^jחD+Qn11@>*X@I hc19(;xpDz˗0cDD8s% M!BEuvt@(* @¥tV) DkbQ`ٴ"fʝKݻ4KUަ|GwtFJ|*cŸ$(YpTr|P8װc)B@(G$X9 `G+pS 0S,fTȞD3G%w'<Sb3rW,KzE`*d` tOd ptD0D(1" )] ̅ 'CuT@ 4p\U|0S-w)fctMFxCrFY nHfxg [ @?0Wq $(@}zP?:Y'_OJ5-e_T1PQbIo~#} K `3+3}(x{C9 \A$+Bm\ɂf`n(^{eCJX` A!ns |qCRW 'H3;oSJ]3H$Da xxs+ &,  G7 s"MJP`ȉ8k``2py}=iEڬ8;J#PB(%RAfc8@J+J {{s]!&*YK2B#*EVvR <1*|%HLn ~9WN,=żys2u" ʂ~81$sL:9H(lx$T-OQ,d⸢)C9j*3'ŨxB^v[^Di 6~y@z;ìV| [)_ä 2"ػ 7G>CBǰ:טqtqV*$`Ƌ6]1geP^ ?+SBi8-|x3ƣ?馻wJy F]}^ؔ$]O|-c@B \ AzTbjZv8 hMװibgdG*묹Vn+ ##2E Bad*3շҹsֳM=ІUaߕ]lW9J0\ ,0Ԭo{[ D&!(G/&#Rt$>Ϫj%w42ULTr/Rmwcq*ӈZ4hc";,{_;P:$|!NO|kJxsnSE@#Zg.!^opY;Iʛ' ܓwr{F4z9*p,LhjVa ;.ApN$xߜ;*9I9LB3u~S KȿT#^jk"gJ:FCݗFvNvI,͜\XYP@S.8`urIPI@=c=Ϥ:Eq.)caRP,``fLkI:Ve#:35f3e38s4в9#OW5Tv76"}7|pHZ>ZPSaw[l!H(HVxXx`NRwwZ8#y",b}x}01&*Up(u& ? v1 "(cm+Z0г/6&BYoLHCr("- Hv&,iױ9.!tvhA#H 20x1}0r}4Vq19Xe5fb͒9wc$pp|bbP!蘎ciU'kR%Jul #'ס7ǁpd w'5 8q4`6%PC#q$O#9X4 9 yx+ʱ"`K2.5&5&Ñq$" #%7@B]6 6%$ Db$3B3y/QQЌ$%rq)_ R&1~GT8HY䨎vyx(|rIw|9eyD٘:e_ y)|ٙ+c]9)uǗ:P ,hy ISn%QvJ+l TșٜJM 9Ys2`zjC!$Yyy Ý9Yyٟ:Zz ڠ:Zzڡ ":$Z&zq4P0iIעk3`U0 "Y͙b|9i蚗\0 (:ii+0J8z P B @-KJ#p]S<:Hdw)!/ʙ0 @t*;|LyI*|k*5F2YeWXQzaʢ!_`UYbrNu=p_zڧopШPo@[G ~cv@zUqYH|mNJ :uGxlP0- ѫ d@jj4iͪuFZz3AW$ơ1>3 b%4lD߁Iv~[T$aѓ*X_) PX$B&(ɲ !! `!6B.$KF: U:" #!5Rp #A$seE293$UH> ~0", L+#%Inf"# @5 #IKŗ}CZO/z [D$ȜkK {{3{{ 57U7K,ƁaN}L*j?L%`+lFkoizX*@LDPI9&̙J5ʾN -,)6Wc _,.׫KHjrå ﳹ)dGUpiMJk5 ]1l:FL@ p  l̈  "P+p̭K7V %.uQPfa֎HAGt}c\ռZՈ-p^6Z&xtnV]?\@}lJv .pLpv_J DiJzZ˖y,qkKte|O$r}W c^NpM:wLe{_N$S-DzFn^'bMX|F|Ve.n Dg*KI>= [cI}МEt5p{G p=Ud3@KA?(ZL͇@8%, 4u?ĦC[X!4T7#PR#cWbLЄZzE{2G< %Y|̜E5Y)8b(#; (u5U8Ŵuo';EzZ"U;  Zmhج 8CO09|%<30GB"6ap-jW(lS`6r0m-urC0".8[He%Cٔ7TmB*B;:+[dR-LpƎC,y2ʖ/cbS H#̺ Q` *tQ+J3Mn,&bo~0d!9jfL=z f jn78=yJ2қ lC>'O5\<?n4`=`4bC4 D@ fnaE jl `CoBU7i&L<dÑ5EvXr_Zdbk٦oYzq^#{&_ZZ"G+*ڨB]YbrکzJ: \ zZ{F+<~=Đ @a,H88!db[&<xcޥE ظ B*gSAN,h1օLzT%2]eH"!;?&/#=.&NzD$2?RsH׆&u$#(MH!H\du %Sׯ9O50tU` K490.ʋ@_#UxSTɊU,q`Ok0 50 qsw61+5\GRc! % \duX)X&7?/9%6R!H XP"8Fs9Ht C'yF$Gᢶ@ lJwZґ@j<*l]J1LdE;|!V 䬆$R2& Rq,yV%N:3 @hV oP9ȋ6! SBJi ua"!w+D7&'b F= AEM|!D t;'6[-n9 ڠ@Pl4Ezݞ2RpxWhPr;]jH*]+u*jbZV fD@ "6[mKe+F&u1@Y1 NPvG% #֪Y_0Ba+-'LB# &1c"/a0z$` EFǸsFltH9O@l(zh =2EF(1NTv!A) (DRFcL d. H"-.`CѢ/ BT}\z יl!EbpPH'&Pf'Grf$\1\E\8WV^le!QC08E0#PEGTRBpD <ਕ2S4f DC4%8RU&Iv[b*DC [mB8EKU6<8N-`dObbtfI­T@! =@T"_m4D(NIdI  ,qΉNKՉyi(;xpp*@'o \Qy K J-DpSOVg2+UC}ә-bS}SQ[΀Bii*;ii韶j" j2jLBj^ۢ:jZjHjjQz꧂jꨒjꩢjꪲjjjjjk kVe # JEi=Jnj,rkNI)V48% O("E B\4F}b*51ut6,r@A Fxf9픖jt+a|e| :=<+k6J 1cVfТ Whe6E }60ƺ)^2`v[Yࠨ,(f!rP䠫- A,Iے}WmLfEXRr5%tF4eyjm#8mdfJs0* 2@y˥#-ImknI['LV AJOBr_`y@\r-(FUV#0@*pa~8)d@BV,GcląU@Ro %l @Q$h$T/IeI4B`QE#mz`a04#B~ cnDxpXj/o+Xp/.ƒBC%TEsJk [>Ք2-EU^Y#<QPGDm &O4TEW, iv GA}"Y']l2Go3+Ot|16reA dQǭ\@&'$zB0J0a Vy'A_Xp7' P+F<]aewRp5cW;g{{\dޯC ,t(i69{y9+{ݴ`T6O+O{=S$W[I^ $+b9MAְBc7>{aлuyU2M˫Q ^| }C}w6oeI)[)M+FP`\7,ou#/E##nR;B`337-{q6G&̤#?A /cx܅ZW,OD+3pxoS۬ vٰ֫˱I|=iF0<Ų`wy@[CRB|d)+&m? ` {Bd4',ssme0ᾁ%m@ׄyhGw`t0PksH2$MP" G.z @՘Z jܮ -A;2f@5(8HXhx蒐bBbI&) T2SP `BsV$39 ̶ C@+y"cƚ" r{S9vLbuNۋErcpu뮰"y.# Tp0( cY YDZࢁR4<2ʕ,5\E;w0ټ3Ν<{ ) A ()  xW  X.*z v0uztHؘF[OaWւ7LQXp@YTr}:]w/Z]`{]P!-X8>g$кrj pPX QVĂ(!7ft m=ṳU=>ۻ?z?`H`.`>aNHa^na~b"Hbihb*b.c26ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fbIfffjfn grIgvމgz I-@M vGFT( iNJ)JQiig.  Jţzqv2ƣڕA RT!UlHBwS/oV6s6U -ֆ_dxW5@0"lv( o5=Eno-ECW$B] 0J*0R) <;s t Iպ U?Ȥ & l8l8PU(wrŀaa8Ԧ,21cAp-<[H "ܡSF}< Њ.7.i4t:&,@k:Pq327$*q`PI1? +> 5Z3HI Y DV2tf-E5q|110fe0 `_(cA頄-Phd8PBA O3B niW($v2QG,OXy3bW=G~SW$l& @hHf5(+! rWIh.LH;n >kkP/(3py" kN₱ 0Œ\! 3B˹֤n d)A F32h1рZ&P0-7hKdY B\h0񀃠A-ʴ¬ѨC{[ o8Cq{(0`aœ eU8$sxsTɿ,*&db{B2 HFi{Ogἱ./'-jWP$l.tk}eN'Y`u@|s\HP{$ fo:[dRB47d1#I79BR Cc̞:!:/ 曻kU+DM8幎zK?HǞ3wEd3,;Z lZAJcV0ID^1ASƠ=r g ~Ю&yO`s y=`ml.oBETkN]5驞ɞ鞨Y)/TWK"eh?|q.xRĠ*C0@iH5AhWZA.#,( 8& g>+*3%77z`Y0qK5h 6UP4+`^C0  GU/ c[ʥ`; Z}@C@ 顔DiG0 UEhsE]D-Jy/}*0bPP@BׁVifɵQa{{*Y*4X䩥ڥ^PયZ`ʨ^PM&BڡGQEm~YGJeɪʬ͊(a:,ѪZq XDJ0 %xp(nWh%WYn0pu2󺞾:,P0Šަū`)Q}ZSW)l[pG&!W_&_*:^KTAsWy:ఀV$7>6p3;[`© sty^RdzO Q+)٣ 51 [Tc<کeЎ74Yi˶Y*>P`A&hK2xr[6ᰅs.8۶Z:˸ ѹ^.HPia$sҴZ! ^F+mи˺)W OL@PqX,$-@[kɛzɼh +Kk׋٫zټKS+Kk狾髾˾ +Kk˿ ,Ll  {jFxq!x9pT,) B~5w 2)$f7,<4,Y lYg A_h+aځ£=)GM3AsęoV?,]Űd6UUT* 4=)P+Ngi2 jo|ƆT,8(A|Lɕ/kQ$m8aBah v(4S eK?.d-, ,<\B =ЁqEv-E Z̧hd4ɖ)I(@`w`P?e2.1]dVS#ůE  6*o#* V˯w2LpD VU`E o=:yL%OFBj p`2 uULL7jI,Lq!LMQu#ҢQ@4 + (CdXFhRwqi'_s!x&iww~ A`B%2q 8KS- maH`<'}kFZSNFHQ<k-I5@C\}֛+/i4MkӕrI,aHW<B]JDTrKk؀ ?\0zD6VفP{ɭJ&HKpIЙrp&@X`qT)?L=\yhkDvGb WmtX9iUo ]!_jXp)ڶ'Z1=3r?spNf;~)(sn`࡛ 55T׽RCeb8 R~|Y_M*nֲap31Bka(M:ư+"fbPKηxZ6_=<W.LR!apPbsւXܝ}4wGu*FݕM7zw TN^lg0py<^~nVaOrylUg7͓yL7'P ǀo~alDxշ ΀{PcCEPr O8rM^XVՃxzn%4fǺ` L8 X3 PMC[q ֹ Ϭ@2nE '$Ss`w$lsvML-s3q"| *CHd#A\D(Ԯx,(`~?፥ˮHђ<l&PEo/Oo/Oo/OſR"$}. ]p[(n``+hvځ 2 =3Z*y.m֏:00 0#Y'+ۺ/sJ8 -#.@;V` z+6r/8,3:^G pP>;>>"QMUʓQd$e%fZU#() cS-n.om܁IAHTClPRI5I@›BBy t36 ‚9A@[3!xgB={ ƣhAc ,X@d kPPFHR@ЀaCA,Y! hҤ,& QN T~q+ذbNz,$A! `.reI"Ng@.CICh.(G6D νtz'SD50ʃ1E!2`sV mՁr^pWikXc@tL^uШh%"=j"$8zu y"P ìr`31q 9?`%3s tN .PZiYK,:,@Q֡Vv0k "(mƧbp ;`u1f &Hڨ;*k`,&c(▰ pK0OW#f owj}((H-{1!)"8* Bc02us+ZB=54vך1C u+[${4EsafI[7D.̴dOq5ؚ] +sSPBW\lc}7Ç΅*续(naVI 'cqhBtYvAF+G +L&i` <96jFqH f($2RPX=+ZӪ$KF v9- 0 jl|1v4G 8ny%\+!JN3cCGh6N=&:5 DuD;UV6,N4A8y DWSZ+r\%gΌM*3)*]bSEd&Kd )9 !FRdfaQ(bMEc:n6Έa]M5k[#e &.fP/.~1Ӹ61s>1|c F>2 B"+N~2l)S(*,f^2,1b2l5n~3,9ӹv3=~3-AІ>4E3ю~4#-ISҖ43MsӞ4C-QԦ>5SUծ~5c-YӺֶ5s]׾5-a>6e3~6-iS־6ms6|'XҬubj``y[xsG"%. 8n&qw*o[f83N D E.2VC'Q+Y.9ӜP8  l8$x3VzFOhNsW"G|D"NL0 J7)C/uRH=$!ӍG[};.Pz 2JQ "▱V3CZ{bA +9ԲrmFh$]mNv0|^`@/P#Q.SȘeө+H➯>v31H R̀hHfyHR%A==u2 =̰IݨL"QBXM؆nUMH<*EM7Ԑ   @TPHPŀ 0 8n(I -Uǃ lAvHt7{&7I6xʪ{ r' Iaoˣ1ʆBo[X4GHnɐ싂*ThwQwne^74-xD)jxE,a$Y&oTL`E/@)C91{9C &R_1|{:ˮ1tuy낈Rc2h9h`qM>5 Kxy8=9T a;w/p% z!:3B<5>f/@+yb,6C ,-F*:. XԒ#y7M 1nGq{Ԉ]B~w9µo/!n]\t9,"sr0NE --_OQ1DdgDCJ\ґqqu4rm?e-9Dj+ُ>闾>ꧾ>뷾>Ǿ>׾>>>??'/?7wBRׇ%h^lXWWڝjYаX8DMy>N`8,hRp\yx1di%"j*a6 0*pH,Ȥr) 4U+X0%!10o2(?X`88, fh'OQ'VX{zV9;d %+ w%dV+$i&#e &d+.dSJRBV*edL^@Q; %B=$xl# `%jW"BN IQ]2ޭDcǃJA@'XET8(@ ̰R70г #;v hTNk#U^ UۻDfuh޿JXo) @cQg#K^$&r#DW[X֊ceWg@.7das S#} JⶉuC 3jWGz) nU8&Q*$ pYѠvIDO}>*ׁ ls$Gn(,>  *qf7Bu'&"mZ:9'8@čP_ClB#}Cl$8#d`q@*$ . Ew&0 p%:Aߑ"a wG0|F h@)ON-m̩:8PQ ntBYB]ɭBrr}#@ap `*$jDžiĶ ::lH$%YEZ 1&Wɀw.Xq ͡-DoӦ pF5"{ښ@-v!3#ē(iC?Qe6pt52@Zc}|- *Xh0\`EK#h5gVBM >Pa} * &O#)~4\{(/L8 7tq4"n %#P-'=P$Zi3@VKZU ⅙op$s%IPd}\8 C}e'?Ҳ6@10+0vNl0a8r}]XLЫEv@:yu|QFP 0L~fjCZ/7)P\G?"C:@ݨyoQB*>y}:_Ft |00⌸ZfrAkVF,|&k(D&R8[HKŃ/X㧘 j~ETQqDW@9X a0w%uF6X>ַXJoԔ\E%P5V)iT`N-͑٫ޢFkقq 3ƳLb}&K ;A !]"8a^LTyD s,c {1ӒЌ r<+UЦ)'ٯ,rN(6SsT0e4Q,G$- n ,dΒ^(,)f5\$+$f n_! 6 Vj(FICn2.u(!QtDØ!:s]P"Ct'.,p, fH4p -u <)E9mȿNҀ>BV6%$-z_{?B?-=n딣0. RE⿉gdxI@/.aP⠸w/~qc*F켉pר0@+\l5a6gv5O)o4G9bn0Sq@-dBj)͆d鴻>4q7lu@ P(O'Q5 4|D_G =Ibx/Yۼ!L-zY@>;1o4ck'Cg4[UGn/tQ}qg2߶9ԡ׿scOMP>8X!O' ؀>Yxrih1 T!x(*,p0-84X6x8x=>@B8DXFxHJL؄NPR8TXVxXZ\؅^`b8dXfxhjl؆npcq 4vx{`jx@uppl6 OwsVgZ`+>& T'7Ispg{#T56rac ~LP6@K <Ez'r%xЊP›(3a݃=x3yQRs<2uwߘkd=D1F#u#i'Z@gX$8` % A=R ~4pKmiј@0"tB"-OHJa7>e iՠqyw8o6q3~JdAQy-5EtN&?Z h_uEdi2 +HZw4Z7JJxI BdHgHH3:' YKr kQoZ ,Y32qKe~L=^Skex2+!hM@]eTytT4f p iu`XwTTy֥OOs5Pt~)mXXXYy0PȒK! ?:R#K'qJsj%X 6:.02X x-,F.]|3LR>T^V~XZ\^`b>d^f~hjlnpr>t^ZE`D>|Jl2hއf縵^ WI}gG%ރҕ sk< 'AG$8{-X@3Ȋ[\A?߭P/i}~N}GL DvaMŦ@Ex],؎C&  '-ߎ~0:}9ծ0ҟJI 㔋Qmrzi!BE#M)h]fT|/3c'PTGkr1yDٯaI6/΅n  /kDV=>=>JBBEAS_1*huO/7})c1]_f@ae%.^dcp)ܢ>tm!sx~7)/@0 ,*TG.()nr+'->A%NX|[]$5i4%`rGwMf"bP*;6bRO-SgQ8. rya7Ј R%_H 8# Q.+&@rpV jO| M `1d)pe$AmMfc6ŜކW=Ꝥ))LĨ26:>>ؐD6-l8 yjN20Dd<\>6-\ v^.?CGKO,8$  ޤu2P(de9*dd/*h') ((GHF\iPX@ t7rp.ED!Шur)]m@ Ihe1EDCE0JE@Ԯb%6.ϭ*Ge +,;WGIik;vC.ZY #_du'GC,aK |Ξ?@*cT@N\X+wtIq (Ie`or[d^L(Nz@zl[$O[L1[ 8 j}XA2(4)cc/@DKtEn۠|4RR(?pT=-PeK )#<2^nH@H(?tbNph-qRQGJ !J9 $F2*؍IջFJ P rTG.i]SHmH IkfcZ Tj6@є/β@" mvxAQF_ohB r0yQ83;c[sFa`Wf(G;я4")IKjғ4*]IkyHu=l_:q@ `O2QE%]J):M'wSG FY8Ioɜ$!\dZt+u/V"1`z*ʵ did 0da=qzP^}R{!)vCyYVaLc)g;ς6=KKsa#׊ڇ(9` 4Xz.JD a3[.ǎr,OpKsi`X.%~"h2 ĢK@;)nSJ`[EPj7/-nc. f "^\$N/ä`5vX M[R^:64 1"088T( > SU IQҰ5|фaUND#aޡT;\BsӖDUw˴Es`rP0 aH2m/ !H!ciNM97aw^z\E<vpzb >Vh}hR E&sVɣ[fcP)rpM$ ~lnL$;慸Sp ^^?   94ov/,4ބI) pWgمÀ}{ <=N(`ޣ00zX@n;nzt6D[ Ԙp^hԁMDmzX5OmX6,uU_{'F,5c=U u]5en=Ӷ_FE;n\;*'˕QM(co#>m߻hD"+oc{<;ݞ=Koӣ>_=[>=ko>={?>o#?_>C?ҟ>oc?> .dԉf%M'*\2aqo)8p@݆xBX1t GRnP^C95ۙ,\ \,KAA8܊h}_M _b_-a``şXBFICq }BIh+P x˹OIG:y(+Fei !"8!bzcu%)l@=[ eU vѐ%] jE $y5 nLaD,WNҕEW<Fh~9G4AAWQB@p.J @ՑMr09Rv@U,Č^&h6#n% n8]AUծG0Uh VFmt] @S (.xL$@B$c 7>AVIc5|cC']J n"dc Xܔe\MB`j 9f }t m đ,`G"!hUtB#ȅ5R4PPHzUY P|@A*1(!h4Q ]^QBϙ&Ap` \E,GNt'ENѽ%UXS]aa"@!gVa-Q c@!$y" F>,:%! l*I ʅG@>`yLAHYz(Y* 5#$yf8mݙ5HVwpl4?AwM@) 4KeGfgN3 @]QRiP'8 +xB|-BGF?.,LY^lhv]la\`|H΀z[ʧR)DB | C,6ßG U%,B E@φ\C6bJL^rD& J̀܀mH7mHR%V5G[u@0(!`:^a%@DE]څ QyAMF-FTJ}i) h( lPvU=)Ye&kMAe9n܏!HԛI Tg} BXFt0j6BlBHwxMca)AHiw"?HD5Y[mԀ34Lj Tedѵ$X bTHWMۆRzFCufpV&ЮzpAFak^x/d<RдS~ 9vQC4/ %KDlB[e$H4R0O@n9ù*b|] 0ll?;Sެd=H_- ,4J l0"DB,B݈eclDB _@ w(^xM =+=-n@$8E} }pn$ AGe!ΗHT P4LWb|7XLr 2rVNG=T 9ޡehB-@Zʊiy$KN fiݐʌ Rvj'ɨ.)P0!ˌqdj C0Zrz4S$3My,nNv̻@@k(eECp%0K `ovUi\0W6 jUmofRNT%p?0ݢ0^0ZǖPB ⒟ c8SiVGyRNZ:$(@ơ3lQ+Cq z)#"YGnGyqX$Yޒq0_N&sTfEOEGs7sanӄ ȷEm0? _B ߪ ̌6h@L1/onH  -РZ˻M&hꭀ\7 LpP,q9k.4hȶ^)M0_iz9W4ZL٭(sQb SB(aMcK??{2 :n"ƨl'vNeLC6{65E Z:ƃT2]Ed AM4mLsKIXs\NKNj(ӓOFYf} dP2 cTHŘt;Ap<.CU6B%v/zE^o?*sWv jg(u'ׇvgSdYn[kH>ջ]$d2jCQN'-87vqD7#|m,,4vq)ZW8#PulL.NXR-WI`rh4T[{Ss)#R61\BL89h8g#fsSj~K| .Dqtm5Ēm}H'WOln3KУ~g#G bٚMyBPsu/{{=IN..ybtttd'pZ/էy "Ε=5|w୽ ӹެ Ȉ [:K[%Mȉb`i$FIh ւgzz CſAāJ ()0՘ܙl%gDŵCy!̷M2WK!<8$^bU4tpD.\$Sm76@wLǬ[dyNjHb&8ET#'! "w}݋ܭ}pqZ ~0SD,}X?u>O>S>vJ' %>5E x僾E,c~>^-e^m4Xƹ~]VE<~~ #+3;CKS[cks{8i70AA?f񇿁Hɫ_ (4ÉbJB\0;JX)Z`v"$Dp'jcqۢlVT!58x訥4`Xiy5ss ):JZjzw`*Z{(0鉂cs yŋ#l P0zc `"`l@k&P6l|3R6σ?$  8JQ:|xX@Ճ1Pc#R @x sBJLzDB3`-JHyˠ` 68!wf|AE C8v7R h`Cŕ58S(Ɣ+[fL}p!zwcɌd9sAVPveވ J, Jߕ/F! cFQk{O ΙT^:JGĹ0m{ ؏ϰEă(<(tr=@ b GO-0,1!w0}BQrfX`D Җs5x8X$,~2@77(1h^> KWhC Ķ"en'e#T2t$IaCT9_;\ PFȩ97f^c]#Ίt:D5pUɑ VDԆ'| t w ėaRfQ5$,WUU0B`#VP/ܺHHHrȡd1ti7'z«3 D~"N_b.bpmOjb5M6?QLjϏݮ59聯Lw,Q(Jg hvrF{h6DEH;& D4Z|ٸZj>seq$UG:B PTm0fu&746%RMd-"6a'+ ݫTqS4`( % AեL}ú͜n )dO$YX~qsq.Z<'#s%{)-F#*c9ӭ*}sC۬Z|BI'`zےUdğK3at9+>,]jIqEBhE \ < \10{! >+ŵdF\s:'(6,,;`ve0Oĉ~2"[D1 Z1EgLK'uBOaLMiwNjH( bu ʑp!(-!1BAW(l^w XYv5r]u#& Wm3PAk=s{]1r~l mK"cҘ =Q'bI~47:iwoC%~ʹjf2hzVɭ}ï;kePBCEGff$nbPF~q!%8gXu0L .OWڗtCN1 'b.jԨX|p+"s?§_{x,Z,4 i5Q_5 :9몙`sZW/oarLY8Ɛeqf$xȨ]ѫ5-Ҍ0wn|0,P E d榙+ӡaln}!1FbUN{. FXF!{] G*=zv9 L/^W}`yWt "pO2D)jHv1mLl{t[7koÃ"ssnlfO=Oxz/<#NW؆$wunV{ۋm?*\Q J8mz' |\s %|zTG9Xj X(d,&1|r{Hmnv7+Bf]Y-QdbN32OWkV/{t9|C!0($,;vw> ɮ44ĨR{OakڂN EN2v0JpxL^o:\f6d<y)e35']XGWMA,݁@PA: {C[`z9 8#L%.xp>W |l>0/tXf'u,T'hX}d2okd}׀* 10_W2A*S7^P}JkeS0q `3C!4'+âߕq9H%h|fǢ Yp~NcyaCW .2 yMufsBoe;Yc腵 f<{YVP7!FOHX'Fg `*lUJ'lxL7@h xv<34~z )*#B6 :u.N,^6gAvuw9E" HIG U Q-^(C,S .S0qx o631r/3rRK=N(5K@/DRQ~75r.26S6ސrZ`6$u3xS{C `5f$=2x8fP'Ф뫄q;#6ײ#GN"`9s4J6CipYr=r4<<ѮD3=OC+ؠ=f`}h}AƝ#Cݓ/\Z5Ow?t3n|lBwB,T8iۋslƦ%IFQ pbLגB 73^/dt;F5İn|C D=Plpr;A M *0~!k\ɎUsrbOJ} eAf]svvtLJ*˶'>K`Hu;lp1]`H$g%rnNWaJ m0sġPPYt[i$pr$&}<4sOɒi"}zNPOSlW7|EPŞOͮZhp%uXrZ18*;b-)@jSLcc?tSc]rS1m,J^rҁ{T^UY0ULGթU-\UT{HL/qVyC}xtaK: lĩ.m@q$&x]4RtMX]9\Y枚vh:˨b; c-[fv{S[B\ &Oyd# zVˁ] "’ѐ!zxtGm~0tpmthݔׇ{+g"4]N v,tQ%2R;}uHf١fRƯ7*~>\'d.Z-´d:6ϵvkqWe`4~,ńα#C?YlI׶-yD%n9"tS4;!o&z+Ò=n-&yr@;z]#y&ڭМIeG [M ЉN&y͘O9u7 ({MW/}$_j}ZqLdb~NB~ĐS!R̲Y y/ y) eMwc`G#?27n:oJ,h%-ƒ ljHH≂1J P},q+mtc :aU{p"` #*KՖL1-V7ST2OL⛱%9Y8y_-&n̓@i7f@ 101nsP8n8W p0ҧS?v`Q4f* ^`A! p0A Y$@Lc!!FGIJX$Bi u,g[`"./dh*NZK0zGi(T$`֗耢a <~4BW*(u18sYlnYt]UMҲYgչIhL0ߒ1l¬FVxԚѠ9 J߉%y#-*)zj)w}bA権:*04:+1X+4Mҥ9,Ҫ NJ;laN{-j,TBQ,r5+.2.?p.F- /`z9$~W6jt20ĒG|1%Я'29@,<3ͯ!;, ^  dihptmߥxH,Cr9@ʘPs⦼`2!X)O0`()hzUpq|5yE{~e@sm~>snLp4jL5sdgPa[~Pl)W(~={)n7)}X- A(.IPB\p"0!!d, `&dihlp,tmx|P6( ШtJZجvzxL.T|N~)nELJ*hlH3h J G HG G  llF# lFEGL HA @r@# h@"D( $vɓ(S\'$~IU \ˀ"cIѣH*EQ.9)JUׯ`Ê5paZI *ll&YIc˷-.L ރ?7^IL˘3`*0Ч&:i:p.>əc˞MЀ\#l~x]j ͼ-b0$'^.i{=F-N4lzk# NtrqMϿΐP/imkM4x@H4fskH{5 Ï0$hUs@Lbc 6hH&I+L~JF)Tj |95 3.X!A k,XrLu ǐP') F,`btvLeN‰1`^n}I_ƘadmTB5#Uc;jSMKM.7Å|{ȼ9@Jh0@yJV2T)3Y\g-2@(d@c^}t 6qFttyRy@rf #2@@L$T݄V< o@/0c!SG;T(0xk!ӹ!!oKaaW2?,&uC?j$}%b=塯(~~oG -vJZ%N%A҄셵U@k}TͮzuxEQhRͤePvM EeV44gq0&E4OMW$E.rV@7bjabecֶѯk}=)EbY ͛O 4xN,s0]wHˢ"\e.oɠ =BPf}lCGs",'B`Ķ'2$1^+HpEVu)@,@l'd"4Q5]1D#'͵BNM )92` ; P)%E3`xX AE5Hd1TFƇFHdO9_d?8g!cC&\rMTIbQuieZra~뾃pFr˼Z-̘]BJ0fN6 ̹ԎUCgQ4xMp$PJ $(Z3ﶻ]jfK,PK*] %5j44`NB^Ӑ߅I% P?: #gT¯uYUGނJ#f}$\I v0+nK פcڡTjkr`T8EybFm7&!{l_hr&u5ŝ {LqdLΙC|nOϘx3)I\z[urmrj7H774^V@Y5tͶŌR-JߚW kǏP|vԎ[R55Oq@5JdCI5&C7ƧsmaĘL7%iq#V/0&B-bu37@xr.9-  'HJKHٔNP)&T*KyPZ\Sٕ` XaYfyDh5|Ȗpr&j9ahz|iqaחZ9Yy٘9Yyٙi4QDOR+p+f0r"Ednb'G`5"^+lrjs>V>nYsɳ6q)$%A|GkB9a=㢐ip9qAnp(Y_|tWœi~u.x'n f̈:|w2`԰x삮 ykrj;]2f}*ϴF}J}[]2W:I~/-Sqi7LT#B)j3A>SV # Ƨc5!n'6~vF`LkPm l$Lbh ~SrJ'.mȱ*!;>? -ݼk|$0y%Rn=B92-~-M&#Q[P;VJ6k2̓pv]R^Ÿ?4P@>Pc7C@p/@ y% -":nCbhAbcҏb AiwV"=,)uD_aFJ HNP?TR_X|pZ^`b?d_fhjlnpr?t_r%埖uX},4Ԋv_ `[~_98&/iV9i$>B aX|ՙ=.9x1)>IRSx3 D>43)礢, x#lTO*yr. IEN19cFvԠӍ*aB.3dž @ y±||*}L@ 0 cT8Hi=5@5: B(NƁOFCC` %-4$ \@:4Uehjhu}I,8l #'+/37;?;CڴȄPkXo`{<(<4!g ! ,-["SB]>wѓ+Az?J&r$ɒ&OLr%˖._233dLf-NJ%zT LQ捂1;MXU#15@0qZ,A(IE0D! gJ8$]A:>oەnIoHH dfM: Oπz5֮_Î-{6M `n~SyOx M8,bR+"Ebx7-)xi(<O86!sdfQ0X ]xu}#P L7! `El4=dV +آ/x{Bq-g/H# 6ȔTtGt̴=BW8/ Y8g?Q$gBtD~ S 008_@F 0u`E* ie<'4؃ ! [ R& V gWUWrG$تzc a؂]ӎӃ`d4fyD _Cpb=_y ` *PGni շ:K2VEiD (%MB,D/!J7? 3G#\ɺa(j”:HE֙; nƼp ZX~>f~u; ,8QO@NlF>K`k<2SUCtdPJ$ ~a d= $E4$".X$w`gtS^嗳2+DoW5(*om-px5*)4a,K0 G&ؕH$8 E/i%S /| :buo$B%8 ǃ'D3~XB^"05h|ϯ码* &nYf 30&6P,3J0<{.^sKE؅xIe 3bO/$P`<^m"׮)ρS@G/1b#m Ι@TZxȆ'%.JR#\3W p}L€e.-S $^+FN"(a_GlV_ENA$L75.hq# hӐ1%/{y Y$F-tC{:fEZZD 騙I"F-*FRmf-0+-.>A]15(|fE)pՄ)o_"4 ].F4zˎ{D1Q.(2Yz@sXZp.YL:2MKAi ˎ-uK}'(ǘ G4d2Cjիbdop&\ 3di 5VםqPS╨ ғ;i6 p4mZ8rDp)*#(+_D B{⫸Z`€W' oEml1 Hw[ղ0; e78xCFpӮ kY7h< [3 F1NM\5u#A)ڡ9';l(!tc@6vv sPs3cX3\Ȱ`78(nˊ7D ) p NUBLg,j+^cFCaJ0 0>R"BQk%R5a>(ZI-OLW8,+[ Q}۸\7;=3ms#tw;Υ=;7oDXҿ n#< EOSl`8+n=7${`@/<"9Kn<*_9[<29kn<:9{=B:ыn#=J_:ӛC=dw+,Z:׻=b;n=j__׊GA5l;n=z; `@׭>>=_<C>/O`ng)σ>=Koo#_=nq=ko>AVz·]jWx &B> 3 P8::&/??'s)#(׎HNj椐qSVM~ ```BP]^GJiHHe )Ctgf ` ޞ9 RQ&UcD+`*2a:`+@TPOP@4bGb?L_RlaaIBH@!teN(_,QN hrb'z'`eb"Q!6]pp aD(Rr"01"c2HbF`@x+8 a؜arO mQF!c2c<BjBIdJ R=8 FyD"1`\D@䞴@1 IdQQ_׽>4oY #*KOF ' eYYeb4e#M7_"&uLLOO/te` `Lv?՚e=X,8~TQ aڵcahfiifjjfkkfllfmmfnn۩Ap'l_q"gr*r2gs:sBgtJtRguZubgvjvrgwzwgxxgyygzzg{JJ ̚vx|~gm8fHA9B&ӷ(-@),hgDՅt4O ա#oz&4T(jt5DhSEOuQҎ.ّFfģjx'` Ä"V~:bkDδ%+HiZ((ZA8}酸_i霖f=<=Zg,@?8_ghټp֘m(Nf/jٖA q5E,9ɭ~MK LeIg5&gjpLQ%YHJo]td"yU"TDi'vl ٖtYxAI  JMإ Ʒ~Q hGz0H4(#G?+I˽"Fn8ԹnΟWPlE[tW砺G*;?eNdL*yL ҵu9\Zn1*Dh6la|F#hzfsvg{gvhhviivjjvkkvllvmmvnnvoowp pwqq#wr+rDsRh}(kv[̲$@ҧ.&tDttQEH$jqxCzKvsw~mH>3&Ǡ"Y# Ț ~Sxs &i812/8 Ԫ H!x yL_I6UdJuM2!TQ gT{x2Nd+[spY*g舯4MM x;:"g@LQ9tilK S-g˶6l pʰQ] BYm]O\Ps2t}e4M4J̈́ #,L+ke9'v<_R{اFUadF¨+E:WV B/pІoN Ǯ%OCX (wRa?|C2TUIt lܻ,B L.o-vճ7#8*ǟ^xץr|ZL{˾ d߉Q L2?PCDBT<*ͼptE/ĊPGì }{邹:pn@G @=",U2l$F]MHΰPQѭd@>} SȢSW܋CFbwՌAEM3$&mV!+~M<@VZD=GNH.ӂ@w;븑Rz~Li?QtS$t`' %p,Ks1v0 QKf1 Is`z gZ5֭\zo0S lj #+`2픆'M levȔHeR#"~Tmt1"J$1٥8e"~ ;ٴkj!a eTARhv >f9Kɾ^]@B)-1e"B5 bIxi8@HAZ.@`X39Ep$,$@R+,'B MHa X G}v`dpBRV S BLA,cM\;, ]C44-ek LiCV֯q曀)H3Hk11;m@CxF~,4ɁPe18PNntcdLG!iƲ n*NCnԏ` S#`8""1 LSfd$@~7$&gGh" ~ BafD;)e#22/(,( ' |-AЄ*t mC шJtE/ #2Aeːsxf֦p Cш'o@JQpOF3"9@8LPD9eL*pcF'ӗLm*: ^g( X [ЌyfXn7)”G~` TCAK!$sa;Du4D$JxWE2}H|e`)` x4J6B< (ij!xD!)Sƀ (b\q#KYJD>QBNA Rw"$-^,.xXt*5KDžiM˹1uS TV[% I+uCM W]`_5% tmz[= Cx4F}8~VB(ֵL@xkV^@4 (No6. 5Q&4I Psv\eٽwFcT$lZ5춌z׼ ` {.?w դ Y"j9qlM B6 UƉnSXŔPEz1ާ=KPE;(p-7o%—dА#庺\]\ gA&ygux ҅bUO@P+E E$P }nqLj v`U?&35.#&Ϫt2x d5 Àl3jF&4`5(70֗݉ X*>t:+~2>K~az0{@3t|8Oԫ~o_~^,@2'K_ /+oKԯk/Ϗo(Hh Ȁ (Hhy1`xl-GP ' TQس(U0RZ-u q@.cG vaW a 27g=̣sPT'!NI!&1IĂI2vXoUj Wgy EhaF227ԅ2 c`H \WF<1J2 mxcuVxxVV4m\'YRy+I"Kz;2;3_Z_kD٫:IiK2j f⾬;cU^+1o*Eۥ+̷a+,"d4B;0".RXJ:C\ VVT^1pd›B+OJ^!YLz(0Q5ÂH"Tp S*^p!F*daw!K kGČS+ T|%HЃf% )_z% 420Y-O2U2׭!"Anڔ#/u$QFP04>+[9m~ @F݋+RM>Սvc (Xtݬc]9Gw rj[>!Fm< C*J7 J 4m&XDH}&e|'[ 8:Z8Md*`` gca1*Dq禜X±@զ0Q[B15Q(#78T(Cdsv"߈@X0 zzFơ>ZQ-~7PVu2+Bil;q"e $r '{aw9wmL> FMyD /2WM$. ٠m9!f1< bI`N-Bs:T쒸.M /Ɩd!';>+RI6 /83k,_Oi^jl/+*㱋K cX~8ߎ'Ia";+3wGJ+ )ni .܎ 5\N|zWhFcA6FRe3&Ѧq_$K  S%TF('VM;@i; 2.33ԁoKWNGop$ 7wυ4>ΕXI fV;8 ; /M zIKm@@!ZXL1bRNbh-P! U-+n!P*>?` !ԁ^ZU]X!d$Hр@@ b&gQ&O #TUCB) $ ] lo1rUA[Mlm2u5ba-A( \b3T9]K@Ceb8%hӅՀ Pw&GG-,lb',9@#WDYbΜ:w"‹,ZHJz *iX RUpNIh#2-k,ڴjזwZҭ*G֜57BAGZЂO@4=h8,T,': 2JA <(TYP03~0#, S03 s0C,&>1S.~1c,Ӹ61s>1,!F>2%3N~2,)SV2-s^2,1̍lӬ5nrLT ެ=~sy͟HV4E3x0Q@h43MsɁQ!,NԦ>5O Y.C2=<=cՀT׾5l8Cc@$A iS־vZ䱪Px@8mJYAD*' 6-yc}[B5 ChŻ?8.V*_! @K'8msx6(HB$i9c.h1 ˷YY-X5vF?:ғnw[JZg=4s8YlO+n4{n;!{~Բ''8 m\w ? o ÿ?_'?  &. 6> FN V^ fn v~ f<Q4z) `T6^d` BB%@S]EG? US)Z1AgWhFЌ|4]uXV]Ј_ \M_ Z@Ұ T 9/ ,_]6 QHW ŨQ)^IA-uE  V!@Y]ؒ/.́[љeW.!9"2Z$$IH#LMK̓Uϙ4F M-n"c8IHzQ0b2bF{=U4c7PP>cW N@ZBRcКHlLZF$ vFVB,@dghь#Mʜ'/ @E>3>=@8M\Vߡ&S Q4d|uЀe!9b\!Dn $MQŀ|9q +J[,<e^QM5 2B$ 0 Uz ,LP"mtbq#c8AZ?Aed}֌W]M8CfƂFZ%%t*\SZ>(f:՝i hoLP%%D>\B> #‚Qj~ЂY~@R(J A6l\ ʨgʮ K%R}ho'E8^!*sFj" \{ÅvLɌBEyb[E5弌|hgqL!|#v^N… T.H腃@QtCP%ҨAb9Ba %_Lݨ~)Iܩ봥Kv-7aŔG&p+{U8"c0FNPbD6Χґ"z6]H&xl,ojKo5d,d܌Tᒨh4he$x* 2fj݈}kq~-k$[ ć ]LFVt$T]&hة~ڃZ@\"&jόbx@.^Y(PWPAAJ*oh(0C'mt+Z9 U i, Pnt%Y@\'T Qn0h9Dh@ +f/in[ !C!j R!D'$vEQR/ݚ/Y#*h,P+/( QjQ.'$O mmbSzu/6geǂc?j oJ8e@) 5%٭AKK-͘h!iQL<6NZqIeR\gR&!T ߰B==L̃Sr'>^ΰZ UBc#$ \"cG,OW-ұ]:mD> ORN 8X NlxA\$Od-V{VH"D蹤H%tn(ő>DZW5Ӱ5o37X6K78783:X93;s}󮹳<3=׳=3>>3??3@@4AA4B'B/4C#5ml/ Ec44-cEDDO n3 AYԝ  r!X2ƈES N =2_/R#4dF#YT lptGgDs1*\t!$H :&IPNNsg @$5f5$OR¬TOuKVguVW+86S0\P"Ji%I SD)Z*rt0qRbc&dk>ba ѡ/b3vEu2HvT nZXBE5:Γ0Ħ\h[k:B0nď akrٚPjr'gֳ<%lc sZ#sZhٴD H"cJB3Pţl%1\4Ȭal暜ߞq_Cd2;lȂ䤖x|}ϭ 'SY<@tcXL7K-b.J$vR 35FPQ3J*TIt_·NH*/z\~ޭ>g8|299=J;wׅfZxhz$YP H+< #bmR,'=V拫7 <ĸ t!Z FG㙻C,DH"cHHvĂ+@+>YQ$℃ 8^rʘq[*$0k>G GMav{7Cns]dփ @"l tmgK M,n"`W <'JE:ށ*_A{$S63EΠ 7)r' Awx }"{~"k"Y T'$gJLP'Y<)fis$p%*(l'Ja' "a9K<>B؇X/Ek"~Vs /?'ɾ=?Dw{Q.3T "WUhУKN=,BwR N*|/yi7w3koP#bE$=B ![ i7GkÕFz-% P.~\7P&ڝvލ88}@(AˆN(Ahelg@{xj&=RĔ(IѤ[Ԓ"o"<_Ue['lꪬ#d*9sj+tG Ó9 VL3$@EutLd_p.R'Z,2 1xН9+@ G} ,@k%vzÍ$ &V;ӟNlmL#j򊟢ew#͵Fsu' ݵn52-rX,WM$+1E@#@GJ(>q_vޜ|;hj"#kq[  \[tokpAEoZ9Y%R< y{ȼS(L g ܐ&9 zbX/ԣ 'IJZLFv,r\k]b̤*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:{0;yzT35=gǑς"!2HD'Jъj!Daюz O7C%(MJWzP (`cj$/IhH@ PP$xA86Ѧ ~XjG$S?ӧVժZVu&PȁdUBn9+iѶf(a 2^Yת򕠁d'PJx ihGK\Zv<-\GټT-lgK[LְP0 | dkK7@p)pyZ*Lnj3 Q" {W`Mnv uCZ!'U~_i7BXe\0CԒ:DB{ JbGLbtLxuMabo3+@k81XtC@rXx쾄HN5πMBs6xo3\C;мXҘt2Jϵњ}I;E6WjZ:trZMZ[L{|\͓VsjIHhж P=<^ )P9pmK֐T9;Nw.B90=a@& Ѐ8ަRYmXSUE$R@0m'7FP 5[@B8DXFxHxL8IPR8TXVxX\HY`b8dXfxX\ȅ=npr8HR0kȄG|؇~xHyȄ48h[V؉ȅ RP@-A^(Xx$(؋{  8ʸ، NChH˜؍ӘC`@(8X؏Yy)F8ّ"9$Yh+c,ْ.0294Y6y8:<ٓ>@B9DYFyHJLٔNPR9TYVyX1;f^`)\YaYfy8lٖn)j ]o9tYhvzX1X`~ɗYy٘q9y>ٙ8ę9YE ٚ9iYuyٛY9NyIƙٜ: q/y)iٝޙ9y晞ٞθY9yX_(j]ʁ;kubectx-0.11.0/internal/000077500000000000000000000000001516137246200150615ustar00rootroot00000000000000kubectx-0.11.0/internal/cmdutil/000077500000000000000000000000001516137246200165225ustar00rootroot00000000000000kubectx-0.11.0/internal/cmdutil/deprecated.go000066400000000000000000000020041516137246200211450ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "io" "strings" "github.com/ahmetb/kubectx/internal/printer" ) func PrintDeprecatedEnvWarnings(out io.Writer, vars []string) { for _, vv := range vars { parts := strings.SplitN(vv, "=", 2) if len(parts) != 2 { continue } key := parts[0] if key == `KUBECTX_CURRENT_FGCOLOR` || key == `KUBECTX_CURRENT_BGCOLOR` { printer.Warning(out, "%s environment variable is now deprecated", key) } } } kubectx-0.11.0/internal/cmdutil/deprecated_test.go000066400000000000000000000025531516137246200222150ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "bytes" "strings" "testing" ) func TestPrintDeprecatedEnvWarnings_noDeprecatedVars(t *testing.T) { var out bytes.Buffer PrintDeprecatedEnvWarnings(&out, []string{ "A=B", "PATH=/foo:/bar:/bin", }) if v := out.String(); len(v) > 0 { t.Fatalf("something written to buf: %v", v) } } func TestPrintDeprecatedEnvWarnings_bgColors(t *testing.T) { var out bytes.Buffer PrintDeprecatedEnvWarnings(&out, []string{ "KUBECTX_CURRENT_FGCOLOR=1", "KUBECTX_CURRENT_BGCOLOR=2", }) v := out.String() if !strings.Contains(v, "KUBECTX_CURRENT_FGCOLOR") { t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_FGCOLOR': \"%s\"", v) } if !strings.Contains(v, "KUBECTX_CURRENT_BGCOLOR") { t.Fatalf("output doesn't contain 'KUBECTX_CURRENT_BGCOLOR': \"%s\"", v) } } kubectx-0.11.0/internal/cmdutil/interactive.go000066400000000000000000000022431516137246200213670ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "os" "os/exec" "github.com/mattn/go-isatty" "github.com/ahmetb/kubectx/internal/env" ) // isTerminal determines if given fd is a TTY. func isTerminal(fd *os.File) bool { return isatty.IsTerminal(fd.Fd()) } // fzfInstalled determines if fzf(1) is in PATH. func fzfInstalled() bool { v, _ := exec.LookPath("fzf") if v != "" { return true } return false } // IsInteractiveMode determines if we can do choosing with fzf. func IsInteractiveMode(stdout *os.File) bool { v := os.Getenv(env.EnvFZFIgnore) return v == "" && isTerminal(stdout) && fzfInstalled() } kubectx-0.11.0/internal/cmdutil/util.go000066400000000000000000000024521516137246200200310ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "errors" "os" "path/filepath" ) func HomeDir() string { home := os.Getenv("HOME") if home == "" { home = os.Getenv("USERPROFILE") // windows } return home } // CacheDir returns XDG_CACHE_HOME if set, otherwise $HOME/.kube, // matching the bash scripts' behavior: ${XDG_CACHE_HOME:-$HOME/.kube}. func CacheDir() string { if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { return xdg } home := HomeDir() if home == "" { return "" } return filepath.Join(home, ".kube") } // IsNotFoundErr determines if the underlying error is os.IsNotExist. func IsNotFoundErr(err error) bool { for e := err; e != nil; e = errors.Unwrap(e) { if os.IsNotExist(e) { return true } } return false } kubectx-0.11.0/internal/cmdutil/util_test.go000066400000000000000000000044141516137246200210700ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmdutil import ( "path/filepath" "testing" ) func Test_homeDir(t *testing.T) { type env struct{ k, v string } cases := []struct { name string envs []env want string }{ { name: "don't use XDG_CACHE_HOME as homedir", envs: []env{ {"XDG_CACHE_HOME", "xdg"}, {"HOME", "home"}, }, want: "home", }, { name: "HOME over USERPROFILE", envs: []env{ {"HOME", "home"}, {"USERPROFILE", "up"}, }, want: "home", }, { name: "only USERPROFILE available", envs: []env{ {"HOME", ""}, {"USERPROFILE", "up"}, }, want: "up", }, { name: "none available", envs: []env{ {"HOME", ""}, {"USERPROFILE", ""}, }, want: "", }, } for _, c := range cases { t.Run(c.name, func(tt *testing.T) { for _, e := range c.envs { tt.Setenv(e.k, e.v) } got := HomeDir() if got != c.want { t.Errorf("expected:%q got:%q", c.want, got) } }) } } func TestCacheDir(t *testing.T) { t.Run("XDG_CACHE_HOME set", func(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "/tmp/xdg-cache") t.Setenv("HOME", "/home/user") if got := CacheDir(); got != "/tmp/xdg-cache" { t.Errorf("expected:%q got:%q", "/tmp/xdg-cache", got) } }) t.Run("XDG_CACHE_HOME unset, falls back to HOME/.kube", func(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "") t.Setenv("HOME", "/home/user") want := filepath.Join("/home/user", ".kube") if got := CacheDir(); got != want { t.Errorf("expected:%q got:%q", want, got) } }) t.Run("neither set", func(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "") t.Setenv("HOME", "") t.Setenv("USERPROFILE", "") if got := CacheDir(); got != "" { t.Errorf("expected:%q got:%q", "", got) } }) } kubectx-0.11.0/internal/env/000077500000000000000000000000001516137246200156515ustar00rootroot00000000000000kubectx-0.11.0/internal/env/constants.go000066400000000000000000000024011516137246200202110ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES 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 const ( // EnvFZFIgnore describes the environment variable to set to disable // interactive context selection when fzf is installed. EnvFZFIgnore = "KUBECTX_IGNORE_FZF" // EnvNoColor describes the environment variable to disable color usage // when printing current context in a list. EnvNoColor = `NO_COLOR` // EnvForceColor describes the "internal" environment variable to force // color usage to show current context in a list. EnvForceColor = `_KUBECTX_FORCE_COLOR` // EnvDebug describes the internal environment variable for more verbose logging. EnvDebug = `DEBUG` EnvIsolatedShell = "KUBECTX_ISOLATED_SHELL" EnvReadonlyShell = "KUBECTX_READONLY_SHELL" ) kubectx-0.11.0/internal/kubeconfig/000077500000000000000000000000001516137246200171755ustar00rootroot00000000000000kubectx-0.11.0/internal/kubeconfig/contextmodify.go000066400000000000000000000031221516137246200224160ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "sigs.k8s.io/kustomize/kyaml/yaml" ) func (k *Kubeconfig) DeleteContextEntry(deleteName string) error { _, fileIdx, err := k.contextNodeWithFileIndex(deleteName) if err != nil { return err } contexts, err := contextsNodeOf(&k.files[fileIdx]) if err != nil { return err } return contexts.PipeE( yaml.ElementSetter{ Keys: []string{"name"}, Values: []string{deleteName}, }, ) } // ModifyCurrentContext always writes to the first file (matching kubectl behavior). func (k *Kubeconfig) ModifyCurrentContext(name string) error { if len(k.files) == 0 { return errNoFiles } return k.files[0].config.PipeE(yaml.SetField("current-context", yaml.NewScalarRNode(name))) } func (k *Kubeconfig) ModifyContextName(old, new string) error { context, _, err := k.contextNodeWithFileIndex(old) if err != nil { return err } if context == nil { return errors.New("\"contexts\" entry is nil") } return context.PipeE(yaml.SetField("name", yaml.NewScalarRNode(new))) } kubectx-0.11.0/internal/kubeconfig/contextmodify_test.go000066400000000000000000000173321516137246200234650ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_DeleteContextEntry_errors(t *testing.T) { kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)) _ = kc.Parse() err := kc.DeleteContextEntry("foo") if err == nil { t.Fatal("supposed to fail on non-mapping nodes") } kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`)) _ = kc.Parse() err = kc.DeleteContextEntry("foo") if err == nil { t.Fatal("supposed to fail if contexts key does not exist") } kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`contexts: "some string"`)) _ = kc.Parse() err = kc.DeleteContextEntry("foo") if err == nil { t.Fatal("supposed to fail if contexts key is not an array") } } func TestKubeconfig_DeleteContextEntry(t *testing.T) { test := WithMockKubeconfigLoader( testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.DeleteContextEntry("c1"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCtxs( testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t) out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } func TestKubeconfig_ModifyCurrentContext_fieldExists(t *testing.T) { test := WithMockKubeconfigLoader( testutil.KC().WithCurrentCtx("abc").Set("field1", "value1").ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyCurrentContext("foo"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCurrentCtx("foo").Set("field1", "value1").ToYAML(t) out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } func TestKubeconfig_ModifyCurrentContext_fieldMissing(t *testing.T) { test := WithMockKubeconfigLoader(`f1: v1`) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyCurrentContext("foo"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := `f1: v1 current-context: foo ` out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } func TestKubeconfig_ModifyContextName_noContextsEntryError(t *testing.T) { // no context entries test := WithMockKubeconfigLoader(`a: b`) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c1", "c2"); err == nil { t.Fatal("was expecting error for no 'contexts' entry; got nil") } } func TestKubeconfig_ModifyContextName_contextsEntryNotSequenceError(t *testing.T) { // no context entries test := WithMockKubeconfigLoader( `contexts: "hello"`) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c1", "c2"); err == nil { t.Fatal("was expecting error for 'context entry not a sequence'; got nil") } } func TestKubeconfig_ModifyContextName_noChange(t *testing.T) { test := WithMockKubeconfigLoader(testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c5", "c6"); err == nil { t.Fatal("was expecting error for 'no changes made'") } } func TestKubeconfig_ModifyContextName(t *testing.T) { test := WithMockKubeconfigLoader(testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(test) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyContextName("c1", "ccc"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCtxs( testutil.Ctx("ccc"), testutil.Ctx("c2"), testutil.Ctx("c3")).ToYAML(t) out := test.Output() if diff := cmp.Diff(expected, out); diff != "" { t.Fatalf("diff: %s", diff) } } func TestKubeconfig_ModifyCurrentContext_MultiFile_WritesToFirst(t *testing.T) { cfg1 := testutil.KC().WithCurrentCtx("ctx1").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) cfg2 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyCurrentContext("ctx2"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } // First file should have new current-context out0 := tl.OutputOf(0) expected0 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) if diff := cmp.Diff(expected0, out0); diff != "" { t.Fatalf("file 0 diff: %s", diff) } // Second file should be unchanged out1 := tl.OutputOf(1) expected1 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) if diff := cmp.Diff(expected1, out1); diff != "" { t.Fatalf("file 1 diff: %s", diff) } } func TestKubeconfig_DeleteContextEntry_MultiFile_FromCorrectFile(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("c1"), testutil.Ctx("c2")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("c3"), testutil.Ctx("c4")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } // Delete c3 which is in file 2 if err := kc.DeleteContextEntry("c3"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } // First file should be unchanged out0 := tl.OutputOf(0) expected0 := testutil.KC().WithCtxs(testutil.Ctx("c1"), testutil.Ctx("c2")).ToYAML(t) if diff := cmp.Diff(expected0, out0); diff != "" { t.Fatalf("file 0 diff: %s", diff) } // Second file should have c3 removed out1 := tl.OutputOf(1) expected1 := testutil.KC().WithCtxs(testutil.Ctx("c4")).ToYAML(t) if diff := cmp.Diff(expected1, out1); diff != "" { t.Fatalf("file 1 diff: %s", diff) } } func TestKubeconfig_ModifyContextName_MultiFile_InCorrectFile(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("c1")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("c2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } // Rename c2 (in file 2) to c2-new if err := kc.ModifyContextName("c2", "c2-new"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } // First file should be unchanged out0 := tl.OutputOf(0) expected0 := testutil.KC().WithCtxs(testutil.Ctx("c1")).ToYAML(t) if diff := cmp.Diff(expected0, out0); diff != "" { t.Fatalf("file 0 diff: %s", diff) } // Second file should have c2 renamed to c2-new out1 := tl.OutputOf(1) expected1 := testutil.KC().WithCtxs(testutil.Ctx("c2-new")).ToYAML(t) if diff := cmp.Diff(expected1, out1); diff != "" { t.Fatalf("file 1 diff: %s", diff) } } kubectx-0.11.0/internal/kubeconfig/contexts.go000066400000000000000000000056741516137246200214070ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "fmt" "slices" "sigs.k8s.io/kustomize/kyaml/yaml" ) func contextsNodeOf(fe *fileEntry) (*yaml.RNode, error) { contexts, err := fe.config.Pipe(yaml.Get("contexts")) if err != nil { return nil, err } if contexts == nil { return nil, errors.New("\"contexts\" entry is nil") } else if contexts.YNode().Kind != yaml.SequenceNode { return nil, errors.New("\"contexts\" is not a sequence node") } return contexts, nil } // contextNodeWithFileIndex searches for a context by name across all files. // Returns the context node and the index of the file that contains it. // Files without a valid "contexts" sequence are skipped, but if errors occur // during lookup they are included in the final error message. func (k *Kubeconfig) contextNodeWithFileIndex(name string) (*yaml.RNode, int, error) { var fileErrors []error for i := range k.files { contexts, err := contextsNodeOf(&k.files[i]) if err != nil { fileErrors = append(fileErrors, fmt.Errorf("file %d: %w", i, err)) continue } context, err := contexts.Pipe(yaml.Lookup("[name=" + name + "]")) if err != nil { fileErrors = append(fileErrors, fmt.Errorf("file %d lookup: %w", i, err)) continue } if context != nil { return context, i, nil } } if len(fileErrors) > 0 { return nil, -1, fmt.Errorf("context with name %q not found (errors in files: %w)", name, errors.Join(fileErrors...)) } return nil, -1, fmt.Errorf("context with name %q not found", name) } func (k *Kubeconfig) contextNode(name string) (*yaml.RNode, error) { node, _, err := k.contextNodeWithFileIndex(name) return node, err } func (k *Kubeconfig) ContextNames() ([]string, error) { seen := make(map[string]bool) var names []string for i := range k.files { contexts, err := k.files[i].config.Pipe(yaml.Get("contexts")) if err != nil { return nil, fmt.Errorf("failed to get contexts: %w", err) } if contexts == nil { continue } fileNames, err := contexts.ElementValues("name") if err != nil { return nil, fmt.Errorf("failed to get context names: %w", err) } for _, n := range fileNames { if !seen[n] { seen[n] = true names = append(names, n) } } } return names, nil } func (k *Kubeconfig) ContextExists(name string) (bool, error) { names, err := k.ContextNames() if err != nil { return false, err } return slices.Contains(names, name), nil } kubectx-0.11.0/internal/kubeconfig/contexts_test.go000066400000000000000000000104771516137246200224430ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_ContextNames(t *testing.T) { tl := WithMockKubeconfigLoader( testutil.KC().WithCtxs( testutil.Ctx("abc"), testutil.Ctx("def"), testutil.Ctx("ghi")).Set("field1", map[string]string{"bar": "zoo"}).ToYAML(t)) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } ctx, err := kc.ContextNames() if err != nil { t.Fatal(err) } expected := []string{"abc", "def", "ghi"} if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) } } func TestKubeconfig_ContextNames_noContextsEntry(t *testing.T) { tl := WithMockKubeconfigLoader(`a: b`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } ctx, err := kc.ContextNames() if err != nil { t.Fatal(err) } var expected []string = nil if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) } } func TestKubeconfig_ContextNames_nonArrayContextsEntry(t *testing.T) { tl := WithMockKubeconfigLoader(`contexts: "hello"`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } _, err := kc.ContextNames() if err == nil { t.Fatal("expected error for non-array contexts entry") } } func TestKubeconfig_CheckContextExists(t *testing.T) { tl := WithMockKubeconfigLoader( testutil.KC().WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } if exists, err := kc.ContextExists("c1"); err != nil || !exists { t.Fatal("c1 actually exists; reported false") } if exists, err := kc.ContextExists("c2"); err != nil || !exists { t.Fatal("c2 actually exists; reported false") } if exists, err := kc.ContextExists("c3"); err != nil { t.Fatal(err) } else if exists { t.Fatal("c3 does not exist; but reported true") } } func TestKubeconfig_ContextNames_MultiFile_Merged(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("a"), testutil.Ctx("b")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("c"), testutil.Ctx("d")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } ctx, err := kc.ContextNames() if err != nil { t.Fatal(err) } expected := []string{"a", "b", "c", "d"} if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) } } func TestKubeconfig_ContextNames_MultiFile_DuplicateFirstWins(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("a"), testutil.Ctx("shared")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("shared"), testutil.Ctx("b")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } ctx, err := kc.ContextNames() if err != nil { t.Fatal(err) } // "shared" should appear only once expected := []string{"a", "shared", "b"} if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) } } func TestKubeconfig_ContextExists_MultiFile(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("c1")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("c2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } if exists, err := kc.ContextExists("c1"); err != nil || !exists { t.Fatal("c1 should exist") } if exists, err := kc.ContextExists("c2"); err != nil || !exists { t.Fatal("c2 should exist (in second file)") } if exists, err := kc.ContextExists("c3"); err != nil { t.Fatal(err) } else if exists { t.Fatal("c3 should not exist") } } kubectx-0.11.0/internal/kubeconfig/currentcontext.go000066400000000000000000000025641516137246200226220ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "fmt" "sigs.k8s.io/kustomize/kyaml/yaml" ) // GetCurrentContext returns "current-context" value from the first file // that has a non-empty current-context, or returns ("", nil) if not found. func (k *Kubeconfig) GetCurrentContext() (string, error) { for _, fe := range k.files { v, err := fe.config.Pipe(yaml.Get("current-context")) if err != nil { return "", fmt.Errorf("failed to read current-context: %w", err) } if s := yaml.GetValue(v); s != "" { return s, nil } } return "", nil } // UnsetCurrentContext clears the current-context field in the first file. func (k *Kubeconfig) UnsetCurrentContext() error { if len(k.files) == 0 { return errNoFiles } return k.files[0].config.PipeE(yaml.SetField("current-context", yaml.NewStringRNode(""))) } kubectx-0.11.0/internal/kubeconfig/currentcontext_test.go000066400000000000000000000077441516137246200236660ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_GetCurrentContext(t *testing.T) { tl := WithMockKubeconfigLoader(`current-context: foo`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } v, err := kc.GetCurrentContext() if err != nil { t.Fatal(err) } expected := "foo" if v != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v) } } func TestKubeconfig_GetCurrentContext_missingField(t *testing.T) { tl := WithMockKubeconfigLoader(`abc: def`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } v, err := kc.GetCurrentContext() if err != nil { t.Fatal(err) } expected := "" if v != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, v) } } func TestKubeconfig_UnsetCurrentContext(t *testing.T) { tl := WithMockKubeconfigLoader(testutil.KC().WithCurrentCtx("foo").ToYAML(t)) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.UnsetCurrentContext(); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } out := tl.Output() expected := testutil.KC().WithCurrentCtx("").ToYAML(t) if out != expected { t.Fatalf("expected=\"%s\"; got=\"%s\"", expected, out) } } func TestKubeconfig_GetCurrentContext_MultiFile_FirstNonEmpty(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) // no current-context cfg2 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } v, err := kc.GetCurrentContext() if err != nil { t.Fatal(err) } if v != "ctx2" { t.Fatalf("expected=\"ctx2\"; got=\"%s\"", v) } } func TestKubeconfig_GetCurrentContext_MultiFile_FirstWins(t *testing.T) { cfg1 := testutil.KC().WithCurrentCtx("ctx1").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) cfg2 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } v, err := kc.GetCurrentContext() if err != nil { t.Fatal(err) } if v != "ctx1" { t.Fatalf("expected=\"ctx1\"; got=\"%s\"", v) } } func TestKubeconfig_UnsetCurrentContext_MultiFile(t *testing.T) { cfg1 := testutil.KC().WithCurrentCtx("ctx1").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) cfg2 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.UnsetCurrentContext(); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } // After unsetting, GetCurrentContext should return ctx2 (from second file) // Re-parse the saved output to verify out0 := tl.OutputOf(0) expected0 := testutil.KC().WithCurrentCtx("").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) if out0 != expected0 { t.Fatalf("file 0: expected=\"%s\"; got=\"%s\"", expected0, out0) } // Second file should be unchanged out1 := tl.OutputOf(1) expected1 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) if out1 != expected1 { t.Fatalf("file 1: expected=\"%s\"; got=\"%s\"", expected1, out1) } } kubectx-0.11.0/internal/kubeconfig/helper_test.go000066400000000000000000000047571516137246200220570ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "bytes" "io" "strings" ) type MockKubeconfigLoader struct { in io.Reader out bytes.Buffer } func (t *MockKubeconfigLoader) Read(p []byte) (n int, err error) { return t.in.Read(p) } func (t *MockKubeconfigLoader) Write(p []byte) (n int, err error) { return t.out.Write(p) } func (t *MockKubeconfigLoader) Close() error { return nil } func (t *MockKubeconfigLoader) Reset() error { return nil } func (t *MockKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) { return []ReadWriteResetCloser{ReadWriteResetCloser(t)}, nil } func (t *MockKubeconfigLoader) Output() string { return t.out.String() } func WithMockKubeconfigLoader(kubecfg string) *MockKubeconfigLoader { return &MockKubeconfigLoader{in: strings.NewReader(kubecfg)} } // mockFile is a single in-memory kubeconfig file for multi-file testing. type mockFile struct { in io.Reader out bytes.Buffer } func (m *mockFile) Read(p []byte) (n int, err error) { return m.in.Read(p) } func (m *mockFile) Write(p []byte) (n int, err error) { return m.out.Write(p) } func (m *mockFile) Close() error { return nil } func (m *mockFile) Reset() error { return nil } // MockMultiKubeconfigLoader implements Loader for testing with multiple kubeconfig files. type MockMultiKubeconfigLoader struct { files []*mockFile } func (m *MockMultiKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) { out := make([]ReadWriteResetCloser, len(m.files)) for i, f := range m.files { out[i] = f } return out, nil } func (m *MockMultiKubeconfigLoader) OutputOf(index int) string { return m.files[index].out.String() } func WithMockMultiKubeconfigLoader(configs ...string) *MockMultiKubeconfigLoader { files := make([]*mockFile, len(configs)) for i, c := range configs { files[i] = &mockFile{in: strings.NewReader(c)} } return &MockMultiKubeconfigLoader{files: files} } kubectx-0.11.0/internal/kubeconfig/kubeconfig.go000066400000000000000000000127171516137246200216500ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "fmt" "io" "sigs.k8s.io/kustomize/kyaml/yaml" ) type ReadWriteResetCloser interface { io.ReadWriteCloser // Reset truncates the file and seeks to the beginning of the file. Reset() error } // PathHinter is optionally implemented by ReadWriteResetCloser to indicate // the file path of the underlying kubeconfig file. This is used to resolve // relative paths (e.g. in exec credential plugins) when building a REST client. type PathHinter interface { Path() string } type Loader interface { Load() ([]ReadWriteResetCloser, error) } type fileEntry struct { f ReadWriteResetCloser path string config *yaml.RNode } type Kubeconfig struct { loader Loader files []fileEntry } var errNoFiles = errors.New("no kubeconfig files loaded") func (k *Kubeconfig) WithLoader(l Loader) *Kubeconfig { k.loader = l return k } func (k *Kubeconfig) Close() error { var firstErr error for _, fe := range k.files { if err := fe.f.Close(); err != nil && firstErr == nil { firstErr = err } } return firstErr } func (k *Kubeconfig) Parse() error { rwcs, err := k.loader.Load() if err != nil { return fmt.Errorf("failed to load: %w", err) } k.files = make([]fileEntry, 0, len(rwcs)) for i, f := range rwcs { var v yaml.Node if err := yaml.NewDecoder(f).Decode(&v); err != nil { // Close all file handles on failure to avoid leaks. for _, rf := range rwcs { rf.Close() } return fmt.Errorf("failed to decode file %d: %w", i, err) } rn := yaml.NewRNode(&v) if rn.YNode().Kind != yaml.MappingNode { for _, rf := range rwcs { rf.Close() } return fmt.Errorf("kubeconfig file %d is not a map document", i) } var p string if ph, ok := f.(PathHinter); ok { p = ph.Path() } k.files = append(k.files, fileEntry{f: f, path: p, config: rn}) } return nil } // ConfigPaths returns the file paths of all loaded kubeconfig files. // Returns nil if the kubeconfig was not loaded from files (e.g. in tests). func (k *Kubeconfig) ConfigPaths() []string { var paths []string for _, fe := range k.files { if fe.path != "" { paths = append(paths, fe.path) } } return paths } func (k *Kubeconfig) Bytes() ([]byte, error) { if len(k.files) == 0 { return nil, errNoFiles } if len(k.files) == 1 { str, err := k.files[0].config.String() if err != nil { return nil, err } return []byte(str), nil } // Build a merged config for multi-file case. // Start with a copy of the first file's structure. merged := k.files[0].config.Copy() // Merge contexts, clusters, and users from all files (first wins for duplicates). for _, key := range []string{"contexts", "clusters", "users"} { mergedSeq, err := mergeSequences(k.files, key) if err != nil { return nil, fmt.Errorf("failed to merge %s: %w", key, err) } if mergedSeq != nil { if err := merged.PipeE(yaml.SetField(key, mergedSeq)); err != nil { return nil, err } } } // Use the first non-empty current-context. cur, err := k.GetCurrentContext() if err != nil { return nil, fmt.Errorf("failed to get current context for merge: %w", err) } if cur != "" { if err := merged.PipeE(yaml.SetField("current-context", yaml.NewScalarRNode(cur))); err != nil { return nil, err } } str, err := merged.String() if err != nil { return nil, err } return []byte(str), nil } // mergeSequences merges a named sequence field (e.g. "contexts") across multiple files. // The first occurrence of each entry (by "name" key) wins. // Files where the field is missing or not a sequence are silently skipped (matching kubectl merge behavior). func mergeSequences(files []fileEntry, field string) (*yaml.RNode, error) { seen := make(map[string]bool) var elements []*yaml.RNode for _, fe := range files { seq, err := fe.config.Pipe(yaml.Get(field)) if err != nil || seq == nil { continue } if seq.YNode().Kind != yaml.SequenceNode { continue } for _, elem := range seq.YNode().Content { rn := yaml.NewRNode(elem) name, err := rn.Pipe(yaml.Get("name")) if err != nil || name == nil { continue } n := yaml.GetValue(name) if n != "" && seen[n] { continue } seen[n] = true elements = append(elements, rn) } } if len(elements) == 0 { return nil, nil } seqNode := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} for _, elem := range elements { seqNode.Content = append(seqNode.Content, elem.YNode()) } return yaml.NewRNode(seqNode), nil } func (k *Kubeconfig) Save() error { for i := range k.files { if err := k.files[i].f.Reset(); err != nil { return fmt.Errorf("failed to reset file %d: %w", i, err) } enc := yaml.NewEncoder(k.files[i].f) enc.SetIndent(0) if err := enc.Encode(k.files[i].config.YNode()); err != nil { return fmt.Errorf("failed to encode file %d: %w", i, err) } if err := enc.Close(); err != nil { return fmt.Errorf("failed to close encoder for file %d: %w", i, err) } } return nil } kubectx-0.11.0/internal/kubeconfig/kubeconfig_test.go000066400000000000000000000062001516137246200226750ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestParse(t *testing.T) { err := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: [1, 2`)).Parse() if err == nil { t.Fatal("expected error from bad yaml") } err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)).Parse() if err == nil { t.Fatal("expected error from not-mapping root node") } err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`current-context: foo`)).Parse() if err != nil { t.Fatal(err) } err = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). WithCurrentCtx("foo"). WithCtxs().ToYAML(t))).Parse() if err != nil { t.Fatal(err) } } func TestSave(t *testing.T) { in := "a: [1, 2, 3]\n" test := WithMockKubeconfigLoader(in) kc := new(Kubeconfig).WithLoader(test) defer kc.Close() if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.ModifyCurrentContext("hello"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := "a: [1, 2, 3]\ncurrent-context: hello\n" if diff := cmp.Diff(expected, test.Output()); diff != "" { t.Fatal(diff) } } func TestParse_MultiFile(t *testing.T) { cfg1 := testutil.KC().WithCurrentCtx("ctx1").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) cfg2 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) defer kc.Close() if err := kc.Parse(); err != nil { t.Fatal(err) } } func TestSave_MultiFile(t *testing.T) { cfg1 := testutil.KC().WithCurrentCtx("ctx1").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) defer kc.Close() if err := kc.Parse(); err != nil { t.Fatal(err) } // Modify current-context (writes to first file) if err := kc.ModifyCurrentContext("ctx2"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } // First file should have updated current-context out0 := tl.OutputOf(0) expected0 := testutil.KC().WithCurrentCtx("ctx2").WithCtxs(testutil.Ctx("ctx1")).ToYAML(t) if diff := cmp.Diff(expected0, out0); diff != "" { t.Fatalf("file 0 diff: %s", diff) } // Second file should be unchanged out1 := tl.OutputOf(1) expected1 := testutil.KC().WithCtxs(testutil.Ctx("ctx2")).ToYAML(t) if diff := cmp.Diff(expected1, out1); diff != "" { t.Fatalf("file 1 diff: %s", diff) } } kubectx-0.11.0/internal/kubeconfig/kubeconfigloader.go000066400000000000000000000042321516137246200230300ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "errors" "fmt" "os" "path/filepath" "github.com/ahmetb/kubectx/internal/cmdutil" ) var ( DefaultLoader Loader = new(StandardKubeconfigLoader) ) type StandardKubeconfigLoader struct{} type kubeconfigFile struct { *os.File path string } func (kf *kubeconfigFile) Path() string { return kf.path } func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) { paths, err := kubeconfigPaths() if err != nil { return nil, fmt.Errorf("cannot determine kubeconfig path: %w", err) } var files []ReadWriteResetCloser for _, p := range paths { f, err := os.OpenFile(p, os.O_RDWR, 0) if err != nil { if os.IsNotExist(err) { continue } return nil, fmt.Errorf("failed to open file %q: %w", p, err) } files = append(files, &kubeconfigFile{File: f, path: p}) } if len(files) == 0 { return nil, fmt.Errorf("kubeconfig file not found: %w", &os.PathError{Op: "open", Path: paths[0], Err: os.ErrNotExist}) } return files, nil } func (kf *kubeconfigFile) Reset() error { if err := kf.Truncate(0); err != nil { return fmt.Errorf("failed to truncate file: %w", err) } if _, err := kf.Seek(0, 0); err != nil { return fmt.Errorf("failed to seek in file: %w", err) } return nil } func kubeconfigPaths() ([]string, error) { // KUBECONFIG env var if v := os.Getenv("KUBECONFIG"); v != "" { return filepath.SplitList(v), nil } // default path home := cmdutil.HomeDir() if home == "" { return nil, errors.New("HOME or USERPROFILE environment variable not set") } return []string{filepath.Join(home, ".kube", "config")}, nil } kubectx-0.11.0/internal/kubeconfig/kubeconfigloader_test.go000066400000000000000000000076261516137246200241010ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "os" "path/filepath" "strings" "testing" "github.com/ahmetb/kubectx/internal/cmdutil" ) func Test_kubeconfigPaths_default(t *testing.T) { t.Setenv("HOME", "/x/y/z") expected := filepath.FromSlash("/x/y/z/.kube/config") got, err := kubeconfigPaths() if err != nil { t.Fatal(err) } if len(got) != 1 || got[0] != expected { t.Fatalf("got=%q expected=[%q]", got, expected) } } func Test_kubeconfigPaths_noEnvVars(t *testing.T) { t.Setenv("XDG_CACHE_HOME", "") t.Setenv("HOME", "") t.Setenv("USERPROFILE", "") _, err := kubeconfigPaths() if err == nil { t.Fatalf("expected error") } } func Test_kubeconfigPaths_envSingleFile(t *testing.T) { t.Setenv("KUBECONFIG", "foo") v, err := kubeconfigPaths() if err != nil { t.Fatal(err) } if len(v) != 1 || v[0] != "foo" { t.Fatalf("expected=[\"foo\"], got=%q", v) } } func Test_kubeconfigPaths_envMultipleFiles(t *testing.T) { path := strings.Join([]string{"file1", "file2", "file3"}, string(os.PathListSeparator)) t.Setenv("KUBECONFIG", path) v, err := kubeconfigPaths() if err != nil { t.Fatal(err) } if len(v) != 3 || v[0] != "file1" || v[1] != "file2" || v[2] != "file3" { t.Fatalf("expected=[file1,file2,file3], got=%q", v) } } func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) { t.Setenv("KUBECONFIG", "foo") kc := new(Kubeconfig).WithLoader(DefaultLoader) err := kc.Parse() if err == nil { t.Fatal("expected err") } if !cmdutil.IsNotFoundErr(err) { t.Fatalf("expected ENOENT error; got=%v", err) } } func TestStandardKubeconfigLoader_multipleFiles_skipsMissing(t *testing.T) { dir := t.TempDir() existing := filepath.Join(dir, "config1") if err := os.WriteFile(existing, []byte("apiVersion: v1\nkind: Config\ncontexts: []\n"), 0644); err != nil { t.Fatal(err) } missing := filepath.Join(dir, "config2") path := strings.Join([]string{existing, missing}, string(os.PathListSeparator)) t.Setenv("KUBECONFIG", path) files, err := new(StandardKubeconfigLoader).Load() if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 1 { t.Fatalf("expected 1 file, got %d", len(files)) } files[0].Close() } func TestStandardKubeconfigLoader_multipleFiles_allMissing(t *testing.T) { dir := t.TempDir() path := strings.Join([]string{ filepath.Join(dir, "missing1"), filepath.Join(dir, "missing2"), }, string(os.PathListSeparator)) t.Setenv("KUBECONFIG", path) _, err := new(StandardKubeconfigLoader).Load() if err == nil { t.Fatal("expected error when all files missing") } if !cmdutil.IsNotFoundErr(err) { t.Fatalf("expected ENOENT error; got=%v", err) } } func TestStandardKubeconfigLoader_multipleFiles_loadsAll(t *testing.T) { dir := t.TempDir() f1 := filepath.Join(dir, "config1") f2 := filepath.Join(dir, "config2") if err := os.WriteFile(f1, []byte("apiVersion: v1\nkind: Config\ncontexts: []\n"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(f2, []byte("apiVersion: v1\nkind: Config\ncontexts: []\n"), 0644); err != nil { t.Fatal(err) } path := strings.Join([]string{f1, f2}, string(os.PathListSeparator)) t.Setenv("KUBECONFIG", path) files, err := new(StandardKubeconfigLoader).Load() if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 2 { t.Fatalf("expected 2 files, got %d", len(files)) } for _, f := range files { f.Close() } } kubectx-0.11.0/internal/kubeconfig/namespace.go000066400000000000000000000024761516137246200214710ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) const ( defaultNamespace = "default" ) func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) { ctx, err := k.contextNode(contextName) if err != nil { return "", err } namespace, err := ctx.Pipe(yaml.Lookup("context", "namespace")) if namespace == nil || err != nil { return defaultNamespace, err } return yaml.GetValue(namespace), nil } func (k *Kubeconfig) SetNamespace(ctxName string, ns string) error { ctx, err := k.contextNode(ctxName) if err != nil { return err } if err := ctx.PipeE( yaml.LookupCreate(yaml.MappingNode, "context"), yaml.SetField("namespace", yaml.NewStringRNode(ns)), ); err != nil { return err } return nil } kubectx-0.11.0/internal/kubeconfig/namespace_test.go000066400000000000000000000075041516137246200225250ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package kubeconfig import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ahmetb/kubectx/internal/testutil" ) func TestKubeconfig_NamespaceOfContext_ctxNotFound(t *testing.T) { kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). WithCtxs(testutil.Ctx("c1")).ToYAML(t))) if err := kc.Parse(); err != nil { t.Fatal(err) } _, err := kc.NamespaceOfContext("c2") if err == nil { t.Fatal("expected err") } } func TestKubeconfig_NamespaceOfContext(t *testing.T) { kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2").Ns("c2n1")).ToYAML(t))) if err := kc.Parse(); err != nil { t.Fatal(err) } v1, err := kc.NamespaceOfContext("c1") if err != nil { t.Fatal("expected err") } if expected := `default`; v1 != expected { t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1) } v2, err := kc.NamespaceOfContext("c2") if err != nil { t.Fatal("expected err") } if expected := `c2n1`; v2 != expected { t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2) } } func TestKubeconfig_SetNamespace(t *testing.T) { l := WithMockKubeconfigLoader(testutil.KC(). WithCtxs( testutil.Ctx("c1"), testutil.Ctx("c2").Ns("c2n1")).ToYAML(t)) kc := new(Kubeconfig).WithLoader(l) if err := kc.Parse(); err != nil { t.Fatal(err) } if err := kc.SetNamespace("c3", "foo"); err == nil { t.Fatalf("expected error for non-existing ctx") } if err := kc.SetNamespace("c1", "c1n1"); err != nil { t.Fatal(err) } if err := kc.SetNamespace("c2", "c2n2"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } expected := testutil.KC().WithCtxs( testutil.Ctx("c1").Ns("c1n1"), testutil.Ctx("c2").Ns("c2n2")).ToYAML(t) if diff := cmp.Diff(l.Output(), expected); diff != "" { t.Fatal(diff) } } func TestKubeconfig_NamespaceOfContext_MultiFile(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("c1").Ns("ns1")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("c2").Ns("ns2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } v, err := kc.NamespaceOfContext("c2") if err != nil { t.Fatal(err) } if v != "ns2" { t.Fatalf("expected=\"ns2\" got=\"%s\"", v) } } func TestKubeconfig_SetNamespace_MultiFile(t *testing.T) { cfg1 := testutil.KC().WithCtxs(testutil.Ctx("c1")).ToYAML(t) cfg2 := testutil.KC().WithCtxs(testutil.Ctx("c2")).ToYAML(t) tl := WithMockMultiKubeconfigLoader(cfg1, cfg2) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } // Set namespace for c2 which is in second file if err := kc.SetNamespace("c2", "my-ns"); err != nil { t.Fatal(err) } if err := kc.Save(); err != nil { t.Fatal(err) } // First file should be unchanged out0 := tl.OutputOf(0) expected0 := testutil.KC().WithCtxs(testutil.Ctx("c1")).ToYAML(t) if diff := cmp.Diff(expected0, out0); diff != "" { t.Fatalf("file 0 diff: %s", diff) } // Second file should have namespace set out1 := tl.OutputOf(1) expected1 := testutil.KC().WithCtxs(testutil.Ctx("c2").Ns("my-ns")).ToYAML(t) if diff := cmp.Diff(expected1, out1); diff != "" { t.Fatalf("file 1 diff: %s", diff) } } kubectx-0.11.0/internal/printer/000077500000000000000000000000001516137246200165445ustar00rootroot00000000000000kubectx-0.11.0/internal/printer/color.go000066400000000000000000000026751516137246200202230ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package printer import ( "os" "github.com/fatih/color" "github.com/ahmetb/kubectx/internal/env" ) var ( ActiveItemColor = color.New(color.FgGreen, color.Bold) ) func init() { EnableOrDisableColor(ActiveItemColor) } // useColors returns true if colors are force-enabled, // false if colors are disabled, or nil for default behavior // which is determined based on factors like if stdout is tty. func useColors() *bool { tr, fa := true, false if os.Getenv(env.EnvForceColor) != "" { return &tr } else if os.Getenv(env.EnvNoColor) != "" { return &fa } return nil } // EnableOrDisableColor determines if color should be force-enabled or force-disabled // or left untouched based on environment configuration. func EnableOrDisableColor(c *color.Color) { if v := useColors(); v != nil && *v { c.EnableColor() } else if v != nil && !*v { c.DisableColor() } } kubectx-0.11.0/internal/printer/color_test.go000066400000000000000000000024171516137246200212540ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package printer import ( "testing" "github.com/google/go-cmp/cmp" ) var ( tr, fa = true, false ) func Test_useColors_forceColors(t *testing.T) { t.Setenv("_KUBECTX_FORCE_COLOR", "1") t.Setenv("NO_COLOR", "1") if v := useColors(); !cmp.Equal(v, &tr) { t.Fatalf("expected useColors() = true; got = %v", v) } } func Test_useColors_disableColors(t *testing.T) { t.Setenv("NO_COLOR", "1") if v := useColors(); !cmp.Equal(v, &fa) { t.Fatalf("expected useColors() = false; got = %v", v) } } func Test_useColors_default(t *testing.T) { t.Setenv("NO_COLOR", "") t.Setenv("_KUBECTX_FORCE_COLOR", "") if v := useColors(); v != nil { t.Fatalf("expected useColors() = nil; got=%v", *v) } } kubectx-0.11.0/internal/printer/printer.go000066400000000000000000000030731516137246200205610ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package printer import ( "fmt" "io" "github.com/fatih/color" ) var ( ErrorColor = color.New(color.FgRed, color.Bold) WarningColor = color.New(color.FgYellow, color.Bold) SuccessColor = color.New(color.FgGreen) ) func init() { colors := useColors() if colors == nil { return } if *colors { ErrorColor.EnableColor() WarningColor.EnableColor() SuccessColor.EnableColor() } else { ErrorColor.DisableColor() WarningColor.DisableColor() SuccessColor.DisableColor() } } func Error(w io.Writer, format string, args ...any) error { _, err := io.WriteString(w, ErrorColor.Sprint("error: ")+fmt.Sprintf(format, args...)+"\n") return err } func Warning(w io.Writer, format string, args ...any) error { _, err := io.WriteString(w, WarningColor.Sprint("warning: ")+fmt.Sprintf(format, args...)+"\n") return err } func Success(w io.Writer, format string, args ...any) error { _, err := io.WriteString(w, SuccessColor.Sprint("✔ ")+fmt.Sprintf(format, args...)+"\n") return err } kubectx-0.11.0/internal/proxy/000077500000000000000000000000001516137246200162425ustar00rootroot00000000000000kubectx-0.11.0/internal/proxy/kubeconfig.go000066400000000000000000000027731516137246200207160ustar00rootroot00000000000000package proxy import ( "fmt" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) // RewriteKubeconfig takes minified kubeconfig bytes and rewrites them so that: // - The cluster server URL points to the local proxy address (plain HTTP). // - insecure-skip-tls-verify is set (needed for plain HTTP). // - Certificate authority data is removed. // - User auth fields (client certs, tokens, exec, auth-provider) are removed // since the proxy handles authentication to the real API server. func RewriteKubeconfig(data []byte, proxyAddr string) ([]byte, error) { cfg, err := clientcmd.Load(data) if err != nil { return nil, fmt.Errorf("failed to parse kubeconfig: %w", err) } for _, cluster := range cfg.Clusters { cluster.Server = "http://" + proxyAddr cluster.InsecureSkipTLSVerify = true cluster.CertificateAuthority = "" cluster.CertificateAuthorityData = nil } for name := range cfg.AuthInfos { cfg.AuthInfos[name] = &clientcmdapi.AuthInfo{} } // Rename contexts with [RO] suffix to indicate readonly mode. renames := make(map[string]string, len(cfg.Contexts)) for name := range cfg.Contexts { renames[name] = name + "[RO]" } for old, roName := range renames { cfg.Contexts[roName] = cfg.Contexts[old] delete(cfg.Contexts, old) if cfg.CurrentContext == old { cfg.CurrentContext = roName } } out, err := clientcmd.Write(*cfg) if err != nil { return nil, fmt.Errorf("failed to serialize kubeconfig: %w", err) } return out, nil } kubectx-0.11.0/internal/proxy/kubeconfig_test.go000066400000000000000000000056521516137246200217540ustar00rootroot00000000000000package proxy import ( "testing" "k8s.io/client-go/tools/clientcmd" ) const sampleKubeconfig = `apiVersion: v1 kind: Config clusters: - cluster: certificate-authority-data: dGVzdC1jYS1kYXRh server: https://my-cluster.example.com:6443 name: my-cluster contexts: - context: cluster: my-cluster user: my-user name: my-context current-context: my-context users: - name: my-user user: client-certificate-data: dGVzdC1jZXJ0LWRhdGE= client-key-data: dGVzdC1rZXktZGF0YQ== token: test-token ` func TestRewriteKubeconfig(t *testing.T) { result, err := RewriteKubeconfig([]byte(sampleKubeconfig), "127.0.0.1:12345") if err != nil { t.Fatalf("RewriteKubeconfig failed: %v", err) } cfg, err := clientcmd.Load(result) if err != nil { t.Fatalf("failed to parse rewritten kubeconfig: %v", err) } cluster := cfg.Clusters["my-cluster"] if cluster == nil { t.Fatal("cluster my-cluster not found") } if cluster.Server != "http://127.0.0.1:12345" { t.Errorf("expected server http://127.0.0.1:12345, got %q", cluster.Server) } if !cluster.InsecureSkipTLSVerify { t.Error("expected insecure-skip-tls-verify to be true") } if len(cluster.CertificateAuthorityData) != 0 { t.Error("expected certificate-authority-data to be removed") } if cluster.CertificateAuthority != "" { t.Error("expected certificate-authority to be removed") } user := cfg.AuthInfos["my-user"] if user == nil { t.Fatal("user my-user not found") } if len(user.ClientCertificateData) != 0 { t.Error("expected client-certificate-data to be removed") } if len(user.ClientKeyData) != 0 { t.Error("expected client-key-data to be removed") } if user.Token != "" { t.Error("expected token to be removed") } if _, ok := cfg.Contexts["my-context[RO]"]; !ok { t.Error("expected context to be renamed to \"my-context[RO]\"") } if _, ok := cfg.Contexts["my-context"]; ok { t.Error("expected original context name to be removed") } if cfg.CurrentContext != "my-context[RO]" { t.Errorf("expected current-context to be \"my-context[RO]\", got %q", cfg.CurrentContext) } } const execKubeconfig = `apiVersion: v1 kind: Config clusters: - cluster: server: https://my-cluster.example.com:6443 name: my-cluster contexts: - context: cluster: my-cluster user: my-user name: my-context current-context: my-context users: - name: my-user user: exec: apiVersion: client.authentication.k8s.io/v1beta1 command: gke-gcloud-auth-plugin ` func TestRewriteKubeconfig_ExecPlugin(t *testing.T) { result, err := RewriteKubeconfig([]byte(execKubeconfig), "127.0.0.1:54321") if err != nil { t.Fatalf("RewriteKubeconfig failed: %v", err) } cfg, err := clientcmd.Load(result) if err != nil { t.Fatalf("failed to parse rewritten kubeconfig: %v", err) } user := cfg.AuthInfos["my-user"] if user == nil { t.Fatal("user my-user not found") } if user.Exec != nil { t.Error("expected exec plugin config to be removed") } } kubectx-0.11.0/internal/proxy/readonly.go000066400000000000000000000137601516137246200204150ustar00rootroot00000000000000package proxy import ( "context" "encoding/json" "fmt" "log" "net" "net/http" "net/http/httputil" "net/url" "os" "regexp" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/ahmetb/kubectx/internal/env" ) // nonMutatingPostPatterns match Kubernetes "review" endpoints that use POST // but don't create persistent resources. Patterns are anchored to known API // groups to prevent spoofing via custom resource names. var nonMutatingPostPatterns = []*regexp.Regexp{ regexp.MustCompile(`^/apis/authorization\.k8s\.io/[^/]+/selfsubjectaccessreviews$`), regexp.MustCompile(`^/apis/authorization\.k8s\.io/[^/]+/subjectaccessreviews$`), regexp.MustCompile(`^/apis/authorization\.k8s\.io/[^/]+/namespaces/[^/]+/localsubjectaccessreviews$`), regexp.MustCompile(`^/apis/authorization\.k8s\.io/[^/]+/selfsubjectrulesreviews$`), regexp.MustCompile(`^/apis/authentication\.k8s\.io/[^/]+/tokenreviews$`), regexp.MustCompile(`^/apis/authentication\.k8s\.io/[^/]+/selfsubjectreviews$`), } var debugLog = func() *log.Logger { if _, ok := os.LookupEnv(env.EnvDebug); ok { return log.New(os.Stderr, "[readonly-proxy] ", log.Ltime) } return log.New(nopWriter{}, "", 0) }() type nopWriter struct{} func (nopWriter) Write(p []byte) (int, error) { return len(p), nil } // ReadonlyProxy is a reverse proxy that only allows read-only HTTP methods. type ReadonlyProxy struct { server *http.Server listener net.Listener } // Config holds information needed to start the readonly proxy. type Config struct { KubeconfigPath string ContextName string } // Start creates and starts a readonly reverse proxy on a random localhost port. // The proxy loads TLS/auth config from the kubeconfig and forwards only // GET, HEAD, and OPTIONS requests (without protocol upgrades) to the real API server. func Start(cfg Config) (*ReadonlyProxy, error) { loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: cfg.KubeconfigPath} overrides := &clientcmd.ConfigOverrides{CurrentContext: cfg.ContextName} clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) restCfg, err := clientConfig.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to load kubeconfig: %w", err) } targetURL, err := url.Parse(restCfg.Host) if err != nil { return nil, fmt.Errorf("failed to parse server URL %q: %w", restCfg.Host, err) } transport, err := rest.TransportFor(restCfg) if err != nil { return nil, fmt.Errorf("failed to create transport: %w", err) } handler := NewHandler(targetURL, transport) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("failed to listen: %w", err) } srv := &http.Server{Handler: handler} go srv.Serve(listener) debugLog.Printf("started on %s, proxying to %s", listener.Addr(), targetURL) return &ReadonlyProxy{ server: srv, listener: listener, }, nil } // Addr returns the listener address (e.g. "127.0.0.1:54321"). func (p *ReadonlyProxy) Addr() string { return p.listener.Addr().String() } // Shutdown gracefully stops the proxy. func (p *ReadonlyProxy) Shutdown(ctx context.Context) error { debugLog.Printf("shutting down") return p.server.Shutdown(ctx) } // NewHandler creates the readonly proxy HTTP handler. // Exported for testing with a fake backend. func NewHandler(target *url.URL, transport http.RoundTripper) http.Handler { proxy := httputil.NewSingleHostReverseProxy(target) proxy.Transport = transport proxy.FlushInterval = -1 // flush immediately for streaming (logs -f, watches) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { debugLog.Printf(">> %s %s", r.Method, r.URL.Path) if reason, ok := checkRequest(r); !ok { debugLog.Printf("<< %s %s -> 405 (%s)", r.Method, r.URL.Path, reason) writeBlockedResponse(w, r.Method, fmt.Sprintf("[kubectx] readonly mode: %s", reason)) return } debugLog.Printf("<< %s %s -> proxied", r.Method, r.URL.Path) proxy.ServeHTTP(w, r) }) } // checkRequest determines whether a request should be proxied or blocked. // Returns ("", true) if allowed, or (reason, false) if blocked. func checkRequest(r *http.Request) (reason string, allowed bool) { if isUpgrade(r) { return "operations like exec, cp, and port-forward are not allowed", false } if isReadOnly(r) { return "", true } if isNonMutatingPost(r) { return "", true } if isDryRun(r) { return "", true } return fmt.Sprintf("%s requests are not allowed", r.Method), false } // isUpgrade returns true if the request is a protocol upgrade (SPDY/WebSocket). func isUpgrade(r *http.Request) bool { return strings.EqualFold(r.Header.Get("Connection"), "Upgrade") || r.Header.Get("Upgrade") != "" } // isReadOnly returns true for safe HTTP methods that never modify state. func isReadOnly(r *http.Request) bool { switch r.Method { case http.MethodGet, http.MethodHead, http.MethodOptions: return true } return false } // isNonMutatingPost returns true for Kubernetes "review" endpoints that use // POST but don't create persistent resources (e.g. SubjectAccessReview). // Patterns are anchored to known API groups to prevent spoofing. func isNonMutatingPost(r *http.Request) bool { if r.Method != http.MethodPost { return false } for _, re := range nonMutatingPostPatterns { if re.MatchString(r.URL.Path) { return true } } return false } // isDryRun returns true if the request has ?dryRun=All, which means // the API server will validate but not persist the request. func isDryRun(r *http.Request) bool { return r.URL.Query().Get("dryRun") == "All" } func writeBlockedResponse(w http.ResponseWriter, method, message string) { status := &metav1.Status{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Status"}, Status: metav1.StatusFailure, Message: message, Reason: metav1.StatusReasonMethodNotAllowed, Code: http.StatusMethodNotAllowed, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusMethodNotAllowed) json.NewEncoder(w).Encode(status) } kubectx-0.11.0/internal/proxy/readonly_security_test.go000066400000000000000000000446541516137246200234110ustar00rootroot00000000000000package proxy import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ============================================================ // Security & Jailbreak Test Suite for kubectx readonly proxy // ============================================================ // --- Jailbreak: HTTP method smuggling --- func TestJailbreak_MethodOverrideHeaders(t *testing.T) { handler, _ := newTestHandler(t) // Attackers might try X-HTTP-Method-Override or similar headers // to smuggle a POST through as a GET. overrideHeaders := []string{ "X-HTTP-Method-Override", "X-HTTP-Method", "X-Method-Override", } for _, hdr := range overrideHeaders { t.Run(hdr, func(t *testing.T) { // Send GET with override header claiming POST req := httptest.NewRequest(http.MethodGet, "/api/v1/namespaces", nil) req.Header.Set(hdr, "POST") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) // The proxy should allow it (it's a GET), but the backend should // NOT see it as a POST. The key is: do these headers reach the backend? // If backend honors the override header, the readonly protection is bypassed. if rr.Code == http.StatusOK { // Check if backend received the override header - it could be dangerous t.Logf("INFO: %s header passed through to backend (method override headers are forwarded)", hdr) } }) } } // --- Jailbreak: Path traversal and URL manipulation --- func TestJailbreak_PathTraversal(t *testing.T) { handler, _ := newTestHandler(t) paths := []struct { name string method string path string want int }{ // Try to sneak a POST through with path encoding {"encoded POST path", http.MethodPost, "/api/v1/%6eamespaces", 405}, {"double-encoded path", http.MethodPost, "/api/v1/%256eamespaces", 405}, // Try making a DELETE look like a review endpoint {"DELETE masquerading as review", http.MethodDelete, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", 405}, // PUT masquerading as review {"PUT masquerading as review", http.MethodPut, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", 405}, // PATCH masquerading as review {"PATCH masquerading as review", http.MethodPatch, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", 405}, } for _, tt := range paths { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(tt.method, tt.path, nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != tt.want { t.Errorf("expected %d, got %d", tt.want, rr.Code) } }) } } // --- Jailbreak: DryRun parameter smuggling --- func TestJailbreak_DryRunSmuggling(t *testing.T) { handler, _ := newTestHandler(t) tests := []struct { name string method string rawURL string wantOK bool comment string }{ {"dryRun=All (legit)", http.MethodPost, "/api/v1/namespaces?dryRun=All", true, "legitimate dry-run should work"}, {"dryRun=all lowercase", http.MethodPost, "/api/v1/namespaces?dryRun=all", false, "case-sensitive: 'all' != 'All'"}, {"dryRun=ALL uppercase", http.MethodPost, "/api/v1/namespaces?dryRun=ALL", false, "case-sensitive: 'ALL' != 'All'"}, {"dryRun=All+extra", http.MethodPost, "/api/v1/namespaces?dryRun=All&dryRun=None", true, "POTENTIAL ISSUE: multiple dryRun params - Go Query().Get() returns first"}, {"dryRun=None+All", http.MethodPost, "/api/v1/namespaces?dryRun=None&dryRun=All", false, "Query().Get() returns first param which is None - should be blocked"}, {"dryRun with spaces", http.MethodPost, "/api/v1/namespaces?dryRun=%20All", false, "space-padded dryRun should be rejected"}, {"dryRun=All with null byte", http.MethodPost, "/api/v1/namespaces?dryRun=All%00None", false, "null byte injection in dryRun value"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(tt.method, tt.rawURL, nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) gotOK := rr.Code == http.StatusOK if gotOK != tt.wantOK { t.Errorf("%s: expected ok=%v, got status %d [%s]", tt.name, tt.wantOK, rr.Code, tt.comment) } else { t.Logf("PASS: %s [%s]", tt.name, tt.comment) } }) } } // --- Jailbreak: Upgrade header smuggling --- func TestJailbreak_UpgradeSmuggling(t *testing.T) { handler, _ := newTestHandler(t) tests := []struct { name string method string path string headers map[string]string wantOK bool }{ // Case variation - Go canonicalizes header keys, so "connection" → "Connection" {"connection key lowercase (Go canonicalizes)", http.MethodGet, "/api/v1/pods/x/exec", map[string]string{"connection": "Upgrade"}, false, // Go normalizes header key to "Connection", value "Upgrade" matches exactly → blocked }, // Fixed: Connection value "upgrade" (lowercase) now caught by strings.EqualFold {"Connection: upgrade value lowercase", http.MethodGet, "/api/v1/pods/x/exec", map[string]string{"Connection": "upgrade"}, false, }, // Multiple Connection header values {"Connection with multiple values", http.MethodGet, "/api/v1/pods/x/exec", map[string]string{"Connection": "keep-alive, Upgrade", "Upgrade": "SPDY/3.1"}, false, // Has Upgrade header set so isUpgrade catches it via second check }, // Empty upgrade header {"empty Upgrade header", http.MethodGet, "/api/v1/pods/x/exec", map[string]string{"Upgrade": ""}, true, // Empty string != "" is false, so this passes through }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(tt.method, tt.path, nil) for k, v := range tt.headers { req.Header.Set(k, v) } rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) gotOK := rr.Code == http.StatusOK if gotOK != tt.wantOK { t.Errorf("expected ok=%v, got status %d", tt.wantOK, rr.Code) } }) } } // --- Jailbreak: Review endpoint spoofing --- func TestJailbreak_ReviewEndpointSpoofing(t *testing.T) { handler, _ := newTestHandler(t) tests := []struct { name string path string wantOK bool }{ // Legitimate {"legit SSAR", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", true}, // Trailing slash {"trailing slash", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews/", false}, // Path with query string {"with query string", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews?foo=bar", true}, // CRD in different API group matching same resource name {"spoofed API group", "/apis/authorization.evil.io/v1/selfsubjectaccessreviews", false}, // Subresource under a review endpoint name {"subresource", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews/status", false}, // Double dot in API group {"double-dot group", "/apis/authorization..k8s..io/v1/selfsubjectaccessreviews", false}, // Unicode homograph {"unicode homograph", "/apis/authorization.k8s.іo/v1/selfsubjectaccessreviews", false}, // Namespace-scoped version of cluster-scoped review {"namespace-scoped SSAR", "/apis/authorization.k8s.io/v1/namespaces/default/selfsubjectaccessreviews", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, tt.path, nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) gotOK := rr.Code == http.StatusOK if gotOK != tt.wantOK { t.Errorf("expected ok=%v, got status %d", tt.wantOK, rr.Code) } }) } } // --- Jailbreak: CONNECT method (HTTP tunneling) --- func TestJailbreak_ConnectMethod(t *testing.T) { handler, _ := newTestHandler(t) req := httptest.NewRequest("CONNECT", "/", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("CONNECT should be blocked, got %d", rr.Code) } } // --- Jailbreak: Custom/unusual HTTP methods --- func TestJailbreak_UnusualMethods(t *testing.T) { handler, _ := newTestHandler(t) methods := []string{ "CONNECT", "TRACE", "PROPFIND", // WebDAV "MKCOL", // WebDAV "COPY", // WebDAV "MOVE", // WebDAV "LOCK", // WebDAV "UNLOCK", // WebDAV "PURGE", // Varnish "LINK", // Link "UNLINK", // Unlink "VIEW", // non-standard "CUSTOMDELETE", // non-standard } for _, method := range methods { t.Run(method, func(t *testing.T) { req := httptest.NewRequest(method, "/api/v1/pods", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("%s should be blocked, got %d", method, rr.Code) } }) } } // --- Jailbreak: Large request body on allowed endpoint --- func TestJailbreak_LargeBodyOnGET(t *testing.T) { handler, _ := newTestHandler(t) // Some proxies might convert a GET with a body to a POST body := strings.NewReader(`{"kind":"Namespace","apiVersion":"v1","metadata":{"name":"evil"}}`) req := httptest.NewRequest(http.MethodGet, "/api/v1/namespaces", body) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) // GET with body should still be treated as GET (allowed) if rr.Code != http.StatusOK { t.Errorf("GET with body should still be allowed, got %d", rr.Code) } } // --- Blocked response format verification --- func TestBlockedResponse_Format(t *testing.T) { handler, _ := newTestHandler(t) req := httptest.NewRequest(http.MethodPost, "/api/v1/pods", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) // Verify Content-Type ct := rr.Header().Get("Content-Type") if ct != "application/json" { t.Errorf("expected Content-Type application/json, got %q", ct) } // Verify response body is valid Kubernetes Status var status metav1.Status body, _ := io.ReadAll(rr.Body) if err := json.Unmarshal(body, &status); err != nil { t.Fatalf("response is not valid JSON: %v\nbody: %s", err, body) } if status.APIVersion != "v1" { t.Errorf("expected apiVersion=v1, got %q", status.APIVersion) } if status.Kind != "Status" { t.Errorf("expected kind=Status, got %q", status.Kind) } if status.Code != 405 { t.Errorf("expected code=405, got %d", status.Code) } if status.Message == "" { t.Error("expected non-empty message") } if !strings.Contains(status.Message, "[kubectx]") { t.Errorf("expected message to contain [kubectx], got %q", status.Message) } } // --- Kubeconfig rewriting security tests --- func TestRewriteKubeconfig_Security(t *testing.T) { // Verify that no credentials leak into the rewritten kubeconfig input := ` apiVersion: v1 kind: Config clusters: - name: test-cluster cluster: server: https://real-server.example.com:6443 certificate-authority-data: c2VjcmV0LWNh contexts: - name: test-ctx context: cluster: test-cluster user: test-user current-context: test-ctx users: - name: test-user user: client-certificate-data: c2VjcmV0LWNlcnQ= client-key-data: c2VjcmV0LWtleQ== token: super-secret-token ` out, err := RewriteKubeconfig([]byte(input), "127.0.0.1:12345") if err != nil { t.Fatal(err) } result := string(out) // Real server URL must not appear if strings.Contains(result, "real-server.example.com") { t.Error("SECURITY: real server URL leaked into rewritten kubeconfig") } // Credentials must not appear sensitiveStrings := []string{ "c2VjcmV0LWNh", // CA data "c2VjcmV0LWNlcnQ=", // client cert "c2VjcmV0LWtleQ==", // client key "super-secret-token", // token } for _, s := range sensitiveStrings { if strings.Contains(result, s) { t.Errorf("SECURITY: credential data %q leaked into rewritten kubeconfig", s) } } // Proxy address must be present if !strings.Contains(result, "127.0.0.1:12345") { t.Error("proxy address not found in rewritten kubeconfig") } // [RO] suffix must be present if !strings.Contains(result, "[RO]") { t.Error("[RO] context suffix not found in rewritten kubeconfig") } } // --- Concurrent request handling --- func TestConcurrent_MixedRequests(t *testing.T) { handler, _ := newTestHandler(t) done := make(chan struct{}, 100) // Fire off mixed GET and POST requests concurrently for i := 0; i < 50; i++ { go func(i int) { defer func() { done <- struct{}{} }() var method string if i%2 == 0 { method = http.MethodGet } else { method = http.MethodPost } req := httptest.NewRequest(method, "/api/v1/pods", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if method == http.MethodGet && rr.Code != http.StatusOK { t.Errorf("GET #%d: expected 200, got %d", i, rr.Code) } if method == http.MethodPost && rr.Code != http.StatusMethodNotAllowed { t.Errorf("POST #%d: expected 405, got %d", i, rr.Code) } }(i) } for i := 0; i < 50; i++ { <-done } } // --- Watchlist (GET with watch param - should be allowed) --- func TestWatch_AllowedViaGET(t *testing.T) { handler, _ := newTestHandler(t) req := httptest.NewRequest(http.MethodGet, "/api/v1/pods?watch=true", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("GET with watch=true should be allowed, got %d", rr.Code) } } // --- kubectl apply --dry-run=server sends POST with dryRun=All --- func TestDryRunApply_ServerSide(t *testing.T) { handler, _ := newTestHandler(t) // Simulates: kubectl apply --dry-run=server body := strings.NewReader(`{"kind":"Namespace","apiVersion":"v1","metadata":{"name":"test"}}`) req := httptest.NewRequest(http.MethodPost, "/api/v1/namespaces?dryRun=All&fieldManager=kubectl-client-side-apply", body) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("server-side dry-run apply should be allowed, got %d", rr.Code) } } // --- kubectl logs should work (GET, no upgrade) --- func TestLogs_AllowedViaGET(t *testing.T) { handler, _ := newTestHandler(t) req := httptest.NewRequest(http.MethodGet, "/api/v1/namespaces/default/pods/my-pod/log?follow=true", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("kubectl logs should be allowed (GET), got %d", rr.Code) } } // --- kubectl exec should be blocked --- func TestExec_Blocked(t *testing.T) { handler, _ := newTestHandler(t) req := httptest.NewRequest(http.MethodPost, "/api/v1/namespaces/default/pods/my-pod/exec?command=sh", nil) req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "SPDY/3.1") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("kubectl exec should be blocked, got %d", rr.Code) } var status metav1.Status json.NewDecoder(rr.Body).Decode(&status) if !strings.Contains(status.Message, "exec") { t.Errorf("blocked message should mention exec, got %q", status.Message) } } // --- kubectl cp should be blocked --- func TestCp_Blocked(t *testing.T) { handler, _ := newTestHandler(t) // kubectl cp first does exec req := httptest.NewRequest(http.MethodPost, "/api/v1/namespaces/default/pods/my-pod/exec?command=tar", nil) req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "SPDY/3.1") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("kubectl cp should be blocked, got %d", rr.Code) } } // --- kubectl port-forward should be blocked --- func TestPortForward_Blocked(t *testing.T) { handler, _ := newTestHandler(t) req := httptest.NewRequest(http.MethodPost, "/api/v1/namespaces/default/pods/my-pod/portforward", nil) req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "SPDY/3.1") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("kubectl port-forward should be blocked, got %d", rr.Code) } } // --- E2E proxy integration test with real HTTP server --- func TestE2E_ProxyWithBackend(t *testing.T) { var backendRequests []string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { backendRequests = append(backendRequests, fmt.Sprintf("%s %s", r.Method, r.URL.Path)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) switch { case r.URL.Path == "/api/v1/pods" && r.Method == "GET": w.Write([]byte(`{"kind":"PodList","apiVersion":"v1","items":[{"metadata":{"name":"test-pod"}}]}`)) case r.URL.Path == "/api/v1/namespaces" && r.Method == "GET": w.Write([]byte(`{"kind":"NamespaceList","apiVersion":"v1","items":[{"metadata":{"name":"default"}}]}`)) default: w.Write([]byte(`{"kind":"Status","apiVersion":"v1","status":"Success"}`)) } })) defer backend.Close() target, _ := url.Parse(backend.URL) handler := NewHandler(target, http.DefaultTransport) proxyServer := httptest.NewServer(handler) defer proxyServer.Close() client := proxyServer.Client() // Test 1: GET pods - should reach backend resp, err := client.Get(proxyServer.URL + "/api/v1/pods") if err != nil { t.Fatal(err) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 200 { t.Errorf("GET pods: expected 200, got %d", resp.StatusCode) } if !strings.Contains(string(body), "test-pod") { t.Error("GET pods: response doesn't contain expected pod") } // Test 2: POST namespace - should be blocked (never reach backend) preCount := len(backendRequests) resp, err = client.Post(proxyServer.URL+"/api/v1/namespaces", "application/json", strings.NewReader(`{"kind":"Namespace","apiVersion":"v1","metadata":{"name":"evil"}}`)) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 405 { t.Errorf("POST namespace: expected 405, got %d", resp.StatusCode) } if len(backendRequests) != preCount { t.Error("POST namespace: request leaked through to backend!") } // Test 3: DELETE pod - should be blocked req, _ := http.NewRequest(http.MethodDelete, proxyServer.URL+"/api/v1/namespaces/default/pods/test-pod", nil) resp, err = client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 405 { t.Errorf("DELETE pod: expected 405, got %d", resp.StatusCode) } // Test 4: GET namespaces - should work resp, err = client.Get(proxyServer.URL + "/api/v1/namespaces") if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 200 { t.Errorf("GET namespaces: expected 200, got %d", resp.StatusCode) } // Test 5: dry-run POST - should reach backend req, _ = http.NewRequest(http.MethodPost, proxyServer.URL+"/api/v1/namespaces?dryRun=All", nil) resp, err = client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 200 { t.Errorf("dry-run POST: expected 200, got %d", resp.StatusCode) } } kubectx-0.11.0/internal/proxy/readonly_test.go000066400000000000000000000244101516137246200214460ustar00rootroot00000000000000package proxy import ( "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func newTestHandler(t *testing.T) (http.Handler, *httptest.Server) { t.Helper() backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Backend-Method", r.Method) w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) })) t.Cleanup(backend.Close) target, err := url.Parse(backend.URL) if err != nil { t.Fatal(err) } handler := NewHandler(target, http.DefaultTransport) return handler, backend } // --- Unit tests for individual filter functions --- func TestIsUpgrade(t *testing.T) { tests := []struct { name string connection string upgrade string want bool }{ {"no headers", "", "", false}, {"Connection: Upgrade", "Upgrade", "", true}, {"Upgrade: SPDY", "", "SPDY/3.1", true}, {"Upgrade: websocket", "", "websocket", true}, {"both headers", "Upgrade", "websocket", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) if tt.connection != "" { r.Header.Set("Connection", tt.connection) } if tt.upgrade != "" { r.Header.Set("Upgrade", tt.upgrade) } if got := isUpgrade(r); got != tt.want { t.Errorf("isUpgrade() = %v, want %v", got, tt.want) } }) } } func TestIsReadOnly(t *testing.T) { tests := []struct { method string want bool }{ {http.MethodGet, true}, {http.MethodHead, true}, {http.MethodOptions, true}, {http.MethodPost, false}, {http.MethodPut, false}, {http.MethodPatch, false}, {http.MethodDelete, false}, } for _, tt := range tests { t.Run(tt.method, func(t *testing.T) { r := httptest.NewRequest(tt.method, "/api/v1/pods", nil) if got := isReadOnly(r); got != tt.want { t.Errorf("isReadOnly(%s) = %v, want %v", tt.method, got, tt.want) } }) } } func TestIsNonMutatingPost(t *testing.T) { tests := []struct { name string method string path string want bool }{ {"selfsubjectaccessreviews", http.MethodPost, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", true}, {"subjectaccessreviews", http.MethodPost, "/apis/authorization.k8s.io/v1/subjectaccessreviews", true}, {"localsubjectaccessreviews", http.MethodPost, "/apis/authorization.k8s.io/v1/namespaces/default/localsubjectaccessreviews", true}, {"selfsubjectrulesreviews", http.MethodPost, "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews", true}, {"tokenreviews", http.MethodPost, "/apis/authentication.k8s.io/v1/tokenreviews", true}, {"selfsubjectreviews", http.MethodPost, "/apis/authentication.k8s.io/v1/selfsubjectreviews", true}, {"regular POST", http.MethodPost, "/api/v1/namespaces", false}, {"GET to review path", http.MethodGet, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", false}, {"DELETE to review path", http.MethodDelete, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", false}, {"spoofed resource name", http.MethodPost, "/apis/evil.io/v1/selfsubjectaccessreviews", false}, {"spoofed suffix in custom group", http.MethodPost, "/apis/custom.example.com/v1/namespaces/default/selfsubjectaccessreviews", false}, {"review name as subresource", http.MethodPost, "/api/v1/namespaces/default/pods/selfsubjectaccessreviews", false}, {"v1beta1 version allowed", http.MethodPost, "/apis/authorization.k8s.io/v1beta1/selfsubjectaccessreviews", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(tt.method, tt.path, nil) if got := isNonMutatingPost(r); got != tt.want { t.Errorf("isNonMutatingPost(%s %s) = %v, want %v", tt.method, tt.path, got, tt.want) } }) } } func TestIsDryRun(t *testing.T) { tests := []struct { name string url string want bool }{ {"dryRun=All", "/api/v1/namespaces?dryRun=All", true}, {"no dryRun", "/api/v1/namespaces", false}, {"dryRun=None", "/api/v1/namespaces?dryRun=None", false}, {"dryRun empty", "/api/v1/namespaces?dryRun=", false}, {"dryRun with other params", "/api/v1/namespaces?fieldManager=kubectl&dryRun=All", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodPost, tt.url, nil) if got := isDryRun(r); got != tt.want { t.Errorf("isDryRun(%s) = %v, want %v", tt.url, got, tt.want) } }) } } // --- Unit tests for the commander --- func TestCheckRequest(t *testing.T) { tests := []struct { name string method string path string headers map[string]string allowed bool }{ {"GET allowed", http.MethodGet, "/api/v1/pods", nil, true}, {"POST blocked", http.MethodPost, "/api/v1/pods", nil, false}, {"upgrade blocked", http.MethodGet, "/api/v1/pods/foo/exec", map[string]string{"Connection": "Upgrade", "Upgrade": "SPDY/3.1"}, false}, {"review POST allowed", http.MethodPost, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", nil, true}, {"dry-run POST allowed", http.MethodPost, "/api/v1/namespaces?dryRun=All", nil, true}, {"dry-run DELETE allowed", http.MethodDelete, "/api/v1/namespaces/foo?dryRun=All", nil, true}, {"upgrade trumps dry-run", http.MethodGet, "/api/v1/pods?dryRun=All", map[string]string{"Connection": "Upgrade"}, false}, {"upgrade trumps review", http.MethodPost, "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", map[string]string{"Connection": "Upgrade"}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(tt.method, tt.path, nil) for k, v := range tt.headers { r.Header.Set(k, v) } reason, ok := checkRequest(r) if ok != tt.allowed { t.Errorf("checkRequest() allowed=%v, want %v (reason=%q)", ok, tt.allowed, reason) } if !ok && reason == "" { t.Error("checkRequest() returned blocked with empty reason") } }) } } // --- Integration tests through the full handler --- func TestHandler_AllowedMethods(t *testing.T) { handler, _ := newTestHandler(t) for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodOptions} { t.Run(method, func(t *testing.T) { req := httptest.NewRequest(method, "/api/v1/pods", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } }) } } func TestHandler_BlockedMethods(t *testing.T) { handler, _ := newTestHandler(t) for _, method := range []string{ http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, } { t.Run(method, func(t *testing.T) { req := httptest.NewRequest(method, "/api/v1/pods", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405, got %d", rr.Code) } var status metav1.Status if err := json.NewDecoder(rr.Body).Decode(&status); err != nil { t.Fatalf("failed to decode response: %v", err) } if status.Status != metav1.StatusFailure { t.Errorf("expected status Failure, got %q", status.Status) } if status.Reason != metav1.StatusReasonMethodNotAllowed { t.Errorf("expected reason MethodNotAllowed, got %q", status.Reason) } if status.Code != http.StatusMethodNotAllowed { t.Errorf("expected code 405, got %d", status.Code) } }) } } func TestHandler_BlocksUpgrade(t *testing.T) { handler, _ := newTestHandler(t) tests := []struct { name string connection string upgrade string }{ {"SPDY upgrade", "Upgrade", "SPDY/3.1"}, {"WebSocket upgrade", "Upgrade", "websocket"}, {"Upgrade header only", "", "websocket"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/pods/foo/exec", nil) if tt.connection != "" { req.Header.Set("Connection", tt.connection) } if tt.upgrade != "" { req.Header.Set("Upgrade", tt.upgrade) } rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405, got %d", rr.Code) } }) } } func TestHandler_AllowsNonMutatingPOST(t *testing.T) { handler, _ := newTestHandler(t) paths := []string{ "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", "/apis/authorization.k8s.io/v1/subjectaccessreviews", "/apis/authorization.k8s.io/v1/namespaces/default/localsubjectaccessreviews", "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews", "/apis/authentication.k8s.io/v1/tokenreviews", "/apis/authentication.k8s.io/v1/selfsubjectreviews", } for _, path := range paths { t.Run(path, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, path, nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200 for POST %s, got %d", path, rr.Code) } }) } } func TestHandler_AllowsDryRun(t *testing.T) { handler, _ := newTestHandler(t) for _, method := range []string{ http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, } { t.Run(method, func(t *testing.T) { req := httptest.NewRequest(method, "/api/v1/namespaces?dryRun=All", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200 for %s with dryRun=All, got %d", method, rr.Code) } }) } } func TestHandler_BlocksDryRunNone(t *testing.T) { handler, _ := newTestHandler(t) req := httptest.NewRequest(http.MethodPost, "/api/v1/namespaces?dryRun=None", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("expected 405 for dryRun=None, got %d", rr.Code) } } func TestHandler_GETResponsePassthrough(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"kind":"PodList","items":[]}`)) })) t.Cleanup(backend.Close) target, _ := url.Parse(backend.URL) handler := NewHandler(target, http.DefaultTransport) req := httptest.NewRequest(http.MethodGet, "/api/v1/pods", nil) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) body, _ := io.ReadAll(rr.Body) if string(body) != `{"kind":"PodList","items":[]}` { t.Errorf("unexpected response body: %s", body) } } kubectx-0.11.0/internal/testutil/000077500000000000000000000000001516137246200167365ustar00rootroot00000000000000kubectx-0.11.0/internal/testutil/kubeconfigbuilder.go000066400000000000000000000031651516137246200227550ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package testutil import ( "strings" "testing" "sigs.k8s.io/kustomize/kyaml/yaml" ) type Context struct { Name string `yaml:"name,omitempty"` Context struct { Namespace string `yaml:"namespace,omitempty"` } `yaml:"context,omitempty"` } func Ctx(name string) *Context { return &Context{Name: name} } func (c *Context) Ns(ns string) *Context { c.Context.Namespace = ns; return c } type Kubeconfig map[string]any func KC() *Kubeconfig { return &Kubeconfig{ "apiVersion": "v1", "kind": "Config"} } func (k *Kubeconfig) Set(key string, v any) *Kubeconfig { (*k)[key] = v; return k } func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k } func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k } func (k *Kubeconfig) ToYAML(t *testing.T) string { t.Helper() var v strings.Builder enc := yaml.NewEncoder(&v) enc.SetIndent(0) if err := enc.Encode(*k); err != nil { t.Fatalf("failed to encode mock kubeconfig: %v", err) } return v.String() } kubectx-0.11.0/kubectx000077500000000000000000000141671516137246200146510ustar00rootroot00000000000000#!/usr/bin/env bash # # kubectx(1) is a utility to manage and switch between kubectl contexts. # Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. [[ -n $DEBUG ]] && set -x set -eou pipefail IFS=$'\n\t' SELF_CMD="$0" KUBECTX="${XDG_CACHE_HOME:-$HOME/.kube}/kubectx" usage() { local SELF SELF="kubectx" if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin SELF="kubectl ctx" fi cat < : switch to context $SELF - : switch to the previous context $SELF -c, --current : show the current context name $SELF = : rename context to $SELF =. : rename current-context to $SELF -d [] : delete context ('.' for current-context) (this command won't delete the user/cluster entry that is used by the context) $SELF -u, --unset : unset the current context $SELF -h,--help : show this message (This executable is the legacy bash-based implementation, consider upgrading to Go-based implementation.) EOF } exit_err() { echo >&2 "${1}" exit 1 } current_context() { $KUBECTL config view -o=jsonpath='{.current-context}' } get_contexts() { $KUBECTL config get-contexts -o=name | sort -n } list_contexts() { set -u pipefail local cur ctx_list cur="$(current_context)" || exit_err "error getting current context" ctx_list=$(get_contexts) || exit_err "error getting context list" local yellow darkbg normal yellow=$(tput setaf 3 || true) darkbg=$(tput setab 0 || true) normal=$(tput sgr0 || true) local cur_ctx_fg cur_ctx_bg cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow} cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg} for c in $ctx_list; do if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \ -t 1 && -z "${NO_COLOR:-}" ]]; then # colored output mode if [[ "${c}" = "${cur}" ]]; then echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}" else echo "${c}" fi else echo "${c}" fi done } read_context() { if [[ -f "${KUBECTX}" ]]; then cat "${KUBECTX}" fi } save_context() { local saved saved="$(read_context)" if [[ "${saved}" != "${1}" ]]; then printf %s "${1}" > "${KUBECTX}" fi } switch_context() { $KUBECTL config use-context "${1}" } choose_context_interactive() { local choice choice="$(_KUBECTX_FORCE_COLOR=1 \ FZF_DEFAULT_COMMAND="${SELF_CMD}" \ fzf --ansi --no-preview || true)" if [[ -z "${choice}" ]]; then echo 2>&1 "error: you did not choose any of the options" exit 1 else set_context "${choice}" fi } set_context() { local prev prev="$(current_context)" || exit_err "error getting current context" switch_context "${1}" if [[ "${prev}" != "${1}" ]]; then save_context "${prev}" fi } swap_context() { local ctx ctx="$(read_context)" if [[ -z "${ctx}" ]]; then echo "error: No previous context found." >&2 exit 1 fi set_context "${ctx}" } context_exists() { grep -q ^"${1}"\$ <($KUBECTL config get-contexts -o=name) } rename_context() { local old_name="${1}" local new_name="${2}" if [[ "${old_name}" == "." ]]; then old_name="$(current_context)" fi if ! context_exists "${old_name}"; then echo "error: Context \"${old_name}\" not found, can't rename it." >&2 exit 1 fi if context_exists "${new_name}"; then echo "Context \"${new_name}\" exists, deleting..." >&2 $KUBECTL config delete-context "${new_name}" 1>/dev/null 2>&1 fi $KUBECTL config rename-context "${old_name}" "${new_name}" } delete_contexts() { for i in "${@}"; do delete_context "${i}" done } delete_context() { local ctx ctx="${1}" if [[ "${ctx}" == "." ]]; then ctx="$(current_context)" || exit_err "error getting current context" fi echo "Deleting context \"${ctx}\"..." >&2 $KUBECTL config delete-context "${ctx}" } unset_context() { echo "Unsetting current context." >&2 $KUBECTL config unset current-context } main() { if [[ -z "${KUBECTL:-}" ]]; then if hash kubectl 2>/dev/null; then KUBECTL=kubectl elif hash kubectl.exe 2>/dev/null; then KUBECTL=kubectl.exe else echo >&2 "kubectl is not installed" exit 1 fi fi if [[ "$#" -eq 0 ]]; then if [[ -t 1 && -z "${KUBECTX_IGNORE_FZF:-}" && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then choose_context_interactive else list_contexts fi elif [[ "${1}" == "-d" ]]; then if [[ "$#" -lt 2 ]]; then echo "error: missing context NAME" >&2 usage exit 1 fi delete_contexts "${@:2}" elif [[ "$#" -gt 1 ]]; then echo "error: too many arguments" >&2 usage exit 1 elif [[ "$#" -eq 1 ]]; then if [[ "${1}" == "-" ]]; then swap_context elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then # we don't call current_context here for two reasons: # - it does not fail when current-context property is not set # - it does not return a trailing newline $KUBECTL config current-context elif [[ "${1}" == '-u' || "${1}" == '--unset' ]]; then unset_context elif [[ "${1}" == '-h' || "${1}" == '--help' ]]; then usage elif [[ "${1}" =~ ^-(.*) ]]; then echo "error: unrecognized flag \"${1}\"" >&2 usage exit 1 elif [[ "${1}" =~ (.+)=(.+) ]]; then rename_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" else set_context "${1}" fi else usage exit 1 fi } main "$@" kubectx-0.11.0/kubens000077500000000000000000000131071516137246200144640ustar00rootroot00000000000000#!/usr/bin/env bash # # kubens(1) is a utility to switch between Kubernetes namespaces. # Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. [[ -n $DEBUG ]] && set -x set -eou pipefail IFS=$'\n\t' SELF_CMD="$0" KUBENS_DIR="${XDG_CACHE_HOME:-$HOME/.kube}/kubens" usage() { local SELF SELF="kubens" if [[ "$(basename "$0")" == kubectl-* ]]; then # invoked as plugin SELF="kubectl ns" fi cat < : change the active namespace of current context $SELF - : switch to the previous namespace in this context $SELF -c, --current : show the current namespace $SELF -h,--help : show this message (This executable is the legacy bash-based implementation, consider upgrading to Go-based implementation.) EOF } exit_err() { echo >&2 "${1}" exit 1 } current_namespace() { local cur_ctx cur_ctx="$(current_context)" || exit_err "error getting current context" ns="$($KUBECTL config view -o=jsonpath="{.contexts[?(@.name==\"${cur_ctx}\")].context.namespace}")" \ || exit_err "error getting current namespace" if [[ -z "${ns}" ]]; then echo "default" else echo "${ns}" fi } current_context() { $KUBECTL config current-context } get_namespaces() { $KUBECTL get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}' } escape_context_name() { echo "${1//\//-}" } namespace_file() { local ctx ctx="$(escape_context_name "${1}")" echo "${KUBENS_DIR}/${ctx}" } read_namespace() { local f f="$(namespace_file "${1}")" [[ -f "${f}" ]] && cat "${f}" return 0 } save_namespace() { mkdir -p "${KUBENS_DIR}" local f saved f="$(namespace_file "${1}")" saved="$(read_namespace "${1}")" if [[ "${saved}" != "${2}" ]]; then printf %s "${2}" > "${f}" fi } switch_namespace() { local ctx="${1}" $KUBECTL config set-context "${ctx}" --namespace="${2}" echo "Active namespace is \"${2}\".">&2 } choose_namespace_interactive() { # directly calling kubens via fzf might fail with a cryptic error like # "$FZF_DEFAULT_COMMAND failed", so try to see if we can list namespaces # locally first if [[ -z "$(list_namespaces)" ]]; then echo >&2 "error: could not list namespaces (is the cluster accessible?)" exit 1 fi local choice choice="$(_KUBECTX_FORCE_COLOR=1 \ FZF_DEFAULT_COMMAND="${SELF_CMD}" \ fzf --ansi --no-preview || true)" if [[ -z "${choice}" ]]; then echo 2>&1 "error: you did not choose any of the options" exit 1 else set_namespace "${choice}" fi } set_namespace() { local ctx prev ctx="$(current_context)" || exit_err "error getting current context" prev="$(current_namespace)" || exit_error "error getting current namespace" if grep -q ^"${1}"\$ <(get_namespaces); then switch_namespace "${ctx}" "${1}" if [[ "${prev}" != "${1}" ]]; then save_namespace "${ctx}" "${prev}" fi else echo "error: no namespace exists with name \"${1}\".">&2 exit 1 fi } list_namespaces() { local yellow darkbg normal yellow=$(tput setaf 3 || true) darkbg=$(tput setab 0 || true) normal=$(tput sgr0 || true) local cur_ctx_fg cur_ctx_bg cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow} cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg} local cur ns_list cur="$(current_namespace)" || exit_err "error getting current namespace" ns_list=$(get_namespaces) || exit_err "error getting namespace list" for c in $ns_list; do if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \ -t 1 && -z "${NO_COLOR:-}" ]]; then # colored output mode if [[ "${c}" = "${cur}" ]]; then echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}" else echo "${c}" fi else echo "${c}" fi done } swap_namespace() { local ctx ns ctx="$(current_context)" || exit_err "error getting current context" ns="$(read_namespace "${ctx}")" if [[ -z "${ns}" ]]; then echo "error: No previous namespace found for current context." >&2 exit 1 fi set_namespace "${ns}" } main() { if [[ -z "${KUBECTL:-}" ]]; then if hash kubectl 2>/dev/null; then KUBECTL=kubectl elif hash kubectl.exe 2>/dev/null; then KUBECTL=kubectl.exe else echo >&2 "kubectl is not installed" exit 1 fi fi if [[ "$#" -eq 0 ]]; then if [[ -t 1 && -z ${KUBECTX_IGNORE_FZF:-} && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then choose_namespace_interactive else list_namespaces fi elif [[ "$#" -eq 1 ]]; then if [[ "${1}" == '-h' || "${1}" == '--help' ]]; then usage elif [[ "${1}" == "-" ]]; then swap_namespace elif [[ "${1}" == '-c' || "${1}" == '--current' ]]; then current_namespace elif [[ "${1}" =~ ^-(.*) ]]; then echo "error: unrecognized flag \"${1}\"" >&2 usage exit 1 elif [[ "${1}" =~ (.+)=(.+) ]]; then alias_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" else set_namespace "${1}" fi else echo "error: too many flags" >&2 usage exit 1 fi } main "$@" kubectx-0.11.0/test/000077500000000000000000000000001516137246200142245ustar00rootroot00000000000000kubectx-0.11.0/test/common.bash000066400000000000000000000010731516137246200163540ustar00rootroot00000000000000#!/usr/bin/env bats # bats setup function setup() { TEMP_HOME="$(mktemp -d)" export TEMP_HOME export HOME=$TEMP_HOME export KUBECONFIG="${TEMP_HOME}/config" } # bats teardown function teardown() { rm -rf "$TEMP_HOME" } use_config() { cp "$BATS_TEST_DIRNAME/testdata/$1" $KUBECONFIG } # wrappers around "kubectl config" command get_namespace() { kubectl config view -o=jsonpath="{.contexts[?(@.name==\"$(get_context)\")].context.namespace}" } get_context() { kubectl config current-context } switch_context() { kubectl config use-context "${1}" } kubectx-0.11.0/test/kubectx.bats000066400000000000000000000112421516137246200165440ustar00rootroot00000000000000#!/usr/bin/env bats COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubectx}" load common @test "--help should not fail" { run ${COMMAND} --help echo "$output" [ "$status" -eq 0 ] } @test "-h should not fail" { run ${COMMAND} -h echo "$output" [ "$status" -eq 0 ] } @test "switch to previous context when no one exists" { use_config config1 run ${COMMAND} - echo "$output" [ "$status" -eq 1 ] [[ $output = *"no previous context found" ]] } @test "list contexts when no kubeconfig exists" { run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "warning: kubeconfig file not found" ]] } @test "get one context and list contexts" { use_config config1 run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "user1@cluster1" ]] } @test "get two contexts and list contexts" { use_config config2 run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = *"user1@cluster1"* ]] [[ "$output" = *"user2@cluster1"* ]] } @test "get two contexts and select contexts" { use_config config2 run ${COMMAND} user1@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user1@cluster1" ]] run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user2@cluster1" ]] } @test "get two contexts and switch between contexts" { use_config config2 run ${COMMAND} user1@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user1@cluster1" ]] run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user2@cluster1" ]] run ${COMMAND} - echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user1@cluster1" ]] run ${COMMAND} - echo "$output" [ "$status" -eq 0 ] echo "$(get_context)" [[ "$(get_context)" = "user2@cluster1" ]] } @test "get one context and switch to non existent context" { use_config config1 run ${COMMAND} "unknown-context" echo "$output" [ "$status" -eq 1 ] } @test "-c/--current fails when no context set" { use_config config1 run "${COMMAND}" -c echo "$output" [ $status -eq 1 ] run "${COMMAND}" --current echo "$output" [ $status -eq 1 ] } @test "-c/--current prints the current context" { use_config config1 run "${COMMAND}" user1@cluster1 [ $status -eq 0 ] run "${COMMAND}" -c echo "$output" [ $status -eq 0 ] [[ "$output" = "user1@cluster1" ]] run "${COMMAND}" --current echo "$output" [ $status -eq 0 ] [[ "$output" = "user1@cluster1" ]] } @test "rename context" { use_config config2 run ${COMMAND} "new-context=user1@cluster1" echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = *"user1@cluster1"* ]] [[ "$output" = *"new-context"* ]] [[ "$output" = *"user2@cluster1"* ]] } @test "rename current context" { use_config config2 run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] run ${COMMAND} new-context=. echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = *"user2@cluster1"* ]] [[ "$output" = *"user1@cluster1"* ]] [[ "$output" = *"new-context"* ]] } @test "delete context" { use_config config2 run ${COMMAND} -d "user1@cluster1" echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = "user1@cluster1" ]] [[ "$output" = "user2@cluster1" ]] } @test "delete current context" { use_config config2 run ${COMMAND} user2@cluster1 echo "$output" [ "$status" -eq 0 ] run ${COMMAND} -d . echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ ! "$output" = "user2@cluster1" ]] [[ "$output" = "user1@cluster1" ]] } @test "delete non existent context" { use_config config1 run ${COMMAND} -d "unknown-context" echo "$output" [ "$status" -eq 1 ] } @test "delete several contexts" { use_config config2 run ${COMMAND} -d "user1@cluster1" "user2@cluster1" echo "$output" [ "$status" -eq 0 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "" ]] } @test "delete several contexts including a non existent one" { use_config config2 run ${COMMAND} -d "user1@cluster1" "non-existent" "user2@cluster1" echo "$output" [ "$status" -eq 1 ] run ${COMMAND} echo "$output" [ "$status" -eq 0 ] [[ "$output" = "user2@cluster1" ]] } @test "unset selected context" { use_config config2 run ${COMMAND} user1@cluster1 [ "$status" -eq 0 ] run ${COMMAND} -u [ "$status" -eq 0 ] run ${COMMAND} -c [ "$status" -ne 0 ] } kubectx-0.11.0/test/kubens.bats000066400000000000000000000061111516137246200163650ustar00rootroot00000000000000#!/usr/bin/env bats COMMAND="${COMMAND:-$BATS_TEST_DIRNAME/../kubens}" # TODO(ahmetb) remove this after bash implementations are deleted export KUBECTL="$BATS_TEST_DIRNAME/../test/mock-kubectl" # short-circuit namespace querying in kubens go implementation export _MOCK_NAMESPACES=1 load common @test "--help should not fail" { run ${COMMAND} --help echo "$output">&2 [[ "$status" -eq 0 ]] } @test "-h should not fail" { run ${COMMAND} -h echo "$output">&2 [[ "$status" -eq 0 ]] } @test "list namespaces when no kubeconfig exists" { run ${COMMAND} echo "$output" [[ "$status" -eq 1 ]] } @test "list namespaces" { use_config config1 switch_context user1@cluster1 run ${COMMAND} echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = *"ns1"* ]] [[ "$output" = *"ns2"* ]] } @test "switch to existing namespace" { use_config config1 switch_context user1@cluster1 run ${COMMAND} "ns1" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = *'Active namespace is "ns1"'* ]] } @test "switch to non-existing namespace" { use_config config1 switch_context user1@cluster1 run ${COMMAND} "unknown-namespace" echo "$output" [[ "$status" -eq 1 ]] [[ "$output" = *'no namespace exists with name "unknown-namespace"'* ]] } @test "switch between namespaces" { use_config config1 switch_context user1@cluster1 run ${COMMAND} ns1 echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns1" ]] run ${COMMAND} ns2 echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns2" ]] run ${COMMAND} - echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns1" ]] run ${COMMAND} - echo "$output" [[ "$status" -eq 0 ]] echo "$(get_namespace)" [[ "$(get_namespace)" = "ns2" ]] } @test "switch to previous namespace when none exists" { use_config config1 switch_context user1@cluster1 run ${COMMAND} - echo "$output" [[ "$status" -eq 1 ]] [[ "$output" = *"No previous namespace found for current context"* ]] } @test "switch to namespace when current context is empty" { use_config config1 run ${COMMAND} - echo "$output" [[ "$status" -eq 1 ]] [[ "$output" = *"current-context is not set"* ]] } @test "-c/--current works when no namespace is set on context" { use_config config1 switch_context user1@cluster1 run ${COMMAND} "-c" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "default" ]] run ${COMMAND} "--current" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "default" ]] } @test "-c/--current prints the namespace after it is set" { use_config config1 switch_context user1@cluster1 ${COMMAND} ns1 run ${COMMAND} "-c" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "ns1" ]] run ${COMMAND} "--current" echo "$output" [[ "$status" -eq 0 ]] [[ "$output" = "ns1" ]] } @test "-c/--current fails when current context is not set" { use_config config1 run ${COMMAND} -c echo "$output" [[ "$status" -eq 1 ]] run ${COMMAND} --current echo "$output" [[ "$status" -eq 1 ]] } kubectx-0.11.0/test/mock-kubectl000077500000000000000000000002321516137246200165270ustar00rootroot00000000000000#!/usr/bin/env bash [[ -n $DEBUG ]] && set -x set -eou pipefail if [[ $@ == *'get namespaces'* ]]; then echo "ns1" echo "ns2" else kubectl $@ fi kubectx-0.11.0/test/testdata/000077500000000000000000000000001516137246200160355ustar00rootroot00000000000000kubectx-0.11.0/test/testdata/config1000066400000000000000000000004021516137246200173020ustar00rootroot00000000000000# config with one context apiVersion: v1 clusters: - cluster: server: "" name: cluster1 contexts: - context: cluster: cluster1 user: user1 name: user1@cluster1 current-context: "" kind: Config preferences: {} users: - name: user1 user: {} kubectx-0.11.0/test/testdata/config2000066400000000000000000000005441516137246200173120ustar00rootroot00000000000000# config with two contexts apiVersion: v1 clusters: - cluster: server: "" name: cluster1 contexts: - context: cluster: cluster1 user: user1 name: user1@cluster1 - context: cluster: cluster1 user: user2 name: user2@cluster1 current-context: "" kind: Config preferences: {} users: - name: user1 user: {} - name: user2 user: {}