From 415e8e822c5972a366ee64d174612a4f9939e0ce Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Fri, 5 Jan 2024 09:17:36 +0100 Subject: [PATCH 01/12] Refactored getting topology. --- cmd/manager/main.go | 1 + pkg/controller/psbackup/controller.go | 87 ++++++++- pkg/mysql/topology/topology.go | 77 -------- pkg/orchestrator/client.go | 242 -------------------------- pkg/orchestrator/clientexec.go | 21 +++ 5 files changed, 107 insertions(+), 321 deletions(-) delete mode 100644 pkg/mysql/topology/topology.go delete mode 100644 pkg/orchestrator/client.go diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 2bca7945f..5ff5535fd 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -163,6 +163,7 @@ func main() { Client: nsClient, Scheme: mgr.GetScheme(), ServerVersion: serverVersion, + ClientCmd: cliCmd, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "PerconaServerMySQLBackup") os.Exit(1) diff --git a/pkg/controller/psbackup/controller.go b/pkg/controller/psbackup/controller.go index 9b81b1bf1..ec0813bf9 100644 --- a/pkg/controller/psbackup/controller.go +++ b/pkg/controller/psbackup/controller.go @@ -41,9 +41,12 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" "github.com/percona/percona-server-mysql-operator/pkg/k8s" - "github.com/percona/percona-server-mysql-operator/pkg/mysql/topology" + "github.com/percona/percona-server-mysql-operator/pkg/mysql" + "github.com/percona/percona-server-mysql-operator/pkg/orchestrator" "github.com/percona/percona-server-mysql-operator/pkg/platform" + "github.com/percona/percona-server-mysql-operator/pkg/replicator" "github.com/percona/percona-server-mysql-operator/pkg/secret" "github.com/percona/percona-server-mysql-operator/pkg/xtrabackup" ) @@ -53,6 +56,7 @@ type PerconaServerMySQLBackupReconciler struct { client.Client Scheme *runtime.Scheme ServerVersion *platform.ServerVersion + ClientCmd clientcmd.Client } //+kubebuilder:rbac:groups=ps.percona.com,resources=perconaservermysqlbackups;perconaservermysqlbackups/status;perconaservermysqlbackups/finalizers,verbs=get;list;watch;create;update;patch;delete @@ -317,7 +321,7 @@ func (r *PerconaServerMySQLBackupReconciler) getBackupSource(ctx context.Context return "", errors.Wrap(err, "get operator password") } - top, err := topology.Get(ctx, cluster, operatorPass) + top, err := r.getTopology(ctx, cluster, operatorPass) if err != nil { return "", errors.Wrap(err, "get topology") } @@ -333,6 +337,85 @@ func (r *PerconaServerMySQLBackupReconciler) getBackupSource(ctx context.Context return source, nil } +type Topology struct { + Primary string + Replicas []string +} + +func (r *PerconaServerMySQLBackupReconciler) getTopology(ctx context.Context, cluster *apiv1alpha1.PerconaServerMySQL, operatorPass string) (Topology, error) { + switch cluster.Spec.MySQL.ClusterType { + case apiv1alpha1.ClusterTypeGR: + firstPod, err := getMySQLPod(ctx, r.Client, cluster, 0) + if err != nil { + return Topology{}, err + } + + fqdn := mysql.FQDN(cluster, 0) + + db, err := replicator.NewReplicatorExec(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) + if err != nil { + return Topology{}, errors.Wrapf(err, "open connection to %s", fqdn) + } + + replicas, err := db.GetGroupReplicationReplicas(ctx) + if err != nil { + return Topology{}, errors.Wrap(err, "get group-replication replicas") + } + + primary, err := db.GetGroupReplicationPrimary(ctx) + if err != nil { + return Topology{}, errors.Wrap(err, "get group-replication primary") + } + return Topology{ + Primary: primary, + Replicas: replicas, + }, nil + case apiv1alpha1.ClusterTypeAsync: + pod, err := getOrcPod(ctx, r.Client, cluster, 0) + if err != nil { + return Topology{}, err + } + primary, err := orchestrator.ClusterPrimaryExec(ctx, r.ClientCmd, pod, cluster.ClusterHint()) + + if err != nil { + return Topology{}, errors.Wrap(err, "get primary") + } + + replicas := make([]string, 0, len(primary.Replicas)) + for _, r := range primary.Replicas { + replicas = append(replicas, r.Hostname) + } + return Topology{ + Primary: primary.Key.Hostname, + Replicas: replicas, + }, nil + default: + return Topology{}, errors.New("unknown cluster type") + } +} + +func getMySQLPod(ctx context.Context, cl client.Reader, cr *apiv1alpha1.PerconaServerMySQL, idx int) (*corev1.Pod, error) { + pod := &corev1.Pod{} + + nn := types.NamespacedName{Namespace: cr.Namespace, Name: mysql.PodName(cr, idx)} + if err := cl.Get(ctx, nn, pod); err != nil { + return nil, err + } + + return pod, nil +} + +func getOrcPod(ctx context.Context, cl client.Reader, cr *apiv1alpha1.PerconaServerMySQL, idx int) (*corev1.Pod, error) { + pod := &corev1.Pod{} + + nn := types.NamespacedName{Namespace: cr.Namespace, Name: orchestrator.PodName(cr, idx)} + if err := cl.Get(ctx, nn, pod); err != nil { + return nil, err + } + + return pod, nil +} + const finalizerDeleteBackup = "delete-backup" func (r *PerconaServerMySQLBackupReconciler) checkFinalizers(ctx context.Context, cr *apiv1alpha1.PerconaServerMySQLBackup) { diff --git a/pkg/mysql/topology/topology.go b/pkg/mysql/topology/topology.go deleted file mode 100644 index 32a5b143a..000000000 --- a/pkg/mysql/topology/topology.go +++ /dev/null @@ -1,77 +0,0 @@ -package topology - -import ( - "context" - - "github.com/pkg/errors" - - apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - "github.com/percona/percona-server-mysql-operator/pkg/mysql" - "github.com/percona/percona-server-mysql-operator/pkg/orchestrator" - "github.com/percona/percona-server-mysql-operator/pkg/replicator" -) - -type Topology struct { - Primary string - Replicas []string -} - -func Get(ctx context.Context, cluster *apiv1alpha1.PerconaServerMySQL, operatorPass string) (Topology, error) { - var err error - var top Topology - switch cluster.Spec.MySQL.ClusterType { - case apiv1alpha1.ClusterTypeGR: - top, err = getGRTopology(ctx, cluster, operatorPass) - if err != nil { - return Topology{}, errors.Wrap(err, "get group-replication topology") - } - case apiv1alpha1.ClusterTypeAsync: - top, err = getAsyncTopology(ctx, cluster) - if err != nil { - return Topology{}, errors.Wrap(err, "get async topology") - } - default: - return Topology{}, errors.New("unknown cluster type") - } - return top, nil -} - -func getGRTopology(ctx context.Context, cluster *apiv1alpha1.PerconaServerMySQL, operatorPass string) (Topology, error) { - fqdn := mysql.FQDN(cluster, 0) - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserOperator, operatorPass, fqdn, mysql.DefaultAdminPort) - if err != nil { - return Topology{}, errors.Wrapf(err, "open connection to %s", fqdn) - } - defer db.Close() - - replicas, err := db.GetGroupReplicationReplicas(ctx) - if err != nil { - return Topology{}, errors.Wrap(err, "get group-replication replicas") - } - - primary, err := db.GetGroupReplicationPrimary(ctx) - if err != nil { - return Topology{}, errors.Wrap(err, "get group-replication primary") - } - return Topology{ - Primary: primary, - Replicas: replicas, - }, nil -} - -func getAsyncTopology(ctx context.Context, cluster *apiv1alpha1.PerconaServerMySQL) (Topology, error) { - orcHost := orchestrator.APIHost(cluster) - primary, err := orchestrator.ClusterPrimary(ctx, orcHost, cluster.ClusterHint()) - if err != nil { - return Topology{}, errors.Wrap(err, "get primary") - } - - replicas := make([]string, 0, len(primary.Replicas)) - for _, r := range primary.Replicas { - replicas = append(replicas, r.Hostname) - } - return Topology{ - Primary: primary.Key.Hostname, - Replicas: replicas, - }, nil -} diff --git a/pkg/orchestrator/client.go b/pkg/orchestrator/client.go deleted file mode 100644 index 5f9ef2bbe..000000000 --- a/pkg/orchestrator/client.go +++ /dev/null @@ -1,242 +0,0 @@ -package orchestrator - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/pkg/errors" -) - -type orcResponse struct { - Code string `json:"Code"` - Message string `json:"Message"` - Details interface{} `json:"Details,omitempty"` -} - -type InstanceKey struct { - Hostname string `json:"Hostname"` - Port int32 `json:"Port"` -} - -type Instance struct { - Key InstanceKey `json:"Key"` - Alias string `json:"InstanceAlias"` - MasterKey InstanceKey `json:"MasterKey"` - Replicas []InstanceKey `json:"Replicas"` - ReadOnly bool `json:"ReadOnly"` -} - -func ClusterPrimary(ctx context.Context, apiHost, clusterHint string) (*Instance, error) { - url := fmt.Sprintf("%s/api/master/%s", apiHost, clusterHint) - - resp, err := doRequest(ctx, url) - if err != nil { - return nil, errors.Wrapf(err, "do request to %s", url) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, errors.Wrap(err, "read response body") - } - - primary := &Instance{} - if err := json.Unmarshal(body, primary); err == nil { - return primary, nil - } - - orcResp := &orcResponse{} - if err := json.Unmarshal(body, orcResp); err != nil { - return nil, errors.Wrap(err, "json decode") - } - - if orcResp.Code == "ERROR" { - return nil, errors.New(orcResp.Message) - } - - return primary, nil -} - -func StopReplication(ctx context.Context, apiHost, host string, port int32) error { - url := fmt.Sprintf("%s/api/stop-replica/%s/%d", apiHost, host, port) - - resp, err := doRequest(ctx, url) - if err != nil { - return errors.Wrapf(err, "do request to %s", url) - } - defer resp.Body.Close() - - orcResp := &orcResponse{} - if err := json.NewDecoder(resp.Body).Decode(orcResp); err != nil { - return errors.Wrap(err, "json decode") - } - - if orcResp.Code == "ERROR" { - return errors.New(orcResp.Message) - } - - return nil -} - -func StartReplication(ctx context.Context, apiHost, host string, port int32) error { - url := fmt.Sprintf("%s/api/start-replica/%s/%d", apiHost, host, port) - - resp, err := doRequest(ctx, url) - if err != nil { - return errors.Wrapf(err, "do request to %s", url) - } - defer resp.Body.Close() - - orcResp := &orcResponse{} - if err := json.NewDecoder(resp.Body).Decode(orcResp); err != nil { - return errors.Wrap(err, "json decode") - } - - if orcResp.Code == "ERROR" { - return errors.New(orcResp.Message) - } - - return nil -} - -func AddPeer(ctx context.Context, apiHost string, peer string) error { - url := fmt.Sprintf("%s/api/raft-add-peer/%s", apiHost, peer) - - resp, err := doRequest(ctx, url) - if err != nil { - return errors.Wrapf(err, "do request to %s", url) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return errors.Wrap(err, "read response body") - } - - // Orchestrator returns peer IP as string on success - o := "" - if err := json.Unmarshal(body, &o); err == nil { - return nil - } - - orcResp := &orcResponse{} - if err := json.Unmarshal(body, &orcResp); err != nil { - return errors.Wrap(err, "json decode") - } - - if orcResp.Code == "ERROR" { - return errors.New(orcResp.Message) - } - - return nil -} - -func RemovePeer(ctx context.Context, apiHost string, peer string) error { - url := fmt.Sprintf("%s/api/raft-remove-peer/%s", apiHost, peer) - - resp, err := doRequest(ctx, url) - if err != nil { - return errors.Wrapf(err, "do request to %s", url) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return errors.Wrap(err, "read response body") - } - - // Orchestrator returns peer IP as string on success - o := "" - if err := json.Unmarshal(body, &o); err == nil { - return nil - } - - orcResp := &orcResponse{} - if err := json.Unmarshal(body, &orcResp); err != nil { - return errors.Wrap(err, "json decode") - } - - if orcResp.Code == "ERROR" { - return errors.New(orcResp.Message) - } - - return nil -} - -func EnsureNodeIsPrimary(ctx context.Context, apiHost, clusterHint, host string, port int) error { - primary, err := ClusterPrimary(ctx, apiHost, clusterHint) - if err != nil { - return errors.Wrap(err, "get cluster primary") - } - - if primary.Alias == host { - return nil - } - - // /api/graceful-master-takeover-auto/cluster1.default/cluster1-mysql-0/3306 - url := fmt.Sprintf("%s/api/graceful-master-takeover-auto/%s/%s/%d", apiHost, clusterHint, host, port) - - resp, err := doRequest(ctx, url) - if err != nil { - return errors.Wrapf(err, "do request to %s", url) - } - defer resp.Body.Close() - - orcResp := &orcResponse{} - if err := json.NewDecoder(resp.Body).Decode(orcResp); err != nil { - return errors.Wrap(err, "json decode") - } - - if orcResp.Code == "ERROR" { - return errors.New(orcResp.Message) - } - - return nil -} - -var ErrEmptyResponse = errors.New("empty response") - -func Discover(ctx context.Context, apiHost, host string, port int) error { - url := fmt.Sprintf("%s/api/discover/%s/%d", apiHost, host, port) - - resp, err := doRequest(ctx, url) - if err != nil { - return errors.Wrapf(err, "do request to %s", url) - } - defer resp.Body.Close() - - orcResp := new(orcResponse) - data, err := io.ReadAll(resp.Body) - if err != nil { - return errors.Wrap(err, "read response body") - } - - if len(data) == 0 { - return ErrEmptyResponse - } - - if err := json.Unmarshal(data, orcResp); err != nil { - return errors.Wrapf(err, "json decode \"%s\"", string(data)) - } - - if orcResp.Code == "ERROR" { - return errors.New(orcResp.Message) - } - return nil -} - -func doRequest(ctx context.Context, url string) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, errors.Wrap(err, "make request") - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, errors.Wrap(err, "do request") - } - - return resp, nil -} diff --git a/pkg/orchestrator/clientexec.go b/pkg/orchestrator/clientexec.go index 41242621d..046eac244 100644 --- a/pkg/orchestrator/clientexec.go +++ b/pkg/orchestrator/clientexec.go @@ -12,6 +12,27 @@ import ( "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" ) +type orcResponse struct { + Code string `json:"Code"` + Message string `json:"Message"` + Details interface{} `json:"Details,omitempty"` +} + +type InstanceKey struct { + Hostname string `json:"Hostname"` + Port int32 `json:"Port"` +} + +type Instance struct { + Key InstanceKey `json:"Key"` + Alias string `json:"InstanceAlias"` + MasterKey InstanceKey `json:"MasterKey"` + Replicas []InstanceKey `json:"Replicas"` + ReadOnly bool `json:"ReadOnly"` +} + +var ErrEmptyResponse = errors.New("empty response") + func exec(ctx context.Context, cliCmd clientcmd.Client, pod *corev1.Pod, endpoint string, outb, errb *bytes.Buffer) error { c := []string{"curl", fmt.Sprintf("localhost:%d/%s", defaultWebPort, endpoint)} err := cliCmd.Exec(ctx, pod, "orc", c, nil, outb, errb, false) From 6363872a2eda8d91458669476049cd7f7264c8d7 Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Mon, 8 Jan 2024 09:23:26 +0100 Subject: [PATCH 02/12] Stop using Replicator interface in bootstrap and heathcheck binaries. --- cmd/bootstrap/async_replication.go | 9 +- cmd/healthcheck/main.go | 11 +- cmd/mysql/mysql.go | 236 +++++++++++++++ pkg/replicator/replicator.go | 385 ++++++++++++++---------- pkg/replicator/replicatorexec.go | 457 ----------------------------- 5 files changed, 478 insertions(+), 620 deletions(-) create mode 100644 cmd/mysql/mysql.go delete mode 100644 pkg/replicator/replicatorexec.go diff --git a/cmd/bootstrap/async_replication.go b/cmd/bootstrap/async_replication.go index 25959293a..0f8b15cb4 100644 --- a/cmd/bootstrap/async_replication.go +++ b/cmd/bootstrap/async_replication.go @@ -11,6 +11,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + database "github.com/percona/percona-server-mysql-operator/cmd/mysql" "github.com/percona/percona-server-mysql-operator/pkg/mysql" "github.com/percona/percona-server-mysql-operator/pkg/replicator" ) @@ -87,9 +88,9 @@ func bootstrapAsyncReplication(ctx context.Context) error { return errors.Wrapf(err, "get %s password", apiv1alpha1.UserOperator) } - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserOperator, operatorPass, podIp, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserOperator, operatorPass, podIp, mysql.DefaultAdminPort) if err != nil { - return errors.Wrap(err, "connect to db") + return errors.Wrap(err, "connect to database") } defer db.Close() @@ -208,7 +209,7 @@ func getTopology(ctx context.Context, peers sets.Set[string]) (string, []string, } for _, peer := range sets.List(peers) { - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserOperator, operatorPass, peer, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserOperator, operatorPass, peer, mysql.DefaultAdminPort) if err != nil { return "", nil, errors.Wrapf(err, "connect to %s", peer) } @@ -255,7 +256,7 @@ func selectDonor(ctx context.Context, fqdn, primary string, replicas []string) ( } for _, replica := range replicas { - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserOperator, operatorPass, replica, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserOperator, operatorPass, replica, mysql.DefaultAdminPort) if err != nil { continue } diff --git a/cmd/healthcheck/main.go b/cmd/healthcheck/main.go index 5dd982f2d..3f11eed3e 100644 --- a/cmd/healthcheck/main.go +++ b/cmd/healthcheck/main.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + database "github.com/percona/percona-server-mysql-operator/cmd/mysql" "github.com/percona/percona-server-mysql-operator/pkg/k8s" "github.com/percona/percona-server-mysql-operator/pkg/mysql" "github.com/percona/percona-server-mysql-operator/pkg/replicator" @@ -81,7 +82,7 @@ func checkReadinessAsync(ctx context.Context) error { return errors.Wrapf(err, "get %s password", apiv1alpha1.UserMonitor) } - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) if err != nil { return errors.Wrap(err, "connect to db") } @@ -116,7 +117,7 @@ func checkReadinessGR(ctx context.Context) error { return errors.Wrapf(err, "get %s password", apiv1alpha1.UserMonitor) } - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) if err != nil { return errors.Wrap(err, "connect to db") } @@ -150,7 +151,7 @@ func checkLivenessAsync(ctx context.Context) error { return errors.Wrapf(err, "get %s password", apiv1alpha1.UserMonitor) } - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) if err != nil { return errors.Wrap(err, "connect to db") } @@ -170,7 +171,7 @@ func checkLivenessGR(ctx context.Context) error { return errors.Wrapf(err, "get %s password", apiv1alpha1.UserMonitor) } - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) if err != nil { return errors.Wrap(err, "connect to db") } @@ -201,7 +202,7 @@ func checkReplication(ctx context.Context) error { return errors.Wrapf(err, "get %s password", apiv1alpha1.UserMonitor) } - db, err := replicator.NewReplicator(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) + db, err := database.NewDatabase(ctx, apiv1alpha1.UserMonitor, monitorPass, podIP, mysql.DefaultAdminPort) if err != nil { return errors.Wrap(err, "connect to db") } diff --git a/cmd/mysql/mysql.go b/cmd/mysql/mysql.go new file mode 100644 index 000000000..68b73db43 --- /dev/null +++ b/cmd/mysql/mysql.go @@ -0,0 +1,236 @@ +package mysql + +import ( + "context" + "database/sql" + "fmt" + + "github.com/go-sql-driver/mysql" + "github.com/pkg/errors" + + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/replicator" +) + +var ErrRestartAfterClone = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") + +type ReplicationStatus int8 + +type DB struct{ db *sql.DB } + +func NewDatabase(ctx context.Context, user apiv1alpha1.SystemUser, pass, host string, port int32) (*DB, error) { + config := mysql.NewConfig() + + config.User = string(user) + config.Passwd = pass + config.Net = "tcp" + config.Addr = fmt.Sprintf("%s:%d", host, port) + config.DBName = "performance_schema" + config.Params = map[string]string{ + "interpolateParams": "true", + "timeout": "10s", + "readTimeout": "10s", + "writeTimeout": "10s", + "tls": "preferred", + } + + db, err := sql.Open("mysql", config.FormatDSN()) + if err != nil { + return nil, errors.Wrap(err, "connect to MySQL") + } + + if err := db.PingContext(ctx); err != nil { + return nil, errors.Wrap(err, "ping DB") + } + + return &DB{db}, nil +} + +func (d *DB) StartReplication(ctx context.Context, host, replicaPass string, port int32) error { + // TODO: Make retries configurable + _, err := d.db.ExecContext(ctx, ` + CHANGE REPLICATION SOURCE TO + SOURCE_USER=?, + SOURCE_PASSWORD=?, + SOURCE_HOST=?, + SOURCE_PORT=?, + SOURCE_SSL=1, + SOURCE_CONNECTION_AUTO_FAILOVER=1, + SOURCE_AUTO_POSITION=1, + SOURCE_RETRY_COUNT=3, + SOURCE_CONNECT_RETRY=60 + `, apiv1alpha1.UserReplication, replicaPass, host, port) + if err != nil { + return errors.Wrap(err, "exec CHANGE REPLICATION SOURCE TO") + } + + _, err = d.db.ExecContext(ctx, "START REPLICA") + return errors.Wrap(err, "start replication") +} + +func (d *DB) StopReplication(ctx context.Context) error { + _, err := d.db.ExecContext(ctx, "STOP REPLICA") + return errors.Wrap(err, "stop replication") +} + +func (d *DB) ResetReplication(ctx context.Context) error { + _, err := d.db.ExecContext(ctx, "RESET REPLICA ALL") + return errors.Wrap(err, "reset replication") +} + +func (d *DB) ReplicationStatus(ctx context.Context) (replicator.ReplicationStatus, string, error) { + row := d.db.QueryRowContext(ctx, ` + SELECT + connection_status.SERVICE_STATE, + applier_status.SERVICE_STATE, + HOST + FROM replication_connection_status connection_status + JOIN replication_connection_configuration connection_configuration + ON connection_status.channel_name = connection_configuration.channel_name + JOIN replication_applier_status applier_status + ON connection_status.channel_name = applier_status.channel_name + WHERE connection_status.channel_name = ? + `, replicator.DefaultChannelName) + + var ioState, sqlState, host string + if err := row.Scan(&ioState, &sqlState, &host); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return replicator.ReplicationStatusNotInitiated, "", nil + } + return replicator.ReplicationStatusError, "", errors.Wrap(err, "scan replication status") + } + + if ioState == "ON" && sqlState == "ON" { + return replicator.ReplicationStatusActive, host, nil + } + + return replicator.ReplicationStatusNotInitiated, "", nil +} + +func (d *DB) IsReplica(ctx context.Context) (bool, error) { + status, _, err := d.ReplicationStatus(ctx) + return status == replicator.ReplicationStatusActive, errors.Wrap(err, "get replication status") +} + +func (d *DB) DisableSuperReadonly(ctx context.Context) error { + _, err := d.db.ExecContext(ctx, "SET GLOBAL SUPER_READ_ONLY=0") + return errors.Wrap(err, "set global super_read_only param to 0") +} + +func (d *DB) IsReadonly(ctx context.Context) (bool, error) { + var readonly int + err := d.db.QueryRowContext(ctx, "select @@read_only and @@super_read_only").Scan(&readonly) + return readonly == 1, errors.Wrap(err, "select global read_only param") +} + +func (d *DB) ReportHost(ctx context.Context) (string, error) { + var reportHost string + err := d.db.QueryRowContext(ctx, "select @@report_host").Scan(&reportHost) + return reportHost, errors.Wrap(err, "select report_host param") +} + +func (d *DB) Close() error { + return d.db.Close() +} + +func (d *DB) CloneInProgress(ctx context.Context) (bool, error) { + rows, err := d.db.QueryContext(ctx, "SELECT STATE FROM clone_status") + if err != nil { + return false, errors.Wrap(err, "fetch clone status") + } + defer rows.Close() + + for rows.Next() { + var state string + if err := rows.Scan(&state); err != nil { + return false, errors.Wrap(err, "scan rows") + } + + if state != "Completed" && state != "Failed" { + return true, nil + } + } + + return false, nil +} + +func (d *DB) Clone(ctx context.Context, donor, user, pass string, port int32) error { + _, err := d.db.ExecContext(ctx, "SET GLOBAL clone_valid_donor_list=?", fmt.Sprintf("%s:%d", donor, port)) + if err != nil { + return errors.Wrap(err, "set clone_valid_donor_list") + } + + _, err = d.db.ExecContext(ctx, "CLONE INSTANCE FROM ?@?:? IDENTIFIED BY ?", user, donor, port, pass) + + mErr, ok := err.(*mysql.MySQLError) + if !ok { + return errors.Wrap(err, "clone instance") + } + + // Error 3707: Restart server failed (mysqld is not managed by supervisor process). + if mErr.Number == uint16(3707) { + return ErrRestartAfterClone + } + + return nil +} + +func (d *DB) DumbQuery(ctx context.Context) error { + _, err := d.db.ExecContext(ctx, "SELECT 1") + return errors.Wrap(err, "SELECT 1") +} + +func (d *DB) GetMemberState(ctx context.Context, host string) (replicator.MemberState, error) { + var state replicator.MemberState + + err := d.db.QueryRowContext(ctx, "SELECT MEMBER_STATE FROM replication_group_members WHERE MEMBER_HOST=?", host).Scan(&state) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return replicator.MemberStateOffline, nil + } + return replicator.MemberStateError, errors.Wrap(err, "query member state") + } + + return state, nil +} + +func (d *DB) CheckIfInPrimaryPartition(ctx context.Context) (bool, error) { + var in bool + + err := d.db.QueryRowContext(ctx, ` + SELECT + MEMBER_STATE = 'ONLINE' + AND ( + ( + SELECT + COUNT(*) + FROM + performance_schema.replication_group_members + WHERE + MEMBER_STATE NOT IN ('ONLINE', 'RECOVERING') + ) >= ( + ( + SELECT + COUNT(*) + FROM + performance_schema.replication_group_members + ) / 2 + ) = 0 + ) + FROM + performance_schema.replication_group_members + JOIN performance_schema.replication_group_member_stats USING(member_id) + WHERE + member_id = @@global.server_uuid; + `).Scan(&in) + if err != nil { + return false, err + } + + return in, nil +} + +func (d *DB) EnableSuperReadonly(ctx context.Context) error { + _, err := d.db.ExecContext(ctx, "SET GLOBAL SUPER_READ_ONLY=1") + return errors.Wrap(err, "set global super_read_only param to 1") +} diff --git a/pkg/replicator/replicator.go b/pkg/replicator/replicator.go index 435d3527c..3a6046826 100644 --- a/pkg/replicator/replicator.go +++ b/pkg/replicator/replicator.go @@ -1,22 +1,32 @@ package replicator import ( + "bytes" "context" "database/sql" + "encoding/csv" "fmt" + "regexp" + "strings" "github.com/go-sql-driver/mysql" + "github.com/gocarina/gocsv" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" "github.com/percona/percona-server-mysql-operator/pkg/innodbcluster" ) +var sensitiveRegexp = regexp.MustCompile(":.*@") + const DefaultChannelName = "" type ReplicationStatus int8 -var ErrRestartAfterClone error = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") +var ErrRestartAfterClone = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") +var ErrGroupReplicationNotReady = errors.New("Error 3092: The server is not configured properly to be an active member of the group.") const ( ReplicationStatusActive ReplicationStatus = iota @@ -34,8 +44,6 @@ const ( MemberStateUnreachable MemberState = "UNREACHABLE" ) -var ErrGroupReplicationNotReady = errors.New("Error 3092: The server is not configured properly to be an active member of the group.") - type Replicator interface { ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error StartReplication(ctx context.Context, host, replicaPass string, port int32) error @@ -64,51 +72,72 @@ type Replicator interface { CheckIfInPrimaryPartition(ctx context.Context) (bool, error) CheckIfPrimaryUnreachable(ctx context.Context) (bool, error) } +type dbImplExec struct { + client clientcmd.Client + pod *corev1.Pod + user apiv1alpha1.SystemUser + pass string + host string +} + +func NewReplicatorExec(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Replicator, error) { + return &dbImplExec{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil +} + +func (d *dbImplExec) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { + cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", d.pass), "-u", string(d.user), "-h", d.host, "-e", stm} -type dbImpl struct{ db *sql.DB } - -func NewReplicator(ctx context.Context, user apiv1alpha1.SystemUser, pass, host string, port int32) (Replicator, error) { - config := mysql.NewConfig() - - config.User = string(user) - config.Passwd = pass - config.Net = "tcp" - config.Addr = fmt.Sprintf("%s:%d", host, port) - config.DBName = "performance_schema" - config.Params = map[string]string{ - "interpolateParams": "true", - "timeout": "10s", - "readTimeout": "10s", - "writeTimeout": "10s", - "tls": "preferred", + err := d.client.Exec(ctx, d.pod, "mysql", cmd, nil, stdout, stderr, false) + if err != nil { + sout := sensitiveRegexp.ReplaceAllString(stdout.String(), ":*****@") + serr := sensitiveRegexp.ReplaceAllString(stderr.String(), ":*****@") + return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) + } + + if strings.Contains(stderr.String(), "ERROR") { + return fmt.Errorf("sql error: %s", stderr) } - db, err := sql.Open("mysql", config.FormatDSN()) + return nil +} + +func (d *dbImplExec) query(ctx context.Context, query string, out interface{}) error { + var errb, outb bytes.Buffer + err := d.exec(ctx, query, &outb, &errb) if err != nil { - return nil, errors.Wrap(err, "connect to MySQL") + return err } - if err := db.PingContext(ctx); err != nil { - return nil, errors.Wrap(err, "ping database") + if !strings.Contains(errb.String(), "ERROR") && outb.Len() == 0 { + return sql.ErrNoRows } - return &dbImpl{db}, nil + r := csv.NewReader(bytes.NewReader(outb.Bytes())) + r.Comma = '\t' + + if err = gocsv.UnmarshalCSV(r, out); err != nil { + return err + } + + return nil } -func (d *dbImpl) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { - // TODO: Make retries configurable - _, err := d.db.ExecContext(ctx, ` - CHANGE REPLICATION SOURCE TO - SOURCE_USER=?, - SOURCE_PASSWORD=?, - SOURCE_HOST=?, - SOURCE_PORT=?, - SOURCE_SSL=1, - SOURCE_CONNECTION_AUTO_FAILOVER=1, - SOURCE_AUTO_POSITION=1, - SOURCE_RETRY_COUNT=3, - SOURCE_CONNECT_RETRY=60 - `, apiv1alpha1.UserReplication, replicaPass, host, port) +func (d *dbImplExec) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { + var errb, outb bytes.Buffer + q := fmt.Sprintf(` + CHANGE REPLICATION SOURCE TO + SOURCE_USER='%s', + SOURCE_PASSWORD='%s', + SOURCE_HOST='%s', + SOURCE_PORT=%d, + SOURCE_SSL=1, + SOURCE_CONNECTION_AUTO_FAILOVER=1, + SOURCE_AUTO_POSITION=1, + SOURCE_RETRY_COUNT=3, + SOURCE_CONNECT_RETRY=60 + `, apiv1alpha1.UserReplication, replicaPass, host, port) + err := d.exec(ctx, q, &outb, &errb) + if err != nil { return errors.Wrap(err, "exec CHANGE REPLICATION SOURCE TO") } @@ -116,99 +145,121 @@ func (d *dbImpl) ChangeReplicationSource(ctx context.Context, host, replicaPass return nil } -func (d *dbImpl) StartReplication(ctx context.Context, host, replicaPass string, port int32) error { +func (d *dbImplExec) StartReplication(ctx context.Context, host, replicaPass string, port int32) error { if err := d.ChangeReplicationSource(ctx, host, replicaPass, port); err != nil { return errors.Wrap(err, "change replication source") } - _, err := d.db.ExecContext(ctx, "START REPLICA") + var errb, outb bytes.Buffer + err := d.exec(ctx, "START REPLICA", &outb, &errb) return errors.Wrap(err, "start replication") } -func (d *dbImpl) StopReplication(ctx context.Context) error { - _, err := d.db.ExecContext(ctx, "STOP REPLICA") +func (d *dbImplExec) StopReplication(ctx context.Context) error { + var errb, outb bytes.Buffer + err := d.exec(ctx, "STOP REPLICA", &outb, &errb) return errors.Wrap(err, "stop replication") } -func (d *dbImpl) ResetReplication(ctx context.Context) error { - _, err := d.db.ExecContext(ctx, "RESET REPLICA ALL") +func (d *dbImplExec) ResetReplication(ctx context.Context) error { + var errb, outb bytes.Buffer + err := d.exec(ctx, "RESET REPLICA ALL", &outb, &errb) return errors.Wrap(err, "reset replication") + } -func (d *dbImpl) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { - row := d.db.QueryRowContext(ctx, ` +func (d *dbImplExec) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { + rows := []*struct { + IoState string `csv:"conn_state"` + SqlState string `csv:"applier_state"` + Host string `csv:"host"` + }{} + + q := fmt.Sprintf(` SELECT - connection_status.SERVICE_STATE, - applier_status.SERVICE_STATE, - HOST + connection_status.SERVICE_STATE as conn_state, + applier_status.SERVICE_STATE as applier_state, + HOST as host FROM replication_connection_status connection_status JOIN replication_connection_configuration connection_configuration ON connection_status.channel_name = connection_configuration.channel_name JOIN replication_applier_status applier_status ON connection_status.channel_name = applier_status.channel_name - WHERE connection_status.channel_name = ? - `, DefaultChannelName) - - var ioState, sqlState, host string - if err := row.Scan(&ioState, &sqlState, &host); err != nil { + WHERE connection_status.channel_name = '%s' + `, DefaultChannelName) + err := d.query(ctx, q, &rows) + if err != nil { if errors.Is(err, sql.ErrNoRows) { return ReplicationStatusNotInitiated, "", nil } return ReplicationStatusError, "", errors.Wrap(err, "scan replication status") } - if ioState == "ON" && sqlState == "ON" { - return ReplicationStatusActive, host, nil + if rows[0].IoState == "ON" && rows[0].SqlState == "ON" { + return ReplicationStatusActive, rows[0].Host, nil } - return ReplicationStatusNotInitiated, "", nil + return ReplicationStatusNotInitiated, "", err } -func (d *dbImpl) IsReplica(ctx context.Context) (bool, error) { +func (d *dbImplExec) IsReplica(ctx context.Context) (bool, error) { status, _, err := d.ReplicationStatus(ctx) return status == ReplicationStatusActive, errors.Wrap(err, "get replication status") } -func (d *dbImpl) EnableSuperReadonly(ctx context.Context) error { - _, err := d.db.ExecContext(ctx, "SET GLOBAL SUPER_READ_ONLY=1") +func (d *dbImplExec) EnableSuperReadonly(ctx context.Context) error { + var errb, outb bytes.Buffer + err := d.exec(ctx, "SET GLOBAL SUPER_READ_ONLY=1", &outb, &errb) return errors.Wrap(err, "set global super_read_only param to 1") } -func (d *dbImpl) DisableSuperReadonly(ctx context.Context) error { - _, err := d.db.ExecContext(ctx, "SET GLOBAL SUPER_READ_ONLY=0") +func (d *dbImplExec) DisableSuperReadonly(ctx context.Context) error { + var errb, outb bytes.Buffer + err := d.exec(ctx, "SET GLOBAL SUPER_READ_ONLY=0", &outb, &errb) return errors.Wrap(err, "set global super_read_only param to 0") } -func (d *dbImpl) IsReadonly(ctx context.Context) (bool, error) { - var readonly int - err := d.db.QueryRowContext(ctx, "select @@read_only and @@super_read_only").Scan(&readonly) - return readonly == 1, errors.Wrap(err, "select global read_only param") +func (d *dbImplExec) IsReadonly(ctx context.Context) (bool, error) { + rows := []*struct { + Readonly int `csv:"readonly"` + }{} + + err := d.query(ctx, "select @@read_only and @@super_read_only as readonly", &rows) + if err != nil { + return false, err + } + + return rows[0].Readonly == 1, nil } -func (d *dbImpl) ReportHost(ctx context.Context) (string, error) { - var reportHost string - err := d.db.QueryRowContext(ctx, "select @@report_host").Scan(&reportHost) - return reportHost, errors.Wrap(err, "select report_host param") +func (d *dbImplExec) ReportHost(ctx context.Context) (string, error) { + rows := []*struct { + Host string `csv:"host"` + }{} + + err := d.query(ctx, "select @@report_host as host", &rows) + if err != nil { + return "", err + } + + return rows[0].Host, nil } -func (d *dbImpl) Close() error { - return d.db.Close() +func (d *dbImplExec) Close() error { + return nil } -func (d *dbImpl) CloneInProgress(ctx context.Context) (bool, error) { - rows, err := d.db.QueryContext(ctx, "SELECT STATE FROM clone_status") +func (d *dbImplExec) CloneInProgress(ctx context.Context) (bool, error) { + rows := []*struct { + State string `csv:"state"` + }{} + err := d.query(ctx, "SELECT STATE FROM clone_status as state", &rows) if err != nil { return false, errors.Wrap(err, "fetch clone status") } - defer rows.Close() - for rows.Next() { - var state string - if err := rows.Scan(&state); err != nil { - return false, errors.Wrap(err, "scan rows") - } - - if state != "Completed" && state != "Failed" { + for _, row := range rows { + if row.State != "Completed" && row.State != "Failed" { return true, nil } } @@ -216,19 +267,18 @@ func (d *dbImpl) CloneInProgress(ctx context.Context) (bool, error) { return false, nil } -func (d *dbImpl) NeedsClone(ctx context.Context, donor string, port int32) (bool, error) { - rows, err := d.db.QueryContext(ctx, "SELECT SOURCE, STATE FROM clone_status") +func (d *dbImplExec) NeedsClone(ctx context.Context, donor string, port int32) (bool, error) { + rows := []*struct { + Source string `csv:"source"` + State string `csv:"state"` + }{} + err := d.query(ctx, "SELECT SOURCE as source, STATE as state FROM clone_status", &rows) if err != nil { return false, errors.Wrap(err, "fetch clone status") } - defer rows.Close() - for rows.Next() { - var source, state string - if err := rows.Scan(&source, &state); err != nil { - return false, errors.Wrap(err, "scan rows") - } - if source == fmt.Sprintf("%s:%d", donor, port) && state == "Completed" { + for _, row := range rows { + if row.Source == fmt.Sprintf("%s:%d", donor, port) && row.State == "Completed" { return false, nil } } @@ -236,46 +286,65 @@ func (d *dbImpl) NeedsClone(ctx context.Context, donor string, port int32) (bool return true, nil } -func (d *dbImpl) Clone(ctx context.Context, donor, user, pass string, port int32) error { - _, err := d.db.ExecContext(ctx, "SET GLOBAL clone_valid_donor_list=?", fmt.Sprintf("%s:%d", donor, port)) +func (d *dbImplExec) Clone(ctx context.Context, donor, user, pass string, port int32) error { + var errb, outb bytes.Buffer + q := fmt.Sprintf("SET GLOBAL clone_valid_donor_list='%s'", fmt.Sprintf("%s:%d", donor, port)) + err := d.exec(ctx, q, &outb, &errb) if err != nil { return errors.Wrap(err, "set clone_valid_donor_list") } - _, err = d.db.ExecContext(ctx, "CLONE INSTANCE FROM ?@?:? IDENTIFIED BY ?", user, donor, port, pass) + q = fmt.Sprintf("CLONE INSTANCE FROM %s@%s:%d IDENTIFIED BY %s", user, donor, port, pass) + err = d.exec(ctx, q, &outb, &errb) - mErr, ok := err.(*mysql.MySQLError) - if !ok { + if strings.Contains(errb.String(), "ERROR") { return errors.Wrap(err, "clone instance") } // Error 3707: Restart server failed (mysqld is not managed by supervisor process). - if mErr.Number == uint16(3707) { + if strings.Contains(errb.String(), "3707") { return ErrRestartAfterClone } return nil } -func (d *dbImpl) DumbQuery(ctx context.Context) error { - _, err := d.db.ExecContext(ctx, "SELECT 1") +func (d *dbImplExec) DumbQuery(ctx context.Context) error { + var errb, outb bytes.Buffer + err := d.exec(ctx, "SELECT 1", &outb, &errb) + return errors.Wrap(err, "SELECT 1") } -func (d *dbImpl) GetGlobal(ctx context.Context, variable string) (interface{}, error) { +func (d *dbImplExec) GetGlobal(ctx context.Context, variable string) (interface{}, error) { + rows := []*struct { + Val interface{} `csv:"val"` + }{} + // TODO: check how to do this without being vulnerable to injection - var value interface{} - err := d.db.QueryRowContext(ctx, fmt.Sprintf("SELECT @@%s", variable)).Scan(&value) - return value, errors.Wrapf(err, "SELECT @@%s", variable) + err := d.query(ctx, fmt.Sprintf("SELECT @@%s as val", variable), &rows) + if err != nil { + return nil, errors.Wrapf(err, "SELECT @@%s", variable) + } + + return rows[0].Val, nil } -func (d *dbImpl) SetGlobal(ctx context.Context, variable, value interface{}) error { - _, err := d.db.ExecContext(ctx, fmt.Sprintf("SET GLOBAL %s=?", variable), value) - return errors.Wrapf(err, "SET GLOBAL %s=%s", variable, value) +func (d *dbImplExec) SetGlobal(ctx context.Context, variable, value interface{}) error { + var errb, outb bytes.Buffer + q := fmt.Sprintf("SET GLOBAL %s=%s", variable, value) + err := d.exec(ctx, q, &outb, &errb) + if err != nil { + return errors.Wrapf(err, "SET GLOBAL %s=%s", variable, value) + + } + return nil } -func (d *dbImpl) StartGroupReplication(ctx context.Context, password string) error { - _, err := d.db.ExecContext(ctx, "START GROUP_REPLICATION USER=?, PASSWORD=?", apiv1alpha1.UserReplication, password) +func (d *dbImplExec) StartGroupReplication(ctx context.Context, password string) error { + var errb, outb bytes.Buffer + q := fmt.Sprintf("START GROUP_REPLICATION USER='%s', PASSWORD='%s'", apiv1alpha1.UserReplication, password) + err := d.exec(ctx, q, &outb, &errb) mErr, ok := err.(*mysql.MySQLError) if !ok { @@ -290,47 +359,50 @@ func (d *dbImpl) StartGroupReplication(ctx context.Context, password string) err return errors.Wrap(err, "start group replication") } -func (d *dbImpl) StopGroupReplication(ctx context.Context) error { - _, err := d.db.ExecContext(ctx, "STOP GROUP_REPLICATION") +func (d *dbImplExec) StopGroupReplication(ctx context.Context) error { + var errb, outb bytes.Buffer + err := d.exec(ctx, "STOP GROUP_REPLICATION", &outb, &errb) return errors.Wrap(err, "stop group replication") } -func (d *dbImpl) GetGroupReplicationPrimary(ctx context.Context) (string, error) { - var host string +func (d *dbImplExec) GetGroupReplicationPrimary(ctx context.Context) (string, error) { + rows := []*struct { + Host string `csv:"host"` + }{} - err := d.db.QueryRowContext(ctx, "SELECT MEMBER_HOST FROM replication_group_members WHERE MEMBER_ROLE='PRIMARY' AND MEMBER_STATE='ONLINE'").Scan(&host) + err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='PRIMARY' AND MEMBER_STATE='ONLINE'", &rows) if err != nil { return "", errors.Wrap(err, "query primary member") } - return host, nil + return rows[0].Host, nil } -func (d *dbImpl) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { - replicas := make([]string, 0) +// TODO: finish implementation +func (d *dbImplExec) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { + rows := []*struct { + Host string `csv:"host"` + }{} - rows, err := d.db.QueryContext(ctx, "SELECT MEMBER_HOST FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'") + err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'", &rows) if err != nil { return nil, errors.Wrap(err, "query replicas") } - defer rows.Close() - for rows.Next() { - var host string - if err := rows.Scan(&host); err != nil { - return nil, errors.Wrap(err, "scan rows") - } - - replicas = append(replicas, host) + replicas := make([]string, 0) + for _, row := range rows { + replicas = append(replicas, row.Host) } return replicas, nil } -func (d *dbImpl) GetMemberState(ctx context.Context, host string) (MemberState, error) { - var state MemberState - - err := d.db.QueryRowContext(ctx, "SELECT MEMBER_STATE FROM replication_group_members WHERE MEMBER_HOST=?", host).Scan(&state) +func (d *dbImplExec) GetMemberState(ctx context.Context, host string) (MemberState, error) { + rows := []*struct { + State MemberState `csv:"state"` + }{} + q := fmt.Sprintf(`SELECT MEMBER_STATE as state FROM replication_group_members WHERE MEMBER_HOST='%s'`, host) + err := d.query(ctx, q, &rows) if err != nil { if errors.Is(err, sql.ErrNoRows) { return MemberStateOffline, nil @@ -338,34 +410,35 @@ func (d *dbImpl) GetMemberState(ctx context.Context, host string) (MemberState, return MemberStateError, errors.Wrap(err, "query member state") } - return state, nil + return rows[0].State, nil } -func (d *dbImpl) GetGroupReplicationMembers(ctx context.Context) ([]string, error) { - members := make([]string, 0) +func (d *dbImplExec) GetGroupReplicationMembers(ctx context.Context) ([]string, error) { + rows := []*struct { + Member string `csv:"member"` + }{} - rows, err := d.db.QueryContext(ctx, "SELECT MEMBER_HOST FROM replication_group_members") + err := d.query(ctx, "SELECT MEMBER_HOST as member FROM replication_group_members", &rows) if err != nil { return nil, errors.Wrap(err, "query members") } - defer rows.Close() - - for rows.Next() { - var host string - if err := rows.Scan(&host); err != nil { - return nil, errors.Wrap(err, "scan rows") - } - members = append(members, host) + members := make([]string, 0) + for _, row := range rows { + members = append(members, row.Member) } return members, nil } -func (d *dbImpl) CheckIfDatabaseExists(ctx context.Context, name string) (bool, error) { - var db string +func (d *dbImplExec) CheckIfDatabaseExists(ctx context.Context, name string) (bool, error) { + rows := []*struct { + DB string `csv:"db"` + }{} + + q := fmt.Sprintf("SELECT SCHEMA_NAME AS db FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE '%s'", name) + err := d.query(ctx, q, &rows) - err := d.db.QueryRowContext(ctx, "SHOW DATABASES LIKE ?", name).Scan(&db) if err != nil { if errors.Is(err, sql.ErrNoRows) { return false, nil @@ -376,10 +449,13 @@ func (d *dbImpl) CheckIfDatabaseExists(ctx context.Context, name string) (bool, return true, nil } -func (d *dbImpl) CheckIfInPrimaryPartition(ctx context.Context) (bool, error) { - var in bool +// TODO: finish implementation +func (d *dbImplExec) CheckIfInPrimaryPartition(ctx context.Context) (bool, error) { + rows := []*struct { + In bool `csv:"in"` + }{} - err := d.db.QueryRowContext(ctx, ` + err := d.query(ctx, ` SELECT MEMBER_STATE = 'ONLINE' AND ( @@ -398,31 +474,32 @@ func (d *dbImpl) CheckIfInPrimaryPartition(ctx context.Context) (bool, error) { performance_schema.replication_group_members ) / 2 ) = 0 - ) + ) as in FROM performance_schema.replication_group_members JOIN performance_schema.replication_group_member_stats USING(member_id) WHERE - member_id = @@global.server_uuid; - `).Scan(&in) + member_id = @@glob, &outb, &errba + `, &rows) + if err != nil { return false, err } - return in, nil + return rows[0].In, nil } -func (d *dbImpl) CheckIfPrimaryUnreachable(ctx context.Context) (bool, error) { +func (d *dbImplExec) CheckIfPrimaryUnreachable(ctx context.Context) (bool, error) { var state string - err := d.db.QueryRowContext(ctx, ` + err := d.query(ctx, ` SELECT MEMBER_STATE FROM performance_schema.replication_group_members WHERE MEMBER_ROLE = 'PRIMARY' - `).Scan(&state) + `, &state) if err != nil { return false, err } diff --git a/pkg/replicator/replicatorexec.go b/pkg/replicator/replicatorexec.go deleted file mode 100644 index 46cb2ee03..000000000 --- a/pkg/replicator/replicatorexec.go +++ /dev/null @@ -1,457 +0,0 @@ -package replicator - -import ( - "bytes" - "context" - "database/sql" - "encoding/csv" - "fmt" - "regexp" - "strings" - - "github.com/go-sql-driver/mysql" - "github.com/gocarina/gocsv" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - - apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" - "github.com/percona/percona-server-mysql-operator/pkg/innodbcluster" -) - -var sensitiveRegexp = regexp.MustCompile(":.*@") - -type dbImplExec struct { - client clientcmd.Client - pod *corev1.Pod - user apiv1alpha1.SystemUser - pass string - host string -} - -func NewReplicatorExec(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Replicator, error) { - return &dbImplExec{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil -} - -func (d *dbImplExec) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { - cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", d.pass), "-u", string(d.user), "-h", d.host, "-e", stm} - - err := d.client.Exec(ctx, d.pod, "mysql", cmd, nil, stdout, stderr, false) - if err != nil { - sout := sensitiveRegexp.ReplaceAllString(stdout.String(), ":*****@") - serr := sensitiveRegexp.ReplaceAllString(stderr.String(), ":*****@") - return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) - } - - if strings.Contains(stderr.String(), "ERROR") { - return fmt.Errorf("sql error: %s", stderr) - } - - return nil -} - -func (d *dbImplExec) query(ctx context.Context, query string, out interface{}) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, query, &outb, &errb) - if err != nil { - return err - } - - if !strings.Contains(errb.String(), "ERROR") && outb.Len() == 0 { - return sql.ErrNoRows - } - - csv := csv.NewReader(bytes.NewReader(outb.Bytes())) - csv.Comma = '\t' - - if err = gocsv.UnmarshalCSV(csv, out); err != nil { - return err - } - - return nil -} - -func (d *dbImplExec) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf(` - CHANGE REPLICATION SOURCE TO - SOURCE_USER='%s', - SOURCE_PASSWORD='%s', - SOURCE_HOST='%s', - SOURCE_PORT=%d, - SOURCE_SSL=1, - SOURCE_CONNECTION_AUTO_FAILOVER=1, - SOURCE_AUTO_POSITION=1, - SOURCE_RETRY_COUNT=3, - SOURCE_CONNECT_RETRY=60 - `, apiv1alpha1.UserReplication, replicaPass, host, port) - err := d.exec(ctx, q, &outb, &errb) - - if err != nil { - return errors.Wrap(err, "exec CHANGE REPLICATION SOURCE TO") - } - - return nil -} - -func (d *dbImplExec) StartReplication(ctx context.Context, host, replicaPass string, port int32) error { - if err := d.ChangeReplicationSource(ctx, host, replicaPass, port); err != nil { - return errors.Wrap(err, "change replication source") - } - - var errb, outb bytes.Buffer - err := d.exec(ctx, "START REPLICA", &outb, &errb) - return errors.Wrap(err, "start replication") -} - -func (d *dbImplExec) StopReplication(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "STOP REPLICA", &outb, &errb) - return errors.Wrap(err, "stop replication") -} - -func (d *dbImplExec) ResetReplication(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "RESET REPLICA ALL", &outb, &errb) - return errors.Wrap(err, "reset replication") - -} - -func (d *dbImplExec) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { - rows := []*struct { - IoState string `csv:"conn_state"` - SqlState string `csv:"applier_state"` - Host string `csv:"host"` - }{} - - q := fmt.Sprintf(` - SELECT - connection_status.SERVICE_STATE as conn_state, - applier_status.SERVICE_STATE as applier_state, - HOST as host - FROM replication_connection_status connection_status - JOIN replication_connection_configuration connection_configuration - ON connection_status.channel_name = connection_configuration.channel_name - JOIN replication_applier_status applier_status - ON connection_status.channel_name = applier_status.channel_name - WHERE connection_status.channel_name = '%s' - `, DefaultChannelName) - err := d.query(ctx, q, &rows) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return ReplicationStatusNotInitiated, "", nil - } - return ReplicationStatusError, "", errors.Wrap(err, "scan replication status") - } - - if rows[0].IoState == "ON" && rows[0].SqlState == "ON" { - return ReplicationStatusActive, rows[0].Host, nil - } - - return ReplicationStatusNotInitiated, "", err -} - -func (d *dbImplExec) IsReplica(ctx context.Context) (bool, error) { - status, _, err := d.ReplicationStatus(ctx) - return status == ReplicationStatusActive, errors.Wrap(err, "get replication status") -} - -func (d *dbImplExec) EnableSuperReadonly(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "SET GLOBAL SUPER_READ_ONLY=1", &outb, &errb) - return errors.Wrap(err, "set global super_read_only param to 1") -} - -func (d *dbImplExec) DisableSuperReadonly(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "SET GLOBAL SUPER_READ_ONLY=0", &outb, &errb) - return errors.Wrap(err, "set global super_read_only param to 0") -} - -func (d *dbImplExec) IsReadonly(ctx context.Context) (bool, error) { - rows := []*struct { - Readonly int `csv:"readonly"` - }{} - - err := d.query(ctx, "select @@read_only and @@super_read_only as readonly", &rows) - if err != nil { - return false, err - } - - return rows[0].Readonly == 1, nil -} - -func (d *dbImplExec) ReportHost(ctx context.Context) (string, error) { - rows := []*struct { - Host string `csv:"host"` - }{} - - err := d.query(ctx, "select @@report_host as host", &rows) - if err != nil { - return "", err - } - - return rows[0].Host, nil -} - -func (d *dbImplExec) Close() error { - return nil -} - -func (d *dbImplExec) CloneInProgress(ctx context.Context) (bool, error) { - rows := []*struct { - State string `csv:"state"` - }{} - err := d.query(ctx, "SELECT STATE FROM clone_status as state", &rows) - if err != nil { - return false, errors.Wrap(err, "fetch clone status") - } - - for _, row := range rows { - if row.State != "Completed" && row.State != "Failed" { - return true, nil - } - } - - return false, nil -} - -func (d *dbImplExec) NeedsClone(ctx context.Context, donor string, port int32) (bool, error) { - rows := []*struct { - Source string `csv:"source"` - State string `csv:"state"` - }{} - err := d.query(ctx, "SELECT SOURCE as source, STATE as state FROM clone_status", &rows) - if err != nil { - return false, errors.Wrap(err, "fetch clone status") - } - - for _, row := range rows { - if row.Source == fmt.Sprintf("%s:%d", donor, port) && row.State == "Completed" { - return false, nil - } - } - - return true, nil -} - -func (d *dbImplExec) Clone(ctx context.Context, donor, user, pass string, port int32) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf("SET GLOBAL clone_valid_donor_list='%s'", fmt.Sprintf("%s:%d", donor, port)) - err := d.exec(ctx, q, &outb, &errb) - if err != nil { - return errors.Wrap(err, "set clone_valid_donor_list") - } - - q = fmt.Sprintf("CLONE INSTANCE FROM %s@%s:%d IDENTIFIED BY %s", user, donor, port, pass) - err = d.exec(ctx, q, &outb, &errb) - - if strings.Contains(errb.String(), "ERROR") { - return errors.Wrap(err, "clone instance") - } - - // Error 3707: Restart server failed (mysqld is not managed by supervisor process). - if strings.Contains(errb.String(), "3707") { - return ErrRestartAfterClone - } - - return nil -} - -func (d *dbImplExec) DumbQuery(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "SELECT 1", &outb, &errb) - - return errors.Wrap(err, "SELECT 1") -} - -func (d *dbImplExec) GetGlobal(ctx context.Context, variable string) (interface{}, error) { - rows := []*struct { - Val interface{} `csv:"val"` - }{} - - // TODO: check how to do this without being vulnerable to injection - err := d.query(ctx, fmt.Sprintf("SELECT @@%s as val", variable), &rows) - if err != nil { - return nil, errors.Wrapf(err, "SELECT @@%s", variable) - } - - return rows[0].Val, nil -} - -func (d *dbImplExec) SetGlobal(ctx context.Context, variable, value interface{}) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf("SET GLOBAL %s=%s", variable, value) - err := d.exec(ctx, q, &outb, &errb) - if err != nil { - return errors.Wrapf(err, "SET GLOBAL %s=%s", variable, value) - - } - return nil -} - -func (d *dbImplExec) StartGroupReplication(ctx context.Context, password string) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf("START GROUP_REPLICATION USER='%s', PASSWORD='%s'", apiv1alpha1.UserReplication, password) - err := d.exec(ctx, q, &outb, &errb) - - mErr, ok := err.(*mysql.MySQLError) - if !ok { - return errors.Wrap(err, "start group replication") - } - - // Error 3092: The server is not configured properly to be an active member of the group. - if mErr.Number == uint16(3092) { - return ErrGroupReplicationNotReady - } - - return errors.Wrap(err, "start group replication") -} - -func (d *dbImplExec) StopGroupReplication(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "STOP GROUP_REPLICATION", &outb, &errb) - return errors.Wrap(err, "stop group replication") -} - -func (d *dbImplExec) GetGroupReplicationPrimary(ctx context.Context) (string, error) { - rows := []*struct { - Host string `csv:"host"` - }{} - - err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='PRIMARY' AND MEMBER_STATE='ONLINE'", &rows) - if err != nil { - return "", errors.Wrap(err, "query primary member") - } - - return rows[0].Host, nil -} - -// TODO: finish implementation -func (d *dbImplExec) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { - rows := []*struct { - Host string `csv:"host"` - }{} - - err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'", &rows) - if err != nil { - return nil, errors.Wrap(err, "query replicas") - } - - replicas := make([]string, 0) - for _, row := range rows { - replicas = append(replicas, row.Host) - } - - return replicas, nil -} - -func (d *dbImplExec) GetMemberState(ctx context.Context, host string) (MemberState, error) { - rows := []*struct { - State MemberState `csv:"state"` - }{} - q := fmt.Sprintf(`SELECT MEMBER_STATE as state FROM replication_group_members WHERE MEMBER_HOST='%s'`, host) - err := d.query(ctx, q, &rows) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return MemberStateOffline, nil - } - return MemberStateError, errors.Wrap(err, "query member state") - } - - return rows[0].State, nil -} - -func (d *dbImplExec) GetGroupReplicationMembers(ctx context.Context) ([]string, error) { - rows := []*struct { - Member string `csv:"member"` - }{} - - err := d.query(ctx, "SELECT MEMBER_HOST as member FROM replication_group_members", &rows) - if err != nil { - return nil, errors.Wrap(err, "query members") - } - - members := make([]string, 0) - for _, row := range rows { - members = append(members, row.Member) - } - - return members, nil -} - -func (d *dbImplExec) CheckIfDatabaseExists(ctx context.Context, name string) (bool, error) { - rows := []*struct { - DB string `csv:"db"` - }{} - - q := fmt.Sprintf("SELECT SCHEMA_NAME AS db FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE '%s'", name) - err := d.query(ctx, q, &rows) - - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return false, nil - } - return false, err - } - - return true, nil -} - -// TODO: finish implementation -func (d *dbImplExec) CheckIfInPrimaryPartition(ctx context.Context) (bool, error) { - rows := []*struct { - In bool `csv:"in"` - }{} - - err := d.query(ctx, ` - SELECT - MEMBER_STATE = 'ONLINE' - AND ( - ( - SELECT - COUNT(*) - FROM - performance_schema.replication_group_members - WHERE - MEMBER_STATE NOT IN ('ONLINE', 'RECOVERING') - ) >= ( - ( - SELECT - COUNT(*) - FROM - performance_schema.replication_group_members - ) / 2 - ) = 0 - ) as in - FROM - performance_schema.replication_group_members - JOIN performance_schema.replication_group_member_stats USING(member_id) - WHERE - member_id = @@glob, &outb, &errba - `, &rows) - - if err != nil { - return false, err - } - - return rows[0].In, nil -} - -func (d *dbImplExec) CheckIfPrimaryUnreachable(ctx context.Context) (bool, error) { - var state string - - err := d.query(ctx, ` - SELECT - MEMBER_STATE - FROM - performance_schema.replication_group_members - WHERE - MEMBER_ROLE = 'PRIMARY' - `, &state) - if err != nil { - return false, err - } - - return state == string(innodbcluster.MemberStateUnreachable), nil -} From cdfaef5f08ea54f9329a324fa149fc848515fdee Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Mon, 8 Jan 2024 09:55:55 +0100 Subject: [PATCH 03/12] Remove unnecessary methods from Replicator interface. --- cmd/mysql/mysql.go | 8 +- pkg/controller/ps/controller.go | 10 +- pkg/controller/psbackup/controller.go | 2 +- pkg/replicator/replicator.go | 331 +------------------------- 4 files changed, 24 insertions(+), 327 deletions(-) diff --git a/cmd/mysql/mysql.go b/cmd/mysql/mysql.go index 68b73db43..7767dfa83 100644 --- a/cmd/mysql/mysql.go +++ b/cmd/mysql/mysql.go @@ -12,11 +12,15 @@ import ( "github.com/percona/percona-server-mysql-operator/pkg/replicator" ) +const defaultChannelName = "" + var ErrRestartAfterClone = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") type ReplicationStatus int8 -type DB struct{ db *sql.DB } +type DB struct { + db *sql.DB +} func NewDatabase(ctx context.Context, user apiv1alpha1.SystemUser, pass, host string, port int32) (*DB, error) { config := mysql.NewConfig() @@ -90,7 +94,7 @@ func (d *DB) ReplicationStatus(ctx context.Context) (replicator.ReplicationStatu JOIN replication_applier_status applier_status ON connection_status.channel_name = applier_status.channel_name WHERE connection_status.channel_name = ? - `, replicator.DefaultChannelName) + `, defaultChannelName) var ioState, sqlState, host string if err := row.Scan(&ioState, &sqlState, &host); err != nil { diff --git a/pkg/controller/ps/controller.go b/pkg/controller/ps/controller.go index 1305ab333..df07da215 100644 --- a/pkg/controller/ps/controller.go +++ b/pkg/controller/ps/controller.go @@ -204,11 +204,10 @@ func (r *PerconaServerMySQLReconciler) deleteMySQLPods(ctx context.Context, cr * firstPodFQDN := fmt.Sprintf("%s.%s.%s", firstPod.Name, mysql.ServiceName(cr), cr.Namespace) firstPodUri := fmt.Sprintf("%s:%s@%s", apiv1alpha1.UserOperator, operatorPass, firstPodFQDN) - db, err := replicator.NewReplicatorExec(&firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, firstPodFQDN) + db, err := replicator.NewReplicator(&firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, firstPodFQDN) if err != nil { return errors.Wrapf(err, "connect to %s", firstPod.Name) } - defer db.Close() mysh, err := mysqlsh.NewWithExec(r.ClientCmd, &firstPod, firstPodUri) if err != nil { @@ -1009,7 +1008,7 @@ func (r *PerconaServerMySQLReconciler) getPrimaryFromGR(ctx context.Context, cr return "", err } - db, err := replicator.NewReplicatorExec(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) + db, err := replicator.NewReplicator(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) if err != nil { return "", errors.Wrapf(err, "open connection to %s", fqdn) } @@ -1060,7 +1059,7 @@ func (r *PerconaServerMySQLReconciler) stopAsyncReplication(ctx context.Context, if err != nil { return err } - repDb, err := replicator.NewReplicatorExec(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) + repDb, err := replicator.NewReplicator(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) if err != nil { return errors.Wrapf(err, "connect to replica %s", hostname) } @@ -1117,11 +1116,10 @@ func (r *PerconaServerMySQLReconciler) startAsyncReplication(ctx context.Context if err != nil { return err } - db, err := replicator.NewReplicatorExec(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) + db, err := replicator.NewReplicator(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) if err != nil { return errors.Wrapf(err, "get db connection to %s", hostname) } - defer db.Close() log.V(1).Info("Change replication source", "primary", primary.Key.Hostname, "replica", hostname) if err := db.ChangeReplicationSource(ctx, primary.Key.Hostname, replicaPass, primary.Key.Port); err != nil { diff --git a/pkg/controller/psbackup/controller.go b/pkg/controller/psbackup/controller.go index ec0813bf9..e70b440d8 100644 --- a/pkg/controller/psbackup/controller.go +++ b/pkg/controller/psbackup/controller.go @@ -352,7 +352,7 @@ func (r *PerconaServerMySQLBackupReconciler) getTopology(ctx context.Context, cl fqdn := mysql.FQDN(cluster, 0) - db, err := replicator.NewReplicatorExec(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) + db, err := replicator.NewReplicator(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) if err != nil { return Topology{}, errors.Wrapf(err, "open connection to %s", fqdn) } diff --git a/pkg/replicator/replicator.go b/pkg/replicator/replicator.go index 3a6046826..c1649267d 100644 --- a/pkg/replicator/replicator.go +++ b/pkg/replicator/replicator.go @@ -9,14 +9,12 @@ import ( "regexp" "strings" - "github.com/go-sql-driver/mysql" "github.com/gocarina/gocsv" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" - "github.com/percona/percona-server-mysql-operator/pkg/innodbcluster" ) var sensitiveRegexp = regexp.MustCompile(":.*@") @@ -37,42 +35,19 @@ const ( type MemberState string const ( - MemberStateOnline MemberState = "ONLINE" - MemberStateRecovering MemberState = "RECOVERING" - MemberStateOffline MemberState = "OFFLINE" - MemberStateError MemberState = "ERROR" - MemberStateUnreachable MemberState = "UNREACHABLE" + MemberStateOnline MemberState = "ONLINE" + MemberStateOffline MemberState = "OFFLINE" + MemberStateError MemberState = "ERROR" ) type Replicator interface { ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error - StartReplication(ctx context.Context, host, replicaPass string, port int32) error - StopReplication(ctx context.Context) error - ResetReplication(ctx context.Context) error ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) - EnableSuperReadonly(ctx context.Context) error - DisableSuperReadonly(ctx context.Context) error - IsReadonly(ctx context.Context) (bool, error) - ReportHost(ctx context.Context) (string, error) - Close() error - CloneInProgress(ctx context.Context) (bool, error) - NeedsClone(ctx context.Context, donor string, port int32) (bool, error) - Clone(ctx context.Context, donor, user, pass string, port int32) error - IsReplica(ctx context.Context) (bool, error) - DumbQuery(ctx context.Context) error - GetGlobal(ctx context.Context, variable string) (interface{}, error) - SetGlobal(ctx context.Context, variable, value interface{}) error - StartGroupReplication(ctx context.Context, password string) error - StopGroupReplication(ctx context.Context) error GetGroupReplicationPrimary(ctx context.Context) (string, error) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) GetMemberState(ctx context.Context, host string) (MemberState, error) - GetGroupReplicationMembers(ctx context.Context) ([]string, error) - CheckIfDatabaseExists(ctx context.Context, name string) (bool, error) - CheckIfInPrimaryPartition(ctx context.Context) (bool, error) - CheckIfPrimaryUnreachable(ctx context.Context) (bool, error) } -type dbImplExec struct { +type db struct { client clientcmd.Client pod *corev1.Pod user apiv1alpha1.SystemUser @@ -80,11 +55,11 @@ type dbImplExec struct { host string } -func NewReplicatorExec(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Replicator, error) { - return &dbImplExec{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil +func NewReplicator(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Replicator, error) { + return &db{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil } -func (d *dbImplExec) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { +func (d *db) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", d.pass), "-u", string(d.user), "-h", d.host, "-e", stm} err := d.client.Exec(ctx, d.pod, "mysql", cmd, nil, stdout, stderr, false) @@ -101,7 +76,7 @@ func (d *dbImplExec) exec(ctx context.Context, stm string, stdout, stderr *bytes return nil } -func (d *dbImplExec) query(ctx context.Context, query string, out interface{}) error { +func (d *db) query(ctx context.Context, query string, out interface{}) error { var errb, outb bytes.Buffer err := d.exec(ctx, query, &outb, &errb) if err != nil { @@ -122,7 +97,7 @@ func (d *dbImplExec) query(ctx context.Context, query string, out interface{}) e return nil } -func (d *dbImplExec) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { +func (d *db) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { var errb, outb bytes.Buffer q := fmt.Sprintf(` CHANGE REPLICATION SOURCE TO @@ -145,30 +120,7 @@ func (d *dbImplExec) ChangeReplicationSource(ctx context.Context, host, replicaP return nil } -func (d *dbImplExec) StartReplication(ctx context.Context, host, replicaPass string, port int32) error { - if err := d.ChangeReplicationSource(ctx, host, replicaPass, port); err != nil { - return errors.Wrap(err, "change replication source") - } - - var errb, outb bytes.Buffer - err := d.exec(ctx, "START REPLICA", &outb, &errb) - return errors.Wrap(err, "start replication") -} - -func (d *dbImplExec) StopReplication(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "STOP REPLICA", &outb, &errb) - return errors.Wrap(err, "stop replication") -} - -func (d *dbImplExec) ResetReplication(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "RESET REPLICA ALL", &outb, &errb) - return errors.Wrap(err, "reset replication") - -} - -func (d *dbImplExec) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { +func (d *db) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { rows := []*struct { IoState string `csv:"conn_state"` SqlState string `csv:"applier_state"` @@ -202,170 +154,7 @@ func (d *dbImplExec) ReplicationStatus(ctx context.Context) (ReplicationStatus, return ReplicationStatusNotInitiated, "", err } -func (d *dbImplExec) IsReplica(ctx context.Context) (bool, error) { - status, _, err := d.ReplicationStatus(ctx) - return status == ReplicationStatusActive, errors.Wrap(err, "get replication status") -} - -func (d *dbImplExec) EnableSuperReadonly(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "SET GLOBAL SUPER_READ_ONLY=1", &outb, &errb) - return errors.Wrap(err, "set global super_read_only param to 1") -} - -func (d *dbImplExec) DisableSuperReadonly(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "SET GLOBAL SUPER_READ_ONLY=0", &outb, &errb) - return errors.Wrap(err, "set global super_read_only param to 0") -} - -func (d *dbImplExec) IsReadonly(ctx context.Context) (bool, error) { - rows := []*struct { - Readonly int `csv:"readonly"` - }{} - - err := d.query(ctx, "select @@read_only and @@super_read_only as readonly", &rows) - if err != nil { - return false, err - } - - return rows[0].Readonly == 1, nil -} - -func (d *dbImplExec) ReportHost(ctx context.Context) (string, error) { - rows := []*struct { - Host string `csv:"host"` - }{} - - err := d.query(ctx, "select @@report_host as host", &rows) - if err != nil { - return "", err - } - - return rows[0].Host, nil -} - -func (d *dbImplExec) Close() error { - return nil -} - -func (d *dbImplExec) CloneInProgress(ctx context.Context) (bool, error) { - rows := []*struct { - State string `csv:"state"` - }{} - err := d.query(ctx, "SELECT STATE FROM clone_status as state", &rows) - if err != nil { - return false, errors.Wrap(err, "fetch clone status") - } - - for _, row := range rows { - if row.State != "Completed" && row.State != "Failed" { - return true, nil - } - } - - return false, nil -} - -func (d *dbImplExec) NeedsClone(ctx context.Context, donor string, port int32) (bool, error) { - rows := []*struct { - Source string `csv:"source"` - State string `csv:"state"` - }{} - err := d.query(ctx, "SELECT SOURCE as source, STATE as state FROM clone_status", &rows) - if err != nil { - return false, errors.Wrap(err, "fetch clone status") - } - - for _, row := range rows { - if row.Source == fmt.Sprintf("%s:%d", donor, port) && row.State == "Completed" { - return false, nil - } - } - - return true, nil -} - -func (d *dbImplExec) Clone(ctx context.Context, donor, user, pass string, port int32) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf("SET GLOBAL clone_valid_donor_list='%s'", fmt.Sprintf("%s:%d", donor, port)) - err := d.exec(ctx, q, &outb, &errb) - if err != nil { - return errors.Wrap(err, "set clone_valid_donor_list") - } - - q = fmt.Sprintf("CLONE INSTANCE FROM %s@%s:%d IDENTIFIED BY %s", user, donor, port, pass) - err = d.exec(ctx, q, &outb, &errb) - - if strings.Contains(errb.String(), "ERROR") { - return errors.Wrap(err, "clone instance") - } - - // Error 3707: Restart server failed (mysqld is not managed by supervisor process). - if strings.Contains(errb.String(), "3707") { - return ErrRestartAfterClone - } - - return nil -} - -func (d *dbImplExec) DumbQuery(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "SELECT 1", &outb, &errb) - - return errors.Wrap(err, "SELECT 1") -} - -func (d *dbImplExec) GetGlobal(ctx context.Context, variable string) (interface{}, error) { - rows := []*struct { - Val interface{} `csv:"val"` - }{} - - // TODO: check how to do this without being vulnerable to injection - err := d.query(ctx, fmt.Sprintf("SELECT @@%s as val", variable), &rows) - if err != nil { - return nil, errors.Wrapf(err, "SELECT @@%s", variable) - } - - return rows[0].Val, nil -} - -func (d *dbImplExec) SetGlobal(ctx context.Context, variable, value interface{}) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf("SET GLOBAL %s=%s", variable, value) - err := d.exec(ctx, q, &outb, &errb) - if err != nil { - return errors.Wrapf(err, "SET GLOBAL %s=%s", variable, value) - - } - return nil -} - -func (d *dbImplExec) StartGroupReplication(ctx context.Context, password string) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf("START GROUP_REPLICATION USER='%s', PASSWORD='%s'", apiv1alpha1.UserReplication, password) - err := d.exec(ctx, q, &outb, &errb) - - mErr, ok := err.(*mysql.MySQLError) - if !ok { - return errors.Wrap(err, "start group replication") - } - - // Error 3092: The server is not configured properly to be an active member of the group. - if mErr.Number == uint16(3092) { - return ErrGroupReplicationNotReady - } - - return errors.Wrap(err, "start group replication") -} - -func (d *dbImplExec) StopGroupReplication(ctx context.Context) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, "STOP GROUP_REPLICATION", &outb, &errb) - return errors.Wrap(err, "stop group replication") -} - -func (d *dbImplExec) GetGroupReplicationPrimary(ctx context.Context) (string, error) { +func (d *db) GetGroupReplicationPrimary(ctx context.Context) (string, error) { rows := []*struct { Host string `csv:"host"` }{} @@ -379,7 +168,7 @@ func (d *dbImplExec) GetGroupReplicationPrimary(ctx context.Context) (string, er } // TODO: finish implementation -func (d *dbImplExec) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { +func (d *db) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { rows := []*struct { Host string `csv:"host"` }{} @@ -397,7 +186,7 @@ func (d *dbImplExec) GetGroupReplicationReplicas(ctx context.Context) ([]string, return replicas, nil } -func (d *dbImplExec) GetMemberState(ctx context.Context, host string) (MemberState, error) { +func (d *db) GetMemberState(ctx context.Context, host string) (MemberState, error) { rows := []*struct { State MemberState `csv:"state"` }{} @@ -412,97 +201,3 @@ func (d *dbImplExec) GetMemberState(ctx context.Context, host string) (MemberSta return rows[0].State, nil } - -func (d *dbImplExec) GetGroupReplicationMembers(ctx context.Context) ([]string, error) { - rows := []*struct { - Member string `csv:"member"` - }{} - - err := d.query(ctx, "SELECT MEMBER_HOST as member FROM replication_group_members", &rows) - if err != nil { - return nil, errors.Wrap(err, "query members") - } - - members := make([]string, 0) - for _, row := range rows { - members = append(members, row.Member) - } - - return members, nil -} - -func (d *dbImplExec) CheckIfDatabaseExists(ctx context.Context, name string) (bool, error) { - rows := []*struct { - DB string `csv:"db"` - }{} - - q := fmt.Sprintf("SELECT SCHEMA_NAME AS db FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE '%s'", name) - err := d.query(ctx, q, &rows) - - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return false, nil - } - return false, err - } - - return true, nil -} - -// TODO: finish implementation -func (d *dbImplExec) CheckIfInPrimaryPartition(ctx context.Context) (bool, error) { - rows := []*struct { - In bool `csv:"in"` - }{} - - err := d.query(ctx, ` - SELECT - MEMBER_STATE = 'ONLINE' - AND ( - ( - SELECT - COUNT(*) - FROM - performance_schema.replication_group_members - WHERE - MEMBER_STATE NOT IN ('ONLINE', 'RECOVERING') - ) >= ( - ( - SELECT - COUNT(*) - FROM - performance_schema.replication_group_members - ) / 2 - ) = 0 - ) as in - FROM - performance_schema.replication_group_members - JOIN performance_schema.replication_group_member_stats USING(member_id) - WHERE - member_id = @@glob, &outb, &errba - `, &rows) - - if err != nil { - return false, err - } - - return rows[0].In, nil -} - -func (d *dbImplExec) CheckIfPrimaryUnreachable(ctx context.Context) (bool, error) { - var state string - - err := d.query(ctx, ` - SELECT - MEMBER_STATE - FROM - performance_schema.replication_group_members - WHERE - MEMBER_ROLE = 'PRIMARY' - `, &state) - if err != nil { - return false, err - } - - return state == string(innodbcluster.MemberStateUnreachable), nil -} From 7b250a15a9150c1aa64095b62f5526058758ae1c Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Mon, 8 Jan 2024 09:59:14 +0100 Subject: [PATCH 04/12] Minor cleanup. --- pkg/controller/ps/controller.go | 6 +++--- pkg/replicator/replicator.go | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/controller/ps/controller.go b/pkg/controller/ps/controller.go index df07da215..eac345b1f 100644 --- a/pkg/controller/ps/controller.go +++ b/pkg/controller/ps/controller.go @@ -537,15 +537,15 @@ func (r *PerconaServerMySQLReconciler) reconcileOrchestrator(ctx context.Context return nil } - cm := &corev1.ConfigMap{} - err := r.Client.Get(ctx, orchestrator.NamespacedName(cr), cm) + cmap := &corev1.ConfigMap{} + err := r.Client.Get(ctx, orchestrator.NamespacedName(cr), cmap) if client.IgnoreNotFound(err) != nil { return errors.Wrap(err, "get config map") } existingNodes := make([]string, 0) if !k8serrors.IsNotFound(err) { - cfg, ok := cm.Data[orchestrator.ConfigFileName] + cfg, ok := cmap.Data[orchestrator.ConfigFileName] if !ok { return errors.Errorf("key %s not found in ConfigMap", orchestrator.ConfigFileName) } diff --git a/pkg/replicator/replicator.go b/pkg/replicator/replicator.go index c1649267d..6efe71b2a 100644 --- a/pkg/replicator/replicator.go +++ b/pkg/replicator/replicator.go @@ -24,7 +24,6 @@ const DefaultChannelName = "" type ReplicationStatus int8 var ErrRestartAfterClone = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") -var ErrGroupReplicationNotReady = errors.New("Error 3092: The server is not configured properly to be an active member of the group.") const ( ReplicationStatusActive ReplicationStatus = iota From 2d8b05e147d2bec88b4fe19090358d3668bdcd2e Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Mon, 8 Jan 2024 11:48:29 +0100 Subject: [PATCH 05/12] Refactor UserManager and Replication interfaces. --- pkg/controller/ps/controller.go | 32 +-- pkg/controller/ps/user.go | 18 +- pkg/controller/psbackup/controller.go | 11 +- pkg/db/db.go | 46 +++ pkg/db/replication.go | 169 +++++++++++ pkg/db/users.go | 73 +++++ pkg/platform/platform.go | 2 +- pkg/replicator/replicator.go | 400 +++++++++++++------------- pkg/users/users.go | 132 --------- pkg/users/usersexec.go | 204 ++++++------- 10 files changed, 613 insertions(+), 474 deletions(-) create mode 100644 pkg/db/db.go create mode 100644 pkg/db/replication.go create mode 100644 pkg/db/users.go delete mode 100644 pkg/users/users.go diff --git a/pkg/controller/ps/controller.go b/pkg/controller/ps/controller.go index eac345b1f..cfcd3b688 100644 --- a/pkg/controller/ps/controller.go +++ b/pkg/controller/ps/controller.go @@ -45,13 +45,13 @@ import ( apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" "github.com/percona/percona-server-mysql-operator/pkg/controller/psrestore" + "github.com/percona/percona-server-mysql-operator/pkg/db" "github.com/percona/percona-server-mysql-operator/pkg/haproxy" "github.com/percona/percona-server-mysql-operator/pkg/k8s" "github.com/percona/percona-server-mysql-operator/pkg/mysql" "github.com/percona/percona-server-mysql-operator/pkg/mysqlsh" "github.com/percona/percona-server-mysql-operator/pkg/orchestrator" "github.com/percona/percona-server-mysql-operator/pkg/platform" - "github.com/percona/percona-server-mysql-operator/pkg/replicator" "github.com/percona/percona-server-mysql-operator/pkg/router" "github.com/percona/percona-server-mysql-operator/pkg/util" ) @@ -204,10 +204,7 @@ func (r *PerconaServerMySQLReconciler) deleteMySQLPods(ctx context.Context, cr * firstPodFQDN := fmt.Sprintf("%s.%s.%s", firstPod.Name, mysql.ServiceName(cr), cr.Namespace) firstPodUri := fmt.Sprintf("%s:%s@%s", apiv1alpha1.UserOperator, operatorPass, firstPodFQDN) - db, err := replicator.NewReplicator(&firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, firstPodFQDN) - if err != nil { - return errors.Wrapf(err, "connect to %s", firstPod.Name) - } + um := db.NewReplicationManager(&firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, firstPodFQDN) mysh, err := mysqlsh.NewWithExec(r.ClientCmd, &firstPod, firstPodUri) if err != nil { @@ -222,12 +219,12 @@ func (r *PerconaServerMySQLReconciler) deleteMySQLPods(ctx context.Context, cr * podFQDN := fmt.Sprintf("%s.%s.%s", pod.Name, mysql.ServiceName(cr), cr.Namespace) - state, err := db.GetMemberState(ctx, podFQDN) + state, err := um.GetMemberState(ctx, podFQDN) if err != nil { return errors.Wrapf(err, "get member state of %s from performance_schema", pod.Name) } - if state == replicator.MemberStateOffline { + if state == db.MemberStateOffline { log.Info("Member is not part of GR or already removed", "member", pod.Name, "memberState", state) continue } @@ -1008,12 +1005,9 @@ func (r *PerconaServerMySQLReconciler) getPrimaryFromGR(ctx context.Context, cr return "", err } - db, err := replicator.NewReplicator(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) - if err != nil { - return "", errors.Wrapf(err, "open connection to %s", fqdn) - } + um := db.NewReplicationManager(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) - return db.GetGroupReplicationPrimary(ctx) + return um.GetGroupReplicationPrimary(ctx) } func (r *PerconaServerMySQLReconciler) getPrimaryHost(ctx context.Context, cr *apiv1alpha1.PerconaServerMySQL) (string, error) { @@ -1059,10 +1053,7 @@ func (r *PerconaServerMySQLReconciler) stopAsyncReplication(ctx context.Context, if err != nil { return err } - repDb, err := replicator.NewReplicator(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) - if err != nil { - return errors.Wrapf(err, "connect to replica %s", hostname) - } + repDb := db.NewReplicationManager(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) if err := orchestrator.StopReplicationExec(gCtx, r.ClientCmd, orcPod, hostname, port); err != nil { return errors.Wrapf(err, "stop replica %s", hostname) @@ -1073,7 +1064,7 @@ func (r *PerconaServerMySQLReconciler) stopAsyncReplication(ctx context.Context, return errors.Wrapf(err, "get replication status of %s", hostname) } - for status == replicator.ReplicationStatusActive { + for status == db.ReplicationStatusActive { time.Sleep(250 * time.Millisecond) status, _, err = repDb.ReplicationStatus(ctx) if err != nil { @@ -1116,13 +1107,10 @@ func (r *PerconaServerMySQLReconciler) startAsyncReplication(ctx context.Context if err != nil { return err } - db, err := replicator.NewReplicator(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) - if err != nil { - return errors.Wrapf(err, "get db connection to %s", hostname) - } + um := db.NewReplicationManager(pod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, hostname) log.V(1).Info("Change replication source", "primary", primary.Key.Hostname, "replica", hostname) - if err := db.ChangeReplicationSource(ctx, primary.Key.Hostname, replicaPass, primary.Key.Port); err != nil { + if err := um.ChangeReplicationSource(ctx, primary.Key.Hostname, replicaPass, primary.Key.Port); err != nil { return errors.Wrapf(err, "change replication source on %s", hostname) } diff --git a/pkg/controller/ps/user.go b/pkg/controller/ps/user.go index ccfff446c..ab25349d7 100644 --- a/pkg/controller/ps/user.go +++ b/pkg/controller/ps/user.go @@ -17,13 +17,13 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/db" "github.com/percona/percona-server-mysql-operator/pkg/haproxy" "github.com/percona/percona-server-mysql-operator/pkg/k8s" "github.com/percona/percona-server-mysql-operator/pkg/mysql" "github.com/percona/percona-server-mysql-operator/pkg/orchestrator" "github.com/percona/percona-server-mysql-operator/pkg/router" "github.com/percona/percona-server-mysql-operator/pkg/secret" - "github.com/percona/percona-server-mysql-operator/pkg/users" ) const ( @@ -216,11 +216,7 @@ func (r *PerconaServerMySQLReconciler) reconcileUsers(ctx context.Context, cr *a return err } - um, err := users.NewManagerExec(primPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, primaryHost) - if err != nil { - return errors.Wrap(err, "init user manager") - } - defer um.Close() + um := db.NewUserManager(primPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, primaryHost) var asyncPrimary *orchestrator.Instance @@ -236,7 +232,7 @@ func (r *PerconaServerMySQLReconciler) reconcileUsers(ctx context.Context, cr *a } } - if err := um.UpdateUserPasswords(updatedUsers); err != nil { + if err := um.UpdateUserPasswords(ctx, updatedUsers); err != nil { return errors.Wrapf(err, "update passwords") } @@ -346,13 +342,9 @@ func (r *PerconaServerMySQLReconciler) discardOldPasswordsAfterNewPropagated( return err } - um, err := users.NewManagerExec(primPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, primaryHost) - if err != nil { - return errors.Wrap(err, "init user manager") - } - defer um.Close() + um := db.NewUserManager(primPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, primaryHost) - if err := um.DiscardOldPasswords(updatedUsers); err != nil { + if err := um.DiscardOldPasswords(ctx, updatedUsers); err != nil { return errors.Wrap(err, "discard old passwords") } diff --git a/pkg/controller/psbackup/controller.go b/pkg/controller/psbackup/controller.go index e70b440d8..fb61e620d 100644 --- a/pkg/controller/psbackup/controller.go +++ b/pkg/controller/psbackup/controller.go @@ -42,11 +42,11 @@ import ( apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" + "github.com/percona/percona-server-mysql-operator/pkg/db" "github.com/percona/percona-server-mysql-operator/pkg/k8s" "github.com/percona/percona-server-mysql-operator/pkg/mysql" "github.com/percona/percona-server-mysql-operator/pkg/orchestrator" "github.com/percona/percona-server-mysql-operator/pkg/platform" - "github.com/percona/percona-server-mysql-operator/pkg/replicator" "github.com/percona/percona-server-mysql-operator/pkg/secret" "github.com/percona/percona-server-mysql-operator/pkg/xtrabackup" ) @@ -352,17 +352,14 @@ func (r *PerconaServerMySQLBackupReconciler) getTopology(ctx context.Context, cl fqdn := mysql.FQDN(cluster, 0) - db, err := replicator.NewReplicator(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) - if err != nil { - return Topology{}, errors.Wrapf(err, "open connection to %s", fqdn) - } + rm := db.NewReplicationManager(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) - replicas, err := db.GetGroupReplicationReplicas(ctx) + replicas, err := rm.GetGroupReplicationReplicas(ctx) if err != nil { return Topology{}, errors.Wrap(err, "get group-replication replicas") } - primary, err := db.GetGroupReplicationPrimary(ctx) + primary, err := rm.GetGroupReplicationPrimary(ctx) if err != nil { return Topology{}, errors.Wrap(err, "get group-replication primary") } diff --git a/pkg/db/db.go b/pkg/db/db.go new file mode 100644 index 000000000..4aa9a5f06 --- /dev/null +++ b/pkg/db/db.go @@ -0,0 +1,46 @@ +package db + +import ( + "bytes" + "context" + "fmt" + "regexp" + "strings" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" +) + +var sensitiveRegexp = regexp.MustCompile(":.*@") + +type db struct { + client clientcmd.Client + pod *corev1.Pod + user apiv1alpha1.SystemUser + pass string + host string +} + +func newDB(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) *db { + return &db{client: cliCmd, pod: pod, user: user, pass: pass, host: host} +} + +func (d *db) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { + cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", d.pass), "-u", string(d.user), "-h", d.host, "-e", stm} + + err := d.client.Exec(ctx, d.pod, "mysql", cmd, nil, stdout, stderr, false) + if err != nil { + sout := sensitiveRegexp.ReplaceAllString(stdout.String(), ":*****@") + serr := sensitiveRegexp.ReplaceAllString(stderr.String(), ":*****@") + return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) + } + + if strings.Contains(stderr.String(), "ERROR") { + return fmt.Errorf("sql error: %s", stderr) + } + + return nil +} diff --git a/pkg/db/replication.go b/pkg/db/replication.go new file mode 100644 index 000000000..8ec7dee26 --- /dev/null +++ b/pkg/db/replication.go @@ -0,0 +1,169 @@ +package db + +import ( + "bytes" + "context" + "database/sql" + "encoding/csv" + "fmt" + "github.com/gocarina/gocsv" + "strings" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" +) + +const defaultChannelName = "" + +type ReplicationStatus int8 + +const ( + ReplicationStatusActive ReplicationStatus = iota + ReplicationStatusError + ReplicationStatusNotInitiated +) + +type MemberState string + +const ( + MemberStateOnline MemberState = "ONLINE" + MemberStateOffline MemberState = "OFFLINE" + MemberStateError MemberState = "ERROR" +) + +type ReplicationDBManager struct { + db *db +} + +func NewReplicationManager(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) *ReplicationDBManager { + return &ReplicationDBManager{db: newDB(pod, cliCmd, user, pass, host)} +} + +func (m *ReplicationDBManager) query(ctx context.Context, query string, out interface{}) error { + var errb, outb bytes.Buffer + err := m.db.exec(ctx, query, &outb, &errb) + if err != nil { + return err + } + + if !strings.Contains(errb.String(), "ERROR") && outb.Len() == 0 { + return sql.ErrNoRows + } + + r := csv.NewReader(bytes.NewReader(outb.Bytes())) + r.Comma = '\t' + + if err = gocsv.UnmarshalCSV(r, out); err != nil { + return err + } + + return nil +} + +func (m *ReplicationDBManager) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { + var errb, outb bytes.Buffer + q := fmt.Sprintf(` + CHANGE REPLICATION SOURCE TO + SOURCE_USER='%s', + SOURCE_PASSWORD='%s', + SOURCE_HOST='%s', + SOURCE_PORT=%d, + SOURCE_SSL=1, + SOURCE_CONNECTION_AUTO_FAILOVER=1, + SOURCE_AUTO_POSITION=1, + SOURCE_RETRY_COUNT=3, + SOURCE_CONNECT_RETRY=60 + `, apiv1alpha1.UserReplication, replicaPass, host, port) + err := m.db.exec(ctx, q, &outb, &errb) + + if err != nil { + return errors.Wrap(err, "exec CHANGE REPLICATION SOURCE TO") + } + + return nil +} + +func (m *ReplicationDBManager) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { + rows := []*struct { + IoState string `csv:"conn_state"` + SqlState string `csv:"applier_state"` + Host string `csv:"host"` + }{} + + q := fmt.Sprintf(` + SELECT + connection_status.SERVICE_STATE as conn_state, + applier_status.SERVICE_STATE as applier_state, + HOST as host + FROM replication_connection_status connection_status + JOIN replication_connection_configuration connection_configuration + ON connection_status.channel_name = connection_configuration.channel_name + JOIN replication_applier_status applier_status + ON connection_status.channel_name = applier_status.channel_name + WHERE connection_status.channel_name = '%s' + `, defaultChannelName) + err := m.query(ctx, q, &rows) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return ReplicationStatusNotInitiated, "", nil + } + return ReplicationStatusError, "", errors.Wrap(err, "scan replication status") + } + + if rows[0].IoState == "ON" && rows[0].SqlState == "ON" { + return ReplicationStatusActive, rows[0].Host, nil + } + + return ReplicationStatusNotInitiated, "", err +} + +func (m *ReplicationDBManager) GetGroupReplicationPrimary(ctx context.Context) (string, error) { + rows := []*struct { + Host string `csv:"host"` + }{} + + err := m.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='PRIMARY' AND MEMBER_STATE='ONLINE'", &rows) + if err != nil { + return "", errors.Wrap(err, "query primary member") + } + + return rows[0].Host, nil +} + +// TODO: finish implementation +func (m *ReplicationDBManager) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { + rows := []*struct { + Host string `csv:"host"` + }{} + + err := m.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'", &rows) + if err != nil { + return nil, errors.Wrap(err, "query replicas") + } + + replicas := make([]string, 0) + for _, row := range rows { + replicas = append(replicas, row.Host) + } + + return replicas, nil +} + +func (m *ReplicationDBManager) GetMemberState(ctx context.Context, host string) (MemberState, error) { + rows := []*struct { + State MemberState `csv:"state"` + }{} + q := fmt.Sprintf(`SELECT MEMBER_STATE as state FROM replication_group_members WHERE MEMBER_HOST='%s'`, host) + err := m.query(ctx, q, &rows) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return MemberStateOffline, nil + } + return MemberStateError, errors.Wrap(err, "query member state") + } + + return rows[0].State, nil +} diff --git a/pkg/db/users.go b/pkg/db/users.go new file mode 100644 index 000000000..eaf3012a5 --- /dev/null +++ b/pkg/db/users.go @@ -0,0 +1,73 @@ +package db + +import ( + "bytes" + "context" + "fmt" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "strings" + + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" + "github.com/percona/percona-server-mysql-operator/pkg/mysql" +) + +type UserDBManager struct { + db *db +} + +func NewUserManager(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) *UserDBManager { + return &UserDBManager{db: newDB(pod, cliCmd, user, pass, host)} +} + +// UpdateUserPasswords updates user passwords but retains the current password using Dual Password feature of MySQL 8 +func (m *UserDBManager) UpdateUserPasswords(ctx context.Context, users []mysql.User) error { + for _, user := range users { + for _, host := range user.Hosts { + q := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s' RETAIN CURRENT PASSWORD", user.Username, host, escapePass(user.Password)) + var errb, outb bytes.Buffer + err := m.db.exec(ctx, q, &outb, &errb) + if err != nil { + return errors.Wrap(err, "alter user") + } + } + } + + var errb, outb bytes.Buffer + err := m.db.exec(ctx, "FLUSH PRIVILEGES", &outb, &errb) + if err != nil { + return errors.Wrap(err, "flush privileges") + } + + return nil +} + +// DiscardOldPasswords discards old passwords of givens users +func (m *UserDBManager) DiscardOldPasswords(ctx context.Context, users []mysql.User) error { + for _, user := range users { + for _, host := range user.Hosts { + q := fmt.Sprintf("ALTER USER '%s'@'%s' DISCARD OLD PASSWORD", user.Username, host) + var errb, outb bytes.Buffer + err := m.db.exec(ctx, q, &outb, &errb) + if err != nil { + return errors.Wrap(err, "discard old password") + } + } + } + + var errb, outb bytes.Buffer + err := m.db.exec(ctx, "FLUSH PRIVILEGES", &outb, &errb) + if err != nil { + return errors.Wrap(err, "flush privileges") + } + + return nil +} + +func escapePass(pass string) string { + s := strings.ReplaceAll(pass, `'`, `\'`) + s = strings.ReplaceAll(s, `"`, `\"`) + s = strings.ReplaceAll(s, `\`, `\\`) + return s +} diff --git a/pkg/platform/platform.go b/pkg/platform/platform.go index c23373963..5b1877121 100644 --- a/pkg/platform/platform.go +++ b/pkg/platform/platform.go @@ -29,7 +29,7 @@ var ( mx sync.Mutex ) -// Server returns server version and platform (k8s|oc) +// GetServerVersion returns server version and platform (k8s|oc) // it performs API requests for the first invocation and then returns "cached" value func GetServerVersion(cliCmd clientcmd.Client) (*ServerVersion, error) { mx.Lock() diff --git a/pkg/replicator/replicator.go b/pkg/replicator/replicator.go index 6efe71b2a..7782dbc4b 100644 --- a/pkg/replicator/replicator.go +++ b/pkg/replicator/replicator.go @@ -1,202 +1,202 @@ package replicator -import ( - "bytes" - "context" - "database/sql" - "encoding/csv" - "fmt" - "regexp" - "strings" - - "github.com/gocarina/gocsv" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - - apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" -) - -var sensitiveRegexp = regexp.MustCompile(":.*@") - -const DefaultChannelName = "" - -type ReplicationStatus int8 - -var ErrRestartAfterClone = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") - -const ( - ReplicationStatusActive ReplicationStatus = iota - ReplicationStatusError - ReplicationStatusNotInitiated -) - -type MemberState string - -const ( - MemberStateOnline MemberState = "ONLINE" - MemberStateOffline MemberState = "OFFLINE" - MemberStateError MemberState = "ERROR" -) - -type Replicator interface { - ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error - ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) - GetGroupReplicationPrimary(ctx context.Context) (string, error) - GetGroupReplicationReplicas(ctx context.Context) ([]string, error) - GetMemberState(ctx context.Context, host string) (MemberState, error) -} -type db struct { - client clientcmd.Client - pod *corev1.Pod - user apiv1alpha1.SystemUser - pass string - host string -} - -func NewReplicator(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Replicator, error) { - return &db{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil -} - -func (d *db) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { - cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", d.pass), "-u", string(d.user), "-h", d.host, "-e", stm} - - err := d.client.Exec(ctx, d.pod, "mysql", cmd, nil, stdout, stderr, false) - if err != nil { - sout := sensitiveRegexp.ReplaceAllString(stdout.String(), ":*****@") - serr := sensitiveRegexp.ReplaceAllString(stderr.String(), ":*****@") - return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) - } - - if strings.Contains(stderr.String(), "ERROR") { - return fmt.Errorf("sql error: %s", stderr) - } - - return nil -} - -func (d *db) query(ctx context.Context, query string, out interface{}) error { - var errb, outb bytes.Buffer - err := d.exec(ctx, query, &outb, &errb) - if err != nil { - return err - } - - if !strings.Contains(errb.String(), "ERROR") && outb.Len() == 0 { - return sql.ErrNoRows - } - - r := csv.NewReader(bytes.NewReader(outb.Bytes())) - r.Comma = '\t' - - if err = gocsv.UnmarshalCSV(r, out); err != nil { - return err - } - - return nil -} - -func (d *db) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { - var errb, outb bytes.Buffer - q := fmt.Sprintf(` - CHANGE REPLICATION SOURCE TO - SOURCE_USER='%s', - SOURCE_PASSWORD='%s', - SOURCE_HOST='%s', - SOURCE_PORT=%d, - SOURCE_SSL=1, - SOURCE_CONNECTION_AUTO_FAILOVER=1, - SOURCE_AUTO_POSITION=1, - SOURCE_RETRY_COUNT=3, - SOURCE_CONNECT_RETRY=60 - `, apiv1alpha1.UserReplication, replicaPass, host, port) - err := d.exec(ctx, q, &outb, &errb) - - if err != nil { - return errors.Wrap(err, "exec CHANGE REPLICATION SOURCE TO") - } - - return nil -} - -func (d *db) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { - rows := []*struct { - IoState string `csv:"conn_state"` - SqlState string `csv:"applier_state"` - Host string `csv:"host"` - }{} - - q := fmt.Sprintf(` - SELECT - connection_status.SERVICE_STATE as conn_state, - applier_status.SERVICE_STATE as applier_state, - HOST as host - FROM replication_connection_status connection_status - JOIN replication_connection_configuration connection_configuration - ON connection_status.channel_name = connection_configuration.channel_name - JOIN replication_applier_status applier_status - ON connection_status.channel_name = applier_status.channel_name - WHERE connection_status.channel_name = '%s' - `, DefaultChannelName) - err := d.query(ctx, q, &rows) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return ReplicationStatusNotInitiated, "", nil - } - return ReplicationStatusError, "", errors.Wrap(err, "scan replication status") - } - - if rows[0].IoState == "ON" && rows[0].SqlState == "ON" { - return ReplicationStatusActive, rows[0].Host, nil - } - - return ReplicationStatusNotInitiated, "", err -} - -func (d *db) GetGroupReplicationPrimary(ctx context.Context) (string, error) { - rows := []*struct { - Host string `csv:"host"` - }{} - - err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='PRIMARY' AND MEMBER_STATE='ONLINE'", &rows) - if err != nil { - return "", errors.Wrap(err, "query primary member") - } - - return rows[0].Host, nil -} - -// TODO: finish implementation -func (d *db) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { - rows := []*struct { - Host string `csv:"host"` - }{} - - err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'", &rows) - if err != nil { - return nil, errors.Wrap(err, "query replicas") - } - - replicas := make([]string, 0) - for _, row := range rows { - replicas = append(replicas, row.Host) - } - - return replicas, nil -} - -func (d *db) GetMemberState(ctx context.Context, host string) (MemberState, error) { - rows := []*struct { - State MemberState `csv:"state"` - }{} - q := fmt.Sprintf(`SELECT MEMBER_STATE as state FROM replication_group_members WHERE MEMBER_HOST='%s'`, host) - err := d.query(ctx, q, &rows) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return MemberStateOffline, nil - } - return MemberStateError, errors.Wrap(err, "query member state") - } - - return rows[0].State, nil -} +//import ( +// "bytes" +// "context" +// "database/sql" +// "encoding/csv" +// "fmt" +// "regexp" +// "strings" +// +// "github.com/gocarina/gocsv" +// "github.com/pkg/errors" +// corev1 "k8s.io/api/core/v1" +// +// apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" +// "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" +//) +// +//var sensitiveRegexp = regexp.MustCompile(":.*@") +// +//const DefaultChannelName = "" +// +//type ReplicationStatus int8 +// +//var ErrRestartAfterClone = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") +// +//const ( +// ReplicationStatusActive ReplicationStatus = iota +// ReplicationStatusError +// ReplicationStatusNotInitiated +//) +// +//type MemberState string +// +//const ( +// MemberStateOnline MemberState = "ONLINE" +// MemberStateOffline MemberState = "OFFLINE" +// MemberStateError MemberState = "ERROR" +//) +// +//type Replicator interface { +// ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error +// ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) +// GetGroupReplicationPrimary(ctx context.Context) (string, error) +// GetGroupReplicationReplicas(ctx context.Context) ([]string, error) +// GetMemberState(ctx context.Context, host string) (MemberState, error) +//} +//type db struct { +// client clientcmd.Client +// pod *corev1.Pod +// user apiv1alpha1.SystemUser +// pass string +// host string +//} +// +//func NewReplicator(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Replicator, error) { +// return &db{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil +//} +// +//func (d *db) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { +// cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", d.pass), "-u", string(d.user), "-h", d.host, "-e", stm} +// +// err := d.client.Exec(ctx, d.pod, "mysql", cmd, nil, stdout, stderr, false) +// if err != nil { +// sout := sensitiveRegexp.ReplaceAllString(stdout.String(), ":*****@") +// serr := sensitiveRegexp.ReplaceAllString(stderr.String(), ":*****@") +// return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) +// } +// +// if strings.Contains(stderr.String(), "ERROR") { +// return fmt.Errorf("sql error: %s", stderr) +// } +// +// return nil +//} +// +//func (d *db) query(ctx context.Context, query string, out interface{}) error { +// var errb, outb bytes.Buffer +// err := d.exec(ctx, query, &outb, &errb) +// if err != nil { +// return err +// } +// +// if !strings.Contains(errb.String(), "ERROR") && outb.Len() == 0 { +// return sql.ErrNoRows +// } +// +// r := csv.NewReader(bytes.NewReader(outb.Bytes())) +// r.Comma = '\t' +// +// if err = gocsv.UnmarshalCSV(r, out); err != nil { +// return err +// } +// +// return nil +//} +// +//func (d *db) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { +// var errb, outb bytes.Buffer +// q := fmt.Sprintf(` +// CHANGE REPLICATION SOURCE TO +// SOURCE_USER='%s', +// SOURCE_PASSWORD='%s', +// SOURCE_HOST='%s', +// SOURCE_PORT=%d, +// SOURCE_SSL=1, +// SOURCE_CONNECTION_AUTO_FAILOVER=1, +// SOURCE_AUTO_POSITION=1, +// SOURCE_RETRY_COUNT=3, +// SOURCE_CONNECT_RETRY=60 +// `, apiv1alpha1.UserReplication, replicaPass, host, port) +// err := d.exec(ctx, q, &outb, &errb) +// +// if err != nil { +// return errors.Wrap(err, "exec CHANGE REPLICATION SOURCE TO") +// } +// +// return nil +//} +// +//func (d *db) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { +// rows := []*struct { +// IoState string `csv:"conn_state"` +// SqlState string `csv:"applier_state"` +// Host string `csv:"host"` +// }{} +// +// q := fmt.Sprintf(` +// SELECT +// connection_status.SERVICE_STATE as conn_state, +// applier_status.SERVICE_STATE as applier_state, +// HOST as host +// FROM replication_connection_status connection_status +// JOIN replication_connection_configuration connection_configuration +// ON connection_status.channel_name = connection_configuration.channel_name +// JOIN replication_applier_status applier_status +// ON connection_status.channel_name = applier_status.channel_name +// WHERE connection_status.channel_name = '%s' +// `, DefaultChannelName) +// err := d.query(ctx, q, &rows) +// if err != nil { +// if errors.Is(err, sql.ErrNoRows) { +// return ReplicationStatusNotInitiated, "", nil +// } +// return ReplicationStatusError, "", errors.Wrap(err, "scan replication status") +// } +// +// if rows[0].IoState == "ON" && rows[0].SqlState == "ON" { +// return ReplicationStatusActive, rows[0].Host, nil +// } +// +// return ReplicationStatusNotInitiated, "", err +//} +// +//func (d *db) GetGroupReplicationPrimary(ctx context.Context) (string, error) { +// rows := []*struct { +// Host string `csv:"host"` +// }{} +// +// err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='PRIMARY' AND MEMBER_STATE='ONLINE'", &rows) +// if err != nil { +// return "", errors.Wrap(err, "query primary member") +// } +// +// return rows[0].Host, nil +//} +// +//// TODO: finish implementation +//func (d *db) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { +// rows := []*struct { +// Host string `csv:"host"` +// }{} +// +// err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'", &rows) +// if err != nil { +// return nil, errors.Wrap(err, "query replicas") +// } +// +// replicas := make([]string, 0) +// for _, row := range rows { +// replicas = append(replicas, row.Host) +// } +// +// return replicas, nil +//} +// +//func (d *db) GetMemberState(ctx context.Context, host string) (MemberState, error) { +// rows := []*struct { +// State MemberState `csv:"state"` +// }{} +// q := fmt.Sprintf(`SELECT MEMBER_STATE as state FROM replication_group_members WHERE MEMBER_HOST='%s'`, host) +// err := d.query(ctx, q, &rows) +// if err != nil { +// if errors.Is(err, sql.ErrNoRows) { +// return MemberStateOffline, nil +// } +// return MemberStateError, errors.Wrap(err, "query member state") +// } +// +// return rows[0].State, nil +//} diff --git a/pkg/users/users.go b/pkg/users/users.go deleted file mode 100644 index 33ed1a0bd..000000000 --- a/pkg/users/users.go +++ /dev/null @@ -1,132 +0,0 @@ -package users - -import ( - "database/sql" - "fmt" - - mysqldriver "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" - - apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - "github.com/percona/percona-server-mysql-operator/pkg/mysql" -) - -type Manager interface { - UpdateUserPasswords(users []mysql.User) error - DiscardOldPasswords(users []mysql.User) error - Close() error -} - -type dbImpl struct{ db *sql.DB } - -func NewManager(user apiv1alpha1.SystemUser, pass, host string, port int32) (Manager, error) { - config := mysqldriver.NewConfig() - - config.User = string(user) - config.Passwd = pass - config.Net = "tcp" - config.Addr = fmt.Sprintf("%s:%d", host, port) - config.DBName = "performance_schema" - config.Params = map[string]string{ - "interpolateParams": "true", - "timeout": "20s", - "readTimeout": "20s", - "writeTimeout": "20s", - "tls": "preferred", - } - - db, err := sql.Open("mysql", config.FormatDSN()) - if err != nil { - return nil, errors.Wrap(err, "connect to MySQL") - } - - if err := db.Ping(); err != nil { - return nil, errors.Wrap(err, "ping database") - } - - return &dbImpl{db}, nil -} - -// UpdateUserPasswords updates user passwords but retains the current password using Dual Password feature of MySQL 8 -func (d *dbImpl) UpdateUserPasswords(users []mysql.User) error { - tx, err := d.db.Begin() - if err != nil { - return errors.Wrap(err, "begin transaction") - } - - for _, user := range users { - for _, host := range user.Hosts { - _, err = tx.Exec("ALTER USER ?@? IDENTIFIED BY ? RETAIN CURRENT PASSWORD", user.Username, host, user.Password) - if err != nil { - err = errors.Wrap(err, "alter user") - - if errT := tx.Rollback(); errT != nil { - return errors.Wrap(errors.Wrap(errT, "rollback"), err.Error()) - } - - return err - } - } - } - - _, err = tx.Exec("FLUSH PRIVILEGES") - if err != nil { - err = errors.Wrap(err, "flush privileges") - - if errT := tx.Rollback(); errT != nil { - return errors.Wrap(errors.Wrap(errT, "rollback"), err.Error()) - } - - return err - } - - if err := tx.Commit(); err != nil { - return errors.Wrap(err, "commit transaction") - } - - return nil -} - -// DiscardOldPasswords discards old passwords of givens users -func (d *dbImpl) DiscardOldPasswords(users []mysql.User) error { - tx, err := d.db.Begin() - if err != nil { - return errors.Wrap(err, "begin transaction") - } - - for _, user := range users { - for _, host := range user.Hosts { - _, err = tx.Exec("ALTER USER ?@? DISCARD OLD PASSWORD", user.Username, host) - if err != nil { - err = errors.Wrap(err, "alter user") - - if errT := tx.Rollback(); errT != nil { - return errors.Wrap(errors.Wrap(errT, "rollback"), err.Error()) - } - - return err - } - } - } - - _, err = tx.Exec("FLUSH PRIVILEGES") - if err != nil { - err = errors.Wrap(err, "flush privileges") - - if errT := tx.Rollback(); errT != nil { - return errors.Wrap(errors.Wrap(errT, "rollback"), err.Error()) - } - - return err - } - - if err := tx.Commit(); err != nil { - return errors.Wrap(err, "commit transaction") - } - - return nil -} - -func (d *dbImpl) Close() error { - return d.db.Close() -} diff --git a/pkg/users/usersexec.go b/pkg/users/usersexec.go index b143c5150..49e33ee0b 100644 --- a/pkg/users/usersexec.go +++ b/pkg/users/usersexec.go @@ -1,101 +1,107 @@ package users -import ( - "bytes" - "context" - "fmt" - "regexp" - "strings" - - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - - apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" - "github.com/percona/percona-server-mysql-operator/pkg/mysql" -) - -var sensitiveRegexp = regexp.MustCompile(":.*@") - -type dbExecImpl struct { - client clientcmd.Client - pod *corev1.Pod - user apiv1alpha1.SystemUser - pass string - host string -} - -func NewManagerExec(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Manager, error) { - return &dbExecImpl{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil -} - -func (d *dbExecImpl) exec(stm string) error { - - cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", escapePass(d.pass)), "-u", string(d.user), "-h", d.host, "-e", stm} - - var outb, errb bytes.Buffer - err := d.client.Exec(context.TODO(), d.pod, "mysql", cmd, nil, &outb, &errb, false) - if err != nil { - sout := sensitiveRegexp.ReplaceAllString(outb.String(), ":*****@") - serr := sensitiveRegexp.ReplaceAllString(errb.String(), ":*****@") - return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) - } - - if strings.Contains(errb.String(), "ERROR") { - serr := sensitiveRegexp.ReplaceAllString(errb.String(), ":*****@") - return fmt.Errorf("sql error: %s", serr) - } - - return nil -} - -// UpdateUserPasswords updates user passwords but retains the current password using Dual Password feature of MySQL 8 -func (d *dbExecImpl) UpdateUserPasswords(users []mysql.User) error { - for _, user := range users { - for _, host := range user.Hosts { - q := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s' RETAIN CURRENT PASSWORD", user.Username, host, escapePass(user.Password)) - err := d.exec(q) - if err != nil { - return errors.Wrap(err, "alter user") - } - } - } - - err := d.exec("FLUSH PRIVILEGES") - if err != nil { - return errors.Wrap(err, "flush privileges") - } - - return nil -} - -// DiscardOldPasswords discards old passwords of givens users -func (d *dbExecImpl) DiscardOldPasswords(users []mysql.User) error { - for _, user := range users { - for _, host := range user.Hosts { - q := fmt.Sprintf("ALTER USER '%s'@'%s' DISCARD OLD PASSWORD", user.Username, host) - err := d.exec(q) - if err != nil { - return errors.Wrap(err, "discard old password") - } - } - } - - err := d.exec("FLUSH PRIVILEGES") - if err != nil { - return errors.Wrap(err, "flush privileges") - } - - return nil -} - -func (d *dbExecImpl) Close() error { - return nil -} - -func escapePass(pass string) string { - s := strings.ReplaceAll(pass, `'`, `\'`) - s = strings.ReplaceAll(s, `"`, `\"`) - s = strings.ReplaceAll(s, `\`, `\\`) - return s -} +//import ( +// "bytes" +// "context" +// "fmt" +// "regexp" +// "strings" +// +// "github.com/pkg/errors" +// corev1 "k8s.io/api/core/v1" +// +// apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" +// "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" +// "github.com/percona/percona-server-mysql-operator/pkg/mysql" +//) +// +//var sensitiveRegexp = regexp.MustCompile(":.*@") +// +//type Manager interface { +// UpdateUserPasswords(users []mysql.User) error +// DiscardOldPasswords(users []mysql.User) error +// Close() error +//} +// +//type dbExecImpl struct { +// client clientcmd.Client +// pod *corev1.Pod +// user apiv1alpha1.SystemUser +// pass string +// host string +//} +// +//func NewManagerExec(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Manager, error) { +// return &dbExecImpl{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil +//} +// +//func (d *dbExecImpl) exec(stm string) error { +// +// cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", escapePass(d.pass)), "-u", string(d.user), "-h", d.host, "-e", stm} +// +// var outb, errb bytes.Buffer +// err := d.client.Exec(context.TODO(), d.pod, "mysql", cmd, nil, &outb, &errb, false) +// if err != nil { +// sout := sensitiveRegexp.ReplaceAllString(outb.String(), ":*****@") +// serr := sensitiveRegexp.ReplaceAllString(errb.String(), ":*****@") +// return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) +// } +// +// if strings.Contains(errb.String(), "ERROR") { +// serr := sensitiveRegexp.ReplaceAllString(errb.String(), ":*****@") +// return fmt.Errorf("sql error: %s", serr) +// } +// +// return nil +//} +// +//// UpdateUserPasswords updates user passwords but retains the current password using Dual Password feature of MySQL 8 +//func (d *dbExecImpl) UpdateUserPasswords(users []mysql.User) error { +// for _, user := range users { +// for _, host := range user.Hosts { +// q := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s' RETAIN CURRENT PASSWORD", user.Username, host, escapePass(user.Password)) +// err := d.exec(q) +// if err != nil { +// return errors.Wrap(err, "alter user") +// } +// } +// } +// +// err := d.exec("FLUSH PRIVILEGES") +// if err != nil { +// return errors.Wrap(err, "flush privileges") +// } +// +// return nil +//} +// +//// DiscardOldPasswords discards old passwords of givens users +//func (d *dbExecImpl) DiscardOldPasswords(users []mysql.User) error { +// for _, user := range users { +// for _, host := range user.Hosts { +// q := fmt.Sprintf("ALTER USER '%s'@'%s' DISCARD OLD PASSWORD", user.Username, host) +// err := d.exec(q) +// if err != nil { +// return errors.Wrap(err, "discard old password") +// } +// } +// } +// +// err := d.exec("FLUSH PRIVILEGES") +// if err != nil { +// return errors.Wrap(err, "flush privileges") +// } +// +// return nil +//} +// +//func (d *dbExecImpl) Close() error { +// return nil +//} +// +//func escapePass(pass string) string { +// s := strings.ReplaceAll(pass, `'`, `\'`) +// s = strings.ReplaceAll(s, `"`, `\"`) +// s = strings.ReplaceAll(s, `\`, `\\`) +// return s +//} From eee9adcecc5346779604bb8cfca408bd116acb56 Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Mon, 8 Jan 2024 12:44:11 +0100 Subject: [PATCH 06/12] Refactor getting topology in psbackup controller. --- pkg/controller/psbackup/controller.go | 33 ++++++--------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/pkg/controller/psbackup/controller.go b/pkg/controller/psbackup/controller.go index fb61e620d..bd5c44e65 100644 --- a/pkg/controller/psbackup/controller.go +++ b/pkg/controller/psbackup/controller.go @@ -345,8 +345,9 @@ type Topology struct { func (r *PerconaServerMySQLBackupReconciler) getTopology(ctx context.Context, cluster *apiv1alpha1.PerconaServerMySQL, operatorPass string) (Topology, error) { switch cluster.Spec.MySQL.ClusterType { case apiv1alpha1.ClusterTypeGR: - firstPod, err := getMySQLPod(ctx, r.Client, cluster, 0) - if err != nil { + firstPod := &corev1.Pod{} + nn := types.NamespacedName{Namespace: cluster.Namespace, Name: mysql.PodName(cluster, 0)} + if err := r.Client.Get(ctx, nn, firstPod); err != nil { return Topology{}, err } @@ -368,10 +369,12 @@ func (r *PerconaServerMySQLBackupReconciler) getTopology(ctx context.Context, cl Replicas: replicas, }, nil case apiv1alpha1.ClusterTypeAsync: - pod, err := getOrcPod(ctx, r.Client, cluster, 0) - if err != nil { + pod := &corev1.Pod{} + nn := types.NamespacedName{Namespace: cluster.Namespace, Name: orchestrator.PodName(cluster, 0)} + if err := r.Client.Get(ctx, nn, pod); err != nil { return Topology{}, err } + primary, err := orchestrator.ClusterPrimaryExec(ctx, r.ClientCmd, pod, cluster.ClusterHint()) if err != nil { @@ -391,28 +394,6 @@ func (r *PerconaServerMySQLBackupReconciler) getTopology(ctx context.Context, cl } } -func getMySQLPod(ctx context.Context, cl client.Reader, cr *apiv1alpha1.PerconaServerMySQL, idx int) (*corev1.Pod, error) { - pod := &corev1.Pod{} - - nn := types.NamespacedName{Namespace: cr.Namespace, Name: mysql.PodName(cr, idx)} - if err := cl.Get(ctx, nn, pod); err != nil { - return nil, err - } - - return pod, nil -} - -func getOrcPod(ctx context.Context, cl client.Reader, cr *apiv1alpha1.PerconaServerMySQL, idx int) (*corev1.Pod, error) { - pod := &corev1.Pod{} - - nn := types.NamespacedName{Namespace: cr.Namespace, Name: orchestrator.PodName(cr, idx)} - if err := cl.Get(ctx, nn, pod); err != nil { - return nil, err - } - - return pod, nil -} - const finalizerDeleteBackup = "delete-backup" func (r *PerconaServerMySQLBackupReconciler) checkFinalizers(ctx context.Context, cr *apiv1alpha1.PerconaServerMySQLBackup) { From f7448f42f8a5498929bb925ddd8fdb2781ac3d9d Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Mon, 8 Jan 2024 13:02:46 +0100 Subject: [PATCH 07/12] Fix cmd/mysql.go --- cmd/mysql/mysql.go | 22 ++-- pkg/replicator/replicator.go | 202 ----------------------------------- pkg/users/usersexec.go | 107 ------------------- 3 files changed, 11 insertions(+), 320 deletions(-) delete mode 100644 pkg/replicator/replicator.go delete mode 100644 pkg/users/usersexec.go diff --git a/cmd/mysql/mysql.go b/cmd/mysql/mysql.go index 7767dfa83..d8ac47811 100644 --- a/cmd/mysql/mysql.go +++ b/cmd/mysql/mysql.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - "github.com/percona/percona-server-mysql-operator/pkg/replicator" + "github.com/percona/percona-server-mysql-operator/pkg/db" ) const defaultChannelName = "" @@ -82,7 +82,7 @@ func (d *DB) ResetReplication(ctx context.Context) error { return errors.Wrap(err, "reset replication") } -func (d *DB) ReplicationStatus(ctx context.Context) (replicator.ReplicationStatus, string, error) { +func (d *DB) ReplicationStatus(ctx context.Context) (db.ReplicationStatus, string, error) { row := d.db.QueryRowContext(ctx, ` SELECT connection_status.SERVICE_STATE, @@ -99,21 +99,21 @@ func (d *DB) ReplicationStatus(ctx context.Context) (replicator.ReplicationStatu var ioState, sqlState, host string if err := row.Scan(&ioState, &sqlState, &host); err != nil { if errors.Is(err, sql.ErrNoRows) { - return replicator.ReplicationStatusNotInitiated, "", nil + return db.ReplicationStatusNotInitiated, "", nil } - return replicator.ReplicationStatusError, "", errors.Wrap(err, "scan replication status") + return db.ReplicationStatusError, "", errors.Wrap(err, "scan replication status") } if ioState == "ON" && sqlState == "ON" { - return replicator.ReplicationStatusActive, host, nil + return db.ReplicationStatusActive, host, nil } - return replicator.ReplicationStatusNotInitiated, "", nil + return db.ReplicationStatusNotInitiated, "", nil } func (d *DB) IsReplica(ctx context.Context) (bool, error) { status, _, err := d.ReplicationStatus(ctx) - return status == replicator.ReplicationStatusActive, errors.Wrap(err, "get replication status") + return status == db.ReplicationStatusActive, errors.Wrap(err, "get replication status") } func (d *DB) DisableSuperReadonly(ctx context.Context) error { @@ -184,15 +184,15 @@ func (d *DB) DumbQuery(ctx context.Context) error { return errors.Wrap(err, "SELECT 1") } -func (d *DB) GetMemberState(ctx context.Context, host string) (replicator.MemberState, error) { - var state replicator.MemberState +func (d *DB) GetMemberState(ctx context.Context, host string) (db.MemberState, error) { + var state db.MemberState err := d.db.QueryRowContext(ctx, "SELECT MEMBER_STATE FROM replication_group_members WHERE MEMBER_HOST=?", host).Scan(&state) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return replicator.MemberStateOffline, nil + return db.MemberStateOffline, nil } - return replicator.MemberStateError, errors.Wrap(err, "query member state") + return db.MemberStateError, errors.Wrap(err, "query member state") } return state, nil diff --git a/pkg/replicator/replicator.go b/pkg/replicator/replicator.go deleted file mode 100644 index 7782dbc4b..000000000 --- a/pkg/replicator/replicator.go +++ /dev/null @@ -1,202 +0,0 @@ -package replicator - -//import ( -// "bytes" -// "context" -// "database/sql" -// "encoding/csv" -// "fmt" -// "regexp" -// "strings" -// -// "github.com/gocarina/gocsv" -// "github.com/pkg/errors" -// corev1 "k8s.io/api/core/v1" -// -// apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" -// "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" -//) -// -//var sensitiveRegexp = regexp.MustCompile(":.*@") -// -//const DefaultChannelName = "" -// -//type ReplicationStatus int8 -// -//var ErrRestartAfterClone = errors.New("Error 3707: Restart server failed (mysqld is not managed by supervisor process).") -// -//const ( -// ReplicationStatusActive ReplicationStatus = iota -// ReplicationStatusError -// ReplicationStatusNotInitiated -//) -// -//type MemberState string -// -//const ( -// MemberStateOnline MemberState = "ONLINE" -// MemberStateOffline MemberState = "OFFLINE" -// MemberStateError MemberState = "ERROR" -//) -// -//type Replicator interface { -// ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error -// ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) -// GetGroupReplicationPrimary(ctx context.Context) (string, error) -// GetGroupReplicationReplicas(ctx context.Context) ([]string, error) -// GetMemberState(ctx context.Context, host string) (MemberState, error) -//} -//type db struct { -// client clientcmd.Client -// pod *corev1.Pod -// user apiv1alpha1.SystemUser -// pass string -// host string -//} -// -//func NewReplicator(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Replicator, error) { -// return &db{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil -//} -// -//func (d *db) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) error { -// cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", d.pass), "-u", string(d.user), "-h", d.host, "-e", stm} -// -// err := d.client.Exec(ctx, d.pod, "mysql", cmd, nil, stdout, stderr, false) -// if err != nil { -// sout := sensitiveRegexp.ReplaceAllString(stdout.String(), ":*****@") -// serr := sensitiveRegexp.ReplaceAllString(stderr.String(), ":*****@") -// return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) -// } -// -// if strings.Contains(stderr.String(), "ERROR") { -// return fmt.Errorf("sql error: %s", stderr) -// } -// -// return nil -//} -// -//func (d *db) query(ctx context.Context, query string, out interface{}) error { -// var errb, outb bytes.Buffer -// err := d.exec(ctx, query, &outb, &errb) -// if err != nil { -// return err -// } -// -// if !strings.Contains(errb.String(), "ERROR") && outb.Len() == 0 { -// return sql.ErrNoRows -// } -// -// r := csv.NewReader(bytes.NewReader(outb.Bytes())) -// r.Comma = '\t' -// -// if err = gocsv.UnmarshalCSV(r, out); err != nil { -// return err -// } -// -// return nil -//} -// -//func (d *db) ChangeReplicationSource(ctx context.Context, host, replicaPass string, port int32) error { -// var errb, outb bytes.Buffer -// q := fmt.Sprintf(` -// CHANGE REPLICATION SOURCE TO -// SOURCE_USER='%s', -// SOURCE_PASSWORD='%s', -// SOURCE_HOST='%s', -// SOURCE_PORT=%d, -// SOURCE_SSL=1, -// SOURCE_CONNECTION_AUTO_FAILOVER=1, -// SOURCE_AUTO_POSITION=1, -// SOURCE_RETRY_COUNT=3, -// SOURCE_CONNECT_RETRY=60 -// `, apiv1alpha1.UserReplication, replicaPass, host, port) -// err := d.exec(ctx, q, &outb, &errb) -// -// if err != nil { -// return errors.Wrap(err, "exec CHANGE REPLICATION SOURCE TO") -// } -// -// return nil -//} -// -//func (d *db) ReplicationStatus(ctx context.Context) (ReplicationStatus, string, error) { -// rows := []*struct { -// IoState string `csv:"conn_state"` -// SqlState string `csv:"applier_state"` -// Host string `csv:"host"` -// }{} -// -// q := fmt.Sprintf(` -// SELECT -// connection_status.SERVICE_STATE as conn_state, -// applier_status.SERVICE_STATE as applier_state, -// HOST as host -// FROM replication_connection_status connection_status -// JOIN replication_connection_configuration connection_configuration -// ON connection_status.channel_name = connection_configuration.channel_name -// JOIN replication_applier_status applier_status -// ON connection_status.channel_name = applier_status.channel_name -// WHERE connection_status.channel_name = '%s' -// `, DefaultChannelName) -// err := d.query(ctx, q, &rows) -// if err != nil { -// if errors.Is(err, sql.ErrNoRows) { -// return ReplicationStatusNotInitiated, "", nil -// } -// return ReplicationStatusError, "", errors.Wrap(err, "scan replication status") -// } -// -// if rows[0].IoState == "ON" && rows[0].SqlState == "ON" { -// return ReplicationStatusActive, rows[0].Host, nil -// } -// -// return ReplicationStatusNotInitiated, "", err -//} -// -//func (d *db) GetGroupReplicationPrimary(ctx context.Context) (string, error) { -// rows := []*struct { -// Host string `csv:"host"` -// }{} -// -// err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='PRIMARY' AND MEMBER_STATE='ONLINE'", &rows) -// if err != nil { -// return "", errors.Wrap(err, "query primary member") -// } -// -// return rows[0].Host, nil -//} -// -//// TODO: finish implementation -//func (d *db) GetGroupReplicationReplicas(ctx context.Context) ([]string, error) { -// rows := []*struct { -// Host string `csv:"host"` -// }{} -// -// err := d.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'", &rows) -// if err != nil { -// return nil, errors.Wrap(err, "query replicas") -// } -// -// replicas := make([]string, 0) -// for _, row := range rows { -// replicas = append(replicas, row.Host) -// } -// -// return replicas, nil -//} -// -//func (d *db) GetMemberState(ctx context.Context, host string) (MemberState, error) { -// rows := []*struct { -// State MemberState `csv:"state"` -// }{} -// q := fmt.Sprintf(`SELECT MEMBER_STATE as state FROM replication_group_members WHERE MEMBER_HOST='%s'`, host) -// err := d.query(ctx, q, &rows) -// if err != nil { -// if errors.Is(err, sql.ErrNoRows) { -// return MemberStateOffline, nil -// } -// return MemberStateError, errors.Wrap(err, "query member state") -// } -// -// return rows[0].State, nil -//} diff --git a/pkg/users/usersexec.go b/pkg/users/usersexec.go deleted file mode 100644 index 49e33ee0b..000000000 --- a/pkg/users/usersexec.go +++ /dev/null @@ -1,107 +0,0 @@ -package users - -//import ( -// "bytes" -// "context" -// "fmt" -// "regexp" -// "strings" -// -// "github.com/pkg/errors" -// corev1 "k8s.io/api/core/v1" -// -// apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" -// "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" -// "github.com/percona/percona-server-mysql-operator/pkg/mysql" -//) -// -//var sensitiveRegexp = regexp.MustCompile(":.*@") -// -//type Manager interface { -// UpdateUserPasswords(users []mysql.User) error -// DiscardOldPasswords(users []mysql.User) error -// Close() error -//} -// -//type dbExecImpl struct { -// client clientcmd.Client -// pod *corev1.Pod -// user apiv1alpha1.SystemUser -// pass string -// host string -//} -// -//func NewManagerExec(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) (Manager, error) { -// return &dbExecImpl{client: cliCmd, pod: pod, user: user, pass: pass, host: host}, nil -//} -// -//func (d *dbExecImpl) exec(stm string) error { -// -// cmd := []string{"mysql", "--database", "performance_schema", fmt.Sprintf("-p%s", escapePass(d.pass)), "-u", string(d.user), "-h", d.host, "-e", stm} -// -// var outb, errb bytes.Buffer -// err := d.client.Exec(context.TODO(), d.pod, "mysql", cmd, nil, &outb, &errb, false) -// if err != nil { -// sout := sensitiveRegexp.ReplaceAllString(outb.String(), ":*****@") -// serr := sensitiveRegexp.ReplaceAllString(errb.String(), ":*****@") -// return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) -// } -// -// if strings.Contains(errb.String(), "ERROR") { -// serr := sensitiveRegexp.ReplaceAllString(errb.String(), ":*****@") -// return fmt.Errorf("sql error: %s", serr) -// } -// -// return nil -//} -// -//// UpdateUserPasswords updates user passwords but retains the current password using Dual Password feature of MySQL 8 -//func (d *dbExecImpl) UpdateUserPasswords(users []mysql.User) error { -// for _, user := range users { -// for _, host := range user.Hosts { -// q := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s' RETAIN CURRENT PASSWORD", user.Username, host, escapePass(user.Password)) -// err := d.exec(q) -// if err != nil { -// return errors.Wrap(err, "alter user") -// } -// } -// } -// -// err := d.exec("FLUSH PRIVILEGES") -// if err != nil { -// return errors.Wrap(err, "flush privileges") -// } -// -// return nil -//} -// -//// DiscardOldPasswords discards old passwords of givens users -//func (d *dbExecImpl) DiscardOldPasswords(users []mysql.User) error { -// for _, user := range users { -// for _, host := range user.Hosts { -// q := fmt.Sprintf("ALTER USER '%s'@'%s' DISCARD OLD PASSWORD", user.Username, host) -// err := d.exec(q) -// if err != nil { -// return errors.Wrap(err, "discard old password") -// } -// } -// } -// -// err := d.exec("FLUSH PRIVILEGES") -// if err != nil { -// return errors.Wrap(err, "flush privileges") -// } -// -// return nil -//} -// -//func (d *dbExecImpl) Close() error { -// return nil -//} -// -//func escapePass(pass string) string { -// s := strings.ReplaceAll(pass, `'`, `\'`) -// s = strings.ReplaceAll(s, `"`, `\"`) -// s = strings.ReplaceAll(s, `\`, `\\`) -// return s -//} From 5891f7599f6618f11b213fa67171f4f358e22056 Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Mon, 8 Jan 2024 15:28:28 +0100 Subject: [PATCH 08/12] Fix and refactor cmd/mysql. --- cmd/bootstrap/async_replication.go | 10 +++++----- cmd/{mysql/mysql.go => db/db.go} | 2 +- cmd/healthcheck/main.go | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename cmd/{mysql/mysql.go => db/db.go} (99%) diff --git a/cmd/bootstrap/async_replication.go b/cmd/bootstrap/async_replication.go index 0f8b15cb4..9679ca53a 100644 --- a/cmd/bootstrap/async_replication.go +++ b/cmd/bootstrap/async_replication.go @@ -11,9 +11,9 @@ import ( "k8s.io/apimachinery/pkg/util/sets" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - database "github.com/percona/percona-server-mysql-operator/cmd/mysql" + database "github.com/percona/percona-server-mysql-operator/cmd/db" + mysqldb "github.com/percona/percona-server-mysql-operator/pkg/db" "github.com/percona/percona-server-mysql-operator/pkg/mysql" - "github.com/percona/percona-server-mysql-operator/pkg/replicator" ) func bootstrapAsyncReplication(ctx context.Context) error { @@ -149,7 +149,7 @@ func bootstrapAsyncReplication(ctx context.Context) error { log.Printf("Cloning from %s", donor) err = db.Clone(ctx, donor, string(apiv1alpha1.UserOperator), operatorPass, mysql.DefaultAdminPort) timer.Stop("clone") - if err != nil && !errors.Is(err, replicator.ErrRestartAfterClone) { + if err != nil && !errors.Is(err, database.ErrRestartAfterClone) { return errors.Wrapf(err, "clone from donor %s", donor) } @@ -175,7 +175,7 @@ func bootstrapAsyncReplication(ctx context.Context) error { return errors.Wrap(err, "check replication status") } - if rStatus == replicator.ReplicationStatusNotInitiated { + if rStatus == mysqldb.ReplicationStatusNotInitiated { log.Println("configuring replication") replicaPass, err := getSecret(apiv1alpha1.UserReplication) @@ -229,7 +229,7 @@ func getTopology(ctx context.Context, peers sets.Set[string]) (string, []string, } replicas.Insert(replicaHost) - if status == replicator.ReplicationStatusActive { + if status == mysqldb.ReplicationStatusActive { primary = source } } diff --git a/cmd/mysql/mysql.go b/cmd/db/db.go similarity index 99% rename from cmd/mysql/mysql.go rename to cmd/db/db.go index d8ac47811..a887b1318 100644 --- a/cmd/mysql/mysql.go +++ b/cmd/db/db.go @@ -1,4 +1,4 @@ -package mysql +package db import ( "context" diff --git a/cmd/healthcheck/main.go b/cmd/healthcheck/main.go index 3f11eed3e..cac0020d9 100644 --- a/cmd/healthcheck/main.go +++ b/cmd/healthcheck/main.go @@ -13,10 +13,10 @@ import ( "github.com/pkg/errors" apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" - database "github.com/percona/percona-server-mysql-operator/cmd/mysql" + database "github.com/percona/percona-server-mysql-operator/cmd/db" + mysqldb "github.com/percona/percona-server-mysql-operator/pkg/db" "github.com/percona/percona-server-mysql-operator/pkg/k8s" "github.com/percona/percona-server-mysql-operator/pkg/mysql" - "github.com/percona/percona-server-mysql-operator/pkg/replicator" ) func main() { @@ -133,7 +133,7 @@ func checkReadinessGR(ctx context.Context) error { return errors.Wrap(err, "get member state") } - if state != replicator.MemberStateOnline { + if state != mysqldb.MemberStateOnline { return errors.Errorf("Member state: %s", state) } From f99c3499349bd696fef36daabdf7245ba5accac2 Mon Sep 17 00:00:00 2001 From: Viacheslav Sarzhan Date: Mon, 8 Jan 2024 20:02:10 +0200 Subject: [PATCH 09/12] fix go-licenses and golicense --- e2e-tests/license/compare/go-licenses | 1 - e2e-tests/license/compare/golicense | 1 - 2 files changed, 2 deletions(-) diff --git a/e2e-tests/license/compare/go-licenses b/e2e-tests/license/compare/go-licenses index 9cff4042d..bfe5cc51f 100644 --- a/e2e-tests/license/compare/go-licenses +++ b/e2e-tests/license/compare/go-licenses @@ -3,4 +3,3 @@ BSD-2-Clause BSD-3-Clause ISC MIT -MPL-2.0 diff --git a/e2e-tests/license/compare/golicense b/e2e-tests/license/compare/golicense index 64653f306..db7c44349 100644 --- a/e2e-tests/license/compare/golicense +++ b/e2e-tests/license/compare/golicense @@ -3,4 +3,3 @@ BSD 2-Clause "Simplified" License BSD 3-Clause "New" or "Revised" License ISC License MIT License -Mozilla Public License 2.0 From 52161a20a9b8fea5f1c4cd84977ec8cbc9acc959 Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Tue, 9 Jan 2024 11:08:41 +0100 Subject: [PATCH 10/12] Don't return error from GetGroupReplicationReplicas if query returns sql.ErrNoRows. --- pkg/db/replication.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/db/replication.go b/pkg/db/replication.go index 8ec7dee26..f5a04b1f3 100644 --- a/pkg/db/replication.go +++ b/pkg/db/replication.go @@ -140,7 +140,7 @@ func (m *ReplicationDBManager) GetGroupReplicationReplicas(ctx context.Context) }{} err := m.query(ctx, "SELECT MEMBER_HOST as host FROM replication_group_members WHERE MEMBER_ROLE='SECONDARY' AND MEMBER_STATE='ONLINE'", &rows) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, errors.Wrap(err, "query replicas") } From 8831094897ddce773e3db6ed3000b3a890470134 Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Tue, 16 Jan 2024 10:09:54 +0100 Subject: [PATCH 11/12] Refactor getting topology. --- pkg/controller/psbackup/controller.go | 70 ++----------------------- pkg/controller/psbackup/topology.go | 75 +++++++++++++++++++++++++++ pkg/db/users.go | 10 ++-- 3 files changed, 85 insertions(+), 70 deletions(-) create mode 100644 pkg/controller/psbackup/topology.go diff --git a/pkg/controller/psbackup/controller.go b/pkg/controller/psbackup/controller.go index bd5c44e65..dbb24c966 100644 --- a/pkg/controller/psbackup/controller.go +++ b/pkg/controller/psbackup/controller.go @@ -42,10 +42,7 @@ import ( apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" - "github.com/percona/percona-server-mysql-operator/pkg/db" "github.com/percona/percona-server-mysql-operator/pkg/k8s" - "github.com/percona/percona-server-mysql-operator/pkg/mysql" - "github.com/percona/percona-server-mysql-operator/pkg/orchestrator" "github.com/percona/percona-server-mysql-operator/pkg/platform" "github.com/percona/percona-server-mysql-operator/pkg/secret" "github.com/percona/percona-server-mysql-operator/pkg/xtrabackup" @@ -321,79 +318,22 @@ func (r *PerconaServerMySQLBackupReconciler) getBackupSource(ctx context.Context return "", errors.Wrap(err, "get operator password") } - top, err := r.getTopology(ctx, cluster, operatorPass) + top, err := getDBTopology(ctx, r.Client, r.ClientCmd, cluster, operatorPass) if err != nil { return "", errors.Wrap(err, "get topology") } var source string - if len(top.Replicas) < 1 { - source = top.Primary - log.Info("no replicas found, using primary as the backup source", "primary", top.Primary) + if len(top.replicas) < 1 { + source = top.primary + log.Info("no replicas found, using primary as the backup source", "primary", top.primary) } else { - source = top.Replicas[0] + source = top.replicas[0] } return source, nil } -type Topology struct { - Primary string - Replicas []string -} - -func (r *PerconaServerMySQLBackupReconciler) getTopology(ctx context.Context, cluster *apiv1alpha1.PerconaServerMySQL, operatorPass string) (Topology, error) { - switch cluster.Spec.MySQL.ClusterType { - case apiv1alpha1.ClusterTypeGR: - firstPod := &corev1.Pod{} - nn := types.NamespacedName{Namespace: cluster.Namespace, Name: mysql.PodName(cluster, 0)} - if err := r.Client.Get(ctx, nn, firstPod); err != nil { - return Topology{}, err - } - - fqdn := mysql.FQDN(cluster, 0) - - rm := db.NewReplicationManager(firstPod, r.ClientCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) - - replicas, err := rm.GetGroupReplicationReplicas(ctx) - if err != nil { - return Topology{}, errors.Wrap(err, "get group-replication replicas") - } - - primary, err := rm.GetGroupReplicationPrimary(ctx) - if err != nil { - return Topology{}, errors.Wrap(err, "get group-replication primary") - } - return Topology{ - Primary: primary, - Replicas: replicas, - }, nil - case apiv1alpha1.ClusterTypeAsync: - pod := &corev1.Pod{} - nn := types.NamespacedName{Namespace: cluster.Namespace, Name: orchestrator.PodName(cluster, 0)} - if err := r.Client.Get(ctx, nn, pod); err != nil { - return Topology{}, err - } - - primary, err := orchestrator.ClusterPrimaryExec(ctx, r.ClientCmd, pod, cluster.ClusterHint()) - - if err != nil { - return Topology{}, errors.Wrap(err, "get primary") - } - - replicas := make([]string, 0, len(primary.Replicas)) - for _, r := range primary.Replicas { - replicas = append(replicas, r.Hostname) - } - return Topology{ - Primary: primary.Key.Hostname, - Replicas: replicas, - }, nil - default: - return Topology{}, errors.New("unknown cluster type") - } -} - const finalizerDeleteBackup = "delete-backup" func (r *PerconaServerMySQLBackupReconciler) checkFinalizers(ctx context.Context, cr *apiv1alpha1.PerconaServerMySQLBackup) { diff --git a/pkg/controller/psbackup/topology.go b/pkg/controller/psbackup/topology.go new file mode 100644 index 000000000..02087120d --- /dev/null +++ b/pkg/controller/psbackup/topology.go @@ -0,0 +1,75 @@ +package psbackup + +import ( + "context" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/clientcmd" + "github.com/percona/percona-server-mysql-operator/pkg/db" + "github.com/percona/percona-server-mysql-operator/pkg/mysql" + "github.com/percona/percona-server-mysql-operator/pkg/orchestrator" +) + +// topology represents the topology of the database cluster. +type topology struct { + primary string + replicas []string +} + +// getDBTopology returns the topology of the database cluster. +func getDBTopology(ctx context.Context, cli client.Client, cliCmd clientcmd.Client, cluster *apiv1alpha1.PerconaServerMySQL, operatorPass string) (topology, error) { + switch cluster.Spec.MySQL.ClusterType { + case apiv1alpha1.ClusterTypeGR: + firstPod := &corev1.Pod{} + nn := types.NamespacedName{Namespace: cluster.Namespace, Name: mysql.PodName(cluster, 0)} + if err := cli.Get(ctx, nn, firstPod); err != nil { + return topology{}, err + } + + fqdn := mysql.FQDN(cluster, 0) + + rm := db.NewReplicationManager(firstPod, cliCmd, apiv1alpha1.UserOperator, operatorPass, fqdn) + + replicas, err := rm.GetGroupReplicationReplicas(ctx) + if err != nil { + return topology{}, errors.Wrap(err, "get group-replication replicas") + } + + primary, err := rm.GetGroupReplicationPrimary(ctx) + if err != nil { + return topology{}, errors.Wrap(err, "get group-replication primary") + } + return topology{ + primary: primary, + replicas: replicas, + }, nil + case apiv1alpha1.ClusterTypeAsync: + pod := &corev1.Pod{} + nn := types.NamespacedName{Namespace: cluster.Namespace, Name: orchestrator.PodName(cluster, 0)} + if err := cli.Get(ctx, nn, pod); err != nil { + return topology{}, err + } + + primary, err := orchestrator.ClusterPrimaryExec(ctx, cliCmd, pod, cluster.ClusterHint()) + + if err != nil { + return topology{}, errors.Wrap(err, "get primary") + } + + replicas := make([]string, 0, len(primary.Replicas)) + for _, r := range primary.Replicas { + replicas = append(replicas, r.Hostname) + } + return topology{ + primary: primary.Key.Hostname, + replicas: replicas, + }, nil + default: + return topology{}, errors.New("unknown cluster type") + } +} diff --git a/pkg/db/users.go b/pkg/db/users.go index eaf3012a5..64e167769 100644 --- a/pkg/db/users.go +++ b/pkg/db/users.go @@ -13,16 +13,16 @@ import ( "github.com/percona/percona-server-mysql-operator/pkg/mysql" ) -type UserDBManager struct { +type UserManager struct { db *db } -func NewUserManager(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) *UserDBManager { - return &UserDBManager{db: newDB(pod, cliCmd, user, pass, host)} +func NewUserManager(pod *corev1.Pod, cliCmd clientcmd.Client, user apiv1alpha1.SystemUser, pass, host string) *UserManager { + return &UserManager{db: newDB(pod, cliCmd, user, pass, host)} } // UpdateUserPasswords updates user passwords but retains the current password using Dual Password feature of MySQL 8 -func (m *UserDBManager) UpdateUserPasswords(ctx context.Context, users []mysql.User) error { +func (m *UserManager) UpdateUserPasswords(ctx context.Context, users []mysql.User) error { for _, user := range users { for _, host := range user.Hosts { q := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s' RETAIN CURRENT PASSWORD", user.Username, host, escapePass(user.Password)) @@ -44,7 +44,7 @@ func (m *UserDBManager) UpdateUserPasswords(ctx context.Context, users []mysql.U } // DiscardOldPasswords discards old passwords of givens users -func (m *UserDBManager) DiscardOldPasswords(ctx context.Context, users []mysql.User) error { +func (m *UserManager) DiscardOldPasswords(ctx context.Context, users []mysql.User) error { for _, user := range users { for _, host := range user.Hosts { q := fmt.Sprintf("ALTER USER '%s'@'%s' DISCARD OLD PASSWORD", user.Username, host) From b184ccf19e07b555ab31549e8d9c14ac4518ff33 Mon Sep 17 00:00:00 2001 From: Inel Pandzic Date: Tue, 16 Jan 2024 10:36:31 +0100 Subject: [PATCH 12/12] Remove sql command from the logs. --- pkg/db/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/db/db.go b/pkg/db/db.go index 4aa9a5f06..0d9636996 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -35,7 +35,7 @@ func (d *db) exec(ctx context.Context, stm string, stdout, stderr *bytes.Buffer) if err != nil { sout := sensitiveRegexp.ReplaceAllString(stdout.String(), ":*****@") serr := sensitiveRegexp.ReplaceAllString(stderr.String(), ":*****@") - return errors.Wrapf(err, "run %s, stdout: %s, stderr: %s", cmd, sout, serr) + return errors.Wrapf(err, "stdout: %s, stderr: %s", sout, serr) } if strings.Contains(stderr.String(), "ERROR") {