diff --git a/Makefile b/Makefile index 33cd93b67d..dc2226355d 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/internal/billing/repository_test.go b/internal/billing/repository_test.go index f84bde4514..6b370324b7 100644 --- a/internal/billing/repository_test.go +++ b/internal/billing/repository_test.go @@ -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()) }) diff --git a/internal/billing/testing.go b/internal/billing/testing.go new file mode 100644 index 0000000000..a657f87977 --- /dev/null +++ b/internal/billing/testing.go @@ -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) +} diff --git a/internal/daemon/controller/common/common.go b/internal/daemon/controller/common/common.go index 9f4eb0a9b2..d733427646 100644 --- a/internal/daemon/controller/common/common.go +++ b/internal/daemon/controller/common/common.go @@ -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 diff --git a/internal/daemon/controller/controller.go b/internal/daemon/controller/controller.go index 97dc2191ec..9414e8aa68 100644 --- a/internal/daemon/controller/controller.go +++ b/internal/daemon/controller/controller.go @@ -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 diff --git a/internal/daemon/controller/handler.go b/internal/daemon/controller/handler.go index 9d6f90d8e8..a682b5b945 100644 --- a/internal/daemon/controller/handler.go +++ b/internal/daemon/controller/handler.go @@ -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 } diff --git a/internal/daemon/controller/handler_test.go b/internal/daemon/controller/handler_test.go index 5f2fadfdd8..6ecc142c1f 100644 --- a/internal/daemon/controller/handler_test.go +++ b/internal/daemon/controller/handler_test.go @@ -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 diff --git a/internal/daemon/controller/handlers/billing/billing_service.go b/internal/daemon/controller/handlers/billing/billing_service.go new file mode 100644 index 0000000000..8173be129c --- /dev/null +++ b/internal/daemon/controller/handlers/billing/billing_service.go @@ -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...) +} diff --git a/internal/daemon/controller/handlers/billing/billing_service_test.go b/internal/daemon/controller/handlers/billing/billing_service_test.go new file mode 100644 index 0000000000..f9b41b0c84 --- /dev/null +++ b/internal/daemon/controller/handlers/billing/billing_service_test.go @@ -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 + }), + )) + }) + } +} diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index cefdcf8699..b56f4c64d3 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -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": { diff --git a/internal/gen/controller/api/services/billing_service.pb.go b/internal/gen/controller/api/services/billing_service.pb.go new file mode 100644 index 0000000000..90674badc6 --- /dev/null +++ b/internal/gen/controller/api/services/billing_service.pb.go @@ -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 +} diff --git a/internal/gen/controller/api/services/billing_service.pb.gw.go b/internal/gen/controller/api/services/billing_service.pb.gw.go new file mode 100644 index 0000000000..71a853af30 --- /dev/null +++ b/internal/gen/controller/api/services/billing_service.pb.gw.go @@ -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 +) diff --git a/internal/gen/controller/api/services/billing_service_grpc.pb.go b/internal/gen/controller/api/services/billing_service_grpc.pb.go new file mode 100644 index 0000000000..3875864f7a --- /dev/null +++ b/internal/gen/controller/api/services/billing_service_grpc.pb.go @@ -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", +} diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index 0cde2e6e99..f776d9f879 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -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 { diff --git a/internal/proto/controller/api/resources/billing/v1/billing.proto b/internal/proto/controller/api/resources/billing/v1/billing.proto new file mode 100644 index 0000000000..02648307eb --- /dev/null +++ b/internal/proto/controller/api/resources/billing/v1/billing.proto @@ -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" +} diff --git a/internal/proto/controller/api/services/v1/billing_service.proto b/internal/proto/controller/api/services/v1/billing_service.proto new file mode 100644 index 0000000000..49e309f9a5 --- /dev/null +++ b/internal/proto/controller/api/services/v1/billing_service.proto @@ -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; +} diff --git a/internal/ratelimit/config.go b/internal/ratelimit/config.go index 68e2eaceb6..7c7b47633d 100644 --- a/internal/ratelimit/config.go +++ b/internal/ratelimit/config.go @@ -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" diff --git a/internal/types/action/action.go b/internal/types/action/action.go index f5045182b2..f8d560590b 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -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] } diff --git a/internal/types/resource/resource.go b/internal/types/resource/resource.go index 02f3589ef4..e9cb5609d5 100644 --- a/internal/types/resource/resource.go +++ b/internal/types/resource/resource.go @@ -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 diff --git a/sdk/pbs/controller/api/resources/billing/billing.pb.go b/sdk/pbs/controller/api/resources/billing/billing.pb.go new file mode 100644 index 0000000000..683809d441 --- /dev/null +++ b/sdk/pbs/controller/api/resources/billing/billing.pb.go @@ -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 +}