├── .github └── workflows │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── .krew.yaml ├── LICENSE.md ├── Makefile ├── README.md ├── cmd └── kube-lineage │ ├── main.go │ └── main_test.go ├── go.mod ├── go.sum ├── internal ├── client │ ├── client.go │ ├── flags.go │ └── resource.go ├── completion │ └── completion.go ├── graph │ ├── graph.go │ ├── helm.go │ └── kubernetes.go ├── log │ └── log.go ├── printers │ ├── flags.go │ ├── flags_humanreadable.go │ ├── printers.go │ └── printers_humanreadable.go └── version │ └── version.go ├── pkg └── cmd │ ├── helm │ ├── completion.go │ ├── flags.go │ └── helm.go │ └── lineage │ ├── completion.go │ ├── flags.go │ └── lineage.go └── scripts ├── fetch.sh └── goreleaser_install.sh /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # This workflow is triggered on push or pull request for the master branch. 2 | # It runs tests and various checks to validate that the proposed changes 3 | # will not introduce any regression after merging the code to the master branch. 4 | name: build 5 | on: 6 | push: 7 | branches: 8 | - master 9 | paths-ignore: 10 | - '*.md' 11 | pull_request: 12 | branches: 13 | - master 14 | paths-ignore: 15 | - '*.md' 16 | permissions: 17 | contents: read 18 | env: 19 | GO_VERSION: "1.17" 20 | jobs: 21 | lint: 22 | name: Run linter 23 | runs-on: ubuntu-18.04 24 | steps: 25 | - name: Setup Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: ${{ env.GO_VERSION }} 29 | - name: Checkout code 30 | uses: actions/checkout@v2 31 | - uses: actions/cache@v2 32 | with: 33 | path: ~/go/pkg/mod 34 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 35 | restore-keys: | 36 | ${{ runner.os }}-go- 37 | - name: Run linter 38 | run: make lint 39 | test: 40 | name: Run tests 41 | runs-on: ubuntu-18.04 42 | steps: 43 | - name: Setup Go 44 | uses: actions/setup-go@v2 45 | with: 46 | go-version: ${{ env.GO_VERSION }} 47 | - name: Checkout code 48 | uses: actions/checkout@v2 49 | - uses: actions/cache@v2 50 | with: 51 | path: ~/go/pkg/mod 52 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 53 | restore-keys: | 54 | ${{ runner.os }}-go- 55 | - name: Run tests 56 | run: make test 57 | release-snapshot: 58 | name: Release unversioned snapshot 59 | needs: 60 | - lint 61 | - test 62 | runs-on: ubuntu-18.04 63 | steps: 64 | - name: Setup Go 65 | uses: actions/setup-go@v2 66 | with: 67 | go-version: ${{ env.GO_VERSION }} 68 | - name: Checkout code 69 | uses: actions/checkout@v2 70 | - uses: actions/cache@v2 71 | with: 72 | path: ~/go/pkg/mod 73 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 74 | restore-keys: | 75 | ${{ runner.os }}-go- 76 | - name: Release 77 | run: make release-snapshot 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # This workflow is triggered on new tags. 2 | # It runs tests to validate that the code is working before publishing a new 3 | # version for plugin "lineage" in krew-index. 4 | name: release 5 | on: 6 | push: 7 | tags: 8 | - 'v*.*.*' 9 | env: 10 | GO_VERSION: "1.17" 11 | jobs: 12 | lint: 13 | name: Run linter 14 | runs-on: ubuntu-18.04 15 | steps: 16 | - name: Setup Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ env.GO_VERSION }} 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | - uses: actions/cache@v2 23 | with: 24 | path: ~/go/pkg/mod 25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 26 | restore-keys: | 27 | ${{ runner.os }}-go- 28 | - name: Run linter 29 | run: make lint 30 | test: 31 | name: Run tests 32 | runs-on: ubuntu-18.04 33 | steps: 34 | - name: Setup Go 35 | uses: actions/setup-go@v2 36 | with: 37 | go-version: ${{ env.GO_VERSION }} 38 | - name: Checkout code 39 | uses: actions/checkout@v2 40 | - uses: actions/cache@v2 41 | with: 42 | path: ~/go/pkg/mod 43 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 44 | restore-keys: | 45 | ${{ runner.os }}-go- 46 | - name: Run tests 47 | run: make test 48 | release: 49 | name: Release 50 | runs-on: ubuntu-18.04 51 | permissions: 52 | contents: write 53 | steps: 54 | - name: Setup Go 55 | uses: actions/setup-go@v2 56 | with: 57 | go-version: ${{ env.GO_VERSION }} 58 | - name: Checkout code 59 | uses: actions/checkout@v2 60 | - uses: actions/cache@v2 61 | with: 62 | path: ~/go/pkg/mod 63 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 64 | restore-keys: | 65 | ${{ runner.os }}-go- 66 | - name: Release 67 | run: make release 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | KREW_GITHUB_TOKEN: ${{ secrets.KREW_GITHUB_TOKEN }} 71 | - name: Update new version for plugin "lineage" in krew-index 72 | uses: rajatjindal/krew-release-bot@v0.0.40 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Output of the GoReleaser tool 15 | dist/ 16 | 17 | # Binaries for make targets, downloaded via fetch.sh script 18 | bin/ 19 | 20 | # GoLand IDEA 21 | /.idea/ 22 | *.iml 23 | 24 | # VS Code 25 | .vscode 26 | 27 | # Emacs 28 | *~ 29 | \#*\# 30 | 31 | # Miscellaneous files 32 | *.sw[op] 33 | *.DS_Store 34 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | linters: 4 | enable-all: true 5 | disable: 6 | - golint # deprecated 7 | - interfacer # deprecated 8 | - maligned # deprecated 9 | - scopelint # deprecated 10 | - cyclop # duplicate of gocyclo 11 | - rowserrcheck # exclude 'sql' preset 12 | - sqlclosecheck # exclude 'sql' preset 13 | - exhaustivestruct 14 | - gochecknoglobals 15 | - goconst 16 | - godox 17 | - goerr113 18 | - gomnd 19 | - gomoddirectives 20 | - lll 21 | - nlreturn 22 | - prealloc 23 | - wrapcheck 24 | - wsl 25 | linters-settings: 26 | depguard: 27 | list-type: blacklist 28 | include-go-root: true 29 | packages: 30 | - errors 31 | - io/ioutil 32 | - sync/atomic 33 | - github.com/pkg/errors 34 | - github.com/stretchr/testify/assert 35 | - gotest.tools/v3 36 | packages-with-error-message: 37 | - errors: "Use github.com/cockroachdb/errors instead." 38 | - io/ioutil: "The 'io/ioutil' package is deprecated. Use corresponding 'os' or 'io' functions instead." 39 | - sync/atomic: "Use go.uber.org/atomic instead." 40 | - github.com/pkg/errors: "Use github.com/cockroachdb/errors instead." 41 | - github.com/stretchr/testify/assert: "Use github.com/stretchr/testify/require instead." 42 | - gotest.tools/v3: "Use github.com/stretchr/testify instead." 43 | gci: 44 | local-prefixes: github.com/tohjustin/kube-lineage 45 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: kube-lineage 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - binary: kube-lineage 7 | main: ./cmd/kube-lineage 8 | goos: 9 | - darwin 10 | - linux 11 | - windows 12 | goarch: 13 | - amd64 14 | - arm64 15 | ignore: 16 | - goos: windows 17 | goarch: arm64 18 | env: 19 | - CGO_ENABLED=0 20 | ldflags: 21 | - -s 22 | - -w 23 | - -X github.com/tohjustin/kube-lineage/internal/version.buildDate={{ .Env.BUILD_DATE }} 24 | - -X github.com/tohjustin/kube-lineage/internal/version.gitCommit={{ .Env.GIT_COMMIT }} 25 | - -X github.com/tohjustin/kube-lineage/internal/version.gitTreeState={{ .Env.GIT_TREE_STATE }} 26 | - -X github.com/tohjustin/kube-lineage/internal/version.gitVersion={{ .Env.GIT_VERSION }} 27 | - -X github.com/tohjustin/kube-lineage/internal/version.gitVersionMajor={{ .Env.GIT_VERSION_MAJOR }} 28 | - -X github.com/tohjustin/kube-lineage/internal/version.gitVersionMinor={{ .Env.GIT_VERSION_MINOR }} 29 | archives: 30 | - files: 31 | - LICENSE.md 32 | - README.md 33 | format_overrides: 34 | - goos: windows 35 | format: zip 36 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" 37 | wrap_in_directory: false 38 | checksum: 39 | name_template: checksums.txt 40 | snapshot: 41 | name_template: "{{ .Tag }}-next" 42 | changelog: 43 | sort: asc 44 | filters: 45 | exclude: 46 | - "^build(\\(.+\\))?:" 47 | - "^chore(\\(.+\\))?:" 48 | - "^ci(\\(.+\\))?:" 49 | - "^docs(\\(.+\\))?:" 50 | - "^perf(\\(.+\\))?:" 51 | - "^refactor(\\(.+\\))?:" 52 | - "^style(\\(.+\\))?:" 53 | - "^test(\\(.+\\))?:" 54 | krews: 55 | - name: lineage 56 | index: 57 | owner: tohjustin 58 | name: kubectl-plugins 59 | branch: master 60 | token: "{{ .Env.KREW_GITHUB_TOKEN }}" 61 | url_template: "https://github.com/tohjustin/kube-lineage/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 62 | commit_author: 63 | name: Justin Toh 64 | email: tohjustin@hotmail.com 65 | commit_msg_template: "Krew plugin update for {{ .ProjectName }} version {{ .Tag }}" 66 | homepage: https://github.com/tohjustin/kube-lineage 67 | short_description: Display all dependent resources or resource dependencies 68 | description: | 69 | This plugin prints a table of dependencies or dependents of the specified 70 | resource. 71 | caveats: | 72 | The tool only shows dependencies or dependents among the resources you have 73 | access to. So for restricted users, the result may be incomplete. 74 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: lineage 5 | spec: 6 | version: {{ .TagName }} 7 | homepage: https://github.com/tohjustin/kube-lineage 8 | shortDescription: Display all dependent resources or resource dependencies 9 | description: | 10 | This plugin prints a table of dependencies or dependents of the specified 11 | resource. 12 | caveats: | 13 | The tool only shows dependencies or dependents among the resources you have 14 | access to. So for restricted users, the result may be incomplete. 15 | platforms: 16 | - selector: 17 | matchLabels: 18 | os: darwin 19 | arch: amd64 20 | {{addURIAndSha "https://github.com/tohjustin/kube-lineage/releases/download/{{ .TagName }}/kube-lineage_darwin_amd64.tar.gz" .TagName }} 21 | bin: kube-lineage 22 | - selector: 23 | matchLabels: 24 | os: darwin 25 | arch: arm64 26 | {{addURIAndSha "https://github.com/tohjustin/kube-lineage/releases/download/{{ .TagName }}/kube-lineage_darwin_arm64.tar.gz" .TagName }} 27 | bin: kube-lineage 28 | - selector: 29 | matchLabels: 30 | os: linux 31 | arch: amd64 32 | {{addURIAndSha "https://github.com/tohjustin/kube-lineage/releases/download/{{ .TagName }}/kube-lineage_linux_amd64.tar.gz" .TagName }} 33 | bin: kube-lineage 34 | - selector: 35 | matchLabels: 36 | os: linux 37 | arch: arm64 38 | {{addURIAndSha "https://github.com/tohjustin/kube-lineage/releases/download/{{ .TagName }}/kube-lineage_linux_arm64.tar.gz" .TagName }} 39 | bin: kube-lineage 40 | - selector: 41 | matchLabels: 42 | os: windows 43 | arch: amd64 44 | {{addURIAndSha "https://github.com/tohjustin/kube-lineage/releases/download/{{ .TagName }}/kube-lineage_windows_amd64.zip" .TagName }} 45 | bin: kube-lineage.exe 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | 3 | GO_VERSION = "1.17" 4 | GOLANGCI_LINT_VERSION = "1.42.1" 5 | GORELEASER_VERSION = "1.0.0" 6 | 7 | export BUILD_DATE = $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 8 | export GIT_COMMIT = $(shell git rev-parse HEAD) 9 | export GIT_TREE_STATE = $(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi) 10 | export GIT_VERSION = $(shell git describe --tags --always | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-.*)?') 11 | export GIT_VERSION_MAJOR = $(shell if [[ "${GIT_VERSION}" ]]; then echo ${GIT_VERSION} | cut -d 'v' -f 2 | cut -d "." -f 1 ; fi) 12 | export GIT_VERSION_MINOR = $(shell if [[ "${GIT_VERSION}" ]]; then echo ${GIT_VERSION} | cut -d 'v' -f 2 | cut -d "." -f 2 ; fi) 13 | export CGO_ENABLED = 1 14 | 15 | REPO = $(shell go list -m) 16 | GO_BUILD_ARGS = \ 17 | -gcflags "all=-trimpath=$(shell dirname $(shell pwd))" \ 18 | -asmflags "all=-trimpath=$(shell dirname $(shell pwd))" \ 19 | -ldflags " \ 20 | -s \ 21 | -w \ 22 | -X '$(REPO)/internal/version.buildDate=$(BUILD_DATE)' \ 23 | -X '$(REPO)/internal/version.gitCommit=$(GIT_COMMIT)' \ 24 | -X '$(REPO)/internal/version.gitTreeState=$(GIT_TREE_STATE)' \ 25 | -X '$(REPO)/internal/version.gitVersion=$(GIT_VERSION)' \ 26 | -X '$(REPO)/internal/version.gitVersionMajor=$(GIT_VERSION_MAJOR)' \ 27 | -X '$(REPO)/internal/version.gitVersionMinor=$(GIT_VERSION_MINOR)' \ 28 | " \ 29 | 30 | .PHONY: all 31 | all: install 32 | 33 | .PHONY: clean 34 | clean: 35 | rm -rf bin dist 36 | 37 | .PHONY: lint 38 | lint: 39 | source ./scripts/fetch.sh; fetch golangci-lint $(GOLANGCI_LINT_VERSION) && ./bin/golangci-lint run 40 | 41 | .PHONY: test 42 | test: 43 | go test ./... 44 | 45 | .PHONY: build 46 | build: 47 | go build $(GO_BUILD_ARGS) -o bin/kube-lineage ./cmd/kube-lineage 48 | 49 | .PHONY: install 50 | install: build 51 | install bin/kube-lineage $(shell go env GOPATH)/bin 52 | 53 | .PHONY: release 54 | RELEASE_ARGS?=release --rm-dist 55 | release: 56 | source ./scripts/fetch.sh; fetch goreleaser $(GORELEASER_VERSION) && ./bin/goreleaser $(RELEASE_ARGS) 57 | 58 | .PHONY: release-snapshot 59 | RELEASE_SNAPSHOT_ARGS?=release --rm-dist --skip-publish --snapshot 60 | release-snapshot: 61 | source ./scripts/fetch.sh; fetch goreleaser $(GORELEASER_VERSION) && ./bin/goreleaser $(RELEASE_SNAPSHOT_ARGS) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kube-lineage 2 | 3 | [![build](https://github.com/tohjustin/kube-lineage/actions/workflows/build.yaml/badge.svg)](https://github.com/tohjustin/kube-lineage/actions/workflows/build.yaml) 4 | [![release](https://aegisbadges.appspot.com/static?subject=release&status=v0.5.0&color=318FE0)](https://github.com/tohjustin/kube-lineage/releases) 5 | [![kubernetes compatibility](https://aegisbadges.appspot.com/static?subject=k8s%20compatibility&status=v1.21%2B&color=318FE0)](https://endoflife.date/kubernetes) 6 | [![helm compatibility](https://aegisbadges.appspot.com/static?subject=helm%20compatibility&status=v3&color=318FE0)](https://helm.sh/docs/topics/v2_v3_migration) 7 | [![license](https://aegisbadges.appspot.com/static?subject=license&status=Apache-2.0&color=318FE0)](./LICENSE.md) 8 | 9 | A CLI tool to display all dependencies or dependents of an object in a Kubernetes cluster. 10 | 11 | ## Usage 12 | 13 | ```shell 14 | $ kube-lineage clusterrole system:metrics-server --output=wide 15 | NAMESPACE NAME READY STATUS AGE RELATIONSHIPS 16 | ClusterRole/system:metrics-server - 30m [] 17 | └── ClusterRoleBinding/system:metrics-server - 30m [ClusterRoleBindingRole] 18 | kube-system └── ServiceAccount/metrics-server - 30m [ClusterRoleBindingSubject] 19 | kube-system ├── Pod/metrics-server-7b4f8b595-8m7rz 1/1 Running 30m [PodServiceAccount] 20 | kube-system │ └── Service/metrics-server - 30m [Service] 21 | │ ├── APIService/v1beta1.metrics.k8s.io True 30m [APIService] 22 | kube-system │ └── EndpointSlice.discovery/metrics-server-wb2cm - 30m [ControllerReference OwnerReference] 23 | kube-system └── Secret/metrics-server-token-nqw85 - 30m [ServiceAccountSecret] 24 | kube-system └── Pod/metrics-server-7b4f8b595-8m7rz 1/1 Running 30m [PodVolume] 25 | ``` 26 | 27 | Use either the `--dependencies` or `-D` flag to show dependencies instead of dependents 28 | 29 | ```shell 30 | $ kube-lineage pod coredns-5cc79d4bf5-xgvkc --dependencies 31 | NAMESPACE NAME READY STATUS AGE 32 | kube-system Pod/coredns-5cc79d4bf5-xgvkc 1/1 Running 30m 33 | ├── Node/k3d-server True KubeletReady 30m 34 | ├── PodSecurityPolicy/system-unrestricted-psp - 30m 35 | kube-system ├── ConfigMap/coredns - 30m 36 | kube-system ├── ReplicaSet/coredns-5cc79d4bf5 1/1 30m 37 | kube-system │ └── Deployment/coredns 1/1 30m 38 | kube-system ├── Secret/coredns-token-6vsx4 - 30m 39 | kube-system │ └── ServiceAccount/coredns - 30m 40 | │ ├── ClusterRoleBinding/system:basic-user - 30m 41 | │ │ └── ClusterRole/system:basic-user - 30m 42 | │ ├── ClusterRoleBinding/system:coredns - 30m 43 | │ │ └── ClusterRole/system:coredns - 30m 44 | │ ├── ClusterRoleBinding/system:discovery - 30m 45 | │ │ └── ClusterRole/system:discovery - 30m 46 | │ ├── ClusterRoleBinding/system:public-info-viewer - 30m 47 | │ │ └── ClusterRole/system:public-info-viewer - 30m 48 | kube-system │ └── RoleBinding/system-unrestricted-svc-acct-psp-rolebinding - 30m 49 | │ └── ClusterRole/system-unrestricted-psp-role - 30m 50 | │ └── PodSecurityPolicy/system-unrestricted-psp - 30m 51 | kube-system └── ServiceAccount/coredns - 30m 52 | ``` 53 | 54 | Use the `helm` subcommand to display Helm release resources & optionally their respective dependents in a Kubernetes cluster. 55 | 56 | ```shell 57 | $ kube-lineage helm kube-state-metrics -n monitoring-system 58 | helm kube-state-metrics -n monitoring-system 59 | NAMESPACE NAME READY STATUS AGE 60 | monitoring-system kube-state-metrics True Deployed 25m 61 | ├── ClusterRole/kube-state-metrics - 25m 62 | │ └── ClusterRoleBinding/kube-state-metrics - 25m 63 | monitoring-system │ └── ServiceAccount/kube-state-metrics - 25m 64 | monitoring-system │ ├── Pod/kube-state-metrics-7dff544777-jb2q2 1/1 Running 25m 65 | monitoring-system │ │ └── Service/kube-state-metrics - 25m 66 | monitoring-system │ │ └── EndpointSlice/kube-state-metrics-rq8wk - 25m 67 | monitoring-system │ └── Secret/kube-state-metrics-token-bsr4q - 25m 68 | monitoring-system │ └── Pod/kube-state-metrics-7dff544777-jb2q2 1/1 Running 25m 69 | ├── ClusterRoleBinding/kube-state-metrics - 25m 70 | monitoring-system ├── Deployment/kube-state-metrics 1/1 25m 71 | monitoring-system │ └── ReplicaSet/kube-state-metrics-7dff544777 1/1 25m 72 | monitoring-system │ └── Pod/kube-state-metrics-7dff544777-jb2q2 1/1 Running 25m 73 | monitoring-system ├── Secret/sh.helm.release.v1.kube-state-metrics.v1 - 25m 74 | monitoring-system ├── Service/kube-state-metrics - 25m 75 | monitoring-system └── ServiceAccount/kube-state-metrics 76 | 77 | $ kube-lineage helm traefik --depth 1 --label-columns app.kubernetes.io/managed-by --label-columns owner 78 | NAMESPACE NAME READY STATUS AGE MANAGED-BY OWNER 79 | kube-system traefik True Deployed 30m 80 | ├── ClusterRole/traefik - 30m Helm 81 | ├── ClusterRoleBinding/traefik - 30m Helm 82 | kube-system ├── ConfigMap/traefik - 30m Helm 83 | kube-system ├── ConfigMap/traefik-test - 30m Helm 84 | kube-system ├── Deployment/traefik 1/1 30m Helm 85 | kube-system ├── Secret/sh.helm.release.v1.traefik.v1 - 30m helm 86 | kube-system ├── Secret/traefik-default-cert - 30m Helm 87 | kube-system ├── Service/traefik - 30m Helm 88 | kube-system ├── Service/traefik-prometheus - 30m Helm 89 | kube-system └── ServiceAccount/traefik - 30m Helm 90 | ``` 91 | 92 | Use either the `split` or `split-wide` output format to display resources grouped by their type. 93 | 94 | ```shell 95 | $ kube-lineage deploy/coredns --output=split --show-group 96 | NAME READY UP-TO-DATE AVAILABLE AGE 97 | deployment.apps/coredns 3/3 3 3 30m 98 | 99 | NAME ADDRESSTYPE PORTS ENDPOINTS AGE 100 | endpointslice.discovery.k8s.io/kube-dns-mz9bw IPv4 53,9153,53 10.42.0.24,10.42.0.26,10.42.0.27 30m 101 | 102 | NAME READY STATUS RESTARTS AGE 103 | pod/coredns-5cc79d4bf5-xgvkc 1/1 Running 0 30m 104 | pod/coredns-5cc79d4bf5-rjc7d 1/1 Running 0 30m 105 | pod/coredns-5cc79d4bf5-tt2zl 1/1 Running 0 30m 106 | 107 | NAME DESIRED CURRENT READY AGE 108 | replicaset.apps/coredns-5cc79d4bf5 3 3 3 30m 109 | 110 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 111 | service/kube-dns ClusterIP 10.43.0.10 53/UDP,53/TCP,9153/TCP 30m 112 | ``` 113 | 114 | ### Flags 115 | 116 | Flags for configuring relationship discovery parameters 117 | 118 | | Flag | Description | 119 | | ---- | ----------- | 120 | | `--all-namespaces`, `-A` | If present, list object relationships across all namespaces | 121 | | `--dependencies`, `-D` | If present, list object dependencies instead of dependents.
Not supported in `helm` subcommand | 122 | | `--depth`, `-d` | Maximum depth to find relationships | 123 | | `--exclude-types` | Accepts a comma separated list of resource types to exclude from relationship discovery.
You can also use multiple flag options like --exclude-types type1 --exclude-types type2... | 124 | | `--include-types` | Accepts a comma separated list of resource types to only include in relationship discovery.
You can also use multiple flag options like --include-types type1 --include-types type2... | 125 | | `--scopes`, `-S` | Accepts a comma separated list of additional namespaces to find relationships.
You can also use multiple flag options like -S namespace1 -S namespace2... | 126 | 127 | Flags for configuring output format 128 | 129 | | Flag | Description | 130 | | ---- | ----------- | 131 | | `--output`, `-o` | Output format. One of: wide \| split \| split-wide | 132 | | `--label-columns`, `-L` | Accepts a comma separated list of labels that are going to be presented as columns.
You can also use multiple flag options like -L label1 -L label2... | 133 | | `--no-headers` | When using the default output format, don't print headers | 134 | | `--show-group` | If present, include the resource group for the requested object(s) | 135 | | `--show-label` | When printing, show all labels as the last column | 136 | | `--show-namespace` | When printing, show namespace as the first column | 137 | 138 | Use the following commands to view the full list of supported flags 139 | 140 | ```shell 141 | $ kube-lineage --help 142 | $ kube-lineage helm --help 143 | ``` 144 | 145 | ## Supported Relationships 146 | 147 | List of supported relationships used for discovering dependent objects: 148 | 149 | - Kubernetes 150 | - [Controller](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/controller-ref.md) & [Owner](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/) References 151 | - Core APIs: [Event](https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/event-v1/), [PersistentVolume](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/), [PersistentVolumeClaim](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/), [Pod](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/), [Service](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/), [ServiceAccount](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/service-account-v1/) 152 | - `policy` APIs: [PodDisruptionBudget](https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/pod-disruption-budget-v1), [PodSecurityPolicy](https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/pod-disruption-budget-v1/) 153 | - `admissionregistration.k8s.io` APIs: [MutatingWebhookConfiguration](https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/mutating-webhook-configuration-v1/) & [ValidatingWebhookConfiguration](https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/validating-webhook-configuration-v1/) 154 | - `apiregistration.k8s.io` APIs: [APIService](https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/api-service-v1/) 155 | - `networking.k8s.io` APIs: [Ingress](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/ingress-v1/), [IngressClass](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/ingress-class-v1/), [NetworkPolicy](https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/network-policy-v1/) 156 | - `node.k8s.io` APIs: [RuntimeClass](https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/runtime-class-v1/) 157 | - `rbac.authorization.k8s.io` APIs: [ClusterRole](https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/cluster-role-v1/), [ClusterRoleBinding](https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/cluster-role-binding-v1/), [Role](https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/role-v1/), [RoleBinding](https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/role-binding-v1/) 158 | - `storage.k8s.io` APIs: [CSINode](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/csi-node-v1/), [CSIStorageCapacity](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/csi-storage-capacity-v1beta1/), [StorageClass](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/storage-class-v1/), [VolumeAttachment](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume-attachment-v1/) 159 | - Helm 160 | - [Helm Release](https://helm.sh/docs/intro/using_helm/#three-big-concepts) 161 | - [Helm Storage](https://helm.sh/docs/topics/advanced/#storage-backends) 162 | 163 | ## Installation 164 | 165 | ### Install via [krew](https://krew.sigs.k8s.io/) 166 | 167 | ```shell 168 | $ kubectl krew install lineage 169 | 170 | $ kubectl lineage --version 171 | ``` 172 | 173 | ### Install from Source 174 | 175 | ```shell 176 | $ git clone git@github.com:tohjustin/kube-lineage.git && cd kube-lineage 177 | $ make install 178 | 179 | $ kube-lineage --version 180 | ``` 181 | 182 | ## Prior Art 183 | 184 | kube-lineage has been inspired by the following projects: 185 | 186 | - [ahmetb/kubectl-tree](https://github.com/ahmetb/kubectl-tree) 187 | - [feloy/kubectl-service-tree](https://github.com/feloy/kubectl-service-tree) 188 | - [nimakaviani/knative-inspect](https://github.com/nimakaviani/knative-inspect/) 189 | - [steveteuber/kubectl-graph](https://github.com/steveteuber/kubectl-graph) 190 | -------------------------------------------------------------------------------- /cmd/kube-lineage/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/pflag" 11 | "k8s.io/cli-runtime/pkg/genericclioptions" 12 | 13 | "github.com/tohjustin/kube-lineage/internal/version" 14 | "github.com/tohjustin/kube-lineage/pkg/cmd/helm" 15 | "github.com/tohjustin/kube-lineage/pkg/cmd/lineage" 16 | ) 17 | 18 | var rootCmdName = "kube-lineage" 19 | 20 | //nolint:gochecknoinits 21 | func init() { 22 | // If executed as a kubectl plugin 23 | if strings.HasPrefix(filepath.Base(os.Args[0]), "kubectl-") { 24 | rootCmdName = "kubectl lineage" 25 | } 26 | } 27 | 28 | func NewCmd(streams genericclioptions.IOStreams) *cobra.Command { 29 | cmd := lineage.NewCmd(streams, rootCmdName, "") 30 | cmd.AddCommand(helm.NewCmd(streams, "", rootCmdName)) 31 | cmd.SetVersionTemplate("{{printf \"%s\" .Version}}\n") 32 | cmd.Version = fmt.Sprintf("%#v", version.Get()) 33 | return cmd 34 | } 35 | 36 | func main() { 37 | flags := pflag.NewFlagSet("kube-lineage", pflag.ExitOnError) 38 | pflag.CommandLine = flags 39 | 40 | streams := genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} 41 | rootCmd := NewCmd(streams) 42 | 43 | if err := rootCmd.Execute(); err != nil { 44 | os.Exit(1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/kube-lineage/main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "k8s.io/cli-runtime/pkg/genericclioptions" 11 | 12 | kubelineage "github.com/tohjustin/kube-lineage/cmd/kube-lineage" 13 | "github.com/tohjustin/kube-lineage/internal/version" 14 | ) 15 | 16 | func runCmd(args ...string) (string, error) { 17 | buf := bytes.NewBufferString("") 18 | streams := genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} 19 | cmd := kubelineage.NewCmd(streams) 20 | cmd.SetOut(buf) 21 | 22 | cmd.SetArgs(args) 23 | if err := cmd.Execute(); err != nil { 24 | return "", err 25 | } 26 | out, err := io.ReadAll(buf) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | return string(out), nil 32 | } 33 | 34 | func TestCommandWithVersionFlag(t *testing.T) { 35 | t.Parallel() 36 | 37 | output, err := runCmd("--version") 38 | if err != nil { 39 | t.Fatalf("failed to run command: %v", err) 40 | } 41 | 42 | expected := fmt.Sprintf("%#v\n", version.Get()) 43 | if output != expected { 44 | t.Fatalf("expected \"%s\" got \"%s\"", expected, output) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tohjustin/kube-lineage 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/spf13/cobra v1.3.0 7 | github.com/spf13/pflag v1.0.5 8 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 9 | helm.sh/helm/v3 v3.8.0 10 | k8s.io/api v0.23.4 11 | k8s.io/apimachinery v0.23.4 12 | k8s.io/apiserver v0.23.4 13 | k8s.io/cli-runtime v0.23.4 14 | k8s.io/client-go v0.23.4 15 | k8s.io/klog/v2 v2.30.0 16 | k8s.io/kube-aggregator v0.23.4 17 | k8s.io/kubectl v0.23.4 18 | ) 19 | 20 | require ( 21 | cloud.google.com/go v0.99.0 // indirect 22 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 23 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 24 | github.com/Azure/go-autorest/autorest v0.11.20 // indirect 25 | github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect 26 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 27 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 28 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 29 | github.com/BurntSushi/toml v0.4.1 // indirect 30 | github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect 31 | github.com/Masterminds/goutils v1.1.1 // indirect 32 | github.com/Masterminds/semver/v3 v3.1.1 // indirect 33 | github.com/Masterminds/sprig/v3 v3.2.2 // indirect 34 | github.com/Masterminds/squirrel v1.5.2 // indirect 35 | github.com/PuerkitoBio/purell v1.1.1 // indirect 36 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 37 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect 38 | github.com/beorn7/perks v1.0.1 // indirect 39 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 40 | github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect 41 | github.com/containerd/containerd v1.5.9 // indirect 42 | github.com/cyphar/filepath-securejoin v0.2.3 // indirect 43 | github.com/davecgh/go-spew v1.1.1 // indirect 44 | github.com/docker/cli v20.10.11+incompatible // indirect 45 | github.com/docker/distribution v2.7.1+incompatible // indirect 46 | github.com/docker/docker v20.10.12+incompatible // indirect 47 | github.com/docker/docker-credential-helpers v0.6.4 // indirect 48 | github.com/docker/go-connections v0.4.0 // indirect 49 | github.com/docker/go-metrics v0.0.1 // indirect 50 | github.com/docker/go-units v0.4.0 // indirect 51 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 52 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 53 | github.com/fatih/color v1.13.0 // indirect 54 | github.com/fvbommel/sortorder v1.0.1 // indirect 55 | github.com/go-errors/errors v1.0.1 // indirect 56 | github.com/go-logr/logr v1.2.0 // indirect 57 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 58 | github.com/go-openapi/jsonreference v0.19.5 // indirect 59 | github.com/go-openapi/swag v0.19.14 // indirect 60 | github.com/gobwas/glob v0.2.3 // indirect 61 | github.com/gogo/protobuf v1.3.2 // indirect 62 | github.com/golang-jwt/jwt/v4 v4.0.0 // indirect 63 | github.com/golang/protobuf v1.5.2 // indirect 64 | github.com/google/btree v1.0.1 // indirect 65 | github.com/google/go-cmp v0.5.6 // indirect 66 | github.com/google/gofuzz v1.1.0 // indirect 67 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 68 | github.com/google/uuid v1.2.0 // indirect 69 | github.com/googleapis/gnostic v0.5.5 // indirect 70 | github.com/gorilla/mux v1.8.0 // indirect 71 | github.com/gosuri/uitable v0.0.4 // indirect 72 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 73 | github.com/huandu/xstrings v1.3.2 // indirect 74 | github.com/imdario/mergo v0.3.12 // indirect 75 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 76 | github.com/jmoiron/sqlx v1.3.4 // indirect 77 | github.com/josharian/intern v1.0.0 // indirect 78 | github.com/json-iterator/go v1.1.12 // indirect 79 | github.com/klauspost/compress v1.13.6 // indirect 80 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 81 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 82 | github.com/lib/pq v1.10.4 // indirect 83 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 84 | github.com/mailru/easyjson v0.7.6 // indirect 85 | github.com/mattn/go-colorable v0.1.12 // indirect 86 | github.com/mattn/go-isatty v0.0.14 // indirect 87 | github.com/mattn/go-runewidth v0.0.9 // indirect 88 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 89 | github.com/mitchellh/copystructure v1.2.0 // indirect 90 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 91 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 92 | github.com/moby/locker v1.0.1 // indirect 93 | github.com/moby/spdystream v0.2.0 // indirect 94 | github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect 95 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 96 | github.com/modern-go/reflect2 v1.0.2 // indirect 97 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 98 | github.com/morikuni/aec v1.0.0 // indirect 99 | github.com/opencontainers/go-digest v1.0.0 // indirect 100 | github.com/opencontainers/image-spec v1.0.2 // indirect 101 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 102 | github.com/pkg/errors v0.9.1 // indirect 103 | github.com/pmezard/go-difflib v1.0.0 // indirect 104 | github.com/prometheus/client_golang v1.11.0 // indirect 105 | github.com/prometheus/client_model v0.2.0 // indirect 106 | github.com/prometheus/common v0.28.0 // indirect 107 | github.com/prometheus/procfs v0.6.0 // indirect 108 | github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect 109 | github.com/russross/blackfriday v1.5.2 // indirect 110 | github.com/shopspring/decimal v1.2.0 // indirect 111 | github.com/sirupsen/logrus v1.8.1 // indirect 112 | github.com/spf13/cast v1.4.1 // indirect 113 | github.com/stretchr/testify v1.7.0 // indirect 114 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 115 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 116 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 117 | github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect 118 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect 119 | golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect 120 | golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d // indirect 121 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 122 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 123 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect 124 | golang.org/x/text v0.3.7 // indirect 125 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 126 | google.golang.org/appengine v1.6.7 // indirect 127 | google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect 128 | google.golang.org/grpc v1.43.0 // indirect 129 | google.golang.org/protobuf v1.27.1 // indirect 130 | gopkg.in/gorp.v1 v1.7.2 // indirect 131 | gopkg.in/inf.v0 v0.9.1 // indirect 132 | gopkg.in/yaml.v2 v2.4.0 // indirect 133 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 134 | k8s.io/apiextensions-apiserver v0.23.4 // indirect 135 | k8s.io/component-base v0.23.4 // indirect 136 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect 137 | k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect 138 | oras.land/oras-go v1.1.0 // indirect 139 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect 140 | sigs.k8s.io/kustomize/api v0.10.1 // indirect 141 | sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect 142 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 143 | sigs.k8s.io/yaml v1.3.0 // indirect 144 | ) 145 | -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | 9 | "golang.org/x/sync/errgroup" 10 | apierrors "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/api/meta" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 | metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | "k8s.io/apimachinery/pkg/util/sets" 18 | "k8s.io/cli-runtime/pkg/resource" 19 | "k8s.io/client-go/discovery" 20 | "k8s.io/client-go/dynamic" 21 | _ "k8s.io/client-go/plugin/pkg/client/auth" //nolint:gci 22 | "k8s.io/client-go/rest" 23 | "k8s.io/klog/v2" 24 | ) 25 | 26 | const ( 27 | clientQPS = 300 28 | clientBurst = 400 29 | ) 30 | 31 | type GetOptions struct { 32 | APIResource APIResource 33 | Namespace string 34 | } 35 | 36 | type GetTableOptions struct { 37 | APIResource APIResource 38 | Namespace string 39 | Names []string 40 | } 41 | 42 | type ListOptions struct { 43 | APIResourcesToExclude []APIResource 44 | APIResourcesToInclude []APIResource 45 | Namespaces []string 46 | } 47 | 48 | type Interface interface { 49 | GetMapper() meta.RESTMapper 50 | IsReachable() error 51 | ResolveAPIResource(s string) (*APIResource, error) 52 | 53 | Get(ctx context.Context, name string, opts GetOptions) (*unstructuredv1.Unstructured, error) 54 | GetAPIResources(ctx context.Context) ([]APIResource, error) 55 | GetTable(ctx context.Context, opts GetTableOptions) (*metav1.Table, error) 56 | List(ctx context.Context, opts ListOptions) (*unstructuredv1.UnstructuredList, error) 57 | } 58 | 59 | type client struct { 60 | configFlags *Flags 61 | 62 | discoveryClient discovery.DiscoveryInterface 63 | dynamicClient dynamic.Interface 64 | mapper meta.RESTMapper 65 | } 66 | 67 | func (c *client) GetMapper() meta.RESTMapper { 68 | return c.mapper 69 | } 70 | 71 | // IsReachable tests connectivity to the cluster. 72 | func (c *client) IsReachable() error { 73 | _, err := c.discoveryClient.ServerVersion() 74 | return err 75 | } 76 | 77 | func (c *client) ResolveAPIResource(s string) (*APIResource, error) { 78 | var gvr schema.GroupVersionResource 79 | var gvk schema.GroupVersionKind 80 | var err error 81 | 82 | // Resolve type string into GVR 83 | fullySpecifiedGVR, gr := schema.ParseResourceArg(strings.ToLower(s)) 84 | if fullySpecifiedGVR != nil { 85 | gvr, _ = c.mapper.ResourceFor(*fullySpecifiedGVR) 86 | } 87 | if gvr.Empty() { 88 | gvr, err = c.mapper.ResourceFor(gr.WithVersion("")) 89 | if err != nil { 90 | if len(gr.Group) == 0 { 91 | err = fmt.Errorf("the server doesn't have a resource type \"%s\"", gr.Resource) 92 | } else { 93 | err = fmt.Errorf("the server doesn't have a resource type \"%s\" in group \"%s\"", gr.Resource, gr.Group) 94 | } 95 | return nil, err 96 | } 97 | } 98 | // Obtain Kind from GVR 99 | gvk, err = c.mapper.KindFor(gvr) 100 | if gvk.Empty() { 101 | if err != nil { 102 | if len(gvr.Group) == 0 { 103 | err = fmt.Errorf("the server couldn't identify a kind for resource type \"%s\"", gvr.Resource) 104 | } else { 105 | err = fmt.Errorf("the server couldn't identify a kind for resource type \"%s\" in group \"%s\"", gvr.Resource, gvr.Group) 106 | } 107 | return nil, err 108 | } 109 | } 110 | // Determine scope of resource 111 | mapping, err := c.mapper.RESTMapping(gvk.GroupKind()) 112 | if err != nil { 113 | if len(gvk.Group) == 0 { 114 | err = fmt.Errorf("the server couldn't identify a group kind for resource type \"%s\"", gvk.Kind) 115 | } else { 116 | err = fmt.Errorf("the server couldn't identify a group kind for resource type \"%s\" in group \"%s\"", gvk.Kind, gvk.Group) 117 | } 118 | return nil, err 119 | } 120 | // NOTE: This is a rather incomplete APIResource object, but it has enough 121 | // information inside for our use case, which is to fetch API objects 122 | res := &APIResource{ 123 | Name: gvr.Resource, 124 | Namespaced: mapping.Scope.Name() == meta.RESTScopeNameNamespace, 125 | Group: gvk.Group, 126 | Version: gvk.Version, 127 | Kind: gvk.Kind, 128 | } 129 | 130 | return res, nil 131 | } 132 | 133 | // Get returns an object that matches the provided name & options on the server. 134 | func (c *client) Get(ctx context.Context, name string, opts GetOptions) (*unstructuredv1.Unstructured, error) { 135 | klog.V(4).Infof("Get \"%s\" with options: %+v", name, opts) 136 | gvr := opts.APIResource.GroupVersionResource() 137 | var ri dynamic.ResourceInterface 138 | if opts.APIResource.Namespaced { 139 | ri = c.dynamicClient.Resource(gvr).Namespace(opts.Namespace) 140 | } else { 141 | ri = c.dynamicClient.Resource(gvr) 142 | } 143 | return ri.Get(ctx, name, metav1.GetOptions{}) 144 | } 145 | 146 | // GetTable returns a table output from the server which contains data of the 147 | // list of objects that matches the provided options. This is similar to an API 148 | // request made by `kubectl get TYPE NAME... [-n NAMESPACE]`. 149 | func (c *client) GetTable(ctx context.Context, opts GetTableOptions) (*metav1.Table, error) { 150 | klog.V(4).Infof("GetTable with options: %+v", opts) 151 | gk := opts.APIResource.GroupVersionKind().GroupKind() 152 | r := resource.NewBuilder(c.configFlags). 153 | Unstructured(). 154 | NamespaceParam(opts.Namespace). 155 | ResourceNames(gk.String(), opts.Names...). 156 | ContinueOnError(). 157 | Latest(). 158 | TransformRequests(func(req *rest.Request) { 159 | req.SetHeader("Accept", strings.Join([]string{ 160 | fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), 161 | fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), 162 | "application/json", 163 | }, ",")) 164 | req.Param("includeObject", string(metav1.IncludeMetadata)) 165 | }). 166 | Do() 167 | r.IgnoreErrors(apierrors.IsNotFound) 168 | if err := r.Err(); err != nil { 169 | return nil, err 170 | } 171 | 172 | infos, err := r.Infos() 173 | if err != nil || infos == nil { 174 | return nil, err 175 | } 176 | var table *metav1.Table 177 | for ix := range infos { 178 | t, err := decodeIntoTable(infos[ix].Object) 179 | if err != nil { 180 | return nil, err 181 | } 182 | if table == nil { 183 | table = t 184 | continue 185 | } 186 | table.Rows = append(table.Rows, t.Rows...) 187 | } 188 | return table, nil 189 | } 190 | 191 | func decodeIntoTable(obj runtime.Object) (*metav1.Table, error) { 192 | u, ok := obj.(*unstructuredv1.Unstructured) 193 | if !ok { 194 | return nil, fmt.Errorf("attempt to decode non-Unstructured object") 195 | } 196 | table := &metav1.Table{} 197 | if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, table); err != nil { 198 | return nil, err 199 | } 200 | 201 | for i := range table.Rows { 202 | row := &table.Rows[i] 203 | if row.Object.Raw == nil || row.Object.Object != nil { 204 | continue 205 | } 206 | converted, err := runtime.Decode(unstructuredv1.UnstructuredJSONScheme, row.Object.Raw) 207 | if err != nil { 208 | return nil, err 209 | } 210 | row.Object.Object = converted 211 | } 212 | 213 | return table, nil 214 | } 215 | 216 | // List returns a list of objects that matches the provided options on the 217 | // server. 218 | // 219 | //nolint:funlen,gocognit 220 | func (c *client) List(ctx context.Context, opts ListOptions) (*unstructuredv1.UnstructuredList, error) { 221 | klog.V(4).Infof("List with options: %+v", opts) 222 | apis, err := c.GetAPIResources(ctx) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | // Filter APIs 228 | if len(opts.APIResourcesToInclude) > 0 { 229 | includeGKSet := ResourcesToGroupKindSet(opts.APIResourcesToInclude) 230 | newAPIs := []APIResource{} 231 | for _, api := range apis { 232 | if _, ok := includeGKSet[api.GroupKind()]; ok { 233 | newAPIs = append(newAPIs, api) 234 | } 235 | } 236 | apis = newAPIs 237 | } 238 | if len(opts.APIResourcesToExclude) > 0 { 239 | excludeGKSet := ResourcesToGroupKindSet(opts.APIResourcesToExclude) 240 | newAPIs := []APIResource{} 241 | for _, api := range apis { 242 | if _, ok := excludeGKSet[api.GroupKind()]; !ok { 243 | newAPIs = append(newAPIs, api) 244 | } 245 | } 246 | apis = newAPIs 247 | } 248 | 249 | // Deduplicate list of namespaces & determine the scope for listing objects 250 | isClusterScopeRequest, nsSet := false, make(map[string]struct{}) 251 | if len(opts.Namespaces) == 0 { 252 | isClusterScopeRequest = true 253 | } 254 | for _, ns := range opts.Namespaces { 255 | if ns != "" { 256 | nsSet[ns] = struct{}{} 257 | } else { 258 | isClusterScopeRequest = true 259 | } 260 | } 261 | 262 | var mu sync.Mutex 263 | var items []unstructuredv1.Unstructured 264 | createListFn := func(ctx context.Context, api APIResource, ns string) func() error { 265 | return func() error { 266 | objs, err := c.listByAPI(ctx, api, ns) 267 | if err != nil { 268 | return err 269 | } 270 | mu.Lock() 271 | items = append(items, objs.Items...) 272 | mu.Unlock() 273 | return nil 274 | } 275 | } 276 | eg, ctx := errgroup.WithContext(ctx) 277 | for i := range apis { 278 | api := apis[i] 279 | clusterScopeListFn := func() error { 280 | return createListFn(ctx, api, "")() 281 | } 282 | namespaceScopeListFn := func() error { 283 | egInner, ctxInner := errgroup.WithContext(ctx) 284 | for ns := range nsSet { 285 | listFn := createListFn(ctxInner, api, ns) 286 | egInner.Go(func() error { 287 | err = listFn() 288 | // If no permissions to list the resource at the namespace scope, 289 | // suppress the error to allow other goroutines to continue listing 290 | if apierrors.IsForbidden(err) { 291 | err = nil 292 | } 293 | return err 294 | }) 295 | } 296 | return egInner.Wait() 297 | } 298 | eg.Go(func() error { 299 | var err error 300 | if isClusterScopeRequest { 301 | err = clusterScopeListFn() 302 | // If no permissions to list the cluster-scoped resource, 303 | // suppress the error to allow other goroutines to continue listing 304 | if !api.Namespaced && apierrors.IsForbidden(err) { 305 | err = nil 306 | } 307 | // If no permissions to list the namespaced resource at the cluster 308 | // scope, don't return the error yet & reattempt to list the resource 309 | // in other namespace(s) 310 | if !api.Namespaced || !apierrors.IsForbidden(err) { 311 | return err 312 | } 313 | } 314 | return namespaceScopeListFn() 315 | }) 316 | } 317 | if err := eg.Wait(); err != nil { 318 | return nil, err 319 | } 320 | 321 | klog.V(4).Infof("Got %4d objects from %d API resources", len(items), len(apis)) 322 | return &unstructuredv1.UnstructuredList{Items: items}, nil 323 | } 324 | 325 | // GetAPIResources returns all API resource registered on the server. 326 | func (c *client) GetAPIResources(_ context.Context) ([]APIResource, error) { 327 | rls, err := c.discoveryClient.ServerPreferredResources() 328 | if err != nil { 329 | if discovery.IsGroupDiscoveryFailedError(err) { 330 | klog.V(3).Info("Ignoring invalid resources") 331 | } else { 332 | return nil, err 333 | } 334 | } 335 | 336 | apis := []APIResource{} 337 | for _, rl := range rls { 338 | if len(rl.APIResources) == 0 { 339 | continue 340 | } 341 | gv, err := schema.ParseGroupVersion(rl.GroupVersion) 342 | if err != nil { 343 | klog.V(4).Infof("Ignoring invalid discovered resource %q: %v", rl.GroupVersion, err) 344 | continue 345 | } 346 | for _, r := range rl.APIResources { 347 | // Filter resources that can be watched, listed & get 348 | if len(r.Verbs) == 0 || !sets.NewString(r.Verbs...).HasAll("watch", "list", "get") { 349 | continue 350 | } 351 | api := APIResource{ 352 | Group: gv.Group, 353 | Version: gv.Version, 354 | Kind: r.Kind, 355 | Name: r.Name, 356 | Namespaced: r.Namespaced, 357 | } 358 | // Exclude duplicated resources (for Kubernetes v1.18 & above) 359 | switch { 360 | // migrated to "events.v1.events.k8s.io" 361 | case api.Group == "" && api.Kind == "Event": 362 | klog.V(4).Infof("Exclude duplicated discovered resource: %s", api) 363 | continue 364 | // migrated to "ingresses.v1.networking.k8s.io" 365 | case api.Group == "extensions" && api.Kind == "Ingress": 366 | klog.V(4).Infof("Exclude duplicated discovered resource: %s", api) 367 | continue 368 | } 369 | apis = append(apis, api) 370 | } 371 | } 372 | 373 | klog.V(4).Infof("Discovered %d available API resources to list", len(apis)) 374 | return apis, nil 375 | } 376 | 377 | // listByAPI list all objects of the provided API & namespace. If listing the 378 | // API at the cluster scope, set the namespace argument as an empty string. 379 | func (c *client) listByAPI(ctx context.Context, api APIResource, ns string) (*unstructuredv1.UnstructuredList, error) { 380 | var ri dynamic.ResourceInterface 381 | var items []unstructuredv1.Unstructured 382 | var next string 383 | 384 | isClusterScopeRequest := !api.Namespaced || ns == "" 385 | if isClusterScopeRequest { 386 | ri = c.dynamicClient.Resource(api.GroupVersionResource()) 387 | } else { 388 | ri = c.dynamicClient.Resource(api.GroupVersionResource()).Namespace(ns) 389 | } 390 | for { 391 | objectList, err := ri.List(ctx, metav1.ListOptions{ 392 | Limit: 250, 393 | Continue: next, 394 | }) 395 | if err != nil { 396 | switch { 397 | case apierrors.IsForbidden(err): 398 | if isClusterScopeRequest { 399 | klog.V(4).Infof("No access to list at cluster scope for resource: %s", api) 400 | } else { 401 | klog.V(4).Infof("No access to list in the namespace \"%s\" for resource: %s", ns, api) 402 | } 403 | return nil, err 404 | case apierrors.IsNotFound(err): 405 | break 406 | default: 407 | if isClusterScopeRequest { 408 | err = fmt.Errorf("failed to list resource type \"%s\" in API group \"%s\" at the cluster scope: %w", api.Name, api.Group, err) 409 | } else { 410 | err = fmt.Errorf("failed to list resource type \"%s\" in API group \"%s\" in the namespace \"%s\": %w", api.Name, api.Group, ns, err) 411 | } 412 | return nil, err 413 | } 414 | } 415 | if objectList == nil { 416 | break 417 | } 418 | items = append(items, objectList.Items...) 419 | next = objectList.GetContinue() 420 | if len(next) == 0 { 421 | break 422 | } 423 | } 424 | 425 | if isClusterScopeRequest { 426 | klog.V(4).Infof("Got %4d objects from resource at the cluster scope: %s", len(items), api) 427 | } else { 428 | klog.V(4).Infof("Got %4d objects from resource in the namespace \"%s\": %s", len(items), ns, api) 429 | } 430 | return &unstructuredv1.UnstructuredList{Items: items}, nil 431 | } 432 | -------------------------------------------------------------------------------- /internal/client/flags.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/pflag" 6 | "k8s.io/cli-runtime/pkg/genericclioptions" 7 | "k8s.io/client-go/dynamic" 8 | "k8s.io/client-go/rest" 9 | "k8s.io/kubectl/pkg/cmd/get" 10 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 11 | "k8s.io/kubectl/pkg/util" 12 | ) 13 | 14 | // Flags composes common client configuration flag structs used in the command. 15 | type Flags struct { 16 | *genericclioptions.ConfigFlags 17 | } 18 | 19 | // Copy returns a copy of Flags for mutation. 20 | func (f *Flags) Copy() Flags { 21 | Flags := *f 22 | return Flags 23 | } 24 | 25 | // AddFlags receives a pflag.FlagSet reference and binds flags related to client 26 | // configuration to it. 27 | func (f *Flags) AddFlags(flags *pflag.FlagSet) { 28 | f.ConfigFlags.AddFlags(flags) 29 | } 30 | 31 | // RegisterFlagCompletionFunc receives a *cobra.Command & register functions to 32 | // to provide completion for flags related to client configuration. 33 | // 34 | // Based off `registerCompletionFuncForGlobalFlags` from 35 | // https://github.com/kubernetes/kubectl/blob/v0.22.1/pkg/cmd/cmd.go#L439-L460 36 | func (*Flags) RegisterFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { 37 | cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 38 | "namespace", 39 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 40 | return get.CompGetResource(f, cmd, "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp 41 | })) 42 | cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 43 | "context", 44 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 45 | return util.ListContextsInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp 46 | })) 47 | cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 48 | "cluster", 49 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 50 | return util.ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp 51 | })) 52 | cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 53 | "user", 54 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 55 | return util.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp 56 | })) 57 | } 58 | 59 | // ToClient returns a client based on the flag configuration. 60 | func (f *Flags) ToClient() (Interface, error) { 61 | config, err := f.ToRESTConfig() 62 | if err != nil { 63 | return nil, err 64 | } 65 | config.WarningHandler = rest.NoWarnings{} 66 | config.QPS = clientQPS 67 | config.Burst = clientBurst 68 | f.WithDiscoveryBurst(clientBurst) 69 | 70 | dyn, err := dynamic.NewForConfig(config) 71 | if err != nil { 72 | return nil, err 73 | } 74 | dis, err := f.ToDiscoveryClient() 75 | if err != nil { 76 | return nil, err 77 | } 78 | mapper, err := f.ToRESTMapper() 79 | if err != nil { 80 | return nil, err 81 | } 82 | c := &client{ 83 | configFlags: f, 84 | discoveryClient: dis, 85 | dynamicClient: dyn, 86 | mapper: mapper, 87 | } 88 | 89 | return c, nil 90 | } 91 | 92 | // NewFlags returns flags associated with client configuration, with default 93 | // values set. 94 | func NewFlags() *Flags { 95 | return &Flags{ 96 | ConfigFlags: genericclioptions.NewConfigFlags(true), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/client/resource.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | // APIResource represents a Kubernetes API resource. 11 | type APIResource metav1.APIResource 12 | 13 | func (r APIResource) GroupKind() schema.GroupKind { 14 | return schema.GroupKind{ 15 | Group: r.Group, 16 | Kind: r.Kind, 17 | } 18 | } 19 | 20 | func (r APIResource) GroupVersionKind() schema.GroupVersionKind { 21 | return schema.GroupVersionKind{ 22 | Group: r.Group, 23 | Version: r.Version, 24 | Kind: r.Kind, 25 | } 26 | } 27 | 28 | func (r APIResource) GroupVersionResource() schema.GroupVersionResource { 29 | return schema.GroupVersionResource{ 30 | Group: r.Group, 31 | Version: r.Version, 32 | Resource: r.Name, 33 | } 34 | } 35 | 36 | func (r APIResource) String() string { 37 | if len(r.Group) == 0 { 38 | return fmt.Sprintf("%s.%s", r.Name, r.Version) 39 | } 40 | return fmt.Sprintf("%s.%s.%s", r.Name, r.Version, r.Group) 41 | } 42 | 43 | func (r APIResource) WithGroupString() string { 44 | if len(r.Group) == 0 { 45 | return r.Name 46 | } 47 | return r.Name + "." + r.Group 48 | } 49 | 50 | func ResourcesToGroupKindSet(apis []APIResource) map[schema.GroupKind]struct{} { 51 | gkSet := map[schema.GroupKind]struct{}{} 52 | for _, api := range apis { 53 | gk := api.GroupKind() 54 | // Account for resources that migrated API groups (for Kubernetes v1.18 & above) 55 | switch { 56 | // migrated from "events.v1" to "events.v1.events.k8s.io" 57 | case gk.Kind == "Event" && (gk.Group == "" || gk.Group == "events.k8s.io"): 58 | gkSet[schema.GroupKind{Kind: gk.Kind, Group: ""}] = struct{}{} 59 | gkSet[schema.GroupKind{Kind: gk.Kind, Group: "events.k8s.io"}] = struct{}{} 60 | // migrated from "ingresses.v1.extensions" to "ingresses.v1.networking.k8s.io" 61 | case gk.Kind == "Ingress" && (gk.Group == "extensions" || gk.Group == "networking.k8s.io"): 62 | gkSet[schema.GroupKind{Kind: gk.Kind, Group: "extensions"}] = struct{}{} 63 | gkSet[schema.GroupKind{Kind: gk.Kind, Group: "networking.k8s.io"}] = struct{}{} 64 | default: 65 | gkSet[gk] = struct{}{} 66 | } 67 | } 68 | return gkSet 69 | } 70 | 71 | // ObjectMeta contains the metadata for identifying a Kubernetes object. 72 | type ObjectMeta struct { 73 | APIResource 74 | Name string 75 | Namespace string 76 | } 77 | 78 | func (o ObjectMeta) String() string { 79 | return fmt.Sprintf("%s/%s", o.APIResource, o.Name) 80 | } 81 | -------------------------------------------------------------------------------- /internal/completion/completion.go: -------------------------------------------------------------------------------- 1 | package completion 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | "k8s.io/kubectl/pkg/cmd/get" 9 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 10 | ) 11 | 12 | // filterString returns all strings from 's', except those with names matching 13 | // 'ignored'. 14 | func filterString(s []string, ignored []string) []string { 15 | if ignored == nil { 16 | return s 17 | } 18 | var filteredStrList []string 19 | for _, str := range s { 20 | found := false 21 | for _, ignoredName := range ignored { 22 | if str == ignoredName { 23 | found = true 24 | break 25 | } 26 | } 27 | if !found { 28 | filteredStrList = append(filteredStrList, str) 29 | } 30 | } 31 | return filteredStrList 32 | } 33 | 34 | // GetScopeNamespaceList provides dynamic auto-completion for scope namespaces. 35 | func GetScopeNamespaceList(f cmdutil.Factory, cmd *cobra.Command, toComplete string) []string { 36 | var comp []string 37 | 38 | allNS := get.CompGetResource(f, cmd, "namespace", "") 39 | existingNS := strings.Split(toComplete, ",") 40 | existingNS = existingNS[:len(existingNS)-1] 41 | ignoreNS := existingNS 42 | if ns, _, err := f.ToRawKubeConfigLoader().Namespace(); err == nil { 43 | ignoreNS = append(ignoreNS, ns) 44 | } 45 | filteredNS := filterString(allNS, ignoreNS) 46 | 47 | compPrefix := strings.Join(existingNS, ",") 48 | for _, ns := range filteredNS { 49 | if len(compPrefix) > 0 { 50 | ns = fmt.Sprintf("%s,%s", compPrefix, ns) 51 | } 52 | comp = append(comp, ns) 53 | } 54 | 55 | return comp 56 | } 57 | -------------------------------------------------------------------------------- /internal/graph/graph.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | eventsv1 "k8s.io/api/events/v1" 10 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 11 | networkingv1 "k8s.io/api/networking/v1" 12 | nodev1 "k8s.io/api/node/v1" 13 | policyv1 "k8s.io/api/policy/v1" 14 | policyv1beta1 "k8s.io/api/policy/v1beta1" 15 | rbacv1 "k8s.io/api/rbac/v1" 16 | storagev1 "k8s.io/api/storage/v1" 17 | storagev1beta1 "k8s.io/api/storage/v1beta1" 18 | "k8s.io/apimachinery/pkg/api/meta" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 | "k8s.io/apimachinery/pkg/labels" 22 | "k8s.io/apimachinery/pkg/types" 23 | "k8s.io/apimachinery/pkg/util/sets" 24 | "k8s.io/klog/v2" 25 | apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" 26 | ) 27 | 28 | // ObjectLabelSelectorKey is a compact representation of an ObjectLabelSelector. 29 | // Typically used as key types for maps. 30 | type ObjectLabelSelectorKey string 31 | 32 | // ObjectLabelSelector is a reference to a collection of Kubernetes objects. 33 | type ObjectLabelSelector struct { 34 | Group string 35 | Kind string 36 | Namespace string 37 | Selector labels.Selector 38 | } 39 | 40 | // Key converts the ObjectLabelSelector into a ObjectLabelSelectorKey. 41 | func (o *ObjectLabelSelector) Key() ObjectLabelSelectorKey { 42 | k := fmt.Sprintf("%s\\%s\\%s\\%s", o.Group, o.Kind, o.Namespace, o.Selector) 43 | return ObjectLabelSelectorKey(k) 44 | } 45 | 46 | // ObjectSelectorKey is a compact representation of an ObjectSelector. 47 | // Typically used as key types for maps. 48 | type ObjectSelectorKey string 49 | 50 | // ObjectSelector is a reference to a collection of Kubernetes objects. 51 | type ObjectSelector struct { 52 | Group string 53 | Kind string 54 | Namespaces sets.String 55 | } 56 | 57 | // Key converts the ObjectSelector into a ObjectSelectorKey. 58 | func (o *ObjectSelector) Key() ObjectSelectorKey { 59 | k := fmt.Sprintf("%s\\%s\\%s", o.Group, o.Kind, o.Namespaces) 60 | return ObjectSelectorKey(k) 61 | } 62 | 63 | // ObjectReferenceKey is a compact representation of an ObjectReference. 64 | // Typically used as key types for maps. 65 | type ObjectReferenceKey string 66 | 67 | // ObjectReference is a reference to a Kubernetes object. 68 | type ObjectReference struct { 69 | Group string 70 | Kind string 71 | Namespace string 72 | Name string 73 | } 74 | 75 | // Key converts the ObjectReference into a ObjectReferenceKey. 76 | func (o *ObjectReference) Key() ObjectReferenceKey { 77 | k := fmt.Sprintf("%s\\%s\\%s\\%s", o.Group, o.Kind, o.Namespace, o.Name) 78 | return ObjectReferenceKey(k) 79 | } 80 | 81 | type sortableStringSlice []string 82 | 83 | func (s sortableStringSlice) Len() int { return len(s) } 84 | func (s sortableStringSlice) Less(i, j int) bool { return s[i] < s[j] } 85 | func (s sortableStringSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 86 | 87 | // Relationship represents a relationship type between two Kubernetes objects. 88 | type Relationship string 89 | 90 | // RelationshipSet contains a set of relationships. 91 | type RelationshipSet map[Relationship]struct{} 92 | 93 | // List returns the contents as a sorted string slice. 94 | func (s RelationshipSet) List() []string { 95 | res := make(sortableStringSlice, 0, len(s)) 96 | for key := range s { 97 | res = append(res, string(key)) 98 | } 99 | sort.Sort(res) 100 | return []string(res) 101 | } 102 | 103 | // RelationshipMap contains a map of relationships a Kubernetes object has with 104 | // other objects in the cluster. 105 | type RelationshipMap struct { 106 | DependenciesByLabelSelector map[ObjectLabelSelectorKey]RelationshipSet 107 | DependenciesByRef map[ObjectReferenceKey]RelationshipSet 108 | DependenciesBySelector map[ObjectSelectorKey]RelationshipSet 109 | DependenciesByUID map[types.UID]RelationshipSet 110 | DependentsByLabelSelector map[ObjectLabelSelectorKey]RelationshipSet 111 | DependentsByRef map[ObjectReferenceKey]RelationshipSet 112 | DependentsBySelector map[ObjectSelectorKey]RelationshipSet 113 | DependentsByUID map[types.UID]RelationshipSet 114 | ObjectLabelSelectors map[ObjectLabelSelectorKey]ObjectLabelSelector 115 | ObjectSelectors map[ObjectSelectorKey]ObjectSelector 116 | } 117 | 118 | func newRelationshipMap() RelationshipMap { 119 | return RelationshipMap{ 120 | DependenciesByLabelSelector: map[ObjectLabelSelectorKey]RelationshipSet{}, 121 | DependenciesByRef: map[ObjectReferenceKey]RelationshipSet{}, 122 | DependenciesBySelector: map[ObjectSelectorKey]RelationshipSet{}, 123 | DependenciesByUID: map[types.UID]RelationshipSet{}, 124 | DependentsByLabelSelector: map[ObjectLabelSelectorKey]RelationshipSet{}, 125 | DependentsByRef: map[ObjectReferenceKey]RelationshipSet{}, 126 | DependentsBySelector: map[ObjectSelectorKey]RelationshipSet{}, 127 | DependentsByUID: map[types.UID]RelationshipSet{}, 128 | ObjectLabelSelectors: map[ObjectLabelSelectorKey]ObjectLabelSelector{}, 129 | ObjectSelectors: map[ObjectSelectorKey]ObjectSelector{}, 130 | } 131 | } 132 | 133 | func (m *RelationshipMap) AddDependencyByKey(k ObjectReferenceKey, r Relationship) { 134 | if _, ok := m.DependenciesByRef[k]; !ok { 135 | m.DependenciesByRef[k] = RelationshipSet{} 136 | } 137 | m.DependenciesByRef[k][r] = struct{}{} 138 | } 139 | 140 | func (m *RelationshipMap) AddDependencyByLabelSelector(o ObjectLabelSelector, r Relationship) { 141 | k := o.Key() 142 | if _, ok := m.DependenciesByLabelSelector[k]; !ok { 143 | m.DependenciesByLabelSelector[k] = RelationshipSet{} 144 | } 145 | m.DependenciesByLabelSelector[k][r] = struct{}{} 146 | m.ObjectLabelSelectors[k] = o 147 | } 148 | 149 | func (m *RelationshipMap) AddDependencyBySelector(o ObjectSelector, r Relationship) { 150 | k := o.Key() 151 | if _, ok := m.DependenciesBySelector[k]; !ok { 152 | m.DependenciesBySelector[k] = RelationshipSet{} 153 | } 154 | m.DependenciesBySelector[k][r] = struct{}{} 155 | m.ObjectSelectors[k] = o 156 | } 157 | 158 | func (m *RelationshipMap) AddDependencyByUID(uid types.UID, r Relationship) { 159 | if _, ok := m.DependenciesByUID[uid]; !ok { 160 | m.DependenciesByUID[uid] = RelationshipSet{} 161 | } 162 | m.DependenciesByUID[uid][r] = struct{}{} 163 | } 164 | 165 | func (m *RelationshipMap) AddDependentByKey(k ObjectReferenceKey, r Relationship) { 166 | if _, ok := m.DependentsByRef[k]; !ok { 167 | m.DependentsByRef[k] = RelationshipSet{} 168 | } 169 | m.DependentsByRef[k][r] = struct{}{} 170 | } 171 | 172 | func (m *RelationshipMap) AddDependentByLabelSelector(o ObjectLabelSelector, r Relationship) { 173 | k := o.Key() 174 | if _, ok := m.DependentsByLabelSelector[k]; !ok { 175 | m.DependentsByLabelSelector[k] = RelationshipSet{} 176 | } 177 | m.DependentsByLabelSelector[k][r] = struct{}{} 178 | m.ObjectLabelSelectors[k] = o 179 | } 180 | 181 | func (m *RelationshipMap) AddDependentBySelector(o ObjectSelector, r Relationship) { 182 | k := o.Key() 183 | if _, ok := m.DependentsBySelector[k]; !ok { 184 | m.DependentsBySelector[k] = RelationshipSet{} 185 | } 186 | m.DependentsBySelector[k][r] = struct{}{} 187 | m.ObjectSelectors[k] = o 188 | } 189 | 190 | func (m *RelationshipMap) AddDependentByUID(uid types.UID, r Relationship) { 191 | if _, ok := m.DependentsByUID[uid]; !ok { 192 | m.DependentsByUID[uid] = RelationshipSet{} 193 | } 194 | m.DependentsByUID[uid][r] = struct{}{} 195 | } 196 | 197 | // Node represents a Kubernetes object in an relationship tree. 198 | type Node struct { 199 | *unstructuredv1.Unstructured 200 | UID types.UID 201 | Group string 202 | Version string 203 | Kind string 204 | Resource string 205 | Namespaced bool 206 | Namespace string 207 | Name string 208 | OwnerReferences []metav1.OwnerReference 209 | Dependencies map[types.UID]RelationshipSet 210 | Dependents map[types.UID]RelationshipSet 211 | Depth uint 212 | } 213 | 214 | func (n *Node) AddDependency(uid types.UID, r Relationship) { 215 | if _, ok := n.Dependencies[uid]; !ok { 216 | n.Dependencies[uid] = RelationshipSet{} 217 | } 218 | n.Dependencies[uid][r] = struct{}{} 219 | } 220 | 221 | func (n *Node) AddDependent(uid types.UID, r Relationship) { 222 | if _, ok := n.Dependents[uid]; !ok { 223 | n.Dependents[uid] = RelationshipSet{} 224 | } 225 | n.Dependents[uid][r] = struct{}{} 226 | } 227 | 228 | func (n *Node) GetDeps(depsIsDependencies bool) map[types.UID]RelationshipSet { 229 | if depsIsDependencies { 230 | return n.Dependencies 231 | } 232 | return n.Dependents 233 | } 234 | 235 | func (n *Node) GetObjectReferenceKey() ObjectReferenceKey { 236 | ref := ObjectReference{ 237 | Group: n.Group, 238 | Kind: n.Kind, 239 | Name: n.Name, 240 | Namespace: n.Namespace, 241 | } 242 | return ref.Key() 243 | } 244 | 245 | func (n *Node) GetNestedString(fields ...string) string { 246 | val, found, err := unstructuredv1.NestedString(n.UnstructuredContent(), fields...) 247 | if !found || err != nil { 248 | return "" 249 | } 250 | return val 251 | } 252 | 253 | func (n *Node) GetAPIResource() metav1.APIResource { 254 | // NOTE: This is a rather incomplete APIResource object, but it has enough 255 | // information inside for our use case, which is to fetch API objects 256 | return metav1.APIResource{ 257 | Group: n.Group, 258 | Version: n.Version, 259 | Kind: n.Kind, 260 | Name: n.Resource, 261 | Namespaced: n.Namespaced, 262 | } 263 | } 264 | 265 | // NodeList contains a list of nodes. 266 | type NodeList []*Node 267 | 268 | func (n NodeList) Len() int { 269 | return len(n) 270 | } 271 | 272 | func (n NodeList) Less(i, j int) bool { 273 | // Sort nodes in following order: Namespace, Kind, Group, Name 274 | a, b := n[i], n[j] 275 | if a.Namespace != b.Namespace { 276 | return a.Namespace < b.Namespace 277 | } 278 | if a.Kind != b.Kind { 279 | return a.Kind < b.Kind 280 | } 281 | if a.Group != b.Group { 282 | return a.Group < b.Group 283 | } 284 | return a.Name < b.Name 285 | } 286 | 287 | func (n NodeList) Swap(i, j int) { 288 | n[i], n[j] = n[j], n[i] 289 | } 290 | 291 | // NodeMap contains a relationship tree stored as a map of nodes. 292 | type NodeMap map[types.UID]*Node 293 | 294 | // ResolveDependencies resolves all dependencies of the provided objects and 295 | // returns a relationship tree. 296 | func ResolveDependencies(m meta.RESTMapper, objects []unstructuredv1.Unstructured, uids []types.UID) (NodeMap, error) { 297 | return resolveDeps(m, objects, uids, true) 298 | } 299 | 300 | // ResolveDependents resolves all dependents of the provided objects and returns 301 | // a relationship tree. 302 | func ResolveDependents(m meta.RESTMapper, objects []unstructuredv1.Unstructured, uids []types.UID) (NodeMap, error) { 303 | return resolveDeps(m, objects, uids, false) 304 | } 305 | 306 | // resolveDeps resolves all dependencies or dependents of the provided objects 307 | // and returns a relationship tree. 308 | //nolint:funlen,gocognit,gocyclo 309 | func resolveDeps(m meta.RESTMapper, objects []unstructuredv1.Unstructured, uids []types.UID, depsIsDependencies bool) (NodeMap, error) { 310 | if len(uids) == 0 { 311 | return NodeMap{}, nil 312 | } 313 | // Create global node maps of all objects, one mapped by node UIDs & the other 314 | // mapped by node keys. This step also helps deduplicate the list of provided 315 | // objects 316 | globalMapByUID := map[types.UID]*Node{} 317 | globalMapByKey := map[ObjectReferenceKey]*Node{} 318 | for ix, o := range objects { 319 | gvk := o.GroupVersionKind() 320 | m, err := m.RESTMapping(gvk.GroupKind(), gvk.Version) 321 | if err != nil { 322 | klog.V(4).Infof("Failed to map resource \"%s\" to GVR", gvk) 323 | return nil, err 324 | } 325 | ns := o.GetNamespace() 326 | node := Node{ 327 | Unstructured: &objects[ix], 328 | UID: o.GetUID(), 329 | Name: o.GetName(), 330 | Namespace: ns, 331 | Namespaced: ns != "", 332 | Group: m.Resource.Group, 333 | Version: m.Resource.Version, 334 | Kind: m.GroupVersionKind.Kind, 335 | Resource: m.Resource.Resource, 336 | OwnerReferences: o.GetOwnerReferences(), 337 | Dependencies: map[types.UID]RelationshipSet{}, 338 | Dependents: map[types.UID]RelationshipSet{}, 339 | } 340 | uid, key := node.UID, node.GetObjectReferenceKey() 341 | if n, ok := globalMapByUID[uid]; ok { 342 | klog.V(4).Infof("Duplicated %s.%s resource \"%s\" in namespace \"%s\"", n.Kind, n.Group, n.Name, n.Namespace) 343 | } 344 | globalMapByUID[uid] = &node 345 | globalMapByKey[key] = &node 346 | 347 | if node.Group == corev1.GroupName && node.Kind == "Node" { 348 | // Node events sent by the Kubelet uses the node's name as the 349 | // ObjectReference UID, so we include them as keys in our global map to 350 | // support lookup by nodename 351 | globalMapByUID[types.UID(node.Name)] = &node 352 | // Node events sent by the kube-proxy uses the node's hostname as the 353 | // ObjectReference UID, so we include them as keys in our global map to 354 | // support lookup by hostname 355 | if hostname, ok := o.GetLabels()[corev1.LabelHostname]; ok { 356 | globalMapByUID[types.UID(hostname)] = &node 357 | } 358 | } 359 | } 360 | 361 | resolveLabelSelectorToNodes := func(o ObjectLabelSelector) []*Node { 362 | var result []*Node 363 | for _, n := range globalMapByUID { 364 | if n.Group == o.Group && n.Kind == o.Kind && n.Namespace == o.Namespace { 365 | if ok := o.Selector.Matches(labels.Set(n.GetLabels())); ok { 366 | result = append(result, n) 367 | } 368 | } 369 | } 370 | return result 371 | } 372 | resolveSelectorToNodes := func(o ObjectSelector) []*Node { 373 | var result []*Node 374 | for _, n := range globalMapByUID { 375 | if n.Group == o.Group && n.Kind == o.Kind { 376 | if len(o.Namespaces) == 0 || o.Namespaces.Has(n.Namespace) { 377 | result = append(result, n) 378 | } 379 | } 380 | } 381 | return result 382 | } 383 | updateRelationships := func(node *Node, rmap *RelationshipMap) { 384 | for k, rset := range rmap.DependenciesByRef { 385 | if n, ok := globalMapByKey[k]; ok { 386 | for r := range rset { 387 | node.AddDependency(n.UID, r) 388 | n.AddDependent(node.UID, r) 389 | } 390 | } 391 | } 392 | for k, rset := range rmap.DependentsByRef { 393 | if n, ok := globalMapByKey[k]; ok { 394 | for r := range rset { 395 | n.AddDependency(node.UID, r) 396 | node.AddDependent(n.UID, r) 397 | } 398 | } 399 | } 400 | for k, rset := range rmap.DependenciesByLabelSelector { 401 | if ols, ok := rmap.ObjectLabelSelectors[k]; ok { 402 | for _, n := range resolveLabelSelectorToNodes(ols) { 403 | for r := range rset { 404 | node.AddDependency(n.UID, r) 405 | n.AddDependent(node.UID, r) 406 | } 407 | } 408 | } 409 | } 410 | for k, rset := range rmap.DependentsByLabelSelector { 411 | if ols, ok := rmap.ObjectLabelSelectors[k]; ok { 412 | for _, n := range resolveLabelSelectorToNodes(ols) { 413 | for r := range rset { 414 | n.AddDependency(node.UID, r) 415 | node.AddDependent(n.UID, r) 416 | } 417 | } 418 | } 419 | } 420 | for k, rset := range rmap.DependenciesBySelector { 421 | if os, ok := rmap.ObjectSelectors[k]; ok { 422 | for _, n := range resolveSelectorToNodes(os) { 423 | for r := range rset { 424 | node.AddDependency(n.UID, r) 425 | n.AddDependent(node.UID, r) 426 | } 427 | } 428 | } 429 | } 430 | for k, rset := range rmap.DependentsBySelector { 431 | if os, ok := rmap.ObjectSelectors[k]; ok { 432 | for _, n := range resolveSelectorToNodes(os) { 433 | for r := range rset { 434 | n.AddDependency(node.UID, r) 435 | node.AddDependent(n.UID, r) 436 | } 437 | } 438 | } 439 | } 440 | for uid, rset := range rmap.DependenciesByUID { 441 | if n, ok := globalMapByUID[uid]; ok { 442 | for r := range rset { 443 | node.AddDependency(n.UID, r) 444 | n.AddDependent(node.UID, r) 445 | } 446 | } 447 | } 448 | for uid, rset := range rmap.DependentsByUID { 449 | if n, ok := globalMapByUID[uid]; ok { 450 | for r := range rset { 451 | n.AddDependency(node.UID, r) 452 | node.AddDependent(n.UID, r) 453 | } 454 | } 455 | } 456 | } 457 | 458 | // Populate dependencies & dependents based on Owner-Dependent relationships 459 | for _, node := range globalMapByUID { 460 | for _, ref := range node.OwnerReferences { 461 | if n, ok := globalMapByUID[ref.UID]; ok { 462 | if ref.Controller != nil && *ref.Controller { 463 | node.AddDependency(n.UID, RelationshipControllerRef) 464 | n.AddDependent(node.UID, RelationshipControllerRef) 465 | } 466 | node.AddDependency(n.UID, RelationshipOwnerRef) 467 | n.AddDependent(node.UID, RelationshipOwnerRef) 468 | } 469 | } 470 | } 471 | 472 | var rmap *RelationshipMap 473 | var err error 474 | for _, node := range globalMapByUID { 475 | switch { 476 | // Populate dependencies & dependents based on PersistentVolume relationships 477 | case node.Group == corev1.GroupName && node.Kind == "PersistentVolume": 478 | rmap, err = getPersistentVolumeRelationships(node) 479 | if err != nil { 480 | klog.V(4).Infof("Failed to get relationships for persistentvolume named \"%s\": %s", node.Name, err) 481 | continue 482 | } 483 | // Populate dependencies & dependents based on PersistentVolumeClaim relationships 484 | case node.Group == corev1.GroupName && node.Kind == "PersistentVolumeClaim": 485 | rmap, err = getPersistentVolumeClaimRelationships(node) 486 | if err != nil { 487 | klog.V(4).Infof("Failed to get relationships for persistentvolumeclaim named \"%s\" in namespace \"%s\": %s", node.Name, node.Namespace, err) 488 | continue 489 | } 490 | // Populate dependencies & dependents based on Pod relationships 491 | case node.Group == corev1.GroupName && node.Kind == "Pod": 492 | rmap, err = getPodRelationships(node) 493 | if err != nil { 494 | klog.V(4).Infof("Failed to get relationships for pod named \"%s\" in namespace \"%s\": %s", node.Name, node.Namespace, err) 495 | continue 496 | } 497 | // Populate dependencies & dependents based on Service relationships 498 | case node.Group == corev1.GroupName && node.Kind == "Service": 499 | rmap, err = getServiceRelationships(node) 500 | if err != nil { 501 | klog.V(4).Infof("Failed to get relationships for service named \"%s\" in namespace \"%s\": %s", node.Name, node.Namespace, err) 502 | continue 503 | } 504 | // Populate dependencies & dependents based on ServiceAccount relationships 505 | case node.Group == corev1.GroupName && node.Kind == "ServiceAccount": 506 | rmap, err = getServiceAccountRelationships(node) 507 | if err != nil { 508 | klog.V(4).Infof("Failed to get relationships for serviceaccount named \"%s\" in namespace \"%s\": %s", node.Name, node.Namespace, err) 509 | continue 510 | } 511 | // Populate dependencies & dependents based on PodSecurityPolicy relationships 512 | case node.Group == policyv1beta1.GroupName && node.Kind == "PodSecurityPolicy": 513 | rmap, err = getPodSecurityPolicyRelationships(node) 514 | if err != nil { 515 | klog.V(4).Infof("Failed to get relationships for podsecuritypolicy named \"%s\": %s", node.Name, err) 516 | continue 517 | } 518 | // Populate dependencies & dependents based on PodDisruptionBudget relationships 519 | case node.Group == policyv1.GroupName && node.Kind == "PodDisruptionBudget": 520 | rmap, err = getPodDisruptionBudgetRelationships(node) 521 | if err != nil { 522 | klog.V(4).Infof("Failed to get relationships for poddisruptionbudget named \"%s\": %s", node.Name, err) 523 | continue 524 | } 525 | // Populate dependencies & dependents based on MutatingWebhookConfiguration relationships 526 | case node.Group == admissionregistrationv1.GroupName && node.Kind == "MutatingWebhookConfiguration": 527 | rmap, err = getMutatingWebhookConfigurationRelationships(node) 528 | if err != nil { 529 | klog.V(4).Infof("Failed to get relationships for mutatingwebhookconfiguration named \"%s\": %s", node.Name, err) 530 | continue 531 | } 532 | // Populate dependencies & dependents based on ValidatingWebhookConfiguration relationships 533 | case node.Group == admissionregistrationv1.GroupName && node.Kind == "ValidatingWebhookConfiguration": 534 | rmap, err = getValidatingWebhookConfigurationRelationships(node) 535 | if err != nil { 536 | klog.V(4).Infof("Failed to get relationships for validatingwebhookconfiguration named \"%s\": %s", node.Name, err) 537 | continue 538 | } 539 | // Populate dependencies & dependents based on APIService relationships 540 | case node.Group == apiregistrationv1.GroupName && node.Kind == "APIService": 541 | rmap, err = getAPIServiceRelationships(node) 542 | if err != nil { 543 | klog.V(4).Infof("Failed to get relationships for apiservice named \"%s\": %s", node.Name, err) 544 | continue 545 | } 546 | // Populate dependencies & dependents based on Event relationships 547 | case (node.Group == eventsv1.GroupName || node.Group == corev1.GroupName) && node.Kind == "Event": 548 | rmap, err = getEventRelationships(node) 549 | if err != nil { 550 | klog.V(4).Infof("Failed to get relationships for event named \"%s\" in namespace \"%s\": %s", node.Name, node.Namespace, err) 551 | continue 552 | } 553 | // Populate dependencies & dependents based on Ingress relationships 554 | case (node.Group == networkingv1.GroupName || node.Group == extensionsv1beta1.GroupName) && node.Kind == "Ingress": 555 | rmap, err = getIngressRelationships(node) 556 | if err != nil { 557 | klog.V(4).Infof("Failed to get relationships for ingress named \"%s\" in namespace \"%s\": %s", node.Name, node.Namespace, err) 558 | continue 559 | } 560 | // Populate dependencies & dependents based on IngressClass relationships 561 | case node.Group == networkingv1.GroupName && node.Kind == "IngressClass": 562 | rmap, err = getIngressClassRelationships(node) 563 | if err != nil { 564 | klog.V(4).Infof("Failed to get relationships for ingressclass named \"%s\": %s", node.Name, err) 565 | continue 566 | } 567 | // Populate dependencies & dependents based on NetworkPolicy relationships 568 | case node.Group == networkingv1.GroupName && node.Kind == "NetworkPolicy": 569 | rmap, err = getNetworkPolicyRelationships(node) 570 | if err != nil { 571 | klog.V(4).Infof("Failed to get relationships for networkpolicy named \"%s\": %s", node.Name, err) 572 | continue 573 | } 574 | // Populate dependencies & dependents based on RuntimeClass relationships 575 | case node.Group == nodev1.GroupName && node.Kind == "RuntimeClass": 576 | rmap, err = getRuntimeClassRelationships(node) 577 | if err != nil { 578 | klog.V(4).Infof("Failed to get relationships for runtimeclass named \"%s\": %s", node.Name, err) 579 | continue 580 | } 581 | // Populate dependencies & dependents based on ClusterRole relationships 582 | case node.Group == rbacv1.GroupName && node.Kind == "ClusterRole": 583 | rmap, err = getClusterRoleRelationships(node) 584 | if err != nil { 585 | klog.V(4).Infof("Failed to get relationships for clusterrole named \"%s\": %s", node.Name, err) 586 | continue 587 | } 588 | // Populate dependencies & dependents based on ClusterRoleBinding relationships 589 | case node.Group == rbacv1.GroupName && node.Kind == "ClusterRoleBinding": 590 | rmap, err = getClusterRoleBindingRelationships(node) 591 | if err != nil { 592 | klog.V(4).Infof("Failed to get relationships for clusterrolebinding named \"%s\": %s", node.Name, err) 593 | continue 594 | } 595 | // Populate dependencies & dependents based on Role relationships 596 | case node.Group == rbacv1.GroupName && node.Kind == "Role": 597 | rmap, err = getRoleRelationships(node) 598 | if err != nil { 599 | klog.V(4).Infof("Failed to get relationships for role named \"%s\" in namespace \"%s\": %s: %s", node.Name, node.Namespace, err) 600 | continue 601 | } 602 | // Populate dependencies & dependents based on RoleBinding relationships 603 | case node.Group == rbacv1.GroupName && node.Kind == "RoleBinding": 604 | rmap, err = getRoleBindingRelationships(node) 605 | if err != nil { 606 | klog.V(4).Infof("Failed to get relationships for rolebinding named \"%s\" in namespace \"%s\": %s: %s", node.Name, node.Namespace, err) 607 | continue 608 | } 609 | // Populate dependencies & dependents based on CSIStorageCapacity relationships 610 | case node.Group == storagev1beta1.GroupName && node.Kind == "CSIStorageCapacity": 611 | rmap, err = getCSIStorageCapacityRelationships(node) 612 | if err != nil { 613 | klog.V(4).Infof("Failed to get relationships for csistoragecapacity named \"%s\": %s: %s", node.Name, err) 614 | continue 615 | } 616 | // Populate dependencies & dependents based on CSINode relationships 617 | case node.Group == storagev1.GroupName && node.Kind == "CSINode": 618 | rmap, err = getCSINodeRelationships(node) 619 | if err != nil { 620 | klog.V(4).Infof("Failed to get relationships for csinode named \"%s\": %s: %s", node.Name, err) 621 | continue 622 | } 623 | // Populate dependencies & dependents based on StorageClass relationships 624 | case node.Group == storagev1.GroupName && node.Kind == "StorageClass": 625 | rmap, err = getStorageClassRelationships(node) 626 | if err != nil { 627 | klog.V(4).Infof("Failed to get relationships for storageclass named \"%s\": %s: %s", node.Name, err) 628 | continue 629 | } 630 | // Populate dependencies & dependents based on VolumeAttachment relationships 631 | case node.Group == storagev1.GroupName && node.Kind == "VolumeAttachment": 632 | rmap, err = getVolumeAttachmentRelationships(node) 633 | if err != nil { 634 | klog.V(4).Infof("Failed to get relationships for volumeattachment named \"%s\": %s: %s", node.Name, err) 635 | continue 636 | } 637 | default: 638 | continue 639 | } 640 | updateRelationships(node, rmap) 641 | } 642 | 643 | // Create submap containing the provided objects & either their dependencies 644 | // or dependents from the global map 645 | var depth uint 646 | nodeMap, uidQueue, uidSet := NodeMap{}, []types.UID{}, map[types.UID]struct{}{} 647 | for _, uid := range uids { 648 | if node := globalMapByUID[uid]; node != nil { 649 | nodeMap[uid] = node 650 | uidQueue = append(uidQueue, uid) 651 | } 652 | } 653 | depth, uidQueue = 0, append(uidQueue, "") 654 | for { 655 | if len(uidQueue) <= 1 { 656 | break 657 | } 658 | uid := uidQueue[0] 659 | if uid == "" { 660 | depth, uidQueue = depth+1, append(uidQueue[1:], "") 661 | continue 662 | } 663 | 664 | // Guard against possible cycles 665 | if _, ok := uidSet[uid]; ok { 666 | uidQueue = uidQueue[1:] 667 | continue 668 | } else { 669 | uidSet[uid] = struct{}{} 670 | } 671 | 672 | if node := nodeMap[uid]; node != nil { 673 | // Allow nodes to keep the smallest depth. For example, if a node has a 674 | // depth of 1 & 7 in the relationship tree, we keep 1 so that when 675 | // printing the tree with a depth of 2, the node will still be printed 676 | if node.Depth == 0 || depth < node.Depth { 677 | node.Depth = depth 678 | } 679 | deps := node.GetDeps(depsIsDependencies) 680 | depUIDs, ix := make([]types.UID, len(deps)), 0 681 | for depUID := range deps { 682 | nodeMap[depUID] = globalMapByUID[depUID] 683 | depUIDs[ix] = depUID 684 | ix++ 685 | } 686 | uidQueue = append(uidQueue[1:], depUIDs...) 687 | } 688 | } 689 | 690 | klog.V(4).Infof("Resolved %d deps for %d objects", len(nodeMap)-1, len(uids)) 691 | return nodeMap, nil 692 | } 693 | -------------------------------------------------------------------------------- /internal/graph/helm.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | const ( 4 | // Helm relationships. 5 | RelationshipHelmRelease Relationship = "HelmRelease" 6 | RelationshipHelmStorage Relationship = "HelmStorage" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | goflag "flag" 5 | 6 | "github.com/spf13/pflag" 7 | "k8s.io/klog/v2" 8 | ) 9 | 10 | // AddFlags adds flags for logging. 11 | func AddFlags(flags *pflag.FlagSet) { 12 | klogFlagSet := goflag.NewFlagSet("klog", goflag.ContinueOnError) 13 | klog.InitFlags(klogFlagSet) 14 | flags.AddGoFlagSet(klogFlagSet) 15 | 16 | // Logs are written to standard error instead of to files 17 | _ = flags.Set("logtostderr", "true") 18 | 19 | // Hide log flags to make our help command consistent with kubectl 20 | _ = flags.MarkHidden("add_dir_header") 21 | _ = flags.MarkHidden("alsologtostderr") 22 | _ = flags.MarkHidden("log_backtrace_at") 23 | _ = flags.MarkHidden("log_dir") 24 | _ = flags.MarkHidden("log_file") 25 | _ = flags.MarkHidden("log_file_max_size") 26 | _ = flags.MarkHidden("logtostderr") 27 | _ = flags.MarkHidden("one_output") 28 | _ = flags.MarkHidden("skip_headers") 29 | _ = flags.MarkHidden("skip_log_headers") 30 | _ = flags.MarkHidden("stderrthreshold") 31 | _ = flags.MarkHidden("v") 32 | _ = flags.MarkHidden("vmodule") 33 | } 34 | -------------------------------------------------------------------------------- /internal/printers/flags.go: -------------------------------------------------------------------------------- 1 | package printers 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/pflag" 8 | "k8s.io/cli-runtime/pkg/genericclioptions" 9 | 10 | "github.com/tohjustin/kube-lineage/internal/client" 11 | ) 12 | 13 | const ( 14 | flagOutputFormat = "output" 15 | flagOutputFormatShorthand = "o" 16 | ) 17 | 18 | // Flags composes common printer flag structs used in the command. 19 | type Flags struct { 20 | HumanReadableFlags *HumanPrintFlags 21 | OutputFormat *string 22 | } 23 | 24 | // AddFlags receives a *pflag.FlagSet reference and binds flags related to 25 | // human-readable printing to it. 26 | func (f *Flags) AddFlags(flags *pflag.FlagSet) { 27 | f.HumanReadableFlags.AddFlags(flags) 28 | 29 | if f.OutputFormat != nil { 30 | flags.StringVarP(f.OutputFormat, flagOutputFormat, flagOutputFormatShorthand, *f.OutputFormat, fmt.Sprintf("Output format. One of: %s.", strings.Join(f.AllowedFormats(), "|"))) 31 | } 32 | } 33 | 34 | // AllowedFormats is the list of formats in which data can be displayed. 35 | func (f *Flags) AllowedFormats() []string { 36 | formats := []string{} 37 | formats = append(formats, f.HumanReadableFlags.AllowedFormats()...) 38 | return formats 39 | } 40 | 41 | // Copy returns a copy of Flags for mutation. 42 | func (f *Flags) Copy() Flags { 43 | printFlags := *f 44 | return printFlags 45 | } 46 | 47 | // EnsureWithGroup ensures that human-readable flags return a printer capable of 48 | // including resource group. 49 | func (f *Flags) EnsureWithGroup() { 50 | f.HumanReadableFlags.EnsureWithGroup() 51 | } 52 | 53 | // IsTableOutputFormat returns true if provided output format is a table format. 54 | func (f *Flags) IsTableOutputFormat(outputFormat string) bool { 55 | return f.HumanReadableFlags.IsSupportedOutputFormat(outputFormat) 56 | } 57 | 58 | // SetShowNamespace configures whether human-readable flags return a printer 59 | // capable of printing with a "namespace" column. 60 | func (f *Flags) SetShowNamespace(b bool) { 61 | f.HumanReadableFlags.SetShowNamespace(b) 62 | } 63 | 64 | // ToPrinter returns a printer based on current flag values. 65 | func (f *Flags) ToPrinter(client client.Interface) (Interface, error) { 66 | outputFormat := "" 67 | if f.OutputFormat != nil { 68 | outputFormat = *f.OutputFormat 69 | } 70 | 71 | var printer Interface 72 | switch { 73 | case f.IsTableOutputFormat(outputFormat), outputFormat == "": 74 | configFlags := f.Copy() 75 | printer = &tablePrinter{ 76 | configFlags: configFlags.HumanReadableFlags, 77 | outputFormat: outputFormat, 78 | client: client, 79 | } 80 | default: 81 | return nil, genericclioptions.NoCompatiblePrinterError{ 82 | AllowedFormats: f.AllowedFormats(), 83 | OutputFormat: &outputFormat, 84 | } 85 | } 86 | 87 | return printer, nil 88 | } 89 | 90 | // NewFlags returns flags associated with human-readable printing, with default 91 | // values set. 92 | func NewFlags() *Flags { 93 | outputFormat := "" 94 | 95 | return &Flags{ 96 | OutputFormat: &outputFormat, 97 | HumanReadableFlags: NewHumanPrintFlags(), 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/printers/flags_humanreadable.go: -------------------------------------------------------------------------------- 1 | package printers 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | "k8s.io/apimachinery/pkg/util/sets" 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | "k8s.io/cli-runtime/pkg/printers" 9 | ) 10 | 11 | const ( 12 | flagColumnLabels = "label-columns" 13 | flagColumnLabelsShorthand = "L" 14 | flagNoHeaders = "no-headers" 15 | flagShowGroup = "show-group" 16 | flagShowLabels = "show-labels" 17 | flagShowNamespace = "show-namespace" 18 | ) 19 | 20 | // List of supported table output formats. 21 | const ( 22 | outputFormatWide = "wide" 23 | outputFormatSplit = "split" 24 | outputFormatSplitWide = "split-wide" 25 | ) 26 | 27 | // HumanPrintFlags provides default flags necessary for printing. Given the 28 | // following flag values, a printer can be requested that knows how to handle 29 | // printing based on these values. 30 | type HumanPrintFlags struct { 31 | ColumnLabels *[]string 32 | NoHeaders *bool 33 | ShowGroup *bool 34 | ShowLabels *bool 35 | ShowNamespace *bool 36 | } 37 | 38 | // EnsureWithGroup sets the "ShowGroup" human-readable option to true. 39 | func (f *HumanPrintFlags) EnsureWithGroup() { 40 | showGroup := true 41 | f.ShowGroup = &showGroup 42 | } 43 | 44 | // SetShowNamespace sets the "ShowNamespace" human-readable option. 45 | func (f *HumanPrintFlags) SetShowNamespace(b bool) { 46 | f.ShowNamespace = &b 47 | } 48 | 49 | // AllowedFormats returns more customized formating options. 50 | func (f *HumanPrintFlags) AllowedFormats() []string { 51 | return []string{ 52 | outputFormatWide, 53 | outputFormatSplit, 54 | outputFormatSplitWide, 55 | } 56 | } 57 | 58 | // IsSupportedOutputFormat returns true if provided output format is supported. 59 | func (f *HumanPrintFlags) IsSupportedOutputFormat(outputFormat string) bool { 60 | return sets.NewString(f.AllowedFormats()...).Has(outputFormat) 61 | } 62 | 63 | // IsSplitOutputFormat returns true if provided output format is a split table 64 | // format. 65 | func (f *HumanPrintFlags) IsSplitOutputFormat(outputFormat string) bool { 66 | return outputFormat == outputFormatSplit || outputFormat == outputFormatSplitWide 67 | } 68 | 69 | // IsWideOutputFormat returns true if provided output format is a wide table 70 | // format. 71 | func (f *HumanPrintFlags) IsWideOutputFormat(outputFormat string) bool { 72 | return outputFormat == outputFormatWide || outputFormat == outputFormatSplitWide 73 | } 74 | 75 | // ToPrinter receives an outputFormat and returns a printer capable of handling 76 | // human-readable output. 77 | func (f *HumanPrintFlags) ToPrinterWithGK(outputFormat string, gk schema.GroupKind) (printers.ResourcePrinter, error) { 78 | if len(outputFormat) > 0 && !f.IsSupportedOutputFormat(outputFormat) { 79 | return nil, genericclioptions.NoCompatiblePrinterError{ 80 | Options: f, 81 | AllowedFormats: f.AllowedFormats(), 82 | } 83 | } 84 | columnLabels := []string{} 85 | if f.ColumnLabels != nil { 86 | columnLabels = *f.ColumnLabels 87 | } 88 | noHeaders := false 89 | if f.NoHeaders != nil { 90 | noHeaders = *f.NoHeaders 91 | } 92 | showLabels := false 93 | if f.ShowLabels != nil { 94 | showLabels = *f.ShowLabels 95 | } 96 | showNamespace := false 97 | if f.ShowLabels != nil { 98 | showNamespace = *f.ShowNamespace 99 | } 100 | p := printers.NewTablePrinter(printers.PrintOptions{ 101 | ColumnLabels: columnLabels, 102 | NoHeaders: noHeaders, 103 | ShowLabels: showLabels, 104 | Kind: gk, 105 | WithKind: !gk.Empty(), 106 | Wide: f.IsWideOutputFormat(outputFormat), 107 | WithNamespace: showNamespace, 108 | }) 109 | return p, nil 110 | } 111 | 112 | // ToPrinter receives an outputFormat and returns a printer capable of handling 113 | // human-readable output. 114 | func (f *HumanPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) { 115 | return f.ToPrinterWithGK(outputFormat, schema.GroupKind{}) 116 | } 117 | 118 | // AddFlags receives a *pflag.FlagSet reference and binds flags related to 119 | // human-readable printing to it. 120 | func (f *HumanPrintFlags) AddFlags(flags *pflag.FlagSet) { 121 | if f.ColumnLabels != nil { 122 | flags.StringSliceVarP(f.ColumnLabels, flagColumnLabels, flagColumnLabelsShorthand, *f.ColumnLabels, "Accepts a comma separated list of labels that are going to be presented as columns. Names are case-sensitive. You can also use multiple flag options like -L label1 -L label2...") 123 | } 124 | if f.NoHeaders != nil { 125 | flags.BoolVar(f.NoHeaders, flagNoHeaders, *f.NoHeaders, "When using the default output format, don't print headers (default print headers)") 126 | } 127 | if f.ShowGroup != nil { 128 | flags.BoolVar(f.ShowGroup, flagShowGroup, *f.ShowGroup, "If present, include the resource group for the requested object(s)") 129 | } 130 | if f.ShowLabels != nil { 131 | flags.BoolVar(f.ShowLabels, flagShowLabels, *f.ShowLabels, "When printing, show all labels as the last column (default hide labels column)") 132 | } 133 | if f.ShowNamespace != nil { 134 | flags.BoolVar(f.ShowNamespace, flagShowNamespace, *f.ShowNamespace, "When printing, show namespace as the first column (default hide namespace column if all objects are in the same namespace)") 135 | } 136 | } 137 | 138 | // NewHumanPrintFlags returns flags associated with human-readable printing, 139 | // with default values set. 140 | func NewHumanPrintFlags() *HumanPrintFlags { 141 | columnLabels := []string{} 142 | noHeaders := false 143 | showGroup := false 144 | showLabels := false 145 | showNamespace := false 146 | 147 | return &HumanPrintFlags{ 148 | ColumnLabels: &columnLabels, 149 | NoHeaders: &noHeaders, 150 | ShowGroup: &showGroup, 151 | ShowLabels: &showLabels, 152 | ShowNamespace: &showNamespace, 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /internal/printers/printers.go: -------------------------------------------------------------------------------- 1 | package printers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "sort" 8 | 9 | "golang.org/x/sync/errgroup" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/apimachinery/pkg/util/sets" 14 | 15 | "github.com/tohjustin/kube-lineage/internal/client" 16 | "github.com/tohjustin/kube-lineage/internal/graph" 17 | ) 18 | 19 | type sortableGroupKind []schema.GroupKind 20 | 21 | func (s sortableGroupKind) Len() int { return len(s) } 22 | func (s sortableGroupKind) Less(i, j int) bool { return lessGroupKind(s[i], s[j]) } 23 | func (s sortableGroupKind) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 24 | 25 | func lessGroupKind(lhs, rhs schema.GroupKind) bool { 26 | return lhs.String() < rhs.String() 27 | } 28 | 29 | type Interface interface { 30 | Print(w io.Writer, nodeMap graph.NodeMap, rootUID types.UID, maxDepth uint, depsIsDependencies bool) error 31 | } 32 | 33 | type tablePrinter struct { 34 | configFlags *HumanPrintFlags 35 | outputFormat string 36 | 37 | // client for fetching server-printed tables when printing in split output 38 | // format 39 | client client.Interface 40 | } 41 | 42 | func (p *tablePrinter) Print(w io.Writer, nodeMap graph.NodeMap, rootUID types.UID, maxDepth uint, depsIsDependencies bool) error { 43 | root, ok := nodeMap[rootUID] 44 | if !ok { 45 | return fmt.Errorf("requested object (uid: %s) not found in list of fetched objects", rootUID) 46 | } 47 | 48 | if p.configFlags.IsSplitOutputFormat(p.outputFormat) { 49 | if p.client == nil { 50 | return fmt.Errorf("client must be provided to get server-printed tables") 51 | } 52 | return p.printTablesByGK(w, nodeMap, maxDepth) 53 | } 54 | 55 | return p.printTable(w, nodeMap, root, maxDepth, depsIsDependencies) 56 | } 57 | 58 | func (p *tablePrinter) printTable(w io.Writer, nodeMap graph.NodeMap, root *graph.Node, maxDepth uint, depsIsDependencies bool) error { 59 | // Generate Table to print 60 | showGroup := false 61 | if sg := p.configFlags.ShowGroup; sg != nil { 62 | showGroup = *sg 63 | } 64 | showGroupFn := createShowGroupFn(nodeMap, showGroup, maxDepth) 65 | t, err := nodeMapToTable(nodeMap, root, maxDepth, depsIsDependencies, showGroupFn) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Setup Table printer 71 | p.configFlags.SetShowNamespace(shouldShowNamespace(nodeMap, maxDepth)) 72 | tableprinter, err := p.configFlags.ToPrinter(p.outputFormat) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | return tableprinter.PrintObj(t, w) 78 | } 79 | 80 | func (p *tablePrinter) printTablesByGK(w io.Writer, nodeMap graph.NodeMap, maxDepth uint) error { 81 | // Generate Tables to print 82 | showGroup, showNamespace := false, false 83 | if sg := p.configFlags.ShowGroup; sg != nil { 84 | showGroup = *sg 85 | } 86 | if sg := p.configFlags.ShowNamespace; sg != nil { 87 | showNamespace = *sg 88 | } 89 | showGroupFn := createShowGroupFn(nodeMap, showGroup, maxDepth) 90 | showNamespaceFn := createShowNamespaceFn(nodeMap, showNamespace, maxDepth) 91 | 92 | tListByGK, err := p.nodeMapToTableByGK(nodeMap, maxDepth) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | // Sort Tables by GroupKind 98 | var gkList sortableGroupKind 99 | for gk := range tListByGK { 100 | gkList = append(gkList, gk) 101 | } 102 | sort.Sort(gkList) 103 | for ix, gk := range gkList { 104 | if t, ok := tListByGK[gk]; ok { 105 | // Setup Table printer 106 | tgk := gk 107 | if !showGroupFn(gk.Kind) { 108 | tgk = schema.GroupKind{Kind: gk.Kind} 109 | } 110 | p.configFlags.SetShowNamespace(showNamespaceFn(gk)) 111 | tableprinter, err := p.configFlags.ToPrinterWithGK(p.outputFormat, tgk) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | // Setup Table printer 117 | err = tableprinter.PrintObj(t, w) 118 | if err != nil { 119 | return err 120 | } 121 | if ix != len(gkList)-1 { 122 | fmt.Fprintf(w, "\n") 123 | } 124 | } 125 | } 126 | 127 | return nil 128 | } 129 | 130 | //nolint:funlen,gocognit 131 | func (p *tablePrinter) nodeMapToTableByGK(nodeMap graph.NodeMap, maxDepth uint) (map[schema.GroupKind](*metav1.Table), error) { 132 | // Filter objects to print based on depth 133 | objUIDs := []types.UID{} 134 | for uid, node := range nodeMap { 135 | if maxDepth == 0 || node.Depth <= maxDepth { 136 | objUIDs = append(objUIDs, uid) 137 | } 138 | } 139 | 140 | // Group objects by GroupKind & Namespace 141 | nodesByGKAndNS := map[schema.GroupKind](map[string]graph.NodeList){} 142 | for _, uid := range objUIDs { 143 | if node, ok := nodeMap[uid]; ok { 144 | gk := schema.GroupKind{Group: node.Group, Kind: node.Kind} 145 | ns := node.Namespace 146 | if _, ok := nodesByGKAndNS[gk]; !ok { 147 | nodesByGKAndNS[gk] = map[string]graph.NodeList{} 148 | } 149 | nodesByGKAndNS[gk][ns] = append(nodesByGKAndNS[gk][ns], node) 150 | } 151 | } 152 | 153 | // Fan-out to get server-print tables for all objects 154 | eg, ctx := errgroup.WithContext(context.Background()) 155 | tableByGKAndNS := map[schema.GroupKind](map[string]*metav1.Table){} 156 | for gk, nodesByNS := range nodesByGKAndNS { 157 | if len(gk.Kind) == 0 { 158 | continue 159 | } 160 | for ns, nodes := range nodesByNS { 161 | if len(nodes) == 0 { 162 | continue 163 | } 164 | gk, api, ns, names := gk, client.APIResource(nodes[0].GetAPIResource()), ns, []string{} 165 | for _, n := range nodes { 166 | names = append(names, n.Name) 167 | } 168 | // Sort TableRows by name 169 | sortedNames := sets.NewString(names...).List() 170 | eg.Go(func() error { 171 | table, err := p.client.GetTable(ctx, client.GetTableOptions{ 172 | APIResource: api, 173 | Namespace: ns, 174 | Names: sortedNames, 175 | }) 176 | if err != nil || table == nil { 177 | return err 178 | } 179 | if _, ok := tableByGKAndNS[gk]; !ok { 180 | tableByGKAndNS[gk] = map[string]*metav1.Table{} 181 | } 182 | if t, ok := tableByGKAndNS[gk][ns]; !ok { 183 | tableByGKAndNS[gk][ns] = table 184 | } else { 185 | t.Rows = append(t.Rows, table.Rows...) 186 | } 187 | return nil 188 | }) 189 | } 190 | } 191 | if err := eg.Wait(); err != nil { 192 | return nil, err 193 | } 194 | 195 | // Sort TableRows by namespace 196 | tableByGK := map[schema.GroupKind]*metav1.Table{} 197 | for gk, tableByNS := range tableByGKAndNS { 198 | var nsList []string 199 | for ns := range tableByNS { 200 | nsList = append(nsList, ns) 201 | } 202 | sortedNSList := sets.NewString(nsList...).List() 203 | var table *metav1.Table 204 | for _, ns := range sortedNSList { 205 | if t, ok := tableByNS[ns]; ok { 206 | if table == nil { 207 | table = t 208 | } else { 209 | table.Rows = append(table.Rows, t.Rows...) 210 | } 211 | } 212 | } 213 | tableByGK[gk] = table 214 | } 215 | 216 | return tableByGK, nil 217 | } 218 | -------------------------------------------------------------------------------- /internal/printers/printers_humanreadable.go: -------------------------------------------------------------------------------- 1 | package printers 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "time" 8 | 9 | appsv1 "k8s.io/api/apps/v1" 10 | corev1 "k8s.io/api/core/v1" 11 | eventsv1 "k8s.io/api/events/v1" 12 | policyv1 "k8s.io/api/policy/v1" 13 | storagev1 "k8s.io/api/storage/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | "k8s.io/apimachinery/pkg/runtime/schema" 18 | "k8s.io/apimachinery/pkg/types" 19 | "k8s.io/apimachinery/pkg/util/duration" 20 | "k8s.io/client-go/util/jsonpath" 21 | apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" 22 | 23 | "github.com/tohjustin/kube-lineage/internal/graph" 24 | ) 25 | 26 | const ( 27 | cellUnknown = "" 28 | cellNotApplicable = "-" 29 | ) 30 | 31 | var ( 32 | // objectColumnDefinitions holds table column definition for Kubernetes objects. 33 | objectColumnDefinitions = []metav1.TableColumnDefinition{ 34 | {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, 35 | {Name: "Ready", Type: "string", Description: "The readiness state of this object."}, 36 | {Name: "Status", Type: "string", Description: "The status of this object."}, 37 | {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, 38 | {Name: "Relationships", Type: "array", Description: "The relationships this object has with its parent.", Priority: -1}, 39 | } 40 | // objectReadyReasonJSONPath is the JSON path to get a Kubernetes object's 41 | // "Ready" condition reason. 42 | objectReadyReasonJSONPath = newJSONPath("reason", "{.status.conditions[?(@.type==\"Ready\")].reason}") 43 | // objectReadyStatusJSONPath is the JSON path to get a Kubernetes object's 44 | // "Ready" condition status. 45 | objectReadyStatusJSONPath = newJSONPath("status", "{.status.conditions[?(@.type==\"Ready\")].status}") 46 | ) 47 | 48 | // createShowGroupFn creates a function that takes in a resource's kind & 49 | // determines whether the resource's group should be included in its name. 50 | func createShowGroupFn(nodeMap graph.NodeMap, showGroup bool, maxDepth uint) func(string) bool { 51 | // Create function that returns true, if showGroup is true 52 | if showGroup { 53 | return func(_ string) bool { 54 | return true 55 | } 56 | } 57 | 58 | // Track every object kind in the node map & the groups that they belong to. 59 | kindToGroupSetMap := map[string](map[string]struct{}){} 60 | for _, node := range nodeMap { 61 | if maxDepth != 0 && node.Depth > maxDepth { 62 | continue 63 | } 64 | if _, ok := kindToGroupSetMap[node.Kind]; !ok { 65 | kindToGroupSetMap[node.Kind] = map[string]struct{}{} 66 | } 67 | kindToGroupSetMap[node.Kind][node.Group] = struct{}{} 68 | } 69 | // When printing an object & if there exists another object in the node map 70 | // that has the same kind but belongs to a different group (eg. "services.v1" 71 | // vs "services.v1.serving.knative.dev"), we prepend the object's name with 72 | // its GroupKind instead of its Kind to clearly indicate which resource type 73 | // it belongs to. 74 | return func(kind string) bool { 75 | return len(kindToGroupSetMap[kind]) > 1 76 | } 77 | } 78 | 79 | // createShowNamespaceFn creates a function that takes in a resource's GroupKind 80 | // & determines whether the resource's namespace should be shown. 81 | func createShowNamespaceFn(nodeMap graph.NodeMap, showNamespace bool, maxDepth uint) func(schema.GroupKind) bool { 82 | showNS := showNamespace || shouldShowNamespace(nodeMap, maxDepth) 83 | if !showNS { 84 | return func(_ schema.GroupKind) bool { 85 | return false 86 | } 87 | } 88 | 89 | clusterScopeGKSet := map[schema.GroupKind]struct{}{} 90 | for _, node := range nodeMap { 91 | if maxDepth != 0 && node.Depth > maxDepth { 92 | continue 93 | } 94 | gk := node.GroupVersionKind().GroupKind() 95 | if !node.Namespaced { 96 | clusterScopeGKSet[gk] = struct{}{} 97 | } 98 | } 99 | return func(gk schema.GroupKind) bool { 100 | _, isClusterScopeGK := clusterScopeGKSet[gk] 101 | return !isClusterScopeGK 102 | } 103 | } 104 | 105 | // shouldShowNamespace determines whether namespace column should be shown. 106 | // Returns true if objects in the provided node map are in different namespaces. 107 | func shouldShowNamespace(nodeMap graph.NodeMap, maxDepth uint) bool { 108 | nsSet := map[string]struct{}{} 109 | for _, node := range nodeMap { 110 | if maxDepth != 0 && node.Depth > maxDepth { 111 | continue 112 | } 113 | ns := node.Namespace 114 | if _, ok := nsSet[ns]; !ok { 115 | nsSet[ns] = struct{}{} 116 | } 117 | } 118 | return len(nsSet) > 1 119 | } 120 | 121 | // newJSONPath returns a JSONPath object created from parsing the provided JSON 122 | // path expression. 123 | func newJSONPath(name, jsonPath string) *jsonpath.JSONPath { 124 | jp := jsonpath.New(name).AllowMissingKeys(true) 125 | if err := jp.Parse(jsonPath); err != nil { 126 | panic(err) 127 | } 128 | return jp 129 | } 130 | 131 | // getNestedString returns the field value of a Kubernetes object at the 132 | // provided JSON path. 133 | func getNestedString(data map[string]interface{}, jp *jsonpath.JSONPath) (string, error) { 134 | values, err := jp.FindResults(data) 135 | if err != nil { 136 | return "", err 137 | } 138 | strValues := []string{} 139 | for arrIx := range values { 140 | for valIx := range values[arrIx] { 141 | strValues = append(strValues, fmt.Sprintf("%v", values[arrIx][valIx].Interface())) 142 | } 143 | } 144 | str := strings.Join(strValues, ",") 145 | 146 | return str, nil 147 | } 148 | 149 | // getObjectReadyStatus returns the ready & status value of a Kubernetes object. 150 | func getObjectReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 151 | data := u.UnstructuredContent() 152 | ready, err := getNestedString(data, objectReadyStatusJSONPath) 153 | if err != nil { 154 | return "", "", err 155 | } 156 | status, err := getNestedString(data, objectReadyReasonJSONPath) 157 | if err != nil { 158 | return ready, "", err 159 | } 160 | 161 | return ready, status, nil 162 | } 163 | 164 | // getAPIServiceReadyStatus returns the ready & status value of a APIService 165 | // which is based off the table cell values computed by printAPIService from 166 | // https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go. 167 | func getAPIServiceReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 168 | var apisvc apiregistrationv1.APIService 169 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &apisvc) 170 | if err != nil { 171 | return "", "", err 172 | } 173 | var ready, status string 174 | for _, condition := range apisvc.Status.Conditions { 175 | if condition.Type == apiregistrationv1.Available { 176 | ready = string(condition.Status) 177 | if condition.Status != apiregistrationv1.ConditionTrue { 178 | status = condition.Reason 179 | } 180 | } 181 | } 182 | 183 | return ready, status, nil 184 | } 185 | 186 | // getDaemonSetReadyStatus returns the ready & status value of a DaemonSet 187 | // which is based off the table cell values computed by printDaemonSet from 188 | // https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go. 189 | //nolint:unparam 190 | func getDaemonSetReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 191 | var ds appsv1.DaemonSet 192 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &ds) 193 | if err != nil { 194 | return "", "", err 195 | } 196 | desiredReplicas := ds.Status.DesiredNumberScheduled 197 | readyReplicas := ds.Status.NumberReady 198 | ready := fmt.Sprintf("%d/%d", readyReplicas, desiredReplicas) 199 | 200 | return ready, "", nil 201 | } 202 | 203 | // getDeploymentReadyStatus returns the ready & status value of a Deployment 204 | // which is based off the table cell values computed by printDeployment from 205 | // https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go. 206 | //nolint:unparam 207 | func getDeploymentReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 208 | var deploy appsv1.Deployment 209 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &deploy) 210 | if err != nil { 211 | return "", "", err 212 | } 213 | desiredReplicas := deploy.Status.Replicas 214 | readyReplicas := deploy.Status.ReadyReplicas 215 | ready := fmt.Sprintf("%d/%d", readyReplicas, desiredReplicas) 216 | 217 | return ready, "", nil 218 | } 219 | 220 | // getEventCoreReadyStatus returns the ready & status value of a Event. 221 | //nolint:unparam 222 | func getEventCoreReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 223 | var status string 224 | var ev corev1.Event 225 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &ev) 226 | if err != nil { 227 | return "", "", err 228 | } 229 | if ev.Count > 1 { 230 | status = fmt.Sprintf("%s: %s (x%d)", ev.Reason, ev.Message, ev.Count) 231 | } else { 232 | status = fmt.Sprintf("%s: %s", ev.Reason, ev.Message) 233 | } 234 | 235 | return "", status, nil 236 | } 237 | 238 | // getEventReadyStatus returns the ready & status value of a Event.events.k8s.io. 239 | //nolint:unparam 240 | func getEventReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 241 | var status string 242 | var ev eventsv1.Event 243 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &ev) 244 | if err != nil { 245 | return "", "", err 246 | } 247 | if ev.DeprecatedCount > 1 { 248 | status = fmt.Sprintf("%s: %s (x%d)", ev.Reason, ev.Note, ev.DeprecatedCount) 249 | } else { 250 | status = fmt.Sprintf("%s: %s", ev.Reason, ev.Note) 251 | } 252 | 253 | return "", status, nil 254 | } 255 | 256 | // getPodReadyStatus returns the ready & status value of a Pod which is based 257 | // off the table cell values computed by printPod from 258 | // https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go. 259 | //nolint:funlen,gocognit,gocyclo 260 | func getPodReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 261 | var pod corev1.Pod 262 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &pod) 263 | if err != nil { 264 | return "", "", err 265 | } 266 | totalContainers := len(pod.Spec.Containers) 267 | readyContainers := 0 268 | reason := string(pod.Status.Phase) 269 | if len(pod.Status.Reason) > 0 { 270 | reason = pod.Status.Reason 271 | } 272 | initializing := false 273 | for i := range pod.Status.InitContainerStatuses { 274 | container := pod.Status.InitContainerStatuses[i] 275 | state := container.State 276 | switch { 277 | case state.Terminated != nil && state.Terminated.ExitCode == 0: 278 | continue 279 | case state.Terminated != nil && len(state.Terminated.Reason) > 0: 280 | reason = state.Terminated.Reason 281 | case state.Terminated != nil && len(state.Terminated.Reason) == 0 && state.Terminated.Signal != 0: 282 | reason = fmt.Sprintf("Signal:%d", state.Terminated.Signal) 283 | case state.Terminated != nil && len(state.Terminated.Reason) == 0 && state.Terminated.Signal == 0: 284 | reason = fmt.Sprintf("ExitCode:%d", state.Terminated.ExitCode) 285 | case state.Waiting != nil && len(state.Waiting.Reason) > 0 && state.Waiting.Reason != "PodInitializing": 286 | reason = state.Waiting.Reason 287 | default: 288 | reason = fmt.Sprintf("%d/%d", i, len(pod.Spec.InitContainers)) 289 | } 290 | reason = fmt.Sprintf("Init:%s", reason) 291 | initializing = true 292 | break 293 | } 294 | if !initializing { 295 | hasRunning := false 296 | for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- { 297 | container := pod.Status.ContainerStatuses[i] 298 | state := container.State 299 | switch { 300 | case state.Terminated != nil && len(state.Terminated.Reason) > 0: 301 | reason = state.Terminated.Reason 302 | case state.Terminated != nil && len(state.Terminated.Reason) == 0 && state.Terminated.Signal != 0: 303 | reason = fmt.Sprintf("Signal:%d", state.Terminated.Signal) 304 | case state.Terminated != nil && len(state.Terminated.Reason) == 0 && state.Terminated.Signal == 0: 305 | reason = fmt.Sprintf("ExitCode:%d", state.Terminated.ExitCode) 306 | case state.Waiting != nil && len(state.Waiting.Reason) > 0: 307 | reason = state.Waiting.Reason 308 | case state.Running != nil && container.Ready: 309 | hasRunning = true 310 | readyContainers++ 311 | } 312 | } 313 | // change pod status back to "Running" if there is at least one container still reporting as "Running" status 314 | if reason == "Completed" && hasRunning { 315 | reason = "NotReady" 316 | for _, condition := range pod.Status.Conditions { 317 | if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue { 318 | reason = "Running" 319 | } 320 | } 321 | } 322 | } 323 | if pod.DeletionTimestamp != nil { 324 | // Hardcode "k8s.io/kubernetes/pkg/util/node.NodeUnreachablePodReason" as 325 | // "NodeLost" so we don't need import the entire k8s.io/kubernetes package 326 | if pod.Status.Reason == "NodeLost" { 327 | reason = "Unknown" 328 | } else { 329 | reason = "Terminating" 330 | } 331 | } 332 | ready := fmt.Sprintf("%d/%d", readyContainers, totalContainers) 333 | 334 | return ready, reason, nil 335 | } 336 | 337 | // getPodDisruptionBudgetReadyStatus returns the ready & status value of a 338 | // PodDisruptionBudget. 339 | //nolint:unparam 340 | func getPodDisruptionBudgetReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 341 | var pdb policyv1.PodDisruptionBudget 342 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &pdb) 343 | if err != nil { 344 | return "", "", err 345 | } 346 | var status string 347 | for _, condition := range pdb.Status.Conditions { 348 | if condition.ObservedGeneration == pdb.Generation { 349 | if condition.Type == policyv1.DisruptionAllowedCondition { 350 | status = condition.Reason 351 | } 352 | } 353 | } 354 | 355 | return "", status, nil 356 | } 357 | 358 | // getReplicaSetReadyStatus returns the ready & status value of a ReplicaSet 359 | // which is based off the table cell values computed by printReplicaSet from 360 | // https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go. 361 | //nolint:unparam 362 | func getReplicaSetReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 363 | var rs appsv1.ReplicaSet 364 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &rs) 365 | if err != nil { 366 | return "", "", err 367 | } 368 | desiredReplicas := rs.Status.Replicas 369 | readyReplicas := rs.Status.ReadyReplicas 370 | ready := fmt.Sprintf("%d/%d", readyReplicas, desiredReplicas) 371 | 372 | return ready, "", nil 373 | } 374 | 375 | // getReplicationControllerReadyStatus returns the ready & status value of a 376 | // ReplicationController which is based off the table cell values computed by 377 | // printReplicationController from 378 | // https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go. 379 | //nolint:unparam 380 | func getReplicationControllerReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 381 | var rc corev1.ReplicationController 382 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &rc) 383 | if err != nil { 384 | return "", "", err 385 | } 386 | desiredReplicas := rc.Status.Replicas 387 | readyReplicas := rc.Status.ReadyReplicas 388 | ready := fmt.Sprintf("%d/%d", readyReplicas, desiredReplicas) 389 | 390 | return ready, "", nil 391 | } 392 | 393 | // getStatefulSetReadyStatus returns the ready & status value of a StatefulSet 394 | // which is based off the table cell values computed by printStatefulSet from 395 | // https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go. 396 | //nolint:unparam 397 | func getStatefulSetReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 398 | var sts appsv1.StatefulSet 399 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &sts) 400 | if err != nil { 401 | return "", "", err 402 | } 403 | desiredReplicas := sts.Status.Replicas 404 | readyReplicas := sts.Status.ReadyReplicas 405 | ready := fmt.Sprintf("%d/%d", readyReplicas, desiredReplicas) 406 | 407 | return ready, "", nil 408 | } 409 | 410 | // getVolumeAttachmentReadyStatus returns the ready & status value of a 411 | // VolumeAttachment. 412 | func getVolumeAttachmentReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) { 413 | var va storagev1.VolumeAttachment 414 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &va) 415 | if err != nil { 416 | return "", "", err 417 | } 418 | var ready, status string 419 | if va.Status.Attached { 420 | ready = "True" 421 | } else { 422 | ready = "False" 423 | } 424 | var errTime time.Time 425 | if err := va.Status.AttachError; err != nil { 426 | status = err.Message 427 | errTime = err.Time.Time 428 | } 429 | if err := va.Status.DetachError; err != nil { 430 | if err.Time.After(errTime) { 431 | status = err.Message 432 | } 433 | } 434 | 435 | return ready, status, nil 436 | } 437 | 438 | // nodeToTableRow converts the provided node into a table row. 439 | //nolint:funlen,gocognit,goconst 440 | func nodeToTableRow(node *graph.Node, rset graph.RelationshipSet, namePrefix string, showGroupFn func(kind string) bool) metav1.TableRow { 441 | var name, ready, status, age string 442 | var relationships interface{} 443 | 444 | switch { 445 | case len(node.Kind) == 0: 446 | name = node.Name 447 | case len(node.Group) > 0 && showGroupFn(node.Kind): 448 | name = fmt.Sprintf("%s%s.%s/%s", namePrefix, node.Kind, node.Group, node.Name) 449 | default: 450 | name = fmt.Sprintf("%s%s/%s", namePrefix, node.Kind, node.Name) 451 | } 452 | switch { 453 | case node.Group == corev1.GroupName && node.Kind == "Event": 454 | ready, status, _ = getEventCoreReadyStatus(node.Unstructured) 455 | case node.Group == corev1.GroupName && node.Kind == "Pod": 456 | ready, status, _ = getPodReadyStatus(node.Unstructured) 457 | case node.Group == corev1.GroupName && node.Kind == "ReplicationController": 458 | ready, status, _ = getReplicationControllerReadyStatus(node.Unstructured) 459 | case node.Group == appsv1.GroupName && node.Kind == "DaemonSet": 460 | ready, status, _ = getDaemonSetReadyStatus(node.Unstructured) 461 | case node.Group == appsv1.GroupName && node.Kind == "Deployment": 462 | ready, status, _ = getDeploymentReadyStatus(node.Unstructured) 463 | case node.Group == appsv1.GroupName && node.Kind == "ReplicaSet": 464 | ready, status, _ = getReplicaSetReadyStatus(node.Unstructured) 465 | case node.Group == appsv1.GroupName && node.Kind == "StatefulSet": 466 | ready, status, _ = getStatefulSetReadyStatus(node.Unstructured) 467 | case node.Group == policyv1.GroupName && node.Kind == "PodDisruptionBudget": 468 | ready, status, _ = getPodDisruptionBudgetReadyStatus(node.Unstructured) 469 | case node.Group == apiregistrationv1.GroupName && node.Kind == "APIService": 470 | ready, status, _ = getAPIServiceReadyStatus(node.Unstructured) 471 | case node.Group == eventsv1.GroupName && node.Kind == "Event": 472 | ready, status, _ = getEventReadyStatus(node.Unstructured) 473 | case node.Group == storagev1.GroupName && node.Kind == "VolumeAttachment": 474 | ready, status, _ = getVolumeAttachmentReadyStatus(node.Unstructured) 475 | case node.Unstructured != nil: 476 | ready, status, _ = getObjectReadyStatus(node.Unstructured) 477 | } 478 | if len(ready) == 0 { 479 | ready = cellNotApplicable 480 | } 481 | if node.Unstructured != nil { 482 | age = translateTimestampSince(node.GetCreationTimestamp()) 483 | } 484 | relationships = []string{} 485 | if rset != nil { 486 | relationships = rset.List() 487 | } 488 | 489 | return metav1.TableRow{ 490 | Object: runtime.RawExtension{Object: node.DeepCopyObject()}, 491 | Cells: []interface{}{ 492 | name, 493 | ready, 494 | status, 495 | age, 496 | relationships, 497 | }, 498 | } 499 | } 500 | 501 | // nodeMapToTable converts the provided node & either its dependencies or 502 | // dependents into table rows. 503 | func nodeMapToTable( 504 | nodeMap graph.NodeMap, 505 | root *graph.Node, 506 | maxDepth uint, 507 | depsIsDependencies bool, 508 | showGroupFn func(kind string) bool) (*metav1.Table, error) { 509 | // Sorts the list of UIDs based on the underlying object in following order: 510 | // Namespace, Kind, Group, Name 511 | sortDepsFn := func(d map[types.UID]graph.RelationshipSet) []types.UID { 512 | nodes, ix := make(graph.NodeList, len(d)), 0 513 | for uid := range d { 514 | nodes[ix] = nodeMap[uid] 515 | ix++ 516 | } 517 | sort.Sort(nodes) 518 | sortedUIDs := make([]types.UID, len(d)) 519 | for ix, node := range nodes { 520 | sortedUIDs[ix] = node.UID 521 | } 522 | return sortedUIDs 523 | } 524 | 525 | var rows []metav1.TableRow 526 | row := nodeToTableRow(root, nil, "", showGroupFn) 527 | uidSet := map[types.UID]struct{}{} 528 | depRows, err := nodeDepsToTableRows(nodeMap, uidSet, root, "", 1, maxDepth, depsIsDependencies, sortDepsFn, showGroupFn) 529 | if err != nil { 530 | return nil, err 531 | } 532 | rows = append(rows, row) 533 | rows = append(rows, depRows...) 534 | table := metav1.Table{ 535 | ColumnDefinitions: objectColumnDefinitions, 536 | Rows: rows, 537 | } 538 | 539 | return &table, nil 540 | } 541 | 542 | // nodeDepsToTableRows converts either the dependencies or dependents of the 543 | // provided node into table rows. 544 | func nodeDepsToTableRows( 545 | nodeMap graph.NodeMap, 546 | uidSet map[types.UID]struct{}, 547 | node *graph.Node, 548 | prefix string, 549 | depth uint, 550 | maxDepth uint, 551 | depsIsDependencies bool, 552 | sortDepsFn func(d map[types.UID]graph.RelationshipSet) []types.UID, 553 | showGroupFn func(kind string) bool) ([]metav1.TableRow, error) { 554 | rows := make([]metav1.TableRow, 0, len(nodeMap)) 555 | 556 | // Guard against possible cycles 557 | if _, ok := uidSet[node.UID]; ok { 558 | return rows, nil 559 | } 560 | uidSet[node.UID] = struct{}{} 561 | 562 | deps := node.GetDeps(depsIsDependencies) 563 | depUIDs := sortDepsFn(deps) 564 | lastIx := len(depUIDs) - 1 565 | for ix, childUID := range depUIDs { 566 | var childPrefix, depPrefix string 567 | if ix != lastIx { 568 | childPrefix, depPrefix = prefix+"├── ", prefix+"│ " 569 | } else { 570 | childPrefix, depPrefix = prefix+"└── ", prefix+" " 571 | } 572 | 573 | child, ok := nodeMap[childUID] 574 | if !ok { 575 | return nil, fmt.Errorf("dependent object (uid: %s) not found in list of fetched objects", childUID) 576 | } 577 | rset, ok := deps[childUID] 578 | if !ok { 579 | return nil, fmt.Errorf("dependent object (uid: %s) not found", childUID) 580 | } 581 | row := nodeToTableRow(child, rset, childPrefix, showGroupFn) 582 | rows = append(rows, row) 583 | if maxDepth == 0 || depth < maxDepth { 584 | depRows, err := nodeDepsToTableRows(nodeMap, uidSet, child, depPrefix, depth+1, maxDepth, depsIsDependencies, sortDepsFn, showGroupFn) 585 | if err != nil { 586 | return nil, err 587 | } 588 | rows = append(rows, depRows...) 589 | } 590 | } 591 | 592 | return rows, nil 593 | } 594 | 595 | // translateTimestampSince returns the elapsed time since timestamp in 596 | // human-readable approximation. 597 | func translateTimestampSince(timestamp metav1.Time) string { 598 | if timestamp.IsZero() { 599 | return cellUnknown 600 | } 601 | 602 | return duration.HumanDuration(time.Since(timestamp.Time)) 603 | } 604 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | gitVersion string // semantic version, derived by build scripts 10 | gitVersionMajor string // major version, always numeric 11 | gitVersionMinor string // minor version, always numeric 12 | gitCommit string // sha1 from git, output of $(git rev-parse HEAD) 13 | gitTreeState string // state of git tree, either "clean" or "dirty" 14 | buildDate string // build date in rfc3339 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 15 | ) 16 | 17 | // Info defines the version. 18 | type Info struct { 19 | Major string `json:"major,omitempty"` 20 | Minor string `json:"minor,omitempty"` 21 | GitVersion string `json:"gitVersion,omitempty"` 22 | GitCommit string `json:"gitCommit,omitempty"` 23 | GitTreeState string `json:"gitTreeState,omitempty"` 24 | BuildDate string `json:"buildDate,omitempty"` 25 | GoVersion string `json:"goVersion,omitempty"` 26 | Compiler string `json:"compiler,omitempty"` 27 | Platform string `json:"platform,omitempty"` 28 | } 29 | 30 | // Get returns metadata and information regarding the version. 31 | func Get() Info { 32 | return Info{ 33 | Major: gitVersionMajor, 34 | Minor: gitVersionMinor, 35 | GitVersion: gitVersion, 36 | GitCommit: gitCommit, 37 | GitTreeState: gitTreeState, 38 | BuildDate: buildDate, 39 | GoVersion: runtime.Version(), 40 | Compiler: runtime.Compiler, 41 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 42 | } 43 | } 44 | 45 | // String returns info as a human-friendly version string. 46 | func (info Info) String() string { 47 | return info.GitVersion 48 | } 49 | -------------------------------------------------------------------------------- /pkg/cmd/helm/completion.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "helm.sh/helm/v3/pkg/action" 8 | ) 9 | 10 | // Provide dynamic auto-completion for release names. 11 | // 12 | // Based off `compListReleases` from https://github.com/helm/helm/blob/v3.7.0/cmd/helm/list.go#L221-L243 13 | func compGetHelmReleaseList(opts *CmdOptions, toComplete string) []string { 14 | cobra.CompDebugln(fmt.Sprintf("completeHelm with \"%s\"", toComplete), false) 15 | if err := opts.Complete(nil, nil); err != nil { 16 | return nil 17 | } 18 | helmClient := action.NewList(opts.ActionConfig) 19 | helmClient.All = true 20 | helmClient.Limit = 0 21 | helmClient.Filter = fmt.Sprintf("^%s", toComplete) 22 | helmClient.SetStateMask() 23 | 24 | var choices []string 25 | releases, err := helmClient.Run() 26 | if err != nil { 27 | cobra.CompErrorln(fmt.Sprintf("Failed to list releases: %s", err)) 28 | return nil 29 | } 30 | for _, rel := range releases { 31 | choices = append(choices, 32 | fmt.Sprintf("%s\t%s-%s -> %s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version, rel.Info.Status.String())) 33 | } 34 | if len(choices) == 0 { 35 | cobra.CompDebugln("No releases found", false) 36 | return nil 37 | } 38 | 39 | return choices 40 | } 41 | -------------------------------------------------------------------------------- /pkg/cmd/helm/flags.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 9 | 10 | "github.com/tohjustin/kube-lineage/internal/completion" 11 | ) 12 | 13 | const ( 14 | flagAllNamespaces = "all-namespaces" 15 | flagAllNamespacesShorthand = "A" 16 | flagDepth = "depth" 17 | flagDepthShorthand = "d" 18 | flagExcludeTypes = "exclude-types" 19 | flagIncludeTypes = "include-types" 20 | flagScopes = "scopes" 21 | flagScopesShorthand = "S" 22 | ) 23 | 24 | // Flags composes common configuration flag structs used in the command. 25 | type Flags struct { 26 | AllNamespaces *bool 27 | Depth *uint 28 | ExcludeTypes *[]string 29 | IncludeTypes *[]string 30 | Scopes *[]string 31 | } 32 | 33 | // Copy returns a copy of Flags for mutation. 34 | func (f *Flags) Copy() Flags { 35 | Flags := *f 36 | return Flags 37 | } 38 | 39 | // AddFlags receives a *pflag.FlagSet reference and binds flags related to 40 | // configuration to it. 41 | func (f *Flags) AddFlags(flags *pflag.FlagSet) { 42 | if f.AllNamespaces != nil { 43 | flags.BoolVarP(f.AllNamespaces, flagAllNamespaces, flagAllNamespacesShorthand, *f.AllNamespaces, "If present, list object relationships across all namespaces") 44 | } 45 | if f.Depth != nil { 46 | flags.UintVarP(f.Depth, flagDepth, flagDepthShorthand, *f.Depth, "Maximum depth to find relationships") 47 | } 48 | if f.ExcludeTypes != nil { 49 | usage := fmt.Sprintf("Accepts a comma separated list of resource types to exclude from relationship discovery. You can also use multiple flag options like --%s kind1 --%s kind1...", flagExcludeTypes, flagExcludeTypes) 50 | flags.StringSliceVar(f.ExcludeTypes, flagExcludeTypes, *f.ExcludeTypes, usage) 51 | } 52 | if f.IncludeTypes != nil { 53 | usage := fmt.Sprintf("Accepts a comma separated list of resource types to only include in relationship discovery. You can also use multiple flag options like --%s kind1 --%s kind1...", flagIncludeTypes, flagIncludeTypes) 54 | flags.StringSliceVar(f.IncludeTypes, flagIncludeTypes, *f.IncludeTypes, usage) 55 | } 56 | if f.Scopes != nil { 57 | usage := fmt.Sprintf("Accepts a comma separated list of additional namespaces to find relationships. You can also use multiple flag options like -%s namespace1 -%s namespace2...", flagScopesShorthand, flagScopesShorthand) 58 | flags.StringSliceVarP(f.Scopes, flagScopes, flagScopesShorthand, *f.Scopes, usage) 59 | } 60 | } 61 | 62 | // RegisterFlagCompletionFunc receives a *cobra.Command & register functions to 63 | // to provide completion for flags related to configuration. 64 | func (*Flags) RegisterFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { 65 | cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 66 | flagScopes, 67 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 68 | return completion.GetScopeNamespaceList(f, cmd, toComplete), cobra.ShellCompDirectiveNoFileComp 69 | })) 70 | } 71 | 72 | // NewConfigFlags returns flags associated with command configuration, 73 | // with default values set. 74 | func NewFlags() *Flags { 75 | allNamespaces := false 76 | depth := uint(0) 77 | excludeTypes := []string{} 78 | includeTypes := []string{} 79 | scopes := []string{} 80 | 81 | return &Flags{ 82 | AllNamespaces: &allNamespaces, 83 | Depth: &depth, 84 | ExcludeTypes: &excludeTypes, 85 | IncludeTypes: &includeTypes, 86 | Scopes: &scopes, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /pkg/cmd/helm/helm.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "helm.sh/helm/v3/pkg/action" 11 | "helm.sh/helm/v3/pkg/release" 12 | "helm.sh/helm/v3/pkg/storage" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/types" 17 | "k8s.io/cli-runtime/pkg/genericclioptions" 18 | "k8s.io/cli-runtime/pkg/resource" 19 | "k8s.io/klog/v2" 20 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 21 | "k8s.io/kubectl/pkg/util" 22 | "k8s.io/kubectl/pkg/util/templates" 23 | 24 | "github.com/tohjustin/kube-lineage/internal/client" 25 | "github.com/tohjustin/kube-lineage/internal/graph" 26 | "github.com/tohjustin/kube-lineage/internal/log" 27 | lineageprinters "github.com/tohjustin/kube-lineage/internal/printers" 28 | ) 29 | 30 | var ( 31 | cmdPath string 32 | cmdName = "helm" 33 | cmdUse = "%CMD% [RELEASE_NAME] [flags]" 34 | cmdExample = templates.Examples(` 35 | # List all resources associated with release named "bar" in the current namespace 36 | %CMD_PATH% bar 37 | 38 | # List all resources associated with release named "bar" in namespace "foo" 39 | %CMD_PATH% bar --namespace=foo 40 | 41 | # List all resources associated with release named "bar" & the corresponding relationship type(s) 42 | %CMD_PATH% bar --output=wide 43 | 44 | # List all resources associated with release named "bar", excluding event & secret resource types 45 | %CMD_PATH% pv/disk --dependencies --exclude-types=ev,secret 46 | 47 | # List only resources provisioned by the release named "bar" 48 | %CMD_PATH% bar --depth=1`) 49 | cmdShort = "Display resources associated with a Helm release & their dependents" 50 | cmdLong = templates.LongDesc(` 51 | Display resources associated with a Helm release & their dependents. 52 | 53 | RELEASE_NAME is the name of a particular Helm release.`) 54 | ) 55 | 56 | // CmdOptions contains all the options for running the helm command. 57 | type CmdOptions struct { 58 | // RequestRelease represents the requested Helm release. 59 | RequestRelease string 60 | Flags *Flags 61 | 62 | Namespace string 63 | HelmDriver string 64 | ActionConfig *action.Configuration 65 | Client client.Interface 66 | ClientFlags *client.Flags 67 | 68 | Printer lineageprinters.Interface 69 | PrintFlags *lineageprinters.Flags 70 | 71 | genericclioptions.IOStreams 72 | } 73 | 74 | // NewCmd returns an initialized Command for the helm command. 75 | func NewCmd(streams genericclioptions.IOStreams, name, parentCmdPath string) *cobra.Command { 76 | o := &CmdOptions{ 77 | Flags: NewFlags(), 78 | ClientFlags: client.NewFlags(), 79 | PrintFlags: lineageprinters.NewFlags(), 80 | IOStreams: streams, 81 | } 82 | 83 | f := cmdutil.NewFactory(o.ClientFlags) 84 | util.SetFactoryForCompletion(f) 85 | 86 | if len(name) > 0 { 87 | cmdName = name 88 | } 89 | cmdPath = cmdName 90 | if len(parentCmdPath) > 0 { 91 | cmdPath = parentCmdPath + " " + cmdName 92 | } 93 | cmd := &cobra.Command{ 94 | Use: strings.ReplaceAll(cmdUse, "%CMD%", cmdName), 95 | Example: strings.ReplaceAll(cmdExample, "%CMD_PATH%", cmdPath), 96 | Short: cmdShort, 97 | Long: cmdLong, 98 | Args: cobra.MaximumNArgs(1), 99 | DisableFlagsInUseLine: true, 100 | DisableSuggestions: true, 101 | SilenceUsage: true, 102 | Run: func(c *cobra.Command, args []string) { 103 | klog.V(4).Infof("Version: %s", c.Root().Version) 104 | cmdutil.CheckErr(o.Complete(c, args)) 105 | cmdutil.CheckErr(o.Validate()) 106 | cmdutil.CheckErr(o.Run()) 107 | }, 108 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 109 | var comp []string 110 | if len(args) == 0 { 111 | comp = compGetHelmReleaseList(o, toComplete) 112 | } 113 | return comp, cobra.ShellCompDirectiveNoFileComp 114 | }, 115 | } 116 | 117 | // Setup flags 118 | o.Flags.AddFlags(cmd.Flags()) 119 | o.ClientFlags.AddFlags(cmd.Flags()) 120 | o.PrintFlags.AddFlags(cmd.Flags()) 121 | log.AddFlags(cmd.Flags()) 122 | 123 | // Setup flag completion function 124 | o.Flags.RegisterFlagCompletionFunc(cmd, f) 125 | o.ClientFlags.RegisterFlagCompletionFunc(cmd, f) 126 | 127 | return cmd 128 | } 129 | 130 | // Complete completes all the required options for the helm command. 131 | func (o *CmdOptions) Complete(cmd *cobra.Command, args []string) error { 132 | var err error 133 | 134 | //nolint:gocritic 135 | switch len(args) { 136 | case 1: 137 | o.RequestRelease = args[0] 138 | } 139 | 140 | // Setup client 141 | o.Namespace, _, err = o.ClientFlags.ToRawKubeConfigLoader().Namespace() 142 | if err != nil { 143 | return err 144 | } 145 | o.Client, err = o.ClientFlags.ToClient() 146 | if err != nil { 147 | return err 148 | } 149 | o.HelmDriver = os.Getenv("HELM_DRIVER") 150 | o.ActionConfig = new(action.Configuration) 151 | err = o.ActionConfig.Init(o.ClientFlags, o.Namespace, o.HelmDriver, klog.V(4).Infof) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | // Setup printer 157 | o.Printer, err = o.PrintFlags.ToPrinter(o.Client) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // Validate validates all the required options for the helm command. 166 | func (o *CmdOptions) Validate() error { 167 | if len(o.RequestRelease) == 0 { 168 | return fmt.Errorf("release name must be specified\nSee '%s -h' for help and examples", cmdPath) 169 | } 170 | 171 | klog.V(4).Infof("Namespace: %s", o.Namespace) 172 | klog.V(4).Infof("RequestRelease: %v", o.RequestRelease) 173 | klog.V(4).Infof("Flags.AllNamespaces: %t", *o.Flags.AllNamespaces) 174 | klog.V(4).Infof("Flags.Depth: %v", *o.Flags.Depth) 175 | klog.V(4).Infof("Flags.ExcludeTypes: %v", *o.Flags.ExcludeTypes) 176 | klog.V(4).Infof("Flags.IncludeTypes: %v", *o.Flags.IncludeTypes) 177 | klog.V(4).Infof("Flags.Scopes: %v", *o.Flags.Scopes) 178 | klog.V(4).Infof("ClientFlags.Context: %s", *o.ClientFlags.Context) 179 | klog.V(4).Infof("ClientFlags.Namespace: %s", *o.ClientFlags.Namespace) 180 | klog.V(4).Infof("PrintFlags.OutputFormat: %s", *o.PrintFlags.OutputFormat) 181 | klog.V(4).Infof("PrintFlags.NoHeaders: %t", *o.PrintFlags.HumanReadableFlags.NoHeaders) 182 | klog.V(4).Infof("PrintFlags.ShowGroup: %t", *o.PrintFlags.HumanReadableFlags.ShowGroup) 183 | klog.V(4).Infof("PrintFlags.ShowLabels: %t", *o.PrintFlags.HumanReadableFlags.ShowLabels) 184 | klog.V(4).Infof("PrintFlags.ShowNamespace: %t", *o.PrintFlags.HumanReadableFlags.ShowNamespace) 185 | 186 | return nil 187 | } 188 | 189 | // Run implements all the necessary functionality for the helm command. 190 | //nolint:funlen,gocognit,gocyclo 191 | func (o *CmdOptions) Run() error { 192 | ctx := context.Background() 193 | 194 | // First check if Kubernetes cluster is reachable 195 | if err := o.Client.IsReachable(); err != nil { 196 | return err 197 | } 198 | 199 | // Fetch the release to ensure it exists before proceeding 200 | helmClient := action.NewGet(o.ActionConfig) 201 | rls, err := helmClient.Run(o.RequestRelease) 202 | if err != nil { 203 | return err 204 | } 205 | klog.V(4).Infof("Release manifest:\n%s\n", rls.Manifest) 206 | 207 | // Fetch all Helm release objects (i.e. resources found in the helm release 208 | // manifests) from the cluster 209 | rlsObjs, err := o.getManifestObjects(ctx, rls) 210 | if err != nil { 211 | return err 212 | } 213 | klog.V(4).Infof("Got %d objects from release manifest", len(rlsObjs)) 214 | 215 | // Fetch the Helm storage object 216 | stgObj, err := o.getStorageObject(ctx, rls) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | // Determine resources to list 222 | excludeAPIs := []client.APIResource{} 223 | if o.Flags.ExcludeTypes != nil { 224 | for _, kind := range *o.Flags.ExcludeTypes { 225 | api, err := o.Client.ResolveAPIResource(kind) 226 | if err != nil { 227 | return err 228 | } 229 | excludeAPIs = append(excludeAPIs, *api) 230 | } 231 | } 232 | includeAPIs := []client.APIResource{} 233 | if o.Flags.IncludeTypes != nil { 234 | for _, kind := range *o.Flags.IncludeTypes { 235 | api, err := o.Client.ResolveAPIResource(kind) 236 | if err != nil { 237 | return err 238 | } 239 | includeAPIs = append(includeAPIs, *api) 240 | } 241 | } 242 | 243 | // Keep only objects that matches any included resource type 244 | if len(includeAPIs) > 0 { 245 | includeGKSet := client.ResourcesToGroupKindSet(includeAPIs) 246 | newRlsObjs := []unstructuredv1.Unstructured{} 247 | for _, i := range rlsObjs { 248 | if _, ok := includeGKSet[i.GroupVersionKind().GroupKind()]; ok { 249 | newRlsObjs = append(newRlsObjs, i) 250 | } 251 | } 252 | rlsObjs = newRlsObjs 253 | if stgObj != nil { 254 | if _, ok := includeGKSet[stgObj.GroupVersionKind().GroupKind()]; !ok { 255 | stgObj = nil 256 | } 257 | } 258 | } 259 | // Filter out objects that matches any excluded resource type 260 | if len(excludeAPIs) > 0 { 261 | excludeGKSet := client.ResourcesToGroupKindSet(excludeAPIs) 262 | newRlsObjs := []unstructuredv1.Unstructured{} 263 | for _, i := range rlsObjs { 264 | if _, ok := excludeGKSet[i.GroupVersionKind().GroupKind()]; !ok { 265 | newRlsObjs = append(newRlsObjs, i) 266 | } 267 | } 268 | rlsObjs = newRlsObjs 269 | if stgObj != nil { 270 | if _, ok := excludeGKSet[stgObj.GroupVersionKind().GroupKind()]; ok { 271 | stgObj = nil 272 | } 273 | } 274 | } 275 | 276 | // Determine the namespaces to list objects 277 | var namespaces []string 278 | nsSet := map[string]struct{}{o.Namespace: {}} 279 | for _, obj := range rlsObjs { 280 | nsSet[obj.GetNamespace()] = struct{}{} 281 | } 282 | for ns := range nsSet { 283 | namespaces = append(namespaces, ns) 284 | } 285 | if o.Flags.AllNamespaces != nil && *o.Flags.AllNamespaces { 286 | namespaces = append(namespaces, "") 287 | } 288 | if o.Flags.Scopes != nil { 289 | namespaces = append(namespaces, *o.Flags.Scopes...) 290 | } 291 | 292 | // Fetch resources in the cluster 293 | objs, err := o.Client.List(ctx, client.ListOptions{ 294 | APIResourcesToExclude: excludeAPIs, 295 | APIResourcesToInclude: includeAPIs, 296 | Namespaces: namespaces, 297 | }) 298 | if err != nil { 299 | return err 300 | } 301 | 302 | // Include release & secret objects into objects to handle cases where user 303 | // has access to get them individually but unable to list their respective 304 | // resource types 305 | objs.Items = append(objs.Items, rlsObjs...) 306 | if stgObj != nil { 307 | objs.Items = append(objs.Items, *stgObj) 308 | } 309 | 310 | // Collect UIDs from release & storage objects 311 | var uids []types.UID 312 | for _, obj := range rlsObjs { 313 | uids = append(uids, obj.GetUID()) 314 | } 315 | if stgObj != nil { 316 | uids = append(uids, stgObj.GetUID()) 317 | } 318 | 319 | // Find all dependents of the release & storage objects 320 | mapper := o.Client.GetMapper() 321 | nodeMap, err := graph.ResolveDependents(mapper, objs.Items, uids) 322 | if err != nil { 323 | return err 324 | } 325 | 326 | // Add the Helm release object to the root of the relationship tree 327 | rootNode := newReleaseNode(rls) 328 | for _, obj := range rlsObjs { 329 | rootNode.AddDependent(obj.GetUID(), graph.RelationshipHelmRelease) 330 | } 331 | if stgObj != nil { 332 | rootNode.AddDependent(stgObj.GetUID(), graph.RelationshipHelmStorage) 333 | } 334 | for _, node := range nodeMap { 335 | node.Depth++ 336 | } 337 | rootUID := rootNode.GetUID() 338 | nodeMap[rootUID] = rootNode 339 | 340 | // Print output 341 | return o.Printer.Print(o.Out, nodeMap, rootUID, *o.Flags.Depth, false) 342 | } 343 | 344 | // getManifestObjects fetches all objects found in the manifest of the provided 345 | // Helm release. 346 | func (o *CmdOptions) getManifestObjects(_ context.Context, rls *release.Release) ([]unstructuredv1.Unstructured, error) { 347 | var objs []unstructuredv1.Unstructured 348 | name, ns := rls.Name, rls.Namespace 349 | r := strings.NewReader(rls.Manifest) 350 | source := fmt.Sprintf("manifest for release \"%s\" in the namespace \"%s\"", name, ns) 351 | result := resource.NewBuilder(o.ActionConfig.RESTClientGetter). 352 | Unstructured(). 353 | NamespaceParam(ns). 354 | DefaultNamespace(). 355 | ContinueOnError(). 356 | Latest(). 357 | Flatten(). 358 | Stream(r, source). 359 | Do() 360 | infos, err := result.Infos() 361 | if err != nil { 362 | return nil, err 363 | } 364 | for _, info := range infos { 365 | u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) 366 | if err != nil { 367 | return nil, err 368 | } 369 | objs = append(objs, unstructuredv1.Unstructured{Object: u}) 370 | } 371 | 372 | return objs, nil 373 | } 374 | 375 | // getStorageObject fetches the underlying object that stores the information of 376 | // the provided Helm release. 377 | func (o *CmdOptions) getStorageObject(ctx context.Context, rls *release.Release) (*unstructuredv1.Unstructured, error) { 378 | var api client.APIResource 379 | switch o.HelmDriver { 380 | case "secret", "": 381 | api = client.APIResource{Version: "v1", Kind: "Secret", Name: "secrets", Namespaced: true} 382 | case "configmap": 383 | api = client.APIResource{Version: "v1", Kind: "ConfigMap", Name: "configmaps", Namespaced: true} 384 | case "memory", "sql": 385 | return nil, nil 386 | default: 387 | return nil, fmt.Errorf("helm driver \"%s\" not supported", o.HelmDriver) 388 | } 389 | return o.Client.Get(ctx, makeKey(rls.Name, rls.Version), client.GetOptions{ 390 | APIResource: api, 391 | Namespace: o.Namespace, 392 | }) 393 | } 394 | 395 | // getReleaseReadyStatus returns the ready & status value of a Helm release 396 | // object. 397 | func getReleaseReadyStatus(rls *release.Release) (string, string) { 398 | switch rls.Info.Status { 399 | case release.StatusDeployed: 400 | return "True", "Deployed" 401 | case release.StatusFailed: 402 | return "False", "Failed" 403 | case release.StatusPendingInstall: 404 | return "False", "PendingInstall" 405 | case release.StatusPendingRollback: 406 | return "False", "PendingRollback" 407 | case release.StatusPendingUpgrade: 408 | return "False", "PendingUpgrade" 409 | case release.StatusSuperseded: 410 | return "False", "Superseded" 411 | case release.StatusUninstalled: 412 | return "False", "Uninstalled" 413 | case release.StatusUninstalling: 414 | return "False", "Uninstalling" 415 | case release.StatusUnknown: 416 | fallthrough 417 | default: 418 | return "False", "Unknown" 419 | } 420 | } 421 | 422 | // newReleaseNode converts a Helm release object into a Node in the relationship 423 | // tree. 424 | func newReleaseNode(rls *release.Release) *graph.Node { 425 | root := new(unstructuredv1.Unstructured) 426 | ready, status := getReleaseReadyStatus(rls) 427 | // Set "Ready" condition values based on the printer.objectReadyReasonJSONPath 428 | // & printer.objectReadyStatusJSONPath paths 429 | root.SetUnstructuredContent( 430 | map[string]interface{}{ 431 | "status": map[string]interface{}{ 432 | "conditions": []interface{}{ 433 | map[string]interface{}{ 434 | "type": "Ready", 435 | "status": ready, 436 | "reason": status, 437 | }, 438 | }, 439 | }, 440 | }, 441 | ) 442 | root.SetUID(types.UID("")) 443 | root.SetName(rls.Name) 444 | root.SetNamespace(rls.Namespace) 445 | root.SetCreationTimestamp(metav1.Time{Time: rls.Info.FirstDeployed.Time}) 446 | 447 | return &graph.Node{ 448 | Unstructured: root, 449 | UID: root.GetUID(), 450 | Name: root.GetName(), 451 | Namespace: root.GetNamespace(), 452 | Dependents: map[types.UID]graph.RelationshipSet{}, 453 | } 454 | } 455 | 456 | // makeKey concatenates the Kubernetes storage object type, a release name and version 457 | // into a string with format:```..v```. 458 | // The storage type is prepended to keep name uniqueness between different 459 | // release storage types. An example of clash when not using the type: 460 | // https://github.com/helm/helm/issues/6435. 461 | // This key is used to uniquely identify storage objects. 462 | // 463 | // NOTE: Unfortunately the makeKey function isn't exported by the 464 | // helm.sh/helm/v3/pkg/storage package so we will have to copy-paste it here. 465 | // ref: https://github.com/helm/helm/blob/v3.7.0/pkg/storage/storage.go#L245-L253 466 | func makeKey(rlsname string, version int) string { 467 | return fmt.Sprintf("%s.%s.v%d", storage.HelmStorageType, rlsname, version) 468 | } 469 | -------------------------------------------------------------------------------- /pkg/cmd/lineage/completion.go: -------------------------------------------------------------------------------- 1 | package lineage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // compGetResourceList provides dynamic auto-completion for resource names. 11 | func compGetResourceList(opts *CmdOptions, toComplete string) []string { 12 | cobra.CompDebugln(fmt.Sprintf("compGetResourceList with \"%s\"", toComplete), false) 13 | if err := opts.Complete(nil, nil); err != nil { 14 | return nil 15 | } 16 | 17 | var choices []string 18 | apis, err := opts.Client.GetAPIResources(context.Background()) 19 | if err != nil { 20 | cobra.CompErrorln(fmt.Sprintf("Failed to list API resources: %s", err)) 21 | return nil 22 | } 23 | for _, api := range apis { 24 | choices = append(choices, api.WithGroupString()) 25 | } 26 | if len(choices) == 0 { 27 | cobra.CompDebugln("No API resources found", false) 28 | return nil 29 | } 30 | 31 | return choices 32 | } 33 | -------------------------------------------------------------------------------- /pkg/cmd/lineage/flags.go: -------------------------------------------------------------------------------- 1 | package lineage 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 9 | 10 | "github.com/tohjustin/kube-lineage/internal/completion" 11 | ) 12 | 13 | const ( 14 | flagAllNamespaces = "all-namespaces" 15 | flagAllNamespacesShorthand = "A" 16 | flagDependencies = "dependencies" 17 | flagDependenciesShorthand = "D" 18 | flagDepth = "depth" 19 | flagDepthShorthand = "d" 20 | flagExcludeTypes = "exclude-types" 21 | flagIncludeTypes = "include-types" 22 | flagScopes = "scopes" 23 | flagScopesShorthand = "S" 24 | ) 25 | 26 | // Flags composes common configuration flag structs used in the command. 27 | type Flags struct { 28 | AllNamespaces *bool 29 | Dependencies *bool 30 | Depth *uint 31 | ExcludeTypes *[]string 32 | IncludeTypes *[]string 33 | Scopes *[]string 34 | } 35 | 36 | // Copy returns a copy of Flags for mutation. 37 | func (f *Flags) Copy() Flags { 38 | Flags := *f 39 | return Flags 40 | } 41 | 42 | // AddFlags receives a *pflag.FlagSet reference and binds flags related to 43 | // configuration to it. 44 | func (f *Flags) AddFlags(flags *pflag.FlagSet) { 45 | if f.AllNamespaces != nil { 46 | flags.BoolVarP(f.AllNamespaces, flagAllNamespaces, flagAllNamespacesShorthand, *f.AllNamespaces, "If present, list object relationships across all namespaces") 47 | } 48 | if f.Dependencies != nil { 49 | flags.BoolVarP(f.Dependencies, flagDependencies, flagDependenciesShorthand, *f.Dependencies, "If present, list object dependencies instead of dependents") 50 | } 51 | if f.Depth != nil { 52 | flags.UintVarP(f.Depth, flagDepth, flagDepthShorthand, *f.Depth, "Maximum depth to find relationships") 53 | } 54 | if f.ExcludeTypes != nil { 55 | usage := fmt.Sprintf("Accepts a comma separated list of resource types to exclude from relationship discovery. You can also use multiple flag options like --%s kind1 --%s kind1...", flagExcludeTypes, flagExcludeTypes) 56 | flags.StringSliceVar(f.ExcludeTypes, flagExcludeTypes, *f.ExcludeTypes, usage) 57 | } 58 | if f.IncludeTypes != nil { 59 | usage := fmt.Sprintf("Accepts a comma separated list of resource types to only include in relationship discovery. You can also use multiple flag options like --%s kind1 --%s kind1...", flagIncludeTypes, flagIncludeTypes) 60 | flags.StringSliceVar(f.IncludeTypes, flagIncludeTypes, *f.IncludeTypes, usage) 61 | } 62 | if f.Scopes != nil { 63 | usage := fmt.Sprintf("Accepts a comma separated list of additional namespaces to find relationships. You can also use multiple flag options like -%s namespace1 -%s namespace2...", flagScopesShorthand, flagScopesShorthand) 64 | flags.StringSliceVarP(f.Scopes, flagScopes, flagScopesShorthand, *f.Scopes, usage) 65 | } 66 | } 67 | 68 | // RegisterFlagCompletionFunc receives a *cobra.Command & register functions to 69 | // to provide completion for flags related to configuration. 70 | func (*Flags) RegisterFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { 71 | cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( 72 | flagScopes, 73 | func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 74 | return completion.GetScopeNamespaceList(f, cmd, toComplete), cobra.ShellCompDirectiveNoFileComp 75 | })) 76 | } 77 | 78 | // NewFlags returns flags associated with command configuration, with default 79 | // values set. 80 | func NewFlags() *Flags { 81 | allNamespaces := false 82 | dependencies := false 83 | depth := uint(0) 84 | excludeTypes := []string{} 85 | includeTypes := []string{} 86 | scopes := []string{} 87 | 88 | return &Flags{ 89 | AllNamespaces: &allNamespaces, 90 | Dependencies: &dependencies, 91 | Depth: &depth, 92 | ExcludeTypes: &excludeTypes, 93 | IncludeTypes: &includeTypes, 94 | Scopes: &scopes, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/cmd/lineage/lineage.go: -------------------------------------------------------------------------------- 1 | package lineage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "k8s.io/apimachinery/pkg/types" 10 | "k8s.io/cli-runtime/pkg/genericclioptions" 11 | "k8s.io/klog/v2" 12 | "k8s.io/kubectl/pkg/cmd/get" 13 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 14 | "k8s.io/kubectl/pkg/util" 15 | "k8s.io/kubectl/pkg/util/templates" 16 | 17 | "github.com/tohjustin/kube-lineage/internal/client" 18 | "github.com/tohjustin/kube-lineage/internal/graph" 19 | "github.com/tohjustin/kube-lineage/internal/log" 20 | lineageprinters "github.com/tohjustin/kube-lineage/internal/printers" 21 | ) 22 | 23 | var ( 24 | cmdPath string 25 | cmdName = "lineage" 26 | cmdUse = "%CMD% (TYPE[.VERSION][.GROUP] [NAME] | TYPE[.VERSION][.GROUP]/NAME) [flags]" 27 | cmdExample = templates.Examples(` 28 | # List all dependents of the deployment named "bar" in the current namespace 29 | %CMD_PATH% deployments bar 30 | 31 | # List all dependents of the cronjob named "bar" in namespace "foo" 32 | %CMD_PATH% cronjobs.batch/bar --namespace=foo 33 | 34 | # List all dependents of the node named "k3d-dev-server" & the corresponding relationship type(s) 35 | %CMD_PATH% node/k3d-dev-server --output=wide 36 | 37 | # List all dependents of the persistentvolume named "disk", excluding event & secret resource types 38 | %CMD_PATH% pv/disk --dependencies --exclude-types=ev,secret 39 | 40 | # List all dependencies of the pod named "bar-5cc79d4bf5-xgvkc" 41 | %CMD_PATH% pod.v1. bar-5cc79d4bf5-xgvkc --dependencies 42 | 43 | # List all dependencies of the serviceaccount named "default" in the current namespace, grouped by resource type 44 | %CMD_PATH% sa/default --dependencies --output=split`) 45 | cmdShort = "Display all dependencies or dependents of a Kubernetes object" 46 | cmdLong = templates.LongDesc(` 47 | Display all dependencies or dependents of a Kubernetes object. 48 | 49 | TYPE is a Kubernetes resource. Shortcuts and groups will be resolved. 50 | NAME is the name of a particular Kubernetes resource.`) 51 | ) 52 | 53 | // CmdOptions contains all the options for running the lineage command. 54 | type CmdOptions struct { 55 | // RequestType represents the type of the requested object. 56 | RequestType string 57 | // RequestName represents the name of the requested object. 58 | RequestName string 59 | Flags *Flags 60 | 61 | Namespace string 62 | Client client.Interface 63 | ClientFlags *client.Flags 64 | 65 | Printer lineageprinters.Interface 66 | PrintFlags *lineageprinters.Flags 67 | 68 | genericclioptions.IOStreams 69 | } 70 | 71 | // NewCmd returns an initialized Command for the lineage command. 72 | func NewCmd(streams genericclioptions.IOStreams, name, parentCmdPath string) *cobra.Command { 73 | o := &CmdOptions{ 74 | Flags: NewFlags(), 75 | ClientFlags: client.NewFlags(), 76 | PrintFlags: lineageprinters.NewFlags(), 77 | IOStreams: streams, 78 | } 79 | 80 | f := cmdutil.NewFactory(o.ClientFlags) 81 | util.SetFactoryForCompletion(f) 82 | 83 | if len(name) > 0 { 84 | cmdName = name 85 | } 86 | cmdPath = cmdName 87 | if len(parentCmdPath) > 0 { 88 | cmdPath = parentCmdPath + " " + cmdName 89 | } 90 | cmd := &cobra.Command{ 91 | Use: strings.ReplaceAll(cmdUse, "%CMD%", cmdName), 92 | Example: strings.ReplaceAll(cmdExample, "%CMD_PATH%", cmdPath), 93 | Short: cmdShort, 94 | Long: cmdLong, 95 | Args: cobra.MaximumNArgs(2), 96 | DisableFlagsInUseLine: true, 97 | DisableSuggestions: true, 98 | SilenceUsage: true, 99 | Run: func(c *cobra.Command, args []string) { 100 | klog.V(4).Infof("Version: %s", c.Root().Version) 101 | cmdutil.CheckErr(o.Complete(c, args)) 102 | cmdutil.CheckErr(o.Validate()) 103 | cmdutil.CheckErr(o.Run()) 104 | }, 105 | ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 106 | var comps []string 107 | switch len(args) { 108 | case 0: 109 | comps = compGetResourceList(o, toComplete) 110 | case 1: 111 | comps = get.CompGetResource(f, cmd, args[0], toComplete) 112 | } 113 | return comps, cobra.ShellCompDirectiveNoFileComp 114 | }, 115 | } 116 | 117 | // Setup flags 118 | o.Flags.AddFlags(cmd.Flags()) 119 | o.ClientFlags.AddFlags(cmd.Flags()) 120 | o.PrintFlags.AddFlags(cmd.Flags()) 121 | log.AddFlags(cmd.Flags()) 122 | 123 | // Setup flag completion function 124 | o.Flags.RegisterFlagCompletionFunc(cmd, f) 125 | o.ClientFlags.RegisterFlagCompletionFunc(cmd, f) 126 | 127 | return cmd 128 | } 129 | 130 | // Complete completes all the required options for the lineage command. 131 | func (o *CmdOptions) Complete(cmd *cobra.Command, args []string) error { 132 | var err error 133 | 134 | switch len(args) { 135 | case 1: 136 | resourceTokens := strings.SplitN(args[0], "/", 2) 137 | if len(resourceTokens) != 2 { 138 | return fmt.Errorf("arguments in / form must have a single resource and name\nSee '%s -h' for help and examples", cmdPath) 139 | } 140 | o.RequestType = resourceTokens[0] 141 | o.RequestName = resourceTokens[1] 142 | case 2: 143 | o.RequestType = args[0] 144 | o.RequestName = args[1] 145 | } 146 | 147 | // Setup client 148 | o.Namespace, _, err = o.ClientFlags.ToRawKubeConfigLoader().Namespace() 149 | if err != nil { 150 | return err 151 | } 152 | o.Client, err = o.ClientFlags.ToClient() 153 | if err != nil { 154 | return err 155 | } 156 | 157 | // Setup printer 158 | o.Printer, err = o.PrintFlags.ToPrinter(o.Client) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // Validate validates all the required options for the lineage command. 167 | func (o *CmdOptions) Validate() error { 168 | if len(o.RequestType) == 0 || len(o.RequestName) == 0 { 169 | return fmt.Errorf("resource must be specified as or /\nSee '%s -h' for help and examples", cmdPath) 170 | } 171 | 172 | klog.V(4).Infof("Namespace: %s", o.Namespace) 173 | klog.V(4).Infof("RequestType: %v", o.RequestType) 174 | klog.V(4).Infof("RequestName: %v", o.RequestName) 175 | klog.V(4).Infof("Flags.AllNamespaces: %t", *o.Flags.AllNamespaces) 176 | klog.V(4).Infof("Flags.Dependencies: %t", *o.Flags.Dependencies) 177 | klog.V(4).Infof("Flags.Depth: %v", *o.Flags.Depth) 178 | klog.V(4).Infof("Flags.ExcludeTypes: %v", *o.Flags.ExcludeTypes) 179 | klog.V(4).Infof("Flags.IncludeTypes: %v", *o.Flags.IncludeTypes) 180 | klog.V(4).Infof("Flags.Scopes: %v", *o.Flags.Scopes) 181 | klog.V(4).Infof("ClientFlags.Context: %s", *o.ClientFlags.Context) 182 | klog.V(4).Infof("ClientFlags.Namespace: %s", *o.ClientFlags.Namespace) 183 | klog.V(4).Infof("PrintFlags.OutputFormat: %s", *o.PrintFlags.OutputFormat) 184 | klog.V(4).Infof("PrintFlags.NoHeaders: %t", *o.PrintFlags.HumanReadableFlags.NoHeaders) 185 | klog.V(4).Infof("PrintFlags.ShowGroup: %t", *o.PrintFlags.HumanReadableFlags.ShowGroup) 186 | klog.V(4).Infof("PrintFlags.ShowLabels: %t", *o.PrintFlags.HumanReadableFlags.ShowLabels) 187 | klog.V(4).Infof("PrintFlags.ShowNamespace: %t", *o.PrintFlags.HumanReadableFlags.ShowNamespace) 188 | 189 | return nil 190 | } 191 | 192 | // Run implements all the necessary functionality for the lineage command. 193 | //nolint:funlen 194 | func (o *CmdOptions) Run() error { 195 | ctx := context.Background() 196 | 197 | // First check if Kubernetes cluster is reachable 198 | if err := o.Client.IsReachable(); err != nil { 199 | return err 200 | } 201 | 202 | // Fetch the provided object to ensure it exists before proceeding 203 | api, err := o.Client.ResolveAPIResource(o.RequestType) 204 | if err != nil { 205 | return err 206 | } 207 | obj := client.ObjectMeta{ 208 | APIResource: *api, 209 | Name: o.RequestName, 210 | Namespace: o.Namespace, 211 | } 212 | root, err := o.Client.Get(ctx, obj.Name, client.GetOptions{ 213 | APIResource: obj.APIResource, 214 | Namespace: o.Namespace, 215 | }) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | // Determine resources to list 221 | excludeAPIs := []client.APIResource{} 222 | if o.Flags.ExcludeTypes != nil { 223 | for _, kind := range *o.Flags.ExcludeTypes { 224 | api, err := o.Client.ResolveAPIResource(kind) 225 | if err != nil { 226 | return err 227 | } 228 | excludeAPIs = append(excludeAPIs, *api) 229 | } 230 | } 231 | includeAPIs := []client.APIResource{} 232 | if o.Flags.IncludeTypes != nil { 233 | for _, kind := range *o.Flags.IncludeTypes { 234 | api, err := o.Client.ResolveAPIResource(kind) 235 | if err != nil { 236 | return err 237 | } 238 | includeAPIs = append(includeAPIs, *api) 239 | } 240 | } 241 | 242 | // Determine the namespaces to list objects 243 | namespaces := []string{o.Namespace} 244 | if o.Flags.AllNamespaces != nil && *o.Flags.AllNamespaces { 245 | namespaces = append(namespaces, "") 246 | } 247 | if o.Flags.Scopes != nil { 248 | namespaces = append(namespaces, *o.Flags.Scopes...) 249 | } 250 | 251 | // Fetch resources in the cluster 252 | objs, err := o.Client.List(ctx, client.ListOptions{ 253 | APIResourcesToExclude: excludeAPIs, 254 | APIResourcesToInclude: includeAPIs, 255 | Namespaces: namespaces, 256 | }) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | // Include root object into objects to handle cases where user has access 262 | // to get the root object but unable to list its resource type 263 | objs.Items = append(objs.Items, *root) 264 | 265 | // Find either all dependencies or dependents of the root object 266 | depsIsDependencies, resolveDeps := false, graph.ResolveDependents 267 | if o.Flags.Dependencies != nil && *o.Flags.Dependencies { 268 | depsIsDependencies, resolveDeps = true, graph.ResolveDependencies 269 | } 270 | mapper := o.Client.GetMapper() 271 | rootUID := root.GetUID() 272 | nodeMap, err := resolveDeps(mapper, objs.Items, []types.UID{rootUID}) 273 | if err != nil { 274 | return err 275 | } 276 | 277 | // Print output 278 | return o.Printer.Print(o.Out, nodeMap, rootUID, *o.Flags.Depth, depsIsDependencies) 279 | } 280 | -------------------------------------------------------------------------------- /scripts/fetch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ROOT=$(git rev-parse --show-toplevel) 4 | 5 | fetch() { 6 | local tool=$1; shift 7 | local ver=$1; shift 8 | 9 | local ver_cmd="" 10 | local tool_fetch_cmd="" 11 | case "$tool" in 12 | "golangci-lint") 13 | ver_cmd="${ROOT}/bin/golangci-lint --version 2>/dev/null | cut -d' ' -f4" 14 | fetch_cmd="curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v${ver}/install.sh | sh -s -- -b \"${ROOT}/bin\" \"v${ver}\"" 15 | ;; 16 | "goreleaser") 17 | ver_cmd="${ROOT}/bin/goreleaser --version 2>/dev/null | grep 'goreleaser version' | cut -d' ' -f3" 18 | fetch_cmd="cat ${ROOT}/scripts/goreleaser_install.sh | sh -s -- -b \"${ROOT}/bin\" -d \"v${ver}\"" 19 | ;; 20 | *) 21 | echo "unknown tool $tool" 22 | return 1 23 | ;; 24 | esac 25 | 26 | if [[ "${ver}" != "$(eval ${ver_cmd})" ]]; then 27 | echo "${tool} missing or not version '${ver}', downloading..." 28 | eval ${fetch_cmd} 29 | fi 30 | } 31 | -------------------------------------------------------------------------------- /scripts/goreleaser_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader on 2019-12-25T12:47:14Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 139 | } 140 | echoerr() { 141 | echo "$@" 1>&2 142 | } 143 | log_prefix() { 144 | echo "$0" 145 | } 146 | _logp=6 147 | log_set_priority() { 148 | _logp="$1" 149 | } 150 | log_priority() { 151 | if test -z "$1"; then 152 | echo "$_logp" 153 | return 154 | fi 155 | [ "$1" -le "$_logp" ] 156 | } 157 | log_tag() { 158 | case $1 in 159 | 0) echo "emerg" ;; 160 | 1) echo "alert" ;; 161 | 2) echo "crit" ;; 162 | 3) echo "err" ;; 163 | 4) echo "warning" ;; 164 | 5) echo "notice" ;; 165 | 6) echo "info" ;; 166 | 7) echo "debug" ;; 167 | *) echo "$1" ;; 168 | esac 169 | } 170 | log_debug() { 171 | log_priority 7 || return 0 172 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 173 | } 174 | log_info() { 175 | log_priority 6 || return 0 176 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 177 | } 178 | log_err() { 179 | log_priority 3 || return 0 180 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 181 | } 182 | log_crit() { 183 | log_priority 2 || return 0 184 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 185 | } 186 | uname_os() { 187 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 188 | case "$os" in 189 | cygwin_nt*) os="windows" ;; 190 | mingw*) os="windows" ;; 191 | msys_nt*) os="windows" ;; 192 | esac 193 | echo "$os" 194 | } 195 | uname_arch() { 196 | arch=$(uname -m) 197 | case $arch in 198 | x86_64) arch="amd64" ;; 199 | x86) arch="386" ;; 200 | i686) arch="386" ;; 201 | i386) arch="386" ;; 202 | aarch64) arch="arm64" ;; 203 | armv5*) arch="armv5" ;; 204 | armv6*) arch="armv6" ;; 205 | armv7*) arch="armv7" ;; 206 | esac 207 | os=$(uname_os) 208 | case "$os" in 209 | darwin) arch="all" ;; 210 | esac 211 | echo ${arch} 212 | } 213 | uname_os_check() { 214 | os=$(uname_os) 215 | case "$os" in 216 | darwin) return 0 ;; 217 | dragonfly) return 0 ;; 218 | freebsd) return 0 ;; 219 | linux) return 0 ;; 220 | android) return 0 ;; 221 | nacl) return 0 ;; 222 | netbsd) return 0 ;; 223 | openbsd) return 0 ;; 224 | plan9) return 0 ;; 225 | solaris) return 0 ;; 226 | windows) return 0 ;; 227 | all) return 0 ;; 228 | esac 229 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 230 | return 1 231 | } 232 | uname_arch_check() { 233 | arch=$(uname_arch) 234 | case "$arch" in 235 | 386) return 0 ;; 236 | amd64) return 0 ;; 237 | arm64) return 0 ;; 238 | armv5) return 0 ;; 239 | armv6) return 0 ;; 240 | armv7) return 0 ;; 241 | ppc64) return 0 ;; 242 | ppc64le) return 0 ;; 243 | mips) return 0 ;; 244 | mipsle) return 0 ;; 245 | mips64) return 0 ;; 246 | mips64le) return 0 ;; 247 | s390x) return 0 ;; 248 | amd64p32) return 0 ;; 249 | all) return 0 ;; 250 | esac 251 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 252 | return 1 253 | } 254 | untar() { 255 | tarball=$1 256 | case "${tarball}" in 257 | *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; 258 | *.tar) tar --no-same-owner -xf "${tarball}" ;; 259 | *.zip) unzip "${tarball}" ;; 260 | *) 261 | log_err "untar unknown archive format for ${tarball}" 262 | return 1 263 | ;; 264 | esac 265 | } 266 | http_download_curl() { 267 | local_file=$1 268 | source_url=$2 269 | header=$3 270 | if [ -z "$header" ]; then 271 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 272 | else 273 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 274 | fi 275 | if [ "$code" != "200" ]; then 276 | log_debug "http_download_curl received HTTP status $code" 277 | return 1 278 | fi 279 | return 0 280 | } 281 | http_download_wget() { 282 | local_file=$1 283 | source_url=$2 284 | header=$3 285 | if [ -z "$header" ]; then 286 | wget -q -O "$local_file" "$source_url" 287 | else 288 | wget -q --header "$header" -O "$local_file" "$source_url" 289 | fi 290 | } 291 | http_download() { 292 | log_debug "http_download $2" 293 | if is_command curl; then 294 | http_download_curl "$@" 295 | return 296 | elif is_command wget; then 297 | http_download_wget "$@" 298 | return 299 | fi 300 | log_crit "http_download unable to find wget or curl" 301 | return 1 302 | } 303 | http_copy() { 304 | tmp=$(mktemp) 305 | http_download "${tmp}" "$1" "$2" || return 1 306 | body=$(cat "$tmp") 307 | rm -f "${tmp}" 308 | echo "$body" 309 | } 310 | github_release() { 311 | owner_repo=$1 312 | version=$2 313 | test -z "$version" && version="latest" 314 | giturl="https://github.com/${owner_repo}/releases/${version}" 315 | json=$(http_copy "$giturl" "Accept:application/json") 316 | test -z "$json" && return 1 317 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 318 | test -z "$version" && return 1 319 | echo "$version" 320 | } 321 | hash_sha256() { 322 | TARGET=${1:-/dev/stdin} 323 | if is_command gsha256sum; then 324 | hash=$(gsha256sum "$TARGET") || return 1 325 | echo "$hash" | cut -d ' ' -f 1 326 | elif is_command sha256sum; then 327 | hash=$(sha256sum "$TARGET") || return 1 328 | echo "$hash" | cut -d ' ' -f 1 329 | elif is_command shasum; then 330 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 331 | echo "$hash" | cut -d ' ' -f 1 332 | elif is_command openssl; then 333 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 334 | echo "$hash" | cut -d ' ' -f a 335 | else 336 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 337 | return 1 338 | fi 339 | } 340 | hash_sha256_verify() { 341 | TARGET=$1 342 | checksums=$2 343 | if [ -z "$checksums" ]; then 344 | log_err "hash_sha256_verify checksum file not specified in arg2" 345 | return 1 346 | fi 347 | BASENAME=${TARGET##*/} 348 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 349 | if [ -z "$want" ]; then 350 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 351 | return 1 352 | fi 353 | got=$(hash_sha256 "$TARGET") 354 | if [ "$want" != "$got" ]; then 355 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 356 | return 1 357 | fi 358 | } 359 | cat /dev/null <