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  }