feat(billing): Add cli subcommand for monthly active user counts
This commit is contained in:
1 parent
111dc87ead
commit
2e69d59dd2
24 files changed
+791
-73
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user