Skip to content

Commit

Permalink
CLOUDGA-16256: Added billing usage api support
Browse files Browse the repository at this point in the history
  • Loading branch information
arishta-yb committed Oct 10, 2023
1 parent 85aeb73 commit 3970d1d
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 1 deletion.
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/yugabyte/ybm-cli/cmd/role"
"github.com/yugabyte/ybm-cli/cmd/signup"
"github.com/yugabyte/ybm-cli/cmd/tools"
"github.com/yugabyte/ybm-cli/cmd/usage"
"github.com/yugabyte/ybm-cli/cmd/user"
"github.com/yugabyte/ybm-cli/cmd/util"
"github.com/yugabyte/ybm-cli/cmd/vpc"
Expand Down Expand Up @@ -123,6 +124,7 @@ func init() {

rootCmd.AddCommand(cluster.ClusterCmd)
rootCmd.AddCommand(backup.BackupCmd)
rootCmd.AddCommand(usage.UsageCmd)
rootCmd.AddCommand(nal.NalCmd)
rootCmd.AddCommand(permission.ResourcePermissionsCmd)
rootCmd.AddCommand(vpc.VPCCmd)
Expand Down
225 changes: 225 additions & 0 deletions cmd/usage/usage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Licensed to Yugabyte, Inc. under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership. Yugabyte
// licenses this file to you 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 usage

import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
ybmAuthClient "github.com/yugabyte/ybm-cli/internal/client"
"github.com/yugabyte/ybm-cli/internal/formatter"
ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal"
)

var UsageCmd = &cobra.Command{
Use: "usage",
Short: "Billing usage for the account in YugabyteDB Managed",
Long: "Billing usage for the account in YugabyteDB Managed",
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}

var getCmd = &cobra.Command{
Use: "get",
Short: "View billing usage data available for the account in YugabyteDB Managed",
Long: "View billing usage data available for the account in YugabyteDB Managed",
Run: func(cmd *cobra.Command, args []string) {
authApi, err := ybmAuthClient.NewAuthApiClient()
if err != nil {
logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err))
}
authApi.GetInfo("", "")
startDate, _ := cmd.Flags().GetString("start")
endDate, _ := cmd.Flags().GetString("end")
clusters, _ := cmd.Flags().GetStringSlice("clusters")
outputFormat, _ := cmd.Flags().GetString("output-format")
outputFile, _ := cmd.Flags().GetString("output-file")
if outputFile == "" {
outputFile = "usage"
}

startDateTime, err := parseAndFormatDate(startDate)
if err != nil {
logrus.Fatalf("Invalid start date format. Use either RFC3339 format (e.g., '2023-09-01T12:30:45.000Z') or 'yyyy-MM-dd' format (e.g., '2023-09-01').\n")
}

endDateTime, err := parseAndFormatDate(endDate)
if err != nil {
logrus.Fatalf("Invalid end date format. Use either RFC3339 format (e.g., '2023-09-01T12:30:45.000Z') or 'yyyy-MM-dd' format (e.g., '2023-09-01').\n")
}

if startDateTime > endDateTime {
logrus.Fatalf("Start date must be before end date.")
}

startDate = startDateTime
endDate = endDateTime

respC, r, err := authApi.ListClustersByDateRange(startDate, endDate).Execute()
if err != nil {
logrus.Debugf("Full HTTP response: %v", r)
logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err))
}
clusterData := respC.Data.GetClusters()
var clusterUUIDMap = make(map[string]string)

for _, cluster := range clusterData {
clusterName := cluster.Name
clusterUUIDMap[clusterName] = cluster.Id
}

var selectedUUIDs []string
if len(clusters) > 0 {
selectedUUIDs = make([]string, 0, len(clusters))
for _, clusterName := range clusters {
if clusterID, ok := clusterUUIDMap[clusterName]; ok {
selectedUUIDs = append(selectedUUIDs, clusterID)
} else {
logrus.Fatalf("Cluster name '%s' not found within the specified time range.\n", clusterName)
}
}
} else {
selectedUUIDs = nil
}

resp, r, err := authApi.GetBillingUsage(startDate, endDate, selectedUUIDs).Execute()

if err != nil {
logrus.Debugf("Full HTTP response: %v", r)
logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err))
}
usageData := resp.GetData()

