├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── .krew.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── kubectl-view-secret.go ├── go.mod ├── go.sum ├── hack └── kind-bootstrap.sh ├── media ├── view-secret.gif └── view-secret.tape └── pkg └── cmd ├── decode.go ├── decode_test.go ├── types.go ├── types_test.go ├── view-secret.go └── view-secret_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @elsesiy 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | on: 4 | - pull_request 5 | jobs: 6 | ci_job: 7 | name: test 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: Create kind cluster 13 | uses: helm/kind-action@v1 14 | with: 15 | cluster_name: kvs-test 16 | install_only: true 17 | - name: Prepare env 18 | run: make bootstrap 19 | - name: Test 20 | run: make test 21 | - name: Generate coverage report 22 | run: make test-cov 23 | - name: Upload results to Codecov 24 | uses: codecov/codecov-action@v4 25 | with: 26 | token: ${{ secrets.CODECOV_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | jobs: 8 | release_job: 9 | name: goreleaser & krew 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: ^1.24 20 | - name: GoReleaser 21 | uses: goreleaser/goreleaser-action@v6 22 | with: 23 | args: release --clean 24 | version: ~> v2 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | - name: Update new version in krew-index 28 | uses: rajatjindal/krew-release-bot@v0.0.47 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | .idea 5 | .vscode 6 | 7 | kubectl-view-secret 8 | coverage.txt 9 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | before: 3 | hooks: 4 | - go mod download 5 | builds: 6 | - main: ./cmd/kubectl-view-secret.go 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - darwin 11 | - linux 12 | - windows 13 | goarch: 14 | - amd64 15 | - arm64 16 | ignore: 17 | - goos: windows 18 | goarch: arm64 19 | archives: 20 | - builds: 21 | - kubectl-view-secret 22 | name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 23 | wrap_in_directory: false 24 | format: tar.gz 25 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: view-secret 5 | spec: 6 | version: {{ .TagName }} 7 | homepage: https://github.com/elsesiy/kubectl-view-secret 8 | shortDescription: Decode Kubernetes secrets 9 | description: |+2 10 | Base64 decode by key or all key/value pairs in a given secret. 11 | 12 | # print secret keys 13 | $ kubectl view-secret 14 | 15 | # decode specific entry 16 | $ kubectl view-secret 17 | 18 | # decode all secret contents 19 | $ kubectl view-secret -a/--all 20 | 21 | # print keys for secret in different namespace 22 | $ kubectl view-secret -n/--namespace foo 23 | 24 | # print keys for secret in different context 25 | $ kubectl view-secret -c/--context ctx 26 | 27 | # print keys for secret by providing kubeconfig 28 | $ kubectl view-secret -k/--kubeconfig 29 | 30 | # suppress info output 31 | $ kubectl view-secret -q/--quiet 32 | platforms: 33 | - selector: 34 | matchLabels: 35 | os: darwin 36 | arch: amd64 37 | {{addURIAndSha "https://github.com/elsesiy/kubectl-view-secret/releases/download/{{ .TagName }}/kubectl-view-secret_{{ .TagName }}_darwin_amd64.tar.gz" .TagName }} 38 | bin: kubectl-view-secret 39 | - selector: 40 | matchLabels: 41 | os: darwin 42 | arch: arm64 43 | {{addURIAndSha "https://github.com/elsesiy/kubectl-view-secret/releases/download/{{ .TagName }}/kubectl-view-secret_{{ .TagName }}_darwin_arm64.tar.gz" .TagName }} 44 | bin: kubectl-view-secret 45 | - selector: 46 | matchLabels: 47 | os: linux 48 | arch: amd64 49 | {{addURIAndSha "https://github.com/elsesiy/kubectl-view-secret/releases/download/{{ .TagName }}/kubectl-view-secret_{{ .TagName }}_linux_amd64.tar.gz" .TagName }} 50 | bin: kubectl-view-secret 51 | - selector: 52 | matchLabels: 53 | os: linux 54 | arch: arm64 55 | {{addURIAndSha "https://github.com/elsesiy/kubectl-view-secret/releases/download/{{ .TagName }}/kubectl-view-secret_{{ .TagName }}_linux_arm64.tar.gz" .TagName }} 56 | bin: kubectl-view-secret 57 | - selector: 58 | matchLabels: 59 | os: windows 60 | arch: amd64 61 | {{addURIAndSha "https://github.com/elsesiy/kubectl-view-secret/releases/download/{{ .TagName }}/kubectl-view-secret_{{ .TagName }}_windows_amd64.tar.gz" .TagName }} 62 | bin: kubectl-view-secret.exe 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.14.0](https://github.com/elsesiy/kubectl-view-secret/tree/0.14.0) (2025-03-31) 4 | 5 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.13.0...0.14.0) 6 | 7 | **Closed issues:** 8 | 9 | - Update to go1.24 [\#58](https://github.com/elsesiy/kubectl-view-secret/issues/58) 10 | - kubectl-view-secret returns null for kubernetes.io/basic-auth secrets [\#55](https://github.com/elsesiy/kubectl-view-secret/issues/55) 11 | - I just added the -git version in the AUR [\#51](https://github.com/elsesiy/kubectl-view-secret/issues/51) 12 | - Add support for helm secrets [\#50](https://github.com/elsesiy/kubectl-view-secret/issues/50) 13 | - New line getting trimmed when using `view-secret -a` [\#44](https://github.com/elsesiy/kubectl-view-secret/issues/44) 14 | 15 | **Merged pull requests:** 16 | 17 | - Bump go toolchain to 1.24 & update dependencies [\#59](https://github.com/elsesiy/kubectl-view-secret/pull/59) ([elsesiy](https://github.com/elsesiy)) 18 | - Add decode default case for unhandled types [\#57](https://github.com/elsesiy/kubectl-view-secret/pull/57) ([elsesiy](https://github.com/elsesiy)) 19 | - Add support for helm secrets [\#54](https://github.com/elsesiy/kubectl-view-secret/pull/54) ([elsesiy](https://github.com/elsesiy)) 20 | - chore: go1.23, bump module dependencies [\#53](https://github.com/elsesiy/kubectl-view-secret/pull/53) ([elsesiy](https://github.com/elsesiy)) 21 | - Don't trim Secrets [\#52](https://github.com/elsesiy/kubectl-view-secret/pull/52) ([rybnico](https://github.com/rybnico)) 22 | 23 | ## [v0.13.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.13.0) (2024-08-14) 24 | 25 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.12.1...v0.13.0) 26 | 27 | **Merged pull requests:** 28 | 29 | - Add instructions for nixpkgs to README [\#49](https://github.com/elsesiy/kubectl-view-secret/pull/49) ([elsesiy](https://github.com/elsesiy)) 30 | - Add test coverage via codecov.io [\#48](https://github.com/elsesiy/kubectl-view-secret/pull/48) ([elsesiy](https://github.com/elsesiy)) 31 | - Integrate with huh for better interface [\#47](https://github.com/elsesiy/kubectl-view-secret/pull/47) ([elsesiy](https://github.com/elsesiy)) 32 | 33 | ## [v0.12.1](https://github.com/elsesiy/kubectl-view-secret/tree/v0.12.1) (2024-08-02) 34 | 35 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.12.0...v0.12.1) 36 | 37 | **Closed issues:** 38 | 39 | - Add support for impersonating groups with flag [\#42](https://github.com/elsesiy/kubectl-view-secret/issues/42) 40 | 41 | **Merged pull requests:** 42 | 43 | - Bump dependencies & fix formatting [\#46](https://github.com/elsesiy/kubectl-view-secret/pull/46) ([elsesiy](https://github.com/elsesiy)) 44 | - Ease copy past of install command [\#45](https://github.com/elsesiy/kubectl-view-secret/pull/45) ([Ph0tonic](https://github.com/Ph0tonic)) 45 | - Issue-42 Adding support for impersonating groups with flag --as-group [\#43](https://github.com/elsesiy/kubectl-view-secret/pull/43) ([pratikkumar-mohite](https://github.com/pratikkumar-mohite)) 46 | 47 | ## [v0.12.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.12.0) (2024-02-25) 48 | 49 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.11.0...v0.12.0) 50 | 51 | **Closed issues:** 52 | 53 | - Update to go1.22 [\#40](https://github.com/elsesiy/kubectl-view-secret/issues/40) 54 | 55 | **Merged pull requests:** 56 | 57 | - Update go toolchain to 1.22, bump dependencies [\#41](https://github.com/elsesiy/kubectl-view-secret/pull/41) ([elsesiy](https://github.com/elsesiy)) 58 | - Add support for impersonating a user or service account with flag [\#39](https://github.com/elsesiy/kubectl-view-secret/pull/39) ([cnmcavoy](https://github.com/cnmcavoy)) 59 | 60 | ## [v0.11.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.11.0) (2023-06-26) 61 | 62 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.10.1...v0.11.0) 63 | 64 | **Closed issues:** 65 | 66 | - Update to go1.20 [\#37](https://github.com/elsesiy/kubectl-view-secret/issues/37) 67 | - Bump dependencies [\#33](https://github.com/elsesiy/kubectl-view-secret/issues/33) 68 | - use escape characters while printing secret values [\#32](https://github.com/elsesiy/kubectl-view-secret/issues/32) 69 | 70 | **Merged pull requests:** 71 | 72 | - Update go toolchain to 1.20 [\#38](https://github.com/elsesiy/kubectl-view-secret/pull/38) ([elsesiy](https://github.com/elsesiy)) 73 | - Add quotes to secret values [\#36](https://github.com/elsesiy/kubectl-view-secret/pull/36) ([pradeepnnv](https://github.com/pradeepnnv)) 74 | 75 | ## [v0.10.1](https://github.com/elsesiy/kubectl-view-secret/tree/v0.10.1) (2022-12-18) 76 | 77 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.10.0...v0.10.1) 78 | 79 | **Closed issues:** 80 | 81 | - Improve CI setup [\#31](https://github.com/elsesiy/kubectl-view-secret/issues/31) 82 | - Update to go1.19 [\#30](https://github.com/elsesiy/kubectl-view-secret/issues/30) 83 | 84 | **Merged pull requests:** 85 | 86 | - Bump dependencies & update fmt \(gofumpt, yaml\) [\#34](https://github.com/elsesiy/kubectl-view-secret/pull/34) ([elsesiy](https://github.com/elsesiy)) 87 | 88 | ## [v0.10.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.10.0) (2022-08-07) 89 | 90 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.9.1...v0.10.0) 91 | 92 | **Closed issues:** 93 | 94 | - AUR package [\#28](https://github.com/elsesiy/kubectl-view-secret/issues/28) 95 | 96 | **Merged pull requests:** 97 | 98 | - Bump to go1.19, run CI in container [\#29](https://github.com/elsesiy/kubectl-view-secret/pull/29) ([elsesiy](https://github.com/elsesiy)) 99 | 100 | ## [v0.9.1](https://github.com/elsesiy/kubectl-view-secret/tree/v0.9.1) (2022-04-28) 101 | 102 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.9.0...v0.9.1) 103 | 104 | **Closed issues:** 105 | 106 | - Replace encoding/json [\#27](https://github.com/elsesiy/kubectl-view-secret/issues/27) 107 | - Update to go1.18 [\#26](https://github.com/elsesiy/kubectl-view-secret/issues/26) 108 | 109 | ## [v0.9.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.9.0) (2021-11-27) 110 | 111 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.8.1...v0.9.0) 112 | 113 | **Closed issues:** 114 | 115 | - Update to go1.17 [\#24](https://github.com/elsesiy/kubectl-view-secret/issues/24) 116 | - support sort values and do not print empty lines [\#20](https://github.com/elsesiy/kubectl-view-secret/issues/20) 117 | 118 | **Merged pull requests:** 119 | 120 | - Improve multi-secret outputs [\#23](https://github.com/elsesiy/kubectl-view-secret/pull/23) ([elsesiy](https://github.com/elsesiy)) 121 | 122 | ## [v0.8.1](https://github.com/elsesiy/kubectl-view-secret/tree/v0.8.1) (2021-05-23) 123 | 124 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.8.0...v0.8.1) 125 | 126 | **Closed issues:** 127 | 128 | - Error while installing in Apple M1 chip [\#22](https://github.com/elsesiy/kubectl-view-secret/issues/22) 129 | - Missing darwin\_arm64 binary [\#21](https://github.com/elsesiy/kubectl-view-secret/issues/21) 130 | - Update to go1.16 [\#18](https://github.com/elsesiy/kubectl-view-secret/issues/18) 131 | 132 | ## [v0.8.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.8.0) (2021-04-09) 133 | 134 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.7.0...v0.8.0) 135 | 136 | **Merged pull requests:** 137 | 138 | - update to go1.16 [\#19](https://github.com/elsesiy/kubectl-view-secret/pull/19) ([elsesiy](https://github.com/elsesiy)) 139 | - fix typo. [\#17](https://github.com/elsesiy/kubectl-view-secret/pull/17) ([jiroshin](https://github.com/jiroshin)) 140 | 141 | ## [v0.7.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.7.0) (2020-11-19) 142 | 143 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.6.0...v0.7.0) 144 | 145 | **Closed issues:** 146 | 147 | - Update to go1.15 [\#15](https://github.com/elsesiy/kubectl-view-secret/issues/15) 148 | - Migrate to GitHub Actions [\#13](https://github.com/elsesiy/kubectl-view-secret/issues/13) 149 | 150 | **Merged pull requests:** 151 | 152 | - update to go1.15 [\#16](https://github.com/elsesiy/kubectl-view-secret/pull/16) ([elsesiy](https://github.com/elsesiy)) 153 | - add GitHub Action for ci/release [\#14](https://github.com/elsesiy/kubectl-view-secret/pull/14) ([elsesiy](https://github.com/elsesiy)) 154 | 155 | ## [v0.6.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.6.0) (2020-07-26) 156 | 157 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.5.0...v0.6.0) 158 | 159 | **Closed issues:** 160 | 161 | - Add kubeconfig option [\#11](https://github.com/elsesiy/kubectl-view-secret/issues/11) 162 | 163 | **Merged pull requests:** 164 | 165 | - Add support for custom kubeconfig [\#12](https://github.com/elsesiy/kubectl-view-secret/pull/12) ([elsesiy](https://github.com/elsesiy)) 166 | 167 | ## [v0.5.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.5.0) (2020-03-21) 168 | 169 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.4.0...v0.5.0) 170 | 171 | **Implemented enhancements:** 172 | 173 | - Handle empty secrets [\#8](https://github.com/elsesiy/kubectl-view-secret/issues/8) 174 | 175 | **Fixed bugs:** 176 | 177 | - Change base64 encoding used during decode [\#9](https://github.com/elsesiy/kubectl-view-secret/issues/9) 178 | 179 | ## [v0.4.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.4.0) (2020-02-05) 180 | 181 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.3.0...v0.4.0) 182 | 183 | **Implemented enhancements:** 184 | 185 | - Support custom context [\#5](https://github.com/elsesiy/kubectl-view-secret/issues/5) 186 | 187 | **Closed issues:** 188 | 189 | - Problem with docs [\#2](https://github.com/elsesiy/kubectl-view-secret/issues/2) 190 | 191 | **Merged pull requests:** 192 | 193 | - Add support for custom context [\#6](https://github.com/elsesiy/kubectl-view-secret/pull/6) ([elsesiy](https://github.com/elsesiy)) 194 | 195 | ## [v0.3.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.3.0) (2019-10-26) 196 | 197 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.2.0...v0.3.0) 198 | 199 | **Closed issues:** 200 | 201 | - Printing decoded secrets should not add a newline [\#1](https://github.com/elsesiy/kubectl-view-secret/issues/1) 202 | 203 | ## [v0.2.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.2.0) (2019-10-24) 204 | 205 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/v0.1.0...v0.2.0) 206 | 207 | ## [v0.1.0](https://github.com/elsesiy/kubectl-view-secret/tree/v0.1.0) (2019-10-21) 208 | 209 | [Full Changelog](https://github.com/elsesiy/kubectl-view-secret/compare/cf6a6b61cf63a4f907f72b5fd74fcc3ceb36c2c0...v0.1.0) 210 | 211 | 212 | 213 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jonas-Taha El Sesiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES := $(shell find . -name '*.go') 2 | BINARY := kubectl-view-secret 3 | COV_REPORT := "coverage.txt" 4 | 5 | build: kubectl-view-secret 6 | 7 | bootstrap: 8 | ./hack/kind-bootstrap.sh 9 | 10 | test: $(SOURCES) 11 | go test -v -short -race -timeout 30s ./... 12 | 13 | test-cov: 14 | go test ./... -coverprofile=$(COV_REPORT) 15 | go tool cover -html=$(COV_REPORT) 16 | 17 | clean: 18 | @rm -rf $(BINARY) 19 | @kind delete cluster --name kvs-test 20 | 21 | mod-update: 22 | @go get -u -t ./... && go mod tidy 23 | 24 | $(BINARY): $(SOURCES) 25 | CGO_ENABLED=0 go build -o $(BINARY) -ldflags="-s -w" ./cmd/$(BINARY).go 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubectl-view-secret 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/elsesiy/kubectl-view-secret)](https://goreportcard.com/report/github.com/elsesiy/kubectl-view-secret) 4 | ![CI](https://github.com/elsesiy/kubectl-view-secret/actions/workflows/ci.yml/badge.svg) 5 | [![codecov](https://codecov.io/github/elsesiy/kubectl-view-secret/graph/badge.svg?token=RODJX5GLDB)](https://codecov.io/github/elsesiy/kubectl-view-secret) 6 | [![Twitter](https://img.shields.io/badge/twitter-@elsesiy-blue.svg)](http://twitter.com/elsesiy) 7 | [![GitHub release](https://img.shields.io/github/release/elsesiy/kubectl-view-secret.svg)](https://github.com/elsesiy/kubectl-view-secret/releases) 8 | 9 | ![gif](./media/view-secret.gif) 10 | 11 | This plugin allows for easy secret decoding. Useful if you want to see what's inside of a secret without always go through the following: 12 | 13 | 1. `kubectl get secret -o yaml` 14 | 2. Copy base64 encoded secret 15 | 3. `echo "b64string" | base64 -d` 16 | 17 | Instead you can now do: 18 | 19 | # print secret keys 20 | kubectl view-secret 21 | 22 | # decode specific entry 23 | kubectl view-secret 24 | 25 | # decode all contents 26 | kubectl view-secret -a/--all 27 | 28 | # print keys for secret in different namespace 29 | kubectl view-secret -n/--namespace 30 | 31 | # print keys for secret in different context 32 | kubectl view-secret -c/--context 33 | 34 | # print keys for secret by providing kubeconfig 35 | kubectl view-secret -k/--kubeconfig 36 | 37 | # suppress info output 38 | kubectl view-secret -q/--quiet 39 | 40 | ## Usage 41 | 42 | ### Krew 43 | 44 | This plugin is available through [krew](https://krew.dev) via: 45 | 46 | ```sh 47 | kubectl krew install view-secret 48 | ``` 49 | 50 | ### Binary releases 51 | 52 | #### GitHub 53 | You can find the latest binaries in the [releases](https://github.com/elsesiy/kubectl-view-secret/releases) section. 54 | To install it, place it somewhere in your `$PATH` for `kubectl` to pick it up. 55 | 56 | **Note**: If you build from source or download the binary, you'll have to change the name of the binary to `kubectl-view_secret` (`-` to `_` in `view-secret`) 57 | due to the enforced naming convention for plugins by `kubectl`. More on this [here](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/#naming-a-plugin). 58 | 59 | #### AUR package 60 | Thanks to external contributions the plugin is available in the Arch user repository. 61 | | Package | Contributor | 62 | | -- | -- | 63 | | [bin](https://aur.archlinux.org/packages/kubectl-view-secret-bin) | [@jocelynthode](https://github.com/jocelynthode) | 64 | | [git](https://aur.archlinux.org/packages/kubectl-view-secret-git) | [@aryklein](https://github.com/aryklein) | 65 | 66 | #### Nix 67 | You can install the latest version from Nixpkgs ([24.11](https://search.nixos.org/packages?channel=24.11&show=kubectl-view-secret&from=0&size=50&sort=relevance&type=packages&query=kubectl-view-secret), [unstable](https://search.nixos.org/packages?channel=unstable&show=kubectl-view-secret&from=0&size=50&sort=relevance&type=packages&query=kubectl-view-secret)) or try it via a temporary nix-shell: 68 | 69 | ``` 70 | nix-shell -p kubectl-view-secret 71 | ``` 72 | 73 | ### Build from source 74 | 75 | # Clone this repository (or your fork) 76 | git clone https://github.com/elsesiy/kubectl-view-secret 77 | cd kubectl-view-secret 78 | make 79 | 80 | ## License 81 | 82 | This repository is available under the [MIT license](https://choosealicense.com/licenses/mit/). 83 | -------------------------------------------------------------------------------- /cmd/kubectl-view-secret.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/elsesiy/kubectl-view-secret/pkg/cmd" 7 | ) 8 | 9 | func main() { 10 | command := cmd.NewCmdViewSecret() 11 | if err := command.Execute(); err != nil { 12 | os.Exit(1) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elsesiy/kubectl-view-secret 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/charmbracelet/bubbletea v1.3.4 7 | github.com/charmbracelet/huh v0.6.0 8 | github.com/goccy/go-json v0.10.5 9 | github.com/spf13/cobra v1.9.1 10 | github.com/stretchr/testify v1.10.0 11 | ) 12 | 13 | require ( 14 | github.com/atotto/clipboard v0.1.4 // indirect 15 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 16 | github.com/catppuccin/go v0.3.0 // indirect 17 | github.com/charmbracelet/bubbles v0.20.0 // indirect 18 | github.com/charmbracelet/colorprofile v0.3.0 // indirect 19 | github.com/charmbracelet/lipgloss v1.1.0 // indirect 20 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 21 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 22 | github.com/charmbracelet/x/exp/strings v0.0.0-20250327172914-2fdc97757edf // indirect 23 | github.com/charmbracelet/x/term v0.2.1 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/dustin/go-humanize v1.0.1 // indirect 26 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 27 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 28 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/mattn/go-localereader v0.0.1 // indirect 31 | github.com/mattn/go-runewidth v0.0.16 // indirect 32 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 33 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 34 | github.com/muesli/cancelreader v0.2.2 // indirect 35 | github.com/muesli/termenv v0.16.0 // indirect 36 | github.com/pmezard/go-difflib v1.0.0 // indirect 37 | github.com/rivo/uniseg v0.4.7 // indirect 38 | github.com/spf13/pflag v1.0.6 // indirect 39 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 40 | golang.org/x/sync v0.12.0 // indirect 41 | golang.org/x/sys v0.31.0 // indirect 42 | golang.org/x/text v0.23.0 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 2 | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 3 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 4 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 5 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 | github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= 8 | github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= 9 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= 10 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= 11 | github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= 12 | github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= 13 | github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= 14 | github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0= 15 | github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= 16 | github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= 17 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 18 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 19 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 20 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 21 | github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 22 | github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 23 | github.com/charmbracelet/x/exp/strings v0.0.0-20250327172914-2fdc97757edf h1:Myu2mjqzk3Kum6T7zFMd8QKfqH6uNCkn5WgbxG0DABk= 24 | github.com/charmbracelet/x/exp/strings v0.0.0-20250327172914-2fdc97757edf/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= 25 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 26 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 27 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 28 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 29 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 31 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 32 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 33 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 34 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 35 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 36 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 37 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 38 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 39 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 40 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 41 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 42 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 43 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 44 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 45 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 46 | github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= 47 | github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 48 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 49 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 50 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 51 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 52 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 53 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 57 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 58 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 59 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 60 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 61 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 62 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 63 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 67 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 68 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 69 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 70 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 71 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 72 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 75 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 76 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 77 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | -------------------------------------------------------------------------------- /hack/kind-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | CLUSTER_NAME="kvs-test" 6 | 7 | # Check if kind & kubectl are installed 8 | which kind &>/dev/null || { echo "failed to find 'kind' binary, please install it" && exit 1; } 9 | which kubectl &>/dev/null || { echo "failed to find 'kubectl' binary, please install it" && exit 1; } 10 | 11 | # Ensure the cluster exists 12 | [[ $(kind get clusters) == *$CLUSTER_NAME* ]] || kind create cluster --name $CLUSTER_NAME 13 | 14 | # Set context in case there are mulitple 15 | kubectl config set-context kind-${CLUSTER_NAME} 16 | 17 | # Seed test secrets 18 | 19 | ## secret 'test' in namespace 'default' (multiple keys) 20 | kubectl apply -f - < 22 | 23 | # decode specific entry 24 | %[1]s view-secret 25 | 26 | # decode all contents 27 | %[1]s view-secret -a/--all 28 | 29 | # print keys for secret in different namespace 30 | %[1]s view-secret -n/--namespace 31 | 32 | # print keys for secret in different context 33 | %[1]s view-secret -c/--context 34 | 35 | # print keys for secret by providing kubeconfig 36 | %[1]s view-secret -k/--kubeconfig 37 | 38 | # suppress info output 39 | %[1]s view-secret -q/--quiet 40 | ` 41 | 42 | secretDescription = "Found %d keys in secret %q. Choose one or select 'all' to view." 43 | secretListDescription = "Found %d secrets. Choose one." 44 | secretListTitle = "Available Secrets" 45 | secretTitle = "Secret Data" 46 | singleKeyDescription = "Viewing only available key: %[1]s" 47 | ) 48 | 49 | var ( 50 | // ErrNoSecretFound is thrown when no secret name was provided but we didn't find any secrets 51 | ErrNoSecretFound = errors.New("no secrets found") 52 | 53 | // ErrSecretEmpty is thrown when there's no data in the secret 54 | ErrSecretEmpty = errors.New("secret is empty") 55 | 56 | // ErrSecretKeyNotFound is thrown if the key doesn't exist in the secret 57 | ErrSecretKeyNotFound = errors.New("provided key not found in secret") 58 | ) 59 | 60 | // CommandOpts is the struct holding common properties 61 | type CommandOpts struct { 62 | customContext string 63 | customNamespace string 64 | decodeAll bool 65 | impersonateAs string 66 | impersonateAsGroups string 67 | kubeConfig string 68 | quiet bool 69 | secretKey string 70 | secretName string 71 | } 72 | 73 | // NewCmdViewSecret creates the cobra command to be executed 74 | func NewCmdViewSecret() *cobra.Command { 75 | res := &CommandOpts{} 76 | 77 | cmd := &cobra.Command{ 78 | Args: cobra.RangeArgs(0, 2), 79 | Example: fmt.Sprintf(example, "kubectl"), 80 | Short: "Decode a kubernetes secret by name & key in the current context/cluster/namespace", 81 | SilenceUsage: true, 82 | Use: "view-secret [secret-name] [secret-key]", 83 | RunE: func(c *cobra.Command, args []string) error { 84 | res.ParseArgs(args) 85 | if err := res.Retrieve(c); err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | }, 91 | } 92 | 93 | cmd.Flags(). 94 | BoolVarP(&res.decodeAll, "all", "a", res.decodeAll, "if true, decodes all secrets without specifying the individual secret keys") 95 | cmd.Flags().BoolVarP(&res.quiet, "quiet", "q", res.quiet, "if true, suppresses info output") 96 | cmd.Flags(). 97 | StringVarP(&res.customNamespace, "namespace", "n", res.customNamespace, "override the namespace defined in the current context") 98 | cmd.Flags().StringVarP(&res.customContext, "context", "c", res.customContext, "override the current context") 99 | cmd.Flags().StringVarP(&res.kubeConfig, "kubeconfig", "k", res.kubeConfig, "explicitly provide the kubeconfig to use") 100 | cmd.Flags().StringVar(&res.impersonateAs, "as", res.impersonateAs, "Username to impersonate for the operation. User could be a regular user or a service account in a namespace.") 101 | cmd.Flags().StringVar(&res.impersonateAsGroups, "as-group", res.impersonateAsGroups, "Groups to impersonate for the operation. Multipe groups can be specified by comma separated.") 102 | 103 | return cmd 104 | } 105 | 106 | // ParseArgs serializes the user supplied program arguments 107 | func (c *CommandOpts) ParseArgs(args []string) { 108 | argLen := len(args) 109 | if argLen >= 1 { 110 | c.secretName = args[0] 111 | 112 | if argLen == 2 { 113 | c.secretKey = args[1] 114 | } 115 | } 116 | } 117 | 118 | // Retrieve reads the kubeconfig and decodes the secret 119 | func (c *CommandOpts) Retrieve(cmd *cobra.Command) error { 120 | nsOverride, _ := cmd.Flags().GetString("namespace") 121 | ctxOverride, _ := cmd.Flags().GetString("context") 122 | kubeConfigOverride, _ := cmd.Flags().GetString("kubeconfig") 123 | impersonateOverride, _ := cmd.Flags().GetString("as") 124 | impersonateGroupOverride, _ := cmd.Flags().GetString("as-group") 125 | 126 | var res, cmdErr bytes.Buffer 127 | 128 | commandArgs := []string{"get", "secret", "-o", "json"} 129 | if c.secretName != "" { 130 | commandArgs = []string{"get", "secret", c.secretName, "-o", "json"} 131 | } 132 | 133 | if nsOverride != "" { 134 | commandArgs = append(commandArgs, "-n", nsOverride) 135 | } 136 | 137 | if ctxOverride != "" { 138 | commandArgs = append(commandArgs, "--context", ctxOverride) 139 | } 140 | 141 | if kubeConfigOverride != "" { 142 | commandArgs = append(commandArgs, "--kubeconfig", kubeConfigOverride) 143 | } 144 | 145 | if impersonateOverride != "" { 146 | commandArgs = append(commandArgs, "--as", impersonateOverride) 147 | } 148 | 149 | if impersonateGroupOverride != "" { 150 | commandArgs = append(commandArgs, "--as-group", impersonateGroupOverride) 151 | } 152 | 153 | out := exec.Command("kubectl", commandArgs...) 154 | out.Stdout = &res 155 | out.Stderr = &cmdErr 156 | err := out.Run() 157 | if err != nil { 158 | _, _ = fmt.Fprint(os.Stderr, cmdErr.String()) 159 | return nil 160 | } 161 | 162 | var secret Secret 163 | if c.secretName == "" { 164 | var secretList SecretList 165 | if err := json.Unmarshal(res.Bytes(), &secretList); err != nil { 166 | return err 167 | } 168 | 169 | // Since we don't query valid namespaces, we'll avoid prompting the user to select a secret if we didn't retrieve any secrets 170 | if len(secretList.Items) == 0 { 171 | return ErrNoSecretFound 172 | } 173 | 174 | opts := []string{} 175 | secretMap := map[string]Secret{} 176 | for _, v := range secretList.Items { 177 | opts = append(opts, v.Metadata.Name) 178 | secretMap[v.Metadata.Name] = v 179 | } 180 | 181 | err := huh.NewForm( 182 | huh.NewGroup( 183 | huh.NewSelect[string](). 184 | Title(secretListTitle). 185 | Description(fmt.Sprintf(secretListDescription, len(secretList.Items))). 186 | Options(huh.NewOptions(opts...)...). 187 | Value(&c.secretName), 188 | ), 189 | ).WithProgramOptions(tea.WithInput(cmd.InOrStdin()), tea.WithOutput(cmd.OutOrStdout())).Run() 190 | if err != nil { 191 | return err 192 | } 193 | 194 | secret = secretMap[c.secretName] 195 | } else { 196 | if err := json.Unmarshal(res.Bytes(), &secret); err != nil { 197 | return err 198 | } 199 | } 200 | 201 | if c.quiet { 202 | return ProcessSecret(cmd.OutOrStdout(), io.Discard, cmd.InOrStdin(), secret, c.secretKey, c.decodeAll) 203 | } 204 | 205 | return ProcessSecret(cmd.OutOrStdout(), cmd.OutOrStderr(), cmd.InOrStdin(), secret, c.secretKey, c.decodeAll) 206 | } 207 | 208 | // ProcessSecret takes the secret and user input to determine the output 209 | func ProcessSecret(outWriter, errWriter io.Writer, inputReader io.Reader, secret Secret, secretKey string, decodeAll bool) error { 210 | data := secret.Data 211 | if len(data) == 0 { 212 | return ErrSecretEmpty 213 | } 214 | 215 | var keys []string 216 | for k := range secret.Data { 217 | keys = append(keys, k) 218 | } 219 | sort.Strings(keys) 220 | 221 | if decodeAll { 222 | for _, k := range keys { 223 | s, _ := secret.Decode(data[k]) 224 | _, _ = fmt.Fprintf(outWriter, "%s='%s'\n", k, s) 225 | } 226 | } else if len(data) == 1 { 227 | for k, v := range data { 228 | _, _ = fmt.Fprintf(errWriter, singleKeyDescription+"\n", k) 229 | s, _ := secret.Decode(v) 230 | _, _ = fmt.Fprintf(outWriter, "%s\n", s) 231 | } 232 | } else if secretKey != "" { 233 | if v, ok := data[secretKey]; ok { 234 | s, _ := secret.Decode(v) 235 | _, _ = fmt.Fprintf(outWriter, "%s\n", s) 236 | } else { 237 | return ErrSecretKeyNotFound 238 | } 239 | } else { 240 | opts := []string{"all"} 241 | for k := range data { 242 | opts = append(opts, k) 243 | } 244 | 245 | var selection string 246 | err := huh.NewForm( 247 | huh.NewGroup( 248 | huh.NewSelect[string](). 249 | Title(secretTitle). 250 | Description(fmt.Sprintf(secretDescription, len(data), secret.Metadata.Name)). 251 | Options(huh.NewOptions(opts...)...). 252 | Value(&selection), 253 | ), 254 | ).WithProgramOptions(tea.WithInput(inputReader), tea.WithOutput(outWriter)).Run() 255 | if err != nil { 256 | return err 257 | } 258 | 259 | if selection == "all" { 260 | decodeAll = true 261 | } 262 | 263 | return ProcessSecret(outWriter, errWriter, inputReader, secret, selection, decodeAll) 264 | } 265 | 266 | return nil 267 | } 268 | -------------------------------------------------------------------------------- /pkg/cmd/view-secret_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var ( 15 | secret = SecretData{ 16 | "TEST_CONN_STR": "bW9uZ29kYjovL215REJSZWFkZXI6RDFmZmljdWx0UCU0MHNzdzByZEBtb25nb2RiMC5leGFtcGxlLmNvbToyNzAxNy8/YXV0aFNvdXJjZT1hZG1pbg==", 17 | "TEST_PASSWORD": "c2VjcmV0Cg==", 18 | "TEST_PASSWORD_2": "dmVyeXNlY3JldAo=", 19 | } 20 | 21 | secretSingle = SecretData{ 22 | "SINGLE_PASSWORD": "c2VjcmV0Cg==", 23 | } 24 | 25 | // echo "helm-test" | gzip -c | base64 | base64 26 | secretHelm = SecretData{ 27 | "release": "SDRzSUFGb2FlR2NBQTh0SXpjblZMVWt0THVFQ0FQdWt3aHdLQUFBQQo=", 28 | } 29 | 30 | secretEmpty = SecretData{} 31 | ) 32 | 33 | func TestParseArgs(t *testing.T) { 34 | opts := CommandOpts{} 35 | tests := map[string]struct { 36 | opts CommandOpts 37 | args []string 38 | wantOpts CommandOpts 39 | }{ 40 | "one arg": {opts, []string{"test"}, CommandOpts{secretName: "test"}}, 41 | "two args": {opts, []string{"test", "key"}, CommandOpts{secretName: "test", secretKey: "key"}}, 42 | } 43 | 44 | for name, test := range tests { 45 | t.Run(name, func(t *testing.T) { 46 | t.Parallel() 47 | 48 | test.opts.ParseArgs(test.args) 49 | got := test.opts 50 | if got != test.wantOpts { 51 | t.Errorf("got %v, want %v", got, test.wantOpts) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestNewCmdViewSecret(t *testing.T) { 58 | tests := map[string]struct { 59 | args []string 60 | feedkeys string 61 | want string 62 | wantErr error 63 | }{ 64 | "all": {args: []string{"test", "--all"}, want: `key1='value1'\nkey2='value2'`}, 65 | "custom ctx": {args: []string{"test", "--context", "gotest"}}, 66 | "custom kubecfg": {args: []string{"test", "--kubeconfig", "cfg"}}, 67 | "custom ns (does not exist)": {args: []string{"test", "--namespace", "bob"}, want: `Error from server (NotFound): namespaces "bob" not found`}, 68 | "custom ns (no secret)": {args: []string{"test", "--namespace", "another"}, want: `Error from server (NotFound): secrets "test" not found`}, 69 | "custom ns (valid secret)": {args: []string{"gopher", "--namespace", "another"}, want: `Viewing only available key: foo\nbar`}, 70 | "helm": {args: []string{"test3", "--namespace", "helm"}, want: `Viewing only available key: release\nhelm-test`}, 71 | "impersonate group": {args: []string{"test", "--as", "gopher"}}, 72 | "impersonate user & group": {args: []string{"test", "--as", "gopher", "--as-group", "golovers"}}, 73 | // make bootstrap sources 2 test secrets in the default namespace, select the first one and print all values 74 | "interactive": {args: []string{"--all"}, feedkeys: "\r", want: `key1='value1'\nkey2='value2'`}, 75 | "interactive custom ns (no secret)": {args: []string{"--namespace", "empty"}, wantErr: ErrNoSecretFound}, 76 | "invalid arg count": {args: []string{"a", "b", "c"}, wantErr: errors.New("accepts between 0 and 2 arg(s), received 3")}, 77 | "quiet": {args: []string{"test2", "--quiet"}, want: `value1`}, 78 | "unknown flag": {args: []string{"--version"}, wantErr: errors.New("unknown flag: --version")}, 79 | } 80 | 81 | for name, tt := range tests { 82 | t.Run(name, func(t *testing.T) { 83 | t.Parallel() 84 | 85 | cmd := NewCmdViewSecret() 86 | outBuf := bytes.NewBufferString("") 87 | readBuf := &strings.Reader{} 88 | if tt.feedkeys != "" { 89 | readBuf = strings.NewReader(tt.feedkeys) 90 | } 91 | 92 | cmd.SetOut(outBuf) 93 | cmd.SetIn(readBuf) 94 | cmd.SetArgs(tt.args) 95 | 96 | err := cmd.Execute() 97 | if err != nil { 98 | if tt.wantErr == nil { 99 | assert.Fail(t, "unexpected error", err) 100 | } else if err.Error() != tt.wantErr.Error() { 101 | assert.Equal(t, tt.wantErr, err) 102 | } 103 | return 104 | } else if tt.wantErr != nil { 105 | assert.Fail(t, "expected error, got nil", tt.wantErr) 106 | return 107 | } 108 | 109 | _, err = io.ReadAll(outBuf) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func TestProcessSecret(t *testing.T) { 118 | tests := map[string]struct { 119 | secretData SecretData 120 | secretType SecretType 121 | wantStdOut []string 122 | wantStdErr []string 123 | secretKey string 124 | decodeAll bool 125 | err error 126 | feedkeys string 127 | }{ 128 | "view-secret ": { 129 | secret, 130 | Opaque, 131 | []string{ 132 | "TEST_CONN_STR='mongodb://myDBReader:D1fficultP%40ssw0rd@mongodb0.example.com:27017/?authSource=admin'", 133 | "TEST_PASSWORD='secret\n'", 134 | "TEST_PASSWORD_2='verysecret\n'", 135 | }, 136 | []string{}, 137 | "", 138 | false, 139 | nil, 140 | "\r", // selects 'all' as it's the default selection 141 | }, 142 | "view-secret ": { 143 | secretSingle, 144 | Opaque, 145 | []string{"secret"}, 146 | []string{fmt.Sprintf(singleKeyDescription, "SINGLE_PASSWORD")}, 147 | "", 148 | false, 149 | nil, 150 | "", 151 | }, 152 | "view-secret ": { 153 | secretHelm, 154 | Helm, 155 | []string{"helm-test"}, 156 | []string{fmt.Sprintf(singleKeyDescription, "release")}, 157 | "", 158 | false, 159 | nil, 160 | "", 161 | }, 162 | "view-secret test TEST_PASSWORD": { 163 | secret, 164 | Opaque, 165 | []string{"secret"}, 166 | nil, 167 | "TEST_PASSWORD", 168 | false, 169 | nil, 170 | "", 171 | }, 172 | "view-secret test -a": { 173 | secret, 174 | Opaque, 175 | []string{ 176 | "TEST_CONN_STR='mongodb://myDBReader:D1fficultP%40ssw0rd@mongodb0.example.com:27017/?authSource=admin'", 177 | "TEST_PASSWORD='secret\n'", 178 | "TEST_PASSWORD_2='verysecret\n'", 179 | }, 180 | nil, 181 | "", 182 | true, 183 | nil, 184 | "", 185 | }, 186 | "view-secret test NONE": { 187 | secret, 188 | Opaque, 189 | nil, 190 | nil, 191 | "NONE", 192 | false, 193 | ErrSecretKeyNotFound, 194 | "", 195 | }, 196 | "view-secret ": { 197 | secretEmpty, 198 | Opaque, 199 | nil, 200 | nil, 201 | "", 202 | false, 203 | ErrSecretEmpty, 204 | "", 205 | }, 206 | } 207 | 208 | for name, test := range tests { 209 | t.Run(name, func(t *testing.T) { 210 | t.Parallel() 211 | 212 | stdOutBuf := bytes.Buffer{} 213 | stdErrBuf := bytes.Buffer{} 214 | readBuf := strings.Reader{} 215 | 216 | if test.feedkeys != "" { 217 | readBuf = *strings.NewReader(test.feedkeys) 218 | } 219 | 220 | err := ProcessSecret(&stdOutBuf, &stdErrBuf, &readBuf, Secret{Data: test.secretData, Type: test.secretType}, test.secretKey, test.decodeAll) 221 | 222 | if test.err != nil { 223 | assert.Equal(t, err, test.err) 224 | } else { 225 | gotStdOut := stdOutBuf.String() 226 | gotStdErr := stdErrBuf.String() 227 | 228 | for _, s := range test.wantStdOut { 229 | if !assert.Contains(t, gotStdOut, s) { 230 | t.Errorf("got %v, want %v", gotStdOut, s) 231 | } 232 | } 233 | 234 | for _, s := range test.wantStdErr { 235 | if !assert.Contains(t, gotStdErr, s) { 236 | t.Errorf("got %v, want %v", gotStdErr, s) 237 | } 238 | } 239 | } 240 | }) 241 | } 242 | } 243 | --------------------------------------------------------------------------------