github.com/percona/percona-xtradb-cluster-operator@v1.14.0/pkg/pxc/queries/queries.go (about)

     1  package queries
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/pkg/errors"
    10  
    11  	"github.com/go-sql-driver/mysql"
    12  	corev1 "k8s.io/api/core/v1"
    13  	"k8s.io/apimachinery/pkg/types"
    14  	"sigs.k8s.io/controller-runtime/pkg/client"
    15  )
    16  
    17  type ReplicationStatus int8
    18  
    19  const (
    20  	ReplicationStatusActive ReplicationStatus = iota
    21  	ReplicationStatusError
    22  	ReplicationStatusNotInitiated
    23  )
    24  
    25  const (
    26  	WriterHostgroup = "writer_hostgroup"
    27  	ReaderHostgroup = "reader_hostgroup"
    28  )
    29  
    30  // value of writer group is hardcoded in ProxySQL config inside docker image
    31  // https://github.com/percona/percona-docker/blob/pxc-operator-1.3.0/proxysql/dockerdir/etc/proxysql-admin.cnf#L23
    32  const writerID = 11
    33  
    34  type Database struct {
    35  	db *sql.DB
    36  }
    37  
    38  type ReplicationConfig struct {
    39  	Source             ReplicationChannelSource
    40  	SourceRetryCount   uint
    41  	SourceConnectRetry uint
    42  	SSL                bool
    43  	SSLSkipVerify      bool
    44  	CA                 string
    45  }
    46  
    47  type ReplicationChannelSource struct {
    48  	Name   string
    49  	Host   string
    50  	Port   int
    51  	Weight int
    52  }
    53  
    54  var ErrNotFound = errors.New("not found")
    55  
    56  func New(client client.Client, namespace, secretName, user, host string, port int32, timeout int32) (Database, error) {
    57  	secretObj := corev1.Secret{}
    58  	err := client.Get(context.TODO(),
    59  		types.NamespacedName{
    60  			Namespace: namespace,
    61  			Name:      secretName,
    62  		},
    63  		&secretObj,
    64  	)
    65  	if err != nil {
    66  		return Database{}, err
    67  	}
    68  
    69  	timeoutStr := fmt.Sprintf("%ds", timeout)
    70  	config := mysql.NewConfig()
    71  	config.User = user
    72  	config.Passwd = string(secretObj.Data[user])
    73  	config.Net = "tcp"
    74  	config.DBName = "mysql"
    75  	config.Addr = fmt.Sprintf("%s:%d", host, port)
    76  	config.Params = map[string]string{
    77  		"interpolateParams": "true",
    78  		"timeout":           timeoutStr,
    79  		"readTimeout":       timeoutStr,
    80  		"writeTimeout":      timeoutStr,
    81  		"tls":               "preferred",
    82  	}
    83  
    84  	db, err := sql.Open("mysql", config.FormatDSN())
    85  	if err != nil {
    86  		return Database{}, err
    87  	}
    88  
    89  	err = db.Ping()
    90  	if err != nil {
    91  		return Database{}, err
    92  	}
    93  
    94  	return Database{
    95  		db: db,
    96  	}, nil
    97  }
    98  
    99  func (p *Database) CurrentReplicationChannels() ([]string, error) {
   100  	rows, err := p.db.Query(`SELECT DISTINCT(Channel_name) from replication_asynchronous_connection_failover`)
   101  	if err != nil {
   102  		if errors.Is(err, sql.ErrNoRows) {
   103  			return nil, nil
   104  		}
   105  		return nil, errors.Wrap(err, "select current replication channels")
   106  	}
   107  
   108  	defer rows.Close()
   109  
   110  	result := make([]string, 0)
   111  	for rows.Next() {
   112  		src := ""
   113  		err = rows.Scan(&src)
   114  		if err != nil {
   115  			return nil, errors.Wrap(err, "scan channel name")
   116  		}
   117  		result = append(result, src)
   118  	}
   119  	return result, nil
   120  }
   121  
   122  func (p *Database) ChangeChannelPassword(channel, password string) error {
   123  	tx, err := p.db.Begin()
   124  	if err != nil {
   125  		return errors.Wrap(err, "start transaction for updating channel password")
   126  	}
   127  	_, err = tx.Exec(`STOP REPLICA IO_THREAD FOR CHANNEL ?`, channel)
   128  	if err != nil {
   129  		errT := tx.Rollback()
   130  		if errT != nil {
   131  			return errors.Wrapf(err, "rollback STOP REPLICA IO_THREAD FOR CHANNEL %s", channel)
   132  		}
   133  		return errors.Wrapf(err, "stop replication IO thread for channel %s", channel)
   134  	}
   135  	_, err = tx.Exec(`CHANGE REPLICATION SOURCE TO SOURCE_PASSWORD=? FOR CHANNEL ?`, password, channel)
   136  	if err != nil {
   137  		errT := tx.Rollback()
   138  		if errT != nil {
   139  			return errors.Wrapf(err, "rollback CHANGE SOURCE_PASSWORD FOR CHANNEL %s", channel)
   140  		}
   141  		return errors.Wrapf(err, "change master password for channel %s", channel)
   142  	}
   143  	_, err = tx.Exec(`START REPLICA IO_THREAD FOR CHANNEL ?`, channel)
   144  	if err != nil {
   145  		errT := tx.Rollback()
   146  		if errT != nil {
   147  			return errors.Wrapf(err, "rollback START REPLICA IO_THREAD FOR CHANNEL %s", channel)
   148  		}
   149  		return errors.Wrapf(err, "start io thread for channel %s", channel)
   150  	}
   151  	return tx.Commit()
   152  }
   153  
   154  func (p *Database) ShowReplicaStatus(ctx context.Context, channel string) (map[string]string, error) {
   155  	rows, err := p.db.Query(`SHOW REPLICA STATUS FOR CHANNEL ?`, channel)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	defer rows.Close()
   160  	columns, err := rows.Columns()
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	ok := rows.Next()
   165  	if !ok {
   166  		return make(map[string]string), nil
   167  	}
   168  
   169  	values := make([]any, 0, len(columns))
   170  	for range columns {
   171  		values = append(values, new([]byte))
   172  	}
   173  	status := make(map[string]string, len(columns))
   174  
   175  	if err := rows.Scan(values...); err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	for i, name := range columns {
   180  		ptr, ok := values[i].(*[]byte)
   181  		if !ok {
   182  			return nil, errors.Errorf("failed to convert %T to *[]byte: %s", values[i], name)
   183  		}
   184  		status[name] = string(*ptr)
   185  	}
   186  
   187  	return status, nil
   188  }
   189  
   190  func (p *Database) ReplicationStatus(ctx context.Context, channel string) (ReplicationStatus, error) {
   191  	statusMap, err := p.ShowReplicaStatus(ctx, channel)
   192  	if err != nil {
   193  		if strings.HasSuffix(err.Error(), "does not exist.") || errors.Is(err, sql.ErrNoRows) {
   194  			return ReplicationStatusNotInitiated, nil
   195  		}
   196  		return ReplicationStatusError, errors.Wrap(err, "get current replica status")
   197  	}
   198  
   199  	IORunning := statusMap["Replica_IO_Running"]
   200  	SQLRunning := statusMap["Replica_SQL_Running"]
   201  	LastErrNo := statusMap["Last_Errno"]
   202  	if IORunning == "Yes" && SQLRunning == "Yes" {
   203  		return ReplicationStatusActive, nil
   204  	}
   205  
   206  	if IORunning == "No" && SQLRunning == "No" && LastErrNo == "0" {
   207  		return ReplicationStatusNotInitiated, nil
   208  	}
   209  
   210  	return ReplicationStatusError, nil
   211  }
   212  
   213  func (p *Database) StopAllReplication() error {
   214  	_, err := p.db.Exec("STOP REPLICA")
   215  	return errors.Wrap(err, "failed to stop replication")
   216  }
   217  
   218  func (p *Database) AddReplicationSource(name, host string, port, weight int) error {
   219  	_, err := p.db.Exec("SELECT asynchronous_connection_failover_add_source(?, ?, ?, null, ?)", name, host, port, weight)
   220  	return errors.Wrap(err, "add replication source")
   221  }
   222  
   223  func (p *Database) ReplicationChannelSources(channelName string) ([]ReplicationChannelSource, error) {
   224  	rows, err := p.db.Query(`
   225          SELECT host,
   226                 port,
   227                 weight
   228          FROM   replication_asynchronous_connection_failover
   229          WHERE  channel_name = ?
   230      `, channelName)
   231  	if err != nil {
   232  		if errors.Is(err, sql.ErrNoRows) {
   233  			return nil, ErrNotFound
   234  		}
   235  		return nil, errors.Wrap(err, "get replication channels")
   236  	}
   237  	defer rows.Close()
   238  	result := make([]ReplicationChannelSource, 0)
   239  	for rows.Next() {
   240  		r := ReplicationChannelSource{}
   241  		err = rows.Scan(&r.Host, &r.Port, &r.Weight)
   242  		if err != nil {
   243  			return nil, errors.Wrap(err, "read replication channel info")
   244  		}
   245  		result = append(result, r)
   246  	}
   247  	return result, nil
   248  }
   249  
   250  func (p *Database) StopReplication(name string) error {
   251  	_, err := p.db.Exec("STOP REPLICA FOR CHANNEL ?", name)
   252  	return errors.Wrap(err, "stop replication for channel "+name)
   253  }
   254  
   255  func (p *Database) EnableReadonly() error {
   256  	_, err := p.db.Exec("SET GLOBAL READ_ONLY=1")
   257  	return errors.Wrap(err, "set global read_only param to 1")
   258  }
   259  
   260  func (p *Database) DisableReadonly() error {
   261  	_, err := p.db.Exec("SET GLOBAL READ_ONLY=0")
   262  	return errors.Wrap(err, "set global read_only param to 0")
   263  }
   264  
   265  func (p *Database) IsReadonly() (bool, error) {
   266  	readonly := 0
   267  	err := p.db.QueryRow("select @@read_only").Scan(&readonly)
   268  	return readonly == 1, errors.Wrap(err, "select global read_only param")
   269  }
   270  
   271  func (p *Database) StartReplication(replicaPass string, config ReplicationConfig, shouldGetMasterKey bool) error {
   272  	var ca string
   273  	var ssl int
   274  	if config.SSL {
   275  		ssl = 1
   276  		ca = config.CA
   277  	}
   278  
   279  	var sslVerify int
   280  	if !config.SSLSkipVerify {
   281  		sslVerify = 1
   282  	}
   283  
   284  	_, err := p.db.Exec(`
   285  	CHANGE REPLICATION SOURCE TO
   286  		SOURCE_USER='replication',
   287  		SOURCE_PASSWORD=?,
   288  		SOURCE_HOST=?,
   289  		SOURCE_PORT=?,
   290  		SOURCE_CONNECTION_AUTO_FAILOVER=1,
   291  		SOURCE_AUTO_POSITION=1,
   292  		SOURCE_RETRY_COUNT=?,
   293  		SOURCE_CONNECT_RETRY=?,
   294  		SOURCE_SSL=?,
   295  		SOURCE_SSL_CA=?,
   296  		SOURCE_SSL_VERIFY_SERVER_CERT=?
   297  		FOR CHANNEL ?
   298  `, replicaPass, config.Source.Host, config.Source.Port, config.SourceRetryCount, config.SourceConnectRetry, ssl, ca, sslVerify, config.Source.Name)
   299  	if err != nil {
   300  		return errors.Wrapf(err, "change source for channel %s", config.Source.Name)
   301  	}
   302  
   303  	if shouldGetMasterKey {
   304  		_, err = p.db.Exec(`CHANGE MASTER TO GET_MASTER_PUBLIC_KEY=1 FOR CHANNEL ?`, config.Source.Name)
   305  		if err != nil {
   306  			return errors.Wrapf(err, "change master to GET_MASTER_PUBLIC_KEY for channel %s", config.Source.Name)
   307  		}
   308  	}
   309  
   310  	_, err = p.db.Exec(`START REPLICA FOR CHANNEL ?`, config.Source.Name)
   311  	return errors.Wrapf(err, "start replica for source %s", config.Source.Name)
   312  }
   313  
   314  func (p *Database) DeleteReplicationSource(name, host string, port int) error {
   315  	_, err := p.db.Exec("SELECT asynchronous_connection_failover_delete_source(?, ?, ?, null)", name, host, port)
   316  	return errors.Wrap(err, "delete replication source "+name)
   317  }
   318  
   319  func (p *Database) ProxySQLInstanceStatus(host string) ([]string, error) {
   320  	rows, err := p.db.Query("SELECT status FROM mysql_servers WHERE hostname LIKE ?;", host+"%")
   321  	if err != nil {
   322  		if errors.Is(err, sql.ErrNoRows) {
   323  			return nil, ErrNotFound
   324  		}
   325  		return nil, err
   326  	}
   327  
   328  	statuses := []string{}
   329  	for rows.Next() {
   330  		var status string
   331  
   332  		err := rows.Scan(&status)
   333  		if err != nil {
   334  			return nil, err
   335  		}
   336  
   337  		statuses = append(statuses, status)
   338  	}
   339  
   340  	return statuses, nil
   341  }
   342  
   343  func (p *Database) PresentInHostgroups(host string) (bool, error) {
   344  	hostgroups := []string{WriterHostgroup, ReaderHostgroup}
   345  	query := fmt.Sprintf(`SELECT COUNT(*) FROM mysql_servers
   346  		INNER JOIN mysql_galera_hostgroups ON hostgroup_id IN (%s)
   347  		WHERE hostname LIKE ? GROUP BY hostname`, strings.Join(hostgroups, ","))
   348  	var count int
   349  	err := p.db.QueryRow(query, host+"%").Scan(&count)
   350  	if err != nil {
   351  		if errors.Is(err, sql.ErrNoRows) {
   352  			return false, ErrNotFound
   353  		}
   354  		return false, err
   355  	}
   356  	if count != len(hostgroups) {
   357  		return false, nil
   358  	}
   359  	return true, nil
   360  }
   361  
   362  func (p *Database) PrimaryHost() (string, error) {
   363  	var host string
   364  	err := p.db.QueryRow("SELECT hostname FROM runtime_mysql_servers WHERE hostgroup_id = ?", writerID).Scan(&host)
   365  	if err != nil {
   366  		if errors.Is(err, sql.ErrNoRows) {
   367  			return "", ErrNotFound
   368  		}
   369  		return "", err
   370  	}
   371  
   372  	return host, nil
   373  }
   374  
   375  func (p *Database) Hostname() (string, error) {
   376  	var hostname string
   377  	err := p.db.QueryRow("SELECT @@hostname hostname").Scan(&hostname)
   378  	if err != nil {
   379  		if errors.Is(err, sql.ErrNoRows) {
   380  			return "", ErrNotFound
   381  		}
   382  		return "", err
   383  	}
   384  
   385  	return hostname, nil
   386  }
   387  
   388  func (p *Database) WsrepLocalStateComment() (string, error) {
   389  	var variable_name string
   390  	var value string
   391  
   392  	err := p.db.QueryRow("SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment'").Scan(&variable_name, &value)
   393  	if err != nil {
   394  		if err == sql.ErrNoRows {
   395  			return "", fmt.Errorf("variable was not found")
   396  		}
   397  		return "", err
   398  	}
   399  
   400  	return value, nil
   401  }
   402  
   403  func (p *Database) Version() (string, error) {
   404  	var version string
   405  
   406  	err := p.db.QueryRow("select @@VERSION;").Scan(&version)
   407  	if err != nil {
   408  		if err == sql.ErrNoRows {
   409  			return "", fmt.Errorf("variable was not found")
   410  		}
   411  		return "", err
   412  	}
   413  
   414  	return version, nil
   415  }
   416  
   417  func (p *Database) ReadVariable(variable string) (string, error) {
   418  	var varName string
   419  	var value string
   420  
   421  	err := p.db.QueryRow("SHOW VARIABLES LIKE ?", variable).Scan(&varName, &value)
   422  	if err != nil {
   423  		if err == sql.ErrNoRows {
   424  			return "", fmt.Errorf("variable was not found")
   425  		}
   426  		return "", err
   427  	}
   428  
   429  	return value, nil
   430  }
   431  
   432  func (p *Database) Close() error {
   433  	return p.db.Close()
   434  }