feat(billing): Add service handlers for monthly active user counts

tmessi-cp-monthly-active-users
Michael Milton 2 years ago committed by Timothy Messier
parent 5ab364cef5
commit c1b9263c26
No known key found for this signature in database
GPG Key ID: EFD2F184F7600572

@ -244,6 +244,8 @@ protobuild:
@protoc-go-inject-tag -input=./internal/gen/controller/servers/servers.pb.go
@protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/policies/policy.pb.go
@protoc-go-inject-tag -input=./internal/gen/controller/api/services/policy_service.pb.go
@protoc-go-inject-tag -input=./sdk/pbs/controller/api/resources/billing/billing.pb.go
@protoc-go-inject-tag -input=./internal/gen/controller/api/services/billing_service.pb.go
# these protos, services and openapi artifacts are purely for testing purposes

@ -15,76 +15,6 @@ import (
"github.com/hashicorp/boundary/internal/errors"
)
const insertQuery = `
with
month_range (date_key, time_key, month) as (
select wh_date_key(s), wh_time_key(s), s
from generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
users (user_id, u) as (
select 'u_____user'||u, u
from generate_series(1, 6, 1) as u
),
user_key (key, user_id) as (
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email)
select users.user_id, 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
from users
returning key, user_id
),
tokens (date_key, time_key, user_id, token_id) as (
select wh_date_key(s), wh_time_key(s), users.user_id, 't_____u'||users.u||'tok'||s as token_id
from users,
generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
tokens_user_keys (date_key, time_key, user_id, token_id, user_key) as (
select tokens.date_key, tokens.time_key, tokens.user_id, tokens.token_id, user_key.key
from tokens
join user_key
on user_key.user_id = tokens.user_id
),
auth_tokens (user_key, user_id, token_id, valid_range) as (
select tokens_user_keys.user_key, tokens_user_keys.user_id, tokens_user_keys.token_id, tstzrange(month_range.month, month_range.month + interval '5 minutes', '[)')
from tokens_user_keys
join month_range
on month_range.date_key = tokens_user_keys.date_key
and month_range.time_key = tokens_user_keys.time_key
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select auth_tokens.token_id, auth_tokens.user_key,
wh_date_key(lower(auth_tokens.valid_range)), wh_time_key(lower(auth_tokens.valid_range)), lower(auth_tokens.valid_range),
coalesce(wh_date_key(upper(auth_tokens.valid_range)), -1), coalesce(wh_time_key(upper(auth_tokens.valid_range)), -1), upper(auth_tokens.valid_range),
wh_date_key(upper(auth_tokens.valid_range)), wh_time_key(upper(auth_tokens.valid_range)), upper(auth_tokens.valid_range),
auth_tokens.valid_range,
auth_tokens.valid_range,
1
from auth_tokens;
`
func TestRepository_New(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
@ -153,25 +83,16 @@ func TestRepository_New(t *testing.T) {
func TestRepository_MonthlyActiveUsers(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
TestGenerateActiveUsers(t, conn)
today := time.Now().UTC()
threeMonthsAgo := time.Date(today.AddDate(0, -3, 0).Year(), today.AddDate(0, -3, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
oneMonthAgo := time.Date(today.AddDate(0, -1, 0).Year(), today.AddDate(0, -1, 0).Month(), 1, 0, 0, 0, 0, time.UTC)
midMonth := time.Date(today.Year(), today.Month(), 15, 0, 0, 0, 0, time.UTC)
db, err := conn.SqlDB(ctx)
if err != nil {
t.Errorf("error getting db connection %s", err)
}
_, err = db.Exec(insertQuery)
if err != nil {
t.Errorf("error %s", err)
}
t.Run("valid-no-options", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
repo := TestRepo(t, conn)
activeUsers, err := repo.MonthlyActiveUsers(ctx)
assert.NoError(t, err)
require.Len(t, activeUsers, 2)
@ -188,8 +109,7 @@ func TestRepository_MonthlyActiveUsers(t *testing.T) {
})
t.Run("valid-with-start-time", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
repo := TestRepo(t, conn)
activeUsers, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&threeMonthsAgo))
assert.NoError(t, err)
require.Len(t, activeUsers, 4)
@ -212,52 +132,50 @@ func TestRepository_MonthlyActiveUsers(t *testing.T) {
})
t.Run("valid-with-start-and-end-time", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
repo := TestRepo(t, conn)
activeUsers, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&threeMonthsAgo), WithEndTime(&oneMonthAgo))
assert.NoError(t, err)
// since the end time is exclusive, we should only get one record of active users
// for the month of three months ago
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.Len(t, activeUsers, 1)
require.Equal(t, uint64(6), activeUsers[0].ActiveUsersCount)
assert.Equal(t, expectedStartTime, activeUsers[0].StartTime)
assert.Equal(t, expectedEndTime, activeUsers[0].EndTime)
})
t.Run("invalid-end-time-without-start-time", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
_, err = repo.MonthlyActiveUsers(ctx, WithEndTime(&oneMonthAgo))
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithEndTime(&oneMonthAgo))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: end time set without start time: parameter violation: error #100", err.Error())
})
t.Run("invalid-end-time-before-start-time", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
_, err = repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&threeMonthsAgo))
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&threeMonthsAgo))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: start time is not before end time: parameter violation: error #100", err.Error())
})
t.Run("invalid-start-time-equals-end-time", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
_, err = repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&oneMonthAgo))
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&oneMonthAgo))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: start time is not before end time: parameter violation: error #100", err.Error())
})
t.Run("invalid-start-time-not-first-day-of-month", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
_, err = repo.MonthlyActiveUsers(ctx, WithStartTime(&midMonth))
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&midMonth))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: start time must be the first day of the month at midnight UTC: parameter violation: error #100", err.Error())
})
t.Run("invalid-end-time-not-first-day-of-month", func(t *testing.T) {
repo, err := NewRepository(ctx, rw, rw)
assert.NoError(t, err)
_, err = repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&midMonth))
repo := TestRepo(t, conn)
_, err := repo.MonthlyActiveUsers(ctx, WithStartTime(&oneMonthAgo), WithEndTime(&midMonth))
assert.Error(t, err)
assert.Equal(t, "billing.Repository.MonthlyActiveUsers: end time must be the first day of the month at midnight UTC: parameter violation: error #100", err.Error())
})

