github.com/verrazzano/verrazzano@v1.7.1/tools/vz/cmd/uninstall/uninstall.go (about) 1 // Copyright (c) 2022, 2024, 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 uninstall 5 6 import ( 7 "bufio" 8 "context" 9 "fmt" 10 "io" 11 "os" 12 "regexp" 13 "time" 14 15 "github.com/spf13/cobra" 16 vzconstants "github.com/verrazzano/verrazzano/pkg/constants" 17 "github.com/verrazzano/verrazzano/platform-operator/apis/verrazzano/v1alpha1" 18 "github.com/verrazzano/verrazzano/tools/vz/cmd/bugreport" 19 cmdhelpers "github.com/verrazzano/verrazzano/tools/vz/cmd/helpers" 20 "github.com/verrazzano/verrazzano/tools/vz/pkg/constants" 21 "github.com/verrazzano/verrazzano/tools/vz/pkg/helpers" 22 adminv1 "k8s.io/api/admissionregistration/v1" 23 corev1 "k8s.io/api/core/v1" 24 rbacv1 "k8s.io/api/rbac/v1" 25 "k8s.io/apimachinery/pkg/api/errors" 26 "k8s.io/apimachinery/pkg/api/meta" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/labels" 29 "k8s.io/apimachinery/pkg/selection" 30 "k8s.io/apimachinery/pkg/types" 31 "k8s.io/client-go/kubernetes" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 ) 34 35 const ( 36 CommandName = "uninstall" 37 crdsFlag = "crds" 38 crdsFlagHelp = "Completely remove all CRDs that were installed by Verrazzano" 39 helpShort = "Uninstall Verrazzano" 40 helpLong = `Uninstall the Verrazzano Platform Operator and all of the currently installed components` 41 helpExample = ` 42 # Uninstall Verrazzano and stream the logs to the console. Stream the logs to the console until the uninstall completes. 43 vz uninstall 44 45 # Uninstall Verrazzano and wait for the command to complete. Timeout the command after 30 minutes. 46 vz uninstall --timeout 30m` 47 ConfirmUninstallFlag = "skip-confirmation" 48 ConfirmUninstallFlagShorthand = "y" 49 ) 50 51 // Number of retries after waiting a second for uninstall job pod to be ready 52 const uninstallDefaultWaitRetries = 300 53 const verrazzanoUninstallJobDetectWait = 1 54 55 var uninstallWaitRetries = uninstallDefaultWaitRetries 56 57 // Used with unit testing 58 func setWaitRetries(retries int) { uninstallWaitRetries = retries } 59 func resetWaitRetries() { uninstallWaitRetries = uninstallDefaultWaitRetries } 60 61 var propagationPolicy = metav1.DeletePropagationBackground 62 var deleteOptions = &client.DeleteOptions{PropagationPolicy: &propagationPolicy} 63 64 var logsEnum = cmdhelpers.LogFormatSimple 65 66 func NewCmdUninstall(vzHelper helpers.VZHelper) *cobra.Command { 67 cmd := cmdhelpers.NewCommand(vzHelper, CommandName, helpShort, helpLong) 68 cmd.RunE = func(cmd *cobra.Command, args []string) error { 69 return runCmdUninstall(cmd, args, vzHelper) 70 } 71 cmd.Example = helpExample 72 73 cmd.PersistentFlags().Bool(constants.WaitFlag, constants.WaitFlagDefault, constants.WaitFlagHelp) 74 cmd.PersistentFlags().Duration(constants.TimeoutFlag, time.Minute*30, constants.TimeoutFlagHelp) 75 cmd.PersistentFlags().Duration(constants.VPOTimeoutFlag, time.Minute*5, constants.VPOTimeoutFlagHelp) 76 cmd.PersistentFlags().Var(&logsEnum, constants.LogFormatFlag, constants.LogFormatHelp) 77 cmd.PersistentFlags().Bool(constants.AutoBugReportFlag, constants.AutoBugReportFlagDefault, constants.AutoBugReportFlagHelp) 78 79 // Remove CRD's flag is still being discussed - keep hidden for now 80 cmd.PersistentFlags().Bool(crdsFlag, false, crdsFlagHelp) 81 _ = cmd.PersistentFlags().MarkHidden(crdsFlag) 82 83 // Dry run flag is still being discussed - keep hidden for now 84 cmd.PersistentFlags().Bool(constants.DryRunFlag, false, "Simulate an uninstall.") 85 _ = cmd.PersistentFlags().MarkHidden(constants.DryRunFlag) 86 87 // Hide the flag for overriding the default wait timeout for the platform-operator 88 cmd.PersistentFlags().MarkHidden(constants.VPOTimeoutFlag) 89 90 // When set to false, uninstall prompt can be suppressed 91 cmd.PersistentFlags().BoolP(constants.SkipConfirmationFlag, constants.SkipConfirmationShort, false, "Used to confirm uninstall and suppress prompt") 92 93 // Verifies that the CLI args are not set at the creation of a command 94 vzHelper.VerifyCLIArgsNil(cmd) 95 96 return cmd 97 } 98 99 func runCmdUninstall(cmd *cobra.Command, args []string, vzHelper helpers.VZHelper) error { 100 // Get the controller runtime client. 101 client, err := vzHelper.GetClient(cmd) 102 if err != nil { 103 return err 104 } 105 106 // Find the Verrazzano resource to uninstall. 107 vz, err := helpers.FindVerrazzanoResource(client) 108 if err != nil { 109 return fmt.Errorf("Verrazzano is not installed: %s", err.Error()) 110 } 111 112 confirmUninstallFlag, err := cmd.Flags().GetBool(ConfirmUninstallFlag) 113 continueUninstall, err := continueUninstall(confirmUninstallFlag) 114 if err != nil { 115 return err 116 } 117 if !continueUninstall { 118 return nil 119 } 120 121 // Decide whether to stream the old uninstall job log or the VPO log. With Verrazzano 1.4.0, 122 // the uninstall job has been removed and the VPO does the uninstall. 123 useUninstallJob, err := cmdhelpers.UsePlatformOperatorUninstallJob(client) 124 if err != nil { 125 return err 126 } 127 if useUninstallJob { 128 // log-format argument ignored with pre 1.4.0 uninstalls if specified 129 if cmd.PersistentFlags().Changed(constants.LogFormatFlag) { 130 fmt.Fprintf(vzHelper.GetOutputStream(), "Warning: --log-format argument is ignored with uninstalls prior to v1.4.0\n") 131 } 132 } 133 134 // Get the kubernetes clientset. This will validate that the kubeconfig and context are valid. 135 kubeClient, err := vzHelper.GetKubeClient(cmd) 136 if err != nil { 137 return err 138 } 139 140 // Get the timeout value for the uninstall command. 141 timeout, err := cmdhelpers.GetWaitTimeout(cmd, constants.TimeoutFlag) 142 if err != nil { 143 return err 144 } 145 146 // Get the VPO timeout 147 vpoTimeout, err := cmdhelpers.GetWaitTimeout(cmd, constants.VPOTimeoutFlag) 148 if err != nil { 149 return err 150 } 151 152 // Get the log format value 153 logFormat, err := cmdhelpers.GetLogFormat(cmd) 154 if err != nil { 155 return err 156 } 157 // Delete the Verrazzano custom resource. 158 err = client.Delete(context.TODO(), vz) 159 if err != nil { 160 // Try to delete the resource as v1alpha1 if the v1beta1 API version did not match 161 if meta.IsNoMatchError(err) { 162 vzV1Alpha1 := &v1alpha1.Verrazzano{} 163 err = vzV1Alpha1.ConvertFrom(vz) 164 if err != nil { 165 return failedToUninstallErr(err) 166 } 167 if err := client.Delete(context.TODO(), vzV1Alpha1); err != nil { 168 return failedToUninstallErr(err) 169 } 170 } else { 171 return bugreport.AutoBugReport(cmd, vzHelper, err) 172 } 173 } 174 _, _ = fmt.Fprintf(vzHelper.GetOutputStream(), "Uninstalling Verrazzano\n") 175 176 // Wait for the Verrazzano uninstall to complete. 177 err = waitForUninstallToComplete(client, kubeClient, vzHelper, types.NamespacedName{Namespace: vz.Namespace, Name: vz.Name}, timeout, vpoTimeout, logFormat, useUninstallJob) 178 if err != nil { 179 return bugreport.AutoBugReport(cmd, vzHelper, err) 180 } 181 return nil 182 } 183 184 // cleanupResources deletes remaining resources that remain after the Verrazzano resource in uninstalled 185 // Resources that fail to delete will log an error but will not return 186 func cleanupResources(client client.Client, vzHelper helpers.VZHelper) { 187 // Delete verrazzano-install namespace 188 err := deleteNamespace(client, constants.VerrazzanoInstall) 189 if err != nil { 190 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 191 } 192 193 // Delete other verrazzano resources 194 err = deleteWebhookConfiguration(client, constants.VerrazzanoPlatformOperatorWebhook) 195 if err != nil { 196 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 197 } 198 199 err = deleteWebhookConfiguration(client, constants.VerrazzanoMysqlInstallValuesWebhook) 200 if err != nil { 201 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 202 } 203 204 err = deleteWebhookConfiguration(client, constants.VerrazzanoRequirementsValidatorWebhook) 205 if err != nil { 206 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 207 } 208 209 err = deleteMutatingWebhookConfiguration(client, constants.MysqlBackupMutatingWebhookName) 210 if err != nil { 211 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 212 } 213 214 err = deleteClusterRoleBinding(client, constants.VerrazzanoPlatformOperator) 215 if err != nil { 216 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 217 } 218 219 err = deleteClusterRole(client, constants.VerrazzanoManagedCluster) 220 if err != nil { 221 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 222 } 223 224 err = deleteClusterRole(client, vzconstants.VerrazzanoClusterRancherName) 225 if err != nil { 226 _, _ = fmt.Fprintf(vzHelper.GetErrorStream(), err.Error()+"\n") 227 } 228 } 229 230 // getUninstallJobPodName returns the name of the pod for the verrazzano-uninstall job 231 // The uninstall job is triggered by deleting the Verrazzano custom resource 232 func getUninstallJobPodName(c client.Client, vzHelper helpers.VZHelper, jobName string) (string, error) { 233 // Find the verrazzano-uninstall pod using the job-name label selector 234 jobNameLabel, _ := labels.NewRequirement("job-name", selection.Equals, []string{jobName}) 235 labelSelector := labels.NewSelector() 236 labelSelector = labelSelector.Add(*jobNameLabel) 237 podList := corev1.PodList{} 238 239 // Provide the user with feedback while waiting for the verrazzano-uninstall pod to be ready 240 feedbackChan := make(chan bool) 241 defer close(feedbackChan) 242 go func(outputStream io.Writer) { 243 seconds := 0 244 for { 245 select { 246 case <-feedbackChan: 247 return 248 default: 249 time.Sleep(verrazzanoUninstallJobDetectWait * time.Second) 250 seconds += verrazzanoUninstallJobDetectWait 251 fmt.Fprintf(outputStream, fmt.Sprintf("\rWaiting for %s pod to be ready before starting uninstall - %d seconds", jobName, seconds)) 252 } 253 } 254 }(vzHelper.GetOutputStream()) 255 256 // Wait for the verrazzano-uninstall pod to be found 257 seconds := 0 258 retryCount := 0 259 for { 260 retryCount++ 261 if retryCount > uninstallWaitRetries { 262 return "", fmt.Errorf("Waiting for %s, %s pod not found in namespace %s", jobName, jobName, vzconstants.VerrazzanoInstallNamespace) 263 } 264 time.Sleep(verrazzanoUninstallJobDetectWait * time.Second) 265 seconds += verrazzanoUninstallJobDetectWait 266 267 err := c.List( 268 context.TODO(), 269 &podList, 270 &client.ListOptions{ 271 Namespace: vzconstants.VerrazzanoInstallNamespace, 272 LabelSelector: labelSelector, 273 }) 274 if err != nil { 275 return "", fmt.Errorf("Waiting for %s, failed to list pods: %s", jobName, err.Error()) 276 } 277 if len(podList.Items) == 0 { 278 continue 279 } 280 if len(podList.Items) > 1 { 281 return "", fmt.Errorf("Waiting for %s, more than one %s pod was found in namespace %s", jobName, jobName, vzconstants.VerrazzanoInstallNamespace) 282 } 283 feedbackChan <- true 284 break 285 } 286 287 // We found the verrazzano-uninstall pod. Wait until it's containers are ready. 288 pod := &corev1.Pod{} 289 seconds = 0 290 for { 291 time.Sleep(verrazzanoUninstallJobDetectWait * time.Second) 292 seconds += verrazzanoUninstallJobDetectWait 293 294 err := c.Get(context.TODO(), types.NamespacedName{Namespace: podList.Items[0].Namespace, Name: podList.Items[0].Name}, pod) 295 if err != nil { 296 return "", err 297 } 298 299 ready := true 300 for _, container := range pod.Status.ContainerStatuses { 301 if !container.Ready { 302 ready = false 303 break 304 } 305 } 306 307 if ready { 308 _, _ = fmt.Fprintf(vzHelper.GetOutputStream(), "\n") 309 break 310 } 311 } 312 return pod.Name, nil 313 } 314 315 // waitForUninstallToComplete waits for the Verrazzano resource to no longer exist 316 func waitForUninstallToComplete(client client.Client, kubeClient kubernetes.Interface, vzHelper helpers.VZHelper, namespacedName types.NamespacedName, timeout time.Duration, vpoTimeout time.Duration, logFormat cmdhelpers.LogFormat, useUninstallJob bool) error { 317 resChan := make(chan error, 1) 318 defer close(resChan) 319 320 feedbackChan := make(chan bool) 321 defer close(feedbackChan) 322 323 rc, err := getScanner(useUninstallJob, client, kubeClient, vzHelper, namespacedName) 324 if err != nil { 325 return err 326 } 327 328 go func(outputStream io.Writer, sc *bufio.Scanner, useUninstallJob bool) { 329 re := regexp.MustCompile(cmdhelpers.VpoSimpleLogFormatRegexp) 330 var err error 331 secondsWaited := 0 332 maxSecondsToWait := int(vpoTimeout.Seconds()) 333 const secondsPerRetry = 10 334 335 for { 336 if sc == nil { 337 sc, err = getScanner(useUninstallJob, client, kubeClient, vzHelper, namespacedName) 338 if err != nil { 339 fmt.Fprintf(outputStream, fmt.Sprintf("Failed to connect to the uninstall output, waited %d of %d seconds to recover: %v\n", secondsWaited, maxSecondsToWait, err)) 340 secondsWaited += secondsPerRetry 341 if secondsWaited > maxSecondsToWait { 342 return 343 } 344 time.Sleep(secondsPerRetry * time.Second) 345 continue 346 } 347 secondsWaited = 0 348 sc.Split(bufio.ScanLines) 349 } 350 351 scannedOk := sc.Scan() 352 if !scannedOk { 353 errText := "" 354 if sc.Err() != nil { 355 errText = fmt.Sprintf(": %v", sc.Err()) 356 } 357 fmt.Fprintf(outputStream, fmt.Sprintf("Lost connection to the uninstall output, attempting to reconnect%s\n", errText)) 358 sc = nil 359 continue 360 } 361 362 if !useUninstallJob && logFormat == cmdhelpers.LogFormatSimple { 363 cmdhelpers.PrintSimpleLogFormat(sc, outputStream, re) 364 } else { 365 _, _ = fmt.Fprintf(outputStream, fmt.Sprintf("%s\n", sc.Text())) 366 } 367 } 368 }(vzHelper.GetOutputStream(), rc, useUninstallJob) 369 370 go func() { 371 for { 372 // Pause before each check 373 time.Sleep(1 * time.Second) 374 select { 375 case <-feedbackChan: 376 return 377 default: 378 // Return when the Verrazzano uninstall has completed 379 vz, err := helpers.GetVerrazzanoResource(client, namespacedName) 380 if vz == nil { 381 resChan <- nil 382 return 383 } 384 if err != nil && !errors.IsNotFound(err) { 385 resChan <- err 386 return 387 } 388 } 389 } 390 }() 391 392 var timeoutErr error 393 select { 394 case result := <-resChan: 395 if result == nil { 396 // Delete remaining Verrazzano resources, excluding CRDs 397 cleanupResources(client, vzHelper) 398 } 399 return result 400 case <-time.After(timeout): 401 if timeout.Nanoseconds() != 0 { 402 feedbackChan <- true 403 timeoutErr = fmt.Errorf("Timeout %v exceeded waiting for uninstall to complete", timeout.String()) 404 } 405 } 406 return timeoutErr 407 } 408 409 // getScanner - get scanner for uninstall console output 410 func getScanner(useUninstallJob bool, client client.Client, kubeClient kubernetes.Interface, vzHelper helpers.VZHelper, namespacedName types.NamespacedName) (*bufio.Scanner, error) { 411 var podName string 412 var err error 413 if useUninstallJob { 414 // Get the uninstall job for streaming the logs 415 jobName := constants.VerrazzanoUninstall + "-" + namespacedName.Name 416 podName, err = getUninstallJobPodName(client, vzHelper, jobName) 417 } else { 418 // Get the VPO pod for streaming the logs 419 podName, err = cmdhelpers.GetVerrazzanoPlatformOperatorPodName(client) 420 } 421 if err != nil { 422 return nil, err 423 } 424 425 var rc io.ReadCloser 426 if useUninstallJob { 427 rc, err = getUninstallJobLogStream(kubeClient, podName) 428 } else { 429 rc, err = cmdhelpers.GetVpoLogStream(kubeClient, podName) 430 } 431 if err != nil { 432 return nil, err 433 } 434 435 return bufio.NewScanner(rc), nil 436 } 437 438 // getUninstallJobLogStream returns the stream to the uninstall job log file 439 func getUninstallJobLogStream(kubeClient kubernetes.Interface, uninstallPodName string) (io.ReadCloser, error) { 440 // Tail the log messages from the uninstall job log starting at the current time. 441 sinceTime := metav1.Now() 442 rc, err := kubeClient.CoreV1().Pods(vzconstants.VerrazzanoInstallNamespace).GetLogs(uninstallPodName, &corev1.PodLogOptions{ 443 Container: "uninstall", 444 Follow: true, 445 SinceTime: &sinceTime, 446 }).Stream(context.TODO()) 447 if err != nil { 448 return nil, fmt.Errorf("Failed to read the %s log file: %s", uninstallPodName, err.Error()) 449 } 450 return rc, nil 451 } 452 453 // deleteNamespace deletes a given Namespace 454 func deleteNamespace(client client.Client, name string) error { 455 ns := &corev1.Namespace{ 456 ObjectMeta: metav1.ObjectMeta{ 457 Name: name, 458 }, 459 } 460 461 err := client.Delete(context.TODO(), ns, deleteOptions) 462 if err != nil && !errors.IsNotFound(err) { 463 return fmt.Errorf("Failed to delete Namespace resource %s: %s", name, err.Error()) 464 } 465 return nil 466 } 467 468 // deleteWebhookConfiguration deletes a given ValidatingWebhookConfiguration 469 func deleteWebhookConfiguration(client client.Client, name string) error { 470 vwc := &adminv1.ValidatingWebhookConfiguration{ 471 ObjectMeta: metav1.ObjectMeta{ 472 Name: name, 473 }, 474 } 475 476 err := client.Delete(context.TODO(), vwc, deleteOptions) 477 if err != nil && !errors.IsNotFound(err) { 478 return fmt.Errorf("Failed to delete ValidatingWebhookConfiguration resource %s: %s", name, err.Error()) 479 } 480 return nil 481 } 482 483 // deleteMutatingWebhookConfiguration deletes a given MutatingWebhookConfiguration 484 func deleteMutatingWebhookConfiguration(client client.Client, name string) error { 485 mwc := &adminv1.MutatingWebhookConfiguration{ 486 ObjectMeta: metav1.ObjectMeta{ 487 Name: name, 488 }, 489 } 490 491 err := client.Delete(context.TODO(), mwc, deleteOptions) 492 if err != nil && !errors.IsNotFound(err) { 493 return fmt.Errorf("Failed to delete MutatingWebhookConfiguration resource %s: %s", name, err.Error()) 494 } 495 return nil 496 } 497 498 // deleteClusterRoleBinding deletes a given ClusterRoleBinding 499 func deleteClusterRoleBinding(client client.Client, name string) error { 500 crb := &rbacv1.ClusterRoleBinding{ 501 ObjectMeta: metav1.ObjectMeta{ 502 Name: name, 503 }, 504 } 505 506 err := client.Delete(context.TODO(), crb, deleteOptions) 507 if err != nil && !errors.IsNotFound(err) { 508 return fmt.Errorf("Failed to delete ClusterRoleBinding resource %s: %s", name, err.Error()) 509 } 510 return nil 511 } 512 513 // deleteClusterRole deletes a given ClusterRole 514 func deleteClusterRole(client client.Client, name string) error { 515 cr := &rbacv1.ClusterRole{ 516 ObjectMeta: metav1.ObjectMeta{ 517 Name: name, 518 }, 519 } 520 521 err := client.Delete(context.TODO(), cr, deleteOptions) 522 if err != nil && !errors.IsNotFound(err) { 523 return fmt.Errorf("Failed to delete ClusterRole resource %s: %s", name, err.Error()) 524 } 525 return nil 526 } 527 528 func failedToUninstallErr(err error) error { 529 return fmt.Errorf("Failed to uninstall Verrazzano: %s", err.Error()) 530 } 531 532 func continueUninstall(confirmUninstall bool) (bool, error) { 533 if confirmUninstall { 534 return true, nil 535 } 536 var response string 537 scanner := bufio.NewScanner(os.Stdin) 538 fmt.Print("Are you sure you want to uninstall Verrazzano? [y/N]: ") 539 if scanner.Scan() { 540 response = scanner.Text() 541 } 542 if err := scanner.Err(); err != nil { 543 return false, err 544 } 545 if response == "y" || response == "Y" { 546 return true, nil 547 } 548 return false, nil 549 }