├── .github ├── img │ ├── kubesql-162.png │ ├── kubesql-248.png │ ├── kubesql-500.png │ └── kubesql.svg └── workflows │ ├── pull_request.yml │ └── release.yml ├── pkg ├── eval │ ├── factory.go │ ├── string_test.go │ ├── string.go │ ├── eval.go │ └── eval_test.go ├── printers │ ├── orderby.go │ ├── name.go │ ├── json.go │ ├── yaml.go │ └── table.go ├── cmd │ ├── sql-version.go │ ├── sql-printer.go │ ├── config.go │ ├── constants.go │ ├── sql.go │ └── sql-sql.go ├── filter │ ├── filter.go │ └── filter_test.go └── client │ └── client.go ├── README_config.md ├── .gitignore ├── cmd └── kubectl-sql │ └── main.go ├── go.mod ├── .krew.yaml ├── kubectl-sql.spec ├── CONTRIBUTING.md ├── README.md ├── Makefile ├── README_language.md ├── CODE_OF_CONDUCT.md ├── README_examples.md ├── LICENSE └── go.sum /.github/img/kubesql-162.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaacov/kubectl-sql/HEAD/.github/img/kubesql-162.png -------------------------------------------------------------------------------- /.github/img/kubesql-248.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaacov/kubectl-sql/HEAD/.github/img/kubesql-248.png -------------------------------------------------------------------------------- /.github/img/kubesql-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaacov/kubectl-sql/HEAD/.github/img/kubesql-500.png -------------------------------------------------------------------------------- /pkg/eval/factory.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "github.com/yaacov/tree-search-language/v6/pkg/walkers/semantics" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | ) 7 | 8 | // EvalFunctionFactory build an evaluation method for one item that returns a value using a key. 9 | func EvalFunctionFactory(item unstructured.Unstructured) semantics.EvalFunc { 10 | return func(key string) (interface{}, bool) { 11 | return ExtractValue(item, key) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 21 | # Cross-platform binaries 22 | /kubectl-sql-linux-amd64 23 | /kubectl-sql-linux-arm64 24 | /kubectl-sql-darwin-amd64 25 | /kubectl-sql-darwin-arm64 26 | /kubectl-sql-windows-amd64.exe 27 | 28 | # Cross-platform archives and checksums 29 | /kubectl-sql-*-linux-amd64.tar.gz 30 | /kubectl-sql-*-linux-arm64.tar.gz 31 | /kubectl-sql-*-darwin-amd64.tar.gz 32 | /kubectl-sql-*-darwin-arm64.tar.gz 33 | /kubectl-sql-*-windows-amd64.zip 34 | *.tar.gz.sha256sum 35 | *.zip.sha256sum 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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-printer.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 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 | 25 | "github.com/yaacov/kubectl-sql/pkg/printers" 26 | ) 27 | 28 | // Printer printout a list of items. 29 | func (o *SQLOptions) Printer(items []unstructured.Unstructured) error { 30 | // Sanity check 31 | if len(items) == 0 { 32 | return nil 33 | } 34 | 35 | p := printers.Config{ 36 | TableFields: o.defaultTableFields, 37 | OrderByFields: o.orderByFields, 38 | Limit: o.limit, 39 | Out: o.Out, 40 | ErrOut: o.ErrOut, 41 | NoHeaders: o.noHeaders, 42 | } 43 | 44 | // Print out 45 | switch o.outputFormat { 46 | case "yaml": 47 | return p.YAML(items) 48 | case "json": 49 | return p.JSON(items) 50 | case "name": 51 | return p.Name(items) 52 | default: 53 | err := p.Table(items) 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /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/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/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 | 38 | // Filter filters items using query. 39 | func (c *Config) Filter(list []unstructured.Unstructured) ([]unstructured.Unstructured, error) { 40 | var ( 41 | tree *tsl.TSLNode 42 | err error 43 | ) 44 | 45 | // If we have a query, prepare the search tree. 46 | tree, err = tsl.ParseTSL(c.Query) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // Check and replace user identifiers if alias exist. 52 | newTree, err := ident.Walk(tree, c.CheckColumnName) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // Filter items using a query. 58 | items := []unstructured.Unstructured{} 59 | for _, item := range list { 60 | // If we have a query, check item. 61 | matchingFilter, err := semantics.Walk(newTree, eval.EvalFunctionFactory(item)) 62 | if err != nil { 63 | continue 64 | } 65 | if match, ok := matchingFilter.(bool); ok && match { 66 | items = append(items, item) 67 | } 68 | } 69 | 70 | return items, nil 71 | } 72 | -------------------------------------------------------------------------------- /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 | "k8s.io/cli-runtime/pkg/genericclioptions" 24 | _ "k8s.io/client-go/plugin/pkg/client/auth" 25 | "k8s.io/client-go/tools/clientcmd" 26 | 27 | "github.com/yaacov/kubectl-sql/pkg/printers" 28 | ) 29 | 30 | // SQLOptions provides information required to update 31 | // the current context on a user's KUBECONFIG 32 | type SQLOptions struct { 33 | configFlags *genericclioptions.ConfigFlags 34 | 35 | rawConfig clientcmd.ClientConfig 36 | args []string 37 | 38 | defaultAliases map[string]string 39 | defaultTableFields printers.TableFieldsMap 40 | orderByFields []printers.OrderByField 41 | limit int 42 | 43 | namespace string 44 | requestedResources []string 45 | requestedQuery string 46 | 47 | outputFormat string 48 | noHeaders bool 49 | 50 | genericclioptions.IOStreams 51 | } 52 | 53 | // NewSQLOptions provides an instance of SQLOptions with default values initialized 54 | func initializeDefaults(o *SQLOptions) { 55 | o.defaultAliases = defaultAliases 56 | o.defaultTableFields = defaultTableFields 57 | o.orderByFields = []printers.OrderByField{} 58 | o.limit = 0 // Default to no limit 59 | } 60 | 61 | // checkColumnName checks if a column name has an alias. 62 | func (o *SQLOptions) checkColumnName(s string) (string, error) { 63 | // Check for aliases. 64 | if v, ok := o.defaultAliases[s]; ok { 65 | return v, nil 66 | } 67 | 68 | // If not found in alias table, return the column name unchanged. 69 | return s, nil 70 | } 71 | -------------------------------------------------------------------------------- /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 Kubernetes 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 | # List first 5 pods ordered by creation time in descending order (newest first). 41 | kubectl sql "select * from pods order by created desc limit 5" 42 | 43 | # Print this help message. 44 | kubectl sql help` 45 | 46 | // Errors. 47 | errUsageTemplate = "bad command or command usage, %s" 48 | 49 | // Defaults. 50 | defaultAliases = map[string]string{ 51 | "name": "metadata.name", 52 | "namespace": "metadata.namespace", 53 | "created": "metadata.creationTimestamp", 54 | "phase": "status.phase", 55 | "uid": "metadata.uid", 56 | } 57 | defaultTableFields = printers.TableFieldsMap{ 58 | "other": { 59 | { 60 | Title: "NAMESPACE", 61 | Name: "namespace", 62 | }, 63 | { 64 | Title: "NAME", 65 | Name: "name", 66 | }, 67 | { 68 | Title: "PHASE", 69 | Name: "status.phase", 70 | }, 71 | { 72 | Title: "CREATION_TIME(RFC3339)", 73 | Name: "created", 74 | }, 75 | }, 76 | } 77 | ) 78 | -------------------------------------------------------------------------------- /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.8 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/fxamacker/cbor/v2 v2.7.0 // indirect 23 | github.com/go-errors/errors v1.4.2 // indirect 24 | github.com/go-logr/logr v1.4.2 // indirect 25 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 26 | github.com/go-openapi/jsonreference v0.20.2 // indirect 27 | github.com/go-openapi/swag v0.23.0 // indirect 28 | github.com/gogo/protobuf v1.3.2 // indirect 29 | github.com/golang/protobuf v1.5.4 // indirect 30 | github.com/google/btree v1.0.1 // indirect 31 | github.com/google/gnostic-models v0.6.8 // indirect 32 | github.com/google/go-cmp v0.6.0 // indirect 33 | github.com/google/gofuzz v1.2.0 // indirect 34 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 37 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 38 | github.com/josharian/intern v1.0.0 // indirect 39 | github.com/json-iterator/go v1.1.12 // indirect 40 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/moby/term v0.5.0 // indirect 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 44 | github.com/modern-go/reflect2 v1.0.2 // indirect 45 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 47 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 48 | github.com/pkg/errors v0.9.1 // indirect 49 | github.com/x448/float16 v0.8.4 // indirect 50 | github.com/xlab/treeprint v1.2.0 // indirect 51 | golang.org/x/net v0.38.0 // indirect 52 | golang.org/x/oauth2 v0.23.0 // indirect 53 | golang.org/x/sync v0.12.0 // indirect 54 | golang.org/x/sys v0.31.0 // indirect 55 | golang.org/x/term v0.30.0 // indirect 56 | golang.org/x/text v0.23.0 // indirect 57 | golang.org/x/time v0.7.0 // indirect 58 | google.golang.org/protobuf v1.36.1 // indirect 59 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 60 | gopkg.in/inf.v0 v0.9.1 // indirect 61 | k8s.io/api v0.32.2 // indirect 62 | k8s.io/klog/v2 v2.130.1 // indirect 63 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 64 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 65 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 66 | sigs.k8s.io/kustomize/api v0.18.0 // indirect 67 | sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect 68 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 69 | sigs.k8s.io/yaml v1.4.0 // indirect 70 | ) 71 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: sql 5 | spec: 6 | version: {{ .TagName }} 7 | homepage: https://github.com/yaacov/kubectl-sql 8 | shortDescription: Query Kubernetes resources using SQL-like syntax 9 | description: | 10 | kubectl-sql is a kubectl plugin that allows you to query Kubernetes resources 11 | using SQL-like syntax. It supports complex queries with WHERE clauses, sorting, 12 | limiting, and filtering across multiple resource types and namespaces. 13 | 14 | Features: 15 | - SQL-like query language with operators (=, !=, ~=, >, <, etc.) 16 | - Support for all Kubernetes resource types 17 | - Cross-namespace queries 18 | - Multiple output formats (table, json, yaml, name) 19 | - Array operations (any, all, len) 20 | - Time and size unit support 21 | caveats: | 22 | This plugin requires kubectl to be installed and configured to access your cluster. 23 | 24 | Examples: 25 | # Get pods with specific labels 26 | kubectl sql "SELECT name, status.phase FROM */pods WHERE metadata.labels.app = 'nginx'" 27 | 28 | # Find pods with high memory usage 29 | kubectl sql "SELECT name FROM */pods WHERE spec.containers[*].resources.requests.memory > 1Gi" 30 | platforms: 31 | - selector: 32 | matchLabels: 33 | os: linux 34 | arch: amd64 35 | {{addURIAndSha "https://github.com/yaacov/kubectl-sql/releases/download/{{ .TagName }}/kubectl-sql-{{ .TagName }}-linux-amd64.tar.gz" .TagName }} 36 | files: 37 | - from: kubectl-sql-linux-amd64 38 | to: kubectl-sql 39 | - from: LICENSE 40 | to: . 41 | bin: kubectl-sql 42 | - selector: 43 | matchLabels: 44 | os: linux 45 | arch: arm64 46 | {{addURIAndSha "https://github.com/yaacov/kubectl-sql/releases/download/{{ .TagName }}/kubectl-sql-{{ .TagName }}-linux-arm64.tar.gz" .TagName }} 47 | files: 48 | - from: kubectl-sql-linux-arm64 49 | to: kubectl-sql 50 | - from: LICENSE 51 | to: . 52 | bin: kubectl-sql 53 | - selector: 54 | matchLabels: 55 | os: darwin 56 | arch: amd64 57 | {{addURIAndSha "https://github.com/yaacov/kubectl-sql/releases/download/{{ .TagName }}/kubectl-sql-{{ .TagName }}-darwin-amd64.tar.gz" .TagName }} 58 | files: 59 | - from: kubectl-sql-darwin-amd64 60 | to: kubectl-sql 61 | - from: LICENSE 62 | to: . 63 | bin: kubectl-sql 64 | - selector: 65 | matchLabels: 66 | os: darwin 67 | arch: arm64 68 | {{addURIAndSha "https://github.com/yaacov/kubectl-sql/releases/download/{{ .TagName }}/kubectl-sql-{{ .TagName }}-darwin-arm64.tar.gz" .TagName }} 69 | files: 70 | - from: kubectl-sql-darwin-arm64 71 | to: kubectl-sql 72 | - from: LICENSE 73 | to: . 74 | bin: kubectl-sql 75 | - selector: 76 | matchLabels: 77 | os: windows 78 | arch: amd64 79 | {{addURIAndSha "https://github.com/yaacov/kubectl-sql/releases/download/{{ .TagName }}/kubectl-sql-{{ .TagName }}-windows-amd64.zip" .TagName }} 80 | files: 81 | - from: kubectl-sql-windows-amd64.exe 82 | to: kubectl-sql.exe 83 | - from: LICENSE 84 | to: . 85 | bin: kubectl-sql.exe 86 | -------------------------------------------------------------------------------- /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.42 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/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/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 | } 41 | 42 | // List resources by resource name. 43 | func (c Config) List(ctx context.Context, resourceName string) ([]unstructured.Unstructured, error) { 44 | var err error 45 | var list *unstructured.UnstructuredList 46 | 47 | resource, group, version, err := c.getResourceGroupVersion(resourceName) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | dynamicClient, err := dynamic.NewForConfig(c.Config) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // Get all resource objects. 58 | res := dynamicClient.Resource(schema.GroupVersionResource{ 59 | Group: group, 60 | Version: version, 61 | Resource: resource.Name, 62 | }) 63 | 64 | // Check for namespace 65 | if len(c.Namespace) > 0 && c.Namespace != "*" && resource.Namespaced { 66 | list, err = res.Namespace(c.Namespace).List(ctx, v1.ListOptions{}) 67 | } else { 68 | list, err = res.List(ctx, v1.ListOptions{}) 69 | } 70 | 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return list.Items, err 76 | } 77 | 78 | // Look for a resource matching request resource name. 79 | func (c Config) getResourceGroupVersion(resourceName string) (v1.APIResource, string, string, error) { 80 | discoveryClient, err := discovery.NewDiscoveryClientForConfig(c.Config) 81 | if err != nil { 82 | return v1.APIResource{}, "", "", err 83 | } 84 | 85 | resources, err := discoveryClient.ServerPreferredResources() 86 | if err != nil { 87 | return v1.APIResource{}, "", "", err 88 | } 89 | 90 | // Search for a matching resource 91 | resource := v1.APIResource{} 92 | resourceList := &v1.APIResourceList{} 93 | for _, rl := range resources { 94 | for _, r := range rl.APIResources { 95 | names := append(r.ShortNames, r.Name) 96 | if stringInSlice(resourceName, names) { 97 | resource = r 98 | resourceList = rl 99 | } 100 | } 101 | 102 | if len(resource.Name) > 0 { 103 | break 104 | } 105 | } 106 | 107 | if len(resource.Name) == 0 { 108 | return v1.APIResource{}, "", "", fmt.Errorf("Failed to find resource") 109 | } 110 | 111 | group, version := getGroupVersion(resourceList) 112 | return resource, group, version, nil 113 | } 114 | 115 | // Get resource group and version. 116 | func getGroupVersion(resourceList *v1.APIResourceList) (string, string) { 117 | group := "" 118 | version := "" 119 | resourceGroupSplit := strings.Split(resourceList.GroupVersion, "/") 120 | if len(resourceGroupSplit) == 2 { 121 | group = resourceGroupSplit[0] 122 | version = resourceGroupSplit[1] 123 | } else { 124 | version = resourceGroupSplit[0] 125 | } 126 | 127 | return group, version 128 | } 129 | 130 | // Check if a string in slice of strings. 131 | func stringInSlice(a string, list []string) bool { 132 | for _, b := range list { 133 | if b == a { 134 | return true 135 | } 136 | } 137 | return false 138 | } 139 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "strings" 25 | 26 | "github.com/spf13/cobra" 27 | "k8s.io/cli-runtime/pkg/genericclioptions" 28 | ) 29 | 30 | // NewSQLOptions provides an instance of SQLOptions with default values 31 | func NewSQLOptions(streams genericclioptions.IOStreams) *SQLOptions { 32 | options := &SQLOptions{ 33 | configFlags: genericclioptions.NewConfigFlags(true), 34 | IOStreams: streams, 35 | outputFormat: "table", 36 | } 37 | 38 | // Initialize default configuration 39 | initializeDefaults(options) 40 | 41 | return options 42 | } 43 | 44 | // NewCmdSQL provides a cobra command wrapping SQLOptions 45 | func NewCmdSQL(streams genericclioptions.IOStreams) *cobra.Command { 46 | o := NewSQLOptions(streams) 47 | 48 | cmd := &cobra.Command{ 49 | Use: "sql [flags] [options]", 50 | Short: "Query Kubernetes resources using SQL-like syntax", 51 | Long: sqlCmdLong, 52 | Example: sqlCmdExample, 53 | TraverseChildren: true, 54 | RunE: func(c *cobra.Command, args []string) error { 55 | if len(args) == 0 { 56 | return fmt.Errorf(errUsageTemplate, "missing SQL query") 57 | } 58 | 59 | // All arguments should be treated as a single SQL query 60 | query := strings.Join(args, " ") 61 | 62 | if err := o.Complete(c, args); err != nil { 63 | return err 64 | } 65 | 66 | if err := o.CompleteSQL(query); err != nil { 67 | return err 68 | } 69 | 70 | if err := o.Validate(); err != nil { 71 | return err 72 | } 73 | 74 | config, err := o.rawConfig.ClientConfig() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | // Execute query based on number of resources 80 | if len(o.requestedResources) >= 1 { 81 | return o.Get(config) 82 | } else { 83 | return fmt.Errorf("invalid number of resources in query") 84 | } 85 | }, 86 | } 87 | 88 | cmdVersion := &cobra.Command{ 89 | Use: "version [flags]", 90 | Short: "Print the SQL client and server version information", 91 | SilenceUsage: true, 92 | RunE: func(c *cobra.Command, args []string) error { 93 | if err := o.Complete(c, args); err != nil { 94 | return err 95 | } 96 | 97 | if err := o.Validate(); err != nil { 98 | return err 99 | } 100 | 101 | config, err := o.rawConfig.ClientConfig() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if err := o.Version(config); err != nil { 107 | return err 108 | } 109 | 110 | return nil 111 | }, 112 | } 113 | 114 | cmd.AddCommand(cmdVersion) 115 | 116 | cmd.Flags().StringVarP(&o.outputFormat, "output", "o", o.outputFormat, 117 | "Output format. One of: json|yaml|table|name") 118 | cmd.Flags().BoolVarP(&o.noHeaders, "no-headers", "H", false, 119 | "When using the table output format, don't print headers (column titles)") 120 | 121 | o.configFlags.AddFlags(cmd.Flags()) 122 | 123 | cmdVersion.Flags().AddFlagSet(cmd.Flags()) 124 | 125 | return cmd 126 | } 127 | 128 | // Complete sets all information required for updating the current context 129 | func (o *SQLOptions) Complete(cmd *cobra.Command, args []string) error { 130 | var err error 131 | o.args = args 132 | 133 | o.rawConfig = o.configFlags.ToRawKubeConfigLoader() 134 | if o.namespace, _, err = o.rawConfig.Namespace(); err != nil { 135 | return err 136 | } 137 | 138 | return nil 139 | } 140 | 141 | // Validate ensures that all required arguments and flag values are provided 142 | func (o *SQLOptions) Validate() error { 143 | formatOptions := map[string]bool{"table": true, "json": true, "yaml": true, "name": true} 144 | 145 | if _, ok := formatOptions[o.outputFormat]; !ok { 146 | return fmt.Errorf("output format must be one of: json|yaml|table|name") 147 | } 148 | 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | name: Build Multi-Platform Binaries 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: Build multi-platform and package 22 | run: make dist-all 23 | env: 24 | VERSION: ${{ github.event.release.tag_name }} 25 | 26 | - name: Upload release assets 27 | uses: actions/github-script@v6 28 | env: 29 | VERSION: ${{ github.event.release.tag_name }} 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | script: | 33 | const fs = require('fs'); 34 | 35 | const platforms = [ 36 | { os: 'linux', arch: 'amd64', ext: 'tar.gz', contentType: 'application/gzip' }, 37 | { os: 'linux', arch: 'arm64', ext: 'tar.gz', contentType: 'application/gzip' }, 38 | { os: 'darwin', arch: 'amd64', ext: 'tar.gz', contentType: 'application/gzip' }, 39 | { os: 'darwin', arch: 'arm64', ext: 'tar.gz', contentType: 'application/gzip' }, 40 | { os: 'windows', arch: 'amd64', ext: 'zip', contentType: 'application/zip' } 41 | ]; 42 | 43 | const version = process.env.VERSION || '${{ github.event.release.tag_name }}'; 44 | const uploadUrl = context.payload.release.upload_url; 45 | 46 | for (const platform of platforms) { 47 | const archiveName = `kubectl-sql-${version}-${platform.os}-${platform.arch}.${platform.ext}`; 48 | const checksumName = `${archiveName}.sha256sum`; 49 | 50 | // Upload archive 51 | const archiveData = fs.readFileSync(archiveName); 52 | await github.rest.repos.uploadReleaseAsset({ 53 | owner: context.repo.owner, 54 | repo: context.repo.repo, 55 | release_id: context.payload.release.id, 56 | name: archiveName, 57 | data: archiveData, 58 | headers: { 59 | 'content-type': platform.contentType, 60 | 'content-length': archiveData.length 61 | } 62 | }); 63 | 64 | // Upload checksum 65 | const checksumData = fs.readFileSync(checksumName); 66 | await github.rest.repos.uploadReleaseAsset({ 67 | owner: context.repo.owner, 68 | repo: context.repo.repo, 69 | release_id: context.payload.release.id, 70 | name: checksumName, 71 | data: checksumData, 72 | headers: { 73 | 'content-type': 'text/plain', 74 | 'content-length': checksumData.length 75 | } 76 | }); 77 | 78 | console.log(`Uploaded ${archiveName} and ${checksumName}`); 79 | } 80 | 81 | console.log('All release assets uploaded successfully'); 82 | 83 | - name: Wait for assets to be available 84 | run: | 85 | echo "Waiting 30 seconds for GitHub to propagate release assets..." 86 | sleep 30 87 | 88 | - name: Verify all assets are available 89 | run: | 90 | VERSION="${{ github.event.release.tag_name }}" 91 | ASSETS=( 92 | "kubectl-sql-${VERSION}-linux-amd64.tar.gz" 93 | "kubectl-sql-${VERSION}-linux-arm64.tar.gz" 94 | "kubectl-sql-${VERSION}-darwin-amd64.tar.gz" 95 | "kubectl-sql-${VERSION}-darwin-arm64.tar.gz" 96 | "kubectl-sql-${VERSION}-windows-amd64.zip" 97 | ) 98 | 99 | echo "Verifying release assets are downloadable..." 100 | for asset in "${ASSETS[@]}"; do 101 | url="https://github.com/yaacov/kubectl-sql/releases/download/${VERSION}/${asset}" 102 | echo "Checking: $url" 103 | if curl -sSL --fail -I "$url" > /dev/null; then 104 | echo "✅ $asset is available" 105 | else 106 | echo "❌ $asset is not available" 107 | exit 1 108 | fi 109 | done 110 | echo "All assets verified successfully!" 111 | 112 | - name: Update Krew index 113 | uses: rajatjindal/krew-release-bot@v0.0.46 114 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.23 or higher 19 | # - curl or wget 20 | # 21 | # Run `make install-tools` to install required development tools 22 | 23 | VERSION_GIT := $(shell git describe --tags) 24 | VERSION ?= ${VERSION_GIT} 25 | 26 | all: kubectl-sql 27 | 28 | .PHONY: install-tools 29 | install-tools: 30 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 31 | 32 | kubesql_cmd := $(wildcard ./cmd/kubectl-sql/*.go) 33 | kubesql_pkg := $(wildcard ./pkg/**/*.go) 34 | GOOS := $(shell go env GOOS) 35 | GOARCH := $(shell go env GOARCH) 36 | 37 | kubectl-sql: clean $(kubesql_cmd) $(kubesql_pkg) 38 | @echo "Building static binary for ${GOOS}/${GOARCH}" 39 | CGO_ENABLED=0 go build \ 40 | -a \ 41 | -ldflags '-s -w -X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' \ 42 | -o kubectl-sql \ 43 | $(kubesql_cmd) 44 | 45 | # Cross-compilation targets 46 | .PHONY: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 build-windows-amd64 47 | build-linux-amd64: clean $(kubesql_cmd) $(kubesql_pkg) 48 | @echo "Building for linux/amd64" 49 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ 50 | -a \ 51 | -ldflags '-s -w -X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' \ 52 | -o kubectl-sql-linux-amd64 \ 53 | $(kubesql_cmd) 54 | 55 | build-linux-arm64: clean $(kubesql_cmd) $(kubesql_pkg) 56 | @echo "Building for linux/arm64" 57 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \ 58 | -a \ 59 | -ldflags '-s -w -X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' \ 60 | -o kubectl-sql-linux-arm64 \ 61 | $(kubesql_cmd) 62 | 63 | build-darwin-amd64: clean $(kubesql_cmd) $(kubesql_pkg) 64 | @echo "Building for darwin/amd64" 65 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build \ 66 | -a \ 67 | -ldflags '-s -w -X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' \ 68 | -o kubectl-sql-darwin-amd64 \ 69 | $(kubesql_cmd) 70 | 71 | build-darwin-arm64: clean $(kubesql_cmd) $(kubesql_pkg) 72 | @echo "Building for darwin/arm64" 73 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build \ 74 | -a \ 75 | -ldflags '-s -w -X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' \ 76 | -o kubectl-sql-darwin-arm64 \ 77 | $(kubesql_cmd) 78 | 79 | build-windows-amd64: clean $(kubesql_cmd) $(kubesql_pkg) 80 | @echo "Building for windows/amd64" 81 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build \ 82 | -a \ 83 | -ldflags '-s -w -X github.com/yaacov/kubectl-sql/pkg/cmd.clientVersion=${VERSION}' \ 84 | -o kubectl-sql-windows-amd64.exe \ 85 | $(kubesql_cmd) 86 | 87 | # Build all platforms 88 | .PHONY: build-all 89 | build-all: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 build-windows-amd64 90 | 91 | # Create release archives for all platforms 92 | .PHONY: dist-all 93 | dist-all: build-all 94 | @echo "Creating release archives..." 95 | tar -zcvf kubectl-sql-${VERSION}-linux-amd64.tar.gz LICENSE kubectl-sql-linux-amd64 96 | tar -zcvf kubectl-sql-${VERSION}-linux-arm64.tar.gz LICENSE kubectl-sql-linux-arm64 97 | tar -zcvf kubectl-sql-${VERSION}-darwin-amd64.tar.gz LICENSE kubectl-sql-darwin-amd64 98 | tar -zcvf kubectl-sql-${VERSION}-darwin-arm64.tar.gz LICENSE kubectl-sql-darwin-arm64 99 | zip kubectl-sql-${VERSION}-windows-amd64.zip LICENSE kubectl-sql-windows-amd64.exe 100 | @echo "Generating individual checksums..." 101 | sha256sum kubectl-sql-${VERSION}-linux-amd64.tar.gz > kubectl-sql-${VERSION}-linux-amd64.tar.gz.sha256sum 102 | sha256sum kubectl-sql-${VERSION}-linux-arm64.tar.gz > kubectl-sql-${VERSION}-linux-arm64.tar.gz.sha256sum 103 | sha256sum kubectl-sql-${VERSION}-darwin-amd64.tar.gz > kubectl-sql-${VERSION}-darwin-amd64.tar.gz.sha256sum 104 | sha256sum kubectl-sql-${VERSION}-darwin-arm64.tar.gz > kubectl-sql-${VERSION}-darwin-arm64.tar.gz.sha256sum 105 | sha256sum kubectl-sql-${VERSION}-windows-amd64.zip > kubectl-sql-${VERSION}-windows-amd64.zip.sha256sum 106 | 107 | .PHONY: lint 108 | lint: 109 | go vet ./pkg/... ./cmd/... 110 | $(shell go env GOPATH)/bin/golangci-lint run ./pkg/... ./cmd/... 111 | 112 | .PHONY: fmt 113 | fmt: 114 | go fmt ./pkg/... ./cmd/... 115 | 116 | .PHONY: dist 117 | dist: kubectl-sql 118 | tar -zcvf kubectl-sql.tar.gz LICENSE kubectl-sql 119 | sha256sum kubectl-sql.tar.gz > kubectl-sql.tar.gz.sha256sum 120 | 121 | .PHONY: clean 122 | clean: 123 | rm -f kubectl-sql kubectl-sql-* 124 | rm -f *.tar.gz *.zip *.sha256sum 125 | 126 | .PHONY: test 127 | test: 128 | go test -v -cover ./pkg/... ./cmd/... 129 | go test -coverprofile=coverage.out ./pkg/... ./cmd/... 130 | go tool cover -func=coverage.out 131 | @rm coverage.out 132 | 133 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/cmd/sql-sql.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "k8s.io/client-go/rest" 11 | 12 | "github.com/yaacov/kubectl-sql/pkg/client" 13 | "github.com/yaacov/kubectl-sql/pkg/filter" 14 | "github.com/yaacov/kubectl-sql/pkg/printers" 15 | ) 16 | 17 | // isValidFieldIdentifier checks if a field name matches the allowed pattern 18 | func isValidFieldIdentifier(field string) bool { 19 | // Check for labels.* pattern 20 | if strings.HasPrefix(field, "labels.") { 21 | labelKey := field[7:] // Remove "labels." prefix 22 | // K8s label keys: alphanumeric, hyphens, underscores, dots 23 | // Must start and end with alphanumeric character 24 | labelPattern := `^[a-zA-Z0-9]([a-zA-Z0-9\-_.]*[a-zA-Z0-9])?$` 25 | match, _ := regexp.MatchString(labelPattern, labelKey) 26 | return match 27 | } 28 | 29 | // Check for annotations.* pattern 30 | if strings.HasPrefix(field, "annotations.") { 31 | annotationKey := field[12:] // Remove "annotations." prefix 32 | // K8s annotation keys: similar to labels but more flexible 33 | // Can contain alphanumeric, hyphens, underscores, dots, and slashes 34 | annotationPattern := `^[a-zA-Z0-9]([a-zA-Z0-9\-_./]*[a-zA-Z0-9])?$` 35 | match, _ := regexp.MatchString(annotationPattern, annotationKey) 36 | return match 37 | } 38 | 39 | // Matches patterns like: 40 | // - simple: name, first_name, my.field 41 | // - array access: items[0], my.array[123] 42 | pattern := `^[a-zA-Z_]([a-zA-Z0-9_.]*(?:\[\d+\])?)*$` 43 | match, _ := regexp.MatchString(pattern, field) 44 | return match 45 | } 46 | 47 | // isValidK8sResourceName checks if a resource name follows Kubernetes naming conventions 48 | func isValidK8sResourceName(resource string) bool { 49 | // Matches lowercase words separated by dots or slashes 50 | // Examples: pods, deployments, apps/v1/deployments 51 | pattern := `^[a-z]+([a-z0-9-]*[a-z0-9])?(/[a-z0-9]+)*$` 52 | match, _ := regexp.MatchString(pattern, resource) 53 | return match 54 | } 55 | 56 | // isValidNamespace checks if a namespace name is valid according to Kubernetes naming conventions 57 | // or if it's the special "*" value for all namespaces 58 | func isValidNamespace(namespace string) bool { 59 | // Special case for "all namespaces" 60 | if namespace == "*" { 61 | return true 62 | } 63 | 64 | pattern := `^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$` 65 | match, _ := regexp.MatchString(pattern, namespace) 66 | return match 67 | } 68 | 69 | // QueryType represents the type of SQL query 70 | type QueryType int 71 | 72 | const ( 73 | SimpleQuery QueryType = iota 74 | ) 75 | 76 | // parseFields extracts and validates SELECT fields 77 | func (o *SQLOptions) parseFields(selectFields string) error { 78 | if selectFields == "*" { 79 | return nil 80 | } 81 | 82 | if len(strings.TrimSpace(selectFields)) == 0 { 83 | return fmt.Errorf("SELECT clause cannot be empty") 84 | } 85 | 86 | fields := strings.Split(selectFields, ",") 87 | tableFields := make([]printers.TableField, 0, len(fields)) 88 | 89 | for _, field := range fields { 90 | field = strings.TrimSpace(field) 91 | 92 | // Check for AS syntax 93 | parts := strings.Split(strings.ToUpper(field), " AS ") 94 | var name, title string 95 | 96 | if len(parts) == 2 { 97 | // We have an AS clause 98 | name = strings.TrimSpace(field[:strings.Index(strings.ToUpper(field), " AS ")]) 99 | title = strings.TrimSpace(field[strings.Index(strings.ToUpper(field), " AS ")+4:]) 100 | 101 | if !isValidFieldIdentifier(name) { 102 | return fmt.Errorf("invalid field identifier before AS: %s", name) 103 | } 104 | if !isValidFieldIdentifier(title) { 105 | return fmt.Errorf("invalid field identifier after AS: %s", title) 106 | } 107 | } else { 108 | // No AS clause, use field as both name and title 109 | if !isValidFieldIdentifier(field) { 110 | return fmt.Errorf("invalid field identifier: %s", field) 111 | } 112 | name = field 113 | title = field 114 | } 115 | 116 | // Append to table fields 117 | tableFields = append(tableFields, printers.TableField{ 118 | Name: name, 119 | Title: title, 120 | }) 121 | 122 | // Append to default aliases 123 | o.defaultAliases[title] = name 124 | } 125 | 126 | o.defaultTableFields[printers.SelectedFields] = tableFields 127 | return nil 128 | } 129 | 130 | // parseResources validates and sets the requested resources 131 | func (o *SQLOptions) parseResources(resources []string, queryType QueryType) error { 132 | for i, r := range resources { 133 | r = strings.TrimSpace(r) 134 | 135 | // Split resource on "/" to check for namespace 136 | parts := strings.Split(r, "/") 137 | var resourceName string 138 | 139 | switch len(parts) { 140 | case 1: 141 | resourceName = parts[0] 142 | case 2: 143 | // Check for namespace validity 144 | namespace := parts[0] 145 | if !isValidNamespace(namespace) { 146 | return fmt.Errorf("invalid namespace: %s", namespace) 147 | } 148 | 149 | // Set namespace options 150 | o.namespace = namespace 151 | resourceName = parts[1] 152 | default: 153 | return fmt.Errorf("invalid resource format: %s, expected [namespace/]resource or */resource for all namespaces", r) 154 | } 155 | 156 | if !isValidK8sResourceName(resourceName) { 157 | return fmt.Errorf("invalid resource name: %s", resourceName) 158 | } 159 | 160 | resources[i] = resourceName 161 | } 162 | 163 | if len(resources) != 1 { 164 | return fmt.Errorf("exactly one resource must be specified") 165 | } 166 | 167 | o.requestedResources = resources 168 | return nil 169 | } 170 | 171 | // identifyQueryType determines the type of SQL query and returns relevant indices 172 | func (o *SQLOptions) identifyQueryType(query string) (QueryType, map[string]int, error) { 173 | upperQuery := strings.ToUpper(query) 174 | if !strings.HasPrefix(upperQuery, "SELECT") { 175 | return SimpleQuery, nil, fmt.Errorf("query must start with SELECT") 176 | } 177 | 178 | indices := map[string]int{ 179 | "SELECT": 0, 180 | "FROM": strings.Index(upperQuery, " FROM "), 181 | "JOIN": strings.Index(upperQuery, " JOIN "), 182 | "ON": strings.Index(upperQuery, " ON "), 183 | "WHERE": strings.Index(upperQuery, " WHERE "), 184 | "ORDER BY": strings.Index(upperQuery, " ORDER BY "), 185 | "LIMIT": strings.Index(upperQuery, " LIMIT "), 186 | } 187 | 188 | if indices["FROM"] == -1 { 189 | return 0, nil, fmt.Errorf("missing FROM clause in query") 190 | } 191 | 192 | return SimpleQuery, indices, nil 193 | } 194 | 195 | // parseOrderBy extracts and validates the ORDER BY clause 196 | func (o *SQLOptions) parseOrderBy(query string, indices map[string]int) error { 197 | if indices["ORDER BY"] == -1 { 198 | return nil 199 | } 200 | 201 | orderByStart := indices["ORDER BY"] + 9 202 | var orderByEnd int 203 | if indices["LIMIT"] != -1 { 204 | orderByEnd = indices["LIMIT"] 205 | } else { 206 | orderByEnd = len(query) 207 | } 208 | 209 | orderByStr := strings.TrimSpace(query[orderByStart:orderByEnd]) 210 | if orderByStr == "" { 211 | return fmt.Errorf("ORDER BY clause cannot be empty") 212 | } 213 | 214 | fields := strings.Split(orderByStr, ",") 215 | orderByFields := make([]printers.OrderByField, 0, len(fields)) 216 | 217 | for _, field := range fields { 218 | field = strings.TrimSpace(field) 219 | if field == "" { 220 | continue 221 | } 222 | 223 | parts := strings.Fields(field) 224 | if len(parts) == 0 { 225 | continue 226 | } 227 | 228 | fieldName := parts[0] 229 | // Check for possible alias 230 | if alias, err := o.checkColumnName(fieldName); err == nil { 231 | fieldName = alias 232 | } 233 | 234 | orderBy := printers.OrderByField{ 235 | Name: fieldName, 236 | Descending: false, 237 | } 238 | 239 | // Check for DESC/ASC modifier 240 | if len(parts) > 1 && strings.ToUpper(parts[1]) == "DESC" { 241 | orderBy.Descending = true 242 | } 243 | 244 | orderByFields = append(orderByFields, orderBy) 245 | } 246 | 247 | o.orderByFields = orderByFields 248 | return nil 249 | } 250 | 251 | // parseLimit extracts and validates the LIMIT clause 252 | func (o *SQLOptions) parseLimit(query string, indices map[string]int) error { 253 | if indices["LIMIT"] == -1 { 254 | return nil 255 | } 256 | 257 | limitStart := indices["LIMIT"] + 6 258 | limitStr := strings.TrimSpace(query[limitStart:]) 259 | 260 | // Check if there are other clauses after LIMIT 261 | if space := strings.Index(limitStr, " "); space != -1 { 262 | limitStr = limitStr[:space] 263 | } 264 | 265 | limit, err := strconv.Atoi(limitStr) 266 | if err != nil { 267 | return fmt.Errorf("invalid LIMIT value: %s", limitStr) 268 | } 269 | 270 | if limit < 0 { 271 | return fmt.Errorf("LIMIT cannot be negative: %d", limit) 272 | } 273 | 274 | o.limit = limit 275 | return nil 276 | } 277 | 278 | // parseQueryParts extracts and validates different parts of the query 279 | func (o *SQLOptions) parseQueryParts(query string, indices map[string]int, queryType QueryType) error { 280 | // Parse FROM resource (only one resource allowed) 281 | var fromEnd int 282 | if indices["WHERE"] != -1 { 283 | fromEnd = indices["WHERE"] 284 | } else if indices["ORDER BY"] != -1 { 285 | fromEnd = indices["ORDER BY"] 286 | } else if indices["LIMIT"] != -1 { 287 | fromEnd = indices["LIMIT"] 288 | } else { 289 | fromEnd = len(query) 290 | } 291 | 292 | fromPart := strings.TrimSpace(query[indices["FROM"]+5 : fromEnd]) 293 | resources := strings.Split(fromPart, ",") 294 | if len(resources) != 1 { 295 | return fmt.Errorf("only one resource allowed in FROM clause") 296 | } 297 | 298 | allResources := []string{resources[0]} 299 | 300 | if err := o.parseResources(allResources, queryType); err != nil { 301 | return err 302 | } 303 | 304 | // Parse SELECT fields 305 | selectFields := strings.TrimSpace(query[6:indices["FROM"]]) 306 | if err := o.parseFields(selectFields); err != nil { 307 | return err 308 | } 309 | 310 | // Parse WHERE clause if present 311 | if indices["WHERE"] != -1 { 312 | whereStart := indices["WHERE"] + 6 313 | var whereEnd int 314 | if indices["ORDER BY"] != -1 { 315 | whereEnd = indices["ORDER BY"] 316 | } else if indices["LIMIT"] != -1 { 317 | whereEnd = indices["LIMIT"] 318 | } else { 319 | whereEnd = len(query) 320 | } 321 | wherePart := strings.TrimSpace(query[whereStart:whereEnd]) 322 | if wherePart == "" { 323 | return fmt.Errorf("WHERE clause cannot be empty") 324 | } 325 | o.requestedQuery = wherePart 326 | } 327 | 328 | // Parse ORDER BY clause if present 329 | if err := o.parseOrderBy(query, indices); err != nil { 330 | return err 331 | } 332 | 333 | // Parse LIMIT clause if present 334 | if err := o.parseLimit(query, indices); err != nil { 335 | return err 336 | } 337 | 338 | return nil 339 | } 340 | 341 | // CompleteSQL parses SQL query into components 342 | func (o *SQLOptions) CompleteSQL(query string) error { 343 | queryType, indices, err := o.identifyQueryType(query) 344 | if err != nil { 345 | return err 346 | } 347 | 348 | if err := o.parseQueryParts(query, indices, queryType); err != nil { 349 | return err 350 | } 351 | 352 | return nil 353 | } 354 | 355 | // Get the resource list. 356 | func (o *SQLOptions) Get(config *rest.Config) error { 357 | c := client.Config{ 358 | Config: config, 359 | Namespace: o.namespace, 360 | } 361 | 362 | if len(o.requestedQuery) > 0 { 363 | return o.printFilteredResources(c) 364 | } 365 | 366 | return o.printResources(c) 367 | } 368 | 369 | // printResources prints resources lists. 370 | func (o *SQLOptions) printResources(c client.Config) error { 371 | ctx := context.Background() 372 | for _, r := range o.requestedResources { 373 | list, err := c.List(ctx, r) 374 | if err != nil { 375 | return err 376 | } 377 | 378 | err = o.Printer(list) 379 | if err != nil { 380 | return err 381 | } 382 | } 383 | 384 | return nil 385 | } 386 | 387 | // printFilteredResources prints filtered resource list. 388 | func (o *SQLOptions) printFilteredResources(c client.Config) error { 389 | ctx := context.Background() 390 | f := filter.Config{ 391 | CheckColumnName: o.checkColumnName, 392 | Query: o.requestedQuery, 393 | } 394 | 395 | // Print resources lists. 396 | for _, r := range o.requestedResources { 397 | list, err := c.List(ctx, r) 398 | if err != nil { 399 | return err 400 | } 401 | 402 | // Filter items by query. 403 | filteredList, err := f.Filter(list) 404 | if err != nil { 405 | return err 406 | } 407 | 408 | err = o.Printer(filteredList) 409 | if err != nil { 410 | return err 411 | } 412 | } 413 | 414 | return nil 415 | } 416 | -------------------------------------------------------------------------------- /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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 16 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 17 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 18 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 19 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 20 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 21 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 22 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 23 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 24 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 25 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 26 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 27 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 28 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 29 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 30 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 31 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 32 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 33 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 34 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 35 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 36 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 37 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 38 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 39 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 41 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 42 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 43 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 44 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 45 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 46 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 47 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 48 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 49 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 50 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= 52 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 53 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 54 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 55 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 56 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 57 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 58 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 59 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 60 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 61 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 62 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 63 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 64 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 65 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 66 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 68 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 69 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 70 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 71 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 72 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 73 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 74 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 75 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 77 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 78 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 79 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 80 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 81 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 82 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 83 | github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= 84 | github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= 85 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 86 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 87 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 88 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 89 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 90 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 91 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 92 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 93 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 95 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 96 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 97 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 98 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 99 | github.com/spf13/cobra v1.9.0 h1:Py5fIuq/lJsRYxcxfOtsJqpmwJWCMOUy2tMJYV8TNHE= 100 | github.com/spf13/cobra v1.9.0/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 101 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 102 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 103 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 104 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 105 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 106 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 107 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 108 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 109 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 110 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 111 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 112 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 113 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 114 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 115 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 116 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 117 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 118 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 119 | github.com/yaacov/tree-search-language/v6 v6.0.8 h1:BnrvqOOh/VS84EH0MSKaZHZEBTYOf6tAMUCdzOdOxuU= 120 | github.com/yaacov/tree-search-language/v6 v6.0.8/go.mod h1:ZXSpcgyyUZswjeC/OOYBnrvUWhhENumaV4+Xeg59NFk= 121 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 122 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 123 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 124 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 125 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 126 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 127 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 128 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 129 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 130 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 131 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 132 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 133 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 134 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 135 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 136 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 137 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 138 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 142 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 148 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 149 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 150 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 151 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 152 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 153 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 154 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 155 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 156 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 157 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 159 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 160 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 161 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 162 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 163 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 168 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 171 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 172 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 173 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 174 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 175 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 176 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 178 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= 180 | k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= 181 | k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= 182 | k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 183 | k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks= 184 | k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8= 185 | k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= 186 | k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= 187 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 188 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 189 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 190 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 191 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 192 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 193 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 194 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 195 | sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= 196 | sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= 197 | sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= 198 | sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= 199 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 200 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 201 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 202 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 203 | -------------------------------------------------------------------------------- /.github/img/kubesql.svg: -------------------------------------------------------------------------------- 1 | 2 | 22 | 24 | 25 | 27 | image/svg+xml 28 | 30 | 31 | 32 | 33 | 34 | 36 | 39 | 43 | 47 | 48 | 57 | 58 | 79 | 96 | 128 | 131 | 134 | 139 | 144 | 149 | 150 | 153 | 158 | 163 | 171 | 176 | 181 | 185 | 191 | 197 | 198 | 202 | 208 | 214 | 215 | 216 | 221 | 225 | 229 | 235 | 242 | 248 | 254 | 255 | 261 | 267 | 271 | 277 | 284 | 290 | 296 | 297 | 298 | 303 | 310 | 316 | 323 | 329 | 330 | 335 | 339 | 345 | 351 | 352 | 353 | 358 | 362 | 368 | 374 | 382 | 390 | 391 | 397 | 403 | 409 | 415 | 421 | 424 | 430 | 436 | 442 | 448 | 449 | 450 | 455 | 461 | 467 | 468 | 473 | 476 | 482 | 488 | 489 | 490 | 491 | 492 | 570 | 571 | --------------------------------------------------------------------------------