Skip to content

Commit 6f711a3

Browse files
author
Harry Li
authored
feat: Add Range Delete Command (#1088)
* feat: Add Range Delete Command * test: Add range_delete_test.go * docs: Add kubecm_range-delete.md and kubecm_range-delete_docs.md * feat(range-delete): extract methods for unit test and remove some comments in range_delete_test.go * feat: move range-delete command under delete command * test(e2e): add delete range CI tests using kind clusters (prefix/suffix/contains) * feat(delete): Adds the `--yes/-y` command to the delete range command to support forced deletion.
1 parent 2a4a223 commit 6f711a3

6 files changed

Lines changed: 380 additions & 0 deletions

File tree

.github/workflows/e2e-test.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ jobs:
5757
kind create cluster --image kindest/node:v1.21.14 --name 3rd-kind --kubeconfig 3rd-kind
5858
kind get clusters
5959
60+
- name: Setup test clusters for delete range
61+
run: |
62+
kind create cluster --image kindest/node:v1.21.14 --name dev-foo --kubeconfig dev-foo
63+
kind create cluster --image kindest/node:v1.21.14 --name foo-prod --kubeconfig foo-prod
64+
kind create cluster --image kindest/node:v1.21.14 --name foo-staging-bar --kubeconfig foo-staging-bar
65+
6066
- name: E2E Test
6167
run: |
6268
bin/kubecm version
@@ -91,6 +97,26 @@ jobs:
9197
echo "********************************************************************************"
9298
bin/kubecm d kind-kind
9399
echo "********************************************************************************"
100+
echo "Adding kind test clusters to kubecm for delete range tests..."
101+
echo "********************************************************************************"
102+
bin/kubecm add -cf dev-foo
103+
bin/kubecm add -cf foo-prod
104+
bin/kubecm add -cf foo-staging-bar
105+
echo "Context list after adding kind test clusters:"
106+
bin/kubecm list
107+
echo "********************************************************************************"
108+
echo "Test prefix mode: delete contexts starting with 'kind-dev-'"
109+
echo "********************************************************************************"
110+
bin/kubecm delete range kind-dev- -y
111+
echo "********************************************************************************"
112+
echo "Test suffix mode: delete contexts ending with 'prod'"
113+
echo "********************************************************************************"
114+
bin/kubecm delete range --mode suffix prod -y
115+
echo "********************************************************************************"
116+
echo "Test contains mode: delete contexts containing 'staging'"
117+
echo "********************************************************************************"
118+
bin/kubecm delete range --mode contains staging -y
119+
echo "********************************************************************************"
94120
echo "Running kubecm global flag --config..."
95121
echo "********************************************************************************"
96122
bin/kubecm s kind-2nd-kind --config merge.config

cmd/delete.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func (dc *DeleteCommand) Init() {
2727
},
2828
Example: deleteExample(),
2929
}
30+
dc.AddCommands(&RangeCommand{})
3031
dc.AddCommands(&DocsCommand{})
3132
}
3233

