/
appconf.go
270 lines (219 loc) · 9.31 KB
/
appconf.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/*
Copyright 2018 Pressinfra SRL
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sidecar
import (
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"github.com/go-ini/ini"
"github.com/presslabs/mysql-operator/pkg/util/constants"
)
// RunConfigCommand generates my.cnf, client.cnf and 10-dynamic.cnf files.
// nolint: gocyclo
func RunConfigCommand(cfg *Config) error {
log.Info("configuring server", "host", cfg.Hostname)
var err error
if err = copyFile(mountConfigDir+"/my.cnf", configDir+"/my.cnf"); err != nil {
return fmt.Errorf("copy file my.cnf: %s", err)
}
if err = copyFile(mountConfigDir+"/"+shPreStop, configDir+"/"+shPreStop); err != nil {
return fmt.Errorf("copy file %s: %s", shPreStop, err)
}
if err = os.Mkdir(confDPath, os.FileMode(0755)); err != nil {
if !os.IsExist(err) {
return fmt.Errorf("error mkdir %s/conf.d: %s", configDir, err)
}
}
reportHost := cfg.FQDNForServer(cfg.ServerID())
var identityCFG, initCFG, clientCFG, heartbeatCFG *ini.File
// mysql server identity configs
if identityCFG, err = getIdentityConfigs(cfg.ServerID(), reportHost); err != nil {
return fmt.Errorf("failed to get dynamic configs: %s", err)
}
if err = identityCFG.SaveTo(path.Join(confDPath, "10-identity.cnf")); err != nil {
return fmt.Errorf("failed to save configs: %s", err)
}
// write initialization sql file. This file is the init-file used by MySQL to configure itself
var gtidPurged string
gtidPurged, err = readPurgedGTID()
if err != nil {
// not a fatal error, log it and continue
log.Info("error while reading PURGE GTID from xtrabackup_binlog_info", "error", err)
}
initFilePath := path.Join(confDPath, "operator-init.sql")
if err = ioutil.WriteFile(initFilePath, initFileQuery(cfg, gtidPurged), 0644); err != nil {
return fmt.Errorf("failed to write init-file: %s", err)
}
// mysql server utility user configs
if initCFG, err = getInitFileConfigs(initFilePath); err != nil {
return fmt.Errorf("failed to configure init file: %s", err)
}
if err = initCFG.SaveTo(path.Join(confDPath, "10-init-file.cnf")); err != nil {
return fmt.Errorf("failed to configure init file: %s", err)
}
// mysql client connect credentials
if clientCFG, err = getClientConfigs(cfg.OperatorUser, cfg.OperatorPassword); err != nil {
return fmt.Errorf("failed to get client configs: %s", err)
}
if err = clientCFG.SaveTo(confClientPath); err != nil {
return fmt.Errorf("failed to save configs: %s", err)
}
// mysql heartbeat connect credentials
if heartbeatCFG, err = getClientConfigs(cfg.HeartBeatUser, cfg.HeartBeatPassword); err != nil {
return fmt.Errorf("failed to get heartbeat configs: %s", err)
}
if err = heartbeatCFG.SaveTo(confHeartbeatPath); err != nil {
return fmt.Errorf("failed to save heartbeat configs: %s", err)
}
return nil
}
func getClientConfigs(user, pass string) (*ini.File, error) {
cfg := ini.Empty()
// create client.cnf file
client := cfg.Section("client")
if _, err := client.NewKey("host", "127.0.0.1"); err != nil {
return nil, err
}
if _, err := client.NewKey("port", mysqlPort); err != nil {
return nil, err
}
if _, err := client.NewKey("user", user); err != nil {
return nil, err
}
if _, err := client.NewKey("password", pass); err != nil {
return nil, err
}
return cfg, nil
}
func getIdentityConfigs(id int, reportHost string) (*ini.File, error) {
cfg := ini.Empty()
mysqld := cfg.Section("mysqld")
if _, err := mysqld.NewKey("server-id", strconv.Itoa(id)); err != nil {
return nil, err
}
if _, err := mysqld.NewKey("report-host", reportHost); err != nil {
return nil, err
}
return cfg, nil
}
func getInitFileConfigs(filePath string) (*ini.File, error) {
cfg := ini.Empty()
mysqld := cfg.Section("mysqld")
if _, err := mysqld.NewKey("init-file", filePath); err != nil {
return nil, err
}
return cfg, nil
}
func initFileQuery(cfg *Config, gtidPurged string) []byte {
queries := []string{
"SET @@SESSION.SQL_LOG_BIN = 0",
}
// set server as read only
// https://github.com/presslabs/mysql-operator/issues/509
queries = append(queries, "SET GLOBAL READ_ONLY = 1")
// create operator database because GRANTS need it
queries = append(queries, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", toolsDbName))
// configure operator utility user
queries = append(queries, createUserQuery(cfg.OperatorUser, cfg.OperatorPassword, "%",
[]string{"SUPER", "SHOW DATABASES", "PROCESS", "RELOAD", "CREATE", "SELECT"}, "*.*",
[]string{"ALL"}, fmt.Sprintf("%s.*", toolsDbName))...)
// configure orchestrator user
queries = append(queries, createUserQuery(cfg.OrchestratorUser, cfg.OrchestratorPassword, "%",
[]string{"SUPER", "PROCESS", "REPLICATION SLAVE", "REPLICATION CLIENT", "RELOAD"}, "*.*",
[]string{"SELECT"}, "mysql.slave_master_info",
[]string{"SELECT", "CREATE"}, fmt.Sprintf("%s.%s", toolsDbName, toolsHeartbeatTableName))...)
// configure replication user
replPermissions := []string{"SELECT", "PROCESS", "RELOAD", "LOCK TABLES", "REPLICATION CLIENT", "REPLICATION SLAVE"}
if cfg.MySQLVersion.Major == 8 {
// if it's a mysql 8 then the backup user needs BACKUP_ADMIN permissions to take backups
replPermissions = append(replPermissions, "BACKUP_ADMIN")
}
queries = append(queries, createUserQuery(cfg.ReplicationUser, cfg.ReplicationPassword, "%",
replPermissions, "*.*")...)
// configure metrics exporter user
queries = append(queries, createUserQuery(cfg.MetricsUser, cfg.MetricsPassword, "127.0.0.1",
[]string{"SELECT", "PROCESS", "REPLICATION CLIENT"}, "*.*",
[]string{"SELECT", "CREATE"}, fmt.Sprintf("%s.%s", toolsDbName, toolsHeartbeatTableName))...)
queries = append(queries, fmt.Sprintf("ALTER USER %s@'127.0.0.1' WITH MAX_USER_CONNECTIONS 3", cfg.MetricsUser))
// configure heartbeat user
// because of pt-heartbeat make sure not to have ALL or SUPER privileges:
// https://github.com/percona/percona-toolkit/blob/e85ce15ef24bc4614b4d2f13792fa73583d68f8e/bin/pt-heartbeat#L6433
queries = append(queries, createUserQuery(cfg.HeartBeatUser, cfg.HeartBeatPassword, "127.0.0.1",
[]string{"CREATE", "SELECT", "DELETE", "UPDATE", "INSERT"}, fmt.Sprintf("%s.%s", toolsDbName, toolsHeartbeatTableName),
[]string{"REPLICATION CLIENT"}, "*.*")...)
// the slave pod doesn't need to back up sys_operator.status, Xtrabackup might have some bugs that
// cause the table to be unclean. We can do this cleanup before the slave pod starts to avoid accidents.
if !cfg.IsFirstPodInSet() {
queries = append(queries, fmt.Sprintf("DROP TABLE IF EXISTS %s.%s",
constants.OperatorDbName, constants.OperatorStatusTableName))
}
// create the status table used by the operator to configure or to mask MySQL node ready
// CSV engine for this table can't be used because we use REPLACE statement that requires PRIMARY KEY or
// UNIQUE KEY index. Also, the table may exists (in case of pod restart) and should not be changed.
// NOTE: value column should be big enough to contain all GTIDs from xtrabackup_slave_info file
// nolint: gosec
queries = append(queries, fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %[1]s.%[2]s ("+
" name varchar(64) PRIMARY KEY,"+
" value varchar(8192) NOT NULL\n)",
constants.OperatorDbName, constants.OperatorStatusTableName))
// mark node as not configured at startup, the operator will mark it configured
// nolint: gosec
queries = append(queries, fmt.Sprintf("REPLACE INTO %s.%s VALUES ('%s', '0')",
constants.OperatorDbName, constants.OperatorStatusTableName, "configured"))
if len(gtidPurged) != 0 {
// if gtid is found in the backup then set it in the status table to be processed by the operator
// nolint: gosec
queries = append(queries, fmt.Sprintf(`REPLACE INTO %s.%s VALUES ('%s', '%s')`,
constants.OperatorDbName, constants.OperatorStatusTableName, "backup_gtid_purged", gtidPurged))
}
// if just recently the node was initialized from a backup then a RESET SLAVE ALL query should be ran
// to avoid not replicate from previous master.
if cfg.ShouldCloneFromBucket() {
queries = append(queries, "RESET SLAVE ALL")
}
if len(cfg.InitFileExtraSQL[0]) > 0 {
queries = append(queries, cfg.InitFileExtraSQL...)
}
return []byte(strings.Join(queries, ";\n") + ";\n")
}
func createUserQuery(name, pass, host string, rights ...interface{}) []string {
user := fmt.Sprintf("%s@'%s'", name, host)
queries := []string{
fmt.Sprintf("DROP USER IF EXISTS %s", user),
fmt.Sprintf("CREATE USER %s IDENTIFIED BY '%s'", user, pass),
}
if len(rights)%2 != 0 {
panic("not a good number of parameters")
}
grants := []string{}
for i := 0; i < len(rights); i += 2 {
var (
right []string
on string
ok bool
)
if right, ok = rights[i].([]string); !ok {
panic("[right] not a good parameter")
}
if on, ok = rights[i+1].(string); !ok {
panic("[on] not a good parameter")
}
grant := fmt.Sprintf("GRANT %s ON %s TO %s", strings.Join(right, ", "), on, user)
grants = append(grants, grant)
}
return append(queries, grants...)
}