├── .github └── workflows │ ├── ci.yaml │ ├── config │ └── changelog-ci-config.json │ ├── gitee-mirror.yaml │ └── release.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .theia └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cmd └── lazykube │ └── lazykube.go ├── docs ├── README_CN.md ├── iterm2-enable-mouse-reporting.png └── lazykube.gif ├── go.mod ├── go.sum ├── pkg ├── app │ ├── action.go │ ├── app.go │ ├── dialog.go │ ├── error.go │ ├── format.go │ ├── handler.go │ ├── keymap.go │ ├── kubecli.go │ ├── panel.go │ ├── render.go │ ├── render_plot.go │ ├── statekey.go │ ├── stream.go │ └── style.go ├── config │ ├── config.go │ ├── gui.go │ ├── log.go │ └── user.go ├── gui │ ├── action.go │ ├── dimension.go │ ├── edit.go │ ├── error.go │ ├── gui.go │ ├── plot.go │ ├── sorter.go │ ├── state.go │ └── view.go ├── kubecli │ ├── apiresources.go │ ├── clusterinfo │ │ ├── .gitkeep │ │ └── clusterinfo.go │ ├── config │ │ ├── .gitkeep │ │ └── current_context.go │ ├── describe.go │ ├── edit.go │ ├── exec.go │ ├── get.go │ ├── get_namespaces.go │ ├── get_pod_metrics.go │ ├── get_resource_gvk.go │ ├── kubecli.go │ ├── logs.go │ ├── rollout_restart.go │ ├── run.go │ ├── top_node.go │ └── top_pod.go ├── log │ └── logger.go └── utils │ ├── click_option.go │ ├── file.go │ ├── labels.go │ ├── math.go │ └── string.go └── scripts └── install_update_linux.sh /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | 12 | build: 13 | name: CI 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/setup-go@v1 17 | with: 18 | go-version: 1.14 19 | - uses: actions/checkout@v2.3.4 20 | - run: go mod download 21 | - name: Build 22 | run: cd ./cmd/lazykube && go build -v . 23 | - name: Lint 24 | uses: Jerome1337/golint-action@v1.0.2 25 | with: 26 | golint-path: ./ 27 | - name: Go report card 28 | # You may pin to the exact commit or the version. 29 | # uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 30 | uses: creekorful/goreportcard-action@v1.0 -------------------------------------------------------------------------------- /.github/workflows/config/changelog-ci-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "header_prefix": "Version:", 3 | "commit_changelog": false, 4 | "comment_changelog": true, 5 | "pull_request_title_regex": "^(?i:release)", 6 | "version_regex": "v?([0-9]{1,2})+[.]+([0-9]{1,2})+[.]+([0-9]{1,2})\\s\\(\\d{1,2}-\\d{1,2}-\\d{4}\\)", 7 | "group_config": [ 8 | { 9 | "title": "Bug Fixes", 10 | "labels": ["bug", "bugfix"] 11 | }, 12 | { 13 | "title": "Code Improvements", 14 | "labels": ["improvements", "enhancement"] 15 | }, 16 | { 17 | "title": "New Features", 18 | "labels": ["feature", "feature request"] 19 | }, 20 | { 21 | "title": "Documentation Updates", 22 | "labels": ["docs", "documentation", "doc"] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/gitee-mirror.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Gitee Mirror 3 | jobs: 4 | run: 5 | name: Run 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout source codes 9 | uses: actions/checkout@v2.3.4 10 | - name: Mirror Github to Gitee 11 | uses: Yikun/hub-mirror-action@v0.11 12 | with: 13 | src: github/TNK-Studio 14 | dst: gitee/TNK-Studio 15 | dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} 16 | dst_token: ${{ secrets.GITEE_TOKEN }} 17 | account_type: org 18 | white_list: 'lazykube' 19 | force_update: true 20 | debug: true 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build release 2 | on: 3 | release: 4 | types: 5 | - published 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/setup-go@v1 11 | with: 12 | go-version: 1.14 13 | 14 | - uses: actions/checkout@v2.3.4 15 | 16 | - run: go mod download 17 | 18 | - name: Get tag 19 | id: tag 20 | uses: dawidd6/action-get-tag@v1 21 | 22 | - name: Current tag 23 | run: echo ${{steps.tag.outputs.tag}} 24 | 25 | - uses: izumin5210/action-go-crossbuild@v1.0.0 26 | with: 27 | goxz-version: v0.6.0 28 | name: lazykube 29 | arch: amd64,386 30 | os: darwin,linux,windows 31 | package: ./cmd/lazykube 32 | ldflags: "-X github.com/TNK-Studio/lazykube/pkg/app.Version=${{steps.tag.outputs.tag}}" 33 | 34 | - uses: izumin5210/action-go-crossbuild@v1.0.0 35 | with: 36 | goxz-version: v0.6.0 37 | name: lazykube 38 | arch: arm 39 | os: linux 40 | package: ./cmd/lazykube 41 | ldflags: "-X github.com/TNK-Studio/lazykube/pkg/app.Version=${{steps.tag.outputs.tag}}" 42 | 43 | - uses: softprops/action-gh-release@v1 44 | with: 45 | files: './dist/*' 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | if: startsWith(github.ref, 'refs/tags/') 49 | 50 | - uses: izumin5210/action-homebrew-tap@v1.0.0 51 | with: 52 | tap: TNK-Studio/homebrew-tools 53 | token: ${{ secrets.GITHUB_TOKEN }} 54 | tap-token: ${{ secrets.TAP_GITHUB_TOKEN }} 55 | if: startsWith(github.ref, 'refs/tags/') 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | .idea/ 3 | .vscode/ 4 | *.log 5 | cmd/lazykube/lazykube 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | # Created by https://www.toptal.com/developers/gitignore/api/OSX 24 | # Edit at https://www.toptal.com/developers/gitignore?templates=OSX 25 | 26 | ### OSX ### 27 | # General 28 | .DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | 32 | # Icon must end with two \r 33 | Icon 34 | 35 | 36 | # Thumbnails 37 | ._* 38 | 39 | # Files that might appear in the root of a volume 40 | .DocumentRevisions-V100 41 | .fseventsd 42 | .Spotlight-V100 43 | .TemporaryItems 44 | .Trashes 45 | .VolumeIcon.icns 46 | .com.apple.timemachine.donotpresent 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | 55 | # End of https://www.toptal.com/developers/gitignore/api/OSX 56 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | RUN brew install zsh 4 | RUN sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key C99B11DEB97541F0 5 | RUN sudo apt-add-repository https://cli.github.com/packages 6 | RUN sudo apt update 7 | RUN sudo apt install gh 8 | RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 9 | RUN npm install -g auto-changelog 10 | RUN echo "zsh" >> ~/.bashrc 11 | 12 | # Install custom tools, runtimes, etc. 13 | # For example "bastet", a command-line tetris clone: 14 | # RUN brew install bastet 15 | # 16 | # More information: https://www.gitpod.io/docs/config-docker/ 17 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | 4 | tasks: 5 | - init: go mod download 6 | command: go run cmd/lazykube/lazykube.go 7 | -------------------------------------------------------------------------------- /.theia/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "lazykube", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "program": "${workspaceFolder}/cmd/lazykube/lazykube.go", 10 | "env": {}, 11 | "args": [] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [v0.10.3](https://github.com/TNK-Studio/lazykube/compare/v0.10.2...v0.10.3) 8 | 9 | > 30 September 2021 10 | 11 | - fix: fix getResourceNamespaceAndName func [`f26cec5`](https://github.com/TNK-Studio/lazykube/commit/f26cec57ad21414ad4617ff01702df0477b907ba) 12 | 13 | #### [v0.10.2](https://github.com/TNK-Studio/lazykube/compare/v0.10.1...v0.10.2) 14 | 15 | > 30 September 2021 16 | 17 | - fix: fix when namespace resource no permission [`#62`](https://github.com/TNK-Studio/lazykube/pull/62) 18 | - doc: update CHANELOG [`c3a3259`](https://github.com/TNK-Studio/lazykube/commit/c3a3259542fa886f01adb14c011d4b5511e4b3bc) 19 | 20 | #### [v0.10.1](https://github.com/TNK-Studio/lazykube/compare/v0.10.0...v0.10.1) 21 | 22 | > 30 September 2021 23 | 24 | - Fix select invalid resource line [`#61`](https://github.com/TNK-Studio/lazykube/pull/61) 25 | - feat: 在启动时读取 kubectl context 中默认的 namespace [`#59`](https://github.com/TNK-Studio/lazykube/pull/59) 26 | - doc: update changelog [`96a1dd3`](https://github.com/TNK-Studio/lazykube/commit/96a1dd3c6e57fef1200a0d283bd3ba091c66095e) 27 | 28 | #### [v0.10.0](https://github.com/TNK-Studio/lazykube/compare/v0.9.0...v0.10.0) 29 | 30 | > 31 March 2021 31 | 32 | - feat: 在启动时读取 kubectl context 中默认的 namespace [`0288d87`](https://github.com/TNK-Studio/lazykube/commit/0288d878645d6223b7fa5d4017718fd4d9d8ba20) 33 | - docs: update CHANGELOG.md [`5d2cc83`](https://github.com/TNK-Studio/lazykube/commit/5d2cc83a963cef0bbaff62c2b556896032276099) 34 | 35 | #### [v0.9.0](https://github.com/TNK-Studio/lazykube/compare/v0.8.6...v0.9.0) 36 | 37 | > 5 January 2021 38 | 39 | - feat: add tail logs [`#57`](https://github.com/TNK-Studio/lazykube/pull/57) 40 | - docs: update change log [`43f6f24`](https://github.com/TNK-Studio/lazykube/commit/43f6f2427a0924b20ab5e9a4e1bf668fb3303b4b) 41 | - Update install_update_linux.sh [`0b4048c`](https://github.com/TNK-Studio/lazykube/commit/0b4048c4d96030eba3a4141615e9c71f45232b33) 42 | 43 | #### [v0.8.6](https://github.com/TNK-Studio/lazykube/compare/v0.8.5...v0.8.6) 44 | 45 | > 6 December 2020 46 | 47 | - fix: fix version check [`8ee8ad3`](https://github.com/TNK-Studio/lazykube/commit/8ee8ad39d9e68af542adb66c5744d529d8be7836) 48 | 49 | #### [v0.8.5](https://github.com/TNK-Studio/lazykube/compare/v0.8.4...v0.8.5) 50 | 51 | > 5 December 2020 52 | 53 | - chore: modify install_update_linux.sh [`#54`](https://github.com/TNK-Studio/lazykube/pull/54) 54 | - fix: fix dialog filter input [`256f386`](https://github.com/TNK-Studio/lazykube/commit/256f386a23ef32f0ed2489505f0967a9b857223e) 55 | - docs: update change log [`f1c01ca`](https://github.com/TNK-Studio/lazykube/commit/f1c01cade3f4c24a8b061dc2306843d60aba84d3) 56 | - docs: update change log [`2d65093`](https://github.com/TNK-Studio/lazykube/commit/2d6509307fb91ab1f67e92ca7194767cefde5578) 57 | 58 | #### [v0.8.4](https://github.com/TNK-Studio/lazykube/compare/v0.8.3...v0.8.4) 59 | 60 | > 27 November 2020 61 | 62 | - fix: Delete custom resource panel. [`#52`](https://github.com/TNK-Studio/lazykube/pull/52) 63 | - fix: fix top pod plot [`#51`](https://github.com/TNK-Studio/lazykube/pull/51) 64 | - docs: update change log [`ca892d1`](https://github.com/TNK-Studio/lazykube/commit/ca892d13daeeb67a19e3fc943e0b2714cbdf4d29) 65 | 66 | #### [v0.8.3](https://github.com/TNK-Studio/lazykube/compare/v0.8.2...v0.8.3) 67 | 68 | > 27 November 2020 69 | 70 | - docs: update change log [`d8b4fb2`](https://github.com/TNK-Studio/lazykube/commit/d8b4fb247228d69b5e2b334f1750f1f4f33c25ac) 71 | - fix: fix focus function [`563952e`](https://github.com/TNK-Studio/lazykube/commit/563952e5b63647fa0f2500b652ec6c96f9ab8ac4) 72 | 73 | #### [v0.8.2](https://github.com/TNK-Studio/lazykube/compare/v0.8.1...v0.8.2) 74 | 75 | > 26 November 2020 76 | 77 | - fix: fix rerender [`#49`](https://github.com/TNK-Studio/lazykube/pull/49) 78 | - docs: update change log [`8842dbc`](https://github.com/TNK-Studio/lazykube/commit/8842dbc142f71f39d0ca75ca049ec48184272773) 79 | 80 | #### [v0.8.1](https://github.com/TNK-Studio/lazykube/compare/v0.8.0...v0.8.1) 81 | 82 | > 26 November 2020 83 | 84 | - docs: update change log [`63cc3a5`](https://github.com/TNK-Studio/lazykube/commit/63cc3a5da368d5add00ad26348b2ec88d0ec54dd) 85 | - chore: fix .github/workflows/release.yml [`6279f1d`](https://github.com/TNK-Studio/lazykube/commit/6279f1d11bc6ea630b20b5de514cab6833ad400a) 86 | 87 | #### [v0.8.0](https://github.com/TNK-Studio/lazykube/compare/v0.7.1...v0.8.0) 88 | 89 | > 26 November 2020 90 | 91 | - feat: add release version checkout [`#48`](https://github.com/TNK-Studio/lazykube/pull/48) 92 | - feat: change context [`#47`](https://github.com/TNK-Studio/lazykube/pull/47) 93 | - Issue 42 [`#46`](https://github.com/TNK-Studio/lazykube/pull/46) 94 | - style: delete unused code [`a1958f9`](https://github.com/TNK-Studio/lazykube/commit/a1958f937e8e15d30983db65ffe8fd0c2a61ec92) 95 | - docs: update change log [`1820cbb`](https://github.com/TNK-Studio/lazykube/commit/1820cbbd3165081848e4144fee044cf302e149a2) 96 | 97 | #### [v0.7.1](https://github.com/TNK-Studio/lazykube/compare/v0.7.0...v0.7.1) 98 | 99 | > 25 November 2020 100 | 101 | - fix: issue #43 [`#44`](https://github.com/TNK-Studio/lazykube/pull/44) 102 | - docs: update change log [`ad07210`](https://github.com/TNK-Studio/lazykube/commit/ad07210e78cc081e437903de82164d3102ed3777) 103 | 104 | #### [v0.7.0](https://github.com/TNK-Studio/lazykube/compare/v0.6.0...v0.7.0) 105 | 106 | > 24 November 2020 107 | 108 | - feat: copy selected line [`#41`](https://github.com/TNK-Studio/lazykube/pull/41) 109 | - feat: select pod logs container [`#40`](https://github.com/TNK-Studio/lazykube/pull/40) 110 | - [WIP] feat: Command support - kubectl exec [`#38`](https://github.com/TNK-Studio/lazykube/pull/38) 111 | - Fully automate dev setup with Gitpod [`#34`](https://github.com/TNK-Studio/lazykube/pull/34) 112 | - docs: update change log [`14437cc`](https://github.com/TNK-Studio/lazykube/commit/14437cc3b1bdb91a34ebc5ec2f14789d036c690c) 113 | - docs: update change log [`6c1ac1f`](https://github.com/TNK-Studio/lazykube/commit/6c1ac1f31185fe56025efc0427a179ec09f1847f) 114 | - Update .gitpod.Dockerfile [`7ffbd8b`](https://github.com/TNK-Studio/lazykube/commit/7ffbd8b5b26aaf63234fe416b61d0280bcd11fbb) 115 | 116 | #### [v0.6.0](https://github.com/TNK-Studio/lazykube/compare/v0.5.0...v0.6.0) 117 | 118 | > 23 November 2020 119 | 120 | - refactor: refactor logs. [`#33`](https://github.com/TNK-Studio/lazykube/pull/33) 121 | - docs: update change log [`4fa1d85`](https://github.com/TNK-Studio/lazykube/commit/4fa1d8580e06e97f29356fdf1bd4cc1034d71cf7) 122 | 123 | #### [v0.5.0](https://github.com/TNK-Studio/lazykube/compare/v0.4.2...v0.5.0) 124 | 125 | > 22 November 2020 126 | 127 | - feat: Add custom resource panel. [`#32`](https://github.com/TNK-Studio/lazykube/pull/32) 128 | 129 | #### [v0.4.2](https://github.com/TNK-Studio/lazykube/compare/v0.4.1...v0.4.2) 130 | 131 | > 21 November 2020 132 | 133 | - fix: add gcp、azure and openstack ... client auth provider [`#31`](https://github.com/TNK-Studio/lazykube/pull/31) 134 | - docs: update change log [`b099a93`](https://github.com/TNK-Studio/lazykube/commit/b099a93ee6f22013ae3a051f8f9f40cdb95620ea) 135 | - docs: update change log [`00f668c`](https://github.com/TNK-Studio/lazykube/commit/00f668c6fa7b56b98d6e648c739852f030a13041) 136 | 137 | #### [v0.4.1](https://github.com/TNK-Studio/lazykube/compare/v0.4.0...v0.4.1) 138 | 139 | > 21 November 2020 140 | 141 | - refactor: optimize rollout restart deployment [`#29`](https://github.com/TNK-Studio/lazykube/pull/29) 142 | - docs: update change log [`16c300d`](https://github.com/TNK-Studio/lazykube/commit/16c300d74b018940e229317eaa26c603ff5b6e1c) 143 | 144 | #### [v0.4.0](https://github.com/TNK-Studio/lazykube/compare/v0.3.1...v0.4.0) 145 | 146 | > 21 November 2020 147 | 148 | - feat: rollout restart deployment [`#28`](https://github.com/TNK-Studio/lazykube/pull/28) 149 | - refactor: add keymap.go [`#27`](https://github.com/TNK-Studio/lazykube/pull/27) 150 | - docs: update change log [`bf6e4fe`](https://github.com/TNK-Studio/lazykube/commit/bf6e4fe257f6819daa141bc4ee3d676138d56206) 151 | 152 | #### [v0.3.1](https://github.com/TNK-Studio/lazykube/compare/v0.3.0...v0.3.1) 153 | 154 | > 20 November 2020 155 | 156 | - fix: fix some bug [`#24`](https://github.com/TNK-Studio/lazykube/pull/24) 157 | - docs: update change log [`738ee9a`](https://github.com/TNK-Studio/lazykube/commit/738ee9a78c870ade421ed46ef9ae207db11f1b17) 158 | 159 | #### [v0.3.0](https://github.com/TNK-Studio/lazykube/compare/v0.2.3...v0.3.0) 160 | 161 | > 19 November 2020 162 | 163 | - Feat edit resource [`#22`](https://github.com/TNK-Studio/lazykube/pull/22) 164 | - docs: update change log [`6963134`](https://github.com/TNK-Studio/lazykube/commit/6963134bdc7261a809e0cd80941e3bd6f6855ad7) 165 | - fix: fix edit resource [`6f29f7c`](https://github.com/TNK-Studio/lazykube/commit/6f29f7c1e62aa433af46ebe429ccb19ad3696f2e) 166 | 167 | #### [v0.2.3](https://github.com/TNK-Studio/lazykube/compare/v0.2.2...v0.2.3) 168 | 169 | > 18 November 2020 170 | 171 | - fix: cluster info navitation [`#21`](https://github.com/TNK-Studio/lazykube/pull/21) 172 | - docs: add README_CN.md [`ee6b20a`](https://github.com/TNK-Studio/lazykube/commit/ee6b20a63de1f8b590abe58aed4087eb6651c674) 173 | - docs: update change log [`361e334`](https://github.com/TNK-Studio/lazykube/commit/361e3348503ac04ef50d073ee7bdd8b407f954bd) 174 | - docs: update change log [`2b08e45`](https://github.com/TNK-Studio/lazykube/commit/2b08e45d0ec39650f68d5ef0fb05bb79d045872b) 175 | 176 | #### [v0.2.2](https://github.com/TNK-Studio/lazykube/compare/v0.2.1...v0.2.2) 177 | 178 | > 18 November 2020 179 | 180 | - fix: fix views style [`ca2bdf4`](https://github.com/TNK-Studio/lazykube/commit/ca2bdf4f44af0778060ac972822f16c7e3f0b0c8) 181 | - docs: update change log [`9b90f8c`](https://github.com/TNK-Studio/lazykube/commit/9b90f8c32f316a7d6d4072254e65fadb3364d12a) 182 | 183 | #### [v0.2.1](https://github.com/TNK-Studio/lazykube/compare/v0.2.0...v0.2.1) 184 | 185 | > 18 November 2020 186 | 187 | - chore: modify actions [`#18`](https://github.com/TNK-Studio/lazykube/pull/18) 188 | - fix: golang.org/x/sys [`ef952f7`](https://github.com/TNK-Studio/lazykube/commit/ef952f73975a459ea2e3285583936156fd7c819e) 189 | 190 | #### [v0.2.0](https://github.com/TNK-Studio/lazykube/compare/v0.1.2...v0.2.0) 191 | 192 | > 18 November 2020 193 | 194 | - feat: add resource filter dialog. [`#17`](https://github.com/TNK-Studio/lazykube/pull/17) 195 | - feat: add resource filter dialog [`7a90453`](https://github.com/TNK-Studio/lazykube/commit/7a90453b8ad08f8e64107d5f1b8c26aecd81d72a) 196 | - feat: filter dialog [`71f52e2`](https://github.com/TNK-Studio/lazykube/commit/71f52e28b0ac1ec5c698eeb0f5b59d809af952bc) 197 | - feat: add dialog [`591b52b`](https://github.com/TNK-Studio/lazykube/commit/591b52bee1fe7554178630f12b66d2eb9778a168) 198 | 199 | #### [v0.1.2](https://github.com/TNK-Studio/lazykube/compare/v0.1.1...v0.1.2) 200 | 201 | > 17 November 2020 202 | 203 | - fix: #15 [`#16`](https://github.com/TNK-Studio/lazykube/pull/16) 204 | - chore: modify actions [`#14`](https://github.com/TNK-Studio/lazykube/pull/14) 205 | - chore: add changelog ci [`#11`](https://github.com/TNK-Studio/lazykube/pull/11) 206 | - docs: update readme. add faq [`#10`](https://github.com/TNK-Studio/lazykube/pull/10) 207 | - fix: Fixed a BUG where the program quits when the K8s API Server returns an error [`4dd8f9a`](https://github.com/TNK-Studio/lazykube/commit/4dd8f9a6222d1a25bb82443e26cad23bf226a956) 208 | - chore: add gitee mirror action [`25fd0f0`](https://github.com/TNK-Studio/lazykube/commit/25fd0f038ce07c41f9d6d4b796a4c8f71f50bacb) 209 | 210 | #### [v0.1.1](https://github.com/TNK-Studio/lazykube/compare/v0.1.0...v0.1.1) 211 | 212 | > 16 November 2020 213 | 214 | - Dev [`#9`](https://github.com/TNK-Studio/lazykube/pull/9) 215 | - docs: modify readme [`c6091be`](https://github.com/TNK-Studio/lazykube/commit/c6091be52028d6860054cc3956e6f19bf4d43abb) 216 | - fix: previous line bug [`42e6c50`](https://github.com/TNK-Studio/lazykube/commit/42e6c506756326d8271bcb21b7fe7164cc8b409b) 217 | - fix: Optimize log rendering [`ec1ba69`](https://github.com/TNK-Studio/lazykube/commit/ec1ba69e773b00da2a5128d9f9111ba411e2f63e) 218 | 219 | #### v0.1.0 220 | 221 | > 15 November 2020 222 | 223 | - feat: modify render plot [`#7`](https://github.com/TNK-Studio/lazykube/pull/7) 224 | - Dev [`#6`](https://github.com/TNK-Studio/lazykube/pull/6) 225 | - Dev [`#5`](https://github.com/TNK-Studio/lazykube/pull/5) 226 | - feat: add kubecli and navigation actions. [`#4`](https://github.com/TNK-Studio/lazykube/pull/4) 227 | - feat: add Navigation View [`#3`](https://github.com/TNK-Studio/lazykube/pull/3) 228 | - feat: add ViewClickHandler [`#2`](https://github.com/TNK-Studio/lazykube/pull/2) 229 | - Dev [`#1`](https://github.com/TNK-Studio/lazykube/pull/1) 230 | - init [`e60a15a`](https://github.com/TNK-Studio/lazykube/commit/e60a15aaed1232b328bab2896657cd7abe92b17f) 231 | - feat: add kubecli.KubeCLI [`453259d`](https://github.com/TNK-Studio/lazykube/commit/453259dd37c54ed11562cadea63d92fd3c595f7b) 232 | - feat: add describe [`af81a90`](https://github.com/TNK-Studio/lazykube/commit/af81a90954456fdfdccb81ec0f788b4f6bac738f) 233 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lazykube 2 | ![lazykube](https://socialify.git.ci/TNK-Studio/lazykube/image?description=1&font=KoHo&forks=1&issues=1&language=1&logo=https%3A%2F%2Ftva1.sinaimg.cn%2Flarge%2F0081Kckwgy1gkzekazh5gj3069069744.jpg&owner=1&pattern=Signal&pulls=1&stargazers=1&theme=Dark) 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/TNK-Studio/lazykube)](https://goreportcard.com/report/github.com/TNK-Studio/lazykube) ![GitHub repo size](https://img.shields.io/github/repo-size/TNK-Studio/lazykube) ![GitHub all releases](https://img.shields.io/github/downloads/TNK-Studio/lazykube/total) ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/TNK-Studio/lazykube) ([English Document](README.md) | [中文文档](./docs/README_CN.md)) 5 | 6 | ![gif](./docs/lazykube.gif) 7 | 8 | You can see more examples here. [(More Demo)]([https://lazykube.tnk-studio.org/](https://tnk-studio.github.io/lazykube.tnk-studio.org/)) 9 | 10 | ## Installation 11 | 12 | ### Mac Homebrew 13 | 14 | #### Install 15 | 16 | ```bash 17 | $ brew install tnk-studio/tools/lazykube 18 | ``` 19 | #### Upgrade 20 | 21 | ```bash 22 | $ brew upgrade tnk-studio/tools/lazykube 23 | ``` 24 | 25 | ### Linux 26 | 27 | ```bash 28 | $ curl https://raw.githubusercontent.com/TNK-Studio/lazykube/main/scripts/install_update_linux.sh | bash 29 | ``` 30 | 31 | ### Windows 32 | 33 | Release page download [(link)](https://github.com/TNK-Studio/lazykube/releases/latest). 34 | 35 | ### Go get 36 | 37 | ```bash 38 | $ go get -u github.com/TNK-Studio/lazykube 39 | ``` 40 | 41 | ## How to use 42 | 43 | ```bash 44 | $ lazykube 45 | ``` 46 | 47 | ## FAQ 48 | 49 | * When using iterm2 as a terminal, mouse clicks cannot be used ? 50 | 51 | ![iterm2-enable-mouse-reporting](./docs/iterm2-enable-mouse-reporting.png) 52 | 53 | ## Change log 54 | 55 | [CHANGELOG](CHANGELOG.md) 56 | -------------------------------------------------------------------------------- /cmd/lazykube/lazykube.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/TNK-Studio/lazykube/pkg/app" 5 | ) 6 | 7 | func main() { 8 | lazykube := app.NewApp() 9 | defer lazykube.Stop() 10 | lazykube.Run() 11 | } 12 | -------------------------------------------------------------------------------- /docs/README_CN.md: -------------------------------------------------------------------------------- 1 | # lazykube 2 | ⎈ 通过鼠标和命令行交互的方式来管理 K8s 集群 ([English Document](../README.md) | [中文文档](README_CN.md)) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/TNK-Studio/lazykube)](https://goreportcard.com/report/github.com/TNK-Studio/lazykube) ![GitHub repo size](https://img.shields.io/github/repo-size/TNK-Studio/lazykube) ![GitHub all releases](https://img.shields.io/github/downloads/TNK-Studio/lazykube/total) ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/TNK-Studio/lazykube) 4 | 5 | ![gif](./lazykube.gif) 6 | 7 | 你可以在这里看到更多的示例。 [(更多示例)](https://tnk-studio.github.io/lazykube.tnk-studio.org/) 8 | 9 | ## 安装方式 10 | 11 | ### Mac Homebrew 12 | 13 | #### 安装 14 | 15 | ```bash 16 | $ brew install tnk-studio/tools/lazykube 17 | ``` 18 | #### 升级 19 | 20 | ```bash 21 | $ brew upgrade tnk-studio/tools/lazykube 22 | ``` 23 | 24 | ### Linux 25 | 26 | ```bash 27 | $ curl https://raw.githubusercontent.com/TNK-Studio/lazykube/main/scripts/install_update_linux.sh | bash 28 | ``` 29 | 30 | ### Windows 31 | 32 | 通过下载页面下载并解压 [(link)](https://github.com/TNK-Studio/lazykube/releases/latest). 33 | 34 | ### Go get 35 | 36 | ```bash 37 | $ go get -u github.com/TNK-Studio/lazykube 38 | ``` 39 | 40 | ## 如何使用 41 | 42 | ```bash 43 | $ lazykube 44 | ``` 45 | 46 | ## 常见问题 47 | 48 | * 当使用 iterm2 作为终端时鼠标无法交互 ? 49 | 50 | ![iterm2-enable-mouse-reporting](./iterm2-enable-mouse-reporting.png) 51 | 52 | ## 更新日志 53 | 54 | [更新日志](../CHANGELOG.md) 55 | -------------------------------------------------------------------------------- /docs/iterm2-enable-mouse-reporting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNK-Studio/lazykube/d6410b444faae32aeda1dfa3dcad61ed22a04799/docs/iterm2-enable-mouse-reporting.png -------------------------------------------------------------------------------- /docs/lazykube.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNK-Studio/lazykube/d6410b444faae32aeda1dfa3dcad61ed22a04799/docs/lazykube.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TNK-Studio/lazykube 2 | 3 | go 1.16 4 | 5 | replace ( 6 | github.com/Matt-Gleich/release => github.com/TNK-Studio/release v0.0.0-20201205162738-c1bc22c24d07 7 | github.com/jroimartin/gocui v0.4.0 => github.com/TNK-Studio/gocui v0.4.1-0.20201118030412-21fac610f2e0 8 | golang.org/x/sys => golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 9 | ) 10 | 11 | require ( 12 | github.com/Matt-Gleich/release v0.0.0-20210604035540-68b9816a6144 13 | github.com/atotto/clipboard v0.1.2 14 | github.com/docker/distribution v2.7.1+incompatible 15 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect 16 | github.com/fatih/camelcase v1.0.0 17 | github.com/go-logr/logr v0.2.0 18 | github.com/gookit/color v1.3.2 19 | github.com/imdario/mergo v0.3.8 // indirect 20 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect 21 | github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee 22 | github.com/jroimartin/gocui v0.4.0 23 | github.com/kr/pretty v0.2.1 // indirect 24 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible 25 | github.com/lestrrat-go/strftime v1.0.3 // indirect 26 | github.com/mattn/go-runewidth v0.0.9 // indirect 27 | github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 28 | github.com/pkg/errors v0.9.1 29 | github.com/sirupsen/logrus v1.7.0 30 | github.com/spf13/cobra v1.0.0 31 | github.com/spkg/bom v1.0.0 32 | github.com/tebeka/strftime v0.1.5 // indirect 33 | golang.org/x/net v0.0.0-20200927032502-5d4f70055728 // indirect 34 | golang.org/x/sys v0.0.0-20200926100807-9d91bd62050c // indirect 35 | google.golang.org/protobuf v1.25.0 // indirect 36 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 37 | k8s.io/api v0.19.3 38 | k8s.io/apimachinery v0.19.3 39 | k8s.io/cli-runtime v0.19.3 40 | k8s.io/client-go v0.19.3 41 | k8s.io/klog/v2 v2.2.0 42 | k8s.io/kubectl v0.19.3 43 | k8s.io/metrics v0.19.3 44 | k8s.io/utils v0.0.0-20200729134348-d5654de09c73 45 | ) 46 | -------------------------------------------------------------------------------- /pkg/app/action.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | guilib "github.com/TNK-Studio/lazykube/pkg/gui" 6 | "github.com/TNK-Studio/lazykube/pkg/kubecli" 7 | "github.com/TNK-Studio/lazykube/pkg/log" 8 | "github.com/jroimartin/gocui" 9 | ) 10 | 11 | var ( 12 | nextFunctionView = &guilib.Action{ 13 | Name: nextFunctionViewAction, 14 | Keys: keyMap[nextFunctionViewAction], 15 | Handler: nextFunctionViewHandler, 16 | Mod: gocui.ModNone, 17 | } 18 | 19 | backToPreviousView = &guilib.Action{ 20 | Name: backToPreviousViewAction, 21 | Keys: keyMap[backToPreviousViewAction], 22 | Handler: backToPreviousViewHandler, 23 | Mod: gocui.ModNone, 24 | } 25 | 26 | toNavigation = &guilib.Action{ 27 | Name: toNavigationAction, 28 | Keys: keyMap[toNavigationAction], 29 | Handler: toNavigationHandler, 30 | Mod: gocui.ModNone, 31 | } 32 | 33 | previousLine = &guilib.Action{ 34 | Name: previousLineAction, 35 | Keys: keyMap[previousLineAction], 36 | Handler: previousLineHandler, 37 | Mod: gocui.ModNone, 38 | } 39 | 40 | nextLine = &guilib.Action{ 41 | Name: nextLineAction, 42 | Keys: keyMap[nextLineAction], 43 | Handler: nextLineHandler, 44 | Mod: gocui.ModNone, 45 | } 46 | 47 | copySelectedLine = &guilib.Action{ 48 | Keys: keyMap[copySelectedLineAction], 49 | Name: copySelectedLineAction, 50 | Handler: copySelectedLineHandler, 51 | Mod: gocui.ModNone, 52 | } 53 | 54 | copySelectedLineMoreAction = &moreAction{ 55 | NeedSelectResource: false, 56 | Action: *copySelectedLine, 57 | } 58 | 59 | filterResource = &guilib.Action{ 60 | Name: filterResourceActionName, 61 | Keys: keyMap[filterResourceActionName], 62 | Handler: func(gui *guilib.Gui, v *guilib.View) error { 63 | resourceName := getViewResourceName(v.Name) 64 | if resourceName == "" { 65 | return nil 66 | } 67 | resourceViewName := resourceViewName(resourceName) 68 | if err := showFilterDialog( 69 | gui, 70 | fmt.Sprintf("Input to filter %s", resourceName), 71 | func(filtered string) error { 72 | if filtered == "" || filtered == filteredNoResource { 73 | return nil 74 | } 75 | 76 | resourceView, err := gui.GetView(resourceViewName) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | y := resourceView.WhichLine(filtered) 82 | if y < 0 { 83 | if err := resourceView.ResetCursorOrigin(); err != nil { 84 | return err 85 | } 86 | } else { 87 | if err := resourceView.SetOrigin(0, y); err != nil { 88 | return err 89 | } 90 | if err := resourceView.SetCursor(0, 0); err != nil { 91 | return err 92 | } 93 | } 94 | if err := closeFilterDialog(gui); err != nil { 95 | return err 96 | } 97 | if err := gui.ReturnPreviousView(); err != nil { 98 | return err 99 | } 100 | return nil 101 | }, 102 | func(string) ([]string, error) { 103 | var data []string 104 | resourceView, err := gui.GetView(resourceViewName) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | data = resourceView.ViewBufferLines() 110 | if len(data) >= 1 { 111 | return data[1:], nil 112 | } 113 | return data, nil 114 | }, 115 | filteredNoResource, 116 | false, 117 | ); err != nil { 118 | return err 119 | } 120 | return nil 121 | }, 122 | Mod: gocui.ModNone, 123 | } 124 | 125 | editResourceAction = &guilib.Action{ 126 | Name: editResourceActionName, 127 | Keys: keyMap[editResourceActionName], 128 | Handler: editResourceHandler, 129 | Mod: gocui.ModNone, 130 | } 131 | 132 | rolloutRestartAction = &guilib.Action{ 133 | Keys: keyMap[rolloutRestartActionName], 134 | Name: rolloutRestartActionName, 135 | Handler: rolloutRestartHandler, 136 | Mod: gocui.ModNone, 137 | } 138 | 139 | editResourceMoreAction = &moreAction{ 140 | NeedSelectResource: true, 141 | Action: *editResourceAction, 142 | } 143 | 144 | addCustomResourcePanelAction = &guilib.Action{ 145 | Keys: keyMap[addCustomResourcePanelActionName], 146 | Name: addCustomResourcePanelActionName, 147 | Handler: addCustomResourcePanelHandler, 148 | Mod: gocui.ModNone, 149 | } 150 | 151 | deleteCustomResourcePanelAction = &guilib.Action{ 152 | Keys: keyMap[deleteCustomResourcePanelActionName], 153 | Name: deleteCustomResourcePanelActionName, 154 | Handler: deleteCustomResourcePanelHandler, 155 | Mod: gocui.ModNone, 156 | } 157 | 158 | containerExecCommandAction = &guilib.Action{ 159 | Keys: keyMap[containerExecCommandActionName], 160 | Name: containerExecCommandActionName, 161 | Handler: containerExecCommandHandler, 162 | Mod: gocui.ModNone, 163 | } 164 | 165 | changePodLogsContainerAction = &guilib.Action{ 166 | Keys: keyMap[changePodLogsContainerActionName], 167 | Name: changePodLogsContainerActionName, 168 | Handler: changePodLogsContainerHandler, 169 | Mod: gocui.ModNone, 170 | } 171 | 172 | tailLogsAction = &guilib.Action{ 173 | Keys: keyMap[tailLogsActionName], 174 | Name: tailLogsActionName, 175 | Handler: tailLogsHandler, 176 | Mod: gocui.ModNone, 177 | } 178 | 179 | scrollLogsAction = &guilib.Action{ 180 | Keys: keyMap[scrollLogsActionName], 181 | Name: scrollLogsActionName, 182 | Handler: scrollLogsHandler, 183 | Mod: gocui.ModNone, 184 | } 185 | 186 | runPodAction = &guilib.Action{ 187 | Keys: keyMap[runPodActionName], 188 | Name: runPodActionName, 189 | Handler: runPodHandler, 190 | Mod: gocui.ModNone, 191 | } 192 | 193 | changeContext = &guilib.Action{ 194 | Keys: keyMap[changeContextActionName], 195 | Name: changeContextActionName, 196 | Handler: changeContextHandler, 197 | Mod: gocui.ModNone, 198 | } 199 | 200 | addCustomResourcePanelMoreAction = &moreAction{ 201 | NeedSelectResource: false, 202 | Action: *addCustomResourcePanelAction, 203 | } 204 | 205 | deleteCustomResourcePanelMoreAction = &moreAction{ 206 | NeedSelectResource: false, 207 | Action: *deleteCustomResourcePanelAction, 208 | } 209 | 210 | containerExecCommandMoreAction = &moreAction{ 211 | NeedSelectResource: true, 212 | Action: *containerExecCommandAction, 213 | } 214 | 215 | runPodMoreAction = &moreAction{ 216 | NeedSelectResource: false, 217 | Action: *runPodAction, 218 | } 219 | 220 | changeContextMoreAction = &moreAction{ 221 | NeedSelectResource: false, 222 | ShowAction: nil, 223 | Action: *changeContext, 224 | } 225 | 226 | commonResourceMoreActions = []*moreAction{ 227 | addCustomResourcePanelMoreAction, 228 | editResourceMoreAction, 229 | } 230 | 231 | moreActionsMap = map[string][]*moreAction{ 232 | clusterInfoViewName: { 233 | addCustomResourcePanelMoreAction, 234 | changeContextMoreAction, 235 | }, 236 | namespaceViewName: append( 237 | commonResourceMoreActions, 238 | copySelectedLineMoreAction, 239 | ), 240 | serviceViewName: append( 241 | commonResourceMoreActions, 242 | copySelectedLineMoreAction, 243 | ), 244 | deploymentViewName: append( 245 | commonResourceMoreActions, 246 | copySelectedLineMoreAction, 247 | &moreAction{ 248 | NeedSelectResource: true, 249 | Action: *newConfirmDialogAction(deploymentViewName, rolloutRestartAction), 250 | }, 251 | ), 252 | podViewName: append( 253 | commonResourceMoreActions, 254 | containerExecCommandMoreAction, 255 | copySelectedLineMoreAction, 256 | runPodMoreAction, 257 | ), 258 | navigationViewName: { 259 | addCustomResourcePanelMoreAction, 260 | copySelectedLineMoreAction, 261 | }, 262 | detailViewName: { 263 | addCustomResourcePanelMoreAction, 264 | copySelectedLineMoreAction, 265 | &moreAction{ 266 | NeedSelectResource: false, 267 | ShowAction: func(gui *guilib.Gui, view *guilib.View) bool { 268 | return navigationPath(activeView.Name, activeNavigationOpt) == navigationPath(podViewName, navigationOptLog) 269 | }, 270 | Action: *changePodLogsContainerAction, 271 | }, 272 | &moreAction{ 273 | NeedSelectResource: false, 274 | ShowAction: func(gui *guilib.Gui, view *guilib.View) bool { 275 | resourceName := resourceViewName(getViewResourceName(activeView.Name)) 276 | if resourceName == "" { 277 | return false 278 | } 279 | return resourceLogAble(resourceName) 280 | }, 281 | Action: *tailLogsAction, 282 | }, 283 | &moreAction{ 284 | NeedSelectResource: false, 285 | ShowAction: func(gui *guilib.Gui, view *guilib.View) bool { 286 | resourceName := resourceViewName(getViewResourceName(activeView.Name)) 287 | if resourceName == "" { 288 | return false 289 | } 290 | return resourceLogAble(resourceName) 291 | }, 292 | Action: *scrollLogsAction, 293 | }, 294 | &moreAction{ 295 | NeedSelectResource: false, 296 | ShowAction: func(gui *guilib.Gui, view *guilib.View) bool { 297 | return activeNavigationOpt == navigationOptConfig 298 | }, 299 | Action: *editResourceAction, 300 | }, 301 | }, 302 | } 303 | 304 | appActions = []*guilib.Action{ 305 | backToPreviousView, 306 | { 307 | Name: previousPageAction, 308 | Keys: keyMap[previousPageAction], 309 | Handler: previousPageHandler, 310 | Mod: gocui.ModNone, 311 | }, 312 | { 313 | Name: nextPageAction, 314 | Keys: keyMap[nextPageAction], 315 | Handler: nextPageHandler, 316 | Mod: gocui.ModNone, 317 | }, 318 | { 319 | Name: scrollUpAction, 320 | Keys: keyMap[scrollUpAction], 321 | Handler: scrollUpHandler, 322 | Mod: gocui.ModNone, 323 | }, 324 | { 325 | Name: scrollDownAction, 326 | Keys: keyMap[scrollDownAction], 327 | Handler: scrollDownHandler, 328 | Mod: gocui.ModNone, 329 | }, 330 | { 331 | Name: scrollTopAction, 332 | Keys: keyMap[scrollTopAction], 333 | Handler: scrollTopHandler, 334 | Mod: gocui.ModNone, 335 | }, 336 | { 337 | Name: scrollBottomAction, 338 | Keys: keyMap[scrollBottomAction], 339 | Handler: scrollBottomHandler, 340 | Mod: gocui.ModNone, 341 | }, 342 | } 343 | ) 344 | 345 | type ( 346 | moreAction struct { 347 | NeedSelectResource bool 348 | ShowAction func(*guilib.Gui, *guilib.View) bool 349 | guilib.Action 350 | } 351 | ) 352 | 353 | func switchNamespace(gui *guilib.Gui, selectedNamespaceLine string) { 354 | kubecli.Cli.SetNamespace(selectedNamespaceLine) 355 | for _, viewName := range []string{serviceViewName, deploymentViewName, podViewName} { 356 | view, err := gui.GetView(viewName) 357 | if err != nil { 358 | return 359 | } 360 | err = view.SetOrigin(0, 0) 361 | if err != nil { 362 | log.Logger.Warningf("switchNamespace - error %s", err) 363 | } 364 | } 365 | 366 | detailView, err := gui.GetView(detailViewName) 367 | if err != nil { 368 | return 369 | } 370 | detailView.Autoscroll = false 371 | err = detailView.SetOrigin(0, 0) 372 | if err != nil { 373 | log.Logger.Warningf("switchNamespace - detailView.SetOrigin(0, 0) error %s", err) 374 | } 375 | gui.ReRenderViews(resizeableViews...) 376 | gui.ReRenderViews(clusterInfoViewName, navigationViewName, detailViewName) 377 | } 378 | 379 | func newMoreActions(moreActions []*moreAction) *guilib.Action { 380 | return &guilib.Action{ 381 | Name: moreActionsName, 382 | Keys: keyMap[moreActionsName], 383 | Handler: func(gui *guilib.Gui, view *guilib.View) error { 384 | if err := showMoreActionDialog(gui, view, "More Actions", moreActions); err != nil { 385 | return err 386 | } 387 | return nil 388 | }, 389 | Mod: gocui.ModNone, 390 | } 391 | } 392 | 393 | func newConfirmDialogAction(relatedViewName string, action *guilib.Action) *guilib.Action { 394 | confirmTitle := fmt.Sprintf("Confirm to '%s' ?", action.Name) 395 | return &guilib.Action{ 396 | Keys: action.Keys, 397 | Name: action.Name, 398 | Key: action.Key, 399 | Handler: newConfirmDialogHandler(confirmTitle, relatedViewName, action.Handler), 400 | ReRenderAllView: action.ReRenderAllView, 401 | Mod: action.Mod, 402 | } 403 | } 404 | 405 | func newConfirmFilterInput(confirmHandler func(string) error) *guilib.Action { 406 | confirmFilterInput := &guilib.Action{ 407 | Name: confirmFilterInputAction, 408 | Keys: keyMap[confirmFilterInputAction], 409 | Handler: func(gui *guilib.Gui, _ *guilib.View) error { 410 | filteredView, err := gui.GetView(filteredViewName) 411 | if err != nil { 412 | return err 413 | } 414 | 415 | _, cy := filteredView.Cursor() 416 | filtered, _ := filteredView.Line(cy) 417 | 418 | return confirmHandler(filtered) 419 | }, 420 | Mod: gocui.ModNone, 421 | } 422 | return confirmFilterInput 423 | } 424 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Matt-Gleich/release" 6 | "github.com/TNK-Studio/lazykube/pkg/config" 7 | guilib "github.com/TNK-Studio/lazykube/pkg/gui" 8 | "github.com/TNK-Studio/lazykube/pkg/log" 9 | "github.com/TNK-Studio/lazykube/pkg/utils" 10 | "github.com/gookit/color" 11 | ) 12 | 13 | const ( 14 | githubRepo = "https://github.com/TNK-Studio/lazykube" 15 | ) 16 | 17 | var ( 18 | Version = "No Version Provided" 19 | ) 20 | 21 | // App lazykube application 22 | type App struct { 23 | version string 24 | ClusterInfo *guilib.View 25 | Namespace *guilib.View 26 | Service *guilib.View 27 | Deployment *guilib.View 28 | Pod *guilib.View 29 | Navigation *guilib.View 30 | Detail *guilib.View 31 | Option *guilib.View 32 | Gui *guilib.Gui 33 | } 34 | 35 | // NewApp new lazykube application 36 | func NewApp() *App { 37 | app := &App{ 38 | version: Version, 39 | ClusterInfo: ClusterInfo, 40 | Namespace: Namespace, 41 | Service: Service, 42 | Deployment: Deployment, 43 | Pod: Pod, 44 | Navigation: Navigation, 45 | Detail: Detail, 46 | Option: Option, 47 | } 48 | 49 | app.Gui = guilib.NewGui( 50 | *config.Conf.GuiConfig, 51 | app.ClusterInfo, 52 | app.Namespace, 53 | app.Service, 54 | app.Deployment, 55 | app.Pod, 56 | app.Navigation, 57 | app.Detail, 58 | app.Option, 59 | ) 60 | app.Gui.OnRender = app.OnRender 61 | app.Gui.OnRenderOptions = app.OnRenderOptions 62 | app.Gui.Actions = appActions 63 | app.Gui.OnSizeChange = func(gui *guilib.Gui) error { 64 | if err := resizePanelHeight(gui); err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | return app 71 | } 72 | 73 | func (app *App) Version() string { 74 | return app.version 75 | } 76 | 77 | func (app *App) CheckRelease() (bool, string, error) { 78 | isOutdated, version, err := release.Check(app.version, githubRepo) 79 | if err != nil { 80 | log.Logger.Error(isOutdated, version, err) 81 | } 82 | return isOutdated, version, err 83 | } 84 | 85 | // Run run 86 | func (app *App) Run() { 87 | app.Gui.Run() 88 | } 89 | 90 | // Stop stop 91 | func (app *App) Stop() { 92 | app.Gui.Close() 93 | isOutdated, version, err := app.CheckRelease() 94 | if err == nil && isOutdated { 95 | fmt.Printf( 96 | "%s 🎉. %s => %s %s/releases/tag/%s\n", 97 | color.Green.Sprint("A new release of lazykube is available"), 98 | color.Yellow.Sprint(app.Version()), 99 | color.Green.Sprint(version), 100 | githubRepo, 101 | version, 102 | ) 103 | } 104 | } 105 | 106 | // OnRender OnRender 107 | func (app *App) OnRender(gui *guilib.Gui) error { 108 | if config.Conf.UserConfig.CustomResourcePanels != nil { 109 | for _, resource := range config.Conf.UserConfig.CustomResourcePanels { 110 | if err := addCustomResourcePanel(gui, resource); err != nil { 111 | log.Logger.Warningf("app.OnRender - addCustomResourcePanel(gui, %s) error %s", resource, err) 112 | } 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | // OnRenderOptions OnRenderOptions 119 | func (app *App) OnRenderOptions(gui *guilib.Gui) error { 120 | return gui.RenderString( 121 | app.Option.Name, 122 | utils.OptionsMapToString( 123 | map[string]string{ 124 | "←→↑↓": "navigate", 125 | "Ctrl+c": "exit", 126 | "Esc": "back", 127 | "PgUp/PgDn": "scroll", 128 | "Home/End": "top/bottom", 129 | "Tab": "next panel", 130 | "f": "filter", 131 | "m": "more action", 132 | }), 133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /pkg/app/error.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "errors" 4 | 5 | var ( 6 | resourceNotFoundErr = errors.New("Resource not found. ") 7 | noResourceSelectedErr = errors.New("No resource selected. ") 8 | noNamespaceSelectedErr = errors.New("No namespace selected. ") 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/app/format.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/TNK-Studio/lazykube/pkg/utils" 5 | "strings" 6 | ) 7 | 8 | func formatSelectedNamespace(selected string) string { 9 | return formatResourceName(selected, 0) 10 | } 11 | 12 | func formatResourceName(selected string, index int) string { 13 | if selected == "" { 14 | return "" 15 | } 16 | selected = utils.DeleteExtraSpace(selected) 17 | formatted := strings.Split(selected, " ") 18 | length := len(formatted) 19 | if index < 0 && length-index >= 0 { 20 | resourceName := formatted[length-index] 21 | if validateResourceName(resourceName) { 22 | return resourceName 23 | } 24 | } 25 | 26 | if index < length { 27 | resourceName := formatted[index] 28 | if validateResourceName(resourceName) { 29 | return resourceName 30 | } 31 | } 32 | return "" 33 | } 34 | 35 | func validateResourceName(resourceName string) bool { 36 | if strings.ToLower(resourceName) != resourceName { 37 | return false 38 | } 39 | 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /pkg/app/handler.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TNK-Studio/lazykube/pkg/config" 6 | guilib "github.com/TNK-Studio/lazykube/pkg/gui" 7 | "github.com/TNK-Studio/lazykube/pkg/kubecli" 8 | "github.com/TNK-Studio/lazykube/pkg/log" 9 | "github.com/atotto/clipboard" 10 | "github.com/jroimartin/gocui" 11 | "github.com/nsf/termbox-go" 12 | "github.com/pkg/errors" 13 | "math" 14 | "os" 15 | "strings" 16 | ) 17 | 18 | const ( 19 | resourceNotFound = "Resource not found." 20 | noHistory = "No History." 21 | 22 | defaultCommand = "/bin/sh" 23 | ) 24 | 25 | func nextFunctionViewHandler(gui *guilib.Gui, _ *guilib.View) error { 26 | currentView := gui.CurrentView() 27 | if currentView == nil { 28 | return nil 29 | } 30 | 31 | var nextViewName string 32 | for index, viewName := range functionViews { 33 | if currentView.Name == viewName { 34 | nextIndex := index + 1 35 | if nextIndex >= len(functionViews) { 36 | nextIndex = 0 37 | } 38 | nextViewName = functionViews[nextIndex] 39 | log.Logger.Debugf("nextFunctionViewHandler - nextViewName: %s", nextViewName) 40 | break 41 | } 42 | } 43 | if nextViewName == "" { 44 | return nil 45 | } 46 | gui.ReRenderViews(navigationViewName, detailViewName) 47 | return gui.FocusView(nextViewName, true) 48 | } 49 | 50 | func backToPreviousViewHandler(gui *guilib.Gui, _ *guilib.View) error { 51 | gui.ReRenderViews(navigationViewName, detailViewName) 52 | if gui.HasPreviousView() { 53 | return gui.ReturnPreviousView() 54 | } 55 | 56 | return gui.FocusView(clusterInfoViewName, false) 57 | } 58 | 59 | func toNavigationHandler(gui *guilib.Gui, _ *guilib.View) error { 60 | return gui.FocusView(navigationViewName, true) 61 | } 62 | 63 | func navigationArrowRightHandler(gui *guilib.Gui, _ *guilib.View) error { 64 | gui.ReRenderViews(navigationViewName, detailViewName) 65 | options := viewNavigationMap[activeView.Name] 66 | if navigationIndex+1 >= len(options) { 67 | return nil 68 | } 69 | switchNavigation(gui, navigationIndex+1) 70 | return nil 71 | } 72 | 73 | func navigationArrowLeftHandler(gui *guilib.Gui, _ *guilib.View) error { 74 | gui.ReRenderViews(navigationViewName, detailViewName) 75 | if navigationIndex-1 < 0 { 76 | return gui.ReturnPreviousView() 77 | } 78 | switchNavigation(gui, navigationIndex-1) 79 | return nil 80 | } 81 | 82 | func nextPageHandler(_ *guilib.Gui, view *guilib.View) error { 83 | view.Autoscroll = false 84 | ox, oy := view.Origin() 85 | _, height := view.Size() 86 | newOy := int(math.Min(float64(len(view.ViewBufferLines())), float64(oy+height))) 87 | return view.SetOrigin(ox, newOy) 88 | } 89 | 90 | func previousPageHandler(_ *guilib.Gui, view *guilib.View) error { 91 | view.Autoscroll = false 92 | ox, oy := view.Origin() 93 | _, height := view.Size() 94 | newOy := int(math.Max(0, float64(oy-height))) 95 | return view.SetOrigin(ox, newOy) 96 | } 97 | 98 | func scrollUpHandler(_ *guilib.Gui, view *guilib.View) error { 99 | view.Autoscroll = false 100 | ox, oy := view.Origin() 101 | newOy := int(math.Max(0, float64(oy-2))) 102 | return view.SetOrigin(ox, newOy) 103 | } 104 | 105 | func scrollDownHandler(_ *guilib.Gui, view *guilib.View) error { 106 | view.Autoscroll = false 107 | ox, oy := view.Origin() 108 | 109 | reservedLines := 0 110 | _, sizeY := view.Size() 111 | reservedLines = sizeY 112 | 113 | totalLines := len(view.ViewBufferLines()) 114 | if oy+reservedLines >= totalLines { 115 | view.Autoscroll = true 116 | return nil 117 | } 118 | 119 | return view.SetOrigin(ox, oy+2) 120 | } 121 | 122 | func scrollTopHandler(_ *guilib.Gui, view *guilib.View) error { 123 | view.Autoscroll = false 124 | ox, _ := view.Origin() 125 | return view.SetOrigin(ox, 0) 126 | } 127 | 128 | func scrollBottomHandler(_ *guilib.Gui, view *guilib.View) error { 129 | totalLines := len(view.ViewBufferLines()) 130 | if totalLines == 0 { 131 | return nil 132 | } 133 | _, vy := view.Size() 134 | if totalLines <= vy { 135 | return nil 136 | } 137 | 138 | ox, _ := view.Origin() 139 | view.Autoscroll = true 140 | return view.SetOrigin(ox, totalLines-1) 141 | } 142 | 143 | func previousLineHandler(gui *guilib.Gui, view *guilib.View) error { 144 | currentView := gui.CurrentView() 145 | if currentView == nil { 146 | return nil 147 | } 148 | 149 | _, height := view.Size() 150 | cx, cy := view.Cursor() 151 | ox, oy := view.Origin() 152 | 153 | if cy-1 <= 0 && oy-1 > 0 { 154 | err := view.SetOrigin(ox, int(math.Max(0, float64(oy-height+1)))) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | err = view.SetCursor(cx, height-1) 160 | if err != nil { 161 | return err 162 | } 163 | return nil 164 | } 165 | 166 | view.MoveCursor(0, -1, false) 167 | return nil 168 | } 169 | 170 | func nextLineHandler(gui *guilib.Gui, view *guilib.View) error { 171 | currentView := gui.CurrentView() 172 | if currentView == nil { 173 | return nil 174 | } 175 | 176 | _, height := view.Size() 177 | cx, cy := view.Cursor() 178 | if cy+1 >= height-1 { 179 | ox, oy := view.Origin() 180 | err := view.SetOrigin(ox, oy+height-1) 181 | if err != nil { 182 | return err 183 | } 184 | err = view.SetCursor(cx, 0) 185 | if err != nil { 186 | return err 187 | } 188 | return nil 189 | } 190 | 191 | view.MoveCursor(0, 1, false) 192 | return nil 193 | } 194 | 195 | func copySelectedLineHandler(gui *guilib.Gui, view *guilib.View) error { 196 | if view.SelectedLine != "" { 197 | clipboard.WriteAll(view.SelectedLine) 198 | } 199 | currentView := gui.CurrentView() 200 | if currentView != nil && currentView.Name == moreActionsViewName { 201 | if err := gui.ReturnPreviousView(); err != nil { 202 | return err 203 | } 204 | } 205 | return nil 206 | } 207 | 208 | func viewSelectedLineChangeHandler(gui *guilib.Gui, view *guilib.View, _ string) error { 209 | gui.ReRenderViews(view.Name, navigationViewName, detailViewName) 210 | gui.ClearViews(detailViewName) 211 | clearDetailViewState(gui) 212 | return nil 213 | } 214 | 215 | func getResourceNamespaceAndName(gui *guilib.Gui, resourceView *guilib.View) (namespace string, resourceName string, err error) { 216 | if resourceView.Name == namespaceViewName { 217 | return "", formatSelectedNamespace(resourceView.SelectedLine), nil 218 | } 219 | 220 | namespace = kubecli.Cli.Namespace() 221 | selected := resourceView.SelectedLine 222 | 223 | if selected == "" { 224 | return "", "", noResourceSelectedErr 225 | } 226 | 227 | if !notResourceSelected(namespace) { 228 | resourceName := formatResourceName(selected, 0) 229 | if notResourceSelected(resourceName) { 230 | return "", "", noResourceSelectedErr 231 | } 232 | return kubecli.Cli.Namespace(), resourceName, nil 233 | } 234 | 235 | namespace = formatResourceName(selected, 0) 236 | resourceName = formatResourceName(selected, 1) 237 | if notResourceSelected(resourceName) { 238 | return kubecli.Cli.Namespace(), "", noResourceSelectedErr 239 | } 240 | 241 | if notResourceSelected(namespace) { 242 | namespace = kubecli.Cli.Namespace() 243 | } 244 | 245 | return namespace, resourceName, nil 246 | } 247 | 248 | func editResourceHandler(gui *guilib.Gui, view *guilib.View) error { 249 | var err error 250 | var resource, namespace, resourceName string 251 | if view.Name == detailViewName { 252 | _, resource, namespace, resourceName, err = resourceMoreActionHandlerHelper(gui, activeView) 253 | } else { 254 | _, resource, namespace, resourceName, err = resourceMoreActionHandlerHelper(gui, view) 255 | } 256 | if errors.Is(err, resourceNotFoundErr) || errors.Is(err, noResourceSelectedErr) { 257 | // Todo: show error on panel 258 | return nil 259 | } 260 | 261 | cli(namespace).Edit(newStdStream(), resource, resourceName).Run() 262 | if err := gui.ForceFlush(); err != nil { 263 | return err 264 | } 265 | gui.ReRenderAll() 266 | return nil 267 | } 268 | 269 | func rolloutRestartHandler(gui *guilib.Gui, view *guilib.View) error { 270 | view, resource, namespace, resourceName, err := resourceMoreActionHandlerHelper(gui, view) 271 | if errors.Is(err, resourceNotFoundErr) || errors.Is(err, noResourceSelectedErr) { 272 | // Todo: show error on panel 273 | return nil 274 | } 275 | 276 | cli(namespace).RolloutRestart(viewStreams(view), resource, resourceName).Run() 277 | view.ReRender() 278 | return nil 279 | } 280 | 281 | func resourceMoreActionHandlerHelper(gui *guilib.Gui, view *guilib.View) (resourceView *guilib.View, resource string, namespace string, resourceName string, err error) { 282 | resource = getViewResourceName(view.Name) 283 | if resource == "" { 284 | return nil, "", "", "", resourceNotFoundErr 285 | } 286 | namespace, resourceName, err = getResourceNamespaceAndName(gui, view) 287 | if err != nil { 288 | return nil, "", "", "", err 289 | } 290 | return view, resource, namespace, resourceName, nil 291 | } 292 | 293 | func newConfirmDialogHandler(title, relatedViewName string, handler guilib.ViewHandler) guilib.ViewHandler { 294 | return func(gui *guilib.Gui, view *guilib.View) error { 295 | if err := showConfirmActionDialog(gui, title, relatedViewName, handler); err != nil { 296 | return err 297 | } 298 | return nil 299 | } 300 | } 301 | 302 | func confirmDialogOptionHandler(gui *guilib.Gui, view *guilib.View, relatedViewName, option string, handler guilib.ViewHandler) error { 303 | if option == cancelDialogOpt { 304 | if err := gui.DeleteView(view.Name); err != nil { 305 | return err 306 | } 307 | if err := gui.ReturnPreviousView(); err != nil { 308 | return err 309 | } 310 | return nil 311 | } 312 | 313 | if option == confirmDialogOpt { 314 | relatedView, err := gui.GetView(relatedViewName) 315 | if err != nil { 316 | return err 317 | } 318 | if err := handler(gui, relatedView); err != nil { 319 | return err 320 | } 321 | if err := gui.DeleteView(view.Name); err != nil { 322 | return err 323 | } 324 | if err := gui.ReturnPreviousView(); err != nil { 325 | return err 326 | } 327 | return nil 328 | } 329 | return nil 330 | } 331 | 332 | func addCustomResourcePanelHandler(gui *guilib.Gui, _ *guilib.View) error { 333 | stream := newStream() 334 | kubecli.Cli.APIResources(stream).Run() 335 | apiResourcesStr := streamToString(stream) 336 | 337 | apiResources := strings.Split(apiResourcesStr, "\n") 338 | if len(apiResources) > 0 { 339 | apiResources = apiResources[1:] 340 | } 341 | 342 | if err := showFilterDialog( 343 | gui, 344 | "Filter resource by name.", 345 | func(resource string) error { 346 | if resource == "" || resource == resourceNotFound { 347 | return nil 348 | } 349 | 350 | resource = formatResourceName(resource, 0) 351 | 352 | if err := addCustomResourcePanel(gui, resource); err != nil { 353 | return err 354 | } 355 | if err := closeFilterDialog(gui); err != nil { 356 | if errors.Is(err, gocui.ErrUnknownView) { 357 | return nil 358 | } 359 | return err 360 | } 361 | return nil 362 | }, 363 | func(string) ([]string, error) { 364 | if len(apiResources) >= 1 { 365 | return apiResources[1:], nil 366 | } 367 | return []string{}, nil 368 | }, 369 | resourceNotFound, 370 | false, 371 | ); err != nil { 372 | return err 373 | } 374 | return nil 375 | } 376 | 377 | func deleteCustomResourcePanelHandler(gui *guilib.Gui, view *guilib.View) error { 378 | if err := deleteCustomResourcePanel(gui, view.Name); err != nil { 379 | return err 380 | } 381 | return nil 382 | } 383 | 384 | func containerExecCommandHandler(gui *guilib.Gui, view *guilib.View) error { 385 | namespace, resourceName, err := getResourceNamespaceAndName(gui, view) 386 | if err != nil { 387 | if errors.Is(err, noResourceSelectedErr) { 388 | return nil 389 | } 390 | return err 391 | } 392 | 393 | containers := getPodContainers(namespace, resourceName) 394 | 395 | if err := showOptionsDialog( 396 | gui, 397 | "Please select a container to execute command.", 398 | 1, 399 | func(containerName string) error { 400 | if containerName == "" { 401 | return nil 402 | } 403 | 404 | if err := showInputDialog( 405 | gui, 406 | "Please input command.", 407 | 2, 408 | func(command string) error { 409 | if err := gui.ReInitTermBox(); err != nil { 410 | return err 411 | } 412 | gui.Config.Mouse = false 413 | gui.Configure() 414 | _ = termbox.Flush() 415 | 416 | cli(namespace). 417 | Exec(newStdStream(), resourceName, command). 418 | SetFlag("container", containerName). 419 | SetFlag("tty", "true"). 420 | SetFlag("stdin", "true"). 421 | Run() 422 | 423 | _, err = fmt.Fprintf(os.Stdout, "\n\n%s\n", "Press 'x' twice time return to lazykube.") 424 | if err != nil { 425 | log.Logger.Error(err) 426 | } 427 | 428 | // Note: Enter key not working, but dont know why ... 429 | if _, err := fmt.Scanln(); err != nil { 430 | log.Logger.Error(err) 431 | } 432 | 433 | if err := gui.ForceFlush(); err != nil { 434 | return err 435 | } 436 | gui.Config.Mouse = true 437 | gui.Configure() 438 | if err := gui.ReturnPreviousView(); err != nil { 439 | return err 440 | } 441 | 442 | gui.ReRenderAll() 443 | return nil 444 | }, 445 | defaultCommand, 446 | ); err != nil { 447 | return err 448 | } 449 | 450 | return nil 451 | }, 452 | func() []string { 453 | return containers 454 | }, 455 | ); err != nil { 456 | return err 457 | } 458 | 459 | return nil 460 | } 461 | 462 | func changePodLogsContainerHandler(gui *guilib.Gui, view *guilib.View) error { 463 | podView, err := gui.GetView(podViewName) 464 | if err != nil { 465 | return err 466 | } 467 | 468 | namespace, resourceName, err := getResourceNamespaceAndName(gui, podView) 469 | if err != nil { 470 | if errors.Is(err, noResourceSelectedErr) { 471 | return nil 472 | } 473 | return err 474 | } 475 | 476 | containers := getPodContainers(namespace, resourceName) 477 | 478 | if err := showOptionsDialog( 479 | gui, 480 | "Please select a container to view logs.", 481 | 1, 482 | func(containerName string) error { 483 | if containerName == "" { 484 | return nil 485 | } 486 | if err := view.SetState(logContainerStateKey, containerName, true); err != nil { 487 | return err 488 | } 489 | if err := view.SetState(viewLastRenderTimeStateKey, nil, true); err != nil { 490 | return err 491 | } 492 | if err := view.SetState(logSinceTimeStateKey, nil, true); err != nil { 493 | return err 494 | } 495 | view.Clear() 496 | if err := view.SetOrigin(0, 0); err != nil { 497 | return err 498 | } 499 | view.ReRender() 500 | if err := gui.FocusView(detailViewName, false); err != nil { 501 | return err 502 | } 503 | return nil 504 | }, 505 | func() []string { 506 | return containers 507 | }, 508 | ); err != nil { 509 | return err 510 | } 511 | 512 | return nil 513 | } 514 | 515 | func tailLogsHandler(gui *guilib.Gui, view *guilib.View) error { 516 | if err := view.SetState(ScrollingLogsStateKey, false, false); err != nil { 517 | return err 518 | } 519 | if err := gui.FocusView(detailViewName, false); err != nil { 520 | return err 521 | } 522 | return nil 523 | } 524 | 525 | func scrollLogsHandler(gui *guilib.Gui, view *guilib.View) error { 526 | if err := view.SetState(ScrollingLogsStateKey, true, false); err != nil { 527 | return err 528 | } 529 | if err := gui.FocusView(detailViewName, false); err != nil { 530 | return err 531 | } 532 | return nil 533 | } 534 | 535 | func runPodHandler(gui *guilib.Gui, _ *guilib.View) error { 536 | if err := showFilterDialog( 537 | gui, 538 | "Please select a namespace to run a pod.", 539 | func(namespace string) error { 540 | namespace = formatSelectedNamespace(namespace) 541 | if notResourceSelected(namespace) { 542 | return nil 543 | } 544 | 545 | return runPodNameInput(gui, namespace) 546 | }, 547 | func(string) ([]string, error) { 548 | namespaceView, err := gui.GetView(namespaceViewName) 549 | if err != nil { 550 | log.Logger.Error(err) 551 | return nil, err 552 | } 553 | 554 | namespaces := namespaceView.ViewBufferLines() 555 | if len(namespaces) >= 1 { 556 | return namespaces[1:], nil 557 | } 558 | 559 | return []string{}, nil 560 | }, 561 | "No namespaces.", 562 | false, 563 | ); err != nil { 564 | return err 565 | } 566 | return nil 567 | } 568 | 569 | func runPodNameInput(gui *guilib.Gui, namespace string) error { 570 | if err := showFilterDialog(gui, "Please input pod name.", 571 | func(podName string) error { 572 | return runPodImageOptions(gui, namespace, podName) 573 | }, 574 | func(inputted string) ([]string, error) { 575 | if config.Conf.UserConfig.History.PodNameHistory != nil { 576 | return config.Conf.UserConfig.History.PodNameHistory, nil 577 | } 578 | 579 | return []string{}, nil 580 | }, noHistory, true); err != nil { 581 | return err 582 | } 583 | return nil 584 | } 585 | 586 | func runPodImageOptions(gui *guilib.Gui, namespace, podName string) error { 587 | if err := showFilterDialog( 588 | gui, 589 | "Please input image of container.", 590 | func(image string) error { 591 | return runPodCommandInput(gui, namespace, podName, image) 592 | }, 593 | func(inputted string) ([]string, error) { 594 | if config.Conf.UserConfig.History.ImageHistory != nil { 595 | return config.Conf.UserConfig.History.ImageHistory, nil 596 | } 597 | 598 | return []string{}, nil 599 | }, 600 | noHistory, 601 | true, 602 | ); err != nil { 603 | return err 604 | } 605 | return nil 606 | } 607 | 608 | func runPodCommandInput(gui *guilib.Gui, namespace, podName, image string) error { 609 | if err := showFilterDialog( 610 | gui, 611 | "Please input command.", 612 | func(command string) error { 613 | if err := gui.ReInitTermBox(); err != nil { 614 | return err 615 | } 616 | gui.Config.Mouse = false 617 | gui.Config.Cursor = true 618 | gui.Configure() 619 | _ = termbox.Flush() 620 | 621 | cli(namespace). 622 | Run(newStdStream(), podName, command). 623 | SetFlag("rm", "true"). 624 | SetFlag("restart", "Never"). 625 | SetFlag("image-pull-policy", "IfNotPresent"). 626 | SetFlag("tty", "true"). 627 | SetFlag("stdin", "true"). 628 | SetFlag("image", image). 629 | Run() 630 | 631 | _, err := fmt.Fprintf(os.Stdout, "\n\n%s\n", "Press 'x' twice time return to lazykube.") 632 | if err != nil { 633 | log.Logger.Error(err) 634 | } 635 | 636 | // Note: Enter key not working, but dont know why ... 637 | if _, err := fmt.Scanln(); err != nil { 638 | log.Logger.Error(err) 639 | } 640 | 641 | if err := gui.ForceFlush(); err != nil { 642 | return err 643 | } 644 | gui.Config.Mouse = true 645 | gui.Config.Cursor = false 646 | gui.Configure() 647 | gui.ReRenderAll() 648 | config.Conf.UserConfig.History.AddPodNameHistory(podName) 649 | config.Conf.UserConfig.History.AddImageHistory(image) 650 | config.Conf.UserConfig.History.AddCommandHistory(command) 651 | config.Save() 652 | if err := gui.ReturnPreviousView(); err != nil { 653 | return err 654 | } 655 | return nil 656 | }, 657 | func(inputted string) ([]string, error) { 658 | if config.Conf.UserConfig.History.CommandHistory != nil { 659 | return config.Conf.UserConfig.History.CommandHistory, nil 660 | } 661 | 662 | return []string{}, nil 663 | }, 664 | noHistory, 665 | true, 666 | ); err != nil { 667 | return err 668 | } 669 | return nil 670 | } 671 | 672 | func changeContextHandler(gui *guilib.Gui, view *guilib.View) error { 673 | if err := showFilterDialog( 674 | gui, 675 | "Selected a context to swicth.", 676 | func(confirmed string) error { 677 | kubecli.Cli.SetCurrentContext(confirmed) 678 | gui.ReRenderAll() 679 | if err := gui.FocusView(clusterInfoViewName, false); err != nil { 680 | return err 681 | } 682 | return nil 683 | }, 684 | func(inputted string) ([]string, error) { 685 | return kubecli.Cli.ListContexts(), nil 686 | }, 687 | "No contexts.", 688 | false, 689 | ); err != nil { 690 | return err 691 | } 692 | 693 | return nil 694 | } 695 | -------------------------------------------------------------------------------- /pkg/app/keymap.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TNK-Studio/lazykube/pkg/utils" 6 | "github.com/gookit/color" 7 | "github.com/jroimartin/gocui" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | // All common actions name 13 | nextFunctionViewAction = "nextFunctionView" 14 | backToPreviousViewAction = "backToPreviousView" 15 | toNavigationAction = "toNavigation" 16 | navigationArrowLeft = "navigationArrowLeft" 17 | navigationArrowRight = "navigationArrowRight" 18 | navigationDown = "navigationDown" 19 | detailToNavigation = "detailToNavigation" 20 | detailArrowUp = "detailArrowUp" 21 | detailArrowDown = "detailArrowDown" 22 | previousLineAction = "previousLine" 23 | nextLineAction = "nextLine" 24 | previousPageAction = "previousPage" 25 | nextPageAction = "nextPage" 26 | scrollUpAction = "scrollUp" 27 | scrollDownAction = "scrollDown" 28 | scrollTopAction = "scrollTop" 29 | scrollBottomAction = "scrollBottom" 30 | filterResourceActionName = "filterResourceAction" 31 | moreActionsName = "moreActions" 32 | toFilteredViewAction = "toFiltered" 33 | toFilterInputAction = "toFilterInput" 34 | filteredNextLineAction = "filteredNextLine" 35 | filteredPreviousLineAction = "filteredPreviousLine" 36 | confirmFilterInputAction = "confirmFilterInput" 37 | switchConfirmDialogOpt = "switchConfirmDialogOpt" 38 | confirmDialogEnter = "confirmDialogEnter" 39 | optionsDialogEnter = "optionsDialogEnter" 40 | inputDialogEnter = "inputDialogEnter" 41 | 42 | // More actions 43 | copySelectedLineAction = "Copy Selected Line" 44 | editResourceActionName = "Edit Resource" 45 | rolloutRestartActionName = "Rollout Restart" 46 | addCustomResourcePanelActionName = "Add custom resource panel" 47 | deleteCustomResourcePanelActionName = "Delete custom resource panel" 48 | containerExecCommandActionName = "Execute the command" 49 | changePodLogsContainerActionName = "Change pod logs container" 50 | tailLogsActionName = "Tail logs" 51 | scrollLogsActionName = "Scroll logs" 52 | runPodActionName = "Run a pod with an image" 53 | changeContextActionName = "Change context" 54 | ) 55 | 56 | var ( 57 | // All common actions key map. 58 | keyMap = map[string][]interface{}{ 59 | nextFunctionViewAction: {gocui.KeyTab}, 60 | backToPreviousViewAction: {gocui.KeyEsc}, 61 | toNavigationAction: {gocui.KeyEnter, gocui.KeyArrowRight, 'l'}, 62 | navigationArrowLeft: {gocui.KeyArrowLeft, 'k'}, 63 | navigationArrowRight: {gocui.KeyArrowRight, 'l'}, 64 | navigationDown: {gocui.KeyArrowDown, 'j', gocui.KeyTab}, 65 | detailToNavigation: {gocui.KeyTab}, 66 | detailArrowUp: {gocui.KeyArrowUp, 'h'}, 67 | detailArrowDown: {gocui.KeyArrowDown, 'j'}, 68 | previousLineAction: {gocui.KeyArrowUp, 'h'}, 69 | nextLineAction: {gocui.KeyArrowDown, 'j'}, 70 | copySelectedLineAction: {'C'}, 71 | previousPageAction: {gocui.KeyPgup}, 72 | nextPageAction: {gocui.KeyPgdn}, 73 | scrollUpAction: {gocui.MouseWheelUp}, 74 | scrollDownAction: {gocui.MouseWheelDown}, 75 | scrollTopAction: {gocui.KeyHome}, 76 | scrollBottomAction: {gocui.KeyEnd}, 77 | filterResourceActionName: {gocui.KeyF4, 'f'}, 78 | editResourceActionName: {'e'}, 79 | rolloutRestartActionName: {'r'}, 80 | moreActionsName: {gocui.KeyF3, 'm'}, 81 | toFilteredViewAction: {gocui.KeyTab, gocui.KeyArrowDown}, 82 | toFilterInputAction: {gocui.KeyTab}, 83 | filteredNextLineAction: {gocui.KeyArrowDown, 'j'}, 84 | filteredPreviousLineAction: {gocui.KeyArrowUp, 'h'}, 85 | confirmFilterInputAction: {gocui.KeyEnter}, 86 | switchConfirmDialogOpt: {gocui.KeyTab, gocui.KeyArrowRight, gocui.KeyArrowLeft, 'k', 'l'}, 87 | confirmDialogEnter: {gocui.KeyEnter}, 88 | addCustomResourcePanelActionName: {'+'}, 89 | deleteCustomResourcePanelActionName: {'-'}, 90 | containerExecCommandActionName: {'x'}, 91 | optionsDialogEnter: {gocui.KeyEnter}, 92 | inputDialogEnter: {gocui.KeyEnter}, 93 | changePodLogsContainerActionName: {'c'}, 94 | tailLogsActionName: {'t'}, 95 | scrollLogsActionName: {'s'}, 96 | runPodActionName: {'r'}, 97 | changeContextActionName: {'~'}, 98 | } 99 | ) 100 | 101 | func keyMapDescription(keys []interface{}, description string) string { 102 | keysName := make([]string, 0) 103 | for _, key := range keys { 104 | keysName = append(keysName, utils.GetKey(key)) 105 | } 106 | return fmt.Sprintf("%-20s %s", color.Blue.Sprintf(strings.Join(keysName, "/")), description) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/app/kubecli.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/TNK-Studio/lazykube/pkg/kubecli" 5 | "strings" 6 | ) 7 | 8 | const matchLabels = "jsonpath='{.spec.selector.matchLabels}'" 9 | 10 | func cli(namespace string) *kubecli.KubeCLI { 11 | if namespace == kubecli.Cli.Namespace() { 12 | return kubecli.Cli 13 | } 14 | return kubecli.Cli.WithNamespace(namespace) 15 | } 16 | 17 | func resourceLabelSelectorJSONPath(resource string) string { 18 | var jsonPath string 19 | switch resource { 20 | case "services", "service", "svc": 21 | jsonPath = "jsonpath='{.spec.selector}'" 22 | case "deployments", "deployment", "deploy": 23 | jsonPath = matchLabels 24 | case "statefulsets", "statefulset", "sts": 25 | jsonPath = matchLabels 26 | case "daemonsets", "daemonset", "ds": 27 | jsonPath = matchLabels 28 | } 29 | return jsonPath 30 | } 31 | 32 | func getPodContainers(namespace, podName string) []string { 33 | // Todo: support others resource 34 | stream := newStream() 35 | cli(namespace). 36 | Get(stream, "pods", podName). 37 | SetFlag("output", "jsonpath='{.spec.containers[*].name}'"). 38 | Run() 39 | 40 | result := strings.ReplaceAll(streamToString(stream), "'", "") 41 | containers := strings.Split(result, " ") 42 | if len(containers) == 0 { 43 | return []string{} 44 | } 45 | return containers 46 | } 47 | -------------------------------------------------------------------------------- /pkg/app/panel.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/TNK-Studio/lazykube/pkg/config" 7 | guilib "github.com/TNK-Studio/lazykube/pkg/gui" 8 | "github.com/TNK-Studio/lazykube/pkg/kubecli" 9 | "github.com/fatih/camelcase" 10 | "github.com/jroimartin/gocui" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | clusterInfoViewName = "clusterInfo" 16 | deploymentViewName = "deployment" 17 | navigationViewName = "navigation" 18 | detailViewName = "detail" 19 | namespaceViewName = "namespace" 20 | optionViewName = "option" 21 | podViewName = "pod" 22 | serviceViewName = "service" 23 | ) 24 | 25 | var ( 26 | ClusterInfo = &guilib.View{ 27 | Name: clusterInfoViewName, 28 | Title: "Cluster Info", 29 | Clickable: true, 30 | ZIndex: zIndexOfFunctionView(clusterInfoViewName), 31 | LowerRightPointXFunc: func(gui *guilib.Gui, view *guilib.View) int { 32 | return leftSideWidth(gui.MaxWidth()) 33 | }, 34 | LowerRightPointYFunc: reactiveHeight, 35 | OnRender: renderClusterInfo, 36 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 37 | toNavigation, 38 | nextFunctionView, 39 | changeContext, 40 | newMoreActions(moreActionsMap[clusterInfoViewName]), 41 | }), 42 | OnFocus: func(gui *guilib.Gui, view *guilib.View) error { 43 | gui.ReRenderViews(navigationViewName, detailViewName) 44 | return nil 45 | }, 46 | } 47 | 48 | Deployment = &guilib.View{ 49 | Name: deploymentViewName, 50 | Title: "Deployments", 51 | FgColor: gocui.ColorDefault, 52 | ZIndex: zIndexOfFunctionView(deploymentViewName), 53 | Clickable: true, 54 | Highlight: true, 55 | SelFgColor: gocui.ColorGreen, 56 | OnRender: resourceListRender, 57 | OnSelectedLineChange: viewSelectedLineChangeHandler, 58 | OnFocus: func(gui *guilib.Gui, view *guilib.View) error { 59 | if err := onFocusClearSelected(gui, view); err != nil { 60 | return err 61 | } 62 | return nil 63 | }, 64 | DimensionFunc: guilib.BeneathView( 65 | func(*guilib.Gui, *guilib.View) string { 66 | return serviceViewName 67 | }, 68 | reactiveHeight, 69 | migrateTopFunc, 70 | ), 71 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 72 | toNavigation, 73 | nextFunctionView, 74 | previousLine, 75 | nextLine, 76 | copySelectedLine, 77 | filterResource, 78 | editResourceAction, 79 | newConfirmDialogAction(deploymentViewName, rolloutRestartAction), 80 | newMoreActions(moreActionsMap[deploymentViewName]), 81 | }), 82 | } 83 | 84 | Navigation = &guilib.View{ 85 | Name: navigationViewName, 86 | Title: "Navigation", 87 | Clickable: true, 88 | CanNotReturn: true, 89 | OnClick: navigationOnClick, 90 | FgColor: gocui.ColorGreen, 91 | DimensionFunc: func(gui *guilib.Gui, view *guilib.View) (int, int, int, int) { 92 | return leftSideWidth(gui.MaxWidth()) + 1, 0, gui.MaxWidth() - 1, 2 93 | }, 94 | OnRender: navigationRender, 95 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 96 | { 97 | Name: navigationArrowLeft, 98 | Keys: keyMap[navigationArrowLeft], 99 | Handler: navigationArrowLeftHandler, 100 | Mod: gocui.ModNone, 101 | }, 102 | { 103 | Name: navigationArrowRight, 104 | Keys: keyMap[navigationArrowRight], 105 | Handler: navigationArrowRightHandler, 106 | Mod: gocui.ModNone, 107 | }, 108 | { 109 | Name: navigationDown, 110 | Keys: keyMap[navigationDown], 111 | Handler: func(gui *guilib.Gui, _ *guilib.View) error { 112 | if err := gui.FocusView(detailViewName, false); err != nil { 113 | return err 114 | } 115 | return nil 116 | }, 117 | Mod: gocui.ModNone, 118 | }, 119 | }), 120 | } 121 | 122 | Detail = &guilib.View{ 123 | Name: detailViewName, 124 | Wrap: true, 125 | Title: "", 126 | Clickable: true, 127 | OnRender: detailRender, 128 | Highlight: true, 129 | SelFgColor: gocui.ColorGreen, 130 | DimensionFunc: func(gui *guilib.Gui, view *guilib.View) (int, int, int, int) { 131 | return leftSideWidth(gui.MaxWidth()) + 1, 2, gui.MaxWidth() - 1, gui.MaxHeight() - 2 132 | }, 133 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 134 | editResourceAction, 135 | copySelectedLine, 136 | { 137 | Keys: keyMap[detailToNavigation], 138 | Name: detailToNavigation, 139 | Handler: func(gui *guilib.Gui, view *guilib.View) error { 140 | return gui.FocusView(navigationViewName, false) 141 | }, 142 | Mod: gocui.ModNone, 143 | }, 144 | { 145 | Name: detailArrowUp, 146 | Keys: keyMap[detailArrowUp], 147 | Handler: func(gui *guilib.Gui, view *guilib.View) error { 148 | _, oy := view.Origin() 149 | if oy == 0 { 150 | err := gui.FocusView(navigationViewName, false) 151 | if err != nil { 152 | return err 153 | } 154 | } 155 | return scrollUpHandler(gui, view) 156 | }, 157 | Mod: gocui.ModNone, 158 | }, 159 | { 160 | Keys: keyMap[detailArrowDown], 161 | Name: detailArrowDown, 162 | Handler: scrollDownHandler, 163 | Mod: gocui.ModNone, 164 | }, 165 | changePodLogsContainerAction, 166 | tailLogsAction, 167 | scrollLogsAction, 168 | newMoreActions(moreActionsMap[detailViewName]), 169 | }), 170 | } 171 | 172 | Namespace = &guilib.View{ 173 | Name: namespaceViewName, 174 | Title: "Namespaces", 175 | ZIndex: zIndexOfFunctionView(deploymentViewName), 176 | Clickable: true, 177 | OnRender: namespaceRender, 178 | OnSelectedLineChange: func(gui *guilib.Gui, view *guilib.View, selectedLine string) error { 179 | if !kubecli.Cli.HasNamespacePermission(context.Background()) { 180 | return nil 181 | } 182 | 183 | formatted := formatResourceName(selectedLine, 0) 184 | if notResourceSelected(formatted) { 185 | formatted = "" 186 | } 187 | 188 | _, err := view.GetState(iniDefaultNamespaceKey) 189 | if err != nil { 190 | if errors.Is(err, guilib.StateKeyError) { 191 | ns := kubecli.Cli.Namespace() 192 | if err := view.SetState(iniDefaultNamespaceKey, ns, false); err != nil { 193 | return err 194 | } 195 | return nil 196 | } 197 | return err 198 | } 199 | 200 | if formatted == "" { 201 | switchNamespace(gui, "") 202 | return nil 203 | } 204 | switchNamespace(gui, formatSelectedNamespace(selectedLine)) 205 | return nil 206 | }, 207 | Highlight: true, 208 | SelFgColor: gocui.ColorGreen, 209 | OnFocus: func(gui *guilib.Gui, view *guilib.View) error { 210 | if err := onFocusClearSelected(gui, view); err != nil { 211 | return err 212 | } 213 | return nil 214 | }, 215 | FgColor: gocui.ColorDefault, 216 | DimensionFunc: guilib.BeneathView( 217 | aboveViewNameFunc, 218 | reactiveHeight, 219 | migrateTopFunc, 220 | ), 221 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 222 | toNavigation, 223 | nextFunctionView, 224 | previousLine, 225 | nextLine, 226 | copySelectedLine, 227 | filterResource, 228 | editResourceAction, 229 | newMoreActions(moreActionsMap[namespaceViewName]), 230 | }), 231 | } 232 | 233 | Option = &guilib.View{ 234 | Name: optionViewName, 235 | DimensionFunc: func(gui *guilib.Gui, view *guilib.View) (int, int, int, int) { 236 | maxWidth, maxHeight := gui.Size() 237 | return 0, maxHeight - 2, maxWidth, maxHeight 238 | }, 239 | AlwaysOnTop: true, 240 | NoFrame: true, 241 | FgColor: gocui.ColorBlue, 242 | } 243 | 244 | Pod = &guilib.View{ 245 | Name: podViewName, 246 | Title: "Pods", 247 | ZIndex: zIndexOfFunctionView(deploymentViewName), 248 | Clickable: true, 249 | OnRender: namespaceResourceListRender("pods"), 250 | OnSelectedLineChange: viewSelectedLineChangeHandler, 251 | Highlight: true, 252 | SelFgColor: gocui.ColorGreen, 253 | OnFocus: func(gui *guilib.Gui, view *guilib.View) error { 254 | if err := onFocusClearSelected(gui, view); err != nil { 255 | return err 256 | } 257 | return nil 258 | }, 259 | FgColor: gocui.ColorDefault, 260 | DimensionFunc: guilib.BeneathView( 261 | aboveViewNameFunc, 262 | reactiveHeight, 263 | migrateTopFunc, 264 | ), 265 | LowerRightPointXFunc: func(gui *guilib.Gui, view *guilib.View) int { 266 | if resizeableViews[len(resizeableViews)-1] == view.Name { 267 | return leftSideWidth(gui.MaxWidth()) 268 | } 269 | 270 | _, _, x1, _ := view.DimensionFunc(gui, view) 271 | return x1 272 | }, 273 | LowerRightPointYFunc: func(gui *guilib.Gui, view *guilib.View) int { 274 | _, y0, _, y1 := view.DimensionFunc(gui, view) 275 | 276 | if resizeableViews[len(resizeableViews)-1] == view.Name { 277 | height := gui.MaxHeight() - 2 278 | if height < y0+1 { 279 | return y0 + 1 280 | } 281 | 282 | return height 283 | } 284 | return y1 285 | }, 286 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 287 | toNavigation, 288 | nextFunctionView, 289 | previousLine, 290 | nextLine, 291 | copySelectedLine, 292 | filterResource, 293 | editResourceAction, 294 | containerExecCommandAction, 295 | runPodAction, 296 | newMoreActions(moreActionsMap[podViewName]), 297 | }), 298 | } 299 | 300 | Service = &guilib.View{ 301 | Name: serviceViewName, 302 | Title: "Services", 303 | ZIndex: zIndexOfFunctionView(deploymentViewName), 304 | Clickable: true, 305 | OnRender: resourceListRender, 306 | OnSelectedLineChange: viewSelectedLineChangeHandler, 307 | Highlight: true, 308 | SelFgColor: gocui.ColorGreen, 309 | OnFocus: func(gui *guilib.Gui, view *guilib.View) error { 310 | if err := onFocusClearSelected(gui, view); err != nil { 311 | return err 312 | } 313 | return nil 314 | }, 315 | DimensionFunc: guilib.BeneathView( 316 | aboveViewNameFunc, 317 | reactiveHeight, 318 | migrateTopFunc, 319 | ), 320 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 321 | toNavigation, 322 | nextFunctionView, 323 | previousLine, 324 | nextLine, 325 | copySelectedLine, 326 | filterResource, 327 | editResourceAction, 328 | newMoreActions(moreActionsMap[namespaceViewName]), 329 | }), 330 | } 331 | 332 | viewNameResourceMap = map[string]string{ 333 | namespaceViewName: namespaceResource, 334 | serviceViewName: serviceResource, 335 | deploymentViewName: deploymentResource, 336 | podViewName: podResource, 337 | } 338 | 339 | restartableResource = []string{"deployments", "statefulsets", "daemonsets"} 340 | 341 | logAbleResource = []string{"deployment", "statefulset", "daemonset", "service", "pod"} 342 | ) 343 | 344 | func getViewResourceName(viewName string) string { 345 | return viewNameResourceMap[viewName] 346 | } 347 | 348 | func newCustomResourcePanel(resource string) *guilib.View { 349 | viewName := resourceViewName(resource) 350 | customResourcePanel := &guilib.View{ 351 | Name: resourceViewName(resource), 352 | Title: resourceViewTitle(resource), 353 | ZIndex: zIndexOfFunctionView(viewName), 354 | Clickable: true, 355 | OnRender: resourceListRender, 356 | OnSelectedLineChange: viewSelectedLineChangeHandler, 357 | Highlight: true, 358 | SelFgColor: gocui.ColorGreen, 359 | OnFocus: func(gui *guilib.Gui, view *guilib.View) error { 360 | if err := onFocusClearSelected(gui, view); err != nil { 361 | return err 362 | } 363 | return nil 364 | }, 365 | DimensionFunc: guilib.BeneathView( 366 | aboveViewNameFunc, 367 | reactiveHeight, 368 | migrateTopFunc, 369 | ), 370 | LowerRightPointXFunc: func(gui *guilib.Gui, view *guilib.View) int { 371 | if resizeableViews[len(resizeableViews)-1] == view.Name { 372 | return leftSideWidth(gui.MaxWidth()) 373 | } 374 | 375 | _, _, x1, _ := view.DimensionFunc(gui, view) 376 | return x1 377 | }, 378 | LowerRightPointYFunc: func(gui *guilib.Gui, view *guilib.View) int { 379 | _, y0, _, y1 := view.DimensionFunc(gui, view) 380 | 381 | if resizeableViews[len(resizeableViews)-1] == view.Name { 382 | height := gui.MaxHeight() - 2 383 | if height < y0+1 { 384 | return y0 + 1 385 | } 386 | 387 | return height 388 | } 389 | return y1 390 | }, 391 | Actions: guilib.ToActionInterfaceArr([]*guilib.Action{ 392 | toNavigation, 393 | nextFunctionView, 394 | previousLine, 395 | nextLine, 396 | filterResource, 397 | editResourceAction, 398 | }), 399 | } 400 | 401 | customPanelMoreActions := []*moreAction{ 402 | // initialization loop 403 | //addCustomResourcePanelMoreAction, 404 | editResourceMoreAction, 405 | deleteCustomResourcePanelMoreAction, 406 | } 407 | if resourceRestartable(resource) { 408 | customPanelMoreActions = append( 409 | customPanelMoreActions, 410 | &moreAction{ 411 | NeedSelectResource: true, 412 | Action: *newConfirmDialogAction(customResourcePanel.Name, rolloutRestartAction), 413 | }, 414 | ) 415 | } 416 | 417 | customResourcePanel.Actions = append(customResourcePanel.Actions, newMoreActions(customPanelMoreActions)) 418 | return customResourcePanel 419 | } 420 | 421 | func addCustomResourcePanel(gui *guilib.Gui, resource string) error { 422 | var customResourcePanel *guilib.View 423 | customResourcePanel, _ = gui.GetView(resourceViewName(resource)) 424 | if customResourcePanel != nil { 425 | return nil 426 | } 427 | 428 | customResourcePanel = newCustomResourcePanel(resource) 429 | viewNameResourceMap[customResourcePanel.Name] = resource 430 | 431 | // Add to function views and resizeable views. 432 | functionViews = append(functionViews, customResourcePanel.Name) 433 | resizeableViews = append(resizeableViews, customResourcePanel.Name) 434 | 435 | // Add custom panel navigation. 436 | viewNavigationMap[customResourcePanel.Name] = []string{navigationOptConfig, navigationOptDescribe} 437 | detailRenderMap[navigationPath(customResourcePanel.Name, navigationOptConfig)] = clearBeforeRender(configRender) 438 | detailRenderMap[navigationPath(customResourcePanel.Name, navigationOptDescribe)] = reRenderInterval(clearBeforeRender(describeRender), reRenderIntervalDuration) 439 | 440 | // Add pods and pods log navigation 441 | if resourceRestartable(resource) { 442 | detailRenderMap[navigationPath(customResourcePanel.Name, navigationOptPods)] = reRenderInterval(clearBeforeRender(labelsPodsRender), reRenderIntervalDuration) 443 | detailRenderMap[navigationPath(customResourcePanel.Name, navigationOptPodsLog)] = reRenderInterval(podsLogsRender, reRenderIntervalDuration) 444 | detailRenderMap[navigationPath(customResourcePanel.Name, navigationOptTopPods)] = reRenderInterval(clearBeforeRender(topPodsRender), reRenderIntervalDuration) 445 | viewNavigationMap[customResourcePanel.Name] = append(viewNavigationMap[customResourcePanel.Name], navigationOptPods, navigationOptPodsLog, navigationOptTopPods) 446 | } 447 | 448 | // Add namespace navigation options. 449 | viewNavigationMap[namespaceViewName] = append(viewNavigationMap[namespaceViewName], customResourcePanel.Title) 450 | detailRenderMap[navigationPath(namespaceViewName, customResourcePanel.Title)] = reRenderInterval( 451 | clearBeforeRender(namespaceResourceListRender(resource)), 452 | reRenderIntervalDuration, 453 | ) 454 | 455 | if err := resizePanelHeight(gui); err != nil { 456 | return err 457 | } 458 | if err := gui.AddView(customResourcePanel); err != nil { 459 | return err 460 | } 461 | 462 | if err := gui.FocusView(customResourcePanel.Name, false); err != nil { 463 | return err 464 | } 465 | config.Conf.UserConfig.AddCustomResourcePanels(resource) 466 | config.Save() 467 | return nil 468 | } 469 | 470 | func deleteCustomResourcePanel(gui *guilib.Gui, viewName string) error { 471 | var customResourcePanel *guilib.View 472 | customResourcePanel, _ = gui.GetView(viewName) 473 | if customResourcePanel == nil { 474 | return nil 475 | } 476 | 477 | for index, eachViewName := range functionViews { 478 | if eachViewName == viewName { 479 | functionViews = append(functionViews[:index], functionViews[index+1:]...) 480 | } 481 | } 482 | 483 | for index, eachViewName := range resizeableViews { 484 | if eachViewName == viewName { 485 | resizeableViews = append(resizeableViews[:index], resizeableViews[index+1:]...) 486 | } 487 | } 488 | 489 | // Delete navigation options of namespace panel 490 | for index, option := range viewNavigationMap[namespaceViewName] { 491 | if option == customResourcePanel.Title { 492 | viewNavigationMap[namespaceViewName] = append( 493 | viewNavigationMap[namespaceViewName][:index], 494 | viewNavigationMap[namespaceViewName][index+1:]..., 495 | ) 496 | } 497 | } 498 | 499 | if err := resizePanelHeight(gui); err != nil { 500 | return err 501 | } 502 | if err := gui.DeleteView(customResourcePanel.Name); err != nil { 503 | return err 504 | } 505 | if err := gui.FocusView(functionViews[0], false); err != nil { 506 | return err 507 | } 508 | config.Conf.UserConfig.DeleteCustomResourcePanels(getViewResourceName(customResourcePanel.Name)) 509 | config.Save() 510 | return nil 511 | } 512 | 513 | func resourceViewName(resource string) string { 514 | gvk := kubecli.Cli.GetResourceGroupVersionKind(resource) 515 | return strings.ToLower(gvk.Kind) 516 | } 517 | 518 | func resourceViewTitle(resource string) string { 519 | gvk := kubecli.Cli.GetResourceGroupVersionKind(resource) 520 | return strings.Join(camelcase.Split(gvk.Kind), " ") 521 | } 522 | 523 | func zIndexOfFunctionView(viewName string) int { 524 | i := 0 525 | for i < len(functionViews) { 526 | if functionViews[i] == viewName { 527 | return i 528 | } 529 | i++ 530 | } 531 | return i 532 | } 533 | 534 | func resourceRestartable(resource string) bool { 535 | for _, restartable := range restartableResource { 536 | if resource == restartable { 537 | return true 538 | } 539 | } 540 | return false 541 | } 542 | 543 | func resourceLogAble(resource string) bool { 544 | for _, logAble := range logAbleResource { 545 | if resource == logAble { 546 | return true 547 | } 548 | } 549 | return false 550 | } 551 | -------------------------------------------------------------------------------- /pkg/app/render.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | guilib "github.com/TNK-Studio/lazykube/pkg/gui" 7 | "github.com/TNK-Studio/lazykube/pkg/kubecli" 8 | "github.com/TNK-Studio/lazykube/pkg/log" 9 | "github.com/TNK-Studio/lazykube/pkg/utils" 10 | "github.com/gookit/color" 11 | "io" 12 | "k8s.io/cli-runtime/pkg/genericclioptions" 13 | "os" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | const ( 19 | optSeparator = " " 20 | navigationPathJoin = " + " 21 | logsTail = "500" 22 | 23 | namespaceResource = "namespace" 24 | serviceResource = "service" 25 | deploymentResource = "deployment" 26 | podResource = "pod" 27 | 28 | reRenderIntervalDuration = 3 * time.Second 29 | ) 30 | 31 | var ( 32 | // Todo: use state to control. 33 | activeView *guilib.View 34 | 35 | navigationIndex int 36 | activeNavigationOpt string 37 | 38 | navigationOptNodes = "Nodes" 39 | navigationOptTopNodes = "Top Nodes" 40 | navigationOptDeployments = "Deployments" 41 | navigationOptPods = "Pods" 42 | navigationOptPodsLog = "Pods Log" 43 | navigationOptTopPods = "Top Pods" 44 | navigationOptServices = "Services" 45 | navigationOptConfig = "Config" 46 | navigationOptDescribe = "Describe" 47 | navigationOptTop = "Top" 48 | navigationOptLog = "Log" 49 | 50 | viewNavigationMap = map[string][]string{ 51 | clusterInfoViewName: {navigationOptNodes, navigationOptTopNodes}, 52 | namespaceViewName: {navigationOptConfig, navigationOptServices, navigationOptDeployments, navigationOptPods}, 53 | serviceViewName: {navigationOptConfig, navigationOptPods, navigationOptPodsLog, navigationOptTopPods}, 54 | deploymentViewName: {navigationOptConfig, navigationOptDescribe, navigationOptPods, navigationOptPodsLog, navigationOptTopPods}, 55 | podViewName: {navigationOptLog, navigationOptConfig, navigationOptDescribe, navigationOptTop}, 56 | } 57 | 58 | detailRenderMap = map[string]guilib.ViewHandler{ 59 | navigationPath(clusterInfoViewName, navigationOptNodes): reRenderInterval(clearBeforeRender(clusterNodesRender), reRenderIntervalDuration), 60 | navigationPath(clusterInfoViewName, navigationOptTopNodes): reRenderInterval(clearBeforeRender(topNodesRender), reRenderIntervalDuration), 61 | navigationPath(namespaceViewName, navigationOptDeployments): reRenderInterval(clearBeforeRender(namespaceResourceListRender("deployments")), reRenderIntervalDuration), 62 | navigationPath(namespaceViewName, navigationOptPods): reRenderInterval(clearBeforeRender(namespaceResourceListRender("pods")), reRenderIntervalDuration), 63 | navigationPath(namespaceViewName, navigationOptServices): reRenderInterval(clearBeforeRender(namespaceResourceListRender("services")), reRenderIntervalDuration), 64 | navigationPath(namespaceViewName, navigationOptConfig): reRenderInterval(clearBeforeRender(configRender), reRenderIntervalDuration), 65 | navigationPath(serviceViewName, navigationOptConfig): reRenderInterval(clearBeforeRender(configRender), reRenderIntervalDuration), 66 | navigationPath(serviceViewName, navigationOptPods): reRenderInterval(clearBeforeRender(labelsPodsRender), reRenderIntervalDuration), 67 | navigationPath(serviceViewName, navigationOptPodsLog): reRenderInterval(podsLogsRender, reRenderIntervalDuration), 68 | navigationPath(serviceViewName, navigationOptTopPods): reRenderInterval(clearBeforeRender(topPodsRender), reRenderIntervalDuration), 69 | navigationPath(deploymentViewName, navigationOptConfig): reRenderInterval(clearBeforeRender(configRender), reRenderIntervalDuration), 70 | navigationPath(deploymentViewName, navigationOptPods): reRenderInterval(clearBeforeRender(labelsPodsRender), reRenderIntervalDuration), 71 | navigationPath(deploymentViewName, navigationOptDescribe): reRenderInterval(clearBeforeRender(describeRender), reRenderIntervalDuration), 72 | navigationPath(deploymentViewName, navigationOptPodsLog): reRenderInterval(podsLogsRender, reRenderIntervalDuration), 73 | navigationPath(deploymentViewName, navigationOptTopPods): reRenderInterval(clearBeforeRender(topPodsRender), reRenderIntervalDuration), 74 | navigationPath(podViewName, navigationOptConfig): reRenderInterval(clearBeforeRender(configRender), reRenderIntervalDuration), 75 | navigationPath(podViewName, navigationOptLog): reRenderInterval(podLogsRender, reRenderIntervalDuration), 76 | navigationPath(podViewName, navigationOptDescribe): reRenderInterval(clearBeforeRender(describeRender), reRenderIntervalDuration), 77 | navigationPath(podViewName, navigationOptTop): reRenderInterval(podMetricsPlotRender, reRenderIntervalDuration), 78 | } 79 | ) 80 | 81 | func notResourceSelected(selectedName string) bool { 82 | if selectedName == "" { 83 | return true 84 | } 85 | return false 86 | } 87 | 88 | func clearBeforeRender(render guilib.ViewHandler) guilib.ViewHandler { 89 | return func(gui *guilib.Gui, view *guilib.View) error { 90 | view.Clear() 91 | return render(gui, view) 92 | } 93 | } 94 | 95 | func reRenderInterval(handler guilib.ViewHandler, interval time.Duration) guilib.ViewHandler { 96 | return func(gui *guilib.Gui, view *guilib.View) error { 97 | now := time.Now() 98 | view.ReRender() 99 | val, _ := view.GetState(viewLastRenderTimeStateKey) 100 | if val == nil { 101 | if err := view.SetState(viewLastRenderTimeStateKey, now, true); err != nil { 102 | return err 103 | } 104 | log.Logger.Debugf("reRenderInterval - interval: %+v handler %+v view %s", interval, handler, view.Name) 105 | if err := handler(gui, view); err != nil { 106 | return nil 107 | } 108 | return nil 109 | } 110 | 111 | viewLastRenderTime := val.(time.Time) 112 | if viewLastRenderTime.Add(interval).After(now) { 113 | return nil 114 | } 115 | if err := view.SetState(viewLastRenderTimeStateKey, now, true); err != nil { 116 | return err 117 | } 118 | log.Logger.Debugf("reRenderInterval - interval: %+v handler %+v view %s", interval, handler, view.Name) 119 | if err := handler(gui, view); err != nil { 120 | return nil 121 | } 122 | return nil 123 | } 124 | } 125 | 126 | func clearLastRenderTime(gui *guilib.Gui, viewName string) error { 127 | view, err := gui.GetView(viewName) 128 | if err != nil { 129 | return err 130 | } 131 | if err := view.SetState(viewLastRenderTimeStateKey, nil, true); err != nil { 132 | return err 133 | } 134 | return nil 135 | } 136 | 137 | func clearDetailViewState(gui *guilib.Gui) { 138 | detailView, err := gui.GetView(detailViewName) 139 | if err != nil { 140 | log.Logger.Warningf("clearDetailViewState - get view error %s", err) 141 | return 142 | } 143 | 144 | if err := clearLastRenderTime(gui, detailViewName); err != nil { 145 | log.Logger.Warningf("clearDetailViewState - clearLastRenderTime err %s", err) 146 | return 147 | } 148 | 149 | if err := detailView.SetState(logSinceTimeStateKey, nil, true); err != nil { 150 | log.Logger.Warningf("clearDetailViewState - clear logSinceTimeStateKey err %s", err) 151 | return 152 | } 153 | 154 | if err := detailView.SetState(logContainerStateKey, nil, true); err != nil { 155 | log.Logger.Warningf("clearDetailViewState - clear logContainerStateKey err %s", err) 156 | return 157 | } 158 | _ = detailView.SetOrigin(0, 0) 159 | _ = detailView.SetCursor(0, 0) 160 | detailView.Clear() 161 | } 162 | 163 | func navigationPath(args ...string) string { 164 | return strings.Join(args, navigationPathJoin) 165 | } 166 | 167 | func switchNavigation(gui *guilib.Gui, index int) string { 168 | err := Detail.SetOrigin(0, 0) 169 | if err != nil { 170 | log.Logger.Warningf("switchNavigation - Detail.SetOrigin(0, 0) error %s", err) 171 | } 172 | 173 | Detail.Clear() 174 | clearDetailViewState(gui) 175 | if index < 0 { 176 | return "" 177 | } 178 | 179 | if activeView != nil { 180 | if index >= len(viewNavigationMap[activeView.Name]) { 181 | return "" 182 | } 183 | navigationIndex = index 184 | activeNavigationOpt = viewNavigationMap[activeView.Name][index] 185 | return activeNavigationOpt 186 | } 187 | return "" 188 | } 189 | 190 | func navigationRender(gui *guilib.Gui, view *guilib.View) error { 191 | currentView := gui.CurrentView() 192 | // Change navigation render 193 | var changeNavigation bool 194 | if currentView != nil { 195 | for _, viewName := range functionViews { 196 | if currentView.Name == viewName { 197 | if activeView != currentView { 198 | changeNavigation = true 199 | } 200 | activeView = currentView 201 | break 202 | } 203 | } 204 | } 205 | 206 | if activeView == nil { 207 | if gui.CurrentView() == nil { 208 | if err := gui.FocusView(functionViews[0], false); err != nil { 209 | log.Logger.Println(err) 210 | } 211 | } 212 | activeView = gui.CurrentView() 213 | } 214 | 215 | options := viewNavigationMap[activeView.Name] 216 | if activeNavigationOpt == "" { 217 | activeNavigationOpt = options[navigationIndex] 218 | } 219 | if changeNavigation { 220 | switchNavigation(gui, 0) 221 | } 222 | 223 | colorfulOptions := make([]string, 0) 224 | for index, opt := range options { 225 | colorfulOpt := color.White.Sprint(opt) 226 | if navigationIndex == index { 227 | colorfulOpt = color.Green.Sprint(opt) 228 | } 229 | colorfulOptions = append(colorfulOptions, colorfulOpt) 230 | } 231 | 232 | view.Clear() 233 | str := strings.Join(colorfulOptions, optSeparator) 234 | 235 | _, err := fmt.Fprint(view, str) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | return nil 241 | } 242 | 243 | func navigationOnClick(gui *guilib.Gui, view *guilib.View) error { 244 | cx, cy := view.Cursor() 245 | log.Logger.Debugf("navigationOnClick - cx %d cy %d", cx, cy) 246 | 247 | options := viewNavigationMap[activeView.Name] 248 | optionIndex, selected := utils.ClickOption(options, optSeparator, cx, 0) 249 | if optionIndex < 0 { 250 | return nil 251 | } 252 | log.Logger.Debugf("navigationOnClick - cx %d selected '%s'", cx, selected) 253 | _ = switchNavigation(gui, optionIndex) 254 | view.ReRender() 255 | Detail.ReRender() 256 | return nil 257 | } 258 | 259 | func renderClusterInfo(_ *guilib.Gui, view *guilib.View) error { 260 | view.Clear() 261 | currentContext := kubecli.Cli.CurrentContext() 262 | currentNs := kubecli.Cli.Namespace() 263 | 264 | if _, err := fmt.Fprintf(view, "Current Context: %s Namespace: %s", color.Green.Sprint(currentContext), color.Green.Sprint(currentNs)); err != nil { 265 | return err 266 | } 267 | return nil 268 | } 269 | 270 | func detailRender(gui *guilib.Gui, view *guilib.View) error { 271 | if activeView == nil { 272 | return nil 273 | } 274 | renderFunc := detailRenderMap[navigationPath(activeView.Name, activeNavigationOpt)] 275 | if renderFunc != nil { 276 | return renderFunc(gui, view) 277 | } 278 | return nil 279 | } 280 | 281 | func viewStreams(view *guilib.View) genericclioptions.IOStreams { 282 | return genericclioptions.IOStreams{ 283 | In: os.Stdin, 284 | Out: view, 285 | ErrOut: view, 286 | } 287 | } 288 | 289 | func clusterNodesRender(_ *guilib.Gui, view *guilib.View) error { 290 | kubecli.Cli.Get(viewStreams(view), navigationOptNodes).Run() 291 | return nil 292 | } 293 | 294 | func topNodesRender(_ *guilib.Gui, view *guilib.View) error { 295 | kubecli.Cli.TopNode(viewStreams(view), nil, "").Run() 296 | view.ReRender() 297 | return nil 298 | } 299 | 300 | func namespaceRender(_ *guilib.Gui, view *guilib.View) error { 301 | view.Clear() 302 | kubecli.Cli.Get(viewStreams(view), namespaceResource).Run() 303 | return nil 304 | } 305 | 306 | func namespaceResourceListRender(resource string) guilib.ViewHandler { 307 | return func(gui *guilib.Gui, view *guilib.View) error { 308 | view.Clear() 309 | if kubecli.Cli.Namespace() == "" { 310 | kubecli.Cli.Get(viewStreams(view), resource).SetFlag("all-namespaces", "true").SetFlag("output", "wide").Run() 311 | return nil 312 | } 313 | kubecli.Cli.Get(viewStreams(view), resource).SetFlag("output", "wide").Run() 314 | return nil 315 | } 316 | } 317 | 318 | func resourceListRender(_ *guilib.Gui, view *guilib.View) error { 319 | view.Clear() 320 | resource := getViewResourceName(view.Name) 321 | if kubecli.Cli.Namespace() == "" { 322 | kubecli.Cli.Get(viewStreams(view), resource).SetFlag("all-namespaces", "true").Run() 323 | return nil 324 | } 325 | kubecli.Cli.Get(viewStreams(view), resource).Run() 326 | return nil 327 | } 328 | 329 | func showPleaseSelected(view io.Writer, name string) { 330 | _, err := fmt.Fprintf(view, "Please select a %s.\n ", name) 331 | if err != nil { 332 | log.Logger.Warningf("showPleaseSelected - error %s", err) 333 | } 334 | } 335 | 336 | func namespaceConfigRender(gui *guilib.Gui, view *guilib.View) error { 337 | view.Clear() 338 | namespaceView, err := gui.GetView(namespaceViewName) 339 | if err != nil { 340 | return nil 341 | } 342 | namespace := formatSelectedNamespace(namespaceView.SelectedLine) 343 | if notResourceSelected(namespace) { 344 | showPleaseSelected(view, namespaceViewName) 345 | return nil 346 | } 347 | 348 | kubecli.Cli.Get(viewStreams(view), "namespaces", namespace).SetFlag("output", "yaml").Run() 349 | return nil 350 | } 351 | 352 | func configRender(gui *guilib.Gui, view *guilib.View) error { 353 | view.Clear() 354 | if activeView == nil { 355 | return nil 356 | } 357 | 358 | namespaceView, err := gui.GetView(namespaceViewName) 359 | if err != nil { 360 | return nil 361 | } 362 | 363 | if activeView == namespaceView { 364 | return namespaceConfigRender(gui, view) 365 | } 366 | 367 | resource := getViewResourceName(activeView.Name) 368 | 369 | if resource == "" { 370 | return nil 371 | } 372 | 373 | namespace, resourceName, err := getResourceNamespaceAndName(gui, activeView) 374 | if err != nil { 375 | if errors.Is(err, noResourceSelectedErr) { 376 | showPleaseSelected(view, resource) 377 | return nil 378 | } 379 | return err 380 | } 381 | 382 | cli(namespace).Get(viewStreams(view), resource, resourceName).SetFlag("output", "yaml").Run() 383 | return nil 384 | } 385 | 386 | func describeRender(gui *guilib.Gui, view *guilib.View) error { 387 | view.Clear() 388 | if activeView == nil { 389 | return nil 390 | } 391 | if activeView.Name == namespaceViewName { 392 | return namespaceConfigRender(gui, view) 393 | } 394 | 395 | resource := getViewResourceName(activeView.Name) 396 | 397 | if resource == "" { 398 | return nil 399 | } 400 | 401 | namespace, resourceName, err := getResourceNamespaceAndName(gui, activeView) 402 | if err != nil { 403 | if errors.Is(err, noResourceSelectedErr) { 404 | showPleaseSelected(view, resource) 405 | return nil 406 | } 407 | 408 | return err 409 | } 410 | 411 | cli(namespace).Describe(viewStreams(view), resource, resourceName).Run() 412 | 413 | view.ReRender() 414 | return nil 415 | } 416 | 417 | func onFocusClearSelected(gui *guilib.Gui, view *guilib.View) error { 418 | for _, functionViewName := range functionViews { 419 | if functionViewName == view.Name || functionViewName == namespaceViewName { 420 | continue 421 | } 422 | functionView, err := gui.GetView(functionViewName) 423 | if err != nil { 424 | log.Logger.Warningf("onFocusClearSelected - view name %s gui.GetView(\"%s\") error %s", view.Name, functionView, err) 425 | continue 426 | } 427 | if err := functionView.SetOrigin(0, 0); err != nil { 428 | return err 429 | } 430 | if err := functionView.SetCursor(0, 0); err != nil { 431 | return err 432 | } 433 | } 434 | return nil 435 | } 436 | 437 | func podLogsRender(gui *guilib.Gui, view *guilib.View) error { 438 | // Todo: Fix chinese character of logs. 439 | scrollLogs := true 440 | if val, _ := view.GetState(ScrollingLogsStateKey); val != nil { 441 | var ok bool 442 | scrollLogs, ok = val.(bool) 443 | if !ok { 444 | scrollLogs = true 445 | } 446 | } 447 | 448 | if !scrollLogs { 449 | return nil 450 | } 451 | 452 | podView, err := gui.GetView(podViewName) 453 | if err != nil { 454 | return err 455 | } 456 | 457 | resource := "pod" 458 | namespace, resourceName, err := getResourceNamespaceAndName(gui, podView) 459 | if err != nil { 460 | if errors.Is(err, noResourceSelectedErr) { 461 | showPleaseSelected(view, resource) 462 | return nil 463 | } 464 | return err 465 | } 466 | 467 | containers := getPodContainers(namespace, resourceName) 468 | 469 | if err := view.SetState(podContainersStateKey, containers, true); err != nil { 470 | return err 471 | } 472 | 473 | var since time.Time 474 | var hasSince bool 475 | if val, _ := view.GetState(logSinceTimeStateKey); val != nil { 476 | hasSince = true 477 | since = val.(time.Time) 478 | } 479 | 480 | var logContainer string 481 | if val, _ := view.GetState(logContainerStateKey); val != nil { 482 | logContainer = val.(string) 483 | } 484 | 485 | cmd := cli(namespace). 486 | Logs(viewStreams(view), resourceName). 487 | SetFlag("tail", logsTail). 488 | SetFlag("prefix", "true") 489 | 490 | if logContainer == "" { 491 | cmd.SetFlag("all-containers", "true") 492 | } else { 493 | cmd.SetFlag("container", logContainer) 494 | } 495 | 496 | if hasSince { 497 | cmd.SetFlag("since-time", since.Format(time.RFC3339)) 498 | } 499 | 500 | cmd.Run() 501 | 502 | if err := view.SetState(logSinceTimeStateKey, time.Now(), true); err != nil { 503 | return err 504 | } 505 | 506 | view.ReRender() 507 | return nil 508 | } 509 | func podsLogsRender(gui *guilib.Gui, view *guilib.View) error { 510 | // Todo: Fix chinese character of logs. 511 | if err := podsSelectorRenderHelper(func(namespace string, labelsArr []string) error { 512 | var since time.Time 513 | var hasSince bool 514 | val, _ := view.GetState(logSinceTimeStateKey) 515 | if val != nil { 516 | hasSince = true 517 | since = val.(time.Time) 518 | } 519 | 520 | streams := newStream() 521 | cmd := kubecli.Cli.WithNamespace(namespace).Logs(streams) 522 | cmd.SetFlag("selector", strings.Join(labelsArr, ",")). 523 | SetFlag("all-containers", "true"). 524 | SetFlag("tail", logsTail). 525 | SetFlag("prefix", "true") 526 | 527 | if hasSince { 528 | cmd.SetFlag("since-time", since.Format(time.RFC3339)) 529 | } 530 | 531 | cmd.Run() 532 | 533 | if err := view.SetState(logSinceTimeStateKey, time.Now(), true); err != nil { 534 | return err 535 | } 536 | streamCopyTo(streams, view) 537 | view.ReRender() 538 | return nil 539 | })(gui, view); err != nil { 540 | return err 541 | } 542 | return nil 543 | } 544 | 545 | func labelsPodsRender(gui *guilib.Gui, view *guilib.View) error { 546 | view.Clear() 547 | if err := podsSelectorRenderHelper(func(namespace string, labelsArr []string) error { 548 | cmd := kubecli.Cli.WithNamespace(namespace).Get(viewStreams(view), "pods") 549 | cmd.SetFlag("selector", strings.Join(labelsArr, ",")) 550 | cmd.SetFlag("output", "wide") 551 | cmd.Run() 552 | view.ReRender() 553 | return nil 554 | })(gui, view); err != nil { 555 | return err 556 | } 557 | return nil 558 | } 559 | 560 | func topPodsRender(gui *guilib.Gui, view *guilib.View) error { 561 | view.Clear() 562 | if err := podsSelectorRenderHelper(func(namespace string, labelsArr []string) error { 563 | cmd := kubecli.Cli.WithNamespace(namespace).TopPod(viewStreams(view), nil) 564 | cmd.SetFlag("selector", strings.Join(labelsArr, ",")) 565 | cmd.Run() 566 | view.ReRender() 567 | return nil 568 | })(gui, view); err != nil { 569 | return err 570 | } 571 | return nil 572 | } 573 | 574 | //nolint:funlen 575 | //nolint:funlen 576 | //nolint:funlen 577 | //nolint:funlen 578 | //nolint:funlen 579 | //nolint:funlen 580 | func podsSelectorRenderHelper(cmdFunc func(namespace string, labelsArr []string) error) func(gui *guilib.Gui, view *guilib.View) error { 581 | return func(gui *guilib.Gui, view *guilib.View) error { 582 | if activeView == nil { 583 | return nil 584 | } 585 | if activeView.Name == namespaceViewName { 586 | return namespaceConfigRender(gui, view) 587 | } 588 | selected := activeView.SelectedLine 589 | resource := getViewResourceName(activeView.Name) 590 | if resource == "" { 591 | return nil 592 | } 593 | 594 | jsonPath := resourceLabelSelectorJSONPath(resource) 595 | if jsonPath == "" { 596 | return nil 597 | } 598 | 599 | if notResourceSelected(selected) { 600 | showPleaseSelected(view, resource) 601 | return nil 602 | } 603 | 604 | namespace, resourceName, err := getResourceNamespaceAndName(gui, activeView) 605 | if err != nil { 606 | if errors.Is(err, noResourceSelectedErr) { 607 | showPleaseSelected(view, resource) 608 | return nil 609 | } 610 | return err 611 | } 612 | 613 | output := newStream() 614 | cli(namespace).Get(output, resource, resourceName).SetFlag("output", jsonPath).Run() 615 | 616 | var labelJSON = streamToString(output) 617 | if labelJSON == "" { 618 | _, err := fmt.Fprint(view, "Pods not found.") 619 | if err != nil { 620 | return err 621 | } 622 | 623 | return nil 624 | } 625 | labelsArr := utils.LabelsToStringArr(labelJSON[1 : len(labelJSON)-1]) 626 | if len(labelsArr) == 0 { 627 | showPleaseSelected(view, resource) 628 | return nil 629 | } 630 | 631 | if err := cmdFunc(namespace, labelsArr); err != nil { 632 | return err 633 | } 634 | return nil 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /pkg/app/render_plot.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | guilib "github.com/TNK-Studio/lazykube/pkg/gui" 7 | "github.com/TNK-Studio/lazykube/pkg/kubecli" 8 | "github.com/TNK-Studio/lazykube/pkg/log" 9 | "github.com/TNK-Studio/lazykube/pkg/utils" 10 | "github.com/gookit/color" 11 | v1 "k8s.io/api/core/v1" 12 | "math" 13 | "time" 14 | ) 15 | 16 | func podMetricsPlotRender(gui *guilib.Gui, view *guilib.View) error { 17 | view.ReRender() 18 | //if !canRenderPlot(gui, view) { 19 | // return nil 20 | //} 21 | view.Clear() 22 | var err error 23 | 24 | podView, err := gui.GetView(podViewName) 25 | if err != nil { 26 | return err 27 | } 28 | resource := "pod" 29 | namespace, resourceName, err := getResourceNamespaceAndName( 30 | gui, 31 | podView, 32 | ) 33 | if err != nil { 34 | if errors.Is(err, noResourceSelectedErr) { 35 | showPleaseSelected(view, resource) 36 | return nil 37 | } 38 | return err 39 | } 40 | 41 | fmt.Fprintln(view) 42 | cpuPlot := getPlot( 43 | gui, 44 | view, 45 | cpuPlotStateKey, 46 | "CPU: %0.0fm (%v)", 47 | namespace, 48 | resourceName, 49 | func() []float64 { 50 | data := make([]float64, 0) 51 | metrics, err := kubecli.Cli.GetPodMetrics(namespace, resourceName, false, nil) 52 | if err != nil { 53 | log.Logger.Warningf("podMetricsDataGetter - kubecli.Cli.GetPodMetrics('%s', '%s', false, nil) error %s", namespace, resourceName, err) 54 | } 55 | if metrics == nil { 56 | return data 57 | } 58 | for _, m := range metrics { 59 | data = append(data, float64(m[v1.ResourceCPU])) 60 | } 61 | return data 62 | }, 63 | v1.ResourceCPU, 64 | color.Blue.Sprintf, 65 | ) 66 | cpuPlot.Render(view) 67 | fmt.Fprintln(view) 68 | memoryPlot := getPlot( 69 | gui, 70 | view, 71 | memoryPlotStateKey, 72 | "Memory: %0.0fMi (%v)", 73 | namespace, 74 | resourceName, 75 | func() []float64 { 76 | data := make([]float64, 0) 77 | metrics, err := kubecli.Cli.GetPodMetrics(namespace, resourceName, false, nil) 78 | if err != nil { 79 | log.Logger.Warningf("podMetricsDataGetter - kubecli.Cli.GetPodMetrics('%s', '%s', false, nil) error %s", namespace, resourceName, err) 80 | } 81 | if metrics == nil { 82 | return data 83 | } 84 | for _, m := range metrics { 85 | data = append(data, float64(m[v1.ResourceMemory])) 86 | } 87 | return data 88 | }, 89 | v1.ResourceMemory, 90 | color.Green.Sprintf, 91 | ) 92 | memoryPlot.Render(view) 93 | return nil 94 | } 95 | 96 | func getPlot(gui *guilib.Gui, view *guilib.View, plotStateKey, captionFormat, namespace, name string, dataGetter func() []float64, resourceName v1.ResourceName, colorSprintf func(format string, args ...interface{}) string) *guilib.Plot { 97 | var plot *guilib.Plot 98 | plotName := fmt.Sprintf("%s - %s", namespace, name) 99 | val, _ := view.GetState(plotStateKey) 100 | newCPUPlot := false 101 | if val == nil { 102 | newCPUPlot = true 103 | } else { 104 | plot = val.(*guilib.Plot) 105 | if plot.Name != plotName { 106 | newCPUPlot = true 107 | } 108 | } 109 | 110 | if newCPUPlot { 111 | plot = guilib.NewPlot( 112 | plotName, 113 | dataGetter, 114 | podPlotHeight(gui, view), 115 | podPlotWidth(gui, view), 116 | podPlotMax(gui, view), 117 | podPlotMin(gui, view), 118 | podPlotCaption(captionFormat), 119 | func(graph string) string { 120 | return colorSprintf(graph) 121 | }, 122 | ) 123 | view.SetState(plotStateKey, plot, true) 124 | } 125 | return plot 126 | } 127 | 128 | func podPlotHeight(gui *guilib.Gui, view *guilib.View) func(*guilib.Plot) int { 129 | return func(*guilib.Plot) int { 130 | _, MaxHeight := view.Size() 131 | 132 | height := MaxHeight/2 - 2 133 | if height <= 0 { 134 | return 0 135 | } 136 | 137 | return height 138 | } 139 | } 140 | 141 | func podPlotWidth(gui *guilib.Gui, view *guilib.View) func(*guilib.Plot) int { 142 | return func(*guilib.Plot) int { 143 | MaxWidth, _ := view.Size() 144 | return MaxWidth - 10 145 | } 146 | } 147 | 148 | func podPlotMax(gui *guilib.Gui, view *guilib.View) func(*guilib.Plot) float64 { 149 | return func(plot *guilib.Plot) float64 { 150 | return utils.MaxFloat64(plot.Data()) * 2 151 | } 152 | } 153 | 154 | func podPlotMin(gui *guilib.Gui, view *guilib.View) func(*guilib.Plot) float64 { 155 | return func(plot *guilib.Plot) float64 { 156 | return math.Min(0, utils.MinFloat64(plot.Data())) 157 | } 158 | } 159 | 160 | func podPlotCaption(format string) func(*guilib.Plot) string { 161 | return func(plot *guilib.Plot) string { 162 | length := len(plot.Data()) 163 | if length == 0 { 164 | return "No data. " 165 | } 166 | return fmt.Sprintf(format, plot.Data()[length-1], time.Since(plot.Since().Round(time.Second))) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /pkg/app/statekey.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | const ( 4 | viewLastRenderTimeStateKey = "viewLastRenderTime" // value type: time.Time 5 | cpuPlotStateKey = "cpuPlot" // value type: *gui.Plot 6 | memoryPlotStateKey = "memoryPlot" // value type: *gui.Plot 7 | moreActionTriggerViewStateKey = "triggerView" // value type: *gui.View 8 | filterInputValueStateKey = "filterInputValue" // value type: string 9 | confirmValueStateKey = "confirmValue" // value type: string 10 | logSinceTimeStateKey = "logSinceTime" // value type: time.Time 11 | ScrollingLogsStateKey = "scrollingLogs" // value type: boolean 12 | podContainersStateKey = "podContainers" // value type: []string 13 | logContainerStateKey = "logContainer" // value type: string 14 | iniDefaultNamespaceKey = "iniDefaultNamespace" // value type: string 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/app/stream.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "github.com/TNK-Studio/lazykube/pkg/log" 6 | "io" 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func newStream() genericclioptions.IOStreams { 13 | return genericclioptions.IOStreams{ 14 | In: &bytes.Buffer{}, 15 | Out: &bytes.Buffer{}, 16 | ErrOut: &bytes.Buffer{}, 17 | } 18 | } 19 | 20 | func newStdStream() genericclioptions.IOStreams { 21 | return genericclioptions.IOStreams{ 22 | In: os.Stdin, 23 | Out: os.Stdout, 24 | ErrOut: os.Stderr, 25 | } 26 | } 27 | 28 | func streamCopyTo(streams genericclioptions.IOStreams, writer io.Writer) { 29 | if _, err := io.Copy(writer, (streams.Out).(io.Reader)); err != nil { 30 | log.Logger.Warningf("streamCopyTo - streams.Out copy error %s", err) 31 | } 32 | if _, err := io.Copy(writer, (streams.ErrOut).(io.Reader)); err != nil { 33 | log.Logger.Warningf("streamCopyTo - streams.ErrOut copy error %s", err) 34 | } 35 | } 36 | 37 | func streamToString(streams genericclioptions.IOStreams) string { 38 | buf := new(strings.Builder) 39 | streamCopyTo(streams, buf) 40 | // check errors 41 | return buf.String() 42 | } 43 | -------------------------------------------------------------------------------- /pkg/app/style.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | guilib "github.com/TNK-Studio/lazykube/pkg/gui" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | resizeableViewMinHeight = 5 11 | clusterInfoViewHeight = 2 12 | optionViewHeight = 1 13 | ) 14 | 15 | var ( 16 | viewHeights = map[string]int{} 17 | functionViews = []string{clusterInfoViewName, namespaceViewName, serviceViewName, deploymentViewName, podViewName} 18 | resizeableViews = []string{namespaceViewName, serviceViewName, deploymentViewName, podViewName} 19 | 20 | // Function cache 21 | reactiveHeightCache = map[string]int{} 22 | migrateTopCache = map[string]int{} 23 | ) 24 | 25 | func aboveViewNameFunc(_ *guilib.Gui, view *guilib.View) string { 26 | for index, viewName := range functionViews { 27 | if viewName == view.Name && index != 0 { 28 | return functionViews[index-1] 29 | } 30 | } 31 | return "" 32 | } 33 | 34 | func resizeableView(viewName string) bool { 35 | for _, resizeableView := range resizeableViews { 36 | if resizeableView == viewName { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | func leftSideWidth(maxWidth int) int { 44 | return maxWidth / 3 45 | } 46 | 47 | func usableSpace(maxHeight int) int { 48 | usedSpace := clusterInfoViewHeight + optionViewHeight + (len(functionViews) - 1) 49 | if maxHeight < resizeableViewMinHeight*len(resizeableViews)-usedSpace { 50 | return maxHeight - 2 51 | } 52 | return maxHeight - usedSpace 53 | } 54 | 55 | func resizePanelHeight(gui *guilib.Gui) error { 56 | _, maxHeight := gui.Size() 57 | 58 | space := usableSpace(maxHeight) 59 | 60 | n := len(resizeableViews) 61 | viewHeights[clusterInfoViewName] = clusterInfoViewHeight 62 | for _, resizeableView := range resizeableViews { 63 | viewHeights[resizeableView] = space / n 64 | } 65 | viewHeights[optionViewName] = optionViewHeight 66 | 67 | return nil 68 | } 69 | 70 | func reactiveHeight(gui *guilib.Gui, view *guilib.View) int { 71 | var currentViewName string 72 | currentView := gui.CurrentView() 73 | if currentView != nil { 74 | currentViewName = currentView.Name 75 | } 76 | previousViewName := gui.PeekPreviousView() 77 | 78 | height := cacheAbleReactiveHeight(gui.MaxHeight(), resizeableViews, currentViewName, previousViewName, view.Name) 79 | return height 80 | } 81 | 82 | func cacheAbleReactiveHeight(maxHeight int, resizeableViews []string, currentViewName, previousViewName, viewName string) int { 83 | key := fmt.Sprintf("%d,[%s],%s,%s,%s", maxHeight, strings.Join(resizeableViews, ","), currentViewName, previousViewName, viewName) 84 | cacheVal, ok := reactiveHeightCache[key] 85 | if ok { 86 | return cacheVal 87 | } 88 | 89 | var resizeView string 90 | if currentViewName == "" { 91 | resizeView = resizeableViews[0] 92 | } else { 93 | resizeView = currentViewName 94 | } 95 | 96 | // When cluster info 、navigation or detail panel selected. 97 | if !resizeableView(resizeView) { 98 | resizeView = previousViewName 99 | 100 | // If previous view is cluster info 、navigation or detail pane. 101 | if !resizeableView(resizeView) { 102 | resizeView = resizeableViews[0] 103 | } 104 | } 105 | 106 | n := len(resizeableViews) 107 | height := viewHeights[viewName] 108 | 109 | if maxHeight < heightBoundary() && resizeableView(viewName) { 110 | if resizeView == viewName { 111 | cacheVal = height + len(resizeableViews) 112 | } else { 113 | cacheVal = 2 114 | } 115 | } else { 116 | if resizeView == viewName { 117 | height += usableSpace(maxHeight) % n 118 | } 119 | } 120 | 121 | cacheVal = height 122 | reactiveHeightCache[key] = cacheVal 123 | return cacheVal 124 | } 125 | 126 | func migrateTopFunc(gui *guilib.Gui, view *guilib.View) int { 127 | var currentViewName string 128 | currentView := gui.CurrentView() 129 | if currentView != nil { 130 | currentViewName = currentView.Name 131 | } 132 | return cacheAbleMigrateTopFunc(gui.MaxHeight(), resizeableViews, currentViewName, view.Name) 133 | } 134 | 135 | func cacheAbleMigrateTopFunc(maxHeight int, resizeableViews []string, currentViewName, viewName string) int { 136 | key := fmt.Sprintf("%d,%s,%s,%s", maxHeight, strings.Join(resizeableViews, ","), currentViewName, viewName) 137 | cacheVal, ok := migrateTopCache[key] 138 | if ok { 139 | return cacheVal 140 | } 141 | 142 | if maxHeight < heightBoundary() { 143 | if currentViewName != "" { 144 | if !resizeableView(viewName) { 145 | if viewName == resizeableViews[0] { 146 | cacheVal = 1 147 | } 148 | } else { 149 | for i, viewName := range functionViews { 150 | if currentViewName == viewName { 151 | index := i + 1 152 | if index < len(functionViews) && functionViews[index] == viewName { 153 | cacheVal = 1 154 | break 155 | } 156 | } 157 | } 158 | } 159 | } else { 160 | cacheVal = -1 161 | } 162 | } 163 | cacheVal = 1 164 | migrateTopCache[key] = cacheVal 165 | return cacheVal 166 | } 167 | 168 | func heightBoundary() int { 169 | usedSpace := clusterInfoViewHeight - optionViewHeight - (len(functionViews) - 1) 170 | boundary := resizeableViewMinHeight*len(resizeableViews) - usedSpace 171 | return boundary 172 | } 173 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/TNK-Studio/lazykube/pkg/utils" 5 | "github.com/jroimartin/gocui" 6 | "github.com/sirupsen/logrus" 7 | "gopkg.in/yaml.v3" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | ) 12 | 13 | var ( 14 | // Todo: check error 15 | HomePath, _ = utils.Home() 16 | LazykubeHomePath = path.Join(HomePath, ".lazykube/") 17 | 18 | Conf = &Config{} 19 | 20 | DefaultConfig = &Config{ 21 | GuiConfig: &GuiConfig{ 22 | Highlight: true, 23 | Cursor: false, 24 | FgColor: gocui.ColorWhite, 25 | SelFgColor: gocui.ColorGreen, 26 | Mouse: true, 27 | InputEsc: true, 28 | }, 29 | LogConfig: &LogConfig{ 30 | Path: path.Join(LazykubeHomePath, "log/"), 31 | Level: logrus.InfoLevel, 32 | }, 33 | UserConfig: &UserConfig{ 34 | CustomResourcePanels: []string{}, 35 | History: &History{ 36 | ImageHistory: []string{}, 37 | CommandHistory: []string{}, 38 | }, 39 | }, 40 | } 41 | ) 42 | 43 | func init() { 44 | Read() 45 | } 46 | 47 | func Read() { 48 | if !utils.FileExited(LazykubeHomePath) { 49 | if err := os.MkdirAll(LazykubeHomePath, 0755); err != nil { 50 | panic(err) 51 | } 52 | } 53 | 54 | if err := Conf.ReadFrom(LazykubeHomePath, "config.yaml"); err != nil { 55 | *Conf = *DefaultConfig 56 | Save() 57 | } 58 | } 59 | 60 | func Save() { 61 | if err := Conf.SaveTo(LazykubeHomePath, "config.yaml"); err != nil { 62 | panic(err) 63 | } 64 | } 65 | 66 | type Config struct { 67 | GuiConfig *GuiConfig `yaml:"gui_config"` 68 | LogConfig *LogConfig `yaml:"log_config"` 69 | UserConfig *UserConfig `yaml:"user_config"` 70 | } 71 | 72 | // ReadFrom read config 73 | func (c *Config) ReadFrom(filePath, fileName string) error { 74 | configFile, err := ioutil.ReadFile(path.Join(filePath, fileName)) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | err = yaml.Unmarshal(configFile, Conf) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // SaveTo save config 88 | func (c *Config) SaveTo(filePath, fileName string) error { 89 | bytes, err := yaml.Marshal(c) 90 | if err != nil { 91 | return err 92 | } 93 | if err := ioutil.WriteFile(path.Join(filePath, fileName), bytes, 0755); err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /pkg/config/gui.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/jroimartin/gocui" 4 | 5 | // GuiConfig GuiConfig 6 | type GuiConfig struct { 7 | Highlight bool `yaml:"highlight"` 8 | Cursor bool `yaml:"cursor"` 9 | FgColor gocui.Attribute `yaml:"fg_color"` 10 | BgColor gocui.Attribute `yaml:"bg_color"` 11 | SelBgColor gocui.Attribute `yaml:"sel_bg_color"` 12 | SelFgColor gocui.Attribute `yaml:"sel_fg_color"` 13 | Mouse bool `yaml:"mouse"` 14 | InputEsc bool `yaml:"input_esc"` 15 | } 16 | -------------------------------------------------------------------------------- /pkg/config/log.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/sirupsen/logrus" 4 | 5 | type LogConfig struct { 6 | Path string `yaml:"path"` 7 | Level logrus.Level `yaml:"level"` 8 | } 9 | -------------------------------------------------------------------------------- /pkg/config/user.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type UserConfig struct { 4 | CustomResourcePanels []string 5 | History *History `yaml:"history"` 6 | } 7 | 8 | func (c *UserConfig) AddCustomResourcePanels(resources ...string) { 9 | for _, resource := range resources { 10 | for _, each := range c.CustomResourcePanels { 11 | if each == resource { 12 | return 13 | } 14 | } 15 | c.CustomResourcePanels = append(c.CustomResourcePanels, resource) 16 | } 17 | } 18 | 19 | func (c *UserConfig) DeleteCustomResourcePanels(resources ...string) { 20 | for _, resource := range resources { 21 | for index, each := range c.CustomResourcePanels { 22 | if each == resource { 23 | c.CustomResourcePanels = append(c.CustomResourcePanels[:index], c.CustomResourcePanels[index+1:]...) 24 | } 25 | } 26 | } 27 | } 28 | 29 | type History struct { 30 | ImageHistory []string `yaml:"image_history"` 31 | CommandHistory []string `yaml:"command_history"` 32 | PodNameHistory []string `yaml:"pod_name_history"` 33 | } 34 | 35 | func (h *History) AddStringHistory(history []string, newOne string) []string { 36 | history = append([]string{newOne}, history...) 37 | for index, each := range history[1:] { 38 | if each == newOne { 39 | history = append(history[:index], history[index+1:]...) 40 | } 41 | } 42 | return history 43 | } 44 | 45 | func (h *History) AddCommandHistory(newOne string) { 46 | h.CommandHistory = h.AddStringHistory(h.CommandHistory, newOne) 47 | } 48 | 49 | func (h *History) AddImageHistory(newOne string) { 50 | h.ImageHistory = h.AddStringHistory(h.ImageHistory, newOne) 51 | } 52 | 53 | func (h *History) AddPodNameHistory(newOne string) { 54 | h.PodNameHistory = h.AddStringHistory(h.PodNameHistory, newOne) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/gui/action.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "errors" 5 | "github.com/TNK-Studio/lazykube/pkg/log" 6 | "github.com/jroimartin/gocui" 7 | ) 8 | 9 | var ( 10 | // Quit Quit 11 | Quit = &Action{ 12 | Name: "Quit", 13 | Key: gocui.KeyCtrlC, 14 | Handler: func(*Gui, *View) error { 15 | return gocui.ErrQuit 16 | }, 17 | Mod: gocui.ModNone, 18 | } 19 | 20 | // ClickView ClickView 21 | ClickView = &Action{ 22 | Name: "clickView", 23 | Key: gocui.MouseLeft, 24 | Handler: ViewClickHandler, 25 | Mod: gocui.ModNone, 26 | } 27 | ) 28 | 29 | // ActionInterface ActionInterface 30 | type ActionInterface interface { 31 | ActionName() string 32 | HandlerFunc(*Gui, *View) error 33 | Modifier() gocui.Modifier 34 | BindKey() interface{} 35 | BindKeys() []interface{} 36 | ReRenderAll() bool 37 | } 38 | 39 | // Action Action 40 | type Action struct { 41 | Keys []interface{} 42 | Name string 43 | Key interface{} 44 | Handler ViewHandler 45 | ReRenderAllView bool 46 | Mod gocui.Modifier 47 | } 48 | 49 | func (a Action) HandlerFunc(gui *Gui, view *View) error { 50 | return a.Handler(gui, view) 51 | } 52 | 53 | func (a Action) ActionName() string { 54 | return a.Name 55 | } 56 | 57 | func (a Action) Modifier() gocui.Modifier { 58 | return a.Mod 59 | } 60 | 61 | func (a Action) BindKey() interface{} { 62 | return a.Key 63 | } 64 | 65 | func (a Action) BindKeys() []interface{} { 66 | return a.Keys 67 | } 68 | 69 | func (a Action) ReRenderAll() bool { 70 | return a.ReRenderAllView 71 | } 72 | 73 | func ToActionInterfaceArr(actions []*Action) []ActionInterface { 74 | arr := make([]ActionInterface, 0) 75 | for _, act := range actions { 76 | arr = append(arr, act) 77 | } 78 | return arr 79 | } 80 | 81 | type ActionHandler func(gui *Gui) func(*gocui.Gui, *gocui.View) error 82 | 83 | func ViewClickHandler(gui *Gui, view *View) error { 84 | viewName := view.Name 85 | log.Logger.Debugf("ViewClickHandler - view '%s' on click.", viewName) 86 | 87 | currentView := gui.CurrentView() 88 | 89 | var canReturn bool 90 | canReturn = true 91 | if currentView == nil || currentView.Name != viewName { 92 | canReturn = true 93 | 94 | if currentView != nil { 95 | canReturn = !currentView.CanNotReturn 96 | } 97 | 98 | if err := gui.FocusView(viewName, canReturn); err != nil { 99 | return err 100 | } 101 | } 102 | 103 | view, err := gui.GetView(viewName) 104 | if err != nil { 105 | if errors.Is(err, gocui.ErrUnknownView) { 106 | log.Logger.Warningf("ViewClickHandler - gui.GetView(%s) error %+v", view, err) 107 | return nil 108 | } 109 | return err 110 | } 111 | 112 | cx, cy := view.Cursor() 113 | log.Logger.Debugf("ViewClickHandler - cx %d cy %d", cx, cy) 114 | line, err := view.Line(cy) 115 | if err != nil { 116 | log.Logger.Warningf("ViewClickHandler - view.Line(%d) error %s", cy, err) 117 | } else { 118 | log.Logger.Debugf("ViewClickHandler - view.Line(%d) line %s", cy, line) 119 | if view.OnLineClick != nil { 120 | if err := view.OnLineClick(gui, view, cy, line); err != nil { 121 | return err 122 | } 123 | } 124 | } 125 | 126 | if view.OnClick != nil { 127 | return view.OnClick(gui, view) 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func actionHandlerWrapper(gui *Gui, handler ViewHandler) func(*gocui.Gui, *gocui.View) error { 134 | return func(g *gocui.Gui, v *gocui.View) error { 135 | view := gui.getView(v.Name()) 136 | return handler(gui, view) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /pkg/gui/dimension.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | // BeneathView BeneathView 4 | func BeneathView(aboveViewNameFunc func(*Gui, *View) string, heightFunc func(*Gui, *View) int, marginTopFunc func(*Gui, *View) int) func(gui *Gui, view *View) (int, int, int, int) { 5 | return func(gui *Gui, view *View) (int, int, int, int) { 6 | aboveViewName := aboveViewNameFunc(gui, view) 7 | aboveX0, _, aboveX1, aboveY1, err := gui.g.ViewPosition(aboveViewName) 8 | if err != nil { 9 | return 0, 0, 0, 0 10 | } 11 | 12 | y0 := aboveY1 + marginTopFunc(gui, view) 13 | 14 | y1 := y0 + heightFunc(gui, view) 15 | if y1 < 0 { 16 | y1 = 0 17 | } 18 | 19 | return aboveX0, y0, aboveX1, y1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/gui/edit.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import "github.com/jroimartin/gocui" 4 | 5 | // NewViewEditor NewViewEditor 6 | func NewViewEditor(gui *Gui, view *View) gocui.Editor { 7 | return gocui.EditorFunc(ViewEditorFunc(gui, view)) 8 | } 9 | 10 | // ViewEditorFunc ViewEditorFunc 11 | func ViewEditorFunc(gui *Gui, view *View) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { 12 | return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) { 13 | gocui.DefaultEditor.Edit(v, key, ch, mod) 14 | if view.OnEditedChange != nil { 15 | view.OnEditedChange(gui, view, key, ch, mod) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/gui/error.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNotEnoughSpace ErrNotEnoughSpace 7 | ErrNotEnoughSpace = errors.New("not enough space") 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/gui/gui.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "errors" 5 | "github.com/TNK-Studio/lazykube/pkg/config" 6 | "github.com/TNK-Studio/lazykube/pkg/log" 7 | "github.com/jroimartin/gocui" 8 | "github.com/nsf/termbox-go" 9 | "sort" 10 | ) 11 | 12 | type ( 13 | // Gui Gui 14 | Gui struct { 15 | views []*View 16 | Actions []*Action 17 | // History of focused views name. 18 | previousViews TowHeadQueue 19 | OnSizeChange func(gui *Gui) error 20 | OnRender func(gui *Gui) error 21 | OnRenderOptions func(gui *Gui) error 22 | previousViewsLimit int 23 | g *gocui.Gui 24 | state State 25 | preHeight int 26 | preWidth int 27 | Config config.GuiConfig 28 | renderTimes int 29 | } 30 | ) 31 | 32 | // NewGui NewGui 33 | func NewGui(config config.GuiConfig, views ...*View) *Gui { 34 | gui := &Gui{ 35 | state: NewStateMap(), 36 | previousViews: NewQueue(), 37 | previousViewsLimit: 20, 38 | Config: config, 39 | } 40 | gui.views = make([]*View, 0) 41 | g, err := gocui.NewGui(gocui.OutputNormal) 42 | 43 | if err != nil { 44 | log.Logger.Panicf("%+v", err) 45 | } 46 | 47 | gui.g = g 48 | gui.Configure() 49 | 50 | gui.g.SetManagerFunc(gui.layout) 51 | 52 | gui.BindAction("", Quit) 53 | 54 | for _, view := range views { 55 | view.BindGui(gui) 56 | gui.views = append(gui.views, view) 57 | } 58 | 59 | return gui 60 | } 61 | 62 | // ReRenderAll ReRenderAll 63 | func (gui *Gui) ReRenderAll() { 64 | gui.renderTimes++ 65 | for _, view := range gui.views { 66 | view.ReRender() 67 | } 68 | } 69 | 70 | func (gui *Gui) layout(*gocui.Gui) error { 71 | height, width := gui.Size() 72 | if gui.preHeight != height || gui.preWidth != width { 73 | gui.preHeight = height 74 | gui.preWidth = width 75 | if gui.OnSizeChange != nil { 76 | if err := gui.OnSizeChange(gui); err != nil { 77 | return err 78 | } 79 | } 80 | gui.ReRenderAll() 81 | gui.SortViewsByZIndex() 82 | } 83 | 84 | if err := gui.Clear(); err != nil { 85 | return err 86 | } 87 | for _, view := range gui.views { 88 | if err := gui.updateSelectedViewLine(view); err != nil { 89 | return err 90 | } 91 | 92 | err := gui.RenderView(view) 93 | if err == nil { 94 | continue 95 | } 96 | 97 | if errors.Is(err, ErrNotEnoughSpace) { 98 | if err := gui.renderNotEnoughSpaceView(); err != nil { 99 | return err 100 | } 101 | err = nil 102 | } 103 | 104 | return err 105 | } 106 | 107 | if gui.renderTimes > 0 { 108 | if err := gui.onRender(); err != nil { 109 | return err 110 | } 111 | } 112 | 113 | if err := gui.renderOptions(); err != nil { 114 | return err 115 | } 116 | 117 | gui.SetAlwaysOnTopViews() 118 | return nil 119 | } 120 | 121 | func (gui *Gui) onRender() error { 122 | gui.renderTimes-- 123 | 124 | if gui.OnRender != nil { 125 | if err := gui.OnRender(gui); err != nil { 126 | return err 127 | } 128 | } 129 | 130 | currentView := gui.CurrentView() 131 | if currentView != nil { 132 | if _, err := gui.SetViewOnTop(currentView.Name); err != nil { 133 | return err 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | func (gui *Gui) updateSelectedViewLine(view *View) error { 140 | if !view.Rendered() { 141 | return nil 142 | } 143 | 144 | _, cy := view.Cursor() 145 | selectedLine, _ := view.Line(cy) 146 | if selectedLine != view.SelectedLine { 147 | view.SelectedLine = selectedLine 148 | if view.OnSelectedLineChange != nil { 149 | if err := view.OnSelectedLineChange(gui, view, selectedLine); err != nil { 150 | return err 151 | } 152 | } 153 | } 154 | return nil 155 | } 156 | 157 | // SetAlwaysOnTopViews SetAlwaysOnTopViews 158 | func (gui *Gui) SetAlwaysOnTopViews() { 159 | views := gui.views 160 | sort.Sort(ViewsZIndexSorter(views)) 161 | 162 | for _, view := range views { 163 | if !view.AlwaysOnTop { 164 | continue 165 | } 166 | if _, err := gui.SetViewOnTop(view.Name); err != nil { 167 | continue 168 | } 169 | } 170 | 171 | return 172 | } 173 | 174 | // SortViewsByZIndex SortViewsByZIndex 175 | func (gui *Gui) SortViewsByZIndex() { 176 | views := gui.views 177 | sort.Sort(ViewsZIndexSorter(views)) 178 | 179 | for _, view := range views { 180 | if view.AlwaysOnTop { 181 | break 182 | } 183 | if _, err := gui.SetViewOnTop(view.Name); err != nil { 184 | continue 185 | } 186 | } 187 | 188 | return 189 | } 190 | 191 | // Configure Configure 192 | func (gui *Gui) Configure() { 193 | gui.g.Highlight = gui.Config.Highlight 194 | gui.g.Cursor = gui.Config.Cursor 195 | gui.g.SelFgColor = gui.Config.SelFgColor 196 | gui.g.SelBgColor = gui.Config.SelBgColor 197 | gui.g.FgColor = gui.Config.FgColor 198 | gui.g.BgColor = gui.Config.BgColor 199 | gui.g.Mouse = gui.Config.Mouse 200 | gui.g.InputEsc = gui.Config.InputEsc 201 | 202 | inputMode := termbox.InputAlt 203 | if gui.g.InputEsc { 204 | inputMode = termbox.InputEsc 205 | } 206 | if gui.g.Mouse { 207 | inputMode |= termbox.InputMouse 208 | } 209 | termbox.SetInputMode(inputMode) 210 | 211 | // Must to set cursor otherwise hide cursor not work. 212 | termbox.SetCursor(0, 0) 213 | if !gui.g.Cursor { 214 | termbox.HideCursor() 215 | } 216 | } 217 | 218 | // Size Size 219 | func (gui *Gui) Size() (int, int) { 220 | return gui.g.Size() 221 | } 222 | 223 | // MaxWidth MaxWidth 224 | func (gui *Gui) MaxWidth() int { 225 | maxWidth, _ := gui.g.Size() 226 | return maxWidth 227 | } 228 | 229 | // MaxHeight MaxHeight 230 | func (gui *Gui) MaxHeight() int { 231 | _, maxHeight := gui.g.Size() 232 | return maxHeight 233 | } 234 | 235 | // GetViews GetViews 236 | func (gui *Gui) GetViews() []*View { 237 | return gui.views 238 | } 239 | 240 | // SetKeybinding SetKeybinding 241 | func (gui *Gui) SetKeybinding(viewName string, key interface{}, mod gocui.Modifier, handler func(*gocui.Gui, *gocui.View) error) { 242 | if err := gui.g.SetKeybinding( 243 | viewName, 244 | key, 245 | mod, 246 | handler, 247 | ); err != nil { 248 | log.Logger.Panicf("%+v", err) 249 | } 250 | } 251 | 252 | // BindAction BindAction 253 | func (gui *Gui) BindAction(viewName string, action ActionInterface) { 254 | var handler ViewHandler 255 | if action.ReRenderAll() { 256 | handler = func(gui *Gui, view *View) error { 257 | if err := action.HandlerFunc(gui, view); err != nil { 258 | return err 259 | } 260 | gui.ReRenderAll() 261 | return nil 262 | } 263 | } else { 264 | handler = action.HandlerFunc 265 | } 266 | 267 | wrappedHandler := actionHandlerWrapper(gui, handler) 268 | if action.BindKey() != nil { 269 | gui.SetKeybinding(viewName, 270 | action.BindKey(), 271 | action.Modifier(), 272 | wrappedHandler, 273 | ) 274 | } 275 | 276 | if action.BindKeys() != nil { 277 | for _, k := range action.BindKeys() { 278 | gui.SetKeybinding(viewName, 279 | k, 280 | action.Modifier(), 281 | wrappedHandler, 282 | ) 283 | } 284 | } 285 | } 286 | 287 | // ViewDimensionValidated ViewDimensionValidated 288 | func (gui *Gui) ViewDimensionValidated(x0, y0, x1, y1 int) bool { 289 | if x0 >= x1 || y0 >= y1 { 290 | return false 291 | } 292 | 293 | return true 294 | } 295 | 296 | // Run Run 297 | func (gui *Gui) Run() { 298 | if gui.Actions != nil { 299 | for _, act := range gui.Actions { 300 | gui.BindAction("", act) 301 | } 302 | } 303 | 304 | for _, view := range gui.views { 305 | if view.Clickable { 306 | gui.BindAction(view.Name, ClickView) 307 | } 308 | if view.Actions != nil { 309 | for _, act := range view.Actions { 310 | gui.BindAction(view.Name, act) 311 | } 312 | } 313 | } 314 | 315 | if err := gui.g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) { 316 | log.Logger.Panicf("MainLoop - %+v", err) 317 | } 318 | } 319 | 320 | // Close Close 321 | func (gui *Gui) Close() { 322 | gui.g.Close() 323 | } 324 | 325 | // GetView GetView 326 | func (gui *Gui) GetView(name string) (*View, error) { 327 | if err := gui.ViewExisted(name); err != nil { 328 | return nil, err 329 | } 330 | 331 | return gui.getView(name), nil 332 | } 333 | 334 | // RenderView RenderView 335 | func (gui *Gui) RenderView(view *View) error { 336 | x0, y0, x1, y1 := view.GetDimensions() 337 | if !gui.ViewDimensionValidated(x0, y0, x1, y1) { 338 | log.Logger.Warningf("View '%s' has not enough space to render. x0: %d, y0: %d, x1: %d, y1: %d", view.Name, x0, y0, x1, y1) 339 | return ErrNotEnoughSpace 340 | } 341 | return gui.renderView(view, x0, y0, x1, y1) 342 | } 343 | 344 | func (gui *Gui) unRenderNotEnoughSpaceView() error { 345 | v, _ := gui.g.View(NotEnoughSpace.Name) 346 | if v != nil { 347 | if err := gui.g.DeleteView(NotEnoughSpace.Name); err != nil { 348 | if errors.Is(err, gocui.ErrUnknownView) { 349 | return nil 350 | } 351 | } 352 | } 353 | return nil 354 | } 355 | 356 | // Clear Clear 357 | func (gui *Gui) Clear() error { 358 | if err := gui.unRenderNotEnoughSpaceView(); err != nil { 359 | return err 360 | } 361 | if err := termbox.Clear(termbox.Attribute(gui.g.FgColor), termbox.Attribute(gui.g.BgColor)); err != nil { 362 | return err 363 | } 364 | return nil 365 | } 366 | 367 | func (gui *Gui) renderNotEnoughSpaceView() error { 368 | NotEnoughSpace.BindGui(gui) 369 | x0, y0, x1, y1 := NotEnoughSpace.GetDimensions() 370 | if !gui.ViewDimensionValidated(x0, y0, x1, y1) { 371 | return nil 372 | } 373 | return gui.renderView(NotEnoughSpace, x0, y0, x1, y1) 374 | } 375 | 376 | // SetView SetView 377 | func (gui *Gui) SetView(view *View, x0, y0, x1, y1 int) (*View, error) { 378 | if v, err := gui.g.SetView( 379 | view.Name, 380 | x0, y0, x1, y1, 381 | ); err != nil { 382 | if !errors.Is(err, gocui.ErrUnknownView) { 383 | return nil, err 384 | } 385 | 386 | if v == nil { 387 | return nil, err 388 | } 389 | 390 | view.v = v 391 | view.x0, view.y0, view.x1, view.y1 = x0, y0, x1, y1 392 | view.InitView() 393 | return view, gocui.ErrUnknownView 394 | } 395 | return view, nil 396 | } 397 | 398 | func (gui *Gui) renderView(view *View, x0, y0, x1, y1 int) error { 399 | if _, err := gui.SetView( 400 | view, 401 | x0, y0, x1, y1, 402 | ); err != nil { 403 | if !errors.Is(err, gocui.ErrUnknownView) { 404 | return err 405 | } 406 | } 407 | 408 | if view != nil { 409 | if err := view.render(); err != nil { 410 | return err 411 | } 412 | } 413 | 414 | return nil 415 | } 416 | 417 | // ViewColors ViewColors 418 | func (gui *Gui) ViewColors(view *View) (gocui.Attribute, gocui.Attribute) { 419 | if gui.Config.Highlight && view == gui.CurrentView() { 420 | return gui.Config.SelFgColor, gui.Config.SelBgColor 421 | } 422 | return gui.Config.FgColor, gui.Config.BgColor 423 | } 424 | 425 | // CurrentView CurrentView 426 | func (gui *Gui) CurrentView() *View { 427 | v := gui.g.CurrentView() 428 | if v == nil { 429 | return nil 430 | } 431 | return gui.getView(v.Name()) 432 | } 433 | 434 | // AddView AddView 435 | func (gui *Gui) AddView(view *View) error { 436 | // Todo: Check if view existed 437 | gui.views = append(gui.views, view) 438 | view.gui = gui 439 | 440 | if view.Clickable { 441 | gui.BindAction(view.Name, ClickView) 442 | } 443 | 444 | if view.Actions != nil { 445 | for _, act := range view.Actions { 446 | gui.BindAction(view.Name, act) 447 | } 448 | } 449 | 450 | view.InitView() 451 | 452 | err := gui.RenderView(view) 453 | if errors.Is(err, ErrNotEnoughSpace) { 454 | if err := gui.renderNotEnoughSpaceView(); err != nil { 455 | return err 456 | } 457 | return nil 458 | } 459 | return nil 460 | } 461 | 462 | // DeleteView DeleteView 463 | func (gui *Gui) DeleteView(name string) error { 464 | if err := gui.ViewExisted(name); err != nil { 465 | return err 466 | } 467 | 468 | if err := gui.g.DeleteView(name); err != nil { 469 | return err 470 | } 471 | 472 | for index, view := range gui.views { 473 | if view.Name == name { 474 | gui.views = append(gui.views[:index], gui.views[index+1:]...) 475 | } 476 | } 477 | 478 | gui.g.DeleteKeybindings(name) 479 | 480 | return nil 481 | } 482 | 483 | // ViewExisted ViewExisted 484 | func (gui *Gui) ViewExisted(name string) error { 485 | _, err := gui.g.View(name) 486 | if err != nil { 487 | return err 488 | } 489 | return nil 490 | } 491 | 492 | // RenderString RenderString 493 | func (gui *Gui) RenderString(viewName, s string) error { 494 | gui.Update(func(g *gocui.Gui) error { 495 | view, err := gui.GetView(viewName) 496 | if err != nil { 497 | return nil // return gracefully if view has been deleted 498 | } 499 | 500 | if err := view.SetOrigin(0, 0); err != nil { 501 | return err 502 | } 503 | if err := view.SetCursor(0, 0); err != nil { 504 | return err 505 | } 506 | 507 | if view != nil { 508 | return view.SetViewContent(s) 509 | } 510 | 511 | return nil 512 | }) 513 | return nil 514 | } 515 | 516 | // Update Update 517 | func (gui *Gui) Update(f func(*gocui.Gui) error) { 518 | gui.g.Update(f) 519 | } 520 | 521 | // SetCurrentView SetCurrentView 522 | func (gui *Gui) SetCurrentView(name string) (*View, error) { 523 | if _, err := gui.g.SetCurrentView(name); err != nil { 524 | return nil, err 525 | } 526 | view := gui.getView(name) 527 | return view, nil 528 | } 529 | 530 | // SetViewOnTop SetViewOnTop 531 | func (gui *Gui) SetViewOnTop(name string) (*View, error) { 532 | if _, err := gui.g.SetViewOnTop(name); err != nil { 533 | return nil, err 534 | } 535 | 536 | for i, view := range gui.views { 537 | if view.Name == name { 538 | s := append(gui.views[:i], gui.views[i+1:]...) 539 | gui.views = append(s, view) 540 | return view, nil 541 | } 542 | } 543 | 544 | return nil, gocui.ErrUnknownView 545 | } 546 | 547 | func (gui *Gui) GetTopView() *View { 548 | length := len(gui.views) - 1 549 | if length < 1 { 550 | return nil 551 | } 552 | 553 | return gui.views[length-1] 554 | } 555 | 556 | func (gui *Gui) getView(name string) *View { 557 | for _, view := range gui.views { 558 | if view.Name == name { 559 | return view 560 | } 561 | } 562 | return nil 563 | } 564 | 565 | func (gui *Gui) popPreviousView() string { 566 | if !gui.previousViews.IsEmpty() { 567 | viewName := gui.previousViews.Pop().(string) 568 | log.Logger.Debugf("popPreviousView pop '%s', previousViews '%+v'", viewName, gui.previousViews) 569 | return viewName 570 | } 571 | 572 | return "" 573 | } 574 | 575 | func (gui *Gui) PeekPreviousView() string { 576 | if !gui.previousViews.IsEmpty() { 577 | return gui.previousViews.Peek().(string) 578 | } 579 | 580 | return "" 581 | } 582 | 583 | func (gui *Gui) pushPreviousView(name string) { 584 | if name == "" || name == gui.PeekPreviousView() { 585 | return 586 | } 587 | gui.previousViews.Push(name) 588 | if gui.previousViews.Len() > gui.previousViewsLimit { 589 | tail := gui.previousViews.PopTail() 590 | log.Logger.Debugf("pushPreviousView - previousViews over limit, pop tail '%s'", tail) 591 | } 592 | 593 | log.Logger.Debugf("pushPreviousView push '%s', previousViews '%+v'", name, gui.previousViews) 594 | } 595 | 596 | func (gui *Gui) FocusView(name string, canReturn bool) error { 597 | log.Logger.Debugf("FocusView - name: %s canReturn: %+v", name, canReturn) 598 | previousView := gui.CurrentView() 599 | 600 | gui.SortViewsByZIndex() 601 | 602 | if err := gui.focusView(name); err != nil { 603 | return err 604 | } 605 | 606 | gui.SetAlwaysOnTopViews() 607 | 608 | currentView := gui.CurrentView() 609 | 610 | if previousView != nil { 611 | if canReturn && !previousView.CanNotReturn && (currentView == nil || (previousView.Name != currentView.Name)) { 612 | gui.pushPreviousView(previousView.Name) 613 | } 614 | if previousView.Name != name { 615 | if err := currentView.focus(); err != nil { 616 | return err 617 | } 618 | } 619 | } else if currentView.OnFocus != nil { 620 | if err := currentView.focus(); err != nil { 621 | return err 622 | } 623 | } 624 | 625 | if previousView != nil && previousView.Name != name { 626 | if err := previousView.focusLost(); err != nil { 627 | return err 628 | } 629 | } 630 | 631 | return nil 632 | } 633 | 634 | func (gui *Gui) focusView(name string) error { 635 | if _, err := gui.SetCurrentView(name); err != nil { 636 | return err 637 | } 638 | if _, err := gui.SetViewOnTop(name); err != nil { 639 | return err 640 | } 641 | return nil 642 | } 643 | 644 | // HasPreviousView HasPreviousView 645 | func (gui *Gui) HasPreviousView() bool { 646 | return !gui.previousViews.IsEmpty() 647 | } 648 | 649 | // ReturnPreviousView ReturnPreviousView 650 | func (gui *Gui) ReturnPreviousView() error { 651 | previousViewName := gui.popPreviousView() 652 | previousView, err := gui.GetView(previousViewName) 653 | if err != nil { 654 | if errors.Is(err, gocui.ErrUnknownView) { 655 | log.Logger.Warningf("ReturnPreviousView view '%s' not found", previousViewName) 656 | return nil 657 | } 658 | return err 659 | } 660 | log.Logger.Debugf("ReturnPreviousView - gui.focusView(%s)", previousView.Name) 661 | return gui.FocusView(previousView.Name, false) 662 | } 663 | 664 | func (gui *Gui) renderOptions() error { 665 | currentView := gui.CurrentView() 666 | if gui.OnRenderOptions != nil { 667 | if err := gui.OnRenderOptions(gui); err != nil { 668 | return nil 669 | } 670 | } 671 | 672 | if currentView != nil { 673 | if err := currentView.renderOptions(); err != nil { 674 | return err 675 | } 676 | } 677 | return nil 678 | } 679 | 680 | // SetRune SetRune 681 | func (gui *Gui) SetRune(x, y int, ch rune, fgColor, bgColor gocui.Attribute) error { 682 | return gui.g.SetRune(x, y, ch, fgColor, bgColor) 683 | } 684 | 685 | // ReRenderViews ReRenderViews 686 | func (gui *Gui) ReRenderViews(viewNames ...string) { 687 | for _, name := range viewNames { 688 | view, err := gui.GetView(name) 689 | if err != nil { 690 | log.Logger.Warningf("ReRenderViews - view '%s' error %s", name, err) 691 | continue 692 | } 693 | 694 | view.ReRender() 695 | } 696 | } 697 | 698 | // ClearViews ClearViews 699 | func (gui *Gui) ClearViews(viewNames ...string) { 700 | for _, name := range viewNames { 701 | view, err := gui.GetView(name) 702 | if err != nil { 703 | log.Logger.Warningf("ClearViews - view '%s' error %s", name, err) 704 | continue 705 | } 706 | 707 | view.Clear() 708 | } 709 | } 710 | 711 | // ForceFlush ForceFlush 712 | func (gui *Gui) ForceFlush() error { 713 | termbox.Close() 714 | if err := termbox.Init(); err != nil { 715 | return err 716 | } 717 | inputMode := termbox.InputAlt 718 | if gui.g.InputEsc { 719 | inputMode = termbox.InputEsc 720 | } 721 | if gui.g.Mouse { 722 | inputMode |= termbox.InputMouse 723 | } 724 | termbox.SetInputMode(inputMode) 725 | 726 | // Must to set cursor otherwise hide cursor not work. 727 | termbox.SetCursor(0, 0) 728 | if !gui.g.Cursor { 729 | termbox.HideCursor() 730 | } 731 | 732 | return termbox.Flush() 733 | } 734 | 735 | func (gui *Gui) ReInitTermBox() error { 736 | termbox.Close() 737 | if err := termbox.Init(); err != nil { 738 | return err 739 | } 740 | return nil 741 | } 742 | 743 | func (gui *Gui) SetState(key string, value interface{}, reRenderAll bool, reRenderViews ...string) error { 744 | err := gui.state.Set(key, value) 745 | if err != nil { 746 | return err 747 | } 748 | if reRenderAll { 749 | gui.ReRenderAll() 750 | return nil 751 | } 752 | 753 | if reRenderViews != nil { 754 | gui.ReRenderViews(reRenderViews...) 755 | return nil 756 | } 757 | return nil 758 | } 759 | 760 | func (gui *Gui) GetState(key string) (interface{}, error) { 761 | return gui.state.Get(key) 762 | } 763 | -------------------------------------------------------------------------------- /pkg/gui/plot.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jesseduffield/asciigraph" 6 | "io" 7 | "time" 8 | ) 9 | 10 | // Plot Plot 11 | type Plot struct { 12 | Name string 13 | data []float64 14 | since time.Time 15 | 16 | DataGetter func() []float64 17 | Height func(plot *Plot) int 18 | Width func(plot *Plot) int 19 | Max func(plot *Plot) float64 20 | Min func(plot *Plot) float64 21 | Caption func(plot *Plot) string 22 | GraphFormatter func(graph string) string 23 | } 24 | 25 | // NewPlot NewPlot 26 | func NewPlot( 27 | name string, 28 | dataGetter func() []float64, 29 | height func(plot *Plot) int, 30 | width func(plot *Plot) int, 31 | max func(plot *Plot) float64, 32 | min func(plot *Plot) float64, 33 | caption func(plot *Plot) string, 34 | graphFormatter func(string) string, 35 | ) *Plot { 36 | return &Plot{ 37 | Name: name, 38 | data: make([]float64, 0), 39 | DataGetter: dataGetter, 40 | Height: height, 41 | Width: width, 42 | Max: max, 43 | Min: min, 44 | Caption: caption, 45 | GraphFormatter: graphFormatter, 46 | since: time.Now(), 47 | } 48 | } 49 | 50 | // Graph Graph 51 | func (plot *Plot) Graph() string { 52 | return plot.formatGraph(asciigraph.Plot( 53 | plot.data, 54 | asciigraph.Height(plot.Height(plot)), 55 | asciigraph.Width(plot.Width(plot)), 56 | asciigraph.Max(plot.Max(plot)), 57 | asciigraph.Min(plot.Min(plot)), 58 | asciigraph.Caption(plot.Caption(plot)), 59 | )) 60 | } 61 | 62 | func (plot *Plot) formatGraph(graph string) string { 63 | if plot.GraphFormatter != nil { 64 | return plot.GraphFormatter(graph) 65 | } 66 | return graph 67 | } 68 | 69 | // Data Data 70 | func (plot *Plot) Data() []float64 { 71 | return plot.data 72 | } 73 | 74 | // Since Since 75 | func (plot *Plot) Since() time.Time { 76 | return plot.since 77 | } 78 | 79 | // Render Render 80 | func (plot *Plot) Render(io io.Writer) { 81 | newData := plot.DataGetter() 82 | plot.data = append(plot.data, newData...) 83 | if len(plot.data) == 0 { 84 | _, _ = fmt.Fprintf(io, "%s - No data. ", plot.Name) 85 | return 86 | } 87 | 88 | _, _ = fmt.Fprint(io, plot.Graph()) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/gui/sorter.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | type ViewsZIndexSorter []*View 4 | 5 | // Len Len 6 | func (views ViewsZIndexSorter) Len() int { return len(views) } 7 | 8 | // Less Less 9 | func (views ViewsZIndexSorter) Less(i, j int) bool { 10 | if views[i].AlwaysOnTop && !views[j].AlwaysOnTop { 11 | return false 12 | } 13 | if !views[i].AlwaysOnTop && views[j].AlwaysOnTop { 14 | return true 15 | } 16 | 17 | return views[i].ZIndex < views[j].ZIndex 18 | } 19 | 20 | // Swap Swap 21 | func (views ViewsZIndexSorter) Swap(i, j int) { views[i], views[j] = views[j], views[i] } 22 | -------------------------------------------------------------------------------- /pkg/gui/state.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import "errors" 4 | 5 | var ( 6 | // StateKeyError StateKeyError 7 | StateKeyError = errors.New("State key not existed. ") 8 | ) 9 | 10 | // State State 11 | type State interface { 12 | Set(key string, val interface{}) error 13 | Get(Ket string) (interface{}, error) 14 | } 15 | 16 | // StateMap StateMap 17 | type StateMap struct { 18 | state map[string]interface{} 19 | } 20 | 21 | // NewStateMap NewStateMap 22 | func NewStateMap() *StateMap { 23 | return &StateMap{state: map[string]interface{}{}} 24 | } 25 | 26 | // Set Set 27 | func (s *StateMap) Set(key string, val interface{}) error { 28 | s.state[key] = val 29 | return nil 30 | } 31 | 32 | // Get Get 33 | func (s *StateMap) Get(key string) (interface{}, error) { 34 | val, ok := s.state[key] 35 | if !ok { 36 | return nil, StateKeyError 37 | } 38 | 39 | return val, nil 40 | } 41 | 42 | // TowHeadQueue TowHeadQueue 43 | type TowHeadQueue interface { 44 | Pop() interface{} 45 | Peek() interface{} 46 | Tail() interface{} 47 | Push(interface{}) 48 | PopTail() interface{} 49 | Len() int 50 | IsEmpty() bool 51 | } 52 | 53 | // Queue Queue 54 | type Queue struct { 55 | arr []interface{} 56 | length int 57 | } 58 | 59 | // NewQueue NewQueue 60 | func NewQueue() *Queue { 61 | return &Queue{ 62 | arr: make([]interface{}, 0), 63 | length: 0, 64 | } 65 | } 66 | 67 | // Pop Pop 68 | func (q *Queue) Pop() interface{} { 69 | if q.length == 0 { 70 | return nil 71 | } 72 | 73 | index := q.length - 1 74 | el := q.arr[index] 75 | q.arr = q.arr[:index] 76 | q.length-- 77 | return el 78 | } 79 | 80 | // Peek Peek 81 | func (q *Queue) Peek() interface{} { 82 | if q.length == 0 { 83 | return nil 84 | } 85 | return q.arr[q.length-1] 86 | } 87 | 88 | // Tail Tail 89 | func (q *Queue) Tail() interface{} { 90 | if q.length == 0 { 91 | return nil 92 | } 93 | return q.arr[0] 94 | } 95 | 96 | // PopTail PopTail 97 | func (q *Queue) PopTail() interface{} { 98 | if q.length == 0 { 99 | return nil 100 | } 101 | el := q.arr[0] 102 | q.arr = q.arr[1:] 103 | q.length-- 104 | return el 105 | } 106 | 107 | // Push Push 108 | func (q *Queue) Push(el interface{}) { 109 | q.length++ 110 | q.arr = append(q.arr, el) 111 | } 112 | 113 | // Len Len 114 | func (q *Queue) Len() int { 115 | return q.length 116 | } 117 | 118 | // IsEmpty IsEmpty 119 | func (q *Queue) IsEmpty() bool { 120 | return q.length == 0 121 | } 122 | -------------------------------------------------------------------------------- /pkg/gui/view.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "fmt" 5 | "github.com/TNK-Studio/lazykube/pkg/log" 6 | "github.com/TNK-Studio/lazykube/pkg/utils" 7 | "github.com/jroimartin/gocui" 8 | ) 9 | 10 | var ( 11 | NotEnoughSpace *View 12 | ) 13 | 14 | func init() { 15 | NotEnoughSpace = &View{ 16 | Name: "notEnoughSpace", 17 | Title: "Not enough space to render.", 18 | DimensionFunc: func(gui *Gui, view *View) (int, int, int, int) { 19 | maxWidth, maxHeight := gui.Size() 20 | return 0, 0, maxWidth - 1, maxHeight - 1 21 | }, 22 | OnRender: func(gui *Gui, view *View) error { 23 | gui.Config.Cursor = false 24 | gui.Configure() 25 | view.ReRender() 26 | return nil 27 | }, 28 | } 29 | } 30 | 31 | type ( 32 | // View View 33 | View struct { 34 | Actions []ActionInterface 35 | Name string 36 | Title string 37 | SelectedLine string 38 | OnClick ViewHandler 39 | OnLineClick func(gui *Gui, view *View, cy int, lineString string) error 40 | OnRender ViewHandler 41 | OnRenderOptions ViewHandler 42 | OnFocus ViewHandler 43 | OnFocusLost ViewHandler 44 | OnCursorChange func(gui *Gui, view *View, x, y int) error 45 | OnEditedChange func(gui *Gui, view *View, key gocui.Key, ch rune, mod gocui.Modifier) 46 | OnSelectedLineChange func(gui *Gui, view *View, selectedLine string) error 47 | DimensionFunc DimensionFunc 48 | UpperLeftPointXFunc ViewPointFunc 49 | UpperLeftPointYFunc ViewPointFunc 50 | LowerRightPointXFunc ViewPointFunc 51 | LowerRightPointYFunc ViewPointFunc 52 | ZIndex int 53 | x0 int 54 | y0 int 55 | x1 int 56 | y1 int 57 | gui *Gui 58 | v *gocui.View 59 | state State 60 | FgColor gocui.Attribute 61 | BgColor gocui.Attribute 62 | SelBgColor gocui.Attribute 63 | SelFgColor gocui.Attribute 64 | Clickable bool 65 | Editable bool 66 | Wrap bool 67 | Autoscroll bool 68 | IgnoreCarriageReturns bool 69 | Highlight bool 70 | NoFrame bool 71 | MouseDisable bool 72 | // When the "CanNotReturn" parameter is true, it will not be placed in previousViews where the view was clicked. 73 | CanNotReturn bool 74 | renderTimes int 75 | AlwaysOnTop bool 76 | } 77 | 78 | ViewHandler func(gui *Gui, view *View) error 79 | ) 80 | 81 | // InitView InitView 82 | func (view *View) InitView() { 83 | if view.state == nil { 84 | view.state = NewStateMap() 85 | } 86 | if view.v != nil { 87 | view.v.Title = view.Title 88 | view.v.Wrap = view.Wrap 89 | view.v.Editable = view.Editable 90 | view.v.Autoscroll = view.Autoscroll 91 | view.v.Highlight = view.Highlight 92 | view.v.Frame = !view.NoFrame 93 | view.v.FgColor = view.FgColor 94 | view.v.BgColor = view.BgColor 95 | view.v.SelBgColor = view.SelBgColor 96 | view.v.SelFgColor = view.SelFgColor 97 | view.v.MouseDisable = view.MouseDisable 98 | view.v.Editor = NewViewEditor(view.gui, view) 99 | view.v.OnCursorChange = view.onCursorChange 100 | } 101 | } 102 | 103 | // BindGui BindGui 104 | func (view *View) BindGui(gui *Gui) { 105 | view.gui = gui 106 | } 107 | 108 | // InitDimension InitDimension 109 | func (view *View) InitDimension() { 110 | if !view.IsBindingGui() { 111 | log.Logger.Warningf("Please run 'InitDimension' after binding Gui.") 112 | return 113 | } 114 | 115 | if view.DimensionFunc == nil { 116 | return 117 | } 118 | 119 | view.x0, view.y0, view.x1, view.y1 = view.DimensionFunc(view.gui, view) 120 | } 121 | 122 | // UpperLeftPointX UpperLeftPointX 123 | func (view *View) UpperLeftPointX() int { 124 | if view.IsBindingGui() && view.UpperLeftPointXFunc != nil { 125 | return view.UpperLeftPointXFunc(view.gui, view) 126 | } 127 | return view.x0 128 | } 129 | 130 | // UpperLeftPointY UpperLeftPointY 131 | func (view *View) UpperLeftPointY() int { 132 | if view.IsBindingGui() && view.UpperLeftPointYFunc != nil { 133 | return view.UpperLeftPointYFunc(view.gui, view) 134 | } 135 | return view.y0 136 | } 137 | 138 | // LowerRightPointX LowerRightPointX 139 | func (view *View) LowerRightPointX() int { 140 | if view.IsBindingGui() && view.LowerRightPointXFunc != nil { 141 | return view.LowerRightPointXFunc(view.gui, view) 142 | } 143 | return view.x1 144 | } 145 | 146 | // LowerRightPointY LowerRightPointY 147 | func (view *View) LowerRightPointY() int { 148 | if view.IsBindingGui() && view.LowerRightPointYFunc != nil { 149 | return view.LowerRightPointYFunc(view.gui, view) 150 | } 151 | return view.y1 152 | } 153 | 154 | // GetDimensions GetDimensions 155 | func (view *View) GetDimensions() (int, int, int, int) { 156 | view.InitDimension() 157 | x0, y0, x1, y1 := view.UpperLeftPointX(), view.UpperLeftPointY(), view.LowerRightPointX(), view.LowerRightPointY() 158 | return x0, y0, x1, y1 159 | } 160 | 161 | // IsBindingGui IsBindingGui 162 | func (view *View) IsBindingGui() bool { 163 | if view.gui != nil && view.gui.g != nil { 164 | return true 165 | } 166 | 167 | return false 168 | } 169 | 170 | // Rendered Rendered 171 | func (view *View) Rendered() bool { 172 | return view.v != nil 173 | } 174 | 175 | // SetViewContent SetViewContent 176 | func (view *View) SetViewContent(s string) error { 177 | view.v.Clear() 178 | if _, err := fmt.Fprint(view.v, utils.CleanString(s)); err != nil { 179 | return err 180 | } 181 | return nil 182 | } 183 | 184 | // SetOrigin SetOrigin 185 | func (view *View) SetOrigin(x, y int) error { 186 | if view.Rendered() { 187 | return view.v.SetOrigin(x, y) 188 | } 189 | return nil 190 | } 191 | 192 | // Origin Origin 193 | func (view *View) Origin() (int, int) { 194 | return view.v.Origin() 195 | } 196 | 197 | // SetCursor SetCursor 198 | func (view *View) SetCursor(x, y int) error { 199 | if view.Rendered() { 200 | return view.v.SetCursor(x, y) 201 | } 202 | return nil 203 | } 204 | 205 | func (view *View) Write(p []byte) (n int, err error) { 206 | return view.v.Write(p) 207 | } 208 | 209 | // Clear Clear 210 | func (view *View) Clear() { 211 | if view.Rendered() { 212 | view.v.Clear() 213 | } 214 | } 215 | 216 | // Cursor Cursor 217 | func (view *View) Cursor() (int, int) { 218 | return view.v.Cursor() 219 | } 220 | 221 | // ViewBufferLines ViewBufferLines 222 | func (view *View) ViewBufferLines() []string { 223 | return view.v.ViewBufferLines() 224 | } 225 | 226 | // ViewBuffer ViewBuffer 227 | func (view *View) ViewBuffer() string { 228 | return view.v.ViewBuffer() 229 | } 230 | 231 | // Line Line 232 | func (view *View) Line(y int) (string, error) { 233 | return view.v.Line(y) 234 | } 235 | 236 | // WhichLine WhichLine 237 | func (view *View) WhichLine(s string) int { 238 | y := -1 239 | for index, line := range view.v.ViewBufferLines() { 240 | if line == s { 241 | return index 242 | } 243 | } 244 | return y 245 | } 246 | 247 | // MoveCursor MoveCursor 248 | func (view *View) MoveCursor(dx, dy int, writeMode bool) { 249 | view.v.MoveCursor(dx, dy, writeMode) 250 | } 251 | 252 | // ReRender ReRender 253 | func (view *View) ReRender() { 254 | view.renderTimes++ 255 | } 256 | 257 | // ReRenderTimes ReRenderTimes 258 | func (view *View) ReRenderTimes(times int) { 259 | view.renderTimes += times 260 | } 261 | 262 | func (view *View) render() error { 263 | if view.renderTimes < 0 { 264 | return nil 265 | } 266 | view.renderTimes-- 267 | 268 | if view.OnRender != nil { 269 | if err := view.OnRender(view.gui, view); err != nil { 270 | return err 271 | } 272 | } 273 | return nil 274 | } 275 | 276 | func (view *View) renderOptions() error { 277 | if view.OnRenderOptions != nil { 278 | if err := view.OnRenderOptions(view.gui, view); err != nil { 279 | return nil 280 | } 281 | } 282 | return nil 283 | } 284 | 285 | func (view *View) focus() error { 286 | log.Logger.Debugf("view.focus - view name :%s", view.Name) 287 | if view.OnFocus != nil { 288 | log.Logger.Debugf("view.OnFocus - view name :%s", view.Name) 289 | if err := view.OnFocus(view.gui, view); err != nil { 290 | return nil 291 | } 292 | } 293 | return nil 294 | } 295 | 296 | func (view *View) focusLost() error { 297 | log.Logger.Debugf("view.focusLost - view name :%s", view.Name) 298 | if view.OnFocusLost != nil { 299 | log.Logger.Debugf("view.OnFocusLost - view name :%s", view.Name) 300 | if err := view.OnFocusLost(view.gui, view); err != nil { 301 | return nil 302 | } 303 | } 304 | return nil 305 | } 306 | 307 | func (view *View) Size() (int, int) { 308 | return view.v.Size() 309 | } 310 | 311 | // ResetCursorOrigin ResetCursorOrigin 312 | func (view *View) ResetCursorOrigin() error { 313 | if err := view.v.SetCursor(0, 0); err != nil { 314 | return err 315 | } 316 | 317 | if err := view.v.SetOrigin(0, 0); err != nil { 318 | return err 319 | } 320 | 321 | return nil 322 | } 323 | 324 | func (view *View) onCursorChange(_ *gocui.View, x, y int) error { 325 | if view.OnCursorChange != nil { 326 | if err := view.OnCursorChange(view.gui, view, x, y); err != nil { 327 | return err 328 | } 329 | } 330 | return nil 331 | } 332 | 333 | func (view *View) SetState(key string, value interface{}, reRender bool) error { 334 | err := view.state.Set(key, value) 335 | if err != nil { 336 | return err 337 | } 338 | 339 | if reRender { 340 | view.ReRender() 341 | } 342 | return nil 343 | } 344 | 345 | func (view *View) GetState(key string) (interface{}, error) { 346 | return view.state.Get(key) 347 | } 348 | 349 | // DimensionFunc DimensionFunc 350 | type DimensionFunc func(gui *Gui, view *View) (int, int, int, int) 351 | 352 | // ViewPointFunc ViewPointFunc 353 | type ViewPointFunc func(gui *Gui, view *View) int 354 | -------------------------------------------------------------------------------- /pkg/kubecli/apiresources.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/apiresources" 6 | ) 7 | 8 | func (cli *KubeCLI) APIResources(streams genericclioptions.IOStreams, args ...string) *Cmd { 9 | cmd := apiresources.NewCmdAPIResources(cli.factory, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kubecli/clusterinfo/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNK-Studio/lazykube/d6410b444faae32aeda1dfa3dcad61ed22a04799/pkg/kubecli/clusterinfo/.gitkeep -------------------------------------------------------------------------------- /pkg/kubecli/clusterinfo/clusterinfo.go: -------------------------------------------------------------------------------- 1 | package clusterinfo 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gookit/color" 6 | corev1 "k8s.io/api/core/v1" 7 | utilnet "k8s.io/apimachinery/pkg/util/net" 8 | "k8s.io/cli-runtime/pkg/resource" 9 | "k8s.io/kubectl/pkg/cmd/util" 10 | "k8s.io/kubectl/pkg/scheme" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // ClusterInfo ClusterInfo 16 | //nolint:gocognit 17 | //nolint:gocognit 18 | //nolint:gocognit 19 | //nolint:gocognit 20 | //nolint:gocognit 21 | //nolint:gocognit 22 | //nolint:gocognit 23 | //nolint:gocognit 24 | func ClusterInfo(factory util.Factory) (string, error) { 25 | client, err := factory.ToRESTConfig() 26 | if err != nil { 27 | return "", err 28 | } 29 | builder := factory.NewBuilder() 30 | b := builder. 31 | WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). 32 | NamespaceParam("kube-system").DefaultNamespace(). 33 | LabelSelectorParam("kubernetes.io/cluster-service=true"). 34 | ResourceTypeOrNameArgs(false, []string{"services"}...). 35 | Latest() 36 | 37 | infoArr := make([]string, 0) 38 | 39 | if err := b.Do().Visit(func(r *resource.Info, err error) error { 40 | if err != nil { 41 | return err 42 | } 43 | infoArr = append(infoArr, fmt.Sprintf("%s %s", color.Green.Sprint("Kubernetes control plane"), client.Host)) 44 | services := r.Object.(*corev1.ServiceList).Items 45 | for _, service := range services { 46 | var link string 47 | if len(service.Status.LoadBalancer.Ingress) > 0 { 48 | ingress := service.Status.LoadBalancer.Ingress[0] 49 | ip := ingress.IP 50 | if ip == "" { 51 | ip = ingress.Hostname 52 | } 53 | for _, port := range service.Spec.Ports { 54 | link += "http://" + ip + ":" + strconv.Itoa(int(port.Port)) + " " 55 | } 56 | } else { 57 | name := service.ObjectMeta.Name 58 | 59 | if len(service.Spec.Ports) > 0 { 60 | port, schemeStr := service.Spec.Ports[0], "" // guess if the scheme is https 61 | if port.Name == "https" || port.Port == 443 { 62 | schemeStr = "https" 63 | } // format is :: 64 | name = utilnet.JoinSchemeNamePort(schemeStr, service.ObjectMeta.Name, port.Name) 65 | } 66 | if len(client.GroupVersion.Group) == 0 { 67 | link = client.Host + "/api/" + client.GroupVersion.Version + "/namespaces/" + service.ObjectMeta.Namespace + "/services/" + name + "/proxy" 68 | } else { 69 | link = client.Host + "/api/" + client.GroupVersion.Group + "/" + client.GroupVersion.Version + "/namespaces/" + service.ObjectMeta.Namespace + "/services/" + name + "/proxy" 70 | } 71 | } 72 | name := service.ObjectMeta.Labels["kubernetes.io/name"] 73 | if len(name) == 0 { 74 | name = service.ObjectMeta.Name 75 | } 76 | infoArr = append(infoArr, fmt.Sprintf("%s %s", color.Green.Sprint(name), link)) 77 | } 78 | return nil 79 | }); err != nil { 80 | return "", nil 81 | } 82 | return strings.Join(infoArr, "\n"), nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/kubecli/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TNK-Studio/lazykube/d6410b444faae32aeda1dfa3dcad61ed22a04799/pkg/kubecli/config/.gitkeep -------------------------------------------------------------------------------- /pkg/kubecli/config/current_context.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "k8s.io/client-go/tools/clientcmd" 5 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 6 | ) 7 | 8 | var ( 9 | config *clientcmdapi.Config 10 | ) 11 | 12 | func init() { 13 | var err error 14 | pathOptions := clientcmd.NewDefaultPathOptions() 15 | config, err = pathOptions.GetStartingConfig() 16 | if err != nil { 17 | panic(err) 18 | } 19 | } 20 | 21 | func CurrentContext() string { 22 | return config.CurrentContext 23 | } 24 | 25 | func SetCurrentContext(context string) { 26 | config.CurrentContext = context 27 | } 28 | 29 | func ContextNamespace() string { 30 | ctx, ok := config.Contexts[config.CurrentContext] 31 | if !ok { 32 | return "" 33 | } 34 | ns := ctx.Namespace 35 | return ns 36 | } 37 | 38 | func ListContexts() []string { 39 | contexts := make([]string, 0) 40 | for name := range config.Contexts { 41 | contexts = append(contexts, name) 42 | } 43 | return contexts 44 | } 45 | -------------------------------------------------------------------------------- /pkg/kubecli/describe.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/describe" 6 | ) 7 | 8 | func (cli *KubeCLI) Describe(streams genericclioptions.IOStreams, args ...string) *Cmd { 9 | cmd := describe.NewCmdDescribe("kubectl", cli.factory, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kubecli/edit.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/edit" 6 | ) 7 | 8 | func (cli *KubeCLI) Edit(streams genericclioptions.IOStreams, args ...string) *Cmd { 9 | cmd := edit.NewCmdEdit(cli.factory, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kubecli/exec.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "k8s.io/cli-runtime/pkg/genericclioptions" 6 | "k8s.io/kubectl/pkg/cmd/exec" 7 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 8 | "k8s.io/kubectl/pkg/util/i18n" 9 | "time" 10 | 11 | "k8s.io/kubectl/pkg/util/templates" 12 | ) 13 | 14 | // Note: Copy code because of argsLenAtDash always "-1" 15 | 16 | var ( 17 | execExample = templates.Examples(i18n.T(` 18 | # Get output from running 'date' command from pod mypod, using the first container by default 19 | kubectl exec mypod -- date 20 | 21 | # Get output from running 'date' command in ruby-container from pod mypod 22 | kubectl exec mypod -c ruby-container -- date 23 | 24 | # Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod mypod 25 | # and sends stdout/stderr from 'bash' back to the client 26 | kubectl exec mypod -c ruby-container -i -t -- bash -il 27 | 28 | # List contents of /usr from the first container of pod mypod and sort by modification time. 29 | # If the command you want to execute in the pod has any flags in common (e.g. -i), 30 | # you must use two dashes (--) to separate your command's flags/arguments. 31 | # Also note, do not surround your command and its flags/arguments with quotes 32 | # unless that is how you would execute it normally (i.e., do ls -t /usr, not "ls -t /usr"). 33 | kubectl exec mypod -i -t -- ls -t /usr 34 | 35 | # Get output from running 'date' command from the first pod of the deployment mydeployment, using the first container by default 36 | kubectl exec deploy/mydeployment -- date 37 | 38 | # Get output from running 'date' command from the first pod of the service myservice, using the first container by default 39 | kubectl exec svc/myservice -- date 40 | `)) 41 | ) 42 | 43 | const ( 44 | defaultPodExecTimeout = 60 * time.Second 45 | ) 46 | 47 | func NewCmdExec(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { 48 | options := &exec.ExecOptions{ 49 | StreamOptions: exec.StreamOptions{ 50 | IOStreams: streams, 51 | }, 52 | 53 | Executor: &exec.DefaultRemoteExecutor{}, 54 | } 55 | cmd := &cobra.Command{ 56 | Use: "exec (POD | TYPE/NAME) [-c CONTAINER] [flags] -- COMMAND [args...]", 57 | DisableFlagsInUseLine: true, 58 | Short: i18n.T("Execute a command in a container"), 59 | Long: "Execute a command in a container.", 60 | Example: execExample, 61 | Run: func(cmd *cobra.Command, args []string) { 62 | // Note: Copy code because of argsLenAtDash always "-1" 63 | // argsLenAtDash := cmd.ArgsLenAtDash() 64 | argsLenAtDash := 1 65 | cmdutil.CheckErr(options.Complete(f, cmd, args, argsLenAtDash)) 66 | cmdutil.CheckErr(options.Validate()) 67 | cmdutil.CheckErr(options.Run()) 68 | }, 69 | } 70 | cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodExecTimeout) 71 | cmdutil.AddJsonFilenameFlag(cmd.Flags(), &options.FilenameOptions.Filenames, "to use to exec into the resource") 72 | // TODO support UID 73 | cmd.Flags().StringVarP(&options.ContainerName, "container", "c", options.ContainerName, "Container name. If omitted, the first container in the pod will be chosen") 74 | cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", options.Stdin, "Pass stdin to the container") 75 | cmd.Flags().BoolVarP(&options.TTY, "tty", "t", options.TTY, "Stdin is a TTY") 76 | return cmd 77 | } 78 | 79 | // Exec Exec 80 | func (cli *KubeCLI) Exec(streams genericclioptions.IOStreams, args ...string) *Cmd { 81 | cmd := NewCmdExec(cli.factory, streams) 82 | return NewCmd(cmd, args, streams) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/kubecli/get.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/get" 6 | ) 7 | 8 | func (cli *KubeCLI) Get(streams genericclioptions.IOStreams, args ...string) *Cmd { 9 | cmd := get.NewCmdGet("kubectl", cli.factory, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kubecli/get_namespaces.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/api/errors" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/client-go/kubernetes" 9 | ) 10 | 11 | func (cli *KubeCLI) GetNamespaces(ctx context.Context, opts metav1.ListOptions) (*v1.NamespaceList, error) { 12 | config, err := cli.factory.ToRESTConfig() 13 | if err != nil { 14 | return nil, err 15 | } 16 | client, err := kubernetes.NewForConfig(config) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return client.CoreV1().Namespaces().List(ctx, opts) 21 | } 22 | 23 | func (cli *KubeCLI) HasNamespacePermission(ctx context.Context) bool { 24 | _, err := cli.GetNamespaces(ctx, metav1.ListOptions{}) 25 | if err == nil { 26 | return true 27 | } 28 | 29 | if errors.IsForbidden(err) { 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /pkg/kubecli/get_pod_metrics.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "context" 5 | v1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/api/resource" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | . "k8s.io/apimachinery/pkg/labels" 9 | "k8s.io/kubectl/pkg/metricsutil" 10 | metricsapi "k8s.io/metrics/pkg/apis/metrics" 11 | metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" 12 | metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned" 13 | ) 14 | 15 | func GetPodMetrics(m *metricsapi.PodMetrics) v1.ResourceList { 16 | podMetrics := make(v1.ResourceList) 17 | for _, res := range metricsutil.MeasuredResources { 18 | podMetrics[res], _ = resource.ParseQuantity("0") 19 | } 20 | 21 | for _, c := range m.Containers { 22 | for _, res := range metricsutil.MeasuredResources { 23 | quantity := podMetrics[res] 24 | quantity.Add(c.Usage[res]) 25 | podMetrics[res] = quantity 26 | } 27 | } 28 | return podMetrics 29 | } 30 | 31 | func GetAllResourceUsages(metrics *metricsutil.ResourceMetricsInfo) map[v1.ResourceName]int64 { 32 | result := make(map[v1.ResourceName]int64) 33 | for _, res := range metricsutil.MeasuredResources { 34 | quantity := metrics.Metrics[res] 35 | usage := GetSingleResourceUsage(res, quantity) 36 | result[res] = usage 37 | if available, found := metrics.Available[res]; found { 38 | fraction := float64(quantity.MilliValue()) / float64(available.MilliValue()) * 100 39 | result[res] = int64(fraction) 40 | } 41 | } 42 | return result 43 | } 44 | 45 | func GetSingleResourceUsage(resourceType v1.ResourceName, quantity resource.Quantity) int64 { 46 | switch resourceType { 47 | case v1.ResourceCPU: 48 | return quantity.MilliValue() 49 | case v1.ResourceMemory: 50 | return quantity.Value() / (1024 * 1024) 51 | default: 52 | return quantity.Value() 53 | } 54 | } 55 | 56 | func (cli *KubeCLI) GetPodRawMetrics( 57 | namespace, name string, 58 | allNamespaces bool, 59 | selector Selector, 60 | ) (*metricsapi.PodMetricsList, error) { 61 | if selector == nil { 62 | selector = Everything() 63 | } 64 | 65 | var err error 66 | config, err := cli.factory.ToRESTConfig() 67 | if err != nil { 68 | return nil, err 69 | } 70 | metricsClient, err := metricsclientset.NewForConfig(config) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | ns := metav1.NamespaceAll 76 | if !allNamespaces { 77 | ns = namespace 78 | } 79 | versionedMetrics := &metricsv1beta1api.PodMetricsList{} 80 | if name != "" { 81 | m, err := metricsClient.MetricsV1beta1().PodMetricses(ns).Get(context.TODO(), name, metav1.GetOptions{}) 82 | if err != nil { 83 | return nil, err 84 | } 85 | versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m} 86 | } else { 87 | versionedMetrics, err = metricsClient.MetricsV1beta1().PodMetricses(ns).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()}) 88 | if err != nil { 89 | return nil, err 90 | } 91 | } 92 | metrics := &metricsapi.PodMetricsList{} 93 | err = metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, metrics, nil) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return metrics, nil 99 | } 100 | 101 | func (cli *KubeCLI) GetPodMetrics(namespace, name string, allNamespaces bool, selector Selector) ([]map[v1.ResourceName]int64, error) { 102 | metrics, err := cli.GetPodRawMetrics(namespace, name, allNamespaces, selector) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | result := make([]map[v1.ResourceName]int64, 0) 108 | for index, metric := range metrics.Items { 109 | var podMetrics = GetPodMetrics(&metrics.Items[index]) 110 | metricsInfo := &metricsutil.ResourceMetricsInfo{ 111 | Name: metric.Name, 112 | Metrics: podMetrics, 113 | Available: v1.ResourceList{}, 114 | } 115 | result = append(result, GetAllResourceUsages(metricsInfo)) 116 | } 117 | return result, nil 118 | } 119 | -------------------------------------------------------------------------------- /pkg/kubecli/get_resource_gvk.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime/schema" 5 | ) 6 | 7 | func (cli *KubeCLI) GetResourceGroupVersionKind(resourceOrKindArg string) schema.GroupVersionKind { 8 | fullySpecifiedGVR, groupResource := schema.ParseResourceArg(resourceOrKindArg) 9 | gvk := schema.GroupVersionKind{} 10 | 11 | restMapper, err := cli.factory.ToRESTMapper() 12 | if err != nil { 13 | return gvk 14 | } 15 | 16 | if fullySpecifiedGVR != nil { 17 | gvk, _ = restMapper.KindFor(*fullySpecifiedGVR) 18 | } 19 | if gvk.Empty() { 20 | gvk, _ = restMapper.KindFor(groupResource.WithVersion("")) 21 | } 22 | return gvk 23 | } 24 | -------------------------------------------------------------------------------- /pkg/kubecli/kubecli.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/TNK-Studio/lazykube/pkg/kubecli/clusterinfo" 7 | "github.com/TNK-Studio/lazykube/pkg/kubecli/config" 8 | "github.com/TNK-Studio/lazykube/pkg/log" 9 | "github.com/go-logr/logr" 10 | "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | "io/ioutil" 13 | "k8s.io/cli-runtime/pkg/genericclioptions" 14 | _ "k8s.io/client-go/plugin/pkg/client/auth/azure" 15 | _ "k8s.io/client-go/plugin/pkg/client/auth/exec" 16 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 17 | _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" 18 | _ "k8s.io/client-go/plugin/pkg/client/auth/openstack" 19 | "k8s.io/klog/v2" 20 | "k8s.io/kubectl/pkg/cmd/util" 21 | ) 22 | 23 | var Cli *KubeCLI 24 | 25 | func init() { 26 | Cli = NewKubeCLI() 27 | // To disable aws warning 28 | disableKlog() 29 | } 30 | 31 | type KubeCLI struct { 32 | factory util.Factory 33 | namespace *string 34 | context *string 35 | } 36 | 37 | type Cmd struct { 38 | cmd *cobra.Command 39 | args []string 40 | streams genericclioptions.IOStreams 41 | } 42 | 43 | func NewCmd(cmd *cobra.Command, args []string, streams genericclioptions.IOStreams) *Cmd { 44 | return &Cmd{ 45 | cmd: cmd, 46 | args: args, 47 | streams: streams, 48 | } 49 | } 50 | 51 | func (c *Cmd) Run() { 52 | util.BehaviorOnFatal(func(s string, i int) { 53 | _, _ = fmt.Fprint(c.streams.ErrOut, s) 54 | }) 55 | c.cmd.Run(c.cmd, c.args) 56 | } 57 | 58 | func (c *Cmd) SetFlag(name, value string) *Cmd { 59 | if err := c.cmd.Flags().Set(name, value); err != nil { 60 | log.Logger.Panicln(err) 61 | } 62 | return c 63 | } 64 | 65 | func NewKubeCLI() *KubeCLI { 66 | namespace := config.ContextNamespace() 67 | context := config.CurrentContext() 68 | kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 69 | kubeConfigFlags.Namespace = &namespace 70 | kubeConfigFlags.Context = &context 71 | 72 | matchVersionKubeConfigFlags := util.NewMatchVersionFlags(kubeConfigFlags) 73 | 74 | k := &KubeCLI{ 75 | factory: util.NewFactory(matchVersionKubeConfigFlags), 76 | namespace: &namespace, 77 | context: &context, 78 | } 79 | return k 80 | } 81 | 82 | func (cli *KubeCLI) SetNamespace(namespace string) { 83 | kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 84 | kubeConfigFlags.Namespace = &namespace 85 | kubeConfigFlags.Context = cli.context 86 | 87 | matchVersionKubeConfigFlags := util.NewMatchVersionFlags(kubeConfigFlags) 88 | cli.factory = util.NewFactory(matchVersionKubeConfigFlags) 89 | cli.namespace = &namespace 90 | } 91 | 92 | func (cli *KubeCLI) SetCurrentContext(context string) { 93 | config.SetCurrentContext(context) 94 | kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 95 | kubeConfigFlags.Namespace = cli.namespace 96 | kubeConfigFlags.Context = &context 97 | 98 | matchVersionKubeConfigFlags := util.NewMatchVersionFlags(kubeConfigFlags) 99 | cli.factory = util.NewFactory(matchVersionKubeConfigFlags) 100 | cli.context = &context 101 | } 102 | 103 | func (cli *KubeCLI) WithNamespace(namespace string) *KubeCLI { 104 | kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 105 | kubeConfigFlags.Namespace = &namespace 106 | kubeConfigFlags.Context = cli.context 107 | 108 | matchVersionKubeConfigFlags := util.NewMatchVersionFlags(kubeConfigFlags) 109 | 110 | k := &KubeCLI{ 111 | factory: util.NewFactory(matchVersionKubeConfigFlags), 112 | namespace: &namespace, 113 | } 114 | return k 115 | } 116 | 117 | func (cli *KubeCLI) Namespace() string { 118 | return *cli.namespace 119 | } 120 | 121 | func (cli *KubeCLI) CurrentContext() string { 122 | return config.CurrentContext() 123 | } 124 | 125 | func (cli *KubeCLI) ListContexts() []string { 126 | return config.ListContexts() 127 | } 128 | 129 | func (cli *KubeCLI) ClusterInfo() (string, error) { 130 | return clusterinfo.ClusterInfo(cli.factory) 131 | } 132 | 133 | func disableKlog() { 134 | flagSet := &flag.FlagSet{} 135 | klog.InitFlags(flagSet) 136 | _ = flagSet.Set("logtostderr", "false") 137 | klog.SetOutput(ioutil.Discard) 138 | klog.SetLogger(NewKLogger(log.Logger)) 139 | } 140 | 141 | type KLogger struct { 142 | logger *logrus.Logger 143 | } 144 | 145 | func NewKLogger(logger *logrus.Logger) *KLogger { 146 | return &KLogger{ 147 | logger: logger, 148 | } 149 | } 150 | 151 | func (K KLogger) Enabled() bool { 152 | return true 153 | } 154 | 155 | func (K KLogger) Info(msg string, _ ...interface{}) { 156 | log.Logger.Info(msg) 157 | } 158 | 159 | func (K KLogger) Error(_ error, msg string, _ ...interface{}) { 160 | log.Logger.Error(msg) 161 | } 162 | 163 | func (K KLogger) V(_ int) logr.Logger { 164 | return K 165 | } 166 | 167 | func (K KLogger) WithValues(_ ...interface{}) logr.Logger { 168 | return K 169 | } 170 | 171 | func (K KLogger) WithName(_ string) logr.Logger { 172 | return K 173 | } 174 | -------------------------------------------------------------------------------- /pkg/kubecli/logs.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/logs" 6 | ) 7 | 8 | func (cli *KubeCLI) Logs(streams genericclioptions.IOStreams, args ...string) *Cmd { 9 | cmd := logs.NewCmdLogs(cli.factory, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kubecli/rollout_restart.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/rollout" 6 | ) 7 | 8 | func (cli *KubeCLI) RolloutRestart(streams genericclioptions.IOStreams, args ...string) *Cmd { 9 | cmd := rollout.NewCmdRolloutRestart(cli.factory, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kubecli/top_node.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/top" 6 | ) 7 | 8 | func (cli *KubeCLI) TopNode(streams genericclioptions.IOStreams, o *top.TopNodeOptions, args ...string) *Cmd { 9 | cmd := top.NewCmdTopNode(cli.factory, o, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/kubecli/top_pod.go: -------------------------------------------------------------------------------- 1 | package kubecli 2 | 3 | import ( 4 | "k8s.io/cli-runtime/pkg/genericclioptions" 5 | "k8s.io/kubectl/pkg/cmd/top" 6 | ) 7 | 8 | func (cli *KubeCLI) TopPod(streams genericclioptions.IOStreams, o *top.TopPodOptions, args ...string) *Cmd { 9 | cmd := top.NewCmdTopPod(cli.factory, o, streams) 10 | return NewCmd(cmd, args, streams) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/TNK-Studio/lazykube/pkg/config" 5 | rotatelogs "github.com/lestrrat-go/file-rotatelogs" 6 | "github.com/sirupsen/logrus" 7 | "path" 8 | "time" 9 | ) 10 | 11 | var ( 12 | Logger *logrus.Logger 13 | ) 14 | 15 | func init() { 16 | Logger = logrus.New() 17 | Logger.SetLevel(config.Conf.LogConfig.Level) 18 | filePath := path.Join(config.Conf.LogConfig.Path, "lazykube.log") 19 | writer, err := rotatelogs.New( 20 | filePath+".%Y%m%d%H%M", 21 | rotatelogs.WithLinkName(filePath), 22 | rotatelogs.WithMaxAge(time.Duration(180)*time.Second), 23 | rotatelogs.WithRotationTime(time.Duration(60)*time.Second), 24 | ) 25 | if err != nil { 26 | panic("unable to log to file") 27 | } 28 | Logger.SetReportCaller(true) 29 | Logger.SetOutput(writer) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/utils/click_option.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func ClickOption(options []string, separator string, cx int, offset int) (int, string) { 4 | sep := len(separator) 5 | sections := make([]int, 0) 6 | preFix := 0 7 | 8 | cx = cx - offset 9 | if cx < 0 { 10 | return -1, "" 11 | } 12 | 13 | var selected string 14 | for i, opt := range options { 15 | left := preFix + i*sep 16 | 17 | words := len([]rune(opt)) 18 | 19 | right := left + words - 1 20 | preFix += words 21 | 22 | sections = append(sections, left, right) 23 | } 24 | 25 | optionIndex := -1 26 | for i := 0; i < len(sections); i += 2 { 27 | left := sections[i] 28 | right := sections[i+1] 29 | if cx >= left && cx <= right { 30 | optionIndex = i / 2 31 | selected = options[optionIndex] 32 | break 33 | } 34 | } 35 | return optionIndex, selected 36 | } 37 | -------------------------------------------------------------------------------- /pkg/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os" 7 | "os/exec" 8 | "os/user" 9 | "runtime" 10 | "strings" 11 | ) 12 | 13 | // FilePath replace ~ -> $HOME 14 | func FilePath(path string) string { 15 | path = strings.Replace(path, "~", os.Getenv("HOME"), 1) 16 | return path 17 | } 18 | 19 | // FileExited check file exited 20 | func FileExited(path string) bool { 21 | info, err := os.Stat(FilePath(path)) 22 | if os.IsNotExist(err) { 23 | return false 24 | } 25 | return !info.IsDir() 26 | } 27 | 28 | // IsDirector IsDir 29 | func IsDirector(path string) bool { 30 | info, err := os.Stat(path) 31 | if os.IsNotExist(err) { 32 | return false 33 | } 34 | return info.IsDir() 35 | } 36 | 37 | // Home returns the home directory for the executing user. 38 | // 39 | // This uses an OS-specific method for discovering the home directory. 40 | // An error is returned if a home directory cannot be detected. 41 | func Home() (string, error) { 42 | current, err := user.Current() 43 | if err == nil { 44 | return current.HomeDir, nil 45 | } 46 | 47 | // cross compile support 48 | 49 | if "windows" == runtime.GOOS { 50 | return HomeWindows() 51 | } 52 | 53 | // Unix-like system, so just assume Unix 54 | return HomeUnix() 55 | } 56 | 57 | func HomeUnix() (string, error) { 58 | // First prefer the HOME environmental variable 59 | if home := os.Getenv("HOME"); home != "" { 60 | return home, nil 61 | } 62 | 63 | // If that fails, try the shell 64 | var stdout bytes.Buffer 65 | cmd := exec.Command("sh", "-c", "eval echo ~$USER") 66 | cmd.Stdout = &stdout 67 | if err := cmd.Run(); err != nil { 68 | return "", err 69 | } 70 | 71 | result := strings.TrimSpace(stdout.String()) 72 | if result == "" { 73 | return "", errors.New("blank output when reading home directory") 74 | } 75 | 76 | return result, nil 77 | } 78 | 79 | func HomeWindows() (string, error) { 80 | drive := os.Getenv("HOMEDRIVE") 81 | path := os.Getenv("HOMEPATH") 82 | home := drive + path 83 | if drive == "" || path == "" { 84 | home = os.Getenv("USERPROFILE") 85 | } 86 | if home == "" { 87 | return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank") 88 | } 89 | 90 | return home, nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/utils/labels.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Convert labels which like '{"k8s-app":"kube-dns"}' to '[]string{"k8s-app=kube-dns"}'. 9 | func LabelsToStringArr(labelsMapString string) []string { 10 | labelsArr := make([]string, 0) 11 | if labelsMapString == "" { 12 | return labelsArr 13 | } 14 | 15 | //labelsMapString = strings.ReplaceAll(labelsMapString, `"`, `\"`) 16 | labelsMap := make(map[string]string, 0) 17 | b := []byte(labelsMapString) 18 | err := json.Unmarshal(b, &labelsMap) 19 | if err != nil { 20 | return labelsArr 21 | } 22 | 23 | for key, val := range labelsMap { 24 | labelsArr = append(labelsArr, fmt.Sprintf("%s=%s", key, val)) 25 | } 26 | return labelsArr 27 | } 28 | -------------------------------------------------------------------------------- /pkg/utils/math.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "math" 4 | 5 | func MaxFloat64(arr []float64) float64 { 6 | if len(arr) == 0 { 7 | return math.MaxFloat64 8 | } 9 | 10 | max := arr[0] 11 | for _, num := range arr { 12 | max = math.Max(max, num) 13 | } 14 | return max 15 | } 16 | 17 | func MinFloat64(arr []float64) float64 { 18 | if len(arr) == 0 { 19 | return float64(math.MinInt64) 20 | } 21 | 22 | min := arr[0] 23 | for _, num := range arr { 24 | min = math.Min(min, num) 25 | } 26 | return min 27 | } 28 | -------------------------------------------------------------------------------- /pkg/utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jroimartin/gocui" 6 | "github.com/spkg/bom" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | func CleanString(s string) string { 13 | output := string(bom.Clean([]byte(s))) 14 | return NormalizeLinefeeds(output) 15 | } 16 | 17 | // NormalizeLinefeeds - Removes all Windows and Mac style line feeds 18 | func NormalizeLinefeeds(str string) string { 19 | str = strings.Replace(str, "\r\n", "\n", -1) 20 | str = strings.Replace(str, "\r", "", -1) 21 | return str 22 | } 23 | 24 | func OptionsMapToString(optionsMap map[string]string) string { 25 | optionsArray := make([]string, 0) 26 | for key, description := range optionsMap { 27 | optionsArray = append(optionsArray, key+": "+description) 28 | } 29 | sort.Strings(optionsArray) 30 | return strings.Join(optionsArray, ", ") 31 | } 32 | 33 | func DeleteExtraSpace(s string) string { 34 | s1 := strings.Replace(s, " ", " ", -1) 35 | regx := "\\s{2,}" 36 | reg, _ := regexp.Compile(regx) 37 | s2 := make([]byte, len(s1)) 38 | copy(s2, s1) 39 | spcIndex := reg.FindStringIndex(string(s2)) 40 | for len(spcIndex) > 0 { 41 | s2 = append(s2[:spcIndex[0]+1], s2[spcIndex[1]:]...) 42 | spcIndex = reg.FindStringIndex(string(s2)) 43 | } 44 | return string(s2) 45 | } 46 | 47 | func GetKey(key interface{}) string { 48 | var k int 49 | if _, ok := key.(rune); ok { 50 | k = int(key.(rune)) 51 | } else { 52 | k = int(key.(gocui.Key)) 53 | } 54 | 55 | // special keys 56 | switch k { 57 | case 27: 58 | return "esc" 59 | case 13: 60 | return "enter" 61 | case 32: 62 | return "space" 63 | case 65514: 64 | return "►" 65 | case 65515: 66 | return "◄" 67 | case 65517: 68 | return "▲" 69 | case 65516: 70 | return "▼" 71 | case 65508: 72 | return "PgUp" 73 | case 65507: 74 | return "PgDn" 75 | } 76 | 77 | if k >= 65 && k <= 90 { 78 | return fmt.Sprintf("Shift+%c", k+32) 79 | } 80 | 81 | return fmt.Sprintf("%c", k) 82 | } 83 | 84 | -------------------------------------------------------------------------------- /scripts/install_update_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # allow specifying different destination directory 4 | DIR="${DIR:-"/usr/local/bin"}" 5 | PROXY="https://gh-proxy.tnk-studio.org/" 6 | 7 | ARCH=$(uname -m) 8 | case $ARCH in 9 | i386|i686|x86) ARCH=386 ;; 10 | armv6*) ARCH=arm ;; 11 | armv7*) ARCH=arm ;; 12 | aarch64*) ARCH=arm ;; 13 | x86_64) ARCH=amd64 ;; 14 | esac 15 | 16 | # prepare the download URL 17 | GITHUB_LATEST_VERSION=$(curl -L -s -H 'Accept: application/json' ${PROXY}https://github.com/TNK-Studio/lazykube/releases/latest | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') 18 | echo "GITHUB_LATEST_VERSION ${GITHUB_LATEST_VERSION}" 19 | GITHUB_FILE="lazykube_linux_${ARCH}" 20 | GITHUB_URL="https://github.com/TNK-Studio/lazykube/releases/download/${GITHUB_LATEST_VERSION}/${GITHUB_FILE}.tar.gz" 21 | echo "GITHUB_URL ${GITHUB_URL}" 22 | 23 | # install/update the local binary 24 | curl -L -o lazykube.tar.gz $PROXY$GITHUB_URL --progress-bar 25 | tar -xzvf lazykube.tar.gz ${GITHUB_FILE}/lazykube 26 | sudo mv -f ${GITHUB_FILE}/lazykube "$DIR" 27 | echo "lazykube install to '${DIR}'" 28 | rm -rf lazykube.tar.gz ${GITHUB_FILE} 29 | 30 | # Compatible with previous installation methods 31 | if [ -f "/usr/bin/lazykube" ]; then 32 | rm -f /usr/bin/lazykube 33 | ln -s ${DIR}/lazykube /usr/bin/lazykube 34 | echo "Add '/usr/bin/lazykube' link to '${DIR}/lazykube'" 35 | fi 36 | --------------------------------------------------------------------------------