@ -0,0 +1,104 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"context"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/stretchr/testify/require"
)
const insertQuery = `
with
month_range (date_key, time_key, month) as (
select wh_date_key(s), wh_time_key(s), s
from generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
users (user_id, u) as (
select 'u_____user'||u, u
from generate_series(1, 6, 1) as u
),
user_key (key, user_id) as (
insert into wh_user_dimension (
user_id, user_name, user_description,
auth_account_id, auth_account_type, auth_account_name, auth_account_description,
auth_method_id, auth_method_type, auth_method_name, auth_method_description,
user_organization_id, user_organization_name, user_organization_description,
current_row_indicator,
row_effective_time, row_expiration_time,
auth_method_external_id, auth_account_external_id, auth_account_full_name, auth_account_email)
select users.user_id, 'None', 'None',
'a______acc1', 'None', 'None', 'None',
'am______am1', 'None', 'None', 'None',
'o______org1', 'None', 'None',
'current',
now(), 'infinity'::timestamptz,
'None', 'None', 'None', 'None'
from users
returning key, user_id
),
tokens (date_key, time_key, user_id, token_id) as (
select wh_date_key(s), wh_time_key(s), users.user_id, 't_____u'||users.u||'tok'||s as token_id
from users,
generate_series(date_trunc('month', now()) - interval '1 year',
date_trunc('month', now()) - interval '1 month',
interval '1 month') as s
),
tokens_user_keys (date_key, time_key, user_id, token_id, user_key) as (
select tokens.date_key, tokens.time_key, tokens.user_id, tokens.token_id, user_key.key
from tokens
join user_key
on user_key.user_id = tokens.user_id
),
auth_tokens (user_key, user_id, token_id, valid_range) as (
select tokens_user_keys.user_key, tokens_user_keys.user_id, tokens_user_keys.token_id, tstzrange(month_range.month, month_range.month + interval '5 minutes', '[)')
from tokens_user_keys
join month_range
on month_range.date_key = tokens_user_keys.date_key
and month_range.time_key = tokens_user_keys.time_key
)
insert into wh_auth_token_accumulating_fact (
auth_token_id, user_key,
auth_token_issued_date_key, auth_token_issued_time_key, auth_token_issued_time,
auth_token_deleted_date_key, auth_token_deleted_time_key, auth_token_deleted_time,
auth_token_approximate_last_access_date_key, auth_token_approximate_last_access_time_key, auth_token_approximate_last_access_time,
auth_token_approximate_active_time_range,
auth_token_valid_time_range,
auth_token_count
)
select auth_tokens.token_id, auth_tokens.user_key,
wh_date_key(lower(auth_tokens.valid_range)), wh_time_key(lower(auth_tokens.valid_range)), lower(auth_tokens.valid_range),
coalesce(wh_date_key(upper(auth_tokens.valid_range)), -1), coalesce(wh_time_key(upper(auth_tokens.valid_range)), -1), upper(auth_tokens.valid_range),
wh_date_key(upper(auth_tokens.valid_range)), wh_time_key(upper(auth_tokens.valid_range)), upper(auth_tokens.valid_range),
auth_tokens.valid_range,
auth_tokens.valid_range,
1
from auth_tokens;
`
// TestRepo creates a repo that can be used for various testing purposes.
func TestRepo(t testing.TB, conn *db.DB) *Repository {
t.Helper()
ctx := context.Background()
require := require.New(t)
rw := db.New(conn)
repo, err := NewRepository(ctx, rw, rw)
require.NoError(err)
return repo
}
// TestGenerateActiveUsers is a test helper that populates the data warehouse
// with active users for the last twelve months.
func TestGenerateActiveUsers(t testing.TB, conn *db.DB) {
t.Helper()
db, err := conn.SqlDB(context.Background())
require.NoError(t, err)
_, err = db.Exec(insertQuery)
require.NoError(t, err)
}

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/billing"
"github.com/hashicorp/boundary/internal/credential"
credstatic "github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential/vault"
@ -39,6 +40,7 @@ type (
ConnectionRepoFactory func() (*session.ConnectionRepository, error)
WorkerAuthRepoStorageFactory func() (*server.WorkerAuthRepositoryStorage, error)
PluginStorageBucketRepoFactory func() (*pluginstorage.Repository, error)
BillingRepoFactory func() (*billing.Repository, error)
)
// Downstreamers provides at least a minimum interface that must be met by a