cmd/delete_range.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
"k8s.io/client-go/tools/clientcmd"
10+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
11+
)
12+
13+
// RangeCommand range subcommand for delete command
14+
type RangeCommand struct {
15+
BaseCommand
16+
matchMode string // support prefix, suffix, contains
17+
yes bool // skip confirmation prompt
18+
}
19+
20+
// Init RangeCommand
21+
func (rc *RangeCommand) Init() {
22+
rc.command = &cobra.Command{
23+
Use: "range",
24+
Short: "Delete contexts matching a pattern",
25+
Long: `Delete all contexts that match a specified pattern from the kubeconfig`,
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
return rc.runRange(cmd, args)
28+
},
29+
Example: rangeExample(),
30+
}
31+
32+
rc.command.Flags().StringVarP(&rc.matchMode, "mode", "", "prefix", "Match mode: prefix, suffix, or contains")
33+
rc.command.Flags().BoolVarP(&rc.yes, "yes", "y", false, "Skip confirmation prompt")
34+
rc.AddCommands(&DocsCommand{})
35+
}
36+
37+
func (rc *RangeCommand) runRange(command *cobra.Command, args []string) error {
38+
if len(args) == 0 {
39+
return errors.New("no pattern specified")
40+
}
41+
42+
config, err := clientcmd.LoadFromFile(cfgFile)
43+
if err != nil {
44+
return fmt.Errorf("failed to load kubeconfig file %q: %w", cfgFile, err)
45+
}
46+
47+
// Select contexts to delete
48+
needDeleteContexts, err := matchContexts(config.Contexts, args[0], rc.matchMode)
49+
if err != nil {
50+
return err
51+
}
52+
53+
if len(needDeleteContexts) == 0 {
54+
return errors.New("no contexts matched the specified pattern")
55+
}
56+
57+
// Confirm delete
58+
fmt.Printf("Found %d contexts matching %s mode with pattern %q:\n", len(needDeleteContexts), rc.matchMode, args[0])
59+
for _, ctx := range needDeleteContexts {
60+
fmt.Printf(" - %s\n", ctx)
61+
}
62+
63+
// Skip confirmation if -y/--yes flag is set
64+
if !rc.yes {
65+
if !strings.EqualFold(BoolUI(fmt.Sprintf("Are you sure you want to delete these %d contexts?", len(needDeleteContexts))), "True") {
66+
return errors.New("range delete operation cancelled")
67+
}
68+
}
69+
70+
if err := rangeDeleteContexts(needDeleteContexts, config); err != nil {
71+
return err
72+
}
73+
74+
if err := WriteConfig(true, cfgFile, config); err != nil {
75+
return fmt.Errorf("failed to write kubeconfig file %q: %w", cfgFile, err)
76+
}
77+
78+
return nil
79+
}
80+
81+
// matchContexts selects contexts that match the given pattern and mode.
82+
func matchContexts(contexts map[string]*clientcmdapi.Context, pattern, matchMode string) ([]string, error) {
83+
if pattern == "" {
84+
return nil, errors.New("pattern cannot be empty")
85+
}
86+
87+
validModes := map[string]bool{"prefix": true, "suffix": true, "contains": true}
88+
if !validModes[matchMode] {
89+
return nil, fmt.Errorf("invalid match mode: %s, must be one of: prefix, suffix, contains", matchMode)
90+
}
91+
92+
var matches []string
93+
for contextName := range contexts {
94+
var matched bool
95+
switch matchMode {
96+
case "prefix":
97+
matched = strings.HasPrefix(contextName, pattern)
98+
case "suffix":
99+
matched = strings.HasSuffix(contextName, pattern)
100+
case "contains":
101+
matched = strings.Contains(contextName, pattern)
102+
}
103+
if matched {
104+
matches = append(matches, contextName)
105+
}
106+
}
107+
108+
return matches, nil
109+
}
110+
111+
// rangeDeleteContexts deletes the specified contexts and their associated clusters and auth infos if not used elsewhere.
112+
func rangeDeleteContexts(needDeleteContexts []string, config *clientcmdapi.Config) error {
113+
for _, ctx := range needDeleteContexts {
114+
if _, exists := config.Contexts[ctx]; !exists {
115+
return fmt.Errorf("context %q does not exist", ctx)
116+
}
117+
118+
delContext := config.Contexts[ctx]
119+
isClusterNameExist, isUserNameExist := checkClusterAndUserNameExceptContextToDelete(config, delContext)
120+
121+
if !isUserNameExist {
122+
delete(config.AuthInfos, delContext.AuthInfo)
123+
}
124+
if !isClusterNameExist {
125+
delete(config.Clusters, delContext.Cluster)
126+
}
127+
delete(config.Contexts, ctx)
128+
129+
fmt.Printf("Context Delete:「%s」\n", ctx)
130+
}
131+
132+
return nil
133+
}
134+
135+
func rangeExample() string {
136+
return `
137+
# Delete all contexts with prefix "dev-"
138+
kubecm delete range dev-
139+
or
140+
kubecm delete range --mode prefix dev-
141+
142+
# Delete all contexts with suffix "prod"
143+
kubecm delete range --mode suffix prod
144+
145+
# Delete all contexts containing "staging"
146+
kubecm delete range --mode contains staging
147+
148+
# Force delete all contexts with prefix "dev-" (skip confirmation)
149+
kubecm delete range dev- -y
150+
`
151+
}

