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

feat: generate context name by specific attributes #956

Merged
merged 6 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions .github/workflows/e2e-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
echo "Running kubecm add..."
echo "********************************************************************************"
bin/kubecm add -cf 2nd-kind
bin/kubecm add -cf 3rd-kind --context-name 3rd
bin/kubecm add -cf 3rd-kind --context-prefix 3rd
echo "********************************************************************************"
echo "Running kubecm merge multiple kubeconfig..."
echo "********************************************************************************"
Expand All @@ -85,7 +85,7 @@ jobs:
echo "********************************************************************************"
echo "Running kubecm switch..."
echo "********************************************************************************"
bin/kubecm s 3rd
bin/kubecm s 3rd-kind-3rd-kind
echo "********************************************************************************"
echo "Running kubecm delete..."
echo "********************************************************************************"
Expand Down
94 changes: 66 additions & 28 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"reflect"
"strconv"
"strings"

"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
Expand Down Expand Up @@ -36,22 +37,34 @@ func (ac *AddCommand) Init() {
},
Example: addExample(),
}
ac.command.Flags().StringP("file", "f", "", "Path to merge kubeconfig files")
ac.command.Flags().String("context-name", "", "override context name when add kubeconfig context")
ac.command.PersistentFlags().BoolP("cover", "c", false, "Overwrite local kubeconfig files")
ac.command.PersistentFlags().Bool("select-context", false, "select the context to be added")
ac.command.Flags().StringP("file", "f", "", "path to merge kubeconfig files")
ac.command.Flags().String("context-prefix", "", "add a prefix before context name")
ac.command.Flags().String("context-name", "", "override context name when add kubeconfig context, when context-name is set, context-prefix and context-template parameters will be ignored")
ac.command.PersistentFlags().BoolP("cover", "c", false, "overwrite local kubeconfig files")
ac.command.Flags().Bool("select-context", false, "select the context to be added")
ac.command.Flags().StringSlice("context-template", []string{"context"}, "define the attributes used for composing the context name, available values: filename, user, cluster, context, namespace")
_ = ac.command.MarkFlagRequired("file")
ac.AddCommands(&DocsCommand{})
}

func (ac *AddCommand) runAdd(cmd *cobra.Command, args []string) error {
file, _ := ac.command.Flags().GetString("file")
cover, _ := ac.command.Flags().GetBool("cover")
contextPrefix, _ := ac.command.Flags().GetString("context-prefix")
contextName, _ := ac.command.Flags().GetString("context-name")
selectContext, _ := ac.command.Flags().GetBool("select-context")
contextTemplate, _ := ac.command.Flags().GetStringSlice("context-template")

var newConfig *clientcmdapi.Config
var err error

if contextName != "" {
contextTemplate = []string{}
contextPrefix = contextName
}
err := validateContextTemplate(contextTemplate)
if err != nil {
return err
}

if file == "-" {
// from stdin
Expand All @@ -75,15 +88,15 @@ func (ac *AddCommand) runAdd(cmd *cobra.Command, args []string) error {
}
}

err = AddToLocal(newConfig, file, contextName, cover, selectContext)
err = AddToLocal(newConfig, file, contextPrefix, cover, selectContext, contextTemplate)
if err != nil {
return err
}
return nil
}

