Repository: radondb/radondb-mysql-kubernetes
Pull request #285 (2021-11-03 10:30:06)

*: Support display the raft status of the node in nodes.conditions. #284
### What type of PR is this?

/enhancement

### Which issue(s) this PR fixes?

Fixes ##284

### What this PR does?

Summary:

- Adding the basic framework code of the xenon http request.
- Support `raft/status` and `raft/trytoleader` 
- Support display the raft status of the node in nodes.conditions.

### Special notes for your reviewer?



Patch:
diff --git a/api/v1alpha1/mysqlcluster_types.go b/api/v1alpha1/mysqlcluster_types.go
index eac3fab..db64f0b 100644
--- a/api/v1alpha1/mysqlcluster_types.go
+++ b/api/v1alpha1/mysqlcluster_types.go
@@ -294,10 +294,21 @@ type NodeStatus struct {
 	Name string `json:"name"`
 	// Full text reason for current status of the node.
 	Message string `json:"message,omitempty"`
+	// RaftStatus is the raft status of the node.
+	RaftStatus RaftStatus `json:"raftStatus,omitempty"`
 	// Conditions contains the list of the node conditions fulfilled.
 	Conditions []NodeCondition `json:"conditions,omitempty"`
 }
 
+type RaftStatus struct {
+	// Role is one of (LEADER/CANDIDATE/FOLLOWER/IDLE/INVALID)
+	Role string `json:"role,omitempty"`
+	// Leader is the name of the Leader of the current node.
+	Leader string `json:"leader,omitempty"`
+	// Nodes is a list of nodes that can be identified by the current node.
+	Nodes []string `json:"nodes,omitempty"`
+}
+
 // NodeCondition defines type for representing node conditions.
 type NodeCondition struct {
 	// Type of the node condition.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 2fd8c7b..22eca00 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -426,6 +426,7 @@ func (in *NodeCondition) DeepCopy() *NodeCondition {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NodeStatus) DeepCopyInto(out *NodeStatus) {
 	*out = *in
+	in.RaftStatus.DeepCopyInto(&out.RaftStatus)
 	if in.Conditions != nil {
 		in, out := &in.Conditions, &out.Conditions
 		*out = make([]NodeCondition, len(*in))
@@ -512,6 +513,26 @@ func (in *PodPolicy) DeepCopy() *PodPolicy {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RaftStatus) DeepCopyInto(out *RaftStatus) {
+	*out = *in
+	if in.Nodes != nil {
+		in, out := &in.Nodes, &out.Nodes
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RaftStatus.
+func (in *RaftStatus) DeepCopy() *RaftStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(RaftStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SecretSelector) DeepCopyInto(out *SecretSelector) {
 	*out = *in
diff --git a/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml b/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml
index bfa0968..e5e6287 100644
--- a/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml
+++ b/charts/mysql-operator/crds/mysql.radondb.com_mysqlclusters.yaml
@@ -1379,6 +1379,23 @@ spec:
                     name:
                       description: Name of the node.
                       type: string
+                    raftStatus:
+                      description: RaftStatus is the raft status of the node.
+                      properties:
+                        leader:
+                          description: Leader is the name of the Leader of the current
+                            node.
+                          type: string
+                        nodes:
+                          description: Nodes is a list of nodes that can be identified
+                            by the current node.
+                          items:
+                            type: string
+                          type: array
+                        role:
+                          description: Role is one of (LEADER/CANDIDATE/FOLLOWER/IDLE/INVALID)
+                          type: string
+                      type: object
                   required:
                   - name
                   type: object
diff --git a/cmd/manager/main.go b/cmd/manager/main.go
index eceafe6..3fb21ca 100644
--- a/cmd/manager/main.go
+++ b/cmd/manager/main.go
@@ -84,6 +84,7 @@ func main() {
 		Scheme:           mgr.GetScheme(),
 		Recorder:         mgr.GetEventRecorderFor("controller.mysqlcluster"),
 		SQLRunnerFactory: internal.NewSQLRunner,
+		XenonExecutor:    internal.NewXenonExecutor(),
 	}).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "MysqlCluster")
 		os.Exit(1)
@@ -93,6 +94,7 @@ func main() {
 		Scheme:           mgr.GetScheme(),
 		Recorder:         mgr.GetEventRecorderFor("controller.status"),
 		SQLRunnerFactory: internal.NewSQLRunner,
+		XenonExecutor:    internal.NewXenonExecutor(),
 	}).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "Status")
 		os.Exit(1)
diff --git a/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml b/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml
index bfa0968..e5e6287 100644
--- a/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml
+++ b/config/crd/bases/mysql.radondb.com_mysqlclusters.yaml
@@ -1379,6 +1379,23 @@ spec:
                     name:
                       description: Name of the node.
                       type: string
+                    raftStatus:
+                      description: RaftStatus is the raft status of the node.
+                      properties:
+                        leader:
+                          description: Leader is the name of the Leader of the current
+                            node.
+                          type: string
+                        nodes:
+                          description: Nodes is a list of nodes that can be identified
+                            by the current node.
+                          items:
+                            type: string
+                          type: array
+                        role:
+                          description: Role is one of (LEADER/CANDIDATE/FOLLOWER/IDLE/INVALID)
+                          type: string
+                      type: object
                   required:
                   - name
                   type: object
diff --git a/controllers/mysqlcluster_controller.go b/controllers/mysqlcluster_controller.go
index 31a850f..7d7c590 100644
--- a/controllers/mysqlcluster_controller.go
+++ b/controllers/mysqlcluster_controller.go
@@ -46,6 +46,8 @@ type MysqlClusterReconciler struct {
 
 	// Mysql query runner.
 	internal.SQLRunnerFactory
+	// XenonExecutor is used to execute Xenon HTTP instructions.
+	internal.XenonExecutor
 }
 
 // +kubebuilder:rbac:groups=mysql.radondb.com,resources=mysqlclusters,verbs=get;list;watch;create;update;patch;delete
@@ -110,6 +112,8 @@ func (r *MysqlClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request
 	cmRev := configMapSyncer.Object().(*corev1.ConfigMap).ResourceVersion
 	sctRev := secretSyncer.Object().(*corev1.Secret).ResourceVersion
 
+	r.XenonExecutor.SetRootPassword(instance.Spec.MysqlOpts.RootPassword)
+
 	// run the syncers for services, pdb and statefulset
 	syncers := []syncer.Interface{
 		clustersyncer.NewRoleSyncer(r.Client, instance),
@@ -118,7 +122,7 @@ func (r *MysqlClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request
 		clustersyncer.NewHeadlessSVCSyncer(r.Client, instance),
 		clustersyncer.NewLeaderSVCSyncer(r.Client, instance),
 		clustersyncer.NewFollowerSVCSyncer(r.Client, instance),
-		clustersyncer.NewStatefulSetSyncer(r.Client, instance, cmRev, sctRev, r.SQLRunnerFactory),
+		clustersyncer.NewStatefulSetSyncer(r.Client, instance, cmRev, sctRev, r.SQLRunnerFactory, r.XenonExecutor),
 		clustersyncer.NewPDBSyncer(r.Client, instance),
 	}
 
diff --git a/controllers/mysqluser_controller.go b/controllers/mysqluser_controller.go
index ba23c28..672db86 100644
--- a/controllers/mysqluser_controller.go
+++ b/controllers/mysqluser_controller.go
@@ -189,7 +189,7 @@ func (r *MysqlUserReconciler) reconcileUserInDB(ctx context.Context, mysqlUser *
 	}
 
 	// Remove allowed hosts for user.
-	toRemove := stringDiffIn(mysqlUser.Status.AllowedHosts, mysqlUser.Spec.Hosts)
+	toRemove := utils.StringDiffIn(mysqlUser.Status.AllowedHosts, mysqlUser.Spec.Hosts)
 	for _, host := range toRemove {
 		if err := internal.DropUser(sqlRunner, mysqlUser.Spec.User, host); err != nil {
 			return err
@@ -199,27 +199,6 @@ func (r *MysqlUserReconciler) reconcileUserInDB(ctx context.Context, mysqlUser *
 	return nil
 }
 
-func stringDiffIn(actual, desired []string) []string {
-	diff := []string{}
-	for _, aStr := range actual {
-		// If is not in the desired list remove it.
-		if _, exists := stringIn(aStr, desired); !exists {
-			diff = append(diff, aStr)
-		}
-	}
-
-	return diff
-}
-
-func stringIn(str string, strs []string) (int, bool) {
-	for i, s := range strs {
-		if s == str {
-			return i, true
-		}
-	}
-	return 0, false
-}
-
 func (r *MysqlUserReconciler) dropUserFromDB(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error {
 	sqlRunner, closeConn, err := r.SQLRunnerFactory(internal.NewConfigFromClusterKey(
 		r.Client, mysqlUser.GetClusterKey(), utils.RootUser, utils.LeaderHost))
diff --git a/controllers/status_controller.go b/controllers/status_controller.go
index 8212126..ca6b707 100644
--- a/controllers/status_controller.go
+++ b/controllers/status_controller.go
@@ -54,6 +54,8 @@ type StatusReconciler struct {
 
 	// Mysql query runner.
 	internal.SQLRunnerFactory
+	// XenonExecutor is used to execute Xenon HTTP instructions.
+	internal.XenonExecutor
 }
 
 // Reconcile is part of the main kubernetes reconciliation loop which aims to
@@ -92,7 +94,9 @@ func (r *StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		}
 	}()
 
-	statusSyncer := clustersyncer.NewStatusSyncer(instance, r.Client, r.SQLRunnerFactory)
+	r.XenonExecutor.SetRootPassword(instance.Spec.MysqlOpts.RootPassword)
+
+	statusSyncer := clustersyncer.NewStatusSyncer(instance, r.Client, r.SQLRunnerFactory, r.XenonExecutor)
 	if err := syncer.Sync(ctx, statusSyncer, r.Recorder); err != nil {
 		return ctrl.Result{}, err
 	}
diff --git a/internal/http_executor.go b/internal/http_executor.go
new file mode 100644
index 0000000..da03f72
--- /dev/null
+++ b/internal/http_executor.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2021 RadonDB.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package internal
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+)
+
+type Request struct {
+	Req *http.Request
+}
+
+type Response struct {
+	Body io.ReadCloser
+}
+
+type HttpClient interface {
+	Do(req *http.Request) (*http.Response, error)
+}
+
+type httpClient struct {
+	client *http.Client
+}
+
+func NewHttpClient(client *http.Client) HttpClient {
+	return &httpClient{
+		client: client,
+	}
+}
+
+func (cli *httpClient) Do(req *http.Request) (*http.Response, error) {
+	return cli.client.Do(req)
+}
+
+type Executor interface {
+	Execute(req *Request) (*Response, error)
+}
+
+type httpExecutor struct {
+	Client HttpClient
+}
+
+func NewHttpExecutor(httpClient HttpClient) Executor {
+	return &httpExecutor{
+		Client: httpClient,
+	}
+}
+
+func (executor *httpExecutor) Execute(req *Request) (*Response, error) {
+	resp, err := executor.Client.Do(req.Req)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("status code is %d", resp.StatusCode)
+	}
+	return &Response{
+		Body: resp.Body,
+	}, nil
+}
diff --git a/internal/pod_executor.go b/internal/pod_executor.go
deleted file mode 100644
index 8b4bf9f..0000000
--- a/internal/pod_executor.go
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
-Copyright 2021 RadonDB.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package internal
-
-import (
-	"io"
-
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/client-go/kubernetes/scheme"
-	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
-	"k8s.io/client-go/rest"
-	"k8s.io/client-go/tools/clientcmd"
-	"k8s.io/client-go/tools/remotecommand"
-)
-
-// PodExecutor is a executor for pod.
-type PodExecutor struct {
-	client corev1client.CoreV1Interface
-	config *rest.Config
-}
-
-// NewPodExecutor return a pointer to PodExecutor.
-func NewPodExecutor() (*PodExecutor, error) {
-	// Instantiate loader for kubeconfig file.
-	kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
-		clientcmd.NewDefaultClientConfigLoadingRules(),
-		&clientcmd.ConfigOverrides{},
-	)
-
-	// Get a rest.Config from the kubeconfig file.  This will be passed into all
-	// the client objects we create.
-	config, err := kubeconfig.ClientConfig()
-	if err != nil {
-		return nil, err
-	}
-
-	// Create a Kubernetes core/v1 client.
-	client, err := corev1client.NewForConfig(config)
-	if err != nil {
-		return nil, err
-	}
-
-	return &PodExecutor{
-		client: client,
-		config: config,
-	}, nil
-}
-
-// Exec used to run the commands in a container.
-func (p *PodExecutor) Exec(namespace, podName, containerName string, stdout, stderr io.Writer, command ...string) error {
-	request := p.client.RESTClient().
-		Post().
-		Resource("pods").
-		Namespace(namespace).
-		Name(podName).
-		SubResource("exec").
-		VersionedParams(&corev1.PodExecOptions{
-			Container: containerName,
-			Command:   command,
-			Stdout:    true,
-			Stderr:    true,
-			Stdin:     false,
-		}, scheme.ParameterCodec)
-
-	exec, err := remotecommand.NewSPDYExecutor(p.config, "POST", request.URL())
-	if err != nil {
-		return err
-	}
-
-	err = exec.Stream(remotecommand.StreamOptions{
-		Stdout: stdout,
-		Stderr: stderr,
-		Stdin:  nil,
-		Tty:    false,
-	})
-
-	return err
-}
diff --git a/internal/xenon_executor.go b/internal/xenon_executor.go
new file mode 100644
index 0000000..3717a3a
--- /dev/null
+++ b/internal/xenon_executor.go
@@ -0,0 +1,88 @@
+/*
+Copyright 2021 RadonDB.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package internal
+
+import (
+	"fmt"
+	"net/http"
+
+	apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1"
+	"github.com/radondb/radondb-mysql-kubernetes/utils"
+)
+
+type xenonExecutor struct {
+	httpExecutor
+	rootPassword string
+}
+
+type XenonExecutor interface {
+	GetRootPassword() string
+	SetRootPassword(rootPassword string)
+	RaftStatus(host string) (*apiv1alpha1.RaftStatus, error)
+	RaftTryToLeader(host string) error
+}
+
+func NewXenonExecutor() XenonExecutor {
+	return &xenonExecutor{httpExecutor: httpExecutor{Client: NewHttpClient(&http.Client{})}}
+}
+
+func (executor *xenonExecutor) GetRootPassword() string {
+	return executor.rootPassword
+}
+
+func (executor *xenonExecutor) SetRootPassword(rootPassword string) {
+	executor.rootPassword = rootPassword
+}
+
+// RaftStatus gets the raft status of incoming host through http.
+func (executor *xenonExecutor) RaftStatus(host string) (*apiv1alpha1.RaftStatus, error) {
+	req, err := NewXenonHttpRequest(NewRequestConfig(host, executor.rootPassword, utils.RaftStatus, nil))
+	if err != nil {
+		return nil, err
+	}
+
+	response, err := executor.httpExecutor.Execute(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get raft status, err: %s", err)
+	}
+
+	var out map[string]interface{}
+	if err = utils.UnmarshalJSON(response.Body, &out); err != nil {
+		return nil, err
+	}
+
+	nodesJson := out["nodes"].([]interface{})
+	nodes := []string{}
+	for _, node := range nodesJson {
+		nodes = append(nodes, node.(string))
+	}
+	return &apiv1alpha1.RaftStatus{Role: out["state"].(string), Leader: out["leader"].(string), Nodes: nodes}, nil
+}
+
+// RaftTryToLeader try setting up incoming host to the leader node.
+func (executor *xenonExecutor) RaftTryToLeader(host string) error {
+	req, err := NewXenonHttpRequest(NewRequestConfig(host, executor.rootPassword, utils.RaftTryToLeader, nil))
+	if err != nil {
+		return err
+	}
+
+	_, err = executor.httpExecutor.Execute(req)
+	if err != nil {
+		return fmt.Errorf("failed to execute raft/trytoleader at host[%s], err: %s", req.Req.URL, err)
+	}
+	return nil
+}
diff --git a/internal/xenon_request.go b/internal/xenon_request.go
new file mode 100644
index 0000000..edea6fc
--- /dev/null
+++ b/internal/xenon_request.go
@@ -0,0 +1,92 @@
+/*
+Copyright 2021 RadonDB.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package internal
+
+import (
+	"bytes"
+	"encoding/base64"
+	"fmt"
+	"net/http"
+
+	"github.com/radondb/radondb-mysql-kubernetes/utils"
+)
+
+// RequestConfig is the configuration required to get xenon http requests.
+type RequestConfig struct {
+	rootPassword string
+	host         string
+	xenonHttpUrl utils.XenonHttpUrl
+	data         interface{}
+}
+
+func NewRequestConfig(host, rootPasswd string, xenonHttpUrl utils.XenonHttpUrl, data interface{}) RequestConfig {
+	return RequestConfig{
+		rootPassword: rootPasswd,
+		host:         host,
+		xenonHttpUrl: xenonHttpUrl,
+		data:         data,
+	}
+}
+
+// NewXenonHttpRequest returns the http request of the corresponding type according to the incoming configuration.
+func NewXenonHttpRequest(cfg RequestConfig) (*Request, error) {
+	requestType, ok := utils.XenonHttpUrls[cfg.xenonHttpUrl]
+	if !ok {
+		return nil, fmt.Errorf("xenon http url[%s] does not exist or does not support", cfg.xenonHttpUrl)
+	}
+
+	switch requestType {
+	case http.MethodGet:
+		return newHttpGetRequest(cfg)
+	case http.MethodPost:
+		return newHttpPostRequest(cfg)
+	default:
+		return nil, fmt.Errorf("request type[%s] does not support", requestType)
+	}
+}
+
+func newHttpGetRequest(cfg RequestConfig) (*Request, error) {
+	req, err := http.NewRequest(http.MethodGet, newXenonRequestUrl(cfg.host, cfg.xenonHttpUrl), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	encoded := base64.StdEncoding.EncodeToString(append([]byte("root:"), cfg.rootPassword...))
+	req.Header.Set("Authorization", "Basic "+encoded)
+	return &Request{Req: req}, nil
+}
+
+func newHttpPostRequest(cfg RequestConfig) (*Request, error) {
+	reqBody := ""
+	if cfg.data != nil {
+		reqBody = cfg.data.(string)
+	}
+
+	req, err := http.NewRequest(http.MethodPost, newXenonRequestUrl(cfg.host, cfg.xenonHttpUrl), bytes.NewBuffer([]byte(reqBody)))
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	encoded := base64.StdEncoding.EncodeToString(append([]byte("root:"), cfg.rootPassword...))
+	req.Header.Set("Authorization", "Basic "+encoded)
+	return &Request{Req: req}, nil
+}
+
+func newXenonRequestUrl(host string, xenonHttpUrl utils.XenonHttpUrl) string {
+	return fmt.Sprintf("http://%s:%d%s", host, utils.XenonPeerPort, xenonHttpUrl)
+}
diff --git a/mysqlcluster/mysqlcluster.go b/mysqlcluster/mysqlcluster.go
index e0e8421..d0d8c3b 100644
--- a/mysqlcluster/mysqlcluster.go
+++ b/mysqlcluster/mysqlcluster.go
@@ -17,7 +17,6 @@ limitations under the License.
 package mysqlcluster
 
 import (
-	"errors"
 	"fmt"
 	"math"
 	"strconv"
@@ -32,13 +31,12 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-	logf "sigs.k8s.io/controller-runtime/pkg/log"
 
 	apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1"
 	"github.com/radondb/radondb-mysql-kubernetes/utils"
 )
 
-// Nolint: megacheck, deadcode, varcheck.
+// nolint: megacheck, deadcode, varcheck
 const (
 	_         = iota // ignore first value by assigning to blank identifier
 	kb uint64 = 1 << (10 * iota)
@@ -46,8 +44,6 @@ const (
 	gb
 )
 
-var log = logf.Log.WithName("mysqlcluster")
-
 // MysqlCluster is the wrapper for apiv1alpha1.MysqlCluster type.
 type MysqlCluster struct {
 	*apiv1alpha1.MysqlCluster
@@ -60,7 +56,7 @@ func New(mc *apiv1alpha1.MysqlCluster) *MysqlCluster {
 	}
 }
 
-// Unwrap returns the api mysqlcluster object.
+// Unwrap returns the api mysqlcluster object
 func (c *MysqlCluster) Unwrap() *apiv1alpha1.MysqlCluster {
 	return c.MysqlCluster
 }
@@ -79,7 +75,7 @@ func (c *MysqlCluster) Validate() error {
 	return nil
 }
 
-// GetLabels returns mysqlcluster labels.
+// GetLabels returns mysqlcluster labels
 func (c *MysqlCluster) GetLabels() labels.Set {
 	instance := c.Name
 	if inst, ok := c.Annotations["app.kubernetes.io/instance"]; ok {
@@ -107,7 +103,7 @@ func (c *MysqlCluster) GetLabels() labels.Set {
 	return labels
 }
 
-// GetSelectorLabels returns the labels that will be used as selector.
+// GetSelectorLabels returns the labels that will be used as selector
 func (c *MysqlCluster) GetSelectorLabels() labels.Set {
 	return labels.Set{
 		"mysql.radondb.com/cluster":    c.Name,
@@ -118,17 +114,12 @@ func (c *MysqlCluster) GetSelectorLabels() labels.Set {
 
 // GetMySQLVersion returns the MySQL server version.
 func (c *MysqlCluster) GetMySQLVersion() string {
-	var version string
-	// Lookup for an alias, this will solve MySQL tags: 5.7 --> 5.7.x
-	if v, ok := utils.MySQLTagsToSemVer[c.Spec.MysqlVersion]; ok {
+	version := c.Spec.MysqlVersion
+	// lookup for an alias, usually this will solve 5.7 to 5.7.x
+	if v, ok := utils.MySQLTagsToSemVer[version]; ok {
 		version = v
-	} else {
-		errmsg := "Invalid mysql version option:" + c.Spec.MysqlVersion
-		log.Error(errors.New(errmsg), "currently we do not support mysql 5.6 or earlier version, default mysql version option should be 5.7 or 8.0")
-		return utils.InvalidMySQLVersion
 	}
 
-	// Check if has specified image.
 	if _, ok := utils.MysqlImageVersions[version]; !ok {
 		version = utils.MySQLDefaultVersion
 	}
diff --git a/mysqlcluster/syncer/metrics_service.go b/mysqlcluster/syncer/metrics_service.go
index 1dea7cd..66540f6 100644
--- a/mysqlcluster/syncer/metrics_service.go
+++ b/mysqlcluster/syncer/metrics_service.go
@@ -56,7 +56,6 @@ func NewMetricsSVCSyncer(cli client.Client, c *mysqlcluster.MysqlCluster) syncer
 		service.Spec.Ports[0].Name = utils.MetricsPortName
 		service.Spec.Ports[0].Port = utils.MetricsPort
 		service.Spec.Ports[0].TargetPort = intstr.FromInt(utils.MetricsPort)
-
 		return nil
 	})
 }
diff --git a/mysqlcluster/syncer/statefulset.go b/mysqlcluster/syncer/statefulset.go
index aec9526..31c8d38 100644
--- a/mysqlcluster/syncer/statefulset.go
+++ b/mysqlcluster/syncer/statefulset.go
@@ -18,11 +18,8 @@ package syncer
 
 import (
 	"context"
-	"encoding/base64"
 	"errors"
 	"fmt"
-	"io"
-	"net/http"
 	"time"
 
 	"github.com/iancoleman/strcase"
@@ -66,10 +63,12 @@ type StatefulSetSyncer struct {
 
 	// Mysql query runner.
 	internal.SQLRunnerFactory
+	// XenonExecutor is used to execute Xenon HTTP instructions.
+	internal.XenonExecutor
 }
 
 // NewStatefulSetSyncer returns a pointer to StatefulSetSyncer.
-func NewStatefulSetSyncer(cli client.Client, c *mysqlcluster.MysqlCluster, cmRev, sctRev string, sqlRunnerFactory internal.SQLRunnerFactory) *StatefulSetSyncer {
+func NewStatefulSetSyncer(cli client.Client, c *mysqlcluster.MysqlCluster, cmRev, sctRev string, sqlRunnerFactory internal.SQLRunnerFactory, xenonExecutor internal.XenonExecutor) *StatefulSetSyncer {
 	return &StatefulSetSyncer{
 		MysqlCluster: c,
 		cli:          cli,
@@ -86,6 +85,7 @@ func NewStatefulSetSyncer(cli client.Client, c *mysqlcluster.MysqlCluster, cmRev
 		cmRev:            cmRev,
 		sctRev:           sctRev,
 		SQLRunnerFactory: sqlRunnerFactory,
+		XenonExecutor:    xenonExecutor,
 	}
 }
 
@@ -422,19 +422,15 @@ func (s *StatefulSetSyncer) preUpdate(ctx context.Context, leader, follower stri
 
 	followerHost := fmt.Sprintf("%s.%s.%s", follower, svcName, nameSpace)
 	if err = retry(time.Second*5, time.Second*60, func() (bool, error) {
-		// Check whether is leader.
-		status, err := checkRole(followerHost, rootPasswd)
-		if err != nil {
-			log.Error(err, "failed to check role", "pod", follower)
-			return false, nil
-		}
-		if status == corev1.ConditionTrue {
-			return true, nil
+		for _, node := range s.Status.Nodes {
+			host := node.Name
+			if host == followerHost && !s.isLeader(host) {
+				if err := s.XenonExecutor.RaftTryToLeader(followerHost); err != nil {
+					return false, err
+				}
+			}
 		}
-
-		// If not leader, try to leader.
-		xenonHttpRequest(followerHost, "POST", "/v1/raft/trytoleader", rootPasswd, nil)
-		return false, nil
+		return true, nil
 	}); err != nil {
 		return err
 	}
@@ -442,6 +438,17 @@ func (s *StatefulSetSyncer) preUpdate(ctx context.Context, leader, follower stri
 	return nil
 }
 
+func (s *StatefulSetSyncer) isLeader(host string) bool {
+	raftStatus, err := s.XenonExecutor.RaftStatus(host)
+	if err != nil {
+		return false
+	}
+	if raftStatus.Role == string(utils.Leader) {
+		return true
+	}
+	return false
+}
+
 // mutate set the statefulset.
 func (s *StatefulSetSyncer) mutate() error {
 	s.sfs.Spec.ServiceName = s.GetNameForResource(utils.StatefulSet)
@@ -656,27 +663,6 @@ func basicEventReason(objKindName string, err error) string {
 	return fmt.Sprintf("%sSyncSuccessfull", strcase.ToCamel(objKindName))
 }
 
-func xenonHttpRequest(host, method, url string, rootPasswd []byte, body io.Reader) (io.ReadCloser, error) {
-	req, err := http.NewRequest(method, fmt.Sprintf("http://%s:%d%s", host, utils.XenonPeerPort, url), body)
-	if err != nil {
-		return nil, err
-	}
-	encoded := base64.StdEncoding.EncodeToString(append([]byte("root:"), rootPasswd...))
-	req.Header.Set("Authorization", "Basic "+encoded)
-
-	client := &http.Client{}
-	resp, err := client.Do(req)
-	if err != nil {
-		return nil, err
-	}
-
-	if resp.StatusCode != 200 {
-		return nil, fmt.Errorf("get raft status failed, status code is %d", resp.StatusCode)
-	}
-
-	return resp.Body, nil
-}
-
 // check the backup is exist and running
 func (s *StatefulSetSyncer) backupIsRunning(ctx context.Context) (bool, error) {
 	backuplist := apiv1alpha1.BackupList{}
diff --git a/mysqlcluster/syncer/status.go b/mysqlcluster/syncer/status.go
index c6cf8ed..9dc2a27 100644
--- a/mysqlcluster/syncer/status.go
+++ b/mysqlcluster/syncer/status.go
@@ -18,17 +18,13 @@ package syncer
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
-	"io"
-	"io/ioutil"
 	"time"
 
 	"github.com/presslabs/controller-util/syncer"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1"
@@ -51,14 +47,17 @@ type StatusSyncer struct {
 
 	// Mysql query runner.
 	internal.SQLRunnerFactory
+	// XenonExecutor is used to execute Xenon HTTP instructions.
+	internal.XenonExecutor
 }
 
 // NewStatusSyncer returns a pointer to StatusSyncer.
-func NewStatusSyncer(c *mysqlcluster.MysqlCluster, cli client.Client, sqlRunnerFactory internal.SQLRunnerFactory) *StatusSyncer {
+func NewStatusSyncer(c *mysqlcluster.MysqlCluster, cli client.Client, sqlRunnerFactory internal.SQLRunnerFactory, xenonExecutor internal.XenonExecutor) *StatusSyncer {
 	return &StatusSyncer{
 		MysqlCluster:     c,
 		cli:              cli,
 		SQLRunnerFactory: sqlRunnerFactory,
+		XenonExecutor:    xenonExecutor,
 	}
 }
 
@@ -177,41 +176,16 @@ func (s *StatusSyncer) updateClusterStatus() apiv1alpha1.ClusterCondition {
 
 // updateNodeStatus update the node status.
 func (s *StatusSyncer) updateNodeStatus(ctx context.Context, cli client.Client, pods []corev1.Pod) error {
-	sctName := s.GetNameForResource(utils.Secret)
-	svcName := s.GetNameForResource(utils.HeadlessSVC)
-	nameSpace := s.Namespace
-
-	secret := &corev1.Secret{}
-	if err := cli.Get(context.TODO(),
-		types.NamespacedName{
-			Namespace: nameSpace,
-			Name:      sctName,
-		},
-		secret,
-	); err != nil {
-		log.V(1).Info("secret not found", "name", sctName)
-		return nil
-	}
-
-	rootPasswd, ok := secret.Data["root-password"]
-	if !ok {
-		return fmt.Errorf("failed to get the root password: %s", rootPasswd)
-	}
-
 	for _, pod := range pods {
 		podName := pod.Name
-		host := fmt.Sprintf("%s.%s.%s", podName, svcName, nameSpace)
+		host := fmt.Sprintf("%s.%s.%s", podName, s.GetNameForResource(utils.HeadlessSVC), s.Namespace)
 		index := s.getNodeStatusIndex(host)
 		node := &s.Status.Nodes[index]
 		node.Message = ""
 
-		isLeader, err := checkRole(host, rootPasswd)
-		if err != nil {
-			log.Error(err, "failed to check the node role", "node", node.Name)
-			node.Message = err.Error()
+		if err := s.updateNodeRaftStatus(node); err != nil {
+			return err
 		}
-		// update apiv1alpha1.NodeConditionLeader.
-		s.updateNodeCondition(node, int(apiv1alpha1.IndexLeader), isLeader)
 
 		isLagged, isReplicating, isReadOnly := corev1.ConditionUnknown, corev1.ConditionUnknown, corev1.ConditionUnknown
 		sqlRunner, closeConn, err := s.SQLRunnerFactory(internal.NewConfigFromClusterKey(
@@ -234,7 +208,7 @@ func (s *StatusSyncer) updateNodeStatus(ctx context.Context, cli client.Client,
 			}
 
 			if !utils.ExistUpdateFile() &&
-				isLeader == corev1.ConditionTrue &&
+				node.RaftStatus.Role == string(utils.Leader) &&
 				isReadOnly != corev1.ConditionFalse {
 				log.V(1).Info("try to correct the leader writeable", "node", node.Name)
 				sqlRunner.QueryExec(internal.NewQuery("SET GLOBAL read_only=off"))
@@ -254,10 +228,6 @@ func (s *StatusSyncer) updateNodeStatus(ctx context.Context, cli client.Client,
 		}
 	}
 
-	// Delete node status of nodes that have been deleted.
-	if len(s.Status.Nodes) > len(pods) {
-		s.Status.Nodes = s.Status.Nodes[:len(pods)]
-	}
 	return nil
 }
 
@@ -311,27 +281,21 @@ func (s *StatusSyncer) updateNodeCondition(node *apiv1alpha1.NodeStatus, idx int
 	}
 }
 
-// checkRole used to check whether the mysql role is leader.
-func checkRole(host string, rootPasswd []byte) (corev1.ConditionStatus, error) {
-	body, err := xenonHttpRequest(host, "GET", "/v1/raft/status", rootPasswd, nil)
+// updateNodeRaftStatus Update Node RaftStatus.
+func (s *StatusSyncer) updateNodeRaftStatus(node *apiv1alpha1.NodeStatus) error {
+	raftStatus, err := s.XenonExecutor.RaftStatus(node.Name)
 	if err != nil {
-		return corev1.ConditionUnknown, err
-	}
-
-	var out map[string]interface{}
-	if err = unmarshalJSON(body, &out); err != nil {
-		return corev1.ConditionUnknown, err
-	}
-
-	if out["state"] == "LEADER" {
-		return corev1.ConditionTrue, nil
+		return err
 	}
+	node.RaftStatus = *raftStatus
 
-	if out["state"] == "FOLLOWER" {
-		return corev1.ConditionFalse, nil
+	isLeader := corev1.ConditionFalse
+	if node.RaftStatus.Role == string(utils.Leader) {
+		isLeader = corev1.ConditionTrue
 	}
-
-	return corev1.ConditionUnknown, nil
+	// update apiv1alpha1.NodeConditionLeader.
+	s.updateNodeCondition(node, int(apiv1alpha1.IndexLeader), isLeader)
+	return nil
 }
 
 // setPodHealthy set the pod lable healthy.
@@ -357,17 +321,3 @@ func (s *StatusSyncer) setPodHealthy(ctx context.Context, pod *corev1.Pod, node
 	}
 	return nil
 }
-
-func unmarshalJSON(in io.Reader, obj interface{}) error {
-	body, err := ioutil.ReadAll(in)
-	if err != nil {
-		return fmt.Errorf("io read error: %s", err)
-	}
-
-	if err = json.Unmarshal(body, obj); err != nil {
-		log.V(1).Info("error unmarshal data", "body", string(body))
-		return err
-	}
-
-	return nil
-}
diff --git a/sidecar/config.go b/sidecar/config.go
index cf9b701..584bd34 100644
--- a/sidecar/config.go
+++ b/sidecar/config.go
@@ -76,7 +76,7 @@ type Config struct {
 
 	// The parameter in xenon means admit defeat count for hearbeat.
 	AdmitDefeatHearbeatCount int32
-	// The parameter in xenon means election timeout(ms).
+	// The parameter in xenon means election timeout(ms)。
 	ElectionTimeout int32
 
 	// Whether the MySQL data exists.
@@ -120,18 +120,12 @@ type Config struct {
 	XRestoreFrom string
 }
 
-// NewInitConfig returns a pointer to Config.
+// NewInitConfig returns the configuration file needed for initialization.
 func NewInitConfig() *Config {
-	// check mysql version is supported or not and then get parse mysql semver version
-	var mysqlSemVer semver.Version
-	if ver := getEnvValue("MYSQL_VERSION"); ver == utils.InvalidMySQLVersion {
-		panic("invalid mysql version, currently we only support 5.7 or 8.0")
-	} else {
-		mysqlSemVer, err := semver.Parse(ver)
-		if err != nil {
-			log.Info("semver get from MYSQL_VERSION is invalid", "semver: ", mysqlSemVer)
-			panic(err)
-		}
+	mysqlVersion, err := semver.Parse(getEnvValue("MYSQL_VERSION"))
+	if err != nil {
+		log.Info("MYSQL_VERSION is not a semver version")
+		mysqlVersion, _ = semver.Parse(utils.MySQLDefaultVersion)
 	}
 
 	replicaStr := getEnvValue("REPLICAS")
@@ -182,7 +176,7 @@ func NewInitConfig() *Config {
 
 		InitTokuDB: initTokuDB,
 
-		MySQLVersion: mysqlSemVer,
+		MySQLVersion: mysqlVersion,
 
 		AdmitDefeatHearbeatCount: int32(admitDefeatHearbeatCount),
 		ElectionTimeout:          int32(electionTimeout),
@@ -310,16 +304,20 @@ func (cfg *Config) buildXenonConf() []byte {
 
 	version := "mysql80"
 	if cfg.MySQLVersion.Major == 5 {
-		version = "mysql57"
+		if cfg.MySQLVersion.Minor == 6 {
+			version = "mysql56"
+		} else {
+			version = "mysql57"
+		}
 	}
 
-	var srcSysVars, replicaSysVars string
+	var masterSysVars, slaveSysVars string
 	if cfg.InitTokuDB {
-		srcSysVars = "tokudb_fsync_log_period=default;sync_binlog=default;innodb_flush_log_at_trx_commit=default"
-		replicaSysVars = "tokudb_fsync_log_period=1000;sync_binlog=1000;innodb_flush_log_at_trx_commit=1"
+		masterSysVars = "tokudb_fsync_log_period=default;sync_binlog=default;innodb_flush_log_at_trx_commit=default"
+		slaveSysVars = "tokudb_fsync_log_period=1000;sync_binlog=1000;innodb_flush_log_at_trx_commit=1"
 	} else {
-		srcSysVars = "sync_binlog=default;innodb_flush_log_at_trx_commit=default"
-		replicaSysVars = "sync_binlog=1000;innodb_flush_log_at_trx_commit=1"
+		masterSysVars = "sync_binlog=default;innodb_flush_log_at_trx_commit=default"
+		slaveSysVars = "sync_binlog=1000;innodb_flush_log_at_trx_commit=1"
 	}
 
 	hostName := fmt.Sprintf("%s.%s.%s", cfg.HostName, cfg.ServiceName, cfg.NameSpace)
@@ -365,7 +363,7 @@ func (cfg *Config) buildXenonConf() []byte {
     }
 }
 `, hostName, utils.XenonPort, hostName, utils.XenonPeerPort, cfg.ReplicationPassword, cfg.ReplicationUser, requestTimeout,
-		pingTimeout, cfg.RootPassword, version, srcSysVars, replicaSysVars, cfg.ElectionTimeout,
+		pingTimeout, cfg.RootPassword, version, masterSysVars, slaveSysVars, cfg.ElectionTimeout,
 		cfg.AdmitDefeatHearbeatCount, heartbeatTimeout)
 	return utils.StringToBytes(str)
 }
diff --git a/utils/common.go b/utils/common.go
index 1ad2306..1d41e9b 100644
--- a/utils/common.go
+++ b/utils/common.go
@@ -17,7 +17,10 @@ limitations under the License.
 package utils
 
 import (
+	"encoding/json"
 	"fmt"
+	"io"
+	"io/ioutil"
 	"os"
 	"sort"
 	"strconv"
@@ -120,3 +123,16 @@ func stringIn(str string, strs []string) (int, bool) {
 
 	return 0, false
 }
+
+func UnmarshalJSON(in io.Reader, obj interface{}) error {
+	body, err := ioutil.ReadAll(in)
+	if err != nil {
+		return fmt.Errorf("io read error: %s", err)
+	}
+
+	if err = json.Unmarshal(body, obj); err != nil {
+		return fmt.Errorf("error unmarshal data, error: %s, body: %s", err, string(body))
+	}
+
+	return nil
+}
diff --git a/utils/constants.go b/utils/constants.go
index e10a065..b9d91dc 100644
--- a/utils/constants.go
+++ b/utils/constants.go
@@ -16,13 +16,12 @@ limitations under the License.
 
 package utils
 
+import "net/http"
+
 var (
 	// MySQLDefaultVersion is the version for mysql that should be used
 	MySQLDefaultVersion = "5.7.34"
 
-	// InvalidMySQLVersion is used for set invalid version that we do not support.
-	InvalidMySQLVersion = "0.0.0"
-
 	// MySQLTagsToSemVer maps simple version to semver versions
 	MySQLTagsToSemVer = map[string]string{
 		"5.7": "5.7.34",
@@ -32,7 +31,12 @@ var (
 	MysqlImageVersions = map[string]string{
 		"5.7.33": "percona/percona-server:5.7.33",
 		"5.7.34": "percona/percona-server:5.7.34",
-		"0.0.0":  "errimage",
+	}
+
+	// XenonHttpUrls saves the xenon http url and its corresponding request type.
+	XenonHttpUrls = map[XenonHttpUrl]string{
+		RaftStatus:      http.MethodGet,
+		RaftTryToLeader: http.MethodPost,
 	}
 )
 
@@ -138,3 +142,20 @@ const (
 
 // JobType
 const BackupJobTypeName = ContainerBackupName
+
+// RaftRole is the role of the node in raft.
+type RaftRole string
+
+const (
+	Leader    RaftRole = "LEADER"
+	Follower  RaftRole = "FOLLOWER"
+	Candidate RaftRole = "CANDIDATE"
+)
+
+// XenonHttpUrl is a http url corresponding to the xenon instruction.
+type XenonHttpUrl string
+
+const (
+	RaftStatus      XenonHttpUrl = "/v1/raft/status"
+	RaftTryToLeader XenonHttpUrl = "/v1/raft/trytoleader"
+)


Test patch:
diff --git a/mysqlcluster/container/init_sidecar_test.go b/mysqlcluster/container/init_sidecar_test.go
index 5940648..45d388a 100644
--- a/mysqlcluster/container/init_sidecar_test.go
+++ b/mysqlcluster/container/init_sidecar_test.go
@@ -54,7 +54,6 @@ var (
 			MetricsOpts: mysqlv1alpha1.MetricsOpts{
 				Enabled: false,
 			},
-			MysqlVersion: "5.7",
 			MysqlOpts: mysqlv1alpha1.MysqlOpts{
 				InitTokuDB: false,
 			},
diff --git a/mysqlcluster/mysqlcluster_test.go b/mysqlcluster/mysqlcluster_test.go
index ddeff02..8a21509 100644
--- a/mysqlcluster/mysqlcluster_test.go
+++ b/mysqlcluster/mysqlcluster_test.go
@@ -131,17 +131,17 @@ func TestGetSelectorLabels(t *testing.T) {
 }
 
 func TestGetMySQLVersion(t *testing.T) {
-	// Invalid, currently not support: 8.0 -> 0.0.0
+	//other 8.0 ->  5.7.34
 	{
 		testMysqlCluster := mysqlCluster
 		testMysqlCluster.Spec.MysqlVersion = "8.0"
 		testCase := MysqlCluster{
 			&testMysqlCluster,
 		}
-		want := utils.InvalidMySQLVersion
+		want := "5.7.34"
 		assert.Equal(t, want, testCase.GetMySQLVersion())
 	}
-	// MySQLTagsToSemVer 5.7 -> 5.7.34
+	//MySQLTagsToSemVer 5.7 -> 5.7.34
 	{
 		testMysqlCluster := mysqlCluster
 		testMysqlCluster.Spec.MysqlVersion = "5.7"
@@ -151,14 +151,14 @@ func TestGetMySQLVersion(t *testing.T) {
 		want := "5.7.34"
 		assert.Equal(t, want, testCase.GetMySQLVersion())
 	}
-	// Invalid, not support MysqlImageVersions 5.7.34 -> 5.7.34
+	//MysqlImageVersions 5.7.34 -> 5.7.34
 	{
 		testMysqlCluster := mysqlCluster
 		testMysqlCluster.Spec.MysqlVersion = "5.7.34"
 		testCase := MysqlCluster{
 			&testMysqlCluster,
 		}
-		want := utils.InvalidMySQLVersion
+		want := "5.7.34"
 		assert.Equal(t, want, testCase.GetMySQLVersion())
 	}
 }