From a684056d27d72b052c241186850854d6e6977545 Mon Sep 17 00:00:00 2001 From: Matthieu Huin Date: Wed, 20 Dec 2023 17:36:18 +0100 Subject: [PATCH] CLI: add "SF wipe" subcommand The subcommand can be used to remove instances, PVs and even the operator from a cluster. It doesn't require Ansible to run. Use the subcommand in CI playbooks. Change-Id: Ia5ba21acaecf13f4ba32950c45d01afe7585f69f --- cli/cmd/root.go | 20 ++ cli/cmd/sf.go | 1 + cli/cmd/wipe.go | 200 ++++++++++++++++++ doc/reference/cli/main.md | 20 ++ playbooks/main.yaml | 2 +- playbooks/upgrade.yaml | 2 +- roles/clean-installations-cli/tasks/main.yaml | 7 + tools/wipe-deployment.sh | 5 - 8 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 cli/cmd/wipe.go create mode 100644 roles/clean-installations-cli/tasks/main.yaml delete mode 100755 tools/wipe-deployment.sh diff --git a/cli/cmd/root.go b/cli/cmd/root.go index fc6edf21..8e2973bf 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -195,6 +195,18 @@ func GetM(env *ENV, name string, obj client.Object) (bool, error) { } } +func DeleteOrDie(env *ENV, obj client.Object) bool { + err := env.Cli.Delete(env.Ctx, obj) + if apierrors.IsNotFound(err) { + return false + } else if err != nil { + msg := fmt.Sprintf("Error while deleting %s \"%s\"", reflect.TypeOf(obj).Name(), obj.GetName()) + ctrl.Log.Error(err, msg) + os.Exit(1) + } + return true +} + func GetMOrDie(env *ENV, name string, obj client.Object) bool { _, err := GetM(env, name, obj) if apierrors.IsNotFound(err) { @@ -232,6 +244,14 @@ func CreateROrDie(env *ENV, obj client.Object) { ctrl.Log.Info(msg) } +func DeleteAllOfOrDie(env *ENV, obj client.Object, opts ...client.DeleteAllOfOption) { + if err := env.Cli.DeleteAllOf(env.Ctx, obj, opts...); err != nil { + var msg = "Error while deleting" + ctrl.Log.Error(err, msg) + os.Exit(1) + } +} + func getCLIctxOrDie(kmd *cobra.Command, args []string, allowedArgs []string) SoftwareFactoryConfigContext { cliCtx, err := GetCLIContext(kmd) if err != nil { diff --git a/cli/cmd/sf.go b/cli/cmd/sf.go index 16f859fe..6d2f78d2 100644 --- a/cli/cmd/sf.go +++ b/cli/cmd/sf.go @@ -66,6 +66,7 @@ func MkSFCmd() *cobra.Command { sfCmd.AddCommand(MkBackupCmd()) sfCmd.AddCommand(MkRestoreCmd()) sfCmd.AddCommand(configureCmd) + sfCmd.AddCommand(MkWipeCmd()) return sfCmd } diff --git a/cli/cmd/wipe.go b/cli/cmd/wipe.go new file mode 100644 index 00000000..f621dd02 --- /dev/null +++ b/cli/cmd/wipe.go @@ -0,0 +1,200 @@ +/* +Copyright © 2023 Red Hat + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +/* +"wipe" subcommand cleans up Software Factory resources. +*/ + +import ( + "context" + "os" + + sfv1 "github.com/softwarefactory-project/sf-operator/api/v1" + + v1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/spf13/cobra" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func getOperatorSelector() labels.Selector { + selector := labels.NewSelector() + req, err := labels.NewRequirement( + "operators.coreos.com/sf-operator.operators", + selection.Exists, + []string{}) + if err != nil { + ctrl.Log.Error(err, "could not set label selector to clean subscriptions") + os.Exit(1) + } + return selector.Add(*req) +} + +func cleanSubscription(env *ENV) { + selector := getOperatorSelector() + + subscriptionListOpts := []client.ListOption{ + client.InNamespace("operators"), + client.MatchingLabelsSelector{ + Selector: selector, + }, + } + + subsList := v1alpha1.SubscriptionList{} + if err := env.Cli.List(env.Ctx, &subsList, subscriptionListOpts...); err != nil { + ctrl.Log.Error(err, "error listing subscriptions") + os.Exit(1) + } + if len(subsList.Items) > 0 { + subscriptionDeleteOpts := []client.DeleteAllOfOption{ + client.InNamespace("operators"), + client.MatchingLabelsSelector{ + Selector: selector, + }, + } + sub := v1alpha1.Subscription{} + DeleteAllOfOrDie(env, &sub, subscriptionDeleteOpts...) + } +} + +func cleanCatalogSource(env *ENV) { + cs := v1alpha1.CatalogSource{} + cs.SetName("sf-operator-catalog") + cs.SetNamespace("operators") + if !DeleteOrDie(env, &cs) { + ctrl.Log.Info("CatalogSource object not found") + } +} + +func cleanClusterServiceVersion(env *ENV) { + selector := getOperatorSelector() + + subscriptionListOpts := []client.ListOption{ + client.InNamespace("operators"), + client.MatchingLabelsSelector{ + Selector: selector, + }, + } + + csvsList := v1alpha1.ClusterServiceVersionList{} + if err := env.Cli.List(env.Ctx, &csvsList, subscriptionListOpts...); err != nil { + ctrl.Log.Error(err, "error listing cluster service versions") + os.Exit(1) + } + if len(csvsList.Items) > 0 { + csvDeleteOpts := []client.DeleteAllOfOption{ + client.InNamespace("operators"), + client.MatchingLabelsSelector{ + Selector: selector, + }, + } + csv := v1alpha1.ClusterServiceVersion{} + DeleteAllOfOrDie(env, &csv, csvDeleteOpts...) + } +} + +func cleanSFInstance(env *ENV, ns string) { + var sf sfv1.SoftwareFactory + sfDeleteOpts := []client.DeleteAllOfOption{ + client.InNamespace(ns), + } + DeleteAllOfOrDie(env, &sf, sfDeleteOpts...) + var cm apiv1.ConfigMap + cm.SetName("sf-standalone-owner") + cm.SetNamespace(ns) + if !DeleteOrDie(env, &cm) { + ctrl.Log.Info("standalone mode configmap not found") + } +} + +func cleanPVCs(env *ENV, ns string) { + selector := labels.NewSelector() + appReq, err := labels.NewRequirement( + "app", + selection.In, + []string{"sf"}) + if err != nil { + ctrl.Log.Error(err, "could not set app label requirement to clean PVCs") + os.Exit(1) + } + runReq, err := labels.NewRequirement( + "run", + selection.NotIn, + []string{"gerrit"}) + if err != nil { + ctrl.Log.Error(err, "could not set run label requirement to clean PVCs") + os.Exit(1) + } + selector = selector.Add([]labels.Requirement{*appReq, *runReq}...) + pvcDeleteOpts := []client.DeleteAllOfOption{ + client.InNamespace(ns), + client.MatchingLabelsSelector{ + Selector: selector, + }, + } + var pvc apiv1.PersistentVolumeClaim + DeleteAllOfOrDie(env, &pvc, pvcDeleteOpts...) +} + +func wipeSFCmd(kmd *cobra.Command, args []string) { + cliCtx, err := GetCLIContext(kmd) + if err != nil { + ctrl.Log.Error(err, "Error initializing") + os.Exit(1) + } + ns := cliCtx.Namespace + kubeContext := cliCtx.KubeContext + delPVCs, _ := kmd.Flags().GetBool("rm-data") + delAll, _ := kmd.Flags().GetBool("all") + env := ENV{ + Cli: CreateKubernetesClientOrDie(kubeContext), + Ctx: context.TODO(), + Ns: ns, + } + cleanSFInstance(&env, ns) + if delPVCs || delAll { + cleanPVCs(&env, ns) + } + if delAll { + cleanSubscription(&env) + cleanCatalogSource(&env) + cleanClusterServiceVersion(&env) + } +} + +func MkWipeCmd() *cobra.Command { + + var ( + deleteData bool + deleteAll bool + wipeCmd = &cobra.Command{ + Use: "wipe", + Short: "wipe SF instance and related resources", + Long: `This command can be used to remove all Software Factory instances in the provided namespace, +their persistent volumes, and even remove the SF operator completely.`, + Run: wipeSFCmd, + } + ) + + wipeCmd.Flags().BoolVar(&deleteData, "rm-data", false, "Delete also all persistent volumes after removing the instances. This will result in data loss, like build results and artifacts.") + wipeCmd.Flags().BoolVar(&deleteAll, "all", false, "Remove also the operator completely from the cluster.") + return wipeCmd +} diff --git a/doc/reference/cli/main.md b/doc/reference/cli/main.md index fff4e397..3da35a12 100644 --- a/doc/reference/cli/main.md +++ b/doc/reference/cli/main.md @@ -19,6 +19,7 @@ deployments, beyond what can be defined in a custom resource manifest. 1. [backup](#backup) 1. [configure TLS](#configure-tls) 1. [restore](#restore) + 1. [wipe](#wipe) ## Running the CLI @@ -212,3 +213,22 @@ Flags: #### restore Not implemented yet + +#### wipe + +The `wipe` subcommand can be used to remove all Software Factory instances in the provided namespace, +their persistent volumes, and even remove the SF operator completely. + +The default behavior is to stop and remove all containers related to a Software Factory deployment, and +keep the existing persistent volumes. + +```sh +go run ./main.go [GLOBAL FLAGS] SF wipe [FLAGS] +``` + +Flags: + +| Argument | Type | Description | Optional | Default | +|----------|------|-------|----|----| +| --rm-data | boolean | Also delete all persistent volumes after removing the instances | yes | False | +| --all | boolean | Remove all data like with the `--rm-data` flag, and remove the operator from the cluster | yes | False | \ No newline at end of file diff --git a/playbooks/main.yaml b/playbooks/main.yaml index c0797cc6..d1bd9929 100644 --- a/playbooks/main.yaml +++ b/playbooks/main.yaml @@ -14,7 +14,7 @@ name: "{{ item }}" loop: - build-operator-assets - - clean-installations + - clean-installations-cli - install-operator - name: Apply the minimal SF CR and ensure reconciled happened diff --git a/playbooks/upgrade.yaml b/playbooks/upgrade.yaml index 0e217286..c9d419d6 100644 --- a/playbooks/upgrade.yaml +++ b/playbooks/upgrade.yaml @@ -9,7 +9,7 @@ - role: build-operator-assets vars: build_bundle: false - - clean-installations + - clean-installations-cli - role: install-operator vars: ci_bundle_img: quay.io/software-factory/sf-operator-bundle:latest diff --git a/roles/clean-installations-cli/tasks/main.yaml b/roles/clean-installations-cli/tasks/main.yaml new file mode 100644 index 00000000..b8bcbf02 --- /dev/null +++ b/roles/clean-installations-cli/tasks/main.yaml @@ -0,0 +1,7 @@ +--- +- name: Clean up previous operator installation + when: remote_os_host + ansible.builtin.shell: > + go run ./main.go --namespace sf SF wipe --all + args: + chdir: "{{ zuul.project.src_dir | default(src_dir) }}" diff --git a/tools/wipe-deployment.sh b/tools/wipe-deployment.sh deleted file mode 100755 index faddf797..00000000 --- a/tools/wipe-deployment.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -ansible-playbook playbooks/wipe.yaml \ - -e "hostname=localhost" \ - -e 'remote_os_host=false' \ No newline at end of file