Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -4843,6 +4843,14 @@ func (q *querier) GetTemplateInsightsByInterval(ctx context.Context, arg databas
return q.db.GetTemplateInsightsByInterval(ctx, arg)
}

func (q *querier) GetTemplateInsightsByTemplate(ctx context.Context, arg database.GetTemplateInsightsByTemplateParams) ([]database.GetTemplateInsightsByTemplateRow, error) {
// Only used by prometheus metrics collector. No need to check update template perms.
if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil {
return nil, err
}
return q.db.GetTemplateInsightsByTemplate(ctx, arg)
}

func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil {
return nil, err
Expand Down Expand Up @@ -9259,6 +9267,13 @@ func (q *querier) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTe
return q.db.UpsertTelemetryItem(ctx, arg)
}

func (q *querier) UpsertTemplateUsageStats(ctx context.Context) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return err
}
return q.db.UpsertTemplateUsageStats(ctx)
}

func (q *querier) UpsertUserAIBudgetOverride(ctx context.Context, arg database.UpsertUserAIBudgetOverrideParams) (database.UserAIBudgetOverride, error) {
// Setting a user's AI budget override affects both the user (their
// per-user spend cap) and the group (spend attribution).
Expand Down
93 changes: 3 additions & 90 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"database/sql"
"encoding/json"
"fmt"
"maps"
"net"
"reflect"
"testing"
Expand Down Expand Up @@ -3049,7 +3048,7 @@ func (s *MethodTestSuite) TestTemplate() {
check.Args(arg).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights)
}))
s.Run("GetTemplateInsightsByTemplate", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.GetTemplateInsightsByTemplateParams{AppFamilies: codersdk.SessionCountAppFamiliesJSON()}
arg := database.GetTemplateInsightsByTemplateParams{}
dbm.EXPECT().GetTemplateInsightsByTemplate(gomock.Any(), arg).Return([]database.GetTemplateInsightsByTemplateRow{}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights)
}))
Expand All @@ -3069,9 +3068,8 @@ func (s *MethodTestSuite) TestTemplate() {
check.Args(arg).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights).Returns([]database.TemplateUsageStat{})
}))
s.Run("UpsertTemplateUsageStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := codersdk.SessionCountAppFamiliesJSON()
dbm.EXPECT().UpsertTemplateUsageStats(gomock.Any(), arg).Return(nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
dbm.EXPECT().UpsertTemplateUsageStats(gomock.Any()).Return(nil).AnyTimes()
check.Args().Asserts(rbac.ResourceSystem, policy.ActionUpdate)
}))
s.Run("UpdatePresetsLastInvalidatedAt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
t1 := testutil.Fake(s.T(), faker, database.Template{})
Expand Down Expand Up @@ -7880,88 +7878,3 @@ func TestAsExternalAuthChecker(t *testing.T) {
}
})
}

// TestSessionCountAppFamiliesRequired ensures the queries that take the app
// family registry fail loudly when it is empty, so a forgotten parameter
// surfaces as an error instead of silently dropping every family's sessions
// from usage reporting.
func TestSessionCountAppFamiliesRequired(t *testing.T) {
t.Parallel()

ctrl := gomock.NewController(t)
defer ctrl.Finish()
dbm := dbmock.NewMockStore(ctrl)
dbm.EXPECT().Wrappers().Return([]string{}).AnyTimes()
q := dbauthz.New(dbm, &coderdtest.RecordingAuthorizer{Wrapped: &coderdtest.FakeAuthorizer{}}, slog.Make(), coderdtest.AccessControlStorePointer())
ctx := dbauthz.As(context.Background(), coderdtest.RandomRBACSubject())

_, err := q.GetTemplateInsightsByTemplate(ctx, database.GetTemplateInsightsByTemplateParams{})
require.ErrorContains(t, err, "developer error")
err = q.UpsertTemplateUsageStats(ctx, nil)
require.ErrorContains(t, err, "developer error")
}

