Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A kubectl plugin that runs commands against every context in your kubeconfig fil
- Run kubectl commands against all contexts simultaneously
- Parallel execution with configurable batching (default: 25 contexts at a time)
- Include/exclude contexts by name pattern
- Support for `version`, `get`, `logs`, `wait`, `top`, `events`, `api-resources`, `api-versions`, and `auth` subcommands
- Support for `list`, `version`, `get`, `logs`, `wait`, `top`, `events`, `api-resources`, `api-versions`, and `auth` subcommands
- Streaming log output with `-f` flag across all contexts
- Watch mode with `-w`/`--watch` flag on `get` and `events` subcommands
- Flexible output formatting:
Expand Down Expand Up @@ -122,6 +122,21 @@ kubectl x --exclude dev --exclude staging get pods
kubectl x --include prod --exclude "us-west" get pods
```

### List Command

List all contexts from your kubeconfig, one per line. Respects `--include` and `--exclude` filters, making it useful for previewing which contexts a command will target before running it:

```bash
# List all contexts
kubectl x list

# List only prod contexts
kubectl x list --include prod

# List prod contexts, excluding US West
kubectl x list --include prod --exclude "us-west"
```

### Version Command

Run `kubectl version` against all contexts:
Expand Down
27 changes: 27 additions & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
Use: "list",
Short: "List all matching contexts",
Long: `List all contexts from kubeconfig, optionally filtered by --include and --exclude.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runList()
},
}

func runList() error {
contexts, err := getContexts()
if err != nil {
return err
}
for _, ctx := range contexts {
fmt.Println(ctx)
}
return nil
}
104 changes: 104 additions & 0 deletions cmd/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cmd

import (
"bytes"
"io"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func writeMinimalKubeconfig(t *testing.T, contextNames []string) string {
t.Helper()
tmpDir := t.TempDir()
path := tmpDir + "/kubeconfig"

var contexts []map[string]interface{}
for _, name := range contextNames {
contexts = append(contexts, map[string]interface{}{"name": name})
}
data, err := yaml.Marshal(map[string]interface{}{
"apiVersion": "v1",
"kind": "Config",
"contexts": contexts,
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, data, 0600))
return path
}

func captureList(t *testing.T) (string, error) {
t.Helper()
old := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w

var buf bytes.Buffer
done := make(chan struct{})
go func() {
io.Copy(&buf, r)
close(done)
}()

runErr := runList()
w.Close()
<-done
os.Stdout = old
r.Close()
return buf.String(), runErr
}

func TestRunList(t *testing.T) {
t.Run("lists all contexts", func(t *testing.T) {
path := writeMinimalKubeconfig(t, []string{"dev-use1-gkg2", "prod-usw2-ejlr", "prod-use1-arj3"})
t.Setenv("KUBECONFIG", path)

out, err := captureList(t)
require.NoError(t, err)

lines := strings.Split(strings.TrimSpace(out), "\n")
assert.Equal(t, []string{"dev-use1-gkg2", "prod-usw2-ejlr", "prod-use1-arj3"}, lines)
})

t.Run("respects --include filter", func(t *testing.T) {
path := writeMinimalKubeconfig(t, []string{"dev-use1-gkg2", "prod-usw2-ejlr", "prod-use1-arj3"})
t.Setenv("KUBECONFIG", path)
filterPatterns = []string{"prod"}
t.Cleanup(func() { filterPatterns = []string{} })

out, err := captureList(t)
require.NoError(t, err)

lines := strings.Split(strings.TrimSpace(out), "\n")
assert.Equal(t, []string{"prod-usw2-ejlr", "prod-use1-arj3"}, lines)
})

t.Run("respects --exclude filter", func(t *testing.T) {
path := writeMinimalKubeconfig(t, []string{"dev-use1-gkg2", "prod-usw2-ejlr", "prod-use1-arj3"})
t.Setenv("KUBECONFIG", path)
excludePatterns = []string{"prod"}
t.Cleanup(func() { excludePatterns = []string{} })

out, err := captureList(t)
require.NoError(t, err)

lines := strings.Split(strings.TrimSpace(out), "\n")
assert.Equal(t, []string{"dev-use1-gkg2"}, lines)
})

t.Run("returns error when no contexts match filter", func(t *testing.T) {
path := writeMinimalKubeconfig(t, []string{"dev-use1-gkg2"})
t.Setenv("KUBECONFIG", path)
filterPatterns = []string{"prod"}
t.Cleanup(func() { filterPatterns = []string{} })

_, err := captureList(t)
require.Error(t, err)
assert.Contains(t, err.Error(), "no contexts match filter patterns")
})
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func init() {
rootCmd.PersistentFlags().StringArrayVar(&filterPatterns, "filter", []string{}, "Alias for --include")
rootCmd.PersistentFlags().MarkDeprecated("filter", "use --include instead")
rootCmd.PersistentFlags().StringArrayVarP(&excludePatterns, "exclude", "e", []string{}, "Exclude contexts by name using regex pattern (can be specified multiple times for OR logic)")
rootCmd.AddCommand(listCmd)
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(getCmd)
rootCmd.AddCommand(logsCmd)
Expand Down
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestRootCmd(t *testing.T) {
}

func TestRootCmdHasSubcommands(t *testing.T) {
expected := []string{"version", "get", "logs", "top", "wait", "events", "api-resources", "api-versions", "auth"}
expected := []string{"list", "version", "get", "logs", "top", "wait", "events", "api-resources", "api-versions", "auth"}
registered := make(map[string]bool)
for _, cmd := range rootCmd.Commands() {
registered[cmd.Use] = true
Expand Down
Loading