github.com/percona/percona-xtradb-cluster-operator@v1.14.0/pkg/controller/pxc/users.go (about) 1 package pxc 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/sha256" 7 "encoding/json" 8 "fmt" 9 "strconv" 10 "strings" 11 12 "github.com/hashicorp/go-version" 13 "github.com/pkg/errors" 14 "golang.org/x/sync/errgroup" 15 corev1 "k8s.io/api/core/v1" 16 k8serrors "k8s.io/apimachinery/pkg/api/errors" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 "k8s.io/apimachinery/pkg/types" 19 "sigs.k8s.io/controller-runtime/pkg/client" 20 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 21 logf "sigs.k8s.io/controller-runtime/pkg/log" 22 23 api "github.com/percona/percona-xtradb-cluster-operator/pkg/apis/pxc/v1" 24 "github.com/percona/percona-xtradb-cluster-operator/pkg/pxc/app/statefulset" 25 "github.com/percona/percona-xtradb-cluster-operator/pkg/pxc/users" 26 ) 27 28 var mysql80 = version.Must(version.NewVersion("8.0.0")) 29 30 // https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html#priv_system-user 31 var privSystemUserAddedIn = version.Must(version.NewVersion("8.0.16")) 32 33 var PassNotPropagatedError = errors.New("password not yet propagated") 34 35 type userUpdateActions struct { 36 restartPXC bool 37 restartProxy bool 38 updateReplicationPass bool 39 } 40 41 type ReconcileUsersResult struct { 42 pxcAnnotations map[string]string 43 proxyAnnotations map[string]string 44 updateReplicationPassword bool 45 } 46 47 func (r *ReconcilePerconaXtraDBCluster) reconcileUsers(ctx context.Context, cr *api.PerconaXtraDBCluster) (*ReconcileUsersResult, error) { 48 log := logf.FromContext(ctx) 49 50 secrets := corev1.Secret{} 51 err := r.client.Get(context.TODO(), 52 types.NamespacedName{ 53 Namespace: cr.Namespace, 54 Name: cr.Spec.SecretsName, 55 }, 56 &secrets, 57 ) 58 if err != nil && k8serrors.IsNotFound(err) { 59 return nil, nil 60 } else if err != nil { 61 return nil, errors.Wrapf(err, "get sys users secret '%s'", cr.Spec.SecretsName) 62 } 63 64 internalSecretName := internalSecretsPrefix + cr.Name 65 66 internalSecrets := corev1.Secret{} 67 err = r.client.Get(context.TODO(), 68 types.NamespacedName{ 69 Namespace: cr.Namespace, 70 Name: internalSecretName, 71 }, 72 &internalSecrets, 73 ) 74 if err != nil && !k8serrors.IsNotFound(err) { 75 return nil, errors.Wrap(err, "get internal sys users secret") 76 } 77 78 if k8serrors.IsNotFound(err) { 79 is := secrets.DeepCopy() 80 is.ObjectMeta = metav1.ObjectMeta{ 81 Name: internalSecretName, 82 Namespace: cr.Namespace, 83 } 84 err = r.client.Create(context.TODO(), is) 85 if err != nil { 86 return nil, errors.Wrap(err, "create internal sys users secret") 87 } 88 return nil, nil 89 } 90 91 mysqlVersion := cr.Status.PXC.Version 92 if mysqlVersion == "" { 93 var err error 94 mysqlVersion, err = r.mysqlVersion(ctx, cr, statefulset.NewNode(cr)) 95 if err != nil { 96 if errors.Is(err, versionNotReadyErr) { 97 return nil, nil 98 } 99 return nil, errors.Wrap(err, "retrieving pxc version") 100 } 101 } 102 103 ver, err := version.NewVersion(mysqlVersion) 104 if err != nil { 105 return nil, errors.Wrap(err, "invalid pxc version") 106 } 107 108 var actions *userUpdateActions 109 if ver.GreaterThanOrEqual(mysql80) { 110 actions, err = r.updateUsers(ctx, cr, &secrets, &internalSecrets) 111 if err != nil { 112 return nil, errors.Wrap(err, "manage sys users") 113 } 114 } else { 115 actions, err = r.updateUsersWithoutDP(ctx, cr, &secrets, &internalSecrets) 116 if err != nil { 117 return nil, errors.Wrap(err, "manage sys users") 118 } 119 } 120 121 newSysData, err := json.Marshal(secrets.Data) 122 if err != nil { 123 return nil, errors.Wrap(err, "marshal sys secret data") 124 } 125 newSecretDataHash := sha256Hash(newSysData) 126 127 result := &ReconcileUsersResult{ 128 updateReplicationPassword: actions.updateReplicationPass, 129 } 130 131 if actions.restartProxy { 132 log.Info("Proxy pods will be restarted", "last-applied-secret", newSecretDataHash) 133 result.proxyAnnotations = map[string]string{"last-applied-secret": newSecretDataHash} 134 } 135 if actions.restartPXC { 136 log.Info("PXC pods will be restarted", "last-applied-secret", newSecretDataHash) 137 result.pxcAnnotations = map[string]string{"last-applied-secret": newSecretDataHash} 138 } 139 140 return result, nil 141 } 142 143 func sha256Hash(data []byte) string { 144 return fmt.Sprintf("%x", sha256.Sum256(data)) 145 } 146 147 func (r *ReconcilePerconaXtraDBCluster) updateUsers(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret) (*userUpdateActions, error) { 148 res := &userUpdateActions{} 149 150 for _, u := range users.UserNames { 151 if _, ok := secrets.Data[u]; !ok { 152 continue 153 } 154 155 switch u { 156 case users.Root: 157 if err := r.handleRootUser(ctx, cr, secrets, internalSecrets, res); err != nil { 158 return res, err 159 } 160 case users.Operator: 161 if err := r.handleOperatorUser(ctx, cr, secrets, internalSecrets, res); err != nil { 162 return res, err 163 } 164 case users.Monitor: 165 if err := r.handleMonitorUser(ctx, cr, secrets, internalSecrets, res); err != nil { 166 if errors.Is(err, PassNotPropagatedError) { 167 continue 168 } 169 return res, err 170 } 171 case users.Xtrabackup: 172 if err := r.handleXtrabackupUser(ctx, cr, secrets, internalSecrets, res); err != nil { 173 return res, err 174 } 175 case users.Replication: 176 if err := r.handleReplicationUser(ctx, cr, secrets, internalSecrets, res); err != nil { 177 return res, err 178 } 179 case users.ProxyAdmin: 180 if err := r.handleProxyadminUser(ctx, cr, secrets, internalSecrets, res); err != nil { 181 return res, err 182 } 183 case users.PMMServer, users.PMMServerKey: 184 if err := r.handlePMMUser(ctx, cr, secrets, internalSecrets, res); err != nil { 185 return res, err 186 } 187 } 188 } 189 190 return res, nil 191 } 192 193 func (r *ReconcilePerconaXtraDBCluster) handleRootUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, actions *userUpdateActions) error { 194 log := logf.FromContext(ctx) 195 196 user := &users.SysUser{ 197 Name: users.Root, 198 Pass: string(secrets.Data[users.Root]), 199 Hosts: []string{"localhost", "%"}, 200 } 201 202 if cr.Status.Status != api.AppStateReady && !r.invalidPasswordApplied(cr.Status) { 203 return nil 204 } 205 206 if err := r.updateUserPassExpirationPolicy(ctx, cr, internalSecrets, user); err != nil { 207 return err 208 } 209 210 passDiscarded, err := r.isOldPasswordDiscarded(cr, internalSecrets, user) 211 if err != nil { 212 return err 213 } 214 215 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && passDiscarded { 216 return nil 217 } 218 219 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && !passDiscarded { 220 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 221 if err != nil { 222 return errors.Wrap(err, "discard old pass") 223 } 224 log.Info("Old password discarded", "user", user.Name) 225 226 return nil 227 } 228 229 log.Info("Password changed, updating user", "user", user.Name) 230 231 err = r.updateUserPassWithRetention(cr, secrets, internalSecrets, user) 232 if err != nil { 233 return errors.Wrap(err, "update root users pass") 234 } 235 log.Info("Password updated", "user", user.Name) 236 237 if err := r.updateMySQLInitFile(ctx, cr, internalSecrets, user); err != nil { 238 return errors.Wrap(err, "update mysql init file") 239 } 240 241 err = r.syncPXCUsersWithProxySQL(ctx, cr) 242 if err != nil { 243 return errors.Wrap(err, "sync users") 244 } 245 246 orig := internalSecrets.DeepCopy() 247 internalSecrets.Data[user.Name] = secrets.Data[user.Name] 248 err = r.client.Patch(context.TODO(), internalSecrets, client.MergeFrom(orig)) 249 if err != nil { 250 return errors.Wrap(err, "update internal secrets root user password") 251 } 252 log.Info("Internal secrets updated", "user", user.Name) 253 254 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 255 if err != nil { 256 return errors.Wrap(err, "discard old password") 257 } 258 log.Info("Old password discarded", "user", user.Name) 259 260 return nil 261 } 262 263 func (r *ReconcilePerconaXtraDBCluster) handleOperatorUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, actions *userUpdateActions) error { 264 log := logf.FromContext(ctx) 265 266 user := &users.SysUser{ 267 Name: users.Operator, 268 Pass: string(secrets.Data[users.Operator]), 269 Hosts: []string{"%"}, 270 } 271 272 if cr.Status.PXC.Ready > 0 { 273 err := r.manageOperatorAdminUser(ctx, cr, secrets, internalSecrets) 274 if err != nil { 275 return errors.Wrap(err, "manage operator admin user") 276 } 277 278 if err := r.updateUserPassExpirationPolicy(ctx, cr, internalSecrets, user); err != nil { 279 return err 280 } 281 } 282 283 if cr.Status.Status != api.AppStateReady && !r.invalidPasswordApplied(cr.Status) { 284 return nil 285 } 286 287 passDiscarded, err := r.isOldPasswordDiscarded(cr, internalSecrets, user) 288 if err != nil { 289 return err 290 } 291 292 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && passDiscarded { 293 return nil 294 } 295 296 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && !passDiscarded { 297 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 298 if err != nil { 299 return errors.Wrap(err, "discard old pass") 300 } 301 log.Info("Old password discarded", "user", user.Name) 302 303 return nil 304 } 305 306 log.Info("Password changed, updating user", "user", user.Name) 307 308 err = r.updateUserPassWithRetention(cr, secrets, internalSecrets, user) 309 if err != nil { 310 return errors.Wrap(err, "update operator users pass") 311 } 312 log.Info("Password updated", "user", user.Name) 313 314 if err := r.updateMySQLInitFile(ctx, cr, internalSecrets, user); err != nil { 315 return errors.Wrap(err, "update mysql init file") 316 } 317 318 orig := internalSecrets.DeepCopy() 319 internalSecrets.Data[user.Name] = secrets.Data[user.Name] 320 err = r.client.Patch(context.TODO(), internalSecrets, client.MergeFrom(orig)) 321 if err != nil { 322 return errors.Wrap(err, "update internal users secrets operator user password") 323 } 324 log.Info("Internal secrets updated", "user", user.Name) 325 326 actions.restartProxy = true 327 328 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 329 if err != nil { 330 return errors.Wrap(err, "discard operator old password") 331 } 332 log.Info("Old password discarded", "user", user.Name) 333 334 return nil 335 } 336 337 // manageOperatorAdminUser ensures that operator user is always present and with the right privileges 338 func (r *ReconcilePerconaXtraDBCluster) manageOperatorAdminUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret) error { 339 log := logf.FromContext(ctx) 340 341 pass, existInSys := secrets.Data[users.Operator] 342 _, existInInternal := internalSecrets.Data[users.Operator] 343 if existInSys && !existInInternal { 344 if internalSecrets.Data == nil { 345 internalSecrets.Data = make(map[string][]byte) 346 } 347 internalSecrets.Data[users.Operator] = pass 348 return nil 349 } 350 if existInSys { 351 return nil 352 } 353 354 pass, err := generatePass() 355 if err != nil { 356 return errors.Wrap(err, "generate password") 357 } 358 addr := cr.Name + "-pxc." + cr.Namespace 359 if cr.CompareVersionWith("1.6.0") >= 0 { 360 addr = cr.Name + "-pxc-unready." + cr.Namespace + ":33062" 361 } 362 um, err := users.NewManager(addr, users.Root, string(secrets.Data[users.Root]), cr.Spec.PXC.ReadinessProbes.TimeoutSeconds) 363 if err != nil { 364 return errors.Wrap(err, "new users manager") 365 } 366 defer um.Close() 367 368 err = um.CreateOperatorUser(string(pass)) 369 if err != nil { 370 return errors.Wrap(err, "create operator user") 371 } 372 373 secrets.Data[users.Operator] = pass 374 internalSecrets.Data[users.Operator] = pass 375 376 err = r.client.Update(context.TODO(), secrets) 377 if err != nil { 378 return errors.Wrap(err, "update sys users secret") 379 } 380 err = r.client.Update(context.TODO(), internalSecrets) 381 if err != nil { 382 return errors.Wrap(err, "update internal users secret") 383 } 384 385 log.Info("User created and privileges granted", "user", users.Operator) 386 return nil 387 } 388 389 func (r *ReconcilePerconaXtraDBCluster) handleMonitorUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, actions *userUpdateActions) error { 390 log := logf.FromContext(ctx) 391 392 user := &users.SysUser{ 393 Name: users.Monitor, 394 Pass: string(secrets.Data[users.Monitor]), 395 Hosts: []string{"%"}, 396 } 397 398 if cr.Status.PXC.Ready > 0 { 399 if err := r.updateUserPassExpirationPolicy(ctx, cr, internalSecrets, user); err != nil { 400 return err 401 } 402 403 um, err := getUserManager(cr, internalSecrets) 404 if err != nil { 405 return err 406 } 407 defer um.Close() 408 409 if cr.CompareVersionWith("1.6.0") >= 0 { 410 err := r.updateMonitorUserGrant(ctx, cr, internalSecrets, um) 411 if err != nil { 412 return errors.Wrap(err, "update monitor user grant") 413 } 414 } 415 416 if cr.CompareVersionWith("1.10.0") >= 0 { 417 mysqlVersion := cr.Status.PXC.Version 418 if mysqlVersion == "" { 419 var err error 420 mysqlVersion, err = r.mysqlVersion(ctx, cr, statefulset.NewNode(cr)) 421 if err != nil { 422 if errors.Is(err, versionNotReadyErr) { 423 return nil 424 } 425 return errors.Wrap(err, "retrieving pxc version") 426 } 427 } 428 429 if mysqlVersion != "" { 430 ver, err := version.NewVersion(mysqlVersion) 431 if err != nil { 432 return errors.Wrap(err, "invalid pxc version") 433 } 434 435 if !ver.LessThan(privSystemUserAddedIn) { 436 if err := r.grantMonitorUserPrivilege(ctx, cr, internalSecrets, um); err != nil { 437 return errors.Wrap(err, "monitor user grant system privilege") 438 } 439 } 440 } 441 } 442 } 443 444 if cr.Status.Status != api.AppStateReady && !r.invalidPasswordApplied(cr.Status) { 445 return nil 446 } 447 448 passDiscarded, err := r.isOldPasswordDiscarded(cr, internalSecrets, user) 449 if err != nil { 450 return err 451 } 452 453 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && passDiscarded { 454 return nil 455 } 456 457 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && !passDiscarded { 458 log.Info("Password updated but old one not discarded", "user", user.Name) 459 460 passPropagated, err := r.isPassPropagated(cr, user) 461 if err != nil { 462 return errors.Wrap(err, "is password propagated") 463 } 464 if !passPropagated { 465 return PassNotPropagatedError 466 } 467 468 actions.restartProxy = true 469 if cr.Spec.PMM != nil && cr.Spec.PMM.IsEnabled(internalSecrets) { 470 actions.restartPXC = true 471 } 472 473 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 474 if err != nil { 475 return errors.Wrap(err, "discard old pass") 476 } 477 log.Info("Old password discarded", "user", user.Name) 478 479 return nil 480 } 481 482 log.Info("Password changed, updating user", "user", user.Name) 483 484 err = r.updateUserPassWithRetention(cr, secrets, internalSecrets, user) 485 if err != nil { 486 return errors.Wrap(err, "update monitor users pass") 487 } 488 log.Info("Password updated", "user", user.Name) 489 490 if err := r.updateMySQLInitFile(ctx, cr, internalSecrets, user); err != nil { 491 return errors.Wrap(err, "update mysql init file") 492 } 493 494 if cr.Spec.ProxySQLEnabled() { 495 err := r.updateProxyUser(cr, internalSecrets, user) 496 if err != nil { 497 return errors.Wrap(err, "update monitor users pass") 498 } 499 log.Info("Proxy user updated", "user", user.Name) 500 } 501 502 actions.restartProxy = true 503 if cr.Spec.PMM != nil && cr.Spec.PMM.IsEnabled(internalSecrets) { 504 actions.restartPXC = true 505 } 506 507 orig := internalSecrets.DeepCopy() 508 internalSecrets.Data[user.Name] = secrets.Data[user.Name] 509 err = r.client.Patch(context.TODO(), internalSecrets, client.MergeFrom(orig)) 510 if err != nil { 511 return errors.Wrap(err, "update internal users secrets monitor user password") 512 } 513 log.Info("Internal secrets updated", "user", user.Name) 514 515 passPropagated, err := r.isPassPropagated(cr, user) 516 if err != nil { 517 return errors.Wrap(err, "is password propagated") 518 } 519 if !passPropagated { 520 return PassNotPropagatedError 521 } 522 523 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 524 if err != nil { 525 return errors.Wrap(err, "discard monitor old password") 526 } 527 log.Info("Old password discarded", "user", user.Name) 528 529 return nil 530 } 531 532 func (r *ReconcilePerconaXtraDBCluster) updateMonitorUserGrant(ctx context.Context, cr *api.PerconaXtraDBCluster, internalSysSecretObj *corev1.Secret, um *users.Manager) error { 533 log := logf.FromContext(ctx) 534 535 annotationName := "grant-for-1.6.0-monitor-user" 536 if internalSysSecretObj.Annotations[annotationName] == "done" { 537 return nil 538 } 539 540 err := um.Update160MonitorUserGrant(string(internalSysSecretObj.Data[users.Monitor])) 541 if err != nil { 542 return errors.Wrap(err, "update monitor grant") 543 } 544 545 if internalSysSecretObj.Annotations == nil { 546 internalSysSecretObj.Annotations = make(map[string]string) 547 } 548 549 internalSysSecretObj.Annotations[annotationName] = "done" 550 err = r.client.Update(context.TODO(), internalSysSecretObj) 551 if err != nil { 552 return errors.Wrap(err, "update internal sys users secret annotation") 553 } 554 555 log.Info("User monitor: granted privileges") 556 return nil 557 } 558 559 func (r *ReconcilePerconaXtraDBCluster) handleXtrabackupUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, actions *userUpdateActions) error { 560 log := logf.FromContext(ctx) 561 562 user := &users.SysUser{ 563 Name: users.Xtrabackup, 564 Pass: string(secrets.Data[users.Xtrabackup]), 565 Hosts: []string{"localhost"}, 566 } 567 568 if cr.CompareVersionWith("1.7.0") >= 0 { 569 user.Hosts = []string{"%"} 570 } 571 572 if cr.Status.PXC.Ready > 0 { 573 if err := r.updateUserPassExpirationPolicy(ctx, cr, internalSecrets, user); err != nil { 574 return err 575 } 576 577 if cr.CompareVersionWith("1.7.0") >= 0 { 578 // xtrabackup user need more grants for work in version more then 1.6.0 579 err := r.updateXtrabackupUserGrant(ctx, cr, internalSecrets) 580 if err != nil { 581 return errors.Wrap(err, "update xtrabackup user grant") 582 } 583 } 584 } 585 586 if cr.Status.Status != api.AppStateReady && !r.invalidPasswordApplied(cr.Status) { 587 return nil 588 } 589 590 passDiscarded, err := r.isOldPasswordDiscarded(cr, internalSecrets, user) 591 if err != nil { 592 return err 593 } 594 595 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && passDiscarded { 596 return nil 597 } 598 599 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && !passDiscarded { 600 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 601 if err != nil { 602 return errors.Wrap(err, "discard old pass") 603 } 604 log.Info("Old password discarded", "user", user.Name) 605 606 return nil 607 } 608 609 log.Info("Password changed, updating user", "user", user.Name) 610 611 err = r.updateUserPassWithRetention(cr, secrets, internalSecrets, user) 612 if err != nil { 613 return errors.Wrap(err, "update xtrabackup users pass") 614 } 615 log.Info("Password updated", "user", user.Name) 616 617 if err := r.updateMySQLInitFile(ctx, cr, internalSecrets, user); err != nil { 618 return errors.Wrap(err, "update mysql init file") 619 } 620 621 orig := internalSecrets.DeepCopy() 622 internalSecrets.Data[user.Name] = secrets.Data[user.Name] 623 err = r.client.Patch(context.TODO(), internalSecrets, client.MergeFrom(orig)) 624 if err != nil { 625 return errors.Wrap(err, "update internal users secrets xtrabackup user password") 626 } 627 log.Info("Internal secrets updated", "user", user.Name) 628 629 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 630 if err != nil { 631 return errors.Wrap(err, "discard xtrabackup old pass") 632 } 633 log.Info("Old password discarded", "user", user.Name) 634 635 actions.restartPXC = true 636 return nil 637 } 638 639 func (r *ReconcilePerconaXtraDBCluster) updateXtrabackupUserGrant(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets *corev1.Secret) error { 640 log := logf.FromContext(ctx) 641 642 annotationName := "grant-for-1.7.0-xtrabackup-user" 643 if secrets.Annotations[annotationName] == "done" { 644 return nil 645 } 646 647 um, err := getUserManager(cr, secrets) 648 if err != nil { 649 return err 650 } 651 defer um.Close() 652 653 err = um.Update170XtrabackupUser(string(secrets.Data[users.Xtrabackup])) 654 if err != nil { 655 return errors.Wrap(err, "update xtrabackup grant") 656 } 657 658 if secrets.Annotations == nil { 659 secrets.Annotations = make(map[string]string) 660 } 661 662 secrets.Annotations[annotationName] = "done" 663 err = r.client.Update(context.TODO(), secrets) 664 if err != nil { 665 return errors.Wrap(err, "update internal sys users secret annotation") 666 } 667 668 log.Info("User xtrabackup: granted privileges") 669 return nil 670 } 671 672 func (r *ReconcilePerconaXtraDBCluster) handleReplicationUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, actions *userUpdateActions) error { 673 log := logf.FromContext(ctx) 674 675 if cr.CompareVersionWith("1.9.0") < 0 { 676 return nil 677 } 678 679 user := &users.SysUser{ 680 Name: users.Replication, 681 Pass: string(secrets.Data[users.Replication]), 682 Hosts: []string{"%"}, 683 } 684 685 if cr.Status.PXC.Ready > 0 { 686 err := r.manageReplicationUser(ctx, cr, secrets, internalSecrets) 687 if err != nil { 688 return errors.Wrap(err, "manage replication user") 689 } 690 691 if err := r.updateUserPassExpirationPolicy(ctx, cr, internalSecrets, user); err != nil { 692 return err 693 } 694 } 695 696 if cr.Status.Status != api.AppStateReady && !r.invalidPasswordApplied(cr.Status) { 697 return nil 698 } 699 700 passDiscarded, err := r.isOldPasswordDiscarded(cr, internalSecrets, user) 701 if err != nil { 702 return err 703 } 704 705 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && passDiscarded { 706 return nil 707 } 708 709 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) && !passDiscarded { 710 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 711 if err != nil { 712 return errors.Wrap(err, "discard old pass") 713 } 714 log.Info("Old password discarded", "user", user.Name) 715 716 return nil 717 } 718 719 log.Info("Password changed, updating user", "user", user.Name) 720 721 err = r.updateUserPassWithRetention(cr, secrets, internalSecrets, user) 722 if err != nil { 723 return errors.Wrap(err, "update replication users pass") 724 } 725 log.Info("Password updated", "user", user.Name) 726 727 if err := r.updateMySQLInitFile(ctx, cr, internalSecrets, user); err != nil { 728 return errors.Wrap(err, "update mysql init file") 729 } 730 731 orig := internalSecrets.DeepCopy() 732 internalSecrets.Data[user.Name] = secrets.Data[user.Name] 733 err = r.client.Patch(context.TODO(), internalSecrets, client.MergeFrom(orig)) 734 if err != nil { 735 return errors.Wrap(err, "update internal users secrets replication user password") 736 } 737 log.Info("Internal secrets updated", "user", user.Name) 738 739 err = r.discardOldPassword(cr, secrets, internalSecrets, user) 740 if err != nil { 741 return errors.Wrap(err, "discard replicaiton old pass") 742 } 743 log.Info("Old password discarded", "user", user.Name) 744 745 actions.updateReplicationPass = true 746 return nil 747 } 748 749 // manageReplicationUser ensures that replication user is always present and with the right privileges 750 func (r *ReconcilePerconaXtraDBCluster) manageReplicationUser(ctx context.Context, cr *api.PerconaXtraDBCluster, sysUsersSecretObj, secrets *corev1.Secret) error { 751 log := logf.FromContext(ctx) 752 753 pass, existInSys := sysUsersSecretObj.Data[users.Replication] 754 _, existInInternal := secrets.Data[users.Replication] 755 if existInSys && !existInInternal { 756 if secrets.Data == nil { 757 secrets.Data = make(map[string][]byte) 758 } 759 secrets.Data[users.Replication] = pass 760 return nil 761 } 762 if existInSys { 763 return nil 764 } 765 766 um, err := getUserManager(cr, secrets) 767 if err != nil { 768 return err 769 } 770 defer um.Close() 771 772 pass, err = generatePass() 773 if err != nil { 774 return errors.Wrap(err, "generate password") 775 } 776 777 err = um.CreateReplicationUser(string(pass)) 778 if err != nil { 779 return errors.Wrap(err, "create replication user") 780 } 781 782 sysUsersSecretObj.Data[users.Replication] = pass 783 secrets.Data[users.Replication] = pass 784 785 err = r.client.Update(context.TODO(), sysUsersSecretObj) 786 if err != nil { 787 return errors.Wrap(err, "update sys users secret") 788 } 789 err = r.client.Update(context.TODO(), secrets) 790 if err != nil { 791 return errors.Wrap(err, "update internal users secret") 792 } 793 794 log.Info("User replication: user created and privileges granted") 795 return nil 796 } 797 798 func (r *ReconcilePerconaXtraDBCluster) handleProxyadminUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, actions *userUpdateActions) error { 799 log := logf.FromContext(ctx) 800 801 if !cr.Spec.ProxySQLEnabled() { 802 return nil 803 } 804 805 user := &users.SysUser{ 806 Name: users.ProxyAdmin, 807 Pass: string(secrets.Data[users.ProxyAdmin]), 808 } 809 810 if bytes.Equal(secrets.Data[user.Name], internalSecrets.Data[user.Name]) { 811 return nil 812 } 813 814 if cr.Status.Status != api.AppStateReady && !r.invalidPasswordApplied(cr.Status) { 815 return nil 816 } 817 818 if err := r.updateUserPassExpirationPolicy(ctx, cr, internalSecrets, user); err != nil { 819 return err 820 } 821 822 log.Info("Password changed, updating user", "user", user.Name) 823 824 err := r.updateProxyUser(cr, internalSecrets, user) 825 if err != nil { 826 return errors.Wrap(err, "update Proxy users") 827 } 828 log.Info("Proxy user updated", "user", user.Name) 829 830 orig := internalSecrets.DeepCopy() 831 internalSecrets.Data[user.Name] = secrets.Data[user.Name] 832 err = r.client.Patch(context.TODO(), internalSecrets, client.MergeFrom(orig)) 833 if err != nil { 834 return errors.Wrap(err, "update internal users secrets proxyadmin user password") 835 } 836 log.Info("Internal secrets updated", "user", user.Name) 837 838 actions.restartProxy = true 839 840 return nil 841 } 842 843 func (r *ReconcilePerconaXtraDBCluster) handlePMMUser(ctx context.Context, cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, actions *userUpdateActions) error { 844 log := logf.FromContext(ctx) 845 846 if cr.Spec.PMM == nil || !cr.Spec.PMM.IsEnabled(secrets) { 847 return nil 848 } 849 850 if key, ok := secrets.Data[users.PMMServerKey]; ok { 851 if _, ok := internalSecrets.Data[users.PMMServerKey]; !ok { 852 internalSecrets.Data[users.PMMServerKey] = key 853 854 err := r.client.Update(context.TODO(), internalSecrets) 855 if err != nil { 856 return errors.Wrap(err, "update internal users secrets pmm user password") 857 } 858 log.Info("Internal secrets updated", "user", users.PMMServerKey) 859 860 return nil 861 } 862 } 863 864 name := users.PMMServerKey 865 if !cr.Spec.PMM.UseAPI(secrets) { 866 name = users.PMMServer 867 } 868 869 if bytes.Equal(secrets.Data[name], internalSecrets.Data[name]) { 870 return nil 871 } 872 873 if cr.Status.Status != api.AppStateReady && !r.invalidPasswordApplied(cr.Status) { 874 return nil 875 } 876 877 log.Info("Password changed, updating user", "user", name) 878 879 orig := internalSecrets.DeepCopy() 880 internalSecrets.Data[name] = secrets.Data[name] 881 err := r.client.Patch(context.TODO(), internalSecrets, client.MergeFrom(orig)) 882 if err != nil { 883 return errors.Wrap(err, "update internal users secrets pmm user password") 884 } 885 log.Info("Internal secrets updated", "user", name) 886 887 actions.restartPXC = true 888 actions.restartProxy = true 889 890 return nil 891 } 892 893 func (r *ReconcilePerconaXtraDBCluster) syncPXCUsersWithProxySQL(ctx context.Context, cr *api.PerconaXtraDBCluster) error { 894 log := logf.FromContext(ctx) 895 896 if !cr.Spec.ProxySQLEnabled() || cr.Status.PXC.Ready < 1 { 897 return nil 898 } 899 if cr.Status.Status != api.AppStateReady || cr.Status.ProxySQL.Status != api.AppStateReady { 900 return nil 901 } 902 903 for i := 0; i < int(cr.Spec.ProxySQL.Size); i++ { 904 pod := corev1.Pod{} 905 err := r.client.Get(context.TODO(), 906 types.NamespacedName{ 907 Namespace: cr.Namespace, 908 Name: cr.Name + "-proxysql-" + strconv.Itoa(i), 909 }, 910 &pod, 911 ) 912 if err != nil && k8serrors.IsNotFound(err) { 913 return err 914 } else if err != nil { 915 return errors.Wrap(err, "get proxysql pod") 916 } 917 var errb, outb bytes.Buffer 918 err = r.clientcmd.Exec(&pod, "proxysql", []string{"proxysql-admin", "--syncusers", "--add-query-rule"}, nil, &outb, &errb, false) 919 if err != nil { 920 return errors.Errorf("exec syncusers: %v / %s / %s", err, outb.String(), errb.String()) 921 } 922 if len(errb.Bytes()) > 0 { 923 return errors.New("syncusers: " + errb.String()) 924 } 925 } 926 927 log.V(1).Info("PXC users synced with ProxySQL") 928 return nil 929 } 930 931 func (r *ReconcilePerconaXtraDBCluster) updateUserPassWithRetention(cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, user *users.SysUser) error { 932 um, err := getUserManager(cr, internalSecrets) 933 if err != nil { 934 return err 935 } 936 defer um.Close() 937 938 err = um.UpdateUserPass(user) 939 if err != nil { 940 return errors.Wrap(err, "update user pass") 941 } 942 943 return nil 944 } 945 946 func (r *ReconcilePerconaXtraDBCluster) discardOldPassword(cr *api.PerconaXtraDBCluster, secrets, internalSecrets *corev1.Secret, user *users.SysUser) error { 947 um, err := getUserManager(cr, internalSecrets) 948 if err != nil { 949 return err 950 } 951 defer um.Close() 952 953 err = um.DiscardOldPassword(user) 954 if err != nil { 955 return errors.Wrap(err, fmt.Sprintf("discard old user %s pass", user.Name)) 956 } 957 958 return nil 959 } 960 961 func (r *ReconcilePerconaXtraDBCluster) isOldPasswordDiscarded(cr *api.PerconaXtraDBCluster, secrets *corev1.Secret, user *users.SysUser) (bool, error) { 962 um, err := getUserManager(cr, secrets) 963 if err != nil { 964 return false, err 965 } 966 defer um.Close() 967 968 discarded, err := um.IsOldPassDiscarded(user) 969 if err != nil { 970 return false, errors.Wrap(err, "is old password discarded") 971 } 972 973 return discarded, nil 974 } 975 976 func (r *ReconcilePerconaXtraDBCluster) isPassPropagated(cr *api.PerconaXtraDBCluster, user *users.SysUser) (bool, error) { 977 components := map[string]int32{ 978 "pxc": cr.Spec.PXC.Size, 979 } 980 981 if cr.HAProxyEnabled() { 982 components["haproxy"] = cr.Spec.HAProxy.Size 983 } 984 985 eg := new(errgroup.Group) 986 987 for component, size := range components { 988 comp := component 989 compCount := size 990 eg.Go(func() error { 991 for i := 0; int32(i) < compCount; i++ { 992 pod := corev1.Pod{} 993 err := r.client.Get(context.TODO(), 994 types.NamespacedName{ 995 Namespace: cr.Namespace, 996 Name: fmt.Sprintf("%s-%s-%d", cr.Name, comp, i), 997 }, 998 &pod, 999 ) 1000 if err != nil && k8serrors.IsNotFound(err) { 1001 return err 1002 } else if err != nil { 1003 return errors.Wrapf(err, "get %s pod", comp) 1004 } 1005 var errb, outb bytes.Buffer 1006 err = r.clientcmd.Exec(&pod, comp, []string{"cat", fmt.Sprintf("/etc/mysql/mysql-users-secret/%s", user.Name)}, nil, &outb, &errb, false) 1007 if err != nil { 1008 return errors.Errorf("exec cat on %s-%d: %v / %s / %s", comp, i, err, outb.String(), errb.String()) 1009 } 1010 if len(errb.Bytes()) > 0 { 1011 return errors.Errorf("cat on %s-%d: %s", comp, i, errb.String()) 1012 } 1013 1014 if outb.String() != user.Pass { 1015 return PassNotPropagatedError 1016 } 1017 } 1018 1019 return nil 1020 }) 1021 } 1022 1023 if err := eg.Wait(); err != nil { 1024 if err == PassNotPropagatedError { 1025 return false, nil 1026 } 1027 return false, err 1028 } 1029 1030 return true, nil 1031 } 1032 1033 func (r *ReconcilePerconaXtraDBCluster) updateProxyUser(cr *api.PerconaXtraDBCluster, internalSecrets *corev1.Secret, user *users.SysUser) error { 1034 if user == nil { 1035 return nil 1036 } 1037 1038 for i := 0; i < int(cr.Spec.ProxySQL.Size); i++ { 1039 um, err := users.NewManager(cr.Name+"-proxysql-"+strconv.Itoa(i)+"."+cr.Name+"-proxysql-unready."+cr.Namespace+":6032", users.ProxyAdmin, string(internalSecrets.Data[users.ProxyAdmin]), cr.Spec.PXC.ReadinessProbes.TimeoutSeconds) 1040 if err != nil { 1041 return errors.Wrap(err, "new users manager") 1042 } 1043 defer um.Close() 1044 err = um.UpdateProxyUser(user) 1045 if err != nil { 1046 return errors.Wrap(err, "update proxy users") 1047 } 1048 } 1049 return nil 1050 } 1051 1052 func (r *ReconcilePerconaXtraDBCluster) grantMonitorUserPrivilege(ctx context.Context, cr *api.PerconaXtraDBCluster, internalSysSecretObj *corev1.Secret, um *users.Manager) error { 1053 log := logf.FromContext(ctx) 1054 1055 annotationName := "grant-for-1.10.0-system-privilege" 1056 if internalSysSecretObj.Annotations[annotationName] == "done" { 1057 return nil 1058 } 1059 1060 if err := um.Update1100MonitorUserPrivilege(); err != nil { 1061 return errors.Wrap(err, "grant system user privilege") 1062 } 1063 1064 if internalSysSecretObj.Annotations == nil { 1065 internalSysSecretObj.Annotations = make(map[string]string) 1066 } 1067 1068 internalSysSecretObj.Annotations[annotationName] = "done" 1069 err := r.client.Update(context.TODO(), internalSysSecretObj) 1070 if err != nil { 1071 return errors.Wrap(err, "update internal sys users secret annotation") 1072 } 1073 1074 log.Info("monitor user privileges granted") 1075 return nil 1076 } 1077 1078 func getUserManager(cr *api.PerconaXtraDBCluster, secrets *corev1.Secret) (*users.Manager, error) { 1079 pxcUser := users.Root 1080 pxcPass := string(secrets.Data[users.Root]) 1081 if _, ok := secrets.Data[users.Operator]; ok { 1082 pxcUser = users.Operator 1083 pxcPass = string(secrets.Data[users.Operator]) 1084 } 1085 1086 addr := cr.Name + "-pxc-unready." + cr.Namespace + ":3306" 1087 hasKey, err := cr.ConfigHasKey("mysqld", "proxy_protocol_networks") 1088 if err != nil { 1089 return nil, errors.Wrap(err, "check if congfig has proxy_protocol_networks key") 1090 } 1091 if hasKey { 1092 addr = cr.Name + "-pxc-unready." + cr.Namespace + ":33062" 1093 } 1094 1095 um, err := users.NewManager(addr, pxcUser, pxcPass, cr.Spec.PXC.ReadinessProbes.TimeoutSeconds) 1096 if err != nil { 1097 return nil, errors.Wrap(err, "new users manager") 1098 } 1099 1100 return &um, nil 1101 } 1102 1103 func (r *ReconcilePerconaXtraDBCluster) updateUserPassExpirationPolicy(ctx context.Context, cr *api.PerconaXtraDBCluster, internalSecrets *corev1.Secret, user *users.SysUser) error { 1104 log := logf.FromContext(ctx) 1105 1106 annotationName := "pass-expire-policy-for-1.13.0-user-" + user.Name 1107 if internalSecrets.Annotations[annotationName] == "done" { 1108 return nil 1109 } 1110 1111 if cr.CompareVersionWith("1.13.0") >= 0 { 1112 um, err := getUserManager(cr, internalSecrets) 1113 if err != nil { 1114 return err 1115 } 1116 1117 if err := um.UpdatePassExpirationPolicy(user); err != nil { 1118 return errors.Wrapf(err, "update %s user password expiration policy", user.Name) 1119 } 1120 1121 if internalSecrets.Annotations == nil { 1122 internalSecrets.Annotations = make(map[string]string) 1123 } 1124 1125 internalSecrets.Annotations[annotationName] = "done" 1126 err = r.client.Update(ctx, internalSecrets) 1127 if err != nil { 1128 return errors.Wrap(err, "update internal sys users secret annotation") 1129 } 1130 1131 log.Info("Password expiration policy updated", "user", user.Name) 1132 return nil 1133 } 1134 1135 return nil 1136 } 1137 1138 func (r *ReconcilePerconaXtraDBCluster) invalidPasswordApplied(status api.PerconaXtraDBClusterStatus) bool { 1139 if len(status.Messages) == 0 { 1140 return false 1141 } 1142 1143 if strings.Contains(status.Messages[0], "password does not satisfy the current policy") { 1144 return true 1145 } 1146 1147 return false 1148 } 1149 1150 func (r *ReconcilePerconaXtraDBCluster) updateMySQLInitFile(ctx context.Context, cr *api.PerconaXtraDBCluster, internalSecret *corev1.Secret, user *users.SysUser) error { 1151 log := logf.FromContext(ctx) 1152 1153 secret := &corev1.Secret{ 1154 ObjectMeta: metav1.ObjectMeta{ 1155 Name: cr.Name + "-mysql-init", 1156 Namespace: cr.Namespace, 1157 }, 1158 } 1159 data := map[string][]byte{ 1160 "init.sql": []byte("SET SESSION wsrep_on=OFF;\nSET SESSION sql_log_bin=0;\n"), 1161 } 1162 if err := r.client.Get(ctx, client.ObjectKeyFromObject(secret), secret); err == nil { 1163 data = secret.Data 1164 } 1165 1166 statements := make([]string, 0) 1167 for _, host := range user.Hosts { 1168 statements = append(statements, fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s';\n", user.Name, host, user.Pass)) 1169 } 1170 1171 opResult, err := controllerutil.CreateOrUpdate(ctx, r.client, secret, func() error { 1172 data["init.sql"] = append(data["init.sql"], []byte(strings.Join(statements, ""))...) 1173 secret.Data = data 1174 return nil 1175 }) 1176 1177 log.Info(fmt.Sprintf("MySQL init secret %s", opResult), "secret", secret.Name, "user", user.Name) 1178 1179 return err 1180 }