github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/kubeblocks/uninstall.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package kubeblocks 21 22 import ( 23 "bytes" 24 "context" 25 "fmt" 26 "sort" 27 "strings" 28 "time" 29 30 "github.com/pkg/errors" 31 "github.com/spf13/cobra" 32 "golang.org/x/exp/maps" 33 "helm.sh/helm/v3/pkg/repo" 34 apierrors "k8s.io/apimachinery/pkg/api/errors" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 k8sapitypes "k8s.io/apimachinery/pkg/types" 39 utilerrors "k8s.io/apimachinery/pkg/util/errors" 40 "k8s.io/apimachinery/pkg/util/wait" 41 "k8s.io/cli-runtime/pkg/genericiooptions" 42 "k8s.io/client-go/dynamic" 43 "k8s.io/klog/v2" 44 cmdutil "k8s.io/kubectl/pkg/cmd/util" 45 "k8s.io/kubectl/pkg/util/templates" 46 47 extensionsv1alpha1 "github.com/1aal/kubeblocks/apis/extensions/v1alpha1" 48 "github.com/1aal/kubeblocks/pkg/cli/printer" 49 "github.com/1aal/kubeblocks/pkg/cli/spinner" 50 "github.com/1aal/kubeblocks/pkg/cli/types" 51 "github.com/1aal/kubeblocks/pkg/cli/util" 52 "github.com/1aal/kubeblocks/pkg/cli/util/helm" 53 ) 54 55 var ( 56 uninstallExample = templates.Examples(` 57 # uninstall KubeBlocks 58 kbcli kubeblocks uninstall`) 59 ) 60 61 type UninstallOptions struct { 62 Factory cmdutil.Factory 63 Options 64 65 // AutoApprove if true, skip interactive approval 66 AutoApprove bool 67 removePVs bool 68 removePVCs bool 69 RemoveNamespace bool 70 addons []*extensionsv1alpha1.Addon 71 Quiet bool 72 force bool 73 } 74 75 func newUninstallCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 76 o := &UninstallOptions{ 77 Options: Options{ 78 IOStreams: streams, 79 }, 80 Factory: f, 81 force: true, 82 } 83 cmd := &cobra.Command{ 84 Use: "uninstall", 85 Short: "Uninstall KubeBlocks.", 86 Args: cobra.NoArgs, 87 Example: uninstallExample, 88 Run: func(cmd *cobra.Command, args []string) { 89 util.CheckErr(o.Complete(f, cmd)) 90 util.CheckErr(o.PreCheck()) 91 util.CheckErr(o.Uninstall()) 92 }, 93 } 94 95 cmd.Flags().BoolVar(&o.AutoApprove, "auto-approve", false, "Skip interactive approval before uninstalling KubeBlocks") 96 cmd.Flags().BoolVar(&o.removePVs, "remove-pvs", false, "Remove PersistentVolume or not") 97 cmd.Flags().BoolVar(&o.removePVCs, "remove-pvcs", false, "Remove PersistentVolumeClaim or not") 98 cmd.Flags().BoolVar(&o.RemoveNamespace, "remove-namespace", false, "Remove default created \"kb-system\" namespace or not") 99 cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for uninstalling KubeBlocks, such as --timeout=5m") 100 cmd.Flags().BoolVar(&o.Wait, "wait", true, "Wait for KubeBlocks to be uninstalled, including all the add-ons. It will wait for a --timeout period") 101 return cmd 102 } 103 104 func (o *UninstallOptions) PreCheck() error { 105 // wait user to confirm 106 if !o.AutoApprove { 107 printer.Warning(o.Out, "this action will remove all KubeBlocks resources.\n") 108 if err := confirmUninstall(o.In); err != nil { 109 return err 110 } 111 } 112 113 // check if there is any resource should be removed first, if so, return error 114 // and ask user to remove them manually 115 if err := checkResources(o.Dynamic); err != nil { 116 return err 117 } 118 119 // verify where kubeblocks is installed 120 kbNamespace, err := util.GetKubeBlocksNamespace(o.Client) 121 if err != nil { 122 printer.Warning(o.Out, "failed to locate KubeBlocks meta, will clean up all KubeBlocks resources.\n") 123 if !o.Quiet { 124 fmt.Fprintf(o.Out, "to find out the namespace where KubeBlocks is installed, please use:\n\t'kbcli kubeblocks status'\n") 125 fmt.Fprintf(o.Out, "to uninstall KubeBlocks completely, please use:\n\t`kbcli kubeblocks uninstall -n <namespace>`\n") 126 } 127 } 128 129 o.Namespace = kbNamespace 130 if kbNamespace != "" { 131 fmt.Fprintf(o.Out, "Uninstall KubeBlocks in namespace \"%s\"\n", kbNamespace) 132 } 133 134 return nil 135 } 136 137 func (o *UninstallOptions) Uninstall() error { 138 printSpinner := func(s spinner.Interface, err error) { 139 if err == nil || apierrors.IsNotFound(err) || 140 strings.Contains(err.Error(), "release: not found") { 141 s.Success() 142 return 143 } 144 s.Fail() 145 fmt.Fprintf(o.Out, " %s\n", err.Error()) 146 } 147 newSpinner := func(msg string) spinner.Interface { 148 return spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", msg))) 149 } 150 151 // uninstall all KubeBlocks addons 152 if err := o.uninstallAddons(); err != nil { 153 fmt.Fprintf(o.Out, "Failed to uninstall addons, run \"kbcli kubeblocks uninstall\" to retry.\n") 154 return err 155 } 156 157 // uninstall helm release that will delete custom resources, but since finalizers is not empty, 158 // custom resources will not be deleted, so we will remove finalizers later. 159 v, _ := util.GetVersionInfo(o.Client) 160 chart := helm.InstallOpts{ 161 Name: types.KubeBlocksChartName, 162 Namespace: o.Namespace, 163 ForceUninstall: o.force, 164 // KubeBlocks chart has a hook to delete addons, but we have already deleted addons, 165 // and that webhook may fail, so we need to disable hooks. 166 DisableHooks: true, 167 } 168 printSpinner(newSpinner("Uninstall helm release "+types.KubeBlocksReleaseName+" "+v.KubeBlocks), 169 chart.Uninstall(o.HelmCfg)) 170 171 // remove repo 172 printSpinner(newSpinner("Remove helm repo "+types.KubeBlocksChartName), 173 helm.RemoveRepo(&repo.Entry{Name: types.KubeBlocksChartName})) 174 175 // get KubeBlocks objects, then try to remove them 176 objs, err := getKBObjects(o.Dynamic, o.Namespace, o.addons) 177 if err != nil { 178 fmt.Fprintf(o.ErrOut, "Failed to get KubeBlocks objects %s", err.Error()) 179 } 180 181 // remove finalizers of custom resources, then that will be deleted 182 printSpinner(newSpinner("Remove built-in custom resources"), removeCustomResources(o.Dynamic, objs)) 183 184 var gvrs []schema.GroupVersionResource 185 for k := range objs { 186 gvrs = append(gvrs, k) 187 } 188 sort.SliceStable(gvrs, func(i, j int) bool { 189 g1 := gvrs[i] 190 g2 := gvrs[j] 191 return strings.Compare(g1.Resource, g2.Resource) < 0 192 }) 193 194 for _, gvr := range gvrs { 195 if gvr == types.PVCGVR() && !o.removePVCs { 196 continue 197 } 198 if gvr == types.PVGVR() && !o.removePVs { 199 continue 200 } 201 if v, ok := objs[gvr]; !ok || len(v.Items) == 0 { 202 continue 203 } 204 printSpinner(newSpinner("Remove "+gvr.Resource), deleteObjects(o.Dynamic, gvr, objs[gvr])) 205 } 206 207 // delete namespace if it is default namespace 208 if o.Namespace == types.DefaultNamespace && o.RemoveNamespace { 209 printSpinner(newSpinner("Remove namespace "+types.DefaultNamespace), 210 deleteNamespace(o.Client, types.DefaultNamespace)) 211 } 212 213 if o.Wait { 214 fmt.Fprintln(o.Out, "Uninstall KubeBlocks done.") 215 } else { 216 fmt.Fprintf(o.Out, "KubeBlocks is uninstalling, run \"kbcli kubeblocks status -A\" to check kubeblocks resources.\n") 217 } 218 return nil 219 } 220 221 // uninstallAddons uninstalls all KubeBlocks addons 222 func (o *UninstallOptions) uninstallAddons() error { 223 var ( 224 allErrs []error 225 err error 226 header = "Wait for addons to be disabled" 227 s spinner.Interface 228 msg string 229 ) 230 231 addons := make(map[string]*extensionsv1alpha1.Addon) 232 processAddons := func(uninstall bool) error { 233 objects, err := o.Dynamic.Resource(types.AddonGVR()).List(context.TODO(), metav1.ListOptions{ 234 LabelSelector: buildKubeBlocksSelectorLabels(), 235 }) 236 if err != nil && !apierrors.IsNotFound(err) { 237 klog.V(1).Infof("Failed to get KubeBlocks addons %s", err.Error()) 238 allErrs = append(allErrs, err) 239 return utilerrors.NewAggregate(allErrs) 240 } 241 if objects == nil { 242 return nil 243 } 244 245 for _, obj := range objects.Items { 246 addon := &extensionsv1alpha1.Addon{} 247 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, addon); err != nil { 248 klog.V(1).Infof("Failed to convert KubeBlocks addon %s", err.Error()) 249 allErrs = append(allErrs, err) 250 continue 251 } 252 253 if uninstall { 254 // we only need to uninstall addons that are not disabled 255 if addon.Spec.InstallSpec.IsDisabled() { 256 continue 257 } 258 addons[addon.Name] = addon 259 o.addons = append(o.addons, addon) 260 261 // uninstall addons 262 if err = disableAddon(o.Dynamic, addon); err != nil { 263 klog.V(1).Infof("Failed to uninstall KubeBlocks addon %s %s", addon.Name, err.Error()) 264 allErrs = append(allErrs, err) 265 } 266 } else { 267 // update cached addon if exists 268 if _, ok := addons[addon.Name]; ok { 269 addons[addon.Name] = addon 270 } 271 } 272 } 273 return utilerrors.NewAggregate(allErrs) 274 } 275 276 suffixMsg := func(msg string) string { 277 return fmt.Sprintf("%-50s", msg) 278 } 279 280 if !o.Wait { 281 s = spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", "Uninstall KubeBlocks addons"))) 282 } else { 283 s = spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", header))) 284 } 285 286 // get all addons and uninstall them 287 if err = processAddons(true); err != nil { 288 s.Fail() 289 return err 290 } 291 292 if len(addons) == 0 || !o.Wait { 293 s.Success() 294 return nil 295 } 296 297 spinnerDone := func() { 298 s.SetFinalMsg(msg) 299 s.Done("") 300 fmt.Fprintln(o.Out) 301 } 302 303 // check if all addons are disabled, if so, then we will stop checking addons 304 // status otherwise, we will wait for a while and check again 305 if err = wait.PollImmediate(5*time.Second, o.Timeout, func() (bool, error) { 306 // we only check addons status, do not try to uninstall addons again 307 if err = processAddons(false); err != nil { 308 return false, err 309 } 310 status := checkAddons(maps.Values(addons), false) 311 msg = suffixMsg(fmt.Sprintf("%s\n %s", header, status.outputMsg)) 312 s.SetMessage(msg) 313 if status.allDisabled { 314 spinnerDone() 315 return true, nil 316 } else if status.hasFailed { 317 return false, errors.New("some addons are failed to disabled") 318 } 319 return false, nil 320 }); err != nil { 321 spinnerDone() 322 printAddonMsg(o.Out, maps.Values(addons), false) 323 allErrs = append(allErrs, err) 324 } 325 return utilerrors.NewAggregate(allErrs) 326 } 327 328 func checkResources(dynamic dynamic.Interface) error { 329 ctx := context.Background() 330 gvrList := []schema.GroupVersionResource{ 331 types.ClusterGVR(), 332 types.BackupGVR(), 333 } 334 335 crs := map[string][]string{} 336 for _, gvr := range gvrList { 337 objList, err := dynamic.Resource(gvr).List(ctx, metav1.ListOptions{}) 338 if err != nil && !apierrors.IsNotFound(err) { 339 return err 340 } 341 342 if objList == nil { 343 continue 344 } 345 346 for _, item := range objList.Items { 347 crs[gvr.Resource] = append(crs[gvr.Resource], item.GetName()) 348 } 349 } 350 351 if len(crs) > 0 { 352 errMsg := bytes.NewBufferString("failed to uninstall, the following resources need to be removed first\n") 353 for k, v := range crs { 354 errMsg.WriteString(fmt.Sprintf(" %s: %s\n", k, strings.Join(v, " "))) 355 } 356 return errors.Errorf(errMsg.String()) 357 } 358 return nil 359 } 360 361 func disableAddon(dynamic dynamic.Interface, addon *extensionsv1alpha1.Addon) error { 362 klog.V(1).Infof("Uninstall %s, status %s", addon.Name, addon.Status.Phase) 363 if _, err := dynamic.Resource(types.AddonGVR()).Patch(context.TODO(), addon.Name, k8sapitypes.JSONPatchType, 364 []byte("[{\"op\": \"replace\", \"path\": \"/spec/install/enabled\", \"value\": false }]"), 365 metav1.PatchOptions{}); err != nil && !apierrors.IsNotFound(err) { 366 return err 367 } 368 return nil 369 }