github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/backup/mysql/mysql_backup_test.go (about) 1 // Copyright (c) 2022, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package mysql 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "github.com/google/uuid" 11 "github.com/verrazzano/verrazzano/platform-operator/constants" 12 common "github.com/verrazzano/verrazzano/tests/e2e/backup/helpers" 13 "github.com/verrazzano/verrazzano/tests/e2e/pkg/test/framework/metrics" 14 "go.uber.org/zap" 15 k8serror "k8s.io/apimachinery/pkg/api/errors" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "os" 18 "strings" 19 "text/template" 20 "time" 21 22 . "github.com/onsi/ginkgo/v2" 23 . "github.com/onsi/gomega" 24 "github.com/verrazzano/verrazzano/pkg/k8sutil" 25 "github.com/verrazzano/verrazzano/tests/e2e/pkg" 26 "github.com/verrazzano/verrazzano/tests/e2e/pkg/test/framework" 27 ) 28 29 const ( 30 shortWaitTimeout = 10 * time.Minute 31 shortPollingInterval = 10 * time.Second 32 waitTimeout = 20 * time.Minute 33 pollingInterval = 30 * time.Second 34 mysqlPvcPrefix = "datadir-mysql" 35 mysqlChartName = "mysql" 36 mysqlInnoDBClusterName = "mysql" 37 vzMySQLChartPath = "../../../../platform-operator/thirdparty/charts/mysql" 38 ) 39 40 var keycloakNamespacePods = []string{"keycloak", "mysql"} 41 var mysqlPods = []string{"mysql"} 42 var keycloakPods = []string{"keycloak"} 43 44 var beforeSuite = t.BeforeSuiteFunc(func() { 45 start := time.Now() 46 common.GatherInfo() 47 file, err := os.CreateTemp("", "mysql-values-") 48 if err != nil { 49 t.Logs.Fatal(err) 50 } 51 defer file.Close() 52 common.MySQLBackupHelmFileName = file.Name() 53 backupPrerequisites() 54 metrics.Emit(t.Metrics.With("deployment_elapsed_time", time.Since(start).Milliseconds())) 55 }) 56 57 var _ = BeforeSuite(beforeSuite) 58 59 var afterSuite = t.AfterSuiteFunc(func() { 60 start := time.Now() 61 cleanUpSuite() 62 os.Remove(common.MySQLBackupHelmFileName) 63 metrics.Emit(t.Metrics.With("undeployment_elapsed_time", time.Since(start).Milliseconds())) 64 }) 65 66 var _ = AfterSuite(afterSuite) 67 68 var t = framework.NewTestFramework("mysql-backup") 69 70 // func CreateInnoDBBackupObjectWithOci() creates mysql operator backup resource to start the backup. 71 func CreateInnoDBBackupObjectWithOci() error { 72 var b bytes.Buffer 73 template, _ := template.New("mysql-backup").Parse(common.InnoDBBackupOci) 74 data := common.InnoDBBackupObject{ 75 InnoDBBackupName: common.BackupMySQLName, 76 InnoDBNamespaceName: constants.KeycloakNamespace, 77 InnoDBClusterName: common.InnoDBClusterName, 78 InnoDBBackupProfileName: common.BackupResourceName, 79 InnoDBBackupObjectStoreBucketName: common.OciBucketName, 80 InnoDBBackupCredentialsName: common.VeleroMySQLSecretName, 81 InnoDBBackupStorageName: common.BackupMySQLStorageName, 82 } 83 template.Execute(&b, data) 84 err := common.DynamicSSA(context.TODO(), b.String(), t.Logs) 85 if err != nil { 86 t.Logs.Errorf("Error creating innodb backup object", zap.Error(err)) 87 return err 88 } 89 90 return nil 91 } 92 93 // func CreateInnoDBBackupObjectWithS3() creates mysql operator backup resource to start the backup. 94 func CreateInnoDBBackupObjectWithS3() error { 95 t.Logs.Infof("Starting MySQL backup with S3") 96 var b bytes.Buffer 97 template, _ := template.New("mysql-backup").Parse(common.InnoDBBackupS3) 98 data := common.InnoDBBackupObject{ 99 InnoDBBackupName: common.BackupMySQLName, 100 InnoDBNamespaceName: constants.KeycloakNamespace, 101 InnoDBClusterName: common.InnoDBClusterName, 102 InnoDBBackupProfileName: common.BackupResourceName, 103 InnoDBBackupObjectStoreBucketName: common.OciBucketName, 104 InnoDBBackupCredentialsName: common.VeleroMySQLSecretName, 105 InnoDBBackupStorageName: common.BackupMySQLStorageName, 106 InnoDBObjectStorageNamespaceName: common.OciNamespaceName, 107 InnoDBBackupRegion: common.BackupRegion, 108 } 109 template.Execute(&b, data) 110 err := common.DynamicSSA(context.TODO(), b.String(), t.Logs) 111 if err != nil { 112 t.Logs.Errorf("Error creating innodb backup object", zap.Error(err)) 113 return err 114 } 115 116 return nil 117 } 118 119 func BackupMySQLValues() error { 120 t.Logs.Infof("Backing up mysql values to file '%s'", common.MySQLBackupHelmFileName) 121 var cmd common.BashCommand 122 var cmdArgs []string 123 cmdArgs = append(cmdArgs, "/bin/sh", "-c", fmt.Sprintf("helm get values %s -n %s > %s", mysqlChartName, constants.KeycloakNamespace, common.MySQLBackupHelmFileName)) 124 cmd.CommandArgs = cmdArgs 125 126 response := common.Runner(&cmd, t.Logs) 127 if response.CommandError != nil { 128 t.Logs.Error("Unable to get mysql helm values due to ", zap.Error(response.CommandError)) 129 return response.CommandError 130 } 131 return nil 132 } 133 134 func NukeMySQL() error { 135 var cmd common.BashCommand 136 var cmdArgs []string 137 cmdArgs = append(cmdArgs, "helm") 138 cmdArgs = append(cmdArgs, "delete") 139 cmdArgs = append(cmdArgs, mysqlChartName) 140 cmdArgs = append(cmdArgs, "-n") 141 cmdArgs = append(cmdArgs, constants.KeycloakNamespace) 142 143 cmd.CommandArgs = cmdArgs 144 145 response := common.Runner(&cmd, t.Logs) 146 if response.CommandError != nil { 147 t.Logs.Error("Unable to cleanup mysql due to ", zap.Error(response.CommandError)) 148 return response.CommandError 149 } 150 151 clientset, err := k8sutil.GetKubernetesClientset() 152 if err != nil { 153 t.Logs.Errorf("Failed to get clientset with error: %v", err) 154 return err 155 } 156 157 t.Logs.Infof("Deleting mysql pvc(s) from namespace '%s'", constants.KeycloakNamespace) 158 for i := 0; i < 3; i++ { 159 err := clientset.CoreV1().PersistentVolumeClaims(constants.KeycloakNamespace).Delete(context.TODO(), fmt.Sprintf("%s-%v", mysqlPvcPrefix, i), metav1.DeleteOptions{}) 160 if err != nil { 161 if !k8serror.IsNotFound(err) { 162 t.Logs.Errorf("Unable to delete opensearch master pvc due to '%v'", zap.Error(err)) 163 return err 164 } 165 } 166 } 167 168 return nil 169 } 170 171 func MySQLRestore() error { 172 t.Logs.Info("Start mysql restore") 173 174 // Get the backup folder name 175 backupInfo, err := common.GetMySQLBackup(constants.KeycloakNamespace, common.BackupMySQLName, t.Logs) 176 if err != nil { 177 t.Logs.Errorf("Unable to fetch backup '%s' due to '%v'", common.BackupMySQLName, zap.Error(err)) 178 return err 179 } 180 backupFolderName := backupInfo.Status.Output 181 182 var cmd common.BashCommand 183 var cmdArgs []string 184 185 if strings.ToLower(common.MySQLBackupMode) == "s3" { 186 t.Logs.Infof("Starting MySQL restore with S3") 187 s3EndPoint := fmt.Sprintf("https://%s.compat.objectstorage.%s.oraclecloud.com", common.OciNamespaceName, common.BackupRegion) 188 cmdArgs = append(cmdArgs, "helm", "install", mysqlChartName, vzMySQLChartPath) 189 cmdArgs = append(cmdArgs, "--namespace", constants.KeycloakNamespace) 190 cmdArgs = append(cmdArgs, "--set", "initDB.dump.name=alpha") 191 cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("initDB.dump.s3.prefix=%s/%s", common.BackupMySQLStorageName, backupFolderName)) 192 cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("initDB.dump.s3.bucketName=%s", common.OciBucketName)) 193 cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("initDB.dump.s3.config=%s", common.VeleroMySQLSecretName)) 194 cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("initDB.dump.s3.endpoint=%s", s3EndPoint)) 195 cmdArgs = append(cmdArgs, "--set", "initDB.dump.s3.profile=default") 196 cmdArgs = append(cmdArgs, "--values", common.MySQLBackupHelmFileName) 197 } else { 198 cmdArgs = append(cmdArgs, "helm", "install", mysqlChartName, vzMySQLChartPath) 199 cmdArgs = append(cmdArgs, "--namespace", constants.KeycloakNamespace) 200 cmdArgs = append(cmdArgs, "--set", "initDB.dump.name=alpha") 201 cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("initDB.dump.ociObjectStorage.prefix=%s/%s", common.BackupMySQLStorageName, backupFolderName)) 202 cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("initDB.dump.ociObjectStorage.bucketName=%s", common.OciBucketName)) 203 cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("initDB.dump.ociObjectStorage.credentials=%s", common.VeleroMySQLSecretName)) 204 cmdArgs = append(cmdArgs, "--values", common.MySQLBackupHelmFileName) 205 } 206 cmd.CommandArgs = cmdArgs 207 208 response := common.Runner(&cmd, t.Logs) 209 if response.CommandError != nil { 210 t.Logs.Error("Unable to restore mysql due to ", zap.Error(response.CommandError)) 211 return response.CommandError 212 } 213 return nil 214 } 215 216 func KeycloakDown() error { 217 clientset, err := k8sutil.GetKubernetesClientset() 218 if err != nil { 219 t.Logs.Errorf("Failed to get clientset with error: %v", err) 220 return err 221 } 222 223 t.Logs.Infof("Scaling down keycloak sts") 224 getScale, err := clientset.AppsV1().StatefulSets(constants.KeycloakNamespace).GetScale(context.TODO(), constants.KeycloakNamespace, metav1.GetOptions{}) 225 if err != nil { 226 return err 227 } 228 common.KeyCloakReplicaCount = getScale.Spec.Replicas 229 scaleDown := *getScale 230 scaleDown.Spec.Replicas = 0 231 232 _, err = clientset.AppsV1().StatefulSets(constants.KeycloakNamespace).UpdateScale(context.TODO(), constants.KeycloakNamespace, &scaleDown, metav1.UpdateOptions{}) 233 if err != nil { 234 t.Logs.Infof("Error = %v", zap.Error(err)) 235 return err 236 } 237 return nil 238 } 239 240 func KeyCloakUp() error { 241 clientset, err := k8sutil.GetKubernetesClientset() 242 if err != nil { 243 t.Logs.Errorf("Failed to get clientset with error: %v", err) 244 return err 245 } 246 247 getKeycloak, err := clientset.AppsV1().StatefulSets(constants.KeycloakNamespace).GetScale(context.TODO(), constants.KeycloakNamespace, metav1.GetOptions{}) 248 if err != nil { 249 return err 250 } 251 252 scaleUp := *getKeycloak 253 scaleUp.Spec.Replicas = common.KeyCloakReplicaCount 254 255 t.Logs.Infof("Scaling up keycloak sts") 256 _, err = clientset.AppsV1().StatefulSets(constants.KeycloakNamespace).UpdateScale(context.TODO(), constants.KeycloakNamespace, &scaleUp, metav1.UpdateOptions{}) 257 if err != nil { 258 t.Logs.Infof("Error = %v", zap.Error(err)) 259 return err 260 } 261 return nil 262 } 263 264 // KeycloakDeleteUsers helps in cleaning up test users at the end of the run 265 func KeycloakDeleteUsers() error { 266 keycloakClient, err := pkg.NewKeycloakAdminRESTClient() 267 if err != nil { 268 t.Logs.Errorf("Unable to get keycloak client due to ", zap.Error(err)) 269 return err 270 } 271 272 for i := 0; i < len(common.KeyCloakUserIDList); i++ { 273 t.Logs.Infof("Deleting user with username '%s'", common.KeyCloakUserIDList[i]) 274 _ = keycloakClient.DeleteUser(constants.VerrazzanoSystemNamespace, common.KeyCloakUserIDList[i]) 275 } 276 277 return nil 278 279 } 280 281 // KeycloakCreateUsers helps in creating test users to populate data 282 func KeycloakCreateUsers(n int) error { 283 284 keycloakClient, err := pkg.NewKeycloakAdminRESTClient() 285 if err != nil { 286 t.Logs.Errorf("Unable to get keycloak client due to ", zap.Error(err)) 287 return err 288 } 289 290 for i := 0; i < n; i++ { 291 id := uuid.New().String() 292 uniqueID := strings.Split(id, "-")[len(strings.Split(id, "-"))-1] 293 userID := fmt.Sprintf("mysql-user-%s", uniqueID) 294 t.Logs.Infof("Creating user with username '%s'", userID) 295 firstName := fmt.Sprintf("john-%v", i+1) 296 lastName := "doe" 297 location, err := keycloakClient.CreateUser(constants.VerrazzanoSystemNamespace, userID, firstName, lastName, "hello@mysql!") 298 if err != nil { 299 t.Logs.Errorf("Unable to get create keycloak user due to ", zap.Error(err)) 300 return err 301 } 302 sqlUserID := strings.Split(location, "/")[len(strings.Split(location, "/"))-1] 303 common.KeyCloakUserIDList = append(common.KeyCloakUserIDList, sqlUserID) 304 } 305 306 return nil 307 308 } 309 310 // KeycloakVerifyUsers helps in verifying if the user exists 311 func KeycloakVerifyUsers() bool { 312 keycloakClient, err := pkg.NewKeycloakAdminRESTClient() 313 if err != nil { 314 t.Logs.Errorf("Unable to get keycloak client due to ", zap.Error(err)) 315 return false 316 } 317 318 for i := 0; i < len(common.KeyCloakUserIDList); i++ { 319 t.Logs.Infof("Verifying user with username '%s' exists after mysql restore", common.KeyCloakUserIDList[i]) 320 ok, err := keycloakClient.VerifyUserExists(constants.VerrazzanoSystemNamespace, common.KeyCloakUserIDList[i]) 321 if err != nil { 322 t.Logs.Errorf("Unable to verify keycloak user due to ", zap.Error(err)) 323 return false 324 } 325 if !ok { 326 t.Logs.Errorf("User '%s' does not exist or could not be verified.", common.KeyCloakUserIDList[i]) 327 return false 328 } 329 t.Logs.Infof("User '%s' found after mysql restore!", common.KeyCloakUserIDList[i]) 330 } 331 return true 332 } 333 334 // 'It' Wrapper to only run spec if the Velero is supported on the current Verrazzano version 335 func WhenMySQLOpInstalledIt(description string, f func()) { 336 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 337 if err != nil { 338 t.It(description, func() { 339 Fail(fmt.Sprintf("Failed to get default kubeconfig path: %s", err.Error())) 340 }) 341 } 342 supported, err := pkg.IsVerrazzanoMinVersion("1.4.0", kubeconfigPath) 343 if err != nil { 344 t.It(description, func() { 345 Fail(fmt.Sprintf("Failed to check Verrazzano version 1.4.0: %s", err.Error())) 346 }) 347 } 348 if !pkg.IsMySQLOperatorEnabled(kubeconfigPath) { 349 supported = false 350 } 351 352 if supported { 353 t.It(description, f) 354 } else { 355 t.Logs.Infof("Skipping check '%v', the MySQL operator not enabled or minimum version detection failed", description) 356 } 357 } 358 359 // checkPodsRunning checks whether the pods are ready in a given namespace 360 func checkPodsRunning(namespace string, expectedPods []string) bool { 361 result, err := pkg.PodsRunning(namespace, expectedPods) 362 if err != nil { 363 AbortSuite(fmt.Sprintf("One or more pods are not running in the namespace: %v, error: %v", namespace, err)) 364 } 365 return result 366 } 367 368 // checkPodsNotRunning checks whether the pods are not ready in a given namespace 369 func checkPodsNotRunning(namespace string, expectedPods []string) bool { 370 result, err := pkg.PodsNotRunning(namespace, expectedPods) 371 if err != nil { 372 AbortSuite(fmt.Sprintf("One or more pods are running in the namespace: %v, error: %v", namespace, err)) 373 } 374 return result 375 } 376 377 func backupPrerequisites() { 378 t.Logs.Info("Setup backup pre-requisites") 379 t.Logs.Info("Create backup secret for innodb backup objects") 380 381 Eventually(func() error { 382 return BackupMySQLValues() 383 }, shortWaitTimeout, shortPollingInterval).Should(BeNil()) 384 385 if strings.ToLower(common.MySQLBackupMode) == "s3" { 386 Eventually(func() error { 387 return common.CreateMySQLCredentialsSecretFromFile(constants.KeycloakNamespace, common.VeleroMySQLSecretName, t.Logs) 388 }, shortWaitTimeout, shortPollingInterval).Should(BeNil()) 389 } else { 390 Eventually(func() error { 391 return common.CreateMySQLCredentialsSecretFromUserPrincipal(constants.KeycloakNamespace, common.VeleroMySQLSecretName, t.Logs) 392 }, shortWaitTimeout, shortPollingInterval).Should(BeNil()) 393 } 394 395 t.Logs.Info("Create a sample keycloak user") 396 Eventually(func() error { 397 return KeycloakCreateUsers(common.KeycloakUserCount) 398 }, shortWaitTimeout, shortPollingInterval).Should(BeNil()) 399 400 } 401 402 func cleanUpSuite() { 403 t.Logs.Info("Cleanup backup and restore objects") 404 405 t.Logs.Info("Cleanup backup object") 406 Eventually(func() error { 407 return common.CrdPruner("mysql.oracle.com", "v2", "mysqlbackups", common.BackupMySQLName, constants.KeycloakNamespace, t.Logs) 408 }, shortWaitTimeout, shortPollingInterval).Should(BeNil()) 409 410 t.Logs.Info("Cleanup mysql backup secrets") 411 Eventually(func() error { 412 return common.DeleteSecret(constants.KeycloakNamespace, common.VeleroMySQLSecretName, t.Logs) 413 }, shortWaitTimeout, shortPollingInterval).Should(BeNil()) 414 415 t.Logs.Info("Delete keycloak user") 416 Eventually(func() error { 417 return KeycloakDeleteUsers() 418 }, shortWaitTimeout, shortPollingInterval).Should(BeNil()) 419 } 420 421 var _ = t.Describe("MySQL Backup and Restore,", Label("f:platform-verrazzano.mysql-backup"), Serial, func() { 422 423 t.Context("MySQL backup operator", func() { 424 WhenMySQLOpInstalledIt("MySQL backup triggered", func() { 425 if strings.ToLower(common.MySQLBackupMode) == "s3" { 426 Eventually(func() error { 427 return CreateInnoDBBackupObjectWithS3() 428 }, waitTimeout, pollingInterval).Should(BeNil()) 429 } else { 430 Eventually(func() error { 431 return CreateInnoDBBackupObjectWithOci() 432 }, waitTimeout, pollingInterval).Should(BeNil()) 433 } 434 }) 435 436 WhenMySQLOpInstalledIt("Check backup progress after mysql backup object was created", func() { 437 Eventually(func() error { 438 return common.TrackOperationProgress("mysql", common.BackupResource, common.BackupMySQLName, constants.KeycloakNamespace, t.Logs) 439 }, waitTimeout, pollingInterval).Should(BeNil()) 440 }) 441 }) 442 443 t.Context("Disaster simulation", func() { 444 WhenMySQLOpInstalledIt("Delete users created as part of pre-suite", func() { 445 Eventually(func() error { 446 return KeycloakDeleteUsers() 447 }, waitTimeout, pollingInterval).Should(BeNil()) 448 }) 449 450 WhenMySQLOpInstalledIt("Delete innodb cluster", func() { 451 Eventually(func() error { 452 return NukeMySQL() 453 }, waitTimeout, pollingInterval).Should(BeNil()) 454 }) 455 456 WhenMySQLOpInstalledIt("Ensure the pods are not running before starting a restore", func() { 457 Eventually(func() bool { 458 return checkPodsNotRunning(constants.KeycloakNamespace, mysqlPods) 459 }, waitTimeout, pollingInterval).Should(BeTrue(), "Check if pods are down") 460 }) 461 }) 462 463 t.Context("MySQL restore", func() { 464 WhenMySQLOpInstalledIt(fmt.Sprintf("Start restore of mysql from backup '%s'", common.BackupMySQLName), func() { 465 Eventually(func() error { 466 return MySQLRestore() 467 }, waitTimeout, pollingInterval).Should(BeNil()) 468 }) 469 WhenMySQLOpInstalledIt("Check MySQL restore progress", func() { 470 Eventually(func() error { 471 return common.TrackOperationProgress("mysql", common.RestoreResource, mysqlInnoDBClusterName, constants.KeycloakNamespace, t.Logs) 472 }, waitTimeout, pollingInterval).Should(BeNil()) 473 }) 474 475 }) 476 477 t.Context("MySQL Data and Infra verification", func() { 478 WhenMySQLOpInstalledIt("After restore is complete scale down keycloak", func() { 479 Eventually(func() error { 480 return KeycloakDown() 481 }, waitTimeout, pollingInterval).Should(BeNil()) 482 }) 483 484 WhenMySQLOpInstalledIt("After scaling down keycloak, wait for all keycloak pods to go down", func() { 485 Eventually(func() bool { 486 return checkPodsNotRunning(constants.KeycloakNamespace, keycloakPods) 487 }, waitTimeout, pollingInterval).Should(BeTrue(), "Check if keycloak pods are down") 488 }) 489 490 WhenMySQLOpInstalledIt("Scale up keycloak", func() { 491 Eventually(func() error { 492 return KeyCloakUp() 493 }, waitTimeout, pollingInterval).Should(BeNil()) 494 }) 495 496 WhenMySQLOpInstalledIt("After scaling up keycloak, wait for all keycloak pods to be up", func() { 497 Eventually(func() bool { 498 return checkPodsRunning(constants.KeycloakNamespace, keycloakPods) 499 }, waitTimeout, pollingInterval).Should(BeTrue(), "Check if keycloak pods are up") 500 }) 501 502 WhenMySQLOpInstalledIt("After restore is complete wait for keycloak and mysql pods to come up", func() { 503 Eventually(func() bool { 504 return checkPodsRunning(constants.KeycloakNamespace, keycloakNamespacePods) 505 }, waitTimeout, pollingInterval).Should(BeTrue(), "Check if keycloak and mysql infra is up") 506 }) 507 508 WhenMySQLOpInstalledIt("Is Restore good? Verify restore", func() { 509 Eventually(func() bool { 510 return KeycloakVerifyUsers() 511 }, waitTimeout, pollingInterval).Should(BeTrue()) 512 }) 513 514 }) 515 })