mirror of
https://github.com/ahmetb/kubectx.git
synced 2025-09-08 03:48:57 +00:00
Implement cascading delete command (-D)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -36,6 +36,7 @@ type InteractiveSwitchOp struct {
|
||||
|
||||
type InteractiveDeleteOp struct {
|
||||
SelfCmd string
|
||||
Cascade bool
|
||||
}
|
||||
|
||||
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
|
||||
@@ -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")
|
||||
}
|
||||
|
@@ -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())
|
||||
|
68
internal/kubeconfig/clusters.go
Normal file
68
internal/kubeconfig/clusters.go
Normal file
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
||||
|
68
internal/kubeconfig/users.go
Normal file
68
internal/kubeconfig/users.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func (k *Kubeconfig) usersNode() (*yaml.Node, error) {
|
||||
users := valueOf(k.rootNode, "users")
|
||||
if users == nil {
|
||||
return nil, errors.New("\"users\" entry is nil")
|
||||
} else if users.Kind != yaml.SequenceNode {
|
||||
return nil, errors.New("\"users\" is not a sequence node")
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (k *Kubeconfig) UserOfContext(contextName string) (string, error) {
|
||||
ctx, err := k.contextNode(contextName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return k.userOfContextNode(ctx)
|
||||
}
|
||||
|
||||
func (k *Kubeconfig) userOfContextNode(contextNode *yaml.Node) (string, error) {
|
||||
ctxBody := valueOf(contextNode, "context")
|
||||
if ctxBody == nil {
|
||||
return "", errors.New("no context field found for context entry")
|
||||
}
|
||||
|
||||
user := valueOf(ctxBody, "user")
|
||||
if user == nil || user.Value == "" {
|
||||
return "", errors.New("no user field found for context entry")
|
||||
}
|
||||
return user.Value, nil
|
||||
}
|
||||
|
||||
func (k *Kubeconfig) CountUserReferences(userName string) (int, error) {
|
||||
contexts, err := k.contextsNode()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, contextNode := range contexts.Content {
|
||||
contextUser, err := k.userOfContextNode(contextNode)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if userName == contextUser {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (k *Kubeconfig) DeleteUserEntry(deleteName string) error {
|
||||
contexts, err := k.usersNode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteNamedChildNode(contexts, deleteName)
|
||||
return nil
|
||||
}
|
22
internal/kubeconfig/yaml.go
Normal file
22
internal/kubeconfig/yaml.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func deleteNamedChildNode(node *yaml.Node, childName string) {
|
||||
i := -1
|
||||
for j, node := range node.Content {
|
||||
nameNode := valueOf(node, "name")
|
||||
if nameNode != nil && nameNode.Kind == yaml.ScalarNode && nameNode.Value == childName {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if i >= 0 {
|
||||
copy(node.Content[i:], node.Content[i+1:])
|
||||
node.Content[len(node.Content)-1] = nil
|
||||
node.Content = node.Content[:len(node.Content)-1]
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user