@ -16,6 +16,7 @@ import (
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/billing"
"github.com/hashicorp/boundary/internal/census"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/config"
@ -148,6 +149,7 @@ type Controller struct {
PluginRepoFn common.PluginRepoFactory
TargetRepoFn target.RepositoryFactory
WorkerAuthRepoStorageFn common.WorkerAuthRepoStorageFactory
BillingRepoFn common.BillingRepoFactory
scheduler *scheduler.Scheduler
@ -448,6 +450,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) {
c.WorkerAuthRepoStorageFn = func() (*server.WorkerAuthRepositoryStorage, error) {
return server.NewRepositoryStorage(ctx, dbase, dbase, c.kms)
}
c.BillingRepoFn = func() (*billing.Repository, error) {
return billing.NewRepository(ctx, dbase, dbase)
}
// Check that credentials are available at startup, to avoid some harmless
// but nasty-looking errors

@ -24,6 +24,7 @@ import (
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"
@ -349,6 +350,13 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error {
opsservices.RegisterHealthServiceServer(s, hs)
c.HealthService = hs
}
if _, ok := currentServices[services.BillingService_ServiceDesc.ServiceName]; !ok {
bs, err := billing.NewService(c.baseContext, c.BillingRepoFn)
if err != nil {
return fmt.Errorf("failed to create billing handler service: %w", err)
}
services.RegisterBillingServiceServer(s, bs)
}
return nil
}
@ -419,6 +427,9 @@ func registerGrpcGatewayEndpoints(ctx context.Context, gwMux *runtime.ServeMux,
if err := services.RegisterPolicyServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
return fmt.Errorf("failed to register policy handler: %w", err)
}
if err := services.RegisterBillingServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
return fmt.Errorf("failed to register billing service handler: %w", err)
}
return nil
}

@ -138,6 +138,7 @@ func TestHandleImplementedPaths(t *testing.T) {
"v1/targets/someid",
"v1/users",
"v1/users/someid",
"v1/billing:monthly-active-users",
},
"POST": {
// Creation end points

@ -0,0 +1,115 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"context"
"time"
"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/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"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing"
"google.golang.org/protobuf/types/known/timestamppb"
)
var (
// IdActions contains the set of actions that can be performed on
// individual resources
IdActions = action.NewActionSet()
// CollectionActions contains the set of actions that can be performed on
// this collection
CollectionActions = action.NewActionSet(
action.MonthlyActiveUsers,
)
)
func init() {
// TODO: refactor to remove IdActions and CollectionActions package variables
action.RegisterResource(resource.Billing, IdActions, CollectionActions)
}
type Service struct {
pbs.UnsafeBillingServiceServer
repoFn common.BillingRepoFactory
}
var _ pbs.BillingServiceServer = (*Service)(nil)
// NewService returns a billing service which handles billing related requests to boundary.
func NewService(
ctx context.Context,
repoFn common.BillingRepoFactory,
) (Service, error) {
const op = "billing.NewService"
if repoFn == nil {
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing billing repository")
}
return Service{
repoFn: repoFn,
}, nil
}
func (s Service) MonthlyActiveUsers(ctx context.Context, req *pbs.MonthlyActiveUsersRequest) (*pbs.MonthlyActiveUsersResponse, error) {
const op = "billing.(Service).MonthlyActiveUsers"
authResults := s.authResult(ctx, action.MonthlyActiveUsers)
if authResults.Error != nil {
return nil, errors.Wrap(ctx, authResults.Error, op)
}
// Get the billing information
repo, err := s.repoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var startTime, endTime *time.Time
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")
}
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")
}
endTime = &et
}
months, err := repo.MonthlyActiveUsers(
ctx,
billing.WithStartTime(startTime),
billing.WithEndTime(endTime),
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var activeUsers []*pb.ActiveUsers
for _, month := range months {
maud := &pb.ActiveUsers{
StartTime: timestamppb.New(month.StartTime),
EndTime: timestamppb.New(month.EndTime),
Count: month.ActiveUsersCount,
}
activeUsers = append(activeUsers, maud)
}
return &pbs.MonthlyActiveUsersResponse{Items: activeUsers}, nil
}
func (s Service) authResult(ctx context.Context, a action.Type) auth.VerifyResults {
opts := []auth.Option{auth.WithType(resource.Billing), auth.WithAction(a)}
return auth.Verify(ctx, opts...)
}

