Repository: radondb/radondb-mysql-kubernetes (go)

Issue:
add mysql clone for mysql8.0 
**Is your feature request related to a problem? Please describe.**

<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

**Describe the solution you'd like**
add mysql clone  to replace xtrabackup fore rebuild
<!-- A clear and concise description of what you want to happen. -->

**Describe alternatives you've considered**

<!-- A clear and concise description of any alternative solutions or features you've considered. -->

**Additional context**

<!-- Add any other context or screenshots about the feature request here. -->


Pull request description:
*: add clone for mysql 8 #808
<!-- Thanks for sending a pull request!

PR Title Format:
1. pkg [, pkg2, pkg3]: what's changed #xxx
2. *: what's changed #xxx

-->

### What type of PR is this?

<!--
Add one of the following types:
/bug
/documentation
/cleanup
-->
/enhancement

### Which issue(s) this PR fixes?

<!--
Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #808 

### What this PR does?

Summary:

### Special notes for your reviewer?


Patch:
diff --git a/mysqlcluster/container/init_mysql.go b/mysqlcluster/container/init_mysql.go
index 55d3bed..78d320a 100644
--- a/mysqlcluster/container/init_mysql.go
+++ b/mysqlcluster/container/init_mysql.go
@@ -45,7 +45,9 @@ func (c *initMysql) getImage() string {
 // getCommand get the container command.
 func (c *initMysql) getCommand() []string {
 	// Because initialize mysql contain error, so do it in commands.
-	return []string{"sh", "-c", "/docker-entrypoint.sh mysqld;if test -f /docker-entrypoint-initdb.d/plugin.sh; then /docker-entrypoint-initdb.d/plugin.sh; fi "}
+	return []string{"bash", "-c", "/docker-entrypoint.sh mysqld;" +
+		"if test -f /docker-entrypoint-initdb.d/clone.sh; then /docker-entrypoint-initdb.d/clone.sh;fi;" +
+		"if test -f /docker-entrypoint-initdb.d/plugin.sh; then /docker-entrypoint-initdb.d/plugin.sh; fi "}
 }
 
 // getEnvVars get the container env.
diff --git a/mysqlcluster/container/init_sidecar.go b/mysqlcluster/container/init_sidecar.go
index 426325d..558af7e 100644
--- a/mysqlcluster/container/init_sidecar.go
+++ b/mysqlcluster/container/init_sidecar.go
@@ -109,7 +109,9 @@ func (c *initSidecar) getEnvVars() []corev1.EnvVar {
 		getEnvVarFromSecret(sctName, "METRICS_PASSWORD", "metrics-password", true),
 		getEnvVarFromSecret(sctName, "OPERATOR_USER", "operator-user", true),
 		getEnvVarFromSecret(sctName, "OPERATOR_PASSWORD", "operator-password", true),
-
+		//get donor and donor password
+		getEnvVarFromSecret(sctName, "DONOR_USER", "donor-user", true),
+		getEnvVarFromSecret(sctName, "DONOR_PASSWORD", "donor-password", true),
 		// backup user password for sidecar http server
 		getEnvVarFromSecret(sctName, "BACKUP_USER", "backup-user", true),
 		getEnvVarFromSecret(sctName, "BACKUP_PASSWORD", "backup-password", true),
diff --git a/mysqlcluster/syncer/mysql_cm.go b/mysqlcluster/syncer/mysql_cm.go
index 151d267..449101f 100644
--- a/mysqlcluster/syncer/mysql_cm.go
+++ b/mysqlcluster/syncer/mysql_cm.go
@@ -176,6 +176,15 @@ func (s *mysqlCMSyncer) appendConf() error {
 	if err := s.createOrReplaceIniKey("my.cnf", s.Spec.MysqlOpts.MysqlConf); err != nil {
 		return err
 	}
+	if s.Spec.MysqlVersion == "8.0" {
+		str := pluginConfigs["plugin-load"]
+		str = str[0:len(str)-1] + ";mysql_clone.so\""
+		if s.Spec.MysqlOpts.PluginConf != nil {
+			s.Spec.MysqlOpts.PluginConf["plugin-load"] = str
+		} else {
+			s.Spec.MysqlOpts.PluginConf = map[string]string{"plugin-load": str}
+		}
+	}
 	if err := s.createOrReplaceIniKey("plugin.cnf", s.Spec.MysqlOpts.PluginConf); err != nil {
 		return err
 	}
@@ -287,7 +296,6 @@ func buildMysqlConf(c *mysqlcluster.MysqlCluster) (string, error) {
 func buildMysqlPluginConf(c *mysqlcluster.MysqlCluster) (string, error) {
 	cfg := ini.Empty(ini.LoadOptions{IgnoreInlineComment: true})
 	sec := cfg.Section("mysqld")
-
 	addKVConfigsToSection(sec, pluginConfigs)
 	addKVConfigsToSection(sec, c.Spec.MysqlOpts.PluginConf)
 	data, err := writeConfigs(cfg)
diff --git a/mysqlcluster/syncer/secret.go b/mysqlcluster/syncer/secret.go
index 75f620c..94503a3 100644
--- a/mysqlcluster/syncer/secret.go
+++ b/mysqlcluster/syncer/secret.go
@@ -57,6 +57,11 @@ func NewSecretSyncer(cli client.Client, c *mysqlcluster.MysqlCluster) syncer.Int
 			return err
 		}
 
+		secret.Data["donor-user"] = []byte(utils.DonorCloneUser)
+		if err := addRandomPassword(secret.Data, "donor-password"); err != nil {
+			return err
+		}
+
 		// xtrabackup http server user and password
 		secret.Data["backup-user"] = []byte(utils.BackupUser)
 		if err := addRandomPassword(secret.Data, "backup-password"); err != nil {
diff --git a/mysqlcluster/syncer/status.go b/mysqlcluster/syncer/status.go
index 9ec2a5a..8ddb201 100644
--- a/mysqlcluster/syncer/status.go
+++ b/mysqlcluster/syncer/status.go
@@ -251,6 +251,10 @@ func (s *StatusSyncer) AutoRebuild(ctx context.Context, pod *corev1.Pod, items [
 	if err := s.cli.Delete(ctx, pod); err != nil {
 		return err
 	}
+	if s.Spec.MysqlVersion == "8.0" {
+		s.log.Info("Do not delete pvc")
+		return nil
+	}
 	// Delete the pvc.
 	pvcName := fmt.Sprintf("%s-%s-%d", utils.DataVolumeName,
 		s.GetNameForResource(utils.StatefulSet), ordinal)
diff --git a/sidecar/config.go b/sidecar/config.go
index b075dec..66dd86b 100644
--- a/sidecar/config.go
+++ b/sidecar/config.go
@@ -51,7 +51,10 @@ type Config struct {
 	Password string
 	// Name for new database to create.
 	Database string
-
+	// Name for donor clone
+	DonorClone string
+	// Name for donor clone
+	DonorClonePassword string
 	// The name of replication user.
 	ReplicationUser string
 	// The password of the replication user.
@@ -181,6 +184,9 @@ func NewInitConfig() *Config {
 		OperatorUser:     getEnvValue("OPERATOR_USER"),
 		OperatorPassword: getEnvValue("OPERATOR_PASSWORD"),
 
+		DonorClone:         getEnvValue("DONOR_USER"),
+		DonorClonePassword: getEnvValue("DONOR_PASSWORD"),
+
 		InitTokuDB: initTokuDB,
 
 		MySQLVersion: mysqlSemVer,
@@ -401,6 +407,9 @@ GRANT SUPER, PROCESS, RELOAD, CREATE, SELECT ON *.* TO '%s'@'%%';
 DROP user IF EXISTS '%s'@'%%';
 CREATE USER '%s'@'%%' IDENTIFIED BY '%s';
 GRANT ALL ON %s.* TO '%s'@'%%' ;
+DROP user IF EXISTS '%s'@'%%';
+CREATE USER '%s'@'%%' IDENTIFIED BY '%s';
+GRANT BACKUP_ADMIN ON *.* TO '%s'@'%%' ;
 FLUSH PRIVILEGES;
 
 %s
@@ -423,6 +432,9 @@ FLUSH PRIVILEGES;
 		cfg.User,               //drop user
 		cfg.User, cfg.Password, //create user
 		cfg.Database, cfg.User, //grant
+		cfg.DonorClone,
+		cfg.DonorClone, cfg.DonorClonePassword,
+		cfg.DonorClone, // grant
 		initSQL,
 	)
 
@@ -452,6 +464,11 @@ func (cfg *Config) buildClientConfig() (*ini.File, error) {
 	if _, err := sec.NewKey("password", cfg.OperatorPassword); err != nil {
 		return nil, err
 	}
+	if cfg.MySQLVersion.Major == 8 {
+		if _, err := sec.NewKey("donor", cfg.DonorClone); err != nil {
+			return nil, err
+		}
+	}
 
 	return conf, nil
 }
diff --git a/sidecar/init.go b/sidecar/init.go
index e5b014c..92b92eb 100644
--- a/sidecar/init.go
+++ b/sidecar/init.go
@@ -90,29 +90,41 @@ func CheckServiceExist(cfg *Config, service string) bool {
 func runCloneAndInit(cfg *Config) (bool, error) {
 	//check follower is exists?
 	serviceURL := ""
+	server := ""
 	var hasInitialized = false
+	var err error
 	// Check the rebuildFrom exist?
-	if service, err := getPod(cfg); err == nil {
-		serviceURL = service
+	if serviceURL, server, err = getPod(cfg); err == nil {
 		log.Info("found the rebuild-from pod", "service", serviceURL)
 	} else {
 		log.Info("found the rebuild-from pod", "error", err.Error())
 	}
 	if len(serviceURL) == 0 && CheckServiceExist(cfg, "follower") {
 		serviceURL = fmt.Sprintf("http://%s-%s:%v", cfg.ClusterName, "follower", utils.XBackupPort)
+		server = fmt.Sprintf("%s-%s", cfg.ClusterName, "follower")
 	}
 	//check leader is exist?
 	if len(serviceURL) == 0 && CheckServiceExist(cfg, "leader") {
 		serviceURL = fmt.Sprintf("http://%s-%s:%v", cfg.ClusterName, "leader", utils.XBackupPort)
+		server = fmt.Sprintf("%s-%s", cfg.ClusterName, "leader")
 	}
 
 	if len(serviceURL) != 0 {
 		// Check has initialized. If so just return.
 		hasInitialized, _ = checkIfPathExists(path.Join(dataPath, "mysql"))
+		// TODO is MySQL8, CLone it
+		if cfg.MySQLVersion.Major == 8 {
+			// TODO return clone int
+			log.Info("server choose ", "server", server)
+			err := DoCLone(cfg, server)
+			return hasInitialized, err
+		}
+
 		if hasInitialized {
 			log.Info("MySQL data directory existing!")
 			return hasInitialized, nil
 		}
+
 		// backup at first
 		Args := fmt.Sprintf("curl --user $BACKUP_USER:$BACKUP_PASSWORD %s/download|xbstream -x -C %s; exit ${PIPESTATUS[0]}",
 			serviceURL, utils.DataVolumeMountPath)
@@ -408,16 +420,16 @@ func copyMySQLchecker() error {
 	return nil
 }
 
-func getPod(cfg *Config) (string, error) {
+func getPod(cfg *Config) (string, string, error) {
 	log.Info("Now check the pod which has got rebuild-from")
 	config, err := rest.InClusterConfig()
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	// creates the clientset
 	clientset, err := kubernetes.NewForConfig(config)
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	match := map[string]string{
 		utils.LabelRebuildFrom: "true",
@@ -426,7 +438,7 @@ func getPod(cfg *Config) (string, error) {
 		List(context.TODO(), v1.ListOptions{
 			LabelSelector: labels.SelectorFromSet(match).String()})
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	if len(podList.Items) == 1 {
 		pod := podList.Items[0]
@@ -434,9 +446,10 @@ func getPod(cfg *Config) (string, error) {
 		if err := removeRebuildFrom(clientset, cfg, pod.Name); err != nil {
 			log.Info("remove rebuild from", "error", err.Error())
 		}
-		return fmt.Sprintf("%s.%s-mysql.%s:%v", pod.Name, cfg.ClusterName, cfg.NameSpace, utils.XBackupPort), nil
+		return fmt.Sprintf("%s.%s-mysql.%s:%v", pod.Name, cfg.ClusterName, cfg.NameSpace, utils.XBackupPort),
+			fmt.Sprintf("%s.%s-mysql.%s", pod.Name, cfg.ClusterName, cfg.NameSpace), nil
 	} else {
-		return "", fmt.Errorf("not correct pod choose")
+		return "", "", fmt.Errorf("not correct pod choose")
 	}
 
 }
@@ -447,3 +460,29 @@ func removeRebuildFrom(clientset *kubernetes.Clientset, cfg *Config, podName str
 
 	return err
 }
+
+func DoCLone(cfg *Config, server string) error {
+	sql := fmt.Sprintf(`SET GLOBAL clone_valid_donor_list = '%s:3306' ;
+	CLONE INSTANCE FROM '%s'@'%s':3306
+	IDENTIFIED BY '%s';
+`, server, cfg.DonorClone, server, cfg.DonorClonePassword)
+	cloneSh := fmt.Sprintf(`#!/bin/bash
+mysqld &
+mysql=( mysql -uroot -hlocalhost  --password="" )
+
+for i in {120..0}; do
+	if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then
+		break
+	fi
+	echo 'MySQL run process in progress...'
+	sleep 1
+done
+mysql -uroot -hlocalhost  --password="" -e "%s"
+`, sql)
+	log.Info("write clone shell", "shell", cloneSh)
+	//write to docker.
+	if err := ioutil.WriteFile(initFilePath+"/clone.sh", []byte(cloneSh), 0755); err != nil {
+		return fmt.Errorf("failed to write clone.sh: %s", err)
+	}
+	return nil
+}
diff --git a/utils/constants.go b/utils/constants.go
index 2870062..07f0868 100644
--- a/utils/constants.go
+++ b/utils/constants.go
@@ -75,6 +75,8 @@ const (
 	MetricsUser = "radondb_metrics"
 	// The MySQL user used for operator to connect to the mysql node for configuration.
 	OperatorUser = "radondb_operator"
+	// donor user
+	DonorCloneUser = "radondb_donor"
 	// The name of the MySQL root user.
 	RootUser = "root"
 


Test patch:
diff --git a/mysqlcluster/container/init_mysql_test.go b/mysqlcluster/container/init_mysql_test.go
index 87194a2..251a7ff 100644
--- a/mysqlcluster/container/init_mysql_test.go
+++ b/mysqlcluster/container/init_mysql_test.go
@@ -113,7 +113,7 @@ func TestGetInitMysqlImage(t *testing.T) {
 }
 
 func TestGetInitMysqlCommand(t *testing.T) {
-	assert.Equal(t, initMysqlCase.Command, []string{"sh", "-c", "/docker-entrypoint.sh mysqld;if test -f /docker-entrypoint-initdb.d/plugin.sh; then /docker-entrypoint-initdb.d/plugin.sh; fi "})
+	assert.Equal(t, initMysqlCase.Command, []string{"bash", "-c", "/docker-entrypoint.sh mysqld;if test -f /docker-entrypoint-initdb.d/clone.sh; then /docker-entrypoint-initdb.d/clone.sh;fi;if test -f /docker-entrypoint-initdb.d/plugin.sh; then /docker-entrypoint-initdb.d/plugin.sh; fi "})
 }
 
 func TestGetInitMysqlEnvVar(t *testing.T) {
diff --git a/mysqlcluster/container/init_sidecar_test.go b/mysqlcluster/container/init_sidecar_test.go
index 48cd922..fd51293 100644
--- a/mysqlcluster/container/init_sidecar_test.go
+++ b/mysqlcluster/container/init_sidecar_test.go
@@ -243,6 +243,30 @@ var (
 				},
 			},
 		},
+		{
+			Name: "DONOR_USER",
+			ValueFrom: &corev1.EnvVarSource{
+				SecretKeyRef: &corev1.SecretKeySelector{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: sctName,
+					},
+					Key:      "donor-user",
+					Optional: &optTrue,
+				},
+			},
+		},
+		{
+			Name: "DONOR_PASSWORD",
+			ValueFrom: &corev1.EnvVarSource{
+				SecretKeyRef: &corev1.SecretKeySelector{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: sctName,
+					},
+					Key:      "donor-password",
+					Optional: &optTrue,
+				},
+			},
+		},
 		{
 			Name: "BACKUP_USER",
 			ValueFrom: &corev1.EnvVarSource{