istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/cmd/mesh/uninstall.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package mesh
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"os"
    21  	"strings"
    22  
    23  	"github.com/spf13/cobra"
    24  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    25  
    26  	"istio.io/api/operator/v1alpha1"
    27  	"istio.io/istio/istioctl/pkg/cli"
    28  	"istio.io/istio/istioctl/pkg/tag"
    29  	iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    30  	"istio.io/istio/operator/pkg/cache"
    31  	"istio.io/istio/operator/pkg/helmreconciler"
    32  	"istio.io/istio/operator/pkg/manifest"
    33  	"istio.io/istio/operator/pkg/object"
    34  	"istio.io/istio/operator/pkg/translate"
    35  	"istio.io/istio/operator/pkg/util/clog"
    36  	"istio.io/istio/operator/pkg/util/progress"
    37  	"istio.io/istio/pkg/kube"
    38  	proxyinfo "istio.io/istio/pkg/proxy"
    39  )
    40  
    41  type uninstallArgs struct {
    42  	// skipConfirmation determines whether the user is prompted for confirmation.
    43  	// If set to true, the user is not prompted and a Yes response is assumed in all cases.
    44  	skipConfirmation bool
    45  	// force proceeds even if there are validation errors
    46  	force bool
    47  	// purge results in deletion of all Istio resources.
    48  	purge bool
    49  	// revision is the Istio control plane revision the command targets.
    50  	revision string
    51  	// filename is the path of input IstioOperator CR.
    52  	filename string
    53  	// set is a string with element format "path=value" where path is an IstioOperator path and the value is a
    54  	// value to set the node at that path to.
    55  	set []string
    56  	// manifestsPath is a path to a charts and profiles directory in the local filesystem with a release tgz.
    57  	manifestsPath string
    58  	// verbose generates verbose output.
    59  	verbose bool
    60  }
    61  
    62  const (
    63  	AllResourcesRemovedWarning = "All Istio resources will be pruned from the cluster\n"
    64  	NoResourcesRemovedWarning  = "No resources will be pruned from the cluster. Please double check the input configs\n"
    65  	GatewaysRemovedWarning     = "You are about to remove the following gateways: %s." +
    66  		" To avoid downtime, please quit this command and reinstall the gateway(s) with a revision that is not being removed from the cluster.\n"
    67  	PurgeWithRevisionOrOperatorSpecifiedWarning = "Purge uninstall will remove all Istio resources, ignoring the specified revision or operator file"
    68  )
    69  
    70  func addUninstallFlags(cmd *cobra.Command, args *uninstallArgs) {
    71  	cmd.PersistentFlags().BoolVarP(&args.skipConfirmation, "skip-confirmation", "y", false, skipConfirmationFlagHelpStr)
    72  	cmd.PersistentFlags().BoolVar(&args.force, "force", false, ForceFlagHelpStr)
    73  	cmd.PersistentFlags().BoolVar(&args.purge, "purge", false, "Delete all Istio related sources for all versions")
    74  	cmd.PersistentFlags().StringVarP(&args.revision, "revision", "r", "", revisionFlagHelpStr)
    75  	cmd.PersistentFlags().StringVarP(&args.filename, "filename", "f", "",
    76  		"The filename of the IstioOperator CR.")
    77  	cmd.PersistentFlags().StringVarP(&args.manifestsPath, "manifests", "d", "", ManifestsFlagHelpStr)
    78  	cmd.PersistentFlags().StringArrayVarP(&args.set, "set", "s", nil, setFlagHelpStr)
    79  	cmd.PersistentFlags().BoolVarP(&args.verbose, "verbose", "v", false, "Verbose output.")
    80  }
    81  
    82  // UninstallCmd command uninstalls Istio from a cluster
    83  func UninstallCmd(ctx cli.Context) *cobra.Command {
    84  	rootArgs := &RootArgs{}
    85  	uiArgs := &uninstallArgs{}
    86  	uicmd := &cobra.Command{
    87  		Use:   "uninstall",
    88  		Short: "Uninstall Istio from a cluster",
    89  		Long:  "The uninstall command uninstalls Istio from a cluster",
    90  		Example: `  # Uninstall a single control plane by revision
    91    istioctl uninstall --revision foo
    92  
    93    # Uninstall a single control plane by iop file
    94    istioctl uninstall -f iop.yaml
    95    
    96    # Uninstall all control planes and shared resources
    97    istioctl uninstall --purge`,
    98  		Args: func(cmd *cobra.Command, args []string) error {
    99  			if uiArgs.revision == "" && manifest.GetValueForSetFlag(uiArgs.set, "revision") == "" && uiArgs.filename == "" && !uiArgs.purge {
   100  				return fmt.Errorf("at least one of the --revision (or --set revision=<revision>), --filename or --purge flags must be set")
   101  			}
   102  			if len(args) > 0 {
   103  				return fmt.Errorf("istioctl uninstall does not take arguments")
   104  			}
   105  			return nil
   106  		},
   107  		RunE: func(cmd *cobra.Command, args []string) error {
   108  			return uninstall(cmd, ctx, rootArgs, uiArgs)
   109  		},
   110  	}
   111  	addFlags(uicmd, rootArgs)
   112  	addUninstallFlags(uicmd, uiArgs)
   113  	return uicmd
   114  }
   115  
   116  // uninstall uninstalls control plane by either pruning by target revision or deleting specified manifests.
   117  func uninstall(cmd *cobra.Command, ctx cli.Context, rootArgs *RootArgs, uiArgs *uninstallArgs) error {
   118  	l := clog.NewConsoleLogger(cmd.OutOrStdout(), cmd.ErrOrStderr(), installerScope)
   119  	cliClient, err := ctx.CLIClient()
   120  	if err != nil {
   121  		return err
   122  	}
   123  	kubeClient, client, err := KubernetesClients(cliClient, l)
   124  	if err != nil {
   125  		l.LogAndFatal(err)
   126  	}
   127  	var kubeClientWithRev kube.CLIClient
   128  	if uiArgs.revision != "" && uiArgs.revision != "default" {
   129  		kubeClientWithRev, err = ctx.CLIClientWithRevision(uiArgs.revision)
   130  		if err != nil {
   131  			return err
   132  		}
   133  	} else {
   134  		kubeClientWithRev = kubeClient
   135  	}
   136  
   137  	if uiArgs.revision != "" {
   138  		revisions, err := tag.ListRevisionDescriptions(kubeClient)
   139  		if err != nil {
   140  			return fmt.Errorf("could not list revisions: %s", err)
   141  		}
   142  		if _, exists := revisions[uiArgs.revision]; !exists {
   143  			return errors.New("could not find target revision")
   144  		}
   145  	}
   146  
   147  	cache.FlushObjectCaches()
   148  	opts := &helmreconciler.Options{DryRun: rootArgs.DryRun, Log: l, ProgressLog: progress.NewLog()}
   149  	var h *helmreconciler.HelmReconciler
   150  
   151  	// If the user is performing a purge install but also specified a revision or filename, we should warn
   152  	// that the purge will still remove all resources
   153  	if uiArgs.purge && (uiArgs.revision != "" || uiArgs.filename != "") {
   154  		l.LogAndPrint(PurgeWithRevisionOrOperatorSpecifiedWarning)
   155  	}
   156  	// If only revision flag is set, we would prune resources by the revision label.
   157  	// Otherwise we would merge the revision flag and the filename flag and delete resources by following the
   158  	// owning name label.
   159  	var iop *iopv1alpha1.IstioOperator
   160  	if uiArgs.filename == "" {
   161  		emptyiops := &v1alpha1.IstioOperatorSpec{Profile: "empty", Revision: uiArgs.revision}
   162  		iop, err = translate.IOPStoIOP(emptyiops, "", "")
   163  		if err != nil {
   164  			return err
   165  		}
   166  	} else {
   167  		_, iop, err = manifest.GenManifests([]string{uiArgs.filename},
   168  			applyFlagAliases(uiArgs.set, uiArgs.manifestsPath, uiArgs.revision), uiArgs.force, nil, kubeClient, l)
   169  		if err != nil {
   170  			return err
   171  		}
   172  		iop.Name = savedIOPName(iop)
   173  	}
   174  
   175  	h, err = helmreconciler.NewHelmReconciler(client, kubeClient, iop, opts)
   176  	if err != nil {
   177  		return fmt.Errorf("failed to create reconciler: %v", err)
   178  	}
   179  	objectsList, err := h.GetPrunedResources(uiArgs.revision, uiArgs.purge, "")
   180  	if err != nil {
   181  		return err
   182  	}
   183  	preCheckWarnings(cmd, kubeClientWithRev, uiArgs, ctx.IstioNamespace(), uiArgs.revision, objectsList, nil, l, rootArgs.DryRun)
   184  
   185  	if err := h.DeleteObjectsList(objectsList, ""); err != nil {
   186  		return fmt.Errorf("failed to delete control plane resources by revision: %v", err)
   187  	}
   188  	opts.ProgressLog.SetState(progress.StateUninstallComplete)
   189  	return nil
   190  }
   191  
   192  // preCheckWarnings checks possible breaking changes and issue warnings to users, it checks the following:
   193  // 1. checks proxies still pointing to the target control plane revision.
   194  // 2. lists to be pruned resources if user uninstall by --revision flag.
   195  func preCheckWarnings(cmd *cobra.Command, kubeClient kube.CLIClient, uiArgs *uninstallArgs, istioNamespace,
   196  	rev string, resourcesList []*unstructured.UnstructuredList, objectsList object.K8sObjects, l *clog.ConsoleLogger, dryRun bool,
   197  ) {
   198  	pids, err := proxyinfo.GetIDsFromProxyInfo(kubeClient, istioNamespace)
   199  	needConfirmation, message := false, ""
   200  	if uiArgs.purge {
   201  		needConfirmation = true
   202  		message += AllResourcesRemovedWarning
   203  	} else {
   204  		rmListString, gwList := constructResourceListOutput(resourcesList, objectsList)
   205  		if rmListString == "" {
   206  			l.LogAndPrint(NoResourcesRemovedWarning)
   207  			return
   208  		}
   209  		if uiArgs.verbose {
   210  			message += fmt.Sprintf("The following resources will be pruned from the cluster: %s\n",
   211  				rmListString)
   212  		}
   213  
   214  		if len(pids) != 0 && rev != "" {
   215  			needConfirmation = true
   216  			message += fmt.Sprintf("There are still %d proxies pointing to the control plane revision %s\n", len(pids), rev)
   217  			// just print the count only if there is a large list of proxies
   218  			if len(pids) <= 30 {
   219  				message += fmt.Sprintf("%s\n", strings.Join(pids, "\n"))
   220  			}
   221  			message += "If you proceed with the uninstall, these proxies will become detached from any control plane" +
   222  				" and will not function correctly.\n"
   223  		} else if rev != "" && err != nil {
   224  			needConfirmation = true
   225  			message += fmt.Sprintf("Unable to find any proxies pointing to the %s control plane. "+
   226  				"This may be because the control plane cannot be connected or there is no %s control plane.\n", rev, rev)
   227  		}
   228  		if gwList != "" {
   229  			needConfirmation = true
   230  			message += fmt.Sprintf(GatewaysRemovedWarning, gwList)
   231  		}
   232  	}
   233  	if dryRun || uiArgs.skipConfirmation {
   234  		l.LogAndPrint(message)
   235  		return
   236  	}
   237  	message += "Proceed? (y/N)"
   238  	if needConfirmation && !Confirm(message, cmd.OutOrStdout()) {
   239  		cmd.Print("Cancelled.\n")
   240  		os.Exit(1)
   241  	}
   242  }
   243  
   244  // constructResourceListOutput is a helper function to construct the output of to be removed resources list
   245  func constructResourceListOutput(resourcesList []*unstructured.UnstructuredList, objectsList object.K8sObjects) (string, string) {
   246  	var items []unstructured.Unstructured
   247  	if objectsList != nil {
   248  		items = objectsList.UnstructuredItems()
   249  	}
   250  	for _, usList := range resourcesList {
   251  		items = append(items, usList.Items...)
   252  	}
   253  	kindNameMap := make(map[string][]string)
   254  	for _, o := range items {
   255  		nameList := kindNameMap[o.GetKind()]
   256  		if nameList == nil {
   257  			kindNameMap[o.GetKind()] = []string{}
   258  		}
   259  		kindNameMap[o.GetKind()] = append(kindNameMap[o.GetKind()], o.GetName())
   260  	}
   261  	if len(kindNameMap) == 0 {
   262  		return "", ""
   263  	}
   264  	output, gwlist := "", []string{}
   265  	for kind, name := range kindNameMap {
   266  		output += fmt.Sprintf("%s: %s. ", kind, strings.Join(name, ", "))
   267  		if kind == "Deployment" {
   268  			for _, n := range name {
   269  				if strings.Contains(n, "gateway") {
   270  					gwlist = append(gwlist, n)
   271  				}
   272  			}
   273  		}
   274  	}
   275  	return output, strings.Join(gwlist, ", ")
   276  }