├── .github └── workflows │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── README_config.md ├── README_examples.md ├── README_language.md ├── cmd └── kubectl-sql │ └── main.go ├── go.mod ├── go.sum ├── img ├── kubesql-162.png ├── kubesql-248.png ├── kubesql-500.png └── kubesql.svg ├── kubectl-sql.json ├── kubectl-sql.spec ├── pkg ├── client │ └── client.go ├── cmd │ ├── config.go │ ├── constants.go │ ├── sql-get.go │ ├── sql-join.go │ ├── sql-sql.go │ ├── sql-version.go │ └── sql.go ├── eval │ ├── eval.go │ ├── eval_test.go │ ├── factory.go │ ├── string.go │ └── string_test.go ├── filter │ ├── filter.go │ └── filter_test.go └── printers │ ├── json.go │ ├── name.go │ ├── orderby.go │ ├── table.go │ └── yaml.go └── sql.yaml /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: '1.23' 18 | 19 | - name: Install tools 20 | run: make install-tools 21 | 22 | - name: Run tests 23 | run: make test 24 | 25 | - name: Run lint 26 | run: make lint 27 | 28 | - name: Build 29 | run: make 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | name: Upload Release Assets 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.23' 20 | 21 | - name: Install musl-gcc 22 | run: | 23 | sudo apt-get update 24 | sudo apt-get install -y musl-tools 25 | 26 | - name: Build and package 27 | run: | 28 | make kubectl-sql-static 29 | make dist 30 | env: 31 | VERSION: ${{ github.event.release.tag_name }} 32 | 33 | - name: Upload Release Assets 34 | uses: actions/upload-release-asset@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | upload_url: ${{ github.event.release.upload_url }} 39 | asset_path: ./kubectl-sql.tar.gz 40 | asset_name: kubectl-sql.tar.gz 41 | asset_content_type: application/gzip 42 | 43 | - name: Upload Checksum 44 | uses: actions/upload-release-asset@v1 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | with: 48 | upload_url: ${{ github.event.release.upload_url }} 49 | asset_path: ./kubectl-sql.tar.gz.sha256sum 50 | asset_name: kubectl-sql.tar.gz.sha256sum 51 | asset_content_type: text/plain 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | /kubectl-sql 18 | /kubectl-sql.tar.gz 19 | /kubectl-sql.tar.gz.sha256sum 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to kubectl-sql 2 | 3 | Thank you for considering contributing to kubectl-sql! We welcome contributions from everyone. Here are some guidelines to help you get started. 4 | 5 | ## Getting Started 6 | 7 | ### Setting up the Development Environment 8 | 9 | Participating in the development of our project involves forking the [repository](https://github.com/yaacov/kubectl-sql), setting up your local development environment, making changes, and then proposing those changes via a pull request. Below, we walk through the general steps to get you started! 10 | 11 | #### 1. Forking the Repository and Setting Up Local Development 12 | 13 | **Fork and Clone:** Begin by forking the repository and then clone your fork locally. For step-by-step instructions, check GitHub's guide on [forking repositories](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and [cloning repositories](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository). 14 | 15 | ```bash 16 | Copy code 17 | git clone https://github.com/[YourUsername]/kubectl-sql.git 18 | cd kubectl-sql 19 | ``` 20 | 21 | Remember to replace `[YourUsername]` with your GitHub username. 22 | 23 | #### 2. Installing Dependencies 24 | 25 | **Install Go:** Ensure Go is installed on your machine. If not, download it from the [official Go site](https://golang.org/dl/) and refer to the [installation guide](https://golang.org/doc/install). 26 | 27 | **Manage Project Dependencies:** Navigate to the project directory and manage the dependencies using Go Modules: 28 | 29 | ```bash 30 | go mod tidy 31 | go mod download 32 | ``` 33 | 34 | #### 3. Building and Running the Project 35 | Build the project using Go, or make if a Makefile is available, and verify that it runs locally. 36 | 37 | ```bash 38 | make 39 | ``` 40 | 41 | Now you should be able to execute the binary or utilize the project as per its functionality and options. 42 | 43 | #### 4. Making Changes and Contributing 44 | 45 | Once your environment is set up and running, you’re ready to code! 46 | 47 | When you're ready to contribute your changes back to the project: 48 | 49 | - Ensure to adhere to the project’s coding standards and guidelines. 50 | - Refer to the GitHub guide for creating a pull request from your fork. 51 | 52 | Congratulations, you’re set up for contributing to the project! Always check any additional CONTRIBUTING guidelines provided by the project repository and engage respectfully with the existing community. Happy coding! 53 | 54 | ## Understanding the Project Structure 55 | 56 | Navigating through a project can be quite daunting if you are unfamiliar with its architecture. Here's a brief overview of our Go project structure to get you started: 57 | 58 | `cmd/` 59 | The `cmd/` directory contains the application's entry points, essentially harboring the command-line interfaces or executables of the project. Each subdirectory within `cmd/` is dedicated to an actionable command that the application can perform. 60 | 61 | `cmd/kubectl-sql`: This subdirectory holds the source code of the specific command-line user interface. The main function within this directory acts as the entry point to the command. 62 | 63 | `pkg/` 64 | The `pkg/` directory encompasses helper modules and libraries that are utilized by the main application and can potentially be shared with other projects. The `pkg/` directory is meant to provide a clear distinction between the application code and the auxiliary code that supports it. 65 | 66 | It's crucial to recognize that the code within pkg/ should be designed with reusability in mind, avoiding dependencies from your cmd/ directory, ensuring clean and modular code. 67 | 68 | ## How to Contribute 69 | 70 | ### Reporting Bugs 71 | Ensure the bug was not already reported by searching on GitHub under Issues. 72 | If you're unable to find an open issue addressing the problem, open a new one. 73 | 74 | ### Suggesting Enhancements 75 | Open a new issue with a detailed explanation of your suggestion. 76 | 77 | ## Your First Code Contribution 78 | Begin by looking for good first issues tags in the Issues. 79 | Do not work on an issue without expressing interest by commenting on the issue. 80 | 81 | ## Pull Requests 82 | - Fork the Repo: Fork the project repository and clone your fork. 83 | - Create a Branch: Make a new branch for your feature or bugfix. 84 | - Commit Your Changes: Make sure your code meets the go style guidelines and add tests for new features. 85 | - Push to Your Fork: And submit a pull request to the main branch. 86 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 Yaacov Zamir 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Prerequisites: 18 | # - go 1.16 or higher 19 | # - curl or wget 20 | # - CGO enabled 21 | # - musl-gcc package installed for static binary compilation 22 | # 23 | # Run `make install-tools` to install required development tools 24 | 25 | VERSION_GIT := $(shell git describe --tags) 26 | VERSION ?= ${VERSION_GIT} 27 | 28 | all: kubectl-sql 29 | 30 | .PHONY: install-tools 31 | install-tools: 32 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 33 | 34 | kubesql_cmd := $(wildcard ./cmd/kubectl-sql/*.go) 35 | kubesql_pkg := $(wildcard ./pkg/**/*.go) 36 | GOOS := $(shell go env GOOS) 37 | GOARCH := $(shell go env GOARCH) 38 | 39 | kubectl-sql: clean $(kubesql_cmd) $(kubesql_pkg) 40 | @echo "Building for ${GOOS}/${GOARCH}" 41 | go build -ldflags='-X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' -o kubectl-sql $(kubesql_cmd) 42 | 43 | kubectl-sql-static: $(kubesql_cmd) $(kubesql_pkg) 44 | CGO_ENABLED=1 CC=musl-gcc go build \ 45 | -tags netgo \ 46 | -ldflags '-linkmode external -extldflags "-static" -X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' \ 47 | -o kubectl-sql \ 48 | $(kubesql_cmd) 49 | 50 | .PHONY: lint 51 | lint: 52 | go vet ./pkg/... ./cmd/... 53 | $(shell go env GOPATH)/bin/golangci-lint run ./pkg/... ./cmd/... 54 | 55 | .PHONY: fmt 56 | fmt: 57 | go fmt ./pkg/... ./cmd/... 58 | 59 | .PHONY: dist 60 | dist: kubectl-sql 61 | tar -zcvf kubectl-sql.tar.gz LICENSE kubectl-sql 62 | sha256sum kubectl-sql.tar.gz > kubectl-sql.tar.gz.sha256sum 63 | 64 | .PHONY: clean 65 | clean: 66 | rm -f kubectl-sql 67 | rm -f kubectl-sql.tar.gz 68 | rm -f kubectl-sql.tar.gz.sha256sum 69 | 70 | .PHONY: test 71 | test: 72 | go test -v -cover ./pkg/... ./cmd/... 73 | go test -coverprofile=coverage.out ./pkg/... ./cmd/... 74 | go tool cover -func=coverage.out 75 | @rm coverage.out 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/yaacov/kubectl-sql)](https://goreportcard.com/report/github.com/yaacov/kubectl-sql) 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | 5 |

6 | kubectl-sql Logo 7 |

8 | 9 | # kubectl-sql 10 | 11 | kubectl-sql is a [kubectl plugin](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/) that use SQL like language to query the [Kubernetes](https://kubernetes.io/) cluster manager 12 | 13 | - [Install](#install) 14 | - [What can I do with it ?](#what-can-i-do-with-it-) 15 | - [Alternatives](#alternatives) 16 | 17 |

18 | 19 |

20 | 21 | ## More docs 22 | 23 | - [kubectl-sql's query language](https://github.com/yaacov/kubectl-sql/blob/master/README_language.md) 24 | - [More kubectl-sql examples](https://github.com/yaacov/kubectl-sql/blob/master/README_examples.md) 25 | - [Using the config file](https://github.com/yaacov/kubectl-sql/blob/master/README_config.md) 26 | 27 | ## Install 28 | 29 | Using [krew](https://sigs.k8s.io/krew) plugin manager to install: 30 | 31 | ``` bash 32 | # Available for linux-amd64 33 | kubectl krew install sql 34 | kubectl sql --help 35 | ``` 36 | 37 | Using Fedora Copr: 38 | 39 | ``` bash 40 | # Available for F41 and F42 (linux-amd64) 41 | dnf copr enable yaacov/kubesql 42 | dnf install kubectl-sql 43 | ``` 44 | 45 | From source: 46 | 47 | ``` bash 48 | # Clone code 49 | git clone git@github.com:yaacov/kubectl-sql.git 50 | cd kubectl-sql 51 | 52 | # Build kubectl-sql 53 | make 54 | 55 | # Install into local machine PATH 56 | sudo install ./kubectl-sql /usr/local/bin/ 57 | ``` 58 | 59 |

60 | 61 |

62 | 63 | ## What can I do with it ? 64 | 65 | kubectl-sql let you select Kubernetes resources based on the value of one or more resource fields, using 66 | human readable easy to use SQL like query language. 67 | 68 | [More kubectl-sql examples](https://github.com/yaacov/kubectl-sql/blob/master/README_examples.md) 69 | 70 | ``` bash 71 | # Get pods in namespace "openshift-multus" that hase name containing "cni" 72 | kubectl-sql "select name, status.phase as phase, status.podIP as ip \ 73 | from openshift-multus/pods \ 74 | where name ~= 'cni' and (ip ~= '5$' or phase = 'Running')" 75 | KIND: Pod COUNT: 2 76 | name phase ip 77 | multus-additional-cni-plugins-7kcsd Running 10.130.10.85 78 | multus-additional-cni-plugins-kc8sz Running 10.131.6.65 79 | ... 80 | ``` 81 | 82 | ``` bash 83 | # Get all persistant volume clames that are less then 20Gi, and output as json. 84 | kubectl-sql -o json "select * from pvc where spec.resources.requests.storage < 20Gi" 85 | ... 86 | ``` 87 | 88 | ```bash 89 | # Get only first 10 pods ordered by name 90 | kubectl-sql "SELECT name, status.phase FROM */pods ORDER BY name LIMIT 10" 91 | ``` 92 | 93 |

94 | 95 |

96 | 97 |

98 | 99 |

100 | 101 |

102 | 103 |

104 | 105 | #### Output formats 106 | 107 | | --output flag | Print format | 108 | |----|---| 109 | | table | Table | 110 | | name | Names only | 111 | | yaml | YAML | 112 | | json | JSON | 113 | 114 | ## Alternatives 115 | 116 | #### jq 117 | 118 | `jq` is a lightweight and flexible command-line JSON processor. It is possible to 119 | pipe the kubectl command output into the `jq` command to create complicated searches ( [Illustrated jq toturial](https://github.com/MoserMichael/jq-illustrated) ) 120 | 121 | 122 | 123 | #### kubectl --field-selector 124 | 125 | Field selectors let you select Kubernetes resources based on the value of one or more resource fields. Here are some examples of field selector queries. 126 | 127 | 128 | -------------------------------------------------------------------------------- /README_config.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | kubectl-sql Logo 4 |

5 | 6 | # kubectl-sql 7 | 8 | ## Config File 9 | 10 |

11 | 12 |

13 | 14 | Users can add aliases and edit the fields displayed in table view using json config files, 15 | [see the example config file](https://github.com/yaacov/kubectl-sql/blob/master/kubectl-sql.json). 16 | 17 | Flag: `--kubectl-sql ` (default: `$HOME/.kube/kubectl-sql.json`) 18 | 19 | Example: 20 | 21 | ``` bash 22 | kubectl-sql --kubectl-sql ./kubectl-sql.json get pods 23 | ... 24 | ``` 25 | -------------------------------------------------------------------------------- /README_examples.md: -------------------------------------------------------------------------------- 1 |

2 | kubectl-sql Logo 3 |

4 | 5 | # kubectl-sql 6 | 7 | ## Examples 8 | 9 |

10 | 11 |

12 | 13 | --- 14 | 15 | **Basic Selection & Namespace Filtering** 16 | 17 | * **Select all pods in `default`:** 18 | 19 | ```bash 20 | kubectl sql "SELECT * FROM default/pods" 21 | ``` 22 | 23 | * **Names & namespaces of deployments:** 24 | 25 | ```bash 26 | kubectl sql "SELECT name, namespace FROM */deployments" 27 | ``` 28 | 29 | * **Service names & types in `kube-system`:** 30 | 31 | ```bash 32 | kubectl sql "SELECT name, spec.type FROM kube-system/services" 33 | ``` 34 | 35 | --- 36 | 37 | **Sorting and Limiting Results** 38 | 39 | * **Sort pods by creation time (newest first):** 40 | 41 | ```bash 42 | kubectl sql "SELECT name, metadata.creationTimestamp FROM */pods ORDER BY metadata.creationTimestamp DESC" 43 | ``` 44 | 45 | * **Get the 5 oldest deployments:** 46 | 47 | ```bash 48 | kubectl sql "SELECT name, metadata.creationTimestamp FROM */deployments ORDER BY metadata.creationTimestamp ASC LIMIT 5" 49 | ``` 50 | 51 | * **Sort pods by name and limit to 10 results:** 52 | 53 | ```bash 54 | kubectl sql "SELECT name, status.phase FROM */pods ORDER BY name LIMIT 10" 55 | ``` 56 | 57 | * **Get pods with most restarts:** 58 | 59 | ```bash 60 | kubectl sql "SELECT name, status.containerStatuses[1].restartCount FROM */pods ORDER BY status.containerStatuses[1].restartCount DESC LIMIT 5" 61 | ``` 62 | 63 | * **Sort services by number of ports (multiple-column sorting):** 64 | 65 | ```bash 66 | kubectl sql "SELECT name, namespace FROM */services ORDER BY namespace ASC, name DESC" 67 | ``` 68 | 69 | --- 70 | 71 | **Filtering with `WHERE` Clause** 72 | 73 | * **Pods with label `app=my-app`:** 74 | 75 | ```bash 76 | kubectl sql "SELECT name FROM */pods WHERE metadata.labels.app = 'my-app'" 77 | ``` 78 | 79 | * **Deployments with image `nginx.*`:** 80 | 81 | ```bash 82 | kubectl sql "SELECT name FROM */deployments WHERE spec.template.spec.containers[1].image ~= 'nginx.*'" 83 | ``` 84 | 85 | * **Services of type `LoadBalancer`:** 86 | 87 | ```bash 88 | kubectl sql "SELECT name FROM */services WHERE spec.type = 'LoadBalancer'" 89 | ``` 90 | 91 | * **Pods not `Running`:** 92 | 93 | ```bash 94 | kubectl sql "SELECT name, status.phase FROM */pods WHERE status.phase != 'Running'" 95 | ``` 96 | 97 | * **Pods with container named nginx:** 98 | 99 | ```bash 100 | kubectl sql "SELECT name from */pods where spec.containers[1].name = 'nginx'" 101 | ``` 102 | 103 | --- 104 | 105 | **Aliasing with `AS` Keyword** 106 | 107 | * **Alias `status.phase` to `pod_phase`:** 108 | 109 | ```bash 110 | kubectl sql "SELECT name, status.phase AS pod_phase FROM */pods" 111 | ``` 112 | 113 | * **Alias container image to `container_image`:** 114 | 115 | ```bash 116 | kubectl sql "SELECT name, spec.template.spec.containers[1].image AS container_image FROM */deployments" 117 | ``` 118 | 119 | --- 120 | 121 | **Time-Based Filtering (using `date`)** 122 | 123 | * **Pods created in last 24 hours:** 124 | 125 | ```bash 126 | kubectl sql "SELECT name, metadata.creationTimestamp FROM */pods WHERE metadata.creationTimestamp > '$(date -Iseconds -d "24 hours ago")'" 127 | ``` 128 | 129 | * **Events related to pods in last 10 minutes:** 130 | 131 | ```bash 132 | kubectl sql "SELECT message, metadata.creationTimestamp, involvedObject.name FROM */events WHERE involvedObject.kind = 'Pod' AND metadata.creationTimestamp > '$(date -Iseconds -d "10 minutes ago")'" 133 | ``` 134 | 135 | --- 136 | 137 | **SI Extension Filtering** 138 | 139 | * **Deployments with memory request < 512Mi:** 140 | 141 | ```bash 142 | kubectl sql "SELECT name, spec.template.spec.containers[1].resources.requests.memory FROM */deployments WHERE spec.template.spec.containers[1].resources.requests.memory < 512Mi" 143 | ``` 144 | 145 | * **PVCs with storage request > 10Gi:** 146 | 147 | ```bash 148 | kubectl sql "SELECT name, spec.resources.requests.storage FROM */persistentvolumeclaims WHERE spec.resources.requests.storage > 10Gi" 149 | ``` 150 | 151 | * **Pods with container memory limit > 1Gi:** 152 | 153 | ```bash 154 | kubectl sql "SELECT name, spec.containers[1].resources.limits.memory FROM */pods WHERE spec.containers[1].resources.limits.memory > 1Gi" 155 | ``` 156 | 157 | --- 158 | 159 | **Array Operations (`any`, `all`, `len`)** 160 | 161 | * **Pods with any container using nginx image:** 162 | 163 | ```bash 164 | kubectl sql "SELECT name FROM */pods WHERE any(spec.containers[*].image ~= 'nginx')" 165 | ``` 166 | 167 | * **Pods with any container requesting more than 1Gi memory:** 168 | 169 | ```bash 170 | kubectl sql "SELECT name FROM */pods WHERE any(spec.containers[*].resources.requests.memory > 1Gi)" 171 | ``` 172 | 173 | * **Deployments where all containers have resource limits:** 174 | 175 | ```bash 176 | kubectl sql "SELECT name FROM */deployments WHERE all(spec.template.spec.containers[*].resources.limits is not null)" 177 | ``` 178 | 179 | * **Pods where all containers are ready:** 180 | 181 | ```bash 182 | kubectl sql "SELECT name FROM */pods WHERE all(status.containerStatuses[*].ready = true)" 183 | ``` 184 | 185 | * **Deployments with more than 2 containers:** 186 | 187 | ```bash 188 | kubectl sql "SELECT name FROM */deployments WHERE len(spec.template.spec.containers) > 2" 189 | ``` 190 | 191 | * **Nodes with many pods:** 192 | 193 | ```bash 194 | kubectl sql "SELECT name FROM nodes WHERE len(status.conditions) > 5" 195 | ``` 196 | 197 | * **Pods with empty volumes list:** 198 | 199 | ```bash 200 | kubectl sql "SELECT name FROM */pods WHERE len(spec.volumes) = 0" 201 | ``` 202 | 203 | --- 204 | 205 | **All namespaces** 206 | 207 | * **Get pods that have name containing "ovs" using regular kubectl all namespaces arg:** 208 | 209 | ```bash 210 | kubectl-sql --all-namespaces "select * from pods where name ~= 'cni'" 211 | NAMESPACE NAME PHASE hostIP CREATION_TIME(RFC3339) 212 | openshift-cnv ovs-cni-amd64-5vgcg Running 192.168.126.58 2020-02-10T23:26:31+02:00 213 | openshift-cnv ovs-cni-amd64-8ts4w Running 192.168.126.12 2020-02-10T22:01:59+02:00 214 | openshift-cnv ovs-cni-amd64-d6vdb Running 192.168.126.53 2020-02-10T23:13:45+02:00 215 | ... 216 | ``` 217 | 218 | --- 219 | 220 | **Namespaced** 221 | 222 | * **Get pods in namespace "openshift-multus" that have name containing "ovs":** 223 | 224 | ```bash 225 | kubectl-sql -n openshift-multus "select * from pods where name ~= 'cni'" 226 | KIND: Pod COUNT: 3 227 | NAMESPACE NAME PHASE CREATION_TIME(RFC3339) 228 | openshift-multus multus-additional-cni-plugins-7kcsd Running 2024-12-02T11:41:45Z 229 | openshift-multus multus-additional-cni-plugins-kc8sz Running 2024-12-02T11:41:45Z 230 | openshift-multus multus-additional-cni-plugins-vrpx9 Running 2024-12-02T11:41:45Z 231 | ... 232 | ``` 233 | 234 | --- 235 | 236 | **Select fields** 237 | 238 | * **Get pods in namespace "openshift-multus" with name containing "cni" and select specific fields:** 239 | 240 | ```bash 241 | kubectl-sql "select name, status.phase, status.podIP \ 242 | from openshift-multus/pods \ 243 | where name ~= 'cni'" 244 | KIND: Pod COUNT: 3 245 | name status.phase status.podIP 246 | multus-additional-cni-plugins-7kcsd Running 10.130.10.85 247 | multus-additional-cni-plugins-kc8sz Running 10.131.6.65 248 | multus-additional-cni-plugins-vrpx9 Running 10.129.8.252 249 | ... 250 | ``` 251 | 252 | --- 253 | 254 | **Alias selected fields** 255 | 256 | * **Get pods matching criteria with aliased fields:** 257 | 258 | ```bash 259 | kubectl-sql "select name, status.phase as phase, status.podIP as ip \ 260 | from openshift-multus/pods \ 261 | where name ~= 'cni' and ip ~= '5$' and phase = 'Running'" 262 | KIND: Pod COUNT: 2 263 | name phase ip 264 | multus-additional-cni-plugins-7kcsd Running 10.130.10.85 265 | multus-additional-cni-plugins-kc8sz Running 10.131.6.65 266 | ... 267 | ``` 268 | 269 | --- 270 | 271 | **Using Regexp** 272 | 273 | * **Get pods with name starting with "virt-" and IP ending with ".84":** 274 | 275 | ```bash 276 | kubectl-sql -n default "select * from pods where name ~= '^virt-' and status.podIP ~= '[.]84$'" 277 | NAMESPACE NAME PHASE hostIP CREATION_TIME(RFC3339) 278 | default virt-launcher-test-bdw2p-lcrwx Running 192.168.126.56 2020-02-12T14:14:01+02:00 279 | ... 280 | ``` 281 | 282 | --- 283 | 284 | **SI Units** 285 | 286 | * **Get PVCs less than 20Gi and output as JSON:** 287 | 288 | ```bash 289 | kubectl-sql -o json "select * from */pvc where spec.resources.requests.storage < 20Gi" 290 | 291 | ... json 292 | { 293 | "storage": "10Gi" 294 | } 295 | ... 296 | ``` 297 | 298 | --- 299 | 300 | **Comparing fields** 301 | 302 | * **Get replica sets with 3 replicas but less ready replicas:** 303 | 304 | ```bash 305 | kubectl-sql --all-namespaces "select * from rs where spec.replicas = 3 and status.readyReplicas < spec.replicas" 306 | 307 | ... 308 | ``` 309 | 310 | --- 311 | 312 | **Escaping Identifiers** 313 | 314 | * **Use square brackets for identifiers with special characters:** 315 | 316 | ```bash 317 | ./kubectl-sql --all-namespaces "select * from pods where name ~= 'cni' and metadata.labels[openshift.io/component] = 'network'" 318 | ... 319 | ``` 320 | 321 | --- 322 | 323 | **Print help** 324 | 325 | * **Display kubectl-sql help:** 326 | 327 | ```bash 328 | kubectl-sql --help 329 | ... 330 | ``` 331 | -------------------------------------------------------------------------------- /README_language.md: -------------------------------------------------------------------------------- 1 |

2 | kubectl-sql Logo 3 |

4 | 5 | # kubectl‑sql — Query Language Reference 6 | 7 | kubectl‑sql uses **Tree Search Language (TSL)** – a human‑readable filtering grammar shared with the [`tree-search-language`](https://github.com/yaacov/tree-search-language) project. The tables below document operators, literals and helper supported by the TSL. 8 | 9 | --- 10 | 11 | ## Operators 12 | 13 | | Category | Operators | Example | 14 | | ---------------- | ----------------------------------------------------------------- | --------------------------------------------------------------- | 15 | | Equality / regex | `=`, `!=`, `~=` *(regex match)*, `~!` *(regex ****not**** match)* | `name ~! '^test-'` | 16 | | Pattern | `like`, `not like`, `ilike`, `not ilike` | `phase not ilike 'run%'` | 17 | | Comparison | `>`, `<`, `>=`, `<=` | `created < 2020‑01‑15T00:00:00Z` | 18 | | Null tests | `is null`, `is not null` | `spec.domain.cpu.dedicatedCpuPlacement is not null` | 19 | | Membership | `in`, `not in` | `memory in [1Gi, 2Gi]` | 20 | | Ranges | `between`, `not between` | `memory between 1Gi and 4Gi` | 21 | | Boolean | `and`, `or`, `not` | `name ~= 'virt-' and not namespace = 'default'` | 22 | | Grouping | `( … )` | `(phase='Running' or phase='Succeeded') and namespace~='^cnv-'` | 23 | 24 | --- 25 | 26 | ## Math & Unary Operators 27 | 28 | | Operator | Description | 29 | | ------------- | ------------------------------------------------------------- | 30 | | `+`, `-` | Addition & subtraction *(prefix **`+x`** / **`-x`** allowed)* | 31 | | `*`, `/`, `%` | Multiplication, division, modulo | 32 | | `( … )` | Parentheses to override precedence | 33 | 34 | --- 35 | 36 | ## Aliases 37 | 38 | | Alias | Resource path | Example | 39 | | ------------- | ---------------------- | ---------------------------- | 40 | | `name` | `metadata.name` | `name ~= '^test-'` | 41 | | `namespace` | `metadata.namespace` | `namespace != 'kube-system'` | 42 | | `labels` | `metadata.labels` | `labels.env = 'prod'` | 43 | | `annotations` | `metadata.annotations` | | 44 | | `created` | creationTimestamp | `created > 2023‑01‑01` | 45 | | `deleted` | deletionTimestamp | | 46 | | `phase` | `status.phase` | `phase = 'Running'` | 47 | 48 | --- 49 | 50 | ## Size & Time Literals 51 | 52 | ### SI / IEC units 53 | 54 | #### SI units (powers of 1000) 55 | 56 | | Suffix | Multiplier | 57 | | ------ | ---------- | 58 | | k / K | 10³ | 59 | | M | 10⁶ | 60 | | G | 10⁹ | 61 | | T | 10¹² | 62 | | P | 10¹⁵ | 63 | 64 | #### IEC units (powers of 1024) 65 | 66 | | Suffix | Multiplier | 67 | | ------ | ---------- | 68 | | Ki | 1024¹ | 69 | | Mi | 1024² | 70 | | Gi | 1024³ | 71 | | Ti | 1024⁴ | 72 | | Pi | 1024⁵ | 73 | 74 | ### Scientific notation 75 | 76 | Numbers may be written as `6.02e23`, `2.5E‑3`, etc. 77 | 78 | --- 79 | 80 | ## Booleans 81 | 82 | The literals `true` and `false` (case‑insensitive) evaluate to boolean values. 83 | 84 | --- 85 | 86 | ## Dates 87 | 88 | | Format | Example | 89 | | ---------- | ------------------------------------------- | 90 | | RFC 3339 | `lastTransitionTime > 2025‑02‑20T11:12:38Z` | 91 | | Short date | `created <= 2025‑02‑20` | 92 | 93 | --- 94 | 95 | ## Arrays & Lists 96 | 97 | Fields may include list indices, wildcards or named keys: 98 | 99 | ```tsl 100 | spec.containers[0].resources.requests.memory = 200Mi 101 | spec.ports[*].protocol = 'TCP' 102 | spec.ports[http‑port].port = 80 103 | ``` 104 | 105 | ### Membership tests with lists 106 | 107 | Use **square‑bracket literals** when testing membership: 108 | 109 | ```tsl 110 | memory in [1Gi, 2Gi, 4Gi] 111 | ``` 112 | 113 | ### Array helpers 114 | 115 | | Helper | Example | 116 | | ------ | ----------------------------------------------------------- | 117 | | `any` | `any (spec.containers[*].resources.requests.memory = 200Mi)` | 118 | | `all` | `all (spec.containers[*].resources.requests.memory != null)` | 119 | | `len` | `len spec.containers[*] > 2` | 120 | | `sum` | `sum spec.containers[*].requested.memory > 2Gi` | 121 | 122 | `any`, `all`, and `len` may be called *with or without* parentheses: `any expr` is equivalent to `any(expr)`. 123 | 124 | --- 125 | 126 | > **Tip – mixing selectors**: Combine aliases, regex, math and list helpers to build expressive filters, e.g. 127 | > 128 | > ```tsl 129 | > any(phase = 'Running') and namespace ~= '^(cnv|virt)-' 130 | > ``` 131 | -------------------------------------------------------------------------------- /cmd/kubectl-sql/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "os" 24 | 25 | "github.com/spf13/pflag" 26 | "k8s.io/cli-runtime/pkg/genericclioptions" 27 | 28 | "github.com/yaacov/kubectl-sql/pkg/cmd" 29 | ) 30 | 31 | func main() { 32 | flags := pflag.NewFlagSet("kubectl-sql", pflag.ExitOnError) 33 | pflag.CommandLine = flags 34 | 35 | root := cmd.NewCmdSQL(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) 36 | if err := root.Execute(); err != nil { 37 | os.Exit(1) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yaacov/kubectl-sql 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/spf13/cobra v1.9.0 9 | github.com/spf13/pflag v1.0.6 10 | github.com/yaacov/tree-search-language/v6 v6.0.7 11 | gopkg.in/yaml.v3 v3.0.1 12 | k8s.io/apimachinery v0.32.2 13 | k8s.io/cli-runtime v0.32.2 14 | k8s.io/client-go v0.32.2 15 | ) 16 | 17 | require ( 18 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 19 | github.com/blang/semver/v4 v4.0.0 // indirect 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 21 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 22 | github.com/fsnotify/fsnotify v1.5.4 // indirect 23 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 24 | github.com/go-errors/errors v1.4.2 // indirect 25 | github.com/go-logr/logr v1.4.2 // indirect 26 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 27 | github.com/go-openapi/jsonreference v0.20.2 // indirect 28 | github.com/go-openapi/swag v0.23.0 // indirect 29 | github.com/gogo/protobuf v1.3.2 // indirect 30 | github.com/golang/protobuf v1.5.4 // indirect 31 | github.com/google/btree v1.0.1 // indirect 32 | github.com/google/gnostic-models v0.6.8 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/gofuzz v1.2.0 // indirect 35 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 36 | github.com/google/uuid v1.6.0 // indirect 37 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 38 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 42 | github.com/mailru/easyjson v0.7.7 // indirect 43 | github.com/moby/term v0.5.0 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 47 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 48 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/x448/float16 v0.8.4 // indirect 51 | github.com/xlab/treeprint v1.2.0 // indirect 52 | golang.org/x/net v0.38.0 // indirect 53 | golang.org/x/oauth2 v0.23.0 // indirect 54 | golang.org/x/sync v0.12.0 // indirect 55 | golang.org/x/sys v0.31.0 // indirect 56 | golang.org/x/term v0.30.0 // indirect 57 | golang.org/x/text v0.23.0 // indirect 58 | golang.org/x/time v0.7.0 // indirect 59 | google.golang.org/protobuf v1.36.1 // indirect 60 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 61 | gopkg.in/inf.v0 v0.9.1 // indirect 62 | k8s.io/api v0.32.2 // indirect 63 | k8s.io/klog/v2 v2.130.1 // indirect 64 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 65 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 66 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 67 | sigs.k8s.io/kustomize/api v0.18.0 // indirect 68 | sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect 69 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 70 | sigs.k8s.io/yaml v1.4.0 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 2 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 4 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 7 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 8 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 14 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 15 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 16 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 17 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 18 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 19 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 20 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 21 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 22 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 24 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 25 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 26 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 27 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 28 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 29 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 30 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 31 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 32 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 33 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 34 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 35 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 36 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 37 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 38 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 39 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 40 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 41 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 42 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 43 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 44 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 45 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 46 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 47 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 48 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 49 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 50 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 51 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 52 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 53 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= 54 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 55 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 56 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 57 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 58 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 59 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 60 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 61 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 62 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 63 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 64 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 65 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 69 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 70 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 71 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 72 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 73 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 74 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 75 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 76 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 77 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 78 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 79 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 80 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 81 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 82 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 83 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 84 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 85 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 86 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 87 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 88 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 89 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 90 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 91 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 92 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 93 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 94 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 95 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 96 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 97 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 99 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 100 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 101 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 102 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 103 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 104 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 105 | github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE= 106 | github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 107 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 108 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 109 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 110 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 111 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 112 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 113 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 114 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 115 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 116 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 117 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 118 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 119 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 120 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 121 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 122 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 123 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 124 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 125 | github.com/yaacov/tree-search-language/v6 v6.0.7 h1:pSt/wkXCFj/URjjYHf46j6xHKo+9sULykc8jl9SksMg= 126 | github.com/yaacov/tree-search-language/v6 v6.0.7/go.mod h1:5Fepe5qWOr8RY9MuFwcD9TFV6BwI9P5YsMOuHRJlfc0= 127 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 128 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 129 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 130 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 131 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 132 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 133 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 134 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 135 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 136 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 137 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 138 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 139 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 140 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 141 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 142 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 143 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 144 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 145 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 146 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 147 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 148 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 149 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 150 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 155 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 156 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 157 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 158 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 159 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 160 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 161 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 162 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 163 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 164 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 165 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 166 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 167 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 168 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 169 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 170 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 171 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 172 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 173 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 174 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 175 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 176 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 177 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 178 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 179 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 180 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 181 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 182 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 183 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 184 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 185 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 186 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 187 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 188 | k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= 189 | k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= 190 | k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= 191 | k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 192 | k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks= 193 | k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8= 194 | k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= 195 | k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= 196 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 197 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 198 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 199 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 200 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 201 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 202 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 203 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 204 | sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= 205 | sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= 206 | sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= 207 | sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= 208 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 209 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 210 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 211 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 212 | -------------------------------------------------------------------------------- /img/kubesql-162.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaacov/kubectl-sql/c498220aa664efe2a81192b91c920b6124d2de8c/img/kubesql-162.png -------------------------------------------------------------------------------- /img/kubesql-248.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaacov/kubectl-sql/c498220aa664efe2a81192b91c920b6124d2de8c/img/kubesql-248.png -------------------------------------------------------------------------------- /img/kubesql-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaacov/kubectl-sql/c498220aa664efe2a81192b91c920b6124d2de8c/img/kubesql-500.png -------------------------------------------------------------------------------- /kubectl-sql.json: -------------------------------------------------------------------------------- 1 | { 2 | "aliases": { 3 | "owner.name": "metadata.ownerReferences[1].name", 4 | "owner.kind": "metadata.ownerReferences[1].kind", 5 | "hostIP": "status.hostIP", 6 | "podIP": "status.podIP" 7 | }, 8 | "table-fields": { 9 | "VirtualMachineInstance": [ 10 | { 11 | "title": "NAMESPACE", 12 | "name": "namespace" 13 | }, 14 | { 15 | "title": "NAME", 16 | "name": "name" 17 | }, 18 | { 19 | "title": "PHASE", 20 | "name": "status.phase" 21 | }, 22 | { 23 | "title": "IP", 24 | "name": "status.interfaces[1].ipAddress" 25 | }, 26 | { 27 | "title": "CREATION_TIME(RFC3339)", 28 | "name": "created" 29 | } 30 | ], 31 | "VirtualMachineInstanceMigration": [ 32 | { 33 | "title": "NAMESPACE", 34 | "name": "namespace" 35 | }, 36 | { 37 | "title": "NAME", 38 | "name": "name" 39 | }, 40 | { 41 | "title": "VMI", 42 | "name": "spec.vmiName" 43 | }, 44 | { 45 | "title": "CREATION_TIME(RFC3339)", 46 | "name": "created" 47 | } 48 | ], 49 | "ReplicaSet": [ 50 | { 51 | "title": "NAMESPACE", 52 | "name": "namespace" 53 | }, 54 | { 55 | "title": "NAME", 56 | "name": "name" 57 | }, 58 | { 59 | "title": "READY", 60 | "name": "status.readyReplicas" 61 | }, 62 | { 63 | "title": "REPLI", 64 | "name": "status.replicas" 65 | }, 66 | { 67 | "title": "CREATION_TIME(RFC3339)", 68 | "name": "created" 69 | } 70 | ] 71 | } 72 | } -------------------------------------------------------------------------------- /kubectl-sql.spec: -------------------------------------------------------------------------------- 1 | %global provider github 2 | %global provider_tld com 3 | %global project yaacov 4 | %global repo kubectl-sql 5 | %global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} 6 | %global import_path %{provider_prefix} 7 | 8 | %undefine _missing_build_ids_terminate_build 9 | %define debug_package %{nil} 10 | 11 | Name: %{repo} 12 | Version: 0.3.35 13 | Release: 1%{?dist} 14 | Summary: kubectl-sql uses sql like language to query the Kubernetes cluster manager 15 | License: Apache 16 | URL: https://%{import_path} 17 | Source0: https://github.com/yaacov/kubectl-sql/archive/v%{version}.tar.gz 18 | 19 | BuildRequires: git 20 | BuildRequires: golang >= 1.23.0 21 | 22 | %description 23 | kubectl-sql let you select Kubernetes resources based on the value of one or more resource fields, using human readable easy to use SQL like query langauge. 24 | 25 | %prep 26 | %setup -q -n kubectl-sql-%{version} 27 | 28 | %build 29 | # set up temporary build gopath, and put our directory there 30 | mkdir -p ./_build/src/github.com/yaacov 31 | ln -s $(pwd) ./_build/src/github.com/yaacov/kubectl-sql 32 | 33 | VERSION=v%{version} make 34 | 35 | %install 36 | install -d %{buildroot}%{_bindir} 37 | install -p -m 0755 ./kubectl-sql %{buildroot}%{_bindir}/kubectl-sql 38 | 39 | %files 40 | %defattr(-,root,root,-) 41 | %doc LICENSE README.md 42 | %{_bindir}/kubectl-sql 43 | 44 | %changelog 45 | 46 | * Wed Feb 19 2025 Yaacov Zamir 0.3.16-1 47 | - use TSL v6 48 | 49 | * Sun Feb 16 2025 Yaacov Zamir 0.3.14-1 50 | - use TSL v6 51 | 52 | * Mon Mar 9 2020 Yaacov Zamir 0.2.11-1 53 | - version should start with v 54 | 55 | * Mon Mar 9 2020 Yaacov Zamir 0.2.10-1 56 | - dont show usage on errors 57 | 58 | * Mon Mar 9 2020 Yaacov Zamir 0.2.9-1 59 | - preety print join 60 | 61 | * Sun Mar 8 2020 Yaacov Zamir 0.2.8-1 62 | - fix docs 63 | 64 | * Fri Mar 6 2020 Yaacov Zamir 0.2.6-1 65 | - fix none namespaced resource display 66 | 67 | * Fri Mar 6 2020 Yaacov Zamir 0.2.5-1 68 | - add join command 69 | 70 | * Thu Mar 5 2020 Yaacov Zamir 0.2.4-1 71 | - rename to kubectl-sql 72 | 73 | * Thu Mar 4 2020 Yaacov Zamir 0.2.2-1 74 | - use git version number 75 | 76 | * Thu Mar 4 2020 Yaacov Zamir 0.2.1-1 77 | - Fix multiple resources 78 | 79 | * Thu Mar 4 2020 Yaacov Zamir 0.2.0-1 80 | - Use kubectl plugin kit 81 | 82 | * Thu Feb 22 2020 Yaacov Zamir 0.1.18-1 83 | - Fix float printing 84 | 85 | * Thu Feb 22 2020 Yaacov Zamir 0.1.17-1 86 | - Add config option 87 | 88 | * Thu Feb 22 2020 Yaacov Zamir 0.1.16-1 89 | - Fix parsing of anotations 90 | 91 | * Thu Feb 22 2020 Yaacov Zamir 0.1.15-1 92 | - Fix parsing of labels and anotations 93 | 94 | * Thu Feb 22 2020 Yaacov Zamir 0.1.14-1 95 | - Fix parsing of numbers 96 | 97 | * Thu Feb 22 2020 Yaacov Zamir 0.1.13-1 98 | - Parse dates and booleans 99 | 100 | * Thu Feb 20 2020 Yaacov Zamir 0.1.12-1 101 | - No debug rpm 102 | 103 | * Thu Feb 20 2020 Yaacov Zamir 0.1.11-1 104 | - Initial RPM release 105 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package client 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "strings" 26 | 27 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 | "k8s.io/apimachinery/pkg/runtime/schema" 30 | "k8s.io/client-go/discovery" 31 | "k8s.io/client-go/dynamic" 32 | _ "k8s.io/client-go/plugin/pkg/client/auth" 33 | "k8s.io/client-go/rest" 34 | ) 35 | 36 | // Config provides information required to query the kubernetes server. 37 | type Config struct { 38 | Config *rest.Config 39 | Namespace string 40 | AllNamespaces bool 41 | } 42 | 43 | // List resources by resource name. 44 | func (c Config) List(ctx context.Context, resourceName string) ([]unstructured.Unstructured, error) { 45 | var err error 46 | var list *unstructured.UnstructuredList 47 | 48 | resource, group, version, err := c.getResourceGroupVersion(resourceName) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | dynamicClient, err := dynamic.NewForConfig(c.Config) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // Get all resource objects. 59 | res := dynamicClient.Resource(schema.GroupVersionResource{ 60 | Group: group, 61 | Version: version, 62 | Resource: resource.Name, 63 | }) 64 | 65 | // Check for namespace 66 | if !c.AllNamespaces && len(c.Namespace) > 0 && resource.Namespaced { 67 | list, err = res.Namespace(c.Namespace).List(ctx, v1.ListOptions{}) 68 | } else { 69 | list, err = res.List(ctx, v1.ListOptions{}) 70 | } 71 | 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return list.Items, err 77 | } 78 | 79 | // Look for a resource matching request resource name. 80 | func (c Config) getResourceGroupVersion(resourceName string) (v1.APIResource, string, string, error) { 81 | discoveryClient, err := discovery.NewDiscoveryClientForConfig(c.Config) 82 | if err != nil { 83 | return v1.APIResource{}, "", "", err 84 | } 85 | 86 | resources, err := discoveryClient.ServerPreferredResources() 87 | if err != nil { 88 | return v1.APIResource{}, "", "", err 89 | } 90 | 91 | // Search for a matching resource 92 | resource := v1.APIResource{} 93 | resourceList := &v1.APIResourceList{} 94 | for _, rl := range resources { 95 | for _, r := range rl.APIResources { 96 | names := append(r.ShortNames, r.Name) 97 | if stringInSlice(resourceName, names) { 98 | resource = r 99 | resourceList = rl 100 | } 101 | } 102 | 103 | if len(resource.Name) > 0 { 104 | break 105 | } 106 | } 107 | 108 | if len(resource.Name) == 0 { 109 | return v1.APIResource{}, "", "", fmt.Errorf("Failed to find resource") 110 | } 111 | 112 | group, version := getGroupVersion(resourceList) 113 | return resource, group, version, nil 114 | } 115 | 116 | // Get resource group and version. 117 | func getGroupVersion(resourceList *v1.APIResourceList) (string, string) { 118 | group := "" 119 | version := "" 120 | resourceGroupSplit := strings.Split(resourceList.GroupVersion, "/") 121 | if len(resourceGroupSplit) == 2 { 122 | group = resourceGroupSplit[0] 123 | version = resourceGroupSplit[1] 124 | } else { 125 | version = resourceGroupSplit[0] 126 | } 127 | 128 | return group, version 129 | } 130 | 131 | // Check if a string in slice of strings. 132 | func stringInSlice(a string, list []string) bool { 133 | for _, b := range list { 134 | if b == a { 135 | return true 136 | } 137 | } 138 | return false 139 | } 140 | -------------------------------------------------------------------------------- /pkg/cmd/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package cmd 21 | 22 | import ( 23 | "encoding/json" 24 | "fmt" 25 | "os" 26 | 27 | "k8s.io/cli-runtime/pkg/genericclioptions" 28 | _ "k8s.io/client-go/plugin/pkg/client/auth" 29 | "k8s.io/client-go/tools/clientcmd" 30 | 31 | "github.com/yaacov/kubectl-sql/pkg/printers" 32 | ) 33 | 34 | // SQLOptions provides information required to update 35 | // the current context on a user's KUBECONFIG 36 | type SQLOptions struct { 37 | configFlags *genericclioptions.ConfigFlags 38 | 39 | rawConfig clientcmd.ClientConfig 40 | defaultSQLConfigPath string 41 | requestedSQLConfigPath string 42 | args []string 43 | 44 | defaultAliases map[string]string 45 | defaultTableFields printers.TableFieldsMap 46 | orderByFields []printers.OrderByField 47 | limit int 48 | 49 | namespace string 50 | allNamespaces bool 51 | requestedResources []string 52 | requestedQuery string 53 | requestedOnQuery string 54 | 55 | outputFormat string 56 | noHeaders bool 57 | 58 | genericclioptions.IOStreams 59 | } 60 | 61 | // SQLConfig describes configuration overrides for SQL queries. 62 | type SQLConfig struct { 63 | Aliases map[string]string `json:"aliases"` 64 | TableFields printers.TableFieldsMap `json:"table-fields"` 65 | OrderByFields []printers.OrderByField `json:"order-by-fields,omitempty"` 66 | Limit int `json:"limit,omitempty"` 67 | } 68 | 69 | // NewSQLConfig provides an instance of SQLConfig with default values 70 | func NewSQLConfig() *SQLConfig { 71 | return &SQLConfig{ 72 | Aliases: defaultAliases, 73 | TableFields: defaultTableFields, 74 | OrderByFields: []printers.OrderByField{}, 75 | Limit: 0, // Default to no limit 76 | } 77 | } 78 | 79 | // Read SQL json config file. 80 | func (o *SQLOptions) readConfigFile(filename string) error { 81 | userConfig := NewSQLConfig() 82 | 83 | // Init default config. 84 | o.defaultAliases = userConfig.Aliases 85 | o.defaultTableFields = userConfig.TableFields 86 | o.orderByFields = userConfig.OrderByFields 87 | o.limit = userConfig.Limit 88 | 89 | // If file is missing, fail quietly. 90 | if !fileExists(o.requestedSQLConfigPath) { 91 | return nil 92 | } 93 | 94 | file, err := os.ReadFile(filename) 95 | if err != nil { 96 | return fmt.Errorf("can't read file '%s', %v", filename, err) 97 | } 98 | 99 | err = json.Unmarshal(file, &userConfig) 100 | if err != nil { 101 | return fmt.Errorf("can't parse json file '%s', %v", filename, err) 102 | } 103 | 104 | // Merge user defined aliases into the default aliases map. 105 | if len(userConfig.Aliases) > 0 { 106 | for k, v := range userConfig.Aliases { 107 | o.defaultAliases[k] = v 108 | } 109 | } 110 | 111 | // Merge user defined tables into the default table headers. 112 | if len(userConfig.TableFields) > 0 { 113 | for k, v := range userConfig.TableFields { 114 | o.defaultTableFields[k] = v 115 | } 116 | } 117 | 118 | // Set OrderByFields from config if specified 119 | if len(userConfig.OrderByFields) > 0 { 120 | o.orderByFields = userConfig.OrderByFields 121 | } 122 | 123 | // Set limit from config if specified 124 | if userConfig.Limit > 0 { 125 | o.limit = userConfig.Limit 126 | } 127 | 128 | return nil 129 | } 130 | 131 | // fileExists checks if a file exists and is not a directory. 132 | func fileExists(filename string) bool { 133 | info, err := os.Stat(filename) 134 | if os.IsNotExist(err) { 135 | return false 136 | } 137 | return !info.IsDir() 138 | } 139 | -------------------------------------------------------------------------------- /pkg/cmd/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package cmd 21 | 22 | import ( 23 | "github.com/yaacov/kubectl-sql/pkg/printers" 24 | ) 25 | 26 | var ( 27 | clientVersion = "GIT-master" 28 | 29 | // sql command. 30 | sqlCmdLong = `Uses SQL-like language to filter and display one or many resources. 31 | 32 | kubectl sql prints information about kubernetes resources filtered using SQL-like query` 33 | 34 | sqlCmdExample = ` # Print client version. 35 | kubectl sql version 36 | 37 | # List all pods where name starts with "test-" case insensitive. 38 | kubectl sql "select * from pods where name ilike 'test-%%'" 39 | 40 | # Display pods by nodes using matching IP address. 41 | kubectl sql "select * from nodes join pods on nodes.status.addresses[1].address = pods.status.hostIP" 42 | 43 | # List first 5 pods ordered by creation time in descending order (newest first). 44 | kubectl sql "select * from pods order by created desc limit 5" 45 | 46 | # Print current available aliases. 47 | kubectl sql aliases 48 | 49 | # Print current available aliases while using a config file. 50 | kubectl sql aliases -q ./kubectl-sql.json 51 | 52 | # Print this help message. 53 | kubectl sql help` 54 | 55 | // sql get command. 56 | sqlGetShort = "Uses SQL-like language to filter and display one or many resources" 57 | sqlGetLong = `Uses SQL-like language to filter and display one or many resources. 58 | 59 | kubectl sql get prints information about kubernetes resources filtered using SQL-like query. 60 | If the desired resource type is namespaced you will only see results in your current 61 | namespace unless you pass --all-namespaces` 62 | 63 | sqlGetExample = ` # List all pods in table output format. 64 | kubectl sql get pods 65 | 66 | # List all replication controllers and services in json output format. 67 | kubectl sql get rc,services --output json 68 | 69 | # List all pods where name starts with "test-" case insensitive. 70 | kubectl sql select * from pods where name ilike 'test-%%'" 71 | 72 | # List all pods where the memory request for the first container is lower or equal to 200Mi. 73 | kubectl sql --all-namespaces "select * from pods where spec.containers[1].resources.requests.memory <= 200Mi" 74 | 75 | # List pods ordered by name, limiting to 10 results. 76 | kubectl sql "select * from pods order by name limit 10"` 77 | 78 | // sql get command. 79 | sqlJoinShort = "Uses SQL-like language to join two resources" 80 | sqlJoinLong = `Uses SQL-like language to join two resources. 81 | 82 | kubectl sql join prints information about kubernetes resources joined using SQL-like query. 83 | If the desired resource type is namespaced you will only see results in your current 84 | namespace unless you pass --all-namespaces` 85 | 86 | sqlJoinExample = ` # List all virtual machine instanaces and pods joined on vim is owner of pod. 87 | kubectl sql "select * from vmis join pods on vmis.metadata.uid = pods.metadata.ownerReferences[1].uid" 88 | 89 | # List all virtual machine instanaces and pods joined on vim is owner of pod for vmis with name matching 'test' regexp. 90 | kubectl sql "select * from vmis join pods on vmis.metadata.uid = pods.metadata.ownerReferences[1].uid where name ~= 'test'" -A 91 | 92 | # Join deployment sets with pods using the uid aliases. 93 | kubectl sql "select ds join pods on ds.uid = pods.owner.uid" 94 | 95 | # Display non running pods by nodes for all namespaces. 96 | kubectl sql "select nodes join pods on nodes.status.addresses[1].address = pods.status.hostIP and not pods.phase ~= 'Running'" -A 97 | 98 | # Display pods by nodes for all namespaces, ordered by node name and limited to 5 results. 99 | kubectl sql "select nodes join pods on nodes.status.addresses[1].address = pods.status.hostIP order by nodes.name limit 5" -A` 100 | 101 | // Errors. 102 | errUsageTemplate = "bad command or command usage, %s" 103 | 104 | // Defaults. 105 | defaultAliases = map[string]string{ 106 | "phase": "status.phase", 107 | "uid": "metadata.uid", 108 | "owner.uid": "metadata.ownerReferences[1].uid", 109 | } 110 | defaultTableFields = printers.TableFieldsMap{ 111 | "other": { 112 | { 113 | Title: "NAMESPACE", 114 | Name: "namespace", 115 | }, 116 | { 117 | Title: "NAME", 118 | Name: "name", 119 | }, 120 | { 121 | Title: "PHASE", 122 | Name: "status.phase", 123 | }, 124 | { 125 | Title: "CREATION_TIME(RFC3339)", 126 | Name: "created", 127 | }, 128 | }, 129 | } 130 | ) 131 | -------------------------------------------------------------------------------- /pkg/cmd/sql-get.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package cmd 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/spf13/cobra" 28 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 | "k8s.io/client-go/rest" 30 | 31 | "github.com/yaacov/kubectl-sql/pkg/client" 32 | "github.com/yaacov/kubectl-sql/pkg/filter" 33 | "github.com/yaacov/kubectl-sql/pkg/printers" 34 | ) 35 | 36 | // CompleteGet sets all information required for updating the current context for get sub command. 37 | func (o *SQLOptions) CompleteGet(cmd *cobra.Command, args []string) error { 38 | var err error 39 | o.args = args 40 | 41 | if len(o.args) != 1 && len(o.args) != 3 { 42 | return fmt.Errorf(errUsageTemplate, "bad number of arguments") 43 | } 44 | 45 | // Read SQL plugin specific configurations. 46 | if err = o.readConfigFile(o.requestedSQLConfigPath); err != nil { 47 | return err 48 | } 49 | 50 | // get [where ] 51 | o.requestedResources = strings.Split(o.args[0], ",") 52 | 53 | // Look for "where" 54 | if len(o.args) == 3 { 55 | if strings.ToLower(o.args[1]) != "where" { 56 | return fmt.Errorf(errUsageTemplate, "missing \"where\" argument") 57 | } 58 | 59 | o.requestedQuery = o.args[2] 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // Get the resource list. 66 | func (o *SQLOptions) Get(config *rest.Config) error { 67 | c := client.Config{ 68 | Config: config, 69 | Namespace: o.namespace, 70 | AllNamespaces: o.allNamespaces, 71 | } 72 | 73 | if len(o.requestedQuery) > 0 { 74 | return o.printFilteredResources(c) 75 | } 76 | 77 | return o.printResources(c) 78 | } 79 | 80 | // printResources prints resources lists. 81 | func (o *SQLOptions) printResources(c client.Config) error { 82 | ctx := context.Background() 83 | for _, r := range o.requestedResources { 84 | list, err := c.List(ctx, r) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | err = o.Printer(list) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // printFilteredResources prints filtered resource list. 99 | func (o *SQLOptions) printFilteredResources(c client.Config) error { 100 | ctx := context.Background() 101 | f := filter.Config{ 102 | CheckColumnName: o.checkColumnName, 103 | Query: o.requestedQuery, 104 | } 105 | 106 | // Print resources lists. 107 | for _, r := range o.requestedResources { 108 | list, err := c.List(ctx, r) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | // Filter items by query. 114 | filteredList, err := f.Filter(list) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | err = o.Printer(filteredList) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // checkColumnName checks if a column name has an alias. 129 | func (o *SQLOptions) checkColumnName(s string) (string, error) { 130 | // Check for aliases. 131 | if v, ok := o.defaultAliases[s]; ok { 132 | return v, nil 133 | } 134 | 135 | // If not found in alias table, return the column name unchanged. 136 | return s, nil 137 | } 138 | 139 | // Printer printout a list of items. 140 | func (o *SQLOptions) Printer(items []unstructured.Unstructured) error { 141 | // Sanity check 142 | if len(items) == 0 { 143 | return nil 144 | } 145 | 146 | p := printers.Config{ 147 | TableFields: o.defaultTableFields, 148 | OrderByFields: o.orderByFields, 149 | Limit: o.limit, 150 | Out: o.Out, 151 | ErrOut: o.ErrOut, 152 | NoHeaders: o.noHeaders, 153 | } 154 | 155 | // Print out 156 | switch o.outputFormat { 157 | case "yaml": 158 | return p.YAML(items) 159 | case "json": 160 | return p.JSON(items) 161 | case "name": 162 | return p.Name(items) 163 | default: 164 | err := p.Table(items) 165 | if err != nil { 166 | return err 167 | } 168 | } 169 | 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /pkg/cmd/sql-join.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package cmd 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/spf13/cobra" 28 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 | "k8s.io/client-go/rest" 30 | 31 | "github.com/yaacov/kubectl-sql/pkg/client" 32 | "github.com/yaacov/kubectl-sql/pkg/filter" 33 | ) 34 | 35 | // CompleteJoin sets all information required for updating the current context for join sub command. 36 | func (o *SQLOptions) CompleteJoin(cmd *cobra.Command, args []string) error { 37 | var err error 38 | o.args = args 39 | 40 | if len(o.args) != 5 && len(o.args) != 3 { 41 | return fmt.Errorf(errUsageTemplate, "bad number of arguments") 42 | } 43 | 44 | // Read SQL plugin specific configurations. 45 | if err = o.readConfigFile(o.requestedSQLConfigPath); err != nil { 46 | return err 47 | } 48 | 49 | // join on where 50 | o.requestedResources = strings.Split(o.args[0], ",") 51 | if len(o.requestedResources) != 2 { 52 | return fmt.Errorf(errUsageTemplate, "join command takes exectly two resources") 53 | } 54 | 55 | // Look for "on" 56 | if strings.ToLower(o.args[1]) != "on" { 57 | return fmt.Errorf(errUsageTemplate, "missing \"on\" argument") 58 | } 59 | 60 | // Look for "where" 61 | if len(o.args) == 5 { 62 | if strings.ToLower(o.args[3]) != "where" { 63 | return fmt.Errorf(errUsageTemplate, "missing \"where\" argument") 64 | } 65 | o.requestedQuery = o.args[4] 66 | } 67 | 68 | o.requestedOnQuery = o.args[2] 69 | 70 | return nil 71 | } 72 | 73 | // Join two resource list. 74 | func (o *SQLOptions) Join(config *rest.Config) error { 75 | ctx := context.Background() 76 | var err error 77 | var filteredList []unstructured.Unstructured 78 | 79 | c := client.Config{ 80 | Config: config, 81 | Namespace: o.namespace, 82 | AllNamespaces: o.allNamespaces, 83 | } 84 | 85 | f := filter.Config{ 86 | CheckColumnName: o.checkColumnName, 87 | Query: o.requestedQuery, 88 | } 89 | 90 | // Get the primary resources lists. 91 | list1, err := c.List(ctx, o.requestedResources[0]) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // Get the joined resources lists. 97 | list2, err := c.List(ctx, o.requestedResources[1]) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | // Filter primary list if needed. 103 | if len(o.requestedQuery) > 0 { 104 | // Filter primary items by query. 105 | filteredList, err = f.Filter(list1) 106 | if err != nil { 107 | return err 108 | } 109 | } else { 110 | filteredList = list1 111 | } 112 | 113 | for _, r := range filteredList { 114 | // Print one item. 115 | err := o.Printer([]unstructured.Unstructured{r}) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | // Print separator. 121 | fmt.Fprintf(o.Out, "\n") 122 | 123 | // Print joined items. 124 | if err := o.printJoinedResources(r, list2); err != nil { 125 | return err 126 | } 127 | 128 | // Print separator. 129 | fmt.Fprintf(o.Out, "\n\n\n") 130 | } 131 | 132 | return nil 133 | } 134 | 135 | // printJoinedResources prints joined resource list. 136 | func (o *SQLOptions) printJoinedResources(item unstructured.Unstructured, list2 []unstructured.Unstructured) error { 137 | f := filter.Config{ 138 | CheckColumnName: o.checkColumnName2, 139 | Query: o.requestedOnQuery, 140 | 141 | Prefix1: o.requestedResources[0], 142 | Prefix2: o.requestedResources[1], 143 | Item: item, 144 | } 145 | 146 | // Filter joined items by primary item and "on" query. 147 | filteredList, err := f.Filter2(list2) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | err = o.Printer(filteredList) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | return nil 158 | } 159 | 160 | // checkColumnName2 checks if a coulumn name has an alias using prefixes. 161 | func (o *SQLOptions) checkColumnName2(s string) (string, error) { 162 | var ( 163 | prefix1 = o.requestedResources[0] 164 | prefix2 = o.requestedResources[1] 165 | ) 166 | 167 | if strings.HasPrefix(s, prefix1+".") { 168 | if v, ok := o.defaultAliases[strings.TrimPrefix(s, prefix1+".")]; ok { 169 | return prefix1 + "." + v, nil 170 | } 171 | } else if strings.HasPrefix(s, prefix2+".") { 172 | if v, ok := o.defaultAliases[strings.TrimPrefix(s, prefix2+".")]; ok { 173 | return prefix2 + "." + v, nil 174 | } 175 | } else if v, ok := o.defaultAliases[s]; ok { 176 | return v, nil 177 | } 178 | 179 | // If not found in alias table, return the column name unchanged. 180 | return s, nil 181 | } 182 | -------------------------------------------------------------------------------- /pkg/cmd/sql-sql.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/yaacov/kubectl-sql/pkg/printers" 10 | ) 11 | 12 | // isValidFieldIdentifier checks if a field name matches the allowed pattern 13 | func isValidFieldIdentifier(field string) bool { 14 | // Matches patterns like: 15 | // - simple: name, first_name, my.field 16 | // - array access: items[0], my.array[123] 17 | pattern := `^[a-zA-Z_]([a-zA-Z0-9_.]*(?:\[\d+\])?)*$` 18 | match, _ := regexp.MatchString(pattern, field) 19 | return match 20 | } 21 | 22 | // isValidK8sResourceName checks if a resource name follows Kubernetes naming conventions 23 | func isValidK8sResourceName(resource string) bool { 24 | // Matches lowercase words separated by dots or slashes 25 | // Examples: pods, deployments, apps/v1/deployments 26 | pattern := `^[a-z]+([a-z0-9-]*[a-z0-9])?(/[a-z0-9]+)*$` 27 | match, _ := regexp.MatchString(pattern, resource) 28 | return match 29 | } 30 | 31 | // isValidNamespace checks if a namespace name is valid according to Kubernetes naming conventions 32 | // or if it's the special "*" value for all namespaces 33 | func isValidNamespace(namespace string) bool { 34 | // Special case for "all namespaces" 35 | if namespace == "*" { 36 | return true 37 | } 38 | 39 | pattern := `^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$` 40 | match, _ := regexp.MatchString(pattern, namespace) 41 | return match 42 | } 43 | 44 | // QueryType represents the type of SQL query 45 | type QueryType int 46 | 47 | const ( 48 | SimpleQuery QueryType = iota 49 | JoinQuery 50 | JoinWhereQuery 51 | ) 52 | 53 | // parseFields extracts and validates SELECT fields 54 | func (o *SQLOptions) parseFields(selectFields string) error { 55 | if selectFields == "*" { 56 | return nil 57 | } 58 | 59 | if len(strings.TrimSpace(selectFields)) == 0 { 60 | return fmt.Errorf("SELECT clause cannot be empty") 61 | } 62 | 63 | fields := strings.Split(selectFields, ",") 64 | tableFields := make([]printers.TableField, 0, len(fields)) 65 | 66 | for _, field := range fields { 67 | field = strings.TrimSpace(field) 68 | 69 | // Check for AS syntax 70 | parts := strings.Split(strings.ToUpper(field), " AS ") 71 | var name, title string 72 | 73 | if len(parts) == 2 { 74 | // We have an AS clause 75 | name = strings.TrimSpace(field[:strings.Index(strings.ToUpper(field), " AS ")]) 76 | title = strings.TrimSpace(field[strings.Index(strings.ToUpper(field), " AS ")+4:]) 77 | 78 | if !isValidFieldIdentifier(name) { 79 | return fmt.Errorf("invalid field identifier before AS: %s", name) 80 | } 81 | if !isValidFieldIdentifier(title) { 82 | return fmt.Errorf("invalid field identifier after AS: %s", title) 83 | } 84 | } else { 85 | // No AS clause, use field as both name and title 86 | if !isValidFieldIdentifier(field) { 87 | return fmt.Errorf("invalid field identifier: %s", field) 88 | } 89 | name = field 90 | title = field 91 | } 92 | 93 | // Append to table fields 94 | tableFields = append(tableFields, printers.TableField{ 95 | Name: name, 96 | Title: title, 97 | }) 98 | 99 | // Append to default aliases 100 | o.defaultAliases[title] = name 101 | } 102 | 103 | o.defaultTableFields[printers.SelectedFields] = tableFields 104 | return nil 105 | } 106 | 107 | // parseResources validates and sets the requested resources 108 | func (o *SQLOptions) parseResources(resources []string, queryType QueryType) error { 109 | for i, r := range resources { 110 | r = strings.TrimSpace(r) 111 | 112 | // Split resource on "/" to check for namespace 113 | parts := strings.Split(r, "/") 114 | var resourceName string 115 | 116 | switch len(parts) { 117 | case 1: 118 | resourceName = parts[0] 119 | case 2: 120 | // Check for namespace validity 121 | namespace := parts[0] 122 | if !isValidNamespace(namespace) { 123 | return fmt.Errorf("invalid namespace: %s", namespace) 124 | } 125 | 126 | // Set namespace options 127 | if namespace == "*" { 128 | o.allNamespaces = true 129 | } else { 130 | o.namespace = namespace 131 | } 132 | resourceName = parts[1] 133 | default: 134 | return fmt.Errorf("invalid resource format: %s, expected [namespace/]resource or */resource for all namespaces", r) 135 | } 136 | 137 | if !isValidK8sResourceName(resourceName) { 138 | return fmt.Errorf("invalid resource name: %s", resourceName) 139 | } 140 | 141 | resources[i] = resourceName 142 | } 143 | 144 | if queryType == SimpleQuery && len(resources) != 1 { 145 | return fmt.Errorf("without ON clause, exactly one resource must be specified") 146 | } 147 | if (queryType == JoinQuery || queryType == JoinWhereQuery) && len(resources) != 2 { 148 | return fmt.Errorf("when using ON clause, exactly two resources must be specified") 149 | } 150 | 151 | o.requestedResources = resources 152 | return nil 153 | } 154 | 155 | // identifyQueryType determines the type of SQL query and returns relevant indices 156 | func (o *SQLOptions) identifyQueryType(query string) (QueryType, map[string]int, error) { 157 | upperQuery := strings.ToUpper(query) 158 | if !strings.HasPrefix(upperQuery, "SELECT") { 159 | return SimpleQuery, nil, fmt.Errorf("query must start with SELECT") 160 | } 161 | 162 | indices := map[string]int{ 163 | "SELECT": 0, 164 | "FROM": strings.Index(upperQuery, " FROM "), 165 | "JOIN": strings.Index(upperQuery, " JOIN "), 166 | "ON": strings.Index(upperQuery, " ON "), 167 | "WHERE": strings.Index(upperQuery, " WHERE "), 168 | "ORDER BY": strings.Index(upperQuery, " ORDER BY "), 169 | "LIMIT": strings.Index(upperQuery, " LIMIT "), 170 | } 171 | 172 | if indices["FROM"] == -1 { 173 | return 0, nil, fmt.Errorf("missing FROM clause in query") 174 | } 175 | 176 | if indices["JOIN"] == -1 { 177 | return SimpleQuery, indices, nil 178 | } 179 | 180 | if indices["ON"] == -1 { 181 | return 0, nil, fmt.Errorf("JOIN clause requires ON condition") 182 | } 183 | 184 | if indices["WHERE"] == -1 { 185 | return JoinQuery, indices, nil 186 | } 187 | 188 | return JoinWhereQuery, indices, nil 189 | } 190 | 191 | // parseOrderBy extracts and validates the ORDER BY clause 192 | func (o *SQLOptions) parseOrderBy(query string, indices map[string]int) error { 193 | if indices["ORDER BY"] == -1 { 194 | return nil 195 | } 196 | 197 | orderByStart := indices["ORDER BY"] + 9 198 | var orderByEnd int 199 | if indices["LIMIT"] != -1 { 200 | orderByEnd = indices["LIMIT"] 201 | } else { 202 | orderByEnd = len(query) 203 | } 204 | 205 | orderByStr := strings.TrimSpace(query[orderByStart:orderByEnd]) 206 | if orderByStr == "" { 207 | return fmt.Errorf("ORDER BY clause cannot be empty") 208 | } 209 | 210 | fields := strings.Split(orderByStr, ",") 211 | orderByFields := make([]printers.OrderByField, 0, len(fields)) 212 | 213 | for _, field := range fields { 214 | field = strings.TrimSpace(field) 215 | if field == "" { 216 | continue 217 | } 218 | 219 | parts := strings.Fields(field) 220 | if len(parts) == 0 { 221 | continue 222 | } 223 | 224 | fieldName := parts[0] 225 | // Check for possible alias 226 | if alias, err := o.checkColumnName(fieldName); err == nil { 227 | fieldName = alias 228 | } 229 | 230 | orderBy := printers.OrderByField{ 231 | Name: fieldName, 232 | Descending: false, 233 | } 234 | 235 | // Check for DESC/ASC modifier 236 | if len(parts) > 1 && strings.ToUpper(parts[1]) == "DESC" { 237 | orderBy.Descending = true 238 | } 239 | 240 | orderByFields = append(orderByFields, orderBy) 241 | } 242 | 243 | o.orderByFields = orderByFields 244 | return nil 245 | } 246 | 247 | // parseLimit extracts and validates the LIMIT clause 248 | func (o *SQLOptions) parseLimit(query string, indices map[string]int) error { 249 | if indices["LIMIT"] == -1 { 250 | return nil 251 | } 252 | 253 | limitStart := indices["LIMIT"] + 6 254 | limitStr := strings.TrimSpace(query[limitStart:]) 255 | 256 | // Check if there are other clauses after LIMIT 257 | if space := strings.Index(limitStr, " "); space != -1 { 258 | limitStr = limitStr[:space] 259 | } 260 | 261 | limit, err := strconv.Atoi(limitStr) 262 | if err != nil { 263 | return fmt.Errorf("invalid LIMIT value: %s", limitStr) 264 | } 265 | 266 | if limit < 0 { 267 | return fmt.Errorf("LIMIT cannot be negative: %d", limit) 268 | } 269 | 270 | o.limit = limit 271 | return nil 272 | } 273 | 274 | // parseQueryParts extracts and validates different parts of the query 275 | func (o *SQLOptions) parseQueryParts(query string, indices map[string]int, queryType QueryType) error { 276 | // Parse FROM resource (only one resource allowed) 277 | var fromEnd int 278 | if indices["JOIN"] != -1 { 279 | fromEnd = indices["JOIN"] 280 | } else if indices["WHERE"] != -1 { 281 | fromEnd = indices["WHERE"] 282 | } else if indices["ORDER BY"] != -1 { 283 | fromEnd = indices["ORDER BY"] 284 | } else if indices["LIMIT"] != -1 { 285 | fromEnd = indices["LIMIT"] 286 | } else { 287 | fromEnd = len(query) 288 | } 289 | 290 | fromPart := strings.TrimSpace(query[indices["FROM"]+5 : fromEnd]) 291 | resources := strings.Split(fromPart, ",") 292 | if len(resources) != 1 { 293 | return fmt.Errorf("only one resource allowed in FROM clause") 294 | } 295 | 296 | // If JOIN exists, add the joined resource 297 | var allResources []string 298 | if queryType != SimpleQuery { 299 | joinStart := indices["JOIN"] + 5 300 | joinEnd := indices["ON"] 301 | joinResource := strings.TrimSpace(query[joinStart:joinEnd]) 302 | allResources = []string{resources[0], joinResource} 303 | } else { 304 | allResources = []string{resources[0]} 305 | } 306 | 307 | if err := o.parseResources(allResources, queryType); err != nil { 308 | return err 309 | } 310 | 311 | // Parse SELECT fields 312 | selectFields := strings.TrimSpace(query[6:indices["FROM"]]) 313 | if err := o.parseFields(selectFields); err != nil { 314 | return err 315 | } 316 | 317 | // Parse ON clause for JOIN queries 318 | if queryType != SimpleQuery { 319 | onStart := indices["ON"] + 3 320 | var onEnd int 321 | if indices["WHERE"] != -1 { 322 | onEnd = indices["WHERE"] 323 | } else if indices["ORDER BY"] != -1 { 324 | onEnd = indices["ORDER BY"] 325 | } else if indices["LIMIT"] != -1 { 326 | onEnd = indices["LIMIT"] 327 | } else { 328 | onEnd = len(query) 329 | } 330 | o.requestedOnQuery = strings.TrimSpace(query[onStart:onEnd]) 331 | if o.requestedOnQuery == "" { 332 | return fmt.Errorf("ON clause cannot be empty") 333 | } 334 | } 335 | 336 | // Parse WHERE clause if present 337 | if indices["WHERE"] != -1 { 338 | whereStart := indices["WHERE"] + 6 339 | var whereEnd int 340 | if indices["ORDER BY"] != -1 { 341 | whereEnd = indices["ORDER BY"] 342 | } else if indices["LIMIT"] != -1 { 343 | whereEnd = indices["LIMIT"] 344 | } else { 345 | whereEnd = len(query) 346 | } 347 | wherePart := strings.TrimSpace(query[whereStart:whereEnd]) 348 | if wherePart == "" { 349 | return fmt.Errorf("WHERE clause cannot be empty") 350 | } 351 | o.requestedQuery = wherePart 352 | } 353 | 354 | // Parse ORDER BY clause if present 355 | if err := o.parseOrderBy(query, indices); err != nil { 356 | return err 357 | } 358 | 359 | // Parse LIMIT clause if present 360 | if err := o.parseLimit(query, indices); err != nil { 361 | return err 362 | } 363 | 364 | return nil 365 | } 366 | 367 | // CompleteSQL parses SQL query into components 368 | func (o *SQLOptions) CompleteSQL(query string) error { 369 | // Read SQL plugin specific configurations 370 | err := o.readConfigFile(o.requestedSQLConfigPath) 371 | if err != nil { 372 | return err 373 | } 374 | 375 | queryType, indices, err := o.identifyQueryType(query) 376 | if err != nil { 377 | return err 378 | } 379 | 380 | if err := o.parseQueryParts(query, indices, queryType); err != nil { 381 | return err 382 | } 383 | 384 | return nil 385 | } 386 | -------------------------------------------------------------------------------- /pkg/cmd/sql-version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | 25 | "k8s.io/client-go/discovery" 26 | "k8s.io/client-go/rest" 27 | ) 28 | 29 | // Version prints the plugin version. 30 | func (o *SQLOptions) Version(config *rest.Config) error { 31 | serverVersionStr := "" 32 | 33 | discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | serverVersion, err := discoveryClient.ServerVersion() 39 | if err == nil { 40 | serverVersionStr = fmt.Sprintf("%v", serverVersion) 41 | } 42 | 43 | fmt.Fprintf(o.Out, "Client version: %s\n", clientVersion) 44 | fmt.Fprintf(o.Out, "Server version: %s\n", serverVersionStr) 45 | fmt.Fprintf(o.Out, "Current namespace: %s\n", o.namespace) 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/cmd/sql.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | "sort" 26 | "strings" 27 | 28 | "github.com/spf13/cobra" 29 | "k8s.io/cli-runtime/pkg/genericclioptions" 30 | ) 31 | 32 | // NewSQLOptions provides an instance of SQLOptions with default values 33 | func NewSQLOptions(streams genericclioptions.IOStreams) *SQLOptions { 34 | options := &SQLOptions{ 35 | configFlags: genericclioptions.NewConfigFlags(true), 36 | IOStreams: streams, 37 | outputFormat: "table", 38 | } 39 | 40 | // Look for a default kubectl-sql.json config file. 41 | if home, err := os.UserHomeDir(); err == nil { 42 | options.defaultSQLConfigPath = fmt.Sprintf("%s/.kube/kubectl-sql.json", home) 43 | } 44 | 45 | return options 46 | } 47 | 48 | // NewCmdSQL provides a cobra command wrapping SQLOptions 49 | func NewCmdSQL(streams genericclioptions.IOStreams) *cobra.Command { 50 | o := NewSQLOptions(streams) 51 | 52 | cmd := &cobra.Command{ 53 | Use: "sql [query|command] [flags] [options]", 54 | Short: "Query Kubernetes resources using SQL-like syntax or subcommands", 55 | Long: sqlCmdLong, 56 | Example: sqlCmdExample, 57 | TraverseChildren: true, 58 | RunE: func(c *cobra.Command, args []string) error { 59 | if len(args) == 0 { 60 | return fmt.Errorf(errUsageTemplate, "missing query or sub command") 61 | } 62 | 63 | // If the first argument starts with "SELECT", treat it as an SQL query 64 | if strings.HasPrefix(strings.ToUpper(args[0]), "SELECT") { 65 | if err := o.Complete(c, args); err != nil { 66 | return err 67 | } 68 | 69 | if err := o.CompleteSQL(args[0]); err != nil { 70 | return err 71 | } 72 | 73 | if err := o.Validate(); err != nil { 74 | return err 75 | } 76 | 77 | config, err := o.rawConfig.ClientConfig() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | // Execute query based on number of resources and presence of ON clause 83 | switch { 84 | case len(o.requestedResources) == 2 && o.requestedOnQuery != "": 85 | return o.Join(config) 86 | case len(o.requestedResources) >= 1: 87 | return o.Get(config) 88 | default: 89 | return fmt.Errorf("invalid number of resources in query") 90 | } 91 | } 92 | 93 | // If not an SQL query, show error about missing subcommand 94 | return fmt.Errorf(errUsageTemplate, "missing sub command") 95 | }, 96 | } 97 | 98 | cmdGet := &cobra.Command{ 99 | Use: "get [where \"\"] [flags] [options]", 100 | Short: sqlGetShort, 101 | Long: sqlGetLong, 102 | Example: sqlGetExample, 103 | TraverseChildren: true, 104 | SilenceUsage: true, 105 | RunE: func(c *cobra.Command, args []string) error { 106 | if err := o.Complete(c, args); err != nil { 107 | return err 108 | } 109 | 110 | if err := o.CompleteGet(c, args); err != nil { 111 | return err 112 | } 113 | 114 | if err := o.Validate(); err != nil { 115 | return err 116 | } 117 | 118 | config, err := o.rawConfig.ClientConfig() 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if err := o.Get(config); err != nil { 124 | return err 125 | } 126 | 127 | return nil 128 | }, 129 | } 130 | 131 | cmdJoin := &cobra.Command{ 132 | Use: "join on \"\" [where \"\"] [flags] [options]", 133 | Short: sqlJoinShort, 134 | Long: sqlJoinLong, 135 | Example: sqlJoinExample, 136 | TraverseChildren: true, 137 | SilenceUsage: true, 138 | RunE: func(c *cobra.Command, args []string) error { 139 | if err := o.Complete(c, args); err != nil { 140 | return err 141 | } 142 | 143 | if err := o.CompleteJoin(c, args); err != nil { 144 | return err 145 | } 146 | 147 | if err := o.Validate(); err != nil { 148 | return err 149 | } 150 | 151 | config, err := o.rawConfig.ClientConfig() 152 | if err != nil { 153 | return err 154 | } 155 | 156 | if err := o.Join(config); err != nil { 157 | return err 158 | } 159 | 160 | return nil 161 | }, 162 | } 163 | 164 | cmdAliases := &cobra.Command{ 165 | Use: "aliases [flags] [options]", 166 | Short: "Display a list of currently used aliases", 167 | SilenceUsage: true, 168 | RunE: func(c *cobra.Command, args []string) error { 169 | if err := o.Complete(c, args); err != nil { 170 | return err 171 | } 172 | 173 | // Read SQL plugin specific configurations. 174 | if err := o.readConfigFile(o.requestedSQLConfigPath); err != nil { 175 | return err 176 | } 177 | 178 | hardCodedAliases := map[string]string{ 179 | "name": "resource name", 180 | "namespace": "resource namespace", 181 | "created": "resource creation time", 182 | "deleted": "resource delition time", 183 | } 184 | 185 | fmt.Fprintf(o.Out, "%-12s\t%s\n", "ALIAS", "PATH") 186 | 187 | // Print hard coded alias table. 188 | for k, v := range hardCodedAliases { 189 | fmt.Fprintf(o.Out, "%-12s\t%s\n", k, v) 190 | } 191 | 192 | // Print alias table, sorted by keys. 193 | keys := sortedKeys(o.defaultAliases) 194 | for _, k := range keys { 195 | fmt.Fprintf(o.Out, "%-12s\t%s\n", k, o.defaultAliases[k]) 196 | } 197 | 198 | return nil 199 | }, 200 | } 201 | 202 | cmdVersion := &cobra.Command{ 203 | Use: "version [flags]", 204 | Short: "Print the SQL client and server version information", 205 | SilenceUsage: true, 206 | RunE: func(c *cobra.Command, args []string) error { 207 | if err := o.Complete(c, args); err != nil { 208 | return err 209 | } 210 | 211 | if err := o.Validate(); err != nil { 212 | return err 213 | } 214 | 215 | config, err := o.rawConfig.ClientConfig() 216 | if err != nil { 217 | return err 218 | } 219 | 220 | if err := o.Version(config); err != nil { 221 | return err 222 | } 223 | 224 | return nil 225 | }, 226 | } 227 | 228 | cmd.AddCommand(cmdGet, cmdJoin, cmdVersion, cmdAliases) 229 | 230 | cmd.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", o.allNamespaces, 231 | "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") 232 | cmd.Flags().StringVarP(&o.requestedSQLConfigPath, "kubectl-sql", "q", o.defaultSQLConfigPath, 233 | "Path to the kubectl-sql.json file to use for kubectl-sql requests.") 234 | cmd.Flags().StringVarP(&o.outputFormat, "output", "o", o.outputFormat, 235 | "Output format. One of: json|yaml|table|name") 236 | cmd.Flags().BoolVarP(&o.noHeaders, "no-headers", "H", false, 237 | "When using the table output format, don't print headers (column titles)") 238 | 239 | o.configFlags.AddFlags(cmd.Flags()) 240 | 241 | cmdGet.Flags().AddFlagSet(cmd.Flags()) 242 | cmdJoin.Flags().AddFlagSet(cmd.Flags()) 243 | cmdAliases.Flags().AddFlagSet(cmd.Flags()) 244 | cmdVersion.Flags().AddFlagSet(cmd.Flags()) 245 | 246 | return cmd 247 | } 248 | 249 | // Complete sets all information required for updating the current context 250 | func (o *SQLOptions) Complete(cmd *cobra.Command, args []string) error { 251 | var err error 252 | o.args = args 253 | 254 | o.rawConfig = o.configFlags.ToRawKubeConfigLoader() 255 | if o.namespace, _, err = o.rawConfig.Namespace(); err != nil { 256 | return err 257 | } 258 | 259 | return nil 260 | } 261 | 262 | // Validate ensures that all required arguments and flag values are provided 263 | func (o *SQLOptions) Validate() error { 264 | formatOptions := map[string]bool{"table": true, "json": true, "yaml": true, "name": true} 265 | 266 | if _, ok := formatOptions[o.outputFormat]; !ok { 267 | return fmt.Errorf("output format must be one of: json|yaml|table|name") 268 | } 269 | 270 | return nil 271 | } 272 | 273 | // Get sorted map keys 274 | func sortedKeys(m map[string]string) []string { 275 | keys := make([]string, 0, len(m)) 276 | for k := range m { 277 | keys = append(keys, k) 278 | } 279 | sort.Strings(keys) 280 | 281 | return keys 282 | } 283 | -------------------------------------------------------------------------------- /pkg/eval/eval.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package eval 21 | 22 | import ( 23 | "bytes" 24 | "encoding/json" 25 | "fmt" 26 | "strconv" 27 | "strings" 28 | "time" 29 | 30 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 | "k8s.io/client-go/util/jsonpath" 32 | ) 33 | 34 | // ExtractValue extract a value from an item using a key. 35 | func ExtractValue(item unstructured.Unstructured, key string) (interface{}, bool) { 36 | // Check for reserved words. 37 | switch key { 38 | case "name": 39 | return item.GetName(), true 40 | case "namespace": 41 | return item.GetNamespace(), true 42 | case "created": 43 | return item.GetCreationTimestamp().Time.UTC(), true 44 | case "deleted": 45 | return item.GetDeletionTimestamp().Time.UTC(), true 46 | } 47 | 48 | // Check for labels and annotations. 49 | if strings.HasPrefix(key, "labels.") { 50 | value, ok := item.GetLabels()[key[7:]] 51 | return handleMetadataValue(value, ok) 52 | } 53 | 54 | if strings.HasPrefix(key, "annotations.") { 55 | value, ok := item.GetAnnotations()[key[12:]] 56 | return handleMetadataValue(value, ok) 57 | } 58 | 59 | // Use Kubernetes JSONPath implementation 60 | // Format the key as a proper JSONPath expression if it's not already 61 | if !strings.HasPrefix(key, "{") { 62 | key = fmt.Sprintf("{.%s}", key) 63 | } 64 | 65 | // Check if the path contains a wildcard pattern 66 | hasWildcard := strings.Contains(key, "[*]") || strings.Contains(key, "..") || 67 | strings.Contains(key, "*") || strings.Contains(key, "?") 68 | 69 | j := jsonpath.New("extract-value") 70 | if err := j.Parse(key); err != nil { 71 | return nil, true 72 | } 73 | 74 | buf := &bytes.Buffer{} 75 | if err := j.Execute(buf, item.Object); err != nil { 76 | return nil, true 77 | } 78 | 79 | // If there's no output, the path doesn't exist 80 | if buf.Len() == 0 { 81 | return nil, true 82 | } 83 | 84 | // Parse the result 85 | var result interface{} 86 | if err := json.Unmarshal(buf.Bytes(), &result); err != nil { 87 | trimmedStr := strings.TrimSpace(buf.String()) 88 | 89 | if hasWildcard { 90 | // If the path has a wildcard but the result couldn't be unmarshaled as JSON, 91 | // split by spaces and create an array 92 | parts := strings.Fields(trimmedStr) 93 | convertedArray := make([]interface{}, len(parts)) 94 | for i, part := range parts { 95 | convertedArray[i] = inferValue(part) 96 | } 97 | return convertedArray, true 98 | } 99 | 100 | convertedValue, _ := convertObjectToValue(trimmedStr) 101 | return convertedValue, true 102 | } 103 | 104 | // If wildcard is present, ensure we return an array 105 | if hasWildcard { 106 | switch v := result.(type) { 107 | case []interface{}: 108 | // Already an array, convert each element 109 | convertedArray := make([]interface{}, len(v)) 110 | for i, item := range v { 111 | convertedArray[i], _ = convertObjectToValue(item) 112 | } 113 | return convertedArray, true 114 | default: 115 | // Convert to array with single element 116 | converted, _ := convertObjectToValue(result) 117 | return []interface{}{converted}, true 118 | } 119 | } 120 | 121 | // If result is a single value array or map with one entry, extract it 122 | switch v := result.(type) { 123 | case []interface{}: 124 | if len(v) == 0 { 125 | return []interface{}{}, true 126 | } 127 | 128 | // Convert each element in the array 129 | convertedArray := make([]interface{}, len(v)) 130 | for i, item := range v { 131 | convertedArray[i], _ = convertObjectToValue(item) 132 | } 133 | return convertedArray, true 134 | } 135 | 136 | return convertObjectToValue(result) 137 | } 138 | 139 | func handleMetadataValue(value string, exists bool) (interface{}, bool) { 140 | if !exists { 141 | return nil, true 142 | } 143 | if len(value) == 0 { 144 | return true, true 145 | } 146 | return inferValue(value), true 147 | } 148 | 149 | func convertObjectToValue(object interface{}) (interface{}, bool) { 150 | switch v := object.(type) { 151 | case bool: 152 | return v, true 153 | case float64: 154 | return v, true 155 | case int64: 156 | return float64(v), true 157 | case string: 158 | return inferValue(v), true 159 | } 160 | return nil, true 161 | } 162 | 163 | // inferValue attempts to convert a string to its most appropriate type: 164 | // bool, int, float, date, or keeps it as string if no conversion works 165 | func inferValue(s string) interface{} { 166 | // Try to parse as boolean 167 | if strings.ToLower(s) == "true" { 168 | return true 169 | } 170 | if strings.ToLower(s) == "false" { 171 | return false 172 | } 173 | 174 | // Try to parse as integer 175 | if i, err := strconv.ParseInt(s, 10, 64); err == nil { 176 | return float64(i) // Using float64 for consistency 177 | } 178 | 179 | // Try to parse as float 180 | if f, err := strconv.ParseFloat(s, 64); err == nil { 181 | return f 182 | } 183 | 184 | // Try to parse as date (RFC3339 format) 185 | if t, err := time.Parse(time.RFC3339, s); err == nil { 186 | return t 187 | } 188 | 189 | // Try additional date formats 190 | dateFormats := []string{ 191 | "2006-01-02", 192 | "2006-01-02 15:04:05", 193 | "2006-01-02T15:04:05", 194 | "2006/01/02", 195 | "01/02/2006", 196 | time.RFC822, 197 | time.RFC1123, 198 | } 199 | 200 | for _, format := range dateFormats { 201 | if t, err := time.Parse(format, s); err == nil { 202 | return t 203 | } 204 | } 205 | 206 | // Default to string 207 | return stringValue(s) 208 | } 209 | -------------------------------------------------------------------------------- /pkg/eval/eval_test.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | ) 10 | 11 | func TestExtractValue(t *testing.T) { 12 | creationTime := time.Now().UTC().Truncate(time.Second) 13 | deletionTime := creationTime.Add(time.Hour) 14 | 15 | item := unstructured.Unstructured{ 16 | Object: map[string]interface{}{ 17 | "metadata": map[string]interface{}{ 18 | "name": "test-pod", 19 | "namespace": "default", 20 | "creationTimestamp": creationTime.Format(time.RFC3339), 21 | "deletionTimestamp": deletionTime.Format(time.RFC3339), 22 | "labels": map[string]interface{}{ 23 | "app": "test", 24 | }, 25 | "annotations": map[string]interface{}{ 26 | "note": "test annotation", 27 | }, 28 | }, 29 | "spec": map[string]interface{}{ 30 | "replicas": int64(3), 31 | "nested": map[string]interface{}{ 32 | "value": "nested-value", 33 | }, 34 | "containers": []interface{}{ 35 | map[string]interface{}{ 36 | "name": "container1", 37 | "image": "nginx:latest", 38 | "ports": []interface{}{ 39 | map[string]interface{}{ 40 | "containerPort": int64(80), 41 | "protocol": "TCP", 42 | }, 43 | map[string]interface{}{ 44 | "containerPort": int64(443), 45 | "protocol": "TCP", 46 | }, 47 | }, 48 | "resources": map[string]interface{}{ 49 | "limits": map[string]interface{}{ 50 | "cpu": "500m", 51 | "memory": "512Mi", 52 | }, 53 | }, 54 | }, 55 | map[string]interface{}{ 56 | "name": "container2", 57 | "image": "redis:latest", 58 | "ports": []interface{}{ 59 | map[string]interface{}{ 60 | "containerPort": int64(6379), 61 | "protocol": "TCP", 62 | }, 63 | }, 64 | }, 65 | }, 66 | "volumes": []interface{}{ 67 | map[string]interface{}{ 68 | "name": "data", 69 | "configMap": map[string]interface{}{ 70 | "name": "config-data", 71 | }, 72 | }, 73 | }, 74 | }, 75 | "status": map[string]interface{}{ 76 | "phase": "Running", 77 | "conditions": []interface{}{ 78 | map[string]interface{}{ 79 | "type": "Ready", 80 | "status": "True", 81 | }, 82 | map[string]interface{}{ 83 | "type": "PodScheduled", 84 | "status": "True", 85 | }, 86 | }, 87 | "podIP": "10.0.0.1", 88 | "hostIP": "192.168.1.1", 89 | "ready": true, 90 | "startTime": creationTime.Add(time.Minute).Format(time.RFC3339), 91 | "containerStatuses": []interface{}{ 92 | map[string]interface{}{ 93 | "name": "container1", 94 | "ready": true, 95 | "restartCount": int64(0), 96 | "started": true, 97 | }, 98 | map[string]interface{}{ 99 | "name": "container2", 100 | "ready": true, 101 | "restartCount": int64(2), 102 | "started": true, 103 | }, 104 | }, 105 | "metrics": map[string]interface{}{ 106 | "cpu": map[string]interface{}{"usage": "250m"}, 107 | "memory": map[string]interface{}{"usage": "256Mi"}, 108 | }, 109 | "numericValues": []interface{}{1, 2, 3, 4, 5}, 110 | "mixedArray": []interface{}{ 111 | "string", 112 | 42, 113 | true, 114 | map[string]interface{}{"key": "value"}, 115 | []interface{}{1, 2, 3}, 116 | }, 117 | }, 118 | }, 119 | } 120 | 121 | tests := []struct { 122 | name string 123 | key string 124 | want interface{} 125 | wantBool bool 126 | }{ 127 | {"name", "name", "test-pod", true}, 128 | {"namespace", "namespace", "default", true}, 129 | {"created", "created", creationTime.UTC(), true}, 130 | {"deleted", "deleted", deletionTime.UTC(), true}, 131 | {"label", "labels.app", "test", true}, 132 | {"annotation", "annotations.note", "test annotation", true}, 133 | {"nested spec", "spec.nested.value", "nested-value", true}, 134 | {"replicas", "spec.replicas", float64(3), true}, 135 | {"non-existent", "invalid.path", nil, true}, 136 | 137 | // Test array indexing 138 | {"container name", "spec.containers[0].name", "container1", true}, 139 | {"container image", "spec.containers[0].image", "nginx:latest", true}, 140 | {"second container", "spec.containers[1].name", "container2", true}, 141 | 142 | // Test nested arrays 143 | {"container port", "spec.containers[0].ports[0].containerPort", float64(80), true}, 144 | {"second port", "spec.containers[0].ports[1].containerPort", float64(443), true}, 145 | 146 | // Test complex nested objects 147 | {"resource limits", "spec.containers[0].resources.limits.cpu", "500m", true}, 148 | {"volume configmap", "spec.volumes[0].configMap.name", "config-data", true}, 149 | 150 | // Test booleans 151 | {"pod ready", "status.ready", true, true}, 152 | {"container ready", "status.containerStatuses[0].ready", true, true}, 153 | 154 | // Test status fields 155 | {"pod phase", "status.phase", "Running", true}, 156 | {"pod IP", "status.podIP", "10.0.0.1", true}, 157 | 158 | // Test conditions 159 | {"condition type", "status.conditions[0].type", "Ready", true}, 160 | 161 | // Test more complex jsonpath expressions 162 | {"all container names", "spec.containers[*].name", []interface{}{"container1", "container2"}, true}, 163 | {"all container ports", "spec.containers[0].ports[*].containerPort", []interface{}{float64(80), float64(443)}, true}, 164 | {"restart counts", "status.containerStatuses[*].restartCount", []interface{}{float64(0), float64(2)}, true}, 165 | 166 | // Test numeric arrays 167 | {"numeric values", "status.numericValues", []interface{}{float64(1), float64(2), float64(3), float64(4), float64(5)}, true}, 168 | {"first numeric value", "status.numericValues[0]", float64(1), true}, 169 | 170 | // Test mixed arrays 171 | {"mixed array string", "status.mixedArray[0]", "string", true}, 172 | {"mixed array number", "status.mixedArray[1]", float64(42), true}, 173 | {"mixed array boolean", "status.mixedArray[2]", true, true}, 174 | {"mixed array object", "status.mixedArray[3].key", "value", true}, 175 | 176 | // Test deep nesting 177 | {"metrics cpu", "status.metrics.cpu.usage", "250m", true}, 178 | } 179 | 180 | for _, tt := range tests { 181 | t.Run(tt.name, func(t *testing.T) { 182 | got, got1 := ExtractValue(item, tt.key) 183 | 184 | // Use reflect.DeepEqual for comparing values, especially arrays/slices 185 | if !reflect.DeepEqual(got, tt.want) { 186 | t.Errorf("extractValue() got = %v (type %T), want %v (type %T)", got, got, tt.want, tt.want) 187 | } 188 | if got1 != tt.wantBool { 189 | t.Errorf("extractValue() got1 = %v, want %v", got1, tt.wantBool) 190 | } 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /pkg/eval/factory.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yaacov/tree-search-language/v6/pkg/walkers/semantics" 7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 8 | ) 9 | 10 | // EvalFunctionFactory build an evaluation method for one item that returns a value using a key. 11 | func EvalFunctionFactory(item unstructured.Unstructured) semantics.EvalFunc { 12 | return func(key string) (interface{}, bool) { 13 | return ExtractValue(item, key) 14 | } 15 | } 16 | 17 | // JoinEvalFunctionFactory build an evaluation method for two items that returns a value using a key. 18 | func JoinEvalFunctionFactory(item1, item2 unstructured.Unstructured, prefix1, prefix2 string) semantics.EvalFunc { 19 | return func(key string) (interface{}, bool) { 20 | // Use item1 if has prefix1 21 | if strings.HasPrefix(key, prefix1+".") { 22 | return ExtractValue(item1, strings.TrimPrefix(key, prefix1+".")) 23 | } 24 | 25 | // Use item2 if has prefix2 26 | if strings.HasPrefix(key, prefix2+".") { 27 | return ExtractValue(item2, strings.TrimPrefix(key, prefix2+".")) 28 | } 29 | 30 | // Default to use item2 31 | return ExtractValue(item2, key) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/eval/string.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // stringValue parses a string to appropriate type (number, boolean, date, or string) 10 | func stringValue(str string) interface{} { 11 | if v := parseNumber(str); v != nil { 12 | return v 13 | } 14 | if v := parseSINumber(str); v != nil { 15 | return v 16 | } 17 | if v := parseBoolean(str); v != nil { 18 | return *v 19 | } 20 | if v := parseDate(str); v != nil { 21 | return *v 22 | } 23 | return str 24 | } 25 | 26 | func parseNumber(str string) interface{} { 27 | // Try parsing as integer first 28 | if i, err := strconv.ParseInt(str, 10, 64); err == nil { 29 | return i 30 | } 31 | // Try parsing as float 32 | if f, err := strconv.ParseFloat(str, 64); err == nil { 33 | return f 34 | } 35 | return nil 36 | } 37 | 38 | func parseSINumber(s string) interface{} { 39 | multiplier := 0.0 40 | base := 1000.0 41 | 42 | // Check for binary prefix 43 | if len(s) > 1 && s[len(s)-1:] == "i" { 44 | base = 1024.0 45 | s = s[:len(s)-1] 46 | } 47 | 48 | // Check for SI postfix 49 | if len(s) > 1 { 50 | postfix := s[len(s)-1:] 51 | switch postfix { 52 | case "K": 53 | multiplier = base 54 | case "M": 55 | multiplier = math.Pow(base, 2) 56 | case "G": 57 | multiplier = math.Pow(base, 3) 58 | case "T": 59 | multiplier = math.Pow(base, 4) 60 | case "P": 61 | multiplier = math.Pow(base, 5) 62 | } 63 | 64 | if multiplier >= 1.0 { 65 | s = s[:len(s)-1] 66 | if i, err := strconv.ParseInt(s, 10, 64); err == nil { 67 | return float64(i) * multiplier 68 | } 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func parseBoolean(str string) *bool { 76 | switch str { 77 | case "true", "True": 78 | v := true 79 | return &v 80 | case "false", "False": 81 | v := false 82 | return &v 83 | } 84 | return nil 85 | } 86 | 87 | func parseDate(str string) *time.Time { 88 | if t, err := time.Parse(time.RFC3339, str); err == nil { 89 | t = t.UTC() 90 | return &t 91 | } 92 | if t, err := time.Parse("2006-01-02", str); err == nil { 93 | t = t.UTC() 94 | return &t 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /pkg/eval/string_test.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestStringValue(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | input string 12 | expected interface{} 13 | }{ 14 | {"integer", "123", int64(123)}, 15 | {"float", "123.45", float64(123.45)}, 16 | {"boolean true", "true", true}, 17 | {"boolean True", "True", true}, 18 | {"boolean false", "false", false}, 19 | {"date RFC3339", "2020-01-02T15:04:05Z", func() time.Time { t, _ := time.Parse(time.RFC3339, "2020-01-02T15:04:05Z"); return t }()}, 20 | {"date short", "2020-01-02", func() time.Time { t, _ := time.Parse("2006-01-02", "2020-01-02"); return t }()}, 21 | {"string", "hello", "hello"}, 22 | } 23 | 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | got := stringValue(tt.input) 27 | if got != tt.expected { 28 | t.Errorf("stringValue(%s) = %v; want %v", tt.input, got, tt.expected) 29 | } 30 | }) 31 | } 32 | } 33 | 34 | func TestParseSINumber(t *testing.T) { 35 | tests := []struct { 36 | name string 37 | input string 38 | expected interface{} 39 | }{ 40 | {"kilobyte", "1K", float64(1000)}, 41 | {"kibibyte", "1Ki", float64(1024)}, 42 | {"megabyte", "1M", float64(1000000)}, 43 | {"mebibyte", "1Mi", float64(1048576)}, 44 | {"gigabyte", "1G", float64(1000000000)}, 45 | {"terabyte", "1T", float64(1000000000000)}, 46 | {"petabyte", "1P", float64(1000000000000000)}, 47 | {"invalid", "1X", nil}, 48 | {"not SI", "123", nil}, 49 | } 50 | 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | got := parseSINumber(tt.input) 54 | if got != tt.expected { 55 | t.Errorf("parseSINumber(%s) = %v; want %v", tt.input, got, tt.expected) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/filter/filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package filter 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | 25 | "github.com/yaacov/tree-search-language/v6/pkg/tsl" 26 | "github.com/yaacov/tree-search-language/v6/pkg/walkers/ident" 27 | "github.com/yaacov/tree-search-language/v6/pkg/walkers/semantics" 28 | 29 | "github.com/yaacov/kubectl-sql/pkg/eval" 30 | ) 31 | 32 | // Config provides information required filter item list by query. 33 | type Config struct { 34 | CheckColumnName func(s string) (string, error) 35 | Query string 36 | 37 | Prefix1 string 38 | Prefix2 string 39 | Item unstructured.Unstructured 40 | } 41 | 42 | // Filter filters items using query. 43 | func (c *Config) Filter(list []unstructured.Unstructured) ([]unstructured.Unstructured, error) { 44 | var ( 45 | tree *tsl.TSLNode 46 | err error 47 | ) 48 | 49 | // If we have a query, prepare the search tree. 50 | tree, err = tsl.ParseTSL(c.Query) 51 | if err != nil { 52 | return nil, err 53 | } 54 | defer tree.Free() 55 | 56 | // Check and replace user identifiers if alias exist. 57 | newTree, err := ident.Walk(tree, c.CheckColumnName) 58 | if err != nil { 59 | return nil, err 60 | } 61 | defer newTree.Free() 62 | 63 | // Filter items using a query. 64 | items := []unstructured.Unstructured{} 65 | for _, item := range list { 66 | // If we have a query, check item. 67 | matchingFilter, err := semantics.Walk(newTree, eval.EvalFunctionFactory(item)) 68 | if err != nil { 69 | continue 70 | } 71 | if match, ok := matchingFilter.(bool); ok && match { 72 | items = append(items, item) 73 | } 74 | } 75 | 76 | return items, nil 77 | } 78 | 79 | // Filter2 filters items using query and a left side item. 80 | func (c *Config) Filter2(list []unstructured.Unstructured) ([]unstructured.Unstructured, error) { 81 | var ( 82 | tree *tsl.TSLNode 83 | err error 84 | ) 85 | 86 | // If we have a query, prepare the search tree. 87 | tree, err = tsl.ParseTSL(c.Query) 88 | if err != nil { 89 | return nil, err 90 | } 91 | defer tree.Free() 92 | 93 | // Check and replace user identifiers if alias exist. 94 | newTree, err := ident.Walk(tree, c.CheckColumnName) 95 | if err != nil { 96 | return nil, err 97 | } 98 | defer newTree.Free() 99 | 100 | // Filter items using query. 101 | items := []unstructured.Unstructured{} 102 | for _, item := range list { 103 | // If we have a query, check item. 104 | matchingFilter, err := semantics.Walk(newTree, eval.JoinEvalFunctionFactory(c.Item, item, c.Prefix1, c.Prefix2)) 105 | if err != nil { 106 | continue 107 | } 108 | if match, ok := matchingFilter.(bool); ok && match { 109 | items = append(items, item) 110 | } 111 | } 112 | 113 | return items, nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "testing" 5 | 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | ) 8 | 9 | func TestFilter(t *testing.T) { 10 | items := []unstructured.Unstructured{ 11 | { 12 | Object: map[string]interface{}{ 13 | "metadata": map[string]interface{}{ 14 | "name": "test1", 15 | "labels": map[string]interface{}{ 16 | "app": "web", 17 | }, 18 | }, 19 | "spec": map[string]interface{}{ 20 | "replicas": int64(3), 21 | "containers": []interface{}{ 22 | map[string]interface{}{ 23 | "name": "nginx", 24 | "ports": []interface{}{ 25 | map[string]interface{}{ 26 | "containerPort": int64(80), 27 | }, 28 | map[string]interface{}{ 29 | "containerPort": int64(443), 30 | }, 31 | }, 32 | }, 33 | map[string]interface{}{ 34 | "name": "sidecar", 35 | "ports": []interface{}{ 36 | map[string]interface{}{ 37 | "containerPort": int64(8080), 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | { 46 | Object: map[string]interface{}{ 47 | "metadata": map[string]interface{}{ 48 | "name": "test2", 49 | "labels": map[string]interface{}{ 50 | "app": "db", 51 | }, 52 | }, 53 | "spec": map[string]interface{}{ 54 | "replicas": int64(1), 55 | "containers": []interface{}{ 56 | map[string]interface{}{ 57 | "name": "postgres", 58 | "ports": []interface{}{ 59 | map[string]interface{}{ 60 | "containerPort": int64(5432), 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | } 69 | 70 | tests := []struct { 71 | name string 72 | query string 73 | wantCount int 74 | wantErr bool 75 | }{ 76 | { 77 | name: "filter by name", 78 | query: "name = 'test1'", 79 | wantCount: 1, 80 | wantErr: false, 81 | }, 82 | { 83 | name: "filter by label", 84 | query: "labels.app = 'web'", 85 | wantCount: 1, 86 | wantErr: false, 87 | }, 88 | { 89 | name: "filter by replicas", 90 | query: "spec.replicas > 2", 91 | wantCount: 1, 92 | wantErr: false, 93 | }, 94 | { 95 | name: "invalid query", 96 | query: "invalid query", 97 | wantCount: 0, 98 | wantErr: true, 99 | }, 100 | { 101 | name: "filter with any on array element", 102 | query: "any (spec.containers[*].name = 'nginx')", 103 | wantCount: 1, 104 | wantErr: false, 105 | }, 106 | { 107 | name: "filter with any on nested array", 108 | query: "any (spec.containers[*].ports[*].containerPort < 400)", 109 | wantCount: 1, 110 | wantErr: false, 111 | }, 112 | { 113 | name: "filter with all on array element", 114 | query: "all (spec.containers[*].ports[*].containerPort < 9000)", 115 | wantCount: 2, 116 | wantErr: false, 117 | }, 118 | { 119 | name: "filter with array count", 120 | query: "len (spec.containers) > 1", 121 | wantCount: 1, 122 | wantErr: false, 123 | }, 124 | { 125 | name: "filter comparing array values", 126 | query: "'postgres' in spec.containers[*].name", 127 | wantCount: 1, 128 | wantErr: false, 129 | }, 130 | { 131 | name: "invalid query", 132 | query: "invalid query", 133 | wantCount: 0, 134 | wantErr: true, 135 | }, 136 | } 137 | 138 | for _, tt := range tests { 139 | t.Run(tt.name, func(t *testing.T) { 140 | c := &Config{ 141 | Query: tt.query, 142 | CheckColumnName: func(s string) (string, error) { 143 | return s, nil 144 | }, 145 | } 146 | 147 | got, err := c.Filter(items) 148 | if (err != nil) != tt.wantErr { 149 | t.Errorf("Filter() error = %v, wantErr %v", err, tt.wantErr) 150 | return 151 | } 152 | if !tt.wantErr && len(got) != tt.wantCount { 153 | t.Errorf("Filter() got = %v items, want %v", len(got), tt.wantCount) 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /pkg/printers/json.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package printers 21 | 22 | import ( 23 | "encoding/json" 24 | "fmt" 25 | 26 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 | ) 28 | 29 | // JSON prints items in JSON format 30 | func (c *Config) JSON(items []unstructured.Unstructured) error { 31 | for _, item := range items { 32 | yaml, err := json.Marshal(item) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | fmt.Fprintf(c.Out, "\n%+v\n", string(yaml)) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/printers/name.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package printers 21 | 22 | import ( 23 | "fmt" 24 | 25 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 | ) 27 | 28 | // Name prints items in Name format 29 | func (c *Config) Name(items []unstructured.Unstructured) error { 30 | for _, item := range items { 31 | fmt.Fprintf(c.Out, "%s\n", item.GetName()) 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/printers/orderby.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package printers 21 | 22 | // OrderByField represents a field to order by in SQL query results 23 | type OrderByField struct { 24 | // Name is the field name to sort by 25 | Name string 26 | // Descending indicates whether to sort in descending order (true) or ascending (false) 27 | Descending bool 28 | } 29 | -------------------------------------------------------------------------------- /pkg/printers/table.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package printers 21 | 22 | import ( 23 | "fmt" 24 | "io" 25 | "reflect" 26 | "sort" 27 | "strconv" 28 | "time" 29 | 30 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 | 32 | "github.com/yaacov/kubectl-sql/pkg/eval" 33 | ) 34 | 35 | // TableField describes how to print the SQL results table. 36 | type TableField struct { 37 | Title string `json:"title"` 38 | Name string `json:"name"` 39 | Width int 40 | Template string 41 | } 42 | type tableFields []TableField 43 | 44 | // TableFieldsMap a map of lists of table field descriptions. 45 | type TableFieldsMap map[string]tableFields 46 | 47 | // Config provides information required filter item list by query. 48 | type Config struct { 49 | // TableFields describe table field columns 50 | TableFields TableFieldsMap 51 | // OrderByFields describes how to sort the table results 52 | OrderByFields []OrderByField 53 | // Limit restricts the number of results displayed (0 means no limit) 54 | Limit int 55 | // NoHeaders if true, don't print header rows 56 | NoHeaders bool 57 | // Out think, os.Stdout 58 | Out io.Writer 59 | // ErrOut think, os.Stderr 60 | ErrOut io.Writer 61 | } 62 | 63 | const ( 64 | // SelectedFields is used to identify fields specifically selected in a SQL query 65 | SelectedFields = "selected" 66 | ) 67 | 68 | // Get the table column titles and fields for the items. 69 | func (c *Config) getTableColumns(items []unstructured.Unstructured) tableFields { 70 | var evalFunc func(string) (interface{}, bool) 71 | 72 | // Get the default template for this kind. 73 | kind := items[0].GetKind() 74 | 75 | // Try different variations of kind name 76 | fields, ok := c.TableFields[SelectedFields] 77 | if !ok || fields == nil { 78 | fields, ok = c.TableFields[kind] 79 | if !ok || fields == nil { 80 | fields = c.TableFields["other"] 81 | } 82 | } 83 | 84 | // Zero out field width 85 | for i := range fields { 86 | fields[i].Width = 0 87 | fields[i].Template = "" 88 | } 89 | 90 | // Calculte field widths 91 | for _, item := range items { 92 | evalFunc = eval.EvalFunctionFactory(item) 93 | 94 | for i, field := range fields { 95 | if value, found := evalFunc(field.Name); found && value != nil { 96 | length := len(fmt.Sprintf("%v", value)) 97 | 98 | if length > fields[i].Width { 99 | fields[i].Width = length 100 | } 101 | } 102 | } 103 | } 104 | 105 | // Calculte field template 106 | for i, field := range fields { 107 | if field.Width > 0 { 108 | // Ajdust for title length 109 | width := len(field.Title) 110 | if width < field.Width { 111 | width = field.Width 112 | } 113 | 114 | fields[i].Template = fmt.Sprintf("%%-%ds\t", width) 115 | } 116 | } 117 | 118 | return fields 119 | } 120 | 121 | // sortItems sorts the slice of unstructured items based on the OrderByFields 122 | func (c *Config) sortItems(items []unstructured.Unstructured) { 123 | if len(c.OrderByFields) == 0 { 124 | return 125 | } 126 | 127 | sort.SliceStable(items, func(i, j int) bool { 128 | for _, orderBy := range c.OrderByFields { 129 | evalFuncI := eval.EvalFunctionFactory(items[i]) 130 | evalFuncJ := eval.EvalFunctionFactory(items[j]) 131 | 132 | valueI, foundI := evalFuncI(orderBy.Name) 133 | valueJ, foundJ := evalFuncJ(orderBy.Name) 134 | 135 | // If either value is not found, prioritize the found value 136 | if !foundI && foundJ { 137 | return !orderBy.Descending 138 | } 139 | if foundI && !foundJ { 140 | return orderBy.Descending 141 | } 142 | if !foundI && !foundJ { 143 | continue 144 | } 145 | 146 | // Both values found, compare them 147 | if valueI == nil && valueJ != nil { 148 | return !orderBy.Descending 149 | } 150 | if valueI != nil && valueJ == nil { 151 | return orderBy.Descending 152 | } 153 | 154 | // Compare values 155 | switch vI := valueI.(type) { 156 | case bool: 157 | vJ := valueJ.(bool) 158 | if vI != vJ { 159 | return vI != orderBy.Descending 160 | } 161 | case float64: 162 | vJ := valueJ.(float64) 163 | if vI != vJ { 164 | return vI < vJ != orderBy.Descending 165 | } 166 | case string: 167 | vJ := valueJ.(string) 168 | if vI != vJ { 169 | return vI < vJ != orderBy.Descending 170 | } 171 | case time.Time: 172 | vJ := valueJ.(time.Time) 173 | if !vI.Equal(vJ) { 174 | return vI.Before(vJ) != orderBy.Descending 175 | } 176 | default: 177 | // Fallback to reflect.DeepEqual for other types 178 | if !reflect.DeepEqual(valueI, valueJ) { 179 | return reflect.DeepEqual(valueI, valueJ) != orderBy.Descending 180 | } 181 | } 182 | } 183 | return false 184 | }) 185 | } 186 | 187 | // Table prints items in Table format 188 | func (c *Config) Table(items []unstructured.Unstructured) error { 189 | var evalFunc func(string) (interface{}, bool) 190 | 191 | // Sort items if OrderByFields is set 192 | c.sortItems(items) 193 | 194 | // Get table fields for the items. 195 | fields := c.getTableColumns(items) 196 | 197 | // Apply limit if set 198 | displayCount := len(items) 199 | if c.Limit > 0 && c.Limit < displayCount { 200 | displayCount = c.Limit 201 | } 202 | 203 | // Print table head if headers are not disabled 204 | if !c.NoHeaders { 205 | fmt.Fprintf(c.Out, "KIND: %s\tCOUNT: %d", items[0].GetKind(), len(items)) 206 | if c.Limit > 0 && c.Limit < len(items) { 207 | fmt.Fprintf(c.Out, "\tDISPLAYING: %d", displayCount) 208 | } 209 | fmt.Fprintf(c.Out, "\n") 210 | 211 | for _, field := range fields { 212 | if field.Width > 0 { 213 | fmt.Fprintf(c.Out, field.Template, field.Title) 214 | } 215 | } 216 | fmt.Print("\n") 217 | } 218 | 219 | // Print table rows 220 | for i, item := range items { 221 | // Respect the limit if set 222 | if c.Limit > 0 && i >= c.Limit { 223 | break 224 | } 225 | 226 | evalFunc = eval.EvalFunctionFactory(item) 227 | 228 | for _, field := range fields { 229 | if field.Width > 0 { 230 | if v, found := evalFunc(field.Name); found && v != nil { 231 | value := v 232 | switch v := v.(type) { 233 | case bool: 234 | value = "false" 235 | if v { 236 | value = "true" 237 | } 238 | case float64: 239 | value = strconv.FormatFloat(v, 'f', -1, 64) 240 | case time.Time: 241 | value = v.Format(time.RFC3339) 242 | } 243 | 244 | fmt.Fprintf(c.Out, field.Template, value) 245 | } else { 246 | fmt.Fprintf(c.Out, field.Template, "") 247 | } 248 | } 249 | } 250 | fmt.Print("\n") 251 | } 252 | 253 | return nil 254 | } 255 | -------------------------------------------------------------------------------- /pkg/printers/yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Yaacov Zamir 3 | and other contributors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Author: 2020 Yaacov Zamir 18 | */ 19 | 20 | package printers 21 | 22 | import ( 23 | "fmt" 24 | 25 | "gopkg.in/yaml.v3" 26 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 | ) 28 | 29 | // YAML prints items in YAML format 30 | func (c *Config) YAML(items []unstructured.Unstructured) error { 31 | for _, item := range items { 32 | y, err := yaml.Marshal(item) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | fmt.Fprintf(c.Out, "\n%+v\n", string(y)) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /sql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: sql 5 | spec: 6 | version: v0.3.21 7 | homepage: https://github.com/yaacov/kubectl-sql 8 | platforms: 9 | - selector: 10 | matchLabels: 11 | os: linux 12 | arch: amd64 13 | uri: https://github.com/yaacov/kubectl-sql/releases/download/v0.3.21/kubectl-sql.tar.gz 14 | sha256: d73d661558b49561912525068da724ef3ab6cd5f1925a6df5300bc6beaa18df6 15 | files: 16 | - from: "*" 17 | to: "." 18 | bin: kubectl-sql 19 | shortDescription: Use SQL like language to query the Kubernetes cluster manager. 20 | description: | 21 | This plugin use SQL like language to query the Kubernetes cluster manager. 22 | caveats: | 23 | Usage: 24 | $ kubectl sql 25 | For additional options: 26 | $ kubectl sql --help 27 | or https://github.com/yaacov/kubectl-sql 28 | --------------------------------------------------------------------------------