├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Readme.md ├── cmd ├── cmd.go ├── cmd_test.go ├── neat.go └── neat_test.go ├── demo.png ├── go.mod ├── go.sum ├── hack └── update-kubernetes-deps.sh ├── krew-package.sh ├── krew-template.yaml ├── main.go ├── pkg ├── defaults │ ├── defaults.go │ └── defaults_test.go └── testutil │ └── testutil.go └── test ├── bats-workaround.bash ├── bats-workaround_test.bats ├── e2e-cli.bats ├── e2e-krew.bats ├── e2e-kubectl.bats ├── fixtures ├── list1-neat.json ├── list1-raw.json ├── list1-raw.yaml ├── pod1-neat.json ├── pod1-raw.json ├── pod1-raw.yaml ├── pv1-neat.json ├── pv1-raw.json ├── pv1-raw.yaml ├── role-neat.json ├── role-raw.json ├── role-raw.yaml ├── secret1-neat.json ├── secret1-raw.json ├── secret1-raw.yaml ├── service1-neat.json ├── service1-raw.json └── service1-raw.yaml └── kubectl-stub /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | jobs: 9 | 10 | build: 11 | name: build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v1 17 | with: 18 | go-version: 1.22 19 | id: go 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v1 23 | 24 | - name: Check gofmt 25 | run: test -z "$(gofmt -s -d .)" 26 | 27 | - name: Build 28 | run: make build 29 | 30 | - name: Test 31 | run: make test-unit 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist 3 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./main.go 3 | env: 4 | - CGO_ENABLED=0 5 | goos: 6 | - darwin 7 | - linux 8 | goarch: 9 | - amd64 10 | - arm64 11 | ldflags: 12 | - -s -w -X github.com/itaysk/kubectl-neat/cmd.Version={{ .Version }} 13 | archives: 14 | - name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" 15 | checksum: 16 | name_template: 'checksums.txt' 17 | snapshot: 18 | name_template: "{{ .Tag }}-next" 19 | changelog: 20 | filters: 21 | exclude: 22 | - '^docs:' 23 | - '^test:' 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for taking interest in contributing to kubectl-neat ! 2 | 3 | ## Issues 4 | 5 | - Feel free to open issues for any reason as long as you make it clear if this issue is about a bug/feature/question/comment. 6 | - Please look for existing issues before you open. 7 | - The issue should clearly explain the reason for opening, the proposal if you have any, and any technical information that's relevant. 8 | 9 | ## Pull Requests 10 | 11 | - Every Pull Request should have an associated Issue unless you are fixing a trivial documentation issue. 12 | - Please add the associated Issue in the PR description. 13 | - Describe what the PR does. There's no convention enforced, but please try to be concise and descriptive. Treat the PR description as a commit message. Titles that starts with "fix"/"add"/"improve"/"remove" are good examples. 14 | - There's no need to add or tag reviewers. 15 | - If a reviewer commented on your code, or asked for changes, please remember to mark the discussion as resolved after you address it. PRs with unresolved issues should not be merged (even if the comment is unclear or requires no action from your side). 16 | - Include tests in Go and/or bats if necessary. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | 2 | # TL;DR: 3 | # make build: build locally 4 | # make test: run all tests 5 | # make test-unit: just unit tests 6 | # make test-e2e: just e2e tests 7 | # make release: after git tag, release to github and prepare krew file 8 | 9 | .PHONY: test test-unit test-e2e build goreleaser release clean 10 | os ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') 11 | arch ?= $(shell go env GOARCH | tr '[:upper:]' '[:lower:]') 12 | underscore = $(word $2,$(subst _, ,$1)) 13 | 14 | test: test-unit test-e2e test-integration 15 | 16 | test-unit: 17 | go test -v ./... 18 | 19 | test-e2e: dist/kubectl-neat_$(os)_$(arch) 20 | bats ./test/e2e-cli.bats 21 | 22 | test-integration: dist/kubectl-neat_$(os)_$(arch).tar.gz dist/kubectl-neat_$(os)_$(arch)*/kubectl-neat dist/checksums.txt 23 | bats ./test/e2e-kubectl.bats 24 | bats ./test/e2e-krew.bats 25 | 26 | build: dist/kubectl-neat_$(os)_$(arch) 27 | 28 | SRC = $(shell find . -type f -name '*.go' -not -path "./vendor/*") 29 | dist/kubectl-neat_%: $(SRC) 30 | GOOS=$(call underscore,$*,1) GOARCH=$(call underscore,$*,2) go build -o dist/$(@F) 31 | 32 | # release by default will not publish. run with `publish=1` to publish 33 | goreleaserflags = --skip=publish --snapshot 34 | ifdef publish 35 | goreleaserflags = 36 | endif 37 | # relase always re-builds (no dependencies on purpose) 38 | goreleaser: $(SRC) 39 | goreleaser --clean $(goreleaserflags) 40 | 41 | dist/kubectl-neat_darwin_arm64.tar.gz dist/kubectl-neat_darwin_amd64.tar.gz dist/kubectl-neat_linux_arm64.tar.gz dist/kubectl-neat_linux_amd64.tar.gz dist/checksums.txt: goreleaser 42 | # no op recipe 43 | @: 44 | 45 | release: publish = 1 46 | release: dist/kubectl-neat_darwin_arm64.tar.gz dist/kubectl-neat_darwin_amd64.tar.gz dist/kubectl-neat_linux_arm64.tar.gz dist/kubectl-neat_linux_amd64.tar.gz dist/checksums.txt 47 | ./krew-package.sh 'darwin' 'arm64' 'neat' './dist' 48 | ./krew-package.sh 'darwin' 'amd64' 'neat' './dist' 49 | ./krew-package.sh 'linux' 'arm64' 'neat' './dist' 50 | ./krew-package.sh 'linux' 'amd64' 'neat' './dist' 51 | # merge 52 | yq -o json "dist/kubectl-neat_darwin_amd64.yaml" > dist/darwin-amd64.json 53 | yq -o json "dist/kubectl-neat_darwin_arm64.yaml" > dist/darwin-arm64.json 54 | yq -o json "dist/kubectl-neat_linux_amd64.yaml" > dist/linux-amd64.json 55 | yq -o json "dist/kubectl-neat_linux_arm64.yaml" > dist/linux-arm64.json 56 | 57 | rm dist/kubectl-neat_darwin_arm64.yaml dist/kubectl-neat_darwin_amd64.yaml dist/kubectl-neat_linux_arm64.yaml dist/kubectl-neat_linux_amd64.yaml 58 | jq --slurp '.[0].spec.platforms += .[1].spec.platforms | .[0]' 'dist/darwin-amd64.json' 'dist/darwin-arm64.json' > 'dist/darwin.json' 59 | jq --slurp '.[0].spec.platforms += .[1].spec.platforms | .[0]' 'dist/linux-amd64.json' 'dist/linux-arm64.json' > 'dist/linux.json' 60 | jq --slurp '.[0].spec.platforms += .[1].spec.platforms | .[0]' 'dist/linux.json' 'dist/darwin.json' > 'dist/kubectl-neat.json' 61 | yq -o yaml --prettyPrint dist/kubectl-neat.json > dist/kubectl-neat.yaml 62 | rm dist/kubectl-neat.json dist/darwin.json dist/linux.json dist/darwin-amd64.json dist/darwin-arm64.json dist/linux-amd64.json dist/linux-arm64.json 63 | 64 | clean: 65 | rm -rf dist 66 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # kubectl-neat 2 | 3 | Remove clutter from Kubernetes manifests to make them more readable. 4 | 5 | ## Demo 6 | 7 | Here is a result of a `kubectl get pod -o yaml` for a simple Pod. The lines marked in red are considered redundant and will be removed from the output by kubectl-neat. 8 | 9 | ![demo](./demo.png) 10 | 11 | ## Why 12 | 13 | When you create a Kubernetes resource, let's say a Pod, Kubernetes adds a whole bunch of internal system information to the yaml or json that you originally authored. This includes: 14 | 15 | - Metadata such as creation timestamp, or some internal IDs 16 | - Fill in for missing attributes with default values 17 | - Additional system attributes created by admission controllers, such as service account token 18 | - Status information 19 | 20 | If you try to `kubectl get` resources you have created, they will no longer look like what you originally authored, and will be unreadably verbose. 21 | `kubectl-neat` cleans up that redundant information for you. 22 | 23 | ## Installation 24 | 25 | ```bash 26 | kubectl krew install neat 27 | ``` 28 | 29 | or just download the binary if you prefer. 30 | 31 | When used as a kubectl plugin the command is `kubectl neat`, and when used as a standalone executable it's `kubectl-neat`. 32 | 33 | ## Usage 34 | 35 | There are two modes of operation that specify where to get the input document from: a local file or from Kubernetes. 36 | 37 | ### Local - file or Stdin 38 | 39 | This is the default mode if you run just `kubectl neat`. This command accepts an optional flag `-f/--file` which specifies the file to neat. It can be a path to a local file, or `-` to read the file from stdin. If omitted, it will default to `-`. The file must be a yaml or json file and a valid Kubernetes resource. 40 | 41 | There's another optional optional flag, `-o/--output` which specifies the format for the output. If omitted it will default to the same format of the input (auto-detected). 42 | 43 | Examples: 44 | ```bash 45 | kubectl get pod mypod -o yaml | kubectl neat 46 | 47 | kubectl get pod mypod -oyaml | kubectl neat -o json 48 | 49 | kubectl neat -f - <./my-pod.json 50 | 51 | kubectl neat -f ./my-pod.json 52 | 53 | kubectl neat -f ./my-pod.json --output yaml 54 | ``` 55 | 56 | ### Kubernetes - kubectl get wrapper 57 | 58 | This mode is invoked by calling the `get` subcommand, i.e `kubectl neat get ...`. It is a convenience to run `kubectl get` and then `kubectl neat` the output in a single command. It accepts any argument that `kubectl get` accepts and passes those arguments as is to `kubectl get`. Since it executes `kubectl`, it need to be able to find it in the path. 59 | 60 | Examples: 61 | ```bash 62 | kubectl neat get -- pod mypod -oyaml 63 | kubectl neat get -- svc -n default myservice --output json 64 | ``` 65 | 66 | # How it works 67 | 68 | Besides general tidying for status, metadata, and empty fields, kubectl-neat primarily looks for two types of things: default values inserted by Kubernetes' object model, and common mutating controllers. 69 | 70 | ## Kubernetes object model defaults 71 | 72 | For de-defaulting Kubernetes' object model, we invoke the same code that Kubernetes would have, and see what default values were assigned. If these observed values look like the ones we have in the incoming spec, we conclude they are default. If they weren't, and the user manually set a field to it's default value, it's not a bad thing to remove it anyway. 73 | 74 | ## Common mutating controllers 75 | 76 | Here are the [recommended](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#what-does-each-admission-controller-do) admission controllers, and their relation to kubectl-neat: 77 | 78 | controller | description | neat 79 | ---|---|--- 80 | NamespaceLifecycle | rejects operations on resources in namespaces being deleted | ignore 81 | LimitRanger | set default values for resource requests and limits | ignore 82 | ServiceAccount | set default service account and assign token | Remove `default-token-*` volumes. Remove deprecated `spec.serviceAccount` 83 | TaintNodesByCondition | automatically taint a node based on node conditions | TODO 84 | Priority | validate priority class and add it's value | ignore 85 | DefaultTolerationSeconds | configure pods to temporarily tolarate notready and unreachable taints | TODO 86 | DefaultStorageClass | validate and set default storage class for new pvc | ignore 87 | StorageObjectInUseProtection | prevent deletion of pvc/pv in use by adding a finalizer | ignore 88 | PersistentVolumeClaimResize | enforce pvc resizing only for enabled storage classes | ignore 89 | MutatingAdmissionWebhook | implement the mutating webhook feature | ignore 90 | ValidatingAdmissionWebhook | implement the validating webhook feature | ignore 91 | RuntimeClass | add pod overhead according to runtime class | TODO 92 | ResourceQuota | implement the resource qouta feature | ignore 93 | Kubernetes Scheduler | assign pods to nodes | Remove `spec.nodeName` -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Itay Shakury @itaysk 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "bytes" 20 | "fmt" 21 | "io/ioutil" 22 | "os" 23 | "os/exec" 24 | "unicode" 25 | 26 | "github.com/ghodss/yaml" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var outputFormat *string 31 | var inputFile *string 32 | 33 | func init() { 34 | outputFormat = rootCmd.PersistentFlags().StringP("output", "o", "yaml", "output format: yaml or json") 35 | inputFile = rootCmd.Flags().StringP("file", "f", "-", "file path to neat, or - to read from stdin") 36 | rootCmd.SetOut(os.Stdout) 37 | rootCmd.SetErr(os.Stderr) 38 | rootCmd.MarkFlagFilename("file") 39 | rootCmd.AddCommand(getCmd) 40 | rootCmd.AddCommand(versionCmd) 41 | } 42 | 43 | // Execute is the entry point for the command package 44 | func Execute() { 45 | if err := rootCmd.Execute(); err != nil { 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | var rootCmd = &cobra.Command{ 51 | Use: "kubectl-neat", 52 | Example: `kubectl get pod mypod -o yaml | kubectl neat 53 | kubectl get pod mypod -oyaml | kubectl neat -o json 54 | kubectl neat -f - <./my-pod.json 55 | kubectl neat -f ./my-pod.json 56 | kubectl neat -f ./my-pod.json --output yaml`, 57 | RunE: func(cmd *cobra.Command, args []string) error { 58 | var in, out []byte 59 | var err error 60 | if *inputFile == "-" { 61 | stdin := cmd.InOrStdin() 62 | in, err = ioutil.ReadAll(stdin) 63 | } else { 64 | in, err = ioutil.ReadFile(*inputFile) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | outFormat := *outputFormat 70 | if !cmd.Flag("output").Changed { 71 | outFormat = "same" 72 | } 73 | out, err = NeatYAMLOrJSON(in, outFormat) 74 | if err != nil { 75 | return err 76 | } 77 | cmd.Print(string(out)) 78 | return nil 79 | }, 80 | } 81 | 82 | var kubectl string = "kubectl" 83 | 84 | var getCmd = &cobra.Command{ 85 | Use: "get", 86 | Example: `kubectl neat get -- pod mypod -oyaml 87 | kubectl neat get -- svc -n default myservice --output json`, 88 | FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, //don't try to validate kubectl get's flags 89 | RunE: func(cmd *cobra.Command, args []string) error { 90 | var out []byte 91 | var err error 92 | //reset defaults 93 | //there are two output settings in this subcommand: kubectl get's and kubectl-neat's 94 | //any combination of those can be provided by using the output flag in either side of the -- 95 | //the most efficient is kubectl: json, kubectl-neat: yaml 96 | //0--0->Y--J #choose what's best for us 97 | //0--Y->Y--Y #user did specify output in kubectl, so respect that 98 | //0--J->J--J #user did specify output in kubectl, so respect that 99 | //Y--0->Y--J #user doesn't care about kubectl so use json but convert back 100 | //J--0->J--J #user expects json so use it for foth 101 | //if the user specified both side we can't touch it 102 | 103 | //the desired kubectl get output is always json, unless it was explicitly set by the user to yaml in which case the arg is overriden when concatenating the args later 104 | cmdArgs := append([]string{"get", "-o", "json"}, args...) 105 | kubectlCmd := exec.Command(kubectl, cmdArgs...) 106 | kres, err := kubectlCmd.CombinedOutput() 107 | if err != nil { 108 | return fmt.Errorf("Error invoking kubectl as %v %v", kubectlCmd.Args, err) 109 | } 110 | //handle the case of 0--J->J--J 111 | outFormat := *outputFormat 112 | kubeout := "yaml" 113 | for _, arg := range args { 114 | if arg == "json" || arg == "ojson" { 115 | outFormat = "json" 116 | } 117 | } 118 | if !cmd.Flag("output").Changed && kubeout == "json" { 119 | outFormat = "json" 120 | } 121 | out, err = NeatYAMLOrJSON(kres, outFormat) 122 | if err != nil { 123 | return err 124 | } 125 | cmd.Println(string(out)) 126 | return nil 127 | }, 128 | } 129 | 130 | // populated by goreleaser 131 | var ( 132 | Version = "v0.0.0+unknown" 133 | ) 134 | 135 | var versionCmd = &cobra.Command{ 136 | Use: "version", 137 | Short: "Print kubectl-neat version", 138 | Long: "Print the version of kubectl-neat", 139 | Run: func(cmd *cobra.Command, args []string) { 140 | fmt.Printf("kubectl-neat version: %s\n", Version) 141 | }, 142 | } 143 | 144 | func isJSON(s []byte) bool { 145 | return bytes.HasPrefix(bytes.TrimLeftFunc(s, unicode.IsSpace), []byte{'{'}) 146 | } 147 | 148 | // NeatYAMLOrJSON converts 'in' to json if needed, invokes neat, and converts back if needed according the the outputFormat argument: yaml/json/same 149 | func NeatYAMLOrJSON(in []byte, outputFormat string) (out []byte, err error) { 150 | var injson, outjson string 151 | itsYaml := !isJSON(in) 152 | if itsYaml { 153 | injsonbytes, err := yaml.YAMLToJSON(in) 154 | if err != nil { 155 | return nil, fmt.Errorf("error converting from yaml to json : %v", err) 156 | } 157 | injson = string(injsonbytes) 158 | } else { 159 | injson = string(in) 160 | } 161 | 162 | outjson, err = Neat(injson) 163 | if err != nil { 164 | return nil, fmt.Errorf("error neating : %v", err) 165 | } 166 | 167 | if outputFormat == "yaml" || (outputFormat == "same" && itsYaml) { 168 | out, err = yaml.JSONToYAML([]byte(outjson)) 169 | if err != nil { 170 | return nil, fmt.Errorf("error converting from json to yaml : %v", err) 171 | } 172 | } else { 173 | out = []byte(outjson) 174 | } 175 | return 176 | } 177 | -------------------------------------------------------------------------------- /cmd/cmd_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Itay Shakury @itaysk 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "bytes" 20 | "io/ioutil" 21 | "os" 22 | "strings" 23 | "testing" 24 | ) 25 | 26 | func assertErrorNil(err error) bool { 27 | return err == nil 28 | } 29 | func TestRootCmd(t *testing.T) { 30 | resourceDataJSONPath := "../test/fixtures/service1-raw.json" 31 | resourceDataJSONBytes, err := ioutil.ReadFile(resourceDataJSONPath) 32 | resourceDataJSON := string(resourceDataJSONBytes) 33 | if err != nil { 34 | t.Errorf("error readin test data file %s: %v", resourceDataJSONPath, err) 35 | } 36 | resourceDataYAMLPath := "../test/fixtures/service1-raw.yaml" 37 | resourceDataYAMLBytes, err := ioutil.ReadFile(resourceDataYAMLPath) 38 | resourceDataYAML := string(resourceDataYAMLBytes) 39 | if err != nil { 40 | t.Errorf("error readin test data file %s: %v", resourceDataYAMLPath, err) 41 | } 42 | 43 | testcases := []struct { 44 | args []string 45 | stdin string 46 | assertError func(err error) bool 47 | expOut string 48 | }{ 49 | { 50 | args: []string{}, 51 | stdin: "", 52 | assertError: assertErrorNil, 53 | expOut: "", 54 | }, 55 | { 56 | args: []string{}, 57 | stdin: resourceDataJSON, 58 | assertError: assertErrorNil, 59 | expOut: "apiVersion", 60 | }, 61 | { 62 | args: []string{}, 63 | stdin: resourceDataYAML, 64 | assertError: assertErrorNil, 65 | expOut: "apiVersion", 66 | }, 67 | { 68 | args: []string{"-f", "-"}, 69 | stdin: resourceDataJSON, 70 | assertError: assertErrorNil, 71 | expOut: "apiVersion", 72 | }, 73 | { 74 | args: []string{"-f", "/nogood"}, 75 | stdin: "", 76 | assertError: func(err error) bool { 77 | _, ok := err.(*os.PathError) 78 | return ok 79 | }, 80 | expOut: "", 81 | }, 82 | { 83 | args: []string{"-f", resourceDataJSONPath}, 84 | stdin: "", 85 | assertError: assertErrorNil, 86 | expOut: "apiVersion", 87 | }, 88 | { 89 | args: []string{"-f", resourceDataYAMLPath}, 90 | stdin: "", 91 | assertError: assertErrorNil, 92 | expOut: "apiVersion", 93 | }, 94 | } 95 | 96 | for _, tc := range testcases { 97 | rootCmd.SetArgs(tc.args) 98 | if tc.stdin != "" { 99 | rootCmd.SetIn(bytes.NewReader([]byte(tc.stdin))) 100 | } 101 | cmdout := new(bytes.Buffer) 102 | cmderr := new(bytes.Buffer) 103 | rootCmd.SetOut(cmdout) 104 | rootCmd.SetErr(cmderr) 105 | rootCmd.ParseFlags(tc.args) 106 | resErr := rootCmd.RunE(rootCmd, tc.args) 107 | resStdout, err := ioutil.ReadAll(cmdout) 108 | if err != nil { 109 | t.Errorf("error reading command output: %v", err) 110 | } 111 | resStderr, err := ioutil.ReadAll(cmderr) 112 | if err != nil { 113 | t.Errorf("error reading command error: %v\ntest case: %v", err, tc) 114 | } 115 | if tc.assertError != nil && !tc.assertError(resErr) { 116 | t.Errorf("error assertion: have: %#v\ntest case: %v", resErr, tc) 117 | } 118 | if !strings.Contains(string(resStdout), tc.expOut) { 119 | t.Errorf("stdout assertion: have: %s\nwant: %s\ntest case: %v", string(resStdout), tc.expOut, tc) 120 | } 121 | if len(resStderr) > 0 { 122 | t.Errorf("stderr not empty: %s\ntest case: %v", string(resStderr), tc) 123 | } 124 | } 125 | } 126 | 127 | func TestGetCmd(t *testing.T) { 128 | kubectl = "../test/kubectl-stub" 129 | testcases := []struct { 130 | args []string 131 | assertError func(err error) bool 132 | expOut string 133 | expErr string 134 | }{ 135 | { 136 | args: []string{""}, 137 | assertError: func(err error) bool { 138 | return strings.HasPrefix(err.Error(), "Error invoking kubectl") 139 | }, 140 | expOut: "", 141 | expErr: "", 142 | }, 143 | { 144 | args: []string{"pods"}, 145 | assertError: assertErrorNil, 146 | expOut: "apiVersion", 147 | expErr: "", 148 | }, 149 | { 150 | args: []string{"pods", "mypod"}, 151 | assertError: assertErrorNil, 152 | expOut: "apiVersion", 153 | expErr: "", 154 | }, 155 | { 156 | args: []string{"pods", "mypod", "-o", "yaml"}, 157 | assertError: assertErrorNil, 158 | expOut: "apiVersion", 159 | expErr: "", 160 | }, 161 | { 162 | args: []string{"pods", "mypod", "-o", "json"}, 163 | assertError: assertErrorNil, 164 | expOut: "apiVersion", 165 | expErr: "", 166 | }, 167 | } 168 | 169 | for _, tc := range testcases { 170 | rootCmd.SetArgs(tc.args) 171 | cmdout := new(bytes.Buffer) 172 | cmderr := new(bytes.Buffer) 173 | rootCmd.SetOut(cmdout) 174 | rootCmd.SetErr(cmderr) 175 | rootCmd.ParseFlags(tc.args) 176 | resErr := getCmd.RunE(getCmd, tc.args) 177 | resStdout, err := ioutil.ReadAll(cmdout) 178 | if err != nil { 179 | t.Errorf("error reading command output: %v", err) 180 | } 181 | resStderr, err := ioutil.ReadAll(cmderr) 182 | if err != nil { 183 | t.Errorf("error reading command error: %v\ntest case: %v", err, tc) 184 | } 185 | if tc.assertError != nil && !tc.assertError(resErr) { 186 | t.Errorf("error assertion: have: %#v\ntest case: %v", resErr, tc) 187 | } 188 | if !strings.Contains(string(resStdout), tc.expOut) { 189 | t.Errorf("stdout assertion: have: %s\nwant: %s\ntest case: %v", string(resStdout), tc.expOut, tc) 190 | } 191 | if len(resStderr) > 0 { 192 | t.Errorf("stderr not empty: %s\ntest case: %v", string(resStderr), tc) 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /cmd/neat.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Itay Shakury @itaysk 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/itaysk/kubectl-neat/pkg/defaults" 23 | 24 | "github.com/tidwall/gjson" 25 | "github.com/tidwall/sjson" 26 | ) 27 | 28 | // Neat gets a Kubernetes resource json as string and de-clutters it to make it more readable. 29 | func Neat(in string) (string, error) { 30 | var err error 31 | draft := in 32 | 33 | if in == "" { 34 | return draft, fmt.Errorf("error in neatPod, input json is empty") 35 | } 36 | if !gjson.Valid(in) { 37 | return draft, fmt.Errorf("error in neatPod, input is not a vaild json: %s", in[:20]) 38 | } 39 | 40 | kind := gjson.Get(in, "kind").String() 41 | 42 | // handle list 43 | if kind == "List" { 44 | items := gjson.Get(draft, "items").Array() 45 | for i, item := range items { 46 | itemNeat, err := Neat(item.String()) 47 | if err != nil { 48 | continue 49 | } 50 | draft, err = sjson.SetRaw(draft, fmt.Sprintf("items.%d", i), itemNeat) 51 | if err != nil { 52 | continue 53 | } 54 | } 55 | // general neating 56 | draft, err = neatMetadata(draft) 57 | if err != nil { 58 | return draft, fmt.Errorf("error in neatMetadata : %v", err) 59 | } 60 | return draft, nil 61 | } 62 | 63 | // defaults neating 64 | draft, err = defaults.NeatDefaults(draft) 65 | if err != nil { 66 | return draft, fmt.Errorf("error in neatDefaults : %v", err) 67 | } 68 | 69 | // controllers neating 70 | draft, err = neatScheduler(draft) 71 | if err != nil { 72 | return draft, fmt.Errorf("error in neatScheduler : %v", err) 73 | } 74 | if kind == "Pod" { 75 | draft, err = neatServiceAccount(draft) 76 | if err != nil { 77 | return draft, fmt.Errorf("error in neatServiceAccount : %v", err) 78 | } 79 | } 80 | 81 | // general neating 82 | draft, err = neatMetadata(draft) 83 | if err != nil { 84 | return draft, fmt.Errorf("error in neatMetadata : %v", err) 85 | } 86 | draft, err = neatStatus(draft) 87 | if err != nil { 88 | return draft, fmt.Errorf("error in neatStatus : %v", err) 89 | } 90 | draft, err = neatEmpty(draft) 91 | if err != nil { 92 | return draft, fmt.Errorf("error in neatEmpty : %v", err) 93 | } 94 | 95 | return draft, nil 96 | } 97 | 98 | func neatMetadata(in string) (string, error) { 99 | var err error 100 | in, err = sjson.Delete(in, `metadata.annotations.kubectl\.kubernetes\.io/last-applied-configuration`) 101 | if err != nil { 102 | return in, fmt.Errorf("error deleting last-applied-configuration : %v", err) 103 | } 104 | // TODO: prettify this. gjson's @pretty is ok but setRaw the pretty code gives unwanted result 105 | newMeta := gjson.Get(in, "{metadata.name,metadata.namespace,metadata.labels,metadata.annotations}") 106 | in, err = sjson.Set(in, "metadata", newMeta.Value()) 107 | if err != nil { 108 | return in, fmt.Errorf("error setting new metadata : %v", err) 109 | } 110 | return in, nil 111 | } 112 | 113 | func neatStatus(in string) (string, error) { 114 | return sjson.Delete(in, "status") 115 | } 116 | 117 | func neatScheduler(in string) (string, error) { 118 | return sjson.Delete(in, "spec.nodeName") 119 | } 120 | 121 | func neatServiceAccount(in string) (string, error) { 122 | var err error 123 | // keep an eye open on https://github.com/tidwall/sjson/issues/11 124 | // when it's implemented, we can do: 125 | // sjson.delete(in, "spec.volumes.#(name%default-token-*)") 126 | // sjson.delete(in, "spec.containers.#.volumeMounts.#(name%default-token-*)") 127 | 128 | for vi, v := range gjson.Get(in, "spec.volumes").Array() { 129 | vname := v.Get("name").String() 130 | if strings.HasPrefix(vname, "default-token-") { 131 | in, err = sjson.Delete(in, fmt.Sprintf("spec.volumes.%d", vi)) 132 | if err != nil { 133 | continue 134 | } 135 | } 136 | } 137 | for ci, c := range gjson.Get(in, "spec.containers").Array() { 138 | for vmi, vm := range c.Get("volumeMounts").Array() { 139 | vmname := vm.Get("name").String() 140 | if strings.HasPrefix(vmname, "default-token-") { 141 | in, err = sjson.Delete(in, fmt.Sprintf("spec.containers.%d.volumeMounts.%d", ci, vmi)) 142 | if err != nil { 143 | continue 144 | } 145 | } 146 | } 147 | } 148 | in, _ = sjson.Delete(in, "spec.serviceAccount") //Deprecated: Use serviceAccountName instead 149 | 150 | return in, nil 151 | } 152 | 153 | // neatEmpty removes all zero length elements in the json 154 | func neatEmpty(in string) (string, error) { 155 | var err error 156 | jsonResult := gjson.Parse(in) 157 | var empties []string 158 | findEmptyPathsRecursive(jsonResult, "", &empties) 159 | for _, emptyPath := range empties { 160 | // if we just delete emptyPath, it may create empty parents 161 | // so we walk the path and re-check for emptiness at every level 162 | emptyPathParts := strings.Split(emptyPath, ".") 163 | for i := len(emptyPathParts); i > 0; i-- { 164 | curPath := strings.Join(emptyPathParts[:i], ".") 165 | cur := gjson.Get(in, curPath) 166 | if isResultEmpty(cur) { 167 | in, err = sjson.Delete(in, curPath) 168 | if err != nil { 169 | continue 170 | } 171 | } 172 | } 173 | } 174 | return in, nil 175 | } 176 | 177 | // findEmptyPathsRecursive builds a list of paths that point to zero length elements 178 | // cur is the current element to look at 179 | // path is the path to cur 180 | // res is a pointer to a list of empty paths to populate 181 | func findEmptyPathsRecursive(cur gjson.Result, path string, res *[]string) { 182 | if isResultEmpty(cur) { 183 | *res = append(*res, path[1:]) //remove '.' from start 184 | return 185 | } 186 | if !(cur.IsArray() || cur.IsObject()) { 187 | return 188 | } 189 | // sjson's ForEach doesn't put track index when iterating arrays, hence the index variable 190 | index := -1 191 | cur.ForEach(func(k gjson.Result, v gjson.Result) bool { 192 | var newPath string 193 | if cur.IsArray() { 194 | index++ 195 | newPath = fmt.Sprintf("%s.%d", path, index) 196 | } else { 197 | newPath = fmt.Sprintf("%s.%s", path, k.Str) 198 | } 199 | findEmptyPathsRecursive(v, newPath, res) 200 | return true 201 | }) 202 | } 203 | 204 | func isResultEmpty(j gjson.Result) bool { 205 | v := j.Value() 206 | switch vt := v.(type) { 207 | // empty string != lack of string. keep empty strings as it's meaningful data 208 | // case string: 209 | // return vt == "" 210 | case []interface{}: 211 | return len(vt) == 0 212 | case map[string]interface{}: 213 | return len(vt) == 0 214 | } 215 | return false 216 | } 217 | -------------------------------------------------------------------------------- /cmd/neat_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Itay Shakury @itaysk 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "io/ioutil" 20 | "path/filepath" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/itaysk/kubectl-neat/pkg/testutil" 25 | ) 26 | 27 | func TestNeatMetadata(t *testing.T) { 28 | cases := []struct { 29 | title string 30 | data string 31 | expect string 32 | }{ 33 | { 34 | title: "pod metadata", 35 | data: `{ 36 | "metadata": { 37 | "creationTimestamp": "2019-04-24T19:55:27Z", 38 | "labels": { 39 | "name": "myapp" 40 | }, 41 | "name": "myapp", 42 | "namespace": "default", 43 | "resourceVersion": "274103", 44 | "selfLink": "/api/v1/namespaces/default/pods/myapp", 45 | "uid": "e8330f3c-66ca-11e9-b6fa-0800271788ca" 46 | } 47 | }`, 48 | expect: `{ 49 | "metadata": { 50 | "labels": { 51 | "name": "myapp" 52 | }, 53 | "name": "myapp", 54 | "namespace": "default" 55 | } 56 | }`, 57 | }, 58 | { 59 | title: "annotations with apply", 60 | data: `{ 61 | "metadata": { 62 | "name": "test", 63 | "namespace": "testns", 64 | "annotations": { 65 | "my-annotation": "is here", 66 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"authentication.istio.io/v1alpha1\",\"kind\":\"Policy\",\"metadata\":{\"annotations\":{},\"name\":\"default\",\"namespace\":\"one\"},\"spec\":{\"peers\":[{\"mtls\":{}}]}}\n" 67 | } 68 | } 69 | }`, 70 | expect: `{ 71 | "metadata": { 72 | "name": "test", 73 | "namespace": "testns", 74 | "annotations": { 75 | "my-annotation": "is here" 76 | } 77 | } 78 | }`, 79 | }, 80 | } 81 | for _, c := range cases { 82 | resJSON, err := neatMetadata(c.data) 83 | if err != nil { 84 | t.Errorf("error in neatMetadata for case '%s': %v", c.title, err) 85 | continue 86 | } 87 | equal, err := testutil.JSONEqual(resJSON, c.expect) 88 | if err != nil { 89 | t.Errorf("error in JSONEqual for case '%s': %v", c.title, err) 90 | continue 91 | } 92 | if !equal { 93 | t.Errorf("test case '%s' failed. want: '%s' have: '%s'", c.title, c.expect, resJSON) 94 | } 95 | 96 | } 97 | } 98 | 99 | func TestNeatScheduler(t *testing.T) { 100 | cases := []struct { 101 | title string 102 | data string 103 | expect string 104 | }{ 105 | { 106 | title: "nodeName", 107 | data: `{ 108 | "apiVersion": "v1", 109 | "kind": "Pod", 110 | "metadata": { 111 | "name": "myapp", 112 | "namespace": "default" 113 | }, 114 | "spec": { 115 | "containers": [ 116 | { 117 | "image": "nginx", 118 | "imagePullPolicy": "Always", 119 | "name": "myapp" 120 | } 121 | ], 122 | "nodeName": "minikube" 123 | } 124 | }`, 125 | expect: `{ 126 | "apiVersion": "v1", 127 | "kind": "Pod", 128 | "metadata": { 129 | "name": "myapp", 130 | "namespace": "default" 131 | }, 132 | "spec": { 133 | "containers": [ 134 | { 135 | "image": "nginx", 136 | "imagePullPolicy": "Always", 137 | "name": "myapp" 138 | } 139 | ] 140 | } 141 | }`, 142 | }, 143 | } 144 | for _, c := range cases { 145 | resJSON, err := neatScheduler(c.data) 146 | if err != nil { 147 | t.Errorf("error in neatScheduler for case '%s': %v", c.title, err) 148 | continue 149 | } 150 | equal, err := testutil.JSONEqual(resJSON, c.expect) 151 | if err != nil { 152 | t.Errorf("error in JSONEqual for case '%s': %v", c.title, err) 153 | continue 154 | } 155 | if !equal { 156 | t.Errorf("test case '%s' failed. want: '%s' have: '%s'", c.title, c.expect, resJSON) 157 | } 158 | 159 | } 160 | } 161 | 162 | func TestNeatServiceAccount(t *testing.T) { 163 | cases := []struct { 164 | title string 165 | data string 166 | expect string 167 | }{ 168 | { 169 | title: "pod multi volumes", 170 | data: `{ 171 | "apiVersion": "v1", 172 | "kind": "Pod", 173 | "metadata": { 174 | "labels": { 175 | "name": "myapp" 176 | }, 177 | "name": "myapp", 178 | "namespace": "default" 179 | }, 180 | "spec": { 181 | "containers": [ 182 | { 183 | "image": "nginx", 184 | "name": "myapp", 185 | "volumeMounts": [ 186 | { 187 | "mountPath": "/my", 188 | "name": "my", 189 | "readOnly": false 190 | }, 191 | { 192 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 193 | "name": "default-token-nmshj", 194 | "readOnly": true 195 | } 196 | ] 197 | } 198 | ], 199 | "serviceAccount": "default", 200 | "serviceAccountName": "default", 201 | "volumes": [ 202 | { 203 | "name": "default-token-nmshj", 204 | "secret": { 205 | "defaultMode": 420, 206 | "secretName": "default-token-nmshj" 207 | } 208 | }, 209 | { 210 | "name": "my", 211 | "hostPath": { 212 | "path": "/my", 213 | "type": "Directory" 214 | } 215 | } 216 | ] 217 | } 218 | }`, 219 | expect: `{ 220 | "apiVersion": "v1", 221 | "kind": "Pod", 222 | "metadata": { 223 | "labels": { 224 | "name": "myapp" 225 | }, 226 | "name": "myapp", 227 | "namespace": "default" 228 | }, 229 | "spec": { 230 | "containers": [ 231 | { 232 | "image": "nginx", 233 | "name": "myapp", 234 | "volumeMounts": [ 235 | { 236 | "mountPath": "/my", 237 | "name": "my", 238 | "readOnly": false 239 | } 240 | ] 241 | } 242 | ], 243 | "serviceAccountName": "default", 244 | "volumes": [ 245 | { 246 | "name": "my", 247 | "hostPath": { 248 | "path": "/my", 249 | "type": "Directory" 250 | } 251 | } 252 | ] 253 | } 254 | }`, 255 | }, 256 | } 257 | for _, c := range cases { 258 | resJSON, err := neatServiceAccount(c.data) 259 | if err != nil { 260 | t.Errorf("error in neatServiceAccount for case '%s': %v", c.title, err) 261 | continue 262 | } 263 | equal, err := testutil.JSONEqual(resJSON, c.expect) 264 | if err != nil { 265 | t.Errorf("error in JSONEqual for case '%s': %v", c.title, err) 266 | continue 267 | } 268 | if !equal { 269 | t.Errorf("test case '%s' failed. want: '%s' have: '%s'", c.title, c.expect, resJSON) 270 | } 271 | 272 | } 273 | } 274 | 275 | func TestNeatEmpty(t *testing.T) { 276 | cases := []struct { 277 | title string 278 | data string 279 | expect string 280 | }{ 281 | { 282 | title: "empty object", 283 | data: `{ "foo": "bar", "baz": {} }`, 284 | expect: `{ "foo": "bar"}`, 285 | }, 286 | { 287 | title: "empty array", 288 | data: `{ "foo": "bar", "baz": [] }`, 289 | expect: `{ "foo": "bar"}`, 290 | }, 291 | { 292 | title: "empty second arrray element", 293 | data: `{ "foo": [ "bar", {} ] }`, 294 | expect: `{ "foo": [ "bar" ] }`, 295 | }, 296 | { 297 | title: "empty array object", 298 | data: `{ "foo": "bar", "baz": { [] } }`, 299 | expect: `{ "foo": "bar"}`, 300 | }, 301 | { 302 | title: "single empty array in object", 303 | data: `{ "foo": "bar", "baz": { "fiz": [] } }`, 304 | expect: `{ "foo": "bar"}`, 305 | }, 306 | } 307 | for _, c := range cases { 308 | resJSON, err := neatEmpty(c.data) 309 | if err != nil { 310 | t.Errorf("error in Neat for case '%s': %v", c.title, err) 311 | continue 312 | } 313 | equal, err := testutil.JSONEqual(resJSON, c.expect) 314 | if err != nil { 315 | t.Errorf("error in JSONEqual for case '%s': %v", c.title, err) 316 | continue 317 | } 318 | if !equal { 319 | t.Errorf("test case '%s' failed. want: '%s' have: '%s'", c.title, c.expect, resJSON) 320 | } 321 | 322 | } 323 | } 324 | 325 | func TestNeat(t *testing.T) { 326 | testsDir := "../test/fixtures" 327 | testFiles, err := ioutil.ReadDir(testsDir) 328 | if err != nil { 329 | t.Fatalf("can't list tests in: %s", testsDir) 330 | } 331 | for _, f := range testFiles { 332 | fName := f.Name() 333 | fParts := strings.Split(fName, "-") 334 | if fParts[1] == "raw.json" { 335 | fFullName := filepath.Join(testsDir, f.Name()) 336 | inBytes, err := ioutil.ReadFile(fFullName) 337 | if err != nil { 338 | t.Errorf("can't read file: %s", fFullName) 339 | } 340 | expFullName := filepath.Join(testsDir, fParts[0]+"-neat.json") 341 | expBytes, err := ioutil.ReadFile(expFullName) 342 | if err != nil { 343 | t.Errorf("can't read file: %s", expFullName) 344 | } 345 | resJSON, err := Neat(string(inBytes)) 346 | if err != nil { 347 | t.Errorf("error in Neat for case: %s: %v", fName, err) 348 | continue 349 | } 350 | equal, err := testutil.JSONEqual(resJSON, string(expBytes)) 351 | if err != nil { 352 | t.Errorf("error in JSONEqual for case: %s: %v", fName, err) 353 | continue 354 | } 355 | if !equal { 356 | t.Errorf("test case failed: %s:\nhave %s\nwant %s", fName, string(expBytes), resJSON) 357 | } 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaysk/kubectl-neat/b6e10595aa3f3e3f9ebd762253bbe8840612f938/demo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/itaysk/kubectl-neat 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.5 6 | 7 | require ( 8 | github.com/ghodss/yaml v1.0.0 9 | github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603 10 | github.com/sirupsen/logrus v1.9.0 11 | github.com/spf13/cobra v1.7.0 12 | github.com/tidwall/gjson v1.9.3 13 | github.com/tidwall/sjson v1.0.4 14 | k8s.io/apimachinery v0.30.2 15 | k8s.io/client-go v0.30.2 16 | k8s.io/kubernetes v1.30.2 17 | ) 18 | 19 | require ( 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/blang/semver/v4 v4.0.0 // indirect 22 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 23 | github.com/distribution/reference v0.5.0 // indirect 24 | github.com/go-logr/logr v1.4.1 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/google/gofuzz v1.2.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/json-iterator/go v1.1.12 // indirect 30 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 | github.com/modern-go/reflect2 v1.0.2 // indirect 33 | github.com/opencontainers/go-digest v1.0.0 // indirect 34 | github.com/prometheus/client_golang v1.16.0 // indirect 35 | github.com/prometheus/client_model v0.4.0 // indirect 36 | github.com/prometheus/common v0.44.0 // indirect 37 | github.com/prometheus/procfs v0.10.1 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/tidwall/match v1.1.1 // indirect 40 | github.com/tidwall/pretty v1.2.0 // indirect 41 | golang.org/x/net v0.23.0 // indirect 42 | golang.org/x/sys v0.18.0 // indirect 43 | golang.org/x/text v0.14.0 // indirect 44 | google.golang.org/protobuf v1.33.0 // indirect 45 | gopkg.in/inf.v0 v0.9.1 // indirect 46 | gopkg.in/yaml.v2 v2.4.0 // indirect 47 | k8s.io/api v0.30.2 // indirect 48 | k8s.io/apiextensions-apiserver v0.0.0 // indirect 49 | k8s.io/apiserver v0.30.2 // indirect 50 | k8s.io/component-base v0.30.2 // indirect 51 | k8s.io/klog/v2 v2.120.1 // indirect 52 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 53 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 54 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 55 | sigs.k8s.io/yaml v1.3.0 // indirect 56 | ) 57 | 58 | replace k8s.io/api => k8s.io/api v0.30.2 59 | 60 | replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.30.2 61 | 62 | replace k8s.io/apimachinery => k8s.io/apimachinery v0.30.2 63 | 64 | replace k8s.io/apiserver => k8s.io/apiserver v0.30.2 65 | 66 | replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.30.2 67 | 68 | replace k8s.io/client-go => k8s.io/client-go v0.30.2 69 | 70 | replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.30.2 71 | 72 | replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.30.2 73 | 74 | replace k8s.io/code-generator => k8s.io/code-generator v0.30.2 75 | 76 | replace k8s.io/component-base => k8s.io/component-base v0.30.2 77 | 78 | replace k8s.io/component-helpers => k8s.io/component-helpers v0.30.2 79 | 80 | replace k8s.io/controller-manager => k8s.io/controller-manager v0.30.2 81 | 82 | replace k8s.io/cri-api => k8s.io/cri-api v0.30.2 83 | 84 | replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.30.2 85 | 86 | replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.30.2 87 | 88 | replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.30.2 89 | 90 | replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.30.2 91 | 92 | replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.30.2 93 | 94 | replace k8s.io/kubectl => k8s.io/kubectl v0.30.2 95 | 96 | replace k8s.io/kubelet => k8s.io/kubelet v0.30.2 97 | 98 | replace k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.30.2 99 | 100 | replace k8s.io/metrics => k8s.io/metrics v0.30.2 101 | 102 | replace k8s.io/mount-utils => k8s.io/mount-utils v0.30.2 103 | 104 | replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.30.2 105 | 106 | replace k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.30.2 107 | 108 | replace k8s.io/sample-controller => k8s.io/sample-controller v0.30.2 109 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 4 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= 12 | github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 13 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 14 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 15 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 16 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 18 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 19 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 21 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 22 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 23 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 24 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 25 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 26 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 27 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 28 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 29 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 30 | github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603 h1:gSech9iGLFCosfl/DC7BWnpSSh/tQClWnKS2I2vdPww= 31 | github.com/jeremywohl/flatten v0.0.0-20180923035001-588fe0d4c603/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= 32 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 34 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 35 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 36 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 37 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 38 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 39 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 40 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 41 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 42 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 45 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 46 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 47 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 48 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 49 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 50 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 51 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 52 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 53 | github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= 54 | github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= 55 | github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= 56 | github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 57 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 58 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 59 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 60 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 61 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 62 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 63 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 64 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 65 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 66 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 67 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 70 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 72 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 73 | github.com/tidwall/gjson v1.9.3 h1:hqzS9wAHMO+KVBBkLxYdkEeeFHuqr95GfClRLKlgK0E= 74 | github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 75 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 76 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 77 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 78 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 79 | github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg= 80 | github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= 81 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 82 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 83 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 84 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 85 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 86 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 87 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 88 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 90 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 92 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 93 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 94 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 98 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 99 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 103 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 104 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 105 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 107 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 108 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 109 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 110 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 111 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 112 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 113 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 114 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 115 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 116 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 117 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 118 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 120 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 121 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 122 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 123 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 125 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 126 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 127 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 128 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 129 | k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= 130 | k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= 131 | k8s.io/apiextensions-apiserver v0.30.2 h1:l7Eue2t6QiLHErfn2vwK4KgF4NeDgjQkCXtEbOocKIE= 132 | k8s.io/apiextensions-apiserver v0.30.2/go.mod h1:lsJFLYyK40iguuinsb3nt+Sj6CmodSI4ACDLep1rgjw= 133 | k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= 134 | k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 135 | k8s.io/apiserver v0.30.2 h1:ACouHiYl1yFI2VFI3YGM+lvxgy6ir4yK2oLOsLI1/tw= 136 | k8s.io/apiserver v0.30.2/go.mod h1:BOTdFBIch9Sv0ypSEcUR6ew/NUFGocRFNl72Ra7wTm8= 137 | k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= 138 | k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= 139 | k8s.io/component-base v0.30.2 h1:pqGBczYoW1sno8q9ObExUqrYSKhtE5rW3y6gX88GZII= 140 | k8s.io/component-base v0.30.2/go.mod h1:yQLkQDrkK8J6NtP+MGJOws+/PPeEXNpwFixsUI7h/OE= 141 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 142 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 143 | k8s.io/kubernetes v1.30.2 h1:11WhS78OYX/lnSy6TXxPO6Hk+E5K9ZNrEsk9JgMSX8I= 144 | k8s.io/kubernetes v1.30.2/go.mod h1:yPbIk3MhmhGigX62FLJm+CphNtjxqCvAIFQXup6RKS0= 145 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 146 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 147 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 148 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 149 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 150 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 151 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 152 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 153 | -------------------------------------------------------------------------------- /hack/update-kubernetes-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=${1#"v"} 4 | if [ -z "$VERSION" ]; then 5 | echo "Please specify the Kubernetes version: e.g." 6 | echo "./hack/update-kubernetes-deps.sh v1.21.0" 7 | exit 1 8 | fi 9 | 10 | set -euo pipefail 11 | 12 | # Find out all the replaced imports, make a list of them. 13 | MODS=($( 14 | curl -sS "https://raw.githubusercontent.com/kubernetes/kubernetes/v${VERSION}/go.mod" | 15 | sed -n 's|.*k8s.io/\(.*\) => ./staging/src/k8s.io/.*|k8s.io/\1|p' 16 | )) 17 | 18 | # Now add those similar replace statements in the local go.mod file, but first find the version that 19 | # the Kubernetes is using for them. 20 | for MOD in "${MODS[@]}"; do 21 | V=$( 22 | go mod download -json "${MOD}@kubernetes-${VERSION}" | 23 | sed -n 's|.*"Version": "\(.*\)".*|\1|p' 24 | ) 25 | 26 | go mod edit "-replace=${MOD}=${MOD}@${V}" 27 | done 28 | 29 | go get "k8s.io/kubernetes@v${VERSION}" 30 | go mod download 31 | -------------------------------------------------------------------------------- /krew-package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script makes a platform specific krew package 3 | # it assumes goreleaser had already run and created the archives and the checksums 4 | # Arguments: 5 | # 1. target os (`linux`/`darwin`) 6 | # 2. target arch (`amd64`/`arm64`) 7 | # 2. plugin name (rename the plugin in tests to avoid conflicts with existing installation) 8 | # 3. path to goreleaser dist directory 9 | 10 | os="$1" 11 | arch="$2" 12 | plugin="$3" 13 | dir="$4" 14 | 15 | sha256=$(grep "${os}_$arch" "$dir/checksums.txt" | cut -f1 -d ' ') 16 | tmp="$dir/kubectl-${plugin}_${os}_${arch}.json" 17 | yq -o json krew-template.yaml >"$tmp" 18 | jq 'delpaths([path(.spec.platforms[] | select( .selector.matchLabels.os != $os or .selector.matchLabels.arch != $arch ))])' --arg os "$os" --arg arch "$arch" "$tmp" | sponge "$tmp" 19 | jq '.metadata.name = $name' --arg name "$plugin" "$tmp" | sponge "$tmp" 20 | jq 'setpath(path(.spec.platforms[] | select( .selector.matchLabels.os == $os and .selector.matchLabels.arch == $arch) | .sha256); $sha)' --arg os "$os" --arg arch "$arch" --arg sha "$sha256" "$tmp" | sponge "$tmp" 21 | yq -o yaml --prettyPrint "$tmp" > "${tmp%.json}.yaml" 22 | rm "$tmp" -------------------------------------------------------------------------------- /krew-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: neat 5 | spec: 6 | version: "v2.0.4" 7 | shortDescription: Remove clutter from Kubernetes manifests to make them more readable. 8 | homepage: https://github.com/itaysk/kubectl-neat 9 | description: | 10 | If you try to `kubectl get` resources you have just created, 11 | they be unreadably verbose. `kubectl-neat` cleans that up by 12 | removing default values, runtime information, and other internal fields. 13 | Examples: 14 | `$ kubectl get pod mypod -o yaml | kubectl neat` 15 | `$ kubectl neat get -- pod mypod -o yaml` 16 | platforms: 17 | - selector: 18 | matchLabels: 19 | os: darwin 20 | arch: amd64 21 | uri: https://github.com/itaysk/kubectl-neat/releases/download/v2.0.4/kubectl-neat_darwin_amd64.tar.gz 22 | sha256: ${sha256} 23 | bin: "./kubectl-neat" 24 | - selector: 25 | matchLabels: 26 | os: darwin 27 | arch: arm64 28 | uri: https://github.com/itaysk/kubectl-neat/releases/download/v2.0.4/kubectl-neat_darwin_arm64.tar.gz 29 | sha256: ${sha256} 30 | bin: "./kubectl-neat" 31 | - selector: 32 | matchLabels: 33 | os: linux 34 | arch: amd64 35 | uri: https://github.com/itaysk/kubectl-neat/releases/download/v2.0.4/kubectl-neat_linux_amd64.tar.gz 36 | sha256: ${sha256} 37 | bin: "./kubectl-neat" 38 | - selector: 39 | matchLabels: 40 | os: linux 41 | arch: arm64 42 | uri: https://github.com/itaysk/kubectl-neat/releases/download/v2.0.4/kubectl-neat_linux_arm64.tar.gz 43 | sha256: ${sha256} 44 | bin: "./kubectl-neat" -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Itay Shakury @itaysk 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "github.com/itaysk/kubectl-neat/cmd" 20 | ) 21 | 22 | func main() { 23 | cmd.Execute() 24 | } 25 | -------------------------------------------------------------------------------- /pkg/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/jeremywohl/flatten" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/tidwall/gjson" 10 | "github.com/tidwall/sjson" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | "k8s.io/client-go/kubernetes/scheme" 14 | apisv1 "k8s.io/kubernetes/pkg/apis/core/v1" 15 | ) 16 | 17 | // NeatDefaults gets a json document representing a Kubernetes resource, and removes all fields with default values. 18 | // default values is determined by invoking the "defaulting" code from Kubernetes apimachinery 19 | func NeatDefaults(in string) (string, error) { 20 | var err error 21 | 22 | var pom metav1.PartialObjectMetadata 23 | err = json.Unmarshal([]byte(in), &pom) 24 | if err != nil { 25 | return "", fmt.Errorf("error unmarshaling as PartialObject : %v", err) 26 | } 27 | if !myscheme.Recognizes(pom.GroupVersionKind()) { 28 | return in, nil 29 | } 30 | 31 | specJSON := gjson.Get(in, "spec") 32 | if !specJSON.Exists() { 33 | return in, nil 34 | } 35 | pathsToDelete, err := flatMapJSON(specJSON.String(), "spec.") 36 | if err != nil { 37 | return "", fmt.Errorf("error flattening json : %v", err) 38 | } 39 | for k, v := range pathsToDelete { 40 | isDefault, err := isDefault(k, v, in) 41 | if err != nil { 42 | log.Error(fmt.Errorf("error determining default for '%s' : %v", k, err)) 43 | continue 44 | } 45 | if !isDefault { 46 | // don't want to delete from 'in' yet because that would affect the following isDefault tests 47 | delete(pathsToDelete, k) 48 | } 49 | } 50 | for k := range pathsToDelete { 51 | in, err = sjson.Delete(in, k) 52 | if err != nil { 53 | log.Error(fmt.Errorf("error deleting default '%s' : %v", k, err)) 54 | continue 55 | } 56 | } 57 | return in, nil 58 | } 59 | 60 | // flatMapJSON gets a json document and builds a map of all the leaf keys and their values 61 | func flatMapJSON(j string, prefix string) (map[string]interface{}, error) { 62 | var jParsed map[string]interface{} 63 | err := json.Unmarshal([]byte(j), &jParsed) 64 | if err != nil { 65 | return nil, fmt.Errorf("error unmarshaling: %v", err) 66 | } 67 | res, err := flatten.Flatten(jParsed, prefix, flatten.DotStyle) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return res, nil 72 | } 73 | 74 | var myscheme *runtime.Scheme 75 | var decoder runtime.Decoder 76 | 77 | func init() { 78 | myscheme = runtime.NewScheme() 79 | apisv1.AddToScheme(myscheme) 80 | decoder = scheme.Codecs.UniversalDeserializer() 81 | } 82 | 83 | // isDefault determins if the observed 'value' of the 'path' (gjson path) to field in 'objJSON' is a default value 84 | func isDefault(path string, value interface{}, objJSON string) (bool, error) { 85 | computed, err := computeDefault(path, objJSON) 86 | if err != nil { 87 | return false, fmt.Errorf("error computing default for '%s' : %v", path, err) 88 | } 89 | expect := fmt.Sprintf("%v", value) 90 | return computed == expect, nil 91 | } 92 | 93 | // computeDefault returns the default value for the 'path' (gjson path) to field in 'objJSON' 94 | func computeDefault(path string, objJSON string) (string, error) { 95 | candidateJSON, err := sjson.Delete(objJSON, path) 96 | if err != nil { 97 | return "", fmt.Errorf("error deleting path to default '%s' : %v", path, err) 98 | } 99 | candidate, _, err := decoder.Decode([]byte(candidateJSON), nil, nil) 100 | if err != nil { 101 | return "", fmt.Errorf("error decoding into kubernetes object : %v", err) 102 | } 103 | 104 | // why this doesn't work? 105 | //scheme.Scheme.Default(candidate) 106 | myscheme.Default(candidate) 107 | 108 | resJSON, err := json.Marshal(candidate) 109 | if err != nil { 110 | return "", fmt.Errorf("error marshaling kubernetes object : %v", err) 111 | } 112 | defaultValue := gjson.Get(string(resJSON), path).String() 113 | return defaultValue, nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/defaults/defaults_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Itay Shakury @itaysk 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package defaults 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/itaysk/kubectl-neat/pkg/testutil" 22 | ) 23 | 24 | func TestComputeDefault(t *testing.T) { 25 | cases := []struct { 26 | title string 27 | path string 28 | data string 29 | expect string 30 | }{ 31 | { 32 | title: "PullPolicyAlways", 33 | path: "spec.containers.0.imagePullPolicy", 34 | data: `{ 35 | "apiVersion": "v1", 36 | "kind": "Pod", 37 | "metadata": { 38 | "name": "myapp", 39 | "namespace": "default" 40 | }, 41 | "spec": { 42 | "containers": [ 43 | { 44 | "image": "foo", 45 | "name": "myapp" 46 | } 47 | ] 48 | } 49 | }`, 50 | expect: "Always", 51 | }, 52 | { 53 | title: "PullPolicyIfNotPresent", 54 | path: "spec.containers.0.imagePullPolicy", 55 | data: `{ 56 | "apiVersion": "v1", 57 | "kind": "Pod", 58 | "metadata": { 59 | "name": "myapp", 60 | "namespace": "default" 61 | }, 62 | "spec": { 63 | "containers": [ 64 | { 65 | "image": "foo:bar", 66 | "name": "myapp" 67 | } 68 | ] 69 | } 70 | }`, 71 | expect: "IfNotPresent", 72 | }, 73 | { 74 | title: "RestartPolicy", 75 | path: "spec.restartPolicy", 76 | data: `{ 77 | "apiVersion": "v1", 78 | "kind": "Pod", 79 | "metadata": { 80 | "name": "myapp", 81 | "namespace": "default" 82 | }, 83 | "spec": { 84 | "containers": [ 85 | { 86 | "image": "foo:bar", 87 | "name": "myapp" 88 | } 89 | ] 90 | } 91 | }`, 92 | expect: "Always", 93 | }, 94 | { 95 | title: "TerminationMessagePath", 96 | path: "spec.containers.0.terminationMessagePath", 97 | data: `{ 98 | "apiVersion": "v1", 99 | "kind": "Pod", 100 | "metadata": { 101 | "name": "myapp", 102 | "namespace": "default" 103 | }, 104 | "spec": { 105 | "containers": [ 106 | { 107 | "image": "foo:bar", 108 | "name": "myapp" 109 | } 110 | ] 111 | } 112 | }`, 113 | expect: "/dev/termination-log", 114 | }, 115 | } 116 | for _, c := range cases { 117 | res, err := computeDefault(c.path, c.data) 118 | if err != nil { 119 | t.Errorf("error in computeDefault for case '%s': %v", c.title, err) 120 | } 121 | if res != c.expect { 122 | t.Errorf("test case '%s' failed. want: '%s' have: '%s'", c.title, c.expect, res) 123 | } 124 | } 125 | } 126 | 127 | func TestIsDefault(t *testing.T) { 128 | cases := []struct { 129 | title string 130 | path string 131 | value interface{} 132 | object string 133 | expect bool 134 | }{ 135 | { 136 | title: "PullPolicyAlways", 137 | path: "spec.containers.0.imagePullPolicy", 138 | object: `{ 139 | "apiVersion": "v1", 140 | "kind": "Pod", 141 | "metadata": { 142 | "name": "myapp", 143 | "namespace": "default" 144 | }, 145 | "spec": { 146 | "containers": [ 147 | { 148 | "image": "foo", 149 | "name": "myapp" 150 | } 151 | ] 152 | } 153 | }`, 154 | value: "Always", 155 | expect: true, 156 | }, 157 | { 158 | title: "PullPolicyIfNotPresent", 159 | path: "spec.containers.0.imagePullPolicy", 160 | object: `{ 161 | "apiVersion": "v1", 162 | "kind": "Pod", 163 | "metadata": { 164 | "name": "myapp", 165 | "namespace": "default" 166 | }, 167 | "spec": { 168 | "containers": [ 169 | { 170 | "image": "foo:bar", 171 | "name": "myapp" 172 | } 173 | ] 174 | } 175 | }`, 176 | value: "IfNotPresent", 177 | expect: true, 178 | }, 179 | { 180 | title: "RestartPolicy", 181 | path: "spec.restartPolicy", 182 | object: `{ 183 | "apiVersion": "v1", 184 | "kind": "Pod", 185 | "metadata": { 186 | "name": "myapp", 187 | "namespace": "default" 188 | }, 189 | "spec": { 190 | "containers": [ 191 | { 192 | "image": "foo:bar", 193 | "name": "myapp" 194 | } 195 | ] 196 | } 197 | }`, 198 | value: "Always", 199 | expect: true, 200 | }, 201 | { 202 | title: "TerminationMessagePath", 203 | path: "spec.containers.0.terminationMessagePath", 204 | object: `{ 205 | "apiVersion": "v1", 206 | "kind": "Pod", 207 | "metadata": { 208 | "name": "myapp", 209 | "namespace": "default" 210 | }, 211 | "spec": { 212 | "containers": [ 213 | { 214 | "image": "foo:bar", 215 | "name": "myapp" 216 | } 217 | ] 218 | } 219 | }`, 220 | value: "/dev/termination-log", 221 | expect: true, 222 | }, 223 | } 224 | for _, c := range cases { 225 | res, err := isDefault(c.path, c.value, c.object) 226 | if err != nil { 227 | t.Errorf("error in isDefault for case '%s': %v", c.title, err) 228 | } 229 | if res != c.expect { 230 | t.Errorf("test case '%s' failed. want: '%v' have: '%v'", c.title, c.expect, res) 231 | } 232 | } 233 | } 234 | 235 | func TestNeatDefault(t *testing.T) { 236 | cases := []struct { 237 | title string 238 | data string 239 | expect string 240 | }{ 241 | { 242 | title: "PullPolicyAlways", 243 | data: `{ 244 | "apiVersion": "v1", 245 | "kind": "Pod", 246 | "metadata": { 247 | "name": "myapp", 248 | "namespace": "default" 249 | }, 250 | "spec": { 251 | "containers": [ 252 | { 253 | "image": "foo", 254 | "imagePullPolicy": "Always", 255 | "name": "myapp" 256 | } 257 | ] 258 | } 259 | }`, 260 | expect: `{ 261 | "apiVersion": "v1", 262 | "kind": "Pod", 263 | "metadata": { 264 | "name": "myapp", 265 | "namespace": "default" 266 | }, 267 | "spec": { 268 | "containers": [ 269 | { 270 | "image": "foo", 271 | "name": "myapp" 272 | } 273 | ] 274 | } 275 | }`, 276 | }, 277 | { 278 | title: "PullPolicyIfNotPresent", 279 | data: `{ 280 | "apiVersion": "v1", 281 | "kind": "Pod", 282 | "metadata": { 283 | "name": "myapp", 284 | "namespace": "default" 285 | }, 286 | "spec": { 287 | "containers": [ 288 | { 289 | "image": "foo:bar", 290 | "imagePullPolicy": "IfNotPresent", 291 | "name": "myapp" 292 | } 293 | ] 294 | } 295 | }`, 296 | expect: `{ 297 | "apiVersion": "v1", 298 | "kind": "Pod", 299 | "metadata": { 300 | "name": "myapp", 301 | "namespace": "default" 302 | }, 303 | "spec": { 304 | "containers": [ 305 | { 306 | "image": "foo:bar", 307 | "name": "myapp" 308 | } 309 | ] 310 | } 311 | }`, 312 | }, 313 | { 314 | title: "RestartPolicy", 315 | data: `{ 316 | "apiVersion": "v1", 317 | "kind": "Pod", 318 | "metadata": { 319 | "name": "myapp", 320 | "namespace": "default" 321 | }, 322 | "spec": { 323 | "restartPolicy": "Always", 324 | "containers": [ 325 | { 326 | "image": "foo:bar", 327 | "name": "myapp" 328 | } 329 | ] 330 | } 331 | }`, 332 | expect: `{ 333 | "apiVersion": "v1", 334 | "kind": "Pod", 335 | "metadata": { 336 | "name": "myapp", 337 | "namespace": "default" 338 | }, 339 | "spec": { 340 | "containers": [ 341 | { 342 | "image": "foo:bar", 343 | "name": "myapp" 344 | } 345 | ] 346 | } 347 | }`, 348 | }, 349 | { 350 | title: "TerminationMessagePath", 351 | data: `{ 352 | "apiVersion": "v1", 353 | "kind": "Pod", 354 | "metadata": { 355 | "name": "myapp", 356 | "namespace": "default" 357 | }, 358 | "spec": { 359 | "containers": [ 360 | { 361 | "terminationMessagePath": "/dev/termination-log", 362 | "image": "foo:bar", 363 | "name": "myapp" 364 | } 365 | ] 366 | } 367 | }`, 368 | expect: `{ 369 | "apiVersion": "v1", 370 | "kind": "Pod", 371 | "metadata": { 372 | "name": "myapp", 373 | "namespace": "default" 374 | }, 375 | "spec": { 376 | "containers": [ 377 | { 378 | "image": "foo:bar", 379 | "name": "myapp" 380 | } 381 | ] 382 | } 383 | }`, 384 | }, 385 | { 386 | title: "CRD", 387 | data: `{ 388 | "apiVersion": "networking.istio.io/v1alpha3", 389 | "kind": "DestinationRule", 390 | "metadata": { 391 | "annotations": { 392 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"networking.istio.io/v1alpha3\",\"kind\":\"DestinationRule\",\"metadata\":{\"annotations\":{},\"name\":\"default\",\"namespace\":\"one\"},\"spec\":{\"host\":\"*.one.svc.cluster.local\",\"trafficPolicy\":{\"tls\":{\"mode\":\"ISTIO_MUTUAL\"}}}}\n" 393 | }, 394 | "creationTimestamp": "2019-11-06T20:14:07Z", 395 | "generation": 1, 396 | "name": "default", 397 | "namespace": "one", 398 | "resourceVersion": "314732", 399 | "selfLink": "/apis/networking.istio.io/v1alpha3/namespaces/one/destinationrules/default", 400 | "uid": "fca04858-00d1-11ea-84b3-025000000001" 401 | }, 402 | "spec": { 403 | "host": "*.one.svc.cluster.local", 404 | "trafficPolicy": { 405 | "tls": { 406 | "mode": "ISTIO_MUTUAL" 407 | } 408 | } 409 | } 410 | }`, 411 | expect: `{ 412 | "apiVersion": "networking.istio.io/v1alpha3", 413 | "kind": "DestinationRule", 414 | "metadata": { 415 | "annotations": { 416 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"networking.istio.io/v1alpha3\",\"kind\":\"DestinationRule\",\"metadata\":{\"annotations\":{},\"name\":\"default\",\"namespace\":\"one\"},\"spec\":{\"host\":\"*.one.svc.cluster.local\",\"trafficPolicy\":{\"tls\":{\"mode\":\"ISTIO_MUTUAL\"}}}}\n" 417 | }, 418 | "creationTimestamp": "2019-11-06T20:14:07Z", 419 | "generation": 1, 420 | "name": "default", 421 | "namespace": "one", 422 | "resourceVersion": "314732", 423 | "selfLink": "/apis/networking.istio.io/v1alpha3/namespaces/one/destinationrules/default", 424 | "uid": "fca04858-00d1-11ea-84b3-025000000001" 425 | }, 426 | "spec": { 427 | "host": "*.one.svc.cluster.local", 428 | "trafficPolicy": { 429 | "tls": { 430 | "mode": "ISTIO_MUTUAL" 431 | } 432 | } 433 | } 434 | }`, 435 | }, 436 | } 437 | for _, c := range cases { 438 | resJSON, err := NeatDefaults(c.data) 439 | if err != nil { 440 | t.Errorf("error in neatDefaults for case '%s': %v", c.title, err) 441 | continue 442 | } 443 | equal, err := testutil.JSONEqual(resJSON, c.expect) 444 | if err != nil { 445 | t.Errorf("error in JSONEqual for case '%s': %v", c.title, err) 446 | continue 447 | } 448 | if !equal { 449 | t.Errorf("test case '%s' failed. want: '%s' have: '%s'", c.title, c.expect, resJSON) 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /pkg/testutil/testutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2019 Itay Shakury @itaysk 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package testutil 17 | 18 | import ( 19 | "encoding/json" 20 | "reflect" 21 | ) 22 | 23 | // JSONEqual compares two json strings. true means they are equal 24 | func JSONEqual(a, b string) (bool, error) { 25 | var ao interface{} 26 | var bo interface{} 27 | 28 | var err error 29 | err = json.Unmarshal([]byte(a), &ao) 30 | if err != nil { 31 | return false, err 32 | } 33 | err = json.Unmarshal([]byte(b), &bo) 34 | if err != nil { 35 | return false, err 36 | } 37 | return reflect.DeepEqual(ao, bo), nil 38 | } 39 | -------------------------------------------------------------------------------- /test/bats-workaround.bash: -------------------------------------------------------------------------------- 1 | # run2 reimplements the "run" helper function from bats in order to make it handle stdout and stderr seperately 2 | # issue tracked: https://github.com/bats-core/bats-core/issues/47 3 | # original function: https://github.com/bats-core/bats-core/blob/90ce85884ca89b48960194b3d3bf6b816285e053/lib/bats-core/test_functions.bash#L32:L45 4 | function run2() { 5 | local origFlags="$-" 6 | set +eETx 7 | local origIFS="$IFS" 8 | # 'output', 'status', 'lines' are global variables available to tests. 9 | local tmperr=$(mktemp) 10 | stdout="$("$@" 2>"$tmperr")" 11 | # shellcheck disable=SC2034 12 | status="$?" 13 | # shellcheck disable=SC2034 14 | stderr="$(cat <"$tmperr")" 15 | rm "$tmperr" 16 | # shellcheck disable=SC2034,SC2206 17 | IFS=$'\n' stdoutLines=($stdout) 18 | # shellcheck disable=SC2034,SC2206 19 | IFS=$'\n' stderrLines=($stderr) 20 | IFS="$origIFS" 21 | set "-$origFlags" 22 | } -------------------------------------------------------------------------------- /test/bats-workaround_test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load bats-workaround.bash 4 | 5 | function exercise_run2() { 6 | echo "out1" 7 | echo "err1" >&2 8 | echo "out2" 9 | echo "err2" >&2 10 | return 42 11 | } 12 | 13 | @test "run2" { 14 | run2 exercise_run2 15 | [[ "$status" -eq 42 ]] 16 | [[ "$stdout" == "out1 17 | out2" ]] 18 | [[ "$stderr" == "err1 19 | err2" ]] 20 | [[ "${stdoutLines[1]}" == "out2" ]] 21 | [[ "${stderrLines[1]}" == "err2" ]] 22 | } -------------------------------------------------------------------------------- /test/e2e-cli.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | load bats-workaround 3 | runtime_os=$(uname -s | tr '[:upper:]' '[:lower:]') 4 | runtime_arch=$(go env GOARCH | tr '[:upper:]' '[:lower:]') 5 | exe="dist/kubectl-neat_${runtime_os}_${runtime_arch}" 6 | rootDir="./test/fixtures" 7 | 8 | @test "invalid args 1" { 9 | echo $exe >&3 10 | run2 "$exe" --foo 11 | [ $status -eq 1 ] 12 | [[ "$stderr" == "Error: unknown flag: --foo"* ]] 13 | } 14 | 15 | @test "invalid args 2" { 16 | run2 "$exe" get --foo 17 | [ $status -eq 1 ] 18 | [[ "$stderr" == "Error: Error invoking kubectl"* ]] 19 | } 20 | 21 | @test "invalid args 3" { 22 | run2 "$exe" foo 23 | [ $status -eq 1 ] 24 | [[ "$stderr" == 'Error: unknown command "foo" for "kubectl-neat"'* ]] 25 | } 26 | 27 | @test "local file" { 28 | run2 "$exe" -f - <"$rootDir/pod1-raw.yaml" 29 | [ $status -eq 0 ] 30 | [[ "$stdout" == "apiVersion"* ]] 31 | } -------------------------------------------------------------------------------- /test/e2e-krew.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | load bats-workaround 3 | 4 | function setup() { 5 | dir="dist" 6 | plugin="neat2" 7 | runtime_os=$(uname -s | tr '[:upper:]' '[:lower:]') 8 | runtime_arch=$(go env GOARCH | tr '[:upper:]' '[:lower:]') 9 | ./krew-package.sh "$runtime_os" "$runtime_arch" "$plugin" "$dir" 10 | kubectl krew install --manifest="$dir/kubectl-${plugin}_${runtime_os}_${runtime_arch}.yaml" --archive="$dir/kubectl-neat_${runtime_os}_${runtime_arch}.tar.gz" 11 | } 12 | 13 | function teardown() { 14 | kubectl krew remove "$plugin" 15 | } 16 | 17 | @test "krew install" { 18 | run2 kubectl "$plugin" get -- svc kubernetes -n default 19 | [ "$status" -eq 0 ] 20 | [ "${stdoutLines[1]}" = "kind: Service" ] 21 | } -------------------------------------------------------------------------------- /test/e2e-kubectl.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | load bats-workaround 3 | 4 | function setup() { 5 | runtime_os=$(uname -s | tr '[:upper:]' '[:lower:]') 6 | runtime_arch=$(go env GOARCH | tr '[:upper:]' '[:lower:]') 7 | tmpdir=$(mktemp -d) 8 | # rename the plugin to avoid conflicts with existing installation 9 | plugin_name="neat2" 10 | plugin="$tmpdir"/kubectl-"$plugin_name" 11 | exe="$PWD/$(find dist -path \*dist/kubectl-neat_${runtime_os}_${runtime_arch}\*/kubectl-neat)" 12 | ln -s "$exe" "$plugin" 13 | # PATH modification here has no external affect since bats runs in a subshell 14 | PATH="$PATH":"$tmpdir" 15 | } 16 | 17 | function teardown() { 18 | rm -rf "$tmpdir" 19 | } 20 | 21 | @test "plugin - json" { 22 | run2 kubectl "$plugin_name" get -o json -- svc kubernetes -n default 23 | [ "$status" -eq 0 ] 24 | [[ $stdout == "{"* ]] 25 | } 26 | 27 | @test "plugin - yaml" { 28 | run2 kubectl "$plugin_name" get -- svc kubernetes -n default 29 | [ "$status" -eq 0 ] 30 | [[ $stdout == "apiVersion"* ]] 31 | } -------------------------------------------------------------------------------- /test/fixtures/list1-neat.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "items": [ 4 | { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": { 8 | "labels": { 9 | "run": "t1" 10 | }, 11 | "name": "t1", 12 | "namespace": "default" 13 | }, 14 | "spec": { 15 | "containers": [ 16 | { 17 | "image": "itaysk/cyan", 18 | "name": "t1" 19 | } 20 | ], 21 | "priority": 0, 22 | "serviceAccountName": "default", 23 | "tolerations": [ 24 | { 25 | "effect": "NoExecute", 26 | "key": "node.kubernetes.io/not-ready", 27 | "operator": "Exists", 28 | "tolerationSeconds": 300 29 | }, 30 | { 31 | "effect": "NoExecute", 32 | "key": "node.kubernetes.io/unreachable", 33 | "operator": "Exists", 34 | "tolerationSeconds": 300 35 | } 36 | ] 37 | } 38 | }, 39 | { 40 | "apiVersion": "v1", 41 | "kind": "Pod", 42 | "metadata": { 43 | "labels": { 44 | "run": "t2" 45 | }, 46 | "name": "t2", 47 | "namespace": "default" 48 | }, 49 | "spec": { 50 | "containers": [ 51 | { 52 | "image": "itaysk/cyan", 53 | "name": "t2" 54 | } 55 | ], 56 | "priority": 0, 57 | "serviceAccountName": "default", 58 | "tolerations": [ 59 | { 60 | "effect": "NoExecute", 61 | "key": "node.kubernetes.io/not-ready", 62 | "operator": "Exists", 63 | "tolerationSeconds": 300 64 | }, 65 | { 66 | "effect": "NoExecute", 67 | "key": "node.kubernetes.io/unreachable", 68 | "operator": "Exists", 69 | "tolerationSeconds": 300 70 | } 71 | ] 72 | } 73 | } 74 | ], 75 | "kind": "List", 76 | "metadata": { 77 | } 78 | } -------------------------------------------------------------------------------- /test/fixtures/list1-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "items": [ 4 | { 5 | "apiVersion": "v1", 6 | "kind": "Pod", 7 | "metadata": { 8 | "creationTimestamp": "2020-05-29T15:59:24Z", 9 | "labels": { 10 | "run": "t1" 11 | }, 12 | "name": "t1", 13 | "namespace": "default", 14 | "resourceVersion": "564", 15 | "selfLink": "/api/v1/namespaces/default/pods/t1", 16 | "uid": "2fd916b3-3df3-41ff-87b7-0213c60210cd" 17 | }, 18 | "spec": { 19 | "containers": [ 20 | { 21 | "image": "itaysk/cyan", 22 | "imagePullPolicy": "Always", 23 | "name": "t1", 24 | "resources": {}, 25 | "terminationMessagePath": "/dev/termination-log", 26 | "terminationMessagePolicy": "File", 27 | "volumeMounts": [ 28 | { 29 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 30 | "name": "default-token-m7wjs", 31 | "readOnly": true 32 | } 33 | ] 34 | } 35 | ], 36 | "dnsPolicy": "ClusterFirst", 37 | "enableServiceLinks": true, 38 | "nodeName": "116-control-plane", 39 | "priority": 0, 40 | "restartPolicy": "Always", 41 | "schedulerName": "default-scheduler", 42 | "securityContext": {}, 43 | "serviceAccount": "default", 44 | "serviceAccountName": "default", 45 | "terminationGracePeriodSeconds": 30, 46 | "tolerations": [ 47 | { 48 | "effect": "NoExecute", 49 | "key": "node.kubernetes.io/not-ready", 50 | "operator": "Exists", 51 | "tolerationSeconds": 300 52 | }, 53 | { 54 | "effect": "NoExecute", 55 | "key": "node.kubernetes.io/unreachable", 56 | "operator": "Exists", 57 | "tolerationSeconds": 300 58 | } 59 | ], 60 | "volumes": [ 61 | { 62 | "name": "default-token-m7wjs", 63 | "secret": { 64 | "defaultMode": 420, 65 | "secretName": "default-token-m7wjs" 66 | } 67 | } 68 | ] 69 | }, 70 | "status": { 71 | "conditions": [ 72 | { 73 | "lastProbeTime": null, 74 | "lastTransitionTime": "2020-05-29T15:59:24Z", 75 | "status": "True", 76 | "type": "Initialized" 77 | }, 78 | { 79 | "lastProbeTime": null, 80 | "lastTransitionTime": "2020-05-29T15:59:32Z", 81 | "status": "True", 82 | "type": "Ready" 83 | }, 84 | { 85 | "lastProbeTime": null, 86 | "lastTransitionTime": "2020-05-29T15:59:32Z", 87 | "status": "True", 88 | "type": "ContainersReady" 89 | }, 90 | { 91 | "lastProbeTime": null, 92 | "lastTransitionTime": "2020-05-29T15:59:24Z", 93 | "status": "True", 94 | "type": "PodScheduled" 95 | } 96 | ], 97 | "containerStatuses": [ 98 | { 99 | "containerID": "containerd://8b896cbabfdac1004b2432632dcc9f56e0bab67fbd9512bf1cc26db5847a55fc", 100 | "image": "docker.io/itaysk/cyan:latest", 101 | "imageID": "docker.io/itaysk/cyan@sha256:0fcfc9231fa11b473973a68c0394e7c85e2117a9ba7a9476897c07e40d718ffa", 102 | "lastState": {}, 103 | "name": "t1", 104 | "ready": true, 105 | "restartCount": 0, 106 | "started": true, 107 | "state": { 108 | "running": { 109 | "startedAt": "2020-05-29T15:59:31Z" 110 | } 111 | } 112 | } 113 | ], 114 | "hostIP": "172.18.0.2", 115 | "phase": "Running", 116 | "podIP": "10.244.0.5", 117 | "podIPs": [ 118 | { 119 | "ip": "10.244.0.5" 120 | } 121 | ], 122 | "qosClass": "BestEffort", 123 | "startTime": "2020-05-29T15:59:24Z" 124 | } 125 | }, 126 | { 127 | "apiVersion": "v1", 128 | "kind": "Pod", 129 | "metadata": { 130 | "creationTimestamp": "2020-05-29T15:59:37Z", 131 | "labels": { 132 | "run": "t2" 133 | }, 134 | "name": "t2", 135 | "namespace": "default", 136 | "resourceVersion": "600", 137 | "selfLink": "/api/v1/namespaces/default/pods/t2", 138 | "uid": "375f3cc4-6bb4-4880-b3f3-0d3c43eef30c" 139 | }, 140 | "spec": { 141 | "containers": [ 142 | { 143 | "image": "itaysk/cyan", 144 | "imagePullPolicy": "Always", 145 | "name": "t2", 146 | "resources": {}, 147 | "terminationMessagePath": "/dev/termination-log", 148 | "terminationMessagePolicy": "File", 149 | "volumeMounts": [ 150 | { 151 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 152 | "name": "default-token-m7wjs", 153 | "readOnly": true 154 | } 155 | ] 156 | } 157 | ], 158 | "dnsPolicy": "ClusterFirst", 159 | "enableServiceLinks": true, 160 | "nodeName": "116-control-plane", 161 | "priority": 0, 162 | "restartPolicy": "Always", 163 | "schedulerName": "default-scheduler", 164 | "securityContext": {}, 165 | "serviceAccount": "default", 166 | "serviceAccountName": "default", 167 | "terminationGracePeriodSeconds": 30, 168 | "tolerations": [ 169 | { 170 | "effect": "NoExecute", 171 | "key": "node.kubernetes.io/not-ready", 172 | "operator": "Exists", 173 | "tolerationSeconds": 300 174 | }, 175 | { 176 | "effect": "NoExecute", 177 | "key": "node.kubernetes.io/unreachable", 178 | "operator": "Exists", 179 | "tolerationSeconds": 300 180 | } 181 | ], 182 | "volumes": [ 183 | { 184 | "name": "default-token-m7wjs", 185 | "secret": { 186 | "defaultMode": 420, 187 | "secretName": "default-token-m7wjs" 188 | } 189 | } 190 | ] 191 | }, 192 | "status": { 193 | "conditions": [ 194 | { 195 | "lastProbeTime": null, 196 | "lastTransitionTime": "2020-05-29T15:59:37Z", 197 | "status": "True", 198 | "type": "Initialized" 199 | }, 200 | { 201 | "lastProbeTime": null, 202 | "lastTransitionTime": "2020-05-29T15:59:40Z", 203 | "status": "True", 204 | "type": "Ready" 205 | }, 206 | { 207 | "lastProbeTime": null, 208 | "lastTransitionTime": "2020-05-29T15:59:40Z", 209 | "status": "True", 210 | "type": "ContainersReady" 211 | }, 212 | { 213 | "lastProbeTime": null, 214 | "lastTransitionTime": "2020-05-29T15:59:37Z", 215 | "status": "True", 216 | "type": "PodScheduled" 217 | } 218 | ], 219 | "containerStatuses": [ 220 | { 221 | "containerID": "containerd://5e3db942e312548c103c334ce3ba00751360c07049971c8a00d2044ab055129b", 222 | "image": "docker.io/itaysk/cyan:latest", 223 | "imageID": "docker.io/itaysk/cyan@sha256:0fcfc9231fa11b473973a68c0394e7c85e2117a9ba7a9476897c07e40d718ffa", 224 | "lastState": {}, 225 | "name": "t2", 226 | "ready": true, 227 | "restartCount": 0, 228 | "started": true, 229 | "state": { 230 | "running": { 231 | "startedAt": "2020-05-29T15:59:39Z" 232 | } 233 | } 234 | } 235 | ], 236 | "hostIP": "172.18.0.2", 237 | "phase": "Running", 238 | "podIP": "10.244.0.7", 239 | "podIPs": [ 240 | { 241 | "ip": "10.244.0.7" 242 | } 243 | ], 244 | "qosClass": "BestEffort", 245 | "startTime": "2020-05-29T15:59:37Z" 246 | } 247 | } 248 | ], 249 | "kind": "List", 250 | "metadata": { 251 | "resourceVersion": "", 252 | "selfLink": "" 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /test/fixtures/list1-raw.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | items: 3 | - apiVersion: v1 4 | kind: Pod 5 | metadata: 6 | creationTimestamp: "2020-05-29T15:59:24Z" 7 | labels: 8 | run: t1 9 | name: t1 10 | namespace: default 11 | resourceVersion: "564" 12 | selfLink: /api/v1/namespaces/default/pods/t1 13 | uid: 2fd916b3-3df3-41ff-87b7-0213c60210cd 14 | spec: 15 | containers: 16 | - image: itaysk/cyan 17 | imagePullPolicy: Always 18 | name: t1 19 | resources: {} 20 | terminationMessagePath: /dev/termination-log 21 | terminationMessagePolicy: File 22 | volumeMounts: 23 | - mountPath: /var/run/secrets/kubernetes.io/serviceaccount 24 | name: default-token-m7wjs 25 | readOnly: true 26 | dnsPolicy: ClusterFirst 27 | enableServiceLinks: true 28 | nodeName: 116-control-plane 29 | priority: 0 30 | restartPolicy: Always 31 | schedulerName: default-scheduler 32 | securityContext: {} 33 | serviceAccount: default 34 | serviceAccountName: default 35 | terminationGracePeriodSeconds: 30 36 | tolerations: 37 | - effect: NoExecute 38 | key: node.kubernetes.io/not-ready 39 | operator: Exists 40 | tolerationSeconds: 300 41 | - effect: NoExecute 42 | key: node.kubernetes.io/unreachable 43 | operator: Exists 44 | tolerationSeconds: 300 45 | volumes: 46 | - name: default-token-m7wjs 47 | secret: 48 | defaultMode: 420 49 | secretName: default-token-m7wjs 50 | status: 51 | conditions: 52 | - lastProbeTime: null 53 | lastTransitionTime: "2020-05-29T15:59:24Z" 54 | status: "True" 55 | type: Initialized 56 | - lastProbeTime: null 57 | lastTransitionTime: "2020-05-29T15:59:32Z" 58 | status: "True" 59 | type: Ready 60 | - lastProbeTime: null 61 | lastTransitionTime: "2020-05-29T15:59:32Z" 62 | status: "True" 63 | type: ContainersReady 64 | - lastProbeTime: null 65 | lastTransitionTime: "2020-05-29T15:59:24Z" 66 | status: "True" 67 | type: PodScheduled 68 | containerStatuses: 69 | - containerID: containerd://8b896cbabfdac1004b2432632dcc9f56e0bab67fbd9512bf1cc26db5847a55fc 70 | image: docker.io/itaysk/cyan:latest 71 | imageID: docker.io/itaysk/cyan@sha256:0fcfc9231fa11b473973a68c0394e7c85e2117a9ba7a9476897c07e40d718ffa 72 | lastState: {} 73 | name: t1 74 | ready: true 75 | restartCount: 0 76 | started: true 77 | state: 78 | running: 79 | startedAt: "2020-05-29T15:59:31Z" 80 | hostIP: 172.18.0.2 81 | phase: Running 82 | podIP: 10.244.0.5 83 | podIPs: 84 | - ip: 10.244.0.5 85 | qosClass: BestEffort 86 | startTime: "2020-05-29T15:59:24Z" 87 | - apiVersion: v1 88 | kind: Pod 89 | metadata: 90 | creationTimestamp: "2020-05-29T15:59:37Z" 91 | labels: 92 | run: t2 93 | name: t2 94 | namespace: default 95 | resourceVersion: "600" 96 | selfLink: /api/v1/namespaces/default/pods/t2 97 | uid: 375f3cc4-6bb4-4880-b3f3-0d3c43eef30c 98 | spec: 99 | containers: 100 | - image: itaysk/cyan 101 | imagePullPolicy: Always 102 | name: t2 103 | resources: {} 104 | terminationMessagePath: /dev/termination-log 105 | terminationMessagePolicy: File 106 | volumeMounts: 107 | - mountPath: /var/run/secrets/kubernetes.io/serviceaccount 108 | name: default-token-m7wjs 109 | readOnly: true 110 | dnsPolicy: ClusterFirst 111 | enableServiceLinks: true 112 | nodeName: 116-control-plane 113 | priority: 0 114 | restartPolicy: Always 115 | schedulerName: default-scheduler 116 | securityContext: {} 117 | serviceAccount: default 118 | serviceAccountName: default 119 | terminationGracePeriodSeconds: 30 120 | tolerations: 121 | - effect: NoExecute 122 | key: node.kubernetes.io/not-ready 123 | operator: Exists 124 | tolerationSeconds: 300 125 | - effect: NoExecute 126 | key: node.kubernetes.io/unreachable 127 | operator: Exists 128 | tolerationSeconds: 300 129 | volumes: 130 | - name: default-token-m7wjs 131 | secret: 132 | defaultMode: 420 133 | secretName: default-token-m7wjs 134 | status: 135 | conditions: 136 | - lastProbeTime: null 137 | lastTransitionTime: "2020-05-29T15:59:37Z" 138 | status: "True" 139 | type: Initialized 140 | - lastProbeTime: null 141 | lastTransitionTime: "2020-05-29T15:59:40Z" 142 | status: "True" 143 | type: Ready 144 | - lastProbeTime: null 145 | lastTransitionTime: "2020-05-29T15:59:40Z" 146 | status: "True" 147 | type: ContainersReady 148 | - lastProbeTime: null 149 | lastTransitionTime: "2020-05-29T15:59:37Z" 150 | status: "True" 151 | type: PodScheduled 152 | containerStatuses: 153 | - containerID: containerd://5e3db942e312548c103c334ce3ba00751360c07049971c8a00d2044ab055129b 154 | image: docker.io/itaysk/cyan:latest 155 | imageID: docker.io/itaysk/cyan@sha256:0fcfc9231fa11b473973a68c0394e7c85e2117a9ba7a9476897c07e40d718ffa 156 | lastState: {} 157 | name: t2 158 | ready: true 159 | restartCount: 0 160 | started: true 161 | state: 162 | running: 163 | startedAt: "2020-05-29T15:59:39Z" 164 | hostIP: 172.18.0.2 165 | phase: Running 166 | podIP: 10.244.0.7 167 | podIPs: 168 | - ip: 10.244.0.7 169 | qosClass: BestEffort 170 | startTime: "2020-05-29T15:59:37Z" 171 | kind: List 172 | metadata: 173 | resourceVersion: "" 174 | selfLink: "" 175 | -------------------------------------------------------------------------------- /test/fixtures/pod1-neat.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Pod", 4 | "metadata": { 5 | "labels": { 6 | "name": "myapp" 7 | }, 8 | "name": "myapp", 9 | "namespace": "default" 10 | }, 11 | "spec": { 12 | "containers": [ 13 | { 14 | "image": "nginx", 15 | "name": "myapp", 16 | "ports": [ 17 | { 18 | "containerPort": 1234 19 | } 20 | ] 21 | } 22 | ], 23 | "priority": 0, 24 | "serviceAccountName": "default", 25 | "tolerations": [ 26 | { 27 | "effect": "NoExecute", 28 | "key": "node.kubernetes.io/not-ready", 29 | "operator": "Exists", 30 | "tolerationSeconds": 300 31 | }, 32 | { 33 | "effect": "NoExecute", 34 | "key": "node.kubernetes.io/unreachable", 35 | "operator": "Exists", 36 | "tolerationSeconds": 300 37 | } 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/pod1-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Pod", 4 | "metadata": { 5 | "creationTimestamp": "2019-04-24T19:55:27Z", 6 | "labels": { 7 | "name": "myapp" 8 | }, 9 | "name": "myapp", 10 | "namespace": "default", 11 | "resourceVersion": "274103", 12 | "selfLink": "/api/v1/namespaces/default/pods/myapp", 13 | "uid": "e8330f3c-66ca-11e9-b6fa-0800271788ca" 14 | }, 15 | "spec": { 16 | "containers": [ 17 | { 18 | "image": "nginx", 19 | "imagePullPolicy": "Always", 20 | "name": "myapp", 21 | "ports": [ 22 | { 23 | "containerPort": 1234, 24 | "protocol": "TCP" 25 | } 26 | ], 27 | "resources": {}, 28 | "terminationMessagePath": "/dev/termination-log", 29 | "terminationMessagePolicy": "File", 30 | "volumeMounts": [ 31 | { 32 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 33 | "name": "default-token-nmshj", 34 | "readOnly": true 35 | } 36 | ] 37 | } 38 | ], 39 | "dnsPolicy": "ClusterFirst", 40 | "enableServiceLinks": true, 41 | "nodeName": "minikube", 42 | "priority": 0, 43 | "restartPolicy": "Always", 44 | "schedulerName": "default-scheduler", 45 | "securityContext": {}, 46 | "serviceAccount": "default", 47 | "serviceAccountName": "default", 48 | "terminationGracePeriodSeconds": 30, 49 | "tolerations": [ 50 | { 51 | "effect": "NoExecute", 52 | "key": "node.kubernetes.io/not-ready", 53 | "operator": "Exists", 54 | "tolerationSeconds": 300 55 | }, 56 | { 57 | "effect": "NoExecute", 58 | "key": "node.kubernetes.io/unreachable", 59 | "operator": "Exists", 60 | "tolerationSeconds": 300 61 | } 62 | ], 63 | "volumes": [ 64 | { 65 | "name": "default-token-nmshj", 66 | "secret": { 67 | "defaultMode": 420, 68 | "secretName": "default-token-nmshj" 69 | } 70 | } 71 | ] 72 | }, 73 | "status": { 74 | "conditions": [ 75 | { 76 | "lastProbeTime": null, 77 | "lastTransitionTime": "2019-04-24T19:55:27Z", 78 | "status": "True", 79 | "type": "Initialized" 80 | }, 81 | { 82 | "lastProbeTime": null, 83 | "lastTransitionTime": "2019-07-06T18:41:25Z", 84 | "status": "True", 85 | "type": "Ready" 86 | }, 87 | { 88 | "lastProbeTime": null, 89 | "lastTransitionTime": "2019-07-06T18:41:25Z", 90 | "status": "True", 91 | "type": "ContainersReady" 92 | }, 93 | { 94 | "lastProbeTime": null, 95 | "lastTransitionTime": "2019-04-24T19:55:27Z", 96 | "status": "True", 97 | "type": "PodScheduled" 98 | } 99 | ], 100 | "containerStatuses": [ 101 | { 102 | "containerID": "docker://92d7dc7a851453c2f1e75c4af42a9e72fea50127fede62dfbd5fbb6fb0481fcc", 103 | "image": "nginx:latest", 104 | "imageID": "docker-pullable://nginx@sha256:96fb261b66270b900ea5a2c17a26abbfabe95506e73c3a3c65869a6dbe83223a", 105 | "lastState": { 106 | "terminated": { 107 | "containerID": "docker://288fc0a2b98708d6a4661f59c54c4ae366c1acea642f000ba9615932dbff411f", 108 | "exitCode": 0, 109 | "finishedAt": "2019-07-04T08:17:20Z", 110 | "reason": "Completed", 111 | "startedAt": "2019-07-03T05:55:39Z" 112 | } 113 | }, 114 | "name": "myapp", 115 | "ready": true, 116 | "restartCount": 3, 117 | "state": { 118 | "running": { 119 | "startedAt": "2019-07-06T18:41:25Z" 120 | } 121 | } 122 | } 123 | ], 124 | "hostIP": "10.0.2.15", 125 | "phase": "Running", 126 | "podIP": "172.17.0.2", 127 | "qosClass": "BestEffort", 128 | "startTime": "2019-04-24T19:55:27Z" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/fixtures/pod1-raw.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | creationTimestamp: "2019-04-24T19:55:27Z" 5 | labels: 6 | name: myapp 7 | name: myapp 8 | namespace: default 9 | resourceVersion: "274103" 10 | selfLink: /api/v1/namespaces/default/pods/myapp 11 | uid: e8330f3c-66ca-11e9-b6fa-0800271788ca 12 | spec: 13 | containers: 14 | - image: nginx 15 | imagePullPolicy: Always 16 | name: myapp 17 | ports: 18 | - containerPort: 1234 19 | protocol: TCP 20 | resources: {} 21 | terminationMessagePath: /dev/termination-log 22 | terminationMessagePolicy: File 23 | volumeMounts: 24 | - mountPath: /var/run/secrets/kubernetes.io/serviceaccount 25 | name: default-token-nmshj 26 | readOnly: true 27 | dnsPolicy: ClusterFirst 28 | enableServiceLinks: true 29 | nodeName: minikube 30 | priority: 0 31 | restartPolicy: Always 32 | schedulerName: default-scheduler 33 | securityContext: {} 34 | serviceAccount: default 35 | serviceAccountName: default 36 | terminationGracePeriodSeconds: 30 37 | tolerations: 38 | - effect: NoExecute 39 | key: node.kubernetes.io/not-ready 40 | operator: Exists 41 | tolerationSeconds: 300 42 | - effect: NoExecute 43 | key: node.kubernetes.io/unreachable 44 | operator: Exists 45 | tolerationSeconds: 300 46 | volumes: 47 | - name: default-token-nmshj 48 | secret: 49 | defaultMode: 420 50 | secretName: default-token-nmshj 51 | status: 52 | conditions: 53 | - lastProbeTime: null 54 | lastTransitionTime: "2019-04-24T19:55:27Z" 55 | status: "True" 56 | type: Initialized 57 | - lastProbeTime: null 58 | lastTransitionTime: "2019-07-06T18:41:25Z" 59 | status: "True" 60 | type: Ready 61 | - lastProbeTime: null 62 | lastTransitionTime: "2019-07-06T18:41:25Z" 63 | status: "True" 64 | type: ContainersReady 65 | - lastProbeTime: null 66 | lastTransitionTime: "2019-04-24T19:55:27Z" 67 | status: "True" 68 | type: PodScheduled 69 | containerStatuses: 70 | - containerID: docker://92d7dc7a851453c2f1e75c4af42a9e72fea50127fede62dfbd5fbb6fb0481fcc 71 | image: nginx:latest 72 | imageID: docker-pullable://nginx@sha256:96fb261b66270b900ea5a2c17a26abbfabe95506e73c3a3c65869a6dbe83223a 73 | lastState: 74 | terminated: 75 | containerID: docker://288fc0a2b98708d6a4661f59c54c4ae366c1acea642f000ba9615932dbff411f 76 | exitCode: 0 77 | finishedAt: "2019-07-04T08:17:20Z" 78 | reason: Completed 79 | startedAt: "2019-07-03T05:55:39Z" 80 | name: myapp 81 | ready: true 82 | restartCount: 3 83 | state: 84 | running: 85 | startedAt: "2019-07-06T18:41:25Z" 86 | hostIP: 10.0.2.15 87 | phase: Running 88 | podIP: 172.17.0.2 89 | qosClass: BestEffort 90 | startTime: "2019-04-24T19:55:27Z" 91 | -------------------------------------------------------------------------------- /test/fixtures/pv1-neat.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "PersistentVolume", 4 | "metadata": { 5 | "name": "pvc-54fad2fe-4d7b-11e9-9172-0800271788ca", 6 | "annotations": { 7 | "hostPathProvisionerIdentity": "7de69121-4d7a-11e9-8684-0800271788ca", 8 | "pv.kubernetes.io/provisioned-by": "k8s.io/minikube-hostpath" 9 | } 10 | }, 11 | "spec": { 12 | "accessModes": [ 13 | "ReadWriteOnce" 14 | ], 15 | "capacity": { 16 | "storage": "2Gi" 17 | }, 18 | "claimRef": { 19 | "apiVersion": "v1", 20 | "kind": "PersistentVolumeClaim", 21 | "name": "prom-prometheus-alertmanager", 22 | "namespace": "default", 23 | "resourceVersion": "860", 24 | "uid": "54fad2fe-4d7b-11e9-9172-0800271788ca" 25 | }, 26 | "hostPath": { 27 | "path": "/tmp/hostpath-provisioner/pvc-54fad2fe-4d7b-11e9-9172-0800271788ca" 28 | }, 29 | "persistentVolumeReclaimPolicy": "Delete", 30 | "storageClassName": "standard" 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/pv1-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "PersistentVolume", 4 | "metadata": { 5 | "annotations": { 6 | "hostPathProvisionerIdentity": "7de69121-4d7a-11e9-8684-0800271788ca", 7 | "pv.kubernetes.io/provisioned-by": "k8s.io/minikube-hostpath" 8 | }, 9 | "creationTimestamp": "2019-03-23T14:52:51Z", 10 | "finalizers": [ 11 | "kubernetes.io/pv-protection" 12 | ], 13 | "name": "pvc-54fad2fe-4d7b-11e9-9172-0800271788ca", 14 | "resourceVersion": "186863", 15 | "selfLink": "/api/v1/persistentvolumes/pvc-54fad2fe-4d7b-11e9-9172-0800271788ca", 16 | "uid": "5527dbad-4d7b-11e9-9172-0800271788ca" 17 | }, 18 | "spec": { 19 | "accessModes": [ 20 | "ReadWriteOnce" 21 | ], 22 | "capacity": { 23 | "storage": "2Gi" 24 | }, 25 | "claimRef": { 26 | "apiVersion": "v1", 27 | "kind": "PersistentVolumeClaim", 28 | "name": "prom-prometheus-alertmanager", 29 | "namespace": "default", 30 | "resourceVersion": "860", 31 | "uid": "54fad2fe-4d7b-11e9-9172-0800271788ca" 32 | }, 33 | "hostPath": { 34 | "path": "/tmp/hostpath-provisioner/pvc-54fad2fe-4d7b-11e9-9172-0800271788ca", 35 | "type": "" 36 | }, 37 | "persistentVolumeReclaimPolicy": "Delete", 38 | "storageClassName": "standard", 39 | "volumeMode": "Filesystem" 40 | }, 41 | "status": { 42 | "phase": "Released" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/fixtures/pv1-raw.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | annotations: 5 | hostPathProvisionerIdentity: 7de69121-4d7a-11e9-8684-0800271788ca 6 | pv.kubernetes.io/provisioned-by: k8s.io/minikube-hostpath 7 | creationTimestamp: "2019-03-23T14:52:51Z" 8 | finalizers: 9 | - kubernetes.io/pv-protection 10 | name: pvc-54fad2fe-4d7b-11e9-9172-0800271788ca 11 | resourceVersion: "186863" 12 | selfLink: /api/v1/persistentvolumes/pvc-54fad2fe-4d7b-11e9-9172-0800271788ca 13 | uid: 5527dbad-4d7b-11e9-9172-0800271788ca 14 | spec: 15 | accessModes: 16 | - ReadWriteOnce 17 | capacity: 18 | storage: 2Gi 19 | claimRef: 20 | apiVersion: v1 21 | kind: PersistentVolumeClaim 22 | name: prom-prometheus-alertmanager 23 | namespace: default 24 | resourceVersion: "860" 25 | uid: 54fad2fe-4d7b-11e9-9172-0800271788ca 26 | hostPath: 27 | path: /tmp/hostpath-provisioner/pvc-54fad2fe-4d7b-11e9-9172-0800271788ca 28 | type: "" 29 | persistentVolumeReclaimPolicy: Delete 30 | storageClassName: standard 31 | volumeMode: Filesystem 32 | status: 33 | phase: Released 34 | -------------------------------------------------------------------------------- /test/fixtures/role-neat.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "rbac.authorization.k8s.io/v1", 3 | "kind": "Role", 4 | "metadata": {"name":"kubeadm:kubelet-config-1.18","namespace":"kube-system"}, 5 | "rules": [ 6 | { 7 | "apiGroups": [ 8 | "" 9 | ], 10 | "resourceNames": [ 11 | "kubelet-config-1.18" 12 | ], 13 | "resources": [ 14 | "configmaps" 15 | ], 16 | "verbs": [ 17 | "get" 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/role-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "rbac.authorization.k8s.io/v1", 3 | "kind": "Role", 4 | "metadata": { 5 | "creationTimestamp": "2020-05-08T19:45:27Z", 6 | "managedFields": [ 7 | { 8 | "apiVersion": "rbac.authorization.k8s.io/v1", 9 | "fieldsType": "FieldsV1", 10 | "fieldsV1": { 11 | "f:rules": {} 12 | }, 13 | "manager": "kubeadm", 14 | "operation": "Update", 15 | "time": "2020-05-08T19:45:27Z" 16 | } 17 | ], 18 | "name": "kubeadm:kubelet-config-1.18", 19 | "namespace": "kube-system", 20 | "resourceVersion": "162", 21 | "selfLink": "/apis/rbac.authorization.k8s.io/v1/namespaces/kube-system/roles/kubeadm:kubelet-config-1.18", 22 | "uid": "bb5dc308-25ee-4cc3-a7d0-77693133f6ef" 23 | }, 24 | "rules": [ 25 | { 26 | "apiGroups": [ 27 | "" 28 | ], 29 | "resourceNames": [ 30 | "kubelet-config-1.18" 31 | ], 32 | "resources": [ 33 | "configmaps" 34 | ], 35 | "verbs": [ 36 | "get" 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/role-raw.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | creationTimestamp: "2020-05-08T19:45:27Z" 5 | managedFields: 6 | - apiVersion: rbac.authorization.k8s.io/v1 7 | fieldsType: FieldsV1 8 | fieldsV1: 9 | f:rules: {} 10 | manager: kubeadm 11 | operation: Update 12 | time: "2020-05-08T19:45:27Z" 13 | name: kubeadm:kubelet-config-1.18 14 | namespace: kube-system 15 | resourceVersion: "162" 16 | selfLink: /apis/rbac.authorization.k8s.io/v1/namespaces/kube-system/roles/kubeadm:kubelet-config-1.18 17 | uid: bb5dc308-25ee-4cc3-a7d0-77693133f6ef 18 | rules: 19 | - apiGroups: 20 | - "" 21 | resourceNames: 22 | - kubelet-config-1.18 23 | resources: 24 | - configmaps 25 | verbs: 26 | - get 27 | -------------------------------------------------------------------------------- /test/fixtures/secret1-neat.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "data": { 4 | ".dockerconfigjson": "eyJhdXRocyI6eyJteXJlZ2lzdHJ5LnRlc3QiOnsidXNlcm5hbWUiOiJ1c2VyIiwicGFzc3dvcmQiOiJwYXNzIiwiZW1haWwiOiJ1c2VyQGVtYWlsLnRlc3QiLCJhdXRoIjoiZFhObGNqcHdZWE56In19fQ==" 5 | }, 6 | "kind": "Secret", 7 | "metadata": { 8 | "name": "myreg", 9 | "namespace": "default" 10 | }, 11 | "type": "kubernetes.io/dockerconfigjson" 12 | } -------------------------------------------------------------------------------- /test/fixtures/secret1-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "data": { 4 | ".dockerconfigjson": "eyJhdXRocyI6eyJteXJlZ2lzdHJ5LnRlc3QiOnsidXNlcm5hbWUiOiJ1c2VyIiwicGFzc3dvcmQiOiJwYXNzIiwiZW1haWwiOiJ1c2VyQGVtYWlsLnRlc3QiLCJhdXRoIjoiZFhObGNqcHdZWE56In19fQ==" 5 | }, 6 | "kind": "Secret", 7 | "metadata": { 8 | "creationTimestamp": "2020-04-03T05:45:54Z", 9 | "name": "myreg", 10 | "namespace": "default", 11 | "resourceVersion": "3376", 12 | "selfLink": "/api/v1/namespaces/default/secrets/myreg", 13 | "uid": "62a187fd-756e-11ea-b27b-0242ac11002d" 14 | }, 15 | "type": "kubernetes.io/dockerconfigjson" 16 | } -------------------------------------------------------------------------------- /test/fixtures/secret1-raw.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | .dockerconfigjson: eyJhdXRocyI6eyJteXJlZ2lzdHJ5LnRlc3QiOnsidXNlcm5hbWUiOiJ1c2VyIiwicGFzc3dvcmQiOiJwYXNzIiwiZW1haWwiOiJ1c2VyQGVtYWlsLnRlc3QiLCJhdXRoIjoiZFhObGNqcHdZWE56In19fQ== 4 | kind: Secret 5 | metadata: 6 | creationTimestamp: "2020-04-03T05:45:54Z" 7 | name: myreg 8 | namespace: default 9 | resourceVersion: "3376" 10 | selfLink: /api/v1/namespaces/default/secrets/myreg 11 | uid: 62a187fd-756e-11ea-b27b-0242ac11002d 12 | type: kubernetes.io/dockerconfigjson -------------------------------------------------------------------------------- /test/fixtures/service1-neat.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "name": "myappservice", 6 | "namespace": "default" 7 | }, 8 | "spec": { 9 | "clusterIP": "None", 10 | "ports": [ 11 | { 12 | "port": 2222 13 | } 14 | ], 15 | "selector": { 16 | "name": "myapp" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/service1-raw.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "creationTimestamp": "2019-04-24T20:12:14Z", 6 | "name": "myappservice", 7 | "namespace": "default", 8 | "resourceVersion": "187503", 9 | "selfLink": "/api/v1/namespaces/default/services/myappservice", 10 | "uid": "409de7fb-66cd-11e9-b6fa-0800271788ca" 11 | }, 12 | "spec": { 13 | "clusterIP": "None", 14 | "ports": [ 15 | { 16 | "port": 2222, 17 | "protocol": "TCP", 18 | "targetPort": 2222 19 | } 20 | ], 21 | "selector": { 22 | "name": "myapp" 23 | }, 24 | "sessionAffinity": "None", 25 | "type": "ClusterIP" 26 | }, 27 | "status": { 28 | "loadBalancer": {} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures/service1-raw.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: "2019-04-24T20:12:14Z" 5 | name: myappservice 6 | namespace: default 7 | resourceVersion: "187503" 8 | selfLink: /api/v1/namespaces/default/services/myappservice 9 | uid: 409de7fb-66cd-11e9-b6fa-0800271788ca 10 | spec: 11 | clusterIP: None 12 | ports: 13 | - port: 2222 14 | protocol: TCP 15 | targetPort: 2222 16 | selector: 17 | name: myapp 18 | sessionAffinity: None 19 | type: ClusterIP 20 | status: 21 | loadBalancer: {} 22 | -------------------------------------------------------------------------------- /test/kubectl-stub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # based on Argbash: https://argbash.io 4 | 5 | die() 6 | { 7 | local _ret=$2 8 | test -n "$_ret" || _ret=1 9 | test "$_PRINT_HELP" = yes && print_help >&2 10 | echo "$1" >&2 11 | exit ${_ret} 12 | } 13 | 14 | _positionals=() 15 | _arg_output= 16 | # _arg_namespace= 17 | 18 | parse_commandline() 19 | { 20 | _positionals_count=0 21 | while test $# -gt 0 22 | do 23 | _key="$1" 24 | case "$_key" in 25 | -o|--output) 26 | test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 27 | _arg_output="$2" 28 | shift 29 | ;; 30 | --output=*) 31 | _arg_output="${_key##--output=}" 32 | ;; 33 | -o*) 34 | _arg_output="${_key##-o}" 35 | ;; 36 | # -n|--namespace) 37 | # test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 38 | # _arg_namespace="$2" 39 | # shift 40 | # ;; 41 | # --namespace=*) 42 | # _arg_namespace="${_key##--namespace=}" 43 | # ;; 44 | # -n*) 45 | # _arg_namespace="${_key##-n}" 46 | # ;; 47 | *) 48 | _last_positional="$1" 49 | _positionals+=("$_last_positional") 50 | _positionals_count=$((_positionals_count + 1)) 51 | ;; 52 | esac 53 | shift 54 | done 55 | } 56 | 57 | parse_commandline "$@" 58 | rootDir="$(dirname "${BASH_SOURCE[0]}")/fixtures" 59 | 60 | [ "${_positionals[0]}" = "get" ] && [ "${_positionals[1]}" = "pods" ] && [ "${_positionals[2]}" = "mypod" ] && cat "$rootDir/pod1-raw.$_arg_output" && exit 0 61 | [ "${_positionals[0]}" = "get" ] && [ "${_positionals[1]}" = "pods" ] && cat "$rootDir/list1-raw.$_arg_output" && exit 0 62 | 63 | die "invalid args: positionals: ${_positionals[*]}. output: $_arg_output" --------------------------------------------------------------------------------