github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/backuprepo/create_test.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 backuprepo
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"net/http"
    26  	"strings"
    27  
    28  	. "github.com/onsi/ginkgo/v2"
    29  	. "github.com/onsi/gomega"
    30  
    31  	"github.com/spf13/cobra"
    32  	"github.com/spf13/pflag"
    33  	corev1 "k8s.io/api/core/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/cli-runtime/pkg/genericiooptions"
    36  	"k8s.io/cli-runtime/pkg/resource"
    37  	"k8s.io/client-go/dynamic/fake"
    38  	clientfake "k8s.io/client-go/rest/fake"
    39  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    40  
    41  	"github.com/1aal/kubeblocks/pkg/cli/scheme"
    42  	"github.com/1aal/kubeblocks/pkg/cli/testing"
    43  )
    44  
    45  var _ = Describe("backuprepo create command", func() {
    46  	var streams genericiooptions.IOStreams
    47  	var tf *cmdtesting.TestFactory
    48  	var cmd *cobra.Command
    49  	var options *createOptions
    50  
    51  	BeforeEach(func() {
    52  		streams, _, _, _ = genericiooptions.NewTestIOStreams()
    53  		tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace)
    54  		codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
    55  		httpResp := func(obj runtime.Object) *http.Response {
    56  			return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}
    57  		}
    58  
    59  		tf.UnstructuredClient = &clientfake.RESTClient{
    60  			NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
    61  			Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
    62  				urlPrefix := "/api/v1/namespaces/" + testing.Namespace
    63  				if req.URL.Path == urlPrefix+"/secrets" && req.Method == http.MethodPost {
    64  					dec := json.NewDecoder(req.Body)
    65  					secret := &corev1.Secret{}
    66  					_ = dec.Decode(secret)
    67  					if secret.Name == "" && secret.GenerateName != "" {
    68  						secret.Name = secret.GenerateName + "123456"
    69  					}
    70  					return httpResp(secret), nil
    71  				}
    72  				if strings.HasPrefix(req.URL.Path, urlPrefix+"/secrets") && req.Method == http.MethodPatch {
    73  					return httpResp(&corev1.Secret{}), nil
    74  				}
    75  				mapping := map[string]*http.Response{
    76  					"/api/v1/secrets": httpResp(&corev1.SecretList{}),
    77  				}
    78  				return mapping[req.URL.Path], nil
    79  			}),
    80  		}
    81  		tf.Client = tf.UnstructuredClient
    82  
    83  		options = &createOptions{}
    84  		cmd = newCreateCommand(options, tf, streams)
    85  		err := options.init(tf)
    86  		Expect(err).ShouldNot(HaveOccurred())
    87  
    88  		providerObj := testing.FakeStorageProvider("fake-s3", nil)
    89  		repoObj := testing.FakeBackupRepo("test-backuprepo", false)
    90  		tf.FakeDynamicClient = fake.NewSimpleDynamicClient(
    91  			scheme.Scheme, providerObj, repoObj)
    92  		err = options.init(tf)
    93  		Expect(err).ShouldNot(HaveOccurred())
    94  	})
    95  
    96  	AfterEach(func() {
    97  		tf.Cleanup()
    98  	})
    99  
   100  	Describe("parseProviderFlags", func() {
   101  		It("should fail if --provider is not specified", func() {
   102  			err := options.parseProviderFlags(cmd, []string{}, tf)
   103  			Expect(err).Should(MatchError(ContainSubstring("please specify the --provider flag")))
   104  		})
   105  
   106  		It("should fail if the specified provider is not existing", func() {
   107  			err := options.parseProviderFlags(cmd, []string{"--provider", "non-existent"}, tf)
   108  			Expect(err).Should(MatchError(ContainSubstring("storage provider \"non-existent\" is not found")))
   109  		})
   110  
   111  		It("should able to parse flags from the provider", func() {
   112  			err := options.parseProviderFlags(cmd, []string{
   113  				"--provider", "fake-s3",
   114  				"--access-key-id", "abc",
   115  				"--secret-access-key", "def",
   116  			}, tf)
   117  			Expect(err).ShouldNot(HaveOccurred())
   118  		})
   119  
   120  		It("should fail to parse unknown flags", func() {
   121  			err := options.parseProviderFlags(cmd, []string{"--provider", "fake-s3", "--foo", "abc"}, tf)
   122  			Expect(err).Should(MatchError(ContainSubstring("unknown flag: --foo")))
   123  		})
   124  
   125  		It("should fail if required flags are not specified", func() {
   126  			err := options.parseProviderFlags(cmd, []string{"--provider", "fake-s3"}, tf)
   127  			Expect(err).Should(MatchError(ContainSubstring("required flag(s) \"access-key-id\", \"secret-access-key\" not set")))
   128  		})
   129  
   130  		It("should set isDefault field", func() {
   131  			err := options.parseProviderFlags(cmd, []string{
   132  				"--provider", "fake-s3", "--access-key-id", "abc", "--secret-access-key", "def", "--default",
   133  			}, tf)
   134  			Expect(err).ShouldNot(HaveOccurred())
   135  			Expect(options.isDefault).Should(BeTrue())
   136  		})
   137  
   138  		It("should return ErrHelp if --help is specified", func() {
   139  			err := options.parseProviderFlags(cmd, []string{"--provider", "fake-s3", "--help"}, tf)
   140  			Expect(err).Should(MatchError(pflag.ErrHelp))
   141  		})
   142  	})
   143  
   144  	Describe("complete", func() {
   145  		It("should set fields in createOptions", func() {
   146  			err := options.parseProviderFlags(cmd, []string{
   147  				"test-backuprepo",
   148  				"--provider", "fake-s3",
   149  				"--access-key-id", "abc",
   150  				"--secret-access-key", "def",
   151  				"--region", "us-west-2",
   152  				"--bucket", "test-bucket",
   153  			}, tf)
   154  			Expect(err).ShouldNot(HaveOccurred())
   155  			err = options.complete(cmd)
   156  			Expect(err).ShouldNot(HaveOccurred())
   157  			Expect(options.repoName).Should(Equal("test-backuprepo"))
   158  			Expect(options.allValues).Should(Equal(map[string]string{
   159  				"accessKeyId":     "abc",
   160  				"secretAccessKey": "def",
   161  				"region":          "us-west-2",
   162  				"bucket":          "test-bucket",
   163  				"endpoint":        "",
   164  				"mountOptions":    "",
   165  			}))
   166  			Expect(options.config).Should(Equal(map[string]string{
   167  				"region":       "us-west-2",
   168  				"bucket":       "test-bucket",
   169  				"endpoint":     "",
   170  				"mountOptions": "",
   171  			}))
   172  			Expect(options.credential).Should(Equal(map[string]string{
   173  				"accessKeyId":     "abc",
   174  				"secretAccessKey": "def",
   175  			}))
   176  		})
   177  	})
   178  
   179  	Describe("validate", func() {
   180  		BeforeEach(func() {
   181  			err := options.parseProviderFlags(cmd, []string{
   182  				"--provider", "fake-s3", "--access-key-id", "abc", "--secret-access-key", "def",
   183  			}, tf)
   184  			Expect(err).ShouldNot(HaveOccurred())
   185  			err = options.complete(cmd)
   186  			Expect(err).ShouldNot(HaveOccurred())
   187  		})
   188  
   189  		It("should validate parameters by the json schema", func() {
   190  			options.allValues["region"] = "invalid-region"
   191  			err := options.validate(cmd)
   192  			Expect(err).Should(MatchError(fmt.Errorf("invalid flags")))
   193  		})
   194  		It("should validate pv reclaim policy", func() {
   195  			options.pvReclaimPolicy = "whatever"
   196  			err := options.validate(cmd)
   197  			Expect(err).Should(MatchError(ContainSubstring("invalid --pv-reclaim-policy")))
   198  		})
   199  		It("should validate volume capacity", func() {
   200  			options.volumeCapacity = "invalid"
   201  			err := options.validate(cmd)
   202  			Expect(err).Should(MatchError(ContainSubstring("invalid --volume-capacity")))
   203  		})
   204  		It("should validate the backup repo is not existing", func() {
   205  			options.repoName = "test-backuprepo"
   206  			err := options.validate(cmd)
   207  			Expect(err).Should(MatchError(ContainSubstring("BackupRepo \"test-backuprepo\" is already exists")))
   208  		})
   209  		It("should validate if there is a default backup repo", func() {
   210  			By("setting up a default backup repo")
   211  			providerObj := testing.FakeStorageProvider("fake-s3", nil)
   212  			repoObj := testing.FakeBackupRepo("test-backuprepo", true)
   213  			tf.FakeDynamicClient = fake.NewSimpleDynamicClient(
   214  				scheme.Scheme, providerObj, repoObj)
   215  			err := options.init(tf)
   216  			Expect(err).ShouldNot(HaveOccurred())
   217  
   218  			By("validating")
   219  			options.isDefault = true
   220  			err = options.validate(cmd)
   221  			Expect(err).Should(MatchError(ContainSubstring("there is already a default backup repo")))
   222  		})
   223  		Context("validate access method", func() {
   224  			const supported = "supported"
   225  			BeforeEach(func() {
   226  				options.providerObject.Spec.StorageClassTemplate = ""
   227  				options.providerObject.Spec.PersistentVolumeClaimTemplate = ""
   228  				options.providerObject.Spec.DatasafedConfigTemplate = ""
   229  				options.accessMethod = "" // unspecified
   230  			})
   231  			It("should return error if the provider doesn't support any access method", func() {
   232  				Expect(options.supportedAccessMethods()).Should(BeEmpty())
   233  				err := options.validate(cmd)
   234  				Expect(err).Should(MatchError(ContainSubstring("it doesn't support any access method")))
   235  			})
   236  			It("should use the mount method if it's the only supported access method", func() {
   237  				options.providerObject.Spec.StorageClassTemplate = supported
   238  				err := options.validate(cmd)
   239  				Expect(err).ShouldNot(HaveOccurred())
   240  				Expect(options.accessMethod).Should(Equal("Mount"))
   241  			})
   242  			It("should use the tool method if it's the only supported access method", func() {
   243  				options.providerObject.Spec.DatasafedConfigTemplate = supported
   244  				err := options.validate(cmd)
   245  				Expect(err).ShouldNot(HaveOccurred())
   246  				Expect(options.accessMethod).Should(Equal("Tool"))
   247  			})
   248  			It("should return error if the specified access method is not supported", func() {
   249  				options.providerObject.Spec.StorageClassTemplate = supported
   250  				options.accessMethod = "Tool"
   251  				err := options.validate(cmd)
   252  				Expect(err).Should(MatchError(ContainSubstring("doesn't support \"Tool\" access method")))
   253  			})
   254  			It("should prefer using the tool method", func() {
   255  				options.providerObject.Spec.StorageClassTemplate = supported
   256  				options.providerObject.Spec.DatasafedConfigTemplate = supported
   257  				options.accessMethod = ""
   258  				err := options.validate(cmd)
   259  				Expect(err).ShouldNot(HaveOccurred())
   260  				Expect(options.accessMethod).Should(Equal("Tool"))
   261  			})
   262  		})
   263  	})
   264  
   265  	Describe("run", func() {
   266  		It("should success", func() {
   267  			By("preparing the options")
   268  			err := options.parseProviderFlags(cmd, []string{
   269  				"--provider", "fake-s3", "--access-key-id", "abc", "--secret-access-key", "def",
   270  				"--region", "us-west-1", "--bucket", "test-bucket", "--default", "--access-method", "Mount",
   271  			}, tf)
   272  			Expect(err).ShouldNot(HaveOccurred())
   273  			err = options.complete(cmd)
   274  			Expect(err).ShouldNot(HaveOccurred())
   275  			err = options.validate(cmd)
   276  			Expect(err).ShouldNot(HaveOccurred())
   277  
   278  			By("running the command")
   279  			err = options.run()
   280  			Expect(err).ShouldNot(HaveOccurred())
   281  		})
   282  	})
   283  })