diff --git a/cmd/kubens/list.go b/cmd/kubens/list.go index 2a3e01f..d6fb4e4 100644 --- a/cmd/kubens/list.go +++ b/cmd/kubens/list.go @@ -22,10 +22,6 @@ func (op ListOp) Run(stdout, stderr io.Writer) error { kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { - if cmdutil.IsNotFoundErr(err) { - printer.Warning(stderr, "kubeconfig file not found") - return nil - } return errors.Wrap(err, "kubeconfig error") } @@ -38,13 +34,9 @@ func (op ListOp) Run(stdout, stderr io.Writer) error { return errors.Wrap(err, "cannot read current namespace") } - kubectl, err := findKubectl() + ns, err := queryNamespaces() if err != nil { - return err - } - ns, err := queryNamespaces(kubectl) - if err != nil { - return err + return errors.Wrap(err, "could not list namespaces (is the cluster accessible?)") } currentColor := color.New(color.FgGreen, color.Bold) @@ -68,7 +60,14 @@ func findKubectl() (string, error) { return v, errors.Wrap(err, "kubectl not found, needed for kubens") } -func queryNamespaces(kubectl string) ([]string, error) { +func queryNamespaces() ([]string, error) { + kubectl ,err := findKubectl() + if err != nil { + return nil ,err + } + + // TODO add a log message to user if kubectl is taking >1s + var b bytes.Buffer cmd := exec.Command(kubectl, "get", "namespaces", `-o=jsonpath={range .items[*].metadata.name}{@}{"\n"}{end}`) cmd.Env = os.Environ() diff --git a/cmd/kubens/statefile.go b/cmd/kubens/statefile.go index 52befde..61bcbc5 100644 --- a/cmd/kubens/statefile.go +++ b/cmd/kubens/statefile.go @@ -9,7 +9,7 @@ import ( "github.com/ahmetb/kubectx/internal/cmdutil" ) -var defaultDir = filepath.Join(cmdutil.HomeDir(), "kubens") +var defaultDir = filepath.Join(cmdutil.HomeDir(), ".kube", "kubens") type NSFile struct { dir string diff --git a/cmd/kubens/switch.go b/cmd/kubens/switch.go index 81f2401..76f2259 100644 --- a/cmd/kubens/switch.go +++ b/cmd/kubens/switch.go @@ -2,10 +2,81 @@ package main import ( "io" + + "github.com/pkg/errors" + + "github.com/ahmetb/kubectx/internal/cmdutil" + "github.com/ahmetb/kubectx/internal/kubeconfig" + "github.com/ahmetb/kubectx/internal/printer" ) -type SwitchOp struct{ Target string } - -func (s SwitchOp) Run(stdout, stderr io.Writer) error { - panic("implement me") +type SwitchOp struct { + Target string // '-' for back and forth, or NAME +} + +func (s SwitchOp) Run(_, stderr io.Writer) error { + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + defer kc.Close() + if err := kc.Parse(); err != nil { + return errors.Wrap(err, "kubeconfig error") + } + + ctx := kc.GetCurrentContext() + if ctx == "" { + return errors.New("current-context is not set") + } + curNS, err := kc.NamespaceOfContext(ctx) + if ctx == "" { + return errors.New("failed to get current namespace") + } + + f := NewNSFile(ctx) + prev, err := f.Load() + if err != nil { + return errors.Wrap(err, "failed to load previous namespace from file") + } + + toNS := s.Target + if s.Target == "-" { + if prev == "" { + return errors.Errorf("No previous namespace found for current context (%s)", ctx) + } + toNS = prev + } + + ok, err := namespaceExists(toNS) + if err != nil { + return errors.Wrap(err, "failed to query if namespace exists (is cluster accessible?)") + } + if !ok { + return errors.Errorf("no namespace exists with name %q", toNS) + } + + if err := kc.SetNamespace(ctx, toNS); err != nil { + return errors.Wrapf(err, "failed to change to namespace %q", toNS) + } + if err := kc.Save(); err != nil { + return errors.Wrap(err, "failed to save kubeconfig file") + } + if curNS != toNS { + if err := f.Save(curNS); err != nil { + return errors.Wrap(err, "failed to save the previous namespace to file") + } + } + + err = printer.Success(stderr, "Active namespace is %q", toNS) + return err +} + +func namespaceExists(ns string) (bool, error) { + nses, err := queryNamespaces() + if err != nil { + return false, err + } + for _, v := range nses { + if v == ns { + return true, nil + } + } + return false, nil } diff --git a/internal/kubeconfig/contextmodify.go b/internal/kubeconfig/contextmodify.go index b3e31c2..4015468 100644 --- a/internal/kubeconfig/contextmodify.go +++ b/internal/kubeconfig/contextmodify.go @@ -6,12 +6,9 @@ import ( ) func (k *Kubeconfig) DeleteContextEntry(deleteName string) error { - contexts := valueOf(k.rootNode, "contexts") - if contexts == nil { - return errors.New("there are no contexts in kubeconfig") - } - if contexts.Kind != yaml.SequenceNode { - return errors.New("'contexts' key is not a sequence") + contexts, err := k.contextsNode() + if err != nil { + return err } i := -1 @@ -51,11 +48,9 @@ func (k *Kubeconfig) ModifyCurrentContext(name string) error { } func (k *Kubeconfig) ModifyContextName(old, new string) error { - contexts := valueOf(k.rootNode, "contexts") - if contexts == nil { - return errors.New("\"contexts\" entry is nil") - } else if contexts.Kind != yaml.SequenceNode { - return errors.New("\"contexts\" is not a sequence node") + contexts, err := k.contextsNode() + if err != nil { + return err } var changed bool diff --git a/internal/kubeconfig/contexts.go b/internal/kubeconfig/contexts.go index d0d3f4b..cfcb616 100644 --- a/internal/kubeconfig/contexts.go +++ b/internal/kubeconfig/contexts.go @@ -1,9 +1,35 @@ package kubeconfig import ( + "github.com/pkg/errors" "gopkg.in/yaml.v3" ) +func (k *Kubeconfig) contextsNode() (*yaml.Node, error) { + contexts := valueOf(k.rootNode, "contexts") + if contexts == nil { + return nil, errors.New("\"contexts\" entry is nil") + } else if contexts.Kind != yaml.SequenceNode { + return nil, errors.New("\"contexts\" is not a sequence node") + } + return contexts, nil +} + +func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) { + contexts, err := k.contextsNode() + if err != nil { + return nil, err + } + + for _, contextNode := range contexts.Content { + nameNode := valueOf(contextNode, "name") + if nameNode.Kind == yaml.ScalarNode && nameNode.Value == name { + return contextNode, nil + } + } + return nil, errors.Errorf("context with name %q not found", name) +} + func (k *Kubeconfig) ContextNames() []string { contexts := valueOf(k.rootNode, "contexts") if contexts == nil { diff --git a/internal/kubeconfig/namespace.go b/internal/kubeconfig/namespace.go index 35852fa..fa9921e 100644 --- a/internal/kubeconfig/namespace.go +++ b/internal/kubeconfig/namespace.go @@ -1,31 +1,11 @@ package kubeconfig -import ( - "github.com/pkg/errors" - "gopkg.in/yaml.v3" -) +import "gopkg.in/yaml.v3" const ( defaultNamespace = "default" ) -func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) { - contexts := valueOf(k.rootNode, "contexts") - if contexts == nil { - return nil, errors.New("\"contexts\" entry is nil") - } else if contexts.Kind != yaml.SequenceNode { - return nil, errors.New("\"contexts\" is not a sequence node") - } - - for _, contextNode := range contexts.Content { - nameNode := valueOf(contextNode, "name") - if nameNode.Kind == yaml.ScalarNode && nameNode.Value == name { - return contextNode, nil - } - } - return nil, errors.Errorf("context with name %q not found", name) -} - func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) { ctx, err := k.contextNode(contextName) if err != nil { @@ -37,3 +17,26 @@ func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) { } return ns.Value, nil } + +func (k *Kubeconfig) SetNamespace(ctxName string, ns string) error { + ctx, err := k.contextNode(ctxName) + if err != nil { + return err + } + nsNode := valueOf(ctx, "namespace") + if nsNode != nil { + nsNode.Value = ns + return nil + } + + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "namespace", + Tag: "!!str"} + valueNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: ns, + Tag: "!!str"} + ctx.Content = append(ctx.Content, keyNode, valueNode) + return nil +} diff --git a/internal/kubeconfig/namespace_test.go b/internal/kubeconfig/namespace_test.go index ff663bb..fadfafb 100644 --- a/internal/kubeconfig/namespace_test.go +++ b/internal/kubeconfig/namespace_test.go @@ -3,6 +3,8 @@ package kubeconfig import ( "testing" + "github.com/google/go-cmp/cmp" + "github.com/ahmetb/kubectx/internal/testutil" ) @@ -44,3 +46,35 @@ func TestKubeconfig_NamespaceOfContext(t *testing.T) { t.Fatalf("c2: expected=%q got=%q", expected, v2) } } + +func TestKubeconfig_SetNamespace(t *testing.T) { + l := WithMockKubeconfigLoader(testutil.KC(). + WithCtxs( + testutil.Ctx("c1"), + testutil.Ctx("c2").Ns("c2n1")).ToYAML(t)) + kc := new(Kubeconfig).WithLoader(l) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + if err := kc.SetNamespace("c3", "foo"); err == nil { + t.Fatalf("expected error for non-existing ctx") + } + + if err := kc.SetNamespace("c1", "c1n1"); err != nil { + t.Fatal(err) + } + if err := kc.SetNamespace("c2", "c2n2"); err != nil { + t.Fatal(err) + } + if err := kc.Save(); err != nil { + t.Fatal(err) + } + + expected := testutil.KC().WithCtxs( + testutil.Ctx("c1").Ns("c1n1"), + testutil.Ctx("c2").Ns("c2n2")).ToYAML(t) + if diff := cmp.Diff(l.Output(), expected); diff != "" { + t.Fatal(diff) + } +}