feat(billing): Add cli subcommand for monthly active user counts

pull/4445/head
Michael Milton 2 years ago committed by Timothy Messier
parent 111dc87ead
commit 2e69d59dd2
No known key found for this signature in database
GPG Key ID: EFD2F184F7600572

@ -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
}

@ -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
}

@ -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)
}
}

@ -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{},

@ -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" ) }}

@ -11,5 +11,5 @@ import "time"
type ActiveUsers struct {
StartTime time.Time
EndTime time.Time
ActiveUsersCount uint64
ActiveUsersCount uint32
}

@ -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

@ -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)
})

@ -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...),

@ -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 }
)

@ -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()
}

@ -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",

@ -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(),

@ -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 }
)

@ -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...)
}

@ -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": {

@ -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 (

@ -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"

@ -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";

@ -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
}

@ -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"
}

@ -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:
//

@ -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

@ -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,

Loading…
Cancel
Save