// AddToLocal add kubeConfig to local
func AddToLocal(newConfig *clientcmdapi.Config, path, newName string, cover bool, selectContext bool) error {
func AddToLocal(newConfig *clientcmdapi.Config, path, contextPrefix string, cover bool, selectContext bool, contextTemplate []string) error {
oldConfig, err := clientcmd.LoadFromFile(cfgFile)
if err != nil {
return err
Expand All @@ -93,7 +106,7 @@ func AddToLocal(newConfig *clientcmdapi.Config, path, newName string, cover bool
fileName: getFileName(path),
}
// merge context loop
outConfig, err := kco.handleContexts(oldConfig, newName, selectContext)
outConfig, err := kco.handleContexts(oldConfig, contextPrefix, selectContext, contextTemplate)
if err != nil {
return err
}
Expand Down Expand Up @@ -121,21 +134,22 @@ func AddToLocal(newConfig *clientcmdapi.Config, path, newName string, cover bool
return nil
}

func (kc *KubeConfigOption) handleContexts(oldConfig *clientcmdapi.Config, contextName string, selectContext bool) (*clientcmdapi.Config, error) {
func (kc *KubeConfigOption) handleContexts(oldConfig *clientcmdapi.Config, contextPrefix string, selectContext bool, contextTemplate []string) (*clientcmdapi.Config, error) {
newConfig := clientcmdapi.NewConfig()
var index int
var newName string
generatedName := make(map[string]int)

for name, ctx := range kc.config.Contexts {
if len(kc.config.Contexts) > 1 {
if contextName == "" {
newName = fmt.Sprintf("%s-%s", kc.fileName, HashSufString(name))
} else {
newName = fmt.Sprintf("%s-%d", contextName, index)
}
} else if contextName == "" {
newName = name
} else {
newName = contextName
newName = kc.generateContextName(name, ctx, contextTemplate)
if contextPrefix != "" {
newName = strings.TrimSuffix(fmt.Sprintf("%s-%s", contextPrefix, newName), "-")
}

// prevent generate duplicate context name
// for example: set --context-template user,cluster, the context1 and context2 have the same user and cluster
generatedName[newName]++
if generatedName[newName] > 1 {
newName = fmt.Sprintf("%s-%d", newName, generatedName[newName])
}

if selectContext {
Expand All @@ -159,12 +173,32 @@ func (kc *KubeConfigOption) handleContexts(oldConfig *clientcmdapi.Config, conte
itemConfig := kc.handleContext(oldConfig, newName, ctx)
newConfig = appendConfig(newConfig, itemConfig)
fmt.Printf("Add Context: %s \n", newName)
index++
}
outConfig := appendConfig(oldConfig, newConfig)
return outConfig, nil
}

func (kc *KubeConfigOption) generateContextName(name string, ctx *clientcmdapi.Context, contextTemplate []string) string {
valueMap := map[string]string{
Filename: kc.fileName,
Context: name,
User: ctx.AuthInfo,
Cluster: ctx.Cluster,
Namespace: ctx.Namespace,
}

var contextValues []string
for _, value := range contextTemplate {
if v, ok := valueMap[value]; ok {
if v != "" {
contextValues = append(contextValues, v)
}
}
}

return strings.Join(contextValues, "-")
}

sunny0826 marked this conversation as resolved.
Show resolved Hide resolved
func checkContextName(name string, oldConfig *clientcmdapi.Config) bool {
if _, ok := oldConfig.Contexts[name]; ok {
return true
Expand Down Expand Up @@ -223,15 +257,19 @@ func (kc *KubeConfigOption) handleContext(oldConfig *clientcmdapi.Config,

func addExample() string {
return `
Note: If -c is set and more than one context is added to the kubeconfig file, the following will occur:
- If --context-name is set, the context will be generated as <context-name-0>, <context-name-1> ...
- If --context-name is not set, it will be generated as <file-name-{hash}> where {hash} is the MD5 hash of the file name.

# Merge test.yaml with $HOME/.kube/config
kubecm add -f test.yaml
# Merge test.yaml with $HOME/.kube/config and rename context name
kubecm add -cf test.yaml --context-name test
# Merge test.yaml with $HOME/.kube/config and add a prefix before context name
kubecm add -cf test.yaml --context-prefix test
# Merge test.yaml with $HOME/.kube/config and define the attributes used for composing the context name
kubecm add -f test.yaml --context-template user,cluster
# Merge test.yaml with $HOME/.kube/config, define the attributes used for composing the context name and add a prefix before context name
kubecm add -f test.yaml --context-template user,cluster --context-prefix demo
# Merge test.yaml with $HOME/.kube/config and override context name, it's useful if there is only one context in the kubeconfig file
kubecm add -f test.yaml --context-name test
# Merge test.yaml with $HOME/.kube/config and select the context to be added in interactive mode
kubecm add -f test.yaml --select-context
# Add kubeconfig from stdin
cat /etc/kubernetes/admin.conf | kubecm add -f -
cat /etc/kubernetes/admin.conf | kubecm add -f -
`
}
89 changes: 72 additions & 17 deletions cmd/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,23 @@ var (
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"black-user": {Token: "black-token"},
"red-user": {Token: "red-token"},
"red-user-7f65b9cc8f": {Token: "red-token"},
"black-user-gtch2cf96d": {Token: "black-token"},
"red-user-cbc897d6ch": {Token: "red-token"},
"black-user-d2m9fd8b7d": {Token: "black-token"},
"not-exist": {Token: "not-exist-token"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"pig-cluster": {Server: "http://pig.org:8080"},
"cow-cluster": {Server: "http://cow.org:8080"},
"cow-cluster-7f65b9cc8f": {Server: "http://cow.org:8080"},
"pig-cluster-gtch2cf96d": {Server: "http://pig.org:8080"},
"cow-cluster-cbc897d6ch": {Server: "http://cow.org:8080"},
"pig-cluster-d2m9fd8b7d": {Server: "http://pig.org:8080"},
"not-exist": {Server: "http://not.exist:8080"},
},
Contexts: map[string]*clientcmdapi.Context{
"root": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"},
"federal": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"},
"test-d2m9fd8b7d": {AuthInfo: "black-user-gtch2cf96d", Cluster: "pig-cluster-gtch2cf96d", Namespace: "saw-ns"},
"test-cbc897d6ch": {AuthInfo: "red-user-7f65b9cc8f", Cluster: "cow-cluster-7f65b9cc8f", Namespace: "hammer-ns"},
"test-2h6782585t": {AuthInfo: "not-exist", Cluster: "not-exist", Namespace: "not-exist-ns"},
"root": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"},
"federal": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"},
"root-context": {AuthInfo: "black-user-d2m9fd8b7d", Cluster: "pig-cluster-d2m9fd8b7d", Namespace: "saw-ns"},
"federal-context": {AuthInfo: "red-user-cbc897d6ch", Cluster: "cow-cluster-cbc897d6ch", Namespace: "hammer-ns"},
"not-exist-context": {AuthInfo: "not-exist", Cluster: "not-exist", Namespace: "not-exist-ns"},
},
}
singleTestConfig = clientcmdapi.Config{
Expand Down Expand Up @@ -110,6 +110,57 @@ var (
},
}
renameSingleTestConfig = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"black-user": {Token: "black-token"},
"red-user": {Token: "red-token"},
"single-user": {Token: "single-token"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"pig-cluster": {Server: "http://pig.org:8080"},
"cow-cluster": {Server: "http://cow.org:8080"},
"single-cluster": {Server: "http://single:8080"},
},
Contexts: map[string]*clientcmdapi.Context{
"root": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"},
"federal": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"},
"rename-single-context": {AuthInfo: "single-user", Cluster: "single-cluster", Namespace: "single-ns"},
},
}
contextTemplateTestConfig = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"black-user": {Token: "black-token"},
"red-user": {Token: "red-token"},
"single-user": {Token: "single-token"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"pig-cluster": {Server: "http://pig.org:8080"},
"cow-cluster": {Server: "http://cow.org:8080"},
"single-cluster": {Server: "http://single:8080"},
},
Contexts: map[string]*clientcmdapi.Context{
"root": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"},
"federal": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"},
"test-single-user-single-cluster": {AuthInfo: "single-user", Cluster: "single-cluster", Namespace: "single-ns"},
},
}
contextTemplateAndPrefixTestConfig = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"black-user": {Token: "black-token"},
"red-user": {Token: "red-token"},
"single-user": {Token: "single-token"},
},
Clusters: map[string]*clientcmdapi.Cluster{
"pig-cluster": {Server: "http://pig.org:8080"},
"cow-cluster": {Server: "http://cow.org:8080"},
"single-cluster": {Server: "http://single:8080"},
},
Contexts: map[string]*clientcmdapi.Context{
"root": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"},
"federal": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"},
"demo-single-user-single-cluster": {AuthInfo: "single-user", Cluster: "single-cluster", Namespace: "single-ns"},
},
}
contextNameTestConfig = clientcmdapi.Config{
AuthInfos: map[string]*clientcmdapi.AuthInfo{
"black-user": {Token: "black-token"},
"red-user": {Token: "red-token"},
Expand All @@ -123,7 +174,7 @@ var (
Contexts: map[string]*clientcmdapi.Context{
"root": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"},
"federal": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"},
"rename": {AuthInfo: "single-user", Cluster: "single-cluster", Namespace: "single-ns"},
"demo": {AuthInfo: "single-user", Cluster: "single-cluster", Namespace: "single-ns"},
},
}
)
Expand Down Expand Up @@ -192,8 +243,9 @@ func TestKubeConfig_handleContexts(t *testing.T) {
fileName string
}
type args struct {
oldConfig *clientcmdapi.Config
newName string
oldConfig *clientcmdapi.Config
contextPrefix string
contextTemplate []string
}
tests := []struct {
name string
Expand All @@ -203,17 +255,20 @@ func TestKubeConfig_handleContexts(t *testing.T) {
wantErr bool
}{
// TODO: Add test cases.
{"not have new context name", fields{config: newConfig, fileName: "test"}, args{&oldTestConfig, ""}, &mergedConfig, false},
{"single context name", fields{config: singleConfig, fileName: "test"}, args{&oldTestConfig, ""}, &mergeSingleTestConfig, false},
{"single context name - new", fields{config: singleConfig, fileName: "test"}, args{&oldTestConfig, "rename"}, &renameSingleTestConfig, false},
{"not have new context name", fields{config: newConfig, fileName: "test"}, args{&oldTestConfig, "", []string{"context"}}, &mergedConfig, false},
{"single context name", fields{config: singleConfig, fileName: "test"}, args{&oldTestConfig, "", []string{"context"}}, &mergeSingleTestConfig, false},
{"single context name - new", fields{config: singleConfig, fileName: "test"}, args{&oldTestConfig, "rename", []string{"context"}}, &renameSingleTestConfig, false},
{"set context template", fields{config: singleConfig, fileName: "test"}, args{&oldTestConfig, "", []string{"filename", "user", "cluster"}}, &contextTemplateTestConfig, false},
{"set context template and context prefix", fields{config: singleConfig, fileName: "test"}, args{&oldTestConfig, "demo", []string{"user", "cluster"}}, &contextTemplateAndPrefixTestConfig, false},
{"set context name", fields{config: singleConfig, fileName: "test"}, args{&oldTestConfig, "demo", []string{}}, &contextNameTestConfig, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kc := &KubeConfigOption{
config: tt.fields.config,
fileName: tt.fields.fileName,
}
got, err := kc.handleContexts(tt.args.oldConfig, tt.args.newName, false)
got, err := kc.handleContexts(tt.args.oldConfig, tt.args.contextPrefix, false, tt.args.contextTemplate)
if (err != nil) != tt.wantErr {
t.Errorf("handleContexts() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down Expand Up @@ -265,7 +320,7 @@ func TestAddToLocal(t *testing.T) {
}

// Test AddToLocal function
err = AddToLocal(newConfig, tempFile.Name(), "", true, false)
err = AddToLocal(newConfig, tempFile.Name(), "", true, false, []string{"context"})
if err != nil {
t.Fatalf("Failed to add to local: %v", err)
}
Expand Down
Loading
Loading