diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 5f11e922..a920c083 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -3,10 +3,8 @@ name: Go on: push: branches: - - "*" + - "master" pull_request: - branches: - - master jobs: test: diff --git a/.gitignore b/.gitignore index a3b21afe..49c53732 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ dist/ kubecm target -tmp \ No newline at end of file +tmp +mergeDir +config.yaml \ No newline at end of file diff --git a/cmd/add.go b/cmd/add.go index e257bf76..f0492a97 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -3,7 +3,7 @@ package cmd import ( "errors" "fmt" - "os" + "strconv" "strings" "github.com/spf13/cobra" @@ -20,26 +20,24 @@ type AddCommand struct { func (ac *AddCommand) Init() { ac.command = &cobra.Command{ Use: "add", - Short: "Merge configuration file with $HOME/.kube/config", - Long: "Merge configuration file with $HOME/.kube/config", + Short: "Add kubeconfig to $HOME/.kube/config", + Long: "Add kubeconfig to $HOME/.kube/config", RunE: func(cmd *cobra.Command, args []string) error { return ac.runAdd(cmd, args) }, Example: addExample(), } ac.command.Flags().StringP("file", "f", "", "Path to merge kubeconfig files") - ac.command.Flags().StringP("name", "n", "", "The name of contexts. if this field is null,it will be named with file name.") - ac.command.Flags().BoolP("cover", "c", false, "Overwrite the original kubeconfig file") _ = ac.command.MarkFlagRequired("file") } func (ac *AddCommand) runAdd(cmd *cobra.Command, args []string) error { file, _ := ac.command.Flags().GetString("file") - if fileNotExists(file) { - return errors.New(file + " file does not exist") + file, err := CheckAndTransformFilePath(file) + if err != nil { + return err } - name := ac.command.Flag("name").Value.String() - newConfig, err := formatNewConfig(file, name) + newConfig, newName, err := formatNewConfig(file) if err != nil { return err } @@ -48,25 +46,29 @@ func (ac *AddCommand) runAdd(cmd *cobra.Command, args []string) error { return err } outConfig := appendConfig(oldConfig, newConfig) - cover, _ := ac.command.Flags().GetBool("cover") - err = WriteConfig(cover, file, outConfig) + cover := BoolUI(fmt.Sprintf("Are you sure you want to add 「%s」 to the 「%s」context?", newName, cfgFile)) + confirm, err := strconv.ParseBool(cover) + if err != nil { + return err + } + err = WriteConfig(confirm, file, outConfig) if err != nil { return err } return nil } -func formatNewConfig(file, nameFlag string) (*clientcmdapi.Config, error) { +func formatNewConfig(file string) (*clientcmdapi.Config, string, error) { config, err := clientcmd.LoadFromFile(file) if err != nil { - return nil, err + return nil, "", err } if len(config.AuthInfos) != 1 { - return nil, errors.New("Only support add 1 context. You can use `merge` cmd") + return nil, "", errors.New("Only support add 1 context. You can use `merge` cmd") } - name, err := formatAndCheckName(file, nameFlag) + name, err := formatAndCheckName(file) if err != nil { - return nil, err + return nil, "", err } suffix := HashSuf(config) userName := fmt.Sprintf("user-%v", suffix) @@ -89,14 +91,16 @@ func formatNewConfig(file, nameFlag string) (*clientcmdapi.Config, error) { break } fmt.Printf("Context Add: %s \n", name) - return config, nil + return config, name, nil } -func formatAndCheckName(file, name string) (string, error) { - if name == "" { - n := strings.Split(file, "/") - result := strings.Split(n[len(n)-1], ".") - name = result[0] +func formatAndCheckName(file string) (string, error) { + n := strings.Split(file, "/") + result := strings.Split(n[len(n)-1], ".") + name := result[0] + nameConfirm := BoolUI(fmt.Sprintf("Need to rename 「%s」 context?", name)) + if nameConfirm == "True" { + name = PromptUI("Rename", name) } config, err := clientcmd.LoadFromFile(cfgFile) if err != nil { @@ -104,29 +108,15 @@ func formatAndCheckName(file, name string) (string, error) { } for key := range config.Contexts { if key == name { - return key, errors.New("The name: " + name + " already exists, please replace it") + return key, errors.New("The name: 「" + name + "」 already exists, please select another one.") } } return name, nil } -func fileNotExists(path string) bool { - _, err := os.Stat(path) //os.Stat获取文件信息 - if err != nil { - return !os.IsExist(err) - } - return false -} - func addExample() string { return ` -# Merge 1.yaml with $HOME/.kube/config -kubecm add -f 1.yaml - -# Merge 1.yaml and name contexts test with $HOME/.kube/config -kubecm add -f 1.yaml -n test - -# Overwrite the original kubeconfig file -kubecm add -f 1.yaml -c +# Merge test.yaml with $HOME/.kube/config +kubecm add -f test.yaml ` } diff --git a/cmd/add_test.go b/cmd/add_test.go deleted file mode 100644 index 772cae14..00000000 --- a/cmd/add_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package cmd - -import ( - "io/ioutil" - "os" - "reflect" - "strings" - "testing" - - apiequality "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/util/diff" - "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" -) - -var ( - addRootConfigConflictAlfa = clientcmdapi.Config{ - AuthInfos: map[string]*clientcmdapi.AuthInfo{ - "black-user": {Token: "black-token"}}, - Clusters: map[string]*clientcmdapi.Cluster{ - "pig-cluster": {Server: "http://pig.org:8080"}}, - Contexts: map[string]*clientcmdapi.Context{ - "root-context": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"}}, - } - addConfigAlfa = clientcmdapi.Config{ - AuthInfos: map[string]*clientcmdapi.AuthInfo{ - "red-user": {Token: "red-token"}}, - Clusters: map[string]*clientcmdapi.Cluster{ - "cow-cluster": {Server: "http://cow.org:8080"}}, - Contexts: map[string]*clientcmdapi.Context{ - "federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"}}, - } - addWantConfigAlfa = clientcmdapi.Config{ - AuthInfos: map[string]*clientcmdapi.AuthInfo{ - "user-gmbtgkhfch": {Token: "red-token"}}, - Clusters: map[string]*clientcmdapi.Cluster{ - "cluster-gmbtgkhfch": {Server: "http://cow.org:8080"}}, - Contexts: map[string]*clientcmdapi.Context{ - "name": {AuthInfo: "user-gmbtgkhfch", Cluster: "cluster-gmbtgkhfch", Namespace: "hammer-ns"}}, - } - addTestWantConfigAlfa = clientcmdapi.Config{ - AuthInfos: map[string]*clientcmdapi.AuthInfo{ - "user-gmbtgkhfch": {Token: "red-token"}}, - Clusters: map[string]*clientcmdapi.Cluster{ - "cluster-gmbtgkhfch": {Server: "http://cow.org:8080"}}, - Contexts: map[string]*clientcmdapi.Context{ - "test": {AuthInfo: "user-gmbtgkhfch", Cluster: "cluster-gmbtgkhfch", Namespace: "hammer-ns"}}, - } -) - -func Test_formatNewConfig(t *testing.T) { - rootConfig, _ := ioutil.TempFile("", "") - defer os.Remove(rootConfig.Name()) - configFile, _ := ioutil.TempFile("", "") - defer os.Remove(configFile.Name()) - _ = clientcmd.WriteToFile(addRootConfigConflictAlfa, rootConfig.Name()) - _ = clientcmd.WriteToFile(addConfigAlfa, configFile.Name()) - wantName := splitTempName(configFile.Name()) - wantConfig := clientcmdapi.NewConfig() - addWantConfigAlfa.DeepCopyInto(wantConfig) - for key, obj := range wantConfig.Contexts { - wantConfig.Contexts[wantName] = obj - delete(wantConfig.Contexts, key) - break - } - cfgFile = rootConfig.Name() - - type args struct { - file string - nameFlag string - } - tests := []struct { - name string - args args - want *clientcmdapi.Config - wantErr bool - }{ - // TODO: Add test cases. - {"name-is-null", args{configFile.Name(), ""}, wantConfig, false}, - {"name-is-set", args{configFile.Name(), "test"}, &addTestWantConfigAlfa, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := formatNewConfig(tt.args.file, tt.args.nameFlag) - if err != nil { - return - } - checkResult(tt.want, got, t) - }) - } -} - -func Test_formatAndCheckName(t *testing.T) { - rootConfig, _ := ioutil.TempFile("", "") - defer os.Remove(rootConfig.Name()) - configFile, _ := ioutil.TempFile("", "") - defer os.Remove(configFile.Name()) - _ = clientcmd.WriteToFile(addRootConfigConflictAlfa, rootConfig.Name()) - _ = clientcmd.WriteToFile(addConfigAlfa, configFile.Name()) - wantName := splitTempName(configFile.Name()) - cfgFile = rootConfig.Name() - - type args struct { - file string - name string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - // TODO: Add test cases. - {"name-is-null", args{configFile.Name(), ""}, wantName, false}, - {"name-is-set", args{configFile.Name(), "test"}, "test", false}, - {"name-is-exists", args{configFile.Name(), "root-context"}, "root-context", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := formatAndCheckName(tt.args.file, tt.args.name) - if (err != nil) != tt.wantErr { - t.Errorf("formatAndCheckName() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("formatAndCheckName() got = %v, want %v", got, tt.want) - } - }) - } -} - -func splitTempName(path string) string { - name := strings.Split(path, "/") - wantName := name[len(name)-1] - return wantName -} - -func checkResult(want, got *clientcmdapi.Config, t *testing.T) { - testSetNilMapsToEmpties(reflect.ValueOf(&got)) - testSetNilMapsToEmpties(reflect.ValueOf(&want)) - testClearLocationOfOrigin(got) - - if !apiequality.Semantic.DeepEqual(want, got) { - t.Errorf("diff: %v", diff.ObjectDiff(want, got)) - t.Errorf("expected: %#v\n actual: %#v", want, got) - } -} - -func testClearLocationOfOrigin(config *clientcmdapi.Config) { - for key, obj := range config.AuthInfos { - obj.LocationOfOrigin = "" - config.AuthInfos[key] = obj - } - for key, obj := range config.Clusters { - obj.LocationOfOrigin = "" - config.Clusters[key] = obj - } - for key, obj := range config.Contexts { - obj.LocationOfOrigin = "" - config.Contexts[key] = obj - } -} - -func testSetNilMapsToEmpties(curr reflect.Value) { - actualCurrValue := curr - if curr.Kind() == reflect.Ptr { - actualCurrValue = curr.Elem() - } - - switch actualCurrValue.Kind() { - case reflect.Map: - for _, mapKey := range actualCurrValue.MapKeys() { - currMapValue := actualCurrValue.MapIndex(mapKey) - testSetNilMapsToEmpties(currMapValue) - } - - case reflect.Struct: - for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { - currFieldValue := actualCurrValue.Field(fieldIndex) - - if currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil() { - newValue := reflect.MakeMap(currFieldValue.Type()) - currFieldValue.Set(newValue) - } else { - testSetNilMapsToEmpties(currFieldValue.Addr()) - } - } - - } - -} diff --git a/cmd/merge.go b/cmd/merge.go index a54e774e..4549b81b 100644 --- a/cmd/merge.go +++ b/cmd/merge.go @@ -3,6 +3,9 @@ package cmd import ( "fmt" "io/ioutil" + "strconv" + + "k8s.io/client-go/tools/clientcmd" "github.com/spf13/cobra" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -26,7 +29,6 @@ func (mc *MergeCommand) Init() { Example: mergeExample(), } mc.command.Flags().StringP("folder", "f", "", "Kubeconfig folder") - mc.command.Flags().BoolP("cover", "c", false, "Overwrite the original kubeconfig file") _ = mc.command.MarkFlagRequired("folder") } @@ -36,14 +38,18 @@ func (mc MergeCommand) runMerge(command *cobra.Command, args []string) error { mc.command.Printf("Loading kubeconfig file: %v \n", files) configs := clientcmdapi.NewConfig() for _, yaml := range files { - config, err := formatNewConfig(yaml, "") + config, err := clientcmd.LoadFromFile(yaml) if err != nil { return err } configs = appendConfig(configs, config) } - cover, _ := mc.command.Flags().GetBool("cover") - err := WriteConfig(cover, folder, configs) + cover := BoolUI(fmt.Sprintf("Are you sure you want to overwrite the「%s」file?", cfgFile)) + confirm, err := strconv.ParseBool(cover) + if err != nil { + return err + } + err = WriteConfig(confirm, folder, configs) if err != nil { return err } @@ -67,8 +73,5 @@ func mergeExample() string { return ` # Merge kubeconfig in the dir directory kubecm merge -f dir - -# Merge kubeconfig in the directory and overwrite the original kubeconfig file -kubecm merge -f dir -c ` } diff --git a/cmd/root.go b/cmd/root.go index f1ab4516..0cc247dc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,7 +45,7 @@ Manage your kubeconfig more easily. | <| |_| | |_) | __/ (__| | | | | | |_|\_\\__,_|_.__/ \___|\___|_| |_| |_| -Find more information at: https://github.com/sunny0826/kubecm +Find more information at: https://kubecm.cloud `, }, } @@ -62,6 +62,15 @@ func (cli *Cli) setFlags() { //Run command func (cli *Cli) Run() error { + // check and format kubeconfig path + config, err := CheckAndTransformFilePath(cfgFile) + if err != nil { + return err + } + err = flag.Set("config", config) + if err != nil { + return err + } return cli.rootCmd.Execute() } diff --git a/cmd/utils.go b/cmd/utils.go index ad87db9e..f7efe73e 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -9,6 +9,7 @@ import ( "log" "os" "os/user" + "path/filepath" "strings" "github.com/bndr/gotabulate" @@ -182,7 +183,7 @@ func BoolUI(label string) string { } prompt := promptui.Select{ Label: label, - Items: []string{"True", "False"}, + Items: []string{"False", "True"}, Templates: templates, Size: 2, } @@ -322,3 +323,15 @@ func appendConfig(c1, c2 *clientcmdapi.Config) *clientcmdapi.Config { _ = mergo.Merge(config, c2) return config } + +// CheckAndTransformFilePath return converted path +func CheckAndTransformFilePath(path string) (string, error) { + if strings.HasPrefix(path, "~/") { + path = filepath.Join(homeDir(), path[2:]) + } + _, err := os.Stat(path) //os.Stat获取文件信息 + if err != nil { + return "", err + } + return path, nil +} diff --git a/cmd/utils_test.go b/cmd/utils_test.go index 75c416a9..ad390ef4 100644 --- a/cmd/utils_test.go +++ b/cmd/utils_test.go @@ -1,8 +1,12 @@ package cmd import ( + "reflect" "testing" + apiequality "k8s.io/apimachinery/pkg/api/equality" + + "k8s.io/apimachinery/pkg/util/diff" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) @@ -55,7 +59,62 @@ func Test_appendConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := appendConfig(tt.args.c1, tt.args.c2) - checkResult(tt.want, got, t) + checkResult(tt.want, got, "", t) }) } } + +func checkResult(want, got *clientcmdapi.Config, wantname string, t *testing.T) { + testSetNilMapsToEmpties(reflect.ValueOf(&got)) + testSetNilMapsToEmpties(reflect.ValueOf(&want)) + testClearLocationOfOrigin(got) + + if !apiequality.Semantic.DeepEqual(want, got) { + t.Errorf("diff: %v", diff.ObjectDiff(want, got)) + t.Errorf("expected: %#v\n actual: %#v", want, got) + } +} + +func testClearLocationOfOrigin(config *clientcmdapi.Config) { + for key, obj := range config.AuthInfos { + obj.LocationOfOrigin = "" + config.AuthInfos[key] = obj + } + for key, obj := range config.Clusters { + obj.LocationOfOrigin = "" + config.Clusters[key] = obj + } + for key, obj := range config.Contexts { + obj.LocationOfOrigin = "" + config.Contexts[key] = obj + } +} + +func testSetNilMapsToEmpties(curr reflect.Value) { + actualCurrValue := curr + if curr.Kind() == reflect.Ptr { + actualCurrValue = curr.Elem() + } + + switch actualCurrValue.Kind() { + case reflect.Map: + for _, mapKey := range actualCurrValue.MapKeys() { + currMapValue := actualCurrValue.MapIndex(mapKey) + testSetNilMapsToEmpties(currMapValue) + } + + case reflect.Struct: + for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { + currFieldValue := actualCurrValue.Field(fieldIndex) + + if currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil() { + newValue := reflect.MakeMap(currFieldValue.Type()) + currFieldValue.Set(newValue) + } else { + testSetNilMapsToEmpties(currFieldValue.Addr()) + } + } + + } + +} diff --git a/docs/README.md b/docs/README.md index 080816ab..75257c2f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ Manage your kubeconfig more easily. | <| |_| | |_) | __/ (__| | | | | | |_|\_\\__,_|_.__/ \___|\___|_| |_| |_| -Find more information at: https://github.com/sunny0826/kubecm +Find more information at: https://kubecm.cloud Usage: kubecm [flags] diff --git a/docs/en-us/cli/kubecm_add.md b/docs/en-us/cli/kubecm_add.md index 6b267686..b41249ef 100644 --- a/docs/en-us/cli/kubecm_add.md +++ b/docs/en-us/cli/kubecm_add.md @@ -1,10 +1,10 @@ ## kubecm add -Merge configuration file with $HOME/.kube/config +Add kubeconfig to $HOME/.kube/config ### Synopsis -Merge configuration file with $HOME/.kube/config +Add kubeconfig to $HOME/.kube/config ``` kubecm add [flags] @@ -14,28 +14,20 @@ kubecm add [flags] ``` -# Merge 1.yaml with $HOME/.kube/config -kubecm add -f 1.yaml - -# Merge 1.yaml and name contexts test with $HOME/.kube/config -kubecm add -f 1.yaml -n test - -# Overwrite the original kubeconfig file -kubecm add -f 1.yaml -c +# Merge test.yaml with $HOME/.kube/config +kubecm add -f test.yaml ``` ### Options ``` - -c, --cover Overwrite the original kubeconfig file -f, --file string Path to merge kubeconfig files -h, --help help for add - -n, --name string The name of contexts. if this field is null,it will be named with file name. ``` ### Options inherited from parent commands ``` - --config string path of kubeconfig (default "/Users/guoxudong/.kube/config") + --config string path of kubeconfig (default "/Users/saybot/.kube/config") ``` diff --git a/docs/en-us/cli/kubecm_merge.md b/docs/en-us/cli/kubecm_merge.md index f4ffa4dd..8a634e32 100644 --- a/docs/en-us/cli/kubecm_merge.md +++ b/docs/en-us/cli/kubecm_merge.md @@ -17,15 +17,11 @@ kubecm merge [flags] # Merge kubeconfig in the dir directory kubecm merge -f dir -# Merge kubeconfig in the directory and overwrite the original kubeconfig file -kubecm merge -f dir -c - ``` ### Options ``` - -c, --cover Overwrite the original kubeconfig file -f, --folder string Kubeconfig folder -h, --help help for merge ``` @@ -33,5 +29,5 @@ kubecm merge -f dir -c ### Options inherited from parent commands ``` - --config string path of kubeconfig (default "/Users/guoxudong/.kube/config") + --config string path of kubeconfig (default "/Users/saybot/.kube/config") ``` diff --git a/docs/zh-cn/cli/kubecm_add.md b/docs/zh-cn/cli/kubecm_add.md index 7ec71e40..7559ca44 100644 --- a/docs/zh-cn/cli/kubecm_add.md +++ b/docs/zh-cn/cli/kubecm_add.md @@ -17,21 +17,13 @@ kubecm add [flags] # Merge 1.yaml with $HOME/.kube/config kubecm add -f 1.yaml -# Merge 1.yaml and name contexts test with $HOME/.kube/config -kubecm add -f 1.yaml -n test - -# Overwrite the original kubeconfig file -kubecm add -f 1.yaml -c - ``` ### 选项 ``` - -c, --cover Overwrite the original kubeconfig file -f, --file string Path to merge kubeconfig files -h, --help help for add - -n, --name string The name of contexts. if this field is null,it will be named with file name. ``` ### 全局选项 diff --git a/docs/zh-cn/cli/kubecm_merge.md b/docs/zh-cn/cli/kubecm_merge.md index 5a9dbc1d..e1a78819 100644 --- a/docs/zh-cn/cli/kubecm_merge.md +++ b/docs/zh-cn/cli/kubecm_merge.md @@ -17,15 +17,11 @@ kubecm merge [flags] # Merge kubeconfig in the dir directory kubecm merge -f dir -# Merge kubeconfig in the directory and overwrite the original kubeconfig file -kubecm merge -f dir -c - ``` ### 选项 ``` - -c, --cover Overwrite the original kubeconfig file -f, --folder string Kubeconfig folder -h, --help help for merge ```