From c44c9306fd90240bfdcbbea5ad6604c2db604631 Mon Sep 17 00:00:00 2001 From: Maciej Malarz Date: Fri, 29 Nov 2024 15:40:56 +0100 Subject: [PATCH] Add filtering contexts --- cmd/kubectx/flags.go | 10 ++++++++++ cmd/kubectx/flags_test.go | 15 +++++++++++++++ cmd/kubectx/fzf.go | 2 +- cmd/kubectx/list.go | 27 ++++++++++++++++++++++++--- internal/kubeconfig/contexts.go | 15 +++++++++++++-- internal/kubeconfig/contexts_test.go | 25 ++++++++++++++++++++++--- 6 files changed, 85 insertions(+), 9 deletions(-) diff --git a/cmd/kubectx/flags.go b/cmd/kubectx/flags.go index 060cd1c..bb14d3b 100644 --- a/cmd/kubectx/flags.go +++ b/cmd/kubectx/flags.go @@ -40,6 +40,16 @@ func parseArgs(argv []string) Op { return ListOp{} } + if argv[0] == "--list" || argv[0] == "-l" { + if len(argv) == 1 { + return ListOp{} + } + if filters, ok := parseFilterSyntax(argv[1:]); ok { + return ListOp{Filters: filters} + } + return UnsupportedOp{Err: fmt.Errorf("'-l' filters must use A=B format")} + } + if argv[0] == "-d" { if len(argv) == 1 { if cmdutil.IsInteractiveMode(os.Stdout) { diff --git a/cmd/kubectx/flags_test.go b/cmd/kubectx/flags_test.go index 071e3bf..b182ac7 100644 --- a/cmd/kubectx/flags_test.go +++ b/cmd/kubectx/flags_test.go @@ -39,6 +39,21 @@ func Test_parseArgs_new(t *testing.T) { {name: "help long form", args: []string{"--help"}, want: HelpOp{}}, + {name: "list shorthand", + args: []string{"-l"}, + want: ListOp{}}, + {name: "list long form", + args: []string{"--list"}, + want: ListOp{}}, + {name: "list long form filters", + args: []string{"--list", "cluster=cl"}, + want: ListOp{Filters: map[string]string{"cluster": "cl"}}}, + {name: "list long form filters - wrong syntax", + args: []string{"--list", "cluster-cl"}, + want: UnsupportedOp{fmt.Errorf("'-l' filters must use A=B format")}}, + {name: "current long form", + args: []string{"--current"}, + want: CurrentOp{}}, {name: "current shorthand", args: []string{"-c"}, want: CurrentOp{}}, diff --git a/cmd/kubectx/fzf.go b/cmd/kubectx/fzf.go index 5006129..126eb69 100644 --- a/cmd/kubectx/fzf.go +++ b/cmd/kubectx/fzf.go @@ -88,7 +88,7 @@ func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error { } kc.Close() - if len(kc.ContextNames()) == 0 { + if len(kc.ContextNames(nil)) == 0 { return errors.New("no contexts found in config") } diff --git a/cmd/kubectx/list.go b/cmd/kubectx/list.go index f3893f1..3a45171 100644 --- a/cmd/kubectx/list.go +++ b/cmd/kubectx/list.go @@ -17,6 +17,7 @@ package main import ( "fmt" "io" + "strings" "facette.io/natsort" "github.com/pkg/errors" @@ -27,9 +28,29 @@ import ( ) // ListOp describes listing contexts. -type ListOp struct{} +type ListOp struct { + Filters map[string]string +} -func (_ ListOp) Run(stdout, stderr io.Writer) error { +// parseFilterSyntax parses multiple A=B form into a map[A]=B and returns +// whether it is parsed correctly. +func parseFilterSyntax(v []string) (map[string]string, bool) { + m := make(map[string]string) + for _, vv := range v { + s := strings.Split(vv, "=") + if len(s) != 2 { + return nil, false + } + key, value := s[0], s[1] + if key == "" || value == "" { + return nil, false + } + m[key] = value + } + return m, true +} + +func (op ListOp) Run(stdout, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { @@ -40,7 +61,7 @@ func (_ ListOp) Run(stdout, stderr io.Writer) error { return errors.Wrap(err, "kubeconfig error") } - ctxs := kc.ContextNames() + ctxs := kc.ContextNames(op.Filters) natsort.Sort(ctxs) cur := kc.GetCurrentContext() diff --git a/internal/kubeconfig/contexts.go b/internal/kubeconfig/contexts.go index e774389..4290596 100644 --- a/internal/kubeconfig/contexts.go +++ b/internal/kubeconfig/contexts.go @@ -44,7 +44,7 @@ func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) { return nil, errors.Errorf("context with name \"%s\" not found", name) } -func (k *Kubeconfig) ContextNames() []string { +func (k *Kubeconfig) ContextNames(filters map[string]string) []string { contexts := valueOf(k.rootNode, "contexts") if contexts == nil { return nil @@ -54,8 +54,19 @@ func (k *Kubeconfig) ContextNames() []string { } var ctxNames []string +ctxLoop: for _, ctx := range contexts.Content { nameVal := valueOf(ctx, "name") + for k, v := range filters { + ctxVal := valueOf(ctx, "context") + if ctxVal == nil { + continue ctxLoop + } + vVal := valueOf(ctxVal, k) + if vVal == nil || vVal.Value != v { + continue ctxLoop + } + } if nameVal != nil { ctxNames = append(ctxNames, nameVal.Value) } @@ -64,7 +75,7 @@ func (k *Kubeconfig) ContextNames() []string { } func (k *Kubeconfig) ContextExists(name string) bool { - ctxNames := k.ContextNames() + ctxNames := k.ContextNames(nil) for _, v := range ctxNames { if v == name { return true diff --git a/internal/kubeconfig/contexts_test.go b/internal/kubeconfig/contexts_test.go index 28d7bc3..de99701 100644 --- a/internal/kubeconfig/contexts_test.go +++ b/internal/kubeconfig/contexts_test.go @@ -33,20 +33,39 @@ func TestKubeconfig_ContextNames(t *testing.T) { t.Fatal(err) } - ctx := kc.ContextNames() + ctx := kc.ContextNames(nil) expected := []string{"abc", "def", "ghi"} if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) } } +func TestKubeconfig_ContextNames_Filtered(t *testing.T) { + tl := WithMockKubeconfigLoader( + testutil.KC().WithCtxs( + testutil.Ctx("abc").Ns("ns1"), + testutil.Ctx("def"), + testutil.Ctx("ghi").Ns("ns2"), + ).Set("field1", map[string]string{"bar": "zoo"}).ToYAML(t)) + kc := new(Kubeconfig).WithLoader(tl) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + ctx := kc.ContextNames(map[string]string{"namespace": "ns2"}) + expected := []string{"ghi"} + if diff := cmp.Diff(expected, ctx); diff != "" { + t.Fatalf("%s", diff) + } +} + func TestKubeconfig_ContextNames_noContextsEntry(t *testing.T) { tl := WithMockKubeconfigLoader(`a: b`) kc := new(Kubeconfig).WithLoader(tl) if err := kc.Parse(); err != nil { t.Fatal(err) } - ctx := kc.ContextNames() + ctx := kc.ContextNames(nil) var expected []string = nil if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff) @@ -59,7 +78,7 @@ func TestKubeconfig_ContextNames_nonArrayContextsEntry(t *testing.T) { if err := kc.Parse(); err != nil { t.Fatal(err) } - ctx := kc.ContextNames() + ctx := kc.ContextNames(nil) var expected []string = nil if diff := cmp.Diff(expected, ctx); diff != "" { t.Fatalf("%s", diff)