@ -0,0 +1,164 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing_test
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/billing"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
billingservice "github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
"github.com/hashicorp/boundary/internal/db"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/types/scope"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_MonthlyActiveUsers(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
repoFn := func() (*billing.Repository, error) {
return billing.TestRepo(t, conn), nil
}
billing.TestGenerateActiveUsers(t, conn)
wrap := db.TestWrapper(t)
iamRepoFn := func() (*iam.Repository, error) {
return iam.TestRepo(t, conn, wrap), nil
}
today := time.Now().UTC()
threeMonthsAgo := time.Date(today.AddDate(0, -3, 0).Year(), today.AddDate(0, -3, 0).Month(), 1, 0, 0, 0, 0, time.UTC).Format("2006-01")
oneMonthAgo := time.Date(today.AddDate(0, -1, 0).Year(), today.AddDate(0, -1, 0).Month(), 1, 0, 0, 0, 0, time.UTC).Format("2006-01")
badFormat := time.Date(today.Year(), today.Month(), 15, 0, 0, 0, 0, time.UTC).String()
cases := []struct {
name string
req *pbs.MonthlyActiveUsersRequest
res *pbs.MonthlyActiveUsersResponse
errContains string
}{
{
name: "Valid no options, current and previous months",
req: &pbs.MonthlyActiveUsersRequest{},
res: &pbs.MonthlyActiveUsersResponse{
Items: []*pb.ActiveUsers{
{
Count: 0,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), today.Day(), today.Hour(), 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
},
},
},
},
{
name: "Valid start time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo},
res: &pbs.MonthlyActiveUsersResponse{
Items: []*pb.ActiveUsers{
{
Count: 0,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), today.Day(), today.Hour(), 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-2, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month()-1, 1, 0, 0, 0, 0, time.UTC)),
},
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-3, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month()-2, 1, 0, 0, 0, 0, time.UTC)),
},
},
},
},
{
name: "Valid start and end time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo, EndTime: oneMonthAgo},
res: &pbs.MonthlyActiveUsersResponse{
Items: []*pb.ActiveUsers{
{
Count: 6,
StartTime: timestamppb.New(time.Date(today.Year(), today.Month()-3, 1, 0, 0, 0, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(today.Year(), today.Month()-2, 1, 0, 0, 0, 0, time.UTC)),
},
},
},
},
{
name: "Invalid end time without start time",
req: &pbs.MonthlyActiveUsersRequest{EndTime: oneMonthAgo},
errContains: "end time set without start time",
},
{
name: "Invalid end time before start time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: oneMonthAgo, EndTime: threeMonthsAgo},
errContains: "start time is not before end time",
},
{
name: "Invalid start time equals end time",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo, EndTime: threeMonthsAgo},
errContains: "start time is not before end time",
},
{
name: "Invalid start time format",
req: &pbs.MonthlyActiveUsersRequest{StartTime: badFormat},
errContains: "start time is in an invalid format",
},
{
name: "Invalid end time format",
req: &pbs.MonthlyActiveUsersRequest{StartTime: threeMonthsAgo, EndTime: badFormat},
errContains: "end time is in an invalid format",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
b, err := billingservice.NewService(ctx, repoFn)
require.NoError(t, err, "Couldn't create new billing service.")
got, gErr := b.MonthlyActiveUsers(auth.DisabledAuthTestContext(iamRepoFn, scope.Global.String(), auth.WithUserId(globals.AnyAuthenticatedUserId)), tc.req)
if tc.errContains != "" {
require.ErrorContains(t, gErr, tc.errContains)
require.Nil(t, got)
return
} else {
require.NoError(t, gErr)
}
assert.Empty(t,
cmp.Diff(
got,
tc.res,
protocmp.Transform(),
protocmp.SortRepeatedFields(got),
cmpopts.SortSlices(func(a, b string) bool {
return a < b
}),
))
})
}
}

