Skip to content

Commit

Permalink
feat: support "kbcli backuprepo delete"
Browse files Browse the repository at this point in the history
  • Loading branch information
zjx20 committed Dec 6, 2023
1 parent 75e52ab commit 4031216
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions pkg/cmd/backuprepo/backuprepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func NewBackupRepoCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *co
newUpdateCommand(nil, f, streams),
newListCommand(f, streams),
newDescribeCommand(f, streams),
newDeleteCommand(f, streams),
)
return cmd
}
91 changes: 91 additions & 0 deletions pkg/cmd/backuprepo/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright (C) 2022-2023 ApeCloud Co., Ltd
This file is part of KubeBlocks project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package backuprepo

import (
"fmt"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericiooptions"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/templates"

"github.com/apecloud/kbcli/pkg/action"
"github.com/apecloud/kbcli/pkg/types"
"github.com/apecloud/kbcli/pkg/util"
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
)

var (
deleteExample = templates.Examples(`
# Delete a backuprepo
kbcli backuprepo delete my-backuprepo
`)
)

func newDeleteCommand(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
o := action.NewDeleteOptions(f, streams, types.BackupRepoGVR())
o.PreDeleteHook = preDeleteBackupRepo
cmd := &cobra.Command{
Use: "delete",
Short: "Delete a backup repository.",
Example: deleteExample,
ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.BackupRepoGVR()),
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(completeForDeleteBackupRepo(o, args))
cmdutil.CheckErr(o.Run())
},
}
return cmd
}

func preDeleteBackupRepo(o *action.DeleteOptions, obj runtime.Object) error {
unstructured := obj.(*unstructured.Unstructured)
repo := &dpv1alpha1.BackupRepo{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured.Object, repo); err != nil {
return err
}

dynamic, err := o.Factory.DynamicClient()
if err != nil {
return err
}
backupNum, _, err := countBackupNumsAndSize(dynamic, repo)
if err != nil {
return err
}
if backupNum > 0 {
return fmt.Errorf("this backup repository cannot be deleted because it is still containing %d backup(s)", backupNum)
}
return nil
}

func completeForDeleteBackupRepo(o *action.DeleteOptions, args []string) error {
if len(args) == 0 {
return fmt.Errorf("missing backup repository name")
}
if len(args) > 1 {
return fmt.Errorf("can't delete multiple backup repositories at once")
}
o.Names = args
return nil
}
129 changes: 129 additions & 0 deletions pkg/cmd/backuprepo/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright (C) 2022-2023 ApeCloud Co., Ltd
This file is part of KubeBlocks project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package backuprepo

import (
"bytes"
"net/http"

"github.com/apecloud/kbcli/pkg/scheme"
"github.com/apecloud/kbcli/pkg/testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource"
clientfake "k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"

"github.com/apecloud/kbcli/pkg/action"
clitesting "github.com/apecloud/kbcli/pkg/testing"
"github.com/apecloud/kbcli/pkg/types"
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
)

var _ = Describe("backuprepo delete command", func() {
var streams genericiooptions.IOStreams
var in *bytes.Buffer
var tf *cmdtesting.TestFactory

BeforeEach(func() {
streams, in, _, _ = genericiooptions.NewTestIOStreams()
tf = testing.NewTestFactory(testing.Namespace)
})

initClient := func(repoObj runtime.Object, otherObjs ...runtime.Object) {
_ = dpv1alpha1.AddToScheme(scheme.Scheme)
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
httpResp := func(obj runtime.Object) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}
}

tf.UnstructuredClient = &clientfake.RESTClient{
GroupVersion: schema.GroupVersion{Group: types.DPAPIGroup, Version: types.DPAPIVersion},
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
return httpResp(repoObj), nil
}),
}

tf.FakeDynamicClient = clitesting.FakeDynamicClient(append([]runtime.Object{repoObj}, otherObjs...)...)
tf.Client = tf.UnstructuredClient
}

AfterEach(func() {
tf.Cleanup()
})

It("tests completeForDeleteBackupRepo function", func() {
o := action.NewDeleteOptions(tf, streams, types.OpsGVR())

By("case: it should fail if no name is specified")
err := completeForDeleteBackupRepo(o, []string{})
Expect(err).Should(HaveOccurred())

By("case: it should fail if multiple names are specified")
err = completeForDeleteBackupRepo(o, []string{"name1", "name2"})
Expect(err).Should(HaveOccurred())

By("case: it should ok")
err = completeForDeleteBackupRepo(o, []string{"name1"})
Expect(err).ShouldNot(HaveOccurred())
})

It("should refuse to delete if the backup repo is not clear", func() {
const testBackupRepo = "test-backuprepo"
repoObj := testing.FakeBackupRepo(testBackupRepo, false)
backupObj := testing.FakeBackup("test-backup")
backupObj.Labels = map[string]string{
associatedBackupRepoKey: testBackupRepo,
}
initClient(repoObj, backupObj)
// confirm
in.Write([]byte(testBackupRepo + "\n"))

o := action.NewDeleteOptions(tf, streams, types.BackupRepoGVR())
o.PreDeleteHook = preDeleteBackupRepo
err := completeForDeleteBackupRepo(o, []string{testBackupRepo})
Expect(err).ShouldNot(HaveOccurred())

err = o.Run()
Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("this backup repository cannot be deleted because it is still containing"))
})

It("should the backup repo", func() {
const testBackupRepo = "test-backuprepo"
repoObj := testing.FakeBackupRepo(testBackupRepo, false)
repoObj.Status.Phase = dpv1alpha1.BackupRepoFailed
initClient(repoObj)
// confirm
in.Write([]byte(testBackupRepo + "\n"))

o := action.NewDeleteOptions(tf, streams, types.BackupRepoGVR())
o.PreDeleteHook = preDeleteBackupRepo
err := completeForDeleteBackupRepo(o, []string{testBackupRepo})
Expect(err).ShouldNot(HaveOccurred())

err = o.Run()
Expect(err).ShouldNot(HaveOccurred())
})
})
16 changes: 16 additions & 0 deletions pkg/testing/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ func testDynamicResources() []*restmapper.APIGroupResources {
},
},
},
{
Group: metav1.APIGroup{
Name: "dataprotection.kubeblocks.io",
Versions: []metav1.GroupVersionForDiscovery{
{GroupVersion: "dataprotection.kubeblocks.io/v1alpha1", Version: "v1alpha1"},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "dataprotection.kubeblocks.io/v1alpha1",
Version: "v1alpha1"},
},
VersionedResources: map[string][]metav1.APIResource{
"v1alpha1": {
{Name: "backuprepos", Namespaced: false, Kind: "BackupRepo"},
},
},
},
{
Group: metav1.APIGroup{
Name: "chaos-mesh.org",
Expand Down

0 comments on commit 4031216

Please sign in to comment.