From 2e69d59dd2c8edd452ddf753b4fbf8a41d963b76 Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Fri, 23 Feb 2024 09:39:23 -0500 Subject: [PATCH] feat(billing): Add cli subcommand for monthly active user counts --- api/billing/active_users.gen.go | 35 +++ api/billing/custom.go | 63 ++++++ api/billing/option.gen.go | 104 +++++++++ internal/api/genapi/input.go | 37 ++++ internal/api/genapi/templates.go | 5 +- internal/billing/active_users.go | 2 +- internal/billing/repository.go | 3 +- internal/billing/repository_test.go | 12 +- internal/cmd/commands.go | 12 ++ .../cmd/commands/billingcmd/billing.gen.go | 201 ++++++++++++++++++ internal/cmd/commands/billingcmd/funcs.go | 156 ++++++++++++++ internal/cmd/common/help.go | 1 + internal/cmd/gencli/input.go | 12 ++ internal/cmd/gencli/templates.go | 12 +- .../handlers/billing/billing_service.go | 18 +- internal/gen/controller.swagger.json | 22 +- .../api/services/billing_service.pb.go | 73 +++---- .../api/resources/billing/v1/billing.proto | 2 +- .../api/services/v1/billing_service.proto | 1 - internal/tests/cli/boundary/_billing.bash | 18 ++ internal/tests/cli/boundary/billing.bats | 61 ++++++ internal/types/action/action.go | 2 +- internal/types/resource/resource.go | 4 + .../api/resources/billing/billing.pb.go | 8 +- 24 files changed, 791 insertions(+), 73 deletions(-) create mode 100644 api/billing/active_users.gen.go create mode 100644 api/billing/custom.go create mode 100644 api/billing/option.gen.go create mode 100644 internal/cmd/commands/billingcmd/billing.gen.go create mode 100644 internal/cmd/commands/billingcmd/funcs.go create mode 100644 internal/tests/cli/boundary/_billing.bash create mode 100644 internal/tests/cli/boundary/billing.bats diff --git a/api/billing/active_users.gen.go b/api/billing/active_users.gen.go new file mode 100644 index 0000000000..a75fb982ac --- /dev/null +++ b/api/billing/active_users.gen.go @@ -0,0 +1,35 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package billing + +import ( + "time" + + "github.com/hashicorp/boundary/api" +) + +type ActiveUsers struct { + Count uint32 `json:"count"` + StartTime time.Time `json:"start_time,omitempty"` + EndTime time.Time `json:"end_time,omitempty"` +} + +// Client is a client for this collection +type Client struct { + client *api.Client +} + +// Creates a new client for this collection. The submitted API client is cloned; +// modifications to it after generating this client will not have effect. If you +// need to make changes to the underlying API client, use ApiClient() to access +// it. +func NewClient(c *api.Client) *Client { + return &Client{client: c.Clone()} +} + +// ApiClient returns the underlying API client +func (c *Client) ApiClient() *api.Client { + return c.client +} diff --git a/api/billing/custom.go b/api/billing/custom.go new file mode 100644 index 0000000000..51496b0443 --- /dev/null +++ b/api/billing/custom.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package billing + +import ( + "context" + "fmt" + "net/url" + + "github.com/hashicorp/boundary/api" +) + +type MonthlyActiveUsersResult struct { + Items []*ActiveUsers + response *api.Response +} + +func (r MonthlyActiveUsersResult) GetItems() any { + return r.Items +} + +func (r MonthlyActiveUsersResult) GetResponse() *api.Response { + return r.response +} + +func (c *Client) MonthlyActiveUsers(ctx context.Context, opt ...Option) (*MonthlyActiveUsersResult, error) { + opts, apiOpts := getOpts(opt...) + + if c.client == nil { + return nil, fmt.Errorf("nil client") + } + + req, err := c.client.NewRequest(ctx, "GET", "billing:monthly-active-users", nil, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating MonthlyActiveUsers request: %w", err) + } + + if len(opts.queryMap) > 0 { + q := url.Values{} + for k, v := range opts.queryMap { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during MonthlyActiveUsers call: %w", err) + } + + mau := new(MonthlyActiveUsersResult) + mau.Items = []*ActiveUsers{} + apiErr, err := resp.Decode(mau) + if err != nil { + return nil, fmt.Errorf("error decoding MonthlyActiveUsers response: %w", err) + } + if apiErr != nil { + return nil, apiErr + } + mau.response = resp + return mau, nil +} diff --git a/api/billing/option.gen.go b/api/billing/option.gen.go new file mode 100644 index 0000000000..932777f7f6 --- /dev/null +++ b/api/billing/option.gen.go @@ -0,0 +1,104 @@ +// Code generated by "make api"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package billing + +import ( + "fmt" + "strings" + + "github.com/hashicorp/boundary/api" +) + +// Option is a func that sets optional attributes for a call. This does not need +// to be used directly, but instead option arguments are built from the +// functions in this package. WithX options set a value to that given in the +// argument; DefaultX options indicate that the value should be set to its +// default. When an API call is made options are processed in the order they +// appear in the function call, so for a given argument X, a succession of WithX +// or DefaultX calls will result in the last call taking effect. +type Option func(*options) + +type options struct { + postMap map[string]interface{} + queryMap map[string]string + withAutomaticVersioning bool + withSkipCurlOutput bool + withFilter string + withListToken string +} + +func getDefaultOptions() options { + return options{ + postMap: make(map[string]interface{}), + queryMap: make(map[string]string), + } +} + +func getOpts(opt ...Option) (options, []api.Option) { + opts := getDefaultOptions() + for _, o := range opt { + if o != nil { + o(&opts) + } + } + var apiOpts []api.Option + if opts.withSkipCurlOutput { + apiOpts = append(apiOpts, api.WithSkipCurlOutput(true)) + } + if opts.withFilter != "" { + opts.queryMap["filter"] = opts.withFilter + } + if opts.withListToken != "" { + opts.queryMap["list_token"] = opts.withListToken + } + return opts, apiOpts +} + +// If set, and if the version is zero during an update, the API will perform a +// fetch to get the current version of the resource and populate it during the +// update call. This is convenient but opens up the possibility for subtle +// order-of-modification issues, so use carefully. +func WithAutomaticVersioning(enable bool) Option { + return func(o *options) { + o.withAutomaticVersioning = enable + } +} + +// WithSkipCurlOutput tells the API to not use the current call for cURL output. +// Useful for when we need to look up versions. +func WithSkipCurlOutput(skip bool) Option { + return func(o *options) { + o.withSkipCurlOutput = true + } +} + +// WithListToken tells the API to use the provided list token +// for listing operations on this resource. +func WithListToken(listToken string) Option { + return func(o *options) { + o.withListToken = listToken + } +} + +// WithFilter tells the API to filter the items returned using the provided +// filter term. The filter should be in a format supported by +// hashicorp/go-bexpr. +func WithFilter(filter string) Option { + return func(o *options) { + o.withFilter = strings.TrimSpace(filter) + } +} + +func WithEndTime(inEndTime string) Option { + return func(o *options) { + o.queryMap["end_time"] = fmt.Sprintf("%v", inEndTime) + } +} + +func WithStartTime(inStartTime string) Option { + return func(o *options) { + o.queryMap["start_time"] = fmt.Sprintf("%v", inStartTime) + } +} diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index fc8e8f3632..3aa2d7dea8 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/accounts" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentiallibraries" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentials" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentialstores" @@ -65,6 +66,7 @@ type fieldInfo struct { Query bool SkipDefault bool JsonTags []string // Appended to a field's `json` tag (comma separated) + AllowEmpty bool } type structInfo struct { @@ -138,6 +140,8 @@ type structInfo struct { // fieldFilter is a set of field names that will not result in generated API // fields fieldFilter []string + + allowEmpty bool } var inputStructs = []*structInfo{ @@ -224,6 +228,39 @@ var inputStructs = []*structInfo{ createResponseTypes: []string{CreateResponseType, ReadResponseType, UpdateResponseType, DeleteResponseType, ListResponseType}, recursiveListing: true, }, + { + inProto: &billing.ActiveUsers{}, + outFile: "billing/active_users.gen.go", + templates: []*template.Template{ + clientTemplate, + }, + fieldOverrides: []fieldInfo{ + { + Name: "Count", + ProtoName: "count", + FieldType: "uint32", + AllowEmpty: true, + }, + }, + extraFields: []fieldInfo{ + { + Name: "StartTime", + ProtoName: "start_time", + FieldType: "string", + SkipDefault: true, + Query: true, + }, + { + Name: "EndTime", + ProtoName: "end_time", + FieldType: "string", + SkipDefault: true, + Query: true, + }, + }, + pluralResourceName: "billing", + versionEnabled: true, + }, // User related resources { inProto: &users.Account{}, diff --git a/internal/api/genapi/templates.go b/internal/api/genapi/templates.go index 703de83ff8..e62a435534 100644 --- a/internal/api/genapi/templates.go +++ b/internal/api/genapi/templates.go @@ -94,6 +94,9 @@ func fillTemplates() { if len(override.JsonTags) != 0 { field.JsonTags = override.JsonTags } + if override.AllowEmpty { + field.AllowEmpty = true + } in.generatedStructure.fields[i] = field } } @@ -715,7 +718,7 @@ import ( ) type {{ .Name }} struct { {{ range .Fields }} -{{ .Name }} {{ .FieldType }} `, "`json:\"{{ .ProtoName }}{{ if ( ne ( len ( .JsonTags ) ) 0 ) }},{{ stringsjoin .JsonTags \",\" }}{{ end }},omitempty\"`", `{{ end }} +{{ .Name }} {{ .FieldType }} `, "`json:\"{{ .ProtoName }}{{ if ( ne ( len ( .JsonTags ) ) 0 ) }},{{ stringsjoin .JsonTags \",\" }}{{ end }}{{ if ( not .AllowEmpty ) }},omitempty{{ end }}\"`", `{{ end }} {{ if ( not ( eq ( len ( .CreateResponseTypes ) ) 0 ) )}} response *api.Response {{ else if ( eq .Name "Error" ) }} diff --git a/internal/billing/active_users.go b/internal/billing/active_users.go index 6549726e47..dec4c826ef 100644 --- a/internal/billing/active_users.go +++ b/internal/billing/active_users.go @@ -11,5 +11,5 @@ import "time" type ActiveUsers struct { StartTime time.Time EndTime time.Time - ActiveUsersCount uint64 + ActiveUsersCount uint32 } diff --git a/internal/billing/repository.go b/internal/billing/repository.go index 80385dc556..c77dfea265 100644 --- a/internal/billing/repository.go +++ b/internal/billing/repository.go @@ -89,7 +89,7 @@ func (r *Repository) MonthlyActiveUsers(ctx context.Context, opt ...Option) ([]A for rows.Next() { var startTime time.Time var endTime time.Time - var count uint64 + var count uint32 if err := rows.Scan(&startTime, &endTime, &count); err != nil { return nil, err } @@ -101,7 +101,6 @@ func (r *Repository) MonthlyActiveUsers(ctx context.Context, opt ...Option) ([]A EndTime: endTime.UTC(), } activeUsers = append(activeUsers, auUTC) - } return activeUsers, nil diff --git a/internal/billing/repository_test.go b/internal/billing/repository_test.go index d6a0e17586..a9519543cb 100644 --- a/internal/billing/repository_test.go +++ b/internal/billing/repository_test.go @@ -84,8 +84,8 @@ func TestRepository_MonthlyActiveUsers(t *testing.T) { assert.NoError(t, err) require.Len(t, activeUsers, 2) // check counts for the last two months - require.Equal(t, uint64(0), activeUsers[0].ActiveUsersCount) - require.Equal(t, uint64(6), activeUsers[1].ActiveUsersCount) + require.Equal(t, uint32(0), activeUsers[0].ActiveUsersCount) + require.Equal(t, uint32(6), activeUsers[1].ActiveUsersCount) // assert start and end times are correct // the current month (contains the hour) assert.Equal(t, time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC), activeUsers[0].StartTime) @@ -103,7 +103,7 @@ func TestRepository_MonthlyActiveUsers(t *testing.T) { for i := 0; i < 4; i++ { // check counts for the last four months if i == 0 { - assert.Equal(t, uint64(0), activeUsers[i].ActiveUsersCount) + assert.Equal(t, uint32(0), activeUsers[i].ActiveUsersCount) // the current month (contains the hour) assert.Equal(t, time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC), activeUsers[i].StartTime) assert.Equal(t, time.Date(today.Year(), today.Month(), today.Day(), today.Hour(), 0, 0, 0, time.UTC), activeUsers[i].EndTime) @@ -111,7 +111,7 @@ func TestRepository_MonthlyActiveUsers(t *testing.T) { // create a sliding window of dates to assert start and end times are correct expectedStartTime := time.Date(today.AddDate(0, -i, 0).Year(), today.AddDate(0, -i, 0).Month(), 1, 0, 0, 0, 0, time.UTC) expectedEndTime := time.Date(today.AddDate(0, -i+1, 0).Year(), today.AddDate(0, -i+1, 0).Month(), 1, 0, 0, 0, 0, time.UTC) - assert.Equal(t, uint64(6), activeUsers[i].ActiveUsersCount) + assert.Equal(t, uint32(6), activeUsers[i].ActiveUsersCount) assert.Equal(t, expectedStartTime, activeUsers[i].StartTime) assert.Equal(t, expectedEndTime, activeUsers[i].EndTime) } @@ -125,13 +125,13 @@ func TestRepository_MonthlyActiveUsers(t *testing.T) { require.Len(t, activeUsers, 2) expectedStartTime := time.Date(today.AddDate(0, -2, 0).Year(), today.AddDate(0, -2, 0).Month(), 1, 0, 0, 0, 0, time.UTC) expectedEndTime := time.Date(today.AddDate(0, -1, 0).Year(), today.AddDate(0, -1, 0).Month(), 1, 0, 0, 0, 0, time.UTC) - require.Equal(t, uint64(6), activeUsers[0].ActiveUsersCount) + require.Equal(t, uint32(6), activeUsers[0].ActiveUsersCount) assert.Equal(t, expectedStartTime, activeUsers[0].StartTime) assert.Equal(t, expectedEndTime, activeUsers[0].EndTime) expectedStartTime = time.Date(today.AddDate(0, -3, 0).Year(), today.AddDate(0, -3, 0).Month(), 1, 0, 0, 0, 0, time.UTC) expectedEndTime = time.Date(today.AddDate(0, -2, 0).Year(), today.AddDate(0, -2, 0).Month(), 1, 0, 0, 0, 0, time.UTC) - require.Equal(t, uint64(6), activeUsers[1].ActiveUsersCount) + require.Equal(t, uint32(6), activeUsers[1].ActiveUsersCount) assert.Equal(t, expectedStartTime, activeUsers[1].StartTime) assert.Equal(t, expectedEndTime, activeUsers[1].EndTime) }) diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 734b3a24b1..04c7c9b935 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/commands/authenticate" "github.com/hashicorp/boundary/internal/cmd/commands/authmethodscmd" "github.com/hashicorp/boundary/internal/cmd/commands/authtokenscmd" + "github.com/hashicorp/boundary/internal/cmd/commands/billingcmd" "github.com/hashicorp/boundary/internal/cmd/commands/config" "github.com/hashicorp/boundary/internal/cmd/commands/connect" "github.com/hashicorp/boundary/internal/cmd/commands/credentiallibrariescmd" @@ -243,6 +244,17 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "list", }), + "billing": func() (cli.Command, error) { + return &billingcmd.Command{ + Command: base.NewCommand(ui, opts...), + }, nil + }, + "billing monthly-active-users": clientCacheWrapper( + &billingcmd.Command{ + Command: base.NewCommand(ui), + Func: "monthly-active-users", + }), + "config": func() (cli.Command, error) { return &config.Command{ Command: base.NewCommand(ui, opts...), diff --git a/internal/cmd/commands/billingcmd/billing.gen.go b/internal/cmd/commands/billingcmd/billing.gen.go new file mode 100644 index 0000000000..075189d415 --- /dev/null +++ b/internal/cmd/commands/billingcmd/billing.gen.go @@ -0,0 +1,201 @@ +// Code generated by "make cli"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package billingcmd + +import ( + "fmt" + "sync" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/billing" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initFlags() { + flagsOnce.Do(func() { + extraFlags := extraActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsMap[k] = append(flagsMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*Command)(nil) + _ cli.CommandAutocomplete = (*Command)(nil) +) + +type Command struct { + *base.Command + + Func string + + plural string + + extraCmdVars +} + +func (c *Command) AutocompleteArgs() complete.Predictor { + initFlags() + return complete.PredictAnything +} + +func (c *Command) AutocompleteFlags() complete.Flags { + initFlags() + return c.Flags().Completions() +} + +func (c *Command) Synopsis() string { + if extra := extraSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "billing" + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *Command) Help() string { + initFlags() + + var helpStr string + helpMap := common.HelpMap("billing") + + switch c.Func { + + default: + + helpStr = c.extraHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsMap = map[string][]string{} + +func (c *Command) Flags() *base.FlagSets { + if len(flagsMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "billing", flagsMap, c.Func) + + extraFlagsFunc(c, set, f) + + return set +} + +func (c *Command) Run(args []string) int { + initFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + case "create": + return cli.RunResultHelp + + case "update": + return cli.RunResultHelp + + } + + c.plural = "billing" + switch c.Func { + case "list": + c.plural = "billing" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + var opts []billing.Option + + if strutil.StrListContains(flagsMap[c.Func], "-id") { + switch c.Func { + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + billingClient := billing.NewClient(client) + + if c.FlagFilter != "" { + opts = append(opts, billing.WithFilter(c.FlagFilter)) + } + + var version uint32 + + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + + resp, err = executeExtraActions(c, resp, err, billingClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + return base.CommandSuccess +} + +func (c *Command) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + flagsOnce = new(sync.Once) + + extraActionsFlagsMapFunc = func() map[string][]string { return nil } + extraSynopsisFunc = func(*Command) string { return "" } + extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]billing.Option) bool { return true } + executeExtraActions = func(_ *Command, inResp *api.Response, inErr error, _ *billing.Client, _ uint32, _ []billing.Option) (*api.Response, error) { + return inResp, inErr + } + printCustomActionOutput = func(*Command) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/billingcmd/funcs.go b/internal/cmd/commands/billingcmd/funcs.go new file mode 100644 index 0000000000..23df91c6a9 --- /dev/null +++ b/internal/cmd/commands/billingcmd/funcs.go @@ -0,0 +1,156 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package billingcmd + +import ( + "fmt" + "strings" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/billing" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/mitchellh/go-wordwrap" +) + +func init() { + extraActionsFlagsMapFunc = extraActionsFlagsMapFuncImpl + extraSynopsisFunc = extraSynopsisFuncImpl + extraFlagsFunc = extraFlagsFuncImpl + extraFlagsHandlingFunc = extraFlagsHandlingFuncImpl + executeExtraActions = executeExtraActionsImpl + printCustomActionOutput = printCustomActionOutputImpl +} + +type extraCmdVars struct { + flagStartTime string + flagEndTime string + monthlyActiveUsers *billing.MonthlyActiveUsersResult +} + +func extraActionsFlagsMapFuncImpl() map[string][]string { + return map[string][]string{ + "monthly-active-users": {"start-time", "end-time"}, + } +} + +func extraSynopsisFuncImpl(c *Command) string { + switch c.Func { + case "monthly-active-users": + var in string + switch { + case strings.HasPrefix(c.Func, "start-time"): + in = "Get monthly active users, starting from this time (YYYY-MM format)." + case strings.HasPrefix(c.Func, "end-time"): + in = "Get monthly active users, ending at this time (YYYY-MM format)." + } + return wordwrap.WrapString(in, base.TermWidth) + + default: + return "" + } +} + +func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { + flagsMap[c.Func] = append(flagsMap[c.Func], "start-time", "end-time") + f.StringVar(&base.StringVar{ + Name: "start-time", + Target: &c.flagStartTime, + Usage: "Get monthly active users, starting from this time (YYYY-MM format).", + }) + f.StringVar(&base.StringVar{ + Name: "end-time", + Target: &c.flagEndTime, + Usage: "Get monthly active users, ending at this time (YYYY-MM format).", + }) +} + +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]billing.Option) bool { + switch c.Func { + case "monthly-active-users": + if len(c.flagStartTime) != 0 { + *opts = append(*opts, billing.WithStartTime(c.flagStartTime)) + } + if len(c.flagEndTime) != 0 { + *opts = append(*opts, billing.WithEndTime(c.flagEndTime)) + } + } + + return true +} + +func executeExtraActionsImpl(c *Command, origResp *api.Response, origError error, billingClient *billing.Client, _ uint32, opts []billing.Option) (*api.Response, error) { + switch c.Func { + case "monthly-active-users": + var err error + c.monthlyActiveUsers, err = billingClient.MonthlyActiveUsers(c.Context, opts...) + if err != nil { + return nil, err + } + } + return origResp, origError +} + +func printCustomActionOutputImpl(c *Command) (bool, error) { + switch c.Func { + case "monthly-active-users": + switch base.Format(c.UI) { + case "table": + items := c.monthlyActiveUsers.GetItems().([]*billing.ActiveUsers) + var ret []string + + ret = append(ret, "Billing information:") + ret = append(ret, "") + for i := range items { + ret = append(ret, + fmt.Sprintf(" Count: %d", items[i].Count), + fmt.Sprintf(" Start Time: %s", items[i].StartTime), + fmt.Sprintf(" End Time: %s", items[i].EndTime), + "", + ) + } + + c.UI.Output(base.WrapForHelpText(ret)) + return true, nil + + case "json": + if ok := c.PrintJsonItem(c.monthlyActiveUsers.GetResponse()); !ok { + return false, fmt.Errorf("error formatting as JSON") + } + return true, nil + } + } + + return false, nil +} + +func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary billing [sub command] [options] [args]", + "", + " This command allows for collecting Boundary billing reports. Example:", + "", + " Monthly active users:", + "", + ` $ boundary billing monthly-active-users`, + "", + " Please see the billing subcommand help for detailed usage information.", + }) + case "monthly-active-users": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary billing monthly-active-users [options]", + "", + " This command allows for collecting active Boundary user reports, by month. Example:", + "", + " Monthly active users between September 2023 and February 2024:", + "", + ` $ boundary billing monthly-active-users -start-time="2023-09" -end-time="2024-02"`, + "", + " Please see the billing subcommand help for detailed usage information.", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/common/help.go b/internal/cmd/common/help.go index 57fec95794..33e5b33f0e 100644 --- a/internal/cmd/common/help.go +++ b/internal/cmd/common/help.go @@ -34,6 +34,7 @@ func HelpMap(resType string) map[string]func() string { resource.AuthToken.String(): "at", resource.AuthMethod.String(): "am", resource.Account.String(): "a", + resource.Billing.String(): "b", resource.Role.String(): "r", resource.Group.String(): "g", resource.User.String(): "u", diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 925aea3ca0..2c9f5913c3 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -17,6 +17,9 @@ type cmdInfo struct { // Standard actions (with standard parameters) used by this resource StdActions []string + // HasCustomList indicates if there is a custom list action + HasCustomList bool + // HasExtraCommandVars controls whether to generate an embedded struct with // extra command variables HasExtraCommandVars bool @@ -211,6 +214,15 @@ var inputStructs = map[string][]*cmdInfo{ Container: "Scope", }, }, + "billing": { + { + ResourceType: resource.Billing.String(), + Pkg: "billing", + HasCustomList: true, + HasExtraCommandVars: true, + HasExtraHelpFunc: true, + }, + }, "credentialstores": { { ResourceType: resource.CredentialStore.String(), diff --git a/internal/cmd/gencli/templates.go b/internal/cmd/gencli/templates.go index 1cecc6d563..2880dc1c2e 100644 --- a/internal/cmd/gencli/templates.go +++ b/internal/cmd/gencli/templates.go @@ -478,16 +478,19 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { } var resp *api.Response + {{ if $input.StdActions -}} var item *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }} {{ if hasAction .StdActions "list" }} var items []*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }} {{ end }} + {{ end }} {{ range $i, $action := $input.StdActions }} {{ if ( not ( hasAction $input.SkipClientCallActions $action) ) }} var {{ $action }}Result *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}{{ camelCase $action }}Result {{ end }} {{ end }} + {{ if $input.StdActions }} switch c.Func { {{ range $i, $action := $input.StdActions }} {{ if eq $action "create" }} @@ -538,8 +541,9 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { {{ end }} {{ end }} } + {{ end }} - resp, item, {{ if hasAction .StdActions "list" }}items, {{ end }}err = executeExtra{{ camelCase .SubActionPrefix }}Actions(c, resp, item, {{ if hasAction .StdActions "list" }}items, {{ end }}err, {{ .Pkg }}Client, version, opts) + resp, {{ if $input.StdActions }}item, {{ if hasAction .StdActions "list" }}items, {{ end }}{{ end }}err = executeExtra{{ camelCase .SubActionPrefix }}Actions(c, resp, {{ if $input.StdActions }}item, {{ if hasAction .StdActions "list" }}items, {{ end }}{{ end }}err, {{ .Pkg }}Client, version, opts) if exitCode := c.checkFuncError(err); exitCode > 0 { return exitCode } @@ -553,6 +557,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { return base.CommandSuccess } + {{ if $input.StdActions }} switch c.Func { {{ range $i, $action := .StdActions }} {{ if eq $action "delete" }} @@ -595,6 +600,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { return base.CommandCliError } } + {{ end }} return base.CommandSuccess } @@ -619,8 +625,8 @@ var ( extra{{ camelCase .SubActionPrefix }}SynopsisFunc = func(*{{ camelCase .SubActionPrefix }}Command) string { return "" } extra{{ camelCase .SubActionPrefix }}FlagsFunc = func(*{{ camelCase .SubActionPrefix }}Command, *base.FlagSets, *base.FlagSet) {} extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc = func(*{{ camelCase .SubActionPrefix }}Command, *base.FlagSets, *[]{{ .Pkg }}.Option) bool { return true } - executeExtra{{ camelCase .SubActionPrefix }}Actions = func(_ *{{ camelCase .SubActionPrefix }}Command, inResp *api.Response, inItem *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}inItems []*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}inErr error, _ *{{ .Pkg }}.Client, _ uint32, _ []{{ .Pkg }}.Option) (*api.Response, *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}[]*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}error) { - return inResp, inItem, {{ if hasAction .StdActions "list" }}inItems, {{ end }}inErr + executeExtra{{ camelCase .SubActionPrefix }}Actions = func(_ *{{ camelCase .SubActionPrefix }}Command, inResp *api.Response, {{ if $input.StdActions }}inItem *{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}inItems []*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}{{ end }}inErr error, _ *{{ .Pkg }}.Client, _ uint32, _ []{{ .Pkg }}.Option) (*api.Response, {{ if $input.StdActions }}*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ if hasAction .StdActions "list" }}[]*{{ $input.Pkg }}.{{ camelCase $input.ResourceType }}, {{ end }}{{ end }}error) { + return inResp, {{ if $input.StdActions }}inItem, {{ if hasAction .StdActions "list" }}inItems, {{ end }}{{ end }}inErr } printCustom{{ camelCase .SubActionPrefix }}ActionOutput = func(*{{ camelCase .SubActionPrefix }}Command) (bool, error) { return false, nil } ) diff --git a/internal/daemon/controller/handlers/billing/billing_service.go b/internal/daemon/controller/handlers/billing/billing_service.go index 8173be129c..303d49cbcc 100644 --- a/internal/daemon/controller/handlers/billing/billing_service.go +++ b/internal/daemon/controller/handlers/billing/billing_service.go @@ -10,11 +10,14 @@ import ( "github.com/hashicorp/boundary/internal/billing" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/common" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" + "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing" + "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -75,17 +78,20 @@ func (s Service) MonthlyActiveUsers(ctx context.Context, req *pbs.MonthlyActiveU if req.GetStartTime() != "" { st, err := time.Parse("2006-01", req.GetStartTime()) if err != nil { - return nil, errors.New(ctx, errors.InvalidTimeStamp, op, "start time is in an invalid format") + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "start time is in an invalid format") } startTime = &st } if req.GetEndTime() != "" { et, err := time.Parse("2006-01", req.GetEndTime()) if err != nil { - return nil, errors.New(ctx, errors.InvalidTimeStamp, op, "end time is in an invalid format") + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "end time is in an invalid format") } endTime = &et } + if startTime != nil && endTime != nil && !endTime.After(*startTime) { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "start time is not before end time") + } months, err := repo.MonthlyActiveUsers( ctx, @@ -93,7 +99,7 @@ func (s Service) MonthlyActiveUsers(ctx context.Context, req *pbs.MonthlyActiveU billing.WithEndTime(endTime), ) if err != nil { - return nil, errors.Wrap(ctx, err, op) + return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error()) } var activeUsers []*pb.ActiveUsers @@ -110,6 +116,10 @@ func (s Service) MonthlyActiveUsers(ctx context.Context, req *pbs.MonthlyActiveU } func (s Service) authResult(ctx context.Context, a action.Type) auth.VerifyResults { - opts := []auth.Option{auth.WithType(resource.Billing), auth.WithAction(a)} + opts := []auth.Option{ + auth.WithType(resource.Billing), + auth.WithAction(a), + auth.WithScopeId(scope.Global.String()), + } return auth.Verify(ctx, opts...) } diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 7a1757773d..e1dc4cc495 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -4794,8 +4794,8 @@ "type": "object", "properties": { "count": { - "type": "string", - "format": "uint64", + "type": "integer", + "format": "int64", "description": "Output only. The number of active users between the start time and end time.", "readOnly": true }, @@ -8835,15 +8835,6 @@ } } }, - "controller.api.services.v1.ReApplyStoragePolicyResponse": { - "type": "object", - "properties": { - "item": { - "$ref": "#/definitions/controller.api.resources.sessionrecordings.v1.SessionRecording", - "description": "The requested recording." - } - } - }, "controller.api.services.v1.MonthlyActiveUsersResponse": { "type": "object", "properties": { @@ -8856,6 +8847,15 @@ } } }, + "controller.api.services.v1.ReApplyStoragePolicyResponse": { + "type": "object", + "properties": { + "item": { + "$ref": "#/definitions/controller.api.resources.sessionrecordings.v1.SessionRecording", + "description": "The requested recording." + } + } + }, "controller.api.services.v1.ReadCertificateAuthorityResponse": { "type": "object", "properties": { diff --git a/internal/gen/controller/api/services/billing_service.pb.go b/internal/gen/controller/api/services/billing_service.pb.go index 90674badc6..c6aa918911 100644 --- a/internal/gen/controller/api/services/billing_service.pb.go +++ b/internal/gen/controller/api/services/billing_service.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: controller/api/services/v1/billing_service.proto @@ -15,7 +15,6 @@ import ( _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - _ "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -144,43 +143,41 @@ var file_controller_api_services_v1_billing_service_proto_rawDesc = []byte{ 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, - 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, - 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, - 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x57, 0x0a, 0x19, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, - 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, - 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, - 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x64, 0x0a, 0x1a, 0x4d, 0x6f, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, + 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x57, 0x0a, 0x19, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x64, 0x0a, 0x1a, 0x4d, 0x6f, 0x6e, 0x74, + 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x2e, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x32, 0xe2, + 0x01, 0x0a, 0x0e, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0xcf, 0x01, 0x0a, 0x12, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x2e, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x74, - 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x32, - 0xe2, 0x01, 0x0a, 0x0e, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0xcf, 0x01, 0x0a, 0x12, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, - 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, - 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, - 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4a, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x52, - 0x65, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x20, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x20, 0x61, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x22, 0x12, 0x20, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x3a, - 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x2d, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x2d, 0x75, - 0x73, 0x65, 0x72, 0x73, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4a, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x52, 0x65, + 0x74, 0x75, 0x72, 0x6e, 0x73, 0x20, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x20, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x20, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x22, 0x12, 0x20, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x3a, 0x6d, + 0x6f, 0x6e, 0x74, 0x68, 0x6c, 0x79, 0x2d, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x2d, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, + 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/proto/controller/api/resources/billing/v1/billing.proto b/internal/proto/controller/api/resources/billing/v1/billing.proto index 02648307eb..e9e391230b 100644 --- a/internal/proto/controller/api/resources/billing/v1/billing.proto +++ b/internal/proto/controller/api/resources/billing/v1/billing.proto @@ -11,7 +11,7 @@ option go_package = "github.com/hashicorp/boundary/sdk/pbs/controller/api/resour message ActiveUsers { // Output only. The number of active users between the start time and end time. - uint64 count = 1; // @gotags: `class:"public"` + uint32 count = 1; // @gotags: `class:"public"` // Output only. The start time of the active users count, inclusive. google.protobuf.Timestamp start_time = 2 [json_name = "start_time"]; // @gotags: class:"public" diff --git a/internal/proto/controller/api/services/v1/billing_service.proto b/internal/proto/controller/api/services/v1/billing_service.proto index 49e309f9a5..06607d484a 100644 --- a/internal/proto/controller/api/services/v1/billing_service.proto +++ b/internal/proto/controller/api/services/v1/billing_service.proto @@ -7,7 +7,6 @@ package controller.api.services.v1; import "controller/api/resources/billing/v1/billing.proto"; import "google/api/annotations.proto"; -import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; option go_package = "github.com/hashicorp/boundary/internal/gen/controller/api/services;services"; diff --git a/internal/tests/cli/boundary/_billing.bash b/internal/tests/cli/boundary/_billing.bash new file mode 100644 index 0000000000..34efca2049 --- /dev/null +++ b/internal/tests/cli/boundary/_billing.bash @@ -0,0 +1,18 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +function active_users_last_two_months() { + boundary billing monthly-active-users -format json +} + +function active_users_start_time() { + boundary billing monthly-active-users -start-time=$1 -format json +} + +function active_users_start_time_and_end_time() { + boundary billing monthly-active-users -start-time=$1 -end-time=$2 -format json +} + +function active_users_end_time() { + boundary billing monthly-active-users -end-time=$1 -format json +} \ No newline at end of file diff --git a/internal/tests/cli/boundary/billing.bats b/internal/tests/cli/boundary/billing.bats new file mode 100644 index 0000000000..f78e6f6cf8 --- /dev/null +++ b/internal/tests/cli/boundary/billing.bats @@ -0,0 +1,61 @@ +#!/usr/bin/env bats + +load _auth +load _billing +load _helpers + +@test "boundary/billing: can login as admin user" { + run login $DEFAULT_LOGIN + [ "$status" -eq 0 ] +} + +@test "boundary/billing: admin user can get last two months" { + run active_users_last_two_months + [ "$status" -eq 0 ] + run has_status_code "$output" "200" +} + +@test "boundary/billing: admin user can get report with start time" { + run active_users_start_time "2023-09" + [ "$status" -eq 0 ] + run has_status_code "$output" "200" +} + +@test "boundary/billing: admin user can get report with start and end times" { + run active_users_start_time_and_end_time "2023-09" "2023-12" + [ "$status" -eq 0 ] + run has_status_code "$output" "200" +} + +@test "boundary/billing: cannot get report with end time before start time" { + run active_users_start_time_and_end_time "2023-09" "2023-08" + [ "$status" -eq 1 ] +} + +@test "boundary/billing: cannot get report with only end time" { + run active_users_end_time "2023-09" + [ "$status" -eq 1 ] +} + +# unpriv tests +@test "boundary/billing: can login as unpriv user" { + run login $DEFAULT_UNPRIVILEGED_LOGIN + [ "$status" -eq 0 ] +} + +@test "boundary/billing: default user cannot get last two months" { + run active_users_last_two_months + [ "$status" -eq 1 ] +} + +@test "boundary/billing: default user cannot get report with start time" { + run active_users_start_time "2023-09" + [ "$status" -eq 1 ] + run has_status_code "$output" "200" +} + +@test "boundary/billing: default user cannot get report with start and end times" { + run active_users_start_time_and_end_time "2023-09" "2023-12" + [ "$status" -eq 1 ] + run has_status_code "$output" "200" +} \ No newline at end of file diff --git a/internal/types/action/action.go b/internal/types/action/action.go index 5805b46331..da557a0c7d 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -74,7 +74,7 @@ const ( AddGrantScopes Type = 60 SetGrantScopes Type = 61 RemoveGrantScopes Type = 62 - MonthlyActiveUsers Type = 57 + MonthlyActiveUsers Type = 63 // When adding new actions, be sure to update: // diff --git a/internal/types/resource/resource.go b/internal/types/resource/resource.go index e9cb5609d5..f9ec1f3c29 100644 --- a/internal/types/resource/resource.go +++ b/internal/types/resource/resource.go @@ -85,6 +85,8 @@ func (r Type) PluralString() string { return "credential-libraries" case Policy: return "policies" + case Billing: // never pluralized + return "billing" default: return r.String() + "s" } @@ -96,6 +98,8 @@ func FromPlural(s string) (Type, bool) { return CredentialLibrary, true case "policies": return Policy, true + case "billing": + return Billing, true default: t, ok := Map[strings.TrimSuffix(s, "s")] return t, ok diff --git a/sdk/pbs/controller/api/resources/billing/billing.pb.go b/sdk/pbs/controller/api/resources/billing/billing.pb.go index 683809d441..b452d526df 100644 --- a/sdk/pbs/controller/api/resources/billing/billing.pb.go +++ b/sdk/pbs/controller/api/resources/billing/billing.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: controller/api/resources/billing/v1/billing.proto @@ -30,7 +30,7 @@ type ActiveUsers struct { unknownFields protoimpl.UnknownFields // Output only. The number of active users between the start time and end time. - Count uint64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty" class:"public"` // @gotags: `class:"public"` + Count uint32 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty" class:"public"` // @gotags: `class:"public"` // Output only. The start time of the active users count, inclusive. StartTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=start_time,proto3" json:"start_time,omitempty" class:"public"` // @gotags: class:"public" // Output only. The end time of the active users count, exclusive. @@ -69,7 +69,7 @@ func (*ActiveUsers) Descriptor() ([]byte, []int) { return file_controller_api_resources_billing_v1_billing_proto_rawDescGZIP(), []int{0} } -func (x *ActiveUsers) GetCount() uint64 { +func (x *ActiveUsers) GetCount() uint32 { if x != nil { return x.Count } @@ -102,7 +102,7 @@ var file_controller_api_resources_billing_v1_billing_proto_rawDesc = []byte{ 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x01, 0x0a, 0x0b, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3a, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,