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 }