diff --git a/cmd/cluster/pitr-config/pitr_config_ops.go b/cmd/cluster/pitr-config/pitr_config_ops.go index b9a22bc..068374d 100644 --- a/cmd/cluster/pitr-config/pitr_config_ops.go +++ b/cmd/cluster/pitr-config/pitr_config_ops.go @@ -17,7 +17,10 @@ package pitrconfig import ( "fmt" + "math" "os" + "strconv" + "strings" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -29,6 +32,7 @@ import ( ) var ClusterName string +var allPitrConfigSpecs []string var listPitrConfigCmd = &cobra.Command{ Use: "list", @@ -116,47 +120,113 @@ var createPitrConfigCmd = &cobra.Command{ logrus.Fatal(err) } - namespaceName, _ := cmd.Flags().GetString("namespace-name") - namespaceType, _ := cmd.Flags().GetString("namespace-type") - validateNamespaceNameType(namespaceName, namespaceType) - retentionPeriod, _ := cmd.Flags().GetInt32("retention-period-in-days") + pitrConfigSpecs, err := ParsePitrConfigSpecs(authApi, allPitrConfigSpecs) + if err != nil { + logrus.Fatalf("Error while parsing PITR Config specs: %s", ybmAuthClient.GetApiErrorDetails(err)) + return + } - pitrConfigSpec, err := authApi.CreatePitrConfigSpec(namespaceName, namespaceType, retentionPeriod) + bulkPitrConfigSpec, err := authApi.CreateBulkPitrConfigSpec(pitrConfigSpecs) if err != nil { logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) } - resp, r, err := authApi.CreatePitrConfig(clusterID).DatabasePitrConfigSpec(*pitrConfigSpec).Execute() + resp, r, err := authApi.CreatePitrConfig(clusterID).BulkCreateDatabasePitrConfigSpec(*bulkPitrConfigSpec).Execute() if err != nil { logrus.Debugf("Full HTTP response: %v", r) logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) } - pitrConfigId := resp.Data.Info.Id + pitrConfigsData := resp.GetData() - msg := fmt.Sprintf("The PITR Configuration for %s namespace %s in cluster %s is being created\n\n", namespaceType, formatter.Colorize(namespaceName, formatter.GREEN_COLOR), formatter.Colorize(ClusterName, formatter.GREEN_COLOR)) + msg := fmt.Sprintf("The requested PITR Configurations are being created\n\n") if viper.GetBool("wait") { - handleTaskCompletion(authApi, clusterID, msg, ybmclient.TASKTYPEENUM_ENABLE_DB_PITR) - fmt.Printf("Successfully created PITR configuration.\n\n") - - getConfigResp, r, err := authApi.GetPitrConfig(clusterID, *pitrConfigId).Execute() - if err != nil { - logrus.Debugf("Full HTTP response: %v", r) - logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + handleTaskCompletion(authApi, clusterID, msg, ybmclient.TASKTYPEENUM_BULK_ENABLE_DB_PITR) + fmt.Printf("Successfully created PITR configurations.\n\n") + createdConfigsData := []ybmclient.DatabasePitrConfigData{} + for _, configData := range pitrConfigsData { + configId := configData.Info.Id + getConfigResp, r, err := authApi.GetPitrConfig(clusterID, *configId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", r) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + createdConfigsData = append(createdConfigsData, getConfigResp.GetData()) } - pitrConfigData := getConfigResp.GetData() + pitrConfigCtx := formatter.Context{ Output: os.Stdout, Format: formatter.NewPitrConfigFormat(viper.GetString("output")), } - formatter.SinglePitrConfigWrite(pitrConfigCtx, pitrConfigData) + formatter.PitrConfigWrite(pitrConfigCtx, createdConfigsData) } else { fmt.Println(msg) } }, } +// Parse array of PITR config spec string to params +func ParsePitrConfigSpecs(authApi *ybmAuthClient.AuthApiClient, configSpecs []string) ([]ybmclient.DatabasePitrConfigSpec, error) { + var err error + pitrConfigSpecs := []ybmclient.DatabasePitrConfigSpec{} + + for _, configSpec := range configSpecs { + var namespaceNameProvided bool + var namespaceTypeProvided bool + var retentionPeriodProvided bool + spec := *ybmclient.NewDatabasePitrConfigSpecWithDefaults() + configSpec := strings.ReplaceAll(configSpec, " ", "") + + for _, subSpec := range strings.Split(configSpec, ",") { + if !strings.Contains(subSpec, "=") { + return nil, fmt.Errorf("namespace-name, namespace-type and retention-period-in-days must be provided as key value pairs for each PITR Config to be created") + } + kvp := strings.Split(subSpec, "=") + key := kvp[0] + val := kvp[1] + n := 0 + err = nil + switch key { + case "namespace-name": + if len(val) == 0 { + return nil, fmt.Errorf("Namespace name must be provided.") + } + spec.SetDatabaseName(val) + namespaceNameProvided = true + case "namespace-type": + if !(val == "YCQL" || val == "YSQL") { + return nil, fmt.Errorf("Only YCQL or YSQL namespace types are allowed.") + } + spec.SetDatabaseType(ybmclient.YbApiEnum(val)) + namespaceTypeProvided = true + case "retention-period-in-days": + n, err = strconv.Atoi(val) + if err != nil { + return nil, err + } + if n > 1 && n <= math.MaxInt32 { + retentionPeriod := int32(n) + spec.SetRetentionPeriod(retentionPeriod) + retentionPeriodProvided = true + } else { + return nil, fmt.Errorf("Minimum retention period is 2 days") + } + } + } + if !(namespaceNameProvided && namespaceTypeProvided && retentionPeriodProvided) { + return nil, fmt.Errorf("namespace-name, namespace-type and retention-period-in-days must be provided for each PITR Config to be created") + } + pitrConfigSpecs = append(pitrConfigSpecs, spec) + } + + if len(pitrConfigSpecs) == 0 { + return nil, fmt.Errorf("namespace-name, namespace-type and retention-period-in-days must be provided for each PITR Config to be created") + } + + return pitrConfigSpecs, nil +} + var restorePitrConfigCmd = &cobra.Command{ Use: "restore", Short: "Restore namespace via PITR Config for a cluster", @@ -309,13 +379,7 @@ func init() { describePitrConfigCmd.MarkFlagRequired("namespace-type") util.AddCommandIfFeatureFlag(PitrConfigCmd, createPitrConfigCmd, util.PITR_CONFIG) - createPitrConfigCmd.Flags().SortFlags = false - createPitrConfigCmd.Flags().String("namespace-name", "", "[REQUIRED] Namespace for which the PITR Config is to be created.") - createPitrConfigCmd.MarkFlagRequired("namespace-name") - createPitrConfigCmd.Flags().String("namespace-type", "", "[REQUIRED] The type of the namespace. Available options are YCQL and YSQL") - createPitrConfigCmd.MarkFlagRequired("namespace-type") - createPitrConfigCmd.Flags().Int32("retention-period-in-days", 2, "[REQUIRED] The time duration in days to retain a snapshot for.") - createPitrConfigCmd.MarkFlagRequired("retention-period-in-days") + createPitrConfigCmd.Flags().StringArrayVarP(&allPitrConfigSpecs, "pitr-config", "p", []string{}, `[REQUIRED] Information for the PITR Configs to be created. All values are mandatory. Available options for namespace type are YCQL and YSQL. Minimum retention period is 2 days. Please provide key value pairs namespace-name=,namespace-type=,retention-period-in-days=.`) util.AddCommandIfFeatureFlag(PitrConfigCmd, restorePitrConfigCmd, util.PITR_RESTORE) restorePitrConfigCmd.Flags().SortFlags = false diff --git a/cmd/pitr_config_test.go b/cmd/pitr_config_test.go index 716b39a..2053bd5 100644 --- a/cmd/pitr_config_test.go +++ b/cmd/pitr_config_test.go @@ -25,7 +25,7 @@ var _ = Describe("PITR Configs Test", func() { responseListCluster openapi.ClusterListResponse responseListPITRConfig openapi.ClusterPitrConfigListResponse responseGetPITRConfig openapi.DatabasePitrConfigResponse - responseCreatePITRConfig openapi.CreateDatabasePitrConfigResponse + responseCreatePITRConfig openapi.BulkCreateDatabasePitrConfigResponse responseRestoreViaPITRConfig openapi.RestoreDatabaseViaPitrResponse ) @@ -97,22 +97,67 @@ test_ysql_db YSQL 5 QUEUED 654321 ghttp.RespondWithJSONEncodedPtr(&statusCode, responseCreatePITRConfig), ), ) - cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--namespace-name", "test_ysql_db", "--namespace-type", "YSQL", "--retention-period-in-days", "5") + cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--pitr-config", "namespace-name=test_ysql_db, namespace-type=YSQL, retention-period-in-days=5", "--pitr-config", "namespace-name=test_ycql_db, namespace-type=YCQL, retention-period-in-days=3") session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait(2) - Expect(session.Out).Should(gbytes.Say(`The PITR Configuration for YSQL namespace test_ysql_db in cluster stunning-sole is being created`)) + Expect(session.Out).Should(gbytes.Say(`The requested PITR Configurations are being created`)) session.Kill() }) It("Should fail if invalid namespace type in PITR Config", func() { - cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--namespace-name", "test_ysql_db", "--namespace-type", "PGSQL", "--retention-period-in-days", "5") + cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--pitr-config", "namespace-name=test_ysql_db, namespace-type=PGSQL, retention-period-in-days=5") session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait(2) Expect(session.Err).Should(gbytes.Say("Only YCQL or YSQL namespace types are allowed.")) session.Kill() }) + + It("Should fail if empty namespace name in PITR Config", func() { + cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--pitr-config", "namespace-name=, namespace-type=YSQL, retention-period-in-days=5", "--pitr-config", "namespace-name=test_ycql_db, namespace-type=YCQL, retention-period-in-days=3") + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait(2) + Expect(session.Err).Should(gbytes.Say("Namespace name must be provided.")) + session.Kill() + }) + + It("Should fail if invalid key value pairs in PITR Config", func() { + cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--pitr-config", "namespace-name test_ysql_db, namespace-type=YSQL, retention-period-in-days=5", "--pitr-config", "namespace-name=test_ycql_db, namespace-type=YCQL, retention-period-in-days=3") + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait(2) + Expect(session.Err).Should(gbytes.Say("namespace-name, namespace-type and retention-period-in-days must be provided as key value pairs for each PITR Config to be created")) + session.Kill() + }) + + It("Should fail if all required params are not in PITR Config", func() { + cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--pitr-config", "namespace-name=test_ysql_db, namespace-type=YSQL ", "--pitr-config", "namespace-name=test_ycql_db, namespace-type=YCQL, retention-period-in-days=3") + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait(2) + Expect(session.Err).Should(gbytes.Say("namespace-name, namespace-type and retention-period-in-days must be provided for each PITR Config to be created")) + session.Kill() + }) + + It("Should fail if non int retention period in PITR Config", func() { + cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--pitr-config", "namespace-name=test_ysql_db, namespace-type=YSQL, retention-period-in-days=five", "--pitr-config", "namespace-name=test_ycql_db, namespace-type=YCQL, retention-period-in-days=3") + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait(2) + Expect(session.Err).Should(gbytes.Say("invalid syntax")) + session.Kill() + }) + + It("Should fail if less than one day retention period in PITR Config", func() { + cmd := exec.Command(compiledCLIPath, "cluster", "pitr-config", "create", "--cluster-name", "stunning-sole", "--pitr-config", "namespace-name=test_ysql_db, namespace-type=YSQL, retention-period-in-days=1", "--pitr-config", "namespace-name=test_ycql_db, namespace-type=YCQL, retention-period-in-days=3") + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait(2) + Expect(session.Err).Should(gbytes.Say("Minimum retention period is 2 days")) + session.Kill() + }) }) var _ = Describe("Restore cluster namespace via PITR config", func() { diff --git a/cmd/test/fixtures/create-cluster-pitr-config.json b/cmd/test/fixtures/create-cluster-pitr-config.json index a2f05b3..253f3bc 100644 --- a/cmd/test/fixtures/create-cluster-pitr-config.json +++ b/cmd/test/fixtures/create-cluster-pitr-config.json @@ -1,19 +1,38 @@ { - "data": { - "spec": { - "database_type": "YSQL", - "database_name": "test_ysql_db", - "retention_period": 5 + "data": [ + { + "spec": { + "database_type": "YSQL", + "database_name": "test_ysql_db", + "retention_period": 5 + }, + "info": { + "id": "3fb11555-b561-4e8f-b0ed-e77b8374cbc3", + "cluster_id": "95c522b3-dfd8-4654-ab10-99f59179ad7c", + "metadata": { + "created_on": "2024-08-07T16:26:08.435Z", + "updated_on": "2024-08-07T16:26:08.435Z" + }, + "backup_interval": 86400, + "state": "QUEUED" + } }, - "info": { - "id": "3fb11555-b561-4e8f-b0ed-e77b8374cbc3", - "cluster_id": "95c522b3-dfd8-4654-ab10-99f59179ad7c", - "metadata": { - "created_on": "2024-08-07T16:26:08.435Z", - "updated_on": "2024-08-07T16:26:08.435Z" + { + "spec": { + "database_type": "YCQL", + "database_name": "test_ycql_db", + "retention_period": 3 }, - "backup_interval": 86400, - "state": "QUEUED" + "info": { + "id": "249f9bf1-4276-4c60-8ab3-2bf1b2f6f1aa", + "cluster_id": "95c522b3-dfd8-4654-ab10-99f59179ad7c", + "metadata": { + "created_on": "2024-08-03T11:38:10.838Z", + "updated_on": "2024-08-03T11:38:10.838Z" + }, + "backup_interval": 86400, + "state": "QUEUED" + } } - } + ] } \ No newline at end of file diff --git a/go.mod b/go.mod index d11f942..f8fd3b9 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 - github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20241129094603-513ccfc1e5ae + github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20241219183048-50fe86c058d8 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/mod v0.20.0 golang.org/x/term v0.25.0 diff --git a/go.sum b/go.sum index 4241133..078a156 100644 --- a/go.sum +++ b/go.sum @@ -282,8 +282,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= -github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20241129094603-513ccfc1e5ae h1:DPZFx2PSJhrCfoZ5vjre2rwvoltlBB75chAlL0Zdapc= -github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20241129094603-513ccfc1e5ae/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20241219183048-50fe86c058d8 h1:criIjOTBfBOy0cZ23Qh2sOI0KLL3P4WKutnUhovUHPA= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20241219183048-50fe86c058d8/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/client/client.go b/internal/client/client.go index 66b110f..2c8ae32 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1549,9 +1549,10 @@ func (a *AuthApiClient) ListClusterPitrConfigs(clusterId string) ybmclient.ApiLi return a.ApiClient.ClusterApi.ListClusterPitrConfigs(a.ctx, a.AccountID, a.ProjectID, clusterId) } -func (a *AuthApiClient) CreatePitrConfigSpec(databaseName string, databaseType string, retentionPeriodInDays int32) (*ybmclient.DatabasePitrConfigSpec, error) { - pitrConfigSpec := ybmclient.NewDatabasePitrConfigSpec(ybmclient.YbApiEnum(databaseType), databaseName, retentionPeriodInDays) - return pitrConfigSpec, nil +func (a *AuthApiClient) CreateBulkPitrConfigSpec(specs []ybmclient.DatabasePitrConfigSpec) (*ybmclient.BulkCreateDatabasePitrConfigSpec, error) { + bulkPitrConfigSpec := ybmclient.NewBulkCreateDatabasePitrConfigSpec() + bulkPitrConfigSpec.SetPitrConfigSpecs(specs) + return bulkPitrConfigSpec, nil } func (a *AuthApiClient) CreatePitrConfig(clusterId string) ybmclient.ApiCreateDatabasePitrConfigRequest {