Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete context with cluster and user #409

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
65 changes: 61 additions & 4 deletions cmd/kubectx/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ import (
// DeleteOp indicates intention to delete contexts.
type DeleteOp struct {
Contexts []string // NAME or '.' to indicate current-context.
Cascade bool // Whether to delete (orphaned-only) users and clusters referenced in the contexts.
}

// deleteContexts deletes context entries one by one.
func (op DeleteOp) Run(_, stderr io.Writer) error {
for _, ctx := range op.Contexts {
// TODO inefficency here. we open/write/close the same file many times.
deletedName, wasActiveContext, err := deleteContext(ctx)
deletedName, wasActiveContext, err := deleteContext(ctx, op.Cascade)
if err != nil {
return errors.Wrapf(err, "error deleting context \"%s\"", deletedName)
}
Expand All @@ -48,7 +49,9 @@ func (op DeleteOp) Run(_, stderr io.Writer) error {

// deleteContext deletes a context entry by NAME or current-context
// indicated by ".".
func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) {
// The cascade flag determines whether to also delete the user and/or cluster entries referenced in the context,
// if they became orphaned by this deletion (i.e., not referenced by any other contexts).
func deleteContext(name string, cascade bool) (deleteName string, wasActiveContext bool, err error) {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
Expand All @@ -59,18 +62,72 @@ func deleteContext(name string) (deleteName string, wasActiveContext bool, err e
// resolve "." to a real name
if name == "." {
if cur == "" {
return deleteName, false, errors.New("can't use '.' as the no active context is set")
return deleteName, false, errors.New("can't use '.' as no active context is set")
}
wasActiveContext = true
name = cur
}

wasActiveContext = name == cur

if !kc.ContextExists(name) {
return name, false, errors.New("context does not exist")
}

if cascade {
err = deleteContextUser(name, kc)
if err != nil {
return name, wasActiveContext, errors.Wrap(err, "failed to delete user for deleted context")
}

err = deleteContextCluster(name, kc)
if err != nil {
return name, wasActiveContext, errors.Wrap(err, "failed to delete cluster for deleted context")
}
}

if err := kc.DeleteContextEntry(name); err != nil {
return name, false, errors.Wrap(err, "failed to modify yaml doc")
}

return name, wasActiveContext, errors.Wrap(kc.Save(), "failed to save modified kubeconfig file")
}

func deleteContextUser(contextName string, kc *kubeconfig.Kubeconfig) error {
userName, err := kc.UserOfContext(contextName)
if err != nil {
return errors.Wrap(err, "user not set for context")
}

refCount, err := kc.CountUserReferences(userName)
if err != nil {
return errors.Wrap(err, "failed to retrieve reference count for user entry")
}

if refCount == 1 {
if err := kc.DeleteUserEntry(userName); err != nil {
return errors.Wrap(err, "failed to modify yaml doc")
}
}

return nil
}

func deleteContextCluster(contextName string, kc *kubeconfig.Kubeconfig) error {
clusterName, err := kc.ClusterOfContext(contextName)
if err != nil {
return errors.Wrap(err, "cluster not set for context")
}

refCount, err := kc.CountClusterReferences(clusterName)
if err != nil {
return errors.Wrap(err, "failed to retrieve reference count for cluster entry")
}

if refCount == 1 {
if err := kc.DeleteClusterEntry(clusterName); err != nil {
return errors.Wrap(err, "failed to modify yaml doc")
}
}

return nil
}
7 changes: 4 additions & 3 deletions cmd/kubectx/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ func parseArgs(argv []string) Op {
return ListOp{}
}

if argv[0] == "-d" {
if argv[0] == "-d" || argv[0] == "-D" {
cascade := argv[0] == "-D"
if len(argv) == 1 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveDeleteOp{SelfCmd: os.Args[0]}
return InteractiveDeleteOp{SelfCmd: os.Args[0], Cascade: cascade}
} else {
return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")}
}
}
return DeleteOp{Contexts: argv[1:]}
return DeleteOp{Contexts: argv[1:], Cascade: cascade}
}

if len(argv) == 1 {
Expand Down
10 changes: 8 additions & 2 deletions cmd/kubectx/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,16 @@ func Test_parseArgs_new(t *testing.T) {
want: UnsupportedOp{fmt.Errorf("'-d' needs arguments")}},
{name: "delete - current context",
args: []string{"-d", "."},
want: DeleteOp{[]string{"."}}},
want: DeleteOp{[]string{"."}, false}},
{name: "delete - multiple contexts",
args: []string{"-d", ".", "a", "b"},
want: DeleteOp{[]string{".", "a", "b"}}},
want: DeleteOp{[]string{".", "a", "b"}, false}},
{name: "delete cascading- current context",
args: []string{"-D", "."},
want: DeleteOp{[]string{"."}, true}},
{name: "delete cascading - multiple contexts",
args: []string{"-D", ".", "a", "b"},
want: DeleteOp{[]string{".", "a", "b"}, true}},
{name: "rename context",
args: []string{"a=b"},
want: RenameOp{"a", "b"}},
Expand Down
3 changes: 2 additions & 1 deletion cmd/kubectx/fzf.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type InteractiveSwitchOp struct {

type InteractiveDeleteOp struct {
SelfCmd string
Cascade bool
}

func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
Expand Down Expand Up @@ -112,7 +113,7 @@ func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error {
return errors.New("you did not choose any of the options")
}

name, wasActiveContext, err := deleteContext(choice)
name, wasActiveContext, err := deleteContext(choice, op.Cascade)
if err != nil {
return errors.Wrap(err, "failed to delete context")
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/kubectx/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func printUsage(out io.Writer) error {
%PROG% -d <NAME> [<NAME...>] : delete context <NAME> ('.' for current-context)
%SPAC% (this command won't delete the user/cluster entry
%SPAC% referenced by the context entry)
%PROG% -D <NAME> [<NAME...>] : delete context <NAME> ('.' for current-context)
%SPAC% (this command also deletes the user/cluster entry
%SPAC% referenced by the context entry, if it is no
%SPAC% longer referenced by any other context entry)
%PROG% -h,--help : show this message
%PROG% -V,--version : show version`
help = strings.ReplaceAll(help, "%PROG%", selfName())
Expand Down
68 changes: 68 additions & 0 deletions internal/kubeconfig/clusters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package kubeconfig

import (
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)

func (k *Kubeconfig) clustersNode() (*yaml.Node, error) {
clusters := valueOf(k.rootNode, "clusters")
if clusters == nil {
return nil, errors.New("\"clusters\" entry is nil")
} else if clusters.Kind != yaml.SequenceNode {
return nil, errors.New("\"clusters\" is not a sequence node")
}
return clusters, nil
}

func (k *Kubeconfig) ClusterOfContext(contextName string) (string, error) {
ctx, err := k.contextNode(contextName)
if err != nil {
return "", err
}

return k.clusterOfContextNode(ctx)
}

func (k *Kubeconfig) clusterOfContextNode(contextNode *yaml.Node) (string, error) {
ctxBody := valueOf(contextNode, "context")
if ctxBody == nil {
return "", errors.New("no context field found for context entry")
}

cluster := valueOf(ctxBody, "cluster")
if cluster == nil || cluster.Value == "" {
return "", errors.New("no cluster field found for context entry")
}
return cluster.Value, nil
}

func (k *Kubeconfig) CountClusterReferences(clusterName string) (int, error) {
contexts, err := k.contextsNode()
if err != nil {
return 0, err
}

count := 0
for _, contextNode := range contexts.Content {
contextCluster, err := k.clusterOfContextNode(contextNode)
if err != nil {
return 0, err
}
if clusterName == contextCluster {
count += 1
}
}

return count, nil
}

func (k *Kubeconfig) DeleteClusterEntry(deleteName string) error {
contexts, err := k.clustersNode()
if err != nil {
return err
}

deleteNamedChildNode(contexts, deleteName)
return nil
}
124 changes: 124 additions & 0 deletions internal/kubeconfig/clusters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package kubeconfig

import (
"testing"

"github.com/google/go-cmp/cmp"

"github.com/ahmetb/kubectx/internal/testutil"
)

func TestKubeconfig_ClusterOfContext_ctxNotFound(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCtxs(testutil.Ctx("c1")).ToYAML(t)))
if err := kc.Parse(); err != nil {
t.Fatal(err)
}

_, err := kc.ClusterOfContext("c2")
if err == nil {
t.Fatal("expected err")
}
}

func TestKubeconfig_ClusterOfContext(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCtxs(
testutil.Ctx("c1").Cluster("c1c1"),
testutil.Ctx("c2").Cluster("c2c2")).ToYAML(t)))
if err := kc.Parse(); err != nil {
t.Fatal(err)
}

v1, err := kc.ClusterOfContext("c1")
if err != nil {
t.Fatal("unexpected err", err)
}
if expected := `c1c1`; v1 != expected {
t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1)
}

v2, err := kc.ClusterOfContext("c2")
if err != nil {
t.Fatal("unexpected err", err)
}
if expected := `c2c2`; v2 != expected {
t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2)
}
}

func TestKubeconfig_DeleteClusterEntry_errors(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`))
_ = kc.Parse()
err := kc.DeleteClusterEntry("foo")
if err == nil {
t.Fatal("supposed to fail on non-mapping nodes")
}

kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`))
_ = kc.Parse()
err = kc.DeleteClusterEntry("foo")
if err == nil {
t.Fatal("supposed to fail if clusters key does not exist")
}

kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`clusters: "some string"`))
_ = kc.Parse()
err = kc.DeleteClusterEntry("foo")
if err == nil {
t.Fatal("supposed to fail if clusters key is not an array")
}
}

func TestKubeconfig_DeleteClusterEntry(t *testing.T) {
test := WithMockKubeconfigLoader(
testutil.KC().WithClusters(
testutil.Cluster("c1"),
testutil.Cluster("c2"),
testutil.Cluster("c3")).ToYAML(t))
kc := new(Kubeconfig).WithLoader(test)
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.DeleteClusterEntry("c1"); err != nil {
t.Fatal(err)
}
if err := kc.Save(); err != nil {
t.Fatal(err)
}

expected := testutil.KC().WithClusters(
testutil.Cluster("c2"),
testutil.Cluster("c3")).ToYAML(t)
out := test.Output()
if diff := cmp.Diff(expected, out); diff != "" {
t.Fatalf("diff: %s", diff)
}
}

func TestKubeconfig_CountClusterReferences_errors(t *testing.T) {
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
WithCtxs(
testutil.Ctx("c1").Cluster("c1c1"),
testutil.Ctx("c2").Cluster("c2c2"),
testutil.Ctx("c3").Cluster("c1c1")).ToYAML(t)))
if err := kc.Parse(); err != nil {
t.Fatal(err)
}

count1, err := kc.CountClusterReferences("c1c1")
if err != nil {
t.Fatal("unexpected err", err)
}
if expected := 2; count1 != expected {
t.Fatalf("c1: expected=\"%d\" got=\"%d\"", expected, count1)
}

count2, err := kc.CountClusterReferences("c2c2")
if err != nil {
t.Fatal("unexpected err", err)
}
if expected := 1; count2 != expected {
t.Fatalf("c1: expected=\"%d\" got=\"%d\"", expected, count2)
}
}
14 changes: 1 addition & 13 deletions internal/kubeconfig/contextmodify.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,7 @@ func (k *Kubeconfig) DeleteContextEntry(deleteName string) error {
return err
}

i := -1
for j, ctxNode := range contexts.Content {
nameNode := valueOf(ctxNode, "name")
if nameNode != nil && nameNode.Kind == yaml.ScalarNode && nameNode.Value == deleteName {
i = j
break
}
}
if i >= 0 {
copy(contexts.Content[i:], contexts.Content[i+1:])
contexts.Content[len(contexts.Content)-1] = nil
contexts.Content = contexts.Content[:len(contexts.Content)-1]
}
deleteNamedChildNode(contexts, deleteName)
return nil
}

Expand Down
Loading