switch strings.ToLower(outputFormat) {
case "csv":
if err := outputCSV(usageData, outputFile); err != nil {
return
}
case "json":
if err := outputJSON(usageData, outputFile); err != nil {
return
}
default:
logrus.Warnf("Unsupported format: %s. Defaulting to CSV.\n", outputFormat)
if err := outputCSV(usageData, outputFile); err != nil {
return
}
}
},
}

func parseAndFormatDate(dateStr string) (string, error) {
// Try parsing in RFC3339 format
parsedDate, err := time.Parse(time.RFC3339, dateStr)
if err != nil {
// If parsing fails, try parsing in "yyyy-MM-dd" format
parsedDate, err = time.Parse("2006-01-02", dateStr)
if err != nil {
return "", err
}
}

// Always format in the desired "yyyy-MM-ddTHH:mm:ss.SSSZ" format
formattedDate := parsedDate.Format("2006-01-02T15:04:05.000Z")
return formattedDate, nil
}

func outputCSV(resp ybmclient.BillingUsageData, filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()

writer := csv.NewWriter(file)
defer writer.Flush()

dimensionHeaders := []string{"Date"}
for _, dimension := range resp.GetDimensions() {
dimensionHeaders = append(dimensionHeaders, string(dimension.GetName())+"_Daily", string(dimension.GetName())+"_Cumulative")
}

err = writer.Write(dimensionHeaders)
if err != nil {
return err
}

for _, dataPoint := range resp.GetDimensions()[0].DataPoints {
row := []string{dataPoint.GetStartDate()}

for _, dimension := range resp.GetDimensions() {
var found bool
for _, point := range dimension.DataPoints {
if point.StartDate == dataPoint.StartDate {
row = append(row, fmt.Sprintf("%f", *point.DailyUsage))
row = append(row, fmt.Sprintf("%f", *point.CumulativeUsage))
found = true
break
}
}

// If no data found for the current dimension, add placeholders
if !found {
row = append(row, "0.000000", "0.000000")
}
}

err := writer.Write(row)
if err != nil {
return err
}
}

fmt.Printf("CSV data written to %s.csv.\n", formatter.Colorize(filename, formatter.GREEN_COLOR))
return nil
}

func outputJSON(data interface{}, filename string) error {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal data to JSON: %v", err)
}

file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to create or open JSON file: %v", err)
}
defer file.Close()

_, err = file.Write(jsonData)
if err != nil {
return fmt.Errorf("failed to write JSON data to file: %v", err)
}

fmt.Printf("JSON data written to %s.json.\n", formatter.Colorize(filename, formatter.GREEN_COLOR))
return nil
}

func init() {
UsageCmd.AddCommand(getCmd)
getCmd.Flags().String("start", "", "[REQUIRED] Start date in RFC3339 format (e.g., '2023-09-01T12:30:45.000Z') or 'yyyy-MM-dd' format (e.g., '2023-09-01').")
getCmd.Flags().String("end", "", "[REQUIRED] End date in RFC3339 format (e.g., '2023-09-30T23:59:59.999Z') or 'yyyy-MM-dd' format (e.g., '2023-09-30').")
getCmd.Flags().StringSliceP("clusters", "c", []string{}, "[OPTIONAL] List of cluster names separated by comma.")
getCmd.Flags().String("output-format", "csv", "[OPTIONAL] Output format. Possible values: csv, json.")
getCmd.Flags().String("output-file", "usage", "[OPTIONAL] Output filename.")
}
1 change: 1 addition & 0 deletions docs/ybm.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ybm [flags]
* [ybm region](ybm_region.md) - Manage cloud regions
* [ybm role](ybm_role.md) - Manage roles
* [ybm signup](ybm_signup.md) - Open a browser to sign up for YugabyteDB Managed
* [ybm usage](ybm_usage.md) - Billing usage for the account in YugabyteDB Managed
* [ybm user](ybm_user.md) - Manage users
* [ybm vpc](ybm_vpc.md) - Manage VPCs

36 changes: 36 additions & 0 deletions docs/ybm_usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## ybm usage

Billing usage for the account in YugabyteDB Managed

### Synopsis

Billing usage for the account in YugabyteDB Managed

```
ybm usage [flags]
```

### Options

```
-h, --help help for usage
```

### Options inherited from parent commands

