├── .github └── workflows │ ├── check-release.yml │ ├── lint.yml │ ├── release.yml │ ├── size.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── Formula └── kubectl-reap.rb ├── LICENSE ├── README.md ├── cmd └── kubectl-reap │ └── main.go ├── docs └── assets │ └── screencast.gif ├── go.mod ├── go.sum └── pkg ├── cmd ├── cmd.go └── cmd_test.go ├── determiner ├── determiner.go ├── determiner_test.go └── fake.go ├── prompt └── prompt.go ├── resource ├── client.go ├── client_test.go ├── fake.go ├── persistentvolume.go ├── persistentvolume_test.go └── resource.go └── version └── version.go /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - labeled 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions-ecosystem/action-release-label@v1 15 | id: release-label 16 | if: ${{ startsWith(github.event.label.name, 'release/') }} 17 | 18 | - uses: actions-ecosystem/action-get-latest-tag@v1 19 | id: get-latest-tag 20 | if: ${{ steps.release-label.outputs.level != null }} 21 | with: 22 | semver_only: true 23 | 24 | - uses: actions-ecosystem/action-bump-semver@v1 25 | id: bump-semver 26 | if: ${{ steps.release-label.outputs.level != null }} 27 | with: 28 | current_version: ${{ steps.get-latest-tag.outputs.tag }} 29 | level: ${{ steps.release-label.outputs.level }} 30 | 31 | - uses: actions-ecosystem/action-create-comment@v1 32 | if: ${{ steps.bump-semver.outputs.new_version != null }} 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | body: | 36 | This PR will update [${{ github.repository }}](https://github.com/${{ github.repository }}) from [${{ steps.get-latest-tag.outputs.tag }}](https://github.com/${{ github.repository }}/releases/tag/${{ steps.get-latest-tag.outputs.tag }}) to **${{ steps.bump-semver.outputs.new_version }}** :rocket: 37 | 38 | Changes: https://github.com/${{ github.repository }}/compare/${{ steps.get-latest-tag.outputs.tag }}...${{ github.event.pull_request.head.ref }} 39 | 40 | If this update isn't as you expected, you may want to change or remove the *release label*. 41 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: reviewdog/action-golangci-lint@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.15 21 | 22 | - name: Cache Go modules 23 | uses: actions/cache@v2 24 | with: 25 | path: ~/go/pkg/mod 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | 30 | - name: Get pull request 31 | uses: actions-ecosystem/action-get-merged-pull-request@v1 32 | id: get-merged-pull-request 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Get release label 37 | uses: actions-ecosystem/action-release-label@v1 38 | id: release-label 39 | if: ${{ steps.get-merged-pull-request.outputs.title != null }} 40 | with: 41 | labels: ${{ steps.get-merged-pull-request.outputs.labels }} 42 | 43 | - name: Get latest Git tag 44 | uses: actions-ecosystem/action-get-latest-tag@v1 45 | id: get-latest-tag 46 | if: ${{ steps.release-label.outputs.level != null }} 47 | with: 48 | semver_only: true 49 | 50 | - name: Bump up version 51 | uses: actions-ecosystem/action-bump-semver@v1 52 | id: bump-semver 53 | if: ${{ steps.release-label.outputs.level != null }} 54 | with: 55 | current_version: ${{ steps.get-latest-tag.outputs.tag }} 56 | level: ${{ steps.release-label.outputs.level }} 57 | 58 | - name: Push new Git tag 59 | uses: actions-ecosystem/action-push-tag@v1 60 | if: ${{ steps.bump-semver.outputs.new_version != null }} 61 | with: 62 | tag: ${{ steps.bump-semver.outputs.new_version }} 63 | message: "${{ steps.bump-semver.outputs.new_version }}: PR #${{ steps.get-merged-pull-request.outputs.number }} ${{ steps.get-merged-pull-request.outputs.title }}" 64 | 65 | - name: Release binaries with GoReleaser 66 | uses: goreleaser/goreleaser-action@v2 67 | if: ${{ steps.release-label.outputs.level == 'major' || steps.release-label.outputs.level == 'minor' || steps.release-label.outputs.level == 'patch' }} 68 | with: 69 | version: latest 70 | args: release --rm-dist 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - name: Post release comment 75 | uses: actions-ecosystem/action-create-comment@v1 76 | if: ${{ steps.bump-semver.outputs.new_version != null }} 77 | with: 78 | github_token: ${{ secrets.GITHUB_TOKEN }} 79 | number: ${{ steps.get-merged-pull-request.outputs.number }} 80 | body: | 81 | The new version [${{ steps.bump-semver.outputs.new_version }}](https://github.com/${{ github.repository }}/releases/tag/${{ steps.bump-semver.outputs.new_version }}) has been released :tada: 82 | 83 | Changes: https://github.com/${{ github.repository }}/compare/${{ steps.get-latest-tag.outputs.tag }}...${{ steps.bump-semver.outputs.new_version }} 84 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: Size 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize] 6 | 7 | jobs: 8 | update_labels: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - uses: actions-ecosystem/action-size@v2 14 | id: size 15 | 16 | - uses: actions-ecosystem/action-remove-labels@v1 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} 19 | labels: ${{ steps.size.outputs.stale_labels }} 20 | 21 | - uses: actions-ecosystem/action-add-labels@v1 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | labels: ${{ steps.size.outputs.new_label }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - uses: actions/setup-go@v2 13 | id: go 14 | with: 15 | go-version: 1.15 16 | 17 | - uses: actions/cache@v2 18 | id: cache 19 | with: 20 | path: ~/go/pkg/mod 21 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 22 | restore-keys: | 23 | ${{ runner.os }}-go- 24 | 25 | - run: go test -v -race -coverprofile=coverage.out ./... 26 | 27 | - uses: codecov/codecov-action@v1 28 | with: 29 | files: coverage.out 30 | verbose: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 2m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - bodyclose 8 | - deadcode 9 | - depguard 10 | - dogsled 11 | - errcheck 12 | - exportloopref 13 | - gocritic 14 | - gocyclo 15 | - goerr113 16 | - gofmt 17 | - goimports 18 | - goprintffuncname 19 | - gosec 20 | - gosimple 21 | - govet 22 | - ineffassign 23 | - misspell 24 | - nakedret 25 | - nolintlint 26 | - prealloc 27 | - rowserrcheck 28 | - scopelint 29 | - staticcheck 30 | - structcheck 31 | - stylecheck 32 | - typecheck 33 | - unconvert 34 | - unparam 35 | - unused 36 | - varcheck 37 | - whitespace 38 | 39 | linters-settings: 40 | goimports: 41 | local-prefixes: github.com/micnncim/kubectl-reap 42 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./cmd/kubectl-reap/main.go 3 | ldflags: 4 | - -s -w 5 | - -X github.com/micnncim/kubectl-reap/pkg/version.Version={{.Tag}} 6 | - -X github.com/micnncim/kubectl-reap/pkg/version.Revision={{.ShortCommit}} 7 | goos: 8 | - linux 9 | - darwin 10 | - windows 11 | 12 | brews: 13 | - tap: 14 | owner: micnncim 15 | name: kubectl-reap 16 | folder: Formula 17 | homepage: https://github.com/micnncim/kubectl-reap 18 | description: kubectl plugin that deletes unused Kubernetes resources 19 | -------------------------------------------------------------------------------- /Formula/kubectl-reap.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | # frozen_string_literal: true 3 | 4 | # This file was generated by GoReleaser. DO NOT EDIT. 5 | class KubectlReap < Formula 6 | desc "kubectl plugin that deletes unused Kubernetes resources" 7 | homepage "https://github.com/micnncim/kubectl-reap" 8 | version "0.11.3" 9 | bottle :unneeded 10 | 11 | if OS.mac? 12 | url "https://github.com/micnncim/kubectl-reap/releases/download/v0.11.3/kubectl-reap_0.11.3_darwin_amd64.tar.gz" 13 | sha256 "53c2074e2dcab8c4d513013de9bd9746f4c4504c4bf07fb4956607de4766ed20" 14 | end 15 | if OS.linux? && Hardware::CPU.intel? 16 | url "https://github.com/micnncim/kubectl-reap/releases/download/v0.11.3/kubectl-reap_0.11.3_linux_amd64.tar.gz" 17 | sha256 "ceec75c07a030717f2658a77c459767b653c31b9f204d767633535f28af57295" 18 | end 19 | if OS.linux? && Hardware::CPU.arm? && Hardware::CPU.is_64_bit? 20 | url "https://github.com/micnncim/kubectl-reap/releases/download/v0.11.3/kubectl-reap_0.11.3_linux_arm64.tar.gz" 21 | sha256 "4af1337b93f2098eb5138349a6692b5a616470a7d2372890cd99cb3c30f7f532" 22 | end 23 | 24 | def install 25 | bin.install "kubectl-reap" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 micnncim 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubectl-reap 2 | 3 | [![actions-workflow-test][actions-workflow-test-badge]][actions-workflow-test] 4 | [![release][release-badge]][release] 5 | [![codecov][codecov-badge]][codecov] 6 | [![pkg.go.dev][pkg.go.dev-badge]][pkg.go.dev] 7 | [![license][license-badge]][license] 8 | 9 | `kubectl-reap` is a kubectl plugin that deletes unused Kubernetes resources. 10 | 11 | ![screencast](/docs/assets/screencast.gif) 12 | 13 | Supported resources: 14 | 15 | | Kind | Condition | 16 | | ----------------------- | ---------------------------------------------------------- | 17 | | Pod | Not running | 18 | | ConfigMap | Not referenced by any Pods or ReplicaSet | 19 | | Secret | Not referenced by any Pods, ReplicaSet, or ServiceAccounts | 20 | | PersistentVolume | Not satisfying any PersistentVolumeClaims | 21 | | PersistentVolumeClaim | Not referenced by any Pods | 22 | | Job | Completed | 23 | | PodDisruptionBudget | Not targeting any Pods | 24 | | HorizontalPodAutoscaler | Not targeting any resources | 25 | 26 | Since this plugin supports dry-run as described below, it also helps you to find resources you misconfigured or forgot to delete. 27 | 28 | Before getting started, read [the caveats of using this plugin](#caveats). 29 | 30 | ## Installation 31 | 32 | Download precompiled binaries from [GitHub Releases](https://github.com/micnncim/kubectl-reap/releases). 33 | 34 | ### Via [Krew](https://github.com/kubernetes-sigs/krew) 35 | 36 | ``` 37 | $ kubectl krew install reap 38 | ``` 39 | 40 | ### Via Homebrew 41 | 42 | ``` 43 | $ brew tap micnncim/kubectl-reap https://github.com/micnncim/kubectl-reap 44 | $ brew install kubectl-reap 45 | ``` 46 | 47 | ### Via Go 48 | 49 | ``` 50 | $ go get github.com/micnncim/kubectl-reap/cmd/kubectl-reap 51 | ``` 52 | 53 | ## Examples 54 | 55 | ### Pods 56 | 57 | In this example, this plugin deletes all Pods whose status is not `Running`. 58 | 59 | ```console 60 | $ kubectl get po 61 | NAME READY STATUS RESTARTS AGE 62 | pod-running 1/1 Running 0 10s 63 | pod-pending 0/1 Pending 0 20s 64 | pod-failed 0/1 Failed 0 30s 65 | pod-unknown 0/1 Unknown 0 40s 66 | job-kqpxc 0/1 Completed 0 50s 67 | 68 | $ kubectl reap po 69 | pod/pod-pending deleted 70 | pod/pod-failed deleted 71 | pod/pod-unknown deleted 72 | pod/job-kqpxc deleted 73 | ``` 74 | 75 | ### ConfigMaps 76 | 77 | In this example, this plugin deletes the unused ConfigMap `config-2`. 78 | 79 | ```console 80 | $ kubectl get cm 81 | NAME DATA AGE 82 | config-1 1 0m15s 83 | config-2 1 0m10s 84 | 85 | $ cat < 208 | 209 | [actions-workflow-test]: https://github.com/micnncim/kubectl-reap/actions?query=workflow%3ATest 210 | [actions-workflow-test-badge]: https://img.shields.io/github/workflow/status/micnncim/kubectl-reap/Test?label=Test&style=for-the-badge&logo=github 211 | 212 | [release]: https://github.com/micnncim/kubectl-reap/releases 213 | [release-badge]: https://img.shields.io/github/v/release/micnncim/kubectl-reap?style=for-the-badge&logo=github 214 | 215 | [codecov]: https://codecov.io/gh/micnncim/kubectl-reap 216 | [codecov-badge]: https://img.shields.io/codecov/c/github/micnncim/kubectl-reap?style=for-the-badge&logo=codecov 217 | 218 | [pkg.go.dev]: https://pkg.go.dev/github.com/micnncim/kubectl-reap?tab=overview 219 | [pkg.go.dev-badge]: http://bit.ly/pkg-go-dev-badge 220 | 221 | [license]: LICENSE 222 | [license-badge]: https://img.shields.io/github/license/micnncim/kubectl-reap?style=for-the-badge 223 | -------------------------------------------------------------------------------- /cmd/kubectl-reap/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | 9 | "github.com/micnncim/kubectl-reap/pkg/cmd" 10 | ) 11 | 12 | func main() { 13 | cmd := cmd.NewCmdReap(genericclioptions.IOStreams{ 14 | In: os.Stdin, 15 | Out: os.Stdout, 16 | ErrOut: os.Stderr, 17 | }) 18 | 19 | if err := cmd.Execute(); err != nil { 20 | fmt.Fprintln(os.Stderr, err) 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/assets/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micnncim/kubectl-reap/94cd3f5c8ceabc8a696355111828a90c7cd3beb8/docs/assets/screencast.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/micnncim/kubectl-reap 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.1.1 7 | github.com/google/go-cmp v0.5.2 8 | github.com/spf13/cobra v1.0.0 9 | k8s.io/api v0.19.0 10 | k8s.io/apimachinery v0.19.0 11 | k8s.io/cli-runtime v0.19.0 12 | k8s.io/client-go v0.19.0 13 | k8s.io/kubectl v0.19.0 14 | ) 15 | 16 | require ( 17 | cloud.google.com/go v0.51.0 // indirect 18 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 19 | github.com/Azure/go-autorest/autorest v0.9.6 // indirect 20 | github.com/Azure/go-autorest/autorest/adal v0.8.2 // indirect 21 | github.com/Azure/go-autorest/autorest/date v0.2.0 // indirect 22 | github.com/Azure/go-autorest/logger v0.1.0 // indirect 23 | github.com/Azure/go-autorest/tracing v0.5.0 // indirect 24 | github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect 25 | github.com/PuerkitoBio/purell v1.1.1 // indirect 26 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 29 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 // indirect 30 | github.com/emicklei/go-restful v2.16.0+incompatible // indirect 31 | github.com/evanphx/json-patch v4.9.0+incompatible // indirect 32 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 33 | github.com/ghodss/yaml v1.0.0 // indirect 34 | github.com/go-logr/logr v0.2.0 // indirect 35 | github.com/go-openapi/jsonpointer v0.19.3 // indirect 36 | github.com/go-openapi/jsonreference v0.19.3 // indirect 37 | github.com/go-openapi/spec v0.19.3 // indirect 38 | github.com/go-openapi/swag v0.19.5 // indirect 39 | github.com/gogo/protobuf v1.3.1 // indirect 40 | github.com/golang/protobuf v1.4.2 // indirect 41 | github.com/google/btree v1.0.0 // indirect 42 | github.com/google/gofuzz v1.1.0 // indirect 43 | github.com/googleapis/gnostic v0.4.1 // indirect 44 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 45 | github.com/hashicorp/golang-lru v0.5.1 // indirect 46 | github.com/imdario/mergo v0.3.5 // indirect 47 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 48 | github.com/json-iterator/go v1.1.10 // indirect 49 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 50 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect 51 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 52 | github.com/mailru/easyjson v0.7.0 // indirect 53 | github.com/mattn/go-colorable v0.1.2 // indirect 54 | github.com/mattn/go-isatty v0.0.8 // indirect 55 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 56 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 57 | github.com/moby/term v0.0.0-20200312100748-672ec06f55cd // indirect 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 59 | github.com/modern-go/reflect2 v1.0.1 // indirect 60 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 61 | github.com/pkg/errors v0.9.1 // indirect 62 | github.com/russross/blackfriday v1.5.2 // indirect 63 | github.com/sirupsen/logrus v1.6.0 // indirect 64 | github.com/spf13/pflag v1.0.5 // indirect 65 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 66 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect 67 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect 68 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect 69 | golang.org/x/text v0.3.3 // indirect 70 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect 71 | google.golang.org/appengine v1.6.5 // indirect 72 | google.golang.org/protobuf v1.24.0 // indirect 73 | gopkg.in/inf.v0 v0.9.1 // indirect 74 | gopkg.in/yaml.v2 v2.2.8 // indirect 75 | k8s.io/component-base v0.19.0 // indirect 76 | k8s.io/klog/v2 v2.2.0 // indirect 77 | k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 // indirect 78 | k8s.io/utils v0.0.0-20200729134348-d5654de09c73 // indirect 79 | sigs.k8s.io/kustomize v2.0.3+incompatible // indirect 80 | sigs.k8s.io/structured-merge-diff/v4 v4.0.1 // indirect 81 | sigs.k8s.io/yaml v1.2.0 // indirect 82 | ) 83 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM= 9 | cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= 10 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 11 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 12 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 13 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 14 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 15 | github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI= 16 | github.com/AlecAivazis/survey/v2 v2.1.1/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= 17 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= 18 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 19 | github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 20 | github.com/Azure/go-autorest/autorest v0.9.6 h1:5YWtOnckcudzIw8lPPBcWOnmIFWMtHci1ZWAZulMSx0= 21 | github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= 22 | github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= 23 | github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0= 24 | github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= 25 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 26 | github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= 27 | github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= 28 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 29 | github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 30 | github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc= 31 | github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= 32 | github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= 33 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 34 | github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= 35 | github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= 36 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 37 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 38 | github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= 39 | github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= 40 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 41 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= 42 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 43 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 44 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 45 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 46 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 47 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 48 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 49 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 50 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 51 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 52 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 53 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 54 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 55 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 56 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 57 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 58 | github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 59 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 60 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 61 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 62 | github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= 63 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 64 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 65 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 66 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 67 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 68 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 69 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 70 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 71 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 72 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 73 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 74 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 75 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 76 | github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= 77 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 78 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 79 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 80 | github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 81 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= 82 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 83 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 84 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= 85 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 86 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 87 | github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 88 | github.com/emicklei/go-restful v2.16.0+incompatible h1:rgqiKNjTnFQA6kkhFe16D8epTksy9HQ1MyrbDXSdYhM= 89 | github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 90 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 91 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 92 | github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= 93 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 94 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= 95 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= 96 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 97 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 98 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 99 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 100 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 101 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 102 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 103 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 104 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 105 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 106 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 107 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 108 | github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= 109 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 110 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 111 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 112 | github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= 113 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 114 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 115 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 116 | github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= 117 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 118 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 119 | github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= 120 | github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 121 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 122 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 123 | github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= 124 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 125 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 126 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 127 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 128 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 129 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 130 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 131 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 132 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 133 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= 134 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 135 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 136 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 137 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 138 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 139 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 140 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 141 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 142 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 143 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 144 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 145 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 146 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 147 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 148 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 149 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 150 | github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= 151 | github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= 152 | github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= 153 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 154 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 155 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 156 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 157 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 158 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 159 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 160 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 161 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 162 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 163 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 164 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 165 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 166 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 167 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 168 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 169 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 170 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 171 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 172 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 173 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 174 | github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= 175 | github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= 176 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 177 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= 178 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 179 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 180 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 181 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 182 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 183 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 184 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 185 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 186 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= 187 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 188 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 189 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 190 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 191 | github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 192 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 193 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 194 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 195 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 196 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 197 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 198 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 199 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 200 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 201 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 202 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 203 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 204 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 205 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 206 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 207 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 208 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 209 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 210 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 211 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 212 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 213 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 214 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 215 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 216 | github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4= 217 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 218 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 219 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 220 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 221 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 222 | github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= 223 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 224 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 225 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 226 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 227 | github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= 228 | github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= 229 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 230 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 231 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 232 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 233 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 234 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 235 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 236 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 237 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 238 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 239 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 240 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 241 | github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI= 242 | github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= 243 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 244 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 245 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 246 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 247 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 248 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 249 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 250 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 251 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 252 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 253 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 254 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 255 | github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= 256 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 257 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 258 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 259 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 260 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 261 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 262 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 263 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 264 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 265 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 266 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 267 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 268 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 269 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 270 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 271 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 272 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 273 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 274 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 275 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 276 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 277 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 278 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 279 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 280 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 281 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 282 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 283 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 284 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 285 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 286 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 287 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 288 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 289 | github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= 290 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 291 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 292 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 293 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 294 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 295 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 296 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 297 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 298 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 299 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 300 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 301 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 302 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 303 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 304 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 305 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 306 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 307 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 308 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 309 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 310 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 311 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 312 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 313 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 314 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 315 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 316 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 317 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 318 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 319 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 320 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 321 | github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= 322 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 323 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 324 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 325 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 326 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 327 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 328 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 329 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 330 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 331 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 332 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 333 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 334 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 335 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 336 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 337 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 338 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 339 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 340 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 341 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 342 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 343 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 344 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 345 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 346 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 347 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 348 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 349 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 350 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 351 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 352 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 353 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 354 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 355 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 356 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 357 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 358 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 359 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 360 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 361 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 362 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 363 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 364 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 365 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 366 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 367 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 368 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 369 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 370 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 371 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 372 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 373 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 374 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 375 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 376 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 377 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 378 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 379 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 380 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 381 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= 382 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 383 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 384 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 385 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 386 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= 387 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 388 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 389 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 390 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 391 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 392 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 393 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 394 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 395 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 396 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 397 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 398 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 399 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 400 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 401 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 411 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= 418 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 420 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 421 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 422 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 423 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 424 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 425 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 426 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= 427 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 428 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 429 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 430 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 431 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 432 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 433 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 434 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 435 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 436 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 437 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 438 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 439 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 440 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 441 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 442 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 443 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 444 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 445 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 446 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 447 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 448 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 449 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 450 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 451 | golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 452 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 453 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 454 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 455 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 456 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 457 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 458 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 459 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 460 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 461 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 462 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 463 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 464 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 465 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 466 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 467 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 468 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 469 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 470 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 471 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 472 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 473 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 474 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 475 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 476 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 477 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 478 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 479 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 480 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 481 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 482 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 483 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 484 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 485 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 486 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 487 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 488 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 489 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 490 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 491 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 492 | google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= 493 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 494 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 495 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 496 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 497 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 498 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 499 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 500 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 501 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 502 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 503 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 504 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 505 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 506 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 507 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 508 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 509 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 510 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 511 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 512 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 513 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 514 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 515 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 516 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 517 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 518 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 519 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 520 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 521 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 522 | k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc= 523 | k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= 524 | k8s.io/apimachinery v0.19.0 h1:gjKnAda/HZp5k4xQYjL0K/Yb66IvNqjthCb03QlKpaQ= 525 | k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= 526 | k8s.io/cli-runtime v0.19.0 h1:wLe+osHSqcItyS3MYQXVyGFa54fppORVA8Jn7DBGSWw= 527 | k8s.io/cli-runtime v0.19.0/go.mod h1:tun9l0eUklT8IHIM0jors17KmUjcrAxn0myoBYwuNuo= 528 | k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k= 529 | k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= 530 | k8s.io/code-generator v0.19.0/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= 531 | k8s.io/component-base v0.19.0 h1:OueXf1q3RW7NlLlUCj2Dimwt7E1ys6ZqRnq53l2YuoE= 532 | k8s.io/component-base v0.19.0/go.mod h1:dKsY8BxkA+9dZIAh2aWJLL/UdASFDNtGYTCItL4LM7Y= 533 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 534 | k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 535 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 536 | k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= 537 | k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 538 | k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ= 539 | k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= 540 | k8s.io/kubectl v0.19.0 h1:t9uxaZzGvqc2jY96mjnPSjFHtaKOxoUegeGZdaGT6aw= 541 | k8s.io/kubectl v0.19.0/go.mod h1:gPCjjsmE6unJzgaUNXIFGZGafiUp5jh0If3F/x7/rRg= 542 | k8s.io/metrics v0.19.0/go.mod h1:WykpW8B60OeAJx1imdwUgyOID2kDljr/Q+1zrPJ98Wo= 543 | k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= 544 | k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 545 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 546 | sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= 547 | sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= 548 | sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= 549 | sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 550 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 551 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 552 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 553 | vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= 554 | -------------------------------------------------------------------------------- /pkg/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/spf13/cobra" 14 | apierrors "k8s.io/apimachinery/pkg/api/errors" 15 | apimeta "k8s.io/apimachinery/pkg/api/meta" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/cli-runtime/pkg/genericclioptions" 19 | "k8s.io/cli-runtime/pkg/printers" 20 | cliresource "k8s.io/cli-runtime/pkg/resource" 21 | "k8s.io/client-go/dynamic" 22 | "k8s.io/client-go/kubernetes/scheme" 23 | _ "k8s.io/client-go/plugin/pkg/client/auth" 24 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 25 | cmdwait "k8s.io/kubectl/pkg/cmd/wait" 26 | 27 | "github.com/micnncim/kubectl-reap/pkg/determiner" 28 | "github.com/micnncim/kubectl-reap/pkg/prompt" 29 | "github.com/micnncim/kubectl-reap/pkg/resource" 30 | "github.com/micnncim/kubectl-reap/pkg/version" 31 | ) 32 | 33 | const ( 34 | reapShortDescription = ` 35 | Delete unused resources. Supported resources: 36 | 37 | - Pods (whose status is not Running) 38 | - ConfigMaps (not referenced by any Pods or ReplicaSets) 39 | - Secrets (not referenced by any Pods, ReplicaSets, or ServiceAccounts) 40 | - PersistentVolumes (not satisfying any PersistentVolumeClaims) 41 | - PersistentVolumeClaims (not referenced by any Pods) 42 | - Jobs (completed) 43 | - PodDisruptionBudgets (not targeting any Pods) 44 | - HorizontalPodAutoscalers (not targeting any resources) 45 | ` 46 | 47 | reapExample = ` 48 | # Delete ConfigMaps not mounted on any Pods and in the current namespace and context 49 | $ kubectl reap configmaps 50 | 51 | # Delete unused ConfigMaps and Secrets in the namespace/my-namespace and context/my-context 52 | $ kubectl reap cm,secret -n my-namespace --context my-context 53 | 54 | # Delete ConfigMaps not mounted on any Pods and across all namespace 55 | $ kubectl reap cm --all-namespaces 56 | 57 | # Delete Pods whose status is not Running as client-side dry-run 58 | $ kubectl reap po --dry-run=client` 59 | 60 | // printedOperationTypeDeleted is used when printer outputs the result of operations. 61 | printedOperationTypeDeleted = "deleted" 62 | ) 63 | 64 | var timeWeek = 168 * time.Hour 65 | 66 | type runner struct { 67 | configFlags *genericclioptions.ConfigFlags 68 | printFlags *genericclioptions.PrintFlags 69 | 70 | namespace string 71 | allNamespaces bool 72 | chunkSize int64 73 | labelSelector string 74 | fieldSelector string 75 | gracePeriod int 76 | forceDeletion bool 77 | needWaitDeletion bool 78 | timeout time.Duration 79 | 80 | quiet bool 81 | interactive bool 82 | 83 | showVersion bool 84 | 85 | dryRunStrategy cmdutil.DryRunStrategy 86 | dryRunVerifier *cliresource.DryRunVerifier 87 | 88 | deleteOpts *metav1.DeleteOptions 89 | 90 | determiner determiner.Determiner 91 | dynamicClient dynamic.Interface 92 | printer printers.ResourcePrinter 93 | result *cliresource.Result 94 | 95 | genericclioptions.IOStreams 96 | } 97 | 98 | func newRunner(ioStreams genericclioptions.IOStreams) *runner { 99 | return &runner{ 100 | configFlags: genericclioptions.NewConfigFlags(true), 101 | printFlags: genericclioptions.NewPrintFlags(printedOperationTypeDeleted).WithTypeSetter(scheme.Scheme), 102 | chunkSize: 500, 103 | IOStreams: ioStreams, 104 | } 105 | } 106 | 107 | func NewCmdReap(streams genericclioptions.IOStreams) *cobra.Command { 108 | r := newRunner(streams) 109 | 110 | cmd := &cobra.Command{ 111 | Use: "kubectl reap RESOURCE_TYPE", 112 | Short: reapShortDescription, 113 | Example: reapExample, 114 | Run: func(cmd *cobra.Command, args []string) { 115 | if r.showVersion { 116 | r.Infof("%s (%s)\n", version.Version, version.Revision) 117 | return 118 | } 119 | 120 | ctx, cancel := context.WithCancel(context.Background()) 121 | defer cancel() 122 | 123 | ch := make(chan os.Signal, 1) 124 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM) 125 | go func() { 126 | <-ch 127 | r.Infof("Canceling execution...\n") 128 | cancel() 129 | }() 130 | 131 | f := cmdutil.NewFactory(r.configFlags) 132 | 133 | cmdutil.CheckErr(r.Validate(args)) 134 | cmdutil.CheckErr(r.Complete(f, args, cmd)) 135 | cmdutil.CheckErr(r.Run(ctx, f)) 136 | }, 137 | } 138 | 139 | r.configFlags.AddFlags(cmd.Flags()) 140 | r.printFlags.AddFlags(cmd) 141 | 142 | cmdutil.AddDryRunFlag(cmd) 143 | 144 | cmd.Flags().BoolVarP(&r.allNamespaces, "all-namespaces", "A", false, "If true, delete the targeted resources across all namespace except kube-system") 145 | cmd.Flags().StringVarP(&r.labelSelector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") 146 | cmd.Flags().StringVar(&r.fieldSelector, "field-selector", "", "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") 147 | cmd.Flags().IntVar(&r.gracePeriod, "grace-period", -1, "Period of time in seconds given to the resource to terminate gracefully. Ignored if negative. Set to 1 for immediate shutdown. Can only be set to 0 when --force is true (force deletion).") 148 | cmd.Flags().BoolVar(&r.forceDeletion, "force", false, "If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation.") 149 | cmd.Flags().BoolVar(&r.needWaitDeletion, "wait", false, "If true, wait for resources to be gone before returning. This waits for finalizers.") 150 | cmd.Flags().DurationVar(&r.timeout, "timeout", 0, "The length of time to wait before giving up on a delete, zero means determine a timeout from the size of the object") 151 | cmd.Flags().BoolVarP(&r.quiet, "quiet", "q", false, "If true, no output is produced") 152 | cmd.Flags().BoolVarP(&r.interactive, "interactive", "i", false, "If true, a prompt asks whether resources can be deleted") 153 | cmd.Flags().BoolVar(&r.showVersion, "version", false, "If true, show the version of this plugin") 154 | 155 | return cmd 156 | } 157 | 158 | func (r *runner) Complete(f cmdutil.Factory, args []string, cmd *cobra.Command) (err error) { 159 | if !r.forceDeletion && r.gracePeriod == 0 { 160 | // To preserve backwards compatibility, but prevent accidental data loss, we convert --grace-period=0 161 | // into --grace-period=1. Users may provide --force to bypass this conversion. 162 | r.gracePeriod = 1 163 | } 164 | if r.forceDeletion && r.gracePeriod < 0 { 165 | r.gracePeriod = 0 166 | } 167 | r.deleteOpts = &metav1.DeleteOptions{} 168 | if r.gracePeriod >= 0 { 169 | r.deleteOpts = metav1.NewDeleteOptions(int64(r.gracePeriod)) 170 | } 171 | 172 | r.namespace, _, err = r.configFlags.ToRawKubeConfigLoader().Namespace() 173 | if err != nil { 174 | return 175 | } 176 | 177 | r.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) 178 | if err != nil { 179 | return 180 | } 181 | 182 | if err = r.completePrinter(); err != nil { 183 | return 184 | } 185 | 186 | if err = r.completeResources(f, args[0]); err != nil { 187 | return 188 | } 189 | 190 | clientset, err := f.KubernetesClientSet() 191 | if err != nil { 192 | return 193 | } 194 | r.dynamicClient, err = f.DynamicClient() 195 | if err != nil { 196 | return 197 | } 198 | resourceClient := resource.NewClient(clientset, r.dynamicClient) 199 | 200 | discoveryClient, err := f.ToDiscoveryClient() 201 | if err != nil { 202 | return err 203 | } 204 | r.dryRunVerifier = cliresource.NewDryRunVerifier(r.dynamicClient, discoveryClient) 205 | 206 | namespace := r.namespace 207 | if r.allNamespaces { 208 | namespace = metav1.NamespaceAll 209 | } 210 | 211 | r.determiner, err = determiner.New(resourceClient, r.result, namespace) 212 | if err != nil { 213 | return 214 | } 215 | 216 | return 217 | } 218 | 219 | func (r *runner) completePrinter() (err error) { 220 | r.printFlags = cmdutil.PrintFlagsWithDryRunStrategy(r.printFlags, r.dryRunStrategy) 221 | 222 | r.printer, err = r.printFlags.ToPrinter() 223 | if err != nil { 224 | return err 225 | } 226 | 227 | return nil 228 | } 229 | 230 | func (r *runner) completeResources(f cmdutil.Factory, resourceTypes string) error { 231 | r.result = f. 232 | NewBuilder(). 233 | Unstructured(). 234 | ContinueOnError(). 235 | NamespaceParam(r.namespace). 236 | DefaultNamespace(). 237 | AllNamespaces(r.allNamespaces). 238 | LabelSelectorParam(r.labelSelector). 239 | FieldSelectorParam(r.fieldSelector). 240 | SelectAllParam(r.labelSelector == "" && r.fieldSelector == ""). 241 | ResourceTypeOrNameArgs(false, resourceTypes). 242 | RequestChunksOf(r.chunkSize). 243 | Flatten(). 244 | Do() 245 | 246 | return r.result.Err() 247 | } 248 | 249 | func (r *runner) Validate(args []string) error { 250 | if len(args) != 1 && !r.showVersion { 251 | return errors.New("arguments must be only resource type(s)") 252 | } 253 | 254 | switch { 255 | case r.forceDeletion && r.gracePeriod == 0: 256 | r.Errorf("warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.\n") 257 | case r.forceDeletion && r.gracePeriod > 0: 258 | return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together") 259 | } 260 | 261 | return nil 262 | } 263 | 264 | func (r *runner) Run(ctx context.Context, f cmdutil.Factory) error { 265 | deletedInfos := []*cliresource.Info{} 266 | uidMap := cmdwait.UIDMap{} 267 | 268 | if err := r.result.Visit(func(info *cliresource.Info, err error) error { 269 | if info.Namespace == metav1.NamespaceSystem { 270 | return nil // ignore resources in kube-system namespace 271 | } 272 | 273 | ok, err := r.determiner.DetermineDeletion(ctx, info) 274 | if err != nil { 275 | return err 276 | } 277 | if !ok { 278 | return nil // skip deletion 279 | } 280 | 281 | if r.interactive { 282 | kind := info.Object.GetObjectKind().GroupVersionKind().Kind 283 | if ok := prompt.Confirm(fmt.Sprintf("Are you sure to delete %s/%s?", strings.ToLower(kind), info.Name)); !ok { 284 | return nil // skip deletion 285 | } 286 | } 287 | 288 | deletedInfos = append(deletedInfos, info) 289 | 290 | if r.dryRunStrategy == cmdutil.DryRunClient && !r.quiet { 291 | r.printObj(info.Object) 292 | return nil // skip deletion 293 | } 294 | if r.dryRunStrategy == cmdutil.DryRunServer { 295 | if err := r.dryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil { 296 | return err 297 | } 298 | } 299 | 300 | resp, err := cliresource. 301 | NewHelper(info.Client, info.Mapping). 302 | DryRun(r.dryRunStrategy == cmdutil.DryRunServer). 303 | DeleteWithOptions(info.Namespace, info.Name, r.deleteOpts) 304 | if err != nil { 305 | return err 306 | } 307 | 308 | if !r.quiet { 309 | r.printObj(info.Object) 310 | } 311 | 312 | loc := cmdwait.ResourceLocation{ 313 | GroupResource: info.Mapping.Resource.GroupResource(), 314 | Namespace: info.Namespace, 315 | Name: info.Name, 316 | } 317 | if status, ok := resp.(*metav1.Status); ok && status.Details != nil { 318 | uidMap[loc] = status.Details.UID 319 | return nil 320 | } 321 | 322 | accessor, err := apimeta.Accessor(resp) 323 | if err != nil { 324 | // we don't have UID, but we didn't fail the delete, next best thing is just skipping the UID 325 | r.Infof("%v\n", err) 326 | return nil 327 | } 328 | uidMap[loc] = accessor.GetUID() 329 | 330 | return nil 331 | }); err != nil { 332 | return err 333 | } 334 | 335 | if !r.needWaitDeletion { 336 | return nil 337 | } 338 | 339 | r.waitDeletion(uidMap, deletedInfos) 340 | 341 | return nil 342 | } 343 | 344 | func (r *runner) waitDeletion(uidMap cmdwait.UIDMap, deletedInfos []*cliresource.Info) { 345 | timeout := r.timeout 346 | if timeout == 0 { 347 | timeout = timeWeek 348 | } 349 | 350 | waitOpts := cmdwait.WaitOptions{ 351 | ResourceFinder: genericclioptions.ResourceFinderForResult(cliresource.InfoListVisitor(deletedInfos)), 352 | UIDMap: uidMap, 353 | DynamicClient: r.dynamicClient, 354 | Timeout: timeout, 355 | Printer: printers.NewDiscardingPrinter(), 356 | ConditionFn: cmdwait.IsDeleted, 357 | IOStreams: r.IOStreams, 358 | } 359 | err := waitOpts.RunWait() 360 | if apierrors.IsForbidden(err) || apierrors.IsMethodNotSupported(err) { 361 | // if we're forbidden from waiting, we shouldn't fail. 362 | // if the resource doesn't support a verb we need, we shouldn't fail. 363 | r.Errorf("%v\n", err) 364 | } 365 | } 366 | 367 | func (r *runner) Infof(format string, a ...interface{}) { 368 | fmt.Fprintf(r.Out, format, a...) 369 | } 370 | 371 | func (r *runner) Errorf(format string, a ...interface{}) { 372 | fmt.Fprintf(r.ErrOut, format, a...) 373 | } 374 | 375 | func (r *runner) printObj(obj runtime.Object) error { 376 | return r.printer.PrintObj(obj, r.Out) 377 | } 378 | -------------------------------------------------------------------------------- /pkg/cmd/cmd_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/cli-runtime/pkg/genericclioptions" 14 | cliresource "k8s.io/cli-runtime/pkg/resource" 15 | "k8s.io/client-go/rest/fake" 16 | cmdtesting "k8s.io/kubectl/pkg/cmd/testing" 17 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 18 | "k8s.io/kubectl/pkg/scheme" 19 | 20 | "github.com/micnncim/kubectl-reap/pkg/determiner" 21 | ) 22 | 23 | func Test_runner_Run(t *testing.T) { 24 | const ( 25 | fakeNamespace = "fake-ns" 26 | fakeAPIVersion = "v1" 27 | fakeKind = "Pod" 28 | fakeResourceType = "pod" 29 | fakeResourceTypePlural = "pods" 30 | fakeObjectToBeDeleted1Name = "fake-obj-to-be-deleted-1" 31 | fakeObjectToBeDeleted2Name = "fake-obj-to-be-deleted-2" 32 | fakeObjectNotToBeDeletedName = "fake-obj-not-to-be-deleted" 33 | ) 34 | 35 | fakeObjectBase := &corev1.Pod{ 36 | TypeMeta: metav1.TypeMeta{ 37 | Kind: fakeKind, 38 | APIVersion: fakeAPIVersion, 39 | }, 40 | ObjectMeta: metav1.ObjectMeta{ 41 | Namespace: fakeNamespace, 42 | }, 43 | } 44 | 45 | fakeObjectToBeDeleted1 := fakeObjectBase.DeepCopy() 46 | fakeObjectToBeDeleted1.Name = fakeObjectToBeDeleted1Name 47 | 48 | fakeObjectToBeDeleted2 := fakeObjectBase.DeepCopy() 49 | fakeObjectToBeDeleted2.Name = fakeObjectToBeDeleted2Name 50 | 51 | fakeObjectNotToBeDeleted := fakeObjectBase.DeepCopy() 52 | fakeObjectNotToBeDeleted.Name = fakeObjectNotToBeDeletedName 53 | 54 | fakeObjectList := &corev1.PodList{ 55 | Items: []corev1.Pod{ 56 | *fakeObjectToBeDeleted1, 57 | *fakeObjectToBeDeleted2, 58 | *fakeObjectNotToBeDeleted, 59 | }, 60 | } 61 | fakeObjectMap := map[string]*corev1.Pod{ 62 | fakeObjectToBeDeleted1Name: fakeObjectToBeDeleted1, 63 | fakeObjectToBeDeleted2Name: fakeObjectToBeDeleted2, 64 | fakeObjectNotToBeDeletedName: fakeObjectNotToBeDeleted, 65 | } 66 | 67 | testFactory := cmdtesting.NewTestFactory().WithNamespace(fakeNamespace) 68 | defer testFactory.Cleanup() 69 | 70 | codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) 71 | 72 | testFactory.UnstructuredClient = &fake.RESTClient{ 73 | NegotiatedSerializer: cliresource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, 74 | Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 75 | switch p, m := req.URL.Path, req.Method; { 76 | case p == fmt.Sprintf("/namespaces/%s/%s", fakeNamespace, fakeResourceTypePlural) && m == http.MethodGet: 77 | return &http.Response{ 78 | StatusCode: http.StatusOK, 79 | Header: cmdtesting.DefaultHeader(), 80 | Body: cmdtesting.ObjBody(codec, fakeObjectList), 81 | }, nil 82 | 83 | case strings.HasPrefix(p, fmt.Sprintf("/namespaces/%s/%s/", fakeNamespace, fakeResourceTypePlural)) && m == http.MethodDelete: 84 | s := strings.Split(p, "/") 85 | objName := s[len(s)-1] 86 | obj, ok := fakeObjectMap[objName] 87 | if !ok { 88 | t.Errorf("unexpected object: %s", objName) 89 | return nil, nil 90 | } 91 | 92 | return &http.Response{ 93 | StatusCode: http.StatusOK, 94 | Header: cmdtesting.DefaultHeader(), 95 | Body: cmdtesting.ObjBody(codec, obj), 96 | }, nil 97 | 98 | default: 99 | t.Errorf("unexpected request: %#v\n%#v", req.URL, req) 100 | } 101 | 102 | return nil, nil 103 | }), 104 | } 105 | 106 | fakeDeterminer, err := determiner.NewFakeDeterminer( 107 | fakeObjectToBeDeleted1, 108 | fakeObjectToBeDeleted2, 109 | ) 110 | if err != nil { 111 | t.Fatalf("failed to construct fake determiner") 112 | } 113 | 114 | type fields struct { 115 | dryRunStrategy cmdutil.DryRunStrategy 116 | } 117 | 118 | tests := []struct { 119 | name string 120 | fields fields 121 | wantOut string 122 | wantErr bool 123 | }{ 124 | { 125 | name: "delete resources that should be deleted", 126 | fields: fields{}, 127 | wantOut: makeOperationMessage( 128 | fakeResourceType, 129 | []string{ 130 | fakeObjectToBeDeleted1Name, 131 | fakeObjectToBeDeleted2Name, 132 | }, 133 | printedOperationTypeDeleted, 134 | cmdutil.DryRunNone, 135 | ), 136 | wantErr: false, 137 | }, 138 | { 139 | name: "does not delete resources that should be deleted when dry-run is set as client", 140 | fields: fields{ 141 | dryRunStrategy: cmdutil.DryRunClient, 142 | }, 143 | wantOut: makeOperationMessage( 144 | fakeResourceType, 145 | []string{ 146 | fakeObjectToBeDeleted1Name, 147 | fakeObjectToBeDeleted2Name, 148 | }, 149 | printedOperationTypeDeleted, 150 | cmdutil.DryRunClient, 151 | ), 152 | wantErr: false, 153 | }, 154 | } 155 | 156 | for _, tt := range tests { 157 | tt := tt 158 | 159 | t.Run(tt.name, func(t *testing.T) { 160 | streams, _, out, _ := genericclioptions.NewTestIOStreams() 161 | 162 | r := &runner{ 163 | printFlags: genericclioptions.NewPrintFlags(printedOperationTypeDeleted).WithTypeSetter(scheme.Scheme), 164 | namespace: fakeNamespace, 165 | chunkSize: 10, 166 | determiner: fakeDeterminer, 167 | dryRunStrategy: tt.fields.dryRunStrategy, 168 | IOStreams: streams, 169 | } 170 | 171 | if err := r.completePrinter(); err != nil { 172 | t.Errorf("failed to complete printer: %v\n", err) 173 | return 174 | } 175 | 176 | if err := r.completeResources(testFactory, fakeResourceTypePlural); err != nil { 177 | t.Errorf("failed to complete resources: %v\n", err) 178 | return 179 | } 180 | 181 | if err := r.Run(context.Background(), testFactory); (err != nil) != tt.wantErr { 182 | t.Errorf("runner.Run() error = %v, wantErr %v", err, tt.wantErr) 183 | return 184 | } 185 | 186 | if diff := cmp.Diff(tt.wantOut, out.String()); diff != "" { 187 | t.Errorf("(-want +got):\n%s", diff) 188 | return 189 | } 190 | }) 191 | } 192 | } 193 | 194 | func makeOperationMessage(resourceType string, objectNames []string, operation string, dryRunStrategy cmdutil.DryRunStrategy) string { 195 | b := strings.Builder{} 196 | 197 | for _, name := range objectNames { 198 | msg := fmt.Sprintf("%s/%s %s", resourceType, name, operation) 199 | switch dryRunStrategy { 200 | case cmdutil.DryRunClient: 201 | msg += " (dry run)" 202 | case cmdutil.DryRunServer: 203 | msg += " (server dry run)" 204 | } 205 | msg += "\n" 206 | 207 | b.WriteString(msg) 208 | } 209 | 210 | return b.String() 211 | } 212 | -------------------------------------------------------------------------------- /pkg/determiner/determiner.go: -------------------------------------------------------------------------------- 1 | package determiner 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | policyv1beta1 "k8s.io/api/policy/v1beta1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/labels" 12 | cliresource "k8s.io/cli-runtime/pkg/resource" 13 | 14 | "github.com/micnncim/kubectl-reap/pkg/resource" 15 | ) 16 | 17 | var checkVolumeSatisfyClaimFunc = resource.CheckVolumeSatisfyClaim 18 | 19 | type Determiner interface { 20 | DetermineDeletion(ctx context.Context, info *cliresource.Info) (bool, error) 21 | } 22 | 23 | // determiner determines whether a resource should be deleted. 24 | type determiner struct { 25 | resourceClient resource.Client 26 | 27 | usedConfigMaps map[string]struct{} // key=ConfigMap.Name 28 | usedSecrets map[string]struct{} // key=Secret.Name 29 | usedPersistentVolumeClaims map[string]struct{} // key=PersistentVolumeClaim.Name 30 | 31 | pods []*corev1.Pod 32 | replicaSets []*appsv1.ReplicaSet 33 | persistentVolumeClaims []*corev1.PersistentVolumeClaim 34 | } 35 | 36 | // Guarantee *determiner implements Determiner. 37 | var _ Determiner = (*determiner)(nil) 38 | 39 | func New(resourceClient resource.Client, r *cliresource.Result, namespace string) (Determiner, error) { 40 | d := &determiner{ 41 | resourceClient: resourceClient, 42 | } 43 | 44 | var ( 45 | reapConfigMaps bool 46 | reapSecrets bool 47 | reapPersistentVolumes bool 48 | reapPersistentVolumeClaims bool 49 | reapPodDisruptionBudgets bool 50 | ) 51 | 52 | if err := r.Visit(func(info *cliresource.Info, err error) error { 53 | switch info.Object.GetObjectKind().GroupVersionKind().Kind { 54 | case resource.KindConfigMap: 55 | reapConfigMaps = true 56 | case resource.KindSecret: 57 | reapSecrets = true 58 | case resource.KindPersistentVolume: 59 | reapPersistentVolumes = true 60 | case resource.KindPersistentVolumeClaim: 61 | reapPersistentVolumeClaims = true 62 | case resource.KindPodDisruptionBudget: 63 | reapPodDisruptionBudgets = true 64 | } 65 | return nil 66 | }); err != nil { 67 | return nil, err 68 | } 69 | 70 | ctx := context.Background() 71 | 72 | if reapConfigMaps || reapSecrets || reapPersistentVolumeClaims || reapPodDisruptionBudgets { 73 | var err error 74 | d.pods, err = d.resourceClient.ListPods(ctx, namespace) 75 | if err != nil { 76 | return nil, err 77 | } 78 | } 79 | 80 | if reapConfigMaps || reapSecrets { 81 | var err error 82 | d.replicaSets, err = d.resourceClient.ListReplicaSets(ctx, namespace) 83 | if err != nil { 84 | return nil, err 85 | } 86 | } 87 | 88 | if reapPersistentVolumes { 89 | var err error 90 | d.persistentVolumeClaims, err = d.resourceClient.ListPersistentVolumeClaims(ctx, namespace) 91 | if err != nil { 92 | return nil, err 93 | } 94 | } 95 | 96 | if reapConfigMaps { 97 | d.usedConfigMaps = d.detectUsedConfigMaps() 98 | } 99 | 100 | if reapSecrets { 101 | sas, err := d.resourceClient.ListServiceAccounts(ctx, namespace) 102 | if err != nil { 103 | return nil, err 104 | } 105 | d.usedSecrets = d.detectUsedSecrets(sas) 106 | } 107 | 108 | if reapPersistentVolumeClaims { 109 | d.usedPersistentVolumeClaims = d.detectUsedPersistentVolumeClaims() 110 | } 111 | 112 | return d, nil 113 | } 114 | 115 | // DetermineDeletion determines whether a resource should be deleted. 116 | func (d *determiner) DetermineDeletion(ctx context.Context, info *cliresource.Info) (bool, error) { 117 | switch kind := info.Object.GetObjectKind().GroupVersionKind().Kind; kind { 118 | case resource.KindPod: 119 | return d.determineDeletionPod(info) 120 | 121 | case resource.KindConfigMap: 122 | return d.determineDeletionConfigMap(info) 123 | 124 | case resource.KindSecret: 125 | return d.determineDeletionSecret(info) 126 | 127 | case resource.KindPersistentVolume: 128 | return d.determineDeletionPersistentVolume(info) 129 | 130 | case resource.KindPersistentVolumeClaim: 131 | return d.determineDeletionPersistentVolumeClaim(info) 132 | 133 | case resource.KindJob: 134 | return d.determineDeletionJob(info) 135 | 136 | case resource.KindPodDisruptionBudget: 137 | return d.determineDeletionPodDisruptionBudget(info) 138 | 139 | case resource.KindHorizontalPodAutoscaler: 140 | return d.determineDeletionHorizontalPodAutoscaler(ctx, info) 141 | 142 | default: 143 | return false, fmt.Errorf("unsupported kind: %s/%s", kind, info.Name) 144 | } 145 | } 146 | 147 | func (d *determiner) determineDeletionPod(info *cliresource.Info) (bool, error) { 148 | pod, err := resource.ObjectToPod(info.Object) 149 | if err != nil { 150 | return false, err 151 | } 152 | 153 | return pod.Status.Phase != corev1.PodRunning, nil 154 | } 155 | 156 | func (d *determiner) determineDeletionConfigMap(info *cliresource.Info) (bool, error) { 157 | _, ok := d.usedConfigMaps[info.Name] 158 | return !ok, nil 159 | } 160 | 161 | func (d *determiner) determineDeletionSecret(info *cliresource.Info) (bool, error) { 162 | _, ok := d.usedSecrets[info.Name] 163 | return !ok, nil 164 | } 165 | 166 | func (d *determiner) determineDeletionPersistentVolume(info *cliresource.Info) (bool, error) { 167 | volume, err := resource.ObjectToPersistentVolume(info.Object) 168 | if err != nil { 169 | return false, err 170 | } 171 | 172 | for _, claim := range d.persistentVolumeClaims { 173 | if ok := checkVolumeSatisfyClaimFunc(volume, claim); ok { 174 | return false, nil 175 | } 176 | } 177 | return true, nil // should delete PV if it doesn't satisfy any PVCs 178 | } 179 | 180 | func (d *determiner) determineDeletionPersistentVolumeClaim(info *cliresource.Info) (bool, error) { 181 | _, ok := d.usedPersistentVolumeClaims[info.Name] 182 | return !ok, nil 183 | } 184 | 185 | func (d *determiner) determineDeletionJob(info *cliresource.Info) (bool, error) { 186 | job, err := resource.ObjectToJob(info.Object) 187 | if err != nil { 188 | return false, err 189 | } 190 | 191 | return job.Status.CompletionTime != nil, nil 192 | } 193 | 194 | func (d *determiner) determineDeletionPodDisruptionBudget(info *cliresource.Info) (bool, error) { 195 | pdb, err := resource.ObjectToPodDisruptionBudget(info.Object) 196 | if err != nil { 197 | return false, err 198 | } 199 | 200 | used, err := d.determineUsedPodDisruptionBudget(pdb) 201 | if err != nil { 202 | return false, err 203 | } 204 | return !used, nil 205 | } 206 | 207 | func (d *determiner) determineDeletionHorizontalPodAutoscaler(ctx context.Context, info *cliresource.Info) (bool, error) { 208 | hpa, err := resource.ObjectToHorizontalPodAutoscaler(info.Object) 209 | if err != nil { 210 | return false, err 211 | } 212 | 213 | ref := hpa.Spec.ScaleTargetRef 214 | u, err := d.resourceClient.GetUnstructured(ctx, ref.APIVersion, ref.Kind, ref.Name, info.Namespace) 215 | if err != nil { 216 | return false, err 217 | } 218 | return u == nil, nil // should delete HPA if ScaleTargetRef's target object is not found 219 | } 220 | 221 | func (d *determiner) detectUsedConfigMaps() map[string]struct{} { 222 | usedConfigMaps := make(map[string]struct{}) 223 | 224 | // Add Secrets used by Pods 225 | for _, pod := range d.pods { 226 | for _, container := range pod.Spec.Containers { 227 | for _, envFrom := range container.EnvFrom { 228 | if envFrom.ConfigMapRef != nil { 229 | usedConfigMaps[envFrom.ConfigMapRef.Name] = struct{}{} 230 | } 231 | } 232 | 233 | for _, env := range container.Env { 234 | if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil { 235 | usedConfigMaps[env.ValueFrom.ConfigMapKeyRef.Name] = struct{}{} 236 | } 237 | } 238 | } 239 | 240 | for _, volume := range pod.Spec.Volumes { 241 | if volume.ConfigMap != nil { 242 | usedConfigMaps[volume.ConfigMap.Name] = struct{}{} 243 | } 244 | 245 | if volume.Projected != nil { 246 | for _, source := range volume.Projected.Sources { 247 | if source.ConfigMap != nil { 248 | usedConfigMaps[source.ConfigMap.Name] = struct{}{} 249 | } 250 | } 251 | } 252 | } 253 | } 254 | 255 | // Add Secrets used by ReplicaSets 256 | for _, rs := range d.replicaSets { 257 | for _, container := range rs.Spec.Template.Spec.Containers { 258 | for _, envFrom := range container.EnvFrom { 259 | if envFrom.ConfigMapRef != nil { 260 | usedConfigMaps[envFrom.ConfigMapRef.Name] = struct{}{} 261 | } 262 | } 263 | 264 | for _, env := range container.Env { 265 | if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil { 266 | usedConfigMaps[env.ValueFrom.ConfigMapKeyRef.Name] = struct{}{} 267 | } 268 | } 269 | } 270 | 271 | for _, volume := range rs.Spec.Template.Spec.Volumes { 272 | if volume.ConfigMap != nil { 273 | usedConfigMaps[volume.ConfigMap.Name] = struct{}{} 274 | } 275 | 276 | if volume.Projected != nil { 277 | for _, source := range volume.Projected.Sources { 278 | if source.ConfigMap != nil { 279 | usedConfigMaps[source.ConfigMap.Name] = struct{}{} 280 | } 281 | } 282 | } 283 | } 284 | } 285 | 286 | return usedConfigMaps 287 | } 288 | 289 | func (d *determiner) detectUsedSecrets(sas []*corev1.ServiceAccount) map[string]struct{} { 290 | usedSecrets := make(map[string]struct{}) 291 | 292 | // Add Secrets used by Pods 293 | for _, pod := range d.pods { 294 | for _, imagePullSecret := range pod.Spec.ImagePullSecrets { 295 | usedSecrets[imagePullSecret.Name] = struct{}{} 296 | } 297 | 298 | for _, container := range pod.Spec.Containers { 299 | for _, envFrom := range container.EnvFrom { 300 | if envFrom.SecretRef != nil { 301 | usedSecrets[envFrom.SecretRef.Name] = struct{}{} 302 | } 303 | } 304 | 305 | for _, env := range container.Env { 306 | if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { 307 | usedSecrets[env.ValueFrom.SecretKeyRef.Name] = struct{}{} 308 | } 309 | } 310 | } 311 | 312 | for _, volume := range pod.Spec.Volumes { 313 | if volume.Secret != nil { 314 | usedSecrets[volume.Secret.SecretName] = struct{}{} 315 | } 316 | 317 | if volume.Projected != nil { 318 | for _, source := range volume.Projected.Sources { 319 | if source.Secret != nil { 320 | usedSecrets[source.Secret.Name] = struct{}{} 321 | } 322 | } 323 | } 324 | } 325 | } 326 | 327 | // Add Secrets used by ReplicaSets 328 | for _, rs := range d.replicaSets { 329 | for _, container := range rs.Spec.Template.Spec.Containers { 330 | for _, envFrom := range container.EnvFrom { 331 | if envFrom.SecretRef != nil { 332 | usedSecrets[envFrom.SecretRef.Name] = struct{}{} 333 | } 334 | } 335 | 336 | for _, env := range container.Env { 337 | if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil { 338 | usedSecrets[env.ValueFrom.SecretKeyRef.Name] = struct{}{} 339 | } 340 | } 341 | } 342 | 343 | for _, volume := range rs.Spec.Template.Spec.Volumes { 344 | if volume.Secret != nil { 345 | usedSecrets[volume.Secret.SecretName] = struct{}{} 346 | } 347 | 348 | if volume.Projected != nil { 349 | for _, source := range volume.Projected.Sources { 350 | if source.Secret != nil { 351 | usedSecrets[source.Secret.Name] = struct{}{} 352 | } 353 | } 354 | } 355 | } 356 | } 357 | 358 | // Add Secrets used by ServiceAccounts 359 | for _, sa := range sas { 360 | for _, secret := range sa.Secrets { 361 | usedSecrets[secret.Name] = struct{}{} 362 | } 363 | } 364 | 365 | return usedSecrets 366 | } 367 | 368 | func (d *determiner) detectUsedPersistentVolumeClaims() map[string]struct{} { 369 | usedPersistentVolumeClaims := make(map[string]struct{}) 370 | 371 | for _, pod := range d.pods { 372 | for _, volume := range pod.Spec.Volumes { 373 | if volume.PersistentVolumeClaim == nil { 374 | continue 375 | } 376 | usedPersistentVolumeClaims[volume.PersistentVolumeClaim.ClaimName] = struct{}{} 377 | } 378 | } 379 | 380 | return usedPersistentVolumeClaims 381 | } 382 | 383 | func (d *determiner) determineUsedPodDisruptionBudget(pdb *policyv1beta1.PodDisruptionBudget) (bool, error) { 384 | selector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector) 385 | if err != nil { 386 | return false, fmt.Errorf("invalid label selector (%s): %w", pdb.Name, err) 387 | } 388 | 389 | for _, pod := range d.pods { 390 | if selector.Matches(labels.Set(pod.Labels)) { 391 | return true, nil 392 | } 393 | } 394 | 395 | return false, nil 396 | } 397 | -------------------------------------------------------------------------------- /pkg/determiner/determiner_test.go: -------------------------------------------------------------------------------- 1 | package determiner 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | appsv1 "k8s.io/api/apps/v1" 10 | autoscalingv1 "k8s.io/api/autoscaling/v1" 11 | batchv1 "k8s.io/api/batch/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | policyv1beta1 "k8s.io/api/policy/v1beta1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | cliresource "k8s.io/cli-runtime/pkg/resource" 17 | 18 | "github.com/micnncim/kubectl-reap/pkg/resource" 19 | ) 20 | 21 | func Test_determiner_DetermineDeletion(t *testing.T) { 22 | const ( 23 | fakePod = "fake-pod" 24 | fakeConfigMap = "fake-cm" 25 | fakeSecret = "fake-secret" 26 | fakePersistentVolumeClaim = "fake-pvc" 27 | fakeJob = "fake-job" 28 | fakePodDisruptionBudget = "fake-pdb" 29 | fakeLabelKey1 = "fake-label1-key" 30 | fakeLabelValue1 = "fake-label1-value" 31 | fakeLabelKey2 = "fake-label2-key" 32 | fakeLabelValue2 = "fake-label2-value" 33 | ) 34 | 35 | fakeTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) 36 | 37 | type fields struct { 38 | usedConfigMaps map[string]struct{} 39 | usedSecrets map[string]struct{} 40 | usedPersistentVolumes map[string]struct{} 41 | pods []*corev1.Pod 42 | } 43 | type args struct { 44 | info *cliresource.Info 45 | } 46 | 47 | tests := []struct { 48 | name string 49 | fields fields 50 | args args 51 | want bool 52 | wantErr bool 53 | }{ 54 | { 55 | name: "Pod should be deleted when it is not running", 56 | args: args{ 57 | info: &cliresource.Info{ 58 | Name: fakePod, 59 | Object: &corev1.Pod{ 60 | TypeMeta: metav1.TypeMeta{ 61 | Kind: resource.KindPod, 62 | }, 63 | Status: corev1.PodStatus{ 64 | Phase: corev1.PodFailed, 65 | }, 66 | }, 67 | }, 68 | }, 69 | want: true, 70 | wantErr: false, 71 | }, 72 | { 73 | name: "Pod should not be deleted when it is running", 74 | args: args{ 75 | info: &cliresource.Info{ 76 | Name: fakePod, 77 | Object: &corev1.Pod{ 78 | TypeMeta: metav1.TypeMeta{ 79 | Kind: resource.KindPod, 80 | }, 81 | Status: corev1.PodStatus{ 82 | Phase: corev1.PodRunning, 83 | }, 84 | }, 85 | }, 86 | }, 87 | want: false, 88 | wantErr: false, 89 | }, 90 | { 91 | name: "ConfigMap should be deleted when it is not used", 92 | args: args{ 93 | info: &cliresource.Info{ 94 | Name: fakeConfigMap, 95 | Object: &corev1.ConfigMap{ 96 | TypeMeta: metav1.TypeMeta{ 97 | Kind: resource.KindConfigMap, 98 | }, 99 | }, 100 | }, 101 | }, 102 | want: true, 103 | wantErr: false, 104 | }, 105 | { 106 | name: "ConfigMap should not be deleted when it is used", 107 | fields: fields{ 108 | usedConfigMaps: map[string]struct{}{ 109 | fakeConfigMap: {}, 110 | }, 111 | }, 112 | args: args{ 113 | info: &cliresource.Info{ 114 | Name: fakeConfigMap, 115 | Object: &corev1.ConfigMap{ 116 | TypeMeta: metav1.TypeMeta{ 117 | Kind: resource.KindConfigMap, 118 | }, 119 | }, 120 | }, 121 | }, 122 | want: false, 123 | wantErr: false, 124 | }, 125 | { 126 | name: "Secret should be deleted when it is not used", 127 | args: args{ 128 | info: &cliresource.Info{ 129 | Name: fakeSecret, 130 | Object: &corev1.Secret{ 131 | TypeMeta: metav1.TypeMeta{ 132 | Kind: resource.KindSecret, 133 | }, 134 | }, 135 | }, 136 | }, 137 | want: true, 138 | wantErr: false, 139 | }, 140 | { 141 | name: "Secret should not be deleted when it is used", 142 | fields: fields{ 143 | usedSecrets: map[string]struct{}{ 144 | fakeSecret: {}, 145 | }, 146 | }, 147 | args: args{ 148 | info: &cliresource.Info{ 149 | Name: fakeSecret, 150 | Object: &corev1.Secret{ 151 | TypeMeta: metav1.TypeMeta{ 152 | Kind: resource.KindSecret, 153 | }, 154 | }, 155 | }, 156 | }, 157 | want: false, 158 | wantErr: false, 159 | }, 160 | { 161 | name: "PersistentVolumeClaim should be deleted when it is not used", 162 | args: args{ 163 | info: &cliresource.Info{ 164 | Name: fakePersistentVolumeClaim, 165 | Object: &corev1.PersistentVolumeClaim{ 166 | TypeMeta: metav1.TypeMeta{ 167 | Kind: resource.KindPersistentVolumeClaim, 168 | }, 169 | }, 170 | }, 171 | }, 172 | want: true, 173 | wantErr: false, 174 | }, 175 | { 176 | name: "PersistentVolumeClaim should not be deleted when it is used", 177 | fields: fields{ 178 | usedPersistentVolumes: map[string]struct{}{ 179 | fakePersistentVolumeClaim: {}, 180 | }, 181 | }, 182 | args: args{ 183 | info: &cliresource.Info{ 184 | Name: fakePersistentVolumeClaim, 185 | Object: &corev1.PersistentVolumeClaim{ 186 | TypeMeta: metav1.TypeMeta{ 187 | Kind: resource.KindPersistentVolumeClaim, 188 | }, 189 | }, 190 | }, 191 | }, 192 | want: false, 193 | wantErr: false, 194 | }, 195 | { 196 | name: "Job should be deleted when it is completed", 197 | args: args{ 198 | info: &cliresource.Info{ 199 | Name: fakeJob, 200 | Object: &batchv1.Job{ 201 | TypeMeta: metav1.TypeMeta{ 202 | Kind: resource.KindJob, 203 | }, 204 | Status: batchv1.JobStatus{ 205 | CompletionTime: &metav1.Time{ 206 | Time: fakeTime, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }, 212 | want: true, 213 | wantErr: false, 214 | }, 215 | { 216 | name: "Job should not be deleted when it is not completed", 217 | args: args{ 218 | info: &cliresource.Info{ 219 | Name: fakeJob, 220 | Object: &batchv1.Job{ 221 | TypeMeta: metav1.TypeMeta{ 222 | Kind: resource.KindJob, 223 | }, 224 | Status: batchv1.JobStatus{}, 225 | }, 226 | }, 227 | }, 228 | want: false, 229 | wantErr: false, 230 | }, 231 | { 232 | name: "PodDisruptionBudget should be deleted when it is not used", 233 | args: args{ 234 | info: &cliresource.Info{ 235 | Name: fakePodDisruptionBudget, 236 | Object: &policyv1beta1.PodDisruptionBudget{ 237 | TypeMeta: metav1.TypeMeta{ 238 | Kind: resource.KindPodDisruptionBudget, 239 | }, 240 | Spec: policyv1beta1.PodDisruptionBudgetSpec{ 241 | Selector: &metav1.LabelSelector{ 242 | MatchLabels: map[string]string{ 243 | fakeLabelKey1: fakeLabelValue1, 244 | }, 245 | }, 246 | }, 247 | }, 248 | }, 249 | }, 250 | want: true, 251 | wantErr: false, 252 | }, 253 | { 254 | name: "PodDisruptionBudget should not be deleted when it is used", 255 | fields: fields{ 256 | pods: []*corev1.Pod{ 257 | { 258 | ObjectMeta: metav1.ObjectMeta{ 259 | Labels: map[string]string{ 260 | fakeLabelKey1: fakeLabelValue1, 261 | }, 262 | }, 263 | }, 264 | }, 265 | }, 266 | args: args{ 267 | info: &cliresource.Info{ 268 | Name: fakePodDisruptionBudget, 269 | Object: &policyv1beta1.PodDisruptionBudget{ 270 | TypeMeta: metav1.TypeMeta{ 271 | Kind: resource.KindPodDisruptionBudget, 272 | }, 273 | Spec: policyv1beta1.PodDisruptionBudgetSpec{ 274 | Selector: &metav1.LabelSelector{ 275 | MatchLabels: map[string]string{ 276 | fakeLabelKey1: fakeLabelValue1, 277 | }, 278 | }, 279 | }, 280 | }, 281 | }, 282 | }, 283 | want: false, 284 | wantErr: false, 285 | }, 286 | } 287 | 288 | for _, tt := range tests { 289 | tt := tt 290 | 291 | t.Run(tt.name, func(t *testing.T) { 292 | t.Parallel() 293 | 294 | d := &determiner{ 295 | usedConfigMaps: tt.fields.usedConfigMaps, 296 | usedSecrets: tt.fields.usedSecrets, 297 | usedPersistentVolumeClaims: tt.fields.usedPersistentVolumes, 298 | pods: tt.fields.pods, 299 | } 300 | 301 | got, err := d.DetermineDeletion(context.Background(), tt.args.info) 302 | if (err != nil) != tt.wantErr { 303 | t.Errorf("determiner.DetermineDeletion() error = %v, wantErr %v", err, tt.wantErr) 304 | return 305 | } 306 | if got != tt.want { 307 | t.Errorf("determiner.DetermineDeletion() = %v, want %v", got, tt.want) 308 | } 309 | }) 310 | } 311 | } 312 | 313 | func Test_determiner_DetermineDeletion_PersistentVolume(t *testing.T) { 314 | const ( 315 | fakePersistentVolume = "fake-pv" 316 | fakePersistentVolumeClaim1 = "fake-pvc1" 317 | fakePersistentVolumeClaim2 = "fake-pvc2" 318 | fakeLabelKey = "fake-label-key" 319 | fakeLabelValue = "fake-label-value" 320 | ) 321 | 322 | var orgCheckVolumeSatisfyClaimFunc func(volume *corev1.PersistentVolume, claim *corev1.PersistentVolumeClaim) bool 323 | orgCheckVolumeSatisfyClaimFunc, checkVolumeSatisfyClaimFunc = 324 | checkVolumeSatisfyClaimFunc, 325 | func(volume *corev1.PersistentVolume, claim *corev1.PersistentVolumeClaim) bool { 326 | return volume.Labels[fakeLabelKey] == claim.Labels[fakeLabelKey] 327 | } 328 | t.Cleanup(func() { 329 | checkVolumeSatisfyClaimFunc = orgCheckVolumeSatisfyClaimFunc 330 | }) 331 | 332 | type fields struct { 333 | persistentVolumeClaims []*corev1.PersistentVolumeClaim 334 | } 335 | type args struct { 336 | info *cliresource.Info 337 | } 338 | 339 | tests := []struct { 340 | name string 341 | fields fields 342 | args args 343 | want bool 344 | wantErr bool 345 | }{ 346 | { 347 | name: "PersistentVolume should be deleted when it is not used", 348 | fields: fields{}, 349 | args: args{ 350 | info: &cliresource.Info{ 351 | Name: fakePersistentVolume, 352 | Object: &corev1.PersistentVolume{ 353 | TypeMeta: metav1.TypeMeta{ 354 | Kind: resource.KindPersistentVolume, 355 | }, 356 | ObjectMeta: metav1.ObjectMeta{ 357 | Labels: map[string]string{ 358 | fakeLabelKey: fakeLabelValue, 359 | }, 360 | }, 361 | }, 362 | }, 363 | }, 364 | want: true, 365 | wantErr: false, 366 | }, 367 | { 368 | name: "PersistentVolume should not be deleted when it is used", 369 | fields: fields{ 370 | persistentVolumeClaims: []*corev1.PersistentVolumeClaim{ 371 | { 372 | ObjectMeta: metav1.ObjectMeta{ 373 | Name: fakePersistentVolumeClaim1, 374 | Labels: map[string]string{ 375 | fakeLabelKey: fakeLabelValue, 376 | }, 377 | }, 378 | }, 379 | { 380 | ObjectMeta: metav1.ObjectMeta{ 381 | Name: fakePersistentVolumeClaim2, 382 | }, 383 | }, 384 | }, 385 | }, 386 | args: args{ 387 | info: &cliresource.Info{ 388 | Name: fakePersistentVolume, 389 | Object: &corev1.PersistentVolume{ 390 | TypeMeta: metav1.TypeMeta{ 391 | Kind: resource.KindPersistentVolume, 392 | }, 393 | ObjectMeta: metav1.ObjectMeta{ 394 | Labels: map[string]string{ 395 | fakeLabelKey: fakeLabelValue, 396 | }, 397 | }, 398 | }, 399 | }, 400 | }, 401 | want: false, 402 | wantErr: false, 403 | }, 404 | } 405 | 406 | for _, tt := range tests { 407 | tt := tt 408 | 409 | t.Run(tt.name, func(t *testing.T) { 410 | t.Parallel() 411 | 412 | d := &determiner{ 413 | persistentVolumeClaims: tt.fields.persistentVolumeClaims, 414 | } 415 | 416 | got, err := d.DetermineDeletion(context.Background(), tt.args.info) 417 | if (err != nil) != tt.wantErr { 418 | t.Errorf("determiner.DetermineDeletion() error = %v, wantErr %v", err, tt.wantErr) 419 | return 420 | } 421 | if got != tt.want { 422 | t.Errorf("determiner.DetermineDeletion() = %v, want %v", got, tt.want) 423 | } 424 | }) 425 | } 426 | } 427 | 428 | func Test_determiner_DetermineDeletion_HorizontalPodAutoscaler(t *testing.T) { 429 | const ( 430 | fakeNamespace = "fake-ns" 431 | fakeHorizontalPodAutoscaler = "fake-hpa" 432 | fakeScaleTargetRefAPIVersion = "apps/v1" 433 | fakeScaleTargetRefKind = "Deployment" 434 | fakeScaleTargetRefName = "fake-deploy" 435 | ) 436 | 437 | type args struct { 438 | info *cliresource.Info 439 | } 440 | 441 | tests := []struct { 442 | name string 443 | args args 444 | fakeObjects []runtime.Object 445 | want bool 446 | wantErr bool 447 | }{ 448 | { 449 | name: "HorizontalPodAutoscaler should be deleted when it is not used", 450 | args: args{ 451 | info: &cliresource.Info{ 452 | Name: fakeHorizontalPodAutoscaler, 453 | Object: &autoscalingv1.HorizontalPodAutoscaler{ 454 | TypeMeta: metav1.TypeMeta{ 455 | Kind: resource.KindHorizontalPodAutoscaler, 456 | }, 457 | Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ 458 | ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ 459 | APIVersion: fakeScaleTargetRefAPIVersion, 460 | Kind: fakeScaleTargetRefKind, 461 | Name: fakeScaleTargetRefName, 462 | }, 463 | }, 464 | }, 465 | }, 466 | }, 467 | fakeObjects: []runtime.Object{}, 468 | want: true, 469 | wantErr: false, 470 | }, 471 | { 472 | name: "HorizontalPodAutoscaler should not be deleted when it is used", 473 | args: args{ 474 | info: &cliresource.Info{ 475 | Name: fakeHorizontalPodAutoscaler, 476 | Namespace: fakeNamespace, 477 | Object: &autoscalingv1.HorizontalPodAutoscaler{ 478 | TypeMeta: metav1.TypeMeta{ 479 | Kind: resource.KindHorizontalPodAutoscaler, 480 | }, 481 | ObjectMeta: metav1.ObjectMeta{ 482 | Name: fakeHorizontalPodAutoscaler, 483 | Namespace: fakeNamespace, 484 | }, 485 | Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ 486 | ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ 487 | APIVersion: fakeScaleTargetRefAPIVersion, 488 | Kind: fakeScaleTargetRefKind, 489 | Name: fakeScaleTargetRefName, 490 | }, 491 | }, 492 | }, 493 | }, 494 | }, 495 | fakeObjects: []runtime.Object{ 496 | &appsv1.Deployment{ 497 | TypeMeta: metav1.TypeMeta{ 498 | APIVersion: fakeScaleTargetRefAPIVersion, 499 | Kind: fakeScaleTargetRefKind, 500 | }, 501 | ObjectMeta: metav1.ObjectMeta{ 502 | Name: fakeScaleTargetRefName, 503 | Namespace: fakeNamespace, 504 | }, 505 | }, 506 | }, 507 | want: false, 508 | wantErr: false, 509 | }, 510 | } 511 | 512 | for _, tt := range tests { 513 | tt := tt 514 | 515 | t.Run(tt.name, func(t *testing.T) { 516 | t.Parallel() 517 | 518 | c, err := resource.NewFakeClient(tt.fakeObjects...) 519 | if err != nil { 520 | t.Errorf("failed to construct fake resource client") 521 | return 522 | } 523 | 524 | d := &determiner{ 525 | resourceClient: c, 526 | } 527 | 528 | got, err := d.DetermineDeletion(context.Background(), tt.args.info) 529 | if (err != nil) != tt.wantErr { 530 | t.Errorf("determiner.DetermineDeletion() error = %v, wantErr %v", err, tt.wantErr) 531 | return 532 | } 533 | if got != tt.want { 534 | t.Errorf("determiner.DetermineDeletion() = %v, want %v", got, tt.want) 535 | } 536 | }) 537 | } 538 | } 539 | 540 | func Test_determiner_determineUsedPodDisruptionBudget(t *testing.T) { 541 | const ( 542 | fakePodDisruptionBudget = "fake-pdb" 543 | fakePod1 = "fake-pod1" 544 | fakePod2 = "fake-pod2" 545 | fakeLabelKey1 = "fake-label1-key" 546 | fakeLabelValue1 = "fake-label1-value" 547 | fakeLabelKey2 = "fake-label2-key" 548 | fakeLabelValue2 = "fake-label2-value" 549 | ) 550 | 551 | type fields struct { 552 | pods []*corev1.Pod 553 | } 554 | type args struct { 555 | pdb *policyv1beta1.PodDisruptionBudget 556 | } 557 | 558 | tests := []struct { 559 | name string 560 | fields fields 561 | args args 562 | want bool 563 | wantErr bool 564 | }{ 565 | { 566 | name: "used PodDisruptionBudget should be determined with MatchLabels", 567 | fields: fields{ 568 | pods: []*corev1.Pod{ 569 | { 570 | ObjectMeta: metav1.ObjectMeta{ 571 | Labels: map[string]string{ 572 | fakeLabelKey1: fakeLabelValue1, 573 | fakeLabelKey2: fakeLabelValue2, 574 | }, 575 | }, 576 | }, 577 | { 578 | ObjectMeta: metav1.ObjectMeta{ 579 | Labels: map[string]string{ 580 | fakeLabelKey2: fakeLabelValue2, 581 | }, 582 | }, 583 | }, 584 | }, 585 | }, 586 | args: args{ 587 | pdb: &policyv1beta1.PodDisruptionBudget{ 588 | ObjectMeta: metav1.ObjectMeta{ 589 | Name: fakePodDisruptionBudget, 590 | }, 591 | Spec: policyv1beta1.PodDisruptionBudgetSpec{ 592 | Selector: &metav1.LabelSelector{ 593 | MatchLabels: map[string]string{ 594 | fakeLabelKey1: fakeLabelValue1, 595 | }, 596 | }, 597 | }, 598 | }, 599 | }, 600 | want: true, 601 | wantErr: false, 602 | }, 603 | { 604 | name: "used PodDisruptionBudget should be determined with MatchExpressions", 605 | fields: fields{ 606 | pods: []*corev1.Pod{ 607 | { 608 | ObjectMeta: metav1.ObjectMeta{ 609 | Labels: map[string]string{ 610 | fakeLabelKey1: fakeLabelValue1, 611 | }, 612 | }, 613 | }, 614 | }, 615 | }, 616 | args: args{ 617 | pdb: &policyv1beta1.PodDisruptionBudget{ 618 | ObjectMeta: metav1.ObjectMeta{ 619 | Name: fakePodDisruptionBudget, 620 | }, 621 | Spec: policyv1beta1.PodDisruptionBudgetSpec{ 622 | Selector: &metav1.LabelSelector{ 623 | MatchExpressions: []metav1.LabelSelectorRequirement{ 624 | { 625 | Key: fakeLabelKey1, 626 | Operator: metav1.LabelSelectorOpIn, 627 | Values: []string{fakeLabelValue1, fakeLabelValue2}, 628 | }, 629 | }, 630 | }, 631 | }, 632 | }, 633 | }, 634 | want: true, 635 | wantErr: false, 636 | }, 637 | { 638 | name: "used PodDisruptionBudget should not be determined when no Pods with corresponding label exist", 639 | fields: fields{ 640 | pods: []*corev1.Pod{ 641 | { 642 | ObjectMeta: metav1.ObjectMeta{ 643 | Labels: map[string]string{ 644 | fakeLabelKey2: fakeLabelValue2, 645 | }, 646 | }, 647 | }, 648 | }, 649 | }, 650 | args: args{ 651 | pdb: &policyv1beta1.PodDisruptionBudget{ 652 | ObjectMeta: metav1.ObjectMeta{ 653 | Name: fakePodDisruptionBudget, 654 | }, 655 | Spec: policyv1beta1.PodDisruptionBudgetSpec{ 656 | Selector: &metav1.LabelSelector{ 657 | MatchLabels: map[string]string{ 658 | fakeLabelKey1: fakeLabelValue1, 659 | }, 660 | }, 661 | }, 662 | }, 663 | }, 664 | want: false, 665 | wantErr: false, 666 | }, 667 | } 668 | 669 | for _, tt := range tests { 670 | tt := tt 671 | 672 | t.Run(tt.name, func(t *testing.T) { 673 | t.Parallel() 674 | 675 | d := &determiner{ 676 | pods: tt.fields.pods, 677 | } 678 | 679 | got, err := d.determineUsedPodDisruptionBudget(tt.args.pdb) 680 | if (err != nil) != tt.wantErr { 681 | t.Errorf("determineUsedPodDisruptionBudget() error = %v, wantErr %v", err, tt.wantErr) 682 | return 683 | } 684 | 685 | if diff := cmp.Diff(tt.want, got); diff != "" { 686 | t.Errorf("(-want +got):\n%s", diff) 687 | } 688 | }) 689 | } 690 | } 691 | func Test_determiner_determineUsedSecret(t *testing.T) { 692 | const ( 693 | fakeSecret = "fake-secret" 694 | ) 695 | type fields struct { 696 | pods []*corev1.Pod 697 | } 698 | type args struct { 699 | secret string 700 | } 701 | tests := []struct { 702 | name string 703 | fields fields 704 | args args 705 | want map[string]struct{} 706 | }{ 707 | { 708 | name: "secrets used in ImagePullSecret should be determined as used", 709 | fields: fields{ 710 | pods: []*corev1.Pod{ 711 | { 712 | Spec: corev1.PodSpec{ 713 | ImagePullSecrets: []corev1.LocalObjectReference{{fakeSecret}}}, 714 | }, 715 | }, 716 | }, 717 | args: args{ 718 | secret: fakeSecret, 719 | }, 720 | want: map[string]struct{}{fakeSecret: {}}, 721 | }, 722 | { 723 | name: "secrets used in EnvFrom should be determined as used", 724 | fields: fields{ 725 | pods: []*corev1.Pod{{ 726 | Spec: corev1.PodSpec{ 727 | Containers: []corev1.Container{{ 728 | EnvFrom: []corev1.EnvFromSource{ 729 | {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: fakeSecret}}}, 730 | }, 731 | }}, 732 | }, 733 | }}, 734 | }, 735 | args: args{ 736 | secret: fakeSecret, 737 | }, 738 | want: map[string]struct{}{fakeSecret: {}}, 739 | }, 740 | } 741 | for _, tt := range tests { 742 | tt := tt 743 | 744 | t.Run(tt.name, func(t *testing.T) { 745 | t.Parallel() 746 | 747 | d := &determiner{ 748 | pods: tt.fields.pods, 749 | } 750 | got := d.detectUsedSecrets(nil) 751 | if diff := cmp.Diff(tt.want, got); diff != "" { 752 | t.Errorf("(-want +got):\n%s", diff) 753 | } 754 | }) 755 | } 756 | } 757 | -------------------------------------------------------------------------------- /pkg/determiner/fake.go: -------------------------------------------------------------------------------- 1 | package determiner 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | apimeta "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | cliresource "k8s.io/cli-runtime/pkg/resource" 10 | ) 11 | 12 | type FakeDeterminer struct { 13 | fakeObjectsToBeDeleted map[fakeObjectKey]struct{} 14 | 15 | mu sync.RWMutex 16 | } 17 | 18 | type fakeObjectKey struct { 19 | kind string 20 | name string 21 | namespace string 22 | } 23 | 24 | // Guarantee *FakeDeterminer implements Determiner. 25 | var _ Determiner = (*FakeDeterminer)(nil) 26 | 27 | func NewFakeDeterminer(objects ...runtime.Object) (*FakeDeterminer, error) { 28 | fakeObjectsToBeDeleted := make(map[fakeObjectKey]struct{}) 29 | 30 | accessor := apimeta.NewAccessor() 31 | 32 | for _, obj := range objects { 33 | kind, err := accessor.Kind(obj) 34 | if err != nil { 35 | return nil, err 36 | } 37 | name, err := accessor.Name(obj) 38 | if err != nil { 39 | return nil, err 40 | } 41 | namespace, err := accessor.Namespace(obj) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | key := fakeObjectKey{ 47 | kind: kind, 48 | name: name, 49 | namespace: namespace, 50 | } 51 | fakeObjectsToBeDeleted[key] = struct{}{} 52 | } 53 | 54 | return &FakeDeterminer{ 55 | fakeObjectsToBeDeleted: fakeObjectsToBeDeleted, 56 | mu: sync.RWMutex{}, 57 | }, nil 58 | } 59 | 60 | func (d *FakeDeterminer) DetermineDeletion(_ context.Context, info *cliresource.Info) (bool, error) { 61 | key := fakeObjectKey{ 62 | kind: info.Object.GetObjectKind().GroupVersionKind().Kind, 63 | name: info.Name, 64 | namespace: info.Namespace, 65 | } 66 | 67 | d.mu.RLock() 68 | _, ok := d.fakeObjectsToBeDeleted[key] 69 | d.mu.RUnlock() 70 | 71 | return ok, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import "github.com/AlecAivazis/survey/v2" 4 | 5 | func Confirm(message string) bool { 6 | c := &survey.Confirm{ 7 | Message: message, 8 | } 9 | 10 | var ans bool 11 | if err := survey.AskOne(c, &ans); err != nil { 12 | return false 13 | } 14 | 15 | return ans 16 | } 17 | -------------------------------------------------------------------------------- /pkg/resource/client.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | 6 | appsv1 "k8s.io/api/apps/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | apimeta "k8s.io/apimachinery/pkg/api/meta" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "k8s.io/client-go/dynamic" 14 | "k8s.io/client-go/kubernetes" 15 | ) 16 | 17 | type Client interface { 18 | ListPods(ctx context.Context, namespace string) ([]*corev1.Pod, error) 19 | ListReplicaSets(ctx context.Context, namespace string) ([]*appsv1.ReplicaSet, error) 20 | ListServiceAccounts(ctx context.Context, namespace string) ([]*corev1.ServiceAccount, error) 21 | ListPersistentVolumeClaims(ctx context.Context, namespace string) ([]*corev1.PersistentVolumeClaim, error) 22 | GetUnstructured(ctx context.Context, apiVersion, kind, name, namespace string) (*unstructured.Unstructured, error) 23 | } 24 | 25 | type client struct { 26 | clientset kubernetes.Interface 27 | dynamicClient dynamic.Interface 28 | } 29 | 30 | // Guarantee *client implements Client. 31 | var _ Client = (*client)(nil) 32 | 33 | func NewClient(clientset kubernetes.Interface, dynamicClient dynamic.Interface) Client { 34 | return &client{ 35 | clientset: clientset, 36 | dynamicClient: dynamicClient, 37 | } 38 | } 39 | 40 | func (c *client) ListPods(ctx context.Context, namespace string) ([]*corev1.Pod, error) { 41 | podList, err := c.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | pods := make([]*corev1.Pod, 0, len(podList.Items)) 47 | for i := range podList.Items { 48 | pods = append(pods, &podList.Items[i]) 49 | } 50 | 51 | return pods, nil 52 | } 53 | 54 | func (c *client) ListReplicaSets(ctx context.Context, namespace string) ([]*appsv1.ReplicaSet, error) { 55 | rsList, err := c.clientset.AppsV1().ReplicaSets(namespace).List(ctx, metav1.ListOptions{}) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | rss := make([]*appsv1.ReplicaSet, 0, len(rsList.Items)) 61 | for i := range rsList.Items { 62 | rss = append(rss, &rsList.Items[i]) 63 | } 64 | 65 | return rss, nil 66 | } 67 | 68 | func (c *client) ListServiceAccounts(ctx context.Context, namespace string) ([]*corev1.ServiceAccount, error) { 69 | saList, err := c.clientset.CoreV1().ServiceAccounts(namespace).List(ctx, metav1.ListOptions{}) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | sas := make([]*corev1.ServiceAccount, 0, len(saList.Items)) 75 | for i := range saList.Items { 76 | sas = append(sas, &saList.Items[i]) 77 | } 78 | 79 | return sas, nil 80 | } 81 | 82 | func (c *client) ListPersistentVolumeClaims(ctx context.Context, namespace string) ([]*corev1.PersistentVolumeClaim, error) { 83 | pvcList, err := c.clientset.CoreV1().PersistentVolumeClaims(namespace).List(ctx, metav1.ListOptions{}) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | pvcs := make([]*corev1.PersistentVolumeClaim, 0, len(pvcList.Items)) 89 | for i := range pvcList.Items { 90 | pvcs = append(pvcs, &pvcList.Items[i]) 91 | } 92 | 93 | return pvcs, nil 94 | } 95 | 96 | func (c *client) GetUnstructured(ctx context.Context, apiVersion, kind, name, namespace string) (*unstructured.Unstructured, error) { 97 | gv, err := schema.ParseGroupVersion(apiVersion) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | gvk := gv.WithKind(kind) 103 | gvr, _ := apimeta.UnsafeGuessKindToResource(gvk) 104 | 105 | u, err := c.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) 106 | switch { 107 | case err == nil: 108 | return u, nil 109 | case apierrors.IsNotFound(err): 110 | return nil, nil 111 | default: 112 | return nil, err 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pkg/resource/client_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | appsv1 "k8s.io/api/apps/v1" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | fakedynamic "k8s.io/client-go/dynamic/fake" 14 | fakeclientset "k8s.io/client-go/kubernetes/fake" 15 | "k8s.io/kubectl/pkg/scheme" 16 | ) 17 | 18 | func Test_client_ListPods(t *testing.T) { 19 | const ( 20 | fakeNamespace = "fake-ns" 21 | fakePod = "fake-pod" 22 | ) 23 | 24 | tests := []struct { 25 | name string 26 | objects []runtime.Object 27 | want []*corev1.Pod 28 | wantErr bool 29 | }{ 30 | { 31 | name: "expected Pods", 32 | objects: []runtime.Object{ 33 | &corev1.Pod{ 34 | ObjectMeta: metav1.ObjectMeta{ 35 | Name: fakePod, 36 | Namespace: fakeNamespace, 37 | }, 38 | }, 39 | }, 40 | want: []*corev1.Pod{ 41 | { 42 | ObjectMeta: metav1.ObjectMeta{ 43 | Name: fakePod, 44 | Namespace: fakeNamespace, 45 | }, 46 | }, 47 | }, 48 | wantErr: false, 49 | }, 50 | } 51 | 52 | for _, tt := range tests { 53 | tt := tt 54 | 55 | t.Run(tt.name, func(t *testing.T) { 56 | t.Parallel() 57 | 58 | c := &client{ 59 | clientset: fakeclientset.NewSimpleClientset(tt.objects...), 60 | } 61 | 62 | got, err := c.ListPods(context.Background(), fakeNamespace) 63 | if (err != nil) != tt.wantErr { 64 | t.Errorf("client.ListPods() error = %v, wantErr %v", err, tt.wantErr) 65 | return 66 | } 67 | if diff := cmp.Diff(tt.want, got); diff != "" { 68 | t.Errorf("(-want +got):\n%s", diff) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func Test_client_ListServiceAccounts(t *testing.T) { 75 | const ( 76 | fakeNamespace = "fake-ns" 77 | fakeServiceAccount = "fake-sa" 78 | ) 79 | 80 | tests := []struct { 81 | name string 82 | objects []runtime.Object 83 | want []*corev1.ServiceAccount 84 | wantErr bool 85 | }{ 86 | { 87 | name: "expected ServiceAccounts", 88 | objects: []runtime.Object{ 89 | &corev1.ServiceAccount{ 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: fakeServiceAccount, 92 | Namespace: fakeNamespace, 93 | }, 94 | }, 95 | }, 96 | want: []*corev1.ServiceAccount{ 97 | { 98 | ObjectMeta: metav1.ObjectMeta{ 99 | Name: fakeServiceAccount, 100 | Namespace: fakeNamespace, 101 | }, 102 | }, 103 | }, 104 | wantErr: false, 105 | }, 106 | } 107 | 108 | for _, tt := range tests { 109 | tt := tt 110 | 111 | t.Run(tt.name, func(t *testing.T) { 112 | t.Parallel() 113 | 114 | c := &client{ 115 | clientset: fakeclientset.NewSimpleClientset(tt.objects...), 116 | } 117 | 118 | got, err := c.ListServiceAccounts(context.Background(), fakeNamespace) 119 | if (err != nil) != tt.wantErr { 120 | t.Errorf("client.ListServiceAccounts() error = %v, wantErr %v", err, tt.wantErr) 121 | return 122 | } 123 | if diff := cmp.Diff(tt.want, got); diff != "" { 124 | t.Errorf("(-want +got):\n%s", diff) 125 | } 126 | }) 127 | } 128 | } 129 | 130 | func Test_client_ListPersistentVolumeClaims(t *testing.T) { 131 | const ( 132 | fakeNamespace = "fake-ns" 133 | fakePersistentVolumeClaim = "fake-pvc" 134 | ) 135 | 136 | tests := []struct { 137 | name string 138 | objects []runtime.Object 139 | want []*corev1.PersistentVolumeClaim 140 | wantErr bool 141 | }{ 142 | { 143 | name: "expected PersistentVolumeClaims", 144 | objects: []runtime.Object{ 145 | &corev1.PersistentVolumeClaim{ 146 | ObjectMeta: metav1.ObjectMeta{ 147 | Name: fakePersistentVolumeClaim, 148 | Namespace: fakeNamespace, 149 | }, 150 | }, 151 | }, 152 | want: []*corev1.PersistentVolumeClaim{ 153 | { 154 | ObjectMeta: metav1.ObjectMeta{ 155 | Name: fakePersistentVolumeClaim, 156 | Namespace: fakeNamespace, 157 | }, 158 | }, 159 | }, 160 | wantErr: false, 161 | }, 162 | } 163 | 164 | for _, tt := range tests { 165 | tt := tt 166 | 167 | t.Run(tt.name, func(t *testing.T) { 168 | t.Parallel() 169 | 170 | c := &client{ 171 | clientset: fakeclientset.NewSimpleClientset(tt.objects...), 172 | } 173 | 174 | got, err := c.ListPersistentVolumeClaims(context.Background(), fakeNamespace) 175 | if (err != nil) != tt.wantErr { 176 | t.Errorf("client.ListPersistentVolumeClaims() error = %v, wantErr %v", err, tt.wantErr) 177 | return 178 | } 179 | if diff := cmp.Diff(tt.want, got); diff != "" { 180 | t.Errorf("(-want +got):\n%s", diff) 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func Test_client_GetUnstructured(t *testing.T) { 187 | const ( 188 | fakeAPIVersion = "apps/v1" 189 | fakeKind = "Deployment" 190 | fakeName = "fake-deploy" 191 | fakeNamespace = "fake-ns" 192 | ) 193 | 194 | type args struct { 195 | apiVersion string 196 | kind string 197 | name string 198 | namespace string 199 | } 200 | 201 | tests := []struct { 202 | name string 203 | args args 204 | fakeObjects []runtime.Object 205 | want *unstructured.Unstructured 206 | wantErr bool 207 | }{ 208 | { 209 | name: "expected unstructured object", 210 | args: args{ 211 | apiVersion: fakeAPIVersion, 212 | kind: fakeKind, 213 | name: fakeName, 214 | namespace: fakeNamespace, 215 | }, 216 | fakeObjects: []runtime.Object{ 217 | &appsv1.Deployment{ 218 | ObjectMeta: metav1.ObjectMeta{ 219 | Name: fakeName, 220 | Namespace: fakeNamespace, 221 | }, 222 | }, 223 | }, 224 | want: &unstructured.Unstructured{ 225 | Object: map[string]interface{}{ 226 | "apiVersion": fakeAPIVersion, 227 | "kind": fakeKind, 228 | "metadata": map[string]interface{}{ 229 | "creationTimestamp": nil, 230 | "name": fakeName, 231 | "namespace": fakeNamespace, 232 | }, 233 | "spec": map[string]interface{}{ 234 | "selector": nil, 235 | "strategy": map[string]interface{}{}, 236 | "template": map[string]interface{}{ 237 | "metadata": map[string]interface{}{ 238 | "creationTimestamp": nil, 239 | }, 240 | "spec": map[string]interface{}{ 241 | "containers": nil, 242 | }, 243 | }, 244 | }, 245 | "status": map[string]interface{}{}, 246 | }, 247 | }, 248 | wantErr: false, 249 | }, 250 | } 251 | 252 | for _, tt := range tests { 253 | tt := tt 254 | 255 | t.Run(tt.name, func(t *testing.T) { 256 | t.Parallel() 257 | 258 | c := &client{ 259 | dynamicClient: fakedynamic.NewSimpleDynamicClient(scheme.Scheme, tt.fakeObjects...), 260 | } 261 | 262 | got, err := c.GetUnstructured(context.Background(), tt.args.apiVersion, tt.args.kind, tt.args.name, tt.args.namespace) 263 | if (err != nil) != tt.wantErr { 264 | t.Errorf("client.GetUnstructured() error = %v, wantErr %v", err, tt.wantErr) 265 | return 266 | } 267 | if diff := cmp.Diff(tt.want, got); diff != "" { 268 | t.Errorf("(-want +got):\n%s", diff) 269 | } 270 | }) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /pkg/resource/fake.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | apimeta "k8s.io/apimachinery/pkg/api/meta" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | ) 13 | 14 | type FakeClient struct { 15 | fakeObjects map[fakeObjectKey]runtime.Object 16 | fakePods []*corev1.Pod 17 | fakeReplicaSets []*appsv1.ReplicaSet 18 | fakeServiceAccounts []*corev1.ServiceAccount 19 | fakePersistentVolumeClaims []*corev1.PersistentVolumeClaim 20 | 21 | mu sync.RWMutex 22 | } 23 | 24 | type fakeObjectKey struct { 25 | apiVersion string 26 | kind string 27 | name string 28 | namespace string 29 | } 30 | 31 | func NewFakeClient(objects ...runtime.Object) (*FakeClient, error) { 32 | fakeObjects := make(map[fakeObjectKey]runtime.Object) 33 | 34 | var ( 35 | fakePods []*corev1.Pod 36 | fakeReplicaSets []*appsv1.ReplicaSet 37 | fakeServiceAccounts []*corev1.ServiceAccount 38 | fakePersistentVolumeClaims []*corev1.PersistentVolumeClaim 39 | ) 40 | 41 | accessor := apimeta.NewAccessor() 42 | 43 | for _, obj := range objects { 44 | kind, err := accessor.Kind(obj) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | switch kind { 50 | case KindPod: 51 | fakePods = append(fakePods, obj.(*corev1.Pod)) 52 | case KindReplicaSet: 53 | fakeReplicaSets = append(fakeReplicaSets, obj.(*appsv1.ReplicaSet)) 54 | case KindServiceAccount: 55 | fakeServiceAccounts = append(fakeServiceAccounts, obj.(*corev1.ServiceAccount)) 56 | case KindPersistentVolumeClaim: 57 | fakePersistentVolumeClaims = append(fakePersistentVolumeClaims, obj.(*corev1.PersistentVolumeClaim)) 58 | } 59 | 60 | apiVersion, err := accessor.APIVersion(obj) 61 | if err != nil { 62 | return nil, err 63 | } 64 | name, err := accessor.Name(obj) 65 | if err != nil { 66 | return nil, err 67 | } 68 | namespace, err := accessor.Namespace(obj) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | key := fakeObjectKey{ 74 | apiVersion: apiVersion, 75 | kind: kind, 76 | name: name, 77 | namespace: namespace, 78 | } 79 | fakeObjects[key] = obj 80 | } 81 | 82 | return &FakeClient{ 83 | fakeObjects: fakeObjects, 84 | fakePods: fakePods, 85 | fakeServiceAccounts: fakeServiceAccounts, 86 | fakePersistentVolumeClaims: fakePersistentVolumeClaims, 87 | }, nil 88 | } 89 | 90 | // Guarantee *FakeClient implements Client. 91 | var _ Client = (*FakeClient)(nil) 92 | 93 | func (c *FakeClient) ListPods(ctx context.Context, namespace string) ([]*corev1.Pod, error) { 94 | c.mu.RLock() 95 | pods := c.fakePods 96 | c.mu.RUnlock() 97 | return pods, nil 98 | } 99 | 100 | func (c *FakeClient) ListReplicaSets(ctx context.Context, namespace string) ([]*appsv1.ReplicaSet, error) { 101 | c.mu.RLock() 102 | rss := c.fakeReplicaSets 103 | c.mu.RUnlock() 104 | return rss, nil 105 | } 106 | 107 | func (c *FakeClient) ListServiceAccounts(ctx context.Context, namespace string) ([]*corev1.ServiceAccount, error) { 108 | c.mu.RLock() 109 | sas := c.fakeServiceAccounts 110 | c.mu.RUnlock() 111 | return sas, nil 112 | } 113 | 114 | func (c *FakeClient) ListPersistentVolumeClaims(ctx context.Context, namespace string) ([]*corev1.PersistentVolumeClaim, error) { 115 | c.mu.RLock() 116 | pvcs := c.fakePersistentVolumeClaims 117 | c.mu.RUnlock() 118 | return pvcs, nil 119 | } 120 | 121 | func (c *FakeClient) GetUnstructured(ctx context.Context, apiVersion, kind, name, namespace string) (*unstructured.Unstructured, error) { 122 | key := fakeObjectKey{ 123 | apiVersion: apiVersion, 124 | kind: kind, 125 | name: name, 126 | namespace: namespace, 127 | } 128 | 129 | c.mu.RLock() 130 | obj, ok := c.fakeObjects[key] 131 | c.mu.RUnlock() 132 | if !ok { 133 | return nil, nil 134 | } 135 | 136 | u, err := unstructuredConverter.ToUnstructured(obj) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | return &unstructured.Unstructured{ 142 | Object: u, 143 | }, nil 144 | } 145 | -------------------------------------------------------------------------------- /pkg/resource/persistentvolume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package resource 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | ) 22 | 23 | // CheckVolumeSatisfyClaim checks if the volume requested by the claim satisfies the requirements of the claim. 24 | func CheckVolumeSatisfyClaim(volume *corev1.PersistentVolume, claim *corev1.PersistentVolumeClaim) bool { 25 | if !checkCapacitySatisfyRequest(volume, claim) { 26 | return false 27 | } 28 | 29 | if !checkStorageClassMatch(volume, claim) { 30 | return false 31 | } 32 | 33 | if !checkVolumeModeMatch(volume, claim) { 34 | return false 35 | } 36 | 37 | if !checkAccessModesMatch(volume, claim) { 38 | return false 39 | } 40 | 41 | return true 42 | } 43 | 44 | // checkCapacitySatisfyRequest returns true if PV's capacity satisfies the PVC's requested resource. 45 | func checkCapacitySatisfyRequest(volume *corev1.PersistentVolume, claim *corev1.PersistentVolumeClaim) bool { 46 | volumeQty := volume.Spec.Capacity[corev1.ResourceStorage] 47 | volumeSize := volumeQty.Value() 48 | 49 | requestedQty := claim.Spec.Resources.Requests[corev1.ResourceName(corev1.ResourceStorage)] 50 | requestedSize := requestedQty.Value() 51 | 52 | return volumeSize >= requestedSize 53 | } 54 | 55 | // checkStorageClassMatch returns true if PV satisfies the PVC's requested StrageClass. 56 | func checkStorageClassMatch(volume *corev1.PersistentVolume, claim *corev1.PersistentVolumeClaim) bool { 57 | var ( 58 | requestedClass string 59 | pvVolumeClass string 60 | ok bool 61 | ) 62 | 63 | // Use beta annotation first 64 | requestedClass, ok = claim.Annotations[corev1.BetaStorageClassAnnotation] 65 | if !ok && claim.Spec.StorageClassName != nil { 66 | requestedClass = *claim.Spec.StorageClassName 67 | } 68 | 69 | // Use beta annotation first 70 | pvVolumeClass, ok = volume.Annotations[corev1.BetaStorageClassAnnotation] 71 | if !ok { 72 | pvVolumeClass = volume.Spec.StorageClassName 73 | } 74 | 75 | return requestedClass == pvVolumeClass 76 | } 77 | 78 | // checkVolumeModeMatch returns true if PV satisfies the PVC's requested VolumeMode. 79 | func checkVolumeModeMatch(volume *corev1.PersistentVolume, claim *corev1.PersistentVolumeClaim) bool { 80 | // In HA upgrades, we cannot guarantee that the apiserver is on a version >= controller-manager. 81 | // So we default a nil volumeMode to filesystem 82 | requestedVolumeMode := corev1.PersistentVolumeFilesystem 83 | if claim.Spec.VolumeMode != nil { 84 | requestedVolumeMode = *claim.Spec.VolumeMode 85 | } 86 | 87 | pvVolumeMode := corev1.PersistentVolumeFilesystem 88 | if volume.Spec.VolumeMode != nil { 89 | pvVolumeMode = *volume.Spec.VolumeMode 90 | } 91 | 92 | return requestedVolumeMode == pvVolumeMode 93 | } 94 | 95 | // checkAccessModesMatch returns true if PV satisfies all the PVC's requested AccessModes. 96 | func checkAccessModesMatch(volume *corev1.PersistentVolume, claim *corev1.PersistentVolumeClaim) bool { 97 | pvAccessModes := make(map[corev1.PersistentVolumeAccessMode]struct{}) 98 | for _, mode := range volume.Spec.AccessModes { 99 | pvAccessModes[mode] = struct{}{} 100 | } 101 | 102 | for _, mode := range claim.Spec.AccessModes { 103 | if _, ok := pvAccessModes[mode]; !ok { 104 | return false 105 | } 106 | } 107 | 108 | return true 109 | } 110 | -------------------------------------------------------------------------------- /pkg/resource/persistentvolume_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "testing" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | apiresource "k8s.io/apimachinery/pkg/api/resource" 8 | ) 9 | 10 | func TestCheckVolumeSatisfyClaim(t *testing.T) { 11 | var ( 12 | fakeResourceQtyLow = "1Gi" 13 | fakeResourceQtyHigh = "2Gi" 14 | fakeStorageClass1 = "standard" 15 | fakeStorageClass2 = "slow" 16 | fakeVolumeMode = corev1.PersistentVolumeFilesystem 17 | ) 18 | 19 | type args struct { 20 | volume *corev1.PersistentVolume 21 | claim *corev1.PersistentVolumeClaim 22 | } 23 | 24 | tests := []struct { 25 | name string 26 | args args 27 | want bool 28 | }{ 29 | { 30 | name: "PersistentVolume should satisfy PersistentVolumeClaim", 31 | args: args{ 32 | volume: &corev1.PersistentVolume{ 33 | Spec: corev1.PersistentVolumeSpec{ 34 | Capacity: corev1.ResourceList{ 35 | corev1.ResourceName(corev1.ResourceStorage): apiresource.MustParse(fakeResourceQtyHigh), 36 | }, 37 | StorageClassName: fakeStorageClass1, 38 | VolumeMode: &fakeVolumeMode, 39 | AccessModes: []corev1.PersistentVolumeAccessMode{ 40 | corev1.ReadWriteOnce, 41 | corev1.ReadWriteMany, 42 | }, 43 | }, 44 | }, 45 | claim: &corev1.PersistentVolumeClaim{ 46 | Spec: corev1.PersistentVolumeClaimSpec{ 47 | Resources: corev1.ResourceRequirements{ 48 | Requests: corev1.ResourceList{ 49 | corev1.ResourceName(corev1.ResourceStorage): apiresource.MustParse(fakeResourceQtyLow), 50 | }, 51 | }, 52 | StorageClassName: &fakeStorageClass1, 53 | VolumeMode: &fakeVolumeMode, 54 | AccessModes: []corev1.PersistentVolumeAccessMode{ 55 | corev1.ReadWriteOnce, 56 | }, 57 | }, 58 | }, 59 | }, 60 | want: true, 61 | }, 62 | { 63 | name: "PersistentVolume with different StorageClass should not satisfy PersistentVolumeClaim", 64 | args: args{ 65 | volume: &corev1.PersistentVolume{ 66 | Spec: corev1.PersistentVolumeSpec{ 67 | Capacity: corev1.ResourceList{ 68 | corev1.ResourceName(corev1.ResourceStorage): apiresource.MustParse(fakeResourceQtyHigh), 69 | }, 70 | StorageClassName: fakeStorageClass1, 71 | VolumeMode: &fakeVolumeMode, 72 | AccessModes: []corev1.PersistentVolumeAccessMode{ 73 | corev1.ReadWriteOnce, 74 | corev1.ReadWriteMany, 75 | }, 76 | }, 77 | }, 78 | claim: &corev1.PersistentVolumeClaim{ 79 | Spec: corev1.PersistentVolumeClaimSpec{ 80 | Resources: corev1.ResourceRequirements{ 81 | Requests: corev1.ResourceList{ 82 | corev1.ResourceName(corev1.ResourceStorage): apiresource.MustParse(fakeResourceQtyLow), 83 | }, 84 | }, 85 | StorageClassName: &fakeStorageClass2, 86 | VolumeMode: &fakeVolumeMode, 87 | AccessModes: []corev1.PersistentVolumeAccessMode{ 88 | corev1.ReadWriteOnce, 89 | }, 90 | }, 91 | }, 92 | }, 93 | want: false, 94 | }, 95 | } 96 | 97 | for _, tt := range tests { 98 | tt := tt 99 | 100 | t.Run(tt.name, func(t *testing.T) { 101 | t.Parallel() 102 | 103 | if got := CheckVolumeSatisfyClaim(tt.args.volume, tt.args.claim); got != tt.want { 104 | t.Errorf("CheckVolumeSatisfyClaim() = %v, want %v", got, tt.want) 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | autoscalingv1 "k8s.io/api/autoscaling/v1" 6 | batchv1 "k8s.io/api/batch/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | policyv1beta1 "k8s.io/api/policy/v1beta1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | ) 11 | 12 | const ( 13 | KindPod = "Pod" 14 | KindReplicaSet = "ReplicaSet" 15 | KindConfigMap = "ConfigMap" 16 | KindSecret = "Secret" 17 | KindServiceAccount = "ServiceAccount" 18 | KindPersistentVolume = "PersistentVolume" 19 | KindPersistentVolumeClaim = "PersistentVolumeClaim" 20 | KindJob = "Job" 21 | KindPodDisruptionBudget = "PodDisruptionBudget" 22 | KindHorizontalPodAutoscaler = "HorizontalPodAutoscaler" 23 | ) 24 | 25 | var unstructuredConverter = runtime.DefaultUnstructuredConverter 26 | 27 | func ObjectToPod(obj runtime.Object) (*corev1.Pod, error) { 28 | u, err := toUnstructured(obj) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | var pod corev1.Pod 34 | if err := fromUnstructured(u, &pod); err != nil { 35 | return nil, err 36 | } 37 | 38 | return &pod, nil 39 | } 40 | 41 | func ObjectToReplicaSet(obj runtime.Object) (*appsv1.ReplicaSet, error) { 42 | u, err := toUnstructured(obj) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | var rs appsv1.ReplicaSet 48 | if err := fromUnstructured(u, &rs); err != nil { 49 | return nil, err 50 | } 51 | 52 | return &rs, nil 53 | } 54 | 55 | func ObjectToPersistentVolume(obj runtime.Object) (*corev1.PersistentVolume, error) { 56 | u, err := toUnstructured(obj) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | var volume corev1.PersistentVolume 62 | if err := fromUnstructured(u, &volume); err != nil { 63 | return nil, err 64 | } 65 | 66 | return &volume, nil 67 | } 68 | 69 | func ObjectToJob(obj runtime.Object) (*batchv1.Job, error) { 70 | u, err := toUnstructured(obj) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | var job batchv1.Job 76 | if err := fromUnstructured(u, &job); err != nil { 77 | return nil, err 78 | } 79 | 80 | return &job, nil 81 | } 82 | 83 | func ObjectToPodDisruptionBudget(obj runtime.Object) (*policyv1beta1.PodDisruptionBudget, error) { 84 | u, err := toUnstructured(obj) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | var pdb policyv1beta1.PodDisruptionBudget 90 | if err := fromUnstructured(u, &pdb); err != nil { 91 | return nil, err 92 | } 93 | 94 | return &pdb, nil 95 | } 96 | 97 | func ObjectToHorizontalPodAutoscaler(obj runtime.Object) (*autoscalingv1.HorizontalPodAutoscaler, error) { 98 | u, err := toUnstructured(obj) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | var hpa autoscalingv1.HorizontalPodAutoscaler 104 | if err := fromUnstructured(u, &hpa); err != nil { 105 | return nil, err 106 | } 107 | 108 | return &hpa, nil 109 | } 110 | 111 | func toUnstructured(obj runtime.Object) (map[string]interface{}, error) { 112 | return unstructuredConverter.ToUnstructured(obj) 113 | } 114 | 115 | func fromUnstructured(u map[string]interface{}, obj interface{}) error { 116 | return unstructuredConverter.FromUnstructured(u, obj) 117 | } 118 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // Dynamically inserted at build time. See `ldflags` in .goreleaser.yml. 4 | var ( 5 | Version = "unset" 6 | Revision = "unset" 7 | ) 8 | --------------------------------------------------------------------------------