cmd/delete_range_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"reflect"
7+
"testing"
8+
9+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
10+
)
11+
12+
// create test config
13+
func createTestContexts() map[string]*clientcmdapi.Context {
14+
return map[string]*clientcmdapi.Context{
15+
"dev-cluster1": {Cluster: "cluster1", AuthInfo: "user1"},
16+
"dev-cluster2": {Cluster: "cluster2", AuthInfo: "user2"},
17+
"prod-cluster": {Cluster: "cluster3", AuthInfo: "user3"},
18+
"test-staging": {Cluster: "cluster4", AuthInfo: "user4"},
19+
}
20+
}
21+
22+
func TestMatchContexts(t *testing.T) {
23+
tests := []struct {
24+
name string
25+
contexts map[string]*clientcmdapi.Context
26+
pattern string
27+
matchMode string
28+
expected []string
29+
expectedError error
30+
}{
31+
{
32+
name: "prefix dev-",
33+
contexts: createTestContexts(),
34+
pattern: "dev-",
35+
matchMode: "prefix",
36+
expected: []string{"dev-cluster1", "dev-cluster2"},
37+
},
38+
{
39+
name: "suffix -cluster",
40+
contexts: createTestContexts(),
41+
pattern: "-cluster",
42+
matchMode: "suffix",
43+
expected: []string{"prod-cluster"},
44+
},
45+
{
46+
name: "contains staging",
47+
contexts: createTestContexts(),
48+
pattern: "staging",
49+
matchMode: "contains",
50+
expected: []string{"test-staging"},
51+
},
52+
{
53+
name: "no matching contexts",
54+
contexts: createTestContexts(),
55+
pattern: "nonexistent",
56+
matchMode: "contains",
57+
expected: nil,
58+
expectedError: nil,
59+
},
60+
{
61+
name: "invalid match mode",
62+
contexts: createTestContexts(),
63+
pattern: "dev-",
64+
matchMode: "invalid",
65+
expected: nil,
66+
expectedError: fmt.Errorf("invalid match mode: %s, must be one of: prefix, suffix, contains", "invalid"),
67+
},
68+
{
69+
name: "empty pattern",
70+
contexts: createTestContexts(),
71+
pattern: "",
72+
matchMode: "prefix",
73+
expected: nil,
74+
expectedError: errors.New("pattern cannot be empty"),
75+
},
76+
{
77+
name: "empty contexts",
78+
contexts: map[string]*clientcmdapi.Context{},
79+
pattern: "dev-",
80+
matchMode: "prefix",
81+
expected: nil,
82+
expectedError: nil,
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
got, err := matchContexts(tt.contexts, tt.pattern, tt.matchMode)
89+
90+
if tt.expectedError != nil {
91+
if err == nil || err.Error() != tt.expectedError.Error() {
92+
t.Errorf("expected error %v, got %v", tt.expectedError, err)
93+
}
94+
if got != nil {
95+
t.Errorf("expected nil result, got %v", got)
96+
}
97+
return
98+
}
99+
if err != nil {
100+
t.Errorf("unexpected error: %v", err)
101+
}
102+
103+
if !reflect.DeepEqual(got, tt.expected) {
104+
t.Errorf("expected matches %v, got %v", tt.expected, got)
105+
}
106+
})
107+
}
108+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## kubecm range-delete
2+
3+
Delete contexts matching a pattern
4+
5+
### Synopsis
6+
7+
Delete all contexts that match a specified pattern from the kubeconfig
8+
9+
```
10+
kubecm range-delete [flags]
11+
```
12+
13+
### Examples
14+
15+
```
16+
17+
# Delete all contexts with prefix "dev-"
18+
kubecm range-delete dev-
19+
or
20+
kubecm range-delete -m prefix dev-
21+
22+
# Delete all contexts with suffix "-prod"
23+
kubecm range-delete -m suffix -prod
24+
25+
# Delete all contexts containing "staging"
26+
kubecm range-delete -m contains staging
27+
28+
```
29+
30+
### Options
31+
32+
```
33+
-h, --help help for range-delete
34+
--mode string Match mode: prefix, suffix, or contains (default "prefix")
35+
```
36+
37+
### Options inherited from parent commands
38+
39+
```
40+
--config string path of kubeconfig (default "$HOME/.kube/config")
41+
--create Create a new kubeconfig file if not exists
42+
-m, --mac-notify enable to display Mac notification banner
43+
-s, --silence-table enable/disable output of context table on successful config update
44+
-u, --ui-size int number of list items to show in menu at once (default 10)
45+
```
46+
47+
### SEE ALSO
48+
49+
* [kubecm](kubecm.md) - KubeConfig Manager.
50+
* [kubecm range-delete docs](kubecm_range-delete_docs.md) - Open document website
51+

0 commit comments

Comments
 (0)