```
-a, --apiKey string YBM Api Key
--config string config file (default is $HOME/.ybm-cli.yaml)
--debug Use debug mode, same as --logLevel debug
-l, --logLevel string Select the desired log level format(info). Default to info
--no-color Disable colors in output , default to false
-o, --output string Select the desired output format (table, json, pretty). Default to table
--timeout duration Wait command timeout, example: 5m, 1h. (default 168h0m0s)
--wait Wait until the task is completed, otherwise it will exit immediately, default to false
```

### SEE ALSO

* [ybm](ybm.md) - ybm - Effortlessly manage your DB infrastructure on YugabyteDB Managed (DBaaS) from command line!
* [ybm usage get](ybm_usage_get.md) - View billing usage data available for the account in YugabyteDB Managed

40 changes: 40 additions & 0 deletions docs/ybm_usage_get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## ybm usage get

View billing usage data available for the account in YugabyteDB Managed

### Synopsis

View billing usage data available for the account in YugabyteDB Managed

```
ybm usage get [flags]
```

### Options

```
-c, --clusters strings [OPTIONAL] List of cluster names separated by comma.
--end string [REQUIRED] End date in RFC3339 format (e.g., '2023-09-30T23:59:59.999Z') or 'yyyy-MM-dd' format (e.g., '2023-09-30').
-h, --help help for get
--output-file string [OPTIONAL] Output filename. (default "usage")
--output-format string [OPTIONAL] Output format. Possible values: csv, json. (default "csv")
--start string [REQUIRED] Start date in RFC3339 format (e.g., '2023-09-01T12:30:45.000Z') or 'yyyy-MM-dd' format (e.g., '2023-09-01').
```

### Options inherited from parent commands

```
-a, --apiKey string YBM Api Key
--config string config file (default is $HOME/.ybm-cli.yaml)
--debug Use debug mode, same as --logLevel debug
-l, --logLevel string Select the desired log level format(info). Default to info
--no-color Disable colors in output , default to false
-o, --output string Select the desired output format (table, json, pretty). Default to table
--timeout duration Wait command timeout, example: 5m, 1h. (default 168h0m0s)
--wait Wait until the task is completed, otherwise it will exit immediately, default to false
```

### SEE ALSO

* [ybm usage](ybm_usage.md) - Billing usage for the account in YugabyteDB Managed

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20230915094706-0a165a0d7d08
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231005213122-c1389755cc7f
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/mod v0.12.0
golang.org/x/term v0.12.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1
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-20230915094706-0a165a0d7d08 h1:ohLS4WahD92jv8Th6tS38VGsTD3767dPnNj/rN5RreM=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20230915094706-0a165a0d7d08/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231002173116-4bef65a4f62e h1:pfJyYqKlg3XDhUeD/e3uEasCAVFziKbkTET+SjJS8QI=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231002173116-4bef65a4f62e/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231005213122-c1389755cc7f h1:6JM4ixKyL6NNjs1iib6HIdZmNNTL8e1SAqfGmhADQk4=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20231005213122-c1389755cc7f/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=
Expand Down
8 changes: 8 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,14 @@ func (a *AuthApiClient) ListClusterNetworkAllowLists(clusterId string) ybmclient
return a.ApiClient.ClusterApi.ListClusterNetworkAllowLists(a.ctx, a.AccountID, a.ProjectID, clusterId)
}

func (a *AuthApiClient) GetBillingUsage(startTimestamp string, endTimestamp string, clusterIds []string) ybmclient.ApiGetBillingUsageRequest {
return a.ApiClient.BillingApi.GetBillingUsage(a.ctx, a.AccountID).StartTimestamp(startTimestamp).EndTimestamp(endTimestamp).Granularity(ybmclient.GRANULARITYENUM_DAILY).ClusterIds(clusterIds)
}

func (a *AuthApiClient) ListClustersByDateRange(startTimestamp string, endTimestamp string) ybmclient.ApiListClustersByDateRangeRequest {
return a.ApiClient.BillingApi.ListClustersByDateRange(a.ctx, a.AccountID).StartTimestamp(startTimestamp).EndTimestamp(endTimestamp).Tier(ybmclient.CLUSTERTIER_PAID)
}

func (a *AuthApiClient) ListClusterCMKs(clusterId string) ybmclient.ApiGetClusterCMKRequest {
return a.ApiClient.ClusterApi.GetClusterCMK(a.ctx, a.AccountID, a.ProjectID, clusterId)
}
Expand Down

0 comments on commit 3970d1d

Please sign in to comment.