mirror of
https://github.com/ahmetb/kubectx.git
synced 2025-08-27 19:38:37 +00:00
Merge f7af8b8d27
into 8fb8c9f2f2
This commit is contained in:
commit
a7e8cfe4db
@ -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 {
|
||||
|
@ -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"}},
|
||||
|
@ -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
|
||||
}
|
124
internal/kubeconfig/clusters_test.go
Normal file
124
internal/kubeconfig/clusters_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
124
internal/kubeconfig/users_test.go
Normal file
124
internal/kubeconfig/users_test.go
Normal file
@ -0,0 +1,124 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/ahmetb/kubectx/internal/testutil"
|
||||
)
|
||||
|
||||
func TestKubeconfig_UserOfContext_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.UserOfContext("c2")
|
||||
if err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubeconfig_UserOfContext(t *testing.T) {
|
||||
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
|
||||
WithCtxs(
|
||||
testutil.Ctx("c1").User("c1u1"),
|
||||
testutil.Ctx("c2").User("c2u2")).ToYAML(t)))
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
v1, err := kc.UserOfContext("c1")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if expected := `c1u1`; v1 != expected {
|
||||
t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1)
|
||||
}
|
||||
|
||||
v2, err := kc.UserOfContext("c2")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if expected := `c2u2`; v2 != expected {
|
||||
t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubeconfig_DeleteUserEntry_errors(t *testing.T) {
|
||||
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`))
|
||||
_ = kc.Parse()
|
||||
err := kc.DeleteUserEntry("foo")
|
||||
if err == nil {
|
||||
t.Fatal("supposed to fail on non-mapping nodes")
|
||||
}
|
||||
|
||||
kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`))
|
||||
_ = kc.Parse()
|
||||
err = kc.DeleteUserEntry("foo")
|
||||
if err == nil {
|
||||
t.Fatal("supposed to fail if users key does not exist")
|
||||
}
|
||||
|
||||
kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`users: "some string"`))
|
||||
_ = kc.Parse()
|
||||
err = kc.DeleteUserEntry("foo")
|
||||
if err == nil {
|
||||
t.Fatal("supposed to fail if users key is not an array")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubeconfig_DeleteUserEntry(t *testing.T) {
|
||||
test := WithMockKubeconfigLoader(
|
||||
testutil.KC().WithUsers(
|
||||
testutil.User("u1"),
|
||||
testutil.User("u2"),
|
||||
testutil.User("u3")).ToYAML(t))
|
||||
kc := new(Kubeconfig).WithLoader(test)
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := kc.DeleteUserEntry("u1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := kc.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := testutil.KC().WithUsers(
|
||||
testutil.User("u2"),
|
||||
testutil.User("u3")).ToYAML(t)
|
||||
out := test.Output()
|
||||
if diff := cmp.Diff(expected, out); diff != "" {
|
||||
t.Fatalf("diff: %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubeconfig_CountUserReferences_errors(t *testing.T) {
|
||||
kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC().
|
||||
WithCtxs(
|
||||
testutil.Ctx("c1").User("c1u1"),
|
||||
testutil.Ctx("c2").User("c2u2"),
|
||||
testutil.Ctx("c3").User("c1u1")).ToYAML(t)))
|
||||
if err := kc.Parse(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
count1, err := kc.CountUserReferences("c1u1")
|
||||
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.CountUserReferences("c2u2")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if expected := 1; count2 != expected {
|
||||
t.Fatalf("c1: expected=\"%d\" got=\"%d\"", expected, count2)
|
||||
}
|
||||
}
|
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]
|
||||
}
|
||||
}
|
@ -21,15 +21,31 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
type ContextObj struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Context struct {
|
||||
Namespace string `yaml:"namespace,omitempty"`
|
||||
User string `yaml:"user,omitempty"`
|
||||
Cluster string `yaml:"cluster,omitempty"`
|
||||
} `yaml:"context,omitempty"`
|
||||
}
|
||||
|
||||
func Ctx(name string) *Context { return &Context{Name: name} }
|
||||
func (c *Context) Ns(ns string) *Context { c.Context.Namespace = ns; return c }
|
||||
func Ctx(name string) *ContextObj { return &ContextObj{Name: name} }
|
||||
func (c *ContextObj) Ns(ns string) *ContextObj { c.Context.Namespace = ns; return c }
|
||||
func (c *ContextObj) User(user string) *ContextObj { c.Context.User = user; return c }
|
||||
func (c *ContextObj) Cluster(cluster string) *ContextObj { c.Context.Cluster = cluster; return c }
|
||||
|
||||
type UserObj struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
func User(name string) *UserObj { return &UserObj{Name: name} }
|
||||
|
||||
type ClusterObj struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
func Cluster(name string) *ClusterObj { return &ClusterObj{Name: name} }
|
||||
|
||||
type Kubeconfig map[string]interface{}
|
||||
|
||||
@ -41,7 +57,9 @@ func KC() *Kubeconfig {
|
||||
|
||||
func (k *Kubeconfig) Set(key string, v interface{}) *Kubeconfig { (*k)[key] = v; return k }
|
||||
func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k }
|
||||
func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k }
|
||||
func (k *Kubeconfig) WithCtxs(c ...*ContextObj) *Kubeconfig { (*k)["contexts"] = c; return k }
|
||||
func (k *Kubeconfig) WithUsers(u ...*UserObj) *Kubeconfig { (*k)["users"] = u; return k }
|
||||
func (k *Kubeconfig) WithClusters(c ...*ClusterObj) *Kubeconfig { (*k)["clusters"] = c; return k }
|
||||
|
||||
func (k *Kubeconfig) ToYAML(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
@ -27,6 +27,14 @@ get_context() {
|
||||
kubectl config current-context
|
||||
}
|
||||
|
||||
get_user() {
|
||||
kubectl config get-users | grep "${1}"
|
||||
}
|
||||
|
||||
get_cluster() {
|
||||
kubectl config get-clusters | grep "${1}"
|
||||
}
|
||||
|
||||
switch_context() {
|
||||
kubectl config use-context "${1}"
|
||||
}
|
||||
|
@ -230,6 +230,28 @@ load common
|
||||
[[ "$output" = "user2@cluster1" ]]
|
||||
}
|
||||
|
||||
@test "delete context including referenced user and cluster" {
|
||||
use_config config1
|
||||
|
||||
run ${COMMAND} -D "user1@cluster1"
|
||||
echo "$output"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ -z "$(get_user user1)" ]]
|
||||
[[ -z "$(get_cluster cluster1)" ]]
|
||||
|
||||
}
|
||||
|
||||
@test "delete context retain referenced cluster" {
|
||||
use_config config2
|
||||
|
||||
run ${COMMAND} -D "user1@cluster1"
|
||||
echo "$output"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ -z "$(get_user user1)" ]]
|
||||
[[ -n "$(get_user user2)" ]]
|
||||
[[ -n "$(get_cluster cluster1)" ]]
|
||||
}
|
||||
|
||||
@test "unset selected context" {
|
||||
use_config config2
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user