github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/playground/destroy.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 playground 21 22 import ( 23 "context" 24 "fmt" 25 "os" 26 "strings" 27 "time" 28 29 "github.com/spf13/cobra" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 32 "k8s.io/apimachinery/pkg/runtime" 33 apitypes "k8s.io/apimachinery/pkg/types" 34 "k8s.io/apimachinery/pkg/util/wait" 35 "k8s.io/cli-runtime/pkg/genericiooptions" 36 "k8s.io/client-go/dynamic" 37 "k8s.io/client-go/kubernetes" 38 "k8s.io/klog/v2" 39 cmdutil "k8s.io/kubectl/pkg/cmd/util" 40 "k8s.io/kubectl/pkg/util/templates" 41 42 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 43 cp "github.com/1aal/kubeblocks/pkg/cli/cloudprovider" 44 "github.com/1aal/kubeblocks/pkg/cli/cmd/kubeblocks" 45 "github.com/1aal/kubeblocks/pkg/cli/printer" 46 "github.com/1aal/kubeblocks/pkg/cli/spinner" 47 "github.com/1aal/kubeblocks/pkg/cli/types" 48 "github.com/1aal/kubeblocks/pkg/cli/util" 49 "github.com/1aal/kubeblocks/pkg/cli/util/helm" 50 "github.com/1aal/kubeblocks/pkg/cli/util/prompt" 51 ) 52 53 var ( 54 destroyExample = templates.Examples(` 55 # destroy playground cluster 56 kbcli playground destroy`) 57 ) 58 59 type destroyOptions struct { 60 genericiooptions.IOStreams 61 baseOptions 62 63 // purge resources, before destroying kubernetes cluster we should delete cluster and 64 // uninstall KubeBlocks 65 autoApprove bool 66 purge bool 67 // timeout represents the timeout for the destruction process. 68 timeout time.Duration 69 } 70 71 func newDestroyCmd(streams genericiooptions.IOStreams) *cobra.Command { 72 o := &destroyOptions{ 73 IOStreams: streams, 74 } 75 cmd := &cobra.Command{ 76 Use: "destroy", 77 Short: "Destroy the playground KubeBlocks and kubernetes cluster.", 78 Example: destroyExample, 79 Run: func(cmd *cobra.Command, args []string) { 80 util.CheckErr(o.complete(cmd)) 81 util.CheckErr(o.validate()) 82 util.CheckErr(o.destroy()) 83 }, 84 } 85 86 cmd.Flags().BoolVar(&o.purge, "purge", true, "Purge all resources before destroying kubernetes cluster, delete all clusters created by KubeBlocks and uninstall KubeBlocks.") 87 cmd.Flags().DurationVar(&o.timeout, "timeout", 300*time.Second, "Time to wait for destroying KubeBlocks, such as --timeout=10m") 88 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before destroying the playground") 89 return cmd 90 } 91 92 func (o *destroyOptions) destroy() error { 93 if o.prevCluster == nil { 94 return fmt.Errorf("no playground cluster found") 95 } 96 97 if o.prevCluster.CloudProvider == cp.Local { 98 return o.destroyLocal() 99 } 100 return o.destroyCloud() 101 } 102 103 // destroyLocal destroy local k3d cluster that will destroy all resources 104 func (o *destroyOptions) destroyLocal() error { 105 provider, _ := cp.New(cp.Local, "", o.Out, o.ErrOut) 106 s := spinner.New(o.Out, spinnerMsg("Delete playground k3d cluster "+o.prevCluster.ClusterName)) 107 defer s.Fail() 108 if err := provider.DeleteK8sCluster(o.prevCluster); err != nil { 109 if !strings.Contains(err.Error(), "no cluster found") && 110 !strings.Contains(err.Error(), "does not exist") { 111 return err 112 } 113 } 114 s.Success() 115 116 if err := o.removeKubeConfig(); err != nil { 117 return err 118 } 119 return o.removeStateFile() 120 } 121 122 // destroyCloud destroys cloud kubernetes cluster, before destroying, we should delete 123 // all clusters created by KubeBlocks, uninstall KubeBlocks and remove the KubeBlocks 124 // namespace that will destroy all resources created by KubeBlocks, avoid to leave resources behind 125 func (o *destroyOptions) destroyCloud() error { 126 var err error 127 128 printer.Warning(o.Out, `This action will destroy the kubernetes cluster, there may be residual resources, 129 please confirm and manually clean up related resources after this action. 130 131 `) 132 133 fmt.Fprintf(o.Out, "Do you really want to destroy the kubernetes cluster %s?\n%s\n\n The operation cannot be rollbacked. Only 'yes' will be accepted to confirm.\n\n", 134 o.prevCluster.ClusterName, o.prevCluster.String()) 135 136 // confirm to destroy 137 if !o.autoApprove { 138 entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() 139 if entered != yesStr { 140 fmt.Fprintf(o.Out, "\nPlayground destroy cancelled.\n") 141 return cmdutil.ErrExit 142 } 143 } 144 145 o.startTime = time.Now() 146 147 // for cloud provider, we should delete all clusters created by KubeBlocks first, 148 // uninstall KubeBlocks and remove the KubeBlocks namespace, then destroy the 149 // playground cluster, avoid to leave resources behind. 150 // delete all clusters created by KubeBlocks, MUST BE VERY CAUTIOUS, use the right 151 // kubeconfig and context, otherwise, it will delete the wrong cluster. 152 if err = o.deleteClustersAndUninstallKB(); err != nil { 153 if strings.Contains(err.Error(), kubeClusterUnreachableErr.Error()) { 154 printer.Warning(o.Out, err.Error()) 155 } else { 156 return err 157 } 158 } 159 160 // destroy playground kubernetes cluster 161 cpPath, err := cloudProviderRepoDir(o.prevCluster.KbcliVersion) 162 if err != nil { 163 return err 164 } 165 166 provider, err := cp.New(o.prevCluster.CloudProvider, cpPath, o.Out, o.ErrOut) 167 if err != nil { 168 return err 169 } 170 171 fmt.Fprintf(o.Out, "Destroy %s %s cluster %s...\n", 172 o.prevCluster.CloudProvider, cp.K8sService(o.prevCluster.CloudProvider), o.prevCluster.ClusterName) 173 if err = provider.DeleteK8sCluster(o.prevCluster); err != nil { 174 return err 175 } 176 177 // remove the cluster kubeconfig from the use default kubeconfig 178 if err = o.removeKubeConfig(); err != nil { 179 return err 180 } 181 182 // at last, remove the state file 183 if err = o.removeStateFile(); err != nil { 184 return err 185 } 186 187 fmt.Fprintf(o.Out, "Playground destroy completed in %s.\n", time.Since(o.startTime).Truncate(time.Second)) 188 return nil 189 } 190 191 func (o *destroyOptions) deleteClustersAndUninstallKB() error { 192 var err error 193 194 if !o.purge { 195 klog.V(1).Infof("Skip to delete all clusters created by KubeBlocks and uninstall KubeBlocks") 196 return nil 197 } 198 199 if o.prevCluster.KubeConfig == "" { 200 fmt.Fprintf(o.Out, "No kubeconfig found for kubernetes cluster %s in %s \n", 201 o.prevCluster.ClusterName, o.stateFilePath) 202 return nil 203 } 204 205 // write kubeconfig content to a temporary file and use it 206 if err = writeAndUseKubeConfig(o.prevCluster.KubeConfig, o.kubeConfigPath, o.Out); err != nil { 207 return err 208 } 209 210 client, dynamic, err := getKubeClient() 211 if err != nil { 212 return err 213 } 214 215 // delete all clusters created by KubeBlocks 216 if err = o.deleteClusters(dynamic); err != nil { 217 return err 218 } 219 220 // uninstall KubeBlocks and remove namespace created by KubeBlocks 221 return o.uninstallKubeBlocks(client, dynamic) 222 } 223 224 // delete all clusters created by KubeBlocks 225 func (o *destroyOptions) deleteClusters(dynamic dynamic.Interface) error { 226 var err error 227 ctx := context.Background() 228 // get all clusters in all namespaces 229 getClusters := func() (*unstructured.UnstructuredList, error) { 230 return dynamic.Resource(types.ClusterGVR()).Namespace(metav1.NamespaceAll). 231 List(context.Background(), metav1.ListOptions{}) 232 } 233 234 // get all clusters and check if satisfy the checkFn 235 checkClusters := func(checkFn func(cluster *appsv1alpha1.Cluster) bool) (bool, error) { 236 res := true 237 clusters, err := getClusters() 238 if err != nil { 239 return false, err 240 } 241 for _, item := range clusters.Items { 242 cluster := &appsv1alpha1.Cluster{} 243 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, cluster); err != nil { 244 return false, err 245 } 246 if !checkFn(cluster) { 247 res = false 248 break 249 } 250 } 251 return res, nil 252 } 253 254 // delete all clusters 255 deleteClusters := func(clusters *unstructured.UnstructuredList) error { 256 for _, cluster := range clusters.Items { 257 if err = dynamic.Resource(types.ClusterGVR()).Namespace(cluster.GetNamespace()). 258 Delete(ctx, cluster.GetName(), *metav1.NewDeleteOptions(0)); err != nil { 259 return err 260 } 261 } 262 return nil 263 } 264 265 s := spinner.New(o.Out, spinnerMsg("Delete clusters created by KubeBlocks")) 266 defer s.Fail() 267 268 // get all clusters 269 clusters, err := getClusters() 270 if clusters == nil || len(clusters.Items) == 0 { 271 s.Success() 272 return nil 273 } 274 275 checkWipeOut := false 276 // set all cluster termination policy to WipeOut to delete all resources, otherwise 277 // the cluster will be deleted but the resources will be left 278 for _, item := range clusters.Items { 279 cluster := &appsv1alpha1.Cluster{} 280 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, cluster); err != nil { 281 return err 282 } 283 if cluster.Spec.TerminationPolicy == appsv1alpha1.WipeOut { 284 continue 285 } 286 287 // terminate policy is not WipeOut, set it to WipeOut 288 klog.V(1).Infof("Set cluster %s termination policy to WipeOut", cluster.Name) 289 if _, err = dynamic.Resource(types.ClusterGVR()).Namespace(cluster.Namespace).Patch(ctx, cluster.Name, apitypes.JSONPatchType, 290 []byte(fmt.Sprintf("[{\"op\": \"replace\", \"path\": \"/spec/terminationPolicy\", \"value\": \"%s\" }]", 291 appsv1alpha1.WipeOut)), metav1.PatchOptions{}); err != nil { 292 return err 293 } 294 295 // set some cluster termination policy to WipeOut, need to check again 296 checkWipeOut = true 297 } 298 299 // check all clusters termination policy is WipeOut 300 if checkWipeOut { 301 if err = wait.PollUntilContextTimeout(context.Background(), 5*time.Second, 302 o.timeout, true, func(_ context.Context) (bool, error) { 303 return checkClusters(func(cluster *appsv1alpha1.Cluster) bool { 304 if cluster.Spec.TerminationPolicy != appsv1alpha1.WipeOut { 305 klog.V(1).Infof("Cluster %s termination policy is %s", cluster.Name, cluster.Spec.TerminationPolicy) 306 } 307 return cluster.Spec.TerminationPolicy == appsv1alpha1.WipeOut 308 }) 309 }); err != nil { 310 return err 311 } 312 } 313 314 // delete all clusters 315 if err = deleteClusters(clusters); err != nil { 316 return err 317 } 318 319 // check and wait all clusters are deleted 320 if err = wait.PollUntilContextTimeout(context.Background(), 5*time.Second, 321 o.timeout, true, func(_ context.Context) (bool, error) { 322 return checkClusters(func(cluster *appsv1alpha1.Cluster) bool { 323 // always return false if any cluster is not deleted 324 klog.V(1).Infof("Cluster %s is not deleted", cluster.Name) 325 return false 326 }) 327 }); err != nil { 328 return err 329 } 330 331 s.Success() 332 return nil 333 } 334 335 func (o *destroyOptions) uninstallKubeBlocks(client kubernetes.Interface, dynamic dynamic.Interface) error { 336 var err error 337 uninstall := kubeblocks.UninstallOptions{ 338 Options: kubeblocks.Options{ 339 IOStreams: o.IOStreams, 340 Client: client, 341 Dynamic: dynamic, 342 Wait: true, 343 }, 344 AutoApprove: true, 345 RemoveNamespace: true, 346 Quiet: true, 347 } 348 349 uninstall.HelmCfg = helm.NewConfig("", o.kubeConfigPath, "", klog.V(1).Enabled()) 350 if err = uninstall.PreCheck(); err != nil { 351 return err 352 } 353 if err = uninstall.Uninstall(); err != nil { 354 return err 355 } 356 return nil 357 } 358 359 func (o *destroyOptions) removeKubeConfig() error { 360 s := spinner.New(o.Out, spinnerMsg("Remove kubeconfig from "+defaultKubeConfigPath)) 361 defer s.Fail() 362 if err := kubeConfigRemove(o.prevCluster.KubeConfig, defaultKubeConfigPath); err != nil { 363 if os.IsNotExist(err) { 364 s.Success() 365 return nil 366 } else { 367 return err 368 } 369 } 370 s.Success() 371 372 clusterContext, err := kubeConfigCurrentContext(o.prevCluster.KubeConfig) 373 if err != nil { 374 return err 375 } 376 377 // check if current context in kubeconfig is deleted, if yes, notify user to set current context 378 currentContext, err := kubeConfigCurrentContextFromFile(defaultKubeConfigPath) 379 if err != nil { 380 return err 381 } 382 383 // current context is deleted, notify user to set current context with kubectl 384 if currentContext == clusterContext { 385 printer.Warning(o.Out, "this removed your active context, use \"kubectl config use-context\" to select a different one\n") 386 } 387 return nil 388 } 389 390 // remove state file 391 func (o *destroyOptions) removeStateFile() error { 392 s := spinner.New(o.Out, spinnerMsg("Remove state file %s", o.stateFilePath)) 393 defer s.Fail() 394 if err := removeStateFile(o.stateFilePath); err != nil { 395 return err 396 } 397 s.Success() 398 return nil 399 } 400 401 func (o *destroyOptions) complete(cmd *cobra.Command) error { 402 // enable log 403 if err := util.EnableLogToFile(cmd.Flags()); err != nil { 404 return fmt.Errorf("failed to enable the log file %s", err.Error()) 405 } 406 return nil 407 }