diff --git a/go.mod b/go.mod index 31b12b7..6b2811d 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/Masterminds/sprig v2.22.0+incompatible github.com/Pallinder/go-randomdata v1.2.0 + github.com/evanphx/json-patch v4.9.0+incompatible github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 github.com/huandu/xstrings v1.3.2 // indirect github.com/linuxsuren/cobra-extension v0.0.11 diff --git a/kubectl-plugin/config/migrate.go b/kubectl-plugin/config/migrate.go index e1eeca8..b786b86 100644 --- a/kubectl-plugin/config/migrate.go +++ b/kubectl-plugin/config/migrate.go @@ -2,15 +2,17 @@ package config import ( "context" + "encoding/json" "errors" "fmt" + + jsonpatch "github.com/evanphx/json-patch" "github.com/kubesphere-sigs/ks/kubectl-plugin/types" "github.com/spf13/cobra" "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" - "strings" ) func newMigrateCmd(client dynamic.Interface) (cmd *cobra.Command) { @@ -48,12 +50,26 @@ func (o *migrateOption) preRunE(cmd *cobra.Command, args []string) (err error) { func (o *migrateOption) runE(cmd *cobra.Command, args []string) (err error) { if err = o.updateKubeSphereConfig("kubesphere-config", "kubesphere-system", map[string]interface{}{ - "enable": false, - "devopsServiceAddress": o.service, + "devops": map[string]interface{}{ + "enable": false, + "devopsServiceAddress": o.service, + }, }); err != nil { return } + patchData := make(map[string]interface{}) + + kubesphereConfig, _, err := o.getKubeSphereConfig("kubesphere-config", "kubesphere-system") + if err != nil { + return err + } + if sonarQube, found, err := unstructured.NestedMap(kubesphereConfig, "sonarQube"); err != nil { + return err + } else if found { + patchData["sonarQube"] = sonarQube + } + var password string if password, err = o.getDevOpsPassword(); password == "" { if err == nil { @@ -62,35 +78,38 @@ func (o *migrateOption) runE(cmd *cobra.Command, args []string) (err error) { err = fmt.Errorf("the password of Jenkins is empty, it might caused by: %v", err) } } else if err == nil { - err = o.updateKubeSphereConfig("devops-config", o.namespace, map[string]interface{}{ + patchData["devops"] = map[string]interface{}{ "password": password, - }) + } } - return + + return o.updateKubeSphereConfig("devops-config", o.namespace, patchData) } -func (o *migrateOption) updateKubeSphereConfig(name, namespace string, ksdataMap map[string]interface{}) (err error) { - var rawConfigMap *unstructured.Unstructured - if rawConfigMap, err = o.client.Resource(types.GetConfigMapSchema()).Namespace(namespace). - Get(context.TODO(), name, metav1.GetOptions{}); err == nil { - data := rawConfigMap.Object["data"] - dataMap := data.(map[string]interface{}) +func (o *migrateOption) updateKubeSphereConfig(name, namespace string, ksdataMap map[string]interface{}) error { + kubeSphereConfig, rawConfigMap, err := o.getKubeSphereConfig(name, namespace) + if err != nil { + return fmt.Errorf("cannot found ConfigMap %s/%s, %v", namespace, name, err) + } - result := updateAuthWithObj(dataMap["kubesphere.yaml"].(string), ksdataMap) - if strings.TrimSpace(result) == "" { - err = fmt.Errorf("error happend when parse kubesphere-config") - return - } + patchedKubeSphereConfig, err := patchKubeSphereConfig(kubeSphereConfig, ksdataMap) + if err != nil { + return err + } + kubeSphereConfigBytes, err := yaml.Marshal(patchedKubeSphereConfig) + if err != nil { + return fmt.Errorf("cannot marshal KubeSphere configuration, %v", err) + } - rawConfigMap.Object["data"] = map[string]interface{}{ - "kubesphere.yaml": result, - } - _, err = o.client.Resource(types.GetConfigMapSchema()).Namespace(namespace).Update(context.TODO(), - rawConfigMap, metav1.UpdateOptions{}) - } else { - err = fmt.Errorf("cannot found configmap kubesphere-config, %v", err) + rawConfigMap.Object["data"] = map[string]interface{}{ + "kubesphere.yaml": string(kubeSphereConfigBytes), } - return + if _, err = o.client.Resource(types.GetConfigMapSchema()). + Namespace(namespace). + Update(context.TODO(), rawConfigMap, metav1.UpdateOptions{}); err != nil { + return err + } + return nil } func (o *migrateOption) getDevOpsPassword() (password string, err error) { @@ -117,23 +136,45 @@ func (o *migrateOption) getDevOpsPassword() (password string, err error) { return } -func updateAuthWithObj(yamlf string, dataMap map[string]interface{}) string { - mapData := make(map[string]interface{}) - if err := yaml.Unmarshal([]byte(yamlf), mapData); err == nil { - var obj interface{} - var ok bool - var mapObj map[string]interface{} - if obj, ok = mapData["devops"]; ok { - mapObj = obj.(map[string]interface{}) - } else { - mapObj = make(map[string]interface{}) - mapData["devops"] = mapObj - } +func (o *migrateOption) getKubeSphereConfig(configMapName, namespace string) (map[string]interface{}, *unstructured.Unstructured, error) { + kubeSphereConfigCM, err := o.client.Resource((types.GetConfigMapSchema())). + Namespace(namespace). + Get(context.Background(), configMapName, metav1.GetOptions{}) + if err != nil { + return nil, nil, fmt.Errorf("cannot found ConfigMap %s/%s, %v", namespace, configMapName, err) + } + kubeSphereConfigYAMLString, found, err := unstructured.NestedString(kubeSphereConfigCM.UnstructuredContent(), "data", "kubesphere.yaml") + if err != nil { + return nil, nil, err + } + if !found { + return nil, nil, fmt.Errorf("cannot found 'kubesphere.yaml' configuration in ConfigMap %s/%s", namespace, configMapName) + } + kubeSphereConfig := make(map[string]interface{}) + if err := yaml.Unmarshal([]byte(kubeSphereConfigYAMLString), kubeSphereConfig); err != nil { + return nil, nil, err + } + return kubeSphereConfig, kubeSphereConfigCM, nil +} - for key, val := range dataMap { - mapObj[key] = val - } +// patchKubeSphereConfig patches patch map into KubeSphereConfig map. +// Refer to https://github.com/evanphx/json-patch#create-and-apply-a-merge-patch. +func patchKubeSphereConfig(kubeSphereConfig map[string]interface{}, patch map[string]interface{}) (map[string]interface{}, error) { + kubeSphereConfigBytes, err := json.Marshal(kubeSphereConfig) + if err != nil { + return nil, err + } + patchBytes, err := json.Marshal(patch) + if err != nil { + return nil, err + } + mergedBytes, err := jsonpatch.MergePatch(kubeSphereConfigBytes, patchBytes) + if err != nil { + return nil, err + } + mergedMap := make(map[string]interface{}) + if err := json.Unmarshal(mergedBytes, &mergedMap); err != nil { + return nil, err } - resultData, _ := yaml.Marshal(mapData) - return string(resultData) + return mergedMap, nil } diff --git a/kubectl-plugin/config/migrate_test.go b/kubectl-plugin/config/migrate_test.go new file mode 100644 index 0000000..571d465 --- /dev/null +++ b/kubectl-plugin/config/migrate_test.go @@ -0,0 +1,173 @@ +package config + +import ( + "reflect" + "testing" +) + +func Test_patchKubeSphereConfig(t *testing.T) { + type args struct { + kubeSphereConfig map[string]interface{} + patch map[string]interface{} + } + tests := []struct { + name string + args args + want map[string]interface{} + wantErr bool + }{{ + name: "Patch non-exist fields recursively", + args: args{ + kubeSphereConfig: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + }, + }, + patch: map[string]interface{}{ + "devops": map[string]interface{}{ + "password": "fake password", + }, + }, + }, + want: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + "password": "fake password", + }, + }, + }, { + name: "Patch non-exist field", + args: args{ + kubeSphereConfig: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + }, + }, + patch: map[string]interface{}{ + "sonarQube": map[string]interface{}{ + "token": "fake token", + }, + }, + }, + want: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + }, + "sonarQube": map[string]interface{}{ + "token": "fake token", + }, + }, + }, { + name: "Patch existing field", + args: args{ + kubeSphereConfig: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + }, + "sonarQube": map[string]interface{}{ + "token": "fake token", + }, + }, + patch: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": false, + }, + }, + }, + want: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": false, + }, + "sonarQube": map[string]interface{}{ + "token": "fake token", + }, + }, + }, { + name: "Report an error if patch is nil", + args: args{ + kubeSphereConfig: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + }, + }, + patch: nil, + }, + wantErr: true, + }, { + name: "Report an error if KubeSphereConfig is nil", + args: args{ + kubeSphereConfig: nil, + patch: map[string]interface{}{}, + }, + wantErr: true, + }, { + name: "Patch with different type", + args: args{ + kubeSphereConfig: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + }, + }, + patch: map[string]interface{}{ + "devops": "awesome", + }, + }, + want: map[string]interface{}{ + "devops": "awesome", + }, + }, { + name: "Patch with map[string]string type", + args: args{ + kubeSphereConfig: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + "password": "fake password", + }, + }, + patch: map[string]interface{}{ + "devops": map[string]string{ + "password": "patch password", + }, + }, + }, + want: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + "password": "patch password", + }, + }, + }, { + name: "Patch without map[string]interface{}", + args: args{ + kubeSphereConfig: map[string]interface{}{ + "devops": map[string]bool{ + "enabled": true, + }, + }, + patch: map[string]interface{}{ + "devops": map[string]bool{ + "disabled": false, + }, + }, + }, + want: map[string]interface{}{ + "devops": map[string]interface{}{ + "enabled": true, + "disabled": false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := patchKubeSphereConfig(tt.args.kubeSphereConfig, tt.args.patch) + if (err != nil) != tt.wantErr { + t.Errorf("patchKubeSphereConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("patchKubeSphereConfig() = %+v, want = %+v", got, tt.want) + } + }) + } +}