// TestSessionCountAppFamiliesMustMatchQueries covers registries that are
// present but wrong. Each query hardcodes one probe per family, so a registry
// whose keys drifted from codersdk.AttributedAppFamilies would report zero
// for the affected family instead of failing.
func TestSessionCountAppFamiliesMustMatchQueries(t *testing.T) {
t.Parallel()

valid := map[codersdk.AppFamilyName][]string{}
for _, family := range codersdk.AttributedAppFamilies() {
valid[family] = []string{string(family)}
}
without := func(drop codersdk.AppFamilyName) json.RawMessage {
families := maps.Clone(valid)
delete(families, drop)
return mustMarshalAppFamilies(t, families)
}

for _, tc := range []struct {
name string
appFamilies json.RawMessage
errContains string
}{
{"EmptyObject", json.RawMessage(`{}`), `missing family "vscode"`},
{"JSONNull", json.RawMessage(`null`), `missing family "vscode"`},
{"NotAnObject", json.RawMessage(`["vscode"]`), "must be a JSON object"},
{"MissingFamily", without(codersdk.AppFamilySSH), `missing family "ssh"`},
{"EmptyAppNames", mustMarshalAppFamilies(t, map[codersdk.AppFamilyName][]string{
codersdk.AppFamilyVSCode: {"vscode"},
codersdk.AppFamilyJetBrains: {"jetbrains"},
codersdk.AppFamilySSH: {},
codersdk.AppFamilyReconnectingPTY: {"reconnecting_pty"},
}), `no app names for family "ssh"`},
{"UnknownFamily", mustMarshalAppFamilies(t, map[codersdk.AppFamilyName][]string{
codersdk.AppFamilyVSCode: {"vscode"},
codersdk.AppFamilyJetBrains: {"jetbrains"},
codersdk.AppFamilySSH: {"ssh"},
codersdk.AppFamilyReconnectingPTY: {"reconnecting_pty"},
"emacs": {"emacs"},
}), `has family "emacs"`},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

ctrl := gomock.NewController(t)
defer ctrl.Finish()
dbm := dbmock.NewMockStore(ctrl)
dbm.EXPECT().Wrappers().Return([]string{}).AnyTimes()
q := dbauthz.New(dbm, &coderdtest.RecordingAuthorizer{Wrapped: &coderdtest.FakeAuthorizer{}}, slog.Make(), coderdtest.AccessControlStorePointer())
ctx := dbauthz.As(context.Background(), coderdtest.RandomRBACSubject())

_, err := q.GetTemplateInsightsByTemplate(ctx, database.GetTemplateInsightsByTemplateParams{AppFamilies: tc.appFamilies})
require.ErrorContains(t, err, tc.errContains)
err = q.UpsertTemplateUsageStats(ctx, tc.appFamilies)
require.ErrorContains(t, err, tc.errContains)
})
}
}

func mustMarshalAppFamilies(t *testing.T, families map[codersdk.AppFamilyName][]string) json.RawMessage {
t.Helper()
raw, err := json.Marshal(families)
require.NoError(t, err)
return raw
}
76 changes: 0 additions & 76 deletions coderd/database/dbauthz/sessioncountparams.go

This file was deleted.

5 changes: 2 additions & 3 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions coderd/database/dbrollup/dbrollup.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/codersdk"
)

const (
Expand Down Expand Up @@ -107,7 +106,7 @@ func (r *Rolluper) start(ctx context.Context) {
}

ev.TemplateUsageStats = true
return tx.UpsertTemplateUsageStats(ctx, codersdk.SessionCountAppFamiliesJSON())
return tx.UpsertTemplateUsageStats(ctx)
}, database.DefaultTXOptions().WithID("db_rollup"))
})

Expand Down
27 changes: 16 additions & 11 deletions coderd/database/dbrollup/dbrollup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package dbrollup_test
import (
"context"
"database/sql"
"encoding/json"
"testing"
"time"

Expand Down Expand Up @@ -44,9 +43,9 @@ func (w *wrapUpsertDB) InTx(fn func(database.Store) error, opts *database.TxOpti
}, opts)
}

func (w *wrapUpsertDB) UpsertTemplateUsageStats(ctx context.Context, appFamilies json.RawMessage) error {
func (w *wrapUpsertDB) UpsertTemplateUsageStats(ctx context.Context) error {
<-w.resume
return w.Store.UpsertTemplateUsageStats(ctx, appFamilies)
return w.Store.UpsertTemplateUsageStats(ctx)
}

func TestRollup_TwoInstancesUseLocking(t *testing.T) {
Expand Down Expand Up @@ -136,7 +135,7 @@ func TestRollup_TwoInstancesUseLocking(t *testing.T) {
func TestRollupTemplateUsageStats(t *testing.T) {
t.Parallel()

db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
db, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t, dbtestutil.WithDumpOnFailure())
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)

anHourAgo := dbtime.Now().Add(-time.Hour).Truncate(time.Hour).UTC()
Expand Down Expand Up @@ -245,15 +244,21 @@ func TestRollupTemplateUsageStats(t *testing.T) {
stats[0].StartTime = stats[0].StartTime.UTC()

require.Equal(t, database.TemplateUsageStat{
TemplateID: tpl.ID,
UserID: user.ID,
StartTime: wags1.CreatedAt,
EndTime: wags1.CreatedAt.Add(30 * time.Minute),
MedianLatencyMs: sql.NullFloat64{Float64: 1, Valid: true},
UsageMins: 3,
ReconnectingPtyMins: 2,
TemplateID: tpl.ID,
UserID: user.ID,
StartTime: wags1.CreatedAt,
EndTime: wags1.CreatedAt.Add(30 * time.Minute),
MedianLatencyMs: sql.NullFloat64{Float64: 1, Valid: true},
UsageMins: 3,
AppUsageMins: database.StringMapOfInt{
app.Slug: 2,
},
}, stats[0])

// Session minutes live in the child table, keyed by app name.
var usageMins int64
err = sqlDB.QueryRowContext(ctx, "SELECT usage_mins FROM template_usage_stats_session_apps WHERE start_time = $1 AND template_id = $2 AND user_id = $3 AND app_name = $4",
wags1.CreatedAt, tpl.ID, user.ID, "reconnecting_pty").Scan(&usageMins)
require.NoError(t, err)
require.EqualValues(t, 2, usageMins)
}
Loading
Loading