@ -17,6 +17,9 @@
{
"name": "controller.api.services.v1.AuthTokenService"
},
{
"name": "controller.api.services.v1.BillingService"
},
{
"name": "controller.api.services.v1.CredentialLibraryService"
},
@ -690,6 +693,39 @@
]
}
},
"/v1/billing:monthly-active-users": {
"get": {
"summary": "Returns monthly active users.",
"operationId": "BillingService_MonthlyActiveUsers",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/controller.api.services.v1.MonthlyActiveUsersResponse"
}
}
},
"parameters": [
{
"name": "start_time",
"description": "An optional start time of the billing period to query, in the format of YYYY-MM.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "end_time",
"description": "An optional end time of the billing period to query, in the format of YYYY-MM.",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"controller.api.services.v1.BillingService"
]
}
},
"/v1/credential-libraries": {
"get": {
"summary": "Lists all Credential Library.",
@ -5261,6 +5297,29 @@
},
"title": "AuthToken contains all fields related to an Auth Token resource"
},
"controller.api.resources.billing.v1.ActiveUsers": {
"type": "object",
"properties": {
"count": {
"type": "string",
"format": "uint64",
"description": "Output only. The number of active users between the start time and end time.",
"readOnly": true
},
"start_time": {
"type": "string",
"format": "date-time",
"description": "Output only. The start time of the active users count, inclusive.",
"readOnly": true
},
"end_time": {
"type": "string",
"format": "date-time",
"description": "Output only. The end time of the active users count, exclusive.",
"readOnly": true
}
}
},
"controller.api.resources.credentiallibraries.v1.CredentialLibrary": {
"type": "object",
"properties": {
@ -9123,6 +9182,18 @@
}
}
},
"controller.api.services.v1.MonthlyActiveUsersResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/controller.api.resources.billing.v1.ActiveUsers"
}
}
}
},
"controller.api.services.v1.ReadCertificateAuthorityResponse": {
"type": "object",
"properties": {

@ -0,0 +1,264 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc (unknown)
// source: controller/api/services/v1/billing_service.proto
package services
import (
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options"
billing "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing"
_ "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"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type MonthlyActiveUsersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// An optional start time of the billing period to query, in the format of YYYY-MM.
StartTime string `protobuf:"bytes,1,opt,name=start_time,proto3" json:"start_time,omitempty" class:"public"` // @gotags: class:"public"
// An optional end time of the billing period to query, in the format of YYYY-MM.
EndTime string `protobuf:"bytes,2,opt,name=end_time,proto3" json:"end_time,omitempty" class:"public"` // @gotags: class:"public"
}
func (x *MonthlyActiveUsersRequest) Reset() {
*x = MonthlyActiveUsersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MonthlyActiveUsersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MonthlyActiveUsersRequest) ProtoMessage() {}
func (x *MonthlyActiveUsersRequest) ProtoReflect() protoreflect.Message {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MonthlyActiveUsersRequest.ProtoReflect.Descriptor instead.
func (*MonthlyActiveUsersRequest) Descriptor() ([]byte, []int) {
return file_controller_api_services_v1_billing_service_proto_rawDescGZIP(), []int{0}
}
func (x *MonthlyActiveUsersRequest) GetStartTime() string {
if x != nil {
return x.StartTime
}
return ""
}
func (x *MonthlyActiveUsersRequest) GetEndTime() string {
if x != nil {
return x.EndTime
}
return ""
}
type MonthlyActiveUsersResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Items []*billing.ActiveUsers `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
}
func (x *MonthlyActiveUsersResponse) Reset() {
*x = MonthlyActiveUsersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MonthlyActiveUsersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MonthlyActiveUsersResponse) ProtoMessage() {}
func (x *MonthlyActiveUsersResponse) ProtoReflect() protoreflect.Message {
mi := &file_controller_api_services_v1_billing_service_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MonthlyActiveUsersResponse.ProtoReflect.Descriptor instead.
func (*MonthlyActiveUsersResponse) Descriptor() ([]byte, []int) {
return file_controller_api_services_v1_billing_service_proto_rawDescGZIP(), []int{1}
}
func (x *MonthlyActiveUsersResponse) GetItems() []*billing.ActiveUsers {
if x != nil {
return x.Items
}
return nil
}
var File_controller_api_services_v1_billing_service_proto protoreflect.FileDescriptor
var file_controller_api_services_v1_billing_service_proto_rawDesc = []byte{
0x0a, 0x30, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c,
0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x1a, 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, 0x1a, 0x31,
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67,
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,
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,
}
var (
file_controller_api_services_v1_billing_service_proto_rawDescOnce sync.Once
file_controller_api_services_v1_billing_service_proto_rawDescData = file_controller_api_services_v1_billing_service_proto_rawDesc
)
func file_controller_api_services_v1_billing_service_proto_rawDescGZIP() []byte {
file_controller_api_services_v1_billing_service_proto_rawDescOnce.Do(func() {
file_controller_api_services_v1_billing_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_services_v1_billing_service_proto_rawDescData)
})
return file_controller_api_services_v1_billing_service_proto_rawDescData
}
var file_controller_api_services_v1_billing_service_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_controller_api_services_v1_billing_service_proto_goTypes = []interface{}{
(*MonthlyActiveUsersRequest)(nil), // 0: controller.api.services.v1.MonthlyActiveUsersRequest
(*MonthlyActiveUsersResponse)(nil), // 1: controller.api.services.v1.MonthlyActiveUsersResponse
(*billing.ActiveUsers)(nil), // 2: controller.api.resources.billing.v1.ActiveUsers
}
var file_controller_api_services_v1_billing_service_proto_depIdxs = []int32{
2, // 0: controller.api.services.v1.MonthlyActiveUsersResponse.items:type_name -> controller.api.resources.billing.v1.ActiveUsers
0, // 1: controller.api.services.v1.BillingService.MonthlyActiveUsers:input_type -> controller.api.services.v1.MonthlyActiveUsersRequest
1, // 2: controller.api.services.v1.BillingService.MonthlyActiveUsers:output_type -> controller.api.services.v1.MonthlyActiveUsersResponse
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_controller_api_services_v1_billing_service_proto_init() }
func file_controller_api_services_v1_billing_service_proto_init() {
if File_controller_api_services_v1_billing_service_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_controller_api_services_v1_billing_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MonthlyActiveUsersRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_controller_api_services_v1_billing_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MonthlyActiveUsersResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_api_services_v1_billing_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_controller_api_services_v1_billing_service_proto_goTypes,
DependencyIndexes: file_controller_api_services_v1_billing_service_proto_depIdxs,
MessageInfos: file_controller_api_services_v1_billing_service_proto_msgTypes,
}.Build()
File_controller_api_services_v1_billing_service_proto = out.File
file_controller_api_services_v1_billing_service_proto_rawDesc = nil
file_controller_api_services_v1_billing_service_proto_goTypes = nil
file_controller_api_services_v1_billing_service_proto_depIdxs = nil
}

@ -0,0 +1,173 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: controller/api/services/v1/billing_service.proto
/*
Package services is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package services
import (
"context"
"io"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = metadata.Join
var (
filter_BillingService_MonthlyActiveUsers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_BillingService_MonthlyActiveUsers_0(ctx context.Context, marshaler runtime.Marshaler, client BillingServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq MonthlyActiveUsersRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BillingService_MonthlyActiveUsers_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.MonthlyActiveUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BillingService_MonthlyActiveUsers_0(ctx context.Context, marshaler runtime.Marshaler, server BillingServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq MonthlyActiveUsersRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BillingService_MonthlyActiveUsers_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.MonthlyActiveUsers(ctx, &protoReq)
return msg, metadata, err
}
// RegisterBillingServiceHandlerServer registers the http handlers for service BillingService to "mux".
// UnaryRPC :call BillingServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterBillingServiceHandlerFromEndpoint instead.
func RegisterBillingServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server BillingServiceServer) error {
mux.Handle("GET", pattern_BillingService_MonthlyActiveUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.BillingService/MonthlyActiveUsers", runtime.WithHTTPPathPattern("/v1/billing:monthly-active-users"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BillingService_MonthlyActiveUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BillingService_MonthlyActiveUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterBillingServiceHandlerFromEndpoint is same as RegisterBillingServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterBillingServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.DialContext(ctx, endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterBillingServiceHandler(ctx, mux, conn)
}
// RegisterBillingServiceHandler registers the http handlers for service BillingService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterBillingServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterBillingServiceHandlerClient(ctx, mux, NewBillingServiceClient(conn))
}
// RegisterBillingServiceHandlerClient registers the http handlers for service BillingService
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "BillingServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "BillingServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "BillingServiceClient" to call the correct interceptors.
func RegisterBillingServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client BillingServiceClient) error {
mux.Handle("GET", pattern_BillingService_MonthlyActiveUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.BillingService/MonthlyActiveUsers", runtime.WithHTTPPathPattern("/v1/billing:monthly-active-users"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BillingService_MonthlyActiveUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BillingService_MonthlyActiveUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_BillingService_MonthlyActiveUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "billing"}, "monthly-active-users"))
)
var (
forward_BillingService_MonthlyActiveUsers_0 = runtime.ForwardResponseMessage
)

@ -0,0 +1,122 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc (unknown)
// source: controller/api/services/v1/billing_service.proto
package services
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
BillingService_MonthlyActiveUsers_FullMethodName = "/controller.api.services.v1.BillingService/MonthlyActiveUsers"
)
// BillingServiceClient is the client API for BillingService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type BillingServiceClient interface {
// MonthlyActiveUsers returns the monthly active users for the given time period.
// If no time period is provided, the current and previous months are returned.
// If the provided request contains a start time but no end time, it will return
// up to the current month. If the provided request contains an end time and no start time,
// or if the end time is prior to the start time, an error will be returned.
MonthlyActiveUsers(ctx context.Context, in *MonthlyActiveUsersRequest, opts ...grpc.CallOption) (*MonthlyActiveUsersResponse, error)
}
type billingServiceClient struct {
cc grpc.ClientConnInterface
}
func NewBillingServiceClient(cc grpc.ClientConnInterface) BillingServiceClient {
return &billingServiceClient{cc}
}
func (c *billingServiceClient) MonthlyActiveUsers(ctx context.Context, in *MonthlyActiveUsersRequest, opts ...grpc.CallOption) (*MonthlyActiveUsersResponse, error) {
out := new(MonthlyActiveUsersResponse)
err := c.cc.Invoke(ctx, BillingService_MonthlyActiveUsers_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// BillingServiceServer is the server API for BillingService service.
// All implementations must embed UnimplementedBillingServiceServer
// for forward compatibility
type BillingServiceServer interface {
// MonthlyActiveUsers returns the monthly active users for the given time period.
// If no time period is provided, the current and previous months are returned.
// If the provided request contains a start time but no end time, it will return
// up to the current month. If the provided request contains an end time and no start time,
// or if the end time is prior to the start time, an error will be returned.
MonthlyActiveUsers(context.Context, *MonthlyActiveUsersRequest) (*MonthlyActiveUsersResponse, error)
mustEmbedUnimplementedBillingServiceServer()
}
// UnimplementedBillingServiceServer must be embedded to have forward compatible implementations.
type UnimplementedBillingServiceServer struct {
}
func (UnimplementedBillingServiceServer) MonthlyActiveUsers(context.Context, *MonthlyActiveUsersRequest) (*MonthlyActiveUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method MonthlyActiveUsers not implemented")
}
func (UnimplementedBillingServiceServer) mustEmbedUnimplementedBillingServiceServer() {}
// UnsafeBillingServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to BillingServiceServer will
// result in compilation errors.
type UnsafeBillingServiceServer interface {
mustEmbedUnimplementedBillingServiceServer()
}
func RegisterBillingServiceServer(s grpc.ServiceRegistrar, srv BillingServiceServer) {
s.RegisterService(&BillingService_ServiceDesc, srv)
}
func _BillingService_MonthlyActiveUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MonthlyActiveUsersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BillingServiceServer).MonthlyActiveUsers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: BillingService_MonthlyActiveUsers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BillingServiceServer).MonthlyActiveUsers(ctx, req.(*MonthlyActiveUsersRequest))
}
return interceptor(ctx, in, info, handler)
}
// BillingService_ServiceDesc is the grpc.ServiceDesc for BillingService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var BillingService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "controller.api.services.v1.BillingService",
HandlerType: (*BillingServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "MonthlyActiveUsers",
Handler: _BillingService_MonthlyActiveUsers_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "controller/api/services/v1/billing_service.proto",
}

@ -932,7 +932,7 @@ func Test_AnonRestrictions(t *testing.T) {
if i == resource.Controller || i == resource.Worker {
continue
}
for j := action.Type(1); j <= action.RemoveGrantScopes; j++ {
for j := action.Type(1); j <= action.MonthlyActiveUsers; j++ {
id := "foobar"
prefixes := globals.ResourcePrefixesFromType(resource.Type(i))
if len(prefixes) > 0 {

@ -0,0 +1,21 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
syntax = "proto3";
package controller.api.resources.billing.v1;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/billing;billing";
message ActiveUsers {
// Output only. The number of active users between the start time and end time.
uint64 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"
// Output only. The end time of the active users count, exclusive.
google.protobuf.Timestamp end_time = 3 [json_name = "end_time"]; // @gotags: class:"public"
}

@ -0,0 +1,37 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
syntax = "proto3";
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";
service BillingService {
// MonthlyActiveUsers returns the monthly active users for the given time period.
// If no time period is provided, the current and previous months are returned.
// If the provided request contains a start time but no end time, it will return
// up to the current month. If the provided request contains an end time and no start time,
// or if the end time is prior to the start time, an error will be returned.
rpc MonthlyActiveUsers(MonthlyActiveUsersRequest) returns (MonthlyActiveUsersResponse) {
option (google.api.http) = {get: "/v1/billing:monthly-active-users"};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {summary: "Returns monthly active users."};
}
}
message MonthlyActiveUsersRequest {
// An optional start time of the billing period to query, in the format of YYYY-MM.
string start_time = 1 [json_name = "start_time"]; // @gotags: class:"public"
// An optional end time of the billing period to query, in the format of YYYY-MM.
string end_time = 2 [json_name = "end_time"]; // @gotags: class:"public"
}
message MonthlyActiveUsersResponse {
repeated resources.billing.v1.ActiveUsers items = 1;
}

@ -19,6 +19,7 @@ import (
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
_ "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"

@ -74,6 +74,7 @@ const (
AddGrantScopes Type = 60
SetGrantScopes Type = 61
RemoveGrantScopes Type = 62
MonthlyActiveUsers Type = 57
// When adding new actions, be sure to update:
//
@ -144,6 +145,7 @@ var Map = map[string]Type{
AddGrantScopes.String(): AddGrantScopes,
SetGrantScopes.String(): SetGrantScopes,
RemoveGrantScopes.String(): RemoveGrantScopes,
MonthlyActiveUsers.String(): MonthlyActiveUsers,
}
var DeprecatedMap = map[string]Type{
@ -220,6 +222,7 @@ func (a Type) String() string {
"add-grant-scopes",
"set-grant-scopes",
"remove-grant-scopes",
"monthly-active-users",
}[a]
}

@ -35,6 +35,7 @@ const (
Credential
StorageBucket
Policy
Billing
// NOTE: When adding a new type, be sure to update:
//
// * The Grant.validateType function and test
@ -74,6 +75,7 @@ func (r Type) String() string {
"credential",
"storage-bucket",
"policy",
"billing",
}[r]
}
@ -124,6 +126,7 @@ var Map = map[string]Type{
Credential.String(): Credential,
StorageBucket.String(): StorageBucket,
Policy.String(): Policy,
Billing.String(): Billing,
}
// Parent returns the parent type for a given type; if there is no parent, it

@ -0,0 +1,185 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc (unknown)
// source: controller/api/resources/billing/v1/billing.proto
package billing
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ActiveUsers struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
// 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.
EndTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=end_time,proto3" json:"end_time,omitempty" class:"public"` // @gotags: class:"public"
}
func (x *ActiveUsers) Reset() {
*x = ActiveUsers{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_api_resources_billing_v1_billing_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ActiveUsers) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ActiveUsers) ProtoMessage() {}
func (x *ActiveUsers) ProtoReflect() protoreflect.Message {
mi := &file_controller_api_resources_billing_v1_billing_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ActiveUsers.ProtoReflect.Descriptor instead.
func (*ActiveUsers) Descriptor() ([]byte, []int) {
return file_controller_api_resources_billing_v1_billing_proto_rawDescGZIP(), []int{0}
}
func (x *ActiveUsers) GetCount() uint64 {
if x != nil {
return x.Count
}
return 0
}
func (x *ActiveUsers) GetStartTime() *timestamppb.Timestamp {
if x != nil {
return x.StartTime
}
return nil
}
func (x *ActiveUsers) GetEndTime() *timestamppb.Timestamp {
if x != nil {
return x.EndTime
}
return nil
}
var File_controller_api_resources_billing_v1_billing_proto protoreflect.FileDescriptor
var file_controller_api_resources_billing_v1_billing_proto_rawDesc = []byte{
0x0a, 0x31, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69,
0x6e, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x23, 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, 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, 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,
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,
0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x65,
0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 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, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74,
0x69, 0x6d, 0x65, 0x42, 0x50, 0x5a, 0x4e, 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, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e,
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x3b, 0x62, 0x69,
0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_controller_api_resources_billing_v1_billing_proto_rawDescOnce sync.Once
file_controller_api_resources_billing_v1_billing_proto_rawDescData = file_controller_api_resources_billing_v1_billing_proto_rawDesc
)
func file_controller_api_resources_billing_v1_billing_proto_rawDescGZIP() []byte {
file_controller_api_resources_billing_v1_billing_proto_rawDescOnce.Do(func() {
file_controller_api_resources_billing_v1_billing_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_resources_billing_v1_billing_proto_rawDescData)
})
return file_controller_api_resources_billing_v1_billing_proto_rawDescData
}
var file_controller_api_resources_billing_v1_billing_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_controller_api_resources_billing_v1_billing_proto_goTypes = []interface{}{
(*ActiveUsers)(nil), // 0: controller.api.resources.billing.v1.ActiveUsers
(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
}
var file_controller_api_resources_billing_v1_billing_proto_depIdxs = []int32{
1, // 0: controller.api.resources.billing.v1.ActiveUsers.start_time:type_name -> google.protobuf.Timestamp
1, // 1: controller.api.resources.billing.v1.ActiveUsers.end_time:type_name -> google.protobuf.Timestamp
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_controller_api_resources_billing_v1_billing_proto_init() }
func file_controller_api_resources_billing_v1_billing_proto_init() {
if File_controller_api_resources_billing_v1_billing_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_controller_api_resources_billing_v1_billing_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ActiveUsers); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_api_resources_billing_v1_billing_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_controller_api_resources_billing_v1_billing_proto_goTypes,
DependencyIndexes: file_controller_api_resources_billing_v1_billing_proto_depIdxs,
MessageInfos: file_controller_api_resources_billing_v1_billing_proto_msgTypes,
}.Build()
File_controller_api_resources_billing_v1_billing_proto = out.File
file_controller_api_resources_billing_v1_billing_proto_rawDesc = nil
file_controller_api_resources_billing_v1_billing_proto_goTypes = nil
file_controller_api_resources_billing_v1_billing_proto_depIdxs = nil
}
Loading…
Cancel
Save